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