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
99#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum CapabilityEntry {
108 Deny,
110 Allow(BTreeSet<String>),
112}
113
114impl CapabilityEntry {
115 pub fn from_caps<I, S>(caps: I) -> Self
117 where
118 I: IntoIterator<Item = S>,
119 S: Into<String>,
120 {
121 Self::Allow(caps.into_iter().map(Into::into).collect())
122 }
123
124 pub fn has(&self, cap: &str) -> bool {
127 match self {
128 Self::Deny => false,
129 Self::Allow(caps) => caps.contains(cap) || caps.contains("*"),
130 }
131 }
132
133 pub fn is_deny(&self) -> bool {
135 matches!(self, Self::Deny)
136 }
137}
138
139impl Serialize for CapabilityEntry {
140 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
141 match self {
142 Self::Deny => serializer.serialize_none(),
143 Self::Allow(caps) => {
144 use serde::ser::SerializeSeq;
145 let mut seq = serializer.serialize_seq(Some(caps.len()))?;
146 for cap in caps {
147 seq.serialize_element(cap)?;
148 }
149 seq.end()
150 }
151 }
152 }
153}
154
155impl<'de> Deserialize<'de> for CapabilityEntry {
156 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
157 #[derive(Deserialize)]
158 #[serde(untagged)]
159 enum Raw {
160 #[allow(dead_code)]
161 Str(String),
162 Seq(Vec<String>),
163 }
164 let opt: Option<Raw> = Option::deserialize(deserializer)?;
165 match opt {
166 None => Ok(Self::Deny),
167 Some(Raw::Seq(v)) if v.is_empty() => Ok(Self::Deny),
168 Some(Raw::Seq(v)) => Ok(Self::Allow(v.into_iter().collect())),
169 Some(Raw::Str(_)) => Err(serde::de::Error::custom(
170 "invalid ACL entry: use a YAML sequence for Allow or null for Deny",
171 )),
172 }
173 }
174}
175
176pub type AclMap = HashMap<String, CapabilityEntry>;
189
190#[cfg(feature = "acl")]
204pub fn check_cap(acl: &AclMap, caller: &str, cap: &str) -> Result<()> {
205 let normalized = normalize_principal(caller);
206 if let Some(direct) = acl.get(normalized) {
207 match direct {
208 CapabilityEntry::Deny => {
209 return Err(Error::Acl(format!("operation denied for {caller}")));
210 }
211 CapabilityEntry::Allow(caps) if caps.contains(cap) || caps.contains("*") => {
212 return Ok(());
213 }
214 CapabilityEntry::Allow(_) => {
215 return Err(Error::Acl(format!(
216 "capability '{cap}' denied for {caller}"
217 )));
218 }
219 }
220 }
221
222 match acl.get("*") {
223 None => Err(Error::Acl(format!("no ACL entry for {caller}"))),
224 Some(CapabilityEntry::Deny) => Err(Error::Acl(format!("operation denied for {caller}"))),
225 Some(CapabilityEntry::Allow(caps)) if caps.contains(cap) || caps.contains("*") => Ok(()),
226 Some(CapabilityEntry::Allow(_)) => Err(Error::Acl(format!(
227 "capability '{cap}' denied for {caller}"
228 ))),
229 }
230}
231
232pub fn is_valid_acl_key(key: &str) -> bool {
238 !key.is_empty()
239}
240
241pub fn is_principal_key(key: &str) -> bool {
243 key == "*"
244 || (key.starts_with("did:") && !key.contains('#'))
245 || (key.starts_with('#') && key.len() > 1)
246 || is_valid_group_key(key)
247}
248
249fn is_valid_group_key(key: &str) -> bool {
255 if let Some(rest) = key.strip_prefix(GROUP_PREFIX) {
256 if let Some(dot) = rest.find('.') {
257 let handle = &rest[..dot];
258 let path = &rest[dot + 1..];
259 return !handle.is_empty() && !path.is_empty();
260 }
261 }
262 false
263}
264
265#[cfg(feature = "acl")]
270pub fn validate_acl_map(acl: &AclMap) -> Result<()> {
271 for key in acl.keys() {
272 if !is_valid_acl_key(key) {
273 return Err(Error::Acl(format!(
274 "invalid ACL key {key:?}: key must be non-empty"
275 )));
276 }
277 }
278 Ok(())
279}
280
281pub fn normalize_principal(did: &str) -> &str {
287 if did.starts_with("did:") {
288 if let Some(pos) = did.find('#') {
289 return &did[..pos];
290 }
291 }
292 did
293}
294
295#[cfg(test)]
298mod tests {
299 use super::*;
300
301 fn allow(caps: &[&str]) -> CapabilityEntry {
302 CapabilityEntry::from_caps(caps.iter().copied())
303 }
304
305 fn m(entries: &[(&str, CapabilityEntry)]) -> AclMap {
306 entries
307 .iter()
308 .map(|(k, v)| (k.to_string(), v.clone()))
309 .collect()
310 }
311
312 #[test]
313 fn wildcard_rpc_allows_rpc() {
314 let acl = m(&[("*", allow(&[CAP_RPC]))]);
315 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
316 }
317
318 #[test]
319 fn wildcard_rpc_denies_ipfs() {
320 let acl = m(&[("*", allow(&[CAP_RPC]))]);
321 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_err());
322 }
323
324 #[test]
325 fn explicit_deny_wins_over_wildcard_allow() {
326 let acl = m(&[
327 ("*", allow(&[CAP_RPC, CAP_IPFS])),
328 ("did:ma:bandit", CapabilityEntry::Deny),
329 ]);
330 assert!(check_cap(&acl, "did:ma:bandit", CAP_RPC).is_err());
331 }
332
333 #[test]
334 fn exact_match_restricts_below_wildcard() {
335 let acl = m(&[
336 ("*", allow(&[CAP_RPC, CAP_IPFS])),
337 ("did:ma:bob", allow(&[CAP_RPC])),
338 ]);
339 assert!(check_cap(&acl, "did:ma:bob", CAP_RPC).is_ok());
340 assert!(check_cap(&acl, "did:ma:bob", CAP_IPFS).is_err());
341 }
342
343 #[test]
344 fn did_url_caller_is_normalized() {
345 let acl = m(&[("did:ma:alice", allow(&[CAP_RPC, CAP_IPFS]))]);
346 assert!(check_cap(&acl, "did:ma:alice#sign", CAP_RPC).is_ok());
347 }
348
349 #[test]
350 fn no_entry_default_deny() {
351 assert!(check_cap(&AclMap::new(), "did:ma:anyone", CAP_RPC).is_err());
352 }
353
354 #[test]
355 fn wildcard_deny_blocks_all() {
356 let acl = m(&[("*", CapabilityEntry::Deny)]);
357 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_err());
358 }
359
360 #[test]
361 fn local_entity_key_allowed() {
362 let acl = m(&[("#agent", allow(&[CAP_RPC]))]);
363 assert!(check_cap(&acl, "#agent", CAP_RPC).is_ok());
364 assert!(check_cap(&acl, "#other", CAP_RPC).is_err());
365 }
366
367 #[test]
368 fn arbitrary_capability_works() {
369 let acl = m(&[("did:ma:alice", allow(&["emote", "reply"]))]);
370 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
371 assert!(check_cap(&acl, "did:ma:alice", "reply").is_ok());
372 assert!(check_cap(&acl, "did:ma:alice", "admin").is_err());
373 }
374
375 #[test]
376 fn wildcard_cap_grants_all_capabilities() {
377 let acl = m(&[("did:ma:alice", allow(&["*"]))]);
378 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
379 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
380 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
381 assert!(check_cap(&acl, "did:ma:alice", "admin").is_ok());
382 }
383
384 #[test]
385 fn owner_capability_is_just_a_string() {
386 let acl = m(&[("did:ma:alice", allow(&["owner"]))]);
387 assert!(check_cap(&acl, "did:ma:alice", "owner").is_ok());
388 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_err());
389 }
390
391 #[test]
392 fn normalize_strips_fragment() {
393 assert_eq!(normalize_principal("did:ma:foo#bar"), "did:ma:foo");
394 assert_eq!(normalize_principal("did:ma:foo"), "did:ma:foo");
395 assert_eq!(normalize_principal("#local"), "#local");
396 assert_eq!(normalize_principal("*"), "*");
397 }
398
399 #[test]
400 fn explicit_deny_without_wildcard() {
401 let acl = m(&[("did:ma:bandit", CapabilityEntry::Deny)]);
403 assert!(check_cap(&acl, "did:ma:bandit", CAP_RPC).is_err());
404 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_err());
406 }
407
408 #[test]
409 fn multiple_caps_in_single_entry() {
410 let acl = m(&[("did:ma:alice", allow(&[CAP_RPC, CAP_IPFS, CAP_READ]))]);
411 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
412 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
413 assert!(check_cap(&acl, "did:ma:alice", CAP_READ).is_ok());
414 assert!(check_cap(&acl, "did:ma:alice", CAP_CREATE).is_err());
415 assert!(check_cap(&acl, "did:ma:alice", CAP_DELETE).is_err());
416 }
417
418 #[test]
419 fn direct_entry_restricts_even_when_wildcard_is_broader() {
420 let acl = m(&[
423 ("*", allow(&[CAP_RPC, CAP_IPFS])),
424 ("did:ma:bob", allow(&[CAP_RPC])),
425 ]);
426 assert!(check_cap(&acl, "did:ma:bob", CAP_RPC).is_ok());
427 assert!(check_cap(&acl, "did:ma:bob", CAP_IPFS).is_err());
428 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
430 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
431 }
432
433 #[test]
434 fn group_principal_allowed() {
435 let acl = m(&[("*", allow(&[CAP_RPC]))]);
438 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_ok());
439 }
440
441 #[test]
442 fn valid_acl_keys() {
443 assert!(is_valid_acl_key("*"));
444 assert!(is_valid_acl_key("did:ma:Qmfoo"));
445 assert!(is_valid_acl_key("#agent"));
446 assert!(is_valid_acl_key("+alice.venner"));
447 assert!(is_valid_acl_key("+runtime.admins"));
448 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}