Driver to connect to Open Policy Agent (OPA) and EOPA deployments.
Reference documentation available at https://open-policy-agent.github.io/opa-typescript
You can use the OPA SDK to connect to Open Policy Agent and EOPA deployments.
npm add @open-policy-agent/opa
pnpm add @open-policy-agent/opa
bun add @open-policy-agent/opa
yarn add @open-policy-agent/opa zod
# Note that Yarn does not install peer dependencies automatically. You will need
# to install zod as shown above.
For more information about the API: Enterprise OPA documentation
For supported JavaScript runtimes, please consult RUNTIMES.md.
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"]
}
}
For a simple boolean response without input, use the SDK as follows:
import { OPAClient } from "@open-policy-agent/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!");
For evaluating the default rule (configured with your OPA service), use evaluateDefault
. input
is optional, and left out in this example:
import { OPAClient } from "@open-policy-agent/opa";
const serverURL = "http://localhost:8181";
const opa = new OPAClient(serverURL);
const allowed = await opa.evaluateDefault();
console.log(allowed ? "allowed!" : "denied!");
Input is provided as a second (optional) argument to evaluate
:
import { OPAClient } from "@open-policy-agent/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!");
Input is provided as an (optional) argument to evaluateDefault
:
import { OPAClient } from "@open-policy-agent/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
.
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 "@open-policy-agent/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 "@open-policy-agent/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 "@open-policy-agent/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);
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 "@open-policy-agent/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);
import { OPAClient } from "@open-policy-agent/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
}
alice: allowed!
bob: denied!
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 := ""
import { OPAClient } from "@open-policy-agent/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));
import { OPAClient } from "@open-policy-agent/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>" } } } }
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')
import { OPAClient } from "@open-policy-agent/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>" } } }
}
}
}
You can provide your custom headers -- for example for bearer authorization -- via an option argument to the OPAClient
constructor.
import { OPAClient } from "@open-policy-agent/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);
You can supply an instance of HTTPClient
to supply your own hooks, for example to examine the request sent to OPA:
import { OPAClient } from "@open-policy-agent/opa";
import { HTTPClient } from "@open-policy-agent/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);
In the StyraOSS/styra-demo-tickethub repository, you'll find a NodeJS backend service that is using @open-policy-agent/opa
:
router.get("/tickets/:id", [param("id").isInt().toInt()], async (req, res) => {
const {
params: { id },
} = req;
await authz.evaluated(path, { action: "get", id }, req);
const ticket = await prisma.tickets.findUniqueOrThrow({
where: { id },
...includeCustomers,
});
return res.status(OK).json(toTicket(ticket));
});
In StyraInc/opa-sdk-demos/nestjs-demo, we have an decorator-based API authorization example using @open-policy-agent/opa
:
@Controller("cats")
@AuthzQuery("cats/allow")
@AuthzStatic({ resource: "cat" })
export class CatsController {
constructor(private catsService: CatsService) {}
@Post()
@Authz(({ body: { name } }) => ({ name, action: "create" }))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get(":name")
@AuthzQuery("cats") // For illustration, we're querying the package extent
@Decision((r) => r.allow)
@Authz(({ params: { name } }) => ({
name,
action: "get",
}))
async findByName(@Param("name") name: string): Promise<Cat> {
return this.catsService.findByName(name);
}
}
Please refer to the repository's README.md for more details.
For low-level SDK usage, see the sections below.
Some of the endpoints in this SDK support retries. If you use the SDK without any configuration, it will fall back to the default retry strategy provided by the API. However, the default retry strategy can be overridden on a per-operation basis, or across the entire SDK.
To change the default retry strategy for a single API call, simply provide a retryConfig object to the call:
import { OpaApiClient } from "@open-policy-agent/opa";
const opaApiClient = new OpaApiClient();
async function run() {
const result = await opaApiClient.executeDefaultPolicyWithInput(4963.69, {
retries: {
strategy: "backoff",
backoff: {
initialInterval: 1,
maxInterval: 50,
exponent: 1.1,
maxElapsedTime: 100,
},
retryConnectionErrors: false,
},
});
// Handle the result
console.log(result);
}
run();
If you'd like to override the default retry strategy for all operations that support retries, you can provide a retryConfig at SDK initialization:
import { OpaApiClient } from "@open-policy-agent/opa";
const opaApiClient = new OpaApiClient({
retryConfig: {
strategy: "backoff",
backoff: {
initialInterval: 1,
maxInterval: 50,
exponent: 1.1,
maxElapsedTime: 100,
},
retryConnectionErrors: false,
},
});
async function run() {
const result = await opaApiClient.executeDefaultPolicyWithInput(4963.69);
// Handle the result
console.log(result);
}
run();
This SDK supports the following security scheme globally:
Name | Type | Scheme |
---|---|---|
bearerAuth |
http | HTTP Bearer |
To authenticate with the API the bearerAuth
parameter must be set when initializing the SDK client instance. For example:
import { OpaApiClient } from "@open-policy-agent/opa";
const opaApiClient = new OpaApiClient({
bearerAuth: "<YOUR_BEARER_TOKEN_HERE>",
});
async function run() {
const result = await opaApiClient.executeDefaultPolicyWithInput(4963.69);
// Handle the result
console.log(result);
}
run();
You can setup your SDK to emit debug logs for SDK requests and responses.
You can pass a logger that matches console
's interface as an SDK option.
Beware that debug logging will reveal secrets, like API tokens in headers, in log messages printed to a console or files. It's recommended to use this feature only during local development and not in production.
import { OpaApiClient } from "@open-policy-agent/opa";
const sdk = new OpaApiClient({ debugLogger: console });
All the methods listed above are available as standalone functions. These functions are ideal for use in applications running in the browser, serverless runtimes or other environments where application bundle size is a primary concern. When using a bundler to build your application, all unused functionality will be either excluded from the final bundle or tree-shaken away.
To read more about standalone functions, check FUNCTIONS.md.
compileQueryWithPartialEvaluation
- Partially evaluate a queryexecuteBatchPolicyWithInput
- Execute a policy given a batch of inputsexecuteDefaultPolicyWithInput
- Execute the default decision given an inputexecutePolicy
- Execute a policyexecutePolicyWithInput
- Execute a policy given an inputhealth
- Verify the server is operationalFor questions, discussions and announcements related to Styra products, services and open source projects, please join the Styra community on Slack!