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."); // A non-positive connect/search timeout would leave the shared library unbounded against a hung // directory, so fail fast. The other four S2 knobs validated here (AuthTimeoutMs, // MaxConcurrentAuthentications, OutageFailureThreshold, OutageCooldownSeconds) went with // LdapOpcUaUserAuthenticator in #520 — they guarded an LDAP bind running inline on OPC UA // session activation, and no remaining LDAP consumer runs on that path. builder.RequireThat(options.ConnectionTimeoutMs > 0, "Ldap:ConnectionTimeoutMs must be > 0."); } }