1package api
2
3import (
4	"bytes"
5	"fmt"
6	"io"
7	"time"
8)
9
10// Intention defines an intention for the Connect Service Graph. This defines
11// the allowed or denied behavior of a connection between two services using
12// Connect.
13type Intention struct {
14	// ID is the UUID-based ID for the intention, always generated by Consul.
15	ID string `json:",omitempty"`
16
17	// Description is a human-friendly description of this intention.
18	// It is opaque to Consul and is only stored and transferred in API
19	// requests.
20	Description string `json:",omitempty"`
21
22	// SourceNS, SourceName are the namespace and name, respectively, of
23	// the source service. Either of these may be the wildcard "*", but only
24	// the full value can be a wildcard. Partial wildcards are not allowed.
25	// The source may also be a non-Consul service, as specified by SourceType.
26	//
27	// DestinationNS, DestinationName is the same, but for the destination
28	// service. The same rules apply. The destination is always a Consul
29	// service.
30	SourceNS, SourceName           string
31	DestinationNS, DestinationName string
32
33	// SourceType is the type of the value for the source.
34	SourceType IntentionSourceType
35
36	// Action is whether this is an allowlist or denylist intention.
37	Action IntentionAction `json:",omitempty"`
38
39	// Permissions is the list of additional L7 attributes that extend the
40	// intention definition.
41	//
42	// NOTE: This field is not editable unless editing the underlying
43	// service-intentions config entry directly.
44	Permissions []*IntentionPermission `json:",omitempty"`
45
46	// DefaultAddr is not used.
47	// Deprecated: DefaultAddr is not used and may be removed in a future version.
48	DefaultAddr string `json:",omitempty"`
49	// DefaultPort is not used.
50	// Deprecated: DefaultPort is not used and may be removed in a future version.
51	DefaultPort int `json:",omitempty"`
52
53	// Meta is arbitrary metadata associated with the intention. This is
54	// opaque to Consul but is served in API responses.
55	Meta map[string]string `json:",omitempty"`
56
57	// Precedence is the order that the intention will be applied, with
58	// larger numbers being applied first. This is a read-only field, on
59	// any intention update it is updated.
60	Precedence int
61
62	// CreatedAt and UpdatedAt keep track of when this record was created
63	// or modified.
64	CreatedAt, UpdatedAt time.Time
65
66	// Hash of the contents of the intention
67	//
68	// This is needed mainly for replication purposes. When replicating from
69	// one DC to another keeping the content Hash will allow us to detect
70	// content changes more efficiently than checking every single field
71	Hash []byte `json:",omitempty"`
72
73	CreateIndex uint64
74	ModifyIndex uint64
75}
76
77// String returns human-friendly output describing ths intention.
78func (i *Intention) String() string {
79	var detail string
80	switch n := len(i.Permissions); n {
81	case 0:
82		detail = string(i.Action)
83	case 1:
84		detail = "1 permission"
85	default:
86		detail = fmt.Sprintf("%d permissions", len(i.Permissions))
87	}
88
89	return fmt.Sprintf("%s => %s (%s)",
90		i.SourceString(),
91		i.DestinationString(),
92		detail)
93}
94
95// SourceString returns the namespace/name format for the source, or
96// just "name" if the namespace is the default namespace.
97func (i *Intention) SourceString() string {
98	return i.partString(i.SourceNS, i.SourceName)
99}
100
101// DestinationString returns the namespace/name format for the source, or
102// just "name" if the namespace is the default namespace.
103func (i *Intention) DestinationString() string {
104	return i.partString(i.DestinationNS, i.DestinationName)
105}
106
107func (i *Intention) partString(ns, n string) string {
108	// For now we omit the default namespace from the output. In the future
109	// we might want to look at this and show this in a multi-namespace world.
110	if ns != "" && ns != IntentionDefaultNamespace {
111		n = ns + "/" + n
112	}
113
114	return n
115}
116
117// IntentionDefaultNamespace is the default namespace value.
118const IntentionDefaultNamespace = "default"
119
120// IntentionAction is the action that the intention represents. This
121// can be "allow" or "deny" to allowlist or denylist intentions.
122type IntentionAction string
123
124const (
125	IntentionActionAllow IntentionAction = "allow"
126	IntentionActionDeny  IntentionAction = "deny"
127)
128
129// IntentionSourceType is the type of the source within an intention.
130type IntentionSourceType string
131
132const (
133	// IntentionSourceConsul is a service within the Consul catalog.
134	IntentionSourceConsul IntentionSourceType = "consul"
135)
136
137// IntentionMatch are the arguments for the intention match API.
138type IntentionMatch struct {
139	By    IntentionMatchType
140	Names []string
141}
142
143// IntentionMatchType is the target for a match request. For example,
144// matching by source will look for all intentions that match the given
145// source value.
146type IntentionMatchType string
147
148const (
149	IntentionMatchSource      IntentionMatchType = "source"
150	IntentionMatchDestination IntentionMatchType = "destination"
151)
152
153// IntentionCheck are the arguments for the intention check API. For
154// more documentation see the IntentionCheck function.
155type IntentionCheck struct {
156	// Source and Destination are the source and destination values to
157	// check. The destination is always a Consul service, but the source
158	// may be other values as defined by the SourceType.
159	Source, Destination string
160
161	// SourceType is the type of the value for the source.
162	SourceType IntentionSourceType
163}
164
165// Intentions returns the list of intentions.
166func (h *Connect) Intentions(q *QueryOptions) ([]*Intention, *QueryMeta, error) {
167	r := h.c.newRequest("GET", "/v1/connect/intentions")
168	r.setQueryOptions(q)
169	rtt, resp, err := requireOK(h.c.doRequest(r))
170	if err != nil {
171		return nil, nil, err
172	}
173	defer closeResponseBody(resp)
174
175	qm := &QueryMeta{}
176	parseQueryMeta(resp, qm)
177	qm.RequestTime = rtt
178
179	var out []*Intention
180	if err := decodeBody(resp, &out); err != nil {
181		return nil, nil, err
182	}
183	return out, qm, nil
184}
185
186// IntentionGetExact retrieves a single intention by its unique name instead of
187// its ID.
188func (h *Connect) IntentionGetExact(source, destination string, q *QueryOptions) (*Intention, *QueryMeta, error) {
189	r := h.c.newRequest("GET", "/v1/connect/intentions/exact")
190	r.setQueryOptions(q)
191	r.params.Set("source", source)
192	r.params.Set("destination", destination)
193	rtt, resp, err := h.c.doRequest(r)
194	if err != nil {
195		return nil, nil, err
196	}
197	defer closeResponseBody(resp)
198
199	qm := &QueryMeta{}
200	parseQueryMeta(resp, qm)
201	qm.RequestTime = rtt
202
203	if resp.StatusCode == 404 {
204		return nil, qm, nil
205	} else if resp.StatusCode != 200 {
206		var buf bytes.Buffer
207		io.Copy(&buf, resp.Body)
208		return nil, nil, fmt.Errorf(
209			"Unexpected response %d: %s", resp.StatusCode, buf.String())
210	}
211
212	var out Intention
213	if err := decodeBody(resp, &out); err != nil {
214		return nil, nil, err
215	}
216	return &out, qm, nil
217}
218
219// IntentionGet retrieves a single intention.
220//
221// Deprecated: use IntentionGetExact instead
222func (h *Connect) IntentionGet(id string, q *QueryOptions) (*Intention, *QueryMeta, error) {
223	r := h.c.newRequest("GET", "/v1/connect/intentions/"+id)
224	r.setQueryOptions(q)
225	rtt, resp, err := h.c.doRequest(r)
226	if err != nil {
227		return nil, nil, err
228	}
229	defer closeResponseBody(resp)
230
231	qm := &QueryMeta{}
232	parseQueryMeta(resp, qm)
233	qm.RequestTime = rtt
234
235	if resp.StatusCode == 404 {
236		return nil, qm, nil
237	} else if resp.StatusCode != 200 {
238		var buf bytes.Buffer
239		io.Copy(&buf, resp.Body)
240		return nil, nil, fmt.Errorf(
241			"Unexpected response %d: %s", resp.StatusCode, buf.String())
242	}
243
244	var out Intention
245	if err := decodeBody(resp, &out); err != nil {
246		return nil, nil, err
247	}
248	return &out, qm, nil
249}
250
251// IntentionDeleteExact deletes a single intention by its unique name instead of its ID.
252func (h *Connect) IntentionDeleteExact(source, destination string, q *WriteOptions) (*WriteMeta, error) {
253	r := h.c.newRequest("DELETE", "/v1/connect/intentions/exact")
254	r.setWriteOptions(q)
255	r.params.Set("source", source)
256	r.params.Set("destination", destination)
257
258	rtt, resp, err := requireOK(h.c.doRequest(r))
259	if err != nil {
260		return nil, err
261	}
262	defer closeResponseBody(resp)
263
264	qm := &WriteMeta{}
265	qm.RequestTime = rtt
266
267	return qm, nil
268}
269
270// IntentionDelete deletes a single intention.
271//
272// Deprecated: use IntentionDeleteExact instead
273func (h *Connect) IntentionDelete(id string, q *WriteOptions) (*WriteMeta, error) {
274	r := h.c.newRequest("DELETE", "/v1/connect/intentions/"+id)
275	r.setWriteOptions(q)
276	rtt, resp, err := requireOK(h.c.doRequest(r))
277	if err != nil {
278		return nil, err
279	}
280	defer closeResponseBody(resp)
281
282	qm := &WriteMeta{}
283	qm.RequestTime = rtt
284
285	return qm, nil
286}
287
288// IntentionMatch returns the list of intentions that match a given source
289// or destination. The returned intentions are ordered by precedence where
290// result[0] is the highest precedence (if that matches, then that rule overrides
291// all other rules).
292//
293// Matching can be done for multiple names at the same time. The resulting
294// map is keyed by the given names. Casing is preserved.
295func (h *Connect) IntentionMatch(args *IntentionMatch, q *QueryOptions) (map[string][]*Intention, *QueryMeta, error) {
296	r := h.c.newRequest("GET", "/v1/connect/intentions/match")
297	r.setQueryOptions(q)
298	r.params.Set("by", string(args.By))
299	for _, name := range args.Names {
300		r.params.Add("name", name)
301	}
302	rtt, resp, err := requireOK(h.c.doRequest(r))
303	if err != nil {
304		return nil, nil, err
305	}
306	defer closeResponseBody(resp)
307
308	qm := &QueryMeta{}
309	parseQueryMeta(resp, qm)
310	qm.RequestTime = rtt
311
312	var out map[string][]*Intention
313	if err := decodeBody(resp, &out); err != nil {
314		return nil, nil, err
315	}
316	return out, qm, nil
317}
318
319// IntentionCheck returns whether a given source/destination would be allowed
320// or not given the current set of intentions and the configuration of Consul.
321func (h *Connect) IntentionCheck(args *IntentionCheck, q *QueryOptions) (bool, *QueryMeta, error) {
322	r := h.c.newRequest("GET", "/v1/connect/intentions/check")
323	r.setQueryOptions(q)
324	r.params.Set("source", args.Source)
325	r.params.Set("destination", args.Destination)
326	if args.SourceType != "" {
327		r.params.Set("source-type", string(args.SourceType))
328	}
329	rtt, resp, err := requireOK(h.c.doRequest(r))
330	if err != nil {
331		return false, nil, err
332	}
333	defer closeResponseBody(resp)
334
335	qm := &QueryMeta{}
336	parseQueryMeta(resp, qm)
337	qm.RequestTime = rtt
338
339	var out struct{ Allowed bool }
340	if err := decodeBody(resp, &out); err != nil {
341		return false, nil, err
342	}
343	return out.Allowed, qm, nil
344}
345
346// IntentionUpsert will update an existing intention. The Source & Destination parameters
347// in the structure must be non-empty. The ID must be empty.
348func (c *Connect) IntentionUpsert(ixn *Intention, q *WriteOptions) (*WriteMeta, error) {
349	r := c.c.newRequest("PUT", "/v1/connect/intentions/exact")
350	r.setWriteOptions(q)
351	r.params.Set("source", maybePrefixNamespace(ixn.SourceNS, ixn.SourceName))
352	r.params.Set("destination", maybePrefixNamespace(ixn.DestinationNS, ixn.DestinationName))
353	r.obj = ixn
354	rtt, resp, err := requireOK(c.c.doRequest(r))
355	if err != nil {
356		return nil, err
357	}
358	defer closeResponseBody(resp)
359
360	wm := &WriteMeta{}
361	wm.RequestTime = rtt
362	return wm, nil
363}
364
365func maybePrefixNamespace(ns, name string) string {
366	if ns == "" {
367		return name
368	}
369	return ns + "/" + name
370}
371
372// IntentionCreate will create a new intention. The ID in the given
373// structure must be empty and a generate ID will be returned on
374// success.
375//
376// Deprecated: use IntentionUpsert instead
377func (c *Connect) IntentionCreate(ixn *Intention, q *WriteOptions) (string, *WriteMeta, error) {
378	r := c.c.newRequest("POST", "/v1/connect/intentions")
379	r.setWriteOptions(q)
380	r.obj = ixn
381	rtt, resp, err := requireOK(c.c.doRequest(r))
382	if err != nil {
383		return "", nil, err
384	}
385	defer closeResponseBody(resp)
386
387	wm := &WriteMeta{}
388	wm.RequestTime = rtt
389
390	var out struct{ ID string }
391	if err := decodeBody(resp, &out); err != nil {
392		return "", nil, err
393	}
394	return out.ID, wm, nil
395}
396
397// IntentionUpdate will update an existing intention. The ID in the given
398// structure must be non-empty.
399//
400// Deprecated: use IntentionUpsert instead
401func (c *Connect) IntentionUpdate(ixn *Intention, q *WriteOptions) (*WriteMeta, error) {
402	r := c.c.newRequest("PUT", "/v1/connect/intentions/"+ixn.ID)
403	r.setWriteOptions(q)
404	r.obj = ixn
405	rtt, resp, err := requireOK(c.c.doRequest(r))
406	if err != nil {
407		return nil, err
408	}
409	defer closeResponseBody(resp)
410
411	wm := &WriteMeta{}
412	wm.RequestTime = rtt
413	return wm, nil
414}
415