Add pluggable Edge Cookie, device, and geo providers gated by a permission model#838
Open
jwrosewell wants to merge 3 commits into
Open
Add pluggable Edge Cookie, device, and geo providers gated by a permission model#838jwrosewell wants to merge 3 commits into
jwrosewell wants to merge 3 commits into
Conversation
a07da17 to
33c77be
Compare
…ssion model
Replace three hard-wired request-time decisions with
configuration-selected providers behind small traits: Edge Cookie
identity (EdgeCookieProvider), the device classification the bot gate
uses (DeviceProvider), and geolocation (PlatformGeo). Gate a provider's
execution on a technical permission model that separates legal policy
from the core, and remove the country-based allows_ec_creation
branching.
Permissions are the single currency every service and provider reads,
and none reads consent directly. Consent is one of several sources (the
country and region baseline, a consent signal, a configuration decision,
or data from elsewhere) that set a request's permissions. Feature and
provider code works against a clean, stable set that does not change as
laws or consent frameworks change; a source needing a new distinction
adds a permission to the model rather than leaking a branch into
consumers. The country and region rules live in a human-editable
permissions.yaml embedded into the build (include_str! in
permissions.rs), so a policy owner changes them in version control
rather than in code. It defines named groups that each spell out every
permission flag (gdpr-eu, gdpr-uk, us-opt-out) and rules mapping a
country or country/state to a group, with optional +permission (granted)
or -permission (denied) overrides. Rule keys use the ISO 3166-1 alpha-2
country and ISO 3166-2 subdivision codes a geo provider returns, matched
case-insensitively, so any geo provider feeds the same rules without
translation. PermissionMaps::standard() parses the file once into a
static cache and hands out a shared reference (no per-request clone). A
request that matches no rule, or whose country the geo provider cannot
resolve, uses the deployer's [geo] default_country. Trusted Server does
not assume a jurisdiction, so that default is required and validated at
startup: startup fails when it is unset or does not resolve to a known
rule. The shipped example uses the most protective baseline (FR,
GDPR-EU, where every permission requires a signal); a deployer sets the
jurisdiction that governs its traffic. Because policy is a declarative
map rather than branching code, a third party can read permissions.yaml
and see exactly what each permission resolves to for any country or
region.
Control stays in the core. A provider advertises the permissions its
data use requires (required_permissions()), and the core, not the
provider, decides whether it runs: it resolves the request's
PermissionState once (in EcContext) and refuses to execute a provider
whose required permissions are not set, so generate_if_needed returns
before the provider is even built.
Route every Trusted Server data decision through the resolved
permissions, not Edge Cookie creation alone. Bidstream EID transmission
gates on the resolved store-on-device and select-personalised-ads
permissions (gate_eids_by_permissions) instead of re-inspecting TCF and
a raw gdpr_applies flag. A request whose permissions do not allow the
Edge Cookie always has its EC response headers stripped, but the
destructive step (expire the browser cookie, delete the consent KV
entry, write the identity-graph tombstone) fires only on an explicit
withdrawal signal (ec_storage_withdrawn: a TCF record refusing storage,
or a US-style opt-out), never on a merely not-permitted state. This
holds on every path: ec_finalize_response, the publisher proxy
(apply_ec_headers), and both adapter bot gates. A pre-consent or
fail-closed request therefore suppresses the Edge Cookie for that
response but does not destroy an already-issued identifier before the
user has had the chance to consent. The jurisdiction-gated
has_explicit_ec_withdrawal and ec_consent_withdrawn are gone.
Map consent to a per-permission signal in one place (permission_signal).
A TCF record is authoritative wherever present, so the EU defers to the
CMP; a US-style opt-out (GPC, GPP sale opt-out, US Privacy opt-out)
revokes a granted baseline, and the map decides where a revoke has an
effect. Precedence is explicit: a present TCF record wins over a
US-style opt-out. Downstream RTB partners (Prebid, APS) still receive
the full regulatory context forwarded verbatim and run their own
enforcement; the permission model governs only Trusted Server's own
actions.
Wire providers by dependency injection. A provider reads request
evidence through the RequestInfo and HostSignals traits (evidence.rs)
rather than a fixed field struct, borrowed at call time so no
per-request HeaderMap is cloned. RequestInfo carries request data any
host can supply (client IP, User-Agent, headers); HostSignals carries
the host-computed TLS JA4 and HTTP/2 fingerprints and is opt-in, so a
neutral provider triggers no host fingerprint call. The adapter is the
composition root: it captures host signals once at the entry point and
supplies them, and a provider that needs a service the host cannot
supply cannot be built, so the request stops rather than minting a
degraded identifier. Core defines the traits and the neutral built-in
defaults and never calls a host SDK.
Support two Edge Cookie provider shapes behind the one trait. A
server-side provider mints at the edge: the built-in HMAC provider (over
the client IP), or a host-signal provider that derives the identifier
from the host fingerprints and client IP on any host that supplies a
HostSignals. A client-side provider defers on the page request, lets the
browser do the work, and posts the result to a new POST
/_ts/api/v1/ec/resolve endpoint that mints the cookie from the verified
value on its own response; verifying the posted value is the provider's
responsibility, so a client cannot forge an Edge Cookie. A neutral
client-fixed demo provider (client and server share one fixed known word
the server verifies) exercises the path end to end with a page script
shipped in the tsjs bundle when it is selected, and is for testing only.
Move the opt-in Fastly device and host geo providers into their own
crates (crates/device/fastly, crates/geo/fastly), selected and injected
by the adapter, so core keeps only the DeviceProvider and PlatformGeo
traits and the neutral defaults. The Fastly device provider reads the
User-Agent from the borrowed RequestInfo and the fingerprints from a
FastlyHostSignals built from the live request, and the adapter shares
that FastlyHostSignals as the host-signal service so the host-signal
Edge Cookie provider can use it too. A placeholder crates/edgecookie
directory marks where vendor Edge Cookie provider crates will live.
Restructure the [ec] configuration section (breaking change). The flat
[ec] passphrase field is removed and [ec] rejects unknown fields, so a
config carrying it fails to parse; the HMAC key moves to
[ec.providers.hmac] passphrase. Edge Cookie identity is off by default:
EC runs only when [ec] provider names a configured provider, so a config
that adds [ec.providers.hmac] but omits provider = "hmac" parses cleanly
and runs statelessly. Migration for an existing deployment:
[ec]
provider = "hmac"
[ec.providers.hmac]
passphrase = "your-existing-passphrase"
The provider selectors ([ec]/[device]/[geo] provider) and the mandatory
[geo] default_country are validated inside
Settings::finalize_deserialized, the single point every deserialization
path shares (TOML file, the runtime config-store blob, and the ts CLI
config push), so no load path can bypass the checks.
trusted-server.example.toml is the checked-in template and shows the new
sections. The default jurisdiction baseline is logged once per settings
load so an operator can see which permissions the unmatched-request
default grants without a signal.
Add an IntegrationResponseMutator hook for outbound response headers. No
built-in integration registers one yet; the registry carries the hook so
an integration crate (for example bot protection emitting Accept-CH) can
register a mutator without a core change.
Standardize spelling on US English, recorded as the convention in
CLAUDE.md, keeping external-source terms such as the IAB TCF purpose
names as their source spells them. Add the provider-architecture and
permission-model sections to CLAUDE.md, and document the model, the
provider selectors, and the permission sources in the guides.
Cover Windows in CI. The Rust adapter test jobs run on both
ubuntu-latest and windows-latest. The Docker-based integration suite and
the Cloudflare worker build remain Linux tools, run through WSL on
Windows as CLAUDE.md documents. The integration app-config fixture
selects the HMAC provider and sets [geo] default_country = US/CA, so a
request with no resolvable geolocation under Viceroy maps to a US
opt-out baseline and the GPC-withdrawal scenario exercises
permission-driven cookie expiry.
The Axum test job now runs on a windows-latest matrix entry alongside ubuntu-latest, to verify the Axum adapter build and tests on Windows. That job also runs the operator CLI test with an explicit host target (x86_64-unknown-linux-gnu), which the Windows runner does not install, so the step failed on Windows with a missing-std error. Gate the CLI step to the Linux runner. The Windows matrix entry still builds and tests the Axum adapter and verifies the Fastly wasm release build; the host-target CLI test keeps its existing Linux-only coverage.
33c77be to
f24e19b
Compare
The main branch ruleset requires a status check named exactly `cargo test`. This PR had added a Windows build matrix to the test-rust job, which renamed its check context to `cargo test (ubuntu-latest)` and `cargo test (windows-latest)` and left the required `cargo test` context unreported, so the PR stayed blocked with "Expected, waiting for status to be reported". Revert test-rust to its Linux-only form so it emits the `cargo test` context again. Windows coverage remains through the axum-native, cloudflare, and CLI jobs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pluggable Edge Cookie, device, and geo providers gated by a permission model
What this delivers
Trusted Server makes several data decisions on every request: whether to create or keep a first-party Edge Cookie identity, how to classify the device, whether to resolve geolocation, and whether to pass identity into the bidstream. Today those decisions are hard-wired. This PR makes each one a choice a publisher configures rather than a behavior baked into the code, and puts every decision behind a single, auditable permission model.
Two changes sit at the center:
Pluggable providers. Edge Cookie identity, device classification, and geolocation each become a selectable component. A deployment picks the implementation that suits it (including none), and can adopt a vendor's implementation later without a code change to Trusted Server. Out of the box the defaults are deliberately neutral: no Edge Cookie is created, device classification uses only the User-Agent, and no geolocation is performed, so a default deployment makes no third-party or host-specific call.
A permission model as the single source of truth. What Trusted Server is allowed to do for a given request is resolved from a human-editable policy file (
permissions.yaml) that maps countries and regions to a set of permissions, combined with the request's privacy signals (TCF, GPP, GPC, US Privacy). Every data decision reads that resolved permission set. Legal and jurisdictional policy lives in that reviewable file, not scattered through the code.Why it matters
Trust and auditability. Because policy is a declarative, version- controlled map, a publisher, auditor, or regulator can open one file and see exactly what is permitted for any country or region, and track every change over time. Just as important, the core decides whether a provider may run based on the permissions that provider declares it needs. A provider cannot authorize itself, so the trust boundary stays in code the publisher controls plus the policy they can read.
Adaptability without re-engineering. Privacy law and consent frameworks change constantly. A new state law or signal is absorbed by editing the policy map, not by threading another special case through the request path.
Privacy-protective by default and by change. The neutral defaults do nothing until a publisher opts in. Where behavior does change, it is strictly more protective: a privacy opt-out now also removes identity from the bidstream, and an opt-out is honored even where geolocation is unavailable. An affirmative withdrawal expires the identity, while a visitor who simply has not yet made a choice is never stripped of an existing identity before they get the chance to decide.
Scope: Trusted Server vs downstream partners
The permission model governs the decisions Trusted Server itself makes. It does not reach into the compliance logic of downstream real-time-bidding partners (Prebid, APS, SSPs), which operate outside the Trusted Server environment and receive the full, unmodified regulatory context to make their own decisions. Nothing is hidden; the two responsibilities are kept separate on purpose.
What a deployer needs to know (breaking change)
The Edge Cookie configuration is restructured, so existing deployments need a one-time config update:
[ec] provider = "hmac"to keep the built-in behavior active.[ec] passphraseto[ec.providers.hmac] passphrase. A configuration still using the old form will be rejected at startup.[geo] default_country) is now required, so Trusted Server never assumes one for you. Startup fails until it is set. The shipped example uses the most protective baseline (FR, GDPR-EU).trusted-server.example.tomlshows the new layout, and the migration is spelled out in the commit message and the configuration guide.Confidence
Format, linting on all adapter targets, the full unit-test suites across every adapter (Fastly, Axum, Cloudflare, Spin), the cross-adapter parity suite, and the JavaScript suite all pass. The Fastly integration suite exercises the Edge Cookie lifecycle end to end, including a GPC-withdrawal scenario that expires the cookie.
Closes #777, #778, #779, #780, #781, #782. Relates to #784 (provider crate homes added; the full crates restructure remains). Issue #790 (select a scenario configuration at startup, backed by a build-time-baked
development.toml) is intentionally not part of this change: the configuration model it targets has been replaced onmainby the runtime config-store pipeline, so that issue should be re-scoped or closed against the new model.