From 8799c364109822852b38936227643851a976abb4 Mon Sep 17 00:00:00 2001 From: Ronald Date: Tue, 30 Jan 2024 17:14:32 -0500 Subject: [PATCH] [NET-6231] Handle Partition traffic permissions when reconciling traffic permissions (#20408) [NET-6231] Partition traffic permissions Co-authored-by: Chris S. Kim --- .../testdata/v2-resource-dependencies.md | 1 + .../trafficpermissions/controller.go | 47 ++ .../trafficpermissions/controller_test.go | 188 +++++ .../types/partition_traffic_permissions.go | 67 ++ .../partition_traffic_permissions_test.go | 705 ++++++++++++++++++ internal/auth/internal/types/types.go | 1 + 6 files changed, 1009 insertions(+) create mode 100644 internal/auth/internal/types/partition_traffic_permissions.go create mode 100644 internal/auth/internal/types/partition_traffic_permissions_test.go diff --git a/agent/consul/testdata/v2-resource-dependencies.md b/agent/consul/testdata/v2-resource-dependencies.md index fd4af46e13..8707c5e213 100644 --- a/agent/consul/testdata/v2-resource-dependencies.md +++ b/agent/consul/testdata/v2-resource-dependencies.md @@ -1,6 +1,7 @@ ```mermaid flowchart TD auth/v2beta1/computedtrafficpermissions --> auth/v2beta1/namespacetrafficpermissions + auth/v2beta1/computedtrafficpermissions --> auth/v2beta1/partitiontrafficpermissions auth/v2beta1/computedtrafficpermissions --> auth/v2beta1/trafficpermissions auth/v2beta1/computedtrafficpermissions --> auth/v2beta1/workloadidentity catalog/v2beta1/computedfailoverpolicy --> catalog/v2beta1/failoverpolicy diff --git a/internal/auth/internal/controllers/trafficpermissions/controller.go b/internal/auth/internal/controllers/trafficpermissions/controller.go index 7ab1c26ce4..23662612d1 100644 --- a/internal/auth/internal/controllers/trafficpermissions/controller.go +++ b/internal/auth/internal/controllers/trafficpermissions/controller.go @@ -52,6 +52,26 @@ func Controller(mapper TrafficPermissionsMapper, sgExpander expander.SamenessGro samenessGroupIndex := GetSamenessGroupIndex() + // Maps incoming PartitionTrafficPermissions to ComputedTrafficPermissions requests by prefix searching + // the CTP's tenancy. + ptpToCtpMapper := func(ctx context.Context, rt controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) { + iter, err := rt.Cache.ListIterator(pbauth.ComputedTrafficPermissionsType, "id", &pbresource.Reference{ + Type: pbauth.ComputedTrafficPermissionsType, + Tenancy: &pbresource.Tenancy{ + Partition: res.Id.Tenancy.GetPartition(), + }, + }, index.IndexQueryOptions{Prefix: true}) + if err != nil { + return nil, err + } + + var reqs []controller.Request + for res := iter.Next(); res != nil; res = iter.Next() { + reqs = append(reqs, controller.Request{ID: res.Id}) + } + + return reqs, nil + } // Maps incoming NamespaceTrafficPermissions to ComputedTrafficPermissions requests by prefix searching // the CTP's tenancy. ntpToCtpMapper := func(ctx context.Context, rt controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) { @@ -74,6 +94,16 @@ func Controller(mapper TrafficPermissionsMapper, sgExpander expander.SamenessGro ctrl := controller.NewController(StatusKey, pbauth.ComputedTrafficPermissionsType). WithWatch(pbauth.WorkloadIdentityType, dependency.ReplaceType(pbauth.ComputedTrafficPermissionsType)). WithWatch(pbauth.TrafficPermissionsType, mapper.MapTrafficPermissions, samenessGroupIndex). + WithWatch(pbauth.PartitionTrafficPermissionsType, ptpToCtpMapper, + indexers.DecodedSingleIndexer( + TenancyIndexName, + index.SingleValueFromArgs(func(t *pbresource.Tenancy) ([]byte, error) { + return index.IndexFromTenancy(t), nil + }), + func(r *types.DecodedPartitionTrafficPermissions) (bool, []byte, error) { + return true, index.IndexFromTenancy(r.Id.Tenancy), nil + }, + )). WithWatch(pbauth.NamespaceTrafficPermissionsType, ntpToCtpMapper, indexers.DecodedSingleIndexer( TenancyIndexName, @@ -174,6 +204,23 @@ func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req c tpResources = append(tpResources, rsp.Resource) } + // Fetch partition traffic permissions for ctp(workload identity)'s tenancy + ptps, err := cache.ListDecoded[*pbauth.PartitionTrafficPermissions]( + rt.Cache, + pbauth.PartitionTrafficPermissionsType, + TenancyIndexName, + &pbresource.Tenancy{Partition: ctpID.Tenancy.GetPartition()}, + ) + if err != nil { + rt.Logger.Error("error reading partitioned traffic permissions resource for computation", "error", err) + writeFailedStatus(ctx, rt, oldResource, nil, err.Error()) + return err + } + for _, ptp := range ptps { + track(trafficPermissionBuilder, ptp) + tpResources = append(tpResources, ptp.Resource) + } + // Fetch namespace traffic permissions for ctp(workload identity)'s tenancy ntps, err := cache.ListDecoded[*pbauth.NamespaceTrafficPermissions]( rt.Cache, diff --git a/internal/auth/internal/controllers/trafficpermissions/controller_test.go b/internal/auth/internal/controllers/trafficpermissions/controller_test.go index f08ea1d50b..08a29c30f7 100644 --- a/internal/auth/internal/controllers/trafficpermissions/controller_test.go +++ b/internal/auth/internal/controllers/trafficpermissions/controller_test.go @@ -112,6 +112,26 @@ func (suite *controllerSuite) TestReconcile_CTPCreate_NoReferencingTrafficPermis WithTenancy(suite.bazTenancy). Write(suite.T(), suite.client) + // Write a Partition Traffic Permission in another tenancy + _ = rtest.Resource(pbauth.PartitionTrafficPermissionsType, "ptp1"). + WithData(suite.T(), &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + IdentityName: "foo", + Namespace: "default", + Partition: "default", + Peer: resource.DefaultPeerName, + }, + }, + }, + }, + }). + WithTenancy(&pbresource.Tenancy{Partition: suite.bazTenancy.GetPartition()}). + Write(suite.T(), suite.client) + wi := rtest.Resource(pbauth.WorkloadIdentityType, "wi1").WithTenancy(tenancy).Write(suite.T(), suite.client) require.NotNil(suite.T(), wi) id := rtest.Resource(pbauth.ComputedTrafficPermissionsType, wi.Id.Name).WithTenancy(tenancy).WithOwner(wi.Id).ID() @@ -220,6 +240,174 @@ func (suite *controllerSuite) TestReconcile_CTPCreate_ReferencingTrafficPermissi rtest.RequireOwner(t, ctp, wi.Id, true) }) }) + suite.Run("only partition trafperms exist", func() { + suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) { + t := suite.T() + // Write allow partition trafperms + ptPerm1 := &pbauth.Permission{ + Sources: []*pbauth.Source{ + { + IdentityName: "bar", + Namespace: "default", + Partition: "default", + Peer: resource.DefaultPeerName, + }}, + } + _ = rtest.Resource(pbauth.PartitionTrafficPermissionsType, "ptp1"). + WithData(t, &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{ptPerm1}, + }). + WithTenancy(&pbresource.Tenancy{Partition: tenancy.GetPartition()}). + Write(t, suite.client) + + // create the workload identity that they reference + wi := rtest.Resource(pbauth.WorkloadIdentityType, "wi1").WithTenancy(tenancy).Write(t, suite.client) + id := rtest.Resource(pbauth.ComputedTrafficPermissionsType, wi.Id.Name).WithTenancy(tenancy).WithOwner(wi.Id).ID() + + require.NoError(t, suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id})) + + // Ensure that the CTP was created + ctp := suite.client.RequireResourceExists(t, id) + suite.requireCTP(ctp, []*pbauth.Permission{ptPerm1}, []*pbauth.Permission{}) + rtest.RequireOwner(t, ctp, wi.Id, true) + }) + }) + suite.Run("trafperms and partition trafperms exist", func() { + suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) { + t := suite.T() + // Workload identity ID == CTP ID + wi1ID := &pbresource.ID{ + Name: "wi1", + Type: pbauth.ComputedTrafficPermissionsType, + Tenancy: tenancy, + } + perm1 := &pbauth.Permission{ + Sources: []*pbauth.Source{ + { + IdentityName: "foo", + Namespace: "default", + Partition: "default", + Peer: resource.DefaultPeerName, + }}, + } + tp1 := rtest.Resource(pbauth.TrafficPermissionsType, "tp1").WithData(suite.T(), &pbauth.TrafficPermissions{ + Destination: &pbauth.Destination{ + IdentityName: "wi1", + }, + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{perm1}, + }). + WithTenancy(tenancy). + Write(t, suite.client) + + suite.requireTrafficPermissionsTracking(tp1, wi1ID) + ptPerm1 := &pbauth.Permission{ + Sources: []*pbauth.Source{ + { + IdentityName: "bar", + Namespace: "default", + Partition: "default", + Peer: resource.DefaultPeerName, + }}, + } + _ = rtest.Resource(pbauth.PartitionTrafficPermissionsType, "ptp1"). + WithData(t, &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{ptPerm1}, + }). + WithTenancy(&pbresource.Tenancy{Partition: tenancy.GetPartition()}). + Write(t, suite.client) + + // create the workload identity that they reference + wi := rtest.Resource(pbauth.WorkloadIdentityType, "wi1").WithTenancy(tenancy).Write(t, suite.client) + id := rtest.Resource(pbauth.ComputedTrafficPermissionsType, wi.Id.Name).WithTenancy(tenancy).WithOwner(wi.Id).ID() + + err := suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id}) + require.NoError(t, err) + + // Ensure that the CTP was created + ctp := suite.client.RequireResourceExists(suite.T(), id) + suite.requireCTP(ctp, []*pbauth.Permission{perm1, ptPerm1}, []*pbauth.Permission{}) + rtest.RequireOwner(t, ctp, wi.Id, true) + }) + }) + suite.Run("trafperms, partition and namespace trafperms exist", func() { + suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) { + t := suite.T() + // Workload identity ID == CTP ID + wi1ID := &pbresource.ID{ + Name: "wi1", + Type: pbauth.ComputedTrafficPermissionsType, + Tenancy: tenancy, + } + perm1 := &pbauth.Permission{ + Sources: []*pbauth.Source{ + { + IdentityName: "foo", + Namespace: "default", + Partition: "default", + Peer: resource.DefaultPeerName, + }}, + } + tp1 := rtest.Resource(pbauth.TrafficPermissionsType, "tp1").WithData(suite.T(), &pbauth.TrafficPermissions{ + Destination: &pbauth.Destination{ + IdentityName: "wi1", + }, + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{perm1}, + }). + WithTenancy(tenancy). + Write(t, suite.client) + + suite.requireTrafficPermissionsTracking(tp1, wi1ID) + nsPerm1 := &pbauth.Permission{ + Sources: []*pbauth.Source{ + { + IdentityName: "bar", + Namespace: "default", + Partition: "default", + Peer: resource.DefaultPeerName, + }}, + } + _ = rtest.Resource(pbauth.NamespaceTrafficPermissionsType, "ntp1"). + WithData(t, &pbauth.NamespaceTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{nsPerm1}, + }). + WithTenancy(tenancy). + Write(t, suite.client) + + ptPerm1 := &pbauth.Permission{ + Sources: []*pbauth.Source{ + { + IdentityName: "boo", + Namespace: "default", + Partition: "default", + Peer: resource.DefaultPeerName, + }}, + } + _ = rtest.Resource(pbauth.PartitionTrafficPermissionsType, "ptp1"). + WithData(t, &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{ptPerm1}, + }). + WithTenancy(&pbresource.Tenancy{Partition: tenancy.GetPartition()}). + Write(t, suite.client) + + // create the workload identity that they reference + wi := rtest.Resource(pbauth.WorkloadIdentityType, "wi1").WithTenancy(tenancy).Write(t, suite.client) + id := rtest.Resource(pbauth.ComputedTrafficPermissionsType, wi.Id.Name).WithTenancy(tenancy).WithOwner(wi.Id).ID() + + err := suite.reconciler.Reconcile(suite.ctx, suite.rt, controller.Request{ID: id}) + require.NoError(t, err) + + // Ensure that the CTP was created + ctp := suite.client.RequireResourceExists(suite.T(), id) + suite.requireCTP(ctp, []*pbauth.Permission{perm1, ptPerm1, nsPerm1}, []*pbauth.Permission{}) + rtest.RequireOwner(t, ctp, wi.Id, true) + }) + }) } func (suite *controllerSuite) TestReconcile_WorkloadIdentityDelete_ReferencingTrafficPermissionsExist() { diff --git a/internal/auth/internal/types/partition_traffic_permissions.go b/internal/auth/internal/types/partition_traffic_permissions.go new file mode 100644 index 0000000000..d9cd1cb6e7 --- /dev/null +++ b/internal/auth/internal/types/partition_traffic_permissions.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package types + +import ( + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/internal/resource" + pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1" + "github.com/hashicorp/go-multierror" +) + +type DecodedPartitionTrafficPermissions = resource.DecodedResource[*pbauth.PartitionTrafficPermissions] + +func RegisterPartitionTrafficPermissions(r resource.Registry) { + r.Register(resource.Registration{ + Type: pbauth.PartitionTrafficPermissionsType, + Proto: &pbauth.PartitionTrafficPermissions{}, + ACLs: &resource.ACLHooks{ + Read: resource.DecodeAndAuthorizeRead(aclReadHookPartitionTrafficPermissions), + Write: resource.DecodeAndAuthorizeWrite(aclWriteHookPartitionTrafficPermissions), + List: resource.NoOpACLListHook, + }, + Validate: ValidatePartitionTrafficPermissions, + Mutate: MutatePartitionTrafficPermissions, + Scope: resource.ScopePartition, + }) +} + +func aclReadHookPartitionTrafficPermissions(authorizer acl.Authorizer, authzContext *acl.AuthorizerContext, res *DecodedPartitionTrafficPermissions) error { + return authorizer.ToAllowAuthorizer().MeshReadAllowed(authzContext) +} + +func aclWriteHookPartitionTrafficPermissions(authorizer acl.Authorizer, authzContext *acl.AuthorizerContext, res *DecodedPartitionTrafficPermissions) error { + return authorizer.ToAllowAuthorizer().MeshWriteAllowed(authzContext) +} + +var ValidatePartitionTrafficPermissions = resource.DecodeAndValidate(validatePartitionTrafficPermissions) + +func validatePartitionTrafficPermissions(res *DecodedPartitionTrafficPermissions) error { + var merr error + + if err := v.ValidateAction(res.Data); err != nil { + merr = multierror.Append(merr, err) + } + if err := validatePermissions(res.Id, res.Data); err != nil { + merr = multierror.Append(merr, err) + } + + return merr +} + +var MutatePartitionTrafficPermissions = resource.DecodeAndMutate(mutatePartitionTrafficPermissions) + +func mutatePartitionTrafficPermissions(res *DecodedPartitionTrafficPermissions) (bool, error) { + var changed bool + + for _, p := range res.Data.Permissions { + for _, s := range p.Sources { + if updated := normalizedTenancyForSource(s, res.Id.Tenancy); updated { + changed = true + } + } + } + + return changed, nil +} diff --git a/internal/auth/internal/types/partition_traffic_permissions_test.go b/internal/auth/internal/types/partition_traffic_permissions_test.go new file mode 100644 index 0000000000..eb38024745 --- /dev/null +++ b/internal/auth/internal/types/partition_traffic_permissions_test.go @@ -0,0 +1,705 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package types + +import ( + "testing" + + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/internal/resource" + "github.com/hashicorp/consul/internal/resource/resourcetest" + pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1" + "github.com/hashicorp/consul/proto-public/pbresource" + "github.com/hashicorp/consul/proto/private/prototest" + "github.com/hashicorp/consul/sdk/testutil" + "github.com/stretchr/testify/require" +) + +func TestValidatePartitionTrafficPermissions_ParseError(t *testing.T) { + data := &pbauth.ComputedTrafficPermissions{AllowPermissions: nil} + + res := resourcetest.Resource(pbauth.PartitionTrafficPermissionsType, "ptp"). + WithData(t, data). + Build() + + err := ValidatePartitionTrafficPermissions(res) + require.Error(t, err) + require.ErrorAs(t, err, &resource.ErrDataParse{}) +} + +func TestValidatePartitionTrafficPermissions(t *testing.T) { + // TODO: refactor test cases as these are similar to namespace traffic permissions + cases := map[string]struct { + id *pbresource.ID + ptp *pbauth.PartitionTrafficPermissions + expectErr string + }{ + "ok-minimal": { + ptp: &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + }, + }, + "unspecified-action": { + ptp: &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_UNSPECIFIED, + }, + expectErr: `invalid "data.action" field`, + }, + "invalid-action": { + ptp: &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action(50), + }, + expectErr: `invalid "data.action" field`, + }, + "source-tenancy": { + ptp: &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Partition: "ap1", + Peer: "cl1", + SamenessGroup: "sg1", + }, + }, + }, + }, + }, + expectErr: `invalid element at index 0 of list "permissions": invalid element at index 0 of list "sources": invalid element at index 0 of list "source": permissions sources may not specify partitions, peers, and sameness_groups together`, + }, + "source-has-same-tenancy-as-tp": { + id: &pbresource.ID{ + Tenancy: &pbresource.Tenancy{ + Partition: resource.DefaultPartitionName, + }, + }, + ptp: &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Partition: resource.DefaultPartitionName, + Peer: resource.DefaultPeerName, + SamenessGroup: "", + }, + }, + }, + }, + }, + }, + "source-has-partition-set": { + id: &pbresource.ID{ + Tenancy: &pbresource.Tenancy{ + Partition: resource.DefaultPartitionName, + }, + }, + ptp: &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Partition: "pt1", + Peer: resource.DefaultPeerName, + SamenessGroup: "", + }, + }, + }, + }, + }, + }, + "source-has-peer-set": { + id: &pbresource.ID{ + Tenancy: &pbresource.Tenancy{ + Partition: resource.DefaultPartitionName, + }, + }, + ptp: &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Partition: resource.DefaultPartitionName, + Peer: "peer", + SamenessGroup: "", + }, + }, + }, + }, + }, + }, + "source-has-sameness-group-set": { + id: &pbresource.ID{ + Tenancy: &pbresource.Tenancy{ + Partition: resource.DefaultPartitionName, + }, + }, + ptp: &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Partition: resource.DefaultPartitionName, + Peer: resource.DefaultPeerName, + SamenessGroup: "sg1", + }, + }, + }, + }, + }, + }, + "source-has-peer-and-partition-set": { + id: &pbresource.ID{ + Tenancy: &pbresource.Tenancy{ + Partition: resource.DefaultPartitionName, + }, + }, + ptp: &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Partition: "part", + Peer: "peer", + SamenessGroup: "", + }, + }, + }, + }, + }, + expectErr: `invalid element at index 0 of list "permissions": invalid element at index 0 of list "sources": invalid element at index 0 of list "source": permissions sources may not specify partitions, peers, and sameness_groups together`, + }, + "source-has-sameness-group-and-partition-set": { + id: &pbresource.ID{ + Tenancy: &pbresource.Tenancy{ + Partition: resource.DefaultPartitionName, + }, + }, + ptp: &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Partition: "part", + Peer: resource.DefaultPeerName, + SamenessGroup: "sg1", + }, + }, + }, + }, + }, + expectErr: `invalid element at index 0 of list "permissions": invalid element at index 0 of list "sources": invalid element at index 0 of list "source": permissions sources may not specify partitions, peers, and sameness_groups together`, + }, + "source-has-sameness-group-and-partition-peer-set": { + id: &pbresource.ID{ + Tenancy: &pbresource.Tenancy{ + Partition: resource.DefaultPartitionName, + }, + }, + ptp: &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Partition: "part", + Peer: "peer", + SamenessGroup: "sg1", + }, + }, + }, + }, + }, + expectErr: `invalid element at index 0 of list "permissions": invalid element at index 0 of list "sources": invalid element at index 0 of list "source": permissions sources may not specify partitions, peers, and sameness_groups together`, + }, + } + + for n, tc := range cases { + t.Run(n, func(t *testing.T) { + resBuilder := resourcetest.Resource(pbauth.PartitionTrafficPermissionsType, "ptp"). + WithData(t, tc.ptp) + if tc.id != nil { + resBuilder = resBuilder.WithTenancy(tc.id.Tenancy) + } + res := resBuilder.Build() + + err := ValidatePartitionTrafficPermissions(res) + if tc.expectErr == "" { + require.NoError(t, err) + } else { + testutil.RequireErrorContains(t, err, tc.expectErr) + } + }) + } +} + +func TestValidatePartitionTrafficPermissions_Permissions(t *testing.T) { + for n, tc := range permissionsTestCases() { + t.Run(n, func(t *testing.T) { + tp := &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + Permissions: []*pbauth.Permission{tc.p}, + } + + res := resourcetest.Resource(pbauth.PartitionTrafficPermissionsType, "ptp"). + WithTenancy(resource.DefaultPartitionedTenancy()). + WithData(t, tp). + Build() + + err := MutatePartitionTrafficPermissions(res) + require.NoError(t, err) + + err = ValidatePartitionTrafficPermissions(res) + if tc.expectErr == "" { + require.NoError(t, err) + } else { + testutil.RequireErrorContains(t, err, tc.expectErr) + } + }) + } +} + +func TestMutatePartitionTrafficPermissions(t *testing.T) { + type testcase struct { + policyTenancy *pbresource.Tenancy + ptp *pbauth.PartitionTrafficPermissions + expect *pbauth.PartitionTrafficPermissions + } + + run := func(t *testing.T, tc testcase) { + tenancy := tc.policyTenancy + if tenancy == nil { + tenancy = resource.DefaultPartitionedTenancy() + } + res := resourcetest.Resource(pbauth.PartitionTrafficPermissionsType, "ptp"). + WithTenancy(tenancy). + WithData(t, tc.ptp). + Build() + + err := MutatePartitionTrafficPermissions(res) + + got := resourcetest.MustDecode[*pbauth.PartitionTrafficPermissions](t, res) + require.NoError(t, err) + prototest.AssertDeepEqual(t, tc.expect, got.Data) + } + + cases := map[string]testcase{ + "empty-1": { + ptp: &pbauth.PartitionTrafficPermissions{}, + expect: &pbauth.PartitionTrafficPermissions{}, + }, + "kitchen-sink-default-partition": { + ptp: &pbauth.PartitionTrafficPermissions{ + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + {}, + { + Peer: "not-default", + }, + { + Namespace: "ns1", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Partition: "ap1", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Peer: "local", + }, + }, + }, + }, + }, + expect: &pbauth.PartitionTrafficPermissions{ + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Partition: "default", + Peer: "local", + }, + { + Peer: "not-default", + }, + { + Namespace: "ns1", + Partition: "default", + Peer: "local", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Partition: "ap1", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Partition: "default", + Peer: "local", + }, + }, + }, + }, + }, + }, + "kitchen-sink-excludes-default-partition": { + ptp: &pbauth.PartitionTrafficPermissions{ + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Exclude: []*pbauth.ExcludeSource{ + {}, + { + Peer: "not-default", + }, + { + Namespace: "ns1", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Partition: "ap1", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Peer: "local", + }, + }, + }, + }, + }, + }, + }, + expect: &pbauth.PartitionTrafficPermissions{ + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Partition: "default", + Peer: "local", + Exclude: []*pbauth.ExcludeSource{ + { + Partition: "default", + Peer: "local", + }, + { + Peer: "not-default", + }, + { + Namespace: "ns1", + Partition: "default", + Peer: "local", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Partition: "ap1", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Partition: "default", + Peer: "local", + }, + }, + }, + }, + }, + }, + }, + }, + "kitchen-sink-non-default-partition": { + policyTenancy: &pbresource.Tenancy{ + Partition: "ap1", + Namespace: "ns3", + }, + ptp: &pbauth.PartitionTrafficPermissions{ + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + {}, + { + Peer: "not-default", + }, + { + Namespace: "ns1", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Partition: "ap5", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Peer: "local", + }, + { + IdentityName: "i2", + }, + { + IdentityName: "i2", + Partition: "non-default", + }, + }, + }, + }, + }, + expect: &pbauth.PartitionTrafficPermissions{ + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Partition: "ap1", + Namespace: "", + Peer: "local", + }, + { + Peer: "not-default", + }, + { + Namespace: "ns1", + Partition: "ap1", + Peer: "local", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Partition: "ap5", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Partition: "ap1", + Peer: "local", + }, + { + IdentityName: "i2", + Namespace: "ns3", + Partition: "ap1", + Peer: "local", + }, + { + IdentityName: "i2", + Namespace: "default", + Partition: "non-default", + Peer: "local", + }, + }, + }, + }, + }, + }, + "kitchen-sink-excludes-non-default-partition": { + policyTenancy: &pbresource.Tenancy{ + Partition: "ap1", + Namespace: "ns3", + }, + ptp: &pbauth.PartitionTrafficPermissions{ + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Exclude: []*pbauth.ExcludeSource{ + {}, + { + Peer: "not-default", + }, + { + Namespace: "ns1", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Partition: "ap5", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Peer: "local", + }, + { + IdentityName: "i2", + }, + }, + }, + }, + }, + }, + }, + expect: &pbauth.PartitionTrafficPermissions{ + Permissions: []*pbauth.Permission{ + { + Sources: []*pbauth.Source{ + { + Partition: "ap1", + Peer: "local", + Exclude: []*pbauth.ExcludeSource{ + { + Partition: "ap1", + Namespace: "", + Peer: "local", + }, + { + Peer: "not-default", + }, + { + Namespace: "ns1", + Partition: "ap1", + Peer: "local", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Partition: "ap5", + }, + { + IdentityName: "i1", + Namespace: "ns1", + Partition: "ap1", + Peer: "local", + }, + { + IdentityName: "i2", + Namespace: "ns3", + Partition: "ap1", + Peer: "local", + }, + }, + }, + }, + }, + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + run(t, tc) + }) + } +} + +func TestPartitionTrafficPermissionsACLs(t *testing.T) { + // Wire up a registry to generically invoke hooks + registry := resource.NewRegistry() + Register(registry) + + type testcase struct { + rules string + readOK string + writeOK string + listOK string + } + + const ( + DENY = "deny" + ALLOW = "allow" + DEFAULT = "default" + ) + + checkF := func(t *testing.T, expect string, got error) { + 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(pbauth.PartitionTrafficPermissionsType) + require.True(t, ok) + + run := func(t *testing.T, tc testcase) { + tpData := &pbauth.PartitionTrafficPermissions{ + Action: pbauth.Action_ACTION_ALLOW, + } + res := resourcetest.Resource(pbauth.PartitionTrafficPermissionsType, "ptp1"). + WithTenancy(resource.DefaultPartitionedTenancy()). + WithData(t, tpData). + 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, res) + 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, + }, + "operator read": { + rules: `operator = "read"`, + readOK: ALLOW, + writeOK: DENY, + listOK: DEFAULT, + }, + "operator write": { + rules: `operator = "write"`, + readOK: ALLOW, + writeOK: ALLOW, + listOK: DEFAULT, + }, + "mesh read": { + rules: `mesh = "read"`, + readOK: ALLOW, + writeOK: DENY, + listOK: DEFAULT, + }, + "partition write": { + rules: `mesh = "write"`, + readOK: ALLOW, + writeOK: ALLOW, + listOK: DEFAULT, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + run(t, tc) + }) + } +} diff --git a/internal/auth/internal/types/types.go b/internal/auth/internal/types/types.go index 809bdbe4fd..cb8ba3b063 100644 --- a/internal/auth/internal/types/types.go +++ b/internal/auth/internal/types/types.go @@ -24,4 +24,5 @@ func Register(r resource.Registry) { RegisterTrafficPermissions(r) RegisterComputedTrafficPermission(r) RegisterNamespaceTrafficPermissions(r) + RegisterPartitionTrafficPermissions(r) }