fix(management-service): resolve ManagementService-005,008,010,011 — supervision strategy, configured command timeout, remove stale ResolveRoles path; ManagementService-012 deferred

This commit is contained in:
Joseph Doherty
2026-05-16 22:24:03 -04:00
parent 858fe24add
commit dab0056d1b
6 changed files with 200 additions and 32 deletions

View File

@@ -43,6 +43,22 @@ public class ManagementActor : ReceiveActor
Receive<ManagementEnvelope>(HandleEnvelope);
}
/// <summary>
/// Builds the supervision strategy for <see cref="ManagementActor"/>. Per the project's
/// Akka.NET conventions, coordinator-style actors use a Resume-based strategy so a faulted
/// child preserves its state rather than restarting. <see cref="ManagementActor"/> spawns
/// no children today, but declaring the strategy explicitly matches the convention and
/// makes the contract correct ahead of any future worker actors (finding
/// ManagementService-005).
/// </summary>
public static SupervisorStrategy CreateSupervisorStrategy() =>
new OneForOneStrategy(
maxNrOfRetries: -1,
withinTimeRange: System.Threading.Timeout.InfiniteTimeSpan,
decider: Decider.From(_ => Directive.Resume));
protected override SupervisorStrategy SupervisorStrategy() => CreateSupervisorStrategy();
/// <summary>
/// Serializer settings for command results. <see cref="ReferenceHandler.IgnoreCycles"/>
/// keeps EF-backed entity graphs with bidirectional navigation properties from throwing;
@@ -304,29 +320,16 @@ public class ManagementActor : ReceiveActor
DiscardParkedMessageCommand cmd => await HandleDiscardParkedMessage(sp, cmd, user),
DebugSnapshotCommand cmd => await HandleDebugSnapshot(sp, cmd, user),
// Role resolution (for CLI LDAP auth)
ResolveRolesCommand cmd => await HandleResolveRoles(sp, cmd),
// NOTE: ResolveRolesCommand is intentionally NOT dispatched. The two-step
// "ResolveRoles + command" flow is retired — the HTTP endpoint performs LDAP
// auth and role resolution itself before sending a single envelope. Leaving a
// handler would expose role-mapping data to any raw ClusterClient sender with
// no role requirement; the command now falls through to the default below
// (finding ManagementService-011).
_ => throw new NotSupportedException($"Unknown command type: {command.GetType().Name}")
};
}
// ========================================================================
// Role resolution
// ========================================================================
private static async Task<object?> HandleResolveRoles(IServiceProvider sp, ResolveRolesCommand cmd)
{
var roleMapper = new RoleMapper(sp.GetRequiredService<ISecurityRepository>());
var result = await roleMapper.MapGroupsToRolesAsync(cmd.LdapGroups);
return new
{
Roles = result.Roles,
PermittedSiteIds = result.PermittedSiteIds,
IsSystemWideDeployment = result.IsSystemWideDeployment
};
}
// ========================================================================
// Site-scope enforcement
// ========================================================================

View File

@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ScadaLink.Commons.Messages.Management;
using ScadaLink.Security;
@@ -13,7 +14,20 @@ namespace ScadaLink.ManagementService;
public static class ManagementEndpoints
{
private static readonly TimeSpan AskTimeout = TimeSpan.FromSeconds(30);
private static readonly TimeSpan DefaultAskTimeout = TimeSpan.FromSeconds(30);
/// <summary>
/// Resolves the ManagementActor Ask timeout from configuration
/// (finding ManagementService-010). Falls back to <see cref="DefaultAskTimeout"/>
/// when options are absent or the configured value is not strictly positive — a
/// zero/negative timeout would make every management call fail immediately.
/// </summary>
public static TimeSpan ResolveAskTimeout(ManagementServiceOptions? options)
{
if (options is { CommandTimeout: { Ticks: > 0 } configured })
return configured;
return DefaultAskTimeout;
}
public static IEndpointRouteBuilder MapManagementAPI(this IEndpointRouteBuilder endpoints)
{
@@ -101,10 +115,13 @@ public static class ManagementEndpoints
var correlationId = Guid.NewGuid().ToString("N");
var envelope = new ManagementEnvelope(authenticatedUser, command, correlationId);
var askTimeout = ResolveAskTimeout(
context.RequestServices.GetService<IOptions<ManagementServiceOptions>>()?.Value);
object response;
try
{
response = await holder.ActorRef.Ask(envelope, AskTimeout);
response = await holder.ActorRef.Ask(envelope, askTimeout);
}
catch (Exception ex)
{