From 954bfae376c98c72fdd82214a14656038f07f125 Mon Sep 17 00:00:00 2001 From: Stephan Renatus Date: Thu, 11 Apr 2024 12:00:15 +0200 Subject: [PATCH] porcelain: allow optional headers and low-level SDKOptions Including a test for each of them. Signed-off-by: Stephan Renatus --- src/porcelain/porcelain.ts | 29 ++++++++++++++++++++-- tests/authorizer.test.ts | 50 +++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/porcelain/porcelain.ts b/src/porcelain/porcelain.ts index 395c039..6f01ef0 100644 --- a/src/porcelain/porcelain.ts +++ b/src/porcelain/porcelain.ts @@ -4,6 +4,8 @@ import { ExecutePolicyWithInputResponse, ExecutePolicyResponse, } from "../models/operations"; +import { SDKOptions } from "../lib/config"; +import { HTTPClient } from "../lib/http"; export type { Input, Result }; @@ -20,6 +22,13 @@ function implementsToInput(object: any): object is ToInput { return u.toInput !== undefined && typeof u.toInput == "function"; } +/** Extra options for using the high-level SDK. + */ +export type Options = { + headers?: Record; + sdk?: SDKOptions; +}; + /** OPAClient is the starting point for using the high-level API. * * Use {@link Opa} if you need some low-level customization. @@ -27,8 +36,24 @@ function implementsToInput(object: any): object is ToInput { export class OPAClient { private opa: Opa; - constructor(serverURL: string) { - this.opa = new Opa({ serverURL }); + /** Create a new `OPA` instance. + * @param serverURL - The OPA URL, e.g. `https://opa.internal.corp:8443/`. + * @param opts - Extra options, ncluding low-level `SDKOptions`. + */ + constructor(serverURL: string, opts?: Options) { + const sdk = { serverURL, ...opts?.sdk }; + if (opts?.headers) { + const hdrs = opts.headers; + const client = opts?.sdk?.httpClient ?? new HTTPClient(); + client.addHook("beforeRequest", (req) => { + for (const k in hdrs) { + req.headers.set(k, hdrs[k] as string); + } + return req; + }); + sdk.httpClient = client; + } + this.opa = new Opa(sdk); } /** `authorize` is used to evaluate the policy at the specified. diff --git a/tests/authorizer.test.ts b/tests/authorizer.test.ts index 99517d0..d5a0564 100644 --- a/tests/authorizer.test.ts +++ b/tests/authorizer.test.ts @@ -2,6 +2,7 @@ import { describe, before, after, it } from "node:test"; import assert from "node:assert"; import { GenericContainer, StartedTestContainer, Wait } from "testcontainers"; import { OPAClient, ToInput, Input, Result } from "../src/porcelain"; +import { HTTPClient } from "../src/lib/http"; // Run these locally, with debug output from testcontainers, like this: // DEBUG='testcontainers*' node --require ts-node/register --test tests/**/*.ts @@ -24,9 +25,25 @@ compound_result.allowed := true slash: `package has["weird/package"].but import rego.v1 -it_is := true +it_is := true`, + token: `package token +import rego.v1 +p := true `, }; + const authzPolicy = `package system.authz +import rego.v1 + +default allow := false +allow if input.method == "PUT" +allow if input.path[0] == "health" +allow if input.path[2] == "test" +allow if input.path[2] == "has" +allow if { + input.path[2] = "token" + input.identity = "opensesame" +} +`; let container: StartedTestContainer; let serverURL: string; @@ -37,9 +54,18 @@ it_is := true "--server", "--disable-telemetry", "--log-level=debug", + "--authentication=token", + "--authorization=basic", + "/authz.rego", ]) .withExposedPorts(8181) .withWaitStrategy(Wait.forHttp("/health", 8181).forStatusCode(200)) + .withCopyContentToContainer([ + { + content: authzPolicy, + target: "/authz.rego", + }, + ]) .start(); serverURL = `http://${container.getHost()}:${container.getMappedPort(8181)}`; @@ -160,5 +186,27 @@ it_is := true assert.deepStrictEqual(res, true); }); + it("allows custom low-level SDKOptions' HTTPClient", async () => { + const httpClient = new HTTPClient({}); + let called = false; + httpClient.addHook("beforeRequest", (req) => { + called = true; + return req; + }); + const res = await new OPAClient(serverURL, { + sdk: { httpClient }, + }).authorize("test/p_bool"); + assert.strictEqual(res, true); + assert.strictEqual(called, true); + }); + + it("allows custom headers", async () => { + const authorization = "Bearer opensesame"; + const res = await new OPAClient(serverURL, { + headers: { authorization }, + }).authorize("token/p"); + assert.strictEqual(res, true); + }); + after(async () => await container.stop()); });