feat: init commit

This commit is contained in:
2023-01-17 22:45:23 +08:00
commit 94130c107c
72 changed files with 7568 additions and 0 deletions

View 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
View File

@@ -0,0 +1,3 @@
target
*.wasm
*.tar.gz

32
crates/burrego/Cargo.toml Normal file
View 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
View 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
View File

@@ -0,0 +1,2 @@
*.tar.gz
*.wasm

View 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(())
}
}
}

View 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

View 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])
}

View File

@@ -0,0 +1,6 @@
package policy
violation[{"msg": msg}] {
false
msg := ""
}

View File

@@ -0,0 +1,5 @@
package policy
violation[{"msg": msg}] {
msg := "this is not allowed"
}

View 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

View 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])
}

View File

@@ -0,0 +1,6 @@
package kubernetes.admission
deny[msg] {
false
msg := ""
}

View File

@@ -0,0 +1,5 @@
package kubernetes.admission
deny[msg] {
msg := "this is not allowed"
}

View 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"
}

View 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.

View 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
}

View 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)
}
}

View 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)
}

View 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());
}
}
}

View 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"
);
}
}

View 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());
}
}

View 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
}

View 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, "{}", &regex_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(())
}
}

View 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(())
}
}

View 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());
}
}

View 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()
);
}
}

View 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,
}

View 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)
})
}
}

View 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)
}
}

View 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
View 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;

View 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(),
})
}

View 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,
)
}
}

View 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
))),
}
}
}

View 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

View 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 ]
}

View 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])
}

View 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": {
}
}
}
}

View 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": {
}
}
}
}

View 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

View 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 ]
}

View 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"
}