cmd/gitbayd/secrets_test.go

e6cd75b5f28bacf51620bb531320c30fd4e66bfd
gitbay/cmd/gitbayd/secrets_test.go history · blame · raw

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}