diff --git a/package.json b/package.json index c8efff7..4e2bad8 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "fix:prettier": "prettier --config .prettierrc \"src/**/*.{ts,css,less,scss,js}\" --write", "fix:lint": "eslint src --ext .ts --fix", "test": "run-s test:*", + "test:unit": "run-s build && ava --verbose build/tests/enforcer.spec.js", "test:integration": "run-s build && ava --verbose build/tests/endpoints/**/*.spec.js", "test:module-imports": "run-s build && ava --verbose build/tests/module-imports/**/*.spec.js", "test:e2e:rbac": "run-s build && ava --verbose build/tests/e2e/rbac.e2e.spec.js", @@ -63,8 +64,7 @@ "path-to-regexp": "^6.2.1", "pino": "8.11.0", "pino-pretty": "10.2.0", - "require-in-the-middle": "^5.1.0", - "url-parse": "^1.5.10" + "require-in-the-middle": "^5.1.0" }, "devDependencies": { "@ava/typescript": "^1.1.1", @@ -74,7 +74,6 @@ "@types/express": "^4.17.9", "@types/lodash": "^4.14.166", "@types/node": "^14.14.14", - "@types/url-parse": "^1.4.11", "@typescript-eslint/eslint-plugin": "^4.0.1", "@typescript-eslint/parser": "^4.0.1", "ava": "^3.12.1", diff --git a/src/enforcement/enforcer.ts b/src/enforcement/enforcer.ts index cf1ec9f..0c46672 100644 --- a/src/enforcement/enforcer.ts +++ b/src/enforcement/enforcer.ts @@ -1,6 +1,5 @@ import axios, { AxiosInstance } from 'axios'; import { Logger } from 'pino'; -import URL from 'url-parse'; import { IPermitConfig } from '../config'; import { CheckConfig, Context, ContextStore } from '../utils/context'; @@ -118,6 +117,25 @@ export interface IEnforcer { ): Promise; } +/** + * Builds the OPA client base URL from the configured PDP URL by forcing the OPA + * port (8181) and appending the OPA data path, using the native WHATWG `URL` + * (Node >= 10) in place of the previous `url-parse` dependency (#106). + * + * @param pdp - The configured PDP base URL (e.g. `http://localhost:7766`). + * @returns The OPA base URL (e.g. `http://localhost:8181/v1/data/permit/`). A PDP + * path with no trailing slash is glued to the data path (`/prefix` -> + * `/prefixv1/data/permit/`), preserved from `url-parse` and locked by a test. + * @throws {TypeError} on input without a scheme (e.g. `localhost`); a `host:port` + * value like `localhost:7766` is misparsed, not rejected — pass a full URL. + */ +export function buildOpaBaseUrl(pdp: string): string { + const opaBaseUrl = new URL(pdp); + opaBaseUrl.port = '8181'; + opaBaseUrl.pathname = `${opaBaseUrl.pathname}v1/data/permit/`; + return opaBaseUrl.toString(); +} + /** * The {@link Enforcer} class is responsible for performing permission checks against the PDP. * It implements the {@link IEnforcer} interface. @@ -133,9 +151,7 @@ export class Enforcer implements IEnforcer { * @param logger - The logger instance for logging. */ constructor(private config: IPermitConfig, private logger: Logger) { - const opaBaseUrl = new URL(this.config.pdp); - opaBaseUrl.set('port', '8181'); - opaBaseUrl.set('pathname', `${opaBaseUrl.pathname}v1/data/permit/`); + const opaBaseUrl = buildOpaBaseUrl(this.config.pdp); const version = process.env.npm_package_version ?? 'unknown'; if (config.axiosInstance) { this.client = config.axiosInstance; @@ -151,11 +167,11 @@ export class Enforcer implements IEnforcer { } if (config.opaAxiosInstance) { this.opaClient = config.opaAxiosInstance; - this.opaClient.defaults.baseURL = opaBaseUrl.toString(); + this.opaClient.defaults.baseURL = opaBaseUrl; this.opaClient.defaults.headers.common['X-Permit-SDK-Version'] = `node:${version}`; } else { this.opaClient = axios.create({ - baseURL: opaBaseUrl.toString(), + baseURL: opaBaseUrl, headers: { 'X-Permit-SDK-Version': `node:${version}`, }, diff --git a/src/tests/enforcer.spec.ts b/src/tests/enforcer.spec.ts new file mode 100644 index 0000000..62780c8 --- /dev/null +++ b/src/tests/enforcer.spec.ts @@ -0,0 +1,57 @@ +import test from 'ava'; +import axios from 'axios'; + +import { buildOpaBaseUrl } from '../enforcement/enforcer'; +import { Permit } from '../index'; + +// The OPA client base URL is derived from the configured PDP URL by forcing the +// OPA port (8181) and appending the OPA data path. This was previously built +// with the `url-parse` package and is now built with the native WHATWG `URL` +// (#106). These assertions lock the produced string so the refactor is proven +// behaviour-equivalent and any regression in the construction logic fails here. + +test('buildOpaBaseUrl: default PDP', (t) => { + t.is(buildOpaBaseUrl('http://localhost:7766'), 'http://localhost:8181/v1/data/permit/'); +}); + +test('buildOpaBaseUrl: trailing slash yields the same URL as no trailing slash', (t) => { + t.is(buildOpaBaseUrl('http://localhost:7766/'), 'http://localhost:8181/v1/data/permit/'); +}); + +test('buildOpaBaseUrl: https host without an explicit port', (t) => { + t.is(buildOpaBaseUrl('https://pdp.example.com'), 'https://pdp.example.com:8181/v1/data/permit/'); +}); + +test('buildOpaBaseUrl: an existing port is overridden with 8181', (t) => { + t.is( + buildOpaBaseUrl('https://pdp.example.com:1234'), + 'https://pdp.example.com:8181/v1/data/permit/', + ); +}); + +test('buildOpaBaseUrl: a path-prefixed PDP preserves the pre-existing concatenation behaviour', (t) => { + // Pre-existing url-parse quirk: a path with no trailing slash glues onto the data path. + t.is( + buildOpaBaseUrl('http://localhost:7766/prefix'), + 'http://localhost:8181/prefixv1/data/permit/', + ); + t.is( + buildOpaBaseUrl('http://localhost:7766/prefix/'), + 'http://localhost:8181/prefix/v1/data/permit/', + ); +}); + +test('buildOpaBaseUrl: a PDP without a scheme throws (invalid absolute URL)', (t) => { + // Scheme-less input (bare host or `//host:port`) throws at construction; `localhost:7766` is misparsed, not rejected. + t.throws(() => buildOpaBaseUrl('localhost'), { instanceOf: TypeError }); + t.throws(() => buildOpaBaseUrl('//localhost:7766'), { instanceOf: TypeError }); +}); + +test('Enforcer wires the OPA client base URL to buildOpaBaseUrl(pdp)', (t) => { + // Guards the integration the refactor actually edits: the constructed OPA axios + // client must take its baseURL from the helper. Passing an opaAxiosInstance lets + // us read what the Enforcer set, without a live PDP or network call. + const opaAxiosInstance = axios.create(); + new Permit({ token: 'test-token', pdp: 'http://localhost:7766', opaAxiosInstance }); + t.is(opaAxiosInstance.defaults.baseURL, buildOpaBaseUrl('http://localhost:7766')); +}); diff --git a/yarn.lock b/yarn.lock index bf3d8fb..9ddb093 100644 --- a/yarn.lock +++ b/yarn.lock @@ -807,11 +807,6 @@ "@types/mime" "*" "@types/node" "*" -"@types/url-parse@^1.4.11": - version "1.4.11" - resolved "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.11.tgz" - integrity sha512-FKvKIqRaykZtd4n47LbK/W/5fhQQ1X7cxxzG9A48h0BGN+S04NH7ervcCjM8tyR0lyGru83FAHSmw2ObgKoESg== - "@typescript-eslint/eslint-plugin@^4.0.1": version "4.33.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz" @@ -5311,11 +5306,6 @@ q@^1.5.1: resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -5527,11 +5517,6 @@ require-main-filename@^2.0.0: resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" @@ -6699,14 +6684,6 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.5.10: - version "1.5.10" - resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz" - integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - urlgrey@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/urlgrey/-/urlgrey-1.0.0.tgz"