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));
}