feat: add fail-fast LDAP options validation in OtOpcUa via ZB.MOM.WW.Configuration
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
using ZB.MOM.WW.Configuration;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fail-fast startup validator for <see cref="LdapOptions"/>, built on the shared
|
||||||
|
/// <c>ZB.MOM.WW.Configuration</c> <see cref="OptionsValidatorBase{TOptions}"/>. When LDAP login
|
||||||
|
/// is enabled, <c>Server</c> and <c>SearchBase</c> must be set and <c>Port</c> must be a valid
|
||||||
|
/// TCP port; when disabled, all checks are skipped. <c>ServiceAccountDn</c>/<c>Password</c> are
|
||||||
|
/// intentionally not required — an empty pair selects the direct-bind path (see
|
||||||
|
/// <see cref="LdapOptions.ServiceAccountDn"/>). Failure messages carry the real <c>"Ldap:"</c>
|
||||||
|
/// section prefix matching the bound configuration section.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LdapOptionsValidator : OptionsValidatorBase<LdapOptions>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Validate(ValidationBuilder builder, LdapOptions options)
|
||||||
|
{
|
||||||
|
if (!options.Enabled) return;
|
||||||
|
|
||||||
|
builder.RequireThat(!string.IsNullOrWhiteSpace(options.Server),
|
||||||
|
"Ldap:Server is required when LDAP login is enabled.");
|
||||||
|
builder.RequireThat(!string.IsNullOrWhiteSpace(options.SearchBase),
|
||||||
|
"Ldap:SearchBase is required when LDAP login is enabled.");
|
||||||
|
builder.Port(options.Port, "Ldap:Port");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ using ZB.MOM.WW.OtOpcUa.ControlPlane;
|
|||||||
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||||
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
|
||||||
using ZB.MOM.WW.OtOpcUa.Host;
|
using ZB.MOM.WW.OtOpcUa.Host;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||||
using ZB.MOM.WW.OtOpcUa.Host.Drivers;
|
using ZB.MOM.WW.OtOpcUa.Host.Drivers;
|
||||||
using ZB.MOM.WW.OtOpcUa.Host.Engines;
|
using ZB.MOM.WW.OtOpcUa.Host.Engines;
|
||||||
using ZB.MOM.WW.OtOpcUa.Host.Health;
|
using ZB.MOM.WW.OtOpcUa.Host.Health;
|
||||||
@@ -20,6 +21,7 @@ using ZB.MOM.WW.OtOpcUa.Runtime;
|
|||||||
using ZB.MOM.WW.OtOpcUa.Security;
|
using ZB.MOM.WW.OtOpcUa.Security;
|
||||||
using ZB.MOM.WW.OtOpcUa.Security.Endpoints;
|
using ZB.MOM.WW.OtOpcUa.Security.Endpoints;
|
||||||
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
|
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
|
||||||
|
using ZB.MOM.WW.Configuration;
|
||||||
using ZB.MOM.WW.Telemetry.Serilog;
|
using ZB.MOM.WW.Telemetry.Serilog;
|
||||||
|
|
||||||
// Roles drive the entire conditional wiring below — see ZB.MOM.WW.OtOpcUa.Cluster.RoleParser.
|
// Roles drive the entire conditional wiring below — see ZB.MOM.WW.OtOpcUa.Cluster.RoleParser.
|
||||||
@@ -96,7 +98,7 @@ if (hasDriver)
|
|||||||
new RoslynScriptedAlarmEvaluator(sp.GetRequiredService<ILoggerFactory>().CreateLogger<RoslynScriptedAlarmEvaluator>()));
|
new RoslynScriptedAlarmEvaluator(sp.GetRequiredService<ILoggerFactory>().CreateLogger<RoslynScriptedAlarmEvaluator>()));
|
||||||
builder.Services.AddSingleton<IScriptedAlarmEvaluator>(sp => sp.GetRequiredService<RoslynScriptedAlarmEvaluator>());
|
builder.Services.AddSingleton<IScriptedAlarmEvaluator>(sp => sp.GetRequiredService<RoslynScriptedAlarmEvaluator>());
|
||||||
|
|
||||||
builder.Services.AddOptions<LdapOptions>().Bind(builder.Configuration.GetSection("Ldap"));
|
builder.Services.AddValidatedOptions<LdapOptions, LdapOptionsValidator>(builder.Configuration, "Ldap");
|
||||||
builder.Services.AddSingleton<ILdapAuthService, LdapAuthService>();
|
builder.Services.AddSingleton<ILdapAuthService, LdapAuthService>();
|
||||||
builder.Services.AddSingleton<IOpcUaUserAuthenticator, LdapOpcUaUserAuthenticator>();
|
builder.Services.AddSingleton<IOpcUaUserAuthenticator, LdapOpcUaUserAuthenticator>();
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<PackageReference Include="ZB.MOM.WW.Health.EntityFrameworkCore" />
|
<PackageReference Include="ZB.MOM.WW.Health.EntityFrameworkCore" />
|
||||||
<PackageReference Include="ZB.MOM.WW.Telemetry" />
|
<PackageReference Include="ZB.MOM.WW.Telemetry" />
|
||||||
<PackageReference Include="ZB.MOM.WW.Telemetry.Serilog" />
|
<PackageReference Include="ZB.MOM.WW.Telemetry.Serilog" />
|
||||||
|
<PackageReference Include="ZB.MOM.WW.Configuration" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Task 3 — verifies the net-new <see cref="LdapOptionsValidator"/> (built on the shared
|
||||||
|
/// <c>ZB.MOM.WW.Configuration</c> <c>OptionsValidatorBase</c>/<c>ValidationBuilder</c>) gates on
|
||||||
|
/// <see cref="LdapOptions.Enabled"/>, and that when enabled it requires <c>Server</c>,
|
||||||
|
/// <c>SearchBase</c>, and a valid <c>Port</c>. Failure messages carry the real <c>"Ldap:"</c>
|
||||||
|
/// section prefix so they read correctly when surfaced at host startup.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LdapOptionsValidatorTests
|
||||||
|
{
|
||||||
|
private static readonly LdapOptionsValidator Sut = new();
|
||||||
|
|
||||||
|
/// <summary>Valid enabled options pass validation.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Valid_enabled_options_succeed()
|
||||||
|
{
|
||||||
|
var options = new LdapOptions
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Server = "ldap",
|
||||||
|
SearchBase = "dc=x",
|
||||||
|
Port = 389,
|
||||||
|
};
|
||||||
|
|
||||||
|
Sut.Validate(null, options).Succeeded.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>When LDAP is disabled all checks are skipped, so a blank config still passes.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Disabled_options_succeed_even_when_blank()
|
||||||
|
{
|
||||||
|
var options = new LdapOptions
|
||||||
|
{
|
||||||
|
Enabled = false,
|
||||||
|
Server = string.Empty,
|
||||||
|
SearchBase = string.Empty,
|
||||||
|
Port = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Sut.Validate(null, options).Succeeded.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Enabled with a blank server reports the required-server failure.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Enabled_with_blank_server_fails()
|
||||||
|
{
|
||||||
|
var options = new LdapOptions
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Server = string.Empty,
|
||||||
|
SearchBase = "dc=x",
|
||||||
|
Port = 389,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = Sut.Validate(null, options);
|
||||||
|
|
||||||
|
result.Failed.ShouldBeTrue();
|
||||||
|
result.Failures.ShouldContain("Ldap:Server is required when LDAP login is enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Enabled with port 0 reports the port-range failure using the shared primitive wording.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Enabled_with_zero_port_fails()
|
||||||
|
{
|
||||||
|
var options = new LdapOptions
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
Server = "ldap",
|
||||||
|
SearchBase = "dc=x",
|
||||||
|
Port = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = Sut.Validate(null, options);
|
||||||
|
|
||||||
|
result.Failed.ShouldBeTrue();
|
||||||
|
result.Failures.ShouldContain("Ldap:Port must be between 1 and 65535 (was 0)");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user