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
41 changes: 40 additions & 1 deletion CedarJava/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ spotbugs {
ignoreFailures.set(false)
}

tasks.withType(com.github.spotbugs.snom.SpotBugsTask).configureEach {
if (name == 'spotbugsJmh') {
excludeFilter = file('config/spotbugs/jmh-exclude.xml')
}
}

repositories {
mavenCentral()
}
Expand All @@ -75,6 +81,15 @@ configurations {
testCompileOnly.extendsFrom compileOnly
}

sourceSets {
jmh {
java.srcDirs = ['src/jmh/java']
resources.srcDirs = ['src/jmh/resources', 'src/test/resources']
compileClasspath += sourceSets.main.output + sourceSets.main.compileClasspath
runtimeClasspath += sourceSets.main.output + sourceSets.main.runtimeClasspath
}
}

dependencies {
def junitVersion = '6.0.0'

Expand All @@ -93,6 +108,9 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junitVersion}"

jmhImplementation 'org.openjdk.jmh:jmh-core:1.37'
jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
}

def ffiDir = '../CedarJavaFFI'
Expand Down Expand Up @@ -127,7 +145,7 @@ tasks.register('installCargoZigbuild', Exec) {
group 'Build'
description 'Installs Cargo Zigbuild for Rust compilation.'

commandLine 'cargo', '+' + RustVersion, 'install', 'cargo-zigbuild@0.22.1'
commandLine 'cargo', '+' + RustVersion, 'install', '--locked', 'cargo-zigbuild@0.22.1'
}

def ZigVersion = '0.11'
Expand Down Expand Up @@ -261,6 +279,27 @@ compileTestJava {
targetCompatibility = "17"
}

compileJmhJava {
sourceCompatibility = "17"
targetCompatibility = "17"
}

tasks.register('jmh', JavaExec) {
group 'Benchmark'
description 'Runs JMH benchmarks for JNI performance.'

dependsOn('compileFFI')
dependsOn('jmhClasses')

mainClass = 'org.openjdk.jmh.Main'
classpath = sourceSets.jmh.runtimeClasspath + files(layout.buildDirectory.dir(compiledLibDir))

// Pass through any -Pjmh.args="..." to JMH (e.g., -Pjmh.args="-f 0 -i 1 -wi 1")
if (project.hasProperty('jmh.args')) {
args((project.property('jmh.args') as String).split('\\s+'))
}
}

compileJava {
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
Expand Down
7 changes: 7 additions & 0 deletions CedarJava/config/spotbugs/jmh-exclude.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
<!-- Exclude JMH-generated code: padding fields and dead stores are intentional for cache-line isolation -->
<Match>
<Package name="~com\.cedarpolicy\.jmh_generated.*"/>
</Match>
</FindBugsFilter>
143 changes: 143 additions & 0 deletions CedarJava/src/jmh/java/com/cedarpolicy/AuthorizationBenchmark.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* 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 com.cedarpolicy.model.AuthorizationRequest;
import com.cedarpolicy.model.AuthorizationResponse;
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.policy.PolicySet;
import com.cedarpolicy.model.schema.Schema;
import com.cedarpolicy.model.schema.Schema.JsonOrCedar;
import com.cedarpolicy.value.EntityTypeName;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
* JMH benchmarks for Cedar authorization engine JNI performance.
*
* <p>Run via: ./gradlew jmh
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
@State(Scope.Benchmark)
public class AuthorizationBenchmark {

private BasicAuthorizationEngine engine;

// Small scenario: minimal request
private AuthorizationRequest smallRequest;
private PolicySet smallPolicySet;
private Set<Entity> smallEntities;

// Medium scenario: photoflash schema with entities and multiple policies
private AuthorizationRequest mediumRequest;
private PolicySet mediumPolicySet;
private Set<Entity> mediumEntities;

// Validation scenario
private ValidationRequest validationRequest;

@Setup(Level.Trial)
public void setUp() throws Exception {
engine = new BasicAuthorizationEngine();

setUpSmallScenario();
setUpMediumScenario();
setUpValidationScenario();
}

private static String readResource(String resourcePath) throws Exception {
return new String(
Files.readAllBytes(Paths.get(
AuthorizationBenchmark.class.getResource(resourcePath).toURI())),
StandardCharsets.UTF_8);
}

private void setUpSmallScenario() throws Exception {
EntityTypeName userType = EntityTypeName.parse("User").get();
EntityTypeName actionType = EntityTypeName.parse("Action").get();
EntityTypeName resourceType = EntityTypeName.parse("Resource").get();

smallRequest = new AuthorizationRequest(
userType.of("alice"), actionType.of("view"), resourceType.of("doc1"), new HashMap<>());

smallPolicySet = PolicySet.parsePolicies(readResource("/small_policies.cedar"));
smallEntities = Entities.parse(readResource("/small_entities.json")).getEntities();
}

private void setUpMediumScenario() throws Exception {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: I think if we anticipate adding more scenarios, we might want to rename this (perhaps basicPhotoFlashScenario), but I think this is good as is

EntityTypeName userType = EntityTypeName.parse("User").get();
EntityTypeName actionType = EntityTypeName.parse("Action").get();
EntityTypeName photoType = EntityTypeName.parse("Photo").get();

mediumRequest = new AuthorizationRequest(
userType.of("alice"), actionType.of("View_Photo"), photoType.of("pic01"), new HashMap<>());

mediumPolicySet = PolicySet.parsePolicies(readResource("/medium_policies.cedar"));
mediumEntities = Entities.parse(readResource("/medium_entities.json")).getEntities();
}

private void setUpValidationScenario() throws Exception {
String schemaText = readResource("/photoflash_schema.json");
Schema schema = new Schema(JsonOrCedar.Json, Optional.of(schemaText), Optional.empty());

PolicySet policySet = PolicySet.parsePolicies(
"permit(principal == User::\"alice\", action == Action::\"View_Photo\", resource);");

validationRequest = new ValidationRequest(schema, policySet);
}

@Benchmark
public AuthorizationResponse isAuthorizedSmall() throws AuthException {
return engine.isAuthorized(smallRequest, smallPolicySet, smallEntities);
}

@Benchmark
public AuthorizationResponse isAuthorizedMedium() throws AuthException {
return engine.isAuthorized(mediumRequest, mediumPolicySet, mediumEntities);
}

@Benchmark
public ValidationResponse validateSmall() throws AuthException {
return engine.validate(validationRequest);
}
}
7 changes: 7 additions & 0 deletions CedarJava/src/jmh/resources/medium_entities.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{"uid": {"type": "User", "id": "alice"}, "attrs": {}, "parents": []},
{"uid": {"type": "Action", "id": "View_Photo"}, "attrs": {}, "parents": []},
{"uid": {"type": "Photo", "id": "pic01"}, "attrs": {}, "parents": [{"type": "Album", "id": "vacation"}]},
{"uid": {"type": "Album", "id": "vacation"}, "attrs": {}, "parents": [{"type": "Account", "id": "account1"}]},
{"uid": {"type": "Account", "id": "account1"}, "attrs": {}, "parents": []}
]
9 changes: 9 additions & 0 deletions CedarJava/src/jmh/resources/medium_policies.cedar
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
permit(principal == User::"alice", action == Action::"View_Photo", resource);

permit(principal, action == Action::"View_Photo", resource in Album::"vacation");

forbid(principal, action, resource) when { resource.private };

permit(principal, action == Action::"Edit_Photo", resource) when { principal == resource.owner };

permit(principal, action == Action::"Delete_Photo", resource) when { principal == resource.owner };
5 changes: 5 additions & 0 deletions CedarJava/src/jmh/resources/small_entities.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
{"uid": {"type": "User", "id": "alice"}, "attrs": {}, "parents": []},
{"uid": {"type": "Action", "id": "view"}, "attrs": {}, "parents": []},
{"uid": {"type": "Resource", "id": "doc1"}, "attrs": {}, "parents": []}
]
1 change: 1 addition & 0 deletions CedarJava/src/jmh/resources/small_policies.cedar
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
permit(principal, action, resource);
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,20 @@
import com.fasterxml.jackson.annotation.JsonInclude;
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;

/** An authorization engine that is compiled in process. Communicated with via JNI. */
public final class BasicAuthorizationEngine implements AuthorizationEngine {
static {
LibraryLoader.loadLibrary();
String jniVersion = getCedarJNIVersion();
Comment thread
geekphilosophy marked this conversation as resolved.
String langVersion = AuthorizationEngine.getCedarLangVersion();
if (!jniVersion.equals(langVersion)) {
throw new ExceptionInInitializerError(
"Error, Java Cedar Language version is " + langVersion
+ " but JNI Cedar Language version is " + jniVersion);
}
}

/** Construct a basic authorization engine. */
Expand Down Expand Up @@ -123,21 +129,11 @@ public void validateEntities(EntityValidationRequest q) throws AuthException {
private static <REQ, RESP> RESP call(String operation, Class<RESP> responseClass, REQ request)
throws AuthException {
try {
final String cedarJNIVersion = getCedarJNIVersion();
if (!cedarJNIVersion.equals(AuthorizationEngine.getCedarLangVersion())) {
throw new AuthException(
"Error, Java Cedar Language version is "
+ AuthorizationEngine.getCedarLangVersion()
+ " but JNI Cedar Language version is "
+ cedarJNIVersion);
}
// Convert the request POJO to a JSON string
final String fullRequest = objectWriter().writeValueAsString(request);

final String response = callCedarJNI(operation, fullRequest);

final JsonNode responseNode = objectReader().readTree(response);
return objectReader().readValue(responseNode, responseClass);
return objectReader().readValue(response, responseClass);
} catch (JsonProcessingException e) {
throw new AuthException("JSON Serialization Error", e);
} catch (IllegalArgumentException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public DetailedError getError() {

@Override
public String toString() {
return String.format("AuthorizationError{policyId=%s, error=%s}", policyId, error);
return String.format("AuthorizationError{policyId=%s, error=%s}", policyId, error);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
public final class LevelValidationRequest {
private final Schema schema;
private final PolicySet policies;
private final long maxDerefLevel; // Must be non-negative (>=0)
private final long maxDerefLevel; // Must be non-negative (>=0)

/**
* Construct a validation request.
Expand Down Expand Up @@ -77,7 +77,7 @@ public PolicySet getPolicySet() {

/**
* Get the maximum deref level.
*
*
* @return The maximum deref level value for validation
*/
public long getMaxDerefLevel() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ public String toCedarFormat() throws InternalException, IllegalStateException, N
* @return JsonNode representing the schema in JSON format
* @throws InternalException If conversion from Cedar to JSON format fails
* @throws IllegalStateException If schema content is missing
* @throws JsonMappingException If invalid JSON
* @throws JsonProcessingException If invalid JSON
* @throws JsonMappingException If invalid JSON
* @throws JsonProcessingException If invalid JSON
* @throws NullPointerException
*/
public JsonNode toJsonFormat()
Expand Down
Loading
Loading