use std::path::{Path, PathBuf};
pub mod action_family;
pub mod config;
pub mod distro;
pub const PRODUCTION_DATABASE_PATH: &str = "/var/lib/sysknife/daemon.sqlite";
pub fn default_listen_uri() -> String {
if let Ok(uri) = std::env::var("SYSKNIFE_LISTEN_URI") {
return uri;
}
if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
let p = PathBuf::from(runtime).join("sysknife/daemon.sock");
return format!("unix://{}", p.display());
}
let uid = current_uid();
format!("unix:///tmp/sysknife-{uid}.sock")
}
pub fn default_database_path() -> PathBuf {
if let Ok(path) = std::env::var("SYSKNIFE_DATABASE_PATH") {
return PathBuf::from(path);
}
if let Ok(state) = std::env::var("XDG_STATE_HOME") {
return PathBuf::from(state).join("sysknife/daemon.sqlite");
}
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home).join(".local/state/sysknife/daemon.sqlite");
}
PathBuf::from(PRODUCTION_DATABASE_PATH)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuditStoreChoice {
Explicit(PathBuf),
PerUser(PathBuf),
System(PathBuf),
}
impl AuditStoreChoice {
pub fn path(&self) -> &Path {
match self {
Self::Explicit(p) | Self::PerUser(p) | Self::System(p) => p,
}
}
pub fn note(&self) -> Option<String> {
match self {
Self::System(path) => Some(format!(
"no per-user audit store found; reading the system daemon's store at {}",
path.display()
)),
Self::Explicit(_) | Self::PerUser(_) => None,
}
}
}
pub fn choose_audit_store(
explicit: Option<PathBuf>,
per_user: PathBuf,
per_user_exists: bool,
system_exists: bool,
) -> AuditStoreChoice {
choose_audit_store_with_paths(
explicit,
per_user,
per_user_exists,
PathBuf::from(PRODUCTION_DATABASE_PATH),
system_exists,
)
}
pub fn choose_audit_store_with_paths(
explicit: Option<PathBuf>,
per_user: PathBuf,
per_user_exists: bool,
system_path: PathBuf,
system_exists: bool,
) -> AuditStoreChoice {
if let Some(path) = explicit {
return AuditStoreChoice::Explicit(path);
}
if per_user_exists {
return AuditStoreChoice::PerUser(per_user);
}
if system_exists {
return AuditStoreChoice::System(system_path);
}
AuditStoreChoice::PerUser(per_user)
}
pub fn path_is_present(path: &Path) -> bool {
path_is_present_result(std::fs::metadata(path).map(|_| ()))
}
pub(crate) fn path_is_present_result(result: Result<(), std::io::Error>) -> bool {
match result {
Ok(()) => true,
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => true,
Err(_) => false,
}
}
pub fn resolve_audit_store() -> AuditStoreChoice {
let explicit = std::env::var_os("SYSKNIFE_DATABASE_PATH").map(PathBuf::from);
let per_user = default_database_path();
let system = PathBuf::from(PRODUCTION_DATABASE_PATH);
resolve_audit_store_from(explicit, per_user, system)
}
pub fn resolve_audit_store_from(
explicit: Option<PathBuf>,
per_user: PathBuf,
system: PathBuf,
) -> AuditStoreChoice {
let per_user_exists = path_is_present(&per_user);
let system_exists = path_is_present(&system);
choose_audit_store_with_paths(explicit, per_user, per_user_exists, system, system_exists)
}
fn current_uid() -> u32 {
std::fs::read_to_string("/proc/self/status")
.ok()
.and_then(|s| {
s.lines()
.find(|l| l.starts_with("Uid:"))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|v| v.parse::<u32>().ok())
})
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::{default_database_path, default_listen_uri, PRODUCTION_DATABASE_PATH};
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn production_database_path_is_absolute() {
assert!(PRODUCTION_DATABASE_PATH.starts_with("/var/lib/"));
}
#[test]
fn database_env_var_wins() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("SYSKNIFE_DATABASE_PATH", "/explicit/path/db.sqlite");
}
let p = default_database_path();
unsafe {
std::env::remove_var("SYSKNIFE_DATABASE_PATH");
}
assert_eq!(p.to_str(), Some("/explicit/path/db.sqlite"));
}
#[test]
fn database_xdg_state_used_when_set() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("SYSKNIFE_DATABASE_PATH");
std::env::set_var("XDG_STATE_HOME", "/xdg/state");
}
let p = default_database_path();
unsafe {
std::env::remove_var("XDG_STATE_HOME");
}
assert_eq!(p.to_str(), Some("/xdg/state/sysknife/daemon.sqlite"));
}
#[test]
fn database_falls_back_to_home_local_state() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("SYSKNIFE_DATABASE_PATH");
std::env::remove_var("XDG_STATE_HOME");
std::env::set_var("HOME", "/home/contributor");
}
let p = default_database_path();
unsafe {
std::env::remove_var("HOME");
}
assert_eq!(
p.to_str(),
Some("/home/contributor/.local/state/sysknife/daemon.sqlite")
);
}
#[test]
fn database_last_resort_is_production_path() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let saved_home = std::env::var("HOME").ok();
unsafe {
std::env::remove_var("SYSKNIFE_DATABASE_PATH");
std::env::remove_var("XDG_STATE_HOME");
std::env::remove_var("HOME");
}
let p = default_database_path();
unsafe {
if let Some(h) = saved_home {
std::env::set_var("HOME", h);
}
}
assert_eq!(p.to_str(), Some(PRODUCTION_DATABASE_PATH));
}
#[test]
fn listen_env_var_wins() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::set_var("SYSKNIFE_LISTEN_URI", "unix:///explicit.sock");
}
let u = default_listen_uri();
unsafe {
std::env::remove_var("SYSKNIFE_LISTEN_URI");
}
assert_eq!(u, "unix:///explicit.sock");
}
#[test]
fn listen_uses_xdg_runtime_dir_when_set() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("SYSKNIFE_LISTEN_URI");
std::env::set_var("XDG_RUNTIME_DIR", "/run/user/1000");
}
let u = default_listen_uri();
unsafe {
std::env::remove_var("XDG_RUNTIME_DIR");
}
assert_eq!(u, "unix:///run/user/1000/sysknife/daemon.sock");
}
#[test]
fn listen_last_resort_is_per_uid_tmp() {
let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe {
std::env::remove_var("SYSKNIFE_LISTEN_URI");
std::env::remove_var("XDG_RUNTIME_DIR");
}
let u = default_listen_uri();
assert!(u.starts_with("unix:///tmp/sysknife-"));
assert!(u.ends_with(".sock"));
}
use std::path::{Path, PathBuf};
#[test]
fn an_explicit_database_path_is_never_second_guessed() {
let choice = super::choose_audit_store(
Some(PathBuf::from("/srv/audit.sqlite")),
PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite"),
true,
true,
);
assert_eq!(choice.path(), Path::new("/srv/audit.sqlite"));
assert_eq!(
choice.note(),
None,
"an explicit choice needs no explanation"
);
}
#[test]
fn a_present_per_user_store_wins_over_the_system_one() {
let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
let choice = super::choose_audit_store(None, per_user.clone(), true, true);
assert_eq!(choice.path(), per_user.as_path());
}
#[test]
fn the_system_store_is_used_when_no_per_user_store_exists() {
let choice = super::choose_audit_store(
None,
PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite"),
false,
true,
);
assert_eq!(choice.path(), Path::new(super::PRODUCTION_DATABASE_PATH));
let note = choice
.note()
.expect("reading another store must be announced");
assert!(
note.contains(super::PRODUCTION_DATABASE_PATH),
"note names the store: {note}"
);
}
#[test]
fn a_supplied_system_store_path_is_used_when_the_per_user_store_is_absent() {
let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
let system = PathBuf::from("/tmp/system/daemon.sqlite");
let choice =
super::choose_audit_store_with_paths(None, per_user, false, system.clone(), true);
assert_eq!(choice, super::AuditStoreChoice::System(system));
}
#[test]
fn with_no_store_anywhere_the_per_user_path_is_reported() {
let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
let choice = super::choose_audit_store(None, per_user.clone(), false, false);
assert_eq!(choice.path(), per_user.as_path());
assert_eq!(choice.note(), None);
}
#[test]
fn permission_denied_counts_as_a_present_store() {
assert!(
super::path_is_present_result(Ok(())),
"a successful stat is present"
);
assert!(
super::path_is_present_result(Err(std::io::Error::from(
std::io::ErrorKind::PermissionDenied
))),
"EACCES must not be treated as missing"
);
assert!(
!super::path_is_present_result(Err(std::io::Error::from(std::io::ErrorKind::NotFound))),
"ENOENT remains missing"
);
}
#[test]
fn unreadable_system_store_wins_over_a_missing_per_user_store() {
let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
let system_exists = super::path_is_present_result(Err(std::io::Error::from(
std::io::ErrorKind::PermissionDenied,
)));
let choice = super::choose_audit_store(None, per_user, false, system_exists);
assert_eq!(
choice.path(),
Path::new(super::PRODUCTION_DATABASE_PATH),
"EACCES on the system store must not fall through to PerUser"
);
assert!(
choice.note().is_some(),
"System choice must still announce itself"
);
}
#[cfg(unix)]
#[test]
fn path_behind_mode_000_directory_counts_as_present() {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().expect("tempdir");
let locked = root.path().join("locked");
std::fs::create_dir(&locked).expect("mkdir");
let db = locked.join("daemon.sqlite");
std::fs::write(&db, b"x").expect("write db");
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000))
.expect("chmod 000");
let present = super::path_is_present(&db);
let exists_under_eacces = db.exists();
let _ = std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o700));
assert!(
present,
"metadata EACCES on a 0700/000 store must count as present, not missing"
);
assert!(
!exists_under_eacces,
"precondition: Path::exists is false under EACCES (the bug we are fixing)"
);
}
#[test]
fn the_system_store_constant_matches_the_packaged_unit() {
let unit = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../packaging/sysknife-daemon.service");
let contents = std::fs::read_to_string(&unit)
.unwrap_or_else(|e| panic!("read {}: {e}", unit.display()));
let expected = format!("SYSKNIFE_DATABASE_PATH={}", super::PRODUCTION_DATABASE_PATH);
assert!(
contents.contains(&expected),
"packaged unit must set {expected}; got:\n{contents}"
);
}
}