Skip to content

fix(security): single-source the request authorization resolver (REST dropped sys_user_role) - #2341

Merged
os-zhuang merged 1 commit into
mainfrom
fix/unify-authz-resolver
Jun 26, 2026
Merged

fix(security): single-source the request authorization resolver (REST dropped sys_user_role)#2341
os-zhuang merged 1 commit into
mainfrom
fix/unify-authz-resolver

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What & why

A security/correctness root-fix. The REST server (@objectstack/rest) and the runtime dispatcher (@objectstack/runtime) each carried their own copy of the request → ExecutionContext identity/role resolver. On a security path, duplicated logic drifts — and it did:

The REST copy silently omitted sys_user_role (so custom roles granted via the ADR-0057 D4 platform-RBAC path — the only way to assign custom roles, since sys_member.role is restricted to owner/admin/member — did not apply over REST), plus sys_role_permission_set, the owner→org_owner membership normalization, the platform-admin derivation, and the ai_seat synthesis.

Fail-closed (legitimate access denied / RLS under-applied), not a privilege escalation — but real: RBAC via sys_user_role was a no-op on REST.

Found by dogfooding #2338: a freshly-provisioned user granted contributor via sys_user_role got 403 "for roles []" on the REST data path, while the dispatcher path resolved the role correctly.

The most-thorough fix (not a band-aid)

Don't keep two mirrors in sync on security code — collapse to one. This finishes the extraction the API-key verifier already modelled (resolveApiKeyPrincipal already lived in @objectstack/core and was shared by both transports):

  • @objectstack/core/security — new resolveAuthzContext (the complete session + role/permission aggregation: sys_member + sys_user_role + sys_user_permission_set + sys_role_permission_set, platform-admin derivation, ai_seat) and resolveLocalizationContext. Core is the lowest common dependency of both rest and runtime, so there's no upward-import problem.
  • @objectstack/runtimeresolveExecutionContext now delegates (469 → 138 lines).
  • @objectstack/rest — the inline identity resolver delegates (drops its divergent ~180-line copy).
  • Guardrails — a contract test (resolve-authz-context.test.ts) locking every authorization source, and a lint gate (scripts/check-single-authz-resolver.mjs, wired into lint.yml) that fails CI on a future duplicate resolver or a dropped delegation.

Verification

  • Contract test 7/7 (custom role via sys_user_role · owner→org_owner · role-bound permission sets · platform_admin only-when-unscoped · ai_seat · anonymous).
  • Suites green: runtime 444/444, rest 130/130, core green. Full turbo build green. ESLint clean. check:authz-resolver passes.
  • Dogfooded: a user granted contributor via sys_user_role now resolves that role on the REST data path (previously roles []).

🤖 Generated with Claude Code

… dropped sys_user_role)
The REST server (@objectstack/rest) and the runtime dispatcher (@objectstack/runtime)
each carried their own copy of the request -> ExecutionContext identity/role resolver.
They drifted on a security path: the REST copy silently omitted sys_user_role (so custom
roles granted via the ADR-0057 D4 platform-RBAC path did not apply over REST),
sys_role_permission_set, the owner->org_owner membership normalization, the platform-admin
derivation, and the ai_seat synthesis. Fail-closed (legitimate access denied), not an
escalation.
Root-fix (finishes the extraction the API-key verifier already modelled): a single shared
resolver `resolveAuthzContext` in @objectstack/core/security — the lowest common dependency
of both rest and runtime. Both entry points now delegate; no parallel copies to keep in sync.
- core: resolveAuthzContext + resolveLocalizationContext (complete role/permission
aggregation) + a contract test asserting every authorization source is honored.
- runtime: resolveExecutionContext delegates (469 -> 138 lines).
- rest: rest-server's identity resolver delegates (drops its divergent ~180-line copy).
- scripts/check-single-authz-resolver.mjs + a lint CI step: guard against a future
duplicate resolver or a dropped delegation.
Verified: contract test 7/7; suites green (runtime 444/444, rest 130/130, core green);
full turbo build green; ESLint clean; dogfooded — a user granted `contributor` via
sys_user_role now resolves that role on the REST data path (previously roles []).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercelBot commented Jun 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJun 26, 2026 9:21am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation ci/cd dependencies Pull requests that update a dependency file tests tooling size/xl labels Jun 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/core, @objectstack/rest, @objectstack/runtime.

30 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/cloud-artifact-api.mdx(via packages/runtime)
  • content/docs/concepts/core/index.mdx(via @objectstack/core)
  • content/docs/concepts/core/plugins.mdx(via @objectstack/core)
  • content/docs/concepts/core/services.mdx(via @objectstack/core)
  • content/docs/concepts/implementation-status.mdx(via @objectstack/core, @objectstack/rest, @objectstack/runtime)
  • content/docs/concepts/north-star.mdx(via packages/core, packages/runtime)
  • content/docs/concepts/packages.mdx(via @objectstack/core, @objectstack/rest, @objectstack/runtime)
  • content/docs/concepts/webhook-delivery.mdx(via @objectstack/core)
  • content/docs/guides/ai-capabilities.mdx(via @objectstack/core)
  • content/docs/guides/api-reference.mdx(via @objectstack/rest, @objectstack/runtime)
  • content/docs/guides/authentication.mdx(via @objectstack/core, @objectstack/runtime)
  • content/docs/guides/cloud-deployment.mdx(via @objectstack/runtime)
  • content/docs/guides/contracts/index.mdx(via @objectstack/core)
  • content/docs/guides/deployment-vercel.mdx(via @objectstack/runtime)
  • content/docs/guides/driver-configuration.mdx(via @objectstack/runtime)
  • content/docs/guides/hook-bodies.mdx(via @objectstack/runtime)
  • content/docs/guides/kernel-services.mdx(via @objectstack/core)
  • content/docs/guides/objectql-migration.mdx(via @objectstack/core)
  • content/docs/guides/packages.mdx(via @objectstack/core, @objectstack/rest, @objectstack/runtime)
  • content/docs/guides/plugin-chatbot-integration.mdx(via @objectstack/runtime)
  • content/docs/guides/plugin-development.mdx(via @objectstack/core)
  • content/docs/guides/plugins.mdx(via @objectstack/core, @objectstack/rest)
  • content/docs/guides/production-readiness.mdx(via @objectstack/runtime)
  • content/docs/guides/runtime-services/examples.mdx(via @objectstack/core)
  • content/docs/guides/single-project-mode.mdx(via @objectstack/runtime)
  • content/docs/protocol/objectos/config-resolution.mdx(via @objectstack/core)
  • content/docs/protocol/objectos/http-protocol.mdx(via @objectstack/runtime)
  • content/docs/protocol/objectos/index.mdx(via @objectstack/core, @objectstack/runtime)
  • content/docs/protocol/objectos/lifecycle.mdx(via @objectstack/core, @objectstack/runtime)
  • content/docs/protocol/objectos/plugin-spec.mdx(via @objectstack/core)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit aa33b02 into mainJun 26, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the fix/unify-authz-resolver branch June 26, 2026 09:29
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang