feat(security): LDAP auth timeout/concurrency/outage-circuit options; project ConnectionTimeoutMs into the shared library (03/S2)

This commit is contained in:
Joseph Doherty
2026-07-13 11:38:37 -04:00
parent ce6d9e21d3
commit 09faa05f06
5 changed files with 197 additions and 1 deletions
@@ -47,5 +47,18 @@ public sealed class LdapOptionsValidator : OptionsValidatorBase<LdapOptions>
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).");
}
}
@@ -83,6 +83,49 @@ public sealed class LdapOptions
/// </summary>
public Dictionary<string, string> GroupToRole { get; set; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Bounds (in milliseconds) the shared-library directory client's socket connect AND each
/// per-operation search inside a single bind flow. Projected into the library's
/// <see cref="LibLdapOptions.ConnectionTimeoutMs"/> via <see cref="ToLibraryOptions"/>. The
/// default (10000) matches the library default, so behaviour is unchanged unless configured.
/// This is the FIRST line of defence against a directory outage; the whole-flow
/// <see cref="AuthTimeoutMs"/> is the backstop (03/S2).
/// </summary>
public int ConnectionTimeoutMs { get; set; } = 10_000;
/// <summary>
/// Hard whole-flow wall-clock bound (in milliseconds) the OPC UA data-plane authenticator
/// (<c>LdapOpcUaUserAuthenticator</c>) applies across the ENTIRE authenticate flow — bind plus
/// role-map — so the OPC UA SDK's synchronous impersonation callback (raised under a
/// <c>SessionManager</c>-wide event lock) can never be wedged by a slow / hung directory.
/// Deliberately &gt; <see cref="ConnectionTimeoutMs"/> (default 15000) so a healthy-but-slow
/// full flow is not cut off while an outage is bounded by the library timeout first (03/S2).
/// </summary>
public int AuthTimeoutMs { get; set; } = 15_000;
/// <summary>
/// Maximum number of in-flight OPC UA authentication flows the data-plane authenticator admits
/// at once. A caller that cannot acquire a slot within its budget is denied "backend busy"
/// (local backpressure, not counted against the outage circuit). Bounds accumulation of
/// orphaned / timed-out bind tasks during an outage (03/S2). Default 8.
/// </summary>
public int MaxConcurrentAuthentications { get; set; } = 8;
/// <summary>
/// Number of CONSECUTIVE system-side failures (boundary timeout, directory-unreachable result,
/// or unexpected throw) after which the data-plane authenticator's outage circuit opens and
/// denies instantly for <see cref="OutageCooldownSeconds"/> without touching LDAP. Any success
/// or user-side (credential) deny resets the counter. <c>0</c> disables the circuit (03/S2).
/// Default 3.
/// </summary>
public int OutageFailureThreshold { get; set; } = 3;
/// <summary>
/// How long (in seconds) the outage circuit stays open before a half-open probe is allowed
/// through. Only meaningful when <see cref="OutageFailureThreshold"/> &gt; 0. Default 15 (03/S2).
/// </summary>
public int OutageCooldownSeconds { get; set; } = 15;
/// <summary>
/// Projects the wire fields onto the shared <c>ZB.MOM.WW.Auth.Ldap</c>
/// <see cref="LibLdapOptions"/> the directory client consumes. App-only concerns
@@ -104,5 +147,6 @@ public sealed class LdapOptions
UserNameAttribute = UserNameAttribute,
DisplayNameAttribute = DisplayNameAttribute,
GroupAttribute = GroupAttribute,
ConnectionTimeoutMs = ConnectionTimeoutMs,
};
}