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:
tokioto provide an async runtime - with themacrosfeature allowing us to easily async-ify the main functionserdeto provide struct and enum (de)serialization - with thederivefeature allowing us to derive theSerializeandDeserializetraits for our enums and structs.reqwestas our HTTP client - with thejsonfeature 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 sending and asynchronously awaiting 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);
}