feat(kms): Implement file-backed key management commands and handlers
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user