1#![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;
41mod type_error;
42pub use type_error::*;
43pub mod human_schema;
44pub mod typecheck;
45use typecheck::Typechecker;
46pub mod types;
47
48#[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 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 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#[derive(Debug)]
83pub struct Validator {
84 schema: ValidatorSchema,
85}
86
87impl Validator {
88 pub fn new(schema: ValidatorSchema) -> Validator {
90 Self { schema }
91 }
92
93 pub fn validate<'a>(
96 &'a self,
97 policies: &'a PolicySet,
98 mode: ValidationMode,
99 ) -> ValidationResult<'a> {
100 let validate_policy_results: (Vec<_>, Vec<_>) = policies
101 .all_templates()
102 .map(|p| self.validate_policy(p, mode))
103 .unzip();
104 let template_and_static_policy_errs = validate_policy_results.0.into_iter().flatten();
105 let template_and_static_policy_warnings = validate_policy_results.1.into_iter().flatten();
106 let link_errs = policies
107 .policies()
108 .filter_map(|p| self.validate_slots(p, mode))
109 .flatten();
110 ValidationResult::new(
111 template_and_static_policy_errs.chain(link_errs),
112 template_and_static_policy_warnings
113 .chain(confusable_string_checks(policies.all_templates())),
114 )
115 }
116
117 fn validate_policy<'a>(
121 &'a self,
122 p: &'a Template,
123 mode: ValidationMode,
124 ) -> (
125 impl Iterator<Item = ValidationError<'a>>,
126 impl Iterator<Item = ValidationWarning<'a>>,
127 ) {
128 let validation_errors = if mode.is_partial() {
129 None
134 } else {
135 Some(
136 self.validate_entity_types(p)
137 .chain(self.validate_action_ids(p))
138 .chain(self.validate_template_action_application(p)),
143 )
144 }
145 .into_iter()
146 .flatten();
147 let (type_errors, warnings) = self.typecheck_policy(p, mode);
148 (validation_errors.chain(type_errors), warnings)
149 }
150
151 fn validate_slots<'a>(
154 &'a self,
155 p: &'a Policy,
156 mode: ValidationMode,
157 ) -> Option<impl Iterator<Item = ValidationError> + 'a> {
158 if p.is_static() {
160 return None;
161 }
162 if mode.is_partial() {
166 return None;
167 }
168 Some(
172 self.validate_entity_types_in_slots(p.env())
173 .map(move |note| ValidationError::with_policy_id(p.id(), None, note))
174 .chain(self.validate_linked_action_application(p)),
175 )
176 }
177
178 fn typecheck_policy<'a>(
184 &'a self,
185 t: &'a Template,
186 mode: ValidationMode,
187 ) -> (
188 impl Iterator<Item = ValidationError<'a>>,
189 impl Iterator<Item = ValidationWarning<'a>>,
190 ) {
191 let typecheck = Typechecker::new(&self.schema, mode);
192 let mut type_errors = HashSet::new();
193 let mut warnings = HashSet::new();
194 typecheck.typecheck_policy(t, &mut type_errors, &mut warnings);
195 (
196 type_errors.into_iter().map(|type_error| {
197 let (kind, location) = type_error.kind_and_location();
198 ValidationError::with_policy_id(
199 t.id(),
200 location,
201 ValidationErrorKind::type_error(kind),
202 )
203 }),
204 warnings.into_iter(),
205 )
206 }
207}
208
209#[cfg(test)]
210mod test {
211 use std::collections::HashMap;
212
213 use crate::types::Type;
214
215 use super::*;
216 use cedar_policy_core::{
217 ast::{self, Expr},
218 parser,
219 };
220
221 #[test]
222 fn top_level_validate() -> Result<()> {
223 let mut set = PolicySet::new();
224 let foo_type = "foo_type";
225 let bar_type = "bar_type";
226 let action_name = "action";
227 let schema_file = NamespaceDefinition::new(
228 [
229 (
230 foo_type.parse().unwrap(),
231 EntityType {
232 member_of_types: vec![],
233 shape: AttributesOrContext::default(),
234 },
235 ),
236 (
237 bar_type.parse().unwrap(),
238 EntityType {
239 member_of_types: vec![],
240 shape: AttributesOrContext::default(),
241 },
242 ),
243 ],
244 [(
245 action_name.into(),
246 ActionType {
247 applies_to: Some(ApplySpec {
248 resource_types: None,
249 principal_types: None,
250 context: AttributesOrContext::default(),
251 }),
252 member_of: None,
253 attributes: None,
254 },
255 )],
256 );
257 let schema = schema_file.try_into().unwrap();
258 let validator = Validator::new(schema);
259
260 let policy_a_src = r#"permit(principal in foo_type::"a", action == Action::"actin", resource == bar_type::"b");"#;
261 let policy_a = parser::parse_policy(Some("pola".to_string()), policy_a_src)
262 .expect("Test Policy Should Parse");
263 set.add_static(policy_a.clone())
264 .expect("Policy already present in PolicySet");
265
266 let policy_b_src = r#"permit(principal in foo_tye::"a", action == Action::"action", resource == br_type::"b");"#;
267 let policy_b = parser::parse_policy(Some("polb".to_string()), policy_b_src)
268 .expect("Test Policy Should Parse");
269 set.add_static(policy_b.clone())
270 .expect("Policy already present in PolicySet");
271
272 let result = validator.validate(&set, ValidationMode::default());
273 let principal_err = ValidationError::with_policy_id(
274 policy_b.id(),
275 None,
276 ValidationErrorKind::unrecognized_entity_type(
277 "foo_tye".to_string(),
278 Some("foo_type".to_string()),
279 ),
280 );
281 let resource_err = ValidationError::with_policy_id(
282 policy_b.id(),
283 None,
284 ValidationErrorKind::unrecognized_entity_type(
285 "br_type".to_string(),
286 Some("bar_type".to_string()),
287 ),
288 );
289 let action_err = ValidationError::with_policy_id(
290 policy_a.id(),
291 None,
292 ValidationErrorKind::unrecognized_action_id(
293 "Action::\"actin\"".to_string(),
294 Some("Action::\"action\"".to_string()),
295 ),
296 );
297
298 assert!(!result.validation_passed());
299 assert!(result
300 .validation_errors()
301 .any(|x| x.error_kind() == principal_err.error_kind()));
302 assert!(result
303 .validation_errors()
304 .any(|x| x.error_kind() == resource_err.error_kind()));
305 assert!(result
306 .validation_errors()
307 .any(|x| x.error_kind() == action_err.error_kind()));
308
309 Ok(())
310 }
311
312 #[test]
313 fn top_level_validate_with_instantiations() -> Result<()> {
314 let mut set = PolicySet::new();
315 let schema: ValidatorSchema = serde_json::from_str::<SchemaFragment>(
316 r#"
317 {
318 "some_namespace": {
319 "entityTypes": {
320 "User": {
321 "shape": {
322 "type": "Record",
323 "attributes": {
324 "department": {
325 "type": "String"
326 },
327 "jobLevel": {
328 "type": "Long"
329 }
330 }
331 },
332 "memberOfTypes": [
333 "UserGroup"
334 ]
335 },
336 "UserGroup": {},
337 "Photo" : {}
338 },
339 "actions": {
340 "view": {
341 "appliesTo": {
342 "resourceTypes": [
343 "Photo"
344 ],
345 "principalTypes": [
346 "User"
347 ]
348 }
349 }
350 }
351 }
352 }
353 "#,
354 )
355 .expect("Schema parse error.")
356 .try_into()
357 .expect("Expected valid schema.");
358 let validator = Validator::new(schema);
359
360 let t = parser::parse_policy_template(
361 Some("template".to_string()),
362 r#"permit(principal == some_namespace::User::"Alice", action, resource in ?resource);"#,
363 )
364 .expect("Parse Error");
365 let loc = t.loc().clone();
366 set.add_template(t)
367 .expect("Template already present in PolicySet");
368
369 let result = validator.validate(&set, ValidationMode::default());
371 assert_eq!(
372 result.validation_errors().collect::<Vec<_>>(),
373 Vec::<&ValidationError>::new()
374 );
375
376 let mut values = HashMap::new();
378 values.insert(
379 ast::SlotId::resource(),
380 ast::EntityUID::from_components(
381 "some_namespace::Photo".parse().unwrap(),
382 ast::Eid::new("foo"),
383 None,
384 ),
385 );
386 set.link(
387 ast::PolicyID::from_string("template"),
388 ast::PolicyID::from_string("link1"),
389 values,
390 )
391 .expect("Linking failed!");
392 let result = validator.validate(&set, ValidationMode::default());
393 assert!(result.validation_passed());
394
395 let mut values = HashMap::new();
397 values.insert(
398 ast::SlotId::resource(),
399 ast::EntityUID::from_components(
400 "some_namespace::Undefined".parse().unwrap(),
401 ast::Eid::new("foo"),
402 None,
403 ),
404 );
405 set.link(
406 ast::PolicyID::from_string("template"),
407 ast::PolicyID::from_string("link2"),
408 values,
409 )
410 .expect("Linking failed!");
411 let result = validator.validate(&set, ValidationMode::default());
412 assert!(!result.validation_passed());
413 assert_eq!(result.validation_errors().count(), 2);
414 let id = ast::PolicyID::from_string("link2");
415 let undefined_err = ValidationError::with_policy_id(
416 &id,
417 None,
418 ValidationErrorKind::unrecognized_entity_type(
419 "some_namespace::Undefined".to_string(),
420 Some("some_namespace::User".to_string()),
421 ),
422 );
423 let invalid_action_err = ValidationError::with_policy_id(
424 &id,
425 loc.clone(),
426 ValidationErrorKind::invalid_action_application(false, false),
427 );
428 assert!(result.validation_errors().any(|x| x == &undefined_err));
429 assert!(result.validation_errors().any(|x| x == &invalid_action_err));
430
431 let mut values = HashMap::new();
433 values.insert(
434 ast::SlotId::resource(),
435 ast::EntityUID::from_components(
436 "some_namespace::User".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("link3"),
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(), 3);
451 let id = ast::PolicyID::from_string("link3");
452 let invalid_action_err = ValidationError::with_policy_id(
453 &id,
454 loc.clone(),
455 ValidationErrorKind::invalid_action_application(false, false),
456 );
457 assert!(result
458 .validation_errors()
459 .any(|x| x.error_kind() == invalid_action_err.error_kind()));
460
461 Ok(())
462 }
463
464 #[test]
465 fn validate_finds_warning_and_error() {
466 let schema: ValidatorSchema = serde_json::from_str::<SchemaFragment>(
467 r#"
468 {
469 "": {
470 "entityTypes": {
471 "User": { }
472 },
473 "actions": {
474 "view": {
475 "appliesTo": {
476 "resourceTypes": [ "User" ],
477 "principalTypes": [ "User" ]
478 }
479 }
480 }
481 }
482 }
483 "#,
484 )
485 .expect("Schema parse error.")
486 .try_into()
487 .expect("Expected valid schema.");
488 let validator = Validator::new(schema);
489
490 let mut set = PolicySet::new();
491 let p = parser::parse_policy(
492 None,
493 r#"permit(principal == User::"һenry", action, resource) when {1 > true};"#,
494 )
495 .unwrap();
496 set.add_static(p).unwrap();
497
498 let result = validator.validate(&set, ValidationMode::default());
499 assert_eq!(
500 result
501 .validation_errors()
502 .map(|err| err.error_kind())
503 .collect::<Vec<_>>(),
504 vec![&ValidationErrorKind::type_error(
505 TypeError::expected_type(
506 Expr::val(1),
507 Type::primitive_long(),
508 Type::singleton_boolean(true),
509 None,
510 )
511 .kind
512 )]
513 );
514 assert_eq!(
515 result
516 .validation_warnings()
517 .map(|warn| warn.kind())
518 .collect::<Vec<_>>(),
519 vec![&ValidationWarningKind::MixedScriptIdentifier(
520 "һenry".into()
521 )]
522 );
523 }
524}