fix(templateengine): reject instance/script/alarm/slot names that are not valid actor names at save (review 2026-10-21 01/SR-C1)

This commit is contained in:
Joseph Doherty
2026-10-22 06:23:47 -04:00
parent 5919a71b8b
commit 7d11a1db8d
4 changed files with 149 additions and 0 deletions
@@ -4,6 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Validation;
using ZB.MOM.WW.ScadaBridge.TemplateEngine;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Services;
@@ -59,6 +60,13 @@ public class InstanceService
if (string.IsNullOrWhiteSpace(uniqueName))
return Result<Instance>.Failure("Instance unique name is required.");
// The unique name becomes the site's Instance Actor name; a character Akka
// rejects would make ActorOf throw inside the Deployment Manager singleton on
// deploy (review 2026-10-21 01/SR-C1). Reject it here with a readable error.
var nameError = ActorNameRules.Describe(uniqueName);
if (nameError != null)
return Result<Instance>.Failure($"Invalid instance unique name: {nameError}");
// Verify template exists
var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
if (template == null)
@@ -4,6 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Validation;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine;
@@ -518,6 +519,10 @@ public class TemplateService
string user,
CancellationToken cancellationToken = default)
{
var nameError = DescribeRuntimeNameError("Alarm", alarm.Name);
if (nameError != null)
return Result<TemplateAlarm>.Failure(nameError);
var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
if (template == null)
return Result<TemplateAlarm>.Failure($"Template with ID {templateId} not found.");
@@ -693,6 +698,10 @@ public class TemplateService
string user,
CancellationToken cancellationToken = default)
{
var nameError = DescribeRuntimeNameError("Script", script.Name);
if (nameError != null)
return Result<TemplateScript>.Failure(nameError);
var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
if (template == null)
return Result<TemplateScript>.Failure($"Template with ID {templateId} not found.");
@@ -869,6 +878,10 @@ public class TemplateService
if (string.IsNullOrWhiteSpace(instanceName))
return Result<TemplateComposition>.Failure("Instance name is required for composition.");
var nameError = DescribeRuntimeNameError("Composition slot", instanceName);
if (nameError != null)
return Result<TemplateComposition>.Failure(nameError);
var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
if (template == null)
return Result<TemplateComposition>.Failure($"Template with ID {templateId} not found.");
@@ -987,6 +1000,10 @@ public class TemplateService
if (string.IsNullOrWhiteSpace(newInstanceName))
return Result<TemplateComposition>.Failure("Slot name is required.");
var nameError = DescribeRuntimeNameError("Composition slot", newInstanceName);
if (nameError != null)
return Result<TemplateComposition>.Failure(nameError);
var composition = await _repository.GetTemplateCompositionByIdAsync(compositionId, cancellationToken);
if (composition == null)
return Result<TemplateComposition>.Failure($"Composition with ID {compositionId} not found.");
@@ -1788,4 +1805,21 @@ public class TemplateService
clone.Compositions.Add(newComp);
return clone;
}
/// <summary>
/// Script, alarm and composition-slot names end up inside site actor names —
/// <c>script-{canonical}</c>, <c>alarm-{canonical}</c>, <c>native-alarm-{canonical}</c>,
/// where the canonical name is the dotted slot path plus the member name. A
/// character Akka rejects makes the Instance Actor's <c>ActorOf</c> throw at
/// deploy, so reject it at save with a readable error (review 2026-10-21 01/SR-C1).
/// Attributes are not actors and are not checked.
/// </summary>
/// <param name="kind">The member kind, for the message.</param>
/// <param name="name">The proposed name.</param>
/// <returns>An error message, or <c>null</c> when the name is usable.</returns>
private static string? DescribeRuntimeNameError(string kind, string? name)
{
var error = ActorNameRules.Describe(name);
return error == null ? null : $"{kind} name is invalid: {error}";
}
}