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