diff --git a/README.md b/README.md index a6b39513..53ff1c7d 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ -**Work in Progress -- Contributions welcome!!** +**Work in Progress -- Contributions welcome!!** # Open Policy Agent WebAssemby NPM Module + This is the source for the [@open-policy-agent/opa-wasm](https://www.npmjs.com/package/@open-policy-agent/opa-wasm) -NPM module which is a small SDK for using WebAssembly (wasm) compiled +NPM module which is a small SDK for using WebAssembly (wasm) compiled [Open Policy Agent](https://www.openpolicyagent.org/) Rego policies. # Getting Started + ## Install the module ``` -npm install @open-policy-agent/opa-wasm +npm install @open-policy-agent/opa-wasm ``` ## Usage @@ -26,34 +28,35 @@ const { loadPolicy } = require("@open-policy-agent/opa-wasm"); ### Load the policy ```javascript -loadPolicy(policyWasm) +loadPolicy(policyWasm); ``` -The `loadPolicy` function returns a Promise with the loaded policy. -Typically this means loading it in an `async` function like: + +The `loadPolicy` function returns a Promise with the loaded policy. Typically +this means loading it in an `async` function like: ```javascript -const policy = await loadPolicy(policyWasm) +const policy = await loadPolicy(policyWasm); ``` Or something like: ```javascript -loadPolicy(policyWasm).then(policy => { - // evaluate or save the policy -}, error => { - console.error("Failed to load policy: " + error) -}) +loadPolicy(policyWasm).then((policy) => { + // evaluate or save the policy +}, (error) => { + console.error("Failed to load policy: " + error); +}); ``` -The `policyWasm` needs to be either the raw byte array of -the compiled policy Wasm file, or a WebAssembly module. +The `policyWasm` needs to be either the raw byte array of the compiled policy +Wasm file, or a WebAssembly module. For example: ```javascript -const fs = require('fs'); +const fs = require("fs"); -const policyWasm = fs.readFileSync('policy.wasm'); +const policyWasm = fs.readFileSync("policy.wasm"); ``` Alternatively the bytes can be pulled in remotely from a `fetch` or in some @@ -65,49 +68,53 @@ javascript context through external APIs. The loaded policy object returned from `loadPolicy()` has a couple of important APIs for policy evaluation: -`setData(obj)` -- Provide an external `data` document for policy evaluation. Requires a JSON serializable object. -`evaluate(input)` -- Evaluates the policy using any loaded data and the supplied `input` document. +`setData(obj)` -- Provide an external `data` document for policy evaluation. +Requires a JSON serializable object. `evaluate(input)` -- Evaluates the policy +using any loaded data and the supplied `input` document. The `input` parameter must be a JSON string. Example: ```javascript - input = '{"path": "/", "role": "admin"}'; -loadPolicy(policyWasm).then(policy => { - resultSet = policy.evaluate(input); - if (resultSet == null) { - console.error("evaluation error") - } - if (resultSet.length == 0) { - console.log("undefined") - } - console.log("allowed = " + allowed[0].result); -}).catch( error => { - console.error("Failed to load policy: ", error); -}) +loadPolicy(policyWasm).then((policy) => { + resultSet = policy.evaluate(input); + if (resultSet == null) { + console.error("evaluation error"); + } + if (resultSet.length == 0) { + console.log("undefined"); + } + console.log("allowed = " + allowed[0].result); +}).catch((error) => { + console.error("Failed to load policy: ", error); +}); ``` > For any `opa build` created WASM binaries the result set, when defined, will - contain a `result` key with the value of the compiled entrypoint. See - [https://www.openpolicyagent.org/docs/latest/wasm/](https://www.openpolicyagent.org/docs/latest/wasm/) - for more details. +> contain a `result` key with the value of the compiled entrypoint. See +> [https://www.openpolicyagent.org/docs/latest/wasm/](https://www.openpolicyagent.org/docs/latest/wasm/) +> for more details. ## Writing the policy -See [https://www.openpolicyagent.org/docs/latest/how-do-i-write-policies/](https://www.openpolicyagent.org/docs/latest/how-do-i-write-policies/) +See +[https://www.openpolicyagent.org/docs/latest/how-do-i-write-policies/](https://www.openpolicyagent.org/docs/latest/how-do-i-write-policies/) ## Compiling the policy -Either use the [Compile REST API](https://www.openpolicyagent.org/docs/latest/rest-api/#compile-api) or `opa build` CLI tool. +Either use the +[Compile REST API](https://www.openpolicyagent.org/docs/latest/rest-api/#compile-api) +or `opa build` CLI tool. For example, with OPA v0.20.5+: ```bash opa build -t wasm -e 'example/allow' example.rego ``` + Which is compiling the `example.rego` policy file with the result set to `data.example.allow`. The result will be an OPA bundle with the `policy.wasm` binary included. See [./examples](./examples) for a more comprehensive example. diff --git a/examples/nodejs-app/README.md b/examples/nodejs-app/README.md index 07b7741e..b9406337 100644 --- a/examples/nodejs-app/README.md +++ b/examples/nodejs-app/README.md @@ -1,37 +1,43 @@ # Simple opa-wasm node application -The application is in [app.js](./app.js) and shows loading a `*.wasm` file, initializing -the policy, and evaluating it with input. +The application is in [app.js](./app.js) and shows loading a `*.wasm` file, +initializing the policy, and evaluating it with input. ## Install dependencies -This requires the `opa-wasm` package, see [package.json](./package.json) for details. +This requires the `opa-wasm` package, see [package.json](./package.json) for +details. ```bash npm install ``` -> The example uses a local path, in "real" use-cases use the standard NPM module. +> The example uses a local path, in "real" use-cases use the standard NPM +> module. ## Build the WebAssembly binary for the example policy: > The syntax shown below requires OPA v0.20.5+ -There is an example policy included with the example, see [example.rego](./example.rego) +There is an example policy included with the example, see +[example.rego](./example.rego) ```bash opa build -t wasm -e 'example/hello' ./example.rego tar -xzf ./bundle.tar.gz /policy.wasm ``` -This will create a bundle tarball with the WASM binary included, and then unpack just the `policy.wasm` from the bundle. +This will create a bundle tarball with the WASM binary included, and then unpack +just the `policy.wasm` from the bundle. ## Run the example Node JS code that invokes the WASM binary: ```bash node app.js '{"message": "world"}' ``` + Produces: + ``` [ { @@ -40,11 +46,12 @@ Produces: ] ``` - ```bash node app.js '{"message": "not-world"}' ``` + Produces: + ``` [ { diff --git a/examples/nodejs-app/app.js b/examples/nodejs-app/app.js index 7780f669..f7ea8406 100644 --- a/examples/nodejs-app/app.js +++ b/examples/nodejs-app/app.js @@ -2,27 +2,25 @@ // Use of this source code is governed by an Apache2 // license that can be found in the LICENSE file. -const fs = require('fs'); +const fs = require("fs"); const { loadPolicy } = require("@open-policy-agent/opa-wasm"); // Read the policy wasm file -const policyWasm = fs.readFileSync('policy.wasm'); +const policyWasm = fs.readFileSync("policy.wasm"); // Load the policy module asynchronously -loadPolicy(policyWasm).then(policy => { +loadPolicy(policyWasm).then((policy) => { + // Use console parameters for the input, do quick + // validation by json parsing. Not efficient.. but + // will raise an error + const input = JSON.parse(process.argv[2]); + // Provide a data document with a string value + policy.setData({ world: "world" }); - // Use console parameters for the input, do quick - // validation by json parsing. Not efficient.. but - // will raise an error - const input = JSON.parse(process.argv[2]); - // Provide a data document with a string value - policy.setData({world: "world"}); - - // Evaluate the policy and log the result - const result = policy.evaluate(input); - console.log(JSON.stringify(result, null, 2)) - -}).catch(err => { - console.log("ERROR: ", err); - process.exit(1); + // Evaluate the policy and log the result + const result = policy.evaluate(input); + console.log(JSON.stringify(result, null, 2)); +}).catch((err) => { + console.log("ERROR: ", err); + process.exit(1); }); diff --git a/examples/nodejs-ts-app-multi-entrypoint/README.md b/examples/nodejs-ts-app-multi-entrypoint/README.md index 792d0c46..6205ada4 100644 --- a/examples/nodejs-ts-app-multi-entrypoint/README.md +++ b/examples/nodejs-ts-app-multi-entrypoint/README.md @@ -1,7 +1,7 @@ # Multi-entrypoint OPA-WASM node demo script -This script demos loading a WASM OPA file and simulates 1,000,000 evaluations -on a few different entrypoints to demonstrate how entrypoints can be used. +This script demos loading a WASM OPA file and simulates 1,000,000 evaluations on +a few different entrypoints to demonstrate how entrypoints can be used. ## Install dependencies @@ -11,9 +11,9 @@ npm install ## Build the WebAssembly binary for the example policies -There are two example policies located in the ./policies directory, these -are compiled into a WASM. Look in the package.json to see how the entrypoints -are defined. +There are two example policies located in the ./policies directory, these are +compiled into a WASM. Look in the package.json to see how the entrypoints are +defined. > Tested with OPA v0.27.1 @@ -28,6 +28,7 @@ npm start ``` Sample Output + ``` Running multi entrypoint demo suite Iterations: 100000 iterations of 10 inputs for 1000000 total evals per entrypoint diff --git a/examples/nodejs-ts-app-multi-entrypoint/app.ts b/examples/nodejs-ts-app-multi-entrypoint/app.ts index a2ee711b..ad524d27 100644 --- a/examples/nodejs-ts-app-multi-entrypoint/app.ts +++ b/examples/nodejs-ts-app-multi-entrypoint/app.ts @@ -1,86 +1,120 @@ -import * as fs from 'fs'; -import { loadPolicy } from '@open-policy-agent/opa-wasm'; +import * as fs from "fs"; +import { loadPolicy } from "@open-policy-agent/opa-wasm"; const iterations = 100000; const inputs = [ - { someProp: 'thisValue', anotherProp: 'thatValue', anyProp: 'aValue', ourProp: 'inTheMiddleOfTheStreet' }, - { someProp: '', anotherProp: 'thatValue', anyProp: 'aValue', ourProp: 'inTheMiddleOfTheStreet' }, - { someProp: 'thisValue', anotherProp: '', anyProp: 'aValue', ourProp: 'inTheMiddleOfTheStreet' }, - { someProp: 'thisValue', anotherProp: 'thatValue', anyProp: '', ourProp: 'inTheMiddleOfTheStreet' }, - { someProp: 'thisValue', anotherProp: 'thatValue', anyProp: 'aValue', ourProp: '' }, - { someProp: 'thisValue', anotherProp: 'thatValue' }, - { anyProp: 'aValue', ourProp: 'inTheMiddleOfTheStreet' }, - { someProp: 'thisValue', ourProp: 'inTheMiddleOfTheStreet' }, - { anotherProp: 'thatValue', anyProp: 'aValue' }, - { }, + { + someProp: "thisValue", + anotherProp: "thatValue", + anyProp: "aValue", + ourProp: "inTheMiddleOfTheStreet", + }, + { + someProp: "", + anotherProp: "thatValue", + anyProp: "aValue", + ourProp: "inTheMiddleOfTheStreet", + }, + { + someProp: "thisValue", + anotherProp: "", + anyProp: "aValue", + ourProp: "inTheMiddleOfTheStreet", + }, + { + someProp: "thisValue", + anotherProp: "thatValue", + anyProp: "", + ourProp: "inTheMiddleOfTheStreet", + }, + { + someProp: "thisValue", + anotherProp: "thatValue", + anyProp: "aValue", + ourProp: "", + }, + { someProp: "thisValue", anotherProp: "thatValue" }, + { anyProp: "aValue", ourProp: "inTheMiddleOfTheStreet" }, + { someProp: "thisValue", ourProp: "inTheMiddleOfTheStreet" }, + { anotherProp: "thatValue", anyProp: "aValue" }, + {}, ]; (async function readPolicy() { - const policy = await loadPolicy(fs.readFileSync('./policy.wasm')) + const policy = await loadPolicy(fs.readFileSync("./policy.wasm")); - console.log(`Running multi entrypoint demo suite`); - console.log(`Iterations: ${iterations} iterations of ${inputs.length} inputs for ${iterations * inputs.length} total evals per entrypoint`); + console.log(`Running multi entrypoint demo suite`); + console.log( + `Iterations: ${iterations} iterations of ${inputs.length} inputs for ${iterations * + inputs.length} total evals per entrypoint`, + ); - // Run the default entrypoint first - console.time(`default entrypoint`); - for(let iteration = 0; iteration < iterations; iteration++) { - for(let input of inputs) { - policy.evaluate(input); - } + // Run the default entrypoint first + console.time(`default entrypoint`); + for (let iteration = 0; iteration < iterations; iteration++) { + for (const input of inputs) { + policy.evaluate(input); } - console.timeEnd(`default entrypoint`); + } + console.timeEnd(`default entrypoint`); - // Run the example one entrypoint, string access - console.time(`example/one entrypoint (via string)`); - for(let iteration = 0; iteration < iterations; iteration++) { - for(let input of inputs) { - policy.evaluate(input, 'example/one'); - } + // Run the example one entrypoint, string access + console.time(`example/one entrypoint (via string)`); + for (let iteration = 0; iteration < iterations; iteration++) { + for (const input of inputs) { + policy.evaluate(input, "example/one"); } - console.timeEnd(`example/one entrypoint (via string)`); + } + console.timeEnd(`example/one entrypoint (via string)`); - // Run the example one entrypoint, number access - const exampleOneEntrypoint = policy.entrypoints['example/one']; - console.time(`example/one entrypoint (via number "${exampleOneEntrypoint}")`); - for(let iteration = 0; iteration < iterations; iteration++) { - for(let input of inputs) { - policy.evaluate(input, exampleOneEntrypoint); - } + // Run the example one entrypoint, number access + const exampleOneEntrypoint = policy.entrypoints["example/one"]; + console.time(`example/one entrypoint (via number "${exampleOneEntrypoint}")`); + for (let iteration = 0; iteration < iterations; iteration++) { + for (const input of inputs) { + policy.evaluate(input, exampleOneEntrypoint); } - console.timeEnd(`example/one entrypoint (via number "${exampleOneEntrypoint}")`); + } + console.timeEnd( + `example/one entrypoint (via number "${exampleOneEntrypoint}")`, + ); - // Run the example two coolRule entrypoint, number access - console.time(`example/two/coolRule entrypoint (via string)`); - for(let iteration = 0; iteration < iterations; iteration++) { - for(let input of inputs) { - policy.evaluate(input, 'example/two/coolRule'); - } + // Run the example two coolRule entrypoint, number access + console.time(`example/two/coolRule entrypoint (via string)`); + for (let iteration = 0; iteration < iterations; iteration++) { + for (const input of inputs) { + policy.evaluate(input, "example/two/coolRule"); } - console.timeEnd(`example/two/coolRule entrypoint (via string)`); + } + console.timeEnd(`example/two/coolRule entrypoint (via string)`); - // Run the example two coolRule entrypoint, number access - const coolRuleEntrypoint = policy.entrypoints['example/two/coolRule']; - console.time(`example/two/coolRule entrypoint (via number "${coolRuleEntrypoint}")`); - for(let iteration = 0; iteration < iterations; iteration++) { - for(let input of inputs) { - policy.evaluate(input, coolRuleEntrypoint); - } + // Run the example two coolRule entrypoint, number access + const coolRuleEntrypoint = policy.entrypoints["example/two/coolRule"]; + console.time( + `example/two/coolRule entrypoint (via number "${coolRuleEntrypoint}")`, + ); + for (let iteration = 0; iteration < iterations; iteration++) { + for (const input of inputs) { + policy.evaluate(input, coolRuleEntrypoint); } - console.timeEnd(`example/two/coolRule entrypoint (via number "${coolRuleEntrypoint}")`); + } + console.timeEnd( + `example/two/coolRule entrypoint (via number "${coolRuleEntrypoint}")`, + ); - console.log(`Evaluate policy from default entrypoint`); - console.dir(policy.evaluate(inputs[0]), {depth: 3}); + console.log(`Evaluate policy from default entrypoint`); + console.dir(policy.evaluate(inputs[0]), { depth: 3 }); - console.log(`Evaluate policy from example/one entrypoint`); - console.dir(policy.evaluate(inputs[1], 'example/one')); + console.log(`Evaluate policy from example/one entrypoint`); + console.dir(policy.evaluate(inputs[1], "example/one")); - console.log(`Evaluate policy from example/two/coolRule entrypoint`); - console.dir(policy.evaluate(inputs[2], 'example/two/coolRule')); + console.log(`Evaluate policy from example/two/coolRule entrypoint`); + console.dir(policy.evaluate(inputs[2], "example/two/coolRule")); - console.log(`Evaluate policy from example/two entrypoint`); - console.dir(policy.evaluate(inputs[3], 'example/two')); -})().catch(err => { - console.log("ERROR: ", err); - process.exit(1); -}); \ No newline at end of file + console.log(`Evaluate policy from example/two entrypoint`); + console.dir(policy.evaluate(inputs[3], "example/two")); +})().catch((err) => { + console.log("ERROR: ", err); + process.exit(1); +}); diff --git a/examples/nodejs-ts-app-multi-entrypoint/tsconfig.json b/examples/nodejs-ts-app-multi-entrypoint/tsconfig.json index fc8e2013..92931137 100644 --- a/examples/nodejs-ts-app-multi-entrypoint/tsconfig.json +++ b/examples/nodejs-ts-app-multi-entrypoint/tsconfig.json @@ -1,13 +1,13 @@ { - "include": ["**/*.ts"], - "compilerOptions": { - "target": "ES2019", - "moduleResolution": "node", - "noEmit": true, - "strict": true, - "baseUrl": "./", - "paths": { - "@open-policy-agent/opa-wasm": ["../../"] - } - }, -} \ No newline at end of file + "include": ["**/*.ts"], + "compilerOptions": { + "target": "ES2019", + "moduleResolution": "node", + "noEmit": true, + "strict": true, + "baseUrl": "./", + "paths": { + "@open-policy-agent/opa-wasm": ["../../"] + } + } +} diff --git a/examples/nodejs-ts-app/README.md b/examples/nodejs-ts-app/README.md index fb6d9621..03b986b0 100644 --- a/examples/nodejs-ts-app/README.md +++ b/examples/nodejs-ts-app/README.md @@ -1,7 +1,7 @@ # Simple opa-wasm node typescript application -The application is in [app.ts](./app.ts) and shows loading a `*.wasm` file, initializing -the policy, and evaluating it with input. +The application is in [app.ts](./app.ts) and shows loading a `*.wasm` file, +initializing the policy, and evaluating it with input. ## Install dependencies @@ -11,7 +11,8 @@ npm install ## Build the WebAssembly binary for the example policy -There is an example policy included with the example, see [example.rego](./example.rego) +There is an example policy included with the example, see +[example.rego](./example.rego) > Requires OPA v0.20.5+ diff --git a/examples/nodejs-ts-app/app.ts b/examples/nodejs-ts-app/app.ts index 3ff04d20..50ba0ca4 100644 --- a/examples/nodejs-ts-app/app.ts +++ b/examples/nodejs-ts-app/app.ts @@ -2,24 +2,24 @@ // Use of this source code is governed by an Apache2 // license that can be found in the LICENSE file. -import { promises as fs } from 'fs'; -import { loadPolicy } from '@open-policy-agent/opa-wasm'; +import { promises as fs } from "fs"; +import { loadPolicy } from "@open-policy-agent/opa-wasm"; (async function readPolicy() { - const policyWasm = await fs.readFile('policy.wasm'); - const policy = await loadPolicy(policyWasm); + const policyWasm = await fs.readFile("policy.wasm"); + const policy = await loadPolicy(policyWasm); - // Use console parameters for the input, do quick - // validation by json parsing. Not efficient.. but - // will raise an error - const input = JSON.parse(process.argv[2]); - // Provide a data document with a string value - policy.setData({ world: "world" }); + // Use console parameters for the input, do quick + // validation by json parsing. Not efficient.. but + // will raise an error + const input = JSON.parse(process.argv[2]); + // Provide a data document with a string value + policy.setData({ world: "world" }); - // Evaluate the policy and log the result - const result = policy.evaluate(input); - console.log(JSON.stringify(result, null, 2)) -})().catch(err => { - console.log("ERROR: ", err); - process.exit(1); + // Evaluate the policy and log the result + const result = policy.evaluate(input); + console.log(JSON.stringify(result, null, 2)); +})().catch((err) => { + console.log("ERROR: ", err); + process.exit(1); }); diff --git a/examples/nodejs-ts-app/tsconfig.json b/examples/nodejs-ts-app/tsconfig.json index fc8e2013..92931137 100644 --- a/examples/nodejs-ts-app/tsconfig.json +++ b/examples/nodejs-ts-app/tsconfig.json @@ -1,13 +1,13 @@ { - "include": ["**/*.ts"], - "compilerOptions": { - "target": "ES2019", - "moduleResolution": "node", - "noEmit": true, - "strict": true, - "baseUrl": "./", - "paths": { - "@open-policy-agent/opa-wasm": ["../../"] - } - }, -} \ No newline at end of file + "include": ["**/*.ts"], + "compilerOptions": { + "target": "ES2019", + "moduleResolution": "node", + "noEmit": true, + "strict": true, + "baseUrl": "./", + "paths": { + "@open-policy-agent/opa-wasm": ["../../"] + } + } +} diff --git a/src/builtins/strings.js b/src/builtins/strings.js index 2c6e15a5..5c7a6155 100644 --- a/src/builtins/strings.js +++ b/src/builtins/strings.js @@ -1,4 +1,4 @@ -const vsprintf = require('sprintf-js').vsprintf +const vsprintf = require("sprintf-js").vsprintf; sprintf = (s, values) => vsprintf(s, values); diff --git a/src/opa.js b/src/opa.js index 75005b3c..755b229b 100644 --- a/src/opa.js +++ b/src/opa.js @@ -2,7 +2,7 @@ // Use of this source code is governed by an Apache2 // license that can be found in the LICENSE file. const builtIns = require("./builtins/index"); -const utf8 = require('utf8'); +const utf8 = require("utf8"); /** * @param {WebAssembly.Memory} mem @@ -87,22 +87,21 @@ const builtinFuncs = builtIns; * @param {{ [builtinId: number]: string }} builtins * @param {string} builtin_id */ -function _builtinCall(wasmInstance, memory, builtins, builtin_id) { - const builtInName = builtins[builtin_id]; +function _builtinCall(wasmInstance, memory, builtins, builtinId) { + const builtInName = builtins[builtinId]; const impl = builtinFuncs[builtInName]; if (impl === undefined) { throw { - message: - "not implemented: built-in function " + - builtin_id + + message: "not implemented: built-in function " + + builtinId + ": " + - builtins[builtin_id], + builtins[builtinId], }; } - var argArray = Array.prototype.slice.apply(arguments); - let args = []; + const argArray = Array.prototype.slice.apply(arguments); + const args = []; for (let i = 4; i < argArray.length; i++) { const jsArg = _dumpJSON(wasmInstance, memory, argArray[i]); @@ -127,50 +126,56 @@ function _builtinCall(wasmInstance, memory, builtins, builtin_id) { async function _loadPolicy(policyWasm, memory) { const addr2string = stringDecoder(memory); - let env = {}; + const env = {}; const wasm = await WebAssembly.instantiate(policyWasm, { env: { - memory: memory, + memory, opa_abort: function (addr) { throw addr2string(addr); }, opa_println: function (addr) { - console.log(addr2string(addr)) + console.log(addr2string(addr)); }, - opa_builtin0: function (builtin_id, _ctx) { - return _builtinCall(env.instance, memory, env.builtins, builtin_id); + opa_builtin0: function (builtinId, _ctx) { + return _builtinCall(env.instance, memory, env.builtins, builtinId); }, - opa_builtin1: function (builtin_id, _ctx, arg1) { - return _builtinCall(env.instance, memory, env.builtins, builtin_id, arg1); + opa_builtin1: function (builtinId, _ctx, arg1) { + return _builtinCall( + env.instance, + memory, + env.builtins, + builtinId, + arg1, + ); }, - opa_builtin2: function (builtin_id, _ctx, arg1, arg2) { + opa_builtin2: function (builtinId, _ctx, arg1, arg2) { return _builtinCall( env.instance, memory, env.builtins, - builtin_id, + builtinId, arg1, arg2, ); }, - opa_builtin3: function (builtin_id, _ctx, arg1, arg2, arg3) { + opa_builtin3: function (builtinId, _ctx, arg1, arg2, arg3) { return _builtinCall( env.instance, memory, env.builtins, - builtin_id, + builtinId, arg1, arg2, arg3, ); }, - opa_builtin4: function (builtin_id, _ctx, arg1, arg2, arg3, arg4) { + opa_builtin4: function (builtinId, _ctx, arg1, arg2, arg3, arg4) { return _builtinCall( env.instance, memory, env.builtins, - builtin_id, + builtinId, arg1, arg2, arg3, @@ -190,7 +195,8 @@ async function _loadPolicy(policyWasm, memory) { console.error("opa_wasm_abi_version undefined"); // logs to stderr } - const abiMinorVersionGlobal = wasm.instance.exports.opa_wasm_abi_minor_version; + const abiMinorVersionGlobal = + wasm.instance.exports.opa_wasm_abi_minor_version; let abiMinorVersion; if (abiMinorVersionGlobal !== undefined) { abiMinorVersion = abiMinorVersionGlobal.value; @@ -209,7 +215,7 @@ async function _loadPolicy(policyWasm, memory) { /** @type {typeof builtIns} */ env.builtins = {}; - for (var key of Object.keys(builtins)) { + for (const key of Object.keys(builtins)) { env.builtins[builtins[key]] = key; } @@ -239,26 +245,30 @@ class LoadedPolicy { this.dataAddr = _loadJSON(this.wasmInstance, this.mem, {}); this.baseHeapPtr = this.wasmInstance.exports.opa_heap_ptr_get(); this.dataHeapPtr = this.baseHeapPtr; - this.entrypoints = _dumpJSON(this.wasmInstance, this.mem, this.wasmInstance.exports.entrypoints()); + this.entrypoints = _dumpJSON( + this.wasmInstance, + this.mem, + this.wasmInstance.exports.entrypoints(), + ); } /** * Evaluates the loaded policy with the given input and * return the result set. This should be re-used for multiple evaluations * of the same policy with different inputs. - * + * * To call a non-default entrypoint in your WASM specify it as the second * param. A list of entrypoints can be accessed with the `this.entrypoints` - * property. + * property. * @param {object} input * @param {number | string} entrypoint ID or name of the entrypoint to call (optional) */ evaluate(input, entrypoint = 0) { // determine entrypoint ID - if (typeof entrypoint === 'number') { + if (typeof entrypoint === "number") { // used as-is - } else if (typeof entrypoint === 'string') { - if(this.entrypoints.hasOwnProperty(entrypoint)) { + } else if (typeof entrypoint === "string") { + if (Object.prototype.hasOwnProperty.call(this.entrypoints, entrypoint)) { entrypoint = this.entrypoints[entrypoint]; } else { throw `entrypoint ${entrypoint} is not valid in this instance`; @@ -273,18 +283,26 @@ class LoadedPolicy { let inputLen = 0; let inputAddr = 0; if (input) { - const inp = JSON.stringify(input); - const buf = new Uint8Array(this.mem.buffer); - inputAddr = this.dataHeapPtr; - inputLen = inp.length; - - for (let i = 0; i < inputLen; i++) { - buf[inputAddr + i] = inp.charCodeAt(i); - } - this.dataHeapPtr = inputAddr + inputLen; + const inp = JSON.stringify(input); + const buf = new Uint8Array(this.mem.buffer); + inputAddr = this.dataHeapPtr; + inputLen = inp.length; + + for (let i = 0; i < inputLen; i++) { + buf[inputAddr + i] = inp.charCodeAt(i); + } + this.dataHeapPtr = inputAddr + inputLen; } - const ret = this.wasmInstance.exports.opa_eval(0, entrypoint, this.dataAddr, inputAddr, inputLen, this.dataHeapPtr, 0); + const ret = this.wasmInstance.exports.opa_eval( + 0, + entrypoint, + this.dataAddr, + inputAddr, + inputLen, + this.dataHeapPtr, + 0, + ); return _dumpJSONRaw(this.mem, ret); } @@ -300,7 +318,6 @@ class LoadedPolicy { this.wasmInstance.exports.opa_eval_ctx_set_data(ctxAddr, this.dataAddr); this.wasmInstance.exports.opa_eval_ctx_set_entrypoint(ctxAddr, entrypoint); - // Actually evaluate the policy this.wasmInstance.exports.eval(ctxAddr); @@ -339,7 +356,7 @@ module.exports = { * and will return a LoadedPolicy object which can be used to evaluate * the policy. * - * To set custom memory size specify number of memory pages + * To set custom memory size specify number of memory pages * as second param. * Defaults to 5 pages (320KB). * @param {BufferSource | WebAssembly.Module} regoWasm @@ -349,5 +366,5 @@ module.exports = { const memory = new WebAssembly.Memory({ initial: memorySize }); const { policy, minorVersion } = await _loadPolicy(regoWasm, memory); return new LoadedPolicy(policy, memory, minorVersion); - } -} + }, +}; diff --git a/test/multiple-entrypoints.test.js b/test/multiple-entrypoints.test.js index de0efbb3..3d892b39 100644 --- a/test/multiple-entrypoints.test.js +++ b/test/multiple-entrypoints.test.js @@ -1,92 +1,106 @@ -const { loadPolicy } = require('../src/opa.js'); -const { readFileSync } = require('fs'); -const { execFileSync } = require('child_process'); - -describe('multiple entrypoints', () => { - let policy = null; - - beforeAll(async () => { - try { - execFileSync('opa', [ - 'build', - `${__dirname}/fixtures/multiple-entrypoints`, - '-o', `${__dirname}/fixtures/multiple-entrypoints/bundle.tar.gz`, - '-t', 'wasm', - '-e', 'example', - '-e', 'example/one', - '-e', 'example/two', - ]); - - execFileSync('tar', [ - '-xzf', `${__dirname}/fixtures/multiple-entrypoints/bundle.tar.gz`, '-C', `${__dirname}/fixtures/multiple-entrypoints/`, `/policy.wasm`, - ]); - } catch (err) { - console.error('Error creating test binary, check that opa is in path'); - throw err; - } - - policy = await loadPolicy(readFileSync(`${__dirname}/fixtures/multiple-entrypoints/policy.wasm`)); - }) - - it('should run with default entrypoint', () => { - const result = policy.evaluate(); - - expect(result.length).not.toBe(0); - expect(result[0]).toMatchObject({ - result: { - one: expect.any(Object), - two: expect.any(Object) - } - }); - }); - - it('should run with numbered entrypoint specified', () => { - const entrypointId = policy.entrypoints['example/one']; - const result = policy.evaluate({}, entrypointId); - - expect(result.length).not.toBe(0); - expect(result[0]).toMatchObject({ - result: { - myRule: false, - myOtherRule: false - } - }); - }); - - it('should run with named entrypoint specified', () => { - const result = policy.evaluate({}, 'example/one'); - - expect(result.length).not.toBe(0); - expect(result[0]).toMatchObject({ - result: { - myRule: false, - myOtherRule: false - } - }); - }); - - it('should run with second entrypoint specified', () => { - const result = policy.evaluate({}, 'example/two'); - - expect(result.length).not.toBe(0); - expect(result[0]).toMatchObject({ - result: { - ourRule: false, - theirRule: false - } - }); - }); - - it('should not run with entrypoint as object', () => { - expect(() => { - policy.evaluate({}, {}); - }).toThrow('entrypoint value is an invalid type, must be either string or number') - }); - - it('should not run if entrypoint string does not exist', () => { - expect(() => { - policy.evaluate({}, 'not/a/real/entrypoint'); - }).toThrow('entrypoint not/a/real/entrypoint is not valid in this instance'); - }); - -}) +const { loadPolicy } = require("../src/opa.js"); +const { readFileSync } = require("fs"); +const { execFileSync } = require("child_process"); + +describe("multiple entrypoints", () => { + let policy = null; + + beforeAll(async () => { + try { + execFileSync("opa", [ + "build", + `${__dirname}/fixtures/multiple-entrypoints`, + "-o", + `${__dirname}/fixtures/multiple-entrypoints/bundle.tar.gz`, + "-t", + "wasm", + "-e", + "example", + "-e", + "example/one", + "-e", + "example/two", + ]); + + execFileSync("tar", [ + "-xzf", + `${__dirname}/fixtures/multiple-entrypoints/bundle.tar.gz`, + "-C", + `${__dirname}/fixtures/multiple-entrypoints/`, + `/policy.wasm`, + ]); + } catch (err) { + console.error("Error creating test binary, check that opa is in path"); + throw err; + } + + policy = await loadPolicy( + readFileSync(`${__dirname}/fixtures/multiple-entrypoints/policy.wasm`), + ); + }); + + it("should run with default entrypoint", () => { + const result = policy.evaluate(); + + expect(result.length).not.toBe(0); + expect(result[0]).toMatchObject({ + result: { + one: expect.any(Object), + two: expect.any(Object), + }, + }); + }); + + it("should run with numbered entrypoint specified", () => { + const entrypointId = policy.entrypoints["example/one"]; + const result = policy.evaluate({}, entrypointId); + + expect(result.length).not.toBe(0); + expect(result[0]).toMatchObject({ + result: { + myRule: false, + myOtherRule: false, + }, + }); + }); + + it("should run with named entrypoint specified", () => { + const result = policy.evaluate({}, "example/one"); + + expect(result.length).not.toBe(0); + expect(result[0]).toMatchObject({ + result: { + myRule: false, + myOtherRule: false, + }, + }); + }); + + it("should run with second entrypoint specified", () => { + const result = policy.evaluate({}, "example/two"); + + expect(result.length).not.toBe(0); + expect(result[0]).toMatchObject({ + result: { + ourRule: false, + theirRule: false, + }, + }); + }); + + it("should not run with entrypoint as object", () => { + expect(() => { + policy.evaluate({}, {}); + }).toThrow( + "entrypoint value is an invalid type, must be either string or number", + ); + }); + + it("should not run if entrypoint string does not exist", () => { + expect(() => { + policy.evaluate({}, "not/a/real/entrypoint"); + }).toThrow( + "entrypoint not/a/real/entrypoint is not valid in this instance", + ); + }); +}); diff --git a/test/opa-node-cases.test.js b/test/opa-node-cases.test.js index 9530bf8c..80958bf3 100644 --- a/test/opa-node-cases.test.js +++ b/test/opa-node-cases.test.js @@ -1,29 +1,31 @@ -const { loadPolicy } = require('../src/opa.js'); -const { readFileSync, readdirSync } = require('fs'); +const { loadPolicy } = require("../src/opa.js"); +const { readFileSync, readdirSync } = require("fs"); let files = []; const path = process.env.OPA_CASES; if (path === undefined) { - describe('opa nodejs cases', () => { - test.todo('not found, set OPA_CASES env var'); + describe("opa nodejs cases", () => { + test.todo("not found, set OPA_CASES env var"); }); } else { files = readdirSync(path); } let numFiles = 0; -var testCases = []; +const testCases = []; -files.forEach(file => { - if (file.endsWith('.json')) { +files.forEach((file) => { + if (file.endsWith(".json")) { numFiles++; const testFile = JSON.parse(readFileSync(path + "/" + file)); if (Array.isArray(testFile.cases)) { - testFile.cases.forEach(testCase => { + testFile.cases.forEach((testCase) => { testCase.note = `${file}: ${testCase.note}`; - if (testCase.note === '018_builtins.json: custom built-in' || - testCase.note === '018_builtins.json: impure built-in' || - testCase.note === '019_call_indirect_optimization.json: memoization') { - testCase.skip = 'skipping tests with custom builtins'; + if ( + testCase.note === "018_builtins.json: custom built-in" || + testCase.note === "018_builtins.json: impure built-in" || + testCase.note === "019_call_indirect_optimization.json: memoization" + ) { + testCase.skip = "skipping tests with custom builtins"; } testCases.push(testCase); }); @@ -31,47 +33,47 @@ files.forEach(file => { } }); -testCases.forEach(tc => { +testCases.forEach((tc) => { const { wasm, input, data, note, - want_defined, - want_result, - want_error, - skip_reason, - skip + want_defined: wantDefined, + want_result: wantResult, + want_error: wantError, + skip_reason: skipReason, + skip, } = tc; describe(note, () => { if (skip) { - test.skip(`skip ${note}: ${skip_reason}`, () => {}); + test.skip(`skip ${note}: ${skipReason}`, () => {}); return; } - if (want_error) { - if('errors', async() => { - const policy = await loadPolicy(Buffer.from(wasm, 'base64')); + if (wantError) { + it("errors", async () => { + const policy = await loadPolicy(Buffer.from(wasm, "base64")); policy.setData(data); - expect(() => policy.evaluate(input)).toThrow(want_error); + expect(() => policy.evaluate(input)).toThrow(wantError); }); return; } - it('has the desired result', async () => { - const policy = await loadPolicy(Buffer.from(wasm, 'base64')); + it("has the desired result", async () => { + const policy = await loadPolicy(Buffer.from(wasm, "base64")); policy.setData(data || {}); const result = policy.evaluate(input); - if (want_defined !== undefined) { - if (want_defined) { + if (wantDefined !== undefined) { + if (wantDefined) { expect(result.length).toBeGreaterThan(0); } else { expect(result.length).toBe(0); } } - if (want_result !== undefined) { - expect(result.length).toEqual(want_result.length); - expect(result).toEqual(expect.arrayContaining(want_result)); + if (wantResult !== undefined) { + expect(result.length).toEqual(wantResult.length); + expect(result).toEqual(expect.arrayContaining(wantResult)); } }); }); diff --git a/test/opa-test-cases.test.js b/test/opa-test-cases.test.js index 136e38fb..28591564 100644 --- a/test/opa-test-cases.test.js +++ b/test/opa-test-cases.test.js @@ -1,26 +1,27 @@ -const { readFileSync, readdirSync, writeFileSync } = require('fs'); -const { execFileSync, spawnSync } = require('child_process'); -const { join } = require('path'); -const { loadPolicy } = require('../src/opa.js'); -const yaml = require('js-yaml'); -const tmp = require('tmp'); -const sort = require('smart-deep-sort'); +const { readFileSync, readdirSync, writeFileSync } = require("fs"); +const { execFileSync, spawnSync } = require("child_process"); +const { join } = require("path"); +const { loadPolicy } = require("../src/opa.js"); +const yaml = require("js-yaml"); +const tmp = require("tmp"); +const sort = require("smart-deep-sort"); // Known failures const exceptions = { - 'sprintf/big_int': 'bit ints are loosing precision', - 'sprintf/big_int/max_cert_serial_number': 'lost precision, scientific format displayed', - 'strings/sprintf: float too big': '2e308 displayed as "Infinity"', - 'strings/sprintf: composite': 'array is concatenated', + "sprintf/big_int": "bit ints are loosing precision", + "sprintf/big_int/max_cert_serial_number": + "lost precision, scientific format displayed", + "strings/sprintf: float too big": '2e308 displayed as "Infinity"', + "strings/sprintf: composite": "array is concatenated", }; function walk(dir) { let results = []; - readdirSync(dir, { withFileTypes: true }).forEach(d => { + readdirSync(dir, { withFileTypes: true }).forEach((d) => { file = join(dir, d.name); - if (d.isDirectory()) { + if (d.isDirectory()) { results = results.concat(walk(file)); - } else { + } else { results.push(file); } }); @@ -28,7 +29,7 @@ function walk(dir) { } function modulesToTempFiles(modules) { - let ret = []; + const ret = []; for (const mod of modules) { const tmpFile = tmp.fileSync(); writeFileSync(tmpFile.fd, mod); @@ -39,7 +40,10 @@ function modulesToTempFiles(modules) { function compileToWasm(modules, query) { if (modules && modules.length < 1) { - return { skip: `empty modules cases are not supported (got ${modules && modules.length})` }; + return { + skip: `empty modules cases are not supported (got ${modules && + modules.length})`, + }; } // NOTE(sr) crude but effective @@ -58,28 +62,44 @@ function compileToWasm(modules, query) { const outFile = tmp.fileSync(); const untarDir = tmp.dirSync(); - const res = spawnSync('opa', ['build', '-t', 'wasm', '--capabilities', 'capabilities.json', '-e', entrypoint, '-o', outFile.name, ...files]); + const res = spawnSync("opa", [ + "build", + "-t", + "wasm", + "--capabilities", + "capabilities.json", + "-e", + entrypoint, + "-o", + outFile.name, + ...files, + ]); if (res.error || res.status != 0) { return { skip: res.stdout }; } - execFileSync('tar', ['xf', outFile.name, '-C', untarDir.name, '/policy.wasm'], { stdio: 'ignore' }); - return { wasm: join(untarDir.name, 'policy.wasm') }; + execFileSync( + "tar", + ["xf", outFile.name, "-C", untarDir.name, "/policy.wasm"], + { stdio: "ignore" }, + ); + return { wasm: join(untarDir.name, "policy.wasm") }; } const path = process.env.OPA_TEST_CASES; if (path === undefined) { - describe('opa external test cases', () => { - test.todo('not found, set OPA_TEST_CASES env var'); + describe("opa external test cases", () => { + test.todo("not found, set OPA_TEST_CASES env var"); }); } for (const file of walk(path)) { describe(file, () => { - const doc = yaml.load(readFileSync(file, 'utf8')); - cases: for (const tc of doc.cases) { + const doc = yaml.load(readFileSync(file, "utf8")); + cases: + for (const tc of doc.cases) { const reason = exceptions[tc.note]; if (reason) { - test. todo(`${tc.note}: ${reason}`); + test.todo(`${tc.note}: ${reason}`); continue cases; } if (tc.input_term) { @@ -95,7 +115,10 @@ for (const file of walk(path)) { } } if (tc.want_result && tc.want_result.length > 1) { - test.todo(`${tc.note}: more than one expected result not supported: ${tc.want_result && tc.want_result.length}`); + test.todo( + `${tc.note}: more than one expected result not supported: ${tc + .want_result && tc.want_result.length}`, + ); continue cases; } let expected = tc.want_result; @@ -113,22 +136,26 @@ for (const file of walk(path)) { policy.setData(tc.data); } let input = tc.input || tc.input_term; - if (typeof input === 'string') { + if (typeof input === "string") { input = JSON.parse(input); } if ((tc.want_error || tc.want_error_code) && !tc.strict_error) { - expect(() => { policy.evaluate(input) }).toThrow(); + expect(() => { + policy.evaluate(input); + }).toThrow(); return; } let res; - expect(() => { res = policy.evaluate(input) }).not.toThrow(); + expect(() => { + res = policy.evaluate(input); + }).not.toThrow(); if (expected) { expect(res).toHaveLength(expected.length); if (tc.sort_bindings) { - res = { result: sort(res[0].result) }; + res = { result: sort(res[0].result) }; expected = { x: sort(expected[0].x) }; } expect(res[0] && res[0].result).toEqual(expected[0] && expected[0].x);