open-consul/agent/structs/config_entry_intentions.go
Mark Anderson ab099e5fcb
Refactor config checks oss (#12550)
Currently the config_entry.go subsystem delegates authorization decisions via the ConfigEntry interface CanRead and CanWrite code. Unfortunately this returns a true/false value and loses the details of the source.

This is not helpful, especially since it the config subsystem can be more complex to understand, since it covers so many domains.

This refactors CanRead/CanWrite to return a structured error message (PermissionDenied or the like) with more details about the reason for denial.

Part of #12241

Signed-off-by: Mark Anderson <manderson@hashicorp.com>
2022-03-11 13:45:51 -08:00

844 lines
22 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) UpdateSourceByLegacyID(legacyID string, update *SourceIntention) bool {
for i, src := range e.Sources {
if src.LegacyID == legacyID {
e.Sources[i] = update
return true
}
}
return false
}
func (e *ServiceIntentionsConfigEntry) UpsertSourceByName(sn ServiceName, upsert *SourceIntention) {
for i, src := range e.Sources {
if src.SourceServiceName() == sn {
e.Sources[i] = upsert
return
}
}
e.Sources = append(e.Sources, upsert)
}
func (e *ServiceIntentionsConfigEntry) DeleteSourceByLegacyID(legacyID string) bool {
for i, src := range e.Sources {
if src.LegacyID == legacyID {
// Delete slice element: https://github.com/golang/go/wiki/SliceTricks#delete
// a = append(a[:i], a[i+1:]...)
e.Sources = append(e.Sources[:i], e.Sources[i+1:]...)
if len(e.Sources) == 0 {
e.Sources = nil
}
return true
}
}
return false
}
func (e *ServiceIntentionsConfigEntry) DeleteSourceByName(sn ServiceName) bool {
for i, src := range e.Sources {
if src.SourceServiceName() == sn {
// Delete slice element: https://github.com/golang/go/wiki/SliceTricks#delete
// a = append(a[:i], a[i+1:]...)
e.Sources = append(e.Sources[:i], e.Sources[i+1:]...)
if len(e.Sources) == 0 {
e.Sources = nil
}
return true
}
}
return false
}
func (e *ServiceIntentionsConfigEntry) ToIntention(src *SourceIntention) *Intention {
meta := e.Meta
if src.LegacyID != "" {
meta = src.LegacyMeta
}
ixn := &Intention{
ID: src.LegacyID,
Description: src.Description,
SourcePartition: src.PartitionOrEmpty(),
SourceNS: src.NamespaceOrDefault(),
SourceName: src.Name,
SourceType: src.Type,
Action: src.Action,
Permissions: src.Permissions,
Meta: meta,
Precedence: src.Precedence,
DestinationPartition: e.PartitionOrEmpty(),
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
//
// NOTE: this is mutually exclusive with the Permissions field.
Action IntentionAction `json:",omitempty"`
// Permissions is the list of additional L7 attributes that extend the
// intention definition.
//
// Permissions are interpreted in the order represented in the slice. In
// default-deny mode, deny permissions are logically subtracted from all
// following allow permissions. Multiple allow permissions are then ORed
// together.
//
// For example:
// ["deny /v2/admin", "allow /v2/*", "allow GET /healthz"]
//
// Is logically interpreted as:
// allow: [
// "(/v2/*) AND NOT (/v2/admin)",
// "(GET /healthz) AND NOT (/v2/admin)"
// ]
Permissions []*IntentionPermission `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.
//
// 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"`
}
type IntentionPermission struct {
Action IntentionAction // required: allow|deny
HTTP *IntentionHTTPPermission `json:",omitempty"`
// If we have non-http match criteria for other protocols
// in the future (gRPC, redis, etc) they can go here.
// Support for edge-decoded JWTs would likely be configured
// in a new top level section here.
// If we ever add Sentinel support, this is one place we may
// wish to add it.
}
func (p *IntentionPermission) Clone() *IntentionPermission {
p2 := *p
if p.HTTP != nil {
p2.HTTP = p.HTTP.Clone()
}
return &p2
}
type IntentionHTTPPermission struct {
// PathExact, PathPrefix, and PathRegex are mutually exclusive.
PathExact string `json:",omitempty" alias:"path_exact"`
PathPrefix string `json:",omitempty" alias:"path_prefix"`
PathRegex string `json:",omitempty" alias:"path_regex"`
Header []IntentionHTTPHeaderPermission `json:",omitempty"`
Methods []string `json:",omitempty"`
}
func (p *IntentionHTTPPermission) Clone() *IntentionHTTPPermission {
p2 := *p
if len(p.Header) > 0 {
p2.Header = make([]IntentionHTTPHeaderPermission, 0, len(p.Header))
for _, hdr := range p.Header {
p2.Header = append(p2.Header, hdr)
}
}
p2.Methods = CloneStringSlice(p.Methods)
return &p2
}
type IntentionHTTPHeaderPermission struct {
Name string
Present bool `json:",omitempty"`
Exact string `json:",omitempty"`
Prefix string `json:",omitempty"`
Suffix string `json:",omitempty"`
Regex string `json:",omitempty"`
Invert bool `json:",omitempty"`
}
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)
if len(x.Permissions) > 0 {
x2.Permissions = make([]*IntentionPermission, 0, len(x.Permissions))
for _, perm := range x.Permissions {
x2.Permissions = append(x2.Permissions, perm.Clone())
}
}
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")
}
// NOTE: this function must be deterministic so that the raft log doesn't
// diverge. This means no ID assignments or time.Now() usage!
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)
}
} 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
}
for _, perm := range src.Permissions {
if perm.HTTP == nil {
continue
}
for j := 0; j < len(perm.HTTP.Methods); j++ {
perm.HTTP.Methods[j] = strings.ToUpper(perm.HTTP.Methods[j])
}
}
}
// 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) HasWildcardDestination() bool {
dstNS := e.EnterpriseMeta.NamespaceOrDefault()
return dstNS == WildcardSpecifier || e.Name == WildcardSpecifier
}
func (e *ServiceIntentionsConfigEntry) HasAnyPermissions() bool {
for _, src := range e.Sources {
if len(src.Permissions) > 0 {
return true
}
}
return false
}
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
}
destIsWild := e.HasWildcardDestination()
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)
}
if err := validateSourceIntentionEnterpriseMeta(&src.EnterpriseMeta, &e.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,
)
}
}
if src.LegacyCreateTime == nil {
return fmt.Errorf("Sources[%d].LegacyCreateTime must be set", i)
}
if src.LegacyUpdateTime == nil {
return fmt.Errorf("Sources[%d].LegacyUpdateTime must be set", i)
}
} 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 src.LegacyCreateTime != nil {
return fmt.Errorf("Sources[%d].LegacyCreateTime must be omitted", i)
}
if src.LegacyUpdateTime != nil {
return fmt.Errorf("Sources[%d].LegacyUpdateTime must be omitted", i)
}
}
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)
}
}
if legacyWrite || len(src.Permissions) == 0 {
switch src.Action {
case IntentionActionAllow, IntentionActionDeny:
default:
return fmt.Errorf("Sources[%d].Action must be set to 'allow' or 'deny'", i)
}
}
if len(src.Permissions) > 0 && src.Action != "" {
return fmt.Errorf("Sources[%d].Action must be omitted if Permissions are specified", i)
}
if destIsWild && len(src.Permissions) > 0 {
return fmt.Errorf("Sources[%d].Permissions cannot be specified on intentions with wildcarded destinations", i)
}
switch src.Type {
case IntentionSourceConsul:
default:
return fmt.Errorf("Sources[%d].Type must be set to 'consul'", i)
}
for j, perm := range src.Permissions {
switch perm.Action {
case IntentionActionAllow, IntentionActionDeny:
default:
return fmt.Errorf("Sources[%d].Permissions[%d].Action must be set to 'allow' or 'deny'", i, j)
}
errorPrefix := "Sources[%d].Permissions[%d].HTTP"
if perm.HTTP == nil {
return fmt.Errorf(errorPrefix+" is required", i, j)
}
pathParts := 0
if perm.HTTP.PathExact != "" {
pathParts++
if !strings.HasPrefix(perm.HTTP.PathExact, "/") {
return fmt.Errorf(
errorPrefix+".PathExact doesn't start with '/': %q",
i, j, perm.HTTP.PathExact,
)
}
}
if perm.HTTP.PathPrefix != "" {
pathParts++
if !strings.HasPrefix(perm.HTTP.PathPrefix, "/") {
return fmt.Errorf(
errorPrefix+".PathPrefix doesn't start with '/': %q",
i, j, perm.HTTP.PathPrefix,
)
}
}
if perm.HTTP.PathRegex != "" {
pathParts++
}
if pathParts > 1 {
return fmt.Errorf(
errorPrefix+" should only contain at most one of PathExact, PathPrefix, or PathRegex",
i, j,
)
}
permParts := pathParts
for k, hdr := range perm.HTTP.Header {
if hdr.Name == "" {
return fmt.Errorf(errorPrefix+".Header[%d] missing required Name field", i, j, k)
}
hdrParts := 0
if hdr.Present {
hdrParts++
}
if hdr.Exact != "" {
hdrParts++
}
if hdr.Regex != "" {
hdrParts++
}
if hdr.Prefix != "" {
hdrParts++
}
if hdr.Suffix != "" {
hdrParts++
}
if hdrParts != 1 {
return fmt.Errorf(errorPrefix+".Header[%d] should only contain one of Present, Exact, Prefix, Suffix, or Regex", i, j, k)
}
permParts++
}
if len(perm.HTTP.Methods) > 0 {
found := make(map[string]struct{})
for _, m := range perm.HTTP.Methods {
if !isValidHTTPMethod(m) {
return fmt.Errorf(errorPrefix+".Methods contains an invalid method %q", i, j, m)
}
if _, ok := found[m]; ok {
return fmt.Errorf(errorPrefix+".Methods contains %q more than once", i, j, m)
}
found[m] = struct{}{}
}
permParts++
}
if permParts == 0 {
return fmt.Errorf(errorPrefix+" should not be empty", i, j)
}
}
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")
}
}
if strings.Contains(entMeta.PartitionOrDefault(), WildcardSpecifier) {
return fmt.Errorf("Partition: cannot use wildcard '*' in partition")
}
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) error {
var authzContext acl.AuthorizerContext
e.FillAuthzContext(&authzContext)
return authz.ToAllowAuthorizer().IntentionReadAllowed(e.GetName(), &authzContext)
}
func (e *ServiceIntentionsConfigEntry) CanWrite(authz acl.Authorizer) error {
var authzContext acl.AuthorizerContext
e.FillAuthzContext(&authzContext)
return authz.ToAllowAuthorizer().IntentionWriteAllowed(e.GetName(), &authzContext)
}
func MigrateIntentions(ixns Intentions) []*ServiceIntentionsConfigEntry {
if len(ixns) == 0 {
return nil
}
collated := make(map[ServiceName]*ServiceIntentionsConfigEntry)
for _, ixn := range ixns {
thisEntry := ixn.ToConfigEntry(true)
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)
}
sort.Slice(out, func(i, j int) bool {
a := out[i]
b := out[j]
if a.PartitionOrDefault() < b.PartitionOrDefault() {
return true
} else if a.PartitionOrDefault() > b.PartitionOrDefault() {
return false
}
if a.NamespaceOrDefault() < b.NamespaceOrDefault() {
return true
} else if a.NamespaceOrDefault() > b.NamespaceOrDefault() {
return false
}
return a.Name < b.Name
})
return out
}