cedar_policy/ffi/
format.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 the Cedar policy formatter. The Cedar Wasm
18//! formatter is generated from the [`format()`] function in this file.
19
20#![allow(clippy::module_name_repetitions)]
21
22use super::utils::DetailedError;
23use cedar_policy_formatter::{policies_str_to_pretty, Config};
24use serde::{Deserialize, Serialize};
25#[cfg(feature = "wasm")]
26use wasm_bindgen::prelude::wasm_bindgen;
27
28#[cfg(feature = "wasm")]
29extern crate tsify;
30
31/// Apply the Cedar policy formatter to a policy set in the Cedar policy format
32#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "formatPolicies"))]
33#[allow(clippy::needless_pass_by_value)]
34pub fn format(call: FormattingCall) -> FormattingAnswer {
35    let config = Config {
36        line_width: call.line_width,
37        indent_width: call.indent_width,
38    };
39    match policies_str_to_pretty(&call.policy_text, &config) {
40        Ok(prettified_policy) => FormattingAnswer::Success {
41            formatted_policy: prettified_policy,
42        },
43        Err(err) => FormattingAnswer::Failure {
44            errors: vec![err.into()],
45        },
46    }
47}
48
49/// Apply the Cedar policy formatter. Input is a JSON encoding of
50/// [`FormattingCall`] and output is a JSON encoding of [`FormattingAnswer`].
51///
52/// # Errors
53///
54/// Will return `Err` if the input JSON cannot be deserialized as a
55/// [`FormattingCall`].
56pub fn format_json(json: serde_json::Value) -> Result<serde_json::Value, serde_json::Error> {
57    let ans = format(serde_json::from_value(json)?);
58    serde_json::to_value(ans)
59}
60
61/// Apply the Cedar policy formatter. Input and output are strings containing
62/// serialized JSON, in the shapes expected by [`format_json()`].
63///
64/// # Errors
65///
66/// Will return `Err` if the input cannot be converted to valid JSON or
67/// deserialized as a [`FormattingCall`].
68pub fn format_json_str(json: &str) -> Result<String, serde_json::Error> {
69    let ans = format(serde_json::from_str(json)?);
70    serde_json::to_string(&ans)
71}
72
73/// Struct containing the input data for formatting
74#[derive(Serialize, Deserialize, Debug)]
75#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
76#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
77#[serde(rename_all = "camelCase")]
78#[serde(deny_unknown_fields)]
79pub struct FormattingCall {
80    /// Policy text. May define multiple policies or templates in the Cedar policy format.
81    policy_text: String,
82    /// Line width (default is 80)
83    #[serde(default = "default_line_width")]
84    line_width: usize,
85    /// Indentation width (default is 2)
86    #[serde(default = "default_indent_width")]
87    indent_width: isize,
88}
89
90const fn default_line_width() -> usize {
91    80
92}
93const fn default_indent_width() -> isize {
94    2
95}
96
97/// Result struct for formatting
98#[derive(Debug, Serialize, Deserialize)]
99#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
100#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
101#[serde(tag = "type")]
102#[serde(rename_all = "camelCase")]
103pub enum FormattingAnswer {
104    /// Represents a failure to call the formatter
105    Failure {
106        /// Policy parse errors
107        errors: Vec<DetailedError>,
108    },
109    /// Represents a successful formatting call
110    Success {
111        /// Formatted policy
112        formatted_policy: String,
113    },
114}
115
116// PANIC SAFETY unit tests
117#[allow(clippy::panic, clippy::indexing_slicing)]
118#[cfg(test)]
119mod test {
120    use super::*;
121
122    use crate::ffi::test_utils::*;
123    use cool_asserts::assert_matches;
124    use serde_json::json;
125
126    /// Assert that [`format_json()`] returns [`FormattingAnswer::Success`] and
127    /// get the formatted policy
128    #[track_caller]
129    fn assert_format_succeeds(json: serde_json::Value) -> String {
130        let ans_val = format_json(json).unwrap();
131        let result: Result<FormattingAnswer, _> = serde_json::from_value(ans_val);
132        assert_matches!(result, Ok(FormattingAnswer::Success { formatted_policy }) => {
133            formatted_policy
134        })
135    }
136
137    /// Assert that [`format_json()`] returns [`FormattingAnswer::Failure`] and
138    /// return the enclosed errors
139    #[track_caller]
140    fn assert_format_fails(json: serde_json::Value) -> Vec<DetailedError> {
141        let ans_val = format_json(json).unwrap();
142        let result: Result<FormattingAnswer, _> = serde_json::from_value(ans_val);
143        assert_matches!(result, Ok(FormattingAnswer::Failure { errors }) => errors)
144    }
145
146    #[test]
147    fn test_format_succeeds() {
148        let json = json!({
149        "policyText": "permit(principal in UserGroup::\"alice_friends\", action == Action::\"viewPhoto\", resource);",
150        "lineWidth": 100,
151        "indentWidth": 4,
152        });
153
154        let result = assert_format_succeeds(json);
155        assert_eq!(result, "permit (\n    principal in UserGroup::\"alice_friends\",\n    action == Action::\"viewPhoto\",\n    resource\n);\n");
156    }
157
158    #[test]
159    fn test_format_succeed_default_values() {
160        let json = json!({
161        "policyText": "permit(principal in UserGroup::\"alice_friends\", action == Action::\"viewPhoto\", resource);",
162        });
163
164        let result = assert_format_succeeds(json);
165        assert_eq!(result, "permit (\n  principal in UserGroup::\"alice_friends\",\n  action == Action::\"viewPhoto\",\n  resource\n);\n");
166    }
167
168    #[test]
169    fn test_format_fails() {
170        let json = json!({
171        "policyText": "foo(principal in UserGroup::\"alice_friends\", action == Action::\"viewPhoto\", resource);",
172        "lineWidth": 100,
173        "indentWidth": 4,
174        });
175
176        let errs = assert_format_fails(json);
177        assert_exactly_one_error(
178            &errs,
179            "cannot parse input policies: invalid policy effect: foo",
180            Some("effect must be either `permit` or `forbid`"),
181        );
182    }
183}