using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; namespace ZB.MOM.WW.ScadaBridge.Security; public class RoleMapper { private readonly ISecurityRepository _securityRepository; /// Initializes the mapper with the security repository. /// Repository used to retrieve LDAP group-to-role mappings and scope rules. public RoleMapper(ISecurityRepository securityRepository) { _securityRepository = securityRepository ?? throw new ArgumentNullException(nameof(securityRepository)); } // virtual: a test seam so HTTP-pipeline tests (e.g. the #23 M8 audit // endpoints) can substitute the LDAP-group→role resolution. /// Maps a list of LDAP group names to ScadaBridge roles and computes site-scope permissions. /// LDAP group names from the authenticated user's directory entry. /// Cancellation token. /// A containing matched roles, permitted site IDs, and the system-wide flag. public virtual async Task MapGroupsToRolesAsync( IReadOnlyList ldapGroups, CancellationToken ct = default) { var allMappings = await _securityRepository.GetAllMappingsAsync(ct); var matchedRoles = new HashSet(StringComparer.OrdinalIgnoreCase); var permittedSiteIds = new HashSet(); var hasDeploymentRole = false; var hasScopedDeploymentMapping = false; var hasUnscopedDeploymentMapping = false; foreach (var mapping in allMappings) { // Match LDAP group names (case-insensitive) if (!ldapGroups.Any(g => g.Equals(mapping.LdapGroupName, StringComparison.OrdinalIgnoreCase))) continue; matchedRoles.Add(mapping.Role); if (mapping.Role.Equals(Roles.Deployer, StringComparison.OrdinalIgnoreCase)) { hasDeploymentRole = true; var scopeRules = await _securityRepository.GetScopeRulesForMappingAsync(mapping.Id, ct); if (scopeRules.Count > 0) { hasScopedDeploymentMapping = true; foreach (var rule in scopeRules) { permittedSiteIds.Add(rule.SiteId.ToString()); } } else { hasUnscopedDeploymentMapping = true; } } } // Union semantics (Security-016): a Deployment user is system-wide iff // *any* matched Deployment mapping has no scope rules. A user in both // SCADA-Deploy-All (unscoped) and SCADA-Deploy-SiteA (scoped to Site A) // gets the broader grant, not the narrower one — matching the design's // "roles are independent — there is no implied hierarchy" rule. var isSystemWide = hasUnscopedDeploymentMapping || (hasDeploymentRole && !hasScopedDeploymentMapping); // When system-wide, drop any accumulated scope ids — the empty // permitted set is the system-wide signal downstream consumers // (SiteScopeService, ManagementActor) already use. if (isSystemWide) { permittedSiteIds.Clear(); } return new RoleMappingResult( matchedRoles.ToList(), permittedSiteIds.ToList(), isSystemWide); } }