271b96a644
I-1: Test Connect / browse configs may carry unsaved edits (the device modal merges the stored DriverConfig with the typed device config). The server (DriverInstanceIdentities.EffectiveOwnerAsync, used by AdminOperationsActor and BrowserSessionService) honours the named owner only when every endpoint value (new DriverCredentialRules.EndpointFields: Galaxy Endpoint, OpcUaClient EndpointUrl/EndpointUrls/DiscoveryUrl, Heidenhain Host/Port) is one that driver stores; otherwise the owner is null and its instance-named secrets are refused. Every Test Connect / browse whose config carries an in-scope secret:/env: reference is recorded (CredentialUseAudit: IAuditWriter event category CredentialUse + a structured Information line: user, driver, endpoint hosts, outcome). L-1: the UserCertificatePath rule is skipped only in an object that explicitly sets AuthType to non-Certificate (string or ordinal); the deploy gate also checks the runtime-merged driver+device config so the switch cannot be split from the path; the form no longer persists the hidden field. L-2: a ConfigDb fault in the browse check is a refusal; RawBrowseModal catches open failures into its error message. L-4: a relative UserCertificateDirectory resolves against AppContext.BaseDirectory. Co-Authored-By: Claude <noreply@anthropic.com>
110 lines
5.4 KiB
C#
110 lines
5.4 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
|
|
|
/// <summary>
|
|
/// The deploy gate for lmxopcua#835 (a credential reference naming ANOTHER driver instance's secret) and #836 (the
|
|
/// OpcUaClient user-certificate path confined to the node's certificate directory).
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class DraftValidatorCredentialInstanceAndPathTests
|
|
{
|
|
private static IReadOnlyList<ValidationError> Validate(params DriverInstance[] drivers) =>
|
|
Validate(drivers, []);
|
|
|
|
private static IReadOnlyList<ValidationError> Validate(DriverInstance[] drivers, Device[] devices) =>
|
|
DraftValidator.Validate(new DraftSnapshot
|
|
{
|
|
GenerationId = 1,
|
|
ClusterId = "c",
|
|
DriverInstances = drivers,
|
|
Devices = devices,
|
|
Tags = [],
|
|
}).Where(e => e.Code is CredentialReferenceDeployChecks.Code or CredentialReferenceDeployChecks.OtherInstanceCode).ToList();
|
|
|
|
private static DriverInstance Drv(string id, string name, string type, string config) =>
|
|
new() { DriverInstanceId = id, ClusterId = "c", Name = name, DriverType = type, DriverConfig = config };
|
|
|
|
[Fact]
|
|
public void A_galaxy_driver_naming_another_instances_key_is_refused()
|
|
{
|
|
var e = Validate(
|
|
Drv("DRV-victim", "Victim", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/DRV-victim/apikey"}}"""),
|
|
Drv("DRV-evil", "Evil", "GalaxyMxGateway", """{"Gateway":{"Endpoint":"http://evil:5001","ApiKeySecretRef":"secret:galaxy/DRV-victim/apikey"}}"""))
|
|
.ShouldHaveSingleItem();
|
|
e.Code.ShouldBe(CredentialReferenceDeployChecks.OtherInstanceCode);
|
|
e.Context.ShouldBe("DRV-evil");
|
|
e.Message.ShouldContain("lmxopcua#835");
|
|
}
|
|
|
|
[Fact]
|
|
public void A_device_config_naming_another_instance_by_name_is_refused() =>
|
|
Validate(
|
|
[
|
|
Drv("DRV-a", "Victim", "OpcUaClient", "{}"),
|
|
Drv("DRV-b", "Evil", "OpcUaClient", "{}"),
|
|
],
|
|
[new Device { DeviceId = "dev-b", DriverInstanceId = "DRV-b", Name = "d", DeviceConfig = """{"Password":"secret:opcuaclient/victim/pw"}""" }])
|
|
.ShouldHaveSingleItem().Context.ShouldBe("dev-b");
|
|
|
|
[Fact]
|
|
public void Shared_and_own_keys_pass() =>
|
|
Validate(
|
|
Drv("DRV-a", "A", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/DRV-a/apikey"}}"""),
|
|
Drv("DRV-b", "B", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/gw/apikey"}}"""),
|
|
Drv("DRV-c", "C", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/gw/apikey"}}"""))
|
|
.ShouldBeEmpty();
|
|
|
|
[Fact]
|
|
public void Heidenhain_cross_instance_is_reported_by_this_gate() =>
|
|
DraftValidator.Validate(new DraftSnapshot
|
|
{
|
|
GenerationId = 1, ClusterId = "c",
|
|
DriverInstances =
|
|
[
|
|
Drv("H1", "line1", "Heidenhain", """{"Access":{"InspectPassword":"secret:heidenhain/line1/inspect"}}"""),
|
|
Drv("H2", "line2", "Heidenhain", """{"Access":{"InspectPassword":"secret:heidenhain/line1/inspect"}}"""),
|
|
],
|
|
Devices = [], Tags = [],
|
|
})
|
|
.Where(e => e.Code == CredentialReferenceDeployChecks.OtherInstanceCode)
|
|
.ShouldHaveSingleItem().Context.ShouldBe("H2");
|
|
|
|
[Theory]
|
|
[InlineData("""{"AuthType":"Certificate","UserCertificatePath":"C:\\Windows\\System32\\config\\systemprofile\\host.pfx"}""")]
|
|
[InlineData("""{"UserCertificatePath":"/etc/ssl/private/host.pfx"}""")]
|
|
[InlineData("""{"UserCertificatePath":"plant1/../../host.pfx"}""")]
|
|
public void An_opcuaclient_user_certificate_path_outside_the_directory_is_refused(string config)
|
|
{
|
|
var e = Validate(Drv("DRV-o", "o", "OpcUaClient", config)).ShouldHaveSingleItem();
|
|
e.Code.ShouldBe(CredentialReferenceDeployChecks.Code);
|
|
e.Message.ShouldContain("lmxopcua#836");
|
|
e.Message.ShouldNotContain("host.pfx");
|
|
}
|
|
|
|
/// <summary>Review L-1: a path in an object that explicitly switches certificate auth off is not checked…</summary>
|
|
[Fact]
|
|
public void A_path_under_explicit_non_certificate_auth_passes() =>
|
|
Validate(Drv("DRV-o", "o", "OpcUaClient", """{"AuthType":"Anonymous","UserCertificatePath":"C:\\stale\\host.pfx"}""")).ShouldBeEmpty();
|
|
|
|
/// <summary>…but the driver/device split cannot smuggle one past the gate: the runtime-merged config is checked too.</summary>
|
|
[Fact]
|
|
public void A_path_split_from_its_certificate_switch_is_caught_on_the_merged_config()
|
|
{
|
|
var e = Validate(
|
|
[Drv("DRV-o", "o", "OpcUaClient", """{"AuthType":"Anonymous","UserCertificatePath":"C:\\stolen\\host.pfx"}""")],
|
|
[new Device { DeviceId = "dev-o", DriverInstanceId = "DRV-o", Name = "d", DeviceConfig = """{"AuthType":"Certificate"}""" }])
|
|
.ShouldHaveSingleItem();
|
|
e.Context.ShouldBe("DRV-o");
|
|
e.Message.ShouldContain("merged with its devices");
|
|
e.Message.ShouldContain("lmxopcua#836");
|
|
}
|
|
|
|
[Fact]
|
|
public void A_relative_user_certificate_path_passes() =>
|
|
Validate(Drv("DRV-o", "o", "OpcUaClient", """{"AuthType":"Certificate","UserCertificatePath":"plant1/user.pfx"}""")).ShouldBeEmpty();
|
|
}
|