Compare commits

...

13 Commits

Author SHA1 Message Date
Anand Krishnamoorthi
1ab27b253b chore: release (#121)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-01 16:16:33 -08:00
Anand Krishnamoorthi
761d11ef48 Document bindings (#119)
* Instructions for WASM/JS binding

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* Document Python, WASM/JS bindings

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-02-01 13:47:27 -08:00
Anand Krishnamoorthi
beea2274d3 Conform to OPA 0.61.0. (#118)
Implement `import rego.v1`
https://www.openpolicyagent.org/docs/latest/policy-language/#the-regov1-import

- `if` required before rule body
- import rego.v1 automatically imports future.keywords
- handle import shadowing
- data, input cannot be shadowed
- deprecated functions as disallowed
- rules must have assignment or body
- `contains` required for parital set

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-01-31 21:39:02 -08:00
Anand Krishnamoorthi
5799a3e6c4 Update publish-python.yml 2024-01-28 23:55:15 -08:00
Anand Krishnamoorthi
ca91c0ea20 Publish python packages (#117)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-01-28 23:12:06 -08:00
Anand Krishnamoorthi
bf75813c43 Publish wasm (#116)
* Create jekyll-gh-pages.yml

* Fix publish-wasm

- --release instead of -r
- set working directory
- use v4

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-01-28 16:45:52 -08:00
Anand Krishnamoorthi
35ec9c03ad Set working-directory for wasm-pack 2024-01-28 15:30:41 -08:00
Anand Krishnamoorthi
8ca863c661 Python bindings (#115)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-01-28 14:39:59 -08:00
Anand Krishnamoorthi
055bdd295f WASM binding (#114)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-01-27 22:14:29 -08:00
Anand Krishnamoorthi
0af97840f7 chore: release (#112)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-01-19 15:07:49 -08:00
Anand Krishnamoorthi
6eca85b497 Improve crate documentation (#111)
- Document QueryResults
- Delete snippets folder
- Document Value

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-01-19 14:51:14 -08:00
Anand Krishnamoorthi
f3884e87e5 Try out manual trigger for release-plz (#110) 2024-01-16 10:49:39 -08:00
Anand Krishnamoorthi
d39200a52c - Document Location, Expression, QueryResult (#109)
Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2024-01-15 17:21:49 -08:00
41 changed files with 3327 additions and 318 deletions

114
.github/workflows/publish-python.yml vendored Normal file
View File

@@ -0,0 +1,114 @@
# This file is autogenerated by maturin v1.4.0
# To update, run
#
# maturin generate-ci --manifest-path bindings/python/Cargo.toml github
#
name: publish-python
on:
workflow_dispatch:
permissions:
contents: read
jobs:
linux:
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter --manifest-path bindings/python/Cargo.toml
sccache: 'true'
manylinux: auto
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
windows:
runs-on: windows-latest
strategy:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
architecture: ${{ matrix.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter --manifest-path bindings/python/Cargo.toml
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
macos:
runs-on: macos-latest
strategy:
matrix:
target: [x86_64, aarch64, universal2-apple-darwin]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter --manifest-path bindings/python/Cargo.toml
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist --manifest-path bindings/python/Cargo.toml
- name: Upload sdist
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
release:
name: Release
runs-on: ubuntu-latest
# Commented out for initial release.
# if: "startsWith(github.ref, 'refs/tags/')"
needs: [linux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v3
with:
name: wheels
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
command: upload
args: --non-interactive --skip-existing *

32
.github/workflows/publish-wasm.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: publish-wasm
permissions:
pull-requests: write
contents: write
on: workflow_dispatch
jobs:
publish-wasm:
name: publish
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build
run: wasm-pack build --target nodejs --release
working-directory: ./bindings/wasm
- name: Publish
run: wasm-pack publish --target nodejs
working-directory: ./bindings/wasm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}

View File

@@ -4,10 +4,7 @@ permissions:
pull-requests: write
contents: write
on:
push:
branches:
- main
on: workflow_dispatch
jobs:
release-plz:
@@ -24,4 +21,4 @@ jobs:
uses: MarcoIeni/release-plz-action@v0.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

View File

@@ -27,11 +27,21 @@ jobs:
- name: Clippy
run: cargo clippy --all-targets --no-deps -- -Dwarnings
- name: Run tests
run: cargo test --verbose
run: cargo test -r --verbose
- name: Build (MUSL)
run: cargo build --verbose --all-targets --target x86_64-unknown-linux-musl
- name: Run tests (MUSL)
run: cargo test --verbose --target x86_64-unknown-linux-musl
run: cargo test -r --verbose --target x86_64-unknown-linux-musl
- name: Run tests (ACI)
run: cargo test -r --test aci
- name: Run tests (OPA Conformance)
run: >-
cargo test --test opa -- $(tr '\n' ' ' < tests/opa.passing)
cargo test -r --test opa -- $(tr '\n' ' ' < tests/opa.passing)
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Run wasm binding tests
run: |
cd bindings/wasm
wasm-pack test --node -r

View File

@@ -6,6 +6,136 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.1.0-alpha.3](https://github.com/microsoft/regorus/compare/regorus-v0.1.0-alpha.2...regorus-v0.1.0-alpha.3) - 2024-02-01
### Fixed
- fix bitwise.and and add tests ([#19](https://github.com/microsoft/regorus/pull/19))
### Other
- Document bindings ([#119](https://github.com/microsoft/regorus/pull/119))
- Conform to OPA 0.61.0. ([#118](https://github.com/microsoft/regorus/pull/118))
- Update publish-python.yml
- Publish python packages ([#117](https://github.com/microsoft/regorus/pull/117))
- Publish wasm ([#116](https://github.com/microsoft/regorus/pull/116))
- Set working-directory for wasm-pack
- Python bindings ([#115](https://github.com/microsoft/regorus/pull/115))
- WASM binding ([#114](https://github.com/microsoft/regorus/pull/114))
- release ([#112](https://github.com/microsoft/regorus/pull/112))
- Improve crate documentation ([#111](https://github.com/microsoft/regorus/pull/111))
- Try out manual trigger for release-plz ([#110](https://github.com/microsoft/regorus/pull/110))
- - Document Location, Expression, QueryResult ([#109](https://github.com/microsoft/regorus/pull/109))
- Update Cargo.toml ([#108](https://github.com/microsoft/regorus/pull/108))
- Change version to `0.1.0-alpha.1` ([#107](https://github.com/microsoft/regorus/pull/107))
- Add crate documentation ([#106](https://github.com/microsoft/regorus/pull/106))
- Release preparation ([#105](https://github.com/microsoft/regorus/pull/105))
- Update READEME.md with current status, grammar etc. ([#102](https://github.com/microsoft/regorus/pull/102))
- Implement builtin `time.parse_duration_ns` method ([#100](https://github.com/microsoft/regorus/pull/100))
- Implement import keyword ([#101](https://github.com/microsoft/regorus/pull/101))
- OPA conformance: Pass refheads test suite ([#90](https://github.com/microsoft/regorus/pull/90))
- OPA conformance: Ensure that `withkeyword` OPA tests pass ([#88](https://github.com/microsoft/regorus/pull/88))
- Handle walk builtin as a loop expression ([#86](https://github.com/microsoft/regorus/pull/86))
- Implement most of the builtin `time` module ([#82](https://github.com/microsoft/regorus/pull/82))
- OPA Conformance
- OPA conformance ([#81](https://github.com/microsoft/regorus/pull/81))
- More OPA conformance ([#77](https://github.com/microsoft/regorus/pull/77))
- OPA conformance ([#71](https://github.com/microsoft/regorus/pull/71))
- Builtin UUID module ([#68](https://github.com/microsoft/regorus/pull/68))
- Add tests for builtin `string::format_int` method ([#65](https://github.com/microsoft/regorus/pull/65))
- More builtins and semantic improvements ([#66](https://github.com/microsoft/regorus/pull/66))
- More OPA conformance; in-progress: ability to trace interpreter ([#63](https://github.com/microsoft/regorus/pull/63))
- More OPA conformant semantics ([#62](https://github.com/microsoft/regorus/pull/62))
- Updated readme. Added bundle support. ([#61](https://github.com/microsoft/regorus/pull/61))
- crypto builtins ([#57](https://github.com/microsoft/regorus/pull/57))
- Regex and Glob builtins ([#56](https://github.com/microsoft/regorus/pull/56))
- Formalize concept of a Number ([#55](https://github.com/microsoft/regorus/pull/55))
- Lock down ACI tests and more OPA test folders ([#54](https://github.com/microsoft/regorus/pull/54))
- Fix scheduling regression ([#53](https://github.com/microsoft/regorus/pull/53))
- add full api to engine ([#50](https://github.com/microsoft/regorus/pull/50))
- Use Rc<str> instead of string. ([#52](https://github.com/microsoft/regorus/pull/52))
- More library functions ([#51](https://github.com/microsoft/regorus/pull/51))
- Added semver.is_valid and semver.compare ([#49](https://github.com/microsoft/regorus/pull/49))
- OPA conformance tests ([#45](https://github.com/microsoft/regorus/pull/45))
- Avoid dependency on `source lifetime. ([#43](https://github.com/microsoft/regorus/pull/43))
- Allow with modifier for builtin and user functions ([#42](https://github.com/microsoft/regorus/pull/42))
- Special cases of refs to data ([#41](https://github.com/microsoft/regorus/pull/41))
- Fix scheduling statements that don't create bindings ([#40](https://github.com/microsoft/regorus/pull/40))
- Ability to run the OPA testsuite ([#39](https://github.com/microsoft/regorus/pull/39))
- Engine ([#38](https://github.com/microsoft/regorus/pull/38))
- Use Ref for storing ast nodes in collections. ([#37](https://github.com/microsoft/regorus/pull/37))
- all, any deprecated functions ([#35](https://github.com/microsoft/regorus/pull/35))
- all, any deprecated functions ([#34](https://github.com/microsoft/regorus/pull/34))
- Improvements ([#33](https://github.com/microsoft/regorus/pull/33))
- Order query expression results ([#32](https://github.com/microsoft/regorus/pull/32))
- Scheduling of statements in user queries ([#31](https://github.com/microsoft/regorus/pull/31))
- eval, lex, parse commands ([#30](https://github.com/microsoft/regorus/pull/30))
- eval_user_query for OPA style results ([#29](https://github.com/microsoft/regorus/pull/29))
- Arity for builtins ([#28](https://github.com/microsoft/regorus/pull/28))
- Handle chained _ ([#27](https://github.com/microsoft/regorus/pull/27))
- Minimize PR 22 ([#26](https://github.com/microsoft/regorus/pull/26))
- improve errors location ([#23](https://github.com/microsoft/regorus/pull/23))
- Fix clippy warning ([#25](https://github.com/microsoft/regorus/pull/25))
- negation of an undefined value should return true ([#21](https://github.com/microsoft/regorus/pull/21))
- Ensure that scopes are cleaned up correctly upon error. ([#20](https://github.com/microsoft/regorus/pull/20))
- support of or-functions ([#18](https://github.com/microsoft/regorus/pull/18))
- Statement Scheduler Implementation
- Remove unnecessary lifetime
- json.filter, object.filter, object.get, object.keys, object.remove
- :to_number builtin
- :trace builtin
- bitwise builtins
- :print builtin
- Partial sprintf implementation.
- All string functions except sprintf. TODO: Add tests
- More string functions without tests
- More string functions
- concat and contains
- string concat (WIP)
- Support build on non Linux platforms
- Prepare for upstreaming
- Test for multi-assign
- Support dependencies between vars defined in same statement
- Statement scheduler (WIP)
- Print small-form table of files without 100% coverage.
- Code tweaks to improve coverage
- Tests for aggregates builtins
- Tests for numbers builtins
- Tests for arrays builtins
- Tests for types functions
- Destructuring of arrays and objects in some-in expressions
- `some .. in` implementation
- Fix key, value in membership and some-in
- refactor
- Arrays and Aggregates
- Implement `every` statement ([#4](https://github.com/microsoft/regorus/pull/4))
- Set loop index variable if not "_" ([#3](https://github.com/microsoft/regorus/pull/3))
- Allow comprehensions in default value. ([#2](https://github.com/microsoft/regorus/pull/2))
- Lock down numbers
- mod function
- Builtin functions for numbers (WIP)
- Implement comparison operators. Formalize semantics.
- Rework assign operations ([#6](https://github.com/microsoft/regorus/pull/6))
- Locked down supported values in default rule.
- Improvements to github workflow ([#4](https://github.com/microsoft/regorus/pull/4))
- Update name to regorus
- Update rust.yml
- Add simple git action
- Add missing config.toml
- Update license to MIT
- Code from github.com/anakrish/rego-rs
- SUPPORT.md committed
- SECURITY.md committed
- README.md committed
- LICENSE committed
- CODE_OF_CONDUCT.md committed
- Initial commit
## [0.1.0-alpha.2](https://github.com/microsoft/regorus/compare/v0.1.0-alpha.1...v0.1.0-alpha.2) - 2024-01-19
### Other
- Improve crate documentation ([#111](https://github.com/microsoft/regorus/pull/111))
- Try out manual trigger for release-plz ([#110](https://github.com/microsoft/regorus/pull/110))
- - Document Location, Expression, QueryResult ([#109](https://github.com/microsoft/regorus/pull/109))
## [0.1.0-alpha.1](https://github.com/microsoft/regorus/releases/tag/v0.1.0-alpha.1) - 2024-01-15
### Fixed

View File

@@ -1,7 +1,14 @@
[workspace]
members = [
"bindings/python",
"bindings/wasm"
]
[package]
name = "regorus"
description = "A fast, lightweight Rego (OPA policy language) interpreter"
version = "0.1.0-alpha.1"
version = "0.1.0-alpha.3"
edition = "2021"
license-file = "LICENSE"
repository = "https://github.com/microsoft/regorus"

View File

@@ -2,11 +2,14 @@
**Regorus** is
- *Rego*-*Rus(t)* - A fast, light-weight [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) interpreter written in Rust.
- *Rego*-*Rus(t)* - A fast, light-weight [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/)
interpreter written in Rust.
- *Rigorous* - A rigorous enforcer of well-defined Rego semantics.
Regorus is available as a library that can be easily integrated into your Rust projects.
Here is an example of evaluating a simple Rego policy:
```rust
use anyhow::Result;
use regorus::*;
@@ -21,11 +24,11 @@ fn main() -> Result<()> {
// Filename to be associated with the policy.
"hello.rego".to_string(),
// Rego policy that just sets a message.
r#"
package test
message = "Hello, World!"
"#.to_string()
// Rego policy that just sets a message.
r#"
package test
message = "Hello, World!"
"#.to_string()
)?;
// Evaluate the policy, fetch the message and print it.
@@ -36,12 +39,39 @@ fn main() -> Result<()> {
}
```
Regorus passes the [OPA v0.60.0 test-suite](https://www.openpolicyagent.org/docs/latest/ir/#test-suite) barring a few builtins.
See [OPA Conformance](#opa-conformance) below.
Regorus is designed with [Confidential Computing](https://confidentialcomputing.io/about/) in mind. In Confidential Computing environments,
it is important to be able to control exactly what is being run. Regorus allows enabling and disabling various components using cargo
features. By default all features are enabled.
The default build of regorus example program is 6.4M:
```bash
$ cargo build -r --example regorus; strip target/release/examples/regorus; ls -lh target/release/examples/regorus
$ cargo build -r --example regorus; strip target/release/examples/regorus; ls -lh target/release/examples/regorus
-rwxr-xr-x 1 anand staff 6.4M Jan 19 11:23 target/release/examples/regorus*
```
When all features except for `yaml` are disabled, the binary size drops down to 2.9M.
```bash
$ cargo build -r --example regorus --features "yaml" --no-default-features; strip target/release/examples/regorus; ls -lh target/release/examples/regorus
-rwxr-xr-x 1 anand staff 2.9M Jan 19 11:26 target/release/examples/regorus*
```
Regorus passes the [OPA v0.61.0 test-suite](https://www.openpolicyagent.org/docs/latest/ir/#test-suite) barring a few
builtins. See [OPA Conformance](#opa-conformance) below.
## Bindings
Regorus can be used from a variety of languages:
- Javascript: To compile Regorus to WASM and use it in Javascript, see [bindings/wasm](bindings/wasm)
- Python: To use Regorus from Python, see [bindings/python](bindings/python)
## Getting Started
[examples/regorus](https://github.com/microsoft/regorus/blob/main/examples/regorus.rs) is an example program that shows how to integrate Regorus into your project and evaluate Rego policies.
[examples/regorus](https://github.com/microsoft/regorus/blob/main/examples/regorus.rs) is an example program that
shows how to integrate Regorus into your project and evaluate Rego policies.
To build and install it, do
@@ -94,7 +124,8 @@ This produces the following output
}
```
Next, evaluate a sample [policy](examples/example.rego) and [input](examples/input.json) (borrowed from [Rego tutorial](https://www.openpolicyagent.org/docs/latest/#2-try-opa-eval)):
Next, evaluate a sample [policy](examples/example.rego) and [input](examples/input.json)
(borrowed from [Rego tutorial](https://www.openpolicyagent.org/docs/latest/#2-try-opa-eval)):
```bash
$ regorus eval -d examples/example.rego -i examples/input.json data.example
@@ -168,7 +199,7 @@ Benchmark 1: opa eval -b tests/aci -d tests/aci/data.json -i tests/aci/input.jso
```
## OPA Conformance
Regorus has been verified to be compliant with [OPA v0.60.0](https://github.com/open-policy-agent/opa/releases/tag/v0.60.0)
Regorus has been verified to be compliant with [OPA v0.61.0](https://github.com/open-policy-agent/opa/releases/tag/v0.61.0)
using a [test driver](https://github.com/microsoft/regorus/blob/main/tests/opa.rs) that loads and runs the OPA testsuite using Regorus, and verifies that expected outputs
are produced.
@@ -178,7 +209,8 @@ The test driver can be invoked by running:
$ cargo test -r --test opa
```
Currently, Regorus passes all the non-builtin specific tests. See [passing tests suites](https://github.com/microsoft/regorus/blob/main/tests/opa.passing).
Currently, Regorus passes all the non-builtin specific tests.
See [passing tests suites](https://github.com/microsoft/regorus/blob/main/tests/opa.passing).
The following test suites don't pass fully due to mising builtins:
- `cryptoparsersaprivatekeys`
@@ -219,7 +251,9 @@ They are captured in the following [github issues](https://github.com/microsoft/
### Grammar
The grammar used by Regorus to parse Rego policies is described in [grammar.md](https://github.com/microsoft/regorus/blob/main/docs/grammar.md) in both [W3C EBNF](https://www.w3.org/Notation.html) and [RailRoad Diagram](https://en.wikipedia.org/wiki/Syntax_diagram) formats.
The grammar used by Regorus to parse Rego policies is described in [grammar.md](https://github.com/microsoft/regorus/blob/main/docs/grammar.md)
in both [W3C EBNF](https://www.w3.org/Notation.html) and [RailRoad Diagram](https://en.wikipedia.org/wiki/Syntax_diagram) formats.
## Contributing

View File

@@ -0,0 +1 @@
pyo3

View File

@@ -0,0 +1,14 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.0](https://github.com/microsoft/regorus/releases/tag/regoruspy-v0.1.0) - 2024-02-01
### Other
- Document bindings ([#119](https://github.com/microsoft/regorus/pull/119))
- Publish python packages ([#117](https://github.com/microsoft/regorus/pull/117))
- Python bindings ([#115](https://github.com/microsoft/regorus/pull/115))

View File

@@ -0,0 +1,20 @@
[package]
name = "regoruspy"
version = "0.1.0"
edition = "2021"
repository = "https://github.com/microsoft/regorus/bindings/python"
description = "Python bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
keywords = ["interpreter", "opa", "policy-as-code", "rego"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
anyhow = "1.0.79"
ordered-float = "4.2.0"
pyo3 = {version = "0.20.2", features = ["anyhow", "extension-module"] }
regorus = { path = "../.." }
serde_json = "1.0.112"

67
bindings/python/README.md Normal file
View File

@@ -0,0 +1,67 @@
# regorus
**Regorus** is
- *Rego*-*Rus(t)* - A fast, light-weight [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/)
interpreter written in Rust.
- *Rigorous* - A rigorous enforcer of well-defined Rego semantics.
Regorus can be used in Python via `regorus` package. (It is not yet available in PyPI, but can be manually built.)
See [Repository](https://github.com/microsoft/regorus).
To build this binding, see [building](https://github.com/microsoft/regorus/bindings/python/building.md)
## Usage
```Python
import regorus
# Create engine
engine = regorus.Engine()
# Load policies
engine.add_policy_from_file('../../tests/aci/framework.rego')
engine.add_policy_from_file('../../tests/aci/api.rego')
engine.add_policy_from_file('../../tests/aci/policy.rego')
# Add policy data
data = {
"metadata": {
"devices": {
"/run/layers/p0-layer0": "1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766",
"/run/layers/p0-layer1": "e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c",
"/run/layers/p0-layer2": "eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79",
"/run/layers/p0-layer3": "41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156",
"/run/layers/p0-layer4": "4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c",
"/run/layers/p0-layer5": "fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a"
}
}
}
engine.add_data(data)
# Set input
input = {
"containerID": "container0",
"layerPaths": [
"/run/layers/p0-layer0",
"/run/layers/p0-layer1",
"/run/layers/p0-layer2",
"/run/layers/p0-layer3",
"/run/layers/p0-layer4",
"/run/layers/p0-layer5"
],
"target": "/run/gcs/c/container0/rootfs"
}
engine.set_input(input)
# Eval query
results = engine.eval_query('data.framework.mount_overlay=x')
# Print results
print(results['result'][0])
# Eval query as json
results_json = engine.eval_query_as_json('data.framework.mount_overlay=x')
print(results_json)
```

View File

@@ -0,0 +1,22 @@
- Install maturin
```
pipx install maturin
```
See [Maturin User Guide](https://www.maturin.rs)
- Build bindings for Python
```
cd bindings/python
maturin build --release --target-dir wheels
```
- Install python wheel
```
pip3 install ../../target/wheels/regorus*.whl --force-reinstall
```
- Run test script
```
python3 test.py
```

View File

@@ -0,0 +1,16 @@
[build-system]
requires = ["maturin>=1.4,<2.0"]
build-backend = "maturin"
[project]
name = "regorus"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dynamic = ["version"]
[tool.maturin]
features = ["pyo3/extension-module"]

301
bindings/python/src/lib.rs Normal file
View File

@@ -0,0 +1,301 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use anyhow::{anyhow, Result};
use pyo3::exceptions::PyTypeError;
use pyo3::prelude::*;
use pyo3::types::*;
use std::collections::{BTreeMap, BTreeSet};
use ::regorus::Value;
/// Regorus engine.
#[pyclass(unsendable)]
pub struct Engine {
engine: ::regorus::Engine,
}
impl Default for Engine {
fn default() -> Self {
Self::new()
}
}
impl Clone for Engine {
/// Clone a [`Engine`]
///
/// To avoid having to parse same policy again, the engine can be cloned
/// after policies and data have been added.
fn clone(&self) -> Self {
Self {
engine: self.engine.clone(),
}
}
}
fn from<'source>(ob: &'source PyAny) -> Result<Value, PyErr> {
// dicts
Ok(if let Ok(dict) = ob.downcast::<PyDict>() {
let mut map = BTreeMap::new();
for (k, v) in dict {
map.insert(from(k)?, from(v)?);
}
map.into()
}
// set
else if let Ok(pset) = ob.downcast::<PySet>() {
let mut set = BTreeSet::new();
for v in pset {
set.insert(from(v)?);
}
set.into()
}
// frozen set
else if let Ok(pfset) = ob.downcast::<PyFrozenSet>() {
//
let mut set = BTreeSet::new();
for v in pfset {
set.insert(from(v)?);
}
set.into()
}
// lists and tuples
else if let Ok(plist) = ob.downcast::<PyList>() {
let mut array = Vec::new();
for v in plist {
array.push(from(v)?);
}
array.into()
} else if let Ok(ptuple) = ob.downcast::<PyTuple>() {
let mut array = Vec::new();
for v in ptuple {
array.push(from(v)?);
}
array.into()
}
// String
else if let Ok(s) = String::extract(ob) {
s.into()
}
// Numeric
else if let Ok(v) = i64::extract(ob) {
v.into()
} else if let Ok(v) = u64::extract(ob) {
v.into()
} else if let Ok(v) = f64::extract(ob) {
v.into()
}
// Boolean
else if let Ok(b) = bool::extract(ob) {
b.into()
}
// None
else if ob.downcast::<PyNone>().is_ok() {
Value::Null
}
// Anything that is a sequence
else if let Ok(pseq) = ob.downcast::<PySequence>() {
let mut array = Vec::new();
for i in 0..pseq.len()? {
array.push(from(pseq.get_item(i)?)?);
}
array.into()
}
// Anything that is a map
else if let Ok(pmap) = ob.downcast::<PyMapping>() {
let mut map = BTreeMap::new();
let keys = pmap.keys()?;
let values = pmap.values()?;
for i in 0..keys.len()? {
let key = keys.get_item(i)?;
let value = values.get_item(i)?;
map.insert(from(key)?, from(value)?);
}
map.into()
} else {
return Err(PyErr::new::<PyTypeError, _>(
"object cannot be converted to RegoValue",
));
})
}
fn to(mut v: Value, py: Python<'_>) -> Result<PyObject> {
Ok(match v {
Value::Null => None::<u64>.to_object(py),
// TODO: Revisit this mapping
Value::Undefined => None::<u64>.to_object(py),
Value::Bool(b) => b.to_object(py),
Value::String(s) => s.to_object(py),
Value::Number(_) => {
if let Ok(f) = v.as_f64() {
f.to_object(py)
} else if let Ok(u) = v.as_u64() {
u.to_object(py)
} else {
v.as_i64()?.to_object(py)
}
}
Value::Array(_) => {
let list = PyList::empty(py);
for v in std::mem::replace(v.as_array_mut()?, Vec::new()) {
list.append(to(v, py)?)?;
}
list.into()
}
Value::Set(_) => {
let set = PySet::empty(py)?;
for v in std::mem::replace(v.as_set_mut()?, BTreeSet::new()) {
set.add(to(v, py)?)?;
}
set.into()
}
Value::Object(_) => {
let dict = PyDict::new(py);
for (k, v) in std::mem::replace(v.as_object_mut()?, BTreeMap::new()) {
dict.set_item(to(k, py)?, to(v, py)?)?;
}
dict.into()
}
})
}
#[pymethods]
impl Engine {
/// Construct a new Engine
#[new]
pub fn new() -> Self {
Self {
engine: ::regorus::Engine::new(),
}
}
/// Add a policy
///
/// The policy is parsed into AST.
///
/// * `path`: A filename to be associated with the policy.
/// * `rego`: Rego policy.
pub fn add_policy(&mut self, path: String, rego: String) -> Result<()> {
self.engine.add_policy(path, rego)
}
/// Add a policy from given file.
///
/// The policy is parsed into AST.
///
/// * `path`: Path to the policy file.
pub fn add_policy_from_file(&mut self, path: String) -> Result<()> {
self.engine.add_policy_from_file(path)
}
/// Add policy data.
///
/// * `data`: Rego value. A Rego value is a number, bool, string, None
/// or a list/set/map whose items themselves are Rego values.
pub fn add_data(&mut self, data: &PyAny) -> Result<()> {
let data = from(data)?;
self.engine.add_data(data)
}
/// Add policy data.
///
/// * `data`: JSON encoded value to be used as policy data.
pub fn add_data_json(&mut self, data: String) -> Result<()> {
let data = Value::from_json_str(&data)?;
self.engine.add_data(data)
}
/// Add policy data from file.
///
/// * `path`: Path to JSON policy data.
pub fn add_data_from_json_file(&mut self, path: String) -> Result<()> {
let data = Value::from_json_file(&path)?;
self.engine.add_data(data)
}
/// Clear policy data.
pub fn clear_data(&mut self) -> Result<()> {
self.engine.clear_data();
Ok(())
}
/// Set input.
///
/// * `input`: Rego value. A Rego value is a number, bool, string, None
/// or a list/set/map whose items themselves are Rego values.
pub fn set_input(&mut self, input: &PyAny) -> Result<()> {
let input = from(input)?;
self.engine.set_input(input);
Ok(())
}
/// Set input.
///
/// * `input`: JSON encoded value to be used as input to query.
pub fn set_input_json(&mut self, input: String) -> Result<()> {
let input = Value::from_json_str(&input)?;
self.engine.set_input(input);
Ok(())
}
/// Set input.
///
/// * `path`: Path to JSON input data.
pub fn set_input_from_json_file(&mut self, path: String) -> Result<()> {
let input = Value::from_json_file(&path)?;
self.engine.set_input(input);
Ok(())
}
/// Evaluate query.
///
/// * `query`: Rego expression to be evaluate.
pub fn eval_query(&mut self, query: String, py: Python<'_>) -> Result<PyObject> {
let results = self.engine.eval_query(query, false)?;
let rlist = PyList::empty(py);
for result in results.result.into_iter() {
let rdict = PyDict::new(py);
let elist = PyList::empty(py);
for expr in result.expressions.into_iter() {
let edict = PyDict::new(py);
edict.set_item("value".to_object(py), to(expr.value, py)?)?;
edict.set_item("text".to_object(py), expr.text.as_ref().to_object(py))?;
let ldict = PyDict::new(py);
ldict.set_item("row".to_object(py), expr.location.row.to_object(py))?;
ldict.set_item("col".to_object(py), expr.location.col.to_object(py))?;
edict.set_item("location".to_object(py), ldict)?;
elist.append(edict)?;
}
rdict.set_item("expressions".to_object(py), elist)?;
rdict.set_item("bindings".to_object(py), to(result.bindings, py)?)?;
rlist.append(rdict)?;
}
let dict = PyDict::new(py);
dict.set_item("result".to_object(py), rlist)?;
Ok(dict.into())
}
/// Evaluate query. Returns result as JSON.
///
/// * `query`: Rego expression to be evaluate.
pub fn eval_query_as_json(&mut self, query: String) -> Result<String> {
let results = self.engine.eval_query(query, false)?;
serde_json::to_string_pretty(&results).map_err(|e| anyhow!("{e}"))
}
}
#[pymodule]
pub fn regorus(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_class::<crate::Engine>()
}

52
bindings/python/test.py Normal file
View File

@@ -0,0 +1,52 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
import regorus
# Create engine
engine = regorus.Engine()
# Load policies
engine.add_policy_from_file('../../tests/aci/framework.rego')
engine.add_policy_from_file('../../tests/aci/api.rego')
engine.add_policy_from_file('../../tests/aci/policy.rego')
# Add policy data
data = {
"metadata": {
"devices": {
"/run/layers/p0-layer0": "1b80f120dbd88e4355d6241b519c3e25290215c469516b49dece9cf07175a766",
"/run/layers/p0-layer1": "e769d7487cc314d3ee748a4440805317c19262c7acd2fdbdb0d47d2e4613a15c",
"/run/layers/p0-layer2": "eb36921e1f82af46dfe248ef8f1b3afb6a5230a64181d960d10237a08cd73c79",
"/run/layers/p0-layer3": "41d64cdeb347bf236b4c13b7403b633ff11f1cf94dbc7cf881a44d6da88c5156",
"/run/layers/p0-layer4": "4dedae42847c704da891a28c25d32201a1ae440bce2aecccfa8e6f03b97a6a6c",
"/run/layers/p0-layer5": "fe84c9d5bfddd07a2624d00333cf13c1a9c941f3a261f13ead44fc6a93bc0e7a"
}
}
}
engine.add_data(data)
# Set input
input = {
"containerID": "container0",
"layerPaths": [
"/run/layers/p0-layer0",
"/run/layers/p0-layer1",
"/run/layers/p0-layer2",
"/run/layers/p0-layer3",
"/run/layers/p0-layer4",
"/run/layers/p0-layer5"
],
"target": "/run/gcs/c/container0/rootfs"
}
engine.set_input(input)
# Eval query
results = engine.eval_query('data.framework.mount_overlay=x')
# Print results
print(results['result'][0])
# Eval query as json
results_json = engine.eval_query_as_json('data.framework.mount_overlay=x')
print(results_json)

View File

@@ -0,0 +1,14 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.0](https://github.com/microsoft/regorus/releases/tag/regorusjs-v0.1.0) - 2024-02-01
### Other
- Document bindings ([#119](https://github.com/microsoft/regorus/pull/119))
- Python bindings ([#115](https://github.com/microsoft/regorus/pull/115))
- WASM binding ([#114](https://github.com/microsoft/regorus/pull/114))

19
bindings/wasm/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "regorusjs"
version = "0.1.0"
edition = "2021"
repository = "https://github.com/microsoft/regorus/bindings/wasm"
description = "WASM bindings for Regorus - a fast, lightweight Rego interpreter written in Rust"
keywords = ["interpreter", "opa", "policy-as-code", "rego"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
regorus = { path = "../.." }
serde_json = "1.0.111"
wasm-bindgen = "0.2.90"
[dev-dependencies]
wasm-bindgen-test = "0.3.40"

81
bindings/wasm/README.md Normal file
View File

@@ -0,0 +1,81 @@
# regorusjs
**Regorus** is
- *Rego*-*Rus(t)* - A fast, light-weight [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/)
interpreter written in Rust.
- *Rigorous* - A rigorous enforcer of well-defined Rego semantics.
`regorusjs` is Regorus compiled into WASM.
See [Repository](https://github.com/microsoft/regorus).
To build this binding, see [building](https://github.com/microsoft/regorus/bindings/wasm/building.md)
## Usage
```javascript
var regorus = require('regorusjs')
// Create an engine.
var engine = new regorus.Engine();
// Add Rego policy.
engine.add_policy(
// Associate this file name with policy
'hello.rego',
// Rego policy
`
package test
# Join messages
message = concat(", ", [input.message, data.message])
`)
// Set policy data
engine.add_data_json(`
{
"message" : "World!"
}
`)
// Set policy input
engine.set_input_json(`
{
"message" : "Hello"
}
`)
// Eval query
results = engine.eval_query('data.test.message')
// Display
console.log(results)
// {
// "result": [
// {
// "expressions": [
// {
// "value": "Hello, World!",
// "text": "data.test.message",
// "location": {
// "row": 1,
// "col": 1
// }
// }
// ]
// }
// ]
// }
// Convert results to object
results = JSON.parse(results)
// Process result
console.log(results.result[0].expressions[0].value)
// Hello, World!
```

34
bindings/wasm/building.md Normal file
View File

@@ -0,0 +1,34 @@
- Install `wasm-pack`
```
cargo install wasm-pack
```
- Build `regorusjs` for nodejs.
```
cd bindings/wasm
wasm-pack build --target nodejs --release
```
- Install [nodejs](https://nodejs.org/en/download)
- Run the test script
```
$ node test.js
\\{
\\ "result": [
\\ {
\\ "expressions": [
\\ {
\\ "value": "Hello, World!",
\\ "text": "data.test.message",
\\ "location": {
\\ "row": 1,
\\ "col": 1
\\ }
\\ }
\\ ]
\\ }
\\ ]
\\}
```

144
bindings/wasm/src/lib.rs Normal file
View File

@@ -0,0 +1,144 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
/// WASM wrapper for [`regorus::Engine`]
pub struct Engine {
engine: regorus::Engine,
}
fn error_to_jsvalue<E: std::fmt::Display>(e: E) -> JsValue {
JsValue::from_str(&format!("{e}"))
}
impl Default for Engine {
fn default() -> Self {
Self::new()
}
}
impl Clone for Engine {
/// Clone a [`Engine`]
///
/// To avoid having to parse same policy again, the engine can be cloned
/// after policies and data have been added.
fn clone(&self) -> Self {
Self {
engine: self.engine.clone(),
}
}
}
#[wasm_bindgen]
impl Engine {
#[wasm_bindgen(constructor)]
/// Construct a new Engine
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html
pub fn new() -> Self {
Self {
engine: regorus::Engine::new(),
}
}
/// Add a policy
///
/// The policy is parsed into AST.
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_policy
///
/// * `path`: A filename to be associated with the policy.
/// * `rego`: Rego policy.
pub fn add_policy(&mut self, path: String, rego: String) -> Result<(), JsValue> {
self.engine.add_policy(path, rego).map_err(error_to_jsvalue)
}
/// Add policy data.
///
/// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_data
/// * `data`: JSON encoded value to be used as policy data.
pub fn add_data_json(&mut self, data: String) -> Result<(), JsValue> {
let data = regorus::Value::from_json_str(&data).map_err(error_to_jsvalue)?;
self.engine.add_data(data).map_err(error_to_jsvalue)
}
/// Clear policy data.
pub fn clear_data(&mut self) -> Result<(), JsValue> {
self.engine.clear_data();
Ok(())
}
/// Set input.
///
/// See https://docs.rs/regorus/0.1.0-alpha.2/regorus/struct.Engine.html#method.set_input
/// * `input`: JSON encoded value to be used as input to query.
pub fn set_input_json(&mut self, input: String) -> Result<(), JsValue> {
let input = regorus::Value::from_json_str(&input).map_err(error_to_jsvalue)?;
self.engine.set_input(input);
Ok(())
}
/// Evaluate query.
///
/// See https://docs.rs/regorus/0.1.0-alpha.2/regorus/struct.Engine.html#method.eval_query
/// * `query`: Rego expression to be evaluate.
pub fn eval_query(&mut self, query: String) -> Result<String, JsValue> {
let results = self
.engine
.eval_query(query, false)
.map_err(error_to_jsvalue)?;
serde_json::to_string_pretty(&results).map_err(error_to_jsvalue)
}
}
#[cfg(test)]
mod tests {
use wasm_bindgen::prelude::*;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test]
pub fn basic() -> Result<(), JsValue> {
let mut engine = crate::Engine::new();
// Exercise all APIs.
engine.add_data_json(
r#"
{
"foo" : "bar"
}
"#
.to_string(),
)?;
engine.set_input_json(
r#"
{
"message" : "Hello"
}
"#
.to_string(),
)?;
engine.add_policy(
"hello.rego".to_string(),
r#"
package test
message = input.message"#
.to_string(),
)?;
let results = engine.eval_query("data".to_string())?;
let r = regorus::Value::from_json_str(&results).map_err(crate::error_to_jsvalue)?;
let v = &r["result"][0]["expressions"][0]["value"];
// Ensure that input and policy were evaluated.
assert_eq!(v["test"]["message"], regorus::Value::from("Hello"));
// Test that data was set.
assert_eq!(v["foo"], regorus::Value::from("bar"));
Ok(())
}
}

47
bindings/wasm/test.js Normal file
View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var regorus = require('./pkg/regorusjs')
// Create an engine.
var engine = new regorus.Engine();
// Add Rego policy.
engine.add_policy(
// Associate this file name with policy
'hello.rego',
// Rego policy
`
package test
# Join messages
message = concat(", ", [input.message, data.message])
`)
// Set policy data
engine.add_data_json(`
{
"message" : "World!"
}
`)
// Set policy input
engine.set_input_json(`
{
"message" : "Hello"
}
`)
// Eval query
results = engine.eval_query('data.test.message')
// Display
console.log(results)
// Convert results to object
results = JSON.parse(results)
// Process result
console.log(results.result[0].expressions[0].value)

239
docs/builtins.md Normal file
View File

@@ -0,0 +1,239 @@
# Built-in Functions
This page lists all the supported Rego built-in functions and the cargo feature that is needed to enable each builtin.
Those builtins that are not need for a specific use of the Regorus crate can be excluded from the binary by not specifying
the corresponding feature. This is useful in Confidential Computing scenarios where
- There needs to be control over what a policy execution can and cannot do.
- There needs to be control over exactly what goes into the [Trusted Computing Base](https://en.wikipedia.org/wiki/Trusted_computing_base).
Currently many builtins are `baked-in`, i.e. there is no way to exclude them from the TCB.
In future, each builtin will be associated with a feature (many builtins could be associated with the same feature).
- [Comparison](https://www.openpolicyagent.org/docs/latest/policy-reference/#comparison)
| Builtin | Feature |
|--------------------------------------------------------------------------------------------------|---------|
| [x == y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-comparison-equal) | _ |
| [x > y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-comparison-gt) | _ |
| [x >= y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-comparison-gte) | _ |
| [x < y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-comparison-lt) | _ |
| [x <= y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-comparison-lte) | _ |
| [x != y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-comparison-neq) | _ |
- [Numbers](https://www.openpolicyagent.org/docs/latest/policy-reference/#numbers)
| Builtin | Feature |
|-----------------------------------------------------------------------------------------------------------------------|---------|
| [abs](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-abs) | _ |
| [ceil](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-ceil) | _ |
| [x / y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-div) | _ |
| [floor](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-floor) | _ |
| [x - y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-minus) | _ |
| [x * y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-mul) | _ |
| [numbers.range](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-numbersrange) | _ |
| [numbers.range_step](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-numbersrange_step) | _ |
| [x + y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-plus) | _ |
| [rand.intn](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-randintn) | _ |
| [x % y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-rem) | _ |
| [round](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-numbers-round) | _ |
- [Aggregates](https://www.openpolicyagent.org/docs/latest/policy-reference/#aggregates)
| Builtin | Feature |
|-----------------------------------------------------------------------------------------------------|---------|
| [count](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-aggregates-count) | _ |
| [max](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-aggregates-max) | _ |
| [min](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-aggregates-min) | _ |
| [product](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-aggregates-product) | _ |
| [sort](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-aggregates-sort) | _ |
| [sum](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-aggregates-sum) | _ |
- [Arrays](https://www.openpolicyagent.org/docs/latest/policy-reference/#arrays-2)
| Builtin | Feature |
|-----------------------------------------------------------------------------------------------------------|---------|
| [array.concat](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayconcat) | _ |
| [array.reverse](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayreverse) | _ |
| [array.slice](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-array-arrayslice) | _ |
- [Sets](https://www.openpolicyagent.org/docs/latest/policy-reference/#sets-2)
| Builtin | Feature |
|---------------------------------------------------------------------------------------------------------|---------|
| [x & y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-sets-and) | _ |
| [intersection](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-sets-intersection) | _ |
| [x - y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-sets-minus) | _ |
| [x \| y](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-sets-or) | _ |
| [union](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-sets-union) | _ |
- [Objects](https://www.openpolicyagent.org/docs/latest/policy-reference/#object)
| Builtin | Feature |
|----------------------------------------------------------------------------------------------------------------------|--------------|
| [json.filter](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-jsonfilter) | _ |
| [json.match_schema](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-jsonmatch_schema) | `jsonschema` |
| [json.remove](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-jsonremove) | _ |
| [json.verify_schema](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-jsonverify_schema) | `jsonschema` |
| [object.filter](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectfilter) | _ |
| [object.get](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectget) | _ |
| [object.keys](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectkeys) | _ |
| [object.remove](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectremove) | _ |
| [object.subset](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectsubset) | _ |
| [object.union](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectunion) | _ |
| [object.union_n](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-object-objectunion_n) | _ |
- [Strings](https://www.openpolicyagent.org/docs/latest/policy-reference/#strings)
| Builtin | Feature |
|-----------------------------------------------------------------------------------------------------------------------------------|---------|
| [concat](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-concat) | _ |
| [contains](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-contains) | _ |
| [endswith](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-endswith) | _ |
| [format_int](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-format_int) | _ |
| [indexof](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-indexof) | _ |
| [indexof_n](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-indexof_n) | _ |
| [lower](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-lower) | _ |
| [replace](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-replace) | _ |
| [split](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-split) | _ |
| [sprintf](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-sprintf) | _ |
| [startswith](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-startswith) | _ |
| [strings.any_prefix_match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsany_prefix_match) | _ |
| [strings.any_suffix_match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsany_suffix_match) | _ |
| [strings.render_template](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsrender_template) | _ |
| [strings.replace_n](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsreplace_n) | _ |
| [strings.reverse](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-stringsreverse) | _ |
| [substring](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-substring) | _ |
| [trim](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim) | _ |
| [trim_left](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim_left) | _ |
| [trim_prefix](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim_prefix) | _ |
| [trim_right](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim_right) | _ |
| [trim_space](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim_space) | _ |
| [trim_suffix](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-trim_suffix) | _ |
| [upper](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-strings-upper) | _ |
- [Regex](https://www.openpolicyagent.org/docs/latest/policy-reference/#regex)
| Builtin | Feature |
|-------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| [regex.find_all_string_submatch_n](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexfind_all_string_submatch_n) | `regex` |
| [regex.find_n](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexfind_n) | `regex` |
| [regex.globs_match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexglobs_match) | `regex` |
| [regex.is_valid](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexis_valid) | `regex` |
| [regex.match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexmatch) | `regex` |
| [regex.replace](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexreplace) | `regex` |
| [regex.split](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regexsplit) | `regex` |
| [regex.template_match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-regex-regextemplate_match) | `regex` |
- [Glob](https://www.openpolicyagent.org/docs/latest/policy-reference/#regex)
| Builtin | Feature |
|--------------------------------------------------------------------------------------------------------------|---------|
| [glob.match](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-glob-globmatch) | `glob` |
| [glob.quote_meta](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-glob-globquote_meta) | `glob` |
- [Bitwise](https://www.openpolicyagent.org/docs/latest/policy-reference/#regex)
| Builtin | Feature |
|------------------------------------------------------------------------------------------------------|---------|
| [bits.and](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-bits-bitsand) | _ |
| [bits.lsh](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-bits-bitslsh) | _ |
| [bits.negate](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-bits-bitsnegate) | _ |
| [bits.or](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-bits-bitsor) | _ |
| [bits.rsh](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-bits-bitsrsh) | _ |
| [bits.xor](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-bits-bitsxor) | _ |
- [Conversions](https://www.openpolicyagent.org/docs/latest/policy-reference/#conversions)
| Builtin | Feature |
|-------|---------|
[to_number](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-conversions-to_number) | _ |
|
- [Units](https://www.openpolicyagent.org/docs/latest/policy-reference/#units)
| Builtin | Feature |
|-------------------------------------------------------------------------------------------------------------------|---------|
| [units.parse](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-units-unitsparse) | _ |
| [units.parse_bytes](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-units-unitsparse_bytes) | _ |
- [Types](https://www.openpolicyagent.org/docs/latest/policy-reference/#types)
| Builtin | Feature |
|------------------------------------------------------------------------------------------------------|---------|
| [is_array](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_array) | _ |
| [is_boolean](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_boolean) | _ |
| [is_null](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_null) | _ |
| [is_number](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_number) | _ |
| [is_object](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_object) | _ |
| [is_set](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_set) | _ |
| [is_string](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-is_string) | _ |
| [type_name](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-types-type_name) | _ |
- [Encoding](https://www.openpolicyagent.org/docs/latest/policy-reference/#encoding)
| Builtin | Feature |
|----------------------------------------------------------------------------------------------------------------------------------|-------------|
| [base64.is_valid](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-base64is_valid) | `base64` |
| [base64url.decode](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-base64urldecode) | `base64` |
| [base64url.encode](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-base64urlencode) | `base64url` |
| [base64url.encode_no_pad](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-base64urlencode_no_pad) | `base64url` |
| [hex.decode](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-hexdecode) | `hex` |
| [hex.encode](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-hexencode) | `hex` |
| [json.is_valid](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-jsonis_valid) | _ |
| [json.marshal](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-jsonmarshal) | _ |
| [json.unmarshal](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-jsonunmarshal) | _ |
| [urlquery.decode](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-urlquerydecode) | `urlquery` |
| [urlquery.decode_object](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-urlquerydecode_object) | `urlquery` |
| [urlquery.encode](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-urlqueryencode) | `urlquery` |
| [urlquery.encode_object](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-urlqueryencode_object) | `urlquery` |
| [yaml.is_valid](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-yamlis_valid) | `yaml` |
| [yaml.marshal](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-yamlmarshal) | `yaml` |
| [yaml.unmarshal](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-encoding-yamlunmarshal) | `yaml` |
- [Time](https://www.openpolicyagent.org/docs/latest/policy-reference/#time)
| Builtin | Feature |
|----------------------------------------------------------------------------------------------------------------------------|---------|
| ([time.add_date](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeadd_date) | `time` |
| [time.add_date](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeadd_date) | `time` |
| [time.clock](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeclock) | `time` |
| [time.date](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timedate) | `time` |
| [time.diff](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timediff) | `time` |
| [time.format](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeformat) | `time` |
| [time.now_ns](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timenow_ns) | `time` |
| [time.parse_duration_ns](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeparse_duration_ns) | `time` |
| [time.parse_ns](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeparse_ns) | `time` |
| [time.parse_rfc3339_ns](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeparse_rfc3339_ns) | `time` |
| [time.weekday](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-time-timeweekday) | `time` |
- [Cryptography](https://www.openpolicyagent.org/docs/latest/policy-reference/#crypto)
| Builtin | Feature |
|---------------------------------------------------------------------------------------------------------------------|----------|
| [crypto.hmac.equal](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptohmacequal) | `crypto` |
| [crypto.hmac.md5](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptohmacmd5) | `crypto` |
| [crypto.hmac.sha1](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptohmacsha1) | `crypto` |
| [crypto.hmac.sha256](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptohmacsha256) | `crypto` |
| [crypto.hmac.sha512](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptohmacsha512) | `crypto` |
| [crypto.md5](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptomd5) | `crypto` |
| [crypto.sha1](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptosha1) | `crypto` |
| [crypto.sha256](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-crypto-cryptosha256) | `crypto` |
- [Graphs](https://www.openpolicyagent.org/docs/latest/policy-reference/#graph)
| Builtin | Feature |
|---------------------------------------------------------------------------------------------------------------|---------|
| [graph.reachable](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-graph-graphreachable) | `graph` |
| [walk](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-graph-walk) | `graph` |
- [UUID](https://www.openpolicyagent.org/docs/latest/policy-reference/#uuid)
| Builtin | Feature |
|--------------------------------------------------------------------------------------------------------|---------|
| [uuid.parse](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-uuid-uuidparse) | `uuid` |
| [uuid.rfc4122](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-uuid-uuidrfc4122) | `uuid` |
- [Semantic Versions](https://www.openpolicyagent.org/docs/latest/policy-reference/#semver)
| Builtin | Feature |
|----------------------------------------------------------------------------------------------------------------|----------|
| [semver.compare](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-semver-semvercompare) | `semver` |
| [semver.is_valid](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-semver-semveris_valid) | `semver` |
- [OPA](https://www.openpolicyagent.org/docs/latest/policy-reference/#opa
| Builtin | Feature |
|-----------------------------------------------------------------------------------------------------|---------|
| [opa.runtime](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-opa-oparuntime) | _ |
- [Debugging](https://www.openpolicyagent.org/docs/latest/policy-reference/#opa)
| Builtin | Feature |
|---------------------------------------------------------------------------------|---------|
| [print(...)](https://www.openpolicyagent.org/docs/latest/policy-reference/#opa) | _ |
- [Tracing](https://www.openpolicyagent.org/docs/latest/policy-reference/#tracing)
| Builtin | Feature |
|----------------------------------------------------------------------------------------------|---------|
| [trace](https://www.openpolicyagent.org/docs/latest/policy-reference/#builtin-tracing-trace) | _ |

View File

@@ -1,23 +0,0 @@
Cpackage play
a := {4}
mydoc(x) := path {
path := "data.play.a"
}
x := [ y |
y := data.play.a | data.play.b with data.play.a as {5} with data.play.b as {6}
]
r := [ m | m := data.play.p with data.play.p as 5 + 6; true ]
allow {
input.x
== 5
input.y == 5
input.y
== 5
}

View File

@@ -353,6 +353,7 @@ pub struct Module {
pub package: Package,
pub imports: Vec<Import>,
pub policy: Vec<Ref<Rule>>,
pub rego_v1: bool,
}
pub type ExprRef = Ref<Expr>;

View File

@@ -28,11 +28,14 @@ fn print(span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> R
let mut msg = String::default();
for a in args {
match a {
Value::Undefined => msg += "<undefined>",
_ => msg += format!("{a}").as_str(),
Value::Undefined => msg += " <undefined>",
Value::String(s) => msg += &format!(" {s}"),
_ => msg += &format!(" {a}"),
};
}
span.message("print", msg.as_str());
if !msg.is_empty() {
println!("{}", &msg[1..]);
}
Ok(Value::Bool(true))
}

View File

@@ -3,14 +3,16 @@
use crate::ast::{Expr, Ref};
use crate::builtins;
#[allow(unused)]
use crate::builtins::utils::{
ensure_args_count, ensure_object, ensure_string, ensure_string_collection,
};
use crate::lexer::Span;
use crate::value::Value;
use std::collections::{BTreeMap, HashMap};
use std::collections::HashMap;
#[allow(unused)]
use anyhow::{anyhow, bail, Context, Result};
pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {
@@ -41,11 +43,6 @@ pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {
m.insert("json.is_valid", (json_is_valid, 1));
m.insert("json.marshal", (json_marshal, 1));
m.insert("json.unmarshal", (json_unmarshal, 1));
#[cfg(feature = "jsonschema")]
{
m.insert("json.match_schema", (json_match_schema, 2));
m.insert("json.verify_schema", (json_verify_schema, 1));
}
#[cfg(feature = "yaml")]
{
@@ -240,7 +237,7 @@ fn urlquery_decode_object(
Err(_) => bail!(params[0].span().error("not a valid url query")),
};
let mut map = BTreeMap::new();
let mut map = std::collections::BTreeMap::new();
for (k, v) in url.query_pairs() {
let key = Value::String(k.clone().into());
let value = Value::String(v.clone().into());
@@ -382,72 +379,3 @@ fn json_unmarshal(
let json_str = ensure_string(name, &params[0], &args[0])?;
Value::from_json_str(&json_str).with_context(|| span.error("could not deserialize json."))
}
#[cfg(feature = "jsonschema")]
fn compile_json_schema(param: &Ref<Expr>, arg: &Value) -> Result<jsonschema::JSONSchema> {
let schema_str = match arg {
Value::String(schema_str) => schema_str.as_ref().to_string(),
_ => arg.to_json_str()?,
};
if let Ok(schema) = serde_json::from_str(&schema_str) {
match jsonschema::JSONSchema::compile(&schema) {
Ok(schema) => return Ok(schema),
Err(e) => bail!(e.to_string()),
}
}
bail!(param.span().error("not a valid json schema"))
}
#[cfg(feature = "jsonschema")]
fn json_verify_schema(
span: &Span,
params: &[Ref<Expr>],
args: &[Value],
strict: bool,
) -> Result<Value> {
let name = "json.verify_schema";
ensure_args_count(span, name, params, args, 1)?;
Ok(Value::from_array(
match compile_json_schema(&params[0], &args[0]) {
Ok(_) => [Value::Bool(true), Value::Null],
Err(e) if strict => bail!(params[0]
.span()
.error(format!("invalid schema: {e}").as_str())),
Err(e) => [Value::Bool(false), Value::String(e.to_string().into())],
}
.to_vec(),
))
}
#[cfg(feature = "jsonschema")]
fn json_match_schema(
span: &Span,
params: &[Ref<Expr>],
args: &[Value],
strict: bool,
) -> Result<Value> {
let name = "json.match_schema";
ensure_args_count(span, name, params, args, 2)?;
// The following is expected to succeed.
let document: serde_json::Value = serde_json::from_str(&args[0].to_json_str()?)?;
Ok(Value::from_array(
match compile_json_schema(&params[1], &args[1]) {
Ok(schema) => match schema.validate(&document) {
Ok(_) => [Value::Bool(true), Value::Null],
Err(e) => [
Value::Bool(false),
Value::from_array(e.map(|e| Value::String(e.to_string().into())).collect()),
],
},
Err(e) if strict => bail!(params[1]
.span()
.error(format!("invalid schema: {e}").as_str())),
Err(e) => [Value::Bool(false), Value::String(e.to_string().into())],
}
.to_vec(),
))
}

View File

@@ -23,6 +23,12 @@ pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {
m.insert("object.subset", (subset, 2));
m.insert("object.union", (object_union, 2));
m.insert("object.union_n", (object_union_n, 1));
#[cfg(feature = "jsonschema")]
{
m.insert("json.match_schema", (json_match_schema, 2));
m.insert("json.verify_schema", (json_verify_schema, 1));
}
}
fn json_filter_impl(v: &Value, filter: &Value) -> Value {
@@ -382,3 +388,72 @@ fn object_union_n(
Ok(u)
}
#[cfg(feature = "jsonschema")]
fn compile_json_schema(param: &Ref<Expr>, arg: &Value) -> Result<jsonschema::JSONSchema> {
let schema_str = match arg {
Value::String(schema_str) => schema_str.as_ref().to_string(),
_ => arg.to_json_str()?,
};
if let Ok(schema) = serde_json::from_str(&schema_str) {
match jsonschema::JSONSchema::compile(&schema) {
Ok(schema) => return Ok(schema),
Err(e) => bail!(e.to_string()),
}
}
bail!(param.span().error("not a valid json schema"))
}
#[cfg(feature = "jsonschema")]
fn json_verify_schema(
span: &Span,
params: &[Ref<Expr>],
args: &[Value],
strict: bool,
) -> Result<Value> {
let name = "json.verify_schema";
ensure_args_count(span, name, params, args, 1)?;
Ok(Value::from_array(
match compile_json_schema(&params[0], &args[0]) {
Ok(_) => [Value::Bool(true), Value::Null],
Err(e) if strict => bail!(params[0]
.span()
.error(format!("invalid schema: {e}").as_str())),
Err(e) => [Value::Bool(false), Value::String(e.to_string().into())],
}
.to_vec(),
))
}
#[cfg(feature = "jsonschema")]
fn json_match_schema(
span: &Span,
params: &[Ref<Expr>],
args: &[Value],
strict: bool,
) -> Result<Value> {
let name = "json.match_schema";
ensure_args_count(span, name, params, args, 2)?;
// The following is expected to succeed.
let document: serde_json::Value = serde_json::from_str(&args[0].to_json_str()?)?;
Ok(Value::from_array(
match compile_json_schema(&params[1], &args[1]) {
Ok(schema) => match schema.validate(&document) {
Ok(_) => [Value::Bool(true), Value::Null],
Err(e) => [
Value::Bool(false),
Value::from_array(e.map(|e| Value::String(e.to_string().into())).collect()),
],
},
Err(e) if strict => bail!(params[1]
.span()
.error(format!("invalid schema: {e}").as_str())),
Err(e) => [Value::Bool(false), Value::String(e.to_string().into())],
}
.to_vec(),
))
}

View File

@@ -8,12 +8,15 @@ use crate::parser::*;
use crate::scheduler::*;
use crate::utils::gather_functions;
use crate::value::*;
use crate::QueryResults;
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use anyhow::{bail, Result};
/// The Rego evaluation engine.
///
#[derive(Clone)]
pub struct Engine {
modules: Vec<Ref<Module>>,
@@ -21,6 +24,7 @@ pub struct Engine {
prepared: bool,
}
/// Create a default engine.
impl Default for Engine {
fn default() -> Self {
Self::new()
@@ -28,6 +32,7 @@ impl Default for Engine {
}
impl Engine {
/// Create an instance of [Engine].
pub fn new() -> Self {
Self {
modules: vec![],
@@ -36,6 +41,29 @@ impl Engine {
}
}
/// Add a policy.
///
/// The policy file will be parsed and converted to AST representation.
/// Multiple policy files may be added to the engine.
///
/// * `path`: A filename to be associated with the policy.
/// * `rego`: The rego policy code.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// engine.add_policy(
/// "test.rego".to_string(),
/// r#"
/// package test
/// allow = input.user == "root"
/// "#.to_string())?;
/// # Ok(())
/// # }
/// ```
///
pub fn add_policy(&mut self, path: String, rego: String) -> Result<()> {
let source = Source::new(path, rego);
let mut parser = Parser::new(&source)?;
@@ -45,6 +73,22 @@ impl Engine {
Ok(())
}
/// Add a policy from a given file.
///
/// The policy file will be parsed and converted to AST representation.
/// Multiple policy files may be added to the engine.
///
/// * `path`: Path to the policy file (.rego).
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// engine.add_policy_from_file("tests/aci/framework.rego")?;
/// # Ok(())
/// # }
/// ```
pub fn add_policy_from_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let source = Source::from_file(path)?;
let mut parser = Parser::new(&source)?;
@@ -53,28 +97,166 @@ impl Engine {
Ok(())
}
/// Set the input document.
///
/// * `input`: Input documented. Typically this [Value] is constructed from JSON or YAML.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// let input = Value::from_json_str(r#"
/// {
/// "role" : "admin",
/// "action": "delete"
/// }"#)?;
///
/// engine.set_input(input);
/// # Ok(())
/// # }
/// ```
pub fn set_input(&mut self, input: Value) {
self.interpreter.set_input(input);
}
/// Clear the data document.
///
/// The data document will be reset to an empty object.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// engine.clear_data();
///
/// // Evaluate data.
/// let results = engine.eval_query("data".to_string(), false)?;
///
/// // Assert that it is empty object.
/// assert_eq!(results.result.len(), 1);
/// assert_eq!(results.result[0].expressions.len(), 1);
/// assert_eq!(results.result[0].expressions[0].value, Value::new_object());
/// # Ok(())
/// # }
/// ```
pub fn clear_data(&mut self) {
self.interpreter.set_data(Value::new_object());
self.prepared = false;
}
/// Add data document.
///
/// The specified data document is merged into existing data document.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// // Only objects can be added.
/// assert!(engine.add_data(Value::from_json_str("[]")?).is_err());
///
/// // Merge { "x" : 1, "y" : {} }
/// assert!(engine.add_data(Value::from_json_str(r#"{ "x" : 1, "y" : {}}"#)?).is_ok());
///
/// // Merge { "z" : 2 }
/// assert!(engine.add_data(Value::from_json_str(r#"{ "z" : 2 }"#)?).is_ok());
///
/// // Merge { "z" : 3 }. Conflict error.
/// assert!(engine.add_data(Value::from_json_str(r#"{ "z" : 3 }"#)?).is_err());
///
/// assert_eq!(
/// engine.eval_query("data".to_string(), false)?.result[0].expressions[0].value,
/// Value::from_json_str(r#"{ "x": 1, "y": {}, "z": 2}"#)?
/// );
/// # Ok(())
/// # }
/// ```
pub fn add_data(&mut self, data: Value) -> Result<()> {
if data.as_object().is_err() {
bail!("data must be object");
}
self.prepared = false;
self.interpreter.get_data_mut().merge(data)
}
pub fn get_modules(&mut self) -> &Vec<Ref<Module>> {
&self.modules
}
/// Set whether builtins should raise errors strictly or not.
///
/// Regorus differs from OPA in that by default builtins will
/// raise errors instead of returning Undefined.
///
/// ----
/// **_NOTE:_** Currently not all builtins honor this flag and will always strictly raise errors.
/// ----
pub fn set_strict_builtin_errors(&mut self, b: bool) {
self.interpreter.set_strict_builtin_errors(b)
}
#[doc(hidden)]
pub fn get_modules(&mut self) -> &Vec<Ref<Module>> {
&self.modules
}
/// Evaluate a Rego query.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let mut engine = Engine::new();
///
/// // Add policies
/// engine.add_policy_from_file("tests/aci/framework.rego")?;
/// engine.add_policy_from_file("tests/aci/api.rego")?;
/// engine.add_policy_from_file("tests/aci/policy.rego")?;
///
/// // Add data document (if any).
/// // If multiple data documents can be added, they will be merged together.
/// engine.add_data(Value::from_json_file("tests/aci/data.json")?)?;
///
/// // At this point the policies and data have been loaded.
/// // Either the same engine can be used to make multiple queries or the engine
/// // can be cloned to avoid having the reload the policies and data.
/// let _clone = engine.clone();
///
/// // Evaluate a query.
/// // Load input and make query.
/// engine.set_input(Value::new_object());
/// let results = engine.eval_query("data.framework.mount_overlay.allowed".to_string(), false)?;
/// assert!(results.result.is_empty());
///
/// // Evaluate query with different inputs.
/// engine.set_input(Value::from_json_file("tests/aci/input.json")?);
/// let results = engine.eval_query("data.framework.mount_overlay.allowed".to_string(), false)?;
/// assert_eq!(results.result[0].expressions[0].value, Value::from(true));
/// # Ok(())
/// # }
pub fn eval_query(&mut self, query: String, enable_tracing: bool) -> Result<QueryResults> {
self.eval_modules(enable_tracing)?;
let query_module = {
let source = Source::new(
"<query_module.rego>".to_owned(),
"package __internal_query_module".to_owned(),
);
Ref::new(Parser::new(&source)?.parse()?)
};
// Parse the query.
let query_source = Source::new("<query.rego>".to_string(), query);
let mut parser = Parser::new(&query_source)?;
let query_node = parser.parse_user_query()?;
let query_schedule = Analyzer::new().analyze_query_snippet(&self.modules, &query_node)?;
self.interpreter.eval_user_query(
&query_module,
&query_node,
&query_schedule,
enable_tracing,
)
}
#[doc(hidden)]
fn prepare_for_eval(&mut self, enable_tracing: bool) -> Result<()> {
self.interpreter.set_traces(enable_tracing);
@@ -107,6 +289,7 @@ impl Engine {
Ok(())
}
#[doc(hidden)]
pub fn eval_rule(
&mut self,
module: &Ref<Module>,
@@ -121,6 +304,7 @@ impl Engine {
Ok(self.interpreter.get_data_mut().clone())
}
#[doc(hidden)]
pub fn eval_modules(&mut self, enable_tracing: bool) -> Result<Value> {
self.prepare_for_eval(enable_tracing)?;
self.interpreter.clean_internal_evaluation_state();
@@ -164,30 +348,4 @@ impl Engine {
self.interpreter.create_rule_prefixes()?;
Ok(self.interpreter.get_data_mut().clone())
}
pub fn eval_query(&mut self, query: String, enable_tracing: bool) -> Result<QueryResults> {
self.eval_modules(false)?;
let query_module = {
let source = Source::new(
"<query_module.rego>".to_owned(),
"package __internal_query_module".to_owned(),
);
Ref::new(Parser::new(&source)?.parse()?)
};
// Parse the query.
let query_source = Source::new("<query.rego>".to_string(), query);
let mut parser = Parser::new(&query_source)?;
let query_node = parser.parse_user_query()?;
let query_schedule = Analyzer::new().analyze_query_snippet(&self.modules, &query_node)?;
let results = self.interpreter.eval_user_query(
&query_module,
&query_node,
&query_schedule,
enable_tracing,
)?;
Ok(results)
}
}

View File

@@ -9,10 +9,10 @@ use crate::parser::Parser;
use crate::scheduler::*;
use crate::utils::*;
use crate::value::*;
use crate::{Expression, Location, QueryResult, QueryResults};
use anyhow::{anyhow, bail, Result};
use log::info;
use serde::Serialize;
use std::collections::btree_map::Entry as BTreeMapEntry;
use std::collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap};
use std::ops::Bound::*;
@@ -73,42 +73,6 @@ impl Default for Interpreter {
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Location {
pub row: u16,
pub col: u16,
}
#[derive(Debug, Clone, Serialize)]
pub struct Expression {
pub value: Value,
pub text: Rc<str>,
pub location: Location,
}
#[derive(Debug, Clone, Serialize)]
pub struct QueryResult {
// Expressions is shown first to match OPA.
pub expressions: Vec<Expression>,
#[serde(skip_serializing_if = "Value::is_empty_object")]
pub bindings: Value,
}
impl Default for QueryResult {
fn default() -> Self {
Self {
bindings: Value::new_object(),
expressions: vec![],
}
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct QueryResults {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub result: Vec<QueryResult>,
}
#[derive(Debug, Clone)]
struct Context {
key_expr: Option<ExprRef>,
@@ -271,6 +235,7 @@ impl Interpreter {
self.loop_var_values.clear();
self.scopes = vec![Scope::new()];
self.contexts = vec![];
self.rule_values.clear();
}
fn current_module(&self) -> Result<Ref<Module>> {
@@ -383,7 +348,7 @@ impl Interpreter {
&& get_root_var(refr)?.text() == "data"
{
let index = index.to_string();
v = obj[&index].clone();
v = obj[index].clone();
}
return Ok(Self::get_value_chained(v, &path[..]));
}
@@ -1346,6 +1311,13 @@ impl Interpreter {
r
}
fn clear_scope(scope: &mut Scope) {
// Set each value to undefined. This is equivalent to removing the key.
for (_, v) in scope.iter_mut() {
*v = Value::Undefined;
}
}
fn eval_stmts_in_loop(&mut self, stmts: &[&LiteralStmt], loops: &[LoopExpr]) -> Result<bool> {
if loops.is_empty() {
if !stmts.is_empty() {
@@ -1409,9 +1381,8 @@ impl Interpreter {
}
}
// Save the current scope and restore it after evaluating the statements so
// that the effects of the current loop iteration are cleared.
let scope_saved = self.current_scope()?.clone();
// Create a new scope.
self.scopes.push(Scope::default());
let query_result = self.get_current_context()?.result.clone();
match loop_expr_value {
@@ -1437,12 +1408,13 @@ impl Interpreter {
result = self.eval_stmts_in_loop(stmts, &loops[1..])? || result;
}
self.loop_var_values.remove(&loop_expr.expr());
*self.current_scope_mut()? = scope_saved.clone();
Self::clear_scope(self.current_scope_mut()?);
if let Some(ctx) = self.contexts.last_mut() {
ctx.result = query_result.clone();
}
}
self.loop_var_values.remove(&loop_expr.expr());
}
Value::Set(items) => {
for v in items.iter() {
@@ -1460,12 +1432,12 @@ impl Interpreter {
result = self.eval_stmts_in_loop(stmts, &loops[1..])? || result;
}
self.loop_var_values.remove(&loop_expr.expr());
*self.current_scope_mut()? = scope_saved.clone();
Self::clear_scope(self.current_scope_mut()?);
if let Some(ctx) = self.contexts.last_mut() {
ctx.result = query_result.clone();
}
}
self.loop_var_values.remove(&loop_expr.expr());
}
Value::Object(obj) => {
for (k, v) in obj.iter() {
@@ -1481,12 +1453,13 @@ impl Interpreter {
if exec {
result = self.eval_stmts_in_loop(stmts, &loops[1..])? || result;
}
self.loop_var_values.remove(&loop_expr.expr());
*self.current_scope_mut()? = scope_saved.clone();
Self::clear_scope(self.current_scope_mut()?);
if let Some(ctx) = self.contexts.last_mut() {
ctx.result = query_result.clone();
}
}
self.loop_var_values.remove(&loop_expr.expr());
}
Value::Undefined => {
result = false;
@@ -1497,6 +1470,8 @@ impl Interpreter {
}
}
self.scopes.pop();
// Return true if at least on iteration returned true
Ok(result)
}
@@ -1728,7 +1703,7 @@ impl Interpreter {
if result
.expressions
.iter()
.all(|v| v.value != Value::Undefined)
.all(|v| v.value != Value::Undefined && v.value != Value::Bool(false))
&& !result.expressions.is_empty()
{
ctx.results.result.push(result);
@@ -1847,7 +1822,7 @@ impl Interpreter {
if result
.expressions
.iter()
.all(|v| v.value != Value::Undefined)
.all(|v| v.value != Value::Undefined && v.value != Value::Bool(false))
&& !result.expressions.is_empty()
{
ctx.results.result.push(result);
@@ -2023,14 +1998,14 @@ impl Interpreter {
}
}
fn lookup_function_by_name(&self, path: &str) -> Option<&Vec<Ref<Rule>>> {
fn lookup_function_by_name(&self, path: &str) -> Option<(&Vec<Ref<Rule>>, &Ref<Module>)> {
let mut path = path.to_owned();
if !path.starts_with("data.") {
path = self.current_module_path.clone() + "." + &path;
}
match self.functions.get(&path) {
Some((f, _)) => Some(f),
Some((f, _, m)) => Some((f, m)),
_ => None,
}
}
@@ -2063,10 +2038,12 @@ impl Interpreter {
// Handle trace function.
// TODO: with modifier.
if let (Some(traces), Value::String(msg)) = (&mut self.traces, &v) {
traces.push(msg.clone());
return Ok(Value::Bool(true));
};
if name == "trace" {
if let (Some(traces), Value::String(msg)) = (&mut self.traces, &v) {
traces.push(msg.clone());
return Ok(Value::Bool(true));
}
}
if let Some(name) = cache {
self.builtins_cache.insert((name, args), v.clone());
@@ -2081,7 +2058,8 @@ impl Interpreter {
#[cfg(feature = "deprecated")]
if let Some(builtin) = builtins::DEPRECATED.get(path) {
if !self.allow_deprecated {
let allow = self.allow_deprecated && !self.current_module()?.rego_v1;
if !allow {
bail!(span.error(format!("{path} is deprecated").as_str()))
}
return Ok(Some(builtin));
@@ -2143,9 +2121,9 @@ impl Interpreter {
_ => orig_fcn_path.clone(),
};
let empty = vec![];
let fcns_rules = match self.lookup_function_by_name(&fcn_path) {
Some(r) => r,
let empty: Vec<Ref<Rule>> = vec![];
let (fcns_rules, fcn_module) = match self.lookup_function_by_name(&fcn_path) {
Some((fcns, m)) => (fcns, Some(m.clone())),
_ => {
if self.default_rules.get(&fcn_path).is_some()
|| self
@@ -2154,10 +2132,10 @@ impl Interpreter {
.is_some()
{
// process default functions later.
&empty
(&empty, self.module.clone())
}
// Look up builtin function.
else if let Ok(Some(builtin)) = self.lookup_builtin(span, &fcn_path) {
else if let Some(builtin) = self.lookup_builtin(span, &fcn_path)? {
let r = self.eval_builtin_call(span, &fcn_path.clone(), *builtin, params);
if let Some(with_functions) = with_functions_saved {
self.with_functions = with_functions;
@@ -2236,6 +2214,7 @@ impl Interpreter {
..Context::default()
};
let prev_module = self.set_current_module(fcn_module.clone())?;
let value = match self.eval_rule_bodies(ctx, span, bodies) {
Ok(v) => v,
Err(e) => {
@@ -2245,6 +2224,7 @@ impl Interpreter {
continue;
}
};
self.set_current_module(prev_module)?;
let result = match &value {
Value::Set(s) if s.len() == 1 => s.iter().next().unwrap().clone(),
@@ -2410,6 +2390,14 @@ impl Interpreter {
self.eval_rule(&module, rule)?;
}
}
let prev_module = self.set_current_module(Some(module.clone()))?;
for rule in &module.policy {
if !self.processed.contains(rule) {
self.eval_default_rule(rule)?;
}
}
self.set_current_module(prev_module)?;
}
}
Ok(())
@@ -3222,7 +3210,7 @@ impl Interpreter {
self.set_current_module(prev_module)?;
if let Some(r) = results.result.last() {
if r.bindings.is_empty_object()
if matches!(&r.bindings, Value::Object(obj) if obj.is_empty())
&& r.expressions.iter().any(|e| e.value == Value::Bool(false))
{
results = QueryResults::default();

View File

@@ -4,6 +4,8 @@
// Use README.md as crate documentation.
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
use serde::Serialize;
mod ast;
mod builtins;
mod engine;
@@ -16,10 +18,250 @@ mod utils;
mod value;
pub use engine::Engine;
pub use interpreter::{QueryResult, QueryResults};
pub use value::Value;
/// Location of an [`Expression`] in a Rego query.
///
/// ```
/// # use regorus::Engine;
/// # fn main() -> anyhow::Result<()> {
/// // Create engine and evaluate " \n 1 + 2".
/// let results = Engine::new().eval_query(" \n 1 + 2".to_string(), false)?;
///
/// // Fetch the location for the expression.
/// let loc = &results.result[0].expressions[0].location;
///
/// assert_eq!(loc.row, 2);
/// assert_eq!(loc.col, 3);
/// # Ok(())
/// # }
/// ````
/// See also [`QueryResult`].
#[derive(Debug, Clone, Serialize)]
pub struct Location {
/// Line number. Starts at 1.
pub row: u16,
/// Column number. Starts at 1.
pub col: u16,
}
/// An expression in a Rego query.
///
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// // Create engine and evaluate "1 + 2".
/// let results = Engine::new().eval_query("1 + 2".to_string(), false)?;
///
/// // Fetch the expression from results.
/// let expr = &results.result[0].expressions[0];
///
/// assert_eq!(expr.value, Value::from(3u64));
/// assert_eq!(expr.text.as_ref(), "1 + 2");
/// # Ok(())
/// # }
/// ```
/// See also [`QueryResult`].
#[derive(Debug, Clone, Serialize)]
pub struct Expression {
/// Computed value of the expression.
pub value: Value,
/// The Rego expression.
pub text: std::rc::Rc<str>,
/// Location of the expression in the query string.
pub location: Location,
}
/// Result of evaluating a Rego query.
///
/// A query containing single expression.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// // Create engine and evaluate "1 + 2".
/// let results = Engine::new().eval_query("1 + 2".to_string(), false)?;
///
/// // Fetch the first (sole) result.
/// let result = &results.result[0];
///
/// assert_eq!(result.expressions[0].value, Value::from(3u64));
/// assert_eq!(result.expressions[0].text.as_ref(), "1 + 2");
/// # Ok(())
/// # }
/// ```
///
/// A query containing multiple expressions.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// // Create engine and evaluate "1 + 2; 3.5 * 4".
/// let results = Engine::new().eval_query("1 + 2; 3.55 * 4".to_string(), false)?;
///
/// // Fetch the first (sole) result.
/// let result = &results.result[0];
///
/// // First expression.
/// assert_eq!(result.expressions[0].value, Value::from(3u64));
/// assert_eq!(result.expressions[0].text.as_ref(), "1 + 2");
///
/// // Second expression.
/// assert_eq!(result.expressions[1].value, Value::from(14.2));
/// assert_eq!(result.expressions[1].text.as_ref(), "3.55 * 4");
/// # Ok(())
/// # }
/// ```
///
/// Expressions that create bindings (i.e. associate names to values) evaluate to
/// either true or false. The value of bindings are available in the `bindings` field.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// // Create engine and evaluate "x = 1; y = x > 0".
/// let results = Engine::new().eval_query("x = 1; y = x > 0".to_string(), false)?;
///
/// // Fetch the first (sole) result.
/// let result = &results.result[0];
///
/// // First expression is true.
/// assert_eq!(result.expressions[0].value, Value::from(true));
/// assert_eq!(result.expressions[0].text.as_ref(), "x = 1");
///
/// // Second expression is true.
/// assert_eq!(result.expressions[1].value, Value::from(true));
/// assert_eq!(result.expressions[1].text.as_ref(), "y = x > 0");
///
/// // bindings contains the value for each named expession.
/// assert_eq!(result.bindings[&Value::from("x")], Value::from(1u64));
/// assert_eq!(result.bindings[&Value::from("y")], Value::from(true));
/// # Ok(())
/// # }
/// ```
///
/// If any expression evaluates to false, then no results are produced.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// // Create engine and evaluate "true; true; false".
/// let results = Engine::new().eval_query("true; true; false".to_string(), false)?;
///
/// assert!(results.result.is_empty());
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, Serialize)]
pub struct QueryResult {
/// Expressions in the query.
///
/// Each statement in the query is treated as a separte expression.
///
pub expressions: Vec<Expression>,
/// Bindings created in the query.
#[serde(skip_serializing_if = "Value::is_empty_object")]
pub bindings: Value,
}
impl Default for QueryResult {
fn default() -> Self {
Self {
bindings: Value::new_object(),
expressions: vec![],
}
}
}
/// Results of evaluating a Rego query.
///
/// Generates the same `json` representation as `opa eval`.
///
/// Queries typically produce a single result.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// // Create engine and evaluate "true; true; false".
/// let results = Engine::new().eval_query("1 + 1".to_string(), false)?;
///
/// assert!(results.result.len() == 1);
/// assert_eq!(results.result[0].expressions[0].value, Value::from(2u64));
/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "1 + 1");
/// # Ok(())
/// # }
/// ```
///
/// If any expression evaluates to false, then no results are produced.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// // Create engine and evaluate "true; true; false".
/// let results = Engine::new().eval_query("true; true; false".to_string(), false)?;
///
/// assert!(results.result.is_empty());
/// # Ok(())
/// # }
/// ```
///
/// Queries containing loops produce multiple results.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let results = Engine::new().eval_query("x = [1, 2, 3][_]".to_string(), false)?;
///
/// // Three results are produced, one of each value of x.
/// assert_eq!(results.result.len(), 3);
///
/// // Assert expressions and bindings of results.
/// assert_eq!(results.result[0].expressions[0].value, Value::Bool(true));
/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "x = [1, 2, 3][_]");
/// assert_eq!(results.result[0].bindings[&Value::from("x")], Value::from(1u64));
///
/// assert_eq!(results.result[1].expressions[0].value, Value::Bool(true));
/// assert_eq!(results.result[1].expressions[0].text.as_ref(), "x = [1, 2, 3][_]");
/// assert_eq!(results.result[1].bindings[&Value::from("x")], Value::from(2u64));
///
/// assert_eq!(results.result[2].expressions[0].value, Value::Bool(true));
/// assert_eq!(results.result[2].expressions[0].text.as_ref(), "x = [1, 2, 3][_]");
/// assert_eq!(results.result[2].bindings[&Value::from("x")], Value::from(3u64));
/// # Ok(())
/// # }
/// ```
///
/// Loop iterations that evaluate to false or undefined don't produce results.
/// ```
/// # use regorus::*;
/// # fn main() -> anyhow::Result<()> {
/// let results = Engine::new().eval_query("x = [1, 2, 3][_]; x >= 2".to_string(), false)?;
///
/// // Two results are produced, one for x = 2 and another for x = 3.
/// assert_eq!(results.result.len(), 2);
///
/// // Assert expressions and bindings of results.
/// assert_eq!(results.result[0].expressions[0].value, Value::Bool(true));
/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "x = [1, 2, 3][_]");
/// assert_eq!(results.result[0].expressions[0].value, Value::Bool(true));
/// assert_eq!(results.result[0].expressions[1].text.as_ref(), "x >= 2");
/// assert_eq!(results.result[0].bindings[&Value::from("x")], Value::from(2u64));
///
/// assert_eq!(results.result[1].expressions[0].value, Value::Bool(true));
/// assert_eq!(results.result[1].expressions[0].text.as_ref(), "x = [1, 2, 3][_]");
/// assert_eq!(results.result[1].expressions[0].value, Value::Bool(true));
/// assert_eq!(results.result[1].expressions[1].text.as_ref(), "x >= 2");
/// assert_eq!(results.result[1].bindings[&Value::from("x")], Value::from(3u64));
/// # Ok(())
/// # }
/// ```
///
/// See [QueryResult] for examples of different kinds of results.
#[derive(Debug, Clone, Default, Serialize)]
pub struct QueryResults {
/// Collection of results of evaluting a query.
#[serde(skip_serializing_if = "Vec::is_empty")]
pub result: Vec<QueryResult>,
}
/// Items in `unstable` are likely to change.
#[doc(hidden)]
pub mod unstable {
pub use crate::ast::*;
pub use crate::lexer::*;

View File

@@ -132,6 +132,26 @@ impl From<f64> for Number {
}
impl Number {
pub fn as_u128(&self) -> Option<u128> {
match self {
Big(b) if b.is_integer() => match u128::try_from(&b.d) {
Ok(v) => Some(v),
_ => None,
},
_ => None,
}
}
pub fn as_i128(&self) -> Option<i128> {
match self {
Big(b) if b.is_integer() => match i128::try_from(&b.d) {
Ok(v) => Some(v),
_ => None,
},
_ => None,
}
}
pub fn as_u64(&self) -> Option<u64> {
match self {
Big(b) if b.is_integer() => match u64::try_from(&b.d) {

View File

@@ -15,6 +15,7 @@ pub struct Parser<'source> {
line: u16,
end: u16,
future_keywords: BTreeMap<String, Span>,
rego_v1: bool,
}
const FUTURE_KEYWORDS: [&str; 4] = ["contains", "every", "if", "in"];
@@ -30,6 +31,7 @@ impl<'source> Parser<'source> {
line: 0,
end: 0,
future_keywords: BTreeMap::new(),
rego_v1: false,
})
}
@@ -76,19 +78,19 @@ impl<'source> Parser<'source> {
pub fn set_future_keyword(&mut self, kw: &str, span: &Span) -> Result<()> {
match &self.future_keywords.get(kw) {
Some(s) if false => Err(self.source.error(
Some(s) if self.rego_v1 => Err(self.source.error(
span.line,
span.col,
format!(
"this import shadows previous import of `{kw}` defined at:{}",
self.source
.message(s.line, s.col, "", "this import is shadowed.")
s.message("", "this import is shadowed.")
)
.as_str(),
)),
_ => {
self.future_keywords.insert(kw.to_string(), span.clone());
if kw == "every" {
if kw == "every" && !self.rego_v1 {
//rego.v1 explicitly adds each keyword.
self.future_keywords.insert("in".to_string(), span.clone());
}
Ok(())
@@ -782,6 +784,17 @@ impl<'source> Parser<'source> {
span.start = start;
let op = match self.token_text() {
"=" => AssignOp::Eq,
":=" if self.rego_v1 => {
if let Expr::Var(v) = &expr {
if v.text() == "input" {
bail!(span.error("input cannot be shadowed"));
}
if v.text() == "data" {
bail!(span.error("data cannot be shadowed"));
}
}
AssignOp::ColEq
}
":=" => AssignOp::ColEq,
_ => {
*self = state;
@@ -974,6 +987,7 @@ impl<'source> Parser<'source> {
let stmt = match self.parse_literal_stmt() {
Ok(stmt) => stmt,
Err(e) if is_definite_query => return Err(e),
Err(e) if matches!(self.token_text(), "=" | ":=") => return Err(e),
Err(_) => {
// There was error parsing the first literal
// Restore the state and return.
@@ -1117,7 +1131,16 @@ impl<'source> Parser<'source> {
let span = self.tok.1.clone();
let mut term = if self.tok.0 == TokenKind::Ident {
Expr::Var(self.parse_var()?)
let v = self.parse_var()?;
if self.rego_v1 {
if v.text() == "input" {
bail!(span.error("input cannot be shadowed"));
}
if v.text() == "data" {
bail!(span.error("data cannot be shadowed"));
}
}
Expr::Var(v)
} else {
return Err(self.source.error(
span.line,
@@ -1311,6 +1334,9 @@ impl<'source> Parser<'source> {
false
}
"{" => {
if self.rego_v1 {
bail!(span.error("`if` keyword is required before rule body"));
}
self.next_token()?;
let query = Ref::new(self.parse_query(span.clone(), "}")?);
span.end = self.end;
@@ -1378,6 +1404,9 @@ impl<'source> Parser<'source> {
});
}
"{" => {
if self.rego_v1 {
bail!(span.error("`if` keyword is required before rule body"));
}
self.next_token()?;
let query = Ref::new(self.parse_query(span.clone(), "}")?);
span.end = self.end;
@@ -1463,6 +1492,25 @@ impl<'source> Parser<'source> {
let head = self.parse_rule_head()?;
let bodies = self.parse_rule_bodies()?;
span.end = self.end;
if self.rego_v1 && bodies.is_empty() {
match &head {
RuleHead::Compr { assign, .. } | RuleHead::Func { assign, .. }
if assign.is_none() =>
{
bail!(span.error("rule must have a body or assignment"));
}
RuleHead::Set { refr, key, .. } if key.is_none() => {
if Self::get_path_ref_components(refr)?.len() == 2 {
bail!(span.error("`contains` keyword is required for partial set rules"));
} else {
bail!(span.error("rule must have a body or assignment"));
}
}
_ => (),
}
}
Ok(Rule::Spec { span, head, bodies })
}
@@ -1526,15 +1574,25 @@ impl<'source> Parser<'source> {
let refr = Ref::new(self.parse_path_ref()?);
let comps = Self::get_path_ref_components(&refr)?;
if !matches!(comps[0].text(), "data" | "future" | "input") {
span.end = self.end;
if !matches!(comps[0].text(), "data" | "future" | "input" | "rego") {
return Err(self.source.error(
comps[0].line,
comps[0].col,
"import path must begin with one of: {data, future, input}",
"import path must begin with one of: {data, future, input, rego}",
));
}
let is_future_kw = self.handle_import_future_keywords(&comps)?;
let is_future_kw =
if comps.len() == 2 && comps[0].text() == "rego" && comps[1].text() == "v1" {
self.rego_v1 = true;
for kw in FUTURE_KEYWORDS {
self.set_future_keyword(kw, &span)?;
}
true
} else {
self.handle_import_future_keywords(&comps)?
};
let var = if self.token_text() == "as" {
if is_future_kw {
@@ -1588,6 +1646,7 @@ impl<'source> Parser<'source> {
package,
imports,
policy,
rego_v1: self.rego_v1,
})
}

View File

@@ -3,7 +3,6 @@
use crate::ast::Expr::*;
use crate::ast::*;
use crate::builtins;
use crate::lexer::*;
use crate::utils::*;
@@ -629,6 +628,7 @@ impl Analyzer {
let mut used_vars = vec![];
let mut comprs = vec![];
let full_expr = expr;
std::convert::identity(&full_expr);
traverse(expr, &mut |e| match e.as_ref() {
Var(v) if !matches!(v.text(), "_" | "input" | "data") => {
let name = v.source_str();
@@ -645,15 +645,18 @@ impl Analyzer {
first_use.entry(name).or_insert(v.clone());
}
} else if !scope.inputs.contains(&name) {
match get_path_string(full_expr, None) {
Ok(path)
if builtins::BUILTINS.contains_key(path.as_str())
|| builtins::deprecated::DEPRECATED.contains_key(path.as_str()) => {
#[cfg(feature = "deprecated")]
{
if let Ok(path) = get_path_string(full_expr, None) {
if crate::builtins::BUILTINS.contains_key(path.as_str())
|| crate::builtins::deprecated::DEPRECATED
.contains_key(path.as_str())
{
return Ok(false);
}
}
_ => bail!(v.error(
format!("use of undefined variable `{name}` is unsafe").as_str()
)),
}
bail!(v.error(format!("use of undefined variable `{name}` is unsafe").as_str()));
}
Ok(false)
}

View File

@@ -84,7 +84,6 @@ fn match_values(computed: &Value, expected: &Value) -> Result<()> {
pub fn check_output(computed_results: &[Value], expected_results: &[Value]) -> Result<()> {
if computed_results.len() != expected_results.len() {
dbg!((&computed_results, &expected_results));
bail!(
"the number of computed results ({}) and expected results ({}) is not equal",
computed_results.len(),

View File

@@ -109,7 +109,7 @@ pub fn get_path_string(refr: &Expr, document: Option<&str>) -> Result<String> {
Ok(comps.join("."))
}
pub type FunctionTable = BTreeMap<String, (Vec<Ref<Rule>>, u8)>;
pub type FunctionTable = BTreeMap<String, (Vec<Ref<Rule>>, u8, Ref<Module>)>;
fn get_extra_arg_impl(
expr: &Expr,
@@ -118,11 +118,11 @@ fn get_extra_arg_impl(
) -> Result<Option<Ref<Expr>>> {
if let Expr::Call { fcn, params, .. } = expr {
let full_path = get_path_string(fcn, module)?;
let n_args = if let Some((_, n_args)) = functions.get(&full_path) {
let n_args = if let Some((_, n_args, _)) = functions.get(&full_path) {
*n_args
} else {
let path = get_path_string(fcn, None)?;
if let Some((_, n_args)) = functions.get(&path) {
if let Some((_, n_args, _)) = functions.get(&path) {
*n_args
} else if let Some((_, n_args)) = BUILTINS.get(path.as_str()) {
*n_args
@@ -169,7 +169,7 @@ pub fn gather_functions(modules: &[Ref<Module>]) -> Result<FunctionTable> {
{
let full_path = get_path_string(refr, Some(module_path.as_str()))?;
if let Some((functions, arity)) = table.get_mut(&full_path) {
if let Some((functions, arity, _)) = table.get_mut(&full_path) {
if args.len() as u8 != *arity {
bail!(span.error(
format!("{full_path} was previously defined with {arity} arguments.")
@@ -178,7 +178,10 @@ pub fn gather_functions(modules: &[Ref<Module>]) -> Result<FunctionTable> {
}
functions.push(rule.clone());
} else {
table.insert(full_path, (vec![rule.clone()], args.len() as u8));
table.insert(
full_path,
(vec![rule.clone()], args.len() as u8, module.clone()),
);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -45,7 +45,7 @@ fn eval_test_case(dir: &Path, case: &TestCase) -> Result<Value> {
let mut values = vec![];
for qr in query_results.result {
values.push(if !qr.bindings.is_empty_object() {
values.push(if !qr.bindings.as_object()?.is_empty() {
qr.bindings.clone()
} else if let Some(v) = qr.expressions.last() {
v.value.clone()
@@ -53,7 +53,7 @@ fn eval_test_case(dir: &Path, case: &TestCase) -> Result<Value> {
Value::Undefined
});
}
let result = Value::from_array(values);
let result = Value::from(values);
// Make result json compatible. (E.g: avoid sets).
Value::from_json_str(&result.to_string())
}

View File

@@ -0,0 +1,481 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
cases:
- note: conflict future after rego.v1
data: {}
modules:
- |
package test
import rego.v1
import future.keywords.in
query: data
error: "this import shadows previous import"
- note: conflict rego after future
data: {}
modules:
- |
package test
import future.keywords.in
import rego.v1
query: data
error: "this import shadows previous import"
- note: conflict rego after rego
data: {}
modules:
- |
package test
import rego.v1
import rego.v1
query: data
error: "this import shadows previous import"
- note: allowed future after future
data: {}
modules:
- |
package test
import future.keywords
import future.keywords
query: data.test
want_result: {}
- note: if-required-before-body
data: {}
modules:
- |
package test
import rego.v1
allow {
1 > 2
}
query: data
error: "`if` keyword is required before rule body"
- note: ok-if-before-body
data: {}
modules:
- |
package test
import rego.v1
allow if {
1 < 2
}
query: data.test
want_result:
allow: true
- note: if-required-before-else-body
data: {}
modules:
- |
package test
import rego.v1
allow if {
1 > 2
} else = 5 {
1 < 2
}
query: data
error: "`if` keyword is required before rule body"
- note: ok-if-before-else-body
data: {}
modules:
- |
package test
import rego.v1
allow if {
1 > 2
} else = 5 if {
1 < 2
}
query: data.test
want_result:
allow: 5
# cases from https://www.openpolicyagent.org/docs/latest/opa-1/#backwards-compatibility-in-opa-v10
- note: invalid1
data: {}
modules:
- |
package test
import rego.v1
p { true }
query: data
error: "`if` keyword is required before rule body"
- note: invalid2
data: {}
modules:
- |
package test
import rego.v1
p.a { true }
query: data
error: "`if` keyword is required before rule body"
- note: invalid3
data: {}
modules:
- |
package test
import rego.v1
p.a.b { true }
query: data
error: "`if` keyword is required before rule body"
- note: valid1
data: {}
modules:
- |
package test
import rego.v1
p if true
query: data.test
want_result:
p: true
- note: valid2
data: {}
modules:
- |
package test
import rego.v1
p.a if true
query: data.test
want_result:
p:
a: true
- note: valid3
data: {}
modules:
- |
package test
import rego.v1
p.a.b if true
query: data.test
want_result:
p:
a:
b: true
- note: valid4
data: {}
modules:
- |
package test
import rego.v1
p contains "a"
query: data.test
want_result:
p:
set!: ["a"]
- note: valid5
data: {}
modules:
- |
package test
import rego.v1
p := 1
query: data.test
want_result:
p: 1
- note: valid6
data: {}
modules:
- |
package test
import rego.v1
p.a := 1
query: data.test
want_result:
p:
a: 1
- note: valid6
data: {}
modules:
- |
package test
import rego.v1
p.a.b := 1
query: data.test
want_result:
p:
a:
b: 1
- note: invalid11
data: {}
modules:
- |
package test
import rego.v1
p
query: data.test
error: rule must have a body
- note: invalid12
data: {}
modules:
- |
package test
import rego.v1
p.a
query: data.test
error: "`contains` keyword is required for partial set rules"
- note: invalid13
data: {}
modules:
- |
package test
import rego.v1
p.a.b
query: data.test
error: rule must have a body
- note: invalid21
data: {}
modules:
- |
package test
import rego.v1
p { true }
query: data.test
error: "`if` keyword is required before rule body"
- note: valid21
data: {}
modules:
- |
package test
import rego.v1
p if { true }
query: data.test
want_result:
p: true
- note: invalid22
data: {}
modules:
- |
package test
import rego.v1
p.a
query: data.test
error: "`contains` keyword is required for partial set rules"
- note: valid22
data: {}
modules:
- |
package test
import rego.v1
p contains "a"
query: data.test
want_result:
p:
set!: ["a"]
- note: invalid23
data: {}
modules:
- |
package test
import rego.v1
p.a { true }
query: data.test
error: "`if` keyword is required before rule body"
- note: valid22
data: {}
modules:
- |
package test
import rego.v1
p contains "a" if { true }
query: data.test
want_result:
p:
set!: ["a"]
- note: invalid24
data: {}
modules:
- |
package test
import rego.v1
p.a.b
query: data.test
error: "rule must have a body or assignment"
- note: valid22
data: {}
modules:
- |
package test
import rego.v1
p.a.b := true
query: data.test
want_result:
p:
a:
b: true
- note: invalid25
data: {}
modules:
- |
package test
import rego.v1
p.a.b { true }
query: data.test
error: "`if` keyword is required before rule body"
- note: valid22
data: {}
modules:
- |
package test
import rego.v1
p.a.b if { true }
query: data.test
want_result:
p:
a:
b: true
- note: data-shadowed-by-rule
data: {}
modules:
- |
package test
data = 1
query: data.test
want_result:
data: 1
- note: invalid-data-shadowed-by-rule
data: {}
modules:
- |
package test
import rego.v1
data = 1
query: data.test
error: data cannot be shadowed
- note: input-shadowed-by-rule
data: {}
modules:
- |
package test
input = 1
query: data.test
want_result:
input: 1
- note: invalid-input-shadowed-by-rule
data: {}
modules:
- |
package test
import rego.v1
input = 1
query: data.test
error: input cannot be shadowed
- note: input-shadowed-by-local-var
data: {}
modules:
- |
package test
x {
input := 1
input > 0
}
y {
# This evaluates to false
input = 1
input > 0
}
query: data.test
want_result:
x: true
- note: invalid-input-shadowed-by-local-var
data: {}
modules:
- |
package test
import rego.v1
x if {
input := 1
input > 0
}
query: data.test
error: input cannot be shadowed
- note: data-shadowed-by-local-var
data: {}
modules:
- |
package test
x {
data := 1
data > 0
}
query: data.test
want_result:
x: true
- note: invalid-data-shadowed-by-local-var
data: {}
modules:
- |
package test
import rego.v1
x if {
data := 1
data > 0
}
query: data.test
error: data cannot be shadowed
- note: deprecated-function
data: {}
modules:
- |
package test
x {
cast_array([1])
}
query: data.test
want_result:
x: true
- note: invalid-deprecated-function
data: {}
modules:
- |
package test
import rego.v1
x if {
cast_array([1])
}
query: data.test
error: is deprecated

View File

@@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
const OPA_REPO: &str = "https://github.com/open-policy-agent/opa";
const OPA_BRANCH: &str = "v0.60.0";
const OPA_BRANCH: &str = "v0.61.0";
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[serde(deny_unknown_fields)]
@@ -84,7 +84,7 @@ fn eval_test_case(case: &TestCase) -> Result<Value> {
let mut values = vec![];
for qr in query_results.result {
values.push(if !qr.bindings.is_empty_object() {
values.push(if !qr.bindings.as_object()?.is_empty() {
if case.sort_bindings == Some(true) {
let mut v = qr.bindings.clone();
let bindings = v.as_object_mut()?;
@@ -105,15 +105,15 @@ fn eval_test_case(case: &TestCase) -> Result<Value> {
});
}
let result = Value::from_array(values);
let result = Value::from(values);
// Make result json compatible. (E.g: avoid sets).
Value::from_json_str(&result.to_string())
}
fn json_schema_tests_check(actual: &Value, expected: &Value) -> bool {
// Fetch `x` binding.
let actual = &actual[0][&Value::String("x".into())];
let expected = &expected[0][&Value::String("x".into())];
let actual = &actual[0]["x"];
let expected = &expected[0]["x"];
match (actual, expected) {
(Value::Array(actual), Value::Array(expected))

View File

@@ -224,13 +224,13 @@ cases:
rego: |
package test
import foo
error: "import path must begin with one of: {data, future, input}"
error: "import path must begin with one of: {data, future, input, rego}"
- note: invalid-beginning-1
rego: |
package test
import foo.bar
error: "import path must begin with one of: {data, future, input}"
error: "import path must begin with one of: {data, future, input, rego}"
- note: missing-field-1
rego: |

View File

@@ -15,7 +15,7 @@ fn non_string_key() -> Result<()> {
obj.as_object_mut()?
.insert(Value::from(std::f64::consts::PI), Value::Null);
obj.as_object_mut()?.insert(
Value::from_array(vec![
Value::from(vec![
Value::Bool(true),
Value::Null,
Value::from(std::f64::consts::PI),
@@ -115,14 +115,14 @@ fn value_as_index() -> Result<()> {
fn string_as_index() -> Result<()> {
let obj = Value::from_json_str(r#"{ "a" : 5, "b" : 6 }"#)?;
assert_eq!(&obj["a"], &Value::from(5.0));
assert_eq!(&obj[&"b".to_owned()], &Value::from(6.0));
assert_eq!(&obj["b".to_owned()], &Value::from(6.0));
Ok(())
}
#[test]
fn usize_as_index() -> Result<()> {
assert_eq!(&Value::from_json_str("[1, 2, 3]")?[0], &Value::from(1.0));
assert_eq!(&Value::from_json_str("[1, 2, 3]")?[5], &Value::Undefined);
assert_eq!(&Value::from_json_str("[1, 2, 3]")?[0u64], &Value::from(1.0));
assert_eq!(&Value::from_json_str("[1, 2, 3]")?[5u64], &Value::Undefined);
Ok(())
}
@@ -135,9 +135,6 @@ fn api() -> Result<()> {
assert_eq!(v["a"], Value::from(3.145));
assert_eq!(v.as_object()?.len(), 1);
// Null
assert!(Value::Null.is_null());
let v = Value::new_set();
assert_eq!(v.as_set()?.len(), 0);