mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
461 lines
14 KiB
461 lines
14 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: MPL-2.0 |
|
|
|
package api |
|
|
|
import ( |
|
"bytes" |
|
"fmt" |
|
"io" |
|
"time" |
|
) |
|
|
|
// 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 `json:",omitempty"` |
|
|
|
// 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 `json:",omitempty"` |
|
|
|
// 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 |
|
|
|
// SourcePartition and DestinationPartition cannot be wildcards "*" and |
|
// are not compatible with legacy intentions. |
|
SourcePartition string `json:",omitempty"` |
|
DestinationPartition string `json:",omitempty"` |
|
|
|
// SourcePeer cannot be a wildcard "*" and is not compatible with legacy |
|
// intentions. Cannot be used with SourcePartition, as both represent the |
|
// same level of tenancy (partition is local to cluster, peer is remote). |
|
SourcePeer string `json:",omitempty"` |
|
|
|
// SourceSamenessGroup cannot be wildcards "*" and |
|
// is not compatible with legacy intentions. |
|
SourceSamenessGroup string `json:",omitempty"` |
|
|
|
// SourceType is the type of the value for the source. |
|
SourceType IntentionSourceType |
|
|
|
// Action is whether this is an allowlist or denylist intention. |
|
Action IntentionAction `json:",omitempty"` |
|
|
|
// Permissions is the list of additional L7 attributes that extend the |
|
// intention definition. |
|
// |
|
// NOTE: This field is not editable unless editing the underlying |
|
// service-intentions config entry directly. |
|
Permissions []*IntentionPermission `json:",omitempty"` |
|
|
|
// DefaultAddr is not used. |
|
// Deprecated: DefaultAddr is not used and may be removed in a future version. |
|
DefaultAddr string `json:",omitempty"` |
|
// DefaultPort is not used. |
|
// Deprecated: DefaultPort is not used and may be removed in a future version. |
|
DefaultPort int `json:",omitempty"` |
|
|
|
// Meta is arbitrary metadata associated with the intention. This is |
|
// opaque to Consul but is served in API responses. |
|
Meta map[string]string `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. |
|
Precedence int |
|
|
|
// CreatedAt and UpdatedAt keep track of when this record was created |
|
// or modified. |
|
CreatedAt, UpdatedAt time.Time |
|
|
|
// 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 `json:",omitempty"` |
|
|
|
CreateIndex uint64 |
|
ModifyIndex uint64 |
|
} |
|
|
|
// String returns human-friendly output describing ths intention. |
|
func (i *Intention) String() string { |
|
var detail string |
|
switch n := len(i.Permissions); n { |
|
case 0: |
|
detail = string(i.Action) |
|
case 1: |
|
detail = "1 permission" |
|
default: |
|
detail = fmt.Sprintf("%d permissions", len(i.Permissions)) |
|
} |
|
|
|
return fmt.Sprintf("%s => %s (%s)", |
|
i.SourceString(), |
|
i.DestinationString(), |
|
detail) |
|
} |
|
|
|
// SourceString returns the namespace/name format for the source, or |
|
// just "name" if the namespace is the default namespace. |
|
func (i *Intention) SourceString() string { |
|
return i.partString(i.SourceNS, i.SourceName) |
|
} |
|
|
|
// DestinationString returns the namespace/name format for the source, or |
|
// just "name" if the namespace is the default namespace. |
|
func (i *Intention) DestinationString() string { |
|
return i.partString(i.DestinationNS, i.DestinationName) |
|
} |
|
|
|
func (i *Intention) partString(ns, n string) string { |
|
// For now we omit the default namespace from the output. In the future |
|
// we might want to look at this and show this in a multi-namespace world. |
|
if ns != "" && ns != IntentionDefaultNamespace { |
|
n = ns + "/" + n |
|
} |
|
|
|
return n |
|
} |
|
|
|
// IntentionDefaultNamespace is the default namespace value. |
|
const IntentionDefaultNamespace = "default" |
|
|
|
// IntentionAction is the action that the intention represents. This |
|
// can be "allow" or "deny" to allowlist or denylist intentions. |
|
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" |
|
) |
|
|
|
// IntentionMatch are the arguments for the intention match API. |
|
type IntentionMatch struct { |
|
By IntentionMatchType |
|
Names []string |
|
} |
|
|
|
// 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" |
|
) |
|
|
|
// IntentionCheck are the arguments for the intention check API. For |
|
// more documentation see the IntentionCheck function. |
|
type IntentionCheck struct { |
|
// Source and Destination are the source and destination values to |
|
// check. The destination is always a Consul service, but the source |
|
// may be other values as defined by the SourceType. |
|
Source, Destination string |
|
|
|
// SourceType is the type of the value for the source. |
|
SourceType IntentionSourceType |
|
} |
|
|
|
// Intentions returns the list of intentions. |
|
func (h *Connect) Intentions(q *QueryOptions) ([]*Intention, *QueryMeta, error) { |
|
r := h.c.newRequest("GET", "/v1/connect/intentions") |
|
r.setQueryOptions(q) |
|
rtt, resp, err := h.c.doRequest(r) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
if err := requireOK(resp); err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
qm := &QueryMeta{} |
|
parseQueryMeta(resp, qm) |
|
qm.RequestTime = rtt |
|
|
|
var out []*Intention |
|
if err := decodeBody(resp, &out); err != nil { |
|
return nil, nil, err |
|
} |
|
return out, qm, nil |
|
} |
|
|
|
// IntentionGetExact retrieves a single intention by its unique name instead of |
|
// its ID. |
|
func (h *Connect) IntentionGetExact(source, destination string, q *QueryOptions) (*Intention, *QueryMeta, error) { |
|
r := h.c.newRequest("GET", "/v1/connect/intentions/exact") |
|
r.setQueryOptions(q) |
|
r.params.Set("source", source) |
|
r.params.Set("destination", destination) |
|
rtt, resp, err := h.c.doRequest(r) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
|
|
qm := &QueryMeta{} |
|
parseQueryMeta(resp, qm) |
|
qm.RequestTime = rtt |
|
|
|
if resp.StatusCode == 404 { |
|
return nil, qm, nil |
|
} else if resp.StatusCode != 200 { |
|
var buf bytes.Buffer |
|
io.Copy(&buf, resp.Body) |
|
return nil, nil, fmt.Errorf( |
|
"Unexpected response %d: %s", resp.StatusCode, buf.String()) |
|
} |
|
|
|
var out Intention |
|
if err := decodeBody(resp, &out); err != nil { |
|
return nil, nil, err |
|
} |
|
return &out, qm, nil |
|
} |
|
|
|
// IntentionGet retrieves a single intention. |
|
// |
|
// Deprecated: use IntentionGetExact instead |
|
func (h *Connect) IntentionGet(id string, q *QueryOptions) (*Intention, *QueryMeta, error) { |
|
r := h.c.newRequest("GET", "/v1/connect/intentions/"+id) |
|
r.setQueryOptions(q) |
|
rtt, resp, err := h.c.doRequest(r) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
|
|
qm := &QueryMeta{} |
|
parseQueryMeta(resp, qm) |
|
qm.RequestTime = rtt |
|
|
|
if resp.StatusCode == 404 { |
|
return nil, qm, nil |
|
} else if resp.StatusCode != 200 { |
|
var buf bytes.Buffer |
|
io.Copy(&buf, resp.Body) |
|
return nil, nil, fmt.Errorf( |
|
"Unexpected response %d: %s", resp.StatusCode, buf.String()) |
|
} |
|
|
|
var out Intention |
|
if err := decodeBody(resp, &out); err != nil { |
|
return nil, nil, err |
|
} |
|
return &out, qm, nil |
|
} |
|
|
|
// IntentionDeleteExact deletes a single intention by its unique name instead of its ID. |
|
func (h *Connect) IntentionDeleteExact(source, destination string, q *WriteOptions) (*WriteMeta, error) { |
|
r := h.c.newRequest("DELETE", "/v1/connect/intentions/exact") |
|
r.setWriteOptions(q) |
|
r.params.Set("source", source) |
|
r.params.Set("destination", destination) |
|
|
|
rtt, resp, err := h.c.doRequest(r) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
if err := requireOK(resp); err != nil { |
|
return nil, err |
|
} |
|
|
|
qm := &WriteMeta{} |
|
qm.RequestTime = rtt |
|
|
|
return qm, nil |
|
} |
|
|
|
// IntentionDelete deletes a single intention. |
|
// |
|
// Deprecated: use IntentionDeleteExact instead |
|
func (h *Connect) IntentionDelete(id string, q *WriteOptions) (*WriteMeta, error) { |
|
r := h.c.newRequest("DELETE", "/v1/connect/intentions/"+id) |
|
r.setWriteOptions(q) |
|
rtt, resp, err := h.c.doRequest(r) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
if err := requireOK(resp); err != nil { |
|
return nil, err |
|
} |
|
|
|
qm := &WriteMeta{} |
|
qm.RequestTime = rtt |
|
|
|
return qm, nil |
|
} |
|
|
|
// IntentionMatch returns the list of intentions that match a given source |
|
// or destination. The returned intentions are ordered by precedence where |
|
// result[0] is the highest precedence (if that matches, then that rule overrides |
|
// all other rules). |
|
// |
|
// Matching can be done for multiple names at the same time. The resulting |
|
// map is keyed by the given names. Casing is preserved. |
|
func (h *Connect) IntentionMatch(args *IntentionMatch, q *QueryOptions) (map[string][]*Intention, *QueryMeta, error) { |
|
r := h.c.newRequest("GET", "/v1/connect/intentions/match") |
|
r.setQueryOptions(q) |
|
r.params.Set("by", string(args.By)) |
|
for _, name := range args.Names { |
|
r.params.Add("name", name) |
|
} |
|
rtt, resp, err := h.c.doRequest(r) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
if err := requireOK(resp); err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
qm := &QueryMeta{} |
|
parseQueryMeta(resp, qm) |
|
qm.RequestTime = rtt |
|
|
|
var out map[string][]*Intention |
|
if err := decodeBody(resp, &out); err != nil { |
|
return nil, nil, err |
|
} |
|
return out, qm, nil |
|
} |
|
|
|
// IntentionCheck returns whether a given source/destination would be allowed |
|
// or not given the current set of intentions and the configuration of Consul. |
|
func (h *Connect) IntentionCheck(args *IntentionCheck, q *QueryOptions) (bool, *QueryMeta, error) { |
|
r := h.c.newRequest("GET", "/v1/connect/intentions/check") |
|
r.setQueryOptions(q) |
|
r.params.Set("source", args.Source) |
|
r.params.Set("destination", args.Destination) |
|
if args.SourceType != "" { |
|
r.params.Set("source-type", string(args.SourceType)) |
|
} |
|
rtt, resp, err := h.c.doRequest(r) |
|
if err != nil { |
|
return false, nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
if err := requireOK(resp); err != nil { |
|
return false, nil, err |
|
} |
|
|
|
qm := &QueryMeta{} |
|
parseQueryMeta(resp, qm) |
|
qm.RequestTime = rtt |
|
|
|
var out struct{ Allowed bool } |
|
if err := decodeBody(resp, &out); err != nil { |
|
return false, nil, err |
|
} |
|
return out.Allowed, qm, nil |
|
} |
|
|
|
// IntentionUpsert will update an existing intention. The Source & Destination parameters |
|
// in the structure must be non-empty. The ID must be empty. |
|
func (c *Connect) IntentionUpsert(ixn *Intention, q *WriteOptions) (*WriteMeta, error) { |
|
r := c.c.newRequest("PUT", "/v1/connect/intentions/exact") |
|
r.setWriteOptions(q) |
|
r.params.Set("source", maybePrefixNamespaceAndPartition(ixn.SourcePartition, ixn.SourceNS, ixn.SourceName)) |
|
r.params.Set("destination", maybePrefixNamespaceAndPartition(ixn.DestinationPartition, ixn.DestinationNS, ixn.DestinationName)) |
|
r.obj = ixn |
|
rtt, resp, err := c.c.doRequest(r) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
if err := requireOK(resp); err != nil { |
|
return nil, err |
|
} |
|
|
|
wm := &WriteMeta{} |
|
wm.RequestTime = rtt |
|
return wm, nil |
|
} |
|
|
|
func maybePrefixNamespaceAndPartition(part, ns, name string) string { |
|
switch { |
|
case part == "" && ns == "": |
|
return name |
|
case part == "" && ns != "": |
|
return ns + "/" + name |
|
case part != "" && ns == "": |
|
return part + "/" + IntentionDefaultNamespace + "/" + name |
|
default: |
|
return part + "/" + ns + "/" + name |
|
} |
|
} |
|
|
|
// IntentionCreate will create a new intention. The ID in the given |
|
// structure must be empty and a generate ID will be returned on |
|
// success. |
|
// |
|
// Deprecated: use IntentionUpsert instead |
|
func (c *Connect) IntentionCreate(ixn *Intention, q *WriteOptions) (string, *WriteMeta, error) { |
|
r := c.c.newRequest("POST", "/v1/connect/intentions") |
|
r.setWriteOptions(q) |
|
r.obj = ixn |
|
rtt, resp, err := c.c.doRequest(r) |
|
if err != nil { |
|
return "", nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
if err := requireOK(resp); err != nil { |
|
return "", nil, err |
|
} |
|
|
|
wm := &WriteMeta{} |
|
wm.RequestTime = rtt |
|
|
|
var out struct{ ID string } |
|
if err := decodeBody(resp, &out); err != nil { |
|
return "", nil, err |
|
} |
|
return out.ID, wm, nil |
|
} |
|
|
|
// IntentionUpdate will update an existing intention. The ID in the given |
|
// structure must be non-empty. |
|
// |
|
// Deprecated: use IntentionUpsert instead |
|
func (c *Connect) IntentionUpdate(ixn *Intention, q *WriteOptions) (*WriteMeta, error) { |
|
r := c.c.newRequest("PUT", "/v1/connect/intentions/"+ixn.ID) |
|
r.setWriteOptions(q) |
|
r.obj = ixn |
|
rtt, resp, err := c.c.doRequest(r) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer closeResponseBody(resp) |
|
if err := requireOK(resp); err != nil { |
|
return nil, err |
|
} |
|
|
|
wm := &WriteMeta{} |
|
wm.RequestTime = rtt |
|
return wm, nil |
|
}
|
|
|