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::{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
56/// Structure containing the context needed for policy validation. This is
57/// currently only the `EntityType`s and `ActionType`s from a single schema.
58#[derive(Debug)]
59pub struct Validator {
60    schema: ValidatorSchema,
61}
62
63impl Validator {
64    /// Construct a new Validator from a schema file.
65    pub fn new(schema: ValidatorSchema) -> Validator {
66        Self { schema }
67    }
68
69    /// Validate all templates in a policy set (which includes static policies) and
70    /// return an iterator of policy notes associated with each policy id.
71    pub fn validate<'a>(
72        &'a self,
73        policies: &'a PolicySet,
74        mode: ValidationMode,
75    ) -> ValidationResult<'a> {
76        let template_errs = policies
77            .all_templates()
78            .flat_map(|p| self.validate_policy(p, mode));
79        let instantiation_errs = policies.policies().flat_map(|p| {
80            self.validate_slots(p.env())
81                .map(move |note| ValidationError::with_policy_id(p.id(), None, note))
82        });
83        ValidationResult::new(template_errs.chain(instantiation_errs))
84    }
85
86    /// Run all validations against a single policy, gathering all validation
87    /// notes from together in the returned iterator.
88    fn validate_policy<'a>(
89        &'a self,
90        p: &'a Template,
91        mode: ValidationMode,
92    ) -> impl Iterator<Item = ValidationError> + 'a {
93        self.validate_entity_types(p)
94            .chain(self.validate_action_ids(p))
95            .chain(self.validate_action_application(p))
96            .map(move |note| ValidationError::with_policy_id(p.id(), None, note))
97            .chain(self.typecheck_policy(p, mode))
98    }
99
100    /// Construct a Typechecker instance and use it to detect any type errors in
101    /// the argument policy in the context of the schema for this validator. Any
102    /// detected type errors are wrapped and returned as `ValidationErrorKind`s.
103    fn typecheck_policy<'a>(
104        &'a self,
105        t: &'a Template,
106        mode: ValidationMode,
107    ) -> impl Iterator<Item = ValidationError> + 'a {
108        let typecheck = Typechecker::new(&self.schema);
109        let mut type_errors = HashSet::new();
110        typecheck.typecheck_policy(t, mode, &mut type_errors);
111        type_errors.into_iter().map(|type_error| {
112            let (kind, location) = type_error.kind_and_location();
113            ValidationError::with_policy_id(t.id(), location, ValidationErrorKind::type_error(kind))
114        })
115    }
116}
117
118#[cfg(test)]
119mod test {
120    use std::collections::HashMap;
121
122    use super::*;
123    use cedar_policy_core::{ast, parser};
124
125    #[test]
126    fn top_level_validate() -> Result<()> {
127        let mut set = PolicySet::new();
128        let foo_type = "foo_type";
129        let bar_type = "bar_type";
130        let action_name = "action";
131        let schema_file = NamespaceDefinition::new(
132            [
133                (
134                    foo_type.into(),
135                    EntityType {
136                        member_of_types: vec![],
137                        shape: AttributesOrContext::default(),
138                    },
139                ),
140                (
141                    bar_type.into(),
142                    EntityType {
143                        member_of_types: vec![],
144                        shape: AttributesOrContext::default(),
145                    },
146                ),
147            ],
148            [(
149                action_name.into(),
150                ActionType {
151                    applies_to: Some(ApplySpec {
152                        resource_types: None,
153                        principal_types: None,
154                        context: AttributesOrContext::default(),
155                    }),
156                    member_of: None,
157                    attributes: None,
158                },
159            )],
160        );
161        let schema = schema_file.try_into().unwrap();
162        let validator = Validator::new(schema);
163
164        let policy_a_src = r#"permit(principal in foo_type::"a", action == Action::"actin", resource == bar_type::"b");"#;
165        let policy_a = parser::parse_policy(Some("pola".to_string()), policy_a_src)
166            .expect("Test Policy Should Parse");
167        set.add_static(policy_a.clone())
168            .expect("Policy already present in PolicySet");
169
170        let policy_b_src = r#"permit(principal in foo_tye::"a", action == Action::"action", resource == br_type::"b");"#;
171        let policy_b = parser::parse_policy(Some("polb".to_string()), policy_b_src)
172            .expect("Test Policy Should Parse");
173        set.add_static(policy_b.clone())
174            .expect("Policy already present in PolicySet");
175
176        let result = validator.validate(&set, ValidationMode::default());
177        let principal_err = ValidationError::with_policy_id(
178            policy_b.id(),
179            None,
180            ValidationErrorKind::unrecognized_entity_type(
181                "foo_tye".to_string(),
182                Some("foo_type".to_string()),
183            ),
184        );
185        let resource_err = ValidationError::with_policy_id(
186            policy_b.id(),
187            None,
188            ValidationErrorKind::unrecognized_entity_type(
189                "br_type".to_string(),
190                Some("bar_type".to_string()),
191            ),
192        );
193        let action_err = ValidationError::with_policy_id(
194            policy_a.id(),
195            None,
196            ValidationErrorKind::unrecognized_action_id(
197                "Action::\"actin\"".to_string(),
198                Some("Action::\"action\"".to_string()),
199            ),
200        );
201        assert!(!result.validation_passed());
202        assert!(result.validation_errors().any(|x| x == &principal_err));
203        assert!(result.validation_errors().any(|x| x == &resource_err));
204        assert!(result.validation_errors().any(|x| x == &action_err));
205
206        Ok(())
207    }
208
209    #[test]
210    fn top_level_validate_with_instantiations() -> Result<()> {
211        let mut set = PolicySet::new();
212        let schema: ValidatorSchema = serde_json::from_str::<SchemaFragment>(
213            r#"
214            {
215                "some_namespace": {
216                    "entityTypes": {
217                        "User": {
218                            "shape": {
219                                "type": "Record",
220                                "attributes": {
221                                    "department": {
222                                        "type": "String"
223                                    },
224                                    "jobLevel": {
225                                        "type": "Long"
226                                    }
227                                }
228                            },
229                            "memberOfTypes": [
230                                "UserGroup"
231                            ]
232                        },
233                        "UserGroup": {},
234                        "Photo" : {}
235                    },
236                    "actions": {
237                        "view": {
238                            "appliesTo": {
239                                "resourceTypes": [
240                                    "Photo"
241                                ],
242                                "principalTypes": [
243                                    "User"
244                                ]
245                            }
246                        }
247                    }
248                }
249            }
250        "#,
251        )
252        .expect("Schema parse error.")
253        .try_into()
254        .expect("Expected valid schema.");
255        let validator = Validator::new(schema);
256
257        let t = parser::parse_policy_template(
258            Some("template".to_string()),
259            r#"permit(principal == some_namespace::User::"Alice", action, resource in ?resource);"#,
260        )
261        .expect("Parse Error");
262        set.add_template(t)
263            .expect("Template already present in PolicySet");
264
265        // the template is valid by itself
266        let result = validator.validate(&set, ValidationMode::default());
267        assert_eq!(
268            result.into_validation_errors().collect::<Vec<_>>(),
269            Vec::new()
270        );
271
272        // a valid instantiation is valid
273        let mut values = HashMap::new();
274        values.insert(
275            ast::SlotId::resource(),
276            ast::EntityUID::from_components(
277                "some_namespace::Photo".parse().unwrap(),
278                ast::Eid::new("foo"),
279            ),
280        );
281        set.link(
282            ast::PolicyID::from_string("template"),
283            ast::PolicyID::from_string("link1"),
284            values,
285        )
286        .expect("Linking failed!");
287        let result = validator.validate(&set, ValidationMode::default());
288        assert!(result.validation_passed());
289
290        // an invalid instantiation results in an error
291        let mut values = HashMap::new();
292        values.insert(
293            ast::SlotId::resource(),
294            ast::EntityUID::from_components(
295                "some_namespace::Undefined".parse().unwrap(),
296                ast::Eid::new("foo"),
297            ),
298        );
299        set.link(
300            ast::PolicyID::from_string("template"),
301            ast::PolicyID::from_string("link2"),
302            values,
303        )
304        .expect("Linking failed!");
305        let result = validator.validate(&set, ValidationMode::default());
306
307        let pid = ast::PolicyID::from_string("link2");
308        let resource_err = ValidationError::with_policy_id(
309            &pid,
310            None,
311            ValidationErrorKind::unrecognized_entity_type(
312                "some_namespace::Undefined".to_string(),
313                Some("some_namespace::User".to_string()),
314            ),
315        );
316        assert!(!result.validation_passed());
317        println!("{:?}", result.validation_errors().collect::<Vec<_>>());
318        assert!(result.validation_errors().any(|x| x == &resource_err));
319
320        Ok(())
321    }
322}