From fd3ecc38ef0c3ceb751fe12a045d027ecae51f8f Mon Sep 17 00:00:00 2001 From: sagitchu Date: Thu, 14 May 2026 18:45:12 +0800 Subject: [PATCH] fix: allow public config fallback for cached login --- .../http/handler/config_access_test.go | 41 +++++++++++++++++-- go-backend/internal/http/handler/handler.go | 7 +++- .../http/handler/system_upgrade_test.go | 4 +- go-backend/internal/http/middleware/auth.go | 20 +++++++++ .../tests/contract/migration_contract_test.go | 10 ++++- 5 files changed, 73 insertions(+), 9 deletions(-) diff --git a/go-backend/internal/http/handler/config_access_test.go b/go-backend/internal/http/handler/config_access_test.go index fd0792c..617cf77 100644 --- a/go-backend/internal/http/handler/config_access_test.go +++ b/go-backend/internal/http/handler/config_access_test.go @@ -42,16 +42,29 @@ func TestPublicConfigGetRejectsSensitiveKeys(t *testing.T) { assertHandlerCodeMsg(t, resp, 403, "禁止访问敏感配置") } -func TestConfigGetNowRequiresAuth(t *testing.T) { - router, _ := setupConfigAccessTestRouter(t) +func TestConfigGetAllowsPublicCloudflareSiteKeyWithoutAuthForCachedLoginPage(t *testing.T) { + router, r := setupConfigAccessTestRouter(t) + seedConfigValue(t, r, "cloudflare_site_key", "site-key") - req := httptest.NewRequest(http.MethodPost, "/api/v1/config/get", bytes.NewBufferString(`{"name":"app_name"}`)) + req := httptest.NewRequest(http.MethodPost, "/api/v1/config/get", bytes.NewBufferString(`{"name":"cloudflare_site_key"}`)) req.Header.Set("Content-Type", "application/json") resp := httptest.NewRecorder() router.ServeHTTP(resp, req) - assertHandlerCodeMsg(t, resp, 401, "未登录或token已过期") + assertHandlerConfigValue(t, resp, "cloudflare_site_key", "site-key") +} + +func TestConfigGetRejectsSensitiveKeysWithoutAuth(t *testing.T) { + router, _ := setupConfigAccessTestRouter(t) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/config/get", bytes.NewBufferString(`{"name":"jwt_secret"}`)) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + + router.ServeHTTP(resp, req) + + assertHandlerCodeMsg(t, resp, 403, "禁止访问敏感配置") } func TestConfigUpdateRejectsSensitiveKeys(t *testing.T) { @@ -139,3 +152,23 @@ func assertHandlerCodeMsg(t *testing.T, rec *httptest.ResponseRecorder, expected t.Fatalf("expected (%d,%q), got (%d,%q)", expectedCode, expectedMsg, out.Code, out.Msg) } } + +func assertHandlerConfigValue(t *testing.T, rec *httptest.ResponseRecorder, expectedName, expectedValue string) { + t.Helper() + var out struct { + Code int `json:"code"` + Data struct { + Name string `json:"name"` + Value string `json:"value"` + } `json:"data"` + } + if err := json.NewDecoder(rec.Body).Decode(&out); err != nil { + t.Fatalf("decode response: %v", err) + } + if out.Code != 0 { + t.Fatalf("expected code 0, got %d", out.Code) + } + if out.Data.Name != expectedName || out.Data.Value != expectedValue { + t.Fatalf("expected config (%q,%q), got (%q,%q)", expectedName, expectedValue, out.Data.Name, out.Data.Value) + } +} diff --git a/go-backend/internal/http/handler/handler.go b/go-backend/internal/http/handler/handler.go index efdf721..e1958f3 100644 --- a/go-backend/internal/http/handler/handler.go +++ b/go-backend/internal/http/handler/handler.go @@ -399,7 +399,12 @@ func (h *Handler) getConfigByName(w http.ResponseWriter, r *http.Request) { return } - cfg, err := h.repo.GetConfigByName(req.Name) + if _, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims); !ok && !repo.IsPublicConfigKey(configName) { + response.WriteJSON(w, response.Err(401, "未登录或token已过期")) + return + } + + cfg, err := h.repo.GetConfigByName(configName) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return diff --git a/go-backend/internal/http/handler/system_upgrade_test.go b/go-backend/internal/http/handler/system_upgrade_test.go index b6df615..8222c54 100644 --- a/go-backend/internal/http/handler/system_upgrade_test.go +++ b/go-backend/internal/http/handler/system_upgrade_test.go @@ -272,7 +272,7 @@ func TestSystemUpgradeFailsFastBeforeMutatingFiles(t *testing.T) { fakeDockerDir := t.TempDir() fakeDockerPath := filepath.Join(fakeDockerDir, "docker") - fakeDockerScript := "#!/bin/sh\ncase \"$1\" in\n --version)\n echo 'Docker version 27.0.0'\n exit 0\n ;;&\n compose)\n if [ \"$2\" = version ]; then\n echo 'Docker Compose version v2.33.0'\n exit 0\n fi\n exit 0\n ;;&\n inspect)\n echo 'No such object: flux-panel-backend' >&2\n exit 1\n ;;&\n *)\n exit 0\n ;;&\n esac\n" + fakeDockerScript := "#!/bin/sh\ncase \"$1\" in\n --version)\n echo 'Docker version 27.0.0'\n exit 0\n ;;\n compose)\n if [ \"$2\" = version ]; then\n echo 'Docker Compose version v2.33.0'\n exit 0\n fi\n exit 0\n ;;\n inspect)\n echo 'No such object: flux-panel-backend' >&2\n exit 1\n ;;\n *)\n exit 0\n ;;\n esac\n" if err := os.WriteFile(fakeDockerPath, []byte(fakeDockerScript), 0o755); err != nil { t.Fatalf("WriteFile() fake docker error = %v", err) } @@ -281,7 +281,7 @@ func TestSystemUpgradeFailsFastBeforeMutatingFiles(t *testing.T) { t.Setenv(panelBackendContainerEnv, "flux-panel-backend") h := &Handler{} - req := httptest.NewRequest(http.MethodPost, "/api/v1/system/upgrade", strings.NewReader(`{"channel":"stable"}`)) + req := httptest.NewRequest(http.MethodPost, "/api/v1/system/upgrade", strings.NewReader(`{"channel":"stable","version":"3.0.0"}`)) rr := httptest.NewRecorder() h.systemUpgrade(rr, req) diff --git a/go-backend/internal/http/middleware/auth.go b/go-backend/internal/http/middleware/auth.go index 09fd77f..16826fe 100644 --- a/go-backend/internal/http/middleware/auth.go +++ b/go-backend/internal/http/middleware/auth.go @@ -34,12 +34,20 @@ func JWT(opts AuthOptions) func(http.Handler) http.Handler { token := strings.TrimSpace(r.Header.Get("Authorization")) if token == "" { + if allowsOptionalAuth(r.URL.Path) { + next.ServeHTTP(w, r) + return + } response.WriteJSON(w, response.Err(401, "未登录或token已过期")) return } claims, ok := auth.ValidateToken(token, opts.JWTSecret) if !ok { + if allowsOptionalAuth(r.URL.Path) { + next.ServeHTTP(w, r) + return + } response.WriteJSON(w, response.Err(401, "无效的token或token已过期")) return } @@ -47,11 +55,19 @@ func JWT(opts AuthOptions) func(http.Handler) http.Handler { if opts.GetUserAuthState != nil { userID, err := strconv.ParseInt(claims.Sub, 10, 64) if err != nil { + if allowsOptionalAuth(r.URL.Path) { + next.ServeHTTP(w, r) + return + } response.WriteJSON(w, response.Err(401, "无效的token或token已过期")) return } state, err := opts.GetUserAuthState(userID) if err != nil || state == nil || state.Status != 1 || state.RoleID != claims.RoleID || claims.IatMs <= state.PasswordChangedAt { + if allowsOptionalAuth(r.URL.Path) { + next.ServeHTTP(w, r) + return + } response.WriteJSON(w, response.Err(401, "无效的token或token已过期")) return } @@ -84,6 +100,10 @@ func RequireAdmin(next http.Handler) http.Handler { }) } +func allowsOptionalAuth(path string) bool { + return path == "/api/v1/config/get" +} + func shouldSkip(path string) bool { switch { case strings.HasPrefix(path, "/flow/"): diff --git a/go-backend/tests/contract/migration_contract_test.go b/go-backend/tests/contract/migration_contract_test.go index e518984..797cc75 100644 --- a/go-backend/tests/contract/migration_contract_test.go +++ b/go-backend/tests/contract/migration_contract_test.go @@ -162,11 +162,17 @@ func TestPublicConfigGetAndAuthConfigContract(t *testing.T) { router.ServeHTTP(secretResp, secretReq) assertCodeMsg(t, secretResp, 403, "禁止访问敏感配置") - configReq := httptest.NewRequest(http.MethodPost, "/api/v1/config/get", bytes.NewBufferString(`{"name":"app_name"}`)) + configReq := httptest.NewRequest(http.MethodPost, "/api/v1/config/get", bytes.NewBufferString(`{"name":"cloudflare_site_key"}`)) configReq.Header.Set("Content-Type", "application/json") configResp := httptest.NewRecorder() router.ServeHTTP(configResp, configReq) - assertCodeMsg(t, configResp, 401, "未登录或token已过期") + assertCode(t, configResp, 0) + + configSecretReq := httptest.NewRequest(http.MethodPost, "/api/v1/config/get", bytes.NewBufferString(`{"name":"jwt_secret"}`)) + configSecretReq.Header.Set("Content-Type", "application/json") + configSecretResp := httptest.NewRecorder() + router.ServeHTTP(configSecretResp, configSecretReq) + assertCodeMsg(t, configSecretResp, 403, "禁止访问敏感配置") } func TestOpenAPISubStoreContracts(t *testing.T) {