using System.Collections.Frozen; using ZB.MOM.WW.OtOpcUa.Core.Authorization; namespace ZB.MOM.WW.OtOpcUa.Server.Security; /// /// Maps a driver-side full reference (e.g. "TestMachine_001/Oven/SetPoint") to the /// the Phase 6.2 evaluator walks. Supports two modes: /// /// /// Cluster-only (pre-ADR-001) — when no path index is supplied the resolver /// returns a flat ClusterId + TagId scope. Sufficient while the /// Config-DB-driven Equipment walker isn't live; Cluster-level grants cascade to every /// tag below per decision #129, so finer per-Equipment grants are effectively /// cluster-wide at dispatch. /// /// /// Full-path (post-ADR-001 Task B) — when an index is supplied, the resolver /// joins the full reference against the index to produce a complete /// Cluster → Namespace → UnsArea → UnsLine → Equipment → Tag scope. Unblocks /// per-Equipment / per-UnsLine ACL grants at the dispatch layer. /// /// /// /// /// The index is pre-loaded by the Server bootstrap against the published generation; /// the resolver itself does no live DB access. Resolve is O(1) dictionary lookup on the /// hot path; the fallback for unknown fullReference strings produces the same cluster-only /// scope the pre-ADR-001 resolver returned — new tags picked up via driver discovery but /// not yet indexed (e.g. between a DiscoverAsync result and the next generation publish) /// stay addressable without a scope-resolver crash. /// /// Thread-safety: both constructor paths freeze inputs into immutable state. Callers /// may cache a single instance per DriverNodeManager without locks. Swap atomically on /// generation change via the server's publish pipeline. /// public sealed class NodeScopeResolver { private readonly string _clusterId; private readonly FrozenDictionary? _index; /// Cluster-only resolver — pre-ADR-001 behavior. Kept for Server processes that /// haven't wired the Config-DB snapshot flow yet. public NodeScopeResolver(string clusterId) { ArgumentException.ThrowIfNullOrWhiteSpace(clusterId); _clusterId = clusterId; _index = null; } /// /// Full-path resolver (ADR-001 Task B). maps each known /// driver-side full reference to its pre-resolved carrying /// every UNS level populated. Entries are typically produced by joining /// Tag → Equipment → UnsLine → UnsArea rows of the published generation against /// the driver's discovered full references (or against Tag.TagConfig directly /// when the walker is config-primary per ADR-001 Option A). /// public NodeScopeResolver(string clusterId, IReadOnlyDictionary pathIndex) { ArgumentException.ThrowIfNullOrWhiteSpace(clusterId); ArgumentNullException.ThrowIfNull(pathIndex); _clusterId = clusterId; _index = pathIndex.ToFrozenDictionary(StringComparer.Ordinal); } /// /// Resolve a node scope for the given driver-side . /// Returns the indexed full-path scope when available; falls back to cluster-only /// (TagId populated only) when the index is absent or the reference isn't indexed. /// The fallback is the same shape the pre-ADR-001 resolver produced, so the authz /// evaluator behaves identically for un-indexed references. /// public NodeScope Resolve(string fullReference) { ArgumentException.ThrowIfNullOrWhiteSpace(fullReference); if (_index is not null && _index.TryGetValue(fullReference, out var indexed)) return indexed; return new NodeScope { ClusterId = _clusterId, TagId = fullReference, Kind = NodeHierarchyKind.Equipment, }; } }