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 USER holds 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/activation or 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"}.

PathGuarded by
DELETE /users/{id}/roles/saaslayer:access-adminreserve before the unassign
POST /batch, changeset apply and rollbackevery 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 unverifiedPOST /users/{id}/activation {activated:false} before Cognito
Editing or deleting the reserved rolerefused for every caller (409, reserved role)
Org member removaldoesn'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:

  1. Offer (A1). An administrator calls POST /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).
  2. Activate (A3). The target signs in with their own deployment login (verified email, no impersonation) and calls POST /me/access-admin-transfers/accept {transferId}. Their activation is recorded and they count. Status: activated.
  3. Leave (A2, only with mode: "transfer"). The initiator's assignment is removed through the same guarded path. Status: completed. With mode: "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-mfa with 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 needs saaslayer:users:account:reset-mfa (this role holds it), is refused under impersonation, and writes one mfa_reset audit 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, and B3 refuses while a usable administrator exists (409 ADMIN_EXISTS): the counter still counts that person. The way back is a second administrator:

    1. If the administrator can still sign in (Cognito MfaMode is OPTIONAL or OFF and their password or a magic link works), they offer the role to a trusted person: saaslayer permissions access-admins offer <suid> --mode add.
    2. That person accepts with their own deployment login (saaslayer permissions access-admins accept <transferId>) and now counts.
    3. 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.

  • 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.