open-consul/agent/structs/config_entry_intentions.go

540 lines
14 KiB
Go
Raw Normal View History

connect: intentions are now managed as a new config entry kind "service-intentions" (#8834) - Upgrade the ConfigEntry.ListAll RPC to be kind-aware so that older copies of consul will not see new config entries it doesn't understand replicate down. - Add shim conversion code so that the old API/CLI method of interacting with intentions will continue to work so long as none of these are edited via config entry endpoints. Almost all of the read-only APIs will continue to function indefinitely. - Add new APIs that operate on individual intentions without IDs so that the UI doesn't need to implement CAS operations. - Add a new serf feature flag indicating support for intentions-as-config-entries. - The old line-item intentions way of interacting with the state store will transparently flip between the legacy memdb table and the config entry representations so that readers will never see a hiccup during migration where the results are incomplete. It uses a piece of system metadata to control the flip. - The primary datacenter will begin migrating intentions into config entries on startup once all servers in the datacenter are on a version of Consul with the intentions-as-config-entries feature flag. When it is complete the old state store representations will be cleared. We also record a piece of system metadata indicating this has occurred. We use this metadata to skip ALL of this code the next time the leader starts up. - The secondary datacenters continue to run the old intentions replicator until all servers in the secondary DC and primary DC support intentions-as-config-entries (via serf flag). Once this condition it met the old intentions replicator ceases. - The secondary datacenters replicate the new config entries as they are migrated in the primary. When they detect that the primary has zeroed it's old state store table it waits until all config entries up to that point are replicated and then zeroes its own copy of the old state store table. We also record a piece of system metadata indicating this has occurred. We use this metadata to skip ALL of this code the next time the leader starts up.
2020-10-06 18:24:05 +00:00
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
}