cedar_policy/ffi/
convert.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
17//! JSON FFI entry points for converting between JSON and Cedar formats. The
18//! Cedar Wasm conversion functions are generated from the functions in this
19//! file.
20
21use super::utils::JsonValueWithNoDuplicateKeys;
22use super::{DetailedError, Policy, Schema, Template};
23use serde::{Deserialize, Serialize};
24#[cfg(feature = "wasm")]
25use wasm_bindgen::prelude::wasm_bindgen;
26
27#[cfg(feature = "wasm")]
28extern crate tsify;
29
30/// Return the Cedar (textual) representation of a policy.
31#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "policyToText"))]
32pub fn policy_to_text(policy: Policy) -> PolicyToTextAnswer {
33    match policy.parse(None) {
34        Ok(policy) => PolicyToTextAnswer::Success {
35            text: policy.to_string(),
36        },
37        Err(e) => PolicyToTextAnswer::Failure {
38            errors: vec![e.into()],
39        },
40    }
41}
42
43/// Return the Cedar (textual) representation of a template.
44#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "templateToText"))]
45pub fn template_to_text(template: Template) -> PolicyToTextAnswer {
46    match template.parse(None) {
47        Ok(template) => PolicyToTextAnswer::Success {
48            text: template.to_string(),
49        },
50        Err(e) => PolicyToTextAnswer::Failure {
51            errors: vec![e.into()],
52        },
53    }
54}
55
56/// Return the JSON representation of a policy.
57#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "policyToJson"))]
58pub fn policy_to_json(policy: Policy) -> PolicyToJsonAnswer {
59    match policy.parse(None) {
60        Ok(policy) => match policy.to_json() {
61            Ok(json) => PolicyToJsonAnswer::Success { json: json.into() },
62            Err(e) => PolicyToJsonAnswer::Failure {
63                errors: vec![miette::Report::new(e).into()],
64            },
65        },
66        Err(e) => PolicyToJsonAnswer::Failure {
67            errors: vec![e.into()],
68        },
69    }
70}
71
72/// Return the JSON representation of a template.
73#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "templateToJson"))]
74pub fn template_to_json(template: Template) -> PolicyToJsonAnswer {
75    match template.parse(None) {
76        Ok(template) => match template.to_json() {
77            Ok(json) => PolicyToJsonAnswer::Success { json: json.into() },
78            Err(e) => PolicyToJsonAnswer::Failure {
79                errors: vec![miette::Report::new(e).into()],
80            },
81        },
82        Err(e) => PolicyToJsonAnswer::Failure {
83            errors: vec![e.into()],
84        },
85    }
86}
87
88/// Return the Cedar (textual) representation of a schema.
89#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "schemaToText"))]
90pub fn schema_to_text(schema: Schema) -> SchemaToTextAnswer {
91    match schema.parse_schema_fragment() {
92        Ok((schema_frag, warnings)) => {
93            match schema_frag.to_cedarschema() {
94                Ok(text) => {
95                    // Before returning, check that the schema fragment corresponds to a valid schema
96                    if let Err(e) = TryInto::<crate::Schema>::try_into(schema_frag) {
97                        SchemaToTextAnswer::Failure {
98                            errors: vec![miette::Report::new(e).into()],
99                        }
100                    } else {
101                        SchemaToTextAnswer::Success {
102                            text,
103                            warnings: warnings.map(|e| miette::Report::new(e).into()).collect(),
104                        }
105                    }
106                }
107                Err(e) => SchemaToTextAnswer::Failure {
108                    errors: vec![miette::Report::new(e).into()],
109                },
110            }
111        }
112        Err(e) => SchemaToTextAnswer::Failure {
113            errors: vec![e.into()],
114        },
115    }
116}
117
118/// Return the JSON representation of a schema.
119#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "schemaToJson"))]
120pub fn schema_to_json(schema: Schema) -> SchemaToJsonAnswer {
121    match schema.parse_schema_fragment() {
122        Ok((schema_frag, warnings)) => match schema_frag.to_json_value() {
123            Ok(json) => {
124                // Before returning, check that the schema fragment corresponds to a valid schema
125                if let Err(e) = crate::Schema::from_json_value(json.clone()) {
126                    SchemaToJsonAnswer::Failure {
127                        errors: vec![miette::Report::new(e).into()],
128                    }
129                } else {
130                    SchemaToJsonAnswer::Success {
131                        json: json.into(),
132                        warnings: warnings.map(|e| miette::Report::new(e).into()).collect(),
133                    }
134                }
135            }
136            Err(e) => SchemaToJsonAnswer::Failure {
137                errors: vec![miette::Report::new(e).into()],
138            },
139        },
140        Err(e) => SchemaToJsonAnswer::Failure {
141            errors: vec![e.into()],
142        },
143    }
144}
145
146/// Result of converting a policy or template to the Cedar format
147#[derive(Debug, Serialize, Deserialize)]
148#[serde(tag = "type")]
149#[serde(rename_all = "camelCase")]
150#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
151#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
152pub enum PolicyToTextAnswer {
153    /// Represents a successful call
154    Success {
155        /// Cedar format policy
156        text: String,
157    },
158    /// Represents a failed call (e.g., because the input is ill-formed)
159    Failure {
160        /// Errors
161        errors: Vec<DetailedError>,
162    },
163}
164
165/// Result of converting a policy or template to JSON
166#[derive(Debug, Serialize, Deserialize)]
167#[serde(tag = "type")]
168#[serde(rename_all = "camelCase")]
169#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
170#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
171pub enum PolicyToJsonAnswer {
172    /// Represents a successful call
173    Success {
174        /// JSON format policy
175        #[cfg_attr(feature = "wasm", tsify(type = "PolicyJson"))]
176        json: JsonValueWithNoDuplicateKeys,
177    },
178    /// Represents a failed call (e.g., because the input is ill-formed)
179    Failure {
180        /// Errors
181        errors: Vec<DetailedError>,
182    },
183}
184
185/// Result of converting a schema to the Cedar format
186#[derive(Debug, Serialize, Deserialize)]
187#[serde(tag = "type")]
188#[serde(rename_all = "camelCase")]
189#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
190#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
191pub enum SchemaToTextAnswer {
192    /// Represents a successful call
193    Success {
194        /// Cedar format schema
195        text: String,
196        /// Warnings
197        warnings: Vec<DetailedError>,
198    },
199    /// Represents a failed call (e.g., because the input is ill-formed)
200    Failure {
201        /// Errors
202        errors: Vec<DetailedError>,
203    },
204}
205
206/// Result of converting a schema to JSON
207#[derive(Debug, Serialize, Deserialize)]
208#[serde(tag = "type")]
209#[serde(rename_all = "camelCase")]
210#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
211#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
212pub enum SchemaToJsonAnswer {
213    /// Represents a successful call
214    Success {
215        /// JSON format schema
216        #[cfg_attr(feature = "wasm", tsify(type = "SchemaJson<string>"))]
217        json: JsonValueWithNoDuplicateKeys,
218        /// Warnings
219        warnings: Vec<DetailedError>,
220    },
221    /// Represents a failed call (e.g., because the input is ill-formed)
222    Failure {
223        /// Errors
224        errors: Vec<DetailedError>,
225    },
226}
227
228#[cfg(test)]
229mod test {
230    use super::*;
231
232    use crate::ffi::test_utils::*;
233    use cool_asserts::assert_matches;
234    use serde_json::json;
235
236    #[test]
237    fn test_policy_to_json() {
238        let text = r#"
239            permit(principal, action, resource)
240            when { principal has "Email" && principal.Email == "a@a.com" };
241        "#;
242        let result = policy_to_json(Policy::Cedar(text.into()));
243        let expected = json!({
244            "effect": "permit",
245            "principal": {
246                "op": "All"
247            },
248            "action": {
249                "op": "All"
250            },
251            "resource": {
252                "op": "All"
253            },
254            "conditions": [
255                {
256                    "kind": "when",
257                    "body": {
258                        "&&": {
259                            "left": {
260                                "has": {
261                                    "left": {
262                                        "Var": "principal"
263                                    },
264                                    "attr": "Email"
265                                }
266                            },
267                            "right": {
268                                "==": {
269                                    "left": {
270                                        ".": {
271                                            "left": {
272                                                "Var": "principal"
273                                            },
274                                            "attr": "Email"
275                                        }
276                                    },
277                                    "right": {
278                                        "Value": "a@a.com"
279                                    }
280                                }
281                            }
282                        }
283                    }
284                }
285            ]
286        });
287        assert_matches!(result, PolicyToJsonAnswer::Success { json } =>
288          assert_eq!(json, expected.into())
289        );
290    }
291
292    #[test]
293    fn test_policy_to_json_error() {
294        let text = r#"
295            permit(principal, action, resource)
296            when { principal has "Email" && principal.Email == };
297        "#;
298        let result = policy_to_json(Policy::Cedar(text.into()));
299        assert_matches!(result, PolicyToJsonAnswer::Failure { errors } => {
300            assert_exactly_one_error(
301                &errors,
302                "failed to parse policy from string: unexpected token `}`",
303                None,
304            );
305        });
306    }
307
308    #[test]
309    fn test_policy_to_text() {
310        let json = json!({
311            "effect": "permit",
312            "action": {
313                "entity": {
314                    "id": "pop",
315                    "type": "Action"
316                },
317                "op": "=="
318            },
319            "principal": {
320                "entity": {
321                    "id": "DeathRowRecords",
322                    "type": "UserGroup"
323                },
324                "op": "in"
325            },
326            "resource": {
327                "op": "All"
328            },
329            "conditions": []
330        });
331        let result = policy_to_text(Policy::Json(json.into()));
332        assert_matches!(result, PolicyToTextAnswer::Success { text } => {
333            assert_eq!(
334                &text,
335                "permit(principal in UserGroup::\"DeathRowRecords\", action == Action::\"pop\", resource);"
336            );
337        });
338    }
339
340    #[test]
341    fn test_template_to_json() {
342        let text = r"
343            permit(principal in ?principal, action, resource);
344        ";
345        let result = template_to_json(Template::Cedar(text.into()));
346        let expected = json!({
347            "effect": "permit",
348            "principal": {
349                "op": "in",
350                "slot": "?principal"
351            },
352            "action": {
353                "op": "All"
354            },
355            "resource": {
356                "op": "All"
357            },
358            "conditions": []
359        });
360        assert_matches!(result, PolicyToJsonAnswer::Success { json } =>
361          assert_eq!(json, expected.into())
362        );
363    }
364
365    #[test]
366    fn test_template_to_text() {
367        let json = json!({
368            "effect": "permit",
369            "principal": {
370                "op": "All"
371            },
372            "action": {
373                "op": "All"
374            },
375            "resource": {
376                "op": "in",
377                "slot": "?resource"
378            },
379            "conditions": []
380        });
381        let result = template_to_text(Template::Json(json.into()));
382        assert_matches!(result, PolicyToTextAnswer::Success { text } => {
383            assert_eq!(
384                &text,
385                "permit(principal, action, resource in ?resource);"
386            );
387        });
388    }
389
390    #[test]
391    fn test_template_to_text_error() {
392        let json = json!({
393            "effect": "permit",
394            "action": {
395                "entity": {
396                    "id": "pop",
397                    "type": "Action"
398                },
399                "op": "=="
400            },
401            "principal": {
402                "entity": {
403                    "id": "DeathRowRecords",
404                    "type": "UserGroup"
405                },
406                "op": "in"
407            },
408            "resource": {
409                "op": "All"
410            },
411            "conditions": []
412        });
413        let result = template_to_text(Template::Json(json.into()));
414        assert_matches!(result, PolicyToTextAnswer::Failure { errors } => {
415            assert_exactly_one_error(
416                &errors,
417                "failed to parse template from JSON: error deserializing a policy/template from JSON: expected a template, got a static policy",
418                Some("a template should include slot(s) `?principal` or `?resource`"),
419            );
420        });
421    }
422
423    #[test]
424    fn test_schema_to_json() {
425        let text = r#"
426            entity User = { "name": String };
427            action sendMessage appliesTo {principal: User, resource: User};
428        "#;
429        let result = schema_to_json(Schema::Cedar(text.into()));
430        let expected = json!({
431        "": {
432            "entityTypes": {
433                "User": {
434                    "shape": {
435                        "type": "Record",
436                        "attributes": {
437                            "name": {"type": "EntityOrCommon", "name": "String"} // this will resolve to the builtin type `String` unless the user defines their own common or entity type `String` in the empty namespace, in another fragment
438                        }
439                    }
440                }
441            },
442            "actions": {
443                "sendMessage": {
444                    "appliesTo": {
445                        "resourceTypes": ["User"],
446                        "principalTypes": ["User"]
447                    }
448                }}
449            }
450        });
451        assert_matches!(result, SchemaToJsonAnswer::Success { json, warnings:_ } =>
452          assert_eq!(json, expected.into())
453        );
454    }
455
456    #[test]
457    fn test_schema_to_json_error() {
458        let text = r"
459            action sendMessage appliesTo {principal: User, resource: User};
460        ";
461        let result = schema_to_json(Schema::Cedar(text.into()));
462        assert_matches!(result, SchemaToJsonAnswer::Failure { errors } => {
463            assert_exactly_one_error(
464                &errors,
465                "failed to resolve types: User, User",
466                Some("`User` has not been declared as an entity type"),
467            );
468        });
469    }
470
471    #[test]
472    fn test_schema_to_text() {
473        let json = json!({
474        "": {
475            "entityTypes": {
476                "User": {
477                    "shape": {
478                        "type": "Record",
479                        "attributes": {
480                            "name": {"type": "String"}
481                        }
482                    }
483                }
484            },
485            "actions": {
486                "sendMessage": {
487                    "appliesTo": {
488                        "resourceTypes": ["User"],
489                        "principalTypes": ["User"]
490                    }
491                }}
492            }
493        });
494        let result = schema_to_text(Schema::Json(json.into()));
495        assert_matches!(result, SchemaToTextAnswer::Success { text, warnings:_ } => {
496            assert_eq!(
497                &text,
498                "entity User = {\"name\": __cedar::String};\naction \"sendMessage\" appliesTo {\n  principal: [User],\n  resource: [User],\n  context: {}\n};\n"
499            );
500        });
501    }
502}