/rust/registry/src/index.crates.io-6f17d22bba15001f/cedar-policy-validator-2.4.2/src/rbac.rs
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright 2022-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. |
3 | | * |
4 | | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | | * you may not use this file except in compliance with the License. |
6 | | * You may obtain a copy of the License at |
7 | | * |
8 | | * https://www.apache.org/licenses/LICENSE-2.0 |
9 | | * |
10 | | * Unless required by applicable law or agreed to in writing, software |
11 | | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | | * See the License for the specific language governing permissions and |
14 | | * limitations under the License. |
15 | | */ |
16 | | |
17 | | //! Contains the validation logic specific to RBAC policy validation. |
18 | | |
19 | | use cedar_policy_core::ast::{ |
20 | | self, ActionConstraint, EntityReference, EntityUID, Name, PrincipalConstraint, |
21 | | PrincipalOrResourceConstraint, ResourceConstraint, SlotEnv, Template, |
22 | | }; |
23 | | |
24 | | use std::{collections::HashSet, sync::Arc}; |
25 | | |
26 | | /// Enum for representing a reference to any variation of a HeadConstraint |
27 | | #[derive(Debug, Clone)] |
28 | | pub enum HeadConstraint<'a> { |
29 | | /// Represents constraints on the principal or resource |
30 | | PrincipalOrResource(&'a PrincipalOrResourceConstraint), |
31 | | /// Represents action constraints |
32 | | Action(&'a ActionConstraint), |
33 | | } |
34 | | |
35 | | impl<'a> From<&'a ActionConstraint> for HeadConstraint<'a> { |
36 | 0 | fn from(a: &'a ActionConstraint) -> Self { |
37 | 0 | HeadConstraint::Action(a) |
38 | 0 | } |
39 | | } |
40 | | |
41 | | impl<'a> From<&'a PrincipalOrResourceConstraint> for HeadConstraint<'a> { |
42 | 0 | fn from(por: &'a PrincipalOrResourceConstraint) -> Self { |
43 | 0 | HeadConstraint::PrincipalOrResource(por) |
44 | 0 | } |
45 | | } |
46 | | |
47 | | use crate::expr_iterator::policy_entity_uids; |
48 | | |
49 | | use super::{ |
50 | | fuzzy_match::fuzzy_search, schema::*, validation_result::ValidationErrorKind, Validator, |
51 | | }; |
52 | | |
53 | | impl Validator { |
54 | | /// Generate UnrecognizedEntityType notes for every entity type in the |
55 | | /// expression that could not also be found in the schema. |
56 | 0 | pub(crate) fn validate_entity_types<'a>( |
57 | 0 | &'a self, |
58 | 0 | template: &'a Template, |
59 | 0 | ) -> impl Iterator<Item = ValidationErrorKind> + 'a { |
60 | 0 | // All valid entity types in the schema. These will be used to generate |
61 | 0 | // suggestion when an entity type is not found. |
62 | 0 | let known_entity_types = self |
63 | 0 | .schema |
64 | 0 | .known_entity_types() |
65 | 0 | .map(ToString::to_string) |
66 | 0 | .collect::<Vec<_>>(); |
67 | 0 |
|
68 | 0 | policy_entity_uids(template).filter_map(move |euid| { |
69 | 0 | let entity_type = euid.entity_type(); |
70 | 0 | match entity_type { |
71 | 0 | cedar_policy_core::ast::EntityType::Unspecified => Some( |
72 | 0 | ValidationErrorKind::unspecified_entity(euid.eid().to_string()), |
73 | 0 | ), |
74 | 0 | cedar_policy_core::ast::EntityType::Concrete(name) => { |
75 | 0 | let is_action_entity_type = is_action_entity_type(name); |
76 | 0 | let is_known_entity_type = self.schema.is_known_entity_type(name); |
77 | 0 |
|
78 | 0 | if !is_action_entity_type && !is_known_entity_type { |
79 | 0 | let actual_entity_type = entity_type.to_string(); |
80 | 0 | let suggested_entity_type = |
81 | 0 | fuzzy_search(&actual_entity_type, known_entity_types.as_slice()); |
82 | 0 | Some(ValidationErrorKind::unrecognized_entity_type( |
83 | 0 | actual_entity_type, |
84 | 0 | suggested_entity_type, |
85 | 0 | )) |
86 | | } else { |
87 | 0 | None |
88 | | } |
89 | | } |
90 | | } |
91 | 0 | }) |
92 | 0 | } |
93 | | |
94 | | /// Generate UnrecognizedActionId notes for every entity id with an action |
95 | | /// entity type where the id could not be found in the actions list from the |
96 | | /// schema. |
97 | 0 | pub(crate) fn validate_action_ids<'a>( |
98 | 0 | &'a self, |
99 | 0 | template: &'a Template, |
100 | 0 | ) -> impl Iterator<Item = ValidationErrorKind> + 'a { |
101 | 0 | // Valid action id names that will be used to generate suggestions if an |
102 | 0 | // action id is not found |
103 | 0 | let known_action_ids = self |
104 | 0 | .schema |
105 | 0 | .known_action_ids() |
106 | 0 | .map(ToString::to_string) |
107 | 0 | .collect::<Vec<_>>(); |
108 | 0 | policy_entity_uids(template).filter_map(move |euid| { |
109 | 0 | let entity_type = euid.entity_type(); |
110 | 0 | match entity_type { |
111 | 0 | ast::EntityType::Unspecified => Some(ValidationErrorKind::unspecified_entity( |
112 | 0 | euid.eid().to_string(), |
113 | 0 | )), |
114 | 0 | ast::EntityType::Concrete(name) => { |
115 | 0 | let is_known_action_entity_id = self.schema.is_known_action_id(euid); |
116 | 0 | let is_action_entity_type = is_action_entity_type(name); |
117 | 0 |
|
118 | 0 | if is_action_entity_type && !is_known_action_entity_id { |
119 | 0 | Some(ValidationErrorKind::unrecognized_action_id( |
120 | 0 | euid.to_string(), |
121 | 0 | fuzzy_search(euid.eid().as_ref(), known_action_ids.as_slice()), |
122 | 0 | )) |
123 | | } else { |
124 | 0 | None |
125 | | } |
126 | | } |
127 | | } |
128 | 0 | }) |
129 | 0 | } |
130 | | |
131 | | /// Generate UnrecognizedEntityType or UnspecifiedEntity notes for every |
132 | | /// entity type in the slot environment that is either not in the schema, |
133 | | /// or unspecified. |
134 | 0 | pub(crate) fn validate_entity_types_in_slots<'a>( |
135 | 0 | &'a self, |
136 | 0 | slots: &'a SlotEnv, |
137 | 0 | ) -> impl Iterator<Item = ValidationErrorKind> + 'a { |
138 | 0 | // All valid entity types in the schema. These will be used to generate |
139 | 0 | // suggestion when an entity type is not found. |
140 | 0 | let known_entity_types = self |
141 | 0 | .schema |
142 | 0 | .known_entity_types() |
143 | 0 | .map(ToString::to_string) |
144 | 0 | .collect::<Vec<_>>(); |
145 | 0 |
|
146 | 0 | slots.values().filter_map(move |euid| { |
147 | 0 | let entity_type = euid.entity_type(); |
148 | 0 | match entity_type { |
149 | 0 | cedar_policy_core::ast::EntityType::Unspecified => Some( |
150 | 0 | ValidationErrorKind::unspecified_entity(euid.eid().to_string()), |
151 | 0 | ), |
152 | 0 | cedar_policy_core::ast::EntityType::Concrete(name) => { |
153 | 0 | if !self.schema.is_known_entity_type(name) { |
154 | 0 | let actual_entity_type = entity_type.to_string(); |
155 | 0 | let suggested_entity_type = |
156 | 0 | fuzzy_search(&actual_entity_type, known_entity_types.as_slice()); |
157 | 0 | Some(ValidationErrorKind::unrecognized_entity_type( |
158 | 0 | actual_entity_type, |
159 | 0 | suggested_entity_type, |
160 | 0 | )) |
161 | | } else { |
162 | 0 | None |
163 | | } |
164 | | } |
165 | | } |
166 | 0 | }) |
167 | 0 | } |
168 | | |
169 | 0 | fn check_if_in_fixes_principal( |
170 | 0 | &self, |
171 | 0 | principal_constraint: &PrincipalConstraint, |
172 | 0 | action_constraint: &ActionConstraint, |
173 | 0 | ) -> bool { |
174 | 0 | self.check_if_in_fixes( |
175 | 0 | principal_constraint.as_inner(), |
176 | 0 | &self |
177 | 0 | .get_apply_specs_for_action(action_constraint) |
178 | 0 | .collect::<Vec<_>>(), |
179 | 0 | &|spec| Box::new(spec.applicable_principal_types()), |
180 | 0 | ) |
181 | 0 | } |
182 | | |
183 | 0 | fn check_if_in_fixes_resource( |
184 | 0 | &self, |
185 | 0 | resource_constraint: &ResourceConstraint, |
186 | 0 | action_constraint: &ActionConstraint, |
187 | 0 | ) -> bool { |
188 | 0 | self.check_if_in_fixes( |
189 | 0 | resource_constraint.as_inner(), |
190 | 0 | &self |
191 | 0 | .get_apply_specs_for_action(action_constraint) |
192 | 0 | .collect::<Vec<_>>(), |
193 | 0 | &|spec| Box::new(spec.applicable_resource_types()), |
194 | 0 | ) |
195 | 0 | } |
196 | | |
197 | 0 | fn check_if_in_fixes<'a>( |
198 | 0 | &'a self, |
199 | 0 | head_var_condition: &PrincipalOrResourceConstraint, |
200 | 0 | apply_specs: &[&'a ValidatorApplySpec], |
201 | 0 | select_apply_spec: &impl Fn( |
202 | 0 | &'a ValidatorApplySpec, |
203 | 0 | ) -> Box<dyn Iterator<Item = &'a ast::EntityType> + 'a>, |
204 | 0 | ) -> bool { |
205 | 0 | let euid = Validator::get_eq_comparison( |
206 | 0 | head_var_condition, |
207 | 0 | PrincipalOrResourceHeadVar::PrincipalOrResource, |
208 | 0 | ); |
209 | 0 |
|
210 | 0 | // Now we check the following property |
211 | 0 | // not exists spec in apply_specs such that lit in spec.principals |
212 | 0 | // AND |
213 | 0 | // exists spec in apply_specs such that there exists principal in spec.principals such that lit `memberOf` principal |
214 | 0 | // (as well as for resource) |
215 | 0 | self.check_if_none_equal(apply_specs, euid.as_ref(), &select_apply_spec) |
216 | 0 | && self.check_if_any_contain(apply_specs, euid.as_ref(), &select_apply_spec) |
217 | 0 | } Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_in_fixes::<<cedar_policy_validator::Validator>::check_if_in_fixes_resource::{closure#0}> Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_in_fixes::<<cedar_policy_validator::Validator>::check_if_in_fixes_principal::{closure#0}> |
218 | | |
219 | | // This checks the first property: |
220 | | // not exists spec in apply_specs such that lit in spec.principals |
221 | 0 | fn check_if_none_equal<'a>( |
222 | 0 | &'a self, |
223 | 0 | specs: &[&'a ValidatorApplySpec], |
224 | 0 | lit_opt: Option<&Name>, |
225 | 0 | select_apply_spec: &impl Fn( |
226 | 0 | &'a ValidatorApplySpec, |
227 | 0 | ) -> Box<dyn Iterator<Item = &'a ast::EntityType> + 'a>, |
228 | 0 | ) -> bool { |
229 | 0 | if let Some(lit) = lit_opt { |
230 | 0 | !specs.iter().any(|spec| { |
231 | 0 | select_apply_spec(spec).any(|e| match e { |
232 | 0 | ast::EntityType::Concrete(e) => e == lit, |
233 | 0 | ast::EntityType::Unspecified => false, |
234 | 0 | }) Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_none_equal::<&<cedar_policy_validator::Validator>::check_if_in_fixes_resource::{closure#0}>::{closure#0}::{closure#0} Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_none_equal::<&<cedar_policy_validator::Validator>::check_if_in_fixes_principal::{closure#0}>::{closure#0}::{closure#0} |
235 | 0 | }) Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_none_equal::<&<cedar_policy_validator::Validator>::check_if_in_fixes_resource::{closure#0}>::{closure#0} Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_none_equal::<&<cedar_policy_validator::Validator>::check_if_in_fixes_principal::{closure#0}>::{closure#0} |
236 | | } else { |
237 | 0 | false |
238 | | } |
239 | 0 | } Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_none_equal::<&<cedar_policy_validator::Validator>::check_if_in_fixes_resource::{closure#0}> Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_none_equal::<&<cedar_policy_validator::Validator>::check_if_in_fixes_principal::{closure#0}> |
240 | | |
241 | | // This checks the second property |
242 | | // exists spec in apply_specs such that there exists principal in spec.principals such that lit `memberOf` principal |
243 | 0 | fn check_if_any_contain<'a>( |
244 | 0 | &'a self, |
245 | 0 | specs: &[&'a ValidatorApplySpec], |
246 | 0 | lit_opt: Option<&Name>, |
247 | 0 | select_apply_spec: &impl Fn( |
248 | 0 | &'a ValidatorApplySpec, |
249 | 0 | ) -> Box<dyn Iterator<Item = &'a ast::EntityType> + 'a>, |
250 | 0 | ) -> bool { |
251 | 0 | if let Some(etype) = lit_opt.and_then(|typename| self.schema.get_entity_type(typename)) { Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_any_contain::<&<cedar_policy_validator::Validator>::check_if_in_fixes_resource::{closure#0}>::{closure#0} Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_any_contain::<&<cedar_policy_validator::Validator>::check_if_in_fixes_principal::{closure#0}>::{closure#0} |
252 | 0 | specs.iter().any(|spec| { |
253 | 0 | select_apply_spec(spec).any(|p| match p { |
254 | 0 | ast::EntityType::Concrete(p) => etype.descendants.contains(p), |
255 | 0 | ast::EntityType::Unspecified => false, |
256 | 0 | }) Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_any_contain::<&<cedar_policy_validator::Validator>::check_if_in_fixes_resource::{closure#0}>::{closure#1}::{closure#0} Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_any_contain::<&<cedar_policy_validator::Validator>::check_if_in_fixes_principal::{closure#0}>::{closure#1}::{closure#0} |
257 | 0 | }) Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_any_contain::<&<cedar_policy_validator::Validator>::check_if_in_fixes_resource::{closure#0}>::{closure#1} Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_any_contain::<&<cedar_policy_validator::Validator>::check_if_in_fixes_principal::{closure#0}>::{closure#1} |
258 | | } else { |
259 | 0 | false |
260 | | } |
261 | 0 | } Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_any_contain::<&<cedar_policy_validator::Validator>::check_if_in_fixes_resource::{closure#0}> Unexecuted instantiation: <cedar_policy_validator::Validator>::check_if_any_contain::<&<cedar_policy_validator::Validator>::check_if_in_fixes_principal::{closure#0}> |
262 | | |
263 | | /// Check if an expression is an equality comparison between a literal EUID and a head variable. |
264 | | /// If it is, return the EUID. |
265 | 0 | fn get_eq_comparison<K>( |
266 | 0 | head_var_condition: &PrincipalOrResourceConstraint, |
267 | 0 | var: impl HeadVar<K>, |
268 | 0 | ) -> Option<K> { |
269 | 0 | match head_var_condition { |
270 | 0 | PrincipalOrResourceConstraint::Eq(EntityReference::EUID(euid)) => { |
271 | 0 | var.get_euid_component(euid.as_ref().clone()) |
272 | | } |
273 | 0 | _ => None, |
274 | | } |
275 | 0 | } |
276 | | |
277 | | // Check that there exists a (action id, principal type, resource type) |
278 | | // entity type pair where the action can be applied to both the principal |
279 | | // and resource. This function takes the three scope constraints as input |
280 | | // (rather than a template) to facilitate code reuse. |
281 | 0 | pub(crate) fn validate_action_application( |
282 | 0 | &self, |
283 | 0 | principal_constraint: &PrincipalConstraint, |
284 | 0 | action_constraint: &ActionConstraint, |
285 | 0 | resource_constraint: &ResourceConstraint, |
286 | 0 | ) -> impl Iterator<Item = ValidationErrorKind> { |
287 | 0 | let mut apply_specs = self.get_apply_specs_for_action(action_constraint); |
288 | 0 | let resources_for_head: HashSet<Name> = self |
289 | 0 | .get_resources_satisfying_constraint(resource_constraint) |
290 | 0 | .collect(); |
291 | 0 | let principals_for_head: HashSet<Name> = self |
292 | 0 | .get_principals_satisfying_constraint(principal_constraint) |
293 | 0 | .collect(); |
294 | 0 |
|
295 | 0 | let would_in_fix_principal = |
296 | 0 | self.check_if_in_fixes_principal(principal_constraint, action_constraint); |
297 | 0 | let would_in_fix_resource = |
298 | 0 | self.check_if_in_fixes_resource(resource_constraint, action_constraint); |
299 | 0 |
|
300 | 0 | Some(ValidationErrorKind::invalid_action_application( |
301 | 0 | would_in_fix_principal, |
302 | 0 | would_in_fix_resource, |
303 | 0 | )) |
304 | 0 | .filter(|_| { |
305 | 0 | !apply_specs.any(|spec| { |
306 | 0 | let action_principals = spec |
307 | 0 | .applicable_principal_types() |
308 | 0 | .filter_map(|ty| match ty { |
309 | 0 | ast::EntityType::Concrete(name) => Some(name.clone()), |
310 | 0 | ast::EntityType::Unspecified => None, |
311 | 0 | }) |
312 | 0 | .collect::<HashSet<_>>(); |
313 | 0 | let applicable_principal_unspecified = spec |
314 | 0 | .applicable_principal_types() |
315 | 0 | .any(|ty| matches!(ty, ast::EntityType::Unspecified)); |
316 | 0 | let action_resources = spec |
317 | 0 | .applicable_resource_types() |
318 | 0 | .filter_map(|ty| match ty { |
319 | 0 | ast::EntityType::Concrete(name) => Some(name.clone()), |
320 | 0 | ast::EntityType::Unspecified => None, |
321 | 0 | }) |
322 | 0 | .collect::<HashSet<_>>(); |
323 | 0 | let applicable_resource_unspecified = spec |
324 | 0 | .applicable_resource_types() |
325 | 0 | .any(|ty| matches!(ty, ast::EntityType::Unspecified)); |
326 | | |
327 | 0 | let matching_principal = applicable_principal_unspecified |
328 | 0 | || !principals_for_head.is_disjoint(&action_principals); |
329 | 0 | let matching_resource = applicable_resource_unspecified |
330 | 0 | || !resources_for_head.is_disjoint(&action_resources); |
331 | 0 | matching_principal && matching_resource |
332 | 0 | }) |
333 | 0 | }) |
334 | 0 | .into_iter() |
335 | 0 | } |
336 | | |
337 | | /// Gather all ApplySpec objects for all actions in the schema. The `applies_to` |
338 | | /// field is optional in the schema. In the case that it was not supplied, the |
339 | | /// `applies_to` field will contain `UnspecifiedEntity`. |
340 | 0 | pub(crate) fn get_apply_specs_for_action<'a>( |
341 | 0 | &'a self, |
342 | 0 | action_constraint: &'a ActionConstraint, |
343 | 0 | ) -> impl Iterator<Item = &ValidatorApplySpec> + 'a { |
344 | 0 | self.get_actions_satisfying_constraint(action_constraint) |
345 | 0 | // Get the action type if the id string exists, and then the |
346 | 0 | // applies_to list. |
347 | 0 | .filter_map(|action_id| self.schema.get_action_id(&action_id)) |
348 | 0 | .map(|action| &action.applies_to) |
349 | 0 | } |
350 | | |
351 | | /// Get the set of principals (entity type strings) that satisfy the principal |
352 | | /// head constraint of the policy. |
353 | 0 | pub(crate) fn get_principals_satisfying_constraint<'a>( |
354 | 0 | &'a self, |
355 | 0 | principal_constraint: &'a PrincipalConstraint, |
356 | 0 | ) -> impl Iterator<Item = Name> + 'a { |
357 | 0 | self.get_entities_satisfying_constraint( |
358 | 0 | HeadConstraint::from(principal_constraint.as_inner()), |
359 | 0 | PrincipalOrResourceHeadVar::PrincipalOrResource, |
360 | 0 | ) |
361 | 0 | } |
362 | | |
363 | | /// Get the set of actions (action entity id strings) that satisfy the |
364 | | /// action head constraint of the policy. |
365 | 0 | pub(crate) fn get_actions_satisfying_constraint<'a>( |
366 | 0 | &'a self, |
367 | 0 | action_constraint: &'a ActionConstraint, |
368 | 0 | ) -> impl Iterator<Item = EntityUID> + 'a { |
369 | 0 | self.get_entities_satisfying_constraint( |
370 | 0 | HeadConstraint::from(action_constraint), |
371 | 0 | ActionHeadVar::Action, |
372 | 0 | ) |
373 | 0 | } |
374 | | |
375 | | /// Get the set of resources (entity type strings) that satisfy the resource |
376 | | /// head constraint of the policy. |
377 | 0 | pub(crate) fn get_resources_satisfying_constraint<'a>( |
378 | 0 | &'a self, |
379 | 0 | resource_constraint: &'a ResourceConstraint, |
380 | 0 | ) -> impl Iterator<Item = Name> + 'a { |
381 | 0 | self.get_entities_satisfying_constraint( |
382 | 0 | HeadConstraint::from(resource_constraint.as_inner()), |
383 | 0 | PrincipalOrResourceHeadVar::PrincipalOrResource, |
384 | 0 | ) |
385 | 0 | } |
386 | | |
387 | | // Get the set of entities satisfying the condition for a particular |
388 | | // variable in the head of the policy. Note: The strings returned by this |
389 | | // function will be entity types in the case that `var` is principal or |
390 | | // resource but will be action ids in the case that it is action. |
391 | 0 | fn get_entities_satisfying_constraint<'a, H, K>( |
392 | 0 | &'a self, |
393 | 0 | head_var_condition: HeadConstraint<'a>, |
394 | 0 | var: H, |
395 | 0 | ) -> Box<dyn Iterator<Item = K> + 'a> |
396 | 0 | where |
397 | 0 | H: 'a + HeadVar<K>, |
398 | 0 | K: 'a + Clone, |
399 | 0 | { |
400 | 0 | match head_var_condition { |
401 | | HeadConstraint::Action(ActionConstraint::Any) |
402 | | | HeadConstraint::PrincipalOrResource(PrincipalOrResourceConstraint::Any) => { |
403 | | // <var> |
404 | 0 | Box::new(var.get_known_vars(&self.schema).map(Clone::clone)) |
405 | | } |
406 | 0 | HeadConstraint::Action(ActionConstraint::Eq(euid)) |
407 | | | HeadConstraint::PrincipalOrResource(PrincipalOrResourceConstraint::Eq( |
408 | 0 | EntityReference::EUID(euid), |
409 | | )) => { |
410 | | // <var> == <literal euid> |
411 | 0 | match self.schema.get_entity_eq(var, euid.as_ref().clone()) { |
412 | 0 | Some(entity) => Box::new([entity].into_iter()), |
413 | 0 | None => Box::new(std::iter::empty()), |
414 | | } |
415 | | } |
416 | | HeadConstraint::PrincipalOrResource(PrincipalOrResourceConstraint::In( |
417 | 0 | EntityReference::EUID(euid), |
418 | 0 | )) => { |
419 | 0 | // <var> in <literal euid> |
420 | 0 | Box::new(self.schema.get_entities_in(var, euid.as_ref().clone())) |
421 | | } |
422 | | HeadConstraint::PrincipalOrResource(PrincipalOrResourceConstraint::Eq( |
423 | | EntityReference::Slot, |
424 | 0 | )) => Box::new(var.get_known_vars(&self.schema).map(Clone::clone)), |
425 | | HeadConstraint::PrincipalOrResource(PrincipalOrResourceConstraint::In( |
426 | | EntityReference::Slot, |
427 | 0 | )) => Box::new(var.get_known_vars(&self.schema).map(Clone::clone)), |
428 | 0 | HeadConstraint::Action(ActionConstraint::In(euids)) => { |
429 | 0 | // <var> in [<literal euid>...] |
430 | 0 | Box::new( |
431 | 0 | self.schema |
432 | 0 | .get_entities_in_set(var, euids.iter().map(Arc::as_ref).cloned()), |
433 | 0 | ) |
434 | | } |
435 | | } |
436 | 0 | } Unexecuted instantiation: <cedar_policy_validator::Validator>::get_entities_satisfying_constraint::<cedar_policy_validator::schema::ActionHeadVar, cedar_policy_core::ast::entity::EntityUID> Unexecuted instantiation: <cedar_policy_validator::Validator>::get_entities_satisfying_constraint::<cedar_policy_validator::schema::PrincipalOrResourceHeadVar, cedar_policy_core::ast::name::Name> |
437 | | } |
438 | | |
439 | | // PANIC SAFETY unit tests |
440 | | #[allow(clippy::panic)] |
441 | | // PANIC SAFETY unit tests |
442 | | #[allow(clippy::indexing_slicing)] |
443 | | #[cfg(test)] |
444 | | mod test { |
445 | | use std::collections::{BTreeMap, HashMap, HashSet}; |
446 | | |
447 | | use cedar_policy_core::{ |
448 | | ast::{Effect, Eid, EntityUID, Expr, PolicyID, PrincipalConstraint, ResourceConstraint}, |
449 | | parser::parse_policy, |
450 | | }; |
451 | | |
452 | | use super::*; |
453 | | use crate::{ |
454 | | err::*, schema_file_format::NamespaceDefinition, schema_file_format::*, TypeErrorKind, |
455 | | UnrecognizedActionId, UnrecognizedEntityType, UnspecifiedEntity, ValidationError, |
456 | | ValidationMode, Validator, |
457 | | }; |
458 | | |
459 | | #[test] |
460 | | fn validate_entity_type_empty_schema() -> Result<()> { |
461 | | let policy = Template::new( |
462 | | PolicyID::from_string("policy0"), |
463 | | BTreeMap::new(), |
464 | | Effect::Permit, |
465 | | PrincipalConstraint::any(), |
466 | | ActionConstraint::any(), |
467 | | ResourceConstraint::is_eq( |
468 | | EntityUID::with_eid_and_type("foo_type", "foo_name").expect("should be valid"), |
469 | | ), |
470 | | Expr::val(true), |
471 | | ); |
472 | | |
473 | | let validate = Validator::new(ValidatorSchema::empty()); |
474 | | let notes: Vec<ValidationErrorKind> = validate.validate_entity_types(&policy).collect(); |
475 | | |
476 | | assert_eq!(1, notes.len()); |
477 | | match notes.get(0) { |
478 | | Some(ValidationErrorKind::UnrecognizedEntityType(UnrecognizedEntityType { |
479 | | actual_entity_type, |
480 | | suggested_entity_type, |
481 | | })) => { |
482 | | assert_eq!("foo_type", actual_entity_type); |
483 | | assert!( |
484 | | suggested_entity_type.is_none(), |
485 | | "Did not expect suggested entity type." |
486 | | ); |
487 | | } |
488 | | _ => panic!("Unexpected variant of ValidationErrorKind."), |
489 | | }; |
490 | | |
491 | | Ok(()) |
492 | | } |
493 | | |
494 | | #[test] |
495 | | fn validate_equals_instead_of_in() -> Result<()> { |
496 | | let user_type = "user"; |
497 | | let group_type = "admins"; |
498 | | let widget_type = "widget"; |
499 | | let bin_type = "bin"; |
500 | | let action_name = "act"; |
501 | | let schema_file = NamespaceDefinition::new( |
502 | | [ |
503 | | ( |
504 | | user_type.into(), |
505 | | EntityType { |
506 | | member_of_types: vec![group_type.into()], |
507 | | shape: AttributesOrContext::default(), |
508 | | }, |
509 | | ), |
510 | | ( |
511 | | group_type.into(), |
512 | | EntityType { |
513 | | member_of_types: vec![], |
514 | | shape: AttributesOrContext::default(), |
515 | | }, |
516 | | ), |
517 | | ( |
518 | | widget_type.into(), |
519 | | EntityType { |
520 | | member_of_types: vec![bin_type.into()], |
521 | | shape: AttributesOrContext::default(), |
522 | | }, |
523 | | ), |
524 | | ( |
525 | | bin_type.into(), |
526 | | EntityType { |
527 | | member_of_types: vec![], |
528 | | shape: AttributesOrContext::default(), |
529 | | }, |
530 | | ), |
531 | | ], |
532 | | [( |
533 | | action_name.into(), |
534 | | ActionType { |
535 | | applies_to: Some(ApplySpec { |
536 | | resource_types: Some(vec![widget_type.into()]), |
537 | | principal_types: Some(vec![user_type.into()]), |
538 | | context: AttributesOrContext::default(), |
539 | | }), |
540 | | member_of: None, |
541 | | attributes: None, |
542 | | }, |
543 | | )], |
544 | | ); |
545 | | let schema = schema_file.try_into().unwrap(); |
546 | | |
547 | | let group_eid = EntityUID::with_eid_and_type(group_type, "admin1").expect(""); |
548 | | |
549 | | let action_eid = EntityUID::with_eid_and_type("Action", action_name).expect(""); |
550 | | |
551 | | let bin_eid = EntityUID::with_eid_and_type(bin_type, "bin").expect(""); |
552 | | |
553 | | let principal_constraint = PrincipalConstraint::is_eq(group_eid); |
554 | | let action_constraint = ActionConstraint::is_eq(action_eid); |
555 | | let resource_constraint = ResourceConstraint::is_eq(bin_eid); |
556 | | |
557 | | let v = Validator::new(schema); |
558 | | |
559 | | let notes = v |
560 | | .validate_action_application( |
561 | | &principal_constraint, |
562 | | &action_constraint, |
563 | | &resource_constraint, |
564 | | ) |
565 | | .collect::<Vec<_>>(); |
566 | | |
567 | | assert_eq!( |
568 | | vec![ValidationErrorKind::invalid_action_application(true, true)], |
569 | | notes, |
570 | | "Validation result did not contain InvalidActionApplication with both suggested fixes." |
571 | | ); |
572 | | Ok(()) |
573 | | } |
574 | | |
575 | | #[test] |
576 | | fn validate_entity_type_in_singleton_schema() -> Result<()> { |
577 | | let foo_type = "foo_type"; |
578 | | let schema_file = NamespaceDefinition::new( |
579 | | [( |
580 | | foo_type.into(), |
581 | | EntityType { |
582 | | member_of_types: vec![], |
583 | | shape: AttributesOrContext::default(), |
584 | | }, |
585 | | )], |
586 | | [], |
587 | | ); |
588 | | let singleton_schema = schema_file.try_into().unwrap(); |
589 | | let policy = Template::new( |
590 | | PolicyID::from_string("policy0"), |
591 | | BTreeMap::new(), |
592 | | Effect::Permit, |
593 | | PrincipalConstraint::any(), |
594 | | ActionConstraint::any(), |
595 | | ResourceConstraint::is_eq( |
596 | | EntityUID::with_eid_and_type(foo_type, "foo_name") |
597 | | .expect("should be a valid identifier"), |
598 | | ), |
599 | | Expr::val(true), |
600 | | ); |
601 | | |
602 | | let validate = Validator::new(singleton_schema); |
603 | | assert!( |
604 | | validate.validate_entity_types(&policy).next().is_none(), |
605 | | "Did not expect any validation notes." |
606 | | ); |
607 | | |
608 | | Ok(()) |
609 | | } |
610 | | |
611 | | #[test] |
612 | | fn validate_entity_type_not_in_singleton_schema() -> Result<()> { |
613 | | let schema_file = NamespaceDefinition::new( |
614 | | [( |
615 | | "foo_type".into(), |
616 | | EntityType { |
617 | | member_of_types: vec![], |
618 | | shape: AttributesOrContext::default(), |
619 | | }, |
620 | | )], |
621 | | [], |
622 | | ); |
623 | | let singleton_schema = schema_file.try_into().unwrap(); |
624 | | let policy = Template::new( |
625 | | PolicyID::from_string("policy0"), |
626 | | BTreeMap::new(), |
627 | | Effect::Permit, |
628 | | PrincipalConstraint::is_eq( |
629 | | EntityUID::with_eid_and_type("bar_type", "bar_name") |
630 | | .expect("should be a valid identifier"), |
631 | | ), |
632 | | ActionConstraint::any(), |
633 | | ResourceConstraint::any(), |
634 | | Expr::val(true), |
635 | | ); |
636 | | |
637 | | let validate = Validator::new(singleton_schema); |
638 | | let notes: Vec<ValidationErrorKind> = validate.validate_entity_types(&policy).collect(); |
639 | | |
640 | | assert_eq!(1, notes.len()); |
641 | | match notes.get(0) { |
642 | | Some(ValidationErrorKind::UnrecognizedEntityType(UnrecognizedEntityType { |
643 | | actual_entity_type, |
644 | | suggested_entity_type, |
645 | | })) => { |
646 | | assert_eq!("bar_type", actual_entity_type); |
647 | | assert_eq!( |
648 | | "foo_type", |
649 | | suggested_entity_type |
650 | | .as_ref() |
651 | | .expect("Expected a suggested entity type") |
652 | | ); |
653 | | } |
654 | | _ => panic!("Unexpected variant of ValidationErrorKind."), |
655 | | }; |
656 | | |
657 | | Ok(()) |
658 | | } |
659 | | |
660 | | #[test] |
661 | | fn validate_action_id_empty_schema() -> Result<()> { |
662 | | let entity = EntityUID::with_eid_and_type("Action", "foo_name") |
663 | | .expect("should be a valid identifier"); |
664 | | let policy = Template::new( |
665 | | PolicyID::from_string("policy0"), |
666 | | BTreeMap::new(), |
667 | | Effect::Permit, |
668 | | PrincipalConstraint::any(), |
669 | | ActionConstraint::is_eq(entity), |
670 | | ResourceConstraint::any(), |
671 | | Expr::val(true), |
672 | | ); |
673 | | |
674 | | let validate = Validator::new(ValidatorSchema::empty()); |
675 | | let notes: Vec<ValidationErrorKind> = validate.validate_action_ids(&policy).collect(); |
676 | | |
677 | | assert_eq!(1, notes.len()); |
678 | | match notes.get(0) { |
679 | | Some(ValidationErrorKind::UnrecognizedActionId(UnrecognizedActionId { |
680 | | actual_action_id, |
681 | | suggested_action_id, |
682 | | })) => { |
683 | | assert_eq!("Action::\"foo_name\"", actual_action_id); |
684 | | assert!( |
685 | | suggested_action_id.is_none(), |
686 | | "Did not expect suggested action id." |
687 | | ); |
688 | | } |
689 | | _ => panic!("Unexpected variant of ValidationErrorKind."), |
690 | | }; |
691 | | |
692 | | Ok(()) |
693 | | } |
694 | | |
695 | | #[test] |
696 | | fn validate_action_id_in_singleton_schema() -> Result<()> { |
697 | | let foo_name = "foo_name"; |
698 | | let schema_file = NamespaceDefinition::new( |
699 | | [], |
700 | | [( |
701 | | foo_name.into(), |
702 | | ActionType { |
703 | | applies_to: None, |
704 | | member_of: None, |
705 | | attributes: None, |
706 | | }, |
707 | | )], |
708 | | ); |
709 | | let singleton_schema = schema_file.try_into().unwrap(); |
710 | | let entity = |
711 | | EntityUID::with_eid_and_type("Action", foo_name).expect("should be a valid identifier"); |
712 | | let policy = Template::new( |
713 | | PolicyID::from_string("policy0"), |
714 | | BTreeMap::new(), |
715 | | Effect::Permit, |
716 | | PrincipalConstraint::any(), |
717 | | ActionConstraint::is_eq(entity), |
718 | | ResourceConstraint::any(), |
719 | | Expr::val(true), |
720 | | ); |
721 | | |
722 | | let validate = Validator::new(singleton_schema); |
723 | | assert!( |
724 | | validate.validate_action_ids(&policy).next().is_none(), |
725 | | "Did not expect any validation notes." |
726 | | ); |
727 | | |
728 | | Ok(()) |
729 | | } |
730 | | |
731 | | #[test] |
732 | | fn validate_principal_slot_in_singleton_schema() { |
733 | | let p_name = "User"; |
734 | | let schema_file = NamespaceDefinition::new( |
735 | | [( |
736 | | p_name.into(), |
737 | | EntityType { |
738 | | member_of_types: vec![], |
739 | | shape: AttributesOrContext::default(), |
740 | | }, |
741 | | )], |
742 | | [], |
743 | | ); |
744 | | let schema = schema_file.try_into().unwrap(); |
745 | | let principal_constraint = PrincipalConstraint::is_eq_slot(); |
746 | | let validator = Validator::new(schema); |
747 | | let entities = validator |
748 | | .get_principals_satisfying_constraint(&principal_constraint) |
749 | | .collect::<Vec<_>>(); |
750 | | assert_eq!(entities.len(), 1); |
751 | | let name = &entities[0]; |
752 | | assert_eq!(name, &p_name.parse().expect("Expected valid entity type.")); |
753 | | } |
754 | | |
755 | | #[test] |
756 | | fn validate_resource_slot_in_singleton_schema() { |
757 | | let p_name = "Package"; |
758 | | let schema_file = NamespaceDefinition::new( |
759 | | [( |
760 | | p_name.into(), |
761 | | EntityType { |
762 | | member_of_types: vec![], |
763 | | shape: AttributesOrContext::default(), |
764 | | }, |
765 | | )], |
766 | | [], |
767 | | ); |
768 | | let schema = schema_file.try_into().unwrap(); |
769 | | let principal_constraint = PrincipalConstraint::any(); |
770 | | let validator = Validator::new(schema); |
771 | | let entities = validator |
772 | | .get_principals_satisfying_constraint(&principal_constraint) |
773 | | .collect::<Vec<_>>(); |
774 | | assert_eq!(entities.len(), 1); |
775 | | let name = &entities[0]; |
776 | | assert_eq!(name, &p_name.parse().expect("Expected valid entity type.")); |
777 | | } |
778 | | |
779 | | #[test] |
780 | | fn undefined_entity_type_in_principal_slot() -> Result<()> { |
781 | | let p_name = "User"; |
782 | | let schema_file = NamespaceDefinition::new( |
783 | | [( |
784 | | p_name.into(), |
785 | | EntityType { |
786 | | member_of_types: vec![], |
787 | | shape: AttributesOrContext::default(), |
788 | | }, |
789 | | )], |
790 | | [], |
791 | | ); |
792 | | let schema = schema_file.try_into().expect("Invalid schema"); |
793 | | |
794 | | let undefined_euid: EntityUID = "Undefined::\"foo\"" |
795 | | .parse() |
796 | | .expect("Expected entity UID to parse."); |
797 | | let env = HashMap::from([(ast::SlotId::principal(), undefined_euid)]); |
798 | | |
799 | | let validator = Validator::new(schema); |
800 | | let notes: Vec<ValidationErrorKind> = |
801 | | validator.validate_entity_types_in_slots(&env).collect(); |
802 | | |
803 | | assert_eq!(1, notes.len()); |
804 | | match notes.get(0) { |
805 | | Some(ValidationErrorKind::UnrecognizedEntityType(UnrecognizedEntityType { |
806 | | actual_entity_type, |
807 | | suggested_entity_type, |
808 | | })) => { |
809 | | assert_eq!("Undefined", actual_entity_type); |
810 | | assert_eq!( |
811 | | "User", |
812 | | suggested_entity_type |
813 | | .as_ref() |
814 | | .expect("Expected a suggested entity type") |
815 | | ); |
816 | | } |
817 | | _ => panic!("Unexpected variant of ValidationErrorKind."), |
818 | | }; |
819 | | |
820 | | Ok(()) |
821 | | } |
822 | | |
823 | | #[test] |
824 | | fn validate_action_id_not_in_singleton_schema() -> Result<()> { |
825 | | let schema_file = NamespaceDefinition::new( |
826 | | [], |
827 | | [( |
828 | | "foo_name".into(), |
829 | | ActionType { |
830 | | applies_to: None, |
831 | | member_of: None, |
832 | | attributes: None, |
833 | | }, |
834 | | )], |
835 | | ); |
836 | | let singleton_schema = schema_file.try_into().unwrap(); |
837 | | let entity = EntityUID::with_eid_and_type("Action", "bar_name") |
838 | | .expect("Should be a valid identifier"); |
839 | | let policy = Template::new( |
840 | | PolicyID::from_string("policy0"), |
841 | | BTreeMap::new(), |
842 | | Effect::Permit, |
843 | | PrincipalConstraint::any(), |
844 | | ActionConstraint::is_eq(entity), |
845 | | ResourceConstraint::any(), |
846 | | Expr::val(true), |
847 | | ); |
848 | | |
849 | | let validate = Validator::new(singleton_schema); |
850 | | let notes: Vec<ValidationErrorKind> = validate.validate_action_ids(&policy).collect(); |
851 | | |
852 | | assert_eq!(1, notes.len()); |
853 | | match notes.get(0) { |
854 | | Some(ValidationErrorKind::UnrecognizedActionId(UnrecognizedActionId { |
855 | | actual_action_id, |
856 | | suggested_action_id, |
857 | | })) => { |
858 | | assert_eq!("Action::\"bar_name\"", actual_action_id); |
859 | | assert_eq!( |
860 | | "Action::\"foo_name\"", |
861 | | suggested_action_id |
862 | | .as_ref() |
863 | | .expect("Expected suggested action id.") |
864 | | ) |
865 | | } |
866 | | _ => panic!("Unexpected variant of ValidationErrorKind."), |
867 | | }; |
868 | | |
869 | | Ok(()) |
870 | | } |
871 | | |
872 | | #[test] |
873 | | fn validate_namespaced_action_id_in_schema() -> Result<()> { |
874 | | let descriptors: SchemaFragment = serde_json::from_str( |
875 | | r#" |
876 | | { |
877 | | "NS": { |
878 | | "entityTypes": {}, |
879 | | "actions": { "foo_name": {} } |
880 | | } |
881 | | }"#, |
882 | | ) |
883 | | .expect("Expected schema parse."); |
884 | | let schema = descriptors.try_into().unwrap(); |
885 | | let entity: EntityUID = "NS::Action::\"foo_name\"" |
886 | | .parse() |
887 | | .expect("Expected entity parse."); |
888 | | let policy = Template::new( |
889 | | PolicyID::from_string("policy0"), |
890 | | BTreeMap::new(), |
891 | | Effect::Permit, |
892 | | PrincipalConstraint::any(), |
893 | | ActionConstraint::is_eq(entity), |
894 | | ResourceConstraint::any(), |
895 | | Expr::val(true), |
896 | | ); |
897 | | |
898 | | let validate = Validator::new(schema); |
899 | | let notes: Vec<ValidationErrorKind> = validate.validate_action_ids(&policy).collect(); |
900 | | assert_eq!(notes, vec![], "Did not expect any invalid action."); |
901 | | Ok(()) |
902 | | } |
903 | | |
904 | | #[test] |
905 | | fn validate_namespaced_invalid_action() -> Result<()> { |
906 | | let descriptors: SchemaFragment = serde_json::from_str( |
907 | | r#" |
908 | | { |
909 | | "NS": { |
910 | | "entityTypes": {}, |
911 | | "actions": { "foo_name": {} } |
912 | | } |
913 | | }"#, |
914 | | ) |
915 | | .expect("Expected schema parse."); |
916 | | let schema = descriptors.try_into().unwrap(); |
917 | | let entity: EntityUID = "Bogus::Action::\"foo_name\"" |
918 | | .parse() |
919 | | .expect("Expected entity parse."); |
920 | | let policy = Template::new( |
921 | | PolicyID::from_string("policy0"), |
922 | | BTreeMap::new(), |
923 | | Effect::Permit, |
924 | | PrincipalConstraint::any(), |
925 | | ActionConstraint::is_eq(entity), |
926 | | ResourceConstraint::any(), |
927 | | Expr::val(true), |
928 | | ); |
929 | | |
930 | | let validate = Validator::new(schema); |
931 | | let notes: Vec<ValidationErrorKind> = validate.validate_action_ids(&policy).collect(); |
932 | | assert_eq!( |
933 | | notes, |
934 | | vec![ValidationErrorKind::unrecognized_action_id( |
935 | | "Bogus::Action::\"foo_name\"".into(), |
936 | | Some("NS::Action::\"foo_name\"".into()), |
937 | | )] |
938 | | ); |
939 | | Ok(()) |
940 | | } |
941 | | |
942 | | #[test] |
943 | | fn validate_namespaced_entity_type_in_schema() -> Result<()> { |
944 | | let descriptors: SchemaFragment = serde_json::from_str( |
945 | | r#" |
946 | | { |
947 | | "NS": { |
948 | | "entityTypes": {"Foo": {} }, |
949 | | "actions": {} |
950 | | } |
951 | | }"#, |
952 | | ) |
953 | | .expect("Expected schema parse."); |
954 | | let schema = descriptors.try_into().unwrap(); |
955 | | let entity_type: Name = "NS::Foo".parse().expect("Expected entity type parse."); |
956 | | let policy = Template::new( |
957 | | PolicyID::from_string("policy0"), |
958 | | BTreeMap::new(), |
959 | | Effect::Permit, |
960 | | PrincipalConstraint::is_eq(EntityUID::from_components(entity_type, Eid::new("bar"))), |
961 | | ActionConstraint::any(), |
962 | | ResourceConstraint::any(), |
963 | | Expr::val(true), |
964 | | ); |
965 | | |
966 | | let validate = Validator::new(schema); |
967 | | let notes: Vec<ValidationErrorKind> = validate.validate_entity_types(&policy).collect(); |
968 | | assert_eq!(notes, vec![], "Did not expect any invalid action."); |
969 | | Ok(()) |
970 | | } |
971 | | |
972 | | #[test] |
973 | | fn validate_namespaced_invalid_entity_type() -> Result<()> { |
974 | | let descriptors: SchemaFragment = serde_json::from_str( |
975 | | r#" |
976 | | { |
977 | | "NS": { |
978 | | "entityTypes": {"Foo": {} }, |
979 | | "actions": {} |
980 | | } |
981 | | }"#, |
982 | | ) |
983 | | .expect("Expected schema parse."); |
984 | | let schema = descriptors.try_into().unwrap(); |
985 | | let entity_type: Name = "Bogus::Foo".parse().expect("Expected entity type parse."); |
986 | | let policy = Template::new( |
987 | | PolicyID::from_string("policy0"), |
988 | | BTreeMap::new(), |
989 | | Effect::Permit, |
990 | | PrincipalConstraint::is_eq(EntityUID::from_components(entity_type, Eid::new("bar"))), |
991 | | ActionConstraint::any(), |
992 | | ResourceConstraint::any(), |
993 | | Expr::val(true), |
994 | | ); |
995 | | |
996 | | let validate = Validator::new(schema); |
997 | | let notes: Vec<ValidationErrorKind> = validate.validate_entity_types(&policy).collect(); |
998 | | assert_eq!( |
999 | | notes, |
1000 | | vec![ValidationErrorKind::unrecognized_entity_type( |
1001 | | "Bogus::Foo".into(), |
1002 | | Some("NS::Foo".into()) |
1003 | | )] |
1004 | | ); |
1005 | | Ok(()) |
1006 | | } |
1007 | | |
1008 | | #[test] |
1009 | | fn get_possible_actions_eq() -> Result<()> { |
1010 | | let foo_name = "foo_name"; |
1011 | | let euid_foo = |
1012 | | EntityUID::with_eid_and_type("Action", foo_name).expect("should be a valid identifier"); |
1013 | | let action_constraint = ActionConstraint::is_eq(euid_foo.clone()); |
1014 | | |
1015 | | let schema_file = NamespaceDefinition::new( |
1016 | | [], |
1017 | | [( |
1018 | | foo_name.into(), |
1019 | | ActionType { |
1020 | | applies_to: None, |
1021 | | member_of: None, |
1022 | | attributes: None, |
1023 | | }, |
1024 | | )], |
1025 | | ); |
1026 | | let singleton_schema = schema_file.try_into().unwrap(); |
1027 | | |
1028 | | let validate = Validator::new(singleton_schema); |
1029 | | let actions = validate |
1030 | | .get_actions_satisfying_constraint(&action_constraint) |
1031 | | .collect(); |
1032 | | assert_eq!(HashSet::from([euid_foo]), actions); |
1033 | | |
1034 | | Ok(()) |
1035 | | } |
1036 | | |
1037 | | #[test] |
1038 | | fn get_possible_actions_in_no_parents() -> Result<()> { |
1039 | | let foo_name = "foo_name"; |
1040 | | let euid_foo = |
1041 | | EntityUID::with_eid_and_type("Action", foo_name).expect("should be a valid identifier"); |
1042 | | let action_constraint = ActionConstraint::is_in(vec![euid_foo.clone()]); |
1043 | | |
1044 | | let schema_file = NamespaceDefinition::new( |
1045 | | [], |
1046 | | [( |
1047 | | foo_name.into(), |
1048 | | ActionType { |
1049 | | applies_to: None, |
1050 | | member_of: None, |
1051 | | attributes: None, |
1052 | | }, |
1053 | | )], |
1054 | | ); |
1055 | | let singleton_schema = schema_file.try_into().unwrap(); |
1056 | | |
1057 | | let validate = Validator::new(singleton_schema); |
1058 | | let actions = validate |
1059 | | .get_actions_satisfying_constraint(&action_constraint) |
1060 | | .collect(); |
1061 | | assert_eq!(HashSet::from([euid_foo]), actions); |
1062 | | |
1063 | | Ok(()) |
1064 | | } |
1065 | | |
1066 | | #[test] |
1067 | | fn get_possible_actions_in_set_no_parents() -> Result<()> { |
1068 | | let foo_name = "foo_name"; |
1069 | | let euid_foo = |
1070 | | EntityUID::with_eid_and_type("Action", foo_name).expect("should be a valid identifier"); |
1071 | | let action_constraint = ActionConstraint::is_in(vec![euid_foo.clone()]); |
1072 | | |
1073 | | let schema_file = NamespaceDefinition::new( |
1074 | | [], |
1075 | | [( |
1076 | | foo_name.into(), |
1077 | | ActionType { |
1078 | | applies_to: None, |
1079 | | member_of: None, |
1080 | | attributes: None, |
1081 | | }, |
1082 | | )], |
1083 | | ); |
1084 | | let singleton_schema = schema_file.try_into().unwrap(); |
1085 | | |
1086 | | let validate = Validator::new(singleton_schema); |
1087 | | let actions = validate |
1088 | | .get_actions_satisfying_constraint(&action_constraint) |
1089 | | .collect(); |
1090 | | assert_eq!(HashSet::from([euid_foo]), actions); |
1091 | | |
1092 | | Ok(()) |
1093 | | } |
1094 | | |
1095 | | #[test] |
1096 | | fn get_possible_principals_eq() -> Result<()> { |
1097 | | let foo_type = "foo_type"; |
1098 | | let euid_foo = EntityUID::with_eid_and_type(foo_type, "foo_name") |
1099 | | .expect("should be a valid identifier"); |
1100 | | let principal_constraint = PrincipalConstraint::is_eq(euid_foo.clone()); |
1101 | | |
1102 | | let schema_file = NamespaceDefinition::new( |
1103 | | [( |
1104 | | foo_type.into(), |
1105 | | EntityType { |
1106 | | member_of_types: vec![], |
1107 | | shape: AttributesOrContext::default(), |
1108 | | }, |
1109 | | )], |
1110 | | [], |
1111 | | ); |
1112 | | let singleton_schema = schema_file.try_into().unwrap(); |
1113 | | |
1114 | | let validate = Validator::new(singleton_schema); |
1115 | | let principals = validate |
1116 | | .get_principals_satisfying_constraint(&principal_constraint) |
1117 | | .map(cedar_policy_core::ast::EntityType::Concrete) |
1118 | | .collect::<HashSet<_>>(); |
1119 | | assert_eq!(HashSet::from([euid_foo.components().0]), principals); |
1120 | | |
1121 | | Ok(()) |
1122 | | } |
1123 | | |
1124 | | fn schema_with_single_principal_action_resource( |
1125 | | ) -> (EntityUID, EntityUID, EntityUID, ValidatorSchema) { |
1126 | | let action_name = "foo"; |
1127 | | let action_euid = EntityUID::with_eid_and_type("Action", action_name) |
1128 | | .expect("should be a valid identifier"); |
1129 | | let principal_type = "bar"; |
1130 | | let principal_euid = EntityUID::with_eid_and_type(principal_type, "principal") |
1131 | | .expect("should be a valid identifier"); |
1132 | | let resource_type = "baz"; |
1133 | | let resource_euid = EntityUID::with_eid_and_type(resource_type, "resource") |
1134 | | .expect("should be a valid identifier"); |
1135 | | |
1136 | | let schema = NamespaceDefinition::new( |
1137 | | [ |
1138 | | ( |
1139 | | principal_type.into(), |
1140 | | EntityType { |
1141 | | member_of_types: vec![], |
1142 | | shape: AttributesOrContext::default(), |
1143 | | }, |
1144 | | ), |
1145 | | ( |
1146 | | resource_type.into(), |
1147 | | EntityType { |
1148 | | member_of_types: vec![], |
1149 | | shape: AttributesOrContext::default(), |
1150 | | }, |
1151 | | ), |
1152 | | ], |
1153 | | [( |
1154 | | action_name.into(), |
1155 | | ActionType { |
1156 | | applies_to: Some(ApplySpec { |
1157 | | resource_types: Some(vec![resource_type.into()]), |
1158 | | principal_types: Some(vec![principal_type.into()]), |
1159 | | context: AttributesOrContext::default(), |
1160 | | }), |
1161 | | member_of: Some(vec![]), |
1162 | | attributes: None, |
1163 | | }, |
1164 | | )], |
1165 | | ) |
1166 | | .try_into() |
1167 | | .expect("Expected valid schema file."); |
1168 | | (principal_euid, action_euid, resource_euid, schema) |
1169 | | } |
1170 | | |
1171 | | #[test] |
1172 | | fn validate_action_apply_correct() -> Result<()> { |
1173 | | let (principal, action, resource, schema) = schema_with_single_principal_action_resource(); |
1174 | | |
1175 | | let policy = Template::new( |
1176 | | PolicyID::from_string("policy0"), |
1177 | | BTreeMap::new(), |
1178 | | Effect::Permit, |
1179 | | PrincipalConstraint::is_eq(principal), |
1180 | | ActionConstraint::is_eq(action), |
1181 | | ResourceConstraint::is_eq(resource), |
1182 | | Expr::val(true), |
1183 | | ); |
1184 | | |
1185 | | let validate = Validator::new(schema); |
1186 | | assert!( |
1187 | | validate |
1188 | | .validate_policy(&policy, ValidationMode::default()) |
1189 | | .next() |
1190 | | .is_none(), |
1191 | | "Did not expect any validation notes." |
1192 | | ); |
1193 | | |
1194 | | Ok(()) |
1195 | | } |
1196 | | |
1197 | | #[test] |
1198 | | fn validate_action_apply_incorrect_principal() -> Result<()> { |
1199 | | let (_, action, resource, schema) = schema_with_single_principal_action_resource(); |
1200 | | |
1201 | | let principal_constraint = PrincipalConstraint::is_eq(resource.clone()); |
1202 | | let action_constraint = ActionConstraint::is_eq(action); |
1203 | | let resource_constraint = ResourceConstraint::is_eq(resource); |
1204 | | |
1205 | | let validate = Validator::new(schema); |
1206 | | let notes: Vec<ValidationErrorKind> = validate |
1207 | | .validate_action_application( |
1208 | | &principal_constraint, |
1209 | | &action_constraint, |
1210 | | &resource_constraint, |
1211 | | ) |
1212 | | .collect(); |
1213 | | assert_eq!(1, notes.len()); |
1214 | | match notes.get(0) { |
1215 | | Some(ValidationErrorKind::InvalidActionApplication(_)) => (), |
1216 | | _ => panic!("Unexpected variant of ValidationErrorKind."), |
1217 | | } |
1218 | | |
1219 | | Ok(()) |
1220 | | } |
1221 | | |
1222 | | #[test] |
1223 | | fn validate_action_apply_incorrect_resource() -> Result<()> { |
1224 | | let (principal, action, _, schema) = schema_with_single_principal_action_resource(); |
1225 | | |
1226 | | let principal_constraint = PrincipalConstraint::is_eq(principal.clone()); |
1227 | | let action_constraint = ActionConstraint::is_eq(action); |
1228 | | let resource_constraint = ResourceConstraint::is_eq(principal); |
1229 | | |
1230 | | let validate = Validator::new(schema); |
1231 | | let notes: Vec<ValidationErrorKind> = validate |
1232 | | .validate_action_application( |
1233 | | &principal_constraint, |
1234 | | &action_constraint, |
1235 | | &resource_constraint, |
1236 | | ) |
1237 | | .collect(); |
1238 | | assert_eq!(1, notes.len()); |
1239 | | match notes.get(0) { |
1240 | | Some(ValidationErrorKind::InvalidActionApplication(_)) => (), |
1241 | | _ => panic!("Unexpected variant of ValidationErrorKind."), |
1242 | | } |
1243 | | |
1244 | | Ok(()) |
1245 | | } |
1246 | | |
1247 | | #[test] |
1248 | | fn validate_action_apply_incorrect_principal_and_resource() -> Result<()> { |
1249 | | let (principal, action, resource, schema) = schema_with_single_principal_action_resource(); |
1250 | | |
1251 | | let principal_constraint = PrincipalConstraint::is_eq(resource); |
1252 | | let action_constraint = ActionConstraint::is_eq(action); |
1253 | | let resource_constraint = ResourceConstraint::is_eq(principal); |
1254 | | |
1255 | | let validate = Validator::new(schema); |
1256 | | let notes: Vec<ValidationErrorKind> = validate |
1257 | | .validate_action_application( |
1258 | | &principal_constraint, |
1259 | | &action_constraint, |
1260 | | &resource_constraint, |
1261 | | ) |
1262 | | .collect(); |
1263 | | assert_eq!(1, notes.len()); |
1264 | | match notes.get(0) { |
1265 | | Some(ValidationErrorKind::InvalidActionApplication(_)) => (), |
1266 | | _ => panic!("Unexpected variant of ValidationErrorKind."), |
1267 | | } |
1268 | | |
1269 | | Ok(()) |
1270 | | } |
1271 | | |
1272 | | #[test] |
1273 | | fn validate_used_as_correct() -> Result<()> { |
1274 | | let (principal, action, resource, schema) = schema_with_single_principal_action_resource(); |
1275 | | let policy = Template::new( |
1276 | | PolicyID::from_string("policy0"), |
1277 | | BTreeMap::new(), |
1278 | | Effect::Permit, |
1279 | | PrincipalConstraint::is_eq(principal), |
1280 | | ActionConstraint::is_eq(action), |
1281 | | ResourceConstraint::is_eq(resource), |
1282 | | Expr::val(true), |
1283 | | ); |
1284 | | |
1285 | | let validate = Validator::new(schema); |
1286 | | assert!( |
1287 | | validate |
1288 | | .validate_policy(&policy, ValidationMode::default()) |
1289 | | .next() |
1290 | | .is_none(), |
1291 | | "Did not expect any validation notes." |
1292 | | ); |
1293 | | |
1294 | | Ok(()) |
1295 | | } |
1296 | | |
1297 | | #[test] |
1298 | | fn validate_used_as_incorrect() -> Result<()> { |
1299 | | let (principal, _, resource, schema) = schema_with_single_principal_action_resource(); |
1300 | | |
1301 | | let principal_constraint = PrincipalConstraint::is_eq(resource); |
1302 | | let action_constraint = ActionConstraint::any(); |
1303 | | let resource_constraint = ResourceConstraint::is_eq(principal); |
1304 | | |
1305 | | let validate = Validator::new(schema); |
1306 | | let notes: Vec<_> = validate |
1307 | | .validate_action_application( |
1308 | | &principal_constraint, |
1309 | | &action_constraint, |
1310 | | &resource_constraint, |
1311 | | ) |
1312 | | .collect(); |
1313 | | assert_eq!( |
1314 | | notes, |
1315 | | vec![ValidationErrorKind::invalid_action_application( |
1316 | | false, false |
1317 | | )], |
1318 | | ); |
1319 | | |
1320 | | Ok(()) |
1321 | | } |
1322 | | |
1323 | | #[test] |
1324 | | fn test_with_tc_computation() -> Result<()> { |
1325 | | let action_name = "foo"; |
1326 | | let action_parent_name = "foo_parent"; |
1327 | | let action_grandparent_name = "foo_grandparent"; |
1328 | | let action_grandparent_euid = |
1329 | | EntityUID::with_eid_and_type("Action", action_grandparent_name) |
1330 | | .expect("should be a valid identifier"); |
1331 | | |
1332 | | let principal_type = "bar"; |
1333 | | |
1334 | | let resource_type = "baz"; |
1335 | | let resource_parent_type = "baz_parent"; |
1336 | | let resource_grandparent_type = "baz_grandparent"; |
1337 | | let resource_grandparent_euid = |
1338 | | EntityUID::with_eid_and_type(resource_parent_type, "resource") |
1339 | | .expect("should be a valid identifier"); |
1340 | | |
1341 | | let schema_file = NamespaceDefinition::new( |
1342 | | [ |
1343 | | ( |
1344 | | principal_type.into(), |
1345 | | EntityType { |
1346 | | member_of_types: vec![], |
1347 | | shape: AttributesOrContext::default(), |
1348 | | }, |
1349 | | ), |
1350 | | ( |
1351 | | resource_type.into(), |
1352 | | EntityType { |
1353 | | member_of_types: vec![resource_parent_type.into()], |
1354 | | shape: AttributesOrContext::default(), |
1355 | | }, |
1356 | | ), |
1357 | | ( |
1358 | | resource_parent_type.into(), |
1359 | | EntityType { |
1360 | | member_of_types: vec![resource_grandparent_type.into()], |
1361 | | shape: AttributesOrContext::default(), |
1362 | | }, |
1363 | | ), |
1364 | | ( |
1365 | | resource_grandparent_type.into(), |
1366 | | EntityType { |
1367 | | member_of_types: vec![], |
1368 | | shape: AttributesOrContext::default(), |
1369 | | }, |
1370 | | ), |
1371 | | ], |
1372 | | [ |
1373 | | ( |
1374 | | action_name.into(), |
1375 | | ActionType { |
1376 | | applies_to: Some(ApplySpec { |
1377 | | resource_types: Some(vec![resource_type.into()]), |
1378 | | principal_types: Some(vec![principal_type.into()]), |
1379 | | context: AttributesOrContext::default(), |
1380 | | }), |
1381 | | member_of: Some(vec![ActionEntityUID { |
1382 | | ty: None, |
1383 | | id: action_parent_name.into(), |
1384 | | }]), |
1385 | | attributes: None, |
1386 | | }, |
1387 | | ), |
1388 | | ( |
1389 | | action_parent_name.into(), |
1390 | | ActionType { |
1391 | | applies_to: None, |
1392 | | member_of: Some(vec![ActionEntityUID { |
1393 | | ty: None, |
1394 | | id: action_grandparent_name.into(), |
1395 | | }]), |
1396 | | attributes: None, |
1397 | | }, |
1398 | | ), |
1399 | | ( |
1400 | | action_grandparent_name.into(), |
1401 | | ActionType { |
1402 | | applies_to: None, |
1403 | | member_of: Some(vec![]), |
1404 | | attributes: None, |
1405 | | }, |
1406 | | ), |
1407 | | ], |
1408 | | ); |
1409 | | let schema = schema_file.try_into().unwrap(); |
1410 | | |
1411 | | let policy = Template::new( |
1412 | | PolicyID::from_string("policy0"), |
1413 | | BTreeMap::new(), |
1414 | | Effect::Permit, |
1415 | | PrincipalConstraint::any(), |
1416 | | ActionConstraint::is_in([action_grandparent_euid]), |
1417 | | ResourceConstraint::is_in(resource_grandparent_euid), |
1418 | | Expr::val(true), |
1419 | | ); |
1420 | | |
1421 | | let validate = Validator::new(schema); |
1422 | | |
1423 | | let notes: Vec<ValidationError> = validate |
1424 | | .validate_policy(&policy, ValidationMode::default()) |
1425 | | .collect(); |
1426 | | assert!( |
1427 | | notes.is_empty(), |
1428 | | "Expected empty validation notes, saw {:?}", |
1429 | | notes |
1430 | | ); |
1431 | | |
1432 | | Ok(()) |
1433 | | } |
1434 | | |
1435 | | #[test] |
1436 | | fn unspecified_entity_in_head() -> Result<()> { |
1437 | | // Note: it's not possible to create an unspecified entity through the parser, |
1438 | | // so we have to test using manually-constructed policies. |
1439 | | let validate = Validator::new(ValidatorSchema::empty()); |
1440 | | |
1441 | | // resource == Unspecified::"foo" |
1442 | | let policy = Template::new( |
1443 | | PolicyID::from_string("policy0"), |
1444 | | BTreeMap::new(), |
1445 | | Effect::Permit, |
1446 | | PrincipalConstraint::any(), |
1447 | | ActionConstraint::any(), |
1448 | | ResourceConstraint::is_eq(EntityUID::unspecified_from_eid(Eid::new("foo"))), |
1449 | | Expr::val(true), |
1450 | | ); |
1451 | | let notes: Vec<ValidationErrorKind> = validate.validate_entity_types(&policy).collect(); |
1452 | | assert_eq!(1, notes.len()); |
1453 | | match notes.get(0) { |
1454 | | Some(ValidationErrorKind::UnspecifiedEntity(UnspecifiedEntity { entity_id })) => { |
1455 | | assert_eq!("foo", entity_id); |
1456 | | } |
1457 | | _ => panic!("Unexpected variant of ValidationErrorKind."), |
1458 | | }; |
1459 | | |
1460 | | // principal in Unspecified::"foo" |
1461 | | let policy = Template::new( |
1462 | | PolicyID::from_string("policy0"), |
1463 | | BTreeMap::new(), |
1464 | | Effect::Permit, |
1465 | | PrincipalConstraint::is_in(EntityUID::unspecified_from_eid(Eid::new("foo"))), |
1466 | | ActionConstraint::any(), |
1467 | | ResourceConstraint::any(), |
1468 | | Expr::val(true), |
1469 | | ); |
1470 | | let notes: Vec<ValidationErrorKind> = validate.validate_entity_types(&policy).collect(); |
1471 | | assert_eq!(1, notes.len()); |
1472 | | match notes.get(0) { |
1473 | | Some(ValidationErrorKind::UnspecifiedEntity(UnspecifiedEntity { entity_id })) => { |
1474 | | assert_eq!("foo", entity_id); |
1475 | | } |
1476 | | _ => panic!("Unexpected variant of ValidationErrorKind."), |
1477 | | }; |
1478 | | |
1479 | | Ok(()) |
1480 | | } |
1481 | | |
1482 | | #[test] |
1483 | | fn unspecified_entity_in_additional_constraints() -> Result<()> { |
1484 | | let validate = Validator::new(ValidatorSchema::empty()); |
1485 | | |
1486 | | // resource == Unspecified::"foo" |
1487 | | let policy = Template::new( |
1488 | | PolicyID::from_string("policy0"), |
1489 | | BTreeMap::new(), |
1490 | | Effect::Permit, |
1491 | | PrincipalConstraint::any(), |
1492 | | ActionConstraint::any(), |
1493 | | ResourceConstraint::any(), |
1494 | | Expr::is_eq( |
1495 | | Expr::var(cedar_policy_core::ast::Var::Resource), |
1496 | | Expr::val(EntityUID::unspecified_from_eid(Eid::new("foo"))), |
1497 | | ), |
1498 | | ); |
1499 | | let notes: Vec<ValidationErrorKind> = validate.validate_entity_types(&policy).collect(); |
1500 | | println!("{:?}", notes); |
1501 | | assert_eq!(1, notes.len()); |
1502 | | match notes.get(0) { |
1503 | | Some(ValidationErrorKind::UnspecifiedEntity(UnspecifiedEntity { entity_id })) => { |
1504 | | assert_eq!("foo", entity_id); |
1505 | | } |
1506 | | _ => panic!("Unexpected variant of ValidationErrorKind."), |
1507 | | }; |
1508 | | |
1509 | | Ok(()) |
1510 | | } |
1511 | | |
1512 | | #[test] |
1513 | | fn action_with_unspecified_resource_applies() { |
1514 | | let schema = serde_json::from_str::<NamespaceDefinition>( |
1515 | | r#" |
1516 | | { |
1517 | | "entityTypes": {"a": {}}, |
1518 | | "actions": { |
1519 | | "": { |
1520 | | "appliesTo": { |
1521 | | "principalTypes": ["a"], |
1522 | | "resourceTypes": null |
1523 | | } |
1524 | | } |
1525 | | } |
1526 | | } |
1527 | | "#, |
1528 | | ) |
1529 | | .unwrap() |
1530 | | .try_into() |
1531 | | .unwrap(); |
1532 | | let policy = parse_policy( |
1533 | | Some("0".to_string()), |
1534 | | r#"permit(principal == a::"", action == Action::"", resource);"#, |
1535 | | ) |
1536 | | .unwrap(); |
1537 | | |
1538 | | let validate = Validator::new(schema); |
1539 | | let (template, _) = Template::link_static_policy(policy); |
1540 | | let mut errs = validate.validate_policy(&template, ValidationMode::default()); |
1541 | | assert!( |
1542 | | errs.next().is_none(), |
1543 | | "Did not expect any validation errors." |
1544 | | ); |
1545 | | } |
1546 | | |
1547 | | #[test] |
1548 | | fn action_with_unspecified_principal_applies() { |
1549 | | let schema = serde_json::from_str::<NamespaceDefinition>( |
1550 | | r#" |
1551 | | { |
1552 | | "entityTypes": {"a": {}}, |
1553 | | "actions": { |
1554 | | "": { |
1555 | | "appliesTo": { |
1556 | | "principalTypes": null, |
1557 | | "resourceTypes": ["a"] |
1558 | | } |
1559 | | } |
1560 | | } |
1561 | | } |
1562 | | "#, |
1563 | | ) |
1564 | | .unwrap() |
1565 | | .try_into() |
1566 | | .unwrap(); |
1567 | | let policy = parse_policy( |
1568 | | Some("0".to_string()), |
1569 | | r#"permit(principal, action == Action::"", resource == a::"");"#, |
1570 | | ) |
1571 | | .unwrap(); |
1572 | | |
1573 | | let validate = Validator::new(schema); |
1574 | | let (template, _) = Template::link_static_policy(policy); |
1575 | | let mut errs = validate.validate_policy(&template, ValidationMode::default()); |
1576 | | assert!( |
1577 | | errs.next().is_none(), |
1578 | | "Did not expect any validation errors." |
1579 | | ); |
1580 | | } |
1581 | | |
1582 | | #[test] |
1583 | | fn unspecified_principal_resource_with_head_conditions() { |
1584 | | let schema = serde_json::from_str::<NamespaceDefinition>( |
1585 | | r#" |
1586 | | { |
1587 | | "entityTypes": {"a": {}}, |
1588 | | "actions": { |
1589 | | "": { } |
1590 | | } |
1591 | | } |
1592 | | "#, |
1593 | | ) |
1594 | | .unwrap() |
1595 | | .try_into() |
1596 | | .unwrap(); |
1597 | | let policy = parse_policy( |
1598 | | Some("0".to_string()), |
1599 | | r#"permit(principal == a::"p", action, resource == a::"r");"#, |
1600 | | ) |
1601 | | .unwrap(); |
1602 | | |
1603 | | let validate = Validator::new(schema); |
1604 | | let (template, _) = Template::link_static_policy(policy); |
1605 | | let errs = validate.validate_policy(&template, ValidationMode::default()); |
1606 | | assert_eq!( |
1607 | | errs.map(|e| e.into_location_and_error_kind().1) |
1608 | | .collect::<Vec<_>>(), |
1609 | | vec![ValidationErrorKind::type_error( |
1610 | | TypeErrorKind::ImpossiblePolicy |
1611 | | )] |
1612 | | ); |
1613 | | } |
1614 | | } |