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
* feat(security)!: secure-by-default requireAuth flip + RBAC gates for transfer/restore/purge (#2561 P0)
Two production-launch blockers from the authorization gap map (#2561):
1. requireAuth default flip (ADR-0056 D2, enforce stage) — BREAKING
- spec: RestApiConfigSchema.requireAuth default(false) → default(true);
rest-server normalizeConfig mirrors (?? true). Anonymous /data/* is
now denied (401) unless the deployment explicitly opts out.
- rest-api-plugin: boot warning now fires only for the EXPLICIT
requireAuth:false opt-out, and reads the correct nested config path
(the flat read previously warned even when requireAuth was on).
- cli serve: keeps the deliberate carve-out — auth-less stacks get an
explicit false (nothing could authenticate against them), warned.
- verify harness: no override — every dogfood proof now runs on the
platform default; anonymous-deny + public-form survival prove the
flip (publicFormGrant was the pre-req that unblocked it).
- conformance matrix: requireAuth-default-flip experimental → enforced.
- REST/client unit tests that dispatch handlers anonymously opt out
explicitly (they test routing mechanics, not the auth gate).
2. #1883 — destructive lifecycle ops RBAC gate (pre-mapped)
- permission-evaluator: OPERATION_TO_PERMISSION now maps
transfer→allowTransfer, restore→allowRestore, purge→allowPurge;
modifyAllRecords super-user bypass covers the class. Ops still do
not exist in ObjectQL (M2) — but there is no ungated window when
they ship; unmapped destructive ops keep failing closed (ADR-0049).
- spec: bits re-described [EXPERIMENTAL — not enforced] →
[RBAC-gated; operation pending M2]; liveness ledger entries added.
- unit tests: deny-without-bit / allow-with-bit / no-cross-leak /
modifyAllRecords bypass.
Verified: full turbo test suite green (129 tasks); browser-driven
showcase run proves anonymous 401, authenticated 200, anonymous public
form 200/201 with the data API still closed to the same session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014y5kiH3aPLWtRRRGcVrXcT
* docs: honesty caveats for pending transfer/restore/purge ops in permission docs
The permissions-matrix cheatsheet and ObjectQL security protocol page listed
allowTransfer/allowRestore/allowPurge without noting the operations do not
exist yet (roadmap M2) — per the declared≠enforced honesty rule, both now
carry the pending-M2 / gate-pre-mapped caveat (#1883). Also documents the
modifyAllRecords bypass extension to the lifecycle class.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014y5kiH3aPLWtRRRGcVrXcT
* docs(adr): record FLS posture limitation + capability authoring-lint in ADR-0066 future refinements
Two findings from the 2026-07 pre-launch authorization assessment that had
no ADR home:
- ⑧ runtime FLS is block-list + most-permissive union — undeclared fields
are visible by default and a field-level deny cannot be expressed until
the muting layer (⑦) covers field grants;
- ⑨ capability strings need an authoring/publish-gate lint when the D1
registry lands (typos currently fail closed but undiscoverably).
Companion engineering issue (not ADR material): #2565 — surface swallowed
dbLoader failures in resolvePermissionSets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014y5kiH3aPLWtRRRGcVrXcT
* fix(security): address self-review findings on the requireAuth flip + destructive-op gates
Correctness (record-level enforcement for the pre-mapped destructive ops):
- security-plugin middleware: extend the write-scope stash, the #1994
pre-image RLS check, and the ADR-0055 controlled-by-parent write gate to
cover transfer/restore/purge — the object-level RBAC bit was gated but the
row-level defenses only listed update/delete/insert, so a granted user
could have destroyed out-of-scope rows by id when the M2 ops ship. purge
maps onto the delete RLS class, transfer/restore onto update. New unit
test proves a not-owned purge is denied by the pre-image check.
Callsite coverage for the default flip:
- cli serve: carve-out now keys on (tierEnabled('auth') || hasAuthPlugin) —
a stack mounting AuthPlugin explicitly under a minimal tier no longer gets
an accidental fail-open override.
- plugin-dev: mirror the serve carve-out — an auth-less dev stack gets an
explicit requireAuth:false so the local data API isn't bricked.
- spec: declare ObjectStackDefinitionSchema.api so the documented opt-out
(and the enableProjectScoping/projectResolution/enforceProjectMembership
knobs serve.ts already reads) survives defineStack strict parsing instead
of being silently stripped. Verified: defineStack now returns the api key.
- rest-api-plugin: warn only on an explicit opt-out; add a misplaced-key
guard for a flat api.requireAuth (silently ignored under the deny default).
- fix two in-tree tests that passed requireAuth at the wrong nesting.
Cleanup:
- derive the modifyAllRecords write-bypass key set from OPERATION_TO_PERMISSION
+ DESTRUCTIVE_OPERATIONS (module-level Set, no per-call array) so a future
destructive op is covered automatically.
Docs / provenance:
- regenerate the auto-gen references (requireAuth + allow*{Transfer,Restore,
Purge} describe text); CHANGELOG entry for the breaking flip; changeset
migration note updated to the defineStack-level api opt-out + scope note.
Pre-existing anonymous-posture gap on /meta, dispatcher /graphql, and raw
hono /data (surfaces that never call enforceAuth) filed as #2567 — out of
scope for this flip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014y5kiH3aPLWtRRRGcVrXcT
---------
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: content/docs/concepts/implementation-status.mdx
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -287,7 +287,7 @@ The `auth` service in `CoreServiceName` covers both **authentication** (identity
287
287
- Client SDK supports bearer token header — but token validation requires the auth plugin
288
288
- Auth route (`/auth/*`) only appears in Discovery when the auth plugin is registered
289
289
- Fine-grained authorization (RLS, sharing, territory) is internal to the auth plugin
290
-
- **Phase-1 RBAC enforcement is live end-to-end**: REST → ObjectQL → SecurityPlugin middleware now receives a populated `ExecutionContext` (userId, tenantId, roles, permissions). Default `member_default` permission set ships a wildcard RLS rule `organization_id == current_user.organization_id` plus per-object overrides `sys_organization_self` (`id == current_user.organization_id`) and `sys_user_self` (`id == current_user.id`) for the global tables that lack an `organization_id` column. The earlier `tenantField` indirection (RLS expressions written against an abstract `tenant_id` column then rewritten to the configured physical column at compile time) was removed — the placeholder, the column name, and `RLSUserContext.organization_id` are now the same name end-to-end. The legacy `objectql.registerTenantMiddleware` (hardcoded `where.tenant_id` injection that pre-dated SecurityPlugin) has been removed; SecurityPlugin is the sole authority for tenant isolation. Analytics now uses the same reusable read scope via `security.getReadFilter`, so dataset-bound dashboards/reports do not bypass RLS. Verified cross-organization isolation on `pnpm dev:crm` across `sys_organization`, `sys_member`, `sys_user`, `sys_user_permission_set`, `sys_role_permission_set`. **Anonymous traffic still falls open by default** (the default-deny flip is release-gated); since ADR-0056 D2 the server logs a boot-time warning when `requireAuth` is unset, and public forms no longer depend on the fall-open — they carry a declaration-derived `publicFormGrant` (ADR-0056 Option A).
290
+
- **Phase-1 RBAC enforcement is live end-to-end**: REST → ObjectQL → SecurityPlugin middleware now receives a populated `ExecutionContext` (userId, tenantId, roles, permissions). Default `member_default` permission set ships a wildcard RLS rule `organization_id == current_user.organization_id` plus per-object overrides `sys_organization_self` (`id == current_user.organization_id`) and `sys_user_self` (`id == current_user.id`) for the global tables that lack an `organization_id` column. The earlier `tenantField` indirection (RLS expressions written against an abstract `tenant_id` column then rewritten to the configured physical column at compile time) was removed — the placeholder, the column name, and `RLSUserContext.organization_id` are now the same name end-to-end. The legacy `objectql.registerTenantMiddleware` (hardcoded `where.tenant_id` injection that pre-dated SecurityPlugin) has been removed; SecurityPlugin is the sole authority for tenant isolation. Analytics now uses the same reusable read scope via `security.getReadFilter`, so dataset-bound dashboards/reports do not bypass RLS. Verified cross-organization isolation on `pnpm dev:crm` across `sys_organization`, `sys_member`, `sys_user`, `sys_user_permission_set`, `sys_role_permission_set`. **Anonymous traffic is denied by default** (the ADR-0056 D2 default-deny flip landed: `requireAuth` defaults to `true`); an explicit `requireAuth: false` opt-out logs a boot-time warning, and public forms do not depend on any fall-open — they carry a declaration-derived `publicFormGrant` (ADR-0056 Option A).
291
291
-**OWD / sharing-model enforcement is live and proven end-to-end (ADR-0056)**: `private`, `public_read`, `public_read_write`, and `controlled_by_parent` are enforced through `plugin-sharing` + `plugin-security` and verified by dogfood proofs over the real HTTP stack. `object.sharingModel` now accepts the canonical OWD vocabulary (`private` / `public_read` / `public_read_write` / `controlled_by_parent`) alongside the legacy `read` / `read_write` / `full` spellings (D1). RLS owner policies resolve `current_user.email` in addition to `id` / `organization_id` / `roles` (#2054). Permission sets may declare `isDefault: true` to act as the app-declared fallback profile (D7).
292
292
293
293
---
@@ -429,7 +429,7 @@ cloud / EE.
429
429
-[x] Organization-Wide Defaults / sharing model — `private`, `public_read`, `public_read_write`, and `controlled_by_parent` enforced via `plugin-sharing` + `plugin-security`, proven by dogfood over the real HTTP stack (ADR-0056). `object.sharingModel` accepts the canonical OWD vocabulary alongside the legacy `read` / `read_write` / `full` spellings (D1)
430
430
-[x] Sharing Rule evaluator — owner + criteria rules re-evaluated on `afterInsert` / `afterUpdate` (`plugin-sharing/rule-hooks.ts`); recipients include user / role / group and configurable `role_and_subordinates` role-hierarchy widening (ADR-0056 D6)
431
431
-[x] App-declarable default profile — a permission set may set `isDefault: true` to be the fallback profile for unassigned users (ADR-0056 D7)
432
-
-[] Default-deny for anonymous traffic — boot-time warning lands when `requireAuth`is unset (ADR-0056 D2) and public forms self-authorize via `publicFormGrant` (Option A); the global default-deny **flip** is release-gated
432
+
-[x] Default-deny for anonymous traffic — the global default-deny **flip landed** (ADR-0056 D2): `requireAuth`defaults to `true`, explicit `requireAuth: false` opt-outs warn at boot, and public forms self-authorize via `publicFormGrant` (Option A)
Copy file name to clipboardExpand all lines: content/docs/guides/cheatsheets/permissions-matrix.mdx
+5-1Lines changed: 5 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -30,7 +30,11 @@ ObjectStack's `ObjectPermission` schema defines these boolean flags for object a
30
30
|**Modify All**|`modifyAllRecords`| Edit/delete all records regardless of ownership | Full object access (bypass sharing) |
31
31
32
32
<Callouttype="tip">
33
-
**Super-user bypass:** When `modifyAllRecords` is set it satisfies write checks (`allowEdit`/`allowDelete`) on any record; `viewAllRecords` (or `modifyAllRecords`) satisfies `allowRead` on any record — both bypass ownership and sharing. See `packages/plugins/plugin-security/src/permission-evaluator.ts`.
33
+
**Super-user bypass:** When `modifyAllRecords` is set it satisfies write checks (`allowEdit`/`allowDelete`, and the lifecycle class `allowTransfer`/`allowRestore`/`allowPurge`) on any record; `viewAllRecords` (or `modifyAllRecords`) satisfies `allowRead` on any record — both bypass ownership and sharing. See `packages/plugins/plugin-security/src/permission-evaluator.ts`.
34
+
</Callout>
35
+
36
+
<Callouttype="warn">
37
+
**Lifecycle operations are pending:** the `transfer` / `restore` / `purge` operations do not exist in ObjectQL yet (roadmap M2). Their RBAC gate is already mapped in the permission evaluator — the moment the operations ship they are denied unless the matching flag (or `modifyAllRecords`) is granted — but authoring these flags today grants nothing (#1883).
Copy file name to clipboardExpand all lines: content/docs/guides/security.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,7 +7,7 @@ description: "Complete guide to implementing enterprise-grade security in Object
7
7
8
8
Complete guide to implementing enterprise-grade security in ObjectStack with fine-grained permissions and data access controls.
9
9
10
-
> **Implementation status — Phase-1 RBAC is live.** REST → ObjectQL now propagates a populated `ExecutionContext` (userId, tenantId, roles, permissions) into the SecurityPlugin middleware, so CRUD / FLS / RLS checks actually fire on every authenticated request. The default `member_default` permission set ships a wildcard RLS rule `organization_id == current_user.organization_id` plus explicit per-object overrides `sys_organization_self` (`id == current_user.organization_id`) and `sys_user_self` (`id == current_user.id`) for the two global tables that lack an `organization_id` column. RLS expressions, the physical column, and `RLSUserContext.organization_id` all use the same canonical name — there is no `tenantField` rewrite indirection (schemas with a different physical tenant column should fork the defaults). The legacy `objectql.registerTenantMiddleware` has been removed; SecurityPlugin is the sole authority for tenant isolation. Analytics also reuses the same read scope: `@objectstack/service-analytics` auto-bridges to `security.getReadFilter(object, context)` when the security service is registered, so dataset-bound dashboards/reports do not bypass RLS. End-to-end verified on `pnpm dev:crm` across `sys_organization`, `sys_member`, `sys_user`, `sys_user_permission_set`, `sys_role_permission_set`. **Anonymous traffic still falls open by default** (the default-deny flip is release-gated); since ADR-0056 D2 the server logs a boot-time warning when `requireAuth` is unset, and public forms self-authorize via a declaration-derived `publicFormGrant` (ADR-0056 Option A — see [Public Forms](./public-forms)). Organization-Wide Defaults (`private` / `public_read` / `public_read_write` / `controlled_by_parent`) and Sharing Rules (owner + criteria, with `role_and_subordinates` hierarchy widening) are live and dogfood-proven (ADR-0056); the Studio RLS visual editor, per-user×org permission cache, and audit UI for denied access are queued. See `CHANGELOG.md` and `concepts/implementation-status.mdx` for the latest matrix.
10
+
> **Implementation status — Phase-1 RBAC is live.** REST → ObjectQL now propagates a populated `ExecutionContext` (userId, tenantId, roles, permissions) into the SecurityPlugin middleware, so CRUD / FLS / RLS checks actually fire on every authenticated request. The default `member_default` permission set ships a wildcard RLS rule `organization_id == current_user.organization_id` plus explicit per-object overrides `sys_organization_self` (`id == current_user.organization_id`) and `sys_user_self` (`id == current_user.id`) for the two global tables that lack an `organization_id` column. RLS expressions, the physical column, and `RLSUserContext.organization_id` all use the same canonical name — there is no `tenantField` rewrite indirection (schemas with a different physical tenant column should fork the defaults). The legacy `objectql.registerTenantMiddleware` has been removed; SecurityPlugin is the sole authority for tenant isolation. Analytics also reuses the same read scope: `@objectstack/service-analytics` auto-bridges to `security.getReadFilter(object, context)` when the security service is registered, so dataset-bound dashboards/reports do not bypass RLS. End-to-end verified on `pnpm dev:crm` across `sys_organization`, `sys_member`, `sys_user`, `sys_user_permission_set`, `sys_role_permission_set`. **Anonymous traffic is denied by default** (the ADR-0056 D2 default-deny flip landed: `requireAuth` defaults to `true`); an explicit `requireAuth: false` opt-out logs a boot-time warning, and public forms self-authorize via a declaration-derived `publicFormGrant` (ADR-0056 Option A — see [Public Forms](./public-forms)). Organization-Wide Defaults (`private` / `public_read` / `public_read_write` / `controlled_by_parent`) and Sharing Rules (owner + criteria, with `role_and_subordinates` hierarchy widening) are live and dogfood-proven (ADR-0056); the Studio RLS visual editor, per-user×org permission cache, and audit UI for denied access are queued. See `CHANGELOG.md` and `concepts/implementation-status.mdx` for the latest matrix.
0 commit comments