80 lines
3.5 KiB
C#
80 lines
3.5 KiB
C#
namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
|
|
|
|
/// <summary>
|
|
/// Single source of truth for mapping a user's LDAP groups to dashboard roles.
|
|
/// Both <see cref="DashboardAuthenticator"/> (the existing login flow) and
|
|
/// <see cref="DashboardGroupRoleMapper"/> (the shared-Auth
|
|
/// <see cref="ZB.MOM.WW.Auth.Abstractions.Roles.IGroupRoleMapper{TRole}"/> seam)
|
|
/// delegate here so the precedence and case rules stay identical.
|
|
/// </summary>
|
|
internal static class DashboardGroupRoleMapping
|
|
{
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
/// <param name="groups">The collection of LDAP groups the user belongs to.</param>
|
|
/// <param name="groupToRole">The mapping from group names to dashboard role names.</param>
|
|
internal static IReadOnlyList<string> MapGroupsToRoles(
|
|
IEnumerable<string> groups,
|
|
IReadOnlyDictionary<string, string> groupToRole)
|
|
{
|
|
if (groupToRole.Count == 0)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
HashSet<string> 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<string> 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];
|
|
}
|
|
|
|
/// <summary>Extracts the first RDN value from a distinguished name.</summary>
|
|
/// <param name="distinguishedName">The LDAP distinguished name.</param>
|
|
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..];
|
|
}
|
|
}
|