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>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user