using ZB.MOM.WW.OtOpcUa.Configuration.Entities; // Fully-qualified rather than relative: this file's own namespace root contains a `Configuration` // namespace (ZB.MOM.WW.OtOpcUa.Host.Configuration), so a relative `Configuration.Enums` binds there and // fails to compile. using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Core.Authorization; namespace ZB.MOM.WW.OtOpcUa.Host.OpcUa; /// /// served from the deployment artifact (#520, stage 4). This is the key /// source for a driver-only node, which has had no ConfigDb since per-cluster mesh Phase 4 and /// until now denied every UserName token. /// /// /// /// Purely in-memory: decodes the keys and grants once per applied /// artifact and installs them on . That matters because the /// SDK raises the impersonation callback synchronously, under a SessionManager-wide /// lock — an I/O-bound lookup here would serialize every session activation in the server /// behind one disk or network round trip. /// /// /// ⚠️ Consequence: revocation lands on the next deployment, not immediately. A node reading /// the ConfigDb sees Enabled = 0 on the next request; this one sees it only once a new /// artifact arrives. That asymmetry is inherent to a node with no config database and is /// documented for operators on the AdminUI key page. /// /// /// Before the first artifact is applied the snapshot is empty, so every key is denied. That is the /// safe direction, and it is also honest — a node that has not been deployed to has no configured /// keys. /// /// /// Returns a principal's FULL grant set (#520 stage 5). It used to narrow to the /// Cluster-scoped rows, because those were the only ones that could confer an interim role. /// The narrowing was always redundant — applies exactly the /// same filter itself — and it became a trap once scope was enforced: a caller reasonably reading /// ApiKeyLookup.Grants as "this principal's grants" would have got a silently truncated set. /// Removing it changes no role. /// /// /// Cluster confinement is not this store's job. /// DeploymentArtifact.ParseDataPlaneAuthority already kept only the node's own cluster's ACL /// rows (ClusterFilterMode.ScopeTo, matched ), /// so the snapshot cannot contain another cluster's grant. reads /// the fleet-wide ConfigDb and therefore has to apply that confinement itself; the two stores agree /// on the outcome, not on the mechanism. /// /// public sealed class ArtifactApiKeyStore : IApiKeyStore { private readonly IDataPlaneAuthoritySource _authority; /// Initializes a new instance of the class. /// The node's current data-plane authorization inputs. public ArtifactApiKeyStore(IDataPlaneAuthoritySource authority) { ArgumentNullException.ThrowIfNull(authority); _authority = authority; } /// public Task FindAsync(string apiKeyId, DateTime utcNow, CancellationToken ct) { // Read the snapshot ONCE. Re-reading Current for the grants would let a deploy land between the // two reads and pair a key from one generation with grants from another. var snapshot = _authority.Current; var key = snapshot.FindUsableKey(apiKeyId, utcNow); return Task.FromResult(key is null ? null : new ApiKeyLookup(key, snapshot.GrantsFor(key.Principal))); } /// public Task> FindGrantsAsync(string principal, CancellationToken ct) => Task.FromResult(_authority.Current.GrantsFor(principal)); }