Account for partitions in ixn match/decision
This commit is contained in:
parent
a8f396c55f
commit
8a9bf3748c
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue