├── .envrc ├── .github └── workflows │ ├── tests.yml │ └── update-rust-toolchain.yaml ├── .gitignore ├── .taplo.toml ├── .typos.toml ├── CODEOWNERS ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── coverage ├── integration-tests │ └── .gitignore └── unit-tests │ └── .gitignore ├── crates └── burrego │ ├── .github │ └── workflows │ │ └── tests.yml │ ├── .gitignore │ ├── Cargo.toml │ ├── Makefile │ ├── examples │ ├── .gitignore │ ├── cli │ │ └── main.rs │ ├── gatekeeper │ │ ├── Makefile │ │ ├── accept-in-namespaces.rego │ │ ├── always-accept.rego │ │ └── always-reject.rego │ └── opa │ │ ├── Makefile │ │ ├── accept-in-namespaces.rego │ │ ├── always-accept.rego │ │ ├── always-reject.rego │ │ ├── no-default-namespace.rego │ │ └── utility │ │ ├── README.md │ │ └── policy.rego │ ├── src │ ├── builtins │ │ ├── builtins_helper.rs │ │ ├── debugging.rs │ │ ├── encoding.rs │ │ ├── glob.rs │ │ ├── json.rs │ │ ├── mod.rs │ │ ├── regex.rs │ │ ├── semver.rs │ │ ├── strings.rs │ │ └── time.rs │ ├── errors.rs │ ├── evaluator.rs │ ├── evaluator_builder.rs │ ├── host_callbacks.rs │ ├── lib.rs │ ├── opa_host_functions.rs │ ├── policy.rs │ └── stack_helper.rs │ └── test_data │ ├── gatekeeper │ ├── Makefile │ ├── e2e.bats │ ├── policy.rego │ ├── request-not-valid.json │ └── request-valid.json │ └── trace │ ├── Makefile │ ├── e2e.bats │ └── policy.rego ├── renovate.json ├── rust-toolchain.toml ├── src ├── admission_request.rs ├── admission_response.rs ├── callback_handler.rs ├── callback_handler │ ├── builder.rs │ ├── crypto.rs │ ├── kubernetes.rs │ ├── kubernetes │ │ ├── client.rs │ │ └── reflector.rs │ ├── oci.rs │ └── sigstore_verification.rs ├── callback_requests.rs ├── constants.rs ├── errors.rs ├── evaluation_context.rs ├── lib.rs ├── policy_artifacthub.rs ├── policy_evaluator.rs ├── policy_evaluator │ ├── errors.rs │ ├── evaluator.rs │ ├── policy_evaluator_builder.rs │ ├── policy_evaluator_pre.rs │ └── stack_pre.rs ├── policy_group_evaluator.rs ├── policy_group_evaluator │ ├── errors.rs │ └── evaluator.rs ├── policy_metadata.rs ├── policy_tracing.rs ├── runtimes.rs └── runtimes │ ├── callback.rs │ ├── rego │ ├── context_aware.rs │ ├── errors.rs │ ├── gatekeeper_inventory.rs │ ├── gatekeeper_inventory_cache.rs │ ├── mod.rs │ ├── opa_inventory.rs │ ├── runtime.rs │ ├── stack.rs │ └── stack_pre.rs │ ├── wapc │ ├── callback.rs │ ├── errors.rs │ ├── mod.rs │ ├── runtime.rs │ ├── stack.rs │ └── stack_pre.rs │ └── wasi_cli │ ├── errors.rs │ ├── mod.rs │ ├── runtime.rs │ ├── stack.rs │ ├── stack_pre.rs │ └── wasi_pipe.rs ├── tests ├── common │ └── mod.rs ├── data │ ├── app_deployment.json │ ├── endless_wasm │ │ ├── .gitignore │ │ ├── Makefile │ │ ├── README.md │ │ ├── wapc_endless_loop.wat │ │ └── wasm_endless_loop.wat │ ├── fixtures │ │ └── kube_context │ │ │ ├── deployments │ │ │ ├── ingress │ │ │ │ └── ingress-nginx.json │ │ │ └── kube-system │ │ │ │ ├── coredns.json │ │ │ │ └── local-path-provisioner.json │ │ │ ├── namespaces │ │ │ ├── cert-manager.json │ │ │ └── kube-system.json │ │ │ └── services │ │ │ └── kube-system │ │ │ ├── kube-dns.json │ │ │ └── metrics-server.json │ ├── gatekeeper_always_happy_policy.wasm │ ├── gatekeeper_always_unhappy_policy.wasm │ ├── pod_creation_flux_cat.json │ ├── pod_with_privileged_containers.json │ ├── raw_mutation.json │ ├── raw_validation.json │ ├── service_clusterip.json │ └── service_loadbalancer.json ├── integration_test.rs └── k8s_mock │ ├── fixtures.rs │ └── mod.rs └── updatecli ├── DEVELOP.md ├── updatecli.d └── update-rust-toolchain.yaml └── values.yaml /.envrc: -------------------------------------------------------------------------------- 1 | export K8S_OPENAPI_ENABLED_VERSION=1.30 2 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | env: 6 | # This is required because commands like `cargo check` do not 7 | # read the cargo development dependencies. 8 | # See https://github.com/Arnavion/k8s-openapi/issues/132 9 | K8S_OPENAPI_ENABLED_VERSION: "1.30" 10 | 11 | jobs: 12 | check: 13 | name: Check 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | - name: "Run cargo check" 18 | run: cargo check 19 | 20 | test: 21 | name: Unit tests 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | - name: "Run cargo test" 26 | run: cargo test --workspace --lib 27 | 28 | integration-test: 29 | name: Integration tests 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | - name: "Run cargo test" 34 | run: cargo test --test '*' 35 | 36 | fmt: 37 | name: Rustfmt 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 41 | - name: "Run cargo fmt" 42 | run: cargo fmt --all -- --check 43 | 44 | clippy: 45 | name: Clippy 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 49 | - name: "Run cargo clippy" 50 | run: cargo clippy --workspace -- -D warnings 51 | 52 | spelling: 53 | name: Spell Check with Typos 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Checkout Actions Repository 57 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 58 | - name: Spell Check Repo 59 | uses: crate-ci/typos@0f0ccba9ed1df83948f0c15026e4f5ccfce46109 # v1.32.0 60 | 61 | coverage: 62 | name: coverage 63 | runs-on: ubuntu-latest 64 | steps: 65 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 66 | - uses: kubewarden/github-actions/tarpaulin-install@7195340a122321bf547fda2ffc07eed6f6ae43f6 # v4.5.1 67 | - name: Generate unit-tests coverage 68 | run: make coverage-unit-tests 69 | - name: Upload unit-tests coverage to Codecov 70 | uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 71 | env: 72 | CODECOV_TOKEN: ${{ secrets.CODECOV_ORG_TOKEN }} 73 | with: 74 | name: unit-tests 75 | directory: coverage/unit-tests 76 | flags: unit-tests 77 | verbose: true 78 | - name: Generate integration-tests coverage 79 | run: make coverage-integration-tests 80 | - name: Upload unit-tests coverage to Codecov 81 | uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 82 | env: 83 | CODECOV_TOKEN: ${{ secrets.CODECOV_ORG_TOKEN }} 84 | with: 85 | name: integration-tests 86 | directory: coverage/integration-tests 87 | flags: integration-tests 88 | verbose: true 89 | -------------------------------------------------------------------------------- /.github/workflows/update-rust-toolchain.yaml: -------------------------------------------------------------------------------- 1 | name: Update rust-toolchain 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "30 3 * * 1" # 3:30 on Monday 7 | 8 | jobs: 9 | update-rust-toolchain: 10 | name: Update Rust toolchain 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | 16 | - name: Install Updatecli in the runner 17 | uses: updatecli/updatecli-action@307ce72e224b82157cc31c78828f168b8e55d47d # v2.84.0 18 | 19 | - name: Update rust version inside of rust-toolchain file 20 | id: update_rust_toolchain 21 | env: 22 | UPDATECLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | UPDATECLI_GITHUB_OWNER: ${{ github.repository_owner }} 24 | run: |- 25 | updatecli apply --config ./updatecli/updatecli.d/update-rust-toolchain.yaml \ 26 | --values updatecli/values.yaml 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | 4 | # coverage instrumentation: 5 | *.profraw 6 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | [formatting] 2 | align_entries = true 3 | reorder_arrays = true 4 | reorder_keys = true 5 | sort_keys = true 6 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | extend-ignore-re = [ 3 | "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", 4 | "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on", 5 | ] 6 | 7 | [files] 8 | extend-exclude = ["*.md", "*.toml"] 9 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @kubewarden/kubewarden-developers 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Code conventions 2 | 3 | Check out our global [CONTRIBUTING guidelines](https://github.com/kubewarden/.github/blob/main/CONTRIBUTING.md) for Rust code conventions 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = [ 3 | "Fabrizio Sestito ", 4 | "Flavio Castelli ", 5 | "Rafael Fernández López ", 6 | "Víctor Cuadrado Juan ", 7 | ] 8 | edition = "2021" 9 | name = "policy-evaluator" 10 | version = "0.25.2" 11 | 12 | [workspace] 13 | members = ["crates/burrego"] 14 | 15 | [dependencies] 16 | anyhow = "1.0" 17 | base64 = "0.22" 18 | burrego = { path = "crates/burrego" } 19 | cached = { version = "0.55", features = ["async_tokio_rt_multi_thread"] } 20 | chrono = { version = "0.4", default-features = false } 21 | dns-lookup = "2.0" 22 | email_address = { version = "0.2", features = ["serde"] } 23 | futures = "0.3" 24 | itertools = "0.14" 25 | json-patch = "4.0" 26 | k8s-openapi = { version = "0.25.0", default-features = false } 27 | kube = { version = "1.0.0", default-features = false, features = [ 28 | "client", 29 | "runtime", 30 | "rustls-tls", 31 | ] } 32 | kubewarden-policy-sdk = { version = "0.13.4", features = ["crd"] } 33 | lazy_static = "1.5" 34 | mail-parser = { version = "0.11", features = ["serde"] } 35 | picky = { version = "7.0.0-rc.8", default-features = false, features = [ 36 | "chrono_conversion", 37 | "x509", 38 | ] } 39 | policy-fetcher = { git = "https://github.com/kubewarden/policy-fetcher", tag = "v0.10.3" } 40 | rhai = { version = "1.21", features = ["sync"] } 41 | semver = { version = "1.0", features = ["serde"] } 42 | serde = { version = "1.0", features = ["derive"] } 43 | serde_json = "1.0" 44 | serde_yaml = "0.9" 45 | sha2 = "0.10" 46 | thiserror = "2.0" 47 | time = { version = "0.3", features = ["serde-human-readable"] } 48 | tokio = { version = "^1", features = ["rt", "rt-multi-thread"] } 49 | tracing = "0.1" 50 | url = { version = "2.5", features = ["serde"] } 51 | validator = { version = "0.20", features = ["derive"] } 52 | wapc = "2.1" 53 | wasi-common = { workspace = true } 54 | wasmparser = "0.231" 55 | wasmtime = { workspace = true } 56 | wasmtime-provider = { version = "2.7.0", features = ["cache"] } 57 | wasmtime-wasi = { workspace = true } 58 | 59 | [workspace.dependencies] 60 | wasi-common = "32.0" 61 | wasmtime = "32.0" 62 | wasmtime-wasi = "32.0" 63 | 64 | [dev-dependencies] 65 | assert-json-diff = "2.0" 66 | hyper = { version = "1" } 67 | k8s-openapi = { version = "0.25.0", default-features = false, features = [ 68 | "v1_30", 69 | ] } 70 | rstest = "0.25" 71 | serial_test = "3.2" 72 | tempfile = "3.19" 73 | test-context = "0.4" 74 | test-log = "0.2" 75 | tower-test = "0.4" 76 | # This is required to have reqwest built using the `rustls-tls-native-roots` 77 | # feature across all the transitive dependencies of policy-fetcher 78 | # This is required to have the integration tests use the system certificates instead of the 79 | # ones bundled inside of rustls. This allows to pull the test policies also from 80 | # self hosted registries (which is great at development time) 81 | reqwest = { version = "0", default-features = false, features = [ 82 | "rustls-tls-native-roots", 83 | ] } 84 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | KUBE_API_VERSION?=1.30 2 | 3 | .PHONY: build 4 | build: 5 | K8S_OPENAPI_ENABLED_VERSION=$(KUBE_API_VERSION) cargo build --release 6 | 7 | .PHONY: fmt 8 | fmt: 9 | K8S_OPENAPI_ENABLED_VERSION=$(KUBE_API_VERSION) cargo fmt --all -- --check 10 | 11 | .PHONY: lint 12 | lint: 13 | K8S_OPENAPI_ENABLED_VERSION=$(KUBE_API_VERSION) cargo clippy --workspace -- -D warnings 14 | 15 | .PHONY: check 16 | check: 17 | K8S_OPENAPI_ENABLED_VERSION=$(KUBE_API_VERSION) cargo check 18 | 19 | .PHONY: typos 20 | typos: 21 | typos # run typo checker from crate-ci/typos 22 | 23 | .PHONY: test 24 | test: fmt lint 25 | cargo test --workspace 26 | 27 | .PHONY: unit-tests 28 | unit-tests: fmt lint 29 | cargo test --workspace --lib 30 | 31 | .PHONY: integration-tests 32 | integration-tests: fmt lint 33 | cargo test --test '*' 34 | 35 | 36 | .PHONY: coverage 37 | coverage: coverage-unit-tests coverage-integration-tests 38 | 39 | .PHONY: coverage-unit-tests 40 | coverage-unit-tests: 41 | # use --skip-clean to not recompile on CI if not needed 42 | cargo tarpaulin --verbose --skip-clean --engine=llvm \ 43 | --lib --bin --follow-exec \ 44 | --out xml --out html --output-dir coverage/unit-tests 45 | 46 | .PHONY: coverage-integration-tests 47 | coverage-integration-tests: 48 | # use --skip-clean to not recompile on CI if not needed 49 | cargo tarpaulin --verbose --skip-clean --engine=llvm \ 50 | --test integration_test --follow-exec \ 51 | --out xml --out html --output-dir coverage/integration-tests 52 | 53 | .PHONY: clean 54 | clean: 55 | cargo clean 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # policy-evaluator 2 | [![Kubewarden Core Repository](https://github.com/kubewarden/community/blob/main/badges/kubewarden-core.svg)](https://github.com/kubewarden/community/blob/main/REPOSITORIES.md#core-scope) 3 | [![Stable](https://img.shields.io/badge/status-stable-brightgreen?style=for-the-badge)](https://github.com/kubewarden/community/blob/main/REPOSITORIES.md#stable) 4 | 5 | Crate used by Kubewarden that is able to evaluate policies with a 6 | given input, request to evaluate and settings. 7 | -------------------------------------------------------------------------------- /coverage/integration-tests/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /coverage/unit-tests/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /crates/burrego/.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 11 | - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1.0.7 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1.0.3 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1.0.7 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | - uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1.0.3 31 | with: 32 | command: test 33 | args: --workspace 34 | 35 | fmt: 36 | name: Rustfmt 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 40 | - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1.0.7 41 | with: 42 | profile: minimal 43 | toolchain: stable 44 | override: true 45 | - run: rustup component add rustfmt 46 | - uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1.0.3 47 | with: 48 | command: fmt 49 | args: --all -- --check 50 | 51 | clippy: 52 | name: Clippy 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 56 | - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1.0.7 57 | with: 58 | profile: minimal 59 | toolchain: stable 60 | override: true 61 | - run: rustup component add clippy 62 | - uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1.0.3 63 | with: 64 | command: clippy 65 | args: -- -D warnings 66 | 67 | e2e: 68 | name: e2e tests 69 | runs-on: ubuntu-latest 70 | steps: 71 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 72 | - name: Setup BATS 73 | run: sudo apt install -y bats 74 | - name: run e2e tests 75 | run: make e2e-test 76 | -------------------------------------------------------------------------------- /crates/burrego/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.wasm 3 | *.tar.gz 4 | -------------------------------------------------------------------------------- /crates/burrego/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "burrego" 3 | version = "0.3.4" 4 | authors = ["Flavio Castelli "] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | base64 = "0.22.0" 11 | chrono = { version = "0.4.38", default-features = false, features = ["clock"] } 12 | chrono-tz = "0.10.0" 13 | gtmpl = "0.7.1" 14 | gtmpl_value = "0.5.1" 15 | itertools = "0.14.0" 16 | json-patch = "4.0.0" 17 | lazy_static = "1.4.0" 18 | regex = "1.5.6" 19 | semver = "1.0.22" 20 | serde_json = "1.0.116" 21 | serde_yaml = "0.9.34" 22 | thiserror = "2.0" 23 | tracing = "0.1" 24 | tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } 25 | url = "2.2.2" 26 | wasmtime = { workspace = true } 27 | 28 | [dev-dependencies] 29 | anyhow = "1.0" 30 | assert-json-diff = "2.0.2" 31 | clap = { version = "4.0", features = ["derive"] } 32 | -------------------------------------------------------------------------------- /crates/burrego/Makefile: -------------------------------------------------------------------------------- 1 | TESTDIRS := $(wildcard test_data/*) 2 | .PHONY: $(TESTDIRS) 3 | 4 | .PHONY: fmt 5 | fmt: 6 | cargo fmt --all -- --check 7 | 8 | .PHONY: lint 9 | lint: 10 | cargo clippy -- -D warnings 11 | 12 | .PHONY: test 13 | test: fmt lint e2e-tests 14 | cargo test 15 | 16 | .PHONY: clean 17 | clean: 18 | cargo clean 19 | 20 | 21 | .PHONY: e2e-tests 22 | e2e-tests: $(TESTDIRS) 23 | $(TESTDIRS): 24 | $(MAKE) -C $@ 25 | -------------------------------------------------------------------------------- /crates/burrego/examples/.gitignore: -------------------------------------------------------------------------------- 1 | *.tar.gz 2 | *.wasm -------------------------------------------------------------------------------- /crates/burrego/examples/cli/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | 3 | use serde_json::json; 4 | use std::{fs::File, io::BufReader, path::PathBuf, process}; 5 | 6 | use tracing::debug; 7 | use tracing_subscriber::prelude::*; 8 | use tracing_subscriber::{fmt, EnvFilter}; 9 | 10 | extern crate burrego; 11 | 12 | extern crate clap; 13 | use clap::Parser; 14 | 15 | #[derive(clap::Parser, Debug)] 16 | #[clap(author, version, about, long_about = None)] 17 | pub(crate) struct Cli { 18 | /// Enable verbose mode 19 | #[clap(short, long, value_parser)] 20 | verbose: bool, 21 | 22 | #[clap(subcommand)] 23 | command: Commands, 24 | } 25 | 26 | #[derive(clap::Subcommand, Debug)] 27 | pub(crate) enum Commands { 28 | /// Evaluate a Rego policy compiled to WebAssembly 29 | Eval { 30 | /// JSON string with the input 31 | #[clap(short, long, value_name = "JSON", value_parser)] 32 | input: Option, 33 | 34 | /// Path to file containing the JSON input 35 | #[clap(long, value_name = "JSON_FILE", value_parser)] 36 | input_path: Option, 37 | 38 | /// JSON string with the data 39 | #[clap(short, long, value_name = "JSON", default_value = "{}", value_parser)] 40 | data: String, 41 | 42 | /// OPA entrypoint to evaluate 43 | #[clap( 44 | short, 45 | long, 46 | value_name = "ENTRYPOINT_ID", 47 | default_value = "0", 48 | value_parser 49 | )] 50 | entrypoint: String, 51 | 52 | /// Path to WebAssembly module to load 53 | #[clap(value_parser, value_name = "WASM_FILE", value_parser)] 54 | policy: String, 55 | }, 56 | /// List the supported builtins 57 | Builtins, 58 | } 59 | 60 | fn main() -> Result<()> { 61 | let cli = Cli::parse(); 62 | 63 | // setup logging 64 | let level_filter = if cli.verbose { "debug" } else { "info" }; 65 | let filter_layer = EnvFilter::new(level_filter) 66 | .add_directive("wasmtime_cranelift=off".parse().unwrap()) // this crate generates lots of tracing events we don't care about 67 | .add_directive("cranelift_codegen=off".parse().unwrap()) // this crate generates lots of tracing events we don't care about 68 | .add_directive("cranelift_wasm=off".parse().unwrap()) // this crate generates lots of tracing events we don't care about 69 | .add_directive("regalloc=off".parse().unwrap()); // this crate generates lots of tracing events we don't care about 70 | tracing_subscriber::registry() 71 | .with(filter_layer) 72 | .with(fmt::layer().with_writer(std::io::stderr)) 73 | .init(); 74 | 75 | match &cli.command { 76 | Commands::Builtins => { 77 | println!("These are the OPA builtins currently supported:"); 78 | for b in burrego::Evaluator::implemented_builtins() { 79 | println!(" - {b}"); 80 | } 81 | Ok(()) 82 | } 83 | Commands::Eval { 84 | input, 85 | input_path, 86 | data, 87 | entrypoint, 88 | policy, 89 | } => { 90 | if input.is_some() && input_path.is_some() { 91 | return Err(anyhow!( 92 | "Cannot use 'input' and 'input-path' at the same time" 93 | )); 94 | } 95 | let input_value: serde_json::Value = if let Some(input_json) = input { 96 | serde_json::from_str(input_json) 97 | .map_err(|e| anyhow!("Cannot parse input: {:?}", e))? 98 | } else if let Some(input_filename) = input_path { 99 | let file = File::open(input_filename) 100 | .map_err(|e| anyhow!("Cannot read input file: {:?}", e))?; 101 | let reader = BufReader::new(file); 102 | serde_json::from_reader(reader)? 103 | } else { 104 | json!({}) 105 | }; 106 | 107 | let mut evaluator = burrego::EvaluatorBuilder::default() 108 | .policy_path(&PathBuf::from(policy)) 109 | .host_callbacks(burrego::HostCallbacks::default()) 110 | .build()?; 111 | 112 | let (major, minor) = evaluator.opa_abi_version()?; 113 | debug!(major, minor, "OPA Wasm ABI"); 114 | 115 | let entrypoints = evaluator.entrypoints(); 116 | debug!(?entrypoints, "OPA entrypoints"); 117 | 118 | let not_implemented_builtins = evaluator.not_implemented_builtins()?; 119 | if !not_implemented_builtins.is_empty() { 120 | eprintln!("Cannot evaluate policy, these builtins are not yet implemented:"); 121 | for b in not_implemented_builtins { 122 | eprintln!(" - {b}"); 123 | } 124 | process::exit(1); 125 | } 126 | 127 | let entrypoint_id = match entrypoint.parse() { 128 | Ok(id) => id, 129 | _ => evaluator.entrypoint_id(&String::from(entrypoint))?, 130 | }; 131 | 132 | let evaluation_res = 133 | evaluator.evaluate(entrypoint_id, &input_value, data.as_bytes())?; 134 | println!("{}", serde_json::to_string_pretty(&evaluation_res)?); 135 | Ok(()) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /crates/burrego/examples/gatekeeper/Makefile: -------------------------------------------------------------------------------- 1 | SOURCES=$(shell find . -name "*.rego") 2 | OBJECTS=$(SOURCES:%.rego=%.wasm) 3 | 4 | all: $(OBJECTS) 5 | 6 | %.wasm: %.rego 7 | opa build -t wasm -e policy/violation -o $*.tar.gz $< 8 | tar -xf $*.tar.gz --transform "s|policy.wasm|$*.wasm|" /policy.wasm 9 | rm $*.tar.gz 10 | 11 | clean: 12 | rm -f *.wasm *.tar.gz 13 | -------------------------------------------------------------------------------- /crates/burrego/examples/gatekeeper/accept-in-namespaces.rego: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | violation[{"msg": msg}] { 4 | object_namespace := input.review.object.metadata.namespace 5 | satisfied := [allowed_namespace | namespace = input.parameters.allowed_namespaces[_]; allowed_namespace = object_namespace == namespace] 6 | not any(satisfied) 7 | msg := sprintf("object created under an invalid namespace %s; allowed namespaces are %v", [object_namespace, input.parameters.allowed_namespaces]) 8 | } 9 | -------------------------------------------------------------------------------- /crates/burrego/examples/gatekeeper/always-accept.rego: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | violation[{"msg": msg}] { 4 | false 5 | msg := "" 6 | } 7 | -------------------------------------------------------------------------------- /crates/burrego/examples/gatekeeper/always-reject.rego: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | violation[{"msg": msg}] { 4 | msg := "this is not allowed" 5 | } 6 | -------------------------------------------------------------------------------- /crates/burrego/examples/opa/Makefile: -------------------------------------------------------------------------------- 1 | SOURCES=$(shell find . -name "*.rego") 2 | OBJECTS=$(SOURCES:%.rego=%.wasm) 3 | 4 | all: $(OBJECTS) 5 | 6 | %.wasm: %.rego 7 | opa build -t wasm -e policy/main utility/policy.rego -o $*.tar.gz $< 8 | tar -xf $*.tar.gz --transform "s|policy.wasm|$*.wasm|" /policy.wasm 9 | rm $*.tar.gz 10 | 11 | clean: 12 | rm -f *.wasm *.tar.gz 13 | -------------------------------------------------------------------------------- /crates/burrego/examples/opa/accept-in-namespaces.rego: -------------------------------------------------------------------------------- 1 | package kubernetes.admission 2 | 3 | deny[msg] { 4 | object_namespace := input.request.object.metadata.namespace 5 | satisfied := [allowed_namespace | namespace = data.allowed_namespaces[_]; allowed_namespace = object_namespace == namespace] 6 | not any(satisfied) 7 | msg := sprintf("object created under an invalid namespace %s; allowed namespaces are %v", [object_namespace, data.allowed_namespaces]) 8 | } 9 | -------------------------------------------------------------------------------- /crates/burrego/examples/opa/always-accept.rego: -------------------------------------------------------------------------------- 1 | package kubernetes.admission 2 | 3 | deny[msg] { 4 | false 5 | msg := "" 6 | } 7 | -------------------------------------------------------------------------------- /crates/burrego/examples/opa/always-reject.rego: -------------------------------------------------------------------------------- 1 | package kubernetes.admission 2 | 3 | deny[msg] { 4 | msg := "this is not allowed" 5 | } 6 | -------------------------------------------------------------------------------- /crates/burrego/examples/opa/no-default-namespace.rego: -------------------------------------------------------------------------------- 1 | package kubernetes.admission 2 | 3 | # RBAC alone would suffice here, but we create a policy just to show 4 | # how it can be done as well. 5 | deny[msg] { 6 | input.request.object.metadata.namespace == "default" 7 | msg := "you cannot use the default namespace" 8 | } 9 | -------------------------------------------------------------------------------- /crates/burrego/examples/opa/utility/README.md: -------------------------------------------------------------------------------- 1 | # Open Policy Agent utility 2 | 3 | This folder contains the entry point for Open Policy Agent policies. 4 | 5 | Since Open Policy Agent policies have to produce an `AdmissionReview` 6 | object, this utility library contains the Rego entry point that 7 | generates such `AdmissionReview`, based on whether the `deny` query 8 | inside the package `kubernetes.admission` (defined by the policy 9 | itself) is evaluated to `true`. 10 | 11 | If `deny` evaluates to true, the produced `AdmissionReview` will 12 | reject the request. Otherwise, it will be accepted. 13 | -------------------------------------------------------------------------------- /crates/burrego/examples/opa/utility/policy.rego: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | import data.kubernetes.admission 4 | 5 | main = { 6 | "apiVersion": "admission.k8s.io/v1", 7 | "kind": "AdmissionReview", 8 | "response": response, 9 | } 10 | 11 | response = { 12 | "uid": input.request.uid, 13 | "allowed": false, 14 | "status": {"message": reason}, 15 | } { 16 | reason = concat(", ", admission.deny) 17 | reason != "" 18 | } else = { 19 | "uid": input.request.uid, 20 | "allowed": true, 21 | } { 22 | true 23 | } 24 | -------------------------------------------------------------------------------- /crates/burrego/src/builtins/builtins_helper.rs: -------------------------------------------------------------------------------- 1 | use super::{get_builtins, BuiltinFunctionsMap}; 2 | use crate::errors::{BurregoError, Result}; 3 | 4 | use lazy_static::lazy_static; 5 | use std::sync::RwLock; 6 | use tracing::debug; 7 | 8 | lazy_static! { 9 | pub(crate) static ref BUILTINS_HELPER: RwLock = { 10 | RwLock::new(BuiltinsHelper { 11 | builtins: get_builtins(), 12 | }) 13 | }; 14 | } 15 | pub(crate) struct BuiltinsHelper { 16 | builtins: BuiltinFunctionsMap, 17 | } 18 | 19 | impl BuiltinsHelper { 20 | pub(crate) fn invoke( 21 | &self, 22 | builtin_name: &str, 23 | args: &[serde_json::Value], 24 | ) -> Result { 25 | let builtin_fn = self 26 | .builtins 27 | .get(builtin_name) 28 | .ok_or_else(|| BurregoError::BuiltinNotImplementedError(builtin_name.to_string()))?; 29 | 30 | debug!( 31 | builtin = builtin_name, 32 | args = serde_json::to_string(&args) 33 | .expect("cannot convert builtins args to JSON") 34 | .as_str(), 35 | "invoking builtin" 36 | ); 37 | builtin_fn(args) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/burrego/src/builtins/debugging.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{BurregoError, Result}; 2 | 3 | #[tracing::instrument(skip(args))] 4 | pub fn trace(args: &[serde_json::Value]) -> Result { 5 | if args.len() != 1 { 6 | return Err(BurregoError::BuiltinError { 7 | name: "trace".to_string(), 8 | message: "Wrong number of arguments".to_string(), 9 | }); 10 | } 11 | 12 | let message_str = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError { 13 | name: "trace".to_string(), 14 | message: "1st parameter is not a string".to_string(), 15 | })?; 16 | 17 | tracing::debug!("{}", message_str); 18 | 19 | Ok(serde_json::Value::Null) 20 | } 21 | -------------------------------------------------------------------------------- /crates/burrego/src/builtins/glob.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{BurregoError, Result}; 2 | 3 | pub fn quote_meta(args: &[serde_json::Value]) -> Result { 4 | if args.len() != 1 { 5 | return Err(BurregoError::BuiltinError { 6 | name: "glob.quote_meta".to_string(), 7 | message: "wrong number of arguments".to_string(), 8 | }); 9 | } 10 | 11 | let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError { 12 | name: "glob.quote_meta".to_string(), 13 | message: "1st parameter is not a string".to_string(), 14 | })?; 15 | 16 | serde_json::to_value(escape(input)).map_err(|e| BurregoError::BuiltinError { 17 | name: "glob.quote_meta".to_string(), 18 | message: format!("cannot convert value into JSON: {e:?}"), 19 | }) 20 | } 21 | 22 | fn escape(s: &str) -> String { 23 | let mut escaped = String::new(); 24 | for c in s.chars() { 25 | match c { 26 | '*' | '?' | '\\' | '[' | ']' | '{' | '}' => { 27 | escaped.push('\\'); 28 | escaped.push(c); 29 | } 30 | c => { 31 | escaped.push(c); 32 | } 33 | } 34 | } 35 | escaped 36 | } 37 | 38 | #[cfg(test)] 39 | mod test { 40 | #[test] 41 | fn escape() { 42 | assert_eq!(super::escape("*.domain.com"), r"\*.domain.com"); 43 | 44 | assert_eq!(super::escape("*.domain-*.com"), r"\*.domain-\*.com"); 45 | 46 | assert_eq!(super::escape("domain.com"), r"domain.com"); 47 | 48 | assert_eq!(super::escape("domain-[ab].com"), r"domain-\[ab\].com"); 49 | 50 | assert_eq!(super::escape("nie?ce"), r"nie\?ce"); 51 | 52 | assert_eq!( 53 | super::escape("some *?\\[]{} text"), 54 | "some \\*\\?\\\\\\[\\]\\{\\} text" 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/burrego/src/builtins/json.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{BurregoError, Result}; 2 | 3 | pub fn patch(args: &[serde_json::Value]) -> Result { 4 | if args.len() != 2 { 5 | return Err(BurregoError::BuiltinError { 6 | name: "json.patch".to_string(), 7 | message: "wrong number of arguments".to_string(), 8 | }); 9 | } 10 | 11 | if !args[0].is_object() { 12 | return Err(BurregoError::BuiltinError { 13 | name: "json.patch".to_string(), 14 | message: "1st parameter is not an object".to_string(), 15 | }); 16 | } 17 | let mut obj = args[0].clone(); 18 | 19 | if !args[1].is_array() { 20 | return Err(BurregoError::BuiltinError { 21 | name: "json.patch".to_string(), 22 | message: "2nd parameter is not an array".to_string(), 23 | }); 24 | } 25 | let patches_str = serde_json::to_string(&args[1]).map_err(|_| BurregoError::BuiltinError { 26 | name: "json.patch".to_string(), 27 | message: "cannot convert 2nd parameter to string".to_string(), 28 | })?; 29 | let patches: json_patch::Patch = serde_json::from_str(&patches_str).unwrap(); 30 | 31 | json_patch::patch(&mut obj, &patches).map_err(|e| BurregoError::BuiltinError { 32 | name: "json.patch".to_string(), 33 | message: format!("cannot apply patch: {e:?}"), 34 | })?; 35 | 36 | serde_json::to_value(obj).map_err(|e| BurregoError::BuiltinError { 37 | name: "json.patch".to_string(), 38 | message: format!("cannot convert value into JSON: {e:?}"), 39 | }) 40 | } 41 | 42 | #[cfg(test)] 43 | mod test { 44 | use super::*; 45 | use assert_json_diff::assert_json_eq; 46 | use serde_json::json; 47 | 48 | #[test] 49 | fn test_patch() { 50 | let args: Vec = vec![ 51 | json!({"a": {"foo": 1}}), 52 | json!([{"op": "add", "path": "/a/bar", "value": 2}]), 53 | ]; 54 | 55 | let actual = patch(&args); 56 | assert!(actual.is_ok()); 57 | assert_json_eq!(json!({"a": {"foo": 1, "bar": 2}}), actual.unwrap()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/burrego/src/builtins/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Result; 2 | use std::collections::HashMap; 3 | 4 | pub(crate) mod builtins_helper; 5 | mod debugging; 6 | mod encoding; 7 | mod glob; 8 | mod json; 9 | mod regex; 10 | mod semver; 11 | mod strings; 12 | mod time; 13 | 14 | pub(crate) use builtins_helper::BUILTINS_HELPER; 15 | 16 | pub(crate) type BuiltinFunctionsMap = 17 | HashMap<&'static str, fn(&[serde_json::Value]) -> Result>; 18 | 19 | pub fn get_builtins() -> BuiltinFunctionsMap { 20 | let mut functions: BuiltinFunctionsMap = HashMap::new(); 21 | 22 | // debugging 23 | functions.insert("trace", debugging::trace); 24 | 25 | // encoding 26 | functions.insert( 27 | "base64url.encode_no_pad", 28 | encoding::base64url::encode_no_pad, 29 | ); 30 | functions.insert("urlquery.encode", encoding::urlquery::encode); 31 | functions.insert("urlquery.decode", encoding::urlquery::decode); 32 | functions.insert("urlquery.encode_object", encoding::urlquery::encode_object); 33 | functions.insert("urlquery.decode_object", encoding::urlquery::decode_object); 34 | functions.insert("json.is_valid", encoding::json::is_valid); 35 | functions.insert("yaml.marshal", encoding::yaml::marshal); 36 | functions.insert("yaml.unmarshal", encoding::yaml::unmarshal); 37 | functions.insert("yaml.is_valid", encoding::yaml::is_valid); 38 | functions.insert("hex.encode", encoding::hex::encode); 39 | functions.insert("hex.decode", encoding::hex::decode); 40 | 41 | // glob 42 | functions.insert("glob.quote_meta", glob::quote_meta); 43 | 44 | // objects 45 | functions.insert("json.patch", json::patch); 46 | 47 | // regex 48 | functions.insert("regex.split", regex::split); 49 | functions.insert("regex.template_match", regex::template_match); 50 | functions.insert("regex.find_n", regex::find_n); 51 | 52 | // semver 53 | functions.insert("semver.is_valid", semver::is_valid); 54 | functions.insert("semver.compare", semver::compare); 55 | 56 | // strings 57 | functions.insert("sprintf", strings::sprintf); 58 | 59 | // time 60 | functions.insert("time.now_ns", time::now_ns); 61 | functions.insert("parse_rfc3339_ns", time::parse_rfc3339_ns); 62 | functions.insert("date", time::date); 63 | 64 | functions 65 | } 66 | -------------------------------------------------------------------------------- /crates/burrego/src/builtins/semver.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{BurregoError, Result}; 2 | use semver::Version; 3 | use std::cmp::Ordering; 4 | 5 | pub fn is_valid(args: &[serde_json::Value]) -> Result { 6 | if args.len() != 1 { 7 | return Err(BurregoError::BuiltinError { 8 | name: "semver.is_valid".to_string(), 9 | message: "wrong number of arguments".to_string(), 10 | }); 11 | } 12 | 13 | let input = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError { 14 | name: "semver.is_valid".to_string(), 15 | message: "1st parameter is not a string".to_string(), 16 | })?; 17 | 18 | let valid_version = Version::parse(input).map(|_| true).unwrap_or(false); 19 | 20 | serde_json::to_value(valid_version).map_err(|e| BurregoError::BuiltinError { 21 | name: "semver.is_valid".to_string(), 22 | message: format!("cannot convert value into JSON: {e:?}"), 23 | }) 24 | } 25 | 26 | pub fn compare(args: &[serde_json::Value]) -> Result { 27 | if args.len() != 2 { 28 | return Err(BurregoError::BuiltinError { 29 | name: "semver.compare".to_string(), 30 | message: "wrong number of arguments".to_string(), 31 | }); 32 | } 33 | 34 | let version_a = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError { 35 | name: "semver.compare".to_string(), 36 | message: "1st parameter is not a string".to_string(), 37 | })?; 38 | 39 | let version_b = args[1].as_str().ok_or_else(|| BurregoError::BuiltinError { 40 | name: "semver.compare".to_string(), 41 | message: "2nd parameter is not a string".to_string(), 42 | })?; 43 | 44 | let version_a = Version::parse(version_a).map_err(|e| BurregoError::BuiltinError { 45 | name: "semver.compare".to_string(), 46 | message: format!("first argument is not a valid semantic version: {e:?}"), 47 | })?; 48 | 49 | let version_b = Version::parse(version_b).map_err(|e| BurregoError::BuiltinError { 50 | name: "semver.compare".to_string(), 51 | message: format!("second argument is not a valid semantic version: {e:?}"), 52 | })?; 53 | 54 | let res = match version_a.cmp(&version_b) { 55 | Ordering::Less => -1, 56 | Ordering::Equal => 0, 57 | Ordering::Greater => 1, 58 | }; 59 | 60 | serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError { 61 | name: "semver.compare".to_string(), 62 | message: format!("cannot convert value into JSON: {e:?}"), 63 | }) 64 | } 65 | 66 | #[cfg(test)] 67 | mod test { 68 | use super::*; 69 | 70 | use serde_json::json; 71 | 72 | #[test] 73 | fn is_valid() -> Result<()> { 74 | assert_eq!(super::is_valid(&[json!("1.0.0")])?, true); 75 | assert_eq!(super::is_valid(&[json!("1.0.0-rc1")])?, true); 76 | assert_eq!(super::is_valid(&[json!("invalidsemver-1.0.0")])?, false); 77 | 78 | Ok(()) 79 | } 80 | 81 | #[test] 82 | fn compare() -> Result<()> { 83 | assert_eq!(super::compare(&[json!("0.0.1"), json!("0.1.0")])?, -1); 84 | assert_eq!( 85 | super::compare(&[json!("1.0.0-rc1"), json!("1.0.0-rc1")])?, 86 | 0 87 | ); 88 | assert_eq!(super::compare(&[json!("0.1.0"), json!("0.0.1")])?, 1); 89 | assert_eq!( 90 | super::compare(&[json!("1.0.0-beta1"), json!("1.0.0-alpha3")])?, 91 | 1 92 | ); 93 | assert_eq!( 94 | super::compare(&[json!("1.0.0-rc2"), json!("1.0.0-rc1")])?, 95 | 1 96 | ); 97 | assert!(super::compare(&[json!("invalidsemver-1.0.0"), json!("0.1.0")]).is_err()); 98 | assert!(super::compare(&[json!("0.1.0"), json!("invalidsemver-1.0.0")]).is_err()); 99 | 100 | Ok(()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/burrego/src/builtins/strings.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{BurregoError, Result}; 2 | use std::{collections::HashMap, convert::From}; 3 | 4 | struct GoTmplValue(gtmpl::Value); 5 | 6 | impl From for GoTmplValue { 7 | fn from(value: serde_json::Value) -> Self { 8 | match value { 9 | serde_json::Value::String(s) => GoTmplValue(gtmpl::Value::String(s)), 10 | serde_json::Value::Number(n) => { 11 | let n: i64 = n.as_i64().unwrap(); 12 | let number: gtmpl_value::Number = n.into(); 13 | GoTmplValue(gtmpl::Value::Number(number)) 14 | } 15 | serde_json::Value::Bool(b) => GoTmplValue(gtmpl::Value::Bool(b)), 16 | serde_json::Value::Array(arr) => { 17 | let res: Vec = arr 18 | .iter() 19 | .map(|i| { 20 | let v: GoTmplValue = i.clone().into(); 21 | v.0 22 | }) 23 | .collect(); 24 | GoTmplValue(gtmpl::Value::Array(res)) 25 | } 26 | serde_json::Value::Object(obj) => { 27 | let res: HashMap = obj 28 | .iter() 29 | .map(|(k, v)| { 30 | let val: GoTmplValue = v.clone().into(); 31 | (k.clone(), val.0) 32 | }) 33 | .collect(); 34 | GoTmplValue(gtmpl::Value::Map(res)) 35 | } 36 | _ => GoTmplValue(gtmpl::Value::Nil), 37 | } 38 | } 39 | } 40 | 41 | pub fn sprintf(args: &[serde_json::Value]) -> Result { 42 | if args.len() != 2 { 43 | return Err(BurregoError::BuiltinError { 44 | name: "sprintf".to_string(), 45 | message: "Wrong number of arguments given".to_string(), 46 | }); 47 | } 48 | 49 | let fmt_str = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError { 50 | name: "sprintf".to_string(), 51 | message: "1st parameter is not a string".to_string(), 52 | })?; 53 | let fmt_args: Vec = args[1] 54 | .as_array() 55 | .ok_or_else(|| BurregoError::BuiltinError { 56 | name: "sprintf".to_string(), 57 | message: "2nd parameter is not an array".to_string(), 58 | })? 59 | .iter() 60 | .map(|i| { 61 | let g: GoTmplValue = i.clone().into(); 62 | g.0 63 | }) 64 | .collect(); 65 | 66 | let mut index_cmds: Vec = Vec::new(); 67 | for i in 0..fmt_args.len() { 68 | index_cmds.push(format!("(index . {i})")); 69 | } 70 | 71 | let template_str = format!(r#"{{{{ printf "{}" {}}}}}"#, fmt_str, index_cmds.join(" ")); 72 | let res = gtmpl::template(&template_str, fmt_args.as_slice()).map_err(|e| { 73 | BurregoError::BuiltinError { 74 | name: "sprintf".to_string(), 75 | message: format!( 76 | "Cannot render go template '{template_str}' with args {fmt_args:?}: {e:?}" 77 | ), 78 | } 79 | })?; 80 | 81 | serde_json::to_value(res).map_err(|e| BurregoError::BuiltinError { 82 | name: "sprintf".to_string(), 83 | message: format!("Cannot convert value into JSON: {e:?}"), 84 | }) 85 | } 86 | 87 | #[cfg(test)] 88 | mod test { 89 | use super::*; 90 | use serde_json::json; 91 | 92 | #[test] 93 | fn sprintf_mixed_input() { 94 | let args: Vec = vec![ 95 | json!("hello %v %v %v"), 96 | json!(["world", 42, ["this", "is", "a", "list"]]), 97 | ]; 98 | 99 | let actual = sprintf(&args); 100 | assert!(actual.is_ok()); 101 | assert_eq!(json!("hello world 42 [this is a list]"), actual.unwrap()); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /crates/burrego/src/builtins/time.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{BurregoError, Result}; 2 | use chrono::{self, DateTime, Datelike, Duration, Local}; 3 | use std::str::FromStr; 4 | 5 | pub fn now_ns(args: &[serde_json::Value]) -> Result { 6 | if !args.is_empty() { 7 | return Err(BurregoError::BuiltinError { 8 | name: "time.now_ns".to_string(), 9 | message: "wrong number of arguments given".to_string(), 10 | }); 11 | } 12 | let now = Local::now(); 13 | serde_json::to_value(now.timestamp_nanos_opt()).map_err(|e| BurregoError::BuiltinError { 14 | name: "time.now_ns".to_string(), 15 | message: format!("cannot convert value into JSON: {e:?}"), 16 | }) 17 | } 18 | 19 | pub fn parse_rfc3339_ns(args: &[serde_json::Value]) -> Result { 20 | if args.len() != 1 { 21 | return Err(BurregoError::BuiltinError { 22 | name: "time.parse_rfc3339_ns".to_string(), 23 | message: "wrong number of arguments given".to_string(), 24 | }); 25 | } 26 | 27 | let value = args[0].as_str().ok_or_else(|| BurregoError::BuiltinError { 28 | name: "time.parse_rfc3339_ns".to_string(), 29 | message: "1st parameter is not a string".to_string(), 30 | })?; 31 | 32 | let dt = DateTime::parse_from_rfc3339(value).map_err(|e| BurregoError::BuiltinError { 33 | name: "time.parse_rfc3339_ns".to_string(), 34 | message: format!(": cannot convert {value}: {e:?}"), 35 | })?; 36 | 37 | serde_json::to_value(dt.timestamp_nanos_opt()).map_err(|e| BurregoError::BuiltinError { 38 | name: "time.parse_rfc3339_ns".to_string(), 39 | message: format!("cannot convert value into JSON: {e:?}"), 40 | }) 41 | } 42 | 43 | pub fn date(args: &[serde_json::Value]) -> Result { 44 | if args.len() != 1 { 45 | return Err(BurregoError::BuiltinError { 46 | name: "time.date".to_string(), 47 | message: "wrong number of arguments given".to_string(), 48 | }); 49 | } 50 | 51 | let nanoseconds: i64; 52 | let mut timezone: chrono_tz::Tz = chrono_tz::UTC; 53 | 54 | match args[0].clone() { 55 | serde_json::Value::Number(val) => { 56 | nanoseconds = val.as_i64().ok_or_else(|| BurregoError::BuiltinError { 57 | name: "time.date".to_string(), 58 | message: "1st parameter is not a number".to_string(), 59 | })?; 60 | } 61 | serde_json::Value::Array(val) => { 62 | if val.len() != 2 { 63 | return Err(BurregoError::BuiltinError { 64 | name: "time.date".to_string(), 65 | message: "wrong number of items inside of input array".to_string(), 66 | }); 67 | } 68 | nanoseconds = val[0].as_i64().ok_or_else(|| BurregoError::BuiltinError { 69 | name: "time.date".to_string(), 70 | message: "1st array item is not a number".to_string(), 71 | })?; 72 | let tz_name = val[1].as_str().ok_or_else(|| BurregoError::BuiltinError { 73 | name: "time.date".to_string(), 74 | message: "2nd array item is not a string".to_string(), 75 | })?; 76 | if tz_name == "Local" { 77 | return date_local(nanoseconds); 78 | } else { 79 | timezone = 80 | chrono_tz::Tz::from_str(tz_name).map_err(|e| BurregoError::BuiltinError { 81 | name: "time.date".to_string(), 82 | message: format!("cannot handle given timezone {tz_name}: {e:?}"), 83 | })?; 84 | } 85 | } 86 | _ => { 87 | return Err(BurregoError::BuiltinError { 88 | name: "time.date".to_string(), 89 | message: "the 1st parameter is neither a number nor an array".to_string(), 90 | }); 91 | } 92 | }; 93 | 94 | let dt = DateTime::UNIX_EPOCH 95 | .checked_add_signed(Duration::nanoseconds(nanoseconds)) 96 | .ok_or_else(|| BurregoError::BuiltinError { 97 | name: "time.date".to_string(), 98 | message: "overflow when building date".to_string(), 99 | })? 100 | .with_timezone(&timezone); 101 | 102 | Ok(serde_json::json!([dt.year(), dt.month(), dt.day(),])) 103 | } 104 | 105 | pub fn date_local(ns: i64) -> Result { 106 | let dt = DateTime::UNIX_EPOCH 107 | .checked_add_signed(Duration::nanoseconds(ns)) 108 | .ok_or_else(|| BurregoError::BuiltinError { 109 | name: "time.date".to_string(), 110 | message: "overflow when building date".to_string(), 111 | })? 112 | .with_timezone(&chrono::Local); 113 | 114 | Ok(serde_json::json!([dt.year(), dt.month(), dt.day(),])) 115 | } 116 | #[cfg(test)] 117 | mod test { 118 | use super::*; 119 | use chrono::TimeZone; 120 | use serde_json::json; 121 | 122 | #[test] 123 | fn test_parse_rfc3339_ns() { 124 | let input_dt = Local::now(); 125 | 126 | let args: Vec = vec![json!(input_dt.to_rfc3339())]; 127 | 128 | let actual = parse_rfc3339_ns(&args); 129 | assert!(actual.is_ok()); 130 | assert_eq!(json!(input_dt.timestamp_nanos_opt()), actual.unwrap()); 131 | } 132 | 133 | #[test] 134 | fn date_with_no_tz() { 135 | let input_dt = Local::now().naive_utc(); 136 | 137 | let args: Vec = vec![json!(input_dt.and_utc().timestamp_nanos_opt())]; 138 | 139 | let actual = date(&args); 140 | assert!(actual.is_ok()); 141 | assert_eq!( 142 | json!([input_dt.year(), input_dt.month(), input_dt.day()]), 143 | actual.unwrap() 144 | ); 145 | } 146 | 147 | #[test] 148 | fn date_with_tz() { 149 | let input_dt = match chrono_tz::US::Pacific.with_ymd_and_hms(1990, 5, 6, 12, 30, 45) { 150 | chrono::LocalResult::Single(dt) => dt, 151 | _ => panic!("didn't get the expected datetime object"), 152 | }; 153 | 154 | let args: Vec = 155 | vec![json!([input_dt.timestamp_nanos_opt(), "US/Pacific"])]; 156 | 157 | let actual = date(&args); 158 | assert!(actual.is_ok()); 159 | assert_eq!( 160 | json!([input_dt.year(), input_dt.month(), input_dt.day()]), 161 | actual.unwrap() 162 | ); 163 | } 164 | 165 | #[test] 166 | fn date_with_local_tz() { 167 | let input_dt = Local::now().naive_utc(); 168 | 169 | let args: Vec = 170 | vec![json!([input_dt.and_utc().timestamp_nanos_opt(), "Local"])]; 171 | 172 | let actual = date(&args); 173 | assert!(actual.is_ok()); 174 | assert_eq!( 175 | json!([input_dt.year(), input_dt.month(), input_dt.day()]), 176 | actual.unwrap() 177 | ); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /crates/burrego/src/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | pub type Result = std::result::Result; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum BurregoError { 7 | #[error("Missing Rego builtins: {0}")] 8 | MissingRegoBuiltins(String), 9 | 10 | #[error("wasm engine error: {0}")] 11 | WasmEngineError(String), 12 | 13 | #[error("Rego wasm error: {0}")] 14 | RegoWasmError(String), 15 | 16 | #[error("{msg}: {source}")] 17 | JSONError { 18 | msg: String, 19 | #[source] 20 | source: serde_json::Error, 21 | }, 22 | 23 | #[error("Evaluator builder error: {0}")] 24 | EvaluatorBuilderError(String), 25 | 26 | #[error("Builtin error [{name:?}]: {message:?}")] 27 | BuiltinError { name: String, message: String }, 28 | 29 | #[error("Builtin not implemented: {0}")] 30 | BuiltinNotImplementedError(String), 31 | 32 | /// Wasmtime execution deadline exceeded 33 | #[error("guest code interrupted, execution deadline exceeded")] 34 | ExecutionDeadlineExceeded, 35 | } 36 | -------------------------------------------------------------------------------- /crates/burrego/src/evaluator.rs: -------------------------------------------------------------------------------- 1 | use crate::builtins; 2 | use crate::errors::{BurregoError, Result}; 3 | use crate::host_callbacks::HostCallbacks; 4 | use crate::opa_host_functions; 5 | use crate::policy::Policy; 6 | use crate::stack_helper::StackHelper; 7 | 8 | use itertools::Itertools; 9 | use std::collections::{HashMap, HashSet}; 10 | use tracing::debug; 11 | use wasmtime::{Engine, Instance, Linker, Memory, MemoryType, Module, Store}; 12 | 13 | macro_rules! set_epoch_deadline_and_call_guest { 14 | ($epoch_deadline:expr, $store:expr, $code:block) => {{ 15 | if let Some(deadline) = $epoch_deadline { 16 | $store.set_epoch_deadline(deadline); 17 | } 18 | $code 19 | }}; 20 | } 21 | 22 | struct EvaluatorStack { 23 | store: Store>, 24 | instance: Instance, 25 | memory: Memory, 26 | policy: Policy, 27 | } 28 | 29 | pub struct Evaluator { 30 | engine: Engine, 31 | module: Module, 32 | store: Store>, 33 | instance: Instance, 34 | memory: Memory, 35 | policy: Policy, 36 | host_callbacks: HostCallbacks, 37 | /// used to tune the [epoch 38 | /// interruption](https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#method.epoch_interruption) 39 | /// feature of wasmtime 40 | epoch_deadline: Option, 41 | entrypoints: HashMap, 42 | used_builtins: HashSet, 43 | } 44 | 45 | impl Evaluator { 46 | pub(crate) fn from_engine_and_module( 47 | engine: Engine, 48 | module: Module, 49 | host_callbacks: HostCallbacks, 50 | epoch_deadline: Option, 51 | ) -> Result { 52 | let stack = Self::setup( 53 | engine.clone(), 54 | module.clone(), 55 | host_callbacks.clone(), 56 | epoch_deadline, 57 | )?; 58 | let mut store = stack.store; 59 | let instance = stack.instance; 60 | let memory = stack.memory; 61 | let policy = stack.policy; 62 | 63 | let used_builtins: HashSet = 64 | set_epoch_deadline_and_call_guest!(epoch_deadline, store, { 65 | policy 66 | .builtins(&mut store, &memory)? 67 | .keys() 68 | .cloned() 69 | .collect() 70 | }); 71 | 72 | let entrypoints = set_epoch_deadline_and_call_guest!(epoch_deadline, store, { 73 | policy.entrypoints(&mut store, &memory) 74 | })?; 75 | 76 | debug!( 77 | used = used_builtins.iter().join(", ").as_str(), 78 | "policy builtins" 79 | ); 80 | 81 | let mut evaluator = Evaluator { 82 | engine, 83 | module, 84 | store, 85 | instance, 86 | memory, 87 | policy, 88 | host_callbacks, 89 | epoch_deadline, 90 | entrypoints, 91 | used_builtins, 92 | }; 93 | 94 | let not_implemented_builtins = evaluator.not_implemented_builtins()?; 95 | if !not_implemented_builtins.is_empty() { 96 | return Err(BurregoError::MissingRegoBuiltins( 97 | not_implemented_builtins.iter().join(", "), 98 | )); 99 | } 100 | 101 | Ok(evaluator) 102 | } 103 | 104 | fn setup( 105 | engine: Engine, 106 | module: Module, 107 | host_callbacks: HostCallbacks, 108 | epoch_deadline: Option, 109 | ) -> Result { 110 | let mut linker = Linker::>::new(&engine); 111 | 112 | let opa_data_helper: Option = None; 113 | let mut store = Store::new(&engine, opa_data_helper); 114 | 115 | let memory_ty = MemoryType::new(5, None); 116 | let memory = Memory::new(&mut store, memory_ty) 117 | .map_err(|e| BurregoError::WasmEngineError(format!("cannot create memory: {e}")))?; 118 | linker 119 | .define(&mut store, "env", "memory", memory) 120 | .map_err(|e| { 121 | BurregoError::WasmEngineError(format!("linker cannot define memory: {e}")) 122 | })?; 123 | 124 | opa_host_functions::add_to_linker(&mut linker)?; 125 | 126 | // All the OPA modules use a shared memory. Because of that the linker, at instantiation 127 | // time, invokes a function provided by the module. This function, for OPA modules, is called 128 | // `_initialize`. 129 | // When the engine is configured to use epoch_deadline, the invocation of this function 130 | // will cause an immediate failure unless the store has some "ticks" inside of it. Like 131 | // any other function invocation 132 | let instance = set_epoch_deadline_and_call_guest!(epoch_deadline, store, { 133 | linker.instantiate(&mut store, &module).map_err(|e| { 134 | BurregoError::WasmEngineError(format!("linker cannot create instance: {e}")) 135 | }) 136 | })?; 137 | 138 | let stack_helper = StackHelper::new( 139 | &instance, 140 | &memory, 141 | &mut store, 142 | host_callbacks.opa_abort, 143 | host_callbacks.opa_println, 144 | )?; 145 | let policy = Policy::new(&instance, &mut store, &memory)?; 146 | _ = store.data_mut().insert(stack_helper); 147 | 148 | Ok(EvaluatorStack { 149 | memory, 150 | store, 151 | instance, 152 | policy, 153 | }) 154 | } 155 | 156 | pub fn reset(&mut self) -> Result<()> { 157 | let stack = Self::setup( 158 | self.engine.clone(), 159 | self.module.clone(), 160 | self.host_callbacks.clone(), 161 | self.epoch_deadline, 162 | )?; 163 | self.store = stack.store; 164 | self.instance = stack.instance; 165 | self.memory = stack.memory; 166 | self.policy = stack.policy; 167 | 168 | Ok(()) 169 | } 170 | 171 | pub fn opa_abi_version(&mut self) -> Result<(i32, i32)> { 172 | let major = self 173 | .instance 174 | .get_global(&mut self.store, "opa_wasm_abi_version") 175 | .and_then(|g| g.get(&mut self.store).i32()) 176 | .ok_or_else(|| { 177 | BurregoError::RegoWasmError("Cannot find OPA Wasm ABI major version".to_string()) 178 | })?; 179 | let minor = self 180 | .instance 181 | .get_global(&mut self.store, "opa_wasm_abi_minor_version") 182 | .and_then(|g| g.get(&mut self.store).i32()) 183 | .ok_or_else(|| { 184 | BurregoError::RegoWasmError("Cannot find OPA Wasm ABI minor version".to_string()) 185 | })?; 186 | 187 | Ok((major, minor)) 188 | } 189 | 190 | pub fn implemented_builtins() -> HashSet { 191 | builtins::get_builtins() 192 | .keys() 193 | .map(|v| String::from(*v)) 194 | .collect() 195 | } 196 | 197 | pub fn not_implemented_builtins(&mut self) -> Result> { 198 | let supported_builtins: HashSet = builtins::get_builtins() 199 | .keys() 200 | .map(|v| String::from(*v)) 201 | .collect(); 202 | Ok(self 203 | .used_builtins 204 | .difference(&supported_builtins) 205 | .cloned() 206 | .collect()) 207 | } 208 | 209 | pub fn entrypoint_id(&mut self, entrypoint: &str) -> Result { 210 | self.entrypoints 211 | .iter() 212 | .find(|(k, _v)| k == &entrypoint) 213 | .map(|(_k, v)| *v) 214 | .ok_or_else(|| { 215 | BurregoError::RegoWasmError(format!( 216 | "Cannot find the specified entrypoint {entrypoint} inside of {:?}", 217 | self.entrypoints 218 | )) 219 | }) 220 | } 221 | 222 | pub fn entrypoints(&self) -> HashMap { 223 | self.entrypoints.clone() 224 | } 225 | 226 | fn has_entrypoint(&self, entrypoint_id: i32) -> bool { 227 | self.entrypoints.iter().any(|(_k, &v)| v == entrypoint_id) 228 | } 229 | 230 | pub fn evaluate( 231 | &mut self, 232 | entrypoint_id: i32, 233 | input: &serde_json::Value, 234 | data: &[u8], 235 | ) -> Result { 236 | set_epoch_deadline_and_call_guest!(self.epoch_deadline, self.store, { 237 | if !self.has_entrypoint(entrypoint_id) { 238 | return Err(BurregoError::RegoWasmError(format!( 239 | "Cannot find the specified entrypoint {entrypoint_id} inside of {:?}", 240 | self.entrypoints 241 | ))); 242 | } 243 | 244 | debug!( 245 | data = serde_json::to_string(&data) 246 | .expect("cannot convert data back to json") 247 | .as_str(), 248 | "setting policy data" 249 | ); 250 | self.policy.set_data(&mut self.store, &self.memory, data)?; 251 | 252 | debug!( 253 | input = serde_json::to_string(&input) 254 | .expect("cannot convert input back to JSON") 255 | .as_str(), 256 | "attempting evaluation" 257 | ); 258 | self.policy 259 | .evaluate(entrypoint_id, &mut self.store, &self.memory, input) 260 | }) 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /crates/burrego/src/evaluator_builder.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{BurregoError, Result}; 2 | use std::path::{Path, PathBuf}; 3 | use wasmtime::{Engine, Module}; 4 | 5 | use crate::{host_callbacks::HostCallbacks, Evaluator}; 6 | 7 | #[derive(Default)] 8 | pub struct EvaluatorBuilder { 9 | policy_path: Option, 10 | module: Option, 11 | engine: Option, 12 | epoch_deadline: Option, 13 | host_callbacks: Option, 14 | } 15 | 16 | impl EvaluatorBuilder { 17 | #[must_use] 18 | pub fn policy_path(mut self, path: &Path) -> Self { 19 | self.policy_path = Some(path.into()); 20 | self 21 | } 22 | 23 | #[must_use] 24 | pub fn module(mut self, module: Module) -> Self { 25 | self.module = Some(module); 26 | self 27 | } 28 | 29 | #[must_use] 30 | pub fn engine(mut self, engine: &Engine) -> Self { 31 | self.engine = Some(engine.clone()); 32 | self 33 | } 34 | 35 | #[must_use] 36 | pub fn enable_epoch_interruptions(mut self, deadline: u64) -> Self { 37 | self.epoch_deadline = Some(deadline); 38 | self 39 | } 40 | 41 | #[must_use] 42 | pub fn host_callbacks(mut self, host_callbacks: HostCallbacks) -> Self { 43 | self.host_callbacks = Some(host_callbacks); 44 | self 45 | } 46 | 47 | fn validate(&self) -> Result<()> { 48 | if self.policy_path.is_some() && self.module.is_some() { 49 | return Err(BurregoError::EvaluatorBuilderError( 50 | "policy_path and module cannot be set at the same time".to_string(), 51 | )); 52 | } 53 | if self.policy_path.is_none() && self.module.is_none() { 54 | return Err(BurregoError::EvaluatorBuilderError( 55 | "Either policy_path or module must be set".to_string(), 56 | )); 57 | } 58 | 59 | if self.host_callbacks.is_none() { 60 | return Err(BurregoError::EvaluatorBuilderError( 61 | "host_callbacks must be set".to_string(), 62 | )); 63 | } 64 | 65 | Ok(()) 66 | } 67 | 68 | pub fn build(&self) -> Result { 69 | self.validate()?; 70 | 71 | let engine = match &self.engine { 72 | Some(e) => e.clone(), 73 | None => { 74 | let mut config = wasmtime::Config::default(); 75 | if self.epoch_deadline.is_some() { 76 | config.epoch_interruption(true); 77 | } 78 | Engine::new(&config).map_err(|e| { 79 | BurregoError::WasmEngineError(format!("cannot create wasmtime Engine: {e:?}")) 80 | })? 81 | } 82 | }; 83 | 84 | let module = match &self.module { 85 | Some(m) => m.clone(), 86 | None => Module::from_file( 87 | &engine, 88 | self.policy_path.clone().expect("policy_path should be set"), 89 | ) 90 | .map_err(|e| { 91 | BurregoError::WasmEngineError(format!("cannot create wasmtime Module: {e:?}")) 92 | })?, 93 | }; 94 | 95 | let host_callbacks = self 96 | .host_callbacks 97 | .clone() 98 | .expect("host callbacks should be set"); 99 | 100 | Evaluator::from_engine_and_module(engine, module, host_callbacks, self.epoch_deadline) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/burrego/src/host_callbacks.rs: -------------------------------------------------------------------------------- 1 | /// HostCallback is a type that references a pointer to a function 2 | /// that can be stored and then invoked by burrego when the Open 3 | /// Policy Agent Wasm target invokes certain Wasm imports. 4 | pub type HostCallback = fn(&str); 5 | 6 | /// HostCallbacks defines a set of pluggable host implementations of 7 | /// OPA documented imports: 8 | /// 9 | #[derive(Clone)] 10 | pub struct HostCallbacks { 11 | pub opa_abort: HostCallback, 12 | pub opa_println: HostCallback, 13 | } 14 | 15 | impl Default for HostCallbacks { 16 | fn default() -> HostCallbacks { 17 | HostCallbacks { 18 | opa_abort: default_opa_abort, 19 | opa_println: default_opa_println, 20 | } 21 | } 22 | } 23 | 24 | fn default_opa_abort(msg: &str) { 25 | eprintln!("OPA abort with message: {msg:?}"); 26 | } 27 | 28 | fn default_opa_println(msg: &str) { 29 | println!("Message coming from the policy: {msg:?}"); 30 | } 31 | -------------------------------------------------------------------------------- /crates/burrego/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod builtins; 2 | pub mod errors; 3 | mod evaluator; 4 | mod evaluator_builder; 5 | pub mod host_callbacks; 6 | mod opa_host_functions; 7 | mod policy; 8 | mod stack_helper; 9 | 10 | pub use builtins::get_builtins; 11 | pub use evaluator::Evaluator; 12 | pub use evaluator_builder::EvaluatorBuilder; 13 | pub use host_callbacks::HostCallbacks; 14 | -------------------------------------------------------------------------------- /crates/burrego/src/stack_helper.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{BurregoError, Result}; 2 | use crate::host_callbacks; 3 | 4 | use std::collections::HashMap; 5 | use std::convert::TryInto; 6 | use wasmtime::{AsContext, AsContextMut, Instance, Memory, TypedFunc}; 7 | 8 | /// StackHelper provides a set of helper methods to share data 9 | /// between the host and the Rego Wasm guest 10 | #[derive(Clone)] 11 | pub(crate) struct StackHelper { 12 | pub(crate) opa_json_dump_fn: TypedFunc, 13 | pub(crate) opa_malloc_fn: TypedFunc, 14 | pub(crate) opa_json_parse_fn: TypedFunc<(i32, i32), i32>, 15 | 16 | pub(crate) opa_abort_host_callback: host_callbacks::HostCallback, 17 | pub(crate) opa_println_host_callback: host_callbacks::HostCallback, 18 | 19 | pub(crate) builtins: HashMap, 20 | } 21 | 22 | impl StackHelper { 23 | pub fn new( 24 | instance: &Instance, 25 | memory: &Memory, 26 | mut store: impl AsContextMut, 27 | opa_abort_host_callback: host_callbacks::HostCallback, 28 | opa_println_host_callback: host_callbacks::HostCallback, 29 | ) -> Result { 30 | let opa_json_dump_fn = instance 31 | .get_typed_func::(store.as_context_mut(), "opa_json_dump") 32 | .map_err(|e| { 33 | BurregoError::RegoWasmError(format!("cannot access opa_json_dump function: {e:?}")) 34 | })?; 35 | let opa_malloc_fn = instance 36 | .get_typed_func::(store.as_context_mut(), "opa_malloc") 37 | .map_err(|e| { 38 | BurregoError::RegoWasmError(format!("Cannot access opa_malloc function: {e:?}")) 39 | })?; 40 | let opa_json_parse_fn = instance 41 | .get_typed_func::<(i32, i32), i32>(store.as_context_mut(), "opa_json_parse") 42 | .map_err(|e| { 43 | BurregoError::RegoWasmError(format!("Cannot access opa_json_parse function: {e:?}")) 44 | })?; 45 | 46 | let builtins_fn = instance 47 | .get_typed_func::<(), i32>(store.as_context_mut(), "builtins") 48 | .map_err(|e| { 49 | BurregoError::RegoWasmError(format!("cannot access builtins function: {e:?}")) 50 | })?; 51 | let addr = builtins_fn.call(store.as_context_mut(), ()).map_err(|e| { 52 | BurregoError::WasmEngineError(format!("cannot invoke builtins function: {e:?}")) 53 | })?; 54 | 55 | let builtins: HashMap = 56 | StackHelper::pull_json(store, memory, &opa_json_dump_fn, addr)? 57 | .as_object() 58 | .ok_or_else(|| { 59 | BurregoError::RegoWasmError( 60 | "OPA builtins didn't return a dictionary".to_string(), 61 | ) 62 | })? 63 | .iter() 64 | .map(|(k, v)| { 65 | let id = v.as_i64().unwrap() as i32; 66 | let builtin = String::from(k.as_str()); 67 | (id, builtin) 68 | }) 69 | .collect(); 70 | 71 | Ok(StackHelper { 72 | opa_json_dump_fn, 73 | opa_malloc_fn, 74 | opa_json_parse_fn, 75 | builtins, 76 | opa_abort_host_callback, 77 | opa_println_host_callback, 78 | }) 79 | } 80 | 81 | /// Read a string from the Wasm guest into the host 82 | /// # Arguments 83 | /// * `store` - the Store associated with the Wasm instance 84 | /// * `memory` - the Wasm linear memory used by the Wasm Instance 85 | /// * `addr` - address inside of the Wasm linear memory where the value is stored 86 | /// # Returns 87 | /// * The data read 88 | pub fn read_string(store: impl AsContext, memory: &Memory, addr: i32) -> Result> { 89 | let mut buffer: [u8; 1] = [0u8]; 90 | let mut data: Vec = vec![]; 91 | let mut raw_addr = addr; 92 | 93 | loop { 94 | memory 95 | .read(&store, raw_addr.try_into().unwrap(), &mut buffer) 96 | .map_err(|e| { 97 | BurregoError::WasmEngineError(format!("cannot read from memory: {e:?}")) 98 | })?; 99 | if buffer[0] == 0 { 100 | break; 101 | } 102 | data.push(buffer[0]); 103 | raw_addr += 1; 104 | } 105 | Ok(data) 106 | } 107 | 108 | /// Pull a JSON data from the Wasm guest into the host 109 | /// # Arguments 110 | /// * `store` - the Store associated with the Wasm instance 111 | /// * `memory` - the Wasm linear memory used by the Wasm Instance 112 | /// * `opa_json_dump_fn` - the `opa_json_dump` function exported by the wasm guest 113 | /// * `addr` - address inside of the Wasm linear memory where the value is stored 114 | /// # Returns 115 | /// * The JSON data read 116 | pub fn pull_json( 117 | mut store: impl AsContextMut, 118 | memory: &Memory, 119 | opa_json_dump_fn: &TypedFunc, 120 | addr: i32, 121 | ) -> Result { 122 | let raw_addr = opa_json_dump_fn 123 | .call(store.as_context_mut(), addr) 124 | .map_err(|e| { 125 | BurregoError::WasmEngineError(format!( 126 | "cannot invoke opa_json_dump function: {e:?}" 127 | )) 128 | })?; 129 | let data = StackHelper::read_string(store, memory, raw_addr)?; 130 | 131 | serde_json::from_slice(&data).map_err(|e| BurregoError::JSONError { 132 | msg: "cannot convert data read from memory into utf8 String".to_string(), 133 | source: e, 134 | }) 135 | } 136 | 137 | /// Push a JSON data from the host into the Wasm guest 138 | /// # Arguments 139 | /// * `store` - the Store associated with the Wasm instance 140 | /// * `memory` - the Wasm linear memory used by the Wasm Instance 141 | /// * `opa_malloc_fn` - the `opa_malloc` function exported by the wasm guest 142 | /// * `opa_json_parse_fn` - the `opa_json_parse` function exported by the wasm guest 143 | /// * `value` - the JSON data to push into the Wasm guest 144 | /// # Returns 145 | /// * Address inside of the Wasm linear memory where the value has been stored 146 | pub fn push_json( 147 | mut store: impl AsContextMut, 148 | memory: &Memory, 149 | opa_malloc_fn: &TypedFunc, 150 | opa_json_parse_fn: &TypedFunc<(i32, i32), i32>, 151 | value: &serde_json::Value, 152 | ) -> Result { 153 | let data = serde_json::to_vec(&value).map_err(|e| BurregoError::JSONError { 154 | msg: "push_json".to_string(), 155 | source: e, 156 | })?; 157 | 158 | StackHelper::push_json_raw( 159 | store.as_context_mut(), 160 | memory, 161 | opa_malloc_fn, 162 | opa_json_parse_fn, 163 | &data, 164 | ) 165 | } 166 | 167 | /// Push a JSON data from the host into the Wasm guest 168 | /// # Arguments 169 | /// * `store` - the Store associated with the Wasm instance 170 | /// * `memory` - the Wasm linear memory used by the Wasm Instance 171 | /// * `opa_malloc_fn` - the `opa_malloc` function exported by the wasm guest 172 | /// * `opa_json_parse_fn` - the `opa_json_parse` function exported by the wasm guest 173 | /// * `value` - the JSON data to push into the Wasm guest 174 | /// # Returns 175 | /// * Address inside of the Wasm linear memory where the value has been stored 176 | pub fn push_json_raw( 177 | mut store: impl AsContextMut, 178 | memory: &Memory, 179 | opa_malloc_fn: &TypedFunc, 180 | opa_json_parse_fn: &TypedFunc<(i32, i32), i32>, 181 | data: &[u8], 182 | ) -> Result { 183 | let data_size: i32 = data.len().try_into().map_err(|_| { 184 | BurregoError::RegoWasmError("push_json: cannot convert size to json".to_string()) 185 | })?; 186 | 187 | // allocate memory to fit the value 188 | let raw_addr = opa_malloc_fn 189 | .call(store.as_context_mut(), data_size) 190 | .map_err(|e| { 191 | BurregoError::WasmEngineError(format!( 192 | "push_json: cannot invoke opa_malloc function: {e:?}" 193 | )) 194 | })?; 195 | memory 196 | .write(store.as_context_mut(), raw_addr.try_into().unwrap(), data) 197 | .map_err(|e| { 198 | BurregoError::WasmEngineError(format!("push_json: cannot write to memory: {e:?}")) 199 | })?; 200 | 201 | match opa_json_parse_fn.call(store.as_context_mut(), (raw_addr, data_size)) { 202 | Ok(0) => Err(BurregoError::RegoWasmError( 203 | "Failed to load json in memory".to_string(), 204 | )), 205 | Ok(addr) => Ok(addr), 206 | Err(e) => Err(BurregoError::RegoWasmError(format!( 207 | "Cannot get memory address: {e:?}" 208 | ))), 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /crates/burrego/test_data/gatekeeper/Makefile: -------------------------------------------------------------------------------- 1 | test: policy.wasm 2 | bats e2e.bats 3 | 4 | policy.wasm: policy.rego 5 | opa build -t wasm -e policy/violation -o policy.tar.gz policy.rego 6 | tar -xf policy.tar.gz /policy.wasm 7 | rm policy.tar.gz 8 | 9 | clean: 10 | rm -f *.wasm *.tar.gz 11 | -------------------------------------------------------------------------------- /crates/burrego/test_data/gatekeeper/e2e.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "[accept in namespace]: valid namespace" { 4 | run cargo run --example cli -- -v eval policy.wasm --input-path request-valid.json 5 | # this prints the output when one the checks below fails 6 | echo "output = ${output}" 7 | 8 | # request accepted 9 | [ "$status" -eq 0 ] 10 | [ $(expr "$output" : '.*"result":.*\[\]') -ne 0 ] 11 | } 12 | 13 | @test "[accept in namespace]: not valid namespace" { 14 | run cargo run --example cli -- -v eval policy.wasm --input-path request-not-valid.json 15 | # this prints the output when one the checks below fails 16 | echo "output = ${output}" 17 | 18 | # request accepted 19 | [ "$status" -eq 0 ] 20 | [ $(expr "$output" : '.*"msg": "object created under an invalid namespace kube-system; allowed namespaces are \[default test\]"') -ne 0 ] 21 | } 22 | -------------------------------------------------------------------------------- /crates/burrego/test_data/gatekeeper/policy.rego: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | violation[{"msg": msg}] { 4 | object_namespace := input.review.object.metadata.namespace 5 | satisfied := [allowed_namespace | namespace = input.parameters.allowed_namespaces[_]; allowed_namespace = object_namespace == namespace] 6 | not any(satisfied) 7 | msg := sprintf("object created under an invalid namespace %s; allowed namespaces are %v", [object_namespace, input.parameters.allowed_namespaces]) 8 | } 9 | -------------------------------------------------------------------------------- /crates/burrego/test_data/gatekeeper/request-not-valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "parameters": { 3 | "allowed_namespaces": [ 4 | "default", 5 | "test" 6 | ] 7 | }, 8 | "review": { 9 | "uid": "1299d386-525b-4032-98ae-1949f69f9cfc", 10 | "kind": { 11 | "group": "networking.k8s.io", 12 | "kind": "Ingress", 13 | "version": "v1" 14 | }, 15 | "object": { 16 | "apiVersion": "networking.k8s.io/v1", 17 | "kind": "Ingress", 18 | "metadata": { 19 | "name": "ingress-wildcard-host", 20 | "namespace": "kube-system" 21 | }, 22 | "spec": { 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/burrego/test_data/gatekeeper/request-valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "parameters": { 3 | "allowed_namespaces": [ 4 | "default", 5 | "test" 6 | ] 7 | }, 8 | "review": { 9 | "uid": "1299d386-525b-4032-98ae-1949f69f9cfc", 10 | "kind": { 11 | "group": "networking.k8s.io", 12 | "kind": "Ingress", 13 | "version": "v1" 14 | }, 15 | "object": { 16 | "apiVersion": "networking.k8s.io/v1", 17 | "kind": "Ingress", 18 | "metadata": { 19 | "name": "ingress-wildcard-host", 20 | "namespace": "default" 21 | }, 22 | "spec": { 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/burrego/test_data/trace/Makefile: -------------------------------------------------------------------------------- 1 | test: policy.wasm 2 | bats e2e.bats 3 | 4 | policy.wasm: policy.rego 5 | opa build -t wasm -e policy/main -o policy.tar.gz policy.rego 6 | tar -xf policy.tar.gz /policy.wasm 7 | rm policy.tar.gz 8 | 9 | clean: 10 | rm -f *.wasm *.tar.gz 11 | -------------------------------------------------------------------------------- /crates/burrego/test_data/trace/e2e.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "input message is not valid" { 4 | run cargo run --example cli -- -v eval policy.wasm -i '{ "message": "mondo" }' 5 | # this prints the output when one the checks below fails 6 | echo "output = ${output}" 7 | 8 | # request rejected 9 | [ "$status" -eq 0 ] 10 | [ $(expr "$output" : '.*"result":.*false') -ne 0 ] 11 | [ $(expr "$output" : ".*input\.message has been set to 'mondo'") -ne 0 ] 12 | } 13 | 14 | @test "input message is valid" { 15 | run cargo run --example cli -- -v eval policy.wasm -i '{ "message": "world" }' 16 | # this prints the output when one the checks below fails 17 | echo "output = ${output}" 18 | 19 | # request rejected 20 | [ "$status" -eq 0 ] 21 | [ $(expr "$output" : '.*"result":.*true') -ne 0 ] 22 | [ $(expr "$output" : ".*input\.message has been set to 'world'") -ne 0 ] 23 | } 24 | -------------------------------------------------------------------------------- /crates/burrego/test_data/trace/policy.rego: -------------------------------------------------------------------------------- 1 | package policy 2 | 3 | default main = false 4 | 5 | main { 6 | trace(sprintf("input.message has been set to '%v'", [input.message])); 7 | m := input.message; 8 | m == "world" 9 | } 10 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>kubewarden/github-actions//renovate-config/default" 4 | ], 5 | "packageRules": [ 6 | { 7 | "description": "Update all wasmtime packages together", 8 | "matchPackageNames": ["wasmtime-wasi", "wasmtime", "wasi-common"], 9 | "groupName": "wasmtime" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | 2 | [toolchain] 3 | channel = "1.87.0" 4 | components = ["clippy", "rust-analyzer", "rustfmt"] 5 | profile = "minimal" 6 | -------------------------------------------------------------------------------- /src/admission_request.rs: -------------------------------------------------------------------------------- 1 | /// This models the admission/v1/AdmissionRequest object of Kubernetes 2 | #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] 3 | #[serde(rename_all = "camelCase")] 4 | pub struct AdmissionRequest { 5 | pub uid: String, 6 | pub kind: GroupVersionKind, 7 | pub resource: GroupVersionResource, 8 | #[serde(skip_serializing_if = "Option::is_none")] 9 | pub sub_resource: Option, 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub request_kind: Option, 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub request_resource: Option, 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub request_sub_resource: Option, 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | pub name: Option, 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub namespace: Option, 20 | pub operation: String, 21 | pub user_info: k8s_openapi::api::authentication::v1::UserInfo, 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | pub object: Option, 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | pub old_object: Option, 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | pub dry_run: Option, 28 | #[serde(skip_serializing_if = "Option::is_none")] 29 | pub options: Option, 30 | } 31 | 32 | #[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize, Eq, PartialEq)] 33 | pub struct GroupVersionKind { 34 | pub group: String, 35 | pub version: String, 36 | pub kind: String, 37 | } 38 | 39 | #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] 40 | pub struct GroupVersionResource { 41 | pub group: String, 42 | pub version: String, 43 | pub resource: String, 44 | } 45 | -------------------------------------------------------------------------------- /src/callback_handler/builder.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use policy_fetcher::sigstore::trust::ManualTrustRoot; 3 | use policy_fetcher::sources::Sources; 4 | use std::sync::Arc; 5 | use tokio::sync::{mpsc, oneshot}; 6 | 7 | use super::CallbackHandler; 8 | use super::{oci, sigstore_verification}; 9 | use crate::callback_requests::CallbackRequest; 10 | 11 | const DEFAULT_CHANNEL_BUFF_SIZE: usize = 100; 12 | 13 | /// Helper struct that creates CallbackHandler objects 14 | pub struct CallbackHandlerBuilder { 15 | oci_sources: Option, 16 | channel_buffer_size: usize, 17 | shutdown_channel: oneshot::Receiver<()>, 18 | trust_root: Option>>, 19 | kube_client: Option, 20 | } 21 | 22 | impl CallbackHandlerBuilder { 23 | pub fn new(shutdown_channel: oneshot::Receiver<()>) -> Self { 24 | CallbackHandlerBuilder { 25 | oci_sources: None, 26 | shutdown_channel, 27 | channel_buffer_size: DEFAULT_CHANNEL_BUFF_SIZE, 28 | trust_root: None, 29 | kube_client: None, 30 | } 31 | } 32 | 33 | /// Provide all the information needed to access OCI registries. Optional 34 | pub fn registry_config(mut self, sources: Option) -> Self { 35 | self.oci_sources = sources; 36 | self 37 | } 38 | 39 | pub fn trust_root(mut self, trust_root: Option>>) -> Self { 40 | self.trust_root = trust_root; 41 | self 42 | } 43 | 44 | /// Set the size of the channel used by the sync world to communicate with 45 | /// the CallbackHandler. Optional 46 | pub fn channel_buffer_size(mut self, size: usize) -> Self { 47 | self.channel_buffer_size = size; 48 | self 49 | } 50 | 51 | /// Set the `kube::Client` to be used by context aware policies. 52 | /// Optional, but strongly recommended to have context aware policies 53 | /// work as expected 54 | pub fn kube_client(mut self, client: kube::Client) -> Self { 55 | self.kube_client = Some(client); 56 | self 57 | } 58 | 59 | /// Create a CallbackHandler object 60 | pub async fn build(self) -> Result { 61 | let (tx, rx) = mpsc::channel::(self.channel_buffer_size); 62 | let oci_client = Arc::new(oci::Client::new(self.oci_sources.clone())); 63 | let sigstore_client = 64 | sigstore_verification::Client::new(self.oci_sources.clone(), self.trust_root.clone()) 65 | .await? 66 | .to_owned(); 67 | 68 | let kubernetes_client = self.kube_client.map(super::kubernetes::Client::new); 69 | 70 | Ok(CallbackHandler { 71 | oci_client, 72 | sigstore_client, 73 | kubernetes_client, 74 | tx, 75 | rx, 76 | shutdown_channel: self.shutdown_channel, 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/callback_handler/kubernetes.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod reflector; 3 | 4 | use anyhow::{anyhow, Result}; 5 | use cached::proc_macro::cached; 6 | use kube::core::ObjectList; 7 | use serde::Serialize; 8 | 9 | pub(crate) use client::Client; 10 | 11 | #[derive(Eq, Hash, PartialEq)] 12 | struct ApiVersionKind { 13 | api_version: String, 14 | kind: String, 15 | } 16 | 17 | #[derive(Debug, Clone, Serialize)] 18 | pub(crate) struct KubeResource { 19 | pub resource: kube::api::ApiResource, 20 | pub namespaced: bool, 21 | } 22 | 23 | pub(crate) async fn list_resources_by_namespace( 24 | client: Option<&mut Client>, 25 | api_version: &str, 26 | kind: &str, 27 | namespace: &str, 28 | label_selector: Option, 29 | field_selector: Option, 30 | ) -> Result>> { 31 | if client.is_none() { 32 | return Err(anyhow!("kube::Client was not initialized properly")).map(cached::Return::new); 33 | } 34 | 35 | client 36 | .unwrap() 37 | .list_resources_by_namespace(api_version, kind, namespace, label_selector, field_selector) 38 | .await 39 | .map(cached::Return::new) 40 | } 41 | 42 | pub(crate) async fn list_resources_all( 43 | client: Option<&mut Client>, 44 | api_version: &str, 45 | kind: &str, 46 | label_selector: Option, 47 | field_selector: Option, 48 | ) -> Result>> { 49 | if client.is_none() { 50 | return Err(anyhow!("kube::Client was not initialized properly")).map(cached::Return::new); 51 | } 52 | 53 | client 54 | .unwrap() 55 | .list_resources_all(api_version, kind, label_selector, field_selector) 56 | .await 57 | .map(cached::Return::new) 58 | } 59 | 60 | pub(crate) async fn get_resource( 61 | client: Option<&mut Client>, 62 | api_version: &str, 63 | kind: &str, 64 | name: &str, 65 | namespace: Option<&str>, 66 | ) -> Result> { 67 | if client.is_none() { 68 | return Err(anyhow!("kube::Client was not initialized properly")); 69 | } 70 | 71 | client 72 | .unwrap() 73 | .get_resource(api_version, kind, name, namespace) 74 | .await 75 | .map(|value| cached::Return { 76 | was_cached: false, 77 | value, 78 | }) 79 | } 80 | 81 | #[cached( 82 | time = 5, 83 | result = true, 84 | sync_writes = "default", 85 | key = "String", 86 | convert = r#"{ format!("get_resource_cached({},{}),{},{:?}", api_version, kind, name, namespace) }"#, 87 | with_cached_flag = true 88 | )] 89 | pub(crate) async fn get_resource_cached( 90 | client: Option<&mut Client>, 91 | api_version: &str, 92 | kind: &str, 93 | name: &str, 94 | namespace: Option<&str>, 95 | ) -> Result> { 96 | get_resource(client, api_version, kind, name, namespace).await 97 | } 98 | 99 | pub(crate) async fn get_resource_plural_name( 100 | client: Option<&mut Client>, 101 | api_version: &str, 102 | kind: &str, 103 | ) -> Result> { 104 | if client.is_none() { 105 | return Err(anyhow!("kube::Client was not initialized properly")); 106 | } 107 | 108 | client 109 | .unwrap() 110 | .get_resource_plural_name(api_version, kind) 111 | .await 112 | .map(|value| cached::Return { 113 | // this is always cached, because the client builds an overview of 114 | // the cluster resources at bootstrap time 115 | was_cached: true, 116 | value, 117 | }) 118 | } 119 | 120 | /// Check if the results of the "list all resources" query have changed since the provided instant 121 | /// This is done by querying the reflector that keeps track of this query 122 | pub(crate) async fn has_list_resources_all_result_changed_since_instant( 123 | client: Option<&mut Client>, 124 | api_version: &str, 125 | kind: &str, 126 | label_selector: Option, 127 | field_selector: Option, 128 | since: tokio::time::Instant, 129 | ) -> Result> { 130 | if client.is_none() { 131 | return Err(anyhow!("kube::Client was not initialized properly")).map(cached::Return::new); 132 | } 133 | 134 | client 135 | .unwrap() 136 | .has_list_resources_all_result_changed_since_instant( 137 | api_version, 138 | kind, 139 | label_selector, 140 | field_selector, 141 | since, 142 | ) 143 | .await 144 | .map(cached::Return::new) 145 | } 146 | -------------------------------------------------------------------------------- /src/callback_handler/kubernetes/reflector.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use futures::{future::ready, Stream, StreamExt, TryStreamExt}; 3 | use kube::{runtime::reflector::store, Resource}; 4 | use kube::{ 5 | runtime::{reflector::store::Writer, watcher, WatchStreamExt}, 6 | ResourceExt, 7 | }; 8 | use std::hash::Hash; 9 | use tokio::{sync::watch, time::Instant}; 10 | use tracing::{debug, info, warn}; 11 | 12 | use crate::callback_handler::kubernetes::KubeResource; 13 | 14 | /// Like `kube::runtime::reflector::reflector`, but also sends the time of the last change to a 15 | /// watch channel 16 | pub fn reflector_tracking_changes_instant( 17 | mut writer: store::Writer, 18 | stream: W, 19 | last_change_seen_at: watch::Sender, 20 | ) -> impl Stream 21 | where 22 | K: Resource + Clone, 23 | K::DynamicType: Eq + Hash + Clone, 24 | W: Stream>>, 25 | { 26 | stream.inspect_ok(move |event| { 27 | if let Err(err) = last_change_seen_at.send(Instant::now()) { 28 | warn!(error = ?err, "failed to set last_change_seen_at"); 29 | } 30 | writer.apply_watcher_event(event) 31 | }) 32 | } 33 | 34 | /// A reflector fetches kubernetes objects based on filtering criteria. 35 | /// When created, the list is populated slowly, to prevent hammering the Kubernetes API server. 36 | /// The items are stored in-memory. The `managedFields` attribute is stripped from all the objects 37 | /// to reduce memory consumption. All the other fields are retained. 38 | /// A Kubernetes Watch is then created to keep the contents of the list updated. 39 | /// 40 | /// This is code relies heavily on the `kube::runtime::reflector` module. 41 | /// 42 | /// ## Stale date 43 | /// 44 | /// There's always some delay involved with Kubernetes notifications. That depends on 45 | /// different factors like: the load on the Kubernetes API server, the number of watchers to be 46 | /// notifies,... That means, changes are not propagated immediately, hence the cache can have stale 47 | /// data. 48 | /// 49 | /// Finally, when started, the Reflector takes some time to make the loaded data available to 50 | /// consumers. 51 | pub(crate) struct Reflector { 52 | /// Read-only access to the data cached by the Reflector 53 | pub reader: kube::runtime::reflector::Store, 54 | last_change_seen_at: watch::Receiver, 55 | } 56 | 57 | impl Reflector { 58 | /// Compute a unique identifier for the Reflector. This is used to prevent the creation of two 59 | /// Reflectors watching the same set of resources. 60 | pub fn compute_id( 61 | resource: &KubeResource, 62 | namespace: Option<&str>, 63 | label_selector: Option<&str>, 64 | field_selector: Option<&str>, 65 | ) -> String { 66 | format!( 67 | "{}|{}|{namespace:?}|{label_selector:?}|{field_selector:?}", 68 | resource.resource.api_version, resource.resource.kind 69 | ) 70 | } 71 | 72 | /// Create the reflector and start a tokio task in the background that keeps 73 | /// the contents of the Reflector updated 74 | pub async fn create_and_run( 75 | kube_client: kube::Client, 76 | resource: KubeResource, 77 | namespace: Option, 78 | label_selector: Option, 79 | field_selector: Option, 80 | ) -> Result { 81 | let group = resource.resource.group.clone(); 82 | let version = resource.resource.version.clone(); 83 | let kind = resource.resource.kind.clone(); 84 | 85 | info!( 86 | group, 87 | version, 88 | kind, 89 | ?namespace, 90 | ?label_selector, 91 | ?field_selector, 92 | "creating new reflector" 93 | ); 94 | 95 | let api = match namespace { 96 | Some(ref ns) => kube::api::Api::::namespaced_with( 97 | kube_client, 98 | ns, 99 | &resource.resource, 100 | ), 101 | None => kube::api::Api::::all_with( 102 | kube_client, 103 | &resource.resource, 104 | ), 105 | }; 106 | 107 | let writer = Writer::new(resource.resource); 108 | let reader = writer.as_reader(); 109 | 110 | let filter = watcher::Config { 111 | label_selector: label_selector.clone(), 112 | field_selector: field_selector.clone(), 113 | ..Default::default() 114 | }; 115 | let stream = watcher(api, filter).map_ok(|ev| { 116 | ev.modify(|obj| { 117 | // clear managed fields to reduce memory usage 118 | obj.managed_fields_mut().clear(); 119 | // clear last-applied-configuration to reduce memory usage 120 | obj.annotations_mut() 121 | .remove("kubectl.kubernetes.io/last-applied-configuration"); 122 | }) 123 | }); 124 | 125 | // this is a watch channel that tracks the last time the reflector saw a change 126 | let (updated_at_watch_tx, updated_at_watch_rx) = watch::channel(Instant::now()); 127 | 128 | let rf = reflector_tracking_changes_instant(writer, stream, updated_at_watch_tx); 129 | 130 | tokio::spawn(async move { 131 | let infinite_watch = rf.default_backoff().touched_objects().for_each(|obj| { 132 | match obj { 133 | Ok(o) => debug!( 134 | group, 135 | version, 136 | kind, 137 | ?namespace, 138 | ?label_selector, 139 | ?field_selector, 140 | object=?o, 141 | "watcher saw object" 142 | ), 143 | Err(e) => warn!( 144 | group, 145 | version, 146 | kind, 147 | ?namespace, 148 | ?label_selector, 149 | ?field_selector, 150 | error=?e, 151 | "watcher error" 152 | ), 153 | }; 154 | ready(()) 155 | }); 156 | infinite_watch.await 157 | }); 158 | 159 | reader.wait_until_ready().await?; 160 | 161 | Ok(Reflector { 162 | reader, 163 | last_change_seen_at: updated_at_watch_rx, 164 | }) 165 | } 166 | 167 | /// Get the last time a change was seen by the reflector 168 | pub async fn last_change_seen_at(&self) -> Instant { 169 | *self.last_change_seen_at.borrow() 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/callback_handler/oci.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cached::proc_macro::cached; 3 | use kubewarden_policy_sdk::host_capabilities::oci::ManifestDigestResponse; 4 | use policy_fetcher::{ 5 | oci_client::{ 6 | manifest::{OciImageManifest, OciManifest}, 7 | Reference, 8 | }, 9 | registry::Registry, 10 | sources::Sources, 11 | }; 12 | use serde::{Deserialize, Serialize}; 13 | 14 | /// Helper struct to interact with an OCI registry 15 | pub(crate) struct Client { 16 | sources: Option, 17 | registry: Registry, 18 | } 19 | 20 | #[derive(Serialize, Deserialize, Debug, Clone)] 21 | pub struct ManifestAndConfigResponse { 22 | pub manifest: OciImageManifest, 23 | pub digest: String, 24 | pub config: serde_json::Value, 25 | } 26 | 27 | impl Client { 28 | pub fn new(sources: Option) -> Self { 29 | let registry = Registry {}; 30 | Client { sources, registry } 31 | } 32 | 33 | /// Fetch the manifest digest of the OCI resource referenced via `image` 34 | pub async fn digest(&self, image: &str) -> Result { 35 | // this is needed to expand names as `busybox` into 36 | // fully resolved references like `docker.io/library/busybox` 37 | let image_ref: Reference = image.parse()?; 38 | 39 | let image_with_proto = format!("registry://{}", image_ref.whole()); 40 | let image_digest = self 41 | .registry 42 | .manifest_digest(&image_with_proto, self.sources.as_ref()) 43 | .await?; 44 | 45 | Ok(image_digest) 46 | } 47 | 48 | pub async fn manifest(&self, image: &str) -> Result { 49 | // this is needed to expand names as `busybox` into 50 | // fully resolved references like `docker.io/library/busybox` 51 | let image_ref: Reference = image.parse()?; 52 | 53 | let image_with_proto = format!("registry://{}", image_ref.whole()); 54 | let manifest = self 55 | .registry 56 | .manifest(&image_with_proto, self.sources.as_ref()) 57 | .await?; 58 | Ok(manifest) 59 | } 60 | 61 | pub async fn manifest_and_config(&self, image: &str) -> Result { 62 | // this is needed to expand names as `busybox` into 63 | // fully resolved references like `docker.io/library/busybox` 64 | let image_ref: Reference = image.parse()?; 65 | let image_with_proto = format!("registry://{}", image_ref.whole()); 66 | let (manifest, digest, config) = self 67 | .registry 68 | .manifest_and_config(&image_with_proto, self.sources.as_ref()) 69 | .await?; 70 | Ok(ManifestAndConfigResponse { 71 | manifest, 72 | digest, 73 | config, 74 | }) 75 | } 76 | } 77 | 78 | // Interacting with a remote OCI registry is time expensive, this can cause a massive slow down 79 | // of policy evaluations, especially inside of PolicyServer. 80 | // Because of that we will keep a cache of the digests results. 81 | // 82 | // Details about this cache: 83 | // * only the image "url" is used as key. oci::Client is not hashable, plus 84 | // the client is always the same 85 | // * the cache is time bound: cached values are purged after 60 seconds 86 | // * only successful results are cached 87 | #[cached( 88 | time = 60, 89 | result = true, 90 | sync_writes = "default", 91 | key = "String", 92 | convert = r#"{ format!("{}", img) }"#, 93 | with_cached_flag = true 94 | )] 95 | pub(crate) async fn get_oci_digest_cached( 96 | oci_client: &Client, 97 | img: &str, 98 | ) -> Result> { 99 | oci_client 100 | .digest(img) 101 | .await 102 | .map(|digest| ManifestDigestResponse { digest }) 103 | .map(cached::Return::new) 104 | } 105 | 106 | // Interacting with a remote OCI registry is time expensive, this can cause a massive slow down 107 | // of policy evaluations, especially inside of PolicyServer. 108 | // Because of that we will keep a cache of the manifest results. 109 | // 110 | // Details about this cache: 111 | // * only the image "url" is used as key. oci::Client is not hashable, plus 112 | // the client is always the same 113 | // * the cache is time bound: cached values are purged after 60 seconds 114 | // * only successful results are cached 115 | #[cached( 116 | time = 60, 117 | result = true, 118 | sync_writes = "default", 119 | key = "String", 120 | convert = r#"{ format!("{}", img) }"#, 121 | with_cached_flag = true 122 | )] 123 | pub(crate) async fn get_oci_manifest_cached( 124 | oci_client: &Client, 125 | img: &str, 126 | ) -> Result> { 127 | oci_client.manifest(img).await.map(cached::Return::new) 128 | } 129 | 130 | #[cached( 131 | time = 60, 132 | result = true, 133 | sync_writes = "default", 134 | key = "String", 135 | convert = r#"{ format!("{}", img) }"#, 136 | with_cached_flag = true 137 | )] 138 | pub(crate) async fn get_oci_manifest_and_config_cached( 139 | oci_client: &Client, 140 | img: &str, 141 | ) -> Result> { 142 | oci_client 143 | .manifest_and_config(img) 144 | .await 145 | .map(cached::Return::new) 146 | } 147 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const KUBEWARDEN_CUSTOM_SECTION_METADATA: &str = "io.kubewarden.metadata"; 2 | 3 | pub const KUBEWARDEN_ANNOTATION_POLICY_TITLE: &str = "io.kubewarden.policy.title"; 4 | pub const KUBEWARDEN_ANNOTATION_POLICY_VERSION: &str = "io.kubewarden.policy.version"; 5 | pub const KUBEWARDEN_ANNOTATION_POLICY_DESCRIPTION: &str = "io.kubewarden.policy.description"; 6 | pub const KUBEWARDEN_ANNOTATION_POLICY_AUTHOR: &str = "io.kubewarden.policy.author"; 7 | pub const KUBEWARDEN_ANNOTATION_POLICY_URL: &str = "io.kubewarden.policy.url"; 8 | pub const KUBEWARDEN_ANNOTATION_POLICY_OCIURL: &str = "io.kubewarden.policy.ociUrl"; 9 | pub const KUBEWARDEN_ANNOTATION_POLICY_SOURCE: &str = "io.kubewarden.policy.source"; 10 | pub const KUBEWARDEN_ANNOTATION_POLICY_LICENSE: &str = "io.kubewarden.policy.license"; 11 | pub const KUBEWARDEN_ANNOTATION_POLICY_USAGE: &str = "io.kubewarden.policy.usage"; 12 | pub const KUBEWARDEN_ANNOTATION_POLICY_SEVERITY: &str = "io.kubewarden.policy.severity"; 13 | pub const KUBEWARDEN_ANNOTATION_POLICY_CATEGORY: &str = "io.kubewarden.policy.category"; 14 | 15 | pub const KUBEWARDEN_ANNOTATION_GITHUB_RELEASE_TAG: &str = "com.github.release.tag"; 16 | 17 | pub const KUBEWARDEN_ANNOTATION_ARTIFACTHUB_RESOURCES: &str = "io.artifacthub.resources"; 18 | pub const KUBEWARDEN_ANNOTATION_ARTIFACTHUB_DISPLAYNAME: &str = "io.artifacthub.displayName"; 19 | pub const KUBEWARDEN_ANNOTATION_ARTIFACTHUB_KEYWORDS: &str = "io.artifacthub.keywords"; 20 | pub const KUBEWARDEN_ANNOTATION_ARTIFACTHUB_HIDDENUI: &str = "io.kubewarden.hidden-ui"; 21 | 22 | pub const KUBEWARDEN_ANNOTATION_KWCTL_VERSION: &str = "io.kubewarden.kwctl"; 23 | 24 | pub const ARTIFACTHUB_ANNOTATION_KUBEWARDEN_MUTATION: &str = "kubewarden/mutation"; 25 | pub const ARTIFACTHUB_ANNOTATION_KUBEWARDEN_CONTEXTAWARE_RESOURCES: &str = 26 | "kubewarden/contextAwareResources"; 27 | pub const ARTIFACTHUB_ANNOTATION_KUBEWARDEN_RESOURCES: &str = "kubewarden/resources"; 28 | pub const ARTIFACTHUB_ANNOTATION_RANCHER_HIDDENUI: &str = "kubewarden/hidden-ui"; 29 | pub const ARTIFACTHUB_ANNOTATION_KUBEWARDEN_RULES: &str = "kubewarden/rules"; 30 | pub const ARTIFACTHUB_ANNOTATION_KUBEWARDEN_QUESTIONSUI: &str = "kubewarden/questions-ui"; 31 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::policy_evaluator::errors::InvalidUserInputError; 4 | 5 | #[derive(Error, Debug, PartialEq, Eq)] 6 | pub enum ArtifactHubError { 7 | #[error("no annotations in policy metadata. policy metadata must specify annotations")] 8 | NoAnnotations, 9 | 10 | #[error("policy version must be in semver: {0}")] 11 | NoSemverVersion(String), 12 | 13 | #[error("questions-ui content cannot be empty")] 14 | EmptyQuestionsUI, 15 | 16 | #[error("policy metadata must specify \"{0}\" in annotations")] 17 | MissingAnnotation(String), 18 | 19 | #[error("annotation \"{annot:?}\" in policy metadata must be a well formed URL: {error:?}")] 20 | MalformedURL { annot: String, error: String }, 21 | 22 | #[error("annotation \"{0}\" in policy metadata is malformed, must be csv values")] 23 | MalformedCSV(String), 24 | 25 | #[error("annotation \"{0}\" in policy metadata is malformed, must be csv values of \"name \"")] 26 | MalformedCSVEmail(String), 27 | 28 | #[error("annotation \"{annot:?}\" in policy metadata must be a well formed email: {error:?}")] 29 | MalformedEmail { annot: String, error: String }, 30 | 31 | #[error("annotation \"{0}\" in policy metadata is malformed, must be a string \"true\" or \"false\"")] 32 | MalformedBoolString(String), 33 | } 34 | 35 | #[derive(Error, Debug)] 36 | pub enum PolicyEvaluatorError { 37 | #[error("protocol_version is only applicable to a Kubewarden policy")] 38 | InvalidProtocolVersion(), 39 | 40 | #[error("protocol_version is only applicable to a Kubewarden policy")] 41 | InvokeWapcProtocolVersion(#[source] crate::runtimes::wapc::errors::WapcRuntimeError), 42 | } 43 | 44 | #[derive(Error, Debug)] 45 | pub enum PolicyEvaluatorBuilderError { 46 | #[error("cannot convert given path to String")] 47 | ConvertPath, 48 | 49 | #[error("protocol_version is only applicable to a Kubewarden policy")] 50 | InvokeWapcProtocolVersion(#[source] crate::runtimes::wapc::errors::WapcRuntimeError), 51 | 52 | #[error(transparent)] 53 | InvalidUserInput(#[from] InvalidUserInputError), 54 | 55 | #[error("error when creating wasmtime engine: {0}")] 56 | WasmtimeEngineBuild(#[source] wasmtime::Error), 57 | 58 | #[error("error when building wasm module: {0}")] 59 | WasmModuleBuild(#[source] wasmtime::Error), 60 | 61 | #[error("error when building wapc precompiled stack: {0}")] 62 | NewWapcStackPre(#[source] crate::runtimes::wapc::errors::WapcRuntimeError), 63 | 64 | #[error("error when building wasi precompiled stack: {0}")] 65 | NewWasiStackPre(#[source] crate::runtimes::wasi_cli::errors::WasiRuntimeError), 66 | 67 | #[error("error when building rego precompiled stack")] 68 | NewRegoStackPre(#[source] wasmtime::Error), 69 | } 70 | 71 | #[derive(Error, Debug)] 72 | pub enum PolicyEvaluatorPreError { 73 | #[error("unable to rehydrate wapc module: {0}")] 74 | RehydrateWapc(#[source] crate::runtimes::wapc::errors::WapcRuntimeError), 75 | 76 | #[error("unable to rehydrate rego module: {0}")] 77 | RehydrateRego(#[source] crate::runtimes::rego::errors::RegoRuntimeError), 78 | } 79 | 80 | #[derive(Error, Debug)] 81 | pub enum MetadataError { 82 | #[error("cannot read metadata from path: {0}")] 83 | Path(#[source] std::io::Error), 84 | 85 | #[error("cannot parse custom section of wasm module: {0}")] 86 | WasmPayload(#[source] wasmparser::BinaryReaderError), 87 | 88 | #[error("cannot deserialize custom section `{section}` of wasm module: {error}")] 89 | Deserialize { 90 | section: String, 91 | #[source] 92 | error: serde_json::Error, 93 | }, 94 | } 95 | 96 | #[derive(Error, Debug)] 97 | pub enum ResponseError { 98 | #[error("cannot deserialize JSONPatch: {0}")] 99 | Deserialize(#[source] serde_json::Error), 100 | } 101 | -------------------------------------------------------------------------------- /src/evaluation_context.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use std::fmt; 3 | use tokio::sync::mpsc; 4 | 5 | use crate::callback_requests::CallbackRequest; 6 | use crate::policy_metadata::ContextAwareResource; 7 | 8 | /// A struct that holds metadata and other data that are needed when a policy 9 | /// is being evaluated 10 | #[derive(Clone, Default)] 11 | pub struct EvaluationContext { 12 | /// The policy identifier. This is mostly relevant for Policy Server, 13 | /// which uses the identifier provided by the user inside of the `policy.yml` 14 | /// file 15 | pub policy_id: String, 16 | 17 | /// Channel used by the synchronous world (like the `host_callback` waPC function, 18 | /// but also Burrego for k8s context aware data), 19 | /// to request the computation of code that can only be run inside of an 20 | /// asynchronous block 21 | pub callback_channel: Option>, 22 | 23 | /// List of ContextAwareResource the policy is granted access to. 24 | pub ctx_aware_resources_allow_list: BTreeSet, 25 | } 26 | 27 | impl EvaluationContext { 28 | /// Checks if a policy has access to a Kubernetes resource, based on the privileges 29 | /// that have been granted by the user 30 | pub(crate) fn can_access_kubernetes_resource(&self, api_version: &str, kind: &str) -> bool { 31 | let wanted_resource = ContextAwareResource { 32 | api_version: api_version.to_string(), 33 | kind: kind.to_string(), 34 | }; 35 | 36 | self.ctx_aware_resources_allow_list 37 | .contains(&wanted_resource) 38 | } 39 | } 40 | 41 | impl fmt::Debug for EvaluationContext { 42 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 43 | let callback_channel = match self.callback_channel { 44 | Some(_) => "Some(...)", 45 | None => "None", 46 | }; 47 | 48 | write!( 49 | f, 50 | r#"EvaluationContext {{ policy_id: "{}", callback_channel: {}, allowed_kubernetes_resources: {:?} }}"#, 51 | self.policy_id, callback_channel, self.ctx_aware_resources_allow_list, 52 | ) 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use super::*; 59 | use rstest::rstest; 60 | 61 | #[rstest] 62 | #[case("nothing allowed", BTreeSet::new(), "v1", "Secret", false)] 63 | #[case( 64 | "try to access denied resource", 65 | BTreeSet::from([ 66 | ContextAwareResource{ 67 | api_version: "v1".to_string(), 68 | kind: "ConfigMap".to_string(), 69 | }]), 70 | "v1", 71 | "Secret", 72 | false, 73 | )] 74 | #[case( 75 | "access allowed resource", 76 | BTreeSet::from([ 77 | ContextAwareResource{ 78 | api_version: "v1".to_string(), 79 | kind: "ConfigMap".to_string(), 80 | }]), 81 | "v1", 82 | "ConfigMap", 83 | true, 84 | )] 85 | 86 | fn can_access_kubernetes_resource( 87 | #[case] name: &str, 88 | #[case] allowed_resources: BTreeSet, 89 | #[case] api_version: &str, 90 | #[case] kind: &str, 91 | #[case] allowed: bool, 92 | ) { 93 | let ctx = EvaluationContext { 94 | policy_id: name.to_string(), 95 | callback_channel: None, 96 | ctx_aware_resources_allow_list: allowed_resources, 97 | }; 98 | 99 | let requested_resource = ContextAwareResource { 100 | api_version: api_version.to_string(), 101 | kind: kind.to_string(), 102 | }; 103 | 104 | assert_eq!( 105 | allowed, 106 | ctx.can_access_kubernetes_resource( 107 | &requested_resource.api_version, 108 | &requested_resource.kind 109 | ) 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub extern crate burrego; 2 | extern crate wasmparser; 3 | 4 | pub mod admission_request; 5 | pub mod admission_response; 6 | pub mod callback_handler; 7 | pub mod callback_requests; 8 | pub mod constants; 9 | pub mod errors; 10 | pub mod evaluation_context; 11 | pub mod policy_artifacthub; 12 | pub mod policy_evaluator; 13 | pub mod policy_group_evaluator; 14 | pub mod policy_metadata; 15 | mod policy_tracing; 16 | pub mod runtimes; 17 | 18 | // API's that expose other crate types (such as Kubewarden Policy SDK 19 | // or `policy_fetcher`) can either implement their own exposed types, 20 | // and means to convert those types internally to their dependencies 21 | // types, or depending on the specific case, re-export dependencies 22 | // API's directly. 23 | // 24 | // Re-exporting specific crates that belong to us is easier for common 25 | // consumers of these libraries along with the `policy-evaluator`, so 26 | // they can access these crates through the `policy-evaluator` itself, 27 | // streamlining their dependencies as well. 28 | pub use kube; 29 | pub use kubewarden_policy_sdk; 30 | pub use kubewarden_policy_sdk::metadata::ProtocolVersion; 31 | pub use policy_evaluator::policy_evaluator_builder; 32 | pub use policy_fetcher; 33 | pub use validator; 34 | pub use wasmtime_provider::wasmtime; 35 | -------------------------------------------------------------------------------- /src/policy_evaluator.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | mod evaluator; 3 | pub mod policy_evaluator_builder; 4 | mod policy_evaluator_pre; 5 | mod stack_pre; 6 | 7 | pub use evaluator::PolicyEvaluator; 8 | pub use policy_evaluator_pre::PolicyEvaluatorPre; 9 | 10 | use anyhow::{anyhow, Result}; 11 | use serde::Serialize; 12 | use serde_json::value; 13 | use std::{convert::TryFrom, fmt}; 14 | 15 | use crate::admission_request::AdmissionRequest; 16 | 17 | #[derive(Copy, Clone, Default, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] 18 | pub enum PolicyExecutionMode { 19 | #[serde(rename = "kubewarden-wapc")] 20 | #[default] 21 | KubewardenWapc, 22 | #[serde(rename = "opa")] 23 | Opa, 24 | #[serde(rename = "gatekeeper")] 25 | OpaGatekeeper, 26 | #[serde(rename = "wasi")] 27 | Wasi, 28 | } 29 | 30 | impl fmt::Display for PolicyExecutionMode { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | let json = serde_json::to_string(self).map_err(|_| fmt::Error {})?; 33 | write!(f, "{}", json.replace('"', "")) 34 | } 35 | } 36 | 37 | /// A validation request that can be sent to a policy evaluator. 38 | /// It can be either a raw JSON object, or a Kubernetes AdmissionRequest. 39 | #[derive(Clone, Debug, Serialize)] 40 | #[serde(untagged)] 41 | pub enum ValidateRequest { 42 | Raw(serde_json::Value), 43 | // This enum uses the `Box` type to avoid the need for a large enum size causing memory layout 44 | // problems. https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant 45 | AdmissionRequest(Box), 46 | } 47 | 48 | impl ValidateRequest { 49 | pub fn uid(&self) -> &str { 50 | match self { 51 | ValidateRequest::Raw(raw_req) => raw_req 52 | .get("uid") 53 | .and_then(value::Value::as_str) 54 | .unwrap_or_default(), 55 | ValidateRequest::AdmissionRequest(adm_req) => &adm_req.uid, 56 | } 57 | } 58 | } 59 | 60 | #[derive(Clone)] 61 | pub(crate) enum RegoPolicyExecutionMode { 62 | Opa, 63 | Gatekeeper, 64 | } 65 | 66 | impl TryFrom for RegoPolicyExecutionMode { 67 | type Error = anyhow::Error; 68 | 69 | fn try_from(execution_mode: PolicyExecutionMode) -> Result { 70 | match execution_mode { 71 | PolicyExecutionMode::Opa => Ok(RegoPolicyExecutionMode::Opa), 72 | PolicyExecutionMode::OpaGatekeeper => Ok(RegoPolicyExecutionMode::Gatekeeper), 73 | PolicyExecutionMode::KubewardenWapc | PolicyExecutionMode::Wasi => Err(anyhow!( 74 | "execution mode not convertible to a Rego based execution mode" 75 | )), 76 | } 77 | } 78 | } 79 | 80 | /// Settings specified by the user for a given policy. 81 | pub type PolicySettings = serde_json::Map; 82 | 83 | #[cfg(test)] 84 | mod tests { 85 | use super::*; 86 | 87 | use serde_json::json; 88 | use std::collections::HashMap; 89 | 90 | #[test] 91 | fn serialize_policy_execution_mode() { 92 | let mut test_data: HashMap = HashMap::new(); 93 | test_data.insert( 94 | serde_json::to_string(&json!("kubewarden-wapc")).unwrap(), 95 | PolicyExecutionMode::KubewardenWapc, 96 | ); 97 | test_data.insert( 98 | serde_json::to_string(&json!("opa")).unwrap(), 99 | PolicyExecutionMode::Opa, 100 | ); 101 | test_data.insert( 102 | serde_json::to_string(&json!("gatekeeper")).unwrap(), 103 | PolicyExecutionMode::OpaGatekeeper, 104 | ); 105 | 106 | for (expected, mode) in &test_data { 107 | let actual = serde_json::to_string(&mode); 108 | assert!(actual.is_ok()); 109 | assert_eq!(expected, &actual.unwrap()); 110 | } 111 | } 112 | 113 | #[test] 114 | fn deserialize_policy_execution_mode() { 115 | let mut test_data: HashMap = HashMap::new(); 116 | test_data.insert( 117 | serde_json::to_string(&json!("kubewarden-wapc")).unwrap(), 118 | PolicyExecutionMode::KubewardenWapc, 119 | ); 120 | test_data.insert( 121 | serde_json::to_string(&json!("opa")).unwrap(), 122 | PolicyExecutionMode::Opa, 123 | ); 124 | test_data.insert( 125 | serde_json::to_string(&json!("gatekeeper")).unwrap(), 126 | PolicyExecutionMode::OpaGatekeeper, 127 | ); 128 | 129 | for (mode_str, expected) in &test_data { 130 | let actual: std::result::Result = 131 | serde_json::from_str(mode_str); 132 | assert_eq!(expected, &actual.unwrap()); 133 | } 134 | 135 | // an unknown policy mode should not be deserializable 136 | let actual: std::result::Result = 137 | serde_json::from_str("hello world"); 138 | assert!(actual.is_err()); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/policy_evaluator/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum InvalidUserInputError { 5 | #[error("cannot specify 'policy_file' and 'policy_contents' at the same time")] 6 | FileAndContents, 7 | 8 | #[error("cannot specify 'policy_file' and 'policy_module' at the same time")] 9 | FileAndModule, 10 | 11 | #[error("cannot specify 'policy_contents' and 'policy_module' at the same time")] 12 | ContentsAndModule, 13 | 14 | #[error("must specify one among: `policy_file`, `policy_contents` and `policy_module`")] 15 | OneOfFileContentsModule, 16 | 17 | #[error( 18 | "you must provide the `engine` that was used to instantiate the given `policy_module`" 19 | )] 20 | EngineForModule, 21 | 22 | #[error("must specify execution mode")] 23 | ExecutionMode, 24 | } 25 | -------------------------------------------------------------------------------- /src/policy_evaluator/evaluator.rs: -------------------------------------------------------------------------------- 1 | use kubewarden_policy_sdk::{metadata::ProtocolVersion, settings::SettingsValidationResponse}; 2 | use std::fmt; 3 | 4 | use crate::admission_response::AdmissionResponse; 5 | use crate::errors::PolicyEvaluatorError; 6 | use crate::evaluation_context::EvaluationContext; 7 | use crate::policy_evaluator::{PolicySettings, ValidateRequest}; 8 | use crate::runtimes::rego::Runtime as BurregoRuntime; 9 | use crate::runtimes::wapc::Runtime as WapcRuntime; 10 | use crate::runtimes::wasi_cli::Runtime as WasiRuntime; 11 | use crate::runtimes::Runtime; 12 | 13 | pub struct PolicyEvaluator { 14 | runtime: Runtime, 15 | eval_ctx: EvaluationContext, 16 | } 17 | 18 | impl PolicyEvaluator { 19 | pub(crate) fn new(runtime: Runtime, eval_ctx: &EvaluationContext) -> Self { 20 | Self { 21 | runtime, 22 | eval_ctx: eval_ctx.to_owned(), 23 | } 24 | } 25 | 26 | #[tracing::instrument(skip(request))] 27 | pub fn validate( 28 | &mut self, 29 | request: ValidateRequest, 30 | settings: &PolicySettings, 31 | ) -> AdmissionResponse { 32 | match self.runtime { 33 | Runtime::Wapc(ref mut wapc_stack) => { 34 | WapcRuntime(wapc_stack).validate(settings, &request) 35 | } 36 | Runtime::Rego(ref mut burrego_evaluator) => { 37 | let kube_ctx = burrego_evaluator.build_kubernetes_context( 38 | self.eval_ctx.callback_channel.as_ref(), 39 | &self.eval_ctx.ctx_aware_resources_allow_list, 40 | ); 41 | match kube_ctx { 42 | Ok(ctx) => BurregoRuntime(burrego_evaluator).validate(settings, &request, &ctx), 43 | Err(e) => { 44 | AdmissionResponse::reject(request.uid().to_string(), e.to_string(), 500) 45 | } 46 | } 47 | } 48 | Runtime::Cli(ref mut cli_stack) => WasiRuntime(cli_stack).validate(settings, &request), 49 | } 50 | } 51 | 52 | #[tracing::instrument] 53 | pub fn validate_settings(&mut self, settings: &PolicySettings) -> SettingsValidationResponse { 54 | let settings_str = match serde_json::to_string(settings) { 55 | Ok(settings) => settings, 56 | Err(err) => { 57 | return SettingsValidationResponse { 58 | valid: false, 59 | message: Some(format!("could not marshal settings: {err}")), 60 | } 61 | } 62 | }; 63 | 64 | match self.runtime { 65 | Runtime::Wapc(ref mut wapc_stack) => { 66 | WapcRuntime(wapc_stack).validate_settings(settings_str) 67 | } 68 | Runtime::Rego(ref mut burrego_evaluator) => { 69 | BurregoRuntime(burrego_evaluator).validate_settings(settings_str) 70 | } 71 | Runtime::Cli(ref mut cli_stack) => { 72 | WasiRuntime(cli_stack).validate_settings(settings_str) 73 | } 74 | } 75 | } 76 | 77 | pub fn protocol_version(&mut self) -> Result { 78 | match &mut self.runtime { 79 | Runtime::Wapc(ref mut wapc_stack) => Ok(WapcRuntime(wapc_stack) 80 | .protocol_version() 81 | .map_err(PolicyEvaluatorError::InvokeWapcProtocolVersion)?), 82 | _ => Err(PolicyEvaluatorError::InvalidProtocolVersion()), 83 | } 84 | } 85 | } 86 | 87 | impl fmt::Debug for PolicyEvaluator { 88 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 89 | let runtime = self.runtime.to_string(); 90 | 91 | f.debug_struct("PolicyEvaluator") 92 | .field("runtime", &runtime) 93 | .finish() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/policy_evaluator/policy_evaluator_pre.rs: -------------------------------------------------------------------------------- 1 | use std::result::Result; 2 | 3 | use crate::errors::PolicyEvaluatorPreError; 4 | use crate::evaluation_context::EvaluationContext; 5 | use crate::policy_evaluator::{stack_pre::StackPre, PolicyEvaluator}; 6 | use crate::runtimes::{rego, wapc, wasi_cli, Runtime}; 7 | 8 | /// This struct provides a way to quickly allocate a `PolicyEvaluator` 9 | /// object. 10 | /// 11 | /// See the [`rehydrate`](PolicyEvaluatorPre::rehydrate) method. 12 | #[derive(Clone)] 13 | pub struct PolicyEvaluatorPre { 14 | stack_pre: StackPre, 15 | } 16 | 17 | impl PolicyEvaluatorPre { 18 | pub(crate) fn new(stack_pre: StackPre) -> Self { 19 | PolicyEvaluatorPre { stack_pre } 20 | } 21 | 22 | /// Create a `PolicyEvaluator` instance. The creation of the instance is achieved by 23 | /// using wasmtime low level primitives (like `wasmtime::InstancePre`) to make the operation 24 | /// as fast as possible. 25 | /// 26 | /// Warning: the Rego stack cannot make use of these low level primitives, but its 27 | /// instantiation times are negligible. More details inside of the 28 | /// documentation of [`rego::StackPre`](crate::runtimes::rego::StackPre). 29 | pub fn rehydrate( 30 | &self, 31 | eval_ctx: &EvaluationContext, 32 | ) -> Result { 33 | let runtime = match &self.stack_pre { 34 | StackPre::Wapc(stack_pre) => { 35 | let wapc_stack = wapc::WapcStack::new_from_pre(stack_pre, eval_ctx) 36 | .map_err(PolicyEvaluatorPreError::RehydrateWapc)?; 37 | Runtime::Wapc(Box::new(wapc_stack)) 38 | } 39 | StackPre::Wasi(stack_pre) => { 40 | let wasi_stack = wasi_cli::Stack::new_from_pre(stack_pre, eval_ctx); 41 | Runtime::Cli(wasi_stack) 42 | } 43 | StackPre::Rego(stack_pre) => { 44 | let rego_stack = rego::Stack::new_from_pre(stack_pre) 45 | .map_err(PolicyEvaluatorPreError::RehydrateRego)?; 46 | Runtime::Rego(Box::new(rego_stack)) 47 | } 48 | }; 49 | 50 | Ok(PolicyEvaluator::new(runtime, eval_ctx)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/policy_evaluator/stack_pre.rs: -------------------------------------------------------------------------------- 1 | use crate::runtimes::{rego, wapc, wasi_cli}; 2 | 3 | /// Holds pre-initialized stacks for all the types of policies we run 4 | /// 5 | /// Pre-initialized instances are key to reduce the evaluation time when 6 | /// using on-demand PolicyEvaluator instances; where on-demand means that 7 | /// each validation request has a brand new PolicyEvaluator that is discarded 8 | /// once the evaluation is done. 9 | #[derive(Clone)] 10 | pub(crate) enum StackPre { 11 | // This enum uses the `Box` type to avoid the need for a large enum size causing memory layout 12 | // problems. https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant 13 | Wapc(Box), 14 | Wasi(crate::runtimes::wasi_cli::StackPre), 15 | Rego(crate::runtimes::rego::StackPre), 16 | } 17 | 18 | impl From for StackPre { 19 | fn from(wapc_stack_pre: wapc::StackPre) -> Self { 20 | StackPre::Wapc(Box::new(wapc_stack_pre)) 21 | } 22 | } 23 | 24 | impl From for StackPre { 25 | fn from(wasi_stack_pre: wasi_cli::StackPre) -> Self { 26 | StackPre::Wasi(wasi_stack_pre) 27 | } 28 | } 29 | 30 | impl From for StackPre { 31 | fn from(rego_stack_pre: rego::StackPre) -> Self { 32 | StackPre::Rego(rego_stack_pre) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/policy_group_evaluator.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeSet, fmt}; 2 | 3 | pub mod errors; 4 | pub mod evaluator; 5 | 6 | use crate::admission_response::AdmissionResponse; 7 | use crate::policy_evaluator::PolicySettings; 8 | use crate::policy_metadata::ContextAwareResource; 9 | 10 | /// The settings of a policy group member 11 | pub struct PolicyGroupMemberSettings { 12 | /// The policy settings 13 | pub settings: PolicySettings, 14 | /// The list of kubernetes resources that are allowed to be accessed by the policy member 15 | pub ctx_aware_resources_allow_list: BTreeSet, 16 | } 17 | 18 | /// This holds the a summary of the evaluation results of a policy group member 19 | struct PolicyGroupMemberEvaluationResult { 20 | /// whether the request is allowed or not 21 | allowed: bool, 22 | /// the optional message included inside of the evaluation result of the policy 23 | message: Option, 24 | } 25 | 26 | impl From for PolicyGroupMemberEvaluationResult { 27 | fn from(response: AdmissionResponse) -> Self { 28 | Self { 29 | allowed: response.allowed, 30 | message: response.status.and_then(|status| status.message), 31 | } 32 | } 33 | } 34 | 35 | impl fmt::Display for PolicyGroupMemberEvaluationResult { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | if self.allowed { 38 | write!(f, "[ALLOWED]")?; 39 | } else { 40 | write!(f, "[DENIED]")?; 41 | } 42 | if let Some(message) = &self.message { 43 | write!(f, " - {}", message)?; 44 | } 45 | 46 | Ok(()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/policy_group_evaluator/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::errors::PolicyEvaluatorPreError; 4 | 5 | pub type Result = std::result::Result; 6 | 7 | #[derive(Debug, Error)] 8 | pub enum EvaluationError { 9 | #[error("EvaluatorPre not found: {0}")] 10 | EvaluatorPreNotFound(String), 11 | 12 | #[error("settings not found policy: {0}")] 13 | SettingsNotFound(String), 14 | 15 | #[error("settings not valid: {0}")] 16 | SettingsNotValid(String), 17 | 18 | #[error("unknown policy: {0}")] 19 | PolicyNotFound(String), 20 | 21 | #[error("Attempted to rehydrated policy '{0}': {1}")] 22 | CannotRehydratePolicyGroupMember(String, PolicyEvaluatorPreError), 23 | 24 | #[error("Policy group evaluation error: '{0}'")] 25 | PolicyGroupRuntimeError(#[from] Box), 26 | } 27 | -------------------------------------------------------------------------------- /src/policy_tracing.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use serde::{Deserialize, Serialize}; 3 | use tracing::{event, Level}; 4 | 5 | use crate::evaluation_context::EvaluationContext; 6 | 7 | #[derive(Debug, Serialize)] 8 | enum PolicyLogEntryLevel { 9 | Trace, 10 | Debug, 11 | Info, 12 | Warning, 13 | Error, 14 | } 15 | 16 | impl<'de> Deserialize<'de> for PolicyLogEntryLevel { 17 | fn deserialize(deserializer: D) -> Result 18 | where 19 | D: serde::de::Deserializer<'de>, 20 | { 21 | let s = String::deserialize(deserializer)?; 22 | match s.to_uppercase().as_str() { 23 | "TRACE" => Ok(PolicyLogEntryLevel::Trace), 24 | "DEBUG" => Ok(PolicyLogEntryLevel::Debug), 25 | "INFO" => Ok(PolicyLogEntryLevel::Info), 26 | "WARNING" => Ok(PolicyLogEntryLevel::Warning), 27 | "ERROR" => Ok(PolicyLogEntryLevel::Error), 28 | _ => Err(anyhow!("unknown log level {}", s)).map_err(serde::de::Error::custom), 29 | } 30 | } 31 | } 32 | 33 | #[derive(Debug, Deserialize, Serialize)] 34 | struct PolicyLogEntry { 35 | level: PolicyLogEntryLevel, 36 | message: Option, 37 | #[serde(flatten)] 38 | data: Option>, 39 | } 40 | 41 | impl EvaluationContext { 42 | #[tracing::instrument(name = "policy_log", skip(contents))] 43 | pub(crate) fn log(&self, contents: &[u8]) -> Result<()> { 44 | let log_entry: PolicyLogEntry = serde_json::from_slice(contents)?; 45 | macro_rules! log { 46 | ($level:path) => { 47 | event!( 48 | target: "policy_log", 49 | $level, 50 | data = %&serde_json::to_string(&log_entry.data.clone().unwrap())?.as_str(), 51 | "{}", 52 | log_entry.message.clone().unwrap_or_default(), 53 | ); 54 | }; 55 | } 56 | 57 | match log_entry.level { 58 | PolicyLogEntryLevel::Trace => { 59 | log!(Level::TRACE); 60 | } 61 | PolicyLogEntryLevel::Debug => { 62 | log!(Level::DEBUG); 63 | } 64 | PolicyLogEntryLevel::Info => { 65 | log!(Level::INFO); 66 | } 67 | PolicyLogEntryLevel::Warning => { 68 | log!(Level::WARN); 69 | } 70 | PolicyLogEntryLevel::Error => { 71 | log!(Level::ERROR); 72 | } 73 | }; 74 | 75 | Ok(()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/runtimes.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::policy_evaluator::RegoPolicyExecutionMode; 4 | 5 | pub(crate) mod callback; 6 | pub(crate) mod rego; 7 | pub(crate) mod wapc; 8 | pub(crate) mod wasi_cli; 9 | 10 | pub(crate) enum Runtime { 11 | // This enum uses the `Box` type to avoid the need for a large enum size causing memory layout 12 | // problems. https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant 13 | Wapc(Box), 14 | Rego(Box), 15 | Cli(wasi_cli::Stack), 16 | } 17 | 18 | impl Display for Runtime { 19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 20 | match self { 21 | Runtime::Cli(_) => write!(f, "wasi"), 22 | Runtime::Wapc(_) => write!(f, "wapc"), 23 | Runtime::Rego(stack) => match stack.policy_execution_mode { 24 | RegoPolicyExecutionMode::Opa => { 25 | write!(f, "OPA") 26 | } 27 | RegoPolicyExecutionMode::Gatekeeper => { 28 | write!(f, "Gatekeeper") 29 | } 30 | }, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/runtimes/rego/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | pub type Result = std::result::Result; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum RegoRuntimeError { 7 | #[error("cannot build Rego context aware data: callback channel is not set")] 8 | CallbackChannelNotSet, 9 | 10 | #[error("cannot convert callback response into a list of kubernetes objects: {0}")] 11 | CallbackConvertList(#[source] serde_json::Error), 12 | 13 | #[error("cannot convert callback response into a boolean: {0}")] 14 | CallbackConvertBool(#[source] serde_json::Error), 15 | 16 | #[error("error sending request over callback channel: {0}")] 17 | CallbackSend(String), // TODO same as CallbackRequest? 18 | 19 | #[error("error obtaining response from callback channel: {0}")] 20 | CallbackResponse(String), 21 | 22 | #[error("cannot perform a request via callback channel: {0}")] 23 | CallbackRequest(#[source] wasmtime::Error), 24 | 25 | #[error("get plural name failure, cannot convert callback response: {0}")] 26 | CallbackGetPluralName(#[source] serde_json::Error), 27 | 28 | #[error("DynamicObject does not have a name")] 29 | GatekeeperInventoryMissingName, 30 | 31 | #[error("DynamicObject does not have a namespace")] 32 | GatekeeperInventoryMissingNamespace, 33 | 34 | #[error("cannot convert Gatekeeper inventory to JSON: {0}")] 35 | GatekeeperInventorySerializationError(#[source] serde_json::Error), 36 | 37 | #[error("DynamicObject does not have a name")] 38 | OpaInventoryMissingName, 39 | 40 | #[error("DynamicObject does not have a namespace")] 41 | OpaInventoryMissingNamespace, 42 | 43 | #[error("trying to add a namespaced resource to a list of clusterwide resources")] 44 | OpaInventoryAddNamespacedRes, 45 | 46 | #[error("trying to add a clusterwide resource to a list of namespaced resources")] 47 | OpaInventoryAddClusterwideRes, 48 | 49 | #[error("cannot find plural name for resource {0}")] 50 | OpaInventoryMissingPluralName(String), 51 | 52 | #[error("invalid response from policy")] 53 | InvalidResponse, 54 | 55 | #[error("invalid response from policy: {0}")] 56 | InvalidResponseWithError(#[source] serde_json::Error), 57 | 58 | #[error("cannot allocate Rego evaluator: {0}")] 59 | EvaluatorError(String), 60 | 61 | #[error("cannot build Rego engine: {0}")] 62 | RegoEngineBuilder(#[source] burrego::errors::BurregoError), 63 | } 64 | -------------------------------------------------------------------------------- /src/runtimes/rego/mod.rs: -------------------------------------------------------------------------------- 1 | mod context_aware; 2 | pub mod errors; 3 | mod gatekeeper_inventory; 4 | mod gatekeeper_inventory_cache; 5 | mod opa_inventory; 6 | mod runtime; 7 | mod stack; 8 | mod stack_pre; 9 | 10 | use burrego::host_callbacks::HostCallbacks; 11 | pub(crate) use runtime::Runtime; 12 | pub(crate) use stack::Stack; 13 | pub(crate) use stack_pre::StackPre; 14 | 15 | #[tracing::instrument(level = "error")] 16 | fn opa_abort(msg: &str) {} 17 | 18 | #[tracing::instrument(level = "info")] 19 | fn opa_println(msg: &str) {} 20 | 21 | pub(crate) fn new_host_callbacks() -> HostCallbacks { 22 | HostCallbacks { 23 | opa_abort, 24 | opa_println, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/runtimes/rego/runtime.rs: -------------------------------------------------------------------------------- 1 | use burrego::errors::BurregoError; 2 | use kubewarden_policy_sdk::settings::SettingsValidationResponse; 3 | use serde::Deserialize; 4 | use serde_json::json; 5 | use tracing::{error, warn}; 6 | 7 | use crate::runtimes::rego::{ 8 | context_aware, context_aware::KubernetesContext, errors::RegoRuntimeError, Stack, 9 | }; 10 | use crate::{ 11 | admission_request, 12 | admission_response::{AdmissionResponse, AdmissionResponseStatus}, 13 | policy_evaluator::{PolicySettings, RegoPolicyExecutionMode, ValidateRequest}, 14 | }; 15 | 16 | pub(crate) struct Runtime<'a>(pub(crate) &'a mut Stack); 17 | 18 | impl Runtime<'_> { 19 | pub fn validate( 20 | &mut self, 21 | settings: &PolicySettings, 22 | request: &ValidateRequest, 23 | ctx_data: &context_aware::KubernetesContext, 24 | ) -> AdmissionResponse { 25 | let uid = request.uid(); 26 | 27 | // OPA and Gatekeeper expect arguments in different ways 28 | let burrego_evaluation = match self.0.policy_execution_mode { 29 | RegoPolicyExecutionMode::Opa => self.evaluate_opa(settings, request, ctx_data), 30 | RegoPolicyExecutionMode::Gatekeeper => { 31 | // Gatekeeper policies expect the `AdmissionRequest` variant only. 32 | let request = match request { 33 | ValidateRequest::AdmissionRequest(adm_req) => adm_req, 34 | ValidateRequest::Raw(_) => { 35 | return AdmissionResponse::reject_internal_server_error( 36 | uid.to_string(), 37 | "Gatekeeper does not support raw validation requests".to_string(), 38 | ); 39 | } 40 | }; 41 | self.evaluate_gatekeeper(settings, request, ctx_data) 42 | } 43 | }; 44 | 45 | match burrego_evaluation { 46 | Ok(evaluation_result) => { 47 | match self.0.policy_execution_mode { 48 | RegoPolicyExecutionMode::Opa => { 49 | // Open Policy agent policies entrypoint 50 | // return a Kubernetes `AdmissionReview` 51 | // object. 52 | let evaluation_result = evaluation_result 53 | .get(0) 54 | .and_then(|r| r.get("result")) 55 | .and_then(|r| r.get("response")); 56 | 57 | match evaluation_result { 58 | Some(evaluation_result) => { 59 | match serde_json::from_value(evaluation_result.clone()) { 60 | Ok(evaluation_result) => AdmissionResponse { 61 | uid: uid.to_string(), 62 | ..evaluation_result 63 | }, 64 | Err(err) => AdmissionResponse::reject_internal_server_error( 65 | uid.to_string(), 66 | err.to_string(), 67 | ), 68 | } 69 | } 70 | None => AdmissionResponse::reject_internal_server_error( 71 | uid.to_string(), 72 | "cannot interpret OPA policy result".to_string(), 73 | ), 74 | } 75 | } 76 | RegoPolicyExecutionMode::Gatekeeper => { 77 | // Gatekeeper entrypoint is usually a 78 | // `violations` rule that might evaluate to a 79 | // list of violations, each violation with a 80 | // `msg` string explaining the violation 81 | // reason. If no violations are reported, the 82 | // request is accepted. Otherwise it is 83 | // rejected. 84 | #[derive(Debug, Deserialize)] 85 | struct Violation { 86 | msg: Option, 87 | } 88 | #[derive(Debug, Default, Deserialize)] 89 | struct Violations { 90 | result: Vec, 91 | } 92 | 93 | let violations: Violations = evaluation_result 94 | .get(0) 95 | .ok_or_else(|| RegoRuntimeError::InvalidResponse) 96 | .and_then(|response| { 97 | serde_json::from_value(response.clone()) 98 | .map_err(RegoRuntimeError::InvalidResponseWithError) 99 | }) 100 | .unwrap_or_default(); 101 | 102 | if violations.result.is_empty() { 103 | AdmissionResponse { 104 | uid: uid.to_string(), 105 | allowed: true, 106 | ..Default::default() 107 | } 108 | } else { 109 | AdmissionResponse { 110 | uid: uid.to_string(), 111 | allowed: false, 112 | status: Some(AdmissionResponseStatus { 113 | message: Some( 114 | violations 115 | .result 116 | .iter() 117 | .filter_map(|violation| violation.msg.clone()) 118 | .collect::>() 119 | .join(", "), 120 | ), 121 | ..Default::default() 122 | }), 123 | ..Default::default() 124 | } 125 | } 126 | } 127 | } 128 | } 129 | Err(err) => { 130 | error!( 131 | error = ?err, 132 | "error evaluating policy with burrego" 133 | ); 134 | if matches!( 135 | err, 136 | burrego::errors::BurregoError::ExecutionDeadlineExceeded 137 | ) { 138 | if let Err(reset_error) = self.0.evaluator.reset() { 139 | error!(?reset_error, "cannot reset burrego evaluator, further invocations might fail or behave not properly"); 140 | } 141 | } 142 | AdmissionResponse::reject_internal_server_error(uid.to_string(), err.to_string()) 143 | } 144 | } 145 | } 146 | 147 | fn evaluate_opa( 148 | &mut self, 149 | settings: &PolicySettings, 150 | request: &ValidateRequest, 151 | ctx_data: &context_aware::KubernetesContext, 152 | ) -> Result { 153 | let input = json!({ 154 | "request": &request, 155 | }); 156 | 157 | // OPA data seems to be free-form, except for the 158 | // Kubernetes context aware data that must be under the 159 | // `kubernetes` key 160 | // We don't know the data that is provided by the users via 161 | // their settings, hence set the context aware data, to 162 | // ensure we overwrite what a user might have set. 163 | let data = match ctx_data { 164 | KubernetesContext::Opa(ctx) => { 165 | let mut data = settings.clone(); 166 | if data.insert("kubernetes".to_string(), json!(ctx)).is_some() { 167 | warn!("OPA policy had user provided setting with key `kubernetes`. This value has been overwritten with the actual kubernetes context data"); 168 | } 169 | json!(data) 170 | } 171 | _ => json!(settings), 172 | }; 173 | 174 | let data_raw = serde_json::to_vec(&data).map_err(|e| BurregoError::JSONError { 175 | msg: "cannot convert OPA data to JSON".to_string(), 176 | source: e, 177 | })?; 178 | 179 | self.0 180 | .evaluator 181 | .evaluate(self.0.entrypoint_id, &input, &data_raw) 182 | } 183 | 184 | fn evaluate_gatekeeper( 185 | &mut self, 186 | settings: &PolicySettings, 187 | request: &admission_request::AdmissionRequest, 188 | ctx_data: &context_aware::KubernetesContext, 189 | ) -> Result { 190 | // Gatekeeper policies include a toplevel `review` 191 | // object that contains the AdmissionRequest to be 192 | // evaluated in an `object` attribute, and the 193 | // parameters -- defined in their `ConstraintTemplate` 194 | // and configured when the Policy is created. 195 | let input = json!({ 196 | "parameters": settings, 197 | "review": request, 198 | }); 199 | 200 | let data_raw = match ctx_data { 201 | KubernetesContext::Gatekeeper(ctx) => ctx, 202 | KubernetesContext::Empty => "{}".as_bytes(), 203 | KubernetesContext::Opa(_) => unreachable!(), 204 | }; 205 | 206 | self.0 207 | .evaluator 208 | .evaluate(self.0.entrypoint_id, &input, data_raw) 209 | } 210 | 211 | pub fn validate_settings(&mut self, _settings: String) -> SettingsValidationResponse { 212 | // The burrego backend is mainly for compatibility with 213 | // existing OPA policies. Those policies don't have a generic 214 | // way of validating settings. Return true 215 | SettingsValidationResponse { 216 | valid: true, 217 | message: None, 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/runtimes/rego/stack.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | use tokio::sync::mpsc; 3 | 4 | use crate::{ 5 | callback_requests::CallbackRequest, 6 | policy_evaluator::RegoPolicyExecutionMode, 7 | policy_metadata::ContextAwareResource, 8 | runtimes::rego::{ 9 | context_aware, 10 | errors::{RegoRuntimeError, Result}, 11 | gatekeeper_inventory_cache::GATEKEEPER_INVENTORY_CACHE, 12 | opa_inventory::OpaInventory, 13 | stack_pre::StackPre, 14 | }, 15 | }; 16 | 17 | pub(crate) struct Stack { 18 | pub evaluator: burrego::Evaluator, 19 | pub entrypoint_id: i32, 20 | pub policy_execution_mode: RegoPolicyExecutionMode, 21 | } 22 | 23 | impl Stack { 24 | /// Create a new `Stack` using a `StackPre` object 25 | pub fn new_from_pre(stack_pre: &StackPre) -> Result { 26 | let evaluator = stack_pre 27 | .rehydrate() 28 | .map_err(|e| RegoRuntimeError::EvaluatorError(e.to_string()))?; 29 | Ok(Self { 30 | evaluator, 31 | entrypoint_id: stack_pre.entrypoint_id, 32 | policy_execution_mode: stack_pre.policy_execution_mode.clone(), 33 | }) 34 | } 35 | 36 | pub fn build_kubernetes_context( 37 | &self, 38 | callback_channel: Option<&mpsc::Sender>, 39 | ctx_aware_resources_allow_list: &BTreeSet, 40 | ) -> Result { 41 | if ctx_aware_resources_allow_list.is_empty() { 42 | return Ok(context_aware::KubernetesContext::Empty); 43 | } 44 | 45 | match callback_channel { 46 | None => Err(RegoRuntimeError::CallbackChannelNotSet), 47 | Some(chan) => match self.policy_execution_mode { 48 | RegoPolicyExecutionMode::Opa => { 49 | let cluster_resources = 50 | context_aware::get_allowed_resources(chan, ctx_aware_resources_allow_list)?; 51 | let plural_names_by_resource = 52 | context_aware::get_plural_names(chan, ctx_aware_resources_allow_list)?; 53 | let inventory = 54 | OpaInventory::new(&cluster_resources, &plural_names_by_resource)?; 55 | Ok(context_aware::KubernetesContext::Opa(inventory)) 56 | } 57 | RegoPolicyExecutionMode::Gatekeeper => { 58 | let cached_inventory = GATEKEEPER_INVENTORY_CACHE 59 | .get_inventory(chan, ctx_aware_resources_allow_list)?; 60 | Ok(context_aware::KubernetesContext::Gatekeeper( 61 | cached_inventory, 62 | )) 63 | } 64 | }, 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/runtimes/rego/stack_pre.rs: -------------------------------------------------------------------------------- 1 | use crate::policy_evaluator::RegoPolicyExecutionMode; 2 | use crate::policy_evaluator_builder::EpochDeadlines; 3 | use crate::runtimes::rego::errors::{RegoRuntimeError, Result}; 4 | 5 | /// This struct allows to follow the `StackPre -> Stack` 6 | /// "pattern" also for Rego policies. 7 | /// 8 | /// However, Rego policies cannot make use of `wasmtime::InstancePre` 9 | /// to reduce the instantiation times. That happens because all 10 | /// Rego WebAssembly policies import their Wasm Memory from the host. 11 | /// The Wasm Memory is defined inside of a `wasmtime::Store`, which is 12 | /// something that `wasmtime::InstancePre` objects do not have (rightfully!). 13 | /// 14 | /// However, Rego Wasm modules are so small that instantiating them from scratch 15 | /// is already a cheap operation. 16 | #[derive(Clone)] 17 | pub(crate) struct StackPre { 18 | engine: wasmtime::Engine, 19 | module: wasmtime::Module, 20 | epoch_deadlines: Option, 21 | pub entrypoint_id: i32, 22 | pub policy_execution_mode: RegoPolicyExecutionMode, 23 | } 24 | 25 | impl StackPre { 26 | pub(crate) fn new( 27 | engine: wasmtime::Engine, 28 | module: wasmtime::Module, 29 | epoch_deadlines: Option, 30 | entrypoint_id: i32, 31 | policy_execution_mode: RegoPolicyExecutionMode, 32 | ) -> Self { 33 | Self { 34 | engine, 35 | module, 36 | epoch_deadlines, 37 | entrypoint_id, 38 | policy_execution_mode, 39 | } 40 | } 41 | 42 | /// Create a fresh `burrego::Evaluator` 43 | pub(crate) fn rehydrate(&self) -> Result { 44 | let mut builder = burrego::EvaluatorBuilder::default() 45 | .engine(&self.engine) 46 | .module(self.module.clone()) 47 | .host_callbacks(crate::runtimes::rego::new_host_callbacks()); 48 | 49 | if let Some(deadlines) = self.epoch_deadlines { 50 | builder = builder.enable_epoch_interruptions(deadlines.wapc_func); 51 | } 52 | let evaluator = builder 53 | .build() 54 | .map_err(RegoRuntimeError::RegoEngineBuilder)?; 55 | Ok(evaluator) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/runtimes/wapc/callback.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Result; 4 | use tracing::debug; 5 | 6 | use crate::evaluation_context::EvaluationContext; 7 | 8 | /// A host callback function that can be used by the waPC runtime. 9 | type HostCallback = Box< 10 | dyn Fn( 11 | u64, 12 | &str, 13 | &str, 14 | &str, 15 | &[u8], 16 | ) -> Result, Box> 17 | + Send 18 | + Sync, 19 | >; 20 | 21 | /// Returns a host callback function that can be used by the waPC runtime. 22 | /// The callback function will be able to access the `EvaluationContext` instance. 23 | pub(crate) fn new_host_callback(eval_ctx: Arc) -> HostCallback { 24 | Box::new({ 25 | move |wapc_id, binding, namespace, operation, payload| { 26 | debug!(wapc_id, "invoking host_callback"); 27 | crate::runtimes::callback::host_callback( 28 | binding, namespace, operation, payload, &eval_ctx, 29 | ) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/runtimes/wapc/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | pub type Result = std::result::Result; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum WapcRuntimeError { 7 | #[error("invalid response format: {0}")] 8 | InvalidResponseFormat(#[source] anyhow::Error), 9 | 10 | #[error("invalid response from policy: {0}")] 11 | InvalidResponseWithError(#[source] serde_json::Error), 12 | 13 | #[error("cannot create ProtocolVersion object from {res:?}: {error}")] 14 | CreateProtocolVersion { 15 | res: std::vec::Vec, 16 | #[source] 17 | error: wasmtime::Error, 18 | }, 19 | 20 | #[error("cannot invoke 'protocol_version' waPC function : {0}")] 21 | InvokeProtocolVersion(#[source] wapc::errors::Error), 22 | 23 | #[error("cannot build Wasmtime engine: {0}")] 24 | WasmtimeEngineBuilder(#[source] wasmtime_provider::errors::Error), 25 | 26 | #[error("cannot build Wapc host: {0}")] 27 | WapcHostBuilder(#[source] wapc::errors::Error), 28 | } 29 | -------------------------------------------------------------------------------- /src/runtimes/wapc/mod.rs: -------------------------------------------------------------------------------- 1 | mod callback; 2 | pub mod errors; 3 | mod runtime; 4 | mod stack; 5 | mod stack_pre; 6 | 7 | pub(crate) use runtime::Runtime; 8 | pub(crate) use stack::WapcStack; 9 | pub(crate) use stack_pre::StackPre; 10 | -------------------------------------------------------------------------------- /src/runtimes/wapc/stack.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::evaluation_context::EvaluationContext; 4 | use crate::runtimes::wapc::{ 5 | callback::new_host_callback, 6 | errors::{Result, WapcRuntimeError}, 7 | }; 8 | 9 | use super::StackPre; 10 | 11 | pub(crate) struct WapcStack { 12 | wapc_host: wapc::WapcHost, 13 | stack_pre: StackPre, 14 | eval_ctx: Arc, 15 | } 16 | 17 | impl WapcStack { 18 | pub(crate) fn new_from_pre(stack_pre: &StackPre, eval_ctx: &EvaluationContext) -> Result { 19 | let eval_ctx = Arc::new(eval_ctx.to_owned()); 20 | let wapc_host = Self::wapc_host_from_pre(stack_pre, eval_ctx.clone())?; 21 | 22 | Ok(Self { 23 | wapc_host, 24 | stack_pre: stack_pre.to_owned(), 25 | eval_ctx: eval_ctx.to_owned(), 26 | }) 27 | } 28 | 29 | /// Provision a new wapc_host. Useful for starting from a clean slate 30 | /// after an epoch deadline interruption is raised. 31 | /// 32 | /// This method takes care of de-registering the old wapc_host and 33 | /// registering the new one inside of the global WAPC_POLICY_MAPPING 34 | /// variable. 35 | pub(crate) fn reset(&mut self) -> Result<()> { 36 | // Create a new wapc_host 37 | let new_wapc_host = Self::wapc_host_from_pre(&self.stack_pre, self.eval_ctx.clone())?; 38 | 39 | self.wapc_host = new_wapc_host; 40 | 41 | Ok(()) 42 | } 43 | 44 | /// Invokes the given waPC function using the provided payload 45 | pub(crate) fn call( 46 | &self, 47 | op: &str, 48 | payload: &[u8], 49 | ) -> std::result::Result, wapc::errors::Error> { 50 | self.wapc_host.call(op, payload) 51 | } 52 | 53 | /// Create a new `WapcHost` by rehydrating the `StackPre`. This is faster than creating the 54 | /// `WasmtimeEngineProvider` from scratch 55 | fn wapc_host_from_pre( 56 | pre: &StackPre, 57 | eval_ctx: Arc, 58 | ) -> Result { 59 | let engine_provider = pre.rehydrate()?; 60 | let wapc_host = 61 | wapc::WapcHost::new(Box::new(engine_provider), Some(new_host_callback(eval_ctx))) 62 | .map_err(WapcRuntimeError::WapcHostBuilder)?; 63 | Ok(wapc_host) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/runtimes/wapc/stack_pre.rs: -------------------------------------------------------------------------------- 1 | use wasmtime_provider::wasmtime; 2 | 3 | use crate::policy_evaluator_builder::EpochDeadlines; 4 | use crate::runtimes::wapc::errors::{Result, WapcRuntimeError}; 5 | 6 | /// Reduce allocation time of new `WasmtimeProviderEngine`, see the `rehydrate` method 7 | #[derive(Clone)] 8 | pub(crate) struct StackPre { 9 | engine_provider_pre: wasmtime_provider::WasmtimeEngineProviderPre, 10 | } 11 | 12 | impl StackPre { 13 | pub(crate) fn new( 14 | engine: wasmtime::Engine, 15 | module: wasmtime::Module, 16 | epoch_deadlines: Option, 17 | ) -> Result { 18 | let mut builder = wasmtime_provider::WasmtimeEngineProviderBuilder::new() 19 | .engine(engine) 20 | .module(module); 21 | if let Some(deadlines) = epoch_deadlines { 22 | builder = builder.enable_epoch_interruptions(deadlines.wapc_init, deadlines.wapc_func); 23 | } 24 | 25 | let engine_provider_pre = builder 26 | .build_pre() 27 | .map_err(WapcRuntimeError::WasmtimeEngineBuilder)?; 28 | Ok(Self { 29 | engine_provider_pre, 30 | }) 31 | } 32 | 33 | /// Allocate a new `WasmtimeEngineProvider` instance by using a pre-allocated instance 34 | pub(crate) fn rehydrate(&self) -> Result { 35 | let engine = self 36 | .engine_provider_pre 37 | .rehydrate() 38 | .map_err(WapcRuntimeError::WasmtimeEngineBuilder)?; 39 | Ok(engine) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/runtimes/wasi_cli/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | pub type Result = std::result::Result; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum WasiRuntimeError { 7 | #[error("{stderr}")] 8 | WasiEvaluation { 9 | stderr: String, 10 | #[source] 11 | error: wasmtime::Error, 12 | }, 13 | 14 | #[error("cannot define host function '{name}': {error}")] 15 | WasmHostFuncDefinitionError { name: String, error: String }, 16 | 17 | #[error("cannot find `_start` function inside of module: {0}")] 18 | WasmMissingStartFn(#[source] wasmtime::Error), 19 | 20 | #[error("{name} pipe conversion error: {error}")] 21 | PipeConversion { name: String, error: String }, 22 | 23 | #[error("cannot instantiate module: {0}")] 24 | WasmLinkerError(#[source] wasmtime::Error), 25 | 26 | #[error("cannot add to linker: {0}")] 27 | WasmInstantiate(#[source] wasmtime::Error), 28 | 29 | #[error("cannot find 'mem' export")] 30 | WasiMemExport, 31 | 32 | #[error("'mem' export cannot be converted into a Memory instance")] 33 | WasiMemExportCannotConvert, 34 | 35 | #[error("cannot build WasiCtxBuilder: {0}")] 36 | WasiCtxBuilder(#[source] wasi_common::StringArrayError), 37 | 38 | #[error("host_call: cannot convert bd to UTF8: {0}")] 39 | WasiMemOpToUtF8(#[source] std::str::Utf8Error), 40 | 41 | // corresponds to a PoisonError, whose error message is not particularly useful anyways 42 | #[error("host_call: cannot write to STDIN")] 43 | WasiCannotWriteStdin(), 44 | 45 | // corresponds to a PoisonError, whose error message is not particularly useful anyways 46 | #[error("host_call: cannot get write access to STDIN")] 47 | WasiWriteAccessStdin(), 48 | } 49 | -------------------------------------------------------------------------------- /src/runtimes/wasi_cli/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | mod runtime; 3 | mod stack; 4 | mod stack_pre; 5 | mod wasi_pipe; 6 | 7 | pub(crate) use runtime::Runtime; 8 | pub(crate) use stack::Stack; 9 | pub(crate) use stack_pre::StackPre; 10 | -------------------------------------------------------------------------------- /src/runtimes/wasi_cli/runtime.rs: -------------------------------------------------------------------------------- 1 | use kubewarden_policy_sdk::response::ValidationResponse as PolicyValidationResponse; 2 | use kubewarden_policy_sdk::settings::SettingsValidationResponse; 3 | use serde_json::json; 4 | use tracing::{error, warn}; 5 | 6 | use crate::admission_response::AdmissionResponse; 7 | use crate::policy_evaluator::{PolicySettings, ValidateRequest}; 8 | use crate::runtimes::wasi_cli::stack::{RunResult, Stack}; 9 | 10 | pub(crate) struct Runtime<'a>(pub(crate) &'a Stack); 11 | 12 | impl Runtime<'_> { 13 | pub fn validate( 14 | &self, 15 | settings: &PolicySettings, 16 | request: &ValidateRequest, 17 | ) -> AdmissionResponse { 18 | let validate_params = json!({ 19 | "request": request, 20 | "settings": settings, 21 | }); 22 | 23 | let input = match serde_json::to_vec(&validate_params) { 24 | Ok(s) => s, 25 | Err(e) => { 26 | error!( 27 | error = e.to_string().as_str(), 28 | "cannot serialize validation params" 29 | ); 30 | return AdmissionResponse::reject_internal_server_error( 31 | request.uid().to_string(), 32 | e.to_string(), 33 | ); 34 | } 35 | }; 36 | let args = ["policy.wasm", "validate"]; 37 | 38 | match self.0.run(&input, &args) { 39 | Ok(RunResult { stdout, stderr }) => { 40 | if !stderr.is_empty() { 41 | warn!( 42 | request = request.uid().to_string(), 43 | operation = "validate", 44 | "stderr: {:?}", 45 | stderr 46 | ) 47 | } 48 | match serde_json::from_slice::(stdout.as_bytes()) { 49 | Ok(pvr) => { 50 | let req_json_value = serde_json::to_value(request) 51 | .expect("cannot convert request to json value"); 52 | let req_obj = match request { 53 | ValidateRequest::Raw(_) => Some(&req_json_value), 54 | ValidateRequest::AdmissionRequest(_) => req_json_value.get("object"), 55 | }; 56 | 57 | AdmissionResponse::from_policy_validation_response( 58 | request.uid().to_string(), 59 | req_obj, 60 | &pvr, 61 | ) 62 | } 63 | .unwrap_or_else(|e| { 64 | AdmissionResponse::reject_internal_server_error( 65 | request.uid().to_string(), 66 | format!("Cannot convert policy validation response: {e}"), 67 | ) 68 | }), 69 | Err(e) => AdmissionResponse::reject_internal_server_error( 70 | request.uid().to_string(), 71 | format!("Cannot deserialize policy validation response: {e}"), 72 | ), 73 | } 74 | } 75 | Err(e) => AdmissionResponse::reject(request.uid().to_string(), e.to_string(), 500), 76 | } 77 | } 78 | 79 | pub fn validate_settings(&self, settings: String) -> SettingsValidationResponse { 80 | let args = ["policy.wasm", "validate-settings"]; 81 | 82 | match self.0.run(settings.as_bytes(), &args) { 83 | Ok(RunResult { stdout, stderr }) => { 84 | if !stderr.is_empty() { 85 | warn!(operation = "validate-settings", "stderr: {:?}", stderr) 86 | } 87 | serde_json::from_slice::(stdout.as_bytes()) 88 | .unwrap_or_else(|e| SettingsValidationResponse { 89 | valid: false, 90 | message: Some(format!( 91 | "Cannot deserialize settings validation response: {e}" 92 | )), 93 | }) 94 | } 95 | Err(e) => SettingsValidationResponse { 96 | valid: false, 97 | message: Some(e.to_string()), 98 | }, 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/runtimes/wasi_cli/stack.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | use std::sync::{Arc, RwLock}; 3 | use tracing::debug; 4 | use wasi_common::pipe::{ReadPipe, WritePipe}; 5 | use wasi_common::sync::WasiCtxBuilder; 6 | use wasi_common::WasiCtx; 7 | 8 | use crate::evaluation_context::EvaluationContext; 9 | use crate::runtimes::wasi_cli::{ 10 | errors::WasiRuntimeError, stack_pre::StackPre, wasi_pipe::WasiPipe, 11 | }; 12 | 13 | const EXIT_SUCCESS: i32 = 0; 14 | 15 | pub(crate) struct Context { 16 | pub(crate) wasi_ctx: WasiCtx, 17 | pub(crate) stdin_pipe: Arc>, 18 | pub(crate) eval_ctx: Arc, 19 | } 20 | 21 | pub(crate) struct Stack { 22 | stack_pre: StackPre, 23 | eval_ctx: Arc, 24 | } 25 | 26 | pub(crate) struct RunResult { 27 | pub stdout: String, 28 | pub stderr: String, 29 | } 30 | 31 | impl Stack { 32 | pub(crate) fn new_from_pre(stack_pre: &StackPre, eval_ctx: &EvaluationContext) -> Self { 33 | Self { 34 | stack_pre: stack_pre.to_owned(), 35 | eval_ctx: Arc::new(eval_ctx.to_owned()), 36 | } 37 | } 38 | 39 | /// Run a WASI program with the given input and args 40 | pub(crate) fn run( 41 | &self, 42 | input: &[u8], 43 | args: &[&str], 44 | ) -> std::result::Result { 45 | let stdout_pipe = WritePipe::new_in_memory(); 46 | let stderr_pipe = WritePipe::new_in_memory(); 47 | let stdin_pipe: Arc> = Arc::new(RwLock::new(WasiPipe::new(input))); 48 | 49 | let args: Vec = args.iter().map(|s| s.to_string()).collect(); 50 | 51 | let wasi_ctx = WasiCtxBuilder::new() 52 | .args(&args) 53 | .map_err(WasiRuntimeError::WasiCtxBuilder)? 54 | .stdin(Box::new(ReadPipe::from_shared(stdin_pipe.clone()))) 55 | .stdout(Box::new(stdout_pipe.clone())) 56 | .stderr(Box::new(stderr_pipe.clone())) 57 | .build(); 58 | let ctx = Context { 59 | wasi_ctx, 60 | stdin_pipe, 61 | eval_ctx: self.eval_ctx.clone(), 62 | }; 63 | 64 | let mut store = self.stack_pre.build_store(ctx); 65 | let instance = self.stack_pre.rehydrate(&mut store)?; 66 | let start_fn = instance 67 | .get_typed_func::<(), ()>(&mut store, "_start") 68 | .map_err(WasiRuntimeError::WasmMissingStartFn)?; 69 | let evaluation_result = start_fn.call(&mut store, ()); 70 | 71 | // Dropping the store, this is no longer needed, plus it's keeping 72 | // references to the WritePipe(s) that we need exclusive access to. 73 | drop(store); 74 | 75 | let stderr = pipe_to_string("stderr", stderr_pipe)?.trim().to_string(); 76 | 77 | if let Err(err) = evaluation_result { 78 | if let Some(exit_error) = err.downcast_ref::() { 79 | if exit_error.0 == EXIT_SUCCESS { 80 | let stdout = pipe_to_string("stdout", stdout_pipe)?; 81 | return Ok(RunResult { stdout, stderr }); 82 | } else { 83 | debug!( 84 | "WASI program exited with error code: {}, error: {}", 85 | exit_error.0, stderr 86 | ); 87 | 88 | return Err(WasiRuntimeError::WasiEvaluation { stderr, error: err }); 89 | } 90 | } 91 | 92 | debug!("WASI program exited with error: {}", stderr); 93 | return Err(WasiRuntimeError::WasiEvaluation { stderr, error: err }); 94 | } 95 | 96 | let stdout = pipe_to_string("stdout", stdout_pipe)?; 97 | Ok(RunResult { stdout, stderr }) 98 | } 99 | } 100 | 101 | fn pipe_to_string( 102 | name: &str, 103 | pipe: WritePipe>>, 104 | ) -> std::result::Result { 105 | match pipe.try_into_inner() { 106 | Ok(cursor) => { 107 | let buf = cursor.into_inner(); 108 | String::from_utf8(buf).map_err(|e| WasiRuntimeError::PipeConversion { 109 | name: name.to_string(), 110 | error: format!("Cannot convert buffer to UTF8 string: {e}"), 111 | }) 112 | } 113 | Err(_) => Err(WasiRuntimeError::PipeConversion { 114 | name: name.to_string(), 115 | error: "cannot convert pipe into inner".to_string(), 116 | }), 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/runtimes/wasi_cli/stack_pre.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use wasmtime::{AsContext, Engine, InstancePre, Linker, Memory, Module, StoreContext}; 4 | 5 | use crate::runtimes::wasi_cli::errors::{Result, WasiRuntimeError}; 6 | 7 | use crate::policy_evaluator_builder::EpochDeadlines; 8 | use crate::runtimes::{callback::host_callback, wasi_cli::stack::Context}; 9 | 10 | /// Reduce the allocation time of a Wasi Stack. This is done by leveraging `wasmtime::InstancePre`. 11 | #[derive(Clone)] 12 | pub(crate) struct StackPre { 13 | engine: Engine, 14 | instance_pre: InstancePre, 15 | epoch_deadlines: Option, 16 | } 17 | 18 | impl StackPre { 19 | pub(crate) fn new( 20 | engine: Engine, 21 | module: Module, 22 | epoch_deadlines: Option, 23 | ) -> Result { 24 | let mut linker = Linker::::new(&engine); 25 | wasi_common::sync::add_to_linker(&mut linker, |c: &mut Context| &mut c.wasi_ctx) 26 | .map_err(WasiRuntimeError::WasmLinkerError)?; 27 | add_host_call_to_linker(&mut linker)?; 28 | 29 | let instance_pre = linker 30 | .instantiate_pre(&module) 31 | .map_err(WasiRuntimeError::WasmInstantiate)?; 32 | Ok(Self { 33 | engine, 34 | instance_pre, 35 | epoch_deadlines, 36 | }) 37 | } 38 | 39 | /// Create a brand new `wasmtime::Store` to be used during an evaluation 40 | pub(crate) fn build_store(&self, ctx: Context) -> wasmtime::Store { 41 | let mut store = wasmtime::Store::new(&self.engine, ctx); 42 | if let Some(deadline) = self.epoch_deadlines { 43 | store.set_epoch_deadline(deadline.wapc_func); 44 | } 45 | 46 | store 47 | } 48 | 49 | /// Allocate a new `wasmtime::Instance` that is bound to the given `wasmtime::Store`. 50 | /// It's recommended to provide a brand new `wasmtime::Store` created by the 51 | /// `build_store` method 52 | pub(crate) fn rehydrate( 53 | &self, 54 | store: &mut wasmtime::Store, 55 | ) -> Result { 56 | self.instance_pre 57 | .instantiate(store) 58 | .map_err(WasiRuntimeError::WasmInstantiate) 59 | } 60 | } 61 | 62 | fn add_host_call_to_linker(linker: &mut wasmtime::Linker) -> Result<()> { 63 | linker 64 | .func_wrap( 65 | "host", 66 | "call", 67 | |mut caller: wasmtime::Caller<'_, Context>, 68 | bd_ptr: i32, 69 | bd_len: i32, 70 | ns_ptr: i32, 71 | ns_len: i32, 72 | op_ptr: i32, 73 | op_len: i32, 74 | ptr: i32, 75 | len: i32| { 76 | let memory_export = caller 77 | .get_export("memory") 78 | .ok_or_else(|| WasiRuntimeError::WasiMemExport)?; 79 | let memory = memory_export 80 | .into_memory() 81 | .ok_or_else(|| WasiRuntimeError::WasiMemExportCannotConvert)?; 82 | 83 | let stdin = caller.data().stdin_pipe.as_ref(); 84 | 85 | let vec = get_vec_from_memory(caller.as_context(), memory, ptr, len); 86 | let bd_vec = get_vec_from_memory(caller.as_context(), memory, bd_ptr, bd_len); 87 | let bd = std::str::from_utf8(&bd_vec).map_err(WasiRuntimeError::WasiMemOpToUtF8)?; 88 | let ns_vec = get_vec_from_memory(caller.as_context(), memory, ns_ptr, ns_len); 89 | let ns = std::str::from_utf8(&ns_vec).map_err(WasiRuntimeError::WasiMemOpToUtF8)?; 90 | let op_vec = get_vec_from_memory(caller.as_context(), memory, op_ptr, op_len); 91 | let op = std::str::from_utf8(&op_vec).map_err(WasiRuntimeError::WasiMemOpToUtF8)?; 92 | 93 | let host_callback_response = 94 | host_callback(bd, ns, op, &vec, &caller.data().eval_ctx); 95 | 96 | // return 1 if the host callback failed, 0 otherwise 97 | let func_return_value = host_callback_response.is_err() as i32; 98 | 99 | let response_msg = match host_callback_response { 100 | Ok(r) => r, 101 | Err(e) => e.to_string().as_bytes().to_owned(), 102 | }; 103 | 104 | let mut stdin_pipe = stdin 105 | .write() 106 | .map_err(|_| WasiRuntimeError::WasiWriteAccessStdin())?; 107 | let _ = stdin_pipe 108 | .write(&response_msg) 109 | .map_err(|_| WasiRuntimeError::WasiCannotWriteStdin())?; 110 | Ok(func_return_value) 111 | }, 112 | ) 113 | .map_err(|e| WasiRuntimeError::WasmHostFuncDefinitionError { 114 | name: "host.call".to_string(), 115 | error: e.to_string(), 116 | })?; 117 | Ok(()) 118 | } 119 | 120 | fn get_vec_from_memory<'a, T: 'a>( 121 | store: impl Into>, 122 | mem: Memory, 123 | ptr: i32, 124 | len: i32, 125 | ) -> Vec { 126 | let data = mem.data(store); 127 | data[ptr as usize..(ptr + len) as usize].to_vec() 128 | } 129 | -------------------------------------------------------------------------------- /src/runtimes/wasi_cli/wasi_pipe.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::VecDeque, 3 | io::{Read, Seek, Write}, 4 | }; 5 | 6 | // Code based on https://github.com/lapce/lapce/blob/f7d2f4ba863e3a00c9bcb0f3ac1896fd446e7d4c/lapce-proxy/src/plugin/wasi.rs#L45 7 | /// A read/write pipe that can be used by Wasmtime's WasiCtx. 8 | /// 9 | /// It can be used to implement the STDIN of a Wasi module. In this scenario, 10 | /// it's safe to write to the `WasiPipe` instance multiple times. The Wasi guest 11 | /// will find the data on its STDIN. 12 | #[derive(Default)] 13 | pub(crate) struct WasiPipe { 14 | buffer: VecDeque, 15 | } 16 | 17 | impl WasiPipe { 18 | pub fn new(data: &[u8]) -> Self { 19 | let mut buffer: VecDeque = VecDeque::new(); 20 | buffer.extend(data); 21 | 22 | Self { buffer } 23 | } 24 | } 25 | 26 | impl Read for WasiPipe { 27 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 28 | let amt = std::cmp::min(buf.len(), self.buffer.len()); 29 | for (i, byte) in self.buffer.drain(..amt).enumerate() { 30 | buf[i] = byte; 31 | } 32 | Ok(amt) 33 | } 34 | } 35 | 36 | impl Write for WasiPipe { 37 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 38 | self.buffer.extend(buf); 39 | Ok(buf.len()) 40 | } 41 | 42 | fn flush(&mut self) -> std::io::Result<()> { 43 | Ok(()) 44 | } 45 | } 46 | 47 | impl Seek for WasiPipe { 48 | fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result { 49 | Err(std::io::Error::other("can not seek in a pipe")) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use tempfile::TempDir; 2 | 3 | use policy_evaluator::{ 4 | evaluation_context::EvaluationContext, policy_evaluator::PolicyEvaluator, 5 | policy_evaluator::PolicyExecutionMode, policy_evaluator_builder::PolicyEvaluatorBuilder, 6 | }; 7 | use policy_fetcher::{policy::Policy, PullDestination}; 8 | 9 | pub(crate) async fn fetch_policy(policy_uri: &str, tempdir: TempDir) -> Policy { 10 | policy_evaluator::policy_fetcher::fetch_policy( 11 | policy_uri, 12 | PullDestination::LocalFile(tempdir.into_path()), 13 | None, 14 | ) 15 | .await 16 | .expect("cannot fetch policy") 17 | } 18 | 19 | pub(crate) fn build_policy_evaluator( 20 | execution_mode: PolicyExecutionMode, 21 | policy: &Policy, 22 | eval_ctx: &EvaluationContext, 23 | ) -> PolicyEvaluator { 24 | let policy_evaluator_builder = PolicyEvaluatorBuilder::new() 25 | .execution_mode(execution_mode) 26 | .policy_file(&policy.local_path) 27 | .expect("cannot read policy file") 28 | .enable_wasmtime_cache() 29 | .enable_epoch_interruptions(1, 2); 30 | 31 | let policy_evaluator_pre = policy_evaluator_builder 32 | .build_pre() 33 | .expect("cannot build policy evaluator pre"); 34 | 35 | policy_evaluator_pre 36 | .rehydrate(eval_ctx) 37 | .expect("cannot rehydrate policy evaluator") 38 | } 39 | 40 | pub(crate) fn load_request_data(request_file_name: &str) -> Vec { 41 | let request_file_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) 42 | .join("tests/data") 43 | .join(request_file_name); 44 | std::fs::read(request_file_path).expect("cannot read request file") 45 | } 46 | -------------------------------------------------------------------------------- /tests/data/app_deployment.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": { 3 | "group": "apps", 4 | "kind": "Deployment", 5 | "version": "v1" 6 | }, 7 | "name": "api", 8 | "namespace": "customer-1", 9 | "object": { 10 | "apiVersion": "apps/v1", 11 | "kind": "Deployment", 12 | "metadata": { 13 | "annotations": { 14 | "deployment.kubernetes.io/revision": "1", 15 | "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"api\",\"app.kubernetes.io/component\":\"api\"},\"name\":\"api\",\"namespace\":\"customer-1\"},\"spec\":{\"replicas\":3,\"selector\":{\"matchLabels\":{\"app\":\"api\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"api\",\"app.kubernetes.io/component\":\"api\"}},\"spec\":{\"containers\":[{\"image\":\"api:1.0.0\",\"name\":\"api\",\"ports\":[{\"containerPort\":8080}]}]}}}}\n" 16 | }, 17 | "creationTimestamp": "2023-12-17T08:52:31Z", 18 | "generation": 1, 19 | "labels": { 20 | "app": "api", 21 | "app.kubernetes.io/component": "api", 22 | "customer-id": "1" 23 | }, 24 | "name": "api", 25 | "namespace": "customer-1", 26 | "resourceVersion": "1167496", 27 | "uid": "8a1e598b-cc4b-49b7-a465-bee6204c18db" 28 | }, 29 | "spec": { 30 | "progressDeadlineSeconds": 600, 31 | "replicas": 3, 32 | "revisionHistoryLimit": 10, 33 | "selector": { 34 | "matchLabels": { 35 | "app": "api" 36 | } 37 | }, 38 | "strategy": { 39 | "rollingUpdate": { 40 | "maxSurge": "25%", 41 | "maxUnavailable": "25%" 42 | }, 43 | "type": "RollingUpdate" 44 | }, 45 | "template": { 46 | "metadata": { 47 | "creationTimestamp": null, 48 | "labels": { 49 | "app": "api", 50 | "app.kubernetes.io/component": "api" 51 | } 52 | }, 53 | "spec": { 54 | "containers": [ 55 | { 56 | "image": "api:1.0.0", 57 | "imagePullPolicy": "IfNotPresent", 58 | "name": "api", 59 | "ports": [ 60 | { 61 | "containerPort": 8080, 62 | "protocol": "TCP" 63 | } 64 | ], 65 | "resources": {}, 66 | "terminationMessagePath": "/dev/termination-log", 67 | "terminationMessagePolicy": "File" 68 | } 69 | ], 70 | "dnsPolicy": "ClusterFirst", 71 | "restartPolicy": "Always", 72 | "schedulerName": "default-scheduler", 73 | "securityContext": {}, 74 | "terminationGracePeriodSeconds": 30 75 | } 76 | } 77 | }, 78 | "status": { 79 | "conditions": [ 80 | { 81 | "lastTransitionTime": "2023-12-17T08:52:32Z", 82 | "lastUpdateTime": "2023-12-17T08:52:32Z", 83 | "message": "Deployment does not have minimum availability.", 84 | "reason": "MinimumReplicasUnavailable", 85 | "status": "False", 86 | "type": "Available" 87 | }, 88 | { 89 | "lastTransitionTime": "2023-12-17T08:52:31Z", 90 | "lastUpdateTime": "2023-12-17T08:52:32Z", 91 | "message": "ReplicaSet \"api-58f88975b6\" is progressing.", 92 | "reason": "ReplicaSetUpdated", 93 | "status": "True", 94 | "type": "Progressing" 95 | } 96 | ], 97 | "observedGeneration": 1, 98 | "replicas": 3, 99 | "unavailableReplicas": 3, 100 | "updatedReplicas": 3 101 | } 102 | }, 103 | 104 | "operation": "CREATE", 105 | "options": { 106 | "apiVersion": "meta.k8s.io/v1", 107 | "fieldManager": "kubectl-client-side-apply", 108 | "kind": "CreateOptions" 109 | }, 110 | "requestKind": { 111 | "group": "apps", 112 | "kind": "Deployment", 113 | "version": "v1" 114 | }, 115 | "requestResource": { 116 | "group": "apps", 117 | "resource": "deployments", 118 | "version": "v1" 119 | }, 120 | "resource": { 121 | "group": "apps", 122 | "resource": "deployments", 123 | "version": "v1" 124 | }, 125 | "uid": "8560a482-887d-49b2-8781-415d04a0dcb0", 126 | "userInfo": { 127 | "groups": ["system:masters", "system:authenticated"], 128 | "username": "system:admin" 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/data/endless_wasm/.gitignore: -------------------------------------------------------------------------------- 1 | *.wasm 2 | -------------------------------------------------------------------------------- /tests/data/endless_wasm/Makefile: -------------------------------------------------------------------------------- 1 | wasm_endless_loop.wasm: wasm_endless_loop.wat 2 | wat2wasm wasm_endless_loop.wat -o wasm_endless_loop.wasm 3 | 4 | wapc_endless_loop.wasm: wapc_endless_loop.wat 5 | wat2wasm wapc_endless_loop.wat -o wapc_endless_loop.wasm 6 | 7 | .PHONY: build 8 | build: wasm_endless_loop.wasm wapc_endless_loop.wasm 9 | 10 | .PHONY: clean 11 | clean: 12 | rm -rf *.wasm 13 | -------------------------------------------------------------------------------- /tests/data/endless_wasm/README.md: -------------------------------------------------------------------------------- 1 | This directory contains the source code of two WebAssembly modules, bot of them 2 | perform an endless loop. 3 | 4 | The code is written using the WebAssembly text format (aka `WAT`). 5 | 6 | ## `wasm_endless_loop.wat` 7 | 8 | This is a module meant to be used with vanilla wasmtime engine. 9 | 10 | The code exports a function called `endless_loop` that just performs 11 | and endless loop. 12 | This function takes zero parameters and doesn't return anything. 13 | 14 | The `start` function of the WebAssembly module invokes the `endless_loop`, that 15 | means that running the final `.wasm` file via something like `wasmtime run` will 16 | cause the endless function to be executed. 17 | 18 | ## `wapc_endless_loop.wat` 19 | 20 | This is a module meant to be used by a waPC host. 21 | 22 | This code cheats a little, from the outside it looks like any regular waPC module 23 | because it exposes the two functions required by a waPC host. However, these 24 | two functions are reduced to the bare minimum. 25 | 26 | The most important difference is that no waPC function is registered by the 27 | module. Calling any kind of waPC function from the host will result in an 28 | endless loop being executed. 29 | -------------------------------------------------------------------------------- /tests/data/endless_wasm/wapc_endless_loop.wat: -------------------------------------------------------------------------------- 1 | ;; This is a module meant to be used by a waPC host. 2 | ;; 3 | ;; This code cheats a little, from the outside it looks like any regular waPC module 4 | ;; because it exposes the two functions required by a waPC host. However, these 5 | ;; two functions are reduced to the bare minimum. 6 | ;; 7 | ;; The most important difference is that no waPC function is registered by the 8 | ;; module. Calling any kind of waPC function from the host will result in an 9 | ;; endless loop being executed. 10 | 11 | (module 12 | (memory (export "memory") 1) 13 | 14 | ;; waPC host expects a function called wapc_init to be exported 15 | (func $wapc_init (export "wapc_init") 16 | ;; we don't do anything in there 17 | nop 18 | ) 19 | 20 | ;; non exported function that performs an endless loop 21 | (func $endless_loop 22 | ;; create a variable and initialize it to 0 23 | (local $am_i_done i32) 24 | 25 | (loop $endless 26 | ;; if $am_i_done is not equal to 1 -> go back to the beginning of the loop 27 | local.get $am_i_done 28 | i32.const 1 29 | i32.ne 30 | br_if $endless 31 | ) 32 | ) 33 | 34 | ;; waPC host expects a function called wapc_init to be exported 35 | ;; A real implementation would look for the name of the waPC function 36 | ;; to be invoked, read its payload, invoke the function and 37 | ;; provide a success/failure boolean as result. 38 | ;; In this case we just start an endless loop. We don't care about the 39 | ;; waPC function to be invoked, nor the payload. 40 | (func $guest_call (export "__guest_call") 41 | (param $operation_size i32) 42 | (param $payload_size i32) 43 | (result i32) 44 | (call $endless_loop) 45 | i32.const 0 46 | ) 47 | ) 48 | -------------------------------------------------------------------------------- /tests/data/endless_wasm/wasm_endless_loop.wat: -------------------------------------------------------------------------------- 1 | (module 2 | (func $endless_loop (export "endless_loop") 3 | ;; create a variable and initialize it to 0 4 | (local $am_i_done i32) 5 | 6 | (loop $endless 7 | ;; if $am_i_done is not equal to 1 -> go back to the beginning of the loop 8 | local.get $am_i_done 9 | i32.const 1 10 | i32.ne 11 | br_if $endless 12 | ) 13 | ) 14 | (start $endless_loop) 15 | ) 16 | -------------------------------------------------------------------------------- /tests/data/fixtures/kube_context/deployments/ingress/ingress-nginx.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "apps/v1", 3 | "kind": "Deployment", 4 | "metadata": { 5 | "name": "ingress-nginx", 6 | "namespace": "ingress" 7 | }, 8 | "spec": { 9 | "replicas": 1 10 | }, 11 | "status": {} 12 | } 13 | -------------------------------------------------------------------------------- /tests/data/fixtures/kube_context/deployments/kube-system/coredns.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "apps/v1", 3 | "kind": "Deployment", 4 | "metadata": { 5 | "name": "coredns", 6 | "namespace": "kube-system" 7 | }, 8 | "spec": { 9 | "replicas": 1 10 | }, 11 | "status": {} 12 | } 13 | -------------------------------------------------------------------------------- /tests/data/fixtures/kube_context/deployments/kube-system/local-path-provisioner.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "apps/v1", 3 | "kind": "Deployment", 4 | "metadata": { 5 | "name": "local-path-provisioner", 6 | "namespace": "kube-system" 7 | }, 8 | "spec": { 9 | "replicas": 1 10 | }, 11 | "status": {} 12 | } 13 | -------------------------------------------------------------------------------- /tests/data/fixtures/kube_context/namespaces/cert-manager.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Namespace", 4 | "metadata": { 5 | "name": "cert-manager" 6 | }, 7 | "spec": { 8 | "finalizers": [ 9 | "kubernetes" 10 | ] 11 | }, 12 | "status": { 13 | "phase": "Active" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/data/fixtures/kube_context/namespaces/kube-system.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Namespace", 4 | "metadata": { 5 | "name": "kube-system" 6 | }, 7 | "spec": { 8 | "finalizers": [ 9 | "kubernetes" 10 | ] 11 | }, 12 | "status": { 13 | "phase": "Active" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/data/fixtures/kube_context/services/kube-system/kube-dns.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Service", 4 | "metadata": { 5 | "name": "kube-dns", 6 | "namespace": "kube-system" 7 | }, 8 | "spec": { 9 | "clusterIP": "10.43.0.10" 10 | }, 11 | "status": { 12 | "loadBalancer": {} 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/data/fixtures/kube_context/services/kube-system/metrics-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "v1", 3 | "kind": "Service", 4 | "metadata": { 5 | "name": "metrics-server", 6 | "namespace": "kube-system" 7 | }, 8 | "spec": { 9 | "clusterIP": "10.43.117.145" 10 | }, 11 | "status": { 12 | "loadBalancer": {} 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/data/gatekeeper_always_happy_policy.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubewarden/policy-evaluator/f1f581286d8928060459f33c351d5bfb1ffd92a0/tests/data/gatekeeper_always_happy_policy.wasm -------------------------------------------------------------------------------- /tests/data/gatekeeper_always_unhappy_policy.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubewarden/policy-evaluator/f1f581286d8928060459f33c351d5bfb1ffd92a0/tests/data/gatekeeper_always_unhappy_policy.wasm -------------------------------------------------------------------------------- /tests/data/pod_creation_flux_cat.json: -------------------------------------------------------------------------------- 1 | { 2 | "uid": "1299d386-525b-4032-98ae-1949f69f9cfc", 3 | "kind": { 4 | "group": "", 5 | "version": "v1", 6 | "kind": "Pod" 7 | }, 8 | "resource": { 9 | "group": "", 10 | "version": "v1", 11 | "resource": "pods" 12 | }, 13 | "requestKind": { 14 | "group": "", 15 | "version": "v1", 16 | "kind": "Pod" 17 | }, 18 | "requestResource": { 19 | "group": "", 20 | "version": "v1", 21 | "resource": "pods" 22 | }, 23 | "name": "nginx", 24 | "namespace": "default", 25 | "operation": "CREATE", 26 | "userInfo": { 27 | "username": "kubernetes-admin", 28 | "groups": ["system:masters", "system:authenticated"] 29 | }, 30 | "object": { 31 | "kind": "Pod", 32 | "apiVersion": "v1", 33 | "metadata": { 34 | "name": "nginx", 35 | "namespace": "default", 36 | "uid": "04dc7a5e-e1f1-4e34-8d65-2c9337a43e64", 37 | "creationTimestamp": "2020-11-12T15:18:36Z", 38 | "labels": { 39 | "env": "test" 40 | }, 41 | "annotations": { 42 | "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"env\":\"test\"},\"name\":\"nginx\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"nginx\",\"imagePullPolicy\":\"IfNotPresent\",\"name\":\"nginx\"}],\"tolerations\":[{\"effect\":\"NoSchedule\",\"key\":\"example-key\",\"operator\":\"Exists\"}]}}\n", 43 | "fluxcd.io/cat": "felix" 44 | }, 45 | "managedFields": [ 46 | { 47 | "manager": "kubectl", 48 | "operation": "Update", 49 | "apiVersion": "v1", 50 | "time": "2020-11-12T15:18:36Z", 51 | "fieldsType": "FieldsV1", 52 | "fieldsV1": { 53 | "f:metadata": { 54 | "f:annotations": { 55 | ".": {}, 56 | "f:kubectl.kubernetes.io/last-applied-configuration": {} 57 | }, 58 | "f:labels": { 59 | ".": {}, 60 | "f:env": {} 61 | } 62 | }, 63 | "f:spec": { 64 | "f:containers": { 65 | "k:{\"name\":\"nginx\"}": { 66 | ".": {}, 67 | "f:image": {}, 68 | "f:imagePullPolicy": {}, 69 | "f:name": {}, 70 | "f:resources": {}, 71 | "f:terminationMessagePath": {}, 72 | "f:terminationMessagePolicy": {} 73 | } 74 | }, 75 | "f:dnsPolicy": {}, 76 | "f:enableServiceLinks": {}, 77 | "f:restartPolicy": {}, 78 | "f:schedulerName": {}, 79 | "f:securityContext": {}, 80 | "f:terminationGracePeriodSeconds": {}, 81 | "f:tolerations": {} 82 | } 83 | } 84 | } 85 | ] 86 | }, 87 | "spec": { 88 | "volumes": [ 89 | { 90 | "name": "default-token-pvpz7", 91 | "secret": { 92 | "secretName": "default-token-pvpz7" 93 | } 94 | } 95 | ], 96 | "containers": [ 97 | { 98 | "name": "sleeping-sidecar", 99 | "image": "alpine", 100 | "command": ["sleep", "1h"], 101 | "resources": {}, 102 | "volumeMounts": [ 103 | { 104 | "name": "default-token-pvpz7", 105 | "readOnly": true, 106 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" 107 | } 108 | ], 109 | "terminationMessagePath": "/dev/termination-log", 110 | "terminationMessagePolicy": "File", 111 | "imagePullPolicy": "IfNotPresent" 112 | }, 113 | { 114 | "name": "nginx", 115 | "image": "nginx", 116 | "resources": {}, 117 | "volumeMounts": [ 118 | { 119 | "name": "default-token-pvpz7", 120 | "readOnly": true, 121 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" 122 | } 123 | ], 124 | "securityContext": { 125 | "privileged": true 126 | }, 127 | "terminationMessagePath": "/dev/termination-log", 128 | "terminationMessagePolicy": "File", 129 | "imagePullPolicy": "IfNotPresent" 130 | } 131 | ], 132 | "restartPolicy": "Always", 133 | "terminationGracePeriodSeconds": 30, 134 | "dnsPolicy": "ClusterFirst", 135 | "serviceAccountName": "default", 136 | "serviceAccount": "default", 137 | "securityContext": {}, 138 | "schedulerName": "default-scheduler", 139 | "tolerations": [ 140 | { 141 | "key": "node.kubernetes.io/not-ready", 142 | "operator": "Exists", 143 | "effect": "NoExecute", 144 | "tolerationSeconds": 300 145 | }, 146 | { 147 | "key": "node.kubernetes.io/unreachable", 148 | "operator": "Exists", 149 | "effect": "NoExecute", 150 | "tolerationSeconds": 300 151 | }, 152 | { 153 | "key": "dedicated", 154 | "operator": "Equal", 155 | "value": "tenantA", 156 | "effect": "NoSchedule" 157 | } 158 | ], 159 | "priority": 0, 160 | "enableServiceLinks": true, 161 | "preemptionPolicy": "PreemptLowerPriority" 162 | }, 163 | "status": { 164 | "phase": "Pending", 165 | "qosClass": "BestEffort" 166 | } 167 | }, 168 | "oldObject": null, 169 | "dryRun": false, 170 | "options": { 171 | "kind": "CreateOptions", 172 | "apiVersion": "meta.k8s.io/v1" 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /tests/data/pod_with_privileged_containers.json: -------------------------------------------------------------------------------- 1 | { 2 | "uid": "1299d386-525b-4032-98ae-1949f69f9cfc", 3 | "kind": { 4 | "group": "", 5 | "version": "v1", 6 | "kind": "Pod" 7 | }, 8 | "resource": { 9 | "group": "", 10 | "version": "v1", 11 | "resource": "pods" 12 | }, 13 | "requestKind": { 14 | "group": "", 15 | "version": "v1", 16 | "kind": "Pod" 17 | }, 18 | "requestResource": { 19 | "group": "", 20 | "version": "v1", 21 | "resource": "pods" 22 | }, 23 | "name": "nginx", 24 | "namespace": "default", 25 | "operation": "CREATE", 26 | "userInfo": { 27 | "username": "kubernetes-admin", 28 | "groups": ["system:masters", "system:authenticated"] 29 | }, 30 | "object": { 31 | "kind": "Pod", 32 | "apiVersion": "v1", 33 | "metadata": { 34 | "name": "nginx", 35 | "namespace": "default", 36 | "uid": "04dc7a5e-e1f1-4e34-8d65-2c9337a43e64", 37 | "creationTimestamp": "2020-11-12T15:18:36Z", 38 | "labels": { 39 | "env": "test" 40 | }, 41 | "annotations": { 42 | "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"labels\":{\"env\":\"test\"},\"name\":\"nginx\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"nginx\",\"imagePullPolicy\":\"IfNotPresent\",\"name\":\"nginx\"}],\"tolerations\":[{\"effect\":\"NoSchedule\",\"key\":\"example-key\",\"operator\":\"Exists\"}]}}\n" 43 | }, 44 | "managedFields": [ 45 | { 46 | "manager": "kubectl", 47 | "operation": "Update", 48 | "apiVersion": "v1", 49 | "time": "2020-11-12T15:18:36Z", 50 | "fieldsType": "FieldsV1", 51 | "fieldsV1": { 52 | "f:metadata": { 53 | "f:annotations": { 54 | ".": {}, 55 | "f:kubectl.kubernetes.io/last-applied-configuration": {} 56 | }, 57 | "f:labels": { 58 | ".": {}, 59 | "f:env": {} 60 | } 61 | }, 62 | "f:spec": { 63 | "f:containers": { 64 | "k:{\"name\":\"nginx\"}": { 65 | ".": {}, 66 | "f:image": {}, 67 | "f:imagePullPolicy": {}, 68 | "f:name": {}, 69 | "f:resources": {}, 70 | "f:terminationMessagePath": {}, 71 | "f:terminationMessagePolicy": {} 72 | } 73 | }, 74 | "f:dnsPolicy": {}, 75 | "f:enableServiceLinks": {}, 76 | "f:restartPolicy": {}, 77 | "f:schedulerName": {}, 78 | "f:securityContext": {}, 79 | "f:terminationGracePeriodSeconds": {}, 80 | "f:tolerations": {} 81 | } 82 | } 83 | } 84 | ] 85 | }, 86 | "spec": { 87 | "volumes": [ 88 | { 89 | "name": "default-token-pvpz7", 90 | "secret": { 91 | "secretName": "default-token-pvpz7" 92 | } 93 | } 94 | ], 95 | "containers": [ 96 | { 97 | "name": "sleeping-sidecar", 98 | "image": "alpine", 99 | "command": ["sleep", "1h"], 100 | "resources": {}, 101 | "volumeMounts": [ 102 | { 103 | "name": "default-token-pvpz7", 104 | "readOnly": true, 105 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" 106 | } 107 | ], 108 | "terminationMessagePath": "/dev/termination-log", 109 | "terminationMessagePolicy": "File", 110 | "imagePullPolicy": "IfNotPresent" 111 | }, 112 | { 113 | "name": "nginx", 114 | "image": "nginx", 115 | "resources": {}, 116 | "volumeMounts": [ 117 | { 118 | "name": "default-token-pvpz7", 119 | "readOnly": true, 120 | "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" 121 | } 122 | ], 123 | "securityContext": { 124 | "privileged": true 125 | }, 126 | "terminationMessagePath": "/dev/termination-log", 127 | "terminationMessagePolicy": "File", 128 | "imagePullPolicy": "IfNotPresent" 129 | } 130 | ], 131 | "restartPolicy": "Always", 132 | "terminationGracePeriodSeconds": 30, 133 | "dnsPolicy": "ClusterFirst", 134 | "serviceAccountName": "default", 135 | "serviceAccount": "default", 136 | "securityContext": {}, 137 | "schedulerName": "default-scheduler", 138 | "tolerations": [ 139 | { 140 | "key": "node.kubernetes.io/not-ready", 141 | "operator": "Exists", 142 | "effect": "NoExecute", 143 | "tolerationSeconds": 300 144 | }, 145 | { 146 | "key": "node.kubernetes.io/unreachable", 147 | "operator": "Exists", 148 | "effect": "NoExecute", 149 | "tolerationSeconds": 300 150 | }, 151 | { 152 | "key": "dedicated", 153 | "operator": "Equal", 154 | "value": "tenantA", 155 | "effect": "NoSchedule" 156 | } 157 | ], 158 | "priority": 0, 159 | "enableServiceLinks": true, 160 | "preemptionPolicy": "PreemptLowerPriority" 161 | }, 162 | "status": { 163 | "phase": "Pending", 164 | "qosClass": "BestEffort" 165 | } 166 | }, 167 | "oldObject": null, 168 | "dryRun": false, 169 | "options": { 170 | "kind": "CreateOptions", 171 | "apiVersion": "meta.k8s.io/v1" 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tests/data/raw_mutation.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": "tonio", 3 | "action": "eats", 4 | "resource": "banana" 5 | } 6 | -------------------------------------------------------------------------------- /tests/data/raw_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": "tonio", 3 | "action": "eats", 4 | "resource": "hay" 5 | } 6 | -------------------------------------------------------------------------------- /tests/data/service_clusterip.json: -------------------------------------------------------------------------------- 1 | { 2 | "uid": "1299d386-525b-4032-98ae-1949f69f9cfc", 3 | "kind": { 4 | "group": "", 5 | "version": "v1", 6 | "kind": "Service" 7 | }, 8 | "resource": { 9 | "group": "", 10 | "version": "v1", 11 | "resource": "services" 12 | }, 13 | "requestKind": { 14 | "group": "", 15 | "version": "v1", 16 | "kind": "Service" 17 | }, 18 | "requestResource": { 19 | "group": "", 20 | "version": "v1", 21 | "resource": "services" 22 | }, 23 | "name": "nginx", 24 | "namespace": "default", 25 | "operation": "CREATE", 26 | "userInfo": { 27 | "username": "kubernetes-admin", 28 | "groups": ["system:masters", "system:authenticated"] 29 | }, 30 | "object": { 31 | "kind": "Service", 32 | "apiVersion": "v1", 33 | "metadata": { 34 | "name": "nginx", 35 | "namespace": "default", 36 | "uid": "04dc7a5e-e1f1-4e34-8d65-2c9337a43e64", 37 | "creationTimestamp": "2020-11-12T15:18:36Z", 38 | "labels": { 39 | "env": "test" 40 | } 41 | }, 42 | "spec": { 43 | "clusterIP": "10.43.22.39", 44 | "clusterIPs": ["10.43.22.39"], 45 | "ports": [ 46 | { 47 | "port": 80, 48 | "protocol": "TCP", 49 | "targetPort": 7878 50 | } 51 | ], 52 | "selector": { 53 | "app": "nginx" 54 | }, 55 | "sessionAffinity": "None", 56 | "type": "ClusterIP" 57 | }, 58 | "status": { 59 | "loadBalancer": {} 60 | } 61 | }, 62 | "oldObject": null, 63 | "dryRun": false, 64 | "options": { 65 | "kind": "CreateOptions", 66 | "apiVersion": "meta.k8s.io/v1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/data/service_loadbalancer.json: -------------------------------------------------------------------------------- 1 | { 2 | "uid": "1299d386-525b-4032-98ae-1949f69f9cfc", 3 | "kind": { 4 | "group": "", 5 | "version": "v1", 6 | "kind": "Service" 7 | }, 8 | "resource": { 9 | "group": "", 10 | "version": "v1", 11 | "resource": "services" 12 | }, 13 | "requestKind": { 14 | "group": "", 15 | "version": "v1", 16 | "kind": "Service" 17 | }, 18 | "requestResource": { 19 | "group": "", 20 | "version": "v1", 21 | "resource": "services" 22 | }, 23 | "name": "nginx", 24 | "namespace": "default", 25 | "operation": "CREATE", 26 | "userInfo": { 27 | "username": "kubernetes-admin", 28 | "groups": ["system:masters", "system:authenticated"] 29 | }, 30 | "object": { 31 | "kind": "Service", 32 | "apiVersion": "v1", 33 | "metadata": { 34 | "name": "nginx", 35 | "namespace": "default", 36 | "uid": "04dc7a5e-e1f1-4e34-8d65-2c9337a43e64", 37 | "creationTimestamp": "2020-11-12T15:18:36Z", 38 | "labels": { 39 | "env": "test" 40 | } 41 | }, 42 | "spec": { 43 | "selector": { 44 | "app": "nginx" 45 | }, 46 | "sessionAffinity": "None", 47 | "type": "LoadBalancer", 48 | "ports": [ 49 | { 50 | "port": 80, 51 | "targetPort": 80, 52 | "nodePort": 30080 53 | } 54 | ] 55 | }, 56 | "status": { 57 | "loadBalancer": {} 58 | } 59 | }, 60 | "oldObject": null, 61 | "dryRun": false, 62 | "options": { 63 | "kind": "CreateOptions", 64 | "apiVersion": "meta.k8s.io/v1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/k8s_mock/fixtures.rs: -------------------------------------------------------------------------------- 1 | use k8s_openapi::{ 2 | api::{ 3 | apps::v1::Deployment, 4 | core::v1::{Namespace, Service}, 5 | }, 6 | apimachinery::pkg::apis::meta::v1::{APIResource, APIResourceList}, 7 | }; 8 | use kube::core::{ 9 | watch::{Bookmark, BookmarkMeta}, 10 | ListMeta, ObjectList, ObjectMeta, TypeMeta, WatchEvent, 11 | }; 12 | use std::collections::BTreeMap; 13 | 14 | pub(crate) fn v1_resource_list() -> APIResourceList { 15 | APIResourceList { 16 | group_version: "v1".to_owned(), 17 | resources: vec![ 18 | APIResource { 19 | name: "namespaces".to_owned(), 20 | singular_name: "namespace".to_owned(), 21 | namespaced: false, 22 | kind: "Namespace".to_owned(), 23 | ..Default::default() 24 | }, 25 | APIResource { 26 | name: "services".to_owned(), 27 | singular_name: "service".to_owned(), 28 | namespaced: true, 29 | kind: "Service".to_owned(), 30 | ..Default::default() 31 | }, 32 | ], 33 | } 34 | } 35 | 36 | pub(crate) fn apps_v1_resource_list() -> APIResourceList { 37 | APIResourceList { 38 | group_version: "apps/v1".to_owned(), 39 | resources: vec![APIResource { 40 | name: "deployments".to_owned(), 41 | singular_name: "deployment".to_owned(), 42 | namespaced: true, 43 | kind: "Deployment".to_owned(), 44 | ..Default::default() 45 | }], 46 | } 47 | } 48 | 49 | pub(crate) fn namespaces() -> ObjectList { 50 | ObjectList { 51 | types: TypeMeta::list::(), 52 | metadata: ListMeta { 53 | resource_version: Some("1".to_owned()), 54 | ..Default::default() 55 | }, 56 | items: vec![Namespace { 57 | metadata: ObjectMeta { 58 | name: Some("customer-1".to_owned()), 59 | labels: Some(BTreeMap::from([("customer-id".to_owned(), "1".to_owned())])), 60 | resource_version: Some("1".to_owned()), 61 | ..Default::default() 62 | }, 63 | ..Default::default() 64 | }], 65 | } 66 | } 67 | 68 | pub(crate) fn namespaces_watch_bookmark(resource_version: &str) -> WatchEvent { 69 | WatchEvent::Bookmark(Bookmark { 70 | types: TypeMeta::list::(), 71 | metadata: BookmarkMeta { 72 | annotations: BTreeMap::new(), 73 | resource_version: resource_version.to_owned(), 74 | }, 75 | }) 76 | } 77 | 78 | pub(crate) fn deployments() -> ObjectList { 79 | ObjectList { 80 | metadata: ListMeta { 81 | resource_version: Some("1".to_owned()), 82 | ..Default::default() 83 | }, 84 | types: TypeMeta::list::(), 85 | items: vec![ 86 | Deployment { 87 | metadata: ObjectMeta { 88 | name: Some("postgres".to_owned()), 89 | namespace: Some("customer-1".to_owned()), 90 | labels: Some(BTreeMap::from([( 91 | "app.kubernetes.io/component".to_owned(), 92 | "database".to_owned(), 93 | )])), 94 | resource_version: Some("1".to_owned()), 95 | ..Default::default() 96 | }, 97 | ..Default::default() 98 | }, 99 | Deployment { 100 | metadata: ObjectMeta { 101 | name: Some("single-page-app".to_owned()), 102 | namespace: Some("customer-1".to_owned()), 103 | labels: Some(BTreeMap::from([( 104 | "app.kubernetes.io/component".to_owned(), 105 | "frontend".to_owned(), 106 | )])), 107 | resource_version: Some("1".to_owned()), 108 | ..Default::default() 109 | }, 110 | ..Default::default() 111 | }, 112 | ], 113 | } 114 | } 115 | 116 | pub(crate) fn deployments_watch_bookmark(resource_version: &str) -> WatchEvent { 117 | WatchEvent::Bookmark(Bookmark { 118 | types: TypeMeta::list::(), 119 | metadata: BookmarkMeta { 120 | annotations: BTreeMap::new(), 121 | resource_version: resource_version.to_owned(), 122 | }, 123 | }) 124 | } 125 | 126 | pub(crate) fn services() -> ObjectList { 127 | ObjectList { 128 | metadata: ListMeta { 129 | resource_version: Some("1".to_owned()), 130 | ..Default::default() 131 | }, 132 | types: TypeMeta::list::(), 133 | items: vec![api_auth_service()], 134 | } 135 | } 136 | 137 | pub(crate) fn services_watch_bookmark(resource_version: &str) -> WatchEvent { 138 | WatchEvent::Bookmark(Bookmark { 139 | types: TypeMeta::list::(), 140 | metadata: BookmarkMeta { 141 | annotations: BTreeMap::new(), 142 | resource_version: resource_version.to_owned(), 143 | }, 144 | }) 145 | } 146 | 147 | pub(crate) fn api_auth_service() -> Service { 148 | Service { 149 | metadata: ObjectMeta { 150 | name: Some("api-auth-service".to_owned()), 151 | namespace: Some("customer-1".to_owned()), 152 | labels: Some(BTreeMap::from([( 153 | "app.kubernetes.io/part-of".to_owned(), 154 | "api".to_owned(), 155 | )])), 156 | resource_version: Some("1".to_owned()), 157 | ..Default::default() 158 | }, 159 | ..Default::default() 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tests/k8s_mock/mod.rs: -------------------------------------------------------------------------------- 1 | mod fixtures; 2 | 3 | use std::collections::HashMap; 4 | 5 | use hyper::{http, Request, Response}; 6 | use kube::client::Body; 7 | use serde::Serialize; 8 | use tower_test::mock::{Handle, SendResponse}; 9 | 10 | pub(crate) async fn wapc_and_wasi_scenario(handle: Handle, Response>) { 11 | tokio::spawn(async move { 12 | let mut handle = handle; 13 | 14 | loop { 15 | let (request, send) = handle.next_request().await.expect("service not called"); 16 | let url = url::Url::parse(&format!("https://localhost{}", request.uri())) 17 | .expect("cannot parse incoming request"); 18 | 19 | let query_params: HashMap = url.query_pairs().into_owned().collect(); 20 | let is_watch_request = query_params.contains_key("watch"); 21 | let watch_resource_version = query_params.get("resourceVersion"); 22 | let label_selector = query_params.get("labelSelector").map(String::as_str); 23 | 24 | println!("request: {:?}", request.uri()); 25 | 26 | match ( 27 | request.method(), 28 | request.uri().path(), 29 | label_selector, 30 | is_watch_request, 31 | ) { 32 | (&http::Method::GET, "/api/v1", None, false) => { 33 | send_response(send, fixtures::v1_resource_list()); 34 | } 35 | (&http::Method::GET, "/apis/apps/v1", None, false) => { 36 | send_response(send, fixtures::apps_v1_resource_list()); 37 | } 38 | (&http::Method::GET, "/api/v1/namespaces", Some("customer-id=1"), false) => { 39 | send_response(send, fixtures::namespaces()); 40 | } 41 | (&http::Method::GET, "/api/v1/namespaces", Some("customer-id=1"), true) => { 42 | send_response( 43 | send, 44 | fixtures::namespaces_watch_bookmark(watch_resource_version.unwrap()), 45 | ); 46 | } 47 | ( 48 | &http::Method::GET, 49 | "/apis/apps/v1/namespaces/customer-1/deployments", 50 | None, 51 | false, 52 | ) => { 53 | send_response(send, fixtures::deployments()); 54 | } 55 | ( 56 | &http::Method::GET, 57 | "/apis/apps/v1/namespaces/customer-1/deployments", 58 | None, 59 | true, 60 | ) => { 61 | send_response( 62 | send, 63 | fixtures::deployments_watch_bookmark(watch_resource_version.unwrap()), 64 | ); 65 | } 66 | ( 67 | &http::Method::GET, 68 | "/api/v1/namespaces/customer-1/services/api-auth-service", 69 | None, 70 | false, 71 | ) => { 72 | send_response(send, fixtures::api_auth_service()); 73 | } 74 | _ => { 75 | panic!("unexpected request: {:?}", request); 76 | } 77 | } 78 | } 79 | }); 80 | } 81 | 82 | pub(crate) async fn rego_scenario(handle: Handle, Response>) { 83 | tokio::spawn(async move { 84 | let mut handle = handle; 85 | 86 | loop { 87 | let (request, send) = handle.next_request().await.expect("service not called"); 88 | let url = url::Url::parse(&format!("https://localhost{}", request.uri())) 89 | .expect("cannot parse incoming request"); 90 | 91 | let query_params: HashMap = url.query_pairs().into_owned().collect(); 92 | let is_watch_request = query_params.contains_key("watch"); 93 | let watch_resource_version = query_params.get("resourceVersion"); 94 | 95 | match (request.method(), request.uri().path(), is_watch_request) { 96 | (&http::Method::GET, "/api/v1", false) => { 97 | send_response(send, fixtures::v1_resource_list()); 98 | } 99 | (&http::Method::GET, "/apis/apps/v1", false) => { 100 | send_response(send, fixtures::apps_v1_resource_list()); 101 | } 102 | (&http::Method::GET, "/api/v1/namespaces", false) => { 103 | send_response(send, fixtures::namespaces()); 104 | } 105 | (&http::Method::GET, "/api/v1/namespaces", true) => { 106 | send_response( 107 | send, 108 | fixtures::namespaces_watch_bookmark(watch_resource_version.unwrap()), 109 | ); 110 | } 111 | (&http::Method::GET, "/apis/apps/v1/deployments", false) => { 112 | send_response(send, fixtures::deployments()); 113 | } 114 | (&http::Method::GET, "/apis/apps/v1/deployments", true) => { 115 | send_response( 116 | send, 117 | fixtures::deployments_watch_bookmark(watch_resource_version.unwrap()), 118 | ); 119 | } 120 | (&http::Method::GET, "/api/v1/services", false) => { 121 | send_response(send, fixtures::services()); 122 | } 123 | (&http::Method::GET, "/api/v1/services", true) => { 124 | send_response( 125 | send, 126 | fixtures::services_watch_bookmark(watch_resource_version.unwrap()), 127 | ); 128 | } 129 | 130 | _ => { 131 | panic!("unexpected request: {:?}", request); 132 | } 133 | } 134 | } 135 | }); 136 | } 137 | 138 | fn send_response(send: SendResponse>, response: T) { 139 | let response = serde_json::to_vec(&response).unwrap(); 140 | send.send_response(Response::builder().body(Body::from(response)).unwrap()); 141 | } 142 | -------------------------------------------------------------------------------- /updatecli/DEVELOP.md: -------------------------------------------------------------------------------- 1 | To test the updatecli manifests locally: 2 | 3 | ```console 4 | export UPDATECLI_GITHUB_TOKEN= 5 | UPDATECLI_GITHUB_OWNER= updatecli diff --config updatecli/updatecli.d/update-rust-toolchain.yaml --values updatecli/values.yaml 6 | ``` 7 | -------------------------------------------------------------------------------- /updatecli/updatecli.d/update-rust-toolchain.yaml: -------------------------------------------------------------------------------- 1 | name: Update Rust version inside of rust-toolchain file 2 | 3 | scms: 4 | github: 5 | kind: github 6 | spec: 7 | user: "{{ .github.author }}" 8 | email: "{{ .github.email }}" 9 | owner: "{{ requiredEnv .github.owner }}" 10 | repository: "policy-evaluator" 11 | token: "{{ requiredEnv .github.token }}" 12 | username: "{{ requiredEnv .github.user }}" 13 | branch: "{{ .github.branch }}" 14 | commitusingapi: true # enable cryptographically signed commits 15 | commitmessage: 16 | hidecredit: true 17 | 18 | sources: 19 | rust-lang: 20 | name: Get the latest release of Rust from rust-lang 21 | kind: githubrelease 22 | spec: 23 | owner: rust-lang 24 | repository: rust 25 | token: "{{ requiredEnv .github.token }}" 26 | versionfilter: 27 | kind: semver 28 | pattern: "*" 29 | 30 | targets: 31 | dataFile: 32 | name: 'deps(rust): update Rust version to {{ source "rust-lang" }}' 33 | kind: toml 34 | scmid: github 35 | spec: 36 | file: "rust-toolchain.toml" 37 | key: toolchain.channel 38 | 39 | actions: 40 | default: 41 | kind: github/pullrequest 42 | scmid: github 43 | spec: 44 | labels: 45 | - kind/chore 46 | -------------------------------------------------------------------------------- /updatecli/values.yaml: -------------------------------------------------------------------------------- 1 | github: 2 | owner: "UPDATECLI_GITHUB_OWNER" 3 | token: "UPDATECLI_GITHUB_TOKEN" 4 | branch: "main" 5 | author: "Kubewarden bot" 6 | email: "cncf-kubewarden-maintainers@lists.cncf.io" 7 | user: "UPDATECLI_GITHUB_OWNER" 8 | --------------------------------------------------------------------------------