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