Consolidate LDAP roles into OPC UA session roles with granular write permissions
Map LDAP groups to custom OPC UA role NodeIds on RoleBasedIdentity.GrantedRoleIds during authentication, replacing the username-to-role side cache. Split ReadWrite into WriteOperate/WriteTune/WriteConfigure so write access is gated per Galaxy security classification. AnonymousCanWrite now behaves consistently regardless of LDAP state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -112,8 +112,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
{
|
||||
Log.Information("Authentication.Ldap.Enabled=true, Host={Host}, Port={Port}, BaseDN={BaseDN}",
|
||||
config.Authentication.Ldap.Host, config.Authentication.Ldap.Port, config.Authentication.Ldap.BaseDN);
|
||||
Log.Information("Authentication.Ldap groups: ReadOnly={ReadOnly}, ReadWrite={ReadWrite}, AlarmAck={AlarmAck}",
|
||||
config.Authentication.Ldap.ReadOnlyGroup, config.Authentication.Ldap.ReadWriteGroup, config.Authentication.Ldap.AlarmAckGroup);
|
||||
Log.Information("Authentication.Ldap groups: ReadOnly={ReadOnly}, WriteOperate={WriteOperate}, WriteTune={WriteTune}, WriteConfigure={WriteConfigure}, AlarmAck={AlarmAck}",
|
||||
config.Authentication.Ldap.ReadOnlyGroup, config.Authentication.Ldap.WriteOperateGroup,
|
||||
config.Authentication.Ldap.WriteTuneGroup, config.Authentication.Ldap.WriteConfigureGroup,
|
||||
config.Authentication.Ldap.AlarmAckGroup);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.Authentication.Ldap.ServiceAccountDn))
|
||||
{
|
||||
|
||||
@@ -55,9 +55,19 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
||||
public string ReadOnlyGroup { get; set; } = "ReadOnly";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LDAP group name that grants read-write access.
|
||||
/// Gets or sets the LDAP group name that grants write access for FreeAccess/Operate attributes.
|
||||
/// </summary>
|
||||
public string ReadWriteGroup { get; set; } = "ReadWrite";
|
||||
public string WriteOperateGroup { get; set; } = "WriteOperate";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LDAP group name that grants write access for Tune attributes.
|
||||
/// </summary>
|
||||
public string WriteTuneGroup { get; set; } = "WriteTune";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LDAP group name that grants write access for Configure attributes.
|
||||
/// </summary>
|
||||
public string WriteConfigureGroup { get; set; } = "WriteConfigure";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LDAP group name that grants alarm acknowledgment access.
|
||||
|
||||
@@ -32,7 +32,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
public static class AppRoles
|
||||
{
|
||||
public const string ReadOnly = "ReadOnly";
|
||||
public const string ReadWrite = "ReadWrite";
|
||||
public const string WriteOperate = "WriteOperate";
|
||||
public const string WriteTune = "WriteTune";
|
||||
public const string WriteConfigure = "WriteConfigure";
|
||||
public const string AlarmAck = "AlarmAck";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
_groupToRole = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ config.ReadOnlyGroup, AppRoles.ReadOnly },
|
||||
{ config.ReadWriteGroup, AppRoles.ReadWrite },
|
||||
{ config.WriteOperateGroup, AppRoles.WriteOperate },
|
||||
{ config.WriteTuneGroup, AppRoles.WriteTune },
|
||||
{ config.WriteConfigureGroup, AppRoles.WriteConfigure },
|
||||
{ config.AlarmAckGroup, AppRoles.AlarmAck }
|
||||
};
|
||||
}
|
||||
|
||||
18
src/ZB.MOM.WW.LmxOpcUa.Host/Domain/LmxRoleIds.cs
Normal file
18
src/ZB.MOM.WW.LmxOpcUa.Host/Domain/LmxRoleIds.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Host.Domain
|
||||
{
|
||||
/// <summary>
|
||||
/// Stable identifiers for custom OPC UA roles mapped from LDAP groups.
|
||||
/// The namespace URI is registered in the server namespace table at startup,
|
||||
/// and the string identifiers are resolved to runtime NodeIds before use.
|
||||
/// </summary>
|
||||
public static class LmxRoleIds
|
||||
{
|
||||
public const string NamespaceUri = "urn:zbmom:lmxopcua:roles";
|
||||
|
||||
public const string ReadOnly = "Role.ReadOnly";
|
||||
public const string WriteOperate = "Role.WriteOperate";
|
||||
public const string WriteTune = "Role.WriteTune";
|
||||
public const string WriteConfigure = "Role.WriteConfigure";
|
||||
public const string AlarmAck = "Role.AlarmAck";
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
private readonly HistorianDataSource? _historianDataSource;
|
||||
private readonly bool _alarmTrackingEnabled;
|
||||
private readonly bool _anonymousCanWrite;
|
||||
private readonly Func<string?, IReadOnlyList<string>?>? _appRoleLookup;
|
||||
private readonly NodeId? _writeOperateRoleId;
|
||||
private readonly NodeId? _writeTuneRoleId;
|
||||
private readonly NodeId? _writeConfigureRoleId;
|
||||
private readonly NodeId? _alarmAckRoleId;
|
||||
private readonly string _namespaceUri;
|
||||
|
||||
// NodeId → full_tag_reference for read/write resolution
|
||||
@@ -70,6 +73,12 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
/// Gets or sets the declared array length from Galaxy metadata when the attribute is modeled as an array.
|
||||
/// </summary>
|
||||
public int? ArrayDimension { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Galaxy security classification (0=FreeAccess, 1=Operate, 4=Tune, 5=Configure, etc.).
|
||||
/// Used at write time to determine which write role is required.
|
||||
/// </summary>
|
||||
public int SecurityClassification { get; set; }
|
||||
}
|
||||
|
||||
// Alarm tracking: maps InAlarm tag reference → alarm source info
|
||||
@@ -194,7 +203,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
HistorianDataSource? historianDataSource = null,
|
||||
bool alarmTrackingEnabled = false,
|
||||
bool anonymousCanWrite = true,
|
||||
Func<string?, IReadOnlyList<string>?>? appRoleLookup = null)
|
||||
NodeId? writeOperateRoleId = null,
|
||||
NodeId? writeTuneRoleId = null,
|
||||
NodeId? writeConfigureRoleId = null,
|
||||
NodeId? alarmAckRoleId = null)
|
||||
: base(server, configuration, namespaceUri)
|
||||
{
|
||||
_namespaceUri = namespaceUri;
|
||||
@@ -203,7 +215,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
_historianDataSource = historianDataSource;
|
||||
_alarmTrackingEnabled = alarmTrackingEnabled;
|
||||
_anonymousCanWrite = anonymousCanWrite;
|
||||
_appRoleLookup = appRoleLookup;
|
||||
_writeOperateRoleId = writeOperateRoleId;
|
||||
_writeTuneRoleId = writeTuneRoleId;
|
||||
_writeConfigureRoleId = writeConfigureRoleId;
|
||||
_alarmAckRoleId = alarmAckRoleId;
|
||||
|
||||
// Wire up data change delivery
|
||||
_mxAccessClient.OnTagValueChanged += OnMxAccessDataChange;
|
||||
@@ -955,7 +970,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
{
|
||||
MxDataType = attr.MxDataType,
|
||||
IsArray = attr.IsArray,
|
||||
ArrayDimension = attr.ArrayDimension
|
||||
ArrayDimension = attr.ArrayDimension,
|
||||
SecurityClassification = attr.SecurityClassification
|
||||
};
|
||||
|
||||
// Track gobject → tag references for incremental sync
|
||||
@@ -1085,8 +1101,6 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
{
|
||||
base.Write(context, nodesToWrite, errors);
|
||||
|
||||
var canWrite = HasWritePermission(context);
|
||||
|
||||
for (int i = 0; i < nodesToWrite.Count; i++)
|
||||
{
|
||||
if (nodesToWrite[i].AttributeId != Attributes.Value)
|
||||
@@ -1096,19 +1110,23 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
if (errors[i] != null && errors[i].StatusCode == StatusCodes.BadNotWritable)
|
||||
continue;
|
||||
|
||||
if (!canWrite)
|
||||
{
|
||||
errors[i] = new ServiceResult(StatusCodes.BadUserAccessDenied);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nodeId = nodesToWrite[i].NodeId;
|
||||
if (nodeId.NamespaceIndex != NamespaceIndex) continue;
|
||||
|
||||
var nodeIdStr = nodeId.Identifier as string;
|
||||
if (nodeIdStr == null) continue;
|
||||
|
||||
if (_nodeIdToTagReference.TryGetValue(nodeIdStr, out var tagRef))
|
||||
if (!_nodeIdToTagReference.TryGetValue(nodeIdStr, out var tagRef))
|
||||
continue;
|
||||
|
||||
// Check write permission based on the node's security classification
|
||||
var secClass = _tagMetadata.TryGetValue(tagRef, out var meta) ? meta.SecurityClassification : 1;
|
||||
if (!HasWritePermission(context, secClass))
|
||||
{
|
||||
errors[i] = new ServiceResult(StatusCodes.BadUserAccessDenied);
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -1146,35 +1164,55 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasWritePermission(OperationContext context)
|
||||
private bool HasWritePermission(OperationContext context, int securityClassification)
|
||||
{
|
||||
if (_appRoleLookup != null)
|
||||
return HasRole(context.UserIdentity, Domain.AppRoles.ReadWrite);
|
||||
var identity = context.UserIdentity;
|
||||
|
||||
// Legacy behavior: reject anonymous writes when AnonymousCanWrite is false
|
||||
if (!_anonymousCanWrite && context.UserIdentity?.GrantedRoleIds != null &&
|
||||
!context.UserIdentity.GrantedRoleIds.Contains(ObjectIds.WellKnownRole_AuthenticatedUser))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Check anonymous sessions against AnonymousCanWrite
|
||||
if (identity?.GrantedRoleIds?.Contains(ObjectIds.WellKnownRole_Anonymous) == true)
|
||||
return _anonymousCanWrite;
|
||||
|
||||
// When role-based auth is active, require the role matching the security classification
|
||||
var requiredRoleId = GetRequiredWriteRole(securityClassification);
|
||||
if (requiredRoleId != null)
|
||||
return HasGrantedRole(identity, requiredRoleId);
|
||||
|
||||
// No role-based auth — authenticated users can write
|
||||
return true;
|
||||
}
|
||||
|
||||
private NodeId? GetRequiredWriteRole(int securityClassification)
|
||||
{
|
||||
switch (securityClassification)
|
||||
{
|
||||
case 0: // FreeAccess
|
||||
case 1: // Operate
|
||||
return _writeOperateRoleId;
|
||||
case 4: // Tune
|
||||
return _writeTuneRoleId;
|
||||
case 5: // Configure
|
||||
return _writeConfigureRoleId;
|
||||
default:
|
||||
// SecuredWrite (2), VerifiedWrite (3), ViewOnly (6) are read-only by AccessLevel
|
||||
// but if somehow reached, require the most restrictive role
|
||||
return _writeConfigureRoleId;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasAlarmAckPermission(ISystemContext context)
|
||||
{
|
||||
if (_appRoleLookup == null)
|
||||
if (_alarmAckRoleId == null)
|
||||
return true;
|
||||
|
||||
var identity = (context as SystemContext)?.UserIdentity;
|
||||
return HasRole(identity, Domain.AppRoles.AlarmAck);
|
||||
return HasGrantedRole(identity, _alarmAckRoleId);
|
||||
}
|
||||
|
||||
private bool HasRole(IUserIdentity? identity, string requiredRole)
|
||||
private static bool HasGrantedRole(IUserIdentity? identity, NodeId? roleId)
|
||||
{
|
||||
var username = identity?.GetIdentityToken() is UserNameIdentityToken token ? token.UserName : null;
|
||||
var roles = _appRoleLookup!(username);
|
||||
return roles != null && roles.Contains(requiredRole);
|
||||
return roleId != null &&
|
||||
identity?.GrantedRoleIds != null &&
|
||||
identity.GrantedRoleIds.Contains(roleId);
|
||||
}
|
||||
|
||||
private static void EnableEventNotifierUpChain(NodeState node)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Opc.Ua;
|
||||
@@ -30,7 +29,14 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
private readonly RedundancyConfiguration _redundancyConfig;
|
||||
private readonly string? _applicationUri;
|
||||
private readonly ServiceLevelCalculator _serviceLevelCalculator = new ServiceLevelCalculator();
|
||||
private readonly ConcurrentDictionary<string, IReadOnlyList<string>> _userAppRoles = new ConcurrentDictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Resolved custom role NodeIds (populated in CreateMasterNodeManager)
|
||||
private NodeId? _readOnlyRoleId;
|
||||
private NodeId? _writeOperateRoleId;
|
||||
private NodeId? _writeTuneRoleId;
|
||||
private NodeId? _writeConfigureRoleId;
|
||||
private NodeId? _alarmAckRoleId;
|
||||
|
||||
private LmxNodeManager? _nodeManager;
|
||||
|
||||
/// <summary>
|
||||
@@ -38,22 +44,6 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
/// </summary>
|
||||
public LmxNodeManager? NodeManager => _nodeManager;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the application-level roles cached for the given username, or null if no roles are stored.
|
||||
/// Called by LmxNodeManager to enforce per-role write and alarm-ack permissions.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? GetUserAppRoles(string? username)
|
||||
{
|
||||
if (username != null && _userAppRoles.TryGetValue(username, out var roles))
|
||||
return roles;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether LDAP role-based access control is active.
|
||||
/// </summary>
|
||||
public bool LdapRolesEnabled => _authProvider is Domain.IRoleProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of active OPC UA sessions currently connected to the server.
|
||||
/// </summary>
|
||||
@@ -85,15 +75,29 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
/// <inheritdoc />
|
||||
protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration)
|
||||
{
|
||||
// Resolve custom role NodeIds from the roles namespace
|
||||
ResolveRoleNodeIds(server);
|
||||
|
||||
var namespaceUri = $"urn:{_galaxyName}:LmxOpcUa";
|
||||
_nodeManager = new LmxNodeManager(server, configuration, namespaceUri, _mxAccessClient, _metrics,
|
||||
_historianDataSource, _alarmTrackingEnabled, _authConfig.AnonymousCanWrite,
|
||||
LdapRolesEnabled ? (Func<string?, IReadOnlyList<string>?>)GetUserAppRoles : null);
|
||||
_writeOperateRoleId, _writeTuneRoleId, _writeConfigureRoleId, _alarmAckRoleId);
|
||||
|
||||
var nodeManagers = new List<INodeManager> { _nodeManager };
|
||||
return new MasterNodeManager(server, configuration, null, nodeManagers.ToArray());
|
||||
}
|
||||
|
||||
private void ResolveRoleNodeIds(IServerInternal server)
|
||||
{
|
||||
var nsIndex = server.NamespaceUris.GetIndexOrAppend(LmxRoleIds.NamespaceUri);
|
||||
_readOnlyRoleId = new NodeId(LmxRoleIds.ReadOnly, nsIndex);
|
||||
_writeOperateRoleId = new NodeId(LmxRoleIds.WriteOperate, nsIndex);
|
||||
_writeTuneRoleId = new NodeId(LmxRoleIds.WriteTune, nsIndex);
|
||||
_writeConfigureRoleId = new NodeId(LmxRoleIds.WriteConfigure, nsIndex);
|
||||
_alarmAckRoleId = new NodeId(LmxRoleIds.AlarmAck, nsIndex);
|
||||
Log.Debug("Resolved custom role NodeIds in namespace index {NsIndex}", nsIndex);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnServerStarted(IServerInternal server)
|
||||
{
|
||||
@@ -155,8 +159,6 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
/// Updates the server's ServiceLevel based on current runtime health.
|
||||
/// Called by the service layer when MXAccess or DB health changes.
|
||||
/// </summary>
|
||||
/// <param name="mxAccessConnected">Whether the MXAccess connection is healthy.</param>
|
||||
/// <param name="dbConnected">Whether the Galaxy repository database is reachable.</param>
|
||||
public void UpdateServiceLevel(bool mxAccessConnected, bool dbConnected)
|
||||
{
|
||||
var level = CalculateCurrentServiceLevel(mxAccessConnected, dbConnected);
|
||||
@@ -206,11 +208,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
if (!_authConfig.AllowAnonymous)
|
||||
throw new ServiceResultException(StatusCodes.BadIdentityTokenRejected, "Anonymous access is disabled");
|
||||
|
||||
var roles = new List<Role> { Role.Anonymous };
|
||||
if (_authConfig.AnonymousCanWrite)
|
||||
roles.Add(Role.AuthenticatedUser);
|
||||
|
||||
args.Identity = new RoleBasedIdentity(new UserIdentity(anonymousToken), roles);
|
||||
args.Identity = new RoleBasedIdentity(
|
||||
new UserIdentity(anonymousToken),
|
||||
new List<Role> { Role.Anonymous });
|
||||
Log.Debug("Anonymous session accepted (canWrite={CanWrite})", _authConfig.AnonymousCanWrite);
|
||||
return;
|
||||
}
|
||||
@@ -227,11 +227,32 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
|
||||
var roles = new List<Role> { Role.AuthenticatedUser };
|
||||
|
||||
// Resolve LDAP-based roles when the provider supports it
|
||||
if (_authProvider is Domain.IRoleProvider roleProvider)
|
||||
if (_authProvider is IRoleProvider roleProvider)
|
||||
{
|
||||
var appRoles = roleProvider.GetUserRoles(userNameToken.UserName);
|
||||
_userAppRoles[userNameToken.UserName] = appRoles;
|
||||
|
||||
foreach (var appRole in appRoles)
|
||||
{
|
||||
switch (appRole)
|
||||
{
|
||||
case AppRoles.ReadOnly:
|
||||
if (_readOnlyRoleId != null) roles.Add(new Role(_readOnlyRoleId, AppRoles.ReadOnly));
|
||||
break;
|
||||
case AppRoles.WriteOperate:
|
||||
if (_writeOperateRoleId != null) roles.Add(new Role(_writeOperateRoleId, AppRoles.WriteOperate));
|
||||
break;
|
||||
case AppRoles.WriteTune:
|
||||
if (_writeTuneRoleId != null) roles.Add(new Role(_writeTuneRoleId, AppRoles.WriteTune));
|
||||
break;
|
||||
case AppRoles.WriteConfigure:
|
||||
if (_writeConfigureRoleId != null) roles.Add(new Role(_writeConfigureRoleId, AppRoles.WriteConfigure));
|
||||
break;
|
||||
case AppRoles.AlarmAck:
|
||||
if (_alarmAckRoleId != null) roles.Add(new Role(_alarmAckRoleId, AppRoles.AlarmAck));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Information("User {Username} authenticated with roles [{Roles}]",
|
||||
userNameToken.UserName, string.Join(", ", appRoles));
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
var policies = new UserTokenPolicyCollection();
|
||||
if (_authConfig.AllowAnonymous)
|
||||
policies.Add(new UserTokenPolicy(UserTokenType.Anonymous));
|
||||
if (_authConfig.Ldap.Enabled)
|
||||
if (_authConfig.Ldap.Enabled || _authProvider != null)
|
||||
policies.Add(new UserTokenPolicy(UserTokenType.UserName));
|
||||
|
||||
if (policies.Count == 0)
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
private readonly IGalaxyRepository? _galaxyRepository;
|
||||
private readonly IMxAccessClient? _mxAccessClientOverride;
|
||||
private readonly bool _hasMxAccessClientOverride;
|
||||
private readonly IUserAuthenticationProvider? _authProviderOverride;
|
||||
private readonly bool _hasAuthProviderOverride;
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
private PerformanceMetrics? _metrics;
|
||||
@@ -74,13 +76,16 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
/// <param name="mxAccessClientOverride">An optional direct MXAccess client substitute that bypasses STA thread setup and COM interop.</param>
|
||||
/// <param name="hasMxAccessClientOverride">A value indicating whether the override client should be used instead of creating a client from <paramref name="mxProxy"/>.</param>
|
||||
internal OpcUaService(AppConfiguration config, IMxProxy? mxProxy, IGalaxyRepository? galaxyRepository,
|
||||
IMxAccessClient? mxAccessClientOverride = null, bool hasMxAccessClientOverride = false)
|
||||
IMxAccessClient? mxAccessClientOverride = null, bool hasMxAccessClientOverride = false,
|
||||
IUserAuthenticationProvider? authProviderOverride = null, bool hasAuthProviderOverride = false)
|
||||
{
|
||||
_config = config;
|
||||
_mxProxy = mxProxy;
|
||||
_galaxyRepository = galaxyRepository;
|
||||
_mxAccessClientOverride = mxAccessClientOverride;
|
||||
_hasMxAccessClientOverride = hasMxAccessClientOverride;
|
||||
_authProviderOverride = authProviderOverride;
|
||||
_hasAuthProviderOverride = hasAuthProviderOverride;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -162,7 +167,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
? new Historian.HistorianDataSource(_config.Historian)
|
||||
: null;
|
||||
Domain.IUserAuthenticationProvider? authProvider = null;
|
||||
if (_config.Authentication.Ldap.Enabled)
|
||||
if (_hasAuthProviderOverride)
|
||||
{
|
||||
authProvider = _authProviderOverride;
|
||||
}
|
||||
else if (_config.Authentication.Ldap.Enabled)
|
||||
{
|
||||
authProvider = new Domain.LdapAuthenticationProvider(_config.Authentication.Ldap);
|
||||
Log.Information("LDAP authentication enabled (server={Host}:{Port}, baseDN={BaseDN})",
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
private bool _mxProxySet;
|
||||
private bool _galaxyRepositorySet;
|
||||
private bool _mxAccessClientSet;
|
||||
private IUserAuthenticationProvider? _authProvider;
|
||||
private bool _authProviderSet;
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the default service configuration used by the test host.
|
||||
@@ -116,6 +118,25 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
/// Disables the embedded dashboard so tests can focus on the runtime bridge without binding the HTTP listener.
|
||||
/// </summary>
|
||||
/// <returns>The current builder so additional overrides can be chained.</returns>
|
||||
/// <summary>
|
||||
/// Injects a custom authentication provider for tests that need deterministic role resolution.
|
||||
/// </summary>
|
||||
public OpcUaServiceBuilder WithAuthProvider(IUserAuthenticationProvider? provider)
|
||||
{
|
||||
_authProvider = provider;
|
||||
_authProviderSet = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the authentication configuration for the test host.
|
||||
/// </summary>
|
||||
public OpcUaServiceBuilder WithAuthentication(AuthenticationConfiguration authConfig)
|
||||
{
|
||||
_config.Authentication = authConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OpcUaServiceBuilder DisableDashboard()
|
||||
{
|
||||
_config.Dashboard.Enabled = false;
|
||||
@@ -176,7 +197,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
|
||||
_mxProxySet ? _mxProxy : null,
|
||||
_galaxyRepositorySet ? _galaxyRepository : null,
|
||||
_mxAccessClientSet ? _mxAccessClient : null,
|
||||
_mxAccessClientSet);
|
||||
_mxAccessClientSet,
|
||||
_authProviderSet ? _authProvider : null,
|
||||
_authProviderSet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -46,7 +46,9 @@
|
||||
"ServiceAccountPassword": "serviceaccount123",
|
||||
"TimeoutSeconds": 5,
|
||||
"ReadOnlyGroup": "ReadOnly",
|
||||
"ReadWriteGroup": "ReadWrite",
|
||||
"WriteOperateGroup": "WriteOperate",
|
||||
"WriteTuneGroup": "WriteTune",
|
||||
"WriteConfigureGroup": "WriteConfigure",
|
||||
"AlarmAckGroup": "AlarmAck"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user