Skip to content

Tenant user management

Let a tenant manage its own users through the API: a TenantAdmin creates, updates, disables, enables and deletes users of their own tenant, and a TenantUser can only read the user list. Nobody can reach users of another tenant, and nobody can grant a role outside the tenant roles.

Method Path Allowed for Body
GET /users TenantAdmin, TenantUser
GET /users/{username} TenantAdmin, TenantUser
POST /users TenantAdmin {"userName","userEmail","userRole"}
PUT /users/{username} TenantAdmin {"userEmail"?, "userRole"?}
DELETE /users/{username}/disable TenantAdmin
PUT /users/{username}/enable TenantAdmin
DELETE /users/{username} TenantAdmin

Unlike products and orders, this is one Lambda with a Powertools APIGatewayRestResolver routing all /users paths.

Users of all pooled tenants live in one Cognito user pool. IAM can’t say “only users in your tenant’s group”, so these rules are enforced in code:

Rule How
Tenant context comes from the token, not the request tenantId, userRole (the caller’s role) and tenantTier are read from the authorizer context. A tenantId in the body is ignored.
Only a TenantAdmin can change users auth_manager.isAuthorizedToManageUsers(actorRole)
Only tenant roles can be assigned auth_manager.isRecognizedTenantRole(userRole): SystemAdmin and anything else gets 403. This is the escalation ceiling, even for TenantAdmins.
Only users of your own tenant Every user is in a Cognito group named after the tenant. Listing uses ListUsersInGroup(tenantId), and every single-user operation first checks AdminListGroupsForUser(username) contains the caller’s tenant.
A user can’t edit their own record PUT /users/{me} is admin-only, because editing yourself would mean choosing your own role.

The checks run in the REST handler and again in the Cognito service class that makes the admin calls (defense in depth). The rules themselves are defined once, in auth_manager.py.

The function’s IAM role allows the Cognito admin actions only on this stack’s user pool.

The original create_user sets only email, email_verified and custom:userRole. A user created through the API therefore has no custom:tenantId or custom:tenantTier, and the authorizer can’t serve them. Here new users inherit the creating admin’s tenantId and tenantTier from the authorizer context. In the verify output below, carol’s /whoami works.

Terminal window
./scripts/deploy.sh you@example.com
Terminal window
A=$(./scripts/tenant-token.sh alice 'Passw0rd!Saas') # TenantAdmin, tenant-a
curl -s -X POST -H "Authorization: Bearer $A" ${API}users \
-d '{"userName":"carol","userEmail":"carol@example.com","userRole":"TenantUser","tenantId":"tenant-b"}'

Cognito emails carol a temporary password. For testing, set one directly: aws cognito-idp admin-set-user-password --user-pool-id <pool> --username carol --password 'Passw0rd!Saas' --permanent.

Results from the test deployment:

Who Call Result
alice create carol (body says tenantId: tenant-b) 200
carol /whoami tenantId: tenant-a, tenantTier: BASIC, userRole: TenantUser. The body’s tenant-b was ignored and the attributes were inherited.
carol create a user 403
carol PUT /users/carol {"userRole":"TenantAdmin"} 403
alice list users [alice (TenantAdmin), carol (TenantUser)]
bob list users [bob (TenantUser)]
alice GET / disable bob (tenant-b) 403 each
alice make carol SystemAdmin 403
alice disable carol, GET carol, enable carol 200, "enabled": false, 200

Unit tests: 42 passed.

Step 08 automates onboarding: registering a tenant in the control plane triggers a provisioning job that creates the tenant admin in this user pool.