Skip to main content

cedar_policy_core/est/
policy_set.rs

1/*
2 * Copyright Cedar Contributors
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
17use super::Clause;
18use super::Policy;
19use super::PolicySetFromJsonError;
20use crate::ast::{self, EntityUID, PolicyID, SlotId};
21use crate::entities::json::err::JsonDeserializationErrorContext;
22use crate::entities::json::EntityUidJson;
23use crate::jsonvalue::deserialize_linked_hash_map_no_duplicates;
24use crate::parser::cst::Policies;
25use crate::parser::err::ParseErrors;
26use crate::parser::Node;
27use linked_hash_map::LinkedHashMap;
28use serde::{Deserialize, Serialize};
29use serde_with::serde_as;
30use std::collections::HashMap;
31
32/// Serde JSON structure for a policy set in the EST format
33#[derive(Debug, Clone, Serialize, Deserialize, Default)]
34#[serde(rename_all = "camelCase")]
35#[serde(deny_unknown_fields)]
36pub struct PolicySet {
37    /// The set of templates in a policy set
38    #[serde(deserialize_with = "deserialize_linked_hash_map_no_duplicates")]
39    pub templates: LinkedHashMap<PolicyID, Policy>,
40    /// The set of static policies in a policy set
41    #[serde(deserialize_with = "deserialize_linked_hash_map_no_duplicates")]
42    pub static_policies: LinkedHashMap<PolicyID, Policy>,
43    /// The set of template links
44    pub template_links: Vec<TemplateLink>,
45}
46
47impl PolicySet {
48    /// Get the static or template-linked policy with the given id.
49    /// Returns an `Option` rather than a `Result` because it is expected to be
50    /// used in cases where the policy set is guaranteed to be well-formed
51    /// (e.g., after successful conversion to an `ast::PolicySet`)
52    pub fn get_policy(&self, id: &PolicyID) -> Option<Policy> {
53        let maybe_static_policy = self.static_policies.get(id).cloned();
54
55        let maybe_link = self
56            .template_links
57            .iter()
58            .filter_map(|link| {
59                if &link.new_id == id {
60                    self.get_template(&link.template_id).and_then(|template| {
61                        let unwrapped_est_vals: HashMap<SlotId, EntityUidJson> =
62                            link.values.iter().map(|(k, v)| (*k, v.into())).collect();
63                        template.link(&unwrapped_est_vals).ok()
64                    })
65                } else {
66                    None
67                }
68            })
69            .next();
70
71        maybe_static_policy.or(maybe_link)
72    }
73
74    /// Get the template with the given id.
75    /// Returns an `Option` rather than a `Result` because it is expected to be
76    /// used in cases where the policy set is guaranteed to be well-formed
77    /// (e.g., after successful conversion to an `ast::PolicySet`)
78    pub fn get_template(&self, id: &PolicyID) -> Option<Policy> {
79        self.templates.get(id).cloned()
80    }
81
82    /// Return the largest `Expr::height` across every condition body in
83    /// every static policy and template in this policy set.
84    /// Returns `None` when there are no conditions.
85    pub fn max_expr_height(&self) -> Option<usize> {
86        self.static_policies
87            .values()
88            .chain(self.templates.values())
89            .flat_map(|p| &p.conditions)
90            .map(|clause| match clause {
91                Clause::When(e) | Clause::Unless(e) => e.height(),
92            })
93            .max()
94    }
95}
96
97/// Serde JSON structure describing a template-linked policy
98#[serde_as]
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101#[serde(deny_unknown_fields)]
102pub struct TemplateLink {
103    /// Id of the template to link against
104    pub template_id: PolicyID,
105    /// Id of the generated policy
106    pub new_id: PolicyID,
107    /// Mapping between slots and entity uids
108    #[serde_as(as = "serde_with::MapPreventDuplicates<_,EntityUidJson<TemplateLinkContext>>")]
109    pub values: HashMap<SlotId, EntityUID>,
110}
111
112/// Statically set the deserialization error context to be deserialization of a template link
113struct TemplateLinkContext;
114
115impl crate::entities::json::DeserializationContext for TemplateLinkContext {
116    fn static_context() -> Option<JsonDeserializationErrorContext> {
117        Some(JsonDeserializationErrorContext::TemplateLink)
118    }
119}
120
121impl TryFrom<PolicySet> for ast::PolicySet {
122    type Error = PolicySetFromJsonError;
123
124    fn try_from(value: PolicySet) -> Result<Self, Self::Error> {
125        let mut ast_pset = ast::PolicySet::default();
126
127        for (id, policy) in value.static_policies {
128            let ast = policy.try_into_ast_policy(Some(id))?;
129            ast_pset.add(ast)?;
130        }
131
132        for (id, policy) in value.templates {
133            let ast = policy.try_into_ast_policy_or_template(Some(id))?;
134            ast_pset.add_template(ast)?;
135        }
136
137        for TemplateLink {
138            template_id,
139            new_id,
140            values,
141        } in value.template_links
142        {
143            ast_pset.link(template_id, new_id, values)?;
144        }
145
146        Ok(ast_pset)
147    }
148}
149
150impl TryFrom<Node<Option<Policies>>> for PolicySet {
151    type Error = ParseErrors;
152
153    fn try_from(policies: Node<Option<Policies>>) -> Result<Self, Self::Error> {
154        let mut templates = LinkedHashMap::new();
155        let mut static_policies = LinkedHashMap::new();
156        let mut all_errs: Vec<ParseErrors> = vec![];
157        for (policy_id, policy) in policies.with_generated_policyids()? {
158            match policy.try_as_inner() {
159                Ok(cst) => match Policy::try_from(cst.clone()) {
160                    Ok(est) => {
161                        if est.is_template() {
162                            templates.insert(policy_id, est);
163                        } else {
164                            static_policies.insert(policy_id, est);
165                        }
166                    }
167                    Err(e) => {
168                        all_errs.push(e);
169                    }
170                },
171                Err(e) => {
172                    all_errs.push(e.into());
173                }
174            };
175        }
176        // fail on any error
177        if let Some(errs) = ParseErrors::flatten(all_errs) {
178            Err(errs)
179        } else {
180            Ok(PolicySet {
181                templates,
182                static_policies,
183                template_links: Vec::new(),
184            })
185        }
186    }
187}
188
189#[cfg(test)]
190mod test {
191    use serde_json::json;
192
193    use super::*;
194
195    #[test]
196    fn valid_example() {
197        let json = json!({
198            "staticPolicies": {
199                "policy1": {
200                    "effect": "permit",
201                    "principal": {
202                        "op": "==",
203                        "entity": { "type": "User", "id": "alice" }
204                    },
205                    "action": {
206                        "op": "==",
207                        "entity": { "type": "Action", "id": "view" }
208                    },
209                    "resource": {
210                        "op": "in",
211                        "entity": { "type": "Folder", "id": "foo" }
212                    },
213                    "conditions": []
214                }
215            },
216            "templates": {
217                "template": {
218                    "effect" : "permit",
219                    "principal" : {
220                        "op" : "==",
221                        "slot" : "?principal"
222                    },
223                    "action" : {
224                        "op" : "all"
225                    },
226                    "resource" : {
227                        "op" : "all",
228                    },
229                    "conditions": []
230                }
231            },
232            "templateLinks" : [
233                {
234                    "newId" : "link",
235                    "templateId" : "template",
236                    "values" : {
237                        "?principal" : { "type" : "User", "id" : "bob" }
238                    }
239                }
240            ]
241        });
242
243        let est_policy_set: PolicySet =
244            serde_json::from_value(json).expect("failed to parse from JSON");
245        let ast_policy_set: ast::PolicySet =
246            est_policy_set.try_into().expect("failed to convert to AST");
247        assert_eq!(ast_policy_set.policies().count(), 2);
248        assert_eq!(ast_policy_set.templates().count(), 1);
249        assert!(ast_policy_set
250            .get_template_arc(&PolicyID::from_string("template"))
251            .is_some());
252        let link = ast_policy_set.get(&PolicyID::from_string("link")).unwrap();
253        assert_eq!(link.template().id(), &PolicyID::from_string("template"));
254        assert_eq!(
255            link.env(),
256            &HashMap::from_iter([(SlotId::principal(), r#"User::"bob""#.parse().unwrap())])
257        );
258        assert_eq!(
259            ast_policy_set
260                .get_linked_policies(&PolicyID::from_string("template"))
261                .unwrap()
262                .count(),
263            1
264        );
265    }
266
267    #[test]
268    fn unknown_field() {
269        let json = json!({
270            "staticPolicies": {
271                "policy1": {
272                    "effect": "permit",
273                    "principal": {
274                        "op": "==",
275                        "entity": { "type": "User", "id": "alice" }
276                    },
277                    "action": {
278                        "op" : "all"
279                    },
280                    "resource": {
281                        "op" : "all"
282                    },
283                    "conditions": []
284                }
285            },
286            "templates": {},
287            "links" : []
288        });
289
290        let err = serde_json::from_value::<PolicySet>(json)
291            .expect_err("should have failed to parse from JSON");
292        assert_eq!(
293            err.to_string(),
294            "unknown field `links`, expected one of `templates`, `staticPolicies`, `templateLinks`"
295        );
296    }
297
298    #[test]
299    fn duplicate_policy_ids() {
300        let str = r#"{
301            "staticPolicies" : {
302                "policy0": {
303                    "effect": "permit",
304                    "principal": {
305                        "op": "==",
306                        "entity": { "type": "User", "id": "alice" }
307                    },
308                    "action": {
309                        "op" : "all"
310                    },
311                    "resource": {
312                        "op" : "all"
313                    },
314                    "conditions": []
315                },
316                "policy0": {
317                    "effect": "permit",
318                    "principal": {
319                        "op": "==",
320                        "entity": { "type": "User", "id": "alice" }
321                    },
322                    "action": {
323                        "op" : "all"
324                    },
325                    "resource": {
326                        "op" : "all"
327                    },
328                    "conditions": []
329                }
330            },
331            "templates" : {},
332            "templateLinks" : []
333        }"#;
334        let err = serde_json::from_str::<PolicySet>(str)
335            .expect_err("should have failed to parse from JSON");
336        assert_eq!(
337            err.to_string(),
338            "invalid entry: found duplicate key at line 31 column 13"
339        );
340    }
341
342    #[test]
343    fn duplicate_slot_ids() {
344        let str = r#"{
345            "newId" : "foo",
346            "templateId" : "bar",
347            "values" : {
348                "?principal" : { "type" : "User", "id" : "John" },
349                "?principal" : { "type" : "User", "id" : "John" },
350            }
351        }"#;
352        let err = serde_json::from_str::<TemplateLink>(str)
353            .expect_err("should have failed to parse from JSON");
354        assert_eq!(
355            err.to_string(),
356            "invalid entry: found duplicate key at line 6 column 65"
357        );
358    }
359
360    #[test]
361    fn try_from_policies_static_only() {
362        let src = r#"
363            permit(principal == User::"alice", action, resource);
364            permit(principal, action == Action::"view", resource);
365        "#;
366        let node = crate::parser::text_to_cst::parse_policies(src).expect("Policies should parse");
367        let policy_set =
368            PolicySet::try_from(node).expect("Conversion to policy set should succeed");
369        assert_eq!(policy_set.static_policies.len(), 2);
370        assert!(policy_set.templates.is_empty());
371        assert!(policy_set.template_links.is_empty());
372    }
373
374    #[test]
375    fn try_from_policies_static_and_templates() {
376        let src = r#"
377            permit(principal == User::"alice", action, resource);
378            permit(principal == ?principal, action == Action::"view", resource);
379        "#;
380        let node = crate::parser::text_to_cst::parse_policies(src).expect("Policies should parse");
381        let policy_set =
382            PolicySet::try_from(node).expect("Conversion to policy set should succeed");
383        assert_eq!(policy_set.static_policies.len(), 1);
384        assert_eq!(policy_set.templates.len(), 1);
385        assert!(policy_set.template_links.is_empty());
386    }
387
388    #[test]
389    fn try_from_policies_with_parse_error() {
390        let src = r#"principal(p, action, resource);"#;
391        let node = crate::parser::text_to_cst::parse_policies(src).expect("policies should parse");
392        PolicySet::try_from(node).expect_err("Expected parse error to result in err");
393    }
394
395    #[test]
396    fn max_expr_height_empty_policy_set() {
397        let json = json!({
398            "staticPolicies": {},
399            "templates": {},
400            "templateLinks": []
401        });
402        let policy_set: PolicySet =
403            serde_json::from_value(json).expect("failed to parse from JSON");
404        assert_eq!(policy_set.max_expr_height(), None);
405    }
406
407    #[test]
408    fn max_expr_height_no_conditions() {
409        let json = json!({
410            "staticPolicies": {
411                "policy1": {
412                    "effect": "permit",
413                    "principal": { "op": "All" },
414                    "action": { "op": "All" },
415                    "resource": { "op": "All" },
416                    "conditions": []
417                }
418            },
419            "templates": {},
420            "templateLinks": []
421        });
422        let policy_set: PolicySet =
423            serde_json::from_value(json).expect("failed to parse from JSON");
424        assert_eq!(policy_set.max_expr_height(), None);
425    }
426
427    #[test]
428    fn max_expr_height_single_literal_condition() {
429        let json = json!({
430            "staticPolicies": {
431                "policy1": {
432                    "effect": "permit",
433                    "principal": { "op": "All" },
434                    "action": { "op": "All" },
435                    "resource": { "op": "All" },
436                    "conditions": [
437                        {
438                            "kind": "when",
439                            "body": { "Value": true }
440                        }
441                    ]
442                }
443            },
444            "templates": {},
445            "templateLinks": []
446        });
447        let policy_set: PolicySet =
448            serde_json::from_value(json).expect("failed to parse from JSON");
449        assert_eq!(policy_set.max_expr_height(), Some(0));
450    }
451
452    #[test]
453    fn max_expr_height_nested_expression() {
454        let json = json!({
455            "staticPolicies": {
456                "policy1": {
457                    "effect": "permit",
458                    "principal": { "op": "All" },
459                    "action": { "op": "All" },
460                    "resource": { "op": "All" },
461                    "conditions": [
462                        {
463                            "kind": "when",
464                            "body": {
465                                "==": {
466                                    "left": { "Value": 1 },
467                                    "right": { "Value": 2 }
468                                }
469                            }
470                        }
471                    ]
472                }
473            },
474            "templates": {},
475            "templateLinks": []
476        });
477        let policy_set: PolicySet =
478            serde_json::from_value(json).expect("failed to parse from JSON");
479        assert_eq!(policy_set.max_expr_height(), Some(1));
480    }
481
482    #[test]
483    fn max_expr_height_multiple_policies_takes_max() {
484        // policy1 condition: `true` => height 0
485        // policy2 condition: `!(1 == 2)` => height 2
486        let json = json!({
487            "staticPolicies": {
488                "policy1": {
489                    "effect": "permit",
490                    "principal": { "op": "All" },
491                    "action": { "op": "All" },
492                    "resource": { "op": "All" },
493                    "conditions": [
494                        {
495                            "kind": "when",
496                            "body": { "Value": true }
497                        }
498                    ]
499                },
500                "policy2": {
501                    "effect": "forbid",
502                    "principal": { "op": "All" },
503                    "action": { "op": "All" },
504                    "resource": { "op": "All" },
505                    "conditions": [
506                        {
507                            "kind": "when",
508                            "body": {
509                                "!": {
510                                    "arg": {
511                                        "==": {
512                                            "left": { "Value": 1 },
513                                            "right": { "Value": 2 }
514                                        }
515                                    }
516                                }
517                            }
518                        }
519                    ]
520                }
521            },
522            "templates": {},
523            "templateLinks": []
524        });
525        let policy_set: PolicySet =
526            serde_json::from_value(json).expect("failed to parse from JSON");
527        assert_eq!(policy_set.max_expr_height(), Some(2));
528    }
529
530    #[test]
531    fn max_expr_height_unless_clause() {
532        // `unless { 1 == 2 }` => height 1
533        let json = json!({
534            "staticPolicies": {
535                "policy1": {
536                    "effect": "permit",
537                    "principal": { "op": "All" },
538                    "action": { "op": "All" },
539                    "resource": { "op": "All" },
540                    "conditions": [
541                        {
542                            "kind": "unless",
543                            "body": {
544                                "==": {
545                                    "left": { "Value": 1 },
546                                    "right": { "Value": 2 }
547                                }
548                            }
549                        }
550                    ]
551                }
552            },
553            "templates": {},
554            "templateLinks": []
555        });
556        let policy_set: PolicySet =
557            serde_json::from_value(json).expect("failed to parse from JSON");
558        assert_eq!(policy_set.max_expr_height(), Some(1));
559    }
560
561    #[test]
562    fn max_expr_height_templates_included() {
563        // static policy condition: `true` => height 0
564        // template condition: `!(1 == 2)` => height 2
565        // max should be 2 (from template)
566        let json = json!({
567            "staticPolicies": {
568                "policy1": {
569                    "effect": "permit",
570                    "principal": { "op": "All" },
571                    "action": { "op": "All" },
572                    "resource": { "op": "All" },
573                    "conditions": [
574                        {
575                            "kind": "when",
576                            "body": { "Value": true }
577                        }
578                    ]
579                }
580            },
581            "templates": {
582                "template1": {
583                    "effect": "permit",
584                    "principal": { "op": "==", "slot": "?principal" },
585                    "action": { "op": "All" },
586                    "resource": { "op": "All" },
587                    "conditions": [
588                        {
589                            "kind": "when",
590                            "body": {
591                                "!": {
592                                    "arg": {
593                                        "==": {
594                                            "left": { "Value": 1 },
595                                            "right": { "Value": 2 }
596                                        }
597                                    }
598                                }
599                            }
600                        }
601                    ]
602                }
603            },
604            "templateLinks": []
605        });
606        let policy_set: PolicySet =
607            serde_json::from_value(json).expect("failed to parse from JSON");
608        assert_eq!(policy_set.max_expr_height(), Some(2));
609    }
610
611    #[test]
612    fn max_expr_height_multiple_conditions_per_policy() {
613        // Two conditions on one policy:
614        //   when { true } => height 0
615        //   when { 1 == 2 } => height 1
616        // Max should be 1
617        let json = json!({
618            "staticPolicies": {
619                "policy1": {
620                    "effect": "permit",
621                    "principal": { "op": "All" },
622                    "action": { "op": "All" },
623                    "resource": { "op": "All" },
624                    "conditions": [
625                        {
626                            "kind": "when",
627                            "body": { "Value": true }
628                        },
629                        {
630                            "kind": "when",
631                            "body": {
632                                "==": {
633                                    "left": { "Value": 1 },
634                                    "right": { "Value": 2 }
635                                }
636                            }
637                        }
638                    ]
639                }
640            },
641            "templates": {},
642            "templateLinks": []
643        });
644        let policy_set: PolicySet =
645            serde_json::from_value(json).expect("failed to parse from JSON");
646        assert_eq!(policy_set.max_expr_height(), Some(1));
647    }
648
649    #[test]
650    fn max_expr_height_literal_nested_set_matches_non_literal() {
651        // A literal set-of-set-of-set expressed as a Value node:
652        // Value(Set([Set([Set([Long(1)])])])) has CedarValueJson height 3,
653        // which is added to the Value node's depth (0), giving height 3.
654        let json_literal = json!({
655            "staticPolicies": {
656                "policy1": {
657                    "effect": "permit",
658                    "principal": { "op": "All" },
659                    "action": { "op": "All" },
660                    "resource": { "op": "All" },
661                    "conditions": [
662                        {
663                            "kind": "when",
664                            "body": {
665                                "Value": [[[1]]]
666                            }
667                        }
668                    ]
669                }
670            },
671            "templates": {},
672            "templateLinks": []
673        });
674        let policy_set_literal: PolicySet =
675            serde_json::from_value(json_literal).expect("failed to parse from JSON");
676        assert_eq!(policy_set_literal.max_expr_height(), Some(3));
677        // The same nesting expressed as non-literal Expr Set nodes:
678        // Set([Set([Set([Value(1)])])])
679        // 3 edges on the longest root-to-leaf path => height 3
680        let json_non_literal = json!({
681            "staticPolicies": {
682                "policy1": {
683                    "effect": "permit",
684                    "principal": { "op": "All" },
685                    "action": { "op": "All" },
686                    "resource": { "op": "All" },
687                    "conditions": [
688                        {
689                            "kind": "when",
690                            "body": {
691                                "Set": [
692                                    { "Set": [
693                                        { "Set": [
694                                            { "Value": 1 }
695                                        ]}
696                                    ]}
697                                ]
698                            }
699                        }
700                    ]
701                }
702            },
703            "templates": {},
704            "templateLinks": []
705        });
706        let policy_set_non_literal: PolicySet =
707            serde_json::from_value(json_non_literal).expect("failed to parse from JSON");
708        assert_eq!(policy_set_non_literal.max_expr_height(), Some(3));
709        // Both representations have the same height
710        assert_eq!(
711            policy_set_literal.max_expr_height(),
712            policy_set_non_literal.max_expr_height()
713        );
714    }
715
716    #[test]
717    fn max_expr_height_deeply_nested() {
718        // !(!(!(true))): 3 edges on the longest root-to-leaf path => height 3
719        let json = json!({
720            "staticPolicies": {
721                "policy1": {
722                    "effect": "permit",
723                    "principal": { "op": "All" },
724                    "action": { "op": "All" },
725                    "resource": { "op": "All" },
726                    "conditions": [
727                        {
728                            "kind": "when",
729                            "body": {
730                                "!": {
731                                    "arg": {
732                                        "!": {
733                                            "arg": {
734                                                "!": {
735                                                    "arg": { "Value": true }
736                                                }
737                                            }
738                                        }
739                                    }
740                                }
741                            }
742                        }
743                    ]
744                }
745            },
746            "templates": {},
747            "templateLinks": []
748        });
749        let policy_set: PolicySet =
750            serde_json::from_value(json).expect("failed to parse from JSON");
751        assert_eq!(policy_set.max_expr_height(), Some(3));
752    }
753}