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
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:
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.
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.tsships adeactivate_capabilityrecord action whose confirmation dialog reads, verbatim:The
activefield is also surfaced in all three list views and inhighlightFields, so deactivation presents as a first-class administrative control.What actually happens
Nothing reads it. Measured on
origin/mainat30f1b7488d:requiredPermissionscompares string sets.PermissionEvaluator.getSystemPermissions()unionspermissionSets[].systemPermissions(strings);normalizeRequiredPermissionsreads the resource's own declared strings. Neither loads a row.find/findOne/count/aggregateagainst'sys_capability', plus every consumer of theplatform-object-namesentry — returns exactly two production call sites, both seeders:bootstrap-system-capabilities.tsandbootstrap-declared-capabilities.ts. Both readname,managed_by,package_id. Neither readsactive, and neither is on an authorization path.validateCapabilityReferences(the authoring lint) resolves againstPLATFORM_CAPABILITY_NAMES, stack declarations and seed-data records — the spec constant and the config, never the table.So
active: falsechanges 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:
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 frombootstrapSystemCapabilitiesreconciles 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.Filed unassigned. Related to #8470, which fixes a different defect in the same seeder and does not touch this.
Generated by Claude Code