User Management
Manage user accounts, active status, password policies, and TOTP
User management lives on the dashboard Users page (/users), and permission depends on the action: read admin:user:read, create admin:user:create, update/activate/deactivate/password policy admin:user:update, delete admin:user:delete. The built-in Admin role holds all four.
This page lists the real endpoint for every action, its validation rules, and what failure looks like. How user roles affect permissions is covered in Roles & Permissions.
Listing and Filtering
GET /api/v1/admin/users (admin:user:read) returns a paginated user list with these parameters:
| Parameter | Description |
|---|---|
page / page_size | Pagination, defaults page=1, page_size=20 |
search | Fuzzy match on username or email (case-insensitive) |
status | Multi-select: active, inactive, pending |
role | Filter by global role name |
exclude_user_id | Exclude specific user IDs (used by pickers) |
Status semantics: active = is_active=true; inactive = is_active=false with approval_status=approved; pending = awaiting approval (approval_status=pending, is_active=false).
The header stats come from GET /api/v1/admin/users/stats (admin:user:read), returning total / active / inactive / pending. The list deliberately does not expose login count, failed-login count, or last password change — the API omits those fields.
Creating Users
POST /api/v1/admin/users (admin:user:create):
- Click Create User.
- Fill in Username (globally unique), Email (globally unique, must be a valid address), Password, and the Active switch (
is_active). - Save.
Backend behaviour and validation:
| Case | Result |
|---|---|
| Username taken | user_with_username_exists |
| Email taken | user_with_email_exists |
No locale provided | Falls back to the site setting default_language (default en) |
| — | The new user's approval_status is forced to approved |
The create form does not accept roles: a new user has no global role at all and must be assigned one afterwards, otherwise they can only see public resources and cannot operate inside a team.
Bulk Import / SSO Provisioning
Not implemented / Roadmap. There is no CSV/JSON/Excel user import; users are created one at a time (
POST /api/v1/admin/users) or auto-provisioned on first SSO login.
SSO provisioning is driven by site settings (Site Settings → SSO/Security):
| Setting | Description |
|---|---|
sso_auto_create_users | Create the account automatically on first SSO login |
default_role_id | Default global role for new users; first initialization points it at Viewer |
default_team_id + default_team_role | Default team and membership role; default_team_role accepts only viewer/member/admin and defaults to member; owner is never auto-assigned |
sso_match_by_email | Match an existing account by email |
sso_require_approval | New accounts require approval (approval_status=pending) |
Not implemented / Roadmap: SSO provisioning has no email-domain allowlist.
Viewing User Details
Click a user to open the detail/edit panel (GET /api/v1/admin/users/{user_id}, admin:user:read). Available fields:
- Identity:
username,email,is_active,approval_status,is_superuser,auth_source(local/sso),email_verified - Preferences:
locale,avatar_url - Timestamps:
created_at,last_login - Password state:
force_password_change,password_expiration_exempt(rendered as a password-status badge in the list) - Authorization:
roles(array of global roles) - SSO:
sso_connections(provider, external account, first/last login); disconnect is available withadmin:sso:update
Not implemented / Roadmap: there is no per-user activity view, login history, active-session list, or session revocation inside user management. Account lockout is enforced by the global login policy
max_login_attempts/lockout_duration_minutes; review user actions in Audit Logs filtered by user.
Editing Users
PUT /api/v1/admin/users/{user_id} (admin:user:update). Submittable fields:
| Field | Description |
|---|---|
email | Change to a new address |
is_active | Activate/deactivate switch |
roles | Array of global role names, replaced wholesale |
password | Optional. Submitting sets a new password (must pass strength validation; failure returns password_validation_failed with data.errors.password) |
avatar_url / locale | Avatar and language preference |
username cannot be changed: the UserUpdate schema has no such field and the edit form has no username input. A wrong username can only be fixed by deleting and recreating the account.
Role Change Protection Rule
roles replaces the set (clear first, then write — omitted roles are lost; unmatched names are ignored). If the target is still owner/admin of some team and the new role set no longer contains team:manage (or *), the request fails with 403 operation_not_permitted (permission: team:manage). Downgrade their team role first, then strip the global role — the reverse order is blocked.
Submitting a password sends the user a user.password_reset notification.
Account Status
| Action | Endpoint | Permission | Notes |
|---|---|---|---|
| Activate | POST /api/v1/admin/users/{user_id}/activate | admin:user:update | Sets is_active=true and approval_status=approved; an already-active user returns user_already_active |
| Deactivate | POST /api/v1/admin/users/{user_id}/deactivate | admin:user:update | Sets is_active=false, approval_status=approved; superusers are rejected (cannot_deactivate_superuser); an already-inactive user returns user_already_inactive |
What deactivation actually does:
- No sign-in: both JWT and API-key authentication reject an
is_active=falseuser (401inactive_user; a pending user getspending_approval_user). - Integrations using that user's API keys fail immediately — the key record is untouched, but authentication rejects it because the user is inactive.
- Resources, team memberships, and conversations are all preserved; reactivating restores access.
Both actions send the user a user.activated / user.deactivated notification and write an audit entry (activate_user / deactivate_user).
Deleting Users
DELETE /api/v1/admin/users/{user_id} (admin:user:delete). The only hard block: an is_superuser=true account returns cannot_delete_superuser. The frontend delete dialog requires typing the username to confirm and lists the impact.
Deletion impact (per model FK rules, verified in models/*.py):
| Deleted (CASCADE) | Nulled/preserved (SET_NULL) |
|---|---|
| The account itself, password history, notifications and read records | Agents / knowledge bases / documents / tools / skills they created (created_by nulled, records kept) |
| Personal conversations and their messages, agent runs | Teams they owned (Team.owner nulled; the team itself is kept) |
| Their API keys and personal memories | Audit logs (kept for compliance, not deleted with the account) |
| All team memberships and scoped role records | Shared resources and other members inside those teams |
Deleting a team owner leaves the team ownerless
TeamMember rows cascade with the user, so deleting a team's owner also deletes their owner membership, Team.owner is nulled, and the team has no owner afterwards — only a superuser can change member roles or restore ownership. To remove a team owner, transfer ownership inside the team first, then delete the account.
Passwords and Password Expiration
There is no "reset to a temporary password" or "send a reset email from the admin panel" flow. Administrators combine these per-user operations with the global password policy:
| Operation | Endpoint | Permission | Behaviour |
|---|---|---|---|
| Force change on next sign-in | POST /api/v1/admin/users/{user_id}/force-password-change | admin:user:update | Sets force_password_change=true and sends a high-priority notification |
| Reset expiration timer | POST /api/v1/admin/users/{user_id}/reset-password-expiration | admin:user:update | Sets password_changed_at=now, recomputes password_expires_at from policy, clears the expiry-notified timestamp |
| Grant/revoke exemption | POST /api/v1/admin/users/{user_id}/exempt-password-expiration with body {"exempt": true/false} | admin:user:update | Writes password_expiration_exempt; exempt=true also clears force_password_change |
| Bulk force change | POST /api/v1/admin/users/bulk-force-password-change with body {"user_ids": [...]} | admin:user:update | Flags each user and notifies them; returns {"count": n}; an empty array is no_users_selected, all-missing is no_users_found |
| Expiration overview | GET /api/v1/admin/users/password-expiration-stats | admin:user:read | Returns total_users/expired_count/expiring_soon_count/force_change_count/exempt_count; if the site setting password_expiration_enabled=false, all counts are 0 |
| Expired/expiring list | GET /api/v1/admin/users/expiring-passwords | admin:user:read | Paginated (page, page_size max 100), filter is all/expired/expiring/force_change; the "expiring soon" window comes from password_expiration_warning_days (default 7 days) |
The frontend exposes these in the detail panel and the row menu, and only when auth_source=local (SSO users have no local password); the global policy is configured in Site Settings → Security.
TOTP (2FA)
| Capability | Endpoint | Permission | UI |
|---|---|---|---|
| TOTP adoption stats | GET /api/v1/admin/totp/stats | admin:dashboard:access | The TOTP card on the dashboard (totp_enabled / total_users / adoption_rate) |
| Check a user's TOTP status | GET /api/v1/admin/totp/users/{user_id}/status | superuser | API only, no UI |
| Disable a user's TOTP | POST /api/v1/admin/totp/users/{user_id}/disable | superuser | API only, no UI |
Users enable, disable, and regenerate backup codes themselves in personal settings (see Sign-in & Accounts).
# Requires a superuser token
curl "https://your-domain.com/api/v1/admin/totp/users/$USER_ID/status" \
-H "Authorization: Bearer $ADMIN_TOKEN"
curl -X POST "https://your-domain.com/api/v1/admin/totp/users/$USER_ID/disable" \
-H "Authorization: Bearer $ADMIN_TOKEN"The user-management UI has no per-user TOTP toggle and no "force 2FA" switch; the available capability is aggregate statistics plus a superuser API fallback to disable TOTP (for example when a user loses their authenticator).
Bulk Operations
Selecting several users opens a floating toolbar at the bottom. Note that several of these are frontend loops over the single-user endpoint — there is no matching bulk endpoint:
| Action | Implementation | Requires |
|---|---|---|
| Send notification | POST /api/v1/admin/notifications (one per user) | admin:notification:create |
| Activate | Loop POST .../{user_id}/activate | admin:user:update |
| Deactivate | Loop POST .../{user_id}/deactivate | admin:user:update |
| Delete | Loop DELETE .../{user_id} | admin:user:delete |
| Force password change | POST /api/v1/admin/users/bulk-force-password-change (a real bulk endpoint) | admin:user:update |
| Grant / revoke password exemption | Loop POST .../{user_id}/exempt-password-expiration | admin:user:update |
Because most loops have no server-side batch, one failure does not roll back the others; the toast count may not match the actual number of successes, so re-check users whose permissions/state conflicted before retrying.
Not implemented / Roadmap: no bulk add/remove from teams, no bulk export, no CSV import/export.
Sending Email
POST /api/v1/admin/users/send-email with body {"user_ids": [...], "subject": "...", "content": "..."}, requires admin:user:update. The response is {"sent_count": n, "skipped_count": n, "total": n}.
Limits (exceeding them fails the request; there is no partial send):
| Limit | Value | Error |
|---|---|---|
SMTP disabled (smtp_enabled=false) | — | smtp_not_configured |
| Per-admin hourly send volume | 100 | email_rate_limit_exceeded (data.limit=100, data.period=hour) |
| Per-recipient daily volume | 5 | that recipient is skipped and counted in skipped_count |
| Request size above the admin's remaining quota | — | email_quota_insufficient (data.requested / data.remaining) |
User Roles
Global roles (multi-select, roles) determine system-level and dashboard capability, while team roles determine in-team level. The 5 built-in roles and their permission sets are in Roles & Permissions:
| Role | Rough capability |
|---|---|
| Super Admin | *, bypasses all checks |
| Admin | Dashboard read/write plus all team resource permissions |
| Team Admin | Member plus team settings and member management |
| Member | Create/edit/run resources inside a team |
| Viewer | Read plus use; the default role |
Team roles (owner/admin/member/viewer) are maintained on the teams pages, not in the user detail: the user detail only displays team membership.
Best Practices
✅ Do:
- Review the user list regularly and deactivate leavers promptly (deactivation blocks sign-in and API keys immediately)
- Assign the minimal necessary global role right after creation
- Require 2FA for privileged accounts and keep a normal rotation policy rather than relying on
password_expiration_exempt - Work through the
expiring-passwordslist ahead of expiry instead of firefighting lockouts - Leave a trail around deactivate/delete/role changes (audit logging does this automatically)
❌ Don't:
- Share an administrator account (audit logs can no longer attribute actions)
- Delete a team owner's account directly (the team becomes ownerless)
- Replace a role set with
PUT /admin/users/{id}and forget a role - Rely on hidden frontend buttons for access control — the endpoint validates independently
Troubleshooting
Cannot Create a User
- Check whether the username or email is already taken (
user_with_username_exists/user_with_email_exists). - Check the email format is valid.
- Check that the caller holds
admin:user:create.
A User Cannot Sign In
- Verify
is_active=true(deactivated or pending users get 401). - Verify the password; if needed, force a change or set a new password first.
- Verify the account is not locked by the login policy (
max_login_attempts/lockout_duration_minutes). - Check TOTP state; if the user lost their authenticator, a superuser disables TOTP through the API (no UI).
- Investigate suspicious sign-ins in Audit Logs filtered by user.
Cannot Delete a User
- The target is a superuser →
cannot_delete_superuser(remove the superuser flag first, or deactivate instead). - The target owns a team → the backend does not block the deletion, but the team loses its owner; transfer ownership inside the team first.
- Everything else deletes successfully, so double-check the cascade list above (conversations, API keys, and memberships disappear with the account).
Role Change Returns 403
The user is still owner/admin of some team while the new role set lacks team:manage. Downgrade their team membership role first, then change the global roles.
API Endpoint Reference
| Endpoint | Method | Permission | Description |
|---|---|---|---|
/api/v1/admin/users | GET | admin:user:read | Paginated list (page/page_size/status/search/role) |
/api/v1/admin/users/stats | GET | admin:user:read | Total/active/inactive/pending counts |
/api/v1/admin/users | POST | admin:user:create | Create a user |
/api/v1/admin/users/{user_id} | GET | admin:user:read | User details |
/api/v1/admin/users/{user_id} | PUT | admin:user:update | Change email/active state/roles/password |
/api/v1/admin/users/{user_id} | DELETE | admin:user:delete | Delete a user |
/api/v1/admin/users/{user_id}/activate | POST | admin:user:update | Activate |
/api/v1/admin/users/{user_id}/deactivate | POST | admin:user:update | Deactivate |
/api/v1/admin/users/{user_id}/force-password-change | POST | admin:user:update | Force a change on next sign-in |
/api/v1/admin/users/{user_id}/reset-password-expiration | POST | admin:user:update | Reset the expiration timer |
/api/v1/admin/users/{user_id}/exempt-password-expiration | POST | admin:user:update | Grant/revoke exemption ({"exempt": bool}) |
/api/v1/admin/users/bulk-force-password-change | POST | admin:user:update | Bulk force password change |
/api/v1/admin/users/password-expiration-stats | GET | admin:user:read | Password expiration overview |
/api/v1/admin/users/expiring-passwords | GET | admin:user:read | Expired/expiring list |
/api/v1/admin/users/send-email | POST | admin:user:update | Send email in bulk |
/api/v1/admin/totp/stats | GET | admin:dashboard:access | TOTP adoption stats |
/api/v1/admin/totp/users/{user_id}/status | GET | superuser | Check a user's TOTP status |
/api/v1/admin/totp/users/{user_id}/disable | POST | superuser | Disable a user's TOTP |
Per-user permission endpoints such as /users/{user_id}/permissions and /users/{user_id}/permissions/check do not exist; permissions come only from global roles and team memberships.
See also:
- Sign-in & Accounts — password policies, TOTP, and account recovery
- Roles & Permissions — global roles, permission codes, and 403 troubleshooting
- Teams & Members — in-team member roles and ownership transfer
How is this guide?