allow_me/core/
mod.rs

1use std::{
2    cmp::Ordering,
3    collections::{btree_map::Entry, BTreeMap},
4};
5
6use crate::errors::Result;
7use crate::{substituter::Substituter, Error, ResourceMatcher};
8
9mod builder;
10pub use builder::{Effect, PolicyBuilder, PolicyDefinition, Statement};
11
12/// Policy engine. Represents a read-only set of rules and can
13/// evaluate [`Request`] based on those rules.
14///
15/// Policy engine consists of two sets:
16/// - static rules
17/// - variable rules - any rule that contains variables ("{{..}}").
18/// Static rules are organized in a data structure with fast querying time.
19/// Variable rules are evaluated on every request.
20#[derive(Debug)]
21pub struct Policy<R, S> {
22    default_decision: Decision,
23    resource_matcher: R,
24    substituter: S,
25    static_rules: BTreeMap<String, Operations>,
26    variable_rules: BTreeMap<String, Operations>,
27}
28
29impl<R, S, RC> Policy<R, S>
30where
31    R: ResourceMatcher<Context = RC>,
32    S: Substituter<Context = RC>,
33{
34    /// Evaluates the provided [`Request`] and produces the [`Decision`].
35    ///
36    /// If no rules match the Request - [the default `Decision`](`PolicyBuilder::with_default_decision`) is returned.
37    pub fn evaluate(&self, request: &Request<RC>) -> Result<Decision> {
38        match self.eval_static_rules(request) {
39            // static rules undefined. Need to check variable rules.
40            Ok(None) => match self.eval_variable_rules(request) {
41                // variable rules undefined as well. Return default decision.
42                Ok(None) => Ok(self.default_decision),
43                // variable rules defined. Return the decision.
44                Ok(Some(effect)) => Ok(effect.into()),
45                Err(e) => Err(e),
46            },
47            // static rules are defined. Evaluate variable rules and compare priority.
48            Ok(Some(static_effect)) => {
49                match self.eval_variable_rules(request) {
50                    // variable rules undefined. Proceed with static rule decision.
51                    Ok(None) => Ok(static_effect.into()),
52                    // variable rules defined. Compare priority and return.
53                    Ok(Some(variable_effect)) => {
54                        // compare order.
55                        Ok(if variable_effect > static_effect {
56                            static_effect
57                        } else {
58                            variable_effect
59                        }
60                        .into())
61                    }
62                    Err(e) => Err(e),
63                }
64            }
65
66            Err(e) => Err(e),
67        }
68    }
69
70    fn eval_static_rules(&self, request: &Request<RC>) -> Result<Option<EffectOrd>> {
71        // lookup an identity
72        match self.static_rules.get(&request.identity) {
73            // identity exists. Look up operations.
74            Some(operations) => match operations.0.get(&request.operation) {
75                // operation exists.
76                Some(resources) => {
77                    // iterate over and match resources.
78                    // we need to go through all resources and find one with highest priority (smallest order).
79                    let mut result: Option<EffectOrd> = None;
80                    for (resource, effect) in &resources.0 {
81                        // check the order first
82                        if effect.order < result.map_or(usize::MAX, |e| e.order)
83                             // only then check that matches
84                            && self.resource_matcher.do_match( // only then check that matches
85                                request,
86                                &request.resource,
87                                &resource,
88                            )
89                        {
90                            result = Some(*effect);
91                        }
92                    }
93                    Ok(result)
94                }
95                None => Ok(None),
96            },
97            None => Ok(None),
98        }
99    }
100
101    fn eval_variable_rules(&self, request: &Request<RC>) -> Result<Option<EffectOrd>> {
102        for (identity, operations) in &self.variable_rules {
103            // process identity variables.
104            let identity = self.substituter.visit_identity(identity, request)?;
105            // check if it does match after processing variables.
106            if identity == request.identity {
107                // lookup operation.
108                return match operations.0.get(&request.operation) {
109                    // operation exists.
110                    Some(resources) => {
111                        // iterate over and match resources.
112                        // we need to go through all resources and find one with highest priority (smallest order).
113                        let mut result: Option<EffectOrd> = None;
114                        for (resource, effect) in &resources.0 {
115                            let resource = self.substituter.visit_resource(resource, request)?;
116                            // check the order first
117                            if effect.order < result.map_or(usize::MAX, |e| e.order)
118                                // only then check that matches
119                                && self.resource_matcher.do_match(
120                                    request,
121                                    &request.resource,
122                                    &resource,
123                                )
124                            {
125                                result = Some(*effect);
126                            }
127                        }
128                        // continue to look for other identity variable rules
129                        // if no resources matched the current one.
130                        if result == None {
131                            continue;
132                        }
133                        Ok(result)
134                    }
135                    // continue to look for other identity variable rules
136                    // if no operation found for the current one.
137                    None => continue,
138                };
139            }
140        }
141        Ok(None)
142    }
143}
144
145#[derive(Debug, Clone)]
146struct Identities(BTreeMap<String, Operations>);
147
148impl Identities {
149    pub fn new() -> Self {
150        Identities(BTreeMap::new())
151    }
152
153    pub fn merge(&mut self, collection: Identities) {
154        for (key, value) in collection.0 {
155            self.insert(&key, value);
156        }
157    }
158
159    fn insert(&mut self, operation: &str, resources: Operations) {
160        if !resources.0.is_empty() {
161            let entry = self.0.entry(operation.to_string());
162            match entry {
163                Entry::Vacant(item) => {
164                    item.insert(resources);
165                }
166                Entry::Occupied(mut item) => item.get_mut().merge(resources),
167            }
168        }
169    }
170}
171
172#[derive(Debug, Clone)]
173struct Operations(BTreeMap<String, Resources>);
174
175impl Operations {
176    pub fn new() -> Self {
177        Operations(BTreeMap::new())
178    }
179
180    pub fn merge(&mut self, collection: Operations) {
181        for (key, value) in collection.0 {
182            self.insert(&key, value);
183        }
184    }
185
186    fn insert(&mut self, operation: &str, resources: Resources) {
187        if !resources.0.is_empty() {
188            let entry = self.0.entry(operation.to_string());
189            match entry {
190                Entry::Vacant(item) => {
191                    item.insert(resources);
192                }
193                Entry::Occupied(mut item) => item.get_mut().merge(resources),
194            }
195        }
196    }
197}
198
199impl From<BTreeMap<String, Resources>> for Operations {
200    fn from(map: BTreeMap<String, Resources>) -> Self {
201        Operations(map)
202    }
203}
204
205#[derive(Debug, Clone)]
206struct Resources(BTreeMap<String, EffectOrd>);
207
208impl Resources {
209    pub fn new() -> Self {
210        Resources(BTreeMap::new())
211    }
212
213    pub fn merge(&mut self, collection: Resources) {
214        for (key, value) in collection.0 {
215            self.insert(&key, value);
216        }
217    }
218
219    fn insert(&mut self, resource: &str, effect: EffectOrd) {
220        let entry = self.0.entry(resource.to_string());
221        match entry {
222            Entry::Vacant(item) => {
223                item.insert(effect);
224            }
225            Entry::Occupied(mut item) => item.get_mut().merge(effect),
226        }
227    }
228}
229
230impl From<BTreeMap<String, EffectOrd>> for Resources {
231    fn from(map: BTreeMap<String, EffectOrd>) -> Self {
232        Resources(map)
233    }
234}
235
236/// Represents a request that needs to be evaluated by [`Policy`] engine.
237#[derive(Debug)]
238pub struct Request<RC> {
239    identity: String,
240    operation: String,
241    resource: String,
242
243    /// Optional request context that can be used for request processing.
244    context: Option<RC>,
245}
246
247impl<RC> Request<RC> {
248    /// Creates a new [`Request`].
249    /// # Errors
250    /// Returns an error if either identity or operation is an empty string.
251    pub fn new(
252        identity: impl Into<String>,
253        operation: impl Into<String>,
254        resource: impl Into<String>,
255    ) -> Result<Self> {
256        Self::create(identity, operation, resource, None)
257    }
258
259    /// Creates a new [`Request`] with provided context data.
260    /// # Errors
261    /// Returns an error if either identity or operation is an empty string.
262    pub fn with_context(
263        identity: impl Into<String>,
264        operation: impl Into<String>,
265        resource: impl Into<String>,
266        context: RC,
267    ) -> Result<Self> {
268        Self::create(identity, operation, resource, Some(context))
269    }
270
271    fn create(
272        identity: impl Into<String>,
273        operation: impl Into<String>,
274        resource: impl Into<String>,
275        context: Option<RC>,
276    ) -> Result<Self> {
277        let (identity, operation, resource) = (identity.into(), operation.into(), resource.into());
278
279        if identity.is_empty() {
280            return Err(Error::BadRequest("Identity must be specified".into()));
281        }
282
283        if operation.is_empty() {
284            return Err(Error::BadRequest("Operation must be specified".into()));
285        }
286
287        Ok(Self {
288            identity,
289            operation,
290            resource,
291            context,
292        })
293    }
294
295    pub fn identity(&self) -> &str {
296        &self.identity
297    }
298
299    pub fn operation(&self) -> &str {
300        &self.operation
301    }
302
303    pub fn resource(&self) -> &str {
304        &self.resource
305    }
306
307    pub fn context(&self) -> Option<&RC> {
308        self.context.as_ref()
309    }
310}
311
312/// Represents a decision on the `Request` to the `Policy` engine.
313#[derive(Debug, Copy, Clone, PartialEq)]
314pub enum Decision {
315    Allowed,
316    Denied,
317}
318
319#[derive(Debug, Copy, Clone, PartialEq)]
320struct EffectOrd {
321    order: usize,
322    effect: Effect,
323}
324
325impl EffectOrd {
326    pub fn new(effect: Effect, order: usize) -> Self {
327        Self { order, effect }
328    }
329
330    /// Merges two `EffectOrd` by replacing with the one with higher priority.
331    ///
332    /// Lower the order value => higher the effect priority.
333    pub fn merge(&mut self, item: EffectOrd) {
334        if self.order > item.order {
335            *self = item;
336        }
337    }
338}
339
340impl PartialOrd for EffectOrd {
341    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
342        Some(self.order.cmp(&other.order))
343    }
344}
345
346impl From<EffectOrd> for Decision {
347    fn from(effect: EffectOrd) -> Self {
348        match effect.effect {
349            Effect::Allow => Decision::Allowed,
350            Effect::Deny => Decision::Denied,
351        }
352    }
353}
354
355impl From<&Statement> for EffectOrd {
356    fn from(statement: &Statement) -> Self {
357        match statement.effect() {
358            builder::Effect::Allow => EffectOrd::new(Effect::Allow, statement.order()),
359            builder::Effect::Deny => EffectOrd::new(Effect::Deny, statement.order()),
360        }
361    }
362}
363
364#[cfg(test)]
365pub(crate) mod tests {
366    use super::*;
367
368    use crate::{matcher::Default, DefaultSubstituter};
369
370    use assert_matches::assert_matches;
371
372    /// Helper method to build a policy.
373    /// Used in both policy and builder tests.
374    pub(crate) fn build_policy(json: &str) -> Policy<Default, DefaultSubstituter> {
375        PolicyBuilder::from_json(json)
376            .with_default_decision(Decision::Denied)
377            .build()
378            .expect("Unable to build policy from json.")
379    }
380
381    #[test]
382    fn evaluate_static_rules() {
383        let json = r#"{
384            "statements": [
385                {
386                    "effect": "deny",
387                    "identities": [
388                        "actor_a"
389                    ],
390                    "operations": [
391                        "write"
392                    ],
393                    "resources": [
394                        "resource_1"
395                    ]
396                },
397                {
398                    "effect": "allow",
399                    "identities": [
400                        "actor_b"
401                    ],
402                    "operations": [
403                        "read"
404                    ],
405                    "resources": [
406                        "resource_1"
407                    ]
408                }
409            ]
410        }"#;
411
412        let policy = build_policy(json);
413
414        let request = Request::new("actor_a", "write", "resource_1").unwrap();
415
416        assert_matches!(policy.evaluate(&request), Ok(Decision::Denied));
417
418        let request = Request::new("actor_b", "read", "resource_1").unwrap();
419
420        assert_matches!(policy.evaluate(&request), Ok(Decision::Allowed));
421    }
422
423    #[test]
424    fn evaluate_undefined_rules_expected_default_action() {
425        let json = r#"{
426            "statements": [
427                {
428                    "effect": "allow",
429                    "identities": [
430                        "actor_a"
431                    ],
432                    "operations": [
433                        "write"
434                    ],
435                    "resources": [
436                        "resource_1"
437                    ]
438                }
439            ]
440        }"#;
441
442        // assert default allow
443        let request = Request::new("other_actor", "write", "resource_1").unwrap();
444
445        let allow_default_policy = PolicyBuilder::from_json(json)
446            .with_default_decision(Decision::Allowed)
447            .build()
448            .expect("Unable to build policy from json.");
449
450        assert_matches!(
451            allow_default_policy.evaluate(&request),
452            Ok(Decision::Allowed)
453        );
454
455        // assert default deny
456        let deny_default_policy = PolicyBuilder::from_json(json)
457            .with_default_decision(Decision::Denied)
458            .build()
459            .expect("Unable to build policy from json.");
460
461        assert_matches!(deny_default_policy.evaluate(&request), Ok(Decision::Denied));
462    }
463
464    #[test]
465    fn evaluate_static_variable_rule_conflict_first_rule_wins() {
466        let json = r#"{
467            "statements": [
468                {
469                    "effect": "allow",
470                    "identities": [
471                        "actor_a"
472                    ],
473                    "operations": [
474                        "write"
475                    ],
476                    "resources": [
477                        "resource_1"
478                    ]
479                },
480                {
481                    "effect": "deny",
482                    "identities": [
483                        "{{test}}"
484                    ],
485                    "operations": [
486                        "write"
487                    ],
488                    "resources": [
489                        "resource_1"
490                    ]
491                },               
492                {
493                    "effect": "allow",
494                    "identities": [
495                        "{{test}}"
496                    ],
497                    "operations": [
498                        "read"
499                    ],
500                    "resources": [
501                        "resource_group"
502                    ]
503                },
504                {
505                    "effect": "deny",
506                    "identities": [
507                        "actor_b"
508                    ],
509                    "operations": [
510                        "read"
511                    ],
512                    "resources": [
513                        "resource_group"
514                    ]
515                }
516            ]
517        }"#;
518
519        let policy = PolicyBuilder::from_json(json)
520            .with_default_decision(Decision::Denied)
521            .with_substituter(TestIdentitySubstituter)
522            .build()
523            .expect("Unable to build policy from json.");
524
525        // assert static rule wins
526        let request = Request::new("actor_a", "write", "resource_1").unwrap();
527
528        assert_matches!(policy.evaluate(&request), Ok(Decision::Allowed));
529
530        // assert variable rule wins
531        let request = Request::new("actor_b", "read", "resource_group").unwrap();
532
533        assert_matches!(policy.evaluate(&request), Ok(Decision::Allowed));
534    }
535
536    #[test]
537    fn evaluate_rule_no_resource() {
538        let json = r#"{
539            "statements": [
540                {
541                    "effect": "allow",
542                    "identities": [
543                        "actor_a"
544                    ],
545                    "operations": [
546                        "connect"
547                    ]
548                }
549            ]
550        }"#;
551
552        let policy = build_policy(json);
553
554        let request = Request::new("actor_a", "connect", "").unwrap();
555
556        assert_matches!(policy.evaluate(&request), Ok(Decision::Allowed));
557    }
558
559    #[test]
560    fn evaluate_variable_rule_no_resource() {
561        let json = r#"{
562            "statements": [
563                {
564                    "effect": "deny",
565                    "identities": [
566                        "actor_a"
567                    ],
568                    "operations": [
569                        "connect"
570                    ]
571                },
572                {
573                    "effect": "allow",
574                    "identities": [
575                        "{{test}}"
576                    ],
577                    "operations": [
578                        "connect"
579                    ]
580                }
581            ]
582        }"#;
583
584        let policy = PolicyBuilder::from_json(json)
585            .with_default_decision(Decision::Denied)
586            .with_substituter(TestIdentitySubstituter)
587            .build()
588            .expect("Unable to build policy from json.");
589
590        let request = Request::new("actor_a", "connect", "").unwrap();
591
592        assert_matches!(policy.evaluate(&request), Ok(Decision::Denied));
593
594        let request = Request::new("other_actor", "connect", "").unwrap();
595
596        assert_matches!(policy.evaluate(&request), Ok(Decision::Allowed));
597    }
598
599    #[test]
600    fn evaluate_definition_no_statements() {
601        let json = r#"{
602            "statements": [ ]
603        }"#;
604
605        let policy = build_policy(json);
606
607        let request = Request::new("actor_a", "connect", "").unwrap();
608
609        // default decision expected.
610        assert_matches!(policy.evaluate(&request), Ok(Decision::Denied));
611    }
612
613    /// Scenario:
614    /// - Have a policy with a custom resource matcher
615    /// - Have conflicting rules for resources that
616    ///   are different, but both will match according to
617    ///   custom resource matcher.
618    /// - Expected: match first "allow" rule
619    ///
620    /// This case is created as a result of a discovered bug.
621    #[test]
622    fn rule_ordering_should_work_for_custom_matchers() {
623        let json = r###"{
624            "statements": [
625                {
626                    "effect": "allow",
627                    "identities": [
628                        "actor_a"
629                    ],
630                    "operations": [
631                        "write"
632                    ],
633                    "resources": [
634                        "hello/b"
635                    ]
636                },
637                {
638                    "effect": "deny",
639                    "identities": [
640                        "actor_a"
641                    ],
642                    "operations": [
643                        "write"
644                    ],
645                    "resources": [
646                        "hello/a"
647                    ]
648                }
649            ]
650        }"###;
651
652        let policy = PolicyBuilder::from_json(json)
653            .with_default_decision(Decision::Denied)
654            .with_substituter(TestIdentitySubstituter)
655            .with_matcher(StartWithMatcher)
656            .build()
657            .expect("Unable to build policy from json.");
658
659        let request = Request::new("actor_a", "write", "hello").unwrap();
660
661        assert_matches!(policy.evaluate(&request), Ok(Decision::Allowed));
662    }
663
664    /// See test case above for details.
665    #[test]
666    fn rule_ordering_should_work_for_custom_matchers_variable_rules() {
667        let json = r###"{
668            "statements": [
669                {
670                    "effect": "allow",
671                    "identities": [
672                        "{{any}}"
673                    ],
674                    "operations": [
675                        "write"
676                    ],
677                    "resources": [
678                        "hello/b"
679                    ]
680                },
681                {
682                    "effect": "deny",
683                    "identities": [
684                        "{{any}}"
685                    ],
686                    "operations": [
687                        "write"
688                    ],
689                    "resources": [
690                        "hello/a"
691                    ]
692                }
693            ]
694        }"###;
695
696        let policy = PolicyBuilder::from_json(json)
697            .with_default_decision(Decision::Denied)
698            .with_substituter(TestIdentitySubstituter)
699            .with_matcher(StartWithMatcher)
700            .build()
701            .expect("Unable to build policy from json.");
702
703        let request = Request::new("actor_a", "write", "hello").unwrap();
704
705        assert_matches!(policy.evaluate(&request), Ok(Decision::Allowed));
706    }
707
708    /// Scenario:
709    /// - Have a policy with a custom identity matcher
710    /// - Have two variable rules (deny and allow) for an identity, such that
711    ///   both rules match a given request identity.
712    /// - But the two rules must be different in resources.
713    /// - Make a request to the allowed resource.
714    /// - The deny rule resources do not match the request.
715    /// - The allow rule resources do match the request.
716    /// - Expected: request allowed.
717    ///
718    /// This case is created as a result of a discovered bug.
719    #[test]
720    fn all_identity_variable_rules_must_be_evaluated_resources_do_not_match() {
721        let json = r###"{
722            "statements": [
723                {
724                    "effect": "deny",
725                    "identities": [
726                        "{{any}}"
727                    ],
728                    "operations": [
729                        "write"
730                    ],
731                    "resources": [
732                        "hello/b"
733                    ]
734                },
735                {
736                    "effect": "allow",
737                    "identities": [
738                        "{{identity}}"
739                    ],
740                    "operations": [
741                        "write"
742                    ],
743                    "resources": [
744                        "hello/a"
745                    ]
746                }
747            ]
748        }"###;
749
750        let policy = PolicyBuilder::from_json(json)
751            .with_default_decision(Decision::Denied)
752            .with_substituter(TestIdentitySubstituter)
753            .with_matcher(Default)
754            .build()
755            .expect("Unable to build policy from json.");
756
757        let request = Request::new("actor_a", "write", "hello/a").unwrap();
758
759        assert_matches!(policy.evaluate(&request), Ok(Decision::Allowed));
760    }
761
762    /// Scenario:
763    /// - The same as test case above,
764    ///   but statement operations are different.
765    ///
766    /// This case is created as a result of a discovered bug.
767    #[test]
768    fn all_identity_variable_rules_must_be_evaluated_operations_do_not_match() {
769        let json = r###"{
770            "statements": [
771                {
772                    "effect": "deny",
773                    "identities": [
774                        "{{any}}"
775                    ],
776                    "operations": [
777                        "read"
778                    ],
779                    "resources": [
780                        "hello/b"
781                    ]
782                },
783                {
784                    "effect": "allow",
785                    "identities": [
786                        "{{identity}}"
787                    ],
788                    "operations": [
789                        "write"
790                    ],
791                    "resources": [
792                        "hello/a"
793                    ]
794                }
795            ]
796        }"###;
797
798        let policy = PolicyBuilder::from_json(json)
799            .with_default_decision(Decision::Denied)
800            .with_substituter(TestIdentitySubstituter)
801            .with_matcher(Default)
802            .build()
803            .expect("Unable to build policy from json.");
804
805        let request = Request::new("actor_a", "write", "hello/a").unwrap();
806
807        assert_matches!(policy.evaluate(&request), Ok(Decision::Allowed));
808    }
809
810    /// `TestSubstituter` replaces any value with the corresponding identity
811    /// from the request, thus making the variable rule to always match the request.
812    #[derive(Debug)]
813    struct TestIdentitySubstituter;
814
815    impl Substituter for TestIdentitySubstituter {
816        type Context = ();
817
818        fn visit_identity(&self, _value: &str, context: &Request<Self::Context>) -> Result<String> {
819            Ok(context.identity.clone())
820        }
821
822        fn visit_operation(
823            &self,
824            _value: &str,
825            context: &Request<Self::Context>,
826        ) -> Result<String> {
827            Ok(context.operation.clone())
828        }
829
830        fn visit_resource(&self, value: &str, _context: &Request<Self::Context>) -> Result<String> {
831            Ok(value.into())
832        }
833    }
834
835    /// `StartWithMatcher` matches resources that start with requested value. For
836    /// example, if a policy defines a resource "hello/world", then request for "hello/"
837    /// will match.
838    #[derive(Debug)]
839    struct StartWithMatcher;
840
841    impl ResourceMatcher for StartWithMatcher {
842        type Context = ();
843
844        fn do_match(&self, _: &Request<Self::Context>, input: &str, policy: &str) -> bool {
845            policy.starts_with(input)
846        }
847    }
848
849    #[cfg(feature = "proptest")]
850    mod proptests {
851        use crate::{Decision, Effect, PolicyBuilder, PolicyDefinition, Request, Statement};
852        use proptest::{collection::vec, prelude::*};
853
854        proptest! {
855            /// The goal of this test is to verify the following scenarios:
856            /// - PolicyBuilder does not crash.
857            /// - All combinations of identity/operation/resource in a statement in the definition
858            ///   should produce expected result.
859            ///   Since some statements can be overridden by the previous ones,
860            ///   we can only safely verify the very first statement.
861            #[test]
862            fn policy_engine_proptest(definition in arb_policy_definition()){
863                use itertools::iproduct;
864
865                // take very first statement, which should have top priority.
866                let statement = &definition.statements()[0];
867                let expected = match statement.effect() {
868                    Effect::Allow => Decision::Allowed,
869                    Effect::Deny => Decision::Denied,
870                };
871
872                // collect all combos of identity/operation/resource
873                // in the statement.
874                let requests = iproduct!(
875                    statement.identities(),
876                    statement.operations(),
877                    statement.resources()
878                )
879                .map(|item| Request::new(item.0, item.1, item.2).expect("unable to create a request"))
880                .collect::<Vec<_>>();
881
882                let policy = PolicyBuilder::from_definition(definition)
883                    .build()
884                    .expect("unable to build policy from definition");
885
886                // evaluate and assert.
887                for request in requests {
888                    assert_eq!(policy.evaluate(&request).unwrap(), expected);
889                }
890            }
891        }
892
893        prop_compose! {
894            pub fn arb_policy_definition()(
895                statements in vec(arb_statement(), 1..5)
896            ) -> PolicyDefinition {
897                PolicyDefinition {
898                    statements
899                }
900            }
901        }
902
903        prop_compose! {
904            pub fn arb_statement()(
905                description in arb_description(),
906                effect in arb_effect(),
907                identities in vec(arb_identity(), 1..5),
908                operations in vec(arb_operation(), 1..5),
909                resources in vec(arb_resource(), 1..5),
910            ) -> Statement {
911                Statement{
912                    order: 0,
913                    description,
914                    effect,
915                    identities,
916                    operations,
917                    resources,
918                }
919            }
920        }
921
922        pub fn arb_effect() -> impl Strategy<Value = Effect> {
923            prop_oneof![Just(Effect::Allow), Just(Effect::Deny)]
924        }
925
926        pub fn arb_description() -> impl Strategy<Value = String> {
927            "\\PC+"
928        }
929
930        pub fn arb_identity() -> impl Strategy<Value = String> {
931            "(\\PC+)|(\\{\\{\\PC+\\}\\})"
932        }
933
934        pub fn arb_operation() -> impl Strategy<Value = String> {
935            "\\PC+"
936        }
937
938        pub fn arb_resource() -> impl Strategy<Value = String> {
939            "\\PC+(/(\\PC+|\\{\\{\\PC+\\}\\}))*"
940        }
941    }
942}