Dashboard: delete revoked API keys + confirm Rotate/Revoke/Delete
Add IApiKeyAdminStore.DeleteAsync that only deletes already-revoked rows (active keys must be revoked first so the revoke event lands in the audit log before the row disappears) and a matching admin-gated DashboardApiKeyManagementService.DeleteAsync. ApiKeysPage now shows Delete on revoked rows in place of the old "No actions" stub, and Rotate/Revoke/Delete all route through ConfirmDialog so each destructive action requires an explicit confirmation step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+63
@@ -112,6 +112,56 @@ public sealed class DashboardApiKeyManagementServiceTests
|
||||
&& entry.Details == "rotated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAsync_UnauthorizedUser_DoesNotCallStore()
|
||||
{
|
||||
FakeApiKeyAdminStore adminStore = new();
|
||||
DashboardApiKeyManagementService service = CreateService(adminStore);
|
||||
|
||||
DashboardApiKeyManagementResult result = await service.DeleteAsync(
|
||||
new ClaimsPrincipal(new ClaimsIdentity()),
|
||||
"operator01",
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.Equal(0, adminStore.DeleteCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAsync_AuthorizedUser_DeletesRevokedKeyAndAudits()
|
||||
{
|
||||
FakeApiKeyAdminStore adminStore = new() { DeleteResult = true };
|
||||
FakeApiKeyAuditStore auditStore = new();
|
||||
DashboardApiKeyManagementService service = CreateService(adminStore, auditStore);
|
||||
|
||||
DashboardApiKeyManagementResult result = await service.DeleteAsync(
|
||||
CreateAuthorizedUser(),
|
||||
"operator01",
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.True(result.Succeeded);
|
||||
Assert.Equal("operator01", adminStore.LastDeletedKeyId);
|
||||
Assert.Contains(auditStore.Entries, entry =>
|
||||
entry.EventType == "dashboard-delete-key"
|
||||
&& entry.KeyId == "operator01"
|
||||
&& entry.Details == "deleted");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteAsync_WhenStoreRefuses_ReportsFriendlyError()
|
||||
{
|
||||
FakeApiKeyAdminStore adminStore = new() { DeleteResult = false };
|
||||
DashboardApiKeyManagementService service = CreateService(adminStore);
|
||||
|
||||
DashboardApiKeyManagementResult result = await service.DeleteAsync(
|
||||
CreateAuthorizedUser(),
|
||||
"operator01",
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(result.Succeeded);
|
||||
Assert.Contains("Revoke", result.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-004 regression: the dashboard create path must reject a request
|
||||
/// carrying a non-canonical scope string rather than persisting a key whose
|
||||
@@ -184,12 +234,18 @@ public sealed class DashboardApiKeyManagementServiceTests
|
||||
|
||||
public int RevokeCount { get; private set; }
|
||||
|
||||
public int DeleteCount { get; private set; }
|
||||
|
||||
public bool RevokeResult { get; init; }
|
||||
|
||||
public bool RotateResult { get; init; }
|
||||
|
||||
public bool DeleteResult { get; init; }
|
||||
|
||||
public string? LastRevokedKeyId { get; private set; }
|
||||
|
||||
public string? LastDeletedKeyId { get; private set; }
|
||||
|
||||
public byte[]? LastRotatedSecretHash { get; private set; }
|
||||
|
||||
public List<ApiKeyCreateRequest> CreatedRequests { get; } = [];
|
||||
@@ -225,6 +281,13 @@ public sealed class DashboardApiKeyManagementServiceTests
|
||||
LastRotatedSecretHash = secretHash;
|
||||
return Task.FromResult(RotateResult);
|
||||
}
|
||||
|
||||
public Task<bool> DeleteAsync(string keyId, CancellationToken cancellationToken)
|
||||
{
|
||||
DeleteCount++;
|
||||
LastDeletedKeyId = keyId;
|
||||
return Task.FromResult(DeleteResult);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeApiKeyAuditStore : IApiKeyAuditStore
|
||||
|
||||
@@ -447,6 +447,11 @@ public sealed class DashboardSnapshotServiceTests
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public Task<bool> DeleteAsync(string keyId, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
private class CountingApiKeyAdminStore(params ApiKeyRecord[] records) : FakeApiKeyAdminStore
|
||||
|
||||
Reference in New Issue
Block a user