use std::fs::{self, DirBuilder, Permissions};
use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt, PermissionsExt};
use std::path::{Component, Path, PathBuf};
use crate::error::{Error, Result};
use crate::exec::{self, Tools};
use crate::identity::MachineIdentity;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Freshness {
Renewed,
Minted,
}
#[derive(Debug, Clone)]
pub struct ArmorCache {
path: PathBuf,
}
impl ArmorCache {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn cache_name(&self) -> String {
format!("FILE:{}", self.path.display())
}
pub fn is_valid(&self, tools: &Tools) -> bool {
exec::succeeds(&tools.klist, ["-s", &self.cache_name()])
}
pub fn renew(&self, tools: &Tools, identity: &MachineIdentity) -> Result<()> {
exec::run_ok(
&tools.kinit,
["-R", "-c", &self.cache_name(), "--", &identity.principal],
)
.map(drop)
}
pub fn mint(&self, tools: &Tools, identity: &MachineIdentity) -> Result<()> {
exec::run_ok(
&tools.kinit,
[
"-k".as_ref(),
"-t".as_ref(),
identity.keytab.as_os_str(),
"-c".as_ref(),
self.cache_name().as_ref(),
"--".as_ref(), identity.principal.as_ref(),
],
)
.map(drop)
}
pub fn ensure(&self, tools: &Tools, identity: &MachineIdentity) -> Result<Freshness> {
if self.renew(tools, identity).is_ok() && self.is_valid(tools) {
return Ok(Freshness::Renewed);
}
self.mint(tools, identity)?;
if self.is_valid(tools) {
Ok(Freshness::Minted)
} else {
Err(Error::CacheStillInvalid(self.path.clone()))
}
}
pub fn set_access(&self, gid: u32) -> Result<()> {
let file = fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW)
.open(&self.path)
.map_err(|e| {
if e.raw_os_error() == Some(libc::ELOOP) {
Error::RefusingSymlink(self.path.clone())
} else {
e.into()
}
})?;
std::os::unix::fs::fchown(&file, None, Some(gid))?;
file.set_permissions(Permissions::from_mode(0o640))?;
Ok(())
}
pub fn klist_text(&self, tools: &Tools) -> Result<String> {
exec::run_ok(&tools.klist, [self.cache_name()])
}
pub fn destroy(&self, tools: &Tools) {
let _ = exec::run(&tools.kdestroy, ["-c", &self.cache_name()]);
let _ = fs::remove_file(&self.path);
}
}
pub fn prepare_dir(dir: &Path, gid: Option<u32>) -> Result<()> {
if !dir.is_absolute()
|| dir.components().any(|c| c == Component::ParentDir)
|| is_shared_dir(dir)
{
return Err(Error::RefusingSharedDir(dir.to_path_buf()));
}
match fs::symlink_metadata(dir) {
Ok(meta) if meta.file_type().is_symlink() => {
return Err(Error::RefusingSymlink(dir.to_path_buf()));
}
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
DirBuilder::new().recursive(true).mode(0o750).create(dir)?;
}
Err(e) => return Err(e.into()),
}
let real = fs::canonicalize(dir)?;
if is_shared_dir(&real) {
return Err(Error::RefusingSharedDir(dir.to_path_buf()));
}
if let Some(gid) = gid {
std::os::unix::fs::chown(&real, None, Some(gid))?;
}
fs::set_permissions(&real, Permissions::from_mode(0o750))?;
Ok(())
}
fn is_shared_dir(canonical: &Path) -> bool {
const SHARED: &[&str] = &[
"/",
"/run",
"/var/run",
"/run/user",
"/tmp",
"/var/tmp",
"/var",
"/etc",
"/home",
"/root",
"/usr",
"/usr/local",
"/usr/local/bin",
"/var/usrlocal",
"/var/lib",
"/opt",
"/srv",
"/boot",
"/dev",
"/dev/shm",
];
SHARED.iter().any(|s| Path::new(s) == canonical)
}
pub fn lookup_gid(tools: &Tools, group: &str) -> Result<u32> {
let line = exec::run(&tools.getent, ["--", "group", group])?;
if !line.status.success() {
return Err(Error::GroupNotFound(group.to_string()));
}
parse_getent_gid(&String::from_utf8_lossy(&line.stdout))
.ok_or_else(|| Error::GroupNotFound(group.to_string()))
}
pub(crate) fn parse_getent_gid(line: &str) -> Option<u32> {
line.trim().split(':').nth(2)?.parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn getent_gid_parses() {
assert_eq!(parse_getent_gid("machine-krb:x:973:dylan\n"), Some(973));
assert_eq!(parse_getent_gid("wheel:x:10:"), Some(10));
assert_eq!(parse_getent_gid(""), None);
assert_eq!(parse_getent_gid("garbage"), None);
assert_eq!(parse_getent_gid("a:b"), None);
}
#[test]
fn prepare_dir_refuses_shared_dirs() {
for d in ["/run", "/tmp", "/", "/var", "/etc", "/usr/local"] {
assert!(matches!(
prepare_dir(Path::new(d), None),
Err(Error::RefusingSharedDir(_))
));
}
}
#[test]
fn prepare_dir_refuses_dotdot_and_relative() {
for d in [
"/run/machine-krb/..",
"/run/../run",
"run/machine-krb",
"../x",
] {
assert!(matches!(
prepare_dir(Path::new(d), None),
Err(Error::RefusingSharedDir(_))
));
}
}
#[test]
fn shared_dir_list_hits_canonical_forms() {
for d in ["/var/usrlocal", "/run/user", "/var/lib", "/opt"] {
assert!(is_shared_dir(Path::new(d)));
}
assert!(!is_shared_dir(Path::new("/run/machine-krb")));
}
#[test]
fn cache_name_is_file_prefixed() {
let c = ArmorCache::new("/run/machine-krb/armor.ccache");
assert_eq!(c.cache_name(), "FILE:/run/machine-krb/armor.ccache");
}
}