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::Policy;
18use super::PolicySetFromJsonError;
19use crate::ast::{self, EntityUID, PolicyID, SlotId};
20use crate::entities::json::err::JsonDeserializationErrorContext;
21use crate::entities::json::EntityUidJson;
22use crate::jsonvalue::deserialize_linked_hash_map_no_duplicates;
23use crate::parser::cst::Policies;
24use crate::parser::err::ParseErrors;
25use crate::parser::Node;
26use linked_hash_map::LinkedHashMap;
27use serde::{Deserialize, Serialize};
28use serde_with::serde_as;
29use std::collections::HashMap;
30
31/// Serde JSON structure for a policy set in the EST format
32#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33#[serde(rename_all = "camelCase")]
34#[serde(deny_unknown_fields)]
35pub struct PolicySet {
36    /// The set of templates in a policy set
37    #[serde(deserialize_with = "deserialize_linked_hash_map_no_duplicates")]
38    pub templates: LinkedHashMap<PolicyID, Policy>,
39    /// The set of static policies in a policy set
40    #[serde(deserialize_with = "deserialize_linked_hash_map_no_duplicates")]
41    pub static_policies: LinkedHashMap<PolicyID, Policy>,
42    /// The set of template links
43    pub template_links: Vec<TemplateLink>,
44}
45
46impl PolicySet {
47    /// Get the static or template-linked policy with the given id.
48    /// Returns an `Option` rather than a `Result` because it is expected to be
49    /// used in cases where the policy set is guaranteed to be well-formed
50    /// (e.g., after successful conversion to an `ast::PolicySet`)
51    pub fn get_policy(&self, id: &PolicyID) -> Option<Policy> {
52        let maybe_static_policy = self.static_policies.get(id).cloned();
53
54        let maybe_link = self
55            .template_links
56            .iter()
57            .filter_map(|link| {
58                if &link.new_id == id {
59                    self.get_template(&link.template_id).and_then(|template| {
60                        let unwrapped_est_vals: HashMap<SlotId, EntityUidJson> =
61                            link.values.iter().map(|(k, v)| (*k, v.into())).collect();
62                        template.link(&unwrapped_est_vals).ok()
63                    })
64                } else {
65                    None
66                }
67            })
68            .next();
69
70        maybe_static_policy.or(maybe_link)
71    }
72
73    /// Get the template with the given id.
74    /// Returns an `Option` rather than a `Result` because it is expected to be
75    /// used in cases where the policy set is guaranteed to be well-formed
76    /// (e.g., after successful conversion to an `ast::PolicySet`)
77    pub fn get_template(&self, id: &PolicyID) -> Option<Policy> {
78        self.templates.get(id).cloned()
79    }
80}
81
82/// Serde JSON structure describing a template-linked policy
83#[serde_as]
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86#[serde(deny_unknown_fields)]
87pub struct TemplateLink {
88    /// Id of the template to link against
89    pub template_id: PolicyID,
90    /// Id of the generated policy
91    pub new_id: PolicyID,
92    /// Mapping between slots and entity uids
93    #[serde_as(as = "serde_with::MapPreventDuplicates<_,EntityUidJson<TemplateLinkContext>>")]
94    pub values: HashMap<SlotId, EntityUID>,
95}
96
97/// Statically set the deserialization error context to be deserialization of a template link
98struct TemplateLinkContext;
99
100impl crate::entities::json::DeserializationContext for TemplateLinkContext {
101    fn static_context() -> Option<JsonDeserializationErrorContext> {
102        Some(JsonDeserializationErrorContext::TemplateLink)
103    }
104}
105
106impl TryFrom<PolicySet> for ast::PolicySet {
107    type Error = PolicySetFromJsonError;
108
109    fn try_from(value: PolicySet) -> Result<Self, Self::Error> {
110        let mut ast_pset = ast::PolicySet::default();
111
112        for (id, policy) in value.static_policies {
113            let ast = policy.try_into_ast_policy(Some(id))?;
114            ast_pset.add(ast)?;
115        }
116
117        for (id, policy) in value.templates {
118            let ast = policy.try_into_ast_policy_or_template(Some(id))?;
119            ast_pset.add_template(ast)?;
120        }
121
122        for TemplateLink {
123            template_id,
124            new_id,
125            values,
126        } in value.template_links
127        {
128            ast_pset.link(template_id, new_id, values)?;
129        }
130
131        Ok(ast_pset)
132    }
133}
134
135impl TryFrom<Node<Option<Policies>>> for PolicySet {
136    type Error = ParseErrors;
137
138    fn try_from(policies: Node<Option<Policies>>) -> Result<Self, Self::Error> {
139        let mut templates = LinkedHashMap::new();
140        let mut static_policies = LinkedHashMap::new();
141        let mut all_errs: Vec<ParseErrors> = vec![];
142        for (policy_id, policy) in policies.with_generated_policyids()? {
143            match policy.try_as_inner() {
144                Ok(cst) => match Policy::try_from(cst.clone()) {
145                    Ok(est) => {
146                        if est.is_template() {
147                            templates.insert(policy_id, est);
148                        } else {
149                            static_policies.insert(policy_id, est);
150                        }
151                    }
152                    Err(e) => {
153                        all_errs.push(e);
154                    }
155                },
156                Err(e) => {
157                    all_errs.push(e.into());
158                }
159            };
160        }
161        // fail on any error
162        if let Some(errs) = ParseErrors::flatten(all_errs) {
163            Err(errs)
164        } else {
165            Ok(PolicySet {
166                templates,
167                static_policies,
168                template_links: Vec::new(),
169            })
170        }
171    }
172}
173
174#[cfg(test)]
175mod test {
176    use serde_json::json;
177
178    use super::*;
179
180    #[test]
181    fn valid_example() {
182        let json = json!({
183            "staticPolicies": {
184                "policy1": {
185                    "effect": "permit",
186                    "principal": {
187                        "op": "==",
188                        "entity": { "type": "User", "id": "alice" }
189                    },
190                    "action": {
191                        "op": "==",
192                        "entity": { "type": "Action", "id": "view" }
193                    },
194                    "resource": {
195                        "op": "in",
196                        "entity": { "type": "Folder", "id": "foo" }
197                    },
198                    "conditions": []
199                }
200            },
201            "templates": {
202                "template": {
203                    "effect" : "permit",
204                    "principal" : {
205                        "op" : "==",
206                        "slot" : "?principal"
207                    },
208                    "action" : {
209                        "op" : "all"
210                    },
211                    "resource" : {
212                        "op" : "all",
213                    },
214                    "conditions": []
215                }
216            },
217            "templateLinks" : [
218                {
219                    "newId" : "link",
220                    "templateId" : "template",
221                    "values" : {
222                        "?principal" : { "type" : "User", "id" : "bob" }
223                    }
224                }
225            ]
226        });
227
228        let est_policy_set: PolicySet =
229            serde_json::from_value(json).expect("failed to parse from JSON");
230        let ast_policy_set: ast::PolicySet =
231            est_policy_set.try_into().expect("failed to convert to AST");
232        assert_eq!(ast_policy_set.policies().count(), 2);
233        assert_eq!(ast_policy_set.templates().count(), 1);
234        assert!(ast_policy_set
235            .get_template_arc(&PolicyID::from_string("template"))
236            .is_some());
237        let link = ast_policy_set.get(&PolicyID::from_string("link")).unwrap();
238        assert_eq!(link.template().id(), &PolicyID::from_string("template"));
239        assert_eq!(
240            link.env(),
241            &HashMap::from_iter([(SlotId::principal(), r#"User::"bob""#.parse().unwrap())])
242        );
243        assert_eq!(
244            ast_policy_set
245                .get_linked_policies(&PolicyID::from_string("template"))
246                .unwrap()
247                .count(),
248            1
249        );
250    }
251
252    #[test]
253    fn unknown_field() {
254        let json = json!({
255            "staticPolicies": {
256                "policy1": {
257                    "effect": "permit",
258                    "principal": {
259                        "op": "==",
260                        "entity": { "type": "User", "id": "alice" }
261                    },
262                    "action": {
263                        "op" : "all"
264                    },
265                    "resource": {
266                        "op" : "all"
267                    },
268                    "conditions": []
269                }
270            },
271            "templates": {},
272            "links" : []
273        });
274
275        let err = serde_json::from_value::<PolicySet>(json)
276            .expect_err("should have failed to parse from JSON");
277        assert_eq!(
278            err.to_string(),
279            "unknown field `links`, expected one of `templates`, `staticPolicies`, `templateLinks`"
280        );
281    }
282
283    #[test]
284    fn duplicate_policy_ids() {
285        let str = r#"{
286            "staticPolicies" : {
287                "policy0": {
288                    "effect": "permit",
289                    "principal": {
290                        "op": "==",
291                        "entity": { "type": "User", "id": "alice" }
292                    },
293                    "action": {
294                        "op" : "all"
295                    },
296                    "resource": {
297                        "op" : "all"
298                    },
299                    "conditions": []
300                },
301                "policy0": {
302                    "effect": "permit",
303                    "principal": {
304                        "op": "==",
305                        "entity": { "type": "User", "id": "alice" }
306                    },
307                    "action": {
308                        "op" : "all"
309                    },
310                    "resource": {
311                        "op" : "all"
312                    },
313                    "conditions": []
314                }
315            },
316            "templates" : {},
317            "templateLinks" : []
318        }"#;
319        let err = serde_json::from_str::<PolicySet>(str)
320            .expect_err("should have failed to parse from JSON");
321        assert_eq!(
322            err.to_string(),
323            "invalid entry: found duplicate key at line 31 column 13"
324        );
325    }
326
327    #[test]
328    fn duplicate_slot_ids() {
329        let str = r#"{
330            "newId" : "foo",
331            "templateId" : "bar",
332            "values" : {
333                "?principal" : { "type" : "User", "id" : "John" },
334                "?principal" : { "type" : "User", "id" : "John" },
335            }
336        }"#;
337        let err = serde_json::from_str::<TemplateLink>(str)
338            .expect_err("should have failed to parse from JSON");
339        assert_eq!(
340            err.to_string(),
341            "invalid entry: found duplicate key at line 6 column 65"
342        );
343    }
344
345    #[test]
346    fn try_from_policies_static_only() {
347        let src = r#"
348            permit(principal == User::"alice", action, resource);
349            permit(principal, action == Action::"view", resource);
350        "#;
351        let node = crate::parser::text_to_cst::parse_policies(src).expect("Policies should parse");
352        let policy_set =
353            PolicySet::try_from(node).expect("Conversion to policy set should succeed");
354        assert_eq!(policy_set.static_policies.len(), 2);
355        assert!(policy_set.templates.is_empty());
356        assert!(policy_set.template_links.is_empty());
357    }
358
359    #[test]
360    fn try_from_policies_static_and_templates() {
361        let src = r#"
362            permit(principal == User::"alice", action, resource);
363            permit(principal == ?principal, action == Action::"view", resource);
364        "#;
365        let node = crate::parser::text_to_cst::parse_policies(src).expect("Policies should parse");
366        let policy_set =
367            PolicySet::try_from(node).expect("Conversion to policy set should succeed");
368        assert_eq!(policy_set.static_policies.len(), 1);
369        assert_eq!(policy_set.templates.len(), 1);
370        assert!(policy_set.template_links.is_empty());
371    }
372
373    #[test]
374    fn try_from_policies_with_parse_error() {
375        let src = r#"principal(p, action, resource);"#;
376        let node = crate::parser::text_to_cst::parse_policies(src).expect("policies should parse");
377        PolicySet::try_from(node).expect_err("Expected parse error to result in err");
378    }
379}