1package acl
2
3import (
4	"fmt"
5	"regexp"
6
7	"github.com/hashicorp/hcl"
8)
9
10const (
11	// The following levels are the only valid values for the `policy = "read"` stanza.
12	// When policies are merged together, the most privilege is granted, except for deny
13	// which always takes precedence and supersedes.
14	PolicyDeny  = "deny"
15	PolicyRead  = "read"
16	PolicyList  = "list"
17	PolicyWrite = "write"
18	PolicyScale = "scale"
19)
20
21const (
22	// The following are the fine-grained capabilities that can be granted within a namespace.
23	// The Policy stanza is a short hand for granting several of these. When capabilities are
24	// combined we take the union of all capabilities. If the deny capability is present, it
25	// takes precedence and overwrites all other capabilities.
26
27	NamespaceCapabilityDeny                 = "deny"
28	NamespaceCapabilityListJobs             = "list-jobs"
29	NamespaceCapabilityReadJob              = "read-job"
30	NamespaceCapabilitySubmitJob            = "submit-job"
31	NamespaceCapabilityDispatchJob          = "dispatch-job"
32	NamespaceCapabilityReadLogs             = "read-logs"
33	NamespaceCapabilityReadFS               = "read-fs"
34	NamespaceCapabilityAllocExec            = "alloc-exec"
35	NamespaceCapabilityAllocNodeExec        = "alloc-node-exec"
36	NamespaceCapabilityAllocLifecycle       = "alloc-lifecycle"
37	NamespaceCapabilitySentinelOverride     = "sentinel-override"
38	NamespaceCapabilityCSIRegisterPlugin    = "csi-register-plugin"
39	NamespaceCapabilityCSIWriteVolume       = "csi-write-volume"
40	NamespaceCapabilityCSIReadVolume        = "csi-read-volume"
41	NamespaceCapabilityCSIListVolume        = "csi-list-volume"
42	NamespaceCapabilityCSIMountVolume       = "csi-mount-volume"
43	NamespaceCapabilityListScalingPolicies  = "list-scaling-policies"
44	NamespaceCapabilityReadScalingPolicy    = "read-scaling-policy"
45	NamespaceCapabilityReadJobScaling       = "read-job-scaling"
46	NamespaceCapabilityScaleJob             = "scale-job"
47	NamespaceCapabilitySubmitRecommendation = "submit-recommendation"
48)
49
50var (
51	validNamespace = regexp.MustCompile("^[a-zA-Z0-9-*]{1,128}$")
52)
53
54const (
55	// The following are the fine-grained capabilities that can be granted for a volume set.
56	// The Policy stanza is a short hand for granting several of these. When capabilities are
57	// combined we take the union of all capabilities. If the deny capability is present, it
58	// takes precedence and overwrites all other capabilities.
59
60	HostVolumeCapabilityDeny           = "deny"
61	HostVolumeCapabilityMountReadOnly  = "mount-readonly"
62	HostVolumeCapabilityMountReadWrite = "mount-readwrite"
63)
64
65var (
66	validVolume = regexp.MustCompile("^[a-zA-Z0-9-*]{1,128}$")
67)
68
69// Policy represents a parsed HCL or JSON policy.
70type Policy struct {
71	Namespaces  []*NamespacePolicy  `hcl:"namespace,expand"`
72	HostVolumes []*HostVolumePolicy `hcl:"host_volume,expand"`
73	Agent       *AgentPolicy        `hcl:"agent"`
74	Node        *NodePolicy         `hcl:"node"`
75	Operator    *OperatorPolicy     `hcl:"operator"`
76	Quota       *QuotaPolicy        `hcl:"quota"`
77	Plugin      *PluginPolicy       `hcl:"plugin"`
78	Raw         string              `hcl:"-"`
79}
80
81// IsEmpty checks to make sure that at least one policy has been set and is not
82// comprised of only a raw policy.
83func (p *Policy) IsEmpty() bool {
84	return len(p.Namespaces) == 0 &&
85		len(p.HostVolumes) == 0 &&
86		p.Agent == nil &&
87		p.Node == nil &&
88		p.Operator == nil &&
89		p.Quota == nil &&
90		p.Plugin == nil
91}
92
93// NamespacePolicy is the policy for a specific namespace
94type NamespacePolicy struct {
95	Name         string `hcl:",key"`
96	Policy       string
97	Capabilities []string
98}
99
100// HostVolumePolicy is the policy for a specific named host volume
101type HostVolumePolicy struct {
102	Name         string `hcl:",key"`
103	Policy       string
104	Capabilities []string
105}
106
107type AgentPolicy struct {
108	Policy string
109}
110
111type NodePolicy struct {
112	Policy string
113}
114
115type OperatorPolicy struct {
116	Policy string
117}
118
119type QuotaPolicy struct {
120	Policy string
121}
122
123type PluginPolicy struct {
124	Policy string
125}
126
127// isPolicyValid makes sure the given string matches one of the valid policies.
128func isPolicyValid(policy string) bool {
129	switch policy {
130	case PolicyDeny, PolicyRead, PolicyWrite, PolicyScale:
131		return true
132	default:
133		return false
134	}
135}
136
137func (p *PluginPolicy) isValid() bool {
138	switch p.Policy {
139	case PolicyDeny, PolicyRead, PolicyList:
140		return true
141	default:
142		return false
143	}
144}
145
146// isNamespaceCapabilityValid ensures the given capability is valid for a namespace policy
147func isNamespaceCapabilityValid(cap string) bool {
148	switch cap {
149	case NamespaceCapabilityDeny, NamespaceCapabilityListJobs, NamespaceCapabilityReadJob,
150		NamespaceCapabilitySubmitJob, NamespaceCapabilityDispatchJob, NamespaceCapabilityReadLogs,
151		NamespaceCapabilityReadFS, NamespaceCapabilityAllocLifecycle,
152		NamespaceCapabilityAllocExec, NamespaceCapabilityAllocNodeExec,
153		NamespaceCapabilityCSIReadVolume, NamespaceCapabilityCSIWriteVolume, NamespaceCapabilityCSIListVolume, NamespaceCapabilityCSIMountVolume, NamespaceCapabilityCSIRegisterPlugin,
154		NamespaceCapabilityListScalingPolicies, NamespaceCapabilityReadScalingPolicy, NamespaceCapabilityReadJobScaling, NamespaceCapabilityScaleJob:
155		return true
156	// Separate the enterprise-only capabilities
157	case NamespaceCapabilitySentinelOverride, NamespaceCapabilitySubmitRecommendation:
158		return true
159	default:
160		return false
161	}
162}
163
164// expandNamespacePolicy provides the equivalent set of capabilities for
165// a namespace policy
166func expandNamespacePolicy(policy string) []string {
167	read := []string{
168		NamespaceCapabilityListJobs,
169		NamespaceCapabilityReadJob,
170		NamespaceCapabilityCSIListVolume,
171		NamespaceCapabilityCSIReadVolume,
172		NamespaceCapabilityReadJobScaling,
173		NamespaceCapabilityListScalingPolicies,
174		NamespaceCapabilityReadScalingPolicy,
175	}
176
177	write := append(read, []string{
178		NamespaceCapabilityScaleJob,
179		NamespaceCapabilitySubmitJob,
180		NamespaceCapabilityDispatchJob,
181		NamespaceCapabilityReadLogs,
182		NamespaceCapabilityReadFS,
183		NamespaceCapabilityAllocExec,
184		NamespaceCapabilityAllocLifecycle,
185		NamespaceCapabilityCSIMountVolume,
186		NamespaceCapabilityCSIWriteVolume,
187		NamespaceCapabilitySubmitRecommendation,
188	}...)
189
190	switch policy {
191	case PolicyDeny:
192		return []string{NamespaceCapabilityDeny}
193	case PolicyRead:
194		return read
195	case PolicyWrite:
196		return write
197	case PolicyScale:
198		return []string{
199			NamespaceCapabilityListScalingPolicies,
200			NamespaceCapabilityReadScalingPolicy,
201			NamespaceCapabilityReadJobScaling,
202			NamespaceCapabilityScaleJob,
203		}
204	default:
205		return nil
206	}
207}
208
209func isHostVolumeCapabilityValid(cap string) bool {
210	switch cap {
211	case HostVolumeCapabilityDeny, HostVolumeCapabilityMountReadOnly, HostVolumeCapabilityMountReadWrite:
212		return true
213	default:
214		return false
215	}
216}
217
218func expandHostVolumePolicy(policy string) []string {
219	switch policy {
220	case PolicyDeny:
221		return []string{HostVolumeCapabilityDeny}
222	case PolicyRead:
223		return []string{HostVolumeCapabilityMountReadOnly}
224	case PolicyWrite:
225		return []string{HostVolumeCapabilityMountReadOnly, HostVolumeCapabilityMountReadWrite}
226	default:
227		return nil
228	}
229}
230
231// Parse is used to parse the specified ACL rules into an
232// intermediary set of policies, before being compiled into
233// the ACL
234func Parse(rules string) (*Policy, error) {
235	// Decode the rules
236	p := &Policy{Raw: rules}
237	if rules == "" {
238		// Hot path for empty rules
239		return p, nil
240	}
241
242	// Attempt to parse
243	if err := hclDecode(p, rules); err != nil {
244		return nil, fmt.Errorf("Failed to parse ACL Policy: %v", err)
245	}
246
247	// At least one valid policy must be specified, we don't want to store only
248	// raw data
249	if p.IsEmpty() {
250		return nil, fmt.Errorf("Invalid policy: %s", p.Raw)
251	}
252
253	// Validate the policy
254	for _, ns := range p.Namespaces {
255		if !validNamespace.MatchString(ns.Name) {
256			return nil, fmt.Errorf("Invalid namespace name: %#v", ns)
257		}
258		if ns.Policy != "" && !isPolicyValid(ns.Policy) {
259			return nil, fmt.Errorf("Invalid namespace policy: %#v", ns)
260		}
261		for _, cap := range ns.Capabilities {
262			if !isNamespaceCapabilityValid(cap) {
263				return nil, fmt.Errorf("Invalid namespace capability '%s': %#v", cap, ns)
264			}
265		}
266
267		// Expand the short hand policy to the capabilities and
268		// add to any existing capabilities
269		if ns.Policy != "" {
270			extraCap := expandNamespacePolicy(ns.Policy)
271			ns.Capabilities = append(ns.Capabilities, extraCap...)
272		}
273	}
274
275	for _, hv := range p.HostVolumes {
276		if !validVolume.MatchString(hv.Name) {
277			return nil, fmt.Errorf("Invalid host volume name: %#v", hv)
278		}
279		if hv.Policy != "" && !isPolicyValid(hv.Policy) {
280			return nil, fmt.Errorf("Invalid host volume policy: %#v", hv)
281		}
282		for _, cap := range hv.Capabilities {
283			if !isHostVolumeCapabilityValid(cap) {
284				return nil, fmt.Errorf("Invalid host volume capability '%s': %#v", cap, hv)
285			}
286		}
287
288		// Expand the short hand policy to the capabilities and
289		// add to any existing capabilities
290		if hv.Policy != "" {
291			extraCap := expandHostVolumePolicy(hv.Policy)
292			hv.Capabilities = append(hv.Capabilities, extraCap...)
293		}
294	}
295
296	if p.Agent != nil && !isPolicyValid(p.Agent.Policy) {
297		return nil, fmt.Errorf("Invalid agent policy: %#v", p.Agent)
298	}
299
300	if p.Node != nil && !isPolicyValid(p.Node.Policy) {
301		return nil, fmt.Errorf("Invalid node policy: %#v", p.Node)
302	}
303
304	if p.Operator != nil && !isPolicyValid(p.Operator.Policy) {
305		return nil, fmt.Errorf("Invalid operator policy: %#v", p.Operator)
306	}
307
308	if p.Quota != nil && !isPolicyValid(p.Quota.Policy) {
309		return nil, fmt.Errorf("Invalid quota policy: %#v", p.Quota)
310	}
311
312	if p.Plugin != nil && !p.Plugin.isValid() {
313		return nil, fmt.Errorf("Invalid plugin policy: %#v", p.Plugin)
314	}
315	return p, nil
316}
317
318// hclDecode wraps hcl.Decode function but handles any unexpected panics
319func hclDecode(p *Policy, rules string) (err error) {
320	defer func() {
321		if rerr := recover(); rerr != nil {
322			err = fmt.Errorf("invalid acl policy: %v", rerr)
323		}
324	}()
325
326	return hcl.Decode(p, rules)
327}
328