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