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,5 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
|
||||
using ZB.MOM.WW.Auth.ApiKeys.Admin;
|
||||
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
|
||||
using ZB.MOM.WW.MxGateway.Server.Security.Authorization;
|
||||
|
||||
@@ -7,12 +9,13 @@ namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
|
||||
|
||||
public sealed class DashboardApiKeyManagementService(
|
||||
DashboardApiKeyAuthorization authorization,
|
||||
ApiKeyAdminCommands adminCommands,
|
||||
IApiKeyAdminStore adminStore,
|
||||
IApiKeyAuditStore auditStore,
|
||||
IApiKeySecretHasher hasher,
|
||||
IHttpContextAccessor httpContextAccessor) : IDashboardApiKeyManagementService
|
||||
{
|
||||
private const string UnauthorizedMessage = "Sign in with an authorized LDAP account to manage API keys.";
|
||||
private const string PepperUnavailableMarker = "pepper unavailable";
|
||||
|
||||
/// <summary>Determines whether the user can manage API keys.</summary>
|
||||
/// <param name="user">The authenticated user principal.</param>
|
||||
@@ -42,28 +45,29 @@ public sealed class DashboardApiKeyManagementService(
|
||||
}
|
||||
|
||||
string keyId = request.KeyId.Trim();
|
||||
string secret = ApiKeySecretGenerator.Generate();
|
||||
string apiKey = FormatApiKey(keyId, secret);
|
||||
|
||||
try
|
||||
{
|
||||
await adminStore.CreateAsync(
|
||||
new ApiKeyCreateRequest(
|
||||
KeyId: keyId,
|
||||
KeyPrefix: $"mxgw_{keyId}",
|
||||
SecretHash: hasher.HashSecret(secret),
|
||||
DisplayName: request.DisplayName.Trim(),
|
||||
Scopes: request.Scopes,
|
||||
Constraints: request.Constraints,
|
||||
CreatedUtc: DateTimeOffset.UtcNow),
|
||||
// The shared command set generates the secret, hashes it with the pepper, persists the
|
||||
// record and assembles the mxgw_<id>_<secret> token (shown once). It also appends its own
|
||||
// "create-key" audit entry; the dashboard layers a "dashboard-create-key" entry with the
|
||||
// caller's remote address on top to preserve the dashboard audit vocabulary.
|
||||
CreateKeyResult created = await adminCommands.CreateKeyAsync(
|
||||
keyId,
|
||||
request.DisplayName.Trim(),
|
||||
request.Scopes,
|
||||
ApiKeyConstraintSerializer.Serialize(request.Constraints),
|
||||
RemoteAddress(),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await AppendAuditAsync(keyId, "dashboard-create-key", null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return DashboardApiKeyManagementResult.Success("API key created. Copy the key now; it will not be shown again.", apiKey);
|
||||
return DashboardApiKeyManagementResult.Success(
|
||||
"API key created. Copy the key now; it will not be shown again.",
|
||||
created.Token);
|
||||
}
|
||||
catch (ApiKeyPepperUnavailableException)
|
||||
catch (InvalidOperationException exception) when (IsPepperUnavailable(exception))
|
||||
{
|
||||
return DashboardApiKeyManagementResult.Fail("API key pepper is not configured.");
|
||||
}
|
||||
@@ -94,18 +98,18 @@ public sealed class DashboardApiKeyManagementService(
|
||||
}
|
||||
|
||||
string normalizedKeyId = keyId.Trim();
|
||||
bool revoked = await adminStore
|
||||
.RevokeAsync(normalizedKeyId, DateTimeOffset.UtcNow, cancellationToken)
|
||||
KeyActionResult result = await adminCommands
|
||||
.RevokeKeyAsync(normalizedKeyId, RemoteAddress(), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await AppendAuditAsync(
|
||||
normalizedKeyId,
|
||||
"dashboard-revoke-key",
|
||||
revoked ? "revoked" : "not-found-or-already-revoked",
|
||||
result.Succeeded ? "revoked" : "not-found-or-already-revoked",
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return revoked
|
||||
return result.Succeeded
|
||||
? DashboardApiKeyManagementResult.Success("API key revoked.")
|
||||
: DashboardApiKeyManagementResult.Fail("API key was not found or is already revoked.");
|
||||
}
|
||||
@@ -131,27 +135,29 @@ public sealed class DashboardApiKeyManagementService(
|
||||
}
|
||||
|
||||
string normalizedKeyId = keyId.Trim();
|
||||
string secret = ApiKeySecretGenerator.Generate();
|
||||
string apiKey = FormatApiKey(normalizedKeyId, secret);
|
||||
|
||||
try
|
||||
{
|
||||
bool rotated = await adminStore
|
||||
.RotateAsync(normalizedKeyId, hasher.HashSecret(secret), DateTimeOffset.UtcNow, cancellationToken)
|
||||
CreateKeyResult rotated = await adminCommands
|
||||
.RotateKeyAsync(normalizedKeyId, RemoteAddress(), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
bool succeeded = rotated.Token is not null;
|
||||
|
||||
await AppendAuditAsync(
|
||||
normalizedKeyId,
|
||||
"dashboard-rotate-key",
|
||||
rotated ? "rotated" : "not-found",
|
||||
succeeded ? "rotated" : "not-found",
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return rotated
|
||||
? DashboardApiKeyManagementResult.Success("API key rotated. Copy the key now; it will not be shown again.", apiKey)
|
||||
return succeeded
|
||||
? DashboardApiKeyManagementResult.Success(
|
||||
"API key rotated. Copy the key now; it will not be shown again.",
|
||||
rotated.Token)
|
||||
: DashboardApiKeyManagementResult.Fail("API key was not found.");
|
||||
}
|
||||
catch (ApiKeyPepperUnavailableException)
|
||||
catch (InvalidOperationException exception) when (IsPepperUnavailable(exception))
|
||||
{
|
||||
return DashboardApiKeyManagementResult.Fail("API key pepper is not configured.");
|
||||
}
|
||||
@@ -194,6 +200,9 @@ public sealed class DashboardApiKeyManagementService(
|
||||
: DashboardApiKeyManagementResult.Fail("API key was not found, or is still active. Revoke it before deleting.");
|
||||
}
|
||||
|
||||
private string? RemoteAddress() =>
|
||||
httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();
|
||||
|
||||
private async Task AppendAuditAsync(
|
||||
string? keyId,
|
||||
string eventType,
|
||||
@@ -204,12 +213,16 @@ public sealed class DashboardApiKeyManagementService(
|
||||
new ApiKeyAuditEntry(
|
||||
KeyId: keyId,
|
||||
EventType: eventType,
|
||||
RemoteAddress: httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString(),
|
||||
RemoteAddress: RemoteAddress(),
|
||||
CreatedUtc: DateTimeOffset.UtcNow,
|
||||
Details: details),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static bool IsPepperUnavailable(InvalidOperationException exception) =>
|
||||
exception.Message.Contains(PepperUnavailableMarker, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string? ValidateCreateRequest(DashboardApiKeyManagementRequest request)
|
||||
{
|
||||
string? keyIdValidation = ValidateKeyId(request.KeyId);
|
||||
@@ -248,9 +261,4 @@ public sealed class DashboardApiKeyManagementService(
|
||||
? null
|
||||
: "API key id may contain only letters, numbers, periods, and hyphens.";
|
||||
}
|
||||
|
||||
private static string FormatApiKey(string keyId, string secret)
|
||||
{
|
||||
return $"mxgw_{keyId}_{secret}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Runtime.CompilerServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
|
||||
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
using ZB.MOM.WW.MxGateway.Server.Galaxy;
|
||||
using ZB.MOM.WW.MxGateway.Server.Metrics;
|
||||
@@ -242,7 +243,7 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
|
||||
KeyId: key.KeyId,
|
||||
DisplayName: key.DisplayName,
|
||||
Scopes: key.Scopes,
|
||||
Constraints: key.Constraints,
|
||||
Constraints: ApiKeyConstraintSerializer.Deserialize(key.ConstraintsJson),
|
||||
CreatedUtc: key.CreatedUtc,
|
||||
LastUsedUtc: key.LastUsedUtc,
|
||||
RevokedUtc: key.RevokedUtc))
|
||||
|
||||
Reference in New Issue
Block a user