diff --git a/agent/connect/authz.go b/agent/connect/authz.go index 605a447374..d3008a1a69 100644 --- a/agent/connect/authz.go +++ b/agent/connect/authz.go @@ -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 diff --git a/agent/connect/authz_test.go b/agent/connect/authz_test.go index 4033abb86f..faebd09ce6 100644 --- a/agent/connect/authz_test.go +++ b/agent/connect/authz_test.go @@ -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) }) diff --git a/agent/connect_auth.go b/agent/connect_auth.go index 5d40ed630a..d27e98bd74 100644 --- a/agent/connect_auth.go +++ b/agent/connect_auth.go @@ -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 } diff --git a/agent/consul/intention_endpoint.go b/agent/consul/intention_endpoint.go index bd763a794c..1c5fec9614 100644 --- a/agent/consul/intention_endpoint.go +++ b/agent/consul/intention_endpoint.go @@ -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) diff --git a/agent/consul/internal_endpoint.go b/agent/consul/internal_endpoint.go index d9ab64b541..03b8a55205 100644 --- a/agent/consul/internal_endpoint.go +++ b/agent/consul/internal_endpoint.go @@ -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) diff --git a/agent/consul/internal_endpoint_test.go b/agent/consul/internal_endpoint_test.go index ee592d4d7a..d5a49525a8 100644 --- a/agent/consul/internal_endpoint_test.go +++ b/agent/consul/internal_endpoint_test.go @@ -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", }, }, diff --git a/agent/consul/state/catalog.go b/agent/consul/state/catalog.go index 927455c563..8308183afa 100644 --- a/agent/consul/state/catalog.go +++ b/agent/consul/state/catalog.go @@ -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) diff --git a/agent/consul/state/intention.go b/agent/consul/state/intention.go index f6f79ec765..8ddab2aa99 100644 --- a/agent/consul/state/intention.go +++ b/agent/consul/state/intention.go @@ -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 { diff --git a/agent/consul/state/intention_test.go b/agent/consul/state/intention_test.go index 7b24e06406..c6da01fdc8 100644 --- a/agent/consul/state/intention_test.go +++ b/agent/consul/state/intention_test.go @@ -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) diff --git a/agent/proxycfg/connect_proxy.go b/agent/proxycfg/connect_proxy.go index 2d2762730d..54886335ec 100644 --- a/agent/proxycfg/connect_proxy.go +++ b/agent/proxycfg/connect_proxy.go @@ -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, diff --git a/agent/ui_endpoint.go b/agent/ui_endpoint.go index a2626135dd..6b0fb28fb8 100644 --- a/agent/ui_endpoint.go +++ b/agent/ui_endpoint.go @@ -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, }, },