allow_me/core/
builder.rs

1use std::error::Error as StdError;
2
3use lazy_static::lazy_static;
4use regex::Regex;
5use serde::Deserialize;
6
7use crate::{
8    core::{Identities, Operations, Resources},
9    matcher, Decision, DefaultSubstituter, DefaultValidator, Error, Policy, PolicyValidator,
10    ResourceMatcher, Result, Substituter,
11};
12
13/// A policy builder, responsible for parsing policy definition
14/// and constructing [`Policy`] struct.
15///
16/// It handles policy definition versioning and allows fine-grained
17/// configuration of [`Policy`] components.
18pub struct PolicyBuilder<V, M, S> {
19    validator: V,
20    matcher: M,
21    substituter: S,
22    source: Source,
23    default_decision: Decision,
24}
25
26impl PolicyBuilder<DefaultValidator, matcher::Default, DefaultSubstituter> {
27    /// Constructs a [`PolicyBuilder`] from provided json policy definition, with
28    /// default configuration.
29    ///
30    /// Call to this method does not parse or validate the json, all heavy work
31    /// is done in `build` method.
32    pub fn from_json(
33        json: impl Into<String>,
34    ) -> PolicyBuilder<DefaultValidator, matcher::Default, DefaultSubstituter> {
35        PolicyBuilder {
36            source: Source::Json(json.into()),
37            validator: DefaultValidator,
38            matcher: matcher::Default,
39            substituter: DefaultSubstituter,
40            default_decision: Decision::Denied,
41        }
42    }
43
44    /// Constructs a [`PolicyBuilder`] from provided policy definition struct, with
45    /// default configuration.
46    ///
47    /// Call to this method does not validate the definition, all heavy work
48    /// is done in `build` method.
49    pub fn from_definition(
50        definition: PolicyDefinition,
51    ) -> PolicyBuilder<DefaultValidator, matcher::Default, DefaultSubstituter> {
52        PolicyBuilder {
53            source: Source::Definition(definition),
54            validator: DefaultValidator,
55            matcher: matcher::Default,
56            substituter: DefaultSubstituter,
57            default_decision: Decision::Denied,
58        }
59    }
60}
61
62impl<V, M, S, E> PolicyBuilder<V, M, S>
63where
64    V: PolicyValidator<Error = E>,
65    M: ResourceMatcher,
66    S: Substituter,
67    E: StdError + Sync + Into<Box<dyn StdError>> + 'static,
68{
69    /// Specifies the [`PolicyValidator`] to validate the policy definition.
70    pub fn with_validator<V1>(self, validator: V1) -> PolicyBuilder<V1, M, S> {
71        PolicyBuilder {
72            source: self.source,
73            validator,
74            matcher: self.matcher,
75            substituter: self.substituter,
76            default_decision: self.default_decision,
77        }
78    }
79
80    /// Specifies the [`ResourceMatcher`] to use with Policy.
81    pub fn with_matcher<M1>(self, matcher: M1) -> PolicyBuilder<V, M1, S> {
82        PolicyBuilder {
83            source: self.source,
84            validator: self.validator,
85            matcher,
86            substituter: self.substituter,
87            default_decision: self.default_decision,
88        }
89    }
90
91    /// Specifies the [`Substituter`] to use with Policy.
92    pub fn with_substituter<S1>(self, substituter: S1) -> PolicyBuilder<V, M, S1> {
93        PolicyBuilder {
94            source: self.source,
95            validator: self.validator,
96            matcher: self.matcher,
97            substituter,
98            default_decision: self.default_decision,
99        }
100    }
101
102    /// Specifies the default decision that [`Policy`] will return if
103    /// no rules match the request.
104    pub fn with_default_decision(mut self, decision: Decision) -> Self {
105        self.default_decision = decision;
106        self
107    }
108
109    /// Builds a [`Policy`] consuming the builder.
110    ///
111    /// This method does all the heavy lifting of deserializing json, validating and
112    /// constructing the policy rules tree.
113    ///
114    /// # Errors
115    /// Returns  [`PolicyValidator::Error`] if any.
116    pub fn build(self) -> Result<Policy<M, S>> {
117        let PolicyBuilder {
118            validator,
119            matcher,
120            substituter,
121            source,
122            default_decision,
123        } = self;
124
125        let mut definition: PolicyDefinition = match source {
126            Source::Json(json) => PolicyDefinition::from_json(&json)?,
127            Source::Definition(definition) => definition,
128        };
129
130        for (order, mut statement) in definition.statements.iter_mut().enumerate() {
131            statement.order = order;
132        }
133
134        validator
135            .validate(&definition)
136            .map_err(|e| Error::Validation(e.into()))?;
137
138        let mut static_rules = Identities::new();
139        let mut variable_rules = Identities::new();
140
141        for statement in definition.statements {
142            process_statement(&statement, &mut static_rules, &mut variable_rules);
143        }
144
145        Ok(Policy {
146            default_decision,
147            resource_matcher: matcher,
148            substituter,
149            static_rules: static_rules.0,
150            variable_rules: variable_rules.0,
151        })
152    }
153}
154
155fn process_statement(
156    statement: &Statement,
157    static_rules: &mut Identities,
158    variable_rules: &mut Identities,
159) {
160    let (static_ids, variable_ids) = process_identities(statement);
161
162    static_rules.merge(static_ids);
163    variable_rules.merge(variable_ids);
164}
165
166fn process_identities(statement: &Statement) -> (Identities, Identities) {
167    let mut static_ids = Identities::new();
168    let mut variable_ids = Identities::new();
169    for identity in &statement.identities {
170        let (static_ops, variable_ops) = process_operations(&statement);
171
172        if is_variable_rule(identity) {
173            // if current identity has substitutions,
174            // then the whole operation subtree need
175            // to be cloned into substitutions tree.
176            let mut all = static_ops.clone();
177            all.merge(variable_ops);
178            variable_ids.insert(identity, all);
179        } else {
180            // else, divide operations and operation substitutions
181            // between identities and identity substitutions.
182            static_ids.insert(identity, static_ops);
183            variable_ids.insert(identity, variable_ops);
184        }
185    }
186
187    (static_ids, variable_ids)
188}
189
190fn process_operations(statement: &Statement) -> (Operations, Operations) {
191    let mut static_ops = Operations::new();
192    let mut variable_ops = Operations::new();
193    for operation in &statement.operations {
194        let (static_res, variable_res) = process_resources(&statement);
195
196        if is_variable_rule(operation) {
197            // if current operation has variables,
198            // then the whole resource subtree need
199            // to be cloned into variables tree.
200            let mut all = static_res.clone();
201            all.merge(variable_res);
202            variable_ops.insert(operation, all);
203        } else {
204            // else, divide static resources and variable resources
205            // between static operations and variable operation.
206            static_ops.insert(operation, static_res);
207            variable_ops.insert(operation, variable_res);
208        }
209    }
210
211    (static_ops, variable_ops)
212}
213
214fn process_resources(statement: &Statement) -> (Resources, Resources) {
215    let mut static_res = Resources::new();
216    let mut variable_res = Resources::new();
217    if statement.resources.is_empty() {
218        static_res.insert("", statement.into());
219    }
220
221    for resource in &statement.resources {
222        // split resources into two buckets - static or variable rules:
223        let map = if is_variable_rule(resource) {
224            &mut variable_res
225        } else {
226            &mut static_res
227        };
228
229        map.insert(resource, statement.into());
230    }
231
232    (static_res, variable_res)
233}
234
235fn is_variable_rule(value: &str) -> bool {
236    lazy_static! {
237        static ref VAR_PATTERN: Regex =
238            Regex::new(r#"\{\{[^\{\}]+\}\}"#).expect("failed to create a Regex from pattern");
239    }
240    VAR_PATTERN.is_match(value)
241}
242
243enum Source {
244    Json(String),
245    Definition(PolicyDefinition),
246}
247
248/// Represents a deserialized policy definition.
249#[derive(Debug, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct PolicyDefinition {
252    pub(super) statements: Vec<Statement>,
253}
254
255impl PolicyDefinition {
256    pub fn from_json(json: &str) -> Result<Self> {
257        let definition: PolicyDefinition =
258            serde_json::from_str(json).map_err(Error::Deserializing)?;
259
260        Ok(definition)
261    }
262
263    pub fn statements(&self) -> &Vec<Statement> {
264        &self.statements
265    }
266}
267
268/// Represents a statement in a policy definition.
269#[derive(Debug, Deserialize)]
270#[serde(rename_all = "camelCase")]
271pub struct Statement {
272    #[serde(default)]
273    pub(super) order: usize,
274    #[serde(default)]
275    pub(super) description: String,
276    pub(super) effect: Effect,
277    pub(super) identities: Vec<String>,
278    pub(super) operations: Vec<String>,
279    #[serde(default)]
280    pub(super) resources: Vec<String>,
281}
282
283impl Statement {
284    pub(crate) fn order(&self) -> usize {
285        self.order
286    }
287
288    pub fn description(&self) -> &str {
289        &self.description
290    }
291
292    pub fn effect(&self) -> Effect {
293        self.effect
294    }
295
296    pub fn identities(&self) -> &Vec<String> {
297        &self.identities
298    }
299
300    pub fn operations(&self) -> &Vec<String> {
301        &self.operations
302    }
303
304    pub fn resources(&self) -> &Vec<String> {
305        &self.resources
306    }
307}
308
309/// Represents an effect on a statement.
310#[derive(Debug, Deserialize, Copy, Clone, PartialOrd, PartialEq)]
311#[serde(rename_all = "camelCase")]
312pub enum Effect {
313    Allow,
314    Deny,
315}
316
317#[cfg(test)]
318mod tests {
319    use std::result::Result as StdResult;
320
321    use assert_matches::assert_matches;
322
323    use crate::{
324        core::{tests::build_policy, Effect, EffectOrd},
325        validator::ValidatorError,
326    };
327
328    use super::*;
329
330    #[test]
331    fn test_basic_definition() {
332        let json = r#"{
333            "statements": [
334                {
335                    "effect": "allow",
336                    "identities": [
337                        "actor_a"
338                    ],
339                    "operations": [
340                        "read"
341                    ],
342                    "resources": [
343                        "resource_group"
344                    ]
345                },
346                {
347                    "effect": "allow",
348                    "identities": [
349                        "actor_b"
350                    ],
351                    "operations": [
352                        "write"
353                    ],
354                    "resources": [
355                        "resource_1"
356                    ]
357                },
358                {
359                    "description": "Deny all other identities to read",
360                    "effect": "deny",
361                    "identities": [
362                        "{{var_actor}}"
363                    ],
364                    "operations": [
365                        "read"
366                    ],
367                    "resources": [
368                        "resource_group"
369                    ]
370                }
371            ]
372        }"#;
373
374        let policy = build_policy(json);
375
376        assert_eq!(1, policy.variable_rules.len());
377        assert_eq!(2, policy.static_rules.len());
378    }
379
380    #[test]
381    fn identity_merge_rules() {
382        let json = r#"{
383            "statements": [
384                {
385                    "effect": "allow",
386                    "identities": [
387                        "actor_a"
388                    ],
389                    "operations": [
390                        "write"
391                    ],
392                    "resources": [
393                        "events/telemetry"
394                    ]
395                },
396                {
397                    "effect": "allow",
398                    "identities": [
399                        "actor_a"
400                    ],
401                    "operations": [
402                        "read"
403                    ],
404                    "resources": [
405                        "resource_1"
406                    ]
407                },
408                {
409                    "effect": "allow",
410                    "identities": [
411                        "actor_a"
412                    ],
413                    "operations": [
414                        "read"
415                    ],
416                    "resources": [
417                        "{{variable}}/#"
418                    ]
419                },
420                {
421                    "effect": "allow",
422                    "identities": [
423                        "actor_a"
424                    ],
425                    "operations": [
426                        "write"
427                    ],
428                    "resources": [
429                        "{{variable}}/#"
430                    ]
431                }
432            ]
433        }"#;
434
435        let policy = build_policy(json);
436
437        // assert static rules have 1 identity and 2 operations
438        assert_eq!(1, policy.static_rules.len());
439        assert_eq!(2, policy.static_rules["actor_a"].0.len());
440
441        // assert variable rules have 1 identity and 2 operations
442        assert_eq!(1, policy.variable_rules.len());
443        assert_eq!(2, policy.variable_rules["actor_a"].0.len());
444    }
445
446    #[test]
447    fn operation_merge_rules() {
448        let json = r#"{
449            "statements": [
450                {
451                    "effect": "allow",
452                    "identities": [
453                        "actor_a"
454                    ],
455                    "operations": [
456                        "write"
457                    ],
458                    "resources": [
459                        "events/telemetry"
460                    ]
461                },
462                {
463                    "effect": "allow",
464                    "identities": [
465                        "actor_a"
466                    ],
467                    "operations": [
468                        "write"
469                    ],
470                    "resources": [
471                        "resource_1"
472                    ]
473                },
474                {
475                    "effect": "allow",
476                    "identities": [
477                        "actor_a"
478                    ],
479                    "operations": [
480                        "read"
481                    ],
482                    "resources": [
483                        "{{variable}}/#"
484                    ]
485                },
486                {
487                    "effect": "allow",
488                    "identities": [
489                        "actor_a"
490                    ],
491                    "operations": [
492                        "read"
493                    ],
494                    "resources": [
495                        "devices/{{variable}}/#"
496                    ]
497                }
498            ]
499        }"#;
500
501        let policy = build_policy(json);
502
503        // assert static rules have 1 identity, 1 operations and 2 resources
504        assert_eq!(1, policy.static_rules["actor_a"].0.len());
505        assert_eq!(2, policy.static_rules["actor_a"].0["write"].0.len());
506
507        // assert variable rules have 1 identity, 1 operations and 2 resources
508        assert_eq!(1, policy.variable_rules["actor_a"].0.len());
509        assert_eq!(2, policy.variable_rules["actor_a"].0["read"].0.len());
510    }
511
512    #[test]
513    fn resource_merge_rules_higher_priority_statement_wins() {
514        let json = r#"{
515            "statements": [
516                {
517                    "effect": "allow",
518                    "identities": [
519                        "actor_a"
520                    ],
521                    "operations": [
522                        "write"
523                    ],
524                    "resources": [
525                        "events/telemetry"
526                    ]
527                },
528                {
529                    "effect": "deny",
530                    "identities": [
531                        "actor_a"
532                    ],
533                    "operations": [
534                        "write"
535                    ],
536                    "resources": [
537                        "events/telemetry"
538                    ]
539                },
540                {
541                    "effect": "allow",
542                    "identities": [
543                        "actor_a"
544                    ],
545                    "operations": [
546                        "read"
547                    ],
548                    "resources": [
549                        "{{variable}}/#"
550                    ]
551                },
552                {
553                    "effect": "deny",
554                    "identities": [
555                        "actor_a"
556                    ],
557                    "operations": [
558                        "read"
559                    ],
560                    "resources": [
561                        "{{variable}}/#"
562                    ]
563                }
564            ]
565        }"#;
566
567        let policy = build_policy(json);
568
569        // assert higher priority rule wins.
570        assert_eq!(
571            EffectOrd {
572                order: 0,
573                effect: Effect::Allow
574            },
575            policy.static_rules["actor_a"].0["write"].0["events/telemetry"]
576        );
577
578        // assert higher priority rule wins for variable rules.
579        assert_eq!(
580            EffectOrd {
581                order: 2,
582                effect: Effect::Allow
583            },
584            policy.variable_rules["actor_a"].0["read"].0["{{variable}}/#"]
585        );
586    }
587
588    #[test]
589    #[allow(clippy::too_many_lines)]
590    fn grouping_rules_with_variables_test() {
591        let json = r#"{
592            "statements": [
593                {
594                    "effect": "allow",
595                    "identities": [
596                        "actor_a",
597                        "actor_b",
598                        "{{var_actor}}"
599                    ],
600                    "operations": [
601                        "write",
602                        "read"
603                    ],
604                    "resources": [
605                        "events/telemetry",
606                        "devices/{{variable}}/#"
607                    ]
608                }
609            ]
610        }"#;
611
612        let policy = build_policy(json);
613
614        // assert static rules.
615        assert_eq!(2, policy.static_rules.len());
616        assert_eq!(
617            policy.static_rules["actor_a"].0["write"].0["events/telemetry"],
618            EffectOrd {
619                effect: Effect::Allow,
620                order: 0
621            }
622        );
623        assert_eq!(
624            policy.static_rules["actor_a"].0["read"].0["events/telemetry"],
625            EffectOrd {
626                effect: Effect::Allow,
627                order: 0
628            }
629        );
630        assert_eq!(
631            policy.static_rules["actor_b"].0["write"].0["events/telemetry"],
632            EffectOrd {
633                effect: Effect::Allow,
634                order: 0
635            }
636        );
637        assert_eq!(
638            policy.static_rules["actor_b"].0["read"].0["events/telemetry"],
639            EffectOrd {
640                effect: Effect::Allow,
641                order: 0
642            }
643        );
644
645        // assert variable rules.
646        assert_eq!(3, policy.variable_rules.len());
647        assert_eq!(
648            policy.variable_rules["actor_a"].0["write"].0["devices/{{variable}}/#"],
649            EffectOrd {
650                effect: Effect::Allow,
651                order: 0
652            }
653        );
654        assert_eq!(
655            policy.variable_rules["actor_a"].0["read"].0["devices/{{variable}}/#"],
656            EffectOrd {
657                effect: Effect::Allow,
658                order: 0
659            }
660        );
661        assert_eq!(
662            policy.variable_rules["actor_b"].0["write"].0["devices/{{variable}}/#"],
663            EffectOrd {
664                effect: Effect::Allow,
665                order: 0
666            }
667        );
668        assert_eq!(
669            policy.variable_rules["actor_b"].0["read"].0["devices/{{variable}}/#"],
670            EffectOrd {
671                effect: Effect::Allow,
672                order: 0
673            }
674        );
675        assert_eq!(
676            policy.variable_rules["{{var_actor}}"].0["write"].0["devices/{{variable}}/#"],
677            EffectOrd {
678                effect: Effect::Allow,
679                order: 0
680            }
681        );
682        assert_eq!(
683            policy.variable_rules["{{var_actor}}"].0["read"].0["devices/{{variable}}/#"],
684            EffectOrd {
685                effect: Effect::Allow,
686                order: 0
687            }
688        );
689    }
690
691    #[test]
692    fn policy_validation_test() {
693        let json = r#"{
694            "statements": [
695                {
696                    "effect": "allow",
697                    "identities": [
698                        "actor_a"
699                    ],
700                    "operations": [
701                        "write"
702                    ],
703                    "resources": [
704                        "events/telemetry"
705                    ]
706                },
707                {
708                    "effect": "allow",
709                    "identities": [
710                        "monitor"
711                    ],
712                    "operations": [
713                        "read"
714                    ],
715                    "resources": [
716                        "events/telemetry"
717                    ]
718                }
719            ]
720        }"#;
721
722        let result = PolicyBuilder::from_json(json)
723            .with_validator(FailAllValidator)
724            .with_default_decision(Decision::Denied)
725            .build();
726
727        assert_matches!(result, Err(Error::Validation(_)));
728    }
729
730    #[derive(Debug)]
731    struct FailAllValidator;
732
733    impl PolicyValidator for FailAllValidator {
734        type Error = ValidatorError;
735
736        fn validate(&self, _definition: &PolicyDefinition) -> StdResult<(), Self::Error> {
737            Err(ValidatorError::ValidationSummary(vec!["error".to_string()]))
738        }
739    }
740}