github.com/ilhicas/nomad@v0.11.8/acl/acl.go (about) 1 package acl 2 3 import ( 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 13 var ManagementACL *ACL 14 15 func 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 24 type capabilitySet map[string]struct{} 25 26 func (c capabilitySet) Check(k string) bool { 27 _, ok := c[k] 28 return ok 29 } 30 31 func (c capabilitySet) Set(k string) { 32 c[k] = struct{}{} 33 } 34 35 func (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. 43 type 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. 70 func 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 86 func 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 214 func (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 219 func (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 236 func (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 257 func (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 274 func (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. 299 func (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. 315 func (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 326 type matchingGlob struct { 327 name string 328 difference int 329 capabilitySet capabilitySet 330 } 331 332 func (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 356 func 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 383 func (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 397 func (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 409 func (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 423 func (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 435 func (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 449 func (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 461 func (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 475 func (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 487 func (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 502 func (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 519 func (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. 526 func 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 }