go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/actor_access_tokens.go (about) 1 // Copyright 2017 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 auth 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "strings" 22 "time" 23 24 "go.opentelemetry.io/otel/attribute" 25 26 "go.chromium.org/luci/common/clock" 27 "go.chromium.org/luci/common/logging" 28 "go.chromium.org/luci/common/retry/transient" 29 30 "go.chromium.org/luci/server/auth/internal/tracing" 31 ) 32 33 // MintAccessTokenParams is passed to MintAccessTokenForServiceAccount. 34 type MintAccessTokenParams struct { 35 // ServiceAccount is an email of a service account to mint a token for. 36 ServiceAccount string 37 38 // Scopes is a list of OAuth scopes the token should have. 39 Scopes []string 40 41 // Delegates is a the sequence of service accounts in a delegation chain. 42 // 43 // Each service account must be granted the "iam.serviceAccountTokenCreator" 44 // role on its next service account in the chain. The last service account in 45 // the chain must be granted the "iam.serviceAccountTokenCreator" role on 46 // the service account specified by ServiceAccount field. 47 Delegates []string 48 49 // MinTTL defines an acceptable token lifetime. 50 // 51 // The returned token will be valid for at least MinTTL, but no longer than 52 // one hour. 53 // 54 // Default is 2 min. 55 MinTTL time.Duration 56 } 57 58 // actorAccessTokenCache is used to store access tokens of service accounts 59 // the current service has "iam.serviceAccountTokenCreator" role in. 60 // 61 // The token is stored in OAuth2Token field. 62 var actorAccessTokenCache = newTokenCache(tokenCacheConfig{ 63 Kind: "as_actor_access_tok", 64 Version: 1, 65 ProcessCacheCapacity: 8192, 66 ExpiryRandomizationThreshold: 5 * time.Minute, // ~10% of regular 1h expiration 67 }) 68 69 // MintAccessTokenForServiceAccount produces an access token for some service 70 // account that the current service has "iam.serviceAccountTokenCreator" role 71 // in. 72 // 73 // Used to implement AsActor authorization kind, but can also be used directly, 74 // if needed. The token is cached internally. Same token may be returned by 75 // multiple calls, if its lifetime allows. 76 // 77 // Recognizes transient errors and marks them, but does not automatically 78 // retry. Has internal timeout of 10 sec. 79 func MintAccessTokenForServiceAccount(ctx context.Context, params MintAccessTokenParams) (_ *Token, err error) { 80 ctx, span := tracing.Start(ctx, "go.chromium.org/luci/server/auth.MintAccessTokenForServiceAccount", 81 attribute.String("cr.dev.account", params.ServiceAccount), 82 ) 83 defer func() { tracing.End(span, err) }() 84 85 report := durationReporter(ctx, mintAccessTokenDuration) 86 87 cfg := getConfig(ctx) 88 if cfg == nil || cfg.AccessTokenProvider == nil { 89 report(ErrNotConfigured, "ERROR_NOT_CONFIGURED") 90 return nil, ErrNotConfigured 91 } 92 93 if params.ServiceAccount == "" || len(params.Scopes) == 0 { 94 err := fmt.Errorf("invalid parameters") 95 report(err, "ERROR_BAD_ARGUMENTS") 96 return nil, err 97 } 98 99 if params.MinTTL == 0 { 100 params.MinTTL = 2 * time.Minute 101 } 102 103 sortedScopes := append([]string(nil), params.Scopes...) 104 sort.Strings(sortedScopes) 105 106 // Construct the cache key. Note that it is hashed by 'actorAccessTokenCache' 107 // and thus can be as long as necessary. 108 cacheKey := cacheKeyBuilder{} 109 if err := cacheKey.add("service account", params.ServiceAccount); err != nil { 110 report(err, "ERROR_BAD_ARGUMENTS") 111 return nil, err 112 } 113 for _, scope := range sortedScopes { 114 if err := cacheKey.add("scope", scope); err != nil { 115 report(err, "ERROR_BAD_ARGUMENTS") 116 return nil, err 117 } 118 } 119 for _, delegate := range params.Delegates { 120 if err := cacheKey.add("delegate", delegate); err != nil { 121 report(err, "ERROR_BAD_ARGUMENTS") 122 return nil, err 123 } 124 } 125 126 ctx = logging.SetFields(ctx, logging.Fields{ 127 "token": "actor", 128 "account": params.ServiceAccount, 129 "scopes": strings.Join(sortedScopes, " "), 130 "delegates": strings.Join(params.Delegates, ":"), 131 }) 132 133 cached, err, label := actorAccessTokenCache.fetchOrMintToken(ctx, &fetchOrMintTokenOp{ 134 CacheKey: cacheKey.finish(), 135 MinTTL: params.MinTTL, 136 MintTimeout: cfg.adjustedTimeout(10 * time.Second), 137 138 // Mint is called on cache miss, under the lock. 139 Mint: func(ctx context.Context) (t *cachedToken, err error, label string) { 140 tok, err := cfg.actorTokensProvider().GenerateAccessToken(ctx, params.ServiceAccount, sortedScopes, params.Delegates) 141 if err != nil { 142 if transient.Tag.In(err) { 143 return nil, err, "ERROR_TRANSIENT_IN_MINTING" 144 } 145 return nil, err, "ERROR_FATAL_IN_MINTING" 146 } 147 148 now := clock.Now(ctx).UTC() 149 logging.Fields{ 150 "fingerprint": tokenFingerprint(tok.AccessToken), 151 "validity": tok.Expiry.Sub(now), 152 }.Debugf(ctx, "Minted new actor OAuth token") 153 154 return &cachedToken{ 155 Created: now, 156 Expiry: tok.Expiry.UTC(), 157 OAuth2Token: tok.AccessToken, 158 }, nil, "SUCCESS_CACHE_MISS" 159 }, 160 }) 161 162 report(err, label) 163 if err != nil { 164 return nil, err 165 } 166 return &Token{ 167 Token: cached.OAuth2Token, 168 Expiry: cached.Expiry, 169 }, nil 170 }