From 0856cd4f9370e37a3dc0cb831293c0e0e6d1c6b9 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 16 Jun 2026 07:47:31 -0400 Subject: [PATCH] docs: dashboard disable-login design + save session-resilience tasklist snapshot --- ...26-06-16-dashboard-disable-login-design.md | 141 ++++++++++++++++++ oldtasks.md | 68 +++++++++ 2 files changed, 209 insertions(+) create mode 100644 docs/plans/2026-06-16-dashboard-disable-login-design.md create mode 100644 oldtasks.md diff --git a/docs/plans/2026-06-16-dashboard-disable-login-design.md b/docs/plans/2026-06-16-dashboard-disable-login-design.md new file mode 100644 index 0000000..b6b5a42 --- /dev/null +++ b/docs/plans/2026-06-16-dashboard-disable-login-design.md @@ -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` → `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` 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( + "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` 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. diff --git a/oldtasks.md b/oldtasks.md new file mode 100644 index 0000000..d675f45 --- /dev/null +++ b/oldtasks.md @@ -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.