use globset::{Glob, GlobSet, GlobSetBuilder};
use crate::Decision;
use crate::grant::{CapabilityGrant, PolicyError, PolicyMode};
use crate::provider::{CapabilityProvider, CompiledCeiling, Explained, ResourceOp};
pub struct GenericProvider;
#[async_trait::async_trait]
impl CapabilityProvider for GenericProvider {
async fn resolve(
&self,
_cap_id: &str,
declared: &[serde_json::Value],
grant: &CapabilityGrant,
) -> Result<Box<dyn CompiledCeiling>, PolicyError> {
let unbounded = declared.is_empty();
let is_declared = !declared.is_empty();
let allow_sets = compile_constraint_globs(&grant.allow)?;
let deny_sets = compile_constraint_globs(&grant.deny)?;
Ok(Box::new(GenericCeiling {
mode: grant.mode,
allow_sets,
deny_sets,
is_declared,
unbounded,
}))
}
}
struct CompiledConstraint {
key_globs: Vec<(String, GlobSet)>,
source: String,
}
impl CompiledConstraint {
fn matches(&self, attrs: &serde_json::Value) -> bool {
self.key_globs.iter().all(|(key, glob_set)| {
let val = attrs.get(key);
if let Some(val) = val {
let s = match val {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
glob_set.is_match(&s)
} else {
false
}
})
}
}
struct GenericCeiling {
mode: PolicyMode,
allow_sets: Vec<CompiledConstraint>,
deny_sets: Vec<CompiledConstraint>,
is_declared: bool,
unbounded: bool,
}
impl GenericCeiling {
fn matched(&self, op: &ResourceOp) -> (Decision, Option<String>) {
if self.deny_sets.iter().any(|c| c.matches(&op.attrs)) {
return (Decision::Deny, None);
}
match self.mode {
PolicyMode::Deny => (Decision::Deny, None),
PolicyMode::Open => (Decision::Allow, None),
PolicyMode::Allowlist => {
match self.allow_sets.iter().find(|c| c.matches(&op.attrs)) {
Some(c) => (Decision::Allow, Some(c.source.clone())),
None => (Decision::Deny, None),
}
}
PolicyMode::Ask => {
if self.unbounded {
(Decision::Ask, None)
} else if let Some(c) = self.allow_sets.iter().find(|c| c.matches(&op.attrs)) {
(Decision::Ask, Some(c.source.clone()))
} else {
(Decision::Deny, None)
}
}
}
}
}
impl CompiledCeiling for GenericCeiling {
fn classify(&self, op: &ResourceOp) -> Decision {
self.matched(op).0
}
fn classify_explained(&self, op: &ResourceOp) -> Explained {
let (decision, rule) = self.matched(op);
Explained { decision, rule }
}
fn declared(&self) -> bool {
self.is_declared
}
}
fn compile_constraint_globs(
cs: &[serde_json::Value],
) -> Result<Vec<CompiledConstraint>, PolicyError> {
cs.iter()
.map(|c| {
let source = c.to_string();
let obj = match c.as_object() {
Some(obj) => obj,
None => {
return Ok(CompiledConstraint {
key_globs: vec![],
source,
});
}
};
let mut key_globs = Vec::new();
for (key, val) in obj {
let pattern = match val {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
let mut builder = GlobSetBuilder::new();
let glob = Glob::new(&pattern).map_err(|e| PolicyError::Glob {
pat: pattern.clone(),
source: e,
})?;
builder.add(glob);
let glob_set = builder.build().map_err(|e| PolicyError::Glob {
pat: pattern.clone(),
source: e,
})?;
key_globs.push((key.clone(), glob_set));
}
Ok(CompiledConstraint { key_globs, source })
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Decision;
use crate::grant::{CapabilityGrant, PolicyMode};
use crate::provider::{CapabilityProvider, ResourceOp};
#[tokio::test]
async fn generic_provider_globs_args() {
let p = GenericProvider;
let declared = vec![serde_json::json!({"database":"staging_*"})];
let grant = CapabilityGrant {
mode: PolicyMode::Allowlist,
allow: vec![serde_json::json!({"database":"staging_*"})],
deny: vec![],
};
let c = p.resolve("db:truncate", &declared, &grant).await.unwrap();
let op = |db: &str| ResourceOp {
cap_id: "db:truncate".into(),
key: db.into(),
action: "".into(),
attrs: serde_json::json!({"database": db}),
};
assert_eq!(c.classify(&op("staging_events")), Decision::Allow);
assert_eq!(c.classify(&op("prod_users")), Decision::Deny); }
#[tokio::test]
async fn generic_provider_permits_undeclared() {
let p = GenericProvider;
let op = ResourceOp {
cap_id: "db:truncate".into(),
key: "orders".into(),
action: "".into(),
attrs: serde_json::json!({"table":"orders"}),
};
let open = CapabilityGrant {
mode: PolicyMode::Open,
allow: vec![],
deny: vec![],
};
assert_eq!(
p.resolve("db:truncate", &[], &open)
.await
.unwrap()
.classify(&op),
Decision::Allow
);
let deny = CapabilityGrant {
mode: PolicyMode::Deny,
allow: vec![],
deny: vec![],
};
assert_eq!(
p.resolve("db:truncate", &[], &deny)
.await
.unwrap()
.classify(&op),
Decision::Deny
);
let ask = CapabilityGrant {
mode: PolicyMode::Ask,
allow: vec![],
deny: vec![],
};
assert_eq!(
p.resolve("db:truncate", &[], &ask)
.await
.unwrap()
.classify(&op),
Decision::Ask
);
}
#[tokio::test]
async fn with_builtins_routes_classes() {
use crate::provider::ProviderRegistry;
let r = ProviderRegistry::with_builtins();
assert!(
r.lookup("db:truncate")
.resolve("db:truncate", &[], &Default::default())
.await
.is_ok()
);
assert!(
r.lookup("wasi:filesystem")
.resolve("wasi:filesystem", &[], &Default::default())
.await
.is_ok()
);
}
#[tokio::test]
async fn generic_deny_wins_over_allow() {
let p = GenericProvider;
let grant = CapabilityGrant {
mode: PolicyMode::Allowlist,
allow: vec![serde_json::json!({"table": "orders"})],
deny: vec![serde_json::json!({"table": "orders"})],
};
let c = p.resolve("db:read", &[], &grant).await.unwrap();
let op = ResourceOp {
cap_id: "db:read".into(),
key: "orders".into(),
action: "".into(),
attrs: serde_json::json!({"table": "orders"}),
};
assert_eq!(c.classify(&op), Decision::Deny);
}
}