Account for partitions in ixn match/decision

This commit is contained in:
freddygv 2021-09-16 13:31:19 -06:00
parent a8f396c55f
commit 8a9bf3748c
11 changed files with 120 additions and 17 deletions

View File

@ -11,13 +11,17 @@ import (
// 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.
func AuthorizeIntentionTarget(
target, targetNS string,
target, targetNS, targetAP string,
ixn *structs.Intention,
matchType structs.IntentionMatchType,
) (auth bool, match bool) {
switch matchType {
case structs.IntentionMatchDestination:
if ixn.DestinationPartition != targetAP {
return false, false
}
if ixn.DestinationNS != structs.WildcardSpecifier && ixn.DestinationNS != targetNS {
// Non-matching namespace
return false, false
@ -29,6 +33,10 @@ func AuthorizeIntentionTarget(
}
case structs.IntentionMatchSource:
if ixn.SourcePartition != targetAP {
return false, false
}
if ixn.SourceNS != structs.WildcardSpecifier && ixn.SourceNS != targetNS {
// Non-matching namespace
return false, false

View File

@ -11,12 +11,27 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
name string
target string
targetNS string
targetAP string
ixn *structs.Intention
matchType structs.IntentionMatchType
auth bool
match bool
}{
// Source match type
{
name: "matching source target and namespace, but not partition",
target: "db",
targetNS: structs.IntentionDefaultNamespace,
targetAP: "foo",
ixn: &structs.Intention{
SourceName: "db",
SourceNS: structs.IntentionDefaultNamespace,
SourcePartition: "not-foo",
},
matchType: structs.IntentionMatchSource,
auth: false,
match: false,
},
{
name: "match exact source, not matching namespace",
target: "web",
@ -95,6 +110,20 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
},
// Destination match type
{
name: "matching destination target and namespace, but not partition",
target: "db",
targetNS: structs.IntentionDefaultNamespace,
targetAP: "foo",
ixn: &structs.Intention{
SourceName: "db",
SourceNS: structs.IntentionDefaultNamespace,
SourcePartition: "not-foo",
},
matchType: structs.IntentionMatchDestination,
auth: false,
match: false,
},
{
name: "match exact destination, not matching namespace",
target: "web",
@ -188,7 +217,7 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
auth, match := AuthorizeIntentionTarget(tc.target, tc.targetNS, tc.ixn, tc.matchType)
auth, match := AuthorizeIntentionTarget(tc.target, tc.targetNS, tc.targetAP, tc.ixn, tc.matchType)
assert.Equal(t, tc.auth, auth)
assert.Equal(t, tc.match, match)
})

View File

@ -89,6 +89,7 @@ func (a *Agent) ConnectAuthorize(token string,
Entries: []structs.IntentionMatchEntry{
{
Namespace: req.TargetNamespace(),
Partition: req.TargetPartition(),
Name: req.Target,
},
},
@ -113,7 +114,8 @@ func (a *Agent) ConnectAuthorize(token string,
var ixnMatch *structs.Intention
for _, ixn := range reply.Matches[0] {
// We match on the intention source because the uriService is the source of the connection to authorize.
if _, ok := connect.AuthorizeIntentionTarget(uriService.Service, uriService.Namespace, ixn, structs.IntentionMatchSource); ok {
if _, ok := connect.AuthorizeIntentionTarget(
uriService.Service, uriService.Namespace, uriService.Partition, ixn, structs.IntentionMatchSource); ok {
ixnMatch = ixn
break
}

View File

@ -721,18 +721,28 @@ func (s *Intention) Check(args *structs.IntentionQueryRequest, reply *structs.In
// which is much more important.
defaultDecision := authz.IntentionDefaultAllow(nil)
state := s.srv.fsm.State()
store := s.srv.fsm.State()
entry := structs.IntentionMatchEntry{
Namespace: query.SourceNS,
Partition: query.SourcePartition,
Name: query.SourceName,
}
_, intentions, err := state.IntentionMatchOne(nil, entry, structs.IntentionMatchSource)
_, intentions, err := store.IntentionMatchOne(nil, entry, structs.IntentionMatchSource)
if err != nil {
return fmt.Errorf("failed to query intentions for %s/%s", query.SourceNS, query.SourceName)
}
decision, err := state.IntentionDecision(query.DestinationName, query.DestinationNS, intentions, structs.IntentionMatchDestination, defaultDecision, false)
opts := state.IntentionDecisionOpts{
Target: query.DestinationName,
Namespace: query.DestinationNS,
Partition: query.DestinationPartition,
Intentions: intentions,
MatchType: structs.IntentionMatchDestination,
DefaultDecision: defaultDecision,
AllowPermissions: false,
}
decision, err := store.IntentionDecision(opts)
if err != nil {
return fmt.Errorf("failed to get intention decision from (%s/%s) to (%s/%s): %v",
query.SourceNS, query.SourceName, query.DestinationNS, query.DestinationName, err)

View File

@ -357,6 +357,7 @@ func (m *Internal) GatewayIntentions(args *structs.IntentionQueryRequest, reply
for _, gs := range gatewayServices {
entry := structs.IntentionMatchEntry{
Namespace: gs.Service.NamespaceOrDefault(),
Partition: gs.Service.PartitionOrDefault(),
Name: gs.Service.Name,
}
idx, intentions, err := state.IntentionMatchOne(ws, entry, structs.IntentionMatchDestination)

View File

@ -1539,6 +1539,7 @@ func TestInternal_GatewayIntentions(t *testing.T) {
Entries: []structs.IntentionMatchEntry{
{
Namespace: structs.IntentionDefaultNamespace,
Partition: acl.DefaultPartitionName,
Name: "terminating-gateway",
},
},
@ -1661,6 +1662,7 @@ service_prefix "terminating-gateway" { policy = "read" }
Entries: []structs.IntentionMatchEntry{
{
Namespace: structs.IntentionDefaultNamespace,
Partition: acl.DefaultPartitionName,
Name: "terminating-gateway",
},
},

View File

@ -3132,6 +3132,7 @@ func (s *Store) ServiceTopology(
matchEntry := structs.IntentionMatchEntry{
Namespace: entMeta.NamespaceOrDefault(),
Partition: entMeta.PartitionOrDefault(),
Name: service,
}
_, srcIntentions, err := compatIntentionMatchOneTxn(
@ -3147,7 +3148,16 @@ func (s *Store) ServiceTopology(
}
for _, un := range upstreamNames {
decision, err := s.IntentionDecision(un.Name, un.NamespaceOrDefault(), srcIntentions, structs.IntentionMatchDestination, defaultAllow, false)
opts := IntentionDecisionOpts{
Target: un.Name,
Namespace: un.NamespaceOrDefault(),
Partition: un.PartitionOrDefault(),
Intentions: srcIntentions,
MatchType: structs.IntentionMatchDestination,
DefaultDecision: defaultAllow,
AllowPermissions: false,
}
decision, err := s.IntentionDecision(opts)
if err != nil {
return 0, nil, fmt.Errorf("failed to get intention decision from (%s) to (%s): %v",
sn.String(), un.String(), err)
@ -3256,7 +3266,16 @@ func (s *Store) ServiceTopology(
return 0, nil, fmt.Errorf("failed to query intentions for %s", sn.String())
}
for _, dn := range downstreamNames {
decision, err := s.IntentionDecision(dn.Name, dn.NamespaceOrDefault(), dstIntentions, structs.IntentionMatchSource, defaultAllow, false)
opts := IntentionDecisionOpts{
Target: dn.Name,
Namespace: dn.NamespaceOrDefault(),
Partition: dn.PartitionOrDefault(),
Intentions: dstIntentions,
MatchType: structs.IntentionMatchSource,
DefaultDecision: defaultAllow,
AllowPermissions: false,
}
decision, err := s.IntentionDecision(opts)
if err != nil {
return 0, nil, fmt.Errorf("failed to get intention decision from (%s) to (%s): %v",
dn.String(), sn.String(), err)

View File

@ -732,26 +732,33 @@ func (s *Store) LegacyIntentionDeleteAll(idx uint64) error {
return tx.Commit()
}
type IntentionDecisionOpts struct {
Target string
Namespace string
Partition string
Intentions structs.Intentions
MatchType structs.IntentionMatchType
DefaultDecision acl.EnforcementDecision
AllowPermissions bool
}
// IntentionDecision returns whether a connection should be allowed to a source or destination given a set of intentions.
//
// allowPermissions determines whether the presence of L7 permissions leads to a DENY decision.
// This should be false when evaluating a connection between a source and destination, but not the request that will be sent.
func (s *Store) IntentionDecision(
target, targetNS string, intentions structs.Intentions, matchType structs.IntentionMatchType,
defaultDecision acl.EnforcementDecision, allowPermissions bool,
) (structs.IntentionDecisionSummary, error) {
func (s *Store) IntentionDecision(opts IntentionDecisionOpts) (structs.IntentionDecisionSummary, error) {
// Figure out which source matches this request.
var ixnMatch *structs.Intention
for _, ixn := range intentions {
if _, ok := connect.AuthorizeIntentionTarget(target, targetNS, ixn, matchType); ok {
for _, ixn := range opts.Intentions {
if _, ok := connect.AuthorizeIntentionTarget(opts.Target, opts.Namespace, opts.Partition, ixn, opts.MatchType); ok {
ixnMatch = ixn
break
}
}
resp := structs.IntentionDecisionSummary{
DefaultAllow: defaultDecision == acl.Allow,
DefaultAllow: opts.DefaultDecision == acl.Allow,
}
if ixnMatch == nil {
// No intention found, fall back to default
@ -764,7 +771,7 @@ func (s *Store) IntentionDecision(
if len(ixnMatch.Permissions) > 0 {
// If any permissions are present, fall back to allowPermissions.
// We are not evaluating requests so we cannot know whether the L7 permission requirements will be met.
resp.Allowed = allowPermissions
resp.Allowed = opts.AllowPermissions
resp.HasPermissions = true
}
resp.ExternalSource = ixnMatch.Meta[structs.MetaExternalSource]
@ -977,6 +984,7 @@ func (s *Store) intentionTopologyTxn(tx ReadTxn, ws memdb.WatchSet,
}
entry := structs.IntentionMatchEntry{
Namespace: target.NamespaceOrDefault(),
Partition: target.PartitionOrDefault(),
Name: target.Name,
}
index, intentions, err := compatIntentionMatchOneTxn(tx, ws, entry, intentionMatchType)
@ -1029,7 +1037,16 @@ func (s *Store) intentionTopologyTxn(tx ReadTxn, ws memdb.WatchSet,
if candidate.Name == structs.ConsulServiceName {
continue
}
decision, err := s.IntentionDecision(candidate.Name, candidate.NamespaceOrDefault(), intentions, decisionMatchType, defaultDecision, true)
opts := IntentionDecisionOpts{
Target: candidate.Name,
Namespace: candidate.NamespaceOrDefault(),
Partition: candidate.PartitionOrDefault(),
Intentions: intentions,
MatchType: decisionMatchType,
DefaultDecision: defaultDecision,
AllowPermissions: true,
}
decision, err := s.IntentionDecision(opts)
if err != nil {
src, dst := target, candidate
if downstreams {

View File

@ -1204,6 +1204,7 @@ func TestStore_IntentionsList(t *testing.T) {
//
// Note that this doesn't need to test the intention sort logic exhaustively
// since this is tested in their sort implementation in the structs.
// TODO(partitions): Update for partition matching
func TestStore_IntentionMatch_table(t *testing.T) {
type testCase struct {
Name string
@ -1391,6 +1392,7 @@ func TestStore_IntentionMatch_table(t *testing.T) {
// Equivalent to TestStore_IntentionMatch_table but for IntentionMatchOne which
// matches a single service
// TODO(partitions): Update for partition matching
func TestStore_IntentionMatchOne_table(t *testing.T) {
type testCase struct {
Name string
@ -1869,12 +1871,23 @@ func TestStore_IntentionDecision(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
entry := structs.IntentionMatchEntry{
Namespace: structs.IntentionDefaultNamespace,
Partition: acl.DefaultPartitionName,
Name: tc.src,
}
_, intentions, err := s.IntentionMatchOne(nil, entry, structs.IntentionMatchSource)
if err != nil {
require.NoError(t, err)
}
opts := s.IntentionDecisionOpts{
target: tc.dst,
namespace: structs.IntentionDefaultNamespace,
partition: structs.IntentionDefaultNamespace,
intentions: intentions,
matchType: tc.matchType,
defaultDecision: tc.defaultDecision,
allowPermissions: tc.allowPermissions,
}
decision, err := s.IntentionDecision(tc.dst, structs.IntentionDefaultNamespace, intentions, tc.matchType, tc.defaultDecision, tc.allowPermissions)
require.NoError(t, err)
require.Equal(t, tc.expect, decision)

View File

@ -263,6 +263,7 @@ func (s *handlerConnectProxy) handleUpdate(ctx context.Context, u cache.UpdateEv
id: svc.String(),
name: svc.Name,
namespace: svc.NamespaceOrDefault(),
partition: svc.PartitionOrDefault(),
datacenter: s.source.Datacenter,
cfg: cfg,
meshGateway: meshGateway,

View File

@ -577,6 +577,7 @@ func (s *HTTPHandlers) UIGatewayIntentions(resp http.ResponseWriter, req *http.R
Entries: []structs.IntentionMatchEntry{
{
Namespace: entMeta.NamespaceOrEmpty(),
Partition: entMeta.PartitionOrDefault(),
Name: name,
},
},