| OLD | NEW |
| 1 // Copyright 2016 The LUCI Authors. All rights reserved. | 1 // Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 | 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. | 3 // that can be found in the LICENSE file. |
| 4 | 4 |
| 5 package delegation | 5 package delegation |
| 6 | 6 |
| 7 import ( | 7 import ( |
| 8 "fmt" |
| 9 "strings" |
| 10 "time" |
| 11 |
| 12 "github.com/golang/protobuf/jsonpb" |
| 8 "golang.org/x/net/context" | 13 "golang.org/x/net/context" |
| 9 "google.golang.org/grpc" | 14 "google.golang.org/grpc" |
| 10 "google.golang.org/grpc/codes" | 15 "google.golang.org/grpc/codes" |
| 11 | 16 |
| 17 "github.com/luci/luci-go/common/clock" |
| 18 "github.com/luci/luci-go/common/errors" |
| 19 "github.com/luci/luci-go/common/logging" |
| 20 "github.com/luci/luci-go/server/auth" |
| 21 "github.com/luci/luci-go/server/auth/identity" |
| 12 "github.com/luci/luci-go/server/auth/signing" | 22 "github.com/luci/luci-go/server/auth/signing" |
| 13 » minter "github.com/luci/luci-go/tokenserver/api/minter/v1" | 23 |
| 24 » admin "github.com/luci/luci-go/tokenserver/api/admin/v1" |
| 25 » "github.com/luci/luci-go/tokenserver/api/minter/v1" |
| 26 » "github.com/luci/luci-go/tokenserver/appengine/utils/identityset" |
| 14 ) | 27 ) |
| 15 | 28 |
| 16 // MintDelegationTokenRPC implements TokenMinter.MintDelegationToken RPC method. | 29 // MintDelegationTokenRPC implements TokenMinter.MintDelegationToken RPC method. |
| 17 type MintDelegationTokenRPC struct { | 30 type MintDelegationTokenRPC struct { |
| 18 // Signer is mocked in tests. | 31 // Signer is mocked in tests. |
| 19 // | 32 // |
| 20 // In prod it is gaesigner.Signer. | 33 // In prod it is gaesigner.Signer. |
| 21 Signer signing.Signer | 34 Signer signing.Signer |
| 35 |
| 36 // ConfigLoader loads delegation config on demand. |
| 37 // |
| 38 // In prod it is DelegationConfigLoader. |
| 39 ConfigLoader func(context.Context) (*DelegationConfig, error) |
| 40 |
| 41 // mintMock call is used in tests. |
| 42 // |
| 43 // In prod it is 'mint' |
| 44 mintMock func(context.Context, *minter.MintDelegationTokenRequest, |
| 45 *RulesQuery, *admin.DelegationRule) (*minter.MintDelegationToken
Response, error) |
| 22 } | 46 } |
| 23 | 47 |
| 24 // MintDelegationToken generates a new bearer delegation token. | 48 // MintDelegationToken generates a new bearer delegation token. |
| 25 func (r *MintDelegationTokenRPC) MintDelegationToken(c context.Context, req *min
ter.MintDelegationTokenRequest) (*minter.MintDelegationTokenResponse, error) { | 49 func (r *MintDelegationTokenRPC) MintDelegationToken(c context.Context, req *min
ter.MintDelegationTokenRequest) (*minter.MintDelegationTokenResponse, error) { |
| 50 state := auth.GetState(c) |
| 51 |
| 52 // Dump the whole request and relevant auth state to the debug log. |
| 53 if logging.IsLogging(c, logging.Debug) { |
| 54 m := jsonpb.Marshaler{Indent: " "} |
| 55 dump, _ := m.MarshalToString(req) |
| 56 logging.Debugf(c, "PeerIdentity: %s", state.PeerIdentity()) |
| 57 logging.Debugf(c, "MintDelegationTokenRequest:\n%s", dump) |
| 58 } |
| 59 |
| 60 // Validate the request authentication context: not an anonymous call, n
o |
| 61 // delegation is used. |
| 62 callerID := state.User().Identity |
| 63 if callerID != state.PeerIdentity() { |
| 64 logging.Errorf(c, "Trying to use delegation, it's forbidden") |
| 65 return nil, grpc.Errorf(codes.PermissionDenied, "delegation is f
orbidden for this API call") |
| 66 } |
| 67 if callerID == identity.AnonymousIdentity { |
| 68 logging.Errorf(c, "Unauthenticated request") |
| 69 return nil, grpc.Errorf(codes.Unauthenticated, "authentication r
equired") |
| 70 } |
| 71 |
| 72 cfg, err := r.ConfigLoader(c) |
| 73 if err != nil { |
| 74 // Don't put error details in the message, it may be returned to |
| 75 // unauthorized callers. ConfigLoader logs the error already. |
| 76 return nil, grpc.Errorf(codes.Internal, "failed to load delegati
on config") |
| 77 } |
| 78 |
| 79 // Make sure the caller is mentioned in the config before doing anything
else. |
| 80 // This rejects unauthorized callers early. Passing this check doesn't m
ean |
| 81 // that there's a matching rule though, so the request still can be reje
cted |
| 82 // later. |
| 83 switch ok, err := cfg.IsAuthorizedRequestor(c, callerID); { |
| 84 case err != nil: |
| 85 logging.WithError(err).Errorf(c, "IsAuthorizedRequestor failed") |
| 86 return nil, grpc.Errorf(codes.Internal, "failed to check authori
zation") |
| 87 case !ok: |
| 88 logging.Errorf(c, "Didn't pass initial authorization") |
| 89 return nil, grpc.Errorf(codes.PermissionDenied, "not authorized"
) |
| 90 } |
| 91 |
| 92 // Validate requested token lifetime. It's not part of the rules query. |
| 93 if req.ValidityDuration == 0 { |
| 94 req.ValidityDuration = 3600 |
| 95 } |
| 96 if req.ValidityDuration < 0 { |
| 97 err = fmt.Errorf("invalid 'validity_duration' (%d)", req.Validit
yDuration) |
| 98 logging.WithError(err).Errorf(c, "Bad request") |
| 99 return nil, grpc.Errorf(codes.InvalidArgument, "bad request - %s
", err) |
| 100 } |
| 101 |
| 102 // Validate and normalize the request. This may do relatively expensive
calls |
| 103 // to resolve "https://<service-url>" entries to "service:<id>" entries. |
| 104 query, err := buildRulesQuery(c, req, callerID) |
| 105 if err != nil { |
| 106 if errors.IsTransient(err) { |
| 107 logging.WithError(err).Errorf(c, "buildRulesQuery failed
") |
| 108 return nil, grpc.Errorf(codes.Internal, "failure when re
solving target service ID - %s", err) |
| 109 } |
| 110 logging.WithError(err).Errorf(c, "Bad request") |
| 111 return nil, grpc.Errorf(codes.InvalidArgument, "bad request - %s
", err) |
| 112 } |
| 113 |
| 114 // Consult the config to find the rule that allows this operation (if an
y). |
| 115 rule, err := cfg.FindMatchingRule(c, query) |
| 116 if err != nil { |
| 117 if errors.IsTransient(err) { |
| 118 logging.WithError(err).Errorf(c, "FindMatchingRule faile
d") |
| 119 return nil, grpc.Errorf(codes.Internal, "failure when ch
ecking rules - %s", err) |
| 120 } |
| 121 logging.WithError(err).Errorf(c, "Didn't pass rules check") |
| 122 return nil, grpc.Errorf(codes.PermissionDenied, "forbidden - %s"
, err) |
| 123 } |
| 124 logging.Infof(c, "Found the matching rule %q in the config rev %s", rule
.Name, cfg.Revision) |
| 125 |
| 126 // Make sure the requested token lifetime is allowed by the rule. |
| 127 if req.ValidityDuration > rule.MaxValidityDuration { |
| 128 err = fmt.Errorf( |
| 129 "the requested validity duration (%d sec) exceeds the ma
ximum allowed one (%d sec)", |
| 130 req.ValidityDuration, rule.MaxValidityDuration) |
| 131 logging.WithError(err).Errorf(c, "Validity duration check didn't
pass") |
| 132 return nil, grpc.Errorf(codes.PermissionDenied, "forbidden - %s"
, err) |
| 133 } |
| 134 |
| 135 if r.mintMock != nil { |
| 136 return r.mintMock(c, req, query, rule) |
| 137 } |
| 138 return r.mint(c, req, query, rule) |
| 139 } |
| 140 |
| 141 // mint is called to make the token after the request has been authorized. |
| 142 func (r *MintDelegationTokenRPC) mint( |
| 143 c context.Context, req *minter.MintDelegationTokenRequest, |
| 144 query *RulesQuery, rule *admin.DelegationRule) (*minter.MintDelegationTo
kenResponse, error) { |
| 145 // TODO(vadimsh): Make the token, record it in the audit log. |
| 26 return nil, grpc.Errorf(codes.Unimplemented, "Not implemented yet") | 146 return nil, grpc.Errorf(codes.Unimplemented, "Not implemented yet") |
| 27 } | 147 } |
| 148 |
| 149 // buildRulesQuery validates the request, extracts and normalizes relevant |
| 150 // fields into RulesQuery object. |
| 151 // |
| 152 // May return transient errors. |
| 153 func buildRulesQuery(c context.Context, req *minter.MintDelegationTokenRequest,
requestor identity.Identity) (*RulesQuery, error) { |
| 154 // Validate 'delegated_identity'. |
| 155 var err error |
| 156 var delegatee identity.Identity |
| 157 if req.DelegatedIdentity == "" { |
| 158 return nil, fmt.Errorf("'delegated_identity' is required") |
| 159 } |
| 160 if req.DelegatedIdentity == Requestor { |
| 161 delegatee = requestor // the requestor is delegating its own ide
ntity |
| 162 } else { |
| 163 if delegatee, err = identity.MakeIdentity(req.DelegatedIdentity)
; err != nil { |
| 164 return nil, fmt.Errorf("bad 'delegated_identity' - %s",
err) |
| 165 } |
| 166 } |
| 167 |
| 168 // Validate 'audience', convert it into a set. |
| 169 if len(req.Audience) == 0 { |
| 170 return nil, fmt.Errorf("'audience' is required") |
| 171 } |
| 172 audienceSet, err := identityset.FromStrings(req.Audience, skipRequestor) |
| 173 if err != nil { |
| 174 return nil, fmt.Errorf("bad 'audience' - %s", err) |
| 175 } |
| 176 if sliceHasString(req.Audience, Requestor) { |
| 177 audienceSet.AddIdentity(requestor) |
| 178 } |
| 179 |
| 180 // Split 'services' into two lists: URLs and everything else (which is |
| 181 // "service:..." and "*" presumably, validated below). |
| 182 if len(req.Services) == 0 { |
| 183 return nil, fmt.Errorf("'services' is required") |
| 184 } |
| 185 urls := make([]string, 0, len(req.Services)) |
| 186 rest := make([]string, 0, len(req.Services)) |
| 187 for _, srv := range req.Services { |
| 188 if strings.HasPrefix(srv, "https://") { |
| 189 urls = append(urls, srv) |
| 190 } else { |
| 191 rest = append(rest, srv) |
| 192 } |
| 193 } |
| 194 |
| 195 // Convert the list into a set, verify it contains only services (or "*"
). |
| 196 servicesSet, err := identityset.FromStrings(rest, nil) |
| 197 if err != nil { |
| 198 return nil, fmt.Errorf("bad 'services' - %s", err) |
| 199 } |
| 200 if len(servicesSet.Groups) != 0 { |
| 201 return nil, fmt.Errorf("bad 'services' - can't specify groups") |
| 202 } |
| 203 for ident := range servicesSet.IDs { |
| 204 if ident.Kind() != identity.Service { |
| 205 return nil, fmt.Errorf("bad 'services' - %q is not a ser
vice ID", ident) |
| 206 } |
| 207 } |
| 208 |
| 209 // Resolve URLs into app IDs. This may involve URL fetch calls (if the c
ache |
| 210 // is cold), so skip this expensive call if already specifying the unive
rsal |
| 211 // set of all services. |
| 212 if !servicesSet.All && len(urls) != 0 { |
| 213 if err = resolveServiceIDs(c, urls, servicesSet); err != nil { |
| 214 return nil, err |
| 215 } |
| 216 } |
| 217 |
| 218 // Done! |
| 219 return &RulesQuery{ |
| 220 Requestor: requestor, |
| 221 Delegatee: delegatee, |
| 222 Audience: audienceSet, |
| 223 Services: servicesSet, |
| 224 }, nil |
| 225 } |
| 226 |
| 227 // fetchLUCIServiceIdentity is replaced in tests. |
| 228 var fetchLUCIServiceIdentity = signing.FetchLUCIServiceIdentity |
| 229 |
| 230 // resolveServiceIDs takes a bunch of service URLs and resolves them to |
| 231 // 'service:<app-id>' identities, putting them in the 'out' set. |
| 232 // |
| 233 // May return transient errors. |
| 234 func resolveServiceIDs(c context.Context, urls []string, out *identityset.Set) e
rror { |
| 235 // URL fetch calls below should be extra fast. If they get stuck, someth
ing is |
| 236 // horribly wrong, better to abort soon. |
| 237 c, abort := clock.WithTimeout(c, 5*time.Second) |
| 238 defer abort() |
| 239 |
| 240 type Result struct { |
| 241 URL string |
| 242 ID identity.Identity |
| 243 Err error |
| 244 } |
| 245 |
| 246 ch := make(chan Result, len(urls)) |
| 247 |
| 248 for _, url := range urls { |
| 249 go func(url string) { |
| 250 id, err := fetchLUCIServiceIdentity(c, url) |
| 251 ch <- Result{url, id, err} |
| 252 }(url) |
| 253 } |
| 254 |
| 255 for i := 0; i < len(urls); i++ { |
| 256 result := <-ch |
| 257 if result.Err != nil { |
| 258 if errors.IsTransient(result.Err) { |
| 259 return result.Err |
| 260 } |
| 261 return fmt.Errorf("could not resolve %q to service ID -
%s", result.URL, result.Err) |
| 262 } |
| 263 out.AddIdentity(result.ID) |
| 264 } |
| 265 |
| 266 return nil |
| 267 } |
| OLD | NEW |