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
45 changes: 45 additions & 0 deletions CedarJava/src/main/java/com/cedarpolicy/model/slice/PolicySet.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,23 @@

package com.cedarpolicy.model.slice;

import com.cedarpolicy.loader.LibraryLoader;

import com.cedarpolicy.model.exception.InternalException;

import java.util.Collections;
import java.util.List;
import java.util.Set;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

/** Policy Set containing policies in the Cedar language. */
public class PolicySet {
static {
LibraryLoader.loadLibrary();
}

/** Policy set. */
public Set<Policy> policies;
Expand All @@ -44,9 +55,43 @@ public PolicySet(Set<Policy> policies) {
this.templateInstantiations = Collections.emptyList();
}

public PolicySet(Set<Policy> policies, Set<Policy> templates) {
this.policies = policies;
this.templates = templates;
this.templateInstantiations = Collections.emptyList();
}

public PolicySet(Set<Policy> policies, Set<Policy> templates, List<TemplateInstantiation> templateInstantiations) {
this.policies = policies;
this.templates = templates;
this.templateInstantiations = templateInstantiations;
}

/**
* Parse multiple policies and templates from a file into a PolicySet.
* @param filePath the path to the file containing the policies
* @return a PolicySet containing the parsed policies
* @throws InternalException
* @throws IOException
* @throws NullPointerException
*/
public static PolicySet parsePolicies(Path filePath) throws InternalException, IOException {
// Read the file contents into a String
String policiesString = Files.readString(filePath);
return parsePolicies(policiesString);
}

/**
* Parse a string containing multiple policies and templates into a PolicySet.
* @param policiesString the string containing the policies
* @return a PolicySet containing the parsed policies
* @throws InternalException
* @throws NullPointerException
*/
public static PolicySet parsePolicies(String policiesString) throws InternalException {
PolicySet policySet = parsePoliciesJni(policiesString);
return policySet;
}

private static native PolicySet parsePoliciesJni(String policiesStr) throws InternalException, NullPointerException;
}
69 changes: 69 additions & 0 deletions CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.cedarpolicy;

import com.cedarpolicy.model.exception.InternalException;
import com.cedarpolicy.model.slice.Policy;
import com.cedarpolicy.model.slice.PolicySet;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.file.Path;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class PolicySetTests {
private static final String TEST_RESOURCES_DIR = "src/test/resources/";

@Test
public void parsePoliciesTests() throws InternalException, IOException {
PolicySet policySet = PolicySet.parsePolicies(Path.of(TEST_RESOURCES_DIR + "policies.cedar"));
for (Policy p: policySet.policies) {
assertNotNull(p.policySrc);
}
// Make sure the policy IDs are unique as Policies are made
assertEquals(2, policySet.policies.stream().map(p -> p.policyID).distinct().count());
assertEquals(2, policySet.policies.size());
assertEquals(0, policySet.templates.size());
}

@Test
public void parsePoliciesStringTests() throws InternalException {
PolicySet policySet = PolicySet.parsePolicies("permit(principal, action, resource);");
PolicySet policySet2 = PolicySet.parsePolicies("permit(principal, action, resource) when { principal has x && principal.x == 5};");
for (Policy p: policySet.policies) {
assertNotNull(p.policySrc);
}
assertEquals(1, policySet.policies.size());
assertEquals(0, policySet.templates.size());
for (Policy p: policySet2.policies) {
assertNotNull(p.policySrc);
}
assertEquals(1, policySet2.policies.size());
assertEquals(0, policySet2.templates.size());
}

@Test
public void parseTemplatesTests() throws InternalException, IOException {
PolicySet policySet = PolicySet.parsePolicies(Path.of(TEST_RESOURCES_DIR + "template.cedar"));
for (Policy p: policySet.policies) {
assertNotNull(p.policySrc);
}
assertEquals(2, policySet.policies.size());

for (Policy p: policySet.templates) {
assertNotNull(p.policySrc);
}
assertEquals(1, policySet.templates.size());
}

@Test
public void parsePoliciesExceptionTests() throws InternalException, IOException {
assertThrows(IOException.class, () -> {
PolicySet.parsePolicies(Path.of("nonExistentFilePath.cedar"));
});
assertThrows(InternalException.class, () -> {
PolicySet.parsePolicies(Path.of(TEST_RESOURCES_DIR + "malformed_policy_set.cedar"));
});
}
}
5 changes: 3 additions & 2 deletions CedarJava/src/test/java/com/cedarpolicy/PolicyTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand All @@ -16,7 +17,7 @@ public void parseStaticPolicyTests() {
assertDoesNotThrow(() -> {
var policy1 = Policy.parseStaticPolicy("permit(principal, action, resource);");
var policy2 = Policy.parseStaticPolicy("permit(principal, action, resource) when { principal has x && principal.x == 5};");
assertNotEquals(policy1.policyID.equals(policy2.policyID), true);
assertNotEquals(policy1.policyID, policy2.policyID);
});
assertThrows(InternalException.class, () -> {
Policy.parseStaticPolicy("permit();");
Expand All @@ -31,7 +32,7 @@ public void parsePolicyTemplateTests() {
assertDoesNotThrow(() -> {
String tbody = "permit(principal == ?principal, action, resource in ?resource);";
var template = Policy.parsePolicyTemplate(tbody);
assertTrue(template.policySrc.equals(tbody));
assertEquals(tbody, template.policySrc);
});
assertThrows(InternalException.class, () -> {
Policy.parsePolicyTemplate("permit(principal in ?resource, action, resource);");
Expand Down
69 changes: 10 additions & 59 deletions CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.cedarpolicy.model.AuthorizationSuccessResponse.Decision;
import com.cedarpolicy.model.exception.AuthException;
import com.cedarpolicy.model.exception.BadRequestException;
import com.cedarpolicy.model.exception.InternalException;
import com.cedarpolicy.model.schema.Schema;
import com.cedarpolicy.model.slice.Entity;
import com.cedarpolicy.model.slice.Policy;
Expand Down Expand Up @@ -211,10 +212,10 @@ private static class JsonEntity {

/**
* This method is the main entry point for JUnit. It returns a list of containers, which contain
* tests for junit to run. JUnit will run all the test returned from this method.
* tests for junit to run. JUnit will run all the tests returned from this method.
*/
@TestFactory
public List<DynamicContainer> integrationTestsFromJson() throws IOException {
public List<DynamicContainer> integrationTestsFromJson() throws InternalException, IOException {
List<DynamicContainer> tests = new ArrayList<>();
// handwritten integration tests
for (String testFile : JSON_TEST_FILES) {
Expand All @@ -236,6 +237,8 @@ public List<DynamicContainer> integrationTestsFromJson() throws IOException {
// inside the forEach we can't throw checked exceptions, but we
// can throw this unchecked exception
throw new UncheckedIOException(e);
} catch (final InternalException e) {
throw new RuntimeException(e);
}
});
}
Expand All @@ -247,14 +250,14 @@ public List<DynamicContainer> integrationTestsFromJson() throws IOException {
* test, and all the test in the json file are grouped into the returned container.
*/
@SuppressFBWarnings("NP_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD")
private DynamicContainer loadJsonTests(String jsonFile) throws IOException {
private DynamicContainer loadJsonTests(String jsonFile) throws InternalException, IOException {
JsonTest test;
try (InputStream jsonIn =
new FileInputStream(resolveIntegrationTestPath(jsonFile).toFile())) {
test = OBJECT_MAPPER.reader().readValue(jsonIn, JsonTest.class);
}
Set<Entity> entities = loadEntities(test.entities);
Set<Policy> policies = loadPolicies(test.policies);
PolicySet policySet = PolicySet.parsePolicies(resolveIntegrationTestPath(test.policies));
Schema schema = loadSchema(test.schema);

return DynamicContainer.dynamicContainer(
Expand All @@ -263,69 +266,18 @@ private DynamicContainer loadJsonTests(String jsonFile) throws IOException {
Stream.of(DynamicTest.dynamicTest(
jsonFile + ": validate",
() ->
executeJsonValidationTest(policies, schema, test.shouldValidate))),
executeJsonValidationTest(policySet.policies, schema, test.shouldValidate))),
test.requests.stream()
.map(
request ->
DynamicTest.dynamicTest(
jsonFile + ": " + request.description,
() ->
executeJsonRequestTest(
entities, policies, request,
entities, policySet, request,
schema)))));
}

/**
* Load all policies from the policy file. The policy file path must be relative to the shared
* integration test root. This should be the case if the path was obtained from a JsonTest
* object. Extra processing is required because the test format does not include policy ids, and
* does not explicit separate policies in a file other than by semicolons.
*/
private Set<Policy> loadPolicies(String policiesFile) throws IOException {
String policiesSrc = String.join("\n", Files.readAllLines(resolveIntegrationTestPath(policiesFile)));

// Get a list of the policy sources for the individual policies in the
// file by splitting the full policy source on semicolons. This will
// break if a semicolon shows up in a string, eid, or comment.
String[] policyStrings = policiesSrc.split(";");
// Some of the corpus tests contain semicolons in strings and/or eids.
// A simple way to check if the code above did the wrong thing in this case
// is to check for unmatched, unescaped quotes in the resulting policies.
for (String policyString : policyStrings) {
if (hasUnmatchedQuote(policyString)) {
policyStrings = null;
}
}

Set<Policy> policies = new HashSet<>();
if (policyStrings == null) {
// This case will only be reached for corpus tests.
// The corpus tests all consist of a single policy, so it is fine to use
// the full policy source as a single policy.
policies.add(new Policy(policiesSrc, "policy0"));
} else {
for (int i = 0; i < policyStrings.length; i++) {
// The policy source doesn't include an explicit policy id, but the expected output
// implicitly assumes policies are numbered by their position in file.
String policyId = "policy" + i;
String policySrc = policyStrings[i];
if (!policySrc.trim().isEmpty()) {
policies.add(new Policy(policySrc + ";", policyId));
}
}
}
return policies;
}

/** Check for unmatched quotes. */
private Boolean hasUnmatchedQuote(String str) {
// Ignore escaped quotes, i.e. \"
// Note that backslashes in the regular expression have to be double escaped.
String newStr = str.replaceAll("\\\\\"", "");
long count = newStr.chars().filter(ch -> ch == '\"').count();
return (count % 2 == 1);
}

/** Load the schema file. */
private Schema loadSchema(String schemaFile) throws IOException {
try (InputStream schemaStream =
Expand Down Expand Up @@ -394,7 +346,7 @@ private void executeJsonValidationTest(Set<Policy> policies, Schema schema, Bool
* that the result is equal to the expected result.
*/
private void executeJsonRequestTest(
Set<Entity> entities, Set<Policy> policies, JsonRequest request, Schema schema) throws AuthException {
Set<Entity> entities, PolicySet policySet, JsonRequest request, Schema schema) throws AuthException {
AuthorizationEngine auth = new BasicAuthorizationEngine();
AuthorizationRequest authRequest =
new AuthorizationRequest(
Expand All @@ -404,7 +356,6 @@ private void executeJsonRequestTest(
Optional.of(request.context),
Optional.of(schema),
request.validateRequest);
PolicySet policySet = new PolicySet(policies);

try {
final AuthorizationResponse response = auth.isAuthorized(authRequest, policySet, entities);
Expand Down
13 changes: 13 additions & 0 deletions CedarJava/src/test/resources/malformed_policy_set.cedar
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@id("Proper Policy")
permit (
principal == User::"Matt",
action == Action::"view",
resource == Photo::"Husky.jpg"
);

@id("Malformed Policy")
forbid (
principal == User::"Liam",
action,
resource = Photo::"Husky.jpg"
);
13 changes: 13 additions & 0 deletions CedarJava/src/test/resources/policies.cedar
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@id("Policy #1")
permit (
principal in UserGroup::"friends",
action == Action::"view",
resource == Photo::"Husky.jpg"
);

@id("Policy #2")
forbid (
principal == User::"Matt",
action,
resource == Photo::"Husky.jpg"
);
19 changes: 19 additions & 0 deletions CedarJava/src/test/resources/template.cedar
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@id("Policy #1")
permit (
principal == User::"Aaron",
action == Action::"view",
resource == Photo::"SomePhoto.jpg"
);

@id("Policy #2")
permit (principal == User::"Josh", action == Action::"comment", resource == Photo::"SomePhoto.jpg");

@id("Template #1")
permit (
principal in ?principal,
action in [Action::"view", Action::"comment"],
resource in ?resource
)
unless {
resource.tag == "private"
};
Loading