Add SourcePeer fields to relevant Intentions types (#13390)

This commit is contained in:
Chris S. Kim 2022-06-08 13:24:10 -04:00 committed by GitHub
parent c1f20d17ee
commit 3e71754e7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 172 additions and 71 deletions

View File

@ -82,7 +82,9 @@ func (m *EnterpriseMeta) MergeNoWildcard(_ *EnterpriseMeta) {
// do nothing
}
func (_ *EnterpriseMeta) Normalize() {}
func (_ *EnterpriseMeta) Normalize() {}
func (_ *EnterpriseMeta) NormalizePartition() {}
func (_ *EnterpriseMeta) NormalizeNamespace() {}
func (m *EnterpriseMeta) Matches(_ *EnterpriseMeta) bool {
return true

View File

@ -123,6 +123,7 @@ func (e *ServiceIntentionsConfigEntry) ToIntention(src *SourceIntention) *Intent
ixn := &Intention{
ID: src.LegacyID,
Description: src.Description,
SourcePeer: src.Peer,
SourcePartition: src.PartitionOrEmpty(),
SourceNS: src.NamespaceOrDefault(),
SourceName: src.Name,
@ -259,6 +260,9 @@ type SourceIntention struct {
// formerly Intention.SourceNS
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
// Peer is the name of the remote peer of the source service, if applicable.
Peer string `json:",omitempty"`
}
type IntentionPermission struct {
@ -361,11 +365,11 @@ func (e *ServiceIntentionsConfigEntry) UpdateOver(rawPrev ConfigEntry) error {
}
var (
prevSourceByName = make(map[ServiceName]*SourceIntention)
prevSourceByName = make(map[PeeredServiceName]*SourceIntention)
prevSourceByLegacyID = make(map[string]*SourceIntention)
)
for _, src := range prev.Sources {
prevSourceByName[src.SourceServiceName()] = src
prevSourceByName[PeeredServiceName{Peer: src.Peer, ServiceName: src.SourceServiceName()}] = src
if src.LegacyID != "" {
prevSourceByLegacyID[src.LegacyID] = src
}
@ -377,7 +381,7 @@ func (e *ServiceIntentionsConfigEntry) UpdateOver(rawPrev ConfigEntry) error {
}
// Check that the LegacyID fields are handled correctly during updates.
if prevSrc, ok := prevSourceByName[src.SourceServiceName()]; ok {
if prevSrc, ok := prevSourceByName[PeeredServiceName{Peer: src.Peer, ServiceName: src.SourceServiceName()}]; ok {
if prevSrc.LegacyID == "" {
return fmt.Errorf("Sources[%d].LegacyID: cannot set this field", i)
} else if src.LegacyID != prevSrc.LegacyID {
@ -423,10 +427,17 @@ func (e *ServiceIntentionsConfigEntry) normalize(legacyWrite bool) error {
src.Type = IntentionSourceConsul
}
// If the source namespace is omitted it inherits that of the
// destination.
src.EnterpriseMeta.MergeNoWildcard(&e.EnterpriseMeta)
src.EnterpriseMeta.Normalize()
// Normalize the source's namespace and partition.
// If the source is not peered, it inherits the destination's
// EnterpriseMeta.
if src.Peer == "" {
src.EnterpriseMeta.MergeNoWildcard(&e.EnterpriseMeta)
src.EnterpriseMeta.Normalize()
} else {
// If the source is peered, normalize the namespace only,
// since peer is mutually exclusive with partition.
src.EnterpriseMeta.NormalizeNamespace()
}
// Compute the precedence only AFTER normalizing namespaces since the
// namespaces are factored into the calculation.
@ -542,7 +553,7 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
return fmt.Errorf("Name is required")
}
if err := validateIntentionWildcards(e.Name, &e.EnterpriseMeta); err != nil {
if err := validateIntentionWildcards(e.Name, &e.EnterpriseMeta, ""); err != nil {
return err
}
@ -568,7 +579,7 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
return fmt.Errorf("Sources[%d].Name is required", i)
}
if err := validateIntentionWildcards(src.Name, &src.EnterpriseMeta); err != nil {
if err := validateIntentionWildcards(src.Name, &src.EnterpriseMeta, src.Peer); err != nil {
return fmt.Errorf("Sources[%d].%v", i, err)
}
@ -576,6 +587,10 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
return fmt.Errorf("Sources[%d].%v", i, err)
}
if src.Peer != "" && src.PartitionOrEmpty() != "" {
return fmt.Errorf("Sources[%d].Peer: cannot set Peer and Partition at the same time.", i)
}
// Length of opaque values
if len(src.Description) > metaValueMaxLength {
return fmt.Errorf(
@ -583,6 +598,10 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
}
if legacyWrite {
if src.Peer != "" {
return fmt.Errorf("Sources[%d].Peer cannot be set by legacy intentions", i)
}
if len(src.LegacyMeta) > metaMaxKeyPairs {
return fmt.Errorf(
"Sources[%d].Meta exceeds maximum element count %d", i, metaMaxKeyPairs)
@ -753,7 +772,7 @@ func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
}
// Wildcard usage verification
func validateIntentionWildcards(name string, entMeta *acl.EnterpriseMeta) error {
func validateIntentionWildcards(name string, entMeta *acl.EnterpriseMeta, peerName string) error {
ns := entMeta.NamespaceOrDefault()
if ns != WildcardSpecifier {
if strings.Contains(ns, WildcardSpecifier) {
@ -772,6 +791,9 @@ func validateIntentionWildcards(name string, entMeta *acl.EnterpriseMeta) error
if strings.Contains(entMeta.PartitionOrDefault(), WildcardSpecifier) {
return fmt.Errorf("Partition: cannot use wildcard '*' in partition")
}
if strings.Contains(peerName, WildcardSpecifier) {
return fmt.Errorf("Peer: cannot use wildcard '*' in peer")
}
return nil
}

View File

@ -57,6 +57,11 @@ type Intention struct {
SourcePartition string `json:",omitempty"`
DestinationPartition string `json:",omitempty"`
// SourcePeer cannot be a wildcard "*" and is not compatible with legacy
// intentions. Cannot be used with SourcePartition, as both represent the
// same level of tenancy (partition is local to cluster, peer is remote).
SourcePeer string `json:",omitempty"`
// SourceType is the type of the value for the source.
SourceType IntentionSourceType
@ -311,7 +316,9 @@ func (ixn *Intention) CanRead(authz acl.Authorizer) bool {
// complete intention. This is so that both ends can be aware of why
// something does or does not work.
if ixn.SourceName != "" {
// If SourcePeer is set, tenancy is irrelevant in the context of the local cluster
// so we skip authorizing on the Source end.
if ixn.SourceName != "" && ixn.SourcePeer == "" {
ixn.FillAuthzContext(&authzContext, false)
if authz.IntentionRead(ixn.SourceName, &authzContext) == acl.Allow {
return true
@ -394,9 +401,13 @@ func (x *Intention) String() string {
idPart = "ID: " + x.ID + ", "
}
var srcPartitionPart string
// Cluster may be either partition (local) or peer (remote)
var srcClusterPart string
if x.SourcePartition != "" {
srcPartitionPart = x.SourcePartition + "/"
srcClusterPart = x.SourcePartition + "/"
}
if x.SourcePeer != "" {
srcClusterPart = "peer(" + x.SourcePeer + ")/"
}
var dstPartitionPart string
@ -412,7 +423,7 @@ func (x *Intention) String() string {
}
return fmt.Sprintf("%s%s/%s => %s%s/%s (%sPrecedence: %d, %s)",
srcPartitionPart, x.SourceNS, x.SourceName,
srcClusterPart, x.SourceNS, x.SourceName,
dstPartitionPart, x.DestinationNS, x.DestinationName,
idPart,
x.Precedence,
@ -461,6 +472,7 @@ func (x *Intention) ToSourceIntention(legacy bool) *SourceIntention {
src := &SourceIntention{
Name: x.SourceName,
EnterpriseMeta: *x.SourceEnterpriseMeta(),
Peer: x.SourcePeer,
Action: x.Action,
Permissions: nil, // explicitly not symmetric with the old APIs
Precedence: 0, // Ignore, let it be computed.
@ -570,7 +582,8 @@ type IntentionMutation struct {
ID string
Destination ServiceName
Source ServiceName
Value *SourceIntention
// TODO(peering): check if this needs peer field
Value *SourceIntention
}
// RequestDatacenter returns the datacenter for a given request.
@ -716,6 +729,8 @@ type IntentionQueryExact struct {
// TODO(partitions): check query works with partitions
SourcePartition string `json:",omitempty"`
DestinationPartition string `json:",omitempty"`
SourcePeer string `json:",omitempty"`
}
// Validate is used to ensure all 4 required parameters are specified.
@ -736,6 +751,7 @@ func (q *IntentionQueryExact) Validate() error {
return err
}
// TODO(peering): add support for listing peer
type IntentionListRequest struct {
Datacenter string
Legacy bool `json:"-"`
@ -764,12 +780,18 @@ func (s IntentionPrecedenceSorter) Less(i, j int) bool {
return a.Precedence > b.Precedence
}
// 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.
// Tie break on lexicographic order of the tuple in canonical form:
//
// (SrcPeer, 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.SourcePeer != b.SourcePeer {
return a.SourcePeer < b.SourcePeer
}
if a.SourcePartition != b.SourcePartition {
return a.SourcePartition < b.SourcePartition
}

View File

@ -242,58 +242,85 @@ func TestIntentionValidate(t *testing.T) {
}
func TestIntentionPrecedenceSorter(t *testing.T) {
type fields struct {
SrcPeer string
SrcNS string
SrcN string
DstNS string
DstN string
}
cases := []struct {
Name string
Input [][]string // SrcNS, SrcN, DstNS, DstN
Expected [][]string // Same structure as Input
Input []fields
Expected []fields
}{
{
"exhaustive list",
[][]string{
{"*", "*", "exact", "*"},
{"*", "*", "*", "*"},
{"exact", "*", "exact", "exact"},
{"*", "*", "exact", "exact"},
{"exact", "exact", "*", "*"},
{"exact", "exact", "exact", "exact"},
{"exact", "exact", "exact", "*"},
{"exact", "*", "exact", "*"},
{"exact", "*", "*", "*"},
[]fields{
// Peer fields
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
},
[][]string{
{"exact", "exact", "exact", "exact"},
{"exact", "*", "exact", "exact"},
{"*", "*", "exact", "exact"},
{"exact", "exact", "exact", "*"},
{"exact", "*", "exact", "*"},
{"*", "*", "exact", "*"},
{"exact", "exact", "*", "*"},
{"exact", "*", "*", "*"},
{"*", "*", "*", "*"},
[]fields{
{SrcPeer: "", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "exact"},
{SrcPeer: "", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "exact"},
{SrcPeer: "", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "exact", DstN: "*"},
{SrcPeer: "", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "exact", DstN: "*"},
{SrcPeer: "", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "exact", DstNS: "*", DstN: "*"},
{SrcPeer: "", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "exact", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcPeer: "", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
{SrcPeer: "peer", SrcNS: "*", SrcN: "*", DstNS: "*", DstN: "*"},
},
},
{
"tiebreak deterministically",
[][]string{
{"a", "*", "a", "b"},
{"a", "*", "a", "a"},
{"b", "a", "a", "a"},
{"a", "b", "a", "a"},
{"a", "a", "b", "a"},
{"a", "a", "a", "b"},
{"a", "a", "a", "a"},
[]fields{
{SrcNS: "a", SrcN: "*", DstNS: "a", DstN: "b"},
{SrcNS: "a", SrcN: "*", DstNS: "a", DstN: "a"},
{SrcNS: "b", SrcN: "a", DstNS: "a", DstN: "a"},
{SrcNS: "a", SrcN: "b", DstNS: "a", DstN: "a"},
{SrcNS: "a", SrcN: "a", DstNS: "b", DstN: "a"},
{SrcNS: "a", SrcN: "a", DstNS: "a", DstN: "b"},
{SrcNS: "a", SrcN: "a", DstNS: "a", DstN: "a"},
},
[][]string{
[]fields{
// Exact matches first in lexicographical order (arbitrary but
// deterministic)
{"a", "a", "a", "a"},
{"a", "a", "a", "b"},
{"a", "a", "b", "a"},
{"a", "b", "a", "a"},
{"b", "a", "a", "a"},
{SrcNS: "a", SrcN: "a", DstNS: "a", DstN: "a"},
{SrcNS: "a", SrcN: "a", DstNS: "a", DstN: "b"},
{SrcNS: "a", SrcN: "a", DstNS: "b", DstN: "a"},
{SrcNS: "a", SrcN: "b", DstNS: "a", DstN: "a"},
{SrcNS: "b", SrcN: "a", DstNS: "a", DstN: "a"},
// Wildcards next, lexicographical
{"a", "*", "a", "a"},
{"a", "*", "a", "b"},
{SrcNS: "a", SrcN: "*", DstNS: "a", DstN: "a"},
{SrcNS: "a", SrcN: "*", DstNS: "a", DstN: "b"},
},
},
}
@ -304,10 +331,11 @@ func TestIntentionPrecedenceSorter(t *testing.T) {
var input Intentions
for _, v := range tc.Input {
input = append(input, &Intention{
SourceNS: v[0],
SourceName: v[1],
DestinationNS: v[2],
DestinationName: v[3],
SourcePeer: v.SrcPeer,
SourceNS: v.SrcNS,
SourceName: v.SrcN,
DestinationNS: v.DstNS,
DestinationName: v.DstN,
})
}
@ -320,13 +348,14 @@ func TestIntentionPrecedenceSorter(t *testing.T) {
sort.Sort(IntentionPrecedenceSorter(input))
// Get back into a comparable form
var actual [][]string
var actual []fields
for _, v := range input {
actual = append(actual, []string{
v.SourceNS,
v.SourceName,
v.DestinationNS,
v.DestinationName,
actual = append(actual, fields{
SrcPeer: v.SourcePeer,
SrcNS: v.SourceNS,
SrcN: v.SourceName,
DstNS: v.DestinationNS,
DstN: v.DestinationName,
})
}
assert.Equal(t, tc.Expected, actual)
@ -443,6 +472,15 @@ func TestIntention_String(t *testing.T) {
},
partitionPrefix + `default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Permissions: 2)`,
},
"L4 allow with source peer": {
&Intention{
SourceName: "foo",
SourcePeer: "billing",
DestinationName: "bar",
Action: IntentionActionAllow,
},
`peer(billing)/default/foo => ` + partitionPrefix + `default/bar (Precedence: 9, Action: ALLOW)`,
},
}
for name, tc := range cases {

View File

@ -1181,7 +1181,7 @@ const (
// ServiceKindDestination is a Destination for the Connect feature.
// This service allows external traffic to exit the mesh through a terminating gateway
//based on centralized configuration.
// based on centralized configuration.
ServiceKindDestination ServiceKind = "destination"
)
@ -2154,6 +2154,12 @@ type IndexedServices struct {
QueryMeta
}
// PeeredServiceName is a basic tuple of ServiceName and peer
type PeeredServiceName struct {
ServiceName ServiceName
Peer string
}
type ServiceName struct {
Name string
acl.EnterpriseMeta

View File

@ -682,6 +682,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},
},
"SourcePeer": &bexpr.FieldConfiguration{
StructFieldName: "SourcePeer",
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,

View File

@ -18,6 +18,7 @@ type ServiceIntentionsConfigEntry struct {
type SourceIntention struct {
Name string
Peer string `json:",omitempty"`
Partition string `json:",omitempty"`
Namespace string `json:",omitempty"`
Action IntentionAction `json:",omitempty"`

View File

@ -35,6 +35,11 @@ type Intention struct {
SourcePartition string `json:",omitempty"`
DestinationPartition string `json:",omitempty"`
// SourcePeer cannot be a wildcard "*" and is not compatible with legacy
// intentions. Cannot be used with SourcePartition, as both represent the
// same level of tenancy (partition is local to cluster, peer is remote).
SourcePeer string `json:",omitempty"`
// SourceType is the type of the value for the source.
SourceType IntentionSourceType