github.com/outbrain/consul@v1.4.5/agent/structs/intention.go (about)

     1  package structs
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/hashicorp/consul/agent/cache"
    10  	"github.com/hashicorp/go-multierror"
    11  	"github.com/mitchellh/hashstructure"
    12  )
    13  
    14  const (
    15  	// IntentionWildcard is the wildcard value.
    16  	IntentionWildcard = "*"
    17  
    18  	// IntentionDefaultNamespace is the default namespace value.
    19  	// NOTE(mitchellh): This is only meant to be a temporary constant.
    20  	// When namespaces are introduced, we should delete this constant and
    21  	// fix up all the places where this was used with the proper namespace
    22  	// value.
    23  	IntentionDefaultNamespace = "default"
    24  )
    25  
    26  // Intention defines an intention for the Connect Service Graph. This defines
    27  // the allowed or denied behavior of a connection between two services using
    28  // Connect.
    29  type Intention struct {
    30  	// ID is the UUID-based ID for the intention, always generated by Consul.
    31  	ID string
    32  
    33  	// Description is a human-friendly description of this intention.
    34  	// It is opaque to Consul and is only stored and transferred in API
    35  	// requests.
    36  	Description string
    37  
    38  	// SourceNS, SourceName are the namespace and name, respectively, of
    39  	// the source service. Either of these may be the wildcard "*", but only
    40  	// the full value can be a wildcard. Partial wildcards are not allowed.
    41  	// The source may also be a non-Consul service, as specified by SourceType.
    42  	//
    43  	// DestinationNS, DestinationName is the same, but for the destination
    44  	// service. The same rules apply. The destination is always a Consul
    45  	// service.
    46  	SourceNS, SourceName           string
    47  	DestinationNS, DestinationName string
    48  
    49  	// SourceType is the type of the value for the source.
    50  	SourceType IntentionSourceType
    51  
    52  	// Action is whether this is a whitelist or blacklist intention.
    53  	Action IntentionAction
    54  
    55  	// DefaultAddr, DefaultPort of the local listening proxy (if any) to
    56  	// make this connection.
    57  	DefaultAddr string
    58  	DefaultPort int
    59  
    60  	// Meta is arbitrary metadata associated with the intention. This is
    61  	// opaque to Consul but is served in API responses.
    62  	Meta map[string]string
    63  
    64  	// Precedence is the order that the intention will be applied, with
    65  	// larger numbers being applied first. This is a read-only field, on
    66  	// any intention update it is updated.
    67  	Precedence int
    68  
    69  	// CreatedAt and UpdatedAt keep track of when this record was created
    70  	// or modified.
    71  	CreatedAt, UpdatedAt time.Time `mapstructure:"-"`
    72  
    73  	RaftIndex
    74  }
    75  
    76  // Validate returns an error if the intention is invalid for inserting
    77  // or updating.
    78  func (x *Intention) Validate() error {
    79  	var result error
    80  
    81  	// Empty values
    82  	if x.SourceNS == "" {
    83  		result = multierror.Append(result, fmt.Errorf("SourceNS must be set"))
    84  	}
    85  	if x.SourceName == "" {
    86  		result = multierror.Append(result, fmt.Errorf("SourceName must be set"))
    87  	}
    88  	if x.DestinationNS == "" {
    89  		result = multierror.Append(result, fmt.Errorf("DestinationNS must be set"))
    90  	}
    91  	if x.DestinationName == "" {
    92  		result = multierror.Append(result, fmt.Errorf("DestinationName must be set"))
    93  	}
    94  
    95  	// Wildcard usage verification
    96  	if x.SourceNS != IntentionWildcard {
    97  		if strings.Contains(x.SourceNS, IntentionWildcard) {
    98  			result = multierror.Append(result, fmt.Errorf(
    99  				"SourceNS: wildcard character '*' cannot be used with partial values"))
   100  		}
   101  	}
   102  	if x.SourceName != IntentionWildcard {
   103  		if strings.Contains(x.SourceName, IntentionWildcard) {
   104  			result = multierror.Append(result, fmt.Errorf(
   105  				"SourceName: wildcard character '*' cannot be used with partial values"))
   106  		}
   107  
   108  		if x.SourceNS == IntentionWildcard {
   109  			result = multierror.Append(result, fmt.Errorf(
   110  				"SourceName: exact value cannot follow wildcard namespace"))
   111  		}
   112  	}
   113  	if x.DestinationNS != IntentionWildcard {
   114  		if strings.Contains(x.DestinationNS, IntentionWildcard) {
   115  			result = multierror.Append(result, fmt.Errorf(
   116  				"DestinationNS: wildcard character '*' cannot be used with partial values"))
   117  		}
   118  	}
   119  	if x.DestinationName != IntentionWildcard {
   120  		if strings.Contains(x.DestinationName, IntentionWildcard) {
   121  			result = multierror.Append(result, fmt.Errorf(
   122  				"DestinationName: wildcard character '*' cannot be used with partial values"))
   123  		}
   124  
   125  		if x.DestinationNS == IntentionWildcard {
   126  			result = multierror.Append(result, fmt.Errorf(
   127  				"DestinationName: exact value cannot follow wildcard namespace"))
   128  		}
   129  	}
   130  
   131  	// Length of opaque values
   132  	if len(x.Description) > metaValueMaxLength {
   133  		result = multierror.Append(result, fmt.Errorf(
   134  			"Description exceeds maximum length %d", metaValueMaxLength))
   135  	}
   136  	if len(x.Meta) > metaMaxKeyPairs {
   137  		result = multierror.Append(result, fmt.Errorf(
   138  			"Meta exceeds maximum element count %d", metaMaxKeyPairs))
   139  	}
   140  	for k, v := range x.Meta {
   141  		if len(k) > metaKeyMaxLength {
   142  			result = multierror.Append(result, fmt.Errorf(
   143  				"Meta key %q exceeds maximum length %d", k, metaKeyMaxLength))
   144  		}
   145  		if len(v) > metaValueMaxLength {
   146  			result = multierror.Append(result, fmt.Errorf(
   147  				"Meta value for key %q exceeds maximum length %d", k, metaValueMaxLength))
   148  		}
   149  	}
   150  
   151  	switch x.Action {
   152  	case IntentionActionAllow, IntentionActionDeny:
   153  	default:
   154  		result = multierror.Append(result, fmt.Errorf(
   155  			"Action must be set to 'allow' or 'deny'"))
   156  	}
   157  
   158  	switch x.SourceType {
   159  	case IntentionSourceConsul:
   160  	default:
   161  		result = multierror.Append(result, fmt.Errorf(
   162  			"SourceType must be set to 'consul'"))
   163  	}
   164  
   165  	return result
   166  }
   167  
   168  // UpdatePrecedence sets the Precedence value based on the fields of this
   169  // structure.
   170  func (x *Intention) UpdatePrecedence() {
   171  	// Max maintains the maximum value that the precedence can be depending
   172  	// on the number of exact values in the destination.
   173  	var max int
   174  	switch x.countExact(x.DestinationNS, x.DestinationName) {
   175  	case 2:
   176  		max = 9
   177  	case 1:
   178  		max = 6
   179  	case 0:
   180  		max = 3
   181  	default:
   182  		// This shouldn't be possible, just set it to zero
   183  		x.Precedence = 0
   184  		return
   185  	}
   186  
   187  	// Given the maximum, the exact value is determined based on the
   188  	// number of source exact values.
   189  	countSrc := x.countExact(x.SourceNS, x.SourceName)
   190  	x.Precedence = max - (2 - countSrc)
   191  }
   192  
   193  // countExact counts the number of exact values (not wildcards) in
   194  // the given namespace and name.
   195  func (x *Intention) countExact(ns, n string) int {
   196  	// If NS is wildcard, it must be zero since wildcards only follow exact
   197  	if ns == IntentionWildcard {
   198  		return 0
   199  	}
   200  
   201  	// Same reasoning as above, a wildcard can only follow an exact value
   202  	// and an exact value cannot follow a wildcard, so if name is a wildcard
   203  	// we must have exactly one.
   204  	if n == IntentionWildcard {
   205  		return 1
   206  	}
   207  
   208  	return 2
   209  }
   210  
   211  // GetACLPrefix returns the prefix to look up the ACL policy for this
   212  // intention, and a boolean noting whether the prefix is valid to check
   213  // or not. You must check the ok value before using the prefix.
   214  func (x *Intention) GetACLPrefix() (string, bool) {
   215  	return x.DestinationName, x.DestinationName != ""
   216  }
   217  
   218  // String returns a human-friendly string for this intention.
   219  func (x *Intention) String() string {
   220  	return fmt.Sprintf("%s %s/%s => %s/%s (ID: %s, Precedence: %d)",
   221  		strings.ToUpper(string(x.Action)),
   222  		x.SourceNS, x.SourceName,
   223  		x.DestinationNS, x.DestinationName,
   224  		x.ID, x.Precedence)
   225  }
   226  
   227  // EstimateSize returns an estimate (in bytes) of the size of this structure when encoded.
   228  func (x *Intention) EstimateSize() int {
   229  	// 60 = 36 (uuid) + 16 (RaftIndex) + 4 (Precedence) + 4 (DefaultPort)
   230  	size := 60 + len(x.Description) + len(x.SourceNS) + len(x.SourceName) + len(x.DestinationNS) +
   231  		len(x.DestinationName) + len(x.SourceType) + len(x.Action) + len(x.DefaultAddr)
   232  
   233  	for k, v := range x.Meta {
   234  		size += len(k) + len(v)
   235  	}
   236  
   237  	return size
   238  }
   239  
   240  // IntentionAction is the action that the intention represents. This
   241  // can be "allow" or "deny" to whitelist or blacklist intentions.
   242  type IntentionAction string
   243  
   244  const (
   245  	IntentionActionAllow IntentionAction = "allow"
   246  	IntentionActionDeny  IntentionAction = "deny"
   247  )
   248  
   249  // IntentionSourceType is the type of the source within an intention.
   250  type IntentionSourceType string
   251  
   252  const (
   253  	// IntentionSourceConsul is a service within the Consul catalog.
   254  	IntentionSourceConsul IntentionSourceType = "consul"
   255  )
   256  
   257  // Intentions is a list of intentions.
   258  type Intentions []*Intention
   259  
   260  // IndexedIntentions represents a list of intentions for RPC responses.
   261  type IndexedIntentions struct {
   262  	Intentions Intentions
   263  	QueryMeta
   264  }
   265  
   266  // IndexedIntentionMatches represents the list of matches for a match query.
   267  type IndexedIntentionMatches struct {
   268  	Matches []Intentions
   269  	QueryMeta
   270  }
   271  
   272  // IntentionOp is the operation for a request related to intentions.
   273  type IntentionOp string
   274  
   275  const (
   276  	IntentionOpCreate IntentionOp = "create"
   277  	IntentionOpUpdate IntentionOp = "update"
   278  	IntentionOpDelete IntentionOp = "delete"
   279  )
   280  
   281  // IntentionRequest is used to create, update, and delete intentions.
   282  type IntentionRequest struct {
   283  	// Datacenter is the target for this request.
   284  	Datacenter string
   285  
   286  	// Op is the type of operation being requested.
   287  	Op IntentionOp
   288  
   289  	// Intention is the intention.
   290  	Intention *Intention
   291  
   292  	// WriteRequest is a common struct containing ACL tokens and other
   293  	// write-related common elements for requests.
   294  	WriteRequest
   295  }
   296  
   297  // RequestDatacenter returns the datacenter for a given request.
   298  func (q *IntentionRequest) RequestDatacenter() string {
   299  	return q.Datacenter
   300  }
   301  
   302  // IntentionMatchType is the target for a match request. For example,
   303  // matching by source will look for all intentions that match the given
   304  // source value.
   305  type IntentionMatchType string
   306  
   307  const (
   308  	IntentionMatchSource      IntentionMatchType = "source"
   309  	IntentionMatchDestination IntentionMatchType = "destination"
   310  )
   311  
   312  // IntentionQueryRequest is used to query intentions.
   313  type IntentionQueryRequest struct {
   314  	// Datacenter is the target this request is intended for.
   315  	Datacenter string
   316  
   317  	// IntentionID is the ID of a specific intention.
   318  	IntentionID string
   319  
   320  	// Match is non-nil if we're performing a match query. A match will
   321  	// find intentions that "match" the given parameters. A match includes
   322  	// resolving wildcards.
   323  	Match *IntentionQueryMatch
   324  
   325  	// Check is non-nil if we're performing a test query. A test will
   326  	// return allowed/deny based on an exact match.
   327  	Check *IntentionQueryCheck
   328  
   329  	// Options for queries
   330  	QueryOptions
   331  }
   332  
   333  // RequestDatacenter returns the datacenter for a given request.
   334  func (q *IntentionQueryRequest) RequestDatacenter() string {
   335  	return q.Datacenter
   336  }
   337  
   338  // CacheInfo implements cache.Request
   339  func (q *IntentionQueryRequest) CacheInfo() cache.RequestInfo {
   340  	// We only support caching Match queries, so if Match isn't set,
   341  	// then return an empty info object which will cause a pass-through
   342  	// (and likely fail).
   343  	if q.Match == nil {
   344  		return cache.RequestInfo{}
   345  	}
   346  
   347  	info := cache.RequestInfo{
   348  		Token:      q.Token,
   349  		Datacenter: q.Datacenter,
   350  		MinIndex:   q.MinQueryIndex,
   351  		Timeout:    q.MaxQueryTime,
   352  	}
   353  
   354  	// Calculate the cache key via just hashing the Match struct. This
   355  	// has been configured so things like ordering of entries has no
   356  	// effect (via struct tags).
   357  	v, err := hashstructure.Hash(q.Match, nil)
   358  	if err == nil {
   359  		// If there is an error, we don't set the key. A blank key forces
   360  		// no cache for this request so the request is forwarded directly
   361  		// to the server.
   362  		info.Key = strconv.FormatUint(v, 16)
   363  	}
   364  
   365  	return info
   366  }
   367  
   368  // IntentionQueryMatch are the parameters for performing a match request
   369  // against the state store.
   370  type IntentionQueryMatch struct {
   371  	Type    IntentionMatchType
   372  	Entries []IntentionMatchEntry
   373  }
   374  
   375  // IntentionMatchEntry is a single entry for matching an intention.
   376  type IntentionMatchEntry struct {
   377  	Namespace string
   378  	Name      string
   379  }
   380  
   381  // IntentionQueryCheck are the parameters for performing a test request.
   382  type IntentionQueryCheck struct {
   383  	// SourceNS, SourceName, DestinationNS, and DestinationName are the
   384  	// source and namespace, respectively, for the test. These must be
   385  	// exact values.
   386  	SourceNS, SourceName           string
   387  	DestinationNS, DestinationName string
   388  
   389  	// SourceType is the type of the value for the source.
   390  	SourceType IntentionSourceType
   391  }
   392  
   393  // GetACLPrefix returns the prefix to look up the ACL policy for this
   394  // request, and a boolean noting whether the prefix is valid to check
   395  // or not. You must check the ok value before using the prefix.
   396  func (q *IntentionQueryCheck) GetACLPrefix() (string, bool) {
   397  	return q.DestinationName, q.DestinationName != ""
   398  }
   399  
   400  // IntentionQueryCheckResponse is the response for a test request.
   401  type IntentionQueryCheckResponse struct {
   402  	Allowed bool
   403  }
   404  
   405  // IntentionPrecedenceSorter takes a list of intentions and sorts them
   406  // based on the match precedence rules for intentions. The intentions
   407  // closer to the head of the list have higher precedence. i.e. index 0 has
   408  // the highest precedence.
   409  type IntentionPrecedenceSorter Intentions
   410  
   411  func (s IntentionPrecedenceSorter) Len() int { return len(s) }
   412  func (s IntentionPrecedenceSorter) Swap(i, j int) {
   413  	s[i], s[j] = s[j], s[i]
   414  }
   415  
   416  func (s IntentionPrecedenceSorter) Less(i, j int) bool {
   417  	a, b := s[i], s[j]
   418  	if a.Precedence != b.Precedence {
   419  		return a.Precedence > b.Precedence
   420  	}
   421  
   422  	// Tie break on lexicographic order of the 4-tuple in canonical form (SrcNS,
   423  	// Src, DstNS, Dst). This is arbitrary but it keeps sorting deterministic
   424  	// which is a nice property for consistency. It is arguably open to abuse if
   425  	// implementations rely on this however by definition the order among
   426  	// same-precedence rules is arbitrary and doesn't affect whether an allow or
   427  	// deny rule is acted on since all applicable rules are checked.
   428  	if a.SourceNS != b.SourceNS {
   429  		return a.SourceNS < b.SourceNS
   430  	}
   431  	if a.SourceName != b.SourceName {
   432  		return a.SourceName < b.SourceName
   433  	}
   434  	if a.DestinationNS != b.DestinationNS {
   435  		return a.DestinationNS < b.DestinationNS
   436  	}
   437  	return a.DestinationName < b.DestinationName
   438  }