open-nomad/nomad/consul_policy_test.go

653 lines
21 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package nomad
import (
"testing"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/stretchr/testify/require"
)
func TestConsulPolicy_ParseConsulPolicy(t *testing.T) {
ci.Parallel(t)
try := func(t *testing.T, text string, expPolicy *ConsulPolicy, expErr string) {
policy, err := parseConsulPolicy(text)
if expErr != "" {
require.EqualError(t, err, expErr)
} else {
require.NoError(t, err)
require.Equal(t, expPolicy, policy)
}
}
t.Run("service", func(t *testing.T) {
text := `service "web" { policy = "read" }`
exp := &ConsulPolicy{
Services: []*ConsulServiceRule{{Name: "web", Policy: "read"}},
}
try(t, text, exp, "")
})
t.Run("service_prefix", func(t *testing.T) {
text := `service_prefix "data" { policy = "write" }`
exp := &ConsulPolicy{
ServicePrefixes: []*ConsulServiceRule{{Name: "data", Policy: "write"}},
}
try(t, text, exp, "")
})
t.Run("key_prefix", func(t *testing.T) {
text := `key_prefix "keys" { policy = "read" }`
exp := &ConsulPolicy{
KeyPrefixes: []*ConsulKeyRule{{Name: "keys", Policy: "read"}},
}
try(t, text, exp, "")
})
t.Run("malformed", func(t *testing.T) {
text := `this is not valid HCL!`
expErr := "failed to parse ACL policy: At 1:22: illegal char"
try(t, text, nil, expErr)
})
t.Run("multi-namespace", func(t *testing.T) {
text := `
service_prefix "z" { policy = "write" }
namespace_prefix "b" {
service_prefix "b" { policy = "write" }
key_prefix "" { policy = "read" }
}
namespace_prefix "c" {
service_prefix "c" { policy = "read" }
key_prefix "" { policy = "read" }
}
namespace_prefix "" {
key_prefix "shared/" { policy = "read" }
}
namespace "foo" {
service "bar" { policy = "read" }
service_prefix "foo-" { policy = "write" }
key_prefix "" { policy = "read" }
}
`
exp := &ConsulPolicy{
ServicePrefixes: []*ConsulServiceRule{{Name: "z", Policy: "write"}},
NamespacePrefixes: map[string]*ConsulPolicy{
"b": {
ServicePrefixes: []*ConsulServiceRule{{Name: "b", Policy: "write"}},
KeyPrefixes: []*ConsulKeyRule{{Name: "", Policy: "read"}},
},
"c": {
ServicePrefixes: []*ConsulServiceRule{{Name: "c", Policy: "read"}},
KeyPrefixes: []*ConsulKeyRule{{Name: "", Policy: "read"}},
},
"": {
KeyPrefixes: []*ConsulKeyRule{{Name: "shared/", Policy: "read"}},
},
},
Namespaces: map[string]*ConsulPolicy{
"foo": {
Services: []*ConsulServiceRule{{Name: "bar", Policy: "read"}},
ServicePrefixes: []*ConsulServiceRule{{Name: "foo-", Policy: "write"}},
KeyPrefixes: []*ConsulKeyRule{{Name: "", Policy: "read"}},
},
},
}
try(t, text, exp, "")
})
}
func TestConsulACLsAPI_allowsServiceWrite(t *testing.T) {
ci.Parallel(t)
try := func(t *testing.T, matches bool, namespace, task string, cp *ConsulPolicy, exp bool) {
// If matches is false, the implication is that the consul acl token is in
// the default namespace, otherwise prior validation would stop the request
// before getting to policy checks. Only consul acl tokens in the default
// namespace are allowed to have namespace_prefix blocks.
result := cp.allowsServiceWrite(matches, namespace, task)
require.Equal(t, exp, result)
}
// create a consul policy backed by service and/or service_prefix rules
//
// if namespace == "_", use the top level service/service_prefix rules, otherwise
// set the rules as a namespace_prefix ruleset
makeCP := func(namespace string, services [][2]string, prefixes [][2]string) *ConsulPolicy {
serviceRules := make([]*ConsulServiceRule, 0, len(services))
for _, service := range services {
serviceRules = append(serviceRules, &ConsulServiceRule{Name: service[0], Policy: service[1]})
}
prefixRules := make([]*ConsulServiceRule, 0, len(prefixes))
for _, prefix := range prefixes {
prefixRules = append(prefixRules, &ConsulServiceRule{Name: prefix[0], Policy: prefix[1]})
}
if namespace == "_" {
return &ConsulPolicy{Services: serviceRules, ServicePrefixes: prefixRules}
}
return &ConsulPolicy{
Namespaces: map[string]*ConsulPolicy{
namespace: {
Services: serviceRules,
ServicePrefixes: prefixRules,
},
},
NamespacePrefixes: map[string]*ConsulPolicy{
namespace: {
Services: serviceRules,
ServicePrefixes: prefixRules,
},
}}
}
t.Run("matching service policy write", func(t *testing.T) {
rule := [][2]string{{"task1", "write"}}
const task = "task1"
t.Run("namespaces match", func(t *testing.T) {
const matches = true
try(t, matches, "default", task, makeCP("_", rule, nil), true)
try(t, matches, "default", task, makeCP("default", rule, nil), true)
try(t, matches, "apple", task, makeCP("_", rule, nil), true)
try(t, matches, "apple", task, makeCP("apple", rule, nil), true)
try(t, matches, "apple", task, makeCP("app", rule, nil), true)
try(t, matches, "other", task, makeCP("", rule, nil), true)
try(t, matches, "other", task, makeCP("apple", rule, nil), false)
})
t.Run("namespaces do not match", func(t *testing.T) {
const matches = false
try(t, matches, "apple", task, makeCP("_", rule, nil), false)
try(t, matches, "apple", task, makeCP("apple", rule, nil), true)
try(t, matches, "apple", task, makeCP("app", rule, nil), true)
try(t, matches, "other", task, makeCP("", rule, nil), true)
try(t, matches, "other", task, makeCP("apple", rule, nil), false)
})
})
t.Run("matching service policy read", func(t *testing.T) {
rule := [][2]string{{"task1", "read"}}
const task = "task1"
t.Run("namespaces match", func(t *testing.T) {
const matches = true
try(t, matches, "default", task, makeCP("_", rule, nil), false)
try(t, matches, "default", task, makeCP("default", rule, nil), false)
try(t, matches, "apple", task, makeCP("_", rule, nil), false)
try(t, matches, "apple", task, makeCP("apple", rule, nil), false)
try(t, matches, "apple", task, makeCP("app", rule, nil), false)
try(t, matches, "other", task, makeCP("", rule, nil), false)
try(t, matches, "other", task, makeCP("apple", rule, nil), false)
})
t.Run("namespaces do not match", func(t *testing.T) {
const matches = false
try(t, matches, "apple", task, makeCP("_", rule, nil), false)
try(t, matches, "apple", task, makeCP("apple", rule, nil), false)
try(t, matches, "apple", task, makeCP("app", rule, nil), false)
try(t, matches, "other", task, makeCP("", rule, nil), false)
try(t, matches, "other", task, makeCP("apple", rule, nil), false)
})
})
t.Run("wildcard service policy write", func(t *testing.T) {
rule := [][2]string{{"*", "write"}}
const task = "task1"
t.Run("namespaces match", func(t *testing.T) {
const matches = true
try(t, matches, "default", task, makeCP("_", rule, nil), true)
try(t, matches, "default", task, makeCP("default", rule, nil), true)
try(t, matches, "apple", task, makeCP("_", rule, nil), true)
try(t, matches, "apple", task, makeCP("app", rule, nil), true)
try(t, matches, "other", task, makeCP("", rule, nil), true)
try(t, matches, "other", task, makeCP("apple", rule, nil), false)
})
t.Run("namespaces do not match", func(t *testing.T) {
const matches = false
try(t, matches, "apple", task, makeCP("_", rule, nil), false)
try(t, matches, "apple", task, makeCP("app", rule, nil), true)
try(t, matches, "other", task, makeCP("", rule, nil), true)
try(t, matches, "other", task, makeCP("apple", rule, nil), false)
})
})
t.Run("wrong service policy write", func(t *testing.T) {
rule := [][2]string{{"task1", "write"}}
const task = "other1"
t.Run("namespaces match", func(t *testing.T) {
const matches = true
try(t, matches, "default", task, makeCP("_", rule, nil), false)
try(t, matches, "default", task, makeCP("default", rule, nil), false)
try(t, matches, "apple", task, makeCP("_", rule, nil), false)
try(t, matches, "apple", task, makeCP("app", rule, nil), false)
try(t, matches, "other", task, makeCP("", rule, nil), false)
try(t, matches, "other", task, makeCP("apple", rule, nil), false)
})
t.Run("namespaces do not match", func(t *testing.T) {
const matches = true
try(t, matches, "apple", task, makeCP("_", rule, nil), false)
try(t, matches, "apple", task, makeCP("app", rule, nil), false)
try(t, matches, "other", task, makeCP("", rule, nil), false)
try(t, matches, "other", task, makeCP("apple", rule, nil), false)
})
})
t.Run("matching prefix policy write", func(t *testing.T) {
rule := [][2]string{{"task-", "write"}}
const task = "task-one"
t.Run("namespaces match", func(t *testing.T) {
const matches = true
try(t, matches, "default", task, makeCP("_", nil, rule), true)
try(t, matches, "default", task, makeCP("default", nil, rule), true)
try(t, matches, "apple", task, makeCP("_", nil, rule), true)
try(t, matches, "apple", task, makeCP("app", nil, rule), true)
try(t, matches, "other", task, makeCP("", nil, rule), true)
try(t, matches, "other", task, makeCP("apple", nil, rule), false)
})
t.Run("namespaces do not match", func(t *testing.T) {
const matches = false
try(t, matches, "apple", task, makeCP("_", nil, rule), false)
try(t, matches, "apple", task, makeCP("app", nil, rule), true)
try(t, matches, "other", task, makeCP("", nil, rule), true)
try(t, matches, "other", task, makeCP("apple", nil, rule), false)
})
})
t.Run("matching prefix policy read", func(t *testing.T) {
rule := [][2]string{{"task-", "read"}}
const task = "task-one"
t.Run("namespaces match", func(t *testing.T) {
const matches = true
try(t, matches, "default", task, makeCP("_", nil, rule), false)
try(t, matches, "default", task, makeCP("default", nil, rule), false)
try(t, matches, "apple", task, makeCP("_", nil, rule), false)
try(t, matches, "apple", task, makeCP("app", nil, rule), false)
try(t, matches, "other", task, makeCP("", nil, rule), false)
try(t, matches, "other", task, makeCP("apple", nil, rule), false)
})
t.Run("namespaces do not match", func(t *testing.T) {
const matches = false
try(t, matches, "apple", task, makeCP("_", nil, rule), false)
try(t, matches, "apple", task, makeCP("app", nil, rule), false)
try(t, matches, "other", task, makeCP("", nil, rule), false)
try(t, matches, "other", task, makeCP("apple", nil, rule), false)
})
})
t.Run("empty prefix policy write", func(t *testing.T) {
rule := [][2]string{{"", "write"}}
const task = "task-one"
t.Run("namespaces match", func(t *testing.T) {
const matches = true
try(t, matches, "default", task, makeCP("_", nil, rule), true)
try(t, matches, "default", task, makeCP("default", nil, rule), true)
try(t, matches, "apple", task, makeCP("_", nil, rule), true)
try(t, matches, "apple", task, makeCP("app", nil, rule), true)
try(t, matches, "other", task, makeCP("", nil, rule), true)
try(t, matches, "other", task, makeCP("apple", nil, rule), false)
})
t.Run("namespaces do not match", func(t *testing.T) {
const matches = false
try(t, matches, "apple", task, makeCP("_", nil, rule), false)
try(t, matches, "apple", task, makeCP("app", nil, rule), true)
try(t, matches, "other", task, makeCP("", nil, rule), true)
try(t, matches, "other", task, makeCP("apple", nil, rule), false)
})
})
t.Run("late matching service", func(t *testing.T) {
rule := [][2]string{{"task0", "write"}, {"task1", "write"}}
const task = "task1"
t.Run("namespaces match", func(t *testing.T) {
const matches = true
try(t, matches, "default", task, makeCP("_", rule, nil), true)
try(t, matches, "default", task, makeCP("default", rule, nil), true)
try(t, matches, "apple", task, makeCP("_", rule, nil), true)
try(t, matches, "apple", task, makeCP("app", rule, nil), true)
try(t, matches, "other", task, makeCP("", rule, nil), true)
try(t, matches, "other", task, makeCP("apple", rule, nil), false)
})
t.Run("namespaces do not match", func(t *testing.T) {
const matches = false
try(t, matches, "apple", task, makeCP("_", rule, nil), false)
try(t, matches, "apple", task, makeCP("app", rule, nil), true)
try(t, matches, "other", task, makeCP("", rule, nil), true)
try(t, matches, "other", task, makeCP("apple", rule, nil), false)
})
})
t.Run("late matching prefix", func(t *testing.T) {
rule := [][2]string{{"foo-", "write"}, {"task-", "write"}}
const task = "task-one"
t.Run("namespaces match", func(t *testing.T) {
const matches = true
try(t, matches, "default", task, makeCP("_", nil, rule), true)
try(t, matches, "default", task, makeCP("default", nil, rule), true)
try(t, matches, "apple", task, makeCP("_", nil, rule), true)
try(t, matches, "apple", task, makeCP("app", nil, rule), true)
try(t, matches, "other", task, makeCP("", nil, rule), true)
try(t, matches, "other", task, makeCP("apple", nil, rule), false)
})
t.Run("namespaces do not match", func(t *testing.T) {
const matches = false
try(t, matches, "apple", task, makeCP("_", nil, rule), false)
try(t, matches, "apple", task, makeCP("app", nil, rule), true)
try(t, matches, "other", task, makeCP("", nil, rule), true)
try(t, matches, "other", task, makeCP("apple", nil, rule), false)
})
})
}
func TestConsulPolicy_isManagementToken(t *testing.T) {
ci.Parallel(t)
aclsAPI := new(consulACLsAPI)
t.Run("nil", func(t *testing.T) {
token := (*api.ACLToken)(nil)
result := aclsAPI.isManagementToken(token)
require.False(t, result)
})
t.Run("no policies", func(t *testing.T) {
token := &api.ACLToken{
Policies: []*api.ACLTokenPolicyLink{},
}
result := aclsAPI.isManagementToken(token)
require.False(t, result)
})
t.Run("management policy", func(t *testing.T) {
token := &api.ACLToken{
Policies: []*api.ACLTokenPolicyLink{{
ID: consulGlobalManagementPolicyID,
}},
}
result := aclsAPI.isManagementToken(token)
require.True(t, result)
})
t.Run("other policy", func(t *testing.T) {
token := &api.ACLToken{
Policies: []*api.ACLTokenPolicyLink{{
ID: uuid.Generate(),
}},
}
result := aclsAPI.isManagementToken(token)
require.False(t, result)
})
t.Run("mixed policies", func(t *testing.T) {
token := &api.ACLToken{
Policies: []*api.ACLTokenPolicyLink{{
ID: uuid.Generate(),
}, {
ID: consulGlobalManagementPolicyID,
}, {
ID: uuid.Generate(),
}},
}
result := aclsAPI.isManagementToken(token)
require.True(t, result)
})
}
func TestConsulPolicy_namespaceCheck(t *testing.T) {
ci.Parallel(t)
withoutNS := &api.ACLToken{Namespace: ""}
withDefault := &api.ACLToken{Namespace: "default"}
withOther := &api.ACLToken{Namespace: "other"}
// ACLs not enabled
t.Run("acl:disable ns:unset", func(t *testing.T) {
err := namespaceCheck("", withoutNS)
require.NoError(t, err)
})
t.Run("acl:disable ns:default", func(t *testing.T) {
err := namespaceCheck("default", withoutNS)
require.EqualError(t, err, `consul ACL token cannot use namespace "default"`)
})
t.Run("acl:disable ns:other", func(t *testing.T) {
err := namespaceCheck("other", withoutNS)
require.EqualError(t, err, `consul ACL token cannot use namespace "other"`)
})
// ACLs with "default" token
t.Run("acl:enable token:default ns:unset", func(t *testing.T) {
// the bypass case where a legacy job (with no namespace set) should work
// with the a token in the "default" consul namespace
err := namespaceCheck("", withDefault)
require.NoError(t, err)
})
t.Run("acl:enable token:default ns:default", func(t *testing.T) {
err := namespaceCheck("default", withDefault)
require.NoError(t, err)
})
t.Run("acl:enable token:default ns:other", func(t *testing.T) {
// the bypass case where a default token could have namespace_prefix
// blocks
err := namespaceCheck("other", withDefault)
require.NoError(t, err)
})
// ACLs with non-"default" token
t.Run("acl:enable token:other ns:unset", func(t *testing.T) {
err := namespaceCheck("", withOther)
require.EqualError(t, err, `consul ACL token requires using namespace "other"`)
})
t.Run("acl:enable token:other ns:default", func(t *testing.T) {
err := namespaceCheck("default", withOther)
require.EqualError(t, err, `consul ACL token cannot use namespace "default"`)
})
t.Run("acl:enable token:other ns:other", func(t *testing.T) {
err := namespaceCheck("other", withOther)
require.NoError(t, err)
})
}
func TestConsulPolicy_allowKeystoreRead(t *testing.T) {
ci.Parallel(t)
t.Run("empty", func(t *testing.T) {
require.False(t, new(ConsulPolicy).allowsKeystoreRead(true, "default"))
})
t.Run("services only", func(t *testing.T) {
policy := &ConsulPolicy{
Services: []*ConsulServiceRule{{
Name: "service1",
Policy: "write",
}},
}
require.False(t, policy.allowsKeystoreRead(true, "default"))
require.False(t, policy.allowsKeystoreRead(false, "apple"))
})
// using top-level key_prefix block
t.Run("kv any read", func(t *testing.T) {
policy := &ConsulPolicy{
KeyPrefixes: []*ConsulKeyRule{{
Name: "",
Policy: "read",
}},
}
require.True(t, policy.allowsKeystoreRead(true, "default"))
require.False(t, policy.allowsKeystoreRead(false, "apple"))
})
t.Run("kv any write", func(t *testing.T) {
policy := &ConsulPolicy{
KeyPrefixes: []*ConsulKeyRule{{
Name: "",
Policy: "write",
}},
}
require.True(t, policy.allowsKeystoreRead(true, "default"))
require.False(t, policy.allowsKeystoreRead(false, "apple"))
})
t.Run("kv limited read", func(t *testing.T) {
policy := &ConsulPolicy{
KeyPrefixes: []*ConsulKeyRule{{
Name: "foo/bar",
Policy: "read",
}},
}
require.False(t, policy.allowsKeystoreRead(true, "default"))
require.False(t, policy.allowsKeystoreRead(false, "apple"))
})
t.Run("kv limited write", func(t *testing.T) {
policy := &ConsulPolicy{
KeyPrefixes: []*ConsulKeyRule{{
Name: "foo/bar",
Policy: "write",
}},
}
require.False(t, policy.allowsKeystoreRead(true, "default"))
require.False(t, policy.allowsKeystoreRead(false, "apple"))
})
// using namespace_prefix block
t.Run("kv wild namespace prefix any read", func(t *testing.T) {
policy := &ConsulPolicy{
NamespacePrefixes: map[string]*ConsulPolicy{
"": {
KeyPrefixes: []*ConsulKeyRule{{
Name: "",
Policy: "read",
}},
},
},
}
require.True(t, policy.allowsKeystoreRead(true, "default"))
require.True(t, policy.allowsKeystoreRead(false, "apple"))
})
t.Run("kv apple namespace prefix any read", func(t *testing.T) {
policy := &ConsulPolicy{
NamespacePrefixes: map[string]*ConsulPolicy{
"apple": {
KeyPrefixes: []*ConsulKeyRule{{
Name: "",
Policy: "read",
}},
},
},
}
require.False(t, policy.allowsKeystoreRead(true, "default"))
require.True(t, policy.allowsKeystoreRead(false, "apple"))
})
t.Run("kv matching namespace prefix any read", func(t *testing.T) {
policy := &ConsulPolicy{
NamespacePrefixes: map[string]*ConsulPolicy{
"app": {
KeyPrefixes: []*ConsulKeyRule{{
Name: "",
Policy: "read",
}},
},
},
}
require.False(t, policy.allowsKeystoreRead(true, "default"))
require.True(t, policy.allowsKeystoreRead(false, "apple"))
})
t.Run("kv other namespace prefix any read", func(t *testing.T) {
policy := &ConsulPolicy{
NamespacePrefixes: map[string]*ConsulPolicy{
"other": {
KeyPrefixes: []*ConsulKeyRule{{
Name: "",
Policy: "read",
}},
},
},
}
require.False(t, policy.allowsKeystoreRead(true, "default"))
require.False(t, policy.allowsKeystoreRead(false, "apple"))
})
// using namespace block
t.Run("kv match namespace any read", func(t *testing.T) {
policy := &ConsulPolicy{
Namespaces: map[string]*ConsulPolicy{
"apple": {
KeyPrefixes: []*ConsulKeyRule{{
Name: "",
Policy: "read",
}},
},
},
}
require.False(t, policy.allowsKeystoreRead(true, "default"))
require.True(t, policy.allowsKeystoreRead(true, "apple"))
})
t.Run("kv mismatch namespace any read", func(t *testing.T) {
policy := &ConsulPolicy{
Namespaces: map[string]*ConsulPolicy{
"other": {
KeyPrefixes: []*ConsulKeyRule{{
Name: "",
Policy: "read",
}},
},
},
}
require.False(t, policy.allowsKeystoreRead(true, "default"))
require.False(t, policy.allowsKeystoreRead(true, "apple"))
})
t.Run("kv matching namespace prefix any read", func(t *testing.T) {
policy := &ConsulPolicy{
Namespaces: map[string]*ConsulPolicy{
"apple": {
KeyPrefixes: []*ConsulKeyRule{{
Name: "",
Policy: "read",
}},
},
},
}
require.False(t, policy.allowsKeystoreRead(false, "default"))
require.True(t, policy.allowsKeystoreRead(false, "apple"))
})
t.Run("kv mismatch namespace prefix any read", func(t *testing.T) {
policy := &ConsulPolicy{
Namespaces: map[string]*ConsulPolicy{
"other": {
KeyPrefixes: []*ConsulKeyRule{{
Name: "",
Policy: "read",
}},
},
},
}
require.False(t, policy.allowsKeystoreRead(true, "default"))
require.False(t, policy.allowsKeystoreRead(true, "apple"))
})
}