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)
|
||||
{
|
||||
|
||||
@@ -1,32 +1,59 @@
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Plugins;
|
||||
using Xunit;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Plugins;
|
||||
using StellaOps.Cli.Plugins.NonCore;
|
||||
using StellaOps.Cli.Tests.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cli.Tests.Plugins;
|
||||
|
||||
public sealed class CliCommandModuleLoaderTests
|
||||
{
|
||||
[Fact]
|
||||
public void RegisterModules_LoadsNonCoreCommandsFromPlugin()
|
||||
{
|
||||
var options = new StellaOpsCliOptions();
|
||||
var repoRoot = Path.GetFullPath(
|
||||
Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
|
||||
|
||||
options.Plugins.BaseDirectory = repoRoot;
|
||||
options.Plugins.Directory = "plugins/cli";
|
||||
options.Plugins.ManifestSearchPattern = "manifest.json";
|
||||
|
||||
var services = new ServiceCollection()
|
||||
.AddSingleton(options)
|
||||
.BuildServiceProvider();
|
||||
[Fact]
|
||||
public void RegisterModules_LoadsNonCoreCommandsFromPlugin()
|
||||
{
|
||||
using var temp = new TempDirectory();
|
||||
var manifestDirectory = Path.Combine(temp.Path, "plugins", "cli");
|
||||
Directory.CreateDirectory(manifestDirectory);
|
||||
|
||||
var assemblyPath = typeof(NonCoreCliCommandModule).Assembly.Location;
|
||||
var manifestPath = Path.Combine(manifestDirectory, "noncore.manifest.json");
|
||||
var manifestBody = new
|
||||
{
|
||||
schemaVersion = CliPluginManifest.CurrentSchemaVersion,
|
||||
id = "stellaops.cli.plugins.noncore",
|
||||
displayName = "NonCore CLI Commands",
|
||||
version = "1.0.0",
|
||||
requiresRestart = true,
|
||||
entryPoint = new
|
||||
{
|
||||
type = "dotnet",
|
||||
assembly = assemblyPath,
|
||||
typeName = typeof(NonCoreCliCommandModule).FullName
|
||||
}
|
||||
};
|
||||
|
||||
File.WriteAllText(
|
||||
manifestPath,
|
||||
JsonSerializer.Serialize(manifestBody, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true }));
|
||||
|
||||
var options = new StellaOpsCliOptions();
|
||||
options.Plugins.BaseDirectory = temp.Path;
|
||||
options.Plugins.Directory = "plugins/cli";
|
||||
options.Plugins.ManifestSearchPattern = "*.manifest.json";
|
||||
options.Plugins.SearchPatterns.Clear();
|
||||
options.Plugins.SearchPatterns.Add(Path.GetFileName(assemblyPath));
|
||||
|
||||
var services = new ServiceCollection()
|
||||
.AddSingleton(options)
|
||||
.BuildServiceProvider();
|
||||
|
||||
var logger = NullLoggerFactory.Instance.CreateLogger<CliCommandModuleLoader>();
|
||||
var loader = new CliCommandModuleLoader(services, options, logger);
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Spectre.Console.Testing" Version="0.48.0" />
|
||||
<ProjectReference Include="../../StellaOps.Cli/StellaOps.Cli.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cli.Plugins.NonCore/StellaOps.Cli.Plugins.NonCore.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user