1use std::collections::BTreeMap;
5
6use serde::Deserialize;
7
8#[derive(Debug, thiserror::Error)]
11pub enum PolicyError {
12 #[error("invalid {cap} constraint: {source}")]
13 Constraint {
14 cap: &'static str,
15 #[source]
16 source: serde_json::Error,
17 },
18 #[error("invalid policy mode '{0}': expected deny / allowlist / open / ask")]
19 InvalidMode(String),
20 #[error("invalid glob {pat:?}: {source}")]
21 Glob {
22 pat: String,
23 #[source]
24 source: globset::Error,
25 },
26 #[error("capability {cap}: {source}")]
27 Capability {
28 cap: String,
29 #[source]
30 source: Box<PolicyError>,
31 },
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum PolicyMode {
39 #[default]
40 Deny,
41 Allowlist,
42 Open,
43 Ask,
48}
49
50impl PolicyMode {
51 pub fn parse(s: &str) -> Result<Self, PolicyError> {
52 match s {
53 "deny" => Ok(Self::Deny),
54 "allowlist" => Ok(Self::Allowlist),
55 "open" => Ok(Self::Open),
56 "ask" => Ok(Self::Ask),
57 other => Err(PolicyError::InvalidMode(other.to_string())),
58 }
59 }
60}
61
62impl std::fmt::Display for PolicyMode {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.write_str(match self {
65 Self::Deny => "deny",
66 Self::Allowlist => "allowlist",
67 Self::Open => "open",
68 Self::Ask => "ask",
69 })
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct FsAllow {
79 pub glob: String,
80 pub mode: act_types::FsMode,
81}
82
83#[derive(Debug, Clone, Default)]
85pub struct FsConfig {
86 pub mode: PolicyMode,
87 pub allow: Vec<FsAllow>,
88 #[allow(dead_code)]
91 pub deny: Vec<String>,
92}
93
94impl FsConfig {
95 #[allow(dead_code)]
96 pub fn deny() -> Self {
97 Self {
98 mode: PolicyMode::Deny,
99 ..Default::default()
100 }
101 }
102}
103
104#[derive(Debug, Clone, Default)]
110pub struct HttpConfig {
111 pub mode: PolicyMode,
112 #[allow(dead_code)]
113 pub allow: Vec<HttpRule>,
114 #[allow(dead_code)]
115 pub deny: Vec<HttpRule>,
116}
117
118#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
120pub struct HttpRule {
121 #[serde(flatten)]
123 pub net: crate::net::NetworkRule,
124 #[serde(default)]
126 pub scheme: Option<String>,
127 #[serde(default)]
129 pub methods: Option<Vec<String>>,
130}
131
132#[derive(Debug, Clone, Default)]
134#[allow(dead_code)] pub struct SocketsConfig {
136 pub mode: PolicyMode,
137 pub allow: Vec<SocketsRule>,
138 pub deny: Vec<SocketsRule>,
139}
140
141#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
143pub struct SocketsRule {
144 #[serde(flatten)]
146 pub net: crate::net::NetworkRule,
147 #[serde(default)]
150 pub protocols: Option<Vec<act_types::SocketProtocol>>,
151}
152
153#[derive(Debug, Clone, Default)]
158pub struct CapabilityGrant {
159 pub mode: PolicyMode,
160 pub allow: Vec<serde_json::Value>,
161 pub deny: Vec<serde_json::Value>,
162}
163
164#[derive(Debug, Clone)]
166pub struct GrantPolicy {
167 pub default: PolicyMode,
168 pub entries: BTreeMap<String, CapabilityGrant>,
169}
170
171impl Default for GrantPolicy {
172 fn default() -> Self {
173 Self {
174 default: PolicyMode::Ask,
179 entries: BTreeMap::new(),
180 }
181 }
182}
183
184impl GrantPolicy {
185 pub fn resolve(&self, id: &str) -> CapabilityGrant {
188 if let Some(g) = self.entries.get(id) {
189 return g.clone();
190 }
191 let mut best: Option<(&str, &CapabilityGrant)> = None;
192 for (k, g) in &self.entries {
193 if let Some(prefix) = k.strip_suffix('*')
194 && id.starts_with(prefix)
195 && best.is_none_or(|(bk, _)| prefix.len() > bk.len() - 1)
196 {
197 best = Some((k, g));
198 }
199 }
200 if let Some((_, g)) = best {
201 return g.clone();
202 }
203 CapabilityGrant {
204 mode: self.default,
205 allow: vec![],
206 deny: vec![],
207 }
208 }
209}
210
211pub fn to_fs_config(gp: &GrantPolicy) -> Result<FsConfig, PolicyError> {
215 let g = gp.resolve(act_types::constants::CAP_FILESYSTEM);
216 let allow = parse_fs_allow_constraints(&g.allow)?;
217 let deny = parse_fs_deny_constraints(&g.deny)?;
218 Ok(FsConfig {
219 mode: g.mode,
220 allow,
221 deny,
222 })
223}
224
225fn parse_fs_allow_constraints(cs: &[serde_json::Value]) -> Result<Vec<FsAllow>, PolicyError> {
226 cs.iter()
227 .map(|c| {
228 let a: act_types::FilesystemAllow =
229 serde_json::from_value(c.clone()).map_err(|e| PolicyError::Constraint {
230 cap: "wasi:filesystem",
231 source: e,
232 })?;
233 Ok(FsAllow {
234 glob: a.path,
235 mode: a.mode,
236 })
237 })
238 .collect()
239}
240
241fn parse_fs_deny_constraints(cs: &[serde_json::Value]) -> Result<Vec<String>, PolicyError> {
242 cs.iter()
243 .map(|c| {
244 let a: act_types::FilesystemAllow =
245 serde_json::from_value(c.clone()).map_err(|e| PolicyError::Constraint {
246 cap: "wasi:filesystem",
247 source: e,
248 })?;
249 Ok(a.path)
250 })
251 .collect()
252}
253
254pub fn to_http_config(gp: &GrantPolicy) -> Result<HttpConfig, PolicyError> {
256 let g = gp.resolve(act_types::constants::CAP_HTTP);
257 Ok(HttpConfig {
258 mode: g.mode,
259 allow: parse_http_constraints(&g.allow)?,
260 deny: parse_http_constraints(&g.deny)?,
261 })
262}
263
264fn parse_http_constraints(cs: &[serde_json::Value]) -> Result<Vec<HttpRule>, PolicyError> {
265 cs.iter()
266 .map(|c| {
267 serde_json::from_value::<HttpRule>(c.clone()).map_err(|e| PolicyError::Constraint {
268 cap: "wasi:http",
269 source: e,
270 })
271 })
272 .collect()
273}
274
275pub fn to_sockets_config(gp: &GrantPolicy) -> Result<SocketsConfig, PolicyError> {
277 let g = gp.resolve(act_types::constants::CAP_SOCKETS);
278 Ok(SocketsConfig {
279 mode: g.mode,
280 allow: parse_sockets_constraints(&g.allow)?,
281 deny: parse_sockets_constraints(&g.deny)?,
282 })
283}
284
285fn parse_sockets_constraints(cs: &[serde_json::Value]) -> Result<Vec<SocketsRule>, PolicyError> {
286 cs.iter()
287 .map(|c| {
288 serde_json::from_value::<SocketsRule>(c.clone()).map_err(|e| PolicyError::Constraint {
289 cap: "wasi:sockets",
290 source: e,
291 })
292 })
293 .collect()
294}
295
296#[cfg(test)]
297mod tests {
298 use super::PolicyMode;
299
300 #[test]
301 fn policy_mode_display_renders_the_config_spellings() {
302 assert_eq!(PolicyMode::Deny.to_string(), "deny");
305 assert_eq!(PolicyMode::Allowlist.to_string(), "allowlist");
306 assert_eq!(PolicyMode::Open.to_string(), "open");
307 assert_eq!(PolicyMode::Ask.to_string(), "ask");
308 }
309}