blob: 2980f36e4e5014bd7b88ea5a6643d1febe5b4b86 [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"fmt"
"sort"
"strings"
"time"
"go.opentelemetry.io/otel/attribute"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/retry/transient"
"go.chromium.org/luci/server/auth/internal/tracing"
)
// MintAccessTokenParams is passed to MintAccessTokenForServiceAccount.
type MintAccessTokenParams struct {
// ServiceAccount is an email of a service account to mint a token for.
ServiceAccount string
// Scopes is a list of OAuth scopes the token should have.
Scopes []string
// Delegates is a the sequence of service accounts in a delegation chain.
//
// Each service account must be granted the "iam.serviceAccountTokenCreator"
// role on its next service account in the chain. The last service account in
// the chain must be granted the "iam.serviceAccountTokenCreator" role on
// the service account specified by ServiceAccount field.
Delegates []string
// MinTTL defines an acceptable token lifetime.
//
// The returned token will be valid for at least MinTTL, but no longer than
// one hour.
//
// Default is 2 min.
MinTTL time.Duration
}
// actorAccessTokenCache is used to store access tokens of service accounts
// the current service has "iam.serviceAccountTokenCreator" role in.
//
// The token is stored in OAuth2Token field.
var actorAccessTokenCache = newTokenCache(tokenCacheConfig{
Kind: "as_actor_access_tok",
Version: 1,
ProcessCacheCapacity: 8192,
ExpiryRandomizationThreshold: 5 * time.Minute, // ~10% of regular 1h expiration
})
// MintAccessTokenForServiceAccount produces an access token for some service
// account that the current service has "iam.serviceAccountTokenCreator" role
// in.
//
// Used to implement AsActor authorization kind, but can also be used directly,
// if needed. The token is cached internally. Same token may be returned by
// multiple calls, if its lifetime allows.
//
// Recognizes transient errors and marks them, but does not automatically
// retry. Has internal timeout of 10 sec.
func MintAccessTokenForServiceAccount(ctx context.Context, params MintAccessTokenParams) (_ *Token, err error) {
ctx, span := tracing.Start(ctx, "go.chromium.org/luci/server/auth.MintAccessTokenForServiceAccount",
attribute.String("cr.dev.account", params.ServiceAccount),
)
defer func() { tracing.End(span, err) }()
report := durationReporter(ctx, mintAccessTokenDuration)
cfg := getConfig(ctx)
if cfg == nil || cfg.AccessTokenProvider == nil {
report(ErrNotConfigured, "ERROR_NOT_CONFIGURED")
return nil, ErrNotConfigured
}
if params.ServiceAccount == "" || len(params.Scopes) == 0 {
err := fmt.Errorf("invalid parameters")
report(err, "ERROR_BAD_ARGUMENTS")
return nil, err
}
if params.MinTTL == 0 {
params.MinTTL = 2 * time.Minute
}
sortedScopes := append([]string(nil), params.Scopes...)
sort.Strings(sortedScopes)
// Construct the cache key. Note that it is hashed by 'actorAccessTokenCache'
// and thus can be as long as necessary.
cacheKey := cacheKeyBuilder{}
if err := cacheKey.add("service account", params.ServiceAccount); err != nil {
report(err, "ERROR_BAD_ARGUMENTS")
return nil, err
}
for _, scope := range sortedScopes {
if err := cacheKey.add("scope", scope); err != nil {
report(err, "ERROR_BAD_ARGUMENTS")
return nil, err
}
}
for _, delegate := range params.Delegates {
if err := cacheKey.add("delegate", delegate); err != nil {
report(err, "ERROR_BAD_ARGUMENTS")
return nil, err
}
}
ctx = logging.SetFields(ctx, logging.Fields{
"token": "actor",
"account": params.ServiceAccount,
"scopes": strings.Join(sortedScopes, " "),
"delegates": strings.Join(params.Delegates, ":"),
})
cached, err, label := actorAccessTokenCache.fetchOrMintToken(ctx, &fetchOrMintTokenOp{
CacheKey: cacheKey.finish(),
MinTTL: params.MinTTL,
MintTimeout: cfg.adjustedTimeout(10 * time.Second),
// Mint is called on cache miss, under the lock.
Mint: func(ctx context.Context) (t *cachedToken, err error, label string) {
tok, err := cfg.actorTokensProvider().GenerateAccessToken(ctx, params.ServiceAccount, sortedScopes, params.Delegates)
if err != nil {
if transient.Tag.In(err) {
return nil, err, "ERROR_TRANSIENT_IN_MINTING"
}
return nil, err, "ERROR_FATAL_IN_MINTING"
}
now := clock.Now(ctx).UTC()
logging.Fields{
"fingerprint": tokenFingerprint(tok.AccessToken),
"validity": tok.Expiry.Sub(now),
}.Debugf(ctx, "Minted new actor OAuth token")
return &cachedToken{
Created: now,
Expiry: tok.Expiry.UTC(),
OAuth2Token: tok.AccessToken,
}, nil, "SUCCESS_CACHE_MISS"
},
})
report(err, label)
if err != nil {
return nil, err
}
return &Token{
Token: cached.OAuth2Token,
Expiry: cached.Expiry,
}, nil
}