24cc5fd0f009063c3cfd8e957da5ae2efa84973c
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
d48099f0d0 |
Resolve Tests-025..026
Tests-025 (Conventions): Extracted the previously-duplicated NullDashboardEventBroadcaster into TestSupport/NullDashboardEventBroadcaster.cs (singleton Instance, private ctor). The two nested copies in EventStreamServiceTests and GatewayEndToEndFakeWorkerSmokeTests were removed; both files now use the shared type via 'using ZB.MOM.WW.MxGateway.Tests.TestSupport;'. The Server-041 regression test's ThrowingDashboardEventBroadcaster is intentionally left nested — single-file usage doesn't warrant promotion to TestSupport. The third copy in IntegrationTests/WorkerLiveMxAccessSmokeTests was handled by IntegrationTests-024 in its own commit. Tests-026 (Testing coverage): Added a new RecordingDashboardEventBroadcaster test double in TestSupport — a thread-safe (ConcurrentQueue<DashboardEventCapture>) recorder. New fixture StreamEventsAsync_PublishesEachEventToDashboardBroadcaster in EventStreamServiceTests pushes two events through the fake session and asserts the broadcaster received both with the correct sessionId and WorkerSequence. TDD red→green confirmed: the deliberately-wrong "Expected 3, Actual 2" red phase proved the recording fake was actually invoked by the production code path. Verification: 486/486 server tests passing (485 previous + 1 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bd1d1f1c0e |
Resolve Contracts-016..017
Contracts-016 (Conventions): QueryActiveAlarmsRequest.session_id header replaced with the unambiguous "Clients may leave session_id empty; the gateway currently ignores it and serves the session-less central-monitor cache. A future version may use it to scope the snapshot to one session." Removes the ambiguity that the prior "reserved for future use" wording introduced. Contracts-017 (Documentation): The rpc QueryActiveAlarms comment now includes the alarm_filter_prefix description: "QueryActiveAlarmsRequest.alarm_filter_prefix optionally narrows the snapshot to alarms whose alarm_full_reference starts with the given prefix; an empty prefix returns the full set." Both are proto-comment-only changes — no wire-format impact, no field renumbering, and the regenerated MxaccessGateway.cs / MxaccessGatewayGrpc.cs carry only the doc-comment delta. Added the additive-only regression guard QueryActiveAlarmsRequest_PinsFieldNumbersAndRoundTripsPrefixFilter to ProtobufContractRoundTripTests — pins session_id=1 / client_correlation_id=2 / alarm_filter_prefix=3 by descriptor lookup and round-trips the message with and without the filter populated. Verification: dotnet build src/ZB.MOM.WW.MxGateway.slnx clean; ProtobufContractRoundTripTests 40/40 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
65943597d4 |
dashboard: side-rail layout + SignalR push hubs (snapshot, alarms, events)
Layout
------
DashboardLayout.razor replaces the inline header nav with a left side rail
modelled on the OtOpcUa admin (Dashboard B). The top bar keeps only the
brand, breadcrumb, and signed-in status pill; navigation moves into a
fixed-width 218px rail with grouped section eyebrows (Overview,
Runtime, Galaxy, Admin) and a Session footer carrying the user name,
role claims, and a Sign-out button. dashboard.css gains the
`.app-shell` flex container, `.side-rail` column, `.rail-eyebrow`,
`.rail-link[.active]`, `.rail-foot`, `.rail-user`, `.rail-roles`, and
`.rail-btn` rules (all driven by the existing theme.css tokens, no new
hard-coded colours).
SignalR (push)
--------------
Adds three hubs under `Dashboard/Hubs/`, all gated by the
`HubClientsPolicy` registered in the previous commit:
* DashboardSnapshotHub (/hubs/snapshot)
Broadcasts the full DashboardSnapshot on every change. Sends the
current snapshot to a new caller in OnConnectedAsync so the first
paint is immediate.
* AlarmsHub (/hubs/alarms)
Connected clients auto-join the `__alarms__` group. Receives
AlarmFeedMessage values (active_alarm / snapshot_complete /
transition) re-broadcast from the gateway's central alarm monitor.
* EventsHub (/hubs/events)
Per-session push surface. Clients call SubscribeSession(sessionId)
to join `session:{id}`. The publisher side is intentionally a
follow-up — the snapshot hub already carries recent-events
rollups; a dedicated MxEvent broadcaster on EventStreamService
will plug into this hub's group convention.
Two BackgroundService publishers wire server-side data sources to the
hubs:
* DashboardSnapshotPublisher subscribes to
`IDashboardSnapshotService.WatchSnapshotsAsync` and forwards every
snapshot to all connected hub clients.
* AlarmsHubPublisher subscribes to `IGatewayAlarmService.StreamAsync`
(no filter) and forwards every AlarmFeedMessage to the
`__alarms__` group, reconnecting with a 5-second backoff if the
stream faults.
Connection + auth plumbing
--------------------------
* `GET /hubs/token` issues a fresh data-protected bearer token
bound to the calling user's identity and roles. Gated by the
cookie-only ViewerPolicy so a Blazor circuit (cookie-authenticated)
can mint a token, but a hub bearer cannot self-bootstrap a new
one.
* DashboardHubConnectionFactory (scoped) is the client-side helper
Razor pages inject. It builds a HubConnection with an
AccessTokenProvider that calls HubTokenService.Issue on every
(re)connect — keeps the connection alive across cookie refresh
boundaries.
Pull → push refactor
--------------------
DashboardPageBase no longer drives its own `WatchSnapshotsAsync`
async-foreach loop. It now:
1. seeds Snapshot synchronously from `IDashboardSnapshotService.GetSnapshot()`
so the first render is non-empty;
2. opens a `DashboardSnapshotHub` connection via the connection
factory;
3. updates Snapshot + triggers StateHasChanged on each
`SnapshotUpdated` push.
The hub connection is best-effort: if SignalR can't start, the
synchronous snapshot seed keeps the UI populated. SignalR's
WithAutomaticReconnect handles the recovery path.
Package
-------
Adds `Microsoft.AspNetCore.SignalR.Client` 10.0.0 to the server csproj
so the in-process Blazor pages can open hub connections back to their
own hosting process.
Verification: 475 server tests (+ 2 new
`DashboardHubsRegistrationTests` that pin the hub negotiate endpoints
and the singleton/scoped DI shape), 275 worker tests (+ 9 dev-rig
skips), 18 integration tests (live MxAccess + LDAP + Galaxy) all pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
27ed65114e |
dashboard: role-based LDAP auth + hub bearer scheme, drop PathBase
Restructure dashboard auth around LDAP-driven Admin/Viewer roles, add a
bearer scheme so SignalR hubs (next commit) can authenticate without
forwarding the HttpOnly browser cookie, and mount the dashboard at the
host root instead of a configurable `/dashboard` prefix.
Configuration changes (breaking):
- `MxGateway:Dashboard:PathBase` removed — the dashboard now serves at `/`.
- `MxGateway:Dashboard:RequireAdminScope` removed — role checks replace
the single admin-scope claim.
- `MxGateway:Ldap:RequiredGroup` removed — replaced by `MxGateway:Dashboard:GroupToRole`,
a map from LDAP group name to dashboard role. Legal role values:
`Admin` and `Viewer`. Users whose LDAP groups don't intersect this
map are rejected at login (the existing fail-closed contract).
- appsettings.json ships a default mapping `{ GwAdmin: Admin, GwReader: Viewer }`.
Auth model:
- DashboardRoles: new static class with `Admin` and `Viewer` constants.
- DashboardAuthenticator.AuthenticateAsync: after LDAP bind, maps the
user's groups through `DashboardOptions.GroupToRole` and emits one
`ClaimTypes.Role` claim per resolved role. Empty result → login fails.
- DashboardAuthorizationRequirement now carries `RequiredRoles`; static
presets `AnyDashboardRole` (Viewer ∨ Admin) and `AdminOnly`.
- DashboardAuthorizationHandler checks `IsInRole` against the
requirement's role list instead of the old scope claim. The
`AuthenticationMode.Disabled` and `AllowAnonymousLocalhost` bypasses
are preserved.
- DashboardApiKeyAuthorization.CanManage now requires the `Admin` role
(was: required LDAP group membership). The constructor's IOptions
parameter is gone.
Policies / schemes:
- DashboardAuthenticationDefaults gains `ViewerPolicy`, `AdminPolicy`,
`HubClientsPolicy`, and `HubAuthenticationScheme`. The legacy
`AuthorizationPolicy` and `ScopeClaimType` constants are removed.
- DashboardServiceCollectionExtensions registers all three policies,
adds the cookie scheme and the HubToken bearer scheme side by side,
calls `AddSignalR()`, and hard-codes the cookie's login/logout/denied
paths to root-relative `/login` etc.
Hub bearer infrastructure (no hubs wired yet — next commit):
- HubTokenService: mints time-limited data-protected JSON tokens
carrying the user's name, NameIdentifier, and roles. 30-minute
lifetime, purpose `ZB.MOM.WW.MxGateway.Dashboard.HubToken.v1`.
- HubTokenAuthenticationHandler: validates the token from
`Authorization: Bearer …` or `?access_token=…` (WebSocket upgrade
query string) and rebuilds the principal.
Endpoint mapping:
- DashboardEndpointRouteBuilderExtensions drops the `MapGroup(pathBase)`
wrapper. Login/logout/denied and Razor component routes are now
mounted at `/`. The login form posts to `/login`. Razor components
require the new `ViewerPolicy`.
- All page `@page "/dashboard/X"` dual-route directives are removed —
pages live at their canonical roots (`@page "/"`, `@page "/sessions"`, …).
- App.razor and DashboardLayout.razor drop their PathBase computations.
EffectiveLdapConfiguration drops `RequiredGroup`; EffectiveDashboardConfiguration
drops `PathBase`/`RequireAdminScope` and gains `GroupToRole`. SettingsPage
renders the role mapping in place of the retired fields.
Tests updated:
- DashboardAuthenticatorTests: covers the new GroupToRole mapping
(short name + DN + multi-role).
- DashboardAuthorizationHandlerTests: split into Viewer-policy and
Admin-policy cases.
- DashboardApiKeyAuthorizationTests, DashboardApiKeyManagementServiceTests:
authorized principal now carries the `Admin` role claim.
- DashboardCookieOptionsTests: expects root-relative login/logout paths.
- GatewayApplicationTests: dashboard component routes registered at `/`,
`/sessions`, … and gated by `ViewerPolicy`. Filter on
`ComponentTypeMetadata` to ignore minimal-API endpoints sharing `/`.
- GatewayOptionsTests + Validator: drop PathBase / RequireAdminScope /
RequiredGroup assertions; add a `GroupToRole` value-validation case.
- DashboardLdapLiveTests: provides the default `GwAdmin` → `Admin`
mapping so the live LDAP bind resolves to a role.
Verification: 473 server tests, 275 worker tests (+9 dev-rig skips), 18
integration tests (live MxAccess + LDAP + Galaxy) all pass.
This commit is intentionally UI-neutral. The sidebar layout and the
SignalR hubs that consume the new HubToken scheme land in a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
dc9c0c950c |
rename: prefix gateway projects/namespaces with ZB.MOM.WW + sln→slnx
Apply the ZB.MOM.WW. prefix to all gateway-side projects, folders,
.csproj/.sln contents, C# namespaces, using directives, generated proto
C# (csharp_namespace + checked-in generated files), InternalsVisibleTo
attributes, project-name string literals (LoadProject, .sln lookups,
worker exe paths, staticwebassets manifest), and the install/script/doc
references that point at any of the above. Migrate the solution from
.sln to .slnx via `dotnet sln migrate` and delete the old file.
External-runtime identifiers are intentionally NOT prefixed so external
configuration keeps working:
- GatewayMetrics.cs MeterName ("MxGateway.Server")
- DashboardAuthenticationDefaults Scheme/Policy ("MxGateway.Dashboard")
- GatewayRequestLoggingMiddleware logger category ("MxGateway.Request")
- StaRuntime thread name ("MxGateway.Worker.STA")
- appsettings.json root section "MxGateway" + env-var prefix
MxGateway__... and secret-name MxGateway:ApiKeyPepper
- C:\ProgramData\MxGateway\ data dir paths
Also fixes two tests that were not rename-related but became visible
while validating the rename:
- WorkerLiveMxAccessSmokeTests.ShutDownAsync: cancellation that the
gateway service correctly maps to RpcException(Cancelled) per gRPC
convention was being misclassified as a stream fault. Added a sibling
catch on RpcException with StatusCode.Cancelled.
- IntegrationTestEnvironment.ResolveRepositoryRoot: extracted IsRepositoryRoot
and made it accept either a .git marker OR a .sln/.slnx next to src/
so the worker-exe walker works in non-git working copies.
clients/proto/proto-inputs.json's protoRoot updated to point at
src/ZB.MOM.WW.MxGateway.Contracts/Protos.
Verified by `dotnet build` and a full `dotnet test` of the .slnx with
MXGATEWAY_RUN_LIVE_{MXACCESS,LDAP,GALAXY}_TESTS=1:
Tests: 472/472 pass
Worker.Tests: 280/280 pass (4 dev-rig [Fact(Skip=...)] skipped)
IntegrationTests: 18/18 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|