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