Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/CredentialNameClaimSqlServerTests.cs
T
Joseph Doherty d08e59eea7 fix(security): a driver name may not claim another driver's credential segment, nor be renamed off its own (#848)
RawTreeService.CreateDriverAsync / RenameDriverAsync / UpdateDriverAsync run
DriverConfigSaveGate.ValidateNameAsync -> DriverInstanceIdentities.DescribeNameChangeProblemsAsync:
- claim: the new name may not be the instance part (secret: segment or env: part)
  of a reference STORED on another same-type driver's DriverConfig or DeviceConfig -
  naming a driver 'gw' used to fail every sharer's next deploy;
- pin: a driver may not be renamed away from a name its own references are named
  after (unless they also name its id or the new name) - the rename would free that
  name for every other driver's save / Test Connect / browse. Re-pointing to the id
  in the same config-modal save is allowed; a case-only rename is never refused.
Messages name the other driver, id, device and field path, never a value. The
driver + device reads are pinned on SQL Server by a [SqlServerFact].

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-11 06:24:59 -05:00

68 lines
3.3 KiB
C#

using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
/// <summary>
/// The SQL-Server-backed half of <see cref="CredentialNameClaimSavePathTests"/> (lmxopcua#848): the driver + device
/// reads of <see cref="DriverInstanceIdentities.DescribeNameChangeProblemsAsync"/> must translate on the real provider,
/// not only run as LINQ-to-Objects under EF InMemory (the #815 live gate's crash-looping boot came from exactly that gap).
/// </summary>
[Collection(nameof(AdminUiSqlServerCollection))]
public sealed class CredentialNameClaimSqlServerTests(AdminUiSqlServerFixture sql)
{
private static CancellationToken Ct => TestContext.Current.CancellationToken;
[SqlServerFact]
public async Task Create_and_rename_are_checked_against_stored_driver_and_device_references()
{
// 8 hex chars: unique per run in the shared database, short enough for the id columns.
var token = Guid.NewGuid().ToString("N")[..8];
var clusterId = $"C-{token}";
var sharerId = $"DRV-s{token}";
await using (var db = await sql.Factory.CreateDbContextAsync(Ct))
{
db.ServerClusters.Add(new ServerCluster
{
ClusterId = clusterId, Name = $"Cluster {token}", Enterprise = "zb", Site = "warsaw-west",
NodeCount = 1, RedundancyMode = RedundancyMode.None, CreatedBy = "test",
});
db.DriverInstances.Add(new DriverInstance
{
DriverInstanceId = sharerId, ClusterId = clusterId, Name = $"Sharer{token}", DriverType = DriverTypeNames.Galaxy,
DriverConfig = "{\"Gateway\":{\"ApiKeySecretRef\":\"secret:galaxy/gw" + token + "/k\"}}",
});
db.Devices.Add(new Device
{
DeviceId = $"DEV-s{token}", DriverInstanceId = sharerId, Name = "Device1",
DeviceConfig = "{\"AlarmAck\":{\"FallbackPasswordSecretRef\":\"env:OTOPCUA_GALAXY_SHARER" + token.ToUpperInvariant() + "_ACK\"}}",
});
await db.SaveChangesAsync(Ct);
}
var svc = new RawTreeService(sql.Factory);
// Claim (driver-level secret: reference).
var create = await svc.CreateDriverAsync(clusterId, null, $"gw{token}", DriverTypeNames.Galaxy, "{}", Ct);
create.Ok.ShouldBeFalse();
create.Error!.ShouldContain("lmxopcua#848");
// Pin (device-level env: reference named after the sharer's own name).
byte[] rv;
await using (var db = await sql.Factory.CreateDbContextAsync(Ct))
rv = await db.DriverInstances.AsNoTracking().Where(d => d.DriverInstanceId == sharerId).Select(d => d.RowVersion).SingleAsync(Ct);
var rename = await svc.RenameDriverAsync(sharerId, $"Moved{token}", rv, Ct);
rename.Ok.ShouldBeFalse();
rename.Error!.ShouldContain("device 'Device1'");
// Positive control: an unclaimed name creates.
(await svc.CreateDriverAsync(clusterId, null, $"free{token}", DriverTypeNames.Galaxy, "{}", Ct)).Ok.ShouldBeTrue();
}
}