namespace ZB.MOM.WW.OtOpcUa.Core.Authorization; /// /// Per-session authorization state cached on the OPC UA session object + keyed on the /// session id. Captures the LDAP group memberships resolved at sign-in, the generation /// the membership was resolved against, and the bounded freshness window. /// /// /// Per decision #151 the membership is bounded by /// (default 15 min). After that, the next hot-path authz call re-resolves LDAP group /// memberships; failure to re-resolve (LDAP unreachable) flips the session to fail-closed /// until a refresh succeeds. /// /// Per decision #152 (default 5 min) is separate from /// Phase 6.1's availability-oriented 24h cache — beyond this window the evaluator returns /// regardless of config-cache warmth. /// public sealed record UserAuthorizationState { /// Opaque session id (reuse OPC UA session handle when possible). public required string SessionId { get; init; } /// Cluster the session is scoped to — every request targets nodes in this cluster. public required string ClusterId { get; init; } /// /// LDAP groups the user is a member of as resolved at sign-in / last membership refresh. /// Case comparison is handled downstream by the evaluator (OrdinalIgnoreCase). /// public required IReadOnlyList LdapGroups { get; init; } /// Timestamp when was last resolved from the directory. public required DateTime MembershipResolvedUtc { get; init; } /// /// Trie generation the session is currently bound to. When /// moves to a new generation, the session's /// (AuthGenerationId, MembershipVersion) stamp no longer matches its /// MonitoredItems and they re-evaluate on next publish (decision #153). /// public required long AuthGenerationId { get; init; } /// /// Monotonic counter incremented every time membership is re-resolved. Combined with /// into the subscription stamp per decision #153. /// public required long MembershipVersion { get; init; } /// Bounded membership freshness window; past this the next authz call refreshes. public TimeSpan MembershipFreshnessInterval { get; init; } = TimeSpan.FromMinutes(15); /// Hard staleness ceiling — beyond this, the evaluator fails closed. public TimeSpan AuthCacheMaxStaleness { get; init; } = TimeSpan.FromMinutes(5); /// /// True when - exceeds /// . The evaluator short-circuits to NotGranted /// whenever this is true. /// public bool IsStale(DateTime utcNow) => utcNow - MembershipResolvedUtc > AuthCacheMaxStaleness; /// /// True when membership is past its freshness interval but still within the staleness /// ceiling — a signal to the caller to kick off an async refresh, while the current /// call still evaluates against the cached memberships. /// public bool NeedsRefresh(DateTime utcNow) => !IsStale(utcNow) && utcNow - MembershipResolvedUtc > MembershipFreshnessInterval; }