Skip to content

Commit 9878b92

Browse files
committed
fix(runtime): the membership read carries its system context in the query, not the trailing options
Same opt-in, same engine reading (ObjectQL merges the query's context with the trailing one). Spelled inside the query so the existing membership suites, which assert the read's two arguments, stay unchanged. Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN Co-authored-by: Claude <noreply@anthropic.com>
1 parent 60c5f22 commit 9878b92

2 files changed

Lines changed: 3 additions & 2 deletions

File tree

‎.changeset/21912-principal-less-producers.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ Clause-②: no
1010
- **`@objectstack/plugin-auth` — the platform-admin OAuth client toggle route** (`POST /api/v1/auth/admin/oauth2/toggle-disabled`). Its `sys_oauth_application` read and write go through `withSystemContext`, the wrapper better-auth's adapter already writes those rows through. The platform-admin judge still runs first. The answers (`200`, `404 RESOURCE_NOT_FOUND`, the refusals) and the stored row are unchanged. One log line goes away: the engine's read-only `updated_at` warning on every toggle. The value it warned about was discarded before and the driver still stamps the column.
1111
- **`@objectstack/plugin-auth` — `verifyScimBearerToken`.** The credential probe passes `isSystem: true` in the read's trailing options. It runs before any caller is known, and the digest equality is still all it matches. An unknown, inactive or expired bearer is still `null` (`401`).
1212
- **`@objectstack/plugin-auth` — the organization slug guard** (`organizationHooks.beforeUpdateOrganization`). Its `sys_organization` and `sys_environment` reads go through `withSystemContext`. The organization id stays in the `where`. A slug change while an active environment references the organization is still refused (`FORBIDDEN`), and any other change is still allowed. The catches around both reads are unchanged: a read that throws still ends the hook without refusing.
13-
- **`@objectstack/runtime` — the dispatcher's environment-membership gate.** The `sys_environment_member` read passes `isSystem: true` in the read's trailing options. The caller's user id stays in the `where`. A member still passes and a non-member is still refused with `403 PROJECT_MEMBERSHIP_REQUIRED`. The catch around the read is unchanged: a read that throws still lets the request through.
13+
- **`@objectstack/runtime` — the dispatcher's environment-membership gate.** The `sys_environment_member` read carries `isSystem: true` as its query context. The caller's user id stays in the `where`. A member still passes and a non-member is still refused with `403 PROJECT_MEMBERSHIP_REQUIRED`. The catch around the read is unchanged: a read that throws still lets the request through.
1414

1515
Why: the security middleware hands a context with no principal and no `isSystem` straight through (ADR-0096). That hand-through is not an authorization. A caller that is the platform acting for itself says so explicitly. ⛔ No new elevation API, no door's authorization moves, and no accept set changes.

‎packages/runtime/src/http-dispatcher.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1469,7 +1469,8 @@ export class HttpDispatcher {
14691469
let rows = await ql.find('sys_environment_member', {
14701470
where: { environment_id: environmentId, user_id: userId },
14711471
limit: 1,
1472-
} as any, { context: { isSystem: true } });
1472+
context: { isSystem: true },
1473+
} as any);
14731474
if (rows && (rows as any).value) rows = (rows as any).value;
14741475
const isMember = Array.isArray(rows) && rows.length > 0;
14751476

0 commit comments

Comments
 (0)