ic_oss_types/
permission.rs

1use std::collections::BTreeSet;
2use std::fmt;
3use std::ops::Deref;
4
5/// Validates the name of a resource, operation, constraint, or resource path.
6///
7/// # Arguments
8/// * `s` - A string slice that holds the name to be validated.
9///
10/// # Returns
11/// * `Ok(())` if the name only contains valid characters (A-Z, a-z, 0-9, '_', '-').
12/// * `Err(String)` if the name is empty or contains invalid characters.
13///
14pub 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/// Represents a resource within the permission model, which could be generic or specific.
28#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
29pub enum Resource {
30    #[default]
31    All, // Represents all resources, denoted by "*"
32    File,
33    Folder,
34    Bucket,
35    Cluster,
36    Other(String),
37}
38
39impl Resource {
40    /// Checks if a given resource matches the current resource.
41    ///
42    /// # Arguments
43    /// * `value` - A reference to another `Resource` to compare with.
44    ///
45    /// # Returns
46    /// * `true` if the resources match or if the current resource represents all resources.
47    /// * `false` otherwise.
48    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    /// Formats the `Resource` enum into a human-readable string.
58    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    /// Attempts to create a `Resource` from a string slice.
74    ///
75    /// # Arguments
76    /// * `value` - The string slice to parse into a `Resource`.
77    ///
78    /// # Returns
79    /// * `Ok(Resource)` if successfully parsed.
80    /// * `Err(String)` if the input is invalid or does not match any known resource.
81    ///
82    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/// Represents an operation that can be performed on a resource.
98#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
99pub enum Operation {
100    #[default]
101    All, // Represents all operations, denoted by "*"
102    List,
103    Read,
104    Write,
105    Delete,
106    Other(String),
107}
108
109impl Operation {
110    /// Checks if a given operation matches the current operation.
111    ///
112    /// # Arguments
113    /// * `value` - A reference to another `Operation` to compare with.
114    ///
115    /// # Returns
116    /// * `true` if the operations match or if the current operation represents all operations.
117    /// * `false` otherwise.
118    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    /// Formats the `Operation` enum into a human-readable string.
128    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    /// Attempts to create an `Operation` from a string slice.
144    ///
145    /// # Arguments
146    /// * `value` - The string slice to parse into an `Operation`.
147    ///
148    /// # Returns
149    /// * `Ok(Operation)` if successfully parsed.
150    /// * `Err(String)` if the input is invalid or does not match any known operation.
151    ///
152    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/// Represents a permission string in the format "Resource.Operation[.Constraint]".
168///
169/// # Fields
170/// * `resource` - The resource to which the permission applies.
171/// * `operation` - The operation allowed on the resource.
172/// * `constraint` - An optional constraint on the resource.
173///
174#[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    /// Checks if the permission grants unrestricted access to all resources and operations.
183    ///
184    /// # Returns
185    /// * `true` if the permission represents all resources and operations without constraints.
186    /// * `false` otherwise.
187    ///
188    pub fn is_all(&self) -> bool {
189        self.resource == Resource::All
190            && self.operation == Operation::All
191            && self.constraint.is_none()
192    }
193
194    /// Compares a given `Permission` object to the current one for a match.
195    ///
196    /// # Arguments
197    /// * `value` - A reference to another `Permission` to compare with.
198    ///
199    /// # Returns
200    /// * `true` if the permissions match, considering resources, operations, and constraints.
201    /// * `false` otherwise.
202    ///
203    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    /// Helper method to check constraints.
210    ///
211    /// # Arguments
212    /// * `value` - Optional reference to a `Resource` that represents the constraint.
213    ///
214    /// # Returns
215    /// * `true` if there are no constraints or the constraints match.
216    /// * `false` otherwise.
217    ///
218    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    /// Formats the `Permission` struct into a human-readable string, considering constraints.
228    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    /// Attempts to create a `Permission` from a string slice.
248    ///
249    /// # Arguments
250    /// * `value` - The string slice to parse into a `Permission`.
251    ///
252    /// # Returns
253    /// * `Ok(Permission)` if successfully parsed.
254    /// * `Err(String)` if the input is invalid or does not match the expected format.
255    ///
256    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
295/// Represents a resource paths.
296pub type ResourcePath = String;
297
298/// Represents a collection of resource paths.
299#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
300pub struct Resources(pub BTreeSet<ResourcePath>);
301
302impl Resources {
303    /// Checks if the collection represents all resources.
304    ///
305    /// # Returns
306    /// * `true` if the collection is empty or contains the wildcard "*".
307    /// * `false` otherwise.
308    ///
309    pub fn is_all(&self) -> bool {
310        self.0.is_empty() || self.0.contains("*")
311    }
312
313    /// Checks if a given resource path is part of the collection.
314    ///
315    /// # Arguments
316    /// * `value` - The resource path to check.
317    ///
318    /// # Returns
319    /// * `true` if the collection contains the resource path or represents all resources.
320    /// * `false` otherwise.
321    ///
322    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    /// Formats the `Resources` struct into a comma-separated string.
346    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    /// Attempts to create `Resources` from a comma-separated string slice.
372    ///
373    /// # Arguments
374    /// * `value` - The string slice to parse into `Resources`.
375    ///
376    /// # Returns
377    /// * `Ok(Resources)` if successfully parsed.
378    /// * `Err(String)` if any resource name is invalid.
379    ///
380    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
394/// A trait for checking permission on a single resource.
395pub trait PermissionChecker<T> {
396    /// Checks if a permission is granted on a resource.
397    ///
398    /// # Arguments
399    /// * `permission` - The permission to check.
400    /// * `resource_path` - The path of the resource.
401    ///
402    /// # Returns
403    /// * `true` if the permission is granted.
404    /// * `false` otherwise.
405    fn has_permission(&self, permission: &Permission, resource_path: T) -> bool;
406}
407
408/// A trait for checking permissions on multiple resources.
409pub trait PermissionCheckerAny<T> {
410    /// Checks if a permission is granted on any of the given resources.
411    ///
412    /// # Arguments
413    /// * `permission` - The permission to check.
414    /// * `resources_path` - The paths of the resources.
415    ///
416    /// # Returns
417    /// * `true` if the permission is granted on any of the resources.
418    /// * `false` otherwise.
419    fn has_permission_any(&self, permission: &Permission, resources_path: &[T]) -> bool;
420}
421
422/// Represents a policy string in the format "Permission:Resource1,Resource2,...".
423///
424/// # Fields
425/// * `permission` - The permission associated with the policy.
426/// * `resources` - The resources associated with the policy.
427///
428#[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    /// Formats the `Policy` struct into a human-readable string.
455    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    /// Attempts to create a `Policy` from a string slice.
472    ///
473    /// # Arguments
474    /// * `value` - The string slice to parse into a `Policy`.
475    ///
476    /// # Returns
477    /// * `Ok(Policy)` if successfully parsed.
478    /// * `Err(String)` if the input is invalid or does not match the expected format.
479    ///
480    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/// Represents a collection of policies.
508#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
509pub struct Policies(pub BTreeSet<Policy>);
510
511impl Policies {
512    /// Creates policies with all permissions for all resources.
513    ///
514    /// # Returns
515    /// * `Policies` containing a policy with all permissions for all resources.
516    ///
517    pub fn all() -> Self {
518        Self(BTreeSet::from([Policy::default()]))
519    }
520
521    /// Creates policies with read and list permissions for all resources.
522    ///
523    /// # Returns
524    /// * `Policies` containing policies with read and list permissions for all resources.
525    ///
526    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    // TODO: compress policies
548    /// Appends policies to the current collection.
549    ///
550    /// # Arguments
551    /// * `policies` - The policies to append.
552    ///
553    pub fn append(&mut self, policies: &mut Policies) {
554        self.0.append(&mut policies.0);
555    }
556
557    /// Removes policies from the current collection.
558    ///
559    /// # Arguments
560    /// * `policies` - The policies to remove.
561    ///
562    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    /// Formats the `Policies` struct into a human-readable string.
605    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    /// Attempts to create `Policies` from a space-separated string slice.
629    ///
630    /// # Arguments
631    /// * `value` - The string slice to parse into `Policies`.
632    ///
633    /// # Returns
634    /// * `Ok(Policies)` if successfully parsed.
635    /// * `Err(String)` if any policy is invalid.
636    ///
637    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        // File.*:1
997        assert!(ps.has_permission(
998            &Permission {
999                resource: Resource::File,
1000                operation: Operation::Delete,
1001                constraint: None,
1002            },
1003            "1"
1004        ));
1005
1006        // File.*:1
1007        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        // File.*:1
1017        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        // File.*:1
1027        assert!(ps.has_permission(
1028            &Permission {
1029                resource: Resource::File,
1030                operation: Operation::All,
1031                constraint: None,
1032            },
1033            "1"
1034        ));
1035
1036        // Folder.*:2,3,5
1037        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        // Folder.*:2,3,5
1047        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        // Folder.*:2,3,5
1057        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        // Folder.Read
1075        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        // Folder.Read
1085        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        // Bucket.Read
1095        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        // Bucket.Read
1105        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}