go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/delegation/rpc_mint_delegation_token.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package delegation 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 "time" 22 23 "go.opentelemetry.io/otel/trace" 24 "google.golang.org/grpc/codes" 25 "google.golang.org/grpc/status" 26 "google.golang.org/protobuf/encoding/protojson" 27 28 "go.chromium.org/luci/auth/identity" 29 "go.chromium.org/luci/common/clock" 30 "go.chromium.org/luci/common/logging" 31 "go.chromium.org/luci/common/retry/transient" 32 "go.chromium.org/luci/server/auth" 33 "go.chromium.org/luci/server/auth/authdb" 34 "go.chromium.org/luci/server/auth/delegation/messages" 35 "go.chromium.org/luci/server/auth/signing" 36 37 "go.chromium.org/luci/tokenserver/api/admin/v1" 38 "go.chromium.org/luci/tokenserver/api/minter/v1" 39 40 "go.chromium.org/luci/tokenserver/appengine/impl/utils" 41 "go.chromium.org/luci/tokenserver/appengine/impl/utils/identityset" 42 "go.chromium.org/luci/tokenserver/appengine/impl/utils/revocation" 43 ) 44 45 // tokenIDSequenceKind defines the namespace of int64 IDs for delegation tokens. 46 // 47 // Changing it will effectively reset the ID generation. 48 const tokenIDSequenceKind = "delegationTokenID" 49 50 // MintDelegationTokenRPC implements TokenMinter.MintDelegationToken RPC method. 51 type MintDelegationTokenRPC struct { 52 // Signer is mocked in tests. 53 // 54 // In prod it is the default server signer that uses server's service account. 55 Signer signing.Signer 56 57 // Rules returns delegation rules to use for the request. 58 // 59 // In prod it is GlobalRulesCache.Rules. 60 Rules func(context.Context) (*Rules, error) 61 62 // LogToken is mocked in tests. 63 // 64 // In prod it is produced by NewTokenLogger. 65 LogToken TokenLogger 66 67 // mintMock call is used in tests. 68 // 69 // In prod it is 'mint' 70 mintMock func(context.Context, *mintParams) (*minter.MintDelegationTokenResponse, error) 71 } 72 73 // MintDelegationToken generates a new bearer delegation token. 74 func (r *MintDelegationTokenRPC) MintDelegationToken(c context.Context, req *minter.MintDelegationTokenRequest) (*minter.MintDelegationTokenResponse, error) { 75 state := auth.GetState(c) 76 77 // Dump the whole request and relevant auth state to the debug log. 78 if logging.IsLogging(c, logging.Debug) { 79 opts := protojson.MarshalOptions{Indent: " "} 80 logging.Debugf(c, "PeerIdentity: %s", state.PeerIdentity()) 81 logging.Debugf(c, "MintDelegationTokenRequest:\n%s", opts.Format(req)) 82 } 83 84 // Validate the request authentication context: not an anonymous call, no 85 // delegation is used. 86 callerID := state.User().Identity 87 if callerID != state.PeerIdentity() { 88 logging.Errorf(c, "Trying to use delegation, it's forbidden") 89 return nil, status.Errorf(codes.PermissionDenied, "delegation is forbidden for this API call") 90 } 91 if callerID == identity.AnonymousIdentity { 92 logging.Errorf(c, "Unauthenticated request") 93 return nil, status.Errorf(codes.Unauthenticated, "authentication required") 94 } 95 96 // Grab a string that identifies token server version. This almost always 97 // just hits local memory cache. 98 serviceVer, err := utils.ServiceVersion(c, r.Signer) 99 if err != nil { 100 return nil, status.Errorf(codes.Internal, "can't grab service version - %s", err) 101 } 102 103 rules, err := r.Rules(c) 104 if err != nil { 105 // Don't put error details in the message, it may be returned to 106 // unauthorized callers. 107 logging.WithError(err).Errorf(c, "Failed to load delegation rules") 108 return nil, status.Errorf(codes.Internal, "failed to load delegation rules") 109 } 110 111 // Make sure the caller is mentioned in the config before doing anything else. 112 // This rejects unauthorized callers early. Passing this check doesn't mean 113 // that there's a matching rule though, so the request still can be rejected 114 // later. 115 switch ok, err := rules.IsAuthorizedRequestor(c, callerID); { 116 case err != nil: 117 logging.WithError(err).Errorf(c, "IsAuthorizedRequestor failed") 118 return nil, status.Errorf(codes.Internal, "failed to check authorization") 119 case !ok: 120 logging.Errorf(c, "Didn't pass initial authorization") 121 return nil, status.Errorf(codes.PermissionDenied, "not authorized") 122 } 123 124 // Validate requested token lifetime. It's not part of the rules query. 125 if req.ValidityDuration == 0 { 126 req.ValidityDuration = 3600 127 } 128 if req.ValidityDuration < 0 { 129 err = fmt.Errorf("invalid 'validity_duration' (%d)", req.ValidityDuration) 130 logging.WithError(err).Errorf(c, "Bad request") 131 return nil, status.Errorf(codes.InvalidArgument, "bad request - %s", err) 132 } 133 134 // Same for tags, they are transferred intact to the final token. 135 if err := utils.ValidateTags(req.Tags); err != nil { 136 err = fmt.Errorf("invalid 'tags': %s", err) 137 logging.WithError(err).Errorf(c, "Bad request") 138 return nil, status.Errorf(codes.InvalidArgument, "bad request - %s", err) 139 } 140 141 // Validate and normalize the request. This may do relatively expensive calls 142 // to resolve "https://<service-url>" entries to "service:<id>" entries. 143 query, err := buildRulesQuery(c, req, callerID) 144 if err != nil { 145 if transient.Tag.In(err) { 146 logging.WithError(err).Errorf(c, "buildRulesQuery failed") 147 return nil, status.Errorf(codes.Internal, "failure when resolving target service ID - %s", err) 148 } 149 logging.WithError(err).Errorf(c, "Bad request") 150 return nil, status.Errorf(codes.InvalidArgument, "bad request - %s", err) 151 } 152 153 // Consult the config to find the rule that allows this operation (if any). 154 rule, err := rules.FindMatchingRule(c, query) 155 if err != nil { 156 if transient.Tag.In(err) { 157 logging.WithError(err).Errorf(c, "FindMatchingRule failed") 158 return nil, status.Errorf(codes.Internal, "failure when checking rules - %s", err) 159 } 160 logging.WithError(err).Errorf(c, "Didn't pass rules check") 161 return nil, status.Errorf(codes.PermissionDenied, "forbidden - %s", err) 162 } 163 logging.Infof(c, "Found the matching rule %q in the config rev %s", rule.Name, rules.ConfigRevision()) 164 165 // Make sure the requested token lifetime is allowed by the rule. 166 if req.ValidityDuration > rule.MaxValidityDuration { 167 err = fmt.Errorf( 168 "the requested validity duration (%d sec) exceeds the maximum allowed one (%d sec)", 169 req.ValidityDuration, rule.MaxValidityDuration) 170 logging.WithError(err).Errorf(c, "Validity duration check didn't pass") 171 return nil, status.Errorf(codes.PermissionDenied, "forbidden - %s", err) 172 } 173 174 var resp *minter.MintDelegationTokenResponse 175 p := mintParams{ 176 request: req, 177 query: query, 178 rule: rule, 179 serviceVer: serviceVer, 180 } 181 if r.mintMock != nil { 182 resp, err = r.mintMock(c, &p) 183 } else { 184 resp, err = r.mint(c, &p) 185 } 186 if err != nil { 187 return nil, err 188 } 189 190 if r.LogToken != nil { 191 // Errors during logging are considered not fatal. We have a monitoring 192 // counter that tracks number of errors, so they are not totally invisible. 193 tokInfo := MintedTokenInfo{ 194 Request: req, 195 Response: resp, 196 ConfigRev: rules.ConfigRevision(), 197 Rule: rule, 198 PeerIP: state.PeerIP(), 199 RequestID: trace.SpanContextFromContext(c).TraceID().String(), 200 AuthDBRev: authdb.Revision(state.DB()), 201 } 202 if logErr := r.LogToken(c, &tokInfo); logErr != nil { 203 logging.WithError(logErr).Errorf(c, "Failed to insert the delegation token into the BigQuery log") 204 } 205 } 206 207 return resp, nil 208 } 209 210 // mintParams are passed to 'mint' function. 211 type mintParams struct { 212 request *minter.MintDelegationTokenRequest // the original RPC request 213 query *RulesQuery // extracted from the request 214 rule *admin.DelegationRule // looked up in the config based on 'query' 215 serviceVer string // version string to put in the response 216 } 217 218 // mint is called to make the token after the request has been authorized. 219 func (r *MintDelegationTokenRPC) mint(c context.Context, p *mintParams) (*minter.MintDelegationTokenResponse, error) { 220 id, err := revocation.GenerateTokenID(c, tokenIDSequenceKind) 221 if err != nil { 222 logging.WithError(err).Errorf(c, "Error when generating token ID.") 223 return nil, status.Errorf(codes.Internal, "error when generating token ID - %s", err) 224 } 225 226 // All the stuff here has already been validated in 'MintDelegationToken'. 227 subtok := &messages.Subtoken{ 228 Kind: messages.Subtoken_BEARER_DELEGATION_TOKEN, 229 SubtokenId: id, 230 DelegatedIdentity: string(p.query.Delegator), 231 RequestorIdentity: string(p.query.Requestor), 232 CreationTime: clock.Now(c).Unix(), 233 ValidityDuration: int32(p.request.ValidityDuration), 234 Audience: p.query.Audience.ToStrings(), 235 Services: p.query.Services.ToStrings(), 236 Tags: p.request.Tags, 237 } 238 239 signed, err := SignToken(c, r.Signer, subtok) 240 if err != nil { 241 logging.WithError(err).Errorf(c, "Error when signing the token.") 242 return nil, status.Errorf(codes.Internal, "error when signing the token - %s", err) 243 } 244 245 return &minter.MintDelegationTokenResponse{ 246 Token: signed, 247 DelegationSubtoken: subtok, 248 ServiceVersion: p.serviceVer, 249 }, nil 250 } 251 252 // buildRulesQuery validates the request, extracts and normalizes relevant 253 // fields into RulesQuery object. 254 // 255 // May return transient errors. 256 func buildRulesQuery(c context.Context, req *minter.MintDelegationTokenRequest, requestor identity.Identity) (*RulesQuery, error) { 257 // Validate 'delegated_identity'. 258 var err error 259 var delegator identity.Identity 260 if req.DelegatedIdentity == "" { 261 return nil, fmt.Errorf("'delegated_identity' is required") 262 } 263 if req.DelegatedIdentity == Requestor { 264 delegator = requestor // the requestor is delegating its own identity 265 } else { 266 if delegator, err = identity.MakeIdentity(req.DelegatedIdentity); err != nil { 267 return nil, fmt.Errorf("bad 'delegated_identity' - %s", err) 268 } 269 } 270 271 // Validate 'audience', convert it into a set. 272 if len(req.Audience) == 0 { 273 return nil, fmt.Errorf("'audience' is required") 274 } 275 audienceSet, err := identityset.FromStrings(req.Audience, skipRequestor) 276 if err != nil { 277 return nil, fmt.Errorf("bad 'audience' - %s", err) 278 } 279 if sliceHasString(req.Audience, Requestor) { 280 audienceSet.AddIdentity(requestor) 281 } 282 283 // Split 'services' into two lists: URLs and everything else (which is 284 // "service:..." and "*" presumably, validated below). 285 if len(req.Services) == 0 { 286 return nil, fmt.Errorf("'services' is required") 287 } 288 urls := make([]string, 0, len(req.Services)) 289 rest := make([]string, 0, len(req.Services)) 290 for _, srv := range req.Services { 291 if strings.HasPrefix(srv, "https://") { 292 urls = append(urls, srv) 293 } else { 294 rest = append(rest, srv) 295 } 296 } 297 298 // Convert the list into a set, verify it contains only services (or "*"). 299 servicesSet, err := identityset.FromStrings(rest, nil) 300 if err != nil { 301 return nil, fmt.Errorf("bad 'services' - %s", err) 302 } 303 if len(servicesSet.Groups) != 0 { 304 return nil, fmt.Errorf("bad 'services' - can't specify groups") 305 } 306 for ident := range servicesSet.IDs { 307 if ident.Kind() != identity.Service { 308 return nil, fmt.Errorf("bad 'services' - %q is not a service ID", ident) 309 } 310 } 311 312 // Resolve URLs into app IDs. This may involve URL fetch calls (if the cache 313 // is cold), so skip this expensive call if already specifying the universal 314 // set of all services. 315 if !servicesSet.All && len(urls) != 0 { 316 if err = resolveServiceIDs(c, urls, servicesSet); err != nil { 317 return nil, err 318 } 319 } 320 321 // Done! 322 return &RulesQuery{ 323 Requestor: requestor, 324 Delegator: delegator, 325 Audience: audienceSet, 326 Services: servicesSet, 327 }, nil 328 } 329 330 // fetchLUCIServiceIdentity is replaced in tests. 331 var fetchLUCIServiceIdentity = signing.FetchLUCIServiceIdentity 332 333 // resolveServiceIDs takes a bunch of service URLs and resolves them to 334 // 'service:<app-id>' identities, putting them in the 'out' set. 335 // 336 // May return transient errors. 337 func resolveServiceIDs(c context.Context, urls []string, out *identityset.Set) error { 338 // URL fetch calls below should be extra fast. If they get stuck, something is 339 // horribly wrong, better to abort soon. 340 c, cancel := clock.WithTimeout(c, 5*time.Second) 341 defer cancel() 342 343 type Result struct { 344 URL string 345 ID identity.Identity 346 Err error 347 } 348 349 ch := make(chan Result, len(urls)) 350 351 for _, url := range urls { 352 go func(url string) { 353 id, err := fetchLUCIServiceIdentity(c, url) 354 ch <- Result{url, id, err} 355 }(url) 356 } 357 358 for i := 0; i < len(urls); i++ { 359 result := <-ch 360 if result.Err != nil { 361 if transient.Tag.In(result.Err) { 362 return result.Err 363 } 364 return fmt.Errorf("could not resolve %q to service ID - %s", result.URL, result.Err) 365 } 366 out.AddIdentity(result.ID) 367 } 368 369 return nil 370 }