Browse Source

Honor access.ui without per-request JWT checks

rust-volume-server
Chris Lu 4 days ago
parent
commit
7aae2330ae
  1. 15
      seaweed-volume/src/server/handlers.rs
  2. 21
      seaweed-volume/tests/http_integration.rs
  3. 7
      test/volume_server/framework/cluster.go
  4. 1
      test/volume_server/matrix/config_profiles.go
  5. 22
      test/volume_server/rust/rust_volume_test.go

15
seaweed-volume/src/server/handlers.rs

@ -2423,20 +2423,7 @@ pub async fn static_asset_handler(Path(path): Path<String>) -> Response {
}
}
pub async fn ui_handler(
State(state): State<Arc<VolumeServerState>>,
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<Arc<VolumeServerState>>) -> Response {
let html = super::ui::render_volume_server_html(&state);
(
StatusCode::OK,

21
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();

7
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")
}

1
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

22
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))

Loading…
Cancel
Save