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