1#![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#[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 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 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#[derive(Debug)]
87pub struct Validator {
88 schema: ValidatorSchema,
89}
90
91impl Validator {
92 pub fn new(schema: ValidatorSchema) -> Validator {
94 Self { schema }
95 }
96
97 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 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 None
138 } else {
139 Some(
140 self.validate_entity_types(p)
141 .chain(self.validate_action_ids(p))
142 .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 fn validate_slots<'a>(
158 &'a self,
159 p: &'a Policy,
160 mode: ValidationMode,
161 ) -> Option<impl Iterator<Item = ValidationError<'a>> + 'a> {
162 if p.is_static() {
164 return None;
165 }
166 if mode.is_partial() {
170 return None;
171 }
172 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 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 let result = validator.validate(&set, ValidationMode::default());
375 assert_eq!(
376 result.validation_errors().collect::<Vec<_>>(),
377 Vec::<&ValidationError>::new()
378 );
379
380 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 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 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 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}