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  }