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(Capability { 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(pub Vec<Capability>);
331
332impl Capabilities {
333 pub fn contains(&self, capability: &Capability) -> bool {
335 self.0.contains(capability)
336 }
337
338 pub fn is_empty(&self) -> bool {
340 self.0.is_empty()
341 }
342
343 pub fn len(&self) -> usize {
345 self.0.len()
346 }
347
348 pub fn iter(&self) -> std::slice::Iter<'_, Capability> {
350 self.0.iter()
351 }
352
353 pub fn from_url(url: &Url) -> Self {
369 let value = url
371 .query_pairs()
372 .find_map(|(k, v)| (k == "caps").then(|| v.to_string()))
373 .unwrap_or_default();
374
375 let caps = value
377 .split(',')
378 .filter_map(|s| Capability::try_from(s).ok())
379 .collect();
380
381 Capabilities(sanitize_caps(caps))
382 }
383
384 pub fn builder() -> CapsBuilder {
392 CapsBuilder::default()
393 }
394
395 #[inline]
411 pub fn as_slice(&self) -> &[Capability] {
412 &self.0
413 }
414
415 #[inline]
428 pub fn to_vec(&self) -> Vec<Capability> {
429 self.0.clone()
430 }
431}
432
433#[derive(Default, Debug)]
438pub struct CapsBuilder {
439 caps: Vec<Capability>,
440}
441
442impl CapsBuilder {
443 pub fn new() -> Self {
445 Self::default()
446 }
447
448 pub fn cap(mut self, cap: Capability) -> Self {
450 self.caps.push(cap);
451 self
452 }
453
454 pub fn capability<F>(mut self, scope: impl Into<String>, f: F) -> Self
464 where
465 F: FnOnce(CapabilityBuilder) -> CapabilityBuilder,
466 {
467 let cap = f(Capability::builder(scope)).finish();
468 self.caps.push(cap);
469 self
470 }
471
472 pub fn read(mut self, scope: impl Into<String>) -> Self {
474 self.caps.push(Capability::read(scope));
475 self
476 }
477
478 pub fn write(mut self, scope: impl Into<String>) -> Self {
480 self.caps.push(Capability::write(scope));
481 self
482 }
483
484 pub fn read_write(mut self, scope: impl Into<String>) -> Self {
486 self.caps.push(Capability::read_write(scope));
487 self
488 }
489
490 pub fn extend<I: IntoIterator<Item = Capability>>(mut self, iter: I) -> Self {
492 self.caps.extend(iter);
493 self
494 }
495
496 pub fn finish(self) -> Capabilities {
498 Capabilities(sanitize_caps(self.caps))
499 }
500}
501
502impl From<Vec<Capability>> for Capabilities {
503 fn from(value: Vec<Capability>) -> Self {
504 Self(value)
505 }
506}
507
508impl From<Capabilities> for Vec<Capability> {
509 fn from(value: Capabilities) -> Self {
510 value.0
511 }
512}
513
514impl TryFrom<&str> for Capabilities {
515 type Error = Error;
516
517 fn try_from(value: &str) -> Result<Self, Self::Error> {
518 let mut caps = vec![];
519
520 for s in value.split(',') {
521 if let Ok(cap) = Capability::try_from(s) {
522 caps.push(cap);
523 };
524 }
525
526 Ok(Capabilities(sanitize_caps(caps)))
527 }
528}
529
530impl From<&Url> for Capabilities {
532 fn from(url: &Url) -> Self {
533 Capabilities::from_url(url)
534 }
535}
536
537impl From<Url> for Capabilities {
539 fn from(url: Url) -> Self {
540 Capabilities::from_url(&url)
541 }
542}
543
544impl Display for Capabilities {
545 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
546 let string = self
547 .0
548 .iter()
549 .map(|c| c.to_string())
550 .collect::<Vec<_>>()
551 .join(",");
552
553 write!(f, "{string}")
554 }
555}
556
557impl Serialize for Capabilities {
558 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
559 where
560 S: serde::Serializer,
561 {
562 self.to_string().serialize(serializer)
563 }
564}
565
566impl<'de> Deserialize<'de> for Capabilities {
567 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
568 where
569 D: serde::Deserializer<'de>,
570 {
571 let string: String = Deserialize::deserialize(deserializer)?;
572
573 let mut caps = vec![];
574
575 for s in string.split(',') {
576 if let Ok(cap) = Capability::try_from(s) {
577 caps.push(cap);
578 };
579 }
580
581 Ok(Capabilities(sanitize_caps(caps)))
582 }
583}
584
585fn normalize_scope(mut s: String) -> String {
588 if !s.starts_with('/') {
589 s.insert(0, '/');
590 }
591 s
592}
593
594fn scope_covers(parent: &str, child: &str) -> bool {
595 if parent == child {
596 return true;
597 }
598
599 if !parent.ends_with('/') {
600 return false;
601 }
602
603 child.starts_with(parent)
604}
605
606fn sanitize_caps(caps: Vec<Capability>) -> Vec<Capability> {
607 let mut merged: Vec<Capability> = Vec::new();
608
609 for mut cap in caps {
610 if let Some(existing) = merged
611 .iter_mut()
612 .find(|existing| existing.scope == cap.scope)
613 {
614 let actions: BTreeSet<Action> = existing
615 .actions
616 .iter()
617 .copied()
618 .chain(cap.actions.iter().copied())
619 .collect();
620 existing.actions = actions.into_iter().collect();
621 continue;
622 }
623
624 let actions: BTreeSet<Action> = cap.actions.iter().copied().collect();
625 cap.actions = actions.into_iter().collect();
626 merged.push(cap);
627 }
628
629 let mut sanitized: Vec<Capability> = Vec::new();
630
631 'outer: for cap in merged.into_iter() {
632 if sanitized.iter().any(|existing| existing.covers(&cap)) {
633 continue 'outer;
634 }
635
636 sanitized.retain(|existing| !cap.covers(existing));
637 sanitized.push(cap);
638 }
639
640 sanitized
641}
642
643#[cfg(test)]
644mod tests {
645 use super::*;
646 use url::Url;
647
648 #[test]
649 fn pubky_caps() {
650 let cap = Capability {
651 scope: "/pub/pubky.app/".to_string(),
652 actions: vec![Action::Read, Action::Write],
653 };
654
655 let expected_string = "/pub/pubky.app/:rw";
657
658 assert_eq!(cap.to_string(), expected_string);
659
660 assert_eq!(Capability::try_from(expected_string), Ok(cap))
661 }
662
663 #[test]
664 fn root_capability_helper() {
665 let cap = Capability::root();
666 assert_eq!(cap.scope, "/");
667 assert_eq!(cap.actions, vec![Action::Read, Action::Write]);
668 assert_eq!(cap.to_string(), "/:rw");
669 assert_eq!(Capability::try_from("/:rw"), Ok(cap));
671 }
672
673 #[test]
674 fn single_capability_via_builder_and_shortcuts() {
675 let cap1 = Capability::builder("/pub/my-cool-app/")
677 .read()
678 .write()
679 .finish();
680 assert_eq!(cap1.to_string(), "/pub/my-cool-app/:rw");
681
682 let cap_rw = Capability::read_write("/pub/my-cool-app/");
684 let cap_r = Capability::read("/pub/file.txt");
685 let cap_w = Capability::write("/pub/uploads/");
686
687 assert_eq!(cap_rw, cap1);
688 assert_eq!(cap_r.to_string(), "/pub/file.txt:r");
689 assert_eq!(cap_w.to_string(), "/pub/uploads/:w");
690 }
691
692 #[test]
693 fn multiple_caps_with_capsbuilder() {
694 let caps = Capabilities::builder()
695 .read("/pub/my-cool-app/") .write("/pub/uploads/") .read_write("/pub/my-cool-app/data/") .finish();
699
700 assert_eq!(
702 caps.to_string(),
703 "/pub/my-cool-app/:r,/pub/uploads/:w,/pub/my-cool-app/data/:rw"
704 );
705
706 assert!(caps.contains(&Capability::read("/pub/my-cool-app/")));
708 assert!(caps.contains(&Capability::write("/pub/uploads/")));
709 assert!(caps.contains(&Capability::read_write("/pub/my-cool-app/data/")));
710 assert!(!caps.contains(&Capability::write("/nope")));
711 }
712
713 #[test]
714 fn build_with_inline_capability_closure() {
715 let caps = Capabilities::builder()
717 .capability("/pub/my-cool-app/", |c| c.read().write())
718 .finish();
719
720 assert_eq!(caps.to_string(), "/pub/my-cool-app/:rw");
721 }
722
723 #[test]
724 fn action_dedup_and_order_are_stable() {
725 let cap = Capability::builder("/")
727 .write()
728 .read()
729 .read()
730 .write()
731 .finish();
732 assert_eq!(cap.actions, vec![Action::Read, Action::Write]);
733 assert_eq!(cap.to_string(), "/:rw");
734 }
735
736 #[test]
737 fn normalize_scope_adds_leading_slash() {
738 let cap = Capability::read("pub/my.app");
740 assert_eq!(cap.scope, "/pub/my.app");
741 assert_eq!(cap.to_string(), "/pub/my.app:r");
742
743 let caps = Capabilities::builder()
745 .read_write("pub/my-cool-app/data")
746 .finish();
747 assert_eq!(caps.to_string(), "/pub/my-cool-app/data:rw");
748 }
749
750 #[test]
751 fn parse_from_string_list() {
752 let parsed = Capabilities::try_from("/:rw,/pub/my-cool-app/:r").unwrap();
754 let built = Capabilities::builder()
755 .read_write("/") .read("/pub/my-cool-app/") .finish();
758
759 assert_eq!(parsed, built);
760 }
761
762 #[test]
763 fn parse_errors_are_informative() {
764 let e = Capability::try_from("not/abs:rw").unwrap_err();
766 assert!(matches!(e, Error::InvalidScope));
767
768 let e = Capability::try_from("/pub/my.app").unwrap_err();
770 assert!(matches!(e, Error::InvalidFormat));
771
772 let e = Capability::try_from("/pub/my.app:rx").unwrap_err();
774 assert!(matches!(e, Error::InvalidAction));
775 }
776
777 #[test]
778 fn redundant_capabilities_builder_dedup() {
779 let caps = Capabilities::builder()
780 .read_write("/pub/example.com/")
781 .read_write("/pub/example.com/")
782 .write("/pub/example.com/subfolder")
783 .finish();
784
785 assert_eq!(caps.to_string(), "/pub/example.com/:rw");
786 }
787
788 #[test]
789 fn redundant_capabilities_string_dedup() {
790 let parsed = Capabilities::try_from(
791 "/pub/example.com/:rw,/pub/example.com/:rw,/pub/example.com/subfolder:w",
792 )
793 .unwrap();
794
795 let caps = Capabilities::builder()
796 .read_write("/pub/example.com/")
797 .finish();
798
799 assert_eq!(caps.to_string(), "/pub/example.com/:rw");
800 assert_eq!(parsed, caps);
801 }
802
803 #[test]
804 fn redundant_capabilities_from_url_dedup() {
805 let url = Url::parse(
806 "https://example.test?caps=/pub/example.com/:rw,/pub/example.com/documents:w",
807 )
808 .unwrap();
809 let caps = Capabilities::from_url(&url);
810
811 assert_eq!(caps.to_string(), "/pub/example.com/:rw");
812 }
813
814 #[test]
815 fn redundant_capabilities_merge_actions() {
816 let caps = Capabilities::builder()
817 .read("/pub/example.com/")
818 .write("/pub/example.com/")
819 .finish();
820
821 assert_eq!(caps.to_string(), "/pub/example.com/:rw");
822 }
823
824 #[test]
825 fn capabilities_len_and_is_empty() {
826 let empty = Capabilities::builder().finish();
827 assert!(empty.is_empty());
828 assert_eq!(empty.len(), 0);
829
830 let one = Capabilities::builder().read("/").finish();
831 assert!(!one.is_empty());
832 assert_eq!(one.len(), 1);
833 }
834
835 #[test]
837 fn serde_roundtrip_as_string() {
838 let caps = Capabilities::builder()
839 .read_write("/pub/my-cool-app/")
840 .read("/pub/file.txt")
841 .finish();
842
843 let json = serde_json::to_string(&caps).unwrap();
844 assert_eq!(json, "\"/pub/my-cool-app/:rw,/pub/file.txt:r\"");
846
847 let back: Capabilities = serde_json::from_str(&json).unwrap();
848 assert_eq!(back, caps);
849 }
850}