Commit 911c19309b
Verified · cmc
Layout: unified · split
internal/httpd/lfs.go +30 −3
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | "time" | 9 | "time" |
| 10 | 10 | ||
| 11 | "gitbay.org/gitbay/internal/lfs" | 11 | "gitbay.org/gitbay/internal/lfs" |
| 12 | "gitbay.org/gitbay/internal/policy" | ||
| 12 | "gitbay.org/gitbay/internal/store" | 13 | "gitbay.org/gitbay/internal/store" |
| 13 | ) | 14 | ) |
| 14 | 15 | ||
| @@ -39,8 +40,9 @@ func (s *Server) lfsSecret() ([]byte, error) { | |||
| 39 | // "download", or "" for no access, and the key the grant rests on (0 | 40 | // "download", or "" for no access, and the key the grant rests on (0 |
| 40 | // for none). A token is bound to the SSH key that obtained it and | 41 | // 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 | // works only while that key is registered, unexpired and on an enabled |
| 42 | // account (#285). Without one, public repos allow anonymous download | 43 | // account, and while the key still has the access its operation needs |
| 43 | // only. | 44 | // on the repo (#285). Without one, public repos allow anonymous |
| 45 | // download only. | ||
| 44 | func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { | 46 | func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { |
| 45 | auth := r.Header.Get("Authorization") | 47 | auth := r.Header.Get("Authorization") |
| 46 | if tok, ok := strings.CutPrefix(auth, "Bearer "); ok { | 48 | if tok, ok := strings.CutPrefix(auth, "Bearer "); ok { |
| @@ -60,7 +62,7 @@ func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { | |||
| 60 | return "", 0 | 62 | return "", 0 |
| 61 | } | 63 | } |
| 62 | live, err := s.st.LiveSSHKeys([]int64{g.KeyID}) | 64 | live, err := s.st.LiveSSHKeys([]int64{g.KeyID}) |
| 63 | if err != nil || !live[g.KeyID] { | 65 | if err != nil || !live[g.KeyID] || !s.lfsKeyAllows(g.KeyID, repo, g.Op == "upload") { |
| 64 | return "", 0 | 66 | return "", 0 |
| 65 | } | 67 | } |
| 66 | return g.Op, g.KeyID | 68 | return g.Op, g.KeyID |
| @@ -71,6 +73,31 @@ func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { | |||
| 71 | return "", 0 | 73 | return "", 0 |
| 72 | } | 74 | } |
| 73 | 75 | ||
| 76 | // lfsKeyAllows repeats git-lfs-authenticate's access check for the key | ||
| 77 | // now: a deploy key by its binding, any other key by its account's | ||
| 78 | // access narrowed by the key's scope. | ||
| 79 | func (s *Server) lfsKeyAllows(keyID int64, repo store.Repo, write bool) bool { | ||
| 80 | key, err := s.st.SSHKeyByID(keyID) | ||
| 81 | if err != nil { | ||
| 82 | return false | ||
| 83 | } | ||
| 84 | if policy.IsDeployScope(key.Scope) { | ||
| 85 | return policy.DeployScopeAllows(key.Scope, repo.ID, write) | ||
| 86 | } | ||
| 87 | user, err := s.st.UserByID(key.UserID) | ||
| 88 | if err != nil { | ||
| 89 | return false | ||
| 90 | } | ||
| 91 | grant, err := s.st.AccessRole(repo.ID, user.ID) | ||
| 92 | if err != nil { | ||
| 93 | return false | ||
| 94 | } | ||
| 95 | if !policy.CanRead(user, repo, grant) || !policy.ScopeAllowsGit(key.Scope, repo.Path(), write) { | ||
| 96 | return false | ||
| 97 | } | ||
| 98 | return !write || policy.CanWrite(user, repo, grant) | ||
| 99 | } | ||
| 100 | |||
| 74 | func lfsError(w http.ResponseWriter, code int, msg string) { | 101 | func lfsError(w http.ResponseWriter, code int, msg string) { |
| 75 | w.Header().Set("Content-Type", lfsMediaType) | 102 | w.Header().Set("Content-Type", lfsMediaType) |
| 76 | w.WriteHeader(code) | 103 | w.WriteHeader(code) |
internal/httpd/lfsauth_test.go +71
| @@ -106,3 +106,74 @@ func TestLFSAnonymousTokenOnlyDownloadsPublic(t *testing.T) { | |||
| 106 | t.Errorf("private download: %q", op) | 106 | t.Errorf("private download: %q", op) |
| 107 | } | 107 | } |
| 108 | } | 108 | } |
| 109 | |||
| 110 | func lfsTestKey(t *testing.T, st *store.Store, uid int64, fp, scope string) int64 { | ||
| 111 | t.Helper() | ||
| 112 | if err := st.AddSSHKey(uid, fp, "ssh-ed25519", []byte(fp), scope, ""); err != nil { | ||
| 113 | t.Fatal(err) | ||
| 114 | } | ||
| 115 | k, err := st.SSHKeyByFingerprint(fp) | ||
| 116 | if err != nil { | ||
| 117 | t.Fatal(err) | ||
| 118 | } | ||
| 119 | return k.ID | ||
| 120 | } | ||
| 121 | |||
| 122 | // A token carries only the access its key's account still has: a | ||
| 123 | // collaborator removed from the repository, or a reader of a public | ||
| 124 | // repository made private, loses the token with the access (#285). | ||
| 125 | func TestLFSTokenNeedsCurrentAccess(t *testing.T) { | ||
| 126 | s, st, u := newTokenTestServer(t) | ||
| 127 | repo := lfsTestRepo(t, st, u.ID, "app", "private") | ||
| 128 | secret, err := s.lfsSecret() | ||
| 129 | if err != nil { | ||
| 130 | t.Fatal(err) | ||
| 131 | } | ||
| 132 | now := time.Now() | ||
| 133 | |||
| 134 | bob, err := st.CreateUser("bob", false) | ||
| 135 | if err != nil { | ||
| 136 | t.Fatal(err) | ||
| 137 | } | ||
| 138 | if err := st.GrantAccess(repo.ID, bob, "write"); err != nil { | ||
| 139 | t.Fatal(err) | ||
| 140 | } | ||
| 141 | bobKey := lfsTestKey(t, st, bob, "SHA256:bob", "full") | ||
| 142 | up := lfs.Sign(secret, repo.ID, bobKey, "upload", now) | ||
| 143 | if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "upload" { | ||
| 144 | t.Fatalf("collaborator upload: %q", op) | ||
| 145 | } | ||
| 146 | if err := st.GrantAccess(repo.ID, bob, "read"); err != nil { | ||
| 147 | t.Fatal(err) | ||
| 148 | } | ||
| 149 | if op, _ := s.lfsAuth(lfsRequest(up), repo); op != "" { | ||
| 150 | t.Errorf("upload after write was taken away: %q", op) | ||
| 151 | } | ||
| 152 | if err := st.RevokeAccess(repo.ID, bob); err != nil { | ||
| 153 | t.Fatal(err) | ||
| 154 | } | ||
| 155 | if op, _ := s.lfsAuth(lfsRequest(lfs.Sign(secret, repo.ID, bobKey, "download", now)), repo); op != "" { | ||
| 156 | t.Errorf("download after access was revoked: %q", op) | ||
| 157 | } | ||
| 158 | |||
| 159 | pub := lfsTestRepo(t, st, u.ID, "big", "public") | ||
| 160 | carol, err := st.CreateUser("carol", false) | ||
| 161 | if err != nil { | ||
| 162 | t.Fatal(err) | ||
| 163 | } | ||
| 164 | carolKey := lfsTestKey(t, st, carol, "SHA256:carol", "full") | ||
| 165 | down := lfs.Sign(secret, pub.ID, carolKey, "download", now) | ||
| 166 | if op, _ := s.lfsAuth(lfsRequest(down), pub); op != "download" { | ||
| 167 | t.Fatalf("public download: %q", op) | ||
| 168 | } | ||
| 169 | if err := st.SetRepoVisibility(pub.ID, "private"); err != nil { | ||
| 170 | t.Fatal(err) | ||
| 171 | } | ||
| 172 | pub, err = st.RepoByID(pub.ID) | ||
| 173 | if err != nil { | ||
| 174 | t.Fatal(err) | ||
| 175 | } | ||
| 176 | if op, _ := s.lfsAuth(lfsRequest(down), pub); op != "" { | ||
| 177 | t.Errorf("download after the repository went private: %q", op) | ||
| 178 | } | ||
| 179 | } | ||