mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: enforce role authority for user management (#616)
Centralize role-authority enforcement across user management, role management, configuration import, SCIM, GDPR, and MFA mutations. Add regression coverage for delegated custom roles and protect higher-privilege accounts from reset, deletion, or takeover.
This commit is contained in:
+15
-11
@@ -57,16 +57,16 @@ Keys are prefixed `si_` and stored as scrypt hashes - the raw key is shown once
|
||||
| `GET` | `/api/auth/session` | Auth | Validate current session |
|
||||
| `POST` | `/api/auth/change-password` | Auth | Change own password (invalidates all other sessions + API keys) |
|
||||
| `GET` | `/api/auth/users` | Admin | List all users |
|
||||
| `POST` | `/api/auth/register` | Admin | Create a new user |
|
||||
| `PUT` | `/api/auth/users/:id` | Admin | Update user role or team |
|
||||
| `POST` | `/api/auth/users/:id/reset-password` | Admin | Reset user's password |
|
||||
| `DELETE` | `/api/auth/users/:id` | Admin | Delete a user |
|
||||
| `POST` | `/api/auth/register` | Admin (`users:manage`; proposed-role authority) | Create a new user |
|
||||
| `PUT` | `/api/auth/users/:id` | Admin (`users:manage`; target authority) | Update user role or team |
|
||||
| `POST` | `/api/auth/users/:id/reset-password` | Admin (`users:manage`; target authority) | Reset user's password |
|
||||
| `DELETE` | `/api/auth/users/:id` | Admin (`users:manage`; target authority) | Delete a user |
|
||||
| `GET` | `/api/v1/config/auth` | Public | Check if authentication is enabled (`{ authEnabled: bool }`) |
|
||||
| `POST` | `/api/auth/mfa/enroll` | Auth | Start TOTP MFA enrollment. Requires the enterprise `mfa` feature |
|
||||
| `POST` | `/api/auth/mfa/verify` | Auth | Confirm MFA enrollment with a TOTP code |
|
||||
| `POST` | `/api/auth/mfa/complete` | Public | Complete a pending MFA login challenge |
|
||||
| `POST` | `/api/auth/mfa/disable` | Auth | Disable MFA for the current user |
|
||||
| `POST` | `/api/auth/users/:id/mfa/reset` | Admin (`users:manage`) | Reset MFA for a user |
|
||||
| `POST` | `/api/auth/users/:id/mfa/reset` | Admin (`users:manage`; target authority) | Reset MFA for a user |
|
||||
| `GET` | `/api/auth/oidc/login` | Public | Start OIDC login when OIDC is enabled |
|
||||
| `GET` | `/api/auth/oidc/callback` | Public | OIDC authorization callback |
|
||||
| `GET` | `/api/auth/saml/metadata` | Public | SAML SP metadata XML when SAML is enabled |
|
||||
@@ -553,6 +553,8 @@ Per-user preferences are separate from instance settings. Any authenticated user
|
||||
|
||||
Custom role management with granular permissions.
|
||||
|
||||
Role creation and mutation are constrained by authority containment: the proposed or current role cannot outrank the actor, exceed the actor's effective permissions, or broaden the actor's tool scope. API-key scopes participate in this check. Deleting a custom role also requires authority to assign the built-in `user` fallback used for its members.
|
||||
|
||||
| Method | Path | Access | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `GET` | `/api/v1/roles` | Admin (`audit:read`) | List all roles with user counts |
|
||||
@@ -633,17 +635,19 @@ Operational endpoints for observability, support, usage reporting, and backup st
|
||||
|
||||
These routes are license-gated by their related enterprise feature. They still require the listed SnapOtter permission.
|
||||
|
||||
**Full built-in admin** means the authenticated actor has the `admin` role and the complete effective admin permission set. An API-key scope that omits any admin permission does not qualify.
|
||||
|
||||
| Method | Path | Access | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `GET` | `/api/v1/enterprise/audit/export` | Admin (`audit:read`) | Export audit entries as JSON or CSV with filters |
|
||||
| `GET` | `/api/v1/enterprise/config/export` | Admin (`system:health`) | Export redacted instance config, custom roles, and teams |
|
||||
| `POST` | `/api/v1/enterprise/config/import` | Admin (`system:health`) | Import config, with optional dry run |
|
||||
| `POST` | `/api/v1/enterprise/config/import` | Full built-in admin | Import config, with optional dry run |
|
||||
| `GET` | `/api/v1/enterprise/ip-allowlist` | Admin (`security:manage`) | Read configured CIDR allowlist |
|
||||
| `PUT` | `/api/v1/enterprise/ip-allowlist` | Admin (`security:manage`) | Update CIDR allowlist with self-lockout prevention |
|
||||
| `GET` | `/api/v1/enterprise/legal-hold` | Admin (`compliance:manage`) | List user and team legal holds |
|
||||
| `PUT` | `/api/v1/enterprise/legal-hold` | Admin (`compliance:manage`) | Apply or release a legal hold on a user or team |
|
||||
| `POST` | `/api/v1/enterprise/scim/token` | Admin (`users:manage`) | Generate a SCIM bearer token, returned once |
|
||||
| `DELETE` | `/api/v1/enterprise/scim/token` | Admin (`users:manage`) | Revoke the current SCIM bearer token |
|
||||
| `POST` | `/api/v1/enterprise/scim/token` | Full built-in admin | Generate a SCIM bearer token, returned once |
|
||||
| `DELETE` | `/api/v1/enterprise/scim/token` | Full built-in admin | Revoke the current SCIM bearer token |
|
||||
| `GET` | `/api/v1/enterprise/siem/config` | Admin (`webhooks:manage`) | Read SIEM forwarding config |
|
||||
| `PUT` | `/api/v1/enterprise/siem/config` | Admin (`webhooks:manage`) | Update SIEM forwarding config |
|
||||
| `GET` | `/api/v1/enterprise/webhooks` | Admin (`webhooks:manage`) | List webhook destinations |
|
||||
@@ -653,15 +657,15 @@ These routes are license-gated by their related enterprise feature. They still r
|
||||
| `POST` | `/api/v1/enterprise/webhooks/:index/test` | Admin (`webhooks:manage`) | Send a test webhook payload |
|
||||
| `POST` | `/api/v1/enterprise/users/:id/export` | Admin (`compliance:manage`) | Start a GDPR user export job |
|
||||
| `GET` | `/api/v1/enterprise/users/:id/export/:jobId` | Admin (`compliance:manage`) | Read GDPR export status and download URL |
|
||||
| `DELETE` | `/api/v1/enterprise/users/:id/purge` | Admin (`compliance:manage`) | Permanently purge a user's data after confirmation |
|
||||
| `DELETE` | `/api/v1/enterprise/teams/:id/purge` | Admin (`compliance:manage`) | Permanently purge a team's data after confirmation |
|
||||
| `DELETE` | `/api/v1/enterprise/users/:id/purge` | Admin (`compliance:manage`; target authority) | Permanently purge a user's data after confirmation |
|
||||
| `DELETE` | `/api/v1/enterprise/teams/:id/purge` | Admin (`compliance:manage`; all-member authority) | Permanently purge a team's data after confirmation |
|
||||
| `GET` | `/api/v1/admin/version` | Admin (`system:health`) | Read app, build, Node, and schema version metadata |
|
||||
| `GET` | `/api/v1/admin/migrations/pending` | Admin (`system:health`) | Compare packaged migrations with applied migrations |
|
||||
| `GET` | `/api/v1/admin/upgrade-check` | Admin (`system:health`) | Run upgrade readiness checks |
|
||||
|
||||
### SCIM 2.0 {#scim-2-0}
|
||||
|
||||
SCIM discovery endpoints are public. User and group endpoints require the SCIM bearer token generated above.
|
||||
SCIM discovery endpoints are public. User and group endpoints require the SCIM bearer token generated above. Legacy unversioned tokens are invalid and must be reissued as `so_scim_v2_...` tokens by a full built-in admin.
|
||||
|
||||
| Method | Path | Access | Description |
|
||||
|--------|------|--------|-------------|
|
||||
|
||||
@@ -14,7 +14,7 @@ SCIM provisioning requires an **enterprise** license with the `scim` feature. It
|
||||
|
||||
- A running SnapOtter instance reachable at a public URL
|
||||
- An enterprise license key with the `scim` feature
|
||||
- Admin access to SnapOtter (the `users:manage` permission is required to generate or revoke a SCIM token)
|
||||
- A built-in SnapOtter `admin` account with its full effective permission set. A delegated custom role or an admin API key missing any admin permission cannot generate or revoke the global SCIM token.
|
||||
- Admin access to your identity provider's provisioning settings
|
||||
|
||||
## Quick start {#quick-start}
|
||||
@@ -31,7 +31,7 @@ The response contains the token. Save it immediately; it cannot be retrieved aga
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "a1b2c3d4e5f6...",
|
||||
"token": "so_scim_v2_a1b2c3d4e5f6...",
|
||||
"message": "Save this token - it cannot be retrieved again"
|
||||
}
|
||||
```
|
||||
@@ -46,15 +46,19 @@ SCIM endpoints use a dedicated Bearer token, separate from user sessions and API
|
||||
|
||||
### Generating a token {#generating-a-token}
|
||||
|
||||
`POST /api/v1/enterprise/scim/token` generates a new SCIM token. This endpoint requires a valid session with the `users:manage` permission.
|
||||
`POST /api/v1/enterprise/scim/token` generates a new SCIM token. Because the token can provision and mutate users across the instance, this endpoint requires the built-in `admin` role with the complete effective admin permission set. Holding `users:manage` in a custom role is not sufficient.
|
||||
|
||||
The token is returned in plaintext exactly once. SnapOtter stores only a scrypt hash. If you lose the token, revoke it and generate a new one.
|
||||
|
||||
Only one SCIM token is active at a time. Generating a new token replaces the previous one.
|
||||
|
||||
::: warning Token reissue after upgrade
|
||||
Legacy unversioned SCIM tokens are rejected. After upgrading to a release that issues `so_scim_v2_...` tokens, generate a new token and update your identity provider before resuming provisioning.
|
||||
:::
|
||||
|
||||
### Revoking a token {#revoking-a-token}
|
||||
|
||||
`DELETE /api/v1/enterprise/scim/token` revokes the current SCIM token. This endpoint also requires `users:manage`.
|
||||
`DELETE /api/v1/enterprise/scim/token` revokes the current SCIM token. It has the same full built-in admin requirement as token generation.
|
||||
|
||||
### Rate limiting {#rate-limiting}
|
||||
|
||||
@@ -276,7 +280,7 @@ The SCIM request did not include an `Authorization: Bearer <token>` header. Chec
|
||||
|
||||
### 401 "Invalid token" {#_401-invalid-token}
|
||||
|
||||
The token does not match the stored hash. This happens if the token was revoked and regenerated. Update the token in your IdP's provisioning settings.
|
||||
The token is malformed, uses the retired unversioned format, or does not match the stored hash. Generate a current `so_scim_v2_...` token and update the token in your IdP's provisioning settings.
|
||||
|
||||
### 401 "SCIM not configured" {#_401-scim-not-configured}
|
||||
|
||||
|
||||
@@ -79,12 +79,12 @@ All 17 permissions. Full control over the instance.
|
||||
| `pipelines:all` | View and manage all users' pipelines |
|
||||
| `settings:read` | View instance settings |
|
||||
| `settings:write` | Modify instance settings |
|
||||
| `users:manage` | Create, update, and delete user accounts |
|
||||
| `users:manage` | Create and manage user accounts within the actor's authority boundary |
|
||||
| `teams:manage` | Create, update, and delete teams |
|
||||
| `features:manage` | Install and manage AI feature bundles |
|
||||
| `system:health` | Access health and readiness endpoints |
|
||||
| `audit:read` | View the audit log and list roles |
|
||||
| `compliance:manage` | Manage GDPR lifecycle and compliance features |
|
||||
| `compliance:manage` | Manage GDPR lifecycle and compliance features; destructive user operations remain authority-bounded |
|
||||
| `webhooks:manage` | Configure outbound webhooks |
|
||||
| `security:manage` | Manage security settings (IP allowlist, SSO enforcement) |
|
||||
|
||||
@@ -107,15 +107,17 @@ curl -X POST http://localhost:1349/api/v1/roles \
|
||||
|
||||
Role names must be 2-30 characters, lowercase alphanumeric with hyphens and underscores.
|
||||
|
||||
### Admin-reserved permissions {#admin-reserved-permissions}
|
||||
### Delegated administration boundaries {#delegated-administration-boundaries}
|
||||
|
||||
Three permissions are reserved for built-in roles and cannot be assigned to custom roles:
|
||||
All 17 permissions can be delegated through custom roles, but an administrative permission does not make that role equivalent to the built-in `admin` role. User mutations authorized by `users:manage`, destructive operations authorized by `compliance:manage`, and custom-role management authorized by `security:manage` are bounded by the actor's current authority:
|
||||
|
||||
- `compliance:manage`
|
||||
- `webhooks:manage`
|
||||
- `security:manage`
|
||||
- Built-in roles follow `admin` > `editor` > `user`; custom roles are below built-in roles.
|
||||
- The target's permissions must be contained by the actor's **effective** permissions. A scoped API key therefore cannot exercise permissions omitted from its scope.
|
||||
- A target role's tool access must be contained by the actor's own tool access.
|
||||
- A disabled account is checked against its original role when that role is recorded as `disabled:<original-role>`.
|
||||
- Deleting a custom role also requires authority to assign the built-in `user` fallback; disabled members remain disabled as `disabled:user`.
|
||||
|
||||
The roles API rejects any request that includes these permissions. Only the built-in `admin` role has access to them.
|
||||
Global credentials and configuration are stricter: issuing or revoking the SCIM token and importing instance configuration require the built-in `admin` role with complete effective admin authority.
|
||||
|
||||
### Tool-level permissions {#tool-level-permissions}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user