docs(cloud-console): document user authentication

This commit is contained in:
2026-07-13 17:55:21 +08:00
parent cdef630e67
commit c72c31de04
10 changed files with 595 additions and 113 deletions
+64 -33
View File
@@ -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.