using System.Collections.Concurrent; using LibApiKeyIdentity = ZB.MOM.WW.Auth.Abstractions.ApiKeys.ApiKeyIdentity; namespace ZB.MOM.WW.MxGateway.Server.Security.Authentication; /// /// Maps the shared (which /// carries the key's scopes plus the opaque constraints JSON blob) onto the gateway's /// (which exposes the deserialized /// the downstream authorization code enforces). /// /// /// The shared verifier does not interpret the constraints column; it returns the stored /// JSON verbatim in . /// This mapper re-hydrates it via so the gateway's /// constraint enforcement (ConstraintEnforcer) and request-identity accessor continue /// to operate on the strongly-typed model unchanged. /// public static class GatewayApiKeyIdentityMapper { // SEC-08: memoize the constraints deserialization keyed by the raw JSON blob so the per-call // JSON parse on the auth hot path collapses to a dictionary lookup. Distinct blobs are bounded // by the number of API keys (small); ApiKeyConstraints is an immutable record, so a parsed // instance is safe to share across callers. The cap is a defensive backstop against a pathological // spread of distinct blobs — past it, we simply parse without caching rather than grow unbounded. private const int MaxCachedConstraintBlobs = 1024; private static readonly ConcurrentDictionary ConstraintCache = new(StringComparer.Ordinal); private static ApiKeyConstraints DeserializeConstraints(string? constraintsJson) { if (string.IsNullOrWhiteSpace(constraintsJson)) { return ApiKeyConstraints.Empty; } if (ConstraintCache.TryGetValue(constraintsJson, out ApiKeyConstraints? cached)) { return cached; } ApiKeyConstraints parsed = ApiKeyConstraintSerializer.Deserialize(constraintsJson); if (ConstraintCache.Count < MaxCachedConstraintBlobs) { ConstraintCache.TryAdd(constraintsJson, parsed); } return parsed; } /// /// Converts a shared API-key identity into the gateway identity, deserializing the opaque /// constraints JSON into . /// /// The shared identity returned by the library verifier. /// The gateway identity carrying the effective constraints. public static ApiKeyIdentity ToGatewayIdentity(LibApiKeyIdentity identity) { ArgumentNullException.ThrowIfNull(identity); // The library stores the opaque constraints blob in Constraints as the ConstraintsJson // string (or null when the key has no constraints). string? constraintsJson = identity.Constraints as string; return new ApiKeyIdentity( KeyId: identity.KeyId, // The gateway token prefix is fixed ("mxgw"); the key id is its own field. KeyPrefix // is retained only for surface compatibility with the gateway identity record. KeyPrefix: "mxgw", DisplayName: identity.DisplayName, Scopes: identity.Scopes, Constraints: DeserializeConstraints(constraintsJson)); } }