Net 6774 Make Sameness Groups Work With Traffic Permissions CE (#20316)

* Make Sameness Groups Work With Traffic Permissions

* Fix controller dependency
This commit is contained in:
Tauhid Anjum 2024-01-23 13:23:03 +05:30 committed by GitHub
parent 5d294b26d3
commit b37fe80eee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 647 additions and 121 deletions

View File

@ -5,6 +5,7 @@ package controllers
import ( import (
"github.com/hashicorp/consul/internal/auth/internal/controllers/trafficpermissions" "github.com/hashicorp/consul/internal/auth/internal/controllers/trafficpermissions"
"github.com/hashicorp/consul/internal/auth/internal/controllers/trafficpermissions/expander"
"github.com/hashicorp/consul/internal/controller" "github.com/hashicorp/consul/internal/controller"
) )
@ -13,5 +14,5 @@ type Dependencies struct {
} }
func Register(mgr *controller.Manager, deps Dependencies) { func Register(mgr *controller.Manager, deps Dependencies) {
mgr.Register(trafficpermissions.Controller(deps.TrafficPermissionsMapper)) mgr.Register(trafficpermissions.Controller(deps.TrafficPermissionsMapper, expander.GetSamenessGroupExpander()))
} }

View File

@ -0,0 +1,87 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package trafficpermissions
import (
"sort"
"github.com/hashicorp/consul/internal/auth/internal/controllers/trafficpermissions/expander"
"github.com/hashicorp/consul/internal/resource"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
pbmulticluster "github.com/hashicorp/consul/proto-public/pbmulticluster/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
)
type trafficPermissionsBuilder struct {
missing map[resource.ReferenceKey]missingSamenessGroupReferences
isDefault bool
allowedPermissions []*pbauth.Permission
denyPermissions []*pbauth.Permission
sgExpander expander.SamenessGroupExpander
sgMap map[string][]*pbmulticluster.SamenessGroupMember
}
type missingSamenessGroupReferences struct {
resource *pbresource.Resource
samenessGroups []string
}
func newTrafficPermissionsBuilder(expander expander.SamenessGroupExpander, sgMap map[string][]*pbmulticluster.SamenessGroupMember) *trafficPermissionsBuilder {
return &trafficPermissionsBuilder{
sgMap: sgMap,
missing: make(map[resource.ReferenceKey]missingSamenessGroupReferences),
isDefault: true,
sgExpander: expander,
allowedPermissions: make([]*pbauth.Permission, 0),
denyPermissions: make([]*pbauth.Permission, 0),
}
}
// 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)
if len(missingSamenessGroups) > 0 {
tpb.missing[resource.NewReferenceKey(dec.Id)] = missingSamenessGroupReferences{
resource: dec.Resource,
samenessGroups: missingSamenessGroups,
}
}
tpb.isDefault = false
if dec.Data.Action == pbauth.Action_ACTION_ALLOW {
tpb.allowedPermissions = append(tpb.allowedPermissions, dec.Data.Permissions...)
} else {
tpb.denyPermissions = append(tpb.denyPermissions, dec.Data.Permissions...)
}
}
func (tpb *trafficPermissionsBuilder) build() (*pbauth.ComputedTrafficPermissions, map[resource.ReferenceKey]missingSamenessGroupReferences) {
return &pbauth.ComputedTrafficPermissions{
AllowPermissions: tpb.allowedPermissions,
DenyPermissions: tpb.denyPermissions,
IsDefault: tpb.isDefault,
}, tpb.missing
}
func missingForCTP(missing map[resource.ReferenceKey]missingSamenessGroupReferences) []string {
m := make(map[string]struct{})
for _, sgRefs := range missing {
for _, sg := range sgRefs.samenessGroups {
m[sg] = struct{}{}
}
}
out := make([]string, 0, len(m))
for sg := range m {
out = append(out, sg)
}
sort.Strings(out)
return out
}

View File

@ -9,6 +9,7 @@ import (
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/anypb"
"github.com/hashicorp/consul/internal/auth/internal/controllers/trafficpermissions/expander"
"github.com/hashicorp/consul/internal/controller" "github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/controller/dependency" "github.com/hashicorp/consul/internal/controller/dependency"
"github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/resource"
@ -33,25 +34,41 @@ type TrafficPermissionsMapper interface {
// Controller creates a controller for automatic ComputedTrafficPermissions management for // Controller creates a controller for automatic ComputedTrafficPermissions management for
// updates to WorkloadIdentity or TrafficPermission resources. // updates to WorkloadIdentity or TrafficPermission resources.
func Controller(mapper TrafficPermissionsMapper) *controller.Controller { func Controller(mapper TrafficPermissionsMapper, sgExpander expander.SamenessGroupExpander) *controller.Controller {
if mapper == nil { if mapper == nil {
panic("No TrafficPermissionsMapper was provided to the TrafficPermissionsController constructor") panic("TrafficPermissionsMapper is required for TrafficPermissionsController constructor")
}
if sgExpander == nil {
panic("SamenessGroupExpander is required for TrafficPermissionsController constructor")
} }
return controller.NewController(StatusKey, pbauth.ComputedTrafficPermissionsType). samenessGroupIndex := GetSamenessGroupIndex()
ctrl := controller.NewController(StatusKey, pbauth.ComputedTrafficPermissionsType).
WithWatch(pbauth.WorkloadIdentityType, dependency.ReplaceType(pbauth.ComputedTrafficPermissionsType)). WithWatch(pbauth.WorkloadIdentityType, dependency.ReplaceType(pbauth.ComputedTrafficPermissionsType)).
WithWatch(pbauth.TrafficPermissionsType, mapper.MapTrafficPermissions). WithWatch(pbauth.TrafficPermissionsType, mapper.MapTrafficPermissions, samenessGroupIndex).
WithReconciler(&reconciler{mapper: mapper}) WithReconciler(&reconciler{mapper: mapper, sgExpander: sgExpander})
return registerEnterpriseControllerWatchers(ctrl)
} }
type reconciler struct { type reconciler struct {
mapper TrafficPermissionsMapper mapper TrafficPermissionsMapper
sgExpander expander.SamenessGroupExpander
} }
// Reconcile will reconcile one ComputedTrafficPermission (CTP) in response to some event. // Reconcile will reconcile one ComputedTrafficPermission (CTP) in response to some event.
// Events include adding, modifying or deleting a WorkloadIdentity or TrafficPermission. // Events include adding, modifying or deleting a WorkloadIdentity or TrafficPermission or SamenessGroupType.
func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req controller.Request) error { func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req controller.Request) error {
rt.Logger = rt.Logger.With("resource-id", req.ID, "controller", StatusKey) rt.Logger = rt.Logger.With("resource-id", req.ID, "controller", StatusKey)
ctpID := req.ID
oldCTPData, err := resource.GetDecodedResource[*pbauth.ComputedTrafficPermissions](ctx, rt.Client, ctpID)
if err != nil {
rt.Logger.Error("error retrieving computed permissions", "error", err)
return err
}
/* /*
* A CTP ID could come in for a variety of reasons. * A CTP ID could come in for a variety of reasons.
* 1. workload identity create / delete: this results in the creation / deletion of a new CTP * 1. workload identity create / delete: this results in the creation / deletion of a new CTP
@ -62,7 +79,7 @@ func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req c
* CTPs are always generated from WorkloadIdentities, therefore the WI resource must already exist. * CTPs are always generated from WorkloadIdentities, therefore the WI resource must already exist.
* If it is missing, that means it was deleted. * If it is missing, that means it was deleted.
*/ */
ctpID := req.ID
wi := resource.ReplaceType(pbauth.WorkloadIdentityType, ctpID) wi := resource.ReplaceType(pbauth.WorkloadIdentityType, ctpID)
workloadIdentity, err := resource.GetDecodedResource[*pbauth.WorkloadIdentity](ctx, rt.Client, wi) workloadIdentity, err := resource.GetDecodedResource[*pbauth.WorkloadIdentity](ctx, rt.Client, wi)
if err != nil { if err != nil {
@ -75,11 +92,6 @@ func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req c
} }
// Check if CTP exists: // Check if CTP exists:
oldCTPData, err := resource.GetDecodedResource[*pbauth.ComputedTrafficPermissions](ctx, rt.Client, ctpID)
if err != nil {
rt.Logger.Error("error retrieving computed permissions", "error", err)
return err
}
var oldResource *pbresource.Resource var oldResource *pbresource.Resource
var owner *pbresource.ID var owner *pbresource.ID
if oldCTPData == nil { if oldCTPData == nil {
@ -91,47 +103,153 @@ func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req c
owner = oldCTPData.Resource.Owner owner = oldCTPData.Resource.Owner
} }
// Part 2: Recompute a CTP from TP create / modify / delete, or create a new CTP from existing TPs: sgMap, err := r.sgExpander.List(ctx, rt, req)
latestTrafficPermissions, err := computeNewTrafficPermissions(ctx, rt, r.mapper, ctpID, oldResource)
if err != nil { if err != nil {
rt.Logger.Error("error calculating computed permissions", "error", err) rt.Logger.Error("error retrieving sameness groups", err.Error())
return err return err
} }
if oldCTPData != nil && proto.Equal(oldCTPData.Data, latestTrafficPermissions) { trafficPermissionBuilder := newTrafficPermissionsBuilder(r.sgExpander, sgMap)
// there are no changes to the computed traffic permissions, and we can return early var tpResources []*pbresource.Resource
// Part 2: Recompute a CTP from TP create / modify / delete, or create a new CTP from existing TPs:
trackedTPs := r.mapper.GetTrafficPermissionsForCTP(ctpID)
if len(trackedTPs) > 0 {
rt.Logger.Trace("got tracked traffic permissions for CTP", "tps:", trackedTPs)
} else {
rt.Logger.Trace("found no tracked traffic permissions for CTP")
}
for _, t := range trackedTPs {
rsp, err := resource.GetDecodedResource[*pbauth.TrafficPermissions](ctx, rt.Client, resource.IDFromReference(t))
if err != nil {
rt.Logger.Error("error reading traffic permissions resource for computation", "error", err)
writeFailedStatus(ctx, rt, oldResource, resource.IDFromReference(t), err.Error())
return err
}
if rsp == nil {
rt.Logger.Trace("untracking deleted TrafficPermissions", "traffic-permissions-name", t.Name)
r.mapper.UntrackTrafficPermissions(resource.IDFromReference(t))
continue
}
trafficPermissionBuilder.track(rsp)
tpResources = append(tpResources, rsp.Resource)
}
latestComputedTrafficPermissions, missing := trafficPermissionBuilder.build()
if err != nil {
rt.Logger.Error("error expanding sameness groups", err.Error())
return err
}
newCTPResource := oldResource
allMissing := missingForCTP(missing)
if (oldCTPData == nil) || (!proto.Equal(oldCTPData.Data, latestComputedTrafficPermissions)) {
rt.Logger.Trace("no new computed traffic permissions") rt.Logger.Trace("no new computed traffic permissions")
// We can't short circuit here because we always need to update statuses.
newCTPData, err := anypb.New(latestComputedTrafficPermissions)
if err != nil {
rt.Logger.Error("error marshalling latest traffic permissions", "error", err)
writeFailedStatus(ctx, rt, oldResource, nil, err.Error())
return err
}
rt.Logger.Trace("writing computed traffic permissions")
rsp, err := rt.Client.Write(ctx, &pbresource.WriteRequest{
Resource: &pbresource.Resource{
Id: req.ID,
Data: newCTPData,
Owner: owner,
},
})
if err != nil {
rt.Logger.Error("error writing new computed traffic permissions", "error", err)
writeFailedStatus(ctx, rt, oldResource, nil, err.Error())
return err
} else {
rt.Logger.Trace("new computed traffic permissions were successfully written")
}
newCTPResource = rsp.Resource
}
if len(allMissing) > 0 {
return writeMissingSgStatuses(ctx, rt, req, allMissing, newCTPResource, missing, tpResources)
}
return writeComputedStatuses(ctx, rt, req, newCTPResource, latestComputedTrafficPermissions.IsDefault, tpResources)
}
func writeComputedStatuses(ctx context.Context, rt controller.Runtime, req controller.Request, ctpResource *pbresource.Resource, isDefault bool,
trackedTPs []*pbresource.Resource) error {
for _, tp := range trackedTPs {
err := writeStatusWithConditions(ctx, rt, tp,
[]*pbresource.Condition{ConditionComputedTrafficPermission()})
if err != nil {
return err
}
}
condition := ConditionComputed(req.ID.Name, isDefault)
return writeStatusWithConditions(ctx, rt, ctpResource, []*pbresource.Condition{condition})
}
func writeMissingSgStatuses(ctx context.Context, rt controller.Runtime, req controller.Request, allMissing []string, newCTPResource *pbresource.Resource,
missing map[resource.ReferenceKey]missingSamenessGroupReferences, tpResources []*pbresource.Resource) error {
condition := ConditionMissingSamenessGroup(req.ID.Tenancy.Partition, allMissing)
rt.Logger.Trace("writing missing sameness groups status")
err := writeStatusWithConditions(ctx, rt, newCTPResource, []*pbresource.Condition{condition})
if err != nil {
return err
}
//writing status to traffic permissions
for _, sgRefs := range missing {
if len(sgRefs.samenessGroups) == 0 {
err := writeStatusWithConditions(ctx, rt, sgRefs.resource,
[]*pbresource.Condition{ConditionComputedTrafficPermission()})
if err != nil {
return err
}
continue
}
conditionTp := ConditionMissingSamenessGroup(req.ID.Tenancy.Partition, sgRefs.samenessGroups)
err := writeStatusWithConditions(ctx, rt, sgRefs.resource, []*pbresource.Condition{conditionTp})
if err != nil {
return err
}
}
for _, trackedTp := range tpResources {
if _, ok := missing[resource.NewReferenceKey(trackedTp.Id)]; ok {
continue
}
err := writeStatusWithConditions(ctx, rt, trackedTp,
[]*pbresource.Condition{ConditionComputedTrafficPermission()})
if err != nil {
return err
}
}
return nil
}
func writeStatusWithConditions(ctx context.Context, rt controller.Runtime, res *pbresource.Resource,
conditions []*pbresource.Condition) error {
newStatus := &pbresource.Status{
ObservedGeneration: res.Generation,
Conditions: conditions,
}
if resource.EqualStatus(res.Status[StatusKey], newStatus, false) {
rt.Logger.Trace("old status is same as new status. skipping write", "resource", res.Id)
return nil return nil
} }
newCTPData, err := anypb.New(latestTrafficPermissions)
if err != nil { _, err := rt.Client.WriteStatus(ctx, &pbresource.WriteStatusRequest{
rt.Logger.Error("error marshalling latest traffic permissions", "error", err) Id: res.Id,
writeFailedStatus(ctx, rt, oldResource, nil, err.Error())
return err
}
rt.Logger.Trace("writing computed traffic permissions")
rsp, err := rt.Client.Write(ctx, &pbresource.WriteRequest{
Resource: &pbresource.Resource{
Id: req.ID,
Data: newCTPData,
Owner: owner,
},
})
if err != nil || rsp.Resource == nil {
rt.Logger.Error("error writing new computed traffic permissions", "error", err)
writeFailedStatus(ctx, rt, oldResource, nil, err.Error())
return err
} else {
rt.Logger.Trace("new computed traffic permissions were successfully written")
}
newStatus := &pbresource.Status{
ObservedGeneration: rsp.Resource.Generation,
Conditions: []*pbresource.Condition{
ConditionComputed(req.ID.Name, latestTrafficPermissions.IsDefault),
},
}
_, err = rt.Client.WriteStatus(ctx, &pbresource.WriteStatusRequest{
Id: rsp.Resource.Id,
Key: StatusKey, Key: StatusKey,
Status: newStatus, Status: newStatus,
}) })
@ -142,55 +260,8 @@ func writeFailedStatus(ctx context.Context, rt controller.Runtime, ctp *pbresour
if ctp == nil { if ctp == nil {
return nil return nil
} }
newStatus := &pbresource.Status{ conditions := []*pbresource.Condition{
ObservedGeneration: ctp.Generation, ConditionFailedToCompute(ctp.Id.Name, tp.GetName(), errDetail),
Conditions: []*pbresource.Condition{
ConditionFailedToCompute(ctp.Id.Name, tp.Name, errDetail),
},
} }
_, err := rt.Client.WriteStatus(ctx, &pbresource.WriteStatusRequest{ return writeStatusWithConditions(ctx, rt, ctp, conditions)
Id: ctp.Id,
Key: StatusKey,
Status: newStatus,
})
return err
}
// computeNewTrafficPermissions will use all associated Traffic Permissions to create new ComputedTrafficPermissions data
func computeNewTrafficPermissions(ctx context.Context, rt controller.Runtime, wm TrafficPermissionsMapper, ctpID *pbresource.ID, ctp *pbresource.Resource) (*pbauth.ComputedTrafficPermissions, error) {
// Part 1: Get all TPs that apply to workload identity
// Get already associated WorkloadIdentities/CTPs for reconcile requests:
trackedTPs := wm.GetTrafficPermissionsForCTP(ctpID)
if len(trackedTPs) > 0 {
rt.Logger.Trace("got tracked traffic permissions for CTP", "tps:", trackedTPs)
} else {
rt.Logger.Trace("found no tracked traffic permissions for CTP")
}
ap := make([]*pbauth.Permission, 0)
dp := make([]*pbauth.Permission, 0)
isDefault := true
for _, t := range trackedTPs {
rsp, err := resource.GetDecodedResource[*pbauth.TrafficPermissions](ctx, rt.Client, resource.IDFromReference(t))
if err != nil {
rt.Logger.Error("error reading traffic permissions resource for computation", "error", err)
writeFailedStatus(ctx, rt, ctp, resource.IDFromReference(t), err.Error())
return nil, err
}
if rsp == nil {
rt.Logger.Trace("untracking deleted TrafficPermissions", "traffic-permissions-name", t.Name)
wm.UntrackTrafficPermissions(resource.IDFromReference(t))
continue
}
isDefault = false
if rsp.Data.Action == pbauth.Action_ACTION_ALLOW {
ap = append(ap, rsp.Data.Permissions...)
} else {
dp = append(dp, rsp.Data.Permissions...)
}
}
return &pbauth.ComputedTrafficPermissions{
AllowPermissions: ap,
DenyPermissions: dp,
IsDefault: isDefault,
}, nil
} }

View File

@ -12,9 +12,11 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing" svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing"
"github.com/hashicorp/consul/internal/auth/internal/controllers/trafficpermissions/expander"
"github.com/hashicorp/consul/internal/auth/internal/mappers/trafficpermissionsmapper" "github.com/hashicorp/consul/internal/auth/internal/mappers/trafficpermissionsmapper"
"github.com/hashicorp/consul/internal/auth/internal/types" "github.com/hashicorp/consul/internal/auth/internal/types"
"github.com/hashicorp/consul/internal/controller" "github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/multicluster"
"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"
rtest "github.com/hashicorp/consul/internal/resource/resourcetest" rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
@ -32,27 +34,31 @@ type controllerSuite struct {
rt controller.Runtime rt controller.Runtime
mapper *trafficpermissionsmapper.TrafficPermissionsMapper mapper *trafficpermissionsmapper.TrafficPermissionsMapper
sgExpander expander.SamenessGroupExpander
reconciler *reconciler reconciler *reconciler
tenancies []*pbresource.Tenancy tenancies []*pbresource.Tenancy
isEnterprise bool isEnterprise bool
} }
func (suite *controllerSuite) SetupTest() { func (suite *controllerSuite) SetupTest() {
suite.isEnterprise = versiontest.IsEnterprise() suite.isEnterprise = versiontest.IsEnterprise()
suite.tenancies = resourcetest.TestTenancies() suite.tenancies = resourcetest.TestTenancies()
suite.ctx = testutil.TestContext(suite.T()) suite.ctx = testutil.TestContext(suite.T())
client := svctest.NewResourceServiceBuilder(). client := svctest.NewResourceServiceBuilder().
WithRegisterFns(types.Register). WithRegisterFns(types.Register, multicluster.RegisterTypes).
WithTenancies(suite.tenancies...). WithTenancies(suite.tenancies...).
Run(suite.T()) Run(suite.T())
suite.client = rtest.NewClient(client) suite.client = rtest.NewClient(client)
suite.rt = controller.Runtime{ suite.rt = controller.Runtime{
Client: suite.client, Client: suite.client,
Logger: testutil.Logger(suite.T()), Logger: testutil.Logger(suite.T()),
} }
suite.mapper = trafficpermissionsmapper.New() suite.mapper = trafficpermissionsmapper.New()
suite.reconciler = &reconciler{mapper: suite.mapper} suite.sgExpander = expander.GetSamenessGroupExpander()
suite.reconciler = &reconciler{mapper: suite.mapper, sgExpander: suite.sgExpander}
} }
func (suite *controllerSuite) requireTrafficPermissionsTracking(tp *pbresource.Resource, ids ...*pbresource.ID) { func (suite *controllerSuite) requireTrafficPermissionsTracking(tp *pbresource.Resource, ids ...*pbresource.ID) {
@ -173,6 +179,7 @@ func (suite *controllerSuite) TestReconcile_WorkloadIdentityDelete_ReferencingTr
}). }).
WithTenancy(tenancy). WithTenancy(tenancy).
Write(suite.T(), suite.client) Write(suite.T(), suite.client)
wi1ID := &pbresource.ID{ wi1ID := &pbresource.ID{
Name: "wi1", Name: "wi1",
Type: pbauth.ComputedTrafficPermissionsType, Type: pbauth.ComputedTrafficPermissionsType,
@ -323,7 +330,7 @@ func (suite *controllerSuite) TestReconcile_TrafficPermissionsCreate_Destination
rtest.RequireOwner(suite.T(), ctpResource, wi.Id, true) rtest.RequireOwner(suite.T(), ctpResource, wi.Id, true)
assertCTPDefaultStatus(suite.T(), ctpResource, false) assertCTPDefaultStatus(suite.T(), ctpResource, false)
// Delete the traffic permissions without updating the caches. Ensure is default is right even when the caches contain stale data. // Delete the traffic permissions without updating the caches. Ensure is default is right even when the caches contain stale samenessGroupsForTrafficPermission.
suite.client.MustDelete(suite.T(), tp1.Id) suite.client.MustDelete(suite.T(), tp1.Id)
suite.client.MustDelete(suite.T(), tp2.Id) suite.client.MustDelete(suite.T(), tp2.Id)
suite.client.MustDelete(suite.T(), tp3.Id) suite.client.MustDelete(suite.T(), tp3.Id)
@ -475,7 +482,7 @@ func (suite *controllerSuite) TestControllerBasic() {
// TODO: refactor this // TODO: refactor this
// In this test we check basic operations for a workload identity and referencing traffic permission // In this test we check basic operations for a workload identity and referencing traffic permission
mgr := controller.NewManager(suite.client, suite.rt.Logger) mgr := controller.NewManager(suite.client, suite.rt.Logger)
mgr.Register(Controller(suite.mapper)) mgr.Register(Controller(suite.mapper, suite.sgExpander))
mgr.SetRaftLeader(true) mgr.SetRaftLeader(true)
go mgr.Run(suite.ctx) go mgr.Run(suite.ctx)
@ -588,7 +595,7 @@ func (suite *controllerSuite) TestControllerBasicWithMultipleTenancyLevels() {
// TODO: refactor this // TODO: refactor this
// In this test we check basic operations for a workload identity and referencing traffic permission // In this test we check basic operations for a workload identity and referencing traffic permission
mgr := controller.NewManager(suite.client, suite.rt.Logger) mgr := controller.NewManager(suite.client, suite.rt.Logger)
mgr.Register(Controller(suite.mapper)) mgr.Register(Controller(suite.mapper, suite.sgExpander))
mgr.SetRaftLeader(true) mgr.SetRaftLeader(true)
go mgr.Run(suite.ctx) go mgr.Run(suite.ctx)
@ -709,7 +716,7 @@ func (suite *controllerSuite) TestControllerMultipleTrafficPermissions() {
suite.T().Skip("flaky behavior observed") suite.T().Skip("flaky behavior observed")
// In this test we check operations for a workload identity and multiple referencing traffic permissions // In this test we check operations for a workload identity and multiple referencing traffic permissions
mgr := controller.NewManager(suite.client, suite.rt.Logger) mgr := controller.NewManager(suite.client, suite.rt.Logger)
mgr.Register(Controller(suite.mapper)) mgr.Register(Controller(suite.mapper, suite.sgExpander))
mgr.SetRaftLeader(true) mgr.SetRaftLeader(true)
go mgr.Run(suite.ctx) go mgr.Run(suite.ctx)

View File

@ -0,0 +1,14 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !consulent
package expander
import (
"github.com/hashicorp/consul/internal/auth/internal/controllers/trafficpermissions/expander/expander_ce"
)
func GetSamenessGroupExpander() *expander_ce.SamenessGroupExpander {
return expander_ce.New()
}

View File

@ -0,0 +1,31 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package expander_ce
import (
"context"
pbmulticluster "github.com/hashicorp/consul/proto-public/pbmulticluster/v2beta1"
"github.com/hashicorp/consul/internal/controller"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
)
type SamenessGroupExpander struct{}
func New() *SamenessGroupExpander {
return &SamenessGroupExpander{}
}
func (sgE *SamenessGroupExpander) Expand(_ *pbauth.TrafficPermissions,
_ map[string][]*pbmulticluster.SamenessGroupMember) []string {
//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
return nil, nil
}

View File

@ -0,0 +1,18 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package expander
import (
"context"
"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
type SamenessGroupExpander interface {
Expand(*pbauth.TrafficPermissions, map[string][]*pbmulticluster.SamenessGroupMember) []string
List(context.Context, controller.Runtime, controller.Request) (map[string][]*pbmulticluster.SamenessGroupMember, error)
}

View File

@ -0,0 +1,32 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !consulent
package trafficpermissions
import (
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/controller/cache/index"
"github.com/hashicorp/consul/internal/controller/cache/indexers"
"github.com/hashicorp/consul/internal/resource"
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1"
)
const SgIndexName = "samenessGroupIndex"
func registerEnterpriseControllerWatchers(ctrl *controller.Controller) *controller.Controller {
return ctrl
}
func GetSamenessGroupIndex() *index.Index {
return indexers.DecodedMultiIndexer(
SgIndexName,
index.ReferenceOrIDFromArgs,
func(r *resource.DecodedResource[*pbauth.TrafficPermissions]) (bool, [][]byte, error) {
//no - op for CE
return false, nil, nil
},
)
}

View File

@ -5,17 +5,21 @@ package trafficpermissions
import ( import (
"fmt" "fmt"
"sort"
"strings"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
) )
const ( const (
StatusKey = "consul.io/traffic-permissions" StatusKey = "consul.io/traffic-permissions"
StatusTrafficPermissionsComputed = "Traffic permissions have been computed" StatusTrafficPermissionsComputed = "Traffic permissions have been computed"
StatusTrafficPermissionsNotComputed = "Traffic permissions have not been computed" StatusTrafficPermissionsNotComputed = "Traffic permissions have not been computed"
ConditionPermissionsAppliedMsg = "Workload identity %s has new permissions" StatusTrafficPermissionsProcessed = "Traffic Permission processed successfully"
ConditionNoPermissionsMsg = "Workload identity %s has no permissions" ConditionPermissionsAppliedMsg = "Workload identity %s has new permissions"
ConditionPermissionsFailedMsg = "Unable to calculate new permission set for Workload identity %s" ConditionNoPermissionsMsg = "Workload identity %s has no permissions"
ConditionPermissionsFailedMsg = "Unable to calculate new permission set for Workload identity %s"
ConditionMissingSamenessGroupInPartition = "Missing Sameness Groups names in partition(%s) - %s"
) )
func ConditionComputed(workloadIdentity string, isDefault bool) *pbresource.Condition { func ConditionComputed(workloadIdentity string, isDefault bool) *pbresource.Condition {
@ -30,6 +34,14 @@ func ConditionComputed(workloadIdentity string, isDefault bool) *pbresource.Cond
} }
} }
func ConditionComputedTrafficPermission() *pbresource.Condition {
return &pbresource.Condition{
Type: StatusTrafficPermissionsComputed,
State: pbresource.Condition_STATE_TRUE,
Message: StatusTrafficPermissionsProcessed,
}
}
func ConditionFailedToCompute(workloadIdentity string, trafficPermissions string, errDetail string) *pbresource.Condition { func ConditionFailedToCompute(workloadIdentity string, trafficPermissions string, errDetail string) *pbresource.Condition {
message := fmt.Sprintf(ConditionPermissionsFailedMsg, workloadIdentity) message := fmt.Sprintf(ConditionPermissionsFailedMsg, workloadIdentity)
if len(trafficPermissions) > 0 { if len(trafficPermissions) > 0 {
@ -44,3 +56,13 @@ func ConditionFailedToCompute(workloadIdentity string, trafficPermissions string
Message: message, Message: message,
} }
} }
func ConditionMissingSamenessGroup(partition string, missingSamenessGroups []string) *pbresource.Condition {
sort.Strings(missingSamenessGroups)
message := fmt.Sprintf(ConditionMissingSamenessGroupInPartition, partition, strings.Join(missingSamenessGroups, ","))
return &pbresource.Condition{
Type: StatusTrafficPermissionsNotComputed,
State: pbresource.Condition_STATE_FALSE,
Message: message,
}
}

View File

@ -41,7 +41,7 @@ func validateComputedTrafficPermissions(res *DecodedComputedTrafficPermissions)
Wrapped: err, Wrapped: err,
} }
} }
if err := validatePermission(permission, wrapErr); err != nil { if err := validatePermission(permission, res.Id, wrapErr); err != nil {
merr = multierror.Append(merr, err) merr = multierror.Append(merr, err)
} }
} }
@ -54,7 +54,7 @@ func validateComputedTrafficPermissions(res *DecodedComputedTrafficPermissions)
Wrapped: err, Wrapped: err,
} }
} }
if err := validatePermission(permission, wrapErr); err != nil { if err := validatePermission(permission, res.Id, wrapErr); err != nil {
merr = multierror.Append(merr, err) merr = multierror.Append(merr, err)
} }
} }

View File

@ -137,7 +137,7 @@ func validateTrafficPermissions(res *DecodedTrafficPermissions) error {
Wrapped: err, Wrapped: err,
} }
} }
if err := validatePermission(permission, wrapErr); err != nil { if err := validatePermission(permission, res.Id, wrapErr); err != nil {
merr = multierror.Append(merr, err) merr = multierror.Append(merr, err)
} }
} }
@ -145,7 +145,7 @@ func validateTrafficPermissions(res *DecodedTrafficPermissions) error {
return merr return merr
} }
func validatePermission(p *pbauth.Permission, wrapErr func(error) error) error { func validatePermission(p *pbauth.Permission, id *pbresource.ID, wrapErr func(error) error) error {
var merr error var merr error
if len(p.Sources) == 0 { if len(p.Sources) == 0 {
@ -163,7 +163,7 @@ func validatePermission(p *pbauth.Permission, wrapErr func(error) error) error {
Wrapped: err, Wrapped: err,
}) })
} }
if sourceHasIncompatibleTenancies(src) { if sourceHasIncompatibleTenancies(src, id) {
merr = multierror.Append(merr, wrapSrcErr(resource.ErrInvalidListElement{ merr = multierror.Append(merr, wrapSrcErr(resource.ErrInvalidListElement{
Name: "source", Name: "source",
Wrapped: errSourcesTenancy, Wrapped: errSourcesTenancy,
@ -194,7 +194,7 @@ func validatePermission(p *pbauth.Permission, wrapErr func(error) error) error {
Wrapped: err, Wrapped: err,
}) })
} }
if sourceHasIncompatibleTenancies(d) { if sourceHasIncompatibleTenancies(d, id) {
merr = multierror.Append(merr, wrapExclSrcErr(resource.ErrInvalidField{ merr = multierror.Append(merr, wrapExclSrcErr(resource.ErrInvalidField{
Name: "exclude_source", Name: "exclude_source",
Wrapped: errSourcesTenancy, Wrapped: errSourcesTenancy,
@ -263,9 +263,12 @@ func validatePermission(p *pbauth.Permission, wrapErr func(error) error) error {
return merr return merr
} }
func sourceHasIncompatibleTenancies(src pbauth.SourceToSpiffe) bool { func sourceHasIncompatibleTenancies(src pbauth.SourceToSpiffe, id *pbresource.ID) bool {
if id.Tenancy == nil {
id.Tenancy = &pbresource.Tenancy{}
}
peerSet := src.GetPeer() != resource.DefaultPeerName peerSet := src.GetPeer() != resource.DefaultPeerName
apSet := src.GetPartition() != resource.DefaultPartitionName apSet := src.GetPartition() != id.Tenancy.Partition
sgSet := src.GetSamenessGroup() != "" sgSet := src.GetSamenessGroup() != ""
return (apSet && peerSet) || (apSet && sgSet) || (peerSet && sgSet) return (apSet && peerSet) || (apSet && sgSet) || (peerSet && sgSet)

View File

@ -33,6 +33,7 @@ func TestValidateTrafficPermissions_ParseError(t *testing.T) {
func TestValidateTrafficPermissions(t *testing.T) { func TestValidateTrafficPermissions(t *testing.T) {
cases := map[string]struct { cases := map[string]struct {
tp *pbauth.TrafficPermissions tp *pbauth.TrafficPermissions
id *pbresource.ID
expectErr string expectErr string
}{ }{
"ok-minimal": { "ok-minimal": {
@ -161,13 +162,187 @@ func TestValidateTrafficPermissions(t *testing.T) {
}, },
expectErr: `invalid element at index 0 of list "permissions": invalid element at index 0 of list "destination_rules": invalid element at index 0 of list "destination_rule": traffic permissions with L7 rules are not yet supported`, expectErr: `invalid element at index 0 of list "permissions": invalid element at index 0 of list "destination_rules": invalid element at index 0 of list "destination_rule": traffic permissions with L7 rules are not yet supported`,
}, },
"source-has-same-tenancy-as-tp": {
id: &pbresource.ID{
Tenancy: &pbresource.Tenancy{
Partition: resource.DefaultPartitionName,
},
},
tp: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: "w1",
},
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,
},
},
tp: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: "w1",
},
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,
},
},
tp: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: "w1",
},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
Partition: resource.DefaultNamespaceName,
Peer: "peer",
SamenessGroup: "",
},
},
},
},
},
},
"source-has-sameness-group-set": {
id: &pbresource.ID{
Tenancy: &pbresource.Tenancy{
Partition: resource.DefaultPartitionName,
},
},
tp: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: "w1",
},
Action: pbauth.Action_ACTION_ALLOW,
Permissions: []*pbauth.Permission{
{
Sources: []*pbauth.Source{
{
Partition: resource.DefaultNamespaceName,
Peer: resource.DefaultPeerName,
SamenessGroup: "sg1",
},
},
},
},
},
},
"source-has-peer-and-partition-set": {
id: &pbresource.ID{
Tenancy: &pbresource.Tenancy{
Partition: resource.DefaultPartitionName,
},
},
tp: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: "w1",
},
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,
},
},
tp: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: "w1",
},
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,
},
},
tp: &pbauth.TrafficPermissions{
Destination: &pbauth.Destination{
IdentityName: "w1",
},
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 { for n, tc := range cases {
t.Run(n, func(t *testing.T) { t.Run(n, func(t *testing.T) {
res := resourcetest.Resource(pbauth.TrafficPermissionsType, "tp"). resBuilder := resourcetest.Resource(pbauth.TrafficPermissionsType, "tp").
WithData(t, tc.tp). WithData(t, tc.tp)
Build() if tc.id != nil {
resBuilder = resBuilder.WithTenancy(tc.id.Tenancy)
}
res := resBuilder.Build()
err := ValidateTrafficPermissions(res) err := ValidateTrafficPermissions(res)
if tc.expectErr == "" { if tc.expectErr == "" {

View File

@ -0,0 +1,15 @@
// 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
}

View File

@ -0,0 +1,50 @@
// 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)
}
}