Access Admins
saaslayer:access-admin is the reserved role that runs a deployment: users, orgs, catalogs (apply and
service registration), roles and machine trust (authorization matrix §4). This page covers the rule that a deployment never loses its last usable
administrator, how to hand the role to someone else, and how an existing administrator gets back in.
The last usable administrator
A deployment always keeps at least one usable human administrator. An administrator is usable when all of these hold:
- a
USERholds the role directly at deployment scope. Machines (service accounts, agents, IAM-mapped principals) never count, whatever they hold. - their sign-in is enabled. user-service records a disable or delete in rbac before it touches Cognito.
- their activation is recorded: they signed in themselves with a verified email, through
POST /me/activationor by accepting an offered seat. An email change that leaves the mailbox unverified clears the activation, and user-service records that in rbac before it touches Cognito.
rbac keeps one counter for the deployment (DEPLOYMENT / ADMIN_COUNT, a seat in src/last-holder.js). Every path
that could drop the count reserves on it first with a conditional write, so two parallel removals of the final two
administrators serialize and exactly one of them is refused. Refusals answer 409 with code LAST_ADMIN and log one
line {"event":"last_admin_guard","path","actorRole","targetSuid","outcome":"refused","code":"LAST_ADMIN"}.
| Path | Guarded by |
|---|---|
DELETE /users/{id}/roles/saaslayer:access-admin | reserve before the unassign |
POST /batch, changeset apply and rollback | every removal in the list reserved up front, all or nothing (a dry run checks only) |
user-service disable (U7) and delete (U9) | POST /users/{id}/sign-in {enabled:false} before Cognito |
user-service reserved-field change (U6) that leaves the email unverified | POST /users/{id}/activation {activated:false} before Cognito |
| Editing or deleting the reserved role | refused for every caller (409, reserved role) |
| Org member removal | doesn't apply: the role is held at deployment scope, not in an org |
An administrator whose activation was never recorded (for example the bootstrap admin before their first
POST /me/activation) doesn't count, but may still be a person who can sign in. Removing or disabling one is allowed
only while a counted administrator remains. On a deployment where nobody has recorded an activation, no administrator
can be removed or disabled until one does.
GET /access-admins lists every holder with activated, signInDisabled and usable, plus
meta.usableAdministrators, the counter.
Handing the role over: transfer and add-and-verify
A transfer is a durable operation. Its id (atr_…) is the operation id, and each step is recorded:
- Offer (
A1). An administrator callsPOST /access-admins/transfers {targetId, mode}. The target gets the role at once (the deployment delegation table, §5.2 row 2), but it doesn't count yet. Status:pending. Offers expire after 7 days by default (expiresInHours, at most 30 days). - Activate (
A3). The target signs in with their own deployment login (verified email, no impersonation) and callsPOST /me/access-admin-transfers/accept {transferId}. Their activation is recorded and they count. Status:activated. - Leave (
A2, only withmode: "transfer"). The initiator's assignment is removed through the same guarded path. Status:completed. Withmode: "add"(add-and-verify-another-admin) the initiator stays.
If an accept is interrupted between steps 2 and 3, both administrators remain and the status stays activated.
Calling accept again finishes the transfer, and calling it after completion returns the outcome. While an offer is
still pending, the initiator remains the deployment's usable administrator and can't be removed. The initiator can
cancel a pending offer with DELETE /access-admins/transfers/{id}, which also revokes the grant the offer
made. An expired offer can't be accepted. Its grant stays until it's cancelled.
The same steps from the CLI:
# as the current administrator (deployment login, --token or --iam)
saaslayer permissions access-admins list --api-base "$PERMISSIONS_URL"
saaslayer permissions access-admins offer u_newadmin --mode transfer --api-base "$PERMISSIONS_URL" # prints the transfer (id = operation id)
saaslayer permissions access-admins status atr_123 --api-base "$PERMISSIONS_URL"
# as the new administrator, with their own deployment login (saaslayer login --deployment …)
saaslayer permissions access-admins accept atr_123 --api-base "$PERMISSIONS_URL"
# record your own activation (e.g. the bootstrap administrator after first sign-in)
saaslayer permissions access-admins activate --api-base "$PERMISSIONS_URL"Keep two usable administrators (--mode add). With only one, losing that person's credentials leaves no in-product
way back (see below).
Recovering an existing administrator's access
These paths are separate from the first-admin delivery and its resend (B2) and recovery (B3), which belong to the
deployer's bootstrap of a deployment that has no usable administrator. None of them creates a standing superuser.
- Forgotten password. The administrator uses forgot-password at sign-in. Cognito mails a code to their verified
email, and a usable administrator has one by definition. Another administrator can start the same reset with
POST /api/users/{suid}/reset-password(saaslayer users reset-password). The current password stops working. - Lost MFA factors. Another administrator resets them:
POST /api/users/{suid}/reset-mfawith a reason (saaslayer users reset-mfa <user> --reason "…", or Reset MFA… on the console's user page; matrix U15). It removes the person's passkeys and Cognito TOTP/SMS MFA, clears their step-up evidence and signs them out everywhere; their password, grants and seat are kept, so they still count. With no strong factor left, a fresh magic-link sign-in lets them enroll a new passkey. The reset needssaaslayer:users:account:reset-mfa(this role holds it), is refused under impersonation, and writes onemfa_resetaudit line with the actor, target and reason. Nobody resets their own MFA without a fresh strong-factor step-up (401 STEP_UP_REQUIRED). MFA recovery codes are not issued yet: user-service can generate and verify them (src/mfa/recovery.js), but no route stores or accepts them. The only administrator lost every MFA factor. No one else holds
reset-mfa, andB3refuses while a usable administrator exists (409 ADMIN_EXISTS): the counter still counts that person. The way back is a second administrator:- If the administrator can still sign in (Cognito
MfaModeisOPTIONALorOFFand their password or a magic link works), they offer the role to a trusted person:saaslayer permissions access-admins offer <suid> --mode add. - That person accepts with their own deployment login (
saaslayer permissions access-admins accept <transferId>) and now counts. - The new administrator resets the first one's MFA (
saaslayer users reset-mfa <first admin> --reason "…").
If Cognito itself demands the lost factor at sign-in (
MfaMode: ON), the administrator can't sign in and there is no in-product path. Keep two administrators so this never happens.- If the administrator can still sign in (Cognito
Disabled by mistake. Another administrator enables them (
POST /api/users/{suid}/enable), and they count again. The last usable administrator can never be disabled in the first place.