using System.Text.Json; using ZB.MOM.WW.Auth.Abstractions.ApiKeys; using ZB.MOM.WW.Auth.ApiKeys.Admin; namespace ZB.MOM.WW.MxGateway.Server.Security.Authentication; /// /// Executes API key administration commands from the CLI. /// /// /// The create/revoke/rotate/list/init-db verbs (secret generation, peppered hashing, token /// assembly and per-action audit) are delegated to the shared /// . This runner adapts the gateway's strongly-typed command and /// output DTOs (which carry ) onto the library's JSON-based contract. /// public sealed class ApiKeyAdminCliRunner(ApiKeyAdminCommands commands) { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; /// /// Runs an API key administration command and writes the output. /// /// API key administration command to execute. /// Text writer for command output. /// Token to cancel the asynchronous operation. public async Task RunAsync( ApiKeyAdminCommand command, TextWriter output, CancellationToken cancellationToken) { ApiKeyAdminOutput result = command.Kind switch { ApiKeyAdminCommandKind.InitDb => await InitDbAsync(cancellationToken).ConfigureAwait(false), ApiKeyAdminCommandKind.CreateKey => await CreateKeyAsync(command, cancellationToken).ConfigureAwait(false), ApiKeyAdminCommandKind.ListKeys => await ListKeysAsync(cancellationToken).ConfigureAwait(false), ApiKeyAdminCommandKind.RevokeKey => await RevokeKeyAsync(command, cancellationToken).ConfigureAwait(false), ApiKeyAdminCommandKind.RotateKey => await RotateKeyAsync(command, cancellationToken).ConfigureAwait(false), _ => throw new InvalidOperationException($"Unsupported API key command '{command.Kind}'.") }; await WriteOutputAsync(command, result, output).ConfigureAwait(false); return 0; } private async Task InitDbAsync(CancellationToken cancellationToken) { await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false); return new ApiKeyAdminOutput("init-db", "initialized", null, []); } private async Task CreateKeyAsync( ApiKeyAdminCommand command, CancellationToken cancellationToken) { // The shared command set requires the schema to exist; init-db is idempotent. await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false); string keyId = Required(command.KeyId); CreateKeyResult created = await commands.CreateKeyAsync( keyId, Required(command.DisplayName), command.Scopes, ApiKeyConstraintSerializer.Serialize(command.Constraints), remoteAddress: null, cancellationToken) .ConfigureAwait(false); return new ApiKeyAdminOutput("create-key", "created", created.Token, []); } private async Task ListKeysAsync(CancellationToken cancellationToken) { await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false); IReadOnlyList keys = await commands.ListKeysAsync(cancellationToken).ConfigureAwait(false); return new ApiKeyAdminOutput( "list-keys", "ok", null, keys.Select(ToListedKey).ToArray()); } private async Task RevokeKeyAsync( ApiKeyAdminCommand command, CancellationToken cancellationToken) { await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false); string keyId = Required(command.KeyId); KeyActionResult result = await commands.RevokeKeyAsync(keyId, remoteAddress: null, cancellationToken) .ConfigureAwait(false); return new ApiKeyAdminOutput("revoke-key", result.Succeeded ? "revoked" : "not-found-or-already-revoked", null, []); } private async Task RotateKeyAsync( ApiKeyAdminCommand command, CancellationToken cancellationToken) { await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false); string keyId = Required(command.KeyId); CreateKeyResult rotated = await commands.RotateKeyAsync(keyId, remoteAddress: null, cancellationToken) .ConfigureAwait(false); bool succeeded = rotated.Token is not null; return new ApiKeyAdminOutput("rotate-key", succeeded ? "rotated" : "not-found", rotated.Token, []); } private static async Task WriteOutputAsync( ApiKeyAdminCommand command, ApiKeyAdminOutput result, TextWriter output) { if (command.Json) { await output.WriteLineAsync(JsonSerializer.Serialize(result, JsonOptions)).ConfigureAwait(false); return; } await output.WriteLineAsync($"{result.Command}: {result.Status}").ConfigureAwait(false); if (result.ApiKey is not null) { await output.WriteLineAsync($"API key: {result.ApiKey}").ConfigureAwait(false); } foreach (ApiKeyAdminListedKey key in result.Keys) { string revoked = key.RevokedUtc is null ? "active" : "revoked"; await output.WriteLineAsync($"{key.KeyId}\t{key.DisplayName}\t{revoked}\t{string.Join(',', key.Scopes)}") .ConfigureAwait(false); } } private static ApiKeyAdminListedKey ToListedKey(ApiKeyListItem key) { return new ApiKeyAdminListedKey( KeyId: key.KeyId, KeyPrefix: key.KeyPrefix, DisplayName: key.DisplayName, Scopes: key.Scopes, Constraints: ApiKeyConstraintSerializer.Deserialize(key.ConstraintsJson), CreatedUtc: key.CreatedUtc, LastUsedUtc: key.LastUsedUtc, RevokedUtc: key.RevokedUtc); } private static string Required(string? value) { return value ?? throw new InvalidOperationException("Required command value was not provided."); } }