use nix::unistd::{Gid, Uid, User, getuid, setgid, setuid};
use std::path::PathBuf;
use tracing::info;
use vfs::VfsPath;
pub const CONFIG_DIR: &str = "/etc/clawshell";
pub fn default_config_path() -> PathBuf {
PathBuf::from(CONFIG_DIR).join("clawshell.toml")
}
pub(crate) fn physical_root() -> VfsPath {
VfsPath::new(vfs::PhysicalFS::new("/"))
}
fn log_file_vfs(root: &VfsPath) -> Result<VfsPath, Box<dyn std::error::Error>> {
Ok(root.join("var/log/clawshell/clawshell.log")?)
}
pub fn log_file_path() -> PathBuf {
PathBuf::from("/var/log/clawshell/clawshell.log")
}
pub(crate) fn ensure_runtime_dirs_vfs(root: &VfsPath) -> Result<(), Box<dyn std::error::Error>> {
let log_path = log_file_vfs(root)?;
log_path.parent().create_dir_all()?;
Ok(())
}
pub fn ensure_runtime_dirs() -> Result<(), Box<dyn std::error::Error>> {
ensure_runtime_dirs_vfs(&physical_root())
}
pub fn drop_privileges() -> Result<(), Box<dyn std::error::Error>> {
if !getuid().is_root() {
return Ok(());
}
let user = User::from_name("clawshell")?
.ok_or("system user 'clawshell' not found — run `sudo clawshell onboard` first")?;
setgid(Gid::from_raw(user.gid.as_raw())).map_err(|e| format!("setgid({}): {}", user.gid, e))?;
setuid(Uid::from_raw(user.uid.as_raw())).map_err(|e| format!("setuid({}): {}", user.uid, e))?;
info!(
uid = user.uid.as_raw(),
gid = user.gid.as_raw(),
"Dropped privileges to 'clawshell'"
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_file_path() {
let path = log_file_path();
let path_str = path.to_str().unwrap();
assert!(path_str.contains("clawshell.log"));
assert!(
path_str.starts_with("/var/log/"),
"Log path should be under /var/log, got: {}",
path_str
);
}
#[test]
fn test_default_config_path() {
let path = default_config_path();
let path_str = path.to_str().unwrap();
assert_eq!(path_str, "/etc/clawshell/clawshell.toml");
}
#[test]
fn test_ensure_runtime_dirs() {
let root = VfsPath::new(vfs::MemoryFS::new());
ensure_runtime_dirs_vfs(&root).unwrap();
let log_parent = log_file_vfs(&root).unwrap().parent();
assert!(log_parent.exists().unwrap());
}
#[test]
fn test_drop_privileges_no_clawshell_user() {
if getuid().is_root() {
if User::from_name("clawshell").ok().flatten().is_none() {
let result = drop_privileges();
assert!(
result.is_err(),
"Should fail when clawshell user doesn't exist"
);
assert!(
result.unwrap_err().to_string().contains("not found"),
"Error should mention user not found"
);
}
} else {
let result = drop_privileges();
assert!(result.is_ok(), "Should succeed as no-op when not root");
}
}
#[test]
fn test_drop_privileges_as_root() {
if !getuid().is_root() {
eprintln!("Skipping test_drop_privileges_as_root: not running as root");
return;
}
if User::from_name("clawshell").ok().flatten().is_none() {
eprintln!("Skipping test_drop_privileges_as_root: clawshell user not found");
return;
}
let user = User::from_name("clawshell").unwrap().unwrap();
assert!(user.uid.as_raw() > 0, "clawshell should not be UID 0");
}
}