Skip to content

Commit 9869ac7

Browse files
committed
Add admin endpoint to echo request EID cookies
Adds GET /_ts/admin/eids, complementing the EC lookup endpoint with the client-side half of EID propagation: it decodes the request's ts-eids and sharedId cookies and previews what cookie ingestion would write into the EC entry's ids map — matched partner UIDs (deduplicated exactly like the ingestion path) and unmatched sources that would be dropped. The endpoint always responds 200; missing or malformed cookies are reported in the payload rather than as errors. It is pure request inspection with no KV access, so every adapter serves the real handler. The path joins Settings::ADMIN_ENDPOINTS for basic-auth coverage validation.
1 parent 12592bb commit 9869ac7

10 files changed

Lines changed: 400 additions & 9 deletions

File tree

crates/trusted-server-adapter-axum/src/app.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ use error_stack::Report;
1212
use trusted_server_core::auction::endpoints::handle_auction;
1313
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
1414
use trusted_server_core::ec::EcContext;
15+
use trusted_server_core::ec::admin::handle_admin_eids_lookup;
16+
use trusted_server_core::ec::registry::PartnerRegistry;
1517
use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError};
1618
use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput};
1719
use trusted_server_core::proxy::{
@@ -253,6 +255,7 @@ enum NamedRouteHandler {
253255
VerifySignature,
254256
AdminNotSupported,
255257
AdminEcNotSupported,
258+
AdminEidsLookup,
256259
/// Legacy `/admin/keys/*` aliases — denied locally with 404 so they never
257260
/// reach the publisher fallback (which would leak admin credentials).
258261
LegacyAdminDenied,
@@ -280,7 +283,7 @@ const LEGACY_ADMIN_DENY_METHODS: &[Method] = &[
280283
Method::DELETE,
281284
];
282285

283-
fn named_routes() -> [NamedRoute; 14] {
286+
fn named_routes() -> [NamedRoute; 15] {
284287
[
285288
NamedRoute {
286289
path: "/.well-known/trusted-server.json",
@@ -318,6 +321,13 @@ fn named_routes() -> [NamedRoute; 14] {
318321
primary_methods: &[Method::GET],
319322
handler: NamedRouteHandler::AdminEcNotSupported,
320323
},
324+
// Admin EIDs echo: pure request inspection (no KV), so the dev
325+
// server serves the real handler.
326+
NamedRoute {
327+
path: "/_ts/admin/eids",
328+
primary_methods: &[Method::GET],
329+
handler: NamedRouteHandler::AdminEidsLookup,
330+
},
321331
// The legacy non-`/_ts` aliases (`/admin/keys/*`) are denied locally with
322332
// a 404, matching the Fastly and Cloudflare adapters: the production
323333
// basic-auth handler regex `^/_ts/admin` does not match them, and letting
@@ -417,6 +427,11 @@ fn named_route_handler(
417427
);
418428
Ok(resp)
419429
}
430+
NamedRouteHandler::AdminEidsLookup => {
431+
let partner_registry =
432+
PartnerRegistry::from_config(&state.settings.ec.partners)?;
433+
handle_admin_eids_lookup(&partner_registry, &req)
434+
}
420435
NamedRouteHandler::LegacyAdminDenied => Ok(legacy_admin_alias_denied()),
421436
NamedRouteHandler::Auction => {
422437
// Build the geo-aware EC context so the auction consent

crates/trusted-server-adapter-axum/tests/routes.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ fn all_explicit_routes_are_registered() {
7676
("POST", "/_ts/admin/keys/deactivate"),
7777
("GET", "/_ts/admin/ec"),
7878
("GET", "/_ts/admin/ec/{id}"),
79+
("GET", "/_ts/admin/eids"),
7980
("POST", "/admin/keys/rotate"),
8081
("POST", "/admin/keys/deactivate"),
8182
("POST", "/auction"),
@@ -290,6 +291,31 @@ async fn authenticated_admin_ec_routes_return_501() {
290291
}
291292
}
292293

294+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
295+
async fn authenticated_admin_eids_route_returns_200() {
296+
// The EIDs echo is pure request inspection (no KV), so the dev server
297+
// serves the real handler.
298+
let mut svc = make_service();
299+
let req = Request::builder()
300+
.method("GET")
301+
.uri("/_ts/admin/eids")
302+
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
303+
.body(AxumBody::empty())
304+
.expect("should build request");
305+
let resp = svc
306+
.ready()
307+
.await
308+
.expect("should be ready")
309+
.call(req)
310+
.await
311+
.expect("should respond");
312+
assert_eq!(
313+
resp.status().as_u16(),
314+
200,
315+
"/_ts/admin/eids should serve the real EIDs echo handler"
316+
);
317+
}
318+
293319
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
294320
async fn legacy_admin_aliases_denied_locally_not_proxied_to_publisher() {
295321
// Regression for the credential-leak finding: the production basic-auth regex

crates/trusted-server-adapter-cloudflare/src/app.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
1313
#[cfg(target_arch = "wasm32")]
1414
use trusted_server_core::config_payload::settings_from_config_blob;
1515
use trusted_server_core::ec::EcContext;
16+
use trusted_server_core::ec::admin::handle_admin_eids_lookup;
17+
use trusted_server_core::ec::registry::PartnerRegistry;
1618
use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError};
1719
use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput};
1820
use trusted_server_core::platform::RuntimeServices;
@@ -486,6 +488,15 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
486488
.get("/_ts/admin/ec/{id}", |_ctx: RequestContext| async {
487489
Ok::<Response, EdgeError>(admin_ec_lookup_not_supported())
488490
})
491+
// Admin EIDs echo: pure request inspection (no KV), so this
492+
// adapter serves the real handler.
493+
.get(
494+
"/_ts/admin/eids",
495+
make_handler(Arc::clone(&state), |s, _services, req| async move {
496+
let partner_registry = PartnerRegistry::from_config(&s.settings.ec.partners)?;
497+
handle_admin_eids_lookup(&partner_registry, &req)
498+
}),
499+
)
489500
.post(
490501
"/auction",
491502
make_handler(Arc::clone(&state), |s, services, req| async move {

crates/trusted-server-adapter-cloudflare/tests/routes.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ fn all_explicit_routes_are_registered() {
217217
("POST", "/_ts/admin/keys/deactivate"),
218218
("GET", "/_ts/admin/ec"),
219219
("GET", "/_ts/admin/ec/{id}"),
220+
("GET", "/_ts/admin/eids"),
220221
("POST", "/auction"),
221222
("GET", "/first-party/proxy"),
222223
("GET", "/first-party/click"),
@@ -292,6 +293,25 @@ async fn authenticated_admin_ec_routes_return_501() {
292293
}
293294
}
294295

296+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
297+
async fn authenticated_admin_eids_route_returns_200() {
298+
// The EIDs echo is pure request inspection (no KV), so this adapter
299+
// serves the real handler.
300+
let req = request_builder()
301+
.method("GET")
302+
.uri("/_ts/admin/eids")
303+
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
304+
.body(edgezero_core::body::Body::empty())
305+
.expect("should build request");
306+
let resp = route(test_router(), req).await;
307+
308+
assert_eq!(
309+
resp.status().as_u16(),
310+
200,
311+
"/_ts/admin/eids should serve the real EIDs echo handler"
312+
);
313+
}
314+
295315
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
296316
async fn admin_route_without_credentials_returns_401() {
297317
let router = test_router();

crates/trusted-server-adapter-fastly/src/app.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
//! | POST | `/_ts/admin/keys/deactivate` | [`handle_deactivate_key`] |
2525
//! | GET | `/_ts/admin/ec` | [`handle_admin_ec_lookup`] |
2626
//! | GET | `/_ts/admin/ec/{id}` | [`handle_admin_ec_lookup`] |
27+
//! | GET | `/_ts/admin/eids` | [`handle_admin_eids_lookup`] |
2728
//! | POST | `/_ts/api/v1/batch-sync` | [`handle_batch_sync`] |
2829
//! | GET | `/_ts/api/v1/identify` | [`handle_identify`] |
2930
//! | GET | `/_ts/set-tester` | [`handle_set_tester`] |
@@ -100,7 +101,7 @@ use trusted_server_core::auction::endpoints::handle_auction;
100101
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
101102
use trusted_server_core::constants::{COOKIE_SHAREDID, COOKIE_TS_EIDS};
102103
use trusted_server_core::ec::EcContext;
103-
use trusted_server_core::ec::admin::handle_admin_ec_lookup;
104+
use trusted_server_core::ec::admin::{handle_admin_ec_lookup, handle_admin_eids_lookup};
104105
use trusted_server_core::ec::batch_sync::handle_batch_sync;
105106
use trusted_server_core::ec::consent::ec_consent_withdrawn;
106107
use trusted_server_core::ec::device::DeviceSignals;
@@ -576,6 +577,10 @@ async fn run_named_route(
576577
let partner_registry = PartnerRegistry::from_config(&state.settings.ec.partners)?;
577578
handle_admin_ec_lookup(kv.as_ref(), &partner_registry, &req)
578579
}
580+
NamedRouteHandler::AdminEidsLookup => {
581+
let partner_registry = PartnerRegistry::from_config(&state.settings.ec.partners)?;
582+
handle_admin_eids_lookup(&partner_registry, &req)
583+
}
579584
NamedRouteHandler::LegacyAdminDenied => Ok(legacy_admin_alias_denied()),
580585
NamedRouteHandler::BatchSync => {
581586
// Dispatched by execute_named before EC state is built.
@@ -999,6 +1004,7 @@ enum NamedRouteHandler {
9991004
RotateKey,
10001005
DeactivateKey,
10011006
AdminEcLookup,
1007+
AdminEidsLookup,
10021008
/// Legacy `/admin/keys/*` aliases — denied locally with 404 so they never
10031009
/// reach the publisher fallback (which would leak admin credentials).
10041010
LegacyAdminDenied,
@@ -1063,6 +1069,13 @@ const NAMED_ROUTES: &[NamedRoute] = &[
10631069
primary_methods: &[Method::GET],
10641070
handler: NamedRouteHandler::AdminEcLookup,
10651071
},
1072+
// Admin EIDs echo: decodes the request's ts-eids/sharedId cookies with
1073+
// an ingestion preview. Pure request inspection — no KV access.
1074+
NamedRoute {
1075+
path: "/_ts/admin/eids",
1076+
primary_methods: &[Method::GET],
1077+
handler: NamedRouteHandler::AdminEidsLookup,
1078+
},
10661079
// The legacy non-`/_ts` aliases (`/admin/keys/*`) are denied locally with a
10671080
// 404 instead of executing key operations: the production basic-auth handler
10681081
// regex `^/_ts/admin` does not match them, and letting them fall through to
@@ -1670,6 +1683,20 @@ mod tests {
16701683
"{path} must have GET as its only primary method"
16711684
);
16721685
}
1686+
1687+
let eids_route = NAMED_ROUTES
1688+
.iter()
1689+
.find(|route| route.path == "/_ts/admin/eids")
1690+
.expect("should register /_ts/admin/eids as a named route");
1691+
assert!(
1692+
matches!(eids_route.handler, NamedRouteHandler::AdminEidsLookup),
1693+
"/_ts/admin/eids must map to the admin EIDs lookup handler"
1694+
);
1695+
assert_eq!(
1696+
eids_route.primary_methods,
1697+
&[Method::GET],
1698+
"/_ts/admin/eids must have GET as its only primary method"
1699+
);
16731700
}
16741701

16751702
#[test]

crates/trusted-server-adapter-spin/src/app.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use error_stack::Report;
1111
use trusted_server_core::auction::endpoints::handle_auction;
1212
use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator};
1313
use trusted_server_core::ec::EcContext;
14+
use trusted_server_core::ec::admin::handle_admin_eids_lookup;
15+
use trusted_server_core::ec::registry::PartnerRegistry;
1416
use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError};
1517
use trusted_server_core::http_util::sanitize_forwarded_headers;
1618
use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput};
@@ -141,14 +143,15 @@ const LEGACY_ADMIN_DENY_METHODS: &[Method] = &[
141143
Method::DELETE,
142144
];
143145

144-
fn named_fallback_paths() -> [(&'static str, &'static [Method]); 14] {
146+
fn named_fallback_paths() -> [(&'static str, &'static [Method]); 15] {
145147
[
146148
("/.well-known/trusted-server.json", &[Method::GET]),
147149
("/verify-signature", &[Method::POST]),
148150
("/_ts/admin/keys/rotate", &[Method::POST]),
149151
("/_ts/admin/keys/deactivate", &[Method::POST]),
150152
("/_ts/admin/ec", &[Method::GET]),
151153
("/_ts/admin/ec/{id}", &[Method::GET]),
154+
("/_ts/admin/eids", &[Method::GET]),
152155
("/admin/keys/rotate", LEGACY_ADMIN_DENY_METHODS),
153156
("/admin/keys/deactivate", LEGACY_ADMIN_DENY_METHODS),
154157
("/auction", &[Method::POST]),
@@ -531,6 +534,19 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
531534
Ok::<Response, EdgeError>(admin_ec_lookup_not_supported())
532535
};
533536

537+
// Admin EIDs echo: pure request inspection (no KV), so this adapter
538+
// serves the real handler.
539+
let s = Arc::clone(&state);
540+
let admin_eids_handler = move |ctx: RequestContext| {
541+
let s = Arc::clone(&s);
542+
async move {
543+
let req = ctx.into_request();
544+
let result = PartnerRegistry::from_config(&s.settings.ec.partners)
545+
.and_then(|registry| handle_admin_eids_lookup(&registry, &req));
546+
Ok::<Response, EdgeError>(result.unwrap_or_else(|e| http_error(&e)))
547+
}
548+
};
549+
534550
// /auction
535551
let s = Arc::clone(&state);
536552
let auction_handler = move |ctx: RequestContext| {
@@ -757,6 +773,7 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
757773
// adapter has no store to read.
758774
.get("/_ts/admin/ec", admin_ec_not_supported_handler)
759775
.get("/_ts/admin/ec/{id}", admin_ec_not_supported_handler)
776+
.get("/_ts/admin/eids", admin_eids_handler)
760777
.post("/auction", auction_handler)
761778
.get("/__ts/page-bids", page_bids_handler)
762779
.route(

crates/trusted-server-adapter-spin/tests/routes.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,25 @@ async fn authenticated_admin_ec_routes_return_501() {
139139
}
140140
}
141141

142+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
143+
async fn authenticated_admin_eids_route_returns_200() {
144+
// The EIDs echo is pure request inspection (no KV), so this adapter
145+
// serves the real handler.
146+
let req = request_builder()
147+
.method("GET")
148+
.uri("/_ts/admin/eids")
149+
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
150+
.body(edgezero_core::body::Body::empty())
151+
.expect("should build request");
152+
let resp = route(test_router(), req).await;
153+
154+
assert_eq!(
155+
resp.status().as_u16(),
156+
200,
157+
"/_ts/admin/eids should serve the real EIDs echo handler"
158+
);
159+
}
160+
142161
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
143162
async fn health_route_returns_ok() {
144163
// Parity with the Fastly/Axum adapters: GET /health is a cheap liveness probe

0 commit comments

Comments
 (0)