test(cli/org): grant enforcement + body/secret-stdin + key-edit coverage
Closes the minor coverage gaps from the final adversarial review: - org add card/key/totp reject ungranted + unknown collections (pins the grant gate on the new write paths, which runs before any secret prompt) - secure-note --body-stdin masks body; totp --secret-stdin round-trips (completes the --*-stdin matrix for the org surface) - key-material edit accept-branch round-trip, verified via get --show
This commit is contained in:
@@ -358,3 +358,101 @@ fn org_edit_totp_interactive_changes_issuer() {
|
||||
let got = f.run(&["org", "get", "AWS root"]);
|
||||
assert!(String::from_utf8_lossy(&got.stdout).contains("Issuer: GitHub"), "issuer edit did not take");
|
||||
}
|
||||
|
||||
// --- grant enforcement + remaining --*-stdin paths for the new types ---------
|
||||
|
||||
#[test]
|
||||
fn org_add_card_key_totp_reject_ungranted_and_unknown_collection() {
|
||||
let f = OrgFixture::new();
|
||||
// `secret` exists but is NOT granted to the owner.
|
||||
assert!(f.run(&["org", "create-collection", "secret", "--name", "secret"]).status.success());
|
||||
|
||||
// ensure_grant runs before any secret prompt in run_add, so these need no
|
||||
// stdin — each new type must be rejected for a collection it lacks a grant for.
|
||||
for args in [
|
||||
vec!["org", "add", "card", "--collection", "secret", "--title", "X", "--kind", "credit"],
|
||||
vec!["org", "add", "key", "--collection", "secret", "--title", "X"],
|
||||
vec!["org", "add", "totp", "--collection", "secret", "--title", "X", "--secret", "JBSWY3DPEHPK3PXP"],
|
||||
] {
|
||||
let out = f.run(&args);
|
||||
assert!(!out.status.success(), "ungranted add must fail: {args:?}");
|
||||
let err = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
assert!(err.contains("access denied") || err.contains("grant"),
|
||||
"expected grant denial for {args:?}: {err}");
|
||||
}
|
||||
|
||||
// …and rejected for a nonexistent collection.
|
||||
for args in [
|
||||
vec!["org", "add", "card", "--collection", "ghost", "--title", "X", "--kind", "credit"],
|
||||
vec!["org", "add", "key", "--collection", "ghost", "--title", "X"],
|
||||
vec!["org", "add", "totp", "--collection", "ghost", "--title", "X", "--secret", "JBSWY3DPEHPK3PXP"],
|
||||
] {
|
||||
let out = f.run(&args);
|
||||
assert!(!out.status.success(), "unknown-collection add must fail: {args:?}");
|
||||
let err = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
assert!(err.contains("does not exist") || err.contains("ghost"),
|
||||
"expected unknown-collection error for {args:?}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn org_add_secure_note_via_body_stdin_masks_body() {
|
||||
let f = OrgFixture::new();
|
||||
f.create_collection_and_grant("eng");
|
||||
// build_secure_note(body_stdin=true) reads the body from stdin to EOF.
|
||||
let out = f.run_stdin(
|
||||
&["org", "add", "secure-note", "--collection", "eng", "--title", "Runbook", "--body-stdin"],
|
||||
"line one\nsuper-secret-line\n",
|
||||
);
|
||||
assert!(out.status.success(), "add note: {}", String::from_utf8_lossy(&out.stderr));
|
||||
|
||||
let got = f.run(&["org", "get", "Runbook"]);
|
||||
let stdout = String::from_utf8_lossy(&got.stdout).to_string();
|
||||
assert!(stdout.contains("********"), "note body must be masked without --show: {stdout}");
|
||||
assert!(!stdout.contains("super-secret-line"), "note body leaked without --show: {stdout}");
|
||||
|
||||
let shown = f.run(&["org", "get", "Runbook", "--show"]);
|
||||
assert!(String::from_utf8_lossy(&shown.stdout).contains("super-secret-line"), "body not revealed with --show");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn org_add_totp_via_secret_stdin_round_trips() {
|
||||
let f = OrgFixture::new();
|
||||
f.create_collection_and_grant("eng");
|
||||
// build_totp(secret_stdin=true) reads one base32 line from stdin.
|
||||
let out = f.run_stdin(
|
||||
&["org", "add", "totp", "--collection", "eng", "--title", "VPN", "--issuer", "Corp", "--secret-stdin"],
|
||||
"JBSWY3DPEHPK3PXP\n",
|
||||
);
|
||||
assert!(out.status.success(), "add totp: {}", String::from_utf8_lossy(&out.stderr));
|
||||
|
||||
let got = f.run(&["org", "get", "VPN"]);
|
||||
assert!(String::from_utf8_lossy(&got.stdout).contains("Issuer: Corp"), "issuer missing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn org_edit_key_replaces_material_and_reveals_with_show() {
|
||||
let f = OrgFixture::new();
|
||||
f.create_collection_and_grant("eng");
|
||||
let out = f.run_stdin(
|
||||
&["org", "add", "key", "--collection", "eng", "--title", "Signing Key",
|
||||
"--label", "ci", "--material-stdin"],
|
||||
"OLD-MATERIAL-aaaa\n",
|
||||
);
|
||||
assert!(out.status.success(), "add key: {}", String::from_utf8_lossy(&out.stderr));
|
||||
|
||||
// Interactive edit: keep title, ACCEPT "Replace key material?" -> new material
|
||||
// read from stdin to EOF (edit_key). Exercises the accept branch + history push.
|
||||
let out = f.run_stdin(&["org", "edit", "Signing Key"], "\ny\nNEW-MATERIAL-bbbb\n");
|
||||
assert!(out.status.success(), "org edit key: {}", String::from_utf8_lossy(&out.stderr));
|
||||
|
||||
let masked = f.run(&["org", "get", "Signing Key"]);
|
||||
let masked = String::from_utf8_lossy(&masked.stdout).to_string();
|
||||
assert!(masked.contains("********"), "material must be masked without --show: {masked}");
|
||||
assert!(!masked.contains("NEW-MATERIAL"), "material leaked without --show: {masked}");
|
||||
|
||||
let shown = f.run(&["org", "get", "Signing Key", "--show"]);
|
||||
let shown = String::from_utf8_lossy(&shown.stdout).to_string();
|
||||
assert!(shown.contains("NEW-MATERIAL-bbbb"), "replaced material not revealed with --show: {shown}");
|
||||
assert!(!shown.contains("OLD-MATERIAL"), "old material still present after replace: {shown}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user