63331b0416
#835: inside one driver namespace, a secret:<ns>/<X>/... reference whose first segment names ANOTHER existing same-type driver instance (id or name, case-insensitive; another-instance match beats an own-name match, so renaming one's driver after a victim's unlocks nothing) is refused at save (DriverConfigSaveGate.ValidateAsync), deploy (CredentialReferenceOtherInstance, Heidenhain included), Test Connect (TestDriverConnect.DriverInstanceId, ConfigDb read off the mailbox) and live browse (BrowserSessionService, fail-closed without a ConfigDb). One rule: DriverCredentialPolicy.DescribeOtherInstanceProblems + DriverInstanceIdentities. Shared names and the driver's own id stay valid; env: is not instance-scoped. Running drivers are unchanged (warn-only window: the deploy gate and the upgrade preflight / /raw banner list them). #836: OpcUaClient UserCertificatePath is a relative path inside Drivers:OpcUaClient:UserCertificateDirectory (default <LocalAppData>/OtOpcUa/pki/user-certificates). Syntactic rule (CredentialFilePaths, a DriverCredentialPolicy path field) at every #815 seam; at load time ResolveSecretRefsAsync joins, canonicalises and requires lexical AND link-resolved containment before the PFX password is resolved, and LoadCertificate refuses an unresolved path. A running driver's pre-#836 absolute path keeps loading with a rate-limited warning (LegacyCredentialReferenceTracker.ReportPath). Co-Authored-By: Claude <noreply@anthropic.com>
131 lines
6.2 KiB
C#
131 lines
6.2 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
|
|
|
/// <summary>
|
|
/// lmxopcua#835 (a credential reference naming ANOTHER driver instance's secret) and #836 (the OpcUaClient
|
|
/// user-certificate path confined to a node directory) at the SAVE path (RawTreeService): nothing reaches the
|
|
/// config DB, through driver and device create + update.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class CredentialInstanceAndPathSavePathTests
|
|
{
|
|
private static CancellationToken Ct => TestContext.Current.CancellationToken;
|
|
|
|
private static string Galaxy(string reference) =>
|
|
"""{"Gateway":{"Endpoint":"http://evil:5001","ApiKeySecretRef":"REF"}}""".Replace("REF", reference);
|
|
|
|
private static async Task<(RawTreeService Svc, string Db, string VictimId)> WithVictimAsync()
|
|
{
|
|
var name = RawTreeTestDb.SeedFresh();
|
|
var svc = new RawTreeService(RawTreeTestDb.Factory(name));
|
|
var victim = await svc.CreateDriverAsync(RawTreeTestDb.ClusterId, null, "Victim", DriverTypeNames.Galaxy,
|
|
Galaxy("secret:galaxy/victim-own/apikey"), Ct);
|
|
victim.Ok.ShouldBeTrue(victim.Error);
|
|
return (svc, name, victim.CreatedId!);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateDriver_refuses_a_reference_naming_another_instance()
|
|
{
|
|
var (svc, name, victimId) = await WithVictimAsync();
|
|
|
|
var byName = await svc.CreateDriverAsync(RawTreeTestDb.ClusterId, null, "evil1", DriverTypeNames.Galaxy,
|
|
Galaxy("secret:galaxy/Victim/apikey"), Ct);
|
|
var byId = await svc.CreateDriverAsync(RawTreeTestDb.ClusterId, null, "evil2", DriverTypeNames.Galaxy,
|
|
Galaxy($"secret:galaxy/{victimId}/apikey"), Ct);
|
|
|
|
byName.Ok.ShouldBeFalse();
|
|
byName.Error!.ShouldContain("lmxopcua#835");
|
|
byId.Ok.ShouldBeFalse();
|
|
using var db = RawTreeTestDb.CreateNamed(name);
|
|
db.DriverInstances.ShouldNotContain(d => d.Name == "evil1" || d.Name == "evil2");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Update_and_device_paths_refuse_and_keep_the_stored_config()
|
|
{
|
|
var (svc, name, victimId) = await WithVictimAsync();
|
|
var evil = await svc.CreateDriverAsync(RawTreeTestDb.ClusterId, null, "evil", DriverTypeNames.Galaxy,
|
|
Galaxy("secret:galaxy/shared/apikey"), Ct);
|
|
evil.Ok.ShouldBeTrue(evil.Error);
|
|
|
|
byte[] rv;
|
|
string deviceId;
|
|
byte[] deviceRv;
|
|
using (var db = RawTreeTestDb.CreateNamed(name))
|
|
{
|
|
rv = db.DriverInstances.Single(d => d.DriverInstanceId == evil.CreatedId).RowVersion;
|
|
var device = db.Devices.Single(d => d.DriverInstanceId == evil.CreatedId);
|
|
deviceId = device.DeviceId;
|
|
deviceRv = device.RowVersion;
|
|
}
|
|
|
|
var stolen = Galaxy($"secret:galaxy/{victimId}/apikey");
|
|
(await svc.UpdateDriverAsync(evil.CreatedId!, "evil", stolen, null, rv, Ct)).Error.ShouldNotBeNull().ShouldContain("lmxopcua#835");
|
|
(await svc.UpdateDeviceAsync(deviceId, "dev", stolen, true, deviceRv, Ct)).Ok.ShouldBeFalse();
|
|
(await svc.CreateDeviceAsync(evil.CreatedId!, "dev2", stolen, Ct)).Ok.ShouldBeFalse();
|
|
|
|
using var check = RawTreeTestDb.CreateNamed(name);
|
|
check.DriverInstances.Single(d => d.DriverInstanceId == evil.CreatedId).DriverConfig.ShouldContain("secret:galaxy/shared/apikey");
|
|
check.Devices.Where(d => d.DriverInstanceId == evil.CreatedId).ShouldAllBe(d => !d.DeviceConfig.Contains(victimId));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_driver_may_save_its_own_id_scoped_secret_on_update_and_on_its_devices()
|
|
{
|
|
var (svc, name, victimId) = await WithVictimAsync();
|
|
byte[] rv;
|
|
string deviceId;
|
|
byte[] deviceRv;
|
|
using (var db = RawTreeTestDb.CreateNamed(name))
|
|
{
|
|
rv = db.DriverInstances.Single(d => d.DriverInstanceId == victimId).RowVersion;
|
|
var device = db.Devices.Single(d => d.DriverInstanceId == victimId);
|
|
deviceId = device.DeviceId;
|
|
deviceRv = device.RowVersion;
|
|
}
|
|
|
|
var own = Galaxy($"secret:galaxy/{victimId}/apikey");
|
|
(await svc.UpdateDriverAsync(victimId, "Victim", own, null, rv, Ct)).Ok.ShouldBeTrue();
|
|
(await svc.UpdateDeviceAsync(deviceId, "dev", Galaxy("secret:galaxy/Victim/apikey"), true, deviceRv, Ct)).Ok.ShouldBeTrue();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("""{"AuthType":"Certificate","UserCertificatePath":"C:\\Windows\\System32\\config\\systemprofile\\host.pfx"}""")]
|
|
[InlineData("""{"AuthType":"Certificate","UserCertificatePath":"/etc/ssl/private/host.pfx"}""")]
|
|
[InlineData("""{"AuthType":"Certificate","UserCertificatePath":"../host.pfx"}""")]
|
|
public async Task An_opcuaclient_user_certificate_path_outside_the_directory_is_never_stored(string config)
|
|
{
|
|
var name = RawTreeTestDb.SeedFresh();
|
|
var svc = new RawTreeService(RawTreeTestDb.Factory(name));
|
|
var result = await svc.CreateDriverAsync(RawTreeTestDb.ClusterId, null, "ouc836", DriverTypeNames.OpcUaClient, config, Ct);
|
|
result.Ok.ShouldBeFalse();
|
|
result.Error!.ShouldContain("lmxopcua#836");
|
|
DriverConfigSaveGate.Validate(DriverTypeNames.OpcUaClient, config).ShouldNotBeNull(); // the modals' live check too
|
|
using var db = RawTreeTestDb.CreateNamed(name);
|
|
db.DriverInstances.ShouldNotContain(d => d.Name == "ouc836");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_relative_user_certificate_path_saves()
|
|
{
|
|
var name = RawTreeTestDb.SeedFresh();
|
|
var svc = new RawTreeService(RawTreeTestDb.Factory(name));
|
|
(await svc.CreateDriverAsync(RawTreeTestDb.ClusterId, null, "ouc-ok", DriverTypeNames.OpcUaClient,
|
|
"""{"AuthType":"Certificate","UserCertificatePath":"plant1/user.pfx"}""", Ct)).Ok.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LoadDeviceDriverInstanceId_names_the_owning_driver()
|
|
{
|
|
var name = RawTreeTestDb.SeedFresh();
|
|
var svc = new RawTreeService(RawTreeTestDb.Factory(name));
|
|
(await svc.LoadDeviceDriverInstanceIdAsync(RawTreeTestDb.FolderedDeviceId, Ct)).ShouldBe(RawTreeTestDb.FolderedDriverId);
|
|
(await svc.LoadDeviceDriverInstanceIdAsync("DEV-nope", Ct)).ShouldBeNull();
|
|
}
|
|
}
|