1#![forbid(unsafe_code)]
19
20use std::collections::HashSet;
21
22use cedar_policy_core::ast::{Policy, PolicySet, Template};
23
24mod err;
25mod str_checks;
26pub use err::*;
27mod expr_iterator;
28mod extension_schema;
29mod extensions;
30mod fuzzy_match;
31mod validation_result;
32use serde::Serialize;
33pub use validation_result::*;
34mod rbac;
35mod schema;
36pub use schema::*;
37mod schema_file_format;
38pub use schema_file_format::*;
39mod type_error;
40pub use type_error::*;
41pub mod typecheck;
42pub mod types;
43
44pub use str_checks::{confusable_string_checks, ValidationWarning, ValidationWarningKind};
45
46use self::typecheck::Typechecker;
47
48#[derive(Default, Eq, PartialEq, Copy, Clone, Debug, Serialize)]
50pub enum ValidationMode {
51 #[default]
52 Strict,
53 Permissive,
54}
55
56impl ValidationMode {
57 fn is_strict(self) -> bool {
59 match self {
60 ValidationMode::Strict => true,
61 ValidationMode::Permissive => false,
62 }
63 }
64}
65
66#[derive(Debug)]
69pub struct Validator {
70 schema: ValidatorSchema,
71}
72
73impl Validator {
74 pub fn new(schema: ValidatorSchema) -> Validator {
76 Self { schema }
77 }
78
79 pub fn validate<'a>(
82 &'a self,
83 policies: &'a PolicySet,
84 mode: ValidationMode,
85 ) -> ValidationResult<'a> {
86 let template_and_static_policy_errs = policies
87 .all_templates()
88 .flat_map(|p| self.validate_policy(p, mode));
89 let link_errs = policies
90 .policies()
91 .filter_map(|p| self.validate_slots(p))
92 .flatten();
93 ValidationResult::new(template_and_static_policy_errs.chain(link_errs))
94 }
95
96 fn validate_policy<'a>(
100 &'a self,
101 p: &'a Template,
102 mode: ValidationMode,
103 ) -> impl Iterator<Item = ValidationError> + 'a {
104 self.validate_entity_types(p)
105 .chain(self.validate_action_ids(p))
106 .chain(self.validate_action_application(
107 p.principal_constraint(),
108 p.action_constraint(),
109 p.resource_constraint(),
110 ))
111 .map(move |note| ValidationError::with_policy_id(p.id(), None, note))
112 .chain(self.typecheck_policy(p, mode))
113 }
114
115 fn validate_slots<'a>(
118 &'a self,
119 p: &'a Policy,
120 ) -> Option<impl Iterator<Item = ValidationError> + 'a> {
121 if p.is_static() {
123 return None;
124 }
125 Some(
129 self.validate_entity_types_in_slots(p.env())
130 .chain(self.validate_action_application(
131 &p.principal_constraint(),
132 p.action_constraint(),
133 &p.resource_constraint(),
134 ))
135 .map(move |note| ValidationError::with_policy_id(p.id(), None, note)),
136 )
137 }
138
139 fn typecheck_policy<'a>(
145 &'a self,
146 t: &'a Template,
147 mode: ValidationMode,
148 ) -> impl Iterator<Item = ValidationError> + 'a {
149 let typecheck = Typechecker::new(&self.schema, mode);
150 let mut type_errors = HashSet::new();
151 typecheck.typecheck_policy(t, &mut type_errors);
152 type_errors.into_iter().map(|type_error| {
153 let (kind, location) = type_error.kind_and_location();
154 ValidationError::with_policy_id(t.id(), location, ValidationErrorKind::type_error(kind))
155 })
156 }
157}
158
159#[cfg(test)]
160mod test {
161 use std::collections::HashMap;
162
163 use super::*;
164 use cedar_policy_core::{ast, parser};
165
166 #[test]
167 fn top_level_validate() -> Result<()> {
168 let mut set = PolicySet::new();
169 let foo_type = "foo_type";
170 let bar_type = "bar_type";
171 let action_name = "action";
172 let schema_file = NamespaceDefinition::new(
173 [
174 (
175 foo_type.into(),
176 EntityType {
177 member_of_types: vec![],
178 shape: AttributesOrContext::default(),
179 },
180 ),
181 (
182 bar_type.into(),
183 EntityType {
184 member_of_types: vec![],
185 shape: AttributesOrContext::default(),
186 },
187 ),
188 ],
189 [(
190 action_name.into(),
191 ActionType {
192 applies_to: Some(ApplySpec {
193 resource_types: None,
194 principal_types: None,
195 context: AttributesOrContext::default(),
196 }),
197 member_of: None,
198 attributes: None,
199 },
200 )],
201 );
202 let schema = schema_file.try_into().unwrap();
203 let validator = Validator::new(schema);
204
205 let policy_a_src = r#"permit(principal in foo_type::"a", action == Action::"actin", resource == bar_type::"b");"#;
206 let policy_a = parser::parse_policy(Some("pola".to_string()), policy_a_src)
207 .expect("Test Policy Should Parse");
208 set.add_static(policy_a.clone())
209 .expect("Policy already present in PolicySet");
210
211 let policy_b_src = r#"permit(principal in foo_tye::"a", action == Action::"action", resource == br_type::"b");"#;
212 let policy_b = parser::parse_policy(Some("polb".to_string()), policy_b_src)
213 .expect("Test Policy Should Parse");
214 set.add_static(policy_b.clone())
215 .expect("Policy already present in PolicySet");
216
217 let result = validator.validate(&set, ValidationMode::default());
218 let principal_err = ValidationError::with_policy_id(
219 policy_b.id(),
220 None,
221 ValidationErrorKind::unrecognized_entity_type(
222 "foo_tye".to_string(),
223 Some("foo_type".to_string()),
224 ),
225 );
226 let resource_err = ValidationError::with_policy_id(
227 policy_b.id(),
228 None,
229 ValidationErrorKind::unrecognized_entity_type(
230 "br_type".to_string(),
231 Some("bar_type".to_string()),
232 ),
233 );
234 let action_err = ValidationError::with_policy_id(
235 policy_a.id(),
236 None,
237 ValidationErrorKind::unrecognized_action_id(
238 "Action::\"actin\"".to_string(),
239 Some("Action::\"action\"".to_string()),
240 ),
241 );
242 assert!(!result.validation_passed());
243 assert!(result.validation_errors().any(|x| x == &principal_err));
244 assert!(result.validation_errors().any(|x| x == &resource_err));
245 assert!(result.validation_errors().any(|x| x == &action_err));
246
247 Ok(())
248 }
249
250 #[test]
251 fn top_level_validate_with_instantiations() -> Result<()> {
252 let mut set = PolicySet::new();
253 let schema: ValidatorSchema = serde_json::from_str::<SchemaFragment>(
254 r#"
255 {
256 "some_namespace": {
257 "entityTypes": {
258 "User": {
259 "shape": {
260 "type": "Record",
261 "attributes": {
262 "department": {
263 "type": "String"
264 },
265 "jobLevel": {
266 "type": "Long"
267 }
268 }
269 },
270 "memberOfTypes": [
271 "UserGroup"
272 ]
273 },
274 "UserGroup": {},
275 "Photo" : {}
276 },
277 "actions": {
278 "view": {
279 "appliesTo": {
280 "resourceTypes": [
281 "Photo"
282 ],
283 "principalTypes": [
284 "User"
285 ]
286 }
287 }
288 }
289 }
290 }
291 "#,
292 )
293 .expect("Schema parse error.")
294 .try_into()
295 .expect("Expected valid schema.");
296 let validator = Validator::new(schema);
297
298 let t = parser::parse_policy_template(
299 Some("template".to_string()),
300 r#"permit(principal == some_namespace::User::"Alice", action, resource in ?resource);"#,
301 )
302 .expect("Parse Error");
303 set.add_template(t)
304 .expect("Template already present in PolicySet");
305
306 let result = validator.validate(&set, ValidationMode::default());
308 assert_eq!(
309 result.into_validation_errors().collect::<Vec<_>>(),
310 Vec::new()
311 );
312
313 let mut values = HashMap::new();
315 values.insert(
316 ast::SlotId::resource(),
317 ast::EntityUID::from_components(
318 "some_namespace::Photo".parse().unwrap(),
319 ast::Eid::new("foo"),
320 ),
321 );
322 set.link(
323 ast::PolicyID::from_string("template"),
324 ast::PolicyID::from_string("link1"),
325 values,
326 )
327 .expect("Linking failed!");
328 let result = validator.validate(&set, ValidationMode::default());
329 assert!(result.validation_passed());
330
331 let mut values = HashMap::new();
333 values.insert(
334 ast::SlotId::resource(),
335 ast::EntityUID::from_components(
336 "some_namespace::Undefined".parse().unwrap(),
337 ast::Eid::new("foo"),
338 ),
339 );
340 set.link(
341 ast::PolicyID::from_string("template"),
342 ast::PolicyID::from_string("link2"),
343 values,
344 )
345 .expect("Linking failed!");
346 let result = validator.validate(&set, ValidationMode::default());
347 assert!(!result.validation_passed());
348 assert_eq!(result.validation_errors().count(), 2);
349 let id = ast::PolicyID::from_string("link2");
350 let undefined_err = ValidationError::with_policy_id(
351 &id,
352 None,
353 ValidationErrorKind::unrecognized_entity_type(
354 "some_namespace::Undefined".to_string(),
355 Some("some_namespace::User".to_string()),
356 ),
357 );
358 let invalid_action_err = ValidationError::with_policy_id(
359 &id,
360 None,
361 ValidationErrorKind::invalid_action_application(false, false),
362 );
363 assert!(result.validation_errors().any(|x| x == &undefined_err));
364 assert!(result.validation_errors().any(|x| x == &invalid_action_err));
365
366 let mut values = HashMap::new();
368 values.insert(
369 ast::SlotId::resource(),
370 ast::EntityUID::from_components(
371 "some_namespace::User".parse().unwrap(),
372 ast::Eid::new("foo"),
373 ),
374 );
375 set.link(
376 ast::PolicyID::from_string("template"),
377 ast::PolicyID::from_string("link3"),
378 values,
379 )
380 .expect("Linking failed!");
381 let result = validator.validate(&set, ValidationMode::default());
382 assert!(!result.validation_passed());
383 assert_eq!(result.validation_errors().count(), 3);
385 let id = ast::PolicyID::from_string("link3");
386 let invalid_action_err = ValidationError::with_policy_id(
387 &id,
388 None,
389 ValidationErrorKind::invalid_action_application(false, false),
390 );
391 assert!(result.validation_errors().any(|x| x == &invalid_action_err));
392
393 Ok(())
394 }
395}