1use serde::{Deserialize, Serialize};
42use std::{collections::BTreeSet, fmt::Display, str::FromStr};
43use url::Url;
44
45#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct Capability {
50 pub scope: String,
52 pub actions: Vec<Action>,
54}
55
56impl Capability {
57 pub fn root() -> Self {
66 Capability {
67 scope: "/".to_string(),
68 actions: vec![Action::Read, Action::Write],
69 }
70 }
71
72 #[inline]
83 pub fn read<S: Into<String>>(scope: S) -> Self {
84 Self::builder(scope).read().finish()
85 }
86
87 #[inline]
94 pub fn write<S: Into<String>>(scope: S) -> Self {
95 Self::builder(scope).write().finish()
96 }
97
98 #[inline]
105 pub fn read_write<S: Into<String>>(scope: S) -> Self {
106 Self::builder(scope).read().write().finish()
107 }
108
109 pub fn builder<S: Into<String>>(scope: S) -> CapabilityBuilder {
119 CapabilityBuilder {
120 scope: normalize_scope(scope.into()),
121 actions: BTreeSet::new(),
122 }
123 }
124
125 fn covers(&self, other: &Capability) -> bool {
126 if !scope_covers(&self.scope, &other.scope) {
127 return false;
128 }
129
130 other
131 .actions
132 .iter()
133 .all(|action| self.actions.contains(action))
134 }
135}
136
137#[derive(Debug, Default)]
141pub struct CapabilityBuilder {
142 scope: String,
143 actions: BTreeSet<Action>,
144}
145
146impl CapabilityBuilder {
147 pub fn read(mut self) -> Self {
149 self.actions.insert(Action::Read);
150 self
151 }
152
153 pub fn write(mut self) -> Self {
155 self.actions.insert(Action::Write);
156 self
157 }
158
159 pub fn allow(mut self, action: Action) -> Self {
161 self.actions.insert(action);
162 self
163 }
164
165 pub fn finish(self) -> Capability {
169 let v: Vec<Action> = self.actions.into_iter().collect();
170 Capability {
172 scope: self.scope,
173 actions: v,
174 }
175 }
176}
177
178#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
182pub enum Action {
183 Read,
185 Write,
187 Unknown(char),
189}
190
191impl From<&Action> for char {
192 fn from(value: &Action) -> Self {
193 match value {
194 Action::Read => 'r',
195 Action::Write => 'w',
196 Action::Unknown(char) => char.to_owned(),
197 }
198 }
199}
200
201impl TryFrom<char> for Action {
202 type Error = Error;
203
204 fn try_from(value: char) -> Result<Self, Error> {
205 match value {
206 'r' => Ok(Self::Read),
207 'w' => Ok(Self::Write),
208 _ => Err(Error::InvalidAction),
209 }
210 }
211}
212
213impl Display for Capability {
214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 write!(
216 f,
217 "{}:{}",
218 self.scope,
219 self.actions.iter().map(char::from).collect::<String>()
220 )
221 }
222}
223
224impl TryFrom<String> for Capability {
225 type Error = Error;
226
227 fn try_from(value: String) -> Result<Self, Error> {
228 value.as_str().try_into()
229 }
230}
231
232impl FromStr for Capability {
233 type Err = Error;
234
235 fn from_str(s: &str) -> Result<Self, Error> {
236 s.try_into()
237 }
238}
239
240impl TryFrom<&str> for Capability {
241 type Error = Error;
242 fn try_from(value: &str) -> Result<Self, Error> {
250 if value.matches(':').count() != 1 {
251 return Err(Error::InvalidFormat);
252 }
253
254 if !value.starts_with('/') {
255 return Err(Error::InvalidScope);
256 }
257
258 let actions_str = value.rsplit(':').next().unwrap_or("");
259
260 let mut actions = Vec::new();
261
262 for char in actions_str.chars() {
263 let ability = Action::try_from(char)?;
264
265 match actions.binary_search_by(|element| char::from(element).cmp(&char)) {
266 Ok(_) => {}
267 Err(index) => {
268 actions.insert(index, ability);
269 }
270 }
271 }
272
273 let scope = value[0..value.len() - actions_str.len() - 1].to_string();
274
275 Ok(Self { scope, actions })
276 }
277}
278
279impl Serialize for Capability {
280 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
281 where
282 S: serde::Serializer,
283 {
284 let string = self.to_string();
285
286 string.serialize(serializer)
287 }
288}
289
290impl<'de> Deserialize<'de> for Capability {
291 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
292 where
293 D: serde::Deserializer<'de>,
294 {
295 let string: String = Deserialize::deserialize(deserializer)?;
296
297 string.try_into().map_err(serde::de::Error::custom)
298 }
299}
300
301#[derive(thiserror::Error, Debug, PartialEq, Eq)]
302pub enum Error {
304 #[error("Capability: Invalid scope: does not start with `/`")]
305 InvalidScope,
307 #[error("Capability: Invalid format should be <scope>:<abilities>")]
308 InvalidFormat,
310 #[error("Capability: Invalid Action")]
311 InvalidAction,
313 #[error("Capabilities: Invalid capabilities format")]
314 InvalidCapabilities,
316}
317
318#[derive(Clone, Default, Debug, PartialEq, Eq)]
329#[must_use]
330pub struct Capabilities(Vec<Capability>);
331
332impl Capabilities {
333 pub fn normalize(self) -> Self {
351 Self(normalize(self.0))
352 }
353
354 pub fn contains(&self, capability: &Capability) -> bool {
356 self.0.contains(capability)
357 }
358
359 pub fn is_empty(&self) -> bool {
361 self.0.is_empty()
362 }
363
364 pub fn len(&self) -> usize {
366 self.0.len()
367 }
368
369 pub fn iter(&self) -> std::slice::Iter<'_, Capability> {
371 self.0.iter()
372 }
373
374 pub fn builder() -> CapsBuilder {
382 CapsBuilder::default()
383 }
384
385 pub fn from_caps_url(url: &Url) -> Self {
401 let value = url
403 .query_pairs()
404 .find_map(|(k, v)| (k == "caps").then(|| v.to_string()))
405 .unwrap_or_default();
406
407 let caps: Vec<_> = value
409 .split(',')
410 .filter_map(|s| Capability::try_from(s).ok())
411 .collect();
412
413 Self::from(caps)
414 }
415
416 #[inline]
432 pub fn as_slice(&self) -> &[Capability] {
433 &self.0
434 }
435}
436
437#[derive(Default, Debug)]
442pub struct CapsBuilder {
443 caps: Vec<Capability>,
444}
445
446impl CapsBuilder {
447 pub fn new() -> Self {
449 Self::default()
450 }
451
452 pub fn cap(mut self, cap: Capability) -> Self {
454 self.caps.push(cap);
455 self
456 }
457
458 pub fn capability<F>(mut self, scope: impl Into<String>, f: F) -> Self
468 where
469 F: FnOnce(CapabilityBuilder) -> CapabilityBuilder,
470 {
471 let cap = f(Capability::builder(scope)).finish();
472 self.caps.push(cap);
473 self
474 }
475
476 pub fn read(mut self, scope: impl Into<String>) -> Self {
478 self.caps.push(Capability::read(scope));
479 self
480 }
481
482 pub fn write(mut self, scope: impl Into<String>) -> Self {
484 self.caps.push(Capability::write(scope));
485 self
486 }
487
488 pub fn read_write(mut self, scope: impl Into<String>) -> Self {
490 self.caps.push(Capability::read_write(scope));
491 self
492 }
493
494 pub fn extend<I: IntoIterator<Item = Capability>>(mut self, iter: I) -> Self {
496 self.caps.extend(iter);
497 self
498 }
499
500 pub fn finish(self) -> Capabilities {
502 Capabilities::from(self.caps).normalize()
503 }
504}
505
506impl From<Vec<Capability>> for Capabilities {
507 fn from(value: Vec<Capability>) -> Self {
508 Self(value)
509 }
510}
511
512impl From<Capabilities> for Vec<Capability> {
513 fn from(value: Capabilities) -> Self {
514 value.0
515 }
516}
517
518impl TryFrom<&str> for Capabilities {
519 type Error = Error;
520
521 fn try_from(value: &str) -> Result<Self, Self::Error> {
522 let mut caps = vec![];
523
524 for s in value.split(',') {
525 if let Ok(cap) = Capability::try_from(s) {
526 caps.push(cap);
527 };
528 }
529
530 Ok(Self::from(caps))
531 }
532}
533
534impl Display for Capabilities {
535 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536 let string = self
537 .0
538 .iter()
539 .map(|c| c.to_string())
540 .collect::<Vec<_>>()
541 .join(",");
542
543 write!(f, "{string}")
544 }
545}
546
547impl Serialize for Capabilities {
548 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
549 where
550 S: serde::Serializer,
551 {
552 self.to_string().serialize(serializer)
553 }
554}
555
556impl<'de> Deserialize<'de> for Capabilities {
557 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
558 where
559 D: serde::Deserializer<'de>,
560 {
561 let string: String = Deserialize::deserialize(deserializer)?;
562
563 let mut caps = vec![];
564
565 for s in string.split(',') {
566 if let Ok(cap) = Capability::try_from(s) {
567 caps.push(cap);
568 };
569 }
570
571 Ok(Self::from(caps))
572 }
573}
574
575fn normalize_scope(mut s: String) -> String {
578 if !s.starts_with('/') {
579 s.insert(0, '/');
580 }
581 s
582}
583
584fn scope_covers(parent: &str, child: &str) -> bool {
585 if parent == child {
586 return true;
587 }
588
589 if !parent.ends_with('/') {
590 return false;
591 }
592
593 child.starts_with(parent)
594}
595
596fn normalize(caps: Vec<Capability>) -> Vec<Capability> {
597 let mut merged: Vec<Capability> = Vec::new();
598
599 for mut cap in caps {
600 if let Some(existing) = merged
601 .iter_mut()
602 .find(|existing| existing.scope == cap.scope)
603 {
604 let actions: BTreeSet<Action> = existing
605 .actions
606 .iter()
607 .copied()
608 .chain(cap.actions.iter().copied())
609 .collect();
610 existing.actions = actions.into_iter().collect();
611 continue;
612 }
613
614 let actions: BTreeSet<Action> = cap.actions.iter().copied().collect();
615 cap.actions = actions.into_iter().collect();
616 merged.push(cap);
617 }
618
619 let mut sanitized: Vec<Capability> = Vec::new();
620
621 'outer: for cap in merged.into_iter() {
622 if sanitized.iter().any(|existing| existing.covers(&cap)) {
623 continue 'outer;
624 }
625
626 sanitized.retain(|existing| !cap.covers(existing));
627 sanitized.push(cap);
628 }
629
630 sanitized
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636 use url::Url;
637
638 #[test]
639 fn pubky_caps() {
640 let cap = Capability {
641 scope: "/pub/pubky.app/".to_string(),
642 actions: vec![Action::Read, Action::Write],
643 };
644
645 let expected_string = "/pub/pubky.app/:rw";
647
648 assert_eq!(cap.to_string(), expected_string);
649
650 assert_eq!(Capability::try_from(expected_string), Ok(cap))
651 }
652
653 #[test]
654 fn root_capability_helper() {
655 let cap = Capability::root();
656 assert_eq!(cap.scope, "/");
657 assert_eq!(cap.actions, vec![Action::Read, Action::Write]);
658 assert_eq!(cap.to_string(), "/:rw");
659 assert_eq!(Capability::try_from("/:rw"), Ok(cap));
661 }
662
663 #[test]
664 fn single_capability_via_builder_and_shortcuts() {
665 let cap1 = Capability::builder("/pub/my-cool-app/")
667 .read()
668 .write()
669 .finish();
670 assert_eq!(cap1.to_string(), "/pub/my-cool-app/:rw");
671
672 let cap_rw = Capability::read_write("/pub/my-cool-app/");
674 let cap_r = Capability::read("/pub/file.txt");
675 let cap_w = Capability::write("/pub/uploads/");
676
677 assert_eq!(cap_rw, cap1);
678 assert_eq!(cap_r.to_string(), "/pub/file.txt:r");
679 assert_eq!(cap_w.to_string(), "/pub/uploads/:w");
680 }
681
682 #[test]
683 fn multiple_caps_with_capsbuilder() {
684 let caps = Capabilities::builder()
685 .read("/pub/my-cool-app/") .write("/pub/uploads/") .read_write("/pub/my-cool-app/data/") .finish();
689
690 assert_eq!(
692 caps.to_string(),
693 "/pub/my-cool-app/:r,/pub/uploads/:w,/pub/my-cool-app/data/:rw"
694 );
695
696 assert!(caps.contains(&Capability::read("/pub/my-cool-app/")));
698 assert!(caps.contains(&Capability::write("/pub/uploads/")));
699 assert!(caps.contains(&Capability::read_write("/pub/my-cool-app/data/")));
700 assert!(!caps.contains(&Capability::write("/nope")));
701 }
702
703 #[test]
704 fn build_with_inline_capability_closure() {
705 let caps = Capabilities::builder()
707 .capability("/pub/my-cool-app/", |c| c.read().write())
708 .finish();
709
710 assert_eq!(caps.to_string(), "/pub/my-cool-app/:rw");
711 }
712
713 #[test]
714 fn action_dedup_and_order_are_stable() {
715 let cap = Capability::builder("/")
717 .write()
718 .read()
719 .read()
720 .write()
721 .finish();
722 assert_eq!(cap.actions, vec![Action::Read, Action::Write]);
723 assert_eq!(cap.to_string(), "/:rw");
724 }
725
726 #[test]
727 fn normalize_scope_adds_leading_slash() {
728 let cap = Capability::read("pub/my.app");
730 assert_eq!(cap.scope, "/pub/my.app");
731 assert_eq!(cap.to_string(), "/pub/my.app:r");
732
733 let caps = Capabilities::builder()
735 .read_write("pub/my-cool-app/data")
736 .finish();
737 assert_eq!(caps.to_string(), "/pub/my-cool-app/data:rw");
738 }
739
740 #[test]
741 fn parse_from_string_list() {
742 let parsed = Capabilities::try_from("/:rw,/pub/my-cool-app/:r")
744 .unwrap()
745 .normalize();
746 let built = Capabilities::builder()
747 .read_write("/") .read("/pub/my-cool-app/") .finish();
750
751 assert_eq!(parsed, built);
752 }
753
754 #[test]
755 fn parse_errors_are_informative() {
756 let e = Capability::try_from("not/abs:rw").unwrap_err();
758 assert_eq!(e, Error::InvalidScope);
759
760 let e = Capability::try_from("/pub/my.app").unwrap_err();
762 assert_eq!(e, Error::InvalidFormat);
763
764 let e = Capability::try_from("/pub/my.app:rx").unwrap_err();
766 assert_eq!(e, Error::InvalidAction);
767 }
768
769 #[test]
770 fn redundant_capabilities_builder_dedup() {
771 let caps = Capabilities::builder()
772 .read_write("/pub/example.com/")
773 .read_write("/pub/example.com/")
774 .write("/pub/example.com/subfolder")
775 .finish()
776 .normalize();
777
778 assert_eq!(caps.to_string(), "/pub/example.com/:rw");
779 }
780
781 #[test]
782 fn redundant_capabilities_string_dedup() {
783 let parsed = Capabilities::try_from(
784 "/pub/example.com/:rw,/pub/example.com/:rw,/pub/example.com/subfolder:w",
785 )
786 .unwrap()
787 .normalize();
788
789 let caps = Capabilities::builder()
790 .read_write("/pub/example.com/")
791 .finish();
792
793 assert_eq!(caps.to_string(), "/pub/example.com/:rw");
794 assert_eq!(parsed, caps);
795 }
796
797 #[test]
798 fn redundant_capabilities_from_url_dedup() {
799 let url = Url::parse(
800 "https://example.test?caps=/pub/example.com/:rw,/pub/example.com/documents:w",
801 )
802 .unwrap();
803 let caps = Capabilities::from_caps_url(&url).normalize();
804
805 assert_eq!(caps.to_string(), "/pub/example.com/:rw");
806 }
807
808 #[test]
809 fn redundant_capabilities_merge_actions() {
810 let caps = Capabilities::builder()
811 .read("/pub/example.com/")
812 .write("/pub/example.com/")
813 .finish()
814 .normalize();
815
816 assert_eq!(caps.to_string(), "/pub/example.com/:rw");
817 }
818
819 #[test]
820 fn capabilities_normalize_dedups_from_vec() {
821 let caps = Capabilities::from(vec![
822 Capability::read_write("/pub/example.com/"),
823 Capability::write("/pub/example.com/subfolder"),
824 Capability::read("/pub/example.com/"),
825 ])
826 .normalize();
827
828 assert_eq!(caps.to_string(), "/pub/example.com/:rw");
829 }
830
831 #[test]
832 fn capabilities_len_and_is_empty() {
833 let empty = Capabilities::builder().finish();
834 assert!(empty.is_empty());
835 assert_eq!(empty.len(), 0);
836
837 let one = Capabilities::builder().read("/").finish();
838 assert!(!one.is_empty());
839 assert_eq!(one.len(), 1);
840 }
841
842 #[test]
844 fn serde_roundtrip_as_string() {
845 let caps = Capabilities::builder()
846 .read_write("/pub/my-cool-app/")
847 .read("/pub/file.txt")
848 .finish();
849
850 let json = serde_json::to_string(&caps).unwrap();
851 assert_eq!(json, "\"/pub/my-cool-app/:rw,/pub/file.txt:r\"");
853
854 let back: Capabilities = serde_json::from_str(&json).unwrap();
855 assert_eq!(back, caps);
856 }
857}