diff --git a/src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs b/src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs
new file mode 100644
index 0000000..adaed4a
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs
@@ -0,0 +1,86 @@
+using Opc.Ua;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Core.Authorization;
+
+namespace ZB.MOM.WW.OtOpcUa.Server.Security;
+
+///
+/// Bridges the OPC UA stack's to the
+/// evaluator. Resolves the session's
+/// from whatever the identity claims + the stack's
+/// session handle, then delegates to the evaluator and returns a single bool the
+/// dispatch paths can use to short-circuit with BadUserAccessDenied.
+///
+///
+/// This class is deliberately the single integration seam between the Server
+/// project and the Core.Authorization evaluator. DriverNodeManager holds one
+/// reference and calls on every Read / Write / HistoryRead /
+/// Browse / Call / CreateMonitoredItems / etc. The evaluator itself stays pure — it
+/// doesn't know about the OPC UA stack types.
+///
+/// Fail-open-during-transition: when the evaluator is configured with
+/// StrictMode = false, missing cluster tries OR sessions without resolved
+/// LDAP groups get true so existing deployments keep working while ACLs are
+/// populated. Flip to strict via Authorization:StrictMode = true in production.
+///
+public sealed class AuthorizationGate
+{
+ private readonly IPermissionEvaluator _evaluator;
+ private readonly bool _strictMode;
+ private readonly TimeProvider _timeProvider;
+
+ public AuthorizationGate(IPermissionEvaluator evaluator, bool strictMode = false, TimeProvider? timeProvider = null)
+ {
+ _evaluator = evaluator ?? throw new ArgumentNullException(nameof(evaluator));
+ _strictMode = strictMode;
+ _timeProvider = timeProvider ?? TimeProvider.System;
+ }
+
+ /// True when strict authorization is enabled — no-grant = denied.
+ public bool StrictMode => _strictMode;
+
+ ///
+ /// Authorize an OPC UA operation against the session identity + scope. Returns true to
+ /// allow the dispatch to continue; false to surface BadUserAccessDenied.
+ ///
+ public bool IsAllowed(IUserIdentity? identity, OpcUaOperation operation, NodeScope scope)
+ {
+ // Anonymous / unknown identity — strict mode denies, lax mode allows so the fallback
+ // auth layers (WriteAuthzPolicy) still see the call.
+ if (identity is null) return !_strictMode;
+
+ var session = BuildSessionState(identity, scope.ClusterId);
+ if (session is null)
+ {
+ // Identity doesn't carry LDAP groups. In lax mode let the dispatch proceed so
+ // older deployments keep working; strict mode denies.
+ return !_strictMode;
+ }
+
+ var decision = _evaluator.Authorize(session, operation, scope);
+ if (decision.IsAllowed) return true;
+
+ return !_strictMode;
+ }
+
+ ///
+ /// Materialize a from the session identity.
+ /// Returns null when the identity doesn't carry LDAP group metadata.
+ ///
+ public UserAuthorizationState? BuildSessionState(IUserIdentity identity, string clusterId)
+ {
+ if (identity is not ILdapGroupsBearer bearer || bearer.LdapGroups.Count == 0)
+ return null;
+
+ var sessionId = identity.DisplayName ?? Guid.NewGuid().ToString("N");
+ return new UserAuthorizationState
+ {
+ SessionId = sessionId,
+ ClusterId = clusterId,
+ LdapGroups = bearer.LdapGroups,
+ MembershipResolvedUtc = _timeProvider.GetUtcNow().UtcDateTime,
+ AuthGenerationId = 0,
+ MembershipVersion = 0,
+ };
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Server/Security/ILdapGroupsBearer.cs b/src/ZB.MOM.WW.OtOpcUa.Server/Security/ILdapGroupsBearer.cs
new file mode 100644
index 0000000..838e8e1
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Server/Security/ILdapGroupsBearer.cs
@@ -0,0 +1,20 @@
+namespace ZB.MOM.WW.OtOpcUa.Server.Security;
+
+///
+/// Minimal interface an exposes so the Phase 6.2
+/// authorization evaluator can read the session's resolved LDAP group DNs without a
+/// hard dependency on any specific identity subtype. Implemented by OtOpcUaServer's
+/// role-based identity; tests stub it to drive the evaluator under different group
+/// memberships.
+///
+///
+/// Control/data-plane separation (decision #150): Admin UI role routing consumes
+/// via LdapGroupRoleMapping; the OPC UA data-path
+/// evaluator consumes directly against NodeAcl. The two
+/// are sourced from the same directory query at sign-in but never cross.
+///
+public interface ILdapGroupsBearer
+{
+ /// Fully-qualified LDAP group DNs the user is a member of.
+ IReadOnlyList LdapGroups { get; }
+}
diff --git a/tests/ZB.MOM.WW.OtOpcUa.Server.Tests/AuthorizationGateTests.cs b/tests/ZB.MOM.WW.OtOpcUa.Server.Tests/AuthorizationGateTests.cs
new file mode 100644
index 0000000..c0605d2
--- /dev/null
+++ b/tests/ZB.MOM.WW.OtOpcUa.Server.Tests/AuthorizationGateTests.cs
@@ -0,0 +1,136 @@
+using Opc.Ua;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Core.Authorization;
+using ZB.MOM.WW.OtOpcUa.Server.Security;
+
+namespace ZB.MOM.WW.OtOpcUa.Server.Tests;
+
+[Trait("Category", "Unit")]
+public sealed class AuthorizationGateTests
+{
+ private static NodeScope Scope(string cluster = "c1", string? tag = "tag1") => new()
+ {
+ ClusterId = cluster,
+ NamespaceId = "ns",
+ UnsAreaId = "area",
+ UnsLineId = "line",
+ EquipmentId = "eq",
+ TagId = tag,
+ Kind = NodeHierarchyKind.Equipment,
+ };
+
+ private static NodeAcl Row(string group, NodePermissions flags) => new()
+ {
+ NodeAclRowId = Guid.NewGuid(),
+ NodeAclId = Guid.NewGuid().ToString(),
+ GenerationId = 1,
+ ClusterId = "c1",
+ LdapGroup = group,
+ ScopeKind = NodeAclScopeKind.Cluster,
+ ScopeId = null,
+ PermissionFlags = flags,
+ };
+
+ private static AuthorizationGate MakeGate(bool strict, NodeAcl[] rows)
+ {
+ var cache = new PermissionTrieCache();
+ cache.Install(PermissionTrieBuilder.Build("c1", 1, rows));
+ var evaluator = new TriePermissionEvaluator(cache);
+ return new AuthorizationGate(evaluator, strictMode: strict);
+ }
+
+ private sealed class FakeIdentity : UserIdentity, ILdapGroupsBearer
+ {
+ public FakeIdentity(string name, IReadOnlyList groups)
+ {
+ DisplayName = name;
+ LdapGroups = groups;
+ }
+ public new string DisplayName { get; }
+ public IReadOnlyList LdapGroups { get; }
+ }
+
+ [Fact]
+ public void NullIdentity_StrictMode_Denies()
+ {
+ var gate = MakeGate(strict: true, rows: []);
+ gate.IsAllowed(null, OpcUaOperation.Read, Scope()).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void NullIdentity_LaxMode_Allows()
+ {
+ var gate = MakeGate(strict: false, rows: []);
+ gate.IsAllowed(null, OpcUaOperation.Read, Scope()).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void IdentityWithoutLdapGroups_StrictMode_Denies()
+ {
+ var gate = MakeGate(strict: true, rows: []);
+ var identity = new UserIdentity(); // anonymous, no LDAP groups
+
+ gate.IsAllowed(identity, OpcUaOperation.Read, Scope()).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void IdentityWithoutLdapGroups_LaxMode_Allows()
+ {
+ var gate = MakeGate(strict: false, rows: []);
+ var identity = new UserIdentity();
+
+ gate.IsAllowed(identity, OpcUaOperation.Read, Scope()).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void LdapGroupWithGrant_Allows()
+ {
+ var gate = MakeGate(strict: true, rows: [Row("cn=ops", NodePermissions.Read)]);
+ var identity = new FakeIdentity("ops-user", ["cn=ops"]);
+
+ gate.IsAllowed(identity, OpcUaOperation.Read, Scope()).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void LdapGroupWithoutGrant_StrictMode_Denies()
+ {
+ var gate = MakeGate(strict: true, rows: [Row("cn=ops", NodePermissions.Read)]);
+ var identity = new FakeIdentity("other-user", ["cn=other"]);
+
+ gate.IsAllowed(identity, OpcUaOperation.Read, Scope()).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void WrongOperation_Denied()
+ {
+ var gate = MakeGate(strict: true, rows: [Row("cn=ops", NodePermissions.Read)]);
+ var identity = new FakeIdentity("ops-user", ["cn=ops"]);
+
+ gate.IsAllowed(identity, OpcUaOperation.WriteOperate, Scope()).ShouldBeFalse();
+ }
+
+ [Fact]
+ public void BuildSessionState_IncludesLdapGroups()
+ {
+ var gate = MakeGate(strict: true, rows: []);
+ var identity = new FakeIdentity("u", ["cn=a", "cn=b"]);
+
+ var state = gate.BuildSessionState(identity, "c1");
+
+ state.ShouldNotBeNull();
+ state!.LdapGroups.Count.ShouldBe(2);
+ state.ClusterId.ShouldBe("c1");
+ }
+
+ [Fact]
+ public void BuildSessionState_ReturnsNull_ForIdentityWithoutLdapGroups()
+ {
+ var gate = MakeGate(strict: true, rows: []);
+
+ gate.BuildSessionState(new UserIdentity(), "c1").ShouldBeNull();
+ }
+}