Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions CedarJava/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,8 @@ tasks.register('downloadIntegrationTests', Download) {
group 'Build'
description 'Downloads Cedar repository with integration tests.'

src 'https://codeload.github.com/cedar-policy/cedar/zip/main'
dest layout.buildDirectory.file('cedar-main.zip')
src 'https://codeload.github.com/cedar-policy/cedar-integration-tests/zip/main'
dest layout.buildDirectory.file('cedar-integration-tests-main.zip')
overwrite false
}

Expand All @@ -193,19 +193,28 @@ tasks.register('extractIntegrationTests', Copy) {
description 'Extracts Cedar integration tests.'

dependsOn('downloadIntegrationTests')
from zipTree(layout.buildDirectory.file('cedar-main.zip'))
from zipTree(layout.buildDirectory.file('cedar-integration-tests-main.zip'))
into layout.buildDirectory.dir('resources/test')
}

tasks.register('extractCorpusTests', Copy) {
group 'Build'
description 'Extracts Cedar corpus tests.'

dependsOn('extractIntegrationTests')
dependsOn('processTestResources')
from tarTree(layout.buildDirectory.file('resources/test/cedar-integration-tests-main/corpus-tests.tar.gz'))
into layout.buildDirectory.dir('resources/test/cedar-integration-tests-main')
}

tasks.named('test') {
useJUnitPlatform()
dependsOn('compileFFI')
dependsOn('extractIntegrationTests')
dependsOn('extractCorpusTests')
classpath += files(layout.buildDirectory.dir(compiledLibDir))
}

test {
//environment "CEDAR_INTEGRATION_TESTS_ROOT", ''set to absolute path of `cedar-integration-tests`'
testLogging {
events "skipped", "failed", "standardOut", "standardError"
showStandardStreams false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,6 @@ public interface AuthorizationEngine {
* @return The Cedar language major version supported
*/
static String getCedarLangVersion() {
return "3.0";
return "4.0";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public Decision getDecision() {
*
* @return list with the policy ids that contributed to the decision
*/
public Set<String> getReasons() {
public Set<String> getReason() {
return diagnostics.reason;
}

Expand Down
137 changes: 89 additions & 48 deletions CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,86 +21,127 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;

import java.util.Optional;

/** Represents a schema. */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public final class Schema {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

static {
LibraryLoader.loadLibrary();
}

// The schema after being parsed as a JSON object.
// (Using "json" as the JsonProperty ensures the FFI knows we are using the JSON format.
// The other option is "human".)
@JsonProperty("json") private final JsonNode schemaJson;
/** Is this schema in the JSON or human format */
@JsonIgnore
public final JsonOrHuman type;
/** This will be present if and only if `type` is `Json`. */
@JsonProperty("json")
private final Optional<JsonNode> schemaJson;
/** This will be present if and only if `type` is `Human`. */
@JsonProperty("human")
public final Optional<String> schemaText;

/**
* Build a Schema from a string containing the JSON source for the model. This constructor will
* fail with an exception if the string does not parse as JSON, but it does not check that the
* parsed JSON object represents a valid schema.
*
* @param schemaJson List of EntityTypes.
* @throws java.io.IOException When any errors are encountered while parsing the authorization
* model json string into json node.
* If `type` is `Json`, `schemaJson` should be present and `schemaText` empty.
* If `type` is `Human`, `schemaText` should be present and `schemaJson` empty.
* This constructor does not check that the input text represents a valid JSON
* or Cedar schema. Use the `parse` function to ensure schema validity.
*
* @param type The schema format used.
* @param schemaJson Optional schema in Cedar's JSON schema format.
* @param schemaText Optional schema in Cedar's human-readable schema format.
*/
@SuppressFBWarnings
public Schema(String schemaJson) throws IOException {
if (schemaJson == null) {
throw new NullPointerException("schemaJson");
}

this.schemaJson = OBJECT_MAPPER.readTree(schemaJson);
public Schema(JsonOrHuman type, Optional<String> schemaJson, Optional<String> schemaText) {
this.type = type;
this.schemaJson = schemaJson.map(jsonStr -> {
try {
return OBJECT_MAPPER.readTree(jsonStr);
} catch (Exception e) {
e.printStackTrace();
return null;
}
});
this.schemaText = schemaText;
}

/**
* Build a Schema from a json node. This does not check that the parsed JSON object represents a
* valid schema.
* Build a Schema from a json node. This does not check that the parsed JSON
* object represents a valid schema. Use `parse` to check validity.
*
* @param schemaJson Schema in Cedar's JSON schema format.
*/
@SuppressFBWarnings
public Schema(JsonNode schemaJson) {
if (schemaJson == null) {
throw new NullPointerException("schemaJson");
}

this.schemaJson = schemaJson;
this.type = JsonOrHuman.Json;
this.schemaJson = Optional.of(schemaJson);
this.schemaText = Optional.empty();
}

/** Equals. */
@Override
public boolean equals(final Object o) {
if (!(o instanceof Schema)) {
return false;
/**
* Build a Schema from a string. This does not check that the string represents
* a valid schema. Use `parse` to check validity.
*
* @param schemaText Schema in Cedar's human-readable schema format.
*/
public Schema(String schemaText) {
if (schemaText == null) {
throw new NullPointerException("schemaText");
}

final Schema other = (Schema) o;
return schemaJson.equals(other.schemaJson);
this.type = JsonOrHuman.Human;
this.schemaJson = Optional.empty();
this.schemaText = Optional.of(schemaText);
}

/** Hash. */
@Override
public int hashCode() {
return Objects.hash(schemaJson);
}

/** Readable string representation. */
public String toString() {
return "Schema(schemaJson=" + schemaJson + ")";
if (type == JsonOrHuman.Json) {
return "Schema(schemaJson=" + schemaJson.get() + ")";
} else {
return "Schema(schemaText=" + schemaText.get() + ")";
}
}

public static Schema parse(String schemaStr) throws IOException, InternalException {
var success = parseSchema(schemaStr).equals("Success");
if (success) {
return new Schema(schemaStr);
/**
* Try to parse a string representing a JSON or Cedar schema. If parsing
* succeeds, return a `Schema`, otherwise raise an exception.
*
* @param type The schema format used.
* @param str Schema text to parse.
* @throws InternalException If parsing fails.
* @throws NullPointerException If the input text is null.
* @return A {@link Schema} that is guaranteed to be valid.
*/
public static Schema parse(JsonOrHuman type, String str) throws InternalException, NullPointerException {
if (type == JsonOrHuman.Json) {
parseJsonSchemaJni(str);
return new Schema(JsonOrHuman.Json, Optional.of(str), Optional.empty());
} else {
throw new IOException("Unable to parse schema");
parseHumanSchemaJni(str);
return new Schema(JsonOrHuman.Human, Optional.empty(), Optional.of(str));
}

}

private static native String parseSchema(String schemaStr) throws InternalException;
/** Specifies the schema format used. */
public enum JsonOrHuman {
/**
* Cedar JSON schema format. See <a href=
* "https://docs.cedarpolicy.com/schema/json-schema.html">https://docs.cedarpolicy.com/schema/json-schema.html</a>
*/
Json,
/**
* Cedar schema format. See <a href=
* "https://docs.cedarpolicy.com/schema/human-readable-schema.html">https://docs.cedarpolicy.com/schema/human-readable-schema.html</a>
*/
Human
}

private static native String parseJsonSchemaJni(String schemaJson) throws InternalException, NullPointerException;

private static native String parseHumanSchemaJni(String schemaText) throws InternalException, NullPointerException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public final class EntityUID extends Value {
}

/**
* Construct an EntityUID from a tyep name and an id
* Construct an EntityUID from a type name and an id
* @param type the Entity Type of this EUID
* @param id the id portion of the EUID
*/
Expand Down
82 changes: 78 additions & 4 deletions CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,94 @@
package com.cedarpolicy;

import com.cedarpolicy.model.schema.Schema;
import com.cedarpolicy.model.schema.Schema.JsonOrHuman;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class SchemaTests {
@Test
public void parseSchema() {
public void parseJsonSchema() {
assertDoesNotThrow(() -> {
Schema.parse(JsonOrHuman.Json, "{}");
Schema.parse(JsonOrHuman.Json, """
{
"Foo::Bar": {
"entityTypes": {},
"actions": {}
}
}
""");
Schema.parse(JsonOrHuman.Json, """
{
"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"name": {
"type": "String",
"required": true
},
"age": {
"type": "Long",
"required": false
}
}
}
},
"Photo": {
"memberOfTypes": [ "Album" ]
},
"Album": {}
},
"actions": {
"view": {
"appliesTo": {
"principalTypes": ["User"],
"resourceTypes": ["Photo", "Album"]
}
}
}
}
}
""");
});
assertThrows(Exception.class, () -> {
Schema.parse(JsonOrHuman.Json, "{\"foo\": \"bar\"}");
Schema.parse(JsonOrHuman.Json, "namespace Foo::Bar;");
});
}

@Test
public void parseHumanSchema() {
assertDoesNotThrow(() -> {
Schema.parse("{\"ns1\": {\"entityTypes\": {}, \"actions\": {}}}");
Schema.parse("{}");
Schema.parse(JsonOrHuman.Human, "");
Schema.parse(JsonOrHuman.Human, "namespace Foo::Bar {}");
Schema.parse(JsonOrHuman.Human, """
entity User = {
name: String,
age?: Long,
};
entity Photo in Album;
entity Album;
action view
appliesTo { principal: [User], resource: [Album, Photo] };
""");
});
assertThrows(Exception.class, () -> {
Schema.parse("{\"foo\": \"bar\"}");
Schema.parse(JsonOrHuman.Human, """
{
"Foo::Bar": {
"entityTypes" {},
"actions": {}
}
}
""");
Schema.parse(JsonOrHuman.Human, "namspace Foo::Bar;");
});
}
}
Loading