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