Commit Graph

154 Commits

Author SHA1 Message Date
Joseph Doherty 7e1af37eb1 feat(auth): MxGateway dashboard adopt ZbClaimTypes + ZbCookieDefaults, keep cookie name (Task 1.5)
- DashboardAuthenticator.CreatePrincipal: emit ZbClaimTypes.Username ("zb:username") with
  the login username, ZbClaimTypes.DisplayName ("zb:displayname") with the display name,
  ZbClaimTypes.Name (== ClaimTypes.Name) for Identity.Name resolution, ZbClaimTypes.Role
  (== ClaimTypes.Role) for IsInRole/[Authorize]. Keep ClaimTypes.NameIdentifier for back-compat
  read-sites; keep mxgateway:ldap_group unchanged (MxGateway-specific, no ZbClaimType for groups).
  ClaimsIdentity built with nameType=ZbClaimTypes.Name, roleType=ZbClaimTypes.Role.
- DashboardServiceCollectionExtensions.AddGatewayDashboard: route cookie hardening through
  ZbCookieDefaults.Apply(requireHttps:true, idleTimeout:8h); set cookie name/path/redirects
  after Apply; PostConfigure still overrides SecurePolicy per RequireHttpsCookie setting.
- DashboardAuthenticatorTests: add AuthenticateAsync_Success_EmitsCanonicalZbClaims asserting
  zb:username, zb:displayname, ZbClaimTypes.Role per role, Identity.Name, and ldap_group preserved.
2026-06-02 06:10:48 -04:00
Joseph Doherty 05009d7370 feat(auth): cut MxGateway API keys over to ZB.MOM.WW.Auth.ApiKeys 0.1.2; keep constraint enforcement+gRPC+CLI on top (Task 1.3) 2026-06-02 02:08:38 -04:00
Joseph Doherty f4dc11bae4 fix(auth): MxGateway 1.2 review fixes — group-claim doc, dedup LdapOptions, 0.1.1 pin 2026-06-02 01:28:57 -04:00
Joseph Doherty c3b466e13d feat(auth): cut MxGateway dashboard LDAP over to ZB.MOM.WW.Auth.Ldap; roles via IGroupRoleMapper (Task 1.2/1.4) 2026-06-02 00:51:10 -04:00
Joseph Doherty 792e3f9445 feat(auth): add IGroupRoleMapper<string> seam (Task 1.1) 2026-06-02 00:31:00 -04:00
Joseph Doherty 3ca2799c90 fix: tighten MxGateway Ldap:Port to 1-65535; catch IOException in path validation
Defect 1: ValidateLdap used AddIfNotPositive for Port, accepting any value
> 0 including 70000. Replaced with builder.Port() from the shared
ZB.MOM.WW.Configuration library, which enforces the 1-65535 TCP range and
emits "MxGateway:Ldap:Port must be between 1 and 65535 (was {value})".

Defect 2: AddIfInvalidPath only caught ArgumentException, NotSupportedException,
and PathTooLongException from Path.GetFullPath. On macOS/Linux a path containing
an embedded null throws IOException, which escaped the catch block and caused
Validate() to throw instead of returning a failure. Added catch (IOException).

Tests: added Validate_Fails_WhenLdapPortIsZero, Validate_Fails_WhenLdapPortExceedsMaximum,
and Validate_Succeeds_WhenLdapEnabledWithValidPort to cover the new range boundary.
2026-06-01 22:45:16 -04:00
Joseph Doherty 459a88b3e7 refactor: adopt ZB.MOM.WW.Configuration in MxGateway (behaviour-preserving) 2026-06-01 18:22:21 -04:00
Joseph Doherty 3965a7741e feat(mxgateway): config-driven OTLP exporter opt-in (default Prometheus) 2026-06-01 16:44:40 -04:00
Joseph Doherty abb2cfb84b feat(mxgateway): normalize metrics — meter ZB.MOM.WW.MxGateway + histograms in seconds 2026-06-01 16:39:56 -04:00
Joseph Doherty 9912389fa1 feat(mxgateway): export GatewayMetrics via AddZbTelemetry + /metrics (name/units unchanged) 2026-06-01 15:53:46 -04:00
Joseph Doherty f1129b969d feat(mxgateway): expose GatewayLogRedactor via shared ILogRedactor seam 2026-06-01 15:49:32 -04:00
Joseph Doherty c51b6f9ce4 feat(mxgateway): adopt AddZbSerilog — MEL→Serilog provider swap (behaviour-preserving) 2026-06-01 15:43:10 -04:00
Joseph Doherty e39972357b config(mxgateway): translate MEL Logging section to Serilog 2026-06-01 15:32:38 -04:00
Joseph Doherty 9ad17e2964 build(mxgateway): reference ZB.MOM.WW.Telemetry + Serilog packages 2026-06-01 15:29:43 -04:00
Joseph Doherty 62ba5e9487 feat: map canonical ZB health tiers; replace bypassing /health/live 2026-06-01 13:44:13 -04:00
Joseph Doherty 136614be94 feat: add AuthStoreHealthCheck readiness probe 2026-06-01 13:33:54 -04:00
Joseph Doherty a912bffad5 build: reference ZB.MOM.WW.Health from the Gitea feed 2026-06-01 13:29:39 -04:00
Joseph Doherty e5c704de69 feat(gateway): add machine FQDN to self-signed cert SANs
Best-effort resolve the host FQDN via Dns.GetHostEntry and add it as a
DNS SAN when it differs (OrdinalIgnoreCase) from the short machine name
and "localhost". SocketException / ArgumentException are caught and
silently skipped so cert generation remains robust when DNS is absent.
2026-06-01 07:52:48 -04:00
Joseph Doherty 4e520f9c0c fix(gateway): delete temp cert file on persist failure
Wrap the WriteAllBytes/Move/HardenPermissions sequence in a try/catch so
that any failure best-effort deletes the hardened .tmp file (which may
already hold PFX/private-key bytes) before rethrowing.  Add a test that
induces a persist failure by pointing SelfSignedCertPath inside a
regular file and asserts no .tmp is left on disk.
2026-06-01 07:45:15 -04:00
Joseph Doherty ddd5721082 fix(gateway): harden self-signed cert persistence and config validation 2026-06-01 07:37:27 -04:00
Joseph Doherty 3775f6bf3b feat(gateway): supply generated cert as Kestrel HTTPS default 2026-06-01 07:30:26 -04:00
Joseph Doherty 330e665f6b fix(gateway): correct ECDSA key usage and dispose CertificateRequest
Drop KeyEncipherment from the self-signed cert's key-usage extension — it
is semantically wrong for ECDSA (RSA key-transport only); DigitalSignature
alone is correct for TLS 1.3 / ECDHE server certs.  CertificateRequest is
unchanged (not IDisposable in .NET 10).  Test now also asserts MachineName,
127.0.0.1 and IPv6 loopback are present in the SAN extension.
2026-06-01 07:27:15 -04:00
Joseph Doherty 77a9108673 feat(gateway): persist/reuse self-signed cert with hardened permissions 2026-06-01 07:23:33 -04:00
Joseph Doherty 192607ab8c fix(gateway): detect Certificate:Thumbprint and cover more KestrelTlsInspector cases 2026-06-01 07:22:24 -04:00
Joseph Doherty fe7d1ce1ec feat(gateway): validate MxGateway:Tls options 2026-06-01 07:19:22 -04:00
Joseph Doherty b8a6695612 feat(gateway): generate self-signed ECDSA cert with SANs 2026-06-01 07:18:39 -04:00
Joseph Doherty 87f86503ef feat(gateway): add MxGateway:Tls options block 2026-06-01 07:08:19 -04:00
Joseph Doherty e912ef960c feat(gateway): detect HTTPS endpoints missing a certificate 2026-06-01 07:08:12 -04:00
Joseph Doherty a155554038 grpc: reuse GalaxyBrowseProjector.ResolveParentId from handler 2026-05-28 15:32:48 -04:00
Joseph Doherty 5abc222c72 galaxy: add by-name and by-path indexes to GalaxyHierarchyIndex 2026-05-28 15:31:56 -04:00
Joseph Doherty f0ec068430 galaxy: add cycle guard to HasMatchingDescendant 2026-05-28 15:30:08 -04:00
Joseph Doherty 76a042d663 grpc: make page_token error strings RPC-name-agnostic 2026-05-28 15:15:40 -04:00
Joseph Doherty 5932fe2fd3 dashboard: surface lazy-load errors via BrowseLoadState.Error 2026-05-28 13:15:26 -04:00
Joseph Doherty 310dfab8b4 dashboard: lazy-load BrowsePage via DashboardBrowseService 2026-05-28 13:10:10 -04:00
Joseph Doherty ba157b4b4f grpc: implement BrowseChildren handler + metadata:read scope 2026-05-28 13:08:45 -04:00
Joseph Doherty 87e22dd529 galaxy: add GalaxyBrowseProjector for direct-children projection 2026-05-28 12:58:07 -04:00
Joseph Doherty d9eaf4b056 galaxy: add ChildrenByParent index for level-at-a-time browse 2026-05-28 12:51:48 -04:00
Joseph Doherty 615b487a77 docs+ui: backfill XML doc comments and finish dashboard layout pass
Adds missing <summary>/<param> XML docs across 99 server, worker, and test
files so CommentChecker reports zero issues (TreatWarningsAsErrors needs the
analyzer clean). Bundles in WIP dashboard work: NavSection extraction,
MainLayout/site.css/js styling alignment, and DashboardOptions/Auth tweaks.
2026-05-27 14:20:10 -04:00
Joseph Doherty ba2b936609 ui: align dashboard styling with ScadaLink master conventions
- Rename DashboardLayout.razor -> MainLayout.razor; dashboard.css -> site.css
- Sidebar 218 -> 220px; add hamburger + Bootstrap collapse for <lg viewports
- Rename .metric-* KPI classes to .agg-* (matches shared theme tokens)
- Rebuild ApiKeysPage create form as card + h6 subsections + bottom Save/Cancel
2026-05-26 01:12:54 -04:00
Joseph Doherty 7fc1955287 Dashboard: handle GET /logout (was 405) by signing out + redirecting to /login
Browsers that navigate directly to /logout via the address bar issued a GET
against a POST-only route and got 405 Method Not Allowed. Logout is
self-destructive, so the GET path can skip antiforgery; the existing POST
form (used by the layout's Sign out button) is unchanged and still
antiforgery-protected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:40:39 -04:00
Joseph Doherty 6bae5ea3a3 Resolve Tests-027..031: flake root cause + coverage gaps
Tests-027  GatewayMetrics exposes its internal Meter; the
           StreamEvents_WhenEventIsWritten_RecordsSendDuration listener
           now filters by ReferenceEquals(instrument.Meter, metrics.Meter)
           instead of Meter.Name, so parallel tests with their own
           GatewayMetrics no longer cross-contaminate the families list.
Tests-028  FakeWorkerClient.Kill now captures LastKillReason;
           SessionManager.KillWorkerAsync tests pin the reason
           propagation end-to-end and cover the blank/null guard. The
           DashboardSessionAdminService kill test pins the literal
           dashboard-admin-kill reason.
Tests-029  Added CloseSessionAsync_BlankSessionId_ReturnsFailure to mirror
           the existing KillWorkerAsync blank-id coverage.
Tests-030  DeleteAsync_WhenStoreRefuses_ReportsFriendlyError renamed and
           extended to assert the dashboard-delete-key audit row with
           Details = not-found-or-active. Added
           DeleteAsync_BlankKeyId_ReturnsFailure.
Tests-031  DashboardSnapshotPublisher reconnect test now measures the
           gap from the first throw inside the fake (firstThrowAt) to
           secondSubscribeAt, isolating Task.Delay from StartAsync /
           scheduling overhead.

All resolved at 2026-05-24; 512/512 gateway tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 09:28:54 -04:00
Joseph Doherty 4d77279e7e Resolve Server-044..050: KillWorker accounting + admin service hardening
Server-044  KillWorkerAsync catch path now calls _metrics.SessionRemoved
            so the open-session gauge does not leak when KillWorker throws.
Server-045  KillWorkerAsync routes through a new
            GatewaySession.KillWorkerWithCloseGateAsync that takes the
            per-session close lock, so concurrent kills count SessionsClosed
            exactly once.
Server-046  CloseSessionCoreAsync's SessionCloseStartedException branch and
            ShutdownAsync's kill fallback both increment SessionsClosed (not
            just the gauge), so the counter and gauge stay consistent.
Server-047  ApiKeysPage.ConfirmPendingAsync holds PendingAction across the
            awaited action and clears it in finally, matching the sessions
            pages.
Server-048  Closed: the 044/045 regression tests cover the previously-
            untested kill paths.
Server-049  IDashboardSessionAdminService + DashboardSessionAdminService
            now carry XML docs that pin the Admin gate, missing-session
            return-Fail semantics, and the dashboard-admin-kill reason.
Server-050  CloseSessionAsync and KillWorkerAsync catch unexpected
            exceptions after the SessionManagerException catches and return
            a friendly Fail; OperationCanceledException tied to the caller
            token still propagates.

All resolved at 2026-05-24; 503/503 gateway tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 08:49:34 -04:00
Joseph Doherty 42b0037376 Dashboard: replace inline fully-qualified type refs with @using
ApiKeysPage and GalaxyPage referenced ApiKeyConstraints and
GalaxyRepositoryOptions by full namespace inside @code. Add the
appropriate @using directive at the top of each file and let the
short type names resolve through that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 07:51:00 -04:00
Joseph Doherty de7639a3e9 Dashboard: fix 500 on / from duplicate endpoint mapping
GatewayApplication.MapGatewayEndpoints registered MapGet("/", ...)
that redirected to /health/live. After the dashboard added a Blazor
component at @page "/", both endpoints matched GET / and the matcher
threw AmbiguousMatchException, surfacing as a 500. The redirect
predated the dashboard home page — drop it so / lands on Overview.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 07:48:43 -04:00
Joseph Doherty 24cc5fd0f0 Dashboard: delete revoked API keys + confirm Rotate/Revoke/Delete
Add IApiKeyAdminStore.DeleteAsync that only deletes already-revoked
rows (active keys must be revoked first so the revoke event lands in
the audit log before the row disappears) and a matching admin-gated
DashboardApiKeyManagementService.DeleteAsync. ApiKeysPage now shows
Delete on revoked rows in place of the old "No actions" stub, and
Rotate/Revoke/Delete all route through ConfirmDialog so each
destructive action requires an explicit confirmation step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 07:30:30 -04:00
Joseph Doherty c5153d68bb Dashboard: fix API keys page stuck on "Loading"
ApiKeysPage.OnInitializedAsync overrode the base without chaining, so
DashboardPageBase's Snapshot seed and hub connect never ran and the
page rendered the null-snapshot empty state forever.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 07:22:38 -04:00
Joseph Doherty 0e56b5befb Dashboard: confirm before Close session / Kill worker
Add a shared ConfirmDialog component and route Sessions, Workers, and
SessionDetails Close/Kill buttons through it. The dialog shows the
target session id and a color-matched confirm button (yellow Close,
red Kill); Cancel dismisses without invoking the admin service.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 07:17:32 -04:00
Joseph Doherty c5e7479ee4 Dashboard: admin-only Close session / Kill worker
Add IDashboardSessionAdminService (Admin-role gate, friendly errors,
audit logging) wrapping a new ISessionManager.KillWorkerAsync that
skips graceful shutdown and cleans up registry/metrics. Sessions,
Workers, and SessionDetails pages render Close / Kill buttons only
when CanManage; the service re-checks the role on every call so
forged clicks return Unauthenticated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 07:10:32 -04:00
Joseph Doherty 327e9c5f94 Resolve Server-031..032 (re-triaged) + Server-038..043
Server-031: re-triaged. The recommended gateway-side
"skip-while-command-in-flight" guard is already in place at
WorkerClient.HeartbeatLoopAsync via WorkerClientOptions.HeartbeatStuckCeiling
(default 75s = 5× HeartbeatGrace). Two regression tests pin the
behaviour. Recommendation #1 (decouple worker-side _writeLock) is a
Worker-module concern (Worker-017 / Worker-023) and out of scope here.

Server-032: re-triaged. Recommendation #2 (rich diagnostic) is already
in EnqueueWorkerEventAsync, with #3 (overflow grace) absorbed by the
TryWrite → WriteAsync-with-timeout fall-through. Test
EnqueueWorkerEvent_WhenChannelFullPastTimeout_FaultsWithRichDiagnostic
pins the diagnostic string. Recommendation #1 (prose contract in
gateway.md / docs) is deferred — outside this pass's edit scope.

Server-038 (Security): EventsHub.SubscribeSession's missing per-session
ACL is documented with a TODO(per-session-acl) and a <remarks> block
explaining the v1 acceptance (any dashboard role can subscribe to any
session — non-secret metadata, redacted value logging). The per-session
ACL design lands in a follow-up once a session-scoped role exists.

Server-039 (Error handling): HubTokenService.Validate now rejects a
deserialized payload where both Name and NameIdentifier are null/empty.
New test file HubTokenServiceTests.cs covers the regression and five
sanity cases. TDD confirmed.

Server-040 (Conventions): MapGroupsToRoles gains a precedence comment
explaining "full literal match first, leading-RDN fallback;
OrdinalIgnoreCase via DashboardOptions.GroupToRole". Documentation-only.

Server-041 (Design adherence): EventStreamService.ProduceEventsAsync
wraps the broadcaster.Publish call in try/catch (Exception). The
producer loop and gRPC stream are no longer at the mercy of the
broadcaster's never-throw discipline. New regression test
StreamEventsAsync_WhenDashboardBroadcasterThrows_StillYieldsEventsAndDoesNotFaultSession.

Server-042 (Performance): DashboardSnapshotPublisher.ExecuteAsync now
mirrors AlarmsHubPublisher's reconnect loop — wraps the await foreach
in a while-not-cancelled, catches general exceptions, and Task.Delays
5s before retrying. An internal ctor accepts a shorter delay for the
test. New test file DashboardSnapshotPublisherTests.cs covers the
throw-then-yield reconnect path and the normal-completion case.

Server-043 (Documentation): HubTokenService class XML doc gains a
<remarks> describing the singleton lifetime, the two consumer scopes
(DashboardHubConnectionFactory scoped, HubTokenAuthenticationHandler
transient), and the thread-safety contract.

Verification: dotnet build src/ZB.MOM.WW.MxGateway.slnx clean
(0 warnings / 0 errors); src/ZB.MOM.WW.MxGateway.Tests 486/486 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 03:18:52 -04:00
Joseph Doherty d692232191 dashboard: clear deferred items — EventsHub publisher + doc refresh
EventsHub publisher (closes the v2.1 follow-up flagged in the previous commit)

EventStreamService now mirrors every MxEvent it forwards to a gRPC client
into the `EventsHub` group for the session. The fan-out goes through a new
singleton `IDashboardEventBroadcaster`:

  * IDashboardEventBroadcaster — abstraction so EventStreamService doesn't
    take a direct dependency on SignalR.
  * DashboardEventBroadcaster — singleton implementation that hands the
    SendAsync to IHubContext<EventsHub> as fire-and-forget. Errors are
    logged at debug and dropped so the source gRPC stream is never
    blocked.

EventStreamService now takes IDashboardEventBroadcaster as a ctor parameter
and calls Publish(sessionId, publicEvent) once per event after sequence
filtering, before the bounded queue write. Test fixtures and the live
integration harness pass NullDashboardEventBroadcaster.Instance so the
broadcaster is a no-op in unit tests.

SessionDetailsPage adds a "Recent events" panel:
  * implements IAsyncDisposable
  * opens a second HubConnection via DashboardHubConnectionFactory targeting
    /hubs/events
  * calls SubscribeSession(SessionId) on Start
  * renders the most recent 50 events in a small table (worker seq, family,
    server/item handle, alarm reference when the event is OnAlarmTransition)
  * shows a live/offline conn-pill driven by HubConnection.Closed /
    Reconnected events

The dashboard mirror is intentionally passive — events appear only while a
gRPC client is also consuming that session's events. Documented as such in
the empty-state copy and in GatewayDashboardDesign.md.

Documentation refresh

Every doc that referenced the retired options (PathBase, RequireAdminScope,
RequiredGroup) and the old API-key-cookie auth flow is updated to describe
the new model:

  * CLAUDE.md — Authentication section now explains LDAP bind +
    GroupToRole + HubToken bearer flow.
  * gateway.md — Dashboard section: root-mounted routes, snapshot/alarms/
    events SignalR hubs, LDAP cookie + bearer scheme.
  * docs/GatewayConfiguration.md — drop PathBase / RequireAdminScope rows,
    add GroupToRole row, append "Authorization policies" and "SignalR hubs"
    subsections describing the three policies and the /hubs/* endpoints.
  * docs/GatewayDashboardDesign.md — hosting model (root mount, new
    endpoint layout), Realtime Updates rewritten as a hub table
    (DashboardSnapshotHub / AlarmsHub / EventsHub with producers, payloads,
    and routing), Authentication And Authorization rewritten around LDAP +
    role mapping + the hub bearer flow, Configuration block updated.
  * docs/GatewayProcessDesign.md — security-section dashboard paragraph
    and the example config block both refreshed to LDAP/role auth.
  * docs/ImplementationPlanGateway.md — dashboard-auth deliverable list
    updated (LDAP bind + GroupToRole + /hubs/token bearer mint replace the
    API-key login flow).
  * docs/GatewayTesting.md — DashboardLdapLiveTests blurb describes the
    GroupToRole fixture (`{ GwAdmin: Admin }`) instead of the retired
    RequiredGroup default; success-path assertion explains the role-claim
    check.

Verification: 475 server tests, 275 worker tests (+ 9 dev-rig skips), 18
integration tests (live MxAccess + LDAP + Galaxy) all pass — including the
live worker smoke test fixture that now constructs EventStreamService with
the new broadcaster parameter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 02:07:30 -04:00