fix(management): reject native alarm source names that are not legal actor names (review 2026-10-21 lane F follow-up to 01/SR-C1)
This commit is contained in:
@@ -113,7 +113,7 @@ The whole folder surface is reachable from the CLI as `template folder list|crea
|
||||
|
||||
- **AddTemplateAttribute** / **UpdateTemplateAttribute** / **DeleteTemplateAttribute**: Manage attributes on a template.
|
||||
- **AddTemplateAlarm** / **UpdateTemplateAlarm** / **DeleteTemplateAlarm**: Manage alarm definitions on a template.
|
||||
- **AddTemplateNativeAlarmSource** / **UpdateTemplateNativeAlarmSource** / **DeleteTemplateNativeAlarmSource**: Manage native alarm source bindings (the read-only OPC UA Alarms & Conditions / MxAccess Gateway mirror) on a template — `Name`, `ConnectionName`, `SourceReference`, optional `ConditionFilter` / `Description`, `IsLocked`. Mutations gated to the **Design** role; handlers write through `ITemplateEngineRepository` directly.
|
||||
- **AddTemplateNativeAlarmSource** / **UpdateTemplateNativeAlarmSource** / **DeleteTemplateNativeAlarmSource**: Manage native alarm source bindings (the read-only OPC UA Alarms & Conditions / MxAccess Gateway mirror) on a template — `Name`, `ConnectionName`, `SourceReference`, optional `ConditionFilter` / `Description`, `IsLocked`. Mutations gated to the **Design** role; handlers write through `ITemplateEngineRepository` directly — so, unlike scripts/alarms/composition slots (checked by `TemplateService`), Add/Update validate `Name` with `ActorNameRules` themselves and reject an illegal Akka actor-name element (space, `/`, leading `$`, …) with a readable error, because the name becomes part of the site actor name `native-alarm-{canonical}` (review 2026-10-21, follow-up to 01/SR-C1).
|
||||
- **ListTemplateNativeAlarmSources** (`TemplateId`): List a template's native alarm source bindings (read-only; any authenticated user).
|
||||
- **AddTemplateScript** / **UpdateTemplateScript** / **DeleteTemplateScript**: Manage scripts on a template.
|
||||
- **AddTemplateComposition** / **DeleteTemplateComposition**: Manage feature module compositions on a template.
|
||||
|
||||
@@ -3309,8 +3309,23 @@ public class ManagementActor : ReceiveActor
|
||||
|
||||
// ── Native alarm source bindings (read-only mirror; repository-direct CRUD) ──
|
||||
|
||||
/// <summary>
|
||||
/// A native alarm source's name becomes part of the site actor name
|
||||
/// <c>native-alarm-{canonical}</c>; an illegal character makes the Instance Actor's
|
||||
/// <c>ActorOf</c> throw at deploy. Scripts, alarms and composition slots are checked by
|
||||
/// <c>TemplateService</c>, but native-source CRUD is repository-direct here, so it applies
|
||||
/// <see cref="ActorNameRules"/> itself (review 2026-10-21, lane F follow-up to 01/SR-C1).
|
||||
/// </summary>
|
||||
private static void ValidateNativeAlarmSourceName(string? name)
|
||||
{
|
||||
var error = ActorNameRules.Describe(name);
|
||||
if (error is not null)
|
||||
throw new ManagementCommandException($"Native alarm source name is invalid: {error}");
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleAddNativeAlarmSource(IServiceProvider sp, AddTemplateNativeAlarmSourceCommand cmd, string user)
|
||||
{
|
||||
ValidateNativeAlarmSourceName(cmd.Name);
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var source = new TemplateNativeAlarmSource(cmd.Name)
|
||||
{
|
||||
@@ -3331,6 +3346,7 @@ public class ManagementActor : ReceiveActor
|
||||
|
||||
private static async Task<object?> HandleUpdateNativeAlarmSource(IServiceProvider sp, UpdateTemplateNativeAlarmSourceCommand cmd, string user)
|
||||
{
|
||||
ValidateNativeAlarmSourceName(cmd.Name);
|
||||
var repo = sp.GetRequiredService<ITemplateEngineRepository>();
|
||||
var source = await repo.GetTemplateNativeAlarmSourceByIdAsync(cmd.NativeAlarmSourceId)
|
||||
?? throw new ManagementCommandException($"Native alarm source {cmd.NativeAlarmSourceId} not found.");
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
using Akka.Actor;
|
||||
using Akka.TestKit.Xunit2;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Review 2026-10-21 (lane F follow-up to 01/SR-C1): a native alarm source's name becomes
|
||||
/// part of the site actor name <c>native-alarm-{canonical}</c>. Template scripts, alarms and
|
||||
/// composition slots are validated by <c>TemplateService</c>, but native-source CRUD is
|
||||
/// repository-direct in the ManagementActor, so it must apply <c>ActorNameRules</c> itself.
|
||||
/// </summary>
|
||||
public class NativeAlarmSourceNameValidationTests : TestKit
|
||||
{
|
||||
private readonly ITemplateEngineRepository _repo = Substitute.For<ITemplateEngineRepository>();
|
||||
private readonly ServiceCollection _services = new ServiceCollection().AddTestConfigurationTransaction();
|
||||
|
||||
/// <summary>Initializes the harness.</summary>
|
||||
public NativeAlarmSourceNameValidationTests()
|
||||
{
|
||||
_services.AddScoped(_ => _repo);
|
||||
_repo.GetTemplateNativeAlarmSourceByIdAsync(4, Arg.Any<CancellationToken>())
|
||||
.Returns(new TemplateNativeAlarmSource("Pressure") { Id = 4, TemplateId = 1 });
|
||||
}
|
||||
|
||||
private IActorRef CreateActor() =>
|
||||
Sys.ActorOf(Props.Create(() => new ManagementActor(
|
||||
_services.BuildServiceProvider(), NullLogger<ManagementActor>.Instance)));
|
||||
|
||||
private static ManagementEnvelope Designer(object command) =>
|
||||
new(new AuthenticatedUser("d", "D", new[] { "Designer" }, Array.Empty<string>()),
|
||||
command, Guid.NewGuid().ToString("N"));
|
||||
|
||||
/// <summary>Adding a source whose name is not a legal actor name is rejected before any write.</summary>
|
||||
[Theory]
|
||||
[InlineData("Tank Pressure")]
|
||||
[InlineData("$system")]
|
||||
[InlineData("a/b")]
|
||||
public void Add_InvalidActorName_IsRejected(string name)
|
||||
{
|
||||
CreateActor().Tell(Designer(new AddTemplateNativeAlarmSourceCommand(1, name, "Opc", "ns=2;s=T01", null, null, false)));
|
||||
|
||||
var error = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Native alarm source name is invalid", error.Error);
|
||||
_repo.DidNotReceive().AddTemplateNativeAlarmSourceAsync(Arg.Any<TemplateNativeAlarmSource>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
/// <summary>Renaming a source to an illegal actor name is rejected before any write.</summary>
|
||||
[Fact]
|
||||
public void Update_RenameToInvalidActorName_IsRejected()
|
||||
{
|
||||
CreateActor().Tell(Designer(new UpdateTemplateNativeAlarmSourceCommand(4, "Tank Pressure", "Opc", "ns=2;s=T01", null, null, false)));
|
||||
|
||||
var error = ExpectMsg<ManagementError>(TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("Native alarm source name is invalid", error.Error);
|
||||
_repo.DidNotReceive().UpdateTemplateNativeAlarmSourceAsync(Arg.Any<TemplateNativeAlarmSource>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user