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!
Get Filters
To use the translation of Rego data filter policies into SQL or UCAST expressions, you need to use Enterprise OPA. These examples assume you run Enterprise OPA with the following Rego policy:
package filters
# METADATA
# scope: document
# custom:
# unknowns: ["input.fruits"]
# mask_rule: masks
include if input.fruits.colour in input.fav_colours
masks.fruits.supplier.replace.value := "<supplier>"
You can more information in Data Filtering.
For Prisma
import { OPAClient } from "@styra/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL);
const path = "filters/include";
const input = { fav_colours: ["red", "green"] };
const primary = "fruits";
const { query, mask } = await opa.getFilters(path, input, primary);
console.log(query);
Here, query
is an object that can readly be used in a Prisma lookup's where
field:
{ colour: { in: [ "red", "green" ] } }
mask
is a function that can be applied to the values returned by that lookup.
For example:
const { query, mask } = await opa.getFilters(path, input, primary);
const fruits = (
await prisma.fruits.findMany({
where: query,
})
).map((fruit) => mask(fruit));
For SQL
import { OPAClient } from "@styra/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL);
const path = "filters/include";
const input = { fav_colours: ["red", "green"] };
const opts = { target: "postgresql" };
const { query, masks } = await opa.getFilters(path, input, opts);
console.log({ query, masks });
Here we get a SQL WHERE clause as query
,
WHERE fruits.colour IN (E'red', E'green')
and masks
contains the evaluated mask rule:
{ fruits: { supplier: { replace: { value: "<supplier>" } } } }
Table name mappings
Generate a SQL filter with different column and table names via tableMappings
:
const opts = {
target: "postgresql",
tableMappings: {
"fruits": { $self: "f", colour: "col"}
}
};
this will generate the SQL clause
WHERE f.col IN (E'red', E'green')
For multiple data sources
import { OPAClient } from "@styra/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL);
const path = "filters/include";
const input = { fav_colours: ["red", "green"] };
const opts = { targets: ["postgresql", "mysql", "ucastPrisma"] };
const result = await opa.getMultipleFilters(path, input, opts);
console.dir(result, {depth: null});
This produces an object keyed by the requested targets:
{
ucast: {
query: {
type: "field",
operator: "in",
field: "fruits.colour",
value: [ "red", "green" ]
},
masks: {
fruits: { supplier: { replace: { value: "<supplier>" } } }
}
},
postgresql: {
query: "WHERE fruits.colour IN (E'red', E'green')",
masks: {
fruits: { supplier: { replace: { value: "<supplier>" } } }
}
},
mysql: {
query: "WHERE fruits.colour IN ('red', 'green')",
masks: {
fruits: { supplier: { replace: { value: "<supplier>" } } }
}
}
}
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);