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