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