From 4cadf46d9d20e80286e2b4f1016af694ed4fcdb2 Mon Sep 17 00:00:00 2001 From: Trezy Date: Sat, 14 Mar 2026 16:53:05 +0000 Subject: [PATCH] docs: add documentation for 2.0 --- docs/getting-started/authentication.md | 6 +++--- docs/getting-started/dashboard.md | 10 +++++++++- docs/getting-started/quickstart.md | 2 +- docs/guides/api-keys.md | 15 ++++++++------- docs/guides/event-logs.md | 47 ++++++++++++++++++++++++++++++++++++++++------- docs/guides/permissions.md | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/guides/scripting.md | 43 +++++++++++++++++++++++++++++++++++++++++++ docs/reference/admin-api.md | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------- docs/reference/architecture.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------- docs/reference/changelog.md | 10 ++++++++++ docs/reference/glossary.md | 6 ++++++ docs/reference/troubleshooting.md | 15 +++++++++++++-- 12 file(s) changed, 483 insertion(s)(+), 58 deletion(s)(-) diff --git a/docs/getting-started/authentication.md b/docs/getting-started/authentication.md --- a/docs/getting-started/authentication.md +++ b/docs/getting-started/authentication.md @@ -8,7 +8,7 @@ | Endpoint type | Auth required? | |---------------|---------------| | Queries (`GET /xrpc/{method}`) | No | | Procedures (`POST /xrpc/{method}`) | Yes | -| Admin API (`/admin/*`) | Yes (must be an admin) | +| Admin API (`/admin/*`) | Yes (must be a user with appropriate [permissions](../guides/permissions.md)) | | Health check (`GET /health`) | No | Authenticated requests must include an `Authorization` header with a token issued by AIP: @@ -60,6 +60,6 @@ Token validation happens on every request; there is no local token caching. ## Admin access -Admin endpoints require the authenticated user's DID to exist in the `admins` table. If the table is empty (fresh deployment), the first authenticated request to any admin endpoint auto-bootstraps that user as the initial admin. +Admin endpoints require the authenticated user's DID to exist in the `users` table with the appropriate [permissions](../guides/permissions.md). If the table is empty (fresh deployment), the first authenticated request to any admin endpoint auto-bootstraps that user as the **super user** with all permissions granted. -To add more admins, use `POST /admin/admins` or the [dashboard](dashboard.md). See [Admin API](../reference/admin-api.md#admin-management) for details. +To add more users, use `POST /admin/users` or the [dashboard](dashboard.md). You can assign permissions individually or use a template (`viewer`, `operator`, `manager`, `full_access`). See [Admin API](../reference/admin-api.md#user-management) for details. diff --git a/docs/getting-started/dashboard.md b/docs/getting-started/dashboard.md --- a/docs/getting-started/dashboard.md +++ b/docs/getting-started/dashboard.md @@ -4,7 +4,15 @@ HappyView ships with a web dashboard that provides a visual interface for everything the [admin API](../reference/admin-api.md) offers: managing lexicons, viewing indexed records, and monitoring backfill jobs. It runs as a separate Next.js application alongside the Rust backend. ## Logging in for the first time -The dashboard uses AT Protocol OAuth via AIP. If no admins exist in the database yet, the first authenticated request to any admin endpoint automatically bootstraps that user as an admin. +The dashboard uses AT Protocol OAuth via AIP. If no users exist in the database yet, the first authenticated request to any admin endpoint automatically bootstraps that user as the super user with all permissions. + +## Settings + +The **Settings** page is organized into sub-pages accessible from the collapsible sidebar: + +- **Users** — manage user accounts and permissions +- **ENV Variables** — view and edit script variables +- **API Keys** — create and revoke API keys ## Adding a lexicon diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -18,7 +18,7 @@ ## 2. Log in to the dashboard Open your HappyView instance in a browser. The built-in [dashboard](dashboard.md) is served at the root URL. -Click **Log in** and authenticate with your AT Protocol identity. On a fresh deployment with no admins configured, the first authenticated request to any admin endpoint automatically bootstraps that user as an admin. +Click **Log in** and authenticate with your AT Protocol identity. On a fresh deployment with no users configured, the first authenticated request to any admin endpoint automatically bootstraps that user as the **super user** with all permissions granted. ## 3. Add your first lexicon diff --git a/docs/guides/api-keys.md b/docs/guides/api-keys.md --- a/docs/guides/api-keys.md +++ b/docs/guides/api-keys.md @@ -12,14 +12,14 @@ ``` When you create a key, HappyView shows the full token **once**. After that, only the prefix (`hv_a1b2c3d4`) is stored for display — the key itself is SHA-256 hashed before being saved to the database. This means nobody (including you) can retrieve the full key after creation. -API keys inherit the permissions of the admin who created them. A revoked key immediately stops working. +Each API key has its own set of **scoped permissions**. When you create a key, you specify which permissions it should have. The key's effective permissions are the **intersection** of the permissions assigned to the key and the permissions of the user who created it — a key can never have more access than its creator. A revoked key immediately stops working. ## Creating a key -1. Go to **Settings** in the dashboard -2. Select the **API Keys** tab -3. Click **Create API Key** -4. Enter a descriptive name (e.g., "CI Deploy", "Monitoring Script") +1. Go to **Settings > API Keys** in the dashboard sidebar +2. Click **Create API Key** +3. Enter a descriptive name (e.g., "CI Deploy", "Monitoring Script") +4. Select the permissions the key should have 5. Copy the full key from the confirmation dialog — you won't see it again ## Using a key @@ -31,7 +31,7 @@ curl http://localhost:3000/admin/lexicons \ -H "Authorization: Bearer hv_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" ``` -This works for all [admin API](../reference/admin-api.md) endpoints — anything you can do with an OAuth token, you can do with an API key. +This works for all [admin API](../reference/admin-api.md) endpoints that the key has permissions for. Unlike OAuth tokens which carry the user's full permissions, API keys are limited to the specific permissions assigned at creation time. ## Revoking a key @@ -47,7 +47,8 @@ The **Last Used** column in the API Keys table shows when each key was last used to authenticate a request. Keys that have never been used show "Never". This helps you identify unused keys that can be safely revoked. ## Security considerations -- **Treat API keys like passwords.** Anyone with the key has full admin access to your HappyView instance. +- **Treat API keys like passwords.** Anyone with the key can access your HappyView instance with the key's permissions. +- **Use the principle of least privilege.** Only grant the permissions a key actually needs. A CI deploy key probably only needs `lexicons:create` and `backfill:create`, not full access. - **Use descriptive names** so you can identify which service uses which key. - **Revoke keys you no longer need.** If a key is compromised, revoke it immediately. - **Don't commit keys to version control.** Use environment variables or secret managers instead. diff --git a/docs/guides/event-logs.md b/docs/guides/event-logs.md --- a/docs/guides/event-logs.md +++ b/docs/guides/event-logs.md @@ -1,6 +1,6 @@ # Event Logs -HappyView maintains an internal event log that records system activity — lexicon changes, record operations, Lua script executions and errors, admin actions, backfill jobs, and Tap connectivity. Events are stored in a Postgres table and queryable via the [admin API](../reference/admin-api.md#event-logs). +HappyView maintains an internal event log that records system activity — lexicon changes, record operations, Lua script executions and errors, user actions, API key events, backfill jobs, and Tap connectivity. Events are stored in a Postgres table and queryable via the [admin API](../reference/admin-api.md#event-logs). ## Event types @@ -14,7 +14,7 @@ | `lexicon.created` | info | Lexicon NSID | `revision`, `has_script`, `source` | | `lexicon.updated` | info | Lexicon NSID | `revision`, `has_script`, `source` | | `lexicon.deleted` | info | Lexicon NSID | — | -Logged when lexicons are uploaded, updated, or deleted via the [admin API](../reference/admin-api.md#lexicons). The `actor_did` is the admin who performed the action. +Logged when lexicons are uploaded, updated, or deleted via the [admin API](../reference/admin-api.md#lexicons). The `actor_did` is the user who performed the action. ### Record events @@ -38,15 +38,48 @@ :::note For query scripts (unauthenticated), `caller_did` and `input` are omitted from the detail since queries don't have an authenticated user or request body. ::: -### Admin events +### User events + +| Event Type | Severity | Subject | Detail | +|---|---|---|---| +| `user.created` | info | New user DID | `template` (if used) | +| `user.deleted` | info | Removed user ID | — | +| `user.bootstrapped` | info | Bootstrapped user DID | — | +| `user.permissions_updated` | info | User ID | `granted`, `revoked` | +| `user.super_transferred` | warn | New super user ID | `from_user_id` | + +The `user.bootstrapped` event is logged when the first user is auto-promoted to super user (see [Auth - Auto-bootstrap](../reference/admin-api.md#auth)). + +### Auth events + +| Event Type | Severity | Subject | Detail | +|---|---|---|---| +| `auth.permission_denied` | error | Endpoint path | `required_permission`, `user_id` | + +Logged when a user attempts to access an endpoint they don't have permission for. + +### API Key events + +| Event Type | Severity | Subject | Detail | +|---|---|---|---| +| `api_key.created` | info | Key ID | `name`, `permissions` | +| `api_key.revoked` | info | Key ID | `name` | + +### Script Variable events + +| Event Type | Severity | Subject | Detail | +|---|---|---|---| +| `script_variable.upserted` | info | Variable key | — | +| `script_variable.deleted` | info | Variable key | — | + +### Hook events | Event Type | Severity | Subject | Detail | |---|---|---|---| -| `admin.created` | info | New admin DID | — | -| `admin.deleted` | info | Removed admin ID | — | -| `admin.bootstrapped` | info | Bootstrapped admin DID | — | +| `hook.executed` | info | Record AT URI | `lexicon_id` | +| `hook.dead_lettered` | error | Record AT URI | `lexicon_id`, `error` | -The `admin.bootstrapped` event is logged when the first user is auto-promoted to admin (see [Auth - Auto-bootstrap](../reference/admin-api.md#auth)). +Logged when [index hooks](index-hooks.md) run. Dead-lettered events indicate a hook failed all retry attempts. ### Backfill events diff --git a/docs/guides/permissions.md b/docs/guides/permissions.md new file mode 100644 --- /dev/null +++ b/docs/guides/permissions.md @@ -0,0 +1,141 @@ +# Permissions + +HappyView uses a granular permission system to control access to the admin API. Each user has a set of permissions that determine which endpoints they can access. Permissions can be assigned individually, via templates, or both. + +## Permission list + +HappyView defines 20 permissions organized by category: + +### Lexicons + +| Permission | Description | +|---|---| +| `lexicons:create` | Upload and upsert lexicons (local and network) | +| `lexicons:read` | List and view lexicon details | +| `lexicons:delete` | Delete lexicons | + +### Records + +| Permission | Description | +|---|---| +| `records:read` | List and view indexed records | +| `records:delete` | Delete individual records | +| `records:delete-collection` | Bulk-delete all records in a collection | + +### Script Variables + +| Permission | Description | +|---|---| +| `script-variables:create` | Create and update script variables | +| `script-variables:read` | List script variables (values are masked) | +| `script-variables:delete` | Delete script variables | + +### Users + +| Permission | Description | +|---|---| +| `users:create` | Add new users | +| `users:read` | List and view user details | +| `users:update` | Modify user permissions | +| `users:delete` | Remove users | + +### API Keys + +| Permission | Description | +|---|---| +| `api-keys:create` | Create new API keys | +| `api-keys:read` | List API keys | +| `api-keys:delete` | Revoke API keys | + +### Operations + +| Permission | Description | +|---|---| +| `backfill:create` | Start backfill jobs | +| `backfill:read` | View backfill job status | +| `stats:read` | View record and Tap statistics | +| `events:read` | Query the event log | + +## Permission templates + +Templates are predefined sets of permissions that simplify user creation. Pass a `template` value when creating a user via `POST /admin/users`. + +### Viewer + +Read-only access. Can browse lexicons, records, stats, events, and user lists but cannot modify anything. + +Includes: `lexicons:read`, `records:read`, `script-variables:read`, `users:read`, `api-keys:read`, `backfill:read`, `stats:read`, `events:read` + +### Operator + +Everything in Viewer, plus the ability to run backfill jobs and manage API keys. + +Adds: `backfill:create`, `api-keys:create`, `api-keys:delete` + +### Manager + +Everything in Operator, plus the ability to manage lexicons, records, and script variables. + +Adds: `lexicons:create`, `lexicons:delete`, `script-variables:create`, `script-variables:delete`, `records:delete` + +### Full Access + +All 20 permissions. Equivalent to granting every permission individually (but still not a super user). + +## Super user + +The super user is a special user created automatically when the first person logs in to a fresh HappyView instance. The super user: + +- Has unrestricted access to all endpoints, regardless of which permissions are assigned +- Is the only user who can call `POST /admin/users/transfer-super` +- Cannot be deleted +- Cannot have their permissions modified by other users + +There is always exactly one super user. Super status can be transferred to another user via the transfer endpoint. + +## Escalation guards + +HappyView prevents privilege escalation: + +- When creating a user or API key, you can only grant permissions that you yourself have. Attempting to grant a permission you lack returns `403 Forbidden`. +- When updating a user's permissions, the same rule applies — you cannot grant permissions beyond your own. + +## Self-modification guards + +Users cannot modify their own account in destructive ways: + +- You cannot delete yourself +- You cannot revoke your own permissions + +These guards prevent accidental lockout. + +## API key permissions + +API keys have their own set of permissions, specified at creation time. The effective permissions of an API key are the **intersection** of: + +1. The permissions assigned to the key +2. The permissions of the user who owns the key + +This means if a user's permissions are later reduced, any API keys they created are also effectively reduced — even though the key's own permission list doesn't change. + +For example, if a user with `lexicons:create` and `lexicons:read` creates a key with both permissions, and the user later loses `lexicons:create`, the key can only use `lexicons:read`. + +## Managing permissions + +### Via the dashboard + +Go to **Settings > Users** to view and manage user permissions. Click on a user to see their current permissions and modify them. You can also assign templates when creating new users. + +### Via the API + +- `POST /admin/users` — create a user with a template or explicit permissions +- `PATCH /admin/users/{id}/permissions` — grant or revoke individual permissions +- `POST /admin/users/transfer-super` — transfer super user status (super user only) + +See the [Admin API reference](../reference/admin-api.md#user-management) for full details. + +## Next steps + +- [Admin API reference](../reference/admin-api.md) — endpoint documentation with required permissions +- [API Keys](api-keys.md) — creating scoped API keys +- [Event Logs](event-logs.md) — permission-denied events are logged for auditing diff --git a/docs/guides/scripting.md b/docs/guides/scripting.md --- a/docs/guides/scripting.md +++ b/docs/guides/scripting.md @@ -330,6 +330,49 @@ local resp = http.delete(url, { headers = { ... } }) local resp = http.head(url) ``` +## AT Protocol API + +The `atproto` table provides AT Protocol utility functions. Available in queries, procedures, and [index hooks](index-hooks.md). + +### atproto.resolve_service_endpoint + +```lua +local endpoint = atproto.resolve_service_endpoint(did) +``` + +Resolves a DID to its AT Protocol service endpoint URL by fetching the DID document. Supports both `did:plc:*` (via the PLC directory) and `did:web:*` (via `.well-known/did.json`). + +| Parameter | Type | Description | +| --------- | ------ | ------------------------ | +| `did` | string | The DID to resolve | + +**Returns:** The service endpoint URL as a string, or `nil` if resolution fails (DID not found, no PDS service in document, network error). + +### Examples + +```lua +-- Resolve a did:plc DID +local endpoint = atproto.resolve_service_endpoint("did:plc:abc123") +-- endpoint = "https://pds.example.com" + +-- Resolve a did:web DID +local endpoint = atproto.resolve_service_endpoint("did:web:example.com") +-- endpoint = "https://example.com" + +-- Handle resolution failure +local endpoint = atproto.resolve_service_endpoint("did:plc:unknown") +if not endpoint then + return { error = "Could not resolve DID" } +end + +-- Use with HTTP API to call a remote XRPC endpoint +local endpoint = atproto.resolve_service_endpoint(did) +if endpoint then + local resp = http.get(endpoint .. "/xrpc/com.example.method") + local data = json.decode(resp.body) +end +``` + ## JSON API The `json` global provides JSON serialization and deserialization. Available in queries, procedures, and [index hooks](index-hooks.md). diff --git a/docs/reference/admin-api.md b/docs/reference/admin-api.md --- a/docs/reference/admin-api.md +++ b/docs/reference/admin-api.md @@ -1,6 +1,6 @@ # Admin API -The admin API lets you manage lexicons, monitor records, run backfill jobs, and control admin access. All endpoints live under `/admin` and require an [AIP](https://github.com/graze-social/aip)-issued Bearer token from a DID that exists in the `admins` table. You can also manage all of this through the [web dashboard](../getting-started/dashboard.md). +The admin API lets you manage lexicons, monitor records, run backfill jobs, and control user access. All endpoints live under `/admin` and require an [AIP](https://github.com/graze-social/aip)-issued Bearer token from a DID that exists in the `users` table, with the appropriate [permissions](../guides/permissions.md) for the endpoint being called. You can also manage all of this through the [web dashboard](../getting-started/dashboard.md). ## Auth @@ -9,11 +9,11 @@ 1. **OAuth (AIP)** — the Bearer token is validated against AIP's `/oauth/userinfo` endpoint to retrieve the caller's DID. 2. **API keys** — read/write tokens starting with `hv_`. See the [API Keys guide](../guides/api-keys.md) for details. -In both cases the resolved DID is checked against the `admins` table. +In both cases the resolved DID is checked against the `users` table, and the user's permissions are loaded to authorize the request. -**Auto-bootstrap**: If the `admins` table is empty, the first authenticated request automatically inserts the caller as the initial admin. +**Auto-bootstrap**: If the `users` table is empty, the first authenticated request automatically creates the caller as the **super user** with all permissions granted. -Non-admin DIDs receive a `403 Forbidden` response. +Non-user DIDs receive a `403 Forbidden` response. Users without the required permission for a specific endpoint also receive `403 Forbidden`. All error responses return JSON with an `error` field: @@ -27,8 +27,8 @@ | Status | Meaning | | ------------------ | -------------------------------------------------------------------------------------------------------------- | | `400 Bad Request` | Invalid input (missing required fields, malformed lexicon JSON) | | `401 Unauthorized` | Missing or invalid Bearer token. See [AIP documentation](https://github.com/graze-social/aip) for token issues | -| `403 Forbidden` | Authenticated DID is not in the admins table | -| `404 Not Found` | Lexicon, admin, or backfill job not found | +| `403 Forbidden` | Authenticated DID is not in the users table, or user lacks the required permission | +| `404 Not Found` | Lexicon, user, or backfill job not found | ```sh # All examples assume $TOKEN is an AIP-issued access token or API key @@ -310,7 +310,7 @@ ``` ## Event Logs -HappyView records an audit trail of system events: lexicon changes, record operations, Lua script executions and errors, admin actions, backfill jobs, and Tap connectivity. See the [Event Logs guide](../guides/event-logs.md) for details on event types and retention. +HappyView records an audit trail of system events: lexicon changes, record operations, Lua script executions and errors, user actions, backfill jobs, and Tap connectivity. See the [Event Logs guide](../guides/event-logs.md) for details on event types and retention. ### List event logs @@ -368,16 +368,22 @@ ``` POST /admin/api-keys ``` +Requires `api-keys:create` permission. + ```sh curl -X POST http://localhost:3000/admin/api-keys \ -H "$AUTH" \ -H "Content-Type: application/json" \ - -d '{ "name": "CI Deploy" }' + -d '{ + "name": "CI Deploy", + "permissions": ["lexicons:read", "lexicons:create", "backfill:create"] + }' ``` -| Field | Type | Required | Description | -| ------ | ------ | -------- | ------------------------------------ | -| `name` | string | yes | A label to identify this key's usage | +| Field | Type | Required | Description | +| ------------- | -------- | -------- | -------------------------------------------------------------------------------------------- | +| `name` | string | yes | A label to identify this key's usage | +| `permissions` | string[] | yes | Permissions to grant the key (must be a subset of the creating user's own permissions) | **Response**: `201 Created` @@ -386,17 +392,20 @@ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "CI Deploy", "key": "hv_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", - "key_prefix": "hv_a1b2c3d4" + "key_prefix": "hv_a1b2c3d4", + "permissions": ["lexicons:read", "lexicons:create", "backfill:create"] } ``` -The `key` field contains the full API key. It is only returned in this response — store it securely. +The `key` field contains the full API key. It is only returned in this response — store it securely. The key's effective permissions are the **intersection** of the permissions specified here and the creating user's permissions at the time of each request. ### List API keys ``` GET /admin/api-keys ``` + +Requires `api-keys:read` permission. ```sh curl http://localhost:3000/admin/api-keys -H "$AUTH" @@ -410,6 +419,7 @@ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "CI Deploy", "key_prefix": "hv_a1b2c3d4", + "permissions": ["lexicons:read", "lexicons:create", "backfill:create"], "created_at": "2026-03-01T00:00:00Z", "last_used_at": "2026-03-06T12:00:00Z", "revoked_at": null @@ -417,13 +427,15 @@ } ] ``` -Only returns keys belonging to the authenticated admin. The full key is never included — only the prefix. +Only returns keys belonging to the authenticated user. The full key is never included — only the prefix. ### Revoke an API key ``` DELETE /admin/api-keys/{id} ``` + +Requires `api-keys:delete` permission. ```sh curl -X DELETE http://localhost:3000/admin/api-keys/550e8400-e29b-41d4-a716-446655440000 \ @@ -434,38 +446,55 @@ Sets `revoked_at` on the key. The key remains in the database for audit purposes but can no longer authenticate. **Response**: `204 No Content` -## Admin management +## User Management -### Add an admin +### Create a user ``` -POST /admin/admins +POST /admin/users ``` +Requires `users:create` permission. You cannot grant permissions you don't have yourself (escalation guard). + ```sh -curl -X POST http://localhost:3000/admin/admins \ +curl -X POST http://localhost:3000/admin/users \ -H "$AUTH" \ -H "Content-Type: application/json" \ - -d '{ "did": "did:plc:newadmin" }' + -d '{ + "did": "did:plc:newuser", + "template": "operator" + }' ``` +| Field | Type | Required | Description | +| ------------- | -------- | -------- | ------------------------------------------------------------------------------------------------- | +| `did` | string | yes | The AT Protocol DID of the user to add | +| `template` | string | no | Permission template: `viewer`, `operator`, `manager`, or `full_access` | +| `permissions` | string[] | no | Explicit list of permissions to grant (used instead of or in addition to `template`) | + +If neither `template` nor `permissions` is provided, the user is created with no permissions. + **Response**: `201 Created` ```json { "id": "550e8400-e29b-41d4-a716-446655440000", - "did": "did:plc:newadmin" + "did": "did:plc:newuser", + "is_super": false, + "permissions": ["lexicons:read", "records:read", "script-variables:read", "users:read", "api-keys:read", "api-keys:create", "api-keys:delete", "backfill:read", "backfill:create", "stats:read", "events:read"] } ``` -### List admins +### List users ``` -GET /admin/admins +GET /admin/users ``` +Requires `users:read` permission. + ```sh -curl http://localhost:3000/admin/admins -H "$AUTH" +curl http://localhost:3000/admin/users -H "$AUTH" ``` **Response**: `200 OK` @@ -475,21 +504,116 @@ [ { "id": "550e8400-e29b-41d4-a716-446655440000", "did": "did:plc:admin", + "is_super": true, + "permissions": ["lexicons:create", "lexicons:read", "lexicons:delete", "records:read", "records:delete", "records:delete-collection", "script-variables:create", "script-variables:read", "script-variables:delete", "users:create", "users:read", "users:update", "users:delete", "api-keys:create", "api-keys:read", "api-keys:delete", "backfill:create", "backfill:read", "stats:read", "events:read"], "created_at": "2025-01-01T00:00:00Z", "last_used_at": "2025-01-02T12:00:00Z" } ] ``` -### Remove an admin +### Get a user ``` -DELETE /admin/admins/{id} +GET /admin/users/{id} ``` +Requires `users:read` permission. + ```sh -curl -X DELETE http://localhost:3000/admin/admins/550e8400-e29b-41d4-a716-446655440000 \ +curl http://localhost:3000/admin/users/550e8400-e29b-41d4-a716-446655440000 -H "$AUTH" +``` + +**Response**: `200 OK` with the same shape as a single item from the list response. + +### Update user permissions + +``` +PATCH /admin/users/{id}/permissions +``` + +Requires `users:update` permission. You cannot grant permissions you don't have yourself, and you cannot modify the super user's permissions. + +```sh +curl -X PATCH http://localhost:3000/admin/users/550e8400-e29b-41d4-a716-446655440000/permissions \ + -H "$AUTH" \ + -H "Content-Type: application/json" \ + -d '{ + "grant": ["lexicons:create", "lexicons:delete"], + "revoke": ["records:delete"] + }' +``` + +| Field | Type | Required | Description | +| -------- | -------- | -------- | ------------------------------ | +| `grant` | string[] | no | Permissions to add | +| `revoke` | string[] | no | Permissions to remove | + +**Response**: `200 OK` with the updated user object. + +### Transfer super user + +``` +POST /admin/users/transfer-super +``` + +Only the current super user can call this endpoint. Transfers super user status to another existing user. + +```sh +curl -X POST http://localhost:3000/admin/users/transfer-super \ + -H "$AUTH" \ + -H "Content-Type: application/json" \ + -d '{ "target_user_id": "550e8400-e29b-41d4-a716-446655440000" }' +``` + +| Field | Type | Required | Description | +| ---------------- | ------ | -------- | ---------------------------------------- | +| `target_user_id` | string | yes | The ID of the user to receive super status | + +**Response**: `200 OK` + +### Delete a user + +``` +DELETE /admin/users/{id} +``` + +Requires `users:delete` permission. You cannot delete the super user or yourself. + +```sh +curl -X DELETE http://localhost:3000/admin/users/550e8400-e29b-41d4-a716-446655440000 \ -H "$AUTH" ``` **Response**: `204 No Content` + +## Permissions + +Each admin API endpoint requires a specific permission. See the [Permissions guide](../guides/permissions.md) for the full list of permissions and templates. + +| Endpoint | Required Permission | +| ------------------------------------- | ---------------------------- | +| `POST /admin/lexicons` | `lexicons:create` | +| `GET /admin/lexicons` | `lexicons:read` | +| `GET /admin/lexicons/{id}` | `lexicons:read` | +| `DELETE /admin/lexicons/{id}` | `lexicons:delete` | +| `POST /admin/network-lexicons` | `lexicons:create` | +| `GET /admin/network-lexicons` | `lexicons:read` | +| `DELETE /admin/network-lexicons/{id}` | `lexicons:delete` | +| `GET /admin/stats` | `stats:read` | +| `GET /admin/tap/stats` | `stats:read` | +| `POST /admin/backfill` | `backfill:create` | +| `GET /admin/backfill/status` | `backfill:read` | +| `GET /admin/events` | `events:read` | +| `POST /admin/api-keys` | `api-keys:create` | +| `GET /admin/api-keys` | `api-keys:read` | +| `DELETE /admin/api-keys/{id}` | `api-keys:delete` | +| `POST /admin/users` | `users:create` | +| `GET /admin/users` | `users:read` | +| `GET /admin/users/{id}` | `users:read` | +| `PATCH /admin/users/{id}/permissions`| `users:update` | +| `DELETE /admin/users/{id}` | `users:delete` | +| `POST /admin/users/transfer-super` | Super user only | +| `GET /admin/script-variables` | `script-variables:read` | +| `POST /admin/script-variables` | `script-variables:create` | +| `DELETE /admin/script-variables/{key}`| `script-variables:delete` | diff --git a/docs/reference/architecture.md b/docs/reference/architecture.md --- a/docs/reference/architecture.md +++ b/docs/reference/architecture.md @@ -49,8 +49,12 @@ middleware.rs Claims extractor (validates Bearer token via AIP /oauth/userinfo) jwks.rs JWKS key fetching admin/ mod.rs Admin route definitions - auth.rs AdminAuth extractor (Claims + DID lookup + auto-bootstrap) - admins.rs Admin CRUD handlers + auth.rs UserAuth extractor (Claims + DID lookup + permission check + auto-bootstrap) + users.rs User CRUD handlers (create, list, get, delete, update permissions, transfer super) + permissions.rs Permission enum (20 permissions), templates (Viewer, Operator, Manager, FullAccess) + api_keys.rs API key CRUD handlers (create, list, revoke) with scoped permissions + events.rs Event log query handler + script_variables.rs Script variable CRUD handlers (list, upsert, delete) lexicons.rs Lexicon CRUD handlers network_lexicons.rs Network lexicon tracking (add, list, remove) records.rs Record listing handler @@ -112,8 +116,8 @@ ``` Client request + Bearer token -> AdminAuth extractor: 1. Claims validation via AIP - 2. DID lookup in admins table (auto-bootstrap if empty) - 3. 403 if not admin + 2. DID lookup in users table (auto-bootstrap super user if empty) + 3. Permission check (403 if missing required permission) -> Admin handler -> JSON response ``` @@ -171,14 +175,58 @@ | `target_collection` | text | For queries/procedures: which record collection | | `created_at` | timestamptz | | | `updated_at` | timestamptz | | -### `admins` +### `users` + +| Column | Type | Description | +| -------------- | ------------- | ------------------------------------------------ | +| `id` | uuid (PK) | | +| `did` | text (unique) | User's AT Protocol DID | +| `is_super` | boolean | Whether this is the super user (only one allowed)| +| `created_at` | timestamptz | | +| `last_used_at` | timestamptz | Updated on each authenticated request | + +### `user_permissions` -| Column | Type | Description | -| -------------- | ------------- | ------------------------------------- | -| `id` | uuid (PK) | | -| `did` | text (unique) | Admin's AT Protocol DID | -| `created_at` | timestamptz | | -| `last_used_at` | timestamptz | Updated on each authenticated request | +| Column | Type | Description | +| ------------ | ----------- | -------------------------------------------- | +| `user_id` | uuid (FK) | References `users.id` | +| `permission` | text | Permission string (e.g. `lexicons:create`) | +| (PK) | | Composite primary key: (`user_id`, `permission`) | + +### `api_keys` + +| Column | Type | Description | +| ------------ | ----------- | -------------------------------------------- | +| `id` | uuid (PK) | | +| `user_id` | uuid (FK) | References `users.id` | +| `name` | text | Descriptive label | +| `key_hash` | text | SHA-256 hash of the full key | +| `key_prefix` | text | First 11 characters for display | +| `permissions`| text[] | Permissions granted to this key | +| `created_at` | timestamptz | | +| `last_used_at`| timestamptz| | +| `revoked_at` | timestamptz | Set when revoked (soft delete) | + +### `event_logs` + +| Column | Type | Description | +| ------------ | ----------- | -------------------------------------------- | +| `id` | uuid (PK) | | +| `event_type` | text | Category.action format (e.g. `user.created`) | +| `severity` | text | `info`, `warn`, or `error` | +| `actor_did` | text | DID of the user who triggered the event | +| `subject` | text | What was affected (DID, NSID, URI, etc.) | +| `detail` | jsonb | Event-specific data | +| `created_at` | timestamptz | | + +### `script_variables` + +| Column | Type | Description | +| ------------ | ----------- | -------------------------------------------- | +| `key` | text (PK) | Variable name | +| `value` | text | Variable value (encrypted at rest) | +| `created_at` | timestamptz | | +| `updated_at` | timestamptz | | ### `backfill_jobs` diff --git a/docs/reference/changelog.md b/docs/reference/changelog.md --- a/docs/reference/changelog.md +++ b/docs/reference/changelog.md @@ -1,5 +1,15 @@ # Changelog +## v2.0.0 — User Permissions & Settings Restructure + +- **User permissions system** — replaced the `admins` table with a `users` table supporting 20 granular permissions, permission templates (Viewer, Operator, Manager, Full Access), and a super user concept with escalation and self-modification guards +- **API key permissions** — API keys now have explicit scoped permissions instead of inheriting full admin access; effective permissions are the intersection of the key's permissions and the user's permissions +- **User handles** — user handles are now displayed alongside DIDs throughout the dashboard +- **Settings sub-pages** — Settings page restructured into Users, ENV Variables, and API Keys sub-pages with collapsible sidebar navigation +- **Dashboard route prefix** — all dashboard pages now live under the `/dashboard` route prefix +- **New endpoints** — `GET /admin/users/{id}`, `PATCH /admin/users/{id}/permissions`, `POST /admin/users/transfer-super`, `GET/POST/DELETE /admin/script-variables` +- **New event types** — `user.permissions_updated`, `user.super_transferred`, `auth.permission_denied`, `api_key.created`, `api_key.revoked`, `script_variable.upserted`, `script_variable.deleted`, `hook.executed`, `hook.dead_lettered` + ## v1.9.0 — Event Logs - **Event logging** — system-wide audit trail for lexicon changes, record operations, Lua script executions/errors, admin actions, backfill jobs, and Tap connectivity diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -40,4 +40,10 @@ **Network lexicon** — A lexicon fetched directly from the AT Protocol network via DNS authority resolution, rather than uploaded manually. See [Lexicons - Network lexicons](../guides/lexicons.md#network-lexicons). **Tap** — A [firehose consumer and backfill worker](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) that handles real-time record streaming, cryptographic verification, and historical record fetching. HappyView connects to Tap via WebSocket to receive record events, and delegates backfill work to Tap via its HTTP API. +**Permission** — A granular access control right that authorizes a specific action in the admin API. HappyView defines 20 permissions organized by category (e.g. `lexicons:create`, `users:read`). See [Permissions](../guides/permissions.md). + +**Permission template** — A predefined set of permissions that can be applied when creating a user. Templates are: **Viewer** (read-only access), **Operator** (viewer + backfill and API key management), **Manager** (operator + lexicon and record management), and **Full Access** (all 20 permissions). + +**Super user** — The bootstrapped user created on first login to a fresh HappyView instance. The super user has unrestricted access to all endpoints regardless of permissions, can transfer super status to another user, and cannot be deleted. + **Target collection** — The record collection that a query or procedure lexicon operates on. Set via the `target_collection` field when uploading a lexicon. diff --git a/docs/reference/troubleshooting.md b/docs/reference/troubleshooting.md --- a/docs/reference/troubleshooting.md +++ b/docs/reference/troubleshooting.md @@ -40,8 +40,19 @@ **Symptom**: Admin API calls return `{"error": "forbidden"}`. **Causes**: -- Your DID is not in the admins table. Ask an existing admin to add you via `POST /admin/admins`. -- If this is a fresh deployment with no admins, the first authenticated request to any admin endpoint automatically bootstraps you as admin. Make sure you're sending a valid Bearer token. +- Your DID is not in the users table. Ask an existing user with `users:create` permission to add you via `POST /admin/users`. +- If this is a fresh deployment with no users, the first authenticated request to any admin endpoint automatically bootstraps you as the super user. Make sure you're sending a valid Bearer token. +- You may be in the users table but lack the required permission for the endpoint you're calling. Check your permissions with `GET /admin/users` or ask a user with `users:update` permission to grant the permission you need. + +## Permission denied errors + +**Symptom**: Admin API calls return `{"error": "insufficient permissions"}` with status 403, even though you can access other endpoints. + +**Causes**: + +- Your user account doesn't have the specific permission required by the endpoint. Each endpoint requires a specific permission — see the [permissions table](admin-api.md#permissions). +- If using an API key, the key's effective permissions are the intersection of the key's permissions and your user permissions. A key can never have more access than the user who created it. +- Only the super user can call `POST /admin/users/transfer-super`. This endpoint cannot be accessed with any permission — it requires super user status. ## Lua script errors -- tangled.sh