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