diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml new file mode 100644 index 00000000..d7eb4b31 --- /dev/null +++ b/.github/workflows/publish-wasm.yml @@ -0,0 +1,23 @@ +name: publish-wasm + +permissions: + pull-requests: write + contents: write + +on: workflow_dispatch + +jobs: + publish-wasm: + name: publish + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + - name: Build + run: wasm-pack build --target nodejs -r + - name: Publish + run: # TODO diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fa46594a..3b9c686e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -37,3 +37,11 @@ jobs: - name: Run tests (OPA Conformance) run: >- cargo test -r --test opa -- $(tr '\n' ' ' < tests/opa.passing) + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Run wasm binding tests + run: | + cd bindings/wasm + wasm-pack test --node -r diff --git a/Cargo.toml b/Cargo.toml index a9006860..9f731223 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,4 @@ +workspace = { members = ["bindings/wasm"] } [package] name = "regorus" description = "A fast, lightweight Rego (OPA policy language) interpreter" diff --git a/README.md b/README.md index 493c7be8..fa91f26b 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,12 @@ $ cargo build -r --example regorus --features "yaml" --no-default-features; stri Regorus passes the [OPA v0.60.0 test-suite](https://www.openpolicyagent.org/docs/latest/ir/#test-suite) barring a few builtins. See [OPA Conformance](#opa-conformance) below. +## Bindings + +Regorus can be used from a variety of languages: + +- Javascript (nodejs): Via npm package `regorus-wasm`. This package is Regorus compiled into WASM. + ## Getting Started [examples/regorus](https://github.com/microsoft/regorus/blob/main/examples/regorus.rs) is an example program that diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml new file mode 100644 index 00000000..719546ae --- /dev/null +++ b/bindings/wasm/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "regorus-wasm" +version = "0.1.0" +edition = "2021" +repository = "https://github.com/microsoft/regorus/bindings/wasm" +description = "WASM bindings for Regorus - a fast, lightweight Rego interpreter written in Rust" +keywords = ["interpreter", "opa", "policy-as-code", "rego"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib"] + +[dependencies] +regorus = { path = "../.." } +serde_json = "1.0.111" +wasm-bindgen = "0.2.90" + +[dev-dependencies] +wasm-bindgen-test = "0.3.40" diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md new file mode 100644 index 00000000..aadb1db9 --- /dev/null +++ b/bindings/wasm/README.md @@ -0,0 +1,28 @@ +# regorus-wasm + +**Regorus** is + + - *Rego*-*Rus(t)* - A fast, light-weight [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) + interpreter written in Rust. + - *Rigorous* - A rigorous enforcer of well-defined Rego semantics. + +See [Repository](https://github.com/microsoft/regorus). + +`regorus-wasm` is Regorus compiled into WASM. + +## Usage + +In nodejs, + +``javascript + +var regorus = require('regorus-wasm') + +// Create an engine. +var engine = new regorus.Engine(); + +// Add Rego policy. +engine.add_policy() + + +``` diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs new file mode 100644 index 00000000..c2ea9ed2 --- /dev/null +++ b/bindings/wasm/src/lib.rs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +/// WASM wrapper for [`regorus::Engine`] +pub struct Engine { + engine: regorus::Engine, +} + +fn error_to_jsvalue(e: E) -> JsValue { + JsValue::from_str(&format!("{e}")) +} + +impl Default for Engine { + fn default() -> Self { + Self::new() + } +} + +impl Clone for Engine { + /// Clone a [`Engine`] + /// + /// To avoid having to parse same policy again, the engine can be cloned + /// after policies and data have been added. + fn clone(&self) -> Self { + Self { + engine: self.engine.clone(), + } + } +} + +#[wasm_bindgen] +impl Engine { + #[wasm_bindgen(constructor)] + /// Construct a new Engine + /// + /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html + pub fn new() -> Self { + Self { + engine: regorus::Engine::new(), + } + } + + /// Add a policy + /// + /// The policy is parsed into AST. + /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_policy + /// + /// * `path`: A filename to be associated with the policy. + /// * `rego`: Rego policy. + pub fn add_policy(&mut self, path: String, rego: String) -> Result<(), JsValue> { + self.engine.add_policy(path, rego).map_err(error_to_jsvalue) + } + + /// Add policy data. + /// + /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_data + /// * `data`: JSON encoded value to be used as policy data. + pub fn add_data_json(&mut self, data: String) -> Result<(), JsValue> { + let data = regorus::Value::from_json_str(&data).map_err(error_to_jsvalue)?; + self.engine.add_data(data).map_err(error_to_jsvalue) + } + + /// Set input. + /// + /// See https://docs.rs/regorus/0.1.0-alpha.2/regorus/struct.Engine.html#method.set_input + /// * `input`: JSON encoded value to be used as input to query. + pub fn set_input_json(&mut self, input: String) -> Result<(), JsValue> { + let input = regorus::Value::from_json_str(&input).map_err(error_to_jsvalue)?; + self.engine.set_input(input); + Ok(()) + } + + /// Evaluate query. + /// + /// See https://docs.rs/regorus/0.1.0-alpha.2/regorus/struct.Engine.html#method.eval_query + /// * `query`: Rego expression to be evaluate. + pub fn eval_query(&mut self, query: String) -> Result { + let results = self + .engine + .eval_query(query, false) + .map_err(error_to_jsvalue)?; + serde_json::to_string_pretty(&results).map_err(error_to_jsvalue) + } +} + +#[cfg(test)] +mod tests { + use wasm_bindgen::prelude::*; + use wasm_bindgen_test::wasm_bindgen_test; + + #[wasm_bindgen_test] + pub fn basic() -> Result<(), JsValue> { + let mut engine = crate::Engine::new(); + + // Exercise all APIs. + engine.add_data_json( + r#" + { + "foo" : "bar" + } + "# + .to_string(), + )?; + + engine.set_input_json( + r#" + { + "message" : "Hello" + } + "# + .to_string(), + )?; + + engine.add_policy( + "hello.rego".to_string(), + r#" + package test + message = input.message"# + .to_string(), + )?; + + let results = engine.eval_query("data".to_string())?; + let r = regorus::Value::from_json_str(&results).map_err(crate::error_to_jsvalue)?; + + let v = &r["result"][0]["expressions"][0]["value"]; + + // Ensure that input and policy were evaluated. + assert_eq!(v["test"]["message"], regorus::Value::from("Hello")); + + // Test that data was set. + assert_eq!(v["foo"], regorus::Value::from("bar")); + + Ok(()) + } +}