Files
vault/vault/acl_test.go
hashicorp-copywrite[bot] 0b12cdcfd1 [COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

1335 lines
34 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package vault
import (
"context"
"fmt"
"reflect"
"sync"
"testing"
"time"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/logical"
)
func TestACL_NewACL(t *testing.T) {
t.Run("root-ns", func(t *testing.T) {
t.Parallel()
testNewACL(t, namespace.RootNamespace)
})
}
func testNewACL(t *testing.T, ns *namespace.Namespace) {
ctx := namespace.ContextWithNamespace(context.Background(), ns)
policy := []*Policy{{Name: "root"}}
_, err := NewACL(ctx, policy)
switch ns.ID {
case namespace.RootNamespaceID:
if err != nil {
t.Fatal(err)
}
default:
if err == nil {
t.Fatal("expected an error")
}
}
}
func TestACL_MFAMethods(t *testing.T) {
t.Run("root-ns", func(t *testing.T) {
t.Parallel()
testACLMFAMethods(t, namespace.RootNamespace)
})
}
func testACLMFAMethods(t *testing.T, ns *namespace.Namespace) {
mfaRules := `
path "secret/foo/*" {
mfa_methods = ["mfa_method_1", "mfa_method_2", "mfa_method_3"]
}
path "secret/exact/path" {
mfa_methods = ["mfa_method_4", "mfa_method_5"]
}
path "secret/split/definition" {
mfa_methods = ["mfa_method_6", "mfa_method_7"]
}
path "secret/split/definition" {
mfa_methods = ["mfa_method_7", "mfa_method_8", "mfa_method_9"]
}
`
policy, err := ParseACLPolicy(ns, mfaRules)
if err != nil {
t.Fatal(err)
}
ctx := namespace.ContextWithNamespace(context.Background(), ns)
acl, err := NewACL(ctx, []*Policy{policy})
if err != nil {
t.Fatal(err)
}
request := &logical.Request{
Operation: logical.UpdateOperation,
Path: "secret/foo/testing/glob/pattern",
}
actual := acl.AllowOperation(ctx, request, false).MFAMethods
expected := []string{"mfa_method_1", "mfa_method_2", "mfa_method_3"}
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("bad: MFA methods; expected: %#v\n actual: %#v\n", expected, actual)
}
request.Path = "secret/exact/path"
actual = acl.AllowOperation(ctx, request, false).MFAMethods
expected = []string{"mfa_method_4", "mfa_method_5"}
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("bad: MFA methods; expected: %#v\n actual: %#v\n", expected, actual)
}
request.Path = "secret/split/definition"
actual = acl.AllowOperation(ctx, request, false).MFAMethods
expected = []string{"mfa_method_6", "mfa_method_7", "mfa_method_8", "mfa_method_9"}
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("bad: MFA methods; expected: %#v\n actual: %#v\n", expected, actual)
}
}
func TestACL_Capabilities(t *testing.T) {
t.Run("root-ns", func(t *testing.T) {
t.Parallel()
policy := []*Policy{{Name: "root"}}
ctx := namespace.RootContext(context.Background())
acl, err := NewACL(ctx, policy)
if err != nil {
t.Fatalf("err: %v", err)
}
actual := acl.Capabilities(ctx, "any/path")
expected := []string{"root"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
testACLCapabilities(t, namespace.RootNamespace)
})
}
func testACLCapabilities(t *testing.T, ns *namespace.Namespace) {
// Create the root policy ACL
ctx := namespace.ContextWithNamespace(context.Background(), ns)
policy, err := ParseACLPolicy(ns, aclPolicy)
if err != nil {
t.Fatalf("err: %v", err)
}
acl, err := NewACL(ctx, []*Policy{policy})
if err != nil {
t.Fatalf("err: %v", err)
}
actual := acl.Capabilities(ctx, "dev")
expected := []string{"deny"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: path: %s\ngot\n%#v\nexpected\n%#v\n", "deny", actual, expected)
}
actual = acl.Capabilities(ctx, "dev/")
expected = []string{"sudo", "read", "list", "update", "delete", "create"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: path: %s\ngot\n%#v\nexpected\n%#v\n", "dev/", actual, expected)
}
actual = acl.Capabilities(ctx, "stage/aws/test")
expected = []string{"sudo", "read", "list", "update"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: path: %s\ngot\n%#v\nexpected\n%#v\n", "stage/aws/test", actual, expected)
}
}
func TestACL_Root(t *testing.T) {
t.Run("root-ns", func(t *testing.T) {
t.Parallel()
testACLRoot(t, namespace.RootNamespace)
})
}
func testACLRoot(t *testing.T, ns *namespace.Namespace) {
// Create the root policy ACL. Always create on root namespace regardless of
// which namespace to ACL check on.
policy := []*Policy{{Name: "root"}}
acl, err := NewACL(namespace.RootContext(context.Background()), policy)
if err != nil {
t.Fatalf("err: %v", err)
}
request := new(logical.Request)
request.Operation = logical.UpdateOperation
request.Path = "sys/mount/foo"
ctx := namespace.ContextWithNamespace(context.Background(), ns)
authResults := acl.AllowOperation(ctx, request, false)
if !authResults.RootPrivs {
t.Fatalf("expected root")
}
if !authResults.Allowed {
t.Fatalf("expected permissions")
}
}
func TestACL_Single(t *testing.T) {
t.Run("root-ns", func(t *testing.T) {
t.Parallel()
testACLSingle(t, namespace.RootNamespace)
})
}
func testACLSingle(t *testing.T, ns *namespace.Namespace) {
policy, err := ParseACLPolicy(ns, aclPolicy)
if err != nil {
t.Fatalf("err: %v", err)
}
ctx := namespace.ContextWithNamespace(context.Background(), ns)
acl, err := NewACL(ctx, []*Policy{policy})
if err != nil {
t.Fatalf("err: %v", err)
}
// Type of operation is not important here as we only care about checking
// sudo/root
ctx = namespace.ContextWithNamespace(context.Background(), ns)
request := new(logical.Request)
request.Operation = logical.ReadOperation
request.Path = "sys/mount/foo"
authResults := acl.AllowOperation(ctx, request, false)
if authResults.RootPrivs {
t.Fatalf("unexpected root")
}
type tcase struct {
op logical.Operation
path string
allowed bool
rootPrivs bool
}
tcases := []tcase{
{logical.ReadOperation, "root", false, false},
{logical.HelpOperation, "root", true, false},
{logical.ReadOperation, "dev/foo", true, true},
{logical.UpdateOperation, "dev/foo", true, true},
{logical.DeleteOperation, "stage/foo", true, false},
{logical.ListOperation, "stage/aws/foo", true, true},
{logical.UpdateOperation, "stage/aws/foo", true, true},
{logical.UpdateOperation, "stage/aws/policy/foo", true, true},
{logical.DeleteOperation, "prod/foo", false, false},
{logical.UpdateOperation, "prod/foo", false, false},
{logical.ReadOperation, "prod/foo", true, false},
{logical.ListOperation, "prod/foo", true, false},
{logical.ReadOperation, "prod/aws/foo", false, false},
{logical.ReadOperation, "foo/bar", true, true},
{logical.ListOperation, "foo/bar", false, true},
{logical.UpdateOperation, "foo/bar", false, true},
{logical.CreateOperation, "foo/bar", true, true},
{logical.ReadOperation, "baz/quux", true, false},
{logical.CreateOperation, "baz/quux", true, false},
{logical.PatchOperation, "baz/quux", true, false},
{logical.ListOperation, "baz/quux", false, false},
{logical.UpdateOperation, "baz/quux", false, false},
// Path segment wildcards
{logical.ReadOperation, "test/foo/bar/segment", false, false},
{logical.ReadOperation, "test/foo/segment", true, false},
{logical.ReadOperation, "test/bar/segment", true, false},
{logical.ReadOperation, "test/segment/at/frond", false, false},
{logical.ReadOperation, "test/segment/at/front", true, false},
{logical.ReadOperation, "test/segment/at/end/foo", true, false},
{logical.ReadOperation, "test/segment/at/end/foo/", false, false},
{logical.ReadOperation, "test/segment/at/end/v2/foo/", true, false},
{logical.ReadOperation, "test/segment/wildcard/at/foo/", true, false},
{logical.ReadOperation, "test/segment/wildcard/at/end", true, false},
{logical.ReadOperation, "test/segment/wildcard/at/end/", true, false},
// Path segment wildcards vs glob
{logical.ReadOperation, "1/2/3/4", false, false},
{logical.ReadOperation, "1/2/3", true, false},
{logical.UpdateOperation, "1/2/3", false, false},
{logical.UpdateOperation, "1/2/3/4", true, false},
{logical.CreateOperation, "1/2/3/4/5", true, false},
}
for _, tc := range tcases {
ctx := namespace.ContextWithNamespace(context.Background(), ns)
request := new(logical.Request)
request.Operation = tc.op
request.Path = tc.path
authResults := acl.AllowOperation(ctx, request, false)
if authResults.Allowed != tc.allowed {
t.Fatalf("bad: case %#v: %v, %v", tc, authResults.Allowed, authResults.RootPrivs)
}
if authResults.RootPrivs != tc.rootPrivs {
t.Fatalf("bad: case %#v: %v, %v", tc, authResults.Allowed, authResults.RootPrivs)
}
}
}
func TestACL_Layered(t *testing.T) {
t.Run("root-ns", func(t *testing.T) {
t.Parallel()
policy1, err := ParseACLPolicy(namespace.RootNamespace, aclPolicy)
if err != nil {
t.Fatalf("err: %v", err)
}
policy2, err := ParseACLPolicy(namespace.RootNamespace, aclPolicy2)
if err != nil {
t.Fatalf("err: %v", err)
}
acl, err := NewACL(namespace.RootContext(context.Background()), []*Policy{policy1, policy2})
if err != nil {
t.Fatalf("err: %v", err)
}
testLayeredACL(t, acl, namespace.RootNamespace)
})
}
func testLayeredACL(t *testing.T, acl *ACL, ns *namespace.Namespace) {
// Type of operation is not important here as we only care about checking
// sudo/root
ctx := namespace.ContextWithNamespace(context.Background(), ns)
request := new(logical.Request)
request.Operation = logical.ReadOperation
request.Path = "sys/mount/foo"
authResults := acl.AllowOperation(ctx, request, false)
if authResults.RootPrivs {
t.Fatalf("unexpected root")
}
type tcase struct {
op logical.Operation
path string
allowed bool
rootPrivs bool
}
tcases := []tcase{
{logical.ReadOperation, "root", false, false},
{logical.HelpOperation, "root", true, false},
{logical.ReadOperation, "dev/foo", true, true},
{logical.UpdateOperation, "dev/foo", true, true},
{logical.ReadOperation, "dev/hide/foo", false, false},
{logical.UpdateOperation, "dev/hide/foo", false, false},
{logical.DeleteOperation, "stage/foo", true, false},
{logical.ListOperation, "stage/aws/foo", true, true},
{logical.UpdateOperation, "stage/aws/foo", true, true},
{logical.UpdateOperation, "stage/aws/policy/foo", false, false},
{logical.DeleteOperation, "prod/foo", true, false},
{logical.UpdateOperation, "prod/foo", true, false},
{logical.ReadOperation, "prod/foo", true, false},
{logical.ListOperation, "prod/foo", true, false},
{logical.ReadOperation, "prod/aws/foo", false, false},
{logical.ReadOperation, "sys/status", false, false},
{logical.UpdateOperation, "sys/seal", true, true},
{logical.ReadOperation, "foo/bar", false, false},
{logical.ListOperation, "foo/bar", false, false},
{logical.UpdateOperation, "foo/bar", false, false},
{logical.CreateOperation, "foo/bar", false, false},
{logical.ReadOperation, "baz/quux", false, false},
{logical.ListOperation, "baz/quux", false, false},
{logical.UpdateOperation, "baz/quux", false, false},
{logical.CreateOperation, "baz/quux", false, false},
{logical.PatchOperation, "baz/quux", false, false},
}
for _, tc := range tcases {
ctx := namespace.ContextWithNamespace(context.Background(), ns)
request := new(logical.Request)
request.Operation = tc.op
request.Path = tc.path
authResults := acl.AllowOperation(ctx, request, false)
if authResults.Allowed != tc.allowed {
t.Fatalf("bad: case %#v: %v, %v", tc, authResults.Allowed, authResults.RootPrivs)
}
if authResults.RootPrivs != tc.rootPrivs {
t.Fatalf("bad: case %#v: %v, %v", tc, authResults.Allowed, authResults.RootPrivs)
}
}
}
func TestACL_ParseMalformedPolicy(t *testing.T) {
_, err := ParseACLPolicy(namespace.RootNamespace, `name{}`)
if err == nil {
t.Fatalf("expected error")
}
}
func TestACL_PolicyMerge(t *testing.T) {
t.Run("root-ns", func(t *testing.T) {
t.Parallel()
testACLPolicyMerge(t, namespace.RootNamespace)
})
}
func testACLPolicyMerge(t *testing.T, ns *namespace.Namespace) {
policy, err := ParseACLPolicy(ns, mergingPolicies)
if err != nil {
t.Fatalf("err: %v", err)
}
ctx := namespace.ContextWithNamespace(context.Background(), ns)
acl, err := NewACL(ctx, []*Policy{policy})
if err != nil {
t.Fatalf("err: %v", err)
}
type tcase struct {
path string
minWrappingTTL *time.Duration
maxWrappingTTL *time.Duration
allowed map[string][]interface{}
denied map[string][]interface{}
required []string
}
createDuration := func(seconds int) *time.Duration {
ret := time.Duration(seconds) * time.Second
return &ret
}
tcases := []tcase{
{"foo/bar", nil, nil, nil, map[string][]interface{}{"zip": {}, "baz": {}}, []string{"baz"}},
{"hello/universe", createDuration(50), createDuration(200), map[string][]interface{}{"foo": {}, "bar": {}}, nil, []string{"foo", "bar"}},
{"allow/all", nil, nil, map[string][]interface{}{"*": {}, "test": {}, "test1": {"foo"}}, nil, nil},
{"allow/all1", nil, nil, map[string][]interface{}{"*": {}, "test": {}, "test1": {"foo"}}, nil, nil},
{"deny/all", nil, nil, nil, map[string][]interface{}{"*": {}, "test": {}}, nil},
{"deny/all1", nil, nil, nil, map[string][]interface{}{"*": {}, "test": {}}, nil},
{"value/merge", nil, nil, map[string][]interface{}{"test": {3, 4, 1, 2}}, map[string][]interface{}{"test": {3, 4, 1, 2}}, nil},
{"value/empty", nil, nil, map[string][]interface{}{"empty": {}}, map[string][]interface{}{"empty": {}}, nil},
}
for _, tc := range tcases {
policyPath := ns.Path + tc.path
raw, ok := acl.exactRules.Get(policyPath)
if !ok {
t.Fatalf("Could not find acl entry for path %s", policyPath)
}
p := raw.(*ACLPermissions)
if !reflect.DeepEqual(tc.allowed, p.AllowedParameters) {
t.Fatalf("Allowed parameters did not match, Expected: %#v, Got: %#v", tc.allowed, p.AllowedParameters)
}
if !reflect.DeepEqual(tc.denied, p.DeniedParameters) {
t.Fatalf("Denied parameters did not match, Expected: %#v, Got: %#v", tc.denied, p.DeniedParameters)
}
if !reflect.DeepEqual(tc.required, p.RequiredParameters) {
t.Fatalf("Required parameters did not match, Expected: %#v, Got: %#v", tc.required, p.RequiredParameters)
}
if tc.minWrappingTTL != nil && *tc.minWrappingTTL != p.MinWrappingTTL {
t.Fatalf("Min wrapping TTL did not match, Expected: %#v, Got: %#v", tc.minWrappingTTL, p.MinWrappingTTL)
}
if tc.minWrappingTTL != nil && *tc.maxWrappingTTL != p.MaxWrappingTTL {
t.Fatalf("Max wrapping TTL did not match, Expected: %#v, Got: %#v", tc.maxWrappingTTL, p.MaxWrappingTTL)
}
}
}
func TestACL_AllowOperation(t *testing.T) {
t.Run("root-ns", func(t *testing.T) {
t.Parallel()
testACLAllowOperation(t, namespace.RootNamespace)
})
}
func testACLAllowOperation(t *testing.T, ns *namespace.Namespace) {
policy, err := ParseACLPolicy(ns, permissionsPolicy)
if err != nil {
t.Fatalf("err: %v", err)
}
ctx := namespace.ContextWithNamespace(context.Background(), ns)
acl, err := NewACL(ctx, []*Policy{policy})
if err != nil {
t.Fatalf("err: %v", err)
}
toperations := []logical.Operation{
logical.UpdateOperation,
logical.CreateOperation,
}
type tcase struct {
path string
wrappingTTL *time.Duration
parameters []string
allowed bool
}
createDuration := func(seconds int) *time.Duration {
ret := time.Duration(seconds) * time.Second
return &ret
}
tcases := []tcase{
{"dev/ops", nil, []string{"zip"}, true},
{"foo/bar", nil, []string{"zap"}, false},
{"foo/bar", nil, []string{"zip"}, false},
{"foo/bar", createDuration(50), []string{"zip"}, false},
{"foo/bar", createDuration(450), []string{"zip"}, false},
{"foo/bar", createDuration(350), []string{"zip"}, true},
{"foo/baz", nil, []string{"hello"}, false},
{"foo/baz", createDuration(50), []string{"hello"}, false},
{"foo/baz", createDuration(450), []string{"hello"}, true},
{"foo/baz", nil, []string{"zap"}, false},
{"broken/phone", nil, []string{"steve"}, false},
{"working/phone", nil, []string{""}, false},
{"working/phone", createDuration(450), []string{""}, false},
{"working/phone", createDuration(350), []string{""}, true},
{"hello/world", nil, []string{"one"}, false},
{"tree/fort", nil, []string{"one"}, true},
{"tree/fort", nil, []string{"foo"}, false},
{"fruit/apple", nil, []string{"pear"}, false},
{"fruit/apple", nil, []string{"one"}, false},
{"cold/weather", nil, []string{"four"}, true},
{"var/aws", nil, []string{"cold", "warm", "kitty"}, false},
{"var/req", nil, []string{"cold", "warm", "kitty"}, false},
{"var/req", nil, []string{"cold", "warm", "kitty", "foo"}, true},
}
for _, tc := range tcases {
request := &logical.Request{
Path: tc.path,
Data: make(map[string]interface{}),
}
for _, parameter := range tc.parameters {
request.Data[parameter] = ""
}
if tc.wrappingTTL != nil {
request.WrapInfo = &logical.RequestWrapInfo{
TTL: *tc.wrappingTTL,
}
}
for _, op := range toperations {
request.Operation = op
ctx := namespace.ContextWithNamespace(context.Background(), ns)
authResults := acl.AllowOperation(ctx, request, false)
if authResults.Allowed != tc.allowed {
t.Fatalf("bad: case %#v: %v", tc, authResults.Allowed)
}
}
}
}
func TestACL_ValuePermissions(t *testing.T) {
t.Run("root-ns", func(t *testing.T) {
t.Parallel()
testACLValuePermissions(t, namespace.RootNamespace)
})
}
func testACLValuePermissions(t *testing.T, ns *namespace.Namespace) {
policy, err := ParseACLPolicy(ns, valuePermissionsPolicy)
if err != nil {
t.Fatalf("err: %v", err)
}
ctx := namespace.ContextWithNamespace(context.Background(), ns)
acl, err := NewACL(ctx, []*Policy{policy})
if err != nil {
t.Fatalf("err: %v", err)
}
toperations := []logical.Operation{
logical.UpdateOperation,
logical.CreateOperation,
}
type tcase struct {
path string
parameters []string
values []interface{}
allowed bool
}
tcases := []tcase{
{"dev/ops", []string{"allow"}, []interface{}{"good"}, true},
{"dev/ops", []string{"allow"}, []interface{}{"bad"}, false},
{"foo/bar", []string{"deny"}, []interface{}{"bad"}, false},
{"foo/bar", []string{"deny"}, []interface{}{"bad glob"}, false},
{"foo/bar", []string{"deny"}, []interface{}{"good"}, true},
{"foo/bar", []string{"allow"}, []interface{}{"good"}, true},
{"foo/bar", []string{"deny"}, []interface{}{nil}, true},
{"foo/bar", []string{"allow"}, []interface{}{nil}, true},
{"foo/baz", []string{"aLLow"}, []interface{}{"good"}, true},
{"foo/baz", []string{"deny"}, []interface{}{"bad"}, false},
{"foo/baz", []string{"deny"}, []interface{}{"good"}, false},
{"foo/baz", []string{"allow", "deny"}, []interface{}{"good", "bad"}, false},
{"foo/baz", []string{"deny", "allow"}, []interface{}{"good", "bad"}, false},
{"foo/baz", []string{"deNy", "allow"}, []interface{}{"bad", "good"}, false},
{"foo/baz", []string{"aLLow"}, []interface{}{"bad"}, false},
{"foo/baz", []string{"Neither"}, []interface{}{"bad"}, false},
{"foo/baz", []string{"allow"}, []interface{}{nil}, false},
{"fizz/buzz", []string{"allow_multi"}, []interface{}{"good"}, true},
{"fizz/buzz", []string{"allow_multi"}, []interface{}{"good1"}, true},
{"fizz/buzz", []string{"allow_multi"}, []interface{}{"good2"}, true},
{"fizz/buzz", []string{"allow_multi"}, []interface{}{"glob good2"}, false},
{"fizz/buzz", []string{"allow_multi"}, []interface{}{"glob good3"}, true},
{"fizz/buzz", []string{"allow_multi"}, []interface{}{"bad"}, false},
{"fizz/buzz", []string{"allow_multi"}, []interface{}{"bad"}, false},
{"fizz/buzz", []string{"allow_multi", "allow"}, []interface{}{"good1", "good"}, true},
{"fizz/buzz", []string{"deny_multi"}, []interface{}{"bad2"}, false},
{"fizz/buzz", []string{"deny_multi", "allow_multi"}, []interface{}{"good", "good2"}, false},
// {"test/types", []string{"array"}, []interface{}{[1]string{"good"}}, true},
{"test/types", []string{"map"}, []interface{}{map[string]interface{}{"good": "one"}}, true},
{"test/types", []string{"map"}, []interface{}{map[string]interface{}{"bad": "one"}}, false},
{"test/types", []string{"int"}, []interface{}{1}, true},
{"test/types", []string{"int"}, []interface{}{3}, false},
{"test/types", []string{"bool"}, []interface{}{false}, true},
{"test/types", []string{"bool"}, []interface{}{true}, false},
{"test/star", []string{"anything"}, []interface{}{true}, true},
{"test/star", []string{"foo"}, []interface{}{true}, true},
{"test/star", []string{"bar"}, []interface{}{false}, true},
{"test/star", []string{"bar"}, []interface{}{true}, false},
}
for _, tc := range tcases {
request := &logical.Request{
Path: tc.path,
Data: make(map[string]interface{}),
}
ctx := namespace.ContextWithNamespace(context.Background(), ns)
for i, parameter := range tc.parameters {
request.Data[parameter] = tc.values[i]
}
for _, op := range toperations {
request.Operation = op
authResults := acl.AllowOperation(ctx, request, false)
if authResults.Allowed != tc.allowed {
t.Fatalf("bad: case %#v: %v", tc, authResults.Allowed)
}
}
}
}
func TestACL_SegmentWildcardPriority(t *testing.T) {
ns := namespace.RootNamespace
ctx := namespace.ContextWithNamespace(context.Background(), ns)
type poltest struct {
policy string
path string
}
// These test cases should each have a read rule and an update rule, where
// the update rule wins out due to being more specific.
poltests := []poltest{
{
// Verify edge conditions. Here '*' is more specific both because
// of first wildcard position (0 vs -1/infinity) and #wildcards.
`
path "+/*" { capabilities = ["read"] }
path "*" { capabilities = ["update"] }
`,
"foo/bar/bar/baz",
},
{
// Verify edge conditions. Here '+/*' is less specific because of
// first wildcard position.
`
path "+/*" { capabilities = ["read"] }
path "foo/+/*" { capabilities = ["update"] }
`,
"foo/bar/bar/baz",
},
{
// Verify that more wildcard segments is lower priority.
`
path "foo/+/+/*" { capabilities = ["read"] }
path "foo/+/bar/baz" { capabilities = ["update"] }
`,
"foo/bar/bar/baz",
},
{
// Verify that more wildcard segments is lower priority.
`
path "foo/+/+/baz" { capabilities = ["read"] }
path "foo/+/bar/baz" { capabilities = ["update"] }
`,
"foo/bar/bar/baz",
},
{
// Verify that first wildcard position is lower priority.
// '(' is used here because it is lexicographically smaller than "+"
`
path "foo/+/(ar/baz" { capabilities = ["read"] }
path "foo/(ar/+/baz" { capabilities = ["update"] }
`,
"foo/(ar/(ar/baz",
},
{
// Verify that a glob has lower priority, even if the prefix is the
// same otherwise.
`
path "foo/bar/+/baz*" { capabilities = ["read"] }
path "foo/bar/+/baz" { capabilities = ["update"] }
`,
"foo/bar/bar/baz",
},
{
// Verify that a shorter prefix has lower priority.
`
path "foo/bar/+/b*" { capabilities = ["read"] }
path "foo/bar/+/ba*" { capabilities = ["update"] }
`,
"foo/bar/bar/baz",
},
}
for i, pt := range poltests {
policy, err := ParseACLPolicy(ns, pt.policy)
if err != nil {
t.Fatalf("err: %v", err)
}
acl, err := NewACL(ctx, []*Policy{policy})
if err != nil {
t.Fatalf("err: %v", err)
}
request := new(logical.Request)
request.Path = pt.path
request.Operation = logical.UpdateOperation
authResults := acl.AllowOperation(ctx, request, false)
if !authResults.Allowed {
t.Fatalf("bad: case %d %#v: %v", i, pt, authResults.Allowed)
}
request.Operation = logical.ReadOperation
authResults = acl.AllowOperation(ctx, request, false)
if authResults.Allowed {
t.Fatalf("bad: case %d %#v: %v", i, pt, authResults.Allowed)
}
}
}
func TestACL_SegmentWildcardPriority_BareMount(t *testing.T) {
ns := namespace.RootNamespace
ctx := namespace.ContextWithNamespace(context.Background(), ns)
type poltest struct {
policy string
mountpath string
hasperms bool
}
// These test cases should have one or more rules and a mount prefix.
// hasperms should be true if there are non-deny perms that apply
// to the mount prefix or something below it.
poltests := []poltest{
{
`path "+" { capabilities = ["read"] }`,
"foo/",
true,
},
{
`path "+/*" { capabilities = ["read"] }`,
"foo/",
true,
},
{
`path "foo/+/+/*" { capabilities = ["read"] }`,
"foo/",
true,
},
{
`path "foo/+/+/*" { capabilities = ["read"] }`,
"foo/bar/",
true,
},
{
`path "foo/+/+/*" { capabilities = ["read"] }`,
"foo/bar/bar/",
true,
},
{
`path "foo/+/+/*" { capabilities = ["read"] }`,
"foo/bar/bar/baz/",
true,
},
{
`path "foo/+/+/baz" { capabilities = ["read"] }`,
"foo/bar/bar/baz/",
true,
},
{
`path "foo/+/bar/baz" { capabilities = ["read"] }`,
"foo/bar/bar/baz/",
true,
},
{
`path "foo/bar/+/baz*" { capabilities = ["read"] }`,
"foo/bar/bar/baz/",
true,
},
{
`path "foo/bar/+/b*" { capabilities = ["read"] }`,
"foo/bar/bar/baz/",
true,
},
{
`path "foo/+" { capabilities = ["read"] }`,
"foo/",
true,
},
}
for i, pt := range poltests {
policy, err := ParseACLPolicy(ns, pt.policy)
if err != nil {
t.Fatalf("err: %v", err)
}
acl, err := NewACL(ctx, []*Policy{policy})
if err != nil {
t.Fatalf("err: %v", err)
}
hasperms := nil != acl.CheckAllowedFromNonExactPaths(pt.mountpath, true)
if hasperms != pt.hasperms {
t.Fatalf("bad: case %d: %#v", i, pt)
}
}
}
// NOTE: this test doesn't catch any races ATM
func TestACL_CreationRace(t *testing.T) {
policy, err := ParseACLPolicy(namespace.RootNamespace, valuePermissionsPolicy)
if err != nil {
t.Fatalf("err: %v", err)
}
var wg sync.WaitGroup
errs := make(chan error)
stopTime := time.Now().Add(20 * time.Second)
for i := 0; i < 50; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
for {
if time.Now().After(stopTime) {
return
}
_, err := NewACL(namespace.RootContext(context.Background()), []*Policy{policy})
if err != nil {
errs <- fmt.Errorf("goroutine %d: %w", i, err)
}
}
}(i)
}
go func() {
wg.Wait()
close(errs)
}()
for err := range errs {
t.Fatalf("err: %v", err)
}
}
func TestACLGrantingPolicies(t *testing.T) {
ns := namespace.RootNamespace
policy, err := ParseACLPolicy(ns, grantingTestPolicy)
if err != nil {
t.Fatalf("err: %v", err)
}
merged, err := ParseACLPolicy(ns, grantingTestPolicyMerged)
if err != nil {
t.Fatalf("err: %v", err)
}
ctx := namespace.ContextWithNamespace(context.Background(), ns)
type tcase struct {
path string
op logical.Operation
policies []*Policy
expected []logical.PolicyInfo
allowed bool
}
policyInfo := logical.PolicyInfo{
Name: "granting_policy",
NamespaceId: "root",
NamespacePath: "",
Type: "acl",
}
mergedInfo := logical.PolicyInfo{
Name: "granting_policy_merged",
NamespaceId: "root",
NamespacePath: "",
Type: "acl",
}
tcases := []tcase{
{"kv/foo", logical.ReadOperation, []*Policy{policy}, []logical.PolicyInfo{policyInfo}, true},
{"kv/foo", logical.UpdateOperation, []*Policy{policy}, []logical.PolicyInfo{policyInfo}, true},
{"kv/bad", logical.ReadOperation, []*Policy{policy}, nil, false},
{"kv/deny", logical.ReadOperation, []*Policy{policy}, nil, false},
{"kv/path/foo", logical.ReadOperation, []*Policy{policy}, []logical.PolicyInfo{policyInfo}, true},
{"kv/path/longer", logical.ReadOperation, []*Policy{policy}, []logical.PolicyInfo{policyInfo}, true},
{"kv/foo", logical.ReadOperation, []*Policy{policy, merged}, []logical.PolicyInfo{policyInfo, mergedInfo}, true},
{"kv/path/longer3", logical.ReadOperation, []*Policy{policy, merged}, []logical.PolicyInfo{mergedInfo}, true},
{"kv/bar", logical.ReadOperation, []*Policy{policy, merged}, []logical.PolicyInfo{mergedInfo}, true},
{"kv/deny", logical.ReadOperation, []*Policy{policy, merged}, nil, false},
{"kv/path/longer", logical.UpdateOperation, []*Policy{policy, merged}, []logical.PolicyInfo{policyInfo}, true},
{"kv/path/foo", logical.ReadOperation, []*Policy{policy, merged}, []logical.PolicyInfo{policyInfo, mergedInfo}, true},
}
for _, tc := range tcases {
request := &logical.Request{
Path: tc.path,
Operation: tc.op,
}
acl, err := NewACL(ctx, tc.policies)
if err != nil {
t.Fatalf("err: %v", err)
}
authResults := acl.AllowOperation(ctx, request, false)
if authResults.Allowed != tc.allowed {
t.Fatalf("bad: case %#v: %v", tc, authResults.Allowed)
}
if !reflect.DeepEqual(authResults.GrantingPolicies, tc.expected) {
t.Fatalf("bad: case %#v: got\n%#v\nexpected\n%#v\n", tc, authResults.GrantingPolicies, tc.expected)
}
}
}
var grantingTestPolicy = `
name = "granting_policy"
path "kv/foo" {
capabilities = ["update", "read"]
}
path "kv/path/*" {
capabilities = ["read"]
}
path "kv/path/longer" {
capabilities = ["update", "read"]
}
path "kv/path/longer2" {
capabilities = ["update"]
}
path "kv/deny" {
capabilities = ["deny"]
}
path "ns1/kv/foo" {
capabilities = ["update", "read"]
}
`
var grantingTestPolicyMerged = `
name = "granting_policy_merged"
path "kv/foo" {
capabilities = ["update", "read"]
}
path "kv/bar" {
capabilities = ["update", "read"]
}
path "kv/path/*" {
capabilities = ["read"]
}
path "kv/path/longer" {
capabilities = ["read"]
}
path "kv/path/longer3" {
capabilities = ["read"]
}
path "kv/deny" {
capabilities = ["update"]
}
`
var tokenCreationPolicy = `
name = "tokenCreation"
path "auth/token/create*" {
capabilities = ["update", "create", "sudo"]
}
`
var aclPolicy = `
name = "DeV"
path "dev/*" {
policy = "sudo"
}
path "stage/*" {
policy = "write"
}
path "stage/aws/*" {
policy = "read"
capabilities = ["update", "sudo"]
}
path "stage/aws/policy/*" {
policy = "sudo"
}
path "prod/*" {
policy = "read"
}
path "prod/aws/*" {
policy = "deny"
}
path "sys/*" {
policy = "deny"
}
path "foo/bar" {
capabilities = ["read", "create", "sudo"]
}
path "baz/quux" {
capabilities = ["read", "create", "patch"]
}
path "test/+/segment" {
capabilities = ["read"]
}
path "+/segment/at/front" {
capabilities = ["read"]
}
path "test/segment/at/end/+" {
capabilities = ["read"]
}
path "test/segment/at/end/v2/+/" {
capabilities = ["read"]
}
path "test/+/wildcard/+/*" {
capabilities = ["read"]
}
path "test/+/wildcardglob/+/end*" {
capabilities = ["read"]
}
path "1/2/*" {
capabilities = ["create"]
}
path "1/2/+" {
capabilities = ["read"]
}
path "1/2/+/+" {
capabilities = ["update"]
}
`
var aclPolicy2 = `
name = "OpS"
path "dev/hide/*" {
policy = "deny"
}
path "stage/aws/policy/*" {
policy = "deny"
# This should have no effect
capabilities = ["read", "update", "sudo"]
}
path "prod/*" {
policy = "write"
}
path "sys/seal" {
policy = "sudo"
}
path "foo/bar" {
capabilities = ["deny"]
}
path "baz/quux" {
capabilities = ["deny"]
}
`
// test merging
var mergingPolicies = `
name = "ops"
path "foo/bar" {
policy = "write"
denied_parameters = {
"baz" = []
}
required_parameters = ["baz"]
}
path "foo/bar" {
policy = "write"
denied_parameters = {
"zip" = []
}
}
path "hello/universe" {
policy = "write"
allowed_parameters = {
"foo" = []
}
required_parameters = ["foo"]
max_wrapping_ttl = 300
min_wrapping_ttl = 100
}
path "hello/universe" {
policy = "write"
allowed_parameters = {
"bar" = []
}
required_parameters = ["bar"]
max_wrapping_ttl = 200
min_wrapping_ttl = 50
}
path "allow/all" {
policy = "write"
allowed_parameters = {
"test" = []
"test1" = ["foo"]
}
}
path "allow/all" {
policy = "write"
allowed_parameters = {
"*" = []
}
}
path "allow/all1" {
policy = "write"
allowed_parameters = {
"*" = []
}
}
path "allow/all1" {
policy = "write"
allowed_parameters = {
"test" = []
"test1" = ["foo"]
}
}
path "deny/all" {
policy = "write"
denied_parameters = {
"test" = []
}
}
path "deny/all" {
policy = "write"
denied_parameters = {
"*" = []
}
}
path "deny/all1" {
policy = "write"
denied_parameters = {
"*" = []
}
}
path "deny/all1" {
policy = "write"
denied_parameters = {
"test" = []
}
}
path "value/merge" {
policy = "write"
allowed_parameters = {
"test" = [1, 2]
}
denied_parameters = {
"test" = [1, 2]
}
}
path "value/merge" {
policy = "write"
allowed_parameters = {
"test" = [3, 4]
}
denied_parameters = {
"test" = [3, 4]
}
}
path "value/empty" {
policy = "write"
allowed_parameters = {
"empty" = []
}
denied_parameters = {
"empty" = [1]
}
}
path "value/empty" {
policy = "write"
allowed_parameters = {
"empty" = [1]
}
denied_parameters = {
"empty" = []
}
}
`
// allow operation testing
var permissionsPolicy = `
name = "dev"
path "dev/*" {
policy = "write"
allowed_parameters = {
"zip" = []
}
}
path "foo/bar" {
policy = "write"
denied_parameters = {
"zap" = []
}
min_wrapping_ttl = 300
max_wrapping_ttl = 400
}
path "foo/baz" {
policy = "write"
allowed_parameters = {
"hello" = []
}
denied_parameters = {
"zap" = []
}
min_wrapping_ttl = 300
}
path "working/phone" {
policy = "write"
max_wrapping_ttl = 400
}
path "broken/phone" {
policy = "write"
allowed_parameters = {
"steve" = []
}
denied_parameters = {
"steve" = []
}
}
path "hello/world" {
policy = "write"
allowed_parameters = {
"*" = []
}
denied_parameters = {
"*" = []
}
}
path "tree/fort" {
policy = "write"
allowed_parameters = {
"*" = []
}
denied_parameters = {
"foo" = []
}
}
path "fruit/apple" {
policy = "write"
allowed_parameters = {
"pear" = []
}
denied_parameters = {
"*" = []
}
}
path "cold/weather" {
policy = "write"
allowed_parameters = {}
denied_parameters = {}
}
path "var/aws" {
policy = "write"
allowed_parameters = {
"*" = []
}
denied_parameters = {
"soft" = []
"warm" = []
"kitty" = []
}
}
path "var/req" {
policy = "write"
required_parameters = ["foo"]
}
`
// allow operation testing
var valuePermissionsPolicy = `
name = "op"
path "dev/*" {
policy = "write"
allowed_parameters = {
"allow" = ["good"]
}
}
path "foo/bar" {
policy = "write"
denied_parameters = {
"deny" = ["bad*"]
}
}
path "foo/baz" {
policy = "write"
allowed_parameters = {
"ALLOW" = ["good"]
}
denied_parameters = {
"dEny" = ["bad"]
}
}
path "fizz/buzz" {
policy = "write"
allowed_parameters = {
"allow_multi" = ["good", "good1", "good2", "*good3"]
"allow" = ["good"]
}
denied_parameters = {
"deny_multi" = ["bad", "bad1", "bad2"]
}
}
path "test/types" {
policy = "write"
allowed_parameters = {
"map" = [{"good" = "one"}]
"int" = [1, 2]
"bool" = [false]
}
denied_parameters = {
}
}
path "test/star" {
policy = "write"
allowed_parameters = {
"*" = []
"foo" = []
"bar" = [false]
}
denied_parameters = {
}
}
`