cedar_policy_validator/
lib.rs

1/*
2 * Copyright 2022-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! Validator for Cedar policies
18#![forbid(unsafe_code)]
19
20use std::collections::HashSet;
21
22use cedar_policy_core::ast::{Policy, PolicySet, Template};
23
24mod err;
25mod str_checks;
26pub use err::*;
27mod expr_iterator;
28mod extension_schema;
29mod extensions;
30mod fuzzy_match;
31mod validation_result;
32use serde::Serialize;
33pub use validation_result::*;
34mod rbac;
35mod schema;
36pub use schema::*;
37mod schema_file_format;
38pub use schema_file_format::*;
39mod type_error;
40pub use type_error::*;
41pub mod typecheck;
42pub mod types;
43
44pub use str_checks::{confusable_string_checks, ValidationWarning, ValidationWarningKind};
45
46use self::typecheck::Typechecker;
47
48/// Used to select how a policy will be validated.
49#[derive(Default, Eq, PartialEq, Copy, Clone, Debug, Serialize)]
50pub enum ValidationMode {
51    #[default]
52    Strict,
53    Permissive,
54}
55
56impl ValidationMode {
57    /// Does this mode apply strict validation rules.
58    fn is_strict(self) -> bool {
59        match self {
60            ValidationMode::Strict => true,
61            ValidationMode::Permissive => false,
62        }
63    }
64}
65
66/// Structure containing the context needed for policy validation. This is
67/// currently only the `EntityType`s and `ActionType`s from a single schema.
68#[derive(Debug)]
69pub struct Validator {
70    schema: ValidatorSchema,
71}
72
73impl Validator {
74    /// Construct a new Validator from a schema file.
75    pub fn new(schema: ValidatorSchema) -> Validator {
76        Self { schema }
77    }
78
79    /// Validate all templates, links, and static policies in a policy set.
80    /// Return an iterator of policy notes associated with each policy id.
81    pub fn validate<'a>(
82        &'a self,
83        policies: &'a PolicySet,
84        mode: ValidationMode,
85    ) -> ValidationResult<'a> {
86        let template_and_static_policy_errs = policies
87            .all_templates()
88            .flat_map(|p| self.validate_policy(p, mode));
89        let link_errs = policies
90            .policies()
91            .filter_map(|p| self.validate_slots(p))
92            .flatten();
93        ValidationResult::new(template_and_static_policy_errs.chain(link_errs))
94    }
95
96    /// Run all validations against a single static policy or template (note
97    /// that Core `Template` includes static policies as well), gathering all
98    /// validation notes together in the returned iterator.
99    fn validate_policy<'a>(
100        &'a self,
101        p: &'a Template,
102        mode: ValidationMode,
103    ) -> impl Iterator<Item = ValidationError> + 'a {
104        self.validate_entity_types(p)
105            .chain(self.validate_action_ids(p))
106            .chain(self.validate_action_application(
107                p.principal_constraint(),
108                p.action_constraint(),
109                p.resource_constraint(),
110            ))
111            .map(move |note| ValidationError::with_policy_id(p.id(), None, note))
112            .chain(self.typecheck_policy(p, mode))
113    }
114
115    /// Run relevant validations against a single template-linked policy,
116    /// gathering all validation notes together in the returned iterator.
117    fn validate_slots<'a>(
118        &'a self,
119        p: &'a Policy,
120    ) -> Option<impl Iterator<Item = ValidationError> + 'a> {
121        // Ignore static policies since they are already handled by `validate_policy`
122        if p.is_static() {
123            return None;
124        }
125        // For template-linked policies `Policy::principal_constraint()` and
126        // `Policy::resource_constraint()` return a copy of the constraint with
127        // the slot filled by the appropriate value.
128        Some(
129            self.validate_entity_types_in_slots(p.env())
130                .chain(self.validate_action_application(
131                    &p.principal_constraint(),
132                    p.action_constraint(),
133                    &p.resource_constraint(),
134                ))
135                .map(move |note| ValidationError::with_policy_id(p.id(), None, note)),
136        )
137    }
138
139    /// Construct a Typechecker instance and use it to detect any type errors in
140    /// the argument static policy or template (note that Core `Template`
141    /// includes static policies as well) in the context of the schema for this
142    /// validator. Any detected type errors are wrapped and returned as
143    /// `ValidationErrorKind`s.
144    fn typecheck_policy<'a>(
145        &'a self,
146        t: &'a Template,
147        mode: ValidationMode,
148    ) -> impl Iterator<Item = ValidationError> + 'a {
149        let typecheck = Typechecker::new(&self.schema, mode);
150        let mut type_errors = HashSet::new();
151        typecheck.typecheck_policy(t, &mut type_errors);
152        type_errors.into_iter().map(|type_error| {
153            let (kind, location) = type_error.kind_and_location();
154            ValidationError::with_policy_id(t.id(), location, ValidationErrorKind::type_error(kind))
155        })
156    }
157}
158
159#[cfg(test)]
160mod test {
161    use std::collections::HashMap;
162
163    use super::*;
164    use cedar_policy_core::{ast, parser};
165
166    #[test]
167    fn top_level_validate() -> Result<()> {
168        let mut set = PolicySet::new();
169        let foo_type = "foo_type";
170        let bar_type = "bar_type";
171        let action_name = "action";
172        let schema_file = NamespaceDefinition::new(
173            [
174                (
175                    foo_type.into(),
176                    EntityType {
177                        member_of_types: vec![],
178                        shape: AttributesOrContext::default(),
179                    },
180                ),
181                (
182                    bar_type.into(),
183                    EntityType {
184                        member_of_types: vec![],
185                        shape: AttributesOrContext::default(),
186                    },
187                ),
188            ],
189            [(
190                action_name.into(),
191                ActionType {
192                    applies_to: Some(ApplySpec {
193                        resource_types: None,
194                        principal_types: None,
195                        context: AttributesOrContext::default(),
196                    }),
197                    member_of: None,
198                    attributes: None,
199                },
200            )],
201        );
202        let schema = schema_file.try_into().unwrap();
203        let validator = Validator::new(schema);
204
205        let policy_a_src = r#"permit(principal in foo_type::"a", action == Action::"actin", resource == bar_type::"b");"#;
206        let policy_a = parser::parse_policy(Some("pola".to_string()), policy_a_src)
207            .expect("Test Policy Should Parse");
208        set.add_static(policy_a.clone())
209            .expect("Policy already present in PolicySet");
210
211        let policy_b_src = r#"permit(principal in foo_tye::"a", action == Action::"action", resource == br_type::"b");"#;
212        let policy_b = parser::parse_policy(Some("polb".to_string()), policy_b_src)
213            .expect("Test Policy Should Parse");
214        set.add_static(policy_b.clone())
215            .expect("Policy already present in PolicySet");
216
217        let result = validator.validate(&set, ValidationMode::default());
218        let principal_err = ValidationError::with_policy_id(
219            policy_b.id(),
220            None,
221            ValidationErrorKind::unrecognized_entity_type(
222                "foo_tye".to_string(),
223                Some("foo_type".to_string()),
224            ),
225        );
226        let resource_err = ValidationError::with_policy_id(
227            policy_b.id(),
228            None,
229            ValidationErrorKind::unrecognized_entity_type(
230                "br_type".to_string(),
231                Some("bar_type".to_string()),
232            ),
233        );
234        let action_err = ValidationError::with_policy_id(
235            policy_a.id(),
236            None,
237            ValidationErrorKind::unrecognized_action_id(
238                "Action::\"actin\"".to_string(),
239                Some("Action::\"action\"".to_string()),
240            ),
241        );
242        assert!(!result.validation_passed());
243        assert!(result.validation_errors().any(|x| x == &principal_err));
244        assert!(result.validation_errors().any(|x| x == &resource_err));
245        assert!(result.validation_errors().any(|x| x == &action_err));
246
247        Ok(())
248    }
249
250    #[test]
251    fn top_level_validate_with_instantiations() -> Result<()> {
252        let mut set = PolicySet::new();
253        let schema: ValidatorSchema = serde_json::from_str::<SchemaFragment>(
254            r#"
255            {
256                "some_namespace": {
257                    "entityTypes": {
258                        "User": {
259                            "shape": {
260                                "type": "Record",
261                                "attributes": {
262                                    "department": {
263                                        "type": "String"
264                                    },
265                                    "jobLevel": {
266                                        "type": "Long"
267                                    }
268                                }
269                            },
270                            "memberOfTypes": [
271                                "UserGroup"
272                            ]
273                        },
274                        "UserGroup": {},
275                        "Photo" : {}
276                    },
277                    "actions": {
278                        "view": {
279                            "appliesTo": {
280                                "resourceTypes": [
281                                    "Photo"
282                                ],
283                                "principalTypes": [
284                                    "User"
285                                ]
286                            }
287                        }
288                    }
289                }
290            }
291        "#,
292        )
293        .expect("Schema parse error.")
294        .try_into()
295        .expect("Expected valid schema.");
296        let validator = Validator::new(schema);
297
298        let t = parser::parse_policy_template(
299            Some("template".to_string()),
300            r#"permit(principal == some_namespace::User::"Alice", action, resource in ?resource);"#,
301        )
302        .expect("Parse Error");
303        set.add_template(t)
304            .expect("Template already present in PolicySet");
305
306        // the template is valid by itself
307        let result = validator.validate(&set, ValidationMode::default());
308        assert_eq!(
309            result.into_validation_errors().collect::<Vec<_>>(),
310            Vec::new()
311        );
312
313        // a valid instantiation is valid
314        let mut values = HashMap::new();
315        values.insert(
316            ast::SlotId::resource(),
317            ast::EntityUID::from_components(
318                "some_namespace::Photo".parse().unwrap(),
319                ast::Eid::new("foo"),
320            ),
321        );
322        set.link(
323            ast::PolicyID::from_string("template"),
324            ast::PolicyID::from_string("link1"),
325            values,
326        )
327        .expect("Linking failed!");
328        let result = validator.validate(&set, ValidationMode::default());
329        assert!(result.validation_passed());
330
331        // an invalid instantiation results in an error
332        let mut values = HashMap::new();
333        values.insert(
334            ast::SlotId::resource(),
335            ast::EntityUID::from_components(
336                "some_namespace::Undefined".parse().unwrap(),
337                ast::Eid::new("foo"),
338            ),
339        );
340        set.link(
341            ast::PolicyID::from_string("template"),
342            ast::PolicyID::from_string("link2"),
343            values,
344        )
345        .expect("Linking failed!");
346        let result = validator.validate(&set, ValidationMode::default());
347        assert!(!result.validation_passed());
348        assert_eq!(result.validation_errors().count(), 2);
349        let id = ast::PolicyID::from_string("link2");
350        let undefined_err = ValidationError::with_policy_id(
351            &id,
352            None,
353            ValidationErrorKind::unrecognized_entity_type(
354                "some_namespace::Undefined".to_string(),
355                Some("some_namespace::User".to_string()),
356            ),
357        );
358        let invalid_action_err = ValidationError::with_policy_id(
359            &id,
360            None,
361            ValidationErrorKind::invalid_action_application(false, false),
362        );
363        assert!(result.validation_errors().any(|x| x == &undefined_err));
364        assert!(result.validation_errors().any(|x| x == &invalid_action_err));
365
366        // this is also an invalid instantiation (not a valid resource type for any action in the schema)
367        let mut values = HashMap::new();
368        values.insert(
369            ast::SlotId::resource(),
370            ast::EntityUID::from_components(
371                "some_namespace::User".parse().unwrap(),
372                ast::Eid::new("foo"),
373            ),
374        );
375        set.link(
376            ast::PolicyID::from_string("template"),
377            ast::PolicyID::from_string("link3"),
378            values,
379        )
380        .expect("Linking failed!");
381        let result = validator.validate(&set, ValidationMode::default());
382        assert!(!result.validation_passed());
383        // `result` contains the two prior error messages plus one new one
384        assert_eq!(result.validation_errors().count(), 3);
385        let id = ast::PolicyID::from_string("link3");
386        let invalid_action_err = ValidationError::with_policy_id(
387            &id,
388            None,
389            ValidationErrorKind::invalid_action_application(false, false),
390        );
391        assert!(result.validation_errors().any(|x| x == &invalid_action_err));
392
393        Ok(())
394    }
395}