internal/httpd/lfsauth_test.go
313 lines · 10523 bytes
1package httpd
2
3import (
4 "fmt"
5 "net/http"
6 "net/http/httptest"
7 "testing"
8 "time"
9
10 "gitbay.org/gitbay/internal/lfs"
11 "gitbay.org/gitbay/internal/store"
12)
13
14func lfsRequest(tok string) *http.Request {
15 r := httptest.NewRequest("GET", "/alice/app.git/info/lfs/objects/x", nil)
16 if tok != "" {
17 r.Header.Set("Authorization", "Bearer "+tok)
18 }
19 return r
20}
21
22func lfsTestRepo(t *testing.T, st *store.Store, uid int64, name, visibility string) store.Repo {
23 t.Helper()
24 id, err := st.CreateRepo("user", uid, name, visibility)
25 if err != nil {
26 t.Fatal(err)
27 }
28 repo, err := st.RepoByID(id)
29 if err != nil {
30 t.Fatal(err)
31 }
32 return repo
33}
34
35// A token works only while its key does: removed, expired or on a
36// disabled account, the key takes its tokens with it (#285).
37func TestLFSTokenNeedsALiveKey(t *testing.T) {
38 s, st, u := newTokenTestServer(t)
39 repo := lfsTestRepo(t, st, u.ID, "app", "private")
40 secret, err := s.lfsSecret()
41 if err != nil {
42 t.Fatal(err)
43 }
44 addKey := func(fp string, exp *time.Time) int64 {
45 t.Helper()
46 if err := st.AddSSHKeyFrom(u.ID, fp, "ssh-ed25519", []byte(fp), "full", "", store.KeyOrigin{ExpiresAt: exp}); err != nil {
47 t.Fatal(err)
48 }
49 k, err := st.SSHKeyByFingerprint(fp)
50 if err != nil {
51 t.Fatal(err)
52 }
53 return k.ID
54 }
55
56 live := addKey("SHA256:live", nil)
57 tok := lfs.Sign(secret, repo.ID, live, "SHA256:live", "upload", time.Now())
58 if op, key := s.lfsAuth(lfsRequest(tok), repo); op != "upload" || key != live {
59 t.Fatalf("live key: %q, %d", op, key)
60 }
61
62 past := time.Now().Add(-time.Minute)
63 expired := addKey("SHA256:expired", &past)
64 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, expired, "SHA256:expired", "upload", time.Now())), repo); op != "" {
65 t.Errorf("expired key: %q", op)
66 }
67
68 if err := st.RemoveSSHKey(u.ID, "SHA256:live"); err != nil {
69 t.Fatal(err)
70 }
71 if op, _ := s.lfsAuth(lfsRequest(tok), repo); op != "" {
72 t.Errorf("removed key: %q", op)
73 }
74
75 other := addKey("SHA256:other", nil)
76 otherTok := lfs.Sign(secret, repo.ID, other, "SHA256:other", "download", time.Now())
77 if op, _ := s.lfsAuth(lfsRequest(otherTok), repo); op != "download" {
78 t.Fatalf("second key before disable: %q", op)
79 }
80 if err := st.SetUserDisabled(u.ID, true); err != nil {
81 t.Fatal(err)
82 }
83 if op, _ := s.lfsAuth(lfsRequest(otherTok), repo); op != "" {
84 t.Errorf("disabled account: %q", op)
85 }
86}
87
88// A token with no key comes from an anonymous batch on a public
89// repository and is worth exactly what anonymous is: a download, while
90// the repository is public.
91func TestLFSAnonymousTokenOnlyDownloadsPublic(t *testing.T) {
92 s, st, u := newTokenTestServer(t)
93 pub := lfsTestRepo(t, st, u.ID, "big", "public")
94 priv := lfsTestRepo(t, st, u.ID, "vault", "private")
95 secret, err := s.lfsSecret()
96 if err != nil {
97 t.Fatal(err)
98 }
99 now := time.Now()
100 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, pub.ID, 0, "", "download", now)), pub); op != "download" {
101 t.Errorf("public download: %q", op)
102 }
103 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, pub.ID, 0, "", "upload", now)), pub); op != "" {
104 t.Errorf("anonymous upload: %q", op)
105 }
106 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, priv.ID, 0, "", "download", now)), priv); op != "" {
107 t.Errorf("private download: %q", op)
108 }
109}
110
111func lfsTestKey(t *testing.T, st *store.Store, uid int64, fp, scope string) int64 {
112 t.Helper()
113 if err := st.AddSSHKey(uid, fp, "ssh-ed25519", []byte(fp), scope, ""); err != nil {
114 t.Fatal(err)
115 }
116 k, err := st.SSHKeyByFingerprint(fp)
117 if err != nil {
118 t.Fatal(err)
119 }
120 return k.ID
121}
122
123// A token carries only the access its key's account still has: a
124// collaborator removed from the repository, or a reader of a public
125// repository made private, loses the token with the access (#285).
126func TestLFSTokenNeedsCurrentAccess(t *testing.T) {
127 s, st, u := newTokenTestServer(t)
128 repo := lfsTestRepo(t, st, u.ID, "app", "private")
129 secret, err := s.lfsSecret()
130 if err != nil {
131 t.Fatal(err)
132 }
133 now := time.Now()
134
135 bob, err := st.CreateUser("bob", false)
136 if err != nil {
137 t.Fatal(err)
138 }
139 if err := st.GrantAccess(repo.ID, bob, "write"); err != nil {
140 t.Fatal(err)
141 }
142 bobKey := lfsTestKey(t, st, bob, "SHA256:bob", "full")
143 up := lfs.Sign(secret, repo.ID, bobKey, "SHA256:bob", "upload", now)
144 if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "upload" {
145 t.Fatalf("collaborator upload: %q", op)
146 }
147 if err := st.GrantAccess(repo.ID, bob, "read"); err != nil {
148 t.Fatal(err)
149 }
150 if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "" {
151 t.Errorf("upload after write was taken away: %q", op)
152 }
153 if err := st.RevokeAccess(repo.ID, bob); err != nil {
154 t.Fatal(err)
155 }
156 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, bobKey, "SHA256:bob", "download", now)), repo); op != "" {
157 t.Errorf("download after access was revoked: %q", op)
158 }
159
160 pub := lfsTestRepo(t, st, u.ID, "big", "public")
161 carol, err := st.CreateUser("carol", false)
162 if err != nil {
163 t.Fatal(err)
164 }
165 carolKey := lfsTestKey(t, st, carol, "SHA256:carol", "full")
166 down := lfs.Sign(secret, pub.ID, carolKey, "SHA256:carol", "download", now)
167 if op, _ := s.lfsAuth(lfsRequest(down), pub); op != "download" {
168 t.Fatalf("public download: %q", op)
169 }
170 if err := st.SetRepoVisibility(pub.ID, "private"); err != nil {
171 t.Fatal(err)
172 }
173 pub, err = st.RepoByID(pub.ID)
174 if err != nil {
175 t.Fatal(err)
176 }
177 if op, _ := s.lfsAuth(lfsRequest(down), pub); op != "" {
178 t.Errorf("download after the repository went private: %q", op)
179 }
180}
181
182// A deploy key's token lasts as long as the deploy key: removed from the
183// repository or on a disabled account it is refused, and a read-only
184// binding never uploads.
185func TestLFSDeployKeyToken(t *testing.T) {
186 s, st, u := newTokenTestServer(t)
187 repo := lfsTestRepo(t, st, u.ID, "app", "private")
188 secret, err := s.lfsSecret()
189 if err != nil {
190 t.Fatal(err)
191 }
192 now := time.Now()
193 rw := fmt.Sprintf("deploy:%d:rw", repo.ID)
194 ro := fmt.Sprintf("deploy:%d:ro", repo.ID)
195
196 live := lfsTestKey(t, st, u.ID, "SHA256:deploy-live", rw)
197 if op, key := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, live, "SHA256:deploy-live", "upload", now)), repo); op != "upload" || key != live {
198 t.Fatalf("live deploy key: %q, %d", op, key)
199 }
200
201 removed := lfsTestKey(t, st, u.ID, "SHA256:deploy-removed", rw)
202 tok := lfs.Sign(secret, repo.ID, removed, "SHA256:deploy-removed", "download", now)
203 if err := st.RemoveDeployKey(repo.ID, "SHA256:deploy-removed"); err != nil {
204 t.Fatal(err)
205 }
206 if op, _ := s.lfsAuth(lfsRequest(tok), repo); op != "" {
207 t.Errorf("removed deploy key: %q", op)
208 }
209
210 readOnly := lfsTestKey(t, st, u.ID, "SHA256:deploy-ro", ro)
211 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, readOnly, "SHA256:deploy-ro", "download", now)), repo); op != "download" {
212 t.Errorf("read-only deploy key download: %q", op)
213 }
214 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, readOnly, "SHA256:deploy-ro", "upload", now)), repo); op != "" {
215 t.Errorf("read-only deploy key upload: %q", op)
216 }
217
218 if err := st.SetUserDisabled(u.ID, true); err != nil {
219 t.Fatal(err)
220 }
221 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, live, "SHA256:deploy-live", "download", now)), repo); op != "" {
222 t.Errorf("deploy key of a disabled account: %q", op)
223 }
224}
225
226// An upload token minted before the repository was archived uploads
227// nothing after it, as git-lfs-authenticate would refuse to mint one.
228func TestLFSUploadTokenRefusedOnceArchived(t *testing.T) {
229 s, st, u := newTokenTestServer(t)
230 repo := lfsTestRepo(t, st, u.ID, "app", "private")
231 secret, err := s.lfsSecret()
232 if err != nil {
233 t.Fatal(err)
234 }
235 key := lfsTestKey(t, st, u.ID, "SHA256:owner", "full")
236 now := time.Now()
237 up := lfs.Sign(secret, repo.ID, key, "SHA256:owner", "upload", now)
238 if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "upload" {
239 t.Fatalf("upload before archiving: %q", op)
240 }
241 if _, err := st.UpdateRepoSettings(repo.ID, func(rs *store.RepoSettings) { rs.Archived = true }); err != nil {
242 t.Fatal(err)
243 }
244 repo, err = st.RepoByID(repo.ID)
245 if err != nil {
246 t.Fatal(err)
247 }
248 if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "" {
249 t.Errorf("upload after archiving: %q", op)
250 }
251 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, key, "SHA256:owner", "download", now)), repo); op != "download" {
252 t.Errorf("download after archiving: %q", op)
253 }
254}
255
256// A removed key's id is not handed to the next key (#306), so its token
257// names no live key. An id freed and taken before ids stopped being
258// reused is still refused by the fingerprint pin; the new key's own token
259// works (#303).
260func TestLFSTokenRefusedOnReusedKeyID(t *testing.T) {
261 s, st, u := newTokenTestServer(t)
262 repo := lfsTestRepo(t, st, u.ID, "app", "private")
263 secret, err := s.lfsSecret()
264 if err != nil {
265 t.Fatal(err)
266 }
267 now := time.Now()
268 oldID := lfsTestKey(t, st, u.ID, "SHA256:old", "full")
269 old := lfs.Sign(secret, repo.ID, oldID, "SHA256:old", "upload", now)
270 if err := st.RemoveSSHKey(u.ID, "SHA256:old"); err != nil {
271 t.Fatal(err)
272 }
273 nextID := lfsTestKey(t, st, u.ID, "SHA256:next", "full")
274 if nextID == oldID {
275 t.Fatalf("new key took the removed key's id %d", oldID)
276 }
277 if op, _ := s.lfsAuth(lfsRequest(old), repo); op != "" {
278 t.Errorf("removed key's token: %q", op)
279 }
280 if _, err := st.DB.Exec("INSERT INTO ssh_keys (id, user_id, fingerprint, algo, blob) VALUES (?, ?, 'SHA256:new', 'ssh-ed25519', x'00')",
281 oldID, u.ID); err != nil {
282 t.Fatal(err)
283 }
284 if op, _ := s.lfsAuth(lfsRequest(old), repo); op != "" {
285 t.Errorf("old key's token on the reused id: %q", op)
286 }
287 if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, oldID, "SHA256:new", "upload", now)), repo); op != "upload" {
288 t.Errorf("new key's own token: %q", op)
289 }
290}
291
292// A token names its repository by id. Deleting the repository does not
293// let the next one take that id and the token with it (#306).
294func TestLFSTokenForDeletedRepository(t *testing.T) {
295 s, st, u := newTokenTestServer(t)
296 gone := lfsTestRepo(t, st, u.ID, "gone", "private")
297 secret, err := s.lfsSecret()
298 if err != nil {
299 t.Fatal(err)
300 }
301 key := lfsTestKey(t, st, u.ID, "SHA256:owner", "full")
302 tok := lfs.Sign(secret, gone.ID, key, "SHA256:owner", "upload", time.Now())
303 if err := st.DeleteRepo(gone.ID); err != nil {
304 t.Fatal(err)
305 }
306 next := lfsTestRepo(t, st, u.ID, "next", "private")
307 if next.ID == gone.ID {
308 t.Fatalf("new repository took the deleted one's id %d", gone.ID)
309 }
310 if op, _ := s.lfsAuth(lfsRequest(tok), next); op != "" {
311 t.Errorf("deleted repository's token on the next repository: %q", op)
312 }
313}