1#![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)] #![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#[derive(Default, Eq, PartialEq, Copy, Clone, Debug, Serialize)]
64pub enum ValidationMode {
65 #[default]
67 Strict,
68 Permissive,
70 #[cfg(feature = "partial-validate")]
73 Partial,
74}
75
76impl ValidationMode {
77 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 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#[derive(Debug)]
101pub struct Validator {
102 schema: ValidatorSchema,
103}
104
105impl Validator {
106 pub fn new(schema: ValidatorSchema) -> Validator {
108 Self { schema }
109 }
110
111 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 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 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 None
176 } else {
177 Some(
178 self.validate_entity_types(p)
179 .chain(self.validate_action_ids(p))
180 .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 fn validate_slots<'a>(
196 &'a self,
197 p: &'a Policy,
198 mode: ValidationMode,
199 ) -> Option<impl Iterator<Item = ValidationError> + 'a> {
200 if p.is_static() {
202 return None;
203 }
204 if mode.is_partial() {
208 return None;
209 }
210 Some(
214 self.validate_entity_types_in_slots(p.id(), p.env())
215 .chain(self.validate_linked_action_application(p)),
216 )
217 }
218
219 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 let result = validator.validate(&set, ValidationMode::default());
405 assert_eq!(
406 result.validation_errors().collect::<Vec<_>>(),
407 Vec::<&ValidationError>::new()
408 );
409
410 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 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 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 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}