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