1#![forbid(unsafe_code)]
19
20use std::collections::HashSet;
21
22use cedar_policy_core::ast::{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
56#[derive(Debug)]
59pub struct Validator {
60 schema: ValidatorSchema,
61}
62
63impl Validator {
64 pub fn new(schema: ValidatorSchema) -> Validator {
66 Self { schema }
67 }
68
69 pub fn validate<'a>(
72 &'a self,
73 policies: &'a PolicySet,
74 mode: ValidationMode,
75 ) -> ValidationResult<'a> {
76 let template_errs = policies
77 .all_templates()
78 .flat_map(|p| self.validate_policy(p, mode));
79 let instantiation_errs = policies.policies().flat_map(|p| {
80 self.validate_slots(p.env())
81 .map(move |note| ValidationError::with_policy_id(p.id(), None, note))
82 });
83 ValidationResult::new(template_errs.chain(instantiation_errs))
84 }
85
86 fn validate_policy<'a>(
89 &'a self,
90 p: &'a Template,
91 mode: ValidationMode,
92 ) -> impl Iterator<Item = ValidationError> + 'a {
93 self.validate_entity_types(p)
94 .chain(self.validate_action_ids(p))
95 .chain(self.validate_action_application(p))
96 .map(move |note| ValidationError::with_policy_id(p.id(), None, note))
97 .chain(self.typecheck_policy(p, mode))
98 }
99
100 fn typecheck_policy<'a>(
104 &'a self,
105 t: &'a Template,
106 mode: ValidationMode,
107 ) -> impl Iterator<Item = ValidationError> + 'a {
108 let typecheck = Typechecker::new(&self.schema);
109 let mut type_errors = HashSet::new();
110 typecheck.typecheck_policy(t, mode, &mut type_errors);
111 type_errors.into_iter().map(|type_error| {
112 let (kind, location) = type_error.kind_and_location();
113 ValidationError::with_policy_id(t.id(), location, ValidationErrorKind::type_error(kind))
114 })
115 }
116}
117
118#[cfg(test)]
119mod test {
120 use std::collections::HashMap;
121
122 use super::*;
123 use cedar_policy_core::{ast, parser};
124
125 #[test]
126 fn top_level_validate() -> Result<()> {
127 let mut set = PolicySet::new();
128 let foo_type = "foo_type";
129 let bar_type = "bar_type";
130 let action_name = "action";
131 let schema_file = NamespaceDefinition::new(
132 [
133 (
134 foo_type.into(),
135 EntityType {
136 member_of_types: vec![],
137 shape: AttributesOrContext::default(),
138 },
139 ),
140 (
141 bar_type.into(),
142 EntityType {
143 member_of_types: vec![],
144 shape: AttributesOrContext::default(),
145 },
146 ),
147 ],
148 [(
149 action_name.into(),
150 ActionType {
151 applies_to: Some(ApplySpec {
152 resource_types: None,
153 principal_types: None,
154 context: AttributesOrContext::default(),
155 }),
156 member_of: None,
157 attributes: None,
158 },
159 )],
160 );
161 let schema = schema_file.try_into().unwrap();
162 let validator = Validator::new(schema);
163
164 let policy_a_src = r#"permit(principal in foo_type::"a", action == Action::"actin", resource == bar_type::"b");"#;
165 let policy_a = parser::parse_policy(Some("pola".to_string()), policy_a_src)
166 .expect("Test Policy Should Parse");
167 set.add_static(policy_a.clone())
168 .expect("Policy already present in PolicySet");
169
170 let policy_b_src = r#"permit(principal in foo_tye::"a", action == Action::"action", resource == br_type::"b");"#;
171 let policy_b = parser::parse_policy(Some("polb".to_string()), policy_b_src)
172 .expect("Test Policy Should Parse");
173 set.add_static(policy_b.clone())
174 .expect("Policy already present in PolicySet");
175
176 let result = validator.validate(&set, ValidationMode::default());
177 let principal_err = ValidationError::with_policy_id(
178 policy_b.id(),
179 None,
180 ValidationErrorKind::unrecognized_entity_type(
181 "foo_tye".to_string(),
182 Some("foo_type".to_string()),
183 ),
184 );
185 let resource_err = ValidationError::with_policy_id(
186 policy_b.id(),
187 None,
188 ValidationErrorKind::unrecognized_entity_type(
189 "br_type".to_string(),
190 Some("bar_type".to_string()),
191 ),
192 );
193 let action_err = ValidationError::with_policy_id(
194 policy_a.id(),
195 None,
196 ValidationErrorKind::unrecognized_action_id(
197 "Action::\"actin\"".to_string(),
198 Some("Action::\"action\"".to_string()),
199 ),
200 );
201 assert!(!result.validation_passed());
202 assert!(result.validation_errors().any(|x| x == &principal_err));
203 assert!(result.validation_errors().any(|x| x == &resource_err));
204 assert!(result.validation_errors().any(|x| x == &action_err));
205
206 Ok(())
207 }
208
209 #[test]
210 fn top_level_validate_with_instantiations() -> Result<()> {
211 let mut set = PolicySet::new();
212 let schema: ValidatorSchema = serde_json::from_str::<SchemaFragment>(
213 r#"
214 {
215 "some_namespace": {
216 "entityTypes": {
217 "User": {
218 "shape": {
219 "type": "Record",
220 "attributes": {
221 "department": {
222 "type": "String"
223 },
224 "jobLevel": {
225 "type": "Long"
226 }
227 }
228 },
229 "memberOfTypes": [
230 "UserGroup"
231 ]
232 },
233 "UserGroup": {},
234 "Photo" : {}
235 },
236 "actions": {
237 "view": {
238 "appliesTo": {
239 "resourceTypes": [
240 "Photo"
241 ],
242 "principalTypes": [
243 "User"
244 ]
245 }
246 }
247 }
248 }
249 }
250 "#,
251 )
252 .expect("Schema parse error.")
253 .try_into()
254 .expect("Expected valid schema.");
255 let validator = Validator::new(schema);
256
257 let t = parser::parse_policy_template(
258 Some("template".to_string()),
259 r#"permit(principal == some_namespace::User::"Alice", action, resource in ?resource);"#,
260 )
261 .expect("Parse Error");
262 set.add_template(t)
263 .expect("Template already present in PolicySet");
264
265 let result = validator.validate(&set, ValidationMode::default());
267 assert_eq!(
268 result.into_validation_errors().collect::<Vec<_>>(),
269 Vec::new()
270 );
271
272 let mut values = HashMap::new();
274 values.insert(
275 ast::SlotId::resource(),
276 ast::EntityUID::from_components(
277 "some_namespace::Photo".parse().unwrap(),
278 ast::Eid::new("foo"),
279 ),
280 );
281 set.link(
282 ast::PolicyID::from_string("template"),
283 ast::PolicyID::from_string("link1"),
284 values,
285 )
286 .expect("Linking failed!");
287 let result = validator.validate(&set, ValidationMode::default());
288 assert!(result.validation_passed());
289
290 let mut values = HashMap::new();
292 values.insert(
293 ast::SlotId::resource(),
294 ast::EntityUID::from_components(
295 "some_namespace::Undefined".parse().unwrap(),
296 ast::Eid::new("foo"),
297 ),
298 );
299 set.link(
300 ast::PolicyID::from_string("template"),
301 ast::PolicyID::from_string("link2"),
302 values,
303 )
304 .expect("Linking failed!");
305 let result = validator.validate(&set, ValidationMode::default());
306
307 let pid = ast::PolicyID::from_string("link2");
308 let resource_err = ValidationError::with_policy_id(
309 &pid,
310 None,
311 ValidationErrorKind::unrecognized_entity_type(
312 "some_namespace::Undefined".to_string(),
313 Some("some_namespace::User".to_string()),
314 ),
315 );
316 assert!(!result.validation_passed());
317 println!("{:?}", result.validation_errors().collect::<Vec<_>>());
318 assert!(result.validation_errors().any(|x| x == &resource_err));
319
320 Ok(())
321 }
322}