use std::collections::BTreeMap;
use std::sync::Arc;
use act_types::constants::{CAP_FILESYSTEM, CAP_HTTP, CAP_SOCKETS};
use crate::grant::{GrantPolicy, PolicyError};
use crate::provider::{CompiledCeiling, ProviderRegistry};
pub const ALWAYS_RESOLVED: &[&str] = &[
CAP_FILESYSTEM,
CAP_HTTP,
CAP_SOCKETS,
crate::providers::credentials::CAP_CREDENTIALS,
];
pub const PHYSICALLY_INTERCEPTED: &[&str] = ALWAYS_RESOLVED;
pub async fn resolve_ceilings(
registry: &ProviderRegistry,
declared: &BTreeMap<String, Vec<serde_json::Value>>,
policy: &GrantPolicy,
) -> Result<BTreeMap<String, Arc<dyn CompiledCeiling>>, PolicyError> {
let mut ids: Vec<&str> = ALWAYS_RESOLVED.to_vec();
for id in declared.keys() {
if !ids.contains(&id.as_str()) {
ids.push(id);
}
}
let mut out: BTreeMap<String, Arc<dyn CompiledCeiling>> = BTreeMap::new();
for id in ids {
let grant = policy.resolve(id);
let ceiling = registry
.lookup(id)
.resolve(id, declared.get(id).map(Vec::as_slice), &grant)
.await
.map_err(|source| PolicyError::Capability {
cap: id.to_string(),
source: Box::new(source),
})?;
out.insert(id.to_string(), Arc::from(ceiling));
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Decision;
use crate::provider::ResourceOp;
#[tokio::test]
async fn resolves_every_always_class_plus_every_declared_one() {
let registry = ProviderRegistry::with_builtins();
let declared = BTreeMap::from([
(
"db:drop".to_string(),
vec![serde_json::json!({"key": "test_*"})],
),
("wasi:http".to_string(), Vec::new()),
]);
let ceilings = resolve_ceilings(®istry, &declared, &GrantPolicy::default())
.await
.unwrap();
for id in ALWAYS_RESOLVED.iter().copied() {
assert!(ceilings.contains_key(id), "{id} must always resolve");
}
assert!(ceilings.contains_key("db:drop"));
assert!(ceilings["db:drop"].declared());
assert!(!ceilings["wasi:filesystem"].declared());
assert!(ceilings.contains_key("wasi:http"));
let op = |key: &str| ResourceOp {
cap_id: "db:drop".into(),
key: key.into(),
action: "request".into(),
attrs: serde_json::Value::Null,
};
assert_eq!(
ceilings["db:drop"].classify(&op("test_events")),
Decision::Ask
);
assert_eq!(
ceilings["db:drop"].classify(&op("production")),
Decision::Deny
);
}
#[tokio::test]
async fn an_undeclared_semantic_class_gets_no_row() {
let registry = ProviderRegistry::with_builtins();
let ceilings = resolve_ceilings(®istry, &BTreeMap::new(), &GrantPolicy::default())
.await
.unwrap();
assert!(
!ceilings.contains_key("db:drop"),
"a class the manifest never declared is not resolved; callers deny \
an unresolved id outright"
);
}
#[tokio::test]
async fn a_failing_provider_names_the_capability_that_failed() {
let registry = ProviderRegistry::with_builtins();
let declared = BTreeMap::from([(
"db:drop".to_string(),
vec![serde_json::json!({"key": "test_["})],
)]);
let err = match resolve_ceilings(®istry, &declared, &GrantPolicy::default()).await {
Ok(_) => panic!("an invalid glob must fail resolution"),
Err(e) => e,
};
let rendered = err.to_string();
assert!(
rendered.contains("db:drop"),
"the error must name the capability that failed: {rendered}"
);
}
}