docs: dashboard disable-login design + save session-resilience tasklist snapshot
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
# Dashboard "Disable Login" Dev Flag — Design
|
||||
|
||||
**Date:** 2026-06-16
|
||||
**Status:** Approved (brainstorming) — ready for implementation plan.
|
||||
|
||||
## Goal
|
||||
|
||||
A config flag that **disables login in the gateway dashboard**. When enabled, every
|
||||
request is auto-authenticated as a fixed dev user (default **`multi-role`**) holding
|
||||
**both** dashboard roles (`Administrator` + `Viewer`), so no login form, cookie, or LDAP
|
||||
bind is involved and the whole UI behaves as a signed-in multi-role admin. Default
|
||||
**off**. Mirrors the sister project OtOpcUa's `Security:Auth:DisableLogin` feature.
|
||||
|
||||
## Why / scope
|
||||
|
||||
Speeds up dashboard testing against the remote dev boxes (10.100.0.48, wonder) with no
|
||||
sign-in round-trip and no GLAuth dependency. Scope is the **dashboard cookie web surface
|
||||
only** — the gRPC API-key auth path (`authorization: Bearer mxgw_…`) and its scopes are a
|
||||
separate auth model and are **untouched**.
|
||||
|
||||
## Background (current dashboard auth, verified)
|
||||
|
||||
- Dashboard auth is a **single cookie scheme** `MxGateway.Dashboard` registered in
|
||||
`Dashboard/DashboardServiceCollectionExtensions.cs::AddGatewayDashboard`
|
||||
(`AddAuthentication("MxGateway.Dashboard").AddCookie(...)`), plus a bearer scheme
|
||||
`MxGateway.Dashboard.HubToken` (`HubTokenAuthenticationHandler`) for SignalR hubs.
|
||||
- Real login: `/login` → `DashboardAuthenticator.AuthenticateAsync` → shared
|
||||
`ILdapAuthService` bind/search → `IGroupRoleMapper<string>` → `CreatePrincipal` builds a
|
||||
`ClaimsPrincipal` (`ZbClaimTypes.Name`/`Username`/`DisplayName` + one `ZbClaimTypes.Role`
|
||||
per role + `LdapGroupClaimType` group claims; identity authType = the cookie scheme,
|
||||
nameType = `ZbClaimTypes.Name`, roleType = `ZbClaimTypes.Role`) → cookie sign-in.
|
||||
- Authorization: a custom `DashboardAuthorizationHandler` evaluates
|
||||
`DashboardAuthorizationRequirement`. Policies: `ViewerPolicy` (AnyDashboardRole),
|
||||
`AdminPolicy` (AdminOnly), `HubClientsPolicy` (cookie **or** hub-token scheme,
|
||||
AnyDashboardRole).
|
||||
- Roles: exactly two — `DashboardRoles.Admin` (`"Administrator"`) and
|
||||
`DashboardRoles.Viewer` (`"Viewer"`).
|
||||
- **Existing escapes (important):** `DashboardAuthorizationHandler` already short-circuits
|
||||
when `Authentication.Mode == Disabled` or when `Dashboard.AllowAnonymousLocalhost`
|
||||
(default **true**) and the request is loopback. **But both only `context.Succeed(...)`
|
||||
the authorization requirement — they do not mint an authenticated principal.** So
|
||||
`HttpContext.User.Identity.IsAuthenticated` stays false, `Identity.Name` is null, and
|
||||
role-gated `AuthorizeView` write affordances stay **hidden**. That is precisely why they
|
||||
do not deliver the "logged-in multi-role admin" experience this feature needs.
|
||||
|
||||
## Approach (chosen: always-authenticating handler under the cookie scheme name)
|
||||
|
||||
When the flag is **on**, **replace the `.AddCookie(...)` registration with a custom
|
||||
`AuthenticationHandler` registered under the *same* scheme name**
|
||||
(`DashboardAuthenticationDefaults.AuthenticationScheme` = `"MxGateway.Dashboard"`). Its
|
||||
`HandleAuthenticateAsync` **always returns `AuthenticateResult.Success`** with the fixed
|
||||
dev principal (configured username, both roles), shaped identically to what
|
||||
`DashboardAuthenticator.CreatePrincipal` produces. `UseAuthentication()` stamps that
|
||||
principal on `HttpContext.User` for **every** request.
|
||||
|
||||
Registering under the cookie scheme name (not a new name) is the load-bearing detail: the
|
||||
`ViewerPolicy`, `AdminPolicy`, and `HubClientsPolicy` all resolve through that scheme via
|
||||
`DashboardAuthorizationHandler`'s role check, so they pass with **no policy or page
|
||||
changes**. The HTTP pipeline (Razor pages, admin endpoints), the Blazor circuit
|
||||
(`AuthorizeView`, `[CascadingParameter] AuthenticationState`), and the SignalR hubs are
|
||||
all covered by the single `HttpContext.User` seam. Because the handler authenticates every
|
||||
request, the feature is inherently **global** (all clients, including remote browsers) —
|
||||
the agreed scope.
|
||||
|
||||
`SignInAsync`/`SignOutAsync` are no-ops (no cookie to write or clear; the next request
|
||||
re-authenticates through the handler).
|
||||
|
||||
**Alternatives rejected:** (2) mint the principal inside the existing
|
||||
`DashboardAuthorizationHandler` bypass branches — authorization runs after authentication,
|
||||
so `HttpContext.User` is set too late for the Blazor auth state, and two seams must agree
|
||||
(this is essentially today's half-feature); (3) a pipeline middleware plus a stubbed
|
||||
`AuthenticationStateProvider` — two components to keep in sync, and a page request still
|
||||
302s to `/login` unless `HttpContext.User` is also set.
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Config surface — two new fields on `DashboardOptions` (`MxGateway:Dashboard:*`)
|
||||
- `DisableLogin` (bool, default **false**).
|
||||
- `AutoLoginUser` (string, default **`"multi-role"`** — this project's GLAuth
|
||||
Administrator test user). Used as `Name`/`Username`/`DisplayName` of the minted
|
||||
principal; blank falls back to `"multi-role"`.
|
||||
|
||||
"All permissions" = principal minted with **both** `DashboardRoles.Admin` and
|
||||
`DashboardRoles.Viewer`.
|
||||
|
||||
### 2. `DashboardAutoLoginAuthenticationHandler`
|
||||
`AuthenticationHandler<AuthenticationSchemeOptions>` implementing
|
||||
`IAuthenticationSignInHandler`. Mirrors OtOpcUa's `AutoLoginAuthenticationHandler`, adapted
|
||||
to this project's claim shape (`ZbClaimTypes.*`, `DashboardRoles.*`). Always `Success`;
|
||||
SignIn/SignOut no-ops.
|
||||
|
||||
### 3. Wiring in `AddGatewayDashboard`
|
||||
Read `MxGateway:Dashboard:DisableLogin` directly from `IConfiguration` at registration
|
||||
time (the same idiom OtOpcUa uses, since scheme registration precedes options binding).
|
||||
- On → `AddScheme<AuthenticationSchemeOptions, DashboardAutoLoginAuthenticationHandler>(
|
||||
"MxGateway.Dashboard", _ => {})` in place of `AddCookie`; the `HubToken` scheme stays
|
||||
registered unchanged.
|
||||
- Off → existing `AddCookie(...)` path unchanged.
|
||||
|
||||
### 4. Safety
|
||||
- Default **off**.
|
||||
- A **loud one-time startup `LogWarning`** ("DASHBOARD LOGIN DISABLED
|
||||
(MxGateway:Dashboard:DisableLogin=true) — every request authenticated as '{user}' with
|
||||
full permissions (Administrator, Viewer). Dev/test only; never enable in production.")
|
||||
via the same options `PostConfigure<ILoggerFactory>` idiom OtOpcUa uses.
|
||||
- The existing `AllowAnonymousLocalhost` / `Authentication.Mode == Disabled` escapes are
|
||||
left untouched — `DisableLogin` is orthogonal (it changes *authentication*, minting a
|
||||
principal, not authorization bypass); when it is on the authorization handler's normal
|
||||
role-check branch succeeds, so the bypass branches simply do not matter.
|
||||
|
||||
## Error handling / edge cases
|
||||
|
||||
- Blank `AutoLoginUser` → falls back to `"multi-role"` (handler never mints a nameless
|
||||
principal).
|
||||
- `/login` still renders when the flag is on but is pointless (the user is already
|
||||
authenticated); `POST /login`'s `SignInAsync` is a no-op. `/logout` is likewise a no-op.
|
||||
No redirect added (YAGNI).
|
||||
- No interaction with the gRPC API-key path — that auth is entirely separate.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Handler unit test:** `HandleAuthenticateAsync` returns `Success`;
|
||||
`principal.Identity.IsAuthenticated`, `Identity.Name == AutoLoginUser`,
|
||||
`IsInRole("Administrator")` && `IsInRole("Viewer")`; blank-user fallback.
|
||||
- **Wiring / integration (`WebApplicationFactory`):** with `DisableLogin=true`, an
|
||||
`AdminPolicy`-gated endpoint returns 200 with **no** cookie, and a `/hubs/*` negotiate
|
||||
authorizes; the startup warning is emitted.
|
||||
- **Regression:** with the flag off (default), the real cookie handler is still registered
|
||||
and existing dashboard auth tests pass.
|
||||
|
||||
## Docs to update in the same change
|
||||
|
||||
- `docs/GatewayConfiguration.md` — new `MxGateway:Dashboard:DisableLogin` /
|
||||
`AutoLoginUser` options.
|
||||
- The dashboard design doc (`docs/GatewayDashboardDesign.md`).
|
||||
- The CLAUDE.md dashboard-auth note (alongside the `AllowAnonymousLocalhost` mention).
|
||||
|
||||
## Scope / verification
|
||||
|
||||
Gateway-server-side only (.NET 10, x64) — builds and tests entirely on macOS. No worker,
|
||||
no `.proto`, no client, no gRPC changes.
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
# Saved Task List — Session Resilience Epic
|
||||
|
||||
> Snapshot taken 2026-06-16, before switching to the dashboard disable-login feature.
|
||||
> This is the in-flight epic from `docs/plans/2026-06-15-session-resilience.md`.
|
||||
|
||||
## How to resume
|
||||
|
||||
```
|
||||
/superpowers-extended-cc:executing-plans docs/plans/2026-06-15-session-resilience.md
|
||||
```
|
||||
|
||||
The authoritative resume state lives in
|
||||
`docs/plans/2026-06-15-session-resilience.md.tasks.json` (tasks 1–12 completed,
|
||||
13–28 pending). This file is just a human-readable mirror.
|
||||
|
||||
## Status
|
||||
|
||||
**12 of 28 tasks complete** (Phases 1–2 + reconnect core of Phase 3). All completed
|
||||
work is merged to `main` (commit `c446bef`, pushed to origin).
|
||||
|
||||
### Completed — Phase 1 (Foundation)
|
||||
- ✅ Task 1 (#108): Add OwnerKeyId to the session
|
||||
- ✅ Task 2 (#109): SessionEventDistributor skeleton
|
||||
- ✅ Task 3 (#110): Bounded replay ring buffer
|
||||
- ✅ Task 4 (#111): Rewire AttachEventSubscriber + EventStreamService onto distributor
|
||||
- ✅ Task 5 (#112): Per-subscriber backpressure isolation
|
||||
- ✅ Task 6 (#113): Dashboard broadcaster becomes a distributor subscriber
|
||||
|
||||
### Completed — Phase 2 (Multi-subscriber fan-out)
|
||||
- ✅ Task 7 (#114): Remove validator block + add subscriber cap option
|
||||
- ✅ Task 8 (#115): Subscriber-lease collection + cap enforcement
|
||||
- ✅ Task 9 (#116): Multi-subscriber end-to-end test (FakeWorkerHarness)
|
||||
|
||||
### Completed — Phase 3 (Reconnect core)
|
||||
- ✅ Task 10 (#117): Proto — ReplayGap signal
|
||||
- ✅ Task 11 (#118): Detach-grace session retention
|
||||
- ✅ Task 12 (#119): Replay-on-reconnect + emit ReplayGap
|
||||
|
||||
### Pending — Phase 3 finish
|
||||
- ⏳ Task 13 (#120): Owner re-validation on reconnect — blockedBy 12, 1
|
||||
- ⏳ Task 14 (#121): Client ReplayGap handling — all 5 clients — blockedBy 10
|
||||
- Carry the per-language presence-check idiom note for `optional` message fields.
|
||||
- ⏳ Task 15 (#122): Reconnect integration test (fake worker) — blockedBy 12
|
||||
|
||||
### Pending — Phase 4 (Per-session dashboard ACL)
|
||||
- ⏳ Task 16 (#123): gRPC session-owner gate + all-sessions admin scope — blockedBy 9, 1
|
||||
- ⏳ Task 17 (#124): Session Tag + dashboard group-to-tag config — blockedBy 9
|
||||
- ⏳ Task 18 (#125): EventsHub per-session ACL + hub-token tag claim — blockedBy 17
|
||||
- Open decision: Viewer default (admin-sees-all vs strict per-session).
|
||||
- ⏳ Task 19 (#126): ACL tests incl. live LDAP users — blockedBy 18
|
||||
|
||||
### Pending — Phase 5 (Orphan-worker reattach)
|
||||
- ⏳ Task 20 (#127): Stable gateway-instance id + stable pipe naming — blockedBy 19
|
||||
- ⏳ Task 21 (#128): Adoption manifest store (SQLite) — blockedBy 20
|
||||
- ⏳ Task 22 (#129): Proto — worker adopt/reconnect frame — blockedBy 21
|
||||
- ⏳ Task 23 (#130): Worker phone-home reconnect loop + self-terminate — blockedBy 22 (net48/x86, windev)
|
||||
- ⏳ Task 24 (#131): Gateway adoption — re-open pipes, nonce-validate, reject impostors — blockedBy 23
|
||||
- ⏳ Task 25 (#132): Resync adopted worker + ReplayGap to subscribers — blockedBy 24, 12
|
||||
- ⏳ Task 26 (#133): EnableOrphanReattach flag (default off) + terminator fallback — blockedBy 24
|
||||
- ⏳ Task 27 (#134): Gateway-restart reattach round-trip (WINDEV + live worker) — blockedBy 25, 26
|
||||
- ⏳ Task 28 (#135): Documented-rule reversals + stillpending refresh — blockedBy 27
|
||||
|
||||
## Notes
|
||||
- Phase 5 reverses the documented "Gateway restart does not reattach orphan workers"
|
||||
rule (CLAUDE.md) — this was explicitly approved during design.
|
||||
- Two deferred follow-ups noted earlier: dashboard visibility of `DetachedAtUtc` on
|
||||
`DashboardSessionSummary`.
|
||||
- Worker (net48/x86) tasks build/test on windev; everything else builds on macOS.
|
||||
Reference in New Issue
Block a user