1use std::collections::HashMap;
50use std::fmt;
51use std::str::FromStr;
52
53use serde::{Deserialize, Deserializer, Serialize, Serializer};
54
55#[cfg(feature = "acl")]
56use crate::{Error, Result};
57
58pub const PERM_R: u8 = 0b100;
62pub const PERM_W: u8 = 0b010;
64pub const PERM_X: u8 = 0b001;
66pub const PERM_RWX: u8 = 0b111;
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum Permissions {
77 Deny,
79 Allow(u8),
81}
82
83impl Permissions {
84 pub const fn grants(self, required: u8) -> bool {
86 match self {
87 Self::Allow(p) => p & required == required,
88 Self::Deny => false,
89 }
90 }
91
92 pub fn is_deny(self) -> bool {
94 self == Self::Deny
95 }
96}
97
98impl fmt::Display for Permissions {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 Self::Deny => write!(f, "-"),
102 Self::Allow(p) => {
103 if p & PERM_R != 0 {
104 write!(f, "r")?;
105 }
106 if p & PERM_W != 0 {
107 write!(f, "w")?;
108 }
109 if p & PERM_X != 0 {
110 write!(f, "x")?;
111 }
112 Ok(())
113 }
114 }
115 }
116}
117
118impl FromStr for Permissions {
119 type Err = anyhow::Error;
120
121 fn from_str(s: &str) -> std::result::Result<Self, anyhow::Error> {
122 if s.is_empty() {
123 return Ok(Self::Deny);
124 }
125 let mut bits = 0u8;
126 for ch in s.chars() {
127 match ch {
128 'r' => bits |= PERM_R,
129 'w' => bits |= PERM_W,
130 'x' => bits |= PERM_X,
131 other => {
132 return Err(anyhow::anyhow!(
133 "unknown permission character '{other}' in '{s}'"
134 ));
135 }
136 }
137 }
138 if bits == 0 {
139 return Err(anyhow::anyhow!("permission string '{s}' has no valid bits"));
140 }
141 Ok(Self::Allow(bits))
142 }
143}
144
145impl Serialize for Permissions {
146 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
147 match self {
148 Self::Deny => serializer.serialize_none(),
149 Self::Allow(_) => serializer.serialize_str(&self.to_string()),
150 }
151 }
152}
153
154impl<'de> Deserialize<'de> for Permissions {
155 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
156 let opt: Option<String> = Option::deserialize(deserializer)?;
157 match opt {
158 None => Ok(Self::Deny),
159 Some(s) if s.is_empty() => Ok(Self::Deny),
160 Some(s) => s.parse::<Permissions>().map_err(serde::de::Error::custom),
161 }
162 }
163}
164
165pub type AclMap = HashMap<String, Permissions>;
178
179#[cfg(feature = "acl")]
190pub fn check_op(acl: &AclMap, caller: &str, required: u8) -> Result<()> {
191 let normalized = normalize_principal(caller);
192 if let Some(direct) = acl.get(normalized) {
193 return if direct.is_deny() {
194 Err(Error::Acl(format!("operation denied for {caller}")))
195 } else if direct.grants(required) {
196 Ok(())
197 } else {
198 Err(Error::Acl(format!("permission denied for {caller}")))
199 };
200 }
201
202 match acl.get("*") {
203 None => Err(Error::Acl(format!("no ACL entry for {caller}"))),
204 Some(e) if e.is_deny() => Err(Error::Acl(format!("operation denied for {caller}"))),
205 Some(e) if e.grants(required) => Ok(()),
206 Some(_) => Err(Error::Acl(format!("permission denied for {caller}"))),
207 }
208}
209
210pub fn is_valid_acl_key(key: &str) -> bool {
218 key == "*"
219 || (key.starts_with("did:") && !key.contains('#'))
220 || (key.starts_with('#') && key.len() > 1)
221 || is_valid_group_key(key)
222}
223
224fn is_valid_group_key(key: &str) -> bool {
226 if let Some(rest) = key.strip_prefix("group:") {
227 if let Some(dot) = rest.find('.') {
228 let handle = &rest[..dot];
229 let name = &rest[dot + 1..];
230 return !handle.is_empty() && !name.is_empty();
231 }
232 }
233 false
234}
235
236#[cfg(feature = "acl")]
241pub fn validate_acl_map(acl: &AclMap) -> Result<()> {
242 for key in acl.keys() {
243 if !is_valid_acl_key(key) {
244 return Err(Error::Acl(format!(
245 "invalid ACL key {key:?}: must be \"*\", a bare DID (\"did:ma:\u{2026}\"), \
246 a local entity (\"#name\"), or a group (\"group:<handle>.<name>\")"
247 )));
248 }
249 }
250 Ok(())
251}
252
253pub fn normalize_principal(did: &str) -> &str {
259 if did.starts_with("did:") {
260 if let Some(pos) = did.find('#') {
261 return &did[..pos];
262 }
263 }
264 did
265}
266
267#[cfg(test)]
270mod tests {
271 use super::*;
272
273 fn m(entries: &[(&str, &str)]) -> AclMap {
274 entries
275 .iter()
276 .map(|(k, v)| (k.to_string(), v.parse().expect("valid permissions")))
277 .collect()
278 }
279
280 #[test]
281 fn wildcard_exec_allows_execute() {
282 let acl = m(&[("*", "x")]);
283 assert!(check_op(&acl, "did:ma:alice", PERM_X).is_ok());
284 }
285
286 #[test]
287 fn wildcard_exec_denies_write() {
288 let acl = m(&[("*", "x")]);
289 assert!(check_op(&acl, "did:ma:alice", PERM_W).is_err());
290 }
291
292 #[test]
293 fn explicit_deny_wins_over_wildcard_allow() {
294 let acl = m(&[("*", "rwx"), ("did:ma:bandit", "")]);
295 assert!(check_op(&acl, "did:ma:bandit", PERM_X).is_err());
296 }
297
298 #[test]
299 fn exact_match_restricts_below_wildcard() {
300 let acl = m(&[("*", "rwx"), ("did:ma:bob", "r")]);
301 assert!(check_op(&acl, "did:ma:bob", PERM_R).is_ok());
302 assert!(check_op(&acl, "did:ma:bob", PERM_X).is_err());
303 }
304
305 #[test]
306 fn did_url_caller_is_normalized() {
307 let acl = m(&[("did:ma:alice", "rwx")]);
308 assert!(check_op(&acl, "did:ma:alice#sign", PERM_X).is_ok());
309 }
310
311 #[test]
312 fn no_entry_default_deny() {
313 assert!(check_op(&AclMap::new(), "did:ma:anyone", PERM_X).is_err());
314 }
315
316 #[test]
317 fn wildcard_deny_blocks_all() {
318 let acl = m(&[("*", "")]);
319 assert!(check_op(&acl, "did:ma:anyone", PERM_X).is_err());
320 }
321
322 #[test]
323 fn local_entity_key_allowed() {
324 let acl = m(&[("#agent", "rwx")]);
325 assert!(check_op(&acl, "#agent", PERM_X).is_ok());
326 assert!(check_op(&acl, "#other", PERM_X).is_err());
327 }
328
329 #[test]
330 fn normalize_strips_fragment() {
331 assert_eq!(normalize_principal("did:ma:foo#bar"), "did:ma:foo");
332 assert_eq!(normalize_principal("did:ma:foo"), "did:ma:foo");
333 assert_eq!(normalize_principal("#local"), "#local");
334 assert_eq!(normalize_principal("*"), "*");
335 }
336
337 #[test]
338 fn valid_acl_keys() {
339 assert!(is_valid_acl_key("*"));
340 assert!(is_valid_acl_key("did:ma:Qmfoo"));
341 assert!(is_valid_acl_key("#agent"));
342 assert!(is_valid_acl_key("group:alice.venner"));
343 assert!(is_valid_acl_key("group:runtime.admins"));
344 assert!(!is_valid_acl_key("did:ma:Qmfoo#sign"));
345 assert!(!is_valid_acl_key("#"));
346 assert!(!is_valid_acl_key(""));
347 assert!(!is_valid_acl_key("group:noname"));
348 assert!(!is_valid_acl_key("group:.nohandle"));
349 assert!(!is_valid_acl_key("group:handle."));
350 }
351
352 #[test]
353 fn permissions_display() {
354 assert_eq!(Permissions::Allow(PERM_RWX).to_string(), "rwx");
355 assert_eq!(Permissions::Allow(PERM_R | PERM_X).to_string(), "rx");
356 assert_eq!(Permissions::Allow(PERM_X).to_string(), "x");
357 assert_eq!(Permissions::Deny.to_string(), "-");
358 }
359
360 #[test]
361 fn permissions_from_str() {
362 assert_eq!(
363 "rwx".parse::<Permissions>().unwrap(),
364 Permissions::Allow(PERM_RWX)
365 );
366 assert_eq!(
367 "rx".parse::<Permissions>().unwrap(),
368 Permissions::Allow(PERM_R | PERM_X)
369 );
370 assert_eq!(
371 "x".parse::<Permissions>().unwrap(),
372 Permissions::Allow(PERM_X)
373 );
374 assert_eq!("".parse::<Permissions>().unwrap(), Permissions::Deny);
375 assert!("z".parse::<Permissions>().is_err());
376 }
377
378 #[cfg(feature = "acl")]
379 #[test]
380 fn permissions_serde_roundtrip() {
381 let acl: AclMap = [
382 ("*".to_string(), Permissions::Allow(PERM_RWX)),
383 ("did:ma:bandit".to_string(), Permissions::Deny),
384 ]
385 .into_iter()
386 .collect();
387 let yaml = serde_yaml::to_string(&acl).unwrap();
388 let roundtrip: AclMap = serde_yaml::from_str(&yaml).unwrap();
389 assert_eq!(acl, roundtrip);
390 }
391
392 #[cfg(feature = "acl")]
393 #[test]
394 fn yaml_null_deserializes_to_deny() {
395 let yaml = "'did:ma:x': ~\n'*': rwx\n";
397 let acl: AclMap = serde_yaml::from_str(yaml).unwrap();
398 assert_eq!(acl.get("did:ma:x"), Some(&Permissions::Deny));
399 assert_eq!(acl.get("*"), Some(&Permissions::Allow(PERM_RWX)));
400 }
401}