Add sameness groups to service intentions. (#17064)

This commit is contained in:
Eric Haberkorn 2023-04-20 12:16:04 -04:00 committed by GitHub
parent 863cd57117
commit b1fae05983
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1218 additions and 1044 deletions

View File

@ -15,47 +15,67 @@ import (
// The return value of `auth` is only valid if the second value `match` is true. // The return value of `auth` is only valid if the second value `match` is true.
// If `match` is false, then the intention doesn't match this target and any result should be ignored. // If `match` is false, then the intention doesn't match this target and any result should be ignored.
func AuthorizeIntentionTarget( func AuthorizeIntentionTarget(
target, targetNS, targetAP string, target, targetNS, targetAP, targetPeer string,
ixn *structs.Intention, ixn *structs.Intention,
matchType structs.IntentionMatchType, matchType structs.IntentionMatchType,
) (auth bool, match bool) { ) (bool, bool) {
match := IntentionMatch(target, targetNS, targetAP, targetPeer, ixn, matchType)
if match {
return ixn.Action == structs.IntentionActionAllow, true
} else {
return false, false
}
}
// IntentionMatch determines whether the target is covered by the given intention.
func IntentionMatch(
target, targetNS, targetAP, targetPeer string,
ixn *structs.Intention,
matchType structs.IntentionMatchType,
) bool {
switch matchType { switch matchType {
case structs.IntentionMatchDestination: case structs.IntentionMatchDestination:
if acl.PartitionOrDefault(ixn.DestinationPartition) != acl.PartitionOrDefault(targetAP) { if acl.PartitionOrDefault(ixn.DestinationPartition) != acl.PartitionOrDefault(targetAP) {
return false, false return false
} }
if ixn.DestinationNS != structs.WildcardSpecifier && ixn.DestinationNS != targetNS { if ixn.DestinationNS != structs.WildcardSpecifier && ixn.DestinationNS != targetNS {
// Non-matching namespace // Non-matching namespace
return false, false return false
} }
if ixn.DestinationName != structs.WildcardSpecifier && ixn.DestinationName != target { if ixn.DestinationName != structs.WildcardSpecifier && ixn.DestinationName != target {
// Non-matching name // Non-matching name
return false, false return false
} }
case structs.IntentionMatchSource: case structs.IntentionMatchSource:
if ixn.SourcePeer != targetPeer {
return false
}
if acl.PartitionOrDefault(ixn.SourcePartition) != acl.PartitionOrDefault(targetAP) { if acl.PartitionOrDefault(ixn.SourcePartition) != acl.PartitionOrDefault(targetAP) {
return false, false return false
} }
if ixn.SourceNS != structs.WildcardSpecifier && ixn.SourceNS != targetNS { if ixn.SourceNS != structs.WildcardSpecifier && ixn.SourceNS != targetNS {
// Non-matching namespace // Non-matching namespace
return false, false return false
} }
if ixn.SourceName != structs.WildcardSpecifier && ixn.SourceName != target { if ixn.SourceName != structs.WildcardSpecifier && ixn.SourceName != target {
// Non-matching name // Non-matching name
return false, false return false
} }
default: default:
// Reject on any un-recognized match type // Reject on any un-recognized match type
return false, false return false
} }
// The name and namespace match, so the destination is covered // The name and namespace match, so the destination is covered
return ixn.Action == structs.IntentionActionAllow, true return true
} }

View File

@ -15,6 +15,7 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
target string target string
targetNS string targetNS string
targetAP string targetAP string
targetPeer string
ixn *structs.Intention ixn *structs.Intention
matchType structs.IntentionMatchType matchType structs.IntentionMatchType
auth bool auth bool
@ -143,11 +144,41 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
auth: false, auth: false,
match: false, match: false,
}, },
{
name: "match peer",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
targetPeer: "cluster-01",
ixn: &structs.Intention{
SourceName: "web",
SourceNS: structs.IntentionDefaultNamespace,
SourcePeer: "cluster-01",
Action: structs.IntentionActionAllow,
},
matchType: structs.IntentionMatchSource,
auth: true,
match: true,
},
{
name: "no peer match",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
targetPeer: "cluster-02",
ixn: &structs.Intention{
SourceName: "web",
SourceNS: structs.IntentionDefaultNamespace,
SourcePeer: "cluster-01",
Action: structs.IntentionActionAllow,
},
matchType: structs.IntentionMatchSource,
auth: false,
match: false,
},
} }
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
auth, match := AuthorizeIntentionTarget(tc.target, tc.targetNS, tc.targetAP, tc.ixn, tc.matchType) auth, match := AuthorizeIntentionTarget(tc.target, tc.targetNS, tc.targetAP, tc.targetPeer, tc.ixn, tc.matchType)
assert.Equal(t, tc.auth, auth) assert.Equal(t, tc.auth, auth)
assert.Equal(t, tc.match, match) assert.Equal(t, tc.match, match)
}) })

View File

@ -119,7 +119,7 @@ func (a *Agent) ConnectAuthorize(token string,
for _, ixn := range reply.Matches[0] { for _, ixn := range reply.Matches[0] {
// We match on the intention source because the uriService is the source of the connection to authorize. // We match on the intention source because the uriService is the source of the connection to authorize.
if _, ok := connect.AuthorizeIntentionTarget( if _, ok := connect.AuthorizeIntentionTarget(
uriService.Service, uriService.Namespace, uriService.Partition, ixn, structs.IntentionMatchSource); ok { uriService.Service, uriService.Namespace, uriService.Partition, "", ixn, structs.IntentionMatchSource); ok {
ixnMatch = ixn ixnMatch = ixn
break break
} }

View File

@ -179,7 +179,7 @@ func (s *Intention) computeApplyChangesLegacyCreate(
if !args.Intention.CanWrite(authz) { if !args.Intention.CanWrite(authz) {
sn := args.Intention.SourceServiceName() sn := args.Intention.SourceServiceName()
dn := args.Intention.DestinationServiceName() dn := args.Intention.DestinationServiceName()
s.logger.Warn("Intention creation denied due to ACLs", s.logger.Debug("Intention creation denied due to ACLs",
"source", sn.String(), "source", sn.String(),
"destination", dn.String(), "destination", dn.String(),
"accessorID", acl.AliasIfAnonymousToken(accessorID)) "accessorID", acl.AliasIfAnonymousToken(accessorID))
@ -252,7 +252,7 @@ func (s *Intention) computeApplyChangesLegacyUpdate(
} }
if !ixn.CanWrite(authz) { if !ixn.CanWrite(authz) {
s.logger.Warn("Update operation on intention denied due to ACLs", s.logger.Debug("Update operation on intention denied due to ACLs",
"intention", args.Intention.ID, "intention", args.Intention.ID,
"accessorID", acl.AliasIfAnonymousToken(accessorID)) "accessorID", acl.AliasIfAnonymousToken(accessorID))
return nil, acl.ErrPermissionDenied return nil, acl.ErrPermissionDenied
@ -314,7 +314,7 @@ func (s *Intention) computeApplyChangesUpsert(
if !args.Intention.CanWrite(authz) { if !args.Intention.CanWrite(authz) {
sn := args.Intention.SourceServiceName() sn := args.Intention.SourceServiceName()
dn := args.Intention.DestinationServiceName() dn := args.Intention.DestinationServiceName()
s.logger.Warn("Intention upsert denied due to ACLs", s.logger.Debug("Intention upsert denied due to ACLs",
"source", sn.String(), "source", sn.String(),
"destination", dn.String(), "destination", dn.String(),
"accessorID", acl.AliasIfAnonymousToken(accessorID)) "accessorID", acl.AliasIfAnonymousToken(accessorID))
@ -373,7 +373,7 @@ func (s *Intention) computeApplyChangesLegacyDelete(
} }
if !ixn.CanWrite(authz) { if !ixn.CanWrite(authz) {
s.logger.Warn("Deletion operation on intention denied due to ACLs", s.logger.Debug("Deletion operation on intention denied due to ACLs",
"intention", args.Intention.ID, "intention", args.Intention.ID,
"accessorID", acl.AliasIfAnonymousToken(accessorID)) "accessorID", acl.AliasIfAnonymousToken(accessorID))
return nil, acl.ErrPermissionDenied return nil, acl.ErrPermissionDenied
@ -395,7 +395,7 @@ func (s *Intention) computeApplyChangesDelete(
if !args.Intention.CanWrite(authz) { if !args.Intention.CanWrite(authz) {
sn := args.Intention.SourceServiceName() sn := args.Intention.SourceServiceName()
dn := args.Intention.DestinationServiceName() dn := args.Intention.DestinationServiceName()
s.logger.Warn("Intention delete denied due to ACLs", s.logger.Debug("Intention delete denied due to ACLs",
"source", sn.String(), "source", sn.String(),
"destination", dn.String(), "destination", dn.String(),
"accessorID", acl.AliasIfAnonymousToken(accessorID)) "accessorID", acl.AliasIfAnonymousToken(accessorID))
@ -485,7 +485,7 @@ func (s *Intention) Get(args *structs.IntentionQueryRequest, reply *structs.Inde
// If ACLs prevented any responses, error // If ACLs prevented any responses, error
if len(reply.Intentions) == 0 { if len(reply.Intentions) == 0 {
accessorID := authz.AccessorID() accessorID := authz.AccessorID()
s.logger.Warn("Request to get intention denied due to ACLs", s.logger.Debug("Request to get intention denied due to ACLs",
"intention", args.IntentionID, "intention", args.IntentionID,
"accessorID", acl.AliasIfAnonymousToken(accessorID)) "accessorID", acl.AliasIfAnonymousToken(accessorID))
return acl.ErrPermissionDenied return acl.ErrPermissionDenied
@ -620,7 +620,7 @@ func (s *Intention) Match(args *structs.IntentionQueryRequest, reply *structs.In
if prefix := entry.Name; prefix != "" { if prefix := entry.Name; prefix != "" {
if err := authz.ToAllowAuthorizer().IntentionReadAllowed(prefix, &authzContext); err != nil { if err := authz.ToAllowAuthorizer().IntentionReadAllowed(prefix, &authzContext); err != nil {
accessorID := authz.AccessorID() accessorID := authz.AccessorID()
s.logger.Warn("Operation on intention prefix denied due to ACLs", s.logger.Debug("Operation on intention prefix denied due to ACLs",
"prefix", prefix, "prefix", prefix,
"accessorID", acl.AliasIfAnonymousToken(accessorID)) "accessorID", acl.AliasIfAnonymousToken(accessorID))
return err return err
@ -745,7 +745,7 @@ func (s *Intention) Check(args *structs.IntentionQueryRequest, reply *structs.In
query.FillAuthzContext(&authzContext) query.FillAuthzContext(&authzContext)
if err := authz.ToAllowAuthorizer().ServiceReadAllowed(prefix, &authzContext); err != nil { if err := authz.ToAllowAuthorizer().ServiceReadAllowed(prefix, &authzContext); err != nil {
accessorID := authz.AccessorID() accessorID := authz.AccessorID()
s.logger.Warn("test on intention denied due to ACLs", s.logger.Debug("test on intention denied due to ACLs",
"prefix", prefix, "prefix", prefix,
"accessorID", acl.AliasIfAnonymousToken(accessorID)) "accessorID", acl.AliasIfAnonymousToken(accessorID))
return err return err

View File

@ -1926,6 +1926,7 @@ func TestIntentionMatch_acl(t *testing.T) {
} }
var resp structs.IndexedIntentionMatches var resp structs.IndexedIntentionMatches
err := msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp) err := msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp)
require.Error(t, err)
require.True(t, acl.IsErrPermissionDenied(err)) require.True(t, acl.IsErrPermissionDenied(err))
require.Len(t, resp.Matches, 0) require.Len(t, resp.Matches, 0)
} }

View File

@ -740,7 +740,8 @@ type IntentionDecisionOpts struct {
Target string Target string
Namespace string Namespace string
Partition string Partition string
Intentions structs.Intentions Peer string
Intentions structs.SimplifiedIntentions
MatchType structs.IntentionMatchType MatchType structs.IntentionMatchType
DefaultDecision acl.EnforcementDecision DefaultDecision acl.EnforcementDecision
AllowPermissions bool AllowPermissions bool
@ -755,7 +756,7 @@ func (s *Store) IntentionDecision(opts IntentionDecisionOpts) (structs.Intention
// Figure out which source matches this request. // Figure out which source matches this request.
var ixnMatch *structs.Intention var ixnMatch *structs.Intention
for _, ixn := range opts.Intentions { for _, ixn := range opts.Intentions {
if _, ok := connect.AuthorizeIntentionTarget(opts.Target, opts.Namespace, opts.Partition, ixn, opts.MatchType); ok { if _, ok := connect.AuthorizeIntentionTarget(opts.Target, opts.Namespace, opts.Partition, opts.Peer, ixn, opts.MatchType); ok {
ixnMatch = ixn ixnMatch = ixn
break break
} }
@ -805,10 +806,39 @@ func (s *Store) IntentionMatch(ws memdb.WatchSet, args *structs.IntentionQueryMa
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
if !usingConfigEntries { if !usingConfigEntries {
return s.legacyIntentionMatchTxn(tx, ws, args) idx, ixnsList, err := s.legacyIntentionMatchTxn(tx, ws, args)
if err != nil {
return 0, nil, err
} }
return s.configIntentionMatchTxn(tx, ws, args)
return idx, ixnsList, nil
}
maxIdx, ixnsList, err := s.configIntentionMatchTxn(tx, ws, args)
if err != nil {
return 0, nil, err
}
if args.WithSamenessGroups {
return maxIdx, ixnsList, err
}
// Non-legacy intentions support sameness groups. We need to simplify them.
var out []structs.Intentions
for i, ixns := range ixnsList {
entry := args.Entries[i]
idx, simplifiedIxns, err := getSimplifiedIntentions(tx, ws, ixns, *entry.GetEnterpriseMeta())
if err != nil {
return 0, nil, err
}
if idx > maxIdx {
maxIdx = idx
}
out = append(out, simplifiedIxns)
}
return maxIdx, out, nil
} }
func (s *Store) legacyIntentionMatchTxn(tx ReadTxn, ws memdb.WatchSet, args *structs.IntentionQueryMatch) (uint64, []structs.Intentions, error) { func (s *Store) legacyIntentionMatchTxn(tx ReadTxn, ws memdb.WatchSet, args *structs.IntentionQueryMatch) (uint64, []structs.Intentions, error) {
@ -847,7 +877,7 @@ func (s *Store) IntentionMatchOne(
entry structs.IntentionMatchEntry, entry structs.IntentionMatchEntry,
matchType structs.IntentionMatchType, matchType structs.IntentionMatchType,
destinationType structs.IntentionTargetType, destinationType structs.IntentionTargetType,
) (uint64, structs.Intentions, error) { ) (uint64, structs.SimplifiedIntentions, error) {
tx := s.db.Txn(false) tx := s.db.Txn(false)
defer tx.Abort() defer tx.Abort()
@ -860,16 +890,34 @@ func compatIntentionMatchOneTxn(
entry structs.IntentionMatchEntry, entry structs.IntentionMatchEntry,
matchType structs.IntentionMatchType, matchType structs.IntentionMatchType,
destinationType structs.IntentionTargetType, destinationType structs.IntentionTargetType,
) (uint64, structs.Intentions, error) { ) (uint64, structs.SimplifiedIntentions, error) {
usingConfigEntries, err := areIntentionsInConfigEntries(tx, ws) usingConfigEntries, err := areIntentionsInConfigEntries(tx, ws)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
if !usingConfigEntries { if !usingConfigEntries {
return legacyIntentionMatchOneTxn(tx, ws, entry, matchType) idx, ixns, err := legacyIntentionMatchOneTxn(tx, ws, entry, matchType)
if err != nil {
return 0, nil, err
} }
return configIntentionMatchOneTxn(tx, ws, entry, matchType, destinationType) return idx, structs.SimplifiedIntentions(ixns), err
}
maxIdx, ixns, err := configIntentionMatchOneTxn(tx, ws, entry, matchType, destinationType)
if err != nil {
return 0, nil, err
}
idx, simplifiedIxns, err := getSimplifiedIntentions(tx, ws, ixns, *entry.GetEnterpriseMeta())
if err != nil {
return 0, nil, err
}
if idx > maxIdx {
maxIdx = idx
}
return maxIdx, structs.SimplifiedIntentions(simplifiedIxns), nil
} }
func legacyIntentionMatchOneTxn( func legacyIntentionMatchOneTxn(

View File

@ -10,9 +10,19 @@ import (
memdb "github.com/hashicorp/go-memdb" memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
) )
func intentionListTxn(tx ReadTxn, _ *acl.EnterpriseMeta) (memdb.ResultIterator, error) { func intentionListTxn(tx ReadTxn, _ *acl.EnterpriseMeta) (memdb.ResultIterator, error) {
// Get all intentions // Get all intentions
return tx.Get(tableConnectIntentions, "id") return tx.Get(tableConnectIntentions, "id")
} }
func getSimplifiedIntentions(
tx ReadTxn,
ws memdb.WatchSet,
ixns structs.Intentions,
entMeta acl.EnterpriseMeta,
) (uint64, structs.Intentions, error) {
return 0, ixns, nil
}

View File

@ -1271,14 +1271,14 @@ func TestStore_IntentionExact_ConfigEntries(t *testing.T) {
func TestStore_IntentionMatch_ConfigEntries(t *testing.T) { func TestStore_IntentionMatch_ConfigEntries(t *testing.T) {
type testcase struct { type testcase struct {
name string name string
input []*structs.ServiceIntentionsConfigEntry configEntries []structs.ConfigEntry
query structs.IntentionQueryMatch query structs.IntentionQueryMatch
expect []structs.Intentions expect []structs.Intentions
} }
run := func(t *testing.T, tc testcase) { run := func(t *testing.T, tc testcase) {
s := testConfigStateStore(t) s := testConfigStateStore(t)
idx := uint64(0) idx := uint64(0)
for _, conf := range tc.input { for _, conf := range tc.configEntries {
require.NoError(t, conf.Normalize()) require.NoError(t, conf.Normalize())
require.NoError(t, conf.Validate()) require.NoError(t, conf.Validate())
idx++ idx++
@ -1300,8 +1300,8 @@ func TestStore_IntentionMatch_ConfigEntries(t *testing.T) {
tcs := []testcase{ tcs := []testcase{
{ {
name: "peered intention matched with destination query", name: "peered intention matched with destination query",
input: []*structs.ServiceIntentionsConfigEntry{ configEntries: []structs.ConfigEntry{
{ &structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
Name: "foo", Name: "foo",
Sources: []*structs.SourceIntention{ Sources: []*structs.SourceIntention{
@ -1360,8 +1360,8 @@ func TestStore_IntentionMatch_ConfigEntries(t *testing.T) {
// This behavior may change in the future but this test is in place // This behavior may change in the future but this test is in place
// to ensure peered intentions cannot accidentally be queried by source // to ensure peered intentions cannot accidentally be queried by source
name: "peered intention cannot be queried by source", name: "peered intention cannot be queried by source",
input: []*structs.ServiceIntentionsConfigEntry{ configEntries: []structs.ConfigEntry{
{ &structs.ServiceIntentionsConfigEntry{
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
Name: "foo", Name: "foo",
Sources: []*structs.SourceIntention{ Sources: []*structs.SourceIntention{

View File

@ -42,6 +42,7 @@ type Store interface {
ExportedServicesForAllPeersByName(ws memdb.WatchSet, dc string, entMeta acl.EnterpriseMeta) (uint64, map[string]structs.ServiceList, error) ExportedServicesForAllPeersByName(ws memdb.WatchSet, dc string, entMeta acl.EnterpriseMeta) (uint64, map[string]structs.ServiceList, error)
FederationStateList(ws memdb.WatchSet) (uint64, []*structs.FederationState, error) FederationStateList(ws memdb.WatchSet) (uint64, []*structs.FederationState, error)
GatewayServices(ws memdb.WatchSet, gateway string, entMeta *acl.EnterpriseMeta) (uint64, structs.GatewayServices, error) GatewayServices(ws memdb.WatchSet, gateway string, entMeta *acl.EnterpriseMeta) (uint64, structs.GatewayServices, error)
IntentionMatchOne(ws memdb.WatchSet, entry structs.IntentionMatchEntry, matchType structs.IntentionMatchType, destinationType structs.IntentionTargetType) (uint64, structs.SimplifiedIntentions, error)
IntentionTopology(ws memdb.WatchSet, target structs.ServiceName, downstreams bool, defaultDecision acl.EnforcementDecision, intentionTarget structs.IntentionTargetType) (uint64, structs.ServiceList, error) IntentionTopology(ws memdb.WatchSet, target structs.ServiceName, downstreams bool, defaultDecision acl.EnforcementDecision, intentionTarget structs.IntentionTargetType) (uint64, structs.ServiceList, error)
ReadResolvedServiceConfigEntries(ws memdb.WatchSet, serviceName string, entMeta *acl.EnterpriseMeta, upstreamIDs []structs.ServiceID, proxyMode structs.ProxyMode) (uint64, *configentry.ResolvedServiceConfigSet, error) ReadResolvedServiceConfigEntries(ws memdb.WatchSet, serviceName string, entMeta *acl.EnterpriseMeta, upstreamIDs []structs.ServiceID, proxyMode structs.ProxyMode) (uint64, *configentry.ResolvedServiceConfigSet, error)
ServiceDiscoveryChain(ws memdb.WatchSet, serviceName string, entMeta *acl.EnterpriseMeta, req discoverychain.CompileRequest) (uint64, *structs.CompiledDiscoveryChain, *configentry.DiscoveryChainSet, error) ServiceDiscoveryChain(ws memdb.WatchSet, serviceName string, entMeta *acl.EnterpriseMeta, req discoverychain.CompileRequest) (uint64, *structs.CompiledDiscoveryChain, *configentry.DiscoveryChainSet, error)

View File

@ -5,17 +5,16 @@ package proxycfgglue
import ( import (
"context" "context"
"fmt"
"sort" "sort"
"sync"
"github.com/hashicorp/go-memdb"
"github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/agent/cache"
cachetype "github.com/hashicorp/consul/agent/cache-types" cachetype "github.com/hashicorp/consul/agent/cache-types"
"github.com/hashicorp/consul/agent/consul/watch"
"github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/proxycfg"
"github.com/hashicorp/consul/agent/rpcclient/configentry"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/submatview" "github.com/hashicorp/consul/agent/structs/aclfilter"
"github.com/hashicorp/consul/proto/private/pbsubscribe"
) )
// CacheIntentions satisfies the proxycfg.Intentions interface by sourcing data // CacheIntentions satisfies the proxycfg.Intentions interface by sourcing data
@ -28,17 +27,19 @@ type cacheIntentions struct {
c *cache.Cache c *cache.Cache
} }
func toIntentionMatchEntry(req *structs.ServiceSpecificRequest) structs.IntentionMatchEntry {
return structs.IntentionMatchEntry{
Partition: req.PartitionOrDefault(),
Namespace: req.NamespaceOrDefault(),
Name: req.ServiceName,
}
}
func (c cacheIntentions) Notify(ctx context.Context, req *structs.ServiceSpecificRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error { func (c cacheIntentions) Notify(ctx context.Context, req *structs.ServiceSpecificRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error {
query := &structs.IntentionQueryRequest{ query := &structs.IntentionQueryRequest{
Match: &structs.IntentionQueryMatch{ Match: &structs.IntentionQueryMatch{
Type: structs.IntentionMatchDestination, Type: structs.IntentionMatchDestination,
Entries: []structs.IntentionMatchEntry{ Entries: []structs.IntentionMatchEntry{toIntentionMatchEntry(req)},
{
Partition: req.PartitionOrDefault(),
Namespace: req.NamespaceOrDefault(),
Name: req.ServiceName,
},
},
}, },
QueryOptions: structs.QueryOptions{Token: req.QueryOptions.Token}, QueryOptions: structs.QueryOptions{Token: req.QueryOptions.Token},
} }
@ -50,9 +51,9 @@ func (c cacheIntentions) Notify(ctx context.Context, req *structs.ServiceSpecifi
return return
} }
var matches structs.Intentions var matches structs.SimplifiedIntentions
if len(rsp.Matches) != 0 { if len(rsp.Matches) != 0 {
matches = rsp.Matches[0] matches = structs.SimplifiedIntentions(rsp.Matches[0])
} }
result = matches result = matches
} }
@ -75,111 +76,29 @@ type serverIntentions struct {
} }
func (s *serverIntentions) Notify(ctx context.Context, req *structs.ServiceSpecificRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error { func (s *serverIntentions) Notify(ctx context.Context, req *structs.ServiceSpecificRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error {
// We may consume *multiple* streams (to handle wildcard intentions) and merge return watch.ServerLocalNotify(ctx, correlationID, s.deps.GetStore,
// them into a single list of intentions. func(ws memdb.WatchSet, store Store) (uint64, structs.SimplifiedIntentions, error) {
// authz, err := s.deps.ACLResolver.ResolveTokenAndDefaultMeta(req.Token, &req.EnterpriseMeta, nil)
// An alternative approach would be to consume events for all intentions and
// filter out the irrelevant ones. This would remove some complexity here but
// at the expense of significant overhead.
subjects := s.buildSubjects(req.ServiceName, req.EnterpriseMeta)
// mu guards state, as the callback functions provided in NotifyCallback below
// will be called in different goroutines.
var mu sync.Mutex
state := make([]*structs.ConfigEntryResponse, len(subjects))
// buildEvent constructs an event containing the matching intentions received
// from NotifyCallback calls below. If we have not received initial snapshots
// for all streams yet, the event will be empty and the second return value will
// be false (causing no event to be emittied).
//
// Note: mu must be held when calling this function.
buildEvent := func() (proxycfg.UpdateEvent, bool) {
intentions := make(structs.Intentions, 0)
for _, result := range state {
if result == nil {
return proxycfg.UpdateEvent{}, false
}
si, ok := result.Entry.(*structs.ServiceIntentionsConfigEntry)
if !ok {
continue
}
intentions = append(intentions, si.ToIntentions()...)
}
sort.Sort(structs.IntentionPrecedenceSorter(intentions))
return newUpdateEvent(correlationID, intentions, nil), true
}
for subjectIdx, subject := range subjects {
subjectIdx := subjectIdx
storeReq := intentionsRequest{
deps: s.deps,
baseReq: req,
subject: subject,
}
err := s.deps.ViewStore.NotifyCallback(ctx, storeReq, correlationID, func(ctx context.Context, cacheEvent cache.UpdateEvent) {
mu.Lock()
state[subjectIdx] = cacheEvent.Result.(*structs.ConfigEntryResponse)
event, ready := buildEvent()
mu.Unlock()
if ready {
select {
case ch <- event:
case <-ctx.Done():
}
}
})
if err != nil { if err != nil {
return err return 0, nil, err
} }
match := toIntentionMatchEntry(req)
index, ixns, err := store.IntentionMatchOne(ws, match, structs.IntentionMatchDestination, structs.IntentionTargetService)
if err != nil {
return 0, nil, err
} }
return nil indexedIntentions := &structs.IndexedIntentions{
Intentions: structs.Intentions(ixns),
} }
type intentionsRequest struct { aclfilter.New(authz, s.deps.Logger).Filter(indexedIntentions)
deps ServerDataSourceDeps
baseReq *structs.ServiceSpecificRequest
subject *pbsubscribe.NamedSubject
}
func (r intentionsRequest) CacheInfo() cache.RequestInfo { sort.Sort(structs.IntentionPrecedenceSorter(indexedIntentions.Intentions))
info := r.baseReq.CacheInfo()
info.Key = fmt.Sprintf("%s/%s/%s/%s",
r.subject.PeerName,
r.subject.Partition,
r.subject.Namespace,
r.subject.Key,
)
return info
}
func (r intentionsRequest) NewMaterializer() (submatview.Materializer, error) { return index, structs.SimplifiedIntentions(indexedIntentions.Intentions), nil
return submatview.NewLocalMaterializer(submatview.LocalMaterializerDeps{
Backend: r.deps.EventPublisher,
ACLResolver: r.deps.ACLResolver,
Deps: submatview.Deps{
View: &configentry.ConfigEntryView{},
Logger: r.deps.Logger,
Request: r.Request,
}, },
}), nil dispatchBlockingQueryUpdate[structs.SimplifiedIntentions](ch),
)
} }
func (r intentionsRequest) Request(index uint64) *pbsubscribe.SubscribeRequest {
return &pbsubscribe.SubscribeRequest{
Topic: pbsubscribe.Topic_ServiceIntentions,
Index: index,
Datacenter: r.baseReq.Datacenter,
Token: r.baseReq.Token,
Subject: &pbsubscribe.SubscribeRequest_NamedSubject{NamedSubject: r.subject},
}
}
func (r intentionsRequest) Type() string { return "proxycfgglue.ServiceIntentions" }

View File

@ -1,83 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package proxycfgglue
import (
"context"
"testing"
"time"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/consul/stream"
"github.com/hashicorp/consul/agent/proxycfg"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/submatview"
"github.com/hashicorp/consul/proto/private/pbsubscribe"
"github.com/hashicorp/consul/sdk/testutil"
)
func TestServerIntentions_Enterprise(t *testing.T) {
// This test asserts that we also subscribe to the wildcard namespace intention.
const (
serviceName = "web"
index = 1
)
logger := hclog.NewNullLogger()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
store := submatview.NewStore(logger)
go store.Run(ctx)
publisher := stream.NewEventPublisher(10 * time.Second)
publisher.RegisterHandler(pbsubscribe.Topic_ServiceIntentions,
func(stream.SubscribeRequest, stream.SnapshotAppender) (uint64, error) { return index, nil },
false)
go publisher.Run(ctx)
intentions := ServerIntentions(ServerDataSourceDeps{
ACLResolver: newStaticResolver(acl.ManageAll()),
ViewStore: store,
EventPublisher: publisher,
Logger: logger,
})
eventCh := make(chan proxycfg.UpdateEvent)
require.NoError(t, intentions.Notify(ctx, &structs.ServiceSpecificRequest{
EnterpriseMeta: *acl.DefaultEnterpriseMeta(),
ServiceName: serviceName,
}, "", eventCh))
testutil.RunStep(t, "initial snapshot", func(t *testing.T) {
getEventResult[structs.Intentions](t, eventCh)
})
testutil.RunStep(t, "publish a namespace-wildcard partition", func(t *testing.T) {
publisher.Publish([]stream.Event{
{
Topic: pbsubscribe.Topic_ServiceIntentions,
Index: index + 1,
Payload: state.EventPayloadConfigEntry{
Op: pbsubscribe.ConfigEntryUpdate_Upsert,
Value: &structs.ServiceIntentionsConfigEntry{
Name: structs.WildcardSpecifier,
EnterpriseMeta: *acl.WildcardEnterpriseMeta(),
Sources: []*structs.SourceIntention{
{Name: structs.WildcardSpecifier, Action: structs.IntentionActionAllow, Precedence: 1},
},
},
},
},
})
result := getEventResult[structs.Intentions](t, eventCh)
require.Len(t, result, 1)
})
}

View File

@ -7,7 +7,6 @@ import (
"context" "context"
"sync" "sync"
"testing" "testing"
"time"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -15,39 +14,47 @@ import (
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/acl/resolver" "github.com/hashicorp/consul/acl/resolver"
"github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/consul/stream"
"github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/proxycfg"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/submatview"
"github.com/hashicorp/consul/proto/private/pbsubscribe"
"github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil"
) )
func TestServerIntentions(t *testing.T) { func TestServerIntentions(t *testing.T) {
const ( nextIndex := indexGenerator()
serviceName = "web"
index = 1
)
logger := hclog.NewNullLogger()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel) t.Cleanup(cancel)
store := submatview.NewStore(logger) store := state.NewStateStore(nil)
go store.Run(ctx)
publisher := stream.NewEventPublisher(10 * time.Second) const (
publisher.RegisterHandler(pbsubscribe.Topic_ServiceIntentions, serviceName = "web"
func(stream.SubscribeRequest, stream.SnapshotAppender) (uint64, error) { return index, nil }, index = 1
false) )
go publisher.Run(ctx) require.NoError(t, store.SystemMetadataSet(1, &structs.SystemMetadataEntry{
Key: structs.SystemMetadataIntentionFormatKey,
Value: structs.SystemMetadataIntentionFormatConfigValue,
}))
require.NoError(t, store.EnsureConfigEntry(nextIndex(), &structs.ServiceIntentionsConfigEntry{
Name: serviceName,
Sources: []*structs.SourceIntention{
{
Name: "db",
Action: structs.IntentionActionAllow,
},
},
}))
authz := policyAuthorizer(t, `
service "web" { policy = "read" }
`)
logger := hclog.NewNullLogger()
intentions := ServerIntentions(ServerDataSourceDeps{ intentions := ServerIntentions(ServerDataSourceDeps{
ACLResolver: newStaticResolver(acl.ManageAll()), ACLResolver: newStaticResolver(authz),
ViewStore: store,
EventPublisher: publisher,
Logger: logger, Logger: logger,
GetStore: func() Store { return store },
}) })
eventCh := make(chan proxycfg.UpdateEvent) eventCh := make(chan proxycfg.UpdateEvent)
@ -57,27 +64,7 @@ func TestServerIntentions(t *testing.T) {
}, "", eventCh)) }, "", eventCh))
testutil.RunStep(t, "initial snapshot", func(t *testing.T) { testutil.RunStep(t, "initial snapshot", func(t *testing.T) {
getEventResult[structs.Intentions](t, eventCh) result := getEventResult[structs.SimplifiedIntentions](t, eventCh)
})
testutil.RunStep(t, "publishing an explicit intention", func(t *testing.T) {
publisher.Publish([]stream.Event{
{
Topic: pbsubscribe.Topic_ServiceIntentions,
Index: index + 1,
Payload: state.EventPayloadConfigEntry{
Op: pbsubscribe.ConfigEntryUpdate_Upsert,
Value: &structs.ServiceIntentionsConfigEntry{
Name: serviceName,
Sources: []*structs.SourceIntention{
{Name: "db", Action: structs.IntentionActionAllow, Precedence: 1},
},
},
},
},
})
result := getEventResult[structs.Intentions](t, eventCh)
require.Len(t, result, 1) require.Len(t, result, 1)
intention := result[0] intention := result[0]
@ -85,53 +72,85 @@ func TestServerIntentions(t *testing.T) {
require.Equal(t, intention.SourceName, "db") require.Equal(t, intention.SourceName, "db")
}) })
testutil.RunStep(t, "publishing a wildcard intention", func(t *testing.T) { testutil.RunStep(t, "updating an intention", func(t *testing.T) {
publisher.Publish([]stream.Event{ require.NoError(t, store.EnsureConfigEntry(nextIndex(), &structs.ServiceIntentionsConfigEntry{
{ Name: serviceName,
Topic: pbsubscribe.Topic_ServiceIntentions,
Index: index + 2,
Payload: state.EventPayloadConfigEntry{
Op: pbsubscribe.ConfigEntryUpdate_Upsert,
Value: &structs.ServiceIntentionsConfigEntry{
Name: structs.WildcardSpecifier,
Sources: []*structs.SourceIntention{ Sources: []*structs.SourceIntention{
{Name: structs.WildcardSpecifier, Action: structs.IntentionActionAllow, Precedence: 0}, {
Name: "api",
Action: structs.IntentionActionAllow,
},
{
Name: "db",
Action: structs.IntentionActionAllow,
}, },
}, },
}, }))
},
})
result := getEventResult[structs.Intentions](t, eventCh) result := getEventResult[structs.SimplifiedIntentions](t, eventCh)
require.Len(t, result, 2) require.Len(t, result, 2)
a := result[0] for i, src := range []string{"api", "db"} {
require.Equal(t, a.DestinationName, serviceName) intention := result[i]
require.Equal(t, a.SourceName, "db") require.Equal(t, intention.DestinationName, serviceName)
require.Equal(t, intention.SourceName, src)
b := result[1] }
require.Equal(t, b.DestinationName, structs.WildcardSpecifier)
require.Equal(t, b.SourceName, structs.WildcardSpecifier)
}) })
testutil.RunStep(t, "publishing a delete event", func(t *testing.T) { testutil.RunStep(t, "publishing a delete event", func(t *testing.T) {
publisher.Publish([]stream.Event{ require.NoError(t, store.DeleteConfigEntry(nextIndex(), structs.ServiceIntentions, serviceName, nil))
{
Topic: pbsubscribe.Topic_ServiceIntentions, result := getEventResult[structs.SimplifiedIntentions](t, eventCh)
Index: index + 3, require.Len(t, result, 0)
Payload: state.EventPayloadConfigEntry{ })
Op: pbsubscribe.ConfigEntryUpdate_Delete, }
Value: &structs.ServiceIntentionsConfigEntry{
func TestServerIntentions_ACLDeny(t *testing.T) {
nextIndex := indexGenerator()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
store := state.NewStateStore(nil)
const (
serviceName = "web"
index = 1
)
require.NoError(t, store.SystemMetadataSet(1, &structs.SystemMetadataEntry{
Key: structs.SystemMetadataIntentionFormatKey,
Value: structs.SystemMetadataIntentionFormatConfigValue,
}))
require.NoError(t, store.EnsureConfigEntry(nextIndex(), &structs.ServiceIntentionsConfigEntry{
Name: serviceName, Name: serviceName,
Sources: []*structs.SourceIntention{
{
Name: "db",
Action: structs.IntentionActionAllow,
}, },
}, },
}, }))
authz := policyAuthorizer(t, ``)
logger := hclog.NewNullLogger()
intentions := ServerIntentions(ServerDataSourceDeps{
ACLResolver: newStaticResolver(authz),
Logger: logger,
GetStore: func() Store { return store },
}) })
result := getEventResult[structs.Intentions](t, eventCh) eventCh := make(chan proxycfg.UpdateEvent)
require.Len(t, result, 1) require.NoError(t, intentions.Notify(ctx, &structs.ServiceSpecificRequest{
}) ServiceName: serviceName,
EnterpriseMeta: *acl.DefaultEnterpriseMeta(),
}, "", eventCh))
testutil.RunStep(t, "initial snapshot", func(t *testing.T) {
result := getEventResult[structs.SimplifiedIntentions](t, eventCh)
require.Len(t, result, 0)
})
} }
type staticResolver struct { type staticResolver struct {

View File

@ -313,7 +313,7 @@ func (s *handlerConnectProxy) handleUpdate(ctx context.Context, u UpdateEvent, s
snap.ConnectProxy.InboundPeerTrustBundlesSet = true snap.ConnectProxy.InboundPeerTrustBundlesSet = true
case u.CorrelationID == intentionsWatchID: case u.CorrelationID == intentionsWatchID:
resp, ok := u.Result.(structs.Intentions) resp, ok := u.Result.(structs.SimplifiedIntentions)
if !ok { if !ok {
return fmt.Errorf("invalid type for response: %T", u.Result) return fmt.Errorf("invalid type for response: %T", u.Result)
} }

View File

@ -643,7 +643,7 @@ func TestManager_SyncState_No_Notify(t *testing.T) {
// update the intentions // update the intentions
notifyCH <- UpdateEvent{ notifyCH <- UpdateEvent{
CorrelationID: intentionsWatchID, CorrelationID: intentionsWatchID,
Result: structs.Intentions{}, Result: structs.SimplifiedIntentions{},
Err: nil, Err: nil,
} }

View File

@ -631,9 +631,9 @@ func (o *configSnapshotTerminatingGateway) DeepCopy() *configSnapshotTerminating
} }
} }
if o.Intentions != nil { if o.Intentions != nil {
cp.Intentions = make(map[structs.ServiceName]structs.Intentions, len(o.Intentions)) cp.Intentions = make(map[structs.ServiceName]structs.SimplifiedIntentions, len(o.Intentions))
for k2, v2 := range o.Intentions { for k2, v2 := range o.Intentions {
var cp_Intentions_v2 structs.Intentions var cp_Intentions_v2 structs.SimplifiedIntentions
if v2 != nil { if v2 != nil {
cp_Intentions_v2 = make([]*structs.Intention, len(v2)) cp_Intentions_v2 = make([]*structs.Intention, len(v2))
copy(cp_Intentions_v2, v2) copy(cp_Intentions_v2, v2)

View File

@ -153,7 +153,7 @@ type configSnapshotConnectProxy struct {
// NOTE: Intentions stores a list of lists as returned by the Intentions // NOTE: Intentions stores a list of lists as returned by the Intentions
// Match RPC. So far we only use the first list as the list of matching // Match RPC. So far we only use the first list as the list of matching
// intentions. // intentions.
Intentions structs.Intentions Intentions structs.SimplifiedIntentions
IntentionsSet bool IntentionsSet bool
DestinationsUpstream watch.Map[UpstreamID, *structs.ServiceConfigEntry] DestinationsUpstream watch.Map[UpstreamID, *structs.ServiceConfigEntry]
@ -230,7 +230,7 @@ type configSnapshotTerminatingGateway struct {
// //
// A key being present implies that we have gotten at least one watch reply for the // A key being present implies that we have gotten at least one watch reply for the
// service. This is logically the same as ConnectProxy.IntentionsSet==true // service. This is logically the same as ConnectProxy.IntentionsSet==true
Intentions map[structs.ServiceName]structs.Intentions Intentions map[structs.ServiceName]structs.SimplifiedIntentions
// WatchedLeaves is a map of ServiceName to a cancel function. // WatchedLeaves is a map of ServiceName to a cancel function.
// This cancel function is tied to the watch of leaf certs for linked services. // This cancel function is tied to the watch of leaf certs for linked services.

View File

@ -839,7 +839,7 @@ func TestState_WatchesAndUpdates(t *testing.T) {
} }
} }
dbIxnMatch := structs.Intentions{ dbIxnMatch := structs.SimplifiedIntentions{
{ {
ID: "abc-123", ID: "abc-123",
SourceNS: "default", SourceNS: "default",

View File

@ -56,7 +56,7 @@ func (s *handlerTerminatingGateway) initialize(ctx context.Context) (ConfigSnaps
snap.TerminatingGateway.WatchedServices = make(map[structs.ServiceName]context.CancelFunc) snap.TerminatingGateway.WatchedServices = make(map[structs.ServiceName]context.CancelFunc)
snap.TerminatingGateway.WatchedIntentions = make(map[structs.ServiceName]context.CancelFunc) snap.TerminatingGateway.WatchedIntentions = make(map[structs.ServiceName]context.CancelFunc)
snap.TerminatingGateway.Intentions = make(map[structs.ServiceName]structs.Intentions) snap.TerminatingGateway.Intentions = make(map[structs.ServiceName]structs.SimplifiedIntentions)
snap.TerminatingGateway.WatchedLeaves = make(map[structs.ServiceName]context.CancelFunc) snap.TerminatingGateway.WatchedLeaves = make(map[structs.ServiceName]context.CancelFunc)
snap.TerminatingGateway.ServiceLeaves = make(map[structs.ServiceName]*structs.IssuedCert) snap.TerminatingGateway.ServiceLeaves = make(map[structs.ServiceName]*structs.IssuedCert)
snap.TerminatingGateway.WatchedConfigs = make(map[structs.ServiceName]context.CancelFunc) snap.TerminatingGateway.WatchedConfigs = make(map[structs.ServiceName]context.CancelFunc)
@ -366,7 +366,7 @@ func (s *handlerTerminatingGateway) handleUpdate(ctx context.Context, u UpdateEv
} }
case strings.HasPrefix(u.CorrelationID, serviceIntentionsIDPrefix): case strings.HasPrefix(u.CorrelationID, serviceIntentionsIDPrefix):
resp, ok := u.Result.(structs.Intentions) resp, ok := u.Result.(structs.SimplifiedIntentions)
if !ok { if !ok {
return fmt.Errorf("invalid type for response: %T", u.Result) return fmt.Errorf("invalid type for response: %T", u.Result)
} }

View File

@ -141,8 +141,8 @@ func TestMeshGatewayLeafForCA(t testing.T, ca *structs.CARoot) *structs.IssuedCe
// TestIntentions returns a sample intentions match result useful to // TestIntentions returns a sample intentions match result useful to
// mocking service discovery cache results. // mocking service discovery cache results.
func TestIntentions() structs.Intentions { func TestIntentions() structs.SimplifiedIntentions {
return structs.Intentions{ return structs.SimplifiedIntentions{
{ {
ID: "foo", ID: "foo",
SourceNS: "default", SourceNS: "default",
@ -950,7 +950,7 @@ func NewTestDataSources() *TestDataSources {
GatewayServices: NewTestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedGatewayServices](), GatewayServices: NewTestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedGatewayServices](),
Health: NewTestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedCheckServiceNodes](), Health: NewTestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedCheckServiceNodes](),
HTTPChecks: NewTestDataSource[*cachetype.ServiceHTTPChecksRequest, []structs.CheckType](), HTTPChecks: NewTestDataSource[*cachetype.ServiceHTTPChecksRequest, []structs.CheckType](),
Intentions: NewTestDataSource[*structs.ServiceSpecificRequest, structs.Intentions](), Intentions: NewTestDataSource[*structs.ServiceSpecificRequest, structs.SimplifiedIntentions](),
IntentionUpstreams: NewTestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceList](), IntentionUpstreams: NewTestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceList](),
IntentionUpstreamsDestination: NewTestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceList](), IntentionUpstreamsDestination: NewTestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceList](),
InternalServiceDump: NewTestDataSource[*structs.ServiceDumpRequest, *structs.IndexedCheckServiceNodes](), InternalServiceDump: NewTestDataSource[*structs.ServiceDumpRequest, *structs.IndexedCheckServiceNodes](),
@ -977,7 +977,7 @@ type TestDataSources struct {
ServiceGateways *TestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceNodes] ServiceGateways *TestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceNodes]
Health *TestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedCheckServiceNodes] Health *TestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedCheckServiceNodes]
HTTPChecks *TestDataSource[*cachetype.ServiceHTTPChecksRequest, []structs.CheckType] HTTPChecks *TestDataSource[*cachetype.ServiceHTTPChecksRequest, []structs.CheckType]
Intentions *TestDataSource[*structs.ServiceSpecificRequest, structs.Intentions] Intentions *TestDataSource[*structs.ServiceSpecificRequest, structs.SimplifiedIntentions]
IntentionUpstreams *TestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceList] IntentionUpstreams *TestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceList]
IntentionUpstreamsDestination *TestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceList] IntentionUpstreamsDestination *TestDataSource[*structs.ServiceSpecificRequest, *structs.IndexedServiceList]
InternalServiceDump *TestDataSource[*structs.ServiceDumpRequest, *structs.IndexedCheckServiceNodes] InternalServiceDump *TestDataSource[*structs.ServiceDumpRequest, *structs.IndexedCheckServiceNodes]

View File

@ -48,7 +48,7 @@ func TestConfigSnapshot(t testing.T, nsFn func(ns *structs.NodeService), extraUp
}, },
{ {
CorrelationID: intentionsWatchID, CorrelationID: intentionsWatchID,
Result: structs.Intentions{}, // no intentions defined Result: structs.SimplifiedIntentions{}, // no intentions defined
}, },
{ {
CorrelationID: svcChecksWatchIDPrefix + webSN, CorrelationID: svcChecksWatchIDPrefix + webSN,
@ -129,7 +129,7 @@ func TestConfigSnapshotDiscoveryChain(
}, },
{ {
CorrelationID: intentionsWatchID, CorrelationID: intentionsWatchID,
Result: structs.Intentions{}, // no intentions defined Result: structs.SimplifiedIntentions{}, // no intentions defined
}, },
{ {
CorrelationID: meshConfigEntryID, CorrelationID: meshConfigEntryID,
@ -188,7 +188,7 @@ func TestConfigSnapshotExposeConfig(t testing.T, nsFn func(ns *structs.NodeServi
}, },
{ {
CorrelationID: intentionsWatchID, CorrelationID: intentionsWatchID,
Result: structs.Intentions{}, // no intentions defined Result: structs.SimplifiedIntentions{}, // no intentions defined
}, },
{ {
CorrelationID: svcChecksWatchIDPrefix + webSN, CorrelationID: svcChecksWatchIDPrefix + webSN,
@ -293,7 +293,7 @@ func TestConfigSnapshotGRPCExposeHTTP1(t testing.T) *ConfigSnapshot {
}, },
{ {
CorrelationID: intentionsWatchID, CorrelationID: intentionsWatchID,
Result: structs.Intentions{}, // no intentions defined Result: structs.SimplifiedIntentions{}, // no intentions defined
}, },
{ {
CorrelationID: svcChecksWatchIDPrefix + structs.ServiceIDString("grpc", nil), CorrelationID: svcChecksWatchIDPrefix + structs.ServiceIDString("grpc", nil),

View File

@ -210,19 +210,19 @@ func TestConfigSnapshotTerminatingGateway(t testing.T, populateServices bool, ns
// no intentions defined for these services // no intentions defined for these services
{ {
CorrelationID: serviceIntentionsIDPrefix + web.String(), CorrelationID: serviceIntentionsIDPrefix + web.String(),
Result: structs.Intentions{}, Result: structs.SimplifiedIntentions{},
}, },
{ {
CorrelationID: serviceIntentionsIDPrefix + api.String(), CorrelationID: serviceIntentionsIDPrefix + api.String(),
Result: structs.Intentions{}, Result: structs.SimplifiedIntentions{},
}, },
{ {
CorrelationID: serviceIntentionsIDPrefix + db.String(), CorrelationID: serviceIntentionsIDPrefix + db.String(),
Result: structs.Intentions{}, Result: structs.SimplifiedIntentions{},
}, },
{ {
CorrelationID: serviceIntentionsIDPrefix + cache.String(), CorrelationID: serviceIntentionsIDPrefix + cache.String(),
Result: structs.Intentions{}, Result: structs.SimplifiedIntentions{},
}, },
// ======== // ========
{ {
@ -390,23 +390,23 @@ func TestConfigSnapshotTerminatingGatewayDestinations(t testing.T, populateDesti
// no intentions defined for these services // no intentions defined for these services
{ {
CorrelationID: serviceIntentionsIDPrefix + externalIPTCP.String(), CorrelationID: serviceIntentionsIDPrefix + externalIPTCP.String(),
Result: structs.Intentions{}, Result: structs.SimplifiedIntentions{},
}, },
{ {
CorrelationID: serviceIntentionsIDPrefix + externalHostnameTCP.String(), CorrelationID: serviceIntentionsIDPrefix + externalHostnameTCP.String(),
Result: structs.Intentions{}, Result: structs.SimplifiedIntentions{},
}, },
{ {
CorrelationID: serviceIntentionsIDPrefix + externalIPHTTP.String(), CorrelationID: serviceIntentionsIDPrefix + externalIPHTTP.String(),
Result: structs.Intentions{}, Result: structs.SimplifiedIntentions{},
}, },
{ {
CorrelationID: serviceIntentionsIDPrefix + externalHostnameHTTP.String(), CorrelationID: serviceIntentionsIDPrefix + externalHostnameHTTP.String(),
Result: structs.Intentions{}, Result: structs.SimplifiedIntentions{},
}, },
{ {
CorrelationID: serviceIntentionsIDPrefix + externalHostnameWithSNI.String(), CorrelationID: serviceIntentionsIDPrefix + externalHostnameWithSNI.String(),
Result: structs.Intentions{}, Result: structs.SimplifiedIntentions{},
}, },
// ======== // ========
{ {

View File

@ -63,6 +63,9 @@ func (f *Filter) Filter(subject any) {
case *structs.IndexedIntentions: case *structs.IndexedIntentions:
v.QueryMeta.ResultsFilteredByACLs = f.filterIntentions(&v.Intentions) v.QueryMeta.ResultsFilteredByACLs = f.filterIntentions(&v.Intentions)
case *structs.IntentionQueryMatch:
f.filterIntentionMatch(v)
case *structs.IndexedNodeDump: case *structs.IndexedNodeDump:
if f.filterNodeDump(&v.Dump) { if f.filterNodeDump(&v.Dump) {
v.QueryMeta.ResultsFilteredByACLs = true v.QueryMeta.ResultsFilteredByACLs = true
@ -440,6 +443,26 @@ func (f *Filter) filterIntentions(ixns *structs.Intentions) bool {
return removed return removed
} }
// filterIntentionMatch filters IntentionQueryMatch to only exclude all
// matches when the user doesn't have access to any match.
func (f *Filter) filterIntentionMatch(args *structs.IntentionQueryMatch) {
var authzContext acl.AuthorizerContext
authz := f.authorizer.ToAllowAuthorizer()
for _, entry := range args.Entries {
entry.FillAuthzContext(&authzContext)
if prefix := entry.Name; prefix != "" {
if err := authz.IntentionReadAllowed(prefix, &authzContext); err != nil {
accessorID := authz.AccessorID
f.logger.Warn("Operation on intention prefix denied due to ACLs",
"prefix", prefix,
"accessorID", acl.AliasIfAnonymousToken(accessorID))
args.Entries = nil
return
}
}
}
}
// filterNodeDump is used to filter through all parts of a node dump and // filterNodeDump is used to filter through all parts of a node dump and
// remove elements the provided ACL token cannot access. Returns true if // remove elements the provided ACL token cannot access. Returns true if
// any elements were removed. // any elements were removed.

View File

@ -134,6 +134,7 @@ func (e *ServiceIntentionsConfigEntry) ToIntention(src *SourceIntention) *Intent
ID: src.LegacyID, ID: src.LegacyID,
Description: src.Description, Description: src.Description,
SourcePeer: src.Peer, SourcePeer: src.Peer,
SourceSamenessGroup: src.SamenessGroup,
SourcePartition: src.PartitionOrEmpty(), SourcePartition: src.PartitionOrEmpty(),
SourceNS: src.NamespaceOrDefault(), SourceNS: src.NamespaceOrDefault(),
SourceName: src.Name, SourceName: src.Name,
@ -274,6 +275,9 @@ type SourceIntention struct {
// Peer is the name of the remote peer of the source service, if applicable. // Peer is the name of the remote peer of the source service, if applicable.
Peer string `json:",omitempty"` Peer string `json:",omitempty"`
// SamenessGroup is the name of the sameness group, if applicable.
SamenessGroup string `json:",omitempty"`
} }
type IntentionJWTRequirement struct { type IntentionJWTRequirement struct {
@ -528,13 +532,13 @@ func (e *ServiceIntentionsConfigEntry) normalize(legacyWrite bool) error {
// Normalize the source's namespace and partition. // Normalize the source's namespace and partition.
// If the source is not peered, it inherits the destination's // If the source is not peered, it inherits the destination's
// EnterpriseMeta. // EnterpriseMeta.
if src.Peer == "" { if src.Peer != "" || src.SamenessGroup != "" {
// If the source is peered or a sameness group, normalize the namespace only,
// since they are mutually exclusive with partition.
src.EnterpriseMeta.NormalizeNamespace()
} else {
src.EnterpriseMeta.MergeNoWildcard(&e.EnterpriseMeta) src.EnterpriseMeta.MergeNoWildcard(&e.EnterpriseMeta)
src.EnterpriseMeta.Normalize() src.EnterpriseMeta.Normalize()
} else {
// If the source is peered, normalize the namespace only,
// since peer is mutually exclusive with partition.
src.EnterpriseMeta.NormalizeNamespace()
} }
// Compute the precedence only AFTER normalizing namespaces since the // Compute the precedence only AFTER normalizing namespaces since the
@ -651,7 +655,7 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
return fmt.Errorf("Name is required") return fmt.Errorf("Name is required")
} }
if err := validateIntentionWildcards(e.Name, &e.EnterpriseMeta, ""); err != nil { if err := validateIntentionWildcards(e.Name, &e.EnterpriseMeta, "", ""); err != nil {
return err return err
} }
@ -677,13 +681,23 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
return fmt.Errorf("At least one source is required") return fmt.Errorf("At least one source is required")
} }
seenSources := make(map[PeeredServiceName]struct{}) type qualifiedServiceName struct {
ServiceName ServiceName
Peer string
SamenessGroup string
}
seenSources := make(map[qualifiedServiceName]struct{})
for i, src := range e.Sources { for i, src := range e.Sources {
if src.Name == "" { if src.Name == "" {
return fmt.Errorf("Sources[%d].Name is required", i) return fmt.Errorf("Sources[%d].Name is required", i)
} }
if err := validateIntentionWildcards(src.Name, &src.EnterpriseMeta, src.Peer); err != nil { if err := src.validateSamenessGroup(); err != nil {
return fmt.Errorf("Sources[%d].SamenessGroup: %v ", i, err)
}
if err := validateIntentionWildcards(src.Name, &src.EnterpriseMeta, src.Peer, src.SamenessGroup); err != nil {
return fmt.Errorf("Sources[%d].%v", i, err) return fmt.Errorf("Sources[%d].%v", i, err)
} }
@ -695,6 +709,14 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
return fmt.Errorf("Sources[%d].Peer: cannot set Peer and Partition at the same time.", i) return fmt.Errorf("Sources[%d].Peer: cannot set Peer and Partition at the same time.", i)
} }
if src.SamenessGroup != "" && src.PartitionOrEmpty() != "" {
return fmt.Errorf("Sources[%d].SamenessGroup: cannot set SamenessGroup and Partition at the same time", i)
}
if src.SamenessGroup != "" && src.Peer != "" {
return fmt.Errorf("Sources[%d].SamenessGroup: cannot set SamenessGroup and Peer at the same time", i)
}
// Length of opaque values // Length of opaque values
if len(src.Description) > metaValueMaxLength { if len(src.Description) > metaValueMaxLength {
return fmt.Errorf( return fmt.Errorf(
@ -706,6 +728,10 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
return fmt.Errorf("Sources[%d].Peer cannot be set by legacy intentions", i) return fmt.Errorf("Sources[%d].Peer cannot be set by legacy intentions", i)
} }
if src.SamenessGroup != "" {
return fmt.Errorf("Sources[%d].SamenessGroup cannot be set by legacy intentions", i)
}
if len(src.LegacyMeta) > metaMaxKeyPairs { if len(src.LegacyMeta) > metaMaxKeyPairs {
return fmt.Errorf( return fmt.Errorf(
"Sources[%d].Meta exceeds maximum element count %d", i, metaMaxKeyPairs) "Sources[%d].Meta exceeds maximum element count %d", i, metaMaxKeyPairs)
@ -869,22 +895,24 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
} }
} }
psn := PeeredServiceName{Peer: src.Peer, ServiceName: src.SourceServiceName()} qsn := qualifiedServiceName{Peer: src.Peer, SamenessGroup: src.SamenessGroup, ServiceName: src.SourceServiceName()}
if _, exists := seenSources[psn]; exists { if _, exists := seenSources[qsn]; exists {
if psn.Peer != "" { if qsn.Peer != "" {
return fmt.Errorf("Sources[%d] defines peer(%q) %q more than once", i, psn.Peer, psn.ServiceName.String()) return fmt.Errorf("Sources[%d] defines peer(%q) %q more than once", i, qsn.Peer, qsn.ServiceName.String())
} else if qsn.SamenessGroup != "" {
return fmt.Errorf("Sources[%d] defines sameness-group(%q) %q more than once", i, qsn.SamenessGroup, qsn.ServiceName.String())
} else { } else {
return fmt.Errorf("Sources[%d] defines %q more than once", i, psn.ServiceName.String()) return fmt.Errorf("Sources[%d] defines %q more than once", i, qsn.ServiceName.String())
} }
} }
seenSources[psn] = struct{}{} seenSources[qsn] = struct{}{}
} }
return nil return nil
} }
// Wildcard usage verification // Wildcard usage verification
func validateIntentionWildcards(name string, entMeta *acl.EnterpriseMeta, peerName string) error { func validateIntentionWildcards(name string, entMeta *acl.EnterpriseMeta, peerName, samenessGroup string) error {
ns := entMeta.NamespaceOrDefault() ns := entMeta.NamespaceOrDefault()
if ns != WildcardSpecifier { if ns != WildcardSpecifier {
if strings.Contains(ns, WildcardSpecifier) { if strings.Contains(ns, WildcardSpecifier) {
@ -906,6 +934,9 @@ func validateIntentionWildcards(name string, entMeta *acl.EnterpriseMeta, peerNa
if strings.Contains(peerName, WildcardSpecifier) { if strings.Contains(peerName, WildcardSpecifier) {
return fmt.Errorf("Peer: cannot use wildcard '*' in peer") return fmt.Errorf("Peer: cannot use wildcard '*' in peer")
} }
if strings.Contains(samenessGroup, WildcardSpecifier) {
return fmt.Errorf("SamenessGroup: cannot use wildcard '*' in sameness group")
}
return nil return nil
} }

View File

@ -7,9 +7,19 @@
package structs package structs
import ( import (
"fmt"
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
) )
func validateSourceIntentionEnterpriseMeta(_, _ *acl.EnterpriseMeta) error { func validateSourceIntentionEnterpriseMeta(_, _ *acl.EnterpriseMeta) error {
return nil return nil
} }
func (s *SourceIntention) validateSamenessGroup() error {
if s.SamenessGroup != "" {
return fmt.Errorf("Sameness groups are a Consul Enterprise feature.")
}
return nil
}

View File

@ -0,0 +1,74 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build !consulent
// +build !consulent
package structs
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/sdk/testutil"
)
func TestEnterprise_ServiceIntentionsConfigEntry(t *testing.T) {
type testcase struct {
entry *ServiceIntentionsConfigEntry
legacy bool
normalizeErr string
validateErr string
// check is called between normalize and validate
check func(t *testing.T, entry *ServiceIntentionsConfigEntry)
}
cases := map[string]testcase{
"No sameness groups": {
entry: &ServiceIntentionsConfigEntry{
Kind: ServiceIntentions,
Name: "test",
Sources: []*SourceIntention{
{
Name: "foo",
SamenessGroup: "blah",
Action: IntentionActionAllow,
},
},
},
validateErr: `Sources[0].SamenessGroup: Sameness groups are a Consul Enterprise feature.`,
},
}
for name, tc := range cases {
tc := tc
t.Run(name, func(t *testing.T) {
var err error
if tc.legacy {
err = tc.entry.LegacyNormalize()
} else {
err = tc.entry.Normalize()
}
if tc.normalizeErr != "" {
testutil.RequireErrorContains(t, err, tc.normalizeErr)
return
}
require.NoError(t, err)
if tc.check != nil {
tc.check(t, tc.entry)
}
if tc.legacy {
err = tc.entry.LegacyValidate()
} else {
err = tc.entry.Validate()
}
if tc.validateErr != "" {
testutil.RequireErrorContains(t, err, tc.validateErr)
return
}
require.NoError(t, err)
})
}
}

View File

@ -65,6 +65,11 @@ type Intention struct {
// same level of tenancy (partition is local to cluster, peer is remote). // same level of tenancy (partition is local to cluster, peer is remote).
SourcePeer string `json:",omitempty"` SourcePeer string `json:",omitempty"`
// SourceSamenessGroup cannot be a wildcard "*" and is not compatible with legacy
// intentions. Cannot be used with SourcePartition, as both represent the
// same level of tenancy (sameness group includes both partitions and cluster peers).
SourceSamenessGroup string `json:",omitempty"`
// SourceType is the type of the value for the source. // SourceType is the type of the value for the source.
SourceType IntentionSourceType SourceType IntentionSourceType
@ -415,6 +420,9 @@ func (x *Intention) String() string {
if x.SourcePeer != "" { if x.SourcePeer != "" {
srcClusterPart = "peer(" + x.SourcePeer + ")/" srcClusterPart = "peer(" + x.SourcePeer + ")/"
} }
if x.SourceSamenessGroup != "" {
srcClusterPart = "sameness-group(" + x.SourceSamenessGroup + ")/"
}
var dstPartitionPart string var dstPartitionPart string
if x.DestinationPartition != "" { if x.DestinationPartition != "" {
@ -479,6 +487,7 @@ func (x *Intention) ToSourceIntention(legacy bool) *SourceIntention {
Name: x.SourceName, Name: x.SourceName,
EnterpriseMeta: *x.SourceEnterpriseMeta(), EnterpriseMeta: *x.SourceEnterpriseMeta(),
Peer: x.SourcePeer, Peer: x.SourcePeer,
SamenessGroup: x.SourceSamenessGroup,
Action: x.Action, Action: x.Action,
Permissions: nil, // explicitly not symmetric with the old APIs Permissions: nil, // explicitly not symmetric with the old APIs
Precedence: 0, // Ignore, let it be computed. Precedence: 0, // Ignore, let it be computed.
@ -674,6 +683,7 @@ func (q *IntentionQueryRequest) CacheInfo() cache.RequestInfo {
type IntentionQueryMatch struct { type IntentionQueryMatch struct {
Type IntentionMatchType Type IntentionMatchType
Entries []IntentionMatchEntry Entries []IntentionMatchEntry
WithSamenessGroups bool
} }
// IntentionMatchEntry is a single entry for matching an intention. // IntentionMatchEntry is a single entry for matching an intention.
@ -737,6 +747,7 @@ type IntentionQueryExact struct {
DestinationPartition string `json:",omitempty"` DestinationPartition string `json:",omitempty"`
SourcePeer string `json:",omitempty"` SourcePeer string `json:",omitempty"`
SourceSamenessGroup string `json:",omitempty"`
} }
// Validate is used to ensure all 4 required parameters are specified. // Validate is used to ensure all 4 required parameters are specified.
@ -769,6 +780,9 @@ func (r *IntentionListRequest) RequestDatacenter() string {
return r.Datacenter return r.Datacenter
} }
// SimplifiedIntentions contains expanded sameness groups.
type SimplifiedIntentions Intentions
// IntentionPrecedenceSorter takes a list of intentions and sorts them // IntentionPrecedenceSorter takes a list of intentions and sorts them
// based on the match precedence rules for intentions. The intentions // based on the match precedence rules for intentions. The intentions
// closer to the head of the list have higher precedence. i.e. index 0 has // closer to the head of the list have higher precedence. i.e. index 0 has
@ -788,13 +802,16 @@ func (s IntentionPrecedenceSorter) Less(i, j int) bool {
// Tie break on lexicographic order of the tuple in canonical form: // Tie break on lexicographic order of the tuple in canonical form:
// //
// (SrcPeer, SrcPxn, SrcNS, Src, DstPxn, DstNS, Dst) // (SrcSamenessGroup, SrcPeer, SrcPxn, SrcNS, Src, DstPxn, DstNS, Dst)
// //
// This is arbitrary but it keeps sorting deterministic which is a nice // This is arbitrary but it keeps sorting deterministic which is a nice
// property for consistency. It is arguably open to abuse if implementations // property for consistency. It is arguably open to abuse if implementations
// rely on this however by definition the order among same-precedence rules // rely on this however by definition the order among same-precedence rules
// is arbitrary and doesn't affect whether an allow or deny rule is acted on // is arbitrary and doesn't affect whether an allow or deny rule is acted on
// since all applicable rules are checked. // since all applicable rules are checked.
if a.SourceSamenessGroup != b.SourceSamenessGroup {
return a.SourceSamenessGroup < b.SourceSamenessGroup
}
if a.SourcePeer != b.SourcePeer { if a.SourcePeer != b.SourcePeer {
return a.SourcePeer < b.SourcePeer return a.SourcePeer < b.SourcePeer
} }

View File

@ -246,6 +246,7 @@ func TestIntentionValidate(t *testing.T) {
func TestIntentionPrecedenceSorter(t *testing.T) { func TestIntentionPrecedenceSorter(t *testing.T) {
type fields struct { type fields struct {
SrcSamenessGroup string
SrcPeer string SrcPeer string
SrcNS string SrcNS string
SrcN string SrcN string
@ -260,6 +261,16 @@ func TestIntentionPrecedenceSorter(t *testing.T) {
{ {
"exhaustive list", "exhaustive list",
[]fields{ []fields{
// Sameness fields
{SrcSamenessGroup: "group", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcSamenessGroup: "group", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
// Peer fields // Peer fields
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"}, {SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"}, {SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
@ -284,22 +295,31 @@ func TestIntentionPrecedenceSorter(t *testing.T) {
[]fields{ []fields{
{SrcPeer: "", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"}, {SrcPeer: "", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"}, {SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcPeer: "", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"}, {SrcPeer: "", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"}, {SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"}, {SrcPeer: "", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"}, {SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcSamenessGroup: "group", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"}, {SrcPeer: "", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"}, {SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcPeer: "", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"}, {SrcPeer: "", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"}, {SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"}, {SrcPeer: "", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"}, {SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"}, {SrcPeer: "", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"}, {SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcPeer: "", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"}, {SrcPeer: "", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"}, {SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcPeer: "", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"}, {SrcPeer: "", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"}, {SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcSamenessGroup: "group", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
}, },
}, },
{ {
@ -334,6 +354,7 @@ func TestIntentionPrecedenceSorter(t *testing.T) {
var input Intentions var input Intentions
for _, v := range tc.Input { for _, v := range tc.Input {
input = append(input, &Intention{ input = append(input, &Intention{
SourceSamenessGroup: v.SrcSamenessGroup,
SourcePeer: v.SrcPeer, SourcePeer: v.SrcPeer,
SourceNS: v.SrcNS, SourceNS: v.SrcNS,
SourceName: v.SrcN, SourceName: v.SrcN,
@ -354,6 +375,7 @@ func TestIntentionPrecedenceSorter(t *testing.T) {
var actual []fields var actual []fields
for _, v := range input { for _, v := range input {
actual = append(actual, fields{ actual = append(actual, fields{
SrcSamenessGroup: v.SourceSamenessGroup,
SrcPeer: v.SourcePeer, SrcPeer: v.SourcePeer,
SrcNS: v.SourceNS, SrcNS: v.SourceNS,
SrcN: v.SourceName, SrcN: v.SourceName,
@ -484,6 +506,15 @@ func TestIntention_String(t *testing.T) {
}, },
`peer(billing)/default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Action: ALLOW)`, `peer(billing)/default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Action: ALLOW)`,
}, },
"L4 allow with source sameness group": {
&Intention{
SourceName: "foo",
SourceSamenessGroup: "group-1",
DestinationName: "bar",
Action: IntentionActionAllow,
},
`sameness-group(group-1)/default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Action: ALLOW)`,
},
} }
for name, tc := range cases { for name, tc := range cases {

View File

@ -744,6 +744,11 @@ var expectedFieldConfigIntention bexpr.FieldConfigurations = bexpr.FieldConfigur
CoerceFn: bexpr.CoerceString, CoerceFn: bexpr.CoerceString,
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches},
}, },
"SourceSamenessGroup": &bexpr.FieldConfiguration{
StructFieldName: "SourceSamenessGroup",
CoerceFn: bexpr.CoerceString,
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches},
},
"SourcePartition": &bexpr.FieldConfiguration{ "SourcePartition": &bexpr.FieldConfiguration{
StructFieldName: "SourcePartition", StructFieldName: "SourcePartition",
CoerceFn: bexpr.CoerceString, CoerceFn: bexpr.CoerceString,

View File

@ -1742,7 +1742,7 @@ func (s *ResourceGenerator) makeTerminatingGatewayListener(
type terminatingGatewayFilterChainOpts struct { type terminatingGatewayFilterChainOpts struct {
cluster string cluster string
service structs.ServiceName service structs.ServiceName
intentions structs.Intentions intentions structs.SimplifiedIntentions
protocol string protocol string
address string // only valid for destination listeners address string // only valid for destination listeners
port int // only valid for destination listeners port int // only valid for destination listeners

View File

@ -23,7 +23,7 @@ import (
) )
func makeRBACNetworkFilter( func makeRBACNetworkFilter(
intentions structs.Intentions, intentions structs.SimplifiedIntentions,
intentionDefaultAllow bool, intentionDefaultAllow bool,
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
peerTrustBundles []*pbpeering.PeeringTrustBundle, peerTrustBundles []*pbpeering.PeeringTrustBundle,
@ -38,7 +38,7 @@ func makeRBACNetworkFilter(
} }
func makeRBACHTTPFilter( func makeRBACHTTPFilter(
intentions structs.Intentions, intentions structs.SimplifiedIntentions,
intentionDefaultAllow bool, intentionDefaultAllow bool,
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
peerTrustBundles []*pbpeering.PeeringTrustBundle, peerTrustBundles []*pbpeering.PeeringTrustBundle,
@ -52,7 +52,7 @@ func makeRBACHTTPFilter(
} }
func intentionListToIntermediateRBACForm( func intentionListToIntermediateRBACForm(
intentions structs.Intentions, intentions structs.SimplifiedIntentions,
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
isHTTP bool, isHTTP bool,
trustBundlesByPeer map[string]*pbpeering.PeeringTrustBundle, trustBundlesByPeer map[string]*pbpeering.PeeringTrustBundle,
@ -478,7 +478,7 @@ type rbacLocalInfo struct {
// //
// Which really is just an allow-list of [A, C AND NOT(B)] // Which really is just an allow-list of [A, C AND NOT(B)]
func makeRBACRules( func makeRBACRules(
intentions structs.Intentions, intentions structs.SimplifiedIntentions,
intentionDefaultAllow bool, intentionDefaultAllow bool,
localInfo rbacLocalInfo, localInfo rbacLocalInfo,
isHTTP bool, isHTTP bool,
@ -590,13 +590,13 @@ func optimizePrincipals(orig []*envoy_rbac_v3.Principal) []*envoy_rbac_v3.Princi
// //
// (backend/* -> default/*) was dropped because it is already known that any service // (backend/* -> default/*) was dropped because it is already known that any service
// in the backend namespace can target default/web. // in the backend namespace can target default/web.
func removeSameSourceIntentions(intentions structs.Intentions) structs.Intentions { func removeSameSourceIntentions(intentions structs.SimplifiedIntentions) structs.SimplifiedIntentions {
if len(intentions) < 2 { if len(intentions) < 2 {
return intentions return intentions
} }
var ( var (
out = make(structs.Intentions, 0, len(intentions)) out = make(structs.SimplifiedIntentions, 0, len(intentions))
changed = false changed = false
seenSource = make(map[structs.PeeredServiceName]struct{}) seenSource = make(map[structs.PeeredServiceName]struct{})
) )

View File

@ -49,11 +49,11 @@ func TestRemoveIntentionPrecedence(t *testing.T) {
ixn.Permissions = perms ixn.Permissions = perms
return ixn return ixn
} }
sorted := func(ixns ...*structs.Intention) structs.Intentions { sorted := func(ixns ...*structs.Intention) structs.SimplifiedIntentions {
sort.SliceStable(ixns, func(i, j int) bool { sort.SliceStable(ixns, func(i, j int) bool {
return ixns[j].Precedence < ixns[i].Precedence return ixns[j].Precedence < ixns[i].Precedence
}) })
return structs.Intentions(ixns) return structs.SimplifiedIntentions(ixns)
} }
testPeerTrustBundle := map[string]*pbpeering.PeeringTrustBundle{ testPeerTrustBundle := map[string]*pbpeering.PeeringTrustBundle{
"peer1": { "peer1": {
@ -106,7 +106,7 @@ func TestRemoveIntentionPrecedence(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
intentionDefaultAllow bool intentionDefaultAllow bool
http bool http bool
intentions structs.Intentions intentions structs.SimplifiedIntentions
expect []*rbacIntention expect []*rbacIntention
}{ }{
"default-allow-path-allow": { "default-allow-path-allow": {
@ -492,11 +492,11 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
}, },
} }
testTrustDomain := "test.consul" testTrustDomain := "test.consul"
sorted := func(ixns ...*structs.Intention) structs.Intentions { sorted := func(ixns ...*structs.Intention) structs.SimplifiedIntentions {
sort.SliceStable(ixns, func(i, j int) bool { sort.SliceStable(ixns, func(i, j int) bool {
return ixns[j].Precedence < ixns[i].Precedence return ixns[j].Precedence < ixns[i].Precedence
}) })
return structs.Intentions(ixns) return structs.SimplifiedIntentions(ixns)
} }
var ( var (
@ -516,7 +516,7 @@ func TestMakeRBACNetworkAndHTTPFilters(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
intentionDefaultAllow bool intentionDefaultAllow bool
intentions structs.Intentions intentions structs.SimplifiedIntentions
}{ }{
"default-deny-mixed-precedence": { "default-deny-mixed-precedence": {
intentionDefaultAllow: false, intentionDefaultAllow: false,
@ -858,15 +858,15 @@ func TestRemoveSameSourceIntentions(t *testing.T) {
ixn.UpdatePrecedence() ixn.UpdatePrecedence()
return ixn return ixn
} }
sorted := func(ixns ...*structs.Intention) structs.Intentions { sorted := func(ixns ...*structs.Intention) structs.SimplifiedIntentions {
sort.SliceStable(ixns, func(i, j int) bool { sort.SliceStable(ixns, func(i, j int) bool {
return ixns[j].Precedence < ixns[i].Precedence return ixns[j].Precedence < ixns[i].Precedence
}) })
return structs.Intentions(ixns) return structs.SimplifiedIntentions(ixns)
} }
tests := map[string]struct { tests := map[string]struct {
in structs.Intentions in structs.SimplifiedIntentions
expect structs.Intentions expect structs.SimplifiedIntentions
}{ }{
"empty": {}, "empty": {},
"one": { "one": {

View File

@ -43,6 +43,10 @@ type Intention struct {
// same level of tenancy (partition is local to cluster, peer is remote). // same level of tenancy (partition is local to cluster, peer is remote).
SourcePeer string `json:",omitempty"` SourcePeer string `json:",omitempty"`
// SourceSamenessGroup cannot be wildcards "*" and
// is not compatible with legacy intentions.
SourceSamenessGroup string `json:",omitempty"`
// SourceType is the type of the value for the source. // SourceType is the type of the value for the source.
SourceType IntentionSourceType SourceType IntentionSourceType

View File

@ -1715,6 +1715,7 @@ func SourceIntentionToStructs(s *SourceIntention, t *structs.SourceIntention) {
t.LegacyUpdateTime = timeToStructs(s.LegacyUpdateTime) t.LegacyUpdateTime = timeToStructs(s.LegacyUpdateTime)
t.EnterpriseMeta = enterpriseMetaToStructs(s.EnterpriseMeta) t.EnterpriseMeta = enterpriseMetaToStructs(s.EnterpriseMeta)
t.Peer = s.Peer t.Peer = s.Peer
t.SamenessGroup = s.SamenessGroup
} }
func SourceIntentionFromStructs(t *structs.SourceIntention, s *SourceIntention) { func SourceIntentionFromStructs(t *structs.SourceIntention, s *SourceIntention) {
if s == nil { if s == nil {
@ -1741,6 +1742,7 @@ func SourceIntentionFromStructs(t *structs.SourceIntention, s *SourceIntention)
s.LegacyUpdateTime = timeFromStructs(t.LegacyUpdateTime) s.LegacyUpdateTime = timeFromStructs(t.LegacyUpdateTime)
s.EnterpriseMeta = enterpriseMetaFromStructs(t.EnterpriseMeta) s.EnterpriseMeta = enterpriseMetaFromStructs(t.EnterpriseMeta)
s.Peer = t.Peer s.Peer = t.Peer
s.SamenessGroup = t.SamenessGroup
} }
func StatusToStructs(s *Status, t *structs.Status) { func StatusToStructs(s *Status, t *structs.Status) {
if s == nil { if s == nil {

File diff suppressed because it is too large Load Diff

View File

@ -430,6 +430,7 @@ message SourceIntention {
// mog: func-to=enterpriseMetaToStructs func-from=enterpriseMetaFromStructs // mog: func-to=enterpriseMetaToStructs func-from=enterpriseMetaFromStructs
common.EnterpriseMeta EnterpriseMeta = 11; common.EnterpriseMeta EnterpriseMeta = 11;
string Peer = 12; string Peer = 12;
string SamenessGroup = 13;
} }
enum IntentionAction { enum IntentionAction {