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