diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java b/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java
index d11bb16e..7a35e951 100644
--- a/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java
+++ b/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java
@@ -16,13 +16,15 @@
package com.cedarpolicy.model.schema;
+import java.util.Optional;
+
import com.cedarpolicy.loader.LibraryLoader;
import com.cedarpolicy.model.exception.InternalException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import java.util.Optional;
-
/** Represents a schema. */
public final class Schema {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@@ -122,16 +124,58 @@ public static Schema parse(JsonOrCedar type, String str) throws InternalExceptio
}
+ /**
+ * Converts a schema to Cedar format
+ *
+ * @return String representing the schema in Cedar format
+ * @throws InternalException If conversion from JSON to Cedar format fails
+ * @throws IllegalStateException If schema content is missing
+ * @throws NullPointerException
+ */
+ public String toCedarFormat() throws InternalException, IllegalStateException, NullPointerException {
+ if (type == JsonOrCedar.Cedar && schemaText.isPresent()) {
+ return schemaText.get();
+ } else if (type == JsonOrCedar.Json && schemaJson.isPresent()) {
+ return jsonToCedarJni(schemaJson.get().toString());
+ } else {
+ throw new IllegalStateException("No schema found");
+ }
+ }
+
+ /**
+ * Converts a Cedar format schema to JSON format
+ *
+ * @return JsonNode representing the schema in JSON format
+ * @throws InternalException If conversion from Cedar to JSON format fails
+ * @throws IllegalStateException If schema content is missing
+ * @throws JsonMappingException If invalid JSON
+ * @throws JsonProcessingException If invalid JSON
+ * @throws NullPointerException
+ */
+ public JsonNode toJsonFormat()
+ throws InternalException, JsonMappingException, JsonProcessingException, NullPointerException,
+ IllegalStateException {
+ if (type == JsonOrCedar.Json && schemaJson.isPresent()) {
+ return schemaJson.get();
+ } else if (type == JsonOrCedar.Cedar && schemaText.isPresent()) {
+ return OBJECT_MAPPER.readTree(cedarToJsonJni(schemaText.get()));
+ } else {
+ throw new IllegalStateException("No schema found");
+ }
+ }
+
/** Specifies the schema format used. */
public enum JsonOrCedar {
/**
- * Cedar JSON schema format. See
- * https://docs.cedarpolicy.com/schema/json-schema.html
+ * Cedar JSON schema format. See
+ *
+ * https://docs.cedarpolicy.com/schema/json-schema.html
*/
Json,
/**
- * Cedar schema format. See
- * https://docs.cedarpolicy.com/schema/human-readable-schema.html
+ * Cedar schema format. See
+ *
+ * https://docs.cedarpolicy.com/schema/human-readable-schema.html
*/
Cedar
}
@@ -139,4 +183,8 @@ public enum JsonOrCedar {
private static native String parseJsonSchemaJni(String schemaJson) throws InternalException, NullPointerException;
private static native String parseCedarSchemaJni(String schemaText) throws InternalException, NullPointerException;
+
+ private static native String jsonToCedarJni(String json) throws InternalException, NullPointerException;
+
+ private static native String cedarToJsonJni(String cedar) throws InternalException, NullPointerException;
}
diff --git a/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java b/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java
index 5657c793..1883e9a2 100644
--- a/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java
+++ b/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java
@@ -16,13 +16,21 @@
package com.cedarpolicy;
-import com.cedarpolicy.model.schema.Schema;
-import com.cedarpolicy.model.schema.Schema.JsonOrCedar;
-
-import org.junit.jupiter.api.Test;
+import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import com.cedarpolicy.model.exception.InternalException;
+import com.cedarpolicy.model.schema.Schema;
+import com.cedarpolicy.model.schema.Schema.JsonOrCedar;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
public class SchemaTests {
@Test
@@ -107,4 +115,124 @@ public void parseCedarSchema() {
Schema.parse(JsonOrCedar.Cedar, "namspace Foo::Bar;");
});
}
+
+ @Nested
+ @DisplayName("toCedarFormat Tests")
+ class ToCedarFormatTests {
+
+ @Test
+ @DisplayName("Should return the same Cedar schema text")
+ void testFromCedar() throws InternalException {
+ String cedarSchema = "entity User;";
+ Schema cedarSchemaObj = new Schema(cedarSchema);
+ String result = cedarSchemaObj.toCedarFormat();
+ assertNotNull(result, "Result should not be null");
+ assertEquals(cedarSchema, result, "Should return the original Cedar schema");
+ }
+
+ @Test
+ @DisplayName("Should convert JSON schema to Cedar format")
+ void testFromJson() throws InternalException {
+ String jsonSchema = """
+ {
+ "": {
+ "entityTypes": {
+ "User": {}
+ },
+ "actions": {}
+ }
+ }
+ """;
+ Schema jsonSchemaObj = Schema.parse(JsonOrCedar.Json, jsonSchema);
+ String result = jsonSchemaObj.toCedarFormat();
+
+ assertNotNull(result, "Result should not be null");
+ String expectedCedar = "entity User;";
+ assertEquals(expectedCedar, result.trim(), "Converted Cedar should match expected format");
+ }
+
+ @Test
+ @DisplayName("Should throw IllegalStateException for empty schema")
+ void testEmptySchema() {
+ Schema emptySchema = new Schema(JsonOrCedar.Cedar, Optional.empty(), Optional.empty());
+ Exception exception = assertThrows(IllegalStateException.class, emptySchema::toCedarFormat);
+ assertEquals("No schema found", exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should throw exception for malformed JSON schema")
+ void testMalformedSchema() {
+ String malformedJson = """
+ {
+ "": {
+ "entityMalformedTypes": {
+ "User": {}
+ },
+ "actions": {}
+ }
+ }
+ """;
+ Schema malformedSchema = new Schema(JsonOrCedar.Json, Optional.of(malformedJson), Optional.empty());
+ assertNotNull(malformedSchema.schemaJson);
+ assertThrows(InternalException.class, malformedSchema::toCedarFormat);
+ }
+ }
+
+ @Nested
+ @DisplayName("toJsonFormat Tests")
+ class ToJsonFormatTests {
+
+ @Test
+ @DisplayName("Should convert Cedar schema to JSON format")
+ void testFromCedar() throws Exception {
+ String cedarSchema = "entity User;";
+ Schema cedarSchemaObj = new Schema(cedarSchema);
+ JsonNode result = cedarSchemaObj.toJsonFormat();
+
+ String expectedJson = "{\"\":{\"entityTypes\":{\"User\":{}},\"actions\":{}}}";
+ JsonNode expectedNode = new ObjectMapper().readTree(expectedJson);
+
+ assertNotNull(result, "Result should not be null");
+ assertEquals(expectedNode, result, "JSON should match expected structure");
+ }
+
+ @Test
+ @DisplayName("Should return the same JSON schema object")
+ void testFromJson() throws Exception {
+ String jsonSchema = """
+ {
+ "": {
+ "entityTypes": {
+ "User": {}
+ },
+ "actions": {}
+ }
+ }
+ """;
+ Schema jsonSchemaObj = Schema.parse(JsonOrCedar.Json, jsonSchema);
+ JsonNode result = jsonSchemaObj.toJsonFormat();
+
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode expectedNode = mapper.readTree(jsonSchema);
+
+ assertNotNull(result, "Result should not be null");
+ assertEquals(expectedNode, result, "JSON should match the original schema");
+ }
+
+ @Test
+ @DisplayName("Should throw IllegalStateException for empty schema")
+ void testEmptySchema() {
+ Schema emptySchema = new Schema(JsonOrCedar.Cedar, Optional.empty(), Optional.empty());
+ Exception exception = assertThrows(IllegalStateException.class, emptySchema::toJsonFormat);
+ assertEquals("No schema found", exception.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should throw exception for malformed Cedar schema")
+ void testMalformedSchema() {
+ String malformedCedar = "entty User";
+ Schema malformedSchema = new Schema(JsonOrCedar.Cedar, Optional.empty(), Optional.of(malformedCedar));
+ assertThrows(InternalException.class, malformedSchema::toJsonFormat);
+ }
+ }
}
diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs
index d0534fa4..8f88f3d3 100644
--- a/CedarJavaFFI/src/interface.rs
+++ b/CedarJavaFFI/src/interface.rs
@@ -16,6 +16,9 @@
use cedar_policy::entities_errors::EntitiesError;
#[cfg(feature = "partial-eval")]
use cedar_policy::ffi::is_authorized_partial_json_str;
+use cedar_policy::ffi::{
+ schema_to_json, schema_to_text, Schema as FFISchema, SchemaToJsonAnswer, SchemaToTextAnswer,
+};
use cedar_policy::{
ffi::{is_authorized_json_str, validate_json_str},
Entities, EntityUid, Policy, PolicySet, Schema, Template,
@@ -702,6 +705,72 @@ fn policies_str_to_pretty_internal<'a>(
}
}
}
+#[jni_fn("com.cedarpolicy.model.schema.Schema")]
+pub fn jsonToCedarJni<'a>(mut env: JNIEnv<'a>, _: JClass, json_schema: JString<'a>) -> jvalue {
+ match get_cedar_schema_internal(&mut env, json_schema) {
+ Ok(val) => val.as_jni(),
+ Err(e) => jni_failed(&mut env, e.as_ref()),
+ }
+}
+pub fn get_cedar_schema_internal<'a>(
+ env: &mut JNIEnv<'a>,
+ schema_json_jstr: JString<'a>,
+) -> Result> {
+ let rust_str = env.get_string(&schema_json_jstr)?;
+ let schema_str = rust_str.to_str()?;
+
+ let schema: FFISchema = serde_json::from_str(schema_str)?;
+ let cedar_format = schema_to_text(schema);
+
+ match cedar_format {
+ SchemaToTextAnswer::Success { text, warnings } => {
+ let jstr = env.new_string(&text)?;
+ Ok(JValueGen::Object(JObject::from(jstr)).into())
+ }
+ SchemaToTextAnswer::Failure { errors } => {
+ let joined_errors = errors
+ .iter()
+ .map(|e| e.message.clone())
+ .collect::>()
+ .join("; ");
+ Err(joined_errors.into())
+ }
+ }
+}
+
+#[jni_fn("com.cedarpolicy.model.schema.Schema")]
+pub fn cedarToJsonJni<'a>(mut env: JNIEnv<'a>, _: JClass, cedar_schema: JString<'a>) -> jvalue {
+ match get_json_schema_internal(&mut env, cedar_schema) {
+ Ok(val) => val.as_jni(),
+ Err(e) => jni_failed(&mut env, e.as_ref()),
+ }
+}
+
+pub fn get_json_schema_internal<'a>(
+ env: &mut JNIEnv<'a>,
+ cedar_schema_jstr: JString<'a>,
+) -> Result> {
+ let schema_jstr = env.get_string(&cedar_schema_jstr)?;
+ let schema_str = schema_jstr.to_str()?;
+ let cedar_schema_str = FFISchema::Cedar(schema_str.into());
+ let json_format = schema_to_json(cedar_schema_str);
+
+ match json_format {
+ SchemaToJsonAnswer::Success { json, warnings: _ } => {
+ let json_pretty = serde_json::to_string_pretty(&json)?;
+ let jstr = env.new_string(&json_pretty)?;
+ Ok(JValueGen::Object(JObject::from(jstr)).into())
+ }
+ SchemaToJsonAnswer::Failure { errors } => {
+ let joined_errors = errors
+ .iter()
+ .map(|e| e.message.clone())
+ .collect::>()
+ .join("; ");
+ Err(joined_errors.into())
+ }
+ }
+}
#[cfg(test)]
pub(crate) mod jvm_based_tests {
@@ -932,38 +1001,38 @@ pub(crate) mod jvm_based_tests {
let mut env = JVM.attach_current_thread().unwrap();
let policy_json = r#"
- {
- "effect": "permit",
- "principal": {
- "op": "==",
- "entity": { "type": "User", "id": "12UA45" }
- },
- "action": {
- "op": "==",
- "entity": { "type": "Action", "id": "view" }
- },
- "resource": {
- "op": "in",
- "entity": { "type": "Folder", "id": "abc" }
- },
- "conditions": [
- {
- "kind": "when",
- "body": {
- "==": {
- "left": {
- ".": {
- "left": { "Var": "context" },
- "attr": "tls_version"
+ {
+ "effect": "permit",
+ "principal": {
+ "op": "==",
+ "entity": { "type": "User", "id": "12UA45" }
+ },
+ "action": {
+ "op": "==",
+ "entity": { "type": "Action", "id": "view" }
+ },
+ "resource": {
+ "op": "in",
+ "entity": { "type": "Folder", "id": "abc" }
+ },
+ "conditions": [
+ {
+ "kind": "when",
+ "body": {
+ "==": {
+ "left": {
+ ".": {
+ "left": { "Var": "context" },
+ "attr": "tls_version"
+ }
+ },
+ "right": { "Value": "1.3" }
+ }
}
- },
- "right": { "Value": "1.3" }
- }
+ }
+ ]
}
- }
- ]
- }
- "#;
+ "#;
let java_str = env.new_string(policy_json).unwrap();
let result = from_json_internal(&mut env, java_str);
@@ -1215,24 +1284,24 @@ pub(crate) mod jvm_based_tests {
fn parse_json_schema_internal_valid_test() {
let mut env = JVM.attach_current_thread().unwrap();
let input = r#"{
- "schema": {
- "entityTypes": {
- "User": {
- "memberOfTypes": ["Group"]
- },
- "Group": {},
- "File": {}
- },
- "actions": {
- "read": {
- "appliesTo": {
- "principalTypes": ["User"],
- "resourceTypes": ["File"]
+ "schema": {
+ "entityTypes": {
+ "User": {
+ "memberOfTypes": ["Group"]
+ },
+ "Group": {},
+ "File": {}
+ },
+ "actions": {
+ "read": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["File"]
+ }
+ }
+ }
}
- }
- }
- }
-}"#;
+ }"#;
let jstr = env.new_string(input).unwrap();
let result = parse_json_schema_internal(&mut env, jstr);
assert!(result.is_ok(), "Expected schema to parse successfully");
@@ -1248,24 +1317,24 @@ pub(crate) mod jvm_based_tests {
fn parse_json_schema_internal_invalid_test() {
let mut env = JVM.attach_current_thread().unwrap();
let invalid_input = r#"{
- "Schema": {
- "entityTypes": {
- "User": {
- "MemberOfTypes": ["Group"]
- },
- "Group": {},
- "File": {}
- },
- "Actions": {
- "read": {
- "AppliesTo": {
- "principalTypes": ["User"],
- "AesourceTypes": ["File"]
+ "Schema": {
+ "entityTypes": {
+ "User": {
+ "MemberOfTypes": ["Group"]
+ },
+ "Group": {},
+ "File": {}
+ },
+ "Actions": {
+ "read": {
+ "AppliesTo": {
+ "principalTypes": ["User"],
+ "AesourceTypes": ["File"]
+ }
+ }
+ }
}
- }
- }
- }
-}"#;
+ }"#;
let jstr = env.new_string(invalid_input).unwrap();
let result = parse_json_schema_internal(&mut env, jstr);
@@ -1351,4 +1420,161 @@ pub(crate) mod jvm_based_tests {
);
}
}
+ mod conversion_tests {
+ use super::*;
+
+ #[test]
+ fn get_cedar_schema_internal_valid() {
+ let mut env = JVM.attach_current_thread().unwrap();
+ let json_input = r#"{
+ "schema": {
+ "entityTypes": {
+ "User": {
+ "memberOfTypes": ["Group"]
+ },
+ "Group": {},
+ "File": {}
+ },
+ "actions": {
+ "read": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["File"]
+ }
+ }
+ }
+ }
+ }"#;
+
+ let jstr = env.new_string(json_input).unwrap();
+ let result = get_cedar_schema_internal(&mut env, jstr);
+ assert!(result.is_ok(), "Expected Cedar conversion to succeed");
+
+ let cedar_jval = result.unwrap();
+ let cedar_jstr = JString::cast(&mut env, cedar_jval.l().unwrap()).unwrap();
+ let cedar_str = String::from(env.get_string(&cedar_jstr).unwrap());
+
+ let expected_cedar = "namespace schema {\n entity File;\n\n entity Group;\n\n entity User in [Group];\n\n action \"read\" appliesTo {\n principal: [User],\n resource: [File],\n context: {}\n };\n}";
+ assert_eq!(
+ cedar_str.trim(),
+ expected_cedar,
+ "Cedar schema output did not match expected"
+ );
+ }
+
+ #[test]
+ fn get_cedar_schema_internal_invalid() {
+ let mut env = JVM.attach_current_thread().unwrap();
+ let json_input = r#"
+
+ entity User = {
+ name: String,
+ age?: Long,
+ };
+ entity Photo in Album;
+ entity Album;
+ action view appliesTo {
+ principal : [User],
+ resource: [Album,Photo]
+ };
+ "#;
+
+ let jstr = env.new_string(json_input).unwrap();
+ let result = get_cedar_schema_internal(&mut env, jstr);
+ assert!(
+ result.is_err(),
+ "Expected get_cedar_schema_internal to fail {:?}",
+ result
+ );
+ }
+
+ #[test]
+ fn get_json_schema_internal_valid() {
+ let mut env = JVM.attach_current_thread().unwrap();
+ let cedar_input = r#"
+ namespace schema {
+ entity File;
+
+ entity Group;
+
+ entity User in [Group];
+
+ action "read" appliesTo {
+ principal: [User],
+ resource: [File],
+ context: {}
+ };
+ }
+ "#;
+
+ let jstr = env.new_string(cedar_input).unwrap();
+ let result = get_json_schema_internal(&mut env, jstr);
+ assert!(result.is_ok(), "Expected JSON conversion to succeed");
+
+ let json_jval = result.unwrap();
+ let json_jstr = JString::cast(&mut env, json_jval.l().unwrap()).unwrap();
+ let json_str = String::from(env.get_string(&json_jstr).unwrap());
+ let actual_json: serde_json::Value = serde_json::from_str(&json_str).unwrap();
+ let expected_json = serde_json::json!({
+ "schema": {
+ "entityTypes": {
+ "File": {},
+ "Group": {},
+ "User": {
+ "memberOfTypes": ["Group"]
+ }
+ },
+ "actions": {
+ "read": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["File"]
+ }
+ }
+ }
+ }
+ });
+
+ assert_eq!(
+ actual_json, expected_json,
+ "JSON schema doesn't match expected structure"
+ );
+ }
+
+ #[test]
+ fn get_json_schema_internal_null() {
+ let mut env = JVM.attach_current_thread().unwrap();
+ let null_str = JString::from(JObject::null());
+ let result = get_json_schema_internal(&mut env, null_str);
+ assert!(result.is_err(), "Expected error on null input");
+ }
+
+ #[test]
+ fn get_json_schema_internal_invalid_input() {
+ let mut env = JVM.attach_current_thread().unwrap();
+ let invalid_cedar = r#"
+ namespace schema {
+ entity File
+
+ entity Group with no semicolon
+
+ entity User in [NonExistentGroup];
+
+ action "read" appliesTo {
+ principal: [MissingEntity],
+ resource: [File],
+ context: {}
+ };
+ }
+ "#;
+ let jstr = env.new_string(invalid_cedar).unwrap();
+
+ let result = get_json_schema_internal(&mut env, jstr);
+ assert!(
+ result.is_err(),
+ "Expected get_json_schema_internal to fail: {:?}",
+ result
+ );
+ }
+ }
}