Merge pull request #11071 from hashicorp/partitions/ixn-decisions

This commit is contained in:
Freddy 2021-09-16 15:18:23 -06:00 committed by GitHub
commit f1b2ef30d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 122 additions and 84 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 structs.PartitionOrDefault(ixn.DestinationPartition) != structs.PartitionOrDefault(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 structs.PartitionOrDefault(ixn.SourcePartition) != structs.PartitionOrDefault(targetAP) {
return false, false
}
if ixn.SourceNS != structs.WildcardSpecifier && ixn.SourceNS != targetNS {
// Non-matching namespace
return false, false

View File

@ -11,31 +11,18 @@ 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: "match exact source, not matching namespace",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
SourceName: "db",
SourceNS: "different",
},
matchType: structs.IntentionMatchSource,
auth: false,
match: false,
},
{
name: "match exact source, not matching name",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
SourceName: "db",
SourceNS: structs.IntentionDefaultNamespace,
},
matchType: structs.IntentionMatchSource,
auth: false,
@ -44,10 +31,8 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
{
name: "match exact source, allow",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
SourceName: "web",
SourceNS: structs.IntentionDefaultNamespace,
Action: structs.IntentionActionAllow,
},
matchType: structs.IntentionMatchSource,
@ -57,18 +42,15 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
{
name: "match exact source, deny",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
SourceName: "web",
SourceNS: structs.IntentionDefaultNamespace,
Action: structs.IntentionActionDeny,
},
matchType: structs.IntentionMatchSource,
auth: false,
match: true,
},
{
name: "match exact sourceNS for wildcard service, deny",
name: "match wildcard service, deny",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
@ -81,12 +63,10 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
match: true,
},
{
name: "match exact sourceNS for wildcard service, allow",
name: "match wildcard service, allow",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
SourceName: structs.WildcardSpecifier,
SourceNS: structs.IntentionDefaultNamespace,
Action: structs.IntentionActionAllow,
},
matchType: structs.IntentionMatchSource,
@ -95,25 +75,11 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
},
// Destination match type
{
name: "match exact destination, not matching namespace",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
DestinationName: "db",
DestinationNS: "different",
},
matchType: structs.IntentionMatchDestination,
auth: false,
match: false,
},
{
name: "match exact destination, not matching name",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
DestinationName: "db",
DestinationNS: structs.IntentionDefaultNamespace,
},
matchType: structs.IntentionMatchDestination,
auth: false,
@ -122,10 +88,8 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
{
name: "match exact destination, allow",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
DestinationName: "web",
DestinationNS: structs.IntentionDefaultNamespace,
Action: structs.IntentionActionAllow,
},
matchType: structs.IntentionMatchDestination,
@ -135,10 +99,8 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
{
name: "match exact destination, deny",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
DestinationName: "web",
DestinationNS: structs.IntentionDefaultNamespace,
Action: structs.IntentionActionDeny,
},
matchType: structs.IntentionMatchDestination,
@ -146,12 +108,10 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
match: true,
},
{
name: "match exact destinationNS for wildcard service, deny",
name: "match wildcard service, deny",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
DestinationName: structs.WildcardSpecifier,
DestinationNS: structs.IntentionDefaultNamespace,
Action: structs.IntentionActionDeny,
},
matchType: structs.IntentionMatchDestination,
@ -159,12 +119,10 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
match: true,
},
{
name: "match exact destinationNS for wildcard service, allow",
name: "match wildcard service, allow",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
DestinationName: structs.WildcardSpecifier,
DestinationNS: structs.IntentionDefaultNamespace,
Action: structs.IntentionActionAllow,
},
matchType: structs.IntentionMatchDestination,
@ -174,10 +132,8 @@ func TestAuthorizeIntentionTarget(t *testing.T) {
{
name: "unknown match type",
target: "web",
targetNS: structs.IntentionDefaultNamespace,
ixn: &structs.Intention{
DestinationName: structs.WildcardSpecifier,
DestinationNS: structs.IntentionDefaultNamespace,
Action: structs.IntentionActionAllow,
},
matchType: structs.IntentionMatchType("unknown"),
@ -188,7 +144,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

@ -678,6 +678,12 @@ func (s *Intention) Check(args *structs.IntentionQueryRequest, reply *structs.In
if query.DestinationNS == "" {
query.DestinationNS = entMeta.NamespaceOrDefault()
}
if query.SourcePartition == "" {
query.SourcePartition = entMeta.PartitionOrDefault()
}
if query.DestinationPartition == "" {
query.DestinationPartition = entMeta.PartitionOrDefault()
}
if err := s.srv.validateEnterpriseIntentionNamespace(query.SourceNS, false); err != nil {
return fmt.Errorf("Invalid source namespace %q: %v", query.SourceNS, err)
@ -721,18 +727,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,13 +1871,24 @@ 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)
}
decision, err := s.IntentionDecision(tc.dst, structs.IntentionDefaultNamespace, intentions, tc.matchType, tc.defaultDecision, tc.allowPermissions)
opts := IntentionDecisionOpts{
Target: tc.dst,
Namespace: structs.IntentionDefaultNamespace,
Partition: acl.DefaultPartitionName,
Intentions: intentions,
MatchType: tc.matchType,
DefaultDecision: tc.defaultDecision,
AllowPermissions: tc.allowPermissions,
}
decision, err := s.IntentionDecision(opts)
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

@ -1747,6 +1747,7 @@ func TestState_WatchesAndUpdates(t *testing.T) {
Name: "db",
EvaluateInDatacenter: "dc1",
EvaluateInNamespace: "default",
EvaluateInPartition: "default",
Datacenter: "dc1",
OverrideConnectTimeout: 6 * time.Second,
OverrideMeshGateway: structs.MeshGatewayConfig{Mode: structs.MeshGatewayModeRemote},
@ -1895,6 +1896,7 @@ func TestState_WatchesAndUpdates(t *testing.T) {
Name: "db",
EvaluateInDatacenter: "dc1",
EvaluateInNamespace: "default",
EvaluateInPartition: "default",
Datacenter: "dc1",
OverrideConnectTimeout: 6 * time.Second,
OverrideMeshGateway: structs.MeshGatewayConfig{Mode: structs.MeshGatewayModeRemote},

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,
},
},