2970f245c5
[VAULT-14497] Ensure Role Governing Policies are only applied down the namespace hierarchy (#23090) --------- Co-authored-by: Kuba Wieczorek <kuba.wieczorek@hashicorp.com>
406 lines
11 KiB
Go
406 lines
11 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package vault
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/vault/helper/namespace"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func mockPolicyWithCore(t *testing.T, disableCache bool) (*Core, *PolicyStore) {
|
|
conf := &CoreConfig{
|
|
DisableCache: disableCache,
|
|
}
|
|
core, _, _ := TestCoreUnsealedWithConfig(t, conf)
|
|
ps := core.policyStore
|
|
|
|
return core, ps
|
|
}
|
|
|
|
func TestPolicyStore_Root(t *testing.T) {
|
|
t.Run("root", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
core, _, _ := TestCoreUnsealed(t)
|
|
ps := core.policyStore
|
|
testPolicyRoot(t, ps, namespace.RootNamespace, true)
|
|
})
|
|
}
|
|
|
|
func testPolicyRoot(t *testing.T, ps *PolicyStore, ns *namespace.Namespace, expectFound bool) {
|
|
// Get should return a special policy
|
|
ctx := namespace.ContextWithNamespace(context.Background(), ns)
|
|
p, err := ps.GetPolicy(ctx, "root", PolicyTypeToken)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
// Handle whether a root token is expected
|
|
if expectFound {
|
|
if p == nil {
|
|
t.Fatalf("bad: %v", p)
|
|
}
|
|
if p.Name != "root" {
|
|
t.Fatalf("bad: %v", p)
|
|
}
|
|
} else {
|
|
if p != nil {
|
|
t.Fatal("expected nil root policy")
|
|
}
|
|
// Create root policy for subsequent modification and deletion failure
|
|
// tests
|
|
p = &Policy{
|
|
Name: "root",
|
|
}
|
|
}
|
|
|
|
// Set should fail
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
err = ps.SetPolicy(ctx, p)
|
|
if err.Error() != `cannot update "root" policy` {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
// Delete should fail
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
err = ps.DeletePolicy(ctx, "root", PolicyTypeACL)
|
|
if err.Error() != `cannot delete "root" policy` {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPolicyStore_CRUD(t *testing.T) {
|
|
t.Run("root-ns", func(t *testing.T) {
|
|
t.Run("cached", func(t *testing.T) {
|
|
_, ps := mockPolicyWithCore(t, false)
|
|
testPolicyStoreCRUD(t, ps, namespace.RootNamespace)
|
|
})
|
|
|
|
t.Run("no-cache", func(t *testing.T) {
|
|
_, ps := mockPolicyWithCore(t, true)
|
|
testPolicyStoreCRUD(t, ps, namespace.RootNamespace)
|
|
})
|
|
})
|
|
}
|
|
|
|
func testPolicyStoreCRUD(t *testing.T, ps *PolicyStore, ns *namespace.Namespace) {
|
|
// Get should return nothing
|
|
ctx := namespace.ContextWithNamespace(context.Background(), ns)
|
|
p, err := ps.GetPolicy(ctx, "Dev", PolicyTypeToken)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if p != nil {
|
|
t.Fatalf("bad: %v", p)
|
|
}
|
|
|
|
// Delete should be no-op
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
err = ps.DeletePolicy(ctx, "deV", PolicyTypeACL)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
// List should be blank
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
out, err := ps.ListPolicies(ctx, PolicyTypeACL)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if len(out) != 1 {
|
|
t.Fatalf("bad: %v", out)
|
|
}
|
|
|
|
// Set should work
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
policy, _ := ParseACLPolicy(ns, aclPolicy)
|
|
err = ps.SetPolicy(ctx, policy)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
// Get should work
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
p, err = ps.GetPolicy(ctx, "dEv", PolicyTypeToken)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(p, policy) {
|
|
t.Fatalf("bad: %v", p)
|
|
}
|
|
|
|
// List should contain two elements
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
out, err = ps.ListPolicies(ctx, PolicyTypeACL)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if len(out) != 2 {
|
|
t.Fatalf("bad: %v", out)
|
|
}
|
|
|
|
expected := []string{"default", "dev"}
|
|
if !reflect.DeepEqual(expected, out) {
|
|
t.Fatalf("expected: %v\ngot: %v", expected, out)
|
|
}
|
|
|
|
// Delete should be clear the entry
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
err = ps.DeletePolicy(ctx, "Dev", PolicyTypeACL)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
// List should contain one element
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
out, err = ps.ListPolicies(ctx, PolicyTypeACL)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if len(out) != 1 || out[0] != "default" {
|
|
t.Fatalf("bad: %v", out)
|
|
}
|
|
|
|
// Get should fail
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
p, err = ps.GetPolicy(ctx, "deV", PolicyTypeToken)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if p != nil {
|
|
t.Fatalf("bad: %v", p)
|
|
}
|
|
}
|
|
|
|
func TestPolicyStore_Predefined(t *testing.T) {
|
|
t.Run("root-ns", func(t *testing.T) {
|
|
_, ps := mockPolicyWithCore(t, false)
|
|
testPolicyStorePredefined(t, ps, namespace.RootNamespace)
|
|
})
|
|
}
|
|
|
|
// Test predefined policy handling
|
|
func testPolicyStorePredefined(t *testing.T, ps *PolicyStore, ns *namespace.Namespace) {
|
|
// List should be two elements
|
|
ctx := namespace.ContextWithNamespace(context.Background(), ns)
|
|
out, err := ps.ListPolicies(ctx, PolicyTypeACL)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
// This shouldn't contain response-wrapping since it's non-assignable
|
|
if len(out) != 1 || out[0] != "default" {
|
|
t.Fatalf("bad: %v", out)
|
|
}
|
|
|
|
// Response-wrapping policy checks
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
pCubby, err := ps.GetPolicy(ctx, "response-wrapping", PolicyTypeToken)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if pCubby == nil {
|
|
t.Fatal("nil cubby policy")
|
|
}
|
|
if pCubby.Raw != responseWrappingPolicy {
|
|
t.Fatalf("bad: expected\n%s\ngot\n%s\n", responseWrappingPolicy, pCubby.Raw)
|
|
}
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
err = ps.SetPolicy(ctx, pCubby)
|
|
if err == nil {
|
|
t.Fatalf("expected err setting %s", pCubby.Name)
|
|
}
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
err = ps.DeletePolicy(ctx, pCubby.Name, PolicyTypeACL)
|
|
if err == nil {
|
|
t.Fatalf("expected err deleting %s", pCubby.Name)
|
|
}
|
|
|
|
// Root policy checks, behavior depending on namespace
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
pRoot, err := ps.GetPolicy(ctx, "root", PolicyTypeToken)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if ns == namespace.RootNamespace {
|
|
if pRoot == nil {
|
|
t.Fatal("nil root policy")
|
|
}
|
|
} else {
|
|
if pRoot != nil {
|
|
t.Fatal("expected nil root policy")
|
|
}
|
|
pRoot = &Policy{
|
|
Name: "root",
|
|
}
|
|
}
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
err = ps.SetPolicy(ctx, pRoot)
|
|
if err == nil {
|
|
t.Fatalf("expected err setting %s", pRoot.Name)
|
|
}
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
err = ps.DeletePolicy(ctx, pRoot.Name, PolicyTypeACL)
|
|
if err == nil {
|
|
t.Fatalf("expected err deleting %s", pRoot.Name)
|
|
}
|
|
}
|
|
|
|
func TestPolicyStore_ACL(t *testing.T) {
|
|
t.Run("root-ns", func(t *testing.T) {
|
|
_, ps := mockPolicyWithCore(t, false)
|
|
testPolicyStoreACL(t, ps, namespace.RootNamespace)
|
|
})
|
|
}
|
|
|
|
func testPolicyStoreACL(t *testing.T, ps *PolicyStore, ns *namespace.Namespace) {
|
|
ctx := namespace.ContextWithNamespace(context.Background(), ns)
|
|
policy, _ := ParseACLPolicy(ns, aclPolicy)
|
|
err := ps.SetPolicy(ctx, policy)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
policy, _ = ParseACLPolicy(ns, aclPolicy2)
|
|
err = ps.SetPolicy(ctx, policy)
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
ctx = namespace.ContextWithNamespace(context.Background(), ns)
|
|
acl, err := ps.ACL(ctx, nil, map[string][]string{ns.ID: {"dev", "ops"}})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
testLayeredACL(t, acl, ns)
|
|
}
|
|
|
|
func TestDefaultPolicy(t *testing.T) {
|
|
ctx := namespace.ContextWithNamespace(context.Background(), namespace.RootNamespace)
|
|
|
|
policy, err := ParseACLPolicy(namespace.RootNamespace, defaultPolicy)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
acl, err := NewACL(ctx, []*Policy{policy})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for name, tc := range map[string]struct {
|
|
op logical.Operation
|
|
path string
|
|
expectAllowed bool
|
|
}{
|
|
"lookup self": {logical.ReadOperation, "auth/token/lookup-self", true},
|
|
"renew self": {logical.UpdateOperation, "auth/token/renew-self", true},
|
|
"revoke self": {logical.UpdateOperation, "auth/token/revoke-self", true},
|
|
"check own capabilities": {logical.UpdateOperation, "sys/capabilities-self", true},
|
|
|
|
"read arbitrary path": {logical.ReadOperation, "foo/bar", false},
|
|
"login at arbitrary path": {logical.UpdateOperation, "auth/foo", false},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
request := new(logical.Request)
|
|
request.Operation = tc.op
|
|
request.Path = tc.path
|
|
|
|
result := acl.AllowOperation(ctx, request, false)
|
|
if result.RootPrivs {
|
|
t.Fatal("unexpected root")
|
|
}
|
|
if tc.expectAllowed != result.Allowed {
|
|
t.Fatalf("Expected %v, got %v", tc.expectAllowed, result.Allowed)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPolicyStore_GetNonEGPPolicyType has five test cases:
|
|
// - happy-acl and happy-rgp: we store a policy in the policy type map and
|
|
// then look up its type successfully.
|
|
// - not-in-map-acl and not-in-map-rgp: ensure that GetNonEGPPolicyType fails
|
|
// returning a nil and an error when the policy doesn't exist in the map.
|
|
// - unknown-policy-type: ensures that GetNonEGPPolicyType fails returning a nil
|
|
// and an error when the policy type in the type map is a value that
|
|
// does not map to a PolicyType.
|
|
func TestPolicyStore_GetNonEGPPolicyType(t *testing.T) {
|
|
t.Parallel()
|
|
tests := map[string]struct {
|
|
policyStoreKey string
|
|
policyStoreValue any
|
|
paramNamespace string
|
|
paramPolicyName string
|
|
paramPolicyType PolicyType
|
|
isErrorExpected bool
|
|
expectedErrorMessage string
|
|
}{
|
|
"happy-acl": {
|
|
policyStoreKey: "1AbcD/policy1",
|
|
policyStoreValue: PolicyTypeACL,
|
|
paramNamespace: "1AbcD",
|
|
paramPolicyName: "policy1",
|
|
paramPolicyType: PolicyTypeACL,
|
|
},
|
|
"happy-rgp": {
|
|
policyStoreKey: "1AbcD/policy1",
|
|
policyStoreValue: PolicyTypeRGP,
|
|
paramNamespace: "1AbcD",
|
|
paramPolicyName: "policy1",
|
|
paramPolicyType: PolicyTypeRGP,
|
|
},
|
|
"not-in-map-acl": {
|
|
policyStoreKey: "2WxyZ/policy2",
|
|
policyStoreValue: PolicyTypeACL,
|
|
paramNamespace: "1AbcD",
|
|
paramPolicyName: "policy1",
|
|
isErrorExpected: true,
|
|
expectedErrorMessage: "policy does not exist in type map: 1AbcD/policy1",
|
|
},
|
|
"not-in-map-rgp": {
|
|
policyStoreKey: "2WxyZ/policy2",
|
|
policyStoreValue: PolicyTypeRGP,
|
|
paramNamespace: "1AbcD",
|
|
paramPolicyName: "policy1",
|
|
isErrorExpected: true,
|
|
expectedErrorMessage: "policy does not exist in type map: 1AbcD/policy1",
|
|
},
|
|
"unknown-policy-type": {
|
|
policyStoreKey: "1AbcD/policy1",
|
|
policyStoreValue: 7,
|
|
paramNamespace: "1AbcD",
|
|
paramPolicyName: "policy1",
|
|
isErrorExpected: true,
|
|
expectedErrorMessage: "unknown policy type for: 1AbcD/policy1",
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
name := name
|
|
tc := tc
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, ps := mockPolicyWithCore(t, false)
|
|
ps.policyTypeMap.Store(tc.policyStoreKey, tc.policyStoreValue)
|
|
got, err := ps.GetNonEGPPolicyType(tc.paramNamespace, tc.paramPolicyName)
|
|
if tc.isErrorExpected {
|
|
require.Error(t, err)
|
|
require.Nil(t, got)
|
|
require.EqualError(t, err, tc.expectedErrorMessage)
|
|
|
|
}
|
|
if !tc.isErrorExpected {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, got)
|
|
require.Equal(t, tc.paramPolicyType, *got)
|
|
}
|
|
})
|
|
}
|
|
}
|