Skip to content

Commit

Permalink
mesh: add DestinationPolicy ACL hook tenancy tests (#19178)
Browse files Browse the repository at this point in the history
Enhance the DestinationPolicy ACL hook tests to cover tenanted situations.
These tests will only execute in enterprise.
  • Loading branch information
rboyer authored Oct 16, 2023
1 parent ad17769 commit df8ea43
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 83 deletions.
151 changes: 72 additions & 79 deletions internal/mesh/internal/types/destination_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
package types

import (
"fmt"
"testing"
"time"

"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"

"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/resourcetest"
Expand Down Expand Up @@ -518,99 +518,92 @@ func TestDestinationPolicyACLs(t *testing.T) {
registry := resource.NewRegistry()
Register(registry)

type testcase struct {
rules string
check func(t *testing.T, authz acl.Authorizer, res *pbresource.Resource)
readOK string
writeOK string
listOK string
newPolicy := func(t *testing.T, tenancyStr string) *pbresource.Resource {
res := resourcetest.Resource(pbmesh.DestinationPolicyType, "api").
WithTenancy(resourcetest.Tenancy(tenancyStr)).
WithData(t, &pbmesh.DestinationPolicy{
PortConfigs: map[string]*pbmesh.DestinationConfig{
"http": {
ConnectTimeout: durationpb.New(55 * time.Second),
},
},
}).
Build()
resourcetest.ValidateAndNormalize(t, registry, res)
return res
}

const (
DENY = "deny"
ALLOW = "allow"
DEFAULT = "default"
DENY = resourcetest.DENY
ALLOW = resourcetest.ALLOW
DEFAULT = resourcetest.DEFAULT
)

checkF := func(t *testing.T, expect string, got error) {
switch expect {
case ALLOW:
if acl.IsErrPermissionDenied(got) {
t.Fatal("should be allowed")
}
case DENY:
if !acl.IsErrPermissionDenied(got) {
t.Fatal("should be denied")
}
case DEFAULT:
require.Nil(t, got, "expected fallthrough decision")
default:
t.Fatalf("unexpected expectation: %q", expect)
}
run := func(t *testing.T, name string, tc resourcetest.ACLTestCase) {
t.Run(name, func(t *testing.T) {
resourcetest.RunACLTestCase(t, tc, registry)
})
}

reg, ok := registry.Resolve(pbmesh.DestinationPolicyType)
require.True(t, ok)
isEnterprise := (structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty() == "default")

run := func(t *testing.T, tc testcase) {
destData := &pbmesh.DestinationPolicy{
PortConfigs: map[string]*pbmesh.DestinationConfig{
"http": {
ConnectTimeout: durationpb.New(55 * time.Second),
},
},
serviceRead := func(partition, namespace, name string) string {
if isEnterprise {
return fmt.Sprintf(` partition %q { namespace %q { service %q { policy = "read" } } }`, partition, namespace, name)
}
res := resourcetest.Resource(pbmesh.DestinationPolicyType, "api").
WithTenancy(resource.DefaultNamespacedTenancy()).
WithData(t, destData).
Build()
resourcetest.ValidateAndNormalize(t, registry, res)

config := acl.Config{
WildcardName: structs.WildcardSpecifier,
return fmt.Sprintf(` service %q { policy = "read" } `, name)
}
serviceWrite := func(partition, namespace, name string) string {
if isEnterprise {
return fmt.Sprintf(` partition %q { namespace %q { service %q { policy = "write" } } }`, partition, namespace, name)
}
authz, err := acl.NewAuthorizerFromRules(tc.rules, &config, nil)
require.NoError(t, err)
authz = acl.NewChainedAuthorizer([]acl.Authorizer{authz, acl.DenyAll()})
return fmt.Sprintf(` service %q { policy = "write" } `, name)
}

t.Run("read", func(t *testing.T) {
err := reg.ACLs.Read(authz, &acl.AuthorizerContext{}, res.Id, nil)
checkF(t, tc.readOK, err)
})
t.Run("write", func(t *testing.T) {
err := reg.ACLs.Write(authz, &acl.AuthorizerContext{}, res)
checkF(t, tc.writeOK, err)
})
t.Run("list", func(t *testing.T) {
err := reg.ACLs.List(authz, &acl.AuthorizerContext{})
checkF(t, tc.listOK, err)
})
assert := func(t *testing.T, name string, rules string, res *pbresource.Resource, readOK, writeOK string) {
tc := resourcetest.ACLTestCase{
AuthCtx: resource.AuthorizerContext(res.Id.Tenancy),
Rules: rules,
Res: res,
ReadOK: readOK,
WriteOK: writeOK,
ListOK: DEFAULT,
}
run(t, name, tc)
}

cases := map[string]testcase{
"no rules": {
rules: ``,
readOK: DENY,
writeOK: DENY,
listOK: DEFAULT,
},
"service api read": {
rules: `service "api" { policy = "read" }`,
readOK: ALLOW,
writeOK: DENY,
listOK: DEFAULT,
},
"service api write": {
rules: `service "api" { policy = "write" }`,
readOK: ALLOW,
writeOK: ALLOW,
listOK: DEFAULT,
},
tenancies := []string{"default.default"}
if isEnterprise {
tenancies = append(tenancies, "default.foo", "alpha.default", "alpha.foo")
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
run(t, tc)
for _, policyTenancyStr := range tenancies {
t.Run("policy tenancy: "+policyTenancyStr, func(t *testing.T) {
for _, aclTenancyStr := range tenancies {
t.Run("acl tenancy: "+aclTenancyStr, func(t *testing.T) {
aclTenancy := resourcetest.Tenancy(aclTenancyStr)

maybe := func(match string) string {
if policyTenancyStr != aclTenancyStr {
return DENY
}
return match
}

t.Run("no rules", func(t *testing.T) {
rules := ``
assert(t, "any", rules, newPolicy(t, policyTenancyStr), DENY, DENY)
})
t.Run("api:read", func(t *testing.T) {
rules := serviceRead(aclTenancy.Partition, aclTenancy.Namespace, "api")
assert(t, "any", rules, newPolicy(t, policyTenancyStr), maybe(ALLOW), DENY)
})
t.Run("api:write", func(t *testing.T) {
rules := serviceWrite(aclTenancy.Partition, aclTenancy.Namespace, "api")
assert(t, "any", rules, newPolicy(t, policyTenancyStr), maybe(ALLOW), maybe(ALLOW))
})
})
}
})
}
}
15 changes: 11 additions & 4 deletions internal/resource/resourcetest/acls.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ var checkF = func(t *testing.T, expect string, got error) {
type ACLTestCase struct {
Rules string

// AuthCtx is optional. If not provided an empty one will be used.
AuthCtx *acl.AuthorizerContext

// One of either Res or Data/Owner/Typ should be set.
Res *pbresource.Resource
Data protoreflect.ProtoMessage
Expand Down Expand Up @@ -92,21 +95,25 @@ func RunACLTestCase(t *testing.T, tc ACLTestCase, registry resource.Registry) {
require.NoError(t, err)
authz = acl.NewChainedAuthorizer([]acl.Authorizer{authz, acl.DenyAll()})

if tc.AuthCtx == nil {
tc.AuthCtx = &acl.AuthorizerContext{}
}

if tc.ReadHookRequiresResource {
err = reg.ACLs.Read(authz, &acl.AuthorizerContext{}, res.Id, nil)
err = reg.ACLs.Read(authz, tc.AuthCtx, res.Id, nil)
require.ErrorIs(t, err, resource.ErrNeedResource, "read hook should require the data payload")
}

t.Run("read", func(t *testing.T) {
err := reg.ACLs.Read(authz, &acl.AuthorizerContext{}, res.Id, res)
err := reg.ACLs.Read(authz, tc.AuthCtx, res.Id, res)
checkF(t, tc.ReadOK, err)
})
t.Run("write", func(t *testing.T) {
err := reg.ACLs.Write(authz, &acl.AuthorizerContext{}, res)
err := reg.ACLs.Write(authz, tc.AuthCtx, res)
checkF(t, tc.WriteOK, err)
})
t.Run("list", func(t *testing.T) {
err := reg.ACLs.List(authz, &acl.AuthorizerContext{})
err := reg.ACLs.List(authz, tc.AuthCtx)
checkF(t, tc.ListOK, err)
})
}

0 comments on commit df8ea43

Please sign in to comment.