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"]);
|
let got = f.run(&["org", "get", "AWS root"]);
|
||||||
assert!(String::from_utf8_lossy(&got.stdout).contains("Issuer: GitHub"), "issuer edit did not take");
|
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