fix(secrets-cli): bundle import — source-key verification, format gate up front, access-denied containment
This commit is contained in:
+59
-1
@@ -100,7 +100,7 @@ public sealed class BundleFlowTests : IDisposable
|
||||
|
||||
Assert.True(new BundleExportFlow().RequiresKek);
|
||||
Assert.True(File.Exists(outPath));
|
||||
Assert.Contains("2", console.Output, StringComparison.Ordinal);
|
||||
Assert.Contains("Exported 2 row(s)", console.Output, StringComparison.Ordinal);
|
||||
// The offered default filename carries the target name and the .json extension.
|
||||
Assert.Contains($"secrets-bundle-{session.Target.Name}", console.Output, StringComparison.Ordinal);
|
||||
Assert.Contains(".json", console.Output, StringComparison.Ordinal);
|
||||
@@ -201,4 +201,62 @@ public sealed class BundleFlowTests : IDisposable
|
||||
|
||||
Assert.DoesNotContain(base64A, console.Output, StringComparison.Ordinal); // pasted key never echoed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_wrong_source_key_is_rejected_without_importing()
|
||||
{
|
||||
LiteralMasterKeyProvider kekA = NewKek(out _);
|
||||
LiteralMasterKeyProvider kekB = NewKek(out _);
|
||||
LiteralMasterKeyProvider wrong = NewKek(out string wrongBase64); // valid format, unrelated key
|
||||
Assert.NotEqual(kekA.KekId, wrong.KekId);
|
||||
|
||||
SecretsSession source = await NewSessionAsync("a.db", kekA);
|
||||
await SealAsync(source, "svc/cross", "cross-value");
|
||||
|
||||
var exportConsole = NewConsole();
|
||||
exportConsole.Input.PushTextWithEnter(BundlePath);
|
||||
exportConsole.Input.PushTextWithEnter("n");
|
||||
await new BundleExportFlow().RunAsync(exportConsole, source, CancellationToken.None);
|
||||
|
||||
SecretsSession target = await NewSessionAsync("b.db", kekB);
|
||||
|
||||
TestConsole console = NewConsole();
|
||||
console.Input.PushTextWithEnter(BundlePath); // path
|
||||
console.Input.PushKey(ConsoleKey.Enter); // source-key selection: paste base64
|
||||
console.Input.PushTextWithEnter(wrongBase64); // a valid-format but wrong key
|
||||
|
||||
await new BundleImportFlow().RunAsync(console, target, CancellationToken.None);
|
||||
|
||||
Assert.Contains("does not match this bundle's source KEK", console.Output, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Imported", console.Output, StringComparison.Ordinal); // no report rendered
|
||||
Assert.Null(await target.Store.GetAsync(new SecretName("svc/cross"), CancellationToken.None)); // untouched
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Import_rejects_unknown_format_version_before_prompting()
|
||||
{
|
||||
LiteralMasterKeyProvider kek = NewKek(out _);
|
||||
SecretsSession target = await NewSessionAsync("b.db", kek);
|
||||
|
||||
// A hand-built bundle envelope on an unreadable format version. Only the path is scripted: if the
|
||||
// flow prompted for anything past the format gate, the missing input would fail the test.
|
||||
const string json = """
|
||||
{
|
||||
"FormatVersion": 99,
|
||||
"ExportedUtc": "2026-01-01T00:00:00+00:00",
|
||||
"SourceKekId": "some-kek",
|
||||
"Entries": []
|
||||
}
|
||||
""";
|
||||
await File.WriteAllTextAsync(BundlePath, json, CancellationToken.None);
|
||||
|
||||
TestConsole console = NewConsole();
|
||||
console.Input.PushTextWithEnter(BundlePath); // path only — no further prompt must be consumed
|
||||
|
||||
await new BundleImportFlow().RunAsync(console, target, CancellationToken.None);
|
||||
|
||||
Assert.Contains("99", console.Output, StringComparison.Ordinal);
|
||||
Assert.Contains("format version", console.Output, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Imported", console.Output, StringComparison.Ordinal); // no report rendered
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user