From 9bbfa048a2af183e6a0522febd6f26cf4b232750 Mon Sep 17 00:00:00 2001 From: "Chris S. Kim" Date: Wed, 8 Sep 2021 11:59:30 -0400 Subject: [PATCH] Sync enterprise changes to oss (#10994) This commit updates OSS with files for enterprise-specific admin partitions feature work --- agent/config_endpoint.go | 2 +- agent/consul/enterprise_server_oss.go | 12 ++++ agent/consul/intention_endpoint.go | 16 +++-- agent/consul/intention_endpoint_test.go | 6 +- agent/consul/leader_intentions.go | 6 ++ agent/consul/state/config_entry_intention.go | 2 +- agent/consul/state/intention.go | 4 +- agent/consul/state/intention_test.go | 8 ++- agent/http_oss.go | 12 ++++ agent/intentions_endpoint.go | 41 ++++++++++--- agent/intentions_endpoint_test.go | 5 +- agent/structs/config_entry.go | 4 -- agent/structs/config_entry_intentions.go | 48 +++++++++++---- agent/structs/config_entry_intentions_test.go | 4 +- agent/structs/config_entry_test.go | 2 - agent/structs/intention.go | 60 ++++++++++++++----- agent/structs/intention_oss.go | 36 +++++++---- agent/structs/intention_test.go | 19 +++--- agent/structs/structs_filtering_test.go | 10 ++++ agent/structs/testing_intention.go | 4 +- 20 files changed, 228 insertions(+), 73 deletions(-) diff --git a/agent/config_endpoint.go b/agent/config_endpoint.go index 61f76cf7ed..c930986b41 100644 --- a/agent/config_endpoint.go +++ b/agent/config_endpoint.go @@ -109,7 +109,7 @@ func (s *HTTPHandlers) configDelete(resp http.ResponseWriter, req *http.Request) return reply, nil } -// ConfigCreate applies the given config entry update. +// ConfigApply applies the given config entry update. func (s *HTTPHandlers) ConfigApply(resp http.ResponseWriter, req *http.Request) (interface{}, error) { args := structs.ConfigEntryRequest{ Op: structs.ConfigEntryUpsert, diff --git a/agent/consul/enterprise_server_oss.go b/agent/consul/enterprise_server_oss.go index 12ede7bffd..8561d2de1a 100644 --- a/agent/consul/enterprise_server_oss.go +++ b/agent/consul/enterprise_server_oss.go @@ -59,6 +59,18 @@ func (s *Server) validateEnterpriseRequest(entMeta *structs.EnterpriseMeta, writ return nil } +func (s *Server) validateEnterpriseIntentionPartition(partition string) error { + if partition == "" { + return nil + } else if strings.ToLower(partition) == "default" { + return nil + } + + // No special handling for wildcard partitions as they are pointless in OSS. + + return errors.New("Partitions is a Consul Enterprise feature") +} + func (s *Server) validateEnterpriseIntentionNamespace(ns string, _ bool) error { if ns == "" { return nil diff --git a/agent/consul/intention_endpoint.go b/agent/consul/intention_endpoint.go index 67f000f5f3..25e35c891b 100644 --- a/agent/consul/intention_endpoint.go +++ b/agent/consul/intention_endpoint.go @@ -173,7 +173,7 @@ func (s *Intention) computeApplyChangesLegacyCreate( ) (*structs.IntentionMutation, error) { // This variant is just for legacy UUID-based intentions. - args.Intention.DefaultNamespaces(entMeta) + args.Intention.FillPartitionAndNamespace(entMeta, true) if !args.Intention.CanWrite(authz) { sn := args.Intention.SourceServiceName() @@ -257,12 +257,12 @@ func (s *Intention) computeApplyChangesLegacyUpdate( return nil, acl.ErrPermissionDenied } - args.Intention.DefaultNamespaces(entMeta) + args.Intention.FillPartitionAndNamespace(entMeta, true) // Prior to v1.9.0 renames of the destination side of an intention were // allowed, but that behavior doesn't work anymore. if ixn.DestinationServiceName() != args.Intention.DestinationServiceName() { - return nil, fmt.Errorf("Cannot modify DestinationNS or DestinationName for an intention once it exists.") + return nil, fmt.Errorf("Cannot modify Destination partition/namespace/name for an intention once it exists.") } // Default source type @@ -308,7 +308,7 @@ func (s *Intention) computeApplyChangesUpsert( return nil, fmt.Errorf("ID must not be specified") } - args.Intention.DefaultNamespaces(entMeta) + args.Intention.FillPartitionAndNamespace(entMeta, true) if !args.Intention.CanWrite(authz) { sn := args.Intention.SourceServiceName() @@ -389,7 +389,7 @@ func (s *Intention) computeApplyChangesDelete( entMeta *structs.EnterpriseMeta, args *structs.IntentionRequest, ) (*structs.IntentionMutation, error) { - args.Intention.DefaultNamespaces(entMeta) + args.Intention.FillPartitionAndNamespace(entMeta, true) if !args.Intention.CanWrite(authz) { sn := args.Intention.SourceServiceName() @@ -753,9 +753,15 @@ func (s *Intention) aclAccessorID(secretID string) string { } func (s *Intention) validateEnterpriseIntention(ixn *structs.Intention) error { + if err := s.srv.validateEnterpriseIntentionPartition(ixn.SourcePartition); err != nil { + return fmt.Errorf("Invalid source partition %q: %v", ixn.SourcePartition, err) + } if err := s.srv.validateEnterpriseIntentionNamespace(ixn.SourceNS, true); err != nil { return fmt.Errorf("Invalid source namespace %q: %v", ixn.SourceNS, err) } + if err := s.srv.validateEnterpriseIntentionPartition(ixn.DestinationPartition); err != nil { + return fmt.Errorf("Invalid destination partition %q: %v", ixn.DestinationPartition, err) + } if err := s.srv.validateEnterpriseIntentionNamespace(ixn.DestinationNS, true); err != nil { return fmt.Errorf("Invalid destination namespace %q: %v", ixn.DestinationNS, err) } diff --git a/agent/consul/intention_endpoint_test.go b/agent/consul/intention_endpoint_test.go index 4857bc09d8..bec1d6c4a2 100644 --- a/agent/consul/intention_endpoint_test.go +++ b/agent/consul/intention_endpoint_test.go @@ -74,6 +74,8 @@ func TestIntentionApply_new(t *testing.T) { actual.Hash = ixn.Intention.Hash //nolint:staticcheck ixn.Intention.UpdatePrecedence() + // Partition fields will be normalized on Intention.Get + ixn.Intention.NormalizePartitionFields() require.Equal(t, ixn.Intention, actual) } @@ -97,7 +99,7 @@ func TestIntentionApply_new(t *testing.T) { var reply string err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn2, &reply) - testutil.RequireErrorContains(t, err, "Cannot modify DestinationNS or DestinationName for an intention once it exists.") + testutil.RequireErrorContains(t, err, "Cannot modify Destination partition/namespace/name for an intention once it exists.") }) } @@ -265,6 +267,8 @@ func TestIntentionApply_updateGood(t *testing.T) { actual.Hash = ixn.Intention.Hash //nolint:staticcheck ixn.Intention.UpdatePrecedence() + // Partition fields will be normalized on Intention.Get + ixn.Intention.NormalizePartitionFields() require.Equal(t, ixn.Intention, actual) } } diff --git a/agent/consul/leader_intentions.go b/agent/consul/leader_intentions.go index 3c07de63d9..b4afc9c594 100644 --- a/agent/consul/leader_intentions.go +++ b/agent/consul/leader_intentions.go @@ -38,6 +38,9 @@ func (s *Server) startIntentionConfigEntryMigration(ctx context.Context) error { // datacenter is composed entirely of compatible servers and there are // no more legacy intentions. if s.DatacenterSupportsIntentionsAsConfigEntries() { + // NOTE: we only have to migrate legacy intentions from the default + // partition because partitions didn't exist when legacy intentions + // were canonical _, ixns, err := s.fsm.State().LegacyIntentions(nil, structs.WildcardEnterpriseMetaInDefaultPartition()) if err != nil { return err @@ -88,6 +91,9 @@ func (s *Server) legacyIntentionMigration(ctx context.Context) error { } state := s.fsm.State() + // NOTE: we only have to migrate legacy intentions from the default + // partition because partitions didn't exist when legacy intentions + // were canonical _, ixns, err := state.LegacyIntentions(nil, structs.WildcardEnterpriseMetaInDefaultPartition()) if err != nil { return err diff --git a/agent/consul/state/config_entry_intention.go b/agent/consul/state/config_entry_intention.go index c509ac38a9..4c7ac078ce 100644 --- a/agent/consul/state/config_entry_intention.go +++ b/agent/consul/state/config_entry_intention.go @@ -128,7 +128,7 @@ func configIntentionsListTxn(tx ReadTxn, ws memdb.WatchSet, entMeta *structs.Ent idx := maxIndexTxn(tx, tableConfigEntries) - iter, err := getConfigEntryKindsWithTxn(tx, structs.ServiceIntentions, structs.WildcardEnterpriseMetaInDefaultPartition()) + iter, err := getConfigEntryKindsWithTxn(tx, structs.ServiceIntentions, entMeta.WildcardEnterpriseMetaForPartition()) if err != nil { return 0, nil, false, fmt.Errorf("failed config entry lookup: %s", err) } diff --git a/agent/consul/state/intention.go b/agent/consul/state/intention.go index 2f0eca61d2..e74edc585b 100644 --- a/agent/consul/state/intention.go +++ b/agent/consul/state/intention.go @@ -3,12 +3,12 @@ package state import ( "errors" "fmt" - "github.com/hashicorp/consul/agent/connect" "sort" "github.com/hashicorp/go-memdb" "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/structs" ) @@ -1008,7 +1008,7 @@ func (s *Store) intentionTopologyTxn(tx ReadTxn, ws memdb.WatchSet, return true } return false - }, structs.WildcardEnterpriseMetaInDefaultPartition()) + }, target.WildcardEnterpriseMetaForPartition()) if err != nil { return index, nil, fmt.Errorf("failed to fetch catalog service list: %v", err) } diff --git a/agent/consul/state/intention_test.go b/agent/consul/state/intention_test.go index 5c24725c99..7b24e06406 100644 --- a/agent/consul/state/intention_test.go +++ b/agent/consul/state/intention_test.go @@ -155,6 +155,8 @@ func TestStore_IntentionSetGet_basic(t *testing.T) { expected.UpdatePrecedence() //nolint:staticcheck expected.SetHash() + + expected.NormalizePartitionFields() } require.True(t, watchFired(ws), "watch fired") @@ -1083,7 +1085,7 @@ func TestStore_IntentionsList(t *testing.T) { require.Equal(t, lastIndex, idx) testIntention := func(src, dst string) *structs.Intention { - return &structs.Intention{ + ret := &structs.Intention{ ID: testUUID(), SourceNS: "default", SourceName: src, @@ -1095,6 +1097,10 @@ func TestStore_IntentionsList(t *testing.T) { CreatedAt: testTimeA, UpdatedAt: testTimeA, } + if !legacy { + ret.NormalizePartitionFields() + } + return ret } testConfigEntry := func(dst string, srcs ...string) *structs.ServiceIntentionsConfigEntry { diff --git a/agent/http_oss.go b/agent/http_oss.go index 3eedaae0fa..79840a6d12 100644 --- a/agent/http_oss.go +++ b/agent/http_oss.go @@ -21,6 +21,18 @@ func (s *HTTPHandlers) parseEntMeta(req *http.Request, entMeta *structs.Enterpri return s.parseEntMetaPartition(req, entMeta) } +func (s *HTTPHandlers) validateEnterpriseIntentionPartition(logName, partition string) error { + if partition == "" { + return nil + } else if strings.ToLower(partition) == "default" { + return nil + } + + // No special handling for wildcard namespaces as they are pointless in OSS. + + return BadRequestError{Reason: "Invalid " + logName + "(" + partition + ")" + ": Partitions is a Consul Enterprise feature"} +} + func (s *HTTPHandlers) validateEnterpriseIntentionNamespace(logName, ns string, _ bool) error { if ns == "" { return nil diff --git a/agent/intentions_endpoint.go b/agent/intentions_endpoint.go index ba57e4733d..70faaa8ac1 100644 --- a/agent/intentions_endpoint.go +++ b/agent/intentions_endpoint.go @@ -54,6 +54,9 @@ func (s *HTTPHandlers) IntentionCreate(resp http.ResponseWriter, req *http.Reque if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil { return nil, err } + if entMeta.PartitionOrDefault() != structs.PartitionOrDefault("") { + return nil, BadRequestError{Reason: "Cannot use a partition with this endpoint"} + } args := structs.IntentionRequest{ Op: structs.IntentionOpCreate, @@ -64,7 +67,8 @@ func (s *HTTPHandlers) IntentionCreate(resp http.ResponseWriter, req *http.Reque return nil, fmt.Errorf("Failed to decode request body: %s", err) } - args.Intention.FillNonDefaultNamespaces(&entMeta) + // TODO(partitions): reject non-empty/non-default partitions from the decoded body + args.Intention.FillPartitionAndNamespace(&entMeta, false) if err := s.validateEnterpriseIntention(args.Intention); err != nil { return nil, err @@ -79,12 +83,19 @@ func (s *HTTPHandlers) IntentionCreate(resp http.ResponseWriter, req *http.Reque } func (s *HTTPHandlers) validateEnterpriseIntention(ixn *structs.Intention) error { + if err := s.validateEnterpriseIntentionPartition("SourcePartition", ixn.SourcePartition); err != nil { + return err + } + if err := s.validateEnterpriseIntentionPartition("DestinationPartition", ixn.DestinationPartition); err != nil { + return err + } if err := s.validateEnterpriseIntentionNamespace("SourceNS", ixn.SourceNS, true); err != nil { return err } if err := s.validateEnterpriseIntentionNamespace("DestinationNS", ixn.DestinationNS, true); err != nil { return err } + return nil } @@ -131,6 +142,7 @@ func (s *HTTPHandlers) IntentionMatch(resp http.ResponseWriter, req *http.Reques } args.Match.Entries[i] = structs.IntentionMatchEntry{ + Partition: entMeta.PartitionOrEmpty(), Namespace: ns, Name: name, } @@ -214,10 +226,12 @@ func (s *HTTPHandlers) IntentionCheck(resp http.ResponseWriter, req *http.Reques // We parse them the same way as matches to extract namespace/name args.Check.SourceName = source[0] if args.Check.SourceType == structs.IntentionSourceConsul { + // TODO(partitions): this func should return partition ns, name, err := parseIntentionStringComponent(source[0], &entMeta) if err != nil { return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) } + args.Check.SourcePartition = entMeta.PartitionOrEmpty() args.Check.SourceNS = ns args.Check.SourceName = name } @@ -227,6 +241,7 @@ func (s *HTTPHandlers) IntentionCheck(resp http.ResponseWriter, req *http.Reques if err != nil { return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) } + args.Check.DestinationPartition = entMeta.PartitionOrEmpty() args.Check.DestinationNS = ns args.Check.DestinationName = name @@ -269,6 +284,7 @@ func (s *HTTPHandlers) IntentionGetExact(resp http.ResponseWriter, req *http.Req if err != nil { return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) } + args.Exact.SourcePartition = entMeta.PartitionOrEmpty() args.Exact.SourceNS = ns args.Exact.SourceName = name } @@ -278,6 +294,7 @@ func (s *HTTPHandlers) IntentionGetExact(resp http.ResponseWriter, req *http.Req if err != nil { return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) } + args.Exact.DestinationPartition = entMeta.PartitionOrEmpty() args.Exact.DestinationNS = ns args.Exact.DestinationName = name } @@ -394,6 +411,9 @@ func (s *HTTPHandlers) IntentionSpecificUpdate(id string, resp http.ResponseWrit if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil { return nil, err } + if entMeta.PartitionOrDefault() != structs.PartitionOrDefault("") { + return nil, BadRequestError{Reason: "Cannot use a partition with this endpoint"} + } args := structs.IntentionRequest{ Op: structs.IntentionOpUpdate, @@ -404,7 +424,7 @@ func (s *HTTPHandlers) IntentionSpecificUpdate(id string, resp http.ResponseWrit return nil, BadRequestError{Reason: fmt.Sprintf("Request decode failed: %v", err)} } - args.Intention.FillNonDefaultNamespaces(&entMeta) + args.Intention.FillPartitionAndNamespace(&entMeta, false) // Use the ID from the URL args.Intention.ID = id @@ -444,12 +464,14 @@ func (s *HTTPHandlers) IntentionPutExact(resp http.ResponseWriter, req *http.Req args.Intention.ID = "" // Use the intention identity from the URL. + args.Intention.SourcePartition = exact.SourcePartition args.Intention.SourceNS = exact.SourceNS args.Intention.SourceName = exact.SourceName + args.Intention.DestinationPartition = exact.DestinationPartition args.Intention.DestinationNS = exact.DestinationNS args.Intention.DestinationName = exact.DestinationName - args.Intention.FillNonDefaultNamespaces(&entMeta) + args.Intention.FillPartitionAndNamespace(&entMeta, false) var ignored string if err := s.agent.RPC("Intention.Apply", &args, &ignored); err != nil { @@ -494,10 +516,12 @@ func (s *HTTPHandlers) IntentionDeleteExact(resp http.ResponseWriter, req *http. Op: structs.IntentionOpDelete, Intention: &structs.Intention{ // NOTE: ID is explicitly empty here - SourceNS: exact.SourceNS, - SourceName: exact.SourceName, - DestinationNS: exact.DestinationNS, - DestinationName: exact.DestinationName, + SourcePartition: exact.SourcePartition, + SourceNS: exact.SourceNS, + SourceName: exact.SourceName, + DestinationPartition: exact.DestinationPartition, + DestinationNS: exact.DestinationNS, + DestinationName: exact.DestinationName, }, } s.parseDC(req, &args.Datacenter) @@ -533,6 +557,7 @@ func parseIntentionQueryExact(req *http.Request, entMeta *structs.EnterpriseMeta if err != nil { return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) } + exact.SourcePartition = entMeta.PartitionOrEmpty() exact.SourceNS = ns exact.SourceName = name } @@ -542,6 +567,7 @@ func parseIntentionQueryExact(req *http.Request, entMeta *structs.EnterpriseMeta if err != nil { return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) } + exact.DestinationPartition = entMeta.PartitionOrEmpty() exact.DestinationNS = ns exact.DestinationName = name } @@ -549,6 +575,7 @@ func parseIntentionQueryExact(req *http.Request, entMeta *structs.EnterpriseMeta return &exact, nil } +// TODO(partitions): update to handle partitions func parseIntentionStringComponent(input string, entMeta *structs.EnterpriseMeta) (string, string, error) { // Get the index to the '/'. If it doesn't exist, we have just a name // so just set that and return. diff --git a/agent/intentions_endpoint_test.go b/agent/intentions_endpoint_test.go index 68f44d6bc8..65d14e27eb 100644 --- a/agent/intentions_endpoint_test.go +++ b/agent/intentions_endpoint_test.go @@ -6,11 +6,12 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/testrpc" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestIntentionList(t *testing.T) { diff --git a/agent/structs/config_entry.go b/agent/structs/config_entry.go index 114881ee0e..bcb7c59593 100644 --- a/agent/structs/config_entry.go +++ b/agent/structs/config_entry.go @@ -169,10 +169,6 @@ func (e *ServiceConfigEntry) Validate() error { if err != nil { validationErr = multierror.Append(validationErr, fmt.Errorf("error in upstream override for %s: %v", override.ServiceName(), err)) } - - if err := validateInnerEnterpriseMeta(&override.EnterpriseMeta, &e.EnterpriseMeta); err != nil { - validationErr = multierror.Append(validationErr, fmt.Errorf("error in upstream override for %s: %v", override.ServiceName(), err)) - } } if e.UpstreamConfig.Defaults != nil { diff --git a/agent/structs/config_entry_intentions.go b/agent/structs/config_entry_intentions.go index 2bd26c1188..9c18b78455 100644 --- a/agent/structs/config_entry_intentions.go +++ b/agent/structs/config_entry_intentions.go @@ -119,19 +119,22 @@ func (e *ServiceIntentionsConfigEntry) ToIntention(src *SourceIntention) *Intent } ixn := &Intention{ - ID: src.LegacyID, - Description: src.Description, - SourceNS: src.NamespaceOrDefault(), - SourceName: src.Name, - SourceType: src.Type, - Action: src.Action, - Permissions: src.Permissions, - Meta: meta, - Precedence: src.Precedence, - DestinationNS: e.NamespaceOrDefault(), - DestinationName: e.Name, - RaftIndex: e.RaftIndex, + ID: src.LegacyID, + Description: src.Description, + SourcePartition: src.PartitionOrEmpty(), + SourceNS: src.NamespaceOrDefault(), + SourceName: src.Name, + SourceType: src.Type, + Action: src.Action, + Permissions: src.Permissions, + Meta: meta, + Precedence: src.Precedence, + DestinationPartition: e.PartitionOrEmpty(), + DestinationNS: e.NamespaceOrDefault(), + DestinationName: e.Name, + RaftIndex: e.RaftIndex, } + if src.LegacyCreateTime != nil { ixn.CreatedAt = *src.LegacyCreateTime } @@ -764,6 +767,9 @@ func validateIntentionWildcards(name string, entMeta *EnterpriseMeta) error { return fmt.Errorf("Name: exact value cannot follow wildcard namespace") } } + if strings.Contains(entMeta.PartitionOrDefault(), WildcardSpecifier) { + return fmt.Errorf("Partition: cannot use wildcard '*' in partition") + } return nil } @@ -815,5 +821,23 @@ func MigrateIntentions(ixns Intentions) []*ServiceIntentionsConfigEntry { for _, entry := range collated { out = append(out, entry) } + sort.Slice(out, func(i, j int) bool { + a := out[i] + b := out[j] + + if a.PartitionOrDefault() < b.PartitionOrDefault() { + return true + } else if a.PartitionOrDefault() > b.PartitionOrDefault() { + return false + } + + if a.NamespaceOrDefault() < b.NamespaceOrDefault() { + return true + } else if a.NamespaceOrDefault() > b.NamespaceOrDefault() { + return false + } + + return a.Name < b.Name + }) return out } diff --git a/agent/structs/config_entry_intentions_test.go b/agent/structs/config_entry_intentions_test.go index 9802f7df51..f2add552b3 100644 --- a/agent/structs/config_entry_intentions_test.go +++ b/agent/structs/config_entry_intentions_test.go @@ -1296,7 +1296,7 @@ func TestMigrateIntentions(t *testing.T) { } anyTime := time.Now().UTC() - entMeta := NodeEnterpriseMetaInDefaultPartition() + entMeta := DefaultEnterpriseMetaInDefaultPartition() cases := map[string]testcase{ "nil": {}, @@ -1487,7 +1487,7 @@ func TestMigrateIntentions(t *testing.T) { tc := tc t.Run(name, func(t *testing.T) { got := MigrateIntentions(tc.in) - require.ElementsMatch(t, tc.expect, got) + require.Equal(t, tc.expect, got) }) } } diff --git a/agent/structs/config_entry_test.go b/agent/structs/config_entry_test.go index 2844ea8c7a..354c24e5f7 100644 --- a/agent/structs/config_entry_test.go +++ b/agent/structs/config_entry_test.go @@ -2418,8 +2418,6 @@ func testConfigEntryNormalizeAndValidate(t *testing.T, cases map[string]configEn err = tc.entry.Validate() if tc.validateErr != "" { - // require.Error(t, err) - // require.Contains(t, err.Error(), tc.validateErr) testutil.RequireErrorContains(t, err, tc.validateErr) return } diff --git a/agent/structs/intention.go b/agent/structs/intention.go index c2240c4149..d30a4c0fc9 100644 --- a/agent/structs/intention.go +++ b/agent/structs/intention.go @@ -52,6 +52,11 @@ type Intention struct { SourceNS, SourceName string DestinationNS, DestinationName string + // SourcePartition and DestinationPartition cannot be wildcards "*" and + // are not compatible with legacy intentions. + SourcePartition string `json:",omitempty"` + DestinationPartition string `json:",omitempty"` + // SourceType is the type of the value for the source. SourceType IntentionSourceType @@ -111,10 +116,12 @@ func (t *Intention) Clone() *Intention { func (t *Intention) ToExact() *IntentionQueryExact { return &IntentionQueryExact{ - SourceNS: t.SourceNS, - SourceName: t.SourceName, - DestinationNS: t.DestinationNS, - DestinationName: t.DestinationName, + SourcePartition: t.SourcePartition, + SourceNS: t.SourceNS, + SourceName: t.SourceName, + DestinationPartition: t.DestinationPartition, + DestinationNS: t.DestinationNS, + DestinationName: t.DestinationName, } } @@ -379,6 +386,16 @@ func (x *Intention) String() string { idPart = "ID: " + x.ID + ", " } + var srcPartitionPart string + if x.SourcePartition != "" { + srcPartitionPart = x.SourcePartition + "/" + } + + var dstPartitionPart string + if x.DestinationPartition != "" { + dstPartitionPart = x.DestinationPartition + "/" + } + var detailPart string if len(x.Permissions) > 0 { detailPart = fmt.Sprintf("Permissions: %d", len(x.Permissions)) @@ -386,9 +403,9 @@ func (x *Intention) String() string { detailPart = "Action: " + strings.ToUpper(string(x.Action)) } - return fmt.Sprintf("%s/%s => %s/%s (%sPrecedence: %d, %s)", - x.SourceNS, x.SourceName, - x.DestinationNS, x.DestinationName, + return fmt.Sprintf("%s%s/%s => %s%s/%s (%sPrecedence: %d, %s)", + srcPartitionPart, x.SourceNS, x.SourceName, + dstPartitionPart, x.DestinationNS, x.DestinationName, idPart, x.Precedence, detailPart, @@ -625,6 +642,7 @@ type IntentionQueryMatch struct { // IntentionMatchEntry is a single entry for matching an intention. type IntentionMatchEntry struct { + Partition string `json:",omitempty"` Namespace string Name string } @@ -637,6 +655,10 @@ type IntentionQueryCheck struct { SourceNS, SourceName string DestinationNS, DestinationName string + // TODO(partitions): check query works with partitions + SourcePartition string `json:",omitempty"` + DestinationPartition string `json:",omitempty"` + // SourceType is the type of the value for the source. SourceType IntentionSourceType } @@ -673,9 +695,13 @@ type IntentionDecisionSummary struct { type IntentionQueryExact struct { SourceNS, SourceName string DestinationNS, DestinationName string + + // TODO(partitions): check query works with partitions + SourcePartition string `json:",omitempty"` + DestinationPartition string `json:",omitempty"` } -// Validate is used to ensure all 4 parameters are specified. +// Validate is used to ensure all 4 required parameters are specified. func (q *IntentionQueryExact) Validate() error { var err error if q.SourceNS == "" { @@ -721,18 +747,24 @@ func (s IntentionPrecedenceSorter) Less(i, j int) bool { return a.Precedence > b.Precedence } - // Tie break on lexicographic order of the 4-tuple in canonical form (SrcNS, - // Src, DstNS, Dst). This is arbitrary but it keeps sorting deterministic - // which is a nice property for consistency. It is arguably open to abuse if - // implementations rely on this however by definition the order among - // same-precedence rules is arbitrary and doesn't affect whether an allow or - // deny rule is acted on since all applicable rules are checked. + // Tie break on lexicographic order of the tuple in canonical form (SrcPxn, + // SrcNS, Src, DstPxn, DstNS, Dst). This is arbitrary but it keeps sorting + // deterministic which is a nice property for consistency. It is arguably + // open to abuse if implementations rely on this however by definition the + // order among same-precedence rules is arbitrary and doesn't affect whether + // an allow or deny rule is acted on since all applicable rules are checked. + if a.SourcePartition != b.SourcePartition { + return a.SourcePartition < b.SourcePartition + } if a.SourceNS != b.SourceNS { return a.SourceNS < b.SourceNS } if a.SourceName != b.SourceName { return a.SourceName < b.SourceName } + if a.DestinationPartition != b.DestinationPartition { + return a.DestinationPartition < b.DestinationPartition + } if a.DestinationNS != b.DestinationNS { return a.DestinationNS < b.DestinationNS } diff --git a/agent/structs/intention_oss.go b/agent/structs/intention_oss.go index 7f2f45b989..274f799d65 100644 --- a/agent/structs/intention_oss.go +++ b/agent/structs/intention_oss.go @@ -47,21 +47,35 @@ func (_ *IntentionQueryCheck) FillAuthzContext(_ *acl.AuthorizerContext) { // do nothing } -// DefaultNamespaces will populate both the SourceNS and DestinationNS fields -// if they are empty with the proper defaults. -func (ixn *Intention) DefaultNamespaces(_ *EnterpriseMeta) { - // Until we support namespaces, we force all namespaces to be default +// FillPartitionAndNamespace will fill in empty source and destination partition/namespaces. +// If fillDefault is true, all fields are defaulted when the given enterprise meta does not +// specify them. +// +// fillDefault MUST be true on servers to ensure that all fields are populated on writes. +// fillDefault MUST be false on clients so that servers can correctly fill in the +// namespace/partition of the ACL token. +func (ixn *Intention) FillPartitionAndNamespace(entMeta *EnterpriseMeta, fillDefault bool) { + if ixn == nil { + return + } + var ns = entMeta.NamespaceOrEmpty() + if fillDefault { + if ns == "" { + ns = IntentionDefaultNamespace + } + } if ixn.SourceNS == "" { - ixn.SourceNS = IntentionDefaultNamespace + ixn.SourceNS = ns } if ixn.DestinationNS == "" { - ixn.DestinationNS = IntentionDefaultNamespace + ixn.DestinationNS = ns } + + ixn.SourcePartition = "" + ixn.DestinationPartition = "" } -// FillNonDefaultNamespaces will populate the SourceNS and DestinationNS fields -// if they are empty with the proper defaults, but only if the proper defaults -// are themselves not "default". -func (ixn *Intention) FillNonDefaultNamespaces(_ *EnterpriseMeta) { - // do nothing +func (ixn *Intention) NormalizePartitionFields() { + ixn.SourcePartition = "" + ixn.DestinationPartition = "" } diff --git a/agent/structs/intention_test.go b/agent/structs/intention_test.go index 5b321cc516..6ca822c509 100644 --- a/agent/structs/intention_test.go +++ b/agent/structs/intention_test.go @@ -369,6 +369,11 @@ func TestIntention_String(t *testing.T) { testID := generateUUID() + partitionPrefix := DefaultEnterpriseMetaInDefaultPartition().PartitionOrEmpty() + if partitionPrefix != "" { + partitionPrefix += "/" + } + cases := map[string]testcase{ "legacy allow": { &Intention{ @@ -377,7 +382,7 @@ func TestIntention_String(t *testing.T) { DestinationName: "bar", Action: IntentionActionAllow, }, - `default/foo => default/bar (ID: ` + testID + `, Precedence: 9, Action: ALLOW)`, + partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (ID: ` + testID + `, Precedence: 9, Action: ALLOW)`, }, "legacy deny": { &Intention{ @@ -386,7 +391,7 @@ func TestIntention_String(t *testing.T) { DestinationName: "bar", Action: IntentionActionDeny, }, - `default/foo => default/bar (ID: ` + testID + `, Precedence: 9, Action: DENY)`, + partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (ID: ` + testID + `, Precedence: 9, Action: DENY)`, }, "L4 allow": { &Intention{ @@ -394,7 +399,7 @@ func TestIntention_String(t *testing.T) { DestinationName: "bar", Action: IntentionActionAllow, }, - `default/foo => default/bar (Precedence: 9, Action: ALLOW)`, + partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Action: ALLOW)`, }, "L4 deny": { &Intention{ @@ -402,7 +407,7 @@ func TestIntention_String(t *testing.T) { DestinationName: "bar", Action: IntentionActionDeny, }, - `default/foo => default/bar (Precedence: 9, Action: DENY)`, + partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Action: DENY)`, }, "L7 one perm": { &Intention{ @@ -417,7 +422,7 @@ func TestIntention_String(t *testing.T) { }, }, }, - `default/foo => default/bar (Precedence: 9, Permissions: 1)`, + partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Permissions: 1)`, }, "L7 two perms": { &Intention{ @@ -438,14 +443,14 @@ func TestIntention_String(t *testing.T) { }, }, }, - `default/foo => default/bar (Precedence: 9, Permissions: 2)`, + partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Permissions: 2)`, }, } for name, tc := range cases { tc := tc // Add a bunch of required fields. - tc.ixn.DefaultNamespaces(DefaultEnterpriseMetaInDefaultPartition()) + tc.ixn.FillPartitionAndNamespace(DefaultEnterpriseMetaInDefaultPartition(), true) tc.ixn.UpdatePrecedence() t.Run(name, func(t *testing.T) { diff --git a/agent/structs/structs_filtering_test.go b/agent/structs/structs_filtering_test.go index 6dc886e38a..b094cf5bdd 100644 --- a/agent/structs/structs_filtering_test.go +++ b/agent/structs/structs_filtering_test.go @@ -652,6 +652,11 @@ var expectedFieldConfigIntention bexpr.FieldConfigurations = bexpr.FieldConfigur CoerceFn: bexpr.CoerceString, SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, }, + "SourcePartition": &bexpr.FieldConfiguration{ + StructFieldName: "SourcePartition", + CoerceFn: bexpr.CoerceString, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, + }, "SourceNS": &bexpr.FieldConfiguration{ StructFieldName: "SourceNS", CoerceFn: bexpr.CoerceString, @@ -662,6 +667,11 @@ var expectedFieldConfigIntention bexpr.FieldConfigurations = bexpr.FieldConfigur CoerceFn: bexpr.CoerceString, SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, }, + "DestinationPartition": &bexpr.FieldConfiguration{ + StructFieldName: "DestinationPartition", + CoerceFn: bexpr.CoerceString, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, + }, "DestinationNS": &bexpr.FieldConfiguration{ StructFieldName: "DestinationNS", CoerceFn: bexpr.CoerceString, diff --git a/agent/structs/testing_intention.go b/agent/structs/testing_intention.go index 10991f8841..3497ba2fcd 100644 --- a/agent/structs/testing_intention.go +++ b/agent/structs/testing_intention.go @@ -6,7 +6,7 @@ import ( // TestIntention returns a valid, uninserted (no ID set) intention. func TestIntention(t testing.T) *Intention { - return &Intention{ + ixn := &Intention{ SourceNS: IntentionDefaultNamespace, SourceName: "api", DestinationNS: IntentionDefaultNamespace, @@ -15,4 +15,6 @@ func TestIntention(t testing.T) *Intention { SourceType: IntentionSourceConsul, Meta: map[string]string{}, } + ixn.NormalizePartitionFields() + return ixn }