diff --git a/CedarJava/CHANGELOG.md b/CedarJava/CHANGELOG.md index 3da643bc..dc1fe4b7 100644 --- a/CedarJava/CHANGELOG.md +++ b/CedarJava/CHANGELOG.md @@ -1,6 +1,25 @@ # Changelog ## Unreleased +### Added +* Added Zig version validation for publishing artifacts [#306](https://github.com/cedar-policy/cedar-java/pull/306) + +## 4.3.0 +### Added +* Introduced new model classes for improved type safety and functionality: + * `com.cedarpolicy.model.Context` - Policy context representation (will replace `Map`) [#286](https://github.com/cedar-policy/cedar-java/pull/286) + * `com.cedarpolicy.model.entity.Entities` - Entity collection management (will replace `Set`) [#293](https://github.com/cedar-policy/cedar-java/pull/293) +* Enhanced `AuthorizationError` with public getters and `.toString()` method [#294](https://github.com/cedar-policy/cedar-java/pull/294) +* Added JSON parsing support for `Entity` [#292](https://github.com/cedar-policy/cedar-java/pull/292) +* Implemented additional constructors to improve instantiation options for `Entity` [#288](https://github.com/cedar-policy/cedar-java/pull/288) +* Added support for policy annotations [#296](https://github.com/cedar-policy/cedar-java/pull/296) + +### Planned Improvements +* The following authorization parameters will be updated in a future release: + * `Map` for context will be replaced by `com.cedarpolicy.model.Context` + * `Set` for entities will be replaced by `com.cedarpolicy.model.entity.Entities` + +## 3.0 * Reworked interface of `com.cedarpolicy.value.EntityUID` to support namespaces * Modified `com.cedarpolicy.model.AuthorizationRequest` to use `com.cedarpolicy.value.EntityUID` instead of Strings diff --git a/CedarJava/build.gradle b/CedarJava/build.gradle index 8049169a..d0a4da70 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' } @@ -123,6 +124,26 @@ tasks.register('installCargoZigbuild', Exec) { commandLine 'cargo', '+' + RustVersion, 'install', 'cargo-zigbuild@0.19.7' } +def ZigVersion = '0.11' +tasks.register('validateZigVersion') { + group = 'Build' + description = 'Validates that the correct zig version is installed' + + doLast { + def output = new ByteArrayOutputStream() + exec { + commandLine 'zig', 'version' + standardOutput = output + } + def version = output.toString().trim() + println "Detected Zig version: ${version}" + if (!version.startsWith(ZigVersion)) { + throw new GradleException("Zig version must be ${ZigVersion} but found: ${version}") + } + println "Zig version validation successful" + } +} + tasks.register('compileFFI') { dependsOn('installCargoZigbuild') group 'Build' @@ -251,6 +272,7 @@ java { /* Configures Maven publishing */ +publish.dependsOn('validateZigVersion') publishing { publications { mavenJava(MavenPublication) { diff --git a/CedarJava/gradle/wrapper/gradle-wrapper.jar b/CedarJava/gradle/wrapper/gradle-wrapper.jar index c1962a79..d64cd491 100644 Binary files a/CedarJava/gradle/wrapper/gradle-wrapper.jar and b/CedarJava/gradle/wrapper/gradle-wrapper.jar differ diff --git a/CedarJava/gradle/wrapper/gradle-wrapper.properties b/CedarJava/gradle/wrapper/gradle-wrapper.properties index 509c4a29..a80b22ce 100644 --- a/CedarJava/gradle/wrapper/gradle-wrapper.properties +++ b/CedarJava/gradle/wrapper/gradle-wrapper.properties @@ -2,5 +2,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/CedarJava/gradlew b/CedarJava/gradlew index aeb74cbb..1aa94a42 100755 --- a/CedarJava/gradlew +++ b/CedarJava/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -130,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -141,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -149,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -198,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/CedarJava/gradlew.bat b/CedarJava/gradlew.bat index 6689b85b..7101f8e4 100644 --- a/CedarJava/gradlew.bat +++ b/CedarJava/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail 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/main/java/com/cedarpolicy/model/schema/Schema.java b/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java index d11bb16e..55b2aba1 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java @@ -16,13 +16,13 @@ package com.cedarpolicy.model.schema; +import java.util.Optional; + import com.cedarpolicy.loader.LibraryLoader; import com.cedarpolicy.model.exception.InternalException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.Optional; - /** Represents a schema. */ public final class Schema { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -101,6 +101,21 @@ public String toString() { } } + public String toCedarFormat() throws InternalException { + if (type != JsonOrCedar.Json || schemaJson.isEmpty()) { + throw new InternalException("Schema is not in JSON format"); + } + String json = schemaJson.get().toString(); + return jsonToCedarJni(json); + } + + public String toJsonFormat() throws InternalException { + if (type != JsonOrCedar.Cedar || schemaText.isEmpty()) { + throw new InternalException("Schema is not in cedar format"); + } + return cedarToJsonJni(schemaText.get()); + } + /** * Try to parse a string representing a JSON or Cedar schema. If parsing * succeeds, return a `Schema`, otherwise raise an exception. @@ -125,13 +140,15 @@ public static Schema parse(JsonOrCedar type, String str) throws InternalExceptio /** Specifies the schema format used. */ public enum JsonOrCedar { /** - * Cedar JSON schema format. See - * https://docs.cedarpolicy.com/schema/json-schema.html + * Cedar JSON schema format. See + * + * https://docs.cedarpolicy.com/schema/json-schema.html */ Json, /** - * Cedar schema format. See - * https://docs.cedarpolicy.com/schema/human-readable-schema.html + * Cedar schema format. See + * + * https://docs.cedarpolicy.com/schema/human-readable-schema.html */ Cedar } @@ -139,4 +156,8 @@ public enum JsonOrCedar { private static native String parseJsonSchemaJni(String schemaJson) throws InternalException, NullPointerException; private static native String parseCedarSchemaJni(String schemaText) throws InternalException, NullPointerException; + + private static native String jsonToCedarJni(String json) throws InternalException, NullPointerException; + + private static native String cedarToJsonJni(String cedar) throws InternalException, NullPointerException; } diff --git a/CedarJava/src/test/java/com/cedarpolicy/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..8644ae9f --- /dev/null +++ b/CedarJava/src/test/java/com/cedarpolicy/EntitiesTests.java @@ -0,0 +1,141 @@ +/* + * 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.json.JSONException; +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, JSONException { + 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, JSONException { + 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, JSONException { + 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/java/com/cedarpolicy/SchemaTests.java b/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java index 5657c793..4d00aca7 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java @@ -16,13 +16,15 @@ package com.cedarpolicy; -import com.cedarpolicy.model.schema.Schema; -import com.cedarpolicy.model.schema.Schema.JsonOrCedar; - -import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import com.cedarpolicy.model.exception.InternalException; +import com.cedarpolicy.model.schema.Schema; +import com.cedarpolicy.model.schema.Schema.JsonOrCedar; public class SchemaTests { @Test @@ -107,4 +109,87 @@ public void parseCedarSchema() { Schema.parse(JsonOrCedar.Cedar, "namspace Foo::Bar;"); }); } + + // write cedar java conversion tests here ... + static class SchemaConversionTests { + @Test + public void toCedarFormatValid() throws InternalException { + String schemaJson = """ + { + "schema": { + "entityTypes": { + "User": { + "memberOfTypes": ["Group"] + }, + "Group": {}, + "File": {} + }, + "actions": { + "read": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["File"] + } + } + } + } + } + """; + Schema schema = Schema.parse(JsonOrCedar.Json, schemaJson); + String cedarSchema = schema.toCedarFormat(); + assertNotNull(schema); + assertTrue(cedarSchema.contains("entity User"), "Expected Cedar to contain 'entity User'"); + } + + @Test + public void toJsonFormatValidCedar() throws InternalException { + String cedarSchema = """ + entity User { + name: String, + age?: Long + }; + entity Photo in [Album]; + entity Album; + action view appliesTo { + principal: [User], + resource: [Album, Photo] + }; + """; + Schema schema = Schema.parse(JsonOrCedar.Cedar, cedarSchema); + String jsonSchema = schema.toJsonFormat(); + assertNotNull(schema); + assertTrue(jsonSchema.contains("User"), "Expected Json to contain 'User'"); + } + + @Test + public void toCedarFormatThrowsIfNotJson() { + String cedarSchema = """ + entity Foo; + """; + + assertThrows(InternalException.class, () -> { + Schema schema = Schema.parse(JsonOrCedar.Cedar, cedarSchema); + schema.toCedarFormat(); // should throw + }); + } + + @Test + public void toJsonFormatThrowsIfNotCedar() { + String jsonSchema = """ + { + "schema": { + "entityTypes": { + "User": {} + } + } + } + """; + + assertThrows(InternalException.class, () -> { + Schema schema = Schema.parse(JsonOrCedar.Json, jsonSchema); + schema.toJsonFormat(); // should throw + }); + } + } + } diff --git a/CedarJava/src/test/java/com/cedarpolicy/Test.java b/CedarJava/src/test/java/com/cedarpolicy/Test.java new file mode 100644 index 00000000..7641ab2b --- /dev/null +++ b/CedarJava/src/test/java/com/cedarpolicy/Test.java @@ -0,0 +1,5 @@ +package com.cedarpolicy; + +public @interface Test { + +} 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":{}} +] diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index 4d607e99..f7ab198d 100644 --- a/CedarJavaFFI/src/interface.rs +++ b/CedarJavaFFI/src/interface.rs @@ -14,9 +14,16 @@ * limitations under the License. */ -use cedar_policy::entities_errors::EntitiesError; + +use anyhow::anyhow; #[cfg(feature = "partial-eval")] use cedar_policy::ffi::is_authorized_partial_json_str; +use cedar_policy::ffi::Schema as FFISchema; +use cedar_policy::ffi::SchemaToJsonAnswer; +use cedar_policy::{ + entities_errors::EntitiesError, + ffi::{schema_to_json, schema_to_text, SchemaToTextAnswer}, +}; use cedar_policy::{ ffi::{is_authorized_json_str, validate_json_str}, Entities, EntityUid, Policy, PolicySet, Schema, Template, @@ -703,6 +710,82 @@ fn policies_str_to_pretty_internal<'a>( } } } +#[jni_fn("com.cedarpolicy.model.schema.Schema")] +pub fn jsonToCedarJni<'a>(mut env: JNIEnv<'a>, _: JClass, json_schema: JString<'a>) -> jvalue { + match get_cedar_schema_internal(&mut env, json_schema) { + Ok(text) => match env.new_string(&text) { + Ok(jstr) => JValueGen::Object(JObject::from(jstr)).as_jni(), + Err(e) => jni_failed(&mut env, &e), + }, + Err(e) => jni_failed(&mut env, e.as_ref()), + } +} + +pub fn get_cedar_schema_internal<'a>( + env: &mut JNIEnv<'a>, + schema_json_jstr: JString<'a>, +) -> Result { + let rust_str = env.get_string(&schema_json_jstr)?; + let schema_str = rust_str.to_str()?; + + let schema: FFISchema = serde_json::from_str(schema_str)?; + let cedar_format = schema_to_text(schema); + + match cedar_format { + SchemaToTextAnswer::Success { text, warnings } => Ok(text), + SchemaToTextAnswer::Failure { errors } => { + let joined_errors = errors + .iter() + .map(|e| e.message.clone()) + .collect::>() + .join("; "); + Err(joined_errors.into()) + + } + } +} + +#[jni_fn("com.cedarpolicy.model.schema.Schema")] +pub fn cedarToJsonJni<'a>(mut env: JNIEnv<'a>, _: JClass, cedar_schema: JString<'a>) -> jvalue { + match get_json_schema_internal(&mut env, cedar_schema) { + Ok(text) => match env.new_string(&text) { + Ok(jstr) => JValueGen::Object(JObject::from(jstr)).as_jni(), // debug needed + Err(e) => { + println!("Possible error"); + jni_failed(&mut env, &e) + } + }, + Err(e) => jni_failed(&mut env, e.as_ref()), + } +} + +pub fn get_json_schema_internal<'a>( + env: &mut JNIEnv<'a>, + cedar_schema_jstr: JString<'a>, +) -> Result { + let schema_jstr = env.get_string(&cedar_schema_jstr)?; + let schema_str = schema_jstr.to_str()?; + let cedar_schema_str = FFISchema::Cedar(schema_str.into()); + let json_format = schema_to_json(cedar_schema_str); + + match json_format { + SchemaToJsonAnswer::Success { json, warnings: _ } => Ok(serde_json::to_string_pretty(&json)?), + SchemaToJsonAnswer::Failure { errors } => { + let errmsg = serde_json::to_string(&errors); + let joined_errors = errors + .iter() + // .enumerate() + // .map(|(i,e)| format!("Error(s) {}: {}", i + 1,e.message)) //method for formated display to make it cleaner + .map(|e| e.message.clone()) + .collect::>() + .join("; "); + Err(joined_errors.into()) + + } + } +} + + #[cfg(test)] mod jvm_based_tests { @@ -871,4 +954,75 @@ mod jvm_based_tests { ) } } + mod Conversion_tests + { + use super::*; + #[test] +fn test_get_cedar_schema_internal_valid() { + let env = &mut JVM.attach_current_thread().unwrap(); + let json_input = r#" + { + "schema": { + "entityTypes": { + "User": { + "memberOfTypes": ["Group"] + }, + "Group": {}, + "File": {} + }, + "actions": { + "read": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["File"] + } + } + } + } + } + "#; + + let jstr = env.new_string(json_input).unwrap(); + let result = get_cedar_schema_internal(env, jstr); + assert!(result.is_ok(), "Expected Cedar conversion to succeed"); + + let cedar = result.unwrap(); + assert!(cedar.contains("entity User"), "Expected output to contain 'entity User'"); +} +#[test] +fn test_get_json_schema_internal_valid() { + let env = &mut JVM.attach_current_thread().unwrap(); + let cedar_input = r#" + entity User = { + name: String, + age?: Long, + }; + entity Photo in Album; + entity Album; + action view appliesTo { + principal : [User], + resource: [Album,Photo] + }; + "#; + + let jstr = env.new_string(cedar_input).unwrap(); + let result = get_json_schema_internal(env, jstr); + assert!(result.is_ok(), "Expected JSON conversion to succeed"); + + let json = result.unwrap(); + assert!(json.contains("\"entityTypes\""), "Expected JSON output to contain 'entityTypes'"); +} +#[test] +fn test_get_json_schema_internal_invalid_input() { + let env = &mut JVM.attach_current_thread().unwrap(); + let invalid_cedar = "this is not cedar schema"; + let jstr = env.new_string(invalid_cedar).unwrap(); + + let result = get_json_schema_internal(env, jstr); + assert!(result.is_err(), "Expected error on invalid Cedar input"); +} + + + } + } diff --git a/README.md b/README.md index 17781bc9..808fc096 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,99 @@ # cedar-java +![Cedar Logo](https://github.com/cedar-policy/cedar/blob/main/logo.svg) -This repository contains the source code for a Java package `CedarJava` that supports using the [Cedar](https://www.cedarpolicy.com) policy language. -It also contains source code for a Rust crate `CedarJavaFFI` that enables calling Cedar library functions (written in Rust) from Java. +This repository contains the source code for a Java package `CedarJava` that supports using the [Cedar](https://www.cedarpolicy.com) policy language. It also contains source code for a Rust crate `CedarJavaFFI` that enables calling Cedar library functions (written in Rust) from Java. + +Cedar is a language for writing and enforcing authorization policies in your applications. Using Cedar, you can write policies that specify your applications' fine-grained permissions. Your applications then authorize access requests by calling Cedar's authorization engine. Because Cedar policies are separate from application code, they can be independently authored, updated, analyzed, and audited. You can use Cedar's validator to check that Cedar policies are consistent with a declared schema which defines your application's authorization model. ## Getting Started -The [CedarJavaFFI](https://github.com/cedar-policy/cedar-java/blob/main/CedarJavaFFI/README.md) and [CedarJava](https://github.com/cedar-policy/cedar-java/blob/main/CedarJava/README.md) directories contain detailed instructions on building individual modules. +### Import `CedarJava` to your application +#### Maven Package +CedarJava is available as a maven package. You can add `CedarJava` as a dependency to your build file. + +Example (Gradle): +``` +dependencies{ + implementation 'com.cedarpolicy:cedar-java:4.2.3:uber' +} +``` +We highly recommend using the `*-uber.jar` as it also contains the shared library from `CedarJavaFFI`. + +See [https://central.sonatype.com/artifact/com.cedarpolicy/cedar-java](https://central.sonatype.com/artifact/com.cedarpolicy/cedar-java) for more details. + +#### Build from Source -The `CedarJava` module uses Gradle to build both modules and run integration tests. The following commands provide general usage for getting started. +The [CedarJavaFFI](https://github.com/cedar-policy/cedar-java/blob/main/CedarJavaFFI/README.md) and [CedarJava](https://github.com/cedar-policy/cedar-java/blob/main/CedarJava/README.md) directories contain detailed instructions on building the individual modules. + +The `CedarJava` module uses Gradle to build both modules and run integration tests. It stores the shared library from `CedarJavaFFI` in the `-uber.jar`. The following commands provide general usage for getting started. ```shell cd CedarJava ./gradlew build ``` +### Perform an Authorization Request +Here is a small snippet on how to perform `isAuthorized()` call using `CedarJava` +```java +package com.mypackage; + +import com.cedarpolicy.AuthorizationEngine; +import com.cedarpolicy.BasicAuthorizationEngine; +import com.cedarpolicy.model.AuthorizationRequest; +import com.cedarpolicy.model.AuthorizationResponse; +import com.cedarpolicy.model.Context; +import com.cedarpolicy.model.entity.Entities; +import com.cedarpolicy.model.entity.Entity; +import com.cedarpolicy.model.policy.PolicySet; +import com.cedarpolicy.value.EntityUID; + +public class SimpleAuthorization { + public static void main(String[] args) throws Exception { + + // Build entities + Entity principal = new Entity(EntityUID.parse("User::\"Alice\"").get()); + Entity action = new Entity(EntityUID.parse("Action::\"view\"").get()); + Entity resource = new Entity(EntityUID.parse("Photo::\"alice_photo\"").get()); + + // Build policies + PolicySet policySet = PolicySet.parsePolicies(""" + permit( + principal == User::"Alice", + action == Action::"view", + resource == Photo::"alice_photo" + ); + + forbid( + principal == User::"Alice", + action == Action::"view", + resource == Photo::"bob_photo" + ); + """); + + // Authorization request + AuthorizationEngine ae = new BasicAuthorizationEngine(); + Entities entities = new Entities(); + Context context = new Context(); + AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, context); + AuthorizationResponse authorizationResponse = ae.isAuthorized(request, policySet, entities); + } +} +``` + +## Examples +Explore our sample applications in [cedar-examples](https://github.com/cedar-policy/cedar-examples/tree/main): +* [**cedar-java-hello-world**](https://github.com/cedar-policy/cedar-examples/tree/main/cedar-java-hello-world): Demonstrates basic authorization calls using Cedar-Java +* [**cedar-java-partial-evaluation**](https://github.com/cedar-policy/cedar-examples/tree/main/cedar-java-partial-evaluation): Illustrates partial evaluation capabilities in Cedar-Java + +## Changelog +For a list of changes and version updates, see [CHANGELOG.md](CedarJava/CHANGELOG.md). + ## Notes `CedarJava` requires JDK 17 or later. -Cedar is primarily developed in Rust (in the [cedar](https://github.com/cedar-policy/cedar) repository). As such, `CedarJava` typically lags behind the newest Cedar features. Notably, as of this writing, `CedarJava` does not expose APIs for partial evaluation. +Cedar is primarily developed in Rust (in the [cedar](https://github.com/cedar-policy/cedar) repository). As such, `CedarJava` typically lags behind the newest Cedar features. The `main` branch of this repository is kept up-to-date with the development version of the Rust code (available in the `main` branch of [cedar](https://github.com/cedar-policy/cedar)). Unless you plan to build the Rust code locally, please use the latest `release/x.x.x` branch instead.