Skip to main content

solid_pod_rs/wac/
resolver.rs

1//! ACL resolver — locates the effective ACL document for a given path.
2//!
3//! `find_effective_acl` walks the storage tree from the resource path
4//! up to the root, returning the first `*.acl` sibling that parses as
5//! JSON-LD or Turtle.
6
7use async_trait::async_trait;
8
9use crate::error::PodError;
10use crate::storage::Storage;
11use crate::wac::document::AclDocument;
12use crate::wac::parse_jsonld_acl;
13use crate::wac::parser::parse_turtle_acl;
14
15/// Resolves the effective ACL document for a resource using the WAC walk-up-the-tree algorithm.
16///
17/// Starting at the resource path, the resolver looks for an `.acl` sidecar at each ancestor
18/// container up to the root. The first parseable ACL document found (JSON-LD or Turtle) is
19/// returned. If no ACL is found at any level, returns `Ok(None)` -- callers must deny access
20/// when no ACL exists (deny-by-default).
21#[async_trait]
22pub trait AclResolver: Send + Sync {
23    /// Locate the nearest ACL document that governs `resource_path`.
24    async fn find_effective_acl(
25        &self,
26        resource_path: &str,
27    ) -> Result<Option<AclDocument>, PodError>;
28}
29
30/// `AclResolver` backed by a [`Storage`] implementation.
31pub struct StorageAclResolver<S: Storage> {
32    storage: std::sync::Arc<S>,
33}
34
35impl<S: Storage> StorageAclResolver<S> {
36    /// Wrap a shared storage handle in a resolver.
37    pub fn new(storage: std::sync::Arc<S>) -> Self {
38        Self { storage }
39    }
40}
41
42#[async_trait]
43impl<S: Storage> AclResolver for StorageAclResolver<S> {
44    /// Walk from `resource_path` toward `/`, returning the first valid `.acl` sidecar found.
45    async fn find_effective_acl(
46        &self,
47        resource_path: &str,
48    ) -> Result<Option<AclDocument>, PodError> {
49        let mut path = resource_path.to_string();
50        loop {
51            let acl_key = if path == "/" {
52                "/.acl".to_string()
53            } else {
54                format!("{}.acl", path.trim_end_matches('/'))
55            };
56            if let Ok((body, meta)) = self.storage.get(&acl_key).await {
57                // JSON-LD first (with bounded parser). A body that
58                // exceeds byte or depth caps returns BadRequest or
59                // PayloadTooLarge and bubbles up so the caller can
60                // reject with 400/413.
61                match parse_jsonld_acl(&body) {
62                    Ok(doc) => return Ok(Some(doc)),
63                    Err(PodError::BadRequest(_)) => {
64                        return Err(PodError::BadRequest(
65                            "ACL document exceeds bounds".into(),
66                        ));
67                    }
68                    Err(PodError::PayloadTooLarge(msg)) => {
69                        return Err(PodError::PayloadTooLarge(msg));
70                    }
71                    Err(_) => {}
72                }
73                let ct = meta.content_type.to_ascii_lowercase();
74                let looks_turtle = ct.starts_with("text/turtle")
75                    || ct.starts_with("application/turtle")
76                    || ct.starts_with("application/x-turtle");
77                let text = std::str::from_utf8(&body).unwrap_or("");
78                if looks_turtle || text.contains("@prefix") || text.contains("acl:Authorization") {
79                    if let Ok(doc) = parse_turtle_acl(text) {
80                        return Ok(Some(doc));
81                    }
82                }
83            }
84            if path == "/" || path.is_empty() {
85                break;
86            }
87            let trimmed = path.trim_end_matches('/');
88            path = match trimmed.rfind('/') {
89                Some(0) => "/".to_string(),
90                Some(pos) => trimmed[..pos].to_string(),
91                None => "/".to_string(),
92            };
93        }
94        Ok(None)
95    }
96}