using System.Security.Claims; using System.Text.Json; using Akka.Actor; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Logging; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.ManagementService; using ZB.MOM.WW.ScadaBridge.Security; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services; /// /// Default implementation — a thin facade that /// dispatches the secured-write management commands to the central /// ManagementActor through the in-process /// (the same Ask seam the HTTP /management endpoint uses). The actor authorizes /// the command against the supplied , enforces /// separation-of-duties, runs the MxGateway device relay on approve, and writes the /// audit row — none of that is re-implemented here. /// /// /// The current Blazor principal is projected to an so /// the server's role gate and no-self-approval guard run against the real identity. /// The three management response shapes plus any transport fault collapse into a typed /// (or an empty list for queries) so the page can /// render inline outcomes without try/catch noise. /// public sealed class SecuredWriteService : ISecuredWriteService { /// /// camelCase + ignore-cycles, matching ManagementActor.SerializeResult's /// options. is produced with those settings, /// so the deserializer must mirror them to bind every property. /// private static readonly JsonSerializerOptions ResultDeserializerOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; private static readonly TimeSpan AskTimeout = TimeSpan.FromSeconds(30); private readonly ManagementActorHolder _holder; private readonly AuthenticationStateProvider _auth; private readonly ILogger _logger; /// /// Initializes a new instance of the . /// /// Holder for the central ManagementActor reference. /// Authentication state provider used to project the current principal. /// Logger instance. public SecuredWriteService( ManagementActorHolder holder, AuthenticationStateProvider auth, ILogger logger) { _holder = holder ?? throw new ArgumentNullException(nameof(holder)); _auth = auth ?? throw new ArgumentNullException(nameof(auth)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// public Task SubmitAsync( string siteId, string connectionName, string tagPath, string valueJson, string valueType, string? comment, CancellationToken cancellationToken = default) => DispatchAsync( new SubmitSecuredWriteCommand(siteId, connectionName, tagPath, valueJson, valueType, comment), cancellationToken); /// public Task ApproveAsync( long id, string? comment, CancellationToken cancellationToken = default) => DispatchAsync(new ApproveSecuredWriteCommand(id, comment), cancellationToken); /// public Task RejectAsync( long id, string? comment, CancellationToken cancellationToken = default) => DispatchAsync(new RejectSecuredWriteCommand(id, comment), cancellationToken); /// public async Task> ListAsync( string? status, string? siteId, CancellationToken cancellationToken = default) { var response = await SendAsync(new ListSecuredWritesCommand(status, siteId), cancellationToken); if (response is ManagementSuccess success) { var result = JsonSerializer.Deserialize( success.JsonData, ResultDeserializerOptions); return result?.Items ?? Array.Empty(); } // Read path: log + return empty so the queue/history tables render gracefully. _logger.LogWarning( "ListSecuredWrites failed: {Response}", DescribeFailure(response)); return Array.Empty(); } /// /// Dispatches a single mutating command and maps the response (or any fault) to a /// typed . /// private async Task DispatchAsync( object command, CancellationToken cancellationToken) { var response = await SendAsync(command, cancellationToken); switch (response) { case ManagementSuccess success: var dto = JsonSerializer.Deserialize( success.JsonData, ResultDeserializerOptions); return dto is null ? SecuredWriteActionResult.Fail("The server returned an unreadable result.") : SecuredWriteActionResult.Ok(dto); case ManagementUnauthorized unauthorized: return SecuredWriteActionResult.Fail(unauthorized.Message); case ManagementError error: return SecuredWriteActionResult.Fail(error.Error); default: return SecuredWriteActionResult.Fail(DescribeFailure(response)); } } /// /// Wraps in a for the /// current principal and Asks the ManagementActor. Transport faults (timeout, /// actor not yet started, cancellation→propagated) become a synthetic /// so callers handle one response shape. /// private async Task SendAsync(object command, CancellationToken cancellationToken) { var actor = _holder.ActorRef; if (actor is null) { return new ManagementError( string.Empty, "Management service is not ready.", "SERVICE_UNAVAILABLE"); } var user = await BuildAuthenticatedUserAsync(); var envelope = new ManagementEnvelope(user, command, Guid.NewGuid().ToString("N")); try { return await actor.Ask(envelope, AskTimeout, cancellationToken); } catch (OperationCanceledException) { // Caller-initiated cancel (e.g. circuit teardown) — propagate cleanly. throw; } catch (Exception ex) { _logger.LogError(ex, "ManagementActor Ask failed for {Command}", command.GetType().Name); return new ManagementError(string.Empty, ex.Message, "TRANSPORT_ERROR"); } } /// /// Projects the current Blazor to the /// the actor authorizes against — username, /// display name, role claims, and the permitted-site scope claims (mirrors the /// claim set the HTTP endpoint constructs). /// private async Task BuildAuthenticatedUserAsync() { var state = await _auth.GetAuthenticationStateAsync(); var principal = state.User; var username = principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value ?? "unknown"; var displayName = principal.FindFirst(JwtTokenService.DisplayNameClaimType)?.Value ?? username; var roles = principal.FindAll(JwtTokenService.RoleClaimType).Select(c => c.Value).ToArray(); var permittedSiteIds = principal.FindAll(JwtTokenService.SiteIdClaimType).Select(c => c.Value).ToArray(); return new AuthenticatedUser(username, displayName, roles, permittedSiteIds); } /// Renders a fallback description for an unexpected/failure response. private static string DescribeFailure(object response) => response switch { ManagementUnauthorized unauthorized => unauthorized.Message, ManagementError error => error.Error, _ => "Unexpected response from the management service." }; }