feat(kms): Implement file-backed key management commands and handlers
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Added `kms export` and `kms import` commands to manage file-backed signing keys.
- Implemented `HandleKmsExportAsync` and `HandleKmsImportAsync` methods in CommandHandlers for exporting and importing key material.
- Introduced KmsPassphrasePrompt for secure passphrase input.
- Updated CLI architecture documentation to include new KMS commands.
- Enhanced unit tests for KMS export and import functionalities.
- Updated project references to include StellaOps.Cryptography.Kms library.
- Marked KMS interface implementation and CLI support tasks as DONE in the task board.
This commit is contained in:
master
2025-10-30 14:41:48 +02:00
parent a3822c88cd
commit 240e8ff25d
13 changed files with 697 additions and 107 deletions

View File

@@ -19,11 +19,12 @@ using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client;
using StellaOps.Cli.Commands;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Telemetry;
using StellaOps.Cli.Tests.Testing;
using StellaOps.Cryptography;
using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Telemetry;
using StellaOps.Cli.Tests.Testing;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
using Spectre.Console;
using Spectre.Console.Testing;
@@ -2073,46 +2074,153 @@ public sealed class CommandHandlersTests
}
}
[Fact]
public async Task HandleAocVerifyAsync_MissingTenant_ReturnsUsageError()
{
var originalExitCode = Environment.ExitCode;
var originalTenant = Environment.GetEnvironmentVariable("STELLA_TENANT");
try
{
Environment.SetEnvironmentVariable("STELLA_TENANT", null);
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleAocVerifyAsync(
provider,
sinceOption: "24h",
limitOption: null,
sourcesOption: null,
codesOption: null,
format: "table",
exportPath: null,
tenantOverride: null,
disableColor: true,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(71, Environment.ExitCode);
}
finally
{
Environment.ExitCode = originalExitCode;
Environment.SetEnvironmentVariable("STELLA_TENANT", originalTenant);
}
}
private static IServiceProvider BuildServiceProvider(
IBackendOperationsClient backend,
IScannerExecutor? executor = null,
IScannerInstaller? installer = null,
StellaOpsCliOptions? options = null,
[Fact]
public async Task HandleAocVerifyAsync_MissingTenant_ReturnsUsageError()
{
var originalExitCode = Environment.ExitCode;
var originalTenant = Environment.GetEnvironmentVariable("STELLA_TENANT");
try
{
Environment.SetEnvironmentVariable("STELLA_TENANT", null);
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleAocVerifyAsync(
provider,
sinceOption: "24h",
limitOption: null,
sourcesOption: null,
codesOption: null,
format: "table",
exportPath: null,
tenantOverride: null,
disableColor: true,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(71, Environment.ExitCode);
}
finally
{
Environment.ExitCode = originalExitCode;
Environment.SetEnvironmentVariable("STELLA_TENANT", originalTenant);
}
}
[Fact]
public async Task HandleKmsExportAsync_WritesKeyBundle()
{
using var kmsRoot = new TempDirectory();
using var exportRoot = new TempDirectory();
const string passphrase = "P@ssw0rd!";
using (var client = new FileKmsClient(new FileKmsOptions
{
RootPath = kmsRoot.Path,
Password = passphrase
}))
{
await client.RotateAsync("cli-export-key");
}
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
var provider = BuildServiceProvider(backend);
var outputPath = Path.Combine(exportRoot.Path, "export.json");
var originalExit = Environment.ExitCode;
try
{
await CommandHandlers.HandleKmsExportAsync(
provider,
kmsRoot.Path,
keyId: "cli-export-key",
versionId: null,
outputPath: outputPath,
overwrite: false,
passphrase: passphrase,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.True(File.Exists(outputPath));
var json = await File.ReadAllTextAsync(outputPath);
var material = JsonSerializer.Deserialize<KmsKeyMaterial>(json, new JsonSerializerOptions(JsonSerializerDefaults.Web));
Assert.NotNull(material);
Assert.Equal("cli-export-key", material!.KeyId);
Assert.False(string.IsNullOrWhiteSpace(material.VersionId));
Assert.NotNull(material.D);
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleKmsImportAsync_ImportsKeyBundle()
{
using var sourceRoot = new TempDirectory();
using var targetRoot = new TempDirectory();
const string passphrase = "AnotherP@ssw0rd!";
KmsKeyMaterial exported;
using (var sourceClient = new FileKmsClient(new FileKmsOptions
{
RootPath = sourceRoot.Path,
Password = passphrase
}))
{
await sourceClient.RotateAsync("cli-import-key");
exported = await sourceClient.ExportAsync("cli-import-key", null);
}
var exportPath = Path.Combine(sourceRoot.Path, "import.json");
var exportJson = JsonSerializer.Serialize(exported, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true });
await File.WriteAllTextAsync(exportPath, exportJson);
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
var provider = BuildServiceProvider(backend);
var originalExit = Environment.ExitCode;
try
{
await CommandHandlers.HandleKmsImportAsync(
provider,
targetRoot.Path,
keyId: "cli-import-key",
inputPath: exportPath,
versionOverride: null,
passphrase: passphrase,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
using var importedClient = new FileKmsClient(new FileKmsOptions
{
RootPath = targetRoot.Path,
Password = passphrase
});
var metadata = await importedClient.GetMetadataAsync("cli-import-key");
Assert.Equal(KmsKeyState.Active, metadata.State);
Assert.Single(metadata.Versions);
Assert.Equal(exported.VersionId, metadata.Versions[0].VersionId);
}
finally
{
Environment.ExitCode = originalExit;
}
}
private static IServiceProvider BuildServiceProvider(
IBackendOperationsClient backend,
IScannerExecutor? executor = null,
IScannerInstaller? installer = null,
StellaOpsCliOptions? options = null,
IStellaOpsTokenClient? tokenClient = null,
IConcelierObservationsClient? concelierClient = null)
{