Sync enterprise changes to oss (#10994)

This commit updates OSS with files for enterprise-specific admin partitions feature work
This commit is contained in:
Chris S. Kim 2021-09-08 11:59:30 -04:00 committed by GitHub
parent 06f3ccebce
commit 9bbfa048a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 228 additions and 73 deletions

View File

@ -109,7 +109,7 @@ func (s *HTTPHandlers) configDelete(resp http.ResponseWriter, req *http.Request)
return reply, nil 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) { func (s *HTTPHandlers) ConfigApply(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
args := structs.ConfigEntryRequest{ args := structs.ConfigEntryRequest{
Op: structs.ConfigEntryUpsert, Op: structs.ConfigEntryUpsert,

View File

@ -59,6 +59,18 @@ func (s *Server) validateEnterpriseRequest(entMeta *structs.EnterpriseMeta, writ
return nil 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 { func (s *Server) validateEnterpriseIntentionNamespace(ns string, _ bool) error {
if ns == "" { if ns == "" {
return nil return nil

View File

@ -173,7 +173,7 @@ func (s *Intention) computeApplyChangesLegacyCreate(
) (*structs.IntentionMutation, error) { ) (*structs.IntentionMutation, error) {
// This variant is just for legacy UUID-based intentions. // This variant is just for legacy UUID-based intentions.
args.Intention.DefaultNamespaces(entMeta) args.Intention.FillPartitionAndNamespace(entMeta, true)
if !args.Intention.CanWrite(authz) { if !args.Intention.CanWrite(authz) {
sn := args.Intention.SourceServiceName() sn := args.Intention.SourceServiceName()
@ -257,12 +257,12 @@ func (s *Intention) computeApplyChangesLegacyUpdate(
return nil, acl.ErrPermissionDenied 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 // Prior to v1.9.0 renames of the destination side of an intention were
// allowed, but that behavior doesn't work anymore. // allowed, but that behavior doesn't work anymore.
if ixn.DestinationServiceName() != args.Intention.DestinationServiceName() { 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 // Default source type
@ -308,7 +308,7 @@ func (s *Intention) computeApplyChangesUpsert(
return nil, fmt.Errorf("ID must not be specified") return nil, fmt.Errorf("ID must not be specified")
} }
args.Intention.DefaultNamespaces(entMeta) args.Intention.FillPartitionAndNamespace(entMeta, true)
if !args.Intention.CanWrite(authz) { if !args.Intention.CanWrite(authz) {
sn := args.Intention.SourceServiceName() sn := args.Intention.SourceServiceName()
@ -389,7 +389,7 @@ func (s *Intention) computeApplyChangesDelete(
entMeta *structs.EnterpriseMeta, entMeta *structs.EnterpriseMeta,
args *structs.IntentionRequest, args *structs.IntentionRequest,
) (*structs.IntentionMutation, error) { ) (*structs.IntentionMutation, error) {
args.Intention.DefaultNamespaces(entMeta) args.Intention.FillPartitionAndNamespace(entMeta, true)
if !args.Intention.CanWrite(authz) { if !args.Intention.CanWrite(authz) {
sn := args.Intention.SourceServiceName() sn := args.Intention.SourceServiceName()
@ -753,9 +753,15 @@ func (s *Intention) aclAccessorID(secretID string) string {
} }
func (s *Intention) validateEnterpriseIntention(ixn *structs.Intention) error { 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 { if err := s.srv.validateEnterpriseIntentionNamespace(ixn.SourceNS, true); err != nil {
return fmt.Errorf("Invalid source namespace %q: %v", ixn.SourceNS, err) 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 { if err := s.srv.validateEnterpriseIntentionNamespace(ixn.DestinationNS, true); err != nil {
return fmt.Errorf("Invalid destination namespace %q: %v", ixn.DestinationNS, err) return fmt.Errorf("Invalid destination namespace %q: %v", ixn.DestinationNS, err)
} }

View File

@ -74,6 +74,8 @@ func TestIntentionApply_new(t *testing.T) {
actual.Hash = ixn.Intention.Hash actual.Hash = ixn.Intention.Hash
//nolint:staticcheck //nolint:staticcheck
ixn.Intention.UpdatePrecedence() ixn.Intention.UpdatePrecedence()
// Partition fields will be normalized on Intention.Get
ixn.Intention.NormalizePartitionFields()
require.Equal(t, ixn.Intention, actual) require.Equal(t, ixn.Intention, actual)
} }
@ -97,7 +99,7 @@ func TestIntentionApply_new(t *testing.T) {
var reply string var reply string
err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn2, &reply) 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 actual.Hash = ixn.Intention.Hash
//nolint:staticcheck //nolint:staticcheck
ixn.Intention.UpdatePrecedence() ixn.Intention.UpdatePrecedence()
// Partition fields will be normalized on Intention.Get
ixn.Intention.NormalizePartitionFields()
require.Equal(t, ixn.Intention, actual) require.Equal(t, ixn.Intention, actual)
} }
} }

View File

@ -38,6 +38,9 @@ func (s *Server) startIntentionConfigEntryMigration(ctx context.Context) error {
// datacenter is composed entirely of compatible servers and there are // datacenter is composed entirely of compatible servers and there are
// no more legacy intentions. // no more legacy intentions.
if s.DatacenterSupportsIntentionsAsConfigEntries() { 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()) _, ixns, err := s.fsm.State().LegacyIntentions(nil, structs.WildcardEnterpriseMetaInDefaultPartition())
if err != nil { if err != nil {
return err return err
@ -88,6 +91,9 @@ func (s *Server) legacyIntentionMigration(ctx context.Context) error {
} }
state := s.fsm.State() 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()) _, ixns, err := state.LegacyIntentions(nil, structs.WildcardEnterpriseMetaInDefaultPartition())
if err != nil { if err != nil {
return err return err

View File

@ -128,7 +128,7 @@ func configIntentionsListTxn(tx ReadTxn, ws memdb.WatchSet, entMeta *structs.Ent
idx := maxIndexTxn(tx, tableConfigEntries) idx := maxIndexTxn(tx, tableConfigEntries)
iter, err := getConfigEntryKindsWithTxn(tx, structs.ServiceIntentions, structs.WildcardEnterpriseMetaInDefaultPartition()) iter, err := getConfigEntryKindsWithTxn(tx, structs.ServiceIntentions, entMeta.WildcardEnterpriseMetaForPartition())
if err != nil { if err != nil {
return 0, nil, false, fmt.Errorf("failed config entry lookup: %s", err) return 0, nil, false, fmt.Errorf("failed config entry lookup: %s", err)
} }

View File

@ -3,12 +3,12 @@ package state
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/hashicorp/consul/agent/connect"
"sort" "sort"
"github.com/hashicorp/go-memdb" "github.com/hashicorp/go-memdb"
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
) )
@ -1008,7 +1008,7 @@ func (s *Store) intentionTopologyTxn(tx ReadTxn, ws memdb.WatchSet,
return true return true
} }
return false return false
}, structs.WildcardEnterpriseMetaInDefaultPartition()) }, target.WildcardEnterpriseMetaForPartition())
if err != nil { if err != nil {
return index, nil, fmt.Errorf("failed to fetch catalog service list: %v", err) return index, nil, fmt.Errorf("failed to fetch catalog service list: %v", err)
} }

View File

@ -155,6 +155,8 @@ func TestStore_IntentionSetGet_basic(t *testing.T) {
expected.UpdatePrecedence() expected.UpdatePrecedence()
//nolint:staticcheck //nolint:staticcheck
expected.SetHash() expected.SetHash()
expected.NormalizePartitionFields()
} }
require.True(t, watchFired(ws), "watch fired") require.True(t, watchFired(ws), "watch fired")
@ -1083,7 +1085,7 @@ func TestStore_IntentionsList(t *testing.T) {
require.Equal(t, lastIndex, idx) require.Equal(t, lastIndex, idx)
testIntention := func(src, dst string) *structs.Intention { testIntention := func(src, dst string) *structs.Intention {
return &structs.Intention{ ret := &structs.Intention{
ID: testUUID(), ID: testUUID(),
SourceNS: "default", SourceNS: "default",
SourceName: src, SourceName: src,
@ -1095,6 +1097,10 @@ func TestStore_IntentionsList(t *testing.T) {
CreatedAt: testTimeA, CreatedAt: testTimeA,
UpdatedAt: testTimeA, UpdatedAt: testTimeA,
} }
if !legacy {
ret.NormalizePartitionFields()
}
return ret
} }
testConfigEntry := func(dst string, srcs ...string) *structs.ServiceIntentionsConfigEntry { testConfigEntry := func(dst string, srcs ...string) *structs.ServiceIntentionsConfigEntry {

View File

@ -21,6 +21,18 @@ func (s *HTTPHandlers) parseEntMeta(req *http.Request, entMeta *structs.Enterpri
return s.parseEntMetaPartition(req, entMeta) 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 { func (s *HTTPHandlers) validateEnterpriseIntentionNamespace(logName, ns string, _ bool) error {
if ns == "" { if ns == "" {
return nil return nil

View File

@ -54,6 +54,9 @@ func (s *HTTPHandlers) IntentionCreate(resp http.ResponseWriter, req *http.Reque
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil { if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
return nil, err return nil, err
} }
if entMeta.PartitionOrDefault() != structs.PartitionOrDefault("") {
return nil, BadRequestError{Reason: "Cannot use a partition with this endpoint"}
}
args := structs.IntentionRequest{ args := structs.IntentionRequest{
Op: structs.IntentionOpCreate, 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) 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 { if err := s.validateEnterpriseIntention(args.Intention); err != nil {
return nil, err 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 { 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 { if err := s.validateEnterpriseIntentionNamespace("SourceNS", ixn.SourceNS, true); err != nil {
return err return err
} }
if err := s.validateEnterpriseIntentionNamespace("DestinationNS", ixn.DestinationNS, true); err != nil { if err := s.validateEnterpriseIntentionNamespace("DestinationNS", ixn.DestinationNS, true); err != nil {
return err return err
} }
return nil return nil
} }
@ -131,6 +142,7 @@ func (s *HTTPHandlers) IntentionMatch(resp http.ResponseWriter, req *http.Reques
} }
args.Match.Entries[i] = structs.IntentionMatchEntry{ args.Match.Entries[i] = structs.IntentionMatchEntry{
Partition: entMeta.PartitionOrEmpty(),
Namespace: ns, Namespace: ns,
Name: name, 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 // We parse them the same way as matches to extract namespace/name
args.Check.SourceName = source[0] args.Check.SourceName = source[0]
if args.Check.SourceType == structs.IntentionSourceConsul { if args.Check.SourceType == structs.IntentionSourceConsul {
// TODO(partitions): this func should return partition
ns, name, err := parseIntentionStringComponent(source[0], &entMeta) ns, name, err := parseIntentionStringComponent(source[0], &entMeta)
if err != nil { if err != nil {
return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) return nil, fmt.Errorf("source %q is invalid: %s", source[0], err)
} }
args.Check.SourcePartition = entMeta.PartitionOrEmpty()
args.Check.SourceNS = ns args.Check.SourceNS = ns
args.Check.SourceName = name args.Check.SourceName = name
} }
@ -227,6 +241,7 @@ func (s *HTTPHandlers) IntentionCheck(resp http.ResponseWriter, req *http.Reques
if err != nil { if err != nil {
return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err)
} }
args.Check.DestinationPartition = entMeta.PartitionOrEmpty()
args.Check.DestinationNS = ns args.Check.DestinationNS = ns
args.Check.DestinationName = name args.Check.DestinationName = name
@ -269,6 +284,7 @@ func (s *HTTPHandlers) IntentionGetExact(resp http.ResponseWriter, req *http.Req
if err != nil { if err != nil {
return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) return nil, fmt.Errorf("source %q is invalid: %s", source[0], err)
} }
args.Exact.SourcePartition = entMeta.PartitionOrEmpty()
args.Exact.SourceNS = ns args.Exact.SourceNS = ns
args.Exact.SourceName = name args.Exact.SourceName = name
} }
@ -278,6 +294,7 @@ func (s *HTTPHandlers) IntentionGetExact(resp http.ResponseWriter, req *http.Req
if err != nil { if err != nil {
return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err)
} }
args.Exact.DestinationPartition = entMeta.PartitionOrEmpty()
args.Exact.DestinationNS = ns args.Exact.DestinationNS = ns
args.Exact.DestinationName = name 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 { if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
return nil, err return nil, err
} }
if entMeta.PartitionOrDefault() != structs.PartitionOrDefault("") {
return nil, BadRequestError{Reason: "Cannot use a partition with this endpoint"}
}
args := structs.IntentionRequest{ args := structs.IntentionRequest{
Op: structs.IntentionOpUpdate, 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)} 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 // Use the ID from the URL
args.Intention.ID = id args.Intention.ID = id
@ -444,12 +464,14 @@ func (s *HTTPHandlers) IntentionPutExact(resp http.ResponseWriter, req *http.Req
args.Intention.ID = "" args.Intention.ID = ""
// Use the intention identity from the URL. // Use the intention identity from the URL.
args.Intention.SourcePartition = exact.SourcePartition
args.Intention.SourceNS = exact.SourceNS args.Intention.SourceNS = exact.SourceNS
args.Intention.SourceName = exact.SourceName args.Intention.SourceName = exact.SourceName
args.Intention.DestinationPartition = exact.DestinationPartition
args.Intention.DestinationNS = exact.DestinationNS args.Intention.DestinationNS = exact.DestinationNS
args.Intention.DestinationName = exact.DestinationName args.Intention.DestinationName = exact.DestinationName
args.Intention.FillNonDefaultNamespaces(&entMeta) args.Intention.FillPartitionAndNamespace(&entMeta, false)
var ignored string var ignored string
if err := s.agent.RPC("Intention.Apply", &args, &ignored); err != nil { 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, Op: structs.IntentionOpDelete,
Intention: &structs.Intention{ Intention: &structs.Intention{
// NOTE: ID is explicitly empty here // NOTE: ID is explicitly empty here
SourceNS: exact.SourceNS, SourcePartition: exact.SourcePartition,
SourceName: exact.SourceName, SourceNS: exact.SourceNS,
DestinationNS: exact.DestinationNS, SourceName: exact.SourceName,
DestinationName: exact.DestinationName, DestinationPartition: exact.DestinationPartition,
DestinationNS: exact.DestinationNS,
DestinationName: exact.DestinationName,
}, },
} }
s.parseDC(req, &args.Datacenter) s.parseDC(req, &args.Datacenter)
@ -533,6 +557,7 @@ func parseIntentionQueryExact(req *http.Request, entMeta *structs.EnterpriseMeta
if err != nil { if err != nil {
return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) return nil, fmt.Errorf("source %q is invalid: %s", source[0], err)
} }
exact.SourcePartition = entMeta.PartitionOrEmpty()
exact.SourceNS = ns exact.SourceNS = ns
exact.SourceName = name exact.SourceName = name
} }
@ -542,6 +567,7 @@ func parseIntentionQueryExact(req *http.Request, entMeta *structs.EnterpriseMeta
if err != nil { if err != nil {
return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err)
} }
exact.DestinationPartition = entMeta.PartitionOrEmpty()
exact.DestinationNS = ns exact.DestinationNS = ns
exact.DestinationName = name exact.DestinationName = name
} }
@ -549,6 +575,7 @@ func parseIntentionQueryExact(req *http.Request, entMeta *structs.EnterpriseMeta
return &exact, nil return &exact, nil
} }
// TODO(partitions): update to handle partitions
func parseIntentionStringComponent(input string, entMeta *structs.EnterpriseMeta) (string, string, error) { 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 // Get the index to the '/'. If it doesn't exist, we have just a name
// so just set that and return. // so just set that and return.

View File

@ -6,11 +6,12 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc" "github.com/hashicorp/consul/testrpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestIntentionList(t *testing.T) { func TestIntentionList(t *testing.T) {

View File

@ -169,10 +169,6 @@ func (e *ServiceConfigEntry) Validate() error {
if err != nil { if err != nil {
validationErr = multierror.Append(validationErr, fmt.Errorf("error in upstream override for %s: %v", override.ServiceName(), err)) 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 { if e.UpstreamConfig.Defaults != nil {

View File

@ -119,19 +119,22 @@ func (e *ServiceIntentionsConfigEntry) ToIntention(src *SourceIntention) *Intent
} }
ixn := &Intention{ ixn := &Intention{
ID: src.LegacyID, ID: src.LegacyID,
Description: src.Description, Description: src.Description,
SourceNS: src.NamespaceOrDefault(), SourcePartition: src.PartitionOrEmpty(),
SourceName: src.Name, SourceNS: src.NamespaceOrDefault(),
SourceType: src.Type, SourceName: src.Name,
Action: src.Action, SourceType: src.Type,
Permissions: src.Permissions, Action: src.Action,
Meta: meta, Permissions: src.Permissions,
Precedence: src.Precedence, Meta: meta,
DestinationNS: e.NamespaceOrDefault(), Precedence: src.Precedence,
DestinationName: e.Name, DestinationPartition: e.PartitionOrEmpty(),
RaftIndex: e.RaftIndex, DestinationNS: e.NamespaceOrDefault(),
DestinationName: e.Name,
RaftIndex: e.RaftIndex,
} }
if src.LegacyCreateTime != nil { if src.LegacyCreateTime != nil {
ixn.CreatedAt = *src.LegacyCreateTime 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") 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 return nil
} }
@ -815,5 +821,23 @@ func MigrateIntentions(ixns Intentions) []*ServiceIntentionsConfigEntry {
for _, entry := range collated { for _, entry := range collated {
out = append(out, entry) 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 return out
} }

View File

@ -1296,7 +1296,7 @@ func TestMigrateIntentions(t *testing.T) {
} }
anyTime := time.Now().UTC() anyTime := time.Now().UTC()
entMeta := NodeEnterpriseMetaInDefaultPartition() entMeta := DefaultEnterpriseMetaInDefaultPartition()
cases := map[string]testcase{ cases := map[string]testcase{
"nil": {}, "nil": {},
@ -1487,7 +1487,7 @@ func TestMigrateIntentions(t *testing.T) {
tc := tc tc := tc
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
got := MigrateIntentions(tc.in) got := MigrateIntentions(tc.in)
require.ElementsMatch(t, tc.expect, got) require.Equal(t, tc.expect, got)
}) })
} }
} }

View File

@ -2418,8 +2418,6 @@ func testConfigEntryNormalizeAndValidate(t *testing.T, cases map[string]configEn
err = tc.entry.Validate() err = tc.entry.Validate()
if tc.validateErr != "" { if tc.validateErr != "" {
// require.Error(t, err)
// require.Contains(t, err.Error(), tc.validateErr)
testutil.RequireErrorContains(t, err, tc.validateErr) testutil.RequireErrorContains(t, err, tc.validateErr)
return return
} }

View File

@ -52,6 +52,11 @@ type Intention struct {
SourceNS, SourceName string SourceNS, SourceName string
DestinationNS, DestinationName 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 is the type of the value for the source.
SourceType IntentionSourceType SourceType IntentionSourceType
@ -111,10 +116,12 @@ func (t *Intention) Clone() *Intention {
func (t *Intention) ToExact() *IntentionQueryExact { func (t *Intention) ToExact() *IntentionQueryExact {
return &IntentionQueryExact{ return &IntentionQueryExact{
SourceNS: t.SourceNS, SourcePartition: t.SourcePartition,
SourceName: t.SourceName, SourceNS: t.SourceNS,
DestinationNS: t.DestinationNS, SourceName: t.SourceName,
DestinationName: t.DestinationName, DestinationPartition: t.DestinationPartition,
DestinationNS: t.DestinationNS,
DestinationName: t.DestinationName,
} }
} }
@ -379,6 +386,16 @@ func (x *Intention) String() string {
idPart = "ID: " + x.ID + ", " 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 var detailPart string
if len(x.Permissions) > 0 { if len(x.Permissions) > 0 {
detailPart = fmt.Sprintf("Permissions: %d", len(x.Permissions)) detailPart = fmt.Sprintf("Permissions: %d", len(x.Permissions))
@ -386,9 +403,9 @@ func (x *Intention) String() string {
detailPart = "Action: " + strings.ToUpper(string(x.Action)) detailPart = "Action: " + strings.ToUpper(string(x.Action))
} }
return fmt.Sprintf("%s/%s => %s/%s (%sPrecedence: %d, %s)", return fmt.Sprintf("%s%s/%s => %s%s/%s (%sPrecedence: %d, %s)",
x.SourceNS, x.SourceName, srcPartitionPart, x.SourceNS, x.SourceName,
x.DestinationNS, x.DestinationName, dstPartitionPart, x.DestinationNS, x.DestinationName,
idPart, idPart,
x.Precedence, x.Precedence,
detailPart, detailPart,
@ -625,6 +642,7 @@ type IntentionQueryMatch struct {
// IntentionMatchEntry is a single entry for matching an intention. // IntentionMatchEntry is a single entry for matching an intention.
type IntentionMatchEntry struct { type IntentionMatchEntry struct {
Partition string `json:",omitempty"`
Namespace string Namespace string
Name string Name string
} }
@ -637,6 +655,10 @@ type IntentionQueryCheck struct {
SourceNS, SourceName string SourceNS, SourceName string
DestinationNS, DestinationName 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 is the type of the value for the source.
SourceType IntentionSourceType SourceType IntentionSourceType
} }
@ -673,9 +695,13 @@ type IntentionDecisionSummary struct {
type IntentionQueryExact struct { type IntentionQueryExact struct {
SourceNS, SourceName string SourceNS, SourceName string
DestinationNS, DestinationName 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 { func (q *IntentionQueryExact) Validate() error {
var err error var err error
if q.SourceNS == "" { if q.SourceNS == "" {
@ -721,18 +747,24 @@ func (s IntentionPrecedenceSorter) Less(i, j int) bool {
return a.Precedence > b.Precedence return a.Precedence > b.Precedence
} }
// Tie break on lexicographic order of the 4-tuple in canonical form (SrcNS, // Tie break on lexicographic order of the tuple in canonical form (SrcPxn,
// Src, DstNS, Dst). This is arbitrary but it keeps sorting deterministic // SrcNS, Src, DstPxn, DstNS, Dst). This is arbitrary but it keeps sorting
// which is a nice property for consistency. It is arguably open to abuse if // deterministic which is a nice property for consistency. It is arguably
// implementations rely on this however by definition the order among // open to abuse if implementations rely on this however by definition the
// same-precedence rules is arbitrary and doesn't affect whether an allow or // order among same-precedence rules is arbitrary and doesn't affect whether
// deny rule is acted on since all applicable rules are checked. // 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 { if a.SourceNS != b.SourceNS {
return a.SourceNS < b.SourceNS return a.SourceNS < b.SourceNS
} }
if a.SourceName != b.SourceName { if a.SourceName != b.SourceName {
return a.SourceName < b.SourceName return a.SourceName < b.SourceName
} }
if a.DestinationPartition != b.DestinationPartition {
return a.DestinationPartition < b.DestinationPartition
}
if a.DestinationNS != b.DestinationNS { if a.DestinationNS != b.DestinationNS {
return a.DestinationNS < b.DestinationNS return a.DestinationNS < b.DestinationNS
} }

View File

@ -47,21 +47,35 @@ func (_ *IntentionQueryCheck) FillAuthzContext(_ *acl.AuthorizerContext) {
// do nothing // do nothing
} }
// DefaultNamespaces will populate both the SourceNS and DestinationNS fields // FillPartitionAndNamespace will fill in empty source and destination partition/namespaces.
// if they are empty with the proper defaults. // If fillDefault is true, all fields are defaulted when the given enterprise meta does not
func (ixn *Intention) DefaultNamespaces(_ *EnterpriseMeta) { // specify them.
// Until we support namespaces, we force all namespaces to be default //
// 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 == "" { if ixn.SourceNS == "" {
ixn.SourceNS = IntentionDefaultNamespace ixn.SourceNS = ns
} }
if ixn.DestinationNS == "" { if ixn.DestinationNS == "" {
ixn.DestinationNS = IntentionDefaultNamespace ixn.DestinationNS = ns
} }
ixn.SourcePartition = ""
ixn.DestinationPartition = ""
} }
// FillNonDefaultNamespaces will populate the SourceNS and DestinationNS fields func (ixn *Intention) NormalizePartitionFields() {
// if they are empty with the proper defaults, but only if the proper defaults ixn.SourcePartition = ""
// are themselves not "default". ixn.DestinationPartition = ""
func (ixn *Intention) FillNonDefaultNamespaces(_ *EnterpriseMeta) {
// do nothing
} }

View File

@ -369,6 +369,11 @@ func TestIntention_String(t *testing.T) {
testID := generateUUID() testID := generateUUID()
partitionPrefix := DefaultEnterpriseMetaInDefaultPartition().PartitionOrEmpty()
if partitionPrefix != "" {
partitionPrefix += "/"
}
cases := map[string]testcase{ cases := map[string]testcase{
"legacy allow": { "legacy allow": {
&Intention{ &Intention{
@ -377,7 +382,7 @@ func TestIntention_String(t *testing.T) {
DestinationName: "bar", DestinationName: "bar",
Action: IntentionActionAllow, 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": { "legacy deny": {
&Intention{ &Intention{
@ -386,7 +391,7 @@ func TestIntention_String(t *testing.T) {
DestinationName: "bar", DestinationName: "bar",
Action: IntentionActionDeny, 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": { "L4 allow": {
&Intention{ &Intention{
@ -394,7 +399,7 @@ func TestIntention_String(t *testing.T) {
DestinationName: "bar", DestinationName: "bar",
Action: IntentionActionAllow, Action: IntentionActionAllow,
}, },
`default/foo => default/bar (Precedence: 9, Action: ALLOW)`, partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Action: ALLOW)`,
}, },
"L4 deny": { "L4 deny": {
&Intention{ &Intention{
@ -402,7 +407,7 @@ func TestIntention_String(t *testing.T) {
DestinationName: "bar", DestinationName: "bar",
Action: IntentionActionDeny, Action: IntentionActionDeny,
}, },
`default/foo => default/bar (Precedence: 9, Action: DENY)`, partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Action: DENY)`,
}, },
"L7 one perm": { "L7 one perm": {
&Intention{ &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": { "L7 two perms": {
&Intention{ &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 { for name, tc := range cases {
tc := tc tc := tc
// Add a bunch of required fields. // Add a bunch of required fields.
tc.ixn.DefaultNamespaces(DefaultEnterpriseMetaInDefaultPartition()) tc.ixn.FillPartitionAndNamespace(DefaultEnterpriseMetaInDefaultPartition(), true)
tc.ixn.UpdatePrecedence() tc.ixn.UpdatePrecedence()
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {

View File

@ -652,6 +652,11 @@ var expectedFieldConfigIntention bexpr.FieldConfigurations = bexpr.FieldConfigur
CoerceFn: bexpr.CoerceString, CoerceFn: bexpr.CoerceString,
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, 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{ "SourceNS": &bexpr.FieldConfiguration{
StructFieldName: "SourceNS", StructFieldName: "SourceNS",
CoerceFn: bexpr.CoerceString, CoerceFn: bexpr.CoerceString,
@ -662,6 +667,11 @@ var expectedFieldConfigIntention bexpr.FieldConfigurations = bexpr.FieldConfigur
CoerceFn: bexpr.CoerceString, CoerceFn: bexpr.CoerceString,
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, 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{ "DestinationNS": &bexpr.FieldConfiguration{
StructFieldName: "DestinationNS", StructFieldName: "DestinationNS",
CoerceFn: bexpr.CoerceString, CoerceFn: bexpr.CoerceString,

View File

@ -6,7 +6,7 @@ import (
// TestIntention returns a valid, uninserted (no ID set) intention. // TestIntention returns a valid, uninserted (no ID set) intention.
func TestIntention(t testing.T) *Intention { func TestIntention(t testing.T) *Intention {
return &Intention{ ixn := &Intention{
SourceNS: IntentionDefaultNamespace, SourceNS: IntentionDefaultNamespace,
SourceName: "api", SourceName: "api",
DestinationNS: IntentionDefaultNamespace, DestinationNS: IntentionDefaultNamespace,
@ -15,4 +15,6 @@ func TestIntention(t testing.T) *Intention {
SourceType: IntentionSourceConsul, SourceType: IntentionSourceConsul,
Meta: map[string]string{}, Meta: map[string]string{},
} }
ixn.NormalizePartitionFields()
return ixn
} }