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