@objectstack/plugin-security@16.0.0
Minor Changes
-
22013aa: Split the overloaded
managedBy: 'system'bucket into engine-owned vs. admin-writable, and enforce engine-owned writes (ADR-0103, #3220). Thesystembucket conflated two incompatible write policies: rows a platform service owns end to end (never user-written), and platform-defined schema whose rows are legitimately admin/user-writable. It carried the same all-false affordance row asbetter-auth/append-onlybut, unlikebetter-auth, had no engine enforcement — a wildcard admin could raw-write these rows through the generic data API (ADR-0049 gap).Rather than add a new
managedByenum value (which would fall through to fully-editableplatformdefaults on already-deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances= bucket default +userActions), and engine-owned is defined as asystem/append-onlyobject that grants no write:- Writable set declares
userActions— the RBAC link tables (sys_user_position,sys_user_permission_set,sys_position_permission_set),sys_user_preference,sys_approval_delegation, and the messaging config grids (sys_notification_preference/…_subscription/…_template) now declareuserActions: { create, edit, delete: true }. The affordance is a declaration only — theDelegatedAdminGate/ RLS / permission sets remain the authz. - Engine-owned objects locked to reads —
apiMethods: ['get','list']added where absent (jobs, notifications, approval request/approver/token/action,sys_record_share,sys_automation_run, mail/settings/secret audit, the messaging delivery pipeline).sys_secretis explicitly read-locked (an emptyapiMethodsarray fails open). sys_import_jobstays engine-owned: the REST import route now writes its job rowsisSystem-elevated (attribution preserved via the explicitcreated_bystamp) and the object is locked to['get','list'].- New engine write guard (
assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes to engine-ownedsystem/append-onlyobjects, keyed off the resolved affordance;isSystemand context-less engine/service writes bypass by construction. Wired into the security middleware alongside the other data-layer gates. reconcileManagedApiMethods(objectql registry) now runs for every managed bucket, not justbetter-auth: any advertised write verb an object's resolved affordances forbid is stripped at registration with a warning (the drift backstop, ADR-0049)./me/permissionsclamp (plugin-hono-server) now clampssystem/append-onlyas well asbetter-auth, so the client hint reflectspermission ∩ guard.
Potentially breaking: a downstream/third-party
systemobject that advertised generic write verbs relying on today's fail-open behaviour will have those verbs stripped (with a warning) and user-context generic writes to it rejected. DeclareuserActionsopening the verbs the object legitimately takes from a user context.better-authkeeps plugin-auth's identity write guard unchanged; the row-levelmanaged_byprovenance vocabulary (ADR-0066) is a different axis and is untouched. - Writable set declares
-
62a2117: Split the overloaded
managedBy: 'system'bucket with an explicitengine-ownedvalue (ADR-0103 addendum, #3343). ADR-0103 deferred the enum split ("revisitable later as a rename") because a newmanagedByvalue would fall through to the fully-editableplatformdefault on deployed Console clients. Both reasons against it are now retired — the server-side write guard /apiMethodsreconciliation //me/permissionsclamp make that fallthrough cosmetic (the write is rejected regardless of what the client renders), and objectui#2712 closed the UI union — so v16 lands it, additively.- New enum value
engine-ownedwith the same all-locked default affordance row assystem(create/import/edit/delete: false,exportCsv: true). It joinsENGINE_OWNED_BUCKETS(the engine write guard) andGUARDED_WRITE_BUCKETS(the/me/permissionsclamp); the guard,reconcileManagedApiMethods, and the clamp mechanisms are unchanged —engine-ownedis an explicit member of the set they already covered by resolved affordance. - 20 objects relabelled
system → engine-owned— the ones the engine owns end to end and that declared no write-openinguserActions(the metadata store, jobs, approval runtime rows, sharing rows,sys_automation_run, the messaging delivery/receipt pipeline,sys_secret, settings). One-line, behaviour-identical per object. - 8 admin/user-writable objects keep
managedBy: 'system'(the RBAC link tables,sys_user_preference,sys_approval_delegation, the messaging config grids) —systemnow reads as "engine-managed schema, writable viauserActions".
Behaviour-, enforcement- and wire-identical: resolved affordances, the guard verdict, the 405
apiMethodsreconciliation, and the permissions clamp are the same before and after — this is a self-documenting relabel, not a policy change. No data migration (managedByis schema metadata) and no code branches on the'system'literal. Retiring the overloadedsystementirely (moving the 8 writable objects to a dedicated bucket) is a breaking rename deferred to v17. - New enum value
Patch Changes
-
2f3c641: ADR-0099 P0: land the probe-vs-carried-rung equivalence gate in the authz matrix (
authz-matrix-gate.test.ts) — seeded-shape equivalence cells, two adversarialKNOWN DIVERGENCEpins (scopedadmin_full_accessgrant; piecemeal platform-exclusive capability), the I2 nesting and I3 narrowing invariant cells, posture-blindness staging pins for the P1 flip, and the EXTERNAL dead-branch cell. Extracts the platform-admin capability probe as the exported purehasPlatformAdminCapability(mechanical, behavior unchanged). Test-only gate; the ADR-0099 P1 flip lands behind it (#3211). -
e38da5b: ADR-0099 P1 (#3211 M2): the Layer 0 cross-tenant exemption gate now reads the carried
ctx.posturerung (#2956) as authoritative, with the platform-admin capability probe demoted to a fallback for resolver-less contexts (delegated-admin bridge, sharing service,getReadFilter). The read and write (insert/update post-image) tenant checks share one decision (computeLayeredRlsFilter), so they cannot drift. A probe↔rung disagreement logs a defect breadcrumb and enforces the narrower rung verdict.Behavior change (security narrowing, multi-org /
@objectstack/organizationsonly): a principal whose carried rung is notPLATFORM_ADMINno longer crosses the tenant wall on private / platform-global / better-auth-managed objects, even when its resolved permission sets carry a platform-exclusive capability. Two shapes are affected: (a) a scopedadmin_full_accessgrant (sys_user_permission_set.organization_idnon-null), and (b) a custom set granting a platform capability (e.g.studio.access) piecemeal alongside a superuser bit. Both are now walled to their own org — the fail-safe direction (the carried rung is a strict subset of the probe). Single-org / env-per-database deployments are unaffected (Layer 0 is inert).Upgrade check: before upgrading, scan
sys_user_permission_setforadmin_full_accessrows with a non-nullorganization_id, and custom permission sets whosesystemPermissionsintersect{manage_metadata, manage_platform_settings, studio.access, manage_users}. To restore cross-tenant operator access for such a principal, grant the unscopedadmin_full_accessinstead. The[authz/ADR-0099]warn log names any principal hitting the divergence at runtime. -
f9b118d: ADR-0099 P2′ (#3211 M3′): pin the two-axis Amendment in the authz matrix. The original P2 (collapse the Layer 1 tier onto posture) was rejected — Layer 1's tier input is the per-object super-bit, a per-principal × per-object delegation primitive posture cannot represent. New cells pin: seeded-face agreement (seeded super-bit holders are already ≥ TENANT_ADMIN), the load-bearing delegation cell (a MEMBER with a delegated per-object
viewAllRecords/modifyAllRecordsshort-circuits Layer 1 yet stays walled by Layer 0 — the auditor pattern), invariant I7 (the scope axis never crosses a boundary posture has not opened), and the contrast that the bit is a real grantable capability, not conditionally inert. Test-only; zero behavior change. -
9d897b3: Derive the better-auth managed-object write denies from the live registry (#3325, follow-through of ADR-0092 / ADR-0103). The default permission sets deny generic writes on better-auth identity tables via a hand-maintained
BETTER_AUTH_MANAGED_OBJECTSlist — exactly the drift ADR-0092 forbids, and it had already drifted (the list carried 17 names while 22 schemas declaremanagedBy: 'better-auth', leavingsys_scim_provider,sys_sso_provider, and threesys_oauth_*tables wildcard-granted for writes at the permission-evaluator layer; the identity write guard still 403'd the actual write, so this was a defense-in-depth gap, not a live hole).- New
applyManagedWriteDenies(managed-object-write-denies.ts) injects a read-only-write deny for every registeredmanagedBy: 'better-auth'object into the four write-granting default sets (organization_admin,member_default,viewer_readonly, MCP write) atkernel:ready, mutating the shared in-memorybootstrapPermissionSetsin place (the array the evaluator resolves and the seeder serializes — a DB-row-only fix would be dead code). Never touchesadmin_full_access, never overrides an existing explicit entry, ignoresuserActions(the better-auth bucket is hard-denied —sys_user'suserActions.editopens only a field-level whitelist the identity guard enforces). - The static
BETTER_AUTH_MANAGED_OBJECTSlist is completed to 22 and kept as a compile-time baseline (covers the pre-kernel:readywindow), now pinned bidirectionally against the@objectstack/platform-objectsschemas by a test so it cannot silently rot again. - Engine-owned
system/append-onlyobjects are deliberately NOT given deny entries — a per-object entry overrides the wildcard and would dropviewAllRecords; their writes are already rejected by the ADR-0103 engine guard.
No public API change; the helper is internal. Behavior is byte-preserving for the 17 already-listed tables and closes the gap on the 5 that had drifted.
- New
-
Updated dependencies [f972574]
-
Updated dependencies [6289ec3]
-
Updated dependencies [22013aa]
-
Updated dependencies [3ad3dd5]
-
Updated dependencies [8efa395]
-
Updated dependencies [3a18b60]
-
Updated dependencies [a8aa34c]
-
Updated dependencies [e057f42]
-
Updated dependencies [a3823b2]
-
Updated dependencies [bc65105]
-
Updated dependencies [43a3efb]
-
Updated dependencies [524696a]
-
Updated dependencies [6b51346]
-
Updated dependencies [80273c8]
-
Updated dependencies [bfa3c3f]
-
Updated dependencies [5e3301d]
-
Updated dependencies [dd9f223]
-
Updated dependencies [46e876c]
-
Updated dependencies [7125007]
-
Updated dependencies [5f05de2]
-
Updated dependencies [021ba4c]
-
Updated dependencies [158aa14]
-
Updated dependencies [62a2117]
-
Updated dependencies [d2723e2]
-
Updated dependencies [fefcd54]
-
Updated dependencies [beaf2de]
-
Updated dependencies [369eb6e]
-
Updated dependencies [06ff734]
-
Updated dependencies [b659111]
-
Updated dependencies [5754a23]
-
Updated dependencies [6c270a6]
-
Updated dependencies [290e2f0]
-
Updated dependencies [668dd17]
-
Updated dependencies [8abf133]
-
Updated dependencies [e0859b1]
-
Updated dependencies [04ecd4e]
-
Updated dependencies [4d5a892]
-
Updated dependencies [16cebeb]
-
Updated dependencies [86d30af]
-
Updated dependencies [8923843]
-
Updated dependencies [ea32ec7]
-
Updated dependencies [a2795f6]
-
Updated dependencies [f16b492]
-
Updated dependencies [4b6fde8]
-
Updated dependencies [2018df9]
-
Updated dependencies [fc5a3a2]
-
Updated dependencies [8ff9210]
- @objectstack/spec@16.0.0
- @objectstack/platform-objects@16.0.0
- @objectstack/core@16.0.0
- @objectstack/formula@16.0.0