cedar_policy/ffi/
check_parse.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 parsing various Cedar structures. The Cedar Wasm
18//! parsing functions are generated from the functions in this file.
19
20#![allow(clippy::module_name_repetitions)]
21
22use super::{utils::DetailedError, Context, Entities, EntityUid, PolicySet, Schema};
23use serde::{Deserialize, Serialize};
24#[cfg(feature = "wasm")]
25use wasm_bindgen::prelude::wasm_bindgen;
26
27#[cfg(feature = "wasm")]
28extern crate tsify;
29
30/// Check whether a policy set successfully parses.
31#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "checkParsePolicySet"))]
32pub fn check_parse_policy_set(policies: PolicySet) -> CheckParseAnswer {
33    policies.parse().into()
34}
35
36/// Check whether a policy set successfully parses. Input is a JSON encoding of
37/// [`PolicySet`] and output is a JSON encoding of [`CheckParseAnswer`].
38///
39/// # Errors
40///
41/// Will return `Err` if the input JSON cannot be deserialized as a
42/// [`PolicySet`].
43pub fn check_parse_policy_set_json(
44    json: serde_json::Value,
45) -> Result<serde_json::Value, serde_json::Error> {
46    let ans = check_parse_policy_set(serde_json::from_value(json)?);
47    serde_json::to_value(ans)
48}
49
50/// Check whether a policy set successfully parses. Input and output are
51/// strings containing serialized JSON, in the shapes expected by
52/// [`check_parse_policy_set_json()`].
53///
54/// # Errors
55///
56/// Will return `Err` if the input cannot be converted to valid JSON or
57/// deserialized as a [`PolicySet`].
58pub fn check_parse_policy_set_json_str(json: &str) -> Result<String, serde_json::Error> {
59    let ans = check_parse_policy_set(serde_json::from_str(json)?);
60    serde_json::to_string(&ans)
61}
62
63/// Check whether a schema successfully parses.
64#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "checkParseSchema"))]
65pub fn check_parse_schema(schema: Schema) -> CheckParseAnswer {
66    schema.parse().into()
67}
68
69/// Check whether a schema successfully parses. Input is a JSON encoding of
70/// [`Schema`] and output is a JSON encoding of [`CheckParseAnswer`].
71///
72/// # Errors
73///
74/// Will return `Err` if the input JSON cannot be deserialized as a [`Schema`].
75pub fn check_parse_schema_json(
76    json: serde_json::Value,
77) -> Result<serde_json::Value, serde_json::Error> {
78    let ans = check_parse_schema(serde_json::from_value(json)?);
79    serde_json::to_value(ans)
80}
81
82/// Check whether a schema successfully parses. Input and output are strings
83/// containing serialized JSON, in the shapes expected by
84/// [`check_parse_schema_json()`].
85///
86/// # Errors
87///
88/// Will return `Err` if the input cannot be converted to valid JSON or
89/// deserialized as a [`Schema`].
90pub fn check_parse_schema_json_str(json: &str) -> Result<String, serde_json::Error> {
91    let ans = check_parse_schema(serde_json::from_str(json)?);
92    serde_json::to_string(&ans)
93}
94
95/// Check whether a set of entities successfully parses.
96#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "checkParseEntities"))]
97pub fn check_parse_entities(call: EntitiesParsingCall) -> CheckParseAnswer {
98    let schema = match call.schema.map(|s| s.parse().map(|res| res.0)).transpose() {
99        Ok(schema) => schema,
100        Err(err) => {
101            return CheckParseAnswer::Failure {
102                errors: vec![err.into()],
103            };
104        }
105    };
106    call.entities.parse(schema.as_ref()).into()
107}
108
109/// Check whether a set of entities successfully parses. Input is a JSON
110/// encoding of [`EntitiesParsingCall`] and output is a JSON encoding of
111/// [`CheckParseAnswer`].
112///
113/// # Errors
114///
115/// Will return `Err` if the input JSON cannot be deserialized as a
116/// [`Entities`].
117pub fn check_parse_entities_json(
118    json: serde_json::Value,
119) -> Result<serde_json::Value, serde_json::Error> {
120    let ans = check_parse_entities(serde_json::from_value(json)?);
121    serde_json::to_value(ans)
122}
123
124/// Check whether a set of entities successfully parses. Input and output are
125/// strings containing serialized JSON, in the shapes expected by
126/// [`check_parse_entities_json()`].
127///
128/// # Errors
129///
130/// Will return `Err` if the input cannot be converted to valid JSON or
131/// deserialized as a [`Entities`].
132pub fn check_parse_entities_json_str(json: &str) -> Result<String, serde_json::Error> {
133    let ans = check_parse_entities(serde_json::from_str(json)?);
134    serde_json::to_string(&ans)
135}
136
137/// Check whether a context successfully parses.
138#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "checkParseContext"))]
139pub fn check_parse_context(call: ContextParsingCall) -> CheckParseAnswer {
140    let action = match call.action.map(|a| a.parse(Some("action"))).transpose() {
141        Ok(action) => action,
142        Err(err) => {
143            return CheckParseAnswer::Failure {
144                errors: vec![err.into()],
145            };
146        }
147    };
148    let schema = match call.schema.map(|s| s.parse().map(|res| res.0)).transpose() {
149        Ok(schema) => schema,
150        Err(err) => {
151            return CheckParseAnswer::Failure {
152                errors: vec![err.into()],
153            };
154        }
155    };
156    call.context.parse(schema.as_ref(), action.as_ref()).into()
157}
158
159/// Check whether a context successfully parses. Input is a JSON encoding of
160/// [`ContextParsingCall`] and output is a JSON encoding of [`CheckParseAnswer`].
161///
162/// # Errors
163///
164/// Will return `Err` if the input JSON cannot be deserialized as a
165/// [`Context`].
166pub fn check_parse_context_json(
167    json: serde_json::Value,
168) -> Result<serde_json::Value, serde_json::Error> {
169    let ans = check_parse_context(serde_json::from_value(json)?);
170    serde_json::to_value(ans)
171}
172
173/// Check whether a context successfully parses. Input and output are
174/// strings containing serialized JSON, in the shapes expected by
175/// [`check_parse_context_json()`].
176///
177/// # Errors
178///
179/// Will return `Err` if the input cannot be converted to valid JSON or
180/// deserialized as a [`Context`].
181pub fn check_parse_context_json_str(json: &str) -> Result<String, serde_json::Error> {
182    let ans = check_parse_context(serde_json::from_str(json)?);
183    serde_json::to_string(&ans)
184}
185
186/// Result struct for syntax validation
187#[derive(Debug, Serialize, Deserialize)]
188#[serde(tag = "type")]
189#[serde(rename_all = "camelCase")]
190#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
191#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
192pub enum CheckParseAnswer {
193    /// Successfully parsed
194    Success,
195    /// Failed to parse
196    Failure {
197        /// Reported errors
198        errors: Vec<DetailedError>,
199    },
200}
201
202impl<T> From<Result<T, miette::Report>> for CheckParseAnswer {
203    fn from(res: Result<T, miette::Report>) -> Self {
204        match res {
205            Ok(_) => Self::Success,
206            Err(err) => Self::Failure {
207                errors: vec![err.into()],
208            },
209        }
210    }
211}
212
213impl<T> From<Result<T, Vec<miette::Report>>> for CheckParseAnswer {
214    fn from(res: Result<T, Vec<miette::Report>>) -> Self {
215        match res {
216            Ok(_) => Self::Success,
217            Err(errs) => Self::Failure {
218                errors: errs.into_iter().map(Into::into).collect(),
219            },
220        }
221    }
222}
223
224/// Struct containing the input data for [`check_parse_entities()`]
225#[derive(Serialize, Deserialize, Debug)]
226#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
227#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
228#[serde(rename_all = "camelCase")]
229#[serde(deny_unknown_fields)]
230pub struct EntitiesParsingCall {
231    /// Input entities
232    entities: Entities,
233    /// Optional schema for schema-based parsing
234    #[serde(default)]
235    schema: Option<Schema>,
236}
237
238/// Struct containing the input data for [`check_parse_context()`]
239#[derive(Serialize, Deserialize, Debug)]
240#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
241#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
242#[serde(rename_all = "camelCase")]
243#[serde(deny_unknown_fields)]
244pub struct ContextParsingCall {
245    /// Input context
246    context: Context,
247    /// Optional schema for schema-based parsing
248    #[serde(default)]
249    schema: Option<Schema>,
250    /// Optional action entity for schema-based parsing
251    #[serde(default)]
252    action: Option<EntityUid>,
253}
254
255#[cfg(test)]
256mod test {
257    use super::*;
258    use crate::ffi::test_utils::assert_exactly_one_error;
259    use cool_asserts::assert_matches;
260    use serde_json::json;
261
262    #[track_caller]
263    fn assert_check_parse_is_ok(parse_result: &CheckParseAnswer) {
264        assert_matches!(parse_result, CheckParseAnswer::Success);
265    }
266
267    #[track_caller]
268    fn assert_check_parse_is_err(parse_result: &CheckParseAnswer) -> &[DetailedError] {
269        assert_matches!(
270            parse_result,
271            CheckParseAnswer::Failure { errors } => errors
272        )
273    }
274
275    #[test]
276    fn can_parse_1_policy() {
277        let call = json!({
278                "staticPolicies": "permit(principal, action, resource);"
279        });
280        let answer = serde_json::from_value(check_parse_policy_set_json(call).unwrap()).unwrap();
281        assert_check_parse_is_ok(&answer);
282    }
283
284    #[test]
285    fn can_parse_multi_policy() {
286        let call = json!({
287            "staticPolicies": "forbid(principal, action, resource); permit(principal == User::\"alice\", action == Action::\"view\", resource in Albums::\"alice_albums\");"
288        });
289        let answer = serde_json::from_value(check_parse_policy_set_json(call).unwrap()).unwrap();
290        assert_check_parse_is_ok(&answer);
291    }
292
293    #[test]
294    fn parse_policy_set_fails() {
295        let call = json!({
296            "staticPolicies": "forbid(principal, action, resource);permit(2pac, action, resource)"
297        });
298        let answer = serde_json::from_value(check_parse_policy_set_json(call).unwrap()).unwrap();
299        let errs = assert_check_parse_is_err(&answer);
300        assert_exactly_one_error(
301            errs,
302            "failed to parse policies from string: unexpected token `2`",
303            None,
304        );
305    }
306
307    #[test]
308    fn can_parse_template() {
309        let call = json!({
310            "templates": {
311                "ID0": "permit (principal == ?principal, action, resource == ?resource);"
312            }
313        });
314        let answer = serde_json::from_value(check_parse_policy_set_json(call).unwrap()).unwrap();
315        assert_check_parse_is_ok(&answer);
316    }
317
318    #[test]
319    fn check_parse_schema_succeeds_empty_schema() {
320        let call = json!({});
321        let answer = serde_json::from_value(check_parse_schema_json(call).unwrap()).unwrap();
322        assert_check_parse_is_ok(&answer);
323    }
324
325    #[test]
326    fn check_parse_schema_succeeds_basic_schema() {
327        let call = json!({
328          "MyNamespace": {
329            "entityTypes": {},
330            "actions": {}
331          }
332        });
333        let answer = serde_json::from_value(check_parse_schema_json(call).unwrap()).unwrap();
334        assert_check_parse_is_ok(&answer);
335    }
336
337    #[test]
338    fn check_parse_schema_fails() {
339        let call = json!({
340          "MyNamespace": {
341            "entityTypes": {}
342          }
343        });
344        let answer = serde_json::from_value(check_parse_schema_json(call).unwrap()).unwrap();
345        let errs = assert_check_parse_is_err(&answer);
346        assert_exactly_one_error(
347            errs,
348            "failed to parse schema from JSON: missing field `actions`",
349            None,
350        );
351    }
352
353    #[test]
354    fn check_parse_entities_succeeds() {
355        let call = json!({
356            "entities": [
357                {
358                    "uid": {
359                        "type": "TheNamespace::User",
360                        "id": "alice"
361                    },
362                    "attrs": {
363                        "department": "HardwareEngineering",
364                        "jobLevel": 5
365                    },
366                    "parents": []
367                }
368            ],
369            "schema": {
370                "TheNamespace": {
371                    "entityTypes": {
372                        "User": {
373                            "memberOfTypes": [],
374                            "shape": {
375                                "attributes": {
376                                    "department": {
377                                        "type": "String"
378                                    },
379                                    "jobLevel": {
380                                        "type": "Long"
381                                    }
382                                },
383                                "type": "Record"
384                            }
385                        }
386                    },
387                    "actions": {}
388                }
389            }
390        });
391        let answer = serde_json::from_value(check_parse_entities_json(call).unwrap()).unwrap();
392        assert_check_parse_is_ok(&answer);
393    }
394
395    #[test]
396    fn check_parse_entities_succeeds_with_no_schema() {
397        let call = json!({
398            "entities": [
399                {
400                    "uid": {
401                        "type": "TheNamespace::User",
402                        "id": "alice"
403                    },
404                    "attrs": {
405                        "department": "HardwareEngineering",
406                        "jobLevel": 5
407                    },
408                    "parents": []
409                }
410            ]
411        });
412        let answer = serde_json::from_value(check_parse_entities_json(call).unwrap()).unwrap();
413        assert_check_parse_is_ok(&answer);
414    }
415
416    #[test]
417    fn check_parse_entities_fails_on_bad_entity() {
418        let call = json!({
419            "entities": [
420                {
421                    "uid": "TheNamespace::User::\"alice\"",
422                    "attrs": {
423                        "benchPress": "doesn'tevenlift"
424                    },
425                    "parents": []
426                }
427            ],
428            "schema": {
429                "TheNamespace": {
430                    "entityTypes": {
431                        "User": {
432                            "memberOfTypes": [],
433                            "shape": {
434                                "attributes": {
435                                    "department": {
436                                        "type": "String"
437                                    }
438                                },
439                                "type": "Record"
440                            }
441                        }
442                    },
443                    "actions": {}
444                }
445            }
446        });
447        let answer = serde_json::from_value(check_parse_entities_json(call).unwrap()).unwrap();
448        let errs = assert_check_parse_is_err(&answer);
449        assert_exactly_one_error(
450            errs,
451            "error during entity deserialization: in uid field of <unknown entity>, expected a literal entity reference, but got `\"TheNamespace::User::\\\"alice\\\"\"`",
452            Some("literal entity references can be made with `{ \"type\": \"SomeType\", \"id\": \"SomeId\" }`")
453        );
454    }
455
456    #[test]
457    fn check_parse_context_succeeds() {
458        let call = json!({
459            "context": {
460                "referrer": "Morpheus"
461            },
462            "action": {
463                "type": "Ex::Action",
464                "id": "Join"
465            },
466            "schema": {
467                "Ex": {
468                    "entityTypes": {
469                        "User": {},
470                        "Folder": {}
471                    },
472                    "actions": {
473                        "Join": {
474                            "appliesTo": {
475                                "principalTypes": ["User"],
476                                "resourceTypes": ["Folder"],
477                                "context": {
478                                    "type": "Record",
479                                    "attributes": {
480                                        "referrer": {
481                                            "type": "String",
482                                            "required": true
483                                        }
484                                    }
485                                }
486                            }
487                        }
488                    }
489                }
490            }
491
492        });
493        let answer = serde_json::from_value(check_parse_context_json(call).unwrap()).unwrap();
494        assert_check_parse_is_ok(&answer);
495    }
496
497    #[test]
498    fn check_parse_context_fails_for_bad_context() {
499        let call = json!({
500            "context": {
501                "wrongAttr": true
502            },
503            "action": {
504                "type": "Ex::Action",
505                "id": "Join"
506            },
507            "schema": {
508                "Ex": {
509                    "entityTypes": {
510                        "User": {},
511                        "Folder": {}
512                    },
513                    "actions": {
514                        "Join": {
515                            "appliesTo": {
516                                "principalTypes" : ["User"],
517                                "resourceTypes": ["Folder"],
518                                "context": {
519                                    "type": "Record",
520                                    "attributes": {
521                                        "referrer": {
522                                            "type": "String",
523                                            "required": true
524                                        }
525                                    }
526                                }
527                            }
528                        }
529                    }
530                }
531            }
532        });
533        let answer = serde_json::from_value(check_parse_context_json(call).unwrap()).unwrap();
534        let errs = assert_check_parse_is_err(&answer);
535        assert_exactly_one_error(errs, "while parsing context, expected the record to have an attribute `referrer`, but it does not", None);
536    }
537}