Skip to content
This repository was archived by the owner on Jul 1, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions src/porcelain/porcelain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
ExecutePolicyWithInputResponse,
ExecutePolicyResponse,
} from "../models/operations";
import { SDKOptions } from "../lib/config";
import { HTTPClient } from "../lib/http";

export type { Input, Result };

Expand All @@ -20,15 +22,38 @@ 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<string, string>;
sdk?: SDKOptions;
};

/** OPAClient is the starting point for using the high-level API.
*
* Use {@link Opa} if you need some low-level customization.
*/
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.
Expand Down
50 changes: 49 additions & 1 deletion tests/authorizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Policy PUT calls happening later in the test setup.

allow if input.path[0] == "health"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Health check, needed for wait strategy

allow if input.path[2] == "test"
allow if input.path[2] == "has"
Comment on lines +40 to +41
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing tests' prefixes

allow if {
input.path[2] = "token"
input.identity = "opensesame"
}
Comment on lines +42 to +45
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New test: requests for /v1/data/token/* only work with a proper authorization header (bearer token)

`;

let container: StartedTestContainer;
let serverURL: string;
Expand All @@ -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",
},
])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The system authz policy needs to be loaded when the server is started

.start();
serverURL = `http://${container.getHost()}:${container.getMappedPort(8181)}`;

Expand Down Expand Up @@ -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";

Check failure

Code scanning / CodeQL

Hard-coded credentials

The hard-coded value "Bearer opensesame" is used as [authorization header](1).
const res = await new OPAClient(serverURL, {
headers: { authorization },
}).authorize("token/p");
assert.strictEqual(res, true);
});

after(async () => await container.stop());
});