mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(docs): Two-Doors home, otter-orange brand, Pagefind search, and enterprise SSO/SCIM/roles guides
Docs site redesign: Two-Doors home, otter-orange brand, Pagefind search, enterprise SSO/SCIM/roles guides.
This commit is contained in:
@@ -6,6 +6,10 @@ description: Set up Single Sign-On with OpenID Connect. Step-by-step guides for
|
||||
|
||||
SnapOtter supports OpenID Connect (OIDC) for single sign-on. Users can log in with an external identity provider such as Keycloak, Authentik, or Google instead of (or alongside) local username/password authentication.
|
||||
|
||||
::: tip See also
|
||||
[SAML SSO](/guide/saml) | [SCIM Provisioning](/guide/scim) | [Users, Roles & Permissions](/guide/users-roles)
|
||||
:::
|
||||
|
||||
## Quick start
|
||||
|
||||
Add these environment variables to your `docker-compose.yml`:
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
---
|
||||
description: Set up SAML 2.0 Single Sign-On for SnapOtter. Step-by-step guides for Okta, Azure AD / Entra ID, Google Workspace, and other SAML identity providers.
|
||||
---
|
||||
|
||||
# SAML SSO
|
||||
|
||||
SnapOtter supports SAML 2.0 for single sign-on. Users can log in via an external identity provider (Okta, Azure AD / Entra ID, Google Workspace, or any standard SAML 2.0 IdP) instead of local username/password authentication.
|
||||
|
||||
::: tip Enterprise feature
|
||||
SAML SSO requires a **team** or **enterprise** license with the `saml_sso` feature. If `SAML_ENABLED=true` is set without a valid license, the SAML routes are silently skipped and a warning is logged.
|
||||
:::
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running SnapOtter instance reachable at a public URL
|
||||
- `EXTERNAL_URL` set to that public URL (e.g. `https://photos.example.com`)
|
||||
- A team or enterprise license key with the `saml_sso` feature
|
||||
- Admin access to your SAML identity provider
|
||||
|
||||
## Quick start
|
||||
|
||||
Add these environment variables to your `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
snapotter:
|
||||
image: snapotter/snapotter:latest
|
||||
environment:
|
||||
EXTERNAL_URL: "https://photos.example.com"
|
||||
SNAPOTTER_LICENSE_KEY: "your-license-key"
|
||||
SAML_ENABLED: "true"
|
||||
SAML_IDP_SSO_URL: "https://idp.example.com/sso/saml"
|
||||
SAML_IDP_CERTIFICATE: |
|
||||
MIICpDCCAYwCCQDU+pQ4pHgSpDANBgkqhkiG9w0BAQsFADAUMRIw
|
||||
...your IdP's signing certificate in PEM format...
|
||||
EAYHKoZIzj0CAQYFK4EEACIDYgAE
|
||||
```
|
||||
|
||||
Restart the container. A "Sign in with SAML" button (or the label set by `SAML_PROVIDER_NAME`) appears on the login page.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `SAML_ENABLED` | `false` | Enable SAML login. |
|
||||
| `SAML_IDP_SSO_URL` | | IdP's SSO endpoint URL. **Required** when SAML is enabled. |
|
||||
| `SAML_IDP_CERTIFICATE` | | IdP's X.509 signing certificate in PEM format (the certificate text itself, not a file path). **Required** when SAML is enabled. |
|
||||
| `EXTERNAL_URL` | | The public URL where SnapOtter is reachable. **Required** when SAML is enabled. |
|
||||
| `SAML_ENTITY_ID` | `${EXTERNAL_URL}/api/auth/saml/metadata` | SP Entity ID / Audience URI sent to the IdP. |
|
||||
| `SAML_CALLBACK_URL` | `${EXTERNAL_URL}/api/auth/saml/callback` | Assertion Consumer Service (ACS) URL. |
|
||||
| `SAML_AUTO_CREATE_USERS` | `true` | Automatically create a local user account on first SAML login. |
|
||||
| `SAML_AUTO_LINK_USERS` | `false` | Link a SAML identity to an existing local user if the email address matches. |
|
||||
| `SAML_DEFAULT_ROLE` | `user` | Role assigned to auto-created SAML users. One of `admin`, `editor`, or `user`. |
|
||||
| `SAML_PROVIDER_NAME` | | Display label for the SAML login button on the frontend (e.g. "Okta", "Azure AD"). If empty, the button says "SAML". |
|
||||
| `SAML_USERNAME_ATTRIBUTE` | | SAML assertion attribute used as the username. If empty, falls back to the email local-part, then NameID. |
|
||||
| `SAML_EMAIL_ATTRIBUTE` | `email` | SAML assertion attribute used as the user's email address. |
|
||||
|
||||
The server refuses to start if `SAML_ENABLED=true` and any of the three required variables (`SAML_IDP_SSO_URL`, `SAML_IDP_CERTIFICATE`, `EXTERNAL_URL`) are missing.
|
||||
|
||||
::: details Security notes
|
||||
Both `wantAuthnResponseSigned` and `wantAssertionsSigned` are hardcoded to `true`. SnapOtter rejects unsigned or improperly signed SAML responses. Assertions from a trusted IdP are treated as email-verified.
|
||||
|
||||
Only SP-initiated login is supported. SnapOtter does not support IdP-initiated (unsolicited) login or Single Logout (SLO). Logging out of SnapOtter does not log the user out of the IdP.
|
||||
:::
|
||||
|
||||
## SP metadata and URLs
|
||||
|
||||
Your IdP needs three values from SnapOtter:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **ACS URL** (Assertion Consumer Service) | `${EXTERNAL_URL}/api/auth/saml/callback` |
|
||||
| **Entity ID** / **Audience URI** | `${EXTERNAL_URL}/api/auth/saml/metadata` |
|
||||
| **SP Metadata** (XML) | `GET ${EXTERNAL_URL}/api/auth/saml/metadata` |
|
||||
|
||||
For example, if `EXTERNAL_URL` is `https://photos.example.com`:
|
||||
|
||||
- ACS URL: `https://photos.example.com/api/auth/saml/callback`
|
||||
- Entity ID: `https://photos.example.com/api/auth/saml/metadata`
|
||||
- Metadata endpoint: `https://photos.example.com/api/auth/saml/metadata` (returns XML)
|
||||
|
||||
Some IdPs can import the SP metadata URL directly, which auto-fills the ACS URL and Entity ID.
|
||||
|
||||
## Provider setup
|
||||
|
||||
### Okta
|
||||
|
||||
1. In the Okta admin console, go to **Applications > Create App Integration**.
|
||||
2. Select **SAML 2.0** and click **Next**.
|
||||
3. Set a name (e.g. "SnapOtter") and click **Next**.
|
||||
4. Configure the SAML settings:
|
||||
- **Single sign-on URL**: Your ACS URL (e.g. `https://photos.example.com/api/auth/saml/callback`)
|
||||
- **Audience URI (SP Entity ID)**: Your Entity ID (e.g. `https://photos.example.com/api/auth/saml/metadata`)
|
||||
- **Name ID format**: EmailAddress
|
||||
- **Application username**: Email
|
||||
5. Under **Attribute Statements**, add `email` mapped to `user.email`.
|
||||
6. Click **Next**, then **Finish**.
|
||||
7. Go to the **Sign On** tab, click **View SAML setup instructions**, and copy:
|
||||
- **Identity Provider Single Sign-On URL** into `SAML_IDP_SSO_URL`
|
||||
- **X.509 Certificate** into `SAML_IDP_CERTIFICATE`
|
||||
|
||||
### Azure AD / Entra ID
|
||||
|
||||
1. In the Azure portal, go to **Microsoft Entra ID > Enterprise applications > New application**.
|
||||
2. Click **Create your own application**, name it "SnapOtter", and select **Integrate any other application you don't find in the gallery**.
|
||||
3. Go to **Single sign-on > SAML** and click **Edit** on the **Basic SAML Configuration** section:
|
||||
- **Identifier (Entity ID)**: Your Entity ID (e.g. `https://photos.example.com/api/auth/saml/metadata`)
|
||||
- **Reply URL (ACS URL)**: Your ACS URL (e.g. `https://photos.example.com/api/auth/saml/callback`)
|
||||
4. Under **SAML Certificates**, download the **Certificate (Base64)**.
|
||||
5. Under **Set up SnapOtter**, copy the **Login URL**.
|
||||
6. Set `SAML_IDP_SSO_URL` to the Login URL and `SAML_IDP_CERTIFICATE` to the downloaded certificate contents.
|
||||
7. Assign users or groups to the application under **Users and groups**.
|
||||
|
||||
### Google Workspace
|
||||
|
||||
1. In the Google Admin console, go to **Apps > Web and mobile apps > Add app > Add custom SAML app**.
|
||||
2. Name the app "SnapOtter" and click **Continue**.
|
||||
3. On the **Google Identity Provider details** page, copy the **SSO URL** and download the **Certificate**. Click **Continue**.
|
||||
4. Configure the Service Provider details:
|
||||
- **ACS URL**: Your ACS URL (e.g. `https://photos.example.com/api/auth/saml/callback`)
|
||||
- **Entity ID**: Your Entity ID (e.g. `https://photos.example.com/api/auth/saml/metadata`)
|
||||
- **Name ID format**: EMAIL
|
||||
- **Name ID**: Basic Information > Primary email
|
||||
5. Click **Continue**, then **Finish**.
|
||||
6. Turn the app **ON** for your organizational units.
|
||||
7. Set `SAML_IDP_SSO_URL` to the SSO URL from step 3 and `SAML_IDP_CERTIFICATE` to the downloaded certificate contents.
|
||||
|
||||
### Generic SAML 2.0 IdP
|
||||
|
||||
For any SAML 2.0 compliant identity provider:
|
||||
|
||||
1. Create a new SAML application/service provider in your IdP.
|
||||
2. Set the **ACS URL** to `${EXTERNAL_URL}/api/auth/saml/callback`.
|
||||
3. Set the **Entity ID** / **Audience** to `${EXTERNAL_URL}/api/auth/saml/metadata`.
|
||||
4. Configure the IdP to send the user's email in an attribute named `email` (or set `SAML_EMAIL_ATTRIBUTE` to match your IdP's attribute name).
|
||||
5. Copy the **IdP SSO URL** and **signing certificate** into `SAML_IDP_SSO_URL` and `SAML_IDP_CERTIFICATE`.
|
||||
|
||||
## User provisioning
|
||||
|
||||
### Auto-create
|
||||
|
||||
When `SAML_AUTO_CREATE_USERS` is `true` (the default), a local user account is created the first time someone logs in via SAML. The role is set to `SAML_DEFAULT_ROLE`.
|
||||
|
||||
The username is derived in this order:
|
||||
|
||||
1. The value of the assertion attribute specified by `SAML_USERNAME_ATTRIBUTE` (if set and present)
|
||||
2. The local-part of the email address (everything before `@`)
|
||||
3. The SAML NameID
|
||||
|
||||
If a username collision occurs, a numeric suffix is appended (e.g. `jane` becomes `jane_2`).
|
||||
|
||||
### Auto-link
|
||||
|
||||
When `SAML_AUTO_LINK_USERS` is `true`, SnapOtter links a SAML identity to an existing local account if the email addresses match. This is useful when you have pre-created user accounts and want them to start using SSO without losing their data.
|
||||
|
||||
::: warning
|
||||
Only enable auto-link if you trust your SAML IdP to verify email addresses. An unverified email from a misconfigured IdP could allow someone to take over another user's account.
|
||||
:::
|
||||
|
||||
### Attribute mapping
|
||||
|
||||
| SnapOtter field | Source | Configuration |
|
||||
|---|---|---|
|
||||
| Email | Assertion attribute | `SAML_EMAIL_ATTRIBUTE` (default: `email`) |
|
||||
| Username | Assertion attribute, email, or NameID | `SAML_USERNAME_ATTRIBUTE` (see derivation order above) |
|
||||
| External ID | NameID | Always the SAML NameID, not configurable |
|
||||
|
||||
## SSO enforcement
|
||||
|
||||
If you want to require all users to log in via SAML (or OIDC) and block local password login, enable SSO enforcement:
|
||||
|
||||
1. Ensure the `sso_enforcement` enterprise feature is licensed (available on team and enterprise plans).
|
||||
2. In **Admin Settings > Security**, toggle **SSO Enforcement** on.
|
||||
3. Set a **break-glass username**: this is the one local account that can still log in with a password, for emergency access if the IdP is unreachable.
|
||||
|
||||
When SSO enforcement is active, any local login attempt (except for the break-glass user) returns a 403 error with the message "Local password login is disabled. Please use SSO."
|
||||
|
||||
::: tip
|
||||
Always configure a break-glass username before enabling SSO enforcement. Without it, you could be locked out of SnapOtter if your IdP goes down.
|
||||
:::
|
||||
|
||||
## Using SAML alongside OIDC
|
||||
|
||||
SAML and OIDC can be enabled simultaneously. When both are active, the login page shows separate buttons for each provider (labeled by `SAML_PROVIDER_NAME` and `OIDC_PROVIDER_NAME`). Users can log in with either method.
|
||||
|
||||
Both providers share the same auto-create, auto-link, and SSO enforcement settings independently: each has its own `*_AUTO_CREATE_USERS`, `*_AUTO_LINK_USERS`, and `*_DEFAULT_ROLE` variables.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Assertion validation failed
|
||||
|
||||
The SAML response signature or assertion signature could not be verified. Check:
|
||||
|
||||
- The certificate in `SAML_IDP_CERTIFICATE` matches the current signing certificate in your IdP (certificates rotate, so check for expiry)
|
||||
- The certificate is in PEM format (begins with `-----BEGIN CERTIFICATE-----`)
|
||||
- The certificate is the full text, not a file path
|
||||
- The ACS URL and Entity ID configured in your IdP match SnapOtter's values exactly (scheme, host, port, path)
|
||||
|
||||
### Missing attributes
|
||||
|
||||
If usernames or emails are empty after login, your IdP may not be sending the expected attributes. Check:
|
||||
|
||||
- Your IdP is configured to release an `email` attribute (or whatever `SAML_EMAIL_ATTRIBUTE` is set to)
|
||||
- If using `SAML_USERNAME_ATTRIBUTE`, verify that attribute is included in the assertion
|
||||
- Some IdPs require explicit attribute mapping configuration before they release claims
|
||||
|
||||
### Clock skew
|
||||
|
||||
SAML assertions include timestamp conditions (`NotBefore`, `NotOnOrAfter`). If your server clock and the IdP clock are out of sync, assertion validation fails. Run NTP on both machines to keep clocks aligned.
|
||||
|
||||
### "SAML is enabled via env but saml_sso enterprise feature is not licensed"
|
||||
|
||||
This warning appears in the server logs when `SAML_ENABLED=true` but the license does not include the `saml_sso` feature. Verify your license key and plan. The `saml_sso` feature is available on team and enterprise plans.
|
||||
|
||||
### Login redirects back with error
|
||||
|
||||
If clicking the SAML login button redirects back to the login page with an error, check the server logs for details. Common causes:
|
||||
|
||||
- The IdP SSO URL is unreachable from the server
|
||||
- The IdP rejected the authentication request (check the IdP's audit logs)
|
||||
- The IdP returned an unsigned response (SnapOtter requires both the response and assertion to be signed)
|
||||
@@ -0,0 +1,295 @@
|
||||
---
|
||||
description: Set up SCIM 2.0 provisioning to sync users and groups from your identity provider to SnapOtter. Covers Okta, Azure AD / Entra ID, and custom integrations.
|
||||
---
|
||||
|
||||
# SCIM Provisioning
|
||||
|
||||
SnapOtter implements SCIM 2.0 (System for Cross-domain Identity Management) for automated user and group provisioning. Your identity provider can create, update, deactivate, and reactivate user accounts and sync group memberships automatically.
|
||||
|
||||
::: tip Enterprise feature
|
||||
SCIM provisioning requires an **enterprise** license with the `scim` feature. It is not available on the team plan. Without the feature, all SCIM endpoints (except discovery) return 403.
|
||||
:::
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- 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 a SCIM token)
|
||||
- Admin access to your identity provider's provisioning settings
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Generate a SCIM bearer token:
|
||||
|
||||
```bash
|
||||
curl -X POST https://photos.example.com/api/v1/enterprise/scim/token \
|
||||
-H "Cookie: snapotter-session=YOUR_SESSION" \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
The response contains the token. Save it immediately; it cannot be retrieved again.
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "a1b2c3d4e5f6...",
|
||||
"message": "Save this token -- it cannot be retrieved again"
|
||||
}
|
||||
```
|
||||
|
||||
2. In your identity provider, configure SCIM provisioning with:
|
||||
- **Base URL**: `https://photos.example.com/api/v1/scim/v2`
|
||||
- **Authentication**: Bearer token (paste the token from step 1)
|
||||
|
||||
## Authentication
|
||||
|
||||
SCIM endpoints use a dedicated Bearer token, separate from user sessions and API keys.
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
### Revoking a token
|
||||
|
||||
`DELETE /api/v1/enterprise/scim/token` revokes the current SCIM token. This endpoint also requires `users:manage`.
|
||||
|
||||
### Rate limiting
|
||||
|
||||
SCIM endpoints are rate-limited to 1000 requests per minute per token. Exceeding this limit returns HTTP 429.
|
||||
|
||||
## Supported resources
|
||||
|
||||
| SCIM resource | SnapOtter concept | Create | Read | Update | Delete |
|
||||
|---|---|---|---|---|---|
|
||||
| User | User account | Yes | Yes | Yes | Soft delete |
|
||||
| Group | Team | Yes | Yes | Yes | Yes |
|
||||
|
||||
::: warning
|
||||
SCIM Groups map to SnapOtter **teams**, not roles. SCIM cannot set a user's role. All users created via SCIM are assigned the `user` role. To change a user's role, use the SnapOtter admin UI.
|
||||
:::
|
||||
|
||||
## User operations
|
||||
|
||||
### Create user
|
||||
|
||||
`POST /api/v1/scim/v2/Users`
|
||||
|
||||
Creates a new user account with `authProvider` set to `scim` and the `user` role. The user is assigned to the Default team. If `active` is `false`, the role is set to `disabled` instead.
|
||||
|
||||
Required attributes: `userName`. Optional: `externalId`, `emails`, `active` (default `true`).
|
||||
|
||||
### List and filter users
|
||||
|
||||
`GET /api/v1/scim/v2/Users`
|
||||
|
||||
Returns a paginated list of users. Supports `startIndex` and `count` query parameters (maximum 200 results per page).
|
||||
|
||||
Filtering supports `eq` (equals) only, on these attributes:
|
||||
|
||||
- `userName eq "jane"`
|
||||
- `externalId eq "ext-12345"`
|
||||
|
||||
Other filter operators and attributes return HTTP 400.
|
||||
|
||||
### Get user
|
||||
|
||||
`GET /api/v1/scim/v2/Users/:id`
|
||||
|
||||
Returns a single user by their SnapOtter user ID.
|
||||
|
||||
### Replace user
|
||||
|
||||
`PUT /api/v1/scim/v2/Users/:id`
|
||||
|
||||
Replaces the user's attributes. Supports `userName`, `externalId`, `emails`, and `active`. Username changes are checked for conflicts (409 if the new username is taken by another user).
|
||||
|
||||
### Patch user
|
||||
|
||||
`PATCH /api/v1/scim/v2/Users/:id`
|
||||
|
||||
Partial update using SCIM PatchOp. Supported operations:
|
||||
|
||||
| Operation | Paths |
|
||||
|---|---|
|
||||
| `replace` | `active`, `userName`, `externalId`, `emails`, `emails[type eq "work"].value`, `name.formatted`, `displayName` |
|
||||
| `add` | Same as `replace` |
|
||||
| `remove` | `externalId`, `emails` |
|
||||
|
||||
The `name.formatted` and `displayName` paths are accepted for compatibility but have no persistent effect (SnapOtter does not store a separate display name).
|
||||
|
||||
Valueless `replace` operations (where the value is an object without a `path`) are also supported, with keys `userName`, `externalId`, `emails`, and `active`.
|
||||
|
||||
### Deactivate user (soft delete)
|
||||
|
||||
`DELETE /api/v1/scim/v2/Users/:id`
|
||||
|
||||
SnapOtter does not hard-delete users via SCIM. Instead, DELETE performs a soft deactivation:
|
||||
|
||||
1. The user's role is changed from its current value (e.g. `editor`) to `disabled:editor`, preserving the original role.
|
||||
2. The user's password is cleared.
|
||||
3. All active sessions are revoked.
|
||||
4. All API keys are revoked.
|
||||
|
||||
The user can no longer log in or use any API keys. Their data (files, history) is retained.
|
||||
|
||||
### Reactivate user
|
||||
|
||||
To reactivate a previously deactivated user, send a `PUT` or `PATCH` request with `active: true`. SnapOtter restores the original role from before deactivation (e.g. `disabled:editor` becomes `editor` again). If the original role cannot be determined, it falls back to `user`.
|
||||
|
||||
::: details Example: deactivate and reactivate via PATCH
|
||||
```json
|
||||
// Deactivate
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{ "op": "replace", "path": "active", "value": false }
|
||||
]
|
||||
}
|
||||
|
||||
// Reactivate
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||
"Operations": [
|
||||
{ "op": "replace", "path": "active", "value": true }
|
||||
]
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
## Group operations
|
||||
|
||||
SCIM Groups map to SnapOtter teams. Creating a group creates a team. Group membership controls which team a user belongs to.
|
||||
|
||||
### Create group
|
||||
|
||||
`POST /api/v1/scim/v2/Groups`
|
||||
|
||||
Required: `displayName`. Optional: `members` (array of `{ value: userId }`).
|
||||
|
||||
### List and filter groups
|
||||
|
||||
`GET /api/v1/scim/v2/Groups`
|
||||
|
||||
Filtering supports `displayName eq "..."` only. Paginated with `startIndex` and `count` (maximum 200 results per page).
|
||||
|
||||
### Get group
|
||||
|
||||
`GET /api/v1/scim/v2/Groups/:id`
|
||||
|
||||
### Replace group
|
||||
|
||||
`PUT /api/v1/scim/v2/Groups/:id`
|
||||
|
||||
Replaces the group name and full membership list. Existing members not in the new list are moved to the Default team.
|
||||
|
||||
### Patch group
|
||||
|
||||
`PATCH /api/v1/scim/v2/Groups/:id`
|
||||
|
||||
Supports these operations:
|
||||
|
||||
| Operation | Path | Effect |
|
||||
|---|---|---|
|
||||
| `add` | `members` | Adds users to the team |
|
||||
| `remove` | `members[value eq "userId"]` | Moves the user to the Default team |
|
||||
| `replace` | `displayName` | Renames the team |
|
||||
| `replace` | `members` | Replaces all members (removed members move to the Default team) |
|
||||
|
||||
### Delete group
|
||||
|
||||
`DELETE /api/v1/scim/v2/Groups/:id`
|
||||
|
||||
Deletes the team. All members of the deleted team are moved to the Default team. Users are not deactivated or deleted.
|
||||
|
||||
## IdP setup
|
||||
|
||||
### Okta
|
||||
|
||||
1. In the Okta admin console, open your SnapOtter application (or create one).
|
||||
2. Go to the **Provisioning** tab and click **Configure API Integration**.
|
||||
3. Check **Enable API Integration** and enter:
|
||||
- **Base URL**: `https://photos.example.com/api/v1/scim/v2`
|
||||
- **API Token**: The SCIM bearer token generated above
|
||||
4. Click **Test API Credentials**, then **Save**.
|
||||
5. Under **Provisioning > To App**, enable:
|
||||
- **Create Users**
|
||||
- **Update User Attributes**
|
||||
- **Deactivate Users**
|
||||
6. Under **Push Groups**, configure which Okta groups to sync as SnapOtter teams.
|
||||
|
||||
### Azure AD / Entra ID
|
||||
|
||||
1. In the Azure portal, go to your SnapOtter enterprise application.
|
||||
2. Go to **Provisioning** and set **Provisioning Mode** to **Automatic**.
|
||||
3. Under **Admin Credentials**, enter:
|
||||
- **Tenant URL**: `https://photos.example.com/api/v1/scim/v2`
|
||||
- **Secret Token**: The SCIM bearer token generated above
|
||||
4. Click **Test Connection**, then **Save**.
|
||||
5. Under **Mappings**, configure the user and group attribute mappings. The defaults typically work, but verify that `userName` maps to `userPrincipalName` or `mail` as desired.
|
||||
6. Set **Provisioning Status** to **On** and save.
|
||||
|
||||
Azure provisions users and groups on a fixed sync cycle (typically every 40 minutes).
|
||||
|
||||
## Discovery endpoints
|
||||
|
||||
These three endpoints are available without authentication and describe the SCIM server's capabilities:
|
||||
|
||||
| Endpoint | Description |
|
||||
|---|---|
|
||||
| `GET /api/v1/scim/v2/ServiceProviderConfig` | Server capabilities and supported features |
|
||||
| `GET /api/v1/scim/v2/Schemas` | User and Group schema definitions |
|
||||
| `GET /api/v1/scim/v2/ResourceTypes` | Available resource types (User, Group) |
|
||||
|
||||
The `ServiceProviderConfig` advertises these capabilities:
|
||||
|
||||
| Feature | Supported |
|
||||
|---|---|
|
||||
| Patch | Yes |
|
||||
| Bulk | No |
|
||||
| Filter | Yes (max 200 results, `eq` operator only) |
|
||||
| Change password | No |
|
||||
| Sort | No |
|
||||
| ETag | No |
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Filtering**: Only the `eq` operator is supported. Complex filters, `and`/`or` operators, `co` (contains), and `sw` (starts with) are not implemented.
|
||||
- **Bulk operations**: Not supported.
|
||||
- **Sort and ETag**: Not supported.
|
||||
- **Roles**: SCIM cannot assign SnapOtter roles. All provisioned users get the `user` role.
|
||||
- **MAX_USERS**: The `MAX_USERS` environment variable limit is not enforced on SCIM user creation. If you need to cap user counts, manage assignments in your IdP.
|
||||
- **One token**: Only one SCIM token can be active at a time. If multiple IdPs need SCIM access, they must share the token.
|
||||
- **Groups are teams**: SCIM Groups correspond to teams, not roles or permission groups.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 403 "SCIM provisioning requires an enterprise license with the scim feature"
|
||||
|
||||
Your license does not include the `scim` feature, or no license is configured. SCIM requires an enterprise plan license. Verify `SNAPOTTER_LICENSE_KEY` is set and the license includes the `scim` feature.
|
||||
|
||||
### 401 "Bearer token required"
|
||||
|
||||
The SCIM request did not include an `Authorization: Bearer <token>` header. Check your IdP's provisioning configuration.
|
||||
|
||||
### 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.
|
||||
|
||||
### 401 "SCIM not configured"
|
||||
|
||||
No SCIM token has been generated yet. Use the `POST /api/v1/enterprise/scim/token` endpoint to create one.
|
||||
|
||||
### 409 "User already exists" / "userName already taken"
|
||||
|
||||
A user with the same username already exists. This can happen when an IdP retries a failed create. Check for duplicate usernames in the SnapOtter admin panel.
|
||||
|
||||
### 429 "SCIM rate limit exceeded"
|
||||
|
||||
The IdP is sending more than 1000 requests per minute. This typically happens during a large initial sync. Most IdPs automatically retry after the rate limit window resets. If the problem persists, check your IdP's provisioning sync interval.
|
||||
|
||||
### Users deprovisioned but not removed from the UI
|
||||
|
||||
SCIM DELETE is a soft deactivation. Deactivated users still appear in the admin user list with a disabled status. This is by design so their data is preserved. Their role shows as `disabled:<original-role>`.
|
||||
@@ -0,0 +1,261 @@
|
||||
---
|
||||
description: Manage users, built-in and custom roles, permissions, API keys, teams, sessions, and the audit log in SnapOtter.
|
||||
---
|
||||
|
||||
# Users, Roles & Permissions
|
||||
|
||||
SnapOtter ships three built-in roles, 17 granular permissions, and support for custom roles with optional per-tool access control. This page covers the full authorization model, API key scoping, team management, and audit logging.
|
||||
|
||||
::: tip Related pages
|
||||
[OIDC / SSO](/guide/oidc) | [SAML SSO](/guide/saml) | [SCIM Provisioning](/guide/scim) | [Security & Hardening](/guide/security)
|
||||
:::
|
||||
|
||||
## Users
|
||||
|
||||
### Creating users
|
||||
|
||||
Admins can create users through the admin panel or the `POST /api/auth/users` endpoint. Each user has a username, role, team assignment, and an optional email address.
|
||||
|
||||
### Default admin
|
||||
|
||||
On first startup SnapOtter creates a default admin account. The credentials come from environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `DEFAULT_USERNAME` | `admin` | Username for the initial admin account |
|
||||
| `DEFAULT_PASSWORD` | `admin` | Password for the initial admin account |
|
||||
|
||||
The default admin is required to change their password on first login.
|
||||
|
||||
### Authentication providers
|
||||
|
||||
Users can authenticate through several methods:
|
||||
|
||||
- **Local** -- username and password stored in the SnapOtter database
|
||||
- **OIDC** -- any OpenID Connect provider (see [OIDC / SSO](/guide/oidc))
|
||||
- **SAML** -- SAML 2.0 identity providers (see [SAML SSO](/guide/saml))
|
||||
- **SCIM** -- automated provisioning from an identity provider (see [SCIM Provisioning](/guide/scim))
|
||||
|
||||
### Disabling authentication
|
||||
|
||||
Set `AUTH_ENABLED=false` to disable authentication entirely. In this mode a synthetic anonymous user with the `admin` role is used for all requests. No login is required.
|
||||
|
||||
::: warning
|
||||
Disabling authentication grants full admin access to anyone who can reach the instance. Only use this in trusted environments.
|
||||
:::
|
||||
|
||||
## Built-in roles
|
||||
|
||||
SnapOtter includes three built-in roles. They cannot be modified or deleted.
|
||||
|
||||
### Admin
|
||||
|
||||
All 17 permissions. Full control over the instance.
|
||||
|
||||
`tools:use` `files:own` `files:all` `apikeys:own` `apikeys:all` `pipelines:own` `pipelines:all` `settings:read` `settings:write` `users:manage` `teams:manage` `features:manage` `system:health` `audit:read` `compliance:manage` `webhooks:manage` `security:manage`
|
||||
|
||||
### Editor
|
||||
|
||||
7 permissions. Can use all tools and manage all files and pipelines, but cannot access admin functions.
|
||||
|
||||
`tools:use` `files:own` `files:all` `apikeys:own` `pipelines:own` `pipelines:all` `settings:read`
|
||||
|
||||
### User
|
||||
|
||||
5 permissions. Can use tools and manage their own resources.
|
||||
|
||||
`tools:use` `files:own` `apikeys:own` `pipelines:own` `settings:read`
|
||||
|
||||
## Permissions reference
|
||||
|
||||
| Permission | Description |
|
||||
|---|---|
|
||||
| `tools:use` | Use any processing tool |
|
||||
| `files:own` | View and manage own files |
|
||||
| `files:all` | View and manage all users' files |
|
||||
| `apikeys:own` | Create and manage own API keys |
|
||||
| `apikeys:all` | View all users' API keys |
|
||||
| `pipelines:own` | Create and manage own pipelines |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `webhooks:manage` | Configure outbound webhooks |
|
||||
| `security:manage` | Manage security settings (IP allowlist, SSO enforcement) |
|
||||
|
||||
## Custom roles
|
||||
|
||||
Admins with the `users:manage` permission can create custom roles through the admin panel or the roles API.
|
||||
|
||||
### Creating a custom role
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:13490/api/v1/roles \
|
||||
-H "Authorization: Bearer si_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "reviewer",
|
||||
"description": "Can use tools and view all files",
|
||||
"permissions": ["tools:use", "files:own", "files:all", "settings:read"]
|
||||
}'
|
||||
```
|
||||
|
||||
Role names must be 2-30 characters, lowercase alphanumeric with hyphens and underscores.
|
||||
|
||||
### Admin-reserved permissions
|
||||
|
||||
Three permissions are reserved for built-in roles and cannot be assigned to custom roles:
|
||||
|
||||
- `compliance:manage`
|
||||
- `webhooks:manage`
|
||||
- `security:manage`
|
||||
|
||||
The roles API rejects any request that includes these permissions. Only the built-in `admin` role has access to them.
|
||||
|
||||
### Tool-level permissions
|
||||
|
||||
Custom roles can optionally restrict which tools users may access. Two modes are available:
|
||||
|
||||
| Mode | Behavior | License requirement |
|
||||
|---|---|---|
|
||||
| `category` | Restrict by modality (image, video, audio, document, file) | None (free) |
|
||||
| `tool` | Restrict by individual tool ID | Requires the `per_tool_permissions` enterprise feature |
|
||||
|
||||
When `tool` mode is set but the enterprise feature is not available, SnapOtter degrades gracefully and allows access to all tools.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "image-only",
|
||||
"permissions": ["tools:use", "files:own"],
|
||||
"toolPermissions": {
|
||||
"mode": "category",
|
||||
"allowed": ["image"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deleting a custom role
|
||||
|
||||
When a custom role is deleted, all users assigned to it are automatically reassigned to the `user` role.
|
||||
|
||||
## Teams
|
||||
|
||||
Teams group users for storage and retention management. A `Default` team is created on first startup.
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `name` | string | Unique team name (1-50 characters) |
|
||||
| `storageQuota` | number | Per-team storage limit in bytes (works without enterprise) |
|
||||
| `retentionHours` | number | Auto-delete outputs after this many hours (requires `team_retention_overrides`, enterprise) |
|
||||
| `legalHold` | boolean | Prevent automatic deletion of team members' files (requires `legal_hold`, enterprise) |
|
||||
|
||||
::: info
|
||||
The `Default` team cannot be deleted. Teams that still have members cannot be deleted. Reassign members first.
|
||||
:::
|
||||
|
||||
## API keys
|
||||
|
||||
Users can generate API keys for programmatic access. Each key uses the `si_` prefix and is shown only once at creation time.
|
||||
|
||||
### Scoped permissions
|
||||
|
||||
API keys can optionally carry a `permissions` array. When set, the effective permissions for a request are the **intersection** of the user's role permissions and the key's scoped permissions. This means an API key can never escalate beyond the user's own permissions.
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:13490/api/v1/api-keys \
|
||||
-H "Authorization: Bearer si_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "CI pipeline key",
|
||||
"permissions": ["tools:use", "files:own"],
|
||||
"expiresAt": "2027-01-01T00:00:00Z"
|
||||
}'
|
||||
```
|
||||
|
||||
### Expiration
|
||||
|
||||
Keys accept an optional `expiresAt` timestamp. Expired keys are rejected at authentication time.
|
||||
|
||||
## Audit log
|
||||
|
||||
SnapOtter records security-relevant events in a structured audit log stored in the `audit_log` database table.
|
||||
|
||||
### Viewing the audit log
|
||||
|
||||
```
|
||||
GET /api/v1/audit-log?page=1&limit=50&action=LOGIN_FAILED&from=2026-01-01T00:00:00Z&to=2026-12-31T23:59:59Z
|
||||
```
|
||||
|
||||
Requires the `audit:read` permission. Supports pagination (`page`, `limit`) and filters (`action`, `ip`, `from`, `to`).
|
||||
|
||||
### Tool operation auditing
|
||||
|
||||
::: warning
|
||||
`TOOL_EXECUTED` events are **not** logged by default. They are opt-in through either of two paths:
|
||||
|
||||
1. Set the `auditToolOperations` admin setting to `true`.
|
||||
2. Hold an active license with the `audit_export` feature (available on both team and enterprise plans).
|
||||
|
||||
Without one of these, individual tool executions are not recorded in the audit log.
|
||||
:::
|
||||
|
||||
### Exporting
|
||||
|
||||
```
|
||||
GET /api/v1/enterprise/audit/export?format=csv&from=2026-01-01T00:00:00Z
|
||||
```
|
||||
|
||||
Requires the `audit:read` permission and the `audit_export` enterprise feature (available on both team and enterprise plans). Supports CSV and JSON formats, filtered by `action`, `actorId`, `targetType`, `targetId`, `from`, and `to`.
|
||||
|
||||
### Tamper-resistant signing
|
||||
|
||||
When enabled, each audit log entry is signed with an HMAC derived from `DATA_ENCRYPTION_KEY`. This requires:
|
||||
|
||||
1. Setting `DATA_ENCRYPTION_KEY` in your environment.
|
||||
2. Enabling the `tamperResistantAudit` admin setting.
|
||||
3. An enterprise license with the `tamper_resistant_audit` feature.
|
||||
|
||||
### Retention
|
||||
|
||||
Set `AUDIT_RETENTION_DAYS` to automatically purge old entries. The default is `0`, which means entries are kept indefinitely.
|
||||
|
||||
### Event reference
|
||||
|
||||
| Event | Category |
|
||||
|---|---|
|
||||
| `LOGIN_SUCCESS`, `LOGIN_FAILED` | Authentication |
|
||||
| `OIDC_LOGIN_SUCCESS`, `OIDC_LOGIN_FAILED` | Authentication |
|
||||
| `SAML_LOGIN_SUCCESS`, `SAML_LOGIN_FAILED` | Authentication |
|
||||
| `LOGOUT` | Authentication |
|
||||
| `USER_CREATED`, `USER_UPDATED`, `USER_DELETED` | User management |
|
||||
| `PASSWORD_CHANGED`, `PASSWORD_RESET` | User management |
|
||||
| `MFA_ENROLLED`, `MFA_DISABLED`, `MFA_VERIFIED`, `MFA_VERIFY_FAILED` | MFA |
|
||||
| `MFA_CHALLENGE_ISSUED`, `MFA_RECOVERY_USED`, `MFA_RESET` | MFA |
|
||||
| `ROLE_CREATED`, `ROLE_UPDATED`, `ROLE_DELETED` | Roles |
|
||||
| `API_KEY_CREATED`, `API_KEY_DELETED` | API keys |
|
||||
| `SETTINGS_UPDATED`, `IP_ALLOWLIST_UPDATED` | Settings |
|
||||
| `FILE_UPLOADED`, `FILE_DELETED` | Files |
|
||||
| `TOOL_EXECUTED` | Tools (opt-in) |
|
||||
| `SCIM_USER_PROVISIONED`, `SCIM_USER_UPDATED`, `SCIM_USER_DEPROVISIONED` | SCIM |
|
||||
| `SCIM_GROUP_SYNCED` | SCIM |
|
||||
| `LEGAL_HOLD_APPLIED`, `LEGAL_HOLD_RELEASED` | Compliance |
|
||||
| `GDPR_EXPORT_INITIATED`, `GDPR_USER_PURGED`, `GDPR_TEAM_PURGED` | Compliance |
|
||||
| `CONFIG_EXPORTED`, `CONFIG_IMPORTED` | Configuration |
|
||||
|
||||
## Session management
|
||||
|
||||
Sessions are cookie-based, controlled by `SESSION_DURATION_HOURS` (default: 168 hours / 7 days).
|
||||
|
||||
### Role changes invalidate sessions
|
||||
|
||||
When an admin changes a user's role, all of that user's active sessions are deleted. The user must log in again to pick up their new permissions.
|
||||
|
||||
### Safety guards
|
||||
|
||||
- **Last-admin protection**: the last remaining admin cannot be demoted to a lower role. The API returns an error if you try.
|
||||
- **Self-delete prevention**: admins cannot delete their own account through the API.
|
||||
Reference in New Issue
Block a user