diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/RoleMapper.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/RoleMapper.cs index 3eb7e690..8e0716e6 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/RoleMapper.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/RoleMapper.cs @@ -1,3 +1,5 @@ +using ZB.MOM.WW.OtOpcUa.Configuration.Entities; + namespace ZB.MOM.WW.OtOpcUa.Security.Ldap; /// @@ -24,4 +26,21 @@ public static class RoleMapper } return [.. roles]; } + + /// + /// Merge the appsettings-derived baseline roles with system-wide DB grants. DB rows are + /// additive; cluster-scoped rows (IsSystemWide == false) are ignored under the global model. + /// + /// Roles already resolved from appsettings (or the dev stub). + /// LdapGroupRoleMapping rows for the user's groups (from GetByGroupsAsync). + public static IReadOnlyList Merge( + IReadOnlyCollection baselineRoles, + IReadOnlyCollection dbRows) + { + var roles = new HashSet(baselineRoles, StringComparer.OrdinalIgnoreCase); + foreach (var row in dbRows) + if (row.IsSystemWide) + roles.Add(row.Role.ToString()); + return [.. roles]; + } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/RoleMapperTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/RoleMapperTests.cs index 9af7aba1..b94a445e 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/RoleMapperTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/RoleMapperTests.cs @@ -1,5 +1,7 @@ using Shouldly; using Xunit; +using ZB.MOM.WW.OtOpcUa.Configuration.Entities; +using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Security.Ldap; namespace ZB.MOM.WW.OtOpcUa.Security.Tests; @@ -59,4 +61,22 @@ public sealed class RoleMapperTests roles.ShouldBe(new[] { "FleetAdmin" }); } + + [Fact] + public void Merge_unions_baseline_and_systemwide_db_roles() + { + var rows = new[] + { + new LdapGroupRoleMapping { LdapGroup = "g1", Role = AdminRole.FleetAdmin, IsSystemWide = true }, + new LdapGroupRoleMapping { LdapGroup = "g2", Role = AdminRole.ConfigEditor, IsSystemWide = false, ClusterId = "SITE-A" }, + }; + var result = RoleMapper.Merge(["ConfigViewer"], rows); + result.ShouldContain("ConfigViewer"); + result.ShouldContain("FleetAdmin"); + result.ShouldNotContain("ConfigEditor"); // cluster-scoped row ignored (global-only) + } + + [Fact] + public void Merge_with_no_db_rows_returns_baseline() + => RoleMapper.Merge(["FleetAdmin"], []).ShouldBe(["FleetAdmin"]); }