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 DestinationPartition 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 `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 }