Users
Manage user accounts, authentication, and role assignments in Mantis.
Overview
Section titled “Overview”User Model
Section titled “User Model”| Field | Type | Description |
|---|---|---|
id | UUID | Unique identifier |
email | String | Email address (unique within the tenant) |
username | String | Display username (unique within the tenant) |
password_hash | String | Argon2id hash (local auth only) |
display_name | String | Optional display name |
status | Enum | active, inactive, locked, pending_verification, pending_approval |
email_verified | Boolean | Email verification status |
tenant_id | UUID | Optional tenant scope |
last_login_at | DateTime | Last successful login |
created_at | DateTime | Account creation time |
updated_at | DateTime | Last update time (nullable) |
User Status
Section titled “User Status”| Status | Description |
|---|---|
active | Account is active and can log in |
inactive | Account is inactive (disabled by admin) |
locked | Account is locked (too many failed logins) |
pending_verification | Invited; waiting on the user to activate |
pending_approval | Provisioned by an identity provider, awaiting approval |
Only active can sign in.
API Operations
Section titled “API Operations”List Users
Section titled “List Users”curl -X GET \ -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/admin/users?page=1&limit=20"Response:
{ "data": [ { "id": "019b937d-4862-8e07-ac3a-1b8589d1b807", "email": "alice@example.com", "username": "alice", "display_name": "Alice Smith", "status": "active", "email_verified": true, "role_count": 2, "tenant_id": null, "last_login_at": "2024-01-15T10:30:00Z", "created_at": "2024-01-01T08:00:00Z" } ], "meta": { "total": 50, "page": 1, "limit": 20, "total_pages": 3, "has_next": true, "has_prev": false }}Get User Details
Section titled “Get User Details”curl -X GET \ -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/admin/users/$USER_ID"Response includes roles, permissions, and external identities:
{ "id": "019b937d-4862-8e07-ac3a-1b8589d1b807", "email": "alice@example.com", "username": "alice", "display_name": "Alice Smith", "status": "active", "email_verified": true, "roles": [ { "id": "019b937d-4862-8e07-ac3a-1b8589d1b807", "name": "operator", "description": "Can manage deployments and targets", "is_system": true } ], "permissions": [ "deployments:create", "deployments:read", "targets:create", "targets:read" ], "tenant_id": null, "tenant_name": null, "external_identities": [ { "provider_id": "019b937d-4862-8def-4567-001122334455", "provider_name": "Okta", "external_username": "alice.smith", "external_email": "alice.smith@company.com", "last_login_at": "2024-01-15T10:30:00Z" } ], "last_login_at": "2024-01-15T10:30:00Z", "created_at": "2024-01-01T08:00:00Z", "updated_at": "2024-01-10T14:00:00Z"}Create User
Section titled “Create User”Creating a user sends an invitation. The account is created pending_verification with
no usable password — the invitee sets their own through the emailed link, so an
administrator never handles it. The request carries no password:
curl -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "bob@example.com", "username": "bob", "display_name": "Bob Jones", "tenant_id": null, "role_ids": ["019b937d-4862-84ae-9c2a-6ac230cc4c7e"] }' \ "https://api.mantis.local/api/v1/admin/users"The invitation needs a working platform mailer — see
Platform email. Links expire after 7 days;
POST /api/v1/admin/users/{id}/resend-invite issues a fresh one and invalidates the
previous link.
Update User
Section titled “Update User”curl -X PUT \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "display_name": "Alice S.", "status": "active", "email_verified": true }' \ "https://api.mantis.local/api/v1/admin/users/$USER_ID"Delete User
Section titled “Delete User”curl -X DELETE \ -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/admin/users/$USER_ID"Reset Password
Section titled “Reset Password”curl -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "new_password": "NewSecurePassword456!" }' \ "https://api.mantis.local/api/v1/admin/users/$USER_ID/password"An admin password reset is also the credential-containment action: it revokes the target user’s OAuth2 tokens, API keys and active sessions, not just the password. That is deliberate — an attacker holding a session or API key would otherwise keep access through a “lockout” reset — but it does mean resetting a password to help a user will break their running integrations.
Recovering an account locked out by 2FA
Section titled “Recovering an account locked out by 2FA”An account with a TOTP enrolment answers POST /auth/login with 200 and
requires_2fa: true — no access token. If the authenticator is gone, no password
reset recovers it: the second factor is checked after the password, so the account
stays unreachable and any non-interactive caller (a deploy, a script) fails on the
missing token rather than on a clear authentication error.
Clear the factor from the host, which needs database access rather than a login:
sudo -u mantis mantisctl user disable-two-factor --email admin@mantis.localIt is idempotent — an account with no enrolment reports zero. The user can enrol a new authenticator afterwards.
Deploys never run this for you: your second factor is yours to keep. A deploy only authenticates when it has just recreated the database, and a freshly seeded admin has no enrolment — so an operator’s 2FA never blocks a converge.
Role Management
Section titled “Role Management”List User Roles
Section titled “List User Roles”curl -X GET \ -H "Authorization: Bearer $TOKEN" \ "https://api.mantis.local/api/v1/admin/users/$USER_ID/roles"Assign Roles
Section titled “Assign Roles”curl -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "role_ids": ["019b937d-4862-8e07-ac3a-1b8589d1b807", "019b937d-4862-84ae-9c2a-6ac230cc4c7e"] }' \ "https://api.mantis.local/api/v1/admin/users/$USER_ID/roles"Remove Roles
Section titled “Remove Roles”curl -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "role_ids": ["019b937d-4862-8e07-ac3a-1b8589d1b807"] }' \ "https://api.mantis.local/api/v1/admin/users/$USER_ID/roles/remove"User Provisioning
Section titled “User Provisioning”Local Users
Section titled “Local Users”An administrator invites the user; the user sets their own password:
SSO/OIDC Users
Section titled “SSO/OIDC Users”Users can be provisioned automatically on first SSO login, provided the identity provider is scoped to a tenant and has auto-provisioning enabled (auto_create_users). System-wide (global) identity providers do not auto-provision new users (SEC-OIDC-02); link the provider to a tenant to enable this:
Tenant Assignment
Section titled “Tenant Assignment”Assigning Users to Tenants
Section titled “Assigning Users to Tenants”curl -X PUT \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tenant_id": "019b937d-4862-8387-9a86-df16bd76ca99" }' \ "https://api.mantis.local/api/v1/admin/users/$USER_ID"Removing Tenant Assignment
Section titled “Removing Tenant Assignment”Set tenant_id to null to make user global:
curl -X PUT \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tenant_id": null }' \ "https://api.mantis.local/api/v1/admin/users/$USER_ID"Password Policy
Section titled “Password Policy”Requirements
Section titled “Requirements”Password requirements are hardcoded and cannot be changed via configuration:
- Minimum 12 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
- At least one special character (
!@#$%^&*()_+-=[]{}|;:,.<>?)
Password Storage
Section titled “Password Storage”- Passwords hashed with Argon2id (library default parameters)
- Original passwords never stored
- Hash comparison for authentication
External Identities
Section titled “External Identities”Linked Accounts
Section titled “Linked Accounts”Users can have multiple external identities:
| Field | Description |
|---|---|
provider_id | Identity provider reference |
provider_name | Name of the identity provider |
external_username | Username from external provider |
external_email | Email from external provider |
last_login_at | Last SSO login via this provider |
Identity Linking
Section titled “Identity Linking”When a user logs in via SSO:
- System checks for an existing
external_identitymatch. - If found, updates last login and uses the linked user.
- If not found, creation/linking is gated, not automatic:
- Auto-provisioning a new user requires the IdP to be tenant-scoped (a system-wide / NULL-tenant provider cannot provision).
- Linking to an existing user by email additionally requires the provider’s
trust_unverified_email = trueand the ID token asserting a verified email; otherwise it fails closed with403 Email is already associated with another account.
Unlink External Identity
Section titled “Unlink External Identity”There is no unlink UI — this is a direct-database operation only:
DELETE FROM external_identitiesWHERE user_id = $USER_IDAND provider_id = $PROVIDER_ID;Audit Trail
Section titled “Audit Trail”All user operations are logged:
| Event | Category | Severity |
|---|---|---|
user.created | user_management | Info |
user.updated | user_management | Info |
user.deleted | user_management | Info |
user.password_reset | user_management | Security |
user.roles_assigned | authorization | Security |
user.roles_removed | authorization | Security |
Example audit entry:
{ "event_type": "user.created", "event_category": "user_management", "actor_type": "user", "actor_id": "019b937d-4862-8e07-ac3a-1b8589d1b801", "actor_name": "admin", "resource_type": "user", "resource_id": "019b937d-4862-8e07-ac3a-1b8589d1b809", "outcome": "success", "outcome_reason": null, "occurred_at": "2026-01-15T14:23:01Z"}Best Practices
Section titled “Best Practices”User Onboarding
Section titled “User Onboarding”- Use SSO when possible - Reduces password management overhead
- Assign minimum required roles - Follow least privilege principle
- Set tenant scope appropriately - Limit access to necessary resources
- Verify email addresses - Ensure contact information is accurate
User Offboarding
Section titled “User Offboarding”- Set status to inactive - blocks new logins and refresh, but does not revoke anything already issued
- Reset the user’s password - this is what actually revokes their OAuth2 tokens, API keys and sessions
- Remove role assignments - clear all permissions
- Review owned resources - transfer ownership if needed
- Delete account - after transition period
Regular Audits
Section titled “Regular Audits”- Review active users quarterly
- Verify role assignments match current responsibilities
- Check for unused accounts (no recent login)
- Audit external identity links
Troubleshooting
Section titled “Troubleshooting”User Cannot Login
Section titled “User Cannot Login”-
Check account status:
SELECT status FROM users WHERE email = 'user@example.com'; -
Verify password (local auth):
- Reset password via admin API
- Check for caps lock / keyboard issues
-
Check external identity (SSO):
- Verify provider is configured
- Check role mappings exist
- Review IdP logs for errors
User Missing Permissions
Section titled “User Missing Permissions”-
List user’s current roles:
Terminal window curl -s -H "Authorization: Bearer $TOKEN" \https://api.mantis.local/api/v1/admin/users/$USER_ID/roles -
Check role permissions:
Terminal window curl -s -H "Authorization: Bearer $TOKEN" \https://api.mantis.local/api/v1/admin/roles/$ROLE_ID/permissions -
Assign missing role:
Terminal window curl -X POST \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"role_ids": ["019b937d-4862-84ae-9c2a-6ac230cc4c7e"]}' \https://api.mantis.local/api/v1/admin/users/$USER_ID/roles
Next Steps
Section titled “Next Steps”- Roles - Role configuration
- Permissions - Permission reference
- Identity Providers - SSO setup
