use sysknife_daemon::actions::{ssh, ActionMechanism};
use sysknife_daemon::executor::execute_spec;
use tempfile::tempdir;
const TEST_KEY: &str =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKeyForSysknifeUnitTestsDoNotUse testuser@sysknife-test";
const OTHER_KEY: &str =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtherKeyForSysknifeUnitTestsOnly other@sysknife-test";
const USERNAME: &str = "testuser";
const WILDCARD_KEY: &str = "ssh-ed25519 .*";
fn redirect_spec_path(
mut spec: sysknife_daemon::actions::ActionSpec,
temp_path: &str,
) -> sysknife_daemon::actions::ActionSpec {
let real_path = format!("/home/{USERNAME}/.ssh/authorized_keys");
if let ActionMechanism::Command {
ref mut program,
ref mut args,
..
} = spec.mechanism
{
if *program == "sudo"
&& args.first().map(String::as_str) == Some("runuser")
&& args.get(3).map(String::as_str) == Some("--")
{
*program = "sh";
args.drain(0..5); } else if *program == "sudo" && args.first().map(String::as_str) == Some("sh") {
*program = "sh";
args.remove(0);
}
for arg in args.iter_mut() {
if arg.contains(&real_path) {
*arg = arg.replace(&real_path, temp_path);
}
}
}
spec
}
#[tokio::test]
async fn add_authorized_key_appends_key_to_existing_empty_file() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, "").unwrap();
let spec = redirect_spec_path(ssh::add_authorized_key(USERNAME, TEST_KEY), &keys_path);
let out = execute_spec(&spec).await.unwrap();
assert_eq!(out.exit_code, 0);
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
content.contains(TEST_KEY),
"key must appear in authorized_keys after add: {content:?}"
);
}
#[tokio::test]
async fn add_authorized_key_creates_file_when_absent() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
let spec = redirect_spec_path(ssh::add_authorized_key(USERNAME, TEST_KEY), &keys_path);
let out = execute_spec(&spec).await.unwrap();
assert_eq!(out.exit_code, 0);
assert!(
std::path::Path::new(&keys_path).exists(),
"authorized_keys must be created when absent"
);
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
content.contains(TEST_KEY),
"key must be in newly created file: {content:?}"
);
}
#[tokio::test]
async fn add_authorized_key_is_idempotent() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, format!("{TEST_KEY}\n")).unwrap();
let spec = redirect_spec_path(ssh::add_authorized_key(USERNAME, TEST_KEY), &keys_path);
execute_spec(&spec).await.unwrap();
let content = std::fs::read_to_string(&keys_path).unwrap();
let count = content.lines().filter(|line| *line == TEST_KEY).count();
assert_eq!(
count, 1,
"key must appear exactly once after idempotent add: {content:?}"
);
}
#[tokio::test]
async fn add_authorized_key_preserves_other_keys() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, format!("{OTHER_KEY}\n")).unwrap();
let spec = redirect_spec_path(ssh::add_authorized_key(USERNAME, TEST_KEY), &keys_path);
execute_spec(&spec).await.unwrap();
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
content.contains(OTHER_KEY),
"pre-existing key must not be removed: {content:?}"
);
assert!(
content.contains(TEST_KEY),
"new key must also be present: {content:?}"
);
}
#[tokio::test]
async fn remove_authorized_key_deletes_exact_matching_line() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, format!("{TEST_KEY}\n")).unwrap();
let spec = redirect_spec_path(ssh::remove_authorized_key(USERNAME, TEST_KEY), &keys_path);
let out = execute_spec(&spec).await.unwrap();
assert_eq!(out.exit_code, 0);
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
!content.contains(TEST_KEY),
"removed key must not remain in authorized_keys: {content:?}"
);
}
#[tokio::test]
async fn remove_authorized_key_preserves_other_keys() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, format!("{TEST_KEY}\n{OTHER_KEY}\n")).unwrap();
let spec = redirect_spec_path(ssh::remove_authorized_key(USERNAME, TEST_KEY), &keys_path);
execute_spec(&spec).await.unwrap();
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
!content.contains(TEST_KEY),
"target key must be removed: {content:?}"
);
assert!(
content.contains(OTHER_KEY),
"other key must remain untouched: {content:?}"
);
}
#[tokio::test]
async fn remove_authorized_key_treats_the_key_as_a_literal_not_a_pattern() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, format!("{TEST_KEY}\n{OTHER_KEY}\n")).unwrap();
let spec = redirect_spec_path(
ssh::remove_authorized_key(USERNAME, WILDCARD_KEY),
&keys_path,
);
let out = execute_spec(&spec).await.unwrap();
assert_eq!(out.exit_code, 0, "no-op removal must still exit 0");
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
content.contains(TEST_KEY),
"a wildcard-shaped key must not delete an unrelated key: {content:?}"
);
assert!(
content.contains(OTHER_KEY),
"a wildcard-shaped key must not delete an unrelated key: {content:?}"
);
}
#[tokio::test]
async fn remove_authorized_key_script_never_embeds_the_key_in_its_text() {
let spec = ssh::remove_authorized_key(USERNAME, TEST_KEY);
let ActionMechanism::Command { args, .. } = &spec.mechanism else {
panic!("remove_authorized_key must use a Command mechanism");
};
let script = args
.iter()
.find(|a| a.contains("grep") || a.contains("sed"))
.expect("script body must be present in argv");
assert!(
!script.contains(TEST_KEY),
"key must not be interpolated into the script body: {script:?}"
);
assert!(
!script.contains("sed"),
"removal must not build a sed address from caller data: {script:?}"
);
assert!(
args.iter().any(|a| a == TEST_KEY),
"key must be passed as its own argv element: {args:?}"
);
}
#[tokio::test]
async fn remove_authorized_key_is_noop_when_key_absent() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, format!("{OTHER_KEY}\n")).unwrap();
let spec = redirect_spec_path(
ssh::remove_authorized_key(USERNAME, TEST_KEY), &keys_path,
);
let out = execute_spec(&spec).await.unwrap();
assert_eq!(
out.exit_code, 0,
"remove when key is absent must exit 0 (no-op)"
);
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
content.contains(OTHER_KEY),
"unrelated key must not be affected by no-op remove: {content:?}"
);
}
#[test]
fn key_edits_drop_to_the_target_user() {
for spec in [
ssh::add_authorized_key(USERNAME, TEST_KEY),
ssh::remove_authorized_key(USERNAME, TEST_KEY),
] {
let ActionMechanism::Command { program, args } = &spec.mechanism else {
panic!("expected a Command mechanism");
};
assert_eq!(*program, "sudo", "{}", spec.action_name);
assert_eq!(
&args[0..4],
&["runuser", "-u", USERNAME, "--"],
"{} must run as {USERNAME}, not root; got {args:?}",
spec.action_name
);
assert!(
!args.iter().any(|a| a == "-lc" || a.contains("chown")),
"{} must not re-elevate; got {args:?}",
spec.action_name
);
}
}