feat(security): re-query LDAP group membership for active sessions (#46)

Auth 0.5.0 (all five packages) adds the passwordless ILdapGroupLookup. A new
singleton DirectoryGroupRefresher asks it for a session's current groups once
the new zb:lastgrouprefresh anchor (stamped at login) is older than
ScadaBridge:Security:LdapGroupRefresh:IntervalMinutes (default 15, Enabled
default true, CacheSeconds 60 per-node answer reuse). CookieSessionValidator
replaces the stored groups and re-maps roles; this reaches HTTP and live
circuits (once per interval, independent of the 10 s circuit refresh).

Failure policy: directory unavailable keeps the stored groups (warn, all paths
incl. fail-closed mutation checks); user not found / disabled / ambiguous /
no groups ends the session. Inert under BindMode=User (NotSupported),
LDAP disabled, or DisableLogin.

Auth 0.4.4 ApiKeyFailure.StoreUnavailable now answers 503 SERVICE_UNAVAILABLE
on the inbound API instead of a misleading 401.

Docs: Component-Security.md, components/Security.md, Component-Host.md,
register row 23 closed.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
Joseph Doherty
2026-11-12 20:31:51 -05:00
parent 139a43fb25
commit 9b0720af9c
16 changed files with 919 additions and 47 deletions
@@ -110,6 +110,20 @@ public static class EndpointExtensions
var verification = await verifier.VerifyAsync(
credential, httpContext.RequestAborted);
if (!verification.Succeeded
&& verification.Failure == ZB.MOM.WW.Auth.Abstractions.ApiKeys.ApiKeyFailure.StoreUnavailable)
{
// Auth 0.4.4+: the key store itself could not be read (shared SQL Server down, SQLite
// I/O fault). That says nothing about the caller's credential, so it is a retryable 503,
// not a 401 that would tell an integration its key is wrong. (Before 0.4.4 the fault
// escaped VerifyAsync as an unhandled 500.)
ScadaBridgeTelemetry.RecordInboundApiRequest("<unauthorized>");
logger.LogWarning(
"Inbound API key store unavailable for method {Method}; answering 503.", methodName);
return Error("SERVICE_UNAVAILABLE", "API key store unavailable; retry later",
StatusCodes.Status503ServiceUnavailable);
}
if (!verification.Succeeded)
{
// 401 for any verifier failure. The failure reason is