1use std::collections::BTreeSet;
2use std::fmt;
3use std::ops::Deref;
4
5pub fn validate_name(s: &str) -> Result<(), String> {
15 if s.is_empty() {
16 return Err("empty string".to_string());
17 }
18
19 for c in s.chars() {
20 if !matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-') {
21 return Err(format!("invalid character: {}", c));
22 }
23 }
24 Ok(())
25}
26
27#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
29pub enum Resource {
30 #[default]
31 All, File,
33 Folder,
34 Bucket,
35 Cluster,
36 Other(String),
37}
38
39impl Resource {
40 pub fn check(&self, value: &Resource) -> bool {
49 match self {
50 Self::All => true,
51 other => value == other,
52 }
53 }
54}
55
56impl fmt::Display for Resource {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 Self::All => write!(f, "*"),
61 Self::File => write!(f, "File"),
62 Self::Folder => write!(f, "Folder"),
63 Self::Bucket => write!(f, "Bucket"),
64 Self::Cluster => write!(f, "Cluster"),
65 Self::Other(ref s) => write!(f, "{}", s),
66 }
67 }
68}
69
70impl TryFrom<&str> for Resource {
71 type Error = String;
72
73 fn try_from(value: &str) -> Result<Self, Self::Error> {
83 match value {
84 "*" => Ok(Self::All),
85 "File" => Ok(Self::File),
86 "Folder" => Ok(Self::Folder),
87 "Bucket" => Ok(Self::Bucket),
88 "Cluster" => Ok(Self::Cluster),
89 _ => match validate_name(value) {
90 Ok(_) => Ok(Self::Other(value.to_string())),
91 Err(err) => Err(format!("invalid resource: {}, {}", value, err)),
92 },
93 }
94 }
95}
96
97#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
99pub enum Operation {
100 #[default]
101 All, List,
103 Read,
104 Write,
105 Delete,
106 Other(String),
107}
108
109impl Operation {
110 pub fn check(&self, value: &Operation) -> bool {
119 match self {
120 Self::All => true,
121 other => value == other,
122 }
123 }
124}
125
126impl fmt::Display for Operation {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 Self::All => write!(f, "*"),
131 Self::List => write!(f, "List"),
132 Self::Read => write!(f, "Read"),
133 Self::Write => write!(f, "Write"),
134 Self::Delete => write!(f, "Delete"),
135 Self::Other(ref s) => write!(f, "{}", s),
136 }
137 }
138}
139
140impl TryFrom<&str> for Operation {
141 type Error = String;
142
143 fn try_from(value: &str) -> Result<Self, Self::Error> {
153 match value {
154 "*" => Ok(Self::All),
155 "List" => Ok(Self::List),
156 "Read" => Ok(Self::Read),
157 "Write" => Ok(Self::Write),
158 "Delete" => Ok(Self::Delete),
159 _ => match validate_name(value) {
160 Ok(_) => Ok(Self::Other(value.to_string())),
161 Err(err) => Err(format!("invalid operation: {}, {}", value, err)),
162 },
163 }
164 }
165}
166
167#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
175pub struct Permission {
176 pub resource: Resource,
177 pub operation: Operation,
178 pub constraint: Option<Resource>,
179}
180
181impl Permission {
182 pub fn is_all(&self) -> bool {
189 self.resource == Resource::All
190 && self.operation == Operation::All
191 && self.constraint.is_none()
192 }
193
194 pub fn check(&self, value: &Permission) -> bool {
204 self.resource.check(&value.resource)
205 && self.operation.check(&value.operation)
206 && self.check_constraint(&value.constraint)
207 }
208
209 pub fn check_constraint(&self, value: &Option<Resource>) -> bool {
219 match self.constraint {
220 None | Some(Resource::All) => true,
221 Some(ref c) => value.as_ref() == Some(c),
222 }
223 }
224}
225
226impl fmt::Display for Permission {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 match self.constraint {
230 Some(ref c) if c != &Resource::All => {
231 write!(f, "{}.{}.{}", self.resource, self.operation, c)
232 }
233 _ => {
234 if self.is_all() {
235 write!(f, "*")
236 } else {
237 write!(f, "{}.{}", self.resource, self.operation)
238 }
239 }
240 }
241 }
242}
243
244impl TryFrom<&str> for Permission {
245 type Error = String;
246
247 fn try_from(value: &str) -> Result<Self, Self::Error> {
257 if value == "*" {
258 return Ok(Self {
259 resource: Resource::All,
260 operation: Operation::All,
261 constraint: None,
262 });
263 }
264
265 let mut parts = value.split('.');
266 let resource = match parts.next() {
267 Some(v) => Resource::try_from(v)?,
268 _ => return Err(format!("invalid permission format {}", value)),
269 };
270
271 let operation = match parts.next() {
272 Some(v) => Operation::try_from(v)?,
273 _ => return Err(format!("invalid permission format {}", value)),
274 };
275
276 let constraint = match parts.next() {
277 Some(v) => {
278 Some(Resource::try_from(v).map_err(|err| format!("invalid constraint: {}", err))?)
279 }
280 None => None,
281 };
282
283 if parts.next().is_some() {
284 return Err(format!("invalid permission format {}", value));
285 }
286
287 Ok(Self {
288 resource,
289 operation,
290 constraint,
291 })
292 }
293}
294
295pub type ResourcePath = String;
297
298#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
300pub struct Resources(pub BTreeSet<ResourcePath>);
301
302impl Resources {
303 pub fn is_all(&self) -> bool {
310 self.0.is_empty() || self.0.contains("*")
311 }
312
313 fn check<T>(&self, value: T) -> bool
323 where
324 T: AsRef<str>,
325 {
326 self.is_all() || self.0.contains(value.as_ref())
327 }
328}
329
330impl Deref for Resources {
331 type Target = BTreeSet<ResourcePath>;
332
333 fn deref(&self) -> &Self::Target {
334 &self.0
335 }
336}
337
338impl AsRef<BTreeSet<ResourcePath>> for Resources {
339 fn as_ref(&self) -> &BTreeSet<ResourcePath> {
340 &self.0
341 }
342}
343
344impl fmt::Display for Resources {
345 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 match self.0.first() {
348 None => Ok(()),
349 Some(v) => {
350 if !self.is_all() {
351 write!(f, "{}", v)?;
352 for r in self.0.iter().skip(1) {
353 write!(f, ",{}", r)?;
354 }
355 }
356 Ok(())
357 }
358 }
359 }
360}
361
362impl<const N: usize> From<[ResourcePath; N]> for Resources {
363 fn from(val: [ResourcePath; N]) -> Self {
364 Self(BTreeSet::from(val))
365 }
366}
367
368impl TryFrom<&str> for Resources {
369 type Error = String;
370
371 fn try_from(value: &str) -> Result<Self, Self::Error> {
381 match value {
382 "" | "*" => Ok(Self::default()),
383 _ => {
384 let rs: BTreeSet<_> = value.split(',').map(|v| v.to_string()).collect();
385 for r in rs.iter() {
386 validate_name(r)?;
387 }
388 Ok(Resources(rs))
389 }
390 }
391 }
392}
393
394pub trait PermissionChecker<T> {
396 fn has_permission(&self, permission: &Permission, resource_path: T) -> bool;
406}
407
408pub trait PermissionCheckerAny<T> {
410 fn has_permission_any(&self, permission: &Permission, resources_path: &[T]) -> bool;
420}
421
422#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
429pub struct Policy {
430 pub permission: Permission,
431 pub resources: Resources,
432}
433
434impl<T> PermissionChecker<T> for Policy
435where
436 T: AsRef<str>,
437{
438 fn has_permission(&self, permission: &Permission, resource_path: T) -> bool {
439 self.permission.check(permission) && self.resources.check(resource_path.as_ref())
440 }
441}
442
443impl<T> PermissionCheckerAny<T> for Policy
444where
445 T: AsRef<str>,
446{
447 fn has_permission_any(&self, permission: &Permission, resources_path: &[T]) -> bool {
448 self.permission.check(permission)
449 && (self.resources.is_all() || resources_path.iter().any(|r| self.resources.check(r)))
450 }
451}
452
453impl fmt::Display for Policy {
454 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456 if self.resources.is_all() {
457 if self.permission.is_all() {
458 write!(f, "*")
459 } else {
460 write!(f, "{}", self.permission)
461 }
462 } else {
463 write!(f, "{}:{}", self.permission, self.resources)
464 }
465 }
466}
467
468impl TryFrom<&str> for Policy {
469 type Error = String;
470
471 fn try_from(value: &str) -> Result<Self, Self::Error> {
481 if value == "*" {
482 return Ok(Self::default());
483 }
484
485 let mut parts = value.split(':');
486 let permission = match parts.next() {
487 Some(v) => Permission::try_from(v)?,
488 _ => return Err(format!("invalid policy format {}", value)),
489 };
490
491 let resources = match parts.next() {
492 Some(v) => Resources::try_from(v)?,
493 _ => Resources::default(),
494 };
495
496 if parts.next().is_some() {
497 return Err(format!("invalid policy format {}", value));
498 }
499
500 Ok(Self {
501 permission,
502 resources,
503 })
504 }
505}
506
507#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
509pub struct Policies(pub BTreeSet<Policy>);
510
511impl Policies {
512 pub fn all() -> Self {
518 Self(BTreeSet::from([Policy::default()]))
519 }
520
521 pub fn read() -> Self {
527 Self(BTreeSet::from([
528 Policy {
529 permission: Permission {
530 resource: Resource::All,
531 operation: Operation::Read,
532 constraint: None,
533 },
534 resources: Resources::default(),
535 },
536 Policy {
537 permission: Permission {
538 resource: Resource::All,
539 operation: Operation::List,
540 constraint: None,
541 },
542 resources: Resources::default(),
543 },
544 ]))
545 }
546
547 pub fn append(&mut self, policies: &mut Policies) {
554 self.0.append(&mut policies.0);
555 }
556
557 pub fn remove(&mut self, policies: &Policies) {
563 self.0.retain(|p| !policies.0.contains(p));
564 }
565}
566
567impl Deref for Policies {
568 type Target = BTreeSet<Policy>;
569
570 fn deref(&self) -> &Self::Target {
571 &self.0
572 }
573}
574
575impl AsRef<BTreeSet<Policy>> for Policies {
576 fn as_ref(&self) -> &BTreeSet<Policy> {
577 &self.0
578 }
579}
580
581impl<T> PermissionChecker<T> for Policies
582where
583 T: AsRef<str>,
584{
585 fn has_permission(&self, permission: &Permission, resource_path: T) -> bool {
586 self.0
587 .iter()
588 .any(|p| p.has_permission(permission, resource_path.as_ref()))
589 }
590}
591
592impl<T> PermissionCheckerAny<T> for Policies
593where
594 T: AsRef<str>,
595{
596 fn has_permission_any(&self, permission: &Permission, resources_any: &[T]) -> bool {
597 self.0
598 .iter()
599 .any(|p| p.has_permission_any(permission, resources_any))
600 }
601}
602
603impl fmt::Display for Policies {
604 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606 match self.0.first() {
607 None => Ok(()),
608 Some(v) => {
609 write!(f, "{}", v)?;
610 for r in self.0.iter().skip(1) {
611 write!(f, " {}", r)?;
612 }
613 Ok(())
614 }
615 }
616 }
617}
618
619impl<const N: usize> From<[Policy; N]> for Policies {
620 fn from(val: [Policy; N]) -> Self {
621 Self(BTreeSet::from(val))
622 }
623}
624
625impl TryFrom<&str> for Policies {
626 type Error = String;
627
628 fn try_from(value: &str) -> Result<Self, Self::Error> {
638 if value.is_empty() {
639 return Ok(Self::default());
640 }
641
642 let policies = value
643 .split(' ')
644 .map(Policy::try_from)
645 .collect::<Result<BTreeSet<_>, _>>()?;
646 Ok(Policies(policies))
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 #[test]
655 fn test_validate_name() {
656 assert!(validate_name("").is_err());
657 assert!(validate_name("*").is_err());
658 assert!(validate_name(" ").is_err());
659 assert!(validate_name(".").is_err());
660 assert!(validate_name(",").is_err());
661 assert!(validate_name(".Info").is_err());
662 assert!(validate_name("Info").is_ok());
663 assert!(validate_name("123").is_ok());
664 assert!(validate_name("Level_1").is_ok());
665 assert!(validate_name("mmrxu-fqaaa-aaaap-ahhna-cai").is_ok());
666 }
667
668 #[test]
669 fn test_permission() {
670 for (s, p) in [
671 (
672 "Bucket.Read.Info",
673 Permission {
674 resource: Resource::Bucket,
675 operation: Operation::Read,
676 constraint: Some(Resource::Other("Info".to_string())),
677 },
678 ),
679 (
680 "Bucket.Read.File",
681 Permission {
682 resource: Resource::Bucket,
683 operation: Operation::Read,
684 constraint: Some(Resource::File),
685 },
686 ),
687 (
688 "SomeResource.some_operation",
689 Permission {
690 resource: Resource::Other("SomeResource".to_string()),
691 operation: Operation::Other("some_operation".to_string()),
692 constraint: None,
693 },
694 ),
695 (
696 "File.Read",
697 Permission {
698 resource: Resource::File,
699 operation: Operation::Read,
700 constraint: None,
701 },
702 ),
703 (
704 "File.*",
705 Permission {
706 resource: Resource::File,
707 operation: Operation::All,
708 constraint: None,
709 },
710 ),
711 (
712 "*.Read",
713 Permission {
714 resource: Resource::All,
715 operation: Operation::Read,
716 constraint: None,
717 },
718 ),
719 (
720 "*",
721 Permission {
722 resource: Resource::All,
723 operation: Operation::All,
724 constraint: None,
725 },
726 ),
727 ] {
728 assert_eq!(p.to_string(), s, "Permission({})", s);
729 assert_eq!(Permission::try_from(s).unwrap(), p);
730 }
731
732 assert!(Permission::try_from(".File").is_err());
733 assert!(Permission::try_from("File").is_err());
734 assert!(Permission::try_from("File.").is_err());
735 assert!(Permission::try_from("File.Read.Info.Info").is_err());
736
737 assert!(Permission::default().check(&Permission::default()));
738 assert!(Permission::default().check(&Permission {
739 resource: Resource::File,
740 operation: Operation::Read,
741 constraint: None,
742 }));
743 assert!(Permission::default().check(&Permission {
744 resource: Resource::Bucket,
745 operation: Operation::Read,
746 constraint: Some(Resource::File),
747 }));
748 assert!(Permission {
749 resource: Resource::Bucket,
750 operation: Operation::Read,
751 constraint: None,
752 }
753 .check(&Permission {
754 resource: Resource::Bucket,
755 operation: Operation::Read,
756 constraint: Some(Resource::Other("Info".to_string())),
757 }));
758
759 assert!(!Permission {
760 resource: Resource::Bucket,
761 operation: Operation::Read,
762 constraint: Some(Resource::Other("Info".to_string())),
763 }
764 .check(&Permission {
765 resource: Resource::Bucket,
766 operation: Operation::Read,
767 constraint: Some(Resource::File),
768 }));
769 assert!(!Permission {
770 resource: Resource::Bucket,
771 operation: Operation::Write,
772 constraint: None,
773 }
774 .check(&Permission {
775 resource: Resource::Bucket,
776 operation: Operation::Read,
777 constraint: None,
778 }));
779 assert!(!Permission {
780 resource: Resource::Folder,
781 operation: Operation::Write,
782 constraint: None,
783 }
784 .check(&Permission {
785 resource: Resource::File,
786 operation: Operation::Write,
787 constraint: None,
788 }));
789 }
790
791 #[test]
792 fn test_resources() {
793 let rs = Resources::default();
794 assert_eq!(rs.to_string(), "");
795 assert_eq!(Resources::try_from("").unwrap(), rs);
796 assert!(rs.check(""));
797 assert!(rs.check("123"));
798 assert!(rs.check("abc"));
799
800 let rs = Resources::try_from("*").unwrap();
801 assert!(rs.check(""));
802 assert!(rs.check("123"));
803 assert!(rs.check("abc"));
804 assert_eq!(rs.to_string(), "");
805
806 let rs = Resources::from(["1".to_string()]);
807 assert_eq!(rs.to_string(), "1");
808 assert_eq!(Resources::try_from("1").unwrap(), rs);
809 assert!(rs.check("1"));
810 assert!(!rs.check("2"));
811 assert!(!rs.check(""));
812 assert!(!rs.check("12"));
813 assert!(!rs.check("a"));
814
815 let rs = Resources::from(["1".to_string(), "2".to_string(), "3".to_string()]);
816 assert_eq!(rs.to_string(), "1,2,3");
817 assert_eq!(Resources::try_from("1,2,3").unwrap(), rs);
818 assert!(rs.check("1"));
819 assert!(rs.check("2"));
820 assert!(!rs.check(""));
821 assert!(!rs.check("12"));
822 assert!(!rs.check("a"));
823
824 assert!(Resources::try_from("1, 2").is_err());
825 assert!(Resources::try_from("1,2 ").is_err());
826 assert!(Resources::try_from("1,2.3").is_err());
827 }
828
829 #[test]
830 fn test_policy() {
831 let po = Policy::default();
832 assert_eq!(po.to_string(), "*");
833 assert_eq!(Policy::try_from("*").unwrap(), po);
834 assert_eq!(Policy::try_from("*:*").unwrap(), po);
835 assert_eq!(Policy::try_from("*.*:*").unwrap(), po);
836 assert!(po.has_permission(
837 &Permission {
838 resource: Resource::File,
839 operation: Operation::Read,
840 constraint: None,
841 },
842 ""
843 ));
844 assert!(po.has_permission(
845 &Permission {
846 resource: Resource::Folder,
847 operation: Operation::Write,
848 constraint: None,
849 },
850 "1"
851 ));
852
853 let po = Policy {
854 permission: Permission {
855 resource: Resource::File,
856 operation: Operation::All,
857 constraint: None,
858 },
859 resources: Resources::from(["123".to_string()]),
860 };
861 assert_eq!(po.to_string(), "File.*:123");
862 assert_eq!(Policy::try_from("File.*:123").unwrap(), po);
863 assert!(po.has_permission(
864 &Permission {
865 resource: Resource::File,
866 operation: Operation::Read,
867 constraint: None,
868 },
869 "123"
870 ));
871 assert!(po.has_permission(
872 &Permission {
873 resource: Resource::File,
874 operation: Operation::Write,
875 constraint: None,
876 },
877 "123"
878 ));
879 assert!(!po.has_permission(
880 &Permission {
881 resource: Resource::File,
882 operation: Operation::Read,
883 constraint: None,
884 },
885 "1"
886 ));
887 assert!(!po.has_permission(
888 &Permission {
889 resource: Resource::File,
890 operation: Operation::Write,
891 constraint: None,
892 },
893 "1"
894 ));
895 assert!(!po.has_permission(
896 &Permission {
897 resource: Resource::Folder,
898 operation: Operation::Write,
899 constraint: None,
900 },
901 "123"
902 ));
903 assert!(!po.has_permission(
904 &Permission {
905 resource: Resource::File,
906 operation: Operation::Write,
907 constraint: None,
908 },
909 ""
910 ));
911 }
912
913 #[test]
914 fn test_policies() {
915 let ps = Policies::default();
916 assert_eq!(ps.to_string(), "");
917 assert!(!ps.has_permission(
918 &Permission {
919 resource: Resource::File,
920 operation: Operation::Read,
921 constraint: None,
922 },
923 ""
924 ));
925
926 let ps = Policies::all();
927
928 assert_eq!(Policies::try_from("*").unwrap(), ps);
929 assert_eq!(Policies::try_from("*:*").unwrap(), ps);
930 assert_eq!(Policies::try_from("*.*:*").unwrap(), ps);
931 assert!(ps.has_permission(
932 &Permission {
933 resource: Resource::File,
934 operation: Operation::Read,
935 constraint: None,
936 },
937 ""
938 ));
939 assert!(ps.has_permission(
940 &Permission {
941 resource: Resource::File,
942 operation: Operation::Read,
943 constraint: None,
944 },
945 "123"
946 ));
947 assert!(ps.has_permission(
948 &Permission {
949 resource: Resource::Bucket,
950 operation: Operation::Write,
951 constraint: Some(Resource::Folder),
952 },
953 "bucket1"
954 ));
955
956 let ps = Policies::from([
957 Policy {
958 permission: Permission {
959 resource: Resource::Bucket,
960 operation: Operation::Read,
961 constraint: Some(Resource::All),
962 },
963 resources: Resources::from([]),
964 },
965 Policy {
966 permission: Permission {
967 resource: Resource::Folder,
968 operation: Operation::Read,
969 constraint: None,
970 },
971 resources: Resources::default(),
972 },
973 Policy {
974 permission: Permission {
975 resource: Resource::Folder,
976 operation: Operation::All,
977 constraint: None,
978 },
979 resources: Resources::from(["2".to_string(), "3".to_string(), "5".to_string()]),
980 },
981 Policy {
982 permission: Permission {
983 resource: Resource::File,
984 operation: Operation::All,
985 constraint: None,
986 },
987 resources: Resources::from(["1".to_string()]),
988 },
989 ]);
990
991 println!("{}", ps);
992 let scope = "File.*:1 Folder.*:2,3,5 Folder.Read Bucket.Read";
993 assert_eq!(ps.to_string(), scope);
994 assert_eq!(Policies::try_from(scope).unwrap().to_string(), scope);
995
996 assert!(ps.has_permission(
998 &Permission {
999 resource: Resource::File,
1000 operation: Operation::Delete,
1001 constraint: None,
1002 },
1003 "1"
1004 ));
1005
1006 assert!(ps.has_permission(
1008 &Permission {
1009 resource: Resource::File,
1010 operation: Operation::Read,
1011 constraint: Some(Resource::Other("Info".to_string())),
1012 },
1013 "1"
1014 ));
1015
1016 assert!(!ps.has_permission(
1018 &Permission {
1019 resource: Resource::File,
1020 operation: Operation::Read,
1021 constraint: Some(Resource::Other("Info".to_string())),
1022 },
1023 "2"
1024 ));
1025
1026 assert!(ps.has_permission(
1028 &Permission {
1029 resource: Resource::File,
1030 operation: Operation::All,
1031 constraint: None,
1032 },
1033 "1"
1034 ));
1035
1036 assert!(ps.has_permission(
1038 &Permission {
1039 resource: Resource::Folder,
1040 operation: Operation::Delete,
1041 constraint: Some(Resource::File),
1042 },
1043 "2"
1044 ));
1045
1046 assert!(!ps.has_permission(
1048 &Permission {
1049 resource: Resource::Folder,
1050 operation: Operation::Delete,
1051 constraint: Some(Resource::File),
1052 },
1053 "4"
1054 ));
1055
1056 assert!(ps.has_permission_any(
1058 &Permission {
1059 resource: Resource::Folder,
1060 operation: Operation::Delete,
1061 constraint: Some(Resource::File),
1062 },
1063 &["4", "5"]
1064 ));
1065 assert!(ps.has_permission_any(
1066 &Permission {
1067 resource: Resource::Folder,
1068 operation: Operation::Delete,
1069 constraint: Some(Resource::File),
1070 },
1071 &[4.to_string(), 5.to_string()]
1072 ));
1073
1074 assert!(ps.has_permission(
1076 &Permission {
1077 resource: Resource::Folder,
1078 operation: Operation::Read,
1079 constraint: Some(Resource::Other("Info".to_string())),
1080 },
1081 "1"
1082 ));
1083
1084 assert!(ps.has_permission(
1086 &Permission {
1087 resource: Resource::Folder,
1088 operation: Operation::Read,
1089 constraint: Some(Resource::File),
1090 },
1091 "6"
1092 ));
1093
1094 assert!(ps.has_permission(
1096 &Permission {
1097 resource: Resource::Bucket,
1098 operation: Operation::Read,
1099 constraint: Some(Resource::Folder),
1100 },
1101 "1"
1102 ));
1103
1104 assert!(!ps.has_permission(
1106 &Permission {
1107 resource: Resource::Bucket,
1108 operation: Operation::Write,
1109 constraint: Some(Resource::Folder),
1110 },
1111 "1"
1112 ));
1113 }
1114}