OPA Java Tutorial: Hello World using gRPC
Enterprise OPA offers a gRPC API for application use cases demanding low-latency policy decisions. This tutorial will demonstrate how to build an integration against the gPRC API to make fast policy decisions from a Java application.
gRPC messages are serialized using Protocol Buffer definitions, as well as controlling the structure of messages they can also be used to generate SDKs to make integration easier in different languages - including Java.
To use the Enterprise OPA gRPC API from Java, we will make use of the SDKs generated from the Protobuf Schema published on Buf rather than the OPA Java SDK.
The gRPC API is only available in Enterprise OPA. If using open source OPA, or interested in integrating with the REST API instead, see the OPA Java Tutorial: Hello World.
Setup
First ensure you have installed the Enterprise OPA binary and have a valid key.
To evaluate Enterprise OPA, you can obtain a trial license by
Downloading eopa
and running
eopa license trial
in the terminal.
Next, in a new directory, run gradle init
. The prompts should be responded to as follows:
Select type of build to generate:
ApplicationSelect implementation language:
JavaEnter target Java version:
21Project name:
docsampleSelect application structure:
Single application projectSelect build script DSL:
GroovySelect test framework:
JUnit JupiterGenerate build using new APIs and behavior:
no
Gradle should print a message like the one below if everything went right.
BUILD SUCCESSFUL in 3m 56s
1 actionable task: 1 executed
Before proceeding, verify that the project Gradle created for us is able to build and run. The output should look similar to the below.
./gradlew run
> Task :app:run
Hello World!
BUILD SUCCESSFUL in 627ms
2 actionable tasks: 2 executed
Adding the gRPC SDK and dependencies
The app/build.gradle
file must also be updated to pull in the Enterprise OPA Java gRPC SDK and necessary gRPC dependencies.
Under the repositories
section, add the following to pull dependencies from buf.build:
maven {
name = 'buf'
url 'https://buf.build/gen/maven'
}
Under the dependencies
section, add the following:
implementation("build.buf.gen:styra_enterprise-opa_grpc_java:+")
implementation("build.buf.gen:styra_enterprise-opa_protocolbuffers_java:+")
implementation("com.google.protobuf:protobuf-java-util:+")
implementation("io.grpc:grpc-okhttp:+")
Using the version string +
causes Gradle to pull in the latest version of that dependency. In a production environment, it may be preferable to pin a specific version of your dependencies instead.
Adding the OPA Java gRPC SDK
Next, modify the Java code to utilize the OPA Java gRPC SDK in app/src/main/java/org/example/App.java
/*
* This source file was generated by the Gradle 'init' task
*/
package org.example;
import com.google.protobuf.util.JsonFormat;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Struct;
import build.buf.gen.eopa.data.v1.*;
import build.buf.gen.eopa.data.v1.DataServiceGrpc.DataServiceBlockingStub;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.concurrent.TimeUnit;
public class App {
public String getGreeting() {
return "Hello World!";
}
public static void main(String[] args) throws InvalidProtocolBufferException, InterruptedException {
System.out.println(new App().getGreeting());
String jsonString = """
{
"user": "bob",
"action": "read",
"resource": "dog123"
}
""";
Struct.Builder structBuilder = Struct.newBuilder();
JsonFormat.parser().ignoringUnknownFields().merge(jsonString, structBuilder);
Struct input = (Struct) structBuilder.build();
InputDocument inputDocument = InputDocument.newBuilder().setDocument(input).build();
GetDataResponse response;
GetDataRequest request = GetDataRequest.newBuilder().setPath("app/abac").setInput(inputDocument).build();
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 9090).usePlaintext().build();
DataServiceBlockingStub stub = DataServiceGrpc.newBlockingStub(channel);
try {
System.out.println("Making request");
response = stub.getData(request);
System.out.println(response.getResult());
} catch (Exception e) {
System.out.println("RPC failed: " + e);
return;
} finally {
channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
}
boolean allowed = response.getResult().getDocument().getStructValue().getFieldsMap().get("allow").getBoolValue();
System.out.println("Allowed: " + allowed);
}
}
Creating policies
The sample application is almost ready to run, but first an Enterprise OPA instance is needed for the gRPC SDK to communicate with. We will be using the ABAC example policy from the OPA Playground.
Create a policy/
folder and the following two files:
policy/app/abac.rego
# Attribute-based Access Control (ABAC)
# -------------------------------------
#
# This example implements ABAC for a Pet Store API. The Pet Store API allows
# users to look at pets, adopt them, update their stats, and so on. The policy
# controls which users can perform actions on which resources. The policy implements
# a Attribute-based Access Control model where users, resources, and actions have
# attributes and the policy makes decisions based on those attributes.
#
# This example shows how to:
#
# * Implement ABAC using Rego that leverages external data.
# * Define helper rules that provide useful abstractions (e.g., `user_is_senior`).
#
# For more information see:
#
# * Rego comparison to other systems: https://www.openpolicyagent.org/docs/latest/comparison-to-other-systems/
#
# Hint: The Coverage feature lets you view the policy statements that were executed
# when the policy was last evaluated. Try enabling Coverage and running evaluation
# with different inputs.
package app.abac
import rego.v1
default allow := false
allow if user_is_owner
allow if {
user_is_employee
action_is_read
}
allow if {
user_is_employee
user_is_senior
action_is_update
}
allow if {
user_is_customer
action_is_read
not pet_is_adopted
}
user_is_owner if data.user_attributes[input.user].title == "owner"
user_is_employee if data.user_attributes[input.user].title == "employee"
user_is_customer if data.user_attributes[input.user].title == "customer"
user_is_senior if data.user_attributes[input.user].tenure > 8
action_is_read if input.action == "read"
action_is_update if input.action == "update"
pet_is_adopted if data.pet_attributes[input.resource].adopted == true
policy/data.json
{
"user_attributes": {
"alice": {
"tenure": 20,
"title": "owner"
},
"bob": {
"tenure": 15,
"title": "employee"
},
"eve": {
"tenure": 5,
"title": "employee"
},
"dave": {
"tenure": 5,
"title": "customer"
}
},
"pet_attributes": {
"dog123": {
"adopted": true,
"age": 2,
"breed": "terrier",
"name": "toto"
},
"dog456": {
"adopted": false,
"age": 3,
"breed": "german-shepherd",
"name": "rintintin"
},
"dog789": {
"adopted": false,
"age": 2,
"breed": "collie",
"name": "lassie"
},
"cat123": {
"adopted": false,
"age": 1,
"breed": "fictitious",
"name": "cheshire"
}
}
}
Running Enterprise OPA
Next create a configuration file for your Enterprise OPA instance that enables gRPC.
config.yaml
plugins:
grpc:
addr:
localhost:9090
Next, run Enterprise OPA. You should see the following output:
eopa run --server --config-file ./config.yaml ./policy
{"addrs":["localhost:8181"],"diagnostic-addrs":[],"level":"info","msg":"Initializing server.","time":"2024-07-11T14:36:34-07:00"}
{"level":"info","msg":"Starting gRPC server on port: localhost:9090","time":"2024-07-11T14:36:34-07:00"}
If you see errors related to not having a license key, follow any instructions to resolve.
Consider running OPA in a separate background terminal, as it will be necessary to run more shell commands.
Verify that the data was loaded into Enterprise OPA correctly using the HTTP API, which is exposed by default on localhost:8181
:
curl -Ss http://localhost:8181/v1/data/user_attributes/alice\?pretty\=true
{
"result": {
"tenure": 20,
"title": "owner"
}
}
Verify that the policy is working as expected using the HTTP API:
curl -Ss http://localhost:8181/v1/data/app/abac/allow -X POST -d '{"input": {"user": "bob", "action": "read", "resource": "dog123"}}'
{"result":true}
curl -Ss http://localhost:8181/v1/data/app/abac/allow -X POST -d '{"input": {"user": "dave", "action": "read", "resource": "dog123"}}'
{"result":false}
Running the application
Finally, run the Java program:
./gradlew run
> Task :app:run
Hello World!
Making request
path: "app/abac"
document {
struct_value {
fields {
key: "action_is_read"
value {
bool_value: true
}
}
fields {
key: "allow"
value {
bool_value: true
}
}
fields {
key: "pet_is_adopted"
value {
bool_value: true
}
}
fields {
key: "user_is_employee"
value {
bool_value: true
}
}
fields {
key: "user_is_senior"
value {
bool_value: true
}
}
}
}
Allowed: true
BUILD SUCCESSFUL in 1s
2 actionable tasks: 1 executed, 1 up-to-date
Refactoring the code
We can explore a bit how the Java gRPC SDK behaves by changing App.java
:
GetDataRequest request = GetDataRequest.newBuilder().setPath("app/abac").setInput(inputDocument).build();
GetDataRequest request = GetDataRequest.newBuilder().setPath("app/abac/allow").setInput(inputDocument).build();
...
boolean allowed = response.getResult().getDocument().getStructValue().getFieldsMap().get("allow").getBoolValue();
boolean allowed = response.getResult().getDocument().getBoolValue();
Re-running the application should return the following result:
./gradlew run
> Task :app:run
Hello World!
Making request
path: "app/abac/allow"
document {
bool_value: true
}
Allowed: true
BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed