Commit 911c19309b
Verified · cmc
Layout: unified · split
internal/httpd/lfs.go +30 −3
| @@ -9,6 +9,7 @@ import ( | ||
| 9 | 9 | "time" |
| 10 | 10 | |
| 11 | 11 | "gitbay.org/gitbay/internal/lfs" |
| 12 | "gitbay.org/gitbay/internal/policy" | |
| 12 | 13 | "gitbay.org/gitbay/internal/store" |
| 13 | 14 | ) |
| 14 | 15 | |
| @@ -39,8 +40,9 @@ func (s *Server) lfsSecret() ([]byte, error) { | ||
| 39 | 40 | // "download", or "" for no access, and the key the grant rests on (0 |
| 40 | 41 | // for none). A token is bound to the SSH key that obtained it and |
| 41 | 42 | // works only while that key is registered, unexpired and on an enabled |
| 42 | // account (#285). Without one, public repos allow anonymous download | |
| 43 | // only. | |
| 43 | // account, and while the key still has the access its operation needs | |
| 44 | // on the repo (#285). Without one, public repos allow anonymous | |
| 45 | // download only. | |
| 44 | 46 | func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { |
| 45 | 47 | auth := r.Header.Get("Authorization") |
| 46 | 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 | 62 | return "", 0 |
| 61 | 63 | } |
| 62 | 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 | 66 | return "", 0 |
| 65 | 67 | } |
| 66 | 68 | return g.Op, g.KeyID |
| @@ -71,6 +73,31 @@ func (s *Server) lfsAuth(r *http.Request, repo store.Repo) (string, int64) { | ||
| 71 | 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 | 101 | func lfsError(w http.ResponseWriter, code int, msg string) { |
| 75 | 102 | w.Header().Set("Content-Type", lfsMediaType) |
| 76 | 103 | w.WriteHeader(code) |
internal/httpd/lfsauth_test.go +71
| @@ -106,3 +106,74 @@ func TestLFSAnonymousTokenOnlyDownloadsPublic(t *testing.T) { | ||
| 106 | 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 | } | |