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_RPC: &str = "rpc";
69pub const CAP_IPFS: &str = "ipfs";
71pub const CAP_READ: &str = "read";
73pub const CAP_CREATE: &str = "create";
75pub const CAP_UPDATE: &str = "update";
77pub const CAP_DELETE: &str = "delete";
79pub const CAP_OWNER: &str = "owner";
81
82#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum CapabilityEntry {
90 Deny,
92 Allow(BTreeSet<String>),
94}
95
96impl CapabilityEntry {
97 pub fn from_caps<I, S>(caps: I) -> Self
99 where
100 I: IntoIterator<Item = S>,
101 S: Into<String>,
102 {
103 Self::Allow(caps.into_iter().map(Into::into).collect())
104 }
105
106 pub fn has(&self, cap: &str) -> bool {
108 match self {
109 Self::Deny => false,
110 Self::Allow(caps) => caps.contains(cap),
111 }
112 }
113
114 pub fn is_deny(&self) -> bool {
116 matches!(self, Self::Deny)
117 }
118}
119
120impl Serialize for CapabilityEntry {
121 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
122 match self {
123 Self::Deny => serializer.serialize_none(),
124 Self::Allow(caps) => {
125 use serde::ser::SerializeSeq;
126 let mut seq = serializer.serialize_seq(Some(caps.len()))?;
127 for cap in caps {
128 seq.serialize_element(cap)?;
129 }
130 seq.end()
131 }
132 }
133 }
134}
135
136impl<'de> Deserialize<'de> for CapabilityEntry {
137 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
138 let opt: Option<Vec<String>> = Option::deserialize(deserializer)?;
139 match opt {
140 None => Ok(Self::Deny),
141 Some(v) if v.is_empty() => Ok(Self::Deny),
142 Some(v) => Ok(Self::Allow(v.into_iter().collect())),
143 }
144 }
145}
146
147pub type AclMap = HashMap<String, CapabilityEntry>;
160
161#[cfg(feature = "acl")]
173pub fn check_cap(acl: &AclMap, caller: &str, cap: &str) -> Result<()> {
174 let normalized = normalize_principal(caller);
175 if let Some(direct) = acl.get(normalized) {
176 return match direct {
177 CapabilityEntry::Deny => Err(Error::Acl(format!("operation denied for {caller}"))),
178 CapabilityEntry::Allow(caps) if caps.contains(cap) => Ok(()),
179 CapabilityEntry::Allow(_) => Err(Error::Acl(format!(
180 "capability '{cap}' denied for {caller}"
181 ))),
182 };
183 }
184
185 match acl.get("*") {
186 None => Err(Error::Acl(format!("no ACL entry for {caller}"))),
187 Some(CapabilityEntry::Deny) => Err(Error::Acl(format!("operation denied for {caller}"))),
188 Some(CapabilityEntry::Allow(caps)) if caps.contains(cap) => Ok(()),
189 Some(CapabilityEntry::Allow(_)) => Err(Error::Acl(format!(
190 "capability '{cap}' denied for {caller}"
191 ))),
192 }
193}
194
195pub fn is_valid_acl_key(key: &str) -> bool {
203 key == "*"
204 || (key.starts_with("did:") && !key.contains('#'))
205 || (key.starts_with('#') && key.len() > 1)
206 || is_valid_group_key(key)
207}
208
209fn is_valid_group_key(key: &str) -> bool {
211 if let Some(rest) = key.strip_prefix("group:") {
212 if let Some(dot) = rest.find('.') {
213 let handle = &rest[..dot];
214 let name = &rest[dot + 1..];
215 return !handle.is_empty() && !name.is_empty();
216 }
217 }
218 false
219}
220
221#[cfg(feature = "acl")]
226pub fn validate_acl_map(acl: &AclMap) -> Result<()> {
227 for key in acl.keys() {
228 if !is_valid_acl_key(key) {
229 return Err(Error::Acl(format!(
230 "invalid ACL key {key:?}: must be \"*\", a bare DID (\"did:ma:\u{2026}\"), \
231 a local entity (\"#name\"), or a group (\"group:<handle>.<name>\")"
232 )));
233 }
234 }
235 Ok(())
236}
237
238pub fn normalize_principal(did: &str) -> &str {
244 if did.starts_with("did:") {
245 if let Some(pos) = did.find('#') {
246 return &did[..pos];
247 }
248 }
249 did
250}
251
252#[cfg(test)]
255mod tests {
256 use super::*;
257
258 fn allow(caps: &[&str]) -> CapabilityEntry {
259 CapabilityEntry::from_caps(caps.iter().copied())
260 }
261
262 fn m(entries: &[(&str, CapabilityEntry)]) -> AclMap {
263 entries
264 .iter()
265 .map(|(k, v)| (k.to_string(), v.clone()))
266 .collect()
267 }
268
269 #[test]
270 fn wildcard_rpc_allows_rpc() {
271 let acl = m(&[("*", allow(&[CAP_RPC]))]);
272 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_ok());
273 }
274
275 #[test]
276 fn wildcard_rpc_denies_ipfs() {
277 let acl = m(&[("*", allow(&[CAP_RPC]))]);
278 assert!(check_cap(&acl, "did:ma:alice", CAP_IPFS).is_err());
279 }
280
281 #[test]
282 fn explicit_deny_wins_over_wildcard_allow() {
283 let acl = m(&[
284 ("*", allow(&[CAP_RPC, CAP_IPFS])),
285 ("did:ma:bandit", CapabilityEntry::Deny),
286 ]);
287 assert!(check_cap(&acl, "did:ma:bandit", CAP_RPC).is_err());
288 }
289
290 #[test]
291 fn exact_match_restricts_below_wildcard() {
292 let acl = m(&[
293 ("*", allow(&[CAP_RPC, CAP_IPFS])),
294 ("did:ma:bob", allow(&[CAP_RPC])),
295 ]);
296 assert!(check_cap(&acl, "did:ma:bob", CAP_RPC).is_ok());
297 assert!(check_cap(&acl, "did:ma:bob", CAP_IPFS).is_err());
298 }
299
300 #[test]
301 fn did_url_caller_is_normalized() {
302 let acl = m(&[("did:ma:alice", allow(&[CAP_RPC, CAP_IPFS]))]);
303 assert!(check_cap(&acl, "did:ma:alice#sign", CAP_RPC).is_ok());
304 }
305
306 #[test]
307 fn no_entry_default_deny() {
308 assert!(check_cap(&AclMap::new(), "did:ma:anyone", CAP_RPC).is_err());
309 }
310
311 #[test]
312 fn wildcard_deny_blocks_all() {
313 let acl = m(&[("*", CapabilityEntry::Deny)]);
314 assert!(check_cap(&acl, "did:ma:anyone", CAP_RPC).is_err());
315 }
316
317 #[test]
318 fn local_entity_key_allowed() {
319 let acl = m(&[("#agent", allow(&[CAP_RPC]))]);
320 assert!(check_cap(&acl, "#agent", CAP_RPC).is_ok());
321 assert!(check_cap(&acl, "#other", CAP_RPC).is_err());
322 }
323
324 #[test]
325 fn arbitrary_capability_works() {
326 let acl = m(&[("did:ma:alice", allow(&["emote", "reply"]))]);
327 assert!(check_cap(&acl, "did:ma:alice", "emote").is_ok());
328 assert!(check_cap(&acl, "did:ma:alice", "reply").is_ok());
329 assert!(check_cap(&acl, "did:ma:alice", "admin").is_err());
330 }
331
332 #[test]
333 fn owner_capability_is_just_a_string() {
334 let acl = m(&[("did:ma:alice", allow(&[CAP_OWNER]))]);
336 assert!(check_cap(&acl, "did:ma:alice", CAP_OWNER).is_ok());
337 assert!(check_cap(&acl, "did:ma:alice", CAP_RPC).is_err());
338 }
339
340 #[test]
341 fn normalize_strips_fragment() {
342 assert_eq!(normalize_principal("did:ma:foo#bar"), "did:ma:foo");
343 assert_eq!(normalize_principal("did:ma:foo"), "did:ma:foo");
344 assert_eq!(normalize_principal("#local"), "#local");
345 assert_eq!(normalize_principal("*"), "*");
346 }
347
348 #[test]
349 fn valid_acl_keys() {
350 assert!(is_valid_acl_key("*"));
351 assert!(is_valid_acl_key("did:ma:Qmfoo"));
352 assert!(is_valid_acl_key("#agent"));
353 assert!(is_valid_acl_key("group:alice.venner"));
354 assert!(is_valid_acl_key("group:runtime.admins"));
355 assert!(!is_valid_acl_key("did:ma:Qmfoo#sign"));
356 assert!(!is_valid_acl_key("#"));
357 assert!(!is_valid_acl_key(""));
358 assert!(!is_valid_acl_key("group:noname"));
359 assert!(!is_valid_acl_key("group:.nohandle"));
360 assert!(!is_valid_acl_key("group:handle."));
361 }
362
363 #[cfg(feature = "acl")]
364 #[test]
365 fn capability_serde_roundtrip() {
366 let acl: AclMap = [
367 (
368 "*".to_string(),
369 CapabilityEntry::from_caps(["rpc", "create"]),
370 ),
371 ("did:ma:bandit".to_string(), CapabilityEntry::Deny),
372 ]
373 .into_iter()
374 .collect();
375 let yaml = serde_yaml::to_string(&acl).unwrap();
376 let roundtrip: AclMap = serde_yaml::from_str(&yaml).unwrap();
377 assert_eq!(acl, roundtrip);
378 }
379
380 #[cfg(feature = "acl")]
381 #[test]
382 fn yaml_null_deserializes_to_deny() {
383 let yaml = "'did:ma:x': ~\n'*':\n- rpc\n- create\n";
384 let acl: AclMap = serde_yaml::from_str(yaml).unwrap();
385 assert_eq!(acl.get("did:ma:x"), Some(&CapabilityEntry::Deny));
386 assert_eq!(
387 acl.get("*"),
388 Some(&CapabilityEntry::from_caps(["rpc", "create"]))
389 );
390 }
391}