Commit 70d7e17c28
Verified · cmc
Layout: unified · split
internal/httpd/lfs.go +15 −2
| @@ -37,8 +37,10 @@ func (s *Server) lfsSecret() ([]byte, error) { | ||
| 37 | 37 | |
| 38 | 38 | // lfsAuth resolves what the request may do to the repo: "upload", |
| 39 | 39 | // "download", or "" for no access, and the key the grant rests on (0 |
| 40 | // for none). Tokens are repo-scoped; without one, public repos allow | |
| 41 | // anonymous download only. | |
| 40 | // for none). A token is bound to the SSH key that obtained it and | |
| 41 | // works only while that key is registered, unexpired and on an enabled | |
| 42 | // account (#285). Without one, public repos allow anonymous download | |
| 43 | // only. | |
| 42 | 44 | func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { |
| 43 | 45 | auth := r.Header.Get("Authorization") |
| 44 | 46 | if tok, ok := strings.CutPrefix(auth, "Bearer "); ok { |
| @@ -50,6 +52,17 @@ func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { | ||
| 50 | 52 | if !ok || g.RepoID != repo.ID { |
| 51 | 53 | return "", 0 |
| 52 | 54 | } |
| 55 | if g.KeyID == 0 { | |
| 56 | // Minted by an anonymous batch: worth what anonymous is. | |
| 57 | if g.Op == "download" && repo.Visibility == "public" { | |
| 58 | return "download", 0 | |
| 59 | } | |
| 60 | return "", 0 | |
| 61 | } | |
| 62 | live, err := s.st.LiveSSHKeys([]int64{g.KeyID}) | |
| 63 | if err != nil || !live[g.KeyID] { | |
| 64 | return "", 0 | |
| 65 | } | |
| 53 | 66 | return g.Op, g.KeyID |
| 54 | 67 | } |
| 55 | 68 | if repo.Visibility == "public" { |
internal/httpd/lfsauth_test.go added +108
| @@ -0,0 +1,108 @@ | ||
| 1 | package httpd | |
| 2 | ||
| 3 | import ( | |
| 4 | "net/http" | |
| 5 | "net/http/httptest" | |
| 6 | "testing" | |
| 7 | "time" | |
| 8 | ||
| 9 | "gitbay.org/gitbay/internal/lfs" | |
| 10 | "gitbay.org/gitbay/internal/store" | |
| 11 | ) | |
| 12 | ||
| 13 | func lfsRequest(tok string) *http.Request { | |
| 14 | r := httptest.NewRequest("GET", "/alice/app.git/info/lfs/objects/x", nil) | |
| 15 | if tok != "" { | |
| 16 | r.Header.Set("Authorization", "Bearer "+tok) | |
| 17 | } | |
| 18 | return r | |
| 19 | } | |
| 20 | ||
| 21 | func lfsTestRepo(t *testing.T, st *store.Store, uid int64, name, visibility string) store.Repo { | |
| 22 | t.Helper() | |
| 23 | id, err := st.CreateRepo("user", uid, name, visibility) | |
| 24 | if err != nil { | |
| 25 | t.Fatal(err) | |
| 26 | } | |
| 27 | repo, err := st.RepoByID(id) | |
| 28 | if err != nil { | |
| 29 | t.Fatal(err) | |
| 30 | } | |
| 31 | return repo | |
| 32 | } | |
| 33 | ||
| 34 | // A token works only while its key does: removed, expired or on a | |
| 35 | // disabled account, the key takes its tokens with it (#285). | |
| 36 | func TestLFSTokenNeedsALiveKey(t *testing.T) { | |
| 37 | s, st, u := newTokenTestServer(t) | |
| 38 | repo := lfsTestRepo(t, st, u.ID, "app", "private") | |
| 39 | secret, err := s.lfsSecret() | |
| 40 | if err != nil { | |
| 41 | t.Fatal(err) | |
| 42 | } | |
| 43 | addKey := func(fp string, exp *time.Time) int64 { | |
| 44 | t.Helper() | |
| 45 | if err := st.AddSSHKeyFrom(u.ID, fp, "ssh-ed25519", []byte(fp), "full", "", store.KeyOrigin{ExpiresAt: exp}); err != nil { | |
| 46 | t.Fatal(err) | |
| 47 | } | |
| 48 | k, err := st.SSHKeyByFingerprint(fp) | |
| 49 | if err != nil { | |
| 50 | t.Fatal(err) | |
| 51 | } | |
| 52 | return k.ID | |
| 53 | } | |
| 54 | ||
| 55 | live := addKey("SHA256:live", nil) | |
| 56 | tok := lfs.Sign(secret, repo.ID, live, "upload", time.Now()) | |
| 57 | if op, key := s.lfsAuth(lfsRequest(tok), repo); op != "upload" || key != live { | |
| 58 | t.Fatalf("live key: %q, %d", op, key) | |
| 59 | } | |
| 60 | ||
| 61 | past := time.Now().Add(-time.Minute) | |
| 62 | expired := addKey("SHA256:expired", &past) | |
| 63 | if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, expired, "upload", time.Now())), repo); op != "" { | |
| 64 | t.Errorf("expired key: %q", op) | |
| 65 | } | |
| 66 | ||
| 67 | if err := st.RemoveSSHKey(u.ID, "SHA256:live"); err != nil { | |
| 68 | t.Fatal(err) | |
| 69 | } | |
| 70 | if op, _ := s.lfsAuth(lfsRequest(tok), repo); op != "" { | |
| 71 | t.Errorf("removed key: %q", op) | |
| 72 | } | |
| 73 | ||
| 74 | other := addKey("SHA256:other", nil) | |
| 75 | otherTok := lfs.Sign(secret, repo.ID, other, "download", time.Now()) | |
| 76 | if op, _ := s.lfsAuth(lfsRequest(otherTok), repo); op != "download" { | |
| 77 | t.Fatalf("second key before disable: %q", op) | |
| 78 | } | |
| 79 | if err := st.SetUserDisabled(u.ID, true); err != nil { | |
| 80 | t.Fatal(err) | |
| 81 | } | |
| 82 | if op, _ := s.lfsAuth(lfsRequest(otherTok), repo); op != "" { | |
| 83 | t.Errorf("disabled account: %q", op) | |
| 84 | } | |
| 85 | } | |
| 86 | ||
| 87 | // A token with no key comes from an anonymous batch on a public | |
| 88 | // repository and is worth exactly what anonymous is: a download, while | |
| 89 | // the repository is public. | |
| 90 | func TestLFSAnonymousTokenOnlyDownloadsPublic(t *testing.T) { | |
| 91 | s, st, u := newTokenTestServer(t) | |
| 92 | pub := lfsTestRepo(t, st, u.ID, "big", "public") | |
| 93 | priv := lfsTestRepo(t, st, u.ID, "vault", "private") | |
| 94 | secret, err := s.lfsSecret() | |
| 95 | if err != nil { | |
| 96 | t.Fatal(err) | |
| 97 | } | |
| 98 | now := time.Now() | |
| 99 | if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, pub.ID, 0, "download", now)), pub); op != "download" { | |
| 100 | t.Errorf("public download: %q", op) | |
| 101 | } | |
| 102 | if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, pub.ID, 0, "upload", now)), pub); op != "" { | |
| 103 | t.Errorf("anonymous upload: %q", op) | |
| 104 | } | |
| 105 | if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, priv.ID, 0, "download", now)), priv); op != "" { | |
| 106 | t.Errorf("private download: %q", op) | |
| 107 | } | |
| 108 | } | |