Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: policy, template, and schema parsing #72

Merged
merged 1 commit into from
Jan 9, 2024
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 CedarJava/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,4 @@ test {
showStandardStreams false
exceptionFormat 'full'
}
}
}
Empty file modified CedarJava/config.sh
100644 → 100755
Empty file.
16 changes: 16 additions & 0 deletions CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.cedarpolicy.model.schema;

import com.cedarpolicy.model.exception.InternalException;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -27,6 +28,10 @@
public final class Schema {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

static {
System.load(System.getenv("CEDAR_JAVA_FFI_LIB"));
}

// The schema after being parsed as a JSON object.
@JsonValue private final JsonNode schemaJson;

Expand Down Expand Up @@ -84,4 +89,15 @@ public int hashCode() {
public String toString() {
return "Schema(schemaJson=" + schemaJson + ")";
}

public static Schema parse(String schemaStr) throws IOException, InternalException {
var success = parseSchema(schemaStr).equals("Success");
if (success) {
return new Schema(schemaStr);
} else {
throw new IOException("Unable to parse schema");
}
}

private static native String parseSchema(String schemaStr) throws InternalException;
}
45 changes: 44 additions & 1 deletion CedarJava/src/main/java/com/cedarpolicy/model/slice/Policy.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,24 @@

package com.cedarpolicy.model.slice;

import com.cedarpolicy.model.exception.InternalException;
import com.cedarpolicy.value.EntityUID;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import java.util.concurrent.atomic.AtomicInteger;


/** Policies in the Cedar language. */
public class Policy {
private static final Logger LOG = LoggerFactory.getLogger(Policy.class);
private static final AtomicInteger idCounter = new AtomicInteger(0);
static {
System.load(System.getenv("CEDAR_JAVA_FFI_LIB"));
}

/** Policy string. */
public final String policySrc;
/** Policy ID. */
Expand All @@ -41,7 +55,7 @@ public Policy(
throw new NullPointerException("Failed to construct policy from null string");
}
if (policyID == null) {
throw new NullPointerException("Failed to construct policy with null ID");
policyID = "policy" + idCounter.addAndGet(1);
}
this.policySrc = policy;
this.policyID = policyID;
Expand All @@ -51,4 +65,33 @@ public Policy(
public String toString() {
return "// Policy ID: " + policyID + "\n" + policySrc;
}

public static Policy parseStaticPolicy(String policyStr) throws InternalException, NullPointerException {
var policyText = parsePolicyJni(policyStr);
return new Policy(policyText, null);
}

public static Policy parsePolicyTemplate(String templateStr) throws InternalException, NullPointerException {
var templateText = parsePolicyTemplateJni(templateStr);
return new Policy(templateText, null);
}

/**
* This method takes in a Policy and a list of Instantiations and calls Cedar JNI to ensure those slots
* can be used to instantiate the template. If the Template is validated ahead of time by using Policy.parsePolicyTemplate
* and the Instantiations are also ensured to be valid (for example, by validating their parts using EntityTypeName.parse
* and EntityIdentifier.parse), then this should only fail because the slots in the template don't match the instantiations
* (barring JNI failures).
* @param p Policy object constructed from a valid template. Best if built from Policy.parsePolicyTemplate
* @param principal EntityUid to put into the principal slot. Leave null if there's no principal slot
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably not as idiomatic for Java, but I strongly prefer Optional

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional is not really a fit though, we need to surface why it failed if it fails. We have two choices that offer library ergonomics that are reasonable to java programmers:

  • Just throw exceptions
  • Use a library that implements an either monad, or implement it ourselves

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I should have been more clear. I'd prefer that the principal and resource be Optional<EntityUID> instead of using null when there's no slot. I agree we should throw an exception on failure, not return an Optional<boolean>.

* @param resource EntityUid to put into the resource slot. Leave null if there's no resource slot
* @return
*/
public static boolean validateTemplateLinkedPolicy(Policy p, EntityUID principal, EntityUID resource) throws InternalException, NullPointerException {
return validateTemplateLinkedPolicyJni(p.policySrc, principal, resource);
}

private static native String parsePolicyJni(String policyStr) throws InternalException, NullPointerException;
private static native String parsePolicyTemplateJni(String policyTemplateStr) throws InternalException, NullPointerException;
private static native boolean validateTemplateLinkedPolicyJni(String templateText, EntityUID principal, EntityUID resource) throws InternalException, NullPointerException;
}
13 changes: 4 additions & 9 deletions CedarJava/src/test/java/com/cedarpolicy/AuthTests.java
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
package com.cedarpolicy;

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

import java.util.Optional;
import java.util.HashSet;
import java.util.HashMap;

import org.junit.jupiter.api.Test;
import com.cedarpolicy.BasicAuthorizationEngine;
import com.cedarpolicy.model.AuthorizationRequest;
import com.cedarpolicy.model.exception.AuthException;
import com.cedarpolicy.model.slice.BasicSlice;
import com.cedarpolicy.model.slice.Policy;
import com.cedarpolicy.model.slice.Slice;
import com.cedarpolicy.value.EntityUID;
import com.cedarpolicy.value.EntityTypeName;

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

public class AuthTests {

@Test
public void simple() {
var auth = new BasicAuthorizationEngine();
Expand All @@ -34,5 +30,4 @@ public void simple() {
}, "Should not throw AuthException");

}

}
35 changes: 18 additions & 17 deletions CedarJava/src/test/java/com/cedarpolicy/EntityTypeNameTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public class EntityTypeNameTests {
"else",
"in",
"like",
"has"
"has",
"is"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

luckily enough i ran into a test failure from this lol

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it is nice that it will "eventually" catch the issue, but I really don't like non-determinism in CI. Created issue here: #73

};

@Test
Expand Down Expand Up @@ -81,8 +82,8 @@ public void partInvalid() {
}

@Test
public void nullSafety() {
assertThrows(NullPointerException.class,
public void nullSafety() {
assertThrows(NullPointerException.class,
() -> EntityTypeName.parse(null),
"Null pointer exception should be thrown"
);
Expand All @@ -97,37 +98,37 @@ public void rejectsKeywords() {
}

@Property
public void equalNull(@ForAll @From("multiLevelName") EntityTypeName n) {
public void equalNull(@ForAll @From("multiLevelName") EntityTypeName n) {
assertFalse(n.equals(null));
}

@Test
public void emptyString() {
public void emptyString() {
assertFalse(EntityTypeName.parse("").isPresent());
}

@Property
public void roundTrip(@ForAll @From("multiLevelName") EntityTypeName name) {
var s = name.toString();
var o = EntityTypeName.parse(s);
@Property
public void roundTrip(@ForAll @From("multiLevelName") EntityTypeName name) {
var s = name.toString();
var o = EntityTypeName.parse(s);
assertTrue(o.isPresent());
assertEquals(o.get(), name);
assertEquals(o.get(), name);
assertEquals(o.get().hashCode(), name.hashCode());
assertEquals(s, o.get().toString());
}

@Property
public void singleLevelRoundTrip(@ForAll @From("validName") String name) {
@Property
public void singleLevelRoundTrip(@ForAll @From("validName") String name) {
var o = EntityTypeName.parse(name);
assertTrue(o.isPresent());
var e = o.get();
assertEquals(e.toString(), name);
var e = o.get();
assertEquals(e.toString(), name);
assertEquals(EntityTypeName.parse(e.toString()).get(), e);
}


@Provide
public static Arbitrary<EntityTypeName> multiLevelName() {
public static Arbitrary<EntityTypeName> multiLevelName() {
Arbitrary<List<String>> namespace = validName().collect(lst -> lst.size() >= 1 );
return namespace.map(parts -> parse(parts));
}
Expand All @@ -143,7 +144,7 @@ public static EntityTypeName parse(List<String> parts) {
}

@Provide
public static Arbitrary<String> validName() {
public static Arbitrary<String> validName() {
var first = Arbitraries.chars().alpha();
var rest = Arbitraries.strings().alpha().numeric().ofMinLength(0);
return Combinators.combine(first, rest).as((f,r) -> f + r).filter(str -> !isKeyword(str));
Expand Down
102 changes: 102 additions & 0 deletions CedarJava/src/test/java/com/cedarpolicy/PolicyTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.cedarpolicy;

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

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

public class PolicyTests {
@Test
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);
});
assertThrows(InternalException.class, () -> {
Policy.parseStaticPolicy("permit();");
});
assertThrows(NullPointerException.class, () -> {
Policy.parseStaticPolicy(null);
});
}

@Test
public void parsePolicyTemplateTests() {
assertDoesNotThrow(() -> {
String tbody = "permit(principal == ?principal, action, resource in ?resource);";
var template = Policy.parsePolicyTemplate(tbody);
assertTrue(template.policySrc.equals(tbody));
});
assertThrows(InternalException.class, () -> {
Policy.parsePolicyTemplate("permit(principal in ?resource, action, resource);");
});
}

@Test
public void validateTemplateLinkedPolicySuccessTest() {
Policy p = new Policy("permit(principal == ?principal, action, resource in ?resource);", null);
EntityUID principal1 = EntityUID.parse("Library::User::\"Victor\"").get();
EntityUID resource1 = EntityUID.parse("Library::Book::\"The black Swan\"").get();

Policy p2 = new Policy("permit(principal, action, resource in ?resource);", null);
EntityUID resource2 = EntityUID.parse("Library::Book::\"Thinking Fast and Slow\"").get();

Policy p3 = new Policy("permit(principal == ?principal, action, resource);", null);

Policy p4 = new Policy("permit(principal, action, resource);", null);

assertDoesNotThrow(() -> {
assertTrue(Policy.validateTemplateLinkedPolicy(p, principal1, resource1));
assertTrue(Policy.validateTemplateLinkedPolicy(p2, null, resource2));
assertTrue(Policy.validateTemplateLinkedPolicy(p3, principal1, null));
assertTrue(Policy.validateTemplateLinkedPolicy(p4, null, null));
});
}
@Test
public void validateTemplateLinkedPolicyFailsWhenExpected() {
Policy p1 = new Policy("permit(principal, action, resource);", null);
EntityUID principal = EntityUID.parse("Library::User::\"Victor\"").get();
EntityUID resource = EntityUID.parse("Library::Book::\"Thinking Fast and Slow\"").get();

Policy p2 = new Policy("permit(principal, action, resource in ?resource);", null);


Policy p3 = new Policy("permit(principal == ?principal, action, resource);", null);

// fails if we fill either slot in a policy with no slots
assertThrows(InternalException.class, () -> {
Policy.validateTemplateLinkedPolicy(p1, principal, null);
});
assertThrows(InternalException.class, () -> {
Policy.validateTemplateLinkedPolicy(p1, null, resource);
});
assertThrows(InternalException.class, () -> {
Policy.validateTemplateLinkedPolicy(p1, null, resource);
});
assertThrows(InternalException.class, () -> {
Policy.validateTemplateLinkedPolicy(p1, principal, resource);
});


// fails if we fill both slots or the wrong slot in a policy with one slot
assertThrows(InternalException.class, () -> {
Policy.validateTemplateLinkedPolicy(p2, principal, null);
});
assertThrows(InternalException.class, () -> {
Policy.validateTemplateLinkedPolicy(p2, principal, resource);
});

assertThrows(InternalException.class, () -> {
Policy.validateTemplateLinkedPolicy(p3, null, resource);
});
assertThrows(InternalException.class, () -> {
Policy.validateTemplateLinkedPolicy(p3, principal, resource);
});
}
}
20 changes: 20 additions & 0 deletions CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.cedarpolicy;

import com.cedarpolicy.model.schema.Schema;
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() {
assertDoesNotThrow(() -> {
Schema.parse("{\"ns1\": {\"entityTypes\": {}, \"actions\": {}}}");
Schema.parse("{}");
});
assertThrows(Exception.class, () -> {
Schema.parse("{\"foo\": \"bar\"}");
});
}
}
2 changes: 1 addition & 1 deletion CedarJavaFFI/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ version = "3.0.0"
[dependencies]
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
cedar-policy = { version = "3.0", path = "../cedar/cedar-policy" } # Need latest version from github
cedar-policy = { version = "3.0", path = "../../cedar/cedar-policy" } # Need latest version from github
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README in CedarJavaFFI says that it should be ../cedar, so you should either update this toml file or the README.


# JNI Support
jni = "0.21.0"
Expand Down
Loading