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#[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 pub fn evaluate(&self, request: &Request<RC>) -> Result<Decision> {
38 match self.eval_static_rules(request) {
39 Ok(None) => match self.eval_variable_rules(request) {
41 Ok(None) => Ok(self.default_decision),
43 Ok(Some(effect)) => Ok(effect.into()),
45 Err(e) => Err(e),
46 },
47 Ok(Some(static_effect)) => {
49 match self.eval_variable_rules(request) {
50 Ok(None) => Ok(static_effect.into()),
52 Ok(Some(variable_effect)) => {
54 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 match self.static_rules.get(&request.identity) {
73 Some(operations) => match operations.0.get(&request.operation) {
75 Some(resources) => {
77 let mut result: Option<EffectOrd> = None;
80 for (resource, effect) in &resources.0 {
81 if effect.order < result.map_or(usize::MAX, |e| e.order)
83 && self.resource_matcher.do_match( 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 let identity = self.substituter.visit_identity(identity, request)?;
105 if identity == request.identity {
107 return match operations.0.get(&request.operation) {
109 Some(resources) => {
111 let mut result: Option<EffectOrd> = None;
114 for (resource, effect) in &resources.0 {
115 let resource = self.substituter.visit_resource(resource, request)?;
116 if effect.order < result.map_or(usize::MAX, |e| e.order)
118 && self.resource_matcher.do_match(
120 request,
121 &request.resource,
122 &resource,
123 )
124 {
125 result = Some(*effect);
126 }
127 }
128 if result == None {
131 continue;
132 }
133 Ok(result)
134 }
135 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#[derive(Debug)]
238pub struct Request<RC> {
239 identity: String,
240 operation: String,
241 resource: String,
242
243 context: Option<RC>,
245}
246
247impl<RC> Request<RC> {
248 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 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#[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 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 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 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 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 let request = Request::new("actor_a", "write", "resource_1").unwrap();
527
528 assert_matches!(policy.evaluate(&request), Ok(Decision::Allowed));
529
530 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 assert_matches!(policy.evaluate(&request), Ok(Decision::Denied));
611 }
612
613 #[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 #[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 #[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 #[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 #[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 #[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 #[test]
862 fn policy_engine_proptest(definition in arb_policy_definition()){
863 use itertools::iproduct;
864
865 let statement = &definition.statements()[0];
867 let expected = match statement.effect() {
868 Effect::Allow => Decision::Allowed,
869 Effect::Deny => Decision::Denied,
870 };
871
872 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 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}