1use super::utils::JsonValueWithNoDuplicateKeys;
22use super::{DetailedError, Policy, Schema, Template};
23use crate::api::{PolicySet, StringifiedPolicySet};
24use serde::{Deserialize, Serialize};
25use std::str::FromStr;
26#[cfg(feature = "wasm")]
27use wasm_bindgen::prelude::wasm_bindgen;
28
29#[cfg(feature = "wasm")]
30extern crate tsify;
31
32#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "policySetTextToParts"))]
35pub fn policy_set_text_to_parts(policyset_str: &str) -> PolicySetTextToPartsAnswer {
36 let parsed_ps: Result<PolicySet, _> = PolicySet::from_str(policyset_str);
37 match parsed_ps {
38 Ok(policy_set) => {
39 if let Some(StringifiedPolicySet {
40 policies,
41 policy_templates,
42 }) = policy_set.stringify()
43 {
44 PolicySetTextToPartsAnswer::Success {
45 policies,
46 policy_templates,
47 }
48 } else {
49 PolicySetTextToPartsAnswer::Failure {
52 errors: vec![DetailedError::from_str(
53 "Policy set input contained template linked policies",
54 )
55 .unwrap_or_default()],
56 }
57 }
58 }
59 Err(e) => PolicySetTextToPartsAnswer::Failure {
60 errors: vec![(&e).into()],
61 },
62 }
63}
64
65#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "policyToText"))]
67pub fn policy_to_text(policy: Policy) -> PolicyToTextAnswer {
68 match policy.parse(None) {
69 Ok(policy) => PolicyToTextAnswer::Success {
70 text: policy.to_string(),
71 },
72 Err(e) => PolicyToTextAnswer::Failure {
73 errors: vec![e.into()],
74 },
75 }
76}
77
78#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "templateToText"))]
80pub fn template_to_text(template: Template) -> PolicyToTextAnswer {
81 match template.parse(None) {
82 Ok(template) => PolicyToTextAnswer::Success {
83 text: template.to_string(),
84 },
85 Err(e) => PolicyToTextAnswer::Failure {
86 errors: vec![e.into()],
87 },
88 }
89}
90
91#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "policyToJson"))]
93pub fn policy_to_json(policy: Policy) -> PolicyToJsonAnswer {
94 match policy.parse(None) {
95 Ok(policy) => match policy.to_json() {
96 Ok(json) => PolicyToJsonAnswer::Success { json: json.into() },
97 Err(e) => PolicyToJsonAnswer::Failure {
98 errors: vec![miette::Report::new(e).into()],
99 },
100 },
101 Err(e) => PolicyToJsonAnswer::Failure {
102 errors: vec![e.into()],
103 },
104 }
105}
106
107#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "templateToJson"))]
109pub fn template_to_json(template: Template) -> PolicyToJsonAnswer {
110 match template.parse(None) {
111 Ok(template) => match template.to_json() {
112 Ok(json) => PolicyToJsonAnswer::Success { json: json.into() },
113 Err(e) => PolicyToJsonAnswer::Failure {
114 errors: vec![miette::Report::new(e).into()],
115 },
116 },
117 Err(e) => PolicyToJsonAnswer::Failure {
118 errors: vec![e.into()],
119 },
120 }
121}
122
123#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "schemaToText"))]
125pub fn schema_to_text(schema: Schema) -> SchemaToTextAnswer {
126 match schema.parse_schema_fragment() {
127 Ok((schema_frag, warnings)) => {
128 match schema_frag.to_cedarschema() {
129 Ok(text) => {
130 if let Err(e) = TryInto::<crate::Schema>::try_into(schema_frag) {
132 SchemaToTextAnswer::Failure {
133 errors: vec![miette::Report::new(e).into()],
134 }
135 } else {
136 SchemaToTextAnswer::Success {
137 text,
138 warnings: warnings.map(|e| miette::Report::new(e).into()).collect(),
139 }
140 }
141 }
142 Err(e) => SchemaToTextAnswer::Failure {
143 errors: vec![miette::Report::new(e).into()],
144 },
145 }
146 }
147 Err(e) => SchemaToTextAnswer::Failure {
148 errors: vec![e.into()],
149 },
150 }
151}
152
153#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "schemaToJson"))]
155pub fn schema_to_json(schema: Schema) -> SchemaToJsonAnswer {
156 match schema.parse_schema_fragment() {
157 Ok((schema_frag, warnings)) => match schema_frag.to_json_value() {
158 Ok(json) => {
159 if let Err(e) = crate::Schema::from_json_value(json.clone()) {
161 SchemaToJsonAnswer::Failure {
162 errors: vec![miette::Report::new(e).into()],
163 }
164 } else {
165 SchemaToJsonAnswer::Success {
166 json: json.into(),
167 warnings: warnings.map(|e| miette::Report::new(e).into()).collect(),
168 }
169 }
170 }
171 Err(e) => SchemaToJsonAnswer::Failure {
172 errors: vec![miette::Report::new(e).into()],
173 },
174 },
175 Err(e) => SchemaToJsonAnswer::Failure {
176 errors: vec![e.into()],
177 },
178 }
179}
180
181#[derive(Debug, Serialize, Deserialize)]
183#[serde(tag = "type")]
184#[serde(rename_all = "camelCase")]
185#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
186#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
187pub enum PolicyToTextAnswer {
188 Success {
190 text: String,
192 },
193 Failure {
195 errors: Vec<DetailedError>,
197 },
198}
199
200#[derive(Debug, Serialize, Deserialize)]
203#[serde(tag = "type")]
204#[serde(rename_all = "camelCase")]
205#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
206#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
207pub enum PolicySetTextToPartsAnswer {
208 Success {
210 policies: Vec<String>,
212 policy_templates: Vec<String>,
214 },
215 Failure {
217 errors: Vec<DetailedError>,
219 },
220}
221
222#[derive(Debug, Serialize, Deserialize)]
224#[serde(tag = "type")]
225#[serde(rename_all = "camelCase")]
226#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
227#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
228pub enum PolicyToJsonAnswer {
229 Success {
231 #[cfg_attr(feature = "wasm", tsify(type = "PolicyJson"))]
233 json: JsonValueWithNoDuplicateKeys,
234 },
235 Failure {
237 errors: Vec<DetailedError>,
239 },
240}
241
242#[derive(Debug, Serialize, Deserialize)]
244#[serde(tag = "type")]
245#[serde(rename_all = "camelCase")]
246#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
247#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
248pub enum SchemaToTextAnswer {
249 Success {
251 text: String,
253 warnings: Vec<DetailedError>,
255 },
256 Failure {
258 errors: Vec<DetailedError>,
260 },
261}
262
263#[derive(Debug, Serialize, Deserialize)]
265#[serde(tag = "type")]
266#[serde(rename_all = "camelCase")]
267#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
268#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
269pub enum SchemaToJsonAnswer {
270 Success {
272 #[cfg_attr(feature = "wasm", tsify(type = "SchemaJson<string>"))]
274 json: JsonValueWithNoDuplicateKeys,
275 warnings: Vec<DetailedError>,
277 },
278 Failure {
280 errors: Vec<DetailedError>,
282 },
283}
284
285#[cfg(test)]
286mod test {
287 use super::*;
288
289 use crate::ffi::test_utils::*;
290 use cool_asserts::assert_matches;
291 use serde_json::json;
292
293 #[test]
294 fn test_policy_to_json() {
295 let text = r#"
296 permit(principal, action, resource)
297 when { principal has "Email" && principal.Email == "a@a.com" };
298 "#;
299 let result = policy_to_json(Policy::Cedar(text.into()));
300 let expected = json!({
301 "effect": "permit",
302 "principal": {
303 "op": "All"
304 },
305 "action": {
306 "op": "All"
307 },
308 "resource": {
309 "op": "All"
310 },
311 "conditions": [
312 {
313 "kind": "when",
314 "body": {
315 "&&": {
316 "left": {
317 "has": {
318 "left": {
319 "Var": "principal"
320 },
321 "attr": "Email"
322 }
323 },
324 "right": {
325 "==": {
326 "left": {
327 ".": {
328 "left": {
329 "Var": "principal"
330 },
331 "attr": "Email"
332 }
333 },
334 "right": {
335 "Value": "a@a.com"
336 }
337 }
338 }
339 }
340 }
341 }
342 ]
343 });
344 assert_matches!(result, PolicyToJsonAnswer::Success { json } =>
345 assert_eq!(json, expected.into())
346 );
347 }
348
349 #[test]
350 fn test_policy_to_json_error() {
351 let text = r#"
352 permit(principal, action, resource)
353 when { principal has "Email" && principal.Email == };
354 "#;
355 let result = policy_to_json(Policy::Cedar(text.into()));
356 assert_matches!(result, PolicyToJsonAnswer::Failure { errors } => {
357 assert_exactly_one_error(
358 &errors,
359 "failed to parse policy from string: unexpected token `}`",
360 None,
361 );
362 });
363 }
364
365 #[test]
366 fn test_policy_to_text() {
367 let json = json!({
368 "effect": "permit",
369 "action": {
370 "entity": {
371 "id": "pop",
372 "type": "Action"
373 },
374 "op": "=="
375 },
376 "principal": {
377 "entity": {
378 "id": "DeathRowRecords",
379 "type": "UserGroup"
380 },
381 "op": "in"
382 },
383 "resource": {
384 "op": "All"
385 },
386 "conditions": []
387 });
388 let result = policy_to_text(Policy::Json(json.into()));
389 assert_matches!(result, PolicyToTextAnswer::Success { text } => {
390 assert_eq!(
391 &text,
392 "permit(principal in UserGroup::\"DeathRowRecords\", action == Action::\"pop\", resource);"
393 );
394 });
395 }
396
397 #[test]
398 fn test_template_to_json() {
399 let text = r"
400 permit(principal in ?principal, action, resource);
401 ";
402 let result = template_to_json(Template::Cedar(text.into()));
403 let expected = json!({
404 "effect": "permit",
405 "principal": {
406 "op": "in",
407 "slot": "?principal"
408 },
409 "action": {
410 "op": "All"
411 },
412 "resource": {
413 "op": "All"
414 },
415 "conditions": []
416 });
417 assert_matches!(result, PolicyToJsonAnswer::Success { json } =>
418 assert_eq!(json, expected.into())
419 );
420 }
421
422 #[test]
423 fn test_template_to_text() {
424 let json = json!({
425 "effect": "permit",
426 "principal": {
427 "op": "All"
428 },
429 "action": {
430 "op": "All"
431 },
432 "resource": {
433 "op": "in",
434 "slot": "?resource"
435 },
436 "conditions": []
437 });
438 let result = template_to_text(Template::Json(json.into()));
439 assert_matches!(result, PolicyToTextAnswer::Success { text } => {
440 assert_eq!(
441 &text,
442 "permit(principal, action, resource in ?resource);"
443 );
444 });
445 }
446
447 #[test]
448 fn test_template_to_text_error() {
449 let json = json!({
450 "effect": "permit",
451 "action": {
452 "entity": {
453 "id": "pop",
454 "type": "Action"
455 },
456 "op": "=="
457 },
458 "principal": {
459 "entity": {
460 "id": "DeathRowRecords",
461 "type": "UserGroup"
462 },
463 "op": "in"
464 },
465 "resource": {
466 "op": "All"
467 },
468 "conditions": []
469 });
470 let result = template_to_text(Template::Json(json.into()));
471 assert_matches!(result, PolicyToTextAnswer::Failure { errors } => {
472 assert_exactly_one_error(
473 &errors,
474 "failed to parse template from JSON: error deserializing a policy/template from JSON: expected a template, got a static policy",
475 Some("a template should include slot(s) `?principal` or `?resource`"),
476 );
477 });
478 }
479
480 #[test]
481 fn test_schema_to_json() {
482 let text = r#"
483 entity User = { "name": String };
484 action sendMessage appliesTo {principal: User, resource: User};
485 "#;
486 let result = schema_to_json(Schema::Cedar(text.into()));
487 let expected = json!({
488 "": {
489 "entityTypes": {
490 "User": {
491 "shape": {
492 "type": "Record",
493 "attributes": {
494 "name": {"type": "EntityOrCommon", "name": "String"} }
496 }
497 }
498 },
499 "actions": {
500 "sendMessage": {
501 "appliesTo": {
502 "resourceTypes": ["User"],
503 "principalTypes": ["User"]
504 }
505 }}
506 }
507 });
508 assert_matches!(result, SchemaToJsonAnswer::Success { json, warnings:_ } =>
509 assert_eq!(json, expected.into())
510 );
511 }
512
513 #[test]
514 fn test_schema_to_json_error() {
515 let text = r"
516 action sendMessage appliesTo {principal: User, resource: User};
517 ";
518 let result = schema_to_json(Schema::Cedar(text.into()));
519 assert_matches!(result, SchemaToJsonAnswer::Failure { errors } => {
520 assert_exactly_one_error(
521 &errors,
522 "failed to resolve types: User, User",
523 Some("`User` has not been declared as an entity type"),
524 );
525 });
526 }
527
528 #[test]
529 fn test_schema_to_text() {
530 let json = json!({
531 "": {
532 "entityTypes": {
533 "User": {
534 "shape": {
535 "type": "Record",
536 "attributes": {
537 "name": {"type": "String"}
538 }
539 }
540 }
541 },
542 "actions": {
543 "sendMessage": {
544 "appliesTo": {
545 "resourceTypes": ["User"],
546 "principalTypes": ["User"]
547 }
548 }}
549 }
550 });
551 let result = schema_to_text(Schema::Json(json.into()));
552 assert_matches!(result, SchemaToTextAnswer::Success { text, warnings:_ } => {
553 assert_eq!(
554 &text,
555 r#"entity User = {
556 "name": __cedar::String
557};
558
559action "sendMessage" appliesTo {
560 principal: [User],
561 resource: [User],
562 context: {}
563};
564"#
565 );
566 });
567 }
568
569 #[test]
570 fn policy_set_to_text_to_parts() {
571 let policy_set_str = r#"
572 permit(principal, action, resource)
573 when { principal has "Email" && principal.Email == "a@a.com" };
574
575 permit(principal in UserGroup::"DeathRowRecords", action == Action::"pop", resource);
576
577 permit(principal in ?principal, action, resource);
578 "#;
579
580 let result = policy_set_text_to_parts(policy_set_str);
581 assert_matches!(result, PolicySetTextToPartsAnswer::Success { policies, policy_templates } => {
582 assert_eq!(policies.len(), 2);
583 assert_eq!(policy_templates.len(), 1);
584 });
585 }
586
587 #[test]
588 fn test_policy_set_text_to_parts_parse_failure() {
589 let invalid_input = "This is not a valid PolicySet string";
590
591 let result = policy_set_text_to_parts(invalid_input);
592
593 assert_matches!(result, PolicySetTextToPartsAnswer::Failure { errors } => {
594 assert_exactly_one_error(
595 &errors,
596 "unexpected token `is`",
597 None,
598 );
599 });
600 }
601}