Chromium Code Reviews| 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 // FetchDelegationConfig loads DelegationConfig entity from the datastore. | |
| 83 // | |
| 84 // Returns empty entity if there is no config stored yet. Doesn't attempt to | |
| 85 // deserialize 'Config' protobuf field. | |
| 86 func FetchDelegationConfig(c context.Context) (*DelegationConfig, error) { | |
| 87 cfg := &DelegationConfig{} | |
| 88 switch err := ds.Get(c, cfg); { | |
| 89 case err == ds.ErrNoSuchEntity: | |
| 90 return cfg, nil | |
| 91 case err != nil: | |
| 92 return nil, errors.WrapTransient(err) | |
| 93 } | |
| 94 return cfg, nil | |
| 95 } | |
| 96 | |
| 97 // DelegationConfigLoader constructs a function that lazy-loads delegation | |
| 98 // config and keeps it cached in memory, refreshing the cached copy each minute. | |
| 99 // | |
| 100 // Used as MintDelegationTokenRPC.ConfigLoader implementation in prod. | |
| 101 func DelegationConfigLoader() func(context.Context) (*DelegationConfig, error) { | |
| 102 slot := lazyslot.Slot{ | |
| 103 Fetcher: func(c context.Context, prev lazyslot.Value) (lazyslot. Value, error) { | |
| 104 newCfg, err := FetchDelegationConfig(c) | |
| 105 if err != nil { | |
| 106 return lazyslot.Value{}, err | |
| 107 } | |
| 108 | |
| 109 // Reuse existing unpacked validated config if the revis ion didn't change. | |
| 110 prevCfg, _ := prev.Value.(*DelegationConfig) | |
| 111 if prevCfg != nil && prevCfg.Revision == newCfg.Revision { | |
| 112 return lazyslot.Value{ | |
| 113 Value: prevCfg, | |
| 114 Expiration: clock.Now(c).Add(time.Minute ), | |
| 115 }, nil | |
| 116 } | |
| 117 | |
| 118 // An error here can happen if previously validated conf ig is no longer | |
| 119 // valid (e.g. if the service code is updated and new co de doesn't like | |
| 120 // the stored config anymore). | |
| 121 // | |
| 122 // If this check fails, the service is effectively offli ne until config is | |
| 123 // updated. Presumably, it is better than silently using no longer valid | |
| 124 // config. | |
| 125 logging.Infof(c, "Using delegation config at ref %s", ne wCfg.Revision) | |
| 126 if err := newCfg.Initialize(); err != nil { | |
| 127 logging.Errorf(c, "Existing delegation config is invalid - %s", err) | |
| 128 return lazyslot.Value{}, err | |
| 129 } | |
| 130 | |
| 131 return lazyslot.Value{ | |
| 132 Value: newCfg, | |
| 133 Expiration: clock.Now(c).Add(time.Minute), | |
| 134 }, nil | |
| 135 }, | |
| 136 } | |
| 137 | |
| 138 return func(c context.Context) (*DelegationConfig, error) { | |
| 139 val, err := slot.Get(c) | |
| 140 if err != nil { | |
| 141 return nil, err | |
| 142 } | |
| 143 return val.Value.(*DelegationConfig), nil | |
| 144 } | |
| 145 } | |
| 146 | |
| 147 // Initialize parses the loaded config, initializing the guts of the object. | |
| 148 func (cfg *DelegationConfig) Initialize() error { | |
| 149 cfg.ParsedConfig = &admin.DelegationPermissions{} | |
|
nodir
2016/10/13 22:03:52
consider mutating cfg in the end before returning
Vadim Sh.
2016/10/27 04:12:00
Done.
| |
| 150 if err := proto.Unmarshal(cfg.Config, cfg.ParsedConfig); err != nil { | |
| 151 return err | |
| 152 } | |
| 153 | |
| 154 rules := make([]*delegationRule, len(cfg.ParsedConfig.Rules)) | |
| 155 requestors := make([]*identityset.Set, len(cfg.ParsedConfig.Rules)) | |
| 156 | |
| 157 for i, msg := range cfg.ParsedConfig.Rules { | |
| 158 rule, err := makeDelegationRule(msg) | |
| 159 if err != nil { | |
| 160 return err | |
| 161 } | |
| 162 rules[i] = rule | |
| 163 requestors[i] = rule.requestors | |
| 164 } | |
| 165 | |
| 166 cfg.rules = rules | |
| 167 cfg.requestors = identityset.Union(requestors...) | |
| 168 | |
| 169 return nil | |
| 170 } | |
| 171 | |
| 172 // makeDelegationRule preprocesses admin.DelegationRule proto. | |
| 173 // | |
| 174 // It also checks that the rule is passing validation. | |
| 175 func makeDelegationRule(rule *admin.DelegationRule) (*delegationRule, error) { | |
| 176 if merr := ValidateRule(rule); len(merr) != 0 { | |
| 177 return nil, merr | |
| 178 } | |
| 179 | |
| 180 // The main validation step has been done above. Here we just assert tha t | |
| 181 // everything looks sane (it should). See corresponding chunks of | |
| 182 // 'ValidateRule' code. | |
| 183 requestors, err := identityset.FromStrings(rule.Requestor, nil) | |
| 184 if err != nil { | |
| 185 panic(err) | |
| 186 } | |
| 187 delegatees, err := identityset.FromStrings(rule.AllowedToImpersonate, sk ipRequestor) | |
| 188 if err != nil { | |
| 189 panic(err) | |
| 190 } | |
| 191 audience, err := identityset.FromStrings(rule.AllowedAudience, skipReque stor) | |
| 192 if err != nil { | |
| 193 panic(err) | |
| 194 } | |
| 195 services, err := identityset.FromStrings(rule.TargetService, nil) | |
| 196 if err != nil { | |
| 197 panic(err) | |
| 198 } | |
| 199 | |
| 200 return &delegationRule{ | |
| 201 rule: rule, | |
| 202 requestors: requestors, | |
| 203 delegatees: delegatees, | |
| 204 audience: audience, | |
| 205 services: services, | |
| 206 addRequestorAsDelegatee: sliceHasString(rule.AllowedToImpersonat e, Requestor), | |
| 207 addRequestorToAudience: sliceHasString(rule.AllowedAudience, Re questor), | |
| 208 }, nil | |
| 209 } | |
| 210 | |
| 211 func skipRequestor(s string) bool { | |
| 212 return s == Requestor | |
| 213 } | |
| 214 | |
| 215 func sliceHasString(slice []string, str string) bool { | |
| 216 for _, s := range slice { | |
| 217 if s == str { | |
| 218 return true | |
| 219 } | |
| 220 } | |
| 221 return false | |
| 222 } | |
| 223 | |
| 224 // IsAuthorizedRequestor returns true if the caller belongs to 'requestor' set | |
| 225 // of at least one rule. | |
| 226 func (cfg *DelegationConfig) IsAuthorizedRequestor(c context.Context, id identit y.Identity) (bool, error) { | |
| 227 return cfg.requestors.IsMember(c, id) | |
| 228 } | |
| 229 | |
| 230 // FindMatchingRule finds one and only one rule matching the query. | |
| 231 // | |
| 232 // If multiple rules match or none rules match, an error is returned. | |
| 233 func (cfg *DelegationConfig) FindMatchingRule(c context.Context, q *RulesQuery) (*admin.DelegationRule, error) { | |
| 234 var matches []*admin.DelegationRule | |
| 235 for _, rule := range cfg.rules { | |
| 236 switch yes, err := rule.matchesQuery(c, q); { | |
| 237 case err != nil: | |
| 238 return nil, err // usually transient | |
| 239 case yes: | |
| 240 matches = append(matches, rule.rule) | |
| 241 } | |
| 242 } | |
| 243 | |
| 244 if len(matches) == 0 { | |
| 245 return nil, fmt.Errorf("no matching delegation rules in the conf ig") | |
| 246 } | |
| 247 | |
| 248 if len(matches) > 1 { | |
| 249 names := make([]string, len(matches)) | |
| 250 for i, m := range matches { | |
| 251 names[i] = fmt.Sprintf("%q", m.Name) | |
| 252 } | |
| 253 return nil, fmt.Errorf( | |
| 254 "ambiguous request, multiple delegation rules match (%s) ", | |
| 255 strings.Join(names, ", ")) | |
| 256 } | |
| 257 | |
| 258 return matches[0], nil | |
| 259 } | |
| 260 | |
| 261 // matchesQuery returns true if this rule matches the query. | |
| 262 // | |
| 263 // See doc in config.proto, DelegationRule for exact description of when this | |
| 264 // happen. Basically, all sets in rule must be supersets of corresponding sets | |
|
nodir
2016/10/13 22:03:52
happens
Vadim Sh.
2016/10/27 04:12:00
Done.
| |
| 265 // in RulesQuery. | |
| 266 // | |
| 267 // May return transient errors. | |
| 268 func (rule *delegationRule) matchesQuery(c context.Context, q *RulesQuery) (bool , error) { | |
| 269 // Rule's 'requestor' set contains the requestor? | |
| 270 switch found, err := rule.requestors.IsMember(c, q.Requestor); { | |
| 271 case err != nil: | |
| 272 return false, err | |
| 273 case !found: | |
| 274 return false, nil | |
| 275 } | |
| 276 | |
| 277 // Rule's 'delegatee' set contains the identity being delegated/imperson ated? | |
| 278 allowedDelegatees := rule.delegatees | |
| 279 if rule.addRequestorAsDelegatee { | |
| 280 allowedDelegatees = identityset.Extend(allowedDelegatees, q.Requ estor) | |
| 281 } | |
| 282 switch found, err := allowedDelegatees.IsMember(c, q.Delegatee); { | |
| 283 case err != nil: | |
| 284 return false, err | |
| 285 case !found: | |
| 286 return false, nil | |
| 287 } | |
| 288 | |
| 289 // Rule's 'audience' is superset of requested audience? | |
| 290 allowedAudience := rule.audience | |
| 291 if rule.addRequestorToAudience { | |
| 292 allowedAudience = identityset.Extend(allowedAudience, q.Requesto r) | |
| 293 } | |
| 294 if !allowedAudience.IsSuperset(q.Audience) { | |
| 295 return false, nil | |
| 296 } | |
| 297 | |
| 298 // Rule's allowed targets is superset of requested targets? | |
| 299 if !rule.services.IsSuperset(q.Services) { | |
| 300 return false, nil | |
| 301 } | |
| 302 | |
| 303 return true, nil | |
| 304 } | |
| OLD | NEW |