go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/delegation.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 auth 16 17 import ( 18 "context" 19 "fmt" 20 "net/http" 21 "sort" 22 "strings" 23 "time" 24 25 "go.opentelemetry.io/otel/attribute" 26 "google.golang.org/grpc" 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" 32 "go.chromium.org/luci/common/retry/transient" 33 "go.chromium.org/luci/grpc/grpcutil" 34 "go.chromium.org/luci/grpc/prpc" 35 "go.chromium.org/luci/tokenserver/api/minter/v1" 36 37 "go.chromium.org/luci/server/auth/delegation/messages" 38 "go.chromium.org/luci/server/auth/internal/tracing" 39 ) 40 41 var ( 42 // ErrTokenServiceNotConfigured is returned by MintDelegationToken if the 43 // token service URL is not configured. This usually means the corresponding 44 // auth service is not paired with a token server. 45 ErrTokenServiceNotConfigured = fmt.Errorf("auth: token service URL is not configured") 46 47 // ErrBrokenTokenService is returned by MintDelegationToken if the RPC to the 48 // token service succeeded, but response doesn't make sense. This should not 49 // generally happen. 50 ErrBrokenTokenService = fmt.Errorf("auth: unrecognized response from the token service") 51 52 // ErrAnonymousDelegation is returned by MintDelegationToken if it is used in 53 // a context of handling of an anonymous call. 54 // 55 // There's no identity to delegate in this case. 56 ErrAnonymousDelegation = fmt.Errorf("auth: can't get delegation token for anonymous user") 57 58 // ErrBadTargetHost is returned by MintDelegationToken if it receives invalid 59 // TargetHost parameter. 60 ErrBadTargetHost = fmt.Errorf("auth: invalid TargetHost (doesn't look like a hostname:port pair)") 61 62 // ErrBadTokenTTL is returned by MintDelegationToken and MintProjectToken if requested 63 // token lifetime is outside of the allowed range. 64 ErrBadTokenTTL = fmt.Errorf("auth: requested token TTL is invalid") 65 66 // ErrBadDelegationTag is returned by MintDelegationToken if some of the 67 // passed tags are malformed. 68 ErrBadDelegationTag = fmt.Errorf("auth: provided delegation tags are invalid") 69 ) 70 71 const ( 72 // MaxDelegationTokenTTL is maximum allowed token lifetime that can be 73 // requested via MintDelegationToken. 74 MaxDelegationTokenTTL = 3 * time.Hour 75 ) 76 77 // DelegationTokenParams is passed to MintDelegationToken. 78 type DelegationTokenParams struct { 79 // TargetHost, if given, is hostname (with, possibly, ":port") of a service 80 // that the token will be sent to. 81 // 82 // If this parameter is used, the resulting delegation token is scoped 83 // only to the service at TargetHost. All other services will reject it. 84 // 85 // Must be set if Untargeted is false. Ignored if Untargeted is true. 86 TargetHost string 87 88 // Untargeted, if true, indicates that the caller is requesting a token that 89 // is not scoped to any particular service. 90 // 91 // Such token can be sent to any supported LUCI service. Only allowlisted set 92 // of callers have such superpower. 93 // 94 // If Untargeted is true, TargetHost is ignored. 95 Untargeted bool 96 97 // MinTTL defines an acceptable token lifetime. 98 // 99 // The returned token will be valid for at least MinTTL, but no longer than 100 // MaxDelegationTokenTTL (which is 3h). 101 // 102 // Default is 10 min. 103 MinTTL time.Duration 104 105 // Intent is a reason why the token is created. 106 // 107 // Used only for logging purposes on the auth service, will be indexed. Should 108 // be a short identifier-like string. 109 // 110 // Optional. 111 Intent string 112 113 // Tags are optional arbitrary key:value pairs embedded into the token. 114 // 115 // They convey circumstance of why the token is created. 116 // 117 // Services that accept the token may use them for additional authorization 118 // decisions. Please use extremely carefully, only when you control both sides 119 // of the delegation link and can guarantee that services involved understand 120 // the tags. 121 Tags []string 122 123 // rpcClient is token server RPC client to use. 124 // 125 // Mocked in tests. 126 rpcClient delegationTokenMinterClient 127 } 128 129 // delegationTokenMinterClient is subset of minter.TokenMinterClient we use. 130 type delegationTokenMinterClient interface { 131 MintDelegationToken(context.Context, *minter.MintDelegationTokenRequest, ...grpc.CallOption) (*minter.MintDelegationTokenResponse, error) 132 } 133 134 // delegationTokenCache is used to store delegation tokens in the cache. 135 // 136 // The token is stored in DelegationToken field. 137 var delegationTokenCache = newTokenCache(tokenCacheConfig{ 138 Kind: "delegation", 139 Version: 7, 140 ProcessCacheCapacity: 8192, 141 ExpiryRandomizationThreshold: MaxDelegationTokenTTL / 10, // 10% 142 }) 143 144 // MintDelegationToken returns a delegation token that can be used by the 145 // current service to "pretend" to be the current caller (as returned by 146 // CurrentIdentity(...)) when sending requests to some other LUCI service. 147 // 148 // DEPRECATED. 149 // 150 // The delegation token is essentially a signed assertion that the current 151 // service is allowed to access some other service on behalf of the current 152 // user. 153 // 154 // A token can be targeted to some single specific service or usable by any 155 // allowed LUCI service (aka 'untargeted'). See TargetHost and Untargeted 156 // fields in DelegationTokenParams. 157 // 158 // The token is cached internally. Same token may be returned by multiple calls, 159 // if its lifetime allows. 160 func MintDelegationToken(ctx context.Context, p DelegationTokenParams) (_ *Token, err error) { 161 ctx, span := tracing.Start(ctx, "go.chromium.org/luci/server/auth.MintDelegationToken", 162 attribute.String("cr.dev.target", p.TargetHost), 163 ) 164 defer func() { tracing.End(span, err) }() 165 166 report := durationReporter(ctx, mintDelegationTokenDuration) 167 168 // Validate TargetHost. 169 target := "" 170 if p.Untargeted { 171 target = "*" 172 } else { 173 p.TargetHost = strings.ToLower(p.TargetHost) 174 if strings.IndexRune(p.TargetHost, '/') != -1 { 175 report(ErrBadTargetHost, "ERROR_BAD_HOST") 176 return nil, ErrBadTargetHost 177 } 178 target = "https://" + p.TargetHost 179 } 180 181 // Validate TTL is sane. 182 if p.MinTTL == 0 { 183 p.MinTTL = 10 * time.Minute 184 } 185 if p.MinTTL < 30*time.Second || p.MinTTL > MaxDelegationTokenTTL { 186 report(ErrBadTokenTTL, "ERROR_BAD_TTL") 187 return nil, ErrBadTokenTTL 188 } 189 190 // Validate tags are sane, sort them. Don't be very pedantic with validation, 191 // the server will apply its more precise validation rules anyway. 192 tags := append([]string(nil), p.Tags...) 193 for _, t := range tags { 194 parts := strings.SplitN(t, ":", 2) 195 if len(parts) < 2 || parts[0] == "" || parts[1] == "" { 196 report(ErrBadDelegationTag, "ERROR_BAD_TAG") 197 return nil, ErrBadDelegationTag 198 } 199 } 200 sort.Strings(tags) 201 202 // The state carries ID of the current user and URL of the token service. 203 state := GetState(ctx) 204 if state == nil { 205 report(ErrNotConfigured, "ERROR_NOT_CONFIGURED") 206 return nil, ErrNotConfigured 207 } 208 209 // Identity we want to impersonate. 210 userID := state.User().Identity 211 if userID == identity.AnonymousIdentity { 212 report(ErrAnonymousDelegation, "ERROR_NO_IDENTITY") 213 return nil, ErrAnonymousDelegation 214 } 215 216 // Grab hostname of the token service we received from the auth service. It 217 // will sign the token, and thus its identity is indirectly defines the 218 // identity of the generated token. For that reason we use it as part of the 219 // cache key. 220 tokenServiceURL, err := state.DB().GetTokenServiceURL(ctx) 221 switch { 222 case err != nil: 223 report(err, "ERROR_AUTH_DB") 224 return nil, err 225 case tokenServiceURL == "": 226 report(ErrTokenServiceNotConfigured, "ERROR_NO_TOKEN_SERVICE") 227 return nil, ErrTokenServiceNotConfigured 228 case !strings.HasPrefix(tokenServiceURL, "https://"): 229 // Note: this never actually happens. 230 logging.Errorf(ctx, "Bad token service URL: %s", tokenServiceURL) 231 report(ErrTokenServiceNotConfigured, "ERROR_NOT_HTTPS_TOKEN_SERVICE") 232 return nil, ErrTokenServiceNotConfigured 233 } 234 tokenServiceHost := tokenServiceURL[len("https://"):] 235 236 ctx = logging.SetFields(ctx, logging.Fields{ 237 "token": "delegation", 238 "target": target, 239 "userID": userID, 240 }) 241 242 cacheKey := fmt.Sprintf("%s\n%s\n%s\n%d\n%s", 243 userID, tokenServiceHost, target, len(tags), strings.Join(tags, "\n")) 244 245 cached, err, label := delegationTokenCache.fetchOrMintToken(ctx, &fetchOrMintTokenOp{ 246 CacheKey: cacheKey, 247 MinTTL: p.MinTTL, 248 MintTimeout: getConfig(ctx).adjustedTimeout(10 * time.Second), 249 250 // Mint is called on cache miss, under the lock. 251 Mint: func(ctx context.Context) (t *cachedToken, err error, label string) { 252 // Grab a token server client (or its mock). 253 rpcClient := p.rpcClient 254 if rpcClient == nil { 255 transport, err := GetRPCTransport(ctx, AsSelf) 256 if err != nil { 257 return nil, err, "ERROR_NO_TRANSPORT" 258 } 259 rpcClient = minter.NewTokenMinterClient(&prpc.Client{ 260 C: &http.Client{Transport: transport}, 261 Host: tokenServiceHost, 262 Options: &prpc.Options{ 263 Retry: func() retry.Iterator { 264 return &retry.ExponentialBackoff{ 265 Limited: retry.Limited{ 266 Delay: 50 * time.Millisecond, 267 Retries: 5, 268 }, 269 } 270 }, 271 }, 272 }) 273 } 274 275 // The actual RPC call. 276 resp, err := rpcClient.MintDelegationToken(ctx, &minter.MintDelegationTokenRequest{ 277 DelegatedIdentity: string(userID), 278 ValidityDuration: int64(MaxDelegationTokenTTL.Seconds()), 279 Audience: []string{"REQUESTOR"}, // make the token usable only by the calling service 280 Services: []string{target}, 281 Intent: p.Intent, 282 Tags: tags, 283 }) 284 if err != nil { 285 err = grpcutil.WrapIfTransient(err) 286 if transient.Tag.In(err) { 287 return nil, err, "ERROR_TRANSIENT_IN_MINTING" 288 } 289 return nil, err, "ERROR_MINTING" 290 } 291 292 // Sanity checks. A correctly working token server should not trigger them. 293 subtoken := resp.DelegationSubtoken 294 good := false 295 switch { 296 case subtoken == nil: 297 logging.Errorf(ctx, "No delegation_subtoken in the response") 298 case subtoken.Kind != messages.Subtoken_BEARER_DELEGATION_TOKEN: 299 logging.Errorf(ctx, "Invalid token kind: %s", subtoken.Kind) 300 case subtoken.ValidityDuration <= 0: 301 logging.Errorf(ctx, "Zero or negative validity_duration in the response") 302 default: 303 good = true 304 } 305 if !good { 306 return nil, ErrBrokenTokenService, "ERROR_BROKEN_TOKEN_SERVICE" 307 } 308 309 // Log details about the new token. 310 logging.Fields{ 311 "fingerprint": tokenFingerprint(resp.Token), 312 "subtokenID": subtoken.SubtokenId, 313 "validity": time.Duration(subtoken.ValidityDuration) * time.Second, 314 }.Debugf(ctx, "Minted new delegation token") 315 316 now := clock.Now(ctx).UTC() 317 exp := now.Add(time.Duration(subtoken.ValidityDuration) * time.Second) 318 return &cachedToken{ 319 Created: now, 320 Expiry: exp, 321 DelegationToken: resp.Token, 322 }, nil, "SUCCESS_CACHE_MISS" 323 }, 324 }) 325 326 report(err, label) 327 if err != nil { 328 return nil, err 329 } 330 return &Token{ 331 Token: cached.DelegationToken, 332 Expiry: cached.Expiry, 333 }, nil 334 }