use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapabilityDenied {
pub store: String,
pub required: String,
pub held: Vec<String>,
}
impl fmt::Display for CapabilityDenied {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"access to axonstore `{}` denied: it requires capability \
`{}`, which the caller does not hold (held: {:?})",
self.store, self.required, self.held
)
}
}
impl std::error::Error for CapabilityDenied {}
pub fn check_store_capability(
store_name: &str,
required: &str,
held: &[String],
) -> Result<(), CapabilityDenied> {
if required.is_empty() || held.iter().any(|h| h == required) {
Ok(())
} else {
Err(CapabilityDenied {
store: store_name.to_string(),
required: required.to_string(),
held: held.to_vec(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn slugs(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
#[test]
fn ungated_store_is_always_allowed() {
assert!(check_store_capability("cache", "", &[]).is_ok());
assert!(check_store_capability("cache", "", &slugs(&["x"])).is_ok());
}
#[test]
fn held_capability_allows_access() {
let held = slugs(&["tenant.read", "audit.write"]);
assert!(check_store_capability("tenants", "tenant.read", &held).is_ok());
}
#[test]
fn missing_capability_is_denied() {
let held = slugs(&["audit.write"]);
match check_store_capability("tenants", "tenant.read", &held) {
Err(denied) => {
assert_eq!(denied.store, "tenants");
assert_eq!(denied.required, "tenant.read");
assert_eq!(denied.held, held);
}
Ok(()) => panic!("expected a capability denial"),
}
}
#[test]
fn empty_held_set_denies_a_gated_store() {
assert!(check_store_capability("tenants", "tenant.read", &[]).is_err());
}
#[test]
fn capability_match_is_exact_not_prefix() {
let held = slugs(&["tenant"]);
assert!(check_store_capability("s", "tenant.read", &held).is_err());
let held2 = slugs(&["tenant.read"]);
assert!(check_store_capability("s", "tenant", &held2).is_err());
}
#[test]
fn capability_denied_display_is_informative() {
let denied = CapabilityDenied {
store: "tenants".into(),
required: "tenant.read".into(),
held: slugs(&["audit.write"]),
};
let msg = denied.to_string();
assert!(msg.contains("tenants"));
assert!(msg.contains("tenant.read"));
assert!(!msg.is_empty());
}
}