Skip to content

Per-action privileges: split the operational privilege catch-alls into separately grantable actions #6325

Description

@hero101

Follow-on to #4764 (Proper design of platform level roles, workspace spec 027-platform-role-redesign).

Description

As a platform operator
I want each operational action to be independently grantable
So that someone who needs to re-index search does not also get the ability to rewrite every authorization policy on the platform

#4764 answers who — it decomposes global-admin into 13 single-purpose roles. It does not change what a privilege is: a privilege still names an action family, so one privilege still unlocks every action in it. Platform Operations Admin is the clearest case. It carries one privilege, PLATFORM_OPERATIONS_ADMIN, and that privilege gates 13 different mutations ranging from a geolocation backfill to deleting a Matrix room. Alongside it, AUTHORIZATION_RESET gates 8 mutations in which "reset one user's policy" and "reset every policy on the platform" are the same permission.

The result is god mode at a smaller scale: narrower than global-admin, but still all-or-nothing inside its boundary. This story makes the individual action the unit of delegation.

There is already precedent in the repo for exactly this split: LICENSE_RESET was carved out of the reset family rather than left under AUTHORIZATION_RESET. This story finishes the job with a principle rather than case by case.

The audit trail already knows the actions that authorization cannot see

PlatformOperationsAuditService.recordOperation() records a distinct action string per mutation. Grepping the source returns 23 named operational actions:

adminCommunicationEnsureAccessToCommunications   adminUpdateGeoLocationData
adminCommunicationMigrateOrphanedConversations   adminUploadFilesFromContentToStorageBucket
adminCommunicationRemoveOrphanedRoom             adminUserAccountDelete
adminCommunicationSyncSpaceHierarchy             aiServerAuthorizationPolicyReset
adminCommunicationUpdateRoomState                aiServerCreateAiPersona
adminIdentityDeleteKratosIdentity                authorizationPlatformRolesAccessReset
adminInAppNotificationsPrune                     authorizationPolicyResetAll
adminMigrateUserAvatar                           authorizationPolicyResetOnPlatform
adminSearchIngestFromScratch                     authorizationPolicyResetToGlobalAdminsAccess
adminUpdateContributorAvatars                    cleanupCollections
deleteUser                                       refreshAllBodiesOfKnowledge
resetLicenseOnAccounts                           updateAssistantActorCapabilities

We can already tell a security owner exactly which of these someone did. We cannot let them do only one of them. The audit model is finer-grained than the authorization model — the trail distinguishes 23 actions where authorization distinguishes 3 privileges. That asymmetry is the whole story in one line.

First-pass privilege list

The proposal, against develop + #4764 Slice A. Where a privilege is new the existing one it splits out of is named.

A3 — the reset family (10 surfaces, 2 privileges today)

Proposed privilege Surfaces Why it separates
AUTHORIZATION_RESET_ENTITY authorizationPolicyResetOnUser, authorizationPolicyResetOnOrganization, authorizationPolicyResetOnAccount, authorizationPolicyResetToGlobalAdminsAccess Single named entity. The routine support action: "this user can't see their space, reset them". Bounded, cheap, recoverable. This is the one that should be handed out.
AUTHORIZATION_RESET_PLATFORM authorizationPolicyResetOnPlatform, authorizationPolicyResetAll, authorizationPlatformRolesAccessReset, aiServerAuthorizationPolicyReset Rewrites authorization across the entire platform, asynchronously through the auth-reset-worker. authorizationPolicyResetAll is plausibly the single highest-blast-radius mutation in the codebase — during #4764's own verification it took down 66 downstream test cells twice, from a stale worker image (027 residual risk corr-ts-37). It is not the same permission as fixing one user.
LICENSE_RESET_ACCOUNT licenseResetOnAccount One account's entitlements.
LICENSE_RESET_ALL resetLicenseOnAccounts Every account on the platform. Same split, same reason. Splits the existing LICENSE_RESET.

A11 — operational machinery (13 surfaces, 1 privilege today)

All four rows below split out of PLATFORM_OPERATIONS_ADMIN.

Proposed privilege Surfaces Why it separates
PLATFORM_REINDEX adminSearchIngestFromScratch, refreshAllBodiesOfKnowledge, cleanupCollections Destroy-and-rebuild of derived data. Search is degraded platform-wide until it completes; re-embedding every VC body of knowledge has a real external cost attached to it. Recoverable, but expensive and slow, and typically the thing you do want to delegate to whoever is debugging search.
PLATFORM_DATA_MIGRATION adminUpdateContributorAvatars, adminUpdateGeoLocationData, adminUploadFilesFromContentToStorageBucket, adminCommunicationMigrateOrphanedConversations, adminMigrateUserAvatar Idempotent bulk backfills. Low risk, high volume, run during upgrades. The lowest-privilege row here and a good candidate for a service account.
PLATFORM_MESSAGING_MANAGE adminCommunicationEnsureAccessToCommunications, adminCommunicationSyncSpaceHierarchy, adminCommunicationUpdateRoomState, adminCommunicationRemoveOrphanedRoom Matrix housekeeping — a distinct subsystem with its own operators. Open question below: adminCommunicationUpdateRoomState flips a room to public / world-visible, which is a user-visible privacy change wearing an ops-action costume, and adminCommunicationRemoveOrphanedRoom is an irreversible delete. Both may warrant splitting again.
PLATFORM_NOTIFICATION_PRUNE adminInAppNotificationsPrune Deletes notification records. Irreversible, unrelated to everything above.

Not maintenance at all — reclassify rather than split

Surfaces Proposal
updateAssistantActorCapabilities, aiServerCreateAiPersona These are configuration changes to AI behaviour that landed under the operations privilege because that is where the AI-server resolvers happened to sit. They change what the platform does, not repair it. They belong under a settings/configuration privilege — probably PLATFORM_SETTINGS_ADMIN — and should move rather than gain a new privilege of their own.

Net first pass: AUTHORIZATION_RESET and LICENSE_RESET become 4; PLATFORM_OPERATIONS_ADMIN becomes 4 plus one reclassification. 3 privileges → 8, covering 23 actions.

Second pass — the same pattern in #4764's own new privileges

#4764 introduces privileges at family granularity too, so they inherit the same property. Worth re-examining after Slice A ships, not before:

  • PLATFORM_USERS_ADMIN — gates login-email change, Kratos identity deletion, account deletion and PII read. Reading a user's PII to answer a support ticket is not the same act as deleting their identity; these are plausibly three privileges.
  • PLATFORM_SUPPORT_ORG_RESOURCES — gates organization-owned pack/hub updates and full template CRUD.
  • PLATFORM_CONTENT_FULL_ACCESS — already flagged in Proper design of platform level roles #4764 as the one role whose blast radius is deliberately not minimised (spec 027, SC-004's named exception). It is the obvious next target once the pattern is proven.

Acceptance criteria

  • Should let an operator run adminSearchIngestFromScratch while being denied authorizationPolicyResetAll — the headline case, impossible today
  • Should let an operator reset one user's authorization policy while being denied the platform-wide reset
  • Should hold every proposed privilege to a stated splitting rule (see below) rather than being split by subsystem or by file layout
  • Should keep Platform Operations Admin reaching everything it reaches today — the role gains the full new privilege set, so this is additive at role level and no operator loses access
  • Should declare each new privilege in the Proper design of platform level roles #4764 census (a.row.surfaces.ts) and satisfy the FR-034 reachability derivation, so the drift detector covers the new gates from day one
  • Should make each new privilege independently assertable in the test-suites role × A-row matrix (the matrix moves from ~10 reset cells on 2 privileges to per-privilege cells)
  • Should record which privilege authorized each action in the operations audit row, so the trail says how the action was authorized and not only that it happened
  • Should state, per split, whether Platform Operations Admin remains the sole owner or a second role becomes eligible — a split is only useful if someone can actually be given the narrow half

The splitting rule this should commit to

Privileges must be the unit of delegation, not the unit of implementation, or this ends in 60 privileges nobody can reason about. A split is justified when at least one holds:

  1. A plausible operator wants one and not the other — a search engineer re-indexing, a support agent resetting one user, an upgrade job backfilling avatars.
  2. Blast radius differs by an order of magnitude — one entity vs. the whole platform.
  3. Reversibility differs — an idempotent repair vs. an irreversible delete.

If none of the three holds, the actions stay on one privilege. This rule should live in ADR 0007 (or a successor ADR) so the next person adding an operational mutation has an answer for where to gate it.

Additional Context

Why this is not #4764

#4764 splits who — 13 roles. This splits what — the actions inside a role's boundary. They compose: 13 roles × per-action privileges is the point at which "least privilege" is a statement about actions rather than about job titles. Doing this one first would have been wrong (there was no role to grant the narrow half to); doing it never leaves Operations Admin as a smaller god mode.

Interaction with #6319 (time-boxed roles)

Per-action privileges and leases solve adjacent halves of the same problem, and together they are noticeably stronger than either alone: a 4-hour grant of AUTHORIZATION_RESET_PLATFORM is a defensible break-glass; a permanent grant of it is what we have today. No hard ordering dependency in either direction — leases operate on roles, this operates on privileges — but if both land, the natural shape is "narrow privilege, briefly held".

Cost, stated honestly

  • Enum churn: AuthorizationPrivilege gains ~8 values and, in the subtractive step, loses 3. The GraphQL enum is client-visible, so this is a breaking API change of exactly the kind Proper design of platform level roles #4764's Slice B already ships — worth asking whether it should ride that release rather than cut its own.
  • Credential rules multiply: each new privilege needs its grant in platform.service.authorization.ts and its census entry. Proper design of platform level roles #4764's Slice A took 11 privileges through that path, so the cost is measured, not guessed.
  • Matrix growth: the test-suites cross-check matrix is a role × surface cross-product; more privileges do not add surfaces, so cell count is roughly unchanged — but per-privilege denial assertions are new.
  • The counter-argument, fairly: nobody has yet asked to hold "re-index search" without "reset authorization". This is a posture improvement ahead of a demonstrated request. The SOC 2 / ISO 27001 least-privilege evidence named in Platform roles redesign: replace global admin with purpose-specific roles #6320 is the argument that it is worth doing anyway.

Scope boundary

In: the AUTHORIZATION_RESET / LICENSE_RESET / PLATFORM_OPERATIONS_ADMIN splits above; the reclassification of the two AI-configuration mutations; the splitting rule recorded as an ADR; census, reachability and matrix coverage for each new privilege; recording the authorizing privilege in the operations audit row.

Out: splitting #4764's other new privileges (second pass, after Slice A ships); any new role (this changes privileges, not the role set); space- or organization-scoped privileges; leases (#6319).

Sequencing

Depends on #4764 Slice A landing — the census, the reachability derivation and the drift detector are the mechanisms that make a privilege split verifiable rather than hopeful. Best done before Slice B if the enum break can ride that release; otherwise after.

Areas that will be affected

To be added during refinement — first indications: common/enums/authorization.privilege.ts, platform/platform/platform.service.authorization.ts, platform/platform-role/verification/ (census + grants + drift), platform-admin/** resolvers, services/ai-server, platform-admin/platform-audit-*, and the test-suites role matrix.


Filed while completing #4764 Slice A verification. The 23-action audit census above is grepped from develop + Slice A; the surface counts are from the #4764 census (a.row.surfaces.ts, rows A3 and A11).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Atlas TeamIssues and Epics worked on by team AtlasPFPlatform Foundation CircleauthorizationThe issue is related to the authorization frameworksecuritysecurity incidentserveruser story

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions