mesh: add DestinationPolicy ACL hook tenancy tests (#19178)

Enhance the DestinationPolicy 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 13:44:24 -05:00 committed by GitHub
parent ad177698f7
commit df8ea430c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 92 additions and 92 deletions

View File

@ -4,13 +4,13 @@
package types package types
import ( import (
"fmt"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/durationpb"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/resourcetest" "github.com/hashicorp/consul/internal/resource/resourcetest"
@ -518,99 +518,92 @@ func TestDestinationPolicyACLs(t *testing.T) {
registry := resource.NewRegistry() registry := resource.NewRegistry()
Register(registry) Register(registry)
type testcase struct { newPolicy := func(t *testing.T, tenancyStr string) *pbresource.Resource {
rules string res := resourcetest.Resource(pbmesh.DestinationPolicyType, "api").
check func(t *testing.T, authz acl.Authorizer, res *pbresource.Resource) WithTenancy(resourcetest.Tenancy(tenancyStr)).
readOK string WithData(t, &pbmesh.DestinationPolicy{
writeOK string PortConfigs: map[string]*pbmesh.DestinationConfig{
listOK string "http": {
ConnectTimeout: durationpb.New(55 * time.Second),
},
},
}).
Build()
resourcetest.ValidateAndNormalize(t, registry, res)
return res
} }
const ( const (
DENY = "deny" DENY = resourcetest.DENY
ALLOW = "allow" ALLOW = resourcetest.ALLOW
DEFAULT = "default" DEFAULT = resourcetest.DEFAULT
) )
checkF := func(t *testing.T, expect string, got error) { run := func(t *testing.T, name string, tc resourcetest.ACLTestCase) {
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)
}
}
reg, ok := registry.Resolve(pbmesh.DestinationPolicyType)
require.True(t, ok)
run := func(t *testing.T, tc testcase) {
destData := &pbmesh.DestinationPolicy{
PortConfigs: map[string]*pbmesh.DestinationConfig{
"http": {
ConnectTimeout: durationpb.New(55 * time.Second),
},
},
}
res := resourcetest.Resource(pbmesh.DestinationPolicyType, "api").
WithTenancy(resource.DefaultNamespacedTenancy()).
WithData(t, destData).
Build()
resourcetest.ValidateAndNormalize(t, registry, res)
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()})
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)
})
}
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,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
run(t, tc) resourcetest.RunACLTestCase(t, tc, registry)
})
}
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)
}
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)
}
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 _, 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))
})
})
}
}) })
} }
} }

View File

@ -41,6 +41,9 @@ var checkF = func(t *testing.T, expect string, got error) {
type ACLTestCase struct { type ACLTestCase struct {
Rules string 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. // One of either Res or Data/Owner/Typ should be set.
Res *pbresource.Resource Res *pbresource.Resource
Data protoreflect.ProtoMessage Data protoreflect.ProtoMessage
@ -92,21 +95,25 @@ func RunACLTestCase(t *testing.T, tc ACLTestCase, registry resource.Registry) {
require.NoError(t, err) require.NoError(t, err)
authz = acl.NewChainedAuthorizer([]acl.Authorizer{authz, acl.DenyAll()}) authz = acl.NewChainedAuthorizer([]acl.Authorizer{authz, acl.DenyAll()})
if tc.AuthCtx == nil {
tc.AuthCtx = &acl.AuthorizerContext{}
}
if tc.ReadHookRequiresResource { 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") require.ErrorIs(t, err, resource.ErrNeedResource, "read hook should require the data payload")
} }
t.Run("read", func(t *testing.T) { 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) checkF(t, tc.ReadOK, err)
}) })
t.Run("write", func(t *testing.T) { 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) checkF(t, tc.WriteOK, err)
}) })
t.Run("list", func(t *testing.T) { 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) checkF(t, tc.ListOK, err)
}) })
} }