Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2)

Unified Diff: tokenserver/appengine/delegation/rpc_mint_delegation_token.go

Issue 2413683004: token-server: Delegation config import, validation and evaluation. (Closed)
Patch Set: also check validity_duration Created 4 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: tokenserver/appengine/delegation/rpc_mint_delegation_token.go
diff --git a/tokenserver/appengine/delegation/rpc_mint_delegation_token.go b/tokenserver/appengine/delegation/rpc_mint_delegation_token.go
index fc5a3ab7a7b245acdb3f039c566dd9e5d26954ba..7211253dec413938c2a20ab52fe098498fd3db5b 100644
--- a/tokenserver/appengine/delegation/rpc_mint_delegation_token.go
+++ b/tokenserver/appengine/delegation/rpc_mint_delegation_token.go
@@ -5,12 +5,25 @@
package delegation
import (
+ "fmt"
+ "strings"
+ "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/errors"
+ "github.com/luci/luci-go/common/logging"
+ "github.com/luci/luci-go/server/auth"
+ "github.com/luci/luci-go/server/auth/identity"
"github.com/luci/luci-go/server/auth/signing"
- minter "github.com/luci/luci-go/tokenserver/api/minter/v1"
+
+ admin "github.com/luci/luci-go/tokenserver/api/admin/v1"
+ "github.com/luci/luci-go/tokenserver/api/minter/v1"
+ "github.com/luci/luci-go/tokenserver/appengine/utils/identityset"
)
// MintDelegationTokenRPC implements TokenMinter.MintDelegationToken RPC method.
@@ -19,9 +32,236 @@ type MintDelegationTokenRPC struct {
//
// In prod it is gaesigner.Signer.
Signer signing.Signer
+
+ // ConfigLoader loads delegation config on demand.
+ //
+ // In prod it is DelegationConfigLoader.
+ ConfigLoader func(context.Context) (*DelegationConfig, error)
+
+ // mintMock call is used in tests.
+ //
+ // In prod it is 'mint'
+ mintMock func(context.Context, *minter.MintDelegationTokenRequest,
+ *RulesQuery, *admin.DelegationRule) (*minter.MintDelegationTokenResponse, error)
}
// MintDelegationToken generates a new bearer delegation token.
func (r *MintDelegationTokenRPC) MintDelegationToken(c context.Context, req *minter.MintDelegationTokenRequest) (*minter.MintDelegationTokenResponse, error) {
+ state := auth.GetState(c)
+
+ // Dump the whole request and relevant auth state to the debug log.
+ if logging.IsLogging(c, logging.Debug) {
+ m := jsonpb.Marshaler{Indent: " "}
+ dump, _ := m.MarshalToString(req)
+ logging.Debugf(c, "PeerIdentity: %s", state.PeerIdentity())
+ logging.Debugf(c, "MintDelegationTokenRequest:\n%s", dump)
+ }
+
+ // Validate the request authentication context: not an anonymous call, no
+ // delegation is used.
+ callerID := state.User().Identity
+ if callerID != state.PeerIdentity() {
+ logging.Errorf(c, "Trying to use delegation, it's forbidden")
+ return nil, grpc.Errorf(codes.PermissionDenied, "delegation is forbidden for this API call")
+ }
+ if callerID == identity.AnonymousIdentity {
+ logging.Errorf(c, "Unauthenticated request")
+ return nil, grpc.Errorf(codes.Unauthenticated, "authentication required")
+ }
+
+ cfg, err := r.ConfigLoader(c)
+ if err != nil {
+ // Don't put error details in the message, it may be returned to
+ // unauthorized callers. ConfigLoader logs the error already.
+ return nil, grpc.Errorf(codes.Internal, "failed to load delegation config")
+ }
+
+ // Make sure the caller is mentioned in the config before doing anything else.
+ // This rejects unauthorized callers early. Passing this check doesn't mean
+ // that there's a matching rule though, so the request still can be rejected
+ // later.
+ switch ok, err := cfg.IsAuthorizedRequestor(c, callerID); {
+ case err != nil:
+ logging.WithError(err).Errorf(c, "IsAuthorizedRequestor failed")
+ return nil, grpc.Errorf(codes.Internal, "failed to check authorization")
+ case !ok:
+ logging.Errorf(c, "Didn't pass initial authorization")
+ return nil, grpc.Errorf(codes.PermissionDenied, "not authorized")
+ }
+
+ // Validate requested token lifetime. It's not part of the rules query.
+ if req.ValidityDuration == 0 {
+ req.ValidityDuration = 3600
+ }
+ if req.ValidityDuration < 0 {
+ err = fmt.Errorf("invalid 'validity_duration' (%d)", req.ValidityDuration)
+ logging.WithError(err).Errorf(c, "Bad request")
+ return nil, grpc.Errorf(codes.InvalidArgument, "bad request - %s", err)
+ }
+
+ // Validate and normalize the request. This may do relatively expensive calls
+ // to resolve "https://<service-url>" entries to "service:<id>" entries.
+ query, err := buildRulesQuery(c, req, callerID)
+ if err != nil {
+ if errors.IsTransient(err) {
+ logging.WithError(err).Errorf(c, "buildRulesQuery failed")
+ return nil, grpc.Errorf(codes.Internal, "failure when resolving target service ID - %s", err)
+ }
+ logging.WithError(err).Errorf(c, "Bad request")
+ return nil, grpc.Errorf(codes.InvalidArgument, "bad request - %s", err)
+ }
+
+ // Consult the config to find the rule that allows this operation (if any).
+ rule, err := cfg.FindMatchingRule(c, query)
+ if err != nil {
+ if errors.IsTransient(err) {
+ logging.WithError(err).Errorf(c, "FindMatchingRule failed")
+ return nil, grpc.Errorf(codes.Internal, "failure when checking rules - %s", err)
+ }
+ logging.WithError(err).Errorf(c, "Didn't pass rules check")
+ return nil, grpc.Errorf(codes.PermissionDenied, "forbidden - %s", err)
+ }
+ logging.Infof(c, "Found the matching rule %q in the config rev %s", rule.Name, cfg.Revision)
+
+ // Make sure the requested token lifetime is allowed by the rule.
+ if req.ValidityDuration > rule.MaxValidityDuration {
+ err = fmt.Errorf(
+ "the requested validity duration (%d sec) exceeds the maximum allowed one (%d sec)",
+ req.ValidityDuration, rule.MaxValidityDuration)
+ logging.WithError(err).Errorf(c, "Validity duration check didn't pass")
+ return nil, grpc.Errorf(codes.PermissionDenied, "forbidden - %s", err)
+ }
+
+ if r.mintMock != nil {
+ return r.mintMock(c, req, query, rule)
+ }
+ return r.mint(c, req, query, rule)
+}
+
+// mint is called to make the token after the request has been authorized.
+func (r *MintDelegationTokenRPC) mint(
+ c context.Context, req *minter.MintDelegationTokenRequest,
+ query *RulesQuery, rule *admin.DelegationRule) (*minter.MintDelegationTokenResponse, error) {
+ // TODO(vadimsh): Make the token, record it in the audit log.
return nil, grpc.Errorf(codes.Unimplemented, "Not implemented yet")
}
+
+// buildRulesQuery validates the request, extracts and normalizes relevant
+// fields into RulesQuery object.
+//
+// May return transient errors.
+func buildRulesQuery(c context.Context, req *minter.MintDelegationTokenRequest, requestor identity.Identity) (*RulesQuery, error) {
+ // Validate 'delegated_identity'.
+ var err error
+ var delegatee identity.Identity
+ if req.DelegatedIdentity == "" {
+ return nil, fmt.Errorf("'delegated_identity' is required")
+ }
+ if req.DelegatedIdentity == Requestor {
+ delegatee = requestor // the requestor is delegating its own identity
+ } else {
+ if delegatee, err = identity.MakeIdentity(req.DelegatedIdentity); err != nil {
+ return nil, fmt.Errorf("bad 'delegated_identity' - %s", err)
+ }
+ }
+
+ // Validate 'audience', convert it into a set.
+ if len(req.Audience) == 0 {
+ return nil, fmt.Errorf("'audience' is required")
+ }
+ audienceSet, err := identityset.FromStrings(req.Audience, skipRequestor)
+ if err != nil {
+ return nil, fmt.Errorf("bad 'audience' - %s", err)
+ }
+ if sliceHasString(req.Audience, Requestor) {
+ audienceSet.AddIdentity(requestor)
+ }
+
+ // Split 'services' into two lists: URLs and everything else (which is
+ // "service:..." and "*" presumably, validated below).
+ if len(req.Services) == 0 {
+ return nil, fmt.Errorf("'services' is required")
+ }
+ urls := make([]string, 0, len(req.Services))
+ rest := make([]string, 0, len(req.Services))
+ for _, srv := range req.Services {
+ if strings.HasPrefix(srv, "https://") {
+ urls = append(urls, srv)
+ } else {
+ rest = append(rest, srv)
+ }
+ }
+
+ // Convert the list into a set, verify it contains only services (or "*").
+ servicesSet, err := identityset.FromStrings(rest, nil)
+ if err != nil {
+ return nil, fmt.Errorf("bad 'services' - %s", err)
+ }
+ if len(servicesSet.Groups) != 0 {
+ return nil, fmt.Errorf("bad 'services' - can't specify groups")
+ }
+ for ident := range servicesSet.IDs {
+ if ident.Kind() != identity.Service {
+ return nil, fmt.Errorf("bad 'services' - %q is not a service ID", ident)
+ }
+ }
+
+ // Resolve URLs into app IDs. This may involve URL fetch calls (if the cache
+ // is cold), so skip this expensive call if already specifying the universal
+ // set of all services.
+ if !servicesSet.All && len(urls) != 0 {
+ if err = resolveServiceIDs(c, urls, servicesSet); err != nil {
+ return nil, err
+ }
+ }
+
+ // Done!
+ return &RulesQuery{
+ Requestor: requestor,
+ Delegatee: delegatee,
+ Audience: audienceSet,
+ Services: servicesSet,
+ }, nil
+}
+
+// fetchLUCIServiceIdentity is replaced in tests.
+var fetchLUCIServiceIdentity = signing.FetchLUCIServiceIdentity
+
+// resolveServiceIDs takes a bunch of service URLs and resolves them to
+// 'service:<app-id>' identities, putting them in the 'out' set.
+//
+// May return transient errors.
+func resolveServiceIDs(c context.Context, urls []string, out *identityset.Set) error {
+ // URL fetch calls below should be extra fast. If they get stuck, something is
+ // horribly wrong, better to abort soon.
+ c, abort := clock.WithTimeout(c, 5*time.Second)
+ defer abort()
+
+ type Result struct {
+ URL string
+ ID identity.Identity
+ Err error
+ }
+
+ ch := make(chan Result, len(urls))
+
+ for _, url := range urls {
+ go func(url string) {
+ id, err := fetchLUCIServiceIdentity(c, url)
+ ch <- Result{url, id, err}
+ }(url)
+ }
+
+ for i := 0; i < len(urls); i++ {
+ result := <-ch
+ if result.Err != nil {
+ if errors.IsTransient(result.Err) {
+ return result.Err
+ }
+ return fmt.Errorf("could not resolve %q to service ID - %s", result.URL, result.Err)
+ }
+ out.AddIdentity(result.ID)
+ }
+
+ return nil
+}

Powered by Google App Engine
This is Rietveld 408576698