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(); } }