Integrate with Rust¶
Preface¶
This guide will explain how to delegate authorization decisions to OPA from a Rust application via the OPA REST API.
Note
This guide assumes you have deployed an OPA instance with a system package as described in the policy writing guide - see the Helm or docker-compose deployment guide for instructions on OPA deployment.
Dependencies¶
We will use the following dependencies:
tokio
to provide an async runtime - with themacros
feature allowing us to easily async-ify the main functionserde
to provide struct and enum (de)serialization - with thederive
feature allowing us to derive theSerialize
andDeserialize
traits for our enums and structs.reqwest
as our HTTP client - with thejson
feature allowing us to easily serialize and deserialize the HTTP bodies usingserde
.
Example
[dependencies]
reqwest = { version = "0.11.27", features = ["json"] }
serde = { version = "1.0.197", features = ["derive"] }
tokio = { version = "1.37.0", features = ["macros"] }
Runtime¶
In this guide, we will use a tokio runtime, this can be created using the #[tokio::main]
macro on an asynchronous main function. The "current_thread"
flavour specifies that a single-threaded runtime will be created, omission will make this multi-threaded, but requires the tokio
rt-multi-thread
feature.
#[tokio::main(flavor = "current_thread")]
async fn main() {}
Serializing Input Data¶
OPA expects a JSON object as it's input, with the exact fields depending on the policy being involked - we will assume our policy requires a subject
name, an action
which is either "read"
or "write"
and an item_id
. This can be therefore represented as the struct Input
, which consists of the required fields - where the action
is represented by the Action
enum. The serde::Serialize
derive macro is used to implement this trivial serialization strategy.
#[derive(Debug, serde::Serialize)]
enum Action {
#[serde(rename = "read")]
Read,
#[serde(rename = "write")]
Write,
}
#[derive(Debug, serde::Serialize)]
struct Input {
subject: String,
action: Action,
item_id: u32,
}
We can now create an instance of this input as so:
let input = Input {
subject: "bob".to_string(),
action: Action::Read,
item_id: 42,
};
Making the Request¶
We will use reqwest
to POST
to the opa root path - shown henceforth as http://opa:8181/
. To do this we create a reqwest::Client
and call the post
method with the OPA root query URL; we will pass the input as json
before send
ing and asynchronously await
ing the response. We will unwind the stack if an error is encountered using unwrap
.
let client = reqwest::Client::new();
let response = client
.post("http://opa:8181/")
.json(&input)
.send()
.await
.unwrap();
Interpreting the Decision¶
OPA returns a decision as a JSON object, with the exact fields depending on the policy being involked - we will assume our policy returns only an allow
boolean. This can therefore be represented as the struct Decision
, which contains the allow
field. The serde::Deserialize
derive macro is used to implement this trivial deserialization strategy.
#[derive(Debug, serde::Deserialize)]
struct Decision {
allow: bool,
}
We can now deserialize the response of OPA using the json
method on the response with the target type:
let decision = response.json::<Decision>().await.unwrap();
Finally, we can access the allow
field of the decision
and print it to stdout:
println!("Allowed: {}", decision.allow);
Complete Code
#[derive(Debug, serde::Serialize)]
enum Action {
#[serde(rename = "read")]
Read,
#[serde(rename = "write")]
Write,
}
#[derive(Debug, serde::Serialize)]
struct Input {
subject: String,
action: Action,
item_id: u32,
}
#[derive(Debug, serde::Deserialize)]
struct Decision {
allow: bool,
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let input = Input {
subject: "bob".to_string(),
action: Action::Read,
item_id: 42,
};
let client = reqwest::Client::new();
let response = client
.post("http://opa:8181/")
.json(&input)
.send()
.await
.unwrap();
let decision = response.json::<Decision>().await.unwrap();
println!("Allowed: {}", decision.allow);
}