├── .github ├── dependabot.yml └── workflows │ ├── check.yaml │ └── release-plz.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── clippy.toml ├── features.txt ├── src ├── bin │ ├── opa-eval.rs │ └── simple.rs ├── builtins │ ├── impls │ │ ├── base64url.rs │ │ ├── crypto.rs │ │ ├── glob.rs │ │ ├── graph.rs │ │ ├── graphql.rs │ │ ├── hex.rs │ │ ├── http.rs │ │ ├── io.rs │ │ ├── json.rs │ │ ├── mod.rs │ │ ├── net.rs │ │ ├── object.rs │ │ ├── opa.rs │ │ ├── rand.rs │ │ ├── regex.rs │ │ ├── rego.rs │ │ ├── semver.rs │ │ ├── time.rs │ │ ├── units.rs │ │ ├── urlquery.rs │ │ ├── uuid.rs │ │ └── yaml.rs │ ├── mod.rs │ └── traits.rs ├── context.rs ├── funcs.rs ├── lib.rs ├── loader.rs ├── policy.rs └── types.rs └── tests ├── fixtures ├── test-loader.false.json ├── test-loader.rego ├── test-loader.true.json ├── test-rand.rego ├── test-time.rego ├── test-units.rego ├── test-urlquery.rego └── test-yaml.rego ├── smoke_test.rs └── snapshots ├── smoke_test__loader_empty.snap ├── smoke_test__loader_false.snap ├── smoke_test__loader_true.snap ├── smoke_test__rand.snap ├── smoke_test__time.snap ├── smoke_test__units.snap ├── smoke_test__urlquery.snap └── smoke_test__yaml.snap /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | versioning-strategy: "auto" 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | rustfmt: 14 | name: Check style 15 | runs-on: ubuntu-latest 16 | 17 | permissions: 18 | contents: read 19 | 20 | steps: 21 | - name: Checkout the code 22 | uses: actions/checkout@v4 23 | 24 | - name: Install toolchain 25 | run: | 26 | rustup toolchain install nightly 27 | rustup default nightly 28 | rustup component add rustfmt 29 | 30 | - name: Check style 31 | run: cargo fmt --all -- --check 32 | 33 | clippy: 34 | name: Run Clippy 35 | runs-on: ubuntu-latest 36 | 37 | permissions: 38 | contents: read 39 | 40 | steps: 41 | - name: Checkout the code 42 | uses: actions/checkout@v4 43 | 44 | - name: Install toolchain 45 | run: | 46 | rustup toolchain install 1.85.0 47 | rustup default 1.85.0 48 | rustup component add clippy 49 | 50 | - name: Setup Rust cache 51 | uses: Swatinem/rust-cache@v2 52 | 53 | - name: Run Clippy 54 | run: | 55 | run_clippy () { 56 | echo "::group::cargo clippy $@" 57 | cargo clippy --bins --tests $@ -- -D warnings 58 | echo "::endgroup::" 59 | } 60 | 61 | run_clippy --all-features 62 | run_clippy --no-default-features 63 | sed -e 's/#.*//' -e '/^\s*$/d' -e 's/\s\+/,/g' features.txt | while read -r FEATURES; do 64 | run_clippy --no-default-features --features "${FEATURES}" 65 | done 66 | 67 | test: 68 | name: Run test suite with Rust ${{ matrix.toolchain }} 69 | needs: [rustfmt, clippy] 70 | runs-on: ubuntu-latest 71 | 72 | permissions: 73 | contents: read 74 | 75 | continue-on-error: "${{ matrix.toolchain == 'beta' || matrix.toolchain == 'nightly' }}" 76 | 77 | strategy: 78 | fail-fast: false # Continue other jobs if one fails to help filling the cache 79 | matrix: 80 | toolchain: 81 | - stable 82 | - beta 83 | - nightly 84 | 85 | steps: 86 | - name: Checkout the code 87 | uses: actions/checkout@v4 88 | 89 | - name: Install toolchain 90 | run: | 91 | rustup toolchain install ${{ matrix.toolchain }} 92 | rustup default ${{ matrix.toolchain }} 93 | 94 | - name: Setup Rust cache 95 | uses: Swatinem/rust-cache@v2 96 | 97 | - name: Setup OPA 98 | uses: open-policy-agent/setup-opa@v2 99 | with: 100 | version: latest 101 | 102 | - name: Build OPA bundles for test 103 | run: make build-opa 104 | 105 | - name: Test 106 | run: cargo test --all-features 107 | 108 | minimal-versions: 109 | name: Run test suite with minimal versions 110 | needs: [rustfmt, clippy] 111 | runs-on: ubuntu-latest 112 | 113 | permissions: 114 | contents: read 115 | 116 | steps: 117 | - name: Checkout the code 118 | uses: actions/checkout@v4 119 | 120 | - name: Install toolchain 121 | run: | 122 | rustup toolchain install 1.76.0 # MSRV 123 | rustup default 1.76.0 # MSRV 124 | 125 | - name: Setup Rust cache 126 | uses: Swatinem/rust-cache@v2 127 | 128 | - name: Setup OPA 129 | uses: open-policy-agent/setup-opa@v2 130 | with: 131 | version: latest 132 | 133 | - name: Build OPA bundles for test 134 | run: make build-opa 135 | 136 | - name: Install minimal versions 137 | env: 138 | RUSTC_BOOTSTRAP: "1" 139 | run: | 140 | cargo update -Z minimal-versions 141 | 142 | - name: Test 143 | run: cargo test --all-features --locked 144 | 145 | coverage: 146 | name: Code coverage 147 | needs: [rustfmt, clippy] 148 | runs-on: ubuntu-latest 149 | 150 | permissions: 151 | contents: read 152 | 153 | steps: 154 | - name: Checkout the code 155 | uses: actions/checkout@v4 156 | 157 | - name: Install toolchain 158 | run: | 159 | rustup toolchain install stable 160 | rustup default stable 161 | 162 | - name: Setup Rust cache 163 | uses: Swatinem/rust-cache@v2 164 | 165 | - name: Install cargo-llvm-cov 166 | uses: taiki-e/install-action@cargo-llvm-cov 167 | 168 | - name: Setup OPA 169 | uses: open-policy-agent/setup-opa@v2 170 | with: 171 | version: latest 172 | 173 | - name: Build OPA bundles for test 174 | run: make build-opa 175 | 176 | - name: Run test suite with profiling enabled 177 | run: | 178 | cargo llvm-cov --all-features --no-fail-fast --tests --codecov --output-path codecov.json 179 | 180 | - name: Upload to codecov.io 181 | uses: codecov/codecov-action@v5 182 | with: 183 | files: codecov.json 184 | token: ${{ secrets.CODECOV_TOKEN }} 185 | fail_ci_if_error: true 186 | 187 | tests-done: 188 | name: Tests done 189 | if: ${{ always() }} 190 | needs: 191 | - rustfmt 192 | - clippy 193 | - test 194 | - coverage 195 | - minimal-versions 196 | runs-on: ubuntu-latest 197 | 198 | steps: 199 | - uses: matrix-org/done-action@v3 200 | with: 201 | needs: ${{ toJSON(needs) }} 202 | -------------------------------------------------------------------------------- /.github/workflows/release-plz.yml: -------------------------------------------------------------------------------- 1 | name: Release Plz 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | release-plz: 14 | name: Release-plz 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Install Rust toolchain 22 | uses: dtolnay/rust-toolchain@stable 23 | - name: Run release-plz 24 | uses: MarcoIeni/release-plz-action@v0.5 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} 27 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /target 3 | tests/fixtures/*.tar.gz 4 | /Cargo.lock 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | comment_width = 80 3 | wrap_comments = true 4 | imports_granularity = "Crate" 5 | use_small_heuristics = "Default" 6 | group_imports = "StdExternalCrate" 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.5](https://github.com/matrix-org/rust-opa-wasm/compare/v0.1.4...v0.1.5) - 2025-04-06 10 | 11 | ### Other 12 | 13 | - *(deps)* update wasmtime requirement from >=22, <31 to >=22, <32 14 | - *(deps)* update duration-str requirement from >=0.11, <0.14 to >=0.11, <0.16 15 | - *(deps)* update json-patch requirement from >=0.2.3, <3.1.0 to >=0.2.3, <4.1.0 16 | 17 | ## [0.1.4](https://github.com/matrix-org/rust-opa-wasm/compare/v0.1.3...v0.1.4) - 2025-02-21 18 | 19 | ### Other 20 | 21 | - Merge pull request [#204](https://github.com/matrix-org/rust-opa-wasm/pull/204) from matrix-org/dependabot/cargo/wasmtime-gte-22-and-lt-31 22 | - Merge pull request [#203](https://github.com/matrix-org/rust-opa-wasm/pull/203) from matrix-org/dependabot/cargo/duration-str-0.13 23 | - Widen the version range for duration-str 24 | - *(deps)* update duration-str requirement from 0.11 to 0.13 25 | - bump clippy to 1.85 26 | - adapt the tests to rego v1 syntax 27 | 28 | ## [0.1.3](https://github.com/matrix-org/rust-opa-wasm/compare/v0.1.2...v0.1.3) - 2024-11-21 29 | 30 | ### Other 31 | 32 | - *(deps)* update wasmtime requirement from >=22, <27 to >=22, <28 33 | - *(deps)* bump codecov/codecov-action from 4 to 5 34 | 35 | ## [0.1.2](https://github.com/matrix-org/rust-opa-wasm/compare/v0.1.1...v0.1.2) - 2024-11-12 36 | 37 | ### Other 38 | 39 | - Merge pull request [#193](https://github.com/matrix-org/rust-opa-wasm/pull/193) from matrix-org/dependabot/cargo/thiserror-2 40 | - Merge pull request [#191](https://github.com/matrix-org/rust-opa-wasm/pull/191) from matrix-org/dependabot/cargo/json-patch-gte-0.2.3-and-lt-3.1.0 41 | - Merge pull request [#192](https://github.com/matrix-org/rust-opa-wasm/pull/192) from matrix-org/dependabot/cargo/wasmtime-gte-22-and-lt-27 42 | - *(deps)* update wasmtime requirement from >=22, <24 to >=22, <27 43 | 44 | ## [0.1.1](https://github.com/matrix-org/rust-opa-wasm/compare/v0.1.0...v0.1.1) - 2024-10-07 45 | 46 | ### Other 47 | 48 | - *(ci)* bump clippy in CI to 1.81.0 49 | - *(deps)* update wasmtime requirement from >=22, <23 to >=22, <24 50 | 51 | ## [0.1.0](https://github.com/matrix-org/rust-opa-wasm/releases/tag/v0.1.0) - 2024-07-01 52 | 53 | ### Added 54 | - Initial release 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing code to `rust-opa-wasm` 2 | 3 | Everyone is welcome to contribute code to `rust-opa-wasm`, provided that they are willing to license their contributions under the same license as the project itself. 4 | We follow a simple 'inbound=outbound' model for contributions: the act of submitting an 'inbound' contribution means that the contributor agrees to license the code under the same terms as the project's overall 'outbound' license - in this case, Apache Software License v2 (see [LICENSE](./LICENSE)). 5 | 6 | ## How to contribute 7 | 8 | The preferred and easiest way to contribute changes to the project is to fork it on GitHub, and then create a pull request to ask us to pull your changes into our repo. 9 | 10 | We use GitHub's pull request workflow to review the contribution, and either ask you to make any refinements needed or merge it and make them ourselves. 11 | 12 | Things that should go into your PR description: 13 | 14 | - References to any bugs fixed by the change 15 | - Notes for the reviewer that might help them to understand why the change is necessary or how they might better review it 16 | 17 | Your PR must also: 18 | 19 | - be based on the `main` branch 20 | - adhere to the [code style](#code-style) 21 | - pass the [test suite](#tests) 22 | - include a [sign off](#sign-off) 23 | 24 | ## Tests 25 | 26 | Integration tests require to compile Open Policy Agent policies to WebAssembly. 27 | It can be done by running `make build-opa` from the project root directory. 28 | It requires the [`opa`](https://www.openpolicyagent.org/docs/latest/#running-opa) CLI tool to be available. 29 | 30 | Running the integration tests require all the features to be enabled, so run them with 31 | 32 | ```sh 33 | cargo test --all-features 34 | ``` 35 | 36 | The integration tests leverage snapshots with [`cargo-insta`](https://insta.rs/). 37 | 38 | ## Code style 39 | 40 | We use the standard Rust code style, and enforce it with `rustfmt`/`cargo fmt`. 41 | A few code style options are set in the [`.rustfmt.toml`](./.rustfmt.toml) file, and some of them are not stable yet and require a nightly version of rustfmt. 42 | 43 | If you're using [`rustup`](https://rustup.rs), the nightly version of `rustfmt` can be installed by doing the following: 44 | 45 | ``` 46 | rustup component add rustfmt --toolchain nightly 47 | ``` 48 | 49 | And then format your code by running: 50 | 51 | ``` 52 | cargo +nightly fmt 53 | ``` 54 | 55 | --- 56 | 57 | We also enforce some code style rules via [`clippy`](https://github.com/rust-lang/rust-clippy). 58 | 59 | Some of those rules are from the `clippy::pedantic` ruleset, which can be sometime too restrictive. 60 | There are legitimate reasons to break some of those rules, so don't hesitate to allow some of them locally via the `#[allow(clippy::name_of_the_rule)]` attribute. 61 | 62 | Make sure to have Clippy lints also pass both with all the features flag enabled and with none of them: 63 | 64 | ```sh 65 | cargo clippy --bins --tests 66 | cargo clippy --bins --tests --all-features 67 | cargo clippy --bins --tests --no-default-features 68 | ``` 69 | 70 | ## Sign off 71 | 72 | In order to have a concrete record that your contribution is intentional and you agree to license it under the same terms as the project's license, we've adopted the same lightweight approach that the Linux Kernel (https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker (https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other projects use: the DCO (Developer Certificate of Origin: http://developercertificate.org/). 73 | This is a simple declaration that you wrote the contribution or otherwise have the right to contribute it to Matrix: 74 | 75 | ``` 76 | Developer Certificate of Origin 77 | Version 1.1 78 | 79 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 80 | 660 York Street, Suite 102, 81 | San Francisco, CA 94110 USA 82 | 83 | Everyone is permitted to copy and distribute verbatim copies of this 84 | license document, but changing it is not allowed. 85 | 86 | Developer's Certificate of Origin 1.1 87 | 88 | By making a contribution to this project, I certify that: 89 | 90 | (a) The contribution was created in whole or in part by me and I 91 | have the right to submit it under the open source license 92 | indicated in the file; or 93 | 94 | (b) The contribution is based upon previous work that, to the best 95 | of my knowledge, is covered under an appropriate open source 96 | license and I have the right under that license to submit that 97 | work with modifications, whether created in whole or in part 98 | by me, under the same open source license (unless I am 99 | permitted to submit under a different license), as indicated 100 | in the file; or 101 | 102 | (c) The contribution was provided directly to me by some other 103 | person who certified (a), (b) or (c) and I have not modified 104 | it. 105 | 106 | (d) I understand and agree that this project and the contribution 107 | are public and that a record of the contribution (including all 108 | personal information I submit with it, including my sign-off) is 109 | maintained indefinitely and may be redistributed consistent with 110 | this project or the open source license(s) involved. 111 | ``` 112 | 113 | If you agree to this for your contribution, then all that's needed is to include the line in your commit or pull request comment: 114 | 115 | ``` 116 | Signed-off-by: Your Name 117 | ``` 118 | 119 | We accept contributions under a legally identifiable name, such as your name on government documentation or common-law names (names claimed by legitimate usage or repute). 120 | Unfortunately, we cannot accept anonymous contributions at this time. 121 | 122 | Git allows you to add this signoff automatically when using the `-s` flag to `git commit`, which uses the name and email set in your `user.name` and `user.email` git configs. 123 | 124 | If you forgot to sign off your commits before making your pull request and are on Git 2.17+ you can mass signoff using rebase: 125 | 126 | ``` 127 | git rebase --signoff origin/main 128 | ``` 129 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "opa-wasm" 3 | version = "0.1.5" 4 | description = "A crate to use OPA policies compiled to WASM." 5 | repository = "https://github.com/matrix-org/rust-opa-wasm" 6 | rust-version = "1.76" 7 | authors = ["Quentin Gliech "] 8 | edition = "2021" 9 | license = "Apache-2.0" 10 | default-run = "opa-eval" 11 | 12 | [dependencies] 13 | anyhow = "1" 14 | serde = { version = "1", features = ["derive"] } 15 | serde_json = "1.0.18" # This is the earliest version which supports 128-bit integers 16 | thiserror = ">=1, <3" 17 | tokio = { version = "1.5", features = ["sync", "macros"] } 18 | tracing = "0.1.27" 19 | wasmtime = { version = ">=22, <34", default-features = false, features = [ 20 | "async", 21 | ] } 22 | 23 | # Loader 24 | tokio-tar = { version = "0.3", optional = true } 25 | async-compression = { version = "0.4", optional = true, features = [ 26 | "tokio", 27 | "gzip", 28 | ] } 29 | futures-util = { version = "0.3", optional = true } 30 | 31 | # CLI 32 | camino = { version = "1", optional = true } 33 | clap = { version = "4", features = ["derive"], optional = true } 34 | tracing-forest = { version = "0.1.4", optional = true } 35 | tracing-subscriber = { version = "0.3", features = [ 36 | "env-filter", 37 | ], optional = true } 38 | 39 | # Builtins 40 | base64 = { version = "0.22", optional = true } 41 | digest = { version = "0.10", optional = true } 42 | hex = { version = "0.4", optional = true } 43 | hmac = { version = "0.12", optional = true } 44 | json-patch = { version = ">=0.2.3, <4.1.0", optional = true, default-features = false } 45 | md-5 = { version = "0.10", optional = true } 46 | rand = { version = "0.8", optional = true } 47 | semver = { version = "1", optional = true } 48 | sha1 = { version = "0.10", optional = true } 49 | sha2 = { version = "0.10", optional = true } 50 | sprintf = { version = ">=0.3, <0.5", optional = true } 51 | parse-size = { version = "1", features = ["std"], optional = true } 52 | serde_yaml = { version = "0.9.1", optional = true } 53 | form_urlencoded = { version = "1", optional = true } 54 | urlencoding = { version = "2", optional = true } 55 | chrono = { version = "0.4.31", optional = true, default-features = false, features = [ 56 | "std", 57 | "clock", 58 | ] } 59 | chrono-tz = { version = ">=0.6, <0.11.0", optional = true } 60 | chronoutil = { version = "0.2", optional = true } 61 | duration-str = { version = ">=0.11, <0.16", optional = true, default-features = false } 62 | 63 | [dev-dependencies.tokio] 64 | version = "1.5" 65 | features = ["macros", "fs", "rt", "rt-multi-thread"] 66 | 67 | [dev-dependencies] 68 | wasmtime = { version = ">=22, <34", default-features = false, features = [ 69 | "cranelift", 70 | ] } 71 | insta = { version = "1", features = ["yaml"] } 72 | 73 | [build-dependencies] 74 | # We would like at least this version of rayon, because older versions depend on old rand, 75 | # which depends on old log, which depends on old libc, which doesn't build with newer rustc 76 | rayon = "^1.6" 77 | 78 | # wasmtime fails to resolve to its minimal version without this 79 | version_check = "^0.9.4" 80 | 81 | [features] 82 | default = ["all-builtins", "fast"] 83 | 84 | loader = [ 85 | "dep:tokio-tar", 86 | "dep:async-compression", 87 | "dep:futures-util", 88 | "tokio/fs", 89 | "tokio/io-util", 90 | ] 91 | 92 | cli = [ 93 | "loader", 94 | "fast", 95 | "dep:camino", 96 | "dep:clap", 97 | "dep:tracing-forest", 98 | "dep:tracing-subscriber", 99 | "tokio/fs", 100 | "tokio/rt-multi-thread", 101 | ] 102 | fast = ["wasmtime/cranelift", "wasmtime/parallel-compilation"] 103 | 104 | rng = ["dep:rand"] 105 | time = ["dep:chrono"] 106 | 107 | base64url-builtins = ["dep:base64", "dep:hex"] 108 | crypto-digest-builtins = ["dep:digest", "dep:hex"] 109 | crypto-hmac-builtins = ["dep:hmac", "dep:hex"] 110 | crypto-md5-builtins = ["dep:md-5"] 111 | crypto-sha1-builtins = ["dep:sha1"] 112 | crypto-sha2-builtins = ["dep:sha2"] 113 | hex-builtins = ["dep:hex"] 114 | semver-builtins = ["dep:semver"] 115 | sprintf-builtins = ["dep:sprintf"] 116 | json-builtins = ["dep:json-patch"] 117 | units-builtins = ["dep:parse-size"] 118 | rand-builtins = ["rng"] 119 | yaml-builtins = ["dep:serde_yaml"] 120 | urlquery-builtins = ["dep:form_urlencoded", "dep:urlencoding"] 121 | time-builtins = ["time", "dep:chrono-tz", "dep:duration-str", "dep:chronoutil"] 122 | 123 | all-crypto-builtins = [ 124 | "crypto-digest-builtins", 125 | "crypto-hmac-builtins", 126 | "crypto-md5-builtins", 127 | "crypto-sha1-builtins", 128 | "crypto-sha2-builtins", 129 | ] 130 | 131 | all-builtins = [ 132 | "all-crypto-builtins", 133 | "base64url-builtins", 134 | "hex-builtins", 135 | "json-builtins", 136 | "rand-builtins", 137 | "semver-builtins", 138 | "sprintf-builtins", 139 | "units-builtins", 140 | "yaml-builtins", 141 | "urlquery-builtins", 142 | "time-builtins", 143 | ] 144 | 145 | [[test]] 146 | name = "smoke_test" 147 | required-features = ["loader"] 148 | 149 | [[bin]] 150 | name = "opa-eval" 151 | required-features = ["cli"] 152 | 153 | [[bin]] 154 | name = "simple" 155 | required-features = ["cli"] 156 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build-opa: 2 | ls tests/fixtures/*.rego | xargs -I {} opa build {} -t wasm -e fixtures -o {}.tar.gz 3 | clean-opa: 4 | rm tests/fixtures/*.tar.gz 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust Open Policy Agent SDK 2 | 3 | A crate to use OPA policies compiled to WASM. 4 | 5 | ## Try it out 6 | 7 | This includes a CLI tool to try out the SDK implementation. 8 | 9 | ```text 10 | cargo run --features=cli -- \ 11 | --module ./policy.wasm \ 12 | --data-path ./data.json \ 13 | --input '{"hello": "world"}' \ 14 | --entrypoint 'hello/world' 15 | ``` 16 | 17 | Set the `RUST_LOG` environment variable to `info` to show timings informations about the execution. 18 | 19 | ```text 20 | opa-wasm 21 | Evaluates OPA policies compiled as WASM modules 22 | 23 | USAGE: 24 | opa-eval [OPTIONS] --entrypoint <--module |--bundle > 25 | 26 | OPTIONS: 27 | -m, --module Path to the WASM module 28 | -b, --bundle Path to the OPA bundle 29 | -e, --entrypoint Entrypoint to use 30 | -d, --data JSON literal to use as data 31 | -D, --data-path Path to a JSON file to load as data 32 | -i, --input JSON literal to use as input 33 | -I, --input-path Path to a JSON file to load as data 34 | -h, --help Print help information 35 | ``` 36 | 37 | ## As a library 38 | 39 | ```rust,no_run 40 | use std::collections::HashMap; 41 | 42 | use anyhow::Result; 43 | 44 | use opa_wasm::{wasmtime, Runtime}; 45 | 46 | #[tokio::main] 47 | async fn main() -> Result<()> { 48 | // Configure the WASM runtime 49 | let mut config = wasmtime::Config::new(); 50 | config.async_support(true); 51 | 52 | let engine = wasmtime::Engine::new(&config)?; 53 | 54 | // Load the policy WASM module 55 | let module = tokio::fs::read("./policy.wasm").await?; 56 | let module = wasmtime::Module::new(&engine, module)?; 57 | 58 | // Create a store which will hold the module instance 59 | let mut store = wasmtime::Store::new(&engine, ()); 60 | 61 | let data = HashMap::from([("hello", "world")]); 62 | let input = HashMap::from([("message", "world")]); 63 | 64 | // Instantiate the module 65 | let runtime = Runtime::new(&mut store, &module).await?; 66 | 67 | let policy = runtime.with_data(&mut store, &data).await?; 68 | 69 | // Evaluate the policy 70 | let res: serde_json::Value = policy.evaluate(&mut store, "hello/world", &input).await?; 71 | 72 | println!("{}", res); 73 | 74 | Ok(()) 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | allow-unwrap-in-tests = true 2 | allow-expect-in-tests = true 3 | -------------------------------------------------------------------------------- /features.txt: -------------------------------------------------------------------------------- 1 | # List of features flag combinations used for clippy in CI 2 | loader 3 | cli 4 | rng 5 | base64url-builtins 6 | crypto-digest-builtins crypto-md5-builtins 7 | crypto-digest-builtins crypto-sha1-builtins 8 | crypto-digest-builtins crypto-sha2-builtins 9 | crypto-hmac-builtins crypto-md5-builtins 10 | crypto-hmac-builtins crypto-sha1-builtins 11 | crypto-hmac-builtins crypto-sha2-builtins 12 | hex-builtins 13 | semver-builtins 14 | sprintf-builtins 15 | json-builtins 16 | units-builtins 17 | rand-builtins 18 | yaml-builtins 19 | time-builtins 20 | all-crypto-builtins 21 | all-builtins 22 | -------------------------------------------------------------------------------- /src/bin/opa-eval.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![deny(clippy::pedantic)] 16 | 17 | use anyhow::Result; 18 | use camino::Utf8PathBuf; 19 | use clap::{ArgGroup, Parser}; 20 | use opa_wasm::Runtime; 21 | use tracing::Instrument; 22 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry}; 23 | use wasmtime::{Config, Engine, Module, Store}; 24 | 25 | /// Evaluates OPA policies compiled as WASM modules 26 | #[derive(Parser)] 27 | #[clap(group( 28 | ArgGroup::new("policy") 29 | .required(true) 30 | ))] 31 | struct Cli { 32 | /// Path to the WASM module 33 | #[arg(short, long, group = "policy")] 34 | module: Option, 35 | 36 | /// Path to the OPA bundle 37 | #[arg(short, long, group = "policy")] 38 | bundle: Option, 39 | 40 | /// Entrypoint to use 41 | #[arg(short, long)] 42 | entrypoint: String, 43 | 44 | /// JSON literal to use as data 45 | #[arg(short, long = "data", group = "data", value_name = "JSON")] 46 | data_value: Option, 47 | 48 | /// Path to a JSON file to load as data 49 | #[arg(short = 'D', long, group = "data", value_name = "PATH")] 50 | data_path: Option, 51 | 52 | /// JSON literal to use as input 53 | #[arg(short, long = "input", group = "input", value_name = "JSON")] 54 | input_value: Option, 55 | 56 | /// Path to a JSON file to load as input 57 | #[arg(short = 'I', long, group = "input", value_name = "PATH")] 58 | input_path: Option, 59 | } 60 | 61 | #[tokio::main] 62 | async fn main() -> Result<()> { 63 | Registry::default() 64 | .with(tracing_forest::ForestLayer::default()) 65 | .with(EnvFilter::from_default_env()) 66 | .init(); 67 | 68 | let (data, input, module, entrypoint) = (async move { 69 | let cli = Cli::parse(); 70 | 71 | let data = if let Some(path) = cli.data_path { 72 | let content = tokio::fs::read(path).await?; 73 | serde_json::from_slice(&content)? 74 | } else if let Some(data) = cli.data_value { 75 | data 76 | } else { 77 | serde_json::Value::Object(serde_json::Map::default()) 78 | }; 79 | 80 | let input = if let Some(path) = cli.input_path { 81 | let content = tokio::fs::read(path).await?; 82 | serde_json::from_slice(&content)? 83 | } else if let Some(input) = cli.input_value { 84 | input 85 | } else { 86 | serde_json::Value::Object(serde_json::Map::default()) 87 | }; 88 | 89 | let module = if let Some(path) = cli.module { 90 | tokio::fs::read(path) 91 | .instrument(tracing::info_span!("read_module")) 92 | .await? 93 | } else if let Some(path) = cli.bundle { 94 | opa_wasm::read_bundle(path).await? 95 | } else { 96 | // This should be enforced by clap 97 | unreachable!() 98 | }; 99 | 100 | let entrypoint = cli.entrypoint; 101 | Ok::<_, anyhow::Error>((data, input, module, entrypoint)) 102 | }) 103 | .instrument(tracing::info_span!("load_args")) 104 | .await?; 105 | 106 | let (mut store, module) = (async move { 107 | // Configure the WASM runtime 108 | let mut config = Config::new(); 109 | config.async_support(true); 110 | 111 | let engine = Engine::new(&config)?; 112 | 113 | // Load the policy WASM module 114 | let module = Module::new(&engine, module)?; 115 | 116 | // Create a store which will hold the module instance 117 | let store = Store::new(&engine, ()); 118 | Ok::<_, anyhow::Error>((store, module)) 119 | }) 120 | .instrument(tracing::info_span!("compile_module")) 121 | .await?; 122 | 123 | // Instantiate the module 124 | let runtime = Runtime::new(&mut store, &module) 125 | .instrument(tracing::info_span!("instanciate_module")) 126 | .await?; 127 | 128 | let policy = runtime 129 | .with_data(&mut store, &data) 130 | .instrument(tracing::info_span!("load_data")) 131 | .await?; 132 | 133 | // Evaluate the policy 134 | let res: serde_json::Value = policy 135 | .evaluate(&mut store, &entrypoint, &input) 136 | .instrument(tracing::info_span!("evaluate")) 137 | .await?; 138 | 139 | println!("{res}"); 140 | 141 | Ok(()) 142 | } 143 | -------------------------------------------------------------------------------- /src/bin/simple.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![deny(clippy::pedantic)] 16 | 17 | use std::collections::HashMap; 18 | 19 | use anyhow::Result; 20 | use opa_wasm::Runtime; 21 | use wasmtime::{Config, Engine, Module, Store}; 22 | 23 | #[tokio::main] 24 | async fn main() -> Result<()> { 25 | // Configure the WASM runtime 26 | let mut config = Config::new(); 27 | config.async_support(true); 28 | 29 | let engine = Engine::new(&config)?; 30 | 31 | // Load the policy WASM module 32 | let module = tokio::fs::read("./policy.wasm").await?; 33 | let module = Module::new(&engine, module)?; 34 | 35 | // Create a store which will hold the module instance 36 | let mut store = Store::new(&engine, ()); 37 | 38 | let data = HashMap::from([("hello", "world")]); 39 | let input = HashMap::from([("message", "world")]); 40 | 41 | // Instantiate the module 42 | let runtime = Runtime::new(&mut store, &module).await?; 43 | 44 | let policy = runtime.with_data(&mut store, &data).await?; 45 | 46 | // Evaluate the policy 47 | let res: serde_json::Value = policy.evaluate(&mut store, "hello/world", &input).await?; 48 | 49 | println!("{res}"); 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /src/builtins/impls/base64url.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins related to base64 encoding and decoding 16 | 17 | use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; 18 | 19 | /// Serializes the input string into base64url encoding without padding. 20 | #[tracing::instrument] 21 | pub fn encode_no_pad(x: String) -> String { 22 | URL_SAFE_NO_PAD.encode(&x) 23 | } 24 | -------------------------------------------------------------------------------- /src/builtins/impls/crypto.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins related to cryptographic operations 16 | 17 | /// Builtins for computing HMAC signatures 18 | #[cfg(all( 19 | feature = "crypto-hmac-builtins", 20 | any( 21 | feature = "crypto-md5-builtins", 22 | feature = "crypto-sha1-builtins", 23 | feature = "crypto-sha2-builtins" 24 | ) 25 | ))] 26 | pub mod hmac { 27 | use anyhow::Result; 28 | use hmac::{Hmac, Mac}; 29 | 30 | #[cfg(feature = "crypto-md5-builtins")] 31 | /// Returns a string representing the MD5 HMAC of the input message using 32 | /// the input key. 33 | #[tracing::instrument(name = "crypto.hmac.md5", err)] 34 | pub fn md5(x: String, key: String) -> Result { 35 | let mut mac = Hmac::::new_from_slice(key.as_bytes())?; 36 | mac.update(x.as_bytes()); 37 | let res = mac.finalize(); 38 | Ok(hex::encode(res.into_bytes())) 39 | } 40 | 41 | #[cfg(feature = "crypto-sha1-builtins")] 42 | /// Returns a string representing the SHA1 HMAC of the input message using 43 | /// the input key. 44 | #[tracing::instrument(name = "crypto.hmac.sha1", err)] 45 | pub fn sha1(x: String, key: String) -> Result { 46 | let mut mac = Hmac::::new_from_slice(key.as_bytes())?; 47 | mac.update(x.as_bytes()); 48 | let res = mac.finalize(); 49 | Ok(hex::encode(res.into_bytes())) 50 | } 51 | 52 | #[cfg(feature = "crypto-sha2-builtins")] 53 | /// Returns a string representing the SHA256 HMAC of the input message using 54 | /// the input key. 55 | #[tracing::instrument(name = "crypto.hmac.sha256", err)] 56 | pub fn sha256(x: String, key: String) -> Result { 57 | let mut mac = Hmac::::new_from_slice(key.as_bytes())?; 58 | mac.update(x.as_bytes()); 59 | let res = mac.finalize(); 60 | Ok(hex::encode(res.into_bytes())) 61 | } 62 | 63 | #[cfg(feature = "crypto-sha2-builtins")] 64 | /// Returns a string representing the SHA512 HMAC of the input message using 65 | /// the input key. 66 | #[tracing::instrument(name = "crypto.hmac.sha512", err)] 67 | pub fn sha512(x: String, key: String) -> Result { 68 | let mut mac = Hmac::::new_from_slice(key.as_bytes())?; 69 | mac.update(x.as_bytes()); 70 | let res = mac.finalize(); 71 | Ok(hex::encode(res.into_bytes())) 72 | } 73 | } 74 | 75 | /// Builtins for computing hashes 76 | #[cfg(all( 77 | feature = "crypto-digest-builtins", 78 | any( 79 | feature = "crypto-md5-builtins", 80 | feature = "crypto-sha1-builtins", 81 | feature = "crypto-sha2-builtins" 82 | ) 83 | ))] 84 | pub mod digest { 85 | use digest::Digest; 86 | 87 | #[cfg(feature = "crypto-md5-builtins")] 88 | /// Returns a string representing the input string hashed with the MD5 89 | /// function 90 | #[tracing::instrument(name = "crypto.md5")] 91 | pub fn md5(x: String) -> String { 92 | let mut hasher = md5::Md5::new(); 93 | hasher.update(x.as_bytes()); 94 | let res = hasher.finalize(); 95 | hex::encode(res) 96 | } 97 | 98 | #[cfg(all(feature = "crypto-digest-builtins", feature = "crypto-sha1-builtins"))] 99 | /// Returns a string representing the input string hashed with the SHA1 100 | /// function 101 | #[tracing::instrument(name = "crypto.sha1")] 102 | pub fn sha1(x: String) -> String { 103 | let mut hasher = sha1::Sha1::new(); 104 | hasher.update(x.as_bytes()); 105 | let res = hasher.finalize(); 106 | hex::encode(res) 107 | } 108 | 109 | #[cfg(all(feature = "crypto-digest-builtins", feature = "crypto-sha2-builtins"))] 110 | /// Returns a string representing the input string hashed with the SHA256 111 | /// function 112 | #[tracing::instrument(name = "crypto.sha256")] 113 | pub fn sha256(x: String) -> String { 114 | let mut hasher = sha2::Sha256::new(); 115 | hasher.update(x.as_bytes()); 116 | let res = hasher.finalize(); 117 | hex::encode(res) 118 | } 119 | } 120 | 121 | /// Builtins related to X509 certificates, keys and certificate requests parsing 122 | /// and validation 123 | pub mod x509 { 124 | use std::collections::HashMap; 125 | 126 | use anyhow::{bail, Result}; 127 | 128 | /// A X509 certificate 129 | type X509 = HashMap; 130 | 131 | /// A JSON Web Key 132 | type Jwk = HashMap; 133 | 134 | /// Returns one or more certificates from the given string containing PEM or 135 | /// base64 encoded DER certificates after verifying the supplied 136 | /// certificates form a complete certificate chain back to a trusted 137 | /// root. 138 | /// 139 | /// The first certificate is treated as the root and the last is treated as 140 | /// the leaf, with all others being treated as intermediates. 141 | #[tracing::instrument(name = "crypto.x509.parse_and_verify_certificates", err)] 142 | pub fn parse_and_verify_certificates(certs: String) -> Result<(bool, Vec)> { 143 | bail!("not implemented"); 144 | } 145 | 146 | /// Returns a PKCS #10 certificate signing request from the given 147 | /// PEM-encoded PKCS#10 certificate signing request. 148 | #[tracing::instrument(name = "crypto.x509.parse_certificate_request", err)] 149 | pub fn parse_certificate_request(csr: String) -> Result { 150 | bail!("not implemented"); 151 | } 152 | 153 | /// Returns one or more certificates from the given base64 encoded string 154 | /// containing DER encoded certificates that have been concatenated. 155 | #[tracing::instrument(name = "crypto.x509.parse_certificates", err)] 156 | pub fn parse_certificates(certs: String) -> Result> { 157 | bail!("not implemented"); 158 | } 159 | 160 | /// Returns a JWK for signing a JWT from the given PEM-encoded RSA private 161 | /// key. 162 | #[tracing::instrument(name = "crypto.x509.parse_rsa_private_key", err)] 163 | pub fn parse_rsa_private_key(pem: String) -> Result { 164 | bail!("not implemented"); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/builtins/impls/glob.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins used when working with globs. 16 | 17 | use anyhow::{bail, Result}; 18 | 19 | /// Returns a string which represents a version of the pattern where all 20 | /// asterisks have been escaped. 21 | #[tracing::instrument(name = "glob.quote_meta", err)] 22 | pub fn quote_meta(pattern: String) -> Result { 23 | bail!("not implemented"); 24 | } 25 | -------------------------------------------------------------------------------- /src/builtins/impls/graph.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins used to navigate through graph-like structures 16 | 17 | use anyhow::{bail, Result}; 18 | 19 | /// Computes the set of reachable paths in the graph from a set of starting 20 | /// nodes. 21 | #[tracing::instrument(name = "graph.reachable_paths", err)] 22 | pub fn reachable_paths(graph: serde_json::Value) -> Result { 23 | bail!("not implemented"); 24 | } 25 | -------------------------------------------------------------------------------- /src/builtins/impls/graphql.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins related to GraphQL schema and query parsing and validation 16 | 17 | use anyhow::{bail, Result}; 18 | 19 | /// Checks that a GraphQL query is valid against a given schema. 20 | #[tracing::instrument(name = "graphql.is_valid", err)] 21 | pub fn is_valid(query: String, schema: String) -> Result { 22 | bail!("not implemented"); 23 | } 24 | 25 | /// Returns AST objects for a given GraphQL query and schema after validating 26 | /// the query against the schema. Returns undefined if errors were encountered 27 | /// during parsing or validation. 28 | #[tracing::instrument(name = "graphql.parse", err)] 29 | pub fn parse(query: String, schema: String) -> Result<(serde_json::Value, serde_json::Value)> { 30 | bail!("not implemented"); 31 | } 32 | 33 | /// Returns a boolean indicating success or failure alongside the parsed ASTs 34 | /// for a given GraphQL query and schema after validating the query against the 35 | /// schema. 36 | #[tracing::instrument(name = "graphql.parse_and_verify", err)] 37 | pub fn parse_and_verify( 38 | query: String, 39 | schema: String, 40 | ) -> Result<(bool, serde_json::Value, serde_json::Value)> { 41 | bail!("not implemented"); 42 | } 43 | 44 | /// Returns an AST object for a GraphQL query. 45 | #[tracing::instrument(name = "graphql.parse_query", err)] 46 | pub fn parse_query(query: String) -> Result { 47 | bail!("not implemented"); 48 | } 49 | 50 | /// Returns an AST object for a GraphQL schema. 51 | #[tracing::instrument(name = "graphql.parse_schema", err)] 52 | pub fn parse_schema(schema: String) -> Result { 53 | bail!("not implemented"); 54 | } 55 | -------------------------------------------------------------------------------- /src/builtins/impls/hex.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins used for encoding and decoding hex data 16 | 17 | use anyhow::Result; 18 | 19 | /// Deserializes the hex-encoded input string. 20 | #[tracing::instrument(name = "hex.decode", err)] 21 | pub fn decode(x: String) -> Result { 22 | let decoded = hex::decode(x)?; 23 | let str = String::from_utf8(decoded)?; 24 | Ok(str) 25 | } 26 | 27 | /// Serializes the input string using hex-encoding. 28 | #[tracing::instrument(name = "hex.encode")] 29 | pub fn encode(x: String) -> String { 30 | hex::encode(x.as_bytes()) 31 | } 32 | -------------------------------------------------------------------------------- /src/builtins/impls/http.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins used to make HTTP request 16 | 17 | use anyhow::{bail, Result}; 18 | 19 | /// Returns a HTTP response to the given HTTP request. 20 | #[tracing::instrument(name = "http.send", err)] 21 | pub fn send(request: serde_json::Value) -> Result { 22 | bail!("not implemented"); 23 | } 24 | -------------------------------------------------------------------------------- /src/builtins/impls/io.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins related to JWTs 16 | 17 | /// Builtins related to JWT encode/decode and verification/signature 18 | pub mod jwt { 19 | use std::collections::HashMap; 20 | 21 | use anyhow::{bail, Result}; 22 | 23 | /// The headers part of a JWT 24 | type Headers = serde_json::Value; 25 | 26 | /// The payload part of a JWT 27 | type Payload = serde_json::Value; 28 | 29 | /// A JSON Web Key 30 | type Jwk = serde_json::Value; 31 | 32 | /// Decodes a JSON Web Token and outputs it as an object. 33 | #[tracing::instrument(name = "io.jwt.decode", err)] 34 | pub fn decode(jwt: String) -> Result<(Headers, Payload, String)> { 35 | bail!("not implemented"); 36 | } 37 | 38 | /// Verifies a JWT signature under parameterized constraints and decodes the 39 | /// claims if it is valid. 40 | /// 41 | /// Supports the following algorithms: HS256, HS384, HS512, RS256, RS384, 42 | /// RS512, ES256, ES384, ES512, PS256, PS384 and PS512. 43 | #[tracing::instrument(name = "io.jwt.decode_verify", err)] 44 | pub fn decode_verify( 45 | jwt: String, 46 | constraints: HashMap, 47 | ) -> Result<(bool, Headers, Payload)> { 48 | bail!("not implemented"); 49 | } 50 | 51 | /// Encodes and optionally signs a JSON Web Token. Inputs are taken as 52 | /// objects, not encoded strings (see `io.jwt.encode_sign_raw`). 53 | #[tracing::instrument(name = "io.jwt.encode_sign", err)] 54 | pub fn encode_sign( 55 | headers: Headers, 56 | payload: Payload, 57 | key: Jwk, 58 | ) -> Result<(bool, Headers, Payload)> { 59 | bail!("not implemented"); 60 | } 61 | 62 | /// Encodes and optionally signs a JSON Web Token. 63 | #[tracing::instrument(name = "io.jwt.encode_sign_raw", err)] 64 | pub fn encode_sign_raw(headers: String, payload: String, key: String) -> Result { 65 | bail!("not implemented"); 66 | } 67 | 68 | /// Verifies if a ES256 JWT signature is valid. 69 | #[tracing::instrument(name = "io.jwt.verify_es256", err)] 70 | pub fn verify_es256(jwt: String, certificate: String) -> Result { 71 | bail!("not implemented"); 72 | } 73 | 74 | /// Verifies if a ES384 JWT signature is valid. 75 | #[tracing::instrument(name = "io.jwt.verify_es384", err)] 76 | pub fn verify_es384(jwt: String, certificate: String) -> Result { 77 | bail!("not implemented"); 78 | } 79 | 80 | /// Verifies if a ES512 JWT signature is valid. 81 | #[tracing::instrument(name = "io.jwt.verify_es512", err)] 82 | pub fn verify_es512(jwt: String, certificate: String) -> Result { 83 | bail!("not implemented"); 84 | } 85 | 86 | /// Verifies if a HS256 (secret) JWT signature is valid. 87 | #[tracing::instrument(name = "io.jwt.verify_hs256", err)] 88 | pub fn verify_hs256(jwt: String, secret: String) -> Result { 89 | bail!("not implemented"); 90 | } 91 | 92 | /// Verifies if a HS384 (secret) JWT signature is valid. 93 | #[tracing::instrument(name = "io.jwt.verify_hs384", err)] 94 | pub fn verify_hs384(jwt: String, secret: String) -> Result { 95 | bail!("not implemented"); 96 | } 97 | 98 | /// Verifies if a HS512 (secret) JWT signature is valid. 99 | #[tracing::instrument(name = "io.jwt.verify_hs512", err)] 100 | pub fn verify_hs512(jwt: String, secret: String) -> Result { 101 | bail!("not implemented"); 102 | } 103 | 104 | /// Verifies if a PS256 JWT signature is valid. 105 | #[tracing::instrument(name = "io.jwt.verify_ps256", err)] 106 | pub fn verify_ps256(jwt: String, certificate: String) -> Result { 107 | bail!("not implemented"); 108 | } 109 | 110 | /// Verifies if a PS384 JWT signature is valid. 111 | #[tracing::instrument(name = "io.jwt.verify_ps384", err)] 112 | pub fn verify_ps384(jwt: String, certificate: String) -> Result { 113 | bail!("not implemented"); 114 | } 115 | 116 | /// Verifies if a PS512 JWT signature is valid. 117 | #[tracing::instrument(name = "io.jwt.verify_ps512", err)] 118 | pub fn verify_ps512(jwt: String, certificate: String) -> Result { 119 | bail!("not implemented"); 120 | } 121 | 122 | /// Verifies if a RS256 JWT signature is valid. 123 | #[tracing::instrument(name = "io.jwt.verify_rs256", err)] 124 | pub fn verify_rs256(jwt: String, certificate: String) -> Result { 125 | bail!("not implemented"); 126 | } 127 | 128 | /// Verifies if a RS384 JWT signature is valid. 129 | #[tracing::instrument(name = "io.jwt.verify_rs384", err)] 130 | pub fn verify_rs384(jwt: String, certificate: String) -> Result { 131 | bail!("not implemented"); 132 | } 133 | 134 | /// Verifies if a RS512 JWT signature is valid. 135 | #[tracing::instrument(name = "io.jwt.verify_rs512", err)] 136 | pub fn verify_rs512(jwt: String, certificate: String) -> Result { 137 | bail!("not implemented"); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/builtins/impls/json.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins related to JSON objects handling 16 | 17 | use json_patch::Patch; 18 | 19 | /// Patches an object according to RFC6902. 20 | /// For example: `json.patch({"a": {"foo": 1}}, [{"op": "add", "path": "/a/bar", 21 | /// "value": 2}])` results in `{"a": {"foo": 1, "bar": 2}`. The patches are 22 | /// applied atomically: if any of them fails, the result will be undefined. 23 | #[tracing::instrument(name = "json.patch")] 24 | pub fn patch(mut object: serde_json::Value, patch: Patch) -> serde_json::Value { 25 | if json_patch::patch(&mut object, &patch).is_err() { 26 | serde_json::Value::Object(serde_json::Map::default()) 27 | } else { 28 | object 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/builtins/impls/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Implementations of all SDK-dependant builtin functions 16 | 17 | // Arguments are passed by value because of the way the builtin trait works 18 | #![allow(clippy::needless_pass_by_value)] 19 | 20 | use anyhow::{bail, Result}; 21 | 22 | #[cfg(feature = "base64url-builtins")] 23 | pub mod base64url; 24 | pub mod crypto; 25 | pub mod glob; 26 | pub mod graph; 27 | pub mod graphql; 28 | #[cfg(feature = "hex-builtins")] 29 | pub mod hex; 30 | pub mod http; 31 | pub mod io; 32 | #[cfg(feature = "json-builtins")] 33 | pub mod json; 34 | pub mod net; 35 | pub mod object; 36 | pub mod opa; 37 | #[cfg(feature = "rng")] 38 | pub mod rand; 39 | pub mod regex; 40 | pub mod rego; 41 | #[cfg(feature = "semver-builtins")] 42 | pub mod semver; 43 | #[cfg(feature = "time-builtins")] 44 | pub mod time; 45 | #[cfg(feature = "units-builtins")] 46 | pub mod units; 47 | #[cfg(feature = "urlquery-builtins")] 48 | pub mod urlquery; 49 | pub mod uuid; 50 | #[cfg(feature = "yaml-builtins")] 51 | pub mod yaml; 52 | 53 | /// Returns a list of all the indexes of a substring contained inside a string. 54 | #[tracing::instrument(err)] 55 | pub fn indexof_n(string: String, search: String) -> Result> { 56 | bail!("not implemented"); 57 | } 58 | 59 | #[cfg(feature = "sprintf-builtins")] 60 | /// Returns the given string, formatted. 61 | #[tracing::instrument(err)] 62 | pub fn sprintf(format: String, values: Vec) -> Result { 63 | use sprintf::{vsprintf, Printf}; 64 | 65 | let values: Result>, _> = values 66 | .into_iter() 67 | .map(|v| -> Result, _> { 68 | match v { 69 | serde_json::Value::Null => Err(anyhow::anyhow!("can't format null")), 70 | serde_json::Value::Bool(_) => Err(anyhow::anyhow!("can't format a boolean")), 71 | serde_json::Value::Number(n) => { 72 | if let Some(n) = n.as_u64() { 73 | Ok(Box::new(n)) 74 | } else if let Some(n) = n.as_i64() { 75 | Ok(Box::new(n)) 76 | } else if let Some(n) = n.as_f64() { 77 | Ok(Box::new(n)) 78 | } else { 79 | Err(anyhow::anyhow!("unreachable")) 80 | } 81 | } 82 | serde_json::Value::String(s) => Ok(Box::new(s)), 83 | serde_json::Value::Array(_) => Err(anyhow::anyhow!("can't format array")), 84 | serde_json::Value::Object(_) => Err(anyhow::anyhow!("can't format object")), 85 | } 86 | }) 87 | .collect(); 88 | let values = values?; 89 | let values: Vec<&dyn Printf> = values.iter().map(std::convert::AsRef::as_ref).collect(); 90 | vsprintf(&format, &values).map_err(|_| anyhow::anyhow!("failed to call printf")) 91 | } 92 | 93 | /// Emits `note` as a `Note` event in the query explanation. Query explanations 94 | /// show the exact expressions evaluated by OPA during policy execution. For 95 | /// example, `trace("Hello There!")` includes `Note "Hello There!"` in the query 96 | /// explanation. To include variables in the message, use `sprintf`. For 97 | /// example, `person := "Bob"; trace(sprintf("Hello There! %v", [person]))` will 98 | /// emit `Note "Hello There! Bob"` inside of the explanation. 99 | #[tracing::instrument(err)] 100 | pub fn trace(note: String) -> Result { 101 | bail!("not implemented"); 102 | } 103 | -------------------------------------------------------------------------------- /src/builtins/impls/net.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins related to network operations and IP handling 16 | 17 | use std::collections::HashSet; 18 | 19 | use anyhow::{bail, Result}; 20 | 21 | /// Checks if collections of cidrs or ips are contained within another 22 | /// collection of cidrs and returns matches. This function is similar to 23 | /// `net.cidr_contains` except it allows callers to pass collections of CIDRs or 24 | /// IPs as arguments and returns the matches (as opposed to a boolean 25 | /// result indicating a match between two CIDRs/IPs). 26 | #[tracing::instrument(name = "net.cidr_contains_matches", err)] 27 | pub fn cidr_contains_matches( 28 | cidrs: serde_json::Value, 29 | cidrs_or_ips: serde_json::Value, 30 | ) -> Result { 31 | bail!("not implemented"); 32 | } 33 | 34 | /// Expands CIDR to set of hosts (e.g., `net.cidr_expand("192.168.0.0/30")` 35 | /// generates 4 hosts: `{"192.168.0.0", "192.168.0.1", "192.168.0.2", 36 | /// "192.168.0.3"}`). 37 | #[tracing::instrument(name = "net.cidr_expand", err)] 38 | pub fn cidr_expand(cidr: String) -> Result> { 39 | bail!("not implemented"); 40 | } 41 | 42 | /// Merges IP addresses and subnets into the smallest possible list of CIDRs 43 | /// (e.g., `net.cidr_merge(["192.0.128.0/24", "192.0.129.0/24"])` generates 44 | /// `{"192.0.128.0/23"}`. This function merges adjacent subnets where possible, 45 | /// those contained within others and also removes any duplicates. 46 | /// 47 | /// Supports both IPv4 and IPv6 notations. IPv6 inputs need a prefix length 48 | /// (e.g. "/128"). 49 | #[tracing::instrument(name = "net.cidr_merge", err)] 50 | pub fn cidr_merge(addrs: serde_json::Value) -> Result> { 51 | bail!("not implemented"); 52 | } 53 | 54 | /// Returns the set of IP addresses (both v4 and v6) that the passed-in `name` 55 | /// resolves to using the standard name resolution mechanisms available. 56 | #[tracing::instrument(name = "net.lookup_ip_addr", err)] 57 | pub async fn lookup_ip_addr(name: String) -> Result> { 58 | bail!("not implemented"); 59 | } 60 | -------------------------------------------------------------------------------- /src/builtins/impls/object.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins to help handling JSON objects 16 | 17 | use anyhow::{bail, Result}; 18 | 19 | /// Creates a new object that is the asymmetric union of all objects merged from 20 | /// left to right. For example: `object.union_n([{"a": 1}, {"b": 2}, {"a": 3}])` 21 | /// will result in `{"b": 2, "a": 3}`. 22 | #[tracing::instrument(name = "object.union_n", err)] 23 | pub fn union_n(objects: Vec) -> Result { 24 | bail!("not implemented"); 25 | } 26 | -------------------------------------------------------------------------------- /src/builtins/impls/opa.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins related to the current OPA environment 16 | 17 | use std::{collections::HashMap, env}; 18 | 19 | use serde::Serialize; 20 | 21 | /// Metadata about the OPA runtime 22 | #[derive(Serialize)] 23 | pub struct Runtime { 24 | /// A map of the current environment variables 25 | env: HashMap, 26 | /// The version of OPA runtime. This is currently set to an empty string 27 | version: String, 28 | /// The commit hash of the OPA runtime. This is currently set to an empty 29 | commit: String, 30 | } 31 | 32 | /// Returns an object that describes the runtime environment where OPA is 33 | /// deployed. 34 | #[tracing::instrument(name = "opa.runtime")] 35 | pub fn runtime() -> Runtime { 36 | let env = env::vars().collect(); 37 | Runtime { 38 | env, 39 | version: String::new(), 40 | commit: String::new(), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/builtins/impls/rand.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins used to generate pseudo-random values 16 | 17 | use anyhow::{bail, Result}; 18 | use rand::Rng; 19 | 20 | use crate::EvaluationContext; 21 | 22 | /// Returns a random integer between `0` and `n` (`n` exlusive). If `n` is `0`, 23 | /// then `y` is always `0`. For any given argument pair (`str`, `n`), the output 24 | /// will be consistent throughout a query evaluation. 25 | #[tracing::instrument(name = "rand.intn", skip(ctx), err)] 26 | pub fn intn(ctx: &mut C, str: String, n: i64) -> Result { 27 | if n == 0 { 28 | return Ok(0); 29 | } 30 | 31 | if n < 0 { 32 | bail!("rand.intn: n must be a positive integer") 33 | } 34 | 35 | let cache_key = ("rand", str, n); 36 | if let Some(v) = ctx.cache_get(&cache_key)? { 37 | return Ok(v); 38 | }; 39 | 40 | let mut rng = ctx.get_rng(); 41 | let val = rng.gen_range(0..n); 42 | ctx.cache_set(&cache_key, &val)?; 43 | Ok(val) 44 | } 45 | -------------------------------------------------------------------------------- /src/builtins/impls/regex.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins related to regular expressions 16 | 17 | use anyhow::{bail, Result}; 18 | 19 | /// Returns the specified number of matches when matching the input against the 20 | /// pattern. 21 | #[tracing::instrument(name = "regex.find_n", err)] 22 | pub fn find_n(pattern: String, value: String, number: i64) -> Result> { 23 | bail!("not implemented"); 24 | } 25 | 26 | /// Checks if the intersection of two glob-style regular expressions matches a 27 | /// non-empty set of non-empty strings. 28 | /// 29 | /// The set of regex symbols is limited for this builtin: only `.`, `*`, `+`, 30 | /// `[`, `-`, `]` and `\\` are treated as special symbols. 31 | #[tracing::instrument(name = "regex.globs_match", err)] 32 | pub fn globs_match(glob1: String, glob2: String) -> Result { 33 | bail!("not implemented"); 34 | } 35 | 36 | /// Splits the input string by the occurrences of the given pattern. 37 | #[tracing::instrument(name = "regex.split", err)] 38 | pub fn split(pattern: String, value: String) -> Result> { 39 | bail!("not implemented"); 40 | } 41 | 42 | /// Matches a string against a pattern, where there pattern may be glob-like 43 | #[tracing::instrument(name = "regex.template_match", err)] 44 | pub fn template_match( 45 | pattern: String, 46 | value: String, 47 | delimiter_start: String, 48 | delimiter_end: String, 49 | ) -> Result { 50 | bail!("not implemented"); 51 | } 52 | -------------------------------------------------------------------------------- /src/builtins/impls/rego.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins related to Rego parsing 16 | 17 | use anyhow::{bail, Result}; 18 | 19 | /// Parses the input Rego string and returns an object representation of the 20 | /// AST. 21 | #[tracing::instrument(name = "rego.parse_module", err)] 22 | pub fn parse_module(filename: String, rego: String) -> Result { 23 | bail!("not implemented"); 24 | } 25 | -------------------------------------------------------------------------------- /src/builtins/impls/semver.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins related to semver version validation and comparison 16 | 17 | use std::cmp::Ordering; 18 | 19 | use anyhow::Result; 20 | use semver::Version; 21 | 22 | /// Compares valid SemVer formatted version strings. 23 | #[tracing::instrument(name = "semver.compare", err)] 24 | pub fn compare(a: String, b: String) -> Result { 25 | let a = Version::parse(&a)?; 26 | let b = Version::parse(&b)?; 27 | match a.cmp(&b) { 28 | Ordering::Less => Ok(-1), 29 | Ordering::Equal => Ok(0), 30 | Ordering::Greater => Ok(1), 31 | } 32 | } 33 | 34 | /// Validates that the input is a valid SemVer string. 35 | #[tracing::instrument(name = "semver.is_valid")] 36 | pub fn is_valid(vsn: String) -> bool { 37 | Version::parse(&vsn).is_ok() 38 | } 39 | -------------------------------------------------------------------------------- /src/builtins/impls/time.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins for date and time-related operations 16 | 17 | use anyhow::{anyhow, Context, Result}; 18 | use chrono::{DateTime, Datelike, TimeZone, Timelike, Utc, Weekday}; 19 | use chrono_tz::Tz; 20 | use chronoutil::RelativeDuration; 21 | use serde::{Deserialize, Serialize}; 22 | 23 | use crate::EvaluationContext; 24 | 25 | /// A type which olds either a timestamp (in nanoseconds) or a timestamp and a 26 | /// timezone string 27 | #[derive(Serialize, Deserialize, Debug)] 28 | #[serde(untagged)] 29 | pub enum TimestampWithOptionalTimezone { 30 | /// Holds a timestamp 31 | Timestamp(i64), 32 | 33 | /// Holds a timestamp and a timezone 34 | TimestampAndTimezone(i64, String), 35 | } 36 | 37 | impl TimestampWithOptionalTimezone { 38 | /// Converts the timestamp into a [`DateTime`] in the UTC timezone 39 | fn into_datetime(self) -> Result> { 40 | let (ts, tz) = match self { 41 | Self::Timestamp(ts) => (ts, Tz::UTC), 42 | Self::TimestampAndTimezone(ts, tz) => ( 43 | ts, 44 | tz.parse() 45 | .map_err(|e| anyhow!("Could not parse timezone: {}", e))?, 46 | ), 47 | }; 48 | 49 | Ok(tz.timestamp_nanos(ts)) 50 | } 51 | } 52 | 53 | /// Returns the nanoseconds since epoch after adding years, months and days to 54 | /// nanoseconds. `undefined` if the result would be outside the valid time range 55 | /// that can fit within an `int64`. 56 | #[tracing::instrument(name = "time.add_date", err)] 57 | pub fn add_date(ns: i64, years: i32, months: i32, days: i64) -> Result { 58 | let date_time = { 59 | Utc.timestamp_nanos(ns) 60 | + RelativeDuration::years(years) 61 | + RelativeDuration::months(months) 62 | + RelativeDuration::days(days) 63 | }; 64 | 65 | date_time.timestamp_nanos_opt().context("Invalid date") 66 | } 67 | 68 | /// Returns the `[hour, minute, second]` of the day for the nanoseconds since 69 | /// epoch. 70 | #[tracing::instrument(name = "time.clock", err)] 71 | pub fn clock(x: TimestampWithOptionalTimezone) -> Result<(u32, u32, u32)> { 72 | let date_time = x.into_datetime()?; 73 | Ok((date_time.hour(), date_time.minute(), date_time.second())) 74 | } 75 | 76 | /// Returns the `[year, month, day]` for the nanoseconds since epoch. 77 | #[tracing::instrument(name = "time.date", err)] 78 | pub fn date(x: TimestampWithOptionalTimezone) -> Result<(i32, u32, u32)> { 79 | let date_time = x.into_datetime()?; 80 | Ok((date_time.year(), date_time.month(), date_time.day())) 81 | } 82 | 83 | /// Returns the difference between two unix timestamps in nanoseconds (with 84 | /// optional timezone strings). 85 | #[tracing::instrument(name = "time.diff", err)] 86 | // todo:: need to implement 87 | pub fn diff(ns1: serde_json::Value, ns2: serde_json::Value) -> Result<(u8, u8, u8, u8, u8, u8)> { 88 | Err(anyhow!("not implemented")) 89 | } 90 | 91 | /// Returns the current time since epoch in nanoseconds. 92 | #[tracing::instrument(name = "time.now_ns", skip(ctx))] 93 | pub fn now_ns(ctx: &mut C) -> Result { 94 | ctx.now() 95 | .timestamp_nanos_opt() 96 | .context("Timestamp out of range") 97 | } 98 | 99 | /// Returns the duration in nanoseconds represented by a string. 100 | #[tracing::instrument(name = "time.parse_duration_ns", err)] 101 | pub fn parse_duration_ns(duration: String) -> Result { 102 | Ok(duration_str::parse(duration.as_str()) 103 | .map_err(|e| anyhow!("{}", e))? 104 | .as_nanos()) 105 | } 106 | 107 | /// Returns the time in nanoseconds parsed from the string in the given format. 108 | /// `undefined` if the result would be outside the valid time range that can fit 109 | /// within an `int64`. 110 | #[tracing::instrument(name = "time.parse_ns", err)] 111 | pub fn parse_ns(layout: String, value: String) -> Result { 112 | Err(anyhow!("not implemented")) 113 | } 114 | 115 | /// Returns the time in nanoseconds parsed from the string in RFC3339 format. 116 | /// `undefined` if the result would be outside the valid time range that can fit 117 | /// within an `int64`. 118 | #[tracing::instrument(name = "time.parse_rfc3339_ns", err)] 119 | pub fn parse_rfc3339_ns(value: String) -> Result { 120 | DateTime::parse_from_rfc3339(value.as_ref())? 121 | .timestamp_nanos_opt() 122 | .context("Invalid date") 123 | } 124 | 125 | /// Returns the day of the week (Monday, Tuesday, ...) for the nanoseconds since 126 | /// epoch. 127 | #[tracing::instrument(name = "time.weekday", err)] 128 | pub fn weekday(x: TimestampWithOptionalTimezone) -> Result<&'static str> { 129 | let date_time = x.into_datetime()?; 130 | Ok(match date_time.weekday() { 131 | Weekday::Mon => "Monday", 132 | Weekday::Tue => "Tuesday", 133 | Weekday::Wed => "Wednesday", 134 | Weekday::Thu => "Thursday", 135 | Weekday::Fri => "Friday", 136 | Weekday::Sat => "Saturday", 137 | Weekday::Sun => "Sunday", 138 | }) 139 | } 140 | -------------------------------------------------------------------------------- /src/builtins/impls/units.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins to parse and convert units 16 | 17 | use anyhow::{Context, Result}; 18 | use parse_size::Config; 19 | use serde::Serialize; 20 | 21 | /// `UsizeOrFloat` is used to give back either type because per Go OPA, `parse` 22 | /// returns usize _or_ float, and this allows to return multiple types. 23 | #[derive(Serialize)] 24 | #[serde(untagged)] 25 | pub enum UsizeOrFloat { 26 | /// Size 27 | Usize(u64), 28 | /// Size (Float) 29 | Float(f64), 30 | } 31 | 32 | /// Converts strings like "10G", "5K", "4M", "1500m" and the like into a number. 33 | /// This number can be a non-integer, such as 1.5, 0.22, etc. Supports standard 34 | /// metric decimal and binary SI units (e.g., K, Ki, M, Mi, G, Gi etc.) m, K, M, 35 | /// G, T, P, and E are treated as decimal units and Ki, Mi, Gi, Ti, Pi, and Ei 36 | /// are treated as binary units. 37 | /// 38 | /// Note that 'm' and 'M' are case-sensitive, to allow distinguishing between 39 | /// "milli" and "mega" units respectively. Other units are case-insensitive. 40 | #[allow(clippy::cast_precision_loss)] 41 | #[tracing::instrument(name = "units.parse", err)] 42 | pub fn parse(x: String) -> Result { 43 | let p = Config::new().with_decimal(); 44 | // edge case here, when 'm' is lowercase that's mili 45 | if let [init @ .., b'm'] = x.as_bytes() { 46 | return Ok(UsizeOrFloat::Float(p.parse_size(init)? as f64 * 0.001)); 47 | } 48 | Ok(UsizeOrFloat::Usize(p.parse_size(x.as_str())?)) 49 | } 50 | 51 | /// Converts strings like "10GB", "5K", "4mb" into an integer number of bytes. 52 | /// Supports standard byte units (e.g., KB, KiB, etc.) KB, MB, GB, and TB are 53 | /// treated as decimal units and KiB, MiB, GiB, and TiB are treated as binary 54 | /// units. The bytes symbol (b/B) in the unit is optional and omitting it wil 55 | /// give the same result (e.g. Mi and MiB). 56 | #[tracing::instrument(name = "units.parse_bytes", err)] 57 | pub fn parse_bytes(x: String) -> Result { 58 | Config::new() 59 | .with_decimal() 60 | .parse_size(x.as_str()) 61 | .context("could not parse value") 62 | } 63 | -------------------------------------------------------------------------------- /src/builtins/impls/urlquery.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins to encode and decode URL-encoded strings 16 | 17 | use std::collections::BTreeMap; 18 | 19 | use anyhow::Result; 20 | use serde::Deserialize; 21 | 22 | /// A wrapper type which can deserialize either one value or an array of values 23 | #[derive(Deserialize)] 24 | #[serde(untagged)] 25 | pub enum OneOrMany { 26 | /// Represents only one value 27 | One(T), 28 | 29 | /// Represents an array of values 30 | Many(Vec), 31 | } 32 | 33 | impl std::fmt::Debug for OneOrMany { 34 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 35 | match self { 36 | Self::One(t) => t.fmt(f), 37 | Self::Many(v) => v.fmt(f), 38 | } 39 | } 40 | } 41 | 42 | /// Decodes a URL-encoded input string. 43 | #[tracing::instrument(name = "urlquery.decode", err)] 44 | pub fn decode(x: String) -> Result { 45 | Ok(urlencoding::decode(&x)?.into_owned()) 46 | } 47 | 48 | /// Decodes the given URL query string into an object. 49 | #[tracing::instrument(name = "urlquery.decode_object")] 50 | pub fn decode_object(x: String) -> BTreeMap> { 51 | let parsed = form_urlencoded::parse(x.as_bytes()).into_owned(); 52 | let mut decoded_object: BTreeMap> = BTreeMap::new(); 53 | for (k, v) in parsed { 54 | decoded_object.entry(k).or_default().push(v); 55 | } 56 | decoded_object 57 | } 58 | 59 | /// Encodes the input string into a URL-encoded string. 60 | #[tracing::instrument(name = "urlquery.encode")] 61 | pub fn encode(x: String) -> String { 62 | form_urlencoded::byte_serialize(x.as_bytes()).collect() 63 | } 64 | 65 | /// Encodes the given object into a URL encoded query string. 66 | #[tracing::instrument(name = "urlquery.encode_object")] 67 | pub fn encode_object(x: BTreeMap>) -> String { 68 | let mut encoded = form_urlencoded::Serializer::new(String::new()); 69 | 70 | for (key, value) in x { 71 | match value { 72 | OneOrMany::One(value) => { 73 | encoded.append_pair(&key, &value); 74 | } 75 | OneOrMany::Many(values) => { 76 | for value in values { 77 | encoded.append_pair(&key, &value); 78 | } 79 | } 80 | } 81 | } 82 | 83 | encoded.finish() 84 | } 85 | -------------------------------------------------------------------------------- /src/builtins/impls/uuid.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins to generate UUIDs 16 | use anyhow::{bail, Result}; 17 | 18 | /// Returns a new UUIDv4. 19 | #[tracing::instrument(name = "uuid.rfc4122", err)] 20 | pub fn rfc4122(k: String) -> Result { 21 | // note: the semantics required here is to generate a UUID that is similar *for 22 | // the duration of the query for every k* the Go implementation uses a 23 | // global builtin cache so that UUIDs per `k` are stored through a life of a 24 | // query. 25 | bail!("not implemented") 26 | } 27 | -------------------------------------------------------------------------------- /src/builtins/impls/yaml.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Builtins parse and serialize YAML documents 16 | 17 | use anyhow::Result; 18 | use serde_yaml; 19 | 20 | /// Verifies the input string is a valid YAML document. 21 | #[tracing::instrument(name = "yaml.is_valid")] 22 | pub fn is_valid(x: String) -> bool { 23 | let parse: Result = serde_yaml::from_str(&x); 24 | parse.is_ok() 25 | } 26 | 27 | /// Serializes the input term to YAML. 28 | #[tracing::instrument(name = "yaml.marshal", err)] 29 | pub fn marshal(x: serde_yaml::Value) -> Result { 30 | let parse: String = serde_yaml::to_string(&x)?; 31 | Ok(parse) 32 | } 33 | 34 | /// Deserializes the input string. 35 | #[tracing::instrument(name = "yaml.unmarshal", err)] 36 | pub fn unmarshal(x: String) -> Result { 37 | let parse: serde_json::Value = serde_yaml::from_str(&x)?; 38 | Ok(parse) 39 | } 40 | -------------------------------------------------------------------------------- /src/builtins/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Handling of builtin functions. 16 | 17 | use anyhow::{bail, Result}; 18 | 19 | use self::traits::{Builtin, BuiltinFunc}; 20 | use crate::EvaluationContext; 21 | 22 | pub mod impls; 23 | pub mod traits; 24 | 25 | /// Resolve a builtin based on its name 26 | /// 27 | /// # Errors 28 | /// 29 | /// Returns an error if the builtin is not known 30 | #[allow(clippy::too_many_lines)] 31 | pub fn resolve(name: &str) -> Result>> { 32 | match name { 33 | #[cfg(feature = "base64url-builtins")] 34 | "base64url.encode_no_pad" => Ok(self::impls::base64url::encode_no_pad.wrap()), 35 | 36 | #[cfg(all(feature = "crypto-md5-builtins", feature = "crypto-hmac-builtins"))] 37 | "crypto.hmac.md5" => Ok(self::impls::crypto::hmac::md5.wrap()), 38 | 39 | #[cfg(all(feature = "crypto-sha1-builtins", feature = "crypto-hmac-builtins"))] 40 | "crypto.hmac.sha1" => Ok(self::impls::crypto::hmac::sha1.wrap()), 41 | 42 | #[cfg(all(feature = "crypto-sha2-builtins", feature = "crypto-hmac-builtins"))] 43 | "crypto.hmac.sha256" => Ok(self::impls::crypto::hmac::sha256.wrap()), 44 | 45 | #[cfg(all(feature = "crypto-sha2-builtins", feature = "crypto-hmac-builtins"))] 46 | "crypto.hmac.sha512" => Ok(self::impls::crypto::hmac::sha512.wrap()), 47 | 48 | #[cfg(all(feature = "crypto-md5-builtins", feature = "crypto-digest-builtins"))] 49 | "crypto.md5" => Ok(self::impls::crypto::digest::md5.wrap()), 50 | 51 | #[cfg(all(feature = "crypto-sha1-builtins", feature = "crypto-digest-builtins"))] 52 | "crypto.sha1" => Ok(self::impls::crypto::digest::sha1.wrap()), 53 | 54 | #[cfg(all(feature = "crypto-sha2-builtins", feature = "crypto-digest-builtins"))] 55 | "crypto.sha256" => Ok(self::impls::crypto::digest::sha256.wrap()), 56 | 57 | "crypto.x509.parse_and_verify_certificates" => { 58 | Ok(self::impls::crypto::x509::parse_and_verify_certificates.wrap()) 59 | } 60 | "crypto.x509.parse_certificate_request" => { 61 | Ok(self::impls::crypto::x509::parse_certificate_request.wrap()) 62 | } 63 | "crypto.x509.parse_certificates" => { 64 | Ok(self::impls::crypto::x509::parse_certificates.wrap()) 65 | } 66 | "crypto.x509.parse_rsa_private_key" => { 67 | Ok(self::impls::crypto::x509::parse_rsa_private_key.wrap()) 68 | } 69 | "glob.quote_meta" => Ok(self::impls::glob::quote_meta.wrap()), 70 | "graph.reachable_paths" => Ok(self::impls::graph::reachable_paths.wrap()), 71 | "graphql.is_valid" => Ok(self::impls::graphql::is_valid.wrap()), 72 | "graphql.parse" => Ok(self::impls::graphql::parse.wrap()), 73 | "graphql.parse_and_verify" => Ok(self::impls::graphql::parse_and_verify.wrap()), 74 | "graphql.parse_query" => Ok(self::impls::graphql::parse_query.wrap()), 75 | "graphql.parse_schema" => Ok(self::impls::graphql::parse_schema.wrap()), 76 | 77 | #[cfg(feature = "hex-builtins")] 78 | "hex.decode" => Ok(self::impls::hex::decode.wrap()), 79 | 80 | #[cfg(feature = "hex-builtins")] 81 | "hex.encode" => Ok(self::impls::hex::encode.wrap()), 82 | 83 | "http.send" => Ok(self::impls::http::send.wrap()), 84 | "indexof_n" => Ok(self::impls::indexof_n.wrap()), 85 | "io.jwt.decode" => Ok(self::impls::io::jwt::decode.wrap()), 86 | "io.jwt.decode_verify" => Ok(self::impls::io::jwt::decode_verify.wrap()), 87 | "io.jwt.encode_sign" => Ok(self::impls::io::jwt::encode_sign.wrap()), 88 | "io.jwt.encode_sign_raw" => Ok(self::impls::io::jwt::encode_sign_raw.wrap()), 89 | "io.jwt.verify_es256" => Ok(self::impls::io::jwt::verify_es256.wrap()), 90 | "io.jwt.verify_es384" => Ok(self::impls::io::jwt::verify_es384.wrap()), 91 | "io.jwt.verify_es512" => Ok(self::impls::io::jwt::verify_es512.wrap()), 92 | "io.jwt.verify_hs256" => Ok(self::impls::io::jwt::verify_hs256.wrap()), 93 | "io.jwt.verify_hs384" => Ok(self::impls::io::jwt::verify_hs384.wrap()), 94 | "io.jwt.verify_hs512" => Ok(self::impls::io::jwt::verify_hs512.wrap()), 95 | "io.jwt.verify_ps256" => Ok(self::impls::io::jwt::verify_ps256.wrap()), 96 | "io.jwt.verify_ps384" => Ok(self::impls::io::jwt::verify_ps384.wrap()), 97 | "io.jwt.verify_ps512" => Ok(self::impls::io::jwt::verify_ps512.wrap()), 98 | "io.jwt.verify_rs256" => Ok(self::impls::io::jwt::verify_rs256.wrap()), 99 | "io.jwt.verify_rs384" => Ok(self::impls::io::jwt::verify_rs384.wrap()), 100 | "io.jwt.verify_rs512" => Ok(self::impls::io::jwt::verify_rs512.wrap()), 101 | 102 | #[cfg(feature = "json-builtins")] 103 | "json.patch" => Ok(self::impls::json::patch.wrap()), 104 | 105 | "net.cidr_contains_matches" => Ok(self::impls::net::cidr_contains_matches.wrap()), 106 | "net.cidr_expand" => Ok(self::impls::net::cidr_expand.wrap()), 107 | "net.cidr_merge" => Ok(self::impls::net::cidr_merge.wrap()), 108 | "net.lookup_ip_addr" => Ok(self::impls::net::lookup_ip_addr.wrap()), 109 | "object.union_n" => Ok(self::impls::object::union_n.wrap()), 110 | "opa.runtime" => Ok(self::impls::opa::runtime.wrap()), 111 | 112 | #[cfg(feature = "rng")] 113 | "rand.intn" => Ok(self::impls::rand::intn.wrap()), 114 | 115 | "regex.find_n" => Ok(self::impls::regex::find_n.wrap()), 116 | "regex.globs_match" => Ok(self::impls::regex::globs_match.wrap()), 117 | "regex.split" => Ok(self::impls::regex::split.wrap()), 118 | "regex.template_match" => Ok(self::impls::regex::template_match.wrap()), 119 | "rego.parse_module" => Ok(self::impls::rego::parse_module.wrap()), 120 | 121 | #[cfg(feature = "semver-builtins")] 122 | "semver.compare" => Ok(self::impls::semver::compare.wrap()), 123 | 124 | #[cfg(feature = "semver-builtins")] 125 | "semver.is_valid" => Ok(self::impls::semver::is_valid.wrap()), 126 | 127 | #[cfg(feature = "sprintf-builtins")] 128 | "sprintf" => Ok(self::impls::sprintf.wrap()), 129 | 130 | #[cfg(feature = "time-builtins")] 131 | "time.add_date" => Ok(self::impls::time::add_date.wrap()), 132 | 133 | #[cfg(feature = "time-builtins")] 134 | "time.clock" => Ok(self::impls::time::clock.wrap()), 135 | 136 | #[cfg(feature = "time-builtins")] 137 | "time.date" => Ok(self::impls::time::date.wrap()), 138 | 139 | #[cfg(feature = "time-builtins")] 140 | "time.diff" => Ok(self::impls::time::diff.wrap()), 141 | 142 | #[cfg(feature = "time-builtins")] 143 | "time.now_ns" => Ok(self::impls::time::now_ns.wrap()), 144 | 145 | #[cfg(feature = "time-builtins")] 146 | "time.parse_duration_ns" => Ok(self::impls::time::parse_duration_ns.wrap()), 147 | 148 | #[cfg(feature = "time-builtins")] 149 | "time.parse_ns" => Ok(self::impls::time::parse_ns.wrap()), 150 | 151 | #[cfg(feature = "time-builtins")] 152 | "time.parse_rfc3339_ns" => Ok(self::impls::time::parse_rfc3339_ns.wrap()), 153 | 154 | #[cfg(feature = "time-builtins")] 155 | "time.weekday" => Ok(self::impls::time::weekday.wrap()), 156 | 157 | "trace" => Ok(self::impls::trace.wrap()), 158 | 159 | #[cfg(feature = "units-builtins")] 160 | "units.parse" => Ok(self::impls::units::parse.wrap()), 161 | 162 | #[cfg(feature = "units-builtins")] 163 | "units.parse_bytes" => Ok(self::impls::units::parse_bytes.wrap()), 164 | 165 | #[cfg(feature = "urlquery-builtins")] 166 | "urlquery.decode" => Ok(self::impls::urlquery::decode.wrap()), 167 | 168 | #[cfg(feature = "urlquery-builtins")] 169 | "urlquery.decode_object" => Ok(self::impls::urlquery::decode_object.wrap()), 170 | 171 | #[cfg(feature = "urlquery-builtins")] 172 | "urlquery.encode" => Ok(self::impls::urlquery::encode.wrap()), 173 | 174 | #[cfg(feature = "urlquery-builtins")] 175 | "urlquery.encode_object" => Ok(self::impls::urlquery::encode_object.wrap()), 176 | 177 | "uuid.rfc4122" => Ok(self::impls::uuid::rfc4122.wrap()), 178 | 179 | #[cfg(feature = "yaml-builtins")] 180 | "yaml.is_valid" => Ok(self::impls::yaml::is_valid.wrap()), 181 | 182 | #[cfg(feature = "yaml-builtins")] 183 | "yaml.marshal" => Ok(self::impls::yaml::marshal.wrap()), 184 | 185 | #[cfg(feature = "yaml-builtins")] 186 | "yaml.unmarshal" => Ok(self::impls::yaml::unmarshal.wrap()), 187 | _ => bail!("unknown builtin"), 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/builtins/traits.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Traits definitions to help managing builtin functions 16 | 17 | use std::{future::Future, marker::PhantomData, pin::Pin}; 18 | 19 | use anyhow::{Context, Result}; 20 | use serde::{Deserialize, Serialize}; 21 | 22 | use crate::EvaluationContext; 23 | 24 | /// A OPA builtin function 25 | pub trait Builtin: Send + Sync { 26 | /// Call the function, with a list of arguments, each argument being a JSON 27 | /// reprensentation of the parameter value. 28 | fn call<'a>( 29 | &'a self, 30 | context: &'a mut C, 31 | args: &'a [&'a [u8]], 32 | ) -> Pin, anyhow::Error>> + Send + 'a>>; 33 | } 34 | 35 | /// A wrapper around a builtin function with various const markers, to help 36 | /// implement the [`Builtin`] trait 37 | #[derive(Clone)] 38 | struct WrappedBuiltin { 39 | /// The actual function to call 40 | func: F, 41 | 42 | /// Phantom data to help with the type inference 43 | _marker: PhantomData (C, P)>, 44 | } 45 | 46 | impl 47 | Builtin for WrappedBuiltin 48 | where 49 | F: BuiltinFunc, 50 | { 51 | fn call<'a>( 52 | &'a self, 53 | context: &'a mut C, 54 | args: &'a [&'a [u8]], 55 | ) -> Pin, anyhow::Error>> + Send + 'a>> { 56 | self.func.call(context, args) 57 | } 58 | } 59 | 60 | /// A utility trait used to help constructing [`Builtin`]s out of a regular 61 | /// function, abstracting away the parameters deserialization, the return value 62 | /// serialization, for async/non-async variants, and Result/non-Result variants 63 | pub(crate) trait BuiltinFunc< 64 | C: 'static, 65 | const ASYNC: bool, 66 | const RESULT: bool, 67 | const CONTEXT: bool, 68 | P: 'static, 69 | >: Sized + Send + Sync + 'static 70 | { 71 | /// Call the function, with a list of arguments, each argument being a JSON 72 | /// reprensentation of the parameter value. 73 | fn call<'a>( 74 | &'a self, 75 | context: &'a mut C, 76 | args: &'a [&'a [u8]], 77 | ) -> Pin, anyhow::Error>> + Send + 'a>>; 78 | 79 | /// Wrap the function into a [`Builtin`] trait object 80 | fn wrap(self) -> Box> { 81 | Box::new(WrappedBuiltin { 82 | func: self, 83 | _marker: PhantomData, 84 | }) 85 | } 86 | } 87 | 88 | /// A macro to count the number of items 89 | macro_rules! count { 90 | () => (0usize); 91 | ( $x:tt $($xs:tt)* ) => (1usize + count!($($xs)*)); 92 | } 93 | 94 | /// A macro to process a builtin return type, based on whether it's an async 95 | /// function and if it returns a [`Result`] or not. 96 | macro_rules! unwrap { 97 | ($tok:expr, result = true, async = true) => { 98 | $tok.await? 99 | }; 100 | ($tok:expr, result = true, async = false) => { 101 | $tok? 102 | }; 103 | ($tok:expr, result = false, async = true) => { 104 | $tok.await 105 | }; 106 | ($tok:expr, result = false, async = false) => { 107 | $tok 108 | }; 109 | } 110 | 111 | /// A helper macro used by the [`trait_impl`] macro to generate the right 112 | /// function call, depending on whether the function takes a context or not 113 | macro_rules! call { 114 | ($self:ident, $ctx:expr, ($($pname:ident),*), context = true) => { 115 | $self($ctx, $($pname),*) 116 | }; 117 | ($self:ident, $ctx:expr, ($($pname:ident),*), context = false) => { 118 | { 119 | let _ctx = $ctx; 120 | $self($($pname),*) 121 | } 122 | }; 123 | } 124 | 125 | /// A helper macro used by the [`trait_impl`] macro to generate the body of the 126 | /// call method in the trait 127 | macro_rules! trait_body { 128 | (($($pname:ident: $ptype:ident),*), async = $async:tt, result = $result:tt, context = $context:tt) => { 129 | fn call<'a>( 130 | &'a self, 131 | context: &'a mut C, 132 | args: &'a [&'a [u8]], 133 | ) -> Pin, anyhow::Error>> + Send + 'a>> { 134 | Box::pin(async move { 135 | let [$($pname),*]: [&'a [u8]; count!($($pname)*)] = 136 | args.try_into().ok().context("invalid arguments")?; 137 | $( 138 | let $pname: $ptype = serde_json::from_slice($pname) 139 | .context(concat!("failed to convert ", stringify!($pname), " argument"))?; 140 | )* 141 | let res = call!(self, context, ($($pname),*), context = $context); 142 | let res = unwrap!(res, result = $result, async = $async); 143 | let res = serde_json::to_vec(&res).context("could not serialize result")?; 144 | Ok(res) 145 | }) 146 | } 147 | }; 148 | } 149 | 150 | /// A macro which implements the [`BuiltinFunc`] trait for a given number of 151 | /// parameters 152 | macro_rules! trait_impl { 153 | ($($pname:ident: $ptype:ident),*) => { 154 | // Implementation for a non-async, non-result function, without context 155 | impl BuiltinFunc for F 156 | where 157 | C: EvaluationContext, 158 | F: Fn($($ptype),*) -> R + Send + Sync + 'static, 159 | $( 160 | $ptype: for<'de> Deserialize<'de> + Send + 'static, 161 | )* 162 | R: Serialize + Send + 'static, 163 | { 164 | trait_body! { 165 | ($($pname: $ptype),*), 166 | async = false, 167 | result = false, 168 | context = false 169 | } 170 | } 171 | 172 | // Implementation for a non-async, result function, without context 173 | impl BuiltinFunc for F 174 | where 175 | C: EvaluationContext, 176 | F: Fn($($ptype),*) -> Result + Send + Sync + 'static, 177 | $( 178 | $ptype: for<'de> Deserialize<'de> + Send + 'static, 179 | )* 180 | R: Serialize + Send + 'static, 181 | E: 'static, 182 | anyhow::Error: From, 183 | { 184 | trait_body! { 185 | ($($pname: $ptype),*), 186 | async = false, 187 | result = true, 188 | context = false 189 | } 190 | } 191 | 192 | // Implementation for an async, non-result function, without context 193 | impl BuiltinFunc for F 194 | where 195 | C: EvaluationContext, 196 | F: Fn($($ptype),*) -> Fut + Send + Sync + 'static, 197 | $( 198 | $ptype: for<'de> Deserialize<'de> + Send + 'static, 199 | )* 200 | R: Serialize + 'static, 201 | Fut: Future + Send, 202 | { 203 | trait_body! { 204 | ($($pname: $ptype),*), 205 | async = true, 206 | result = false, 207 | context = false 208 | } 209 | } 210 | 211 | // Implementation for an async, result function, without context 212 | impl BuiltinFunc for F 213 | where 214 | C: EvaluationContext, 215 | F: Fn($($ptype),*) -> Fut + Send + Sync + 'static, 216 | $( 217 | $ptype: for<'de> Deserialize<'de> + Send + 'static, 218 | )* 219 | R: Serialize + 'static, 220 | E: 'static, 221 | anyhow::Error: From, 222 | Fut: Future> + Send, 223 | { 224 | trait_body! { 225 | ($($pname: $ptype),*), 226 | async = true, 227 | result = true, 228 | context = false 229 | } 230 | } 231 | // 232 | // Implementation for a non-async, non-result function, with context 233 | impl BuiltinFunc for F 234 | where 235 | C: EvaluationContext, 236 | F: Fn(&mut C, $($ptype),*) -> R + Send + Sync + 'static, 237 | $( 238 | $ptype: for<'de> Deserialize<'de> + Send + 'static, 239 | )* 240 | R: Serialize + Send + 'static, 241 | { 242 | trait_body! { 243 | ($($pname: $ptype),*), 244 | async = false, 245 | result = false, 246 | context = true 247 | } 248 | } 249 | 250 | // Implementation for a non-async, result function, with context 251 | impl BuiltinFunc for F 252 | where 253 | C: EvaluationContext, 254 | F: Fn(&mut C, $($ptype),*) -> Result + Send + Sync + 'static, 255 | $( 256 | $ptype: for<'de> Deserialize<'de> + Send + 'static, 257 | )* 258 | R: Serialize + Send + 'static, 259 | E: 'static, 260 | anyhow::Error: From, 261 | { 262 | trait_body! { 263 | ($($pname: $ptype),*), 264 | async = false, 265 | result = true, 266 | context = true 267 | } 268 | } 269 | 270 | // Implementation for an async, non-result function, with context 271 | impl BuiltinFunc for F 272 | where 273 | C: EvaluationContext, 274 | F: Fn(&mut C, $($ptype),*) -> Fut + Send + Sync + 'static, 275 | $( 276 | $ptype: for<'de> Deserialize<'de> + Send + 'static, 277 | )* 278 | R: Serialize + 'static, 279 | Fut: Future + Send, 280 | { 281 | trait_body! { 282 | ($($pname: $ptype),*), 283 | async = true, 284 | result = false, 285 | context = true 286 | } 287 | } 288 | 289 | // Implementation for an async, result function, with context 290 | impl BuiltinFunc for F 291 | where 292 | C: EvaluationContext, 293 | F: Fn(&mut C, $($ptype),*) -> Fut + Send + Sync + 'static, 294 | $( 295 | $ptype: for<'de> Deserialize<'de> + Send + 'static, 296 | )* 297 | R: Serialize + 'static, 298 | E: 'static, 299 | anyhow::Error: From, 300 | Fut: Future> + Send, 301 | { 302 | trait_body! { 303 | ($($pname: $ptype),*), 304 | async = true, 305 | result = true, 306 | context = true 307 | } 308 | } 309 | } 310 | } 311 | 312 | trait_impl!(); 313 | trait_impl!(first: P1); 314 | trait_impl!(first: P1, second: P2); 315 | trait_impl!(first: P1, second: P2, third: P3); 316 | trait_impl!(first: P1, second: P2, third: P3, fourth: P4); 317 | 318 | #[cfg(test)] 319 | mod tests { 320 | use super::*; 321 | use crate::DefaultContext; 322 | 323 | #[tokio::test] 324 | async fn builtins_call() { 325 | let mut ctx = DefaultContext::default(); 326 | let uppercase = |foo: String| foo.to_uppercase(); 327 | let uppercase: Box> = uppercase.wrap(); 328 | let args = [b"\"hello\"" as &[u8]]; 329 | let result = uppercase.call(&mut ctx, &args[..]).await.unwrap(); 330 | assert_eq!(result, b"\"HELLO\""); 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Trait definition for the context passed through builtin evaluation 16 | 17 | #![allow(clippy::module_name_repetitions)] 18 | 19 | use std::collections::HashMap; 20 | 21 | use anyhow::Result; 22 | #[cfg(feature = "time")] 23 | use chrono::TimeZone; 24 | use serde::{de::DeserializeOwned, Serialize}; 25 | 26 | /// Context passed through builtin evaluation 27 | pub trait EvaluationContext: Send + 'static { 28 | /// The type of random number generator used by this context 29 | #[cfg(feature = "rng")] 30 | type Rng: rand::Rng; 31 | 32 | /// Get a [`rand::Rng`] 33 | #[cfg(feature = "rng")] 34 | fn get_rng(&mut self) -> Self::Rng; 35 | 36 | /// Get the current date and time 37 | #[cfg(feature = "time")] 38 | fn now(&self) -> chrono::DateTime; 39 | 40 | /// Notify the context on evaluation start, so it can clean itself up 41 | fn evaluation_start(&mut self); 42 | 43 | /// Get a value from the evaluation cache 44 | /// 45 | /// # Errors 46 | /// 47 | /// If the key failed to serialize, or the value failed to deserialize 48 | fn cache_get(&mut self, key: &K) -> Result>; 49 | 50 | /// Push a value to the evaluation cache 51 | /// 52 | /// # Errors 53 | /// 54 | /// If the key or the value failed to serialize 55 | fn cache_set(&mut self, key: &K, content: &C) -> Result<()>; 56 | } 57 | 58 | /// The default evaluation context implementation 59 | pub struct DefaultContext { 60 | /// The cache used to store values during evaluation 61 | cache: HashMap, 62 | 63 | /// The time at which the evaluation started 64 | #[cfg(feature = "time")] 65 | evaluation_time: chrono::DateTime, 66 | } 67 | 68 | #[allow(clippy::derivable_impls)] 69 | impl Default for DefaultContext { 70 | fn default() -> Self { 71 | Self { 72 | cache: HashMap::new(), 73 | 74 | #[cfg(feature = "time")] 75 | evaluation_time: chrono::Utc.timestamp_nanos(0), 76 | } 77 | } 78 | } 79 | 80 | impl EvaluationContext for DefaultContext { 81 | #[cfg(feature = "rng")] 82 | type Rng = rand::rngs::ThreadRng; 83 | 84 | #[cfg(feature = "rng")] 85 | fn get_rng(&mut self) -> Self::Rng { 86 | rand::thread_rng() 87 | } 88 | 89 | #[cfg(feature = "time")] 90 | fn now(&self) -> chrono::DateTime { 91 | self.evaluation_time 92 | } 93 | 94 | fn evaluation_start(&mut self) { 95 | // Clear the cache 96 | self.cache = HashMap::new(); 97 | 98 | #[cfg(feature = "time")] 99 | { 100 | // Set the evaluation time to now 101 | self.evaluation_time = chrono::Utc::now(); 102 | } 103 | } 104 | 105 | fn cache_get(&mut self, key: &K) -> Result> { 106 | let key = serde_json::to_string(&key)?; 107 | let Some(value) = self.cache.get(&key) else { 108 | return Ok(None); 109 | }; 110 | 111 | let value = serde_json::from_value(value.clone())?; 112 | Ok(value) 113 | } 114 | 115 | fn cache_set(&mut self, key: &K, content: &C) -> Result<()> { 116 | let key = serde_json::to_string(key)?; 117 | let content = serde_json::to_value(content)?; 118 | self.cache.insert(key, content); 119 | Ok(()) 120 | } 121 | } 122 | 123 | /// Test utilities 124 | pub mod tests { 125 | use anyhow::Result; 126 | #[cfg(feature = "time")] 127 | use chrono::TimeZone; 128 | use serde::{de::DeserializeOwned, Serialize}; 129 | 130 | use crate::{DefaultContext, EvaluationContext}; 131 | 132 | /// A context used in tests 133 | pub struct TestContext { 134 | /// The inner [`DefaultContext`] 135 | inner: DefaultContext, 136 | 137 | /// The mocked time 138 | #[cfg(feature = "time")] 139 | clock: chrono::DateTime, 140 | 141 | /// The seed used for the random number generator 142 | #[cfg(feature = "rng")] 143 | seed: u64, 144 | } 145 | 146 | #[allow(clippy::derivable_impls)] 147 | impl Default for TestContext { 148 | fn default() -> Self { 149 | Self { 150 | inner: DefaultContext::default(), 151 | 152 | #[cfg(feature = "time")] 153 | clock: chrono::Utc 154 | // Corresponds to 2020-07-14T12:53:22Z 155 | // We're using this method because it's available on old versions of chrono 156 | .timestamp_opt(1_594_731_202, 0) 157 | .unwrap(), 158 | 159 | #[cfg(feature = "rng")] 160 | seed: 0, 161 | } 162 | } 163 | } 164 | 165 | impl EvaluationContext for TestContext { 166 | #[cfg(feature = "rng")] 167 | type Rng = rand::rngs::StdRng; 168 | 169 | fn evaluation_start(&mut self) { 170 | self.inner.evaluation_start(); 171 | } 172 | 173 | #[cfg(feature = "time")] 174 | fn now(&self) -> chrono::DateTime { 175 | self.clock 176 | } 177 | 178 | #[cfg(feature = "rng")] 179 | fn get_rng(&mut self) -> Self::Rng { 180 | use rand::SeedableRng; 181 | 182 | rand::rngs::StdRng::seed_from_u64(self.seed) 183 | } 184 | 185 | fn cache_get(&mut self, key: &K) -> Result> { 186 | self.inner.cache_get(key) 187 | } 188 | 189 | fn cache_set(&mut self, key: &K, content: &C) -> Result<()> { 190 | self.inner.cache_set(key, content) 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/funcs.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Typed functions exported by the OPA WASM module 16 | 17 | use anyhow::{Context, Result}; 18 | use wasmtime::{AsContextMut, Caller, Instance, Memory, TypedFunc}; 19 | 20 | use crate::types::{Addr, Ctx, EntrypointId, Heap, NulStr, OpaError, Value}; 21 | 22 | /// Get a [`TypedFunc`] for the given export name from a wasmtime [`Caller`] 23 | fn from_caller( 24 | name: &'static str, 25 | caller: &mut Caller<'_, T>, 26 | ) -> Result> 27 | where 28 | Params: wasmtime::WasmParams, 29 | Results: wasmtime::WasmResults, 30 | { 31 | caller 32 | .get_export(name) 33 | .with_context(|| format!("could not find export {name:?}"))? 34 | .into_func() 35 | .with_context(|| format!("export {name:?} is not a function"))? 36 | .typed(caller) 37 | .with_context(|| format!("exported function {name:?} does not have the right signature")) 38 | } 39 | 40 | /// Get a [`TypedFunc`] for the given export name from a wasmtime [`Instance`] 41 | fn from_instance( 42 | name: &'static str, 43 | mut store: impl AsContextMut, 44 | instance: &Instance, 45 | ) -> Result> 46 | where 47 | Params: wasmtime::WasmParams, 48 | Results: wasmtime::WasmResults, 49 | { 50 | instance 51 | .get_export(&mut store, name) 52 | .with_context(|| format!("could not find export {name:?}"))? 53 | .into_func() 54 | .with_context(|| format!("export {name:?} is not a function"))? 55 | .typed(&mut store) 56 | .with_context(|| format!("exported function {name:?} does not have the right signature")) 57 | } 58 | 59 | /// A helper trait which helps extracting a WASM function to be called from a 60 | /// Rust context 61 | pub trait Func: Sized { 62 | /// The name of the WASM export to extract 63 | const EXPORT: &'static str; 64 | /// The type of the parameters of the function 65 | type Params: wasmtime::WasmParams; 66 | /// The type of the results of the function 67 | type Results: wasmtime::WasmResults; 68 | 69 | /// Create a new instance of the function from a `TypedFunc` 70 | fn from_func(func: TypedFunc) -> Self; 71 | 72 | /// Create a new instance of the function from a wasmtime [`Caller`] 73 | fn from_caller(caller: &mut Caller<'_, T>) -> Result { 74 | Ok(Self::from_func(from_caller(Self::EXPORT, caller)?)) 75 | } 76 | 77 | /// Create a new instance of the function from a wasmtime [`Instance`] 78 | fn from_instance(store: impl AsContextMut, instance: &Instance) -> Result { 79 | Ok(Self::from_func(from_instance( 80 | Self::EXPORT, 81 | store, 82 | instance, 83 | )?)) 84 | } 85 | } 86 | 87 | /// `i32 eval(ctx_addr)` 88 | pub struct Eval(TypedFunc); 89 | 90 | impl Func for Eval { 91 | const EXPORT: &'static str = "eval"; 92 | type Params = i32; 93 | type Results = i32; 94 | 95 | fn from_func(func: TypedFunc) -> Self { 96 | Self(func) 97 | } 98 | } 99 | 100 | impl Eval { 101 | #[tracing::instrument(name = "eval", skip_all, err)] 102 | pub async fn call( 103 | &self, 104 | store: impl AsContextMut, 105 | ctx: &Ctx, 106 | ) -> Result { 107 | let res = self.0.call_async(store, ctx.0).await?; 108 | Ok(res) 109 | } 110 | } 111 | 112 | /// `value_addr builtins()` 113 | pub struct Builtins(TypedFunc<(), i32>); 114 | 115 | impl Func for Builtins { 116 | const EXPORT: &'static str = "builtins"; 117 | type Params = (); 118 | type Results = i32; 119 | 120 | fn from_func(func: TypedFunc) -> Self { 121 | Self(func) 122 | } 123 | } 124 | 125 | impl Builtins { 126 | #[tracing::instrument(name = "builtins", skip_all, err)] 127 | pub async fn call(&self, store: impl AsContextMut) -> Result { 128 | let res = self.0.call_async(store, ()).await?; 129 | Ok(Value(res)) 130 | } 131 | } 132 | 133 | /// `value_addr entrypoints()` 134 | pub struct Entrypoints(TypedFunc<(), i32>); 135 | 136 | impl Func for Entrypoints { 137 | const EXPORT: &'static str = "entrypoints"; 138 | type Params = (); 139 | type Results = i32; 140 | 141 | fn from_func(func: TypedFunc) -> Self { 142 | Self(func) 143 | } 144 | } 145 | 146 | impl Entrypoints { 147 | #[tracing::instrument(name = "entrypoints", skip_all, err)] 148 | pub async fn call(&self, store: impl AsContextMut) -> Result { 149 | let res = self.0.call_async(store, ()).await?; 150 | Ok(Value(res)) 151 | } 152 | } 153 | 154 | /// `ctx_addr opa_eval_ctx_new(void)` 155 | pub struct OpaEvalCtxNew(TypedFunc<(), i32>); 156 | 157 | impl Func for OpaEvalCtxNew { 158 | const EXPORT: &'static str = "opa_eval_ctx_new"; 159 | type Params = (); 160 | type Results = i32; 161 | 162 | fn from_func(func: TypedFunc) -> Self { 163 | Self(func) 164 | } 165 | } 166 | 167 | impl OpaEvalCtxNew { 168 | #[tracing::instrument(name = "opa_eval_ctx_new", skip_all, err)] 169 | pub async fn call(&self, store: impl AsContextMut) -> Result { 170 | let res = self.0.call_async(store, ()).await?; 171 | Ok(Ctx(res)) 172 | } 173 | } 174 | 175 | /// `void opa_eval_ctx_set_input(ctx_addr, value_addr)` 176 | pub struct OpaEvalCtxSetInput(TypedFunc<(i32, i32), ()>); 177 | 178 | impl Func for OpaEvalCtxSetInput { 179 | const EXPORT: &'static str = "opa_eval_ctx_set_input"; 180 | type Params = (i32, i32); 181 | type Results = (); 182 | 183 | fn from_func(func: TypedFunc) -> Self { 184 | Self(func) 185 | } 186 | } 187 | 188 | impl OpaEvalCtxSetInput { 189 | #[tracing::instrument(name = "opa_eval_ctx_set_input", skip_all, err)] 190 | pub async fn call( 191 | &self, 192 | store: impl AsContextMut, 193 | ctx: &Ctx, 194 | input: &Value, 195 | ) -> Result<()> { 196 | self.0.call_async(store, (ctx.0, input.0)).await?; 197 | Ok(()) 198 | } 199 | } 200 | 201 | /// `void opa_eval_ctx_set_data(ctx_addr, value_addr)` 202 | pub struct OpaEvalCtxSetData(TypedFunc<(i32, i32), ()>); 203 | 204 | impl Func for OpaEvalCtxSetData { 205 | const EXPORT: &'static str = "opa_eval_ctx_set_data"; 206 | type Params = (i32, i32); 207 | type Results = (); 208 | 209 | fn from_func(func: TypedFunc) -> Self { 210 | Self(func) 211 | } 212 | } 213 | 214 | impl OpaEvalCtxSetData { 215 | #[tracing::instrument(name = "opa_eval_ctx_set_data", skip_all, err)] 216 | pub async fn call( 217 | &self, 218 | store: impl AsContextMut, 219 | ctx: &Ctx, 220 | data: &Value, 221 | ) -> Result<()> { 222 | self.0.call_async(store, (ctx.0, data.0)).await?; 223 | Ok(()) 224 | } 225 | } 226 | 227 | /// `void opa_eval_ctx_set_entrypoint(ctx_addr, entrypoint_id)` 228 | pub struct OpaEvalCtxSetEntrypoint(TypedFunc<(i32, i32), ()>); 229 | 230 | impl Func for OpaEvalCtxSetEntrypoint { 231 | const EXPORT: &'static str = "opa_eval_ctx_set_entrypoint"; 232 | type Params = (i32, i32); 233 | type Results = (); 234 | 235 | fn from_func(func: TypedFunc) -> Self { 236 | Self(func) 237 | } 238 | } 239 | 240 | impl OpaEvalCtxSetEntrypoint { 241 | #[tracing::instrument(name = "opa_eval_ctx_set_entrypoint", skip_all, err)] 242 | pub async fn call( 243 | &self, 244 | store: impl AsContextMut, 245 | ctx: &Ctx, 246 | entrypoint: &EntrypointId, 247 | ) -> Result<()> { 248 | self.0.call_async(store, (ctx.0, entrypoint.0)).await?; 249 | Ok(()) 250 | } 251 | } 252 | 253 | /// `value_addr opa_eval_ctx_get_result(ctx_addr)` 254 | pub struct OpaEvalCtxGetResult(TypedFunc); 255 | 256 | impl Func for OpaEvalCtxGetResult { 257 | const EXPORT: &'static str = "opa_eval_ctx_get_result"; 258 | type Params = i32; 259 | type Results = i32; 260 | 261 | fn from_func(func: TypedFunc) -> Self { 262 | Self(func) 263 | } 264 | } 265 | 266 | impl OpaEvalCtxGetResult { 267 | #[tracing::instrument(name = "opa_eval_ctx_get_result", skip_all, err)] 268 | pub async fn call( 269 | &self, 270 | store: impl AsContextMut, 271 | ctx: &Ctx, 272 | ) -> Result { 273 | let res = self.0.call_async(store, ctx.0).await?; 274 | Ok(Value(res)) 275 | } 276 | } 277 | 278 | /// `addr opa_malloc(int32 size)` 279 | pub struct OpaMalloc(TypedFunc); 280 | 281 | impl Func for OpaMalloc { 282 | const EXPORT: &'static str = "opa_malloc"; 283 | type Params = i32; 284 | type Results = i32; 285 | 286 | fn from_func(func: TypedFunc) -> Self { 287 | Self(func) 288 | } 289 | } 290 | 291 | impl OpaMalloc { 292 | #[tracing::instrument(name = "opa_malloc", skip_all, err)] 293 | pub async fn call( 294 | &self, 295 | store: impl AsContextMut, 296 | len: usize, 297 | ) -> Result { 298 | let len = len.try_into().context("invalid parameter")?; 299 | let ptr = self.0.call_async(store, len).await?; 300 | Ok(Heap { 301 | ptr, 302 | len, 303 | freed: false, 304 | }) 305 | } 306 | } 307 | 308 | /// `void opa_free(addr)` 309 | pub struct OpaFree(TypedFunc); 310 | 311 | impl Func for OpaFree { 312 | const EXPORT: &'static str = "opa_free"; 313 | type Params = i32; 314 | type Results = (); 315 | 316 | fn from_func(func: TypedFunc) -> Self { 317 | Self(func) 318 | } 319 | } 320 | 321 | impl OpaFree { 322 | #[tracing::instrument(name = "opa_free", skip_all, err)] 323 | pub async fn call( 324 | &self, 325 | store: impl AsContextMut, 326 | mut heap: Heap, 327 | ) -> Result<()> { 328 | self.0.call_async(store, heap.ptr).await?; 329 | heap.freed = true; 330 | drop(heap); 331 | Ok(()) 332 | } 333 | } 334 | 335 | /// `value_addr opa_json_parse(str_addr, size)` 336 | pub struct OpaJsonParse(TypedFunc<(i32, i32), i32>); 337 | 338 | impl Func for OpaJsonParse { 339 | const EXPORT: &'static str = "opa_json_parse"; 340 | type Params = (i32, i32); 341 | type Results = i32; 342 | 343 | fn from_func(func: TypedFunc) -> Self { 344 | Self(func) 345 | } 346 | } 347 | 348 | impl OpaJsonParse { 349 | #[tracing::instrument(name = "opa_json_parse", skip_all, err)] 350 | pub async fn call( 351 | &self, 352 | store: impl AsContextMut, 353 | heap: &Heap, 354 | ) -> Result { 355 | let res = self.0.call_async(store, (heap.ptr, heap.len)).await?; 356 | Ok(Value(res)) 357 | } 358 | } 359 | 360 | /// `value_addr opa_value_parse(str_addr, size)` 361 | pub struct OpaValueParse(TypedFunc<(i32, i32), i32>); 362 | 363 | impl Func for OpaValueParse { 364 | const EXPORT: &'static str = "opa_value_parse"; 365 | type Params = (i32, i32); 366 | type Results = i32; 367 | 368 | fn from_func(func: TypedFunc) -> Self { 369 | Self(func) 370 | } 371 | } 372 | 373 | impl OpaValueParse { 374 | #[allow(dead_code)] 375 | #[tracing::instrument(name = "opa_value_parse", skip_all, err)] 376 | pub async fn call( 377 | &self, 378 | store: impl AsContextMut, 379 | heap: &Heap, 380 | ) -> Result { 381 | let res = self.0.call_async(store, (heap.ptr, heap.len)).await?; 382 | Ok(Value(res)) 383 | } 384 | } 385 | 386 | /// `str_addr opa_json_dump(value_addr)` 387 | pub struct OpaJsonDump(TypedFunc); 388 | 389 | impl Func for OpaJsonDump { 390 | const EXPORT: &'static str = "opa_json_dump"; 391 | type Params = i32; 392 | type Results = i32; 393 | 394 | fn from_func(func: TypedFunc) -> Self { 395 | Self(func) 396 | } 397 | } 398 | 399 | impl OpaJsonDump { 400 | #[tracing::instrument(name = "opa_json_dump", skip_all, err)] 401 | pub async fn call( 402 | &self, 403 | store: impl AsContextMut, 404 | value: &Value, 405 | ) -> Result { 406 | let res = self.0.call_async(store, value.0).await?; 407 | Ok(NulStr(res)) 408 | } 409 | 410 | /// Decode the JSON value at the given memory pointer 411 | pub async fn decode serde::Deserialize<'de>, T: Send>( 412 | &self, 413 | mut store: impl AsContextMut, 414 | memory: &Memory, 415 | value: &Value, 416 | ) -> Result { 417 | let json = self.call(&mut store, value).await?; 418 | let json = json.read(&store, memory)?; 419 | let json = serde_json::from_slice(json.to_bytes())?; 420 | Ok(json) 421 | } 422 | } 423 | 424 | /// `void opa_heap_ptr_set(addr)` 425 | pub struct OpaHeapPtrSet(TypedFunc); 426 | 427 | impl Func for OpaHeapPtrSet { 428 | const EXPORT: &'static str = "opa_heap_ptr_set"; 429 | type Params = i32; 430 | type Results = (); 431 | 432 | fn from_func(func: TypedFunc) -> Self { 433 | Self(func) 434 | } 435 | } 436 | 437 | impl OpaHeapPtrSet { 438 | #[tracing::instrument(name = "opa_heap_ptr_set", skip_all, err)] 439 | pub async fn call( 440 | &self, 441 | store: impl AsContextMut, 442 | addr: &Addr, 443 | ) -> Result<()> { 444 | self.0.call_async(store, addr.0).await?; 445 | Ok(()) 446 | } 447 | } 448 | 449 | /// `addr opa_heap_ptr_get()` 450 | pub struct OpaHeapPtrGet(TypedFunc<(), i32>); 451 | 452 | impl Func for OpaHeapPtrGet { 453 | const EXPORT: &'static str = "opa_heap_ptr_get"; 454 | type Params = (); 455 | type Results = i32; 456 | 457 | fn from_func(func: TypedFunc) -> Self { 458 | Self(func) 459 | } 460 | } 461 | 462 | impl OpaHeapPtrGet { 463 | #[tracing::instrument(name = "opa_heap_ptr_get", skip_all, err)] 464 | pub async fn call(&self, store: impl AsContextMut) -> Result { 465 | let res = self.0.call_async(store, ()).await?; 466 | Ok(Addr(res)) 467 | } 468 | } 469 | 470 | /// `int32 opa_value_add_path(base_value_addr, path_value_addr, value_addr)` 471 | pub struct OpaValueAddPath(TypedFunc<(i32, i32, i32), i32>); 472 | 473 | impl Func for OpaValueAddPath { 474 | const EXPORT: &'static str = "opa_value_add_path"; 475 | type Params = (i32, i32, i32); 476 | type Results = i32; 477 | 478 | fn from_func(func: TypedFunc) -> Self { 479 | Self(func) 480 | } 481 | } 482 | 483 | impl OpaValueAddPath { 484 | #[allow(dead_code)] 485 | #[tracing::instrument(name = "opa_value_add_path", skip_all, err)] 486 | pub async fn call( 487 | &self, 488 | store: impl AsContextMut, 489 | base: &Value, 490 | path: &Value, 491 | value: &Value, 492 | ) -> Result<()> { 493 | let res = self.0.call_async(store, (base.0, path.0, value.0)).await?; 494 | Ok(OpaError::from_code(res)?) 495 | } 496 | } 497 | 498 | /// `int32 opa_value_remove_path(base_value_addr, path_value_addr)` 499 | pub struct OpaValueRemovePath(TypedFunc<(i32, i32), i32>); 500 | 501 | impl Func for OpaValueRemovePath { 502 | const EXPORT: &'static str = "opa_value_remove_path"; 503 | type Params = (i32, i32); 504 | type Results = i32; 505 | 506 | fn from_func(func: TypedFunc) -> Self { 507 | Self(func) 508 | } 509 | } 510 | 511 | impl OpaValueRemovePath { 512 | #[allow(dead_code)] 513 | #[tracing::instrument(name = "opa_value_remove_path", skip_all, err)] 514 | pub async fn call( 515 | &self, 516 | store: impl AsContextMut, 517 | base: &Value, 518 | path: &Value, 519 | ) -> Result<()> { 520 | let res = self.0.call_async(store, (base.0, path.0)).await?; 521 | Ok(OpaError::from_code(res)?) 522 | } 523 | } 524 | 525 | /// `str_addr opa_value_dump(value_addr)` 526 | pub struct OpaValueDump(TypedFunc); 527 | 528 | impl Func for OpaValueDump { 529 | const EXPORT: &'static str = "opa_value_dump"; 530 | type Params = i32; 531 | type Results = i32; 532 | 533 | fn from_func(func: TypedFunc) -> Self { 534 | Self(func) 535 | } 536 | } 537 | 538 | impl OpaValueDump { 539 | #[allow(dead_code)] 540 | #[tracing::instrument(name = "opa_value_dump", skip_all, err)] 541 | pub async fn call( 542 | &self, 543 | store: impl AsContextMut, 544 | value: &Value, 545 | ) -> Result { 546 | let res = self.0.call_async(store, value.0).await?; 547 | Ok(Value(res)) 548 | } 549 | } 550 | 551 | /// `str_addr opa_eval(_ addr, entrypoint_id int32, data value_addr, input 552 | /// str_addr, input_len int32, heap_ptr addr, format int32)` 553 | #[allow(clippy::type_complexity)] 554 | pub struct OpaEval(TypedFunc<(i32, i32, i32, i32, i32, i32, i32), i32>); 555 | 556 | impl Func for OpaEval { 557 | const EXPORT: &'static str = "opa_eval"; 558 | type Params = (i32, i32, i32, i32, i32, i32, i32); 559 | type Results = i32; 560 | 561 | fn from_func(func: TypedFunc) -> Self { 562 | Self(func) 563 | } 564 | } 565 | 566 | impl OpaEval { 567 | #[tracing::instrument(name = "opa_eval", skip_all, err)] 568 | pub async fn call( 569 | &self, 570 | store: impl AsContextMut, 571 | entrypoint: &EntrypointId, 572 | data: &Value, 573 | input: &Heap, 574 | heap_ptr: &Addr, 575 | ) -> Result { 576 | let res = self 577 | .0 578 | .call_async( 579 | store, 580 | (0, entrypoint.0, data.0, input.ptr, input.len, heap_ptr.0, 0), 581 | ) 582 | .await?; 583 | Ok(NulStr(res)) 584 | } 585 | } 586 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![doc = include_str!("../README.md")] 16 | #![deny( 17 | missing_docs, 18 | clippy::all, 19 | clippy::pedantic, 20 | clippy::missing_docs_in_private_items, 21 | clippy::panic, // Disallow panics 22 | clippy::print_stderr, // Disallow directly writing to stderr. Use tracing instead 23 | clippy::print_stdout, // Disallow directly writing to stdout. Use tracing instead 24 | clippy::unwrap_used, // Disallow the use of Result::{unwrap,expect}. Propagate errors instaed 25 | clippy::unwrap_in_result, 26 | clippy::expect_used, 27 | )] 28 | #![allow(clippy::blocks_in_conditions)] 29 | 30 | mod builtins; 31 | mod context; 32 | mod funcs; 33 | #[cfg(feature = "loader")] 34 | mod loader; 35 | mod policy; 36 | mod types; 37 | 38 | // Re-export wasmtime to make it easier to keep the verisons in sync 39 | pub use wasmtime; 40 | 41 | #[cfg(feature = "loader")] 42 | pub use self::loader::{load_bundle, read_bundle}; 43 | pub use self::{ 44 | context::{tests::TestContext, DefaultContext, EvaluationContext}, 45 | policy::{Policy, Runtime}, 46 | types::AbiVersion, 47 | }; 48 | -------------------------------------------------------------------------------- /src/loader.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Helpers to load OPA compiled bundles 16 | 17 | use std::path::Path; 18 | 19 | use anyhow::Context; 20 | use async_compression::tokio::bufread::GzipDecoder; 21 | use futures_util::TryStreamExt; 22 | use tokio::io::{AsyncBufRead, AsyncReadExt, BufReader}; 23 | use tokio_tar::Archive; 24 | use tracing::{info_span, Instrument}; 25 | 26 | /// Read an OPA compiled bundle from disk 27 | #[tracing::instrument(err)] 28 | pub async fn read_bundle(path: impl AsRef + std::fmt::Debug) -> anyhow::Result> { 29 | let file = tokio::fs::File::open(path).await?; 30 | let reader = BufReader::new(file); 31 | load_bundle(reader).await 32 | } 33 | 34 | /// Load an OPA compiled bundle 35 | #[tracing::instrument(skip_all, err)] 36 | pub async fn load_bundle( 37 | reader: impl AsyncBufRead + Unpin + Send + Sync, 38 | ) -> anyhow::Result> { 39 | // Wrap the reader in a gzip decoder, then in a tar unarchiver 40 | let reader = GzipDecoder::new(reader); 41 | let mut archive = Archive::new(reader); 42 | 43 | // Go through the archive entries to find the /policy.wasm one 44 | let entries = archive.entries()?; 45 | let mut entry = entries 46 | .try_filter(|e| { 47 | std::future::ready( 48 | e.path() 49 | .map(|p| p.as_os_str() == "/policy.wasm") 50 | .unwrap_or(false), 51 | ) 52 | }) 53 | .try_next() 54 | .instrument(info_span!("find_bundle_entry")) 55 | .await? 56 | .context("could not find WASM policy in tar archive")?; 57 | 58 | // Once we found it, read it completely to a buffer 59 | let mut buf = Vec::new(); 60 | entry 61 | .read_to_end(&mut buf) 62 | .instrument(info_span!("read_module")) 63 | .await?; 64 | 65 | Ok(buf) 66 | } 67 | -------------------------------------------------------------------------------- /src/policy.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! The policy evaluation logic, which includes the [`Policy`] and [`Runtime`] 16 | //! structures. 17 | 18 | use std::{ 19 | collections::{HashMap, HashSet}, 20 | ffi::CString, 21 | fmt::Debug, 22 | ops::Deref, 23 | sync::Arc, 24 | }; 25 | 26 | use anyhow::{Context, Result}; 27 | use tokio::sync::{Mutex, OnceCell}; 28 | use tracing::Instrument; 29 | use wasmtime::{AsContextMut, Caller, Linker, Memory, MemoryType, Module}; 30 | 31 | use crate::{ 32 | builtins::traits::Builtin, 33 | funcs::{self, Func}, 34 | types::{AbiVersion, Addr, BuiltinId, EntrypointId, Heap, NulStr, Value}, 35 | DefaultContext, EvaluationContext, 36 | }; 37 | 38 | /// Utility to allocate a string in the Wasm memory and return a pointer to it. 39 | async fn alloc_str>, T: Send>( 40 | opa_malloc: &funcs::OpaMalloc, 41 | mut store: impl AsContextMut, 42 | memory: &Memory, 43 | value: V, 44 | ) -> Result { 45 | let value = CString::new(value)?; 46 | let value = value.as_bytes_with_nul(); 47 | let heap = opa_malloc.call(&mut store, value.len()).await?; 48 | 49 | memory.write( 50 | &mut store, 51 | heap.ptr 52 | .try_into() 53 | .context("opa_malloc returned an invalid pointer value")?, 54 | value, 55 | )?; 56 | 57 | Ok(heap) 58 | } 59 | 60 | /// Utility to load a JSON value into the WASM memory. 61 | async fn load_json( 62 | opa_malloc: &funcs::OpaMalloc, 63 | opa_free: &funcs::OpaFree, 64 | opa_json_parse: &funcs::OpaJsonParse, 65 | mut store: impl AsContextMut, 66 | memory: &Memory, 67 | data: &V, 68 | ) -> Result { 69 | let json = serde_json::to_vec(data)?; 70 | let json = alloc_str(opa_malloc, &mut store, memory, json).await?; 71 | let data = opa_json_parse.call(&mut store, &json).await?; 72 | opa_free.call(&mut store, json).await?; 73 | Ok(data) 74 | } 75 | 76 | /// A structure which holds the builtins referenced by the policy. 77 | struct LoadedBuiltins { 78 | /// A map of builtin IDs to the name and the builtin itself. 79 | builtins: HashMap>)>, 80 | 81 | /// The inner [`EvaluationContext`] which will be passed when calling 82 | /// some builtins 83 | context: Mutex, 84 | } 85 | 86 | impl std::fmt::Debug for LoadedBuiltins { 87 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 88 | f.debug_struct("LoadedBuiltins") 89 | .field("builtins", &()) 90 | .finish() 91 | } 92 | } 93 | 94 | impl LoadedBuiltins 95 | where 96 | C: EvaluationContext, 97 | { 98 | /// Resolve the builtins from a map of builtin IDs to their names. 99 | fn from_map(map: HashMap, context: C) -> Result { 100 | let res: Result<_> = map 101 | .into_iter() 102 | .map(|(k, v)| { 103 | let builtin = crate::builtins::resolve(&k)?; 104 | Ok((v.0, (k, builtin))) 105 | }) 106 | .collect(); 107 | Ok(Self { 108 | builtins: res?, 109 | context: Mutex::new(context), 110 | }) 111 | } 112 | 113 | /// Call the given builtin given its ID and arguments. 114 | async fn builtin( 115 | &self, 116 | mut caller: Caller<'_, T>, 117 | memory: &Memory, 118 | builtin_id: i32, 119 | args: [i32; N], 120 | ) -> Result { 121 | let (name, builtin) = self 122 | .builtins 123 | .get(&builtin_id) 124 | .with_context(|| format!("unknown builtin id {builtin_id}"))?; 125 | 126 | let span = tracing::info_span!("builtin", %name); 127 | let _enter = span.enter(); 128 | 129 | let opa_json_dump = funcs::OpaJsonDump::from_caller(&mut caller)?; 130 | let opa_json_parse = funcs::OpaJsonParse::from_caller(&mut caller)?; 131 | let opa_malloc = funcs::OpaMalloc::from_caller(&mut caller)?; 132 | let opa_free = funcs::OpaFree::from_caller(&mut caller)?; 133 | 134 | // Call opa_json_dump on each argument 135 | let mut args_json = Vec::with_capacity(N); 136 | for arg in args { 137 | args_json.push(opa_json_dump.call(&mut caller, &Value(arg)).await?); 138 | } 139 | 140 | // Extract the JSON value of each argument 141 | let mut mapped_args = Vec::with_capacity(N); 142 | for arg_json in args_json { 143 | let arg = arg_json.read(&caller, memory)?; 144 | mapped_args.push(arg.to_bytes()); 145 | } 146 | 147 | let mut ctx = self.context.lock().await; 148 | 149 | // Actually call the function 150 | let ret = (async move { builtin.call(&mut ctx, &mapped_args).await }) 151 | .instrument(tracing::info_span!("builtin.call")) 152 | .await?; 153 | 154 | let json = alloc_str(&opa_malloc, &mut caller, memory, ret).await?; 155 | let data = opa_json_parse.call(&mut caller, &json).await?; 156 | opa_free.call(&mut caller, json).await?; 157 | 158 | Ok(data.0) 159 | } 160 | 161 | /// Called when the policy evaluation starts, to reset the context and 162 | /// record the evaluation starting time 163 | async fn evaluation_start(&self) { 164 | self.context.lock().await.evaluation_start(); 165 | } 166 | } 167 | 168 | /// An instance of a policy with builtins and entrypoints resolved, but with no 169 | /// data provided yet 170 | #[allow(clippy::missing_docs_in_private_items)] 171 | pub struct Runtime { 172 | version: AbiVersion, 173 | memory: Memory, 174 | entrypoints: HashMap, 175 | loaded_builtins: Arc>>, 176 | 177 | eval_func: funcs::Eval, 178 | opa_eval_ctx_new_func: funcs::OpaEvalCtxNew, 179 | opa_eval_ctx_set_input_func: funcs::OpaEvalCtxSetInput, 180 | opa_eval_ctx_set_data_func: funcs::OpaEvalCtxSetData, 181 | opa_eval_ctx_set_entrypoint_func: funcs::OpaEvalCtxSetEntrypoint, 182 | opa_eval_ctx_get_result_func: funcs::OpaEvalCtxGetResult, 183 | opa_malloc_func: funcs::OpaMalloc, 184 | opa_free_func: funcs::OpaFree, 185 | opa_json_parse_func: funcs::OpaJsonParse, 186 | opa_json_dump_func: funcs::OpaJsonDump, 187 | opa_heap_ptr_set_func: funcs::OpaHeapPtrSet, 188 | opa_heap_ptr_get_func: funcs::OpaHeapPtrGet, 189 | opa_eval_func: Option, 190 | } 191 | 192 | impl Debug for Runtime { 193 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 194 | f.debug_struct("Runtime") 195 | .field("version", &self.version) 196 | .field("memory", &self.memory) 197 | .field("entrypoints", &self.entrypoints) 198 | .finish_non_exhaustive() 199 | } 200 | } 201 | 202 | impl Runtime { 203 | /// Load a new WASM policy module into the given store, with the default 204 | /// evaluation context. 205 | /// 206 | /// # Errors 207 | /// 208 | /// It will raise an error if one of the following condition is met: 209 | /// 210 | /// - the provided [`wasmtime::Store`] isn't an async one 211 | /// - the [`wasmtime::Module`] was created with a different 212 | /// [`wasmtime::Engine`] than the [`wasmtime::Store`] 213 | /// - the WASM module is not a valid OPA WASM compiled policy, and lacks 214 | /// some of the exported functions 215 | /// - it failed to load the entrypoints or the builtins list 216 | #[allow(clippy::too_many_lines)] 217 | pub async fn new(store: impl AsContextMut, module: &Module) -> Result { 218 | let context = DefaultContext::default(); 219 | Self::new_with_evaluation_context(store, module, context).await 220 | } 221 | } 222 | 223 | impl Runtime { 224 | /// Load a new WASM policy module into the given store, with a given 225 | /// evaluation context. 226 | /// 227 | /// # Errors 228 | /// 229 | /// It will raise an error if one of the following condition is met: 230 | /// 231 | /// - the provided [`wasmtime::Store`] isn't an async one 232 | /// - the [`wasmtime::Module`] was created with a different 233 | /// [`wasmtime::Engine`] than the [`wasmtime::Store`] 234 | /// - the WASM module is not a valid OPA WASM compiled policy, and lacks 235 | /// some of the exported functions 236 | /// - it failed to load the entrypoints or the builtins list 237 | #[allow(clippy::too_many_lines)] 238 | pub async fn new_with_evaluation_context( 239 | mut store: impl AsContextMut, 240 | module: &Module, 241 | context: C, 242 | ) -> Result 243 | where 244 | C: EvaluationContext, 245 | { 246 | let ty = MemoryType::new(2, None); 247 | let memory = Memory::new_async(&mut store, ty).await?; 248 | 249 | // TODO: make the context configurable and reset it on evaluation 250 | let eventually_builtins = Arc::new(OnceCell::>::new()); 251 | 252 | let mut linker = Linker::new(store.as_context_mut().engine()); 253 | linker.define(&store, "env", "memory", memory)?; 254 | 255 | linker.func_wrap( 256 | "env", 257 | "opa_abort", 258 | move |caller: Caller<'_, _>, addr: i32| -> Result<(), anyhow::Error> { 259 | let addr = NulStr(addr); 260 | let msg = addr.read(&caller, &memory)?; 261 | let msg = msg.to_string_lossy().into_owned(); 262 | tracing::error!("opa_abort: {}", msg); 263 | anyhow::bail!(msg) 264 | }, 265 | )?; 266 | 267 | linker.func_wrap( 268 | "env", 269 | "opa_println", 270 | move |caller: Caller<'_, _>, addr: i32| { 271 | let addr = NulStr(addr); 272 | let msg = addr.read(&caller, &memory)?; 273 | tracing::info!("opa_print: {}", msg.to_string_lossy()); 274 | Ok(()) 275 | }, 276 | )?; 277 | 278 | { 279 | let eventually_builtins = eventually_builtins.clone(); 280 | linker.func_wrap_async( 281 | "env", 282 | "opa_builtin0", 283 | move |caller: Caller<'_, _>, (builtin_id, _ctx): (i32, i32)| { 284 | let eventually_builtins = eventually_builtins.clone(); 285 | Box::new(async move { 286 | eventually_builtins 287 | .get() 288 | .context("builtins where never initialized")? 289 | .builtin(caller, &memory, builtin_id, []) 290 | .await 291 | }) 292 | }, 293 | )?; 294 | } 295 | 296 | { 297 | let eventually_builtins = eventually_builtins.clone(); 298 | linker.func_wrap_async( 299 | "env", 300 | "opa_builtin1", 301 | move |caller: Caller<'_, _>, (builtin_id, _ctx, param1): (i32, i32, i32)| { 302 | let eventually_builtins = eventually_builtins.clone(); 303 | Box::new(async move { 304 | eventually_builtins 305 | .get() 306 | .context("builtins where never initialized")? 307 | .builtin(caller, &memory, builtin_id, [param1]) 308 | .await 309 | }) 310 | }, 311 | )?; 312 | } 313 | 314 | { 315 | let eventually_builtins = eventually_builtins.clone(); 316 | linker.func_wrap_async( 317 | "env", 318 | "opa_builtin2", 319 | move |caller: Caller<'_, _>, 320 | (builtin_id, _ctx, param1, param2): (i32, i32, i32, i32)| { 321 | let eventually_builtins = eventually_builtins.clone(); 322 | Box::new(async move { 323 | eventually_builtins 324 | .get() 325 | .context("builtins where never initialized")? 326 | .builtin(caller, &memory, builtin_id, [param1, param2]) 327 | .await 328 | }) 329 | }, 330 | )?; 331 | } 332 | 333 | { 334 | let eventually_builtins = eventually_builtins.clone(); 335 | linker.func_wrap_async( 336 | "env", 337 | "opa_builtin3", 338 | move |caller: Caller<'_, _>, 339 | (builtin_id, 340 | _ctx, 341 | param1, 342 | param2, 343 | param3): (i32, i32, i32, i32, i32)| { 344 | let eventually_builtins = eventually_builtins.clone(); 345 | Box::new(async move { 346 | eventually_builtins 347 | .get() 348 | .context("builtins where never initialized")? 349 | .builtin(caller, &memory, builtin_id, [param1, param2, param3]) 350 | .await 351 | }) 352 | }, 353 | )?; 354 | } 355 | 356 | { 357 | let eventually_builtins = eventually_builtins.clone(); 358 | linker.func_wrap_async( 359 | "env", 360 | "opa_builtin4", 361 | move |caller: Caller<'_, _>, 362 | (builtin_id, _ctx, param1, param2, param3, param4): ( 363 | i32, 364 | i32, 365 | i32, 366 | i32, 367 | i32, 368 | i32, 369 | )| { 370 | let eventually_builtins = eventually_builtins.clone(); 371 | Box::new(async move { 372 | eventually_builtins 373 | .get() 374 | .context("builtins where never initialized")? 375 | .builtin( 376 | caller, 377 | &memory, 378 | builtin_id, 379 | [param1, param2, param3, param4], 380 | ) 381 | .await 382 | }) 383 | }, 384 | )?; 385 | } 386 | 387 | let instance = linker.instantiate_async(&mut store, module).await?; 388 | 389 | let version = AbiVersion::from_instance(&mut store, &instance)?; 390 | tracing::debug!(%version, "Module ABI version"); 391 | 392 | let opa_json_dump_func = funcs::OpaJsonDump::from_instance(&mut store, &instance)?; 393 | 394 | // Load the builtins map 395 | let builtins = funcs::Builtins::from_instance(&mut store, &instance)? 396 | .call(&mut store) 397 | .await?; 398 | let builtins = opa_json_dump_func 399 | .decode(&mut store, &memory, &builtins) 400 | .await?; 401 | let builtins = LoadedBuiltins::from_map(builtins, context)?; 402 | eventually_builtins.set(builtins)?; 403 | 404 | // Load the entrypoints map 405 | let entrypoints = funcs::Entrypoints::from_instance(&mut store, &instance)? 406 | .call(&mut store) 407 | .await?; 408 | let entrypoints = opa_json_dump_func 409 | .decode(&mut store, &memory, &entrypoints) 410 | .await?; 411 | 412 | let opa_eval_func = version 413 | .has_eval_fastpath() 414 | .then(|| funcs::OpaEval::from_instance(&mut store, &instance)) 415 | .transpose()?; 416 | 417 | Ok(Self { 418 | version, 419 | memory, 420 | entrypoints, 421 | loaded_builtins: eventually_builtins, 422 | 423 | eval_func: funcs::Eval::from_instance(&mut store, &instance)?, 424 | opa_eval_ctx_new_func: funcs::OpaEvalCtxNew::from_instance(&mut store, &instance)?, 425 | opa_eval_ctx_set_input_func: funcs::OpaEvalCtxSetInput::from_instance( 426 | &mut store, &instance, 427 | )?, 428 | opa_eval_ctx_set_data_func: funcs::OpaEvalCtxSetData::from_instance( 429 | &mut store, &instance, 430 | )?, 431 | opa_eval_ctx_set_entrypoint_func: funcs::OpaEvalCtxSetEntrypoint::from_instance( 432 | &mut store, &instance, 433 | )?, 434 | opa_eval_ctx_get_result_func: funcs::OpaEvalCtxGetResult::from_instance( 435 | &mut store, &instance, 436 | )?, 437 | opa_malloc_func: funcs::OpaMalloc::from_instance(&mut store, &instance)?, 438 | opa_free_func: funcs::OpaFree::from_instance(&mut store, &instance)?, 439 | opa_json_parse_func: funcs::OpaJsonParse::from_instance(&mut store, &instance)?, 440 | opa_json_dump_func, 441 | opa_heap_ptr_set_func: funcs::OpaHeapPtrSet::from_instance(&mut store, &instance)?, 442 | opa_heap_ptr_get_func: funcs::OpaHeapPtrGet::from_instance(&mut store, &instance)?, 443 | opa_eval_func, 444 | }) 445 | } 446 | 447 | /// Load a JSON value into the WASM memory 448 | async fn load_json( 449 | &self, 450 | store: impl AsContextMut, 451 | data: &V, 452 | ) -> Result { 453 | load_json( 454 | &self.opa_malloc_func, 455 | &self.opa_free_func, 456 | &self.opa_json_parse_func, 457 | store, 458 | &self.memory, 459 | data, 460 | ) 461 | .await 462 | } 463 | 464 | /// Instanciate the policy with an empty `data` object 465 | /// 466 | /// # Errors 467 | /// 468 | /// If it failed to load the empty data object in memory 469 | pub async fn without_data( 470 | self, 471 | store: impl AsContextMut, 472 | ) -> Result> { 473 | let data = serde_json::Value::Object(serde_json::Map::default()); 474 | self.with_data(store, &data).await 475 | } 476 | 477 | /// Instanciate the policy with the given `data` object 478 | /// 479 | /// # Errors 480 | /// 481 | /// If it failed to serialize and load the `data` object 482 | pub async fn with_data( 483 | self, 484 | mut store: impl AsContextMut, 485 | data: &V, 486 | ) -> Result> { 487 | let data = self.load_json(&mut store, data).await?; 488 | let heap_ptr = self.opa_heap_ptr_get_func.call(&mut store).await?; 489 | Ok(Policy { 490 | runtime: self, 491 | data, 492 | heap_ptr, 493 | }) 494 | } 495 | 496 | /// Get the default entrypoint of this module. May return [`None`] if no 497 | /// entrypoint with ID 0 was found 498 | #[must_use] 499 | pub fn default_entrypoint(&self) -> Option<&str> { 500 | self.entrypoints 501 | .iter() 502 | .find_map(|(k, v)| (v.0 == 0).then_some(k.as_str())) 503 | } 504 | 505 | /// Get the list of entrypoints found in this module. 506 | #[must_use] 507 | pub fn entrypoints(&self) -> HashSet<&str> { 508 | self.entrypoints.keys().map(String::as_str).collect() 509 | } 510 | 511 | /// Get the ABI version detected for this module 512 | #[must_use] 513 | pub fn abi_version(&self) -> AbiVersion { 514 | self.version 515 | } 516 | } 517 | 518 | /// An instance of a policy, ready to be executed 519 | #[derive(Debug)] 520 | pub struct Policy { 521 | /// The runtime this policy instance belongs to 522 | runtime: Runtime, 523 | 524 | /// The data object loaded for this policy 525 | data: Value, 526 | 527 | /// A pointer to the heap, used for efficient allocations 528 | heap_ptr: Addr, 529 | } 530 | 531 | impl Policy { 532 | /// Evaluate a policy with the given entrypoint and input. 533 | /// 534 | /// # Errors 535 | /// 536 | /// Returns an error if the policy evaluation failed, or if this policy did 537 | /// not belong to the given store. 538 | pub async fn evaluate serde::Deserialize<'de>, T: Send>( 539 | &self, 540 | mut store: impl AsContextMut, 541 | entrypoint: &str, 542 | input: &V, 543 | ) -> Result 544 | where 545 | C: EvaluationContext, 546 | { 547 | // Lookup the entrypoint 548 | let entrypoint = self 549 | .runtime 550 | .entrypoints 551 | .get(entrypoint) 552 | .with_context(|| format!("could not find entrypoint {entrypoint}"))?; 553 | 554 | self.loaded_builtins 555 | .get() 556 | .context("builtins where never initialized")? 557 | .evaluation_start() 558 | .await; 559 | 560 | // Take the fast path if it is awailable 561 | if let Some(opa_eval) = &self.runtime.opa_eval_func { 562 | // Write the input 563 | let input = serde_json::to_vec(&input)?; 564 | let input_heap = Heap { 565 | ptr: self.heap_ptr.0, 566 | len: input.len().try_into().context("input too long")?, 567 | // Not managed by a malloc 568 | freed: true, 569 | }; 570 | 571 | // Check if we need to grow the memory first 572 | let current_pages = self.runtime.memory.size(&store); 573 | let needed_pages = input_heap.pages(); 574 | if current_pages < needed_pages { 575 | self.runtime 576 | .memory 577 | .grow_async(&mut store, needed_pages - current_pages) 578 | .await?; 579 | } 580 | 581 | // Write the JSON input to memory 582 | self.runtime.memory.write( 583 | &mut store, 584 | input_heap.ptr.try_into().context("invalid heap pointer")?, 585 | &input[..], 586 | )?; 587 | 588 | let heap_ptr = Addr(input_heap.end()); 589 | 590 | // Call the eval fast-path 591 | let result = opa_eval 592 | .call(&mut store, entrypoint, &self.data, &input_heap, &heap_ptr) 593 | .await?; 594 | 595 | // Read back the JSON-formatted result 596 | let result = result.read(&store, &self.runtime.memory)?; 597 | let result = serde_json::from_slice(result.to_bytes())?; 598 | Ok(result) 599 | } else { 600 | // Reset the heap pointer 601 | self.runtime 602 | .opa_heap_ptr_set_func 603 | .call(&mut store, &self.heap_ptr) 604 | .await?; 605 | 606 | // Load the input 607 | let input = self.runtime.load_json(&mut store, input).await?; 608 | 609 | // Create a new evaluation context 610 | let ctx = self.runtime.opa_eval_ctx_new_func.call(&mut store).await?; 611 | 612 | // Set the data location 613 | self.runtime 614 | .opa_eval_ctx_set_data_func 615 | .call(&mut store, &ctx, &self.data) 616 | .await?; 617 | // Set the input location 618 | self.runtime 619 | .opa_eval_ctx_set_input_func 620 | .call(&mut store, &ctx, &input) 621 | .await?; 622 | 623 | // Set the entrypoint 624 | self.runtime 625 | .opa_eval_ctx_set_entrypoint_func 626 | .call(&mut store, &ctx, entrypoint) 627 | .await?; 628 | 629 | // Evaluate the policy 630 | self.runtime.eval_func.call(&mut store, &ctx).await?; 631 | 632 | // Get the results back 633 | let result = self 634 | .runtime 635 | .opa_eval_ctx_get_result_func 636 | .call(&mut store, &ctx) 637 | .await?; 638 | 639 | let result = self 640 | .runtime 641 | .opa_json_dump_func 642 | .decode(&mut store, &self.runtime.memory, &result) 643 | .await?; 644 | 645 | Ok(result) 646 | } 647 | } 648 | } 649 | 650 | impl Deref for Policy { 651 | type Target = Runtime; 652 | fn deref(&self) -> &Self::Target { 653 | &self.runtime 654 | } 655 | } 656 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Type wrappers to help with interacting with the OPA WASM module 16 | 17 | use std::ffi::CStr; 18 | 19 | use anyhow::{bail, Context, Result}; 20 | use serde::Deserialize; 21 | use wasmtime::{AsContext, AsContextMut, Instance, Memory}; 22 | 23 | /// An entrypoint ID, as returned by the `entrypoints` export, and given to the 24 | /// `opa_eval_ctx_set_entrypoint` exports 25 | #[derive(Debug, Deserialize, Clone)] 26 | #[serde(transparent)] 27 | pub struct EntrypointId(pub(crate) i32); 28 | 29 | /// The ID of a builtin, as returned by the `builtins` export, and passed to the 30 | /// `opa_builtin*` imports 31 | #[derive(Debug, Deserialize, Clone)] 32 | #[serde(transparent)] 33 | pub struct BuiltinId(pub(crate) i32); 34 | 35 | /// A value stored on the WASM heap, as used by the `opa_value_*` exports 36 | #[derive(Debug)] 37 | pub struct Value(pub(crate) i32); 38 | 39 | /// A generic value on the WASM memory 40 | #[derive(Debug)] 41 | pub struct Addr(pub(crate) i32); 42 | 43 | /// A heap allocation on the WASM memory 44 | #[derive(Debug)] 45 | pub struct Heap { 46 | /// The pointer to the start of the heap allocation 47 | pub(crate) ptr: i32, 48 | 49 | /// The length of the heap allocation 50 | pub(crate) len: i32, 51 | 52 | /// Whether the heap allocation has been freed 53 | pub(crate) freed: bool, 54 | } 55 | 56 | impl Heap { 57 | /// Get the end of the heap allocation 58 | pub const fn end(&self) -> i32 { 59 | self.ptr + self.len 60 | } 61 | 62 | /// Calculate the number of pages this heap allocation occupies 63 | pub fn pages(&self) -> u64 { 64 | let page_size = 64 * 1024; 65 | let addr = self.end(); 66 | let page = addr / page_size; 67 | // This is safe as the heap pointers will never be negative. We use i32 for 68 | // convenience to avoid having to cast all the time. 69 | #[allow(clippy::expect_used)] 70 | if addr % page_size > 0 { page + 1 } else { page } 71 | .try_into() 72 | .expect("invalid heap address") 73 | } 74 | } 75 | 76 | impl Drop for Heap { 77 | fn drop(&mut self) { 78 | if !self.freed { 79 | tracing::warn!("forgot to free heap allocation"); 80 | self.freed = true; 81 | } 82 | } 83 | } 84 | 85 | /// A null-terminated string on the WASM memory 86 | #[derive(Debug)] 87 | pub struct NulStr(pub(crate) i32); 88 | 89 | impl NulStr { 90 | /// Read the null-terminated string from the WASM memory 91 | pub fn read<'s, T: AsContext>(&self, store: &'s T, memory: &Memory) -> Result<&'s CStr> { 92 | let mem = memory.data(store); 93 | let start: usize = self.0.try_into().context("invalid address")?; 94 | let mem = mem.get(start..).context("memory address out of bounds")?; 95 | let nul = mem 96 | .iter() 97 | .position(|c| *c == 0) 98 | .context("malformed string")?; 99 | let mem = mem 100 | .get(..=nul) 101 | .context("issue while extracting nul-terminated string")?; 102 | let res = CStr::from_bytes_with_nul(mem)?; 103 | Ok(res) 104 | } 105 | } 106 | 107 | /// The address of the evaluation context, used by the `opa_eval_ctx_*` exports 108 | #[derive(Debug)] 109 | pub struct Ctx(pub(crate) i32); 110 | 111 | /// An error returned by the OPA module 112 | #[derive(Debug, thiserror::Error)] 113 | pub enum OpaError { 114 | /// Unrecoverable internal error 115 | #[error("Unrecoverable internal error")] 116 | Internal, 117 | 118 | /// Invalid value type was encountered 119 | #[error("Invalid value type was encountered")] 120 | InvalidType, 121 | 122 | /// Invalid object path reference 123 | #[error("Invalid object path reference")] 124 | InvalidPath, 125 | 126 | /// Unrecognized error code 127 | #[error("Unrecognized error code: {0}")] 128 | Other(i32), 129 | } 130 | 131 | impl OpaError { 132 | /// Convert an error code to an `OpaError` 133 | pub(crate) fn from_code(code: i32) -> Result<(), OpaError> { 134 | match code { 135 | 0 => Ok(()), 136 | 1 => Err(Self::Internal), 137 | 2 => Err(Self::InvalidType), 138 | 3 => Err(Self::InvalidPath), 139 | x => Err(Self::Other(x)), 140 | } 141 | } 142 | } 143 | 144 | /// Represents the ABI version of a WASM OPA module 145 | #[derive(Debug, Clone, Copy)] 146 | pub enum AbiVersion { 147 | /// Version 1.0 148 | V1_0, 149 | 150 | /// Version 1.1 151 | V1_1, 152 | 153 | /// Version 1.2 154 | V1_2, 155 | 156 | /// Version >1.2, <2.0 157 | V1_2Plus(i32), 158 | } 159 | 160 | impl AbiVersion { 161 | /// Get the ABI version out of an instanciated WASM policy 162 | /// 163 | /// # Errors 164 | /// 165 | /// Returns an error if the WASM module lacks ABI version information 166 | pub(crate) fn from_instance( 167 | mut store: impl AsContextMut, 168 | instance: &Instance, 169 | ) -> Result { 170 | let abi_version = instance 171 | .get_global(&mut store, "opa_wasm_abi_version") 172 | .context("missing global opa_wasm_abi_version")? 173 | .get(&mut store) 174 | .i32() 175 | .context("opa_wasm_abi_version is not an i32")?; 176 | 177 | let abi_minor_version = instance 178 | .get_global(&mut store, "opa_wasm_abi_minor_version") 179 | .context("missing global opa_wasm_abi_minor_version")? 180 | .get(&mut store) 181 | .i32() 182 | .context("opa_wasm_abi_minor_version is not an i32")?; 183 | 184 | Self::new(abi_version, abi_minor_version) 185 | } 186 | 187 | /// Create a new ABI version out of the minor and major version numbers. 188 | fn new(major: i32, minor: i32) -> Result { 189 | match (major, minor) { 190 | (1, 0) => Ok(Self::V1_0), 191 | (1, 1) => Ok(Self::V1_1), 192 | (1, 2) => Ok(Self::V1_2), 193 | (1, n @ 2..) => Ok(Self::V1_2Plus(n)), 194 | (major, minor) => bail!("unsupported ABI version {}.{}", major, minor), 195 | } 196 | } 197 | 198 | /// Check if this ABI version has support for the `eval` fastpath 199 | #[must_use] 200 | pub(crate) const fn has_eval_fastpath(self) -> bool { 201 | matches!(self, Self::V1_2 | Self::V1_2Plus(_)) 202 | } 203 | } 204 | 205 | impl std::fmt::Display for AbiVersion { 206 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 207 | match self { 208 | AbiVersion::V1_0 => write!(f, "1.0"), 209 | AbiVersion::V1_1 => write!(f, "1.1"), 210 | AbiVersion::V1_2 => write!(f, "1.2"), 211 | AbiVersion::V1_2Plus(n) => write!(f, "1.{n} (1.2+ compatible)"), 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /tests/fixtures/test-loader.false.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": { 3 | "destination": { 4 | "address": { 5 | "socketAddress": { 6 | "portValue": 8000, 7 | "address": "10.25.95.68" 8 | } 9 | } 10 | }, 11 | "metadataContext": { 12 | "filterMetadata": { 13 | "envoy.filters.http.header_to_metadata": { 14 | "policy_type": "ingress" 15 | } 16 | } 17 | }, 18 | "request": { 19 | "http": { 20 | "headers": { 21 | ":authority": "example-app", 22 | ":method": "GET", 23 | ":path": "/", 24 | "accept": "*/*", 25 | "authorization": "Basic ZXZlOnBhc3N3b3Jk", 26 | "content-length": "0", 27 | "user-agent": "curl/7.68.0-DEV", 28 | "x-ext-auth-allow": "yes", 29 | "x-forwarded-proto": "http", 30 | "x-request-id": "1455bbb0-0623-4810-a2c6-df73ffd8863a" 31 | }, 32 | "host": "example-app", 33 | "id": "8306787481883314548", 34 | "method": "POST", 35 | "path": "/", 36 | "protocol": "HTTP/1.1" 37 | } 38 | }, 39 | "source": { 40 | "address": { 41 | "socketAddress": { 42 | "portValue": 33772, 43 | "address": "10.25.95.69" 44 | } 45 | } 46 | } 47 | }, 48 | "parsed_body": {}, 49 | "parsed_path": [], 50 | "parsed_query": {} 51 | } 52 | -------------------------------------------------------------------------------- /tests/fixtures/test-loader.rego: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Matrix.org Foundation C.I.C. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | package fixtures 16 | 17 | import rego.v1 18 | 19 | default allow := false 20 | 21 | allow if { 22 | input.attributes.request.http.method == "GET" 23 | input.attributes.request.http.path == "/" 24 | } 25 | 26 | allow if { 27 | input.attributes.request.http.headers.authorization == "Basic charlie" 28 | } 29 | -------------------------------------------------------------------------------- /tests/fixtures/test-loader.true.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": { 3 | "destination": { 4 | "address": { 5 | "socketAddress": { 6 | "portValue": 8000, 7 | "address": "10.25.95.68" 8 | } 9 | } 10 | }, 11 | "metadataContext": { 12 | "filterMetadata": { 13 | "envoy.filters.http.header_to_metadata": { 14 | "policy_type": "ingress" 15 | } 16 | } 17 | }, 18 | "request": { 19 | "http": { 20 | "headers": { 21 | ":authority": "example-app", 22 | ":method": "GET", 23 | ":path": "/", 24 | "accept": "*/*", 25 | "authorization": "Basic ZXZlOnBhc3N3b3Jk", 26 | "content-length": "0", 27 | "user-agent": "curl/7.68.0-DEV", 28 | "x-ext-auth-allow": "yes", 29 | "x-forwarded-proto": "http", 30 | "x-request-id": "1455bbb0-0623-4810-a2c6-df73ffd8863a" 31 | }, 32 | "host": "example-app", 33 | "id": "8306787481883314548", 34 | "method": "GET", 35 | "path": "/", 36 | "protocol": "HTTP/1.1" 37 | } 38 | }, 39 | "source": { 40 | "address": { 41 | "socketAddress": { 42 | "portValue": 33772, 43 | "address": "10.25.95.69" 44 | } 45 | } 46 | } 47 | }, 48 | "parsed_body": {}, 49 | "parsed_path": [], 50 | "parsed_query": {} 51 | } 52 | -------------------------------------------------------------------------------- /tests/fixtures/test-rand.rego: -------------------------------------------------------------------------------- 1 | # Copyright 2022 The Matrix.org Foundation C.I.C. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | package fixtures 16 | 17 | import rego.v1 18 | 19 | zero := rand.intn("zero", 0) 20 | 21 | first := rand.intn("first", 10) 22 | 23 | second := rand.intn("second", 100) 24 | 25 | cache := rand.intn("second", 100) 26 | -------------------------------------------------------------------------------- /tests/fixtures/test-time.rego: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import rego.v1 4 | 5 | now_ns := time.now_ns() 6 | 7 | # still not implemented 8 | # parse_ns = time.parse_ns("2006-01-01", "2022-01-01") 9 | # parse_ns1 = time.parse_ns("2006-01-01", "2022-01-08") 10 | 11 | parse_rfc3339_ns := time.parse_rfc3339_ns("2022-07-31T12:22:40.727411+00:00") 12 | 13 | parse_duration_ns_us := time.parse_duration_ns("1us") 14 | 15 | parse_duration_ns_us1 := time.parse_duration_ns("1ns") 16 | 17 | parse_duration_ns_us2 := time.parse_duration_ns("1µs") 18 | 19 | parse_duration_ns_ms := time.parse_duration_ns("1ms") 20 | 21 | parse_duration_ns_s := time.parse_duration_ns("1s") 22 | 23 | parse_duration_ns_m := time.parse_duration_ns("1m") 24 | 25 | parse_duration_ns_h := time.parse_duration_ns("1h") 26 | 27 | date := time.date(1659996459131330000) 28 | 29 | date_by_tz := time.date([1659996459131330000, "Europe/Paris"]) 30 | 31 | clock := time.clock(1659996459131330000) 32 | 33 | clock_tz := time.clock([1659996459131330000, "Europe/Paris"]) 34 | 35 | weekday := time.weekday(1659996459131330000) 36 | 37 | weekday2 := time.weekday([1659996459131330000, "Europe/Paris"]) 38 | 39 | add_date := time.add_date(1659996459131330000, 1, 1, 1) 40 | 41 | # still not implemented 42 | # diff := time.diff(1659996459131330000, 1659017824635051000) 43 | # diff2 := time.diff([1659996459131330000, "Europe/Paris"], [1658997582413084200, "Europe/Paris"]) 44 | -------------------------------------------------------------------------------- /tests/fixtures/test-units.rego: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import rego.v1 4 | 5 | v_bytes := units.parse_bytes("1K") 6 | 7 | v_bytes_nounit := units.parse_bytes("100") 8 | 9 | v_bytes_mib := units.parse_bytes("1MiB") 10 | 11 | v_bytes_lower := units.parse_bytes("200m") 12 | 13 | v_bytes_frac := units.parse_bytes("1.5mib") 14 | 15 | v_bytes_zero := units.parse_bytes("0M") 16 | 17 | v_decimal := units.parse("1K") 18 | 19 | v_decimal_nounit := units.parse("100") 20 | 21 | v_decimal_lower := units.parse("1mb") 22 | 23 | v_decimal_mili := units.parse("200m") 24 | 25 | v_decimal_frac := units.parse("1.5M") 26 | 27 | v_decimal_zero := units.parse("0M") 28 | -------------------------------------------------------------------------------- /tests/fixtures/test-urlquery.rego: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import rego.v1 4 | 5 | encode_1 := urlquery.encode("?foo=1&bar=test") 6 | 7 | encode_2 := urlquery.decode("&&&&") 8 | 9 | encode_3 := urlquery.decode("====") 10 | 11 | encode_4 := urlquery.decode("&=&=") 12 | 13 | encode_object := urlquery.encode_object({"foo": "1", "bar": "foo&foo", "arr": ["foo", "bar"], "obj": {"obj1", "obj2"}}) 14 | 15 | decode := urlquery.decode("%3Ffoo%3D1%26bar%3Dtest") 16 | 17 | decode_object := urlquery.decode_object("arr=foo&arr=bar&bar=test&foo=1&obj=obj1&obj=obj2") 18 | -------------------------------------------------------------------------------- /tests/fixtures/test-yaml.rego: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import rego.v1 4 | 5 | is_valid := yaml.is_valid("--\nfoo: bar num: 1\n") 6 | 7 | is_valid_1 := yaml.is_valid("foo: bar\nnum: 1") 8 | 9 | marshal := yaml.marshal({"foo": "bar", "num": 1}) 10 | 11 | unmarshal := yaml.unmarshal("foo: bar\nnum: 1\nnum2: 2") 12 | -------------------------------------------------------------------------------- /tests/smoke_test.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::path::Path; 16 | 17 | use anyhow::Result as AnyResult; 18 | use insta::assert_yaml_snapshot; 19 | use opa_wasm::{read_bundle, Runtime, TestContext}; 20 | use wasmtime::{Config, Engine, Module, Store}; 21 | 22 | macro_rules! integration_test { 23 | ($name:ident, $suite:expr) => { 24 | #[tokio::test] 25 | async fn $name() { 26 | assert_yaml_snapshot!(test_policy($suite, None) 27 | .await 28 | .expect("error in test suite")); 29 | } 30 | }; 31 | ($name:ident, $suite:expr, input = $input:expr) => { 32 | #[tokio::test] 33 | async fn $name() { 34 | assert_yaml_snapshot!(test_policy($suite, Some($input)) 35 | .await 36 | .expect("error in test suite")); 37 | } 38 | }; 39 | } 40 | 41 | async fn eval_policy( 42 | bundle: &str, 43 | entrypoint: &str, 44 | input: &serde_json::Value, 45 | ) -> AnyResult { 46 | let module = read_bundle(bundle).await?; 47 | 48 | // Configure the WASM runtime 49 | let mut config = Config::new(); 50 | config.async_support(true); 51 | 52 | let engine = Engine::new(&config)?; 53 | 54 | let module = Module::new(&engine, module)?; 55 | 56 | // Create a store which will hold the module instance 57 | let mut store = Store::new(&engine, ()); 58 | 59 | let ctx = TestContext::default(); 60 | 61 | // Instantiate the module 62 | let runtime = Runtime::new_with_evaluation_context(&mut store, &module, ctx).await?; 63 | 64 | let policy = runtime.without_data(&mut store).await?; 65 | 66 | // Evaluate the policy 67 | let p: serde_json::Value = policy.evaluate(&mut store, entrypoint, &input).await?; 68 | Ok(p) 69 | } 70 | 71 | fn bundle(name: &str) -> String { 72 | Path::new("tests/fixtures") 73 | .join(name) 74 | .to_string_lossy() 75 | .into() 76 | } 77 | 78 | fn input(name: &str) -> String { 79 | Path::new("tests/fixtures") 80 | .join(name) 81 | .to_string_lossy() 82 | .into() 83 | } 84 | 85 | async fn test_policy(bundle_name: &str, data: Option<&str>) -> AnyResult { 86 | let input = if let Some(data) = data { 87 | let input_bytes = tokio::fs::read(input(&format!("{}.json", data))).await?; 88 | serde_json::from_slice(&input_bytes[..])? 89 | } else { 90 | serde_json::Value::Object(serde_json::Map::default()) 91 | }; 92 | eval_policy( 93 | &bundle(&format!("{}.rego.tar.gz", bundle_name)), 94 | "fixtures", 95 | &input, 96 | ) 97 | .await 98 | } 99 | 100 | #[tokio::test] 101 | async fn infra_loader_works() { 102 | let module = read_bundle("tests/fixtures/test-loader.rego.tar.gz") 103 | .await 104 | .unwrap(); 105 | 106 | // Look for the WASM magic preamble 107 | assert_eq!(module[..4], [0x00, 0x61, 0x73, 0x6D]); 108 | // And for the WASM binary format version 109 | assert_eq!(module[4..8], [0x01, 0x00, 0x00, 0x00]); 110 | } 111 | 112 | integration_test!( 113 | test_loader_false, 114 | "test-loader", 115 | input = "test-loader.false" 116 | ); 117 | integration_test!(test_loader_true, "test-loader", input = "test-loader.true"); 118 | integration_test!(test_loader_empty, "test-loader"); 119 | integration_test!(test_units, "test-units"); 120 | integration_test!(test_rand, "test-rand"); 121 | integration_test!(test_yaml, "test-yaml"); 122 | integration_test!(test_urlquery, "test-urlquery"); 123 | integration_test!(test_time, "test-time"); 124 | 125 | /* 126 | #[tokio::test] 127 | async fn test_uuid() { 128 | assert_yaml_snapshot!(test_policy("test-uuid", "test-uuid").await); 129 | } 130 | */ 131 | -------------------------------------------------------------------------------- /tests/snapshots/smoke_test__loader_empty.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/smoke_test.rs 3 | expression: "test_policy(\"test-loader\", None).await.expect(\"error in test suite\")" 4 | --- 5 | - result: 6 | allow: false 7 | 8 | -------------------------------------------------------------------------------- /tests/snapshots/smoke_test__loader_false.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/smoke_test.rs 3 | expression: "test_policy(\"test-loader\",\n Some(\"test-loader.false\")).await.expect(\"error in test suite\")" 4 | --- 5 | - result: 6 | allow: false 7 | 8 | -------------------------------------------------------------------------------- /tests/snapshots/smoke_test__loader_true.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/smoke_test.rs 3 | expression: "test_policy(\"test-loader\",\n Some(\"test-loader.true\")).await.expect(\"error in test suite\")" 4 | --- 5 | - result: 6 | allow: true 7 | 8 | -------------------------------------------------------------------------------- /tests/snapshots/smoke_test__rand.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/smoke_test.rs 3 | expression: "test_policy(\"test-rand\", None).await.expect(\"error in test suite\")" 4 | --- 5 | - result: 6 | cache: 73 7 | first: 7 8 | second: 73 9 | zero: 0 10 | 11 | -------------------------------------------------------------------------------- /tests/snapshots/smoke_test__time.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/smoke_test.rs 3 | expression: "test_policy(\"test-time\", None).await.expect(\"error in test suite\")" 4 | --- 5 | - result: 6 | add_date: 1694297259131330000 7 | clock: 8 | - 22 9 | - 7 10 | - 39 11 | clock_tz: 12 | - 0 13 | - 7 14 | - 39 15 | date: 16 | - 2022 17 | - 8 18 | - 8 19 | date_by_tz: 20 | - 2022 21 | - 8 22 | - 9 23 | now_ns: 1594731202000000000 24 | parse_duration_ns_h: 3600000000000 25 | parse_duration_ns_m: 60000000000 26 | parse_duration_ns_ms: 1000000 27 | parse_duration_ns_s: 1000000000 28 | parse_duration_ns_us: 1000 29 | parse_duration_ns_us1: 1 30 | parse_duration_ns_us2: 1000 31 | parse_rfc3339_ns: 1659270160727411000 32 | weekday: Monday 33 | weekday2: Tuesday 34 | 35 | -------------------------------------------------------------------------------- /tests/snapshots/smoke_test__units.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/smoke_test.rs 3 | expression: "test_policy(\"test-units\", None).await.expect(\"error in test suite\")" 4 | --- 5 | - result: 6 | v_bytes: 1000 7 | v_bytes_frac: 1572864 8 | v_bytes_lower: 200000000 9 | v_bytes_mib: 1048576 10 | v_bytes_nounit: 100 11 | v_bytes_zero: 0 12 | v_decimal: 1000 13 | v_decimal_frac: 1500000 14 | v_decimal_lower: 1000000 15 | v_decimal_mili: 0.2 16 | v_decimal_nounit: 100 17 | v_decimal_zero: 0 18 | 19 | -------------------------------------------------------------------------------- /tests/snapshots/smoke_test__urlquery.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/smoke_test.rs 3 | expression: "test_policy(\"test-urlquery\", None).await.expect(\"error in test suite\")" 4 | --- 5 | - result: 6 | decode: "?foo=1&bar=test" 7 | decode_object: 8 | arr: 9 | - foo 10 | - bar 11 | bar: 12 | - test 13 | foo: 14 | - "1" 15 | obj: 16 | - obj1 17 | - obj2 18 | encode_1: "%3Ffoo%3D1%26bar%3Dtest" 19 | encode_2: "&&&&" 20 | encode_3: "====" 21 | encode_4: "&=&=" 22 | encode_object: arr=foo&arr=bar&bar=foo%26foo&foo=1&obj=obj1&obj=obj2 23 | 24 | -------------------------------------------------------------------------------- /tests/snapshots/smoke_test__yaml.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/smoke_test.rs 3 | expression: "test_policy(\"test-yaml\", None).await.expect(\"error in test suite\")" 4 | --- 5 | - result: 6 | is_valid: false 7 | is_valid_1: true 8 | marshal: "foo: bar\nnum: 1\n" 9 | unmarshal: 10 | foo: bar 11 | num: 1 12 | num2: 2 13 | 14 | --------------------------------------------------------------------------------