feat(auth): cut OtOpcUa over to ZB.MOM.WW.Auth.Ldap; preserve DevStubMode; route roles via IGroupRoleMapper (Task 1.2/1.4)
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.Auth.Abstractions.Roles;
|
||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
|
||||
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
|
||||
|
||||
@@ -8,15 +10,23 @@ namespace ZB.MOM.WW.OtOpcUa.Host.OpcUa;
|
||||
/// Production <see cref="IOpcUaUserAuthenticator"/> adapter that bridges OPC UA UserName
|
||||
/// tokens to the same <see cref="ILdapAuthService"/> the Admin UI cookie/JWT flows use, so a
|
||||
/// single LDAP source-of-truth governs both control-plane (Admin) and data-plane (OPC UA)
|
||||
/// session identities. Roles flow through unchanged — the data-plane ACL evaluator reads
|
||||
/// them off <c>OperationContext.UserIdentity</c> downstream.
|
||||
/// session identities. Roles are resolved through the shared
|
||||
/// <see cref="IGroupRoleMapper{TRole}"/> seam from the LDAP groups returned by the directory —
|
||||
/// the same seam the login endpoint uses — and the resolved set is attached to the OPC UA
|
||||
/// session identity for the downstream data-plane ACL evaluator.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This authenticator is registered as a singleton, but <see cref="IGroupRoleMapper{TRole}"/>
|
||||
/// (and its DbContext-backed mapping service) is scoped. A per-call DI scope is opened to
|
||||
/// resolve the mapper so the singleton never captures a scoped dependency.
|
||||
/// </remarks>
|
||||
public sealed class LdapOpcUaUserAuthenticator(
|
||||
ILdapAuthService ldap,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<LdapOpcUaUserAuthenticator> logger)
|
||||
: IOpcUaUserAuthenticator
|
||||
{
|
||||
/// <summary>Authenticates an OPC UA UserName token via LDAP.</summary>
|
||||
/// <summary>Authenticates an OPC UA UserName token via LDAP, resolving roles through the mapper.</summary>
|
||||
/// <param name="username">The username to authenticate.</param>
|
||||
/// <param name="password">The password to authenticate.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
@@ -29,7 +39,9 @@ public sealed class LdapOpcUaUserAuthenticator(
|
||||
{
|
||||
return OpcUaUserAuthResult.Deny(result.Error ?? "Invalid credentials");
|
||||
}
|
||||
return OpcUaUserAuthResult.Allow(result.DisplayName ?? username, result.Roles);
|
||||
|
||||
var roles = await ResolveRolesAsync(result.Groups, result.Roles, username, ct).ConfigureAwait(false);
|
||||
return OpcUaUserAuthResult.Allow(result.DisplayName ?? username, roles);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
@@ -37,4 +49,36 @@ public sealed class LdapOpcUaUserAuthenticator(
|
||||
return OpcUaUserAuthResult.Deny("Authentication backend error");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the user's roles from their LDAP groups via the scoped
|
||||
/// <see cref="IGroupRoleMapper{TRole}"/>, unioned with any pre-resolved roles (the DevStub
|
||||
/// FleetAdmin grant). A mapper fault (e.g. a DB outage) must not deny an otherwise-authenticated
|
||||
/// session: it falls back to the pre-resolved roles, matching the login endpoint's behaviour.
|
||||
/// </summary>
|
||||
/// <param name="groups">The LDAP groups returned by the directory.</param>
|
||||
/// <param name="preResolved">Pre-resolved roles (empty on the real path; FleetAdmin under DevStub).</param>
|
||||
/// <param name="username">The login name, for diagnostics.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
private async Task<IReadOnlyList<string>> ResolveRolesAsync(
|
||||
IReadOnlyList<string> groups, IReadOnlyList<string> preResolved, string username, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var mapper = scope.ServiceProvider.GetRequiredService<IGroupRoleMapper<string>>();
|
||||
var mapping = await mapper.MapAsync(groups, ct).ConfigureAwait(false);
|
||||
|
||||
var roles = new HashSet<string>(preResolved, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var role in mapping.Roles)
|
||||
roles.Add(role);
|
||||
return [.. roles];
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
logger.LogWarning(ex,
|
||||
"Role-map lookup failed for OPC UA user {User}; using pre-resolved baseline roles", username);
|
||||
return preResolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user