dynamic host volumes: ACL policies (#24356)

This changeset implements the ACLs required for dynamic host volumes RPCs:
* `host-volume-write` is a coarse-grained policy that implies all operations.
* `host-volume-register` is the highest fine-grained privilege because it
  potentially bypasses quotas.
* `host-volume-create` is implicitly granted by `host-volume-register`
* `host-volume-delete` is implicitly granted only by `host-volume-write`
* `host-volume-read` is implicitly granted by `policy = "read"`,

These are namespaced operations, so the testing here is predominantly around
parsing and granting of implicit capabilities rather than the well-tested
`AllowNamespaceOperation` method.

This changeset does not include any changes to the `host_volumes` policy which
we'll need for claiming volumes on job submit. That'll be covered in a later PR.

Ref: https://hashicorp.atlassian.net/browse/NET-11549
This commit is contained in:
Tim Gross
2024-11-05 15:19:34 -05:00
parent 8bb7c1315d
commit d8c901570b
3 changed files with 110 additions and 18 deletions

View File

@@ -79,10 +79,12 @@ func TestACLManagement(t *testing.T) {
// Check default namespace rights
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilityHostVolumeCreate))
must.True(t, acl.AllowNamespace("default"))
// Check non-specified namespace
must.True(t, acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
must.True(t, acl.AllowNamespaceOperation("foo", NamespaceCapabilityHostVolumeCreate))
must.True(t, acl.AllowNamespace("foo"))
// Check node pool rights.
@@ -155,9 +157,11 @@ func TestACLMerge(t *testing.T) {
// Check default namespace rights
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
must.False(t, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
must.False(t, acl.AllowNamespaceOperation("default", NamespaceCapabilityHostVolumeRegister))
// Check non-specified namespace
must.False(t, acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
must.False(t, acl.AllowNamespaceOperation("foo", NamespaceCapabilityHostVolumeCreate))
// Check rights in the node pool specified in policies.
must.True(t, acl.AllowNodePoolOperation("my-pool", NodePoolCapabilityRead))

View File

@@ -47,6 +47,11 @@ const (
NamespaceCapabilityCSIReadVolume = "csi-read-volume"
NamespaceCapabilityCSIListVolume = "csi-list-volume"
NamespaceCapabilityCSIMountVolume = "csi-mount-volume"
NamespaceCapabilityHostVolumeCreate = "host-volume-create"
NamespaceCapabilityHostVolumeRegister = "host-volume-register"
NamespaceCapabilityHostVolumeRead = "host-volume-read"
NamespaceCapabilityHostVolumeWrite = "host-volume-write"
NamespaceCapabilityHostVolumeDelete = "host-volume-delete"
NamespaceCapabilityListScalingPolicies = "list-scaling-policies"
NamespaceCapabilityReadScalingPolicy = "read-scaling-policy"
NamespaceCapabilityReadJobScaling = "read-job-scaling"
@@ -207,7 +212,7 @@ func isNamespaceCapabilityValid(cap string) bool {
NamespaceCapabilityReadFS, NamespaceCapabilityAllocLifecycle,
NamespaceCapabilityAllocExec, NamespaceCapabilityAllocNodeExec,
NamespaceCapabilityCSIReadVolume, NamespaceCapabilityCSIWriteVolume, NamespaceCapabilityCSIListVolume, NamespaceCapabilityCSIMountVolume, NamespaceCapabilityCSIRegisterPlugin,
NamespaceCapabilityListScalingPolicies, NamespaceCapabilityReadScalingPolicy, NamespaceCapabilityReadJobScaling, NamespaceCapabilityScaleJob:
NamespaceCapabilityListScalingPolicies, NamespaceCapabilityReadScalingPolicy, NamespaceCapabilityReadJobScaling, NamespaceCapabilityScaleJob, NamespaceCapabilityHostVolumeCreate, NamespaceCapabilityHostVolumeRegister, NamespaceCapabilityHostVolumeWrite, NamespaceCapabilityHostVolumeRead:
return true
// Separate the enterprise-only capabilities
case NamespaceCapabilitySentinelOverride, NamespaceCapabilitySubmitRecommendation:
@@ -241,6 +246,7 @@ func expandNamespacePolicy(policy string) []string {
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
NamespaceCapabilityHostVolumeRead,
}
write := make([]string, len(read))
@@ -257,6 +263,7 @@ func expandNamespacePolicy(policy string) []string {
NamespaceCapabilityCSIMountVolume,
NamespaceCapabilityCSIWriteVolume,
NamespaceCapabilitySubmitRecommendation,
NamespaceCapabilityHostVolumeCreate,
}...)
switch policy {
@@ -278,6 +285,32 @@ func expandNamespacePolicy(policy string) []string {
}
}
// expandNamespaceCapabilities adds extra capabilities implied by fine-grained
// capabilities.
func expandNamespaceCapabilities(ns *NamespacePolicy) {
extraCaps := []string{}
for _, cap := range ns.Capabilities {
switch cap {
case NamespaceCapabilityHostVolumeWrite:
extraCaps = append(extraCaps,
NamespaceCapabilityHostVolumeRegister,
NamespaceCapabilityHostVolumeCreate,
NamespaceCapabilityHostVolumeDelete,
NamespaceCapabilityHostVolumeRead)
case NamespaceCapabilityHostVolumeRegister:
extraCaps = append(extraCaps,
NamespaceCapabilityHostVolumeCreate,
NamespaceCapabilityHostVolumeRead)
case NamespaceCapabilityHostVolumeCreate:
extraCaps = append(extraCaps, NamespaceCapabilityHostVolumeRead)
}
}
// These may end up being duplicated, but they'll get deduplicated in NewACL
// when inserted into the radix tree.
ns.Capabilities = append(ns.Capabilities, extraCaps...)
}
func isNodePoolCapabilityValid(cap string) bool {
switch cap {
case NodePoolCapabilityDelete, NodePoolCapabilityRead, NodePoolCapabilityWrite,
@@ -388,6 +421,9 @@ func Parse(rules string) (*Policy, error) {
ns.Capabilities = append(ns.Capabilities, extraCap...)
}
// Expand implicit capabilities
expandNamespaceCapabilities(ns)
if ns.Variables != nil {
if len(ns.Variables.Paths) == 0 {
return nil, fmt.Errorf("Invalid variable policy: no variable paths in namespace %s", ns.Name)

View File

@@ -5,7 +5,6 @@ package acl
import (
"fmt"
"strings"
"testing"
"github.com/hashicorp/nomad/ci"
@@ -17,9 +16,9 @@ func TestParse(t *testing.T) {
ci.Parallel(t)
type tcase struct {
Raw string
ErrStr string
Expect *Policy
Raw string
ExpectErr string
Expect *Policy
}
tcases := []tcase{
{
@@ -43,6 +42,7 @@ func TestParse(t *testing.T) {
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
NamespaceCapabilityHostVolumeRead,
},
},
},
@@ -118,6 +118,7 @@ func TestParse(t *testing.T) {
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
NamespaceCapabilityHostVolumeRead,
},
},
{
@@ -132,6 +133,7 @@ func TestParse(t *testing.T) {
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
NamespaceCapabilityHostVolumeRead,
NamespaceCapabilityScaleJob,
NamespaceCapabilitySubmitJob,
NamespaceCapabilityDispatchJob,
@@ -142,6 +144,8 @@ func TestParse(t *testing.T) {
NamespaceCapabilityCSIMountVolume,
NamespaceCapabilityCSIWriteVolume,
NamespaceCapabilitySubmitRecommendation,
NamespaceCapabilityHostVolumeCreate,
NamespaceCapabilityHostVolumeRead,
},
},
{
@@ -338,6 +342,7 @@ func TestParse(t *testing.T) {
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
NamespaceCapabilityHostVolumeRead,
},
},
{
@@ -352,6 +357,7 @@ func TestParse(t *testing.T) {
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
NamespaceCapabilityHostVolumeRead,
NamespaceCapabilityScaleJob,
NamespaceCapabilitySubmitJob,
NamespaceCapabilityDispatchJob,
@@ -362,6 +368,8 @@ func TestParse(t *testing.T) {
NamespaceCapabilityCSIMountVolume,
NamespaceCapabilityCSIWriteVolume,
NamespaceCapabilitySubmitRecommendation,
NamespaceCapabilityHostVolumeCreate,
NamespaceCapabilityHostVolumeRead,
},
},
{
@@ -638,6 +646,54 @@ func TestParse(t *testing.T) {
},
},
},
{
`
namespace "default" {
capabilities = ["host-volume-register"]
}
namespace "other" {
capabilities = ["host-volume-create"]
}
namespace "foo" {
capabilities = ["host-volume-write"]
}
`,
"",
&Policy{
Namespaces: []*NamespacePolicy{
{
Name: "default",
Policy: "",
Capabilities: []string{
NamespaceCapabilityHostVolumeRegister,
NamespaceCapabilityHostVolumeCreate,
NamespaceCapabilityHostVolumeRead,
},
},
{
Name: "other",
Policy: "",
Capabilities: []string{
NamespaceCapabilityHostVolumeCreate,
NamespaceCapabilityHostVolumeRead,
},
},
{
Name: "foo",
Policy: "",
Capabilities: []string{
NamespaceCapabilityHostVolumeWrite,
NamespaceCapabilityHostVolumeRegister,
NamespaceCapabilityHostVolumeCreate,
NamespaceCapabilityHostVolumeDelete,
NamespaceCapabilityHostVolumeRead,
},
},
},
},
},
{
`
node_pool "pool-read-only" {
@@ -878,22 +934,18 @@ func TestParse(t *testing.T) {
}
for idx, tc := range tcases {
t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
t.Run(fmt.Sprintf("%02d", idx), func(t *testing.T) {
p, err := Parse(tc.Raw)
if err != nil {
if tc.ErrStr == "" {
t.Fatalf("Unexpected err: %v", err)
}
if !strings.Contains(err.Error(), tc.ErrStr) {
t.Fatalf("Unexpected err: %v", err)
}
return
if tc.ExpectErr == "" {
must.NoError(t, err)
} else {
must.ErrorContains(t, err, tc.ExpectErr)
}
if err == nil && tc.ErrStr != "" {
t.Fatalf("Missing expected err")
if tc.Expect != nil {
tc.Expect.Raw = tc.Raw
must.Eq(t, tc.Expect, p)
}
tc.Expect.Raw = tc.Raw
assert.EqualValues(t, tc.Expect, p)
})
}
}