use async_trait::async_trait;
use crate::error::PodError;
#[cfg(feature = "tokio-runtime")]
use crate::storage::Storage;
use crate::wac::document::AclDocument;
use crate::wac::parse_jsonld_acl;
use crate::wac::parser::parse_turtle_acl;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InvalidPolicyReason {
Malformed(String),
TooLarge(String),
TooDeep(String),
NotUtf8,
}
impl std::fmt::Display for InvalidPolicyReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InvalidPolicyReason::Malformed(m) => write!(f, "malformed ACL document: {m}"),
InvalidPolicyReason::TooLarge(m) => write!(f, "ACL document too large: {m}"),
InvalidPolicyReason::TooDeep(m) => write!(f, "ACL document too deeply nested: {m}"),
InvalidPolicyReason::NotUtf8 => write!(f, "ACL document is not valid UTF-8"),
}
}
}
#[derive(Debug, Clone)]
pub enum PolicyOutcome {
Found(AclDocument),
Missing,
Invalid {
policy_path: String,
reason: InvalidPolicyReason,
},
Unavailable {
policy_path: String,
detail: String,
},
}
impl PolicyOutcome {
#[must_use]
pub fn document(&self) -> Option<&AclDocument> {
match self {
PolicyOutcome::Found(doc) => Some(doc),
_ => None,
}
}
#[must_use]
pub fn is_failure(&self) -> bool {
matches!(
self,
PolicyOutcome::Invalid { .. } | PolicyOutcome::Unavailable { .. }
)
}
#[must_use]
pub fn may_inherit(&self) -> bool {
matches!(self, PolicyOutcome::Missing)
}
pub fn into_result(self) -> Result<Option<AclDocument>, PodError> {
match self {
PolicyOutcome::Found(doc) => Ok(Some(doc)),
PolicyOutcome::Missing => Ok(None),
PolicyOutcome::Invalid {
policy_path,
reason,
} => Err(match reason {
InvalidPolicyReason::TooLarge(m) => PodError::PayloadTooLarge(m),
InvalidPolicyReason::TooDeep(m) => PodError::BadRequest(m),
other => PodError::AclParse(format!("{policy_path}: {other}")),
}),
PolicyOutcome::Unavailable {
policy_path,
detail,
} => Err(PodError::Backend(format!(
"ACL read failed at {policy_path}: {detail}"
))),
}
}
pub fn from_legacy(result: Result<Option<AclDocument>, PodError>, resource_path: &str) -> Self {
match result {
Ok(Some(doc)) => PolicyOutcome::Found(doc),
Ok(None) => PolicyOutcome::Missing,
Err(PodError::PayloadTooLarge(m)) => PolicyOutcome::Invalid {
policy_path: resource_path.to_string(),
reason: InvalidPolicyReason::TooLarge(m),
},
Err(PodError::BadRequest(m)) => PolicyOutcome::Invalid {
policy_path: resource_path.to_string(),
reason: InvalidPolicyReason::TooDeep(m),
},
Err(PodError::AclParse(m)) => PolicyOutcome::Invalid {
policy_path: resource_path.to_string(),
reason: InvalidPolicyReason::Malformed(m),
},
Err(e) => PolicyOutcome::Unavailable {
policy_path: resource_path.to_string(),
detail: e.to_string(),
},
}
}
}
#[derive(Debug)]
pub enum PolicyRead<'a> {
Absent,
Present {
body: &'a [u8],
content_type: &'a str,
},
Failed(String),
}
#[derive(Debug)]
pub enum PolicyStep {
Ascend,
Settled(PolicyOutcome),
}
pub fn classify_policy_read(
policy_path: &str,
read: PolicyRead<'_>,
inherited: bool,
) -> PolicyStep {
let (body, content_type) = match read {
PolicyRead::Absent => return PolicyStep::Ascend,
PolicyRead::Failed(detail) => {
return PolicyStep::Settled(PolicyOutcome::Unavailable {
policy_path: policy_path.to_string(),
detail,
})
}
PolicyRead::Present { body, content_type } => (body, content_type),
};
let invalid = |reason: InvalidPolicyReason| {
PolicyStep::Settled(PolicyOutcome::Invalid {
policy_path: policy_path.to_string(),
reason,
})
};
match parse_jsonld_acl(body) {
Ok(mut doc) => {
doc.inherited = inherited;
return PolicyStep::Settled(PolicyOutcome::Found(doc));
}
Err(PodError::PayloadTooLarge(m)) => return invalid(InvalidPolicyReason::TooLarge(m)),
Err(PodError::BadRequest(m)) => return invalid(InvalidPolicyReason::TooDeep(m)),
Err(_) => {}
}
let Ok(text) = std::str::from_utf8(body) else {
return invalid(InvalidPolicyReason::NotUtf8);
};
let ct = content_type.to_ascii_lowercase();
let looks_turtle = ct.starts_with("text/turtle")
|| ct.starts_with("application/turtle")
|| ct.starts_with("application/x-turtle");
if !looks_turtle && !text.contains("@prefix") && !text.contains("acl:Authorization") {
return invalid(InvalidPolicyReason::Malformed(
"body is neither JSON-LD nor Turtle".into(),
));
}
match parse_turtle_acl(text) {
Ok(mut doc) => {
doc.inherited = inherited;
PolicyStep::Settled(PolicyOutcome::Found(doc))
}
Err(PodError::PayloadTooLarge(m)) => invalid(InvalidPolicyReason::TooLarge(m)),
Err(e) => invalid(InvalidPolicyReason::Malformed(e.to_string())),
}
}
#[must_use]
pub fn acl_sidecar_key(path: &str) -> String {
if path == "/" {
"/.acl".to_string()
} else {
format!("{}.acl", path.trim_end_matches('/'))
}
}
#[must_use]
pub fn parent_container(path: &str) -> Option<String> {
if path == "/" || path.is_empty() {
return None;
}
let trimmed = path.trim_end_matches('/');
Some(match trimmed.rfind('/') {
Some(0) => "/".to_string(),
Some(pos) => trimmed[..pos].to_string(),
None => "/".to_string(),
})
}
#[cfg(feature = "tokio-runtime")]
pub async fn resolve_policy_from_storage(
storage: &dyn Storage,
resource_path: &str,
) -> PolicyOutcome {
let mut path = resource_path.to_string();
let mut inherited = false;
loop {
let acl_key = acl_sidecar_key(&path);
let got = storage.get(&acl_key).await;
let read = match &got {
Ok((body, meta)) => PolicyRead::Present {
body,
content_type: &meta.content_type,
},
Err(PodError::NotFound(_)) => PolicyRead::Absent,
Err(e) => PolicyRead::Failed(e.to_string()),
};
match classify_policy_read(&acl_key, read, inherited) {
PolicyStep::Settled(outcome) => return outcome,
PolicyStep::Ascend => {}
}
match parent_container(&path) {
Some(parent) => {
inherited = true;
path = parent;
}
None => return PolicyOutcome::Missing,
}
}
}
#[async_trait]
pub trait AclResolver: Send + Sync {
async fn find_effective_acl(
&self,
resource_path: &str,
) -> Result<Option<AclDocument>, PodError>;
async fn resolve_policy(&self, resource_path: &str) -> PolicyOutcome {
PolicyOutcome::from_legacy(self.find_effective_acl(resource_path).await, resource_path)
}
}
#[cfg(feature = "tokio-runtime")]
pub struct StorageAclResolver<S: Storage> {
storage: std::sync::Arc<S>,
}
#[cfg(feature = "tokio-runtime")]
impl<S: Storage> StorageAclResolver<S> {
pub fn new(storage: std::sync::Arc<S>) -> Self {
Self { storage }
}
}
#[cfg(feature = "tokio-runtime")]
#[async_trait]
impl<S: Storage> AclResolver for StorageAclResolver<S> {
async fn find_effective_acl(
&self,
resource_path: &str,
) -> Result<Option<AclDocument>, PodError> {
self.resolve_policy(resource_path).await.into_result()
}
async fn resolve_policy(&self, resource_path: &str) -> PolicyOutcome {
resolve_policy_from_storage(&*self.storage, resource_path).await
}
}