1use std::path::{Path, PathBuf};
4
5pub mod action_family;
6pub mod config;
7pub mod distro;
8
9pub const PRODUCTION_DATABASE_PATH: &str = "/var/lib/sysknife/daemon.sqlite";
13
14pub fn default_listen_uri() -> String {
24 if let Ok(uri) = std::env::var("SYSKNIFE_LISTEN_URI") {
25 return uri;
26 }
27 if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
28 let p = PathBuf::from(runtime).join("sysknife/daemon.sock");
29 return format!("unix://{}", p.display());
30 }
31 let uid = current_uid();
32 format!("unix:///tmp/sysknife-{uid}.sock")
33}
34
35pub fn default_database_path() -> PathBuf {
44 if let Ok(path) = std::env::var("SYSKNIFE_DATABASE_PATH") {
45 return PathBuf::from(path);
46 }
47 if let Ok(state) = std::env::var("XDG_STATE_HOME") {
48 return PathBuf::from(state).join("sysknife/daemon.sqlite");
49 }
50 if let Ok(home) = std::env::var("HOME") {
51 return PathBuf::from(home).join(".local/state/sysknife/daemon.sqlite");
52 }
53 PathBuf::from(PRODUCTION_DATABASE_PATH)
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum AuditStoreChoice {
71 Explicit(PathBuf),
74 PerUser(PathBuf),
76 System(PathBuf),
78}
79
80impl AuditStoreChoice {
81 pub fn path(&self) -> &Path {
83 match self {
84 Self::Explicit(p) | Self::PerUser(p) | Self::System(p) => p,
85 }
86 }
87
88 pub fn note(&self) -> Option<String> {
92 match self {
93 Self::System(path) => Some(format!(
94 "no per-user audit store found; reading the system daemon's store at {}",
95 path.display()
96 )),
97 Self::Explicit(_) | Self::PerUser(_) => None,
98 }
99 }
100}
101
102pub fn choose_audit_store(
105 explicit: Option<PathBuf>,
106 per_user: PathBuf,
107 per_user_exists: bool,
108 system_exists: bool,
109) -> AuditStoreChoice {
110 choose_audit_store_with_paths(
111 explicit,
112 per_user,
113 per_user_exists,
114 PathBuf::from(PRODUCTION_DATABASE_PATH),
115 system_exists,
116 )
117}
118
119pub fn choose_audit_store_with_paths(
124 explicit: Option<PathBuf>,
125 per_user: PathBuf,
126 per_user_exists: bool,
127 system_path: PathBuf,
128 system_exists: bool,
129) -> AuditStoreChoice {
130 if let Some(path) = explicit {
131 return AuditStoreChoice::Explicit(path);
132 }
133 if per_user_exists {
134 return AuditStoreChoice::PerUser(per_user);
135 }
136 if system_exists {
137 return AuditStoreChoice::System(system_path);
138 }
139 AuditStoreChoice::PerUser(per_user)
142}
143
144pub fn path_is_present(path: &Path) -> bool {
152 path_is_present_result(std::fs::metadata(path).map(|_| ()))
153}
154
155pub(crate) fn path_is_present_result(result: Result<(), std::io::Error>) -> bool {
160 match result {
161 Ok(()) => true,
162 Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => true,
163 Err(_) => false,
164 }
165}
166
167pub fn resolve_audit_store() -> AuditStoreChoice {
170 let explicit = std::env::var_os("SYSKNIFE_DATABASE_PATH").map(PathBuf::from);
171 let per_user = default_database_path();
172 let system = PathBuf::from(PRODUCTION_DATABASE_PATH);
173 resolve_audit_store_from(explicit, per_user, system)
174}
175
176pub fn resolve_audit_store_from(
181 explicit: Option<PathBuf>,
182 per_user: PathBuf,
183 system: PathBuf,
184) -> AuditStoreChoice {
185 let per_user_exists = path_is_present(&per_user);
186 let system_exists = path_is_present(&system);
187 choose_audit_store_with_paths(explicit, per_user, per_user_exists, system, system_exists)
188}
189
190fn current_uid() -> u32 {
197 std::fs::read_to_string("/proc/self/status")
198 .ok()
199 .and_then(|s| {
200 s.lines()
201 .find(|l| l.starts_with("Uid:"))
202 .and_then(|l| l.split_whitespace().nth(1))
203 .and_then(|v| v.parse::<u32>().ok())
204 })
205 .unwrap_or(0)
206}
207
208#[cfg(test)]
209mod tests {
210 use super::{default_database_path, default_listen_uri, PRODUCTION_DATABASE_PATH};
211 use std::sync::Mutex;
212
213 static ENV_LOCK: Mutex<()> = Mutex::new(());
214
215 #[test]
216 fn production_database_path_is_absolute() {
217 assert!(PRODUCTION_DATABASE_PATH.starts_with("/var/lib/"));
218 }
219
220 #[test]
221 fn database_env_var_wins() {
222 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
223 unsafe {
224 std::env::set_var("SYSKNIFE_DATABASE_PATH", "/explicit/path/db.sqlite");
225 }
226 let p = default_database_path();
227 unsafe {
228 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
229 }
230 assert_eq!(p.to_str(), Some("/explicit/path/db.sqlite"));
231 }
232
233 #[test]
234 fn database_xdg_state_used_when_set() {
235 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
236 unsafe {
237 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
238 std::env::set_var("XDG_STATE_HOME", "/xdg/state");
239 }
240 let p = default_database_path();
241 unsafe {
242 std::env::remove_var("XDG_STATE_HOME");
243 }
244 assert_eq!(p.to_str(), Some("/xdg/state/sysknife/daemon.sqlite"));
245 }
246
247 #[test]
248 fn database_falls_back_to_home_local_state() {
249 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
250 unsafe {
251 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
252 std::env::remove_var("XDG_STATE_HOME");
253 std::env::set_var("HOME", "/home/contributor");
254 }
255 let p = default_database_path();
256 unsafe {
257 std::env::remove_var("HOME");
258 }
259 assert_eq!(
260 p.to_str(),
261 Some("/home/contributor/.local/state/sysknife/daemon.sqlite")
262 );
263 }
264
265 #[test]
266 fn database_last_resort_is_production_path() {
267 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
268 let saved_home = std::env::var("HOME").ok();
270 unsafe {
271 std::env::remove_var("SYSKNIFE_DATABASE_PATH");
272 std::env::remove_var("XDG_STATE_HOME");
273 std::env::remove_var("HOME");
274 }
275 let p = default_database_path();
276 unsafe {
277 if let Some(h) = saved_home {
278 std::env::set_var("HOME", h);
279 }
280 }
281 assert_eq!(p.to_str(), Some(PRODUCTION_DATABASE_PATH));
282 }
283
284 #[test]
285 fn listen_env_var_wins() {
286 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
287 unsafe {
288 std::env::set_var("SYSKNIFE_LISTEN_URI", "unix:///explicit.sock");
289 }
290 let u = default_listen_uri();
291 unsafe {
292 std::env::remove_var("SYSKNIFE_LISTEN_URI");
293 }
294 assert_eq!(u, "unix:///explicit.sock");
295 }
296
297 #[test]
298 fn listen_uses_xdg_runtime_dir_when_set() {
299 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
300 unsafe {
301 std::env::remove_var("SYSKNIFE_LISTEN_URI");
302 std::env::set_var("XDG_RUNTIME_DIR", "/run/user/1000");
303 }
304 let u = default_listen_uri();
305 unsafe {
306 std::env::remove_var("XDG_RUNTIME_DIR");
307 }
308 assert_eq!(u, "unix:///run/user/1000/sysknife/daemon.sock");
309 }
310
311 #[test]
312 fn listen_last_resort_is_per_uid_tmp() {
313 let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
314 unsafe {
315 std::env::remove_var("SYSKNIFE_LISTEN_URI");
316 std::env::remove_var("XDG_RUNTIME_DIR");
317 }
318 let u = default_listen_uri();
319 assert!(u.starts_with("unix:///tmp/sysknife-"));
320 assert!(u.ends_with(".sock"));
321 }
322
323 use std::path::{Path, PathBuf};
328
329 #[test]
330 fn an_explicit_database_path_is_never_second_guessed() {
331 let choice = super::choose_audit_store(
332 Some(PathBuf::from("/srv/audit.sqlite")),
333 PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite"),
334 true,
335 true,
336 );
337 assert_eq!(choice.path(), Path::new("/srv/audit.sqlite"));
338 assert_eq!(
339 choice.note(),
340 None,
341 "an explicit choice needs no explanation"
342 );
343 }
344
345 #[test]
346 fn a_present_per_user_store_wins_over_the_system_one() {
347 let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
348 let choice = super::choose_audit_store(None, per_user.clone(), true, true);
349 assert_eq!(choice.path(), per_user.as_path());
350 }
351
352 #[test]
355 fn the_system_store_is_used_when_no_per_user_store_exists() {
356 let choice = super::choose_audit_store(
357 None,
358 PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite"),
359 false,
360 true,
361 );
362 assert_eq!(choice.path(), Path::new(super::PRODUCTION_DATABASE_PATH));
363 let note = choice
364 .note()
365 .expect("reading another store must be announced");
366 assert!(
367 note.contains(super::PRODUCTION_DATABASE_PATH),
368 "note names the store: {note}"
369 );
370 }
371
372 #[test]
373 fn a_supplied_system_store_path_is_used_when_the_per_user_store_is_absent() {
374 let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
375 let system = PathBuf::from("/tmp/system/daemon.sqlite");
376 let choice =
377 super::choose_audit_store_with_paths(None, per_user, false, system.clone(), true);
378 assert_eq!(choice, super::AuditStoreChoice::System(system));
379 }
380
381 #[test]
382 fn with_no_store_anywhere_the_per_user_path_is_reported() {
383 let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
384 let choice = super::choose_audit_store(None, per_user.clone(), false, false);
385 assert_eq!(choice.path(), per_user.as_path());
386 assert_eq!(choice.note(), None);
387 }
388
389 #[test]
390 fn permission_denied_counts_as_a_present_store() {
391 assert!(
392 super::path_is_present_result(Ok(())),
393 "a successful stat is present"
394 );
395 assert!(
396 super::path_is_present_result(Err(std::io::Error::from(
397 std::io::ErrorKind::PermissionDenied
398 ))),
399 "EACCES must not be treated as missing"
400 );
401 assert!(
402 !super::path_is_present_result(Err(std::io::Error::from(std::io::ErrorKind::NotFound))),
403 "ENOENT remains missing"
404 );
405 }
406
407 #[test]
411 fn unreadable_system_store_wins_over_a_missing_per_user_store() {
412 let per_user = PathBuf::from("/home/u/.local/state/sysknife/daemon.sqlite");
413 let system_exists = super::path_is_present_result(Err(std::io::Error::from(
414 std::io::ErrorKind::PermissionDenied,
415 )));
416 let choice = super::choose_audit_store(None, per_user, false, system_exists);
417 assert_eq!(
418 choice.path(),
419 Path::new(super::PRODUCTION_DATABASE_PATH),
420 "EACCES on the system store must not fall through to PerUser"
421 );
422 assert!(
423 choice.note().is_some(),
424 "System choice must still announce itself"
425 );
426 }
427
428 #[cfg(unix)]
431 #[test]
432 fn path_behind_mode_000_directory_counts_as_present() {
433 use std::os::unix::fs::PermissionsExt;
434
435 let root = tempfile::tempdir().expect("tempdir");
436 let locked = root.path().join("locked");
437 std::fs::create_dir(&locked).expect("mkdir");
438 let db = locked.join("daemon.sqlite");
439 std::fs::write(&db, b"x").expect("write db");
440 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000))
441 .expect("chmod 000");
442
443 let present = super::path_is_present(&db);
444 let exists_under_eacces = db.exists();
445
446 let _ = std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o700));
448 assert!(
449 present,
450 "metadata EACCES on a 0700/000 store must count as present, not missing"
451 );
452 assert!(
453 !exists_under_eacces,
454 "precondition: Path::exists is false under EACCES (the bug we are fixing)"
455 );
456 }
457
458 #[test]
462 fn the_system_store_constant_matches_the_packaged_unit() {
463 let unit = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
464 .join("../../packaging/sysknife-daemon.service");
465 let contents = std::fs::read_to_string(&unit)
466 .unwrap_or_else(|e| panic!("read {}: {e}", unit.display()));
467 let expected = format!("SYSKNIFE_DATABASE_PATH={}", super::PRODUCTION_DATABASE_PATH);
468 assert!(
469 contents.contains(&expected),
470 "packaged unit must set {expected}; got:\n{contents}"
471 );
472 }
473}