using ZB.MOM.WW.Configuration;
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
using LdapTransport = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapTransport;
namespace ZB.MOM.WW.OtOpcUa.Host.Configuration;
///
/// Fail-fast startup validator for , built on the shared
/// ZB.MOM.WW.Configuration . When LDAP login
/// is enabled, Server and SearchBase must be set and Port must be a valid
/// TCP port; when disabled — or when DevStubMode bypasses the real bind — all checks are
/// skipped. ServiceAccountDn/Password are
/// intentionally not required — an empty pair selects the direct-bind path (see
/// ). Failure messages use "Ldap:" as a
/// human-readable field prefix — not the literal bound section path, which is
/// Security:Ldap (see ).
///
///
/// Insecure-transport guard (review fix): a real-LDAP config that selects plaintext transport
/// () without opting in via
/// now FAILS startup validation, so an insecure-by-accident production overlay never boots.
/// This mirrors the login-time fail-closed guard in and is
/// gated on the same conditions ( AND not
/// ): a disabled or dev-stub config is exempt, exactly as it
/// is exempt from the real bind. The login-time guard remains as defence in depth.
///
public sealed class LdapOptionsValidator : OptionsValidatorBase
{
///
protected override void Validate(ValidationBuilder builder, LdapOptions options)
{
// Skip the real-LDAP field checks when LDAP login is disabled, or when the dev stub is
// active — DevStubMode bypasses the real bind entirely, so Server/SearchBase/Port are
// irrelevant and would otherwise force dev configs to carry meaningless placeholders.
if (!options.Enabled || options.DevStubMode) 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");
// Fail closed at startup on a plaintext transport unless explicitly opted in — same
// condition the login-time guard in OtOpcUaLdapAuthService enforces, lifted to boot so an
// insecure-by-accident production overlay refuses to start rather than silently failing
// every bind at login.
builder.RequireThat(
!(options.Transport == LdapTransport.None && !options.AllowInsecure),
"LDAP transport is None (plaintext) but AllowInsecure is false — set Transport to Ldaps/StartTls or set AllowInsecure for dev.");
// S2 resilience knobs — a non-positive timeout / concurrency bound would silently disable the
// wall-clock/concurrency protection the OPC UA data-plane authenticator relies on, so fail fast.
builder.RequireThat(options.ConnectionTimeoutMs > 0,
"Ldap:ConnectionTimeoutMs must be > 0.");
builder.RequireThat(options.AuthTimeoutMs > 0,
"Ldap:AuthTimeoutMs must be > 0.");
builder.RequireThat(options.MaxConcurrentAuthentications > 0,
"Ldap:MaxConcurrentAuthentications must be > 0.");
// An open circuit with no cooldown would re-probe LDAP on every call — pointless. Only enforce
// a positive cooldown when the circuit is actually enabled (OutageFailureThreshold > 0; 0 disables).
builder.RequireThat(options.OutageFailureThreshold <= 0 || options.OutageCooldownSeconds > 0,
"Ldap:OutageCooldownSeconds must be > 0 when Ldap:OutageFailureThreshold > 0 (0 disables the circuit).");
}
}