diff --git a/src/api/http/src/handler/http/router/mod.rs b/src/api/http/src/handler/http/router/mod.rs index 5b5548437f..861747dfbb 100644 --- a/src/api/http/src/handler/http/router/mod.rs +++ b/src/api/http/src/handler/http/router/mod.rs @@ -1282,18 +1282,11 @@ fn alert_calibration_routes(router: Router) -> Router { #[rustfmt::skip] pub fn service_routes() -> Router { - let cfg = get_config(); - - // Set server header - #[cfg(feature = "enterprise")] - let server = format!( - "{}-{}", - get_o2_config().super_cluster.region, - cfg.common.instance_name_short - ); - #[cfg(not(feature = "enterprise"))] - let server = cfg.common.instance_name_short.to_string(); + with_service_layers(service_route_table()) +} +/// The `/api` route table before the auth, audit and org layers wrap it. +fn service_route_table() -> Router { // Downtimes, Slack, search and service streams live in their own tables to keep this function under the line limit. let mut router = public_dashboard_admin_routes( Router::new() @@ -2253,6 +2246,22 @@ pub fn service_routes() -> Router { ); } + router +} + +fn with_service_layers(router: Router) -> Router { + let cfg = get_config(); + + // Set server header + #[cfg(feature = "enterprise")] + let server = format!( + "{}-{}", + get_o2_config().super_cluster.region, + cfg.common.instance_name_short + ); + #[cfg(not(feature = "enterprise"))] + let server = cfg.common.instance_name_short.to_string(); + // Snappy preprocessing sits outside RequestDecompressionLayer, which rejects snappy with 415. router .layer(middleware::from_fn(blocked_orgs_middleware)) @@ -5088,8 +5097,8 @@ mod tests { /// `service_routes()` minus comments, whitespace and wrap commas. fn service_routes_registrations() -> String { let source = include_str!("mod.rs"); - let start = source.find("pub fn service_routes() -> Router {").unwrap(); - let end = start + source[start..].find("\npub fn ").unwrap(); + let start = source.find("fn service_route_table() -> Router {").unwrap(); + let end = start + source[start..].find("\n}\n").unwrap(); let mut body = String::new(); let mut rest = &source[start..end]; while let Some(at) = rest.find("/*") { diff --git a/src/api/http/src/handler/http/router/openapi.rs b/src/api/http/src/handler/http/router/openapi.rs index 018cae1925..50642abc05 100644 --- a/src/api/http/src/handler/http/router/openapi.rs +++ b/src/api/http/src/handler/http/router/openapi.rs @@ -1681,7 +1681,7 @@ mod tests { const ROUTER_SOURCE: &str = include_str!("mod.rs"); #[cfg(feature = "enterprise")] - const SERVICE_ROUTES_SIGNATURE: &str = "pub fn service_routes("; + const SERVICE_ROUTES_SIGNATURE: &str = "fn service_route_table("; #[cfg(feature = "enterprise")] const SEARCH_ROUTES_SIGNATURE: &str = "fn search_routes_with("; diff --git a/src/api/http/src/handler/http/router/search_gate_tests.rs b/src/api/http/src/handler/http/router/search_gate_tests.rs index 677f183e66..7a8c9dcb6b 100644 --- a/src/api/http/src/handler/http/router/search_gate_tests.rs +++ b/src/api/http/src/handler/http/router/search_gate_tests.rs @@ -693,6 +693,33 @@ async fn an_operational_stream_in_the_path_does_not_open_the_gate() { assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS); } +#[cfg(feature = "enterprise")] +#[tokio::test] +async fn closed_gate_leaves_the_ungated_search_job_reads_open() { + use o2_enterprise::enterprise::license::close_search_gate_for_tests; + + let _gate = close_search_gate_for_tests().await; + // the table without its auth layers, which would answer 401 before any route + let app = service_route_table(); + for (method, uri, gated) in [ + (Method::GET, "/acme/search_jobs", false), + (Method::GET, "/acme/search_jobs/j1", false), + (Method::POST, "/acme/search_jobs", true), + ] { + let request = axum::http::Request::builder() + .method(method.clone()) + .uri(uri) + .body(Body::empty()) + .unwrap(); + let status = app.clone().oneshot(request).await.unwrap().status(); + assert_eq!( + status == StatusCode::TOO_MANY_REQUESTS, + gated, + "{method} {uri}: {status}" + ); + } +} + #[cfg(feature = "enterprise")] #[tokio::test] async fn meta_org_passes_the_closed_gate() {