cmd/gitbayd/secrets_test.go
224 lines · 6464 bytes
8 symbols in this file
1package main
2
3import (
4 "bytes"
5 "encoding/base64"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 "gitbay.org/gitbay/internal/config"
13 "gitbay.org/gitbay/internal/seal"
14 "gitbay.org/gitbay/internal/store"
15)
16
17func TestOpenStoreRefusesWithoutKeyFile(t *testing.T) {
18 cfg := testConfig(t)
19 cfg.Server.SecretKeyFile = filepath.Join(t.TempDir(), "absent.key")
20 _, err := openStore(cfg)
21 if err == nil || !strings.Contains(err.Error(), "gitbayd admin secrets init") || !strings.Contains(err.Error(), cfg.Server.SecretKeyFile) {
22 t.Fatalf("openStore without a key file: %v", err)
23 }
24}
25
26// storeWithSecret opens cfg's store and stores one build secret.
27func storeWithSecret(t *testing.T, cfg config.Config) (*store.Store, int64) {
28 t.Helper()
29 st, err := openStore(cfg)
30 if err != nil {
31 t.Fatal(err)
32 }
33 uid, err := st.CreateUser("alice", false)
34 if err != nil {
35 t.Fatal(err)
36 }
37 repoID, err := st.CreateRepo("user", uid, "app", "public")
38 if err != nil {
39 t.Fatal(err)
40 }
41 if err := st.SetBuildSecret(repoID, "TOKEN", "v1"); err != nil {
42 t.Fatal(err)
43 }
44 return st, repoID
45}
46
47func TestRotateSecrets(t *testing.T) {
48 cfg := testConfig(t)
49 before, err := seal.ReadKeys(cfg.Server.SecretKeyFile)
50 if err != nil {
51 t.Fatal(err)
52 }
53 st, repoID := storeWithSecret(t, cfg)
54 st.Close()
55
56 var out bytes.Buffer
57 if err := rotateSecrets(cfg, &out); err != nil {
58 t.Fatalf("rotate: %v", err)
59 }
60 after, err := seal.ReadKeys(cfg.Server.SecretKeyFile)
61 if err != nil {
62 t.Fatal(err)
63 }
64 if len(after) != 1 || after[0].ID == before[0].ID {
65 t.Fatalf("key file after rotation holds %v, before %v", after, before)
66 }
67 assertNoKeyMaterial(t, out.String(), append(before, after...))
68 st, err = openStore(cfg)
69 if err != nil {
70 t.Fatal(err)
71 }
72 defer st.Close()
73 use, err := st.SecretKeyUse()
74 if err != nil || use[after[0].ID] != 1 || len(use) != 1 {
75 t.Fatalf("SecretKeyUse after rotation = %v, %v", use, err)
76 }
77 if got, _ := st.BuildSecrets(repoID); got["TOKEN"] != "v1" {
78 t.Fatalf("value after rotation: %v", got)
79 }
80}
81
82// A reseal that fails leaves the old keys in the file, so every value
83// still opens.
84func TestRotateSecretsKeepsOldKeysWhenResealFails(t *testing.T) {
85 cfg := testConfig(t)
86 before, err := seal.ReadKeys(cfg.Server.SecretKeyFile)
87 if err != nil {
88 t.Fatal(err)
89 }
90 st, repoID := storeWithSecret(t, cfg)
91 // A second value sealed under a key the file does not hold.
92 if _, err := st.DB.Exec("INSERT INTO build_secrets (repo_id, name, value) VALUES (?, 'BAD', 'gbs1:deadbeef:AAAA')", repoID); err != nil {
93 t.Fatal(err)
94 }
95 st.Close()
96
97 if err := rotateSecrets(cfg, &bytes.Buffer{}); err == nil {
98 t.Fatal("rotate succeeded over a value that does not open")
99 }
100 after, err := seal.ReadKeys(cfg.Server.SecretKeyFile)
101 if err != nil {
102 t.Fatal(err)
103 }
104 if len(after) != 2 || after[0].ID != before[0].ID {
105 t.Fatalf("key file after a failed rotation holds %v", after)
106 }
107}
108
109func TestInitSecrets(t *testing.T) {
110 cfg := testConfig(t)
111 cfg.Server.SecretKeyFile = filepath.Join(t.TempDir(), "secret.key")
112 var out bytes.Buffer
113 if err := initSecrets(cfg, &out); err != nil {
114 t.Fatal(err)
115 }
116 fi, err := os.Stat(cfg.Server.SecretKeyFile)
117 if err != nil {
118 t.Fatal(err)
119 }
120 if fi.Mode().Perm() != 0o600 {
121 t.Fatalf("mode %04o", fi.Mode().Perm())
122 }
123 keys, err := seal.ReadKeys(cfg.Server.SecretKeyFile)
124 if err != nil || len(keys) != 1 {
125 t.Fatalf("keys %v, %v", keys, err)
126 }
127 if !strings.Contains(out.String(), keys[0].ID) {
128 t.Fatalf("output does not name the key id: %q", out.String())
129 }
130 assertNoKeyMaterial(t, out.String(), keys)
131 if err := initSecrets(cfg, &out); err == nil || !strings.Contains(err.Error(), "already exists") {
132 t.Fatalf("second init: %v", err)
133 }
134 if again, _ := seal.ReadKeys(cfg.Server.SecretKeyFile); again[0].ID != keys[0].ID {
135 t.Fatal("second init replaced the key")
136 }
137}
138
139func TestCheckSecrets(t *testing.T) {
140 cfg := testConfig(t)
141 keys, err := seal.ReadKeys(cfg.Server.SecretKeyFile)
142 if err != nil {
143 t.Fatal(err)
144 }
145 st, repoID := storeWithSecret(t, cfg)
146
147 var out bytes.Buffer
148 if err := checkSecrets(cfg, &out); err != nil {
149 t.Fatalf("check: %v\n%s", err, out.String())
150 }
151 if !strings.Contains(out.String(), "build_secrets.value: key "+keys[0].ID+" 1") {
152 t.Fatalf("check output:\n%s", out.String())
153 }
154 assertNoKeyMaterial(t, out.String(), keys)
155
156 if _, err := st.DB.Exec("INSERT INTO build_secrets (repo_id, name, value) VALUES (?, 'BAD', 'gbs1:deadbeef:AAAA')", repoID); err != nil {
157 t.Fatal(err)
158 }
159 st.Close()
160 out.Reset()
161 err = checkSecrets(cfg, &out)
162 if err == nil || !strings.Contains(err.Error(), "does not open 1 stored value") {
163 t.Fatalf("check over a value that does not open: %v", err)
164 }
165 if !strings.Contains(out.String(), "deadbeef") || !strings.Contains(out.String(), "build_secrets.value row") {
166 t.Fatalf("check output does not name the failing row:\n%s", out.String())
167 }
168}
169
170func assertNoKeyMaterial(t *testing.T, out string, keys []seal.Key) {
171 t.Helper()
172 for _, k := range keys {
173 if strings.Contains(out, base64.StdEncoding.EncodeToString(k.Secret)) {
174 t.Fatalf("output carries key %s's secret", k.ID)
175 }
176 }
177}
178
179// A rotate waits while another holds the key file's lock, and two run
180// at once both finish with every value still opening.
181func TestRotateSecretsSerializes(t *testing.T) {
182 cfg := testConfig(t)
183 st, repoID := storeWithSecret(t, cfg)
184 st.Close()
185
186 unlock, err := lockKeyFile(cfg.Server.SecretKeyFile)
187 if err != nil {
188 t.Fatal(err)
189 }
190 done := make(chan error, 2)
191 go func() { done <- rotateSecrets(cfg, &bytes.Buffer{}) }()
192 go func() { done <- rotateSecrets(cfg, &bytes.Buffer{}) }()
193 select {
194 case err := <-done:
195 t.Fatalf("rotate finished while the lock was held: %v", err)
196 case <-time.After(200 * time.Millisecond):
197 }
198 unlock()
199 for range 2 {
200 select {
201 case err := <-done:
202 if err != nil {
203 t.Fatalf("rotate: %v", err)
204 }
205 case <-time.After(30 * time.Second):
206 t.Fatal("rotate never finished")
207 }
208 }
209 keys, err := seal.ReadKeys(cfg.Server.SecretKeyFile)
210 if err != nil || len(keys) != 1 {
211 t.Fatalf("key file after two rotations: %v, %v", keys, err)
212 }
213 st, err = openStore(cfg)
214 if err != nil {
215 t.Fatal(err)
216 }
217 defer st.Close()
218 if got, err := st.BuildSecrets(repoID); err != nil || got["TOKEN"] != "v1" {
219 t.Fatalf("value after two rotations: %v, %v", got, err)
220 }
221 if fi, err := os.Lstat(cfg.Server.SecretKeyFile + ".lock"); err != nil || fi.Mode().Perm() != 0o600 {
222 t.Fatalf("lock file: %v, %v", fi, err)
223 }
224}