go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/projectscope/rpc_mint_project_token.go (about) 1 // Copyright 2019 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 projectscope 16 17 import ( 18 "context" 19 "time" 20 21 "go.opentelemetry.io/otel/trace" 22 "google.golang.org/grpc/codes" 23 "google.golang.org/grpc/status" 24 "google.golang.org/protobuf/types/known/timestamppb" 25 26 "go.chromium.org/luci/common/clock" 27 "go.chromium.org/luci/common/logging" 28 "go.chromium.org/luci/server/auth" 29 "go.chromium.org/luci/server/auth/authdb" 30 "go.chromium.org/luci/server/auth/signing" 31 "go.chromium.org/luci/tokenserver/api/minter/v1" 32 "go.chromium.org/luci/tokenserver/appengine/impl/utils" 33 "go.chromium.org/luci/tokenserver/appengine/impl/utils/projectidentity" 34 ) 35 36 const ( 37 // projectActorsGroup is a group of identities and subgroups authorized to obtain project tokens. 38 projectActorsGroup = "auth-project-actors" 39 ) 40 41 // MintProjectTokenRPC implements TokenMinter.MintProjectToken. 42 // method. 43 type MintProjectTokenRPC struct { 44 // Signer is mocked in tests. 45 // 46 // In prod it is the default server signer that uses server's service account. 47 Signer signing.Signer 48 49 // MintAccessToken produces an OAuth token for a service account. 50 // 51 // In prod it is auth.MintAccessTokenForServiceAccount. 52 MintAccessToken func(context.Context, auth.MintAccessTokenParams) (*auth.Token, error) 53 54 // ProjectIdentities manages project scoped identities. 55 // 56 // In prod it is projectidentity.ProjectIdentities. 57 ProjectIdentities func(context.Context) projectidentity.Storage 58 59 // LogToken is mocked in tests. 60 // 61 // In prod it is produced by NewTokenLogger. 62 LogToken TokenLogger 63 } 64 65 // MintProjectToken mints a project-scoped service account OAuth2 token. 66 // 67 // Project-scoped service accounts are identities tied to an individual LUCI project. 68 // Therefore they provide a way to safely interact with LUCI APIs and prevent accidental 69 // cross-project operations. 70 func (r *MintProjectTokenRPC) MintProjectToken(c context.Context, req *minter.MintProjectTokenRequest) (*minter.MintProjectTokenResponse, error) { 71 state := auth.GetState(c) 72 callerID := state.User().Identity 73 74 // Make sure we log the request as early as possible. 75 utils.LogRequest(c, r, req, callerID) 76 77 // Perform bounds checking and corrections on the requested token validity lifetime. 78 if err := utils.ValidateAndNormalizeRequest(c, req.OauthScope, &req.MinValidityDuration, req.AuditTags); err != nil { 79 logging.WithError(err).Errorf(c, "invalid request %v", req) 80 return nil, status.Errorf(codes.InvalidArgument, "invalid request: %s", err.Error()) 81 } 82 if err := utils.ValidateProject(c, req.LuciProject); err != nil { 83 logging.WithError(err).Errorf(c, "invalid request %v", req) 84 return nil, status.Errorf(codes.InvalidArgument, "invalid request: %s", err.Error()) 85 } 86 87 // Using delegation to obtain a project scoped account is forbidden. 88 if callerID != state.PeerIdentity() { 89 logging.Errorf(c, "Trying to use delegation, it's forbidden") 90 return nil, status.Errorf(codes.PermissionDenied, "delegation is forbidden for this API call") 91 } 92 93 // Perform authorization check first. 94 // Internal error: Retry 95 // !Member: PermissionDenied 96 // Member: Continue 97 member, err := auth.IsMember(c, projectActorsGroup) 98 switch { 99 case err != nil: 100 logging.WithError(err).Errorf(c, "unable to perform group check of member %s and group %s", callerID, projectActorsGroup) 101 return nil, status.Errorf(codes.Internal, "internal authorization error") 102 case !member: 103 logging.Infof(c, "Denied access to %s, authorization failed", callerID) 104 return nil, status.Errorf(codes.PermissionDenied, "access denied") 105 } 106 107 projectIdentity, err := r.ProjectIdentities(c).LookupByProject(c, req.LuciProject) 108 if err != nil { 109 switch { 110 case err == projectidentity.ErrNotFound: 111 // TODO(tandrii): upgrade to Errorf once project scoped identity migration 112 // is re-started. 113 logging.WithError(err).Warningf(c, "no project identity for project %s", req.LuciProject) 114 return nil, status.Errorf(codes.NotFound, "unable to find project identity for project %s", req.LuciProject) 115 case err != nil: 116 logging.WithError(err).Errorf(c, "error while looking for scoped identity of project %s", req.LuciProject) 117 return nil, status.Errorf(codes.Internal, "internal error") 118 119 } 120 } 121 122 // All checks passed, mint the token. 123 accessTok, err := r.MintAccessToken(c, auth.MintAccessTokenParams{ 124 ServiceAccount: projectIdentity.Email, 125 Scopes: req.OauthScope, 126 MinTTL: time.Duration(req.MinValidityDuration) * time.Second, 127 }) 128 if err != nil { 129 logging.WithError(err).Errorf(c, "Failed to mint project scoped oauth token for caller %q in project %q for identity %q", 130 callerID, req.LuciProject, projectIdentity.Email) 131 return nil, status.Errorf(codes.Internal, "failed to mint token for service account %s", projectIdentity.Email) 132 } 133 134 // Determine service version for token logging. 135 serviceVer, err := utils.ServiceVersion(c, r.Signer) 136 if err != nil { 137 return nil, status.Errorf(codes.Internal, "can't grab service version - %s", err) 138 } 139 140 // Create response object. 141 resp := &minter.MintProjectTokenResponse{ 142 ServiceAccountEmail: projectIdentity.Email, 143 AccessToken: accessTok.Token, 144 Expiry: timestamppb.New(accessTok.Expiry), 145 ServiceVersion: serviceVer, 146 } 147 148 if r.LogToken != nil { 149 // Errors during logging are considered not fatal. We have a monitoring 150 // counter that tracks number of errors, so they are not totally invisible. 151 tokInfo := MintedTokenInfo{ 152 Request: req, 153 Response: resp, 154 RequestedAt: timestamppb.New(clock.Now(c)), 155 Expiration: resp.Expiry, 156 PeerIP: state.PeerIP(), 157 PeerIdentity: state.PeerIdentity(), 158 RequestID: trace.SpanContextFromContext(c).TraceID().String(), 159 AuthDBRev: authdb.Revision(state.DB()), 160 } 161 if logErr := r.LogToken(c, &tokInfo); logErr != nil { 162 logging.WithError(logErr).Errorf(c, "Failed to insert the delegation token into the BigQuery log") 163 } 164 } 165 166 return resp, nil 167 } 168 169 // Name implements utils.RPC interface. 170 func (r *MintProjectTokenRPC) Name() string { 171 return "MintProjectTokenRPC" 172 }