mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
834 lines
26 KiB
834 lines
26 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: MPL-2.0 |
|
|
|
package structs |
|
|
|
import ( |
|
"encoding/binary" |
|
"encoding/json" |
|
"errors" |
|
"fmt" |
|
"sort" |
|
"strconv" |
|
"strings" |
|
"time" |
|
|
|
"github.com/hashicorp/go-multierror" |
|
"github.com/mitchellh/hashstructure" |
|
|
|
"github.com/hashicorp/consul/acl" |
|
"github.com/hashicorp/consul/agent/cache" |
|
"github.com/hashicorp/consul/lib" |
|
|
|
"golang.org/x/crypto/blake2b" |
|
) |
|
|
|
const ( |
|
// IntentionDefaultNamespace is the default namespace value. |
|
// NOTE(mitchellh): This is only meant to be a temporary constant. |
|
// When namespaces are introduced, we should delete this constant and |
|
// fix up all the places where this was used with the proper namespace |
|
// value. |
|
IntentionDefaultNamespace = "default" |
|
) |
|
|
|
// Intention defines an intention for the Connect Service Graph. This defines |
|
// the allowed or denied behavior of a connection between two services using |
|
// Connect. |
|
type Intention struct { |
|
// ID is the UUID-based ID for the intention, always generated by Consul. |
|
ID string `json:",omitempty"` |
|
|
|
// Description is a human-friendly description of this intention. |
|
// It is opaque to Consul and is only stored and transferred in API |
|
// requests. |
|
Description string `json:",omitempty"` |
|
|
|
// SourceNS, SourceName are the namespace and name, respectively, of |
|
// the source service. Either of these may be the wildcard "*", but only |
|
// the full value can be a wildcard. Partial wildcards are not allowed. |
|
// The source may also be a non-Consul service, as specified by SourceType. |
|
// |
|
// DestinationNS, DestinationName is the same, but for the destination |
|
// service. The same rules apply. The destination is always a Consul |
|
// service. |
|
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"` |
|
|
|
// 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"` |
|
|
|
// SourceSamenessGroup cannot be a wildcard "*" and is not compatible with legacy |
|
// intentions. Cannot be used with SourcePartition, as both represent the |
|
// same level of tenancy (sameness group includes both partitions and cluster peers). |
|
SourceSamenessGroup string `json:",omitempty"` |
|
|
|
// SourceType is the type of the value for the source. |
|
SourceType IntentionSourceType |
|
|
|
// Action is whether this is an allowlist or denylist intention. |
|
Action IntentionAction `json:",omitempty"` |
|
|
|
// Permissions is the list of additional L7 attributes that extend the |
|
// intention definition. |
|
// |
|
// NOTE: This field is not editable unless editing the underlying |
|
// service-intentions config entry directly. |
|
Permissions []*IntentionPermission `bexpr:"-" json:",omitempty"` |
|
|
|
// JWT specifies JWT authn that applies to incoming requests. |
|
JWT *IntentionJWTRequirement `bexpr:"-" json:",omitempty"` |
|
|
|
// DefaultAddr is not used. |
|
// Deprecated: DefaultAddr is not used and may be removed in a future version. |
|
DefaultAddr string `bexpr:"-" codec:",omitempty" json:",omitempty"` |
|
// DefaultPort is not used. |
|
// Deprecated: DefaultPort is not used and may be removed in a future version. |
|
DefaultPort int `bexpr:"-" codec:",omitempty" json:",omitempty"` |
|
|
|
// Meta is arbitrary metadata associated with the intention. This is |
|
// opaque to Consul but is served in API responses. |
|
Meta map[string]string `json:",omitempty"` |
|
|
|
// Precedence is the order that the intention will be applied, with |
|
// larger numbers being applied first. This is a read-only field, on |
|
// any intention update it is updated. |
|
Precedence int |
|
|
|
// CreatedAt and UpdatedAt keep track of when this record was created |
|
// or modified. |
|
CreatedAt, UpdatedAt time.Time `mapstructure:"-" bexpr:"-"` |
|
|
|
// Hash of the contents of the intention. This is only necessary for legacy |
|
// intention replication purposes. |
|
// |
|
// This is needed mainly for legacy replication purposes. When replicating |
|
// from one DC to another keeping the content Hash will allow us to detect |
|
// content changes more efficiently than checking every single field |
|
Hash []byte `bexpr:"-" json:",omitempty"` |
|
|
|
RaftIndex `bexpr:"-"` |
|
} |
|
|
|
func (t *Intention) Clone() *Intention { |
|
t2 := *t |
|
if len(t.Permissions) > 0 { |
|
t2.Permissions = make([]*IntentionPermission, 0, len(t.Permissions)) |
|
for _, perm := range t.Permissions { |
|
t2.Permissions = append(t2.Permissions, perm.Clone()) |
|
} |
|
} |
|
t2.Meta = cloneStringStringMap(t.Meta) |
|
t2.Hash = nil |
|
return &t2 |
|
} |
|
|
|
func (t *Intention) ToExact() *IntentionQueryExact { |
|
return &IntentionQueryExact{ |
|
SourcePartition: t.SourcePartition, |
|
SourceNS: t.SourceNS, |
|
SourceName: t.SourceName, |
|
DestinationPartition: t.DestinationPartition, |
|
DestinationNS: t.DestinationNS, |
|
DestinationName: t.DestinationName, |
|
} |
|
} |
|
|
|
func (t *Intention) MarshalJSON() ([]byte, error) { |
|
type Alias Intention |
|
exported := &struct { |
|
CreatedAt, UpdatedAt *time.Time `json:",omitempty"` |
|
*Alias |
|
}{ |
|
Alias: (*Alias)(t), |
|
} |
|
if !t.CreatedAt.IsZero() { |
|
exported.CreatedAt = &t.CreatedAt |
|
} |
|
if !t.UpdatedAt.IsZero() { |
|
exported.UpdatedAt = &t.UpdatedAt |
|
} |
|
return json.Marshal(exported) |
|
} |
|
|
|
func (t *Intention) UnmarshalJSON(data []byte) (err error) { |
|
type Alias Intention |
|
aux := &struct { |
|
Hash string |
|
CreatedAt, UpdatedAt string // effectively `json:"-"` on CreatedAt and UpdatedAt |
|
|
|
*Alias |
|
}{ |
|
Alias: (*Alias)(t), |
|
} |
|
if err = lib.UnmarshalJSON(data, &aux); err != nil { |
|
return err |
|
} |
|
|
|
if aux.Hash != "" { |
|
t.Hash = []byte(aux.Hash) |
|
} |
|
return nil |
|
} |
|
|
|
// SetHash calculates Intention.Hash from any mutable "content" fields. |
|
// |
|
// The Hash is primarily used for legacy intention replication to determine if |
|
// an intention has changed and should be updated locally. |
|
// |
|
// Deprecated: this is only used for legacy intention CRUD and replication |
|
func (x *Intention) SetHash() { |
|
hash, err := blake2b.New256(nil) |
|
if err != nil { |
|
panic(err) |
|
} |
|
|
|
// Write all the user set fields |
|
hash.Write([]byte(x.ID)) |
|
hash.Write([]byte(x.Description)) |
|
hash.Write([]byte(x.SourceNS)) |
|
hash.Write([]byte(x.SourceName)) |
|
hash.Write([]byte(x.DestinationNS)) |
|
hash.Write([]byte(x.DestinationName)) |
|
hash.Write([]byte(x.SourceType)) |
|
hash.Write([]byte(x.Action)) |
|
// hash.Write can not return an error, so the only way for binary.Write to |
|
// error is to pass it data with an invalid data type. Doing so would be a |
|
// programming error, so panic in that case. |
|
if err := binary.Write(hash, binary.LittleEndian, uint64(x.Precedence)); err != nil { |
|
panic(err) |
|
} |
|
|
|
// sort keys to ensure hash stability when meta is stored later |
|
var keys []string |
|
for k := range x.Meta { |
|
keys = append(keys, k) |
|
} |
|
sort.Strings(keys) |
|
|
|
for _, k := range keys { |
|
hash.Write([]byte(k)) |
|
hash.Write([]byte(x.Meta[k])) |
|
} |
|
|
|
x.Hash = hash.Sum(nil) |
|
} |
|
|
|
// Validate returns an error if the intention is invalid for inserting |
|
// or updating via the legacy APIs. |
|
// |
|
// Deprecated: this is only used for legacy intention CRUD |
|
func (x *Intention) Validate() error { |
|
var result error |
|
|
|
// Empty values |
|
if x.SourceNS == "" { |
|
result = multierror.Append(result, fmt.Errorf("SourceNS must be set")) |
|
} |
|
if x.SourceName == "" { |
|
result = multierror.Append(result, fmt.Errorf("SourceName must be set")) |
|
} |
|
if x.DestinationNS == "" { |
|
result = multierror.Append(result, fmt.Errorf("DestinationNS must be set")) |
|
} |
|
if x.DestinationName == "" { |
|
result = multierror.Append(result, fmt.Errorf("DestinationName must be set")) |
|
} |
|
|
|
// Wildcard usage verification |
|
if x.SourceNS != WildcardSpecifier { |
|
if strings.Contains(x.SourceNS, WildcardSpecifier) { |
|
result = multierror.Append(result, fmt.Errorf( |
|
"SourceNS: wildcard character '*' cannot be used with partial values")) |
|
} |
|
} |
|
if x.SourceName != WildcardSpecifier { |
|
if strings.Contains(x.SourceName, WildcardSpecifier) { |
|
result = multierror.Append(result, fmt.Errorf( |
|
"SourceName: wildcard character '*' cannot be used with partial values")) |
|
} |
|
|
|
if x.SourceNS == WildcardSpecifier { |
|
result = multierror.Append(result, fmt.Errorf( |
|
"SourceName: exact value cannot follow wildcard namespace")) |
|
} |
|
} |
|
if x.DestinationNS != WildcardSpecifier { |
|
if strings.Contains(x.DestinationNS, WildcardSpecifier) { |
|
result = multierror.Append(result, fmt.Errorf( |
|
"DestinationNS: wildcard character '*' cannot be used with partial values")) |
|
} |
|
} |
|
if x.DestinationName != WildcardSpecifier { |
|
if strings.Contains(x.DestinationName, WildcardSpecifier) { |
|
result = multierror.Append(result, fmt.Errorf( |
|
"DestinationName: wildcard character '*' cannot be used with partial values")) |
|
} |
|
|
|
if x.DestinationNS == WildcardSpecifier { |
|
result = multierror.Append(result, fmt.Errorf( |
|
"DestinationName: exact value cannot follow wildcard namespace")) |
|
} |
|
} |
|
|
|
// Length of opaque values |
|
if len(x.Description) > metaValueMaxLength { |
|
result = multierror.Append(result, fmt.Errorf( |
|
"Description exceeds maximum length %d", metaValueMaxLength)) |
|
} |
|
if len(x.Meta) > metaMaxKeyPairs { |
|
result = multierror.Append(result, fmt.Errorf( |
|
"Meta exceeds maximum element count %d", metaMaxKeyPairs)) |
|
} |
|
for k, v := range x.Meta { |
|
if len(k) > metaKeyMaxLength { |
|
result = multierror.Append(result, fmt.Errorf( |
|
"Meta key %q exceeds maximum length %d", k, metaKeyMaxLength)) |
|
} |
|
if len(v) > metaValueMaxLength { |
|
result = multierror.Append(result, fmt.Errorf( |
|
"Meta value for key %q exceeds maximum length %d", k, metaValueMaxLength)) |
|
} |
|
} |
|
|
|
switch x.Action { |
|
case IntentionActionAllow, IntentionActionDeny: |
|
default: |
|
result = multierror.Append(result, fmt.Errorf( |
|
"Action must be set to 'allow' or 'deny'")) |
|
} |
|
|
|
if len(x.Permissions) > 0 { |
|
result = multierror.Append(result, fmt.Errorf( |
|
"Permissions must not be set when using the legacy APIs")) |
|
} |
|
|
|
switch x.SourceType { |
|
case IntentionSourceConsul: |
|
default: |
|
result = multierror.Append(result, fmt.Errorf( |
|
"SourceType must be set to 'consul'")) |
|
} |
|
|
|
return result |
|
} |
|
|
|
func (ixn *Intention) CanRead(authz acl.Authorizer) bool { |
|
var authzContext acl.AuthorizerContext |
|
|
|
// Read access on either end of the intention allows you to read the |
|
// complete intention. This is so that both ends can be aware of why |
|
// something does or does not work. |
|
|
|
// 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 |
|
} |
|
} |
|
|
|
if ixn.DestinationName != "" { |
|
ixn.FillAuthzContext(&authzContext, true) |
|
if authz.IntentionRead(ixn.DestinationName, &authzContext) == acl.Allow { |
|
return true |
|
} |
|
} |
|
|
|
return false |
|
} |
|
|
|
func (ixn *Intention) CanWrite(authz acl.Authorizer) bool { |
|
if ixn.DestinationName == "" { |
|
// This is likely a strange form of legacy intention data validation |
|
// that happened within the authorization check, since intentions without |
|
// a destination cannot be written. |
|
// This may be able to be removed later. |
|
return false |
|
} |
|
|
|
var authzContext acl.AuthorizerContext |
|
ixn.FillAuthzContext(&authzContext, true) |
|
return authz.IntentionWrite(ixn.DestinationName, &authzContext) == acl.Allow |
|
} |
|
|
|
// UpdatePrecedence sets the Precedence value based on the fields of this |
|
// structure. |
|
// |
|
// Deprecated: this is only used for legacy intention CRUD. |
|
func (x *Intention) UpdatePrecedence() { |
|
// Max maintains the maximum value that the precedence can be depending |
|
// on the number of exact values in the destination. |
|
var max int |
|
switch x.countExact(x.DestinationNS, x.DestinationName) { |
|
case 2: |
|
max = 9 |
|
case 1: |
|
max = 6 |
|
case 0: |
|
max = 3 |
|
default: |
|
// This shouldn't be possible, just set it to zero |
|
x.Precedence = 0 |
|
return |
|
} |
|
|
|
// Given the maximum, the exact value is determined based on the |
|
// number of source exact values. |
|
countSrc := x.countExact(x.SourceNS, x.SourceName) |
|
x.Precedence = max - (2 - countSrc) |
|
} |
|
|
|
// countExact counts the number of exact values (not wildcards) in |
|
// the given namespace and name. |
|
func (x *Intention) countExact(ns, n string) int { |
|
// If NS is wildcard, it must be zero since wildcards only follow exact |
|
if ns == WildcardSpecifier { |
|
return 0 |
|
} |
|
|
|
// Same reasoning as above, a wildcard can only follow an exact value |
|
// and an exact value cannot follow a wildcard, so if name is a wildcard |
|
// we must have exactly one. |
|
if n == WildcardSpecifier { |
|
return 1 |
|
} |
|
|
|
return 2 |
|
} |
|
|
|
// String returns a human-friendly string for this intention. |
|
func (x *Intention) String() string { |
|
var idPart string |
|
if x.ID != "" { |
|
idPart = "ID: " + x.ID + ", " |
|
} |
|
|
|
// Cluster may be either partition (local) or peer (remote) |
|
var srcClusterPart string |
|
if x.SourcePartition != "" { |
|
srcClusterPart = x.SourcePartition + "/" |
|
} |
|
if x.SourcePeer != "" { |
|
srcClusterPart = "peer(" + x.SourcePeer + ")/" |
|
} |
|
if x.SourceSamenessGroup != "" { |
|
srcClusterPart = "sameness-group(" + x.SourceSamenessGroup + ")/" |
|
} |
|
|
|
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)) |
|
} else { |
|
detailPart = "Action: " + strings.ToUpper(string(x.Action)) |
|
} |
|
|
|
return fmt.Sprintf("%s%s/%s => %s%s/%s (%sPrecedence: %d, %s)", |
|
srcClusterPart, x.SourceNS, x.SourceName, |
|
dstPartitionPart, x.DestinationNS, x.DestinationName, |
|
idPart, |
|
x.Precedence, |
|
detailPart, |
|
) |
|
} |
|
|
|
// LegacyEstimateSize returns an estimate (in bytes) of the size of this structure when encoded. |
|
// |
|
// Deprecated: only exists for legacy intention replication during migration to 1.9.0+ cluster. |
|
func (x *Intention) LegacyEstimateSize() int { |
|
// 56 = 36 (uuid) + 16 (RaftIndex) + 4 (Precedence) |
|
size := 56 + len(x.Description) + len(x.SourceNS) + len(x.SourceName) + len(x.DestinationNS) + |
|
len(x.DestinationName) + len(x.SourceType) + len(x.Action) |
|
|
|
for k, v := range x.Meta { |
|
size += len(k) + len(v) |
|
} |
|
|
|
return size |
|
} |
|
|
|
func (x *Intention) SourceServiceName() ServiceName { |
|
return NewServiceName(x.SourceName, x.SourceEnterpriseMeta()) |
|
} |
|
|
|
func (x *Intention) DestinationServiceName() ServiceName { |
|
return NewServiceName(x.DestinationName, x.DestinationEnterpriseMeta()) |
|
} |
|
|
|
// NOTE this is just used to manipulate user-provided data before an insert |
|
// The RPC execution will do Normalize + Validate for us. |
|
func (x *Intention) ToConfigEntry(legacy bool) *ServiceIntentionsConfigEntry { |
|
return &ServiceIntentionsConfigEntry{ |
|
Kind: ServiceIntentions, |
|
Name: x.DestinationName, |
|
EnterpriseMeta: *x.DestinationEnterpriseMeta(), |
|
Sources: []*SourceIntention{x.ToSourceIntention(legacy)}, |
|
} |
|
} |
|
|
|
func (x *Intention) ToSourceIntention(legacy bool) *SourceIntention { |
|
ct := x.CreatedAt // copy |
|
ut := x.UpdatedAt |
|
|
|
src := &SourceIntention{ |
|
Name: x.SourceName, |
|
EnterpriseMeta: *x.SourceEnterpriseMeta(), |
|
Peer: x.SourcePeer, |
|
SamenessGroup: x.SourceSamenessGroup, |
|
Action: x.Action, |
|
Permissions: nil, // explicitly not symmetric with the old APIs |
|
Precedence: 0, // Ignore, let it be computed. |
|
LegacyID: x.ID, |
|
Type: x.SourceType, |
|
Description: x.Description, |
|
LegacyMeta: x.Meta, |
|
LegacyCreateTime: &ct, |
|
LegacyUpdateTime: &ut, |
|
} |
|
if !legacy { |
|
src.Permissions = x.Permissions |
|
} |
|
return src |
|
} |
|
|
|
// IntentionAction is the action that the intention represents. This |
|
// can be "allow" or "deny". |
|
type IntentionAction string |
|
|
|
const ( |
|
IntentionActionAllow IntentionAction = "allow" |
|
IntentionActionDeny IntentionAction = "deny" |
|
) |
|
|
|
// IntentionSourceType is the type of the source within an intention. |
|
type IntentionSourceType string |
|
|
|
const ( |
|
// IntentionSourceConsul is a service within the Consul catalog. |
|
IntentionSourceConsul IntentionSourceType = "consul" |
|
) |
|
|
|
type IntentionTargetType string |
|
|
|
const ( |
|
// IntentionTargetService is a service within the Consul catalog. |
|
IntentionTargetService IntentionTargetType = "service" |
|
// IntentionTargetDestination is a destination defined through a service-default config entry. |
|
IntentionTargetDestination IntentionTargetType = "destination" |
|
) |
|
|
|
// Intentions is a list of intentions. |
|
type Intentions []*Intention |
|
|
|
// IndexedIntentions represents a list of intentions for RPC responses. |
|
type IndexedIntentions struct { |
|
Intentions Intentions |
|
|
|
// DataOrigin is used to indicate if this query was satisfied against the |
|
// old legacy intentions ("legacy") memdb table or via config entries |
|
// ("config"). This is really only of value for the legacy intention |
|
// replication routine to correctly detect that it should exit. |
|
DataOrigin string `json:"-"` |
|
QueryMeta |
|
} |
|
|
|
const ( |
|
IntentionDataOriginLegacy = "legacy" |
|
IntentionDataOriginConfigEntries = "config" |
|
) |
|
|
|
// IndexedIntentionMatches represents the list of matches for a match query. |
|
type IndexedIntentionMatches struct { |
|
Matches []Intentions |
|
QueryMeta |
|
} |
|
|
|
// IntentionOp is the operation for a request related to intentions. |
|
type IntentionOp string |
|
|
|
const ( |
|
IntentionOpCreate IntentionOp = "create" |
|
IntentionOpUpdate IntentionOp = "update" |
|
IntentionOpDelete IntentionOp = "delete" |
|
IntentionOpDeleteAll IntentionOp = "delete-all" // NOTE: this is only accepted when it comes from the leader, RPCs will reject this |
|
IntentionOpUpsert IntentionOp = "upsert" // config-entry only |
|
) |
|
|
|
// IntentionRequest is used to create, update, and delete intentions. |
|
type IntentionRequest struct { |
|
// Datacenter is the target for this request. |
|
Datacenter string |
|
|
|
// Op is the type of operation being requested. |
|
Op IntentionOp |
|
|
|
// Intention is the intention. |
|
// |
|
// This is mutually exclusive with the Mutation field. |
|
Intention *Intention |
|
|
|
// Mutation is a change to make to an Intention. |
|
// |
|
// This is mutually exclusive with the Intention field. |
|
// |
|
// This field is only set by the leader before writing to the raft log and |
|
// is not settable via the API or an RPC. |
|
Mutation *IntentionMutation |
|
|
|
// WriteRequest is a common struct containing ACL tokens and other |
|
// write-related common elements for requests. |
|
WriteRequest |
|
} |
|
|
|
type IntentionMutation struct { |
|
ID string |
|
Destination ServiceName |
|
Source ServiceName |
|
// TODO(peering): check if this needs peer field |
|
Value *SourceIntention |
|
} |
|
|
|
// RequestDatacenter returns the datacenter for a given request. |
|
func (q *IntentionRequest) RequestDatacenter() string { |
|
return q.Datacenter |
|
} |
|
|
|
// IntentionMatchType is the target for a match request. For example, |
|
// matching by source will look for all intentions that match the given |
|
// source value. |
|
type IntentionMatchType string |
|
|
|
const ( |
|
IntentionMatchSource IntentionMatchType = "source" |
|
IntentionMatchDestination IntentionMatchType = "destination" |
|
) |
|
|
|
// IntentionQueryRequest is used to query intentions. |
|
type IntentionQueryRequest struct { |
|
// Datacenter is the target this request is intended for. |
|
Datacenter string |
|
|
|
// IntentionID is the ID of a specific intention. |
|
IntentionID string |
|
|
|
// Match is non-nil if we're performing a match query. A match will |
|
// find intentions that "match" the given parameters. A match includes |
|
// resolving wildcards. |
|
Match *IntentionQueryMatch |
|
|
|
// Check is non-nil if we're performing a test query. A test will |
|
// return allowed/deny based on an exact match. |
|
Check *IntentionQueryCheck |
|
|
|
// Exact is non-nil if we're performing a lookup of an intention by its |
|
// unique name instead of its ID. |
|
Exact *IntentionQueryExact |
|
|
|
// Options for queries |
|
QueryOptions |
|
} |
|
|
|
// RequestDatacenter returns the datacenter for a given request. |
|
func (q *IntentionQueryRequest) RequestDatacenter() string { |
|
return q.Datacenter |
|
} |
|
|
|
// CacheInfo implements cache.Request |
|
func (q *IntentionQueryRequest) CacheInfo() cache.RequestInfo { |
|
info := cache.RequestInfo{ |
|
Token: q.Token, |
|
Datacenter: q.Datacenter, |
|
MinIndex: q.MinQueryIndex, |
|
Timeout: q.MaxQueryTime, |
|
} |
|
|
|
v, err := hashstructure.Hash(struct { |
|
IntentionID string |
|
Match *IntentionQueryMatch |
|
Check *IntentionQueryCheck |
|
Exact *IntentionQueryExact |
|
Filter string |
|
}{ |
|
IntentionID: q.IntentionID, |
|
Check: q.Check, |
|
Match: q.Match, |
|
Exact: q.Exact, |
|
Filter: q.QueryOptions.Filter, |
|
}, nil) |
|
if err == nil { |
|
// If there is an error, we don't set the key. A blank key forces |
|
// no cache for this request so the request is forwarded directly |
|
// to the server. |
|
info.Key = strconv.FormatUint(v, 16) |
|
} |
|
|
|
return info |
|
} |
|
|
|
// IntentionQueryMatch are the parameters for performing a match request |
|
// against the state store. |
|
type IntentionQueryMatch struct { |
|
Type IntentionMatchType |
|
Entries []IntentionMatchEntry |
|
WithSamenessGroups bool |
|
} |
|
|
|
// IntentionMatchEntry is a single entry for matching an intention. |
|
type IntentionMatchEntry struct { |
|
Partition string `json:",omitempty"` |
|
Namespace string |
|
Name string |
|
} |
|
|
|
// IntentionQueryCheck are the parameters for performing a test request. |
|
type IntentionQueryCheck struct { |
|
// SourceNS, SourceName, DestinationNS, and DestinationName are the |
|
// source and namespace, respectively, for the test. These must be |
|
// exact values. |
|
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 |
|
} |
|
|
|
// GetACLPrefix returns the prefix to look up the ACL policy for this |
|
// request, and a boolean noting whether the prefix is valid to check |
|
// or not. You must check the ok value before using the prefix. |
|
func (q *IntentionQueryCheck) GetACLPrefix() (string, bool) { |
|
return q.DestinationName, q.DestinationName != "" |
|
} |
|
|
|
// IntentionQueryCheckResponse is the response for a test request. |
|
type IntentionQueryCheckResponse struct { |
|
Allowed bool |
|
} |
|
|
|
// IntentionDecisionSummary contains a summary of a set of intentions between two services |
|
// Currently contains: |
|
// - Whether all actions are allowed |
|
// - Whether the matching intention has L7 permissions attached |
|
// - Whether the intention is managed by an external source like k8s |
|
// - Whether there is an exact, or wildcard, intention referencing the two services |
|
// - Whether ACLs are in DefaultAllow mode |
|
type IntentionDecisionSummary struct { |
|
Allowed bool |
|
HasPermissions bool |
|
ExternalSource string |
|
HasExact bool |
|
DefaultAllow bool |
|
} |
|
|
|
// IntentionQueryExact holds the parameters for performing a lookup of an |
|
// intention by its unique name instead of its ID. |
|
type IntentionQueryExact struct { |
|
SourceNS, SourceName string |
|
DestinationNS, DestinationName string |
|
|
|
// TODO(partitions): check query works with partitions |
|
SourcePartition string `json:",omitempty"` |
|
DestinationPartition string `json:",omitempty"` |
|
|
|
SourcePeer string `json:",omitempty"` |
|
SourceSamenessGroup string `json:",omitempty"` |
|
} |
|
|
|
// Validate is used to ensure all 4 required parameters are specified. |
|
func (q *IntentionQueryExact) Validate() error { |
|
var err error |
|
if q.SourceNS == "" { |
|
err = multierror.Append(err, errors.New("SourceNS is missing")) |
|
} |
|
if q.SourceName == "" { |
|
err = multierror.Append(err, errors.New("SourceName is missing")) |
|
} |
|
if q.DestinationNS == "" { |
|
err = multierror.Append(err, errors.New("DestinationNS is missing")) |
|
} |
|
if q.DestinationName == "" { |
|
err = multierror.Append(err, errors.New("DestinationName is missing")) |
|
} |
|
return err |
|
} |
|
|
|
// TODO(peering): add support for listing peer |
|
type IntentionListRequest struct { |
|
Datacenter string |
|
Legacy bool `json:"-"` |
|
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"` |
|
QueryOptions |
|
} |
|
|
|
func (r *IntentionListRequest) RequestDatacenter() string { |
|
return r.Datacenter |
|
} |
|
|
|
// SimplifiedIntentions contains expanded sameness groups. |
|
type SimplifiedIntentions Intentions |
|
|
|
// IntentionPrecedenceSorter takes a list of intentions and sorts them |
|
// based on the match precedence rules for intentions. The intentions |
|
// closer to the head of the list have higher precedence. i.e. index 0 has |
|
// the highest precedence. |
|
type IntentionPrecedenceSorter Intentions |
|
|
|
func (s IntentionPrecedenceSorter) Len() int { return len(s) } |
|
func (s IntentionPrecedenceSorter) Swap(i, j int) { |
|
s[i], s[j] = s[j], s[i] |
|
} |
|
|
|
func (s IntentionPrecedenceSorter) Less(i, j int) bool { |
|
a, b := s[i], s[j] |
|
if a.Precedence != b.Precedence { |
|
return a.Precedence > b.Precedence |
|
} |
|
|
|
// Tie break on lexicographic order of the tuple in canonical form: |
|
// |
|
// (SrcSamenessGroup, 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.SourceSamenessGroup != b.SourceSamenessGroup { |
|
return a.SourceSamenessGroup < b.SourceSamenessGroup |
|
} |
|
if a.SourcePeer != b.SourcePeer { |
|
return a.SourcePeer < b.SourcePeer |
|
} |
|
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 |
|
} |
|
return a.DestinationName < b.DestinationName |
|
}
|
|
|