fix(server): wire PermissionTrieCache into AuthorizationGate for generation pinning
Core-002 fixed TriePermissionEvaluator to evaluate each request against the session's bound AuthGenerationId rather than whatever the cache currently holds. AuthorizationGate.BuildSessionState was not updated at the same time: it hardcoded AuthGenerationId = 0, so the evaluator's GetTrie(cluster, 0) call returned null for any generation != 0, causing every gated operation to silently fail with NotGranted regardless of actual grants. The 42 gate/matrix/deferred-hardening tests all started failing as a result. Fix: add an optional PermissionTrieCache parameter to AuthorizationGate; BuildSessionState now stamps AuthGenerationId from the cache's current generation for the session's cluster. AuthorizationBootstrap.BuildGateAsync passes the cache it creates. All 7 test MakeGate helpers updated to pass the cache so tests produce a valid AuthGenerationId. 433/433 server tests now pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -86,7 +86,10 @@ public sealed class AuthorizationBootstrap(
|
||||
var cache = new PermissionTrieCache();
|
||||
cache.Install(PermissionTrieBuilder.Build(nodeOptions.ClusterId, generationId, rows));
|
||||
var evaluator = new TriePermissionEvaluator(cache);
|
||||
return new AuthorizationGate(evaluator, strictMode: nodeOptions.Authorization.StrictMode);
|
||||
// Pass the cache so AuthorizationGate.BuildSessionState can stamp AuthGenerationId
|
||||
// correctly (Core-002 generation-pinning fix requires the session to carry the
|
||||
// current generation, not a placeholder 0).
|
||||
return new AuthorizationGate(evaluator, strictMode: nodeOptions.Authorization.StrictMode, trieCache: cache);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -28,18 +28,46 @@ namespace ZB.MOM.WW.OtOpcUa.Server.Security;
|
||||
/// <see cref="AuthorizationVerdict.Denied"/> verdict from an authored deny rule is a
|
||||
/// definite decision and is honoured in both strict and lax mode — lax mode never
|
||||
/// overrides a deny.</para>
|
||||
///
|
||||
/// <para><see cref="BuildSessionState"/> stamps <see cref="UserAuthorizationState.AuthGenerationId"/>
|
||||
/// from the <see cref="PermissionTrieCache"/> current generation so the
|
||||
/// <see cref="TriePermissionEvaluator"/> generation-pinning check (Core-002) evaluates the
|
||||
/// session against the trie that was current when the session was resolved, not an out-of-
|
||||
/// date generation 0 placeholder.</para>
|
||||
/// </remarks>
|
||||
public sealed class AuthorizationGate
|
||||
{
|
||||
private readonly IPermissionEvaluator _evaluator;
|
||||
private readonly bool _strictMode;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly PermissionTrieCache? _trieCache;
|
||||
|
||||
public AuthorizationGate(IPermissionEvaluator evaluator, bool strictMode = false, TimeProvider? timeProvider = null)
|
||||
/// <summary>
|
||||
/// Constructs an <see cref="AuthorizationGate"/>.
|
||||
/// </summary>
|
||||
/// <param name="evaluator">The permission evaluator (required).</param>
|
||||
/// <param name="strictMode">
|
||||
/// When <c>true</c>, indeterminate (not-granted) decisions deny rather than allow.
|
||||
/// Default <c>false</c> for lax-mode deployments still populating ACLs.
|
||||
/// </param>
|
||||
/// <param name="timeProvider">Optional time provider for staleness checks.</param>
|
||||
/// <param name="trieCache">
|
||||
/// Optional cache reference used by <see cref="BuildSessionState"/> to stamp the
|
||||
/// session's <c>AuthGenerationId</c> with the current trie generation, so the
|
||||
/// evaluator's generation-pinning check evaluates against the correct grant set.
|
||||
/// When null, <c>AuthGenerationId</c> is set to 0 (acceptable only when the cache
|
||||
/// was seeded with generation 0, as in some test scenarios).
|
||||
/// </param>
|
||||
public AuthorizationGate(
|
||||
IPermissionEvaluator evaluator,
|
||||
bool strictMode = false,
|
||||
TimeProvider? timeProvider = null,
|
||||
PermissionTrieCache? trieCache = null)
|
||||
{
|
||||
_evaluator = evaluator ?? throw new ArgumentNullException(nameof(evaluator));
|
||||
_strictMode = strictMode;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_trieCache = trieCache;
|
||||
}
|
||||
|
||||
/// <summary>True when strict authorization is enabled — no-grant = denied.</summary>
|
||||
@@ -90,13 +118,20 @@ public sealed class AuthorizationGate
|
||||
return null;
|
||||
|
||||
var sessionId = identity.DisplayName ?? Guid.NewGuid().ToString("N");
|
||||
|
||||
// Stamp the session with the cache's current generation so the evaluator's
|
||||
// generation-pinning check (Core-002 / TriePermissionEvaluator.Authorize) evaluates
|
||||
// against the trie that was current at resolve time, not a stale generation-0 placeholder.
|
||||
// When no cache is available (some test scenarios), fall back to 0.
|
||||
var authGenerationId = _trieCache?.CurrentGenerationId(clusterId) ?? 0L;
|
||||
|
||||
return new UserAuthorizationState
|
||||
{
|
||||
SessionId = sessionId,
|
||||
ClusterId = clusterId,
|
||||
LdapGroups = bearer.LdapGroups,
|
||||
MembershipResolvedUtc = _timeProvider.GetUtcNow().UtcDateTime,
|
||||
AuthGenerationId = 0,
|
||||
AuthGenerationId = authGenerationId,
|
||||
MembershipVersion = 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ public sealed class AuthorizationGateTests
|
||||
var cache = new PermissionTrieCache();
|
||||
cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
|
||||
var evaluator = new TriePermissionEvaluator(cache);
|
||||
return new AuthorizationGate(evaluator, strictMode: strict);
|
||||
// Pass the cache so BuildSessionState stamps AuthGenerationId = 1 (Core-002 fix).
|
||||
return new AuthorizationGate(evaluator, strictMode: strict, trieCache: cache);
|
||||
}
|
||||
|
||||
private sealed class FakeIdentity : UserIdentity, ILdapGroupsBearer
|
||||
|
||||
@@ -141,7 +141,7 @@ public sealed class BrowseGatingTests
|
||||
var cache = new PermissionTrieCache();
|
||||
cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
|
||||
var evaluator = new TriePermissionEvaluator(cache);
|
||||
return new AuthorizationGate(evaluator, strictMode: strict);
|
||||
return new AuthorizationGate(evaluator, strictMode: strict, trieCache: cache);
|
||||
}
|
||||
|
||||
private static IUserIdentity NewIdentity(string name, params string[] groups) => new FakeIdentity(name, groups);
|
||||
|
||||
@@ -209,7 +209,7 @@ public sealed class CallGatingTests
|
||||
var cache = new PermissionTrieCache();
|
||||
cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
|
||||
var evaluator = new TriePermissionEvaluator(cache);
|
||||
return new AuthorizationGate(evaluator, strictMode: strict);
|
||||
return new AuthorizationGate(evaluator, strictMode: strict, trieCache: cache);
|
||||
}
|
||||
|
||||
private static IUserIdentity NewIdentity(string name, params string[] groups) => new FakeIdentity(name, groups);
|
||||
|
||||
@@ -375,7 +375,7 @@ public sealed class DeferredGateHardeningTests
|
||||
var cache = new PermissionTrieCache();
|
||||
cache.Install(PermissionTrieBuilder.Build(Cluster, 1, rows));
|
||||
var evaluator = new TriePermissionEvaluator(cache);
|
||||
return new AuthorizationGate(evaluator, strictMode: strict);
|
||||
return new AuthorizationGate(evaluator, strictMode: strict, trieCache: cache);
|
||||
}
|
||||
|
||||
private static IUserIdentity NewIdentity(string name, params string[] groups) => new FakeIdentity(name, groups);
|
||||
|
||||
@@ -118,7 +118,7 @@ public sealed class EquipmentIdentificationAuthzTests
|
||||
var cache = new PermissionTrieCache();
|
||||
cache.Install(PermissionTrieBuilder.Build(Cluster, 1, [aclRow], paths));
|
||||
var evaluator = new TriePermissionEvaluator(cache);
|
||||
var gate = new AuthorizationGate(evaluator, strictMode: true);
|
||||
var gate = new AuthorizationGate(evaluator, strictMode: true, trieCache: cache);
|
||||
|
||||
_ = content;
|
||||
return (gate, resolver);
|
||||
|
||||
@@ -128,7 +128,7 @@ public sealed class MonitoredItemGatingTests
|
||||
var cache = new PermissionTrieCache();
|
||||
cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
|
||||
var evaluator = new TriePermissionEvaluator(cache);
|
||||
return new AuthorizationGate(evaluator, strictMode: strict);
|
||||
return new AuthorizationGate(evaluator, strictMode: strict, trieCache: cache);
|
||||
}
|
||||
|
||||
private static IUserIdentity NewIdentity(string name, params string[] groups) => new FakeIdentity(name, groups);
|
||||
|
||||
@@ -122,7 +122,7 @@ public sealed class ThreeUserInteropMatrixTests
|
||||
{
|
||||
var cache = new PermissionTrieCache();
|
||||
cache.Install(PermissionTrieBuilder.Build(ClusterId, 1, AclMatrix()));
|
||||
return new AuthorizationGate(new TriePermissionEvaluator(cache), strictMode: true);
|
||||
return new AuthorizationGate(new TriePermissionEvaluator(cache), strictMode: true, trieCache: cache);
|
||||
}
|
||||
|
||||
private sealed class LdapBoundIdentity : UserIdentity, ILdapGroupsBearer
|
||||
|
||||
Reference in New Issue
Block a user