Only allow local folder uploads when SHOW_LOCAL=true

Local folder jobs are now refused with 403 unless the server runs with
SHOW_LOCAL=true, and job kinds other than local or github are rejected.
/api/health reports localFolders so the frontend can hide the option.
Tests enable it explicitly where they index local folders.
This commit is contained in:
Andy Dixon
2026-07-19 15:27:47 +01:00
parent 599ca2235a
commit 9299f3a9c4
3 changed files with 38 additions and 2 deletions
+11 -1
View File
@@ -71,6 +71,7 @@ type Server struct {
github githubApp github githubApp
authMu sync.Mutex authMu sync.Mutex
auth map[string]*githubAuth // by session ID auth map[string]*githubAuth // by session ID
showLocal bool // SHOW_LOCAL=true allows uploading local folders
} }
func init() { func init() {
@@ -93,6 +94,7 @@ func NewServer(workspace string) *Server {
indexSlots: make(chan struct{}, maxIndexing), indexSlots: make(chan struct{}, maxIndexing),
github: githubAppFromEnv(), github: githubAppFromEnv(),
auth: map[string]*githubAuth{}, auth: map[string]*githubAuth{},
showLocal: os.Getenv("SHOW_LOCAL") == "true",
} }
} }
@@ -138,7 +140,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case r.Method == http.MethodOptions: case r.Method == http.MethodOptions:
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
case path == "/api/health" && r.Method == http.MethodGet: case path == "/api/health" && r.Method == http.MethodGet:
writeJSON(w, 200, map[string]any{"ok": true, "version": "0.1.0"}) writeJSON(w, 200, map[string]any{"ok": true, "version": "0.1.0", "localFolders": s.showLocal})
case path == "/api/jobs" && r.Method == http.MethodPost: case path == "/api/jobs" && r.Method == http.MethodPost:
s.createJob(w, r) s.createJob(w, r)
case strings.HasPrefix(path, "/api/jobs/"): case strings.HasPrefix(path, "/api/jobs/"):
@@ -164,6 +166,14 @@ func (s *Server) createJob(w http.ResponseWriter, r *http.Request) {
writeError(w, 400, "Invalid job request") writeError(w, 400, "Invalid job request")
return return
} }
switch {
case request.Kind == "local" && !s.showLocal:
writeError(w, 403, "Local folders are disabled on this server")
return
case request.Kind != "local" && request.Kind != "github":
writeError(w, 400, "Invalid job request")
return
}
owner := sessionID(w, r) owner := sessionID(w, r)
url := "" url := ""
if request.Kind == "github" { if request.Kind == "github" {
+27 -1
View File
@@ -107,6 +107,7 @@ func (c client) json(method, path, body string, wantStatus int, out any) {
func newTestServer(t *testing.T) (*Server, client) { func newTestServer(t *testing.T) (*Server, client) {
server := NewServer(filepath.Join(t.TempDir(), "codenavigator")) server := NewServer(filepath.Join(t.TempDir(), "codenavigator"))
server.showLocal = true // most tests index local folders
ts := httptest.NewServer(server) ts := httptest.NewServer(server)
t.Cleanup(ts.Close) t.Cleanup(ts.Close)
return server, client{t, ts.URL, nil}.stranger() return server, client{t, ts.URL, nil}.stranger()
@@ -240,7 +241,7 @@ func TestRoutesAndHeaders(t *testing.T) {
t.Fatalf("preflight = %d %v", res.StatusCode, res.Header) t.Fatalf("preflight = %d %v", res.StatusCode, res.Header)
} }
for path, contentType := range map[string]string{"/": "text/html", "/api.mjs": "text/javascript", "/styles.css": "text/css", "/config.js": "text/javascript", "/inter-latin.woff2": "font/woff2"} { for path, contentType := range map[string]string{"/": "text/html", "/api.mjs": "text/javascript", "/styles.css": "text/css", "/config.js": "text/javascript", "/fonts/archivo-black.woff2": "font/woff2"} {
res, err := http.Get(c.url + path) res, err := http.Get(c.url + path)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -434,3 +435,28 @@ func TestProgressStreamSendsKeepAlivesWhileQueued(t *testing.T) {
t.Fatalf("stream ended without a snapshot: %+v", job) t.Fatalf("stream ended without a snapshot: %+v", job)
} }
} }
func TestLocalFoldersDisabledByDefault(t *testing.T) {
t.Setenv("SHOW_LOCAL", "")
server := NewServer(filepath.Join(t.TempDir(), "codenavigator"))
ts := httptest.NewServer(server)
defer ts.Close()
c := client{t, ts.URL, nil}.stranger()
var health map[string]any
c.json("GET", "/api/health", "", 200, &health)
if health["localFolders"] != false {
t.Fatalf("health = %v", health)
}
c.json("POST", "/api/jobs", `{"kind":"local"}`, 403, nil)
c.json("POST", "/api/jobs", `{"kind":"mystery"}`, 400, nil)
t.Setenv("SHOW_LOCAL", "true")
enabled := httptest.NewServer(NewServer(filepath.Join(t.TempDir(), "codenavigator")))
defer enabled.Close()
on := client{t, enabled.URL, nil}.stranger()
on.json("GET", "/api/health", "", 200, &health)
if health["localFolders"] != true {
t.Fatalf("health with SHOW_LOCAL=true = %v", health)
}
on.json("POST", "/api/jobs", `{"kind":"local"}`, 201, nil)
}
Binary file not shown.