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