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. 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 (Server-040): 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 e.g. // "GwAdmin" and "gwadmin" both match. // // Review C1: 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. 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..]; } }