Skip to content

feat: per-project origin allowlisting for event ingestion - #2

Merged
Athenox14 merged 1 commit into
mainfrom
claude/iris-claude-mentions-uncny6
Jul 13, 2026
Merged

feat: per-project origin allowlisting for event ingestion#2
Athenox14 merged 1 commit into
mainfrom
claude/iris-claude-mentions-uncny6

Conversation

@Athenox14

@Athenox14 Athenox14 commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Currently project_id is a free-form string: any site that knows it can post events under it (confirmed: no auth, CorsLayer::very_permissive(), no api key anywhere in the codebase). This adds an opt-in "site verification" layer like Plausible/PostHog use, without touching the existing (fully open) GraphQL admin surface.
  • New upsertProject(id, name, allowedOrigins) GraphQL mutation registers the browser origins a project is actually embedded on, and a project(id) query to read it back.
  • Once a project has at least one registered origin, /capture, /batch, /replay, and the capture GraphQL mutation reject requests (403) whose Origin header (falling back to Referer) doesn't match one of them.
  • A project that's never been registered, or registered with an empty allowedOrigins, stays fully open — no breaking change for existing deployments.
  • Documented honestly: this is not a secret. Origin/Referer are just headers, so a non-browser client (curl, a script) that already knows the project_id and expected origin can still spoof them. It raises the bar against browser-context abuse (a page on site B silently posting events under site A's project_id in a visitor's browser), same limitation as the equivalent feature in Plausible/PostHog.

Changes

  • crates/iris-core/src/origin.rs (new): is_origin_allowed, the core matching logic (Origin header, or origin parsed from Referer, against a project's allowlist), with unit tests.
  • crates/iris-core/src/models.rs: new Project { id, name, allowed_origins }.
  • crates/iris-server/src/storage.rs: new projects table + upsert_project/get_project/allowed_origins.
  • crates/iris-server/src/capture.rs: origin check on /capture, /batch (checked per unique project_id in the batch), /replay.
  • crates/iris-server/src/schema.rs: project query, upsertProject mutation, origin check on the capture mutation (reads Origin/Referer from request headers via GraphQL context data).
  • crates/iris-server/src/main.rs: switches /graphql from post_service(GraphQL::new(schema)) to a small handler that injects request headers into the GraphQL request's context data, so the capture resolver can read them.
  • crates/iris-server/Cargo.toml: bumps axum from 0.7 to 0.8 to match what async-graphql-axum already pulls in transitively — needed to avoid two incompatible copies of axum's Handler/IntoResponse traits in the dependency tree once the GraphQL route uses axum extractors.
  • README.md / DOC.md: document the new (optional) origin allowlisting and its limitations.

Test plan

  • cargo build --workspace / cargo test --workspace — clean build, all tests pass (including new origin::tests).
  • Ran the server locally end-to-end:
    • unregistered project: /capture accepted regardless of Origin (back-compat).
    • registered project with allowedOrigins: ["https://oxalisheberg.fr"]: /capture returns 403 for a mismatched Origin, 200 for the matching one, 403 when Origin/Referer are both absent, and 200 when only a matching Referer is present (fallback path).
    • capture GraphQL mutation: same 403/success behavior based on Origin.
    • project(id) query returns the registered allowedOrigins.
    • Rebased onto main after PR feat: add GeoIP session location (country/city/coordinates from IP) #1 (GeoIP) merged — verified both features coexist (origin check + sessionGeo query both work on the same request/project).

Registering a project's allowed origins (upsertProject) makes /capture, /batch,
/replay, and the capture GraphQL mutation reject requests whose Origin/Referer
doesn't match one of them (403). Unregistered projects, or ones with no origins
configured, stay open — this doesn't break existing deployments. Mirrors the
"site verification" model Plausible/PostHog use: it stops a page on another
site from silently posting events under a different project_id in a visitor's
browser, though it's not a substitute for a secret since a non-browser client
can still set Origin/Referer to whatever it wants.

Bumps iris-server's axum dependency from 0.7 to 0.8 to match what
async-graphql-axum already pulls in, avoiding two copies of axum in the
dependency tree (needed so the capture mutation's resolver can read request
headers via GraphQL context data).
@Athenox14
Athenox14 force-pushed the claude/iris-claude-mentions-uncny6 branch from d6c2f0f to cb22f83 Compare July 13, 2026 19:11
@Athenox14
Athenox14 merged commit 89efe5e into main Jul 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants