1use std::collections::{BTreeSet, HashMap};
59
60use serde::{Deserialize, Deserializer, Serialize, Serializer};
61
62#[cfg(feature = "acl")]
63use crate::{Error, Result};
64
65pub const CAP_INBOX: &str = "inbox";
69pub const CAP_RPC: &str = "rpc";
71pub const CAP_IPFS: &str = "ipfs";
73pub const CAP_CRUD: &str = "crud";
75pub const CAP_READ: &str = "read";
77pub const CAP_CREATE: &str = "create";
79pub const CAP_UPDATE: &str = "update";
81pub const CAP_DELETE: &str = "delete";
83pub const CAP_ACL: &str = "acl";
89
90pub const GROUP_PREFIX: &str = "+";
98
99pub const LOCAL_ENTITY_WILDCARD: &str = "#";
125
126#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum CapabilityEntry {
135 Deny,
137 Allow(BTreeSet<String>),
139}
140
141impl CapabilityEntry {
142 pub fn from_caps<I, S>(caps: I) -> Self
144 where
145 I: IntoIterator<Item = S>,
146 S: Into<String>,
147 {
148 Self::Allow(caps.into_iter().map(Into::into).collect())
149 }
150
151 pub fn has(&self, cap: &str) -> bool {
154 match self {
155 Self::Deny => false,
156 Self::Allow(caps) => caps.contains(cap) || caps.contains("*"),
157 }
158 }
159
160 pub fn is_deny(&self) -> bool {
162 matches!(self, Self::Deny)
163 }
164}
165
166impl Serialize for CapabilityEntry {
167 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
168 match self {
169 Self::Deny => serializer.serialize_none(),
170 Self::Allow(caps) => {
171 use serde::ser::SerializeSeq;
172 let mut seq = serializer.serialize_seq(Some(caps.len()))?;
173 for cap in caps {
174 seq.serialize_element(cap)?;
175 }
176 seq.end()
177 }
178 }
179 }
180}
181
182impl<'de> Deserialize<'de> for CapabilityEntry {
183 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
184 #[derive(Deserialize)]
185 #[serde(untagged)]
186 enum Raw {
187 #[allow(dead_code)]
188 Str(String),
189 Seq(Vec<String>),
190 }
191 let opt: Option<Raw> = Option::deserialize(deserializer)?;
192 match opt {
193 None => Ok(Self::Deny),
194 Some(Raw::Seq(v)) if v.is_empty() => Ok(Self::Deny),
195 Some(Raw::Seq(v)) => Ok(Self::Allow(v.into_iter().collect())),
196 Some(Raw::Str(_)) => Err(serde::de::Error::custom(
197 "invalid ACL entry: use a YAML sequence for Allow or null for Deny",
198 )),
199 }
200 }
201}
202
203pub type AclMap = HashMap<String, CapabilityEntry>;
216
217#[cfg(feature = "acl")]
231pub fn check_cap(acl: &AclMap, caller: &str, cap: &str) -> Result<()> {
232 let normalized = normalize_principal(caller);
233
234 if let Some(direct) = acl.get(normalized) {
236 return match direct {
237 CapabilityEntry::Deny => Err(Error::Acl(format!("operation denied for {caller}"))),
238 CapabilityEntry::Allow(caps) if caps.contains(cap) || caps.contains("*") => Ok(()),
239 CapabilityEntry::Allow(_) => Err(Error::Acl(format!(
240 "capability '{cap}' denied for {caller}"
241 ))),
242 };
243 }
244
245 if normalized.starts_with('#') {
247 if let Some(local_wild) = acl.get(LOCAL_ENTITY_WILDCARD) {
248 return match local_wild {
249 CapabilityEntry::Deny => Err(Error::Acl(format!("operation denied for {caller}"))),
250 CapabilityEntry::Allow(caps) if caps.contains(cap) || caps.contains("*") => Ok(()),
251 CapabilityEntry::Allow(_) => Err(Error::Acl(format!(
252 "capability '{cap}' denied for {caller}"
253 ))),
254 };
255 }
256 }
257
258 match acl.get("*") {
260 None => Err(Error::Acl(format!("no ACL entry for {caller}"))),
261 Some(CapabilityEntry::Deny) => Err(Error::Acl(format!("operation denied for {caller}"))),
262 Some(CapabilityEntry::Allow(caps)) if caps.contains(cap) || caps.contains("*") => Ok(()),
263 Some(CapabilityEntry::Allow(_)) => Err(Error::Acl(format!(
264 "capability '{cap}' denied for {caller}"
265 ))),
266 }
267}
268
269pub fn is_valid_acl_key(key: &str) -> bool {
275 !key.is_empty()
276}
277
278pub fn is_principal_key(key: &str) -> bool {
287 key == "*"
288 || (key.starts_with("did:") && !key.contains('#'))
289 || key.starts_with('#') || is_valid_group_key(key)
291}
292
293fn is_valid_group_key(key: &str) -> bool {
299 if let Some(rest) = key.strip_prefix(GROUP_PREFIX) {
300 if let Some(dot) = rest.find('.') {
301 let handle = &rest[..dot];
302 let path = &rest[dot + 1..];
303 return !handle.is_empty() && !path.is_empty();
304 }
305 }
306 false
307}
308
309#[cfg(feature = "acl")]
314pub fn validate_acl_map(acl: &AclMap) -> Result<()> {
315 for key in acl.keys() {
316 if !is_valid_acl_key(key) {
317 return Err(Error::Acl(format!(
318 "invalid ACL key {key:?}: key must be non-empty"
319 )));
320 }
321 }
322 Ok(())
323}
324
325pub fn normalize_principal(did: &str) -> &str {
338 if did.starts_with("did:") {
339 if let Some(pos) = did.find('#') {
340 return &did[..pos];
341 }
342 }
343 did
344}
345
346#[cfg(test)]
349mod tests {
350 use super::*;
351
352 fn allow(caps: &[&str]) -> CapabilityEntry {
353 CapabilityEntry::from_caps(caps.iter().copied())
354 }
355
356 fn m(entries: &[(&str, CapabilityEntry)]) -> AclMap {
357 entries
358 .iter()
359 .map(|(k, v)| (k.to_string(), v.clone()))
360 .collect()
361 }
362
363 #[test]
364 fn wildcard_rpc_allows_rpc() {
365 let acl = m(&[("*", allow(&[CAP_RPC]))]);
366 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
367 }
368
369 #[test]
370 fn wildcard_rpc_denies_ipfs() {
371 let acl = m(&[("*", allow(&[CAP_RPC]))]);
372 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_err());
373 }
374
375 #[test]
376 fn explicit_deny_wins_over_wildcard_allow() {
377 let acl = m(&[
378 ("*", allow(&[CAP_RPC, CAP_IPFS])),
379 ("did:ma:bandit", CapabilityEntry::Deny),
380 ]);
381 assert!(check_cap(&acl, "did:ma:bandit", CAP_RPC).is_err());
382 }
383
384 #[test]
385 fn exact_match_restricts_below_wildcard() {
386 let acl = m(&[
387 ("*", allow(&[CAP_RPC, CAP_IPFS])),
388 ("did:ma:bob", allow(&[CAP_RPC])),
389 ]);
390 assert!(check_cap(&acl, "did:ma:bob", CAP_RPC).is_ok());
391 assert!(check_cap(&acl, "did:ma:bob", CAP_IPFS).is_err());
392 }
393
394 #[test]
395 fn did_url_caller_is_normalized() {
396 let acl = m(&[("did:ma:alice", allow(&[CAP_RPC, CAP_IPFS]))]);
397 assert!(check_cap(&acl, "did:ma:alice#sign", CAP_RPC).is_ok());
398 }
399
400 #[test]
401 fn no_entry_default_deny() {
402 assert!(check_cap(&AclMap::new(), "did:ma:anyone", CAP_RPC).is_err());
403 }
404
405 #[test]
406 fn wildcard_deny_blocks_all() {
407 let acl = m(&[("*", CapabilityEntry::Deny)]);
408 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_err());
409 }
410
411 #[test]
412 fn local_entity_key_allowed() {
413 let acl = m(&[("#agent", allow(&[CAP_RPC]))]);
414 assert!(check_cap(&acl, "#agent", CAP_RPC).is_ok());
415 assert!(check_cap(&acl, "#other", CAP_RPC).is_err());
416 }
417
418 #[test]
419 fn arbitrary_capability_works() {
420 let acl = m(&[("did:ma:alice", allow(&["emote", "reply"]))]);
421 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
422 assert!(check_cap(&acl, "did:ma:alice", "reply").is_ok());
423 assert!(check_cap(&acl, "did:ma:alice", "admin").is_err());
424 }
425
426 #[test]
427 fn wildcard_cap_grants_all_capabilities() {
428 let acl = m(&[("did:ma:alice", allow(&["*"]))]);
429 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
430 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
431 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
432 assert!(check_cap(&acl, "did:ma:alice", "admin").is_ok());
433 }
434
435 #[test]
436 fn owner_capability_is_just_a_string() {
437 let acl = m(&[("did:ma:alice", allow(&["owner"]))]);
438 assert!(check_cap(&acl, "did:ma:alice", "owner").is_ok());
439 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_err());
440 }
441
442 #[test]
443 fn normalize_strips_fragment() {
444 assert_eq!(normalize_principal("did:ma:foo#bar"), "did:ma:foo");
445 assert_eq!(normalize_principal("did:ma:foo"), "did:ma:foo");
446 assert_eq!(normalize_principal("#local"), "#local");
447 assert_eq!(normalize_principal("#"), "#");
448 assert_eq!(normalize_principal("*"), "*");
449 }
450
451 #[test]
454 fn local_wildcard_allows_any_hash_prefixed_caller() {
455 let acl = m(&[("#", allow(&[CAP_RPC]))]);
457 assert!(check_cap(&acl, "#fortune", CAP_RPC).is_ok());
458 assert!(check_cap(&acl, "#scheduler", CAP_RPC).is_ok());
459 assert!(check_cap(&acl, "#any_entity", CAP_RPC).is_ok());
460 }
461
462 #[test]
463 fn local_wildcard_does_not_match_remote_callers() {
464 let acl = m(&[("#", allow(&[CAP_RPC]))]);
466 assert!(check_cap(&acl, "did:ma:remote", CAP_RPC).is_err());
467 }
468
469 #[test]
470 fn specific_local_entity_wins_over_local_wildcard() {
471 let acl = m(&[
473 ("#", allow(&[CAP_RPC, CAP_IPFS])),
474 ("#fortune", allow(&[CAP_RPC])), ]);
476 assert!(check_cap(&acl, "#fortune", CAP_RPC).is_ok());
477 assert!(check_cap(&acl, "#fortune", CAP_IPFS).is_err());
478 assert!(check_cap(&acl, "#other", CAP_IPFS).is_ok());
480 }
481
482 #[test]
483 fn local_wildcard_deny_blocks_all_local_entities() {
484 let acl = m(&[
485 ("#", CapabilityEntry::Deny),
486 ("*", allow(&[CAP_RPC])), ]);
488 assert!(check_cap(&acl, "#fortune", CAP_RPC).is_err());
489 assert!(check_cap(&acl, "#any", CAP_RPC).is_err());
490 assert!(check_cap(&acl, "did:ma:remote", CAP_RPC).is_ok());
492 }
493
494 #[test]
495 fn specific_local_entity_allow_overrides_local_wildcard_deny() {
496 let acl = m(&[
498 ("#", CapabilityEntry::Deny),
499 ("#fortune", allow(&[CAP_RPC])),
500 ]);
501 assert!(check_cap(&acl, "#fortune", CAP_RPC).is_ok());
502 assert!(check_cap(&acl, "#other", CAP_RPC).is_err());
503 }
504
505 #[test]
506 fn global_wildcard_not_triggered_for_hash_caller_when_local_wildcard_present() {
507 let acl = m(&[("#", CapabilityEntry::Deny), ("*", allow(&[CAP_RPC]))]);
509 assert!(check_cap(&acl, "#fortune", CAP_RPC).is_err());
511 }
512
513 #[test]
514 fn local_wildcard_is_key_form_valid() {
515 assert!(is_principal_key("#"));
516 assert!(is_principal_key("#fortune"));
517 assert!(is_principal_key("*"));
518 assert!(is_principal_key("did:ma:alice"));
519 }
520
521 #[test]
522 fn explicit_deny_without_wildcard() {
523 let acl = m(&[("did:ma:bandit", CapabilityEntry::Deny)]);
525 assert!(check_cap(&acl, "did:ma:bandit", CAP_RPC).is_err());
526 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_err());
528 }
529
530 #[test]
531 fn multiple_caps_in_single_entry() {
532 let acl = m(&[("did:ma:alice", allow(&[CAP_RPC, CAP_IPFS, CAP_READ]))]);
533 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
534 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
535 assert!(check_cap(&acl, "did:ma:alice", CAP_READ).is_ok());
536 assert!(check_cap(&acl, "did:ma:alice", CAP_CREATE).is_err());
537 assert!(check_cap(&acl, "did:ma:alice", CAP_DELETE).is_err());
538 }
539
540 #[test]
541 fn direct_entry_restricts_even_when_wildcard_is_broader() {
542 let acl = m(&[
545 ("*", allow(&[CAP_RPC, CAP_IPFS])),
546 ("did:ma:bob", allow(&[CAP_RPC])),
547 ]);
548 assert!(check_cap(&acl, "did:ma:bob", CAP_RPC).is_ok());
549 assert!(check_cap(&acl, "did:ma:bob", CAP_IPFS).is_err());
550 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
552 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
553 }
554
555 #[test]
556 fn group_principal_allowed() {
557 let acl = m(&[("*", allow(&[CAP_RPC]))]);
560 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_ok());
561 }
562
563 #[test]
564 fn valid_acl_keys() {
565 assert!(is_valid_acl_key("*"));
566 assert!(is_valid_acl_key("did:ma:Qmfoo"));
567 assert!(is_valid_acl_key("#agent"));
568 assert!(is_valid_acl_key("+alice.venner"));
569 assert!(is_valid_acl_key("+runtime.admins"));
570 assert!(is_valid_acl_key("fortune"));
571 assert!(is_valid_acl_key("admin"));
572 assert!(is_valid_acl_key("emote"));
573 assert!(!is_valid_acl_key(""));
574 }
575
576 #[cfg(feature = "acl")]
577 #[test]
578 fn capability_serde_roundtrip() {
579 let acl: AclMap = [
580 (
581 "*".to_string(),
582 CapabilityEntry::from_caps(["rpc", "create"]),
583 ),
584 ("did:ma:bandit".to_string(), CapabilityEntry::Deny),
585 ]
586 .into_iter()
587 .collect();
588 let yaml = serde_yaml::to_string(&acl).unwrap();
589 let roundtrip: AclMap = serde_yaml::from_str(&yaml).unwrap();
590 assert_eq!(acl, roundtrip);
591 }
592
593 #[cfg(feature = "acl")]
594 #[test]
595 fn yaml_null_deserializes_to_deny() {
596 let yaml = "'did:ma:x': ~\n'*':\n- rpc\n- create\n";
597 let acl: AclMap = serde_yaml::from_str(yaml).unwrap();
598 assert_eq!(acl.get("did:ma:x"), Some(&CapabilityEntry::Deny));
599 assert_eq!(
600 acl.get("*"),
601 Some(&CapabilityEntry::from_caps(["rpc", "create"]))
602 );
603 }
604}