1package state
2
3import (
4	"fmt"
5	"regexp"
6
7	"github.com/hashicorp/consul/agent/consul/prepared_query"
8	"github.com/hashicorp/consul/agent/structs"
9	"github.com/hashicorp/go-memdb"
10)
11
12// preparedQueriesTableSchema returns a new table schema used for storing
13// prepared queries.
14func preparedQueriesTableSchema() *memdb.TableSchema {
15	return &memdb.TableSchema{
16		Name: "prepared-queries",
17		Indexes: map[string]*memdb.IndexSchema{
18			"id": &memdb.IndexSchema{
19				Name:         "id",
20				AllowMissing: false,
21				Unique:       true,
22				Indexer: &memdb.UUIDFieldIndex{
23					Field: "ID",
24				},
25			},
26			"name": &memdb.IndexSchema{
27				Name:         "name",
28				AllowMissing: true,
29				Unique:       true,
30				Indexer: &memdb.StringFieldIndex{
31					Field:     "Name",
32					Lowercase: true,
33				},
34			},
35			"template": &memdb.IndexSchema{
36				Name:         "template",
37				AllowMissing: true,
38				Unique:       true,
39				Indexer:      &PreparedQueryIndex{},
40			},
41			"session": &memdb.IndexSchema{
42				Name:         "session",
43				AllowMissing: true,
44				Unique:       false,
45				Indexer: &memdb.UUIDFieldIndex{
46					Field: "Session",
47				},
48			},
49		},
50	}
51}
52
53func init() {
54	registerSchema(preparedQueriesTableSchema)
55}
56
57// validUUID is used to check if a given string looks like a UUID
58var validUUID = regexp.MustCompile(`(?i)^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$`)
59
60// isUUID returns true if the given string is a valid UUID.
61func isUUID(str string) bool {
62	const uuidLen = 36
63	if len(str) != uuidLen {
64		return false
65	}
66
67	return validUUID.MatchString(str)
68}
69
70// queryWrapper is an internal structure that is used to store a query alongside
71// its compiled template, which can be nil.
72type queryWrapper struct {
73	// We embed the PreparedQuery structure so that the UUID field indexer
74	// can see the ID directly.
75	*structs.PreparedQuery
76
77	// ct is the compiled template, or nil if the query isn't a template. The
78	// state store manages this and keeps it up to date every time the query
79	// changes.
80	ct *prepared_query.CompiledTemplate
81}
82
83// toPreparedQuery unwraps the internal form of a prepared query and returns
84// the regular struct.
85func toPreparedQuery(wrapped interface{}) *structs.PreparedQuery {
86	if wrapped == nil {
87		return nil
88	}
89	return wrapped.(*queryWrapper).PreparedQuery
90}
91
92// PreparedQueries is used to pull all the prepared queries from the snapshot.
93func (s *Snapshot) PreparedQueries() (structs.PreparedQueries, error) {
94	queries, err := s.tx.Get("prepared-queries", "id")
95	if err != nil {
96		return nil, err
97	}
98
99	var ret structs.PreparedQueries
100	for wrapped := queries.Next(); wrapped != nil; wrapped = queries.Next() {
101		ret = append(ret, toPreparedQuery(wrapped))
102	}
103	return ret, nil
104}
105
106// PreparedQuery is used when restoring from a snapshot. For general inserts,
107// use PreparedQuerySet.
108func (s *Restore) PreparedQuery(query *structs.PreparedQuery) error {
109	// If this is a template, compile it, otherwise leave the compiled
110	// template field nil.
111	var ct *prepared_query.CompiledTemplate
112	if prepared_query.IsTemplate(query) {
113		var err error
114		ct, err = prepared_query.Compile(query)
115		if err != nil {
116			return fmt.Errorf("failed compiling template: %s", err)
117		}
118	}
119
120	// Insert the wrapped query.
121	if err := s.tx.Insert("prepared-queries", &queryWrapper{query, ct}); err != nil {
122		return fmt.Errorf("failed restoring prepared query: %s", err)
123	}
124	if err := indexUpdateMaxTxn(s.tx, query.ModifyIndex, "prepared-queries"); err != nil {
125		return fmt.Errorf("failed updating index: %s", err)
126	}
127
128	return nil
129}
130
131// PreparedQuerySet is used to create or update a prepared query.
132func (s *Store) PreparedQuerySet(idx uint64, query *structs.PreparedQuery) error {
133	tx := s.db.Txn(true)
134	defer tx.Abort()
135
136	if err := s.preparedQuerySetTxn(tx, idx, query); err != nil {
137		return err
138	}
139
140	tx.Commit()
141	return nil
142}
143
144// preparedQuerySetTxn is the inner method used to insert a prepared query with
145// the proper indexes into the state store.
146func (s *Store) preparedQuerySetTxn(tx *memdb.Txn, idx uint64, query *structs.PreparedQuery) error {
147	// Check that the ID is set.
148	if query.ID == "" {
149		return ErrMissingQueryID
150	}
151
152	// Check for an existing query.
153	wrapped, err := tx.First("prepared-queries", "id", query.ID)
154	if err != nil {
155		return fmt.Errorf("failed prepared query lookup: %s", err)
156	}
157	existing := toPreparedQuery(wrapped)
158
159	// Set the indexes.
160	if existing != nil {
161		query.CreateIndex = existing.CreateIndex
162		query.ModifyIndex = idx
163	} else {
164		query.CreateIndex = idx
165		query.ModifyIndex = idx
166	}
167
168	// Verify that the query name doesn't already exist, or that we are
169	// updating the same instance that has this name. If this is a template
170	// and the name is empty then we make sure there's not an empty template
171	// already registered.
172	if query.Name != "" {
173		wrapped, err := tx.First("prepared-queries", "name", query.Name)
174		if err != nil {
175			return fmt.Errorf("failed prepared query lookup: %s", err)
176		}
177		other := toPreparedQuery(wrapped)
178		if other != nil && (existing == nil || existing.ID != other.ID) {
179			return fmt.Errorf("name '%s' aliases an existing query name", query.Name)
180		}
181	} else if prepared_query.IsTemplate(query) {
182		wrapped, err := tx.First("prepared-queries", "template", query.Name)
183		if err != nil {
184			return fmt.Errorf("failed prepared query lookup: %s", err)
185		}
186		other := toPreparedQuery(wrapped)
187		if other != nil && (existing == nil || existing.ID != other.ID) {
188			return fmt.Errorf("a query template with an empty name already exists")
189		}
190	}
191
192	// Verify that the name doesn't alias any existing ID. We allow queries
193	// to be looked up by ID *or* name so we don't want anyone to try to
194	// register a query with a name equal to some other query's ID in an
195	// attempt to hijack it. We also look up by ID *then* name in order to
196	// prevent this, but it seems prudent to prevent these types of rogue
197	// queries from ever making it into the state store. Note that we have
198	// to see if the name looks like a UUID before checking since the UUID
199	// index will complain if we look up something that's not formatted
200	// like one.
201	if isUUID(query.Name) {
202		wrapped, err := tx.First("prepared-queries", "id", query.Name)
203		if err != nil {
204			return fmt.Errorf("failed prepared query lookup: %s", err)
205		}
206		if wrapped != nil {
207			return fmt.Errorf("name '%s' aliases an existing query ID", query.Name)
208		}
209	}
210
211	// Verify that the session exists.
212	if query.Session != "" {
213		sess, err := firstWithTxn(tx, "sessions", "id", query.Session, nil)
214		if err != nil {
215			return fmt.Errorf("invalid session: %v", err)
216		}
217		if sess == nil {
218			return fmt.Errorf("invalid session %#v", query.Session)
219		}
220	}
221
222	// We do not verify the service here, nor the token, if any. These are
223	// checked at execute time and not doing integrity checking on them
224	// helps avoid bootstrapping chicken and egg problems.
225
226	// If this is a template, compile it, otherwise leave the compiled
227	// template field nil.
228	var ct *prepared_query.CompiledTemplate
229	if prepared_query.IsTemplate(query) {
230		var err error
231		ct, err = prepared_query.Compile(query)
232		if err != nil {
233			return fmt.Errorf("failed compiling template: %s", err)
234		}
235	}
236
237	// Insert the wrapped query.
238	if err := tx.Insert("prepared-queries", &queryWrapper{query, ct}); err != nil {
239		return fmt.Errorf("failed inserting prepared query: %s", err)
240	}
241	if err := tx.Insert("index", &IndexEntry{"prepared-queries", idx}); err != nil {
242		return fmt.Errorf("failed updating index: %s", err)
243	}
244
245	return nil
246}
247
248// PreparedQueryDelete deletes the given query by ID.
249func (s *Store) PreparedQueryDelete(idx uint64, queryID string) error {
250	tx := s.db.Txn(true)
251	defer tx.Abort()
252
253	if err := s.preparedQueryDeleteTxn(tx, idx, queryID); err != nil {
254		return fmt.Errorf("failed prepared query delete: %s", err)
255	}
256
257	tx.Commit()
258	return nil
259}
260
261// preparedQueryDeleteTxn is the inner method used to delete a prepared query
262// with the proper indexes into the state store.
263func (s *Store) preparedQueryDeleteTxn(tx *memdb.Txn, idx uint64, queryID string) error {
264	// Pull the query.
265	wrapped, err := tx.First("prepared-queries", "id", queryID)
266	if err != nil {
267		return fmt.Errorf("failed prepared query lookup: %s", err)
268	}
269	if wrapped == nil {
270		return nil
271	}
272
273	// Delete the query and update the index.
274	if err := tx.Delete("prepared-queries", wrapped); err != nil {
275		return fmt.Errorf("failed prepared query delete: %s", err)
276	}
277	if err := tx.Insert("index", &IndexEntry{"prepared-queries", idx}); err != nil {
278		return fmt.Errorf("failed updating index: %s", err)
279	}
280
281	return nil
282}
283
284// PreparedQueryGet returns the given prepared query by ID.
285func (s *Store) PreparedQueryGet(ws memdb.WatchSet, queryID string) (uint64, *structs.PreparedQuery, error) {
286	tx := s.db.Txn(false)
287	defer tx.Abort()
288
289	// Get the table index.
290	idx := maxIndexTxn(tx, "prepared-queries")
291
292	// Look up the query by its ID.
293	watchCh, wrapped, err := tx.FirstWatch("prepared-queries", "id", queryID)
294	if err != nil {
295		return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err)
296	}
297	ws.Add(watchCh)
298	return idx, toPreparedQuery(wrapped), nil
299}
300
301// PreparedQueryResolve returns the given prepared query by looking up an ID or
302// Name. If the query was looked up by name and it's a template, then the
303// template will be rendered before it is returned.
304func (s *Store) PreparedQueryResolve(queryIDOrName string, source structs.QuerySource) (uint64, *structs.PreparedQuery, error) {
305	tx := s.db.Txn(false)
306	defer tx.Abort()
307
308	// Get the table index.
309	idx := maxIndexTxn(tx, "prepared-queries")
310
311	// Explicitly ban an empty query. This will never match an ID and the
312	// schema is set up so it will never match a query with an empty name,
313	// but we check it here to be explicit about it (we'd never want to
314	// return the results from the first query w/o a name).
315	if queryIDOrName == "" {
316		return 0, nil, ErrMissingQueryID
317	}
318
319	// Try first by ID if it looks like they gave us an ID. We check the
320	// format before trying this because the UUID index will complain if
321	// we look up something that's not formatted like one.
322	if isUUID(queryIDOrName) {
323		wrapped, err := tx.First("prepared-queries", "id", queryIDOrName)
324		if err != nil {
325			return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err)
326		}
327		if wrapped != nil {
328			query := toPreparedQuery(wrapped)
329			if prepared_query.IsTemplate(query) {
330				return idx, nil, fmt.Errorf("prepared query templates can only be resolved up by name, not by ID")
331			}
332			return idx, query, nil
333		}
334	}
335
336	// prep will check to see if the query is a template and render it
337	// first, otherwise it will just return a regular query.
338	prep := func(wrapped interface{}) (uint64, *structs.PreparedQuery, error) {
339		wrapper := wrapped.(*queryWrapper)
340		if prepared_query.IsTemplate(wrapper.PreparedQuery) {
341			render, err := wrapper.ct.Render(queryIDOrName, source)
342			if err != nil {
343				return idx, nil, err
344			}
345			return idx, render, nil
346		}
347		return idx, wrapper.PreparedQuery, nil
348	}
349
350	// Next, look for an exact name match. This is the common case for static
351	// prepared queries, and could also apply to templates.
352	{
353		wrapped, err := tx.First("prepared-queries", "name", queryIDOrName)
354		if err != nil {
355			return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err)
356		}
357		if wrapped != nil {
358			return prep(wrapped)
359		}
360	}
361
362	// Next, look for the longest prefix match among the prepared query
363	// templates.
364	{
365		wrapped, err := tx.LongestPrefix("prepared-queries", "template_prefix", queryIDOrName)
366		if err != nil {
367			return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err)
368		}
369		if wrapped != nil {
370			return prep(wrapped)
371		}
372	}
373
374	return idx, nil, nil
375}
376
377// PreparedQueryList returns all the prepared queries.
378func (s *Store) PreparedQueryList(ws memdb.WatchSet) (uint64, structs.PreparedQueries, error) {
379	tx := s.db.Txn(false)
380	defer tx.Abort()
381
382	// Get the table index.
383	idx := maxIndexTxn(tx, "prepared-queries")
384
385	// Query all of the prepared queries in the state store.
386	queries, err := tx.Get("prepared-queries", "id")
387	if err != nil {
388		return 0, nil, fmt.Errorf("failed prepared query lookup: %s", err)
389	}
390	ws.Add(queries.WatchCh())
391
392	// Go over all of the queries and build the response.
393	var result structs.PreparedQueries
394	for wrapped := queries.Next(); wrapped != nil; wrapped = queries.Next() {
395		result = append(result, toPreparedQuery(wrapped))
396	}
397	return idx, result, nil
398}
399