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

Side by Side Diff: tokenserver/appengine/delegation/config.go

Issue 2413683004: token-server: Delegation config import, validation and evaluation. (Closed)
Patch Set: rebase 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 unified diff | Download patch
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698