OPA TypeScript SDK Usage
Installation
npm:
npm add @styra/opa
Yarn:
yarn add @styra/opa zod
Yarn does not install peer dependencies automatically. You will need to install zod
as shown above.
Code Examples
The following examples assume an OPA server equipped with the following Rego policy:
package authz
import rego.v1
default allow := false
allow if input.subject == "alice"
and this data:
{
"roles": {
"admin": ["read", "write"]
}
}
Simple Query
For a simple boolean response without input, use the SDK as follows:
import { OPAClient } from "@styra/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL);
const path = "authz/allow";
const allowed = await opa.evaluate(path);
console.log(allowed ? "allowed!" : "denied!");
Default Rule
For evaluating the default rule (configured with your OPA service), use evaluateDefault
. input
is optional, and left out in this example:
import { OPAClient } from "@styra/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL);
const allowed = await opa.evaluateDefault();
console.log(allowed ? "allowed!" : "denied!");
Input
Input is provided as a second (optional) argument to evaluate
:
import { OPAClient } from "@styra/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL);
const path = "authz/allow";
const input = { subject: "alice" };
const allowed = await opa.evaluate(path, input);
console.log(allowed ? "allowed!" : "denied!");
Default Rule with Input
Input is provided as an (optional) argument to evaluateDefault
:
import { OPAClient } from "@styra/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL);
const input = { subject: "alice" };
const allowed = await opa.evaluateDefault(input);
console.log(allowed ? "allowed!" : "denied!");
Everything that follows applies in the same way to evaluateDefault
and evaluate
.
Input and Result Types
It's possible to provide your own types for input and results.
The evaluate
function will then return a typed result, and TypeScript will ensure that you pass the proper types (as declared) to evaluated
.
import { OPAClient } from "@styra/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL);
const path = "authz";
interface myInput {
subject: string;
}
interface myResult {
allow: boolean;
}
const input: myInput = { subject: "alice" };
const result = await opa.evaluate<myInput, myResult>(path, input);
console.log(result);
If you pass in an arbitrary object as input, it'll be stringified (JSON.stringify
):
import { OPAClient } from "@styra/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL);
const path = "authz/allow";
class User {
subject: string;
constructor(name: string) {
this.subject = name;
}
}
const inp = new User("alice");
const allowed = await opa.evaluate<User, boolean>(path, inp);
console.log(allowed);
You can control the input that's constructed from an object by implementing ToInput
:
import { OPAClient, ToInput } from "@styra/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL);
const path = "authz/allow";
class User implements ToInput {
private n: string;
constructor(name: string) {
this.n = name;
}
toInput(): Input {
return { subject: this.n };
}
}
const inp = new User("alice");
const allowed = await opa.evaluate<User, boolean>(path, inp);
console.log(allowed);
Result Transformations
If the result format of the policy evaluation does not match what you want it to be, you can provide a third argument, a function that transforms the API result.
Assuming that the policy evaluates to
{
"allowed": true,
"details": ["input.a is OK", "input.b is OK"]
}
like this (contrived) example:
package authz
import rego.v1
good_a := ["a", "A", "A!"]
good_b := ["b"]
response.allowed if input.subject == "alice"
response.details contains "input.a is OK" if input.a in good_a
response.details contains "input.b is OK" if input.b in good_b
you can turn it into a boolean result like this:
import { OPAClient } from "@styra/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL);
const path = "authz/response";
const input = { subject: "alice", a: "A", b: "b" };
const allowed = await opa.evaluate<any, boolean>(
path,
input,
{
fromResult: (r?: Result) => (r as Record<string, any>)["allowed"] ?? false,
},
);
console.log(allowed);
Batched Queries
import { OPAClient } from "@styra/opa";
const serverURL = "http://localhost:8181";
const path = "authz/allow";
const opa = new OPAClient(serverURL);
const alice = { subject: "alice" };
const bob = { subject: "bob" };
const inputs = { alice: alice, bob: bob };
const responses = await opa.evaluateBatch(path, inputs);
for (const key in responses) {
console.log(key + ": " + (responses[key] ? "allowed!" : "denied!")); // Logic here
}
Result
alice: allowed!
bob: denied!
Advanced options
Request Headers
You can provide your custom headers -- for example for bearer authorization -- via an option argument to the OPAClient
constructor.
import { OPAClient } from "@styra/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL, { headers: { authorization: "Bearer opensesame" } });
const path = "authz/allow";
const allowed = await opa.evaluate(path);
console.log(allowed);
HTTPClient
You can supply an instance of HTTPClient
to supply your own hooks, for example to examine the request sent to OPA:
import { OPAClient } from "@styra/opa";
import { HTTPClient } from "@styra/opa/lib/http";
const httpClient = new HTTPClient({});
httpClient.addHook("response", (response, request) => {
console.group("Request Debugging");
console.log(request.headers);
console.log(`${request.method} ${request.url} => ${response.status} ${response.statusText}`);
console.groupEnd();
});
const serverURL = "http://localhost:8181";
const headers = { authorization: "Bearer opensesame" };
const opa = new OPAClient(serverURL, { sdk: { httpClient }, headers });
const path = "authz/allow";
const allowed = await opa.evaluate(path);
console.log(allowed);