use std::path::{Path, PathBuf};
use ikigai_core::Capability;
pub fn tenant_root(file_root: &Path, segment: &str) -> PathBuf {
if segment.is_empty() {
file_root.to_path_buf()
} else {
file_root.join(segment)
}
}
pub fn root_fs_scopes(capability: &Capability, tenant_root: &Path) -> Capability {
let Some(scopes) = capability.scopes() else {
return Capability::root();
};
Capability::scoped(
scopes
.iter()
.map(|scope| root_fs_scope(scope, tenant_root))
.collect::<Vec<_>>(),
)
}
fn root_fs_scope(scope: &str, tenant_root: &Path) -> String {
let Some((action, deny, path)) = split_fs_scope(scope) else {
return scope.to_string();
};
if path == "*" || Path::new(path).is_absolute() {
return scope.to_string();
}
let rooted = if path.is_empty() || path == "." {
tenant_root.to_path_buf()
} else {
tenant_root.join(path)
};
let dash = if deny { "-" } else { "" };
format!("urn:cap:fs:{action}:{dash}{}", rooted.display())
}
pub fn unaddressable_fs_scopes(scopes: &[String], file_root: &Path) -> Vec<String> {
scopes
.iter()
.filter(|scope| {
let Some((_, _, path)) = split_fs_scope(scope) else {
return false;
};
let path = Path::new(path);
path != Path::new("*") && path.is_absolute() && !path.starts_with(file_root)
})
.cloned()
.collect()
}
fn split_fs_scope(scope: &str) -> Option<(&str, bool, &str)> {
let rest = scope.strip_prefix("urn:cap:fs:")?;
let (action, path) = rest.split_once(':')?;
if !matches!(action, "read" | "write" | "delete") {
return None;
}
match path.strip_prefix('-') {
Some(path) => Some((action, true, path)),
None => Some((action, false, path)),
}
}
#[cfg(test)]
mod tests {
use super::*;
const ROOT: &str = "/home/u/.ikigai/workspace";
const SEG: &str = "0123456789abcdef";
fn tenant() -> PathBuf {
tenant_root(Path::new(ROOT), SEG)
}
fn scopes_of(capability: &Capability) -> Vec<String> {
capability.scopes().unwrap().iter().cloned().collect()
}
#[test]
fn a_relative_scope_resolves_to_the_path_the_tenants_iri_reaches() {
let capability = Capability::scoped(["urn:cap:fs:read:notes".to_string()]);
assert_eq!(
scopes_of(&root_fs_scopes(&capability, &tenant())),
vec![format!("urn:cap:fs:read:{ROOT}/{SEG}/notes")]
);
}
#[test]
fn a_relative_deny_rule_is_rooted_like_the_allow_it_narrows() {
let capability = Capability::scoped([
"urn:cap:fs:read:.".to_string(),
"urn:cap:fs:read:-secret".to_string(),
]);
let mut rooted = scopes_of(&root_fs_scopes(&capability, &tenant()));
rooted.sort();
assert_eq!(
rooted,
vec![
format!("urn:cap:fs:read:-{ROOT}/{SEG}/secret"),
format!("urn:cap:fs:read:{ROOT}/{SEG}"),
]
);
}
#[test]
fn an_absolute_scope_is_never_silently_reinterpreted() {
let capability = Capability::scoped(["urn:cap:fs:read:/etc".to_string()]);
assert_eq!(
scopes_of(&root_fs_scopes(&capability, &tenant())),
vec!["urn:cap:fs:read:/etc".to_string()]
);
}
#[test]
fn the_wildcard_declaration_form_is_left_alone() {
let capability = Capability::scoped(["urn:cap:fs:read:*".to_string()]);
assert_eq!(
scopes_of(&root_fs_scopes(&capability, &tenant())),
vec!["urn:cap:fs:read:*".to_string()]
);
assert!(
unaddressable_fs_scopes(&["urn:cap:fs:read:*".to_string()], Path::new(ROOT)).is_empty()
);
}
#[test]
fn non_file_scopes_and_root_pass_through() {
let capability = Capability::scoped([
"urn:cap:personal:contacts:read".to_string(),
"urn:cap:fs:list:notes".to_string(),
]);
let mut rooted = scopes_of(&root_fs_scopes(&capability, &tenant()));
rooted.sort();
assert_eq!(
rooted,
vec![
"urn:cap:fs:list:notes".to_string(),
"urn:cap:personal:contacts:read".to_string(),
]
);
assert!(root_fs_scopes(&Capability::root(), &tenant()).is_root());
}
#[test]
fn an_absolute_scope_outside_the_jail_is_unaddressable() {
let scopes = [
"urn:cap:fs:read:/Users/brian/notes".to_string(),
"urn:cap:fs:read:-/Users/brian/secrets".to_string(),
"urn:cap:personal:contacts:read".to_string(),
];
assert_eq!(
unaddressable_fs_scopes(&scopes, Path::new(ROOT)),
vec![
"urn:cap:fs:read:/Users/brian/notes".to_string(),
"urn:cap:fs:read:-/Users/brian/secrets".to_string(),
]
);
}
#[test]
fn a_scope_inside_the_jail_is_addressable() {
let scopes = [
format!("urn:cap:fs:read:{ROOT}"),
format!("urn:cap:fs:write:{ROOT}/{SEG}/notes"),
"urn:cap:fs:read:notes".to_string(),
];
assert!(unaddressable_fs_scopes(&scopes, Path::new(ROOT)).is_empty());
}
#[test]
fn a_sibling_with_a_shared_name_prefix_is_outside_the_jail() {
let scopes = [format!("urn:cap:fs:read:{ROOT}-backup")];
assert_eq!(
unaddressable_fs_scopes(&scopes, Path::new(ROOT)),
vec![format!("urn:cap:fs:read:{ROOT}-backup")]
);
}
use ikigai_core::{Bindings, Endpoint, Error, Invocation, Iri, Request, Verb};
fn localized(segment: &str, rel: &str) -> String {
format!("{segment}/{rel}")
}
fn source(root: &Path, path: &str, capability: &Capability) -> Result<(), Error> {
let endpoint = ikigai_fs::FileEndpoint::new(root);
let request = Request::new(Verb::Source, Iri::parse("urn:file:x").unwrap());
let mut bindings = Bindings::new();
bindings.insert("path", path);
let invocation = Invocation::detached(&request, &bindings, capability);
futures::executor::block_on(endpoint.invoke(&invocation)).map(|_| ())
}
fn jail() -> PathBuf {
let root = std::env::temp_dir().join(format!(
"ikigai-tenant-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
for dir in ["notes", "other"] {
std::fs::create_dir_all(root.join(SEG).join(dir)).unwrap();
}
std::fs::write(root.join(SEG).join("notes/todo.txt"), b"todo").unwrap();
std::fs::write(root.join(SEG).join("other/secret.txt"), b"secret").unwrap();
root
}
#[test]
fn a_rooted_relative_grant_authorizes_exactly_the_directory_it_names() {
let root = jail();
let granted = root_fs_scopes(
&Capability::scoped(["urn:cap:fs:read:notes".to_string()]),
&tenant_root(&root, SEG),
);
source(&root, &localized(SEG, "notes/todo.txt"), &granted)
.expect("the grant names the directory this IRI resolves into");
assert!(
matches!(
source(&root, &localized(SEG, "other/secret.txt"), &granted),
Err(Error::Denied(_))
),
"a grant for `notes` must not reach a sibling directory"
);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn the_same_grant_unrooted_silently_authorizes_nothing() {
let root = jail();
let unrooted = Capability::scoped(["urn:cap:fs:read:notes".to_string()]);
assert!(
matches!(
source(&root, &localized(SEG, "notes/todo.txt"), &unrooted),
Err(Error::Denied(_))
),
"an unrooted relative scope must not have started working by accident"
);
std::fs::remove_dir_all(&root).ok();
}
#[test]
fn an_absolute_grant_outside_the_jail_authorizes_nothing() {
let root = jail();
let outside = Capability::scoped(["urn:cap:fs:read:/Users/brian/notes".to_string()]);
assert!(
matches!(
source(&root, &localized(SEG, "notes/todo.txt"), &outside),
Err(Error::Denied(_))
),
"a path outside the jail cannot authorize anything inside it"
);
assert_eq!(
unaddressable_fs_scopes(&["urn:cap:fs:read:/Users/brian/notes".to_string()], &root)
.len(),
1,
"and the startup check must be what catches it"
);
std::fs::remove_dir_all(&root).ok();
}
}