np: implement ACL for node pools (#17365)
This commit is contained in:
parent
53ca8b9552
commit
45b0391378
118
acl/acl.go
118
acl/acl.go
|
@ -50,23 +50,26 @@ 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.
|
||||
// The attributes below map polices that have fine-grained capabilities
|
||||
// with a capabilitySet.
|
||||
//
|
||||
// The attributes prefixed with `wildcard` maps the policies for glob
|
||||
// patterns to a capabilitySet. We use an iradix for the purposes of
|
||||
// ordered iteration.
|
||||
namespaces *iradix.Tree[capabilitySet]
|
||||
wildcardNamespaces *iradix.Tree[capabilitySet]
|
||||
|
||||
// hostVolumes maps a named host volume to a capabilitySet
|
||||
hostVolumes *iradix.Tree[capabilitySet]
|
||||
nodePools *iradix.Tree[capabilitySet]
|
||||
wildcardNodePools *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.
|
||||
hostVolumes *iradix.Tree[capabilitySet]
|
||||
wildcardHostVolumes *iradix.Tree[capabilitySet]
|
||||
|
||||
variables *iradix.Tree[capabilitySet]
|
||||
wildcardVariables *iradix.Tree[capabilitySet]
|
||||
|
||||
// The attributes below store the policy value for policies that don't have
|
||||
// fine-grained capabilities.
|
||||
agent string
|
||||
node string
|
||||
operator string
|
||||
|
@ -102,8 +105,13 @@ func NewACL(management bool, policies []*Policy) (*ACL, error) {
|
|||
acl := &ACL{}
|
||||
nsTxn := iradix.New[capabilitySet]().Txn()
|
||||
wnsTxn := iradix.New[capabilitySet]().Txn()
|
||||
|
||||
npTxn := iradix.New[capabilitySet]().Txn()
|
||||
wnpTxn := iradix.New[capabilitySet]().Txn()
|
||||
|
||||
hvTxn := iradix.New[capabilitySet]().Txn()
|
||||
whvTxn := iradix.New[capabilitySet]().Txn()
|
||||
|
||||
svTxn := iradix.New[capabilitySet]().Txn()
|
||||
wsvTxn := iradix.New[capabilitySet]().Txn()
|
||||
|
||||
|
@ -178,6 +186,42 @@ func NewACL(management bool, policies []*Policy) (*ACL, error) {
|
|||
}
|
||||
}
|
||||
|
||||
NODEPOOLS:
|
||||
for _, np := range policy.NodePools {
|
||||
// Use wildcard transaction if policy name uses glob matching.
|
||||
txn := npTxn
|
||||
if strings.Contains(np.Name, "*") {
|
||||
txn = wnpTxn
|
||||
}
|
||||
|
||||
// Check for existing capabilities.
|
||||
var capabilities capabilitySet
|
||||
|
||||
raw, ok := txn.Get([]byte(np.Name))
|
||||
if ok {
|
||||
capabilities = raw
|
||||
} else {
|
||||
capabilities = make(capabilitySet)
|
||||
txn.Insert([]byte(np.Name), capabilities)
|
||||
}
|
||||
|
||||
// Deny always takes precedence.
|
||||
if capabilities.Check(NodePoolCapabilityDeny) {
|
||||
continue NODEPOOLS
|
||||
}
|
||||
|
||||
// Add in all the capabilities.
|
||||
for _, cap := range np.Capabilities {
|
||||
if cap == NodePoolCapabilityDeny {
|
||||
// Overwrite any existing capabilities.
|
||||
capabilities.Clear()
|
||||
capabilities.Set(NodePoolCapabilityDeny)
|
||||
continue NODEPOOLS
|
||||
}
|
||||
capabilities.Set(cap)
|
||||
}
|
||||
}
|
||||
|
||||
HOSTVOLUMES:
|
||||
for _, hv := range policy.HostVolumes {
|
||||
// Should the volume be matched using a glob?
|
||||
|
@ -239,11 +283,16 @@ func NewACL(management bool, policies []*Policy) (*ACL, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Finalize the namespaces
|
||||
// Finalize policies with capabilities.
|
||||
acl.namespaces = nsTxn.Commit()
|
||||
acl.wildcardNamespaces = wnsTxn.Commit()
|
||||
|
||||
acl.nodePools = npTxn.Commit()
|
||||
acl.wildcardNodePools = wnpTxn.Commit()
|
||||
|
||||
acl.hostVolumes = hvTxn.Commit()
|
||||
acl.wildcardHostVolumes = whvTxn.Commit()
|
||||
|
||||
acl.variables = svTxn.Commit()
|
||||
acl.wildcardVariables = wsvTxn.Commit()
|
||||
|
||||
|
@ -323,6 +372,44 @@ func (a *ACL) AllowNamespace(ns string) bool {
|
|||
return !capabilities.Check(PolicyDeny)
|
||||
}
|
||||
|
||||
// AllowNodePoolOperation returns true if the given operation is allowed in the
|
||||
// node pool specified.
|
||||
func (a *ACL) AllowNodePoolOperation(pool string, op string) bool {
|
||||
// Hot path if ACL is not enabled or if it's a management token.
|
||||
if a == nil || a.management {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for matching capability set.
|
||||
capabilities, ok := a.matchingNodePoolCapabilitySet(pool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the capability has been granted.
|
||||
return capabilities.Check(op)
|
||||
}
|
||||
|
||||
// AllowNodePool returns true if any operation is allowed for the node pool.
|
||||
func (a *ACL) AllowNodePool(pool string) bool {
|
||||
// Hot path if ACL is not enabled or if it's a management token.
|
||||
if a == nil || a.management {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for matching capability set.
|
||||
capabilities, ok := a.matchingNodePoolCapabilitySet(pool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -453,6 +540,17 @@ func (a *ACL) anyNamespaceAllows(cb func(capabilitySet) bool) bool {
|
|||
return allow
|
||||
}
|
||||
|
||||
// matchingNodePoolCapabilitySet returns the capabilitySet that closest match
|
||||
// the node pool.
|
||||
func (a *ACL) matchingNodePoolCapabilitySet(pool string) (capabilitySet, bool) {
|
||||
raw, ok := a.nodePools.Get([]byte(pool))
|
||||
if ok {
|
||||
return raw, true
|
||||
}
|
||||
|
||||
return a.findClosestMatchingGlob(a.wildcardNodePools, pool)
|
||||
}
|
||||
|
||||
// matchingHostVolumeCapabilitySet looks for a capabilitySet that matches the host volume name,
|
||||
// if no concrete definitions are found, then we return the closest matching
|
||||
// glob.
|
||||
|
|
376
acl/acl_test.go
376
acl/acl_test.go
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -71,119 +72,152 @@ func TestMaxPrivilege(t *testing.T) {
|
|||
func TestACLManagement(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
// Create management ACL
|
||||
acl, err := NewACL(true, nil)
|
||||
assert.Nil(err)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Check default namespace rights
|
||||
assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
|
||||
assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
|
||||
assert.True(acl.AllowNamespace("default"))
|
||||
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
|
||||
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
|
||||
must.True(t, acl.AllowNamespace("default"))
|
||||
|
||||
// Check non-specified namespace
|
||||
assert.True(acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
|
||||
assert.True(acl.AllowNamespace("foo"))
|
||||
must.True(t, acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
|
||||
must.True(t, acl.AllowNamespace("foo"))
|
||||
|
||||
// Check node pool rights.
|
||||
must.True(t, acl.AllowNodePoolOperation("my-pool", NodePoolCapabilityWrite))
|
||||
must.True(t, acl.AllowNodePool("my-pool"))
|
||||
|
||||
// Check the other simpler operations
|
||||
assert.True(acl.IsManagement())
|
||||
assert.True(acl.AllowAgentRead())
|
||||
assert.True(acl.AllowAgentWrite())
|
||||
assert.True(acl.AllowNodeRead())
|
||||
assert.True(acl.AllowNodeWrite())
|
||||
assert.True(acl.AllowOperatorRead())
|
||||
assert.True(acl.AllowOperatorWrite())
|
||||
assert.True(acl.AllowQuotaRead())
|
||||
assert.True(acl.AllowQuotaWrite())
|
||||
must.True(t, acl.IsManagement())
|
||||
must.True(t, acl.AllowAgentRead())
|
||||
must.True(t, acl.AllowAgentWrite())
|
||||
must.True(t, acl.AllowNodeRead())
|
||||
must.True(t, acl.AllowNodeWrite())
|
||||
must.True(t, acl.AllowOperatorRead())
|
||||
must.True(t, acl.AllowOperatorWrite())
|
||||
must.True(t, acl.AllowQuotaRead())
|
||||
must.True(t, acl.AllowQuotaWrite())
|
||||
}
|
||||
|
||||
func TestACLMerge(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
// Merge read + write policy
|
||||
p1, err := Parse(readAll)
|
||||
assert.Nil(err)
|
||||
must.NoError(t, err)
|
||||
p2, err := Parse(writeAll)
|
||||
assert.Nil(err)
|
||||
must.NoError(t, err)
|
||||
acl, err := NewACL(false, []*Policy{p1, p2})
|
||||
assert.Nil(err)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Check default namespace rights
|
||||
assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
|
||||
assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
|
||||
assert.True(acl.AllowNamespace("default"))
|
||||
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
|
||||
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
|
||||
must.True(t, acl.AllowNamespace("default"))
|
||||
|
||||
// Check non-specified namespace
|
||||
assert.False(acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
|
||||
assert.False(acl.AllowNamespace("foo"))
|
||||
must.False(t, acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
|
||||
must.False(t, acl.AllowNamespace("foo"))
|
||||
|
||||
// Check rights in the node pool specified in policies.
|
||||
must.True(t, acl.AllowNodePoolOperation("my-pool", NodePoolCapabilityRead))
|
||||
must.True(t, acl.AllowNodePoolOperation("my-pool", NodePoolCapabilityWrite))
|
||||
must.True(t, acl.AllowNodePool("my-pool"))
|
||||
|
||||
// Check non-specified node pool policies.
|
||||
must.False(t, acl.AllowNodePoolOperation("other-pool", NodePoolCapabilityRead))
|
||||
must.False(t, acl.AllowNodePoolOperation("other-pool", NodePoolCapabilityWrite))
|
||||
must.False(t, acl.AllowNodePool("other-pool"))
|
||||
|
||||
// Check the other simpler operations
|
||||
assert.False(acl.IsManagement())
|
||||
assert.True(acl.AllowAgentRead())
|
||||
assert.True(acl.AllowAgentWrite())
|
||||
assert.True(acl.AllowNodeRead())
|
||||
assert.True(acl.AllowNodeWrite())
|
||||
assert.True(acl.AllowOperatorRead())
|
||||
assert.True(acl.AllowOperatorWrite())
|
||||
assert.True(acl.AllowQuotaRead())
|
||||
assert.True(acl.AllowQuotaWrite())
|
||||
must.False(t, acl.IsManagement())
|
||||
must.True(t, acl.AllowAgentRead())
|
||||
must.True(t, acl.AllowAgentWrite())
|
||||
must.True(t, acl.AllowNodeRead())
|
||||
must.True(t, acl.AllowNodeWrite())
|
||||
must.True(t, acl.AllowOperatorRead())
|
||||
must.True(t, acl.AllowOperatorWrite())
|
||||
must.True(t, acl.AllowQuotaRead())
|
||||
must.True(t, acl.AllowQuotaWrite())
|
||||
|
||||
// Merge read + blank
|
||||
p3, err := Parse("")
|
||||
assert.Nil(err)
|
||||
must.NoError(t, err)
|
||||
acl, err = NewACL(false, []*Policy{p1, p3})
|
||||
assert.Nil(err)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Check default namespace rights
|
||||
assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
|
||||
assert.False(acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
|
||||
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
|
||||
must.False(t, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
|
||||
|
||||
// Check non-specified namespace
|
||||
assert.False(acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
|
||||
must.False(t, acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
|
||||
|
||||
// Check rights in the node pool specified in policies.
|
||||
must.True(t, acl.AllowNodePoolOperation("my-pool", NodePoolCapabilityRead))
|
||||
must.False(t, acl.AllowNodePoolOperation("my-pool", NodePoolCapabilityWrite))
|
||||
must.True(t, acl.AllowNodePool("my-pool"))
|
||||
|
||||
// Check non-specified node pool policies.
|
||||
must.False(t, acl.AllowNodePoolOperation("other-pool", NodePoolCapabilityRead))
|
||||
must.False(t, acl.AllowNodePoolOperation("other-pool", NodePoolCapabilityWrite))
|
||||
must.False(t, acl.AllowNodePool("other-pool"))
|
||||
|
||||
// Check the other simpler operations
|
||||
assert.False(acl.IsManagement())
|
||||
assert.True(acl.AllowAgentRead())
|
||||
assert.False(acl.AllowAgentWrite())
|
||||
assert.True(acl.AllowNodeRead())
|
||||
assert.False(acl.AllowNodeWrite())
|
||||
assert.True(acl.AllowOperatorRead())
|
||||
assert.False(acl.AllowOperatorWrite())
|
||||
assert.True(acl.AllowQuotaRead())
|
||||
assert.False(acl.AllowQuotaWrite())
|
||||
must.False(t, acl.IsManagement())
|
||||
must.True(t, acl.AllowAgentRead())
|
||||
must.False(t, acl.AllowAgentWrite())
|
||||
must.True(t, acl.AllowNodeRead())
|
||||
must.False(t, acl.AllowNodeWrite())
|
||||
must.True(t, acl.AllowOperatorRead())
|
||||
must.False(t, acl.AllowOperatorWrite())
|
||||
must.True(t, acl.AllowQuotaRead())
|
||||
must.False(t, acl.AllowQuotaWrite())
|
||||
|
||||
// Merge read + deny
|
||||
p4, err := Parse(denyAll)
|
||||
assert.Nil(err)
|
||||
must.NoError(t, err)
|
||||
acl, err = NewACL(false, []*Policy{p1, p4})
|
||||
assert.Nil(err)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Check default namespace rights
|
||||
assert.False(acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
|
||||
assert.False(acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
|
||||
must.False(t, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
|
||||
must.False(t, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
|
||||
|
||||
// Check non-specified namespace
|
||||
assert.False(acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
|
||||
must.False(t, acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
|
||||
|
||||
// Check rights in the node pool specified in policies.
|
||||
must.False(t, acl.AllowNodePoolOperation("my-pool", NodePoolCapabilityRead))
|
||||
must.False(t, acl.AllowNodePoolOperation("my-pool", NodePoolCapabilityWrite))
|
||||
must.False(t, acl.AllowNodePool("my-pool"))
|
||||
|
||||
// Check non-specified node pool policies.
|
||||
must.False(t, acl.AllowNodePoolOperation("other-pool", NodePoolCapabilityRead))
|
||||
must.False(t, acl.AllowNodePoolOperation("other-pool", NodePoolCapabilityWrite))
|
||||
must.False(t, acl.AllowNodePool("other-pool"))
|
||||
|
||||
// Check the other simpler operations
|
||||
assert.False(acl.IsManagement())
|
||||
assert.False(acl.AllowAgentRead())
|
||||
assert.False(acl.AllowAgentWrite())
|
||||
assert.False(acl.AllowNodeRead())
|
||||
assert.False(acl.AllowNodeWrite())
|
||||
assert.False(acl.AllowOperatorRead())
|
||||
assert.False(acl.AllowOperatorWrite())
|
||||
assert.False(acl.AllowQuotaRead())
|
||||
assert.False(acl.AllowQuotaWrite())
|
||||
must.False(t, acl.IsManagement())
|
||||
must.False(t, acl.AllowAgentRead())
|
||||
must.False(t, acl.AllowAgentWrite())
|
||||
must.False(t, acl.AllowNodeRead())
|
||||
must.False(t, acl.AllowNodeWrite())
|
||||
must.False(t, acl.AllowOperatorRead())
|
||||
must.False(t, acl.AllowOperatorWrite())
|
||||
must.False(t, acl.AllowQuotaRead())
|
||||
must.False(t, acl.AllowQuotaWrite())
|
||||
}
|
||||
|
||||
var readAll = `
|
||||
namespace "default" {
|
||||
policy = "read"
|
||||
}
|
||||
node_pool "my-pool" {
|
||||
policy = "read"
|
||||
}
|
||||
agent {
|
||||
policy = "read"
|
||||
}
|
||||
|
@ -202,6 +236,9 @@ var writeAll = `
|
|||
namespace "default" {
|
||||
policy = "write"
|
||||
}
|
||||
node_pool "my-pool" {
|
||||
policy = "write"
|
||||
}
|
||||
agent {
|
||||
policy = "write"
|
||||
}
|
||||
|
@ -220,6 +257,9 @@ var denyAll = `
|
|||
namespace "default" {
|
||||
policy = "deny"
|
||||
}
|
||||
node_pool "my-pool" {
|
||||
policy = "deny"
|
||||
}
|
||||
agent {
|
||||
policy = "deny"
|
||||
}
|
||||
|
@ -398,6 +438,214 @@ func TestWildcardNamespaceMatching(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNodePool(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
policy string
|
||||
pool string
|
||||
allowOps []string
|
||||
denyOps []string
|
||||
allow bool
|
||||
}{
|
||||
{
|
||||
name: "policy read",
|
||||
policy: `
|
||||
node_pool "my-pool" {
|
||||
policy = "read"
|
||||
}
|
||||
`,
|
||||
pool: "my-pool",
|
||||
allowOps: []string{NodePoolCapabilityRead},
|
||||
denyOps: []string{
|
||||
NodePoolCapabilityDelete,
|
||||
NodePoolCapabilityWrite,
|
||||
},
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
name: "policy write",
|
||||
policy: `
|
||||
node_pool "my-pool" {
|
||||
policy = "write"
|
||||
}
|
||||
`,
|
||||
pool: "my-pool",
|
||||
allowOps: []string{
|
||||
NodePoolCapabilityDelete,
|
||||
NodePoolCapabilityRead,
|
||||
NodePoolCapabilityWrite,
|
||||
},
|
||||
denyOps: []string{},
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
name: "capability write",
|
||||
policy: `
|
||||
node_pool "my-pool" {
|
||||
capabilities = ["write"]
|
||||
}
|
||||
`,
|
||||
pool: "my-pool",
|
||||
allowOps: []string{
|
||||
NodePoolCapabilityWrite,
|
||||
},
|
||||
denyOps: []string{
|
||||
NodePoolCapabilityDelete,
|
||||
NodePoolCapabilityRead,
|
||||
},
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
name: "multiple capabilities",
|
||||
policy: `
|
||||
node_pool "my-pool" {
|
||||
capabilities = ["read", "delete"]
|
||||
}
|
||||
`,
|
||||
pool: "my-pool",
|
||||
allowOps: []string{
|
||||
NodePoolCapabilityRead,
|
||||
NodePoolCapabilityDelete,
|
||||
},
|
||||
denyOps: []string{
|
||||
NodePoolCapabilityWrite,
|
||||
},
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
name: "policy deny takes precedence",
|
||||
policy: `
|
||||
node_pool "my-pool" {
|
||||
policy = "deny"
|
||||
capabilities = ["write", "delete"]
|
||||
}
|
||||
`,
|
||||
pool: "my-pool",
|
||||
allowOps: []string{},
|
||||
denyOps: []string{
|
||||
NodePoolCapabilityDelete,
|
||||
NodePoolCapabilityRead,
|
||||
NodePoolCapabilityWrite,
|
||||
},
|
||||
allow: false,
|
||||
},
|
||||
{
|
||||
name: "capability deny takes precedence",
|
||||
policy: `
|
||||
node_pool "my-pool" {
|
||||
capabilities = ["write", "delete", "deny"]
|
||||
}
|
||||
`,
|
||||
pool: "my-pool",
|
||||
allowOps: []string{},
|
||||
denyOps: []string{
|
||||
NodePoolCapabilityDelete,
|
||||
NodePoolCapabilityRead,
|
||||
NodePoolCapabilityWrite,
|
||||
},
|
||||
allow: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard matches all",
|
||||
policy: `
|
||||
node_pool "*" {
|
||||
policy = "read"
|
||||
}
|
||||
`,
|
||||
pool: "my-pool",
|
||||
allowOps: []string{NodePoolCapabilityRead},
|
||||
denyOps: []string{
|
||||
NodePoolCapabilityDelete,
|
||||
NodePoolCapabilityWrite,
|
||||
},
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard matches subset",
|
||||
policy: `
|
||||
node_pool "my-pool-*" {
|
||||
policy = "read"
|
||||
}
|
||||
`,
|
||||
pool: "my-pool-1",
|
||||
allowOps: []string{NodePoolCapabilityRead},
|
||||
denyOps: []string{
|
||||
NodePoolCapabilityDelete,
|
||||
NodePoolCapabilityWrite,
|
||||
},
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard doesn't match subset",
|
||||
policy: `
|
||||
node_pool "my-pool-*" {
|
||||
policy = "read"
|
||||
}
|
||||
`,
|
||||
pool: "your-pool-1",
|
||||
allowOps: []string{},
|
||||
denyOps: []string{
|
||||
NodePoolCapabilityDelete,
|
||||
NodePoolCapabilityRead,
|
||||
NodePoolCapabilityWrite,
|
||||
},
|
||||
allow: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard matches closest",
|
||||
policy: `
|
||||
node_pool "my-pool-dev-*" {
|
||||
policy = "read"
|
||||
}
|
||||
|
||||
node_pool "my-pool-*" {
|
||||
policy = "write"
|
||||
}
|
||||
|
||||
node_pool "*" {
|
||||
policy = "deny"
|
||||
}
|
||||
`,
|
||||
pool: "my-pool-dev-1",
|
||||
allowOps: []string{NodePoolCapabilityRead},
|
||||
denyOps: []string{
|
||||
NodePoolCapabilityDelete,
|
||||
NodePoolCapabilityWrite,
|
||||
},
|
||||
allow: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
policy, err := Parse(tc.policy)
|
||||
must.NoError(t, err)
|
||||
must.NotNil(t, policy.NodePools)
|
||||
|
||||
acl, err := NewACL(false, []*Policy{policy})
|
||||
must.NoError(t, err)
|
||||
|
||||
for _, op := range tc.allowOps {
|
||||
got := acl.AllowNodePoolOperation(tc.pool, op)
|
||||
assert.True(t, got, must.Sprintf("expected operation %q to be allowed", op))
|
||||
}
|
||||
|
||||
for _, op := range tc.denyOps {
|
||||
got := acl.AllowNodePoolOperation(tc.pool, op)
|
||||
assert.False(t, got, must.Sprintf("expected operation %q to be denied", op))
|
||||
}
|
||||
|
||||
if tc.allow {
|
||||
must.True(t, acl.AllowNodePool(tc.pool), must.Sprint("expected node pool to be allowed"))
|
||||
} else {
|
||||
must.False(t, acl.AllowNodePool(tc.pool), must.Sprint("expected node pool to be denied"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWildcardHostVolumeMatching(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ const (
|
|||
|
||||
const (
|
||||
// The following are the fine-grained capabilities that can be granted within a namespace.
|
||||
// The Policy block is a short hand for granting several of these. When capabilities are
|
||||
// The Policy field is a short hand for granting several of these. When capabilities are
|
||||
// combined we take the union of all capabilities. If the deny capability is present, it
|
||||
// takes precedence and overwrites all other capabilities.
|
||||
|
||||
|
@ -55,9 +55,28 @@ var (
|
|||
validNamespace = regexp.MustCompile("^[a-zA-Z0-9-*]{1,128}$")
|
||||
)
|
||||
|
||||
const (
|
||||
// The following are the fine-grained capabilities that can be granted for
|
||||
// node volume management.
|
||||
//
|
||||
// The Policy field is a short hand for granting several of these. When
|
||||
// capabilities are combined we take the union of all capabilities. If the
|
||||
// deny capability is present, it takes precedence and overwrites all other
|
||||
// capabilities.
|
||||
|
||||
NodePoolCapabilityDelete = "delete"
|
||||
NodePoolCapabilityDeny = "deny"
|
||||
NodePoolCapabilityRead = "read"
|
||||
NodePoolCapabilityWrite = "write"
|
||||
)
|
||||
|
||||
var (
|
||||
validNodePool = regexp.MustCompile("^[a-zA-Z0-9-_*]{1,128}$")
|
||||
)
|
||||
|
||||
const (
|
||||
// The following are the fine-grained capabilities that can be granted for a volume set.
|
||||
// The Policy block is a short hand for granting several of these. When capabilities are
|
||||
// The Policy field is a short hand for granting several of these. When capabilities are
|
||||
// combined we take the union of all capabilities. If the deny capability is present, it
|
||||
// takes precedence and overwrites all other capabilities.
|
||||
|
||||
|
@ -84,6 +103,7 @@ const (
|
|||
// Policy represents a parsed HCL or JSON policy.
|
||||
type Policy struct {
|
||||
Namespaces []*NamespacePolicy `hcl:"namespace,expand"`
|
||||
NodePools []*NodePoolPolicy `hcl:"node_pool,expand"`
|
||||
HostVolumes []*HostVolumePolicy `hcl:"host_volume,expand"`
|
||||
Agent *AgentPolicy `hcl:"agent"`
|
||||
Node *NodePolicy `hcl:"node"`
|
||||
|
@ -97,6 +117,7 @@ type Policy struct {
|
|||
// comprised of only a raw policy.
|
||||
func (p *Policy) IsEmpty() bool {
|
||||
return len(p.Namespaces) == 0 &&
|
||||
len(p.NodePools) == 0 &&
|
||||
len(p.HostVolumes) == 0 &&
|
||||
p.Agent == nil &&
|
||||
p.Node == nil &&
|
||||
|
@ -113,6 +134,13 @@ type NamespacePolicy struct {
|
|||
Variables *VariablesPolicy `hcl:"variables"`
|
||||
}
|
||||
|
||||
// NodePoolPolicy is the policfy for a specific node pool.
|
||||
type NodePoolPolicy struct {
|
||||
Name string `hcl:",key"`
|
||||
Policy string
|
||||
Capabilities []string
|
||||
}
|
||||
|
||||
type VariablesPolicy struct {
|
||||
Paths []*VariablesPathPolicy `hcl:"path"`
|
||||
}
|
||||
|
@ -247,6 +275,33 @@ func expandNamespacePolicy(policy string) []string {
|
|||
}
|
||||
}
|
||||
|
||||
func isNodePoolCapabilityValid(cap string) bool {
|
||||
switch cap {
|
||||
case NodePoolCapabilityDelete, NodePoolCapabilityRead, NodePoolCapabilityWrite,
|
||||
NodePoolCapabilityDeny:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func expandNodePoolPolicy(policy string) []string {
|
||||
switch policy {
|
||||
case PolicyDeny:
|
||||
return []string{NodePoolCapabilityDeny}
|
||||
case PolicyRead:
|
||||
return []string{NodePoolCapabilityRead}
|
||||
case PolicyWrite:
|
||||
return []string{
|
||||
NodePoolCapabilityDelete,
|
||||
NodePoolCapabilityRead,
|
||||
NodePoolCapabilityWrite,
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func isHostVolumeCapabilityValid(cap string) bool {
|
||||
switch cap {
|
||||
case HostVolumeCapabilityDeny, HostVolumeCapabilityMountReadOnly, HostVolumeCapabilityMountReadWrite:
|
||||
|
@ -352,6 +407,25 @@ func Parse(rules string) (*Policy, error) {
|
|||
|
||||
}
|
||||
|
||||
for _, np := range p.NodePools {
|
||||
if !validNodePool.MatchString(np.Name) {
|
||||
return nil, fmt.Errorf("Invalid node pool name '%s'", np.Name)
|
||||
}
|
||||
if np.Policy != "" && !isPolicyValid(np.Policy) {
|
||||
return nil, fmt.Errorf("Invalid node pool policy '%s' for '%s'", np.Policy, np.Name)
|
||||
}
|
||||
for _, cap := range np.Capabilities {
|
||||
if !isNodePoolCapabilityValid(cap) {
|
||||
return nil, fmt.Errorf("Invalid node pool capability '%s' for '%s'", cap, np.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if np.Policy != "" {
|
||||
extraCap := expandNodePoolPolicy(np.Policy)
|
||||
np.Capabilities = append(np.Capabilities, extraCap...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, hv := range p.HostVolumes {
|
||||
if !validVolume.MatchString(hv.Name) {
|
||||
return nil, fmt.Errorf("Invalid host volume name: %#v", hv)
|
||||
|
|
|
@ -296,6 +296,126 @@ func TestParse(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
`
|
||||
node_pool "pool-read-only" {
|
||||
policy = "read"
|
||||
}
|
||||
|
||||
node_pool "pool-read-write" {
|
||||
policy = "write"
|
||||
}
|
||||
|
||||
node_pool "pool-read-upsert" {
|
||||
policy = "read"
|
||||
capabilities = ["write"]
|
||||
}
|
||||
|
||||
node_pool "pool-multiple-capabilities" {
|
||||
policy = "read"
|
||||
capabilities = ["write", "delete"]
|
||||
}
|
||||
|
||||
node_pool "pool-deny-policy" {
|
||||
policy = "deny"
|
||||
capabilities = ["write"]
|
||||
}
|
||||
|
||||
node_pool "pool-deny-capability" {
|
||||
capabilities = ["deny", "read"]
|
||||
}
|
||||
|
||||
node_pool "pool-*" {
|
||||
policy = "read"
|
||||
}
|
||||
`,
|
||||
"",
|
||||
&Policy{
|
||||
NodePools: []*NodePoolPolicy{
|
||||
{
|
||||
Name: "pool-read-only",
|
||||
Policy: PolicyRead,
|
||||
Capabilities: []string{
|
||||
NodePoolCapabilityRead,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pool-read-write",
|
||||
Policy: PolicyWrite,
|
||||
Capabilities: []string{
|
||||
NodePoolCapabilityDelete,
|
||||
NodePoolCapabilityRead,
|
||||
NodePoolCapabilityWrite,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pool-read-upsert",
|
||||
Policy: PolicyRead,
|
||||
Capabilities: []string{
|
||||
NodePoolCapabilityWrite,
|
||||
NodePoolCapabilityRead,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pool-multiple-capabilities",
|
||||
Policy: PolicyRead,
|
||||
Capabilities: []string{
|
||||
NodePoolCapabilityWrite,
|
||||
NodePoolCapabilityDelete,
|
||||
NodePoolCapabilityRead,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pool-deny-policy",
|
||||
Policy: PolicyDeny,
|
||||
Capabilities: []string{
|
||||
NodePoolCapabilityWrite,
|
||||
NodePoolCapabilityDeny,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pool-deny-capability",
|
||||
Policy: "",
|
||||
Capabilities: []string{
|
||||
NodePoolCapabilityDeny,
|
||||
NodePoolCapabilityRead,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pool-*",
|
||||
Policy: PolicyRead,
|
||||
Capabilities: []string{
|
||||
NodePoolCapabilityRead,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
`
|
||||
node_pool "" {
|
||||
}
|
||||
`,
|
||||
"Invalid node pool name",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
`
|
||||
node_pool "pool%" {
|
||||
}
|
||||
`,
|
||||
"Invalid node pool name",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
`
|
||||
node_pool "my-pool" {
|
||||
capabilities = ["read", "invalid"]
|
||||
}
|
||||
`,
|
||||
"Invalid node pool capability",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
`
|
||||
host_volume "production-tls-*" {
|
||||
|
|
|
@ -248,6 +248,62 @@ The `policy` field for the node rule can have one of the following values:
|
|||
- `deny`: do not allow the resource to be read or modified. Deny takes
|
||||
precedence when multiple policies are associated with a token.
|
||||
|
||||
## Node Pools rules
|
||||
|
||||
Node pool rules are defined with a `node_pool` block. An ACL policy can
|
||||
include zero, one, or more node pool rules.
|
||||
|
||||
<!-- TODO(luiz): add link to node pools API docs -->
|
||||
Node pool rule controls access to the Node Pool API such as create, update, and
|
||||
list node pools.
|
||||
|
||||
Each node pool rule is labeled with the node pool name it applies to. You may
|
||||
use wildcard globs (`"*"`) in the label to apply a rule to multiple node pools.
|
||||
|
||||
Similarly to [namespace rules](#namespace-rules) only one `node_pool` rule can
|
||||
be applied. First an _exact match_ is tried before falling back to a glob-based
|
||||
lookup, where the rule with the greatest number of matched characters is
|
||||
chosen.
|
||||
|
||||
Each node pool rule can include a coarse-grained `policy` field and a
|
||||
fine-grained `capabilities` field.
|
||||
|
||||
The `policy` field for node pool rules can have one of the following values.
|
||||
|
||||
- `read` allows node pools to be listed and read but not modified.
|
||||
- `write` allows node pools to be read, create, updated, and deleted.
|
||||
- `deny` forbids node pools to be read or modified. Deny takes precedence when
|
||||
multiple policies are associated with a token.
|
||||
|
||||
In addition to the coarse-grained `policy`, you can provide a fine-grained list
|
||||
of `capabilities`.
|
||||
|
||||
- `deny` forbids node pools to be read or modified. Deny takes precedence when
|
||||
multiple policies are associated with a token.
|
||||
- `delete` allows node pools to be deleted.
|
||||
- `read` allows node pools to be listed and read.
|
||||
- `write` allows node pools to be created and updated.
|
||||
|
||||
The coarse-grained policy permissions are shorthand for the following
|
||||
fine-grained capabilities.
|
||||
|
||||
| Policy | Capabilities |
|
||||
| ------- | ------------------------- |
|
||||
| `deny` | `deny` |
|
||||
| `read` | `read` |
|
||||
| `write` | `delete`, `read`, `write` |
|
||||
|
||||
If you provide both a `policy` and `capabilities` list the capabilities are
|
||||
merged. For example, the policy below adds the `write` capability to the `read`
|
||||
policy, thus allowing `read` and `write` permission but no `delete`.
|
||||
|
||||
```hcl
|
||||
node_pool "dev-*" {
|
||||
policy = "read"
|
||||
capabilities = ["write"]
|
||||
}
|
||||
```
|
||||
|
||||
## Agent rules
|
||||
|
||||
The `agent` rule controls access to the [Agent API][api_agent] such as join and
|
||||
|
|
Loading…
Reference in New Issue