internal/httpd/lfsauth_test.go

e6cd75b5f28bacf51620bb531320c30fd4e66bfd
gitbay/internal/httpd/lfsauth_test.go history · blame · raw

313 lines · 10523 bytes

10 symbols in this file
  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}