go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/serviceaccounts/rpc_mint_service_account_token.go (about) 1 // Copyright 2020 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 serviceaccounts 16 17 import ( 18 "context" 19 "fmt" 20 "time" 21 22 "go.opentelemetry.io/otel/trace" 23 "google.golang.org/grpc/codes" 24 "google.golang.org/grpc/status" 25 "google.golang.org/protobuf/encoding/protojson" 26 "google.golang.org/protobuf/types/known/timestamppb" 27 28 "go.chromium.org/luci/auth/identity" 29 "go.chromium.org/luci/common/clock" 30 "go.chromium.org/luci/common/data/stringset" 31 "go.chromium.org/luci/common/logging" 32 "go.chromium.org/luci/common/retry/transient" 33 "go.chromium.org/luci/server/auth" 34 "go.chromium.org/luci/server/auth/authdb" 35 "go.chromium.org/luci/server/auth/realms" 36 "go.chromium.org/luci/server/auth/signing" 37 38 "go.chromium.org/luci/tokenserver/api/minter/v1" 39 "go.chromium.org/luci/tokenserver/appengine/impl/utils" 40 "go.chromium.org/luci/tokenserver/appengine/impl/utils/projectidentity" 41 ) 42 43 var ( 44 // Grants permission to mint tokens for accounts that belong to a realm. 45 permMintToken = realms.RegisterPermission("luci.serviceAccounts.mintToken") 46 // Grants permission to *be* a service account that is in the realm. 47 permExistInRealm = realms.RegisterPermission("luci.serviceAccounts.existInRealm") 48 ) 49 50 // MintServiceAccountTokenRPC implements the corresponding method. 51 type MintServiceAccountTokenRPC struct { 52 // Signer is used only for its ServiceInfo. 53 // 54 // In prod it is the default server signer that uses server's service account. 55 Signer signing.Signer 56 57 // Mapping returns project<->account mapping to use for the request. 58 // 59 // In prod it is GlobalMappingCache.Mapping. 60 Mapping func(context.Context) (*Mapping, error) 61 62 // ProjectIdentities manages project scoped identities. 63 // 64 // In prod it is projectidentity.ProjectIdentities. 65 ProjectIdentities func(context.Context) projectidentity.Storage 66 67 // MintAccessToken produces an OAuth token for a service account. 68 // 69 // In prod it is auth.MintAccessTokenForServiceAccount. 70 MintAccessToken func(context.Context, auth.MintAccessTokenParams) (*auth.Token, error) 71 72 // MintIDToken produces an ID token for a service account. 73 // 74 // In prod it is auth.MintIDTokenForServiceAccount. 75 MintIDToken func(context.Context, auth.MintIDTokenParams) (*auth.Token, error) 76 77 // LogToken is mocked in tests. 78 // 79 // In prod it is produced by NewTokenLogger. 80 LogToken TokenLogger 81 } 82 83 // validatedRequest is extracted from MintServiceAccountTokenRequest. 84 type validatedRequest struct { 85 kind minter.ServiceAccountTokenKind 86 account string // e.g. "something@blah.iam.gserviceaccount.com" 87 realm string // e.g. "<project>:<realm>" 88 project string // just "<project>" part 89 oauthScopes []string // non-empty iff kind is ..._ACCESS_TOKEN 90 idTokenAudience string // non-empty iff kind is ..._ID_TOKEN 91 minTTL time.Duration 92 auditTags []string 93 } 94 95 // callEnv groups a bunch of arguments to simplify passing them to functions. 96 // 97 // They all are basically extracted from context.Context and do not depend on 98 // the body of the request. 99 type callEnv struct { 100 state auth.State 101 db authdb.DB 102 caller identity.Identity // used in ACLs 103 peer identity.Identity // used in logs only 104 mapping *Mapping 105 } 106 107 // MintServiceAccountToken mints an OAuth2 access token or OpenID ID token 108 // that belongs to some service account using LUCI Realms for authorization. 109 // 110 // See proto docs for more details. 111 func (r *MintServiceAccountTokenRPC) MintServiceAccountToken(ctx context.Context, req *minter.MintServiceAccountTokenRequest) (*minter.MintServiceAccountTokenResponse, error) { 112 state := auth.GetState(ctx) 113 env := &callEnv{ 114 state: state, 115 db: state.DB(), 116 caller: state.User().Identity, 117 peer: state.PeerIdentity(), 118 } 119 120 // Mapping is needed to check ACLs (step 3). 121 var err error 122 if env.mapping, err = r.Mapping(ctx); err != nil { 123 logging.Errorf(ctx, "Failed to grab Mapping: %s", err) 124 return nil, status.Errorf(codes.Internal, "internal server error") 125 } 126 127 // Log the request and details about the call environment. 128 r.logRequest(ctx, env, req) 129 130 // Validate the format of the request (e.g. check required fields and so on). 131 validated, err := r.validateRequest(req) 132 if err != nil { 133 return nil, status.Errorf(codes.InvalidArgument, "%s", err) 134 } 135 136 // Check it passes ACLs as described in the proto doc for this RPC. 137 if err := r.checkACLs(ctx, env, validated); err != nil { 138 return nil, err 139 } 140 141 // Impersonate through a project-scoped account if the LUCI project is 142 // opted-in to use this mechanism. 143 // 144 // There's a special case for accounts belonging to "@internal:..." realms. 145 // They are not part of any LUCI project and they are defined in global LUCI 146 // configs. Keep using token server's own global account when impersonating 147 // them. 148 var delegates []string 149 if env.mapping.UseProjectScopedAccount(validated.project) && validated.project != realms.InternalProject { 150 switch ident, err := r.ProjectIdentities(ctx).LookupByProject(ctx, validated.project); { 151 case err == projectidentity.ErrNotFound: 152 logging.WithError(err).Errorf(ctx, "No project-scoped account for project %s", validated.project) 153 return nil, status.Errorf(codes.InvalidArgument, "project-scoped account for project %s is not configured", validated.project) 154 case err != nil: 155 logging.WithError(err).Errorf(ctx, "Error while looking up project-scoped account for %s", validated.project) 156 return nil, status.Errorf(codes.Internal, "internal error") 157 default: 158 logging.Infof(ctx, "Delegating through project-scoped account %q", ident.Email) 159 delegates = []string{ident.Email} 160 } 161 } 162 163 // Mint the token of the corresponding kind. 164 var tok *auth.Token 165 switch { 166 case validated.kind == minter.ServiceAccountTokenKind_SERVICE_ACCOUNT_TOKEN_ACCESS_TOKEN: 167 tok, err = r.MintAccessToken(ctx, auth.MintAccessTokenParams{ 168 ServiceAccount: validated.account, 169 Scopes: validated.oauthScopes, 170 Delegates: delegates, 171 MinTTL: validated.minTTL, 172 }) 173 case validated.kind == minter.ServiceAccountTokenKind_SERVICE_ACCOUNT_TOKEN_ID_TOKEN: 174 tok, err = r.MintIDToken(ctx, auth.MintIDTokenParams{ 175 ServiceAccount: validated.account, 176 Audience: validated.idTokenAudience, 177 Delegates: delegates, 178 MinTTL: validated.minTTL, 179 }) 180 default: 181 panic("impossible") // already checked in validateRequest 182 } 183 184 if err != nil { 185 logging.Errorf(ctx, "Failed to mint a token for %q: %s", validated.account, err) 186 code := codes.InvalidArgument // mostly likely misconfigured IAM roles 187 if transient.Tag.In(err) { 188 code = codes.Internal 189 } 190 return nil, status.Errorf(code, "failed to mint token for %q - %s", validated.account, err) 191 } 192 193 // Grab a string that identifies token server version. This almost always 194 // just hits local memory cache. 195 serviceVer, err := utils.ServiceVersion(ctx, r.Signer) 196 if err != nil { 197 return nil, status.Errorf(codes.Internal, "can't grab service version - %s", err) 198 } 199 200 // The RPC response. 201 resp := &minter.MintServiceAccountTokenResponse{ 202 Token: tok.Token, 203 Expiry: timestamppb.New(tok.Expiry), 204 ServiceVersion: serviceVer, 205 } 206 207 // Log it to BigQuery. 208 if r.LogToken != nil { 209 info := MintedTokenInfo{ 210 Request: req, 211 Response: resp, 212 RequestedAt: clock.Now(ctx), 213 OAuthScopes: validated.oauthScopes, 214 RequestIdentity: env.caller, 215 PeerIdentity: env.peer, 216 ConfigRev: env.mapping.ConfigRevision(), 217 PeerIP: env.state.PeerIP(), 218 RequestID: trace.SpanContextFromContext(ctx).TraceID().String(), 219 AuthDBRev: authdb.Revision(state.DB()), 220 } 221 // Errors during logging are considered not fatal. We have a monitoring 222 // counter that tracks number of errors, so they are not totally invisible. 223 if err := r.LogToken(ctx, &info); err != nil { 224 logging.Errorf(ctx, "Failed to insert the token info into the BigQuery log: %s", err) 225 } 226 } 227 228 return resp, nil 229 } 230 231 // logRequest logs the body of the request and details about the call. 232 func (r *MintServiceAccountTokenRPC) logRequest(ctx context.Context, env *callEnv, req *minter.MintServiceAccountTokenRequest) { 233 if !logging.IsLogging(ctx, logging.Debug) { 234 return 235 } 236 opts := protojson.MarshalOptions{Indent: " "} 237 logging.Debugf(ctx, "Peer: %s", env.peer) 238 logging.Debugf(ctx, "Identity: %s", env.caller) 239 logging.Debugf(ctx, "Mapping: %s", env.mapping.ConfigRevision()) 240 logging.Debugf(ctx, "AuthDB: %d", authdb.Revision(env.db)) 241 logging.Debugf(ctx, "MintServiceAccountTokenRequest:\n%s", opts.Format(req)) 242 } 243 244 // validateRequest checks the request is well-formed. 245 func (r *MintServiceAccountTokenRPC) validateRequest(req *minter.MintServiceAccountTokenRequest) (*validatedRequest, error) { 246 // Validate TokenKind. 247 switch req.TokenKind { 248 case minter.ServiceAccountTokenKind_SERVICE_ACCOUNT_TOKEN_ACCESS_TOKEN: 249 case minter.ServiceAccountTokenKind_SERVICE_ACCOUNT_TOKEN_ID_TOKEN: 250 // good 251 case minter.ServiceAccountTokenKind_SERVICE_ACCOUNT_TOKEN_UNSPECIFIED: 252 return nil, fmt.Errorf("token_kind is required") 253 default: 254 return nil, fmt.Errorf("unrecognized token_kind %d", req.TokenKind) 255 } 256 257 // Validate ServiceAccount. 258 if req.ServiceAccount == "" { 259 return nil, fmt.Errorf("service_account is required") 260 } 261 if _, err := identity.MakeIdentity("user:" + req.ServiceAccount); err != nil { 262 return nil, fmt.Errorf("bad service_account: %s", err) 263 } 264 265 // Validate and parse Realm. 266 if req.Realm == "" { 267 return nil, fmt.Errorf("realm is required") 268 } 269 if err := realms.ValidateRealmName(req.Realm, realms.GlobalScope); err != nil { 270 return nil, fmt.Errorf("bad realm: %s", err) 271 } 272 project, _ := realms.Split(req.Realm) 273 274 // Validate SERVICE_ACCOUNT_TOKEN_ACCESS_TOKEN fields. 275 var oauthScopes stringset.Set 276 if req.TokenKind == minter.ServiceAccountTokenKind_SERVICE_ACCOUNT_TOKEN_ACCESS_TOKEN { 277 if len(req.OauthScope) == 0 { 278 return nil, fmt.Errorf("oauth_scope is required when token_kind is %s", req.TokenKind) 279 } 280 for _, scope := range req.OauthScope { 281 if scope == "" { 282 return nil, fmt.Errorf("bad oauth_scope: got an empty string") 283 } 284 } 285 oauthScopes = stringset.NewFromSlice(req.OauthScope...) 286 } else { 287 if len(req.OauthScope) != 0 { 288 return nil, fmt.Errorf("oauth_scope must not be used when token_kind is %s", req.TokenKind) 289 } 290 } 291 292 // Validate SERVICE_ACCOUNT_TOKEN_ID_TOKEN fields. 293 if req.TokenKind == minter.ServiceAccountTokenKind_SERVICE_ACCOUNT_TOKEN_ID_TOKEN { 294 if req.IdTokenAudience == "" { 295 return nil, fmt.Errorf("id_token_audience is required when token_kind is %s", req.TokenKind) 296 } 297 } else { 298 if req.IdTokenAudience != "" { 299 return nil, fmt.Errorf("id_token_audience must not be used when token_kind is %s", req.TokenKind) 300 } 301 } 302 303 // Validate MinValidityDuration, substitute defaults. 304 minTTL := time.Duration(req.MinValidityDuration) * time.Second 305 if minTTL == 0 { 306 minTTL = 5 * time.Minute 307 } 308 switch { 309 case minTTL < 0: 310 return nil, fmt.Errorf("bad min_validity_duration: got %d, must be positive", req.MinValidityDuration) 311 case minTTL > time.Hour: 312 return nil, fmt.Errorf("bad min_validity_duration: got %d, must be not greater than 3600", req.MinValidityDuration) 313 } 314 315 // Validate AuditTags. 316 if err := utils.ValidateTags(req.AuditTags); err != nil { 317 return nil, fmt.Errorf("bad audit_tags: %s", err) 318 } 319 320 return &validatedRequest{ 321 kind: req.TokenKind, 322 account: req.ServiceAccount, 323 realm: req.Realm, 324 project: project, 325 oauthScopes: oauthScopes.ToSortedSlice(), 326 idTokenAudience: req.IdTokenAudience, 327 minTTL: minTTL, 328 auditTags: req.AuditTags, 329 }, nil 330 } 331 332 // checkACLs returns an grpc error if the request is forbidden. 333 // 334 // Logs errors inside. 335 func (r *MintServiceAccountTokenRPC) checkACLs(ctx context.Context, env *callEnv, req *validatedRequest) error { 336 // Check that caller is allowed to mint tokens for accounts in the realm. 337 switch yes, err := env.db.HasPermission(ctx, env.caller, permMintToken, req.realm, nil); { 338 case err != nil: 339 logging.Errorf(ctx, "HasPermission(%q, %q, %q) failed: %s", env.caller, permMintToken, req.realm, err) 340 return status.Errorf(codes.Internal, "internal server error") 341 case !yes: 342 logging.Errorf(ctx, "Caller %q has no permission to mint tokens in the realm %q or it doesn't exist", env.caller, req.realm) 343 return status.Errorf(codes.PermissionDenied, "unknown realm or no permission to use service accounts there") 344 } 345 346 // Check the service account is defined in the realm. 347 accountID := identity.Identity("user:" + req.account) 348 switch yes, err := env.db.HasPermission(ctx, accountID, permExistInRealm, req.realm, nil); { 349 case err != nil: 350 logging.Errorf(ctx, "HasPermission(%q, %q, %q) failed: %s", accountID, permExistInRealm, req.realm, err) 351 return status.Errorf(codes.Internal, "internal server error") 352 case !yes: 353 logging.Errorf(ctx, "Service account %q is not in the realm %q", req.account, req.realm) 354 return status.Errorf(codes.PermissionDenied, "the service account %q is not in the realm %q", req.account, req.realm) 355 } 356 357 // Check the service account is allowed to be defined in this realm at all 358 // according to the global Token Server config. Skip if we'll be using 359 // the project-scoped account to mint the token. The mapping is essentially 360 // stored in IAM policies in this case. 361 if !env.mapping.UseProjectScopedAccount(req.project) { 362 if !env.mapping.CanProjectUseAccount(req.project, req.account) { 363 logging.Errorf(ctx, "Service account %q is not allowed to be used by the project %q", req.account, req.project) 364 return status.Errorf(codes.PermissionDenied, 365 "the service account %q is not allowed to be used by the project %q per %s configuration", 366 req.account, req.project, configFileName) 367 } 368 } 369 370 return nil 371 }