Add authentication and role-based write access control
Implements configurable user authentication (anonymous + username/password) with pluggable credential provider (IUserAuthenticationProvider). Anonymous writes can be disabled via AnonymousCanWrite setting while reads remain open. Adds -U/-P flags to all CLI commands for authenticated sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Server;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Historian;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
|
||||
@@ -8,15 +11,19 @@ using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom OPC UA server that creates the LmxNodeManager. (OPC-001, OPC-012)
|
||||
/// Custom OPC UA server that creates the LmxNodeManager and handles user authentication. (OPC-001, OPC-012)
|
||||
/// </summary>
|
||||
public class LmxOpcUaServer : StandardServer
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext<LmxOpcUaServer>();
|
||||
|
||||
private readonly string _galaxyName;
|
||||
private readonly IMxAccessClient _mxAccessClient;
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly HistorianDataSource? _historianDataSource;
|
||||
private readonly bool _alarmTrackingEnabled;
|
||||
private readonly AuthenticationConfiguration _authConfig;
|
||||
private readonly IUserAuthenticationProvider? _authProvider;
|
||||
private LmxNodeManager? _nodeManager;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,34 +43,73 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a custom OPC UA server for the specified Galaxy namespace.
|
||||
/// </summary>
|
||||
/// <param name="galaxyName">The Galaxy name used to construct the namespace URI and product URI.</param>
|
||||
/// <param name="mxAccessClient">The runtime client used by the node manager for live data access.</param>
|
||||
/// <param name="metrics">The metrics collector shared with the node manager.</param>
|
||||
/// <param name="historianDataSource">The optional historian adapter used when clients issue OPC UA history reads.</param>
|
||||
/// <param name="alarmTrackingEnabled">Enables alarm condition tracking for alarm-capable Galaxy attributes.</param>
|
||||
public LmxOpcUaServer(string galaxyName, IMxAccessClient mxAccessClient, PerformanceMetrics metrics,
|
||||
HistorianDataSource? historianDataSource = null, bool alarmTrackingEnabled = false)
|
||||
HistorianDataSource? historianDataSource = null, bool alarmTrackingEnabled = false,
|
||||
AuthenticationConfiguration? authConfig = null, IUserAuthenticationProvider? authProvider = null)
|
||||
{
|
||||
_galaxyName = galaxyName;
|
||||
_mxAccessClient = mxAccessClient;
|
||||
_metrics = metrics;
|
||||
_historianDataSource = historianDataSource;
|
||||
_alarmTrackingEnabled = alarmTrackingEnabled;
|
||||
_authConfig = authConfig ?? new AuthenticationConfiguration();
|
||||
_authProvider = authProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration)
|
||||
{
|
||||
var namespaceUri = $"urn:{_galaxyName}:LmxOpcUa";
|
||||
_nodeManager = new LmxNodeManager(server, configuration, namespaceUri, _mxAccessClient, _metrics, _historianDataSource, _alarmTrackingEnabled);
|
||||
_nodeManager = new LmxNodeManager(server, configuration, namespaceUri, _mxAccessClient, _metrics,
|
||||
_historianDataSource, _alarmTrackingEnabled, _authConfig.AnonymousCanWrite);
|
||||
|
||||
var nodeManagers = new List<INodeManager> { _nodeManager };
|
||||
return new MasterNodeManager(server, configuration, null, nodeManagers.ToArray());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnServerStarted(IServerInternal server)
|
||||
{
|
||||
base.OnServerStarted(server);
|
||||
server.SessionManager.ImpersonateUser += OnImpersonateUser;
|
||||
}
|
||||
|
||||
private void OnImpersonateUser(Session session, ImpersonateEventArgs args)
|
||||
{
|
||||
if (args.NewIdentity is AnonymousIdentityToken anonymousToken)
|
||||
{
|
||||
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);
|
||||
Log.Debug("Anonymous session accepted (canWrite={CanWrite})", _authConfig.AnonymousCanWrite);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.NewIdentity is UserNameIdentityToken userNameToken)
|
||||
{
|
||||
var password = userNameToken.DecryptedPassword ?? "";
|
||||
|
||||
if (_authProvider == null || !_authProvider.ValidateCredentials(userNameToken.UserName, password))
|
||||
{
|
||||
Log.Warning("Authentication failed for user {Username}", userNameToken.UserName);
|
||||
throw new ServiceResultException(StatusCodes.BadUserAccessDenied, "Invalid username or password");
|
||||
}
|
||||
|
||||
args.Identity = new RoleBasedIdentity(
|
||||
new UserIdentity(userNameToken),
|
||||
new List<Role> { Role.AuthenticatedUser });
|
||||
Log.Information("User {Username} authenticated", userNameToken.UserName);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ServiceResultException(StatusCodes.BadIdentityTokenRejected, "Unsupported token type");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ServerProperties LoadServerProperties()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user