internal/store/secrets_test.go
349 lines · 10931 bytes
9 symbols in this file
1package store
2
3import (
4 "path/filepath"
5 "strings"
6 "testing"
7
8 "gitbay.org/gitbay/internal/seal"
9)
10
11// keyedStore is a migrated store with a key file of one key.
12func keyedStore(t *testing.T) (*Store, string, int64, int64) {
13 t.Helper()
14 s := open(t)
15 if err := s.MigrateUp(); err != nil {
16 t.Fatal(err)
17 }
18 path := filepath.Join(t.TempDir(), "secret.key")
19 k, err := seal.NewKey()
20 if err != nil {
21 t.Fatal(err)
22 }
23 if err := seal.WriteKeys(path, []seal.Key{k}); err != nil {
24 t.Fatal(err)
25 }
26 ring, err := seal.Load(path)
27 if err != nil {
28 t.Fatal(err)
29 }
30 s.SetKeyring(ring)
31 uid, err := s.CreateUser("alice", false)
32 if err != nil {
33 t.Fatal(err)
34 }
35 repoID, err := s.CreateRepo("user", uid, "app", "public")
36 if err != nil {
37 t.Fatal(err)
38 }
39 return s, path, uid, repoID
40}
41
42// raw reads every stored value of the secret columns.
43func raw(t *testing.T, s *Store) []string {
44 t.Helper()
45 var out []string
46 for _, sc := range secretColumns {
47 rows, err := s.DB.Query("SELECT " + sc.column + " FROM " + sc.table + " WHERE " + sc.column + " != ''")
48 if err != nil {
49 t.Fatal(err)
50 }
51 for rows.Next() {
52 var v string
53 if err := rows.Scan(&v); err != nil {
54 t.Fatal(err)
55 }
56 out = append(out, v)
57 }
58 rows.Close()
59 }
60 return out
61}
62
63func TestSecretColumnsAreSealed(t *testing.T) {
64 s, _, uid, repoID := keyedStore(t)
65 if err := s.SetBuildSecret(repoID, "DEPLOY", "ci-secret"); err != nil {
66 t.Fatal(err)
67 }
68 if _, err := s.AddWebhook(repoID, "https://hook.example/x", "hook-secret", "*"); err != nil {
69 t.Fatal(err)
70 }
71 if _, err := s.AddMirror(repoID, "push", "https://mirror.example/r.git", "u", "mirror-token"); err != nil {
72 t.Fatal(err)
73 }
74 if _, err := s.AddPushDevice(uid, "apns-token", "phone"); err != nil {
75 t.Fatal(err)
76 }
77
78 vals := raw(t, s)
79 if len(vals) != 4 {
80 t.Fatalf("stored %d values, want 4: %v", len(vals), vals)
81 }
82 for _, v := range vals {
83 if !seal.IsSealed(v) {
84 t.Errorf("stored in clear: %q", v)
85 }
86 for _, plain := range []string{"ci-secret", "hook-secret", "mirror-token", "apns-token"} {
87 if strings.Contains(v, plain) {
88 t.Errorf("%q carries %q", v, plain)
89 }
90 }
91 }
92
93 secrets, err := s.BuildSecrets(repoID)
94 if err != nil || secrets["DEPLOY"] != "ci-secret" {
95 t.Fatalf("BuildSecrets = %v, %v", secrets, err)
96 }
97 hooks, err := s.ListWebhooks(repoID)
98 if err != nil || len(hooks) != 1 || hooks[0].Secret != "hook-secret" {
99 t.Fatalf("ListWebhooks = %+v, %v", hooks, err)
100 }
101 ms, err := s.ListMirrors(repoID)
102 if err != nil || len(ms) != 1 || ms[0].Token != "mirror-token" {
103 t.Fatalf("ListMirrors = %+v, %v", ms, err)
104 }
105 due, err := s.DueMirrors(3600)
106 if err != nil || len(due) != 1 || due[0].Token != "mirror-token" {
107 t.Fatalf("DueMirrors = %+v, %v", due, err)
108 }
109 ds, err := s.PushDevices(uid)
110 if err != nil || len(ds) != 1 || ds[0].Token != "apns-token" {
111 t.Fatalf("PushDevices = %+v, %v", ds, err)
112 }
113
114 // An empty webhook secret or mirror token stays empty: it means none.
115 if _, err := s.AddWebhook(repoID, "https://hook.example/y", "", "*"); err != nil {
116 t.Fatal(err)
117 }
118 if _, err := s.AddMirror(repoID, "push", "https://mirror.example/s.git", "", ""); err != nil {
119 t.Fatal(err)
120 }
121 var empty int
122 s.DB.QueryRow("SELECT (SELECT COUNT(*) FROM webhooks WHERE secret = '') + (SELECT COUNT(*) FROM mirrors WHERE token = '')").Scan(&empty)
123 if empty != 2 {
124 t.Errorf("empty values stored as %d rows of '', want 2", empty)
125 }
126}
127
128// The queue readers open what they join.
129func TestSealedQueueReaders(t *testing.T) {
130 s, _, uid, repoID := keyedStore(t)
131 hook, err := s.AddWebhook(repoID, "https://hook.example/x", "hook-secret", "*")
132 if err != nil {
133 t.Fatal(err)
134 }
135 if _, err := s.DB.Exec("INSERT INTO events (repo_id, kind, data_json) VALUES (?, 'push', '{}')", repoID); err != nil {
136 t.Fatal(err)
137 }
138 if _, err := s.DB.Exec("INSERT INTO webhook_deliveries (webhook_id, event_id) SELECT ?, MAX(id) FROM events", hook); err != nil {
139 t.Fatal(err)
140 }
141 dd, err := s.DueDeliveries(10)
142 if err != nil || len(dd) != 1 || dd[0].Secret != "hook-secret" {
143 t.Fatalf("DueDeliveries = %+v, %v", dd, err)
144 }
145
146 if _, err := s.AddPushDevice(uid, "apns-token", "phone"); err != nil {
147 t.Fatal(err)
148 }
149 if err := s.EnqueuePush(uid, "t", "b", "/p"); err != nil {
150 t.Fatal(err)
151 }
152 qp, err := s.DuePush(10)
153 if err != nil || len(qp) != 1 || qp[0].Token != "apns-token" {
154 t.Fatalf("DuePush = %+v, %v", qp, err)
155 }
156}
157
158// A sealed value copied into another row of its column does not open:
159// the additional data names the row as well as the column.
160func TestSealedValueBoundToRow(t *testing.T) {
161 s, _, uid, repoID := keyedStore(t)
162 other, err := s.CreateRepo("user", uid, "other", "public")
163 if err != nil {
164 t.Fatal(err)
165 }
166 bob, err := s.CreateUser("bob", false)
167 if err != nil {
168 t.Fatal(err)
169 }
170 must := func(err error) {
171 t.Helper()
172 if err != nil {
173 t.Fatal(err)
174 }
175 }
176 must(s.SetBuildSecret(repoID, "A", "a"))
177 must(s.SetBuildSecret(repoID, "B", "b"))
178 must(s.SetBuildSecret(other, "A", "c"))
179 _, err = s.AddWebhook(repoID, "https://hook.example/1", "s1", "*")
180 must(err)
181 _, err = s.AddWebhook(repoID, "https://hook.example/2", "s2", "*")
182 must(err)
183 _, err = s.AddMirror(repoID, "push", "https://m.example/1.git", "", "t1")
184 must(err)
185 _, err = s.AddMirror(repoID, "pull", "https://m.example/2.git", "", "t2")
186 must(err)
187 _, err = s.AddPushDevice(uid, "d1", "")
188 must(err)
189 _, err = s.AddPushDevice(bob, "d2", "")
190 must(err)
191
192 // push_devices.token is unique, so alice's row goes before her
193 // sealed token is copied into bob's.
194 var aliceTok string
195 must(s.DB.QueryRow("SELECT token FROM push_devices WHERE user_id = ?", uid).Scan(&aliceTok))
196 _, err = s.DB.Exec("DELETE FROM push_devices WHERE user_id = ?", uid)
197 must(err)
198
199 cases := []struct {
200 name string
201 copy string
202 read func() error
203 }{
204 {"build secret to another name",
205 "UPDATE build_secrets SET value = (SELECT value FROM build_secrets WHERE repo_id = ?1 AND name = 'A') WHERE repo_id = ?1 AND name = 'B'",
206 func() error { _, err := s.BuildSecrets(repoID); return err }},
207 {"build secret to another repository",
208 "UPDATE build_secrets SET value = (SELECT value FROM build_secrets WHERE repo_id = ?1 AND name = 'A') WHERE repo_id = ?2 AND name = 'A'",
209 func() error { _, err := s.BuildSecrets(other); return err }},
210 {"webhook secret",
211 "UPDATE webhooks SET secret = (SELECT secret FROM webhooks WHERE url LIKE '%/1') WHERE url LIKE '%/2'",
212 func() error { _, err := s.ListWebhooks(repoID); return err }},
213 {"mirror token",
214 "UPDATE mirrors SET token = (SELECT token FROM mirrors WHERE direction = 'push') WHERE direction = 'pull'",
215 func() error { _, err := s.ListMirrors(repoID); return err }},
216 {"push token",
217 "UPDATE push_devices SET token = ?5 WHERE user_id = ?4",
218 func() error { _, err := s.PushDevices(bob); return err }},
219 }
220 for _, c := range cases {
221 if _, err := s.DB.Exec(c.copy, repoID, other, uid, bob, aliceTok); err != nil {
222 t.Fatalf("%s: %v", c.name, err)
223 }
224 if err := c.read(); err == nil {
225 t.Errorf("%s: a value copied from another row opened", c.name)
226 }
227 }
228}
229
230// A token re-registered under another account changes hands by its
231// hash, since two seals of one token differ.
232func TestPushDeviceUpsertBySealedToken(t *testing.T) {
233 s, _, uid, _ := keyedStore(t)
234 bob, err := s.CreateUser("bob", false)
235 if err != nil {
236 t.Fatal(err)
237 }
238 first, err := s.AddPushDevice(uid, "tok", "phone")
239 if err != nil {
240 t.Fatal(err)
241 }
242 second, err := s.AddPushDevice(bob, "tok", "ipad")
243 if err != nil {
244 t.Fatal(err)
245 }
246 if first != second {
247 t.Fatalf("re-registration made row %d beside %d", second, first)
248 }
249 d, err := s.PushDevices(bob)
250 if err != nil || len(d) != 1 || d[0].Token != "tok" {
251 t.Fatalf("PushDevices after handover = %+v, %v", d, err)
252 }
253 if err := s.DeletePushDeviceByToken("tok"); err != nil {
254 t.Fatal(err)
255 }
256 if d, _ := s.PushDevices(bob); len(d) != 0 {
257 t.Fatalf("device left after delete by token: %+v", d)
258 }
259}
260
261// A device row from before token_hash existed is found by its clear
262// token until ResealSecrets fills the hash.
263func TestPushDeviceUnhashedRow(t *testing.T) {
264 s, _, uid, _ := keyedStore(t)
265 if _, err := s.DB.Exec("INSERT INTO push_devices (user_id, token, label) VALUES (?, 'old', '')", uid); err != nil {
266 t.Fatal(err)
267 }
268 var first int64
269 s.DB.QueryRow("SELECT id FROM push_devices").Scan(&first)
270 id, err := s.AddPushDevice(uid, "old", "phone")
271 if err != nil || id != first {
272 t.Fatalf("AddPushDevice = %d, %v; want row %d", id, err, first)
273 }
274 if _, err := s.DB.Exec("INSERT INTO push_devices (user_id, token, label) VALUES (?, 'older', '')", uid); err != nil {
275 t.Fatal(err)
276 }
277 if err := s.DeletePushDeviceByToken("older"); err != nil {
278 t.Fatal(err)
279 }
280 if d, _ := s.PushDevices(uid); len(d) != 1 || d[0].Token != "old" {
281 t.Fatalf("PushDevices = %+v", d)
282 }
283}
284
285// Rows written before sealing existed, and rows under a retired key,
286// end up under the current key.
287func TestResealSecrets(t *testing.T) {
288 s, path, uid, repoID := keyedStore(t)
289 if _, err := s.DB.Exec("INSERT INTO build_secrets (repo_id, name, value) VALUES (?, 'OLD', 'clear-value')", repoID); err != nil {
290 t.Fatal(err)
291 }
292 if _, err := s.DB.Exec("INSERT INTO push_devices (user_id, token, label) VALUES (?, 'clear-token', '')", uid); err != nil {
293 t.Fatal(err)
294 }
295 n, err := s.ResealSecrets()
296 if err != nil || n != 2 {
297 t.Fatalf("ResealSecrets = %d, %v; want 2", n, err)
298 }
299 for _, v := range raw(t, s) {
300 if !seal.IsSealed(v) {
301 t.Errorf("still clear: %q", v)
302 }
303 }
304 var hash string
305 s.DB.QueryRow("SELECT COALESCE(token_hash, '') FROM push_devices").Scan(&hash)
306 if hash != tokenHash("clear-token") {
307 t.Errorf("token_hash = %q", hash)
308 }
309 if d, err := s.PushDevices(uid); err != nil || len(d) != 1 || d[0].Token != "clear-token" {
310 t.Fatalf("PushDevices after reseal = %+v, %v", d, err)
311 }
312 if n, _ := s.ResealSecrets(); n != 0 {
313 t.Errorf("second reseal rewrote %d values", n)
314 }
315
316 // Rotation: add a key, reseal, drop the old key; the value still opens.
317 old, err := seal.ReadKeys(path)
318 if err != nil {
319 t.Fatal(err)
320 }
321 next, _ := seal.NewKey()
322 if err := seal.WriteKeys(path, append(old, next)); err != nil {
323 t.Fatal(err)
324 }
325 if n, err := s.ResealSecrets(); err != nil || n != 2 {
326 t.Fatalf("reseal after rotation = %d, %v; want 2", n, err)
327 }
328 if err := seal.WriteKeys(path, []seal.Key{next}); err != nil {
329 t.Fatal(err)
330 }
331 use, err := s.SecretKeyUse()
332 if err != nil || use[next.ID] != 2 || len(use) != 1 {
333 t.Fatalf("SecretKeyUse = %v, %v", use, err)
334 }
335 if got, _ := s.BuildSecrets(repoID); got["OLD"] != "clear-value" {
336 t.Fatalf("value after rotation: %v", got)
337 }
338}
339
340func TestSealedValueWithoutKeyFails(t *testing.T) {
341 s, _, _, repoID := keyedStore(t)
342 if err := s.SetBuildSecret(repoID, "X", "v"); err != nil {
343 t.Fatal(err)
344 }
345 s.SetKeyring(nil)
346 if _, err := s.BuildSecrets(repoID); err == nil {
347 t.Fatal("opened a sealed value with no key loaded")
348 }
349}