Uh oh!
There was an error while loading. Please reload this page.
feat(security): split managedBy:'system' into engine-owned vs admin-writable, guard engine-owned writes (#3220) - #3315
Conversation
…ritable, guard engine-owned writes (#3220) The `system` bucket conflated two incompatible write policies — engine-owned rows (never user-written) and platform-schema, admin/user-writable data — under one all-false affordance row with no engine enforcement (only better-auth had a write guard). This left a wildcard admin able to raw-write engine-owned rows through the generic data API (ADR-0049 gap). Rather than add a new managedBy enum value (which falls through to fully-editable platform defaults on deployed Console clients), the write policy is now the resolved affordance (resolveCrudAffordances = bucket default + userActions), and "engine-owned" is defined as a system/append-only object granting no write: - Writable set declares userActions{create,edit,delete}: the RBAC link tables, sys_user_preference, sys_approval_delegation, and the messaging config grids (notification preference/subscription/template). Affordance only — the DelegatedAdminGate / RLS / permission sets remain the authz. - Engine-owned objects locked to apiMethods ['get','list'] where absent (jobs, notifications, approvals, record-share, automation-run, mail/settings/secret audit, messaging delivery pipeline). sys_secret is read-locked explicitly (an empty apiMethods array fails open). - sys_import_job stays engine-owned: the REST import route writes job rows isSystem-elevated (attribution preserved via explicit created_by). - New engine write guard (assertEngineOwnedWriteAllowed, plugin-security) fail-closed rejects user-context generic writes keyed off resolved affordances; isSystem/context-less writes bypass. Wired into the security middleware alongside the other data-layer gates. - reconcileManagedApiMethods (objectql registry) now runs for every managed bucket, stripping advertised write verbs the affordances forbid. - /me/permissions clamp (plugin-hono-server) now clamps system/append-only too. ADR-0103. Refs #3220, follows #3222 / ADR-0049 / ADR-0092. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fp9yZxRQ3mb7p4vVwqFXKE
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📓 Docs Drift CheckThis PR changes 10 package(s): 114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
Uh oh!
There was an error while loading. Please reload this page.
Closes the root cause tracked in #3220 (follow-through on #3222 / #1591 / #3213). ADR-0103.
Problem
managedBy: 'system'conflated two incompatible write policies:isSystem/ a serviceSYSTEM_CTX/ a context-less engine call (jobs, notifications, approvals, sharing, automation runs, the messaging delivery pipeline, settings/secrets/audit).DelegatedAdminGate-governed),sys_user_preference,sys_approval_delegation, and the messaging config grids surfaced in Setup.The bucket carried the same all-false affordance row as
better-auth/append-onlybut, unlikebetter-auth, had no engine enforcement — a wildcard admin could raw-write engine-owned rows through the generic data API (ADR-0049 gap). #3222 could only patch two latent holes; this is the taxonomy fix.Approach — resolved affordance is the policy (no new enum value)
A new
managedByvalue would fall through to the fully-editableplatformdefault on already-deployed Console clients. Instead, write policy isresolveCrudAffordances(bucket default +userActions), and engine-owned ≔ asystem/append-onlyobject granting no write. Guard,apiMethodsreconciliation, and the/me/permissionsclamp all key off that one contract.What's in the diff
userActions{create,edit,delete}—sys_user_position,sys_user_permission_set,sys_position_permission_set,sys_user_preference,sys_approval_delegation,sys_notification_preference/_subscription/_template. Affordance only —DelegatedAdminGate/ RLS / permission sets stay the authz.apiMethods: ['get','list']where absent.sys_secretread-locked explicitly (an emptyapiMethodsarray fails open).sys_import_jobstays engine-owned — the REST import route writes its job rowsisSystem-elevated (attribution preserved via the explicitcreated_bystamp).assertEngineOwnedWriteAllowed(plugin-security) — fail-closed on user-context generic writes to engine-owned objects;isSystem/ context-less writes bypass. Wired into the security middleware alongside the other data-layer gates.reconcileManagedApiMethodsnow runs for every managed bucket (wasbetter-auth-only) — the drift backstop.clampManagedObjectWritesnow clampssystem/append-onlytoo.Audit finding (write-context, the guard's precondition)
Confirmed every engine-owned writer uses
isSystem/SYSTEM_CTX/ a context-less engine call (incl. the metadata-protocol repository, whose transaction context carries nouserId). The audit expanded the writable set from 5 to 8:notification_preference/_subscription/_templateare Setup nav grids users/admins author, so they joined the writable set rather than being locked down.managed_byprovenance vocabulary (platform/package/admin, ADR-0066) — a different axis, untouched.Verification
All green:
spec(6795) ·objectql(1023, incl. new reconcile cases) ·plugin-security(521, incl. newsystem-write-guardsuite) ·plugin-authidentity-write-guard (15) ·plugin-hono-server(92, updated fold/clamp) ·rest(323) ·platform-objects/plugin-approvals/service-messaging/service-automation/plugin-sharing/metadata-core.delegation-of-duty+showcase-permission-zoo(18) — confirm delegated administration and direct permission-set grants under user context still work with the guard in place.Potentially breaking
A downstream/third-party
systemobject that advertised generic write verbs relying on today's fail-open will have those verbs stripped (with a warning) and user-context generic writes rejected — declareuserActionsopening the verbs it legitimately takes.better-authkeeps plugin-auth's identity guard unchanged.The Console copy refinements (badge/empty-state for the writable-system case) ship in the paired objectui PR; they are not required for correctness (the UI already honours
userActions).🤖 Generated with Claude Code
Generated by Claude Code