feat: init commit
This commit is contained in:
77
crates/burrego/.github/workflows/tests.yml
vendored
Normal file
77
crates/burrego/.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
on: [push, pull_request]
|
||||
|
||||
name: Continuous integration
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --workspace
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add clippy
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
|
||||
e2e:
|
||||
name: e2e tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup BATS
|
||||
uses: mig4/setup-bats@v1
|
||||
with:
|
||||
bats-version: 1.2.1
|
||||
- name: run e2e tests
|
||||
run: make e2e-test
|
||||
3
crates/burrego/.gitignore
vendored
Normal file
3
crates/burrego/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
target
|
||||
*.wasm
|
||||
*.tar.gz
|
||||
32
crates/burrego/Cargo.toml
Normal file
32
crates/burrego/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "burrego"
|
||||
version = "0.3.1"
|
||||
authors = ["Flavio Castelli <fcastelli@suse.com>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.21.0"
|
||||
chrono = "0.4.23"
|
||||
chrono-tz = "0.8.1"
|
||||
gtmpl = "0.7.1"
|
||||
gtmpl_value = "0.5.1"
|
||||
itertools = "0.10.5"
|
||||
json-patch = "0.3.0"
|
||||
lazy_static = "1.4.0"
|
||||
regex = "1.5.6"
|
||||
semver = "1.0.16"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.91"
|
||||
serde_yaml = "0.9.16"
|
||||
thiserror = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version= "0.3", features = ["fmt", "env-filter"] }
|
||||
url = "2.2.2"
|
||||
wasmtime = "4.0"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0"
|
||||
assert-json-diff = "2.0.2"
|
||||
clap = { version = "4.0", features = [ "derive" ] }
|
||||
24
crates/burrego/Makefile
Normal file
24
crates/burrego/Makefile
Normal file
@@ -0,0 +1,24 @@
|
||||
TESTDIRS := $(wildcard test_data/*)
|
||||
.PHONY: $(TESTDIRS)
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
cargo fmt --all -- --check
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
cargo clippy -- -D warnings
|
||||
|
||||
.PHONY: test
|
||||
test: fmt lint e2e-tests
|
||||
cargo test
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
cargo clean
|
||||
|
||||
|
||||
.PHONY: e2e-tests
|
||||
e2e-tests: $(TESTDIRS)
|
||||
$(TESTDIRS):
|
||||
$(MAKE) -C $@
|
||||
2
crates/burrego/examples/.gitignore
vendored
Normal file
2
crates/burrego/examples/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.tar.gz
|
||||
*.wasm
|
||||
139
crates/burrego/examples/cli/main.rs
Normal file
139
crates/burrego/examples/cli/main.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use serde_json::json;
|
||||
use std::{fs::File, io::BufReader, path::PathBuf, process};
|
||||
|
||||
use tracing::debug;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
extern crate burrego;
|
||||
|
||||
extern crate clap;
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
pub(crate) struct Cli {
|
||||
/// Enable verbose mode
|
||||
#[clap(short, long, value_parser)]
|
||||
verbose: bool,
|
||||
|
||||
#[clap(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
pub(crate) enum Commands {
|
||||
/// Evaluate a Rego policy compiled to WebAssembly
|
||||
Eval {
|
||||
/// JSON string with the input
|
||||
#[clap(short, long, value_name = "JSON", value_parser)]
|
||||
input: Option<String>,
|
||||
|
||||
/// Path to file containing the JSON input
|
||||
#[clap(long, value_name = "JSON_FILE", value_parser)]
|
||||
input_path: Option<String>,
|
||||
|
||||
/// JSON string with the data
|
||||
#[clap(short, long, value_name = "JSON", default_value = "{}", value_parser)]
|
||||
data: String,
|
||||
|
||||
/// OPA entrypoint to evaluate
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
value_name = "ENTRYPOINT_ID",
|
||||
default_value = "0",
|
||||
value_parser
|
||||
)]
|
||||
entrypoint: String,
|
||||
|
||||
/// Path to WebAssembly module to load
|
||||
#[clap(value_parser, value_name = "WASM_FILE", value_parser)]
|
||||
policy: String,
|
||||
},
|
||||
/// List the supported builtins
|
||||
Builtins,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// setup logging
|
||||
let level_filter = if cli.verbose { "debug" } else { "info" };
|
||||
let filter_layer = EnvFilter::new(level_filter)
|
||||
.add_directive("wasmtime_cranelift=off".parse().unwrap()) // this crate generates lots of tracing events we don't care about
|
||||
.add_directive("cranelift_codegen=off".parse().unwrap()) // this crate generates lots of tracing events we don't care about
|
||||
.add_directive("cranelift_wasm=off".parse().unwrap()) // this crate generates lots of tracing events we don't care about
|
||||
.add_directive("regalloc=off".parse().unwrap()); // this crate generates lots of tracing events we don't care about
|
||||
tracing_subscriber::registry()
|
||||
.with(filter_layer)
|
||||
.with(fmt::layer().with_writer(std::io::stderr))
|
||||
.init();
|
||||
|
||||
match &cli.command {
|
||||
Commands::Builtins => {
|
||||
println!("These are the OPA builtins currently supported:");
|
||||
for b in burrego::Evaluator::implemented_builtins() {
|
||||
println!(" - {}", b);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Commands::Eval {
|
||||
input,
|
||||
input_path,
|
||||
data,
|
||||
entrypoint,
|
||||
policy,
|
||||
} => {
|
||||
if input.is_some() && input_path.is_some() {
|
||||
return Err(anyhow!(
|
||||
"Cannot use 'input' and 'input-path' at the same time"
|
||||
));
|
||||
}
|
||||
let input_value: serde_json::Value = if let Some(input_json) = input {
|
||||
serde_json::from_str(&input_json)
|
||||
.map_err(|e| anyhow!("Cannot parse input: {:?}", e))?
|
||||
} else if let Some(input_filename) = input_path {
|
||||
let file = File::open(input_filename)
|
||||
.map_err(|e| anyhow!("Cannot read input file: {:?}", e))?;
|
||||
let reader = BufReader::new(file);
|
||||
serde_json::from_reader(reader)?
|
||||
} else {
|
||||
json!({})
|
||||
};
|
||||
|
||||
let data_value: serde_json::Value =
|
||||
serde_json::from_str(&data).map_err(|e| anyhow!("Cannot parse data: {:?}", e))?;
|
||||
let mut evaluator = burrego::EvaluatorBuilder::default()
|
||||
.policy_path(&PathBuf::from(policy))
|
||||
.host_callbacks(burrego::HostCallbacks::default())
|
||||
.build()?;
|
||||
|
||||
let (major, minor) = evaluator.opa_abi_version()?;
|
||||
debug!(major, minor, "OPA Wasm ABI");
|
||||
|
||||
let entrypoints = evaluator.entrypoints()?;
|
||||
debug!(?entrypoints, "OPA entrypoints");
|
||||
|
||||
let not_implemented_builtins = evaluator.not_implemented_builtins()?;
|
||||
if !not_implemented_builtins.is_empty() {
|
||||
eprintln!("Cannot evaluate policy, these builtins are not yet implemented:");
|
||||
for b in not_implemented_builtins {
|
||||
eprintln!(" - {}", b);
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let entrypoint_id = match entrypoint.parse() {
|
||||
Ok(id) => id,
|
||||
_ => evaluator.entrypoint_id(&String::from(entrypoint))?,
|
||||
};
|
||||
|
||||
let evaluation_res = evaluator.evaluate(entrypoint_id, &input_value, &data_value)?;
|
||||
println!("{}", serde_json::to_string_pretty(&evaluation_res)?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
12
crates/burrego/examples/gatekeeper/Makefile
Normal file
12
crates/burrego/examples/gatekeeper/Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
SOURCES=$(shell find . -name "*.rego")
|
||||
OBJECTS=$(SOURCES:%.rego=%.wasm)
|
||||
|
||||
all: $(OBJECTS)
|
||||
|
||||
%.wasm: %.rego
|
||||
opa build -t wasm -e policy/violation -o $*.tar.gz $<
|
||||
tar -xf $*.tar.gz --transform "s|policy.wasm|$*.wasm|" /policy.wasm
|
||||
rm $*.tar.gz
|
||||
|
||||
clean:
|
||||
rm -f *.wasm *.tar.gz
|
||||
@@ -0,0 +1,8 @@
|
||||
package policy
|
||||
|
||||
violation[{"msg": msg}] {
|
||||
object_namespace := input.review.object.metadata.namespace
|
||||
satisfied := [allowed_namespace | namespace = input.parameters.allowed_namespaces[_]; allowed_namespace = object_namespace == namespace]
|
||||
not any(satisfied)
|
||||
msg := sprintf("object created under an invalid namespace %s; allowed namespaces are %v", [object_namespace, input.parameters.allowed_namespaces])
|
||||
}
|
||||
6
crates/burrego/examples/gatekeeper/always-accept.rego
Normal file
6
crates/burrego/examples/gatekeeper/always-accept.rego
Normal file
@@ -0,0 +1,6 @@
|
||||
package policy
|
||||
|
||||
violation[{"msg": msg}] {
|
||||
false
|
||||
msg := ""
|
||||
}
|
||||
5
crates/burrego/examples/gatekeeper/always-reject.rego
Normal file
5
crates/burrego/examples/gatekeeper/always-reject.rego
Normal file
@@ -0,0 +1,5 @@
|
||||
package policy
|
||||
|
||||
violation[{"msg": msg}] {
|
||||
msg := "this is not allowed"
|
||||
}
|
||||
12
crates/burrego/examples/opa/Makefile
Normal file
12
crates/burrego/examples/opa/Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
SOURCES=$(shell find . -name "*.rego")
|
||||
OBJECTS=$(SOURCES:%.rego=%.wasm)
|
||||
|
||||
all: $(OBJECTS)
|
||||
|
||||
%.wasm: %.rego
|
||||
opa build -t wasm -e policy/main utility/policy.rego -o $*.tar.gz $<
|
||||
tar -xf $*.tar.gz --transform "s|policy.wasm|$*.wasm|" /policy.wasm
|
||||
rm $*.tar.gz
|
||||
|
||||
clean:
|
||||
rm -f *.wasm *.tar.gz
|
||||
8
crates/burrego/examples/opa/accept-in-namespaces.rego
Normal file
8
crates/burrego/examples/opa/accept-in-namespaces.rego
Normal file
@@ -0,0 +1,8 @@
|
||||
package kubernetes.admission
|
||||
|
||||
deny[msg] {
|
||||
object_namespace := input.request.object.metadata.namespace
|
||||
satisfied := [allowed_namespace | namespace = data.allowed_namespaces[_]; allowed_namespace = object_namespace == namespace]
|
||||
not any(satisfied)
|
||||
msg := sprintf("object created under an invalid namespace %s; allowed namespaces are %v", [object_namespace, data.allowed_namespaces])
|
||||
}
|
||||
6
crates/burrego/examples/opa/always-accept.rego
Normal file
6
crates/burrego/examples/opa/always-accept.rego
Normal file
@@ -0,0 +1,6 @@
|
||||
package kubernetes.admission
|
||||
|
||||
deny[msg] {
|
||||
false
|
||||
msg := ""
|
||||
}
|
||||
5
crates/burrego/examples/opa/always-reject.rego
Normal file
5
crates/burrego/examples/opa/always-reject.rego
Normal file
@@ -0,0 +1,5 @@
|
||||
package kubernetes.admission
|
||||
|
||||
deny[msg] {
|
||||
msg := "this is not allowed"
|
||||
}
|
||||
8
crates/burrego/examples/opa/no-default-namespace.rego
Normal file
8
crates/burrego/examples/opa/no-default-namespace.rego
Normal file
@@ -0,0 +1,8 @@
|
||||
package kubernetes.admission
|
||||
|
||||
# RBAC alone would suffice here, but we create a policy just to show
|
||||
# how it can be done as well.
|
||||
deny[msg] {
|
||||
input.request.object.metadata.namespace == "default"
|
||||
msg := "you cannot use the default namespace"
|
||||
}
|
||||
12
crates/burrego/examples/opa/utility/README.md
Normal file
12
crates/burrego/examples/opa/utility/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Open Policy Agent utility
|
||||
|
||||
This folder contains the entry point for Open Policy Agent policies.
|
||||
|
||||
Since Open Policy Agent policies have to produce an `AdmissionReview`
|
||||
object, this utility library contains the Rego entry point that
|
||||
generates such `AdmissionReview`, based on whether the `deny` query
|
||||
inside the package `kubernetes.admission` (defined by the policy
|
||||
itself) is evaluated to `true`.
|
||||
|
||||
If `deny` evaluates to true, the produced `AdmissionReview` will
|
||||
reject the request. Otherwise, it will be accepted.
|
||||
23
crates/burrego/examples/opa/utility/policy.rego
Normal file
23
crates/burrego/examples/opa/utility/policy.rego
Normal file
@@ -0,0 +1,23 @@
|
||||
package policy
|
||||
|
||||
import data.kubernetes.admission
|
||||
|
||||
main = {
|
||||
"apiVersion": "admission.k8s.io/v1",
|
||||
"kind": "AdmissionReview",
|
||||
"response": response,
|
||||
}
|
||||
|
||||
response = {
|
||||
"uid": input.request.uid,
|
||||
"allowed": false,
|
||||
"status": {"message": reason},
|
||||
} {
|
||||
reason = concat(", ", admission.deny)
|
||||
reason != ""
|
||||
} else = {
|
||||
"uid": input.request.uid,
|
||||
"allowed": true,
|
||||
} {
|
||||
true
|
||||
}
|
||||
39
crates/burrego/src/builtins/builtins_helper.rs
Normal file
39
crates/burrego/src/builtins/builtins_helper.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use super::{get_builtins, BuiltinFunctionsMap};
|
||||
use crate::errors::{BurregoError, Result};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use std::sync::RwLock;
|
||||
use tracing::debug;
|
||||
|
||||
lazy_static! {
|
||||
pub(crate) static ref BUILTINS_HELPER: RwLock<BuiltinsHelper> = {
|
||||
RwLock::new(BuiltinsHelper {
|
||||
builtins: get_builtins(),
|
||||
})
|
||||
};
|
||||
}
|
||||
pub(crate) struct BuiltinsHelper {
|
||||
builtins: BuiltinFunctionsMap,
|
||||
}
|
||||
|
||||
impl BuiltinsHelper {
|
||||
pub(crate) fn invoke(
|
||||
&self,
|
||||
builtin_name: &str,
|
||||
args: &[serde_json::Value],
|
||||
) -> Result<serde_json::Value> {
|
||||
let builtin_fn = self
|
||||
.builtins
|
||||
.get(builtin_name)
|
||||
.ok_or_else(|| BurregoError::BuiltinNotImplementedError(builtin_name.to_string()))?;
|
||||
|
||||
debug!(
|
||||
builtin = builtin_name,
|
||||
args = serde_json::to_string(&args)
|
||||
.expect("cannot convert builtins args to JSON")
|
||||
.as_str(),
|
||||
"invoking builtin"
|
||||
);
|
||||
builtin_fn(args)
|
||||
}
|
||||
}
|
||||
20
crates/burrego/src/builtins/debugging.rs
Normal file
20
crates/burrego/src/builtins/debugging.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use crate::errors::{BurregoError, Result};
|
||||
|
||||
#[tracing::instrument(skip(args))]
|
||||
pub fn trace(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "trace".to_string(),
|
||||
message: "Wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let message_str = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "trace".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
tracing::debug!("{}", message_str);
|
||||
|
||||
Ok(serde_json::Value::Null)
|
||||
}
|
||||
583
crates/burrego/src/builtins/encoding.rs
Normal file
583
crates/burrego/src/builtins/encoding.rs
Normal file
@@ -0,0 +1,583 @@
|
||||
pub mod base64url {
|
||||
use crate::errors::{BurregoError, Result};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
|
||||
/// A base64 engine that uses URL_SAFE alphabet and escapes using no padding
|
||||
/// For performance reasons, it's recommended to cache its creation
|
||||
pub const BASE64_ENGINE: general_purpose::GeneralPurpose =
|
||||
general_purpose::GeneralPurpose::new(&base64::alphabet::URL_SAFE, general_purpose::NO_PAD);
|
||||
|
||||
pub fn encode_no_pad(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "base64url.encode_no_pad".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "base64url.encode_no_pad".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let res = BASE64_ENGINE.encode(input);
|
||||
|
||||
serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "base64url.encode_no_pad".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_encode_no_pad() {
|
||||
let input = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!(input)];
|
||||
let actual = encode_no_pad(&args);
|
||||
assert!(actual.is_ok());
|
||||
|
||||
let actual = actual.unwrap();
|
||||
assert_eq!(json!(BASE64_ENGINE.encode(input)), actual);
|
||||
|
||||
let engine_with_pad = general_purpose::GeneralPurpose::new(
|
||||
&base64::alphabet::URL_SAFE,
|
||||
general_purpose::PAD,
|
||||
);
|
||||
|
||||
assert_ne!(json!(engine_with_pad.encode(input)), actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod urlquery {
|
||||
use crate::errors::{BurregoError, Result};
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
pub fn encode(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "urlquery.encode".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "urlquery.encode".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let mut url =
|
||||
Url::parse("https://example.com/").map_err(|e| BurregoError::BuiltinError {
|
||||
name: "urlquery.encode".to_string(),
|
||||
message: format!("internal error 1 - {:?}", e),
|
||||
})?;
|
||||
url.set_query(Some(format!("input={}", input).as_str()));
|
||||
|
||||
let res = url.query().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "urlquery.encode".to_string(),
|
||||
message: "internal error 2".to_string(),
|
||||
})?;
|
||||
let res = res
|
||||
.strip_prefix("input=")
|
||||
.ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "urlquery.encode".to_string(),
|
||||
message: "internal error 3".to_string(),
|
||||
})?;
|
||||
|
||||
serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "urlquery.encode".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decode(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "urlquery.decode".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "urlquery.decode".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let mut url =
|
||||
Url::parse("https://example.com/").map_err(|e| BurregoError::BuiltinError {
|
||||
name: "urlquery.decode".to_string(),
|
||||
message: format!("internal error 1 - {:?}", e),
|
||||
})?;
|
||||
url.set_query(Some(format!("input={}", input).as_str()));
|
||||
|
||||
let mut pairs = url.query_pairs();
|
||||
if pairs.count() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "urlquery.decode".to_string(),
|
||||
message: "internal error 2".to_string(),
|
||||
});
|
||||
}
|
||||
let (_, value) = pairs.next().unwrap();
|
||||
serde_json::to_value(value).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "urlquery.decode".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode_object(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "urlquery.encode_object".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let obj = args[0]
|
||||
.as_object()
|
||||
.ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "urlquery.encode_object".to_string(),
|
||||
message: "1st parameter is not an object".to_string(),
|
||||
})?;
|
||||
|
||||
let mut url =
|
||||
Url::parse("https://example.com/").map_err(|e| BurregoError::BuiltinError {
|
||||
name: "urlquery.encode_object".to_string(),
|
||||
message: format!("internal error 1 - {:?}", e),
|
||||
})?;
|
||||
|
||||
let mut queries: Vec<String> = Vec::new();
|
||||
for (key, value) in obj.iter() {
|
||||
let value_str = value.as_str();
|
||||
if value_str.is_none() {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "urlquery.encode_object".to_string(),
|
||||
message: format!("the value of key {} is not a string", key),
|
||||
});
|
||||
}
|
||||
queries.push(format!("{}={}", key, value_str.unwrap()));
|
||||
}
|
||||
url.set_query(Some(queries.join("&").as_str()));
|
||||
|
||||
let res = url.query().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "urlquery.encode_object".to_string(),
|
||||
message: "internal error 2".to_string(),
|
||||
})?;
|
||||
|
||||
serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "urlquery.encode_object".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decode_object(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "urlquery.decode_object".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "urlquery.decode".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let mut url =
|
||||
Url::parse("https://example.com/").map_err(|e| BurregoError::BuiltinError {
|
||||
name: "urlquery.decode_object".to_string(),
|
||||
message: format!("internal error 1 - {:?}", e),
|
||||
})?;
|
||||
url.set_query(Some(input));
|
||||
|
||||
let mut res: HashMap<String, String> = HashMap::new();
|
||||
let pairs = url.query_pairs();
|
||||
for (key, value) in pairs {
|
||||
res.insert(String::from(key), String::from(value));
|
||||
}
|
||||
|
||||
serde_json::to_value(&res).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "urlquery.decode_object".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_encode() {
|
||||
let input = "español";
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!(input)];
|
||||
let actual = encode(&args);
|
||||
assert!(actual.is_ok());
|
||||
|
||||
let actual = actual.unwrap();
|
||||
assert_eq!(json!("espa%C3%B1ol"), actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
let input = "espa%C3%B1ol";
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!(input)];
|
||||
let actual = decode(&args);
|
||||
assert!(actual.is_ok());
|
||||
|
||||
let actual = actual.unwrap();
|
||||
assert_eq!(json!("español"), actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_object() {
|
||||
let input = json!(
|
||||
{
|
||||
"language": "español",
|
||||
"name": "Rafael Fernández López"
|
||||
});
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!(input)];
|
||||
let actual = encode_object(&args);
|
||||
assert!(actual.is_ok());
|
||||
|
||||
assert_json_eq!(
|
||||
json!("language=espa%C3%B1ol&name=Rafael%20Fern%C3%A1ndez%20L%C3%B3pez"),
|
||||
actual.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_object_does_not_have_string_values() {
|
||||
let input = json!(
|
||||
{
|
||||
"language": "español",
|
||||
"name": "Rafael Fernández López",
|
||||
"awesomeness": 100,
|
||||
});
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!(input)];
|
||||
let actual = encode_object(&args);
|
||||
assert!(actual.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_object() {
|
||||
let expected = json!(
|
||||
{
|
||||
"language": "español",
|
||||
"name": "Rafael Fernández López"
|
||||
});
|
||||
let input = json!("language=espa%C3%B1ol&name=Rafael%20Fern%C3%A1ndez%20L%C3%B3pez");
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!(input)];
|
||||
let actual = decode_object(&args);
|
||||
assert!(actual.is_ok());
|
||||
assert_json_eq!(expected, actual.unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod json {
|
||||
use crate::errors::{BurregoError, Result};
|
||||
|
||||
pub fn is_valid(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "json.is_valid".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "json.is_valid".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let v: serde_json::Result<serde_json::Value> = serde_json::from_str(input);
|
||||
let res = v.is_ok();
|
||||
|
||||
serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "json.is_valid".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_is_valid() {
|
||||
let mut cases: HashMap<String, bool> = HashMap::new();
|
||||
cases.insert(String::from("[1,2]"), true);
|
||||
cases.insert(String::from("[1,2"), false);
|
||||
|
||||
for (input, expected) in cases.iter() {
|
||||
let args: Vec<serde_json::Value> = vec![json!(input)];
|
||||
let actual = is_valid(&args);
|
||||
assert!(actual.is_ok());
|
||||
|
||||
let actual = actual.unwrap();
|
||||
assert_json_eq!(json!(expected), actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod yaml {
|
||||
use crate::errors::{BurregoError, Result};
|
||||
|
||||
pub fn marshal(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "yaml.marshal".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let input: serde_json::Value = args[0].clone();
|
||||
|
||||
// convert the generic input json value into a generic yaml value
|
||||
let value: serde_yaml::Value =
|
||||
serde_json::from_value(input).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "yaml.marshal".to_string(),
|
||||
message: format!(" cannot convert input object to yaml - {:?}", e),
|
||||
})?;
|
||||
|
||||
// marshal from yaml to string
|
||||
let res = serde_yaml::to_string(&value).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "yaml.marshal".to_string(),
|
||||
|
||||
message: format!("marshal error - {:?}", e),
|
||||
})?;
|
||||
|
||||
serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "yaml.marshal".to_string(),
|
||||
message: format!("cannot convert result into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn unmarshal(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "yaml.unmarshal".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "yaml.unmarshal".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let res: serde_json::Value =
|
||||
serde_yaml::from_str(input).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "yaml.unmarshal".to_string(),
|
||||
message: format!("cannot convert input object to json - {:?}", e),
|
||||
})?;
|
||||
|
||||
serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "yaml.unmarshal".to_string(),
|
||||
message: format!("cannot convert result into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_valid(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "yaml.is_valid".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "yaml.is_valid".to_string(),
|
||||
message: "parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let v: serde_yaml::Result<serde_yaml::Value> = serde_yaml::from_str(input);
|
||||
let res = v.is_ok();
|
||||
|
||||
serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "yaml.is_valid".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_marshal() {
|
||||
let input = json!({
|
||||
"hello": "world",
|
||||
"number": 42,
|
||||
"list": [1,2,3]
|
||||
});
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!(input)];
|
||||
let actual = marshal(&args);
|
||||
assert!(actual.is_ok());
|
||||
|
||||
let actual_str = actual.unwrap();
|
||||
let actual_json: serde_json::Value =
|
||||
serde_yaml::from_str(actual_str.as_str().unwrap()).unwrap();
|
||||
|
||||
assert_json_eq!(input, actual_json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unmarshal() {
|
||||
let input_str = r#"---
|
||||
hello: world
|
||||
list:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
number: 42
|
||||
"#;
|
||||
|
||||
let input = json!(input_str);
|
||||
|
||||
let expected = json!({
|
||||
"hello": "world",
|
||||
"number": 42,
|
||||
"list": [1,2,3]
|
||||
});
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!(input)];
|
||||
let actual = unmarshal(&args);
|
||||
assert!(actual.is_ok());
|
||||
|
||||
let actual = actual.unwrap();
|
||||
assert_json_eq!(json!(expected), actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_valid() {
|
||||
let mut cases: HashMap<String, bool> = HashMap::new();
|
||||
cases.insert(
|
||||
String::from("some_key: [1,2]\nsome_other_key: [3.0, 4.0]"),
|
||||
true,
|
||||
);
|
||||
cases.insert(String::from("some_key: [1,2"), false);
|
||||
|
||||
for (input, expected) in cases.iter() {
|
||||
let args: Vec<serde_json::Value> = vec![json!(input)];
|
||||
let actual = is_valid(&args);
|
||||
assert!(actual.is_ok());
|
||||
|
||||
let actual = actual.unwrap();
|
||||
assert_json_eq!(json!(expected), actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod hex {
|
||||
use crate::errors::{BurregoError, Result};
|
||||
use core::num;
|
||||
|
||||
pub fn encode(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "hex.encode".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "hex.encode".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let res: Vec<String> = input
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.map(|v| format!("{:x?}", v))
|
||||
.collect();
|
||||
let res = res.join("");
|
||||
|
||||
serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "hex.encode".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decode(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "hex.decode".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "hex.decode".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let value: std::result::Result<Vec<u8>, num::ParseIntError> = (0..input.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&input[i..i + 2], 16))
|
||||
.collect();
|
||||
let value = value.map_err(|e| BurregoError::BuiltinError {
|
||||
name: "hex.decode".to_string(),
|
||||
message: format!("cannot parse input - {:?}", e),
|
||||
})?;
|
||||
|
||||
let res = String::from_utf8(value).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "hex.decode".to_string(),
|
||||
message: format!("cannot parse string - {:?}", e),
|
||||
})?;
|
||||
|
||||
serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "hex.decode".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_encode() {
|
||||
let input = "hello";
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!(input)];
|
||||
let actual = encode(&args);
|
||||
|
||||
assert!(actual.is_ok());
|
||||
assert_eq!(json!("68656c6c6f"), actual.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
let input = "68656c6c6f";
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!(input)];
|
||||
let actual = decode(&args);
|
||||
|
||||
assert!(actual.is_ok());
|
||||
assert_eq!(json!("hello"), actual.unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
57
crates/burrego/src/builtins/glob.rs
Normal file
57
crates/burrego/src/builtins/glob.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use crate::errors::{BurregoError, Result};
|
||||
|
||||
pub fn quote_meta(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "glob.quote_meta".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "glob.quote_meta".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
serde_json::to_value(escape(input)).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "glob.quote_meta".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
fn escape(s: &str) -> String {
|
||||
let mut escaped = String::new();
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'*' | '?' | '\\' | '[' | ']' | '{' | '}' => {
|
||||
escaped.push('\\');
|
||||
escaped.push(c);
|
||||
}
|
||||
c => {
|
||||
escaped.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
escaped
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
fn escape() {
|
||||
assert_eq!(super::escape("*.domain.com"), r"\*.domain.com");
|
||||
|
||||
assert_eq!(super::escape("*.domain-*.com"), r"\*.domain-\*.com");
|
||||
|
||||
assert_eq!(super::escape("domain.com"), r"domain.com");
|
||||
|
||||
assert_eq!(super::escape("domain-[ab].com"), r"domain-\[ab\].com");
|
||||
|
||||
assert_eq!(super::escape("nie?ce"), r"nie\?ce");
|
||||
|
||||
assert_eq!(
|
||||
super::escape("some *?\\[]{} text"),
|
||||
"some \\*\\?\\\\\\[\\]\\{\\} text"
|
||||
);
|
||||
}
|
||||
}
|
||||
59
crates/burrego/src/builtins/json.rs
Normal file
59
crates/burrego/src/builtins/json.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use crate::errors::{BurregoError, Result};
|
||||
|
||||
pub fn patch(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 2 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "json.patch".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if !args[0].is_object() {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "json.patch".to_string(),
|
||||
message: "1st parameter is not an object".to_string(),
|
||||
});
|
||||
}
|
||||
let mut obj = args[0].clone();
|
||||
|
||||
if !args[1].is_array() {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "json.patch".to_string(),
|
||||
message: "2nd parameter is not an array".to_string(),
|
||||
});
|
||||
}
|
||||
let patches_str = serde_json::to_string(&args[1]).map_err(|_| BurregoError::BuiltinError {
|
||||
name: "json.patch".to_string(),
|
||||
message: "cannot convert 2nd parameter to string".to_string(),
|
||||
})?;
|
||||
let patches: json_patch::Patch = serde_json::from_str(&patches_str).unwrap();
|
||||
|
||||
json_patch::patch(&mut obj, &patches).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "json.patch".to_string(),
|
||||
message: format!("cannot apply patch: {:?}", e),
|
||||
})?;
|
||||
|
||||
serde_json::to_value(obj).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "json.patch".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_patch() {
|
||||
let args: Vec<serde_json::Value> = vec![
|
||||
json!({"a": {"foo": 1}}),
|
||||
json!([{"op": "add", "path": "/a/bar", "value": 2}]),
|
||||
];
|
||||
|
||||
let actual = patch(&args);
|
||||
assert!(actual.is_ok());
|
||||
assert_json_eq!(json!({"a": {"foo": 1, "bar": 2}}), actual.unwrap());
|
||||
}
|
||||
}
|
||||
65
crates/burrego/src/builtins/mod.rs
Normal file
65
crates/burrego/src/builtins/mod.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::errors::Result;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) mod builtins_helper;
|
||||
mod debugging;
|
||||
mod encoding;
|
||||
mod glob;
|
||||
mod json;
|
||||
mod regex;
|
||||
mod semver;
|
||||
mod strings;
|
||||
mod time;
|
||||
|
||||
pub(crate) use builtins_helper::BUILTINS_HELPER;
|
||||
|
||||
pub(crate) type BuiltinFunctionsMap =
|
||||
HashMap<&'static str, fn(&[serde_json::Value]) -> Result<serde_json::Value>>;
|
||||
|
||||
pub fn get_builtins() -> BuiltinFunctionsMap {
|
||||
let mut functions: BuiltinFunctionsMap = HashMap::new();
|
||||
|
||||
// debugging
|
||||
functions.insert("trace", debugging::trace);
|
||||
|
||||
// encoding
|
||||
functions.insert(
|
||||
"base64url.encode_no_pad",
|
||||
encoding::base64url::encode_no_pad,
|
||||
);
|
||||
functions.insert("urlquery.encode", encoding::urlquery::encode);
|
||||
functions.insert("urlquery.decode", encoding::urlquery::decode);
|
||||
functions.insert("urlquery.encode_object", encoding::urlquery::encode_object);
|
||||
functions.insert("urlquery.decode_object", encoding::urlquery::decode_object);
|
||||
functions.insert("json.is_valid", encoding::json::is_valid);
|
||||
functions.insert("yaml.marshal", encoding::yaml::marshal);
|
||||
functions.insert("yaml.unmarshal", encoding::yaml::unmarshal);
|
||||
functions.insert("yaml.is_valid", encoding::yaml::is_valid);
|
||||
functions.insert("hex.encode", encoding::hex::encode);
|
||||
functions.insert("hex.decode", encoding::hex::decode);
|
||||
|
||||
// glob
|
||||
functions.insert("glob.quote_meta", glob::quote_meta);
|
||||
|
||||
// objects
|
||||
functions.insert("json.patch", json::patch);
|
||||
|
||||
// regex
|
||||
functions.insert("regex.split", regex::split);
|
||||
functions.insert("regex.template_match", regex::template_match);
|
||||
functions.insert("regex.find_n", regex::find_n);
|
||||
|
||||
// semver
|
||||
functions.insert("semver.is_valid", semver::is_valid);
|
||||
functions.insert("semver.compare", semver::compare);
|
||||
|
||||
// strings
|
||||
functions.insert("sprintf", strings::sprintf);
|
||||
|
||||
// time
|
||||
functions.insert("time.now_ns", time::now_ns);
|
||||
functions.insert("parse_rfc3339_ns", time::parse_rfc3339_ns);
|
||||
functions.insert("date", time::date);
|
||||
|
||||
functions
|
||||
}
|
||||
320
crates/burrego/src/builtins/regex.rs
Normal file
320
crates/burrego/src/builtins/regex.rs
Normal file
@@ -0,0 +1,320 @@
|
||||
use crate::errors::{BurregoError, Result};
|
||||
use core::fmt::Display;
|
||||
use regex::{escape as regex_escape, Regex};
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
pub fn split(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 2 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "regex.split".to_string(),
|
||||
message: "Wrong number of arguments given".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let pattern_str = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "regex.split".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
let string_str = args[1].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "regex.split".to_string(),
|
||||
message: "2nd parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
serde_json::to_value(
|
||||
Regex::new(pattern_str)
|
||||
.map_err(|e| BurregoError::BuiltinError {
|
||||
name: "regex.split".to_string(),
|
||||
message: format!(
|
||||
"cannot build regex from the given pattern string '{}': {:?}",
|
||||
pattern_str, e
|
||||
),
|
||||
})?
|
||||
.split(string_str)
|
||||
.collect::<String>(),
|
||||
)
|
||||
.map_err(|e| BurregoError::BuiltinError {
|
||||
name: "regex.split".to_string(),
|
||||
message: format!("cannot convert result into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn template_match(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 4 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "regex.template_match".to_string(),
|
||||
message: "Wrong number of arguments given".to_string(),
|
||||
});
|
||||
}
|
||||
let pattern_str = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "regex.template_match".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
let string_str = args[1].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "regex.template_match".to_string(),
|
||||
message: "2nd parameter is not a string".to_string(),
|
||||
})?;
|
||||
let delimiter_start_str = args[2].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "regex.template_match".to_string(),
|
||||
message: "3rd parameter is not a string".to_string(),
|
||||
})?;
|
||||
if delimiter_start_str.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "regex.template_match".to_string(),
|
||||
message: "3rd parameter has to be exactly one character long".to_string(),
|
||||
});
|
||||
}
|
||||
let delimiter_end_str = args[3].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "regex.template_match".to_string(),
|
||||
message: "4th parameter is not a string".to_string(),
|
||||
})?;
|
||||
if delimiter_end_str.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "regex.template_match".to_string(),
|
||||
message: "4th parameter has to be exactly one character long".to_string(),
|
||||
});
|
||||
}
|
||||
let computed_regexp = TemplateMatch::regexp_from_template(
|
||||
pattern_str,
|
||||
// safe, since we have ensured that the length is 1
|
||||
delimiter_start_str.chars().next().unwrap(),
|
||||
// safe, since we have ensured that the length is 1
|
||||
delimiter_end_str.chars().next().unwrap(),
|
||||
)?;
|
||||
serde_json::to_value(computed_regexp.is_match(string_str)).map_err(|e| {
|
||||
BurregoError::BuiltinError {
|
||||
name: "regex.template_match".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_n(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 3 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "regex.find_n".to_string(),
|
||||
message: "Wrong number of arguments given to ".to_string(),
|
||||
});
|
||||
}
|
||||
let pattern_str = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "regex.find_n".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
let string_str = args[1].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "regex.find_n".to_string(),
|
||||
message: "2nd parameter is not a string".to_string(),
|
||||
})?;
|
||||
let take_number = args[2].as_i64().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "regex.find_n".to_string(),
|
||||
message: "3rd parameter is not a number".to_string(),
|
||||
})?;
|
||||
|
||||
let take_n = if take_number != -1 {
|
||||
take_number as usize
|
||||
} else {
|
||||
Regex::new(pattern_str)
|
||||
.map_err(|e| BurregoError::BuiltinError {
|
||||
name: "regex.find_n".to_string(),
|
||||
message: format!(
|
||||
"cannot build regex from the given pattern string '{}': {:?}",
|
||||
pattern_str, e
|
||||
),
|
||||
})?
|
||||
.find_iter(string_str)
|
||||
.count()
|
||||
};
|
||||
|
||||
let matches: Vec<String> = Regex::new(pattern_str)
|
||||
.map_err(|e| BurregoError::BuiltinError {
|
||||
name: "regex.find_n".to_string(),
|
||||
message: format!(
|
||||
"cannot build regex from the given pattern string '{}': {:?}",
|
||||
pattern_str, e
|
||||
),
|
||||
})?
|
||||
.find_iter(string_str)
|
||||
.take(take_n)
|
||||
.map(|match_| String::from(match_.as_str()))
|
||||
.collect();
|
||||
|
||||
serde_json::to_value(matches).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "regex.find_n".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
struct Expression {
|
||||
is_regexp: bool,
|
||||
expression: String,
|
||||
}
|
||||
|
||||
impl Display for Expression {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.is_regexp {
|
||||
write!(f, "{}", &self.expression)
|
||||
} else {
|
||||
write!(f, "{}", ®ex_escape(&self.expression))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ExpressionList(Vec<Expression>);
|
||||
|
||||
impl Display for ExpressionList {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for expression in self.0.iter() {
|
||||
write!(f, "{}", expression)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct TemplateMatch {}
|
||||
|
||||
impl TemplateMatch {
|
||||
fn regexp_from_template(
|
||||
template: &str,
|
||||
delimiter_start: char,
|
||||
delimiter_end: char,
|
||||
) -> Result<Regex> {
|
||||
let mut expressions = ExpressionList(Vec::new());
|
||||
let mut current_expression = Expression {
|
||||
is_regexp: false,
|
||||
expression: String::new(),
|
||||
};
|
||||
let mut delimiters_open = 0;
|
||||
|
||||
for c in template.chars() {
|
||||
if c == delimiter_start {
|
||||
delimiters_open += 1;
|
||||
if delimiters_open == 1 {
|
||||
if !current_expression.expression.is_empty() {
|
||||
expressions.0.push(current_expression);
|
||||
}
|
||||
current_expression = Expression {
|
||||
is_regexp: true,
|
||||
expression: String::new(),
|
||||
}
|
||||
}
|
||||
} else if c == delimiter_end {
|
||||
delimiters_open -= 1;
|
||||
if delimiters_open == 0 {
|
||||
if !current_expression.expression.is_empty() {
|
||||
expressions.0.push(current_expression);
|
||||
}
|
||||
current_expression = Expression {
|
||||
is_regexp: false,
|
||||
expression: String::new(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current_expression.expression.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
if !current_expression.expression.is_empty() {
|
||||
expressions.0.push(current_expression);
|
||||
}
|
||||
|
||||
Regex::from_str(&format!("{}", expressions)).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "regex".to_string(),
|
||||
message: format!("tried to initialize an invalid regular expression: {:?}", e),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn regex_from_template() -> Result<()> {
|
||||
assert!(
|
||||
TemplateMatch::regexp_from_template("urn:foo:bar:baz", '{', '}',)?
|
||||
.is_match("urn:foo:bar:baz"),
|
||||
);
|
||||
|
||||
assert!(
|
||||
TemplateMatch::regexp_from_template("urn:foo:{.*}", '{', '}',)?
|
||||
.is_match("urn:foo:bar:baz"),
|
||||
);
|
||||
|
||||
assert!(
|
||||
TemplateMatch::regexp_from_template("urn:foo:<.*>", '<', '>',)?
|
||||
.is_match("urn:foo:bar:baz"),
|
||||
);
|
||||
|
||||
assert!(
|
||||
TemplateMatch::regexp_from_template("urn:foo:{.*}", '<', '>',)?
|
||||
.is_match("urn:foo:{.*}"),
|
||||
);
|
||||
|
||||
assert!(TemplateMatch::regexp_from_template(
|
||||
"urn:foo:test:section-<[0-9]{2}>:alert-<[0-9]{4}>",
|
||||
'<',
|
||||
'>',
|
||||
)?
|
||||
.is_match("urn:foo:test:section-42:alert-1234"),);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_n() -> Result<()> {
|
||||
assert_eq!(
|
||||
super::find_n(&vec![
|
||||
serde_json::to_value("a.").unwrap(),
|
||||
serde_json::to_value("paranormal").unwrap(),
|
||||
serde_json::to_value(1).unwrap(),
|
||||
])?
|
||||
.as_array()
|
||||
.unwrap(),
|
||||
&vec!["ar",],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
super::find_n(&vec![
|
||||
serde_json::to_value("a.").unwrap(),
|
||||
serde_json::to_value("paranormal").unwrap(),
|
||||
serde_json::to_value(2).unwrap(),
|
||||
])?
|
||||
.as_array()
|
||||
.unwrap(),
|
||||
&vec!["ar", "an",],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
super::find_n(&vec![
|
||||
serde_json::to_value("a.").unwrap(),
|
||||
serde_json::to_value("paranormal").unwrap(),
|
||||
serde_json::to_value(10).unwrap(),
|
||||
])?
|
||||
.as_array()
|
||||
.unwrap(),
|
||||
&vec!["ar", "an", "al"],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
super::find_n(&vec![
|
||||
serde_json::to_value("a.").unwrap(),
|
||||
serde_json::to_value("paranormal").unwrap(),
|
||||
serde_json::to_value(-1).unwrap(),
|
||||
])?
|
||||
.as_array()
|
||||
.unwrap(),
|
||||
&vec!["ar", "an", "al"],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
super::find_n(&vec![
|
||||
serde_json::to_value("nomatch").unwrap(),
|
||||
serde_json::to_value("paranormal").unwrap(),
|
||||
serde_json::to_value(-1).unwrap(),
|
||||
])?
|
||||
.as_array()
|
||||
.unwrap(),
|
||||
&vec![] as &Vec<String>,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
102
crates/burrego/src/builtins/semver.rs
Normal file
102
crates/burrego/src/builtins/semver.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use crate::errors::{BurregoError, Result};
|
||||
use semver::Version;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
pub fn is_valid(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "semver.is_valid".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "semver.is_valid".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let valid_version = Version::parse(input).map(|_| true).unwrap_or(false);
|
||||
|
||||
serde_json::to_value(valid_version).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "semver.is_valid".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn compare(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 2 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "semver.compare".to_string(),
|
||||
message: "wrong number of arguments".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let version_a = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "semver.compare".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let version_b = args[1].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "semver.compare".to_string(),
|
||||
message: "2nd parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let version_a = Version::parse(version_a).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "semver.compare".to_string(),
|
||||
message: format!("first argument is not a valid semantic version: {:?}", e),
|
||||
})?;
|
||||
|
||||
let version_b = Version::parse(version_b).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "semver.compare".to_string(),
|
||||
message: format!("second argument is not a valid semantic version: {:?}", e),
|
||||
})?;
|
||||
|
||||
let res = match version_a.cmp(&version_b) {
|
||||
Ordering::Less => -1,
|
||||
Ordering::Equal => 0,
|
||||
Ordering::Greater => 1,
|
||||
};
|
||||
|
||||
serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "semver.compare".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn is_valid() -> Result<()> {
|
||||
assert_eq!(super::is_valid(&[json!("1.0.0")])?, true);
|
||||
assert_eq!(super::is_valid(&[json!("1.0.0-rc1")])?, true);
|
||||
assert_eq!(super::is_valid(&[json!("invalidsemver-1.0.0")])?, false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compare() -> Result<()> {
|
||||
assert_eq!(super::compare(&[json!("0.0.1"), json!("0.1.0")])?, -1);
|
||||
assert_eq!(
|
||||
super::compare(&[json!("1.0.0-rc1"), json!("1.0.0-rc1")])?,
|
||||
0
|
||||
);
|
||||
assert_eq!(super::compare(&[json!("0.1.0"), json!("0.0.1")])?, 1);
|
||||
assert_eq!(
|
||||
super::compare(&[json!("1.0.0-beta1"), json!("1.0.0-alpha3")])?,
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
super::compare(&[json!("1.0.0-rc2"), json!("1.0.0-rc1")])?,
|
||||
1
|
||||
);
|
||||
assert!(super::compare(&[json!("invalidsemver-1.0.0"), json!("0.1.0")]).is_err());
|
||||
assert!(super::compare(&[json!("0.1.0"), json!("invalidsemver-1.0.0")]).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
104
crates/burrego/src/builtins/strings.rs
Normal file
104
crates/burrego/src/builtins/strings.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use crate::errors::{BurregoError, Result};
|
||||
use std::{collections::HashMap, convert::From};
|
||||
|
||||
struct GoTmplValue(gtmpl::Value);
|
||||
|
||||
impl From<serde_json::Value> for GoTmplValue {
|
||||
fn from(value: serde_json::Value) -> Self {
|
||||
match value {
|
||||
serde_json::Value::String(s) => GoTmplValue(gtmpl::Value::String(s)),
|
||||
serde_json::Value::Number(n) => {
|
||||
let n: i64 = n.as_i64().unwrap();
|
||||
let number: gtmpl_value::Number = n.into();
|
||||
GoTmplValue(gtmpl::Value::Number(number))
|
||||
}
|
||||
serde_json::Value::Bool(b) => GoTmplValue(gtmpl::Value::Bool(b)),
|
||||
serde_json::Value::Array(arr) => {
|
||||
let res: Vec<gtmpl::Value> = arr
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let v: GoTmplValue = i.clone().into();
|
||||
v.0
|
||||
})
|
||||
.collect();
|
||||
GoTmplValue(gtmpl::Value::Array(res))
|
||||
}
|
||||
serde_json::Value::Object(obj) => {
|
||||
let res: HashMap<String, gtmpl::Value> = obj
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
let val: GoTmplValue = v.clone().into();
|
||||
(k.clone(), val.0)
|
||||
})
|
||||
.collect();
|
||||
GoTmplValue(gtmpl::Value::Map(res))
|
||||
}
|
||||
_ => GoTmplValue(gtmpl::Value::Nil),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sprintf(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 2 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "sprintf".to_string(),
|
||||
message: "Wrong number of arguments given".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let fmt_str = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "sprintf".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
let fmt_args: Vec<gtmpl::Value> = args[1]
|
||||
.as_array()
|
||||
.ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "sprintf".to_string(),
|
||||
message: "2nd parameter is not an array".to_string(),
|
||||
})?
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let g: GoTmplValue = i.clone().into();
|
||||
g.0
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut index_cmds: Vec<String> = Vec::new();
|
||||
for i in 0..fmt_args.len() {
|
||||
index_cmds.push(format!("(index . {})", i));
|
||||
}
|
||||
|
||||
let template_str = format!(r#"{{{{ printf "{}" {}}}}}"#, fmt_str, index_cmds.join(" "));
|
||||
let res = gtmpl::template(&template_str, fmt_args.as_slice()).map_err(|e| {
|
||||
BurregoError::BuiltinError {
|
||||
name: "sprintf".to_string(),
|
||||
message: format!(
|
||||
"Cannot render go template '{}' with args {:?}: {:?}",
|
||||
template_str, fmt_args, e
|
||||
),
|
||||
}
|
||||
})?;
|
||||
|
||||
serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "sprintf".to_string(),
|
||||
message: format!("Cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn sprintf_mixed_input() {
|
||||
let args: Vec<serde_json::Value> = vec![
|
||||
json!("hello %v %v %v"),
|
||||
json!(["world", 42, ["this", "is", "a", "list"]]),
|
||||
];
|
||||
|
||||
let actual = sprintf(&args);
|
||||
assert!(actual.is_ok());
|
||||
assert_eq!(json!("hello world 42 [this is a list]"), actual.unwrap());
|
||||
}
|
||||
}
|
||||
195
crates/burrego/src/builtins/time.rs
Normal file
195
crates/burrego/src/builtins/time.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use crate::errors::{BurregoError, Result};
|
||||
use chrono::{self, DateTime, Datelike, Duration, Local};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub fn now_ns(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if !args.is_empty() {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "time.now_ns".to_string(),
|
||||
message: "wrong number of arguments given".to_string(),
|
||||
});
|
||||
}
|
||||
let now = Local::now();
|
||||
serde_json::to_value(now.timestamp_nanos()).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "time.now_ns".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_rfc3339_ns(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "time.parse_rfc3339_ns".to_string(),
|
||||
message: "wrong number of arguments given".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let value = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "time.parse_rfc3339_ns".to_string(),
|
||||
message: "1st parameter is not a string".to_string(),
|
||||
})?;
|
||||
|
||||
let dt = DateTime::parse_from_rfc3339(value).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "time.parse_rfc3339_ns".to_string(),
|
||||
message: format!(": cannot convert {}: {:?}", value, e),
|
||||
})?;
|
||||
|
||||
serde_json::to_value(dt.timestamp_nanos()).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "time.parse_rfc3339_ns".to_string(),
|
||||
message: format!("cannot convert value into JSON: {:?}", e),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn date(args: &[serde_json::Value]) -> Result<serde_json::Value> {
|
||||
if args.len() != 1 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "time.date".to_string(),
|
||||
message: "wrong number of arguments given".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let nanoseconds: i64;
|
||||
let mut timezone: chrono_tz::Tz = chrono_tz::UTC;
|
||||
|
||||
match args[0].clone() {
|
||||
serde_json::Value::Number(val) => {
|
||||
nanoseconds = val.as_i64().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "time.date".to_string(),
|
||||
message: "1st parameter is not a number".to_string(),
|
||||
})?;
|
||||
}
|
||||
serde_json::Value::Array(val) => {
|
||||
if val.len() != 2 {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "time.date".to_string(),
|
||||
message: "wrong number of items inside of input array".to_string(),
|
||||
});
|
||||
}
|
||||
nanoseconds = val[0].as_i64().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "time.date".to_string(),
|
||||
message: "1st array item is not a number".to_string(),
|
||||
})?;
|
||||
let tz_name = val[1].as_str().ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "time.date".to_string(),
|
||||
message: "2nd array item is not a string".to_string(),
|
||||
})?;
|
||||
if tz_name == "Local" {
|
||||
return date_local(nanoseconds);
|
||||
} else {
|
||||
timezone =
|
||||
chrono_tz::Tz::from_str(tz_name).map_err(|e| BurregoError::BuiltinError {
|
||||
name: "time.date".to_string(),
|
||||
message: format!("cannot handle given timezone {}: {:?}", tz_name, e),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(BurregoError::BuiltinError {
|
||||
name: "time.date".to_string(),
|
||||
message: "the 1st parameter is neither a number nor an array".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let unix_epoch = DateTime::<chrono::Utc>::from_utc(
|
||||
chrono::NaiveDateTime::from_timestamp_opt(0, 0).ok_or_else(|| {
|
||||
BurregoError::BuiltinError {
|
||||
name: "time.date".to_string(),
|
||||
message: "cannot create timestamp".to_string(),
|
||||
}
|
||||
})?,
|
||||
chrono::Utc,
|
||||
);
|
||||
let dt = unix_epoch
|
||||
.checked_add_signed(Duration::nanoseconds(nanoseconds))
|
||||
.ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "time.date".to_string(),
|
||||
message: "overflow when building date".to_string(),
|
||||
})?
|
||||
.with_timezone(&timezone);
|
||||
|
||||
Ok(serde_json::json!([dt.year(), dt.month(), dt.day(),]))
|
||||
}
|
||||
|
||||
pub fn date_local(ns: i64) -> Result<serde_json::Value> {
|
||||
let unix_epoch = DateTime::<chrono::Utc>::from_utc(
|
||||
chrono::NaiveDateTime::from_timestamp_opt(0, 0).ok_or_else(|| {
|
||||
BurregoError::BuiltinError {
|
||||
name: "time.date".to_string(),
|
||||
message: "cannot create timestamp".to_string(),
|
||||
}
|
||||
})?,
|
||||
chrono::Utc,
|
||||
);
|
||||
let dt = unix_epoch
|
||||
.checked_add_signed(Duration::nanoseconds(ns))
|
||||
.ok_or_else(|| BurregoError::BuiltinError {
|
||||
name: "time.date".to_string(),
|
||||
message: "overflow when building date".to_string(),
|
||||
})?
|
||||
.with_timezone(&chrono::Local);
|
||||
|
||||
Ok(serde_json::json!([dt.year(), dt.month(), dt.day(),]))
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_parse_rfc3339_ns() {
|
||||
let input_dt = Local::now();
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!(input_dt.to_rfc3339())];
|
||||
|
||||
let actual = parse_rfc3339_ns(&args);
|
||||
assert!(actual.is_ok());
|
||||
assert_eq!(json!(input_dt.timestamp_nanos()), actual.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_with_no_tz() {
|
||||
let input_dt = Local::now().naive_utc();
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!(input_dt.timestamp_nanos())];
|
||||
|
||||
let actual = date(&args);
|
||||
assert!(actual.is_ok());
|
||||
assert_eq!(
|
||||
json!([input_dt.year(), input_dt.month(), input_dt.day()]),
|
||||
actual.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_with_tz() {
|
||||
let input_dt = match chrono_tz::US::Pacific.with_ymd_and_hms(1990, 5, 6, 12, 30, 45) {
|
||||
chrono::LocalResult::Single(dt) => dt,
|
||||
_ => panic!("didn't get the expected datetime object"),
|
||||
};
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!([input_dt.timestamp_nanos(), "US/Pacific"])];
|
||||
|
||||
let actual = date(&args);
|
||||
assert!(actual.is_ok());
|
||||
assert_eq!(
|
||||
json!([input_dt.year(), input_dt.month(), input_dt.day()]),
|
||||
actual.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_with_local_tz() {
|
||||
let input_dt = Local::now().naive_utc();
|
||||
|
||||
let args: Vec<serde_json::Value> = vec![json!([input_dt.timestamp_nanos(), "Local"])];
|
||||
|
||||
let actual = date(&args);
|
||||
assert!(actual.is_ok());
|
||||
assert_eq!(
|
||||
json!([input_dt.year(), input_dt.month(), input_dt.day()]),
|
||||
actual.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
31
crates/burrego/src/errors.rs
Normal file
31
crates/burrego/src/errors.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, BurregoError>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BurregoError {
|
||||
#[error("Missing Rego builtins: {0}")]
|
||||
MissingRegoBuiltins(String),
|
||||
|
||||
#[error("wasm engine error: {0}")]
|
||||
WasmEngineError(String),
|
||||
|
||||
#[error("Rego wasm error: {0}")]
|
||||
RegoWasmError(String),
|
||||
|
||||
#[error("JSON error: {0}")]
|
||||
JSONError(String),
|
||||
|
||||
#[error("Evaluator builder error: {0}")]
|
||||
EvaluatorBuilderError(String),
|
||||
|
||||
#[error("Builtin error [{name:?}]: {message:?}")]
|
||||
BuiltinError { name: String, message: String },
|
||||
|
||||
#[error("Builtin not implemented: {0}")]
|
||||
BuiltinNotImplementedError(String),
|
||||
|
||||
/// Wasmtime execution deadline exceeded
|
||||
#[error("guest code interrupted, execution deadline exceeded")]
|
||||
ExecutionDeadlineExceeded,
|
||||
}
|
||||
244
crates/burrego/src/evaluator.rs
Normal file
244
crates/burrego/src/evaluator.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use crate::builtins;
|
||||
use crate::errors::{BurregoError, Result};
|
||||
use crate::host_callbacks::HostCallbacks;
|
||||
use crate::opa_host_functions;
|
||||
use crate::policy::Policy;
|
||||
use crate::stack_helper::StackHelper;
|
||||
|
||||
use itertools::Itertools;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tracing::debug;
|
||||
use wasmtime::{Engine, Instance, Linker, Memory, MemoryType, Module, Store};
|
||||
|
||||
macro_rules! set_epoch_deadline_and_call_guest {
|
||||
($epoch_deadline:expr, $store:expr, $code:block) => {{
|
||||
if let Some(deadline) = $epoch_deadline {
|
||||
$store.set_epoch_deadline(deadline);
|
||||
}
|
||||
$code
|
||||
}};
|
||||
}
|
||||
|
||||
struct EvaluatorStack {
|
||||
store: Store<Option<StackHelper>>,
|
||||
instance: Instance,
|
||||
memory: Memory,
|
||||
policy: Policy,
|
||||
}
|
||||
|
||||
pub struct Evaluator {
|
||||
engine: Engine,
|
||||
module: Module,
|
||||
store: Store<Option<StackHelper>>,
|
||||
instance: Instance,
|
||||
memory: Memory,
|
||||
policy: Policy,
|
||||
host_callbacks: HostCallbacks,
|
||||
/// used to tune the [epoch
|
||||
/// interruption](https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#method.epoch_interruption)
|
||||
/// feature of wasmtime
|
||||
epoch_deadline: Option<u64>,
|
||||
}
|
||||
|
||||
impl Evaluator {
|
||||
pub(crate) fn from_engine_and_module(
|
||||
engine: Engine,
|
||||
module: Module,
|
||||
host_callbacks: HostCallbacks,
|
||||
epoch_deadline: Option<u64>,
|
||||
) -> Result<Evaluator> {
|
||||
let stack = Self::setup(engine.clone(), module.clone(), host_callbacks.clone())?;
|
||||
let mut store = stack.store;
|
||||
let instance = stack.instance;
|
||||
let memory = stack.memory;
|
||||
let policy = stack.policy;
|
||||
|
||||
let used_builtins = set_epoch_deadline_and_call_guest!(epoch_deadline, store, {
|
||||
policy
|
||||
.builtins(&mut store, &memory)?
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
});
|
||||
|
||||
debug!(used = used_builtins.as_str(), "policy builtins");
|
||||
|
||||
let mut evaluator = Evaluator {
|
||||
engine,
|
||||
module,
|
||||
store,
|
||||
instance,
|
||||
memory,
|
||||
policy,
|
||||
host_callbacks,
|
||||
epoch_deadline,
|
||||
};
|
||||
|
||||
let not_implemented_builtins = evaluator.not_implemented_builtins()?;
|
||||
if !not_implemented_builtins.is_empty() {
|
||||
return Err(BurregoError::MissingRegoBuiltins(
|
||||
not_implemented_builtins.iter().join(", "),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(evaluator)
|
||||
}
|
||||
|
||||
fn setup(
|
||||
engine: Engine,
|
||||
module: Module,
|
||||
host_callbacks: HostCallbacks,
|
||||
) -> Result<EvaluatorStack> {
|
||||
let mut linker = Linker::<Option<StackHelper>>::new(&engine);
|
||||
|
||||
let opa_data_helper: Option<StackHelper> = None;
|
||||
let mut store = Store::new(&engine, opa_data_helper);
|
||||
|
||||
let memory_ty = MemoryType::new(5, None);
|
||||
let memory = Memory::new(&mut store, memory_ty)
|
||||
.map_err(|e| BurregoError::WasmEngineError(format!("cannot create memory: {}", e)))?;
|
||||
linker.define("env", "memory", memory).map_err(|e| {
|
||||
BurregoError::WasmEngineError(format!("linker cannot define memory: {}", e))
|
||||
})?;
|
||||
|
||||
opa_host_functions::add_to_linker(&mut linker)?;
|
||||
|
||||
let instance = linker.instantiate(&mut store, &module).map_err(|e| {
|
||||
BurregoError::WasmEngineError(format!("linker cannot create instance: {}", e))
|
||||
})?;
|
||||
|
||||
let stack_helper = StackHelper::new(
|
||||
&instance,
|
||||
&memory,
|
||||
&mut store,
|
||||
host_callbacks.opa_abort,
|
||||
host_callbacks.opa_println,
|
||||
)?;
|
||||
let policy = Policy::new(&instance, &mut store, &memory)?;
|
||||
_ = store.data_mut().insert(stack_helper);
|
||||
|
||||
Ok(EvaluatorStack {
|
||||
memory,
|
||||
store,
|
||||
instance,
|
||||
policy,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) -> Result<()> {
|
||||
let stack = Self::setup(
|
||||
self.engine.clone(),
|
||||
self.module.clone(),
|
||||
self.host_callbacks.clone(),
|
||||
)?;
|
||||
self.store = stack.store;
|
||||
self.instance = stack.instance;
|
||||
self.memory = stack.memory;
|
||||
self.policy = stack.policy;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn opa_abi_version(&mut self) -> Result<(i32, i32)> {
|
||||
let major = self
|
||||
.instance
|
||||
.get_global(&mut self.store, "opa_wasm_abi_version")
|
||||
.and_then(|g| g.get(&mut self.store).i32())
|
||||
.ok_or_else(|| {
|
||||
BurregoError::RegoWasmError("Cannot find OPA Wasm ABI major version".to_string())
|
||||
})?;
|
||||
let minor = self
|
||||
.instance
|
||||
.get_global(&mut self.store, "opa_wasm_abi_minor_version")
|
||||
.and_then(|g| g.get(&mut self.store).i32())
|
||||
.ok_or_else(|| {
|
||||
BurregoError::RegoWasmError("Cannot find OPA Wasm ABI minor version".to_string())
|
||||
})?;
|
||||
|
||||
Ok((major, minor))
|
||||
}
|
||||
|
||||
pub fn implemented_builtins() -> HashSet<String> {
|
||||
builtins::get_builtins()
|
||||
.keys()
|
||||
.map(|v| String::from(*v))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn not_implemented_builtins(&mut self) -> Result<HashSet<String>> {
|
||||
let used_builtins: HashSet<String> = self
|
||||
.policy
|
||||
.builtins(&mut self.store, &self.memory)?
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect();
|
||||
let supported_builtins: HashSet<String> = builtins::get_builtins()
|
||||
.keys()
|
||||
.map(|v| String::from(*v))
|
||||
.collect();
|
||||
Ok(used_builtins
|
||||
.difference(&supported_builtins)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn entrypoint_id(&mut self, entrypoint: &str) -> Result<i32> {
|
||||
let entrypoints = self.policy.entrypoints(&mut self.store, &self.memory)?;
|
||||
entrypoints
|
||||
.iter()
|
||||
.find(|(k, _v)| k == &entrypoint)
|
||||
.map(|(_k, v)| *v)
|
||||
.ok_or_else(|| {
|
||||
BurregoError::RegoWasmError(format!(
|
||||
"Cannot find the specified entrypoint {} inside of {:?}",
|
||||
entrypoint, entrypoints
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn entrypoints(&mut self) -> Result<HashMap<String, i32>> {
|
||||
set_epoch_deadline_and_call_guest!(self.epoch_deadline, self.store, {
|
||||
self.policy.entrypoints(&mut self.store, &self.memory)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn evaluate(
|
||||
&mut self,
|
||||
entrypoint_id: i32,
|
||||
input: &serde_json::Value,
|
||||
data: &serde_json::Value,
|
||||
) -> Result<serde_json::Value> {
|
||||
let entrypoints = self.policy.entrypoints(&mut self.store, &self.memory)?;
|
||||
entrypoints
|
||||
.iter()
|
||||
.find(|(_k, &v)| v == entrypoint_id)
|
||||
.ok_or_else(|| {
|
||||
BurregoError::RegoWasmError(format!(
|
||||
"Cannot find the specified entrypoint {} inside of {:?}",
|
||||
entrypoint_id, entrypoints
|
||||
))
|
||||
})?;
|
||||
|
||||
debug!(
|
||||
data = serde_json::to_string(&data)
|
||||
.expect("cannot convert data back to json")
|
||||
.as_str(),
|
||||
"setting policy data"
|
||||
);
|
||||
set_epoch_deadline_and_call_guest!(self.epoch_deadline, self.store, {
|
||||
self.policy.set_data(&mut self.store, &self.memory, data)
|
||||
})?;
|
||||
|
||||
debug!(
|
||||
input = serde_json::to_string(&input)
|
||||
.expect("cannot convert input back to JSON")
|
||||
.as_str(),
|
||||
"attempting evaluation"
|
||||
);
|
||||
set_epoch_deadline_and_call_guest!(self.epoch_deadline, self.store, {
|
||||
self.policy
|
||||
.evaluate(entrypoint_id, &mut self.store, &self.memory, input)
|
||||
})
|
||||
}
|
||||
}
|
||||
102
crates/burrego/src/evaluator_builder.rs
Normal file
102
crates/burrego/src/evaluator_builder.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use crate::errors::{BurregoError, Result};
|
||||
use std::path::{Path, PathBuf};
|
||||
use wasmtime::{Engine, Module};
|
||||
|
||||
use crate::{host_callbacks::HostCallbacks, Evaluator};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EvaluatorBuilder {
|
||||
policy_path: Option<PathBuf>,
|
||||
module: Option<Module>,
|
||||
engine: Option<Engine>,
|
||||
epoch_deadline: Option<u64>,
|
||||
host_callbacks: Option<HostCallbacks>,
|
||||
}
|
||||
|
||||
impl EvaluatorBuilder {
|
||||
#[must_use]
|
||||
pub fn policy_path(mut self, path: &Path) -> Self {
|
||||
self.policy_path = Some(path.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn module(mut self, module: Module) -> Self {
|
||||
self.module = Some(module);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn engine(mut self, engine: &Engine) -> Self {
|
||||
self.engine = Some(engine.clone());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn enable_epoch_interruptions(mut self, deadline: u64) -> Self {
|
||||
self.epoch_deadline = Some(deadline);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn host_callbacks(mut self, host_callbacks: HostCallbacks) -> Self {
|
||||
self.host_callbacks = Some(host_callbacks);
|
||||
self
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<()> {
|
||||
if self.policy_path.is_some() && self.module.is_some() {
|
||||
return Err(BurregoError::EvaluatorBuilderError(
|
||||
"policy_path and module cannot be set at the same time".to_string(),
|
||||
));
|
||||
}
|
||||
if self.policy_path.is_none() && self.module.is_none() {
|
||||
return Err(BurregoError::EvaluatorBuilderError(
|
||||
"Either policy_path or module must be set".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if self.host_callbacks.is_none() {
|
||||
return Err(BurregoError::EvaluatorBuilderError(
|
||||
"host_callbacks must be set".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build(&self) -> Result<Evaluator> {
|
||||
self.validate()?;
|
||||
|
||||
let engine = match &self.engine {
|
||||
Some(e) => e.clone(),
|
||||
None => {
|
||||
let mut config = wasmtime::Config::default();
|
||||
if self.epoch_deadline.is_some() {
|
||||
config.epoch_interruption(true);
|
||||
}
|
||||
Engine::new(&config).map_err(|e| {
|
||||
BurregoError::WasmEngineError(format!("cannot create wasmtime Engine: {:?}", e))
|
||||
})?
|
||||
}
|
||||
};
|
||||
|
||||
let module = match &self.module {
|
||||
Some(m) => m.clone(),
|
||||
None => Module::from_file(
|
||||
&engine,
|
||||
self.policy_path.clone().expect("policy_path should be set"),
|
||||
)
|
||||
.map_err(|e| {
|
||||
BurregoError::WasmEngineError(format!("cannot create wasmtime Module: {:?}", e))
|
||||
})?,
|
||||
};
|
||||
|
||||
let host_callbacks = self
|
||||
.host_callbacks
|
||||
.clone()
|
||||
.expect("host callbacks should be set");
|
||||
|
||||
Evaluator::from_engine_and_module(engine, module, host_callbacks, self.epoch_deadline)
|
||||
}
|
||||
}
|
||||
30
crates/burrego/src/host_callbacks.rs
Normal file
30
crates/burrego/src/host_callbacks.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
/// HostCallback is a type that references a pointer to a function
|
||||
/// that can be stored and then invoked by burrego when the Open
|
||||
/// Policy Agent Wasm target invokes certain Wasm imports.
|
||||
pub type HostCallback = fn(&str);
|
||||
|
||||
/// HostCallbacks defines a set of pluggable host implementations of
|
||||
/// OPA documented imports:
|
||||
/// https://www.openpolicyagent.org/docs/latest/wasm/#imports
|
||||
#[derive(Clone)]
|
||||
pub struct HostCallbacks {
|
||||
pub opa_abort: HostCallback,
|
||||
pub opa_println: HostCallback,
|
||||
}
|
||||
|
||||
impl Default for HostCallbacks {
|
||||
fn default() -> HostCallbacks {
|
||||
HostCallbacks {
|
||||
opa_abort: default_opa_abort,
|
||||
opa_println: default_opa_println,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_opa_abort(msg: &str) {
|
||||
eprintln!("OPA abort with message: {:?}", msg);
|
||||
}
|
||||
|
||||
fn default_opa_println(msg: &str) {
|
||||
println!("Message coming from the policy: {:?}", msg);
|
||||
}
|
||||
13
crates/burrego/src/lib.rs
Normal file
13
crates/burrego/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
mod builtins;
|
||||
pub mod errors;
|
||||
mod evaluator;
|
||||
mod evaluator_builder;
|
||||
pub mod host_callbacks;
|
||||
mod opa_host_functions;
|
||||
mod policy;
|
||||
mod stack_helper;
|
||||
|
||||
pub use builtins::get_builtins;
|
||||
pub use evaluator::Evaluator;
|
||||
pub use evaluator_builder::EvaluatorBuilder;
|
||||
pub use host_callbacks::HostCallbacks;
|
||||
370
crates/burrego/src/opa_host_functions.rs
Normal file
370
crates/burrego/src/opa_host_functions.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
use crate::errors::{BurregoError, Result};
|
||||
use tracing::{debug, error};
|
||||
use wasmtime::{AsContextMut, Caller, Linker};
|
||||
|
||||
use crate::builtins::BUILTINS_HELPER;
|
||||
use crate::stack_helper::StackHelper;
|
||||
|
||||
/// Add OPA host callbacks to the linker.
|
||||
/// The callbackes are the one listed at https://www.openpolicyagent.org/docs/latest/wasm/#imports
|
||||
pub(crate) fn add_to_linker(linker: &mut Linker<Option<StackHelper>>) -> Result<()> {
|
||||
register_opa_abort_func(linker)?;
|
||||
register_opa_println_func(linker)?;
|
||||
register_opa_builtin0_func(linker)?;
|
||||
register_opa_builtin1_func(linker)?;
|
||||
register_opa_builtin2_func(linker)?;
|
||||
register_opa_builtin3_func(linker)?;
|
||||
register_opa_builtin4_func(linker)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_opa_abort_func(
|
||||
linker: &mut Linker<Option<StackHelper>>,
|
||||
) -> Result<&mut Linker<Option<StackHelper>>> {
|
||||
linker
|
||||
.func_wrap(
|
||||
"env",
|
||||
"opa_abort",
|
||||
|mut caller: Caller<'_, Option<StackHelper>>, addr: i32| {
|
||||
let stack_helper = caller.data().as_ref().unwrap();
|
||||
let opa_abort_host_callback = stack_helper.opa_abort_host_callback;
|
||||
|
||||
let memory_export = caller.get_export("memory").ok_or_else(|| BurregoError::RegoWasmError("cannot find 'memory' export".to_string()))?;
|
||||
let memory = memory_export.into_memory().ok_or_else(|| BurregoError::RegoWasmError("'memory' export cannot be converted into a memory object".to_string()))?;
|
||||
|
||||
let msg = StackHelper::read_string(caller.as_context_mut(), &memory, addr)
|
||||
.map_or_else(
|
||||
|e| format!("cannot decode opa_abort message: {:?}", e),
|
||||
|data| String::from_utf8(data).unwrap_or_else(|e| format!("cannot decode opa_abort message: didn't read a valid string from memory - {:?}", e)),
|
||||
);
|
||||
opa_abort_host_callback(&msg);
|
||||
|
||||
Ok(())
|
||||
},
|
||||
).map_err(|e| BurregoError::BuiltinError{
|
||||
name: "opa_abort".to_string(),
|
||||
message: e.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn register_opa_println_func(
|
||||
linker: &mut Linker<Option<StackHelper>>,
|
||||
) -> Result<&mut Linker<Option<StackHelper>>> {
|
||||
linker.func_wrap(
|
||||
"env",
|
||||
"opa_println",
|
||||
|mut caller: Caller<'_, Option<StackHelper>>, addr: i32| {
|
||||
let stack_helper = caller.data().as_ref().unwrap();
|
||||
let opa_println_host_callback = stack_helper.opa_println_host_callback;
|
||||
|
||||
let memory_export = caller.get_export("memory").ok_or_else(|| BurregoError::RegoWasmError("cannot find 'memory' export".to_string()))?;
|
||||
let memory = memory_export.into_memory().ok_or_else(|| BurregoError::RegoWasmError("'memory' export cannot be converted into a memory object".to_string()))?;
|
||||
|
||||
let msg = StackHelper::read_string(caller.as_context_mut(), &memory, addr)
|
||||
.map_or_else(
|
||||
|e| format!("cannot decode opa_println message: {:?}", e),
|
||||
|data| String::from_utf8(data).unwrap_or_else(|e| format!("cannot decode opa_println message: didn't read a valid string from memory - {:?}", e)),
|
||||
);
|
||||
opa_println_host_callback(&msg);
|
||||
|
||||
Ok(())
|
||||
},
|
||||
).map_err(|e| BurregoError::BuiltinError{
|
||||
name: "opa_println".to_string(),
|
||||
message: e.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
/// env.opa_builtin0 (builtin_id, ctx) addr
|
||||
/// Called to dispatch the built-in function identified by the builtin_id.
|
||||
/// The ctx parameter reserved for future use. The result addr must refer to a value in the shared-memory buffer. The function accepts 0 arguments.
|
||||
fn register_opa_builtin0_func(
|
||||
linker: &mut Linker<Option<StackHelper>>,
|
||||
) -> Result<&mut Linker<Option<StackHelper>>> {
|
||||
linker.func_wrap(
|
||||
"env",
|
||||
"opa_builtin0",
|
||||
|mut caller: Caller<'_, Option<StackHelper>>, builtin_id: i32, _ctx: i32| {
|
||||
debug!(builtin_id, "opa_builtin0");
|
||||
|
||||
let stack_helper = caller.data().as_ref().unwrap();
|
||||
let opa_malloc_fn = stack_helper.opa_malloc_fn;
|
||||
let opa_json_parse_fn = stack_helper.opa_json_parse_fn;
|
||||
let builtin_name = stack_helper
|
||||
.builtins
|
||||
.get(&builtin_id)
|
||||
.ok_or_else(|| {
|
||||
error!(builtin_id, builtins =? stack_helper.builtins, "opa_builtin0: cannot find builtin");
|
||||
BurregoError::BuiltinNotImplementedError(format!("opa_builtin0: cannot find builtin {}", builtin_id))
|
||||
})?.clone();
|
||||
let args = vec![];
|
||||
|
||||
let memory_export = caller.get_export("memory").ok_or_else(|| BurregoError::RegoWasmError("cannot find 'memory' export".to_string()))?;
|
||||
let memory = memory_export.into_memory().ok_or_else(|| BurregoError::RegoWasmError("'memory' export cannot be converted into a memory object".to_string()))?;
|
||||
|
||||
let builtin_helper = BUILTINS_HELPER
|
||||
.read()
|
||||
.map_err(|e| BurregoError::RegoWasmError(format!("Cannot access global builtin helper: {:?}", e)))?;
|
||||
|
||||
let builtin_result = builtin_helper
|
||||
.invoke(&builtin_name, &args)?;
|
||||
|
||||
let addr = StackHelper::push_json(
|
||||
caller.as_context_mut(),
|
||||
&memory,
|
||||
opa_malloc_fn,
|
||||
opa_json_parse_fn,
|
||||
&builtin_result,
|
||||
)?;
|
||||
|
||||
Ok(addr)
|
||||
},
|
||||
).map_err(|e| BurregoError::BuiltinError{
|
||||
name: "opa_builtin0".to_string(),
|
||||
message: e.to_string()})
|
||||
}
|
||||
|
||||
/// env.opa_builtin1(builtin_id, ctx, _1) addr
|
||||
/// Same as previous except the function accepts 1 argument.
|
||||
fn register_opa_builtin1_func(
|
||||
linker: &mut Linker<Option<StackHelper>>,
|
||||
) -> Result<&mut Linker<Option<StackHelper>>> {
|
||||
linker.func_wrap(
|
||||
"env",
|
||||
"opa_builtin1",
|
||||
move |mut caller: Caller<'_, Option<StackHelper>>,
|
||||
builtin_id: i32,
|
||||
_ctx: i32,
|
||||
p1: i32| {
|
||||
debug!(builtin_id, p1, "opa_builtin1");
|
||||
|
||||
let stack_helper = caller.data().as_ref().unwrap();
|
||||
let opa_malloc_fn = stack_helper.opa_malloc_fn;
|
||||
let opa_json_parse_fn = stack_helper.opa_json_parse_fn;
|
||||
let opa_json_dump_fn = stack_helper.opa_json_dump_fn;
|
||||
let builtin_name = stack_helper
|
||||
.builtins
|
||||
.get(&builtin_id)
|
||||
.ok_or_else(|| {
|
||||
error!(builtin_id, builtins =? stack_helper.builtins, "opa_builtin0: cannot find builtin");
|
||||
BurregoError::BuiltinNotImplementedError(
|
||||
format!("opa_bunltin1: cannot find builtin {}", builtin_id))
|
||||
})?.clone();
|
||||
|
||||
let memory_export = caller.get_export("memory").ok_or_else(|| BurregoError::RegoWasmError("cannot find 'memory' export".to_string()))?;
|
||||
let memory = memory_export.into_memory().ok_or_else(|| BurregoError::RegoWasmError("'memory' export cannot be converted into a memory object".to_string()))?;
|
||||
|
||||
|
||||
let p1 =
|
||||
StackHelper::pull_json(caller.as_context_mut(), &memory, opa_json_dump_fn, p1)?;
|
||||
let args = vec![p1];
|
||||
|
||||
let builtin_helper = BUILTINS_HELPER
|
||||
.read()
|
||||
.map_err(|e| BurregoError::RegoWasmError(format!("Cannot access global builtin helper: {:?}", e)))?;
|
||||
|
||||
let builtin_result = builtin_helper
|
||||
.invoke(&builtin_name, &args)?;
|
||||
|
||||
let addr = StackHelper::push_json(
|
||||
caller.as_context_mut(),
|
||||
&memory,
|
||||
opa_malloc_fn,
|
||||
opa_json_parse_fn,
|
||||
&builtin_result,
|
||||
)?;
|
||||
|
||||
Ok(addr)
|
||||
},
|
||||
).map_err(|e| BurregoError::BuiltinError{
|
||||
name: "opa_bunltin1".to_string(),
|
||||
message: e.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// env.opa_builtin2 (builtin_id, ctx, _1, _2) addr
|
||||
/// Same as previous except the function accepts 2 arguments.
|
||||
fn register_opa_builtin2_func(
|
||||
linker: &mut Linker<Option<StackHelper>>,
|
||||
) -> Result<&mut Linker<Option<StackHelper>>> {
|
||||
linker.func_wrap(
|
||||
"env",
|
||||
"opa_builtin2",
|
||||
move |mut caller: Caller<'_, Option<StackHelper>>,
|
||||
builtin_id: i32,
|
||||
_ctx: i32,
|
||||
p1: i32,
|
||||
p2: i32| {
|
||||
debug!(builtin_id, p1, p2, "opa_builtin2");
|
||||
|
||||
let stack_helper = caller.data().as_ref().unwrap();
|
||||
let opa_malloc_fn = stack_helper.opa_malloc_fn;
|
||||
let opa_json_parse_fn = stack_helper.opa_json_parse_fn;
|
||||
let opa_json_dump_fn = stack_helper.opa_json_dump_fn;
|
||||
let builtin_name = stack_helper
|
||||
.builtins
|
||||
.get(&builtin_id)
|
||||
.ok_or_else(|| {
|
||||
error!(builtin_id, builtins =? stack_helper.builtins, "opa_builtin0: cannot find builtin");
|
||||
BurregoError::BuiltinNotImplementedError(format!("opa_builtin2: cannot find builtin {}", builtin_id))
|
||||
})?.clone();
|
||||
|
||||
let memory_export = caller.get_export("memory").ok_or_else(|| BurregoError::RegoWasmError("cannot find 'memory' export".to_string()))?;
|
||||
let memory = memory_export.into_memory().ok_or_else(|| BurregoError::RegoWasmError("'memory' export cannot be converted into a memory object".to_string()))?;
|
||||
|
||||
let p1 =
|
||||
StackHelper::pull_json(caller.as_context_mut(), &memory, opa_json_dump_fn, p1)?;
|
||||
let p2 =
|
||||
StackHelper::pull_json(caller.as_context_mut(), &memory, opa_json_dump_fn, p2)?;
|
||||
|
||||
let args = vec![p1, p2];
|
||||
|
||||
let builtin_helper = BUILTINS_HELPER
|
||||
.read()
|
||||
.map_err(|e| BurregoError::RegoWasmError(format!("Cannot access global builtin helper: {:?}", e)))?;
|
||||
|
||||
let builtin_result = builtin_helper.invoke(&builtin_name, &args)?;
|
||||
|
||||
let addr = StackHelper::push_json(
|
||||
caller.as_context_mut(),
|
||||
&memory,
|
||||
opa_malloc_fn,
|
||||
opa_json_parse_fn,
|
||||
&builtin_result,
|
||||
)?;
|
||||
|
||||
Ok(addr)
|
||||
},
|
||||
).map_err(|e| BurregoError::BuiltinError{
|
||||
name: "opa_builtin2".to_string(),
|
||||
message: e.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
/// env.opa_builtin3 (builtin_id, ctx, _1, _2, _3) addr
|
||||
/// Same as previous except the function accepts 3 arguments.
|
||||
fn register_opa_builtin3_func(
|
||||
linker: &mut Linker<Option<StackHelper>>,
|
||||
) -> Result<&mut Linker<Option<StackHelper>>> {
|
||||
linker.func_wrap(
|
||||
"env",
|
||||
"opa_builtin3",
|
||||
move |mut caller: Caller<'_, Option<StackHelper>>,
|
||||
builtin_id: i32,
|
||||
_ctx: i32,
|
||||
p1: i32,
|
||||
p2: i32,
|
||||
p3: i32| {
|
||||
debug!(builtin_id, p1, p2, p3, "opa_builtin3");
|
||||
|
||||
let stack_helper = caller.data().as_ref().unwrap();
|
||||
let opa_malloc_fn = stack_helper.opa_malloc_fn;
|
||||
let opa_json_parse_fn = stack_helper.opa_json_parse_fn;
|
||||
let opa_json_dump_fn = stack_helper.opa_json_dump_fn;
|
||||
let builtin_name = stack_helper
|
||||
.builtins
|
||||
.get(&builtin_id)
|
||||
.ok_or_else(|| {
|
||||
error!(builtin_id, builtins =? stack_helper.builtins, "opa_builtin0: cannot find builtin");
|
||||
BurregoError::BuiltinNotImplementedError(format!("opa_builtin3: cannot find builtin {}", builtin_id))
|
||||
})?.clone();
|
||||
|
||||
let memory_export = caller.get_export("memory").ok_or_else(|| BurregoError::RegoWasmError("cannot find 'memory' export".to_string()))?;
|
||||
let memory = memory_export.into_memory().ok_or_else(|| BurregoError::RegoWasmError("'memory' export cannot be converted into a memory object".to_string()))?;
|
||||
|
||||
let p1 =
|
||||
StackHelper::pull_json(caller.as_context_mut(), &memory, opa_json_dump_fn, p1)?;
|
||||
let p2 =
|
||||
StackHelper::pull_json(caller.as_context_mut(), &memory, opa_json_dump_fn, p2)?;
|
||||
let p3 =
|
||||
StackHelper::pull_json(caller.as_context_mut(), &memory, opa_json_dump_fn, p3)?;
|
||||
|
||||
let args = vec![p1, p2, p3];
|
||||
|
||||
let builtin_helper = BUILTINS_HELPER
|
||||
.read()
|
||||
.map_err(|e| BurregoError::RegoWasmError(format!("Cannot access global builtin helper: {:?}", e)))?;
|
||||
|
||||
let builtin_result = builtin_helper.invoke(&builtin_name, &args)?;
|
||||
|
||||
let addr = StackHelper::push_json(
|
||||
caller.as_context_mut(),
|
||||
&memory,
|
||||
opa_malloc_fn,
|
||||
opa_json_parse_fn,
|
||||
&builtin_result,
|
||||
)?;
|
||||
|
||||
Ok(addr)
|
||||
},
|
||||
).map_err(|e| BurregoError::BuiltinError{
|
||||
name: "opa_builtin3".to_string(),
|
||||
message: e.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// env.opa_builtin4 (builtin_id, ctx, _1, _2, _3, _4) addr
|
||||
/// Same as previous except the function accepts 4 arguments.
|
||||
fn register_opa_builtin4_func(
|
||||
linker: &mut Linker<Option<StackHelper>>,
|
||||
) -> Result<&mut Linker<Option<StackHelper>>> {
|
||||
linker.func_wrap(
|
||||
"env",
|
||||
"opa_builtin4",
|
||||
move |mut caller: Caller<'_, Option<StackHelper>>,
|
||||
builtin_id: i32,
|
||||
_ctx: i32,
|
||||
p1: i32,
|
||||
p2: i32,
|
||||
p3: i32,
|
||||
p4: i32| {
|
||||
debug!(builtin_id, p1, p2, p3, p4, "opa_builtin4");
|
||||
|
||||
let stack_helper = caller.data().as_ref().unwrap();
|
||||
let opa_malloc_fn = stack_helper.opa_malloc_fn;
|
||||
let opa_json_parse_fn = stack_helper.opa_json_parse_fn;
|
||||
let opa_json_dump_fn = stack_helper.opa_json_dump_fn;
|
||||
let builtin_name = stack_helper
|
||||
.builtins
|
||||
.get(&builtin_id)
|
||||
.ok_or_else(|| {
|
||||
error!(builtin_id, builtins =? stack_helper.builtins, "opa_builtin0: cannot find builtin");
|
||||
BurregoError::BuiltinNotImplementedError(format!("opa_builtin4: cannot find builtin {}", builtin_id))
|
||||
})?.clone();
|
||||
|
||||
let memory_export = caller.get_export("memory").ok_or_else(|| BurregoError::RegoWasmError("cannot find 'memory' export".to_string()))?;
|
||||
let memory = memory_export.into_memory().ok_or_else(|| BurregoError::RegoWasmError("'memory' export cannot be converted into a memory object".to_string()))?;
|
||||
|
||||
let p1 =
|
||||
StackHelper::pull_json(caller.as_context_mut(), &memory, opa_json_dump_fn, p1)?;
|
||||
let p2 =
|
||||
StackHelper::pull_json(caller.as_context_mut(), &memory, opa_json_dump_fn, p2)?;
|
||||
let p3 =
|
||||
StackHelper::pull_json(caller.as_context_mut(), &memory, opa_json_dump_fn, p3)?;
|
||||
let p4 =
|
||||
StackHelper::pull_json(caller.as_context_mut(), &memory, opa_json_dump_fn, p4)?;
|
||||
|
||||
let args = vec![p1, p2, p3, p4];
|
||||
|
||||
let builtin_helper = BUILTINS_HELPER
|
||||
.read()
|
||||
.map_err(|e| BurregoError::RegoWasmError(format!("Cannot access global builtin helper: {:?}", e)))?;
|
||||
|
||||
let builtin_result = builtin_helper.invoke(&builtin_name, &args)?;
|
||||
|
||||
let addr = StackHelper::push_json(
|
||||
caller.as_context_mut(),
|
||||
&memory,
|
||||
opa_malloc_fn,
|
||||
opa_json_parse_fn,
|
||||
&builtin_result,
|
||||
)?;
|
||||
|
||||
Ok(addr)
|
||||
},
|
||||
).map_err(|e| BurregoError::BuiltinError{
|
||||
name: "opa_builtin4".to_string(),
|
||||
message: e.to_string(),
|
||||
})
|
||||
}
|
||||
306
crates/burrego/src/policy.rs
Normal file
306
crates/burrego/src/policy.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
use crate::errors::{BurregoError, Result};
|
||||
use crate::stack_helper::StackHelper;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use wasmtime::{AsContextMut, Instance, Memory, TypedFunc};
|
||||
|
||||
/// Handle errors returned when calling a wasmtime function
|
||||
/// The macro looks into the error type and, when an epoch interruption
|
||||
/// happens, maps the error to BurregoError::ExecutionDeadlineExceeded
|
||||
macro_rules! map_call_error {
|
||||
($err:expr, $msg:expr) => {{
|
||||
if let Some(trap) = $err.downcast_ref::<wasmtime::Trap>() {
|
||||
if matches!(trap, wasmtime::Trap::Interrupt) {
|
||||
BurregoError::ExecutionDeadlineExceeded
|
||||
} else {
|
||||
BurregoError::WasmEngineError(format!("{}: {:?}", $msg, $err))
|
||||
}
|
||||
} else {
|
||||
BurregoError::WasmEngineError(format!("{}: {:?}", $msg, $err))
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) struct Policy {
|
||||
builtins_fn: TypedFunc<(), i32>,
|
||||
entrypoints_fn: TypedFunc<(), i32>,
|
||||
opa_heap_ptr_get_fn: TypedFunc<(), i32>,
|
||||
opa_heap_ptr_set_fn: TypedFunc<i32, ()>,
|
||||
opa_eval_ctx_new_fn: TypedFunc<(), i32>,
|
||||
opa_eval_ctx_set_input_fn: TypedFunc<(i32, i32), ()>,
|
||||
opa_eval_ctx_set_data_fn: TypedFunc<(i32, i32), ()>,
|
||||
opa_eval_ctx_set_entrypoint_fn: TypedFunc<(i32, i32), ()>,
|
||||
opa_eval_ctx_get_result_fn: TypedFunc<i32, i32>,
|
||||
opa_json_dump_fn: TypedFunc<i32, i32>,
|
||||
opa_malloc_fn: TypedFunc<i32, i32>,
|
||||
opa_json_parse_fn: TypedFunc<(i32, i32), i32>,
|
||||
eval_fn: TypedFunc<i32, i32>,
|
||||
|
||||
data_addr: i32,
|
||||
base_heap_ptr: i32,
|
||||
data_heap_ptr: i32,
|
||||
}
|
||||
|
||||
impl Policy {
|
||||
pub fn new(
|
||||
instance: &Instance,
|
||||
mut store: impl AsContextMut,
|
||||
memory: &Memory,
|
||||
) -> Result<Policy> {
|
||||
let mut policy = Policy {
|
||||
builtins_fn: instance
|
||||
.get_typed_func::<(), i32>(store.as_context_mut(), "builtins")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!("cannot get builtins function: {:?}", e))
|
||||
})?,
|
||||
entrypoints_fn: instance
|
||||
.get_typed_func::<(), i32>(store.as_context_mut(), "entrypoints")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!("cannot get entrypoints function: {:?}", e))
|
||||
})?,
|
||||
opa_heap_ptr_get_fn: instance
|
||||
.get_typed_func::<(), i32>(store.as_context_mut(), "opa_heap_ptr_get")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!(
|
||||
"cannot get opa_heap_ptr_get function: {:?}",
|
||||
e
|
||||
))
|
||||
})?,
|
||||
opa_heap_ptr_set_fn: instance
|
||||
.get_typed_func::<i32, ()>(store.as_context_mut(), "opa_heap_ptr_set")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!(
|
||||
"cannot get opa_heap_ptr_set function: {:?}",
|
||||
e
|
||||
))
|
||||
})?,
|
||||
opa_eval_ctx_new_fn: instance
|
||||
.get_typed_func::<(), i32>(store.as_context_mut(), "opa_eval_ctx_new")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!(
|
||||
"cannot get opa_eval_ctx_new function: {:?}",
|
||||
e
|
||||
))
|
||||
})?,
|
||||
opa_eval_ctx_set_input_fn: instance
|
||||
.get_typed_func::<(i32, i32), ()>(store.as_context_mut(), "opa_eval_ctx_set_input")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!(
|
||||
"cannot get opa_eval_ctx_set_input function: {:?}",
|
||||
e
|
||||
))
|
||||
})?,
|
||||
opa_eval_ctx_set_data_fn: instance
|
||||
.get_typed_func::<(i32, i32), ()>(store.as_context_mut(), "opa_eval_ctx_set_data")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!(
|
||||
"cannot get opa_eval_ctx_set_data function: {:?}",
|
||||
e
|
||||
))
|
||||
})?,
|
||||
opa_eval_ctx_set_entrypoint_fn: instance
|
||||
.get_typed_func::<(i32, i32), ()>(
|
||||
store.as_context_mut(),
|
||||
"opa_eval_ctx_set_entrypoint",
|
||||
)
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!(
|
||||
"cannot get opa_eval_ctx_set_entrypoint function: {:?}",
|
||||
e
|
||||
))
|
||||
})?,
|
||||
opa_eval_ctx_get_result_fn: instance
|
||||
.get_typed_func::<i32, i32>(store.as_context_mut(), "opa_eval_ctx_get_result")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!(
|
||||
"cannot get opa_eval_ctx_get_result function: {:?}",
|
||||
e
|
||||
))
|
||||
})?,
|
||||
opa_json_dump_fn: instance
|
||||
.get_typed_func::<i32, i32>(store.as_context_mut(), "opa_json_dump")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!(
|
||||
"cannot get opa_json_dump function: {:?}",
|
||||
e
|
||||
))
|
||||
})?,
|
||||
opa_malloc_fn: instance
|
||||
.get_typed_func::<i32, i32>(store.as_context_mut(), "opa_malloc")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!("cannot get opa_malloc function: {:?}", e))
|
||||
})?,
|
||||
opa_json_parse_fn: instance
|
||||
.get_typed_func::<(i32, i32), i32>(store.as_context_mut(), "opa_json_parse")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!(
|
||||
"cannot get opa_json_parse function: {:?}",
|
||||
e
|
||||
))
|
||||
})?,
|
||||
eval_fn: instance
|
||||
.get_typed_func::<i32, i32>(store.as_context_mut(), "eval")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!("cannot get eval function: {:?}", e))
|
||||
})?,
|
||||
data_addr: 0,
|
||||
base_heap_ptr: 0,
|
||||
data_heap_ptr: 0,
|
||||
};
|
||||
|
||||
// init data
|
||||
let initial_data = json!({});
|
||||
policy.data_addr = StackHelper::push_json(
|
||||
store.as_context_mut(),
|
||||
memory,
|
||||
policy.opa_malloc_fn,
|
||||
policy.opa_json_parse_fn,
|
||||
&initial_data,
|
||||
)?;
|
||||
|
||||
policy.base_heap_ptr = policy
|
||||
.opa_heap_ptr_get_fn
|
||||
.call(store.as_context_mut(), ())
|
||||
.map_err(|e| map_call_error!(e, "error invoking opa_heap_ptr_get function"))?;
|
||||
policy.data_heap_ptr = policy.base_heap_ptr;
|
||||
|
||||
Ok(policy)
|
||||
}
|
||||
|
||||
pub fn builtins(
|
||||
&self,
|
||||
mut store: impl AsContextMut,
|
||||
memory: &Memory,
|
||||
) -> Result<HashMap<String, i32>> {
|
||||
let addr = self
|
||||
.builtins_fn
|
||||
.call(store.as_context_mut(), ())
|
||||
.map_err(|e| map_call_error!(e, "error invoking builtins function"))?;
|
||||
|
||||
let builtins: HashMap<String, i32> =
|
||||
StackHelper::pull_json(store, memory, self.opa_json_dump_fn, addr)?
|
||||
.as_object()
|
||||
.ok_or_else(|| {
|
||||
BurregoError::RegoWasmError(
|
||||
"OPA builtins didn't return a dictionary".to_string(),
|
||||
)
|
||||
})?
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
let id = v.as_i64().unwrap() as i32;
|
||||
let builtin = String::from(k.as_str());
|
||||
(builtin, id)
|
||||
})
|
||||
.collect();
|
||||
Ok(builtins)
|
||||
}
|
||||
|
||||
pub fn entrypoints(
|
||||
&self,
|
||||
mut store: impl AsContextMut,
|
||||
memory: &Memory,
|
||||
) -> Result<HashMap<String, i32>> {
|
||||
let addr = self
|
||||
.entrypoints_fn
|
||||
.call(store.as_context_mut(), ())
|
||||
.map_err(|e| map_call_error!(e, "error invoking entrypoints function"))?;
|
||||
let res =
|
||||
StackHelper::pull_json(store.as_context_mut(), memory, self.opa_json_dump_fn, addr)?
|
||||
.as_object()
|
||||
.ok_or_else(|| {
|
||||
BurregoError::RegoWasmError(
|
||||
"OPA entrypoints didn't return a dictionary".to_string(),
|
||||
)
|
||||
})?
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
let id = v.as_i64().unwrap();
|
||||
let entrypoint = String::from(k.as_str());
|
||||
(entrypoint, i32::try_from(id).unwrap())
|
||||
})
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn set_data(
|
||||
&mut self,
|
||||
mut store: impl AsContextMut,
|
||||
memory: &Memory,
|
||||
data: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
self.opa_heap_ptr_set_fn
|
||||
.call(store.as_context_mut(), self.base_heap_ptr)
|
||||
.map_err(|e| map_call_error!(e, "error invoking opa_heap_ptr_set function"))?;
|
||||
self.data_addr = StackHelper::push_json(
|
||||
store.as_context_mut(),
|
||||
memory,
|
||||
self.opa_malloc_fn,
|
||||
self.opa_json_parse_fn,
|
||||
data,
|
||||
)?;
|
||||
self.data_heap_ptr = self
|
||||
.opa_heap_ptr_get_fn
|
||||
.call(store.as_context_mut(), ())
|
||||
.map_err(|e| map_call_error!(e, "error invoking opa_heap_ptr_get function"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn evaluate(
|
||||
&self,
|
||||
entrypoint_id: i32,
|
||||
mut store: impl AsContextMut,
|
||||
memory: &Memory,
|
||||
input: &serde_json::Value,
|
||||
) -> Result<serde_json::Value> {
|
||||
// Reset the heap pointer before each evaluation
|
||||
self.opa_heap_ptr_set_fn
|
||||
.call(store.as_context_mut(), self.data_heap_ptr)
|
||||
.map_err(|e| map_call_error!(e, "error invoking opa_heap_ptr_set function"))?;
|
||||
|
||||
// Load the input data
|
||||
let input_addr = StackHelper::push_json(
|
||||
store.as_context_mut(),
|
||||
memory,
|
||||
self.opa_malloc_fn,
|
||||
self.opa_json_parse_fn,
|
||||
input,
|
||||
)?;
|
||||
|
||||
// Setup the evaluation context
|
||||
let ctx_addr = self
|
||||
.opa_eval_ctx_new_fn
|
||||
.call(store.as_context_mut(), ())
|
||||
.map_err(|e| map_call_error!(e, "error invoking opa_eval_ctx_new function"))?;
|
||||
self.opa_eval_ctx_set_input_fn
|
||||
.call(store.as_context_mut(), (ctx_addr, input_addr))
|
||||
.map_err(|e| map_call_error!(e, "error invoking opa_eval_ctx_set_input function"))?;
|
||||
self.opa_eval_ctx_set_data_fn
|
||||
.call(store.as_context_mut(), (ctx_addr, self.data_addr))
|
||||
.map_err(|e| map_call_error!(e, "error invoking opa_eval_ctx_set_data function"))?;
|
||||
self.opa_eval_ctx_set_entrypoint_fn
|
||||
.call(store.as_context_mut(), (ctx_addr, entrypoint_id))
|
||||
.map_err(|e| {
|
||||
map_call_error!(e, "error invoking opa_eval_ctx_set_entrypoint function")
|
||||
})?;
|
||||
|
||||
// Perform evaluation
|
||||
self.eval_fn
|
||||
.call(store.as_context_mut(), ctx_addr)
|
||||
.map_err(|e| map_call_error!(e, "error invoking opa_eval function"))?;
|
||||
|
||||
// Retrieve the result
|
||||
let res_addr = self
|
||||
.opa_eval_ctx_get_result_fn
|
||||
.call(store.as_context_mut(), ctx_addr)
|
||||
.map_err(|e| map_call_error!(e, "error invoking opa_eval_ctx_get_result function"))?;
|
||||
|
||||
StackHelper::pull_json(
|
||||
store.as_context_mut(),
|
||||
memory,
|
||||
self.opa_json_dump_fn,
|
||||
res_addr,
|
||||
)
|
||||
}
|
||||
}
|
||||
193
crates/burrego/src/stack_helper.rs
Normal file
193
crates/burrego/src/stack_helper.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
use crate::errors::{BurregoError, Result};
|
||||
use crate::host_callbacks;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use wasmtime::{AsContext, AsContextMut, Instance, Memory, TypedFunc};
|
||||
|
||||
/// StackHelper provides a set of helper methods to share data
|
||||
/// between the host and the Rego Wasm guest
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct StackHelper {
|
||||
pub(crate) opa_json_dump_fn: TypedFunc<i32, i32>,
|
||||
pub(crate) opa_malloc_fn: TypedFunc<i32, i32>,
|
||||
pub(crate) opa_json_parse_fn: TypedFunc<(i32, i32), i32>,
|
||||
|
||||
pub(crate) opa_abort_host_callback: host_callbacks::HostCallback,
|
||||
pub(crate) opa_println_host_callback: host_callbacks::HostCallback,
|
||||
|
||||
pub(crate) builtins: HashMap<i32, String>,
|
||||
}
|
||||
|
||||
impl StackHelper {
|
||||
pub fn new(
|
||||
instance: &Instance,
|
||||
memory: &Memory,
|
||||
mut store: impl AsContextMut,
|
||||
opa_abort_host_callback: host_callbacks::HostCallback,
|
||||
opa_println_host_callback: host_callbacks::HostCallback,
|
||||
) -> Result<StackHelper> {
|
||||
let opa_json_dump_fn = instance
|
||||
.get_typed_func::<i32, i32>(store.as_context_mut(), "opa_json_dump")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!("cannot access opa_json_dump fuction: {:?}", e))
|
||||
})?;
|
||||
let opa_malloc_fn = instance
|
||||
.get_typed_func::<i32, i32>(store.as_context_mut(), "opa_malloc")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!("Cannot access opa_malloc fuction: {:?}", e))
|
||||
})?;
|
||||
let opa_json_parse_fn = instance
|
||||
.get_typed_func::<(i32, i32), i32>(store.as_context_mut(), "opa_json_parse")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!(
|
||||
"Cannot access opa_json_parse fuction: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
let builtins_fn = instance
|
||||
.get_typed_func::<(), i32>(store.as_context_mut(), "builtins")
|
||||
.map_err(|e| {
|
||||
BurregoError::RegoWasmError(format!("cannot access builtins function: {:?}", e))
|
||||
})?;
|
||||
let addr = builtins_fn.call(store.as_context_mut(), ()).map_err(|e| {
|
||||
BurregoError::WasmEngineError(format!("cannot invoke builtins function: {:?}", e))
|
||||
})?;
|
||||
|
||||
let builtins: HashMap<i32, String> =
|
||||
StackHelper::pull_json(store, memory, opa_json_dump_fn, addr)?
|
||||
.as_object()
|
||||
.ok_or_else(|| {
|
||||
BurregoError::RegoWasmError(
|
||||
"OPA builtins didn't return a dictionary".to_string(),
|
||||
)
|
||||
})?
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
let id = v.as_i64().unwrap() as i32;
|
||||
let builtin = String::from(k.as_str());
|
||||
(id, builtin)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(StackHelper {
|
||||
opa_json_dump_fn,
|
||||
opa_malloc_fn,
|
||||
opa_json_parse_fn,
|
||||
builtins,
|
||||
opa_abort_host_callback,
|
||||
opa_println_host_callback,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read a string from the Wasm guest into the host
|
||||
/// # Arguments
|
||||
/// * `store` - the Store associated with the Wasm instance
|
||||
/// * `memory` - the Wasm linear memory used by the Wasm Instance
|
||||
/// * `addr` - address inside of the Wasm linear memory where the value is stored
|
||||
/// # Returns
|
||||
/// * The data read
|
||||
pub fn read_string(store: impl AsContext, memory: &Memory, addr: i32) -> Result<Vec<u8>> {
|
||||
let mut buffer: [u8; 1] = [0u8];
|
||||
let mut data: Vec<u8> = vec![];
|
||||
let mut raw_addr = addr;
|
||||
|
||||
loop {
|
||||
memory
|
||||
.read(&store, raw_addr.try_into().unwrap(), &mut buffer)
|
||||
.map_err(|e| {
|
||||
BurregoError::WasmEngineError(format!("cannot read from memory: {:?}", e))
|
||||
})?;
|
||||
if buffer[0] == 0 {
|
||||
break;
|
||||
}
|
||||
data.push(buffer[0]);
|
||||
raw_addr += 1;
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Pull a JSON data from the Wasm guest into the host
|
||||
/// # Arguments
|
||||
/// * `store` - the Store associated with the Wasm instance
|
||||
/// * `memory` - the Wasm linear memory used by the Wasm Instance
|
||||
/// * `opa_json_dump_fn` - the `opa_json_dump` function exported by the wasm guest
|
||||
/// * `addr` - address inside of the Wasm linear memory where the value is stored
|
||||
/// # Returns
|
||||
/// * The JSON data read
|
||||
pub fn pull_json(
|
||||
mut store: impl AsContextMut,
|
||||
memory: &Memory,
|
||||
opa_json_dump_fn: TypedFunc<i32, i32>,
|
||||
addr: i32,
|
||||
) -> Result<serde_json::Value> {
|
||||
let raw_addr = opa_json_dump_fn
|
||||
.call(store.as_context_mut(), addr)
|
||||
.map_err(|e| {
|
||||
BurregoError::WasmEngineError(format!(
|
||||
"cannot invoke opa_json_dump function: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
let data = StackHelper::read_string(store, memory, raw_addr)?;
|
||||
|
||||
serde_json::from_slice(&data).map_err(|e| {
|
||||
BurregoError::JSONError(format!(
|
||||
"cannot convert data read from memory into utf8 String: {:?}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Push a JSON data from the host into the Wasm guest
|
||||
/// # Arguments
|
||||
/// * `store` - the Store associated with the Wasm instance
|
||||
/// * `memory` - the Wasm linear memory used by the Wasm Instance
|
||||
/// * `opa_malloc_fn` - the `opa_malloc` function exported by the wasm guest
|
||||
/// * `opa_json_parse_fn` - the `opa_json_parse` function exported by the wasm guest
|
||||
/// * `value` - the JSON data to push into the Wasm guest
|
||||
/// # Returns
|
||||
/// * Address inside of the Wasm linear memory where the value has been stored
|
||||
pub fn push_json(
|
||||
mut store: impl AsContextMut,
|
||||
memory: &Memory,
|
||||
opa_malloc_fn: TypedFunc<i32, i32>,
|
||||
opa_json_parse_fn: TypedFunc<(i32, i32), i32>,
|
||||
value: &serde_json::Value,
|
||||
) -> Result<i32> {
|
||||
let data = serde_json::to_vec(&value).map_err(|e| {
|
||||
BurregoError::JSONError(format!("push_json: cannot convert value to json: {:?}", e))
|
||||
})?;
|
||||
|
||||
let data_size: i32 = data.len().try_into().map_err(|e| {
|
||||
BurregoError::JSONError(format!("push_json: cannot convert size to json: {:?}", e))
|
||||
})?;
|
||||
|
||||
// allocate memory to fit the value
|
||||
let raw_addr = opa_malloc_fn
|
||||
.call(store.as_context_mut(), data_size)
|
||||
.map_err(|e| {
|
||||
BurregoError::WasmEngineError(format!(
|
||||
"push_json: cannot invoke opa_malloc function: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
memory
|
||||
.write(store.as_context_mut(), raw_addr.try_into().unwrap(), &data)
|
||||
.map_err(|e| {
|
||||
BurregoError::WasmEngineError(format!("push_json: cannot write to memory: {:?}", e))
|
||||
})?;
|
||||
|
||||
match opa_json_parse_fn.call(store.as_context_mut(), (raw_addr, data_size)) {
|
||||
Ok(0) => Err(BurregoError::RegoWasmError(
|
||||
"Failed to load json in memory".to_string(),
|
||||
)),
|
||||
Ok(addr) => Ok(addr),
|
||||
Err(e) => Err(BurregoError::RegoWasmError(format!(
|
||||
"Cannot get memory address: {:?}",
|
||||
e
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
10
crates/burrego/test_data/gatekeeper/Makefile
Normal file
10
crates/burrego/test_data/gatekeeper/Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
test: policy.wasm
|
||||
bats e2e.bats
|
||||
|
||||
policy.wasm: policy.rego
|
||||
opa build -t wasm -e policy/violation -o policy.tar.gz policy.rego
|
||||
tar -xf policy.tar.gz /policy.wasm
|
||||
rm policy.tar.gz
|
||||
|
||||
clean:
|
||||
rm -f *.wasm *.tar.gz
|
||||
21
crates/burrego/test_data/gatekeeper/e2e.bats
Normal file
21
crates/burrego/test_data/gatekeeper/e2e.bats
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
@test "[accept in namespace]: valid namespace" {
|
||||
run cargo run --example cli -- -v eval policy.wasm --input-path request-valid.json
|
||||
# this prints the output when one the checks below fails
|
||||
echo "output = ${output}"
|
||||
|
||||
# request accepted
|
||||
[ "$status" -eq 0 ]
|
||||
[ $(expr "$output" : '.*"result":.*\[\]') -ne 0 ]
|
||||
}
|
||||
|
||||
@test "[accept in namespace]: not valid namespace" {
|
||||
run cargo run --example cli -- -v eval policy.wasm --input-path request-not-valid.json
|
||||
# this prints the output when one the checks below fails
|
||||
echo "output = ${output}"
|
||||
|
||||
# request accepted
|
||||
[ "$status" -eq 0 ]
|
||||
[ $(expr "$output" : '.*"msg": "object created under an invalid namespace kube-system; allowed namespaces are \[default test\]"') -ne 0 ]
|
||||
}
|
||||
8
crates/burrego/test_data/gatekeeper/policy.rego
Normal file
8
crates/burrego/test_data/gatekeeper/policy.rego
Normal file
@@ -0,0 +1,8 @@
|
||||
package policy
|
||||
|
||||
violation[{"msg": msg}] {
|
||||
object_namespace := input.review.object.metadata.namespace
|
||||
satisfied := [allowed_namespace | namespace = input.parameters.allowed_namespaces[_]; allowed_namespace = object_namespace == namespace]
|
||||
not any(satisfied)
|
||||
msg := sprintf("object created under an invalid namespace %s; allowed namespaces are %v", [object_namespace, input.parameters.allowed_namespaces])
|
||||
}
|
||||
26
crates/burrego/test_data/gatekeeper/request-not-valid.json
Normal file
26
crates/burrego/test_data/gatekeeper/request-not-valid.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"parameters": {
|
||||
"allowed_namespaces": [
|
||||
"default",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
"review": {
|
||||
"uid": "1299d386-525b-4032-98ae-1949f69f9cfc",
|
||||
"kind": {
|
||||
"group": "networking.k8s.io",
|
||||
"kind": "Ingress",
|
||||
"version": "v1"
|
||||
},
|
||||
"object": {
|
||||
"apiVersion": "networking.k8s.io/v1",
|
||||
"kind": "Ingress",
|
||||
"metadata": {
|
||||
"name": "ingress-wildcard-host",
|
||||
"namespace": "kube-system"
|
||||
},
|
||||
"spec": {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
crates/burrego/test_data/gatekeeper/request-valid.json
Normal file
26
crates/burrego/test_data/gatekeeper/request-valid.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"parameters": {
|
||||
"allowed_namespaces": [
|
||||
"default",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
"review": {
|
||||
"uid": "1299d386-525b-4032-98ae-1949f69f9cfc",
|
||||
"kind": {
|
||||
"group": "networking.k8s.io",
|
||||
"kind": "Ingress",
|
||||
"version": "v1"
|
||||
},
|
||||
"object": {
|
||||
"apiVersion": "networking.k8s.io/v1",
|
||||
"kind": "Ingress",
|
||||
"metadata": {
|
||||
"name": "ingress-wildcard-host",
|
||||
"namespace": "default"
|
||||
},
|
||||
"spec": {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
crates/burrego/test_data/trace/Makefile
Normal file
10
crates/burrego/test_data/trace/Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
test: policy.wasm
|
||||
bats e2e.bats
|
||||
|
||||
policy.wasm: policy.rego
|
||||
opa build -t wasm -e policy/main -o policy.tar.gz policy.rego
|
||||
tar -xf policy.tar.gz /policy.wasm
|
||||
rm policy.tar.gz
|
||||
|
||||
clean:
|
||||
rm -f *.wasm *.tar.gz
|
||||
23
crates/burrego/test_data/trace/e2e.bats
Normal file
23
crates/burrego/test_data/trace/e2e.bats
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
@test "input message is not valid" {
|
||||
run cargo run --example cli -- -v eval policy.wasm -i '{ "message": "mondo" }'
|
||||
# this prints the output when one the checks below fails
|
||||
echo "output = ${output}"
|
||||
|
||||
# request rejected
|
||||
[ "$status" -eq 0 ]
|
||||
[ $(expr "$output" : '.*"result":.*false') -ne 0 ]
|
||||
[ $(expr "$output" : ".*input\.message has been set to 'mondo'") -ne 0 ]
|
||||
}
|
||||
|
||||
@test "input message is valid" {
|
||||
run cargo run --example cli -- -v eval policy.wasm -i '{ "message": "world" }'
|
||||
# this prints the output when one the checks below fails
|
||||
echo "output = ${output}"
|
||||
|
||||
# request rejected
|
||||
[ "$status" -eq 0 ]
|
||||
[ $(expr "$output" : '.*"result":.*true') -ne 0 ]
|
||||
[ $(expr "$output" : ".*input\.message has been set to 'world'") -ne 0 ]
|
||||
}
|
||||
9
crates/burrego/test_data/trace/policy.rego
Normal file
9
crates/burrego/test_data/trace/policy.rego
Normal file
@@ -0,0 +1,9 @@
|
||||
package policy
|
||||
|
||||
default main = false
|
||||
|
||||
main {
|
||||
trace(sprintf("input.message has been set to '%v'", [input.message]));
|
||||
m := input.message;
|
||||
m == "world"
|
||||
}
|
||||
Reference in New Issue
Block a user