1package acl
2
3import (
4	"fmt"
5	"sort"
6	"strings"
7
8	iradix "github.com/hashicorp/go-immutable-radix"
9	glob "github.com/ryanuber/go-glob"
10)
11
12// ManagementACL is a singleton used for management tokens
13var ManagementACL *ACL
14
15func init() {
16	var err error
17	ManagementACL, err = NewACL(true, nil)
18	if err != nil {
19		panic(fmt.Errorf("failed to setup management ACL: %v", err))
20	}
21}
22
23// capabilitySet is a type wrapper to help managing a set of capabilities
24type capabilitySet map[string]struct{}
25
26func (c capabilitySet) Check(k string) bool {
27	_, ok := c[k]
28	return ok
29}
30
31func (c capabilitySet) Set(k string) {
32	c[k] = struct{}{}
33}
34
35func (c capabilitySet) Clear() {
36	for cap := range c {
37		delete(c, cap)
38	}
39}
40
41// ACL object is used to convert a set of policies into a structure that
42// can be efficiently evaluated to determine if an action is allowed.
43type ACL struct {
44	// management tokens are allowed to do anything
45	management bool
46
47	// namespaces maps a namespace to a capabilitySet
48	namespaces *iradix.Tree
49
50	// wildcardNamespaces maps a glob pattern of a namespace to a capabilitySet
51	// We use an iradix for the purposes of ordered iteration.
52	wildcardNamespaces *iradix.Tree
53
54	// hostVolumes maps a named host volume to a capabilitySet
55	hostVolumes *iradix.Tree
56
57	// wildcardHostVolumes maps a glob pattern of host volume names to a capabilitySet
58	// We use an iradix for the purposes of ordered iteration.
59	wildcardHostVolumes *iradix.Tree
60
61	agent    string
62	node     string
63	operator string
64	quota    string
65	plugin   string
66}
67
68// maxPrivilege returns the policy which grants the most privilege
69// This handles the case of Deny always taking maximum precedence.
70func maxPrivilege(a, b string) string {
71	switch {
72	case a == PolicyDeny || b == PolicyDeny:
73		return PolicyDeny
74	case a == PolicyWrite || b == PolicyWrite:
75		return PolicyWrite
76	case a == PolicyRead || b == PolicyRead:
77		return PolicyRead
78	case a == PolicyList || b == PolicyList:
79		return PolicyList
80	default:
81		return ""
82	}
83}
84
85// NewACL compiles a set of policies into an ACL object
86func NewACL(management bool, policies []*Policy) (*ACL, error) {
87	// Hot-path management tokens
88	if management {
89		return &ACL{management: true}, nil
90	}
91
92	// Create the ACL object
93	acl := &ACL{}
94	nsTxn := iradix.New().Txn()
95	wnsTxn := iradix.New().Txn()
96	hvTxn := iradix.New().Txn()
97	whvTxn := iradix.New().Txn()
98
99	for _, policy := range policies {
100	NAMESPACES:
101		for _, ns := range policy.Namespaces {
102			// Should the namespace be matched using a glob?
103			globDefinition := strings.Contains(ns.Name, "*")
104
105			// Check for existing capabilities
106			var capabilities capabilitySet
107
108			if globDefinition {
109				raw, ok := wnsTxn.Get([]byte(ns.Name))
110				if ok {
111					capabilities = raw.(capabilitySet)
112				} else {
113					capabilities = make(capabilitySet)
114					wnsTxn.Insert([]byte(ns.Name), capabilities)
115				}
116			} else {
117				raw, ok := nsTxn.Get([]byte(ns.Name))
118				if ok {
119					capabilities = raw.(capabilitySet)
120				} else {
121					capabilities = make(capabilitySet)
122					nsTxn.Insert([]byte(ns.Name), capabilities)
123				}
124			}
125
126			// Deny always takes precedence
127			if capabilities.Check(NamespaceCapabilityDeny) {
128				continue NAMESPACES
129			}
130
131			// Add in all the capabilities
132			for _, cap := range ns.Capabilities {
133				if cap == NamespaceCapabilityDeny {
134					// Overwrite any existing capabilities
135					capabilities.Clear()
136					capabilities.Set(NamespaceCapabilityDeny)
137					continue NAMESPACES
138				}
139				capabilities.Set(cap)
140			}
141		}
142
143	HOSTVOLUMES:
144		for _, hv := range policy.HostVolumes {
145			// Should the volume be matched using a glob?
146			globDefinition := strings.Contains(hv.Name, "*")
147
148			// Check for existing capabilities
149			var capabilities capabilitySet
150
151			if globDefinition {
152				raw, ok := whvTxn.Get([]byte(hv.Name))
153				if ok {
154					capabilities = raw.(capabilitySet)
155				} else {
156					capabilities = make(capabilitySet)
157					whvTxn.Insert([]byte(hv.Name), capabilities)
158				}
159			} else {
160				raw, ok := hvTxn.Get([]byte(hv.Name))
161				if ok {
162					capabilities = raw.(capabilitySet)
163				} else {
164					capabilities = make(capabilitySet)
165					hvTxn.Insert([]byte(hv.Name), capabilities)
166				}
167			}
168
169			// Deny always takes precedence
170			if capabilities.Check(HostVolumeCapabilityDeny) {
171				continue
172			}
173
174			// Add in all the capabilities
175			for _, cap := range hv.Capabilities {
176				if cap == HostVolumeCapabilityDeny {
177					// Overwrite any existing capabilities
178					capabilities.Clear()
179					capabilities.Set(HostVolumeCapabilityDeny)
180					continue HOSTVOLUMES
181				}
182				capabilities.Set(cap)
183			}
184		}
185
186		// Take the maximum privilege for agent, node, and operator
187		if policy.Agent != nil {
188			acl.agent = maxPrivilege(acl.agent, policy.Agent.Policy)
189		}
190		if policy.Node != nil {
191			acl.node = maxPrivilege(acl.node, policy.Node.Policy)
192		}
193		if policy.Operator != nil {
194			acl.operator = maxPrivilege(acl.operator, policy.Operator.Policy)
195		}
196		if policy.Quota != nil {
197			acl.quota = maxPrivilege(acl.quota, policy.Quota.Policy)
198		}
199		if policy.Plugin != nil {
200			acl.plugin = maxPrivilege(acl.plugin, policy.Plugin.Policy)
201		}
202	}
203
204	// Finalize the namespaces
205	acl.namespaces = nsTxn.Commit()
206	acl.wildcardNamespaces = wnsTxn.Commit()
207	acl.hostVolumes = hvTxn.Commit()
208	acl.wildcardHostVolumes = whvTxn.Commit()
209
210	return acl, nil
211}
212
213// AllowNsOp is shorthand for AllowNamespaceOperation
214func (a *ACL) AllowNsOp(ns string, op string) bool {
215	return a.AllowNamespaceOperation(ns, op)
216}
217
218// AllowNamespaceOperation checks if a given operation is allowed for a namespace
219func (a *ACL) AllowNamespaceOperation(ns string, op string) bool {
220	// Hot path management tokens
221	if a.management {
222		return true
223	}
224
225	// Check for a matching capability set
226	capabilities, ok := a.matchingNamespaceCapabilitySet(ns)
227	if !ok {
228		return false
229	}
230
231	// Check if the capability has been granted
232	return capabilities.Check(op)
233}
234
235// AllowNamespace checks if any operations are allowed for a namespace
236func (a *ACL) AllowNamespace(ns string) bool {
237	// Hot path management tokens
238	if a.management {
239		return true
240	}
241
242	// Check for a matching capability set
243	capabilities, ok := a.matchingNamespaceCapabilitySet(ns)
244	if !ok {
245		return false
246	}
247
248	// Check if the capability has been granted
249	if len(capabilities) == 0 {
250		return false
251	}
252
253	return !capabilities.Check(PolicyDeny)
254}
255
256// AllowHostVolumeOperation checks if a given operation is allowed for a host volume
257func (a *ACL) AllowHostVolumeOperation(hv string, op string) bool {
258	// Hot path management tokens
259	if a.management {
260		return true
261	}
262
263	// Check for a matching capability set
264	capabilities, ok := a.matchingHostVolumeCapabilitySet(hv)
265	if !ok {
266		return false
267	}
268
269	// Check if the capability has been granted
270	return capabilities.Check(op)
271}
272
273// AllowHostVolume checks if any operations are allowed for a HostVolume
274func (a *ACL) AllowHostVolume(ns string) bool {
275	// Hot path management tokens
276	if a.management {
277		return true
278	}
279
280	// Check for a matching capability set
281	capabilities, ok := a.matchingHostVolumeCapabilitySet(ns)
282	if !ok {
283		return false
284	}
285
286	// Check if the capability has been granted
287	if len(capabilities) == 0 {
288		return false
289	}
290
291	return !capabilities.Check(PolicyDeny)
292}
293
294// matchingNamespaceCapabilitySet looks for a capabilitySet that matches the namespace,
295// if no concrete definitions are found, then we return the closest matching
296// glob.
297// The closest matching glob is the one that has the smallest character
298// difference between the namespace and the glob.
299func (a *ACL) matchingNamespaceCapabilitySet(ns string) (capabilitySet, bool) {
300	// Check for a concrete matching capability set
301	raw, ok := a.namespaces.Get([]byte(ns))
302	if ok {
303		return raw.(capabilitySet), true
304	}
305
306	// We didn't find a concrete match, so lets try and evaluate globs.
307	return a.findClosestMatchingGlob(a.wildcardNamespaces, ns)
308}
309
310// matchingHostVolumeCapabilitySet looks for a capabilitySet that matches the host volume name,
311// if no concrete definitions are found, then we return the closest matching
312// glob.
313// The closest matching glob is the one that has the smallest character
314// difference between the volume name and the glob.
315func (a *ACL) matchingHostVolumeCapabilitySet(name string) (capabilitySet, bool) {
316	// Check for a concrete matching capability set
317	raw, ok := a.hostVolumes.Get([]byte(name))
318	if ok {
319		return raw.(capabilitySet), true
320	}
321
322	// We didn't find a concrete match, so lets try and evaluate globs.
323	return a.findClosestMatchingGlob(a.wildcardHostVolumes, name)
324}
325
326type matchingGlob struct {
327	name          string
328	difference    int
329	capabilitySet capabilitySet
330}
331
332func (a *ACL) findClosestMatchingGlob(radix *iradix.Tree, ns string) (capabilitySet, bool) {
333	// First, find all globs that match.
334	matchingGlobs := findAllMatchingWildcards(radix, ns)
335
336	// If none match, let's return.
337	if len(matchingGlobs) == 0 {
338		return capabilitySet{}, false
339	}
340
341	// If a single matches, lets be efficient and return early.
342	if len(matchingGlobs) == 1 {
343		return matchingGlobs[0].capabilitySet, true
344	}
345
346	// Stable sort the matched globs, based on the character difference between
347	// the glob definition and the requested namespace. This allows us to be
348	// more consistent about results based on the policy definition.
349	sort.SliceStable(matchingGlobs, func(i, j int) bool {
350		return matchingGlobs[i].difference <= matchingGlobs[j].difference
351	})
352
353	return matchingGlobs[0].capabilitySet, true
354}
355
356func findAllMatchingWildcards(radix *iradix.Tree, name string) []matchingGlob {
357	var matches []matchingGlob
358
359	nsLen := len(name)
360
361	radix.Root().Walk(func(bk []byte, iv interface{}) bool {
362		k := string(bk)
363		v := iv.(capabilitySet)
364
365		isMatch := glob.Glob(k, name)
366		if isMatch {
367			pair := matchingGlob{
368				name:          k,
369				difference:    nsLen - len(k) + strings.Count(k, glob.GLOB),
370				capabilitySet: v,
371			}
372			matches = append(matches, pair)
373		}
374
375		// We always want to walk the entire tree, never terminate early.
376		return false
377	})
378
379	return matches
380}
381
382// AllowAgentRead checks if read operations are allowed for an agent
383func (a *ACL) AllowAgentRead() bool {
384	switch {
385	case a.management:
386		return true
387	case a.agent == PolicyWrite:
388		return true
389	case a.agent == PolicyRead:
390		return true
391	default:
392		return false
393	}
394}
395
396// AllowAgentWrite checks if write operations are allowed for an agent
397func (a *ACL) AllowAgentWrite() bool {
398	switch {
399	case a.management:
400		return true
401	case a.agent == PolicyWrite:
402		return true
403	default:
404		return false
405	}
406}
407
408// AllowNodeRead checks if read operations are allowed for a node
409func (a *ACL) AllowNodeRead() bool {
410	switch {
411	case a.management:
412		return true
413	case a.node == PolicyWrite:
414		return true
415	case a.node == PolicyRead:
416		return true
417	default:
418		return false
419	}
420}
421
422// AllowNodeWrite checks if write operations are allowed for a node
423func (a *ACL) AllowNodeWrite() bool {
424	switch {
425	case a.management:
426		return true
427	case a.node == PolicyWrite:
428		return true
429	default:
430		return false
431	}
432}
433
434// AllowOperatorRead checks if read operations are allowed for a operator
435func (a *ACL) AllowOperatorRead() bool {
436	switch {
437	case a.management:
438		return true
439	case a.operator == PolicyWrite:
440		return true
441	case a.operator == PolicyRead:
442		return true
443	default:
444		return false
445	}
446}
447
448// AllowOperatorWrite checks if write operations are allowed for a operator
449func (a *ACL) AllowOperatorWrite() bool {
450	switch {
451	case a.management:
452		return true
453	case a.operator == PolicyWrite:
454		return true
455	default:
456		return false
457	}
458}
459
460// AllowQuotaRead checks if read operations are allowed for all quotas
461func (a *ACL) AllowQuotaRead() bool {
462	switch {
463	case a.management:
464		return true
465	case a.quota == PolicyWrite:
466		return true
467	case a.quota == PolicyRead:
468		return true
469	default:
470		return false
471	}
472}
473
474// AllowQuotaWrite checks if write operations are allowed for quotas
475func (a *ACL) AllowQuotaWrite() bool {
476	switch {
477	case a.management:
478		return true
479	case a.quota == PolicyWrite:
480		return true
481	default:
482		return false
483	}
484}
485
486// AllowPluginRead checks if read operations are allowed for all plugins
487func (a *ACL) AllowPluginRead() bool {
488	switch {
489	// ACL is nil only if ACLs are disabled
490	case a == nil:
491		return true
492	case a.management:
493		return true
494	case a.plugin == PolicyRead:
495		return true
496	default:
497		return false
498	}
499}
500
501// AllowPluginList checks if list operations are allowed for all plugins
502func (a *ACL) AllowPluginList() bool {
503	switch {
504	// ACL is nil only if ACLs are disabled
505	case a == nil:
506		return true
507	case a.management:
508		return true
509	case a.plugin == PolicyList:
510		return true
511	case a.plugin == PolicyRead:
512		return true
513	default:
514		return false
515	}
516}
517
518// IsManagement checks if this represents a management token
519func (a *ACL) IsManagement() bool {
520	return a.management
521}
522
523// NamespaceValidator returns a func that wraps ACL.AllowNamespaceOperation in
524// a list of operations. Returns true (allowed) if acls are disabled or if
525// *any* capabilities match.
526func NamespaceValidator(ops ...string) func(*ACL, string) bool {
527	return func(acl *ACL, ns string) bool {
528		// Always allow if ACLs are disabled.
529		if acl == nil {
530			return true
531		}
532
533		for _, op := range ops {
534			if acl.AllowNamespaceOperation(ns, op) {
535				// An operation is allowed, return true
536				return true
537			}
538		}
539
540		// No operations are allowed by this ACL, return false
541		return false
542	}
543}
544