1026 lines
24 KiB
Go
1026 lines
24 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package acl
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/shoenig/test/must"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCapabilitySet(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
// Create management ACL
|
|
acl, err := NewACL(true, nil)
|
|
must.NoError(t, err)
|
|
|
|
// Check default namespace rights
|
|
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
|
|
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
|
|
must.True(t, acl.AllowNamespace("default"))
|
|
|
|
// Check non-specified namespace
|
|
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
|
|
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)
|
|
|
|
// Merge read + write policy
|
|
p1, err := Parse(readAll)
|
|
must.NoError(t, err)
|
|
p2, err := Parse(writeAll)
|
|
must.NoError(t, err)
|
|
acl, err := NewACL(false, []*Policy{p1, p2})
|
|
must.NoError(t, err)
|
|
|
|
// Check default namespace rights
|
|
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
|
|
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
|
|
must.True(t, acl.AllowNamespace("default"))
|
|
|
|
// Check non-specified namespace
|
|
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
|
|
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("")
|
|
must.NoError(t, err)
|
|
acl, err = NewACL(false, []*Policy{p1, p3})
|
|
must.NoError(t, err)
|
|
|
|
// Check default namespace rights
|
|
must.True(t, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
|
|
must.False(t, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
|
|
|
|
// Check non-specified namespace
|
|
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
|
|
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)
|
|
must.NoError(t, err)
|
|
acl, err = NewACL(false, []*Policy{p1, p4})
|
|
must.NoError(t, err)
|
|
|
|
// Check default namespace rights
|
|
must.False(t, acl.AllowNamespaceOperation("default", NamespaceCapabilityListJobs))
|
|
must.False(t, acl.AllowNamespaceOperation("default", NamespaceCapabilitySubmitJob))
|
|
|
|
// Check non-specified namespace
|
|
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
|
|
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"
|
|
}
|
|
node {
|
|
policy = "read"
|
|
}
|
|
operator {
|
|
policy = "read"
|
|
}
|
|
quota {
|
|
policy = "read"
|
|
}
|
|
`
|
|
|
|
var writeAll = `
|
|
namespace "default" {
|
|
policy = "write"
|
|
}
|
|
node_pool "my-pool" {
|
|
policy = "write"
|
|
}
|
|
agent {
|
|
policy = "write"
|
|
}
|
|
node {
|
|
policy = "write"
|
|
}
|
|
operator {
|
|
policy = "write"
|
|
}
|
|
quota {
|
|
policy = "write"
|
|
}
|
|
`
|
|
|
|
var denyAll = `
|
|
namespace "default" {
|
|
policy = "deny"
|
|
}
|
|
node_pool "my-pool" {
|
|
policy = "deny"
|
|
}
|
|
agent {
|
|
policy = "deny"
|
|
}
|
|
node {
|
|
policy = "deny"
|
|
}
|
|
operator {
|
|
policy = "deny"
|
|
}
|
|
quota {
|
|
policy = "deny"
|
|
}
|
|
`
|
|
|
|
func TestAllowNamespace(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
policy string
|
|
allow bool
|
|
namespace string
|
|
}{
|
|
{
|
|
name: "foo namespace - no capabilities",
|
|
policy: `namespace "foo" {}`,
|
|
allow: false,
|
|
namespace: "foo",
|
|
},
|
|
{
|
|
name: "foo namespace - deny policy",
|
|
policy: `namespace "foo" { policy = "deny" }`,
|
|
allow: false,
|
|
namespace: "foo",
|
|
},
|
|
{
|
|
name: "foo namespace - deny capability",
|
|
policy: `namespace "foo" { capabilities = ["deny"] }`,
|
|
allow: false,
|
|
namespace: "foo",
|
|
},
|
|
{
|
|
name: "foo namespace - with capability",
|
|
policy: `namespace "foo" { capabilities = ["list-jobs"] }`,
|
|
allow: true,
|
|
namespace: "foo",
|
|
},
|
|
{
|
|
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: "*",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
policy, err := Parse(tc.policy)
|
|
require.NoError(t, err)
|
|
|
|
acl, err := NewACL(false, []*Policy{policy})
|
|
require.NoError(t, err)
|
|
|
|
got := acl.AllowNamespace(tc.namespace)
|
|
require.Equal(t, tc.allow, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
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" }
|
|
namespace "prod-api-*" { policy = "write" }`, // 4 vs 8 chars
|
|
allow: false,
|
|
namespace: "prod-api-services",
|
|
},
|
|
{
|
|
name: "closest character match wins - prefix",
|
|
policy: `namespace "prod-api-*" { policy = "write" }
|
|
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 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)
|
|
|
|
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,
|
|
},
|
|
{
|
|
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)
|
|
})
|
|
}
|
|
|
|
}
|