| 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
|
| +}
|
|
|