Skip to content
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
63 changes: 43 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,27 @@ docker run -d \
The proxy is configured via environment variables.
The following environment variables are supported:

| Name | Description | Default | Note |
| ----------------------------- | ------------------------------------------------------------------------------------ | ----------- | ------------------------------------------ |
| `APP_PORT` | Port to listen on. | `8080` | |
| `APP_BASE_PATH` | Base path fo the proxy. | `/` | |
| `APP_POLICY` | Policy to use, either `builtin` or `custom`. | `builtin` | |
| `APP_POLICY_TYPE` | Type of the builtin policy to use, either `allow_all` or `allow_org_wide`. | `allow_all` | |
| `APP_POLICY_CONFIG` | Config to pass to the policy evaluation. Format: `key=value;key=value` | | |
| `APP_POLICY_PATH` | Path to the custom policy (_.wasm_). | | |
| `APP_GH_AUTH_TYPE` | Type of the GitHub authentication to use, either `app` or `token`. | | **Required** |
| `APP_GH_AUTH_APP_ID` | ID of the GitHub App to use for authentication. (Only for `app` auth type.) | | Required if `APP_GH_AUTH_TYPE` is `app`. |
| `APP_GH_AUTH_APP_PRIVATE_KEY` | Private key of the GitHub App to use for authentication. (Only for `app` auth type.) | | Required if `APP_GH_AUTH_TYPE` is `app`. |
| `APP_GH_AUTH_TOKEN` | Token to use for authentication. (Only for `token` auth type.) | | Required if `APP_GH_AUTH_TYPE` is `token`. |
| Name | Description | Default | Note |
| ----------------------------- | ------------------------------------------------------------------------------------ | ----------- | ---------------------------------------------------- |
| `APP_PORT` | Port to listen on. | `8080` | |
| `APP_BASE_PATH` | Base path fo the proxy. | `/` | |
| `APP_POLICY` | Policy to use, either `builtin`, `opa-wasm` or `cel`. | `builtin` | |
| `APP_POLICY_TYPE` | Type of the builtin policy to use, either `allow_all` or `allow_org_wide`. | `allow_all` | |
| `APP_POLICY_CONFIG` | Config to pass to the policy evaluation. Format: `key=value;key=value` | | |
| `APP_POLICY_PATH` | Path to the opa-wasm policy (_.wasm_ file). | | Only available if `APP_POLICY` is set to `opa-wasm`. |
| `APP_POLICY_EXPRESSION` | Express when using CEL. | | Only available if `APP_POLICY` is set to `cel`. |
| `APP_GH_AUTH_TYPE` | Type of the GitHub authentication to use, either `app` or `token`. | | **Required** |
| `APP_GH_AUTH_APP_ID` | ID of the GitHub App to use for authentication. (Only for `app` auth type.) | | Required if `APP_GH_AUTH_TYPE` is `app`. |
| `APP_GH_AUTH_APP_PRIVATE_KEY` | Private key of the GitHub App to use for authentication. (Only for `app` auth type.) | | Required if `APP_GH_AUTH_TYPE` is `app`. |
| `APP_GH_AUTH_TOKEN` | Token to use for authentication. (Only for `token` auth type.) | | Required if `APP_GH_AUTH_TYPE` is `token`. |

## Policies

Policies are used to determine whether a request is allowed or not.
The implementation is based on
the [Open Policy Agent (OPA)](https://www.openpolicyagent.org/).

This is implemented using [cross-policy](https://github.com/abinnovision/cross-policy). As of right now,
the [opa-wasm target](https://github.com/abinnovision/cross-policy/tree/main/packages/target-opa-wasm)
and [cel target](https://github.com/abinnovision/cross-policy/tree/main/packages/target-cel) is supported.

### Built-in Policies

Expand All @@ -64,11 +67,11 @@ This policy allows all requests from within the configured organization.
It requires the `organization` configuration to be set on the
`APP_POLICY_CONFIG` environment variable.

### Custom Policies
### Open Policy Agent (OPA) WASM Policies

Custom policies can be used by setting the `APP_POLICY` environment variable
to `custom` and providing the path to the policy file via `APP_POLICY_PATH`. The
policy file must be a valid WebAssembly file built with
OPA WASM can be used by setting the `APP_POLICY` environment variable
to `opa-wasm` and providing the path to the policy file via `APP_POLICY_PATH`.
The policy file must be a valid WebAssembly file built with
[Open Policy Agent (OPA)](https://www.openpolicyagent.org/).

To get started,
Expand All @@ -78,10 +81,30 @@ Also, see the
source [code of the built-in policies for examples](./policies).

[This script](./policies/build.sh) can be used to build the policy file. It can
be place in the folder with many .rego files and it will build a .wasm file in
be placed in the folder with many .rego files, and it will build a .wasm file in
the same folder for each .rego file.

#### Input schema for policies
### Common Expression Language (CEL) Policies

CEL can be used by setting the `APP_POLICY` environment variable
to `cel` and providing the expression via `APP_POLICY_EXPRESSION`.

CEL is a language for expressing policies in a way that is straightforward to understand and write.
It is used by Google in many of its services.

See the [@cross-policy/target-cel](http://npmjs.com/package/@cross-policy/target-cel) package for further details and
possible limitations.

#### Example configuration

This example configuration allows only requests from the owner `abinnovision`.

```
APP_POLICY=cel
APP_POLICY_EXPRESSION='caller.owner == "abinnovision"'
```

### Input schema for policies

The input schema for the policy is defined as follows:

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"prettier": "@abinnovision/prettier-config",
"dependencies": {
"@cross-policy/core": "^0.1.0",
"@cross-policy/target-cel": "^0.1.0",
"@cross-policy/target-opa-wasm": "^0.1.0",
"@octokit/auth-app": "^7.1.1",
"@octokit/auth-callback": "^5.0.1",
Expand Down
4 changes: 0 additions & 4 deletions policies/allow_all.rego

This file was deleted.

Binary file removed policies/allow_all.wasm
Binary file not shown.
15 changes: 0 additions & 15 deletions policies/allow_org_wide.rego

This file was deleted.

Binary file removed policies/allow_org_wide.wasm
Binary file not shown.
32 changes: 32 additions & 0 deletions src/dispatch/policy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,36 @@ describe("handler/policy", () => {
expect(result).toBe(false);
});
});

describe("target/cel", () => {
it("should throw exception on invalid expression", async () => {
vi.stubEnv("APP_GH_AUTH_TYPE", "token");
vi.stubEnv("APP_GH_AUTH_TOKEN", "token");
vi.stubEnv("APP_POLICY", "cel");
vi.stubEnv("APP_POLICY_EXPRESSION", "this is not a valid expression");

const { evaluatePolicyForRequest } = await import("./policy.js");

await expect(
evaluatePolicyForRequest({
target: {
owner: "abinnovison",
repository: "github-workflow-dispatch-proxy",
ref: "main",
workflow: "update-version",

inputs: {
test: "test",
},
},
caller: {
owner: "abinnovison",
repository: "github-workflow-dispatch-proxy",
ref: "main",
workflow: "build",
},
}),
).rejects.toThrow();
});
});
});
41 changes: 24 additions & 17 deletions src/dispatch/policy.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
import { createCrossPolicy } from "@cross-policy/core";
import { celPolicyTarget } from "@cross-policy/target-cel";
import { opaWasmPolicyTarget } from "@cross-policy/target-opa-wasm";
import * as path from "path";
import z from "zod";

import { getConfig } from "../utils/config.js";

// The built-in policies.
const builtInPolicyMapping = {
allow_all: "allow_all.wasm",
allow_org_wide: "allow_org_wide.wasm",
/**
* Built-in expressions written in CEL.
*/
const builtinExpressions = {
allow_all: `true`,
allow_org_wide: `config.organization == caller.owner && config.organization == target.owner`,
};

/**
* Provides the policy to use based on the current config.
* Creates the cross-policy compatible target based on the current config.
*/
function getPolicyPath(): string {
function createCrossPolicyTarget() {
const config = getConfig();

let policyPath: string;
if (config.POLICY === "custom") {
policyPath = config.POLICY_PATH;
if (config.POLICY === "opa-wasm") {
return opaWasmPolicyTarget({
policyPath: config.POLICY_PATH,
});
} else if (config.POLICY === "cel") {
return celPolicyTarget({
expression: config.POLICY_EXPRESSION,
});
} else if (config.POLICY === "builtin") {
return celPolicyTarget({
expression: builtinExpressions[config.POLICY_TYPE],
});
} else {
policyPath = path.join(
process.env.POLICY_DIR as string,
builtInPolicyMapping[config.POLICY_TYPE],
);
// This should never happen.
throw new Error(`Unsupported policy type`);
}

return policyPath;
}

const schema = z.object({
Expand All @@ -51,7 +58,7 @@ const schema = z.object({
});

const crossPolicy = createCrossPolicy({
target: opaWasmPolicyTarget({ policyPath: getPolicyPath() }),
target: createCrossPolicyTarget(),
schema,
});

Expand Down
7 changes: 6 additions & 1 deletion src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,15 @@ const policySchema = z.discriminatedUnion("POLICY", [
POLICY_CONFIG: z.string().optional(),
}),
z.object({
POLICY: z.literal("custom"),
POLICY: z.literal("opa-wasm"),
POLICY_PATH: z.string(),
POLICY_CONFIG: z.string().optional(),
}),
z.object({
POLICY: z.literal("cel"),
POLICY_EXPRESSION: z.string(),
POLICY_CONFIG: z.string().optional(),
}),
]);

const githubAuthSchema = z.discriminatedUnion("GH_AUTH_TYPE", [
Expand Down
91 changes: 91 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,48 @@ __metadata:
languageName: node
linkType: hard

"@chevrotain/cst-dts-gen@npm:11.0.3":
version: 11.0.3
resolution: "@chevrotain/cst-dts-gen@npm:11.0.3"
dependencies:
"@chevrotain/gast": "npm:11.0.3"
"@chevrotain/types": "npm:11.0.3"
lodash-es: "npm:4.17.21"
checksum: 10/601d23fa3312bd0e32816bd3f9ca2dcba775a52192a082fd6c5e4a2e8ee068523401191babbe2c346d6d2551900a67b549f2f74d7ebb7d5b2ee1b6fa3c8857a0
languageName: node
linkType: hard

"@chevrotain/gast@npm:11.0.3":
version: 11.0.3
resolution: "@chevrotain/gast@npm:11.0.3"
dependencies:
"@chevrotain/types": "npm:11.0.3"
lodash-es: "npm:4.17.21"
checksum: 10/7169453a8fbfa994e91995523dea09eab87ab23062ad93f6e51f4a3b03f5e2958e0a8b99d5ca6fa067fccfbbbb8bcf1a4573ace2e1b5a455f6956af9eaccb35a
languageName: node
linkType: hard

"@chevrotain/regexp-to-ast@npm:11.0.3":
version: 11.0.3
resolution: "@chevrotain/regexp-to-ast@npm:11.0.3"
checksum: 10/7387a1c61c5a052de41e1172b33eaaedea166fcdb1ffe4c381b86d00051a8014855a031d28fb658768a62c833ef5f5b0689d0c40de3d7bed556f8fea24396e69
languageName: node
linkType: hard

"@chevrotain/types@npm:11.0.3":
version: 11.0.3
resolution: "@chevrotain/types@npm:11.0.3"
checksum: 10/49a82b71d2de8ceb2383ff2709fa61d245f2ab2e42790b70c57102c80846edaa318d0b3645aedc904d23ea7bd9be8a58f2397b1341760a15eb5aa95a1336e2a9
languageName: node
linkType: hard

"@chevrotain/utils@npm:11.0.3":
version: 11.0.3
resolution: "@chevrotain/utils@npm:11.0.3"
checksum: 10/29b5d84373a7761ad055c53e2f540a67b5b56550d5be1c473149f6b8923eef87ff391ce021c06ac7653843b0149f6ff0cf30b5e48c3f825d295eb06a6c517bd3
languageName: node
linkType: hard

"@commitlint/cli@npm:^19.7.1":
version: 19.7.1
resolution: "@commitlint/cli@npm:19.7.1"
Expand Down Expand Up @@ -334,6 +376,16 @@ __metadata:
languageName: node
linkType: hard

"@cross-policy/target-cel@npm:^0.1.0":
version: 0.1.0
resolution: "@cross-policy/target-cel@npm:0.1.0"
dependencies:
"@cross-policy/core": "npm:^0.1.0"
cel-js: "npm:^0.3.0"
checksum: 10/fedc0646ac9867994f4552e66e739205cae60da6222da2e984cadfe73698518ccb21962116daf21ec65363ba846ecc30dff766fe32123aa660081ab9e263edc0
languageName: node
linkType: hard

"@cross-policy/target-opa-wasm@npm:^0.1.0":
version: 0.1.0
resolution: "@cross-policy/target-opa-wasm@npm:0.1.0"
Expand Down Expand Up @@ -2325,6 +2377,16 @@ __metadata:
languageName: node
linkType: hard

"cel-js@npm:^0.3.0":
version: 0.3.1
resolution: "cel-js@npm:0.3.1"
dependencies:
chevrotain: "npm:11.0.3"
ramda: "npm:0.30.0"
checksum: 10/10dee5175fa9e3c6cf1165977fe3c99008f1f163967aaa91854cc549658e572ea182cfc5c5c3272a385c196c82fb6861306e4dd9564dabdb3fd66f7cc6f6d96e
languageName: node
linkType: hard

"chai@npm:^5.1.2":
version: 5.2.0
resolution: "chai@npm:5.2.0"
Expand Down Expand Up @@ -2373,6 +2435,20 @@ __metadata:
languageName: node
linkType: hard

"chevrotain@npm:11.0.3":
version: 11.0.3
resolution: "chevrotain@npm:11.0.3"
dependencies:
"@chevrotain/cst-dts-gen": "npm:11.0.3"
"@chevrotain/gast": "npm:11.0.3"
"@chevrotain/regexp-to-ast": "npm:11.0.3"
"@chevrotain/types": "npm:11.0.3"
"@chevrotain/utils": "npm:11.0.3"
lodash-es: "npm:4.17.21"
checksum: 10/8fa6253e51320dd4c3d386315b925734943e509d7954a2cd917746c0604461191bea57b0fb8fbab1903e0508fd94bfd35ebd0f8eace77cd0f3f42a9ee4f8f676
languageName: node
linkType: hard

"chokidar@npm:^3.5.2":
version: 3.6.0
resolution: "chokidar@npm:3.6.0"
Expand Down Expand Up @@ -3982,6 +4058,7 @@ __metadata:
"@abinnovision/prettier-config": "npm:^2.1.3"
"@commitlint/cli": "npm:^19.7.1"
"@cross-policy/core": "npm:^0.1.0"
"@cross-policy/target-cel": "npm:^0.1.0"
"@cross-policy/target-opa-wasm": "npm:^0.1.0"
"@octokit/auth-app": "npm:^7.1.1"
"@octokit/auth-callback": "npm:^5.0.1"
Expand Down Expand Up @@ -4902,6 +4979,13 @@ __metadata:
languageName: node
linkType: hard

"lodash-es@npm:4.17.21":
version: 4.17.21
resolution: "lodash-es@npm:4.17.21"
checksum: 10/03f39878ea1e42b3199bd3f478150ab723f93cc8730ad86fec1f2804f4a07c6e30deaac73cad53a88e9c3db33348bb8ceeb274552390e7a75d7849021c02df43
languageName: node
linkType: hard

"lodash.camelcase@npm:^4.3.0":
version: 4.3.0
resolution: "lodash.camelcase@npm:4.3.0"
Expand Down Expand Up @@ -6023,6 +6107,13 @@ __metadata:
languageName: node
linkType: hard

"ramda@npm:0.30.0":
version: 0.30.0
resolution: "ramda@npm:0.30.0"
checksum: 10/18112bc9328bbbdc7b1e59ad5e548e7636813defe00446a083720d7fe8247df23b9b049348f7a10edeec0d2be6813fc19427a5cff792f6e1404079f4136d75f8
languageName: node
linkType: hard

"range-parser@npm:~1.2.1":
version: 1.2.1
resolution: "range-parser@npm:1.2.1"
Expand Down
Loading