d3ac52758c
Audited every Server-0xx finding whose Resolution described a documentation-only or comment-only change, and spot-checked the doc sub-claims of otherwise test-backed resolutions. 20 entries annotated in place (append-only; no historical resolution text rewritten). Two corrections had not survived and are re-applied: Server-040: the MapGroupsToRoles lookup-precedence comment moved intact into DashboardGroupRoleMapping (792e3f9) and was then deleted wholesale byfca978d, a sweep meant only to strip (Server-NNN) tracking markers. That also removed a later, substantive paragraph recording that the shared ZB.MOM.WW.Auth.Ldap provider pre-strips groups to short RDN names, so a full-DN GroupToRole key is unsupported. Both paragraphs restored, minus the tracking IDs. Server-009: the WAL / busy_timeout note vanished when the Storage section of docs/Authentication.md was rewritten to delegate connection-factory detail to ZB.MOM.WW.Auth.ApiKeys. The behavior is still live in the library (confirmed against 0.2.1), so the fix is prose-only. Server-011/014/022/023 are annotated as moot rather than regressed: the IAlarmRpcDispatcher trio was deleted indc9c0c9and no stale 'not yet wired' / 'PR A.6/A.7' prose survives in Server source. Server-038's documented v1 ACL gap was later closed by IDashboardSessionAcl, so its remarks are current. Comment/doc-only; no logic changes.
80 lines
3.7 KiB
C#
80 lines
3.7 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>
|
|
/// <returns>The distinct dashboard roles the groups map to, or an empty list if none match.</returns>
|
|
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: 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<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>
|
|
/// <returns>The value of the first RDN component, or the input unchanged if it contains no '=' separator.</returns>
|
|
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..];
|
|
}
|
|
}
|