using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Authorization; /// /// Default implementation. Resolves the /// for the session's cluster (via /// ), walks it collecting matched grants, OR-s the /// permission flags, and maps against the operation-specific required permission. /// public sealed class TriePermissionEvaluator : IPermissionEvaluator { private readonly PermissionTrieCache _cache; private readonly TimeProvider _timeProvider; public TriePermissionEvaluator(PermissionTrieCache cache, TimeProvider? timeProvider = null) { ArgumentNullException.ThrowIfNull(cache); _cache = cache; _timeProvider = timeProvider ?? TimeProvider.System; } public AuthorizationDecision Authorize(UserAuthorizationState session, OpcUaOperation operation, NodeScope scope) { ArgumentNullException.ThrowIfNull(session); ArgumentNullException.ThrowIfNull(scope); // Decision #152 — beyond the staleness ceiling every call fails closed regardless of // cache warmth elsewhere in the process. if (session.IsStale(_timeProvider.GetUtcNow().UtcDateTime)) return AuthorizationDecision.NotGranted(); if (!string.Equals(session.ClusterId, scope.ClusterId, StringComparison.OrdinalIgnoreCase)) return AuthorizationDecision.NotGranted(); var trie = _cache.GetTrie(scope.ClusterId); if (trie is null) return AuthorizationDecision.NotGranted(); var matches = trie.CollectMatches(scope, session.LdapGroups); if (matches.Count == 0) return AuthorizationDecision.NotGranted(); var required = MapOperationToPermission(operation); var granted = NodePermissions.None; foreach (var m in matches) granted |= m.PermissionFlags; return (granted & required) == required ? AuthorizationDecision.Allowed(matches) : AuthorizationDecision.NotGranted(); } /// Maps each to the bit required to grant it. public static NodePermissions MapOperationToPermission(OpcUaOperation op) => op switch { OpcUaOperation.Browse => NodePermissions.Browse, OpcUaOperation.Read => NodePermissions.Read, OpcUaOperation.WriteOperate => NodePermissions.WriteOperate, OpcUaOperation.WriteTune => NodePermissions.WriteTune, OpcUaOperation.WriteConfigure => NodePermissions.WriteConfigure, OpcUaOperation.HistoryRead => NodePermissions.HistoryRead, OpcUaOperation.HistoryUpdate => NodePermissions.HistoryRead, // HistoryUpdate bit not yet in NodePermissions; TODO Stream C follow-up OpcUaOperation.CreateMonitoredItems => NodePermissions.Subscribe, OpcUaOperation.TransferSubscriptions=> NodePermissions.Subscribe, OpcUaOperation.Call => NodePermissions.MethodCall, OpcUaOperation.AlarmAcknowledge => NodePermissions.AlarmAcknowledge, OpcUaOperation.AlarmConfirm => NodePermissions.AlarmConfirm, OpcUaOperation.AlarmShelve => NodePermissions.AlarmShelve, _ => throw new ArgumentOutOfRangeException(nameof(op), op, $"No permission mapping defined for operation {op}."), }; }