docs(cloud-console): document user authentication
This commit is contained in:
+64
-33
@@ -186,33 +186,62 @@ PY
|
||||
## Cloud Console (Web UI)
|
||||
|
||||
The repository ships an independent Vue 3 + Vite SPA at `cloud-console/` that
|
||||
renders the task queue/history, device pool, host registry, and plugin
|
||||
registry, and exposes the existing plugin-registration action. It authenticates
|
||||
the same way `CloudClient` does: by attaching a pre-issued bearer token to
|
||||
every request. There is no login or session system.
|
||||
renders task history, devices, hosts, plugins, and the user directory. Human
|
||||
operators sign in with a username and password; the Cloud API creates an
|
||||
expiring, revocable `HttpOnly` session cookie and uses a separate CSRF
|
||||
cookie/header for writes. Existing bearer tokens remain available through the
|
||||
Console's explicit **Use API token** action and for SDK, Host Agent, and
|
||||
automation compatibility.
|
||||
|
||||
### Provision an operator bearer token
|
||||
### HTTPS and session configuration
|
||||
|
||||
Add a `CLOUD_PUBLIC_CREDENTIALS_JSON` entry whose scopes cover what the
|
||||
console operators need to do. The least-privilege set for read-only dashboards
|
||||
is `tasks:read`, `pool:read`, and `plugins:read`. Add `tasks:submit` only if
|
||||
operators should submit ad-hoc tasks from the same tab, and `plugins:admin`
|
||||
only if operators should register plugins:
|
||||
`CLOUD_ENVIRONMENT=production` requires `CLOUD_SESSION_COOKIE_SECURE=true`.
|
||||
Terminate TLS at a reverse proxy and open the same-origin Console through HTTPS,
|
||||
for example `https://cloud.example.com/console/`. Direct `http://host:8001`
|
||||
access is for local/test mode only; it cannot retain production login cookies.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"principal_id": "console-operator",
|
||||
"token": "replace-with-a-long-random-opaque-token",
|
||||
"scopes": ["tasks:read", "pool:read", "plugins:read", "plugins:admin"]
|
||||
}
|
||||
]
|
||||
The defaults are an 8-hour idle session TTL, 7-day absolute TTL, and a temporary
|
||||
block after five failed logins in a 15-minute username/client-address window:
|
||||
|
||||
```text
|
||||
CLOUD_USER_SESSION_IDLE_SECONDS=28800
|
||||
CLOUD_USER_SESSION_ABSOLUTE_SECONDS=604800
|
||||
CLOUD_LOGIN_FAILURE_LIMIT=5
|
||||
CLOUD_LOGIN_FAILURE_WINDOW_SECONDS=900
|
||||
CLOUD_LOGIN_BLOCK_SECONDS=900
|
||||
CLOUD_SESSION_COOKIE_SECURE=true
|
||||
CLOUD_TRUST_PROXY_HEADERS=false
|
||||
```
|
||||
|
||||
Rotate the token the same way as any other credential entry: deploy the
|
||||
updated Cloud API credential set and instruct operators to paste the new token
|
||||
into the console. The console keeps the token only in browser `sessionStorage`
|
||||
for that tab; closing the tab discards it.
|
||||
Set `CLOUD_TRUST_PROXY_HEADERS=true` only when a trusted proxy overwrites
|
||||
`X-Forwarded-For` before requests reach the Cloud API.
|
||||
|
||||
### Create and recover administrator accounts
|
||||
|
||||
After migrations and Cloud API startup, create the first account interactively:
|
||||
|
||||
```bash
|
||||
docker compose exec cloud-api \
|
||||
device-cloud-admin users create \
|
||||
--username admin --display-name "Cloud Administrator" --role admin
|
||||
```
|
||||
|
||||
The command prompts twice for the password, so it does not enter shell history,
|
||||
Compose configuration, process arguments, logs, or container inspection output.
|
||||
Recovery commands are also interactive:
|
||||
|
||||
```bash
|
||||
docker compose exec cloud-api device-cloud-admin users reset-password --username admin
|
||||
docker compose exec cloud-api device-cloud-admin users enable --username admin
|
||||
docker compose exec cloud-api device-cloud-admin users revoke-sessions --username admin
|
||||
```
|
||||
|
||||
Roles are fixed: `viewer` can read tasks/pool/plugins; `operator` additionally
|
||||
submits tasks; `admin` has unrestricted Cloud API access and manages users.
|
||||
Administrators create users, reset passwords, change roles, disable accounts,
|
||||
and revoke sessions from the **Users** Console view. New and reset users must
|
||||
change their temporary password before accessing other resources, and the API
|
||||
will not disable or demote the last enabled administrator.
|
||||
|
||||
### Configure the CORS allow-list
|
||||
|
||||
@@ -231,7 +260,7 @@ export CLOUD_CONSOLE_CORS_ORIGINS="https://console.example.com"
|
||||
Restart the Cloud API after changing this env. The middleware is added only
|
||||
when the allow-list is non-empty — existing deployments see no behavior change
|
||||
until an operator opts in. Blanket `allow_origins=["*"]` is intentionally not
|
||||
supported because every console request carries a bearer token.
|
||||
supported because browser sessions are credentialed.
|
||||
|
||||
### Run the console
|
||||
|
||||
@@ -244,8 +273,10 @@ npm run dev
|
||||
```
|
||||
|
||||
Vite prints a local URL (default `http://127.0.0.1:5173`). That exact origin
|
||||
must be in `CLOUD_CONSOLE_CORS_ORIGINS` on the Cloud API. Open the dev URL,
|
||||
paste the operator token, and the dashboards become available.
|
||||
must be in `CLOUD_CONSOLE_CORS_ORIGINS` on the Cloud API. For local development
|
||||
set `CLOUD_SESSION_COOKIE_SECURE=false`, then open the dev URL and sign in with
|
||||
a user account. The Console sends credentialed requests and attaches CSRF proof
|
||||
to writes.
|
||||
|
||||
For a production build, run `npm run build` and serve the resulting `dist/`
|
||||
behind any static file server or CDN, with `VITE_CLOUD_API_BASE_URL` baked in
|
||||
@@ -257,15 +288,15 @@ The Jenkins-built Docker image already carries the SPA at `/app/console-static`,
|
||||
and `compose.yaml` / `compose.deploy.yaml` set
|
||||
`CLOUD_CONSOLE_STATIC_DIR=/app/console-static` on the `cloud-api` service. In
|
||||
this mode the Cloud API itself serves the console at `/console/` (visiting `/`
|
||||
307-redirects there), so operators can open `https://<cloud-api-host>:8001/`
|
||||
directly — no separate dev server, no static host, no CORS allow-list needed
|
||||
(the SPA and the API share one origin).
|
||||
307-redirects there). Put that origin behind an HTTPS reverse proxy, then open
|
||||
for example `https://cloud.example.com/` directly — no separate dev server, no
|
||||
static host, and no CORS allow-list are needed because the SPA and API share one
|
||||
origin.
|
||||
|
||||
The SPA shell (`index.html`, JS, CSS) is served without a bearer token by
|
||||
design — `_authorize(...)` is called inside the `/v1/*` route handlers, not in
|
||||
middleware, so the SPA can boot before the operator pastes a token. All
|
||||
`/v1/*` API calls still require `tasks:read`/`pool:read`/`plugins:read` scopes
|
||||
as before.
|
||||
The SPA shell (`index.html`, JS, CSS) is served without credentials by design
|
||||
so it can render the login page. All `/v1/*` resource calls remain scope-gated,
|
||||
and unsafe cookie-authenticated calls require CSRF proof. The browser receives
|
||||
only the non-secret CSRF value; it never receives the `HttpOnly` session secret.
|
||||
|
||||
To opt out (e.g. for local development where you run `npm run dev`), leave
|
||||
`CLOUD_CONSOLE_STATIC_DIR` unset. The mount is conditional on that env var.
|
||||
|
||||
Reference in New Issue
Block a user