Phase 3 PR 29 — Account/session page with roles + capabilities #28

Merged
dohertj2 merged 1 commits from phase-3-pr29-account-page into v2 2026-04-18 14:46:46 -04:00
Owner

Expands the minimal sidebar role display into a dedicated /account route.

What's shown

  • Identity — username (from ClaimTypes.NameIdentifier) + display name (from ClaimTypes.Name).
  • Admin roles — the set of Admin roles as badges (from ClaimTypes.Role, populated from LDAP group mapping at sign-in).
  • LDAP groups — raw LDAP groups that produced those roles (from the ldap_group claim that Login.razor already adds).
  • Capability table — each Admin capability, its required role, and a Yes/No badge for whether this session has it. Mirrors Program.cs's AddAuthorizationBuilder policies + each page's [Authorize] attribute so operators can self-service check access without trial-and-error navigation.

Capabilities covered:

Capability Required role
View clusters + fleet status any admin role
Edit configuration drafts (CanEdit) ConfigEditor or FleetAdmin
Publish generations (CanPublish) FleetAdmin
Manage certificate trust FleetAdmin (PR 28 gate)
Manage external-ID reservations ConfigEditor or FleetAdmin

Sidebar change

The 'Signed in as' line in MainLayout.razor now wraps the display name in a link to /account. Sign-out stays where it is for muscle memory.

Design notes

  • Page is [Authorize] (any authenticated user) — the capability table deliberately works for every signed-in session so operators can see what they don't have access to. Helps them file the right ticket with their LDAP admin instead of getting blank Access Denied after navigating blindly.
  • Capability list is a private record list in the page, not a service. It's a UI-presentation mirror; the runtime policy is Program.cs + each page's [Authorize], and pulling it behind a service would just be indirection. A comment on the list reminds future-me to extend it when a new policy or [Authorize] page lands.

Tests

No new tests — the page is pure display over claims; its only logic (Any-overlap between session roles and required roles) is what [Authorize(Roles=...)] does in-framework, and mirroring that in a unit test would test ASP.NET rather than our code. Admin.Tests Unit stays 23 pass / 0 fail. Admin build clean — 0 errors, 0 warnings.

Closes

The third of the three Admin UI gap items the user selected to complete this round (Fleet dashboard = PR 27, Cert trust = PR 28, Account page = this one). Driver-specific admin pages remain deferred until driver options are finalized.

Expands the minimal sidebar role display into a dedicated `/account` route. ## What's shown - **Identity** — username (from `ClaimTypes.NameIdentifier`) + display name (from `ClaimTypes.Name`). - **Admin roles** — the set of Admin roles as badges (from `ClaimTypes.Role`, populated from LDAP group mapping at sign-in). - **LDAP groups** — raw LDAP groups that produced those roles (from the `ldap_group` claim that `Login.razor` already adds). - **Capability table** — each Admin capability, its required role, and a Yes/No badge for whether this session has it. Mirrors `Program.cs`'s `AddAuthorizationBuilder` policies + each page's `[Authorize]` attribute so operators can self-service check access without trial-and-error navigation. Capabilities covered: | Capability | Required role | | --- | --- | | View clusters + fleet status | any admin role | | Edit configuration drafts (`CanEdit`) | ConfigEditor or FleetAdmin | | Publish generations (`CanPublish`) | FleetAdmin | | Manage certificate trust | FleetAdmin (PR 28 gate) | | Manage external-ID reservations | ConfigEditor or FleetAdmin | ## Sidebar change The 'Signed in as' line in `MainLayout.razor` now wraps the display name in a link to `/account`. Sign-out stays where it is for muscle memory. ## Design notes - Page is `[Authorize]` (any authenticated user) — the capability table deliberately works for every signed-in session so operators can see what they **don't** have access to. Helps them file the right ticket with their LDAP admin instead of getting blank Access Denied after navigating blindly. - Capability list is a private record list in the page, not a service. It's a UI-presentation mirror; the runtime policy is `Program.cs` + each page's `[Authorize]`, and pulling it behind a service would just be indirection. A comment on the list reminds future-me to extend it when a new policy or `[Authorize]` page lands. ## Tests No new tests — the page is pure display over claims; its only logic (Any-overlap between session roles and required roles) is what `[Authorize(Roles=...)]` does in-framework, and mirroring that in a unit test would test ASP.NET rather than our code. Admin.Tests Unit stays 23 pass / 0 fail. Admin build clean — 0 errors, 0 warnings. ## Closes The third of the three Admin UI gap items the user selected to complete this round (Fleet dashboard = PR 27, Cert trust = PR 28, Account page = this one). Driver-specific admin pages remain deferred until driver options are finalized.
dohertj2 added 1 commit 2026-04-18 14:46:44 -04:00
Sidebar's 'Signed in as' line now wraps the display name in a link to /account so the existing sidebar-compact view becomes the entry point for the fuller page — keeps the sign-out button where it was for muscle memory, just adds the detail page one click away. Page is gated with [Authorize] (any authenticated admin) rather than a specific role — the capability table deliberately works for every signed-in user so they can see what they DON'T have access to, which helps them file the right ticket with their LDAP admin instead of getting a plain Access Denied when navigating blindly.
Capability → required-role table is defined as a private readonly record list in the page rather than pulled from a service because it's a UI-presentation concern, not runtime policy state — the runtime policy IS Program.cs's AddAuthorizationBuilder + each page's [Authorize] attribute, and this table just mirrors it for operator readability. Comment on the list reminds future-me to extend it when a new policy or [Authorize] page lands. No behavior change if roles are empty, but the page surfaces a hint ('Sign-in would have been blocked, so if you're seeing this, the session claim is likely stale') that nudges the operator toward signing out + back in.
No new tests added — the page is pure display over claims; its only logic is the 'has-capability' Any-overlap check which is exactly what ASP.NET's [Authorize(Roles=...)] does in-framework, and duplicating that in a unit test would test the framework rather than our code. Admin.Tests Unit stays 23 pass / 0 fail. Admin build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dohertj2 merged commit 901d2b8019 into v2 2026-04-18 14:46:46 -04:00
dohertj2 referenced this issue from a commit 2026-04-30 08:21:24 -04:00
Phase 1 Streams B–E scaffold + Phase 2 Streams A–C scaffold — 8 new projects with ~70 new tests, all green alongside the 494 v1 IntegrationTests baseline (parity preserved: no v1 tests broken; legacy OtOpcUa.Host untouched). Phase 1 finish: Configuration project (16 entities + 10 enums + DbContext + DesignTimeDbContextFactory + InitialSchema/StoredProcedures/AuthorizationGrants migrations — 8 procs including sp_PublishGeneration with MERGE on ExternalIdReservation per decision #124, sp_RollbackToGeneration cloning rows into a new published generation, sp_ValidateDraft with cross-cluster-namespace + EquipmentUuid-immutability + ZTag/SAPID reservation pre-flight, sp_ComputeGenerationDiff with CHECKSUM-based row signature — plus OtOpcUaNode/OtOpcUaAdmin SQL roles with EXECUTE grants scoped to per-principal-class proc sets and DENY UPDATE/DELETE/INSERT/SELECT on dbo schema); managed DraftValidator covering UNS segment regex, path length, EquipmentUuid immutability across generations, same-cluster namespace binding (decision #122), reservation pre-flight, EquipmentId derivation (decision #125), driver↔namespace compatibility — returning every failing rule in one pass; LiteDB local cache with round-trip + ring pruning + corruption-fast-fail; GenerationApplier with per-entity Added/Removed/Modified diff and dependency-ordered callbacks (namespace → driver → device → equipment → poll-group → tag, Removed before Added); Core project with GenericDriverNodeManager (scaffold for the Phase 2 Galaxy port) and DriverHost lifecycle registry; Server project using Microsoft.Extensions.Hosting BackgroundService replacing TopShelf, with NodeBootstrap that falls back to LiteDB cache when the central DB is unreachable (decision #79); Admin project scaffolded as Blazor Server with Bootstrap 5 sidebar layout, cookie auth, three admin roles (ConfigViewer/ConfigEditor/FleetAdmin), Cluster + Generation services fronting the stored procs. Phase 2 scaffold: Driver.Galaxy.Shared (netstandard2.0) with full MessagePack IPC contract surface — Hello version negotiation, Open/CloseSession, Heartbeat, DiscoverHierarchy + GalaxyObjectInfo/GalaxyAttributeInfo, Read/WriteValues, Subscribe/Unsubscribe/OnDataChange, AlarmSubscribe/Event/Ack, HistoryRead, HostConnectivityStatus, Recycle — plus length-prefixed framing (decision #28) with a 16 MiB cap and thread-safe FrameWriter/FrameReader; Driver.Galaxy.Host (net48) implementing the Tier C cross-cutting protections from driver-stability.md — strict PipeAcl (allow configured server SID only, explicit deny on LocalSystem + Administrators), PipeServer with caller-SID verification via pipe.RunAsClient + WindowsIdentity.GetCurrent and per-process shared-secret Hello, Galaxy-specific MemoryWatchdog (warn at max(1.5×baseline, +200 MB), soft-recycle at max(2×baseline, +200 MB), hard ceiling 1.5 GB, slope ≥5 MB/min over 30-min rolling window), RecyclePolicy (1 soft recycle per hour cap + 03:00 local daily scheduled), PostMortemMmf (1000-entry ring buffer in %ProgramData%\OtOpcUa\driver-postmortem\galaxy.mmf, survives hard crash, readable cross-process), MxAccessHandle : SafeHandle (ReleaseHandle loops Marshal.ReleaseComObject until refcount=0 then calls optional unregister callback), StaPump with responsiveness probe (BlockingCollection dispatcher for Phase 1 — real Win32 GetMessage/DispatchMessage pump slots in with the same semantics when the Galaxy code lift happens), IsExternalInit shim for init setters on .NET 4.8; Driver.Galaxy.Proxy (net10) implementing IDriver + ITagDiscovery forwarding over the IPC channel with MX data-type and security-classification mapping, plus Supervisor pieces — Backoff (5s → 15s → 60s capped, reset-on-stable-run), CircuitBreaker (3 crashes per 5 min opens; 1h → 4h → manual cooldown escalation; sticky alert doesn't auto-clear), HeartbeatMonitor (2s cadence, 3 consecutive misses = host dead per driver-stability.md). Infrastructure: docker SQL Server remapped to host port 14330 to coexist with the native MSSQL14 Galaxy ZB DB instance on 1433; NuGetAuditSuppress applied per-project for two System.Security.Cryptography.Xml advisories that only reach via EF Core Design with PrivateAssets=all (fix ships in 11.0.0-preview); .slnx gains 14 project registrations. Deferred with explicit TODOs in docs/v2/implementation/phase-2-partial-exit-evidence.md: Phase 1 Stream E Admin UI pages (Generations listing + draft-diff-publish, Equipment CRUD with OPC 40010 fields, UNS Areas/Lines tabs, ACLs + permission simulator, Generic JSON config editor, SignalR real-time, Release-Reservation + Merge-Equipment workflows, LDAP login page, AppServer smoke test per decision #142), Phase 2 Stream D (Galaxy MXAccess code lift out of legacy OtOpcUa.Host, dual-service installer, appsettings → DriverConfig migration script, legacy Host deletion — blocked by parity), Phase 2 Stream E (v1 IntegrationTests against v2 topology, Client.CLI walkthrough diff, four 2026-04-13 stability findings regression tests, adversarial review — requires live MXAccess runtime).
Sign in to join this conversation.