1use std::collections::{BTreeSet, HashMap};
65
66use serde::{Deserialize, Deserializer, Serialize, Serializer};
67
68#[cfg(feature = "acl")]
69use crate::{Error, Result};
70
71pub const CAP_INBOX: &str = "inbox";
75pub const CAP_RPC: &str = "rpc";
77pub const CAP_IPFS: &str = "ipfs";
79pub const CAP_READ: &str = "read";
81pub const CAP_CREATE: &str = "create";
83pub const CAP_UPDATE: &str = "update";
85pub const CAP_DELETE: &str = "delete";
87
88#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum CapabilityEntry {
99 Deny,
101 Allow(BTreeSet<String>),
103 Grant(Vec<String>),
107}
108
109impl CapabilityEntry {
110 pub fn from_caps<I, S>(caps: I) -> Self
112 where
113 I: IntoIterator<Item = S>,
114 S: Into<String>,
115 {
116 Self::Allow(caps.into_iter().map(Into::into).collect())
117 }
118
119 pub fn has(&self, cap: &str) -> bool {
122 match self {
123 Self::Deny | Self::Grant(_) => false,
124 Self::Allow(caps) => caps.contains(cap) || caps.contains("*"),
125 }
126 }
127
128 pub fn is_deny(&self) -> bool {
130 matches!(self, Self::Deny)
131 }
132
133 pub fn grantees(&self) -> Option<&[String]> {
135 if let Self::Grant(refs) = self {
136 Some(refs)
137 } else {
138 None
139 }
140 }
141}
142
143impl Serialize for CapabilityEntry {
144 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
145 match self {
146 Self::Deny => serializer.serialize_none(),
147 Self::Allow(caps) => {
148 use serde::ser::SerializeSeq;
149 let mut seq = serializer.serialize_seq(Some(caps.len()))?;
150 for cap in caps {
151 seq.serialize_element(cap)?;
152 }
153 seq.end()
154 }
155 Self::Grant(refs) => serializer.serialize_str(&refs.join(",")),
156 }
157 }
158}
159
160impl<'de> Deserialize<'de> for CapabilityEntry {
161 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
162 #[derive(Deserialize)]
163 #[serde(untagged)]
164 enum Raw {
165 Str(String),
166 Seq(Vec<String>),
167 }
168 let opt: Option<Raw> = Option::deserialize(deserializer)?;
169 match opt {
170 None => Ok(Self::Deny),
171 Some(Raw::Seq(v)) if v.is_empty() => Ok(Self::Deny),
172 Some(Raw::Seq(v)) => Ok(Self::Allow(v.into_iter().collect())),
173 Some(Raw::Str(s)) => {
174 let refs: Vec<String> = s
175 .split(',')
176 .map(|r| r.trim().to_string())
177 .filter(|r| !r.is_empty())
178 .collect();
179 if refs.is_empty() {
180 Ok(Self::Deny)
181 } else {
182 Ok(Self::Grant(refs))
183 }
184 }
185 }
186 }
187}
188
189pub type AclMap = HashMap<String, CapabilityEntry>;
202
203#[cfg(feature = "acl")]
217pub fn check_cap(acl: &AclMap, caller: &str, cap: &str) -> Result<()> {
218 let normalized = normalize_principal(caller);
219 if let Some(direct) = acl.get(normalized) {
220 match direct {
221 CapabilityEntry::Deny => {
222 return Err(Error::Acl(format!("operation denied for {caller}")));
223 }
224 CapabilityEntry::Allow(caps) if caps.contains(cap) || caps.contains("*") => {
225 return Ok(());
226 }
227 CapabilityEntry::Allow(_) => {
228 return Err(Error::Acl(format!(
229 "capability '{cap}' denied for {caller}"
230 )));
231 }
232 CapabilityEntry::Grant(_) => {
233 }
236 }
237 }
238
239 match acl.get("*") {
240 Some(CapabilityEntry::Grant(_)) | None => {
241 Err(Error::Acl(format!("no ACL entry for {caller}")))
242 }
243 Some(CapabilityEntry::Deny) => Err(Error::Acl(format!("operation denied for {caller}"))),
244 Some(CapabilityEntry::Allow(caps)) if caps.contains(cap) || caps.contains("*") => Ok(()),
245 Some(CapabilityEntry::Allow(_)) => Err(Error::Acl(format!(
246 "capability '{cap}' denied for {caller}"
247 ))),
248 }
249}
250
251pub fn is_valid_acl_key(key: &str) -> bool {
258 !key.is_empty()
259}
260
261pub fn is_principal_key(key: &str) -> bool {
263 key == "*"
264 || (key.starts_with("did:") && !key.contains('#'))
265 || (key.starts_with('#') && key.len() > 1)
266 || is_valid_group_key(key)
267}
268
269fn is_valid_group_key(key: &str) -> bool {
271 if let Some(rest) = key.strip_prefix("group:") {
272 if let Some(dot) = rest.find('.') {
273 let handle = &rest[..dot];
274 let name = &rest[dot + 1..];
275 return !handle.is_empty() && !name.is_empty();
276 }
277 }
278 false
279}
280
281#[cfg(feature = "acl")]
286pub fn validate_acl_map(acl: &AclMap) -> Result<()> {
287 for key in acl.keys() {
288 if !is_valid_acl_key(key) {
289 return Err(Error::Acl(format!(
290 "invalid ACL key {key:?}: key must be non-empty"
291 )));
292 }
293 }
294 Ok(())
295}
296
297pub fn normalize_principal(did: &str) -> &str {
303 if did.starts_with("did:") {
304 if let Some(pos) = did.find('#') {
305 return &did[..pos];
306 }
307 }
308 did
309}
310
311#[cfg(test)]
314mod tests {
315 use super::*;
316
317 fn allow(caps: &[&str]) -> CapabilityEntry {
318 CapabilityEntry::from_caps(caps.iter().copied())
319 }
320
321 fn m(entries: &[(&str, CapabilityEntry)]) -> AclMap {
322 entries
323 .iter()
324 .map(|(k, v)| (k.to_string(), v.clone()))
325 .collect()
326 }
327
328 #[test]
329 fn wildcard_rpc_allows_rpc() {
330 let acl = m(&[("*", allow(&[CAP_RPC]))]);
331 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
332 }
333
334 #[test]
335 fn wildcard_rpc_denies_ipfs() {
336 let acl = m(&[("*", allow(&[CAP_RPC]))]);
337 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_err());
338 }
339
340 #[test]
341 fn explicit_deny_wins_over_wildcard_allow() {
342 let acl = m(&[
343 ("*", allow(&[CAP_RPC, CAP_IPFS])),
344 ("did:ma:bandit", CapabilityEntry::Deny),
345 ]);
346 assert!(check_cap(&acl, "did:ma:bandit", CAP_RPC).is_err());
347 }
348
349 #[test]
350 fn exact_match_restricts_below_wildcard() {
351 let acl = m(&[
352 ("*", allow(&[CAP_RPC, CAP_IPFS])),
353 ("did:ma:bob", allow(&[CAP_RPC])),
354 ]);
355 assert!(check_cap(&acl, "did:ma:bob", CAP_RPC).is_ok());
356 assert!(check_cap(&acl, "did:ma:bob", CAP_IPFS).is_err());
357 }
358
359 #[test]
360 fn did_url_caller_is_normalized() {
361 let acl = m(&[("did:ma:alice", allow(&[CAP_RPC, CAP_IPFS]))]);
362 assert!(check_cap(&acl, "did:ma:alice#sign", CAP_RPC).is_ok());
363 }
364
365 #[test]
366 fn no_entry_default_deny() {
367 assert!(check_cap(&AclMap::new(), "did:ma:anyone", CAP_RPC).is_err());
368 }
369
370 #[test]
371 fn wildcard_deny_blocks_all() {
372 let acl = m(&[("*", CapabilityEntry::Deny)]);
373 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_err());
374 }
375
376 #[test]
377 fn local_entity_key_allowed() {
378 let acl = m(&[("#agent", allow(&[CAP_RPC]))]);
379 assert!(check_cap(&acl, "#agent", CAP_RPC).is_ok());
380 assert!(check_cap(&acl, "#other", CAP_RPC).is_err());
381 }
382
383 #[test]
384 fn arbitrary_capability_works() {
385 let acl = m(&[("did:ma:alice", allow(&["emote", "reply"]))]);
386 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
387 assert!(check_cap(&acl, "did:ma:alice", "reply").is_ok());
388 assert!(check_cap(&acl, "did:ma:alice", "admin").is_err());
389 }
390
391 #[test]
392 fn wildcard_cap_grants_all_capabilities() {
393 let acl = m(&[("did:ma:alice", allow(&["*"]))]);
394 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
395 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
396 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
397 assert!(check_cap(&acl, "did:ma:alice", "admin").is_ok());
398 }
399
400 #[test]
401 fn grant_entry_is_skipped_by_check_cap() {
402 let mut acl = AclMap::new();
403 acl.insert("*".to_string(), allow(&[CAP_RPC]));
404 acl.insert(
405 "fortune".to_string(),
406 CapabilityEntry::Grant(vec!["group:carlotta.friends".to_string()]),
407 );
408 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_ok());
409 assert!(check_cap(&acl, "did:ma:anyone", "fortune").is_err());
410 }
411
412 #[test]
413 fn grant_entry_serde_round_trip() {
414 let entry = CapabilityEntry::Grant(vec![
415 "group:carlotta.friends".to_string(),
416 "did:ma:alice".to_string(),
417 ]);
418 let yaml = serde_yaml::to_string(&entry).expect("serialize");
419 assert!(yaml.contains("group:carlotta.friends"));
420 let round: CapabilityEntry = serde_yaml::from_str(yaml.trim()).expect("deserialize");
421 assert_eq!(round, entry);
422 }
423
424 #[test]
425 fn owner_capability_is_just_a_string() {
426 let acl = m(&[("did:ma:alice", allow(&["owner"]))]);
428 assert!(check_cap(&acl, "did:ma:alice", "owner").is_ok());
429 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_err());
430 }
431
432 #[test]
433 fn normalize_strips_fragment() {
434 assert_eq!(normalize_principal("did:ma:foo#bar"), "did:ma:foo");
435 assert_eq!(normalize_principal("did:ma:foo"), "did:ma:foo");
436 assert_eq!(normalize_principal("#local"), "#local");
437 assert_eq!(normalize_principal("*"), "*");
438 }
439
440 #[test]
441 fn valid_acl_keys() {
442 assert!(is_valid_acl_key("*"));
443 assert!(is_valid_acl_key("did:ma:Qmfoo"));
444 assert!(is_valid_acl_key("#agent"));
445 assert!(is_valid_acl_key("group:alice.venner"));
446 assert!(is_valid_acl_key("group:runtime.admins"));
447 assert!(is_valid_acl_key("fortune"));
449 assert!(is_valid_acl_key("admin"));
450 assert!(is_valid_acl_key("emote"));
451 assert!(!is_valid_acl_key(""));
452 }
453
454 #[cfg(feature = "acl")]
455 #[test]
456 fn capability_serde_roundtrip() {
457 let acl: AclMap = [
458 (
459 "*".to_string(),
460 CapabilityEntry::from_caps(["rpc", "create"]),
461 ),
462 ("did:ma:bandit".to_string(), CapabilityEntry::Deny),
463 ]
464 .into_iter()
465 .collect();
466 let yaml = serde_yaml::to_string(&acl).unwrap();
467 let roundtrip: AclMap = serde_yaml::from_str(&yaml).unwrap();
468 assert_eq!(acl, roundtrip);
469 }
470
471 #[cfg(feature = "acl")]
472 #[test]
473 fn yaml_null_deserializes_to_deny() {
474 let yaml = "'did:ma:x': ~\n'*':\n- rpc\n- create\n";
475 let acl: AclMap = serde_yaml::from_str(yaml).unwrap();
476 assert_eq!(acl.get("did:ma:x"), Some(&CapabilityEntry::Deny));
477 assert_eq!(
478 acl.get("*"),
479 Some(&CapabilityEntry::from_caps(["rpc", "create"]))
480 );
481 }
482}