// 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")) }) }