use globset::{Glob, GlobSet, GlobSetBuilder};
use crate::Decision;
use crate::grant::{CapabilityGrant, PolicyError, PolicyMode};
use crate::provider::{CapabilityProvider, CompiledCeiling, Explained, ResourceOp};
const KEY_DIMENSION: &str = "key";
pub struct GenericProvider;
#[async_trait::async_trait]
impl CapabilityProvider for GenericProvider {
async fn resolve(
&self,
_cap_id: &str,
declared: Option<&[serde_json::Value]>,
grant: &CapabilityGrant,
) -> Result<Box<dyn CompiledCeiling>, PolicyError> {
Ok(Box::new(GenericCeiling {
mode: grant.mode,
allow_sets: compile_constraint_globs(&grant.allow)?,
deny_sets: compile_constraint_globs(&grant.deny)?,
declared_sets: match declared {
Some(d) => Some(compile_constraint_globs(d)?),
None => None,
},
}))
}
}
struct CompiledConstraint {
key_globs: Vec<(String, GlobSet)>,
source: String,
}
impl CompiledConstraint {
fn matches(&self, op: &ResourceOp) -> bool {
self.dimensions_match(op, false)
}
fn matches_for_deny(&self, op: &ResourceOp) -> bool {
self.dimensions_match(op, true)
}
fn dimensions_match(&self, op: &ResourceOp, absent_matches: bool) -> bool {
self.key_globs.iter().all(|(dim, glob_set)| {
if dim == KEY_DIMENSION {
return glob_set.is_match(&op.key);
}
match op.attrs.get(dim) {
Some(serde_json::Value::String(s)) => glob_set.is_match(s),
Some(other) => glob_set.is_match(other.to_string().as_str()),
None => absent_matches,
}
})
}
}
struct GenericCeiling {
mode: PolicyMode,
allow_sets: Vec<CompiledConstraint>,
deny_sets: Vec<CompiledConstraint>,
declared_sets: Option<Vec<CompiledConstraint>>,
}
impl GenericCeiling {
fn matched(&self, op: &ResourceOp) -> (Decision, Option<String>) {
if let Some(c) = self.deny_sets.iter().find(|c| c.matches_for_deny(op)) {
return (Decision::Deny, Some(c.source.clone()));
}
let declared_sets = match &self.declared_sets {
None => return (Decision::Deny, Some("not declared in act:component".into())),
Some(sets) => sets,
};
if !declared_sets.is_empty() && !declared_sets.iter().any(|c| c.matches(op)) {
return (Decision::Deny, Some("outside the declared ceiling".into()));
}
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)) {
Some(c) => (Decision::Allow, Some(c.source.clone())),
None => (Decision::Deny, None),
},
PolicyMode::Ask => match self.allow_sets.iter().find(|c| c.matches(op)) {
Some(c) => (Decision::Ask, Some(c.source.clone())),
None if self.allow_sets.is_empty() => (Decision::Ask, None),
None => (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.declared_sets.is_some()
}
fn effective_mode(&self) -> PolicyMode {
if self.declared_sets.is_some() {
self.mode
} else {
PolicyMode::Deny
}
}
}
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", Some(&declared), &grant)
.await
.unwrap();
let op = |db: &str| ResourceOp {
cap_id: "db:truncate".into(),
key: db.into(),
action: String::new(),
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_denies_undeclared() {
let op = ResourceOp {
cap_id: "db:truncate".into(),
key: "orders".into(),
action: "request".into(),
attrs: serde_json::json!({"table": "orders"}),
};
for mode in [
PolicyMode::Open,
PolicyMode::Deny,
PolicyMode::Ask,
PolicyMode::Allowlist,
] {
let grant = CapabilityGrant {
mode,
allow: vec![serde_json::json!({"table": "orders"})],
deny: vec![],
};
let c = GenericProvider
.resolve("db:truncate", None, &grant)
.await
.unwrap();
assert_eq!(
c.classify(&op),
Decision::Deny,
"undeclared class must deny under mode {mode}"
);
assert!(!c.declared());
}
}
#[tokio::test]
async fn open_grant_does_not_step_over_the_declaration() {
let declared = vec![serde_json::json!({"key": "test_*"})];
let grant = CapabilityGrant {
mode: PolicyMode::Open,
allow: vec![],
deny: vec![],
};
let c = GenericProvider
.resolve("db:drop", Some(&declared), &grant)
.await
.unwrap();
let op = |key: &str| ResourceOp {
cap_id: "db:drop".into(),
key: key.into(),
action: "request".into(),
attrs: serde_json::Value::Null,
};
assert_eq!(c.classify(&op("test_events")), Decision::Allow);
assert_eq!(c.classify(&op("production")), Decision::Deny);
}
#[tokio::test]
async fn allowlist_grant_wider_than_the_declaration_does_not_widen_the_ceiling() {
let declared = vec![serde_json::json!({"key": "test_*"})];
let grant = CapabilityGrant {
mode: PolicyMode::Allowlist,
allow: vec![serde_json::json!({"key": "*"})],
deny: vec![],
};
let c = GenericProvider
.resolve("db:drop", Some(&declared), &grant)
.await
.unwrap();
let op = |key: &str| ResourceOp {
cap_id: "db:drop".into(),
key: key.into(),
action: "request".into(),
attrs: serde_json::Value::Null,
};
assert_eq!(c.classify(&op("test_events")), Decision::Allow);
assert_eq!(c.classify(&op("production")), Decision::Deny);
}
#[tokio::test]
async fn ask_grant_wider_than_the_declaration_does_not_widen_the_ceiling() {
let declared = vec![serde_json::json!({"key": "test_*"})];
let grant = CapabilityGrant {
mode: PolicyMode::Ask,
allow: vec![serde_json::json!({"key": "*"})],
deny: vec![],
};
let c = GenericProvider
.resolve("db:drop", Some(&declared), &grant)
.await
.unwrap();
let op = |key: &str| ResourceOp {
cap_id: "db:drop".into(),
key: key.into(),
action: "request".into(),
attrs: serde_json::Value::Null,
};
assert_eq!(c.classify(&op("test_events")), Decision::Ask);
assert_eq!(c.classify(&op("production")), Decision::Deny);
}
#[tokio::test]
async fn bare_declaration_leaves_the_class_unconstrained() {
let grant = CapabilityGrant {
mode: PolicyMode::Ask,
allow: vec![],
deny: vec![],
};
let c = GenericProvider
.resolve("db:drop", Some(&[]), &grant)
.await
.unwrap();
let op = ResourceOp {
cap_id: "db:drop".into(),
key: "anything".into(),
action: "request".into(),
attrs: serde_json::Value::Null,
};
assert_eq!(c.classify(&op), Decision::Ask);
assert!(c.declared());
}
#[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", None, &Default::default())
.await
.is_ok()
);
assert!(
r.lookup("wasi:filesystem")
.resolve("wasi:filesystem", None, &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 declared = vec![serde_json::json!({"table": "orders"})];
let c = p.resolve("db:read", Some(&declared), &grant).await.unwrap();
let op = ResourceOp {
cap_id: "db:read".into(),
key: "orders".into(),
action: String::new(),
attrs: serde_json::json!({"table": "orders"}),
};
assert_eq!(c.classify(&op), Decision::Deny);
}
#[tokio::test]
async fn key_is_a_matchable_dimension() {
let declared = vec![serde_json::json!({"key": "test_*"})];
let grant = CapabilityGrant {
mode: PolicyMode::Allowlist,
allow: vec![serde_json::json!({"key": "test_*"})],
deny: vec![],
};
let c = GenericProvider
.resolve("db:drop", Some(&declared), &grant)
.await
.unwrap();
let op = |key: &str| ResourceOp {
cap_id: "db:drop".into(),
key: key.into(),
action: "request".into(),
attrs: serde_json::Value::Null,
};
assert_eq!(c.classify(&op("test_events")), Decision::Allow);
assert_eq!(c.classify(&op("production")), Decision::Deny);
}
#[tokio::test]
async fn host_key_beats_a_guest_supplied_key_in_attrs() {
let declared = vec![serde_json::json!({"key": "test_*"})];
let grant = CapabilityGrant {
mode: PolicyMode::Allowlist,
allow: vec![serde_json::json!({"key": "test_*"})],
deny: vec![],
};
let c = GenericProvider
.resolve("db:drop", Some(&declared), &grant)
.await
.unwrap();
let op = ResourceOp {
cap_id: "db:drop".into(),
key: "production".into(),
action: "request".into(),
attrs: serde_json::json!({"key": "test_decoy"}),
};
assert_eq!(c.classify(&op), Decision::Deny);
}
#[tokio::test]
async fn a_constraint_can_require_both_key_and_an_attrs_dimension() {
let declared = vec![serde_json::json!({"key": "test_*", "table": "orders"})];
let grant = CapabilityGrant {
mode: PolicyMode::Allowlist,
allow: vec![serde_json::json!({"key": "test_*", "table": "orders"})],
deny: vec![],
};
let c = GenericProvider
.resolve("db:drop", Some(&declared), &grant)
.await
.unwrap();
let op = |key: &str, table: &str| ResourceOp {
cap_id: "db:drop".into(),
key: key.into(),
action: "request".into(),
attrs: serde_json::json!({"table": table}),
};
assert_eq!(c.classify(&op("test_events", "users")), Decision::Deny);
assert_eq!(c.classify(&op("production", "orders")), Decision::Deny);
assert_eq!(c.classify(&op("test_events", "orders")), Decision::Allow);
}
#[tokio::test]
async fn a_deny_over_an_omitted_attrs_dimension_still_denies() {
let grant = CapabilityGrant {
mode: PolicyMode::Open,
allow: vec![],
deny: vec![serde_json::json!({"table": "events"})],
};
let c = GenericProvider
.resolve("db:drop", Some(&[]), &grant)
.await
.unwrap();
let op_without_table = ResourceOp {
cap_id: "db:drop".into(),
key: "analytics".into(),
action: "request".into(),
attrs: serde_json::Value::Null,
};
assert_eq!(
c.classify(&op_without_table),
Decision::Deny,
"a deny over `table` must refuse a request that sends no table at all"
);
let op_other_table = ResourceOp {
cap_id: "db:drop".into(),
key: "analytics".into(),
action: "request".into(),
attrs: serde_json::json!({"table": "orders"}),
};
assert_eq!(c.classify(&op_other_table), Decision::Allow);
let op_matching_table = ResourceOp {
cap_id: "db:drop".into(),
key: "analytics".into(),
action: "request".into(),
attrs: serde_json::json!({"table": "events"}),
};
assert_eq!(c.classify(&op_matching_table), Decision::Deny);
}
#[tokio::test]
async fn a_deny_over_key_is_unaffected_by_the_fail_closed_change() {
let grant = CapabilityGrant {
mode: PolicyMode::Open,
allow: vec![],
deny: vec![serde_json::json!({"key": "production"})],
};
let c = GenericProvider
.resolve("db:drop", Some(&[]), &grant)
.await
.unwrap();
let op = |key: &str| ResourceOp {
cap_id: "db:drop".into(),
key: key.into(),
action: "request".into(),
attrs: serde_json::Value::Null,
};
assert_eq!(c.classify(&op("production")), Decision::Deny);
assert_eq!(c.classify(&op("analytics")), Decision::Allow);
}
#[tokio::test]
async fn an_allow_over_an_omitted_dimension_still_fails_to_match() {
let grant = CapabilityGrant {
mode: PolicyMode::Allowlist,
allow: vec![serde_json::json!({"table": "orders"})],
deny: vec![],
};
let c = GenericProvider
.resolve("db:read", Some(&[]), &grant)
.await
.unwrap();
let op = ResourceOp {
cap_id: "db:read".into(),
key: "orders".into(),
action: "request".into(),
attrs: serde_json::Value::Null,
};
assert_eq!(
c.classify(&op),
Decision::Deny,
"an allow naming a dimension the request omits must not match"
);
}
#[tokio::test]
async fn a_declared_constraint_with_an_invalid_glob_fails_to_resolve() {
let declared = vec![serde_json::json!({"key": "test_["})];
let grant = CapabilityGrant {
mode: PolicyMode::Ask,
allow: vec![],
deny: vec![],
};
let result = GenericProvider
.resolve("db:drop", Some(&declared), &grant)
.await;
assert!(result.is_err());
}
}