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

Unified Diff: tokenserver/appengine/delegation/config.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
« no previous file with comments | « tokenserver/api/token_file.pb.go ('k') | tokenserver/appengine/delegation/config_test.go » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: tokenserver/appengine/delegation/config.go
diff --git a/tokenserver/appengine/delegation/config.go b/tokenserver/appengine/delegation/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..a72738874e0f9aecc9cff41023048272a2101fd9
--- /dev/null
+++ b/tokenserver/appengine/delegation/config.go
@@ -0,0 +1,308 @@
+// Copyright 2016 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 delegation
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/golang/protobuf/proto"
+ "golang.org/x/net/context"
+
+ ds "github.com/luci/gae/service/datastore"
+
+ "github.com/luci/luci-go/common/clock"
+ "github.com/luci/luci-go/common/data/caching/lazyslot"
+ "github.com/luci/luci-go/common/errors"
+ "github.com/luci/luci-go/common/logging"
+ "github.com/luci/luci-go/server/auth/identity"
+
+ "github.com/luci/luci-go/tokenserver/api/admin/v1"
+ "github.com/luci/luci-go/tokenserver/appengine/utils/identityset"
+)
+
+// Requestor is magical token that may be used in the config and requests as
+// a substitute for caller's ID.
+//
+// See config.proto for more info.
+const Requestor = "REQUESTOR"
+
+// DelegationConfig is a singleton entity that stores imported delegation.cfg.
+type DelegationConfig struct {
+ _id int64 `gae:"$id,1"`
+
+ // Revision this config was imported from.
+ Revision string `gae:",noindex"`
+
+ // Config is serialized DelegationPermissions proto message.
+ Config []byte `gae:",noindex"`
+
+ // ParsedConfig is deserialized message stored in Config.
+ ParsedConfig *admin.DelegationPermissions `gae:"-"`
+
+ // rules is preprocessed config rules.
+ //
+ // Used by 'FindMatchingRule', built in 'Initialize'.
+ rules []*delegationRule `gae:"-"`
+
+ // requestors is a union of all 'Requestor' fields in all rules.
+ //
+ // Used by 'IsAuthorizedRequestor', built in 'Initialize'.
+ requestors *identityset.Set `gae:"-"`
+}
+
+// RulesQuery contains parameters to match against the delegation rules.
+//
+// Used by 'FindMatchingRule'.
+type RulesQuery struct {
+ Requestor identity.Identity // who is requesting the token
+ Delegatee identity.Identity // what identity will be delegated/impersonated
+ Audience *identityset.Set // the requested audience set
+ Services *identityset.Set // the requested target services set
+}
+
+// delegationRule is preprocessed admin.DelegationRule message.
+//
+// This object is used by 'FindMatchingRule'.
+type delegationRule struct {
+ rule *admin.DelegationRule // the original unaltered rule proto
+
+ requestors *identityset.Set // matched to RulesQuery.Requestor
+ delegatees *identityset.Set // matched to RulesQuery.Delegatee
+ audience *identityset.Set // matched to RulesQuery.Audience
+ services *identityset.Set // matched to RulesQuery.Services
+
+ addRequestorAsDelegatee bool // if true, add RulesQuery.Requestor to 'delegatees' set
+ addRequestorToAudience bool // if true, add RulesQuery.Requestor to 'audience' set
+}
+
+// procCacheExpiration is how long to keep DelegationConfig in process memory.
+const procCacheExpiration = time.Minute
+
+// FetchDelegationConfig loads DelegationConfig entity from the datastore.
+//
+// Returns empty entity if there is no config stored yet. Doesn't attempt to
+// deserialize 'Config' protobuf field.
+func FetchDelegationConfig(c context.Context) (*DelegationConfig, error) {
+ cfg := &DelegationConfig{}
+ switch err := ds.Get(c, cfg); {
+ case err == ds.ErrNoSuchEntity:
+ return cfg, nil
+ case err != nil:
+ return nil, errors.WrapTransient(err)
+ }
+ return cfg, nil
+}
+
+// DelegationConfigLoader constructs a function that lazy-loads delegation
+// config and keeps it cached in memory, refreshing the cached copy each minute.
+//
+// Used as MintDelegationTokenRPC.ConfigLoader implementation in prod.
+func DelegationConfigLoader() func(context.Context) (*DelegationConfig, error) {
+ slot := lazyslot.Slot{
+ Fetcher: func(c context.Context, prev lazyslot.Value) (lazyslot.Value, error) {
+ newCfg, err := FetchDelegationConfig(c)
+ if err != nil {
+ return lazyslot.Value{}, err
+ }
+
+ // Reuse existing unpacked validated config if the revision didn't change.
+ prevCfg, _ := prev.Value.(*DelegationConfig)
+ if prevCfg != nil && prevCfg.Revision == newCfg.Revision {
+ return lazyslot.Value{
+ Value: prevCfg,
+ Expiration: clock.Now(c).Add(procCacheExpiration),
+ }, nil
+ }
+
+ // An error here can happen if previously validated config is no longer
+ // valid (e.g. if the service code is updated and new code doesn't like
+ // the stored config anymore).
+ //
+ // If this check fails, the service is effectively offline until config is
+ // updated. Presumably, it is better than silently using no longer valid
+ // config.
+ logging.Infof(c, "Using delegation config at ref %s", newCfg.Revision)
+ if err := newCfg.Initialize(); err != nil {
+ logging.Errorf(c, "Existing delegation config is invalid - %s", err)
+ return lazyslot.Value{}, err
+ }
+
+ return lazyslot.Value{
+ Value: newCfg,
+ Expiration: clock.Now(c).Add(procCacheExpiration),
+ }, nil
+ },
+ }
+
+ return func(c context.Context) (*DelegationConfig, error) {
+ val, err := slot.Get(c)
+ if err != nil {
+ return nil, err
+ }
+ return val.Value.(*DelegationConfig), nil
+ }
+}
+
+// Initialize parses the loaded config, initializing the guts of the object.
+func (cfg *DelegationConfig) Initialize() error {
+ parsed := &admin.DelegationPermissions{}
+ if err := proto.Unmarshal(cfg.Config, parsed); err != nil {
+ return err
+ }
+
+ rules := make([]*delegationRule, len(parsed.Rules))
+ requestors := make([]*identityset.Set, len(parsed.Rules))
+
+ for i, msg := range parsed.Rules {
+ rule, err := makeDelegationRule(msg)
+ if err != nil {
+ return err
+ }
+ rules[i] = rule
+ requestors[i] = rule.requestors
+ }
+
+ cfg.ParsedConfig = parsed
+ cfg.rules = rules
+ cfg.requestors = identityset.Union(requestors...)
+
+ return nil
+}
+
+// makeDelegationRule preprocesses admin.DelegationRule proto.
+//
+// It also checks that the rule is passing validation.
+func makeDelegationRule(rule *admin.DelegationRule) (*delegationRule, error) {
+ if merr := ValidateRule(rule); len(merr) != 0 {
+ return nil, merr
+ }
+
+ // The main validation step has been done above. Here we just assert that
+ // everything looks sane (it should). See corresponding chunks of
+ // 'ValidateRule' code.
+ requestors, err := identityset.FromStrings(rule.Requestor, nil)
+ if err != nil {
+ panic(err)
+ }
+ delegatees, err := identityset.FromStrings(rule.AllowedToImpersonate, skipRequestor)
+ if err != nil {
+ panic(err)
+ }
+ audience, err := identityset.FromStrings(rule.AllowedAudience, skipRequestor)
+ if err != nil {
+ panic(err)
+ }
+ services, err := identityset.FromStrings(rule.TargetService, nil)
+ if err != nil {
+ panic(err)
+ }
+
+ return &delegationRule{
+ rule: rule,
+ requestors: requestors,
+ delegatees: delegatees,
+ audience: audience,
+ services: services,
+ addRequestorAsDelegatee: sliceHasString(rule.AllowedToImpersonate, Requestor),
+ addRequestorToAudience: sliceHasString(rule.AllowedAudience, Requestor),
+ }, nil
+}
+
+func skipRequestor(s string) bool {
+ return s == Requestor
+}
+
+func sliceHasString(slice []string, str string) bool {
+ for _, s := range slice {
+ if s == str {
+ return true
+ }
+ }
+ return false
+}
+
+// IsAuthorizedRequestor returns true if the caller belongs to 'requestor' set
+// of at least one rule.
+func (cfg *DelegationConfig) IsAuthorizedRequestor(c context.Context, id identity.Identity) (bool, error) {
+ return cfg.requestors.IsMember(c, id)
+}
+
+// FindMatchingRule finds one and only one rule matching the query.
+//
+// If multiple rules match or none rules match, an error is returned.
+func (cfg *DelegationConfig) FindMatchingRule(c context.Context, q *RulesQuery) (*admin.DelegationRule, error) {
+ var matches []*admin.DelegationRule
+ for _, rule := range cfg.rules {
+ switch yes, err := rule.matchesQuery(c, q); {
+ case err != nil:
+ return nil, err // usually transient
+ case yes:
+ matches = append(matches, rule.rule)
+ }
+ }
+
+ if len(matches) == 0 {
+ return nil, fmt.Errorf("no matching delegation rules in the config")
+ }
+
+ if len(matches) > 1 {
+ names := make([]string, len(matches))
+ for i, m := range matches {
+ names[i] = fmt.Sprintf("%q", m.Name)
+ }
+ return nil, fmt.Errorf(
+ "ambiguous request, multiple delegation rules match (%s)",
+ strings.Join(names, ", "))
+ }
+
+ return matches[0], nil
+}
+
+// matchesQuery returns true if this rule matches the query.
+//
+// See doc in config.proto, DelegationRule for exact description of when this
+// happens. Basically, all sets in rule must be supersets of corresponding sets
+// in RulesQuery.
+//
+// May return transient errors.
+func (rule *delegationRule) matchesQuery(c context.Context, q *RulesQuery) (bool, error) {
+ // Rule's 'requestor' set contains the requestor?
+ switch found, err := rule.requestors.IsMember(c, q.Requestor); {
+ case err != nil:
+ return false, err
+ case !found:
+ return false, nil
+ }
+
+ // Rule's 'delegatee' set contains the identity being delegated/impersonated?
+ allowedDelegatees := rule.delegatees
+ if rule.addRequestorAsDelegatee {
+ allowedDelegatees = identityset.Extend(allowedDelegatees, q.Requestor)
+ }
+ switch found, err := allowedDelegatees.IsMember(c, q.Delegatee); {
+ case err != nil:
+ return false, err
+ case !found:
+ return false, nil
+ }
+
+ // Rule's 'audience' is superset of requested audience?
+ allowedAudience := rule.audience
+ if rule.addRequestorToAudience {
+ allowedAudience = identityset.Extend(allowedAudience, q.Requestor)
+ }
+ if !allowedAudience.IsSuperset(q.Audience) {
+ return false, nil
+ }
+
+ // Rule's allowed targets is superset of requested targets?
+ if !rule.services.IsSuperset(q.Services) {
+ return false, nil
+ }
+
+ return true, nil
+}
« no previous file with comments | « tokenserver/api/token_file.pb.go ('k') | tokenserver/appengine/delegation/config_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698