fix(security): pre-wiring identity admission for GraphQL + realtime surfaces (#2992, ADR-0096 D4) - #3013
Merged
Merged
Conversation
…urfaces (#2992, ADR-0096 D4) Two latent execution surfaces dropped/lacked the caller identity and would have fallen open the instant a real client transport was wired. Fix the identity story now and pin it in CI, per ADR-0096: GraphQL (surface 1 — context-drop, now threaded): - handleGraphQL resolved identity only under requireAuth and passed only { request } to kernel.graphql, dropping the ExecutionContext. It now resolves the caller identity even on the direct dispatcher-plugin route and even when requireAuth is off, and threads it as options.context — so the first real engine runs caller-scoped, never context-less (the security middleware falls OPEN on a missing principal). - IGraphQLService.execute documents the admission requirement: forward the context to every data-engine call as options.context. - Matrix row graphql-identity-thread + a source probe pin the threading: removing `context:` from the kernel.graphql call goes STALE → red CI. - Unit tests cover user/system/guest threading postures. realtime (surface 2 — no per-recipient authz seam, posture registered): - Matrix row realtime-delivery-authz registers the honest posture: pure fan-out, subscriptions carry no principal, full after-row payload — trusted server-internal subscribers only, with the admission requirement (per-recipient RLS/FLS/tenant re-check on delivery, or id-only payload + client re-fetch) stated on the row. - Transport TRIPWIRE probes (adapter handleUpgrade, plugin transport, dispatcher handleRealtime/Upgrade/Subscribe, client WebSocket/ EventSource, rest /realtime route) discover keys covered by NO row — wiring a transport fails CI as UNCLASSIFIED until the identity story ships with it. Ratchet-bites tests prove both new pins fire. - service-realtime README rewritten: it advertised authorizeChannel / broadcastToUser / presence auth / rooms that do not exist. It now documents the real surface and the security posture; the contract and the publish fan-out carry the same admission note at the seam. Closes #2992. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvQFjTKVdP5HUPxzcBvpCF
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 108 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…ealtime protocol status banner The page's planned wire protocol shows only a subscribe-time permission error; the admission requirement is per-delivery re-authorization (or id-only payloads). State it where a transport implementer will read first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CvQFjTKVdP5HUPxzcBvpCF
os-zhuang
marked this pull request as ready for review
July 16, 2026 05:12
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.
Closes #2992.
Two latent execution surfaces (neither wired to a client today) drop or lack the caller's identity, and would fall open the instant a real transport ships. Per ADR-0096 D4 and the issue's "fix before wiring" directive, this PR threads the GraphQL identity preemptively and registers both surfaces in the authz conformance matrix with CI ratchets that block wiring a transport without the identity story.
Surface 1 — GraphQL (latent context-drop → threaded)
handleGraphQLpassed only{ request }tokernel.graphql, dropping the resolvedExecutionContext. It now threads it asoptions.context— the same key the RESTcallDatapath uses — so the first real engine implementation runs caller-scoped instead of context-less (the security middleware falls open on a missing principal = full authority).requireAuthis off: an authenticated caller on an open deployment still runs under their own authority, and an anonymous caller resolves to an explicit guest principal (mirroringdispatch()), never a forged identity. The [security] Anonymous posture inconsistent across surfaces: dispatcher /graphql and raw hono /data bypass requireAuth (/meta now gated) #2567 anonymous-deny gate is unchanged.IGraphQLService.execute(spec) now documents the admission requirement: implementations MUST forward the context to every data-engine call asoptions.context./graphqlauth gate requested by the issue already landed in [security] Anonymous posture inconsistent across surfaces: dispatcher /graphql and raw hono /data bypass requireAuth (/meta now gated) #2567; this PR completes the identity story for the surface.Surface 2 — realtime (no per-recipient authz seam → posture registered + tripwired)
realtime-delivery-authz(stateexperimental) registers the honest posture: pure fan-out,Subscriptioncarries no principal,matchesSubscriptionfilters only object+eventTypes (options.filterdeclared but never read), engine publishes the fullafterrow — safe only while every subscriber is server-internal (webhook auto-enqueuer, knowledge sync). The row states the admission requirement: per-recipient RLS/FLS/tenant re-check on delivery (subscription carries the subscriber'sExecutionContext) or id-only payload + client re-fetch.service-realtime/README.mdrewritten: it advertisedauthorizeChannel,broadcastToUser, presence auth, rooms, and REST endpoints that do not exist. It now documents the real surface and its security posture. The contract (IRealtimeService) and the adapter'spublishfan-out carry the same admission note at the seam.The CI ratchet (D4 meta-test extension)
Extends the existing #2567
discover()probe table indogfood/test/authz-conformance.test.ts:kernel.graphql(...)call site is probed forcontext:in its options. Dropping the threading makes thegraphql-identity-threadrow'scoversgo STALE → red CI.publishis discovered and covered by therealtime-delivery-authzrow.handleUpgradein the adapter, WebSocket/SSE wiring in the realtime plugin,handleRealtime|Upgrade|Subscribehandlers in the dispatcher,new WebSocket/new EventSourcein the client'srealtime-api.ts, and a/realtimeroute literal inrest-server.ts. Any of these appearing → UNCLASSIFIED surface → red CI with a checklist, until the identity story is registered.Tests
packages/runtime— 532/532 pass (incl. 4 new identity-threading tests: user, system, requireAuth-off threading, anonymous-guest no-authority).packages/dogfood—authz-conformance.test.ts7/7; e2e proofshowcase-anonymous-deny-surfaces.dogfood.test.ts6/6.packages/speccontract tests +service-realtimesuite pass; touched packages build clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01CvQFjTKVdP5HUPxzcBvpCF
Generated by Claude Code