User Management
EnterpriseCreate and manage user accounts, assign roles, and link LDAP/OIDC auth sources in the NetStacks Controller admin panel and REST API.
Overview
User management in the NetStacks Controller provides centralized control over who can access the platform and what they can do once authenticated. Every person who logs in — whether through a local password, LDAP, or OIDC — is represented as a user object with a unique username, optional email and display name, an authentication source, and zero or more assigned roles.
Users are the foundation of NetStacks role-based access control (RBAC). Each user is assigned roles, and roles carry granular permission keys such as devices.manage, config.deploy, or users.manage. A user's effective permissions are the union of the permission keys across all of their roles. See Roles & Permissions for the full permission catalog.
Key capabilities of user management include:
- Creating local users with Argon2id-hashed passwords through the admin panel or REST API
- Auto-provisioning LDAP and OIDC users on their first successful login
- Assigning one or more roles to control feature access through permission keys
- Disabling or enabling accounts (
is_active) without deleting user history - Resetting a local user's password, optionally syncing it to their personal device credential
- Automatic creation of a personal SSH password credential for each new user
- A full audit trail of every user-management action
All user endpoints are scoped to the caller's organization. Every handler verifies that the target user belongs to the same org_id as the requester, and the database queries filter by org_id. A user from another organization returns 404 Not Found.
User management, RBAC, and external authentication are features of the self-hosted NetStacks Controller (Enterprise tier). The desktop client does not have a user database.
How It Works
User records are stored in PostgreSQL. The fields returned by the API are id (UUID), org_id, username (unique within the org), email, display_name, auth_source (local, ldap, or oidc), is_active, created_at, and last_login. The password_hash column is never serialized into API responses.
Password hashing & policy
Local passwords are hashed with Argon2id (via the netstacks_password_hashing crate) before storage; they are never stored in plaintext or returned by the API. Every endpoint that accepts a new password — user create and admin password reset — first runs a password policy check that enforces a minimum length of 12 characters and rejects a deny-list of common passwords (for example password1234 or netstacks1234). A password that fails the policy returns 400 Bad Request.
Sessions & tokens
When a user logs in, the Controller issues a short-lived JWT access token and a refresh token (hashed with SHA-256 and stored in the database). The access-token lifetime is governed by the session.access_token_hours setting (default 24 hours) and the refresh-token lifetime by session.refresh_token_days (default 30 days). Session revocation is self-service: a signed-in user can list and revoke their own sessions via /api/auth/sessions, or revoke all of them by logging out. There is no seat-based limit that rejects new sessions.
Role assignment
Users link to roles through a many-to-many relationship, so a user can hold multiple roles at once. Role assignments can be managed one at a time (assign or remove a single role) or replaced wholesale with the PUT .../roles endpoint, which computes the diff and applies it in a single transaction so a partial failure rolls back.
Auto-created personal credential
When a local user is created, the Controller automatically generates a personal SSH password credential in the encrypted vault named <username> (default). It uses the new user's username and password, is owned by that user (a personal-vault entry), records sessions by default, and grants the user view access to their own credential. If credential creation fails, the user is still created — the failure is logged, not fatal.
Audit logging
Every mutation writes a non-blocking audit event: user.created, user.updated, user.deleted, user.role_assigned, user.role_removed, and user.roles_updated. Authentication events use auth.login.success and auth.login.failed. See Audit Logs.
Step-by-Step Guide
Creating a local user
- Open the Users page in the Controller admin panel.
- Click Add User.
- Enter a unique username (for example
jdoe). - Optionally enter a display name and email.
- Enter a password. The server enforces a 12-character minimum and a common-password deny-list, so choose a strong passphrase.
- Click Create. The user can now log in, and a personal SSH credential named
<username> (default)is auto-created in the vault.
The admin panel creates users with auth_source = local. LDAP and OIDC users are not created from this form — they are provisioned automatically on first login when auto-create is enabled (see below). Roles are assigned afterward on the user's edit page.
Assigning roles
- From the Users list, click a user row to open their edit page.
- In the Roles section, toggle the roles you want assigned. A user may hold several roles; effective permissions are the union of all of them.
- Click Save. Changed role sets are written atomically.
Editing a user
- Open the user's edit page from the Users list. Use the search box to filter by username, email, or display name.
- Update the email, display name, Active status, or roles.
- Click Save.
The update endpoint rejects changes to your own account with 400 Bad Request (“Cannot update your own user profile”) to prevent accidental self-lockout. Have another admin make the change.
Disabling and enabling a user
- Open the user's edit page and toggle Active off.
- Click Save. A disabled user can no longer authenticate or obtain new tokens.
- Toggle Active back on and save to restore access.
Disabling blocks new logins and token issuance. Access tokens already issued remain valid until they expire. To force an immediate disconnect, the user (or you, on your own account) can revoke active sessions via /api/auth/sessions.
Resetting a local user's password
- Open the user's edit page and type a new password (leave blank to keep the current one).
- Optionally tick Also update default device credential to sync the new password to the user's personal SSH password credentials in the vault.
- Click Save. The new password is validated against the policy, then Argon2id-hashed.
Deleting a user
- On the Users list, click the delete action on the user's row.
- Confirm in the dialog. The account is permanently removed.
- Audit log entries referencing the user are retained for compliance.
The delete endpoint rejects deletion of your own account with 400 Bad Request (“Cannot delete your own user account”). Have another administrator perform the deletion.
Code Examples
All user-management endpoints live under /api/admin/users and require a valid JWT bearer token whose roles include the users.manage permission. The wildcard permission * also grants access. Requests without it return 403 Forbidden; unauthenticated requests return 401.
Create a local user
curl -X POST https://netstacks.example.net/api/admin/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "jdoe",
"email": "[email protected]",
"display_name": "Jane Doe",
"password": "a-perfectly-fine-passphrase-2026"
}'Returns the created user object. Duplicate usernames return 400 with {"error":"Username already exists"}; a password under 12 characters or on the deny-list also returns 400.
List users with search and pagination
curl "https://netstacks.example.net/api/admin/users?limit=25&offset=0&search=doe" \
-H "Authorization: Bearer $TOKEN"limit is capped at 100 server-side; offset drives pagination; search matches username, email, or display name. Response:
{
"users": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"org_id": "00000000-0000-0000-0000-000000000001",
"username": "jdoe",
"email": "[email protected]",
"display_name": "Jane Doe",
"auth_source": "local",
"created_at": "2026-03-15T10:30:00Z",
"last_login": null,
"is_active": true
}
],
"total": 1
}Get a user with their roles
curl https://netstacks.example.net/api/admin/users/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer $TOKEN"{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"org_id": "00000000-0000-0000-0000-000000000001",
"username": "jdoe",
"email": "[email protected]",
"display_name": "Jane Doe",
"auth_source": "local",
"created_at": "2026-03-15T10:30:00Z",
"last_login": "2026-03-15T14:22:00Z",
"is_active": true,
"roles": [
{
"role_id": "11111111-1111-1111-1111-111111111111",
"role_name": "Operator",
"protected": false,
"assigned_at": "2026-03-15T10:31:00Z"
}
]
}Disable a user, reset a password, sync device credential
The same PUT /api/admin/users/{id} endpoint updates email, display name, password, and active status. Set update_device_credential: true to also push the new password to the user's personal SSH password credentials.
# Disable a user
curl -X PUT https://netstacks.example.net/api/admin/users/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"is_active": false}'
# Reset password and sync it to the personal device credential
curl -X PUT https://netstacks.example.net/api/admin/users/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"password": "another-strong-passphrase-2026",
"update_device_credential": true
}'Assign or replace roles
# Assign a single role (idempotent)
curl -X POST https://netstacks.example.net/api/admin/users/a1b2c3d4-e5f6-7890-abcd-ef1234567890/roles \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"role_id": "11111111-1111-1111-1111-111111111111"}'
# Remove a single role (idempotent)
curl -X DELETE https://netstacks.example.net/api/admin/users/a1b2c3d4-e5f6-7890-abcd-ef1234567890/roles/11111111-1111-1111-1111-111111111111 \
-H "Authorization: Bearer $TOKEN"
# Replace the full role set in one transaction
curl -X PUT https://netstacks.example.net/api/admin/users/a1b2c3d4-e5f6-7890-abcd-ef1234567890/roles \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"role_ids": [
"11111111-1111-1111-1111-111111111111",
"22222222-2222-2222-2222-222222222222"
]
}'Delete a user
curl -X DELETE https://netstacks.example.net/api/admin/users/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer $TOKEN"Returns {"success": true}. Deleting your own account returns 400.
Questions & Answers
- How do I create a new user in NetStacks?
- Open the Users page in the admin panel, click Add User, enter a username, optional email and display name, and a password of at least 12 characters, then click Create. You can also POST to
/api/admin/userswith a token that has theusers.managepermission. The admin form creates local users only. - What permission is required to manage users?
- Every
/api/admin/usersendpoint requires theusers.managepermission. The wildcard permission*also grants it. There is no separate read-only permission for users — both reads and writes requireusers.manage. See Roles & Permissions. - How are passwords stored?
- Local passwords are hashed with Argon2id and never stored or returned in plaintext. New passwords must pass a policy check (12-character minimum plus a common-password deny-list) before they are hashed.
- Can I import users from LDAP or OIDC automatically?
- Yes. With
auth.ldap.auto_create_users(orauth.oidc.auto_create_users) set totrue, a user who authenticates successfully for the first time is created automatically with the correspondingauth_source. Profile fields are populated from the directory or token claims. See Authentication. - Can a user have multiple roles?
- Yes. A user's effective permission set is the union of the permission keys across all assigned roles. Assign roles individually, or replace the entire set in one transaction with
PUT /api/admin/users/{id}/roles. - How do I revoke a user's active sessions?
- Session management is self-service per user via
/api/auth/sessions(list and revoke) and/api/auth/logout(revoke all). There is no admin endpoint that revokes another user's sessions. To cut off a user, disable their account (which blocks new logins and token refreshes); tokens already issued remain valid until they expire. - What happens when I disable a user?
- A disabled user (
is_active = false) cannot log in or obtain new tokens. Access tokens already issued stay valid until they expire (default 24 hours, persession.access_token_hours). The user's data, roles, and audit history are preserved, and re-enabling restores access. - What is the auto-created SSH credential?
- When you create a local user, NetStacks generates a personal SSH password credential named
<username> (default)in the encrypted vault, owned by that user, using their username and password. It gives the user immediate password-based SSH access without manual credential setup. See Personal Vaults.
Troubleshooting
User cannot log in
- Confirm the user's
is_activeistrue. Disabled accounts cannot authenticate. - Check the auth source. A user created as LDAP or OIDC will not work with a local password, and vice versa.
- For external users, verify the LDAP/OIDC provider is reachable. Connection failures to the directory or identity provider block authentication.
- Review the audit log for
auth.login.failedevents. See Auth Problems.
“This password is on our common-password deny list” or “too short”
- Passwords must be at least 12 characters. The admin create dialog may show a shorter hint, but the server enforces 12.
- Avoid deny-listed values such as
password1234ornetstacks1234; the check is case-insensitive. - The policy applies to user creation and admin password resets alike.
Password reset not working
- Password resets apply only to
localusers. LDAP and OIDC users manage passwords in their identity provider. - With Also update default device credential enabled, the password change still succeeds even if the credential sync fails — the sync error is logged, not fatal.
LDAP/OIDC user not appearing in the list
- External users are created on their first successful login, not on a sync schedule. They must authenticate at least once.
- Verify
auth.ldap.auto_create_users(orauth.oidc.auto_create_users) is enabled. See Authentication settings. - For LDAP, confirm the user falls within the configured
auth.ldap.base_dnandauth.ldap.user_filter.
Session expired sooner than expected
- Access-token lifetime is set by
session.access_token_hours(default 24 hours). If the client does not refresh before expiry, the session ends. - Refresh-token lifetime is
session.refresh_token_days(default 30 days). - Check whether the user revoked their own sessions via
/api/auth/sessionsor logged out everywhere.
“Cannot update/delete your own user” error
- The API blocks editing or deleting your own account to prevent self-lockout. Have another admin perform the change.
Related Features
- Roles & Permissions — the full permission catalog and the roles you assign to users.
- Authentication (LDAP/OIDC) — configure external identity providers and auto-provisioning.
- Audit Logs — review
user.*andauth.*events. - Personal Vaults — where each user's auto-created SSH credential lives.
- API Authentication — JWT access tokens, refresh flow, and session management.
- Auth Problems — diagnose login and token issues.