diff --git a/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueDeserializer.java b/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueDeserializer.java index b2cf656..f36c3fd 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueDeserializer.java +++ b/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueDeserializer.java @@ -28,6 +28,7 @@ import com.cedarpolicy.value.PrimBool; import com.cedarpolicy.value.PrimLong; import com.cedarpolicy.value.PrimString; +import com.cedarpolicy.value.Unknown; import com.cedarpolicy.value.Value; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; @@ -120,11 +121,12 @@ public Value deserialize(JsonParser parser, DeserializationContext context) thro throw new InvalidValueDeserializationException(parser, "Not textual node: " + arg.toString(), node.asToken(), Map.class); } - if (fn.textValue().equals("ip")) { return new IpAddress(arg.textValue()); } else if (fn.textValue().equals("decimal")) { return new Decimal(arg.textValue()); + } else if (fn.textValue().equals("unknown")) { + return new Unknown(arg.textValue()); } else { throw new InvalidValueDeserializationException(parser, "Invalid function type: " + fn.toString(), node.asToken(), Map.class); diff --git a/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueSerializer.java b/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueSerializer.java index 11f0d39..4bf45de 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueSerializer.java +++ b/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueSerializer.java @@ -25,6 +25,7 @@ import com.cedarpolicy.value.PrimBool; import com.cedarpolicy.value.PrimLong; import com.cedarpolicy.value.PrimString; +import com.cedarpolicy.value.Unknown; import com.cedarpolicy.value.Value; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; @@ -91,6 +92,16 @@ public void serialize( jsonGenerator.writeString(value.toString()); jsonGenerator.writeEndObject(); jsonGenerator.writeEndObject(); + } else if (value instanceof Unknown) { + jsonGenerator.writeStartObject(); + jsonGenerator.writeFieldName(EXTENSION_ESCAPE_SEQ); + jsonGenerator.writeStartObject(); + jsonGenerator.writeFieldName("fn"); + jsonGenerator.writeString("unknown"); + jsonGenerator.writeFieldName("arg"); + jsonGenerator.writeString(value.toString()); + jsonGenerator.writeEndObject(); + jsonGenerator.writeEndObject(); } else { // It is recommended that you extend the Value classes in // main.java.com.cedarpolicy.model.value or that you convert your class to a CedarMap diff --git a/CedarJava/src/main/java/com/cedarpolicy/value/Unknown.java b/CedarJava/src/main/java/com/cedarpolicy/value/Unknown.java new file mode 100644 index 0000000..4fedc93 --- /dev/null +++ b/CedarJava/src/main/java/com/cedarpolicy/value/Unknown.java @@ -0,0 +1,85 @@ +/* + * 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; + +import java.util.Objects; + +import com.cedarpolicy.Experimental; +import com.cedarpolicy.ExperimentalFeature; + + +/** + * Represents a Cedar unknown extension value. + * This class can only be used with partial evaluation. + */ +@Experimental(ExperimentalFeature.PARTIAL_EVALUATION) +public class Unknown extends Value { + + + /** + * arg as a string. + */ + private final String arg; + + /** + * Construct Unknown. + * + * @param arg for the unknown extension + */ + public Unknown(String arg) throws NullPointerException, IllegalArgumentException { + this.arg = arg; + } + + /** + * Convert Decimal to Cedar expr that can be used in a Cedar policy. + */ + @Override + public String toCedarExpr() { + return "Unknown(\"" + arg + "\")"; + } + + /** + * Equals. + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Unknown unknown = (Unknown) o; + return arg.equals(unknown.arg); + } + + /** + * Hash. + */ + @Override + public int hashCode() { + return Objects.hash(arg); + } + + /** + * As a string. + */ + @Override + public String toString() { + return arg; + } +} diff --git a/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java b/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java index 8b4aa7d..703f6ba 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java @@ -30,11 +30,15 @@ import com.cedarpolicy.model.policy.PolicySet; import com.cedarpolicy.value.EntityTypeName; import com.cedarpolicy.value.EntityUID; +import com.cedarpolicy.value.Unknown; +import com.cedarpolicy.value.Value; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import static org.junit.jupiter.api.Assertions.*; +import java.util.Map; import java.util.Set; public class AuthTests { @@ -100,6 +104,27 @@ public void residual() { }); } + @Test + public void residualWithUnknownValue() { + var auth = new BasicAuthorizationEngine(); + var alice = new EntityUID(EntityTypeName.parse("User").get(), "alice"); + var view = new EntityUID(EntityTypeName.parse("Action").get(), "view"); + Map context = Map.of("authenticated", new Unknown("AuthenticatedIsUnknown")); + var q = PartialAuthorizationRequest.builder().principal(alice).action(view).resource(alice).context(context).build(); + var policies = new HashSet(); + policies.add(new Policy("permit(principal == User::\"alice\",action,resource) when{context.authenticated};", "p0")); + var policySet = new PolicySet(policies); + assumePartialEvaluation(() -> { + try { + final PartialAuthorizationResponse response = auth.isAuthorizedPartial(q, policySet, new HashSet<>()); + assertNull(response.success.orElseThrow().getDecision()); + assertEquals("p0", response.success.orElseThrow().getResiduals().entrySet().iterator().next().getKey()); + } catch (Exception e) { + fail("error: " + e.toString()); + } + }); + } + private void assumePartialEvaluation(Executable executable) { try { executable.execute(); diff --git a/CedarJava/src/test/java/com/cedarpolicy/JSONTests.java b/CedarJava/src/test/java/com/cedarpolicy/JSONTests.java index b3824e4..25ceb68 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/JSONTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/JSONTests.java @@ -31,6 +31,7 @@ import com.cedarpolicy.value.PrimBool; import com.cedarpolicy.value.PrimLong; import com.cedarpolicy.value.PrimString; +import com.cedarpolicy.value.Unknown; import com.cedarpolicy.value.Value; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.exc.StreamConstraintsException; @@ -242,6 +243,27 @@ public void testList() { assertJSONEqual(listJson, l); } + @Test + public void testUnknown() { + Unknown unknown = new Unknown("test"); + ObjectNode n = JsonNodeFactory.instance.objectNode(); + ObjectNode inner = JsonNodeFactory.instance.objectNode(); + inner.put("fn", "unknown"); + inner.put("arg", "test"); + n.replace("__extn", inner); + assertJSONEqual(n, unknown); + } + + /** Tests deserialization of unknown value */ + @Test + public void testDeserializationUnknown() throws JsonProcessingException { + String json = "{\"__extn\":{\"fn\":\"unknown\",\"arg\":\"test\"}}"; + Value value = CedarJson.objectMapper().readValue(json, Value.class); + assertInstanceOf(Unknown.class, value); + Unknown unknown = (Unknown) value; + assertEquals("test", unknown.toString()); + } + /** Tests deserialization of value that causes stack overflow */ @Test public void testDeserializationStackOverflow() {