use std::collections::BTreeMap;
use serde::Deserialize;
#[derive(Debug, thiserror::Error)]
pub enum PolicyError {
#[error("invalid {cap} constraint: {source}")]
Constraint {
cap: &'static str,
#[source]
source: serde_json::Error,
},
#[error("invalid policy mode '{0}': expected deny / allowlist / open / ask")]
InvalidMode(String),
#[error("invalid glob {pat:?}: {source}")]
Glob {
pat: String,
#[source]
source: globset::Error,
},
#[error("capability {cap}: {source}")]
Capability {
cap: String,
#[source]
source: Box<PolicyError>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PolicyMode {
#[default]
Deny,
Allowlist,
Open,
Ask,
}
impl PolicyMode {
pub fn parse(s: &str) -> Result<Self, PolicyError> {
match s {
"deny" => Ok(Self::Deny),
"allowlist" => Ok(Self::Allowlist),
"open" => Ok(Self::Open),
"ask" => Ok(Self::Ask),
other => Err(PolicyError::InvalidMode(other.to_string())),
}
}
}
impl std::fmt::Display for PolicyMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Deny => "deny",
Self::Allowlist => "allowlist",
Self::Open => "open",
Self::Ask => "ask",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FsAllow {
pub glob: String,
pub mode: act_types::FsMode,
}
#[derive(Debug, Clone, Default)]
pub struct FsConfig {
pub mode: PolicyMode,
pub allow: Vec<FsAllow>,
#[allow(dead_code)]
pub deny: Vec<String>,
}
impl FsConfig {
#[allow(dead_code)]
pub fn deny() -> Self {
Self {
mode: PolicyMode::Deny,
..Default::default()
}
}
}
#[derive(Debug, Clone, Default)]
pub struct HttpConfig {
pub mode: PolicyMode,
#[allow(dead_code)]
pub allow: Vec<HttpRule>,
#[allow(dead_code)]
pub deny: Vec<HttpRule>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
pub struct HttpRule {
#[serde(flatten)]
pub net: crate::net::NetworkRule,
#[serde(default)]
pub scheme: Option<String>,
#[serde(default)]
pub methods: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default)]
#[allow(dead_code)] pub struct SocketsConfig {
pub mode: PolicyMode,
pub allow: Vec<SocketsRule>,
pub deny: Vec<SocketsRule>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
pub struct SocketsRule {
#[serde(flatten)]
pub net: crate::net::NetworkRule,
#[serde(default)]
pub protocols: Option<Vec<act_types::SocketProtocol>>,
}
#[derive(Debug, Clone, Default)]
pub struct CapabilityGrant {
pub mode: PolicyMode,
pub allow: Vec<serde_json::Value>,
pub deny: Vec<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct GrantPolicy {
pub default: PolicyMode,
pub entries: BTreeMap<String, CapabilityGrant>,
}
impl Default for GrantPolicy {
fn default() -> Self {
Self {
default: PolicyMode::Ask,
entries: BTreeMap::new(),
}
}
}
impl GrantPolicy {
pub fn resolve(&self, id: &str) -> CapabilityGrant {
if let Some(g) = self.entries.get(id) {
return g.clone();
}
let mut best: Option<(&str, &CapabilityGrant)> = None;
for (k, g) in &self.entries {
if let Some(prefix) = k.strip_suffix('*')
&& id.starts_with(prefix)
&& best.is_none_or(|(bk, _)| prefix.len() > bk.len() - 1)
{
best = Some((k, g));
}
}
if let Some((_, g)) = best {
return g.clone();
}
CapabilityGrant {
mode: self.default,
allow: vec![],
deny: vec![],
}
}
}
pub fn to_fs_config(gp: &GrantPolicy) -> Result<FsConfig, PolicyError> {
let g = gp.resolve(act_types::constants::CAP_FILESYSTEM);
let allow = parse_fs_allow_constraints(&g.allow)?;
let deny = parse_fs_deny_constraints(&g.deny)?;
Ok(FsConfig {
mode: g.mode,
allow,
deny,
})
}
fn parse_fs_allow_constraints(cs: &[serde_json::Value]) -> Result<Vec<FsAllow>, PolicyError> {
cs.iter()
.map(|c| {
let a: act_types::FilesystemAllow =
serde_json::from_value(c.clone()).map_err(|e| PolicyError::Constraint {
cap: "wasi:filesystem",
source: e,
})?;
Ok(FsAllow {
glob: a.path,
mode: a.mode,
})
})
.collect()
}
fn parse_fs_deny_constraints(cs: &[serde_json::Value]) -> Result<Vec<String>, PolicyError> {
cs.iter()
.map(|c| {
let a: act_types::FilesystemAllow =
serde_json::from_value(c.clone()).map_err(|e| PolicyError::Constraint {
cap: "wasi:filesystem",
source: e,
})?;
Ok(a.path)
})
.collect()
}
pub fn to_http_config(gp: &GrantPolicy) -> Result<HttpConfig, PolicyError> {
let g = gp.resolve(act_types::constants::CAP_HTTP);
Ok(HttpConfig {
mode: g.mode,
allow: parse_http_constraints(&g.allow)?,
deny: parse_http_constraints(&g.deny)?,
})
}
fn parse_http_constraints(cs: &[serde_json::Value]) -> Result<Vec<HttpRule>, PolicyError> {
cs.iter()
.map(|c| {
serde_json::from_value::<HttpRule>(c.clone()).map_err(|e| PolicyError::Constraint {
cap: "wasi:http",
source: e,
})
})
.collect()
}
pub fn to_sockets_config(gp: &GrantPolicy) -> Result<SocketsConfig, PolicyError> {
let g = gp.resolve(act_types::constants::CAP_SOCKETS);
Ok(SocketsConfig {
mode: g.mode,
allow: parse_sockets_constraints(&g.allow)?,
deny: parse_sockets_constraints(&g.deny)?,
})
}
fn parse_sockets_constraints(cs: &[serde_json::Value]) -> Result<Vec<SocketsRule>, PolicyError> {
cs.iter()
.map(|c| {
serde_json::from_value::<SocketsRule>(c.clone()).map_err(|e| PolicyError::Constraint {
cap: "wasi:sockets",
source: e,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::PolicyMode;
#[test]
fn policy_mode_display_renders_the_config_spellings() {
assert_eq!(PolicyMode::Deny.to_string(), "deny");
assert_eq!(PolicyMode::Allowlist.to_string(), "allowlist");
assert_eq!(PolicyMode::Open.to_string(), "open");
assert_eq!(PolicyMode::Ask.to_string(), "ask");
}
}