mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
ACL policies are parsed when creating, updating, or compiling the resulting ACL object when used. This parsing was silently ignoring duplicate singleton keys, or invalid keys which does not grant any additional access, but is a poor UX and can be unexpected. This change parses all new policy writes and updates, so that duplicate or invalid keys return an error to the caller. This is called strict parsing. In order to correctly handle upgrades of clusters which have existing policies that would fall foul of the change, a lenient parsing mode is also available. This allows the policy to continue to be parsed and compiled after an upgrade without the need for an operator to correct the policy document prior to further use. Co-authored-by: Tim Gross <tgross@hashicorp.com>
1093 lines
26 KiB
Go
1093 lines
26 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package acl
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/shoenig/test/must"
|
|
)
|
|
|
|
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.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.
|
|
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())
|
|
must.True(t, acl.AllowServerOp())
|
|
must.True(t, acl.AllowClientOp())
|
|
}
|
|
|
|
func TestACLMerge(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
// Merge read + write policy
|
|
p1, err := Parse(readAll, PolicyParseStrict)
|
|
must.NoError(t, err)
|
|
p2, err := Parse(writeAll, PolicyParseStrict)
|
|
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())
|
|
must.False(t, acl.AllowServerOp())
|
|
must.False(t, acl.AllowClientOp())
|
|
|
|
// Merge read + blank
|
|
p3, err := Parse("", PolicyParseStrict)
|
|
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))
|
|
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))
|
|
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())
|
|
must.False(t, acl.AllowServerOp())
|
|
must.False(t, acl.AllowClientOp())
|
|
|
|
// Merge read + deny
|
|
p4, err := Parse(denyAll, PolicyParseStrict)
|
|
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())
|
|
must.False(t, acl.AllowServerOp())
|
|
}
|
|
|
|
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, PolicyParseStrict)
|
|
must.NoError(t, err)
|
|
|
|
acl, err := NewACL(false, []*Policy{policy})
|
|
must.NoError(t, err)
|
|
|
|
got := acl.AllowNamespace(tc.namespace)
|
|
must.Eq(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, PolicyParseStrict)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, policy.Namespaces)
|
|
|
|
acl, err := NewACL(false, []*Policy{policy})
|
|
must.NoError(t, err)
|
|
|
|
got := acl.AllowNamespace(tc.namespace)
|
|
must.Eq(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, PolicyParseStrict)
|
|
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)
|
|
must.True(t, got, must.Sprintf("expected operation %q to be allowed", op))
|
|
}
|
|
|
|
for _, op := range tc.denyOps {
|
|
got := acl.AllowNodePoolOperation(tc.pool, op)
|
|
must.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) {
|
|
policy, err := Parse(tc.Policy, PolicyParseStrict)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, policy.HostVolumes)
|
|
|
|
acl, err := NewACL(false, []*Policy{policy})
|
|
must.NoError(t, err)
|
|
|
|
must.Eq(t, 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, PolicyParseStrict)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, policy.Namespaces[0].Variables)
|
|
|
|
acl, err := NewACL(false, []*Policy{policy})
|
|
must.NoError(t, err)
|
|
allowed := acl.AllowVariableOperation(tc.ns, tc.path, tc.op, tc.claim)
|
|
must.Eq(t, tc.allow, allowed)
|
|
})
|
|
}
|
|
|
|
t.Run("search over namespace", func(t *testing.T) {
|
|
policy, err := Parse(`namespace "ns" {
|
|
variables { path "foo/bar" { capabilities = ["read"] }}}`, PolicyParseStrict)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, policy.Namespaces[0].Variables)
|
|
|
|
acl, err := NewACL(false, []*Policy{policy})
|
|
must.NoError(t, err)
|
|
must.True(t, acl.AllowVariableSearch("ns"))
|
|
must.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) {
|
|
policy, err := Parse(tc.Policy, PolicyParseStrict)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, policy.Namespaces)
|
|
|
|
acl, err := NewACL(false, []*Policy{policy})
|
|
must.NoError(t, err)
|
|
|
|
var namespaces []string
|
|
for _, cs := range findAllMatchingWildcards(acl.wildcardNamespaces, tc.NS) {
|
|
namespaces = append(namespaces, cs.name)
|
|
}
|
|
|
|
must.Eq(t, 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) {
|
|
policy, err := Parse(tc.Policy, PolicyParseStrict)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, policy.Namespaces)
|
|
|
|
acl, err := NewACL(false, []*Policy{policy})
|
|
must.NoError(t, err)
|
|
|
|
matches := findAllMatchingWildcards(acl.wildcardNamespaces, tc.NS)
|
|
must.Eq(t, tc.Difference, matches[0].difference)
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestAgentDebug(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
policy string
|
|
aclsDisabled bool
|
|
isDebugEnabled bool
|
|
expect bool
|
|
}{
|
|
{
|
|
name: "policy read debug not enabled",
|
|
policy: `agent { policy = "read" }`,
|
|
isDebugEnabled: false,
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "policy read debug enabled",
|
|
policy: `agent { policy = "read" }`,
|
|
isDebugEnabled: true,
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "policy no read debug enabled",
|
|
policy: `node { policy = "read" }`,
|
|
isDebugEnabled: true,
|
|
expect: false,
|
|
},
|
|
{
|
|
name: "policy no read debug not enabled",
|
|
policy: `node { policy = "read" }`,
|
|
isDebugEnabled: false,
|
|
expect: false,
|
|
},
|
|
{
|
|
name: "no acls debug enabled",
|
|
aclsDisabled: true,
|
|
isDebugEnabled: true,
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "no acls debug not enabled",
|
|
aclsDisabled: true,
|
|
isDebugEnabled: false,
|
|
expect: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
acl := ACLsDisabledACL
|
|
if !tc.aclsDisabled {
|
|
policy, err := Parse(tc.policy, PolicyParseStrict)
|
|
must.NoError(t, err)
|
|
|
|
acl, err = NewACL(false, []*Policy{policy})
|
|
must.NoError(t, err)
|
|
}
|
|
|
|
must.Eq(t, tc.expect, acl.AllowAgentDebug(tc.isDebugEnabled))
|
|
})
|
|
}
|
|
}
|