feat(graphql-server): stamp actor entity attribution on token sessions - #1857
pyramation wants to merge 2 commits into
Conversation
Authenticated public GraphQL requests set jwt.claims.user_id/principal_id but never jwt.claims.entity_id/entity_type, so work created by a signed-in user (e.g. startExecution) had no entity attribution. Resolve the pair server-side via app_scope.actor_entity(database_id, user_id) in the auth middleware (principal credentials resolve to their owner) and stamp it in the token branch of the graphile context. Tenant databases without app_scope are unchanged; when it is present an unresolvable actor fails the request rather than proceeding entityless.
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
Review complete. 🟡 1 medium 💬 Inline comments (1)
🧹 Nitpicks (2) — 🟢 2 low
This PR introduces entity attribution for the GraphQL server: a resolver probes whether the tenant database has
Reviewed commit: 23b48dd |
There was a problem hiding this comment.
Adds actor-to-entity attribution middleware that resolves the acting entity per request and stamps it into the PostGraphile DB session claims for billing attribution.
Key findings
- 🟡 Negative
app_scopeprobe cached for process lifetime — auth.ts:38
This comment has been minimized.
This comment has been minimized.
…dule; re-probe the entity model on a TTL
|
Closing unmerged. |
Summary
Fixes https://github.com/constructive-io/constructive-planning/issues/2107.
Authenticated requests on the public GraphQL surface set
jwt.claims.user_id/jwt.claims.principal_idbut neverjwt.claims.entity_id/jwt.claims.entity_type— the anonymous branch (database), the private in-cluster branch (X-Entity-*headers) and the sync gateway all stamp the pair, the token branch didn't. So a signed-in user callingstartExecutionproduced an execution with no entity attribution and the agent run was refused downstream.The pair is now resolved server-side, the same way the sync gateway does it:
app_scope.actor_entitydoes that), so a principal is never an entity of its own. Nothing is accepted from client headers or mutation arguments on the public surface.hasEntityModel=app_scope.actor_entityis deployed andmetaschema_modules_public.users_modulehas a row for thisdatabase_id. A database with no users module (bare RLS module, the auth-onlygraphql/server-testfixtures) has no entity for an actor to carry, so no pair is stamped and it behaves exactly as before — that was theerror-events.integration.test.tsfailure on the first head, whereactor_entityraisedACTOR_ENTITY_UNRESOLVED: … installs no users module. The probe is re-run on a 60s TTL so a users module deployed while the server runs starts attributing without a restart.middleware/actor-entity.ts(createActorEntityResolver,hasEntityModel,pgActorEntityQuery) + tests covering user, principal→owner, unresolvable, no-app_scope, app_scope-without-users-module and anonymous paths.graphql/serverunit suite and botherror-events*integration suites pass locally.Follow-up in constructive-db once this publishes: regression test that
startExecutionover the public API as a user yields an execution/run carrying the entity pair.Link to Devin session: https://app.devin.ai/sessions/3c993d055ffb47f6be27862718a9cd42
Open in Devin Desktop: https://app.devin.ai/desktop/session/3c993d055ffb47f6be27862718a9cd42?variant=devin
Requested by: @pyramation