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.
The rules and where they’re enforced
Section titled “The rules and where they’re enforced”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.
Bug fixed from the original
Section titled “Bug fixed from the original”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.
- server/src/UserManagementService/user_management.py: routes and authorization.
PermissionErrormaps to 403 via an exception handler. - server/src/layers/cognito/cognito_user_management_service.py: Cognito admin calls with the second layer of checks.
- server/src/layers/cognito/user_management_util.py: create user (with tenant attributes), group helpers,
validate_user_tenancy. - server/src/layers/abstract_classes/idp_user_management_abstract_class.py: the IdP-independent interface.
- server/cdk/lib/tenant-template/services.ts: function, IAM grants on the pool, routes.
- server/tests/test_user_management.py: runs the real handler against moto’s in-memory Cognito.
Deploy
Section titled “Deploy”./scripts/deploy.sh you@example.comVerify
Section titled “Verify”A=$(./scripts/tenant-token.sh alice 'Passw0rd!Saas') # TenantAdmin, tenant-acurl -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.