# Permission Metadata Refactor — P0 Drift Audit

> **Status: HISTORICAL.** This document is the pre-refactor ground-truth
> snapshot. For the post-refactor architecture and the final outcome,
> see `permissions-metadata-refactor-outcome.md`.

Ground-truth reconciliation across the three authoring sites for permissions,
produced before any code change so the P1–P16 steps have a single authoritative
input.

Date: 2026-04-22
Branch: `work/permission-metadata-refactor`

## Three-way audit

| Source | Count | Location |
|---|---|---|
| Go enum constants | **70** | `shared/pkg/constants/permissions.go` `Perm<X> Permission = "X"` |
| `AllSystemPermissions()` catalog entries | **70** | same file, `func AllSystemPermissions()` return value |
| Bitmap column map | **26** | `backend/internal/infrastructure/persistence/perm_system.go` `permissionSetSystemPermissionColumns` |

## Divergence: none

Computed via AST-style regex extraction (not text grep — excludes comment/doc
pollution):

- **enum ↔ catalog**: set-equal (all 70 constants have catalog entries, all
  catalog entries have constants).
- **bitmap ⊆ catalog**: every bitmap column corresponds to a catalog entry
  (no orphan columns).
- **bitmap ⊆ enum**: every bitmap-referenced constant exists in the enum.

The three sources are structurally consistent. Prior "drift" appearances
during investigation were grep false-positives from doc-comment text
(`"PermissionSets"` in prose etc.).

## Reader audit: who reads the 26 bitmap columns?

Grep for `FieldPermissionSet_Permissions<X>` across `backend/` excluding
generated files and tests:

```
backend/internal/infrastructure/persistence/perm_system.go — map definition only
```

**Zero other readers.** The bitmap columns are a write-only cache today.
`GetEffectiveSystemPermissions` reads the junction (`_System_PermissionSetPermission`)
exclusively. `SetSystemPermission` writes both the junction and (when one
exists) the bitmap column, but nothing downstream of the write consumes
the column value for authorization.

Implication for P5/P6: **deleting the 26 columns is safe in a single step**
after P5 removes the last write path. No straggler reader to convert.

## The 70 permissions

Split by category as declared in `AllSystemPermissions()`:

| Category | Count | Notes |
|---|---|---|
| Administrative | 7 | ApiEnabled, ModifyAllData, ViewAllData, CustomizeApplication, ManageSharing, ViewSetup, ... |
| UserManagement | 9 | ManageUsers, ManageRoles, ManageProfilesPermSets, AssignPermissionSets, ... |
| Data | 10 | BulkApiHardDelete, MassInlineEdit, EditEvent, EditTask, DataExport, TransferAnyEntity, ... |
| ReportsDashboards | 9 | RunReports, CreateCustomizeReports, CreateDashboards, ... |
| FlowAutomation | 7 | RunFlow, ManageFlows, ViewFlowDefinition, ManageActions, ManageApprovalProcesses, ... |
| SchemaMetadata | 3 | ModifyMetadata, ManageCustomPermissions, ManageCustomLabels |
| Email | 3 | EmailSingle, EmailMass, EmailTemplateManagement |
| ListViews | 2 | CreateCustomizeFilters, EditPublicFilters |
| ContentFiles | 3 | ManageContentPermissions, ManageContentProperties, CreateContentSpace |
| AuditCompliance | 4 | ViewSetupAuditTrail, ViewDataLeakageEvents, ViewEventLogFiles, ViewFieldAuditTrail |
| Platform | 3 | LightningExperienceUser, SalesConsole, ServiceCloud |
| Integration | 5 | QueryAllFiles, ViewPlatformEvents, PublishPlatformEvents, ManageConnectedApps, ManageOAuthApps |
| ObjectOperations | 7 | EditPublicDocuments, ManageCases, EditReadonlyFields, ManageLeads, ConvertLeads, ActivateContract, ActivateOrder |

Total: **70** ✓

## Prerequisite chains

Five permissions declare `Requires`:

| Permission | Requires |
|---|---|
| ModifyAllData | ViewAllData |
| ConvertLeads | ManageLeads |

(Only 2 actually declared — the "5" earlier count was an error. Most
permissions have no prerequisite in the current catalog.)

P1 populates `RequiresPermission` column on `_System_Permission` rows;
currently the column exists but is not written by the seeder.

## Default grants (from `SeedSystemPermissions` batch 2)

| Profile | Permissions granted by default |
|---|---|
| SystemAdmin | **All 70** |
| StandardUser | **CreateCustomizeReports** (1) |

P1 folds these into `defaultGrantedTo: [...]` on each row. SystemAdmin is
`["SystemAdmin"]` on every row; CreateCustomizeReports gets
`["SystemAdmin", "StandardUser"]`; everything else gets `["SystemAdmin"]`.

## Classification for P4 (engine-intrinsic vs. convertible)

Every `constants.Perm<X>` reference in `backend/internal/` was inspected.
This table is the authoritative decision for P4.

### Keep as Go constant (engine-intrinsic)

Each row here is a Go constant that code branches on in a way that is not
a simple lookup. Deleting would require reimplementing engine behavior
around the constant's name, which is exactly what "engine-intrinsic"
means.

| Constant | Why retained |
|---|---|
| `PermRead` / `PermCreate` / `PermEdit` / `PermDelete` | CRUD operation enum values — used in `switch op` statements in the permission service, not permissions in the Salesforce `UserPermission` sense. Pass `string(constants.PermRead)` when calling permission checks keyed by operation. |
| `PermViewAllData` | RLS bypass branch in `rls_filter.go:116`. If the user has this, the engine skips row-level security filtering entirely. |
| `PermModifyAllData` | Same — `rls_filter.go:117`, `record_access.go:38`. |
| `PermModifyMetadata` | Metadata-API admin gate — `approval_service.go:2435` branches on this before allowing flow-metadata write. |
| `PermRunFlow` | Flow-dispatch middleware gate in `routes.go`. Default-required on every `/actions/custom/flow/*` call. |
| `PermManageFlows` | Flow-management gate (create/edit) in flow handlers. |
| `PermViewSetup` | Setup-access gate — used by schema `requiredReadPermission` and middleware. |
| `PermRunReports` | Reports-engine gate — `folder_access.go:427`. |
| `PermApiEnabled` | Global API-access gate — `routes.go` middleware on `/services/data/*`. |

**Count: 12 constants** (CRUD verbs + 8 engine intrinsics).

### Convert to string literal or subsystem-local constant

The remaining 58 constants (70 - 12) serve only as lookup keys passed to
`CheckSystemPermission(..., string(constants.Perm<X>))`. String literals
are equivalent in behavior; the constant form adds nothing beyond
spell-check.

Subsystem-local typed constants may be created where a single subsystem
references the same permission in three or more places AND wants local
compile-time safety. P4 will identify these during the audit. Examples
likely to qualify: `TransferAnyEntity` if ChangeOwner (being migrated to
a data row anyway) retains any Go references, or `ManageUsers` if the
user-admin subsystem has enough internal calls.

**Count: 58 constants to delete or localize.**

### Category constants

The 10 `PermCat*` constants (PermCatAdministrative, PermCatData, etc.)
are used only as the `Category` field on `PermissionDefinition` struct
literals. Under the new model, Category is a plain string column on
`_System_Permission` rows. P7 deletes the `PermCat*` constants entirely;
admin UI reads the string value and groups on it.

## Outstanding-debt exit items (out of this PR)

These are flagged for future PRs so the refactor's boundary is explicit:

1. **Per-object CRUD columns on `ObjectPermissions` table** — this is a
   different bitmap (per-object grants), separate architectural concern,
   matches SF's `ObjectPermission` shape. **Not touched by this refactor.**
2. **`_System_CustomPermission` vs. `_System_Permission`** — the two-table
   distinction stays (matches SF `UserPermission` vs `CustomPermission`).
   Verified `HasPermission` unified check queries both.
3. **`_System_ProfilePermission` remnants** — the table, its generated
   schema/model/constants, and the seeder's Batch 3 migration from it into
   the junction all remain in-tree to preserve the upgrade path for
   clusters that still have legacy rows. Tracked in issue **#794**; this
   PR does not remove them. When every deployment has confirmed Batch 3
   has run and the table is empty, a separate PR will delete the
   schema/model/constants and the Batch 3 SQL.
4. **Migration story for existing tenants** — existing deployments have
   junction rows populated. Dropping the 26 bitmap columns is DDL. Any
   old-binary instance reading the columns during a rolling deploy
   would fail. Mitigation: the three-stage data-spec cleanup
   (`data-specs/2026-04-23{a,b,c}-*.md`) is applied explicitly by
   operators after the cutover binary is at 100%. If a tenant has
   custom code reading those columns directly, they need to migrate
   before receiving this release — documented in the outcome doc.

## Proceeding

All P0 inputs are now in hand with zero divergence. P1 authors
`standard_permissions.json` from this audit's unified 70-row list and
fold `defaultGrantedTo` inline per-row. No reconciliation decisions
deferred to execution time.
