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
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:
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)
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.
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.
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.
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
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 deniedauthorizationPolicyResetAll — 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:
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.
Blast radius differs by an order of magnitude — one entity vs. the whole platform.
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.
#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.
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 #4764Slice 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).
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-admininto 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_RESETgates 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_RESETwas carved out of the reset family rather than left underAUTHORIZATION_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 distinctactionstring per mutation. Grepping the source returns 23 named operational actions: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)
AUTHORIZATION_RESET_ENTITYauthorizationPolicyResetOnUser,authorizationPolicyResetOnOrganization,authorizationPolicyResetOnAccount,authorizationPolicyResetToGlobalAdminsAccessAUTHORIZATION_RESET_PLATFORMauthorizationPolicyResetOnPlatform,authorizationPolicyResetAll,authorizationPlatformRolesAccessReset,aiServerAuthorizationPolicyResetauth-reset-worker.authorizationPolicyResetAllis 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 (027residual riskcorr-ts-37). It is not the same permission as fixing one user.LICENSE_RESET_ACCOUNTlicenseResetOnAccountLICENSE_RESET_ALLresetLicenseOnAccountsLICENSE_RESET.A11 — operational machinery (13 surfaces, 1 privilege today)
All four rows below split out of
PLATFORM_OPERATIONS_ADMIN.PLATFORM_REINDEXadminSearchIngestFromScratch,refreshAllBodiesOfKnowledge,cleanupCollectionsPLATFORM_DATA_MIGRATIONadminUpdateContributorAvatars,adminUpdateGeoLocationData,adminUploadFilesFromContentToStorageBucket,adminCommunicationMigrateOrphanedConversations,adminMigrateUserAvatarPLATFORM_MESSAGING_MANAGEadminCommunicationEnsureAccessToCommunications,adminCommunicationSyncSpaceHierarchy,adminCommunicationUpdateRoomState,adminCommunicationRemoveOrphanedRoomadminCommunicationUpdateRoomStateflips a room to public / world-visible, which is a user-visible privacy change wearing an ops-action costume, andadminCommunicationRemoveOrphanedRoomis an irreversible delete. Both may warrant splitting again.PLATFORM_NOTIFICATION_PRUNEadminInAppNotificationsPruneNot maintenance at all — reclassify rather than split
updateAssistantActorCapabilities,aiServerCreateAiPersonaPLATFORM_SETTINGS_ADMIN— and should move rather than gain a new privilege of their own.Net first pass:
AUTHORIZATION_RESETandLICENSE_RESETbecome 4;PLATFORM_OPERATIONS_ADMINbecomes 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 (spec027, SC-004's named exception). It is the obvious next target once the pattern is proven.Acceptance criteria
adminSearchIngestFromScratchwhile being deniedauthorizationPolicyResetAll— the headline case, impossible todaya.row.surfaces.ts) and satisfy the FR-034 reachability derivation, so the drift detector covers the new gates from day onetest-suitesrole × A-row matrix (the matrix moves from ~10 reset cells on 2 privileges to per-privilege cells)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:
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_PLATFORMis 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
AuthorizationPrivilegegains ~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.platform.service.authorization.tsand 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.test-suitescross-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.Scope boundary
In: the
AUTHORIZATION_RESET/LICENSE_RESET/PLATFORM_OPERATIONS_ADMINsplits 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 thetest-suitesrole 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).