mirror of https://github.com/status-im/consul.git
Add sameness groups to service intentions. (#17064)
This commit is contained in:
parent
863cd57117
commit
b1fae05983
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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" }
|
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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{},
|
||||||
},
|
},
|
||||||
// ========
|
// ========
|
||||||
{
|
{
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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{})
|
||||||
)
|
)
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue