74e41543304d1e12fd54fc59e410bd428373dd34
- Author
- Kieran Klukas <kieran@dunkirk.sh>
- Committer
- Kieran Klukas <kieran@dunkirk.sh>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/pkg/backend/user.go b/pkg/backend/user.go
2index aa93aeb10177eb4500c6f8a9bbe3dd3e3ef7814a..11e24058eb61c2597189bd3757ec0d284ce03536 100644
3--- a/pkg/backend/user.go
4+++ b/pkg/backend/user.go
5@@ -56,10 +56,7 @@ func (d *Backend) AccessLevelForUser(ctx context.Context, repo string, user prot
6 }
7
8 // If the repository exists, check if the user is a collaborator.
9- r := proto.RepositoryFromContext(ctx)
10- if r == nil {
11- r, _ = d.Repository(ctx, repo)
12- }
13+ r, _ := d.Repository(ctx, repo)
14
15 if r != nil {
16 if user != nil {
17@@ -70,9 +67,7 @@ func (d *Backend) AccessLevelForUser(ctx context.Context, repo string, user prot
18 }
19
20 // If the user is a collaborator, they have return their access level.
21- // On a private repository the collaborator's level is authoritative;
22- // the anonymous floor only applies to public repositories, otherwise
23- // a no-access collaborator would be granted read access.
24+ // The anonymous floor only applies to public repositories.
25 collabAccess, isCollab, _ := d.IsCollaborator(ctx, repo, username)
26 if isCollab {
27 if !r.IsPrivate() && anon > collabAccess {
28diff --git a/pkg/web/git.go b/pkg/web/git.go
29index 16c967ffab9ff0fe83d93e1346f3c8abf8a11072..5807fefa993aac2f7edd6b1f83044dbec88f86cc 100644
30--- a/pkg/web/git.go
31+++ b/pkg/web/git.go
32@@ -113,7 +113,7 @@ func GitController(_ context.Context, r *mux.Router) {
33 }
34
35 // Handle go-get
36- r.Handle(basePrefix, withParams(withAccess(http.HandlerFunc(GoGetHandler)))).Methods(http.MethodGet)
37+ r.Handle(basePrefix, withParams(withCaller(http.HandlerFunc(GoGetHandler)))).Methods(http.MethodGet)
38 }
39
40 var gitRoutes = []GitRoute{
41@@ -196,46 +196,85 @@ func askCredentials(w http.ResponseWriter, _ *http.Request) {
42 w.Header().Set("LFS-Authenticate", `Basic realm="Git LFS" charset="UTF-8", Token, Bearer`)
43 }
44
45-// withAccess handles auth.
46-func withAccess(next http.Handler) http.HandlerFunc {
47- return func(w http.ResponseWriter, r *http.Request) {
48- ctx := r.Context()
49- cfg := config.FromContext(ctx)
50- logger := log.FromContext(ctx)
51- be := backend.FromContext(ctx)
52+// identify stores the requested repository and the authenticated caller in
53+// the request context, and returns the authentication error, if any.
54+func identify(r *http.Request) (*http.Request, proto.Repository, error) {
55+ ctx := r.Context()
56+ logger := log.FromContext(ctx)
57+ be := backend.FromContext(ctx)
58
59- // Store repository in context
60- // We're not checking for errors here because we want to allow
61- // repo creation on the fly.
62- repoName := mux.Vars(r)["repo"]
63- repo, _ := be.Repository(ctx, repoName)
64- ctx = proto.WithRepositoryContext(ctx, repo)
65- r = r.WithContext(ctx)
66+ // A missing repository is not an error here; pushes create it.
67+ repo, _ := be.Repository(ctx, mux.Vars(r)["repo"])
68+ ctx = proto.WithRepositoryContext(ctx, repo)
69+ r = r.WithContext(ctx)
70
71- user, err := authenticate(r)
72- if err != nil {
73- switch {
74- case errors.Is(err, ErrInvalidToken):
75- case errors.Is(err, proto.ErrUserNotFound):
76- default:
77- logger.Error("failed to authenticate", "err", err)
78- }
79+ user, err := authenticate(r)
80+ if err != nil {
81+ switch {
82+ case errors.Is(err, ErrInvalidToken):
83+ case errors.Is(err, proto.ErrUserNotFound):
84+ default:
85+ logger.Error("failed to authenticate", "err", err)
86 }
87+ }
88+
89+ if user != nil {
90+ logger.Debug("authenticated", "username", user.Username())
91+ }
92
93- if user == nil && !be.AllowKeyless(ctx) {
94- askCredentials(w, r)
95- renderUnauthorized(w, r)
96+ return r.WithContext(proto.WithUserContext(ctx, user)), repo, err
97+}
98+
99+// badCredentials reports whether the caller sent credentials that failed.
100+func badCredentials(err error) bool {
101+ return errors.Is(err, ErrInvalidToken) || errors.Is(err, ErrInvalidPassword)
102+}
103+
104+// requireCaller rejects anonymous requests when keyless access is disabled.
105+func requireCaller(w http.ResponseWriter, r *http.Request) bool {
106+ ctx := r.Context()
107+ if proto.UserFromContext(ctx) == nil && !backend.FromContext(ctx).AllowKeyless(ctx) {
108+ askCredentials(w, r)
109+ renderUnauthorized(w, r)
110+ return false
111+ }
112+ return true
113+}
114+
115+// withCaller authenticates the caller without authorizing a repository.
116+// Handlers behind it must check access to any repository they reveal.
117+func withCaller(next http.Handler) http.HandlerFunc {
118+ return func(w http.ResponseWriter, r *http.Request) {
119+ r, _, err := identify(r)
120+ if !requireCaller(w, r) {
121 return
122 }
123
124- // Store user in context
125- ctx = proto.WithUserContext(ctx, user)
126- r = r.WithContext(ctx)
127+ if badCredentials(err) {
128+ renderForbidden(w, r)
129+ return
130+ }
131
132diff --git a/pkg/web/git_lfs.go b/pkg/web/git_lfs.go
133index 6d7dfe863d255e001d533d188076de4799e0d131..c9b8ba1779f115e51070fe611c1eb5eaa81d8f0c 100644
134--- a/pkg/web/git_lfs.go
135+++ b/pkg/web/git_lfs.go
136@@ -259,6 +259,13 @@ func serviceLfsBasicDownload(w http.ResponseWriter, r *http.Request) {
137 ctx := r.Context()
138 oid := mux.Vars(r)["oid"]
139 repo := proto.RepositoryFromContext(ctx)
140+ if repo == nil {
141+ renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
142+ Message: "repository not found",
143+ })
144+ return
145+ }
146+
147 cfg := config.FromContext(ctx)
148 logger := log.FromContext(ctx).WithPrefix("http.lfs-basic")
149 datastore := store.FromContext(ctx)
150@@ -325,6 +332,13 @@ func serviceLfsBasicUpload(w http.ResponseWriter, r *http.Request) {
151 datastore := store.FromContext(ctx)
152 logger := log.FromContext(ctx).WithPrefix("http.lfs-basic")
153 repo := proto.RepositoryFromContext(ctx)
154+ if repo == nil {
155+ renderJSON(w, http.StatusNotFound, lfs.ErrorResponse{
156+ Message: "repository not found",
157+ })
158+ return
159+ }
160+
161 repoID := strconv.FormatInt(repo.ID(), 10)
162 strg := storage.NewLocalStorage(filepath.Join(cfg.DataPath, "lfs", repoID))
163 name := mux.Vars(r)["repo"]
164diff --git a/pkg/web/goget.go b/pkg/web/goget.go
165index fa486d5b3d8919fd934093e71f2c1a59ff555b3a..219c54f8cca664e70c3af79df46e4b698aa1bf15 100644
166--- a/pkg/web/goget.go
167+++ b/pkg/web/goget.go
168@@ -7,8 +7,10 @@ import (
169 "text/template"
170
171 "charm.land/log/v2"
172+ "github.com/charmbracelet/soft-serve/pkg/access"
173 "github.com/charmbracelet/soft-serve/pkg/backend"
174 "github.com/charmbracelet/soft-serve/pkg/config"
175+ "github.com/charmbracelet/soft-serve/pkg/proto"
176 "github.com/charmbracelet/soft-serve/pkg/utils"
177 "github.com/gorilla/mux"
178 "github.com/prometheus/client_golang/prometheus"
179@@ -45,9 +47,8 @@ func GoGetHandler(w http.ResponseWriter, r *http.Request) {
180
181 // Handle go get requests.
182 //
183- // Always return a 200 status code, even if the repo path doesn't exist.
184- // It will try to find the repo by walking up the path until it finds one.
185- // If it can't find one, it will return a 404.
186+ // Walk up the path to the closest repository the caller can read, or
187+ // return a 404 if there is none.
188 //
189 // https://golang.org/cmd/go/#hdr-Remote_import_paths
190 // https://go.dev/ref/mod#vcs-branch
191@@ -59,9 +60,10 @@ func GoGetHandler(w http.ResponseWriter, r *http.Request) {
192 return
193 }
194
195- // find the repo
196+ // Find the closest repository the caller can read; skip the rest.
197+ user := proto.UserFromContext(ctx)
198 for {
199- if _, err := be.Repository(ctx, repo); err == nil {
200+ if _, err := be.Repository(ctx, repo); err == nil && be.AccessLevelForUser(ctx, repo, user) >= access.ReadOnlyAccess {
201 break
202 }
203
204diff --git a/testscript/testdata/http.txtar b/testscript/testdata/http.txtar
205index 796c6421a7a73eb8e1c3c0a8e11eb93fa31a60e9..9dfe6a4d941192af201f2fbc494e4865f3deb81b 100644
206--- a/testscript/testdata/http.txtar
207+++ b/testscript/testdata/http.txtar
208@@ -137,6 +137,30 @@ stdout '404.*'
209 curl http://$TOKEN@localhost:$HTTP_PORT/repo2.git?go-get=1
210 cmpenv stdout goget.txt
211
212+# go-get subpackage of a private repo does not reveal it (GHSA-fgxm-5hvv-x4vh)
213+curl http://localhost:$HTTP_PORT/repo2/subpackage?go-get=1
214+stdout '404.*'
215+! stdout 'go-import'
216+curl http://$UTOKEN@localhost:$HTTP_PORT/repo2/sub/package?go-get=1
217+stdout '404.*'
218+! stdout 'go-import'
219+curl http://$TOKEN@localhost:$HTTP_PORT/repo2/subpackage?go-get=1
220+cmpenv stdout goget.txt
221+
222+# go-get resolves a readable repo nested under an unreadable one
223+soft repo create outer -p
224+soft repo create outer/inner
225+curl http://localhost:$HTTP_PORT/outer/inner/pkg?go-get=1
226+stdout 'go-import.*/outer/inner git'
227+curl http://localhost:$HTTP_PORT/outer/pkg?go-get=1
228+stdout '404.*'
229+
230+# go-get does not bypass repository checks on other routes
231+curl http://localhost:$HTTP_PORT/nope.git/info/lfs/objects/basic/0000000000000000000000000000000000000000000000000000000000000000?go-get=1
232+stdout '404.*'
233+curl http://localhost:$HTTP_PORT/nope.git/objects/info/packs?go-get=1
234+stdout '404.*'
235+
236 # stop the server
237 [windows] stopserver
238 [windows] ! stderr .