feat(auth): cut MxGateway API keys over to ZB.MOM.WW.Auth.ApiKeys 0.1.2; keep constraint enforcement+gRPC+CLI on top (Task 1.3)
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
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>
|
||||
public sealed class ApiKeyAdminCliRunner(
|
||||
IAuthStoreMigrator migrator,
|
||||
IApiKeyAdminStore adminStore,
|
||||
IApiKeyAuditStore auditStore,
|
||||
IApiKeySecretHasher hasher)
|
||||
/// <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()
|
||||
{
|
||||
@@ -44,8 +48,7 @@ public sealed class ApiKeyAdminCliRunner(
|
||||
|
||||
private async Task<ApiKeyAdminOutput> InitDbAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await migrator.MigrateAsync(cancellationToken).ConfigureAwait(false);
|
||||
await AppendAuditAsync(null, "init-db", null, cancellationToken).ConfigureAwait(false);
|
||||
await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new ApiKeyAdminOutput("init-db", "initialized", null, []);
|
||||
}
|
||||
@@ -54,33 +57,26 @@ public sealed class ApiKeyAdminCliRunner(
|
||||
ApiKeyAdminCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await migrator.MigrateAsync(cancellationToken).ConfigureAwait(false);
|
||||
// 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);
|
||||
string secret = ApiKeySecretGenerator.Generate();
|
||||
string apiKey = FormatApiKey(keyId, secret);
|
||||
|
||||
await adminStore.CreateAsync(
|
||||
new ApiKeyCreateRequest(
|
||||
KeyId: keyId,
|
||||
KeyPrefix: $"mxgw_{keyId}",
|
||||
SecretHash: hasher.HashSecret(secret),
|
||||
DisplayName: Required(command.DisplayName),
|
||||
Scopes: command.Scopes,
|
||||
Constraints: command.Constraints,
|
||||
CreatedUtc: DateTimeOffset.UtcNow),
|
||||
CreateKeyResult created = await commands.CreateKeyAsync(
|
||||
keyId,
|
||||
Required(command.DisplayName),
|
||||
command.Scopes,
|
||||
ApiKeyConstraintSerializer.Serialize(command.Constraints),
|
||||
remoteAddress: null,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await AppendAuditAsync(keyId, "create-key", null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new ApiKeyAdminOutput("create-key", "created", apiKey, []);
|
||||
return new ApiKeyAdminOutput("create-key", "created", created.Token, []);
|
||||
}
|
||||
|
||||
private async Task<ApiKeyAdminOutput> ListKeysAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await migrator.MigrateAsync(cancellationToken).ConfigureAwait(false);
|
||||
IReadOnlyList<ApiKeyRecord> keys = await adminStore.ListAsync(cancellationToken).ConfigureAwait(false);
|
||||
await AppendAuditAsync(null, "list-keys", null, cancellationToken).ConfigureAwait(false);
|
||||
await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false);
|
||||
IReadOnlyList<ApiKeyListItem> keys = await commands.ListKeysAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new ApiKeyAdminOutput(
|
||||
"list-keys",
|
||||
@@ -93,35 +89,28 @@ public sealed class ApiKeyAdminCliRunner(
|
||||
ApiKeyAdminCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await migrator.MigrateAsync(cancellationToken).ConfigureAwait(false);
|
||||
await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string keyId = Required(command.KeyId);
|
||||
bool revoked = await adminStore.RevokeAsync(keyId, DateTimeOffset.UtcNow, cancellationToken)
|
||||
KeyActionResult result = await commands.RevokeKeyAsync(keyId, remoteAddress: null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await AppendAuditAsync(keyId, "revoke-key", revoked ? "revoked" : "not-found-or-already-revoked", cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new ApiKeyAdminOutput("revoke-key", revoked ? "revoked" : "not-found-or-already-revoked", null, []);
|
||||
return new ApiKeyAdminOutput("revoke-key", result.Succeeded ? "revoked" : "not-found-or-already-revoked", null, []);
|
||||
}
|
||||
|
||||
private async Task<ApiKeyAdminOutput> RotateKeyAsync(
|
||||
ApiKeyAdminCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await migrator.MigrateAsync(cancellationToken).ConfigureAwait(false);
|
||||
await commands.InitDbAsync(remoteAddress: null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string keyId = Required(command.KeyId);
|
||||
string secret = ApiKeySecretGenerator.Generate();
|
||||
string apiKey = FormatApiKey(keyId, secret);
|
||||
|
||||
bool rotated = await adminStore.RotateAsync(keyId, hasher.HashSecret(secret), DateTimeOffset.UtcNow, cancellationToken)
|
||||
CreateKeyResult rotated = await commands.RotateKeyAsync(keyId, remoteAddress: null, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await AppendAuditAsync(keyId, "rotate-key", rotated ? "rotated" : "not-found", cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
bool succeeded = rotated.Token is not null;
|
||||
|
||||
return new ApiKeyAdminOutput("rotate-key", rotated ? "rotated" : "not-found", rotated ? apiKey : null, []);
|
||||
return new ApiKeyAdminOutput("rotate-key", succeeded ? "rotated" : "not-found", rotated.Token, []);
|
||||
}
|
||||
|
||||
private static async Task WriteOutputAsync(
|
||||
@@ -150,40 +139,19 @@ public sealed class ApiKeyAdminCliRunner(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AppendAuditAsync(
|
||||
string? keyId,
|
||||
string eventType,
|
||||
string? details,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await auditStore.AppendAsync(
|
||||
new ApiKeyAuditEntry(
|
||||
KeyId: keyId,
|
||||
EventType: eventType,
|
||||
RemoteAddress: null,
|
||||
Details: details),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static ApiKeyAdminListedKey ToListedKey(ApiKeyRecord key)
|
||||
private static ApiKeyAdminListedKey ToListedKey(ApiKeyListItem key)
|
||||
{
|
||||
return new ApiKeyAdminListedKey(
|
||||
KeyId: key.KeyId,
|
||||
KeyPrefix: key.KeyPrefix,
|
||||
DisplayName: key.DisplayName,
|
||||
Scopes: key.Scopes,
|
||||
Constraints: key.Constraints,
|
||||
Constraints: ApiKeyConstraintSerializer.Deserialize(key.ConstraintsJson),
|
||||
CreatedUtc: key.CreatedUtc,
|
||||
LastUsedUtc: key.LastUsedUtc,
|
||||
RevokedUtc: key.RevokedUtc);
|
||||
}
|
||||
|
||||
private static string FormatApiKey(string keyId, string secret)
|
||||
{
|
||||
return $"mxgw_{keyId}_{secret}";
|
||||
}
|
||||
|
||||
private static string Required(string? value)
|
||||
{
|
||||
return value ?? throw new InvalidOperationException("Required command value was not provided.");
|
||||
|
||||
Reference in New Issue
Block a user