mirror of https://github.com/hashicorp/consul
835 lines
26 KiB
Go
835 lines
26 KiB
Go
// 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
|
|
}
|