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  }