Skip to content

fix(auth): resolve model permissions off the target org's role, not the session's active-org role#4944

Merged
pedrofrxncx merged 1 commit into
mainfrom
fix/model-permissions-target-org-role
Jul 21, 2026
Merged

fix(auth): resolve model permissions off the target org's role, not the session's active-org role#4944
pedrofrxncx merged 1 commit into
mainfrom
fix/model-permissions-target-org-role

Conversation

@pedrofrxncx

@pedrofrxncx pedrofrxncx commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Source: bug found while auditing remaining ctx.auth.user?.role call sites after #4934/#4926/#8ece109 fixed the same active-org-vs-target-org role confusion elsewhere (member-add, member-update-role, AuthTransport's connection AccessControl).

Why a maintainer wants this: AI_PROVIDER_KEY_LIST, AI_PROVIDERS_LIST_MODELS, and the decopilot chat dispatch / GET /api/:org/decopilot/allowed-models paths all resolve model permissions with fetchModelPermissions(db, org.id, ctx.auth.user?.role). ctx.auth.user.role reflects the caller's role in their session's active organization (documented on OrganizationScope.role in studio-context.ts), while org/organization here is the path-resolved target org set by resolveOrgFromPath — every org-scoped route, including these MCP tool calls, runs through it. The two can differ.

Failure scenario: a user who is admin (unrestricted models) in their session's active org but holds only a custom, model-restricted role in a different org B calls AI_PROVIDER_KEY_LIST / AI_PROVIDERS_LIST_MODELS / GET /api/:org/decopilot/allowed-models scoped to org B. fetchModelPermissions resolves permissions using the stale active-org role instead of the caller's real role in B, silently bypassing B's model restrictions (they see/use provider keys and models their B-role should have blocked).

Fix: prefer the path-resolved org.role / organization.role, falling back to ctx.auth.user?.role only when it's unset — the exact pattern mcp-clients/outbound/transports/auth.ts already uses for its connection-scoped AccessControl (ctx.organization?.role ?? ctx.auth.user?.role).

Net change: +12/-4 across 3 files, one concern (role source for model-permission checks).

Reviewer verification: bunx tsc --noEmit in apps/mesh; the existing bun test apps/mesh/src/api/routes/decopilot/model-permissions.test.ts still passes (untouched pure logic, only the caller-supplied role changed). No test added — same as the precedent fix in #4934, this needs a real Postgres-backed org/member setup to exercise end-to-end and isn't unit-testable without a stubbed StudioContext (banned per TESTING.md); full e2e coverage runs in CI.

Locally run: bun run fmt, cd apps/mesh && bunx tsc --noEmit, bun test apps/mesh/src/api/routes/decopilot/model-permissions.test.ts — all green. Full CI (lint, e2e) validates the rest.

Note: dispatch-run.ts's analogous call site (line ~915) was deliberately left alone — it's shared by multiple entry points (HTTP, durable-workflow resume, legacy automation) where the relationship between ctx.organization and input.organizationId isn't uniformly guaranteed, so a blind swap there risks introducing a new mismatch rather than fixing one. Also left org-sso.ts untouched — the same-tick reactive session already opened #4943 for it.


Summary by cubic

Fixes model-permission checks to use the target org’s role, not the session’s active-org role, preventing cross-org permission bypass. Affects AI_PROVIDER_KEY_LIST, AI_PROVIDERS_LIST_MODELS, and GET /api/:org/decopilot/allowed-models.

  • Bug Fixes
    • Pass organization.role ?? ctx.auth.user?.role to fetchModelPermissions in decopilot/routes.ts, ai-providers/key-list.ts, and ai-providers/list-models.ts.
    • Prevents users with an active-org admin role from seeing models/keys in another org where they have a restricted role.

Written for commit 5952ebd. Summary will update on new commits.

Review in cubic

…he session's active-org role

AI_PROVIDER_KEY_LIST, AI_PROVIDERS_LIST_MODELS, and the decopilot
chat/allowed-models paths all call fetchModelPermissions(db, org.id,
ctx.auth.user?.role). But ctx.auth.user.role reflects the caller's role in
their session's ACTIVE organization, while `org`/`organization` here is the
path-resolved target org (set by resolveOrgFromPath) — the two can differ,
as already documented on OrganizationScope.role and fixed for role-assignment
in member-add.ts/member-update-role.ts (#4934) and for the AccessControl
bypass in mcp-clients/outbound/transports/auth.ts.

Failure scenario: a user who is "admin" (unrestricted models) in their active
org but only holds a custom, model-restricted role in a different org B calls
AI_PROVIDER_KEY_LIST / AI_PROVIDERS_LIST_MODELS / GET
/api/:org/decopilot/allowed-models scoped to org B. fetchModelPermissions
resolves permissions using the stale active-org role instead of the caller's
real role in B, bypassing B's model restrictions.

Fix: prefer the path-resolved `org.role` / `organization.role`, falling back
to ctx.auth.user?.role only when it's unset — the same pattern already used
in AuthTransport's connection-scoped AccessControl construction.
pedrofrxncx added a commit that referenced this pull request Jul 21, 2026
…n dispatch-run (#4947)

Follows #4943/#4944 hardening. `fetchModelPermissions` in dispatchRunAndWait's
resume/automation re-entry path used `ctx.auth.user?.role` (the session's
active-org role) instead of the path-resolved role for `input.organizationId`,
letting an owner/admin of one org bypass model-tier restrictions when a
dispatch targets a different, lower-privileged org. #4944 already fixed the
same fetchModelPermissions call in routes.ts/key-list.ts/list-models.ts but
missed this call site.
@pedrofrxncx
pedrofrxncx enabled auto-merge (squash) July 21, 2026 17:34
@pedrofrxncx
pedrofrxncx merged commit 8daca71 into main Jul 21, 2026
21 of 22 checks passed
@pedrofrxncx
pedrofrxncx deleted the fix/model-permissions-target-org-role branch July 21, 2026 17:35
decocms Bot pushed a commit that referenced this pull request Jul 21, 2026
PR: #4944 fix(auth): resolve model permissions off the target org's role, not the session's active-org role
Bump type: patch

- decocms (apps/mesh/package.json): 4.83.8 -> 4.83.9

Deploy-Scope: server
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.

1 participant