Renames all 11 projects (5 src + 6 tests), the .slnx solution file, all source-file namespaces, all axaml namespace references, and all v1 documentation references in CLAUDE.md and docs/*.md (excluding docs/v2/ which is already in OtOpcUa form). Also updates the TopShelf service registration name from "LmxOpcUa" to "OtOpcUa" per Phase 0 Task 0.6.
Preserves runtime identifiers per Phase 0 Out-of-Scope rules to avoid breaking v1/v2 client trust during coexistence: OPC UA `ApplicationUri` defaults (`urn:{GalaxyName}:LmxOpcUa`), server `EndpointPath` (`/LmxOpcUa`), `ServerName` default (feeds cert subject CN), `MxAccessConfiguration.ClientName` default (defensive — stays "LmxOpcUa" for MxAccess audit-trail consistency), client OPC UA identifiers (`ApplicationName = "LmxOpcUaClient"`, `ApplicationUri = "urn:localhost:LmxOpcUaClient"`, cert directory `%LocalAppData%\LmxOpcUaClient\pki\`), and the `LmxOpcUaServer` class name (class rename out of Phase 0 scope per Task 0.5 sed pattern; happens in Phase 1 alongside `LmxNodeManager → GenericDriverNodeManager` Core extraction). 23 LmxOpcUa references retained, all enumerated and justified in `docs/v2/implementation/exit-gate-phase-0.md`.
Build clean: 0 errors, 30 warnings (lower than baseline 167). Tests at strict improvement over baseline: 821 passing / 1 failing vs baseline 820 / 2 (one flaky pre-existing failure passed this run; the other still fails — both pre-existing and unrelated to the rename). `Client.UI.Tests`, `Historian.Aveva.Tests`, `Client.Shared.Tests`, `IntegrationTests` all match baseline exactly. Exit gate compliance results recorded in `docs/v2/implementation/exit-gate-phase-0.md` with all 7 checks PASS or DEFERRED-to-PR-review (#7 service install verification needs Windows service permissions on the reviewer's box).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
231 lines
7.5 KiB
C#
231 lines
7.5 KiB
C#
using System;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
|
using ZB.MOM.WW.OtOpcUa.Host.Domain;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Tests.Authentication
|
|
{
|
|
public class UserAuthenticationTests
|
|
{
|
|
[Fact]
|
|
public void AuthenticationConfiguration_Defaults()
|
|
{
|
|
var config = new AuthenticationConfiguration();
|
|
|
|
config.AllowAnonymous.ShouldBeTrue();
|
|
config.AnonymousCanWrite.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void AuthenticationConfiguration_LdapDefaults()
|
|
{
|
|
var config = new AuthenticationConfiguration();
|
|
|
|
config.Ldap.ShouldNotBeNull();
|
|
config.Ldap.Enabled.ShouldBeFalse();
|
|
config.Ldap.Host.ShouldBe("localhost");
|
|
config.Ldap.Port.ShouldBe(3893);
|
|
config.Ldap.BaseDN.ShouldBe("dc=lmxopcua,dc=local");
|
|
config.Ldap.ReadOnlyGroup.ShouldBe("ReadOnly");
|
|
config.Ldap.WriteOperateGroup.ShouldBe("WriteOperate");
|
|
config.Ldap.WriteTuneGroup.ShouldBe("WriteTune");
|
|
config.Ldap.WriteConfigureGroup.ShouldBe("WriteConfigure");
|
|
config.Ldap.AlarmAckGroup.ShouldBe("AlarmAck");
|
|
config.Ldap.TimeoutSeconds.ShouldBe(5);
|
|
}
|
|
|
|
[Fact]
|
|
public void LdapConfiguration_BindDnTemplate_Default()
|
|
{
|
|
var config = new LdapConfiguration();
|
|
config.BindDnTemplate.ShouldBe("cn={username},dc=lmxopcua,dc=local");
|
|
}
|
|
|
|
[Fact]
|
|
public void LdapAuthenticationProvider_ValidBind_ReturnsTrue()
|
|
{
|
|
// This test requires GLAuth running on localhost:3893
|
|
// Skip if not available
|
|
var ldapConfig = CreateGlAuthConfig();
|
|
var provider = new LdapAuthenticationProvider(ldapConfig);
|
|
|
|
try
|
|
{
|
|
provider.ValidateCredentials("readonly", "readonly123").ShouldBeTrue();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// GLAuth not running - skip gracefully
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LdapAuthenticationProvider_InvalidPassword_ReturnsFalse()
|
|
{
|
|
var ldapConfig = CreateGlAuthConfig();
|
|
var provider = new LdapAuthenticationProvider(ldapConfig);
|
|
|
|
try
|
|
{
|
|
provider.ValidateCredentials("readonly", "wrongpassword").ShouldBeFalse();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LdapAuthenticationProvider_UnknownUser_ReturnsFalse()
|
|
{
|
|
var ldapConfig = CreateGlAuthConfig();
|
|
var provider = new LdapAuthenticationProvider(ldapConfig);
|
|
|
|
try
|
|
{
|
|
provider.ValidateCredentials("nonexistent", "anything").ShouldBeFalse();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LdapAuthenticationProvider_ReadOnlyUser_HasReadOnlyRole()
|
|
{
|
|
var ldapConfig = CreateGlAuthConfig();
|
|
var provider = new LdapAuthenticationProvider(ldapConfig);
|
|
|
|
try
|
|
{
|
|
provider.ValidateCredentials("readonly", "readonly123").ShouldBeTrue();
|
|
var roles = provider.GetUserRoles("readonly");
|
|
roles.ShouldContain("ReadOnly");
|
|
roles.ShouldNotContain("WriteOperate");
|
|
roles.ShouldNotContain("AlarmAck");
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LdapAuthenticationProvider_WriteOperateUser_HasWriteOperateRole()
|
|
{
|
|
var ldapConfig = CreateGlAuthConfig();
|
|
var provider = new LdapAuthenticationProvider(ldapConfig);
|
|
|
|
try
|
|
{
|
|
provider.ValidateCredentials("writeop", "writeop123").ShouldBeTrue();
|
|
var roles = provider.GetUserRoles("writeop");
|
|
roles.ShouldContain("WriteOperate");
|
|
roles.ShouldNotContain("AlarmAck");
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LdapAuthenticationProvider_AlarmAckUser_HasAlarmAckRole()
|
|
{
|
|
var ldapConfig = CreateGlAuthConfig();
|
|
var provider = new LdapAuthenticationProvider(ldapConfig);
|
|
|
|
try
|
|
{
|
|
provider.ValidateCredentials("alarmack", "alarmack123").ShouldBeTrue();
|
|
var roles = provider.GetUserRoles("alarmack");
|
|
roles.ShouldContain("AlarmAck");
|
|
roles.ShouldNotContain("WriteOperate");
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LdapAuthenticationProvider_AdminUser_HasAllRoles()
|
|
{
|
|
var ldapConfig = CreateGlAuthConfig();
|
|
var provider = new LdapAuthenticationProvider(ldapConfig);
|
|
|
|
try
|
|
{
|
|
provider.ValidateCredentials("admin", "admin123").ShouldBeTrue();
|
|
var roles = provider.GetUserRoles("admin");
|
|
roles.ShouldContain("ReadOnly");
|
|
roles.ShouldContain("WriteOperate");
|
|
roles.ShouldContain("WriteTune");
|
|
roles.ShouldContain("WriteConfigure");
|
|
roles.ShouldContain("AlarmAck");
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LdapAuthenticationProvider_ImplementsIRoleProvider()
|
|
{
|
|
var ldapConfig = CreateGlAuthConfig();
|
|
var provider = new LdapAuthenticationProvider(ldapConfig);
|
|
|
|
(provider is IRoleProvider).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void LdapAuthenticationProvider_ConnectionFailure_ReturnsFalse()
|
|
{
|
|
var ldapConfig = new LdapConfiguration
|
|
{
|
|
Enabled = true,
|
|
Host = "localhost",
|
|
Port = 19999, // no server here
|
|
TimeoutSeconds = 1
|
|
};
|
|
var provider = new LdapAuthenticationProvider(ldapConfig);
|
|
|
|
provider.ValidateCredentials("anyone", "anything").ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void LdapAuthenticationProvider_ConnectionFailure_GetUserRoles_FallsBackToReadOnly()
|
|
{
|
|
var ldapConfig = new LdapConfiguration
|
|
{
|
|
Enabled = true,
|
|
Host = "localhost",
|
|
Port = 19999, // no server here
|
|
TimeoutSeconds = 1,
|
|
ServiceAccountDn = "cn=svc,dc=test",
|
|
ServiceAccountPassword = "test"
|
|
};
|
|
var provider = new LdapAuthenticationProvider(ldapConfig);
|
|
|
|
var roles = provider.GetUserRoles("anyone");
|
|
roles.ShouldContain("ReadOnly");
|
|
}
|
|
|
|
private static LdapConfiguration CreateGlAuthConfig()
|
|
{
|
|
return new LdapConfiguration
|
|
{
|
|
Enabled = true,
|
|
Host = "localhost",
|
|
Port = 3893,
|
|
BaseDN = "dc=lmxopcua,dc=local",
|
|
BindDnTemplate = "cn={username},dc=lmxopcua,dc=local",
|
|
ServiceAccountDn = "cn=serviceaccount,dc=lmxopcua,dc=local",
|
|
ServiceAccountPassword = "serviceaccount123",
|
|
TimeoutSeconds = 5,
|
|
ReadOnlyGroup = "ReadOnly",
|
|
WriteOperateGroup = "WriteOperate",
|
|
WriteTuneGroup = "WriteTune",
|
|
WriteConfigureGroup = "WriteConfigure",
|
|
AlarmAckGroup = "AlarmAck"
|
|
};
|
|
}
|
|
}
|
|
} |