mesh: add xRoute ACL hook tenancy tests (#19177)

Enhance the xRoute ACL hook tests to cover tenanted situations.
These tests will only execute in enterprise.
This commit is contained in:
R.B. Boyer 2023-10-16 12:18:56 -05:00 committed by GitHub
parent 3716b69792
commit 6c7d0759e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 170 additions and 128 deletions

View File

@ -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"
@ -114,6 +113,7 @@ func getXRouteParentRefTestCases() map[string]xRouteParentRefTestcase {
Port: port,
}
}
return map[string]xRouteParentRefTestcase{
"no parent refs": {
routeTenancy: resource.DefaultNamespacedTenancy(),
@ -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 {

View File

@ -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)
@ -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)