using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; namespace ZB.MOM.WW.ScadaBridge.CLI; /// /// In-memory, process-lifetime store of the bearer tokens the CLI obtains from /// POST /auth/token (review 2026-10-21 05/U1, 05/P1). One entry per /// (management URL, username, password) so every a /// single CLI invocation creates shares one credential exchange — one LDAP bind per invocation /// instead of one per management call. /// /// /// Tokens are never written to disk, logged or echoed; the store dies with the process. The key /// includes a SHA-256 digest of the password rather than the password itself, so a different /// password for the same user never reuses a token minted for the old one. /// internal sealed class CliTokenStore { /// The process-wide store used by the public constructor. public static CliTokenStore Shared { get; } = new(); private readonly ConcurrentDictionary _sessions = new(StringComparer.Ordinal); /// Returns the (shared) session for a management URL and credential pair. /// The management base URL. /// The CLI username. /// The CLI password. /// The session carrying the cached token, if any. public Session For(string baseUrl, string username, string password) { var digest = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(password))); var key = $"{baseUrl.TrimEnd('/').ToLowerInvariant()}\n{username}\n{digest}"; return _sessions.GetOrAdd(key, _ => new Session()); } /// Token state for one credential pair against one server. internal sealed class Session { /// Serialises token exchanges so concurrent calls share one bind. public SemaphoreSlim Gate { get; } = new(1, 1); /// The current bearer token, or null when none is held. public string? Token { get; set; } /// When should be treated as stale (expiry minus a safety margin). public DateTimeOffset RefreshAfter { get; set; } /// /// True once the server has shown it does not take bearer tokens on the management /// surfaces (no /auth/token, an older server whose tokens nothing accepts, or a /// login-disabled deployment): every later call goes straight to HTTP Basic. /// public bool BasicOnly { get; set; } /// True while a non-stale token is held. /// The current instant. /// Whether can be sent. public bool HasUsableToken(DateTimeOffset now) => Token is not null && now < RefreshAfter; } }