use crate::contracts::kinds::ZebFsAclContract;
use crate::contracts::{ContractMetadata, read_optional_contract, write_contract};
use crate::zebfs::acl::{ACL_MANIFEST_PATH, ZebFsAclManifest};
use crate::zebfs::{LocalZebFs, ZebFsAccess, ZebFsAclScope, ZebFsError};
pub fn read(zebfs: &LocalZebFs) -> Result<ZebFsAclManifest, ZebFsError> {
let path = zebfs.root().join(ACL_MANIFEST_PATH);
match read_optional_contract::<ZebFsAclContract>(&path) {
Ok(document) => Ok(document.map(|value| value.spec).unwrap_or_default()),
Err(err) => match read_pre_contract_manifest(&path) {
Some(manifest) => Ok(manifest),
None => Err(ZebFsError::new(
"ZEBFS_ACL_READ",
format!("{} ({})", err, err.category()),
)),
},
}
}
fn read_pre_contract_manifest(path: &std::path::Path) -> Option<ZebFsAclManifest> {
let bytes = std::fs::read(path).ok()?;
let mut value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
let object = value.as_object_mut()?;
if object.contains_key("apiVersion") || object.contains_key("kind") {
return None;
}
object.remove("version");
serde_json::from_value(value).ok()
}
pub fn write(zebfs: &LocalZebFs, manifest: &ZebFsAclManifest) -> Result<(), ZebFsError> {
let path = zebfs.root().join(ACL_MANIFEST_PATH);
write_contract::<ZebFsAclContract>(
&path,
ContractMetadata::named("access-control"),
manifest.clone(),
)
.map_err(|err| ZebFsError::new("ZEBFS_ACL_WRITE", format!("{} ({})", err, err.category())))
}
pub fn effective_access(zebfs: &LocalZebFs, path: &str) -> Result<ZebFsAccess, ZebFsError> {
read(zebfs)?.effective_access(path)
}
pub fn is_public_read(zebfs: &LocalZebFs, path: &str) -> Result<bool, ZebFsError> {
Ok(effective_access(zebfs, path)? == ZebFsAccess::PublicRead)
}
pub fn set_access(
zebfs: &LocalZebFs,
path: &str,
access: ZebFsAccess,
scope: ZebFsAclScope,
) -> Result<String, ZebFsError> {
let mut manifest = read(zebfs)?;
let normalized = manifest.set_rule(path, access, scope)?;
write(zebfs, &manifest)?;
Ok(normalized)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn acl_roundtrips_and_rejects_future_documents() {
let root = tempfile::tempdir().unwrap();
let zebfs = LocalZebFs::new(root.path().to_path_buf());
set_access(
&zebfs,
"public",
ZebFsAccess::PublicRead,
ZebFsAclScope::Prefix,
)
.unwrap();
assert!(is_public_read(&zebfs, "public/file.txt").unwrap());
let acl_path = root.path().join(ACL_MANIFEST_PATH);
std::fs::write(
&acl_path,
br#"{"apiVersion":"zebflow.com/v9","kind":"ZebFsAcl","metadata":{"name":"a"},"spec":{"rules":{}}}"#,
)
.unwrap();
assert_eq!(
effective_access(&zebfs, "public/file.txt")
.unwrap_err()
.code,
"ZEBFS_ACL_READ"
);
}
#[test]
fn pre_contract_acl_is_read_and_rewritten_enveloped_on_the_next_change() {
let root = tempfile::tempdir().unwrap();
let zebfs = LocalZebFs::new(root.path().to_path_buf());
let acl_path = root.path().join(ACL_MANIFEST_PATH);
std::fs::create_dir_all(acl_path.parent().unwrap()).unwrap();
std::fs::write(
&acl_path,
br#"{"version":1,"rules":{"shared":{"access":"public_read","scope":"prefix","updated_at":1782287499}}}"#,
)
.unwrap();
assert!(is_public_read(&zebfs, "shared/photo.jpg").unwrap());
assert!(!is_public_read(&zebfs, "private/photo.jpg").unwrap());
set_access(
&zebfs,
"also-shared",
ZebFsAccess::PublicRead,
ZebFsAclScope::Prefix,
)
.unwrap();
let rewritten: serde_json::Value =
serde_json::from_slice(&std::fs::read(&acl_path).unwrap()).unwrap();
assert_eq!(rewritten["apiVersion"], "zebflow.com/v1");
assert_eq!(rewritten["kind"], "ZebFsAcl");
assert!(rewritten["spec"]["rules"]["shared"].is_object());
assert!(is_public_read(&zebfs, "shared/photo.jpg").unwrap());
assert!(is_public_read(&zebfs, "also-shared/photo.jpg").unwrap());
}
#[test]
fn pre_contract_allowance_does_not_accept_arbitrary_documents() {
let root = tempfile::tempdir().unwrap();
let zebfs = LocalZebFs::new(root.path().to_path_buf());
let acl_path = root.path().join(ACL_MANIFEST_PATH);
std::fs::create_dir_all(acl_path.parent().unwrap()).unwrap();
std::fs::write(&acl_path, br#"{"rules":{"a":{"access":"nope"}}}"#).unwrap();
assert_eq!(read(&zebfs).unwrap_err().code, "ZEBFS_ACL_READ");
std::fs::write(&acl_path, b"not json at all").unwrap();
assert_eq!(read(&zebfs).unwrap_err().code, "ZEBFS_ACL_READ");
std::fs::write(
&acl_path,
br#"{"apiVersion":"zebflow.com/v1","kind":"ZebFsAcl","metadata":{"name":"a"},"spec":{"rules":{}},"extra":1}"#,
)
.unwrap();
assert_eq!(read(&zebfs).unwrap_err().code, "ZEBFS_ACL_READ");
}
}