use super::utils::JsonValueWithNoDuplicateKeys;
use super::{DetailedError, Policy, Schema, Template};
use crate::api::{PolicySet, StringifiedPolicySet};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::wasm_bindgen;
#[cfg(feature = "wasm")]
extern crate tsify;
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "policySetTextToParts"))]
pub fn policy_set_text_to_parts(policyset_str: &str) -> PolicySetTextToPartsAnswer {
let parsed_ps: Result<PolicySet, _> = PolicySet::from_str(policyset_str);
match parsed_ps {
Ok(policy_set) => {
if let Some(StringifiedPolicySet {
policies,
policy_templates,
}) = policy_set.stringify()
{
PolicySetTextToPartsAnswer::Success {
policies,
policy_templates,
}
} else {
PolicySetTextToPartsAnswer::Failure {
errors: vec![DetailedError::from_str(
"Policy set input contained template linked policies",
)
.unwrap_or_default()],
}
}
}
Err(e) => PolicySetTextToPartsAnswer::Failure {
errors: vec![(&e).into()],
},
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "policyToText"))]
pub fn policy_to_text(policy: Policy) -> PolicyToTextAnswer {
match policy.parse(None) {
Ok(policy) => PolicyToTextAnswer::Success {
text: policy.to_string(),
},
Err(e) => PolicyToTextAnswer::Failure {
errors: vec![e.into()],
},
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "templateToText"))]
pub fn template_to_text(template: Template) -> PolicyToTextAnswer {
match template.parse(None) {
Ok(template) => PolicyToTextAnswer::Success {
text: template.to_string(),
},
Err(e) => PolicyToTextAnswer::Failure {
errors: vec![e.into()],
},
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "policyToJson"))]
pub fn policy_to_json(policy: Policy) -> PolicyToJsonAnswer {
match policy.parse(None) {
Ok(policy) => match policy.to_json() {
Ok(json) => PolicyToJsonAnswer::Success { json: json.into() },
Err(e) => PolicyToJsonAnswer::Failure {
errors: vec![miette::Report::new(e).into()],
},
},
Err(e) => PolicyToJsonAnswer::Failure {
errors: vec![e.into()],
},
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "templateToJson"))]
pub fn template_to_json(template: Template) -> PolicyToJsonAnswer {
match template.parse(None) {
Ok(template) => match template.to_json() {
Ok(json) => PolicyToJsonAnswer::Success { json: json.into() },
Err(e) => PolicyToJsonAnswer::Failure {
errors: vec![miette::Report::new(e).into()],
},
},
Err(e) => PolicyToJsonAnswer::Failure {
errors: vec![e.into()],
},
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "schemaToText"))]
pub fn schema_to_text(schema: Schema) -> SchemaToTextAnswer {
match schema.parse_schema_fragment() {
Ok((schema_frag, warnings)) => {
match schema_frag.to_cedarschema() {
Ok(text) => {
if let Err(e) = TryInto::<crate::Schema>::try_into(schema_frag) {
SchemaToTextAnswer::Failure {
errors: vec![miette::Report::new(e).into()],
}
} else {
SchemaToTextAnswer::Success {
text,
warnings: warnings.map(|e| miette::Report::new(e).into()).collect(),
}
}
}
Err(e) => SchemaToTextAnswer::Failure {
errors: vec![miette::Report::new(e).into()],
},
}
}
Err(e) => SchemaToTextAnswer::Failure {
errors: vec![e.into()],
},
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "schemaToJson"))]
pub fn schema_to_json(schema: Schema) -> SchemaToJsonAnswer {
match schema.parse_schema_fragment() {
Ok((schema_frag, warnings)) => match schema_frag.to_json_value() {
Ok(json) => {
if let Err(e) = crate::Schema::from_json_value(json.clone()) {
SchemaToJsonAnswer::Failure {
errors: vec![miette::Report::new(e).into()],
}
} else {
SchemaToJsonAnswer::Success {
json: json.into(),
warnings: warnings.map(|e| miette::Report::new(e).into()).collect(),
}
}
}
Err(e) => SchemaToJsonAnswer::Failure {
errors: vec![miette::Report::new(e).into()],
},
},
Err(e) => SchemaToJsonAnswer::Failure {
errors: vec![e.into()],
},
}
}
#[cfg_attr(
feature = "wasm",
wasm_bindgen(js_name = "schemaToJsonWithResolvedTypes")
)]
pub fn schema_to_json_with_resolved_types(schema_str: &str) -> SchemaToJsonWithResolvedTypesAnswer {
match crate::api::schema_str_to_json_with_resolved_types(schema_str) {
Ok((json_value, warnings)) => SchemaToJsonWithResolvedTypesAnswer::Success {
json: json_value.into(),
warnings: warnings.iter().map(std::convert::Into::into).collect(),
},
Err(error) => {
SchemaToJsonWithResolvedTypesAnswer::Failure {
errors: vec![(&error).into()],
}
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum PolicyToTextAnswer {
Success {
text: String,
},
Failure {
errors: Vec<DetailedError>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum PolicySetTextToPartsAnswer {
Success {
policies: Vec<String>,
policy_templates: Vec<String>,
},
Failure {
errors: Vec<DetailedError>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum PolicyToJsonAnswer {
Success {
#[cfg_attr(feature = "wasm", tsify(type = "PolicyJson"))]
json: JsonValueWithNoDuplicateKeys,
},
Failure {
errors: Vec<DetailedError>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum SchemaToTextAnswer {
Success {
text: String,
warnings: Vec<DetailedError>,
},
Failure {
errors: Vec<DetailedError>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum SchemaToJsonAnswer {
Success {
#[cfg_attr(feature = "wasm", tsify(type = "SchemaJson<string>"))]
json: JsonValueWithNoDuplicateKeys,
warnings: Vec<DetailedError>,
},
Failure {
errors: Vec<DetailedError>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum SchemaToJsonWithResolvedTypesAnswer {
Success {
#[cfg_attr(feature = "wasm", tsify(type = "SchemaJson<string>"))]
json: JsonValueWithNoDuplicateKeys,
warnings: Vec<DetailedError>,
},
Failure {
errors: Vec<DetailedError>,
},
}
#[cfg(test)]
mod test {
use super::*;
use crate::ffi::test_utils::*;
use assert_json_diff::assert_json_eq;
use cool_asserts::assert_matches;
use serde_json::json;
#[test]
fn test_policy_to_json() {
let text = r#"
permit(principal, action, resource)
when { principal has "Email" && principal.Email == "a@a.com" };
"#;
let result = policy_to_json(Policy::Cedar(text.into()));
let expected = json!({
"effect": "permit",
"principal": {
"op": "All"
},
"action": {
"op": "All"
},
"resource": {
"op": "All"
},
"conditions": [
{
"kind": "when",
"body": {
"&&": {
"left": {
"has": {
"left": {
"Var": "principal"
},
"attr": "Email"
}
},
"right": {
"==": {
"left": {
".": {
"left": {
"Var": "principal"
},
"attr": "Email"
}
},
"right": {
"Value": "a@a.com"
}
}
}
}
}
}
]
});
assert_matches!(result, PolicyToJsonAnswer::Success { json } =>
assert_eq!(json, expected.into())
);
}
#[test]
fn test_policy_to_json_error() {
let text = r#"
permit(principal, action, resource)
when { principal has "Email" && principal.Email == };
"#;
let result = policy_to_json(Policy::Cedar(text.into()));
assert_matches!(result, PolicyToJsonAnswer::Failure { errors } => {
assert_exactly_one_error(
&errors,
"failed to parse policy from string: unexpected token `}`",
None,
);
});
}
#[test]
fn test_policy_to_text() {
let json = json!({
"effect": "permit",
"action": {
"entity": {
"id": "pop",
"type": "Action"
},
"op": "=="
},
"principal": {
"entity": {
"id": "DeathRowRecords",
"type": "UserGroup"
},
"op": "in"
},
"resource": {
"op": "All"
},
"conditions": []
});
let result = policy_to_text(Policy::Json(json.into()));
assert_matches!(result, PolicyToTextAnswer::Success { text } => {
assert_eq!(
&text,
"permit(principal in UserGroup::\"DeathRowRecords\", action == Action::\"pop\", resource);"
);
});
}
#[test]
fn test_template_to_json() {
let text = r"
permit(principal in ?principal, action, resource);
";
let result = template_to_json(Template::Cedar(text.into()));
let expected = json!({
"effect": "permit",
"principal": {
"op": "in",
"slot": "?principal"
},
"action": {
"op": "All"
},
"resource": {
"op": "All"
},
"conditions": []
});
assert_matches!(result, PolicyToJsonAnswer::Success { json } =>
assert_eq!(json, expected.into())
);
}
#[test]
fn test_template_to_text() {
let json = json!({
"effect": "permit",
"principal": {
"op": "All"
},
"action": {
"op": "All"
},
"resource": {
"op": "in",
"slot": "?resource"
},
"conditions": []
});
let result = template_to_text(Template::Json(json.into()));
assert_matches!(result, PolicyToTextAnswer::Success { text } => {
assert_eq!(
&text,
"permit(principal, action, resource in ?resource);"
);
});
}
#[test]
fn test_template_to_text_error() {
let json = json!({
"effect": "permit",
"action": {
"entity": {
"id": "pop",
"type": "Action"
},
"op": "=="
},
"principal": {
"entity": {
"id": "DeathRowRecords",
"type": "UserGroup"
},
"op": "in"
},
"resource": {
"op": "All"
},
"conditions": []
});
let result = template_to_text(Template::Json(json.into()));
assert_matches!(result, PolicyToTextAnswer::Failure { errors } => {
assert_exactly_one_error(
&errors,
"failed to parse template from JSON: error deserializing a policy/template from JSON: expected a template, got a static policy",
Some("a template should include slot(s) `?principal` or `?resource`"),
);
});
}
#[test]
fn test_schema_to_json() {
let text = r#"
entity User = { "name": String };
action sendMessage appliesTo {principal: User, resource: User};
"#;
let result = schema_to_json(Schema::Cedar(text.into()));
let expected = json!({
"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"name": {"type": "EntityOrCommon", "name": "String"} }
}
}
},
"actions": {
"sendMessage": {
"appliesTo": {
"resourceTypes": ["User"],
"principalTypes": ["User"]
}
}}
}
});
assert_matches!(result, SchemaToJsonAnswer::Success { json, warnings:_ } =>
assert_eq!(json, expected.into())
);
}
#[test]
fn test_schema_to_json_error() {
let text = r"
action sendMessage appliesTo {principal: User, resource: User};
";
let result = schema_to_json(Schema::Cedar(text.into()));
assert_matches!(result, SchemaToJsonAnswer::Failure { errors } => {
assert_exactly_one_error(
&errors,
"failed to resolve types: User, User",
Some("`User` has not been declared as an entity type"),
);
});
}
#[test]
fn test_schema_to_text() {
let json = json!({
"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"name": {"type": "String"}
}
}
}
},
"actions": {
"sendMessage": {
"appliesTo": {
"resourceTypes": ["User"],
"principalTypes": ["User"]
}
}}
}
});
let result = schema_to_text(Schema::Json(json.into()));
assert_matches!(result, SchemaToTextAnswer::Success { text, warnings:_ } => {
assert_eq!(
&text,
r#"entity User = {
name: __cedar::String
};
action "sendMessage" appliesTo {
principal: [User],
resource: [User],
context: {}
};
"#
);
});
}
#[test]
fn policy_set_to_text_to_parts() {
let policy_set_str = r#"
permit(principal, action, resource)
when { principal has "Email" && principal.Email == "a@a.com" };
permit(principal in UserGroup::"DeathRowRecords", action == Action::"pop", resource);
permit(principal in ?principal, action, resource);
"#;
let result = policy_set_text_to_parts(policy_set_str);
assert_matches!(result, PolicySetTextToPartsAnswer::Success { policies, policy_templates } => {
assert_eq!(policies.len(), 2);
assert_eq!(policy_templates.len(), 1);
});
}
#[test]
fn test_policy_set_text_to_parts_parse_failure() {
let invalid_input = "This is not a valid PolicySet string";
let result = policy_set_text_to_parts(invalid_input);
assert_matches!(result, PolicySetTextToPartsAnswer::Failure { errors } => {
assert_exactly_one_error(
&errors,
"unexpected token `is`",
None,
);
});
}
#[test]
fn test_schema_to_json_with_resolved_types() {
let schema_str = r#"
entity User = { "name": String };
action sendMessage appliesTo {principal: User, resource: User};
namespace MyApp {
entity AppUser = {
"name": __cedar::String
};
action view appliesTo {
principal: [AppUser],
resource: [AppUser],
context: {
prop1: Long
}
};
}
namespace MyApp2 {
entity AppUser = {
"name": __cedar::String
};
action view appliesTo {
principal: [AppUser],
resource: [AppUser],
context: {}
};
}
"#;
let result = schema_to_json_with_resolved_types(schema_str);
match result {
SchemaToJsonWithResolvedTypesAnswer::Success { json, warnings } => {
let json_value: serde_json::Value = json.into();
let json_str = serde_json::to_string(&json_value).unwrap();
assert!(!json_str.contains("EntityOrCommon"));
assert!(warnings.len() == 0);
let expected = json!({
"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"name": {"type": "String"}
}
}
}
},
"actions": {
"sendMessage": {
"appliesTo": {
"principalTypes": ["User"],
"resourceTypes": ["User"]
}
}
}
},
"MyApp": {
"entityTypes": {
"AppUser": {
"shape": {
"type": "Record",
"attributes": {
"name": {"type": "__cedar::String"}
}
}
}
},
"actions": {
"view": {
"appliesTo": {
"principalTypes": ["MyApp::AppUser"],
"resourceTypes": ["MyApp::AppUser"],
"context": {
"type": "Record",
"attributes": {
"prop1": {"type": "Long"}
}
}
}
}
}
},
"MyApp2": {
"entityTypes": {
"AppUser": {
"shape": {
"type": "Record",
"attributes": {
"name": {"type": "__cedar::String"}
}
}
}
},
"actions": {
"view": {
"appliesTo": {
"principalTypes": ["MyApp2::AppUser"],
"resourceTypes": ["MyApp2::AppUser"]
}
}
}
}
});
assert_json_eq!(json_value, expected);
}
SchemaToJsonWithResolvedTypesAnswer::Failure { errors } => {
panic!("Expected success but got errors: {:?}", errors);
}
}
}
#[test]
fn test_schema_to_json_with_resolved_types_simple() {
let schema_str = r#"
entity User;
entity Document;
action view appliesTo {principal: User, resource: Document};
"#;
let result = schema_to_json_with_resolved_types(schema_str);
match result {
SchemaToJsonWithResolvedTypesAnswer::Success { json, warnings } => {
let json_value: serde_json::Value = json.into();
let json_str = serde_json::to_string(&json_value).unwrap();
assert!(!json_str.contains("EntityOrCommon"));
assert!(warnings.len() == 0);
let expected = json!({
"": {
"entityTypes": {
"User": {},
"Document": {}
},
"actions": {
"view": {
"appliesTo": {
"principalTypes": ["User"],
"resourceTypes": ["Document"]
}
}
}
}
});
assert_json_eq!(json_value, expected);
}
SchemaToJsonWithResolvedTypesAnswer::Failure { errors } => {
panic!("Expected success but got errors: {:?}", errors);
}
}
}
#[test]
fn test_schema_to_json_with_resolved_types_common_type() {
let schema_str = r#"
type MyString = String;
entity User = { "name": MyString };
action sendMessage appliesTo {principal: User, resource: User};
"#;
let result = schema_to_json_with_resolved_types(schema_str);
match result {
SchemaToJsonWithResolvedTypesAnswer::Success { json, warnings } => {
let json_value: serde_json::Value = json.into();
let json_str = serde_json::to_string(&json_value).unwrap();
assert!(!json_str.contains("EntityOrCommon"));
assert!(warnings.len() == 0);
let expected = json!({
"": {
"commonTypes": {
"MyString": {"type": "String"}
},
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"name": {"type": "MyString"}
}
}
}
},
"actions": {
"sendMessage": {
"appliesTo": {
"principalTypes": ["User"],
"resourceTypes": ["User"]
}
}
}
}
});
assert_json_eq!(json_value, expected);
}
SchemaToJsonWithResolvedTypesAnswer::Failure { errors } => {
panic!("Expected success but got errors: {:?}", errors);
}
}
}
#[test]
fn test_shadowing_types() {
insta::glob!("test_schemas/shadowed_*.cedarschema", |path| {
let schema_str = std::fs::read_to_string(path).unwrap();
let result = schema_to_json_with_resolved_types(&schema_str);
match result {
SchemaToJsonWithResolvedTypesAnswer::Success { json, warnings } => {
let json_value: serde_json::Value = json.into();
let json_str = serde_json::to_string(&json_value).unwrap();
assert!(!json_str.contains("EntityOrCommon"));
assert!(warnings.len() > 0);
}
SchemaToJsonWithResolvedTypesAnswer::Failure { errors } => {
panic!("Expected success but got errors: {:?}", errors);
}
}
});
}
#[test]
fn test_schema_resolution_round_trip() {
insta::glob!("test_schemas/*.cedarschema", |path| {
let mut insta_settings = insta::Settings::clone_current();
insta_settings.set_sort_maps(true);
let file_name = path.file_stem().unwrap().to_str().unwrap();
insta_settings.set_snapshot_suffix(file_name);
let schema_str = std::fs::read_to_string(path).unwrap();
let validator_schema_direct_parse = match crate::Schema::from_str(&schema_str) {
Ok(schema) => schema,
Err(e) => panic!("Failed to parse original schema from {:?}: {:?}", path, e),
};
let result = schema_to_json_with_resolved_types(&schema_str);
let resolved_json = match result {
SchemaToJsonWithResolvedTypesAnswer::Success { json, .. } => json,
SchemaToJsonWithResolvedTypesAnswer::Failure { errors } => {
panic!(
"Failed to convert schema to JSON with resolved types for {:?}: {:?}",
path, errors
);
}
};
let resolved_json_stringified = serde_json::to_string_pretty(&resolved_json)
.expect("Serialization of json failed!");
insta_settings.bind(|| {
insta::assert_json_snapshot!(resolved_json);
});
let validator_schema_round_tripped =
match crate::Schema::from_json_str(&resolved_json_stringified) {
Ok(schema) => schema,
Err(e) => panic!(
"Failed to parse resolved JSON back to schema for {:?}: {:?}",
path, e
),
};
let entity_types_v1: std::collections::HashSet<_> =
validator_schema_direct_parse.entity_types().collect();
let entity_types_v2: std::collections::HashSet<_> =
validator_schema_round_tripped.entity_types().collect();
assert_eq!(
entity_types_v1, entity_types_v2,
"Entity types should be identical for {:?}",
path
);
let action_ids_v1: std::collections::HashSet<_> =
validator_schema_direct_parse.actions().collect();
let action_ids_v2: std::collections::HashSet<_> =
validator_schema_round_tripped.actions().collect();
assert_eq!(
action_ids_v1, action_ids_v2,
"Action definitions should be identical for {:?}",
path
);
});
}
}