fix(security): finish the bearer-token design and cache role mappings (review 2026-10-21 05/U1, 05/P1)
- ManagementAuthenticator accepts Authorization: Bearer <jwt> alongside Basic on /management, the audit REST endpoints and the debug-stream hub. Tokens are validated locally (HS256 only, alg=none rejected, issuer/audience, lifetime with 30 s skew); roles and site scope inside a valid token are authoritative for its lifetime, so a bearer call costs no LDAP bind and no role re-map. - /auth/token returns expires_in + management_bearer:true, carries username, display name, roles and permitted sites, and answers 404 under DisableLogin. - CLI exchanges credentials once per process (in-memory CliTokenStore), sends Bearer, re-exchanges once on 401 then falls back to Basic; older/login-disabled servers are Basic-only; bad credentials are not retried with Basic. Debug stream uses the token via AccessTokenProvider. SCADABRIDGE_AUTH_MODE=basic opts out. - StartupValidator fails a Central boot on a JwtSigningKey under 32 UTF-8 bytes; fixed the false 'fail-fast at construction' comment. Removed the dead RefreshToken/RecordActivity/ShouldRefresh/IsIdleTimedOut members and the inert JwtRefreshThresholdMinutes; JwtExpiryMinutes now documented as the bearer lifetime (validated > 0). - RoleMapper reads through a process-wide RoleMappingCache (RoleMappingCacheSeconds, default 30 s): one refresh loads the table plus every Deployer mapping's scope rules, Invalidate() drops it (generation-guarded against in-flight refreshes). - Component-Security.md / Component-CLI.md / CLI README describe the real model (cookie claims ticket, bearer for CLI) instead of a cookie-embedded JWT.
This commit is contained in:
@@ -28,7 +28,7 @@ scadabridge --url http://central-host:5000 <command>
|
||||
scadabridge --url http://localhost:9000 <command>
|
||||
```
|
||||
|
||||
The management API is stateless (Basic Auth per request), so sticky sessions are unnecessary. Direct node URLs (`9001`/`9002`) are useful for diagnostics; use Traefik for active-node routing and failover.
|
||||
The management API is stateless (a self-contained bearer token, or Basic Auth per request), so sticky sessions are unnecessary. Direct node URLs (`9001`/`9002`) are useful for diagnostics; use Traefik for active-node routing and failover.
|
||||
|
||||
## Global Options
|
||||
|
||||
@@ -61,6 +61,7 @@ For the Docker test environment, see `docker/README.md` for a ready-to-use confi
|
||||
| `SCADABRIDGE_FORMAT` | Default output format (overrides config file) |
|
||||
| `SCADABRIDGE_USERNAME` | LDAP username (fallback when `--username` is not supplied) |
|
||||
| `SCADABRIDGE_PASSWORD` | LDAP password (fallback when `--password` is not supplied). Preferred over `--password` on the command line, which leaks into process listings and shell history. |
|
||||
| `SCADABRIDGE_AUTH_MODE` | `token` (default) exchanges credentials for a bearer token once per process; `basic` sends HTTP Basic on every call (see *Authentication* below). |
|
||||
|
||||
## Output
|
||||
|
||||
@@ -2672,13 +2673,23 @@ client deadline; explicit bundle/Flow Debugger timeouts retain their own policie
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
The CLI connects to the Central cluster over **HTTP** (`ManagementHttpClient`), posting to the `/management` endpoints at the configured `managementUrl` — normally the Traefik load balancer, which routes to the active central node. It carries HTTP **Basic** credentials from `--username`/`--password`. Central's endpoint handler asks the `ManagementActor` in-process via `ManagementActorHolder`.
|
||||
The CLI connects to the Central cluster over **HTTP** (`ManagementHttpClient`), posting to the `/management` endpoints at the configured `managementUrl` — normally the Traefik load balancer, which routes to the active central node. It authenticates with a bearer token (see *Authentication*). Central's endpoint handler asks the `ManagementActor` in-process via `ManagementActorHolder`.
|
||||
|
||||
There is no Akka dependency in the CLI at all: it does not join the cluster, does not use `ClusterClient`, and does not contact a `ClusterClientReceptionist`. (Earlier revisions of this document described a ClusterClient transport that was never built.)
|
||||
|
||||
An `HttpClient` is created per command invocation and disposed when the command completes.
|
||||
|
||||
Role enforcement is applied by the ManagementActor on the server side. The CLI sends `--username` / `--password` as HTTP Basic credentials. The server performs LDAP authentication, resolves group memberships, and maps them to roles such as Administrator, Designer, and Deployer. Operations require the appropriate role — for example, creating templates requires `Designer`, deploying requires `Deployer`. In the test environment, use the `multi-role` user (password: `password`) which has all three roles.
|
||||
Role enforcement is applied by the ManagementActor on the server side. The server performs LDAP authentication (once, at the token exchange — or per call on the Basic fallback), resolves group memberships, and maps them to roles such as Administrator, Designer, and Deployer. Operations require the appropriate role — for example, creating templates requires `Designer`, deploying requires `Deployer`. In the test environment, use the `multi-role` user (password: `password`) which has all three roles.
|
||||
|
||||
## Authentication
|
||||
|
||||
The CLI resolves `--username` / `--password` (or `SCADABRIDGE_USERNAME` / `SCADABRIDGE_PASSWORD`) and, on the first management call of the process, exchanges them at `POST /auth/token` for a short-lived bearer JWT (default 15 minutes) carrying the username, roles and permitted sites. That exchange is the invocation's only LDAP bind: every later call — `/management`, the audit REST endpoints, and `debug stream` (including its reconnects) — sends `Authorization: Bearer <token>`, which the server validates without contacting LDAP, so a short LDAP outage does not interrupt a run that already holds a token.
|
||||
|
||||
- The token is kept **in memory only** (never written to disk, logged or printed) and dies with the process.
|
||||
- A call refused with `401` re-exchanges once, then retries with HTTP Basic.
|
||||
- A server without bearer support — no `/auth/token`, a login-disabled deployment, or an older server whose token response lacks `management_bearer: true` — is used with HTTP Basic for the rest of the process (an LDAP bind per call, as before).
|
||||
- Bad credentials or a login-throttle lockout at the exchange are reported directly (exit 1) and not retried with Basic.
|
||||
- Set `SCADABRIDGE_AUTH_MODE=basic` to skip the exchange entirely.
|
||||
|
||||
## Issues & Missing Features
|
||||
|
||||
|
||||
Reference in New Issue
Block a user