go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/delegation/rpc_mint_delegation_token.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 delegation
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  	"time"
    22  
    23  	"go.opentelemetry.io/otel/trace"
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/grpc/status"
    26  	"google.golang.org/protobuf/encoding/protojson"
    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/transient"
    32  	"go.chromium.org/luci/server/auth"
    33  	"go.chromium.org/luci/server/auth/authdb"
    34  	"go.chromium.org/luci/server/auth/delegation/messages"
    35  	"go.chromium.org/luci/server/auth/signing"
    36  
    37  	"go.chromium.org/luci/tokenserver/api/admin/v1"
    38  	"go.chromium.org/luci/tokenserver/api/minter/v1"
    39  
    40  	"go.chromium.org/luci/tokenserver/appengine/impl/utils"
    41  	"go.chromium.org/luci/tokenserver/appengine/impl/utils/identityset"
    42  	"go.chromium.org/luci/tokenserver/appengine/impl/utils/revocation"
    43  )
    44  
    45  // tokenIDSequenceKind defines the namespace of int64 IDs for delegation tokens.
    46  //
    47  // Changing it will effectively reset the ID generation.
    48  const tokenIDSequenceKind = "delegationTokenID"
    49  
    50  // MintDelegationTokenRPC implements TokenMinter.MintDelegationToken RPC method.
    51  type MintDelegationTokenRPC struct {
    52  	// Signer is mocked in tests.
    53  	//
    54  	// In prod it is the default server signer that uses server's service account.
    55  	Signer signing.Signer
    56  
    57  	// Rules returns delegation rules to use for the request.
    58  	//
    59  	// In prod it is GlobalRulesCache.Rules.
    60  	Rules func(context.Context) (*Rules, error)
    61  
    62  	// LogToken is mocked in tests.
    63  	//
    64  	// In prod it is produced by NewTokenLogger.
    65  	LogToken TokenLogger
    66  
    67  	// mintMock call is used in tests.
    68  	//
    69  	// In prod it is 'mint'
    70  	mintMock func(context.Context, *mintParams) (*minter.MintDelegationTokenResponse, error)
    71  }
    72  
    73  // MintDelegationToken generates a new bearer delegation token.
    74  func (r *MintDelegationTokenRPC) MintDelegationToken(c context.Context, req *minter.MintDelegationTokenRequest) (*minter.MintDelegationTokenResponse, error) {
    75  	state := auth.GetState(c)
    76  
    77  	// Dump the whole request and relevant auth state to the debug log.
    78  	if logging.IsLogging(c, logging.Debug) {
    79  		opts := protojson.MarshalOptions{Indent: "  "}
    80  		logging.Debugf(c, "PeerIdentity: %s", state.PeerIdentity())
    81  		logging.Debugf(c, "MintDelegationTokenRequest:\n%s", opts.Format(req))
    82  	}
    83  
    84  	// Validate the request authentication context: not an anonymous call, no
    85  	// delegation is used.
    86  	callerID := state.User().Identity
    87  	if callerID != state.PeerIdentity() {
    88  		logging.Errorf(c, "Trying to use delegation, it's forbidden")
    89  		return nil, status.Errorf(codes.PermissionDenied, "delegation is forbidden for this API call")
    90  	}
    91  	if callerID == identity.AnonymousIdentity {
    92  		logging.Errorf(c, "Unauthenticated request")
    93  		return nil, status.Errorf(codes.Unauthenticated, "authentication required")
    94  	}
    95  
    96  	// Grab a string that identifies token server version. This almost always
    97  	// just hits local memory cache.
    98  	serviceVer, err := utils.ServiceVersion(c, r.Signer)
    99  	if err != nil {
   100  		return nil, status.Errorf(codes.Internal, "can't grab service version - %s", err)
   101  	}
   102  
   103  	rules, err := r.Rules(c)
   104  	if err != nil {
   105  		// Don't put error details in the message, it may be returned to
   106  		// unauthorized callers.
   107  		logging.WithError(err).Errorf(c, "Failed to load delegation rules")
   108  		return nil, status.Errorf(codes.Internal, "failed to load delegation rules")
   109  	}
   110  
   111  	// Make sure the caller is mentioned in the config before doing anything else.
   112  	// This rejects unauthorized callers early. Passing this check doesn't mean
   113  	// that there's a matching rule though, so the request still can be rejected
   114  	// later.
   115  	switch ok, err := rules.IsAuthorizedRequestor(c, callerID); {
   116  	case err != nil:
   117  		logging.WithError(err).Errorf(c, "IsAuthorizedRequestor failed")
   118  		return nil, status.Errorf(codes.Internal, "failed to check authorization")
   119  	case !ok:
   120  		logging.Errorf(c, "Didn't pass initial authorization")
   121  		return nil, status.Errorf(codes.PermissionDenied, "not authorized")
   122  	}
   123  
   124  	// Validate requested token lifetime. It's not part of the rules query.
   125  	if req.ValidityDuration == 0 {
   126  		req.ValidityDuration = 3600
   127  	}
   128  	if req.ValidityDuration < 0 {
   129  		err = fmt.Errorf("invalid 'validity_duration' (%d)", req.ValidityDuration)
   130  		logging.WithError(err).Errorf(c, "Bad request")
   131  		return nil, status.Errorf(codes.InvalidArgument, "bad request - %s", err)
   132  	}
   133  
   134  	// Same for tags, they are transferred intact to the final token.
   135  	if err := utils.ValidateTags(req.Tags); err != nil {
   136  		err = fmt.Errorf("invalid 'tags': %s", err)
   137  		logging.WithError(err).Errorf(c, "Bad request")
   138  		return nil, status.Errorf(codes.InvalidArgument, "bad request - %s", err)
   139  	}
   140  
   141  	// Validate and normalize the request. This may do relatively expensive calls
   142  	// to resolve "https://<service-url>" entries to "service:<id>" entries.
   143  	query, err := buildRulesQuery(c, req, callerID)
   144  	if err != nil {
   145  		if transient.Tag.In(err) {
   146  			logging.WithError(err).Errorf(c, "buildRulesQuery failed")
   147  			return nil, status.Errorf(codes.Internal, "failure when resolving target service ID - %s", err)
   148  		}
   149  		logging.WithError(err).Errorf(c, "Bad request")
   150  		return nil, status.Errorf(codes.InvalidArgument, "bad request - %s", err)
   151  	}
   152  
   153  	// Consult the config to find the rule that allows this operation (if any).
   154  	rule, err := rules.FindMatchingRule(c, query)
   155  	if err != nil {
   156  		if transient.Tag.In(err) {
   157  			logging.WithError(err).Errorf(c, "FindMatchingRule failed")
   158  			return nil, status.Errorf(codes.Internal, "failure when checking rules - %s", err)
   159  		}
   160  		logging.WithError(err).Errorf(c, "Didn't pass rules check")
   161  		return nil, status.Errorf(codes.PermissionDenied, "forbidden - %s", err)
   162  	}
   163  	logging.Infof(c, "Found the matching rule %q in the config rev %s", rule.Name, rules.ConfigRevision())
   164  
   165  	// Make sure the requested token lifetime is allowed by the rule.
   166  	if req.ValidityDuration > rule.MaxValidityDuration {
   167  		err = fmt.Errorf(
   168  			"the requested validity duration (%d sec) exceeds the maximum allowed one (%d sec)",
   169  			req.ValidityDuration, rule.MaxValidityDuration)
   170  		logging.WithError(err).Errorf(c, "Validity duration check didn't pass")
   171  		return nil, status.Errorf(codes.PermissionDenied, "forbidden - %s", err)
   172  	}
   173  
   174  	var resp *minter.MintDelegationTokenResponse
   175  	p := mintParams{
   176  		request:    req,
   177  		query:      query,
   178  		rule:       rule,
   179  		serviceVer: serviceVer,
   180  	}
   181  	if r.mintMock != nil {
   182  		resp, err = r.mintMock(c, &p)
   183  	} else {
   184  		resp, err = r.mint(c, &p)
   185  	}
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	if r.LogToken != nil {
   191  		// Errors during logging are considered not fatal. We have a monitoring
   192  		// counter that tracks number of errors, so they are not totally invisible.
   193  		tokInfo := MintedTokenInfo{
   194  			Request:   req,
   195  			Response:  resp,
   196  			ConfigRev: rules.ConfigRevision(),
   197  			Rule:      rule,
   198  			PeerIP:    state.PeerIP(),
   199  			RequestID: trace.SpanContextFromContext(c).TraceID().String(),
   200  			AuthDBRev: authdb.Revision(state.DB()),
   201  		}
   202  		if logErr := r.LogToken(c, &tokInfo); logErr != nil {
   203  			logging.WithError(logErr).Errorf(c, "Failed to insert the delegation token into the BigQuery log")
   204  		}
   205  	}
   206  
   207  	return resp, nil
   208  }
   209  
   210  // mintParams are passed to 'mint' function.
   211  type mintParams struct {
   212  	request    *minter.MintDelegationTokenRequest // the original RPC request
   213  	query      *RulesQuery                        // extracted from the request
   214  	rule       *admin.DelegationRule              // looked up in the config based on 'query'
   215  	serviceVer string                             // version string to put in the response
   216  }
   217  
   218  // mint is called to make the token after the request has been authorized.
   219  func (r *MintDelegationTokenRPC) mint(c context.Context, p *mintParams) (*minter.MintDelegationTokenResponse, error) {
   220  	id, err := revocation.GenerateTokenID(c, tokenIDSequenceKind)
   221  	if err != nil {
   222  		logging.WithError(err).Errorf(c, "Error when generating token ID.")
   223  		return nil, status.Errorf(codes.Internal, "error when generating token ID - %s", err)
   224  	}
   225  
   226  	// All the stuff here has already been validated in 'MintDelegationToken'.
   227  	subtok := &messages.Subtoken{
   228  		Kind:              messages.Subtoken_BEARER_DELEGATION_TOKEN,
   229  		SubtokenId:        id,
   230  		DelegatedIdentity: string(p.query.Delegator),
   231  		RequestorIdentity: string(p.query.Requestor),
   232  		CreationTime:      clock.Now(c).Unix(),
   233  		ValidityDuration:  int32(p.request.ValidityDuration),
   234  		Audience:          p.query.Audience.ToStrings(),
   235  		Services:          p.query.Services.ToStrings(),
   236  		Tags:              p.request.Tags,
   237  	}
   238  
   239  	signed, err := SignToken(c, r.Signer, subtok)
   240  	if err != nil {
   241  		logging.WithError(err).Errorf(c, "Error when signing the token.")
   242  		return nil, status.Errorf(codes.Internal, "error when signing the token - %s", err)
   243  	}
   244  
   245  	return &minter.MintDelegationTokenResponse{
   246  		Token:              signed,
   247  		DelegationSubtoken: subtok,
   248  		ServiceVersion:     p.serviceVer,
   249  	}, nil
   250  }
   251  
   252  // buildRulesQuery validates the request, extracts and normalizes relevant
   253  // fields into RulesQuery object.
   254  //
   255  // May return transient errors.
   256  func buildRulesQuery(c context.Context, req *minter.MintDelegationTokenRequest, requestor identity.Identity) (*RulesQuery, error) {
   257  	// Validate 'delegated_identity'.
   258  	var err error
   259  	var delegator identity.Identity
   260  	if req.DelegatedIdentity == "" {
   261  		return nil, fmt.Errorf("'delegated_identity' is required")
   262  	}
   263  	if req.DelegatedIdentity == Requestor {
   264  		delegator = requestor // the requestor is delegating its own identity
   265  	} else {
   266  		if delegator, err = identity.MakeIdentity(req.DelegatedIdentity); err != nil {
   267  			return nil, fmt.Errorf("bad 'delegated_identity' - %s", err)
   268  		}
   269  	}
   270  
   271  	// Validate 'audience', convert it into a set.
   272  	if len(req.Audience) == 0 {
   273  		return nil, fmt.Errorf("'audience' is required")
   274  	}
   275  	audienceSet, err := identityset.FromStrings(req.Audience, skipRequestor)
   276  	if err != nil {
   277  		return nil, fmt.Errorf("bad 'audience' - %s", err)
   278  	}
   279  	if sliceHasString(req.Audience, Requestor) {
   280  		audienceSet.AddIdentity(requestor)
   281  	}
   282  
   283  	// Split 'services' into two lists: URLs and everything else (which is
   284  	// "service:..." and "*" presumably, validated below).
   285  	if len(req.Services) == 0 {
   286  		return nil, fmt.Errorf("'services' is required")
   287  	}
   288  	urls := make([]string, 0, len(req.Services))
   289  	rest := make([]string, 0, len(req.Services))
   290  	for _, srv := range req.Services {
   291  		if strings.HasPrefix(srv, "https://") {
   292  			urls = append(urls, srv)
   293  		} else {
   294  			rest = append(rest, srv)
   295  		}
   296  	}
   297  
   298  	// Convert the list into a set, verify it contains only services (or "*").
   299  	servicesSet, err := identityset.FromStrings(rest, nil)
   300  	if err != nil {
   301  		return nil, fmt.Errorf("bad 'services' - %s", err)
   302  	}
   303  	if len(servicesSet.Groups) != 0 {
   304  		return nil, fmt.Errorf("bad 'services' - can't specify groups")
   305  	}
   306  	for ident := range servicesSet.IDs {
   307  		if ident.Kind() != identity.Service {
   308  			return nil, fmt.Errorf("bad 'services' - %q is not a service ID", ident)
   309  		}
   310  	}
   311  
   312  	// Resolve URLs into app IDs. This may involve URL fetch calls (if the cache
   313  	// is cold), so skip this expensive call if already specifying the universal
   314  	// set of all services.
   315  	if !servicesSet.All && len(urls) != 0 {
   316  		if err = resolveServiceIDs(c, urls, servicesSet); err != nil {
   317  			return nil, err
   318  		}
   319  	}
   320  
   321  	// Done!
   322  	return &RulesQuery{
   323  		Requestor: requestor,
   324  		Delegator: delegator,
   325  		Audience:  audienceSet,
   326  		Services:  servicesSet,
   327  	}, nil
   328  }
   329  
   330  // fetchLUCIServiceIdentity is replaced in tests.
   331  var fetchLUCIServiceIdentity = signing.FetchLUCIServiceIdentity
   332  
   333  // resolveServiceIDs takes a bunch of service URLs and resolves them to
   334  // 'service:<app-id>' identities, putting them in the 'out' set.
   335  //
   336  // May return transient errors.
   337  func resolveServiceIDs(c context.Context, urls []string, out *identityset.Set) error {
   338  	// URL fetch calls below should be extra fast. If they get stuck, something is
   339  	// horribly wrong, better to abort soon.
   340  	c, cancel := clock.WithTimeout(c, 5*time.Second)
   341  	defer cancel()
   342  
   343  	type Result struct {
   344  		URL string
   345  		ID  identity.Identity
   346  		Err error
   347  	}
   348  
   349  	ch := make(chan Result, len(urls))
   350  
   351  	for _, url := range urls {
   352  		go func(url string) {
   353  			id, err := fetchLUCIServiceIdentity(c, url)
   354  			ch <- Result{url, id, err}
   355  		}(url)
   356  	}
   357  
   358  	for i := 0; i < len(urls); i++ {
   359  		result := <-ch
   360  		if result.Err != nil {
   361  			if transient.Tag.In(result.Err) {
   362  				return result.Err
   363  			}
   364  			return fmt.Errorf("could not resolve %q to service ID - %s", result.URL, result.Err)
   365  		}
   366  		out.AddIdentity(result.ID)
   367  	}
   368  
   369  	return nil
   370  }