1package state
2
3import (
4	"fmt"
5	"sort"
6
7	"github.com/hashicorp/consul/agent/structs"
8	"github.com/hashicorp/go-memdb"
9)
10
11const (
12	intentionsTableName = "connect-intentions"
13)
14
15// intentionsTableSchema returns a new table schema used for storing
16// intentions for Connect.
17func intentionsTableSchema() *memdb.TableSchema {
18	return &memdb.TableSchema{
19		Name: intentionsTableName,
20		Indexes: map[string]*memdb.IndexSchema{
21			"id": &memdb.IndexSchema{
22				Name:         "id",
23				AllowMissing: false,
24				Unique:       true,
25				Indexer: &memdb.UUIDFieldIndex{
26					Field: "ID",
27				},
28			},
29			"destination": &memdb.IndexSchema{
30				Name:         "destination",
31				AllowMissing: true,
32				// This index is not unique since we need uniqueness across the whole
33				// 4-tuple.
34				Unique: false,
35				Indexer: &memdb.CompoundIndex{
36					Indexes: []memdb.Indexer{
37						&memdb.StringFieldIndex{
38							Field:     "DestinationNS",
39							Lowercase: true,
40						},
41						&memdb.StringFieldIndex{
42							Field:     "DestinationName",
43							Lowercase: true,
44						},
45					},
46				},
47			},
48			"source": &memdb.IndexSchema{
49				Name:         "source",
50				AllowMissing: true,
51				// This index is not unique since we need uniqueness across the whole
52				// 4-tuple.
53				Unique: false,
54				Indexer: &memdb.CompoundIndex{
55					Indexes: []memdb.Indexer{
56						&memdb.StringFieldIndex{
57							Field:     "SourceNS",
58							Lowercase: true,
59						},
60						&memdb.StringFieldIndex{
61							Field:     "SourceName",
62							Lowercase: true,
63						},
64					},
65				},
66			},
67			"source_destination": &memdb.IndexSchema{
68				Name:         "source_destination",
69				AllowMissing: true,
70				Unique:       true,
71				Indexer: &memdb.CompoundIndex{
72					Indexes: []memdb.Indexer{
73						&memdb.StringFieldIndex{
74							Field:     "SourceNS",
75							Lowercase: true,
76						},
77						&memdb.StringFieldIndex{
78							Field:     "SourceName",
79							Lowercase: true,
80						},
81						&memdb.StringFieldIndex{
82							Field:     "DestinationNS",
83							Lowercase: true,
84						},
85						&memdb.StringFieldIndex{
86							Field:     "DestinationName",
87							Lowercase: true,
88						},
89					},
90				},
91			},
92		},
93	}
94}
95
96func init() {
97	registerSchema(intentionsTableSchema)
98}
99
100// Intentions is used to pull all the intentions from the snapshot.
101func (s *Snapshot) Intentions() (structs.Intentions, error) {
102	ixns, err := s.tx.Get(intentionsTableName, "id")
103	if err != nil {
104		return nil, err
105	}
106
107	var ret structs.Intentions
108	for wrapped := ixns.Next(); wrapped != nil; wrapped = ixns.Next() {
109		ret = append(ret, wrapped.(*structs.Intention))
110	}
111
112	return ret, nil
113}
114
115// Intention is used when restoring from a snapshot.
116func (s *Restore) Intention(ixn *structs.Intention) error {
117	// Insert the intention
118	if err := s.tx.Insert(intentionsTableName, ixn); err != nil {
119		return fmt.Errorf("failed restoring intention: %s", err)
120	}
121	if err := indexUpdateMaxTxn(s.tx, ixn.ModifyIndex, intentionsTableName); err != nil {
122		return fmt.Errorf("failed updating index: %s", err)
123	}
124
125	return nil
126}
127
128// Intentions returns the list of all intentions.
129func (s *Store) Intentions(ws memdb.WatchSet) (uint64, structs.Intentions, error) {
130	tx := s.db.Txn(false)
131	defer tx.Abort()
132
133	// Get the index
134	idx := maxIndexTxn(tx, intentionsTableName)
135	if idx < 1 {
136		idx = 1
137	}
138
139	// Get all intentions
140	iter, err := tx.Get(intentionsTableName, "id")
141	if err != nil {
142		return 0, nil, fmt.Errorf("failed intention lookup: %s", err)
143	}
144	ws.Add(iter.WatchCh())
145
146	var results structs.Intentions
147	for ixn := iter.Next(); ixn != nil; ixn = iter.Next() {
148		results = append(results, ixn.(*structs.Intention))
149	}
150
151	// Sort by precedence just because that's nicer and probably what most clients
152	// want for presentation.
153	sort.Sort(structs.IntentionPrecedenceSorter(results))
154
155	return idx, results, nil
156}
157
158// IntentionSet creates or updates an intention.
159func (s *Store) IntentionSet(idx uint64, ixn *structs.Intention) error {
160	tx := s.db.Txn(true)
161	defer tx.Abort()
162
163	if err := s.intentionSetTxn(tx, idx, ixn); err != nil {
164		return err
165	}
166
167	tx.Commit()
168	return nil
169}
170
171// intentionSetTxn is the inner method used to insert an intention with
172// the proper indexes into the state store.
173func (s *Store) intentionSetTxn(tx *memdb.Txn, idx uint64, ixn *structs.Intention) error {
174	// ID is required
175	if ixn.ID == "" {
176		return ErrMissingIntentionID
177	}
178
179	// Ensure Precedence is populated correctly on "write"
180	ixn.UpdatePrecedence()
181
182	// Check for an existing intention
183	existing, err := tx.First(intentionsTableName, "id", ixn.ID)
184	if err != nil {
185		return fmt.Errorf("failed intention lookup: %s", err)
186	}
187	if existing != nil {
188		oldIxn := existing.(*structs.Intention)
189		ixn.CreateIndex = oldIxn.CreateIndex
190		ixn.CreatedAt = oldIxn.CreatedAt
191	} else {
192		ixn.CreateIndex = idx
193	}
194	ixn.ModifyIndex = idx
195
196	// Check for duplicates on the 4-tuple.
197	duplicate, err := tx.First(intentionsTableName, "source_destination",
198		ixn.SourceNS, ixn.SourceName, ixn.DestinationNS, ixn.DestinationName)
199	if err != nil {
200		return fmt.Errorf("failed intention lookup: %s", err)
201	}
202	if duplicate != nil {
203		dupIxn := duplicate.(*structs.Intention)
204		// Same ID is OK - this is an update
205		if dupIxn.ID != ixn.ID {
206			return fmt.Errorf("duplicate intention found: %s", dupIxn.String())
207		}
208	}
209
210	// We always force meta to be non-nil so that we its an empty map.
211	// This makes it easy for API responses to not nil-check this everywhere.
212	if ixn.Meta == nil {
213		ixn.Meta = make(map[string]string)
214	}
215
216	// Insert
217	if err := tx.Insert(intentionsTableName, ixn); err != nil {
218		return err
219	}
220	if err := tx.Insert("index", &IndexEntry{intentionsTableName, idx}); err != nil {
221		return fmt.Errorf("failed updating index: %s", err)
222	}
223
224	return nil
225}
226
227// IntentionGet returns the given intention by ID.
228func (s *Store) IntentionGet(ws memdb.WatchSet, id string) (uint64, *structs.Intention, error) {
229	tx := s.db.Txn(false)
230	defer tx.Abort()
231
232	// Get the table index.
233	idx := maxIndexTxn(tx, intentionsTableName)
234	if idx < 1 {
235		idx = 1
236	}
237
238	// Look up by its ID.
239	watchCh, intention, err := tx.FirstWatch(intentionsTableName, "id", id)
240	if err != nil {
241		return 0, nil, fmt.Errorf("failed intention lookup: %s", err)
242	}
243	ws.Add(watchCh)
244
245	// Convert the interface{} if it is non-nil
246	var result *structs.Intention
247	if intention != nil {
248		result = intention.(*structs.Intention)
249	}
250
251	return idx, result, nil
252}
253
254// IntentionDelete deletes the given intention by ID.
255func (s *Store) IntentionDelete(idx uint64, id string) error {
256	tx := s.db.Txn(true)
257	defer tx.Abort()
258
259	if err := s.intentionDeleteTxn(tx, idx, id); err != nil {
260		return fmt.Errorf("failed intention delete: %s", err)
261	}
262
263	tx.Commit()
264	return nil
265}
266
267// intentionDeleteTxn is the inner method used to delete a intention
268// with the proper indexes into the state store.
269func (s *Store) intentionDeleteTxn(tx *memdb.Txn, idx uint64, queryID string) error {
270	// Pull the query.
271	wrapped, err := tx.First(intentionsTableName, "id", queryID)
272	if err != nil {
273		return fmt.Errorf("failed intention lookup: %s", err)
274	}
275	if wrapped == nil {
276		return nil
277	}
278
279	// Delete the query and update the index.
280	if err := tx.Delete(intentionsTableName, wrapped); err != nil {
281		return fmt.Errorf("failed intention delete: %s", err)
282	}
283	if err := tx.Insert("index", &IndexEntry{intentionsTableName, idx}); err != nil {
284		return fmt.Errorf("failed updating index: %s", err)
285	}
286
287	return nil
288}
289
290// IntentionMatch returns the list of intentions that match the namespace and
291// name for either a source or destination. This applies the resolution rules
292// so wildcards will match any value.
293//
294// The returned value is the list of intentions in the same order as the
295// entries in args. The intentions themselves are sorted based on the
296// intention precedence rules. i.e. result[0][0] is the highest precedent
297// rule to match for the first entry.
298func (s *Store) IntentionMatch(ws memdb.WatchSet, args *structs.IntentionQueryMatch) (uint64, []structs.Intentions, error) {
299	tx := s.db.Txn(false)
300	defer tx.Abort()
301
302	// Get the table index.
303	idx := maxIndexTxn(tx, intentionsTableName)
304	if idx < 1 {
305		idx = 1
306	}
307
308	// Make all the calls and accumulate the results
309	results := make([]structs.Intentions, len(args.Entries))
310	for i, entry := range args.Entries {
311		// Each search entry may require multiple queries to memdb, so this
312		// returns the arguments for each necessary Get. Note on performance:
313		// this is not the most optimal set of queries since we repeat some
314		// many times (such as */*). We can work on improving that in the
315		// future, the test cases shouldn't have to change for that.
316		getParams, err := s.intentionMatchGetParams(entry)
317		if err != nil {
318			return 0, nil, err
319		}
320
321		// Perform each call and accumulate the result.
322		var ixns structs.Intentions
323		for _, params := range getParams {
324			iter, err := tx.Get(intentionsTableName, string(args.Type), params...)
325			if err != nil {
326				return 0, nil, fmt.Errorf("failed intention lookup: %s", err)
327			}
328
329			ws.Add(iter.WatchCh())
330
331			for ixn := iter.Next(); ixn != nil; ixn = iter.Next() {
332				ixns = append(ixns, ixn.(*structs.Intention))
333			}
334		}
335
336		// Sort the results by precedence
337		sort.Sort(structs.IntentionPrecedenceSorter(ixns))
338
339		// Store the result
340		results[i] = ixns
341	}
342
343	return idx, results, nil
344}
345
346// intentionMatchGetParams returns the tx.Get parameters to find all the
347// intentions for a certain entry.
348func (s *Store) intentionMatchGetParams(entry structs.IntentionMatchEntry) ([][]interface{}, error) {
349	// We always query for "*/*" so include that. If the namespace is a
350	// wildcard, then we're actually done.
351	result := make([][]interface{}, 0, 3)
352	result = append(result, []interface{}{"*", "*"})
353	if entry.Namespace == structs.IntentionWildcard {
354		return result, nil
355	}
356
357	// Search for NS/* intentions. If we have a wildcard name, then we're done.
358	result = append(result, []interface{}{entry.Namespace, "*"})
359	if entry.Name == structs.IntentionWildcard {
360		return result, nil
361	}
362
363	// Search for the exact NS/N value.
364	result = append(result, []interface{}{entry.Namespace, entry.Name})
365	return result, nil
366}
367