diff --git a/CedarJava/build.gradle b/CedarJava/build.gradle index 8049169a..ef81c444 100644 --- a/CedarJava/build.gradle +++ b/CedarJava/build.gradle @@ -85,6 +85,7 @@ dependencies { compileOnly 'com.github.spotbugs:spotbugs-annotations:4.8.6' testImplementation 'net.jqwik:jqwik:1.9.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.4' + testImplementation 'org.skyscreamer:jsonassert:2.0-rc1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.4' } diff --git a/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java b/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java index 25f64dc0..abcde75e 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java +++ b/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java @@ -16,14 +16,21 @@ package com.cedarpolicy; -import com.cedarpolicy.model.*; +import java.util.Set; + +import com.cedarpolicy.model.AuthorizationRequest; +import com.cedarpolicy.model.AuthorizationResponse; +import com.cedarpolicy.model.EntityValidationRequest; +import com.cedarpolicy.model.PartialAuthorizationRequest; +import com.cedarpolicy.model.PartialAuthorizationResponse; +import com.cedarpolicy.model.ValidationRequest; +import com.cedarpolicy.model.ValidationResponse; +import com.cedarpolicy.model.entity.Entities; +import com.cedarpolicy.model.entity.Entity; import com.cedarpolicy.model.exception.AuthException; import com.cedarpolicy.model.exception.BadRequestException; -import com.cedarpolicy.model.entity.Entity; import com.cedarpolicy.model.policy.PolicySet; -import java.util.Set; - /** * Implementations of the AuthorizationEngine interface invoke Cedar to respond to an authorization * or validation request. For authorization, the input includes the relevant policies and entities for @@ -51,6 +58,21 @@ public interface AuthorizationEngine { */ AuthorizationResponse isAuthorized(AuthorizationRequest request, PolicySet policySet, Set entities) throws AuthException; + /** + * Asks whether the given AuthorizationRequest q is approved by the policySet and + * entities hierarchy given. Overloaded method to accept Entities object. + * + * @param request The request to evaluate + * @param policySet The policy set to evaluate against + * @param entities The entities to evaluate against + * @return The result of the request evaluation + * @throws BadRequestException if any errors were found in the syntax of the policies. + * @throws AuthException On failure to make the authorization request. Note that errors inside the + * authorization engine are included in the errors field on the + * AuthorizationResponse. + */ + AuthorizationResponse isAuthorized(AuthorizationRequest request, PolicySet policySet, Entities entities) throws AuthException; + /** * Asks whether the given AuthorizationRequest q is approved by the policySet and * entities given. If information required to answer is missing, residual policies are returned. @@ -68,6 +90,24 @@ public interface AuthorizationEngine { PartialAuthorizationResponse isAuthorizedPartial(PartialAuthorizationRequest request, PolicySet policySet, Set entities) throws AuthException; + /** + * Asks whether the given AuthorizationRequest q is approved by the policySet and + * entities given. If information required to answer is missing, residual policies are returned. + * Overloaded method to accept Entities object. + * + * @param request The request to evaluate + * @param policySet The policy set to evaluate against + * @param entities The entities to evaluate against + * @return The result of the request evaluation + * @throws BadRequestException if any errors were found in the syntax of the policies. + * @throws AuthException On failure to make the authorization request. Note that errors inside the + * authorization engine are included in the errors field on the + * AuthorizationResponse. + */ + @Experimental(ExperimentalFeature.PARTIAL_EVALUATION) + PartialAuthorizationResponse isAuthorizedPartial(PartialAuthorizationRequest request, + PolicySet policySet, Entities entities) throws AuthException; + /** * Asks whether the policies in the given {@link ValidationRequest} q are correct * when validated against the schema it describes. diff --git a/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java b/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java index 2cdde63c..1b001bb6 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java +++ b/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java @@ -20,14 +20,21 @@ import static com.cedarpolicy.CedarJson.objectWriter; import java.io.IOException; +import java.util.List; +import java.util.Set; import com.cedarpolicy.loader.LibraryLoader; -import com.cedarpolicy.model.*; +import com.cedarpolicy.model.AuthorizationResponse; +import com.cedarpolicy.model.EntityValidationRequest; +import com.cedarpolicy.model.PartialAuthorizationResponse; +import com.cedarpolicy.model.ValidationRequest; +import com.cedarpolicy.model.ValidationResponse; +import com.cedarpolicy.model.entity.Entities; +import com.cedarpolicy.model.entity.Entity; import com.cedarpolicy.model.exception.AuthException; import com.cedarpolicy.model.exception.BadRequestException; import com.cedarpolicy.model.exception.InternalException; import com.cedarpolicy.model.exception.MissingExperimentalFeatureException; -import com.cedarpolicy.model.entity.Entity; import com.cedarpolicy.model.policy.PolicySet; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -35,10 +42,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.util.List; -import java.util.Set; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** An authorization engine that is compiled in process. Communicated with via JNI. */ public final class BasicAuthorizationEngine implements AuthorizationEngine { @@ -57,6 +62,15 @@ public AuthorizationResponse isAuthorized(com.cedarpolicy.model.AuthorizationReq return call("AuthorizationOperation", AuthorizationResponse.class, request); } + /** + * Overloaded method to accept Entities object + */ + @Override + public AuthorizationResponse isAuthorized(com.cedarpolicy.model.AuthorizationRequest q, + PolicySet policySet, Entities entities) throws AuthException { + return isAuthorized(q, policySet, entities.getEntities()); + } + @Experimental(ExperimentalFeature.PARTIAL_EVALUATION) @Override public PartialAuthorizationResponse isAuthorizedPartial(com.cedarpolicy.model.PartialAuthorizationRequest q, @@ -73,6 +87,16 @@ public PartialAuthorizationResponse isAuthorizedPartial(com.cedarpolicy.model.Pa } } + /** + * Overloaded method to accept Entities object + */ + @Experimental(ExperimentalFeature.PARTIAL_EVALUATION) + @Override + public PartialAuthorizationResponse isAuthorizedPartial(com.cedarpolicy.model.PartialAuthorizationRequest q, + PolicySet policySet, Entities entities) throws AuthException { + return isAuthorizedPartial(q, policySet, entities.getEntities()); + } + @Override public ValidationResponse validate(ValidationRequest q) throws AuthException { return call("ValidateOperation", ValidationResponse.class, q); diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entities.java b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entities.java new file mode 100644 index 00000000..add62abb --- /dev/null +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entities.java @@ -0,0 +1,93 @@ +/* + * 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.model.entity; + +import static com.cedarpolicy.CedarJson.objectReader; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.HashSet; + +/** + * A class representing a collection of Cedar policy entities. + */ +public class Entities { + private Set entities; + + /** + * Constructs a new empty Entities collection. Creates a new HashSet to store Entity objects. + */ + public Entities() { + this.entities = new HashSet<>(); + } + + /** + * Constructs a new Entities collection from a given Set of Entity objects. + * + * @param entities The Set of Entity objects to initialize this collection with + */ + public Entities(Set entities) { + this.entities = new HashSet<>(entities); + } + + /** + * Returns a copy of the set of entities in this collection. + * + * @return A new HashSet containing all Entity objects in this collection + */ + public Set getEntities() { + return new HashSet<>(entities); + } + + /** + * Parses a JSON string representation into an Entities collection. + * + * @param jsonString The JSON string containing entity data to parse + * + * @return A new Entities instance containing the parsed entities + * @throws JsonProcessingException If the JSON string cannot be parsed into valid entities + */ + public static Entities parse(String jsonString) throws JsonProcessingException { + return new Entities(objectReader().forType(new TypeReference>() { + }).readValue(jsonString)); + } + + /** + * Parses a JSON file at the specified path into an Entities collection. + * + * @param filePath The path to the JSON file containing entity data to parse + * + * @return A new Entities instance containing the parsed entities + * @throws IOException If there is an error reading the file + * @throws JsonProcessingException If the JSON content cannot be parsed into valid entities + */ + public static Entities parse(Path filePath) throws IOException, JsonProcessingException { + String jsonString = Files.readString(filePath); + return new Entities(objectReader().forType(new TypeReference>() { + }).readValue(jsonString)); + } + + @Override + public String toString() { + return String.join("\n", this.entities.stream().map(Entity::toString).toList()); + } +} diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java index ae05e0ae..b156b0d2 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -94,7 +94,7 @@ public Entity(EntityUID uid, Map attributes, Set paren * Get the value for the given attribute, or null if not present. * * @param attribute Attribute key - * + * * @return Attribute value for the given key or null if not present * @throws IllegalArgumentException if attribute is null */ diff --git a/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java b/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java index 7b288b7c..8da4afe3 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java @@ -34,6 +34,7 @@ import com.cedarpolicy.value.Unknown; import com.cedarpolicy.value.Value; import com.cedarpolicy.value.PrimBool; +import com.cedarpolicy.model.entity.Entities; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; @@ -47,6 +48,14 @@ public class AuthTests { private void assertAllowed(AuthorizationRequest q, PolicySet policySet, Set entities) { assertDoesNotThrow(() -> { + // Using Entities object + Entities entitiesObj = new Entities(entities); + final var responseWithEntities = new BasicAuthorizationEngine().isAuthorized(q, policySet, entitiesObj); + assertEquals(responseWithEntities.type, SuccessOrFailure.Success); + final var successWithEntities = responseWithEntities.success.get(); + assertTrue(successWithEntities.isAllowed()); + + // Backward compatible using Set final var response = new BasicAuthorizationEngine().isAuthorized(q, policySet, entities); assertEquals(response.type, SuccessOrFailure.Success); final var success = response.success.get(); @@ -197,6 +206,30 @@ public void partialAuthConcreteWithContextObject() { }); } + @Test + public void partialAuthConcreteWithEntitiesObject() { + var auth = new BasicAuthorizationEngine(); + var alice = new EntityUID(EntityTypeName.parse("User").get(), "alice"); + var view = new EntityUID(EntityTypeName.parse("Action").get(), "view"); + Map contextMap = new HashMap<>(); + contextMap.put("authenticated", new PrimBool(true)); + Context context = new Context(contextMap); + 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 == true};", "p0")); + var policySet = new PolicySet(policies); + assumePartialEvaluation(() -> { + try { + final PartialAuthorizationResponse response = auth.isAuthorizedPartial(q, policySet, new Entities()); + assertEquals(Decision.Allow, response.success.orElseThrow().getDecision()); + assertEquals(response.success.orElseThrow().getMustBeDetermining().iterator().next(), "p0"); + assertTrue(response.success.orElseThrow().getNontrivialResiduals().isEmpty()); + } catch (Exception e) { + fail("error: " + e.toString()); + } + }); + } + @Test public void residual() { var auth = new BasicAuthorizationEngine(); diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntitiesTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntitiesTests.java new file mode 100644 index 00000000..f27ed5a1 --- /dev/null +++ b/CedarJava/src/test/java/com/cedarpolicy/EntitiesTests.java @@ -0,0 +1,140 @@ +/* + * 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 org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.cedarpolicy.value.*; +import com.cedarpolicy.model.entity.Entity; +import com.cedarpolicy.model.entity.Entities; +import static com.cedarpolicy.CedarJson.objectWriter; + +import org.skyscreamer.jsonassert.*; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +public class EntitiesTests { + private static final String TEST_RESOURCES_DIR = "src/test/resources/"; + + @Test + public void givenValidEntitySetConstructorConstructs() { + Entity alice = new Entity(EntityUID.parse("User::\"Alice\"").get()); + Set parentAlice = new HashSet<>(); + parentAlice.add(alice.getEUID()); + + PrimString stringAttr = new PrimString("stringAttrValue"); + HashMap attrs = new HashMap<>(); + attrs.put("stringAttr", stringAttr); + + Entity aliceChild = new Entity(EntityUID.parse("User::\"Alice_child\"").get(), attrs, parentAlice); + + Set entitySet = new HashSet<>(); + entitySet.add(aliceChild); + entitySet.add(alice); + + Entities entities = new Entities(entitySet); + + assertEquals(entitySet, entities.getEntities()); + } + + @Test + public void givenValidJSONStringParseReturns() throws JsonProcessingException { + String validEntitiesJson = """ + [ + {"uid":{"type":"Photo","id":"pic02"},"parents":[{"type":"PhotoParent","id":"picParent"}], + "attrs":{"dummyIP": {"__extn":{"fn":"ip","arg":"199.168.1.130"}}}}, + {"uid":{"type":"Photo","id":"pic01"},"parents":[{"type":"Photo","id":"pic02"}],"attrs":{}} + ] + """; + + String expectedRepresentation = "{\"entities\":[" + "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic02\"}," + + "\"attrs\":{\"dummyIP\":{\"__extn\":{\"fn\":\"ip\",\"arg\":\"199.168.1.130\"}}}," + + "\"parents\":[{\"type\":\"PhotoParent\",\"id\":\"picParent\"}]," + "\"tags\":{}}," + + "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic01\"}," + "\"attrs\":{}," + + "\"parents\":[{\"type\":\"Photo\",\"id\":\"pic02\"}]," + "\"tags\":{}}]}"; + + Entities entities = Entities.parse(validEntitiesJson); + String actualRepresentation = objectWriter().writeValueAsString(entities); + + JSONAssert.assertEquals(expectedRepresentation, actualRepresentation, JSONCompareMode.NON_EXTENSIBLE); + + validEntitiesJson = """ + [ + {"uid":{"type":"Photo","id":"pic01"},"parents":[],"attrs":{}}, + {"uid":{"type":"Photo","id":"pic02"},"parents":[],"attrs":{}} + ] + """; + + expectedRepresentation = "{\"entities\":[" + "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic01\"}," + "\"attrs\":{}," + + "\"parents\":[]," + "\"tags\":{}}," + "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic02\"}," + + "\"attrs\":{}," + "\"parents\":[]," + "\"tags\":{}}]}"; + + entities = Entities.parse(validEntitiesJson); + actualRepresentation = objectWriter().writeValueAsString(entities); + + JSONAssert.assertEquals(expectedRepresentation, actualRepresentation, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void givenInvalidJSONStringParseThrows() throws JsonProcessingException { + String invalidEntityJson = """ + [{"uid":{"type":"Photo","id":"pic01"}}, + {"uid":{"type":"Photo","id":"pic02"},"parents":[],"attrs":{}}] + """; + + assertThrows(JsonProcessingException.class, () -> { + Entities.parse(invalidEntityJson); + }); + + String invalidEntityJson2 = """ + [{"uid":{"type":"Photo","id":"pic02"}, "parents":[{"parent_id":"Alice"}]}, + {"uid":{"type":"Photo","id":"pic01"},"parents":[],"attrs":{}}] + """; + + assertThrows(JsonProcessingException.class, () -> { + Entities.parse(invalidEntityJson2); + }); + } + + @Test + public void givenValidJSONFileParseReturns() throws JsonProcessingException, IOException { + Entities entities = Entities.parse(Path.of(TEST_RESOURCES_DIR + "valid_entities.json")); + String actualRepresentation = objectWriter().writeValueAsString(entities); + String expectedRepresentation = "{\"entities\":[" + "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic02\"}," + + "\"attrs\":{\"dummyIP\":{\"__extn\":{\"fn\":\"ip\",\"arg\":\"199.168.1.130\"}}}," + + "\"parents\":[{\"type\":\"PhotoParent\",\"id\":\"picParent\"}]," + "\"tags\":{}}," + + "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic01\"}," + "\"attrs\":{}," + + "\"parents\":[{\"type\":\"Photo\",\"id\":\"pic02\"}]," + "\"tags\":{}}]}"; + + JSONAssert.assertEquals(expectedRepresentation, actualRepresentation, JSONCompareMode.NON_EXTENSIBLE); + } + + @Test + public void givenInvalidJSONFileParseThrows() throws JsonProcessingException, IOException { + assertThrows(JsonProcessingException.class, () -> { + Entities.parse(Path.of(TEST_RESOURCES_DIR + "invalid_entities.json")); + }); + } +} diff --git a/CedarJava/src/test/resources/invalid_entities.json b/CedarJava/src/test/resources/invalid_entities.json new file mode 100644 index 00000000..d37cd9f8 --- /dev/null +++ b/CedarJava/src/test/resources/invalid_entities.json @@ -0,0 +1 @@ +{"uid":{"type":"Photo","id":"pic01"}} diff --git a/CedarJava/src/test/resources/valid_entities.json b/CedarJava/src/test/resources/valid_entities.json new file mode 100644 index 00000000..3746887d --- /dev/null +++ b/CedarJava/src/test/resources/valid_entities.json @@ -0,0 +1,5 @@ +[ + {"uid":{"type":"Photo","id":"pic02"},"parents":[{"type":"PhotoParent","id":"picParent"}], + "attrs":{"dummyIP": {"__extn":{"fn":"ip","arg":"199.168.1.130"}}}}, + {"uid":{"type":"Photo","id":"pic01"},"parents":[{"type":"Photo","id":"pic02"}],"attrs":{}} +]