use crate::defaults::CREDENTIAL_CONFIG_FULL_DENY;
use std::path::Path;
pub fn is_fully_denied(path: &Path) -> bool {
is_fully_denied_with_home(path, resolve_home_dir().as_deref())
}
fn resolve_home_dir() -> Option<String> {
std::env::var("HOME")
.ok()
.or_else(|| std::env::var("USERPROFILE").ok())
.filter(|s| !s.is_empty())
}
pub(crate) fn is_fully_denied_with_home(path: &Path, home: Option<&str>) -> bool {
if let Some(home) = home {
let home_path = Path::new(home);
if CREDENTIAL_CONFIG_FULL_DENY
.iter()
.any(|rel| path.starts_with(home_path.join(".config").join(rel)))
{
return true;
}
}
let components: Vec<&std::ffi::OsStr> = path
.components()
.filter_map(|c| match c {
std::path::Component::Normal(s) => Some(s),
_ => None,
})
.collect();
CREDENTIAL_CONFIG_FULL_DENY.iter().any(|rel| {
let mut needle: Vec<&str> = vec![".config"];
needle.extend(rel.split('/'));
components.windows(needle.len()).any(|window| {
window
.iter()
.zip(needle.iter())
.all(|(comp, want)| comp.to_str() == Some(*want))
})
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fully_denied_blocks_koda_db() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".into());
let koda_db = Path::new(&home).join(".config/koda/db");
assert!(
is_fully_denied(&koda_db),
"~/.config/koda/db must be fully denied"
);
assert!(is_fully_denied(&koda_db.join("koda.db")));
}
#[test]
fn fully_denied_allows_credential_dirs() {
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".into());
let home = Path::new(&home);
for rel in &[".ssh", ".aws", ".gnupg", ".config/gh", ".config/gcloud"] {
assert!(
!is_fully_denied(&home.join(rel)),
"{rel} must NOT be fully denied (reads allowed)"
);
}
}
#[test]
fn fully_denied_allows_project_and_system_paths() {
assert!(!is_fully_denied(Path::new(
"/home/user/project/src/main.rs"
)));
assert!(!is_fully_denied(Path::new("/tmp/scratch.txt")));
assert!(!is_fully_denied(Path::new("/etc/hosts")));
}
#[test]
fn fully_denied_blocks_koda_db_when_home_is_none() {
for path in [
"/root/.config/koda/db",
"/root/.config/koda/db/koda.db",
"/home/runner/.config/koda/db/koda.db",
"/proc/self/root/.config/koda/db/koda.db",
".config/koda/db/koda.db", ] {
assert!(
is_fully_denied_with_home(Path::new(path), None),
"{path:?} must be denied even with HOME=None"
);
}
}
#[test]
fn fully_denied_no_home_still_allows_normal_paths() {
for path in [
"/home/user/project/src/main.rs",
"/tmp/scratch.txt",
"/etc/hosts",
"/home/user/.config/git/config", "/home/user/.config/koda/agents/foo.json", "/var/lib/koda-db-backups/2025.tar", ] {
assert!(
!is_fully_denied_with_home(Path::new(path), None),
"{path:?} must NOT be denied (no koda secrets)"
);
}
}
#[test]
fn fully_denied_uses_home_when_provided() {
let custom_home = "/srv/koda-runner";
assert!(is_fully_denied_with_home(
Path::new("/srv/koda-runner/.config/koda/db/koda.db"),
Some(custom_home),
));
assert!(!is_fully_denied_with_home(
Path::new("/srv/koda-runner/notes.md"),
Some(custom_home),
));
}
#[test]
fn resolve_home_dir_treats_empty_as_unset() {
assert!(is_fully_denied_with_home(
Path::new("/anywhere/.config/koda/db/koda.db"),
None, ));
}
}