You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit b7b408e
Browse filesBrowse the repository at this point in the historyBrowse files
refactor(plugin-security): move the authorization middleware out of security-plugin.ts
The one statement of start() that registers the authorization
middleware on the ObjectQL engine moves unedited to
security-plugin-authz-middleware.ts as registerAuthzMiddleware. It closed
over start()'s ctx, ql and metadata and nothing else, so it takes them as
parameters; start() keeps the statement's position, the comment above it
and the log line after it, and calls it from one block that hands it a
host literal of exactly the members it reads. The two module-level
declarations only the handler reads move with it: WriteImageCheckSeam,
which gains export so that security-plugin.ts can import it back
type-only for a docblock link, and userFacingDenialMessage. Import names
only the moved code reads leave security-plugin.ts, each whole import
with the comment lines that describe it.
The two readers that key on the moved read are re-pointed: the
system-context page's rows 1, 2 and 30 anchor the new function, and the
census ledger row that declares collapsesOntoRead for the owner_id guard
names it too. Move-only: no verdict changes.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BoLT8Gaa8t9a7AWVH8qZa
Copy file name to clipboardExpand all lines: content/docs/permissions/system-context.mdx
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -96,9 +96,9 @@ that silently does not happen.
96
96
97
97
| # | Behaviour when `isSystem`| Package | What you get / what you lose | Anchor |
98
98
|:--|:---|:---|:---|:---|
99
-
| 1 |**The whole security middleware short-circuits** before any gate runs | plugin-security | Get: every CRUD/FLS/tenant/owner gate below skipped in one branch. Lose: every gate under it in this lane at once, down to and including the write-bypass row that widens the effective write scope to `org` (row 7) — this is the single largest behaviour on the page |`packages/plugins/plugin-security/src/security-plugin.ts#start`|
99
+
| 1 |**The whole security middleware short-circuits** before any gate runs | plugin-security | Get: every CRUD/FLS/tenant/owner gate below skipped in one branch. Lose: every gate under it in this lane at once, down to and including the write-bypass row that widens the effective write scope to `org` (row 7) — this is the single largest behaviour on the page |`packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts#registerAuthzMiddleware`|
100
100
| 1b |**A flow run's system context is REFUSED governed writes to the identity-assignment objects** — the one check ahead of the middleware's short-circuit | plugin-security | Get: nothing — an insert, update, delete, transfer, restore or purge of `sys_user_position`, `sys_user_permission_set`, `sys_invitation` or `sys_member` from a context carrying `isSystem` and a `flowRunId` (a `runAs: 'system'` flow's data nodes, at any depth of their writes) is refused `PERMISSION_DENIED` before the short-circuit skips the ADR-0090 D12 gate and the identity write guard. Lose: nothing a platform writer held — a system context without a `flowRunId` (the seeders, better-auth and its reconciliation, invitation placement) passes on to the short-circuit unchanged, reads are not judged, and a `runAs: 'user'` run carries no `isSystem`, so the D12 gate judges it as the user it acts for |`packages/plugins/plugin-security/src/flow-system-identity-write-refusal.ts#assertFlowSystemIdentityWriteAllowed`|
101
-
| 2 |**`owner_id` is not auto-stamped on INSERT** (the step 3.5 anchor guard is inside the block row 1 skips) | plugin-security | Lose: the row lands `owner_id = NULL`, so the default `owner_only_writes` policy hides it **from its own creator**. Get: nothing — this is a gap, not a capability | the step 3.5 guard block and the short-circuit that skips it are both inside `packages/plugins/plugin-security/src/security-plugin.ts#start`|
101
+
| 2 |**`owner_id` is not auto-stamped on INSERT** (the step 3.5 anchor guard is inside the block row 1 skips) | plugin-security | Lose: the row lands `owner_id = NULL`, so the default `owner_only_writes` policy hides it **from its own creator**. Get: nothing — this is a gap, not a capability | the step 3.5 guard block and the short-circuit that skips it are both inside `packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts#registerAuthzMiddleware`|
@@ -141,7 +141,7 @@ that silently does not happen.
141
141
| 29 | Reference-cleanup log attributes the write to `'system'`| objectql | Get: an honest actor label instead of `anonymous` when the context carries neither `userId` nor `actor`|`packages/objectql/src/engine.ts#recordReferenceCheckElevation`|
142
142
| 29b | A traversing validation rule's related read skips the **caller's bounds**| objectql | Get: no row bound from the caller's own read on a related object no organization wall scopes, no read door on a header the rule reads through a `master_detail` field, and a related read even with no organization under a walled posture (a `tenantId` on the context still scopes it). Every caller that is not system is held to all three: a header its read door does not return is unavailable to the rule exactly as a header that exists nowhere is, whatever it holds, and a header it can read is judged on its stored values. A plain `lookup` to an object an organization wall scopes asks no read door, system caller or not |`packages/objectql/src/engine.ts#resolveOneHopRelated`|
143
143
| 29c | A detail write's master-detail header read asks **no read door**| objectql | Get: the write binds the header a parent-scoped `requiredWhen` / `readonlyWhen` reads as `parent` from the header's stored row whoever can read it, as before. Every caller that is not system meets its own read door first: a header that read does not return answers as a header that exists nowhere — an unbound `parent`, and `reference_not_found` on the reference check (which a system caller's write skips altogether) |`packages/objectql/src/engine.ts#masterDetailHeadersCallerReads`|
144
-
| 30 |**Bulk data event `organizationId` OMITTED** — the batch is published "not asserted" | plugin-security | Get: nothing — the `data.records.*` event still publishes. Lose: the per-organization attribution: this exit is taken before the security middleware composes any tenant wall, so it records no Layer 0 verdict on the operation (`OperationContext.tenantLayer0Verdict`, #15813), and the engine's bulk producer — which reads that recorded verdict and nothing else — omits the key rather than filling it from the caller's `tenantId`; a tenant-scoped consumer then does not deliver the event inside an organization wall (#15225) |`packages/plugins/plugin-security/src/security-plugin.ts#start`|
144
+
| 30 |**Bulk data event `organizationId` OMITTED** — the batch is published "not asserted" | plugin-security | Get: nothing — the `data.records.*` event still publishes. Lose: the per-organization attribution: this exit is taken before the security middleware composes any tenant wall, so it records no Layer 0 verdict on the operation (`OperationContext.tenantLayer0Verdict`, #15813), and the engine's bulk producer — which reads that recorded verdict and nothing else — omits the key rather than filling it from the caller's `tenantId`; a tenant-scoped consumer then does not deliver the event inside an organization wall (#15225) |`packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts#registerAuthzMiddleware`|
145
145
| 30b |**The import doors admit a control opt-out** — `runAutomations: false` and `treatAsHistorical: true` on `POST /data/:object/import` and `/import/jobs`| rest | Get: an import whose rows are written with `skipAutomations` (no metadata-bound hook, no record-change trigger: an app's approvals and record freezes among them) or with `skipStateMachine` and `preserveAudit`. Every other caller, every signed-in user and administrator included, is refused `403 PERMISSION_DENIED` before the body is parsed, so nothing is written and no job is created. Inbound HTTP never carries the flag, so this is an in-process caller's door. Note: the flag still suppresses none of those controls by itself (see below); it only admits the opt-outs that do |`packages/rest/src/import-prepare.ts#importControlOptOutRefusal`|
146
146
| 30c |**The import undo keeps the control skips** — `POST /data/import/jobs/:jobId/undo`| rest | Get: an undo whose deletes and restores are written with `skipAutomations` and `skipStateMachine`, so no metadata-bound hook, record-change trigger or `state_machine` rule refuses a reversal. Every other caller, every signed-in user and administrator included, undoes under those controls: a reversal one of them refuses is reported per row in the undo answer's `failures` and kept in the job's undo log, and the job is stamped reverted only once every row is. A historical job's `preserveAudit` belongs to the job, not the caller, and rides every undo. Inbound HTTP never carries the flag, so this is an in-process caller's door |`packages/rest/src/data-action-routes.ts#importUndoWriteContext`|
0 commit comments