Chromium Code Reviews| Index: tokenserver/appengine/impl/serviceaccounts/rpc_mint_oauth_token_grant.go |
| diff --git a/tokenserver/appengine/impl/serviceaccounts/rpc_mint_oauth_token_grant.go b/tokenserver/appengine/impl/serviceaccounts/rpc_mint_oauth_token_grant.go |
| index c111a1600536ed09a746f0b619add4bb9ac3ae14..dbbaa5d478cc843de3e3cb2a9f9e2bb606e72f43 100644 |
| --- a/tokenserver/appengine/impl/serviceaccounts/rpc_mint_oauth_token_grant.go |
| +++ b/tokenserver/appengine/impl/serviceaccounts/rpc_mint_oauth_token_grant.go |
| @@ -5,18 +5,206 @@ |
| package serviceaccounts |
| import ( |
| + "fmt" |
| + "time" |
| + |
| + "github.com/golang/protobuf/jsonpb" |
| "golang.org/x/net/context" |
| "google.golang.org/grpc" |
| "google.golang.org/grpc/codes" |
| + "github.com/luci/luci-go/common/clock" |
| + "github.com/luci/luci-go/common/logging" |
| + "github.com/luci/luci-go/common/proto/google" |
| + "github.com/luci/luci-go/server/auth" |
| + "github.com/luci/luci-go/server/auth/identity" |
| + "github.com/luci/luci-go/server/auth/signing" |
| + |
| + "github.com/luci/luci-go/tokenserver/api" |
| "github.com/luci/luci-go/tokenserver/api/minter/v1" |
| + "github.com/luci/luci-go/tokenserver/appengine/impl/utils" |
| + "github.com/luci/luci-go/tokenserver/appengine/impl/utils/revocation" |
| ) |
| +// tokenIDSequenceKind defines the namespace of int64 IDs for grant tokens. |
| +// |
| +// Changing it will effectively reset the ID generation. |
| +const tokenIDSequenceKind = "oauthTokenGrantID" |
| + |
| // MintOAuthTokenGrantRPC implements TokenMinter.MintOAuthTokenGrant method. |
| type MintOAuthTokenGrantRPC struct { |
| + // Signer is mocked in tests. |
| + // |
| + // In prod it is gaesigner.Signer. |
| + Signer signing.Signer |
| + |
| + // Rules returns service account rules to use for the request. |
| + // |
| + // In prod it is GlobalRulesCache.Rules. |
| + Rules func(context.Context) (*Rules, error) |
| + |
| + // mintMock call is used in tests. |
| + // |
| + // In prod it is 'mint' |
| + mintMock func(context.Context, *mintParams) (*minter.MintOAuthTokenGrantResponse, error) |
| } |
| // MintOAuthTokenGrant produces new OAuth token grant. |
| func (r *MintOAuthTokenGrantRPC) MintOAuthTokenGrant(c context.Context, req *minter.MintOAuthTokenGrantRequest) (*minter.MintOAuthTokenGrantResponse, error) { |
|
Vadim Sh.
2017/08/04 06:37:37
similar to https://github.com/luci/luci-go/blob/ma
|
| - return nil, grpc.Errorf(codes.Unavailable, "not implemented") |
| + state := auth.GetState(c) |
| + |
| + // Dump the whole request and relevant auth state to the debug log. |
| + callerID := state.User().Identity |
| + if logging.IsLogging(c, logging.Debug) { |
| + m := jsonpb.Marshaler{Indent: " "} |
| + dump, _ := m.MarshalToString(req) |
| + logging.Debugf(c, "Identity: %s", callerID) |
| + logging.Debugf(c, "MintOAuthTokenGrantRequest:\n%s", dump) |
| + } |
| + |
| + // Grab a string that identifies token server version. This almost always |
| + // just hits local memory cache. |
| + serviceVer, err := utils.ServiceVersion(c, r.Signer) |
| + if err != nil { |
| + return nil, grpc.Errorf(codes.Internal, "can't grab service version - %s", err) |
| + } |
| + |
| + // Reject obviously bad requests (and parse end_user along the way). |
| + switch { |
| + case req.ServiceAccount == "": |
| + err = fmt.Errorf("service_account is required") |
| + case req.ValidityDuration < 0: |
| + err = fmt.Errorf("validity_duration must be positive, not %d", req.ValidityDuration) |
| + case req.EndUser == "": |
| + err = fmt.Errorf("end_user is required") |
| + } |
| + var endUserID identity.Identity |
| + if err == nil { |
| + if endUserID, err = identity.MakeIdentity(req.EndUser); err != nil { |
| + err = fmt.Errorf("bad end_user - %s", err) |
| + } |
| + } |
| + if err != nil { |
| + logging.WithError(err).Errorf(c, "Bad request") |
| + return nil, grpc.Errorf(codes.InvalidArgument, "bad request - %s", err) |
| + } |
| + |
| + // TODO(vadimsh): Verify that this user is present by requiring the end user's |
| + // credentials, e.g make Swarming forward user's OAuth token to the token |
| + // server, so it can be validated here. |
| + |
| + // Fetch service account rules. They are hot in memory most of the time. |
| + rules, err := r.Rules(c) |
| + if err != nil { |
| + // Don't put error details in the message, it may be returned to |
| + // unauthorized callers. |
| + logging.WithError(err).Errorf(c, "Failed to load service accounts rules") |
| + return nil, grpc.Errorf(codes.Internal, "failed to load service accounts rules") |
| + } |
| + |
| + // Grab the rule for this account. Don't leak information about presence or |
| + // absence of the account to the caller, they may not be authorized to see the |
| + // account at all. |
| + rule := rules.Rule(req.ServiceAccount) |
| + if rule == nil { |
| + logging.Errorf(c, "No rule for service account %q in the config rev %s", req.ServiceAccount, rules.ConfigRevision()) |
| + return nil, grpc.Errorf(codes.PermissionDenied, "unknown service account or not enough permissions to use it") |
| + } |
| + logging.Infof(c, "Found the matching rule %q in the config rev %s", rule.Rule.Name, rules.ConfigRevision()) |
| + |
| + // If the caller is in 'Proxies' list, we assume it's known to us and we trust |
| + // it enough to start returning more detailed error messages. |
| + switch known, err := rule.Proxies.IsMember(c, callerID); { |
| + case err != nil: |
| + logging.WithError(err).Errorf(c, "Failed to check membership of caller %q", callerID) |
| + return nil, grpc.Errorf(codes.Internal, "membership check failed") |
| + case !known: |
| + logging.Errorf(c, "Caller %q is not authorized to use account %q", callerID, req.ServiceAccount) |
| + return nil, grpc.Errorf(codes.PermissionDenied, "unknown service account or not enough permissions to use it") |
| + } |
| + |
| + // Check ValidityDuration next, it is easiest check. |
| + if req.ValidityDuration == 0 { |
| + req.ValidityDuration = 3600 |
| + } |
| + if req.ValidityDuration > rule.Rule.MaxGrantValidityDuration { |
| + logging.Errorf(c, "Requested validity is larger than max allowed: %d > %d", req.ValidityDuration, rule.Rule.MaxGrantValidityDuration) |
| + return nil, grpc.Errorf(codes.InvalidArgument, "per rule %q the validity duration should be <= %d", rule.Rule.Name, rule.Rule.MaxGrantValidityDuration) |
| + } |
| + |
| + // Next is EndUsers check (involves membership lookups). |
| + switch known, err := rule.EndUsers.IsMember(c, endUserID); { |
| + case err != nil: |
| + logging.WithError(err).Errorf(c, "Failed to check membership of end user %q", endUserID) |
| + return nil, grpc.Errorf(codes.Internal, "membership check failed") |
| + case !known: |
| + logging.Errorf(c, "End user %q is not authorized to use account %q", endUserID, req.ServiceAccount) |
| + return nil, grpc.Errorf( |
| + codes.PermissionDenied, "per rule %q the user %q is not authorized to use the service account %q", |
| + rule.Rule.Name, endUserID, req.ServiceAccount) |
| + } |
| + |
| + // All checks are done! Note that AllowedScopes is checked later during |
| + // MintOAuthTokenViaGrant. Here we don't even know what OAuth scopes will be |
| + // requested. |
| + var resp *minter.MintOAuthTokenGrantResponse |
| + p := mintParams{ |
| + serviceAccount: req.ServiceAccount, |
| + proxyID: callerID, |
| + endUserID: endUserID, |
| + validityDuration: req.ValidityDuration, |
| + serviceVer: serviceVer, |
| + } |
| + if r.mintMock != nil { |
| + resp, err = r.mintMock(c, &p) |
| + } else { |
| + resp, err = r.mint(c, &p) |
| + } |
| + if err != nil { |
| + return nil, err |
| + } |
| + |
| + // TODO(vadimsh): Log the generated token to BigQuery. |
| + |
| + return resp, nil |
| +} |
| + |
| +type mintParams struct { |
| + serviceAccount string |
| + proxyID identity.Identity |
| + endUserID identity.Identity |
| + validityDuration int64 |
| + serviceVer string |
| +} |
| + |
| +// mint is called to make the token after the request has been authorized. |
| +func (r *MintOAuthTokenGrantRPC) mint(c context.Context, p *mintParams) (*minter.MintOAuthTokenGrantResponse, error) { |
| + id, err := revocation.GenerateTokenID(c, tokenIDSequenceKind) |
| + if err != nil { |
| + logging.WithError(err).Errorf(c, "Error when generating token ID") |
| + return nil, grpc.Errorf(codes.Internal, "error when generating token ID - %s", err) |
| + } |
| + |
| + now := clock.Now(c).UTC() |
| + expiry := now.Add(time.Duration(p.validityDuration) * time.Second) |
| + |
| + // All the stuff here has already been validated in 'MintOAuthTokenGrant'. |
| + signed, err := SignGrant(c, r.Signer, &tokenserver.OAuthTokenGrantBody{ |
| + TokenId: id, |
| + ServiceAccount: p.serviceAccount, |
| + Proxy: string(p.proxyID), |
| + EndUser: string(p.endUserID), |
| + IssuedAt: google.NewTimestamp(now), |
| + ValidityDuration: p.validityDuration, |
| + }) |
| + if err != nil { |
| + logging.WithError(err).Errorf(c, "Error when signing the token") |
| + return nil, grpc.Errorf(codes.Internal, "error when signing the token - %s", err) |
| + } |
| + |
| + return &minter.MintOAuthTokenGrantResponse{ |
| + GrantToken: signed, |
| + Expiry: google.NewTimestamp(expiry), |
| + ServiceVersion: p.serviceVer, |
| + }, nil |
| } |