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