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();
|
var cache = new PermissionTrieCache();
|
||||||
cache.Install(PermissionTrieBuilder.Build(nodeOptions.ClusterId, generationId, rows));
|
cache.Install(PermissionTrieBuilder.Build(nodeOptions.ClusterId, generationId, rows));
|
||||||
var evaluator = new TriePermissionEvaluator(cache);
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -28,18 +28,46 @@ namespace ZB.MOM.WW.OtOpcUa.Server.Security;
|
|||||||
/// <see cref="AuthorizationVerdict.Denied"/> verdict from an authored deny rule is a
|
/// <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
|
/// definite decision and is honoured in both strict and lax mode — lax mode never
|
||||||
/// overrides a deny.</para>
|
/// 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>
|
/// </remarks>
|
||||||
public sealed class AuthorizationGate
|
public sealed class AuthorizationGate
|
||||||
{
|
{
|
||||||
private readonly IPermissionEvaluator _evaluator;
|
private readonly IPermissionEvaluator _evaluator;
|
||||||
private readonly bool _strictMode;
|
private readonly bool _strictMode;
|
||||||
private readonly TimeProvider _timeProvider;
|
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));
|
_evaluator = evaluator ?? throw new ArgumentNullException(nameof(evaluator));
|
||||||
_strictMode = strictMode;
|
_strictMode = strictMode;
|
||||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
_trieCache = trieCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>True when strict authorization is enabled — no-grant = denied.</summary>
|
/// <summary>True when strict authorization is enabled — no-grant = denied.</summary>
|
||||||
@@ -90,13 +118,20 @@ public sealed class AuthorizationGate
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
var sessionId = identity.DisplayName ?? Guid.NewGuid().ToString("N");
|
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
|
return new UserAuthorizationState
|
||||||
{
|
{
|
||||||
SessionId = sessionId,
|
SessionId = sessionId,
|
||||||
ClusterId = clusterId,
|
ClusterId = clusterId,
|
||||||
LdapGroups = bearer.LdapGroups,
|
LdapGroups = bearer.LdapGroups,
|
||||||
MembershipResolvedUtc = _timeProvider.GetUtcNow().UtcDateTime,
|
MembershipResolvedUtc = _timeProvider.GetUtcNow().UtcDateTime,
|
||||||
AuthGenerationId = 0,
|
AuthGenerationId = authGenerationId,
|
||||||
MembershipVersion = 0,
|
MembershipVersion = 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ public sealed class AuthorizationGateTests
|
|||||||
var cache = new PermissionTrieCache();
|
var cache = new PermissionTrieCache();
|
||||||
cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
|
cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
|
||||||
var evaluator = new TriePermissionEvaluator(cache);
|
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
|
private sealed class FakeIdentity : UserIdentity, ILdapGroupsBearer
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ public sealed class BrowseGatingTests
|
|||||||
var cache = new PermissionTrieCache();
|
var cache = new PermissionTrieCache();
|
||||||
cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
|
cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
|
||||||
var evaluator = new TriePermissionEvaluator(cache);
|
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);
|
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();
|
var cache = new PermissionTrieCache();
|
||||||
cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
|
cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
|
||||||
var evaluator = new TriePermissionEvaluator(cache);
|
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);
|
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();
|
var cache = new PermissionTrieCache();
|
||||||
cache.Install(PermissionTrieBuilder.Build(Cluster, 1, rows));
|
cache.Install(PermissionTrieBuilder.Build(Cluster, 1, rows));
|
||||||
var evaluator = new TriePermissionEvaluator(cache);
|
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);
|
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();
|
var cache = new PermissionTrieCache();
|
||||||
cache.Install(PermissionTrieBuilder.Build(Cluster, 1, [aclRow], paths));
|
cache.Install(PermissionTrieBuilder.Build(Cluster, 1, [aclRow], paths));
|
||||||
var evaluator = new TriePermissionEvaluator(cache);
|
var evaluator = new TriePermissionEvaluator(cache);
|
||||||
var gate = new AuthorizationGate(evaluator, strictMode: true);
|
var gate = new AuthorizationGate(evaluator, strictMode: true, trieCache: cache);
|
||||||
|
|
||||||
_ = content;
|
_ = content;
|
||||||
return (gate, resolver);
|
return (gate, resolver);
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ public sealed class MonitoredItemGatingTests
|
|||||||
var cache = new PermissionTrieCache();
|
var cache = new PermissionTrieCache();
|
||||||
cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
|
cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
|
||||||
var evaluator = new TriePermissionEvaluator(cache);
|
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);
|
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();
|
var cache = new PermissionTrieCache();
|
||||||
cache.Install(PermissionTrieBuilder.Build(ClusterId, 1, AclMatrix()));
|
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
|
private sealed class LdapBoundIdentity : UserIdentity, ILdapGroupsBearer
|
||||||
|
|||||||
Reference in New Issue
Block a user