89 lines
4.2 KiB
C#
89 lines
4.2 KiB
C#
using System.Collections.Frozen;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Server.Security;
|
|
|
|
/// <summary>
|
|
/// Maps a driver-side full reference (e.g. <c>"TestMachine_001/Oven/SetPoint"</c>) to the
|
|
/// <see cref="NodeScope"/> the Phase 6.2 evaluator walks. Supports two modes:
|
|
/// <list type="bullet">
|
|
/// <item>
|
|
/// <b>Cluster-only (pre-ADR-001)</b> — when no path index is supplied the resolver
|
|
/// returns a flat <c>ClusterId + TagId</c> 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.
|
|
/// </item>
|
|
/// <item>
|
|
/// <b>Full-path (post-ADR-001 Task B)</b> — when an index is supplied, the resolver
|
|
/// joins the full reference against the index to produce a complete
|
|
/// <c>Cluster → Namespace → UnsArea → UnsLine → Equipment → Tag</c> scope. Unblocks
|
|
/// per-Equipment / per-UnsLine ACL grants at the dispatch layer.
|
|
/// </item>
|
|
/// </list>
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>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.</para>
|
|
///
|
|
/// <para>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.</para>
|
|
/// </remarks>
|
|
public sealed class NodeScopeResolver
|
|
{
|
|
private readonly string _clusterId;
|
|
private readonly FrozenDictionary<string, NodeScope>? _index;
|
|
|
|
/// <summary>Cluster-only resolver — pre-ADR-001 behavior. Kept for Server processes that
|
|
/// haven't wired the Config-DB snapshot flow yet.</summary>
|
|
public NodeScopeResolver(string clusterId)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
|
|
_clusterId = clusterId;
|
|
_index = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Full-path resolver (ADR-001 Task B). <paramref name="pathIndex"/> maps each known
|
|
/// driver-side full reference to its pre-resolved <see cref="NodeScope"/> carrying
|
|
/// every UNS level populated. Entries are typically produced by joining
|
|
/// <c>Tag → Equipment → UnsLine → UnsArea</c> rows of the published generation against
|
|
/// the driver's discovered full references (or against <c>Tag.TagConfig</c> directly
|
|
/// when the walker is config-primary per ADR-001 Option A).
|
|
/// </summary>
|
|
public NodeScopeResolver(string clusterId, IReadOnlyDictionary<string, NodeScope> pathIndex)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
|
|
ArgumentNullException.ThrowIfNull(pathIndex);
|
|
_clusterId = clusterId;
|
|
_index = pathIndex.ToFrozenDictionary(StringComparer.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve a node scope for the given driver-side <paramref name="fullReference"/>.
|
|
/// 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.
|
|
/// </summary>
|
|
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,
|
|
};
|
|
}
|
|
}
|