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