np: implement ACL for node pools (#17365)

This commit is contained in:
Luiz Aoqui 2023-06-01 13:03:20 -04:00 committed by GitHub
parent 53ca8b9552
commit 45b0391378
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 672 additions and 76 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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-*" {

View File

@ -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