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

View File

@ -11,12 +11,27 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
name string name string
target string target string
targetNS string targetNS string
targetAP string
ixn *structs.Intention ixn *structs.Intention
matchType structs.IntentionMatchType matchType structs.IntentionMatchType
auth bool auth bool
match bool match bool
}{ }{
// Source match type // 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", name: "match exact source, not matching namespace",
target: "web", target: "web",
@ -95,6 +110,20 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
}, },
// Destination match type // 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", name: "match exact destination, not matching namespace",
target: "web", target: "web",
@ -188,7 +217,7 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
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.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.auth, auth)
assert.Equal(t, tc.match, match) assert.Equal(t, tc.match, match)
}) })

View File

@ -89,6 +89,7 @@ func (a *Agent) ConnectAuthorize(token string,
Entries: []structs.IntentionMatchEntry{ Entries: []structs.IntentionMatchEntry{
{ {
Namespace: req.TargetNamespace(), Namespace: req.TargetNamespace(),
Partition: req.TargetPartition(),
Name: req.Target, Name: req.Target,
}, },
}, },
@ -113,7 +114,8 @@ func (a *Agent) ConnectAuthorize(token string,
var ixnMatch *structs.Intention var ixnMatch *structs.Intention
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(uriService.Service, uriService.Namespace, ixn, structs.IntentionMatchSource); ok { if _, ok := connect.AuthorizeIntentionTarget(
uriService.Service, uriService.Namespace, uriService.Partition, ixn, structs.IntentionMatchSource); ok {
ixnMatch = ixn ixnMatch = ixn
break break
} }

View File

@ -721,18 +721,28 @@ func (s *Intention) Check(args *structs.IntentionQueryRequest, reply *structs.In
// which is much more important. // which is much more important.
defaultDecision := authz.IntentionDefaultAllow(nil) defaultDecision := authz.IntentionDefaultAllow(nil)
state := s.srv.fsm.State() store := s.srv.fsm.State()
entry := structs.IntentionMatchEntry{ entry := structs.IntentionMatchEntry{
Namespace: query.SourceNS, Namespace: query.SourceNS,
Partition: query.SourcePartition,
Name: query.SourceName, Name: query.SourceName,
} }
_, intentions, err := state.IntentionMatchOne(nil, entry, structs.IntentionMatchSource) _, intentions, err := store.IntentionMatchOne(nil, entry, structs.IntentionMatchSource)
if err != nil { if err != nil {
return fmt.Errorf("failed to query intentions for %s/%s", query.SourceNS, query.SourceName) 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 { if err != nil {
return fmt.Errorf("failed to get intention decision from (%s/%s) to (%s/%s): %v", return fmt.Errorf("failed to get intention decision from (%s/%s) to (%s/%s): %v",
query.SourceNS, query.SourceName, query.DestinationNS, query.DestinationName, err) 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 { for _, gs := range gatewayServices {
entry := structs.IntentionMatchEntry{ entry := structs.IntentionMatchEntry{
Namespace: gs.Service.NamespaceOrDefault(), Namespace: gs.Service.NamespaceOrDefault(),
Partition: gs.Service.PartitionOrDefault(),
Name: gs.Service.Name, Name: gs.Service.Name,
} }
idx, intentions, err := state.IntentionMatchOne(ws, entry, structs.IntentionMatchDestination) idx, intentions, err := state.IntentionMatchOne(ws, entry, structs.IntentionMatchDestination)

View File

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

View File

@ -3132,6 +3132,7 @@ func (s *Store) ServiceTopology(
matchEntry := structs.IntentionMatchEntry{ matchEntry := structs.IntentionMatchEntry{
Namespace: entMeta.NamespaceOrDefault(), Namespace: entMeta.NamespaceOrDefault(),
Partition: entMeta.PartitionOrDefault(),
Name: service, Name: service,
} }
_, srcIntentions, err := compatIntentionMatchOneTxn( _, srcIntentions, err := compatIntentionMatchOneTxn(
@ -3147,7 +3148,16 @@ func (s *Store) ServiceTopology(
} }
for _, un := range upstreamNames { 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 { if err != nil {
return 0, nil, fmt.Errorf("failed to get intention decision from (%s) to (%s): %v", return 0, nil, fmt.Errorf("failed to get intention decision from (%s) to (%s): %v",
sn.String(), un.String(), err) 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()) return 0, nil, fmt.Errorf("failed to query intentions for %s", sn.String())
} }
for _, dn := range downstreamNames { 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 { if err != nil {
return 0, nil, fmt.Errorf("failed to get intention decision from (%s) to (%s): %v", return 0, nil, fmt.Errorf("failed to get intention decision from (%s) to (%s): %v",
dn.String(), sn.String(), err) dn.String(), sn.String(), err)

View File

@ -732,26 +732,33 @@ func (s *Store) LegacyIntentionDeleteAll(idx uint64) error {
return tx.Commit() 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. // 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. // 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. // 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( func (s *Store) IntentionDecision(opts IntentionDecisionOpts) (structs.IntentionDecisionSummary, error) {
target, targetNS string, intentions structs.Intentions, matchType structs.IntentionMatchType,
defaultDecision acl.EnforcementDecision, allowPermissions bool,
) (structs.IntentionDecisionSummary, error) {
// 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 intentions { for _, ixn := range opts.Intentions {
if _, ok := connect.AuthorizeIntentionTarget(target, targetNS, ixn, matchType); ok { if _, ok := connect.AuthorizeIntentionTarget(opts.Target, opts.Namespace, opts.Partition, ixn, opts.MatchType); ok {
ixnMatch = ixn ixnMatch = ixn
break break
} }
} }
resp := structs.IntentionDecisionSummary{ resp := structs.IntentionDecisionSummary{
DefaultAllow: defaultDecision == acl.Allow, DefaultAllow: opts.DefaultDecision == acl.Allow,
} }
if ixnMatch == nil { if ixnMatch == nil {
// No intention found, fall back to default // No intention found, fall back to default
@ -764,7 +771,7 @@ func (s *Store) IntentionDecision(
if len(ixnMatch.Permissions) > 0 { if len(ixnMatch.Permissions) > 0 {
// If any permissions are present, fall back to allowPermissions. // 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. // 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.HasPermissions = true
} }
resp.ExternalSource = ixnMatch.Meta[structs.MetaExternalSource] resp.ExternalSource = ixnMatch.Meta[structs.MetaExternalSource]
@ -977,6 +984,7 @@ func (s *Store) intentionTopologyTxn(tx ReadTxn, ws memdb.WatchSet,
} }
entry := structs.IntentionMatchEntry{ entry := structs.IntentionMatchEntry{
Namespace: target.NamespaceOrDefault(), Namespace: target.NamespaceOrDefault(),
Partition: target.PartitionOrDefault(),
Name: target.Name, Name: target.Name,
} }
index, intentions, err := compatIntentionMatchOneTxn(tx, ws, entry, intentionMatchType) 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 { if candidate.Name == structs.ConsulServiceName {
continue 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 { if err != nil {
src, dst := target, candidate src, dst := target, candidate
if downstreams { 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 // Note that this doesn't need to test the intention sort logic exhaustively
// since this is tested in their sort implementation in the structs. // since this is tested in their sort implementation in the structs.
// TODO(partitions): Update for partition matching
func TestStore_IntentionMatch_table(t *testing.T) { func TestStore_IntentionMatch_table(t *testing.T) {
type testCase struct { type testCase struct {
Name string Name string
@ -1391,6 +1392,7 @@ func TestStore_IntentionMatch_table(t *testing.T) {
// Equivalent to TestStore_IntentionMatch_table but for IntentionMatchOne which // Equivalent to TestStore_IntentionMatch_table but for IntentionMatchOne which
// matches a single service // matches a single service
// TODO(partitions): Update for partition matching
func TestStore_IntentionMatchOne_table(t *testing.T) { func TestStore_IntentionMatchOne_table(t *testing.T) {
type testCase struct { type testCase struct {
Name string Name string
@ -1869,12 +1871,23 @@ func TestStore_IntentionDecision(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
entry := structs.IntentionMatchEntry{ entry := structs.IntentionMatchEntry{
Namespace: structs.IntentionDefaultNamespace, Namespace: structs.IntentionDefaultNamespace,
Partition: acl.DefaultPartitionName,
Name: tc.src, Name: tc.src,
} }
_, intentions, err := s.IntentionMatchOne(nil, entry, structs.IntentionMatchSource) _, intentions, err := s.IntentionMatchOne(nil, entry, structs.IntentionMatchSource)
if err != nil { if err != nil {
require.NoError(t, err) 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) decision, err := s.IntentionDecision(tc.dst, structs.IntentionDefaultNamespace, intentions, tc.matchType, tc.defaultDecision, tc.allowPermissions)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tc.expect, decision) 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(), id: svc.String(),
name: svc.Name, name: svc.Name,
namespace: svc.NamespaceOrDefault(), namespace: svc.NamespaceOrDefault(),
partition: svc.PartitionOrDefault(),
datacenter: s.source.Datacenter, datacenter: s.source.Datacenter,
cfg: cfg, cfg: cfg,
meshGateway: meshGateway, meshGateway: meshGateway,

View File

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