1use std::collections::{BTreeSet, HashMap};
65
66use serde::{Deserialize, Deserializer, Serialize, Serializer};
67
68#[cfg(feature = "acl")]
69use crate::{Error, Result};
70
71pub const CAP_RPC: &str = "rpc";
75pub const CAP_IPFS: &str = "ipfs";
77pub const CAP_READ: &str = "read";
79pub const CAP_CREATE: &str = "create";
81pub const CAP_UPDATE: &str = "update";
83pub const CAP_DELETE: &str = "delete";
85
86#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum CapabilityEntry {
97 Deny,
99 Allow(BTreeSet<String>),
101 Grant(Vec<String>),
105}
106
107impl CapabilityEntry {
108 pub fn from_caps<I, S>(caps: I) -> Self
110 where
111 I: IntoIterator<Item = S>,
112 S: Into<String>,
113 {
114 Self::Allow(caps.into_iter().map(Into::into).collect())
115 }
116
117 pub fn has(&self, cap: &str) -> bool {
120 match self {
121 Self::Deny | Self::Grant(_) => false,
122 Self::Allow(caps) => caps.contains(cap) || caps.contains("*"),
123 }
124 }
125
126 pub fn is_deny(&self) -> bool {
128 matches!(self, Self::Deny)
129 }
130
131 pub fn grantees(&self) -> Option<&[String]> {
133 if let Self::Grant(refs) = self {
134 Some(refs)
135 } else {
136 None
137 }
138 }
139}
140
141impl Serialize for CapabilityEntry {
142 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
143 match self {
144 Self::Deny => serializer.serialize_none(),
145 Self::Allow(caps) => {
146 use serde::ser::SerializeSeq;
147 let mut seq = serializer.serialize_seq(Some(caps.len()))?;
148 for cap in caps {
149 seq.serialize_element(cap)?;
150 }
151 seq.end()
152 }
153 Self::Grant(refs) => serializer.serialize_str(&refs.join(",")),
154 }
155 }
156}
157
158impl<'de> Deserialize<'de> for CapabilityEntry {
159 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
160 #[derive(Deserialize)]
161 #[serde(untagged)]
162 enum Raw {
163 Str(String),
164 Seq(Vec<String>),
165 }
166 let opt: Option<Raw> = Option::deserialize(deserializer)?;
167 match opt {
168 None => Ok(Self::Deny),
169 Some(Raw::Seq(v)) if v.is_empty() => Ok(Self::Deny),
170 Some(Raw::Seq(v)) => Ok(Self::Allow(v.into_iter().collect())),
171 Some(Raw::Str(s)) => {
172 let refs: Vec<String> = s
173 .split(',')
174 .map(|r| r.trim().to_string())
175 .filter(|r| !r.is_empty())
176 .collect();
177 if refs.is_empty() {
178 Ok(Self::Deny)
179 } else {
180 Ok(Self::Grant(refs))
181 }
182 }
183 }
184 }
185}
186
187pub type AclMap = HashMap<String, CapabilityEntry>;
200
201#[cfg(feature = "acl")]
215pub fn check_cap(acl: &AclMap, caller: &str, cap: &str) -> Result<()> {
216 let normalized = normalize_principal(caller);
217 if let Some(direct) = acl.get(normalized) {
218 match direct {
219 CapabilityEntry::Deny => {
220 return Err(Error::Acl(format!("operation denied for {caller}")));
221 }
222 CapabilityEntry::Allow(caps) if caps.contains(cap) || caps.contains("*") => {
223 return Ok(());
224 }
225 CapabilityEntry::Allow(_) => {
226 return Err(Error::Acl(format!(
227 "capability '{cap}' denied for {caller}"
228 )));
229 }
230 CapabilityEntry::Grant(_) => {
231 }
234 }
235 }
236
237 match acl.get("*") {
238 Some(CapabilityEntry::Grant(_)) | None => {
239 Err(Error::Acl(format!("no ACL entry for {caller}")))
240 }
241 Some(CapabilityEntry::Deny) => Err(Error::Acl(format!("operation denied for {caller}"))),
242 Some(CapabilityEntry::Allow(caps)) if caps.contains(cap) || caps.contains("*") => Ok(()),
243 Some(CapabilityEntry::Allow(_)) => Err(Error::Acl(format!(
244 "capability '{cap}' denied for {caller}"
245 ))),
246 }
247}
248
249pub fn is_valid_acl_key(key: &str) -> bool {
256 !key.is_empty()
257}
258
259pub fn is_principal_key(key: &str) -> bool {
261 key == "*"
262 || (key.starts_with("did:") && !key.contains('#'))
263 || (key.starts_with('#') && key.len() > 1)
264 || is_valid_group_key(key)
265}
266
267fn is_valid_group_key(key: &str) -> bool {
269 if let Some(rest) = key.strip_prefix("group:") {
270 if let Some(dot) = rest.find('.') {
271 let handle = &rest[..dot];
272 let name = &rest[dot + 1..];
273 return !handle.is_empty() && !name.is_empty();
274 }
275 }
276 false
277}
278
279#[cfg(feature = "acl")]
284pub fn validate_acl_map(acl: &AclMap) -> Result<()> {
285 for key in acl.keys() {
286 if !is_valid_acl_key(key) {
287 return Err(Error::Acl(format!(
288 "invalid ACL key {key:?}: key must be non-empty"
289 )));
290 }
291 }
292 Ok(())
293}
294
295pub fn normalize_principal(did: &str) -> &str {
301 if did.starts_with("did:") {
302 if let Some(pos) = did.find('#') {
303 return &did[..pos];
304 }
305 }
306 did
307}
308
309#[cfg(test)]
312mod tests {
313 use super::*;
314
315 fn allow(caps: &[&str]) -> CapabilityEntry {
316 CapabilityEntry::from_caps(caps.iter().copied())
317 }
318
319 fn m(entries: &[(&str, CapabilityEntry)]) -> AclMap {
320 entries
321 .iter()
322 .map(|(k, v)| (k.to_string(), v.clone()))
323 .collect()
324 }
325
326 #[test]
327 fn wildcard_rpc_allows_rpc() {
328 let acl = m(&[("*", allow(&[CAP_RPC]))]);
329 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
330 }
331
332 #[test]
333 fn wildcard_rpc_denies_ipfs() {
334 let acl = m(&[("*", allow(&[CAP_RPC]))]);
335 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_err());
336 }
337
338 #[test]
339 fn explicit_deny_wins_over_wildcard_allow() {
340 let acl = m(&[
341 ("*", allow(&[CAP_RPC, CAP_IPFS])),
342 ("did:ma:bandit", CapabilityEntry::Deny),
343 ]);
344 assert!(check_cap(&acl, "did:ma:bandit", CAP_RPC).is_err());
345 }
346
347 #[test]
348 fn exact_match_restricts_below_wildcard() {
349 let acl = m(&[
350 ("*", allow(&[CAP_RPC, CAP_IPFS])),
351 ("did:ma:bob", allow(&[CAP_RPC])),
352 ]);
353 assert!(check_cap(&acl, "did:ma:bob", CAP_RPC).is_ok());
354 assert!(check_cap(&acl, "did:ma:bob", CAP_IPFS).is_err());
355 }
356
357 #[test]
358 fn did_url_caller_is_normalized() {
359 let acl = m(&[("did:ma:alice", allow(&[CAP_RPC, CAP_IPFS]))]);
360 assert!(check_cap(&acl, "did:ma:alice#sign", CAP_RPC).is_ok());
361 }
362
363 #[test]
364 fn no_entry_default_deny() {
365 assert!(check_cap(&AclMap::new(), "did:ma:anyone", CAP_RPC).is_err());
366 }
367
368 #[test]
369 fn wildcard_deny_blocks_all() {
370 let acl = m(&[("*", CapabilityEntry::Deny)]);
371 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_err());
372 }
373
374 #[test]
375 fn local_entity_key_allowed() {
376 let acl = m(&[("#agent", allow(&[CAP_RPC]))]);
377 assert!(check_cap(&acl, "#agent", CAP_RPC).is_ok());
378 assert!(check_cap(&acl, "#other", CAP_RPC).is_err());
379 }
380
381 #[test]
382 fn arbitrary_capability_works() {
383 let acl = m(&[("did:ma:alice", allow(&["emote", "reply"]))]);
384 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
385 assert!(check_cap(&acl, "did:ma:alice", "reply").is_ok());
386 assert!(check_cap(&acl, "did:ma:alice", "admin").is_err());
387 }
388
389 #[test]
390 fn wildcard_cap_grants_all_capabilities() {
391 let acl = m(&[("did:ma:alice", allow(&["*"]))]);
392 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
393 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_ok());
394 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
395 assert!(check_cap(&acl, "did:ma:alice", "admin").is_ok());
396 }
397
398 #[test]
399 fn grant_entry_is_skipped_by_check_cap() {
400 let mut acl = AclMap::new();
401 acl.insert("*".to_string(), allow(&[CAP_RPC]));
402 acl.insert(
403 "fortune".to_string(),
404 CapabilityEntry::Grant(vec!["group:carlotta.friends".to_string()]),
405 );
406 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_ok());
407 assert!(check_cap(&acl, "did:ma:anyone", "fortune").is_err());
408 }
409
410 #[test]
411 fn grant_entry_serde_round_trip() {
412 let entry = CapabilityEntry::Grant(vec![
413 "group:carlotta.friends".to_string(),
414 "did:ma:alice".to_string(),
415 ]);
416 let yaml = serde_yaml::to_string(&entry).expect("serialize");
417 assert!(yaml.contains("group:carlotta.friends"));
418 let round: CapabilityEntry = serde_yaml::from_str(yaml.trim()).expect("deserialize");
419 assert_eq!(round, entry);
420 }
421
422 #[test]
423 fn owner_capability_is_just_a_string() {
424 let acl = m(&[("did:ma:alice", allow(&["owner"]))]);
426 assert!(check_cap(&acl, "did:ma:alice", "owner").is_ok());
427 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_err());
428 }
429
430 #[test]
431 fn normalize_strips_fragment() {
432 assert_eq!(normalize_principal("did:ma:foo#bar"), "did:ma:foo");
433 assert_eq!(normalize_principal("did:ma:foo"), "did:ma:foo");
434 assert_eq!(normalize_principal("#local"), "#local");
435 assert_eq!(normalize_principal("*"), "*");
436 }
437
438 #[test]
439 fn valid_acl_keys() {
440 assert!(is_valid_acl_key("*"));
441 assert!(is_valid_acl_key("did:ma:Qmfoo"));
442 assert!(is_valid_acl_key("#agent"));
443 assert!(is_valid_acl_key("group:alice.venner"));
444 assert!(is_valid_acl_key("group:runtime.admins"));
445 assert!(is_valid_acl_key("fortune"));
447 assert!(is_valid_acl_key("admin"));
448 assert!(is_valid_acl_key("emote"));
449 assert!(!is_valid_acl_key(""));
450 }
451
452 #[cfg(feature = "acl")]
453 #[test]
454 fn capability_serde_roundtrip() {
455 let acl: AclMap = [
456 (
457 "*".to_string(),
458 CapabilityEntry::from_caps(["rpc", "create"]),
459 ),
460 ("did:ma:bandit".to_string(), CapabilityEntry::Deny),
461 ]
462 .into_iter()
463 .collect();
464 let yaml = serde_yaml::to_string(&acl).unwrap();
465 let roundtrip: AclMap = serde_yaml::from_str(&yaml).unwrap();
466 assert_eq!(acl, roundtrip);
467 }
468
469 #[cfg(feature = "acl")]
470 #[test]
471 fn yaml_null_deserializes_to_deny() {
472 let yaml = "'did:ma:x': ~\n'*':\n- rpc\n- create\n";
473 let acl: AclMap = serde_yaml::from_str(yaml).unwrap();
474 assert_eq!(acl.get("did:ma:x"), Some(&CapabilityEntry::Deny));
475 assert_eq!(
476 acl.get("*"),
477 Some(&CapabilityEntry::from_caps(["rpc", "create"]))
478 );
479 }
480}