fix(management-service): resolve ManagementService-014..017 — site-scope enforcement on QueryDeployments, atomic override validation, curated fault messages, test coverage
This commit is contained in:
@@ -4,6 +4,7 @@ using System.Text.Json.Serialization;
|
||||
using Akka.Actor;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Entities.Deployment;
|
||||
using ScadaLink.Commons.Entities.ExternalSystems;
|
||||
using ScadaLink.Commons.Entities.InboundApi;
|
||||
using ScadaLink.Commons.Entities.Instances;
|
||||
@@ -128,7 +129,17 @@ public class ManagementActor : ReceiveActor
|
||||
|
||||
_logger.LogError(cause, "Management command {Command} failed (CorrelationId={CorrelationId})",
|
||||
command.GetType().Name, correlationId);
|
||||
return new ManagementError(correlationId, cause.Message, "COMMAND_FAILED");
|
||||
|
||||
// Curated handler failures (ManagementCommandException) carry a message
|
||||
// that is safe to surface to the caller. Any other exception is an
|
||||
// unanticipated fault whose raw .Message can disclose internal detail
|
||||
// (server/database names, constraint names, stack context) — return a
|
||||
// generic message and let the operator correlate via the server log
|
||||
// using the correlation ID (finding ManagementService-016).
|
||||
var clientMessage = cause is ManagementCommandException
|
||||
? cause.Message
|
||||
: $"An internal error occurred (CorrelationId={correlationId}).";
|
||||
return new ManagementError(correlationId, clientMessage, "COMMAND_FAILED");
|
||||
}
|
||||
|
||||
private static string? GetRequiredRole(object command) => command switch
|
||||
@@ -173,6 +184,7 @@ public class ManagementActor : ReceiveActor
|
||||
or SetInstanceAlarmOverrideCommand or DeleteInstanceAlarmOverrideCommand
|
||||
or GetDeploymentDiffCommand
|
||||
or MgmtDeployArtifactsCommand
|
||||
or QueryDeploymentsCommand
|
||||
or RetryParkedMessageCommand or DiscardParkedMessageCommand
|
||||
or DebugSnapshotCommand => "Deployment",
|
||||
|
||||
@@ -303,7 +315,7 @@ public class ManagementActor : ReceiveActor
|
||||
|
||||
// Deployments
|
||||
MgmtDeployArtifactsCommand cmd => await HandleDeployArtifacts(sp, cmd, user.Username),
|
||||
QueryDeploymentsCommand cmd => await HandleQueryDeployments(sp, cmd),
|
||||
QueryDeploymentsCommand cmd => await HandleQueryDeployments(sp, cmd, user),
|
||||
GetDeploymentDiffCommand cmd => await HandleGetDeploymentDiff(sp, cmd, user),
|
||||
|
||||
// Audit Log
|
||||
@@ -428,7 +440,7 @@ public class ManagementActor : ReceiveActor
|
||||
var result = await svc.CreateTemplateAsync(cmd.Name, cmd.Description, cmd.ParentTemplateId, user);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleUpdateTemplate(IServiceProvider sp, UpdateTemplateCommand cmd, string user)
|
||||
@@ -437,7 +449,7 @@ public class ManagementActor : ReceiveActor
|
||||
var result = await svc.UpdateTemplateAsync(cmd.TemplateId, cmd.Name, cmd.Description, cmd.ParentTemplateId, user);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteTemplate(IServiceProvider sp, DeleteTemplateCommand cmd, string user)
|
||||
@@ -446,7 +458,7 @@ public class ManagementActor : ReceiveActor
|
||||
var result = await svc.DeleteTemplateAsync(cmd.TemplateId, user);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleValidateTemplate(IServiceProvider sp, ValidateTemplateCommand cmd)
|
||||
@@ -455,7 +467,7 @@ public class ManagementActor : ReceiveActor
|
||||
|
||||
// Load the template with all members
|
||||
var template = await repo.GetTemplateWithChildrenAsync(cmd.TemplateId)
|
||||
?? throw new InvalidOperationException($"Template with ID {cmd.TemplateId} not found.");
|
||||
?? throw new ManagementCommandException($"Template with ID {cmd.TemplateId} not found.");
|
||||
|
||||
var attributes = await repo.GetAttributesByTemplateIdAsync(cmd.TemplateId);
|
||||
var alarms = await repo.GetAlarmsByTemplateIdAsync(cmd.TemplateId);
|
||||
@@ -527,35 +539,35 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateFolderService>();
|
||||
var result = await svc.CreateFolderAsync(cmd.Name, cmd.ParentFolderId, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleRenameTemplateFolder(IServiceProvider sp, RenameTemplateFolderCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateFolderService>();
|
||||
var result = await svc.RenameFolderAsync(cmd.FolderId, cmd.NewName, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleMoveTemplateFolder(IServiceProvider sp, MoveTemplateFolderCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateFolderService>();
|
||||
var result = await svc.MoveFolderAsync(cmd.FolderId, cmd.NewParentFolderId, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteTemplateFolder(IServiceProvider sp, DeleteTemplateFolderCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateFolderService>();
|
||||
var result = await svc.DeleteFolderAsync(cmd.FolderId, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleMoveTemplateToFolder(IServiceProvider sp, MoveTemplateToFolderCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateService>();
|
||||
var result = await svc.MoveTemplateAsync(cmd.TemplateId, cmd.NewFolderId, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -589,7 +601,7 @@ public class ManagementActor : ReceiveActor
|
||||
EnforceSiteScope(user, cmd.SiteId);
|
||||
var svc = sp.GetRequiredService<InstanceService>();
|
||||
var result = await svc.CreateInstanceAsync(cmd.UniqueName, cmd.TemplateId, cmd.SiteId, cmd.AreaId, user.Username);
|
||||
if (!result.IsSuccess) throw new InvalidOperationException(result.Error);
|
||||
if (!result.IsSuccess) throw new ManagementCommandException(result.Error);
|
||||
await AuditAsync(sp, user.Username, "Create", "Instance", result.Value.Id.ToString(), result.Value.UniqueName, result.Value);
|
||||
return result.Value;
|
||||
}
|
||||
@@ -601,7 +613,7 @@ public class ManagementActor : ReceiveActor
|
||||
var result = await svc.DeployInstanceAsync(cmd.InstanceId, user.Username);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleEnableInstance(IServiceProvider sp, MgmtEnableInstanceCommand cmd, AuthenticatedUser user)
|
||||
@@ -611,7 +623,7 @@ public class ManagementActor : ReceiveActor
|
||||
var result = await svc.EnableInstanceAsync(cmd.InstanceId, user.Username);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDisableInstance(IServiceProvider sp, MgmtDisableInstanceCommand cmd, AuthenticatedUser user)
|
||||
@@ -621,7 +633,7 @@ public class ManagementActor : ReceiveActor
|
||||
var result = await svc.DisableInstanceAsync(cmd.InstanceId, user.Username);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteInstance(IServiceProvider sp, MgmtDeleteInstanceCommand cmd, AuthenticatedUser user)
|
||||
@@ -631,7 +643,7 @@ public class ManagementActor : ReceiveActor
|
||||
var result = await svc.DeleteInstanceAsync(cmd.InstanceId, user.Username);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleSetConnectionBindings(IServiceProvider sp, SetConnectionBindingsCommand cmd, AuthenticatedUser user)
|
||||
@@ -641,18 +653,45 @@ public class ManagementActor : ReceiveActor
|
||||
var result = await svc.SetConnectionBindingsAsync(cmd.InstanceId, cmd.Bindings, user.Username);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleSetInstanceOverrides(IServiceProvider sp, SetInstanceOverridesCommand cmd, AuthenticatedUser user)
|
||||
{
|
||||
await EnforceSiteScopeForInstance(sp, user, cmd.InstanceId);
|
||||
|
||||
// Multi-override apply is all-or-nothing (finding ManagementService-015).
|
||||
// InstanceService.SetAttributeOverrideAsync commits each override
|
||||
// independently, so a mid-batch failure on an invalid attribute would
|
||||
// otherwise leave the instance partially mutated. Validate every
|
||||
// requested attribute up front against the instance's template; only
|
||||
// apply once the whole batch is known to be valid. (A genuine database
|
||||
// fault mid-apply remains theoretically possible without a shared
|
||||
// transaction, but the realistic failure modes — unknown or locked
|
||||
// attribute — are now rejected before any write.)
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var instance = await repo.GetInstanceByIdAsync(cmd.InstanceId)
|
||||
?? throw new ManagementCommandException($"Instance with ID {cmd.InstanceId} not found.");
|
||||
|
||||
var templateAttrs = await repo.GetAttributesByTemplateIdAsync(instance.TemplateId);
|
||||
var attrsByName = templateAttrs.ToDictionary(a => a.Name);
|
||||
foreach (var attrName in cmd.Overrides.Keys)
|
||||
{
|
||||
if (!attrsByName.TryGetValue(attrName, out var templateAttr))
|
||||
throw new ManagementCommandException(
|
||||
$"Attribute '{attrName}' does not exist in template {instance.TemplateId}. " +
|
||||
"No overrides were applied.");
|
||||
if (templateAttr.IsLocked)
|
||||
throw new ManagementCommandException(
|
||||
$"Attribute '{attrName}' is locked and cannot be overridden. No overrides were applied.");
|
||||
}
|
||||
|
||||
var svc = sp.GetRequiredService<InstanceService>();
|
||||
var results = new List<InstanceAttributeOverride>();
|
||||
foreach (var (attrName, overrideValue) in cmd.Overrides)
|
||||
{
|
||||
var result = await svc.SetAttributeOverrideAsync(cmd.InstanceId, attrName, overrideValue, user.Username);
|
||||
if (!result.IsSuccess) throw new InvalidOperationException(result.Error);
|
||||
if (!result.IsSuccess) throw new ManagementCommandException(result.Error);
|
||||
results.Add(result.Value);
|
||||
}
|
||||
return results;
|
||||
@@ -665,7 +704,7 @@ public class ManagementActor : ReceiveActor
|
||||
var result = await svc.AssignToAreaAsync(cmd.InstanceId, cmd.AreaId, user.Username);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleSetInstanceAlarmOverride(IServiceProvider sp, SetInstanceAlarmOverrideCommand cmd, AuthenticatedUser user)
|
||||
@@ -678,7 +717,7 @@ public class ManagementActor : ReceiveActor
|
||||
user.Username);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteInstanceAlarmOverride(IServiceProvider sp, DeleteInstanceAlarmOverrideCommand cmd, AuthenticatedUser user)
|
||||
@@ -689,7 +728,7 @@ public class ManagementActor : ReceiveActor
|
||||
cmd.InstanceId, cmd.AlarmCanonicalName, user.Username);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleListInstanceAlarmOverrides(IServiceProvider sp, ListInstanceAlarmOverridesCommand cmd, AuthenticatedUser user)
|
||||
@@ -706,7 +745,7 @@ public class ManagementActor : ReceiveActor
|
||||
var result = await svc.GetDeploymentComparisonAsync(cmd.InstanceId);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleRetryParkedMessage(IServiceProvider sp, RetryParkedMessageCommand cmd, AuthenticatedUser user)
|
||||
@@ -775,7 +814,7 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<ISiteRepository>();
|
||||
var site = await repo.GetSiteByIdAsync(cmd.SiteId)
|
||||
?? throw new InvalidOperationException($"Site with ID {cmd.SiteId} not found.");
|
||||
?? throw new ManagementCommandException($"Site with ID {cmd.SiteId} not found.");
|
||||
site.Name = cmd.Name;
|
||||
site.Description = cmd.Description;
|
||||
site.NodeAAddress = cmd.NodeAAddress;
|
||||
@@ -796,7 +835,7 @@ public class ManagementActor : ReceiveActor
|
||||
var site = await repo.GetSiteByIdAsync(cmd.SiteId);
|
||||
var instances = await repo.GetInstancesBySiteIdAsync(cmd.SiteId);
|
||||
if (instances.Count > 0)
|
||||
throw new InvalidOperationException(
|
||||
throw new ManagementCommandException(
|
||||
$"Cannot delete site {cmd.SiteId}: it has {instances.Count} instance(s).");
|
||||
await repo.DeleteSiteAsync(cmd.SiteId);
|
||||
await repo.SaveChangesAsync();
|
||||
@@ -876,7 +915,7 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<ISiteRepository>();
|
||||
var conn = await repo.GetDataConnectionByIdAsync(cmd.DataConnectionId)
|
||||
?? throw new InvalidOperationException($"DataConnection with ID {cmd.DataConnectionId} not found.");
|
||||
?? throw new ManagementCommandException($"DataConnection with ID {cmd.DataConnectionId} not found.");
|
||||
conn.Name = cmd.Name;
|
||||
conn.Protocol = cmd.Protocol;
|
||||
conn.PrimaryConfiguration = cmd.PrimaryConfiguration;
|
||||
@@ -931,7 +970,7 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<IExternalSystemRepository>();
|
||||
var def = await repo.GetExternalSystemByIdAsync(cmd.ExternalSystemId)
|
||||
?? throw new InvalidOperationException($"ExternalSystem with ID {cmd.ExternalSystemId} not found.");
|
||||
?? throw new ManagementCommandException($"ExternalSystem with ID {cmd.ExternalSystemId} not found.");
|
||||
def.Name = cmd.Name;
|
||||
def.EndpointUrl = cmd.EndpointUrl;
|
||||
def.AuthType = cmd.AuthType;
|
||||
@@ -982,7 +1021,7 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<IExternalSystemRepository>();
|
||||
var method = await repo.GetExternalSystemMethodByIdAsync(cmd.MethodId)
|
||||
?? throw new InvalidOperationException($"ExternalSystemMethod with ID {cmd.MethodId} not found.");
|
||||
?? throw new ManagementCommandException($"ExternalSystemMethod with ID {cmd.MethodId} not found.");
|
||||
if (cmd.Name != null) method.Name = cmd.Name;
|
||||
if (cmd.HttpMethod != null) method.HttpMethod = cmd.HttpMethod;
|
||||
if (cmd.Path != null) method.Path = cmd.Path;
|
||||
@@ -1037,7 +1076,7 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<INotificationRepository>();
|
||||
var list = await repo.GetNotificationListByIdAsync(cmd.NotificationListId)
|
||||
?? throw new InvalidOperationException($"NotificationList with ID {cmd.NotificationListId} not found.");
|
||||
?? throw new ManagementCommandException($"NotificationList with ID {cmd.NotificationListId} not found.");
|
||||
list.Name = cmd.Name;
|
||||
|
||||
var existingRecipients = await repo.GetRecipientsByListIdAsync(cmd.NotificationListId);
|
||||
@@ -1079,7 +1118,7 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<INotificationRepository>();
|
||||
var config = await repo.GetSmtpConfigurationByIdAsync(cmd.SmtpConfigId)
|
||||
?? throw new InvalidOperationException($"SmtpConfiguration with ID {cmd.SmtpConfigId} not found.");
|
||||
?? throw new ManagementCommandException($"SmtpConfiguration with ID {cmd.SmtpConfigId} not found.");
|
||||
config.Host = cmd.Server;
|
||||
config.Port = cmd.Port;
|
||||
config.AuthType = cmd.AuthMode;
|
||||
@@ -1114,7 +1153,7 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<ISecurityRepository>();
|
||||
var mapping = await repo.GetMappingByIdAsync(cmd.MappingId)
|
||||
?? throw new InvalidOperationException($"RoleMapping with ID {cmd.MappingId} not found.");
|
||||
?? throw new ManagementCommandException($"RoleMapping with ID {cmd.MappingId} not found.");
|
||||
mapping.LdapGroupName = cmd.LdapGroupName;
|
||||
mapping.Role = cmd.Role;
|
||||
await repo.UpdateMappingAsync(mapping);
|
||||
@@ -1168,15 +1207,45 @@ public class ManagementActor : ReceiveActor
|
||||
var result = await svc.DeployToAllSitesAsync(user);
|
||||
return result.IsSuccess
|
||||
? result.Value
|
||||
: throw new InvalidOperationException(result.Error);
|
||||
: throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleQueryDeployments(IServiceProvider sp, QueryDeploymentsCommand cmd)
|
||||
private static async Task<object?> HandleQueryDeployments(IServiceProvider sp, QueryDeploymentsCommand cmd, AuthenticatedUser user)
|
||||
{
|
||||
var repo = sp.GetRequiredService<IDeploymentManagerRepository>();
|
||||
|
||||
// Instance-scoped query: enforce against the target instance's site
|
||||
// before reading its deployment history (finding ManagementService-014).
|
||||
if (cmd.InstanceId.HasValue)
|
||||
{
|
||||
await EnforceSiteScopeForInstance(sp, user, cmd.InstanceId.Value);
|
||||
return await repo.GetDeploymentsByInstanceIdAsync(cmd.InstanceId.Value);
|
||||
return await repo.GetAllDeploymentRecordsAsync();
|
||||
}
|
||||
|
||||
// Unfiltered query: a site-scoped Deployment user must only see records
|
||||
// for instances at sites within their scope. DeploymentRecord has no
|
||||
// SiteId, so resolve each record's instance to its site and filter
|
||||
// (mirrors the HandleListInstances / HandleListSites filter pattern).
|
||||
var records = await repo.GetAllDeploymentRecordsAsync();
|
||||
if (user.PermittedSiteIds.Length == 0 || user.Roles.Contains("Admin", StringComparer.OrdinalIgnoreCase))
|
||||
return records;
|
||||
|
||||
var permittedIds = new HashSet<string>(user.PermittedSiteIds);
|
||||
var templateRepo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var instanceSiteCache = new Dictionary<int, int?>();
|
||||
var scoped = new List<DeploymentRecord>();
|
||||
foreach (var record in records)
|
||||
{
|
||||
if (!instanceSiteCache.TryGetValue(record.InstanceId, out var siteId))
|
||||
{
|
||||
var instance = await templateRepo.GetInstanceByIdAsync(record.InstanceId);
|
||||
siteId = instance?.SiteId;
|
||||
instanceSiteCache[record.InstanceId] = siteId;
|
||||
}
|
||||
if (siteId.HasValue && permittedIds.Contains(siteId.Value.ToString()))
|
||||
scoped.Add(record);
|
||||
}
|
||||
return scoped;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -1223,7 +1292,7 @@ public class ManagementActor : ReceiveActor
|
||||
IsLocked = cmd.IsLocked
|
||||
};
|
||||
var result = await svc.AddAttributeAsync(cmd.TemplateId, attr, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleUpdateAttribute(IServiceProvider sp, UpdateTemplateAttributeCommand cmd, string user)
|
||||
@@ -1238,14 +1307,14 @@ public class ManagementActor : ReceiveActor
|
||||
IsLocked = cmd.IsLocked
|
||||
};
|
||||
var result = await svc.UpdateAttributeAsync(cmd.AttributeId, attr, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteAttribute(IServiceProvider sp, DeleteTemplateAttributeCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateService>();
|
||||
var result = await svc.DeleteAttributeAsync(cmd.AttributeId, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleAddAlarm(IServiceProvider sp, AddTemplateAlarmCommand cmd, string user)
|
||||
@@ -1260,7 +1329,7 @@ public class ManagementActor : ReceiveActor
|
||||
IsLocked = cmd.IsLocked
|
||||
};
|
||||
var result = await svc.AddAlarmAsync(cmd.TemplateId, alarm, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleUpdateAlarm(IServiceProvider sp, UpdateTemplateAlarmCommand cmd, string user)
|
||||
@@ -1275,14 +1344,14 @@ public class ManagementActor : ReceiveActor
|
||||
IsLocked = cmd.IsLocked
|
||||
};
|
||||
var result = await svc.UpdateAlarmAsync(cmd.AlarmId, alarm, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteAlarm(IServiceProvider sp, DeleteTemplateAlarmCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateService>();
|
||||
var result = await svc.DeleteAlarmAsync(cmd.AlarmId, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleAddScript(IServiceProvider sp, AddTemplateScriptCommand cmd, string user)
|
||||
@@ -1297,7 +1366,7 @@ public class ManagementActor : ReceiveActor
|
||||
ReturnDefinition = cmd.ReturnDefinition
|
||||
};
|
||||
var result = await svc.AddScriptAsync(cmd.TemplateId, script, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleUpdateScript(IServiceProvider sp, UpdateTemplateScriptCommand cmd, string user)
|
||||
@@ -1312,28 +1381,28 @@ public class ManagementActor : ReceiveActor
|
||||
ReturnDefinition = cmd.ReturnDefinition
|
||||
};
|
||||
var result = await svc.UpdateScriptAsync(cmd.ScriptId, script, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteScript(IServiceProvider sp, DeleteTemplateScriptCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateService>();
|
||||
var result = await svc.DeleteScriptAsync(cmd.ScriptId, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleAddComposition(IServiceProvider sp, AddTemplateCompositionCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateService>();
|
||||
var result = await svc.AddCompositionAsync(cmd.TemplateId, cmd.ComposedTemplateId, cmd.InstanceName, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteComposition(IServiceProvider sp, DeleteTemplateCompositionCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<TemplateService>();
|
||||
var result = await svc.DeleteCompositionAsync(cmd.CompositionId, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -1356,21 +1425,21 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var svc = sp.GetRequiredService<SharedScriptService>();
|
||||
var result = await svc.CreateSharedScriptAsync(cmd.Name, cmd.Code, cmd.ParameterDefinitions, cmd.ReturnDefinition, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleUpdateSharedScript(IServiceProvider sp, UpdateSharedScriptCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<SharedScriptService>();
|
||||
var result = await svc.UpdateSharedScriptAsync(cmd.SharedScriptId, cmd.Code, cmd.ParameterDefinitions, cmd.ReturnDefinition, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteSharedScript(IServiceProvider sp, DeleteSharedScriptCommand cmd, string user)
|
||||
{
|
||||
var svc = sp.GetRequiredService<SharedScriptService>();
|
||||
var result = await svc.DeleteSharedScriptAsync(cmd.SharedScriptId, user);
|
||||
return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error);
|
||||
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
@@ -1403,7 +1472,7 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<IExternalSystemRepository>();
|
||||
var def = await repo.GetDatabaseConnectionByIdAsync(cmd.DatabaseConnectionId)
|
||||
?? throw new InvalidOperationException($"DatabaseConnection with ID {cmd.DatabaseConnectionId} not found.");
|
||||
?? throw new ManagementCommandException($"DatabaseConnection with ID {cmd.DatabaseConnectionId} not found.");
|
||||
def.Name = cmd.Name;
|
||||
def.ConnectionString = cmd.ConnectionString;
|
||||
await repo.UpdateDatabaseConnectionAsync(def);
|
||||
@@ -1457,7 +1526,7 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<IInboundApiRepository>();
|
||||
var method = await repo.GetApiMethodByIdAsync(cmd.ApiMethodId)
|
||||
?? throw new InvalidOperationException($"ApiMethod with ID {cmd.ApiMethodId} not found.");
|
||||
?? throw new ManagementCommandException($"ApiMethod with ID {cmd.ApiMethodId} not found.");
|
||||
method.Script = cmd.Script;
|
||||
method.TimeoutSeconds = cmd.TimeoutSeconds;
|
||||
method.ParameterDefinitions = cmd.ParameterDefinitions;
|
||||
@@ -1489,7 +1558,7 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<IInboundApiRepository>();
|
||||
var key = await repo.GetApiKeyByIdAsync(cmd.ApiKeyId)
|
||||
?? throw new InvalidOperationException($"ApiKey with ID {cmd.ApiKeyId} not found.");
|
||||
?? throw new ManagementCommandException($"ApiKey with ID {cmd.ApiKeyId} not found.");
|
||||
key.IsEnabled = cmd.IsEnabled;
|
||||
await repo.UpdateApiKeyAsync(key);
|
||||
await repo.SaveChangesAsync();
|
||||
@@ -1530,7 +1599,7 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var area = await repo.GetAreaByIdAsync(cmd.AreaId)
|
||||
?? throw new InvalidOperationException($"Area with ID {cmd.AreaId} not found.");
|
||||
?? throw new ManagementCommandException($"Area with ID {cmd.AreaId} not found.");
|
||||
area.Name = cmd.Name;
|
||||
await repo.UpdateAreaAsync(area);
|
||||
await repo.SaveChangesAsync();
|
||||
@@ -1576,13 +1645,13 @@ public class ManagementActor : ReceiveActor
|
||||
{
|
||||
var instanceRepo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var instance = await instanceRepo.GetInstanceByIdAsync(cmd.InstanceId)
|
||||
?? throw new InvalidOperationException($"Instance {cmd.InstanceId} not found.");
|
||||
?? throw new ManagementCommandException($"Instance {cmd.InstanceId} not found.");
|
||||
|
||||
EnforceSiteScope(user, instance.SiteId);
|
||||
|
||||
var siteRepo = sp.GetRequiredService<ISiteRepository>();
|
||||
var site = await siteRepo.GetSiteByIdAsync(instance.SiteId)
|
||||
?? throw new InvalidOperationException($"Site {instance.SiteId} not found.");
|
||||
?? throw new ManagementCommandException($"Site {instance.SiteId} not found.");
|
||||
|
||||
var commService = sp.GetRequiredService<CommunicationService>();
|
||||
var request = new DebugSnapshotRequest(instance.UniqueName, Guid.NewGuid().ToString("N"));
|
||||
@@ -1597,3 +1666,16 @@ public class SiteScopeViolationException : Exception
|
||||
{
|
||||
public SiteScopeViolationException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thrown by a command handler to signal a curated, caller-safe failure (finding
|
||||
/// ManagementService-016). Its <see cref="Exception.Message"/> is intended to be
|
||||
/// surfaced verbatim to the HTTP/CLI caller — e.g. a validation result or a
|
||||
/// "not found" message. Unanticipated exceptions (database faults, parse errors,
|
||||
/// null-reference, etc.) must NOT be this type, so that <c>MapFault</c> can return
|
||||
/// a generic message for them and avoid leaking internal detail.
|
||||
/// </summary>
|
||||
public class ManagementCommandException : Exception
|
||||
{
|
||||
public ManagementCommandException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user