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:
@@ -231,6 +231,32 @@ public sealed class EndpointExtensionsTests : IDisposable
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auth 0.4.4 <c>ApiKeyFailure.StoreUnavailable</c>: a key store that throws is a retryable 503,
|
||||
/// never a 401 (which would blame the caller's key) and never an unhandled 500.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task KeyStoreThrows_Returns503_NotUnauthorized()
|
||||
{
|
||||
var method = SeedMethod(1, "echo", "return 1;");
|
||||
var throwingStore = Substitute.For<IApiKeyStore>();
|
||||
throwingStore.FindByKeyIdAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns<Task<ApiKeyRecord?>>(_ => throw new InvalidOperationException("store down"));
|
||||
|
||||
using var host = await BuildHostAsync(method, additionalServices: services =>
|
||||
{
|
||||
services.RemoveAll<IApiKeyStore>();
|
||||
services.AddSingleton(throwingStore);
|
||||
});
|
||||
var client = host.GetTestClient();
|
||||
|
||||
var response = await client.SendAsync(BuildPost("echo", "{}", "sbk_key1_" + new string('a', 43)));
|
||||
|
||||
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
|
||||
Assert.Contains("SERVICE_UNAVAILABLE", await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a whitespace Authorization header with a valid X-API-Key returns 200.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user