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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ jobs:
needs: get-branch-name
uses: ./.github/workflows/run_cedar_java_reusable.yml
with:
cedar_policy_ref: "refs/heads/main" # use the latest commit on main
cedar_policy_ref: ${{ needs.get-branch-name.outputs.branch_name }} # use the same branch of cedar-policy
cedar_java_ref: "${{ github.href }}" # use the current PR's commit
20 changes: 18 additions & 2 deletions CedarJava/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
# Changelog

## Unreleased
## 4.11.0
* Pinned to `cedar-policy 4.11.2`
* Added `PolicyParseException`, thrown by `PolicySet.parsePolicies` when policy text fails to parse. It is a subclass of `InternalException`, so existing `catch` blocks and `getMessage()` are unaffected, and adds `getDetailedErrors()` returning Cedar's structured diagnostics - source span, expected tokens, and help text - for each error. `getErrors()` now carries one entry per parse error rather than a single entry for the whole document [#367](https://github.com/cedar-policy/cedar-java/pull/367)
* Adds Type-Aware Partial Evaluation (TPE) support [#372](https://github.com/cedar-policy/cedar-java/pull/372). This is an experimental feature in cedar-policy and can change behavior in future releases (See [experimental-features](https://github.com/cedar-policy/rfcs/blob/main/README.md#experimental-features)).

## 4.10.0
* Pinned to `cedar-policy 4.10.0`

## 4.9.0
* Pinned to `cedar-policy 4.9.1`
* Improve toCedarExpr() correctness and add CedarStrings utility [#351](https://github.com/cedar-policy/cedar-java/pull/351)
* Add PolicySet and Schema caching [#350](https://github.com/cedar-policy/cedar-java/pull/350)
* Optimize JNI performance and fix Decimal validation [#349](https://github.com/cedar-policy/cedar-java/pull/349)
* * Updated Java source and target compatibility from 1.8 to 17 aligning with the pre-requisites in README [#350](https://github.com/cedar-policy/cedar-java/pull/350)
* Bugfix: EntityIdentifier equals implementation [#352](https://github.com/cedar-policy/cedar-java/pull/352)


## 4.8.0
* Added Schema conversion APIs [#325](https://github.com/cedar-policy/cedar-java/pull/325)
* Added Level Validation [#327](https://github.com/cedar-policy/cedar-java/pull/327)
* Added DateTime extension support [#328](https://github.com/cedar-policy/cedar-java/pull/328)
* Added Duration extension support [#331](https://github.com/cedar-policy/cedar-java/pull/331)
* Added Offset function support [#331](https://github.com/cedar-policy/cedar-java/pull/331)
* Added PolicySet to JSON conversion API [#329](https://github.com/cedar-policy/cedar-java/pull/329)
* Added Cedar Schema support for Entity Validation [#332](https://github.com/cedar-policy/cedar-java/pull/332)
* Added `PolicyParseException`, thrown by `PolicySet.parsePolicies` when policy text fails to parse. It is a subclass of `InternalException`, so existing `catch` blocks and `getMessage()` are unaffected, and adds `getDetailedErrors()` returning Cedar's structured diagnostics - source span, expected tokens, and help text - for each error. `getErrors()` now carries one entry per parse error rather than a single entry for the whole document [#367](https://github.com/cedar-policy/cedar-java/pull/367)

## 4.3.1
### Added
Expand Down
20 changes: 14 additions & 6 deletions CedarJava/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,17 @@ tasks.register('uberJar', Jar) {
from(layout.buildDirectory.dir(compiledLibDir))
}

// The cedar-integration-tests branch to test against; should match the cedar-policy branch in CedarJavaFFI/Cargo.toml.
def IntegrationTestsBranch = 'release/4.11.x'
// GitHub names the zip's top-level folder after the branch, with `/` replaced by `-`.
def IntegrationTestsZipRoot = 'cedar-integration-tests-' + IntegrationTestsBranch.replace('/', '-')

tasks.register('downloadIntegrationTests', Download) {
group 'Build'
description 'Downloads Cedar repository with integration tests.'

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

Expand All @@ -245,8 +250,11 @@ tasks.register('extractIntegrationTests', Copy) {

dependsOn('checkstyleTest')
dependsOn('downloadIntegrationTests')
from zipTree(layout.buildDirectory.file('cedar-integration-tests-main.zip'))
from zipTree(layout.buildDirectory.file("${IntegrationTestsZipRoot}.zip"))
into layout.buildDirectory.dir('resources/test')
// Extract under a fixed folder name so the tests need not know which branch was downloaded.
eachFile { it.path = it.path.replaceFirst("^${IntegrationTestsZipRoot}/", 'cedar-integration-tests/') }
includeEmptyDirs = false
}

tasks.register('extractCorpusTests', Copy) {
Expand All @@ -255,8 +263,8 @@ tasks.register('extractCorpusTests', Copy) {

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')
from tarTree(layout.buildDirectory.file('resources/test/cedar-integration-tests/corpus-tests.tar.gz'))
into layout.buildDirectory.dir('resources/test/cedar-integration-tests')
}

tasks.named('test') {
Expand Down Expand Up @@ -324,7 +332,7 @@ publishing {
from components.java
groupId = 'com.cedarpolicy'
artifactId = 'cedar-java'
version = '3.1.2'
version = '4.11.0'

artifacts {
jar
Expand Down
24 changes: 10 additions & 14 deletions CedarJava/src/test/java/com/cedarpolicy/PartialEntitiesTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import static com.cedarpolicy.TestUtil.assertMessageContains;
import static com.cedarpolicy.TestUtil.buildEuidObject;
import static com.cedarpolicy.TestUtil.buildUidObject;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand All @@ -58,13 +59,11 @@ public void testChecksAgainstCedarFormatSchema() throws InternalException {
new PartialEntity(alice, Optional.empty(), Optional.empty(), Optional.empty(), TPE_SCHEMA_CEDAR));
assertEquals(1, new PartialEntities(entities, TPE_SCHEMA_CEDAR).getEntities().size());

// A collection-level rule, which only the collection validator can catch.
// Accepted, as with the JSON-format schema: see `testAcceptsDuplicateUid`.
var duplicated = Set.of(
new PartialEntity(alice, Optional.empty(), Optional.empty(), Optional.empty(), TPE_SCHEMA_CEDAR),
new PartialEntity(alice, Optional.of(Map.of()), Optional.empty(), Optional.empty(), TPE_SCHEMA_CEDAR));
InternalException e =
assertThrows(InternalException.class, () -> new PartialEntities(duplicated, TPE_SCHEMA_CEDAR));
assertMessageContains(e, "duplicate entity entry", "User::\"alice\"");
assertDoesNotThrow(() -> new PartialEntities(duplicated, TPE_SCHEMA_CEDAR));
}

@Test
Expand All @@ -77,7 +76,7 @@ public void testRejectsParentWithUnknownParents() throws InternalException {
new PartialEntity(admins, Optional.empty(), Optional.empty(), Optional.empty(), TPE_SCHEMA));
InternalException e =
assertThrows(InternalException.class, () -> new PartialEntities(entities, TPE_SCHEMA));
assertMessageContains(e, "ancestor `Group::\"admins\"`", "of `User::\"alice\"`", "has unknown ancestors");
assertMessageContains(e, "User::\"alice\"", "ancestor `Group::\"admins\"`", "has unknown ancestors");
}

@Test
Expand All @@ -97,15 +96,14 @@ public void testAllowsParentAbsentFromTheCollection() throws InternalException {
assertEquals(2, withParent.getEntities().size());
}

/** Cedar 4.11 does not reject a repeated uid in partial entities, and CedarJava follows Cedar. */
@Test
public void testRejectsDuplicateUid() throws InternalException {
public void testAcceptsDuplicateUid() throws InternalException {
var alice = new EntityUID(EntityTypeName.parse("User").get(), "alice");
var entities = Set.of(
new PartialEntity(alice, Optional.empty(), Optional.empty(), Optional.empty(), TPE_SCHEMA),
new PartialEntity(alice, Optional.of(Map.of()), Optional.empty(), Optional.empty(), TPE_SCHEMA));
InternalException e =
assertThrows(InternalException.class, () -> new PartialEntities(entities, TPE_SCHEMA));
assertMessageContains(e, "duplicate entity entry", "User::\"alice\"");
assertDoesNotThrow(() -> new PartialEntities(entities, TPE_SCHEMA));
}

@Test
Expand Down Expand Up @@ -222,8 +220,8 @@ public void testFromJsonRoundTrip() throws InternalException {
}

@Test
public void testFromJsonRejectsDuplicateUid() {
// The same rule as `testRejectsDuplicateUid`, reached through the JSON encoding. Worth covering separately
public void testFromJsonAcceptsDuplicateUid() {
// The same rule as `testAcceptsDuplicateUid`, reached through the JSON encoding. Worth covering separately
// because a JSON array can carry two entries with the same uid, whereas a `Set` relies on the elements
// differing to keep both.
ObjectNode first = JsonNodeFactory.instance.objectNode();
Expand All @@ -233,9 +231,7 @@ public void testFromJsonRejectsDuplicateUid() {
second.set("attrs", JsonNodeFactory.instance.objectNode());
ArrayNode json = JsonNodeFactory.instance.arrayNode().add(first).add(second);

InternalException e =
assertThrows(InternalException.class, () -> PartialEntities.fromJson(json, TPE_SCHEMA));
assertMessageContains(e, "duplicate entity entry", "User::\"alice\"");
assertDoesNotThrow(() -> PartialEntities.fromJson(json, TPE_SCHEMA));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ private Path resolveIntegrationTestPath(String path) {
if (resolved.isAbsolute()) {
return resolved;
} else {
final URL integrationTestsLocation = getClass().getResource("/cedar-integration-tests-main");
final URL integrationTestsLocation = getClass().getResource("/cedar-integration-tests");
return integrationTestsLocation == null ? resolved : Paths.get(integrationTestsLocation.getPath(), path);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public void tpeUnknownPrincipalIdYieldsAResidual() {
assertEquals(Set.of("p0"), TestUtil.ids(success.getNontrivialResiduals()));
// The principal id is unknown, so `principal` survives as a variable.
assertEquals("permit(principal, action, resource) when "
+ "{ (principal has department) && (principal.department == \"eng\") };",
+ "{ (principal has department) && ((principal.department) == \"eng\") };",
success.getResiduals().iterator().next().getSource());
});
}
Expand Down Expand Up @@ -145,7 +145,7 @@ public void tpeResidualTextShowsTheFoldedCondition() {

assertEquals("p0", residual.getID());
assertEquals("@owner(\"team-a\")\npermit(principal, action, resource) when "
+ "{ (User::\"alice\" has department) && (User::\"alice\".department == \"eng\") };",
+ "{ (User::\"alice\" has department) && ((User::\"alice\".department) == \"eng\") };",
residual.getSource());
// The annotation survives, and the residual is still a usable Policy because nothing errored.
assertEquals(Map.of("owner", "team-a"), residual.getAnnotations());
Expand Down Expand Up @@ -209,7 +209,7 @@ public void tpeUnknownAttrsYieldsAResidual() {
// The principal id is known here, unlike the test above, so `principal` is substituted with the
// entity — which is why a residual must not be evaluated for a different principal.
assertEquals("permit(principal, action, resource) when "
+ "{ (User::\"alice\" has department) && (User::\"alice\".department == \"eng\") };",
+ "{ (User::\"alice\" has department) && ((User::\"alice\".department) == \"eng\") };",
success.getResiduals().iterator().next().getSource());
});
}
Expand Down Expand Up @@ -590,7 +590,7 @@ public void tpeNestedErrorIsBothNontrivialAndErrored() {
assertEquals(Set.of("p0"), success.getErroredResidualIds(),
"a nested error must still be reported, which error_permits() alone would not do");
assertEquals("permit(principal, action, resource) when "
+ "{ (User::\"alice\" has department) || error() };",
+ "{ (User::\"alice\" has department) || (error()) };",
success.getResiduals().iterator().next().getSource());
});
}
Expand Down Expand Up @@ -638,35 +638,34 @@ record Mismatch(String label, AuthorizationRequest request, Entities entities, S
new AuthorizationRequest(bob, TPE_VIEW, TPE_DOOR, new HashMap<String, Value>()),
new Entities(Set.of(aliceEng,
new Entity(bob, Map.of("department", new PrimString("eng")), Set.of(), Map.of()))),
"partial request principal id `alice` does not match concrete request principal id `bob`"),
"Principal eid `alice` and `bob` do not match"),
new Mismatch("resource id",
new AuthorizationRequest(TPE_ALICE, TPE_VIEW, window, new HashMap<String, Value>()),
new Entities(Set.of(aliceEng)),
"partial request resource id `door` does not match concrete request resource id `window`"),
"Resource eid `door` and `window` do not match"),
new Mismatch("action",
new AuthorizationRequest(TPE_ALICE, edit, TPE_DOOR, new HashMap<String, Value>()),
new Entities(Set.of(aliceEng)),
"does not match concrete request action"),
"Actions `Action::\"view\"` and `Action::\"edit\"` do not match"),
new Mismatch("context",
new AuthorizationRequest(TPE_ALICE, TPE_VIEW, TPE_DOOR,
new HashMap<>(Map.of("authenticated", new PrimBool(true)))),
new Entities(Set.of(aliceEng)),
"the partial and concrete request contexts do not match"),
"Contexts are inconsistent"),
new Mismatch("entity missing from the concrete store",
new AuthorizationRequest(TPE_ALICE, TPE_VIEW, TPE_DOOR, new HashMap<String, Value>()),
new Entities(Set.of()),
"entity `User::\"alice\"` is present in the partial entities but missing from the "
+ "concrete entities"),
"Concrete entities does not include `User::\"alice\"`"),
new Mismatch("parents",
new AuthorizationRequest(TPE_ALICE, TPE_VIEW, TPE_DOOR, new HashMap<String, Value>()),
new Entities(Set.of(new Entity(TPE_ALICE,
Map.of("department", new PrimString("eng")), Set.of(group), Map.of()))),
"concrete entity `User::\"alice\"` has ancestors that do not match the partial entity"),
"Entity `User::\"alice\"`'s ancestors do not match"),
new Mismatch("tags",
new AuthorizationRequest(TPE_ALICE, TPE_VIEW, TPE_DOOR, new HashMap<String, Value>()),
new Entities(Set.of(new Entity(TPE_ALICE, Map.of("department", new PrimString("eng")),
Set.of(), Map.of("clearance", new PrimString("high"))))),
"concrete entity `User::\"alice\"` has tag values that do not match the partial entity"));
"Entity `User::\"alice\"`'s tags do not match"));

for (var m : mismatches) {
var response = success.reauthorize(m.request(), m.entities());
Expand Down Expand Up @@ -708,7 +707,7 @@ public void tpeReauthorizeRejectsAChangedAttributeValue() {
new Entity(TPE_ALICE, Map.of("department", new PrimString("sales")), Set.of(), Map.of()))));
assertEquals(SuccessOrFailure.Failure, changed.type);
assertTrue(changed.errors.orElseThrow().get(0).message.contains(
"concrete entity `User::\"alice\"` has attribute values that do not match the partial entity"),
"Entity `User::\"alice\"`'s attributes do not match"),
"unexpected message: " + changed.errors.orElseThrow().get(0).message);
});
}
Expand Down Expand Up @@ -750,7 +749,7 @@ public void tpeReauthorizeRejectsAChangedPrincipalTypeWhenTheIdWasUnknown() {
new Entity(root, Map.of("department", new PrimString("eng")), Set.of(), Map.of()))));
assertEquals(SuccessOrFailure.Failure, asAdmin.type);
assertTrue(asAdmin.errors.orElseThrow().get(0).message.contains(
"partial request principal type `User` does not match concrete request principal type `Admin`"),
"Principal types `User` and `Admin` do not match"),
"unexpected message: " + asAdmin.errors.orElseThrow().get(0).message);
});
}
Expand Down
Loading
Loading