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