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