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)."); } }