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")]
234fn apply_entry(entry: &CapabilityEntry, cap: &str, caller: &str) -> Result<()> {
235 match entry {
236 CapabilityEntry::Deny => Err(Error::Acl(format!("operation denied for {caller}"))),
237 CapabilityEntry::Allow(caps) if caps.contains(cap) || caps.contains("*") => Ok(()),
238 CapabilityEntry::Allow(_) => Err(Error::Acl(format!(
239 "capability '{cap}' denied for {caller}"
240 ))),
241 }
242}
243
244#[cfg(feature = "acl")]
245pub fn check_cap(acl: &AclMap, caller: &str, cap: &str) -> Result<()> {
246 let normalized = normalize_principal(caller);
247
248 if let Some(direct) = acl.get(normalized) {
250 return apply_entry(direct, cap, caller);
251 }
252
253 if normalized.starts_with('#') {
255 if let Some(local_wild) = acl.get(LOCAL_ENTITY_WILDCARD) {
256 return apply_entry(local_wild, cap, caller);
257 }
258 }
259
260 match acl.get("*") {
262 None => Err(Error::Acl(format!("no ACL entry for {caller}"))),
263 Some(entry) => apply_entry(entry, cap, caller),
264 }
265}
266
267pub fn is_valid_acl_key(key: &str) -> bool {
273 !key.is_empty()
274}
275
276pub fn is_principal_key(key: &str) -> bool {
285 key == "*"
286 || (key.starts_with("did:") && !key.contains('#'))
287 || key.starts_with('#') || is_valid_group_key(key)
289}
290
291fn is_valid_group_key(key: &str) -> bool {
297 if let Some(rest) = key.strip_prefix(GROUP_PREFIX) {
298 if let Some(dot) = rest.find('.') {
299 let handle = &rest[..dot];
300 let path = &rest[dot + 1..];
301 return !handle.is_empty() && !path.is_empty();
302 }
303 }
304 false
305}
306
307#[cfg(feature = "acl")]
312pub fn validate_acl_map(acl: &AclMap) -> Result<()> {
313 for key in acl.keys() {
314 if !is_valid_acl_key(key) {
315 return Err(Error::Acl(format!(
316 "invalid ACL key {key:?}: key must be non-empty"
317 )));
318 }
319 }
320 Ok(())
321}
322
323pub fn normalize_principal(did: &str) -> &str {
336 if did.starts_with("did:") {
337 if let Some(pos) = did.find('#') {
338 return &did[..pos];
339 }
340 }
341 did
342}
343
344#[cfg(test)]
347mod tests {
348 use super::*;
349
350 fn allow(caps: &[&str]) -> CapabilityEntry {
351 CapabilityEntry::from_caps(caps.iter().copied())
352 }
353
354 fn m(entries: &[(&str, CapabilityEntry)]) -> AclMap {
355 entries
356 .iter()
357 .map(|(k, v)| (k.to_string(), v.clone()))
358 .collect()
359 }
360
361 #[test]
362 fn wildcard_rpc_allows_rpc() {
363 let acl = m(&[("*", allow(&[CAP_RPC]))]);
364 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
365 }
366
367 #[test]
368 fn wildcard_rpc_denies_ipfs() {
369 let acl = m(&[("*", allow(&[CAP_RPC]))]);
370 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_err());
371 }
372
373 #[test]
374 fn explicit_deny_wins_over_wildcard_allow() {
375 let acl = m(&[
376 ("*", allow(&[CAP_RPC, CAP_IPFS])),
377 ("did:ma:bandit", CapabilityEntry::Deny),
378 ]);
379 assert!(check_cap(&acl, "did:ma:bandit", CAP_RPC).is_err());
380 }
381
382 #[test]
383 fn exact_match_restricts_below_wildcard() {
384 let acl = m(&[
385 ("*", allow(&[CAP_RPC, CAP_IPFS])),
386 ("did:ma:bob", allow(&[CAP_RPC])),
387 ]);
388 assert!(check_cap(&acl, "did:ma:bob", CAP_RPC).is_ok());
389 assert!(check_cap(&acl, "did:ma:bob", CAP_IPFS).is_err());
390 }
391
392 #[test]
393 fn did_url_caller_is_normalized() {
394 let acl = m(&[("did:ma:alice", allow(&[CAP_RPC, CAP_IPFS]))]);
395 assert!(check_cap(&acl, "did:ma:alice#sign", CAP_RPC).is_ok());
396 }
397
398 #[test]
399 fn no_entry_default_deny() {
400 assert!(check_cap(&AclMap::new(), "did:ma:anyone", CAP_RPC).is_err());
401 }
402
403 #[test]
404 fn wildcard_deny_blocks_all() {
405 let acl = m(&[("*", CapabilityEntry::Deny)]);
406 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_err());
407 }
408
409 #[test]
410 fn local_entity_key_allowed() {
411 let acl = m(&[("#agent", allow(&[CAP_RPC]))]);
412 assert!(check_cap(&acl, "#agent", CAP_RPC).is_ok());
413 assert!(check_cap(&acl, "#other", CAP_RPC).is_err());
414 }
415
416 #[test]
417 fn arbitrary_capability_works() {
418 let acl = m(&[("did:ma:alice", allow(&["emote", "reply"]))]);
419 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
420 assert!(check_cap(&acl, "did:ma:alice", "reply").is_ok());
421 assert!(check_cap(&acl, "did:ma:alice", "admin").is_err());
422 }
423
424 #[test]
425 fn wildcard_cap_grants_all_capabilities() {
426 let acl = m(&[("did:ma:alice", allow(&["*"]))]);
427 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
428 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
429 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
430 assert!(check_cap(&acl, "did:ma:alice", "admin").is_ok());
431 }
432
433 #[test]
434 fn owner_capability_is_just_a_string() {
435 let acl = m(&[("did:ma:alice", allow(&["owner"]))]);
436 assert!(check_cap(&acl, "did:ma:alice", "owner").is_ok());
437 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_err());
438 }
439
440 #[test]
441 fn normalize_strips_fragment() {
442 assert_eq!(normalize_principal("did:ma:foo#bar"), "did:ma:foo");
443 assert_eq!(normalize_principal("did:ma:foo"), "did:ma:foo");
444 assert_eq!(normalize_principal("#local"), "#local");
445 assert_eq!(normalize_principal("#"), "#");
446 assert_eq!(normalize_principal("*"), "*");
447 }
448
449 #[test]
452 fn local_wildcard_allows_any_hash_prefixed_caller() {
453 let acl = m(&[("#", allow(&[CAP_RPC]))]);
455 assert!(check_cap(&acl, "#fortune", CAP_RPC).is_ok());
456 assert!(check_cap(&acl, "#scheduler", CAP_RPC).is_ok());
457 assert!(check_cap(&acl, "#any_entity", CAP_RPC).is_ok());
458 }
459
460 #[test]
461 fn local_wildcard_does_not_match_remote_callers() {
462 let acl = m(&[("#", allow(&[CAP_RPC]))]);
464 assert!(check_cap(&acl, "did:ma:remote", CAP_RPC).is_err());
465 }
466
467 #[test]
468 fn specific_local_entity_wins_over_local_wildcard() {
469 let acl = m(&[
471 ("#", allow(&[CAP_RPC, CAP_IPFS])),
472 ("#fortune", allow(&[CAP_RPC])), ]);
474 assert!(check_cap(&acl, "#fortune", CAP_RPC).is_ok());
475 assert!(check_cap(&acl, "#fortune", CAP_IPFS).is_err());
476 assert!(check_cap(&acl, "#other", CAP_IPFS).is_ok());
478 }
479
480 #[test]
481 fn local_wildcard_deny_blocks_all_local_entities() {
482 let acl = m(&[
483 ("#", CapabilityEntry::Deny),
484 ("*", allow(&[CAP_RPC])), ]);
486 assert!(check_cap(&acl, "#fortune", CAP_RPC).is_err());
487 assert!(check_cap(&acl, "#any", CAP_RPC).is_err());
488 assert!(check_cap(&acl, "did:ma:remote", CAP_RPC).is_ok());
490 }
491
492 #[test]
493 fn specific_local_entity_allow_overrides_local_wildcard_deny() {
494 let acl = m(&[
496 ("#", CapabilityEntry::Deny),
497 ("#fortune", allow(&[CAP_RPC])),
498 ]);
499 assert!(check_cap(&acl, "#fortune", CAP_RPC).is_ok());
500 assert!(check_cap(&acl, "#other", CAP_RPC).is_err());
501 }
502
503 #[test]
504 fn global_wildcard_not_triggered_for_hash_caller_when_local_wildcard_present() {
505 let acl = m(&[("#", CapabilityEntry::Deny), ("*", allow(&[CAP_RPC]))]);
507 assert!(check_cap(&acl, "#fortune", CAP_RPC).is_err());
509 }
510
511 #[test]
512 fn local_wildcard_is_key_form_valid() {
513 assert!(is_principal_key("#"));
514 assert!(is_principal_key("#fortune"));
515 assert!(is_principal_key("*"));
516 assert!(is_principal_key("did:ma:alice"));
517 }
518
519 #[test]
520 fn explicit_deny_without_wildcard() {
521 let acl = m(&[("did:ma:bandit", CapabilityEntry::Deny)]);
523 assert!(check_cap(&acl, "did:ma:bandit", CAP_RPC).is_err());
524 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_err());
526 }
527
528 #[test]
529 fn multiple_caps_in_single_entry() {
530 let acl = m(&[("did:ma:alice", allow(&[CAP_RPC, CAP_IPFS, CAP_READ]))]);
531 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
532 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
533 assert!(check_cap(&acl, "did:ma:alice", CAP_READ).is_ok());
534 assert!(check_cap(&acl, "did:ma:alice", CAP_CREATE).is_err());
535 assert!(check_cap(&acl, "did:ma:alice", CAP_DELETE).is_err());
536 }
537
538 #[test]
539 fn direct_entry_restricts_even_when_wildcard_is_broader() {
540 let acl = m(&[
543 ("*", allow(&[CAP_RPC, CAP_IPFS])),
544 ("did:ma:bob", allow(&[CAP_RPC])),
545 ]);
546 assert!(check_cap(&acl, "did:ma:bob", CAP_RPC).is_ok());
547 assert!(check_cap(&acl, "did:ma:bob", CAP_IPFS).is_err());
548 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
550 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
551 }
552
553 #[test]
554 fn group_principal_allowed() {
555 let acl = m(&[("*", allow(&[CAP_RPC]))]);
558 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_ok());
559 }
560
561 #[test]
562 fn valid_acl_keys() {
563 assert!(is_valid_acl_key("*"));
564 assert!(is_valid_acl_key("did:ma:Qmfoo"));
565 assert!(is_valid_acl_key("#agent"));
566 assert!(is_valid_acl_key("+alice.venner"));
567 assert!(is_valid_acl_key("+runtime.admins"));
568 assert!(is_valid_acl_key("fortune"));
569 assert!(is_valid_acl_key("admin"));
570 assert!(is_valid_acl_key("emote"));
571 assert!(!is_valid_acl_key(""));
572 }
573
574 #[cfg(feature = "acl")]
575 #[test]
576 fn capability_serde_roundtrip() {
577 let acl: AclMap = [
578 (
579 "*".to_string(),
580 CapabilityEntry::from_caps(["rpc", "create"]),
581 ),
582 ("did:ma:bandit".to_string(), CapabilityEntry::Deny),
583 ]
584 .into_iter()
585 .collect();
586 let yaml = serde_yaml::to_string(&acl).unwrap();
587 let roundtrip: AclMap = serde_yaml::from_str(&yaml).unwrap();
588 assert_eq!(acl, roundtrip);
589 }
590
591 #[cfg(feature = "acl")]
592 #[test]
593 fn yaml_null_deserializes_to_deny() {
594 let yaml = "'did:ma:x': ~\n'*':\n- rpc\n- create\n";
595 let acl: AclMap = serde_yaml::from_str(yaml).unwrap();
596 assert_eq!(acl.get("did:ma:x"), Some(&CapabilityEntry::Deny));
597 assert_eq!(
598 acl.get("*"),
599 Some(&CapabilityEntry::from_caps(["rpc", "create"]))
600 );
601 }
602}