Skip to content

sys_capability.active is read by nothing — the Deactivate action tells the admin grants stop resolving, and they do not #8535

Description

@os-zhuang

Found while implementing #8470, which required establishing whether any runtime path resolves a sys_capabilityrow (as opposed to the bare capability name string). The answer is no — and that answer has a consequence beyond #8470.

The claim the product makes

sys-capability.object.ts ships a deactivate_capability record action whose confirmation dialog reads, verbatim:

Deactivate this capability? Grants and resource requirements that reference it stop resolving until re-activated.

The active field is also surfaced in all three list views and in highlightFields, so deactivation presents as a first-class administrative control.

What actually happens

Nothing reads it. Measured on origin/main at 30f1b7488d:

  • The AND-gate that enforces requiredPermissions compares string sets. PermissionEvaluator.getSystemPermissions() unions permissionSets[].systemPermissions (strings); normalizeRequiredPermissions reads the resource's own declared strings. Neither loads a row.
  • A repo-wide search for row reads of the object — find/findOne/count/aggregate against 'sys_capability', plus every consumer of the platform-object-names entry — returns exactly two production call sites, both seeders: bootstrap-system-capabilities.ts and bootstrap-declared-capabilities.ts. Both read name, managed_by, package_id. Neither reads active, and neither is on an authorization path.
  • validateCapabilityReferences (the authoring lint) resolves against PLATFORM_CAPABILITY_NAMES, stack declarations and seed-data records — the spec constant and the config, never the table.

So active: false changes no authorization decision, no lint verdict, and no runtime resolution. It changes a column value and a badge in Setup.

Why this is worth a card rather than a shrug

The direction of the falsehood is the bad one. An admin who wants to withdraw a capability is offered a control that states, in a confirmation dialog, that withdrawing it takes effect — and the withdrawal silently does not happen. That is a security-relevant misstatement about a security surface even though it is not itself a privilege escalation: the escalation is what the admin believes they prevented.

This is also an ADR-0049 enforce-or-remove shape: a declared field with a declared meaning and no enforcement.

The two directions, not a recommendation

I am deliberately not choosing here, because the choice is a contract decision:

  1. Enforce it — capability resolution consults the row and an inactive capability stops satisfying requiredPermissions. This makes the registry load-bearing on the authorization hot path, which is a real architectural change (every gate needs the row, with the caching and fail-closed questions that implies) and it gives an org-authored row influence over a platform capability unless the ownership scoping from bootstrapSystemCapabilities reconciles an arbitrary row when a curated capability name exists in more than one organization — find(..., limit: 1) has no ORDER BY, so the platform's own row can be left unseeded #8470 is carried through.
  2. Remove the claim — drop the action and the field, or reword the dialog to say what deactivation actually does (a catalogue/visibility flag). Cheapest, and honest.

Filed unassigned. Related to #8470, which fixes a different defect in the same seeder and does not touch this.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions