Skip to content

Commit

Permalink
catalog: add FailoverPolicy ACL hook tenancy test
Browse files Browse the repository at this point in the history
  • Loading branch information
rboyer committed Oct 13, 2023
1 parent ea86249 commit e67f2d1
Showing 1 changed file with 138 additions and 86 deletions.
224 changes: 138 additions & 86 deletions internal/catalog/internal/types/failover_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
package types

import (
"strings"
"fmt"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -140,7 +140,7 @@ func TestMutateFailoverPolicy(t *testing.T) {
},
},
"dest ref tenancy defaulting": {
policyTenancy: newTestTenancy("foo.bar"),
policyTenancy: resourcetest.Tenancy("foo.bar"),
failover: &pbcatalog.FailoverPolicy{
Config: &pbcatalog.FailoverConfig{
Mode: pbcatalog.FailoverMode_FAILOVER_MODE_SEQUENTIAL,
Expand Down Expand Up @@ -685,12 +685,39 @@ func TestFailoverPolicyACLs(t *testing.T) {
registry := resource.NewRegistry()
Register(registry)

newFailover := func(t *testing.T, name, tenancyStr string, destRefs []*pbresource.Reference) []*pbresource.Resource {
var dr []*pbcatalog.FailoverDestination
for _, destRef := range destRefs {
dr = append(dr, &pbcatalog.FailoverDestination{Ref: destRef})
}

res1 := resourcetest.Resource(pbcatalog.FailoverPolicyType, name).
WithTenancy(resourcetest.Tenancy(tenancyStr)).
WithData(t, &pbcatalog.FailoverPolicy{
Config: &pbcatalog.FailoverConfig{Destinations: dr},
}).
Build()
resourcetest.ValidateAndNormalize(t, registry, res1)

res2 := resourcetest.Resource(pbcatalog.FailoverPolicyType, name).
WithTenancy(resourcetest.Tenancy(tenancyStr)).
WithData(t, &pbcatalog.FailoverPolicy{
PortConfigs: map[string]*pbcatalog.FailoverConfig{
"http": {Destinations: dr},
},
}).
Build()
resourcetest.ValidateAndNormalize(t, registry, res2)

return []*pbresource.Resource{res1, res2}
}

type testcase struct {
res *pbresource.Resource
rules string
check func(t *testing.T, authz acl.Authorizer, res *pbresource.Resource)
readOK string
writeOK string
listOK string
}

const (
Expand All @@ -699,91 +726,135 @@ func TestFailoverPolicyACLs(t *testing.T) {
DEFAULT = "default"
)

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

serviceRef := func(tenancy, name string) *pbresource.Reference {
return newRefWithTenancy(pbcatalog.ServiceType, tenancy, name)
}

resOneDest := func(tenancy, destTenancy string) []*pbresource.Resource {
return newFailover(t, "api", tenancy, []*pbresource.Reference{
serviceRef(destTenancy, "dest1"),
})
}

resTwoDests := func(tenancy, destTenancy string) []*pbresource.Resource {
return newFailover(t, "api", tenancy, []*pbresource.Reference{
serviceRef(destTenancy, "dest1"),
serviceRef(destTenancy, "dest2"),
})
}

reg, ok := registry.Resolve(pbcatalog.FailoverPolicyType)
require.True(t, ok)

run := func(t *testing.T, tc testcase) {
failoverData := &pbcatalog.FailoverPolicy{
Config: &pbcatalog.FailoverConfig{
Destinations: []*pbcatalog.FailoverDestination{
{Ref: newRef(pbcatalog.ServiceType, "api-backup")},
},
},
}
res := resourcetest.Resource(pbcatalog.FailoverPolicyType, "api").
WithTenancy(resource.DefaultNamespacedTenancy()).
WithData(t, failoverData).
Build()
resourcetest.ValidateAndNormalize(t, registry, res)
run := func(t *testing.T, name string, tc testcase) {
t.Run(name, func(t *testing.T) {
config := acl.Config{
WildcardName: structs.WildcardSpecifier,
}
authz, err := acl.NewAuthorizerFromRules(tc.rules, &config, nil)
require.NoError(t, err)
authz = acl.NewChainedAuthorizer([]acl.Authorizer{authz, acl.DenyAll()})

config := acl.Config{
WildcardName: structs.WildcardSpecifier,
}
authz, err := acl.NewAuthorizerFromRules(tc.rules, &config, nil)
require.NoError(t, err)
authz = acl.NewChainedAuthorizer([]acl.Authorizer{authz, acl.DenyAll()})
authCtx := resource.AuthorizerContext(tc.res.Id.Tenancy)

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)
checkF(t, "read", tc.readOK, reg.ACLs.Read(authz, authCtx, tc.res.Id, nil))
checkF(t, "write", tc.writeOK, reg.ACLs.Write(authz, authCtx, tc.res))
checkF(t, "list", DEFAULT, reg.ACLs.List(authz, authCtx))
})
}

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: DENY,
listOK: DEFAULT,
},
"service api write and api-backup read": {
rules: `service "api" { policy = "write" } service "api-backup" { policy = "read" }`,
readOK: ALLOW,
writeOK: ALLOW,
listOK: DEFAULT,
},
isEnterprise := (structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty() == "default")

serviceRead := func(partition, namespace, name string) string {
if isEnterprise {
return fmt.Sprintf(` partition %q { namespace %q { service %q { policy = "read" } } }`, partition, namespace, name)
}
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)
}
return fmt.Sprintf(` service %q { policy = "write" } `, name)
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
run(t, tc)
assert := func(t *testing.T, name string, rules string, resList []*pbresource.Resource, readOK, writeOK string) {
for i, res := range resList {
tc := testcase{
res: res,
rules: rules,
readOK: readOK,
writeOK: writeOK,
}
run(t, fmt.Sprintf("%s-%d", name, i), tc)
}
}

tenancies := []string{"default.default"}
if isEnterprise {
tenancies = append(tenancies, "default.foo", "alpha.default", "alpha.foo")
}

for _, policyTenancyStr := range tenancies {
t.Run("policy tenancy: "+policyTenancyStr, func(t *testing.T) {
for _, destTenancyStr := range tenancies {
t.Run("dest tenancy: "+destTenancyStr, 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, parentOnly bool) string {
if policyTenancyStr != aclTenancyStr {
return DENY
}
if !parentOnly && destTenancyStr != aclTenancyStr {
return DENY
}
return match
}

t.Run("no rules", func(t *testing.T) {
rules := ``
assert(t, "1dest", rules, resOneDest(policyTenancyStr, destTenancyStr), DENY, DENY)
assert(t, "2dests", rules, resTwoDests(policyTenancyStr, destTenancyStr), DENY, DENY)
})
t.Run("api:read", func(t *testing.T) {
rules := serviceRead(aclTenancy.Partition, aclTenancy.Namespace, "api")
assert(t, "1dest", rules, resOneDest(policyTenancyStr, destTenancyStr), maybe(ALLOW, true), DENY)
assert(t, "2dests", rules, resTwoDests(policyTenancyStr, destTenancyStr), maybe(ALLOW, true), DENY)
})
t.Run("api:write", func(t *testing.T) {
rules := serviceWrite(aclTenancy.Partition, aclTenancy.Namespace, "api")
assert(t, "1dest", rules, resOneDest(policyTenancyStr, destTenancyStr), maybe(ALLOW, true), DENY)
assert(t, "2dests", rules, resTwoDests(policyTenancyStr, destTenancyStr), maybe(ALLOW, true), DENY)
})
t.Run("api:write dest1:read", func(t *testing.T) {
rules := serviceWrite(aclTenancy.Partition, aclTenancy.Namespace, "api") +
serviceRead(aclTenancy.Partition, aclTenancy.Namespace, "dest1")
assert(t, "1dest", rules, resOneDest(policyTenancyStr, destTenancyStr), maybe(ALLOW, true), maybe(ALLOW, false))
assert(t, "2dests", rules, resTwoDests(policyTenancyStr, destTenancyStr), maybe(ALLOW, true), DENY)
})
})
}
})
}
})
}
}
Expand All @@ -796,7 +867,7 @@ func newRef(typ *pbresource.Type, name string) *pbresource.Reference {

func newRefWithTenancy(typ *pbresource.Type, tenancyStr, name string) *pbresource.Reference {
return resourcetest.Resource(typ, name).
WithTenancy(newTestTenancy(tenancyStr)).
WithTenancy(resourcetest.Tenancy(tenancyStr)).
Reference("")
}

Expand All @@ -805,22 +876,3 @@ func newRefWithPeer(typ *pbresource.Type, name string, peer string) *pbresource.
ref.Tenancy.PeerName = peer
return ref
}

func newTestTenancy(s string) *pbresource.Tenancy {
parts := strings.Split(s, ".")
switch len(parts) {
case 0:
return resource.DefaultClusteredTenancy()
case 1:
v := resource.DefaultPartitionedTenancy()
v.Partition = parts[0]
return v
case 2:
v := resource.DefaultNamespacedTenancy()
v.Partition = parts[0]
v.Namespace = parts[1]
return v
default:
return &pbresource.Tenancy{Partition: "BAD", Namespace: "BAD", PeerName: "BAD"}
}
}

0 comments on commit e67f2d1

Please sign in to comment.