diff --git a/Cargo.toml b/Cargo.toml index f8bab90..f2887e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ members = [ "bindings/ffi", "bindings/python", - "bindings/wasm" + "bindings/wasm", + "bindings/java", ] [package] diff --git a/bindings/java/.gitignore b/bindings/java/.gitignore new file mode 100644 index 0000000..9f97022 --- /dev/null +++ b/bindings/java/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml new file mode 100644 index 0000000..45258f7 --- /dev/null +++ b/bindings/java/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "regorus-java" +version = "0.1.0" +edition = "2021" +repository = "https://github.com/microsoft/regorus/bindings/java" +description = "Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust" +keywords = ["interpreter", "opa", "policy-as-code", "rego"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1.0.79" +serde_json = "1.0.112" +jni = "0.21.1" +regorus = { path = "../.." } diff --git a/bindings/java/README.md b/bindings/java/README.md new file mode 100644 index 0000000..de2f788 --- /dev/null +++ b/bindings/java/README.md @@ -0,0 +1,49 @@ +# Regorus Java + +**Regorus** is + + - *Rego*-*Rus(t)* - A fast, light-weight [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) + interpreter written in Rust. + - *Rigorous* - A rigorous enforcer of well-defined Rego semantics. + +See main [Regorus page](https://github.com/microsoft/regorus) for more details about the project. + +Regorus can be used in Java via `com.microsoft.regorus` package. (It is not yet available in Maven Central, but can be manually built.) + +## Building + +You can build this binding using [Maven](https://maven.apache.org/): +```shell +$ mvn package +$ file target/regorus-java-0.0.1* +target/regorus-java-0.0.1-osx-aarch_64.jar: Zip archive data, at least v1.0 to extract, compression method=deflate +target/regorus-java-0.0.1.jar: Zip archive data, at least v1.0 to extract, compression method=deflate +``` + +## Usage + +```java +import com.microsoft.regorus.Engine; + +public class Test { + public static void main(String[] args) { + try (Engine engine = new Engine()) { + engine.pubAddPolicy( + "hello.rego", + "package test\nmessage = concat(\", \", [input.message, data.message])" + ); + engine.pubAddDataJson("{\"message\":\"World!\"}"); + engine.pubSetInputJson("{\"message\":\"Hello\"}"); + String resJson = engine.pubEvalQuery("data.test.message"); + + System.out.println(resJson); + } + } +} +``` + +and run it with: +```shell +$ java -cp target/regorus-java-0.0.1.jar:target/regorus-java-0.0.1-osx-aarch_64.jar Test.java +{"result":[{"expressions":[{"value":"Hello, World!","text":"data.test.message","location":{"row":1,"col":1}}]}]} +``` \ No newline at end of file diff --git a/bindings/java/com_microsoft_regorus_Engine.h b/bindings/java/com_microsoft_regorus_Engine.h new file mode 100644 index 0000000..c5e1638 --- /dev/null +++ b/bindings/java/com_microsoft_regorus_Engine.h @@ -0,0 +1,85 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_microsoft_regorus_Engine */ + +#ifndef _Included_com_microsoft_regorus_Engine +#define _Included_com_microsoft_regorus_Engine +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_microsoft_regorus_Engine + * Method: newEngine + * Signature: ()J + */ +JNIEXPORT jlong JNICALL Java_com_microsoft_regorus_Engine_newEngine + (JNIEnv *, jclass); + +/* + * Class: com_microsoft_regorus_Engine + * Method: addPolicy + * Signature: (JLjava/lang/String;Ljava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_addPolicy + (JNIEnv *, jclass, jlong, jstring, jstring); + +/* + * Class: com_microsoft_regorus_Engine + * Method: addPolicyFromFile + * Signature: (JLjava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_addPolicyFromFile + (JNIEnv *, jclass, jlong, jstring); + +/* + * Class: com_microsoft_regorus_Engine + * Method: addDataJson + * Signature: (JLjava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_addDataJson + (JNIEnv *, jclass, jlong, jstring); + +/* + * Class: com_microsoft_regorus_Engine + * Method: addDataJsonFromFile + * Signature: (JLjava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_addDataJsonFromFile + (JNIEnv *, jclass, jlong, jstring); + +/* + * Class: com_microsoft_regorus_Engine + * Method: setInputJson + * Signature: (JLjava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_setInputJson + (JNIEnv *, jclass, jlong, jstring); + +/* + * Class: com_microsoft_regorus_Engine + * Method: setInputJsonFromFile + * Signature: (JLjava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_setInputJsonFromFile + (JNIEnv *, jclass, jlong, jstring); + +/* + * Class: com_microsoft_regorus_Engine + * Method: evalQuery + * Signature: (JLjava/lang/String;)Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_com_microsoft_regorus_Engine_evalQuery + (JNIEnv *, jclass, jlong, jstring); + +/* + * Class: com_microsoft_regorus_Engine + * Method: destroyEngine + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_com_microsoft_regorus_Engine_destroyEngine + (JNIEnv *, jclass, jlong); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/bindings/java/pom.xml b/bindings/java/pom.xml new file mode 100644 index 0000000..f8edba5 --- /dev/null +++ b/bindings/java/pom.xml @@ -0,0 +1,158 @@ + + + + + 4.0.0 + + com.microsoft.regorus + regorus-java + 0.0.1 + + Regorus Java + Java bindings for Regorus - a fast, lightweight Rego interpreter written in Rust + https://github.com/microsoft/regorus/bindings/java + + + UTF-8 + 1.8 + 1.8 + + + + + junit + junit + 3.8.1 + test + + + com.google.code.gson + gson + 2.10.1 + test + + + + + + + + maven-clean-plugin + 3.1.0 + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + + + + kr.motd.maven + os-maven-plugin + 1.7.0 + + + + + + + exec-maven-plugin + org.codehaus.mojo + 3.1.0 + + + compile-native-code + compile + + exec + + + python3 + + ${project.basedir}/tools/build.py + --classifier + ${os.detected.classifier} + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + org.apache.maven.plugins + maven-jar-plugin + + + + default-jar + + + native/** + + + + + + native-jar + + jar + + + ${os.detected.classifier} + + native/** + + + + + + + + + + + + maven-project-info-reports-plugin + + + + diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs new file mode 100644 index 0000000..98ffd22 --- /dev/null +++ b/bindings/java/src/lib.rs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use anyhow::Result; +use jni::objects::{JClass, JObject, JString}; +use jni::sys::{jlong, jstring}; +use jni::JNIEnv; + +use regorus::{Engine, Value}; + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Engine_newEngine( + _env: JNIEnv, + _class: JClass, +) -> jlong { + let engine = Engine::new(); + Box::into_raw(Box::new(engine)) as jlong +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Engine_addPolicy( + env: JNIEnv, + _class: JClass, + engine_ptr: jlong, + path: JString, + rego: JString, +) { + let _ = throw_err(env, |env| { + let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; + let path: String = env.get_string(&path)?.into(); + let rego: String = env.get_string(®o)?.into(); + engine.add_policy(path, rego)?; + Ok(()) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Engine_addPolicyFromFile( + env: JNIEnv, + _class: JClass, + engine_ptr: jlong, + path: JString, +) { + let _ = throw_err(env, |env| { + let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; + let path: String = env.get_string(&path)?.into(); + engine.add_policy_from_file(path)?; + Ok(()) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Engine_addDataJson( + env: JNIEnv, + _class: JClass, + engine_ptr: jlong, + data: JString, +) { + let _ = throw_err(env, |env| { + let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; + let data: String = env.get_string(&data)?.into(); + engine.add_data_json(&data)?; + Ok(()) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Engine_addDataJsonFromFile( + env: JNIEnv, + _class: JClass, + engine_ptr: jlong, + path: JString, +) { + let _ = throw_err(env, |env| { + let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; + let path: String = env.get_string(&path)?.into(); + engine.add_data(Value::from_json_file(path)?)?; + Ok(()) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Engine_setInputJson( + env: JNIEnv, + _class: JClass, + engine_ptr: jlong, + input: JString, +) { + let _ = throw_err(env, |env| { + let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; + let input: String = env.get_string(&input)?.into(); + engine.set_input_json(&input)?; + Ok(()) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Engine_setInputJsonFromFile( + env: JNIEnv, + _class: JClass, + engine_ptr: jlong, + path: JString, +) { + let _ = throw_err(env, |env| { + let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; + let path: String = env.get_string(&path)?.into(); + engine.set_input(Value::from_json_file(&path)?); + Ok(()) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Engine_evalQuery( + env: JNIEnv, + _class: JClass, + engine_ptr: jlong, + query: JString, +) -> jstring { + let res = throw_err(env, |env| { + let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; + let query: String = env.get_string(&query)?.into(); + let results = engine.eval_query(query, false)?; + let output = env.new_string(serde_json::to_string(&results)?)?; + Ok(output.into_raw()) + }); + + match res { + Ok(val) => val, + Err(_) => JObject::null().into_raw(), + } +} + +#[no_mangle] +pub unsafe extern "system" fn Java_com_microsoft_regorus_Engine_destroyEngine( + _env: JNIEnv, + _class: JClass, + engine_ptr: jlong, +) { + let _engine = Box::from_raw(engine_ptr as *mut Engine); +} + +fn throw_err(mut env: JNIEnv, mut f: impl FnMut(&mut JNIEnv) -> Result) -> Result { + match f(&mut env) { + Ok(val) => Ok(val), + Err(err) => { + env.throw(err.to_string())?; + Err(err) + } + } +} diff --git a/bindings/java/src/main/java/com/microsoft/regorus/Engine.java b/bindings/java/src/main/java/com/microsoft/regorus/Engine.java new file mode 100644 index 0000000..17b2ed3 --- /dev/null +++ b/bindings/java/src/main/java/com/microsoft/regorus/Engine.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + **/ + +package com.microsoft.regorus; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.atomic.AtomicReference; + +public class Engine implements AutoCloseable { + // Methods exposed from Rust side, you can run + // `javac -h . src/main/java/com/microsoft/regorus/Engine.java` to update + // expected native header at `bindings/java/com_microsoft_regorus_Engine.h` + // if you update the native API. + private static native long newEngine(); + private static native void addPolicy(long enginePtr, String path, String rego); + private static native void addPolicyFromFile(long enginePtr, String path); + private static native void addDataJson(long enginePtr, String data); + private static native void addDataJsonFromFile(long enginePtr, String path); + private static native void setInputJson(long enginePtr, String input); + private static native void setInputJsonFromFile(long enginePtr, String path); + private static native String evalQuery(long enginePtr, String query); + private static native void destroyEngine(long enginePtr); + + // Pointer to Engine allocated on Rust's heap, all native methods works on + // engine expects this pointer. It is free'd in `close` method. + private final long enginePtr; + + public Engine() { + enginePtr = newEngine(); + } + + public void pubAddPolicy(String path, String rego) { + addPolicy(enginePtr, path, rego); + } + + public void pubAddDataJson(String path) { + addDataJson(enginePtr, path); + } + + public void pubSetInputJson(String path) { + setInputJson(enginePtr, path); + } + + public String pubEvalQuery(String path) { + return evalQuery(enginePtr, path); + } + + @Override + public void close() { + destroyEngine(enginePtr); + } + + // Loading native library from jar is adapted from: + // https://github.com/apache/opendal/blob/93e5f65bbf30df2fed4bdd95bb0685c73c6418c2/bindings/java/src/main/java/org/apache/opendal/NativeLibrary.java + // https://github.com/apache/opendal/blob/93e5f65bbf30df2fed4bdd95bb0685c73c6418c2/bindings/java/src/main/java/org/apache/opendal/Environment.java + private static final String classifier; + static { + final StringBuilder classifierBuilder = new StringBuilder(); + final String os = System.getProperty("os.name").toLowerCase(); + if (os.startsWith("windows")) { + classifierBuilder.append("windows"); + } else if (os.startsWith("mac")) { + classifierBuilder.append("osx"); + } else { + classifierBuilder.append("linux"); + } + classifierBuilder.append("-"); + final String arch = System.getProperty("os.arch").toLowerCase(); + if (arch.equals("aarch64")) { + classifierBuilder.append("aarch_64"); + } else { + classifierBuilder.append("x86_64"); + } + classifier = classifierBuilder.toString(); + + loadNativeLibrary(); + } + + private static void loadNativeLibrary() { + try { + // try dynamic library - the search path can be configured via "-Djava.library.path" + System.loadLibrary("regorus_java"); + return; + } catch (UnsatisfiedLinkError ignore) { + // ignore - try from classpath + } + + final String libraryPath = bundledLibraryPath(); + try (final InputStream is = Engine.class.getResourceAsStream(libraryPath)) { + if (is == null) { + throw new RuntimeException("cannot find " + libraryPath); + } + final int dot = libraryPath.indexOf('.'); + final File tmpFile = File.createTempFile(libraryPath.substring(0, dot), libraryPath.substring(dot)); + tmpFile.deleteOnExit(); + Files.copy(is, tmpFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + System.load(tmpFile.getAbsolutePath()); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + } + + private static String bundledLibraryPath() { + final String libraryName = System.mapLibraryName("regorus_java"); + return "/native/" + classifier + "/" + libraryName; + } +} diff --git a/bindings/java/src/test/java/com/microsoft/regorus/EngineTest.java b/bindings/java/src/test/java/com/microsoft/regorus/EngineTest.java new file mode 100644 index 0000000..3807e5f --- /dev/null +++ b/bindings/java/src/test/java/com/microsoft/regorus/EngineTest.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + **/ +package com.microsoft.regorus; + +import java.util.Map; +import java.util.ArrayList; +import junit.framework.TestCase; +import junit.framework.Assert; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public class EngineTest extends TestCase +{ + public void test_engine() + { + String resJson; + try (Engine engine = new Engine()) { + engine.pubAddPolicy( + "hello.rego", + "package test\nmessage = concat(\", \", [input.message, data.message])" + ); + engine.pubAddDataJson("{\"message\":\"World!\"}"); + engine.pubSetInputJson("{\"message\":\"Hello\"}"); + resJson = engine.pubEvalQuery("data.test.message"); + } + + Gson gson = new Gson(); + Map res = gson.fromJson(resJson, Map.class); + ArrayList results = (ArrayList) res.get("result"); + ArrayList expressions = (ArrayList) ((Map) results.get(0)).get("expressions"); + Map expression = (Map) expressions.get(0); + Assert.assertEquals("Hello, World!", expression.get("value")); + } +} diff --git a/bindings/java/tools/build.py b/bindings/java/tools/build.py new file mode 100644 index 0000000..4d2eed1 --- /dev/null +++ b/bindings/java/tools/build.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# +# This script adapted from: +# https://github.com/apache/opendal/blob/93e5f65bbf30df2fed4bdd95bb0685c73c6418c2/bindings/java/tools/build.py + +from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser +from pathlib import Path +import shutil +import subprocess + +def classifier_to_target(classifier: str) -> str: + if classifier == 'osx-aarch_64': + return 'aarch64-apple-darwin' + if classifier == 'osx-x86_64': + return 'x86_64-apple-darwin' + if classifier == 'linux-aarch_64': + return 'aarch64-unknown-linux-gnu' + if classifier == 'linux-x86_64': + return 'x86_64-unknown-linux-gnu' + if classifier == 'windows-x86_64': + return 'x86_64-pc-windows-msvc' + raise Exception(f'Unsupported classifier: {classifier}') + +def get_cargo_artifact_name(classifier: str) -> str: + if classifier.startswith('osx'): + return 'libregorus_java.dylib' + if classifier.startswith('linux'): + return 'libregorus_java.so' + if classifier.startswith('windows'): + return 'regorus_java.dll' + raise Exception(f'Unsupported classifier: {classifier}') + + +if __name__ == '__main__': + basedir = Path(__file__).parent.parent + + parser = ArgumentParser() + parser.add_argument('--classifier', type=str, required=True) + args = parser.parse_args() + + target = classifier_to_target(args.classifier) + + # Setup target. + command = ['rustup', 'target', 'add', target] + print('$ ' + subprocess.list2cmdline(command)) + subprocess.run(command, cwd=basedir, check=True) + + cmd = ['cargo', 'build', '--target', target] + + output = basedir / 'target' / 'bindings' + Path(output).mkdir(exist_ok=True, parents=True) + cmd += ['--target-dir', str(output)] + + print('$ ' + subprocess.list2cmdline(cmd)) + subprocess.run(cmd, cwd=basedir, check=True) + + # History reason of cargo profiles. + profile = 'debug' + artifact = get_cargo_artifact_name(args.classifier) + src = output / target / profile / artifact + dst = basedir / 'target' / 'classes' / 'native' / args.classifier / artifact + dst.parent.mkdir(exist_ok=True, parents=True) + shutil.copy2(src, dst)