blob: af35485eb285bea4aab54ff9fd0b0aaa1798f0b3 [file] [log] [blame]
// Copyright 2017 The LUCI Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package serviceaccounts
import (
"context"
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/server/auth/signing"
tokenserver "go.chromium.org/luci/tokenserver/api"
"go.chromium.org/luci/tokenserver/api/admin/v1"
)
// InspectOAuthTokenGrantRPC implements admin.InspectOAuthTokenGrant method.
type InspectOAuthTokenGrantRPC struct {
// Signer is mocked in tests.
//
// In prod it is the default server signer that uses server's service account.
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)
}
// InspectOAuthTokenGrant decodes the given OAuth token grant.
func (r *InspectOAuthTokenGrantRPC) InspectOAuthTokenGrant(c context.Context, req *admin.InspectOAuthTokenGrantRequest) (*admin.InspectOAuthTokenGrantResponse, error) {
inspection, err := InspectGrant(c, r.Signer, req.Token)
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
resp := &admin.InspectOAuthTokenGrantResponse{
Valid: inspection.Signed && inspection.NonExpired,
Signed: inspection.Signed,
NonExpired: inspection.NonExpired,
InvalidityReason: inspection.InvalidityReason,
}
addInvalidityReason := func(msg string, args ...interface{}) {
reason := fmt.Sprintf(msg, args...)
resp.Valid = false
if resp.InvalidityReason == "" {
resp.InvalidityReason = reason
} else {
resp.InvalidityReason += "; " + reason
}
}
if env, _ := inspection.Envelope.(*tokenserver.OAuthTokenGrantEnvelope); env != nil {
resp.SigningKeyId = env.KeyId
}
// Examine the body, even if the token is expired or unsigned. This helps to
// debug expired or unsigned tokens...
resp.TokenBody, _ = inspection.Body.(*tokenserver.OAuthTokenGrantBody)
if resp.TokenBody != nil {
rules, err := r.Rules(c)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to load service accounts rules")
}
// Always return the rule that matches the service account, even if the
// token itself is not allowed by it (we check it separately below).
rule, err := rules.Rule(c, resp.TokenBody.ServiceAccount, "")
switch {
case status.Code(err) == codes.PermissionDenied:
s, _ := status.FromError(err)
addInvalidityReason("%s", s.Message())
return resp, nil
case err != nil: // usually codes.Internal
return nil, err
default:
resp.MatchingRule = rule.Rule
}
q := &RulesQuery{
ServiceAccount: resp.TokenBody.ServiceAccount,
Rule: rule,
Proxy: identity.Identity(resp.TokenBody.Proxy),
EndUser: identity.Identity(resp.TokenBody.EndUser),
}
switch _, err = rules.Check(c, q); {
case err == nil:
resp.AllowedByRules = true
case status.Code(err) == codes.Internal:
return nil, err // a transient error when checking rules
default: // fatal gRPC error => the rules forbid the token
addInvalidityReason("not allowed by the rules (rev %s)", rules.ConfigRevision())
}
}
return resp, nil
}