160 lines
6.3 KiB
C#
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.");
|
|
}
|
|
}
|