| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. |
| 4 |
| 5 package delegation |
| 6 |
| 7 import ( |
| 8 "fmt" |
| 9 "strings" |
| 10 "time" |
| 11 |
| 12 "github.com/golang/protobuf/proto" |
| 13 "golang.org/x/net/context" |
| 14 |
| 15 ds "github.com/luci/gae/service/datastore" |
| 16 |
| 17 "github.com/luci/luci-go/common/clock" |
| 18 "github.com/luci/luci-go/common/data/caching/lazyslot" |
| 19 "github.com/luci/luci-go/common/errors" |
| 20 "github.com/luci/luci-go/common/logging" |
| 21 "github.com/luci/luci-go/server/auth/identity" |
| 22 |
| 23 "github.com/luci/luci-go/tokenserver/api/admin/v1" |
| 24 "github.com/luci/luci-go/tokenserver/appengine/utils/identityset" |
| 25 ) |
| 26 |
| 27 // Requestor is magical token that may be used in the config and requests as |
| 28 // a substitute for caller's ID. |
| 29 // |
| 30 // See config.proto for more info. |
| 31 const Requestor = "REQUESTOR" |
| 32 |
| 33 // DelegationConfig is a singleton entity that stores imported delegation.cfg. |
| 34 type DelegationConfig struct { |
| 35 _id int64 `gae:"$id,1"` |
| 36 |
| 37 // Revision this config was imported from. |
| 38 Revision string `gae:",noindex"` |
| 39 |
| 40 // Config is serialized DelegationPermissions proto message. |
| 41 Config []byte `gae:",noindex"` |
| 42 |
| 43 // ParsedConfig is deserialized message stored in Config. |
| 44 ParsedConfig *admin.DelegationPermissions `gae:"-"` |
| 45 |
| 46 // rules is preprocessed config rules. |
| 47 // |
| 48 // Used by 'FindMatchingRule', built in 'Initialize'. |
| 49 rules []*delegationRule `gae:"-"` |
| 50 |
| 51 // requestors is a union of all 'Requestor' fields in all rules. |
| 52 // |
| 53 // Used by 'IsAuthorizedRequestor', built in 'Initialize'. |
| 54 requestors *identityset.Set `gae:"-"` |
| 55 } |
| 56 |
| 57 // RulesQuery contains parameters to match against the delegation rules. |
| 58 // |
| 59 // Used by 'FindMatchingRule'. |
| 60 type RulesQuery struct { |
| 61 Requestor identity.Identity // who is requesting the token |
| 62 Delegatee identity.Identity // what identity will be delegated/impersona
ted |
| 63 Audience *identityset.Set // the requested audience set |
| 64 Services *identityset.Set // the requested target services set |
| 65 } |
| 66 |
| 67 // delegationRule is preprocessed admin.DelegationRule message. |
| 68 // |
| 69 // This object is used by 'FindMatchingRule'. |
| 70 type delegationRule struct { |
| 71 rule *admin.DelegationRule // the original unaltered rule proto |
| 72 |
| 73 requestors *identityset.Set // matched to RulesQuery.Requestor |
| 74 delegatees *identityset.Set // matched to RulesQuery.Delegatee |
| 75 audience *identityset.Set // matched to RulesQuery.Audience |
| 76 services *identityset.Set // matched to RulesQuery.Services |
| 77 |
| 78 addRequestorAsDelegatee bool // if true, add RulesQuery.Requestor to 'de
legatees' set |
| 79 addRequestorToAudience bool // if true, add RulesQuery.Requestor to 'au
dience' set |
| 80 } |
| 81 |
| 82 // procCacheExpiration is how long to keep DelegationConfig in process memory. |
| 83 const procCacheExpiration = time.Minute |
| 84 |
| 85 // FetchDelegationConfig loads DelegationConfig entity from the datastore. |
| 86 // |
| 87 // Returns empty entity if there is no config stored yet. Doesn't attempt to |
| 88 // deserialize 'Config' protobuf field. |
| 89 func FetchDelegationConfig(c context.Context) (*DelegationConfig, error) { |
| 90 cfg := &DelegationConfig{} |
| 91 switch err := ds.Get(c, cfg); { |
| 92 case err == ds.ErrNoSuchEntity: |
| 93 return cfg, nil |
| 94 case err != nil: |
| 95 return nil, errors.WrapTransient(err) |
| 96 } |
| 97 return cfg, nil |
| 98 } |
| 99 |
| 100 // DelegationConfigLoader constructs a function that lazy-loads delegation |
| 101 // config and keeps it cached in memory, refreshing the cached copy each minute. |
| 102 // |
| 103 // Used as MintDelegationTokenRPC.ConfigLoader implementation in prod. |
| 104 func DelegationConfigLoader() func(context.Context) (*DelegationConfig, error) { |
| 105 slot := lazyslot.Slot{ |
| 106 Fetcher: func(c context.Context, prev lazyslot.Value) (lazyslot.
Value, error) { |
| 107 newCfg, err := FetchDelegationConfig(c) |
| 108 if err != nil { |
| 109 return lazyslot.Value{}, err |
| 110 } |
| 111 |
| 112 // Reuse existing unpacked validated config if the revis
ion didn't change. |
| 113 prevCfg, _ := prev.Value.(*DelegationConfig) |
| 114 if prevCfg != nil && prevCfg.Revision == newCfg.Revision
{ |
| 115 return lazyslot.Value{ |
| 116 Value: prevCfg, |
| 117 Expiration: clock.Now(c).Add(procCacheEx
piration), |
| 118 }, nil |
| 119 } |
| 120 |
| 121 // An error here can happen if previously validated conf
ig is no longer |
| 122 // valid (e.g. if the service code is updated and new co
de doesn't like |
| 123 // the stored config anymore). |
| 124 // |
| 125 // If this check fails, the service is effectively offli
ne until config is |
| 126 // updated. Presumably, it is better than silently using
no longer valid |
| 127 // config. |
| 128 logging.Infof(c, "Using delegation config at ref %s", ne
wCfg.Revision) |
| 129 if err := newCfg.Initialize(); err != nil { |
| 130 logging.Errorf(c, "Existing delegation config is
invalid - %s", err) |
| 131 return lazyslot.Value{}, err |
| 132 } |
| 133 |
| 134 return lazyslot.Value{ |
| 135 Value: newCfg, |
| 136 Expiration: clock.Now(c).Add(procCacheExpiration
), |
| 137 }, nil |
| 138 }, |
| 139 } |
| 140 |
| 141 return func(c context.Context) (*DelegationConfig, error) { |
| 142 val, err := slot.Get(c) |
| 143 if err != nil { |
| 144 return nil, err |
| 145 } |
| 146 return val.Value.(*DelegationConfig), nil |
| 147 } |
| 148 } |
| 149 |
| 150 // Initialize parses the loaded config, initializing the guts of the object. |
| 151 func (cfg *DelegationConfig) Initialize() error { |
| 152 parsed := &admin.DelegationPermissions{} |
| 153 if err := proto.Unmarshal(cfg.Config, parsed); err != nil { |
| 154 return err |
| 155 } |
| 156 |
| 157 rules := make([]*delegationRule, len(parsed.Rules)) |
| 158 requestors := make([]*identityset.Set, len(parsed.Rules)) |
| 159 |
| 160 for i, msg := range parsed.Rules { |
| 161 rule, err := makeDelegationRule(msg) |
| 162 if err != nil { |
| 163 return err |
| 164 } |
| 165 rules[i] = rule |
| 166 requestors[i] = rule.requestors |
| 167 } |
| 168 |
| 169 cfg.ParsedConfig = parsed |
| 170 cfg.rules = rules |
| 171 cfg.requestors = identityset.Union(requestors...) |
| 172 |
| 173 return nil |
| 174 } |
| 175 |
| 176 // makeDelegationRule preprocesses admin.DelegationRule proto. |
| 177 // |
| 178 // It also checks that the rule is passing validation. |
| 179 func makeDelegationRule(rule *admin.DelegationRule) (*delegationRule, error) { |
| 180 if merr := ValidateRule(rule); len(merr) != 0 { |
| 181 return nil, merr |
| 182 } |
| 183 |
| 184 // The main validation step has been done above. Here we just assert tha
t |
| 185 // everything looks sane (it should). See corresponding chunks of |
| 186 // 'ValidateRule' code. |
| 187 requestors, err := identityset.FromStrings(rule.Requestor, nil) |
| 188 if err != nil { |
| 189 panic(err) |
| 190 } |
| 191 delegatees, err := identityset.FromStrings(rule.AllowedToImpersonate, sk
ipRequestor) |
| 192 if err != nil { |
| 193 panic(err) |
| 194 } |
| 195 audience, err := identityset.FromStrings(rule.AllowedAudience, skipReque
stor) |
| 196 if err != nil { |
| 197 panic(err) |
| 198 } |
| 199 services, err := identityset.FromStrings(rule.TargetService, nil) |
| 200 if err != nil { |
| 201 panic(err) |
| 202 } |
| 203 |
| 204 return &delegationRule{ |
| 205 rule: rule, |
| 206 requestors: requestors, |
| 207 delegatees: delegatees, |
| 208 audience: audience, |
| 209 services: services, |
| 210 addRequestorAsDelegatee: sliceHasString(rule.AllowedToImpersonat
e, Requestor), |
| 211 addRequestorToAudience: sliceHasString(rule.AllowedAudience, Re
questor), |
| 212 }, nil |
| 213 } |
| 214 |
| 215 func skipRequestor(s string) bool { |
| 216 return s == Requestor |
| 217 } |
| 218 |
| 219 func sliceHasString(slice []string, str string) bool { |
| 220 for _, s := range slice { |
| 221 if s == str { |
| 222 return true |
| 223 } |
| 224 } |
| 225 return false |
| 226 } |
| 227 |
| 228 // IsAuthorizedRequestor returns true if the caller belongs to 'requestor' set |
| 229 // of at least one rule. |
| 230 func (cfg *DelegationConfig) IsAuthorizedRequestor(c context.Context, id identit
y.Identity) (bool, error) { |
| 231 return cfg.requestors.IsMember(c, id) |
| 232 } |
| 233 |
| 234 // FindMatchingRule finds one and only one rule matching the query. |
| 235 // |
| 236 // If multiple rules match or none rules match, an error is returned. |
| 237 func (cfg *DelegationConfig) FindMatchingRule(c context.Context, q *RulesQuery)
(*admin.DelegationRule, error) { |
| 238 var matches []*admin.DelegationRule |
| 239 for _, rule := range cfg.rules { |
| 240 switch yes, err := rule.matchesQuery(c, q); { |
| 241 case err != nil: |
| 242 return nil, err // usually transient |
| 243 case yes: |
| 244 matches = append(matches, rule.rule) |
| 245 } |
| 246 } |
| 247 |
| 248 if len(matches) == 0 { |
| 249 return nil, fmt.Errorf("no matching delegation rules in the conf
ig") |
| 250 } |
| 251 |
| 252 if len(matches) > 1 { |
| 253 names := make([]string, len(matches)) |
| 254 for i, m := range matches { |
| 255 names[i] = fmt.Sprintf("%q", m.Name) |
| 256 } |
| 257 return nil, fmt.Errorf( |
| 258 "ambiguous request, multiple delegation rules match (%s)
", |
| 259 strings.Join(names, ", ")) |
| 260 } |
| 261 |
| 262 return matches[0], nil |
| 263 } |
| 264 |
| 265 // matchesQuery returns true if this rule matches the query. |
| 266 // |
| 267 // See doc in config.proto, DelegationRule for exact description of when this |
| 268 // happens. Basically, all sets in rule must be supersets of corresponding sets |
| 269 // in RulesQuery. |
| 270 // |
| 271 // May return transient errors. |
| 272 func (rule *delegationRule) matchesQuery(c context.Context, q *RulesQuery) (bool
, error) { |
| 273 // Rule's 'requestor' set contains the requestor? |
| 274 switch found, err := rule.requestors.IsMember(c, q.Requestor); { |
| 275 case err != nil: |
| 276 return false, err |
| 277 case !found: |
| 278 return false, nil |
| 279 } |
| 280 |
| 281 // Rule's 'delegatee' set contains the identity being delegated/imperson
ated? |
| 282 allowedDelegatees := rule.delegatees |
| 283 if rule.addRequestorAsDelegatee { |
| 284 allowedDelegatees = identityset.Extend(allowedDelegatees, q.Requ
estor) |
| 285 } |
| 286 switch found, err := allowedDelegatees.IsMember(c, q.Delegatee); { |
| 287 case err != nil: |
| 288 return false, err |
| 289 case !found: |
| 290 return false, nil |
| 291 } |
| 292 |
| 293 // Rule's 'audience' is superset of requested audience? |
| 294 allowedAudience := rule.audience |
| 295 if rule.addRequestorToAudience { |
| 296 allowedAudience = identityset.Extend(allowedAudience, q.Requesto
r) |
| 297 } |
| 298 if !allowedAudience.IsSuperset(q.Audience) { |
| 299 return false, nil |
| 300 } |
| 301 |
| 302 // Rule's allowed targets is superset of requested targets? |
| 303 if !rule.services.IsSuperset(q.Services) { |
| 304 return false, nil |
| 305 } |
| 306 |
| 307 return true, nil |
| 308 } |
| OLD | NEW |