/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Opc.Ua.Server.UserManagement { /// /// Binds the standard ServerConfiguration.UserManagement object /// (NodeId i=24290) to an implementation /// using the source-generated typed proxies (, /// , , /// , ). /// /// /// /// Every typed method state's OnCallAsync delegate is wired through /// for the admin methods /// (AddUser, ModifyUser, RemoveUser) and /// for /// ChangePassword per Part 18 §5.2. /// /// /// On , the binding closes /// every active session for that user via the supplied /// to satisfy Part 18 §5.2.6 / §5.2.7. /// /// public sealed class UserManagementBinding : IDisposable { private readonly IUserManagement m_userManagement; private readonly ISessionManager? m_sessionManager; private readonly ILogger m_logger; private UserManagementState? m_state; private bool m_disposed; private UserManagementBinding( IUserManagement userManagement, ISessionManager? sessionManager, ITelemetryContext? telemetry) { m_userManagement = userManagement; m_sessionManager = sessionManager; m_logger = telemetry?.CreateLogger() ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; } /// /// Resolves the typed in the /// 's predefined nodes, wires every /// typed OnCallAsync delegate to , /// keeps the Users property in sync, and closes sessions on /// . Returns the binding /// instance so the caller can dispose it on server shutdown, or /// null if the standard UserManagement object is not /// present in the address space. /// /// /// is null. /// /// /// is null. /// public static UserManagementBinding? Bind( AsyncCustomNodeManager nodeManager, IUserManagement userManagement, ISessionManager? sessionManager) { if (nodeManager == null) { throw new ArgumentNullException(nameof(nodeManager)); } if (userManagement == null) { throw new ArgumentNullException(nameof(userManagement)); } UserManagementState? state = nodeManager.FindPredefinedNode( new NodeId(Objects.UserManagement)); if (state == null) { return null; } var binding = new UserManagementBinding( userManagement, sessionManager, nodeManager.Server?.Telemetry); binding.Initialize(state); return binding; } /// public void Dispose() { if (m_disposed) { return; } m_disposed = true; m_userManagement.UserDeactivated -= OnUserDeactivated; } private void Initialize(UserManagementState state) { m_state = state; if (state.AddUser != null) { state.AddUser.OnCall = null; state.AddUser.OnCallAsync = OnAddUserAsync; } if (state.ModifyUser != null) { state.ModifyUser.OnCall = null; state.ModifyUser.OnCallAsync = OnModifyUserAsync; } if (state.RemoveUser != null) { state.RemoveUser.OnCall = null; state.RemoveUser.OnCallAsync = OnRemoveUserAsync; } if (state.ChangePassword != null) { state.ChangePassword.OnCall = null; state.ChangePassword.OnCallAsync = OnChangePasswordAsync; } SyncProperties(); m_userManagement.UserDeactivated += OnUserDeactivated; } private void SyncProperties() { if (m_state == null) { return; } m_state.Users?.Value = ArrayOf.Wrapped( System.Linq.Enumerable.ToArray(m_userManagement.SnapshotUsers())); m_state.PasswordLength?.Value = m_userManagement.PasswordLength; m_state.PasswordOptions?.Value = (uint)m_userManagement.PasswordOptions; m_state.PasswordRestrictions?.Value = m_userManagement.PasswordRestrictions ?? LocalizedText.Null; } private async ValueTask OnAddUserAsync( ISystemContext context, MethodState method, NodeId objectId, string userName, string password, uint userConfiguration, string description, CancellationToken cancellationToken) { await Task.Yield(); var result = new AddUserMethodStateResult(); ServiceResult auth = RoleAuthorizationGate.CheckAdmin(context); if (ServiceResult.IsBad(auth)) { result.ServiceResult = auth; return result; } result.ServiceResult = m_userManagement.AddUser( userName, password, (UserConfigurationMask)userConfiguration, description); if (ServiceResult.IsGood(result.ServiceResult)) { SyncProperties(); } return result; } private async ValueTask OnModifyUserAsync( ISystemContext context, MethodState method, NodeId objectId, string userName, bool modifyPassword, string password, bool modifyUserConfiguration, uint userConfiguration, bool modifyDescription, string description, CancellationToken cancellationToken) { await Task.Yield(); var result = new ModifyUserMethodStateResult(); ServiceResult auth = RoleAuthorizationGate.CheckAdmin(context); if (ServiceResult.IsBad(auth)) { result.ServiceResult = auth; return result; } result.ServiceResult = m_userManagement.ModifyUser( userName, modifyPassword, password, modifyUserConfiguration, (UserConfigurationMask)userConfiguration, modifyDescription, description, RoleAuthorizationGate.GetUserIdentity(context)?.DisplayName); if (ServiceResult.IsGood(result.ServiceResult)) { SyncProperties(); } return result; } private async ValueTask OnRemoveUserAsync( ISystemContext context, MethodState method, NodeId objectId, string userName, CancellationToken cancellationToken) { await Task.Yield(); var result = new RemoveUserMethodStateResult(); ServiceResult auth = RoleAuthorizationGate.CheckAdmin(context); if (ServiceResult.IsBad(auth)) { result.ServiceResult = auth; return result; } result.ServiceResult = m_userManagement.RemoveUser( userName, RoleAuthorizationGate.GetUserIdentity(context)?.DisplayName); if (ServiceResult.IsGood(result.ServiceResult)) { SyncProperties(); } return result; } private async ValueTask OnChangePasswordAsync( ISystemContext context, MethodState method, NodeId objectId, string oldPassword, string newPassword, CancellationToken cancellationToken) { await Task.Yield(); var result = new ChangePasswordMethodStateResult(); IUserIdentity? identity = RoleAuthorizationGate.GetUserIdentity(context); if (identity == null || string.IsNullOrEmpty(identity.DisplayName)) { result.ServiceResult = new ServiceResult(StatusCodes.BadInvalidState, new LocalizedText("ChangePassword requires a USERNAME identity token.")); return result; } ServiceResult auth = RoleAuthorizationGate.CheckSelfUserName(context, identity.DisplayName); if (ServiceResult.IsBad(auth)) { result.ServiceResult = auth; return result; } result.ServiceResult = m_userManagement.ChangePassword( identity.DisplayName, oldPassword, newPassword); return result; } private void OnUserDeactivated(object? sender, UserDeactivatedEventArgs e) { if (m_disposed || m_sessionManager == null || string.IsNullOrEmpty(e.UserName)) { return; } try { foreach (ISession session in m_sessionManager.GetSessions()) { if (string.Equals(session.Identity?.DisplayName, e.UserName, StringComparison.Ordinal)) { try { session.Dispose(); } catch (Exception ex) { m_logger.LogWarning(ex, "Failed to close session {SessionId} for deactivated user {UserName}.", session.Id, e.UserName); } } } } catch (Exception ex) { m_logger.LogError(ex, "Error closing sessions for deactivated user {UserName}.", e.UserName); } } } }