1#![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#[derive(Default, Eq, PartialEq, Copy, Clone, Debug, Serialize)]
59pub enum ValidationMode {
60 #[default]
62 Strict,
63 Permissive,
65 #[cfg(feature = "partial-validate")]
68 Partial,
69}
70
71impl ValidationMode {
72 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 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#[derive(Debug, Clone)]
96pub struct Validator {
97 schema: ValidatorSchema,
98}
99
100impl Validator {
101 pub fn new(schema: ValidatorSchema) -> Validator {
103 Self { schema }
104 }
105
106 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 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 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 None
171 } else {
172 Some(
173 self.validate_entity_types(p)
174 .chain(self.validate_action_ids(p))
175 .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 fn validate_slots<'a>(
191 &'a self,
192 p: &'a Policy,
193 mode: ValidationMode,
194 ) -> Option<impl Iterator<Item = ValidationError> + 'a> {
195 if p.is_static() {
197 return None;
198 }
199 if mode.is_partial() {
203 return None;
204 }
205 Some(
209 self.validate_entity_types_in_slots(p.id(), p.env())
210 .chain(self.validate_linked_action_application(p)),
211 )
212 }
213
214 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 let result = validator.validate(&set, ValidationMode::default());
407 assert_eq!(
408 result.validation_errors().collect::<Vec<_>>(),
409 Vec::<&ValidationError>::new()
410 );
411
412 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 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 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 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}