fix(core): resolve High code-review findings (Core-001, Core-002)
Core-001: swap the authorization-cache defaults so MembershipFreshnessInterval (5 min, inner re-resolve trigger) is strictly less than AuthCacheMaxStaleness (15 min, fail-closed ceiling), so NeedsRefresh's warm-refresh path is reachable. Core-002: TriePermissionEvaluator.Authorize now compares the trie's GenerationId against the session's AuthGenerationId and re-fetches the session's bound generation on mismatch, failing closed when that generation has been pruned. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,21 @@ public sealed class TriePermissionEvaluator : IPermissionEvaluator
|
||||
var trie = _cache.GetTrie(scope.ClusterId);
|
||||
if (trie is null) return AuthorizationDecision.NotGranted();
|
||||
|
||||
// Decision #153 / Phase 6.2 adversarial-review item #3 (redundancy-safe invalidation):
|
||||
// the GetTrie shortcut returns whatever generation the cache currently holds, which may
|
||||
// have advanced past the generation this session was bound to (another node published).
|
||||
// Evaluate against the session's *bound* generation so a grant added or removed in a
|
||||
// newer generation cannot silently take effect mid-session, and so the provenance in the
|
||||
// AuthorizationDecision reports the generation that actually produced the verdict.
|
||||
if (trie.GenerationId != session.AuthGenerationId)
|
||||
{
|
||||
trie = _cache.GetTrie(scope.ClusterId, session.AuthGenerationId);
|
||||
|
||||
// The session's bound generation has been pruned out of the cache — fail closed and
|
||||
// force the caller to re-resolve the session's auth state before retrying.
|
||||
if (trie is null) return AuthorizationDecision.NotGranted();
|
||||
}
|
||||
|
||||
var matches = trie.CollectMatches(scope, session.LdapGroups);
|
||||
if (matches.Count == 0) return AuthorizationDecision.NotGranted();
|
||||
|
||||
|
||||
@@ -7,13 +7,19 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Authorization;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Per decision #151 the membership is bounded by <see cref="MembershipFreshnessInterval"/>
|
||||
/// (default 15 min). After that, the next hot-path authz call re-resolves LDAP group
|
||||
/// (default 5 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 <see cref="AuthCacheMaxStaleness"/> (default 5 min) is separate from
|
||||
/// Per decision #152 <see cref="AuthCacheMaxStaleness"/> (default 15 min) is separate from
|
||||
/// Phase 6.1's availability-oriented 24h cache — beyond this window the evaluator returns
|
||||
/// <see cref="AuthorizationVerdict.NotGranted"/> regardless of config-cache warmth.
|
||||
///
|
||||
/// The freshness window is the inner trigger and the staleness ceiling the outer hard
|
||||
/// limit: <see cref="MembershipFreshnessInterval"/> MUST be strictly less than
|
||||
/// <see cref="AuthCacheMaxStaleness"/> so that <see cref="NeedsRefresh"/> ("re-resolve
|
||||
/// while still serving cached memberships") has a non-empty window before
|
||||
/// <see cref="IsStale"/> fails the session closed.
|
||||
/// </remarks>
|
||||
public sealed record UserAuthorizationState
|
||||
{
|
||||
@@ -47,10 +53,10 @@ public sealed record UserAuthorizationState
|
||||
public required long MembershipVersion { get; init; }
|
||||
|
||||
/// <summary>Bounded membership freshness window; past this the next authz call refreshes.</summary>
|
||||
public TimeSpan MembershipFreshnessInterval { get; init; } = TimeSpan.FromMinutes(15);
|
||||
public TimeSpan MembershipFreshnessInterval { get; init; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>Hard staleness ceiling — beyond this, the evaluator fails closed.</summary>
|
||||
public TimeSpan AuthCacheMaxStaleness { get; init; } = TimeSpan.FromMinutes(5);
|
||||
public TimeSpan AuthCacheMaxStaleness { get; init; } = TimeSpan.FromMinutes(15);
|
||||
|
||||
/// <summary>
|
||||
/// True when <paramref name="utcNow"/> - <see cref="MembershipResolvedUtc"/> exceeds
|
||||
|
||||
Reference in New Issue
Block a user