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