540 lines
14 KiB
Go
540 lines
14 KiB
Go
|
package structs
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/hashicorp/consul/acl"
|
||
|
)
|
||
|
|
||
|
type ServiceIntentionsConfigEntry struct {
|
||
|
Kind string
|
||
|
Name string // formerly DestinationName
|
||
|
|
||
|
Sources []*SourceIntention
|
||
|
|
||
|
Meta map[string]string `json:",omitempty"` // formerly Intention.Meta
|
||
|
|
||
|
EnterpriseMeta `hcl:",squash" mapstructure:",squash"` // formerly DestinationNS
|
||
|
RaftIndex
|
||
|
}
|
||
|
|
||
|
var _ UpdatableConfigEntry = (*ServiceIntentionsConfigEntry)(nil)
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) GetKind() string {
|
||
|
return ServiceIntentions
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) GetName() string {
|
||
|
if e == nil {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
return e.Name
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) GetMeta() map[string]string {
|
||
|
if e == nil {
|
||
|
return nil
|
||
|
}
|
||
|
return e.Meta
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) Clone() *ServiceIntentionsConfigEntry {
|
||
|
e2 := *e
|
||
|
|
||
|
e2.Meta = cloneStringStringMap(e.Meta)
|
||
|
|
||
|
e2.Sources = make([]*SourceIntention, len(e.Sources))
|
||
|
for i, src := range e.Sources {
|
||
|
e2.Sources[i] = src.Clone()
|
||
|
}
|
||
|
|
||
|
return &e2
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) DestinationServiceName() ServiceName {
|
||
|
return NewServiceName(e.Name, &e.EnterpriseMeta)
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) ToIntention(src *SourceIntention) *Intention {
|
||
|
meta := e.Meta
|
||
|
if src.LegacyID != "" {
|
||
|
meta = src.LegacyMeta
|
||
|
}
|
||
|
|
||
|
ixn := &Intention{
|
||
|
ID: src.LegacyID,
|
||
|
Description: src.Description,
|
||
|
SourceNS: src.NamespaceOrDefault(),
|
||
|
SourceName: src.Name,
|
||
|
SourceType: src.Type,
|
||
|
Action: src.Action,
|
||
|
Meta: meta,
|
||
|
Precedence: src.Precedence,
|
||
|
DestinationNS: e.NamespaceOrDefault(),
|
||
|
DestinationName: e.Name,
|
||
|
RaftIndex: e.RaftIndex,
|
||
|
}
|
||
|
if src.LegacyCreateTime != nil {
|
||
|
ixn.CreatedAt = *src.LegacyCreateTime
|
||
|
}
|
||
|
if src.LegacyUpdateTime != nil {
|
||
|
ixn.UpdatedAt = *src.LegacyUpdateTime
|
||
|
}
|
||
|
|
||
|
if src.LegacyID != "" {
|
||
|
// Ensure that pre-1.9.0 secondaries can still replicate legacy
|
||
|
// intentions via the APIs. These require the Hash field to be
|
||
|
// populated.
|
||
|
//
|
||
|
//nolint:staticcheck
|
||
|
ixn.SetHash()
|
||
|
}
|
||
|
return ixn
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) LegacyIDFieldsAreAllEmpty() bool {
|
||
|
for _, src := range e.Sources {
|
||
|
if src.LegacyID != "" {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) LegacyIDFieldsAreAllSet() bool {
|
||
|
for _, src := range e.Sources {
|
||
|
if src.LegacyID == "" {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) ToIntentions() Intentions {
|
||
|
out := make(Intentions, 0, len(e.Sources))
|
||
|
for _, src := range e.Sources {
|
||
|
out = append(out, e.ToIntention(src))
|
||
|
}
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
type SourceIntention struct {
|
||
|
// Name is the name of the source service. This can be a 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.
|
||
|
//
|
||
|
// formerly Intention.SourceName
|
||
|
Name string
|
||
|
|
||
|
// Action is whether this is an allowlist or denylist intention.
|
||
|
//
|
||
|
// formerly Intention.Action
|
||
|
Action IntentionAction
|
||
|
|
||
|
// 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.
|
||
|
//
|
||
|
// Note we will technically decode this over the wire during a write, but
|
||
|
// we always recompute it on save.
|
||
|
//
|
||
|
// formerly Intention.Precedence
|
||
|
Precedence int
|
||
|
|
||
|
// LegacyID is manipulated just by the bridging code
|
||
|
// used as part of backwards compatibility.
|
||
|
//
|
||
|
// formerly Intention.ID
|
||
|
LegacyID string `json:",omitempty" alias:"legacy_id"`
|
||
|
|
||
|
// Type is the type of the value for the source.
|
||
|
//
|
||
|
// formerly Intention.SourceType
|
||
|
Type IntentionSourceType
|
||
|
|
||
|
// Description is a human-friendly description of this intention.
|
||
|
// It is opaque to Consul and is only stored and transferred in API
|
||
|
// requests.
|
||
|
//
|
||
|
// formerly Intention.Description
|
||
|
Description string `json:",omitempty"`
|
||
|
|
||
|
// LegacyMeta is arbitrary metadata associated with the intention. This is
|
||
|
// opaque to Consul but is served in API responses.
|
||
|
//
|
||
|
// formerly Intention.Meta
|
||
|
LegacyMeta map[string]string `json:",omitempty" alias:"legacy_meta"`
|
||
|
|
||
|
// LegacyCreateTime is formerly Intention.CreatedAt
|
||
|
LegacyCreateTime *time.Time `json:",omitempty" alias:"legacy_create_time"`
|
||
|
// LegacyUpdateTime is formerly Intention.UpdatedAt
|
||
|
LegacyUpdateTime *time.Time `json:",omitempty" alias:"legacy_update_time"`
|
||
|
|
||
|
// Things like L7 rules or Sentinel rules could go here later.
|
||
|
|
||
|
// formerly Intention.SourceNS
|
||
|
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
||
|
}
|
||
|
|
||
|
func cloneStringStringMap(m map[string]string) map[string]string {
|
||
|
if m == nil {
|
||
|
return nil
|
||
|
}
|
||
|
m2 := make(map[string]string)
|
||
|
for k, v := range m {
|
||
|
m2[k] = v
|
||
|
}
|
||
|
return m2
|
||
|
}
|
||
|
|
||
|
func (x *SourceIntention) SourceServiceName() ServiceName {
|
||
|
return NewServiceName(x.Name, &x.EnterpriseMeta)
|
||
|
}
|
||
|
|
||
|
func (x *SourceIntention) Clone() *SourceIntention {
|
||
|
x2 := *x
|
||
|
|
||
|
x2.LegacyMeta = cloneStringStringMap(x.LegacyMeta)
|
||
|
|
||
|
return &x2
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) UpdateOver(rawPrev ConfigEntry) error {
|
||
|
if rawPrev == nil {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
prev, ok := rawPrev.(*ServiceIntentionsConfigEntry)
|
||
|
if !ok {
|
||
|
return fmt.Errorf("previous config entry is not of type %T: %T", e, rawPrev)
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
prevSourceByName = make(map[ServiceName]*SourceIntention)
|
||
|
prevSourceByLegacyID = make(map[string]*SourceIntention)
|
||
|
)
|
||
|
for _, src := range prev.Sources {
|
||
|
prevSourceByName[src.SourceServiceName()] = src
|
||
|
if src.LegacyID != "" {
|
||
|
prevSourceByLegacyID[src.LegacyID] = src
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for i, src := range e.Sources {
|
||
|
if src.LegacyID == "" {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Check that the LegacyID fields are handled correctly during updates.
|
||
|
if prevSrc, ok := prevSourceByName[src.SourceServiceName()]; ok {
|
||
|
if prevSrc.LegacyID == "" {
|
||
|
return fmt.Errorf("Sources[%d].LegacyID: cannot set this field", i)
|
||
|
} else if src.LegacyID != prevSrc.LegacyID {
|
||
|
return fmt.Errorf("Sources[%d].LegacyID: cannot set this field to a different value", i)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Now ensure legacy timestamps carry over properly. We always retain the LegacyCreateTime.
|
||
|
if prevSrc, ok := prevSourceByLegacyID[src.LegacyID]; ok {
|
||
|
if prevSrc.LegacyCreateTime != nil {
|
||
|
// NOTE: we don't want to share the memory here
|
||
|
src.LegacyCreateTime = timePointer(*prevSrc.LegacyCreateTime)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) Normalize() error {
|
||
|
return e.normalize(false)
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) LegacyNormalize() error {
|
||
|
return e.normalize(true)
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) normalize(legacyWrite bool) error {
|
||
|
if e == nil {
|
||
|
return fmt.Errorf("config entry is nil")
|
||
|
}
|
||
|
|
||
|
e.Kind = ServiceIntentions
|
||
|
|
||
|
e.EnterpriseMeta.Normalize()
|
||
|
|
||
|
for _, src := range e.Sources {
|
||
|
// Default source type
|
||
|
if src.Type == "" {
|
||
|
src.Type = IntentionSourceConsul
|
||
|
}
|
||
|
|
||
|
// If the source namespace is omitted it inherits that of the
|
||
|
// destination.
|
||
|
src.EnterpriseMeta.MergeNoWildcard(&e.EnterpriseMeta)
|
||
|
src.EnterpriseMeta.Normalize()
|
||
|
|
||
|
// Compute the precedence only AFTER normalizing namespaces since the
|
||
|
// namespaces are factored into the calculation.
|
||
|
src.Precedence = computeIntentionPrecedence(e, src)
|
||
|
|
||
|
if legacyWrite {
|
||
|
// We always force meta to be non-nil so that it's an empty map. This
|
||
|
// makes it easy for API responses to not nil-check this everywhere.
|
||
|
if src.LegacyMeta == nil {
|
||
|
src.LegacyMeta = make(map[string]string)
|
||
|
}
|
||
|
// Set the created/updated times. If this is an update instead of an insert
|
||
|
// the UpdateOver() will fix it up appropriately.
|
||
|
now := time.Now().UTC()
|
||
|
src.LegacyCreateTime = timePointer(now)
|
||
|
src.LegacyUpdateTime = timePointer(now)
|
||
|
} else {
|
||
|
// Legacy fields are cleared, except LegacyMeta which we leave
|
||
|
// populated so that we can later fail the write in Validate() and
|
||
|
// give the user a warning about possible data loss.
|
||
|
src.LegacyID = ""
|
||
|
src.LegacyCreateTime = nil
|
||
|
src.LegacyUpdateTime = nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// The source intentions closer to the head of the list have higher
|
||
|
// precedence. i.e. index 0 has the highest precedence.
|
||
|
sort.SliceStable(e.Sources, func(i, j int) bool {
|
||
|
return e.Sources[i].Precedence > e.Sources[j].Precedence
|
||
|
})
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func timePointer(t time.Time) *time.Time {
|
||
|
if t.IsZero() {
|
||
|
return nil
|
||
|
}
|
||
|
return &t
|
||
|
}
|
||
|
|
||
|
// NOTE: this assumes that the namespaces have been fully normalized.
|
||
|
func computeIntentionPrecedence(entry *ServiceIntentionsConfigEntry, src *SourceIntention) int {
|
||
|
// Max maintains the maximum value that the precedence can be depending
|
||
|
// on the number of exact values in the destination.
|
||
|
var max int
|
||
|
switch intentionCountExact(entry.Name, &entry.EnterpriseMeta) {
|
||
|
case 2:
|
||
|
max = 9
|
||
|
case 1:
|
||
|
max = 6
|
||
|
case 0:
|
||
|
max = 3
|
||
|
default:
|
||
|
// This shouldn't be possible, just set it to zero
|
||
|
return 0
|
||
|
}
|
||
|
// Given the maximum, the exact value is determined based on the
|
||
|
// number of source exact values.
|
||
|
countSrc := intentionCountExact(src.Name, &src.EnterpriseMeta)
|
||
|
return max - (2 - countSrc)
|
||
|
}
|
||
|
|
||
|
// intentionCountExact counts the number of exact values (not wildcards) in
|
||
|
// the given namespace and name.
|
||
|
func intentionCountExact(name string, entMeta *EnterpriseMeta) int {
|
||
|
ns := entMeta.NamespaceOrDefault()
|
||
|
|
||
|
// If NS is wildcard, pair must be */* since an exact service cannot follow a wildcard NS
|
||
|
// */* is allowed, but */foo is not
|
||
|
if ns == WildcardSpecifier {
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
// only the namespace must be exact, since the */* case already returned.
|
||
|
if name == WildcardSpecifier {
|
||
|
return 1
|
||
|
}
|
||
|
|
||
|
return 2
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) Validate() error {
|
||
|
return e.validate(false)
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) LegacyValidate() error {
|
||
|
return e.validate(true)
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) validate(legacyWrite bool) error {
|
||
|
if e.Name == "" {
|
||
|
return fmt.Errorf("Name is required")
|
||
|
}
|
||
|
|
||
|
if err := validateIntentionWildcards(e.Name, &e.EnterpriseMeta); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if legacyWrite {
|
||
|
if len(e.Meta) > 0 {
|
||
|
return fmt.Errorf("Meta must be omitted for legacy intention writes")
|
||
|
}
|
||
|
} else {
|
||
|
if err := validateConfigEntryMeta(e.Meta); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if len(e.Sources) == 0 {
|
||
|
return fmt.Errorf("At least one source is required")
|
||
|
}
|
||
|
|
||
|
seenSources := make(map[ServiceName]struct{})
|
||
|
for i, src := range e.Sources {
|
||
|
if src.Name == "" {
|
||
|
return fmt.Errorf("Sources[%d].Name is required", i)
|
||
|
}
|
||
|
|
||
|
if err := validateIntentionWildcards(src.Name, &src.EnterpriseMeta); err != nil {
|
||
|
return fmt.Errorf("Sources[%d].%v", i, err)
|
||
|
}
|
||
|
|
||
|
// Length of opaque values
|
||
|
if len(src.Description) > metaValueMaxLength {
|
||
|
return fmt.Errorf(
|
||
|
"Sources[%d].Description exceeds maximum length %d", i, metaValueMaxLength)
|
||
|
}
|
||
|
|
||
|
if legacyWrite {
|
||
|
if len(src.LegacyMeta) > metaMaxKeyPairs {
|
||
|
return fmt.Errorf(
|
||
|
"Sources[%d].Meta exceeds maximum element count %d", i, metaMaxKeyPairs)
|
||
|
}
|
||
|
for k, v := range src.LegacyMeta {
|
||
|
if len(k) > metaKeyMaxLength {
|
||
|
return fmt.Errorf(
|
||
|
"Sources[%d].Meta key %q exceeds maximum length %d",
|
||
|
i, k, metaKeyMaxLength,
|
||
|
)
|
||
|
}
|
||
|
if len(v) > metaValueMaxLength {
|
||
|
return fmt.Errorf(
|
||
|
"Sources[%d].Meta value for key %q exceeds maximum length %d",
|
||
|
i, k, metaValueMaxLength,
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
if len(src.LegacyMeta) > 0 {
|
||
|
return fmt.Errorf("Sources[%d].LegacyMeta must be omitted", i)
|
||
|
}
|
||
|
src.LegacyMeta = nil // ensure it's completely unset
|
||
|
}
|
||
|
|
||
|
if legacyWrite {
|
||
|
if src.LegacyID == "" {
|
||
|
return fmt.Errorf("Sources[%d].LegacyID must be set", i)
|
||
|
}
|
||
|
} else {
|
||
|
if src.LegacyID != "" {
|
||
|
return fmt.Errorf("Sources[%d].LegacyID must be omitted", i)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch src.Action {
|
||
|
case IntentionActionAllow, IntentionActionDeny:
|
||
|
default:
|
||
|
return fmt.Errorf("Sources[%d].Action must be set to 'allow' or 'deny'", i)
|
||
|
}
|
||
|
|
||
|
switch src.Type {
|
||
|
case IntentionSourceConsul:
|
||
|
default:
|
||
|
return fmt.Errorf("Sources[%d].Type must be set to 'consul'", i)
|
||
|
}
|
||
|
|
||
|
serviceName := src.SourceServiceName()
|
||
|
if _, exists := seenSources[serviceName]; exists {
|
||
|
return fmt.Errorf("Sources[%d] defines %q more than once", i, serviceName.String())
|
||
|
}
|
||
|
seenSources[serviceName] = struct{}{}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Wildcard usage verification
|
||
|
func validateIntentionWildcards(name string, entMeta *EnterpriseMeta) error {
|
||
|
ns := entMeta.NamespaceOrDefault()
|
||
|
if ns != WildcardSpecifier {
|
||
|
if strings.Contains(ns, WildcardSpecifier) {
|
||
|
return fmt.Errorf("Namespace: wildcard character '*' cannot be used with partial values")
|
||
|
}
|
||
|
}
|
||
|
if name != WildcardSpecifier {
|
||
|
if strings.Contains(name, WildcardSpecifier) {
|
||
|
return fmt.Errorf("Name: wildcard character '*' cannot be used with partial values")
|
||
|
}
|
||
|
|
||
|
if ns == WildcardSpecifier {
|
||
|
return fmt.Errorf("Name: exact value cannot follow wildcard namespace")
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) GetRaftIndex() *RaftIndex {
|
||
|
if e == nil {
|
||
|
return &RaftIndex{}
|
||
|
}
|
||
|
|
||
|
return &e.RaftIndex
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) GetEnterpriseMeta() *EnterpriseMeta {
|
||
|
if e == nil {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
return &e.EnterpriseMeta
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) CanRead(authz acl.Authorizer) bool {
|
||
|
var authzContext acl.AuthorizerContext
|
||
|
e.FillAuthzContext(&authzContext)
|
||
|
return authz.IntentionRead(e.GetName(), &authzContext) == acl.Allow
|
||
|
}
|
||
|
|
||
|
func (e *ServiceIntentionsConfigEntry) CanWrite(authz acl.Authorizer) bool {
|
||
|
var authzContext acl.AuthorizerContext
|
||
|
e.FillAuthzContext(&authzContext)
|
||
|
return authz.IntentionWrite(e.GetName(), &authzContext) == acl.Allow
|
||
|
}
|
||
|
|
||
|
func MigrateIntentions(ixns Intentions) []*ServiceIntentionsConfigEntry {
|
||
|
if len(ixns) == 0 {
|
||
|
return nil
|
||
|
}
|
||
|
collated := make(map[ServiceName]*ServiceIntentionsConfigEntry)
|
||
|
for _, ixn := range ixns {
|
||
|
thisEntry := ixn.ToConfigEntry()
|
||
|
sn := thisEntry.DestinationServiceName()
|
||
|
|
||
|
if entry, ok := collated[sn]; ok {
|
||
|
entry.Sources = append(entry.Sources, thisEntry.Sources...)
|
||
|
} else {
|
||
|
collated[sn] = thisEntry
|
||
|
}
|
||
|
}
|
||
|
|
||
|
out := make([]*ServiceIntentionsConfigEntry, 0, len(collated))
|
||
|
for _, entry := range collated {
|
||
|
out = append(out, entry)
|
||
|
}
|
||
|
return out
|
||
|
}
|