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