fix(management-service): resolve ManagementService-004,006,007,013 — PipeTo dispatch, JsonDocument disposal, unified serialization, endpoint tests; re-triage MS-009
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Akka.Actor;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Entities.ExternalSystems;
|
||||
@@ -42,6 +43,25 @@ public class ManagementActor : ReceiveActor
|
||||
Receive<ManagementEnvelope>(HandleEnvelope);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializer settings for command results. <see cref="ReferenceHandler.IgnoreCycles"/>
|
||||
/// keeps EF-backed entity graphs with bidirectional navigation properties from throwing;
|
||||
/// camelCase matches what the CLI / HTTP layer expect (finding ManagementService-007).
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerOptions ResultSerializerOptions = new()
|
||||
{
|
||||
ReferenceHandler = ReferenceHandler.IgnoreCycles,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a command result to JSON using <see cref="System.Text.Json"/> — the same
|
||||
/// serializer the HTTP endpoint uses — with cycle-safe settings.
|
||||
/// </summary>
|
||||
public static string SerializeResult(object? result) =>
|
||||
JsonSerializer.Serialize(result, ResultSerializerOptions);
|
||||
|
||||
private void HandleEnvelope(ManagementEnvelope envelope)
|
||||
{
|
||||
var sender = Sender;
|
||||
@@ -57,27 +77,42 @@ public class ManagementActor : ReceiveActor
|
||||
return;
|
||||
}
|
||||
|
||||
// Process command asynchronously with scoped DI
|
||||
Task.Run(async () =>
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
try
|
||||
{
|
||||
var result = await DispatchCommand(scope.ServiceProvider, envelope.Command, user);
|
||||
var json = JsonConvert.SerializeObject(result, Formatting.None);
|
||||
sender.Tell(new ManagementSuccess(correlationId, json));
|
||||
}
|
||||
catch (SiteScopeViolationException ex)
|
||||
{
|
||||
sender.Tell(new ManagementUnauthorized(correlationId, ex.Message));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Management command {Command} failed (CorrelationId={CorrelationId})",
|
||||
envelope.Command.GetType().Name, correlationId);
|
||||
sender.Tell(new ManagementError(correlationId, ex.Message, "COMMAND_FAILED"));
|
||||
}
|
||||
});
|
||||
// Process the command and pipe the mapped result back to the captured sender.
|
||||
// PipeTo (rather than Task.Run + Tell-from-continuation) is the project's Akka.NET
|
||||
// convention: faults are mapped in the failure continuation, no actor state is
|
||||
// captured in the closure, and synchronous faults in command setup are still mapped.
|
||||
ProcessCommand(envelope, user)
|
||||
.PipeTo(sender,
|
||||
success: result => result,
|
||||
failure: ex => MapFault(ex, correlationId, envelope.Command));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a command on a scoped service provider and maps the result to a management
|
||||
/// response message. Returns a faulted task on error so the PipeTo failure
|
||||
/// continuation maps it uniformly.
|
||||
/// </summary>
|
||||
private async Task<object> ProcessCommand(ManagementEnvelope envelope, AuthenticatedUser user)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var result = await DispatchCommand(scope.ServiceProvider, envelope.Command, user);
|
||||
return new ManagementSuccess(envelope.CorrelationId, SerializeResult(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an exception from command processing to the appropriate management response.
|
||||
/// </summary>
|
||||
private object MapFault(Exception ex, string correlationId, object command)
|
||||
{
|
||||
// PipeTo wraps continuation exceptions; unwrap to find the real cause.
|
||||
var cause = ex is AggregateException agg ? agg.Flatten().InnerException ?? ex : ex;
|
||||
|
||||
if (cause is SiteScopeViolationException scope)
|
||||
return new ManagementUnauthorized(correlationId, scope.Message);
|
||||
|
||||
_logger.LogError(cause, "Management command {Command} failed (CorrelationId={CorrelationId})",
|
||||
command.GetType().Name, correlationId);
|
||||
return new ManagementError(correlationId, cause.Message, "COMMAND_FAILED");
|
||||
}
|
||||
|
||||
private static string? GetRequiredRole(object command) => command switch
|
||||
@@ -345,8 +380,23 @@ public class ManagementActor : ReceiveActor
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to log an audit entry after a successful mutation.
|
||||
/// Logs an audit entry after a successful mutation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Audit-logging contract (finding ManagementService-009). Every mutating operation is
|
||||
/// audited exactly once, by whichever layer owns the write:
|
||||
/// <list type="bullet">
|
||||
/// <item>Handlers that mutate a repository directly (site, area, data-connection,
|
||||
/// external-system, notification, security, API-key, scope-rule) call this helper
|
||||
/// explicitly after the successful change.</item>
|
||||
/// <item>Handlers that delegate to a domain service (<c>TemplateService</c>,
|
||||
/// <c>SharedScriptService</c>, <c>InstanceService</c>, <c>AreaService</c>,
|
||||
/// <c>SiteService</c>, <c>TemplateFolderService</c>, <c>DeploymentService</c>,
|
||||
/// <c>ArtifactDeploymentService</c>) do NOT call this helper — those services own their
|
||||
/// own <see cref="IAuditService"/> dependency and audit internally. Adding an explicit
|
||||
/// <see cref="AuditAsync"/> call in such a handler would double-log.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
private static async Task AuditAsync(IServiceProvider sp, string user, string action, string entityType, string entityId, string entityName, object? afterState)
|
||||
{
|
||||
var auditService = sp.GetRequiredService<IAuditService>();
|
||||
|
||||
Reference in New Issue
Block a user