open-nomad/acl/policy_test.go
Luiz Aoqui ac90c6f008 acl: fix parsing of policies with blocks w/o label
An ACL policy with a block without label generates unexpected results.
For example, a policy such as this:

```
namespace {
  policy = "read"
}
```

Is applied to a namespace called `policy` instead of the documented
behaviour of applying it to the `default` namespace.

This happens because of the way HCL1 decodes blocks. Since it doesn't
know if a block is expected to have a label it applies the `key` tag to
the content of the block and, in the example above, the first key is
`policy`, so it sets that as the `namespace` block label.

Since this happens internally in the HCL decoder it's not possible to
detect the problem externally.

Fixing the problem inside the decoder is challenging because the JSON
and HCL parsers generate different ASTs that makes impossible to
differentiate between a JSON tree from an invalid HCL tree within the
decoder.

The fix in this commit consists of manually parsing the policy after
decoding to clear labels that were not set in the file. This allows the
validation rules to consistently catch and return any errors, no matter
if the policy is an invalid HCL or JSON.
2023-07-18 12:09:37 -04:00

901 lines
16 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package acl
import (
"fmt"
"strings"
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/stretchr/testify/assert"
)
func TestParse(t *testing.T) {
ci.Parallel(t)
type tcase struct {
Raw string
ErrStr string
Expect *Policy
}
tcases := []tcase{
{
`
namespace "default" {
policy = "read"
}
`,
"",
&Policy{
Namespaces: []*NamespacePolicy{
{
Name: "default",
Policy: PolicyRead,
Capabilities: []string{
NamespaceCapabilityListJobs,
NamespaceCapabilityParseJob,
NamespaceCapabilityReadJob,
NamespaceCapabilityCSIListVolume,
NamespaceCapabilityCSIReadVolume,
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
},
},
},
},
},
{
`
namespace "default" {
policy = "read"
}
namespace "other" {
policy = "write"
}
namespace "secret" {
capabilities = ["deny", "read-logs"]
}
namespace "apps" {
variables {
path "jobs/write-does-not-imply-read-or-delete" {
capabilities = ["write"]
}
path "project/read-implies-list" {
capabilities = ["read"]
}
path "project/explicit" {
capabilities = ["read", "list", "destroy"]
}
}
}
namespace "autoscaler" {
policy = "scale"
}
host_volume "production-tls-*" {
capabilities = ["mount-readonly"]
}
host_volume "staging-tls-*" {
policy = "write"
}
node_pool "prod" {
capabilities = ["read"]
}
node_pool "dev" {
policy = "write"
}
agent {
policy = "read"
}
node {
policy = "write"
}
operator {
policy = "deny"
}
quota {
policy = "read"
}
plugin {
policy = "read"
}
`,
"",
&Policy{
Namespaces: []*NamespacePolicy{
{
Name: "default",
Policy: PolicyRead,
Capabilities: []string{
NamespaceCapabilityListJobs,
NamespaceCapabilityParseJob,
NamespaceCapabilityReadJob,
NamespaceCapabilityCSIListVolume,
NamespaceCapabilityCSIReadVolume,
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
},
},
{
Name: "other",
Policy: PolicyWrite,
Capabilities: []string{
NamespaceCapabilityListJobs,
NamespaceCapabilityParseJob,
NamespaceCapabilityReadJob,
NamespaceCapabilityCSIListVolume,
NamespaceCapabilityCSIReadVolume,
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
NamespaceCapabilityScaleJob,
NamespaceCapabilitySubmitJob,
NamespaceCapabilityDispatchJob,
NamespaceCapabilityReadLogs,
NamespaceCapabilityReadFS,
NamespaceCapabilityAllocExec,
NamespaceCapabilityAllocLifecycle,
NamespaceCapabilityCSIMountVolume,
NamespaceCapabilityCSIWriteVolume,
NamespaceCapabilitySubmitRecommendation,
},
},
{
Name: "secret",
Capabilities: []string{
NamespaceCapabilityDeny,
NamespaceCapabilityReadLogs,
},
},
{
Name: "apps",
Variables: &VariablesPolicy{
Paths: []*VariablesPathPolicy{
{
PathSpec: "jobs/write-does-not-imply-read-or-delete",
Capabilities: []string{VariablesCapabilityWrite},
},
{
PathSpec: "project/read-implies-list",
Capabilities: []string{
VariablesCapabilityRead,
VariablesCapabilityList,
},
},
{
PathSpec: "project/explicit",
Capabilities: []string{
VariablesCapabilityRead,
VariablesCapabilityList,
VariablesCapabilityDestroy,
},
},
},
},
},
{
Name: "autoscaler",
Policy: PolicyScale,
Capabilities: []string{
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityScaleJob,
},
},
},
HostVolumes: []*HostVolumePolicy{
{
Name: "production-tls-*",
Capabilities: []string{"mount-readonly"},
},
{
Name: "staging-tls-*",
Policy: "write",
Capabilities: []string{
"mount-readonly",
"mount-readwrite",
},
},
},
NodePools: []*NodePoolPolicy{
{
Name: "prod",
Capabilities: []string{"read"},
},
{
Name: "dev",
Policy: "write",
Capabilities: []string{"delete", "read", "write"},
},
},
Agent: &AgentPolicy{
Policy: PolicyRead,
},
Node: &NodePolicy{
Policy: PolicyWrite,
},
Operator: &OperatorPolicy{
Policy: PolicyDeny,
},
Quota: &QuotaPolicy{
Policy: PolicyRead,
},
Plugin: &PluginPolicy{
Policy: PolicyRead,
},
},
},
{
`
{
"namespace": [
{
"default": {
"policy": "read"
},
},
{
"other": {
"policy": "write"
},
},
{
"secret": {
"capabilities": [
"deny",
"read-logs"
]
}
},
{
"apps": {
"variables": [
{
"path": [
{
"jobs/write-does-not-imply-read-or-delete": {
"capabilities": ["write"],
},
},
{
"project/read-implies-list": {
"capabilities": ["read"],
},
},
{
"project/explicit": {
"capabilities": ["read", "list", "destroy"],
},
},
],
},
],
},
},
{
"autoscaler": {
"policy": "scale"
},
},
],
"host_volume": [
{
"production-tls-*": {
"capabilities": ["mount-readonly"]
}
},
{
"staging-tls-*": {
"policy": "write"
}
}
],
"node_pool": [
{
"prod": {
"capabilities": ["read"]
}
},
{
"dev": {
"policy": "write"
}
}
],
"agent": {
"policy": "read"
},
"node": {
"policy": "write"
},
"operator": {
"policy": "deny"
},
"quota": {
"policy": "read"
},
"plugin": {
"policy": "read"
}
}`,
"",
&Policy{
Namespaces: []*NamespacePolicy{
{
Name: "default",
Policy: PolicyRead,
Capabilities: []string{
NamespaceCapabilityListJobs,
NamespaceCapabilityParseJob,
NamespaceCapabilityReadJob,
NamespaceCapabilityCSIListVolume,
NamespaceCapabilityCSIReadVolume,
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
},
},
{
Name: "other",
Policy: PolicyWrite,
Capabilities: []string{
NamespaceCapabilityListJobs,
NamespaceCapabilityParseJob,
NamespaceCapabilityReadJob,
NamespaceCapabilityCSIListVolume,
NamespaceCapabilityCSIReadVolume,
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
NamespaceCapabilityScaleJob,
NamespaceCapabilitySubmitJob,
NamespaceCapabilityDispatchJob,
NamespaceCapabilityReadLogs,
NamespaceCapabilityReadFS,
NamespaceCapabilityAllocExec,
NamespaceCapabilityAllocLifecycle,
NamespaceCapabilityCSIMountVolume,
NamespaceCapabilityCSIWriteVolume,
NamespaceCapabilitySubmitRecommendation,
},
},
{
Name: "secret",
Capabilities: []string{
NamespaceCapabilityDeny,
NamespaceCapabilityReadLogs,
},
},
{
Name: "apps",
Variables: &VariablesPolicy{
Paths: []*VariablesPathPolicy{
{
PathSpec: "jobs/write-does-not-imply-read-or-delete",
Capabilities: []string{VariablesCapabilityWrite},
},
{
PathSpec: "project/read-implies-list",
Capabilities: []string{
VariablesCapabilityRead,
VariablesCapabilityList,
},
},
{
PathSpec: "project/explicit",
Capabilities: []string{
VariablesCapabilityRead,
VariablesCapabilityList,
VariablesCapabilityDestroy,
},
},
},
},
},
{
Name: "autoscaler",
Policy: PolicyScale,
Capabilities: []string{
NamespaceCapabilityListScalingPolicies,
NamespaceCapabilityReadScalingPolicy,
NamespaceCapabilityReadJobScaling,
NamespaceCapabilityScaleJob,
},
},
},
HostVolumes: []*HostVolumePolicy{
{
Name: "production-tls-*",
Capabilities: []string{"mount-readonly"},
},
{
Name: "staging-tls-*",
Policy: "write",
Capabilities: []string{
"mount-readonly",
"mount-readwrite",
},
},
},
NodePools: []*NodePoolPolicy{
{
Name: "prod",
Capabilities: []string{"read"},
},
{
Name: "dev",
Policy: "write",
Capabilities: []string{"delete", "read", "write"},
},
},
Agent: &AgentPolicy{
Policy: PolicyRead,
},
Node: &NodePolicy{
Policy: PolicyWrite,
},
Operator: &OperatorPolicy{
Policy: PolicyDeny,
},
Quota: &QuotaPolicy{
Policy: PolicyRead,
},
Plugin: &PluginPolicy{
Policy: PolicyRead,
},
},
},
{
`
namespace "default" {
policy = "foo"
}
`,
"Invalid namespace policy",
nil,
},
{
`
namespace {
policy = "read"
}
`,
"Invalid namespace name",
nil,
},
{
`
{
"namespace": [
{
"": {
"policy": "read"
}
}
]
}
`,
"Invalid namespace name",
nil,
},
{
`
namespace "dev" {
variables "*" {
capabilities = ["read", "write"]
}
}
`,
"Invalid variable policy: no variable paths in namespace dev",
nil,
},
{
`
namespace "dev" {
policy = "read"
variables {
path {}
path "nomad/jobs/example" {
capabilities = ["read"]
}
}
}
`,
"Invalid missing variable path in namespace",
nil,
},
{
`
{
"namespace": [
{
"dev": {
"policy": "read",
"variables": [
{
"paths": [
{
"": {
"capabilities": ["read"]
}
}
]
]
]
}
}
]
}
`,
"no variable paths in namespace dev",
nil,
},
{
`
namespace "default" {
capabilities = ["deny", "foo"]
}
`,
"Invalid namespace capability",
nil,
},
{
`namespace {}`,
"invalid acl policy",
nil,
},
{
`
agent {
policy = "foo"
}
`,
"Invalid agent policy",
nil,
},
{
`
node {
policy = "foo"
}
`,
"Invalid node policy",
nil,
},
{
`
operator {
policy = "foo"
}
`,
"Invalid operator policy",
nil,
},
{
`
quota {
policy = "foo"
}
`,
"Invalid quota policy",
nil,
},
{
`
{
"Name": "my-policy",
"Description": "This is a great policy",
"Rules": "anything"
}
`,
"Invalid policy",
nil,
},
{
`
namespace "has a space"{
policy = "read"
}
`,
"Invalid namespace name",
nil,
},
{
`
namespace "default" {
capabilities = ["sentinel-override"]
}
`,
"",
&Policy{
Namespaces: []*NamespacePolicy{
{
Name: "default",
Policy: "",
Capabilities: []string{
NamespaceCapabilitySentinelOverride,
},
},
},
},
},
{
`
node_pool "pool-read-only" {
policy = "read"
}
node_pool "pool-read-write" {
policy = "write"
}
node_pool "pool-read-upsert" {
policy = "read"
capabilities = ["write"]
}
node_pool "pool-multiple-capabilities" {
policy = "read"
capabilities = ["write", "delete"]
}
node_pool "pool-deny-policy" {
policy = "deny"
capabilities = ["write"]
}
node_pool "pool-deny-capability" {
capabilities = ["deny", "read"]
}
node_pool "pool-*" {
policy = "read"
}
`,
"",
&Policy{
NodePools: []*NodePoolPolicy{
{
Name: "pool-read-only",
Policy: PolicyRead,
Capabilities: []string{
NodePoolCapabilityRead,
},
},
{
Name: "pool-read-write",
Policy: PolicyWrite,
Capabilities: []string{
NodePoolCapabilityDelete,
NodePoolCapabilityRead,
NodePoolCapabilityWrite,
},
},
{
Name: "pool-read-upsert",
Policy: PolicyRead,
Capabilities: []string{
NodePoolCapabilityWrite,
NodePoolCapabilityRead,
},
},
{
Name: "pool-multiple-capabilities",
Policy: PolicyRead,
Capabilities: []string{
NodePoolCapabilityWrite,
NodePoolCapabilityDelete,
NodePoolCapabilityRead,
},
},
{
Name: "pool-deny-policy",
Policy: PolicyDeny,
Capabilities: []string{
NodePoolCapabilityWrite,
NodePoolCapabilityDeny,
},
},
{
Name: "pool-deny-capability",
Policy: "",
Capabilities: []string{
NodePoolCapabilityDeny,
NodePoolCapabilityRead,
},
},
{
Name: "pool-*",
Policy: PolicyRead,
Capabilities: []string{
NodePoolCapabilityRead,
},
},
},
},
},
{
`
node_pool "" {
}
`,
"Invalid node pool name",
nil,
},
{
`
node_pool "pool%" {
}
`,
"Invalid node pool name",
nil,
},
{
`
node_pool "my-pool" {
capabilities = ["read", "invalid"]
}
`,
"Invalid node pool capability",
nil,
},
{
`
node_pool {
policy = "read"
}
`,
"Invalid node pool name",
nil,
},
{
`
{
"node_pool": [
{
"": {
"policy": "read"
}
}
]
}
`,
"Invalid node pool name",
nil,
},
{
`
host_volume "production-tls-*" {
capabilities = ["mount-readonly"]
}
`,
"",
&Policy{
HostVolumes: []*HostVolumePolicy{
{
Name: "production-tls-*",
Policy: "",
Capabilities: []string{
HostVolumeCapabilityMountReadOnly,
},
},
},
},
},
{
`
host_volume "production-tls-*" {
capabilities = ["mount-readwrite"]
}
`,
"",
&Policy{
HostVolumes: []*HostVolumePolicy{
{
Name: "production-tls-*",
Policy: "",
Capabilities: []string{
HostVolumeCapabilityMountReadWrite,
},
},
},
},
},
{
`
host_volume "volume has a space" {
capabilities = ["mount-readwrite"]
}
`,
"Invalid host volume name",
nil,
},
{
`
host_volume {
policy = "read"
}
`,
"Invalid host volume name",
nil,
},
{
`
{
"host_volume": [
{
"": {
"policy": "read"
}
}
]
}
`,
"Invalid host volume name",
nil,
},
{
`
plugin {
policy = "list"
}
`,
"",
&Policy{
Plugin: &PluginPolicy{
Policy: PolicyList,
},
},
},
{
`
plugin {
policy = "reader"
}
`,
"Invalid plugin policy",
nil,
},
}
for idx, tc := range tcases {
t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
p, err := Parse(tc.Raw)
if err != nil {
if tc.ErrStr == "" {
t.Fatalf("Unexpected err: %v", err)
}
if !strings.Contains(err.Error(), tc.ErrStr) {
t.Fatalf("Unexpected err: %v", err)
}
return
}
if err == nil && tc.ErrStr != "" {
t.Fatalf("Missing expected err")
}
tc.Expect.Raw = tc.Raw
assert.EqualValues(t, tc.Expect, p)
})
}
}
func TestParse_BadInput(t *testing.T) {
ci.Parallel(t)
inputs := []string{
`namespace "\500" {}`,
}
for i, c := range inputs {
t.Run(fmt.Sprintf("%d: %v", i, c), func(t *testing.T) {
_, err := Parse(c)
assert.Error(t, err)
})
}
}