fix(mesh-phase4): driver-only nodes map LDAP groups from appsettings (no ConfigDb grants)

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
Joseph Doherty
2026-07-23 11:44:44 -04:00
parent a41582ea6a
commit 833e45db9c
3 changed files with 121 additions and 2 deletions
+10 -2
View File
@@ -13,6 +13,7 @@ using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
using ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis;
using ZB.MOM.WW.OtOpcUa.Cluster;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Services;
using ZB.MOM.WW.OtOpcUa.ControlPlane;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Redundancy;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
@@ -319,11 +320,18 @@ if (hasDriver)
// OtOpcUaLdapAuthService is the app ILdapAuthService (Enabled switch + DevStubMode over the
// shared ZB.MOM.WW.Auth.Ldap service). The data-plane authenticator resolves IGroupRoleMapper
// <string> per call to turn the directory's groups into roles, so register it here for driver-
// only nodes (AddOtOpcUaAuth registers it on admin nodes); ILdapGroupRoleMappingService it
// depends on is already registered unconditionally by AddOtOpcUaConfigDb above.
// only nodes (AddOtOpcUaAuth registers it on admin nodes). OtOpcUaGroupRoleMapper also depends
// on ILdapGroupRoleMappingService (the control-plane DB-grants CRUD surface) — Phase 4 gated
// AddOtOpcUaConfigDb on hasAdmin, so a driver-only node has no ConfigDb to read it from. Bind
// NullLdapGroupRoleMappingService (always "no DB grants") here via TryAdd so the mapper falls
// back to the appsettings Security:Ldap:GroupToRole baseline; on a fused admin+driver node the
// hasAdmin block above already ran AddOtOpcUaConfigDb's non-Try AddScoped<ILdapGroupRoleMapping
// Service, LdapGroupRoleMappingService>, so this TryAdd is a no-op there and the real EF service
// wins.
// Note: AddValidatedOptions<LdapOptions, LdapOptionsValidator> is now registered unconditionally
// above both role blocks so admin-only nodes also get fail-fast LDAP startup validation.
builder.Services.TryAddSingleton<ILdapAuthService, OtOpcUaLdapAuthService>();
builder.Services.TryAddScoped<ILdapGroupRoleMappingService, NullLdapGroupRoleMappingService>();
builder.Services.TryAddScoped<IGroupRoleMapper<string>, OtOpcUaGroupRoleMapper>();
builder.Services.AddSingleton<IOpcUaUserAuthenticator, LdapOpcUaUserAuthenticator>();
@@ -0,0 +1,44 @@
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Services;
namespace ZB.MOM.WW.OtOpcUa.Security.Ldap;
/// <summary>
/// Driver-only-node stand-in for the EF-backed <see cref="ILdapGroupRoleMappingService"/>.
/// A driver-only node (Akka role <c>driver</c>, not <c>admin</c>) has no ConfigDb connection
/// (Phase 4 gated <c>AddOtOpcUaConfigDb</c> on <c>hasAdmin</c>), so it can never read the
/// control-plane <c>LdapGroupRoleMapping</c> DB grants that <see cref="OtOpcUaGroupRoleMapper"/>
/// normally unions into its result. This implementation always reports "no DB grants" so the
/// data-path mapper falls back to exactly the appsettings <c>Security:Ldap:GroupToRole</c>
/// baseline — the same safe fallback the mapper already takes when the real service simply
/// has no matching rows. <see cref="LdapGroupRoleMapping"/> rows are not composed into the
/// deployment artifact either, so a driver node never had DB grants via config; reporting
/// empty here is honest, not lossy.
/// </summary>
/// <remarks>
/// The write surface (<see cref="CreateAsync"/>/<see cref="DeleteAsync"/>) is control-plane-only
/// Admin UI functionality that a driver-only node — which has no Admin UI — never calls. It
/// throws rather than silently no-opping so an accidental call surfaces immediately instead of
/// pretending to persist a grant that a driver-only node cannot store.
/// </remarks>
public sealed class NullLdapGroupRoleMappingService : ILdapGroupRoleMappingService
{
/// <inheritdoc />
public Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync(
IEnumerable<string> ldapGroups, CancellationToken cancellationToken)
=> Task.FromResult<IReadOnlyList<LdapGroupRoleMapping>>(Array.Empty<LdapGroupRoleMapping>());
/// <inheritdoc />
public Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken)
=> Task.FromResult<IReadOnlyList<LdapGroupRoleMapping>>(Array.Empty<LdapGroupRoleMapping>());
/// <inheritdoc />
public Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken)
=> throw new NotSupportedException(
"LDAP group-role grants are control-plane only; a driver-only node has no ConfigDb.");
/// <inheritdoc />
public Task DeleteAsync(Guid id, CancellationToken cancellationToken)
=> throw new NotSupportedException(
"LDAP group-role grants are control-plane only; a driver-only node has no ConfigDb.");
}