cedar_policy_validator/
lib.rs

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