Skip to content

sys_permission_set.active and sys_position.active are unenforced too — both Deactivate dialogs promise access stops, and it does not #8613

Description

@os-zhuang

Found while implementing #8535 (which withdraws the identical false claim on sys_capability only). Filed unassigned — out of scope for #8535's PR, which deliberately touches nothing but sys_capability.

#8535 established that sys_capability.active is read by nothing while its Deactivate dialog claims grants stop resolving. The same shape exists on the other two RBAC catalogue objects, and on those it is worse: a permission set and a position are the grant itself, not a catalogue entry.

The claims

sys-permission-set.object.ts, deactivate_permission_set:

Deactivate this permission set? Existing assignments stay in place but stop granting access until re-activated.

sys-position.object.ts, deactivate_position:

Deactivate this position? Users keep their assignment but the position stops granting permissions until re-activated.

Both ship translated into all four locales.

What actually happens (measured on b45c71e85a)

Nothing filters on active anywhere in the resolution chain:

  • SecurityPlugin.resolvePermissionSetsForContext (security-plugin.ts:3626) builds requested from context.positions + context.permissions plus the additive baseline. No active check.
  • PermissionEvaluator.resolvePermissionSets (permission-evaluator.ts:398) matches metadata sets by name, then bootstrap sets by name, then falls through to the DB loader. No active check in any of the three sources.
  • The DB loader (security-plugin.ts:812) queries sys_permission_set with where: { name: { $in: names } } — no active predicate — and the row is mapped to a PermissionSet that does not carry active at all, so nothing downstream could filter on it even if it wanted to.
  • Every non-test sys_position row read (delegated-admin-gate.ts, security-plugin.ts, the bootstrap seeders) looks up by name or id. None reads active.

So active: false on a permission set or a position changes a column value and a badge in Setup. The assignments keep granting.

Why this is worth its own card

Same direction of falsehood as #8535, higher blast radius. An admin revoking a compromised or over-broad permission set is told in a confirmation dialog that access stopped; it did not. The admin's likely next action is to not do the thing that would actually work (delete the set, or remove the assignments).

Note sys_position was partially touched by #8556/#8601, but that was different text — the deactivate_position dialog above is still on main making the claim.

Not a recommendation

The direction is the same contract decision the maintainer already ruled on for sys_capability (2026-08-13, ADR-0049 enforce-or-remove, option B: withdraw the claim rather than put the registry on the authorization hot path). Whether that ruling extends to these two is not something this finding assumes: unlike a capability catalogue, a permission set's active flag has a plausible real enforcement point (the DB loader's where, one predicate) and a plausible real business pull (switching a grant off without deleting it). Both options are live and the trade-off is different from #8535's. Needs triage, not a rider.

Activity

  1. added theissue type on Aug 14, 2026
  2. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Triage: needs-user-decision + domain:identity + security; native type Bug (both dialogs declare behavior the runtime does not deliver — declared ≠ enforced). Triage seat Routine, 2026-08-14 ~02:15Z.

    Routing note: unlike sibling #8535 (domain:metadata), every surface this card names lives in packages/plugins/plugin-security/ — the object definitions (src/objects/sys-permission-set.object.ts, sys-position.object.ts), the four generated locale bundles, and the plausible enforcement point (security-plugin.ts DB loader). Anchoring rule ⇒ identity lane.

    Why the inbox, not a rider on the #8535 ruling: the 2026-08-13 sys_capability ruling (option B — withdraw the claim) was argued from "don't put the catalogue registry on the authorization hot path". Neither ground carries over: sys_permission_set rows are already read on the hot path (the DB loader the card cites), and a permission set / position is the grant itself, not a catalogue entry. Enforcement here changes runtime access-resolution semantics ⇒ human floor, correctly filed as its own decision.

    Four-prism block (required):

    1. Platform long-term coherence — split. Withdrawing makes active a uniform never-enforced catalogue flag across all three RBAC objects (consistent with sys_capability.active is read by nothing — the Deactivate action tells the admin grants stop resolving, and they do not #8535). Enforcing gives active real semantics exactly where the object is the grant — but leaves the column meaning two different things across the family unless capability's ruling is ever revisited.
    2. Measured business pull — favors enforce. "Switch a grant off without deleting it" (compromised or over-broad set) is a real admin operation; the promise already ships in four locales; the card's measured hazard is that an admin who trusts the dialog will not take the action that would actually work.
    3. AI-agent error-resistance — enforce fully or withdraw loudly, never the middle. A partial enforce (DB-loader predicate only) leaves the metadata-by-name and bootstrap-by-name resolution sources unfiltered — a wall that looks enforced and is not, which is worse than today's honest absence. Either full-coverage enforcement across all three sources, or withdrawal; no silent middle.
    4. Startup scope discipline — favors withdraw. Withdraw is a patch-sized text change; enforce adds a permanent hot-path obligation plus a behavior flip for existing active:false rows on deployed instances.

    Recommendation: enforce, for both objects, at the resolution seam (DB loader predicate + carry active through the mapped PermissionSet shape + the same filter on the metadata and bootstrap sources; for sys_position, the named row-read seams). The behavior flip on existing active:false rows lands on the side of what the admin was told when they clicked Deactivate — but it is a behavior change on deployed data and needs the breaking/release-notes judgment made explicitly. Fallback if scope discipline wins: withdraw both dialogs now (as #8535 option B), and file enforcement as a Feature card. Partial enforcement is rejected under prism 3 either way.

    Serial constraints (for the eventual dispatch): the withdraw path touches plugin-security's generated locale bundles — same file family as in-flight PR #8599 (#8554) and queued #8601; hard serial after those land. No file overlap with #8535's PR (sys_capability only), but adjudicate with that ruling in view.


    Generated by Claude Code

  3. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Maintainer ruling (2026-08-14, verbatim: 「同意你的建议」, approving item 4 of the eight-item decision-box list: 「#8613:enforce 全覆盖,target:v17 进席 A?」): enforce active for both sys_permission_set and sys_position, at every resolution source — DB loader, metadata/bootstrap sources, positional lookups — never partial coverage, per the triage analysis already on this card. Deactivation is an incident-response control; it must actually stop access. needs-user-decision → pm:queue, target:v17; Seat A (#8667). Serial note: the enforce path as scoped does not touch the plugin-security locale bundles; the serial-after-#8599/#8601 constraint applies only if any dialog copy changes end up in scope.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions