From c73cc3cc6bb6ae277d0f4c09984816196c197df5 Mon Sep 17 00:00:00 2001 From: Chris Simmons <140448826+geekphilosophy@users.noreply.github.com> Date: Mon, 11 May 2026 12:26:00 -0700 Subject: [PATCH 1/3] Improve toCedarExpr() correctness and add CedarStrings utility (#351) Signed-off-by: Chris Simmons Co-authored-by: Chris Simmons --- .../serializer/ValueCedarSerializer.java | 8 +- .../java/com/cedarpolicy/value/CedarMap.java | 25 ++++- .../com/cedarpolicy/value/CedarStrings.java | 63 ++++++++++++ .../com/cedarpolicy/value/PrimString.java | 2 +- .../cedarpolicy/CedarExprEscapingTests.java | 72 ++++++++++++++ .../cedarpolicy/CedarMapReservedKeyTests.java | 96 +++++++++++++++++++ 6 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 CedarJava/src/main/java/com/cedarpolicy/value/CedarStrings.java create mode 100644 CedarJava/src/test/java/com/cedarpolicy/CedarExprEscapingTests.java create mode 100644 CedarJava/src/test/java/com/cedarpolicy/CedarMapReservedKeyTests.java diff --git a/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueCedarSerializer.java b/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueCedarSerializer.java index 6c9a6547..9df9cb44 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueCedarSerializer.java +++ b/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueCedarSerializer.java @@ -68,7 +68,13 @@ public void serialize( } else if (value instanceof CedarMap) { jsonGenerator.writeStartObject(); for (Map.Entry entry : ((CedarMap) value).entrySet()) { - jsonGenerator.writeObjectField(entry.getKey(), entry.getValue()); + String key = entry.getKey(); + if (ENTITY_ESCAPE_SEQ.equals(key) || EXTENSION_ESCAPE_SEQ.equals(key)) { + throw new InvalidValueSerializationException( + "CedarMap key \"" + key + "\" is reserved by the Cedar JSON protocol" + + " and cannot be used as a record key."); + } + jsonGenerator.writeObjectField(key, entry.getValue()); } jsonGenerator.writeEndObject(); } else if (value instanceof IpAddress) { diff --git a/CedarJava/src/main/java/com/cedarpolicy/value/CedarMap.java b/CedarJava/src/main/java/com/cedarpolicy/value/CedarMap.java index 316e878f..2dbe40d1 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/value/CedarMap.java +++ b/CedarJava/src/main/java/com/cedarpolicy/value/CedarMap.java @@ -25,6 +25,10 @@ /** Represents a Cedar Map value. Maps support mapping strings to arbitrary values. */ public final class CedarMap extends Value implements Map { + /** Reserved keys in the Cedar JSON protocol that cannot be used as record keys. */ + private static final String ENTITY_ESCAPE_SEQ = "__entity"; + private static final String EXTENSION_ESCAPE_SEQ = "__extn"; + /** Internal map data. */ private final java.util.Map map; @@ -34,6 +38,13 @@ public final class CedarMap extends Value implements Map { * @param source map to copy from */ public CedarMap(java.util.Map source) { + for (String key : source.keySet()) { + if (ENTITY_ESCAPE_SEQ.equals(key) || EXTENSION_ESCAPE_SEQ.equals(key)) { + throw new IllegalArgumentException( + "Key \"" + key + "\" is reserved by the Cedar JSON protocol" + + " and cannot be used as a record key."); + } + } this.map = new HashMap<>(source); } @@ -66,7 +77,7 @@ public int hashCode() { public String toCedarExpr() { return "{" + map.entrySet().stream() - .map(e -> '\"' + e.getKey() + "\": " + e.getValue().toCedarExpr()) + .map(e -> '\"' + CedarStrings.escape(e.getKey()) + "\": " + e.getValue().toCedarExpr()) .collect(Collectors.joining(", ")) + "}"; } @@ -118,12 +129,24 @@ public Value put(String k, Value v) throws NullPointerException { if (v == null) { throw new NullPointerException("Attempt to put null value in CedarMap"); } + if (ENTITY_ESCAPE_SEQ.equals(k) || EXTENSION_ESCAPE_SEQ.equals(k)) { + throw new IllegalArgumentException( + "Key \"" + k + "\" is reserved by the Cedar JSON protocol" + + " and cannot be used as a record key."); + } return map.put(k, v); } @Override public void putAll(Map m) { + for (String key : m.keySet()) { + if (ENTITY_ESCAPE_SEQ.equals(key) || EXTENSION_ESCAPE_SEQ.equals(key)) { + throw new IllegalArgumentException( + "Key \"" + key + "\" is reserved by the Cedar JSON protocol" + + " and cannot be used as a record key."); + } + } map.putAll(m); } diff --git a/CedarJava/src/main/java/com/cedarpolicy/value/CedarStrings.java b/CedarJava/src/main/java/com/cedarpolicy/value/CedarStrings.java new file mode 100644 index 00000000..5f954033 --- /dev/null +++ b/CedarJava/src/main/java/com/cedarpolicy/value/CedarStrings.java @@ -0,0 +1,63 @@ +/* + * Copyright Cedar Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cedarpolicy.value; + +/** + * Utility methods for working with Cedar string literals. + */ +public final class CedarStrings { + + private CedarStrings() { } + + /** + * Escape a string for safe inclusion in Cedar source code as a string literal. + * Handles backslashes, double quotes, and control characters recognized by Cedar. + * + * @param s the raw string value + * @return the escaped string (without surrounding quotes) + */ + public static String escape(String s) { + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '\\': + sb.append("\\\\"); + break; + case '"': + sb.append("\\\""); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + case '\0': + sb.append("\\0"); + break; + default: + sb.append(c); + break; + } + } + return sb.toString(); + } +} diff --git a/CedarJava/src/main/java/com/cedarpolicy/value/PrimString.java b/CedarJava/src/main/java/com/cedarpolicy/value/PrimString.java index c3419d2a..ed9533a9 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/value/PrimString.java +++ b/CedarJava/src/main/java/com/cedarpolicy/value/PrimString.java @@ -60,6 +60,6 @@ public String toString() { /** To Cedar expr that can be used in a Cedar policy. */ @Override public String toCedarExpr() { - return "\"" + value + "\""; + return "\"" + CedarStrings.escape(value) + "\""; } } diff --git a/CedarJava/src/test/java/com/cedarpolicy/CedarExprEscapingTests.java b/CedarJava/src/test/java/com/cedarpolicy/CedarExprEscapingTests.java new file mode 100644 index 00000000..d11ea45e --- /dev/null +++ b/CedarJava/src/test/java/com/cedarpolicy/CedarExprEscapingTests.java @@ -0,0 +1,72 @@ +/* + * Copyright Cedar Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cedarpolicy; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.cedarpolicy.value.CedarMap; +import com.cedarpolicy.value.PrimString; +import com.cedarpolicy.value.Value; + +public class CedarExprEscapingTests { + + @Test + public void testPrimStringEscapesDoubleQuotes() { + PrimString s = new PrimString("x\" && false && \"x"); + assertEquals("\"x\\\" && false && \\\"x\"", s.toCedarExpr()); + } + + @Test + public void testPrimStringEscapesBackslash() { + PrimString s = new PrimString("path\\to\\file"); + assertEquals("\"path\\\\to\\\\file\"", s.toCedarExpr()); + } + + @Test + public void testPrimStringEscapesControlCharacters() { + PrimString s = new PrimString("line1\nline2\ttab\r\0"); + assertEquals("\"line1\\nline2\\ttab\\r\\0\"", s.toCedarExpr()); + } + + @Test + public void testPrimStringPlainStringUnchanged() { + PrimString s = new PrimString("hello world"); + assertEquals("\"hello world\"", s.toCedarExpr()); + } + + @Test + public void testPrimStringEmptyString() { + PrimString s = new PrimString(""); + assertEquals("\"\"", s.toCedarExpr()); + } + + @Test + public void testCedarMapEscapesKeys() { + Map source = new HashMap<>(); + source.put("key\"injection", new PrimString("value")); + CedarMap map = new CedarMap(source); + String expr = map.toCedarExpr(); + assertTrue(expr.contains("key\\\"injection")); + assertFalse(expr.contains("key\"injection")); + } + +} diff --git a/CedarJava/src/test/java/com/cedarpolicy/CedarMapReservedKeyTests.java b/CedarJava/src/test/java/com/cedarpolicy/CedarMapReservedKeyTests.java new file mode 100644 index 00000000..9706ac09 --- /dev/null +++ b/CedarJava/src/test/java/com/cedarpolicy/CedarMapReservedKeyTests.java @@ -0,0 +1,96 @@ +/* + * Copyright Cedar Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cedarpolicy; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.cedarpolicy.value.CedarMap; +import com.cedarpolicy.value.PrimString; +import com.cedarpolicy.value.Value; + +public class CedarMapReservedKeyTests { + + @Test + public void testPutRejectsEntityKey() { + CedarMap map = new CedarMap(); + assertThrows(IllegalArgumentException.class, () -> { + map.put("__entity", new PrimString("spoofed")); + }); + } + + @Test + public void testPutRejectsExtnKey() { + CedarMap map = new CedarMap(); + assertThrows(IllegalArgumentException.class, () -> { + map.put("__extn", new PrimString("spoofed")); + }); + } + + @Test + public void testConstructorRejectsEntityKey() { + Map source = new HashMap<>(); + source.put("__entity", new PrimString("spoofed")); + assertThrows(IllegalArgumentException.class, () -> { + new CedarMap(source); + }); + } + + @Test + public void testConstructorRejectsExtnKey() { + Map source = new HashMap<>(); + source.put("__extn", new PrimString("spoofed")); + assertThrows(IllegalArgumentException.class, () -> { + new CedarMap(source); + }); + } + + @Test + public void testPutAllRejectsEntityKey() { + CedarMap map = new CedarMap(); + Map source = new HashMap<>(); + source.put("safe", new PrimString("ok")); + source.put("__entity", new PrimString("spoofed")); + assertThrows(IllegalArgumentException.class, () -> { + map.putAll(source); + }); + } + + @Test + public void testPutAllRejectsExtnKey() { + CedarMap map = new CedarMap(); + Map source = new HashMap<>(); + source.put("__extn", new PrimString("spoofed")); + assertThrows(IllegalArgumentException.class, () -> { + map.putAll(source); + }); + } + + @Test + public void testNormalKeysStillWork() { + CedarMap map = new CedarMap(); + assertDoesNotThrow(() -> { + map.put("entity", new PrimString("ok")); + map.put("__other", new PrimString("ok")); + map.put("_entity", new PrimString("ok")); + }); + } +} From a141d58c2cdc9171b2dcb6677ae5107540cd2674 Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Wed, 13 May 2026 11:30:32 -0400 Subject: [PATCH 2/3] update github workflow to use latest stable rust Signed-off-by: Mudit Chaudhary --- .github/workflows/run_cedar_java_reusable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_cedar_java_reusable.yml b/.github/workflows/run_cedar_java_reusable.yml index 4985c46d..5ef8b699 100644 --- a/.github/workflows/run_cedar_java_reusable.yml +++ b/.github/workflows/run_cedar_java_reusable.yml @@ -40,7 +40,7 @@ jobs: ref: ${{ inputs.cedar_policy_ref }} path: ./cedar - name: Prepare Rust Build - run: rustup install 1.80.0 && rustup default 1.80.0 && rustup component add rustfmt + run: rustup install stable && rustup default stable && rustup component add rustfmt - name: Configure CedarJavaFFI for CI build run: bash configure_ci_build.sh - name: Check FFI Formatting From 15bb98808c7369edd3b9ccb12d80976164e2e928 Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Wed, 13 May 2026 11:33:40 -0400 Subject: [PATCH 3/3] bump CedarJava version to 3.4.1 --- CedarJava/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CedarJava/build.gradle b/CedarJava/build.gradle index 73c6e7d9..3b489691 100644 --- a/CedarJava/build.gradle +++ b/CedarJava/build.gradle @@ -233,7 +233,7 @@ publishing { from components.java groupId = 'com.cedarpolicy' artifactId = 'cedar-java' - version = '3.4.0' + version = '3.4.1' artifacts { jar