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:
@@ -21,11 +21,11 @@ The component registers:
|
||||
|
||||
Authentication uses a direct username/password bind against the LDAP/AD server via the shared `ILdapAuthService` (`ZB.MOM.WW.Auth.Ldap`). The flow is: service-account bind → search for the user entry by username → user-credential bind → group-membership query. The app never caches credentials locally. LDAPS (port 636) or StartTLS is required for production; the `AllowInsecure` flag in `LdapOptions` gates unencrypted use to explicitly opted-in deployments (local dev only). No Kerberos/NTLM path exists.
|
||||
|
||||
LDAP failure behavior is fail-closed at the login boundary and fail-open at the session boundary: a new login fails immediately if the directory is unreachable; an active cookie session continues with its current claims (its mid-session role refresh is DB-only and never calls LDAP), and a CLI holding a bearer token keeps working until the token expires. This avoids disrupting engineers mid-work during a brief directory outage. Group-membership changes are picked up at the next login.
|
||||
LDAP failure behavior is fail-closed at the login boundary and fail-open at the session boundary: a new login fails immediately if the directory is unreachable; an active cookie session continues with its current claims (its periodic directory group re-query keeps the stored groups when the directory cannot answer), and a CLI holding a bearer token keeps working until the token expires. This avoids disrupting engineers mid-work during a brief directory outage. Group-membership changes reach an active session within `LdapGroupRefresh:IntervalMinutes` (default 15, ScadaBridge#46); a deleted or disabled account ends the session.
|
||||
|
||||
### Sessions: cookie (UI) and bearer JWT (CLI/API)
|
||||
|
||||
**Interactive UI.** `POST /auth/login` signs the user in with ASP.NET Core cookie authentication. The HttpOnly/Secure cookie carries a data-protected claims ticket (no embedded JWT) built by the shared `SessionClaimBuilder`: display name, username, roles, permitted site IDs (site-scoped Deployer), the raw LDAP groups and the idle / role-refresh anchors. The cookie is the transport because Blazor Server's SignalR circuits do not carry `Authorization` headers on reconnect. Both central nodes share the Data Protection key ring, so either node validates a cookie the other issued. The cookie's `ExpireTimeSpan` is `IdleTimeoutMinutes` (default 30) with sliding expiration; passive traffic does not renew it. Once the role-refresh anchor is older than `RoleRefreshThresholdMinutes` (default 15) the validation hook re-runs the DB-backed `RoleMapper` on the stored groups (no LDAP call).
|
||||
**Interactive UI.** `POST /auth/login` signs the user in with ASP.NET Core cookie authentication. The HttpOnly/Secure cookie carries a data-protected claims ticket (no embedded JWT) built by the shared `SessionClaimBuilder`: display name, username, roles, permitted site IDs (site-scoped Deployer), the raw LDAP groups and the idle / role-refresh anchors. The cookie is the transport because Blazor Server's SignalR circuits do not carry `Authorization` headers on reconnect. Both central nodes share the Data Protection key ring, so either node validates a cookie the other issued. The cookie's `ExpireTimeSpan` is `IdleTimeoutMinutes` (default 30) with sliding expiration; passive traffic does not renew it. Once the role-refresh anchor is older than `RoleRefreshThresholdMinutes` (default 15) the validation hook re-runs the DB-backed `RoleMapper` on the stored groups. Separately, once the `zb:lastgrouprefresh` anchor is older than `LdapGroupRefresh:IntervalMinutes` (default 15), the groups themselves are re-read from the directory with the LDAP service account (`DirectoryGroupRefresher` over Auth `ILdapGroupLookup`, ScadaBridge#46) before the re-map.
|
||||
|
||||
**CLI / API.** `POST /auth/token` performs one LDAP bind per invocation and returns a short-lived HS256 JWT (`JwtExpiryMinutes`, default 15) signed with `JwtSigningKey`, shared by both central nodes; the response advertises `management_bearer: true`. `ManagementAuthenticator` accepts `Authorization: Bearer` alongside HTTP Basic on `POST /management`, the audit REST endpoints and the debug-stream hub. Roles inside a valid token are authoritative until it expires, so a revocation reaches a bearer caller within `JwtExpiryMinutes`; rotating `JwtSigningKey` invalidates every token. Under `DisableLogin`, `/auth/token` answers 404. Full spec: `docs/requirements/Component-Security.md`.
|
||||
|
||||
@@ -138,7 +138,7 @@ Login flow (Central UI `/auth/login` and `/auth/token`):
|
||||
2. On success, call `RoleMapper.MapGroupsToRolesAsync(ldapGroups)` to resolve roles and site scope.
|
||||
3. `/auth/login`: build the claims via `SessionClaimBuilder` and call the ASP.NET cookie `SignInAsync`. `/auth/token`: call `JwtTokenService.GenerateToken(displayName, username, roles, permittedSiteIds)` and return the bearer JWT.
|
||||
|
||||
On each subsequent cookie request the validation hook enforces the idle timeout and, past the role-refresh threshold, re-maps the stored groups against the DB. Bearer requests validate the token with `JwtTokenService.ValidateToken` and perform no LDAP bind or role re-map.
|
||||
On each subsequent cookie request the validation hook enforces the idle timeout and, past the role-refresh threshold, re-maps the stored groups against the DB (re-reading them from the directory first when the group re-query is due). Bearer requests validate the token with `JwtTokenService.ValidateToken` and perform no LDAP bind or role re-map.
|
||||
|
||||
Authorization gates use the named policies:
|
||||
|
||||
@@ -162,7 +162,10 @@ if (!AuthorizationPolicies.OperationalAuditRoles.Contains(userRole))
|
||||
| `JwtSigningKey` | *(required)* | Symmetric HMAC-SHA256 signing key. Must be at least 32 bytes (256 bits); `StartupValidator` rejects a shorter key on Central. |
|
||||
| `JwtExpiryMinutes` | `15` | Lifetime of the bearer JWT issued by `/auth/token`. |
|
||||
| `IdleTimeoutMinutes` | `30` | Cookie session idle timeout; set as the cookie `ExpireTimeSpan`. |
|
||||
| `RoleRefreshThresholdMinutes` | `15` | Age of the role-refresh anchor after which the cookie's roles are re-mapped from the DB (LDAP-free). |
|
||||
| `RoleRefreshThresholdMinutes` | `15` | Age of the role-refresh anchor after which the cookie's roles are re-mapped from the DB. |
|
||||
| `LdapGroupRefresh:Enabled` | `true` | Mid-session re-query of the user's directory groups (ScadaBridge#46). Inert when LDAP has no service account (`BindMode = User`) or under `DisableLogin`. |
|
||||
| `LdapGroupRefresh:IntervalMinutes` | `15` | Age of the `zb:lastgrouprefresh` anchor after which the groups are re-read from the directory (≥ 1). |
|
||||
| `LdapGroupRefresh:CacheSeconds` | `60` | Per-node reuse of one user's directory answer (0 disables). |
|
||||
| `RoleMappingCacheSeconds` | `30` | Process-wide `RoleMappingCache` TTL (`0` disables); invalidated after mapping/scope-rule edits on the local node and, over the central cache-invalidation topic, on the peer central node (#100). |
|
||||
| `RequireHttpsCookie` | `true` | When `true`, the cookie is `Secure`-only (HTTPS required). Set `false` for HTTP-only dev deployments; a warning is logged at startup. |
|
||||
| `CookieName` | `ZB.MOM.WW.ScadaBridge.Auth` | Authentication cookie name. Override per deployment when two ScadaBridge stacks share a hostname — browsers scope cookies by host+path but not by port. |
|
||||
|
||||
@@ -23,7 +23,7 @@ All 7 fix-now items landed via PLAN-04/05/06/07/08 (verified in review 08 round
|
||||
| 11 | **→ #43** Central-persisted OPC UA cert-trust audit | m7 follow-ups | Broadcast-to-both-nodes covers HA | Governance/audit requirement for trust decisions |
|
||||
| 17 | **→ #44** Unified notifications+site-calls outbox page | stillpending :118 | Explicit M9 decision to keep two pages | Operator confusion reports |
|
||||
| 19 | **→ #45** Bundle signing / cluster-to-cluster pull / differential bundles | transport-design :402 | v1 manifest hash + AES-GCM held sufficient | Non-repudiation requirement across orgs |
|
||||
| 23 | **→ #46** Live LDAP group-membership re-query for an active session | `docs/requirements/Component-Security.md` :61-69 (+ :78-79) | Blocked on an external package. The mid-session refresh re-maps the **stored** groups against the central DB with **no LDAP call**, so a directory group-membership change lands only at next login. A live re-query needs a passwordless service-account group-search method on the shared `ZB.MOM.WW.Auth.Ldap` library — an external NuGet `PackageReference` (`src/ZB.MOM.WW.ScadaBridge.Security/…csproj:23`) exposing only `AuthenticateAsync(username, password, ct)`. Central role-mapping/scope changes still apply within ~15 min (`RoleRefreshThresholdMinutes`). | `ZB.MOM.WW.Auth.Ldap` gains a standalone group-search API, or a requirement that a directory-side group revocation take effect mid-session rather than at next login |
|
||||
| 23 | **→ #46 — DONE 2026-11-12** Live LDAP group-membership re-query for an active session | `docs/requirements/Component-Security.md` *Directory group re-query* | Resolved: Auth `0.5.0` added the passwordless `ILdapGroupLookup` (service account); ScadaBridge re-reads an active session's groups every `LdapGroupRefresh:IntervalMinutes` (default 15) via `DirectoryGroupRefresher` — directory unavailable keeps stored groups, user not found/disabled/ambiguous/no groups ends the session. Residual: `BindMode = User` deployments cannot re-query (next-login behaviour). | — (closed) |
|
||||
| 24 | **→ #47** M8 large-bundle performance hardening | `docs/plans/2026-06-15-stillpending-completion-design.md:106` — "Small follow-ups logged (not blocking): … large-bundle/perf hardening" | Logged as a non-blocking follow-up when M8 shipped and never given an artifact: **no plan, no task entry, no perf/load test exists** (`tests/…Transport.Tests/Import/BundleImporterLoadTests.cs` is a `LoadAsync` unit suite despite the name). No measured problem; the only sizing controls in place are the 5-minute CLI transport timeout, `LineDiffer`'s `MaxInputLines`=4000 summary-only cap, and `MaxConcurrentImportSessions`=8. | First real bundle that times out, exhausts memory, or makes the import wizard's diff step unusable |
|
||||
| 25 | ~~**Phase-8 WP-4 target-scale load test** (10 sites x 500 instances x 75 tags = 37,500 subscriptions/site, 375,000 total)~~ **RESOLVED 2026-08-15 — run, measured, evidenced.** The 107-byte stub is replaced by a real full-scale run: **10 x 500 x 75 = 375,000 live tag subscriptions**, 37,518 tag updates/s achieved against 37,500 nominal (100.0%), **45,021,375 updates offered** over a 20-minute steady-state window on an M4 Pro / 14-core / 48 GB host with the 8-node `docker/` rig still running. Headline numbers: tag update latency **P50 0.88 ms / P99 4.57 ms / max 37.41 ms** end-to-end (DCL boundary to stream subscriber, the emit instant carried verbatim through production code); **0 events dropped** at 100 live subscribers; health report collect+ingest **P99 0.31 ms** with 10/10 sites tracked; debug view snapshot under load **P99 2.19 ms, 0 timeouts**; 500 instances deployed to a site in **2.6 s**; CPU **2.9% of the box**; working-set slope **+8.83 MB/min**. **11 clean passes, 1 pass with a caveat (F1: 20 min with zero gen-2 collections cannot fully settle the leak question — the 1-hour run would), 0 failures.** Harness: `tests/ZB.MOM.WW.ScadaBridge.LoadHarness/` (standalone Exe — the `Category=Performance` trait enables a filter but does NOT exclude by default, so a 20-minute test could not live in `PerformanceTests`) plus a CI-scale smoke `[Fact]` (`PerformanceTests/TargetScale/TargetScaleHarnessSmokeTests.cs`). Real: per-site ActorSystem + LocalDb, the real DCL via the documented `DataConnectionFactory.RegisterAdapter` seam, real `InstanceActor`s, real `SiteStreamManager`, real `StreamRelayActor` + production-capacity bounded channel, real S&F, real health collector/aggregator; only the socket hops are stood in for. **Results: `docs/plans/2026-08-15-target-scale-load-test-results.md`; design + deviations + thresholds: `docs/plans/2026-08-15-target-scale-load-test-design.md`.** | as before | Closed. Residual scope: `[xc-3]` site-event-log retention volume, `[xc-4]` audit-vs-central degradation, `[xc-5]` large-template flattening and `[xc-8]` UI responsiveness are NOT covered by this harness (central-cluster/browser concerns) and remain open WP-4 scope — see results doc SS6. | Closed 2026-08-15. |
|
||||
| 26 | **→ #48** Ipsen MES MoveIn tail: leak-test (`-LT`) receivers + routing, PLC-output-flag writes, `Z28062` BTDB data completeness | `docs/plans/2026-06-16-ipsen-mes-movein.md:409` ("Out of scope (future)"); design `2026-06-16-ipsen-mes-movein-design.md:58-60, :196-198` | Customer-site scope, not a platform gap. `-LT` routing needs an MES-receiver child + Galaxy reference that do not exist on the reactor template (any `-LT`/unknown suffix returns `WasSuccessful=false` with an "unsupported side/target" message by decision); `MoveInComplete`/`Successful`/`ErrorText` are **PLC-owned** by locked decision, so ScadaBridge deliberately does not write them; `Z28062` completeness is an operational data fix, not code. Note the separate alarm-status path already handles the suffix — `_LT` is stripped before side-scoping (`2026-06-30-mes-alarm-status-api.md:158`). | Ipsen creates the leak-test receiver + Galaxy reference, or asks ScadaBridge to own the PLC-output flags — otherwise a **candidate won't-do** (`[PERM]`) at the next Ipsen scope review |
|
||||
|
||||
@@ -58,7 +58,7 @@ The Host must bind configuration sections from `appsettings.json` to strongly-ty
|
||||
| `ScadaBridge:HealthMonitoring` | `HealthMonitoringOptions` | Health Monitoring | ReportInterval, OfflineTimeout |
|
||||
| `ScadaBridge:SiteEventLog` | `SiteEventLogOptions` | Site Event Logging | RetentionDays, MaxStorageMb, PurgeScheduleCron |
|
||||
| `ScadaBridge:Communication` | `CommunicationOptions` | Communication | DeploymentTimeout, LifecycleTimeout, QueryTimeout, TransportHeartbeatInterval, TransportFailureThreshold |
|
||||
| `ScadaBridge:Security` | `SecurityOptions` | Security & Auth | LdapServer, LdapPort, LdapUseTls, JwtSigningKey (HS256 key for the CLI/API bearer token from `/auth/token`; >= 32 bytes, validated on Central), JwtExpiryMinutes (bearer lifetime), IdleTimeoutMinutes (cookie idle window), RoleRefreshThresholdMinutes, RoleMappingCacheSeconds |
|
||||
| `ScadaBridge:Security` | `SecurityOptions` | Security & Auth | LdapServer, LdapPort, LdapUseTls, JwtSigningKey (HS256 key for the CLI/API bearer token from `/auth/token`; >= 32 bytes, validated on Central), JwtExpiryMinutes (bearer lifetime), IdleTimeoutMinutes (cookie idle window), RoleRefreshThresholdMinutes, RoleMappingCacheSeconds, LdapGroupRefresh (`Enabled`/`IntervalMinutes`/`CacheSeconds` — mid-session directory group re-query, ScadaBridge#46) |
|
||||
| `ScadaBridge:InboundApi` | `InboundApiOptions` | Inbound API | DefaultMethodTimeout, MaxRequestBodyBytes, ApiMethodCacheTtl, StandbyForwardPeerEndpoints (HTTP base URLs of the other central node — a standby relays `POST /api/*` there instead of answering 503), StandbyForwardTimeout |
|
||||
| `ScadaBridge:Notification` | `NotificationOptions` | Notification Service | (SMTP config is stored in the central config DB, not in appsettings) |
|
||||
| `ScadaBridge:NotificationOutbox` | `NotificationOutboxOptions` | Notification Outbox | Dispatcher poll interval, stuck-age threshold, retention window (delivery retry settings reuse the central SMTP configuration) |
|
||||
|
||||
@@ -52,22 +52,25 @@ Set in a local or docker-dev environment via the environment variable `ScadaBrid
|
||||
### Cookie session lifecycle
|
||||
|
||||
- **Idle timeout**: Configurable, default **30 minutes**. Without eligible activity within the idle window, the session is rejected and the user must re-login. Passive `/auth/ping`, activity prevalidation, and Blazor transport requests preserve the activity anchor and suppress sliding renewal; the activity endpoint renews only after authentication and antiforgery validation. Tracked via a `LastActivity` last-activity timestamp claim. The cookie's `ExpireTimeSpan` is set to the idle timeout and `SlidingExpiration` renews it on activity, so the cookie window and the explicit `OnValidatePrincipal` idle check use the **same** value and cannot contradict each other.
|
||||
- **Role-mapping refresh (LDAP-free)**: Configurable, default **15 minutes** (`SecurityOptions.RoleRefreshThresholdMinutes`). At login the session stores the user's raw LDAP groups (one `zb:group` claim each) plus a `zb:lastrolerefresh` anchor. Once the anchor is older than the threshold, `OnValidatePrincipal` re-runs the **DB-backed** `RoleMapper` on the stored groups — **with no LDAP call** — rebuilds the role/scope claims via the shared claim-builder, advances the anchor, and re-issues the cookie. Central role-mapping (DB) changes — including a **revoked** mapping that drops the user's roles, and changed site-scope rules — take effect within this window. Ordinary HTTP validation uses this threshold. A live circuit refreshes mappings in fresh scopes on authentication-state reads — display reads (`GetAuthenticationStateAsync`) at most once per `CookieAuthenticationStateProvider.NonAuthoritativeRefreshInterval` (10 s) per circuit — and mutation entrypoints (`GetAuthoritativeStateAsync`) require a successful current mapping before every dispatch.
|
||||
- **Role-mapping refresh (DB)**: Configurable, default **15 minutes** (`SecurityOptions.RoleRefreshThresholdMinutes`). At login the session stores the user's raw LDAP groups (one `zb:group` claim each) plus a `zb:lastrolerefresh` anchor. Once the anchor is older than the threshold, `OnValidatePrincipal` re-runs the **DB-backed** `RoleMapper` on the session's groups (the stored ones, or the ones just re-read from the directory — see the next item), rebuilds the role/scope claims via the shared claim-builder, advances the anchor, and re-issues the cookie. Central role-mapping (DB) changes — including a **revoked** mapping that drops the user's roles, and changed site-scope rules — take effect within this window. Ordinary HTTP validation uses this threshold. A live circuit refreshes mappings in fresh scopes on authentication-state reads — display reads (`GetAuthenticationStateAsync`) at most once per `CookieAuthenticationStateProvider.NonAuthoritativeRefreshInterval` (10 s) per circuit — and mutation entrypoints (`GetAuthoritativeStateAsync`) require a successful current mapping before every dispatch.
|
||||
- **Directory group re-query (ScadaBridge#46)**: Configurable, default **on, every 15 minutes** (`SecurityOptions.LdapGroupRefresh`: `Enabled` = `true`, `IntervalMinutes` = `15`, `CacheSeconds` = `60`; config `ScadaBridge:Security:LdapGroupRefresh:*`). Login also stamps a `zb:lastgrouprefresh` anchor. Once it is older than `IntervalMinutes`, the next validation — HTTP `OnValidatePrincipal`, a circuit display read or an authoritative mutation check — re-reads the user's groups from the directory through the shared library's passwordless `ILdapGroupLookup` (Auth `0.5.0`; it binds as the configured **service account**, never the user), replaces the stored `zb:group` claims, re-runs the role mapping, and advances the anchor. A directory group removal or addition therefore reaches an open session — browser and circuit — within the interval, without re-login. Its cadence is independent of `RoleRefreshThresholdMinutes` and of the 10 s circuit refresh: a circuit queries the directory at most once per interval. One user's answer is reused on a node for `CacheSeconds`, so the cookie path and every circuit of that user cost one directory query. A cookie issued before this feature (no anchor) is due on its first validation.
|
||||
- **Outcome policy.** *Directory unavailable* (connect/TLS, service-account bind or search failed on every endpoint, including `FallbackServers`) **keeps the stored groups and roles** and logs a warning — on every path, including the fail-closed authoritative check — and the anchor still advances, so an outage costs one attempt per interval rather than one per request (availability first, consistent with *LDAP Connection Failure* below). Any answer under which a **new login would now be refused for a user-side reason** — account **not found** (deleted/renamed), **disabled** (AD `userAccountControl`), **ambiguous** (two entries), or found with **no groups** — **ends the session** on every path: HTTP rejects the principal and clears the cookie (the user lands on `/login`), and a circuit's state becomes anonymous so the next gate denies. Ending the session (rather than leaving a signed-in, role-less session) is chosen because it is what the login path would decide and it cannot be mistaken for a working account. *Not supported* (LDAP configured with `BindMode = User`, i.e. no service account) or LDAP disabled keeps the stored groups; the feature is then inert and logs that once.
|
||||
- **Off switches.** `LdapGroupRefresh:Enabled = false` restores the pre-#46 behaviour (stored groups until next login). Under `ScadaBridge:Security:Auth:DisableLogin` no cookie validation runs at all, so no directory query is made. Bearer tokens and HTTP Basic are unaffected: a token's claims are authoritative for its lifetime, and Basic binds per request.
|
||||
- **Role-mapping cache**: `RoleMapper` reads the LDAP-group→role table and the scope rules of every Deployer mapping through a process-wide `RoleMappingCache` snapshot (`SecurityOptions.RoleMappingCacheSeconds`, default **30**; `0` disables). One refresh loads the whole table plus all Deployer scope rules, so a cache hit costs no database round-trip — previously every Basic-authenticated management request and every circuit auth-state read issued `GetAllMappingsAsync` plus one `GetScopeRulesForMappingAsync` per matched Deployer mapping. Without an explicit invalidation a mapping or scope-rule change is therefore visible on every node within the TTL. `RoleMappingCache.Invalidate()` discards the calling node's snapshot so its next mapping re-reads the database (a refresh started before an invalidation is never published over it); the Management Service's mapping/scope-rule handlers also publish `RoleMappingsInvalidated` on the central cache-invalidation topic, so the peer central node discards its snapshot too (audit D1, ScadaBridge#100 item 3) and the TTL is only the fallback. `RoleMappingCacheSeconds = 0` bypasses the snapshot and reads only the matched mappings' scope rules per call.
|
||||
|
||||
#### Session Validation (`OnValidatePrincipal`)
|
||||
- The cookie principal is built at login by a **single shared claim-builder** (`SessionClaimBuilder`). The `OnValidatePrincipal` role-refresh path rebuilds the principal through the **same** builder, so the login and refresh claim shapes cannot drift.
|
||||
- **Failure policy**: ordinary cookie validation and display-state refresh retain existing grants on a mapping lookup error. Authoritative circuit mutation checks fail closed on the same error, so a mutation cannot proceed using grants that could not be refreshed. Idle timeout rejects the principal.
|
||||
|
||||
> **Residual limitation — live LDAP group-membership changes (follow-up).** The
|
||||
> mid-session refresh re-maps the **stored** groups against the central database; it does
|
||||
> **not** re-query LDAP, so a change to the user's actual **group membership** in the
|
||||
> directory is picked up only at **next login**. A live group re-query for an active
|
||||
> session would require a new passwordless service-account group-search method on the
|
||||
> shared `ZB.MOM.WW.Auth.Ldap` library, which is an **external NuGet package** and exposes
|
||||
> only `AuthenticateAsync(username, password, ct)` (no standalone group search). Adding
|
||||
> that method is tracked as a follow-up. Until then: central role-mapping/scope changes are
|
||||
> reflected during HTTP refresh and immediately at circuit mutation checks; directory group-membership changes require re-login.
|
||||
> **Directory group-membership changes (resolved by ScadaBridge#46).** Earlier builds re-mapped
|
||||
> only the **stored** groups mid-session, so a directory group change waited for the next login;
|
||||
> the shared `ZB.MOM.WW.Auth.Ldap` library had no passwordless group search. Auth `0.5.0` added
|
||||
> `ILdapGroupLookup`, and the *Directory group re-query* above now re-reads an active session's
|
||||
> groups every `LdapGroupRefresh:IntervalMinutes` (default 15). Remaining bounds: a change lands
|
||||
> within that interval (plus up to `CacheSeconds` of per-node answer reuse), not instantly; a
|
||||
> bearer token keeps its claims until it expires (≤ `JwtExpiryMinutes`); and a deployment that
|
||||
> authenticates with `BindMode = User` (no service account) cannot re-query, so it keeps the
|
||||
> next-login behaviour.
|
||||
|
||||
### Load Balancer Compatibility
|
||||
- Neither the cookie nor the bearer token needs server-side session state. A load balancer in front of the central cluster can route requests to either node without sticky sessions or a shared session store.
|
||||
@@ -76,8 +79,8 @@ Set in a local or docker-dev environment via the environment variable `ScadaBrid
|
||||
## LDAP Connection Failure
|
||||
|
||||
- **New logins**: If the LDAP/AD server is unreachable, login attempts **fail**. Users cannot be authenticated without LDAP.
|
||||
- **Active sessions**: Users with a valid (not-idle-timed-out) session can **continue operating** with their current roles during an LDAP outage. Interactive cookie sessions never re-query LDAP mid-session (the mid-session role-mapping refresh is DB-only — see *Session Validation* above), so a brief LDAP outage does not disrupt engineers mid-work; central role-mapping changes still apply within the refresh window regardless of LDAP availability. A CLI holding a bearer token likewise keeps working until the token expires; HTTP Basic calls bind per request and fail during the outage.
|
||||
- **Recovery (group-membership changes)**: Because the mid-session refresh is LDAP-free, a change to a user's **directory group membership** is picked up at the user's **next login** (when LDAP is queried again), not mid-session — see the *Residual limitation* note above.
|
||||
- **Active sessions**: Users with a valid (not-idle-timed-out) session can **continue operating** with their current roles during an LDAP outage. An interactive cookie session's periodic directory group re-query treats an unreachable directory as "keep the stored groups" (see *Directory group re-query* above), so a brief LDAP outage does not disrupt engineers mid-work; central role-mapping changes still apply within the refresh window regardless of LDAP availability. A CLI holding a bearer token likewise keeps working until the token expires; HTTP Basic calls bind per request and fail during the outage.
|
||||
- **Recovery (group-membership changes)**: once the directory answers again, the next due re-query (within `LdapGroupRefresh:IntervalMinutes`) applies any group-membership change made during the outage — no re-login needed.
|
||||
|
||||
## Roles
|
||||
|
||||
|
||||
Reference in New Issue
Block a user