diff --git a/CedarWasm/.gitignore b/CedarWasm/.gitignore
new file mode 100644
index 00000000..d03db9c5
--- /dev/null
+++ b/CedarWasm/.gitignore
@@ -0,0 +1,3 @@
+wasm-build/target/
+core/target/
+benchmark/target/
diff --git a/CedarWasm/README.md b/CedarWasm/README.md
new file mode 100644
index 00000000..f7414243
--- /dev/null
+++ b/CedarWasm/README.md
@@ -0,0 +1,77 @@
+# Cedar Wasm
+
+Pure-Java Cedar policy engine using WebAssembly. The Cedar Rust crate is compiled to Wasm and executed via [Chicory Redline](https://github.com/AnyTimeTraveler/chicory-redline) — no JNI or native libraries required.
+
+## Project structure
+
+```
+CedarWasm/
+├── wasm-build/ Rust crate compiled to wasm32-unknown-unknown
+├── core/ Java module (CedarEngine + Chicory Redline)
+└── benchmark/ JMH benchmarks comparing JNI vs Wasm
+```
+
+## Prerequisites
+
+- Java 17+
+- Rust 1.89+ with `wasm32-unknown-unknown` target
+- `wasm-opt` (from [binaryen](https://github.com/WebAssembly/binaryen))
+- [Chicory Redline](https://github.com/AnyTimeTraveler/chicory-redline) installed to local Maven repo (`mvn install -DskipTests`)
+
+## Building the Wasm module
+
+```bash
+cd wasm-build
+rustup target add wasm32-unknown-unknown
+cargo build --release --target wasm32-unknown-unknown
+wasm-opt --enable-bulk-memory -O3 \
+ target/wasm32-unknown-unknown/release/cedar_wasm.wasm \
+ -o ../core/wasm/cedar_wasm.wasm
+```
+
+## Building and testing
+
+```bash
+cd CedarWasm
+mvn clean test -pl core
+```
+
+## Running benchmarks (JNI vs Wasm)
+
+First, build the JNI uber jar (requires Gradle + Rust):
+
+```bash
+cd CedarJava
+./gradlew uberJar -x test -x spotbugsMain -x spotbugsTest -x spotbugsJmh \
+ -x checkstyleMain -x checkstyleTest -x jacocoTestCoverageVerification
+```
+
+Then run the benchmarks:
+
+```bash
+cd CedarWasm
+mvn install -DskipTests
+mvn exec:java -pl benchmark
+```
+
+## Sample results
+
+```
+Benchmark Mode Cnt Score Error Units
+CedarBenchmark.jniCachedLarge avgt 3 1077.137 ± 2500.105 us/op
+CedarBenchmark.jniCachedMedium avgt 3 68.613 ± 44.063 us/op
+CedarBenchmark.jniCachedSmall avgt 3 29.142 ± 7.306 us/op
+CedarBenchmark.jniCachedXLarge avgt 3 476.329 ± 1036.358 us/op
+CedarBenchmark.jniUncachedLarge avgt 3 2209.292 ± 2208.573 us/op
+CedarBenchmark.jniUncachedMedium avgt 3 335.507 ± 269.147 us/op
+CedarBenchmark.jniUncachedSmall avgt 3 64.400 ± 20.911 us/op
+CedarBenchmark.jniUncachedXLarge avgt 3 9672.329 ± 1197.832 us/op
+CedarBenchmark.wasmCachedLarge avgt 3 2080.047 ± 1085.834 us/op
+CedarBenchmark.wasmCachedMedium avgt 3 218.603 ± 112.547 us/op
+CedarBenchmark.wasmCachedSmall avgt 3 131.787 ± 148.610 us/op
+CedarBenchmark.wasmCachedXLarge avgt 3 906.421 ± 664.702 us/op
+CedarBenchmark.wasmUncachedLarge avgt 3 2788.757 ± 364.868 us/op
+CedarBenchmark.wasmUncachedMedium avgt 3 840.366 ± 441.384 us/op
+CedarBenchmark.wasmUncachedSmall avgt 3 233.398 ± 41.424 us/op
+CedarBenchmark.wasmUncachedXLarge avgt 3 21890.498 ± 12325.095 us/op
+```
diff --git a/CedarWasm/benchmark/pom.xml b/CedarWasm/benchmark/pom.xml
new file mode 100644
index 00000000..2890c16f
--- /dev/null
+++ b/CedarWasm/benchmark/pom.xml
@@ -0,0 +1,99 @@
+
+
+ 4.0.0
+
+ com.cedarpolicy
+ cedar-wasm-parent
+ 4.0.0-SNAPSHOT
+
+ cedar-benchmark
+ Cedar JNI vs Wasm Benchmark
+
+
+
+
+ com.cedarpolicy
+ cedar-wasm
+ ${project.version}
+
+
+
+ com.cedarpolicy
+ cedar-java
+ 3.1.2
+ system
+ ${project.basedir}/../../CedarJava/build/libs/CedarJava-uber.jar
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.20.0
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jdk8
+ 2.20.0
+
+
+ com.fizzed
+ jne
+ 4.5.3
+
+
+ com.google.guava
+ guava
+ 33.5.0-jre
+
+
+
+ org.openjdk.jmh
+ jmh-core
+ ${jmh.version}
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.5.0
+
+ test
+ org.openjdk.jmh.Main
+
+ -f
+ 0
+ -i
+ 3
+ -wi
+ 3
+ -r
+ 1
+ -w
+ 1
+
+
+
+
+
+
diff --git a/CedarWasm/benchmark/src/test/java/com/cedarpolicy/benchmark/CedarBenchmark.java b/CedarWasm/benchmark/src/test/java/com/cedarpolicy/benchmark/CedarBenchmark.java
new file mode 100644
index 00000000..dba554a0
--- /dev/null
+++ b/CedarWasm/benchmark/src/test/java/com/cedarpolicy/benchmark/CedarBenchmark.java
@@ -0,0 +1,357 @@
+package com.cedarpolicy.benchmark;
+
+import com.cedarpolicy.BasicAuthorizationEngine;
+import com.cedarpolicy.model.AuthorizationRequest;
+import com.cedarpolicy.model.AuthorizationResponse;
+import com.cedarpolicy.model.entity.Entity;
+import com.cedarpolicy.model.entity.Entities;
+import com.cedarpolicy.model.exception.AuthException;
+import com.cedarpolicy.model.policy.Policy;
+import com.cedarpolicy.model.policy.PolicySet;
+import com.cedarpolicy.value.EntityTypeName;
+import com.cedarpolicy.wasm.CedarEngine;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(1)
+@State(Scope.Benchmark)
+public class CedarBenchmark {
+
+ // --- JNI ---
+ private BasicAuthorizationEngine jniEngine;
+ private AuthorizationRequest jniSmallRequest;
+ private PolicySet jniSmallPolicySet;
+ private Set jniSmallEntities;
+
+ private AuthorizationRequest jniMediumRequest;
+ private PolicySet jniMediumPolicySet;
+ private Set jniMediumEntities;
+
+ private AuthorizationRequest jniLargeRequest;
+ private PolicySet jniLargePolicySet;
+ private Set jniLargeEntities;
+
+ private PolicySet jniXLargePolicySet;
+
+ // --- JNI Cached ---
+ private PolicySet jniSmallPolicySetCached;
+ private PolicySet jniMediumPolicySetCached;
+ private PolicySet jniLargePolicySetCached;
+ private PolicySet jniXLargePolicySetCached;
+
+ // --- Wasm ---
+ private CedarEngine wasmEngine;
+ private String wasmSmallRequest;
+ private String wasmMediumRequest;
+ private String wasmLargeRequest;
+ private String wasmXLargeRequest;
+
+ // --- Wasm Cached ---
+ private String wasmCachedSmallRequest;
+ private String wasmCachedMediumRequest;
+ private String wasmCachedLargeRequest;
+ private String wasmCachedXLargeRequest;
+
+ @Setup(Level.Trial)
+ public void setUp() throws Exception {
+ setUpJni();
+ setUpWasm();
+ }
+
+ // ── JNI setup ──────────────────────────────────────────────────────
+
+ private void setUpJni() throws Exception {
+ jniEngine = new BasicAuthorizationEngine();
+
+ EntityTypeName userType = EntityTypeName.parse("User").get();
+ EntityTypeName actionType = EntityTypeName.parse("Action").get();
+ EntityTypeName resourceType = EntityTypeName.parse("Resource").get();
+ EntityTypeName photoType = EntityTypeName.parse("Photo").get();
+ EntityTypeName docType = EntityTypeName.parse("Document").get();
+
+ jniSmallRequest = new AuthorizationRequest(
+ userType.of("alice"), actionType.of("view"), resourceType.of("doc1"), new HashMap<>());
+ jniSmallPolicySet = PolicySet.parsePolicies(readResource("/small_policies.cedar"));
+ jniSmallEntities = Entities.parse(readResource("/small_entities.json")).getEntities();
+
+ jniMediumRequest = new AuthorizationRequest(
+ userType.of("alice"), actionType.of("View_Photo"), photoType.of("pic01"), new HashMap<>());
+ jniMediumPolicySet = PolicySet.parsePolicies(readResource("/medium_policies.cedar"));
+ jniMediumEntities = Entities.parse(readResource("/medium_entities.json")).getEntities();
+
+ HashMap context = new HashMap<>();
+ context.put("sourceIp", new com.cedarpolicy.value.IpAddress("10.1.2.3"));
+ context.put("requestId", new com.cedarpolicy.value.PrimString("req-bench-001"));
+ jniLargeRequest = new AuthorizationRequest(
+ userType.of("alice"), actionType.of("ViewDocument"), docType.of("doc1"), context);
+ jniLargePolicySet = PolicySet.parsePolicies(readResource("/large_policies.cedar"));
+ jniLargeEntities = Entities.parse(readResource("/large_entities.json")).getEntities();
+
+ jniXLargePolicySet = buildJniXLargePolicies();
+
+ jniSmallPolicySetCached = PolicySet.parsePolicies(readResource("/small_policies.cedar"));
+ jniSmallPolicySetCached.cache();
+ jniMediumPolicySetCached = PolicySet.parsePolicies(readResource("/medium_policies.cedar"));
+ jniMediumPolicySetCached.cache();
+ jniLargePolicySetCached = PolicySet.parsePolicies(readResource("/large_policies.cedar"));
+ jniLargePolicySetCached.cache();
+ jniXLargePolicySetCached = buildJniXLargePolicies();
+ jniXLargePolicySetCached.cache();
+ }
+
+ private static PolicySet buildJniXLargePolicies() throws Exception {
+ String[] roles = {"admin", "editor", "viewer", "auditor", "deployer",
+ "security", "billing", "support", "developer", "manager"};
+ String[] resources = {"Document", "Folder", "Project", "Comment", "Webhook",
+ "ApiKey", "AuditLog", "Setting", "Integration", "Pipeline",
+ "Environment", "Secret", "Deployment", "Report", "Dashboard",
+ "Alert", "Ticket", "Release", "Config", "Template"};
+ StringBuilder policies = new StringBuilder();
+ for (String role : roles) {
+ for (String resource : resources) {
+ policies.append(String.format(
+ "permit(principal in UserGroup::\"%s-group\", action, resource is %s);%n",
+ role, resource));
+ }
+ }
+ for (String resource : resources) {
+ policies.append(String.format(
+ "forbid(principal, action, resource is %s) when { resource.archived };%n",
+ resource));
+ }
+ return PolicySet.parsePolicies(policies.toString());
+ }
+
+ // ── Wasm setup ─────────────────────────────────────────────────────
+
+ private void setUpWasm() throws Exception {
+ wasmEngine = CedarEngine.create();
+
+ String smallPolicies = readResource("/small_policies.cedar");
+ String smallEntities = readResource("/small_entities.json");
+ String mediumPolicies = readResource("/medium_policies.cedar");
+ String mediumEntities = readResource("/medium_entities.json");
+ String largePolicies = readResource("/large_policies.cedar");
+ String largeEntities = readResource("/large_entities.json");
+ String xlargePolicies = buildXLargePoliciesCedar();
+
+ String largeContext = "{\"sourceIp\": {\"__extn\": {\"fn\": \"ip\", \"arg\": \"10.1.2.3\"}}, \"requestId\": \"req-bench-001\"}";
+
+ wasmSmallRequest = wasmAuthRequest(
+ entity("User", "alice"), entity("Action", "view"), entity("Resource", "doc1"),
+ "{}", smallPolicies, smallEntities);
+ wasmMediumRequest = wasmAuthRequest(
+ entity("User", "alice"), entity("Action", "View_Photo"), entity("Photo", "pic01"),
+ "{}", mediumPolicies, mediumEntities);
+ wasmLargeRequest = wasmAuthRequest(
+ entity("User", "alice"), entity("Action", "ViewDocument"), entity("Document", "doc1"),
+ largeContext, largePolicies, largeEntities);
+ wasmXLargeRequest = wasmAuthRequest(
+ entity("User", "alice"), entity("Action", "view"), entity("Resource", "doc1"),
+ "{}", xlargePolicies, smallEntities);
+
+ // Pre-parse policy sets for cached benchmarks
+ wasmEngine.preParsePolicySet("small", policySetFromCedar(smallPolicies));
+ wasmEngine.preParsePolicySet("medium", policySetFromCedar(mediumPolicies));
+ wasmEngine.preParsePolicySet("large", policySetFromCedar(largePolicies));
+ wasmEngine.preParsePolicySet("xlarge", policySetFromCedar(xlargePolicies));
+
+ wasmCachedSmallRequest = statefulRequest(
+ entity("User", "alice"), entity("Action", "view"), entity("Resource", "doc1"),
+ "{}", "small", smallEntities);
+ wasmCachedMediumRequest = statefulRequest(
+ entity("User", "alice"), entity("Action", "View_Photo"), entity("Photo", "pic01"),
+ "{}", "medium", mediumEntities);
+ wasmCachedLargeRequest = statefulRequest(
+ entity("User", "alice"), entity("Action", "ViewDocument"), entity("Document", "doc1"),
+ largeContext, "large", largeEntities);
+ wasmCachedXLargeRequest = statefulRequest(
+ entity("User", "alice"), entity("Action", "view"), entity("Resource", "doc1"),
+ "{}", "xlarge", smallEntities);
+ }
+
+ private static String buildXLargePoliciesCedar() {
+ String[] roles = {"admin", "editor", "viewer", "auditor", "deployer",
+ "security", "billing", "support", "developer", "manager"};
+ String[] resources = {"Document", "Folder", "Project", "Comment", "Webhook",
+ "ApiKey", "AuditLog", "Setting", "Integration", "Pipeline",
+ "Environment", "Secret", "Deployment", "Report", "Dashboard",
+ "Alert", "Ticket", "Release", "Config", "Template"};
+ var sb = new StringBuilder();
+ for (String role : roles) {
+ for (String resource : resources) {
+ sb.append(String.format(
+ "permit(principal in UserGroup::\"%s-group\", action, resource is %s);%n",
+ role, resource));
+ }
+ }
+ for (String resource : resources) {
+ sb.append(String.format(
+ "forbid(principal, action, resource is %s) when { resource.archived };%n",
+ resource));
+ }
+ return sb.toString();
+ }
+
+ // ── Wasm helpers ───────────────────────────────────────────────────
+
+ private static String entity(String type, String id) {
+ return "{\"type\": \"" + type + "\", \"id\": \"" + id + "\"}";
+ }
+
+ private static String policySetFromCedar(String cedarText) {
+ String[] policyTexts = cedarText.split("(?<=;)\\s*\n");
+ var sb = new StringBuilder("{\"staticPolicies\": {");
+ int idx = 0;
+ for (String p : policyTexts) {
+ String trimmed = p.trim();
+ if (trimmed.isEmpty() || trimmed.startsWith("//")) continue;
+ if (idx > 0) sb.append(", ");
+ sb.append("\"p").append(idx).append("\": ");
+ sb.append("\"").append(trimmed.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", " ")).append("\"");
+ idx++;
+ }
+ sb.append("}, \"templates\": {}, \"templateLinks\": []}");
+ return sb.toString();
+ }
+
+ private static String wasmAuthRequest(String principal, String action, String resource,
+ String context, String policiesCedar, String entitiesJson) {
+ String policySet = policySetFromCedar(policiesCedar);
+ return "{\"principal\": " + principal + ", \"action\": " + action + ", \"resource\": " + resource
+ + ", \"context\": " + context + ", \"policies\": " + policySet + ", \"entities\": " + entitiesJson + "}";
+ }
+
+ private static String statefulRequest(String principal, String action, String resource,
+ String context, String policySetId, String entitiesJson) {
+ return "{\"principal\": " + principal + ", \"action\": " + action + ", \"resource\": " + resource
+ + ", \"context\": " + context + ", \"preparsedPolicySetId\": \"" + policySetId
+ + "\", \"validateRequest\": false, \"entities\": " + entitiesJson + "}";
+ }
+
+ // ── Utilities ──────────────────────────────────────────────────────
+
+ private static String readResource(String path) throws Exception {
+ return new String(
+ Files.readAllBytes(Paths.get(CedarBenchmark.class.getResource(path).toURI())),
+ StandardCharsets.UTF_8);
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // JNI Benchmarks
+ // ═══════════════════════════════════════════════════════════════════
+
+ @Benchmark
+ public AuthorizationResponse jniUncachedSmall() throws AuthException {
+ return jniEngine.isAuthorized(jniSmallRequest, jniSmallPolicySet, jniSmallEntities);
+ }
+
+ @Benchmark
+ public AuthorizationResponse jniUncachedMedium() throws AuthException {
+ return jniEngine.isAuthorized(jniMediumRequest, jniMediumPolicySet, jniMediumEntities);
+ }
+
+ @Benchmark
+ public AuthorizationResponse jniUncachedLarge() throws AuthException {
+ return jniEngine.isAuthorized(jniLargeRequest, jniLargePolicySet, jniLargeEntities);
+ }
+
+ @Benchmark
+ public AuthorizationResponse jniUncachedXLarge() throws AuthException {
+ return jniEngine.isAuthorized(jniSmallRequest, jniXLargePolicySet, jniSmallEntities);
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // JNI Cached Benchmarks
+ // ═══════════════════════════════════════════════════════════════════
+
+ @Benchmark
+ public AuthorizationResponse jniCachedSmall() throws AuthException {
+ return jniEngine.isAuthorized(jniSmallRequest, jniSmallPolicySetCached, jniSmallEntities);
+ }
+
+ @Benchmark
+ public AuthorizationResponse jniCachedMedium() throws AuthException {
+ return jniEngine.isAuthorized(jniMediumRequest, jniMediumPolicySetCached, jniMediumEntities);
+ }
+
+ @Benchmark
+ public AuthorizationResponse jniCachedLarge() throws AuthException {
+ return jniEngine.isAuthorized(jniLargeRequest, jniLargePolicySetCached, jniLargeEntities);
+ }
+
+ @Benchmark
+ public AuthorizationResponse jniCachedXLarge() throws AuthException {
+ return jniEngine.isAuthorized(jniSmallRequest, jniXLargePolicySetCached, jniSmallEntities);
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // Wasm Benchmarks
+ // ═══════════════════════════════════════════════════════════════════
+
+ @Benchmark
+ public String wasmUncachedSmall() {
+ return wasmEngine.authorize( wasmSmallRequest);
+ }
+
+ @Benchmark
+ public String wasmUncachedMedium() {
+ return wasmEngine.authorize( wasmMediumRequest);
+ }
+
+ @Benchmark
+ public String wasmUncachedLarge() {
+ return wasmEngine.authorize( wasmLargeRequest);
+ }
+
+ @Benchmark
+ public String wasmUncachedXLarge() {
+ return wasmEngine.authorize( wasmXLargeRequest);
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // Wasm Cached Benchmarks
+ // ═══════════════════════════════════════════════════════════════════
+
+ @Benchmark
+ public String wasmCachedSmall() {
+ return wasmEngine.statefulAuthorize(wasmCachedSmallRequest);
+ }
+
+ @Benchmark
+ public String wasmCachedMedium() {
+ return wasmEngine.statefulAuthorize(wasmCachedMediumRequest);
+ }
+
+ @Benchmark
+ public String wasmCachedLarge() {
+ return wasmEngine.statefulAuthorize(wasmCachedLargeRequest);
+ }
+
+ @Benchmark
+ public String wasmCachedXLarge() {
+ return wasmEngine.statefulAuthorize(wasmCachedXLargeRequest);
+ }
+}
diff --git a/CedarWasm/benchmark/src/test/resources/large_entities.json b/CedarWasm/benchmark/src/test/resources/large_entities.json
new file mode 100644
index 00000000..a340bfe1
--- /dev/null
+++ b/CedarWasm/benchmark/src/test/resources/large_entities.json
@@ -0,0 +1,54 @@
+[
+ {"uid": {"type": "Organization", "id": "acme"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Department", "id": "engineering"}, "attrs": {}, "parents": [{"type": "Organization", "id": "acme"}]},
+ {"uid": {"type": "Department", "id": "product"}, "attrs": {}, "parents": [{"type": "Organization", "id": "acme"}]},
+ {"uid": {"type": "Team", "id": "engineering"}, "attrs": {}, "parents": [{"type": "Department", "id": "engineering"}]},
+ {"uid": {"type": "Team", "id": "platform"}, "attrs": {}, "parents": [{"type": "Department", "id": "engineering"}]},
+ {"uid": {"type": "Team", "id": "design"}, "attrs": {}, "parents": [{"type": "Department", "id": "product"}]},
+ {"uid": {"type": "UserGroup", "id": "org-admins"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "UserGroup", "id": "project-admins"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "UserGroup", "id": "developers"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "UserGroup", "id": "viewers"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "User", "id": "alice"}, "attrs": {"email": "alice@acme.com", "role": "admin", "mfaEnabled": true, "loginCount": 542, "lastLoginIp": {"__extn": {"fn": "ip", "arg": "10.1.2.3"}}}, "parents": [{"type": "Team", "id": "engineering"}, {"type": "UserGroup", "id": "org-admins"}, {"type": "UserGroup", "id": "project-admins"}]},
+ {"uid": {"type": "User", "id": "bob"}, "attrs": {"email": "bob@acme.com", "role": "developer", "mfaEnabled": true, "loginCount": 201, "lastLoginIp": {"__extn": {"fn": "ip", "arg": "10.1.2.4"}}}, "parents": [{"type": "Team", "id": "engineering"}, {"type": "UserGroup", "id": "developers"}]},
+ {"uid": {"type": "User", "id": "carol"}, "attrs": {"email": "carol@acme.com", "role": "developer", "mfaEnabled": false, "loginCount": 89, "lastLoginIp": {"__extn": {"fn": "ip", "arg": "10.1.2.5"}}}, "parents": [{"type": "Team", "id": "platform"}, {"type": "UserGroup", "id": "developers"}]},
+ {"uid": {"type": "User", "id": "dave"}, "attrs": {"email": "dave@acme.com", "role": "designer", "mfaEnabled": true, "loginCount": 45, "lastLoginIp": {"__extn": {"fn": "ip", "arg": "192.168.1.10"}}}, "parents": [{"type": "Team", "id": "design"}, {"type": "UserGroup", "id": "viewers"}]},
+ {"uid": {"type": "User", "id": "eve"}, "attrs": {"email": "eve@acme.com", "role": "manager", "mfaEnabled": true, "loginCount": 312, "lastLoginIp": {"__extn": {"fn": "ip", "arg": "10.1.3.1"}}}, "parents": [{"type": "Department", "id": "engineering"}, {"type": "UserGroup", "id": "project-admins"}]},
+ {"uid": {"type": "ServiceAccount", "id": "ci-bot"}, "attrs": {"owner": {"__entity": {"type": "User", "id": "alice"}}, "scopes": ["read", "deploy"]}, "parents": [{"type": "ServiceRole", "id": "deployer"}]},
+ {"uid": {"type": "ServiceAccount", "id": "monitoring"}, "attrs": {"owner": {"__entity": {"type": "User", "id": "bob"}}, "scopes": ["read"]}, "parents": [{"type": "ServiceRole", "id": "reader"}]},
+ {"uid": {"type": "ServiceRole", "id": "deployer"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "ServiceRole", "id": "reader"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Project", "id": "proj1"}, "attrs": {"visibility": "internal", "archived": false}, "parents": [{"type": "Organization", "id": "acme"}]},
+ {"uid": {"type": "Project", "id": "proj2"}, "attrs": {"visibility": "public", "archived": false}, "parents": [{"type": "Organization", "id": "acme"}]},
+ {"uid": {"type": "Folder", "id": "root"}, "attrs": {"owner": {"__entity": {"type": "User", "id": "alice"}}}, "parents": [{"type": "Project", "id": "proj1"}]},
+ {"uid": {"type": "Folder", "id": "designs"}, "attrs": {"owner": {"__entity": {"type": "User", "id": "dave"}}}, "parents": [{"type": "Folder", "id": "root"}]},
+ {"uid": {"type": "Folder", "id": "src"}, "attrs": {"owner": {"__entity": {"type": "User", "id": "bob"}}}, "parents": [{"type": "Folder", "id": "root"}]},
+ {"uid": {"type": "Document", "id": "doc1"}, "attrs": {"owner": {"__entity": {"type": "User", "id": "alice"}}, "classification": "internal", "createdAt": 1700000000, "tags": ["architecture", "design"]}, "parents": [{"type": "Folder", "id": "root"}]},
+ {"uid": {"type": "Document", "id": "doc2"}, "attrs": {"owner": {"__entity": {"type": "User", "id": "bob"}}, "classification": "restricted", "createdAt": 1700100000, "tags": ["security", "credentials"]}, "parents": [{"type": "Folder", "id": "src"}]},
+ {"uid": {"type": "Document", "id": "doc3"}, "attrs": {"owner": {"__entity": {"type": "User", "id": "carol"}}, "classification": "public", "createdAt": 1700200000, "tags": ["readme"]}, "parents": [{"type": "Folder", "id": "src"}]},
+ {"uid": {"type": "Document", "id": "doc4"}, "attrs": {"owner": {"__entity": {"type": "User", "id": "dave"}}, "classification": "internal", "createdAt": 1700300000, "tags": ["mockup"]}, "parents": [{"type": "Folder", "id": "designs"}]},
+ {"uid": {"type": "Comment", "id": "c1"}, "attrs": {"author": {"__entity": {"type": "User", "id": "bob"}}}, "parents": [{"type": "Document", "id": "doc1"}]},
+ {"uid": {"type": "Comment", "id": "c2"}, "attrs": {"author": {"__entity": {"type": "User", "id": "carol"}}}, "parents": [{"type": "Document", "id": "doc1"}]},
+ {"uid": {"type": "Webhook", "id": "wh1"}, "attrs": {"url": "https://hooks.example.com/deploy", "active": true}, "parents": [{"type": "Project", "id": "proj1"}]},
+ {"uid": {"type": "ApiKey", "id": "key1"}, "attrs": {"expiresAt": 1800000000}, "parents": [{"type": "ServiceAccount", "id": "ci-bot"}]},
+ {"uid": {"type": "AuditLog", "id": "audit-acme"}, "attrs": {}, "parents": [{"type": "Organization", "id": "acme"}]},
+ {"uid": {"type": "Setting", "id": "proj1-settings"}, "attrs": {}, "parents": [{"type": "Project", "id": "proj1"}]},
+ {"uid": {"type": "Integration", "id": "github"}, "attrs": {"provider": "github", "enabled": true}, "parents": [{"type": "Project", "id": "proj1"}]},
+ {"uid": {"type": "Pipeline", "id": "pipe1"}, "attrs": {"status": "idle"}, "parents": [{"type": "Project", "id": "proj1"}]},
+ {"uid": {"type": "Environment", "id": "staging"}, "attrs": {"tier": "staging", "locked": false}, "parents": [{"type": "Project", "id": "proj1"}]},
+ {"uid": {"type": "Environment", "id": "production"}, "attrs": {"tier": "production", "locked": true}, "parents": [{"type": "Project", "id": "proj1"}]},
+ {"uid": {"type": "Secret", "id": "db-password"}, "attrs": {"rotatedAt": 1699000000}, "parents": [{"type": "Environment", "id": "production"}]},
+ {"uid": {"type": "Deployment", "id": "deploy-v1"}, "attrs": {"version": "1.2.3", "deployer": {"__entity": {"type": "User", "id": "alice"}}}, "parents": [{"type": "Environment", "id": "staging"}]},
+ {"uid": {"type": "Action", "id": "ViewDocument"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "EditDocument"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "DeleteDocument"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "ShareDocument"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "CreateDocument"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "CreateFolder"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "ViewProject"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "EditProject"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "Deploy"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "ViewSecret"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "TriggerPipeline"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "ViewPipeline"}, "attrs": {}, "parents": []}
+]
diff --git a/CedarWasm/benchmark/src/test/resources/large_policies.cedar b/CedarWasm/benchmark/src/test/resources/large_policies.cedar
new file mode 100644
index 00000000..f9164d43
--- /dev/null
+++ b/CedarWasm/benchmark/src/test/resources/large_policies.cedar
@@ -0,0 +1,63 @@
+// Organization-wide read access for all members
+permit(principal, action == Action::"ViewDocument", resource)
+when { resource in principal };
+
+// Project owners get full access
+permit(principal, action, resource)
+when { resource in Project::"proj1" && principal == Project::"proj1".owner };
+
+// Team leads can manage their team's projects
+permit(principal in Team::"engineering", action == Action::"EditProject", resource in Project::"proj1");
+permit(principal in Team::"engineering", action == Action::"DeleteProject", resource in Project::"proj1");
+
+// Document owners can edit and delete their own documents
+permit(principal, action == Action::"EditDocument", resource)
+when { principal == resource.owner };
+permit(principal, action == Action::"DeleteDocument", resource)
+when { principal == resource.owner };
+permit(principal, action == Action::"ShareDocument", resource)
+when { principal == resource.owner };
+
+// Comment authors can delete their own comments
+permit(principal, action == Action::"DeleteComment", resource)
+when { principal == resource.author };
+
+// Anyone in a project can create documents and folders
+permit(principal, action == Action::"CreateDocument", resource in Project::"proj1")
+when { principal in Department::"engineering" };
+permit(principal, action == Action::"CreateFolder", resource in Project::"proj1")
+when { principal in Department::"engineering" };
+
+// Pipeline access for CI service accounts
+permit(principal == ServiceAccount::"ci-bot", action == Action::"TriggerPipeline", resource in Project::"proj1");
+permit(principal == ServiceAccount::"ci-bot", action == Action::"ViewPipeline", resource in Project::"proj1");
+
+// Deployment requires MFA
+permit(principal, action == Action::"Deploy", resource)
+when { principal.mfaEnabled };
+
+// Secrets require MFA and internal IP
+permit(principal, action == Action::"ViewSecret", resource)
+when { principal.mfaEnabled && context.sourceIp.isInRange(ip("10.0.0.0/8")) };
+permit(principal, action == Action::"EditSecret", resource)
+when { principal.mfaEnabled && context.sourceIp.isInRange(ip("10.0.0.0/8")) };
+
+// Audit logs only for org admins
+permit(principal in UserGroup::"org-admins", action == Action::"ViewAuditLog", resource);
+
+// Settings management for project admins
+permit(principal in UserGroup::"project-admins", action == Action::"EditSetting", resource in Project::"proj1");
+permit(principal, action == Action::"ViewSetting", resource in Project::"proj1")
+when { principal in Department::"engineering" };
+
+// Forbid access to archived projects
+forbid(principal, action, resource in Project::"proj1")
+when { Project::"proj1".archived };
+
+// Forbid deployments to locked environments
+forbid(principal, action == Action::"Deploy", resource)
+when { resource.locked };
+
+// Forbid access from external IPs to classified documents
+forbid(principal, action == Action::"ViewDocument", resource)
+when { resource.classification == "restricted" && !context.sourceIp.isInRange(ip("10.0.0.0/8")) };
diff --git a/CedarWasm/benchmark/src/test/resources/large_schema.json b/CedarWasm/benchmark/src/test/resources/large_schema.json
new file mode 100644
index 00000000..f69aeaad
--- /dev/null
+++ b/CedarWasm/benchmark/src/test/resources/large_schema.json
@@ -0,0 +1,372 @@
+{
+ "": {
+ "entityTypes": {
+ "User": {
+ "memberOfTypes": ["UserGroup", "Team", "Department"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "email": { "type": "String" },
+ "role": { "type": "String" },
+ "mfaEnabled": { "type": "Boolean" },
+ "loginCount": { "type": "Long" },
+ "lastLoginIp": { "type": "Extension", "name": "ipaddr" }
+ }
+ }
+ },
+ "UserGroup": {
+ "memberOfTypes": ["UserGroup"]
+ },
+ "Team": {
+ "memberOfTypes": ["Department"]
+ },
+ "Department": {
+ "memberOfTypes": ["Organization"]
+ },
+ "Organization": {},
+ "ServiceAccount": {
+ "memberOfTypes": ["ServiceRole"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "owner": { "type": "Entity", "name": "User" },
+ "scopes": { "type": "Set", "element": { "type": "String" } }
+ }
+ }
+ },
+ "ServiceRole": {},
+ "Document": {
+ "memberOfTypes": ["Folder", "Project"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "owner": { "type": "Entity", "name": "User" },
+ "classification": { "type": "String" },
+ "createdAt": { "type": "Long" },
+ "tags": { "type": "Set", "element": { "type": "String" } }
+ }
+ }
+ },
+ "Folder": {
+ "memberOfTypes": ["Folder", "Project"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "owner": { "type": "Entity", "name": "User" }
+ }
+ }
+ },
+ "Project": {
+ "memberOfTypes": ["Organization"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "visibility": { "type": "String" },
+ "archived": { "type": "Boolean" }
+ }
+ }
+ },
+ "Comment": {
+ "memberOfTypes": ["Document"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "author": { "type": "Entity", "name": "User" }
+ }
+ }
+ },
+ "Webhook": {
+ "memberOfTypes": ["Project"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "url": { "type": "String" },
+ "active": { "type": "Boolean" }
+ }
+ }
+ },
+ "ApiKey": {
+ "memberOfTypes": ["ServiceAccount"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "expiresAt": { "type": "Long" }
+ }
+ }
+ },
+ "AuditLog": {
+ "memberOfTypes": ["Organization"]
+ },
+ "Setting": {
+ "memberOfTypes": ["Project", "Organization"]
+ },
+ "Integration": {
+ "memberOfTypes": ["Project"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "provider": { "type": "String" },
+ "enabled": { "type": "Boolean" }
+ }
+ }
+ },
+ "Pipeline": {
+ "memberOfTypes": ["Project"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "status": { "type": "String" }
+ }
+ }
+ },
+ "Environment": {
+ "memberOfTypes": ["Project"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "tier": { "type": "String" },
+ "locked": { "type": "Boolean" }
+ }
+ }
+ },
+ "Secret": {
+ "memberOfTypes": ["Environment", "Project"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "rotatedAt": { "type": "Long" }
+ }
+ }
+ },
+ "Deployment": {
+ "memberOfTypes": ["Environment"],
+ "shape": {
+ "type": "Record",
+ "attributes": {
+ "version": { "type": "String" },
+ "deployer": { "type": "Entity", "name": "User" }
+ }
+ }
+ }
+ },
+ "actions": {
+ "ViewDocument": {
+ "appliesTo": {
+ "principalTypes": ["User", "ServiceAccount"],
+ "resourceTypes": ["Document"],
+ "context": {
+ "type": "Record",
+ "attributes": {
+ "sourceIp": { "type": "Extension", "name": "ipaddr" },
+ "requestId": { "type": "String" }
+ }
+ }
+ }
+ },
+ "EditDocument": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Document"],
+ "context": {
+ "type": "Record",
+ "attributes": {
+ "sourceIp": { "type": "Extension", "name": "ipaddr" },
+ "requestId": { "type": "String" }
+ }
+ }
+ }
+ },
+ "DeleteDocument": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Document"],
+ "context": {
+ "type": "Record",
+ "attributes": {
+ "sourceIp": { "type": "Extension", "name": "ipaddr" },
+ "requestId": { "type": "String" }
+ }
+ }
+ }
+ },
+ "ShareDocument": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Document"],
+ "context": {
+ "type": "Record",
+ "attributes": {
+ "sourceIp": { "type": "Extension", "name": "ipaddr" },
+ "shareWith": { "type": "Entity", "name": "User" }
+ }
+ }
+ }
+ },
+ "CreateDocument": {
+ "appliesTo": {
+ "principalTypes": ["User", "ServiceAccount"],
+ "resourceTypes": ["Folder", "Project"]
+ }
+ },
+ "CreateFolder": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Folder", "Project"]
+ }
+ },
+ "DeleteFolder": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Folder"]
+ }
+ },
+ "ViewProject": {
+ "appliesTo": {
+ "principalTypes": ["User", "ServiceAccount"],
+ "resourceTypes": ["Project"]
+ }
+ },
+ "EditProject": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Project"]
+ }
+ },
+ "DeleteProject": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Project"]
+ }
+ },
+ "CreateComment": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Document"]
+ }
+ },
+ "DeleteComment": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Comment"]
+ }
+ },
+ "ManageWebhook": {
+ "appliesTo": {
+ "principalTypes": ["User", "ServiceAccount"],
+ "resourceTypes": ["Webhook"]
+ }
+ },
+ "CreateApiKey": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["ServiceAccount"]
+ }
+ },
+ "RevokeApiKey": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["ApiKey"]
+ }
+ },
+ "ViewAuditLog": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["AuditLog"]
+ }
+ },
+ "EditSetting": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Setting"]
+ }
+ },
+ "ViewSetting": {
+ "appliesTo": {
+ "principalTypes": ["User", "ServiceAccount"],
+ "resourceTypes": ["Setting"]
+ }
+ },
+ "ManageIntegration": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Integration"]
+ }
+ },
+ "TriggerPipeline": {
+ "appliesTo": {
+ "principalTypes": ["User", "ServiceAccount"],
+ "resourceTypes": ["Pipeline"]
+ }
+ },
+ "CancelPipeline": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Pipeline"]
+ }
+ },
+ "ViewPipeline": {
+ "appliesTo": {
+ "principalTypes": ["User", "ServiceAccount"],
+ "resourceTypes": ["Pipeline"]
+ }
+ },
+ "Deploy": {
+ "appliesTo": {
+ "principalTypes": ["User", "ServiceAccount"],
+ "resourceTypes": ["Environment"],
+ "context": {
+ "type": "Record",
+ "attributes": {
+ "version": { "type": "String" },
+ "sourceIp": { "type": "Extension", "name": "ipaddr" }
+ }
+ }
+ }
+ },
+ "Rollback": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Deployment"]
+ }
+ },
+ "ViewSecret": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Secret"],
+ "context": {
+ "type": "Record",
+ "attributes": {
+ "sourceIp": { "type": "Extension", "name": "ipaddr" },
+ "reason": { "type": "String" }
+ }
+ }
+ }
+ },
+ "EditSecret": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Secret"],
+ "context": {
+ "type": "Record",
+ "attributes": {
+ "sourceIp": { "type": "Extension", "name": "ipaddr" },
+ "reason": { "type": "String" }
+ }
+ }
+ }
+ },
+ "ManageMembers": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Team", "Department", "Organization"]
+ }
+ },
+ "TransferOwnership": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Project", "Document"]
+ }
+ }
+ }
+ }
+}
diff --git a/CedarWasm/benchmark/src/test/resources/medium_entities.json b/CedarWasm/benchmark/src/test/resources/medium_entities.json
new file mode 100644
index 00000000..99c3194b
--- /dev/null
+++ b/CedarWasm/benchmark/src/test/resources/medium_entities.json
@@ -0,0 +1,7 @@
+[
+ {"uid": {"type": "User", "id": "alice"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "View_Photo"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Photo", "id": "pic01"}, "attrs": {}, "parents": [{"type": "Album", "id": "vacation"}]},
+ {"uid": {"type": "Album", "id": "vacation"}, "attrs": {}, "parents": [{"type": "Account", "id": "account1"}]},
+ {"uid": {"type": "Account", "id": "account1"}, "attrs": {}, "parents": []}
+]
diff --git a/CedarWasm/benchmark/src/test/resources/medium_policies.cedar b/CedarWasm/benchmark/src/test/resources/medium_policies.cedar
new file mode 100644
index 00000000..1b00713d
--- /dev/null
+++ b/CedarWasm/benchmark/src/test/resources/medium_policies.cedar
@@ -0,0 +1,9 @@
+permit(principal == User::"alice", action == Action::"View_Photo", resource);
+
+permit(principal, action == Action::"View_Photo", resource in Album::"vacation");
+
+forbid(principal, action, resource) when { resource.private };
+
+permit(principal, action == Action::"Edit_Photo", resource) when { principal == resource.owner };
+
+permit(principal, action == Action::"Delete_Photo", resource) when { principal == resource.owner };
diff --git a/CedarWasm/benchmark/src/test/resources/photoflash_schema.json b/CedarWasm/benchmark/src/test/resources/photoflash_schema.json
new file mode 100644
index 00000000..2875b8f0
--- /dev/null
+++ b/CedarWasm/benchmark/src/test/resources/photoflash_schema.json
@@ -0,0 +1,65 @@
+{
+ "": {
+ "entityTypes": {
+ "User": {
+ "memberOfTypes": [
+ "UserGroup"
+ ]
+ },
+ "Photo": {
+ "memberOfTypes": [
+ "Album",
+ "Account"
+ ]
+ },
+ "Album": {},
+ "UserGroup": {},
+ "Account": {}
+ },
+ "actions": {
+ "readOnly": {},
+ "readWrite": {},
+ "createAlbum": {
+ "appliesTo": {
+ "resourceTypes": [
+ "Account",
+ "Album"
+ ],
+ "principalTypes": [
+ "User"
+ ]
+ }
+ },
+ "addPhotoToAlbum": {
+ "appliesTo": {
+ "principalTypes": [
+ "User"
+ ],
+ "resourceTypes": [
+ "Album"
+ ]
+ }
+ },
+ "viewPhoto": {
+ "appliesTo": {
+ "principalTypes": [
+ "User"
+ ],
+ "resourceTypes": [
+ "Photo"
+ ]
+ }
+ },
+ "viewComments": {
+ "appliesTo": {
+ "principalTypes": [
+ "User"
+ ],
+ "resourceTypes": [
+ "Photo"
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/CedarWasm/benchmark/src/test/resources/small_entities.json b/CedarWasm/benchmark/src/test/resources/small_entities.json
new file mode 100644
index 00000000..49905922
--- /dev/null
+++ b/CedarWasm/benchmark/src/test/resources/small_entities.json
@@ -0,0 +1,5 @@
+[
+ {"uid": {"type": "User", "id": "alice"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Action", "id": "view"}, "attrs": {}, "parents": []},
+ {"uid": {"type": "Resource", "id": "doc1"}, "attrs": {}, "parents": []}
+]
diff --git a/CedarWasm/benchmark/src/test/resources/small_policies.cedar b/CedarWasm/benchmark/src/test/resources/small_policies.cedar
new file mode 100644
index 00000000..41c318cd
--- /dev/null
+++ b/CedarWasm/benchmark/src/test/resources/small_policies.cedar
@@ -0,0 +1 @@
+permit(principal, action, resource);
diff --git a/CedarWasm/core/pom.xml b/CedarWasm/core/pom.xml
new file mode 100644
index 00000000..2008ea25
--- /dev/null
+++ b/CedarWasm/core/pom.xml
@@ -0,0 +1,58 @@
+
+
+ 4.0.0
+
+ com.cedarpolicy
+ cedar-wasm-parent
+ 4.0.0-SNAPSHOT
+
+ cedar-wasm
+ Cedar Wasm
+
+
+
+ io.roastedroot
+ redline
+ ${redline.version}
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.version}
+ test
+
+
+
+
+
+
+ io.roastedroot
+ redline-compiler-maven-plugin
+ ${redline.version}
+
+
+ compile-cedar-wasm
+
+ compile
+
+
+ com.cedarpolicy.wasm.CedarWasmModule
+ ${project.basedir}/wasm/cedar_wasm.wasm
+ com.cedarpolicy.wasm.CedarWasmExports
+ true
+
+ x86_64-unknown-linux-gnu
+
+
+
+
+
+
+
+
diff --git a/CedarWasm/core/src/main/java/com/cedarpolicy/wasm/CedarEngine.java b/CedarWasm/core/src/main/java/com/cedarpolicy/wasm/CedarEngine.java
new file mode 100644
index 00000000..a714e738
--- /dev/null
+++ b/CedarWasm/core/src/main/java/com/cedarpolicy/wasm/CedarEngine.java
@@ -0,0 +1,109 @@
+package com.cedarpolicy.wasm;
+
+public class CedarEngine implements AutoCloseable {
+ private final CedarWasm wasm;
+
+ private CedarEngine(CedarWasm wasm) {
+ this.wasm = wasm;
+ }
+
+ public static CedarEngine create() {
+ return new CedarEngine(CedarWasm.create());
+ }
+
+ public String authorize(String requestJson) {
+ return wasm.authorize(requestJson);
+ }
+
+ public String statefulAuthorize(String requestJson) {
+ return wasm.statefulAuthorize(requestJson);
+ }
+
+ public String validate(String requestJson) {
+ return wasm.call("ValidateOperation", requestJson);
+ }
+
+ public String validateEntities(String requestJson) {
+ return wasm.call("ValidateEntities", requestJson);
+ }
+
+ public String validateWithLevel(String requestJson) {
+ return wasm.call("ValidateWithLevelOperation", requestJson);
+ }
+
+ public String parsePolicy(String policyText) {
+ return wasm.callExport(wasm.exports()::cedarParsePolicy, policyText);
+ }
+
+ public String parseTemplate(String templateText) {
+ return wasm.callExport(wasm.exports()::cedarParseTemplate, templateText);
+ }
+
+ public String policyEffect(String policyText) {
+ return wasm.callExport(wasm.exports()::cedarPolicyEffect, policyText);
+ }
+
+ public String templateEffect(String templateText) {
+ return wasm.callExport(wasm.exports()::cedarTemplateEffect, templateText);
+ }
+
+ public String policyToJson(String policyText) {
+ return wasm.callExport(wasm.exports()::cedarPolicyToJson, policyText);
+ }
+
+ public String policyFromJson(String policyJson) {
+ return wasm.callExport(wasm.exports()::cedarPolicyFromJson, policyJson);
+ }
+
+ public String getPolicyAnnotations(String policyText) {
+ return wasm.callExport(wasm.exports()::cedarGetPolicyAnnotations, policyText);
+ }
+
+ public String getTemplateAnnotations(String templateText) {
+ return wasm.callExport(wasm.exports()::cedarGetTemplateAnnotations, templateText);
+ }
+
+ public String parsePolicies(String policiesText) {
+ return wasm.callExport(wasm.exports()::cedarParsePolicies, policiesText);
+ }
+
+ public String policySetToJson(String policySetJson) {
+ return wasm.callExport(wasm.exports()::cedarPolicySetToJson, policySetJson);
+ }
+
+ public String formatPolicies(String policiesText) {
+ return wasm.callExport(wasm.exports()::cedarFormatPolicies, policiesText);
+ }
+
+ public String parseJsonSchema(String schemaJson) {
+ return wasm.callExport(wasm.exports()::cedarParseJsonSchema, schemaJson);
+ }
+
+ public String parseCedarSchema(String schemaText) {
+ return wasm.callExport(wasm.exports()::cedarParseCedarSchema, schemaText);
+ }
+
+ public String schemaToCedar(String schemaJson) {
+ return wasm.callExport(wasm.exports()::cedarSchemaToCedar, schemaJson);
+ }
+
+ public String schemaToJson(String cedarSchema) {
+ return wasm.callExport(wasm.exports()::cedarSchemaToJson, cedarSchema);
+ }
+
+ public String getVersion() {
+ int widePtr = wasm.exports().cedarVersion();
+ return wasm.readWidePtr(widePtr);
+ }
+
+ public String preParsePolicySet(String id, String policiesJson) {
+ return wasm.callExport(wasm.exports()::cedarPreparsePolicySet, id, policiesJson);
+ }
+
+ public String preParseSchema(String id, String schemaJson) {
+ return wasm.callExport(wasm.exports()::cedarPreparseSchema, id, schemaJson);
+ }
+
+ @Override
+ public void close() {}
+}
diff --git a/CedarWasm/core/src/main/java/com/cedarpolicy/wasm/CedarWasm.java b/CedarWasm/core/src/main/java/com/cedarpolicy/wasm/CedarWasm.java
new file mode 100644
index 00000000..7644c013
--- /dev/null
+++ b/CedarWasm/core/src/main/java/com/cedarpolicy/wasm/CedarWasm.java
@@ -0,0 +1,129 @@
+package com.cedarpolicy.wasm;
+
+import com.dylibso.chicory.runtime.Memory;
+import java.nio.charset.StandardCharsets;
+
+public class CedarWasm {
+ private final CedarWasmExports_ModuleExports exports;
+ private final Memory memory;
+
+ private static final int BUF_SIZE = 256 * 1024;
+ private int inputBufPtr;
+ private int inputBufCap;
+ private int outputBufPtr;
+ private int outputBufCap;
+
+ private CedarWasm(CedarWasmExports_ModuleExports exports) {
+ this.exports = exports;
+ this.memory = exports.memory();
+ this.inputBufCap = BUF_SIZE;
+ this.inputBufPtr = exports.alloc(inputBufCap);
+ this.outputBufCap = BUF_SIZE;
+ this.outputBufPtr = exports.alloc(outputBufCap);
+ }
+
+ public static CedarWasm create() {
+ var instance = CedarWasmModule.builder().build().instance();
+ return new CedarWasm(new CedarWasmExports_ModuleExports(instance));
+ }
+
+ public CedarWasmExports_ModuleExports exports() {
+ return exports;
+ }
+
+ public String authorize(String input) {
+ return callBuf(exports::cedarAuthorizeBuf, input);
+ }
+
+ public String statefulAuthorize(String input) {
+ return callBuf(exports::cedarStatefulAuthorizeBuf, input);
+ }
+
+ public String call(String operation, String input) {
+ byte[] opBytes = operation.getBytes(StandardCharsets.UTF_8);
+ byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);
+ int opPtr = allocAndWrite(opBytes);
+ int inputPtr = allocAndWrite(inputBytes);
+ int widePtr = exports.cedarCall(opPtr, opBytes.length, inputPtr, inputBytes.length);
+ exports.dealloc(opPtr, opBytes.length);
+ exports.dealloc(inputPtr, inputBytes.length);
+ return readWidePtr(widePtr);
+ }
+
+ public String callExport(ExportFn fn, String input) {
+ byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);
+ int inputPtr = allocAndWrite(inputBytes);
+ int widePtr = fn.apply(inputPtr, inputBytes.length);
+ exports.dealloc(inputPtr, inputBytes.length);
+ return readWidePtr(widePtr);
+ }
+
+ public String callExport(ExportFn2 fn, String arg1, String arg2) {
+ byte[] a1 = arg1.getBytes(StandardCharsets.UTF_8);
+ byte[] a2 = arg2.getBytes(StandardCharsets.UTF_8);
+ int p1 = allocAndWrite(a1);
+ int p2 = allocAndWrite(a2);
+ int widePtr = fn.apply(p1, a1.length, p2, a2.length);
+ exports.dealloc(p1, a1.length);
+ exports.dealloc(p2, a2.length);
+ return readWidePtr(widePtr);
+ }
+
+ // ── Internal ──────────────────────────────────────────────────────
+
+ private String callBuf(BufFn fn, String input) {
+ byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);
+ ensureBuf(inputBytes.length);
+ memory.write(inputBufPtr, inputBytes);
+ int written = fn.apply(inputBufPtr, inputBytes.length, outputBufPtr, outputBufCap);
+ if (written < 0) {
+ growOutputBuf(-written);
+ memory.write(inputBufPtr, inputBytes);
+ written = fn.apply(inputBufPtr, inputBytes.length, outputBufPtr, outputBufCap);
+ }
+ return new String(memory.readBytes(outputBufPtr, written), StandardCharsets.UTF_8);
+ }
+
+ private void ensureBuf(int needed) {
+ if (needed <= inputBufCap) return;
+ exports.dealloc(inputBufPtr, inputBufCap);
+ inputBufCap = needed * 2;
+ inputBufPtr = exports.alloc(inputBufCap);
+ }
+
+ private void growOutputBuf(int needed) {
+ exports.dealloc(outputBufPtr, outputBufCap);
+ outputBufCap = needed * 2;
+ outputBufPtr = exports.alloc(outputBufCap);
+ }
+
+ private int allocAndWrite(byte[] data) {
+ int ptr = exports.alloc(data.length);
+ memory.write(ptr, data);
+ return ptr;
+ }
+
+ String readWidePtr(int widePtr) {
+ int dataPtr = memory.readInt(widePtr);
+ int dataLen = memory.readInt(widePtr + 4);
+ byte[] bytes = memory.readBytes(dataPtr, dataLen);
+ exports.dealloc(dataPtr, dataLen);
+ exports.dealloc(widePtr, 8);
+ return new String(bytes, StandardCharsets.UTF_8);
+ }
+
+ @FunctionalInterface
+ public interface ExportFn {
+ int apply(int ptr, int len);
+ }
+
+ @FunctionalInterface
+ public interface ExportFn2 {
+ int apply(int p1, int l1, int p2, int l2);
+ }
+
+ @FunctionalInterface
+ private interface BufFn {
+ int apply(int inPtr, int inLen, int outPtr, int outCap);
+ }
+}
diff --git a/CedarWasm/core/src/test/java/com/cedarpolicy/wasm/CedarEngineTest.java b/CedarWasm/core/src/test/java/com/cedarpolicy/wasm/CedarEngineTest.java
new file mode 100644
index 00000000..9372fdde
--- /dev/null
+++ b/CedarWasm/core/src/test/java/com/cedarpolicy/wasm/CedarEngineTest.java
@@ -0,0 +1,217 @@
+package com.cedarpolicy.wasm;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class CedarEngineTest {
+
+ private static CedarEngine engine;
+
+ @BeforeAll
+ static void setUp() {
+ engine = CedarEngine.create();
+ }
+
+ @Test
+ void cedarVersion() {
+ assertEquals("4.0", engine.getVersion());
+ }
+
+ @Test
+ void simplePermit() {
+ String request = """
+ {
+ "principal": {"type": "User", "id": "alice"},
+ "action": {"type": "Action", "id": "view"},
+ "resource": {"type": "Resource", "id": "doc1"},
+ "context": {},
+ "policies": {
+ "staticPolicies": {"p0": "permit(principal,action,resource);"},
+ "templates": {},
+ "templateLinks": []
+ },
+ "entities": []
+ }
+ """;
+ String result = engine.authorize(request);
+ assertTrue(result.contains("\"decision\":\"allow\""), "Expected allow, got: " + result);
+ }
+
+ @Test
+ void simpleForbid() {
+ String request = """
+ {
+ "principal": {"type": "User", "id": "alice"},
+ "action": {"type": "Action", "id": "view"},
+ "resource": {"type": "Resource", "id": "doc1"},
+ "context": {},
+ "policies": {
+ "staticPolicies": {"p0": "forbid(principal,action,resource);"},
+ "templates": {},
+ "templateLinks": []
+ },
+ "entities": []
+ }
+ """;
+ String result = engine.authorize(request);
+ assertTrue(result.contains("\"decision\":\"deny\""), "Expected deny, got: " + result);
+ }
+
+ @Test
+ void parsePolicy() {
+ String result = engine.parsePolicy("permit(principal,action,resource);");
+ assertFalse(result.startsWith("ERROR:"), "Parse failed: " + result);
+ assertTrue(result.contains("permit"), "Expected permit in parsed output: " + result);
+ }
+
+ @Test
+ void parsePolicyInvalid() {
+ String result = engine.parsePolicy("not a valid policy");
+ assertTrue(result.startsWith("ERROR:"), "Expected error for invalid policy");
+ }
+
+ @Test
+ void policyEffect() {
+ assertEquals("permit", engine.policyEffect("permit(principal,action,resource);"));
+ assertEquals("forbid", engine.policyEffect("forbid(principal,action,resource);"));
+ }
+
+ @Test
+ void parseTemplate() {
+ String result = engine.parseTemplate(
+ "permit(principal==?principal,action,resource==?resource);");
+ assertFalse(result.startsWith("ERROR:"), "Parse failed: " + result);
+ }
+
+ @Test
+ void templateEffect() {
+ assertEquals("permit",
+ engine.templateEffect("permit(principal==?principal,action,resource==?resource);"));
+ }
+
+ @Test
+ void policyToJsonAndBack() {
+ String policyText = "permit(principal,action,resource);";
+ String json = engine.policyToJson(policyText);
+ assertFalse(json.startsWith("ERROR:"), "toJson failed: " + json);
+
+ String roundTripped = engine.policyFromJson(json);
+ assertFalse(roundTripped.startsWith("ERROR:"), "fromJson failed: " + roundTripped);
+ assertTrue(roundTripped.contains("permit"));
+ }
+
+ @Test
+ void policyAnnotations() {
+ String policy = "@id(\"myPolicy\") @myKey(\"myValue\") permit(principal,action,resource);";
+ String annotations = engine.getPolicyAnnotations(policy);
+ assertFalse(annotations.startsWith("ERROR:"), "annotations failed: " + annotations);
+ assertTrue(annotations.contains("\"id\""), "Expected 'id' annotation");
+ assertTrue(annotations.contains("\"myPolicy\""), "Expected 'myPolicy' value");
+ assertTrue(annotations.contains("\"myKey\""), "Expected 'myKey' annotation");
+ }
+
+ @Test
+ void parseJsonSchema() {
+ String schema = """
+ {
+ "": {
+ "entityTypes": {
+ "User": {"memberOfTypes": ["Group"]},
+ "Group": {},
+ "File": {}
+ },
+ "actions": {
+ "read": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["File"]
+ }
+ }
+ }
+ }
+ }
+ """;
+ String result = engine.parseJsonSchema(schema);
+ assertEquals("success", result);
+ }
+
+ @Test
+ void parseCedarSchema() {
+ String schema = """
+ entity User = { name: String };
+ entity Photo;
+ action view appliesTo {
+ principal: [User],
+ resource: [Photo]
+ };
+ """;
+ String result = engine.parseCedarSchema(schema);
+ assertEquals("success", result);
+ }
+
+ @Test
+ void formatPolicies() {
+ String result = engine.formatPolicies("permit(principal,action,resource);");
+ assertFalse(result.startsWith("ERROR:"), "format failed: " + result);
+ assertTrue(result.contains("permit"));
+ }
+
+ @Test
+ void validatePolicies() {
+ String request = """
+ {
+ "schema": {
+ "": {
+ "entityTypes": {
+ "User": {},
+ "Photo": {}
+ },
+ "actions": {
+ "viewPhoto": {
+ "appliesTo": {
+ "principalTypes": ["User"],
+ "resourceTypes": ["Photo"]
+ }
+ }
+ }
+ }
+ },
+ "policies": {
+ "staticPolicies": {
+ "p0": "permit(principal == User::\\"alice\\", action == Action::\\"viewPhoto\\", resource);"
+ },
+ "templates": {},
+ "templateLinks": []
+ }
+ }
+ """;
+ String result = engine.validate(request);
+ assertNotNull(result);
+ assertFalse(result.isEmpty());
+ }
+
+ @Test
+ void cachedAuthorization() {
+ String policiesJson = """
+ {"staticPolicies": {"p0": "permit(principal,action,resource);"}, "templates": {}, "templateLinks": []}
+ """;
+ String parseResult = engine.preParsePolicySet("test-policies", policiesJson);
+ assertFalse(parseResult.contains("errors"), "preparse failed: " + parseResult);
+
+ String request = """
+ {
+ "principal": {"type": "User", "id": "alice"},
+ "action": {"type": "Action", "id": "view"},
+ "resource": {"type": "Resource", "id": "doc1"},
+ "context": {},
+ "preparsedPolicySetId": "test-policies",
+ "validateRequest": false,
+ "entities": []
+ }
+ """;
+ String result = engine.statefulAuthorize(request);
+ assertTrue(result.contains("\"decision\":\"allow\""), "Expected allow, got: " + result);
+ }
+}
diff --git a/CedarWasm/core/wasm/cedar_wasm.wasm b/CedarWasm/core/wasm/cedar_wasm.wasm
new file mode 100644
index 00000000..3a813f2a
Binary files /dev/null and b/CedarWasm/core/wasm/cedar_wasm.wasm differ
diff --git a/CedarWasm/pom.xml b/CedarWasm/pom.xml
new file mode 100644
index 00000000..bb61d084
--- /dev/null
+++ b/CedarWasm/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+ com.cedarpolicy
+ cedar-wasm-parent
+ 4.0.0-SNAPSHOT
+ pom
+ Cedar Wasm for Java
+ Evaluate Cedar policies via Wasm using the Chicory Redline runtime
+
+
+
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0
+
+
+
+
+ core
+ benchmark
+
+
+
+ 17
+ 17
+ 17
+ 3.15.0
+ 3.5.5
+ UTF-8
+ 5.14.3
+ 0.0.4
+ 1.37
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven-compiler-plugin.version}
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+ ${maven.compiler.release}
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven-surefire-plugin.version}
+
+
+
+
diff --git a/CedarWasm/wasm-build/Cargo.toml b/CedarWasm/wasm-build/Cargo.toml
new file mode 100644
index 00000000..156fbea3
--- /dev/null
+++ b/CedarWasm/wasm-build/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "cedar-wasm"
+license = "Apache-2.0"
+description = "Cedar policy engine compiled to WebAssembly."
+edition = "2021"
+version = "4.0.0"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+miette = "7.6.0"
+itertools = "0.14"
+
+[features]
+partial-eval = ["cedar-policy/partial-eval"]
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies.cedar-policy]
+version = "4.0.0"
+git = "https://github.com/cedar-policy/cedar"
+branch = "main"
+
+[dependencies.cedar-policy-formatter]
+version = "4.0.0"
+git = "https://github.com/cedar-policy/cedar"
+branch = "main"
+
+[profile.release]
+opt-level = 3
+lto = true
+strip = true
diff --git a/CedarWasm/wasm-build/rust-toolchain.toml b/CedarWasm/wasm-build/rust-toolchain.toml
new file mode 100644
index 00000000..d0fee7c9
--- /dev/null
+++ b/CedarWasm/wasm-build/rust-toolchain.toml
@@ -0,0 +1,3 @@
+[toolchain]
+channel = "1.89"
+targets = ["wasm32-unknown-unknown"]
diff --git a/CedarWasm/wasm-build/src/lib.rs b/CedarWasm/wasm-build/src/lib.rs
new file mode 100644
index 00000000..fe67ff94
--- /dev/null
+++ b/CedarWasm/wasm-build/src/lib.rs
@@ -0,0 +1,592 @@
+use cedar_policy::entities_errors::EntitiesError;
+#[cfg(feature = "partial-eval")]
+use cedar_policy::ffi::is_authorized_partial_json_str;
+use cedar_policy::ffi::{
+ is_authorized_json_str, preparse_policy_set, preparse_schema,
+ schema_to_json, schema_to_text, stateful_is_authorized, validate_json_str,
+ CheckParseAnswer, PolicySet as PolicySetFFI, Schema as FFISchema,
+ SchemaToJsonAnswer, SchemaToTextAnswer, StatefulAuthorizationCall,
+};
+use cedar_policy::{Entities as CedarEntities, Policy, PolicySet, Schema, Template};
+use cedar_policy_formatter::{policies_str_to_pretty, Config};
+use serde::{Deserialize, Serialize};
+use serde_json::{from_str, Value};
+use std::alloc::Layout;
+use std::str::FromStr;
+
+// ── Memory management ──────────────────────────────────────────────────
+
+#[no_mangle]
+pub extern "C" fn alloc(size: usize) -> *mut u8 {
+ if size == 0 {
+ return std::ptr::null_mut();
+ }
+ let layout = Layout::from_size_align(size, 1).unwrap();
+ unsafe { std::alloc::alloc(layout) }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn dealloc(ptr: *mut u8, size: usize) {
+ if size > 0 && !ptr.is_null() {
+ let layout = Layout::from_size_align(size, 1).unwrap();
+ std::alloc::dealloc(ptr, layout);
+ }
+}
+
+// ── Wide pointer helpers ───────────────────────────────────────────────
+
+fn return_string(s: &str) -> *const u32 {
+ return_bytes(s.as_bytes())
+}
+
+fn return_bytes(data: &[u8]) -> *const u32 {
+ let len = data.len();
+ let data_ptr = alloc(len);
+ unsafe {
+ core::ptr::copy_nonoverlapping(data.as_ptr(), data_ptr, len);
+ let wide = alloc(8) as *mut u32;
+ core::ptr::write(wide, data_ptr as u32);
+ core::ptr::write(wide.add(1), len as u32);
+ wide as *const u32
+ }
+}
+
+fn read_str<'a>(ptr: *const u8, len: usize) -> &'a str {
+ unsafe { core::str::from_utf8_unchecked(core::slice::from_raw_parts(ptr, len)) }
+}
+
+// ── Answer type (mirrors CedarJavaFFI/src/answer.rs) ───────────────────
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(tag = "success")]
+enum Answer {
+ #[serde(rename = "true")]
+ Success { result: String },
+ #[serde(rename = "false")]
+ Failure {
+ #[serde(rename = "isInternal")]
+ is_internal: bool,
+ errors: Vec,
+ },
+}
+
+impl Answer {
+ fn fail_internally(message: String) -> Self {
+ Self::Failure {
+ is_internal: true,
+ errors: vec![message],
+ }
+ }
+
+ fn fail_bad_request(errors: Vec) -> Self {
+ Self::Failure {
+ is_internal: false,
+ errors,
+ }
+ }
+}
+
+// ── Validate entities ──────────────────────────────────────────────────
+
+#[derive(Serialize, Deserialize)]
+struct ValidateEntityCall {
+ schema: Value,
+ entities: Value,
+}
+
+fn json_validate_entities(input: &str) -> serde_json::Result {
+ let ans = validate_entities(input)?;
+ serde_json::to_string(&ans)
+}
+
+fn validate_entities(input: &str) -> serde_json::Result {
+ let call = from_str::(input)?;
+ let schema = match call.schema {
+ Value::String(cedarschema_str) => match Schema::from_cedarschema_str(&cedarschema_str) {
+ Ok(s) => s.0,
+ Err(e) => return Ok(Answer::fail_bad_request(vec![e.to_string()])),
+ },
+ cedarschema_json_obj => match Schema::from_json_value(cedarschema_json_obj) {
+ Ok(s) => s,
+ Err(e) => return Ok(Answer::fail_bad_request(vec![e.to_string()])),
+ },
+ };
+ match CedarEntities::from_json_value(call.entities, Some(&schema)) {
+ Err(error) => {
+ let err_message = match error {
+ EntitiesError::Serialization(err) => err.to_string(),
+ EntitiesError::Deserialization(err) => err.to_string(),
+ EntitiesError::Duplicate(err) => err.to_string(),
+ EntitiesError::TransitiveClosureError(err) => err.to_string(),
+ EntitiesError::InvalidEntity(err) => err.to_string(),
+ };
+ Ok(Answer::fail_bad_request(vec![err_message]))
+ }
+ Ok(_) => Ok(Answer::Success {
+ result: "null".to_string(),
+ }),
+ }
+}
+
+// ── Validate with level (helpers.rs port) ──────────────────────────────
+
+use cedar_policy::ffi::{
+ JsonValueWithNoDuplicateKeys, PolicySet as FFIPolicies, ValidationAnswer, ValidationError,
+};
+use cedar_policy::{SchemaFragment, SchemaWarning, ValidationMode, Validator};
+
+#[derive(Serialize, Deserialize, Debug, Default)]
+#[serde(rename_all = "camelCase")]
+#[serde(deny_unknown_fields)]
+struct ValidationSettings {
+ mode: ValidationMode,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+#[serde(deny_unknown_fields)]
+struct LevelValidationCall {
+ #[serde(default)]
+ validation_settings: ValidationSettings,
+ schema: FFISchemaLocal,
+ policies: FFIPolicies,
+ max_deref_level: u32,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(untagged)]
+enum FFISchemaLocal {
+ Cedar(String),
+ Json(JsonValueWithNoDuplicateKeys),
+}
+
+impl FFISchemaLocal {
+ fn parse(self) -> Result<(Schema, Box>), miette::Report> {
+ use miette::Context;
+ let (frag, warnings) = match self {
+ Self::Cedar(s) => {
+ let (frag, w) = SchemaFragment::from_cedarschema_str(&s)
+ .wrap_err("failed to parse schema from string")?;
+ (
+ frag,
+ Box::new(w) as Box>,
+ )
+ }
+ Self::Json(val) => {
+ let frag = SchemaFragment::from_json_value(val.into())
+ .wrap_err("failed to parse schema from JSON")?;
+ (
+ frag,
+ Box::new(std::iter::empty()) as Box>,
+ )
+ }
+ };
+ Ok((frag.try_into()?, warnings))
+ }
+}
+
+fn validate_with_level_json_str(json: &str) -> Result {
+ let call: LevelValidationCall = serde_json::from_str(json)?;
+ let ans = validate_with_level(call);
+ serde_json::to_string(&ans)
+}
+
+fn validate_with_level(call: LevelValidationCall) -> ValidationAnswer {
+ let mut errs = vec![];
+ let policies = match call.policies.parse() {
+ Ok(p) => p,
+ Err(e) => {
+ errs.extend(e);
+ PolicySet::new()
+ }
+ };
+ let pair = match call.schema.parse() {
+ Ok((schema, warnings)) => Some((schema, warnings)),
+ Err(e) => {
+ errs.push(e);
+ None
+ }
+ };
+ match (errs.is_empty(), pair) {
+ (true, Some((schema, warnings))) => {
+ let validator = Validator::new(schema);
+ let result =
+ validator.validate_with_level(&policies, call.validation_settings.mode, call.max_deref_level);
+ let validation_errors: Vec = result
+ .validation_errors()
+ .map(|e| ValidationError {
+ policy_id: e.policy_id().clone(),
+ error: miette::Report::new(e.clone()).into(),
+ })
+ .collect();
+ let validation_warnings: Vec = result
+ .validation_warnings()
+ .map(|e| ValidationError {
+ policy_id: e.policy_id().clone(),
+ error: miette::Report::new(e.clone()).into(),
+ })
+ .collect();
+ ValidationAnswer::Success {
+ validation_errors,
+ validation_warnings,
+ other_warnings: warnings.map(|w| miette::Report::new(w).into()).collect(),
+ }
+ }
+ _ => ValidationAnswer::Failure {
+ errors: errs.into_iter().map(Into::into).collect(),
+ warnings: vec![],
+ },
+ }
+}
+
+// ── Main dispatcher ────────────────────────────────────────────────────
+
+const V0_AUTH_OP: &str = "AuthorizationOperation";
+#[cfg(feature = "partial-eval")]
+const V0_AUTH_PARTIAL_OP: &str = "AuthorizationPartialOperation";
+const V0_VALIDATE_OP: &str = "ValidateOperation";
+const V0_VALIDATE_LEVEL_OP: &str = "ValidateWithLevelOperation";
+const V0_VALIDATE_ENTITIES: &str = "ValidateEntities";
+const V0_STATEFUL_AUTH_OP: &str = "StatefulAuthorizationOperation";
+
+fn stateful_is_authorized_json_str(json: &str) -> Result {
+ let call: StatefulAuthorizationCall = serde_json::from_str(json)?;
+ let ans = stateful_is_authorized(call);
+ serde_json::to_string(&ans)
+}
+
+fn call_cedar(call: &str, input: &str) -> String {
+ let result = match call {
+ V0_AUTH_OP => is_authorized_json_str(input),
+ #[cfg(feature = "partial-eval")]
+ V0_AUTH_PARTIAL_OP => is_authorized_partial_json_str(input),
+ V0_VALIDATE_OP => validate_json_str(input),
+ V0_VALIDATE_ENTITIES => json_validate_entities(input),
+ V0_VALIDATE_LEVEL_OP => validate_with_level_json_str(input),
+ V0_STATEFUL_AUTH_OP => stateful_is_authorized_json_str(input),
+ _ => {
+ let ires = Answer::fail_internally(format!("unsupported operation: {}", call));
+ serde_json::to_string(&ires)
+ }
+ };
+ result.unwrap_or_else(|err| {
+ panic!("failed to handle call {call} with input {input}\nError: {err}")
+ })
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_call(
+ op_ptr: *const u8,
+ op_len: usize,
+ input_ptr: *const u8,
+ input_len: usize,
+) -> *const u32 {
+ let op = read_str(op_ptr, op_len);
+ let input = read_str(input_ptr, input_len);
+ let result = call_cedar(op, input);
+ return_string(&result)
+}
+
+// ── Version ────────────────────────────────────────────────────────────
+
+#[no_mangle]
+pub extern "C" fn cedar_version() -> *const u32 {
+ return_string("4.0")
+}
+
+// ── Policy utilities ───────────────────────────────────────────────────
+
+#[no_mangle]
+pub extern "C" fn cedar_parse_policy(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match Policy::from_str(input) {
+ Ok(p) => return_string(&p.to_string()),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_parse_template(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match Template::from_str(input) {
+ Ok(t) => return_string(&t.to_string()),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_policy_effect(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match Policy::from_str(input) {
+ Ok(p) => return_string(&p.effect().to_string()),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_template_effect(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match Template::from_str(input) {
+ Ok(t) => return_string(&t.effect().to_string()),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_policy_to_json(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match Policy::from_str(input) {
+ Ok(p) => match serde_json::to_string(&p.to_json().unwrap()) {
+ Ok(json) => return_string(&json),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ },
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_policy_from_json(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match serde_json::from_str::(input) {
+ Ok(json_val) => match Policy::from_json(None, json_val) {
+ Ok(p) => return_string(&p.to_string()),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ },
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_get_policy_annotations(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match Policy::from_str(input) {
+ Ok(p) => {
+ let map: std::collections::HashMap<&str, &str> = p.annotations().collect();
+ match serde_json::to_string(&map) {
+ Ok(json) => return_string(&json),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+ }
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_get_template_annotations(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match Template::from_str(input) {
+ Ok(t) => {
+ let map: std::collections::HashMap<&str, &str> = t.annotations().collect();
+ match serde_json::to_string(&map) {
+ Ok(json) => return_string(&json),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+ }
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_parse_policies(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match PolicySet::from_str(input) {
+ Ok(ps) => {
+ let policies: Vec = ps
+ .policies()
+ .map(|p| {
+ serde_json::json!({
+ "id": p.id().to_string(),
+ "text": p.to_string(),
+ })
+ })
+ .collect();
+ let templates: Vec = ps
+ .templates()
+ .map(|t| {
+ serde_json::json!({
+ "id": t.id().to_string(),
+ "text": t.to_string(),
+ })
+ })
+ .collect();
+ let result = serde_json::json!({
+ "policies": policies,
+ "templates": templates,
+ });
+ return_string(&result.to_string())
+ }
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_policy_set_to_json(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match serde_json::from_str::(input) {
+ Ok(ffi_ps) => match ffi_ps.parse() {
+ Ok(ps) => match serde_json::to_string(&ps.to_json().unwrap()) {
+ Ok(json) => return_string(&json),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ },
+ Err(errs) => {
+ let msg = errs
+ .into_iter()
+ .map(|e| e.to_string())
+ .collect::>()
+ .join("; ");
+ return_string(&format!("ERROR:{}", msg))
+ }
+ },
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_format_policies(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match policies_str_to_pretty(input, &Config::default()) {
+ Ok(formatted) => return_string(&formatted),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+// ── Schema utilities ───────────────────────────────────────────────────
+
+#[no_mangle]
+pub extern "C" fn cedar_parse_json_schema(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match Schema::from_json_str(input) {
+ Ok(_) => return_string("success"),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_parse_cedar_schema(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match Schema::from_cedarschema_str(input) {
+ Ok(_) => return_string("success"),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_schema_to_cedar(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match serde_json::from_str::(input) {
+ Ok(schema) => match schema_to_text(schema) {
+ SchemaToTextAnswer::Success { text, .. } => return_string(&text),
+ SchemaToTextAnswer::Failure { errors } => {
+ let msg = errors.iter().map(|e| e.message.clone()).collect::>().join("; ");
+ return_string(&format!("ERROR:{}", msg))
+ }
+ },
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_schema_to_json(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ let cedar_schema = FFISchema::Cedar(input.to_string());
+ match schema_to_json(cedar_schema) {
+ SchemaToJsonAnswer::Success { json, .. } => {
+ match serde_json::to_string_pretty(&json) {
+ Ok(s) => return_string(&s),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+ }
+ SchemaToJsonAnswer::Failure { errors } => {
+ let msg = errors.iter().map(|e| e.message.clone()).collect::>().join("; ");
+ return_string(&format!("ERROR:{}", msg))
+ }
+ }
+}
+
+// ── Caching ────────────────────────────────────────────────────────────
+
+#[no_mangle]
+pub extern "C" fn cedar_preparse_policy_set(
+ id_ptr: *const u8, id_len: usize,
+ json_ptr: *const u8, json_len: usize,
+) -> *const u32 {
+ let id = read_str(id_ptr, id_len).to_string();
+ let json = read_str(json_ptr, json_len);
+ match serde_json::from_str::(json) {
+ Ok(policies) => {
+ let ans = preparse_policy_set(id, policies);
+ return_string(&serde_json::to_string(&ans).unwrap())
+ }
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_preparse_schema(
+ id_ptr: *const u8, id_len: usize,
+ json_ptr: *const u8, json_len: usize,
+) -> *const u32 {
+ let id = read_str(id_ptr, id_len).to_string();
+ let json = read_str(json_ptr, json_len);
+ match serde_json::from_str::(json) {
+ Ok(schema) => {
+ let ans = preparse_schema(id, schema);
+ return_string(&serde_json::to_string(&ans).unwrap())
+ }
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn cedar_stateful_authorize(ptr: *const u8, len: usize) -> *const u32 {
+ let input = read_str(ptr, len);
+ match stateful_is_authorized_json_str(input) {
+ Ok(result) => return_string(&result),
+ Err(e) => return_string(&format!("ERROR:{}", e)),
+ }
+}
+
+/// Single-crossing stateful authorization.
+/// Input is already in Wasm memory at `input_ptr` (written by host via memory.write).
+/// Result is written to `out_ptr` (a host-provided buffer).
+/// Returns the number of bytes written to out_ptr, or negative on overflow.
+#[no_mangle]
+pub extern "C" fn cedar_stateful_authorize_buf(
+ input_ptr: *const u8, input_len: usize,
+ out_ptr: *mut u8, out_cap: usize,
+) -> i32 {
+ let input = read_str(input_ptr, input_len);
+ let result = match stateful_is_authorized_json_str(input) {
+ Ok(r) => r,
+ Err(e) => format!("ERROR:{}", e),
+ };
+ let result_bytes = result.as_bytes();
+ if result_bytes.len() > out_cap {
+ return -(result_bytes.len() as i32);
+ }
+ unsafe {
+ core::ptr::copy_nonoverlapping(result_bytes.as_ptr(), out_ptr, result_bytes.len());
+ }
+ result_bytes.len() as i32
+}
+
+/// Same for regular (non-cached) authorization.
+#[no_mangle]
+pub extern "C" fn cedar_authorize_buf(
+ input_ptr: *const u8, input_len: usize,
+ out_ptr: *mut u8, out_cap: usize,
+) -> i32 {
+ let input = read_str(input_ptr, input_len);
+ let result = match is_authorized_json_str(input) {
+ Ok(r) => r,
+ Err(e) => format!("ERROR:{}", e),
+ };
+ let result_bytes = result.as_bytes();
+ if result_bytes.len() > out_cap {
+ return -(result_bytes.len() as i32);
+ }
+ unsafe {
+ core::ptr::copy_nonoverlapping(result_bytes.as_ptr(), out_ptr, result_bytes.len());
+ }
+ result_bytes.len() as i32
+}