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
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
11 changes: 4 additions & 7 deletions crates/trusted-server-adapter-axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ use trusted_server_core::auction::{
use trusted_server_core::cache_policy::EdgeCacheHeader;
use trusted_server_core::ec::EcContext;
use trusted_server_core::ec::admin::{
admin_ec_lookup_not_supported, deny_admin_diagnostic_fallback, handle_admin_eids_lookup,
admin_ec_lookup_not_supported, admin_eids_lookup_retired, deny_admin_diagnostic_fallback,
};
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 Down Expand Up @@ -495,11 +494,9 @@ fn named_route_handler(
// dev server has no store to read.
Ok(admin_ec_lookup_not_supported())
}
NamedRouteHandler::AdminEidsLookup => {
let partner_registry =
PartnerRegistry::from_config(&state.settings.ec.partners)?;
handle_admin_eids_lookup(&partner_registry, &req)
}
// Keep the authenticated route local. Removing it would
// risk forwarding admin credentials to publisher fallback.
NamedRouteHandler::AdminEidsLookup => Ok(admin_eids_lookup_retired()),
NamedRouteHandler::LegacyAdminDenied => Ok(legacy_admin_alias_denied()),
NamedRouteHandler::Auction => {
// Build the geo-aware EC context so the auction consent
Expand Down
40 changes: 35 additions & 5 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 retired_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_local_retirement_denial() {
// Retired routes must not forward admin credentials to 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 @@ -431,8 +449,20 @@ async fn authenticated_admin_eids_route_returns_200() {
.expect("should respond");
assert_eq!(
resp.status().as_u16(),
200,
"/_ts/admin/eids should serve the real EIDs echo handler"
410,
"/_ts/admin/eids should deny locally after authentication"
);
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")
);
}

Expand Down
13 changes: 5 additions & 8 deletions crates/trusted-server-adapter-cloudflare/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ use trusted_server_core::config_payload::CONFIG_BLOB_KEY;
use trusted_server_core::config_payload::{DEFAULT_SECRET_STORE_ID, settings_from_config_blob};
use trusted_server_core::ec::EcContext;
use trusted_server_core::ec::admin::{
admin_ec_lookup_not_supported as core_admin_ec_lookup_not_supported,
deny_admin_diagnostic_fallback, handle_admin_eids_lookup,
admin_ec_lookup_not_supported as core_admin_ec_lookup_not_supported, admin_eids_lookup_retired,
deny_admin_diagnostic_fallback,
};
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 Down Expand Up @@ -643,13 +642,11 @@ 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 retired diagnostic local and behind existing authentication.
.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)
make_handler(Arc::clone(&state), |_s, _services, _req| async move {
Ok(admin_eids_lookup_retired())
}),
)
.post(
Expand Down
11 changes: 6 additions & 5 deletions crates/trusted-server-adapter-cloudflare/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,9 +370,8 @@ 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_local_retirement() {
// Retired diagnostics must not forward authenticated requests to origin.
let req = request_builder()
.method("GET")
.uri("/_ts/admin/eids")
Expand All @@ -383,9 +382,11 @@ async fn authenticated_admin_eids_route_returns_200() {

assert_eq!(
resp.status().as_u16(),
200,
"/_ts/admin/eids should serve the real EIDs echo handler"
410,
"should deny the retired EID diagnostic locally"
);
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
20 changes: 7 additions & 13 deletions crates/trusted-server-adapter-fastly/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
//! | POST | `/_ts/admin/keys/deactivate` | [`handle_deactivate_key`] |
//! | GET | `/_ts/admin/ec` | [`handle_admin_ec_lookup`] |
//! | GET | `/_ts/admin/ec/{id}` | [`handle_admin_ec_lookup`] |
//! | GET | `/_ts/admin/eids` | [`handle_admin_eids_lookup`] |
//! | GET | `/_ts/admin/eids` | [`admin_eids_lookup_retired`] |
//! | POST | `/_ts/api/v1/batch-sync` | [`handle_batch_sync`] |
//! | GET | `/_ts/api/v1/identify` | [`handle_identify`] |
//! | GET | `/_ts/set-tester` | [`handle_set_tester`] |
Expand Down Expand Up @@ -107,9 +107,9 @@ 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,
admin_eids_lookup_retired, deny_admin_diagnostic_fallback, handle_admin_ec_lookup,
};
use trusted_server_core::ec::batch_sync::handle_batch_sync;
use trusted_server_core::ec::consent::ec_consent_withdrawn;
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 Down Expand Up @@ -455,7 +451,6 @@ fn build_ec_request_state(
ec_context,
kv_graph,
finalize_kv_graph,
eids_cookie,
sharedid_cookie,
is_real_browser,
services: services.clone(),
Expand Down Expand Up @@ -579,7 +574,7 @@ async fn execute_named(
let kv = crate::maybe_identity_graph(&state.settings);
handle_admin_ec_lookup(kv.as_ref(), &registry, &req)
}
NamedRouteHandler::AdminEidsLookup => handle_admin_eids_lookup(&registry, &req),
NamedRouteHandler::AdminEidsLookup => Ok(admin_eids_lookup_retired()),
_ => unreachable!("admin diagnostics should use early dispatch"),
})
.unwrap_or_else(|error| http_error(&error));
Expand Down Expand Up @@ -731,7 +726,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 +740,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 @@ -2552,13 +2545,14 @@ mod tests {

let response = route(&router, request);

assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.status(), StatusCode::GONE);
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(
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
20 changes: 5 additions & 15 deletions crates/trusted-server-adapter-spin/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ use trusted_server_core::cache_policy::EdgeCacheHeader;
use trusted_server_core::config_payload::settings_from_config_blob;
use trusted_server_core::ec::EcContext;
use trusted_server_core::ec::admin::{
admin_ec_lookup_not_supported as core_admin_ec_lookup_not_supported,
deny_admin_diagnostic_fallback, handle_admin_eids_lookup,
admin_ec_lookup_not_supported as core_admin_ec_lookup_not_supported, admin_eids_lookup_retired,
deny_admin_diagnostic_fallback,
};
use trusted_server_core::ec::registry::PartnerRegistry;
use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError};
use trusted_server_core::http_util::sanitize_forwarded_headers;
use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput};
Expand Down Expand Up @@ -645,18 +644,9 @@ 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.
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)))
}
};
// Retain authenticated local denial, never publisher fallback.
let admin_eids_handler =
|_ctx: RequestContext| async { Ok::<Response, EdgeError>(admin_eids_lookup_retired()) };

// /auction
let s = Arc::clone(&state);
Expand Down
Loading
Loading