From 7aae2330ae3f8c8711800f889b2dda154b2ab705 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Mon, 16 Mar 2026 15:29:36 -0700 Subject: [PATCH] Honor access.ui without per-request JWT checks --- seaweed-volume/src/server/handlers.rs | 15 +------------ seaweed-volume/tests/http_integration.rs | 21 +++++++++++++++++-- test/volume_server/framework/cluster.go | 7 +++++++ test/volume_server/matrix/config_profiles.go | 1 + test/volume_server/rust/rust_volume_test.go | 22 ++++++++++++++++++++ 5 files changed, 50 insertions(+), 16 deletions(-) diff --git a/seaweed-volume/src/server/handlers.rs b/seaweed-volume/src/server/handlers.rs index 0e74569fd..25836306e 100644 --- a/seaweed-volume/src/server/handlers.rs +++ b/seaweed-volume/src/server/handlers.rs @@ -2423,20 +2423,7 @@ pub async fn static_asset_handler(Path(path): Path) -> Response { } } -pub async fn ui_handler( - State(state): State>, - headers: HeaderMap, -) -> Response { - // If JWT signing is enabled, require auth - let token = extract_jwt(&headers, &axum::http::Uri::from_static("/ui/index.html")); - let guard = state.guard.read().unwrap(); - if let Err(e) = guard.check_jwt(token.as_deref(), false) { - if guard.has_read_signing_key() { - return (StatusCode::UNAUTHORIZED, format!("JWT error: {}", e)).into_response(); - } - } - drop(guard); - +pub async fn ui_handler(State(state): State>) -> Response { let html = super::ui::render_volume_server_html(&state); ( StatusCode::OK, diff --git a/seaweed-volume/tests/http_integration.rs b/seaweed-volume/tests/http_integration.rs index 6899a9165..6012d68aa 100644 --- a/seaweed-volume/tests/http_integration.rs +++ b/seaweed-volume/tests/http_integration.rs @@ -635,8 +635,6 @@ async fn admin_router_can_expose_ui_with_explicit_override() { .await .unwrap(); - // UI handler does JWT check inside but read_signing_key is empty in this test, - // so it returns 200 (auth is only enforced when read key is set) assert_eq!(response.status(), StatusCode::OK); let body = body_bytes(response).await; let html = String::from_utf8(body).unwrap(); @@ -645,6 +643,25 @@ async fn admin_router_can_expose_ui_with_explicit_override() { assert!(html.contains("Volumes")); } +#[tokio::test] +async fn admin_router_ui_override_ignores_read_jwt_checks() { + let (state, _tmp) = test_state_with_signing_key(b"write-secret".to_vec()); + state.guard.write().unwrap().read_signing_key = SigningKey(b"read-secret".to_vec()); + let app = build_admin_router_with_ui(state, true); + + let response = app + .oneshot( + Request::builder() + .uri("/ui/index.html") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); +} + #[tokio::test] async fn admin_router_serves_volume_ui_static_assets() { let (state, _tmp) = test_state(); diff --git a/test/volume_server/framework/cluster.go b/test/volume_server/framework/cluster.go index a0d88ec03..1f9d30740 100644 --- a/test/volume_server/framework/cluster.go +++ b/test/volume_server/framework/cluster.go @@ -332,6 +332,13 @@ func writeSecurityConfig(configDir string, profile matrix.Profile) error { b.WriteString("\"\n") b.WriteString("expires_after_seconds = 60\n") } + if profile.EnableUIAccess { + if b.Len() > 0 { + b.WriteString("\n") + } + b.WriteString("[access]\n") + b.WriteString("ui = true\n") + } if b.Len() == 0 { b.WriteString("# optional security config generated for integration tests\n") } diff --git a/test/volume_server/matrix/config_profiles.go b/test/volume_server/matrix/config_profiles.go index c359eb029..e01e35fc1 100644 --- a/test/volume_server/matrix/config_profiles.go +++ b/test/volume_server/matrix/config_profiles.go @@ -12,6 +12,7 @@ type Profile struct { EnableJWT bool JWTSigningKey string JWTReadKey string + EnableUIAccess bool EnableMaintain bool ConcurrentUploadLimitMB int diff --git a/test/volume_server/rust/rust_volume_test.go b/test/volume_server/rust/rust_volume_test.go index 39fa8a227..6f1a0b74a 100644 --- a/test/volume_server/rust/rust_volume_test.go +++ b/test/volume_server/rust/rust_volume_test.go @@ -278,6 +278,28 @@ func TestRustMetricsEndpointIsNotOnAdminPortByDefault(t *testing.T) { } } +func TestRustUiAccessOverrideIgnoresReadJwt(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + profile := matrix.P3() + profile.EnableUIAccess = true + + cluster := framework.StartRustVolumeCluster(t, profile) + client := framework.NewHTTPClient() + + resp := framework.DoRequest(t, client, mustNewRequest(t, http.MethodGet, cluster.VolumeAdminURL()+"/ui/index.html")) + body := framework.ReadAllAndClose(t, resp) + + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected /ui/index.html 200 with access.ui override, got %d body=%s", resp.StatusCode, string(body)) + } + if len(body) == 0 { + t.Fatalf("expected non-empty UI response body") + } +} + // keys returns the keys of a map for diagnostic messages. func keys(m map[string]interface{}) []string { ks := make([]string, 0, len(m))