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 | // lfsAuth resolves what the request may do to the repo: "upload", | 38 | // lfsAuth resolves what the request may do to the repo: "upload", |
| 39 | // "download", or "" for no access, and the key the grant rests on (0 | 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 | 40 | // for none). A token is bound to the SSH key that obtained it and |
| 41 | // anonymous download only. | 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 | func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { | 44 | func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { |
| 43 | auth := r.Header.Get("Authorization") | 45 | auth := r.Header.Get("Authorization") |
| 44 | if tok, ok := strings.CutPrefix(auth, "Bearer "); ok { | 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 | if !ok || g.RepoID != repo.ID { | 52 | if !ok || g.RepoID != repo.ID { |
| 51 | return "", 0 | 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 | return g.Op, g.KeyID | 66 | return g.Op, g.KeyID |
| 54 | } | 67 | } |
| 55 | if repo.Visibility == "public" { | 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 | } | ||