package structs import ( "encoding/binary" "fmt" "sort" "strconv" "strings" "time" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/lib" "github.com/hashicorp/go-multierror" "github.com/mitchellh/hashstructure" "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 // 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 // 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 // SourceType is the type of the value for the source. SourceType IntentionSourceType // Action is whether this is an allowlist or denylist intention. Action IntentionAction // DefaultAddr, DefaultPort of the local listening proxy (if any) to // make this connection. DefaultAddr string DefaultPort int // Meta is arbitrary metadata associated with the intention. This is // opaque to Consul but is served in API responses. Meta map[string]string // 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 needed mainly for 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:"-"` RaftIndex `bexpr:"-"` } func (t *Intention) UnmarshalJSON(data []byte) (err error) { type Alias Intention aux := &struct { Hash string CreatedAt, UpdatedAt string // effectively `json:"-"` on Intention type *Alias }{ Alias: (*Alias)(t), } if err = lib.UnmarshalJSON(data, &aux); err != nil { return err } if aux.Hash != "" { t.Hash = []byte(aux.Hash) } return nil } // nolint: staticcheck // should be fixed in https://github.com/hashicorp/consul/pull/7900 func (x *Intention) SetHash(force bool) []byte { if force || x.Hash == nil { hash, err := blake2b.New256(nil) if err != nil { panic(err) } // Any non-immutable "content" fields should be involved with the // overall hash. The IDs are immutable which is why they aren't here. // The raft indices are metadata similar to the hash which is why they // aren't incorporated. CreateTime is similarly immutable // // The Hash is really only used for replication to determine if a token // has changed and should be updated locally. // 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([]byte(x.DefaultAddr)) binary.Write(hash, binary.LittleEndian, x.DefaultPort) binary.Write(hash, binary.LittleEndian, x.Precedence) // hashing the metadata var keys []string for k := range x.Meta { keys = append(keys, k) } // keep them sorted to ensure hash stability sort.Strings(keys) for _, k := range keys { hash.Write([]byte(k)) hash.Write([]byte(x.Meta[k])) } // Finalize the hash hashVal := hash.Sum(nil) x.Hash = hashVal } return x.Hash } // Validate returns an error if the intention is invalid for inserting // or updating. 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'")) } 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 { if authz == nil { return true } var authzContext acl.AuthorizerContext if ixn.SourceName != "" { 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 authz == nil { return true } var authzContext acl.AuthorizerContext if ixn.DestinationName == "" { return false } ixn.FillAuthzContext(&authzContext, true) return authz.IntentionWrite(ixn.DestinationName, &authzContext) == acl.Allow } // UpdatePrecedence sets the Precedence value based on the fields of this // structure. 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 { return fmt.Sprintf("%s %s/%s => %s/%s (ID: %s, Precedence: %d)", strings.ToUpper(string(x.Action)), x.SourceNS, x.SourceName, x.DestinationNS, x.DestinationName, x.ID, x.Precedence) } // EstimateSize returns an estimate (in bytes) of the size of this structure when encoded. func (x *Intention) EstimateSize() int { // 60 = 36 (uuid) + 16 (RaftIndex) + 4 (Precedence) + 4 (DefaultPort) size := 60 + len(x.Description) + len(x.SourceNS) + len(x.SourceName) + len(x.DestinationNS) + len(x.DestinationName) + len(x.SourceType) + len(x.Action) + len(x.DefaultAddr) for k, v := range x.Meta { size += len(k) + len(v) } return size } // 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" ) // Intentions is a list of intentions. type Intentions []*Intention // IndexedIntentions represents a list of intentions for RPC responses. type IndexedIntentions struct { Intentions Intentions QueryMeta } // 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" ) // 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. Intention *Intention // WriteRequest is a common struct containing ACL tokens and other // write-related common elements for requests. WriteRequest } // 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 // 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 { // We only support caching Match queries, so if Match isn't set, // then return an empty info object which will cause a pass-through // (and likely fail). if q.Match == nil { return cache.RequestInfo{} } info := cache.RequestInfo{ Token: q.Token, Datacenter: q.Datacenter, MinIndex: q.MinQueryIndex, Timeout: q.MaxQueryTime, } // Calculate the cache key via just hashing the Match struct. This // has been configured so things like ordering of entries has no // effect (via struct tags). v, err := hashstructure.Hash(q.Match, 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 } // IntentionMatchEntry is a single entry for matching an intention. type IntentionMatchEntry struct { 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 // 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 } // 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 4-tuple in canonical form (SrcNS, // Src, 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.SourceNS != b.SourceNS { return a.SourceNS < b.SourceNS } if a.SourceName != b.SourceName { return a.SourceName < b.SourceName } if a.DestinationNS != b.DestinationNS { return a.DestinationNS < b.DestinationNS } return a.DestinationName < b.DestinationName }