Skip to content

StyraInc/opa-typescript

Repository files navigation

OPA Typescript SDK

The Styra-supported driver to connect to Open Policy Agent (OPA) and Enterprise OPA deployments.

License NPM Version

The documentation for this SDK lives at https://docs.styra.com/sdk, with reference documentation available at https://styrainc.github.io/opa-typescript

You can use the Styra OPA SDK to connect to Open Policy Agent and Enterprise OPA deployments.

SDK Installation

NPM

npm add @styra/opa

Yarn

yarn add @styra/opa

Requirements

For supported JavaScript runtimes, please consult RUNTIMES.md.

SDK Example Usage (high-level)

All the code examples that follow assume that the high-level SDK module has been imported, and that an OPA instance was created:

import { OPAClient } from "@styra/opa";

const serverURL = "http://opa-host:8181";
const path = "authz/allow";
const opa = new OPAClient(serverURL);

Simple query

For a simple boolean response without input, use the SDK as follows:

const allowed = await opa.evaluate(path);
console.log(allowed ? "allowed!" : "denied!");

Note that allowed will be of type any. You can change that by providing type parameters to evaluate:

const allowed = await opa.evaluate<never, boolean>(path);

The first parameter is the type of input passed into evaluate; we don't have any in this example, so you can use anything for it (any, unknown, or never).

HTTP Request
POST /v1/data/authz/allow
Content-Type: application/json

{}

Input

Input is provided as a second (optional) argument to evaluate:

const input = { user: "alice" };
const allowed = await opa.evaluate(path, input);
console.log(allowed ? "allowed!" : "denied!");

For providing types, use

interface myInput {
  user: string;
}
const input: myInput = { user: "alice" };
const allowed = await opa.evaluate<myInput, boolean>(path, input);
console.log(allowed ? "allowed!" : "denied!");
HTTP Request
POST /v1/data/authz/allow
Content-Type: application/json

{ "input": { "user": "alice" } }

Result Types

When the result of the policy evaluation is more complex, you can pass its type to evaluate and get a typed result:

interface myInput {
  user: string;
}
interface myResult {
  authorized: boolean;
  details: string[];
}
const input: myInput = { user: "alice" };
const result = await opa.evaluate<myInput, myResult>(path, input);
console.log(result.evaluated ? "allowed!" : "denied!");

Input Transformations

If you pass in an arbitrary object as input, it'll be stringified (JSON.stringify):

class A {
  // With these names, JSON.stringify() returns the right thing.
  name: string;
  list: any[];

  constructor(name: string, list: any[]) {
    this.name = name;
    this.list = list;
  }
}
const inp = new A("alice", [1, 2, true]);
const allowed = await opa.evaluate<myInput, boolean>(path, inp);
console.log(allowed ? "allowed!" : "denied!");

You can control the input that's constructed from an object by implementing ToInput:

class A implements ToInput {
  // With these names, JSON.stringify() doesn't return the right thing.
  private n: string;
  private l: any[];

  constructor(name: string, list: any[]) {
    this.n = name;
    this.l = list;
  }

  toInput(): Input {
    return { name: this.n, list: this.l };
  }
}
const inp = new A("alice", [1, 2, true]);
const allowed = await opa.evaluate<myInput, boolean>(path, inp);
console.log(allowed ? "allowed!" : "denied!");
HTTP Request
POST /v1/data/authz/allow
Content-Type: application/json

{ "input": { "name": "alice", "list": [ 1, 2, true ] } }

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": ["property-a is OK", "property-B is OK"]
}

you can turn it into a boolean result like this:

const allowed = await opa.evaluate<any, boolean>(
  path,
  undefined,
  (r?: Result) => (r as Record<string, any>)["allowed"] ?? false,
);
console.log(allowed ? "allowed!" : "denied!");

Example Projects

Express

In the StyraInc/styra-demo-tickethub repository, you'll find a NodeJS backend service that is using @styra/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));
});

NestJS

In StyraInc/opa-typescript-example-nestjs, we have an decorator-based API authorization example using @styra/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.

Note: For low-level SDK usage, see the sections below.


OPA OpenAPI SDK (low-level)

Available Resources and Operations

Development

Maturity

This SDK is in beta, and there may be breaking changes between versions without a major version update. Therefore, we recommend pinning usage to a specific package version. This way, you can install the same version each time without breaking changes unless you are intentionally looking for the latest version.