Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion crates/trusted-server-adapter-axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use error_stack::Report;
use trusted_server_core::auction::endpoints::handle_auction;
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
use trusted_server_core::ec::EcContext;
use trusted_server_core::ec::admin::handle_admin_eids_lookup;
use trusted_server_core::ec::registry::PartnerRegistry;
use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError};
use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput};
use trusted_server_core::proxy::{
Expand All @@ -29,6 +31,7 @@ use trusted_server_core::settings::Settings;
use trusted_server_core::settings_data::{
default_config_key, default_config_store_name, get_settings_from_config_store,
};
use trusted_server_core::trace_cookie::handle_trace_mode;

use trusted_server_core::platform::RuntimeServices;

Expand Down Expand Up @@ -252,9 +255,12 @@ enum NamedRouteHandler {
TrustedServerDiscovery,
VerifySignature,
AdminNotSupported,
AdminEcNotSupported,
AdminEidsLookup,
/// Legacy `/admin/keys/*` aliases — denied locally with 404 so they never
/// reach the publisher fallback (which would leak admin credentials).
LegacyAdminDenied,
TraceMode,
Auction,
PageBids,
FirstPartyProxy,
Expand All @@ -279,7 +285,7 @@ const LEGACY_ADMIN_DENY_METHODS: &[Method] = &[
Method::DELETE,
];

fn named_routes() -> [NamedRoute; 12] {
fn named_routes() -> [NamedRoute; 16] {
[
NamedRoute {
path: "/.well-known/trusted-server.json",
Expand All @@ -304,6 +310,26 @@ fn named_routes() -> [NamedRoute; 12] {
primary_methods: &[Method::POST],
handler: NamedRouteHandler::AdminNotSupported,
},
// Admin EC lookup routes. Registered explicitly (like the key routes
// above) so they never fall through to the publisher fallback, and
// they match `Settings::ADMIN_ENDPOINTS` for auth coverage.
NamedRoute {
path: "/_ts/admin/ec",
primary_methods: &[Method::GET],
handler: NamedRouteHandler::AdminEcNotSupported,
},
NamedRoute {
path: "/_ts/admin/ec/{id}",
primary_methods: &[Method::GET],
handler: NamedRouteHandler::AdminEcNotSupported,
},
// Admin EIDs echo: pure request inspection (no KV), so the dev
// server serves the real handler.
NamedRoute {
path: "/_ts/admin/eids",
primary_methods: &[Method::GET],
handler: NamedRouteHandler::AdminEidsLookup,
},
// The legacy non-`/_ts` aliases (`/admin/keys/*`) are denied locally with
// a 404, matching the Fastly and Cloudflare adapters: the production
// basic-auth handler regex `^/_ts/admin` does not match them, and letting
Expand All @@ -320,6 +346,11 @@ fn named_routes() -> [NamedRoute; 12] {
primary_methods: LEGACY_ADMIN_DENY_METHODS,
handler: NamedRouteHandler::LegacyAdminDenied,
},
NamedRoute {
path: "/_ts/trace",
primary_methods: &[Method::GET],
handler: NamedRouteHandler::TraceMode,
},
NamedRoute {
path: "/auction",
primary_methods: &[Method::POST],
Expand Down Expand Up @@ -388,7 +419,30 @@ fn named_route_handler(
);
Ok(resp)
}
NamedRouteHandler::AdminEcNotSupported => {
// The EC identity graph is Fastly KV backed; the Axum
// dev server has no store to read.
let body = edgezero_core::body::Body::from(
"Admin EC lookup is not supported on the Axum dev server.\n\
Use the Fastly adapter (via Viceroy or deployed) to inspect EC entries.\n",
);
let mut resp = Response::new(body);
*resp.status_mut() = StatusCode::NOT_IMPLEMENTED;
resp.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
);
Ok(resp)
}
NamedRouteHandler::AdminEidsLookup => {
let partner_registry =
PartnerRegistry::from_config(&state.settings.ec.partners)?;
handle_admin_eids_lookup(&partner_registry, &req)
}
NamedRouteHandler::LegacyAdminDenied => Ok(legacy_admin_alias_denied()),
NamedRouteHandler::TraceMode => {
handle_trace_mode(&state.settings, req.uri().query())
}
NamedRouteHandler::Auction => {
// Build the geo-aware EC context so the auction consent
// gate sees the caller's jurisdiction — `EcContext::default()`
Expand Down
60 changes: 60 additions & 0 deletions crates/trusted-server-adapter-axum/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ fn all_explicit_routes_are_registered() {
("POST", "/verify-signature"),
("POST", "/_ts/admin/keys/rotate"),
("POST", "/_ts/admin/keys/deactivate"),
("GET", "/_ts/admin/ec"),
("GET", "/_ts/admin/ec/{id}"),
("GET", "/_ts/admin/eids"),
("POST", "/admin/keys/rotate"),
("POST", "/admin/keys/deactivate"),
("POST", "/auction"),
Expand Down Expand Up @@ -256,6 +259,63 @@ async fn admin_route_without_credentials_returns_401() {
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn authenticated_admin_ec_routes_return_501() {
// The EC identity graph is Fastly KV backed, so the Axum dev server
// answers the admin EC lookup routes locally with 501 instead of letting
// them fall through to the publisher fallback.
let sample_ec_id = format!("{}.abc123", "a".repeat(64));
for path in [
"/_ts/admin/ec".to_owned(),
format!("/_ts/admin/ec/{sample_ec_id}"),
] {
let mut svc = make_service();
let req = Request::builder()
.method("GET")
.uri(&path)
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
.body(AxumBody::empty())
.expect("should build request");
let resp = svc
.ready()
.await
.expect("should be ready")
.call(req)
.await
.expect("should respond");
assert_eq!(
resp.status().as_u16(),
501,
"{path} should report that Axum EC lookup is unsupported"
);
}
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn authenticated_admin_eids_route_returns_200() {
// The EIDs echo is pure request inspection (no KV), so the dev server
// serves the real handler.
let mut svc = make_service();
let req = Request::builder()
.method("GET")
.uri("/_ts/admin/eids")
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
.body(AxumBody::empty())
.expect("should build request");
let resp = svc
.ready()
.await
.expect("should be ready")
.call(req)
.await
.expect("should respond");
assert_eq!(
resp.status().as_u16(),
200,
"/_ts/admin/eids should serve the real EIDs echo handler"
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn legacy_admin_aliases_denied_locally_not_proxied_to_publisher() {
// Regression for the credential-leak finding: the production basic-auth regex
Expand Down
46 changes: 46 additions & 0 deletions crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
#[cfg(target_arch = "wasm32")]
use trusted_server_core::config_payload::settings_from_config_blob;
use trusted_server_core::ec::EcContext;
use trusted_server_core::ec::admin::handle_admin_eids_lookup;
use trusted_server_core::ec::registry::PartnerRegistry;
use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError};
use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput};
use trusted_server_core::platform::RuntimeServices;
Expand All @@ -28,6 +30,7 @@ use trusted_server_core::request_signing::{
handle_trusted_server_discovery, handle_verify_signature,
};
use trusted_server_core::settings::Settings;
use trusted_server_core::trace_cookie::handle_trace_mode;

use crate::middleware::{AuthMiddleware, FinalizeResponseMiddleware};
use crate::platform::build_runtime_services;
Expand Down Expand Up @@ -242,6 +245,20 @@ fn admin_key_management_not_supported() -> Response {
response
}

fn admin_ec_lookup_not_supported() -> Response {
let body = edgezero_core::body::Body::from(
"Admin EC lookup is not supported on Cloudflare Workers.\n\
Use the Fastly adapter (via Viceroy or deployed) to inspect EC entries.\n",
);
let mut response = Response::new(body);
*response.status_mut() = StatusCode::NOT_IMPLEMENTED;
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
);
response
}

/// Builds the local `404 Not Found` returned for legacy `/admin/keys/*`
/// aliases on the Cloudflare adapter.
///
Expand Down Expand Up @@ -461,6 +478,35 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
.post("/_ts/admin/keys/deactivate", |_ctx: RequestContext| async {
Ok::<Response, EdgeError>(admin_key_management_not_supported())
})
// Admin EC lookup routes. Registered explicitly (like the key
// routes above) so they never fall through to the publisher
// fallback, and they match `Settings::ADMIN_ENDPOINTS` for auth
// coverage. The EC identity graph is Fastly KV backed, so this
// adapter has no store to read.
.get("/_ts/admin/ec", |_ctx: RequestContext| async {
Ok::<Response, EdgeError>(admin_ec_lookup_not_supported())
})
.get("/_ts/admin/ec/{id}", |_ctx: RequestContext| async {
Ok::<Response, EdgeError>(admin_ec_lookup_not_supported())
})
// Admin EIDs echo: pure request inspection (no KV), so this
// adapter serves the real handler.
.get(
"/_ts/admin/eids",
make_handler(Arc::clone(&state), |s, _services, req| async move {
let partner_registry = PartnerRegistry::from_config(&s.settings.ec.partners)?;
handle_admin_eids_lookup(&partner_registry, &req)
}),
)
// Render-trace toggle: arms/disarms the ts-trace cookie and
// redirects to `/`. Gated by [debug] trace_route_enabled (404 when
// off).
.get(
"/_ts/trace",
make_handler(Arc::clone(&state), |s, _services, req| async move {
handle_trace_mode(&s.settings, req.uri().query())
}),
)
.post(
"/auction",
make_handler(Arc::clone(&state), |s, services, req| async move {
Expand Down
48 changes: 48 additions & 0 deletions crates/trusted-server-adapter-cloudflare/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ fn all_explicit_routes_are_registered() {
("POST", "/verify-signature"),
("POST", "/_ts/admin/keys/rotate"),
("POST", "/_ts/admin/keys/deactivate"),
("GET", "/_ts/admin/ec"),
("GET", "/_ts/admin/ec/{id}"),
("GET", "/_ts/admin/eids"),
("POST", "/auction"),
("GET", "/first-party/proxy"),
("GET", "/first-party/click"),
Expand Down Expand Up @@ -264,6 +267,51 @@ async fn authenticated_admin_routes_return_501() {
}
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn authenticated_admin_ec_routes_return_501() {
// The EC identity graph is Fastly KV backed, so Cloudflare answers the
// admin EC lookup routes locally with 501 instead of letting them fall
// through to the publisher fallback.
let sample_ec_id = format!("{}.abc123", "a".repeat(64));
for path in [
"/_ts/admin/ec".to_owned(),
format!("/_ts/admin/ec/{sample_ec_id}"),
] {
let req = request_builder()
.method("GET")
.uri(&path)
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
.body(edgezero_core::body::Body::empty())
.expect("should build request");
let resp = route(test_router(), req).await;

assert_eq!(
resp.status().as_u16(),
501,
"{path} should report that Cloudflare EC lookup is unsupported"
);
}
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn authenticated_admin_eids_route_returns_200() {
// The EIDs echo is pure request inspection (no KV), so this adapter
// serves the real handler.
let req = request_builder()
.method("GET")
.uri("/_ts/admin/eids")
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
.body(edgezero_core::body::Body::empty())
.expect("should build request");
let resp = route(test_router(), req).await;

assert_eq!(
resp.status().as_u16(),
200,
"/_ts/admin/eids should serve the real EIDs echo handler"
);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn admin_route_without_credentials_returns_401() {
let router = test_router();
Expand Down
Loading
Loading