1cf28996e7
ACL policies can be associated with a job so that the job's Workload Identity can have expanded access to other policy objects, including other variables. Policies set on the variables the job automatically has access to were ignored, but this includes policies with `deny` capabilities. Additionally, when resolving claims for a workload identity without any attached policies, the `ResolveClaims` method returned a `nil` ACL object, which is treated similarly to a management token. While this was safe in Nomad 1.4.x, when the workload identity token was exposed to the task via the `identity` block, this allows a user with `submit-job` capabilities to escalate their privileges. We originally implemented automatic workload access to Variables as a separate code path in the Variables RPC endpoint so that we don't have to generate on-the-fly policies that blow up the ACL policy cache. This is fairly brittle but also the behavior around wildcard paths in policies different from the rest of our ACL polices, which is hard to reason about. Add an `ACLClaim` parameter to the `AllowVariableOperation` method so that we can push all this logic into the `acl` package and the behavior can be consistent. This will allow a `deny` policy to override automatic access (and probably speed up checks of non-automatic variable access).
716 lines
18 KiB
Go
716 lines
18 KiB
Go
package acl
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
iradix "github.com/hashicorp/go-immutable-radix/v2"
|
|
glob "github.com/ryanuber/go-glob"
|
|
)
|
|
|
|
// Redefine this value from structs to avoid circular dependency.
|
|
const AllNamespacesSentinel = "*"
|
|
|
|
// ManagementACL is a singleton used for management tokens
|
|
var ManagementACL *ACL
|
|
|
|
func init() {
|
|
var err error
|
|
ManagementACL, err = NewACL(true, nil)
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to setup management ACL: %v", err))
|
|
}
|
|
}
|
|
|
|
// capabilitySet is a type wrapper to help managing a set of capabilities
|
|
type capabilitySet map[string]struct{}
|
|
|
|
func (c capabilitySet) Check(k string) bool {
|
|
_, ok := c[k]
|
|
return ok
|
|
}
|
|
|
|
func (c capabilitySet) Set(k string) {
|
|
c[k] = struct{}{}
|
|
}
|
|
|
|
func (c capabilitySet) Clear() {
|
|
for cap := range c {
|
|
delete(c, cap)
|
|
}
|
|
}
|
|
|
|
// ACL object is used to convert a set of policies into a structure that
|
|
// can be efficiently evaluated to determine if an action is allowed.
|
|
type ACL struct {
|
|
// management tokens are allowed to do anything
|
|
management bool
|
|
|
|
// namespaces maps a namespace to a capabilitySet
|
|
namespaces *iradix.Tree[capabilitySet]
|
|
|
|
// wildcardNamespaces maps a glob pattern of a namespace to a capabilitySet
|
|
// We use an iradix for the purposes of ordered iteration.
|
|
wildcardNamespaces *iradix.Tree[capabilitySet]
|
|
|
|
// hostVolumes maps a named host volume to a capabilitySet
|
|
hostVolumes *iradix.Tree[capabilitySet]
|
|
|
|
// wildcardHostVolumes maps a glob pattern of host volume names to a capabilitySet
|
|
// We use an iradix for the purposes of ordered iteration.
|
|
wildcardHostVolumes *iradix.Tree[capabilitySet]
|
|
|
|
variables *iradix.Tree[capabilitySet]
|
|
wildcardVariables *iradix.Tree[capabilitySet]
|
|
|
|
agent string
|
|
node string
|
|
operator string
|
|
quota string
|
|
plugin string
|
|
}
|
|
|
|
// maxPrivilege returns the policy which grants the most privilege
|
|
// This handles the case of Deny always taking maximum precedence.
|
|
func maxPrivilege(a, b string) string {
|
|
switch {
|
|
case a == PolicyDeny || b == PolicyDeny:
|
|
return PolicyDeny
|
|
case a == PolicyWrite || b == PolicyWrite:
|
|
return PolicyWrite
|
|
case a == PolicyRead || b == PolicyRead:
|
|
return PolicyRead
|
|
case a == PolicyList || b == PolicyList:
|
|
return PolicyList
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// NewACL compiles a set of policies into an ACL object
|
|
func NewACL(management bool, policies []*Policy) (*ACL, error) {
|
|
// Hot-path management tokens
|
|
if management {
|
|
return &ACL{management: true}, nil
|
|
}
|
|
|
|
// Create the ACL object
|
|
acl := &ACL{}
|
|
nsTxn := iradix.New[capabilitySet]().Txn()
|
|
wnsTxn := iradix.New[capabilitySet]().Txn()
|
|
hvTxn := iradix.New[capabilitySet]().Txn()
|
|
whvTxn := iradix.New[capabilitySet]().Txn()
|
|
svTxn := iradix.New[capabilitySet]().Txn()
|
|
wsvTxn := iradix.New[capabilitySet]().Txn()
|
|
|
|
for _, policy := range policies {
|
|
NAMESPACES:
|
|
for _, ns := range policy.Namespaces {
|
|
// Should the namespace be matched using a glob?
|
|
globDefinition := strings.Contains(ns.Name, "*")
|
|
|
|
// Check for existing capabilities
|
|
var capabilities capabilitySet
|
|
|
|
if globDefinition {
|
|
raw, ok := wnsTxn.Get([]byte(ns.Name))
|
|
if ok {
|
|
capabilities = raw
|
|
} else {
|
|
capabilities = make(capabilitySet)
|
|
wnsTxn.Insert([]byte(ns.Name), capabilities)
|
|
}
|
|
} else {
|
|
raw, ok := nsTxn.Get([]byte(ns.Name))
|
|
if ok {
|
|
capabilities = raw
|
|
} else {
|
|
capabilities = make(capabilitySet)
|
|
nsTxn.Insert([]byte(ns.Name), capabilities)
|
|
}
|
|
}
|
|
|
|
if ns.Variables != nil {
|
|
for _, pathPolicy := range ns.Variables.Paths {
|
|
key := []byte(ns.Name + "\x00" + pathPolicy.PathSpec)
|
|
var svCapabilities capabilitySet
|
|
if globDefinition || strings.Contains(pathPolicy.PathSpec, "*") {
|
|
raw, ok := wsvTxn.Get(key)
|
|
if ok {
|
|
svCapabilities = raw
|
|
} else {
|
|
svCapabilities = make(capabilitySet)
|
|
}
|
|
wsvTxn.Insert(key, svCapabilities)
|
|
} else {
|
|
raw, ok := svTxn.Get(key)
|
|
if ok {
|
|
svCapabilities = raw
|
|
} else {
|
|
svCapabilities = make(capabilitySet)
|
|
}
|
|
svTxn.Insert(key, svCapabilities)
|
|
}
|
|
for _, cap := range pathPolicy.Capabilities {
|
|
svCapabilities.Set(cap)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deny always takes precedence
|
|
if capabilities.Check(NamespaceCapabilityDeny) {
|
|
continue NAMESPACES
|
|
}
|
|
|
|
// Add in all the capabilities
|
|
for _, cap := range ns.Capabilities {
|
|
if cap == NamespaceCapabilityDeny {
|
|
// Overwrite any existing capabilities
|
|
capabilities.Clear()
|
|
capabilities.Set(NamespaceCapabilityDeny)
|
|
continue NAMESPACES
|
|
}
|
|
capabilities.Set(cap)
|
|
}
|
|
}
|
|
|
|
HOSTVOLUMES:
|
|
for _, hv := range policy.HostVolumes {
|
|
// Should the volume be matched using a glob?
|
|
globDefinition := strings.Contains(hv.Name, "*")
|
|
|
|
// Check for existing capabilities
|
|
var capabilities capabilitySet
|
|
|
|
if globDefinition {
|
|
raw, ok := whvTxn.Get([]byte(hv.Name))
|
|
if ok {
|
|
capabilities = raw
|
|
} else {
|
|
capabilities = make(capabilitySet)
|
|
whvTxn.Insert([]byte(hv.Name), capabilities)
|
|
}
|
|
} else {
|
|
raw, ok := hvTxn.Get([]byte(hv.Name))
|
|
if ok {
|
|
capabilities = raw
|
|
} else {
|
|
capabilities = make(capabilitySet)
|
|
hvTxn.Insert([]byte(hv.Name), capabilities)
|
|
}
|
|
}
|
|
|
|
// Deny always takes precedence
|
|
if capabilities.Check(HostVolumeCapabilityDeny) {
|
|
continue
|
|
}
|
|
|
|
// Add in all the capabilities
|
|
for _, cap := range hv.Capabilities {
|
|
if cap == HostVolumeCapabilityDeny {
|
|
// Overwrite any existing capabilities
|
|
capabilities.Clear()
|
|
capabilities.Set(HostVolumeCapabilityDeny)
|
|
continue HOSTVOLUMES
|
|
}
|
|
capabilities.Set(cap)
|
|
}
|
|
}
|
|
|
|
// Take the maximum privilege for agent, node, and operator
|
|
if policy.Agent != nil {
|
|
acl.agent = maxPrivilege(acl.agent, policy.Agent.Policy)
|
|
}
|
|
if policy.Node != nil {
|
|
acl.node = maxPrivilege(acl.node, policy.Node.Policy)
|
|
}
|
|
if policy.Operator != nil {
|
|
acl.operator = maxPrivilege(acl.operator, policy.Operator.Policy)
|
|
}
|
|
if policy.Quota != nil {
|
|
acl.quota = maxPrivilege(acl.quota, policy.Quota.Policy)
|
|
}
|
|
if policy.Plugin != nil {
|
|
acl.plugin = maxPrivilege(acl.plugin, policy.Plugin.Policy)
|
|
}
|
|
}
|
|
|
|
// Finalize the namespaces
|
|
acl.namespaces = nsTxn.Commit()
|
|
acl.wildcardNamespaces = wnsTxn.Commit()
|
|
acl.hostVolumes = hvTxn.Commit()
|
|
acl.wildcardHostVolumes = whvTxn.Commit()
|
|
acl.variables = svTxn.Commit()
|
|
acl.wildcardVariables = wsvTxn.Commit()
|
|
|
|
return acl, nil
|
|
}
|
|
|
|
// AllowNsOp is shorthand for AllowNamespaceOperation
|
|
func (a *ACL) AllowNsOp(ns string, op string) bool {
|
|
return a.AllowNamespaceOperation(ns, op)
|
|
}
|
|
|
|
// AllowNsOpFunc is a helper that returns a function that can be used to check
|
|
// namespace permissions.
|
|
func (a *ACL) AllowNsOpFunc(ops ...string) func(string) bool {
|
|
return func(ns string) bool {
|
|
return NamespaceValidator(ops...)(a, ns)
|
|
}
|
|
}
|
|
|
|
// AllowNamespaceOperation checks if a given operation is allowed for a namespace.
|
|
func (a *ACL) AllowNamespaceOperation(ns string, op string) bool {
|
|
// Hot path if ACL is not enabled.
|
|
if a == nil {
|
|
return true
|
|
}
|
|
|
|
// Hot path management tokens
|
|
if a.management {
|
|
return true
|
|
}
|
|
|
|
// If using the all namespaces wildcard, allow if any namespace allows the
|
|
// operation.
|
|
if ns == AllNamespacesSentinel && a.anyNamespaceAllowsOp(op) {
|
|
return true
|
|
}
|
|
|
|
// Check for a matching capability set
|
|
capabilities, ok := a.matchingNamespaceCapabilitySet(ns)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Check if the capability has been granted
|
|
return capabilities.Check(op)
|
|
}
|
|
|
|
// AllowNamespace checks if any operations are allowed for a namespace
|
|
func (a *ACL) AllowNamespace(ns string) bool {
|
|
// Hot path if ACL is not enabled.
|
|
if a == nil {
|
|
return true
|
|
}
|
|
|
|
// Hot path management tokens
|
|
if a.management {
|
|
return true
|
|
}
|
|
|
|
// If using the all namespaces wildcard, allow if any namespace allows any
|
|
// operation.
|
|
if ns == AllNamespacesSentinel && a.anyNamespaceAllowsAnyOp() {
|
|
return true
|
|
}
|
|
|
|
// Check for a matching capability set
|
|
capabilities, ok := a.matchingNamespaceCapabilitySet(ns)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Check if the capability has been granted
|
|
if len(capabilities) == 0 {
|
|
return false
|
|
}
|
|
|
|
return !capabilities.Check(PolicyDeny)
|
|
}
|
|
|
|
// AllowHostVolumeOperation checks if a given operation is allowed for a host volume
|
|
func (a *ACL) AllowHostVolumeOperation(hv string, op string) bool {
|
|
// Hot path management tokens
|
|
if a.management {
|
|
return true
|
|
}
|
|
|
|
// Check for a matching capability set
|
|
capabilities, ok := a.matchingHostVolumeCapabilitySet(hv)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Check if the capability has been granted
|
|
return capabilities.Check(op)
|
|
}
|
|
|
|
// AllowHostVolume checks if any operations are allowed for a HostVolume
|
|
func (a *ACL) AllowHostVolume(ns string) bool {
|
|
// Hot path management tokens
|
|
if a.management {
|
|
return true
|
|
}
|
|
|
|
// Check for a matching capability set
|
|
capabilities, ok := a.matchingHostVolumeCapabilitySet(ns)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Check if the capability has been granted
|
|
if len(capabilities) == 0 {
|
|
return false
|
|
}
|
|
|
|
return !capabilities.Check(PolicyDeny)
|
|
}
|
|
|
|
func (a *ACL) AllowVariableOperation(ns, path, op string, claim *ACLClaim) bool {
|
|
if a.management {
|
|
return true
|
|
}
|
|
|
|
// Check for a matching capability set
|
|
capabilities, ok := a.matchingVariablesCapabilitySet(ns, path, claim)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
return capabilities.Check(op)
|
|
}
|
|
|
|
type ACLClaim struct {
|
|
Namespace string
|
|
Job string
|
|
Group string
|
|
Task string
|
|
}
|
|
|
|
// AllowVariableSearch is a very loose check that the token has *any* access to
|
|
// a variables path for the namespace, with an expectation that the actual
|
|
// search result will be filtered by specific paths
|
|
func (a *ACL) AllowVariableSearch(ns string) bool {
|
|
if a.management {
|
|
return true
|
|
}
|
|
iter := a.variables.Root().Iterator()
|
|
iter.SeekPrefix([]byte(ns))
|
|
_, _, ok := iter.Next()
|
|
if ok {
|
|
return true
|
|
}
|
|
|
|
iter = a.wildcardVariables.Root().Iterator()
|
|
iter.SeekPrefix([]byte(ns))
|
|
_, _, ok = iter.Next()
|
|
return ok
|
|
}
|
|
|
|
// matchingNamespaceCapabilitySet looks for a capabilitySet that matches the namespace,
|
|
// if no concrete definitions are found, then we return the closest matching
|
|
// glob.
|
|
// The closest matching glob is the one that has the smallest character
|
|
// difference between the namespace and the glob.
|
|
func (a *ACL) matchingNamespaceCapabilitySet(ns string) (capabilitySet, bool) {
|
|
// Check for a concrete matching capability set
|
|
raw, ok := a.namespaces.Get([]byte(ns))
|
|
if ok {
|
|
return raw, true
|
|
}
|
|
|
|
// We didn't find a concrete match, so lets try and evaluate globs.
|
|
return a.findClosestMatchingGlob(a.wildcardNamespaces, ns)
|
|
}
|
|
|
|
// anyNamespaceAllowsOp returns true if any namespace in ACL object allows the
|
|
// given operation.
|
|
func (a *ACL) anyNamespaceAllowsOp(op string) bool {
|
|
return a.anyNamespaceAllows(func(c capabilitySet) bool {
|
|
return c.Check(op)
|
|
})
|
|
}
|
|
|
|
// anyNamespaceAllowsAnyOp returns true if any namespace in ACL object allows
|
|
// at least one operation.
|
|
func (a *ACL) anyNamespaceAllowsAnyOp() bool {
|
|
return a.anyNamespaceAllows(func(c capabilitySet) bool {
|
|
return len(c) > 0 && !c.Check(PolicyDeny)
|
|
})
|
|
}
|
|
|
|
// anyNamespaceAllows returns true if the callback cb returns true for any
|
|
// namespace operation of the ACL object.
|
|
func (a *ACL) anyNamespaceAllows(cb func(capabilitySet) bool) bool {
|
|
allow := false
|
|
|
|
checkFn := func(_ []byte, v capabilitySet) bool {
|
|
allow = cb(v)
|
|
return allow
|
|
}
|
|
|
|
a.namespaces.Root().Walk(checkFn)
|
|
if allow {
|
|
return true
|
|
}
|
|
|
|
a.wildcardNamespaces.Root().Walk(checkFn)
|
|
return allow
|
|
}
|
|
|
|
// matchingHostVolumeCapabilitySet looks for a capabilitySet that matches the host volume name,
|
|
// if no concrete definitions are found, then we return the closest matching
|
|
// glob.
|
|
// The closest matching glob is the one that has the smallest character
|
|
// difference between the volume name and the glob.
|
|
func (a *ACL) matchingHostVolumeCapabilitySet(name string) (capabilitySet, bool) {
|
|
// Check for a concrete matching capability set
|
|
raw, ok := a.hostVolumes.Get([]byte(name))
|
|
if ok {
|
|
return raw, true
|
|
}
|
|
|
|
// We didn't find a concrete match, so lets try and evaluate globs.
|
|
return a.findClosestMatchingGlob(a.wildcardHostVolumes, name)
|
|
}
|
|
|
|
var workloadVariablesCapabilitySet = capabilitySet{"read": struct{}{}, "list": struct{}{}}
|
|
|
|
// matchingVariablesCapabilitySet looks for a capabilitySet in the following order:
|
|
// - matching the namespace and path from a policy
|
|
// - automatic access based on the claim
|
|
// - closest matching glob
|
|
//
|
|
// The closest matching glob is the one that has the smallest character
|
|
// difference between the namespace and the glob.
|
|
func (a *ACL) matchingVariablesCapabilitySet(ns, path string, claim *ACLClaim) (capabilitySet, bool) {
|
|
// Check for a concrete matching capability set
|
|
capSet, ok := a.variables.Get([]byte(ns + "\x00" + path))
|
|
if ok {
|
|
return capSet, true
|
|
}
|
|
if claim != nil && ns == claim.Namespace {
|
|
switch path {
|
|
case "nomad/jobs",
|
|
fmt.Sprintf("nomad/jobs/%s", claim.Job),
|
|
fmt.Sprintf("nomad/jobs/%s/%s", claim.Job, claim.Group),
|
|
fmt.Sprintf("nomad/jobs/%s/%s/%s", claim.Job, claim.Group, claim.Task):
|
|
return workloadVariablesCapabilitySet, true
|
|
default:
|
|
}
|
|
}
|
|
|
|
// We didn't find a concrete match, so lets try and evaluate globs.
|
|
return a.findClosestMatchingGlob(a.wildcardVariables, ns+"\x00"+path)
|
|
}
|
|
|
|
type matchingGlob struct {
|
|
name string
|
|
difference int
|
|
capabilitySet capabilitySet
|
|
}
|
|
|
|
func (a *ACL) findClosestMatchingGlob(radix *iradix.Tree[capabilitySet], ns string) (capabilitySet, bool) {
|
|
// First, find all globs that match.
|
|
matchingGlobs := findAllMatchingWildcards(radix, ns)
|
|
|
|
// If none match, let's return.
|
|
if len(matchingGlobs) == 0 {
|
|
return capabilitySet{}, false
|
|
}
|
|
|
|
// If a single matches, lets be efficient and return early.
|
|
if len(matchingGlobs) == 1 {
|
|
return matchingGlobs[0].capabilitySet, true
|
|
}
|
|
|
|
// Stable sort the matched globs, based on the character difference between
|
|
// the glob definition and the requested namespace. This allows us to be
|
|
// more consistent about results based on the policy definition.
|
|
sort.SliceStable(matchingGlobs, func(i, j int) bool {
|
|
return matchingGlobs[i].difference <= matchingGlobs[j].difference
|
|
})
|
|
|
|
return matchingGlobs[0].capabilitySet, true
|
|
}
|
|
|
|
func findAllMatchingWildcards(radix *iradix.Tree[capabilitySet], name string) []matchingGlob {
|
|
var matches []matchingGlob
|
|
|
|
nsLen := len(name)
|
|
|
|
radix.Root().Walk(func(bk []byte, v capabilitySet) bool {
|
|
k := string(bk)
|
|
|
|
isMatch := glob.Glob(k, name)
|
|
if isMatch {
|
|
pair := matchingGlob{
|
|
name: k,
|
|
difference: nsLen - len(k) + strings.Count(k, glob.GLOB),
|
|
capabilitySet: v,
|
|
}
|
|
matches = append(matches, pair)
|
|
}
|
|
|
|
// We always want to walk the entire tree, never terminate early.
|
|
return false
|
|
})
|
|
|
|
return matches
|
|
}
|
|
|
|
// AllowAgentRead checks if read operations are allowed for an agent
|
|
func (a *ACL) AllowAgentRead() bool {
|
|
switch {
|
|
case a.management:
|
|
return true
|
|
case a.agent == PolicyWrite:
|
|
return true
|
|
case a.agent == PolicyRead:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// AllowAgentWrite checks if write operations are allowed for an agent
|
|
func (a *ACL) AllowAgentWrite() bool {
|
|
switch {
|
|
case a.management:
|
|
return true
|
|
case a.agent == PolicyWrite:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// AllowNodeRead checks if read operations are allowed for a node
|
|
func (a *ACL) AllowNodeRead() bool {
|
|
switch {
|
|
case a.management:
|
|
return true
|
|
case a.node == PolicyWrite:
|
|
return true
|
|
case a.node == PolicyRead:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// AllowNodeWrite checks if write operations are allowed for a node
|
|
func (a *ACL) AllowNodeWrite() bool {
|
|
switch {
|
|
case a.management:
|
|
return true
|
|
case a.node == PolicyWrite:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// AllowOperatorRead checks if read operations are allowed for a operator
|
|
func (a *ACL) AllowOperatorRead() bool {
|
|
switch {
|
|
case a.management:
|
|
return true
|
|
case a.operator == PolicyWrite:
|
|
return true
|
|
case a.operator == PolicyRead:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// AllowOperatorWrite checks if write operations are allowed for a operator
|
|
func (a *ACL) AllowOperatorWrite() bool {
|
|
switch {
|
|
case a.management:
|
|
return true
|
|
case a.operator == PolicyWrite:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// AllowQuotaRead checks if read operations are allowed for all quotas
|
|
func (a *ACL) AllowQuotaRead() bool {
|
|
switch {
|
|
case a.management:
|
|
return true
|
|
case a.quota == PolicyWrite:
|
|
return true
|
|
case a.quota == PolicyRead:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// AllowQuotaWrite checks if write operations are allowed for quotas
|
|
func (a *ACL) AllowQuotaWrite() bool {
|
|
switch {
|
|
case a.management:
|
|
return true
|
|
case a.quota == PolicyWrite:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// AllowPluginRead checks if read operations are allowed for all plugins
|
|
func (a *ACL) AllowPluginRead() bool {
|
|
switch {
|
|
// ACL is nil only if ACLs are disabled
|
|
case a == nil:
|
|
return true
|
|
case a.management:
|
|
return true
|
|
case a.plugin == PolicyRead:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// AllowPluginList checks if list operations are allowed for all plugins
|
|
func (a *ACL) AllowPluginList() bool {
|
|
switch {
|
|
// ACL is nil only if ACLs are disabled
|
|
case a == nil:
|
|
return true
|
|
case a.management:
|
|
return true
|
|
case a.plugin == PolicyList:
|
|
return true
|
|
case a.plugin == PolicyRead:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// IsManagement checks if this represents a management token
|
|
func (a *ACL) IsManagement() bool {
|
|
return a.management
|
|
}
|
|
|
|
// NamespaceValidator returns a func that wraps ACL.AllowNamespaceOperation in
|
|
// a list of operations. Returns true (allowed) if acls are disabled or if
|
|
// *any* capabilities match.
|
|
func NamespaceValidator(ops ...string) func(*ACL, string) bool {
|
|
return func(acl *ACL, ns string) bool {
|
|
// Always allow if ACLs are disabled.
|
|
if acl == nil {
|
|
return true
|
|
}
|
|
|
|
for _, op := range ops {
|
|
if acl.AllowNamespaceOperation(ns, op) {
|
|
// An operation is allowed, return true
|
|
return true
|
|
}
|
|
}
|
|
|
|
// No operations are allowed by this ACL, return false
|
|
return false
|
|
}
|
|
}
|