From b596e67f4752d5e6d364a0bac10b9291c65af7af Mon Sep 17 00:00:00 2001 From: Lang Martin Date: Tue, 17 Mar 2020 17:32:39 -0400 Subject: [PATCH] csi: implement volume ACLs (#7339) * acl/policy: add the volume ACL policies * nomad/csi_endpoint: enforce ACLs for volume access * nomad/search_endpoint_oss: volume acls * acl/acl: add plugin read as a global policy * acl/policy: add PluginPolicy global cap type * nomad/csi_endpoint: check the global plugin ACL policy * nomad/mock/acl: PluginPolicy * nomad/csi_endpoint: fix list rebase * nomad/core_sched_test: new test since #7358 * nomad/csi_endpoint_test: use correct permissions for list * nomad/csi_endpoint: allowCSIMount keeps ACL checks together * nomad/job_endpoint: check mount permission for jobs * nomad/job_endpoint_test: need plugin read, too --- acl/acl.go | 20 +++++++++++++ acl/policy.go | 54 ++++++++++++++++++++++++------------ acl/policy_test.go | 14 ++++++++++ nomad/core_sched_test.go | 13 +++++---- nomad/csi_endpoint.go | 30 ++++++++++++++------ nomad/csi_endpoint_test.go | 19 +++++++------ nomad/job_endpoint.go | 35 +++++++++++++---------- nomad/job_endpoint_test.go | 15 ++++++++-- nomad/mock/acl.go | 5 ++++ nomad/search_endpoint_oss.go | 20 ++++++++++++- 10 files changed, 167 insertions(+), 58 deletions(-) diff --git a/acl/acl.go b/acl/acl.go index 2a6be0e5a..569158968 100644 --- a/acl/acl.go +++ b/acl/acl.go @@ -62,6 +62,7 @@ type ACL struct { node string operator string quota string + plugin string } // maxPrivilege returns the policy which grants the most privilege @@ -193,6 +194,9 @@ func NewACL(management bool, policies []*Policy) (*ACL, error) { 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 @@ -477,6 +481,22 @@ func (a *ACL) AllowQuotaWrite() bool { } } +// AllowPluginRead checks if read operations are allowed for all plugins +func (a *ACL) AllowPluginRead() bool { + // ACL is nil only if ACLs are disabled + if a == nil { + return true + } + switch { + case a.management: + 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 diff --git a/acl/policy.go b/acl/policy.go index b077d053e..4287fa6d6 100644 --- a/acl/policy.go +++ b/acl/policy.go @@ -35,7 +35,10 @@ const ( NamespaceCapabilitySentinelOverride = "sentinel-override" NamespaceCapabilityPrivilegedTask = "privileged-task" NamespaceCapabilityCSIAccess = "csi-access" - NamespaceCapabilityCSICreateVolume = "csi-create-volume" + NamespaceCapabilityCSIWriteVolume = "csi-write-volume" + NamespaceCapabilityCSIReadVolume = "csi-read-volume" + NamespaceCapabilityCSIListVolume = "csi-list-volume" + NamespaceCapabilityCSIMountVolume = "csi-mount-volume" ) var ( @@ -65,6 +68,7 @@ type Policy struct { Node *NodePolicy `hcl:"node"` Operator *OperatorPolicy `hcl:"operator"` Quota *QuotaPolicy `hcl:"quota"` + Plugin *PluginPolicy `hcl:"plugin"` Raw string `hcl:"-"` } @@ -76,7 +80,8 @@ func (p *Policy) IsEmpty() bool { p.Agent == nil && p.Node == nil && p.Operator == nil && - p.Quota == nil + p.Quota == nil && + p.Plugin == nil } // NamespacePolicy is the policy for a specific namespace @@ -109,6 +114,10 @@ type QuotaPolicy struct { Policy string } +type PluginPolicy struct { + Policy string +} + // isPolicyValid makes sure the given string matches one of the valid policies. func isPolicyValid(policy string) bool { switch policy { @@ -126,7 +135,8 @@ func isNamespaceCapabilityValid(cap string) bool { NamespaceCapabilitySubmitJob, NamespaceCapabilityDispatchJob, NamespaceCapabilityReadLogs, NamespaceCapabilityReadFS, NamespaceCapabilityAllocLifecycle, NamespaceCapabilityAllocExec, NamespaceCapabilityAllocNodeExec, - NamespaceCapabilityCSIAccess, NamespaceCapabilityCSICreateVolume: + NamespaceCapabilityCSIAccess, // TODO(langmartin): remove after plugin caps are done + NamespaceCapabilityCSIReadVolume, NamespaceCapabilityCSIWriteVolume, NamespaceCapabilityCSIListVolume, NamespaceCapabilityCSIMountVolume: return true // Separate the enterprise-only capabilities case NamespaceCapabilitySentinelOverride: @@ -139,25 +149,31 @@ func isNamespaceCapabilityValid(cap string) bool { // expandNamespacePolicy provides the equivalent set of capabilities for // a namespace policy func expandNamespacePolicy(policy string) []string { + read := []string{ + NamespaceCapabilityListJobs, + NamespaceCapabilityReadJob, + NamespaceCapabilityCSIListVolume, + NamespaceCapabilityCSIReadVolume, + } + + write := append(read, []string{ + NamespaceCapabilitySubmitJob, + NamespaceCapabilityDispatchJob, + NamespaceCapabilityReadLogs, + NamespaceCapabilityReadFS, + NamespaceCapabilityAllocExec, + NamespaceCapabilityAllocLifecycle, + NamespaceCapabilityCSIMountVolume, + NamespaceCapabilityCSIWriteVolume, + }...) + switch policy { case PolicyDeny: return []string{NamespaceCapabilityDeny} case PolicyRead: - return []string{ - NamespaceCapabilityListJobs, - NamespaceCapabilityReadJob, - } + return read case PolicyWrite: - return []string{ - NamespaceCapabilityListJobs, - NamespaceCapabilityReadJob, - NamespaceCapabilitySubmitJob, - NamespaceCapabilityDispatchJob, - NamespaceCapabilityReadLogs, - NamespaceCapabilityReadFS, - NamespaceCapabilityAllocExec, - NamespaceCapabilityAllocLifecycle, - } + return write default: return nil } @@ -265,5 +281,9 @@ func Parse(rules string) (*Policy, error) { if p.Quota != nil && !isPolicyValid(p.Quota.Policy) { return nil, fmt.Errorf("Invalid quota policy: %#v", p.Quota) } + + if p.Plugin != nil && !isPolicyValid(p.Plugin.Policy) { + return nil, fmt.Errorf("Invalid plugin policy: %#v", p.Plugin) + } return p, nil } diff --git a/acl/policy_test.go b/acl/policy_test.go index 831e80076..48b5a6a35 100644 --- a/acl/policy_test.go +++ b/acl/policy_test.go @@ -30,6 +30,8 @@ func TestParse(t *testing.T) { Capabilities: []string{ NamespaceCapabilityListJobs, NamespaceCapabilityReadJob, + NamespaceCapabilityCSIListVolume, + NamespaceCapabilityCSIReadVolume, }, }, }, @@ -58,6 +60,9 @@ func TestParse(t *testing.T) { quota { policy = "read" } + plugin { + policy = "read" + } `, "", &Policy{ @@ -68,6 +73,8 @@ func TestParse(t *testing.T) { Capabilities: []string{ NamespaceCapabilityListJobs, NamespaceCapabilityReadJob, + NamespaceCapabilityCSIListVolume, + NamespaceCapabilityCSIReadVolume, }, }, { @@ -76,12 +83,16 @@ func TestParse(t *testing.T) { Capabilities: []string{ NamespaceCapabilityListJobs, NamespaceCapabilityReadJob, + NamespaceCapabilityCSIListVolume, + NamespaceCapabilityCSIReadVolume, NamespaceCapabilitySubmitJob, NamespaceCapabilityDispatchJob, NamespaceCapabilityReadLogs, NamespaceCapabilityReadFS, NamespaceCapabilityAllocExec, NamespaceCapabilityAllocLifecycle, + NamespaceCapabilityCSIMountVolume, + NamespaceCapabilityCSIWriteVolume, }, }, { @@ -104,6 +115,9 @@ func TestParse(t *testing.T) { Quota: &QuotaPolicy{ Policy: PolicyRead, }, + Plugin: &PluginPolicy{ + Policy: PolicyRead, + }, }, }, { diff --git a/nomad/core_sched_test.go b/nomad/core_sched_test.go index 27f534877..773804e24 100644 --- a/nomad/core_sched_test.go +++ b/nomad/core_sched_test.go @@ -2340,16 +2340,17 @@ func TestCSI_GCVolumeClaims_Controller(t *testing.T) { err := state.UpsertNode(99, node) require.NoError(t, err) volId0 := uuid.Generate() + ns := structs.DefaultNamespace vols := []*structs.CSIVolume{{ ID: volId0, - Namespace: "notTheNamespace", + Namespace: ns, PluginID: "csi-plugin-example", AccessMode: structs.CSIVolumeAccessModeMultiNodeSingleWriter, AttachmentMode: structs.CSIVolumeAttachmentModeFilesystem, }} err = state.CSIVolumeRegister(100, vols) require.NoError(t, err) - vol, err := state.CSIVolumeByID(ws, volId0) + vol, err := state.CSIVolumeByID(ws, ns, volId0) require.NoError(t, err) require.True(t, vol.ControllerRequired) @@ -2387,11 +2388,11 @@ func TestCSI_GCVolumeClaims_Controller(t *testing.T) { require.NoError(t, err) // Claim the volumes and verify the claims were set - err = state.CSIVolumeClaim(105, volId0, alloc1, structs.CSIVolumeClaimWrite) + err = state.CSIVolumeClaim(105, ns, volId0, alloc1, structs.CSIVolumeClaimWrite) require.NoError(t, err) - err = state.CSIVolumeClaim(106, volId0, alloc2, structs.CSIVolumeClaimRead) + err = state.CSIVolumeClaim(106, ns, volId0, alloc2, structs.CSIVolumeClaimRead) require.NoError(t, err) - vol, err = state.CSIVolumeByID(ws, volId0) + vol, err = state.CSIVolumeByID(ws, ns, volId0) require.NoError(t, err) require.Len(t, vol.ReadAllocs, 1) require.Len(t, vol.WriteAllocs, 1) @@ -2425,7 +2426,7 @@ func TestCSI_GCVolumeClaims_Controller(t *testing.T) { require.NoError(t, err) // Verify both claims were released - vol, err = state.CSIVolumeByID(ws, volId0) + vol, err = state.CSIVolumeByID(ws, ns, volId0) require.NoError(t, err) require.Len(t, vol.ReadAllocs, 0) require.Len(t, vol.WriteAllocs, 0) diff --git a/nomad/csi_endpoint.go b/nomad/csi_endpoint.go index 3a6981e1a..758570bec 100644 --- a/nomad/csi_endpoint.go +++ b/nomad/csi_endpoint.go @@ -95,13 +95,16 @@ func (v *CSIVolume) List(args *structs.CSIVolumeListRequest, reply *structs.CSIV return err } - allowCSIAccess := acl.NamespaceValidator(acl.NamespaceCapabilityCSIAccess) + allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIListVolume, + acl.NamespaceCapabilityCSIReadVolume, + acl.NamespaceCapabilityCSIMountVolume, + acl.NamespaceCapabilityListJobs) aclObj, err := v.srv.QueryACLObj(&args.QueryOptions, false) if err != nil { return err } - if !allowCSIAccess(aclObj, args.RequestNamespace()) { + if !allowVolume(aclObj, args.RequestNamespace()) { return structs.ErrPermissionDenied } @@ -163,7 +166,9 @@ func (v *CSIVolume) Get(args *structs.CSIVolumeGetRequest, reply *structs.CSIVol return err } - allowCSIAccess := acl.NamespaceValidator(acl.NamespaceCapabilityCSIAccess) + allowCSIAccess := acl.NamespaceValidator(acl.NamespaceCapabilityCSIReadVolume, + acl.NamespaceCapabilityCSIMountVolume, + acl.NamespaceCapabilityReadJob) aclObj, err := v.srv.QueryACLObj(&args.QueryOptions, true) if err != nil { return err @@ -254,7 +259,7 @@ func (v *CSIVolume) Register(args *structs.CSIVolumeRegisterRequest, reply *stru return err } - allowCSIVolumeManagement := acl.NamespaceValidator(acl.NamespaceCapabilityCSICreateVolume) + allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIWriteVolume) aclObj, err := v.srv.WriteACLObj(&args.WriteRequest, false) if err != nil { return err @@ -263,7 +268,7 @@ func (v *CSIVolume) Register(args *structs.CSIVolumeRegisterRequest, reply *stru metricsStart := time.Now() defer metrics.MeasureSince([]string{"nomad", "volume", "register"}, metricsStart) - if !allowCSIVolumeManagement(aclObj, args.RequestNamespace()) { + if !allowVolume(aclObj, args.RequestNamespace()) || !aclObj.AllowPluginRead() { return structs.ErrPermissionDenied } @@ -275,6 +280,7 @@ func (v *CSIVolume) Register(args *structs.CSIVolumeRegisterRequest, reply *stru if err = vol.Validate(); err != nil { return err } + plugin, err := v.srv.pluginValidateVolume(args, vol) if err != nil { return err @@ -304,7 +310,7 @@ func (v *CSIVolume) Deregister(args *structs.CSIVolumeDeregisterRequest, reply * return err } - allowCSIVolumeManagement := acl.NamespaceValidator(acl.NamespaceCapabilityCSICreateVolume) + allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIWriteVolume) aclObj, err := v.srv.WriteACLObj(&args.WriteRequest, false) if err != nil { return err @@ -314,7 +320,7 @@ func (v *CSIVolume) Deregister(args *structs.CSIVolumeDeregisterRequest, reply * defer metrics.MeasureSince([]string{"nomad", "volume", "deregister"}, metricsStart) ns := args.RequestNamespace() - if !allowCSIVolumeManagement(aclObj, ns) { + if !allowVolume(aclObj, ns) { return structs.ErrPermissionDenied } @@ -338,7 +344,7 @@ func (v *CSIVolume) Claim(args *structs.CSIVolumeClaimRequest, reply *structs.CS return err } - allowCSIAccess := acl.NamespaceValidator(acl.NamespaceCapabilityCSIAccess) + allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIMountVolume) aclObj, err := v.srv.WriteACLObj(&args.WriteRequest, true) if err != nil { return err @@ -347,7 +353,7 @@ func (v *CSIVolume) Claim(args *structs.CSIVolumeClaimRequest, reply *structs.CS metricsStart := time.Now() defer metrics.MeasureSince([]string{"nomad", "volume", "claim"}, metricsStart) - if !allowCSIAccess(aclObj, args.RequestNamespace()) { + if !allowVolume(aclObj, args.RequestNamespace()) || !aclObj.AllowPluginRead() { return structs.ErrPermissionDenied } @@ -374,6 +380,12 @@ func (v *CSIVolume) Claim(args *structs.CSIVolumeClaimRequest, reply *structs.CS return nil } +// allowCSIMount is called on Job register to check mount permission +func allowCSIMount(aclObj *acl.ACL, namespace string) bool { + return aclObj.AllowPluginRead() && + aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityCSIMountVolume) +} + // CSIPlugin wraps the structs.CSIPlugin with request data and server context type CSIPlugin struct { srv *Server diff --git a/nomad/csi_endpoint_test.go b/nomad/csi_endpoint_test.go index 46987ed52..8548dc772 100644 --- a/nomad/csi_endpoint_test.go +++ b/nomad/csi_endpoint_test.go @@ -70,7 +70,7 @@ func TestCSIVolumeEndpoint_Get_ACL(t *testing.T) { state := srv.fsm.State() state.BootstrapACLTokens(1, 0, mock.ACLManagementToken()) srv.config.ACLEnabled = true - policy := mock.NamespacePolicy(ns, "", []string{acl.NamespaceCapabilityCSIAccess}) + policy := mock.NamespacePolicy(ns, "", []string{acl.NamespaceCapabilityCSIReadVolume}) validToken := mock.CreatePolicyAndToken(t, state, 1001, "csi-access", policy) codec := rpcClient(t, srv) @@ -319,10 +319,11 @@ func TestCSIVolumeEndpoint_ClaimWithController(t *testing.T) { ns := structs.DefaultNamespace state := srv.fsm.State() state.BootstrapACLTokens(1, 0, mock.ACLManagementToken()) - policy := mock.NamespacePolicy(ns, "", - []string{acl.NamespaceCapabilityCSICreateVolume, acl.NamespaceCapabilityCSIAccess}) - accessToken := mock.CreatePolicyAndToken(t, state, 1001, - acl.NamespaceCapabilityCSIAccess, policy) + + policy := mock.NamespacePolicy(ns, "", []string{acl.NamespaceCapabilityCSIMountVolume}) + + mock.PluginPolicy("read") + accessToken := mock.CreatePolicyAndToken(t, state, 1001, "claim", policy) + codec := rpcClient(t, srv) id0 := uuid.Generate() @@ -397,7 +398,8 @@ func TestCSIVolumeEndpoint_List(t *testing.T) { srv.config.ACLEnabled = true codec := rpcClient(t, srv) - nsPolicy := mock.NamespacePolicy(ns, "", []string{acl.NamespaceCapabilityCSIAccess}) + nsPolicy := mock.NamespacePolicy(ns, "", []string{acl.NamespaceCapabilityCSIReadVolume}) + + mock.PluginPolicy("read") nsTok := mock.CreatePolicyAndToken(t, state, 1000, "csi-access", nsPolicy) id0 := uuid.Generate() @@ -463,8 +465,9 @@ func TestCSIVolumeEndpoint_List(t *testing.T) { require.Equal(t, vols[1].ID, resp.Volumes[0].ID) // Query by PluginID in ms - msPolicy := mock.NamespacePolicy(ms, "", []string{acl.NamespaceCapabilityCSIAccess}) - msTok := mock.CreatePolicyAndToken(t, state, 1003, "csi-access", msPolicy) + msPolicy := mock.NamespacePolicy(ms, "", []string{acl.NamespaceCapabilityCSIListVolume}) + + mock.PluginPolicy("read") + msTok := mock.CreatePolicyAndToken(t, state, 1003, "csi-access2", msPolicy) req = &structs.CSIVolumeListRequest{ PluginID: "paddy", diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index 76c235a9f..10e0417a9 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -107,23 +107,28 @@ func (j *Job) Register(args *structs.JobRegisterRequest, reply *structs.JobRegis // Validate Volume Permissions for _, tg := range args.Job.TaskGroups { for _, vol := range tg.Volumes { - if vol.Type != structs.VolumeTypeHost { + switch vol.Type { + case structs.VolumeTypeCSI: + if !allowCSIMount(aclObj, args.RequestNamespace()) { + return structs.ErrPermissionDenied + } + case structs.VolumeTypeHost: + // If a volume is readonly, then we allow access if the user has ReadOnly + // or ReadWrite access to the volume. Otherwise we only allow access if + // they have ReadWrite access. + if vol.ReadOnly { + if !aclObj.AllowHostVolumeOperation(vol.Source, acl.HostVolumeCapabilityMountReadOnly) && + !aclObj.AllowHostVolumeOperation(vol.Source, acl.HostVolumeCapabilityMountReadWrite) { + return structs.ErrPermissionDenied + } + } else { + if !aclObj.AllowHostVolumeOperation(vol.Source, acl.HostVolumeCapabilityMountReadWrite) { + return structs.ErrPermissionDenied + } + } + default: return structs.ErrPermissionDenied } - - // If a volume is readonly, then we allow access if the user has ReadOnly - // or ReadWrite access to the volume. Otherwise we only allow access if - // they have ReadWrite access. - if vol.ReadOnly { - if !aclObj.AllowHostVolumeOperation(vol.Source, acl.HostVolumeCapabilityMountReadOnly) && - !aclObj.AllowHostVolumeOperation(vol.Source, acl.HostVolumeCapabilityMountReadWrite) { - return structs.ErrPermissionDenied - } - } else { - if !aclObj.AllowHostVolumeOperation(vol.Source, acl.HostVolumeCapabilityMountReadWrite) { - return structs.ErrPermissionDenied - } - } } for _, t := range tg.Tasks { diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go index 47c78c3a1..4373a61f0 100644 --- a/nomad/job_endpoint_test.go +++ b/nomad/job_endpoint_test.go @@ -384,6 +384,10 @@ func TestJobEndpoint_Register_ACL(t *testing.T) { Source: "prod-ca-certs", ReadOnly: readonlyVolume, }, + "csi": { + Type: structs.VolumeTypeCSI, + Source: "prod-db", + }, } tg.Tasks[0].VolumeMounts = []*structs.VolumeMount{ @@ -404,11 +408,18 @@ func TestJobEndpoint_Register_ACL(t *testing.T) { volumesPolicyReadWrite := mock.HostVolumePolicy("prod-*", "", []string{acl.HostVolumeCapabilityMountReadWrite}) - submitJobWithVolumesReadWriteToken := mock.CreatePolicyAndToken(t, s1.State(), 1002, "test-submit-volumes", submitJobPolicy+"\n"+volumesPolicyReadWrite) + volumesPolicyCSIMount := mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityCSIMountVolume}) + + mock.PluginPolicy("read") + + submitJobWithVolumesReadWriteToken := mock.CreatePolicyAndToken(t, s1.State(), 1002, "test-submit-volumes", submitJobPolicy+ + volumesPolicyReadWrite+ + volumesPolicyCSIMount) volumesPolicyReadOnly := mock.HostVolumePolicy("prod-*", "", []string{acl.HostVolumeCapabilityMountReadOnly}) - submitJobWithVolumesReadOnlyToken := mock.CreatePolicyAndToken(t, s1.State(), 1003, "test-submit-volumes-readonly", submitJobPolicy+"\n"+volumesPolicyReadOnly) + submitJobWithVolumesReadOnlyToken := mock.CreatePolicyAndToken(t, s1.State(), 1003, "test-submit-volumes-readonly", submitJobPolicy+ + volumesPolicyReadOnly+ + volumesPolicyCSIMount) cases := []struct { Name string diff --git a/nomad/mock/acl.go b/nomad/mock/acl.go index 599bed4b5..d41a76016 100644 --- a/nomad/mock/acl.go +++ b/nomad/mock/acl.go @@ -73,6 +73,11 @@ func QuotaPolicy(policy string) string { return fmt.Sprintf("quota {\n\tpolicy = %q\n}\n", policy) } +// PluginPolicy is a helper for generating the hcl for a given plugin policy. +func PluginPolicy(policy string) string { + return fmt.Sprintf("plugin {\n\tpolicy = %q\n}\n", policy) +} + // CreatePolicy creates a policy with the given name and rule. func CreatePolicy(t testing.T, state StateStore, index uint64, name, rule string) { t.Helper() diff --git a/nomad/search_endpoint_oss.go b/nomad/search_endpoint_oss.go index aaecf1b34..b4d80c634 100644 --- a/nomad/search_endpoint_oss.go +++ b/nomad/search_endpoint_oss.go @@ -44,7 +44,13 @@ func anySearchPerms(aclObj *acl.ACL, namespace string, context structs.Context) nodeRead := aclObj.AllowNodeRead() jobRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob) - if !nodeRead && !jobRead { + allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIListVolume, + acl.NamespaceCapabilityCSIReadVolume, + acl.NamespaceCapabilityListJobs, + acl.NamespaceCapabilityReadJob) + volRead := allowVolume(aclObj, namespace) + + if !nodeRead && !jobRead && !volRead { return false } @@ -60,6 +66,9 @@ func anySearchPerms(aclObj *acl.ACL, namespace string, context structs.Context) return false } } + if !volRead && context == structs.Volumes { + return false + } return true } @@ -83,6 +92,11 @@ func searchContexts(aclObj *acl.ACL, namespace string, context structs.Context) } jobRead := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob) + allowVolume := acl.NamespaceValidator(acl.NamespaceCapabilityCSIListVolume, + acl.NamespaceCapabilityCSIReadVolume, + acl.NamespaceCapabilityListJobs, + acl.NamespaceCapabilityReadJob) + volRead := allowVolume(aclObj, namespace) // Filter contexts down to those the ACL grants access to available := make([]structs.Context, 0, len(all)) @@ -96,6 +110,10 @@ func searchContexts(aclObj *acl.ACL, namespace string, context structs.Context) if aclObj.AllowNodeRead() { available = append(available, c) } + case structs.Volumes: + if volRead { + available = append(available, c) + } } } return available