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..];
}
}