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;
10// `Storage` lives behind `tokio-runtime`; the storage-backed resolver
11// impl below is gated to match. The `AclResolver` trait is pure and
12// remains available under `core` so wasm32 consumers can implement
13// their own KV-backed resolver against the same contract.
14#[cfg(feature = "tokio-runtime")]
15use crate::storage::Storage;
16use crate::wac::document::AclDocument;
17#[cfg(feature = "tokio-runtime")]
18use crate::wac::parse_jsonld_acl;
19#[cfg(feature = "tokio-runtime")]
20use crate::wac::parser::parse_turtle_acl;
21
22/// Resolves the effective ACL document for a resource using the WAC walk-up-the-tree algorithm.
23///
24/// Starting at the resource path, the resolver looks for an `.acl` sidecar at each ancestor
25/// container up to the root. The first parseable ACL document found (JSON-LD or Turtle) is
26/// returned. If no ACL is found at any level, returns `Ok(None)` -- callers must deny access
27/// when no ACL exists (deny-by-default).
28#[async_trait]
29pub trait AclResolver: Send + Sync {
30    /// Locate the nearest ACL document that governs `resource_path`.
31    async fn find_effective_acl(
32        &self,
33        resource_path: &str,
34    ) -> Result<Option<AclDocument>, PodError>;
35}
36
37/// `AclResolver` backed by a [`Storage`] implementation.
38#[cfg(feature = "tokio-runtime")]
39pub struct StorageAclResolver<S: Storage> {
40    storage: std::sync::Arc<S>,
41}
42
43#[cfg(feature = "tokio-runtime")]
44impl<S: Storage> StorageAclResolver<S> {
45    /// Wrap a shared storage handle in a resolver.
46    pub fn new(storage: std::sync::Arc<S>) -> Self {
47        Self { storage }
48    }
49}
50
51#[cfg(feature = "tokio-runtime")]
52#[async_trait]
53impl<S: Storage> AclResolver for StorageAclResolver<S> {
54    /// Walk from `resource_path` toward `/`, returning the first valid `.acl` sidecar found.
55    async fn find_effective_acl(
56        &self,
57        resource_path: &str,
58    ) -> Result<Option<AclDocument>, PodError> {
59        let mut path = resource_path.to_string();
60        // The first iteration probes the resource's OWN `.acl` sidecar
61        // (a direct ACL); every later iteration walks up to an ANCESTOR
62        // container, whose ACL is INHERITED. The evaluator must treat the
63        // two differently โ€” inherited ACLs honour only `acl:default`
64        // (WAC ยง4.2), so tag the document accordingly (P2).
65        let mut inherited = false;
66        loop {
67            let acl_key = if path == "/" {
68                "/.acl".to_string()
69            } else {
70                format!("{}.acl", path.trim_end_matches('/'))
71            };
72            if let Ok((body, meta)) = self.storage.get(&acl_key).await {
73                // JSON-LD first (with bounded parser). A body that
74                // exceeds byte or depth caps returns BadRequest or
75                // PayloadTooLarge and bubbles up so the caller can
76                // reject with 400/413.
77                match parse_jsonld_acl(&body) {
78                    Ok(mut doc) => {
79                        doc.inherited = inherited;
80                        return Ok(Some(doc));
81                    }
82                    Err(PodError::BadRequest(_)) => {
83                        return Err(PodError::BadRequest("ACL document exceeds bounds".into()));
84                    }
85                    Err(PodError::PayloadTooLarge(msg)) => {
86                        return Err(PodError::PayloadTooLarge(msg));
87                    }
88                    Err(_) => {}
89                }
90                let ct = meta.content_type.to_ascii_lowercase();
91                let looks_turtle = ct.starts_with("text/turtle")
92                    || ct.starts_with("application/turtle")
93                    || ct.starts_with("application/x-turtle");
94                let text = std::str::from_utf8(&body).unwrap_or("");
95                if looks_turtle || text.contains("@prefix") || text.contains("acl:Authorization") {
96                    if let Ok(mut doc) = parse_turtle_acl(text) {
97                        doc.inherited = inherited;
98                        return Ok(Some(doc));
99                    }
100                }
101            }
102            if path == "/" || path.is_empty() {
103                break;
104            }
105            // Every subsequent ACL is resolved from an ancestor.
106            inherited = true;
107            let trimmed = path.trim_end_matches('/');
108            path = match trimmed.rfind('/') {
109                Some(0) => "/".to_string(),
110                Some(pos) => trimmed[..pos].to_string(),
111                None => "/".to_string(),
112            };
113        }
114        Ok(None)
115    }
116}