Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminCliRunner.cs
T

160 lines
6.3 KiB
C#

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;
/// <summary>
/// Executes API key administration commands from the CLI.
/// </summary>
/// <remarks>
/// The create/revoke/rotate/list/init-db verbs (secret generation, peppered hashing, token
/// assembly and per-action audit) are delegated to the shared
/// <see cref="ApiKeyAdminCommands"/>. This runner adapts the gateway's strongly-typed command and
/// output DTOs (which carry <see cref="ApiKeyConstraints"/>) onto the library's JSON-based contract.
/// </remarks>
public sealed class ApiKeyAdminCliRunner(ApiKeyAdminCommands commands)
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true
};
/// <summary>
/// Runs an API key administration command and writes the output.
/// </summary>
/// <param name="command">API key administration command to execute.</param>
/// <param name="output">Text writer for command output.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
public async Task<int> 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<ApiKeyAdminOutput> InitDbAsync(CancellationToken cancellationToken)
{
await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false);
return new ApiKeyAdminOutput("init-db", "initialized", null, []);
}
private async Task<ApiKeyAdminOutput> 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<ApiKeyAdminOutput> ListKeysAsync(CancellationToken cancellationToken)
{
await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false);
IReadOnlyList<ApiKeyListItem> keys = await commands.ListKeysAsync(cancellationToken).ConfigureAwait(false);
return new ApiKeyAdminOutput(
"list-keys",
"ok",
null,
keys.Select(ToListedKey).ToArray());
}
private async Task<ApiKeyAdminOutput> 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<ApiKeyAdminOutput> 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.");
}
}