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";
83
84pub const GROUP_PREFIX: &str = "+";
92
93#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum CapabilityEntry {
102 Deny,
104 Allow(BTreeSet<String>),
106}
107
108impl CapabilityEntry {
109 pub fn from_caps<I, S>(caps: I) -> Self
111 where
112 I: IntoIterator<Item = S>,
113 S: Into<String>,
114 {
115 Self::Allow(caps.into_iter().map(Into::into).collect())
116 }
117
118 pub fn has(&self, cap: &str) -> bool {
121 match self {
122 Self::Deny => false,
123 Self::Allow(caps) => caps.contains(cap) || caps.contains("*"),
124 }
125 }
126
127 pub fn is_deny(&self) -> bool {
129 matches!(self, Self::Deny)
130 }
131}
132
133impl Serialize for CapabilityEntry {
134 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
135 match self {
136 Self::Deny => serializer.serialize_none(),
137 Self::Allow(caps) => {
138 use serde::ser::SerializeSeq;
139 let mut seq = serializer.serialize_seq(Some(caps.len()))?;
140 for cap in caps {
141 seq.serialize_element(cap)?;
142 }
143 seq.end()
144 }
145 }
146 }
147}
148
149impl<'de> Deserialize<'de> for CapabilityEntry {
150 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
151 #[derive(Deserialize)]
152 #[serde(untagged)]
153 enum Raw {
154 #[allow(dead_code)]
155 Str(String),
156 Seq(Vec<String>),
157 }
158 let opt: Option<Raw> = Option::deserialize(deserializer)?;
159 match opt {
160 None => Ok(Self::Deny),
161 Some(Raw::Seq(v)) if v.is_empty() => Ok(Self::Deny),
162 Some(Raw::Seq(v)) => Ok(Self::Allow(v.into_iter().collect())),
163 Some(Raw::Str(_)) => Err(serde::de::Error::custom(
164 "invalid ACL entry: use a YAML sequence for Allow or null for Deny",
165 )),
166 }
167 }
168}
169
170pub type AclMap = HashMap<String, CapabilityEntry>;
183
184#[cfg(feature = "acl")]
198pub fn check_cap(acl: &AclMap, caller: &str, cap: &str) -> Result<()> {
199 let normalized = normalize_principal(caller);
200 if let Some(direct) = acl.get(normalized) {
201 match direct {
202 CapabilityEntry::Deny => {
203 return Err(Error::Acl(format!("operation denied for {caller}")));
204 }
205 CapabilityEntry::Allow(caps) if caps.contains(cap) || caps.contains("*") => {
206 return Ok(());
207 }
208 CapabilityEntry::Allow(_) => {
209 return Err(Error::Acl(format!(
210 "capability '{cap}' denied for {caller}"
211 )));
212 }
213 }
214 }
215
216 match acl.get("*") {
217 None => Err(Error::Acl(format!("no ACL entry for {caller}"))),
218 Some(CapabilityEntry::Deny) => Err(Error::Acl(format!("operation denied for {caller}"))),
219 Some(CapabilityEntry::Allow(caps)) if caps.contains(cap) || caps.contains("*") => Ok(()),
220 Some(CapabilityEntry::Allow(_)) => Err(Error::Acl(format!(
221 "capability '{cap}' denied for {caller}"
222 ))),
223 }
224}
225
226pub fn is_valid_acl_key(key: &str) -> bool {
232 !key.is_empty()
233}
234
235pub fn is_principal_key(key: &str) -> bool {
237 key == "*"
238 || (key.starts_with("did:") && !key.contains('#'))
239 || (key.starts_with('#') && key.len() > 1)
240 || is_valid_group_key(key)
241}
242
243fn is_valid_group_key(key: &str) -> bool {
249 if let Some(rest) = key.strip_prefix(GROUP_PREFIX) {
250 if let Some(dot) = rest.find('.') {
251 let handle = &rest[..dot];
252 let path = &rest[dot + 1..];
253 return !handle.is_empty() && !path.is_empty();
254 }
255 }
256 false
257}
258
259#[cfg(feature = "acl")]
264pub fn validate_acl_map(acl: &AclMap) -> Result<()> {
265 for key in acl.keys() {
266 if !is_valid_acl_key(key) {
267 return Err(Error::Acl(format!(
268 "invalid ACL key {key:?}: key must be non-empty"
269 )));
270 }
271 }
272 Ok(())
273}
274
275pub fn normalize_principal(did: &str) -> &str {
281 if did.starts_with("did:") {
282 if let Some(pos) = did.find('#') {
283 return &did[..pos];
284 }
285 }
286 did
287}
288
289#[cfg(test)]
292mod tests {
293 use super::*;
294
295 fn allow(caps: &[&str]) -> CapabilityEntry {
296 CapabilityEntry::from_caps(caps.iter().copied())
297 }
298
299 fn m(entries: &[(&str, CapabilityEntry)]) -> AclMap {
300 entries
301 .iter()
302 .map(|(k, v)| (k.to_string(), v.clone()))
303 .collect()
304 }
305
306 #[test]
307 fn wildcard_rpc_allows_rpc() {
308 let acl = m(&[("*", allow(&[CAP_RPC]))]);
309 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
310 }
311
312 #[test]
313 fn wildcard_rpc_denies_ipfs() {
314 let acl = m(&[("*", allow(&[CAP_RPC]))]);
315 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_err());
316 }
317
318 #[test]
319 fn explicit_deny_wins_over_wildcard_allow() {
320 let acl = m(&[
321 ("*", allow(&[CAP_RPC, CAP_IPFS])),
322 ("did:ma:bandit", CapabilityEntry::Deny),
323 ]);
324 assert!(check_cap(&acl, "did:ma:bandit", CAP_RPC).is_err());
325 }
326
327 #[test]
328 fn exact_match_restricts_below_wildcard() {
329 let acl = m(&[
330 ("*", allow(&[CAP_RPC, CAP_IPFS])),
331 ("did:ma:bob", allow(&[CAP_RPC])),
332 ]);
333 assert!(check_cap(&acl, "did:ma:bob", CAP_RPC).is_ok());
334 assert!(check_cap(&acl, "did:ma:bob", CAP_IPFS).is_err());
335 }
336
337 #[test]
338 fn did_url_caller_is_normalized() {
339 let acl = m(&[("did:ma:alice", allow(&[CAP_RPC, CAP_IPFS]))]);
340 assert!(check_cap(&acl, "did:ma:alice#sign", CAP_RPC).is_ok());
341 }
342
343 #[test]
344 fn no_entry_default_deny() {
345 assert!(check_cap(&AclMap::new(), "did:ma:anyone", CAP_RPC).is_err());
346 }
347
348 #[test]
349 fn wildcard_deny_blocks_all() {
350 let acl = m(&[("*", CapabilityEntry::Deny)]);
351 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_err());
352 }
353
354 #[test]
355 fn local_entity_key_allowed() {
356 let acl = m(&[("#agent", allow(&[CAP_RPC]))]);
357 assert!(check_cap(&acl, "#agent", CAP_RPC).is_ok());
358 assert!(check_cap(&acl, "#other", CAP_RPC).is_err());
359 }
360
361 #[test]
362 fn arbitrary_capability_works() {
363 let acl = m(&[("did:ma:alice", allow(&["emote", "reply"]))]);
364 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
365 assert!(check_cap(&acl, "did:ma:alice", "reply").is_ok());
366 assert!(check_cap(&acl, "did:ma:alice", "admin").is_err());
367 }
368
369 #[test]
370 fn wildcard_cap_grants_all_capabilities() {
371 let acl = m(&[("did:ma:alice", allow(&["*"]))]);
372 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
373 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
374 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
375 assert!(check_cap(&acl, "did:ma:alice", "admin").is_ok());
376 }
377
378 #[test]
379 fn owner_capability_is_just_a_string() {
380 let acl = m(&[("did:ma:alice", allow(&["owner"]))]);
381 assert!(check_cap(&acl, "did:ma:alice", "owner").is_ok());
382 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_err());
383 }
384
385 #[test]
386 fn normalize_strips_fragment() {
387 assert_eq!(normalize_principal("did:ma:foo#bar"), "did:ma:foo");
388 assert_eq!(normalize_principal("did:ma:foo"), "did:ma:foo");
389 assert_eq!(normalize_principal("#local"), "#local");
390 assert_eq!(normalize_principal("*"), "*");
391 }
392
393 #[test]
394 fn explicit_deny_without_wildcard() {
395 let acl = m(&[("did:ma:bandit", CapabilityEntry::Deny)]);
397 assert!(check_cap(&acl, "did:ma:bandit", CAP_RPC).is_err());
398 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_err());
400 }
401
402 #[test]
403 fn multiple_caps_in_single_entry() {
404 let acl = m(&[("did:ma:alice", allow(&[CAP_RPC, CAP_IPFS, CAP_READ]))]);
405 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
406 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
407 assert!(check_cap(&acl, "did:ma:alice", CAP_READ).is_ok());
408 assert!(check_cap(&acl, "did:ma:alice", CAP_CREATE).is_err());
409 assert!(check_cap(&acl, "did:ma:alice", CAP_DELETE).is_err());
410 }
411
412 #[test]
413 fn direct_entry_restricts_even_when_wildcard_is_broader() {
414 let acl = m(&[
417 ("*", allow(&[CAP_RPC, CAP_IPFS])),
418 ("did:ma:bob", allow(&[CAP_RPC])),
419 ]);
420 assert!(check_cap(&acl, "did:ma:bob", CAP_RPC).is_ok());
421 assert!(check_cap(&acl, "did:ma:bob", CAP_IPFS).is_err());
422 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
424 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
425 }
426
427 #[test]
428 fn group_principal_allowed() {
429 let acl = m(&[("*", allow(&[CAP_RPC]))]);
432 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_ok());
433 }
434
435 #[test]
436 fn valid_acl_keys() {
437 assert!(is_valid_acl_key("*"));
438 assert!(is_valid_acl_key("did:ma:Qmfoo"));
439 assert!(is_valid_acl_key("#agent"));
440 assert!(is_valid_acl_key("+alice.venner"));
441 assert!(is_valid_acl_key("+runtime.admins"));
442 assert!(is_valid_acl_key("fortune"));
443 assert!(is_valid_acl_key("admin"));
444 assert!(is_valid_acl_key("emote"));
445 assert!(!is_valid_acl_key(""));
446 }
447
448 #[cfg(feature = "acl")]
449 #[test]
450 fn capability_serde_roundtrip() {
451 let acl: AclMap = [
452 (
453 "*".to_string(),
454 CapabilityEntry::from_caps(["rpc", "create"]),
455 ),
456 ("did:ma:bandit".to_string(), CapabilityEntry::Deny),
457 ]
458 .into_iter()
459 .collect();
460 let yaml = serde_yaml::to_string(&acl).unwrap();
461 let roundtrip: AclMap = serde_yaml::from_str(&yaml).unwrap();
462 assert_eq!(acl, roundtrip);
463 }
464
465 #[cfg(feature = "acl")]
466 #[test]
467 fn yaml_null_deserializes_to_deny() {
468 let yaml = "'did:ma:x': ~\n'*':\n- rpc\n- create\n";
469 let acl: AclMap = serde_yaml::from_str(yaml).unwrap();
470 assert_eq!(acl.get("did:ma:x"), Some(&CapabilityEntry::Deny));
471 assert_eq!(
472 acl.get("*"),
473 Some(&CapabilityEntry::from_caps(["rpc", "create"]))
474 );
475 }
476}