Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Hubs/AlertHub.cs
Joseph Doherty 3de688f8d6 fix(admin): resolve High code-review findings (Admin-003, Admin-004, Admin-005)
Admin-003 — SignalR hubs were anonymously reachable: an unauthenticated
client could open /hubs/fleet, /hubs/alerts and /hubs/script-log and
stream fleet state, alert detail text and server script-log contents.
Added [Authorize] to FleetStatusHub, AlertHub and ScriptLogHub, and
chained .RequireAuthorization() onto all three MapHub() calls as a
belt-and-braces backstop.

Admin-004 — appsettings.json committed live-looking secrets (the `sa`
ConfigDb password and the LDAP ServiceAccountPassword) in plaintext.
Replaced both with empty placeholders sourced from user-secrets (dev) or
the ConnectionStrings__ConfigDb / Authentication__Ldap__ServiceAccountPassword
environment variables (prod); added a UserSecretsId to the Admin csproj
and a fail-fast guard in Program.cs when ConfigDb is empty/missing.

Admin-005 — Login.razor performed SignInAsync from an interactive Blazor
circuit, where the original HTTP response has long completed so the auth
cookie was not emitted. Rewrote it as a static-rendered plain HTML form
(data-enhance="false") posting to a new AuthEndpoints.MapAuthEndpoints()
minimal-API handler (/auth/login, /auth/logout) that does the LDAP bind,
grant resolution, cookie SignInAsync and redirect while the endpoint
still owns the response. Includes an open-redirect guard on returnUrl.

Added xUnit + Shouldly regression tests: AuthEndpointsTests (login cookie
issuance, failed-bind redirect, open-redirect rejection, logout, anonymous
hub negotiate rejection) and AppSettingsSecretHygieneTests (no committed
secrets). All 26 auth-related tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 06:27:38 -04:00

38 lines
1.2 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace ZB.MOM.WW.OtOpcUa.Admin.Hubs;
/// <summary>
/// Pushes sticky alerts (crash-loop circuit trips, failed applies, reservation-release
/// anomalies) to subscribed admin clients. Alerts don't auto-clear — the operator acks them
/// from the UI via <see cref="AcknowledgeAsync"/>.
/// </summary>
/// <remarks>
/// <see cref="AuthorizeAttribute"/> gates the hub so failure-detail alert text is not pushed
/// to anonymous connections (Admin-003).
/// </remarks>
[Authorize]
public sealed class AlertHub : Hub
{
public const string AllAlertsGroup = "__alerts__";
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, AllAlertsGroup);
await base.OnConnectedAsync();
}
/// <summary>Client-initiated ack. The server side of ack persistence is deferred — v2.1.</summary>
public Task AcknowledgeAsync(string alertId) => Task.CompletedTask;
}
public sealed record AlertMessage(
string AlertId,
string Severity,
string Title,
string Detail,
DateTime RaisedAtUtc,
string? ClusterId,
string? NodeId);