Skip to content
Closed
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
6 changes: 5 additions & 1 deletion .github/actions/setup-integration-test-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,11 @@ runs:
- name: Build WordPress test container
if: ${{ inputs.build-test-images == 'true' }}
shell: bash
# Docker's official ECR mirror avoids Docker Hub anonymous pull limits.
run: |
docker build -t test-wordpress:latest \
docker build \
--build-arg DOCKER_LIBRARY=public.ecr.aws/docker/library \
-t test-wordpress:latest \
crates/trusted-server-integration-tests/fixtures/frameworks/wordpress/

- name: Build Next.js test container
Expand All @@ -106,6 +109,7 @@ runs:
run: |
docker build \
--build-arg NODE_VERSION=${{ steps.node-version.outputs.node-version }} \
--build-arg DOCKER_LIBRARY=public.ecr.aws/docker/library \
-t test-nextjs:latest \
crates/trusted-server-integration-tests/fixtures/frameworks/nextjs/

Expand Down
57 changes: 51 additions & 6 deletions crates/trusted-server-adapter-axum/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,14 +412,32 @@ async fn admin_ec_route_without_credentials_returns_401() {
}

#[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.
async fn admin_eids_route_still_requires_authentication() {
let req = Request::builder()
.method("GET")
.uri("/_ts/admin/eids")
.body(AxumBody::empty())
.expect("should build request");
let resp = make_service()
.ready()
.await
.expect("should be ready")
.call(req)
.await
.expect("should respond");
assert_eq!(resp.status().as_u16(), 401);
assert!(resp.headers().contains_key("www-authenticate"));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn authenticated_admin_eids_route_returns_diagnostic_locally() {
// The diagnostic is served locally, never via publisher fallback.
let mut svc = make_service();
let req = Request::builder()
.method("GET")
.uri("/_ts/admin/eids")
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
.header("cookie", "ts-eids=private-cookie-value")
.body(AxumBody::empty())
.expect("should build request");
let resp = svc
Expand All @@ -429,10 +447,37 @@ async fn authenticated_admin_eids_route_returns_200() {
.call(req)
.await
.expect("should respond");
assert_eq!(resp.status().as_u16(), 200);
assert_eq!(
resp.status().as_u16(),
200,
"/_ts/admin/eids should serve the real EIDs echo handler"
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.map(|v| v.split(';').next().unwrap_or(v)),
Some("application/json")
);
assert_eq!(
resp.headers()
.get("cache-control")
.and_then(|v| v.to_str().ok()),
Some("no-store")
);
assert_eq!(
resp.headers()
.get("x-content-type-options")
.and_then(|v| v.to_str().ok()),
Some("nosniff")
);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.expect("should read admin EIDs response body");
let body = std::str::from_utf8(&body).expect("should read UTF-8 admin EIDs response");
assert!(
body.contains("\"cookie_present\":true"),
"should inspect the request cookie locally"
);
assert!(
body.contains("ts-eids"),
"should include the cookie diagnostic"
);
}

Expand Down
3 changes: 1 addition & 2 deletions crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,8 +643,7 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
.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.
// Keep the authenticated diagnostic local; do not route it to publisher fallback.
.get(
"/_ts/admin/eids",
make_handler(Arc::clone(&state), |s, _services, req| async move {
Expand Down
22 changes: 14 additions & 8 deletions crates/trusted-server-adapter-cloudflare/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,9 +370,17 @@ async fn admin_ec_route_without_credentials_returns_401() {
}

#[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.
async fn authenticated_admin_eids_route_returns_diagnostic_locally() {
// The diagnostic is local and must never forward credentials to origin.
let unauthenticated = request_builder()
.method("GET")
.uri("/_ts/admin/eids")
.body(edgezero_core::body::Body::empty())
.expect("should build unauthenticated admin EIDs request");
let unauthorized = route(test_router(), unauthenticated).await;
assert_eq!(unauthorized.status().as_u16(), 401);
assert!(unauthorized.headers().contains_key("www-authenticate"));

let req = request_builder()
.method("GET")
.uri("/_ts/admin/eids")
Expand All @@ -381,11 +389,9 @@ async fn authenticated_admin_eids_route_returns_200() {
.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"
);
assert_eq!(resp.status().as_u16(), 200);
assert_eq!(resp.headers()["cache-control"], "no-store");
assert_eq!(resp.headers()["x-content-type-options"], "nosniff");
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
Expand Down
15 changes: 5 additions & 10 deletions crates/trusted-server-adapter-fastly/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ use trusted_server_core::auction::{
};
use trusted_server_core::cache_policy::EdgeCacheHeader;
use trusted_server_core::config_payload::DEFAULT_SECRET_STORE_ID;
use trusted_server_core::constants::{COOKIE_SHAREDID, COOKIE_TS_EIDS};
use trusted_server_core::constants::COOKIE_SHAREDID;
use trusted_server_core::ec::admin::{
deny_admin_diagnostic_fallback, handle_admin_ec_lookup, handle_admin_eids_lookup,
};
Expand Down Expand Up @@ -317,7 +317,6 @@ pub(crate) struct EcFinalizeState {
/// in response extensions, so `edgezero_main` rebuilds the graph from
/// settings when this is set.
pub(crate) use_finalize_kv: bool,
pub(crate) eids_cookie: Option<String>,
pub(crate) sharedid_cookie: Option<String>,
pub(crate) is_real_browser: bool,
/// Per-request services carried to the entry point so the pull-sync
Expand All @@ -333,7 +332,6 @@ struct EcRequestState {
ec_context: EcContext,
kv_graph: Option<KvIdentityGraph>,
finalize_kv_graph: Option<KvIdentityGraph>,
eids_cookie: Option<String>,
sharedid_cookie: Option<String>,
is_real_browser: bool,
services: RuntimeServices,
Expand All @@ -353,7 +351,6 @@ impl EcRequestState {
EcFinalizeState {
ec_context: self.ec_context,
use_finalize_kv: self.finalize_kv_graph.is_some(),
eids_cookie: self.eids_cookie,
sharedid_cookie: self.sharedid_cookie,
is_real_browser: self.is_real_browser,
services: self.services,
Expand Down Expand Up @@ -411,7 +408,6 @@ fn build_ec_request_state(
);
}

let eids_cookie = crate::extract_cookie_value(req, COOKIE_TS_EIDS);
let sharedid_cookie = crate::extract_cookie_value(req, COOKIE_SHAREDID);

let geo_info = services
Expand All @@ -422,7 +418,7 @@ fn build_ec_request_state(
None
});

let (ec_context, setup_error) =
let (mut ec_context, setup_error) =
match EcContext::read_from_request_with_geo(settings, req, services, geo_info.as_ref()) {
Ok(mut context) => {
context.set_device_signals(device_signals);
Expand Down Expand Up @@ -450,12 +446,12 @@ fn build_ec_request_state(
None
};
let kv_graph = if is_real_browser { kv_graph } else { None };
ec_context.set_identity_handoff_supported(finalize_kv_graph.is_some());

EcRequestState {
ec_context,
kv_graph,
finalize_kv_graph,
eids_cookie,
sharedid_cookie,
is_real_browser,
services: services.clone(),
Expand Down Expand Up @@ -731,7 +727,6 @@ async fn run_named_route(
fn run_batch_sync(state: &AppState, services: &RuntimeServices, req: Request) -> Response {
let device_signals = device_signals_for(&req);
let is_real_browser = device_signals.looks_like_browser();
let eids_cookie = crate::extract_cookie_value(&req, COOKIE_TS_EIDS);
let sharedid_cookie = crate::extract_cookie_value(&req, COOKIE_SHAREDID);

let result = crate::require_identity_graph(&state.settings).and_then(|kv| {
Expand All @@ -746,7 +741,6 @@ fn run_batch_sync(state: &AppState, services: &RuntimeServices, req: Request) ->
response.extensions_mut().insert(EcFinalizeState {
ec_context: EcContext::default(),
use_finalize_kv: false,
eids_cookie,
sharedid_cookie,
is_real_browser,
services: services.clone(),
Expand Down Expand Up @@ -2554,12 +2548,13 @@ mod tests {
let response = route(&router, request);

assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers()[header::CACHE_CONTROL], "no-store");
assert!(
response
.extensions()
.get::<super::EcFinalizeState>()
.is_none(),
"admin EIDs diagnostics should not attach EC finalization state"
"retired admin diagnostics should not attach EC finalization state"
);
}

Expand Down
35 changes: 28 additions & 7 deletions crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use fastly::{Request as FastlyRequest, Response as FastlyResponse};
use trusted_server_core::cache_policy::{EdgeCacheHeader, cache_control_headers_have_directive};
use trusted_server_core::ec::device::DeviceSignals;
use trusted_server_core::ec::finalize::ec_finalize_response;
use trusted_server_core::ec::identity::{IdentityEffects, send_then_apply_identity};
use trusted_server_core::ec::kv::KvIdentityGraph;
use trusted_server_core::ec::pull_sync::{
PullSyncContext, build_pull_sync_context, dispatch_pull_sync,
Expand Down Expand Up @@ -502,12 +503,14 @@ fn edgezero_main(mut req: FastlyRequest, sandbox: &mut Sandbox, ordinal: u64, re
if let Some(settings) = settings_snapshot.as_deref() {
match apply_edgezero_ec_finalize(settings, &mut ec_state, &mut response) {
Ok(partner_registry) => {
send_edgezero_response(
send_identity_response(
response,
request_filter_effects.as_ref(),
counters.as_ref(),
settings,
&partner_registry,
&mut ec_state,
);
run_edgezero_pull_sync_after_send(settings, &partner_registry, &ec_state);
return;
}
Err(e) => {
Expand All @@ -521,15 +524,13 @@ fn edgezero_main(mut req: FastlyRequest, sandbox: &mut Sandbox, ordinal: u64, re
Ok(settings) => {
match apply_edgezero_ec_finalize(&settings, &mut ec_state, &mut response) {
Ok(partner_registry) => {
send_edgezero_response(
send_identity_response(
response,
request_filter_effects.as_ref(),
counters.as_ref(),
);
run_edgezero_pull_sync_after_send(
&settings,
&partner_registry,
&ec_state,
&mut ec_state,
);
return;
}
Expand Down Expand Up @@ -601,13 +602,33 @@ fn apply_edgezero_ec_finalize(
&mut ec_state.ec_context,
finalize_kv_graph.as_ref(),
&partner_registry,
ec_state.eids_cookie.as_deref(),
ec_state.sharedid_cookie.as_deref(),
response,
);
Ok(partner_registry)
}

/// Keeps capture effects through cookie finalization, then consumes them after send.
fn send_identity_response(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run_edgezero_pull_sync_after_send refuses to run when !ec_state.is_real_browser (line 637), but this path applies the staged capture effects regardless. A script replaying a copied ts-ec cookie can POST to the Lockr proxy and, if upstream issues a token, enrich the row; with capture_global_withdrawal on, it can tombstone it. Can we apply the same gate here before send_then_apply_identity?

mut response: HttpResponse,
filters: Option<&RequestFilterEffects>,
counters: Option<&SandboxCounters>,
settings: &Settings,
partners: &PartnerRegistry,
state: &mut EcFinalizeState,
) {
let effects = response.extensions_mut().remove::<IdentityEffects>();
if send_then_apply_identity(
effects,
&mut state.ec_context,
partners,
|| send_edgezero_response(response, filters, counters),
|| require_identity_graph(settings).ok(),
) {
run_edgezero_pull_sync_after_send(settings, partners, state);
}
}

fn run_edgezero_pull_sync_after_send(
settings: &Settings,
partner_registry: &PartnerRegistry,
Expand Down
5 changes: 2 additions & 3 deletions crates/trusted-server-adapter-spin/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -645,16 +645,15 @@ fn build_router(state: &Arc<AppState>) -> RouterService {
Ok::<Response, EdgeError>(admin_ec_lookup_not_supported())
};

// Admin EIDs echo: pure request inspection (no KV), so this adapter
// serves the real handler.
// Keep the authenticated diagnostic local; do not route it to publisher fallback.
let s = Arc::clone(&state);
let admin_eids_handler = move |ctx: RequestContext| {
let s = Arc::clone(&s);
async move {
let req = ctx.into_request();
let result = PartnerRegistry::from_config(&s.settings.ec.partners)
.and_then(|registry| handle_admin_eids_lookup(&registry, &req));
Ok::<Response, EdgeError>(result.unwrap_or_else(|e| http_error(&e)))
Ok::<Response, EdgeError>(result.unwrap_or_else(|error| http_error(&error)))
}
};

Expand Down
32 changes: 26 additions & 6 deletions crates/trusted-server-adapter-spin/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,21 +168,41 @@ async fn admin_ec_route_without_credentials_returns_401() {
}

#[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.
async fn admin_eids_route_still_requires_authentication() {
let req = request_builder()
.method("GET")
.uri("/_ts/admin/eids")
.body(edgezero_core::body::Body::empty())
.expect("should build request");
let resp = route(test_router(), req).await;
assert_eq!(resp.status().as_u16(), 401);
assert!(resp.headers().contains_key("www-authenticate"));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn authenticated_admin_eids_route_returns_diagnostic_locally() {
// The diagnostic is local and must not forward admin credentials to fallback.
let req = request_builder()
.method("GET")
.uri("/_ts/admin/eids")
.header("authorization", "Basic YWRtaW46YWRtaW4tcGFzcw==")
.header("cookie", "ts-eids=private-cookie-value")
.body(edgezero_core::body::Body::empty())
.expect("should build request");
let resp = route(test_router(), req).await;

assert_eq!(resp.status().as_u16(), 200);
assert_eq!(
resp.status().as_u16(),
200,
"/_ts/admin/eids should serve the real EIDs echo handler"
resp.headers()
.get("cache-control")
.and_then(|v| v.to_str().ok()),
Some("no-store")
);
assert_eq!(
resp.headers()
.get("x-content-type-options")
.and_then(|v| v.to_str().ok()),
Some("nosniff")
);
}

Expand Down
Loading
Loading