cedar_policy_validator/
lib.rs

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