Skip to content

Commit

Permalink
mesh: add xRoute ACL hook tenancy tests (#19177)
Browse files Browse the repository at this point in the history
Enhance the xRoute ACL hook tests to cover tenanted situations.
These tests will only execute in enterprise.
  • Loading branch information
rboyer authored Oct 16, 2023
1 parent 3716b69 commit 6c7d075
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 128 deletions.
245 changes: 130 additions & 115 deletions internal/mesh/internal/types/xroute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/wrapperspb"

"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 @@ -114,6 +113,7 @@ func getXRouteParentRefTestCases() map[string]xRouteParentRefTestcase {
Port: port,
}
}

return map[string]xRouteParentRefTestcase{
"no parent refs": {
routeTenancy: resource.DefaultNamespacedTenancy(),
Expand Down Expand Up @@ -372,145 +372,160 @@ func testXRouteACLs[R XRouteData](t *testing.T, newRoute func(t *testing.T, pare

userNewRoute := newRoute
newRoute = func(t *testing.T, parentRefs, backendRefs []*pbresource.Reference) *pbresource.Resource {
require.NotEmpty(t, parentRefs)
require.NotEmpty(t, backendRefs)
res := userNewRoute(t, parentRefs, backendRefs)
res.Id.Tenancy = parentRefs[0].Tenancy
resourcetest.ValidateAndNormalize(t, registry, res)
return res
}

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

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

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

resOneParentOneBackend := newRoute(t,
[]*pbresource.Reference{
newRef(pbcatalog.ServiceType, "api1"),
},
[]*pbresource.Reference{
newRef(pbcatalog.ServiceType, "backend1"),
},
)
resTwoParentsOneBackend := newRoute(t,
[]*pbresource.Reference{
newRef(pbcatalog.ServiceType, "api1"),
newRef(pbcatalog.ServiceType, "api2"),
},
[]*pbresource.Reference{
newRef(pbcatalog.ServiceType, "backend1"),
},
)
resOneParentTwoBackends := newRoute(t,
[]*pbresource.Reference{
newRef(pbcatalog.ServiceType, "api1"),
},
[]*pbresource.Reference{
newRef(pbcatalog.ServiceType, "backend1"),
newRef(pbcatalog.ServiceType, "backend2"),
},
)
resTwoParentsTwoBackends := newRoute(t,
[]*pbresource.Reference{
newRef(pbcatalog.ServiceType, "api1"),
newRef(pbcatalog.ServiceType, "api2"),
},
[]*pbresource.Reference{
newRef(pbcatalog.ServiceType, "backend1"),
newRef(pbcatalog.ServiceType, "backend2"),
},
)
resOneParentOneBackend := func(parentTenancy, backendTenancy string) *pbresource.Resource {
return newRoute(t,
[]*pbresource.Reference{
serviceRef(parentTenancy, "api1"),
},
[]*pbresource.Reference{
serviceRef(backendTenancy, "backend1"),
},
)
}
resTwoParentsOneBackend := func(parentTenancy, backendTenancy string) *pbresource.Resource {
return newRoute(t,
[]*pbresource.Reference{
serviceRef(parentTenancy, "api1"),
serviceRef(parentTenancy, "api2"),
},
[]*pbresource.Reference{
serviceRef(backendTenancy, "backend1"),
},
)
}
resOneParentTwoBackends := func(parentTenancy, backendTenancy string) *pbresource.Resource {
return newRoute(t,
[]*pbresource.Reference{
serviceRef(parentTenancy, "api1"),
},
[]*pbresource.Reference{
serviceRef(backendTenancy, "backend1"),
serviceRef(backendTenancy, "backend2"),
},
)
}
resTwoParentsTwoBackends := func(parentTenancy, backendTenancy string) *pbresource.Resource {
return newRoute(t,
[]*pbresource.Reference{
serviceRef(parentTenancy, "api1"),
serviceRef(parentTenancy, "api2"),
},
[]*pbresource.Reference{
serviceRef(backendTenancy, "backend1"),
serviceRef(backendTenancy, "backend2"),
},
)
}

run := func(t *testing.T, name string, tc testcase) {
run := func(t *testing.T, name string, tc resourcetest.ACLTestCase) {
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()})

reg, ok := registry.Resolve(tc.res.Id.GetType())
require.True(t, ok)

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

checkF(t, "read", tc.readOK, reg.ACLs.Read(authz, &acl.AuthorizerContext{}, tc.res.Id, tc.res))
checkF(t, "write", tc.writeOK, reg.ACLs.Write(authz, &acl.AuthorizerContext{}, tc.res))
checkF(t, "list", DEFAULT, reg.ACLs.List(authz, &acl.AuthorizerContext{}))
resourcetest.RunACLTestCase(t, tc, registry)
})
}

serviceRead := func(name string) string {
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(name string) string {
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)
}

assert := func(t *testing.T, name string, rules string, res *pbresource.Resource, readOK, writeOK string) {
tc := testcase{
res: res,
rules: rules,
readOK: readOK,
writeOK: writeOK,
tc := resourcetest.ACLTestCase{
Rules: rules,
Res: res,
ReadOK: readOK,
WriteOK: writeOK,
ListOK: DEFAULT,
ReadHookRequiresResource: true,
}
run(t, name, tc)
}

t.Run("no rules", func(t *testing.T) {
rules := ``
assert(t, "1parent 1backend", rules, resOneParentOneBackend, DENY, DENY)
assert(t, "1parent 2backends", rules, resOneParentTwoBackends, DENY, DENY)
assert(t, "2parents 1backend", rules, resTwoParentsOneBackend, DENY, DENY)
assert(t, "2parents 2backends", rules, resTwoParentsTwoBackends, DENY, DENY)
})
t.Run("api1:read", func(t *testing.T) {
rules := serviceRead("api1")
assert(t, "1parent 1backend", rules, resOneParentOneBackend, ALLOW, DENY)
assert(t, "1parent 2backends", rules, resOneParentTwoBackends, ALLOW, DENY)
assert(t, "2parents 1backend", rules, resTwoParentsOneBackend, DENY, DENY)
assert(t, "2parents 2backends", rules, resTwoParentsTwoBackends, DENY, DENY)
})
t.Run("api1:write", func(t *testing.T) {
rules := serviceWrite("api1")
assert(t, "1parent 1backend", rules, resOneParentOneBackend, ALLOW, DENY)
assert(t, "1parent 2backends", rules, resOneParentTwoBackends, ALLOW, DENY)
assert(t, "2parents 1backend", rules, resTwoParentsOneBackend, DENY, DENY)
assert(t, "2parents 2backends", rules, resTwoParentsTwoBackends, DENY, DENY)
})
t.Run("api1:write backend1:read", func(t *testing.T) {
rules := serviceWrite("api1") + serviceRead("backend1")
assert(t, "1parent 1backend", rules, resOneParentOneBackend, ALLOW, ALLOW)
assert(t, "1parent 2backends", rules, resOneParentTwoBackends, ALLOW, DENY)
assert(t, "2parents 1backend", rules, resTwoParentsOneBackend, DENY, DENY)
assert(t, "2parents 2backends", rules, resTwoParentsTwoBackends, DENY, DENY)
})
tenancies := []string{"default.default"}
if isEnterprise {
tenancies = append(tenancies, "default.foo", "alpha.default", "alpha.foo")
}

for _, parentTenancyStr := range tenancies {
t.Run("route tenancy: "+parentTenancyStr, func(t *testing.T) {
for _, backendTenancyStr := range tenancies {
t.Run("backend tenancy: "+backendTenancyStr, 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 parentTenancyStr != aclTenancyStr {
return DENY
}
if !parentOnly && backendTenancyStr != aclTenancyStr {
return DENY
}
return match
}

t.Run("no rules", func(t *testing.T) {
rules := ``
assert(t, "1parent 1backend", rules, resOneParentOneBackend(parentTenancyStr, backendTenancyStr), DENY, DENY)
assert(t, "1parent 2backends", rules, resOneParentTwoBackends(parentTenancyStr, backendTenancyStr), DENY, DENY)
assert(t, "2parents 1backend", rules, resTwoParentsOneBackend(parentTenancyStr, backendTenancyStr), DENY, DENY)
assert(t, "2parents 2backends", rules, resTwoParentsTwoBackends(parentTenancyStr, backendTenancyStr), DENY, DENY)
})
t.Run("api1:read", func(t *testing.T) {
rules := serviceRead(aclTenancy.Partition, aclTenancy.Namespace, "api1")
assert(t, "1parent 1backend", rules, resOneParentOneBackend(parentTenancyStr, backendTenancyStr), maybe(ALLOW, true), DENY)
assert(t, "1parent 2backends", rules, resOneParentTwoBackends(parentTenancyStr, backendTenancyStr), maybe(ALLOW, true), DENY)
assert(t, "2parents 1backend", rules, resTwoParentsOneBackend(parentTenancyStr, backendTenancyStr), DENY, DENY)
assert(t, "2parents 2backends", rules, resTwoParentsTwoBackends(parentTenancyStr, backendTenancyStr), DENY, DENY)
})
t.Run("api1:write", func(t *testing.T) {
rules := serviceWrite(aclTenancy.Partition, aclTenancy.Namespace, "api1")
assert(t, "1parent 1backend", rules, resOneParentOneBackend(parentTenancyStr, backendTenancyStr), maybe(ALLOW, true), DENY)
assert(t, "1parent 2backends", rules, resOneParentTwoBackends(parentTenancyStr, backendTenancyStr), maybe(ALLOW, true), DENY)
assert(t, "2parents 1backend", rules, resTwoParentsOneBackend(parentTenancyStr, backendTenancyStr), DENY, DENY)
assert(t, "2parents 2backends", rules, resTwoParentsTwoBackends(parentTenancyStr, backendTenancyStr), DENY, DENY)
})
t.Run("api1:write backend1:read", func(t *testing.T) {
rules := serviceWrite(aclTenancy.Partition, aclTenancy.Namespace, "api1") +
serviceRead(aclTenancy.Partition, aclTenancy.Namespace, "backend1")
assert(t, "1parent 1backend", rules, resOneParentOneBackend(parentTenancyStr, backendTenancyStr), maybe(ALLOW, true), maybe(ALLOW, false))
assert(t, "1parent 2backends", rules, resOneParentTwoBackends(parentTenancyStr, backendTenancyStr), maybe(ALLOW, true), DENY)
assert(t, "2parents 1backend", rules, resTwoParentsOneBackend(parentTenancyStr, backendTenancyStr), DENY, DENY)
assert(t, "2parents 2backends", rules, resTwoParentsTwoBackends(parentTenancyStr, backendTenancyStr), DENY, DENY)
})
})
}
})
}
})
}
}

func newRef(typ *pbresource.Type, name string) *pbresource.Reference {
Expand Down
53 changes: 40 additions & 13 deletions internal/resource/resourcetest/acls.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,49 @@ var checkF = func(t *testing.T, expect string, got error) {
}

type ACLTestCase struct {
Rules string
Data protoreflect.ProtoMessage
Owner *pbresource.ID
Typ *pbresource.Type
Rules string

// One of either Res or Data/Owner/Typ should be set.
Res *pbresource.Resource
Data protoreflect.ProtoMessage
Owner *pbresource.ID
Typ *pbresource.Type

ReadOK string
WriteOK string
ListOK string

ReadHookRequiresResource bool
}

func RunACLTestCase(t *testing.T, tc ACLTestCase, registry resource.Registry) {
reg, ok := registry.Resolve(tc.Typ)
require.True(t, ok)
var (
typ *pbresource.Type
res *pbresource.Resource
)
if tc.Res != nil {
require.Nil(t, tc.Data)
require.Nil(t, tc.Owner)
require.Nil(t, tc.Typ)
typ = tc.Res.Id.GetType()
res = tc.Res
} else {
require.NotNil(t, tc.Data)
require.NotNil(t, tc.Typ)
typ = tc.Typ

resolvedType, ok := registry.Resolve(tc.Typ)
require.True(t, ok)
resolvedType, ok := registry.Resolve(typ)
require.True(t, ok)

res := Resource(tc.Typ, "test").
WithTenancy(DefaultTenancyForType(t, resolvedType)).
WithOwner(tc.Owner).
WithData(t, tc.Data).
Build()
res = Resource(tc.Typ, "test").
WithTenancy(DefaultTenancyForType(t, resolvedType)).
WithOwner(tc.Owner).
WithData(t, tc.Data).
Build()
}

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

ValidateAndNormalize(t, registry, res)

Expand All @@ -70,6 +92,11 @@ func RunACLTestCase(t *testing.T, tc ACLTestCase, registry resource.Registry) {
require.NoError(t, err)
authz = acl.NewChainedAuthorizer([]acl.Authorizer{authz, acl.DenyAll()})

if tc.ReadHookRequiresResource {
err = reg.ACLs.Read(authz, &acl.AuthorizerContext{}, 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)
checkF(t, tc.ReadOK, err)
Expand Down

0 comments on commit 6c7d075

Please sign in to comment.