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, ValidationWarning, ValidationWarningKind};
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 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 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 None
127 } else {
128 Some(
129 self.validate_entity_types(p)
130 .chain(self.validate_action_ids(p))
131 .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 fn validate_slots<'a>(
151 &'a self,
152 p: &'a Policy,
153 mode: ValidationMode,
154 ) -> Option<impl Iterator<Item = ValidationError> + 'a> {
155 if p.is_static() {
157 return None;
158 }
159 if mode.is_partial() {
163 return None;
164 }
165 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 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 let result = validator.validate(&set, ValidationMode::default());
353 assert_eq!(
354 result.validation_errors().collect::<Vec<_>>(),
355 Vec::<&ValidationError>::new()
356 );
357
358 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 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 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 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}