Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mesh: add xRoute ACL hook tenancy tests #19177

Merged
merged 2 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if you are running in enterprise but in the default partition? In that case, we won't be testing the partitioned policy for the default partition it seems

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This specific call to PartitionOrEmpty() returns the following:

  • CE: ""
  • ENT: "default"

So as long as the build was produced using -tags consulent then isEnterprise will always be true.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can switch it to version.VersionMetadata == "ent" if that's any clearer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok! My question was more about if both builds return the same value, but in this case that's not a problem.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll replace this hack with #19201 to make it more obvious what's going on.


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