namespace ZB.MOM.WW.MxGateway.Server.Dashboard; /// /// Single source of truth for mapping a user's LDAP groups to dashboard roles. /// Both (the existing login flow) and /// (the shared-Auth /// seam) /// delegate here so the precedence and case rules stay identical. /// internal static class DashboardGroupRoleMapping { /// /// Maps the user's LDAP groups to dashboard roles. A user can pick up /// multiple roles; Admin and Viewer are the only legal values. Returns /// an empty list when no group matches (caller rejects the login). /// /// The collection of LDAP groups the user belongs to. /// The mapping from group names to dashboard role names. /// The distinct dashboard roles the groups map to, or an empty list if none match. internal static IReadOnlyList MapGroupsToRoles( IEnumerable groups, IReadOnlyDictionary groupToRole) { if (groupToRole.Count == 0) { return []; } HashSet roles = new(StringComparer.Ordinal); foreach (string group in groups) { string normalizedGroup = group.Trim(); // Lookup precedence: the full literal group string is tried first; only if // that misses do we fall back to the leading RDN value (e.g. "GwAdmin" // extracted from "ou=GwAdmin,ou=groups,..."). The map's comparer is // OrdinalIgnoreCase (see DashboardOptions.GroupToRole), so "GwAdmin" and // "gwadmin" both match. // // With the shared ZB.MOM.WW.Auth.Ldap provider, groups arrive here already // stripped to short RDN names (the library calls FirstRdnValue before // returning them). So through the live login path the full-string branch // only ever sees short names and the RDN fallback is effectively a no-op — // they collapse to the same key. The fallback is retained because this // mapping is also reachable directly via the IGroupRoleMapper seam // (DashboardGroupRoleMapper), where a caller could still pass a full DN. // CONSEQUENCE: configuring a full-DN GroupToRole *key* (e.g. // "ou=GwAdmin,ou=groups,...") is UNSUPPORTED with the shared library — the // incoming group is a short name, so it will never equal a full-DN key. // Keep GroupToRole keys as short group names. if (groupToRole.TryGetValue(normalizedGroup, out string? mapped) || groupToRole.TryGetValue(ExtractFirstRdnValue(normalizedGroup), out mapped)) { roles.Add(mapped); } } return [.. roles]; } /// Extracts the first RDN value from a distinguished name. /// The LDAP distinguished name. /// The value of the first RDN component, or the input unchanged if it contains no '=' separator. internal static string ExtractFirstRdnValue(string distinguishedName) { int equalsIndex = distinguishedName.IndexOf('='); if (equalsIndex < 0) { return distinguishedName; } int valueStart = equalsIndex + 1; int commaIndex = distinguishedName.IndexOf(',', valueStart); return commaIndex > valueStart ? distinguishedName[valueStart..commaIndex] : distinguishedName[valueStart..]; } }