From 0d69e8fed4d2003914489a754a4c0f3edfcaca68 Mon Sep 17 00:00:00 2001 From: Dave Kempe Date: Sat, 18 Apr 2026 20:45:29 +1000 Subject: [PATCH] =?UTF-8?q?Rename=20Address=20Book=20=E2=86=92=20Connectio?= =?UTF-8?q?ns;=20allowed=5Fgroups=20picker;=20session=20privacy=20(#102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three pieces of v1.6.0 work that happened together and are easier to review as one save point. Rename: Address Book → Connections - static/addressbook.html renamed to static/connections.html - Nav links, page titles, empty states, onboarding, and prose updated across all 8 static pages (connections, admin, docs, index, recordings, reports, sessions, tokens). - README, CLAUDE.md, and every file under docs/ updated. - src/main.rs: connections.html added to the branded-page map and route list; /addressbook.html returns a 308 permanent redirect so existing bookmarks keep working. - Backend API paths, Rust types, and Vault storage paths are deliberately unchanged — internal only. Folder allowed_groups picker - New SQLite table `seen_groups` tracks OIDC groups observed in any user login; OIDC callback upserts after extracting groups. - `GET /api/auth/known-groups` (admin-only) returns the union of group_role_mappings and seen_groups. - `GET /api/addressbook/folders/{scope}/{folder}/config` adds the missing endpoint the frontend was already calling — existing allowed_groups now prefill the edit-folder modal. - Folder modal swaps the free-text comma-separated input for a chip picker with a themed combobox dropdown: autocomplete over known groups, keyboard nav, "+ add custom" row for unlisted groups. Active session visibility (GitHub #102) - `GET /api/sessions` scopes to the caller's own sessions by default; `?all=true` lets admins opt in (used by the Sessions page). - `GET /api/sessions/{id}` and the thumbnail GET/PUT endpoints are now owner-or-admin, returning 404 for other callers so session existence isn't leaked. - Connections' Active Sessions strip is now always owner-scoped — admins still manage everyone via the Sessions page. --- CLAUDE.md | 16 +- README.md | 14 +- docs/api.md | 18 +- docs/configuration.md | 2 +- docs/credential-variables.md | 10 +- docs/deployment-guide.md | 10 +- docs/installation.md | 8 +- docs/integrations.md | 24 +- docs/migration.md | 10 +- docs/netbox.md | 18 +- docs/overview.md | 12 +- docs/rdp-video-performance.md | 4 +- docs/reports.md | 4 +- docs/roles-and-access-control.md | 12 +- docs/security.md | 10 +- docs/vdi.md | 10 +- docs/web-sessions.md | 26 +- src/api.rs | 156 ++++++++- src/db.rs | 46 +++ src/main.rs | 16 +- src/oidc.rs | 8 + static/admin.html | 4 +- static/{addressbook.html => connections.html} | 326 ++++++++++++++++-- static/docs.html | 2 +- static/index.html | 6 +- static/recordings.html | 2 +- static/reports.html | 2 +- static/sessions.html | 9 +- static/tokens.html | 2 +- 29 files changed, 639 insertions(+), 148 deletions(-) rename static/{addressbook.html => connections.html} (92%) diff --git a/CLAUDE.md b/CLAUDE.md index eae6ed7..3097d5c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,10 +25,10 @@ rustguac is a lightweight Rust replacement for the Apache Guacamole Java webapp. - `src/config.rs` — TOML config loading with defaults - `src/auth.rs` — API key auth middleware (SHA-256, IP allowlists, expiry), role system - `src/oidc.rs` — OIDC authentication (login, callback, logout, group extraction) -- `src/vault.rs` — Vault/OpenBao KV v2 client for address book (AppRole auth, token renewal) +- `src/vault.rs` — Vault/OpenBao KV v2 client for connections (AppRole auth, token renewal) - `src/db.rs` — SQLite admin database (rusqlite, bundled) - `static/client.html` — Guacamole JS client with auto-scaling display -- `static/addressbook.html` — Vault-backed address book UI (folder/entry management, connect) +- `static/connections.html` — Vault-backed connections UI (folder/entry management, connect) - `static/recordings.html` — recording playback with auto-scaling - `static/sessions.html` — session management dashboard - `dev.sh` — development script (build guacd, run, deps) @@ -39,9 +39,9 @@ rustguac is a lightweight Rust replacement for the Apache Guacamole Java webapp. TOML config file (`config.local.toml` for dev, `--config` flag for production). Key settings: `listen_addr`, `guacd_addr`, `recording_path`, `static_path`, `db_path`, `xvnc_path`, `chromium_path`, `display_range_start/end`. -### Vault / Address Book +### Vault / Connections -Optional `[vault]` section enables the Vault-backed address book. Connection entries (SSH/RDP/Web) are stored in Vault KV v2 — credentials never touch disk or the browser. +Optional `[vault]` section enables the Vault-backed connections. Connection entries (SSH/RDP/Web) are stored in Vault KV v2 — credentials never touch disk or the browser. ```toml [vault] @@ -67,9 +67,9 @@ Optional `[oidc]` section enables OpenID Connect authentication. Key settings: ` ### Roles 4-tier role hierarchy: `admin` (4) > `poweruser` (3) > `operator` (2) > `viewer` (1). -- **admin**: full access, address book folder/entry management -- **poweruser**: ad-hoc session creation + address book connect -- **operator**: address book connect only (no ad-hoc sessions) +- **admin**: full access, connections folder/entry management +- **poweruser**: ad-hoc session creation + connections connect +- **operator**: connections connect only (no ad-hoc sessions) - **viewer**: read-only ## Deployment @@ -114,7 +114,7 @@ Ephemeral per-user Docker desktop containers. `VdiDriver` trait in `src/vdi/mod. - Credentials: auto-generated per session (username from OIDC, random hex password), `chpasswd` updates on reuse - BYO image: any Docker image with xrdp on port 3389 accepting `VDI_USERNAME`/`VDI_PASSWORD` env vars - Test image: `contrib/vdi-test-image/` (Debian trixie + xrdp + xorgxrdp + xfce4) -- Thumbnails: client captures display screenshot every 10s, shown in address book "Active Sessions" +- Thumbnails: client captures display screenshot every 10s, shown in connections "Active Sessions" - Config: `[vdi]` section — `enabled`, `docker_socket`, `default_cpu_limit`, `default_memory_limit`, `ready_timeout_secs`, `idle_timeout_mins`, `allowed_images`, `home_base` - Requires: `rustguac` user in `docker` group for socket access diff --git a/README.md b/README.md index 1250bb3..8a810b7 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ guacd (C, from guacamole-server) - **OIDC single sign-on** — Authentik, Google, Okta, Keycloak, or any OpenID Connect provider - **4-tier role system** — admin, poweruser, operator, viewer with OIDC group mapping - **API key auth** — SHA-256 hashed keys with IP allowlists and expiry -- **Vault-backed address book** — credentials in HashiCorp Vault / OpenBao KV v2, never reach the browser +- **Vault-backed connections** — credentials in HashiCorp Vault / OpenBao KV v2, never reach the browser - **TLS everywhere** — HTTPS for clients, TLS between rustguac and guacd - **CIDR allowlists** — per-protocol network restrictions for session targets - **Per-entry clipboard control** — disable copy and/or paste for data loss prevention @@ -58,21 +58,21 @@ guacd (C, from guacamole-server) - **Multi-hop SSH tunnels** — chain jump hosts/bastions to reach isolated networks (all session types) - **Session sharing** — share tokens for read-only or collaborative access - **Encrypted file transfer** — LUKS-encrypted per-session drive storage (RDP), SFTP (SSH) -- **Credential variables** — shared credentials across address book entries +- **Credential variables** — shared credentials across connections entries ### VDI desktop containers - **Docker-based** — one container per user, deterministic naming, BYO image - **Persist after disconnect** — reconnect to the same desktop within idle timeout - **Logout detection** — desktop logout stops the container, tab close preserves it -- **Session thumbnails** — live preview in the address book, click to reconnect +- **Session thumbnails** — live preview in the connections, click to reconnect - **Persistent home directories** — bind-mounted user data survives container restarts -- **Per-entry resource limits** — CPU, memory, idle timeout per address book entry +- **Per-entry resource limits** — CPU, memory, idle timeout per connections entry - **VdiDriver trait** — extensible for downstream forks (Nomad, Proxmox, cloud) ### UI -- **Address book** with folder-based organisation and OIDC group access control +- **Connections** with folder-based organisation and OIDC group access control - **Active Sessions** section with live thumbnail previews - **Session ended overlay** with Reconnect/Close buttons - **8 built-in themes** with CSS gradient backgrounds, or configure your own @@ -126,7 +126,7 @@ sudo usermod -aG docker rustguac sudo systemctl restart rustguac ``` -Add `[vdi]` to your config and create a VDI entry in the address book. See [VDI Desktop Containers](docs/vdi.md) for image requirements and configuration. +Add `[vdi]` to your config and create a VDI entry in the connections. See [VDI Desktop Containers](docs/vdi.md) for image requirements and configuration. ## Documentation @@ -145,7 +145,7 @@ Add `[vdi]` to your config and create a VDI entry in the address book. See [VDI ### Integration & reference - [Integrations](docs/integrations.md) — Vault, LUKS drives, SSH tunnels, Kerberos, HAProxy, Knocknoc -- [NetBox](docs/netbox.md) — address book sync via custom fields and webhooks +- [NetBox](docs/netbox.md) — connections sync via custom fields and webhooks - [Security](docs/security.md) — TLS, rate limiting, headers, audit logging, hardening - [API Reference](docs/api.md) — REST API endpoints - [Migration from Apache Guacamole](docs/migration.md) — MySQL/MariaDB to Vault diff --git a/docs/api.md b/docs/api.md index e005653..6554529 100644 --- a/docs/api.md +++ b/docs/api.md @@ -18,7 +18,7 @@ Quick-connect endpoint for external integrations (e.g., NetBox Custom Links). Cr /api/connect?hostname=10.0.1.50&protocol=ssh -**Address book mode** (operator+): +**Connections mode** (operator+): /api/connect?scope=shared&folder=production&entry=web-server-01 @@ -29,16 +29,16 @@ Quick-connect endpoint for external integrations (e.g., NetBox Custom Links). Cr | `port` | integer | Target port (uses protocol default if omitted) | | `username` | string | Username (optional) | | `url` | string | Target URL (web sessions) | -| `scope` | string | Address book scope: `shared` or `instance` | -| `folder` | string | Address book folder name | -| `entry` | string | Address book entry name | +| `scope` | string | Connections scope: `shared` or `instance` | +| `folder` | string | Connections folder name | +| `entry` | string | Connections entry name | | `width` | integer | Display width in pixels | | `height` | integer | Display height in pixels | | `dpi` | integer | Display DPI | -When `scope`, `folder`, and `entry` are all provided, the endpoint connects via the address book (credentials from Vault). Otherwise it creates an ad-hoc session. No credentials are passed in the URL for ad-hoc mode — if the target requires authentication, the user will see guacd's login prompt. +When `scope`, `folder`, and `entry` are all provided, the endpoint connects via the connections (credentials from Vault). Otherwise it creates an ad-hoc session. No credentials are passed in the URL for ad-hoc mode — if the target requires authentication, the user will see guacd's login prompt. -If the address book entry has `prompt_credentials: true` or has no stored password/key, the endpoint returns an inline credential form instead of creating the session immediately. The user enters credentials, which are POSTed to the connect endpoint and used for that session only (never stored). +If the connections entry has `prompt_credentials: true` or has no stored password/key, the endpoint returns an inline credential form instead of creating the session immediately. The user enters credentials, which are POSTed to the connect endpoint and used for that session only (never stored). See [NetBox Integration](netbox.md) for usage with NetBox Custom Links. @@ -348,7 +348,7 @@ Update a mapping. Delete a mapping. -## Address Book (requires Vault) +## Connections (requires Vault) ### `GET /api/addressbook/folders` @@ -360,7 +360,7 @@ List entries in a folder. Scope is `shared` or `instance`. Requires folder group ### `POST /api/addressbook/folders/:scope/:folder/entries/:entry/connect` -Create a session from an address book entry. Reads credentials (including jump host credentials) from Vault server-side and creates a session. Requires **operator** role and folder group access. +Create a session from an connections entry. Reads credentials (including jump host credentials) from Vault server-side and creates a session. Requires **operator** role and folder group access. Optional body to override or supply credentials at connect time: @@ -422,7 +422,7 @@ Create a connection entry. The body includes a `name` field plus all entry field } ``` -**Address book entry fields:** +**Connections entry fields:** | Field | Type | Used by | Description | |-------|------|---------|-------------| diff --git a/docs/configuration.md b/docs/configuration.md index 56c3420..62340a6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -119,7 +119,7 @@ Enables OpenID Connect authentication. When configured, the web UI shows a login ## `[vault]` section -Enables the Vault-backed address book. Requires `VAULT_SECRET_ID` environment variable. +Enables the Vault-backed connections. Requires `VAULT_SECRET_ID` environment variable. | Key | Default | Description | |-----|---------|-------------| diff --git a/docs/credential-variables.md b/docs/credential-variables.md index 1ce82c3..f1a2b48 100644 --- a/docs/credential-variables.md +++ b/docs/credential-variables.md @@ -1,12 +1,12 @@ # Credential Variables -Credential variables let address book entries reference shared credentials by name instead of storing passwords directly. Users maintain their own credential values in Vault via the **My Credentials** dialog (gear menu). When a session launches, rustguac substitutes the variables from the user's saved values. +Credential variables let connections entries reference shared credentials by name instead of storing passwords directly. Users maintain their own credential values in Vault via the **My Credentials** dialog (gear menu). When a session launches, rustguac substitutes the variables from the user's saved values. This gives a similar experience to LDAP credential passthrough in Apache Guacamole — users log in once and sessions just work — without rustguac needing to bind to LDAP. Credentials stay in Vault, never on disk or in the browser. ## How it works -1. **Admin** creates address book entries with variable references like `$corp_username` and `$corp_password` in the credential fields +1. **Admin** creates connections entries with variable references like `$corp_username` and `$corp_password` in the credential fields 2. **Users** open **My Credentials** from the gear menu and fill in their values (stored per-user in Vault) 3. **At connect time**, rustguac substitutes the variables. If all are set, the session launches silently. If any are missing, the user is prompted. @@ -27,7 +27,7 @@ The `` is a logical name chosen by the admin to group related credential ## Example -An admin creates two address book entries: +An admin creates two connections entries: - **Production SSH** — username: `$corp_username`, password: `$corp_password` - **Staging SSH** — username: `$corp_username`, password: `$corp_password` @@ -38,7 +38,7 @@ An entry can also mix variables with static values. For example, an RDP entry mi ## My Credentials dialog -Access via the gear icon in the top-right corner of the address book page. The dialog: +Access via the gear icon in the top-right corner of the connections page. The dialog: - Shows all credential variables used across entries the user has access to - Groups variables by domain prefix @@ -64,7 +64,7 @@ Each user gets a single Vault secret containing all their credential key-value p ### Required Vault policy -In addition to the existing address book policy, add: +In addition to the existing connections policy, add: ```hcl # User credential variables (read/write own credentials) diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index 8c49dfa..e4b892b 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -11,7 +11,7 @@ Internet | [HAProxy] ── TLS termination, rate limiting, Knocknoc ACL | -[rustguac] ── session management, WebSocket proxy, address book +[rustguac] ── session management, WebSocket proxy, connections | [guacd] ── protocol translation (SSH, RDP, VNC) | @@ -181,7 +181,7 @@ The script runs in three phases: Run `bash setup-xrdp-gfx.sh --help` for all options, or `bash setup-xrdp-gfx.sh --diagnose` to troubleshoot after setup. -In the rustguac address book, enable these settings on the RDP entry: +In the rustguac connections, enable these settings on the RDP entry: - **Enable Graphics Pipeline (GFX)** -- checked - **H.264 Passthrough** -- checked - **Enable Desktop Composition** -- not needed for Linux (Windows-only DWM setting) @@ -247,9 +247,9 @@ Once OIDC is working and you have an admin user, remove the initial API key: API keys are powerful (full admin, no MFA). For day-to-day use, OIDC with group-based roles is more secure. If you need programmatic API access, create scoped [user API tokens](roles-and-access-control.md) instead. -## Step 6: Set Up the Address Book (Vault) +## Step 6: Set Up the Connections (Vault) -The address book stores connection entries in HashiCorp Vault or OpenBao. Credentials stay server-side — they never reach the browser. +The connections stores connection entries in HashiCorp Vault or OpenBao. Credentials stay server-side — they never reach the browser. ```toml [vault] @@ -365,7 +365,7 @@ Back up these paths: - `/opt/rustguac/env` — secrets (Vault secret ID, OIDC client secret) - `/opt/rustguac/recordings/` — session recordings (if needed for compliance) -The address book is in Vault — back up Vault separately. +The connections is in Vault — back up Vault separately. ### Security checklist diff --git a/docs/installation.md b/docs/installation.md index 7553d58..46fa9e2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -32,13 +32,13 @@ sudo systemctl enable --now rustguac This starts both `rustguac-guacd` (the protocol daemon) and `rustguac` (the web proxy). -4. **(Recommended) Set up the address book with Vault / OpenBao:** +4. **(Recommended) Set up the connections with Vault / OpenBao:** -The address book is rustguac's primary way to manage connections. It stores SSH, RDP, VNC, and web session entries in [HashiCorp Vault](https://www.vaultproject.io/) or [OpenBao](https://openbao.org/) KV v2. Credentials are stored server-side and never sent to the browser. +The connections is rustguac's primary way to manage connections. It stores SSH, RDP, VNC, and web session entries in [HashiCorp Vault](https://www.vaultproject.io/) or [OpenBao](https://openbao.org/) KV v2. Credentials are stored server-side and never sent to the browser. -Without Vault, rustguac can still create ad-hoc sessions via the API, but the address book UI (the main user-facing feature) will not be available. +Without Vault, rustguac can still create ad-hoc sessions via the API, but the connections UI (the main user-facing feature) will not be available. -See [Vault / OpenBao Address Book](integrations.md#vault--openbao-address-book) for full setup instructions, including Vault policy, AppRole configuration, and the `[vault]` config section. +See [Vault / OpenBao Connections](integrations.md#vault--openbao-address-book) for full setup instructions, including Vault policy, AppRole configuration, and the `[vault]` config section. 6. **(Optional) Set up encrypted drive storage:** diff --git a/docs/integrations.md b/docs/integrations.md index b1b65a9..7062ce8 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -37,7 +37,7 @@ chmod 600 /opt/rustguac/env rustguac extracts group memberships from the OIDC ID token. The claim name is configurable (default: `groups`). Groups are used for: - **Automatic role assignment** via group-to-role mappings (see [Roles and Access Control](roles-and-access-control.md)) -- **Address book folder access** — folders can be restricted to specific OIDC groups +- **Connections folder access** — folders can be restricted to specific OIDC groups If your provider requires additional scopes to include groups in the token, add them to `extra_scopes`: @@ -112,9 +112,9 @@ Create groups in Authentik (e.g., `rustguac-admins`, `rustguac-operators`) and a --- -## Vault / OpenBao Address Book +## Vault / OpenBao Connections -The address book stores connection entries in [HashiCorp Vault](https://www.vaultproject.io/) or [OpenBao](https://openbao.org/) KV v2. Credentials are read server-side and never sent to the browser. +The connections stores connection entries in [HashiCorp Vault](https://www.vaultproject.io/) or [OpenBao](https://openbao.org/) KV v2. Credentials are read server-side and never sent to the browser. ### Vault setup @@ -128,13 +128,13 @@ vault secrets enable -path=secret kv-v2 ```bash vault policy write rustguac - <<'EOF' -# Address book entries: create, read, update, soft-delete +# Connections entries: create, read, update, soft-delete path "secret/data/rustguac/*" { capabilities = ["create", "read", "update", "delete"] } # Folder/entry listing and permanent deletion -# - "list" + "read": browse the address book +# - "list" + "read": browse the connections # - "delete": permanently remove entries and folders # (KV v2 permanent deletes go through the metadata/ path, not data/) path "secret/metadata/rustguac/*" { @@ -239,7 +239,7 @@ This allows a fleet of rustguac instances to share common entries while maintain ### Entry types -Address book entries can be SSH, RDP, VNC, or Web connections. Each entry stores: +Connections entries can be SSH, RDP, VNC, or Web connections. Each entry stores: - Connection type and target (hostname, port, URL) - Credentials (username, password, private key) @@ -259,7 +259,7 @@ Folder and entry names are validated: alphanumeric characters, hyphens, undersco ### Credential prompting -Address book entries can be configured to prompt users for credentials at connect time. This is useful for: +Connections entries can be configured to prompt users for credentials at connect time. This is useful for: - **Entries without stored credentials** — e.g., RDP servers where each user has their own AD account. The admin creates the entry with just hostname/port, and users supply their own credentials when connecting. - **Entries with stored credentials but prompt enabled** — e.g., a jump host where the stored credentials are a fallback, but users should normally use their own. @@ -294,9 +294,9 @@ The chain is set up sequentially (each hop must connect before the next starts) ### Configuration -#### Address book entries +#### Connections entries -Admins configure jump hosts per entry in the address book editor. Click "Add Jump Host" to add hops to the chain. Each hop has its own credentials (username + password or private key). A visual flow diagram shows the tunnel path. +Admins configure jump hosts per entry in the connections editor. Click "Add Jump Host" to add hops to the chain. Each hop has its own credentials (username + password or private key). A visual flow diagram shows the tunnel path. Jump host credentials are stored in Vault alongside the entry's other credentials and are never sent to the browser. When editing an entry, existing hop passwords and keys are preserved if the edit form omits them (per-hop credential merge by index). @@ -365,7 +365,7 @@ FreeRDP's WinPR layer implements Windows SSPI on top of the system's MIT Kerbero ### Per-entry configuration -These settings are configured per address book entry in the admin UI: +These settings are configured per connections entry in the admin UI: | Setting | Values | Description | |---------|--------|-------------| @@ -423,7 +423,7 @@ FreeRDP/libkrb5 finds the KDC in this order of priority: **Option 1: KDC Proxy URL (simplest for remote networks)** -Set the **KDC URL** field on the address book entry to your KDC Proxy endpoint (e.g., `https://dc.example.com/KdcProxy`). This bypasses DNS SRV and krb5.conf entirely. Windows Server's KDC Proxy Service can serve this role. +Set the **KDC URL** field on the connections entry to your KDC Proxy endpoint (e.g., `https://dc.example.com/KdcProxy`). This bypasses DNS SRV and krb5.conf entirely. Windows Server's KDC Proxy Service can serve this role. **Option 2: DNS SRV records (simplest for on-network)** @@ -469,7 +469,7 @@ The **Domain** field should be the AD domain name (e.g., `EXAMPLE.COM`). ### Example: Kerberos RDP entry -Create an address book entry with: +Create an connections entry with: - **Type**: RDP - **Hostname**: `fileserver.corp.example.com` (must be FQDN) diff --git a/docs/migration.md b/docs/migration.md index ea6c103..c7bd349 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1,6 +1,6 @@ # Migrating from Apache Guacamole -rustguac can import connections from an Apache Guacamole MySQL/MariaDB database into its Vault-backed address book. +rustguac can import connections from an Apache Guacamole MySQL/MariaDB database into its Vault-backed connections. ## Prerequisites @@ -67,15 +67,15 @@ rustguac --config /opt/rustguac/config.toml \ | Flag | Default | Description | |------|---------|-------------| | `--file` | (required) | Path to the mysqldump SQL file | -| `--folder` | `imported` | Target folder in the address book | +| `--folder` | `imported` | Target folder in the connections | | `--scope` | `shared` | `shared` (visible to all instances) or `instance` (this instance only) | | `--dry-run` | off | Preview without writing to Vault | ## What gets imported -The importer maps Guacamole connection parameters to rustguac address book fields: +The importer maps Guacamole connection parameters to rustguac connections fields: -| Guacamole parameter | Address book field | +| Guacamole parameter | Connections field | |--------------------|--------------------| | `hostname` | `hostname` | | `port` | `port` | @@ -113,7 +113,7 @@ Guacamole's connection group hierarchy is flattened into entry name prefixes. Fo ## After import -Once imported, connections appear in the address book UI. You can: +Once imported, connections appear in the connections UI. You can: - Edit entries to add features not available in Guacamole (login scripts, autofill, domain allowlists) - Move entries between folders diff --git a/docs/netbox.md b/docs/netbox.md index b9e1d61..6c8408b 100644 --- a/docs/netbox.md +++ b/docs/netbox.md @@ -11,20 +11,20 @@ Go to **Customization > Custom Fields** and create the following fields. Assign | Name | Type | Default | Description | |------|------|---------|-------------| | `console_enabled` | Boolean | false | Opt-in: enables remote console links on the device page | -| `console_mode` | Selection: `addressbook`, `adhoc` | — | How to connect: via address book entry (Vault credentials) or ad-hoc (direct to IP) | -| `remote_protocol` | Selection: `ssh`, `rdp`, `vnc`, `web` | — | Protocol for ad-hoc connections (address book entries have their own) | +| `console_mode` | Selection: `addressbook`, `adhoc` | — | How to connect: via connections entry (Vault credentials) or ad-hoc (direct to IP) | +| `remote_protocol` | Selection: `ssh`, `rdp`, `vnc`, `web` | — | Protocol for ad-hoc connections (connections entries have their own) | | `remote_port` | Integer | — | Port override for ad-hoc connections (leave blank for protocol default) | The `console_enabled` field is the master switch — no links appear until it's checked. The `console_mode` field controls which link is shown: -- **`addressbook`** — connects via a Vault address book entry. Credentials are managed in Vault and never appear in the URL. Requires a matching entry name (lowercase device name). Minimum role: **operator**. +- **`addressbook`** — connects via a Vault connections entry. Credentials are managed in Vault and never appear in the URL. Requires a matching entry name (lowercase device name). Minimum role: **operator**. - **`adhoc`** — connects directly to the device's primary IP. No stored credentials — the user sees guacd's login prompt. Minimum role: **poweruser**. ## Custom Links Create **two** Custom Links in **Customization > Custom Links**. Each link only renders when `console_mode` matches its mode, so only one appears per device. -### Link 1: Address Book Console (green) +### Link 1: Connections Console (green) | Setting | Value | |---------|-------| @@ -43,7 +43,7 @@ Create **two** Custom Links in **Customization > Custom Links**. Each link only https://console.example.com/api/connect?scope=shared&folder=production&entry={{ object.name | lower }} ``` -Replace `production` with your address book folder name. The entry name must match the lowercase device name in Vault. +Replace `production` with your connections folder name. The entry name must match the lowercase device name in Vault. ### Link 2: Ad-hoc SSH (blue outline) @@ -78,12 +78,12 @@ Use NetBox's **bulk edit** to enable across multiple devices at once. | Mode | Minimum role | Description | |------|-------------|-------------| -| Address book | operator | Connects via Vault entry (credentials from Vault) | +| Connections | operator | Connects via Vault entry (credentials from Vault) | | Ad-hoc | poweruser | Creates session directly to hostname | -## Webhook-Driven Address Book Sync +## Webhook-Driven Connections Sync -Automatically sync NetBox devices to rustguac's Vault-backed address book using Event Rules and Webhooks. This keeps address book entries in sync with NetBox — when a device is created or updated, the corresponding entry is created in Vault. +Automatically sync NetBox devices to rustguac's Vault-backed connections using Event Rules and Webhooks. This keeps connections entries in sync with NetBox — when a device is created or updated, the corresponding entry is created in Vault. ### Filtering: control what syncs @@ -205,5 +205,5 @@ Both NetBox and rustguac support OIDC authentication. When configured with the s 3. Create the four custom fields (`console_enabled`, `console_mode`, `remote_protocol`, `remote_port`) 4. Create the two Custom Links (Console green, Quick SSH blue) 5. On devices you want to enable: check `console_enabled`, set `console_mode` -6. For address book mode: ensure matching entries exist in Vault (manually or via webhook sync) +6. For connections mode: ensure matching entries exist in Vault (manually or via webhook sync) 7. Users click the button on a device page and land in a session diff --git a/docs/overview.md b/docs/overview.md index da132d3..63614af 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -4,7 +4,7 @@ rustguac is a lightweight Rust replacement for the Apache Guacamole Java webapp. It provides browser-based remote access to SSH, RDP, VNC, web browser sessions, and VDI desktop containers through [guacd](https://github.com/apache/guacamole-server), the Guacamole protocol daemon. -rustguac sits between web browsers and guacd, proxying the Guacamole protocol over WebSockets. It manages session lifecycle, authentication, session recording, VDI container orchestration, and an optional Vault-backed address book. +rustguac sits between web browsers and guacd, proxying the Guacamole protocol over WebSockets. It manages session lifecycle, authentication, session recording, VDI container orchestration, and an optional Vault-backed connections. ## Why not Apache Guacamole? @@ -14,7 +14,7 @@ Apache Guacamole is a mature, feature-rich platform. rustguac is a purpose-built - **Security-first design** — CIDR allowlists, TLS everywhere, LUKS-encrypted file transfer, Vault integration, rate limiting, audit logging. - **Simpler deployment** — one binary + guacd. Install with a single script or Docker image. - **VDI desktops** — ephemeral Docker containers give each user an isolated Linux desktop on demand. No VM infrastructure required. -- **Address book in Vault** — connection credentials stored in HashiCorp Vault / OpenBao KV v2. Credentials never reach the browser. +- **Connections in Vault** — connection credentials stored in HashiCorp Vault / OpenBao KV v2. Credentials never reach the browser. - **Zero-trust integration** — works with [Knocknoc](https://knocknoc.io) for identity-aware network access control at the HAProxy layer. ## Similarities to Apache Guacamole @@ -113,7 +113,7 @@ Supports optional [multi-hop SSH tunnel chains](#ssh-tunnel--jump-hosts) to reac Spawns an ephemeral Docker container running xrdp and a Linux desktop, then connects guacd via RDP to the container. Each user gets a dedicated container named `rustguac-vdi-{username}`. Containers persist after disconnect for reconnection and are automatically cleaned up after an idle timeout. -VDI sessions support persistent home directories, per-entry resource limits and idle timeouts, session thumbnails, and active session previews in the address book. See [VDI Desktop Containers](vdi.md) for configuration and image requirements. +VDI sessions support persistent home directories, per-entry resource limits and idle timeouts, session thumbnails, and active session previews in the connections. See [VDI Desktop Containers](vdi.md) for configuration and image requirements. ## SSH tunnel / jump hosts @@ -126,10 +126,10 @@ You -> bastion-1:22 -> bastion-2:22 -> target:3389 RDP ``` Jump hosts can be configured: -- **Per address book entry** — admins configure the tunnel chain in the entry editor +- **Per connections entry** — admins configure the tunnel chain in the entry editor - **Per ad-hoc session** — powerusers add jump hosts when creating sessions from the Sessions page -Each hop supports independent credentials (username + password or private key). Jump host credentials are stored in Vault alongside the address book entry and are never sent to the browser. +Each hop supports independent credentials (username + password or private key). Jump host credentials are stored in Vault alongside the connections entry and are never sent to the browser. ## Ports @@ -186,7 +186,7 @@ scripts/ Utility scripts (drive-setup.sh) ### Integrations - [Integrations](integrations.md) -- OIDC, Vault, SSH tunnels, Kerberos, HAProxy, Knocknoc, drive/LUKS -- [NetBox](netbox.md) -- address book sync via custom fields and webhooks +- [NetBox](netbox.md) -- connections sync via custom fields and webhooks - [Migration from Apache Guacamole](migration.md) -- MySQL/MariaDB to Vault ### Reference diff --git a/docs/rdp-video-performance.md b/docs/rdp-video-performance.md index a11b5e0..9f65238 100644 --- a/docs/rdp-video-performance.md +++ b/docs/rdp-video-performance.md @@ -2,7 +2,7 @@ Guide to optimizing RDP video quality and frame rate through rustguac, particularly for video monitoring workloads. -## Address Book Settings +## Connections Settings Three per-entry settings control RDP video behaviour: @@ -12,7 +12,7 @@ Three per-entry settings control RDP video behaviour: - **Force Lossless** — Forces PNG-only encoding (no JPEG/WebP lossy compression). Better for text-heavy workloads where visual fidelity matters. Uses significantly more bandwidth — not recommended for video content. -These settings appear in the address book entry editor for RDP entries under "Video Performance". +These settings appear in the connections entry editor for RDP entries under "Video Performance". ## Windows RDP Server Tuning diff --git a/docs/reports.md b/docs/reports.md index 00fe71b..a062aed 100644 --- a/docs/reports.md +++ b/docs/reports.md @@ -13,7 +13,7 @@ The top of the reports page shows four summary metrics: ## Session history -A searchable, sortable table of all past and current sessions. Each row includes the user who created the session, the address book entry and folder (if applicable), session type (SSH, RDP, VNC, Web), hostname, start time, duration, status, and a link to the recording if one exists. +A searchable, sortable table of all past and current sessions. Each row includes the user who created the session, the connections entry and folder (if applicable), session type (SSH, RDP, VNC, Web), hostname, start time, duration, status, and a link to the recording if one exists. The table supports: @@ -63,7 +63,7 @@ All endpoints require authentication (API key, user token, or OIDC session cooki | Parameter | Description | |-----------|-------------| | `user` | Filter by username (partial match) | -| `entry` | Filter by address book entry name (partial match) | +| `entry` | Filter by connections entry name (partial match) | | `type` | Filter by session type: `ssh`, `rdp`, `vnc`, `web` | | `from` | Start date filter (ISO 8601, e.g. `2025-01-01T00:00:00Z`) | | `to` | End date filter (ISO 8601) | diff --git a/docs/roles-and-access-control.md b/docs/roles-and-access-control.md index d0d68b6..61353fd 100644 --- a/docs/roles-and-access-control.md +++ b/docs/roles-and-access-control.md @@ -6,9 +6,9 @@ rustguac implements a 4-tier role hierarchy: | Role | Level | Description | |------|-------|-------------| -| **admin** | 4 | Full access — manage users, address book, recordings, sessions, group mappings, all API tokens | -| **poweruser** | 3 | Ad-hoc session creation + address book connect + self-service API tokens | -| **operator** | 2 | Address book connect only (no ad-hoc sessions); can view own API tokens | +| **admin** | 4 | Full access — manage users, connections, recordings, sessions, group mappings, all API tokens | +| **poweruser** | 3 | Ad-hoc session creation + connections connect + self-service API tokens | +| **operator** | 2 | Connections connect only (no ad-hoc sessions); can view own API tokens | | **viewer** | 1 | Read-only — view sessions and recordings; no API token access | Roles are hierarchical: each role includes all permissions of lower roles. For example, a poweruser can do everything an operator can, plus create ad-hoc sessions. @@ -54,13 +54,13 @@ OIDC users are assigned a role through three mechanisms (in order of precedence) | `GET /api/sessions/:id` | operator | View session details | | `DELETE /api/sessions/:id` | operator | Non-admins can only delete their own sessions | -### Address book +### Connections | Endpoint | Required role | Notes | |----------|--------------|-------| | `GET /api/addressbook/folders` | operator | Filtered by OIDC group membership | | `GET /api/addressbook/folders/:scope/:folder/entries` | operator | Requires folder group access | -| `POST .../entries/:entry/connect` | operator | Creates session from address book entry | +| `POST .../entries/:entry/connect` | operator | Creates session from connections entry | | `POST /api/addressbook/folders` | admin | Create folders | | `PUT /api/addressbook/folders/:scope/:folder` | admin | Update folder config | | `DELETE /api/addressbook/folders/:scope/:folder` | admin | Delete folders | @@ -125,7 +125,7 @@ Operators can view their tokens (created by an admin on their behalf) but cannot ## Folder access control -Address book folders have group-based access control. Each folder has an `allowed_groups` list stored in its `.config` entry in Vault. +Connections folders have group-based access control. Each folder has an `allowed_groups` list stored in its `.config` entry in Vault. - **Admins** bypass group checks and see all folders - **Operators and powerusers** see only folders where their OIDC groups intersect with the folder's `allowed_groups` diff --git a/docs/security.md b/docs/security.md index 5555748..2d26ffc 100644 --- a/docs/security.md +++ b/docs/security.md @@ -177,7 +177,7 @@ Additionally, user API token operations are logged to a persistent `token_audit_ ## Clipboard control -Clipboard copy (server → client) and paste (client → server) can be independently disabled per address book entry. This uses guacd's native `disable-copy` and `disable-paste` parameters, which work for all session types (SSH, RDP, VNC, and web browser sessions). +Clipboard copy (server → client) and paste (client → server) can be independently disabled per connections entry. This uses guacd's native `disable-copy` and `disable-paste` parameters, which work for all session types (SSH, RDP, VNC, and web browser sessions). Use cases: - **Disable copy** — prevents users from copying data out of sensitive sessions (data loss prevention) @@ -210,7 +210,7 @@ Policies applied: ### Per-entry domain allowlisting -Address book entries can specify an `allowed_domains` list. When set, Chromium can only reach those domains (plus localhost). All other domains are blocked via Chromium's `--host-rules` flag, which prevents DNS resolution for non-allowed hosts. +Connections entries can specify an `allowed_domains` list. When set, Chromium can only reach those domains (plus localhost). All other domains are blocked via Chromium's `--host-rules` flag, which prevents DNS resolution for non-allowed hosts. Subdomains are automatically included — adding `example.com` allows `*.example.com` as well. @@ -243,7 +243,7 @@ All database queries use parameterised statements via rusqlite's `params!` macro ## Path traversal protection -Recording file access validates filenames to block path traversal (`/`, `\`, `..`). The Vault address book also validates entry and folder names (alphanumeric, hyphens, underscores, dots only; length 1-64). +Recording file access validates filenames to block path traversal (`/`, `\`, `..`). The Vault connections also validates entry and folder names (alphanumeric, hyphens, underscores, dots only; length 1-64). ## XSS protection @@ -263,8 +263,8 @@ trusted_proxies = ["127.0.0.1/32"] ## Credential handling -- **Vault credentials** — address book entries are read server-side from Vault. Connection passwords and private keys are never sent to the browser. -- **SSH tunnel credentials** — jump host passwords and private keys are stored in Vault alongside the address book entry. They are read server-side when establishing the tunnel chain and are never sent to the browser. For ad-hoc sessions, jump host credentials are provided in the session creation request and exist only in memory during tunnel setup. +- **Vault credentials** — connections entries are read server-side from Vault. Connection passwords and private keys are never sent to the browser. +- **SSH tunnel credentials** — jump host passwords and private keys are stored in Vault alongside the connections entry. They are read server-side when establishing the tunnel chain and are never sent to the browser. For ad-hoc sessions, jump host credentials are provided in the session creation request and exist only in memory during tunnel setup. - **API keys** — only the SHA-256 hash is stored. The plaintext key is shown once at creation and cannot be retrieved. - **User API tokens** — same SHA-256 hash storage as admin API keys. The `rgu_` prefix enables secret scanning. Plaintext shown once at creation only. - **OIDC client secret** — can be provided via `OIDC_CLIENT_SECRET` environment variable instead of the config file. diff --git a/docs/vdi.md b/docs/vdi.md index e4dd972..ee1e2a0 100644 --- a/docs/vdi.md +++ b/docs/vdi.md @@ -4,7 +4,7 @@ rustguac can spawn ephemeral Docker desktop containers on demand. Each user gets ## How it works -1. An admin creates a VDI entry in the address book, specifying a Docker image +1. An admin creates a VDI entry in the connections, specifying a Docker image 2. When a user clicks Connect, rustguac creates a Docker container from that image 3. The container runs xrdp on port 3389, and guacd connects to it via RDP 4. The user sees a full Linux desktop in their browser @@ -87,9 +87,9 @@ xrdp-sesman --nodaemon & exec xrdp --nodaemon ``` -## Address book setup +## Connections setup -1. Create a folder in the address book (or use an existing one) +1. Create a folder in the connections (or use an existing one) 2. Add a new entry with type **VDI (Docker)** 3. Set the **Container Image** (e.g. `rustguac-vdi-test:latest`) 4. Optionally set CPU limit, memory limit, environment variables, idle timeout @@ -121,13 +121,13 @@ Each user gets `{home_base}/{username}` mounted as `/home/{username}` inside the ## Active Sessions -The address book shows an **Active Sessions** section with thumbnail previews of running sessions. Thumbnails are captured every 10 seconds from the browser display. Click a thumbnail to reconnect. +The connections shows an **Active Sessions** section with thumbnail previews of running sessions. Thumbnails are captured every 10 seconds from the browser display. Click a thumbnail to reconnect. Dormant VDI containers (running but no active browser session) also appear with their last captured thumbnail. ## Per-entry settings -Each VDI address book entry can override: +Each VDI connections entry can override: - **CPU limit** (cores) — overrides `default_cpu_limit` - **Memory limit** (MB) — overrides `default_memory_limit` diff --git a/docs/web-sessions.md b/docs/web-sessions.md index 1abaf0b..d47c33e 100644 --- a/docs/web-sessions.md +++ b/docs/web-sessions.md @@ -41,9 +41,9 @@ Xvnc (virtual display :100–:199) ## Quick start -### Address book entry +### Connections entry -Create a web entry in the address book with at minimum: +Create a web entry in the connections with at minimum: | Field | Value | |-------|-------| @@ -89,7 +89,7 @@ When the user clicks on a login form, Chromium shows its familiar autofill dropd ### Configuring autofill -The `autofill` field on an address book entry is a JSON string containing an array of credential objects: +The `autofill` field on an connections entry is a JSON string containing an array of credential objects: ```json [ @@ -142,15 +142,15 @@ This is Chromium's own obfuscation layer for the headless Linux case (no keyring ### UI -In the address book entry editor, the **Autofill** section provides a visual builder. Click **"Add site"** to add credential rows. The URL field auto-populates with the entry's target URL. Save the entry and the UI serialises the rows to JSON. +In the connections entry editor, the **Autofill** section provides a visual builder. Click **"Add site"** to add credential rows. The URL field auto-populates with the entry's target URL. Save the entry and the UI serialises the rows to JSON. ## Domain allowlisting -Each address book entry can specify an `allowed_domains` list to restrict which websites the browser can reach. This is enforced inside Chromium via the `--host-rules` flag, which blocks DNS resolution for non-allowed domains. +Each connections entry can specify an `allowed_domains` list to restrict which websites the browser can reach. This is enforced inside Chromium via the `--host-rules` flag, which blocks DNS resolution for non-allowed domains. ### Configuring allowed domains -In the address book entry editor, expand the **Allowed Domains** section and add domain names: +In the connections entry editor, expand the **Allowed Domains** section and add domain names: ``` example.com @@ -166,14 +166,14 @@ There are two separate mechanisms that control what a web session can access: | Layer | Config | Applied | Scope | |-------|--------|---------|-------| | **`web_allowed_networks`** | `config.toml` (global) | Server-side, at session creation | CIDR ranges — controls which IPs rustguac will connect to | -| **`allowed_domains`** | Address book entry | Client-side, inside Chromium at runtime | Domain names — controls which sites the user can navigate to | +| **`allowed_domains`** | Connections entry | Client-side, inside Chromium at runtime | Domain names — controls which sites the user can navigate to | They don't conflict — both can be active simultaneously for defense in depth: - `web_allowed_networks` prevents rustguac from initiating connections to disallowed networks (SSRF protection) - `allowed_domains` prevents the user from navigating to sites outside the allowlist within an already-running session -**Example:** Your config allows `10.0.0.0/8` for web sessions (server-side). An address book entry for the internal wiki sets `allowed_domains: ["wiki.internal.example.com"]`. The session can only reach the wiki — even though the server-side allowlist permits the entire `10.0.0.0/8` range. +**Example:** Your config allows `10.0.0.0/8` for web sessions (server-side). An connections entry for the internal wiki sets `allowed_domains: ["wiki.internal.example.com"]`. The session can only reach the wiki — even though the server-side allowlist permits the entire `10.0.0.0/8` range. ### API @@ -196,7 +196,7 @@ A login script is a server-side executable that connects to the already-running ### How it works -1. The address book entry specifies a `login_script` filename (e.g., `portal-login.js`) +1. The connections entry specifies a `login_script` filename (e.g., `portal-login.js`) 2. When the session starts, Chromium is launched with `--remote-debugging-port={cdp_port}` 3. After Chromium is ready, rustguac spawns the script as a child process 4. The script connects to Chromium via CDP, performs automation, then exits @@ -366,7 +366,7 @@ main().catch((err) => { 1. Save it to `/opt/rustguac/scripts/login-example.js` 2. Make it executable: `chmod +x /opt/rustguac/scripts/login-example.js` 3. Install Playwright: `cd /opt/rustguac/scripts && npm install playwright-core` -4. Set the `login_script` field on an address book entry to `login-example.js` +4. Set the `login_script` field on an connections entry to `login-example.js` ### Example: Shell script with curl @@ -422,7 +422,7 @@ The autofill database is written before Chromium launches, and the login script ## Clipboard control -Clipboard copy and paste can be independently disabled per address book entry. This uses guacd's native `disable-copy` and `disable-paste` parameters. +Clipboard copy and paste can be independently disabled per connections entry. This uses guacd's native `disable-copy` and `disable-paste` parameters. | Field | Effect | |-------|--------| @@ -511,9 +511,9 @@ POST /api/sessions | `disable_paste` | boolean | No | Disable clipboard paste (default: false) | | `jump_hosts` | array | No | SSH tunnel hops (see [SSH tunnels](#ssh-tunnels-for-web-sessions)) | -### Address book entry fields +### Connections entry fields -When creating entries via the Vault address book (UI or API), the same fields are available: +When creating entries via the Vault connections (UI or API), the same fields are available: ```json { diff --git a/src/api.rs b/src/api.rs index 5f11e83..396f909 100644 --- a/src/api.rs +++ b/src/api.rs @@ -130,21 +130,51 @@ fn redact_share_url( info } -/// GET /api/sessions — List all sessions. All authenticated roles. +#[derive(Deserialize, Default)] +pub struct ListSessionsQuery { + /// When `true`, admins receive all active sessions instead of only + /// their own. Ignored for non-admins. Used by the Sessions + /// management page; the Address Book keeps the owner-only default. + #[serde(default)] + pub all: bool, +} + +/// GET /api/sessions — List active sessions. +/// Default behaviour (GitHub #102): callers see only sessions they +/// created. The Sessions management page passes `?all=true` to let +/// admins see everyone. pub async fn list_sessions( State(manager): State, identity: Option>, + axum::extract::Query(q): axum::extract::Query, ) -> impl IntoResponse { + let is_admin = identity + .as_ref() + .map(|Extension(id)| id.has_role("admin")) + .unwrap_or(false); + let owner = identity + .as_ref() + .map(|Extension(id)| id.display_name().to_string()); + let show_all = q.all && is_admin; + let sessions: Vec<_> = manager .list_sessions() .await .into_iter() + .filter(|s| { + show_all + || owner + .as_deref() + .map(|o| s.created_by == o) + .unwrap_or(false) + }) .map(|s| redact_share_url(s, &identity)) .collect(); Json(json!(sessions)) } -/// GET /api/sessions/:id — Get session info. All authenticated roles. +/// GET /api/sessions/:id — Get session info. +/// Non-admins can only inspect their own sessions (GitHub #102). pub async fn get_session( State(manager): State, Path(id): Path, @@ -152,6 +182,21 @@ pub async fn get_session( ) -> impl IntoResponse { match manager.get_session(id).await { Some(info) => { + let is_admin = identity + .as_ref() + .map(|Extension(id)| id.has_role("admin")) + .unwrap_or(false); + let is_owner = identity + .as_ref() + .map(|Extension(id)| info.created_by == id.display_name()) + .unwrap_or(false); + if !is_admin && !is_owner { + return ( + StatusCode::NOT_FOUND, + Json(json!({ "error": "session not found" })), + ) + .into_response(); + } let info = redact_share_url(info, &identity); (StatusCode::OK, Json(json!(info))).into_response() } @@ -227,13 +272,27 @@ pub async fn delete_session( /// PUT /api/sessions/:id/thumbnail — Upload a session thumbnail (JPEG). /// Called by the client periodically to update the session preview. +/// Only the session owner (or an admin) can upload. pub async fn put_session_thumbnail( State(manager): State, Path(id): Path, + identity: Option>, body: axum::body::Bytes, ) -> impl IntoResponse { // Validate: session must exist - if manager.get_session(id).await.is_none() { + let Some(info) = manager.get_session(id).await else { + return StatusCode::NOT_FOUND.into_response(); + }; + // Authorise: owner or admin only (privacy — GitHub #102) + let is_admin = identity + .as_ref() + .map(|Extension(id)| id.has_role("admin")) + .unwrap_or(false); + let is_owner = identity + .as_ref() + .map(|Extension(id)| info.created_by == id.display_name()) + .unwrap_or(false); + if !is_admin && !is_owner { return StatusCode::NOT_FOUND.into_response(); } // Reject oversized thumbnails (100KB max) @@ -262,10 +321,28 @@ pub async fn put_session_thumbnail( } /// GET /api/sessions/:id/thumbnail — Serve a session thumbnail (JPEG). +/// Only the session owner (or an admin) can read. Returns 404 for +/// non-owners so we don't leak session existence. pub async fn get_session_thumbnail( State(manager): State, Path(id): Path, + identity: Option>, ) -> impl IntoResponse { + let Some(info) = manager.get_session(id).await else { + return StatusCode::NOT_FOUND.into_response(); + }; + let is_admin = identity + .as_ref() + .map(|Extension(id)| id.has_role("admin")) + .unwrap_or(false); + let is_owner = identity + .as_ref() + .map(|Extension(id)| info.created_by == id.display_name()) + .unwrap_or(false); + if !is_admin && !is_owner { + return StatusCode::NOT_FOUND.into_response(); + } + let path = manager.thumbnail_path(id); match tokio::fs::read(&path).await { Ok(data) => ( @@ -1345,6 +1422,36 @@ pub async fn list_group_mappings( } } +/// GET /api/auth/known-groups — List known OIDC groups (union of role +/// mappings + groups ever seen in a user's login claims). Admin only; +/// used by the folder modal to populate the allowed_groups picker. +pub async fn list_known_groups( + identity: Option>, + Extension(database): Extension, +) -> impl IntoResponse { + if !identity + .as_ref() + .map(|Extension(id)| id.has_role("admin")) + .unwrap_or(false) + { + return ( + StatusCode::FORBIDDEN, + Json(json!({"error": "admin role required"})), + ) + .into_response(); + } + + let db_clone = database.clone(); + match tokio::task::spawn_blocking(move || db::list_known_groups(&db_clone)).await { + Ok(Ok(groups)) => Json(json!({ "groups": groups })).into_response(), + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({"error": "failed to list groups"})), + ) + .into_response(), + } +} + /// POST /api/admin/group-mappings — Create a group-to-role mapping. Admin only. pub async fn create_group_mapping( identity: Option>, @@ -2158,6 +2265,49 @@ pub async fn ab_update_folder( } } +/// GET /api/addressbook/folders/:scope/:folder/config — Read the stored +/// FolderConfig (allowed_groups, description). Admin only — used by the +/// folder modal to prefill the allowed-groups picker on edit. +pub async fn ab_get_folder_config( + identity: Option>, + Extension(vault): Extension, + Path((scope, folder)): Path<(String, String)>, +) -> impl IntoResponse { + let vault = match require_vault(&vault).await { + Ok(v) => v, + Err(resp) => return resp, + }; + if !identity + .as_ref() + .map(|Extension(id)| id.has_role("admin")) + .unwrap_or(false) + { + return ( + StatusCode::FORBIDDEN, + Json(json!({"error": "admin role required"})), + ) + .into_response(); + } + + match vault.get_folder_config(&scope, &folder).await { + Ok(cfg) => Json(json!({ + "allowed_groups": cfg.allowed_groups, + "description": cfg.description, + })) + .into_response(), + Err(crate::vault::VaultError::NotFound) => Json(json!({ + "allowed_groups": Vec::::new(), + "description": "", + })) + .into_response(), + Err(e) => ( + StatusCode::BAD_GATEWAY, + Json(json!({"error": e.to_string()})), + ) + .into_response(), + } +} + /// DELETE /api/addressbook/folders/:scope/:folder — Delete folder and all entries. Admin only. pub async fn ab_delete_folder( identity: Option>, diff --git a/src/db.rs b/src/db.rs index bddea56..0be5e99 100644 --- a/src/db.rs +++ b/src/db.rs @@ -115,6 +115,12 @@ pub fn init_db(path: &Path) -> rusqlite::Result { created_at TEXT NOT NULL DEFAULT (datetime('now')) ); + CREATE TABLE IF NOT EXISTS seen_groups ( + name TEXT PRIMARY KEY, + first_seen TEXT NOT NULL DEFAULT (datetime('now')), + last_seen TEXT NOT NULL DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS user_api_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), @@ -669,6 +675,46 @@ pub fn delete_group_mapping(db: &Db, id: i64) -> rusqlite::Result { Ok(changed > 0) } +/// Upsert OIDC groups observed in a login token, updating last_seen. +pub fn upsert_seen_groups(db: &Db, groups: &[String]) -> rusqlite::Result<()> { + if groups.is_empty() { + return Ok(()); + } + let mut conn = db.lock().unwrap(); + let tx = conn.transaction()?; + { + let mut stmt = tx.prepare( + "INSERT INTO seen_groups (name) VALUES (?1) + ON CONFLICT(name) DO UPDATE SET last_seen = datetime('now')", + )?; + for g in groups { + let trimmed = g.trim(); + if !trimmed.is_empty() { + stmt.execute(params![trimmed])?; + } + } + } + tx.commit()?; + Ok(()) +} + +/// List all known OIDC groups — union of configured role-mappings and groups +/// ever seen in a user's login claims. Sorted case-insensitively. +pub fn list_known_groups(db: &Db) -> rusqlite::Result> { + let conn = db.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT g FROM ( + SELECT oidc_group AS g FROM group_role_mappings + UNION + SELECT name AS g FROM seen_groups + ) + WHERE g IS NOT NULL AND g <> '' + ORDER BY g COLLATE NOCASE", + )?; + let rows = stmt.query_map([], |row| row.get::<_, String>(0))?; + rows.collect() +} + // ── User API tokens ── /// Create a user API token. Returns the plaintext token (shown once). diff --git a/src/main.rs b/src/main.rs index 1ea93d8..b011a63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -622,7 +622,7 @@ async fn run_server(config: Config, database: Db) { // Disk-served HTML pages for name in &[ "index.html", - "addressbook.html", + "connections.html", "sessions.html", "recordings.html", "reports.html", @@ -879,6 +879,7 @@ async fn run_server(config: Config, database: Db) { .route("/api/users/{email}/enable", post(api::enable_user)) .route("/api/admin/group-mappings", get(api::list_group_mappings)) .route("/api/admin/group-mappings", post(api::create_group_mapping)) + .route("/api/auth/known-groups", get(api::list_known_groups)) .route( "/api/admin/group-mappings/{id}", put(api::update_group_mapping), @@ -922,6 +923,10 @@ async fn run_server(config: Config, database: Db) { "/api/addressbook/folders/{scope}/{folder}", delete(api::ab_delete_folder), ) + .route( + "/api/addressbook/folders/{scope}/{folder}/config", + get(api::ab_get_folder_config), + ) .route( "/api/addressbook/folders/{scope}/{folder}/subfolders", get(api::ab_list_subfolders), @@ -1004,9 +1009,16 @@ async fn run_server(config: Config, database: Db) { let html_routes = Router::new() .route("/", get(serve_branded_page)) .route("/index.html", get(serve_branded_page)) - .route("/addressbook.html", get(serve_branded_page)) + .route("/connections.html", get(serve_branded_page)) + // Legacy path — the page was renamed from Address Book → Connections. + // Permanent redirect so bookmarks keep working. + .route( + "/addressbook.html", + get(|| async { axum::response::Redirect::permanent("/connections.html") }), + ) .route("/sessions.html", get(serve_branded_page)) .route("/recordings.html", get(serve_branded_page)) + .route("/reports.html", get(serve_branded_page)) .route("/admin.html", get(serve_branded_page)) .route("/tokens.html", get(serve_branded_page)) .route("/docs.html", get(serve_branded_page)); diff --git a/src/oidc.rs b/src/oidc.rs index 05eea36..224b4c6 100644 --- a/src/oidc.rs +++ b/src/oidc.rs @@ -297,6 +297,14 @@ pub async fn callback( let groups = extract_groups_from_jwt(&id_token.to_string(), &oidc.config.groups_claim); if !groups.is_empty() { tracing::info!(email = %email, groups = ?groups, "OIDC groups extracted"); + let db_for_seen = database.clone(); + let groups_for_seen = groups.clone(); + let _ = tokio::task::spawn_blocking(move || { + if let Err(e) = db::upsert_seen_groups(&db_for_seen, &groups_for_seen) { + tracing::warn!(error = %e, "failed to persist seen OIDC groups"); + } + }) + .await; } // Resolve role from group-to-role mappings (highest matching wins). diff --git a/static/admin.html b/static/admin.html index b9e6e87..612226b 100644 --- a/static/admin.html +++ b/static/admin.html @@ -15,7 +15,7 @@

rustguac