Browse Source

Handle NamespaceTrafficPermissions when reconciling TrafficPermissions (#20407)

pull/20408/head
Chris S. Kim 10 months ago committed by GitHub
parent
commit
7cc88a1577
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      agent/consul/testdata/v2-resource-dependencies.md
  2. 17
      internal/auth/internal/controllers/trafficpermissions/builder.go
  3. 59
      internal/auth/internal/controllers/trafficpermissions/controller.go
  4. 199
      internal/auth/internal/controllers/trafficpermissions/controller_test.go
  5. 15
      internal/auth/internal/controllers/trafficpermissions/expander/expander_ce/expander_ce.go
  6. 6
      internal/auth/internal/controllers/trafficpermissions/expander/interface.go
  7. 68
      internal/auth/internal/types/namespace_traffic_permissions.go
  8. 709
      internal/auth/internal/types/namespace_traffic_permissions_test.go
  9. 18
      internal/auth/internal/types/traffic_permissions.go
  10. 4
      internal/auth/internal/types/traffic_permissions_validator_ce.go
  11. 13
      internal/auth/internal/types/types.go
  12. 15
      proto-public/pbauth/v2beta1/traffic_permissions_helper.go
  13. 50
      proto-public/pbauth/v2beta1/traffic_permissions_helper_test.go

1
agent/consul/testdata/v2-resource-dependencies.md vendored

@ -1,5 +1,6 @@
```mermaid
flowchart TD
auth/v2beta1/computedtrafficpermissions --> auth/v2beta1/namespacetrafficpermissions
auth/v2beta1/computedtrafficpermissions --> auth/v2beta1/trafficpermissions
auth/v2beta1/computedtrafficpermissions --> auth/v2beta1/workloadidentity
catalog/v2beta1/computedfailoverpolicy --> catalog/v2beta1/failoverpolicy

17
internal/auth/internal/controllers/trafficpermissions/builder.go

@ -7,6 +7,7 @@ import (
"sort"
"github.com/hashicorp/consul/internal/auth/internal/controllers/trafficpermissions/expander"
"github.com/hashicorp/consul/internal/auth/internal/types"
"github.com/hashicorp/consul/internal/resource"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
pbmulticluster "github.com/hashicorp/consul/proto-public/pbmulticluster/v2beta1"
@ -38,23 +39,23 @@ func newTrafficPermissionsBuilder(expander expander.SamenessGroupExpander, sgMap
}
}
// track will use all associated Traffic Permissions to create new ComputedTrafficPermissions samenessGroupsForTrafficPermission
func (tpb *trafficPermissionsBuilder) track(dec *resource.DecodedResource[*pbauth.TrafficPermissions]) {
missingSamenessGroups := tpb.sgExpander.Expand(dec.Data, tpb.sgMap)
// track will use all associated XTrafficPermissions to create new ComputedTrafficPermissions samenessGroupsForTrafficPermission
func track[S types.XTrafficPermissions](tpb *trafficPermissionsBuilder, xtp *resource.DecodedResource[S]) {
missingSamenessGroups := tpb.sgExpander.Expand(xtp.Data, tpb.sgMap)
if len(missingSamenessGroups) > 0 {
tpb.missing[resource.NewReferenceKey(dec.Id)] = missingSamenessGroupReferences{
resource: dec.Resource,
tpb.missing[resource.NewReferenceKey(xtp.Id)] = missingSamenessGroupReferences{
resource: xtp.Resource,
samenessGroups: missingSamenessGroups,
}
}
tpb.isDefault = false
if dec.Data.Action == pbauth.Action_ACTION_ALLOW {
tpb.allowedPermissions = append(tpb.allowedPermissions, dec.Data.Permissions...)
if xtp.Data.GetAction() == pbauth.Action_ACTION_ALLOW {
tpb.allowedPermissions = append(tpb.allowedPermissions, xtp.Data.GetPermissions()...)
} else {
tpb.denyPermissions = append(tpb.denyPermissions, dec.Data.Permissions...)
tpb.denyPermissions = append(tpb.denyPermissions, xtp.Data.GetPermissions()...)
}
}

59
internal/auth/internal/controllers/trafficpermissions/controller.go

@ -10,13 +10,21 @@ import (
"google.golang.org/protobuf/types/known/anypb"
"github.com/hashicorp/consul/internal/auth/internal/controllers/trafficpermissions/expander"
"github.com/hashicorp/consul/internal/auth/internal/types"
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/controller/cache"
"github.com/hashicorp/consul/internal/controller/cache/index"
"github.com/hashicorp/consul/internal/controller/cache/indexers"
"github.com/hashicorp/consul/internal/controller/dependency"
"github.com/hashicorp/consul/internal/resource"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
)
const (
TenancyIndexName = "tenancy"
)
// TrafficPermissionsMapper is used to map a watch event for a TrafficPermissions resource and translate
// it to a ComputedTrafficPermissions resource which contains the effective permissions
// from all referencing TrafficPermissions resources.
@ -44,9 +52,38 @@ func Controller(mapper TrafficPermissionsMapper, sgExpander expander.SamenessGro
samenessGroupIndex := GetSamenessGroupIndex()
// 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) {
iter, err := rt.Cache.ListIterator(pbauth.ComputedTrafficPermissionsType, "id", &pbresource.Reference{
Type: pbauth.ComputedTrafficPermissionsType,
Tenancy: res.Id.Tenancy,
}, 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
}
ctrl := controller.NewController(StatusKey, pbauth.ComputedTrafficPermissionsType).
WithWatch(pbauth.WorkloadIdentityType, dependency.ReplaceType(pbauth.ComputedTrafficPermissionsType)).
WithWatch(pbauth.TrafficPermissionsType, mapper.MapTrafficPermissions, samenessGroupIndex).
WithWatch(pbauth.NamespaceTrafficPermissionsType, ntpToCtpMapper,
indexers.DecodedSingleIndexer(
TenancyIndexName,
index.SingleValueFromArgs(func(t *pbresource.Tenancy) ([]byte, error) {
return index.IndexFromTenancy(t), nil
}),
func(r *types.DecodedNamespaceTrafficPermissions) (bool, []byte, error) {
return true, index.IndexFromTenancy(r.Id.Tenancy), nil
},
)).
WithReconciler(&reconciler{mapper: mapper, sgExpander: sgExpander})
return registerEnterpriseControllerWatchers(ctrl)
@ -133,16 +170,28 @@ func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req c
r.mapper.UntrackTrafficPermissions(resource.IDFromReference(t))
continue
}
trafficPermissionBuilder.track(rsp)
track(trafficPermissionBuilder, rsp)
tpResources = append(tpResources, rsp.Resource)
}
latestComputedTrafficPermissions, missing := trafficPermissionBuilder.build()
// Fetch namespace traffic permissions for ctp(workload identity)'s tenancy
ntps, err := cache.ListDecoded[*pbauth.NamespaceTrafficPermissions](
rt.Cache,
pbauth.NamespaceTrafficPermissionsType,
TenancyIndexName,
ctpID.Tenancy,
)
if err != nil {
rt.Logger.Error("error expanding sameness groups", err.Error())
rt.Logger.Error("error reading namespaced traffic permissions resource for computation", "error", err)
writeFailedStatus(ctx, rt, oldResource, nil, err.Error())
return err
}
for _, ntp := range ntps {
track(trafficPermissionBuilder, ntp)
tpResources = append(tpResources, ntp.Resource)
}
latestComputedTrafficPermissions, missing := trafficPermissionBuilder.build()
newCTPResource := oldResource
@ -206,7 +255,7 @@ func writeMissingSgStatuses(ctx context.Context, rt controller.Runtime, req cont
if err != nil {
return err
}
//writing status to traffic permissions
// writing status to traffic permissions
for _, sgRefs := range missing {
if len(sgRefs.samenessGroups) == 0 {
err := writeStatusWithConditions(ctx, rt, sgRefs.resource,

199
internal/auth/internal/controllers/trafficpermissions/controller_test.go

@ -30,35 +30,42 @@ import (
type controllerSuite struct {
suite.Suite
ctx context.Context
ctl *controller.TestController
client *rtest.Client
rt controller.Runtime
mapper *trafficpermissionsmapper.TrafficPermissionsMapper
sgExpander expander.SamenessGroupExpander
reconciler *reconciler
reconciler controller.Reconciler
tenancies []*pbresource.Tenancy
isEnterprise bool
// bazTenancy is used to test that resources in baz/baz do not affect
// computed resources in the standard resourcetest.TestTenancies.
bazTenancy *pbresource.Tenancy
}
func (suite *controllerSuite) SetupTest() {
// note that we don't append bazTenancy to suite.tenancies since
// we don't want it to be used as a testcase.
suite.bazTenancy = &pbresource.Tenancy{Partition: "baz", Namespace: "baz"}
suite.isEnterprise = versiontest.IsEnterprise()
suite.tenancies = resourcetest.TestTenancies()
suite.ctx = testutil.TestContext(suite.T())
client := svctest.NewResourceServiceBuilder().
WithRegisterFns(types.Register, multicluster.RegisterTypes).
WithTenancies(suite.tenancies...).
WithTenancies(append(suite.tenancies, suite.bazTenancy)...).
Run(suite.T())
suite.client = rtest.NewClient(client)
suite.rt = controller.Runtime{
Client: suite.client,
Logger: testutil.Logger(suite.T()),
}
// TODO: a lot of the fields below should be consolidated to controller only
suite.mapper = trafficpermissionsmapper.New()
suite.sgExpander = expander.GetSamenessGroupExpander()
suite.reconciler = &reconciler{mapper: suite.mapper, sgExpander: suite.sgExpander}
suite.ctl = controller.NewTestController(
Controller(suite.mapper, suite.sgExpander),
client,
).WithLogger(testutil.Logger(suite.T()))
suite.reconciler = suite.ctl.Reconciler()
suite.client = resourcetest.NewClient(suite.ctl.Runtime().Client)
suite.rt = suite.ctl.Runtime()
}
func (suite *controllerSuite) requireTrafficPermissionsTracking(tp *pbresource.Resource, ids ...*pbresource.ID) {
@ -83,7 +90,28 @@ func (suite *controllerSuite) requireCTP(resource *pbresource.Resource, allowExp
}
func (suite *controllerSuite) TestReconcile_CTPCreate_NoReferencingTrafficPermissionsExist() {
suite.tenancies = resourcetest.TestTenancies()
suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
// Write an NTP in another tenancy
_ = rtest.Resource(pbauth.NamespaceTrafficPermissionsType, "ntp1").
WithData(suite.T(), &pbauth.NamespaceTrafficPermissions{
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
IdentityName: "bar",
Namespace: "default",
Partition: "default",
Peer: resource.DefaultPeerName,
},
},
},
},
}).
WithTenancy(suite.bazTenancy).
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()
@ -94,68 +122,103 @@ func (suite *controllerSuite) TestReconcile_CTPCreate_NoReferencingTrafficPermis
// Ensure that the CTP was created
ctp := suite.client.RequireResourceExists(suite.T(), id)
// NTP from baz should not be included
suite.requireCTP(ctp, []*pbauth.Permission{}, []*pbauth.Permission{})
})
}
func (suite *controllerSuite) TestReconcile_CTPCreate_ReferencingTrafficPermissionsExist() {
// create dead-end traffic permissions
suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
p1 := &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{p1},
}).
WithTenancy(tenancy).
Write(suite.T(), suite.client)
wi1ID := &pbresource.ID{
Name: "wi1",
Type: pbauth.ComputedTrafficPermissionsType,
Tenancy: tp1.Id.Tenancy,
}
suite.requireTrafficPermissionsTracking(tp1, wi1ID)
p2 := &pbauth.Permission{
Sources: []*pbauth.Source{
{
IdentityName: "wi2",
Namespace: "default",
Partition: "default",
Peer: resource.DefaultPeerName,
}},
}
tp2 := rtest.Resource(pbauth.TrafficPermissionsType, "tp2").WithData(suite.T(), &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: "wi1",
},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{p2},
}).
WithTenancy(tenancy).
Write(suite.T(), suite.client)
suite.requireTrafficPermissionsTracking(tp2, wi1ID)
// create the workload identity that they reference
wi := rtest.Resource(pbauth.WorkloadIdentityType, "wi1").WithTenancy(tenancy).Write(suite.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(suite.T(), err)
// Ensure that the CTP was created
ctp := suite.client.RequireResourceExists(suite.T(), id)
suite.requireCTP(ctp, []*pbauth.Permission{p1, p2}, []*pbauth.Permission{})
rtest.RequireOwner(suite.T(), ctp, wi.Id, true)
suite.Run("trafperms 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)
// 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, nsPerm1}, []*pbauth.Permission{})
rtest.RequireOwner(t, ctp, wi.Id, true)
})
})
suite.Run("only namespace trafperms exist", func() {
suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
t := suite.T()
// Write allow namespace trafperms
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)
// 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{nsPerm1}, []*pbauth.Permission{})
rtest.RequireOwner(t, ctp, wi.Id, true)
})
})
}

15
internal/auth/internal/controllers/trafficpermissions/expander/expander_ce/expander_ce.go

@ -6,26 +6,31 @@ package expander_ce
import (
"context"
pbmulticluster "github.com/hashicorp/consul/proto-public/pbmulticluster/v2beta1"
"github.com/hashicorp/consul/internal/auth/internal/types"
"github.com/hashicorp/consul/internal/controller"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
pbmulticluster "github.com/hashicorp/consul/proto-public/pbmulticluster/v2beta1"
)
type XTrafficPermissions interface {
GetAction() pbauth.Action
GetPermissions() []*pbauth.Permission
}
type SamenessGroupExpander struct{}
func New() *SamenessGroupExpander {
return &SamenessGroupExpander{}
}
func (sgE *SamenessGroupExpander) Expand(_ *pbauth.TrafficPermissions,
func (sgE *SamenessGroupExpander) Expand(_ types.XTrafficPermissions,
_ map[string][]*pbmulticluster.SamenessGroupMember) []string {
//no-op for CE
// no-op for CE
return nil
}
func (sgE *SamenessGroupExpander) List(_ context.Context, _ controller.Runtime,
_ controller.Request) (map[string][]*pbmulticluster.SamenessGroupMember, error) {
//no-op for CE
// no-op for CE
return nil, nil
}

6
internal/auth/internal/controllers/trafficpermissions/expander/interface.go

@ -6,13 +6,13 @@ package expander
import (
"context"
"github.com/hashicorp/consul/internal/auth/internal/types"
"github.com/hashicorp/consul/internal/controller"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
pbmulticluster "github.com/hashicorp/consul/proto-public/pbmulticluster/v2beta1"
)
// SamenessgroupExpander is used to expand sameness group for a ComputedTrafficPermission resource
// SamenessGroupExpander is used to expand sameness group for a ComputedTrafficPermission resource
type SamenessGroupExpander interface {
Expand(*pbauth.TrafficPermissions, map[string][]*pbmulticluster.SamenessGroupMember) []string
Expand(types.XTrafficPermissions, map[string][]*pbmulticluster.SamenessGroupMember) []string
List(context.Context, controller.Runtime, controller.Request) (map[string][]*pbmulticluster.SamenessGroupMember, error)
}

68
internal/auth/internal/types/namespace_traffic_permissions.go

@ -0,0 +1,68 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package types
import (
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/internal/resource"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
)
type DecodedNamespaceTrafficPermissions = resource.DecodedResource[*pbauth.NamespaceTrafficPermissions]
func RegisterNamespaceTrafficPermissions(r resource.Registry) {
r.Register(resource.Registration{
Type: pbauth.NamespaceTrafficPermissionsType,
Proto: &pbauth.NamespaceTrafficPermissions{},
ACLs: &resource.ACLHooks{
Read: resource.DecodeAndAuthorizeRead(aclReadHookNamespaceTrafficPermissions),
Write: resource.DecodeAndAuthorizeWrite(aclWriteHookNamespaceTrafficPermissions),
List: resource.NoOpACLListHook,
},
Validate: ValidateNamespaceTrafficPermissions,
Mutate: MutateNamespaceTrafficPermissions,
Scope: resource.ScopeNamespace,
})
}
func aclReadHookNamespaceTrafficPermissions(authorizer acl.Authorizer, authzContext *acl.AuthorizerContext, res *DecodedNamespaceTrafficPermissions) error {
return authorizer.ToAllowAuthorizer().MeshReadAllowed(authzContext)
}
func aclWriteHookNamespaceTrafficPermissions(authorizer acl.Authorizer, authzContext *acl.AuthorizerContext, res *DecodedNamespaceTrafficPermissions) error {
return authorizer.ToAllowAuthorizer().MeshWriteAllowed(authzContext)
}
var ValidateNamespaceTrafficPermissions = resource.DecodeAndValidate(validateNamespaceTrafficPermissions)
func validateNamespaceTrafficPermissions(res *DecodedNamespaceTrafficPermissions) 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 MutateNamespaceTrafficPermissions = resource.DecodeAndMutate(mutateNamespaceTrafficPermissions)
func mutateNamespaceTrafficPermissions(res *DecodedNamespaceTrafficPermissions) (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
}

709
internal/auth/internal/types/namespace_traffic_permissions_test.go

@ -0,0 +1,709 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package types
import (
"testing"
"github.com/stretchr/testify/require"
"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"
)
func TestValidateNamespaceTrafficPermissions_ParseError(t *testing.T) {
data := &pbauth.ComputedTrafficPermissions{AllowPermissions: nil}
res := resourcetest.Resource(pbauth.NamespaceTrafficPermissionsType, "tp").
WithData(t, data).
Build()
err := ValidateNamespaceTrafficPermissions(res)
require.Error(t, err)
require.ErrorAs(t, err, &resource.ErrDataParse{})
}
// todo: this test is copy-pasted from traffic permissions tests.
// would be nice to refator this to keep them in sync.
func TestValidateNamespaceTrafficPermissions(t *testing.T) {
cases := map[string]struct {
id *pbresource.ID
ntp *pbauth.NamespaceTrafficPermissions
expectErr string
}{
"ok-minimal": {
ntp: &pbauth.NamespaceTrafficPermissions{
Action: pbauth.Action_ACTION_ALLOW,
},
},
"unspecified-action": {
// Any type other than the TrafficPermissions type would work
// to cause the error we are expecting
ntp: &pbauth.NamespaceTrafficPermissions{
Action: pbauth.Action_ACTION_UNSPECIFIED,
},
expectErr: `invalid "data.action" field`,
},
"invalid-action": {
ntp: &pbauth.NamespaceTrafficPermissions{
Action: pbauth.Action(50),
},
expectErr: `invalid "data.action" field`,
},
"source-tenancy": {
ntp: &pbauth.NamespaceTrafficPermissions{
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,
},
},
ntp: &pbauth.NamespaceTrafficPermissions{
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,
},
},
ntp: &pbauth.NamespaceTrafficPermissions{
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
Partition: "part",
Peer: resource.DefaultPeerName,
SamenessGroup: "",
},
},
},
},
},
},
"source-has-peer-set": {
id: &pbresource.ID{
Tenancy: &pbresource.Tenancy{
Partition: resource.DefaultPartitionName,
},
},
ntp: &pbauth.NamespaceTrafficPermissions{
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,
},
},
ntp: &pbauth.NamespaceTrafficPermissions{
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,
},
},
ntp: &pbauth.NamespaceTrafficPermissions{
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,
},
},
ntp: &pbauth.NamespaceTrafficPermissions{
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,
},
},
ntp: &pbauth.NamespaceTrafficPermissions{
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.NamespaceTrafficPermissionsType, "ntp").
WithData(t, tc.ntp)
if tc.id != nil {
resBuilder = resBuilder.WithTenancy(tc.id.Tenancy)
}
res := resBuilder.Build()
err := ValidateNamespaceTrafficPermissions(res)
if tc.expectErr == "" {
require.NoError(t, err)
} else {
testutil.RequireErrorContains(t, err, tc.expectErr)
}
})
}
}
func TestValidateNamespaceTrafficPermissions_Permissions(t *testing.T) {
for n, tc := range permissionsTestCases() {
t.Run(n, func(t *testing.T) {
tp := &pbauth.NamespaceTrafficPermissions{
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{tc.p},
}
res := resourcetest.Resource(pbauth.NamespaceTrafficPermissionsType, "tp").
WithTenancy(resource.DefaultNamespacedTenancy()).
WithData(t, tp).
Build()
err := MutateNamespaceTrafficPermissions(res)
require.NoError(t, err)
err = ValidateNamespaceTrafficPermissions(res)
if tc.expectErr == "" {
require.NoError(t, err)
} else {
testutil.RequireErrorContains(t, err, tc.expectErr)
}
})
}
}
func TestMutateNamespaceTrafficPermissions(t *testing.T) {
type testcase struct {
policyTenancy *pbresource.Tenancy
tp *pbauth.NamespaceTrafficPermissions
expect *pbauth.NamespaceTrafficPermissions
}
run := func(t *testing.T, tc testcase) {
tenancy := tc.policyTenancy
if tenancy == nil {
tenancy = resource.DefaultNamespacedTenancy()
}
res := resourcetest.Resource(pbauth.NamespaceTrafficPermissionsType, "ntp").
WithTenancy(tenancy).
WithData(t, tc.tp).
Build()
err := MutateNamespaceTrafficPermissions(res)
got := resourcetest.MustDecode[*pbauth.NamespaceTrafficPermissions](t, res)
require.NoError(t, err)
prototest.AssertDeepEqual(t, tc.expect, got.Data)
}
cases := map[string]testcase{
"empty-1": {
tp: &pbauth.NamespaceTrafficPermissions{},
expect: &pbauth.NamespaceTrafficPermissions{},
},
"kitchen-sink-default-partition": {
tp: &pbauth.NamespaceTrafficPermissions{
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.NamespaceTrafficPermissions{
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": {
tp: &pbauth.NamespaceTrafficPermissions{
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.NamespaceTrafficPermissions{
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",
},
tp: &pbauth.NamespaceTrafficPermissions{
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.NamespaceTrafficPermissions{
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",
},
tp: &pbauth.NamespaceTrafficPermissions{
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.NamespaceTrafficPermissions{
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 TestNamespaceTrafficPermissionsACLs(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.NamespaceTrafficPermissionsType)
require.True(t, ok)
run := func(t *testing.T, tc testcase) {
tpData := &pbauth.NamespaceTrafficPermissions{
Action: pbauth.Action_ACTION_ALLOW,
}
res := resourcetest.Resource(pbauth.NamespaceTrafficPermissionsType, "ntp1").
WithTenancy(resource.DefaultNamespacedTenancy()).
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,
},
"namespace 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)
})
}
}

18
internal/auth/internal/types/traffic_permissions.go

@ -115,13 +115,13 @@ var ValidateTrafficPermissions = resource.DecodeAndValidate(validateTrafficPermi
// validator takes a traffic permission and ensures that it conforms to the actions allowed in
// either CE or Enterprise versions of Consul
type validator interface {
ValidateAction(res *DecodedTrafficPermissions) error
ValidateAction(data interface{ GetAction() pbauth.Action }) error
}
func validateTrafficPermissions(res *DecodedTrafficPermissions) error {
var merr error
err := v.ValidateAction(res)
err := v.ValidateAction(res.Data)
if err != nil {
merr = multierror.Append(merr, err)
}
@ -133,7 +133,16 @@ func validateTrafficPermissions(res *DecodedTrafficPermissions) error {
})
}
// Validate permissions
for i, permission := range res.Data.Permissions {
if err := validatePermissions(res.Id, res.Data); err != nil {
merr = multierror.Append(merr, err)
}
return merr
}
func validatePermissions(id *pbresource.ID, data interface{ GetPermissions() []*pbauth.Permission }) error {
var merr error
for i, permission := range data.GetPermissions() {
wrapErr := func(err error) error {
return resource.ErrInvalidListElement{
Name: "permissions",
@ -141,11 +150,10 @@ func validateTrafficPermissions(res *DecodedTrafficPermissions) error {
Wrapped: err,
}
}
if err := validatePermission(permission, res.Id, wrapErr); err != nil {
if err := validatePermission(permission, id, wrapErr); err != nil {
merr = multierror.Append(merr, err)
}
}
return merr
}

4
internal/auth/internal/types/traffic_permissions_validator_ce.go

@ -16,9 +16,9 @@ var v validator = &actionValidator{}
type actionValidator struct{}
func (v *actionValidator) ValidateAction(res *DecodedTrafficPermissions) error {
func (v *actionValidator) ValidateAction(data interface{ GetAction() pbauth.Action }) error {
// enumcover:pbauth.Action
switch res.Data.Action {
switch data.GetAction() {
case pbauth.Action_ACTION_ALLOW:
case pbauth.Action_ACTION_UNSPECIFIED:
fallthrough

13
internal/auth/internal/types/types.go

@ -4,11 +4,24 @@
package types
import (
"google.golang.org/protobuf/proto"
"github.com/hashicorp/consul/internal/resource"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
)
// XTrafficPermissions is an interface to allow generic handling of
// TrafficPermissions, NamespaceTrafficPermissions, and PartitionTrafficPermissions.
type XTrafficPermissions interface {
proto.Message
GetAction() pbauth.Action
GetPermissions() []*pbauth.Permission
}
func Register(r resource.Registry) {
RegisterWorkloadIdentity(r)
RegisterTrafficPermissions(r)
RegisterComputedTrafficPermission(r)
RegisterNamespaceTrafficPermissions(r)
}

15
proto-public/pbauth/v2beta1/traffic_permissions_helper.go

@ -1,15 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package authv2beta1
func (ctp *TrafficPermissions) HasReferencedSamenessGroups() bool {
for _, dp := range ctp.Permissions {
for _, source := range dp.Sources {
if source.SamenessGroup != "" {
return true
}
}
}
return false
}

50
proto-public/pbauth/v2beta1/traffic_permissions_helper_test.go

@ -1,50 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package authv2beta1
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestHasReferencedSamenessGroups(t *testing.T) {
type testCase struct {
tp *TrafficPermissions
expected bool
}
testCases := []*testCase{
{
tp: &TrafficPermissions{
Permissions: []*Permission{
{
Sources: []*Source{
{
SamenessGroup: "sg1",
},
},
},
},
},
expected: true,
},
{
tp: &TrafficPermissions{
Permissions: []*Permission{
{
Sources: []*Source{
{
Peer: "peer",
},
},
},
},
},
expected: false,
},
}
for _, tc := range testCases {
require.Equal(t, tc.tp.HasReferencedSamenessGroups(), tc.expected)
}
}
Loading…
Cancel
Save