1use 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#[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#[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#[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#[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#[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 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#[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 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#[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 Success {
155 text: String,
157 },
158 Failure {
160 errors: Vec<DetailedError>,
162 },
163}
164
165#[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 Success {
174 #[cfg_attr(feature = "wasm", tsify(type = "PolicyJson"))]
176 json: JsonValueWithNoDuplicateKeys,
177 },
178 Failure {
180 errors: Vec<DetailedError>,
182 },
183}
184
185#[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 Success {
194 text: String,
196 warnings: Vec<DetailedError>,
198 },
199 Failure {
201 errors: Vec<DetailedError>,
203 },
204}
205
206#[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 Success {
215 #[cfg_attr(feature = "wasm", tsify(type = "SchemaJson<string>"))]
217 json: JsonValueWithNoDuplicateKeys,
218 warnings: Vec<DetailedError>,
220 },
221 Failure {
223 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"} }
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}