OutlabsAuth
Auth

User Management API

Admin and self-service user HTTP surface.
HTTP surface for user admin and self-service. Mount details: Routers & Prefixes.

Paths below assume the common example mount prefix="/v1/users". Your host may use /iam/users or another base — only the suffix after that prefix matters.

When this page and a running app disagree, trust live OpenAPI at {your-api}/docs and outlabs_auth/routers/users.py.


What to mount

FactoryWhen
get_users_router(auth, prefix="/v1/users")Full admin + self-service (examples, OutlabsAuth UI)
get_self_service_users_router(...)Tiny embed: own profile + permission names only
from outlabs_auth.routers import get_users_router

app.include_router(get_users_router(auth, prefix="/v1/users"))

Optional: requires_verification=True tightens auth on /me* routes.

Invites are mostly on the auth router — see User Invitations. This router only exposes POST /{user_id}/resend-invite. Entity membership CRUD lives on the memberships router — see Entity Memberships.


Permissions at a glance

ActionTypical permission
Create useruser:create
List / get / roles / audit / sessions (admin)user:read
Update profile, status, roles, revoke sessions/keysuser:update
Soft-deleteuser:delete
Grant/revoke is_superuserSuperuser only (require_superuser)
/me* self-serviceAuthenticated (optional email verification)

Simple vs Enterprise scope

Same router for both presets. Behavior depends on flags:

ConcernBehavior
SimpleRBAC (enable_entity_hierarchy=False)Scope filtering is effectively off — list/get are system-wide for permitted actors
Enterprise + enforce_user_scope=True (default)Non-global actors only see users rooted in, or holding an active membership in, their scope. Out of scope → 404 (not 403). Self always allowed. The same rule protects GET /memberships/user/{id}, GET /permissions/user/{id} and POST /permissions/check
Mutations (Enterprise)Only accounts of the actor's own tenant — rooted at the actor's root or below it — can be modified (profile, password, status, restore, delete, invite resend, roles, sessions, API keys). An account seen only through a membership (an unrooted legacy member, or a member of a subtree a global actor moved to another tree on an earlier release — such moves now fail closed, decision 17) stays readable but answers 403 (DD-061 decision 16); unrooted non-global actors modify no accounts
Global actorsSuperusers and holders of an active direct system-wide role. Only a global actor may grant a system-wide role directly (POST /{user_id}/roles, invite without entity, reactivation), and only a global actor may mutate a global actor's account (profile, password, status, delete, roles, sessions, API keys → 403 for tenant admins; reads stay allowed). That includes any account with a direct system-wide role row that is scheduled, suspended, expired or revoked, since it can become active later
root_entity_id on listNarrows within the actor’s scope; never widens it
/orphaned, /{id}/membership-historyMeaningful when membership service exists (Enterprise); otherwise empty pages

Set enforce_user_scope=False only as a transitional escape hatch.


Admin CRUD and lifecycle

MethodPathPermissionNotes
POST/user:createAdmin create (not public register). Only superusers may set is_superuser. Tenant-scoped actors must name a root_entity_id inside their scope and in their own root's tree (403 otherwise; an in-scope root reached only through a membership in another tenant's tree answers 403 with details.reason = root_entity_outside_tenant, DD-061 decision 17); omitted, it defaults to the actor's own root. Unrooted non-global actors create no accounts
GET/user:readPaginated list. Query: page, limit, search, status, is_superuser, root_entity_id
GET/{user_id}user:readSingle user (scoped)
PATCH/{user_id}user:updateAdmin profile update
DELETE/{user_id}user:deleteSoft delete
PATCH/{user_id}/passworduser:updateAdmin set password (no current password)
PATCH/{user_id}/statususer:updateactive / suspended / banned only — not deleted
POST/{user_id}/restoreuser:updateRestore a deleted identity (does not restore grants/credentials)
PATCH/{user_id}/superuserSuperuserGrant/revoke platform superuser; cannot revoke yourself
POST/{user_id}/resend-inviteuser:updateNew invite token for INVITED users

List status filter: active | suspended | banned | deleted.

Create body (UserCreateRequest): email, password, optional names / phone / is_superuser.
Update body (UserUpdateRequest): email, names, phone (partial).
Responses: UserResponse (UUID id, profile fields, status flags, has_password — false for OAuth-only, magic-link-only and invited accounts).
List: PaginatedResponse[UserResponse] (items, total, page, limit, pages).


Self-service (/me)

MethodPathAuthNotes
GET/meAuthenticatedOwn profile
PATCH/meAuthenticatedOwn profile (SelfUserUpdateRequest). Changing email is refused (403) unless the host sets allow_self_service_email_change; then current_password is required. Resending the unchanged email is accepted
POST/me/change-passwordAuthenticatedChangePasswordRequest (current + new) → 204
POST/me/phone/request-codeAuthenticatedOTP to registered phone (rate-limited) → 204
POST/me/phone/verify-codeAuthenticatedPhoneVerifyCodeRequest → UserResponse

Roles and effective permissions

Direct role memberships (flat RBAC and Enterprise “direct” roles):

MethodPathPermissionNotes
GET/{user_id}/rolesuser:read?include_inactive → RoleResponse[]
GET/{user_id}/role-membershipsuser:readMembership rows + embedded role
POST/{user_id}/rolesuser:updateAssignRoleRequest → membership (201). Actor must hold all permissions on the role where it takes effect (an org-scoped role: at its root). Enterprise: a system-wide role needs a global actor (403); a role outside the actor's scope answers 404 like a missing one; an org-scoped role only goes to a user rooted in its organization (422, details.reason = role_root_mismatch, for every actor)
DELETE/{user_id}/roles/{role_id}user:updateSoft-revoke
PATCH/{user_id}/role-memberships/{membership_id}user:updateValidity window / status. Reactivating or widening the window re-runs the assignment checks (403 / 404 / 422 as for POST /{user_id}/roles); suspending or narrowing never does
GET/{user_id}/permissionsSelf or user:readEffective perms: direct roles and entity-membership roles when present → UserPermissionSource[]

Entity-scoped role assignment (membership + roles on an entity) is not here — use Entity Memberships.


Orphans and membership history (Enterprise)

MethodPathPermissionNotes
GET/orphaneduser:readUsers with no active entity memberships. Scoped actors see orphans rooted inside their scope. Soft-deleted users are excluded unless ?status=deleted
GET/{user_id}/membership-historyuser:readAppend-only entity membership lifecycle events

Personal API keys (admin view)

MethodPathPermissionNotes
GET/{user_id}/api-keysuser:readList personal keys (no secrets)
DELETE/{user_id}/api-keys/{key_id}user:updateAdmin revoke

Self-service key minting and system/integration keys: API Key Host Integration.


Sessions, social accounts, audit (elsewhere)

Do not duplicate those tables here:

AreaGuide
List / revoke sessions (/me/sessions, /{user_id}/sessions)Sessions & Audit
Per-user audit (/{user_id}/audit-events) + cross-user searchsame
Link / unlink social (/me/social-accounts)OAuth & Social Login

Minimal self-service router

For hosts that only need “who am I?”:

from outlabs_auth.routers import get_self_service_users_router

app.include_router(get_self_service_users_router(auth, prefix="/v1/account"))
MethodPathResponse
GET/meUserResponse
GET/me/permissionslist[str] — permission names only (not UserPermissionSource)

Schemas quick reference

ModelRole
UserResponseProfile payload
UserCreateRequest / UserUpdateRequestAdmin create / patch
ChangePasswordRequest / AdminResetPasswordRequestPassword changes
UserStatusUpdateRequest / UserSuperuserUpdateRequestStatus / superuser
PhoneVerifyCodeRequestPhone OTP confirm
AssignRoleRequest / UserRoleMembershipUpdateDirect roles
UserRoleMembershipResponse / UserRoleMembershipDetailResponseMembership rows
UserPermissionSourceEffective permission + source metadata
OrphanedUserResponse / MembershipHistoryEventResponseEnterprise helpers
PaginatedResponse[T]List envelopes