Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSessionAdminService.cs
T
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

137 lines
4.8 KiB
C#

using System.Security.Claims;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.MxGateway.Server.Sessions;
namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
public sealed class DashboardSessionAdminService(
ISessionManager sessionManager,
IHttpContextAccessor httpContextAccessor,
ILogger<DashboardSessionAdminService>? logger = null) : IDashboardSessionAdminService
{
private const string UnauthorizedMessage = "Sign in as an Admin to close sessions or kill workers.";
private const string KillReason = "dashboard-admin-kill";
private readonly ILogger<DashboardSessionAdminService> _logger =
logger ?? NullLogger<DashboardSessionAdminService>.Instance;
public bool CanManage(ClaimsPrincipal user)
{
ArgumentNullException.ThrowIfNull(user);
return user.Identity?.IsAuthenticated == true
&& user.IsInRole(DashboardRoles.Admin);
}
public async Task<DashboardSessionAdminResult> CloseSessionAsync(
ClaimsPrincipal user,
string sessionId,
CancellationToken cancellationToken)
{
if (!CanManage(user))
{
return DashboardSessionAdminResult.Fail(UnauthorizedMessage);
}
if (string.IsNullOrWhiteSpace(sessionId))
{
return DashboardSessionAdminResult.Fail("Session id is required.");
}
string actor = ResolveActor(user);
try
{
SessionCloseResult result = await sessionManager
.CloseSessionAsync(sessionId, cancellationToken)
.ConfigureAwait(false);
_logger.LogInformation(
"Dashboard admin {Actor} closed session {SessionId} from {RemoteAddress}; alreadyClosed={AlreadyClosed}.",
actor,
sessionId,
ResolveRemoteAddress(),
result.AlreadyClosed);
return DashboardSessionAdminResult.Success(
result.AlreadyClosed
? $"Session {sessionId} was already closed."
: $"Session {sessionId} closed.");
}
catch (SessionManagerException exception) when (exception.ErrorCode == SessionManagerErrorCode.SessionNotFound)
{
return DashboardSessionAdminResult.Fail($"Session {sessionId} was not found.");
}
catch (SessionManagerException exception)
{
_logger.LogWarning(
exception,
"Dashboard admin {Actor} close failed for session {SessionId}.",
actor,
sessionId);
return DashboardSessionAdminResult.Fail(
$"Close failed: {exception.Message}");
}
}
public async Task<DashboardSessionAdminResult> KillWorkerAsync(
ClaimsPrincipal user,
string sessionId,
CancellationToken cancellationToken)
{
if (!CanManage(user))
{
return DashboardSessionAdminResult.Fail(UnauthorizedMessage);
}
if (string.IsNullOrWhiteSpace(sessionId))
{
return DashboardSessionAdminResult.Fail("Session id is required.");
}
string actor = ResolveActor(user);
try
{
SessionCloseResult result = await sessionManager
.KillWorkerAsync(sessionId, KillReason, cancellationToken)
.ConfigureAwait(false);
_logger.LogInformation(
"Dashboard admin {Actor} killed worker for session {SessionId} from {RemoteAddress}; alreadyClosed={AlreadyClosed}.",
actor,
sessionId,
ResolveRemoteAddress(),
result.AlreadyClosed);
return DashboardSessionAdminResult.Success(
result.AlreadyClosed
? $"Session {sessionId} was already closed."
: $"Worker for session {sessionId} killed.");
}
catch (SessionManagerException exception) when (exception.ErrorCode == SessionManagerErrorCode.SessionNotFound)
{
return DashboardSessionAdminResult.Fail($"Session {sessionId} was not found.");
}
catch (SessionManagerException exception)
{
_logger.LogWarning(
exception,
"Dashboard admin {Actor} kill failed for session {SessionId}.",
actor,
sessionId);
return DashboardSessionAdminResult.Fail(
$"Kill failed: {exception.Message}");
}
}
private static string ResolveActor(ClaimsPrincipal user)
{
return user.Identity?.Name ?? "<unknown>";
}
private string? ResolveRemoteAddress()
{
return httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();
}
}