1430 lines
32 KiB
Markdown
1430 lines
32 KiB
Markdown
# SSO Manager API Documentation
|
|
|
|
> Looking for a plainer explanation of what API tokens are and when you'd
|
|
> want one, instead of a full endpoint reference? See
|
|
> [API Tokens](/docs/api-tokens) (in-app) or
|
|
> [concepts-api-tokens.md](docs/concepts-api-tokens.md) (repo).
|
|
|
|
## Overview
|
|
|
|
API documentation for the SSO Manager Node application. Provides endpoints for authentication, user management, group management, token management, notifications, and OAuth 2.0 / OpenID Connect.
|
|
|
|
**Base URL:** `https://your-domain.com`
|
|
|
|
**Content Type:** `application/json` (all request and response bodies)
|
|
|
|
---
|
|
|
|
## Authentication
|
|
|
|
### Getting a Token
|
|
|
|
Login via `POST /api/auth/login` to receive an auth token.
|
|
|
|
### Using the Token
|
|
|
|
Include the token in every protected request:
|
|
|
|
```
|
|
auth-token: <token>
|
|
```
|
|
|
|
### Protected Routes
|
|
|
|
- `/api/user/*` — user management
|
|
- `/api/group/*` — group management
|
|
- `/api/token/*` — token management
|
|
- `/api/notification/*` — notifications
|
|
- `/api/oauth/client/*` — OAuth client management
|
|
|
|
### Permission Groups
|
|
|
|
| Group | Grants |
|
|
|-------|--------|
|
|
| `app_sso_admin` | Full user/group/notification management |
|
|
| `app_sso_oauth_admin` | Register and manage OAuth clients |
|
|
| `app_sso_invite` | Invitation management |
|
|
| Group owner | Manage membership of that specific group |
|
|
|
|
Self-service: users can always read and modify their own account without admin membership.
|
|
|
|
### Rate Limits
|
|
|
|
| Endpoint | Limit |
|
|
|----------|-------|
|
|
| `POST /api/auth/login` | 10 requests / 15 min |
|
|
| `POST /api/auth/resetpassword` | 5 requests / hour |
|
|
| `POST /api/auth/otp/request` | 5 requests / 15 min |
|
|
| `POST /api/auth/otp/verify` | 10 requests / 15 min |
|
|
| `POST /api/auth/invite/*` | 20 requests / hour |
|
|
|
|
Limits are per IP. The server must be behind a trusted proxy for these to apply correctly.
|
|
|
|
---
|
|
|
|
## Authentication Endpoints
|
|
|
|
Base path: `/api/auth`
|
|
|
|
### Login
|
|
|
|
**`POST /api/auth/login`** — No auth required
|
|
|
|
**Request:**
|
|
```json
|
|
{ "uid": "username", "password": "user_password" }
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "login": true, "token": "auth_token_string", "message": "username logged in!" }
|
|
```
|
|
|
|
---
|
|
|
|
### Logout
|
|
|
|
**`ALL /api/auth/logout`** — No auth required
|
|
|
|
**Response:**
|
|
```json
|
|
{ "message": "Bye" }
|
|
```
|
|
|
|
---
|
|
|
|
### Username Suggestions
|
|
|
|
Returns available username suggestions based on name and optional date of birth.
|
|
|
|
**`GET /api/auth/username-suggestions`** — No auth required
|
|
|
|
**Query Parameters:**
|
|
- `givenName` — First name
|
|
- `sn` — Last name
|
|
- `dob` (optional) — Date of birth (used to generate year-variant suggestions)
|
|
|
|
**Response:**
|
|
```json
|
|
{ "suggestions": ["jsmith", "jsmith1990", "johnsmith"] }
|
|
```
|
|
|
|
---
|
|
|
|
### Request Password Reset
|
|
|
|
**`POST /api/auth/resetpassword`** — No auth required
|
|
|
|
**Request:**
|
|
```json
|
|
{ "mail": "user@example.com" }
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "message": "If the email address is in our system, you will receive a message." }
|
|
```
|
|
|
|
The same response is returned whether or not the email exists. Reset tokens expire after 24 hours.
|
|
|
|
---
|
|
|
|
### Complete Password Reset
|
|
|
|
**`POST /api/auth/resetpassword/:token`** — No auth required
|
|
|
|
**URL Parameters:** `token` — from the reset email
|
|
|
|
**Request:**
|
|
```json
|
|
{ "password": "new_password", "confirm": "new_password" }
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "message": "Password has been changed." }
|
|
```
|
|
|
|
The token is invalidated after a successful reset.
|
|
|
|
---
|
|
|
|
### Request OTP Code
|
|
|
|
Send a one-time login code via email or SMS.
|
|
|
|
**`POST /api/auth/otp/request`** — No auth required
|
|
|
|
**Request:**
|
|
```json
|
|
{ "login": "username_or_email", "method": "email" }
|
|
```
|
|
|
|
`method` must be `email` or `sms`. For `sms`, the account must have a mobile number on file.
|
|
|
|
**Response:**
|
|
```json
|
|
{ "message": "Code sent", "method": "email", "expires_at": 1234567890000 }
|
|
```
|
|
|
|
---
|
|
|
|
### Verify OTP Code
|
|
|
|
Verify a one-time code and receive an auth token. Also marks the corresponding contact method (email or phone) as verified on the account.
|
|
|
|
**`POST /api/auth/otp/verify`** — No auth required
|
|
|
|
**Request:**
|
|
```json
|
|
{ "login": "username_or_email", "code": "123456" }
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "login": true, "token": "auth_token_string" }
|
|
```
|
|
|
|
Returns `401` if the code is invalid or expired.
|
|
|
|
---
|
|
|
|
### Accept Invite (with Email Verification)
|
|
|
|
Create an account from an invite token after verifying email.
|
|
|
|
**`POST /api/auth/invite/:token/:mailToken`** — No auth required
|
|
|
|
**URL Parameters:** `token` — invite token, `mailToken` — email verification token
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"uid": "username",
|
|
"password": "user_password",
|
|
"givenName": "First Name",
|
|
"sn": "Last Name",
|
|
"mail": "user@example.com"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "user": "username", "token": "auth_token_string" }
|
|
```
|
|
|
|
---
|
|
|
|
### Send Invite Verification Email
|
|
|
|
Send an email verification link for an invite token.
|
|
|
|
**`POST /api/auth/invite/:token`** — No auth required
|
|
|
|
**URL Parameters:** `token` — invite token
|
|
|
|
**Request:**
|
|
```json
|
|
{ "mail": "user@example.com" }
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "message": "sent" }
|
|
```
|
|
|
|
---
|
|
|
|
### Impersonate User (Admin)
|
|
|
|
Creates a temporary password for a target user and returns it. Allows admins to log in as another user for support purposes.
|
|
|
|
**`POST /api/auth/impersonate/:uid`** — Auth required, `app_sso_admin`
|
|
|
|
**URL Parameters:** `uid` — target username
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"uid": "target_username",
|
|
"temp_password": "random_temp_password",
|
|
"expires_at": 1234567890000
|
|
}
|
|
```
|
|
|
|
The temp password is short-lived. Any previous impersonation session for the same target is revoked first.
|
|
|
|
---
|
|
|
|
### End Impersonation (Admin)
|
|
|
|
Revoke all active impersonation sessions for a user.
|
|
|
|
**`DELETE /api/auth/impersonate/:uid`** — Auth required, `app_sso_admin`
|
|
|
|
**URL Parameters:** `uid` — target username
|
|
|
|
**Response:**
|
|
```json
|
|
{ "message": "Impersonation ended for username", "revoked": 1 }
|
|
```
|
|
|
|
---
|
|
|
|
## User Management Endpoints
|
|
|
|
Base path: `/api/user`
|
|
|
|
All endpoints require authentication.
|
|
|
|
### List All Users
|
|
|
|
**`GET /api/user/`** — `app_sso_admin` required
|
|
|
|
**Query Parameters:**
|
|
- `detail` (optional) — return full user objects instead of a minimal list
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"results": [
|
|
{ "uid": "username", "mail": "user@example.com", "givenName": "First", "sn": "Last" }
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Create User
|
|
|
|
**`POST /api/user/`** — `app_sso_admin` required
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"uid": "username",
|
|
"password": "user_password",
|
|
"givenName": "First Name",
|
|
"sn": "Last Name",
|
|
"mail": "user@example.com"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": { "uid": "username", "dn": "uid=username,ou=people,dc=..." } }
|
|
```
|
|
|
|
Admin-created accounts have `password_must_change` set automatically.
|
|
|
|
---
|
|
|
|
### Get Current User
|
|
|
|
**`GET /api/user/me`** — Any authenticated user
|
|
|
|
Returns the full user object for the authenticated user, including onboarding state.
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"uid": "username",
|
|
"mail": "user@example.com",
|
|
"givenName": "First",
|
|
"sn": "Last",
|
|
"dn": "uid=username,ou=people,dc=...",
|
|
"onboardingRequired": "yes",
|
|
"onboardingNeeds": ["tos", "dob", "password"],
|
|
"memberOf": ["cn=group_name,ou=groups,dc=..."]
|
|
}
|
|
```
|
|
|
|
`onboardingRequired` is `"yes"` when `onboardingNeeds` is non-empty. Needs can be `"tos"`, `"dob"`, or `"password"`.
|
|
|
|
---
|
|
|
|
### Accept Terms of Service
|
|
|
|
Mark TOS as accepted for the authenticated user.
|
|
|
|
**`POST /api/user/accept-tos`** — Any authenticated user
|
|
|
|
**Response:**
|
|
```json
|
|
{ "success": true }
|
|
```
|
|
|
|
---
|
|
|
|
### Get Admin Stats
|
|
|
|
Returns user and group counts, recent signups, and inactive users.
|
|
|
|
**`GET /api/user/stats`** — `app_sso_admin` required
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"totalUsers": 42,
|
|
"activeUsers": 38,
|
|
"inactiveUsers": 4,
|
|
"totalGroups": 10,
|
|
"recentSignups": [
|
|
{ "uid": "newuser", "givenName": "New", "sn": "User", "mail": "new@example.com", "createTimestamp": "20240101000000Z" }
|
|
],
|
|
"inactiveList": [
|
|
{ "uid": "lockeduser", "givenName": "Locked", "sn": "User", "mail": "locked@example.com" }
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Export Users CSV
|
|
|
|
Download all users as a CSV file.
|
|
|
|
**`GET /api/user/export`** — `app_sso_admin` required
|
|
|
|
**Response:** `Content-Type: text/csv`, attachment download
|
|
|
|
Columns: `uid`, `givenName`, `sn`, `mail`, `mobile`, `uidNumber`, `isActive`, `createTimestamp`
|
|
|
|
---
|
|
|
|
### Get User Verification Status
|
|
|
|
**`GET /api/user/:uid/verification`** — `app_sso_admin` required
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"uid": "username",
|
|
"emailVerified": true,
|
|
"emailVerifiedAt": 1234567890000,
|
|
"phoneVerified": false,
|
|
"phoneVerifiedAt": null,
|
|
"tosAccepted": true,
|
|
"tosAcceptedAt": 1234567890000
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Get User
|
|
|
|
**`GET /api/user/:uid`** — Any authenticated user
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": { "uid": "username", "mail": "user@example.com", "givenName": "First", "sn": "Last" } }
|
|
```
|
|
|
|
---
|
|
|
|
### Update User
|
|
|
|
**`PUT /api/user/:uid`** — Own account, or `app_sso_admin` for others
|
|
|
|
**Request:** Any subset of editable fields:
|
|
```json
|
|
{
|
|
"givenName": "New First Name",
|
|
"sn": "New Last Name",
|
|
"mail": "newemail@example.com",
|
|
"dateOfBirth": "1990-01-15",
|
|
"mobile": "+15551234567",
|
|
"sshPublicKey": "ssh-rsa AAAA..."
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": { <updated user object> }, "message": "Updated username user" }
|
|
```
|
|
|
|
---
|
|
|
|
### Set User Active/Inactive
|
|
|
|
Lock or unlock a user account.
|
|
|
|
**`PUT /api/user/:uid/active`** — `app_sso_admin` required
|
|
|
|
**Request:**
|
|
```json
|
|
{ "active": true }
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "uid": "username", "active": true, "message": "User username activated" }
|
|
```
|
|
|
|
---
|
|
|
|
### Delete User
|
|
|
|
**`DELETE /api/user/:uid`** — Own account, or `app_sso_admin` for others
|
|
|
|
**Response:**
|
|
```json
|
|
{ "uid": "username", "results": true }
|
|
```
|
|
|
|
---
|
|
|
|
### Change Own Password
|
|
|
|
**`PUT /api/user/password`** — Any authenticated user
|
|
|
|
**Request:**
|
|
```json
|
|
{ "password": "new_password", "confirm": "new_password" }
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": true }
|
|
```
|
|
|
|
Clears `password_must_change` on the account.
|
|
|
|
---
|
|
|
|
### Change User Password (Admin)
|
|
|
|
**`PUT /api/user/:uid/password`** — Own account, or `app_sso_admin` for others
|
|
|
|
**Request:**
|
|
```json
|
|
{ "password": "new_password", "confirm": "new_password" }
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": true, "message": "User username password changed." }
|
|
```
|
|
|
|
When an admin changes another user's password, `password_must_change` is set on that account.
|
|
|
|
---
|
|
|
|
### Generate Invite Token
|
|
|
|
Create an invite token, optionally emailing it to the invitee and pre-assigning
|
|
LDAP groups the new account should be added to on signup.
|
|
|
|
**`POST /api/user/invite`** — `app_sso_admin` or `app_sso_invite` required
|
|
|
|
**Request:**
|
|
```json
|
|
{ "mail": "invitee@example.com", "groups": ["group_name"] }
|
|
```
|
|
|
|
`mail` and `groups` are both optional. If `mail` is provided, a verification
|
|
email is sent to that address.
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"token": "invite_token_string",
|
|
"link": "https://your-domain.com/login/invite/invite_token_string",
|
|
"mail_sent": true
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### List Invite Tokens
|
|
|
|
**`GET /api/user/invite`** — `app_sso_admin` or `app_sso_invite` required
|
|
|
|
Admins (`app_sso_admin`) see all invite tokens; non-admin `app_sso_invite`
|
|
members see only invites they created.
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"results": [
|
|
{
|
|
"token": "invite_token_string",
|
|
"created_by": "username",
|
|
"created_on": 1234567890000,
|
|
"is_valid": true,
|
|
"mail": "invitee@example.com",
|
|
"groups": "[\"group_name\"]"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Update Invite Token
|
|
|
|
Update the groups an invite will assign, or change/clear the invitee's email
|
|
(re-sends verification if a new email is set).
|
|
|
|
**`PUT /api/user/invite/:token`** — `app_sso_admin`, or the `app_sso_invite`
|
|
member who created the invite
|
|
|
|
**URL Parameters:** `token` — invite token
|
|
|
|
**Request:** Any subset of:
|
|
```json
|
|
{ "groups": ["group_name"], "mail": "newinvitee@example.com" }
|
|
```
|
|
|
|
Set `mail` to `null`/empty to clear it. Fails with `400` if the token is no
|
|
longer valid.
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": { "token": "invite_token_string", "is_valid": true, "...": "..." } }
|
|
```
|
|
|
|
---
|
|
|
|
### Revoke Invite Token
|
|
|
|
**`DELETE /api/user/invite/:token`** — `app_sso_admin`, or the `app_sso_invite`
|
|
member who created the invite
|
|
|
|
**URL Parameters:** `token` — invite token
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": true }
|
|
```
|
|
|
|
---
|
|
|
|
### Add SSH Key
|
|
|
|
**`POST /api/user/key`** — Any authenticated user
|
|
|
|
**Request:**
|
|
```json
|
|
{ "key": "ssh-rsa AAAAB3NzaC1yc2E... user@host" }
|
|
```
|
|
|
|
**Response:**
|
|
- `200` — `{ "message": true }`
|
|
- `400` — `{ "message": "error description" }`
|
|
|
|
---
|
|
|
|
## Group Management Endpoints
|
|
|
|
Base path: `/api/group`
|
|
|
|
All endpoints require authentication.
|
|
|
|
### List Groups
|
|
|
|
**`GET /api/group/`** — Any authenticated user
|
|
|
|
**Query Parameters:**
|
|
- `detail` (optional) — return full group objects
|
|
- `member` (optional) — filter to groups containing this UID as a member
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": [{ "cn": "group_name", "description": "Group description" }] }
|
|
```
|
|
|
|
---
|
|
|
|
### Create Group
|
|
|
|
**`POST /api/group/`** — `app_sso_admin` required
|
|
|
|
**Request:**
|
|
```json
|
|
{ "name": "group_name", "description": "Group description" }
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": { "cn": "group_name" }, "message": "group_name was added!" }
|
|
```
|
|
|
|
The authenticated user is automatically set as the group owner.
|
|
|
|
---
|
|
|
|
### Get Group
|
|
|
|
**`GET /api/group/:name`** — Any authenticated user
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"results": {
|
|
"cn": "group_name",
|
|
"description": "Group description",
|
|
"member": ["uid=user1,ou=people,dc=...", "uid=user2,ou=people,dc=..."],
|
|
"owner": ["uid=owner,ou=people,dc=..."]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Add Group Owner
|
|
|
|
**`PUT /api/group/owner/:group/:uid`** — `app_sso_admin` or group owner
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": true, "message": "Added owner uid to group group." }
|
|
```
|
|
|
|
---
|
|
|
|
### Remove Group Owner
|
|
|
|
**`DELETE /api/group/owner/:group/:uid`** — `app_sso_admin` or group owner
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": true, "message": "Removed Owner uid from group group." }
|
|
```
|
|
|
|
---
|
|
|
|
### Add User to Group
|
|
|
|
**`PUT /api/group/:group/:uid`** — `app_sso_admin` or group owner
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": true, "message": "Added user uid to group group." }
|
|
```
|
|
|
|
Returns `409` if the user is already a member — common in practice, since
|
|
`groupOfNames` requires at least one member and so seeds whoever created the
|
|
group into it.
|
|
|
|
---
|
|
|
|
### Remove User from Group
|
|
|
|
**`DELETE /api/group/:group/:uid`** — `app_sso_admin` or group owner
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": true, "message": "Removed user uid from group group." }
|
|
```
|
|
|
|
---
|
|
|
|
### Nest a Group Inside Another
|
|
|
|
**`PUT /api/group/:group/nested/:child`** — `app_sso_admin` or group owner
|
|
|
|
Makes `:child` a member of `:group`, so everyone in `:child` is a member of
|
|
`:group` at any depth.
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": { "cn": "group", "member": ["..."] }, "message": "Nested child inside group." }
|
|
```
|
|
|
|
**Errors:**
|
|
|
|
| Status | When |
|
|
|--------|------|
|
|
| `400` | `:group` and `:child` are the same group |
|
|
| `409` | already nested, or the nesting would create a loop (`:child` already contains `:group`, directly or transitively) |
|
|
|
|
---
|
|
|
|
### Un-nest a Group
|
|
|
|
**`DELETE /api/group/:group/nested/:child`** — `app_sso_admin` or group owner
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": { "cn": "group", "member": ["..."] }, "message": "Removed child from group." }
|
|
```
|
|
|
|
**Errors:**
|
|
|
|
| Status | When |
|
|
|--------|------|
|
|
| `409` | `:child` is the only member — `groupOfNames` requires at least one |
|
|
|
|
---
|
|
|
|
### Effective Membership
|
|
|
|
**`GET /api/group/:group/effective`** — Any authenticated user
|
|
|
|
Who a group actually grants. `direct` is users listed on the group itself
|
|
(never groups); `nestedGroups` is what is nested into it; `effective` is every
|
|
user reachable through the whole chain.
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"results": {
|
|
"cn": "app_gitea_access",
|
|
"direct": ["cn=alice,ou=people,dc=example,dc=com"],
|
|
"nestedGroups": [{ "cn": "developers", "dn": "cn=developers,ou=groups,dc=example,dc=com" }],
|
|
"effective": ["cn=alice,ou=people,dc=example,dc=com", "cn=bob,ou=people,dc=example,dc=com"]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Delete Group
|
|
|
|
**`DELETE /api/group/:group`** — `app_sso_admin` or group owner
|
|
|
|
**Response:**
|
|
```json
|
|
{ "removed": true, "results": { "cn": "group_name" }, "message": "Group group_name Deleted" }
|
|
```
|
|
|
|
---
|
|
|
|
## Token Management Endpoints
|
|
|
|
Base path: `/api/token`
|
|
|
|
All endpoints require authentication.
|
|
|
|
### List Token Types
|
|
|
|
**`GET /api/token/`** — Any authenticated user
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": ["InviteToken", "PasswordResetToken"] }
|
|
```
|
|
|
|
---
|
|
|
|
### List Tokens by Type
|
|
|
|
**`GET /api/token/:name`** — Any authenticated user
|
|
|
|
**Query Parameters:** `detail` (optional) — include full token objects
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"results": [
|
|
{ "token": "token_string", "created_by": "username", "created_on": 1234567890000, "is_valid": true }
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Get Specific Token
|
|
|
|
**`GET /api/token/:name/:token`** — Any authenticated user
|
|
|
|
**Response:**
|
|
```json
|
|
{ "results": { "token": "token_string", "created_by": "username", "created_on": 1234567890000, "is_valid": true } }
|
|
```
|
|
|
|
---
|
|
|
|
## Notification Endpoints
|
|
|
|
Base path: `/api/notification`
|
|
|
|
All endpoints require authentication and `app_sso_admin` membership.
|
|
|
|
Notifications send email blasts to filtered groups of users and record history.
|
|
|
|
### Send Notification
|
|
|
|
**`POST /api/notification`** — `app_sso_admin` required
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"subject": "Maintenance window tonight",
|
|
"message": "<p>We will be performing maintenance starting at midnight.</p>",
|
|
"filter_type": "group",
|
|
"filter_value": "host_hec-bot_admin, app_sso_admin",
|
|
"active_only": true
|
|
}
|
|
```
|
|
|
|
**`filter_type` values:**
|
|
|
|
| Value | Description |
|
|
|-------|-------------|
|
|
| `all_active` | All active (unlocked) users |
|
|
| `all` | All users including inactive |
|
|
| `group` | Members of one or more LDAP groups |
|
|
| `users` | Specific users by UID |
|
|
|
|
**`filter_value`:**
|
|
- For `group`: comma-separated group names (e.g. `"host_hec-bot_admin, app_sso_admin"`)
|
|
- For `users`: JSON array of UIDs (e.g. `'["wmantly","jsmith"]'`)
|
|
- Unused for `all_active` and `all`
|
|
|
|
**`active_only`** (boolean, default `false`): when `true` with `group` or `all` filter types, only active (unlocked) users receive the notification. Has no effect on `all_active` (which is always active-only).
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"results": {
|
|
"notification_id": "uuid",
|
|
"created_by": "wmantly",
|
|
"created_on": 1234567890000,
|
|
"subject": "Maintenance window tonight",
|
|
"message": "<p>...</p>",
|
|
"filter_type": "group",
|
|
"filter_value": "host_hec-bot_admin",
|
|
"active_only": true,
|
|
"status": "sent",
|
|
"sent_count": 5,
|
|
"failed_count": 0,
|
|
"sent_at": 1234567890123
|
|
}
|
|
}
|
|
```
|
|
|
|
**External API usage example:**
|
|
|
|
```bash
|
|
# Login once, store token
|
|
TOKEN=$(curl -s -X POST https://sso.example.com/api/auth/login \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"uid":"monitor","password":"..."}' | jq -r .token)
|
|
|
|
# Send notification to a group
|
|
curl -X POST https://sso.example.com/api/notification \
|
|
-H "auth-token: $TOKEN" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{
|
|
"subject": "hec-bot maintenance",
|
|
"message": "<p>The hec-bot VM will restart at 3am.</p>",
|
|
"filter_type": "group",
|
|
"filter_value": "host_hec-bot_admin",
|
|
"active_only": true
|
|
}'
|
|
```
|
|
|
|
---
|
|
|
|
### List Notification History
|
|
|
|
**`GET /api/notification`** — `app_sso_admin` required
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"results": [
|
|
{
|
|
"notification_id": "uuid",
|
|
"subject": "Maintenance window",
|
|
"filter_type": "group",
|
|
"filter_value": "host_hec-bot_admin",
|
|
"active_only": true,
|
|
"status": "sent",
|
|
"sent_count": 5,
|
|
"failed_count": 0,
|
|
"created_by": "wmantly",
|
|
"created_on": 1234567890000,
|
|
"sent_at": 1234567890123
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Results are sorted by `created_on` descending (most recent first).
|
|
|
|
---
|
|
|
|
### Get Notification Record
|
|
|
|
**`GET /api/notification/:id`** — `app_sso_admin` required
|
|
|
|
**Response:** `{ "results": { <notification object> } }`
|
|
|
|
---
|
|
|
|
## OAuth 2.0 / OpenID Connect
|
|
|
|
The SSO Manager acts as an OAuth 2.0 Authorization Server and OpenID Connect Provider.
|
|
|
|
### Discovery
|
|
|
|
**`GET /.well-known/openid-configuration`** — No auth required
|
|
|
|
Returns the OIDC discovery document with endpoint URLs, supported scopes, and signing algorithms.
|
|
|
|
---
|
|
|
|
### Authorization Endpoint
|
|
|
|
**`GET /oauth/authorize`** — No auth required (redirects to login if not authenticated)
|
|
|
|
**Query Parameters:**
|
|
- `response_type` — Must be `code`
|
|
- `client_id` — Registered OAuth client ID
|
|
- `redirect_uri` — Must match a URI registered for the client, either exactly
|
|
or against a registered wildcard pattern (`*` = one hostname label, `**` =
|
|
any number of labels)
|
|
- `scope` — Space-separated: `openid`, `profile`, `email`
|
|
- `state` — Opaque value returned unchanged in the redirect
|
|
- `code_challenge` — PKCE challenge (SHA-256 of code_verifier, base64url-encoded)
|
|
- `code_challenge_method` — Must be `S256`
|
|
|
|
Renders the consent screen.
|
|
|
|
---
|
|
|
|
### Authorize (Issue Code)
|
|
|
|
Issues the authorization code after the user approves the consent form shown
|
|
by the Authorization Endpoint above. This is called by the consent page itself
|
|
(an authenticated request, via `auth-token`), not by the OAuth client
|
|
directly.
|
|
|
|
**`POST /api/oauth/authorize`** — Auth required (`auth-token` header)
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"response_type": "code",
|
|
"client_id": "uuid",
|
|
"redirect_uri": "https://ha.example.com/auth/external/callback",
|
|
"scope": "openid profile email",
|
|
"state": "opaque-state-value",
|
|
"code_challenge": "pkce-challenge",
|
|
"code_challenge_method": "S256"
|
|
}
|
|
```
|
|
|
|
Only scopes the client is actually registered for are granted, even if more
|
|
are requested. If the client has `allowed_groups` set, the authenticated user
|
|
must be a member of at least one of those groups or the request is rejected
|
|
with `403`.
|
|
|
|
**Response:**
|
|
```json
|
|
{ "redirect_url": "https://ha.example.com/auth/external/callback?code=<code>&state=<state>" }
|
|
```
|
|
|
|
The caller (the consent page) redirects the browser to `redirect_url`, which
|
|
completes the flow described in the Authorization Endpoint section above.
|
|
|
|
---
|
|
|
|
### Token Endpoint
|
|
|
|
**`POST /oauth/token`** — No auth required (client authenticates via credentials)
|
|
|
|
**Content-Type:** `application/x-www-form-urlencoded` or `application/json`
|
|
|
|
**Client Authentication:** `client_id` + `client_secret` in the request body, or HTTP Basic Auth.
|
|
|
|
#### Authorization Code Grant
|
|
|
|
```
|
|
grant_type=authorization_code
|
|
&code=<auth_code>
|
|
&redirect_uri=<redirect_uri>
|
|
&client_id=<client_id>
|
|
&client_secret=<client_secret>
|
|
&code_verifier=<pkce_verifier>
|
|
```
|
|
|
|
#### Refresh Token Grant
|
|
|
|
```
|
|
grant_type=refresh_token
|
|
&refresh_token=<refresh_token>
|
|
&client_id=<client_id>
|
|
&client_secret=<client_secret>
|
|
```
|
|
|
|
Refresh tokens are rotated on each use — the old token is invalidated and a new one is returned.
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"access_token": "uuid",
|
|
"token_type": "Bearer",
|
|
"expires_in": 3600,
|
|
"refresh_token": "uuid",
|
|
"id_token": "<jwt>"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### UserInfo Endpoint
|
|
|
|
**`GET /oauth/userinfo`** — Bearer token required (`Authorization: Bearer <access_token>`)
|
|
|
|
**Response** (claims vary by granted scopes):
|
|
```json
|
|
{
|
|
"sub": "username",
|
|
"name": "First Last",
|
|
"given_name": "First",
|
|
"family_name": "Last",
|
|
"preferred_username": "username",
|
|
"email": "user@example.com"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## OAuth Client Management
|
|
|
|
Base path: `/api/oauth/client`
|
|
|
|
All endpoints require authentication and `app_sso_oauth_admin` membership.
|
|
|
|
### List Clients
|
|
|
|
**`GET /api/oauth/client/`**
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"results": [
|
|
{
|
|
"client_id": "uuid",
|
|
"name": "Home Assistant",
|
|
"description": "Home automation",
|
|
"redirect_uris": ["https://ha.example.com/auth/external/callback"],
|
|
"scopes": ["openid", "profile", "email"],
|
|
"allowed_groups": [],
|
|
"token_lifetime": { "access_token": 3600, "refresh_token": 2592000 },
|
|
"created_by": "wmantly",
|
|
"created_on": 1234567890000
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Register Client
|
|
|
|
**`POST /api/oauth/client/`**
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"name": "Home Assistant",
|
|
"description": "Home automation dashboard",
|
|
"redirect_uris": ["https://ha.example.com/auth/external/callback"],
|
|
"scopes": ["openid", "profile", "email"],
|
|
"allowed_groups": [],
|
|
"token_lifetime": { "access_token": 3600, "refresh_token": 2592000 }
|
|
}
|
|
```
|
|
|
|
`redirect_uris` may also be a newline-separated string. `scopes` may also be a space-separated string.
|
|
`allowed_groups` restricts the client to members of the listed SSO groups (empty/omitted = any valid user).
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"results": { "client_id": "uuid", "name": "Home Assistant" },
|
|
"client_secret": "raw-secret-shown-once",
|
|
"message": "OAuth client 'Home Assistant' created. Save the client secret — it will not be shown again."
|
|
}
|
|
```
|
|
|
|
The `client_secret` is shown **only once**. Store it immediately.
|
|
|
|
---
|
|
|
|
### Get Client
|
|
|
|
**`GET /api/oauth/client/:client_id`**
|
|
|
|
**Response:** `{ "results": { <client object> } }`
|
|
|
|
---
|
|
|
|
### Update Client
|
|
|
|
**`PUT /api/oauth/client/:client_id`**
|
|
|
|
**Request:** Any subset of `name`, `description`, `redirect_uris`, `scopes`, `allowed_groups`, `token_lifetime`, `is_valid`.
|
|
|
|
**Response:** `{ "results": { <updated client> }, "message": "..." }`
|
|
|
|
---
|
|
|
|
### Delete Client
|
|
|
|
**`DELETE /api/oauth/client/:client_id`**
|
|
|
|
**Response:** `{ "client_id": "uuid", "message": "OAuth client '...' deleted." }`
|
|
|
|
---
|
|
|
|
### Rotate Client Secret
|
|
|
|
**`POST /api/oauth/client/:client_id/rotate`**
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"client_secret": "new-raw-secret-shown-once",
|
|
"message": "Client secret rotated for '...'. Save it — it will not be shown again."
|
|
}
|
|
```
|
|
|
|
The old secret is invalidated immediately. The new secret is shown **only once**.
|
|
|
|
---
|
|
|
|
### Token Lifetimes
|
|
|
|
Configurable per-client via `token_lifetime`. Global defaults (in seconds):
|
|
|
|
```json
|
|
{ "access_token": 3600, "refresh_token": 2592000 }
|
|
```
|
|
|
|
---
|
|
|
|
## Plugin Endpoints
|
|
|
|
Base path: `/api/plugins`
|
|
|
|
All endpoints require authentication and `app_sso_admin`, `app_sso_directory_admin`, or `app_super_admin` membership. Secret field values are always returned masked (`********`); they are stored in OpenBao at `secret/plugins/<instance-id>/conf`, never in the database row. See [Plugins](docs/plugins.html).
|
|
|
|
### List Plugin Types
|
|
|
|
**`GET /api/plugins/types`**
|
|
|
|
Returns the installed plugin types and their `configSchema` (used to build the create-instance form).
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"results": [
|
|
{
|
|
"type": "proxmox",
|
|
"category": "discovery",
|
|
"name": "Proxmox VE",
|
|
"description": "Discover VMs, containers, and hypervisor nodes from a Proxmox VE API endpoint.",
|
|
"configSchema": [
|
|
{ "key": "url", "label": "API URL", "type": "url", "required": true },
|
|
{ "key": "tokenId", "label": "Token ID", "type": "text", "required": true },
|
|
{ "key": "tokenSecret", "label": "Token Secret", "type": "password", "required": true, "secret": true }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### List Plugin Instances
|
|
|
|
**`GET /api/plugins/`**
|
|
|
|
**Response:** `{ "results": [ { "id", "pluginType", "category", "name", "slug", "enabled", "cron", "config", "secrets": {…masked…}, "lastRunAt", "lastStatus", "lastError" } ] }`
|
|
|
|
---
|
|
|
|
### Get One Instance
|
|
|
|
**`GET /api/plugins/:id`** — same shape as a list entry.
|
|
|
|
---
|
|
|
|
### Create Instance
|
|
|
|
**`POST /api/plugins/`**
|
|
|
|
`config` is a flat object of **all** field values (secret and non-secret); the server splits it — non-secret fields go to the DB row, secret fields to OpenBao. Creating an enabled instance schedules it and kicks one immediate run. `slug` is the discovery source name (lowercase letters/digits/_/-, max 64, unique).
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"pluginType": "proxmox",
|
|
"name": "Proxmox — Home Lab",
|
|
"slug": "proxmox-homelab",
|
|
"cron": "0 * * * *",
|
|
"config": { "url": "https://pve:8006", "tokenId": "u@pam!t", "tokenSecret": "secret-value" }
|
|
}
|
|
```
|
|
|
|
Errors: `400` if the plugin type is unknown, the slug is malformed/duplicated, or a required field is missing; `400` with an OpenBao hint if writing the secret fails (re-run `./setup.sh` with theta-suite ≥ v1.30.1).
|
|
|
|
---
|
|
|
|
### Update Instance
|
|
|
|
**`PUT /api/plugins/:id`** — update `name`, `cron`, `enabled`, and non-secret `config`. Secret fields are changed via `PUT /:id/secrets`. Re-schedules if `cron` or `enabled` changed.
|
|
|
|
---
|
|
|
|
### Update Secrets
|
|
|
|
**`PUT /api/plugins/:id/secrets`** — body is a flat object of secret field values. Blank/`********` values are ignored (kept as-is).
|
|
|
|
---
|
|
|
|
### Test Instance
|
|
|
|
**`POST /api/plugins/:id/test`** — runs the plugin's `validate`. Returns `{ "ok": true }` or `400 { "ok": false, "error": "..." }`.
|
|
|
|
---
|
|
|
|
### Load / Unload / Run Now
|
|
|
|
- **`POST /api/plugins/:id/load`** — enable + schedule + run now.
|
|
- **`POST /api/plugins/:id/unload`** — unschedule + disable.
|
|
- **`POST /api/plugins/:id/run`** — enqueue one immediate run (regardless of enabled).
|
|
|
|
---
|
|
|
|
### Last Run Status
|
|
|
|
**`GET /api/plugins/:id/runs`** → `{ "results": { "lastRunAt", "lastStatus", "lastError" } }` (`lastStatus` is `ok` | `error` | `running`).
|
|
|
|
---
|
|
|
|
### Delete Instance
|
|
|
|
**`DELETE /api/plugins/:id`** — unschedules, removes the OpenBao secret namespace, and deletes the row.
|
|
|
|
## Configuration Endpoints
|
|
|
|
Base path: `/api/conf`
|
|
|
|
All endpoints require authentication and `app_sso_admin` membership. Runtime configuration (SMTP, discovery, OAuth) is stored in OpenBao at `secret/sso-manager/conf` and overlaid onto the live app config; changes take effect immediately and persist across restarts. Secret fields (`smtp.pass`, `oauth.jwtSecret`) are **always returned masked** (`********`); submit a blank or `********` value to keep the current stored secret, or a new non-blank value to replace it.
|
|
|
|
### Get Configuration
|
|
|
|
**`GET /api/conf`** — returns the editable config groups (`smtp`, `discovery`, `oauth`) with secret fields masked to `********`.
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"smtp": { "host": "smtp.example.com", "port": 587, "secure": false, "user": "noreply@example.com", "pass": "********", "from": "SSO Manager <noreply@example.com>" },
|
|
"discovery": { },
|
|
"oauth": { "issuer": "https://sso.example.com", "jwtSecret": "********", "token_lifetime": { "access_token": 3600, "refresh_token": 2592000 } }
|
|
}
|
|
```
|
|
|
|
### Save Configuration
|
|
|
|
**`POST /api/conf`** — deep-merges the submitted groups into `secret/sso-manager/conf` (per-key shallow merge of nested objects) and re-applies them to the live config. A blank or `********` value for `smtp.pass` or `oauth.jwtSecret` preserves the stored secret.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"smtp": { "host": "smtp.example.com", "port": 587, "secure": false, "user": "noreply@example.com", "pass": "********", "from": "SSO Manager <noreply@example.com>" },
|
|
"oauth": { "issuer": "https://sso.example.com", "token_lifetime": { "access_token": 3600, "refresh_token": 2592000 } }
|
|
}
|
|
```
|
|
|
|
**Response:** `{ "success": true }`
|
|
|
|
---
|
|
|
|
## Subtype Driver Operations Endpoints
|
|
|
|
Base path: `/api/directory-admin/resources`
|
|
|
|
All endpoints require authentication and `app_sso_admin`, `app_sso_directory_admin`, or `admin` permission.
|
|
|
|
### Get Subtype Driver Metrics
|
|
|
|
**`GET /api/directory-admin/resources/:id/driver-metrics`**
|
|
|
|
Resolves the operational driver for the resource via the 4-tier engine (`theta-agent`, specialized subtype driver, parent hypervisor provider, or unmanaged fallback) and returns real-time telemetry.
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"resourceId": "res-id",
|
|
"metrics": {
|
|
"status": "online",
|
|
"driver": "database",
|
|
"subType": "redis",
|
|
"redis": { "connectedClients": 4, "usedMemoryBytes": 12582912, "opsPerSec": 42 }
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Execute Subtype Driver Action
|
|
|
|
**`POST /api/directory-admin/resources/:id/driver-action`**
|
|
|
|
Executes a protocol action on the target resource (e.g. systemd restart, Proxmox power control, Redis flush, K8s scale).
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"action": "restart",
|
|
"params": { "serviceName": "emby-server" }
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"resourceId": "res-id",
|
|
"result": { "status": "ok", "driver": "docker_socket", "action": "restart" }
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Get Subtype Driver Logs
|
|
|
|
**`GET /api/directory-admin/resources/:id/driver-logs?lines=100`**
|
|
|
|
Retrieves recent operational logs for the resource via the resolved driver (`journalctl`, `docker logs`, Proxmox task logs, K8s pod logs).
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"resourceId": "res-id",
|
|
"logs": "[docker logs --tail 100 emby-server]\nContainer initialized..."
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Error Responses
|
|
|
|
All endpoints return errors in this format:
|
|
|
|
```json
|
|
{ "name": "ErrorName", "message": "Error message description" }
|
|
```
|
|
|
|
### HTTP Status Codes
|
|
|
|
| Code | Meaning |
|
|
|------|---------|
|
|
| `200` | Success |
|
|
| `400` | Bad Request — invalid input |
|
|
| `401` | Unauthorized — auth required, token invalid, or insufficient permission |
|
|
| `404` | Not Found |
|
|
| `429` | Too Many Requests — rate limit exceeded |
|
|
| `500` | Internal Server Error |
|
|
| `502` | Bad Gateway — upstream service failure (e.g. SMS delivery) |
|