Administering roles safely
All names, domains, and identifiers in these examples are fictional. So far the Gazette's checks have protected photos and albums. The roles and assignments that decide who can touch those photos deserve at least as much care, because anyone who can change them can change everything else.
Who can change access
In the Gazette's library, changing access is itself an action, checked like any other. Two permissions cover it, and they are separate on purpose. role.edit lets someone create roles and change what they contain. role.assign lets someone give a role to a person or group in some scope, or take it away.
The two have very different reach. Assigning Contributor to Lena on the festival album changes one person's access to one album. Adding a permission to Contributor changes the access of every Contributor, in every scope where the role is assigned, all at once. Theo, as library administrator, holds both. The sports desk lead holds a custom Sports desk lead role that includes role.assign in the Sports library, so new freelancers can join sports assignments without waiting for Theo. The desk lead cannot change what Contributor means for the whole Gazette, and cannot assign anything in the News library: an assignment asks can(user, 'role.assign', scope) for the library or album it will cover, like any other check.
Granting only what you hold
Being allowed to assign roles cannot mean being allowed to assign any role. If the desk lead could assign Legal reviewer, the desk lead could assign it to their own account, lift the hold on a photo the legal team had frozen, and delete it. role.assign would have become a route to every permission in the library.
The rule that prevents this is a delegation limit: whoever grants access may grant only permissions they hold themselves, and only within the scope where they hold them. The desk lead does not hold photo.place_hold, so any assignment of Legal reviewer from the desk lead is refused, whoever it is for. Delegation still needs a starting point, and in the Gazette that is the Library administrator role, which may grant any permission in the catalog. That is why only Theo and a deputy administrator hold it, and why their own changes are recorded and reviewed.
With the direct route closed, a quieter one remains. The sports desk later asks to maintain its own custom roles, and Theo adds role.edit to the Sports desk lead role, limited to roles used in the Sports library. Now the desk lead edits the Sports desk lead role, the one they already hold, and adds photo.place_hold. No new assignment is made, so a library that applies delegation limits only to assignments finds nothing to check, and the desk lead's next request runs with legal hold powers all the same. Editing a role you hold is a way of granting yourself permissions without making an assignment.
The same delegation limit therefore applies to role edits. Every permission added to a role must be one the editor holds, in every scope where the role is assigned, because an edit reaches every holder of the role. Here is the desk lead's attempt, and the answer:
PATCH /roles/sports-desk-lead HTTP/1.1
Host: api.photos.example
Authorization: Bearer demo-access-token-31
Content-Type: application/json
{
"add_permissions": ["photo.place_hold"]
}
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "delegation_not_allowed",
"message": "This change would grant a permission you cannot delegate.",
"request_id": "req-7f3a"
}
The response says what kind of refusal this is and gives a request ID to quote when asking for help. It does not list the permissions the desk lead holds, the role's other holders, or the rule that refused the change. Error messages that spelled all of that out would become a map of the library's access. The full detail goes to the library's audit trail instead:
{
"time": "2026-07-14T14:22:09Z",
"request_id": "req-7f3a",
"operation": "update_role",
"actor": "user-1745",
"role": "sports-desk-lead",
"outcome": "rejected",
"reason": "delegation_limit",
"permissions_not_delegable": ["photo.place_hold"],
"actor_holds_role": true
}
The record names the specific permission and role, and notes that the desk lead holds the role being edited, which is exactly what someone looking into an attempted escalation needs. It holds no token and no copy of the request.
A delegation limit is the least a library should enforce. Many organizations also separate editing roles from assigning them, so that a change to a role and an assignment of it pass through different people, or require a second approver for any edit to a role the editor holds. Separation in administration describes those arrangements, and Escalating privilege follows the routes an attacker takes where they are missing.
Separating duties
Some risks come not from one person holding too much, but from one person doing two things that are meant to check each other. Maya's fortnight covering the sports picture desk for Omar, from Designing a role model, is an example. With Picture editor in the Sports library, Maya can approve photos for publication, including the festival photos Maya took. Approval is supposed to be a second person's judgment that a photo is accurate, fairly captioned and safe to run, and approving your own photo skips the second person.
Separation of duties arranges access so that certain combinations of actions need two different people. Static separation says a person may never hold both of two conflicting roles. The Gazette uses it for Legal reviewer and Picture editor in the same library, because the people who decide that a photo must be preserved should not also be able to lift the hold and delete it. Static rules are checked when access is granted, including through groups: adding Raj to Sports desk editors would create the conflict without any new assignment, so group changes need the same check.
Static separation is too blunt for Maya's case. A rule that nobody may hold both photo.upload and photo.approve would rule out the Picture editor role itself, since picture editors routinely upload pictures they took. What the Gazette wants is narrower: any picture editor may approve photos, just not their own uploads. That is per-item separation: the same person may not perform both steps on the same item. It is related to dynamic separation, in which a person may hold two conflicting roles but not activate both in the same session, and like dynamic separation it is checked when access is used rather than when it is granted. Here it is checked at decision time against the particular photo:
forbid photo.approve
when resource.uploaded_by is subject.id
In the Gazette's policy a forbid rule overrides anything that grants access, so no role assignment can outweigh this one. During the fortnight Maya can approve other people's photos, and Maya's own wait for another editor. The rule is only as reliable as uploaded_by, which is why the library sets that field itself at upload and never accepts it from a request. The same idea applies to access: the Gazette requires a second administrator to approve any assignment Theo makes to Theo's own account. Separation of duties, in the governance lessons, covers how organizations find conflicting combinations like these and handle the exceptions a small team cannot avoid.
Keeping someone in charge
Theo and the deputy administrator are the only people who can change roles across the whole Gazette. If both of their Library administrator assignments disappeared, nobody could assign or edit roles anymore, including the role that would let someone repair the situation. The organization would be locked out of its own access management.
The library prevents this by refusing any change that would leave the organization without an active administrator. If Theo, tidying up old assignments, removes Theo's own administrator assignment while the deputy's account is suspended, the removal is refused. The same check covers indirect routes, such as disabling the last administrator's account or removing them from a group that carries the role. A suspended account or an assignment about to expire does not count as keeping anyone in charge.
Concurrent changes need more than a simple count. Suppose Theo and the deputy each remove the other's administrator assignment at the same moment. Each request checks whether another administrator would remain, and each finds one: the person sending it. Both checks pass, both removals are saved, and the Gazette has no administrator. Each request checked a state that the other was about to change.
The check and the change have to happen as one step that no other change can slip between. The library performs both inside a single database transaction that first locks the organization's set of administrator assignments. The second removal waits for the first to finish, counts again, finds one administrator left, and is refused.
Organizations still sometimes need a way back in, for example when both administrators leave in the same week, or an administrator's account is compromised and has to be disabled. That path is emergency access, usually through emergency administrator accounts kept outside ordinary role management, and it needs controls of its own so it does not become a permanent back door. Temporary, privileged, and emergency access covers how those accounts are used and reviewed, and Protecting privileged access covers how they are protected and tested.
When access is taken away
Once the festival photos are in, Theo removes Lena's assignment at 10:00 rather than waiting for it to expire at the end of the month. From the library's point of view the assignment is gone immediately. Whether Lena's next request is refused depends on where that request gets its answer.
If can reads Lena's current assignments when it decides, as the Gazette's does, Lena's next upload at 10:01 is refused. Several common designs answer from a copy instead, and each copy lags behind:
- Sessions that cached permissions at sign-in. If the library's web app worked out Lena's permissions at sign-in, at 08:00, and stored them in the session, the session keeps answering yes until it ends, perhaps many hours later.
- Tokens carrying role claims. An access token issued at 09:45 with
"roles": ["contributor"]still carries that claim until it expires at 10:45, and an API that trusts the claim keeps accepting it. - Caches. A cache of effective permissions or recent decisions, kept to make
canfaster, holds the old answer until its entry expires or is cleared.
Each copy exists for a good reason, usually speed. The design question is how long each may lag, and that should be decided rather than discovered after a removal fails to take effect. Keep copies short-lived, clear cached permissions when assignments or group memberships change, and end or refresh a person's sessions when their access is removed. For actions where even a few minutes of stale access would matter, such as approving, deleting, downloading unpublished photos or changing roles, check current assignments even when a cached answer is at hand. Staying signed in described how sessions end, and Token revocation described what revoking a token can and cannot reach before it expires.