open-nomad/acl/acl_test.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

778 lines
19 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2017-08-04 02:35:08 +00:00
package acl
import (
"testing"
"github.com/hashicorp/nomad/ci"
2017-08-04 02:35:08 +00:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
2017-08-04 02:35:08 +00:00
)
func TestCapabilitySet(t *testing.T) {
ci.Parallel(t)
2017-08-04 02:35:08 +00:00
var cs capabilitySet = make(map[string]struct{})
// Check no capabilities by default
if cs.Check(PolicyDeny) {
t.Fatalf("unexpected check")
}
// Do a set and check
cs.Set(PolicyDeny)
if !cs.Check(PolicyDeny) {
t.Fatalf("missing check")
}
// Clear and check
cs.Clear()
if cs.Check(PolicyDeny) {
t.Fatalf("unexpected check")
}
}
func TestMaxPrivilege(t *testing.T) {
ci.Parallel(t)
2017-08-04 02:35:08 +00:00
type tcase struct {
Privilege string
PrecedenceOver []string
}
tcases := []tcase{
{
PolicyDeny,
[]string{PolicyDeny, PolicyWrite, PolicyRead, ""},
},
{
PolicyWrite,
[]string{PolicyWrite, PolicyRead, ""},
},
{
PolicyRead,
[]string{PolicyRead, ""},
},
}
for idx1, tc := range tcases {
for idx2, po := range tc.PrecedenceOver {
if maxPrivilege(tc.Privilege, po) != tc.Privilege {
t.Fatalf("failed %d %d", idx1, idx2)
}
if maxPrivilege(po, tc.Privilege) != tc.Privilege {
t.Fatalf("failed %d %d", idx1, idx2)
}
}
}
}
func TestACLManagement(t *testing.T) {
ci.Parallel(t)
2017-10-13 21:36:02 +00:00
assert := assert.New(t)
2017-08-04 02:35:08 +00:00
// Create management ACL
acl, err := NewACL(true, nil)
2017-10-13 21:36:02 +00:00
assert.Nil(err)
2017-08-04 02:35:08 +00:00
// Check default namespace rights
2017-10-13 21:36:02 +00:00
assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
assert.True(acl.AllowNamespace("default"))
2017-08-04 02:35:08 +00:00
// Check non-specified namespace
2017-10-13 21:36:02 +00:00
assert.True(acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
assert.True(acl.AllowNamespace("foo"))
2017-08-04 02:35:08 +00:00
// Check the other simpler operations
2017-10-13 21:36:02 +00:00
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())
2017-08-04 02:35:08 +00:00
}
func TestACLMerge(t *testing.T) {
ci.Parallel(t)
2017-10-13 21:36:02 +00:00
assert := assert.New(t)
2017-08-04 02:35:08 +00:00
// Merge read + write policy
p1, err := Parse(readAll)
2017-10-13 21:36:02 +00:00
assert.Nil(err)
2017-08-04 02:35:08 +00:00
p2, err := Parse(writeAll)
2017-10-13 21:36:02 +00:00
assert.Nil(err)
2017-08-04 02:35:08 +00:00
acl, err := NewACL(false, []*Policy{p1, p2})
2017-10-13 21:36:02 +00:00
assert.Nil(err)
2017-08-04 02:35:08 +00:00
// Check default namespace rights
2017-10-13 21:36:02 +00:00
assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
assert.True(acl.AllowNamespace("default"))
2017-08-04 02:35:08 +00:00
// Check non-specified namespace
2017-10-13 21:36:02 +00:00
assert.False(acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
assert.False(acl.AllowNamespace("foo"))
2017-08-04 02:35:08 +00:00
// Check the other simpler operations
2017-10-13 21:36:02 +00:00
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())
2017-08-04 02:35:08 +00:00
// Merge read + blank
p3, err := Parse("")
2017-10-13 21:36:02 +00:00
assert.Nil(err)
2017-08-04 02:35:08 +00:00
acl, err = NewACL(false, []*Policy{p1, p3})
2017-10-13 21:36:02 +00:00
assert.Nil(err)
2017-08-04 02:35:08 +00:00
// Check default namespace rights
2017-10-13 21:36:02 +00:00
assert.True(acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
assert.False(acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
2017-08-04 02:35:08 +00:00
// Check non-specified namespace
2017-10-13 21:36:02 +00:00
assert.False(acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
2017-08-04 02:35:08 +00:00
// Check the other simpler operations
2017-10-13 21:36:02 +00:00
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())
2017-08-04 02:35:08 +00:00
// Merge read + deny
p4, err := Parse(denyAll)
2017-10-13 21:36:02 +00:00
assert.Nil(err)
2017-08-04 02:35:08 +00:00
acl, err = NewACL(false, []*Policy{p1, p4})
2017-10-13 21:36:02 +00:00
assert.Nil(err)
2017-08-04 02:35:08 +00:00
// Check default namespace rights
2017-10-13 21:36:02 +00:00
assert.False(acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
assert.False(acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
2017-08-04 02:35:08 +00:00
// Check non-specified namespace
2017-10-13 21:36:02 +00:00
assert.False(acl.AllowNamespaceOperation("foo", NamespaceCapabilityListJobs))
2017-08-04 02:35:08 +00:00
// Check the other simpler operations
2017-10-13 21:36:02 +00:00
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())
2017-08-04 02:35:08 +00:00
}
var readAll = `
namespace "default" {
policy = "read"
}
agent {
policy = "read"
}
node {
policy = "read"
}
operator {
policy = "read"
}
2017-10-13 21:36:02 +00:00
quota {
policy = "read"
}
2017-08-04 02:35:08 +00:00
`
var writeAll = `
namespace "default" {
policy = "write"
}
agent {
policy = "write"
}
node {
policy = "write"
}
operator {
policy = "write"
}
2017-10-13 21:36:02 +00:00
quota {
policy = "write"
}
2017-08-04 02:35:08 +00:00
`
var denyAll = `
namespace "default" {
policy = "deny"
}
agent {
policy = "deny"
}
node {
policy = "deny"
}
operator {
policy = "deny"
}
2017-10-13 21:36:02 +00:00
quota {
policy = "deny"
}
2017-08-04 02:35:08 +00:00
`
2017-10-13 21:36:02 +00:00
func TestAllowNamespace(t *testing.T) {
ci.Parallel(t)
2017-10-13 21:36:02 +00:00
tests := []struct {
name string
policy string
allow bool
namespace string
2017-10-13 21:36:02 +00:00
}{
{
name: "foo namespace - no capabilities",
policy: `namespace "foo" {}`,
allow: false,
namespace: "foo",
2017-10-13 21:36:02 +00:00
},
{
name: "foo namespace - deny policy",
policy: `namespace "foo" { policy = "deny" }`,
allow: false,
namespace: "foo",
2017-10-13 21:36:02 +00:00
},
{
name: "foo namespace - deny capability",
policy: `namespace "foo" { capabilities = ["deny"] }`,
allow: false,
namespace: "foo",
2017-10-13 21:36:02 +00:00
},
{
name: "foo namespace - with capability",
policy: `namespace "foo" { capabilities = ["list-jobs"] }`,
allow: true,
namespace: "foo",
2017-10-13 21:36:02 +00:00
},
{
name: "foo namespace - with policy",
policy: `namespace "foo" { policy = "read" }`,
allow: true,
namespace: "foo",
},
{
name: "wildcard namespace - no capabilities",
policy: `namespace "foo" {}`,
allow: false,
namespace: "*",
},
{
name: "wildcard namespace - deny policy",
policy: `namespace "foo" { policy = "deny" }`,
allow: false,
namespace: "*",
},
{
name: "wildcard namespace - deny capability",
policy: `namespace "foo" { capabilities = ["deny"] }`,
allow: false,
namespace: "*",
},
{
name: "wildcard namespace - with capability",
policy: `namespace "foo" { capabilities = ["list-jobs"] }`,
allow: true,
namespace: "*",
},
{
name: "wildcard namespace - with policy",
policy: `namespace "foo" { policy = "read" }`,
allow: true,
namespace: "*",
},
{
name: "wildcard namespace - no namespace rule",
policy: `agent { policy = "read" }`,
allow: false,
namespace: "*",
2017-10-13 21:36:02 +00:00
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
policy, err := Parse(tc.policy)
require.NoError(t, err)
2017-10-13 21:36:02 +00:00
acl, err := NewACL(false, []*Policy{policy})
require.NoError(t, err)
2017-10-13 21:36:02 +00:00
got := acl.AllowNamespace(tc.namespace)
require.Equal(t, tc.allow, got)
2017-10-13 21:36:02 +00:00
})
}
}
func TestWildcardNamespaceMatching(t *testing.T) {
ci.Parallel(t)
tests := []struct {
name string
policy string
allow bool
namespace string
}{
{
name: "wildcard matches",
policy: `namespace "prod-api-*" { policy = "write" }`,
allow: true,
namespace: "prod-api-services",
},
{
name: "non globbed namespaces are not wildcards",
policy: `namespace "prod-api" { policy = "write" }`,
allow: false,
namespace: "prod-api-services",
},
{
name: "concrete matches take precedence",
policy: `namespace "prod-api-services" { policy = "deny" }
namespace "prod-api-*" { policy = "write" }`,
allow: false,
namespace: "prod-api-services",
},
{
name: "glob match",
policy: `namespace "prod-api-*" { policy = "deny" }
namespace "prod-api-services" { policy = "write" }`,
allow: true,
namespace: "prod-api-services",
},
{
name: "closest character match wins - suffix",
policy: `namespace "*-api-services" { policy = "deny" }
2018-12-12 11:43:16 +00:00
namespace "prod-api-*" { policy = "write" }`, // 4 vs 8 chars
allow: false,
namespace: "prod-api-services",
2018-12-12 11:43:16 +00:00
},
{
name: "closest character match wins - prefix",
policy: `namespace "prod-api-*" { policy = "write" }
2018-12-12 11:43:16 +00:00
namespace "*-api-services" { policy = "deny" }`, // 4 vs 8 chars
allow: false,
namespace: "prod-api-services",
},
{
name: "wildcard namespace with glob match",
policy: `namespace "prod-api-*" { policy = "deny" }
namespace "prod-api-services" { policy = "write" }`,
allow: true,
namespace: "*",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
policy, err := Parse(tc.policy)
require.NoError(t, err)
require.NotNil(t, policy.Namespaces)
acl, err := NewACL(false, []*Policy{policy})
require.NoError(t, err)
got := acl.AllowNamespace(tc.namespace)
require.Equal(t, tc.allow, got)
})
}
}
func TestWildcardHostVolumeMatching(t *testing.T) {
ci.Parallel(t)
tests := []struct {
Policy string
Allow bool
}{
{ // Wildcard matches
Policy: `host_volume "prod-api-*" { policy = "write" }`,
Allow: true,
},
{ // Non globbed volumes are not wildcards
Policy: `host_volume "prod-api" { policy = "write" }`,
Allow: false,
},
{ // Concrete matches take precedence
Policy: `host_volume "prod-api-services" { policy = "deny" }
host_volume "prod-api-*" { policy = "write" }`,
Allow: false,
},
{
Policy: `host_volume "prod-api-*" { policy = "deny" }
host_volume "prod-api-services" { policy = "write" }`,
Allow: true,
},
{ // The closest character match wins
Policy: `host_volume "*-api-services" { policy = "deny" }
host_volume "prod-api-*" { policy = "write" }`, // 4 vs 8 chars
Allow: false,
},
{
Policy: `host_volume "prod-api-*" { policy = "write" }
host_volume "*-api-services" { policy = "deny" }`, // 4 vs 8 chars
Allow: false,
},
}
for _, tc := range tests {
t.Run(tc.Policy, func(t *testing.T) {
assert := assert.New(t)
policy, err := Parse(tc.Policy)
assert.NoError(err)
assert.NotNil(policy.HostVolumes)
acl, err := NewACL(false, []*Policy{policy})
assert.Nil(err)
assert.Equal(tc.Allow, acl.AllowHostVolume("prod-api-services"))
})
}
}
func TestVariablesMatching(t *testing.T) {
ci.Parallel(t)
tests := []struct {
name string
policy string
ns string
path string
op string
claim *ACLClaim
allow bool
}{
{
name: "concrete namespace with concrete path matches",
policy: `namespace "ns" {
variables { path "foo/bar" { capabilities = ["read"] }}}`,
ns: "ns",
path: "foo/bar",
op: "read",
allow: true,
},
{
name: "concrete namespace with concrete path matches for expanded caps",
policy: `namespace "ns" {
variables { path "foo/bar" { capabilities = ["read"] }}}`,
ns: "ns",
path: "foo/bar",
op: "list",
allow: true,
},
{
name: "concrete namespace with wildcard path matches",
policy: `namespace "ns" {
variables { path "foo/*" { capabilities = ["read"] }}}`,
ns: "ns",
path: "foo/bar",
op: "read",
allow: true,
},
{
name: "concrete namespace with non-prefix wildcard path matches",
policy: `namespace "ns" {
variables { path "*/bar" { capabilities = ["read"] }}}`,
ns: "ns",
path: "foo/bar",
op: "read",
allow: true,
},
{
name: "concrete namespace with overlapping wildcard path prefix over suffix matches",
policy: `namespace "ns" {
variables {
path "*/bar" { capabilities = ["list"] }
path "foo/*" { capabilities = ["write"] }
}}`,
ns: "ns",
path: "foo/bar",
op: "write",
allow: true,
},
{
name: "concrete namespace with overlapping wildcard path prefix over suffix denied",
policy: `namespace "ns" {
variables {
path "*/bar" { capabilities = ["list"] }
path "foo/*" { capabilities = ["write"] }
}}`,
ns: "ns",
path: "foo/bar",
op: "list",
allow: false,
},
{
name: "concrete namespace with wildcard path matches most specific only",
policy: `namespace "ns" {
variables {
path "*" { capabilities = ["read"] }
path "foo/*" { capabilities = ["read"] }
path "foo/bar" { capabilities = ["list"] }
}}`,
ns: "ns",
path: "foo/bar",
op: "read",
allow: false,
},
{
name: "concrete namespace with invalid concrete path fails",
policy: `namespace "ns" {
variables { path "bar" { capabilities = ["read"] }}}`,
ns: "ns",
path: "foo/bar",
op: "read",
allow: false,
},
{
name: "concrete namespace with invalid wildcard path fails",
policy: `namespace "ns" {
variables { path "*/foo" { capabilities = ["read"] }}}`,
ns: "ns",
path: "foo/bar",
op: "read",
allow: false,
},
{
name: "wildcard namespace with concrete path matches",
policy: `namespace "*" {
variables { path "foo/bar" { capabilities = ["read"] }}}`,
ns: "ns",
path: "foo/bar",
op: "read",
allow: true,
},
{
name: "wildcard namespace with invalid concrete path fails",
policy: `namespace "*" {
variables { path "bar" { capabilities = ["read"] }}}`,
ns: "ns",
path: "foo/bar",
op: "read",
allow: false,
},
{
name: "wildcard in user provided path fails",
policy: `namespace "ns" {
variables { path "foo/bar" { capabilities = ["read"] }}}`,
ns: "ns",
path: "*",
op: "read",
allow: false,
},
{
name: "wildcard attempt to bypass delimiter null byte fails",
policy: `namespace "ns" {
variables { path "foo/bar" { capabilities = ["read"] }}}`,
ns: "ns*",
path: "bar",
op: "read",
allow: false,
},
{
name: "wildcard with more specific denied path",
policy: `namespace "ns" {
variables {
path "*" { capabilities = ["list"] }
path "system/*" { capabilities = ["deny"] }}}`,
ns: "ns",
path: "system/not-allowed",
op: "list",
allow: false,
},
{
name: "multiple namespace with overlapping paths",
policy: `namespace "ns" {
variables {
path "*" { capabilities = ["list"] }
path "system/*" { capabilities = ["deny"] }}}
namespace "prod" {
variables {
path "*" { capabilities = ["list"]}}}`,
ns: "prod",
path: "system/is-allowed",
op: "list",
allow: true,
},
{
name: "claim with more specific policy",
policy: `namespace "ns" {
variables { path "nomad/jobs/example" { capabilities = ["deny"] }}}`,
ns: "ns",
path: "nomad/jobs/example",
op: "read",
claim: &ACLClaim{Namespace: "ns", Job: "example", Group: "foo", Task: "bar"},
allow: false,
},
{
name: "claim with less specific policy",
policy: `namespace "ns" {
variables { path "nomad/jobs" { capabilities = ["deny"] }}}`,
ns: "ns",
path: "nomad/jobs/example",
op: "read",
claim: &ACLClaim{Namespace: "ns", Job: "example", Group: "foo", Task: "bar"},
allow: true,
},
{
name: "claim with less specific wildcard policy",
policy: `namespace "ns" {
variables { path "nomad/jobs/*" { capabilities = ["deny"] }}}`,
ns: "ns",
path: "nomad/jobs/example",
op: "read",
claim: &ACLClaim{Namespace: "ns", Job: "example", Group: "foo", Task: "bar"},
allow: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
policy, err := Parse(tc.policy)
require.NoError(t, err)
require.NotNil(t, policy.Namespaces[0].Variables)
acl, err := NewACL(false, []*Policy{policy})
require.NoError(t, err)
allowed := acl.AllowVariableOperation(tc.ns, tc.path, tc.op, tc.claim)
require.Equal(t, tc.allow, allowed)
})
}
t.Run("search over namespace", func(t *testing.T) {
policy, err := Parse(`namespace "ns" {
variables { path "foo/bar" { capabilities = ["read"] }}}`)
require.NoError(t, err)
require.NotNil(t, policy.Namespaces[0].Variables)
acl, err := NewACL(false, []*Policy{policy})
require.NoError(t, err)
require.True(t, acl.AllowVariableSearch("ns"))
require.False(t, acl.AllowVariableSearch("no-access"))
})
}
func TestACL_matchingCapabilitySet_returnsAllMatches(t *testing.T) {
ci.Parallel(t)
tests := []struct {
Policy string
NS string
MatchingGlobs []string
}{
{
Policy: `namespace "production-*" { policy = "write" }`,
NS: "production-api",
MatchingGlobs: []string{"production-*"},
},
{
Policy: `namespace "prod-*" { policy = "write" }`,
NS: "production-api",
MatchingGlobs: nil,
},
{
Policy: `namespace "production-*" { policy = "write" }
namespace "production-*-api" { policy = "deny" }`,
NS: "production-admin-api",
MatchingGlobs: []string{"production-*", "production-*-api"},
},
}
for _, tc := range tests {
t.Run(tc.Policy, func(t *testing.T) {
assert := assert.New(t)
policy, err := Parse(tc.Policy)
assert.NoError(err)
assert.NotNil(policy.Namespaces)
acl, err := NewACL(false, []*Policy{policy})
assert.Nil(err)
var namespaces []string
for _, cs := range findAllMatchingWildcards(acl.wildcardNamespaces, tc.NS) {
namespaces = append(namespaces, cs.name)
}
assert.Equal(tc.MatchingGlobs, namespaces)
})
}
}
func TestACL_matchingCapabilitySet_difference(t *testing.T) {
ci.Parallel(t)
tests := []struct {
Policy string
NS string
Difference int
}{
{
Policy: `namespace "production-*" { policy = "write" }`,
NS: "production-api",
Difference: 3,
},
{
Policy: `namespace "production-*" { policy = "write" }`,
NS: "production-admin-api",
Difference: 9,
},
2018-12-12 11:43:16 +00:00
{
Policy: `namespace "production-**" { policy = "write" }`,
NS: "production-admin-api",
Difference: 9,
},
{
Policy: `namespace "*" { policy = "write" }`,
NS: "production-admin-api",
Difference: 20,
},
{
Policy: `namespace "*admin*" { policy = "write" }`,
NS: "production-admin-api",
Difference: 15,
},
}
for _, tc := range tests {
t.Run(tc.Policy, func(t *testing.T) {
assert := assert.New(t)
policy, err := Parse(tc.Policy)
assert.NoError(err)
assert.NotNil(policy.Namespaces)
acl, err := NewACL(false, []*Policy{policy})
assert.Nil(err)
matches := findAllMatchingWildcards(acl.wildcardNamespaces, tc.NS)
assert.Equal(tc.Difference, matches[0].difference)
})
}
}