From fdc995e6f5f2c134d57b4ac080385f1aa1ea891b Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 8 Sep 2026 14:31:42 -0400 Subject: [PATCH 1/3] Intercept and authorize wasi:http calls The gate components virtualize the wasi:http interfaces allowing http client and handler calls to be intercepted and authorized before the call precedes. A latch interface is imported containing the authorization logic. Multiple latches can be combined using the latch-n components. The latch-method component authorizes requests based on the HTTP request's method and a wasi:config object. The latch-method-readonly component is a configuration of latch-method that denies all methods except for those generally known not to cause side effects on a remote server. Signed-off-by: Scott Andrews --- .github/workflows/ci.yaml | 4 + Cargo.lock | 70 +++ Cargo.toml | 7 +- Makefile | 31 +- README.md | 14 + components/gate-client/Cargo.toml | 11 + components/gate-client/README.md | 9 + components/gate-client/src/lib.rs | 90 ++++ components/gate-handler/Cargo.toml | 11 + components/gate-handler/README.md | 9 + components/gate-handler/src/lib.rs | 90 ++++ components/gate/README.md | 11 + components/gate/gate.wac | 4 + components/latch-deny-all/Cargo.toml | 11 + components/latch-deny-all/README.md | 7 + components/latch-deny-all/src/lib.rs | 22 + components/latch-grant-all/Cargo.toml | 11 + components/latch-grant-all/README.md | 7 + components/latch-grant-all/src/lib.rs | 20 + .../latch-method-readonly-config/README.md | 7 + .../latch-method-readonly-config.properties | 5 + components/latch-method-readonly/README.md | 7 + .../latch-method-readonly.wac | 6 + components/latch-method/Cargo.toml | 11 + components/latch-method/README.md | 10 + components/latch-method/src/lib.rs | 85 +++ components/latch-n2/Cargo.toml | 11 + components/latch-n2/README.md | 8 + components/latch-n2/src/lib.rs | 18 + components/latch-n3/Cargo.toml | 11 + components/latch-n3/README.md | 8 + components/latch-n3/src/lib.rs | 18 + components/latch-n4/Cargo.toml | 11 + components/latch-n4/README.md | 8 + components/latch-n4/src/lib.rs | 23 + components/latch-n5/Cargo.toml | 11 + components/latch-n5/README.md | 8 + components/latch-n5/src/lib.rs | 24 + .../componentized-http-0.1.0-dev/package.wit | 77 +++ .../deps/wasi-config-0.2.0-rc.1/package.wit | 33 ++ .../deps/wasi-logging-0.1.0-draft/package.wit | 36 ++ components/wit/worlds.wit | 32 ++ components/wkg.lock | 18 + crates/latch-n/Cargo.toml | 8 + crates/latch-n/src/lib.rs | 34 ++ wit/deps/wasi-cli-0.3.0/package.wit | 28 + wit/deps/wasi-clocks-0.3.0/package.wit | 43 ++ wit/deps/wasi-config-0.2.0-rc.1/package.wit | 33 ++ wit/deps/wasi-http-0.3.0/package.wit | 509 ++++++++++++++++++ wit/deps/wasi-random-0.3.0/package.wit | 18 + wit/http.wit | 2 - wit/latch.wit | 67 +++ wit/worlds.wit | 8 + wkg.lock | 19 +- 54 files changed, 1687 insertions(+), 7 deletions(-) create mode 100644 components/gate-client/Cargo.toml create mode 100644 components/gate-client/README.md create mode 100644 components/gate-client/src/lib.rs create mode 100644 components/gate-handler/Cargo.toml create mode 100644 components/gate-handler/README.md create mode 100644 components/gate-handler/src/lib.rs create mode 100644 components/gate/README.md create mode 100644 components/gate/gate.wac create mode 100644 components/latch-deny-all/Cargo.toml create mode 100644 components/latch-deny-all/README.md create mode 100644 components/latch-deny-all/src/lib.rs create mode 100644 components/latch-grant-all/Cargo.toml create mode 100644 components/latch-grant-all/README.md create mode 100644 components/latch-grant-all/src/lib.rs create mode 100644 components/latch-method-readonly-config/README.md create mode 100644 components/latch-method-readonly-config/latch-method-readonly-config.properties create mode 100644 components/latch-method-readonly/README.md create mode 100644 components/latch-method-readonly/latch-method-readonly.wac create mode 100644 components/latch-method/Cargo.toml create mode 100644 components/latch-method/README.md create mode 100644 components/latch-method/src/lib.rs create mode 100644 components/latch-n2/Cargo.toml create mode 100644 components/latch-n2/README.md create mode 100644 components/latch-n2/src/lib.rs create mode 100644 components/latch-n3/Cargo.toml create mode 100644 components/latch-n3/README.md create mode 100644 components/latch-n3/src/lib.rs create mode 100644 components/latch-n4/Cargo.toml create mode 100644 components/latch-n4/README.md create mode 100644 components/latch-n4/src/lib.rs create mode 100644 components/latch-n5/Cargo.toml create mode 100644 components/latch-n5/README.md create mode 100644 components/latch-n5/src/lib.rs create mode 100644 components/wit/deps/wasi-config-0.2.0-rc.1/package.wit create mode 100644 components/wit/deps/wasi-logging-0.1.0-draft/package.wit create mode 100644 crates/latch-n/Cargo.toml create mode 100644 crates/latch-n/src/lib.rs create mode 100644 wit/deps/wasi-cli-0.3.0/package.wit create mode 100644 wit/deps/wasi-clocks-0.3.0/package.wit create mode 100644 wit/deps/wasi-config-0.2.0-rc.1/package.wit create mode 100644 wit/deps/wasi-http-0.3.0/package.wit create mode 100644 wit/deps/wasi-random-0.3.0/package.wit create mode 100644 wit/latch.wit diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 472364b..9a671ec 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,6 +19,10 @@ jobs: uses: cargo-bins/cargo-binstall@main - name: Install wasmtime run: cargo binstall --force wasmtime-cli + - name: Install static-config + run: cargo binstall --force static-config + - name: Install wac + run: cargo binstall --force wac-cli - name: Install wkg run: cargo binstall --force wkg - name: Install wasm-tools diff --git a/Cargo.lock b/Cargo.lock index ef765ce..c5eddd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,20 @@ dependencies = [ "slab", ] +[[package]] +name = "gate-client" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "gate-handler" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -257,6 +271,62 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "latch-deny-all" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "latch-grant-all" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "latch-method" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "latch-n" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "latch-n2" +version = "0.1.0" +dependencies = [ + "latch-n", +] + +[[package]] +name = "latch-n3" +version = "0.1.0" +dependencies = [ + "latch-n", +] + +[[package]] +name = "latch-n4" +version = "0.1.0" +dependencies = [ + "latch-n", +] + +[[package]] +name = "latch-n5" +version = "0.1.0" +dependencies = [ + "latch-n", +] + [[package]] name = "leb128fmt" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index be849b6..f50ec0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,12 +2,15 @@ resolver = "2" members = [ "components/*", + "crates/*", ] exclude = [ + "components/gate", + "components/latch-method-readonly", + "components/latch-method-readonly-config", "components/wit", ] [workspace.dependencies] -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +latch-n = { path = "./crates/latch-n" } wit-bindgen = "0.61.0" diff --git a/Makefile b/Makefile index 150ffbd..fb08393 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,10 @@ SHELL := /bin/bash export RUST_BACKTRACE ?= 1 export WASMTIME_BACKTRACE_DETAILS ?= 1 -COMPONENTS = $(sort $(notdir $(patsubst %/,%,$(dir $(wildcard components/*/Cargo.toml))))) +CARGO_COMPONENTS = $(sort $(notdir $(patsubst %/,%,$(dir $(wildcard components/*/Cargo.toml))))) +CONFIG_COMPONENTS = $(sort $(notdir $(patsubst %/,%,$(dir $(wildcard components/*/*.properties))))) +WAC_COMPONENTS = $(sort $(notdir $(patsubst %/,%,$(dir $(wildcard components/*/*.wac))))) +COMPONENTS = $(CARGO_COMPONENTS) $(CONFIG_COMPONENTS) $(WAC_COMPONENTS) .PHONY: all all: components @@ -19,13 +22,15 @@ test: @echo "TODO add tests" .PHONY: components -components: lib/interface.wasm $(foreach component,$(COMPONENTS),lib/$(component).wasm $(foreach component,$(COMPONENTS),lib/$(component).debug.wasm)) +components: lib/interface.wasm $(foreach component,$(COMPONENTS),lib/$(component).wasm) $(foreach component,$(COMPONENTS),lib/$(component).debug.wasm) define BUILD_COMPONENT .PHONY: components/$1 components/$1: lib/$1.wasm lib/$1.debug.wasm +ifneq ($(wildcard components/$1/Cargo.toml),) + lib/$1.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find components/$1 -type f) cargo build -p $1 --target wasm32-unknown-unknown --release wasm-tools component new target/wasm32-unknown-unknown/release/$(subst -,_,$1).wasm -o lib/$1.wasm @@ -36,6 +41,28 @@ lib/$1.debug.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find compon wasm-tools component new target/wasm32-unknown-unknown/debug/$(subst -,_,$1).wasm -o lib/$1.debug.wasm cp components/$1/README.md lib/$1.debug.wasm.md +else ifneq ($(wildcard components/$1/$1.properties),) + +lib/$1.wasm: components/$1/$1.properties components/$1/README.md + static-config -f components/$1/$1.properties -o lib/$1.wasm + cp components/$1/README.md lib/$1.wasm.md + +lib/$1.debug.wasm: components/$1/$1.properties components/$1/README.md + static-config -f components/$1/$1.properties -o lib/$1.debug.wasm + cp components/$1/README.md lib/$1.debug.wasm.md + +else ifneq ($(wildcard components/$1/$1.wac),) + +lib/$1.wasm: components/$1/$1.wac components/$1/README.md $(foreach component,$(CARGO_COMPONENTS),lib/$(component).wasm) $(foreach component,$(CONFIG_COMPONENTS),lib/$(component).wasm) + wac compose $(foreach component,$(COMPONENTS),-d local:$(component)=lib/$(component).wasm) -o lib/$1.wasm components/$1/$1.wac + cp components/$1/README.md lib/$1.wasm.md + +lib/$1.debug.wasm: components/$1/$1.wac components/$1/README.md $(foreach component,$(CARGO_COMPONENTS),lib/$(component).debug.wasm) $(foreach component,$(CONFIG_COMPONENTS),lib/$(component).debug.wasm) + wac compose $(foreach component,$(COMPONENTS),-d local:$(component)=lib/$(component).debug.wasm) -o lib/$1.debug.wasm components/$1/$1.wac + cp components/$1/README.md lib/$1.debug.wasm.md + +endif + endef $(foreach component,$(COMPONENTS),$(eval $(call BUILD_COMPONENT,$(component)))) diff --git a/README.md b/README.md index f4b6065..2732673 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ Prereqs: - a rust toolchain - [`wasm-tools`](https://github.com/bytecodealliance/wasm-tools) - [`wkg`](https://github.com/bytecodealliance/wasm-pkg-tools) +- [`wac`](https://github.com/bytecodealliance/wac) +- [`static-config`](https://github.com/componentized/static-config/) ```sh make components @@ -27,7 +29,19 @@ make components ### Components +- [`gate`](./components/gate/) +- [`gate-client`](./components/gate-client/) +- [`gate-handler`](./components/gate-handler/) - [`http-client`](./components/http-client/) +- [`latch-deny-all`](./components/latch-deny-all/) +- [`latch-grant-all`](./components/latch-grant-all/) +- [`latch-method`](./components/latch-method/) +- [`latch-method-readonly`](./components/latch-method-readonly/) +- [`latch-method-readonly-config`](./components/latch-method-readonly-config/) +- [`latch-n2`](./components/latch-n2/) +- [`latch-n3`](./components/latch-n3/) +- [`latch-n4`](./components/latch-n4/) +- [`latch-n5`](./components/latch-n5/) ## Community diff --git a/components/gate-client/Cargo.toml b/components/gate-client/Cargo.toml new file mode 100644 index 0000000..32395f4 --- /dev/null +++ b/components/gate-client/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "gate-client" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/gate-client/README.md b/components/gate-client/README.md new file mode 100644 index 0000000..8aaf2b2 --- /dev/null +++ b/components/gate-client/README.md @@ -0,0 +1,9 @@ +# `gate-client` + +HTTP client gate access control. + +## The `gate-client` World + +- exports `wasi:http/client@0.3` +- imports `componentized:http/latch` +- imports `wasi:http/client@0.3` diff --git a/components/gate-client/src/lib.rs b/components/gate-client/src/lib.rs new file mode 100644 index 0000000..a37a16e --- /dev/null +++ b/components/gate-client/src/lib.rs @@ -0,0 +1,90 @@ +#![no_main] + +use std::fmt::Display; + +use crate::{ + componentized::http::latch::{ + self, authorize, ClientOperation, Decision::Denied, Operation, SendArgs, + }, + exports::wasi::http::client::{ErrorCode, Guest, Request, Response}, + wasi::{ + http::{client, types}, + logging::logging::{log, Level}, + }, +}; + +macro_rules! warn { + ($dst:expr, $($arg:tt)*) => { + log(Level::Warn, "componentized-gate", &format!($dst, $($arg)*)); + }; + ($dst:expr) => { + log(Level::Warn, "componentized-gate", &format!($dst)); + }; +} + +struct GatedHttpClient {} + +impl Guest for GatedHttpClient { + #[doc = "/ This function may be used to either send an outgoing request over the"] + #[doc = "/ network or to forward it to another component."] + #[allow(async_fn_in_trait)] + async fn send(request: Request) -> Result { + match authorize(&Operation::Client(ClientOperation::Send(SendArgs { + request: &request, + })))? { + Some(Denied(reason)) => { + warn!( + "Denied REASON={reason} OPERATION=wasi:http/client#send METHOD={} PATH={}", + request.get_method(), + request.get_path_with_query().unwrap_or("/".to_string()) + ); + Err(reason) + } + _ => client::send(request).await, + } + } +} + +impl Display for types::Request { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let method = self.get_method(); + let url = self.get_path_with_query().unwrap_or("/".to_string()); + f.write_fmt(format_args!("{method} {url}")) + } +} + +impl Display for types::Method { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let method = match self { + types::Method::Get => "GET", + types::Method::Head => "HEAD", + types::Method::Post => "POST", + types::Method::Put => "PUT", + types::Method::Delete => "DELETE", + types::Method::Connect => "CONNECT", + types::Method::Options => "OPTIONS", + types::Method::Trace => "TRACE", + types::Method::Patch => "PATCH", + types::Method::Other(method) => &method.to_uppercase(), + }; + f.write_str(method) + } +} + +impl From for ErrorCode { + fn from(value: latch::ErrorCode) -> Self { + match value { + latch::ErrorCode::Http(error_code) => error_code, + latch::ErrorCode::Other(error_code) => Self::InternalError(error_code), + } + } +} + +wit_bindgen::generate!({ + path: "../wit", + world: "gate-client", + merge_structurally_equal_types: true, + generate_all +}); + +export!(GatedHttpClient); diff --git a/components/gate-handler/Cargo.toml b/components/gate-handler/Cargo.toml new file mode 100644 index 0000000..d257a32 --- /dev/null +++ b/components/gate-handler/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "gate-handler" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/gate-handler/README.md b/components/gate-handler/README.md new file mode 100644 index 0000000..68ec103 --- /dev/null +++ b/components/gate-handler/README.md @@ -0,0 +1,9 @@ +# `gate-handler` + +HTTP handler gate access control. + +## The `gate-handler` World + +- exports `wasi:http/hander@0.3` +- imports `componentized:http/latch` +- imports `wasi:http/hander@0.3` diff --git a/components/gate-handler/src/lib.rs b/components/gate-handler/src/lib.rs new file mode 100644 index 0000000..287053b --- /dev/null +++ b/components/gate-handler/src/lib.rs @@ -0,0 +1,90 @@ +#![no_main] + +use std::fmt::Display; + +use crate::{ + componentized::http::latch::{ + self, authorize, Decision::Denied, HandleArgs, HandlerOperation, Operation, + }, + exports::wasi::http::handler::{ErrorCode, Guest, Request, Response}, + wasi::{ + http::{handler, types}, + logging::logging::{log, Level}, + }, +}; + +macro_rules! warn { + ($dst:expr, $($arg:tt)*) => { + log(Level::Warn, "componentized-gate", &format!($dst, $($arg)*)); + }; + ($dst:expr) => { + log(Level::Warn, "componentized-gate", &format!($dst)); + }; +} + +struct GatedHttpHandler {} + +impl Guest for GatedHttpHandler { + #[doc = "/ This function may be called with either an incoming request read from the"] + #[doc = "/ network or a request synthesized or forwarded by another component."] + #[allow(async_fn_in_trait)] + async fn handle(request: Request) -> Result { + match authorize(&Operation::Handler(HandlerOperation::Handle(HandleArgs { + request: &request, + })))? { + Some(Denied(reason)) => { + warn!( + "Denied REASON={reason} OPERATION=wasi:http/handler#handle METHOD={} PATH={}", + request.get_method(), + request.get_path_with_query().unwrap_or("/".to_string()) + ); + Err(reason) + } + _ => handler::handle(request).await, + } + } +} + +impl Display for types::Request { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let method = self.get_method(); + let url = self.get_path_with_query().unwrap_or("/".to_string()); + f.write_fmt(format_args!("{method} {url}")) + } +} + +impl Display for types::Method { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let method = match self { + types::Method::Get => "GET", + types::Method::Head => "HEAD", + types::Method::Post => "POST", + types::Method::Put => "PUT", + types::Method::Delete => "DELETE", + types::Method::Connect => "CONNECT", + types::Method::Options => "OPTIONS", + types::Method::Trace => "TRACE", + types::Method::Patch => "PATCH", + types::Method::Other(method) => &method.to_uppercase(), + }; + f.write_str(method) + } +} + +impl From for ErrorCode { + fn from(value: latch::ErrorCode) -> Self { + match value { + latch::ErrorCode::Http(error_code) => error_code, + latch::ErrorCode::Other(error_code) => Self::InternalError(error_code), + } + } +} + +wit_bindgen::generate!({ + path: "../wit", + world: "gate-handler", + merge_structurally_equal_types: true, + generate_all +}); + +export!(GatedHttpHandler); diff --git a/components/gate/README.md b/components/gate/README.md new file mode 100644 index 0000000..634b90a --- /dev/null +++ b/components/gate/README.md @@ -0,0 +1,11 @@ +# `gate` + +HTTP client and handler gate access control. + +## The `gate` World + +- exports `wasi:http/client@0.3` +- exports `wasi:http/hander@0.3` +- imports `componentized:http/latch` +- imports `wasi:http/client@0.3` +- imports `wasi:http/hander@0.3` diff --git a/components/gate/gate.wac b/components/gate/gate.wac new file mode 100644 index 0000000..9cde461 --- /dev/null +++ b/components/gate/gate.wac @@ -0,0 +1,4 @@ +package componentized:http; + +export new local:gate-client{ ... }...; +export new local:gate-handler{ ... }...; diff --git a/components/latch-deny-all/Cargo.toml b/components/latch-deny-all/Cargo.toml new file mode 100644 index 0000000..741f659 --- /dev/null +++ b/components/latch-deny-all/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-deny-all" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/latch-deny-all/README.md b/components/latch-deny-all/README.md new file mode 100644 index 0000000..4a5b12b --- /dev/null +++ b/components/latch-deny-all/README.md @@ -0,0 +1,7 @@ +# `latch-deny-all` + +HTTP latch that implicitly denies all operations. + +## The `latch-deny-all` World + +- exports `componentized:http/latch` diff --git a/components/latch-deny-all/src/lib.rs b/components/latch-deny-all/src/lib.rs new file mode 100644 index 0000000..c6f88cf --- /dev/null +++ b/components/latch-deny-all/src/lib.rs @@ -0,0 +1,22 @@ +#![no_main] + +use crate::exports::componentized::http::latch::{ + Decision, ErrorCode, Guest as Latch, HttpErrorCode, Operation, +}; + +struct DenyAllLatch {} + +impl Latch for DenyAllLatch { + fn authorize(_: Operation) -> Result, ErrorCode> { + Ok(Some(Decision::Denied(HttpErrorCode::HttpRequestDenied))) + } +} + +wit_bindgen::generate!({ + path: "../wit", + world: "http-latch", + merge_structurally_equal_types: true, + generate_all +}); + +export!(DenyAllLatch); diff --git a/components/latch-grant-all/Cargo.toml b/components/latch-grant-all/Cargo.toml new file mode 100644 index 0000000..e37c55f --- /dev/null +++ b/components/latch-grant-all/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-grant-all" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/latch-grant-all/README.md b/components/latch-grant-all/README.md new file mode 100644 index 0000000..9a90838 --- /dev/null +++ b/components/latch-grant-all/README.md @@ -0,0 +1,7 @@ +# `latch-grant-all` + +HTTP latch that implicitly grants all operations. + +## The `latch-grant-all` World + +- exports `componentized:http/latch` diff --git a/components/latch-grant-all/src/lib.rs b/components/latch-grant-all/src/lib.rs new file mode 100644 index 0000000..5963e01 --- /dev/null +++ b/components/latch-grant-all/src/lib.rs @@ -0,0 +1,20 @@ +#![no_main] + +use crate::exports::componentized::http::latch::{Decision, ErrorCode, Guest as Latch, Operation}; + +struct GrantAllLatch {} + +impl Latch for GrantAllLatch { + fn authorize(_: Operation) -> Result, ErrorCode> { + Ok(Some(Decision::Granted)) + } +} + +wit_bindgen::generate!({ + path: "../wit", + world: "http-latch", + merge_structurally_equal_types: true, + generate_all +}); + +export!(GrantAllLatch); diff --git a/components/latch-method-readonly-config/README.md b/components/latch-method-readonly-config/README.md new file mode 100644 index 0000000..2c4e148 --- /dev/null +++ b/components/latch-method-readonly-config/README.md @@ -0,0 +1,7 @@ +# `latch-method-readonly-config` + +Configuration for a latch-method HTTP latch that implicitly denies all request methods except for GET, HEAD, QUERY, and OPTIONS. + +## The `latch-method-readonly-config` World + +- exports `wasi:config/store@0.2` diff --git a/components/latch-method-readonly-config/latch-method-readonly-config.properties b/components/latch-method-readonly-config/latch-method-readonly-config.properties new file mode 100644 index 0000000..029c94d --- /dev/null +++ b/components/latch-method-readonly-config/latch-method-readonly-config.properties @@ -0,0 +1,5 @@ +get=abstained +head=abstained +query=abstained +options=abstained +*=denied diff --git a/components/latch-method-readonly/README.md b/components/latch-method-readonly/README.md new file mode 100644 index 0000000..22e43ff --- /dev/null +++ b/components/latch-method-readonly/README.md @@ -0,0 +1,7 @@ +# `latch-method-readonly` + +HTTP latch that implicitly denies all request methods except for GET, HEAD, QUERY, and OPTIONS. + +## The `latch-method-readonly` World + +- exports `componentized:http/latch` diff --git a/components/latch-method-readonly/latch-method-readonly.wac b/components/latch-method-readonly/latch-method-readonly.wac new file mode 100644 index 0000000..4c92728 --- /dev/null +++ b/components/latch-method-readonly/latch-method-readonly.wac @@ -0,0 +1,6 @@ +package componentized:http; + +export new local:latch-method{ + store: new local:latch-method-readonly-config{}.store, + ... +}...; diff --git a/components/latch-method/Cargo.toml b/components/latch-method/Cargo.toml new file mode 100644 index 0000000..0703a77 --- /dev/null +++ b/components/latch-method/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-method" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/components/latch-method/README.md b/components/latch-method/README.md new file mode 100644 index 0000000..9fadc83 --- /dev/null +++ b/components/latch-method/README.md @@ -0,0 +1,10 @@ +# `latch-method` + +HTTP latch that makes decisions based on the request's method. + +Authorization is granted or denied based on the wasi:config with the lowercase method value as the config key, and `granted`, `denied`, or `abstained` as the value. A default decision may specified under the `*` key. + +## The `latch-method` World + +- imports `wasi:config/store@0.2` +- exports `componentized:http/latch` diff --git a/components/latch-method/src/lib.rs b/components/latch-method/src/lib.rs new file mode 100644 index 0000000..fcbf28d --- /dev/null +++ b/components/latch-method/src/lib.rs @@ -0,0 +1,85 @@ +#![no_main] + +use crate::{ + exports::componentized::http::latch::{ + ClientOperation, Decision, ErrorCode, Guest as Latch, HandlerOperation, HttpErrorCode, + Operation, + }, + wasi::{config::store as config, http::types::Method}, +}; + +const ABSTAINED: &str = "abstained"; +const GRANTED: &str = "granted"; +const DENIED: &str = "denied"; +const WILDCARD: &str = "*"; + +struct MethodLatch {} + +impl MethodLatch { + fn authorize_method(method: Method) -> Result, ErrorCode> { + let method = match method { + Method::Get => "get", + Method::Head => "head", + Method::Post => "post", + Method::Put => "put", + Method::Delete => "delete", + Method::Connect => "connect", + Method::Options => "options", + Method::Trace => "trace", + Method::Patch => "patch", + Method::Other(method) => &method.to_lowercase(), + }; + + match config::get(method)? { + Some(method_value) => Self::parse_decision(method_value), + None => match config::get(WILDCARD)? { + Some(default_value) => Self::parse_decision(default_value), + None => Ok(None), + }, + } + } + + fn parse_decision(value: String) -> Result, ErrorCode> { + match value.as_str() { + "" | ABSTAINED => Ok(None), + GRANTED => Ok(Some(Decision::Granted)), + DENIED => Ok(Some(Decision::Denied( + HttpErrorCode::HttpRequestMethodInvalid, + ))), + val => Err(ErrorCode::Other(Some(format!( + "unknown decision value '{val}', expected one of: '{ABSTAINED}', '{GRANTED}', '{DENIED}'" + )))), + } + } +} + +impl Latch for MethodLatch { + fn authorize(op: Operation) -> Result, ErrorCode> { + match op { + Operation::Client(client_operation) => match client_operation { + ClientOperation::Send(args) => Self::authorize_method(args.request.get_method()), + }, + Operation::Handler(handler_operation) => match handler_operation { + HandlerOperation::Handle(args) => Self::authorize_method(args.request.get_method()), + }, + } + } +} + +impl From for ErrorCode { + fn from(value: config::Error) -> Self { + match value { + config::Error::Upstream(error) => Self::Other(Some(error)), + config::Error::Io(error) => Self::Other(Some(error)), + } + } +} + +wit_bindgen::generate!({ + path: "../wit", + world: "http-latch", + merge_structurally_equal_types: true, + generate_all +}); + +export!(MethodLatch); diff --git a/components/latch-n2/Cargo.toml b/components/latch-n2/Cargo.toml new file mode 100644 index 0000000..4fbab2a --- /dev/null +++ b/components/latch-n2/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-n2" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +latch-n = { workspace = true } diff --git a/components/latch-n2/README.md b/components/latch-n2/README.md new file mode 100644 index 0000000..6055554 --- /dev/null +++ b/components/latch-n2/README.md @@ -0,0 +1,8 @@ +# `latch-n2` + +HTTP latch that aggregates two other HTTP latches. + +## The `latch-n2` World + +- imports `componentized:http/latch` (as latch and latch1) +- exports `componentized:http/latch` diff --git a/components/latch-n2/src/lib.rs b/components/latch-n2/src/lib.rs new file mode 100644 index 0000000..44a7106 --- /dev/null +++ b/components/latch-n2/src/lib.rs @@ -0,0 +1,18 @@ +#![no_main] + +use latch_n::bindings::componentized::http::{latch as latch0, latch1}; +use latch_n::bindings::exports::componentized::http::latch::{ + Decision, ErrorCode, Guest as Latch, Operation, +}; + +struct LatchN2 {} + +impl Latch for LatchN2 { + #[allow(async_fn_in_trait)] + fn authorize(operation: Operation<'_>) -> Result, ErrorCode> { + let authorizers = vec![latch0::authorize, latch1::authorize]; + latch_n::authorize(operation, authorizers) + } +} + +latch_n::export!(LatchN2 with_types_in latch_n::bindings); diff --git a/components/latch-n3/Cargo.toml b/components/latch-n3/Cargo.toml new file mode 100644 index 0000000..baf0a07 --- /dev/null +++ b/components/latch-n3/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-n3" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +latch-n = { workspace = true } diff --git a/components/latch-n3/README.md b/components/latch-n3/README.md new file mode 100644 index 0000000..d94f1d4 --- /dev/null +++ b/components/latch-n3/README.md @@ -0,0 +1,8 @@ +# `latch-n3` + +HTTP latch that aggregates three other HTTP latches. + +## The `latch-n3` World + +- imports `componentized:http/latch` (as latch, latch1, and latch2) +- exports `componentized:http/latch` diff --git a/components/latch-n3/src/lib.rs b/components/latch-n3/src/lib.rs new file mode 100644 index 0000000..96b612f --- /dev/null +++ b/components/latch-n3/src/lib.rs @@ -0,0 +1,18 @@ +#![no_main] + +use latch_n::bindings::componentized::http::{latch as latch0, latch1, latch2}; +use latch_n::bindings::exports::componentized::http::latch::{ + Decision, ErrorCode, Guest as Latch, Operation, +}; + +struct LatchN3 {} + +impl Latch for LatchN3 { + #[allow(async_fn_in_trait)] + fn authorize(operation: Operation<'_>) -> Result, ErrorCode> { + let authorizers = vec![latch0::authorize, latch1::authorize, latch2::authorize]; + latch_n::authorize(operation, authorizers) + } +} + +latch_n::export!(LatchN3 with_types_in latch_n::bindings); diff --git a/components/latch-n4/Cargo.toml b/components/latch-n4/Cargo.toml new file mode 100644 index 0000000..d6bc670 --- /dev/null +++ b/components/latch-n4/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-n4" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +latch-n = { workspace = true } diff --git a/components/latch-n4/README.md b/components/latch-n4/README.md new file mode 100644 index 0000000..8469613 --- /dev/null +++ b/components/latch-n4/README.md @@ -0,0 +1,8 @@ +# `latch-n4` + +HTTP latch that aggregates four other HTTP latches. + +## The `latch-n4` World + +- imports `componentized:http/latch` (as latch, latch1, latch2, and latch3) +- exports `componentized:http/latch` diff --git a/components/latch-n4/src/lib.rs b/components/latch-n4/src/lib.rs new file mode 100644 index 0000000..6733d43 --- /dev/null +++ b/components/latch-n4/src/lib.rs @@ -0,0 +1,23 @@ +#![no_main] + +use latch_n::bindings::componentized::http::{latch as latch0, latch1, latch2, latch3}; +use latch_n::bindings::exports::componentized::http::latch::{ + Decision, ErrorCode, Guest as Latch, Operation, +}; + +struct LatchN4 {} + +impl Latch for LatchN4 { + #[allow(async_fn_in_trait)] + fn authorize(operation: Operation<'_>) -> Result, ErrorCode> { + let authorizers = vec![ + latch0::authorize, + latch1::authorize, + latch2::authorize, + latch3::authorize, + ]; + latch_n::authorize(operation, authorizers) + } +} + +latch_n::export!(LatchN4 with_types_in latch_n::bindings); diff --git a/components/latch-n5/Cargo.toml b/components/latch-n5/Cargo.toml new file mode 100644 index 0000000..1140d89 --- /dev/null +++ b/components/latch-n5/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "latch-n5" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +latch-n = { workspace = true } diff --git a/components/latch-n5/README.md b/components/latch-n5/README.md new file mode 100644 index 0000000..ecba733 --- /dev/null +++ b/components/latch-n5/README.md @@ -0,0 +1,8 @@ +# `latch-n5` + +HTTP latch that aggregates five other HTTP latches. + +## The `latch-n5` World + +- imports `componentized:http/latch` (as latch, latch1, latch2, latch3, and latch4) +- exports `componentized:http/latch` diff --git a/components/latch-n5/src/lib.rs b/components/latch-n5/src/lib.rs new file mode 100644 index 0000000..595fe08 --- /dev/null +++ b/components/latch-n5/src/lib.rs @@ -0,0 +1,24 @@ +#![no_main] + +use latch_n::bindings::componentized::http::{latch as latch0, latch1, latch2, latch3, latch4}; +use latch_n::bindings::exports::componentized::http::latch::{ + Decision, ErrorCode, Guest as Latch, Operation, +}; + +struct LatchN5 {} + +impl Latch for LatchN5 { + #[allow(async_fn_in_trait)] + fn authorize(operation: Operation<'_>) -> Result, ErrorCode> { + let authorizers = vec![ + latch0::authorize, + latch1::authorize, + latch2::authorize, + latch3::authorize, + latch4::authorize, + ]; + latch_n::authorize(operation, authorizers) + } +} + +latch_n::export!(LatchN5 with_types_in latch_n::bindings); diff --git a/components/wit/deps/componentized-http-0.1.0-dev/package.wit b/components/wit/deps/componentized-http-0.1.0-dev/package.wit index 38d3bb1..be7102e 100644 --- a/components/wit/deps/componentized-http-0.1.0-dev/package.wit +++ b/components/wit/deps/componentized-http-0.1.0-dev/package.wit @@ -66,6 +66,83 @@ interface client { query: async func(url: string, headers: list>, body: stream, options: option) -> result; } +interface latch { + use wasi:http/types@0.3.0.{error-code as http-error-code, request}; + + record send-args { + request: borrow, + } + + record handle-args { + request: borrow, + } + + variant client-operation { + send(send-args), + } + + variant handler-operation { + handle(handle-args), + } + + variant operation { + client(client-operation), + handler(handler-operation), + } + + variant decision { + granted, + denied(http-error-code), + } + + variant error-code { + http(http-error-code), + other(option), + } + + authorize: func(operation: operation) -> result, error-code>; +} + +interface latch0 { + use latch.{operation, decision, error-code}; + + authorize: func(operation: operation) -> result, error-code>; +} + +interface latch1 { + use latch.{operation, decision, error-code}; + + authorize: func(operation: operation) -> result, error-code>; +} + +interface latch2 { + use latch.{operation, decision, error-code}; + + authorize: func(operation: operation) -> result, error-code>; +} + +interface latch3 { + use latch.{operation, decision, error-code}; + + authorize: func(operation: operation) -> result, error-code>; +} + +interface latch4 { + use latch.{operation, decision, error-code}; + + authorize: func(operation: operation) -> result, error-code>; +} + world imports { import client; + import wasi:clocks/types@0.3.0; + import wasi:http/types@0.3.0; + import latch; +} +world http-latch { + import wasi:config/store@0.2.0-rc.1; + import wasi:clocks/types@0.3.0; + import wasi:http/types@0.3.0; + + export latch; } diff --git a/components/wit/deps/wasi-config-0.2.0-rc.1/package.wit b/components/wit/deps/wasi-config-0.2.0-rc.1/package.wit new file mode 100644 index 0000000..d8950ee --- /dev/null +++ b/components/wit/deps/wasi-config-0.2.0-rc.1/package.wit @@ -0,0 +1,33 @@ +package wasi:config@0.2.0-rc.1; + +interface store { + /// An error type that encapsulates the different errors that can occur fetching configuration values. + variant error { + /// This indicates an error from an "upstream" config source. + /// As this could be almost _anything_ (such as Vault, Kubernetes ConfigMaps, KeyValue buckets, etc), + /// the error message is a string. + upstream(string), + /// This indicates an error from an I/O operation. + /// As this could be almost _anything_ (such as a file read, network connection, etc), + /// the error message is a string. + /// Depending on how this ends up being consumed, + /// we may consider moving this to use the `wasi:io/error` type instead. + /// For simplicity right now in supporting multiple implementations, it is being left as a string. + io(string), + } + + /// Gets a configuration value of type `string` associated with the `key`. + /// + /// The value is returned as an `option`. If the key is not found, + /// `Ok(none)` is returned. If an error occurs, an `Err(error)` is returned. + get: func(key: string) -> result, error>; + + /// Gets a list of configuration key-value pairs of type `string`. + /// + /// If an error occurs, an `Err(error)` is returned. + get-all: func() -> result>, error>; +} + +world imports { + import store; +} diff --git a/components/wit/deps/wasi-logging-0.1.0-draft/package.wit b/components/wit/deps/wasi-logging-0.1.0-draft/package.wit new file mode 100644 index 0000000..164cb5b --- /dev/null +++ b/components/wit/deps/wasi-logging-0.1.0-draft/package.wit @@ -0,0 +1,36 @@ +package wasi:logging@0.1.0-draft; + +/// WASI Logging is a logging API intended to let users emit log messages with +/// simple priority levels and context values. +interface logging { + /// A log level, describing a kind of message. + enum level { + /// Describes messages about the values of variables and the flow of + /// control within a program. + trace, + /// Describes messages likely to be of interest to someone debugging a + /// program. + debug, + /// Describes messages likely to be of interest to someone monitoring a + /// program. + info, + /// Describes messages indicating hazardous situations. + warn, + /// Describes messages indicating serious errors. + error, + /// Describes messages indicating fatal errors. + critical, + } + + /// Emit a log message. + /// + /// A log message has a `level` describing what kind of message is being + /// sent, a context, which is an uninterpreted string meant to help + /// consumers group similar messages, and a string containing the message + /// text. + log: func(level: level, context: string, message: string); +} + +world imports { + import logging; +} diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index 18cb318..9806932 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -4,3 +4,35 @@ world http-client { import wasi:http/client@0.3.0; export componentized:http/client@0.1.0-dev; } + +world gate-client { + import componentized:http/latch@0.1.0-dev; + import wasi:config/store@0.2.0-rc.1; + import wasi:logging/logging@0.1.0-draft; + import wasi:http/client@0.3.0; + export wasi:http/client@0.3.0; +} + +world gate-handler { + import componentized:http/latch@0.1.0-dev; + import wasi:config/store@0.2.0-rc.1; + import wasi:logging/logging@0.1.0-draft; + import wasi:http/handler@0.3.0; + export wasi:http/handler@0.3.0; +} + +world http-latch { + import wasi:config/store@0.2.0-rc.1; + export componentized:http/latch@0.1.0-dev; +} + +world http-latch-n { + export componentized:http/latch@0.1.0-dev; + import componentized:http/latch@0.1.0-dev; + import componentized:http/latch1@0.1.0-dev; + import componentized:http/latch2@0.1.0-dev; + import componentized:http/latch3@0.1.0-dev; + import componentized:http/latch4@0.1.0-dev; + import wasi:http/client@0.3.0; + import wasi:http/types@0.3.0; +} diff --git a/components/wkg.lock b/components/wkg.lock index 3912411..7889439 100644 --- a/components/wkg.lock +++ b/components/wkg.lock @@ -11,6 +11,15 @@ requirement = "=0.3.0" version = "0.3.0" digest = "sha256:59e1f4079e64ada450e19ffc9d08854c904e356ed8cc1fda36ff4fe150264db2" +[[packages]] +name = "wasi:config" +registry = "wasi.dev" + +[[packages.versions]] +requirement = "=0.2.0-rc.1" +version = "0.2.0-rc.1" +digest = "sha256:1b7f1b0fd07bb4cede16c6a6ec8852815dfb924639a78735fc7bdffdc164485d" + [[packages]] name = "wasi:http" registry = "wasi.dev" @@ -24,3 +33,12 @@ digest = "sha256:92cd8f3730c00226dc15626a2e7b21834dd187fc221f09818720d228585bbbf requirement = "=0.3.1" version = "0.3.1" digest = "sha256:fac6e40c9b8101cb0a32c76ae63271ffb7c9a66007214d57f3ae8048844d2b5f" + +[[packages]] +name = "wasi:logging" +registry = "wasi.dev" + +[[packages.versions]] +requirement = "=0.1.0-draft" +version = "0.1.0-draft" +digest = "sha256:09621a45b12b0a9cddc798517f778aac0e5ae4bd234077b3d70758d6cf625580" diff --git a/crates/latch-n/Cargo.toml b/crates/latch-n/Cargo.toml new file mode 100644 index 0000000..89c2b9e --- /dev/null +++ b/crates/latch-n/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "latch-n" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[dependencies] +wit-bindgen = { workspace = true } diff --git a/crates/latch-n/src/lib.rs b/crates/latch-n/src/lib.rs new file mode 100644 index 0000000..49af068 --- /dev/null +++ b/crates/latch-n/src/lib.rs @@ -0,0 +1,34 @@ +#![no_main] + +use crate::bindings::exports::componentized::http::latch::{Decision, ErrorCode, Operation}; + +pub fn authorize( + operation: Operation, + authorizers: Vec) -> Result, ErrorCode>>, +) -> Result, ErrorCode> { + for authorize in authorizers { + match authorize(&operation)? { + None => {} + Some(Decision::Granted) => return Ok(Some(Decision::Granted)), + Some(Decision::Denied(error_code)) => return Ok(Some(Decision::Denied(error_code))), + } + } + Ok(None) +} + +pub mod bindings { + wit_bindgen::generate!({ + path: "../../components/wit", + world: "http-latch-n", + pub_export_macro: true, + merge_structurally_equal_types: true, + generate_all + }); +} + +#[macro_export] +macro_rules! export { + ($($t:tt)*) => { + $crate::bindings::export!($($t)*); + }; +} diff --git a/wit/deps/wasi-cli-0.3.0/package.wit b/wit/deps/wasi-cli-0.3.0/package.wit new file mode 100644 index 0000000..d0b02bb --- /dev/null +++ b/wit/deps/wasi-cli-0.3.0/package.wit @@ -0,0 +1,28 @@ +package wasi:cli@0.3.0; + +interface types { + enum error-code { + io, + illegal-byte-sequence, + pipe, + } +} + +interface stdout { + use types.{error-code}; + + write-via-stream: func(data: stream) -> future>; +} + +interface stderr { + use types.{error-code}; + + write-via-stream: func(data: stream) -> future>; +} + +interface stdin { + use types.{error-code}; + + read-via-stream: func() -> tuple, future>>; +} + diff --git a/wit/deps/wasi-clocks-0.3.0/package.wit b/wit/deps/wasi-clocks-0.3.0/package.wit new file mode 100644 index 0000000..871adf0 --- /dev/null +++ b/wit/deps/wasi-clocks-0.3.0/package.wit @@ -0,0 +1,43 @@ +package wasi:clocks@0.3.0; + +interface types { + type duration = u64; +} + +interface monotonic-clock { + use types.{duration}; + + type mark = u64; + + now: func() -> mark; + + get-resolution: func() -> duration; + + wait-until: async func(when: mark); + + wait-for: async func(how-long: duration); +} + +interface system-clock { + use types.{duration}; + + record instant { + seconds: s64, + nanoseconds: u32, + } + + now: func() -> instant; + + get-resolution: func() -> duration; +} + +interface timezone { + use system-clock.{instant}; + + iana-id: func() -> option; + + utc-offset: func(when: instant) -> option; + + to-debug-string: func() -> string; +} + diff --git a/wit/deps/wasi-config-0.2.0-rc.1/package.wit b/wit/deps/wasi-config-0.2.0-rc.1/package.wit new file mode 100644 index 0000000..d8950ee --- /dev/null +++ b/wit/deps/wasi-config-0.2.0-rc.1/package.wit @@ -0,0 +1,33 @@ +package wasi:config@0.2.0-rc.1; + +interface store { + /// An error type that encapsulates the different errors that can occur fetching configuration values. + variant error { + /// This indicates an error from an "upstream" config source. + /// As this could be almost _anything_ (such as Vault, Kubernetes ConfigMaps, KeyValue buckets, etc), + /// the error message is a string. + upstream(string), + /// This indicates an error from an I/O operation. + /// As this could be almost _anything_ (such as a file read, network connection, etc), + /// the error message is a string. + /// Depending on how this ends up being consumed, + /// we may consider moving this to use the `wasi:io/error` type instead. + /// For simplicity right now in supporting multiple implementations, it is being left as a string. + io(string), + } + + /// Gets a configuration value of type `string` associated with the `key`. + /// + /// The value is returned as an `option`. If the key is not found, + /// `Ok(none)` is returned. If an error occurs, an `Err(error)` is returned. + get: func(key: string) -> result, error>; + + /// Gets a list of configuration key-value pairs of type `string`. + /// + /// If an error occurs, an `Err(error)` is returned. + get-all: func() -> result>, error>; +} + +world imports { + import store; +} diff --git a/wit/deps/wasi-http-0.3.0/package.wit b/wit/deps/wasi-http-0.3.0/package.wit new file mode 100644 index 0000000..08458f7 --- /dev/null +++ b/wit/deps/wasi-http-0.3.0/package.wit @@ -0,0 +1,509 @@ +package wasi:http@0.3.0; + +/// This interface defines all of the types and methods for implementing HTTP +/// Requests and Responses, as well as their headers, trailers, and bodies. +@since(version = 0.3.0) +interface types { + use wasi:clocks/types@0.3.0.{duration}; + + /// This type corresponds to HTTP standard Methods. + @since(version = 0.3.0) + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string), + } + + /// This type corresponds to HTTP standard Related Schemes. + @since(version = 0.3.0) + variant scheme { + HTTP, + HTTPS, + other(string), + } + + /// Defines the case payload type for `DNS-error` above: + @since(version = 0.3.0) + record DNS-error-payload { + rcode: option, + info-code: option, + } + + /// Defines the case payload type for `TLS-alert-received` above: + @since(version = 0.3.0) + record TLS-alert-received-payload { + alert-id: option, + alert-message: option, + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + @since(version = 0.3.0) + record field-size-payload { + field-name: option, + field-size: option, + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// + @since(version = 0.3.0) + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option), + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + @since(version = 0.3.0) + variant header-error { + /// This error indicates that a `field-name` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + /// This error indicates that a forbidden `field-name` was used when trying + /// to set a header in a `fields`. + forbidden, + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + /// This error indicates that the operation would exceed an + /// implementation-defined limit on field sizes. This may apply to + /// an individual `field-value`, a single `field-name` plus all its + /// values, or the total aggregate size of all fields. + size-exceeded, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. Implementations can use this to extend the error + /// type without breaking existing code. It also includes an optional + /// string for an unstructured description of the error. Users should not + /// depend on the string for diagnosing errors, as it's not required to be + /// consistent between implementations. + other(option), + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting fields of a `request-options` resource. + @since(version = 0.3.0) + variant request-options-error { + /// Indicates the specified field is not supported by this implementation. + not-supported, + /// Indicates that the operation on the `request-options` was not permitted + /// because it is immutable. + immutable, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. Implementations can use this to extend the error + /// type without breaking existing code. It also includes an optional + /// string for an unstructured description of the error. Users should not + /// depend on the string for diagnosing errors, as it's not required to be + /// consistent between implementations. + other(option), + } + + /// Field names are always strings. + /// + /// Field names should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + @since(version = 0.3.0) + type field-name = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + @since(version = 0.3.0) + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `request.headers`) might be be immutable. In an immutable fields, the + /// `set`, `append`, and `delete` operations will fail with + /// `header-error.immutable`. + /// + /// A `fields` resource should store `field-name`s and `field-value`s in their + /// original casing used to construct or mutate the `fields` resource. The `fields` + /// resource should use that original casing when serializing the fields for + /// transport or when returning them from a method. + /// + /// Implementations may impose limits on individual field values and on total + /// aggregate field section size. Operations that would exceed these limits + /// fail with `header-error.size-exceeded` + @since(version = 0.3.0) + resource fields { + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + constructor(); + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The tuple is a pair of the field name, represented as a string, and + /// Value, represented as a list of bytes. In a valid Fields, all names + /// and values are valid UTF-8 strings. However, values are not always + /// well-formed, so they are represented as a raw list of bytes. + /// + /// An error result will be returned if any header or value was + /// syntactically invalid, if a header was forbidden, or if the + /// entries would exceed an implementation size limit. + from-list: static func(entries: list>) -> result; + /// Get all of the values corresponding to a name. If the name is not present + /// in this `fields`, an empty list is returned. However, if the name is + /// present but empty, this is represented by a list with one or more + /// empty field-values present. + get: func(name: field-name) -> list; + /// Returns `true` when the name is present in this `fields`. If the name is + /// syntactically invalid, `false` is returned. + has: func(name: field-name) -> bool; + /// Set all of the values for a name. Clears any existing values for that + /// name, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.size-exceeded` if the name or values would + /// exceed an implementation-defined size limit. + set: func(name: field-name, value: list) -> result<_, header-error>; + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + delete: func(name: field-name) -> result<_, header-error>; + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Returns all values previously corresponding to the name, if any. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + get-and-delete: func(name: field-name) -> result, header-error>; + /// Append a value for a name. Does not change or delete any existing + /// values for that name. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.size-exceeded` if the value would exceed + /// an implementation-defined size limit. + append: func(name: field-name, value: field-value) -> result<_, header-error>; + /// Retrieve the full set of names and values in the Fields. Like the + /// constructor, the list represents each name-value pair. + /// + /// The outer list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The names and values are always returned in the original casing and in + /// the order in which they will be serialized for transport. + copy-all: func() -> list>; + /// Make a deep copy of the Fields. Equivalent in behavior to calling the + /// `fields` constructor on the return value of `copy-all`. The resulting + /// `fields` is mutable. + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + @since(version = 0.3.0) + type headers = fields; + + /// Trailers is an alias for Fields. + @since(version = 0.3.0) + type trailers = fields; + + /// Represents an HTTP Request. + @since(version = 0.3.0) + resource request { + /// Construct a new `request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// `headers` is the HTTP Headers for the Request. + /// + /// `contents` is the optional body content stream with `none` + /// representing a zero-length content stream. + /// Once it is closed, `trailers` future must resolve to a result. + /// If `trailers` resolves to an error, underlying connection + /// will be closed immediately. + /// + /// `options` is optional `request-options` resource to be used + /// if the request is sent over a network connection. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, a `request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `handler.handle` implementation + /// to reject invalid constructions of `request`. + /// + /// The returned future resolves to result of transmission of this request. + new: static func(headers: headers, contents: option>, trailers: future, error-code>>, options: option) -> tuple>>; + /// Get the Method for the Request. + get-method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + set-method: func(method: method) -> result; + /// Get the combination of the HTTP Path and Query for the Request. When + /// `none`, this represents an empty Path and empty Query. + get-path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. When + /// `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + set-path-with-query: func(path-with-query: option) -> result; + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + get-scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + set-scheme: func(scheme: option) -> result; + /// Get the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. + get-authority: func() -> option; + /// Set the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid URI authority. + set-authority: func(authority: option) -> result; + /// Get the `request-options` to be associated with this request + /// + /// The returned `request-options` resource is immutable: `set-*` operations + /// will fail if invoked. + /// + /// This `request-options` resource is a child: it must be dropped before + /// the parent `request` is dropped, or its ownership is transferred to + /// another component by e.g. `handler.handle`. + get-options: func() -> option; + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + get-headers: func() -> headers; + /// Get body of the Request. + /// + /// Stream returned by this method represents the contents of the body. + /// Once the stream is reported as closed, callers should await the returned + /// future to determine whether the body was received successfully. + /// The future will only resolve after the stream is reported as closed. + /// + /// This function takes a `res` future as a parameter, which can be used to + /// communicate an error in handling of the request. + /// + /// Note that function will move the `request`, but references to headers or + /// request options acquired from it previously will remain valid. + consume-body: static func(this: request, res: future>) -> tuple, future, error-code>>>; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound an + /// asynchronous call. + @since(version = 0.3.0) + resource request-options { + /// Construct a default `request-options` value. + constructor(); + /// The timeout for the initial connect to the HTTP Server. + get-connect-timeout: func() -> option; + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported or that this + /// handle is immutable. + set-connect-timeout: func(duration: option) -> result<_, request-options-error>; + /// The timeout for receiving the first byte of the Response body. + get-first-byte-timeout: func() -> option; + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported or that + /// this handle is immutable. + set-first-byte-timeout: func(duration: option) -> result<_, request-options-error>; + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + get-between-bytes-timeout: func() -> option; + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported or that this handle is immutable. + set-between-bytes-timeout: func(duration: option) -> result<_, request-options-error>; + /// Make a deep copy of the `request-options`. + /// The resulting `request-options` is mutable. + clone: func() -> request-options; + } + + /// This type corresponds to the HTTP standard Status Code. + @since(version = 0.3.0) + type status-code = u16; + + /// Represents an HTTP Response. + @since(version = 0.3.0) + resource response { + /// Construct a new `response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// `headers` is the HTTP Headers for the Response. + /// + /// `contents` is the optional body content stream with `none` + /// representing a zero-length content stream. + /// Once it is closed, `trailers` future must resolve to a result. + /// If `trailers` resolves to an error, underlying connection + /// will be closed immediately. + /// + /// The returned future resolves to result of transmission of this response. + new: static func(headers: headers, contents: option>, trailers: future, error-code>>) -> tuple>>; + /// Get the HTTP Status Code for the Response. + get-status-code: func() -> status-code; + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + set-status-code: func(status-code: status-code) -> result; + /// Get the headers associated with the Response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + get-headers: func() -> headers; + /// Get body of the Response. + /// + /// Stream returned by this method represents the contents of the body. + /// Once the stream is reported as closed, callers should await the returned + /// future to determine whether the body was received successfully. + /// The future will only resolve after the stream is reported as closed. + /// + /// This function takes a `res` future as a parameter, which can be used to + /// communicate an error in handling of the response. + /// + /// Note that function will move the `response`, but references to headers + /// acquired from it previously will remain valid. + consume-body: static func(this: response, res: future>) -> tuple, future, error-code>>>; + } +} + +/// This interface defines a handler of HTTP Requests. +/// +/// In a `wasi:http/service` this interface is exported to respond to an +/// incoming HTTP Request with a Response. +/// +/// In `wasi:http/middleware` this interface is both exported and imported as +/// the "downstream" and "upstream" directions of the middleware chain. +@since(version = 0.3.0) +interface handler { + use types.{request, response, error-code}; + + /// This function may be called with either an incoming request read from the + /// network or a request synthesized or forwarded by another component. + handle: async func(request: request) -> result; +} + +/// This interface defines an HTTP client for sending "outgoing" requests. +/// +/// Most components are expected to import this interface to provide the +/// capability to send HTTP requests to arbitrary destinations on a network. +/// +/// The type signature of `client.send` is the same as `handler.handle`. This +/// duplication is currently necessary because some Component Model tooling +/// (including WIT itself) is unable to represent a component importing two +/// instances of the same interface. A `client.send` import may be linked +/// directly to a `handler.handle` export to bypass the network. +@since(version = 0.3.0) +interface client { + use types.{request, response, error-code}; + + /// This function may be used to either send an outgoing request over the + /// network or to forward it to another component. + send: async func(request: request) -> result; +} + +/// The `wasi:http/service` world captures a broad category of HTTP services +/// including web applications, API servers, and proxies. It may be `include`d +/// in more specific worlds such as `wasi:http/middleware`. +@since(version = 0.3.0) +world service { + import wasi:cli/types@0.3.0; + import wasi:cli/stdout@0.3.0; + import wasi:cli/stderr@0.3.0; + import wasi:cli/stdin@0.3.0; + import wasi:clocks/types@0.3.0; + import types; + import client; + import wasi:clocks/monotonic-clock@0.3.0; + import wasi:clocks/system-clock@0.3.0; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0; + import wasi:random/random@0.3.0; + import wasi:random/insecure@0.3.0; + import wasi:random/insecure-seed@0.3.0; + + export handler; +} +/// The `wasi:http/middleware` world captures HTTP services that forward HTTP +/// Requests to another handler. +/// +/// Components may implement this world to allow them to participate in handler +/// "chains" where a `request` flows through handlers on its way to some terminal +/// `service` and corresponding `response` flows in the opposite direction. +@since(version = 0.3.0) +world middleware { + import wasi:clocks/types@0.3.0; + import types; + import handler; + import wasi:cli/types@0.3.0; + import wasi:cli/stdout@0.3.0; + import wasi:cli/stderr@0.3.0; + import wasi:cli/stdin@0.3.0; + import client; + import wasi:clocks/monotonic-clock@0.3.0; + import wasi:clocks/system-clock@0.3.0; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0; + import wasi:random/random@0.3.0; + import wasi:random/insecure@0.3.0; + import wasi:random/insecure-seed@0.3.0; + + export handler; +} diff --git a/wit/deps/wasi-random-0.3.0/package.wit b/wit/deps/wasi-random-0.3.0/package.wit new file mode 100644 index 0000000..f6cfb81 --- /dev/null +++ b/wit/deps/wasi-random-0.3.0/package.wit @@ -0,0 +1,18 @@ +package wasi:random@0.3.0; + +interface random { + get-random-bytes: func(max-len: u64) -> list; + + get-random-u64: func() -> u64; +} + +interface insecure { + get-insecure-random-bytes: func(max-len: u64) -> list; + + get-insecure-random-u64: func() -> u64; +} + +interface insecure-seed { + get-insecure-seed: func() -> tuple; +} + diff --git a/wit/http.wit b/wit/http.wit index 5dfbcb6..5b4b021 100644 --- a/wit/http.wit +++ b/wit/http.wit @@ -1,5 +1,3 @@ -package componentized:http@0.1.0-dev; - interface client { enum method { get, diff --git a/wit/latch.wit b/wit/latch.wit new file mode 100644 index 0000000..37f2cd5 --- /dev/null +++ b/wit/latch.wit @@ -0,0 +1,67 @@ + +interface latch { + use wasi:http/types@0.3.0.{error-code as http-error-code, request}; + + record send-args { + request: borrow, + } + + record handle-args { + request: borrow, + } + + variant client-operation { + send(send-args), + } + + variant handler-operation { + handle(handle-args), + } + + variant operation { + client(client-operation), + handler(handler-operation), + } + + variant decision { + granted, + denied(http-error-code), + } + + variant error-code { + http(http-error-code), + other(option), + } + + authorize: func(operation: operation) -> result, error-code>; +} + +interface latch0 { + use latch.{operation, decision, error-code}; + + authorize: func(operation: operation) -> result, error-code>; +} + +interface latch1 { + use latch.{operation, decision, error-code}; + + authorize: func(operation: operation) -> result, error-code>; +} + +interface latch2 { + use latch.{operation, decision, error-code}; + + authorize: func(operation: operation) -> result, error-code>; +} + +interface latch3 { + use latch.{operation, decision, error-code}; + + authorize: func(operation: operation) -> result, error-code>; +} + +interface latch4 { + use latch.{operation, decision, error-code}; + + authorize: func(operation: operation) -> result, error-code>; +} diff --git a/wit/worlds.wit b/wit/worlds.wit index e520f1f..a0c743f 100644 --- a/wit/worlds.wit +++ b/wit/worlds.wit @@ -1,3 +1,11 @@ +package componentized:http@0.1.0-dev; + world imports { import client; + import latch; +} + +world http-latch { + import wasi:config/store@0.2.0-rc.1; + export latch; } diff --git a/wkg.lock b/wkg.lock index 2ad4aa8..fa8b507 100644 --- a/wkg.lock +++ b/wkg.lock @@ -1,4 +1,21 @@ # This file is automatically generated. # It is not intended for manual editing. version = 1 -packages = [] + +[[packages]] +name = "wasi:config" +registry = "wasi.dev" + +[[packages.versions]] +requirement = "=0.2.0-rc.1" +version = "0.2.0-rc.1" +digest = "sha256:1b7f1b0fd07bb4cede16c6a6ec8852815dfb924639a78735fc7bdffdc164485d" + +[[packages]] +name = "wasi:http" +registry = "wasi.dev" + +[[packages.versions]] +requirement = "=0.3.0" +version = "0.3.0" +digest = "sha256:92cd8f3730c00226dc15626a2e7b21834dd187fc221f09818720d228585bbbf7" From 816c2ae2b073afa3a112740b72cdd8dcef3b08e2 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 9 Sep 2026 21:07:47 -0400 Subject: [PATCH 2/3] drop granted decision Signed-off-by: Scott Andrews --- components/gate-client/src/lib.rs | 8 +++++--- components/gate-handler/src/lib.rs | 8 +++++--- components/latch-deny-all/src/lib.rs | 4 ++-- components/latch-grant-all/src/lib.rs | 4 ++-- components/latch-method/README.md | 2 +- components/latch-method/src/lib.rs | 18 +++++++----------- components/latch-n2/src/lib.rs | 2 +- components/latch-n3/src/lib.rs | 2 +- components/latch-n4/src/lib.rs | 2 +- components/latch-n5/src/lib.rs | 2 +- .../componentized-http-0.1.0-dev/package.wit | 14 +++++++------- crates/latch-n/src/lib.rs | 11 +++++------ wit/latch.wit | 14 +++++++------- 13 files changed, 45 insertions(+), 46 deletions(-) diff --git a/components/gate-client/src/lib.rs b/components/gate-client/src/lib.rs index a37a16e..e3cc4ef 100644 --- a/components/gate-client/src/lib.rs +++ b/components/gate-client/src/lib.rs @@ -4,7 +4,9 @@ use std::fmt::Display; use crate::{ componentized::http::latch::{ - self, authorize, ClientOperation, Decision::Denied, Operation, SendArgs, + self, authorize, ClientOperation, + Decision::{Abstained, Denied}, + Operation, SendArgs, }, exports::wasi::http::client::{ErrorCode, Guest, Request, Response}, wasi::{ @@ -32,7 +34,7 @@ impl Guest for GatedHttpClient { match authorize(&Operation::Client(ClientOperation::Send(SendArgs { request: &request, })))? { - Some(Denied(reason)) => { + Denied(reason) => { warn!( "Denied REASON={reason} OPERATION=wasi:http/client#send METHOD={} PATH={}", request.get_method(), @@ -40,7 +42,7 @@ impl Guest for GatedHttpClient { ); Err(reason) } - _ => client::send(request).await, + Abstained => client::send(request).await, } } } diff --git a/components/gate-handler/src/lib.rs b/components/gate-handler/src/lib.rs index 287053b..f9f7095 100644 --- a/components/gate-handler/src/lib.rs +++ b/components/gate-handler/src/lib.rs @@ -4,7 +4,9 @@ use std::fmt::Display; use crate::{ componentized::http::latch::{ - self, authorize, Decision::Denied, HandleArgs, HandlerOperation, Operation, + self, authorize, + Decision::{Abstained, Denied}, + HandleArgs, HandlerOperation, Operation, }, exports::wasi::http::handler::{ErrorCode, Guest, Request, Response}, wasi::{ @@ -32,7 +34,7 @@ impl Guest for GatedHttpHandler { match authorize(&Operation::Handler(HandlerOperation::Handle(HandleArgs { request: &request, })))? { - Some(Denied(reason)) => { + Denied(reason) => { warn!( "Denied REASON={reason} OPERATION=wasi:http/handler#handle METHOD={} PATH={}", request.get_method(), @@ -40,7 +42,7 @@ impl Guest for GatedHttpHandler { ); Err(reason) } - _ => handler::handle(request).await, + Abstained => handler::handle(request).await, } } } diff --git a/components/latch-deny-all/src/lib.rs b/components/latch-deny-all/src/lib.rs index c6f88cf..4304efe 100644 --- a/components/latch-deny-all/src/lib.rs +++ b/components/latch-deny-all/src/lib.rs @@ -7,8 +7,8 @@ use crate::exports::componentized::http::latch::{ struct DenyAllLatch {} impl Latch for DenyAllLatch { - fn authorize(_: Operation) -> Result, ErrorCode> { - Ok(Some(Decision::Denied(HttpErrorCode::HttpRequestDenied))) + fn authorize(_: Operation) -> Result { + Ok(Decision::Denied(HttpErrorCode::HttpRequestDenied)) } } diff --git a/components/latch-grant-all/src/lib.rs b/components/latch-grant-all/src/lib.rs index 5963e01..678f5c4 100644 --- a/components/latch-grant-all/src/lib.rs +++ b/components/latch-grant-all/src/lib.rs @@ -5,8 +5,8 @@ use crate::exports::componentized::http::latch::{Decision, ErrorCode, Guest as L struct GrantAllLatch {} impl Latch for GrantAllLatch { - fn authorize(_: Operation) -> Result, ErrorCode> { - Ok(Some(Decision::Granted)) + fn authorize(_: Operation) -> Result { + Ok(Decision::Abstained) } } diff --git a/components/latch-method/README.md b/components/latch-method/README.md index 9fadc83..7d2a5cc 100644 --- a/components/latch-method/README.md +++ b/components/latch-method/README.md @@ -2,7 +2,7 @@ HTTP latch that makes decisions based on the request's method. -Authorization is granted or denied based on the wasi:config with the lowercase method value as the config key, and `granted`, `denied`, or `abstained` as the value. A default decision may specified under the `*` key. +Authorization decisions are based on the wasi:config with the lowercase method value as the config key, using `denied` or `abstained` as the value. A default decision may specified under the `*` key. ## The `latch-method` World diff --git a/components/latch-method/src/lib.rs b/components/latch-method/src/lib.rs index fcbf28d..ee35502 100644 --- a/components/latch-method/src/lib.rs +++ b/components/latch-method/src/lib.rs @@ -9,14 +9,13 @@ use crate::{ }; const ABSTAINED: &str = "abstained"; -const GRANTED: &str = "granted"; const DENIED: &str = "denied"; const WILDCARD: &str = "*"; struct MethodLatch {} impl MethodLatch { - fn authorize_method(method: Method) -> Result, ErrorCode> { + fn authorize_method(method: Method) -> Result { let method = match method { Method::Get => "get", Method::Head => "head", @@ -34,27 +33,24 @@ impl MethodLatch { Some(method_value) => Self::parse_decision(method_value), None => match config::get(WILDCARD)? { Some(default_value) => Self::parse_decision(default_value), - None => Ok(None), + None => Ok(Decision::Abstained), }, } } - fn parse_decision(value: String) -> Result, ErrorCode> { + fn parse_decision(value: String) -> Result { match value.as_str() { - "" | ABSTAINED => Ok(None), - GRANTED => Ok(Some(Decision::Granted)), - DENIED => Ok(Some(Decision::Denied( - HttpErrorCode::HttpRequestMethodInvalid, - ))), + "" | ABSTAINED => Ok(Decision::Abstained), + DENIED => Ok(Decision::Denied(HttpErrorCode::HttpRequestMethodInvalid)), val => Err(ErrorCode::Other(Some(format!( - "unknown decision value '{val}', expected one of: '{ABSTAINED}', '{GRANTED}', '{DENIED}'" + "unknown decision value '{val}', expected one of: '{ABSTAINED}', '{DENIED}'" )))), } } } impl Latch for MethodLatch { - fn authorize(op: Operation) -> Result, ErrorCode> { + fn authorize(op: Operation) -> Result { match op { Operation::Client(client_operation) => match client_operation { ClientOperation::Send(args) => Self::authorize_method(args.request.get_method()), diff --git a/components/latch-n2/src/lib.rs b/components/latch-n2/src/lib.rs index 44a7106..cd3d30d 100644 --- a/components/latch-n2/src/lib.rs +++ b/components/latch-n2/src/lib.rs @@ -9,7 +9,7 @@ struct LatchN2 {} impl Latch for LatchN2 { #[allow(async_fn_in_trait)] - fn authorize(operation: Operation<'_>) -> Result, ErrorCode> { + fn authorize(operation: Operation<'_>) -> Result { let authorizers = vec![latch0::authorize, latch1::authorize]; latch_n::authorize(operation, authorizers) } diff --git a/components/latch-n3/src/lib.rs b/components/latch-n3/src/lib.rs index 96b612f..ed6ebcd 100644 --- a/components/latch-n3/src/lib.rs +++ b/components/latch-n3/src/lib.rs @@ -9,7 +9,7 @@ struct LatchN3 {} impl Latch for LatchN3 { #[allow(async_fn_in_trait)] - fn authorize(operation: Operation<'_>) -> Result, ErrorCode> { + fn authorize(operation: Operation<'_>) -> Result { let authorizers = vec![latch0::authorize, latch1::authorize, latch2::authorize]; latch_n::authorize(operation, authorizers) } diff --git a/components/latch-n4/src/lib.rs b/components/latch-n4/src/lib.rs index 6733d43..8e8f516 100644 --- a/components/latch-n4/src/lib.rs +++ b/components/latch-n4/src/lib.rs @@ -9,7 +9,7 @@ struct LatchN4 {} impl Latch for LatchN4 { #[allow(async_fn_in_trait)] - fn authorize(operation: Operation<'_>) -> Result, ErrorCode> { + fn authorize(operation: Operation<'_>) -> Result { let authorizers = vec![ latch0::authorize, latch1::authorize, diff --git a/components/latch-n5/src/lib.rs b/components/latch-n5/src/lib.rs index 595fe08..8b2d1a8 100644 --- a/components/latch-n5/src/lib.rs +++ b/components/latch-n5/src/lib.rs @@ -9,7 +9,7 @@ struct LatchN5 {} impl Latch for LatchN5 { #[allow(async_fn_in_trait)] - fn authorize(operation: Operation<'_>) -> Result, ErrorCode> { + fn authorize(operation: Operation<'_>) -> Result { let authorizers = vec![ latch0::authorize, latch1::authorize, diff --git a/components/wit/deps/componentized-http-0.1.0-dev/package.wit b/components/wit/deps/componentized-http-0.1.0-dev/package.wit index be7102e..b30f7e2 100644 --- a/components/wit/deps/componentized-http-0.1.0-dev/package.wit +++ b/components/wit/deps/componentized-http-0.1.0-dev/package.wit @@ -91,7 +91,7 @@ interface latch { } variant decision { - granted, + abstained, denied(http-error-code), } @@ -100,37 +100,37 @@ interface latch { other(option), } - authorize: func(operation: operation) -> result, error-code>; + authorize: func(operation: operation) -> result; } interface latch0 { use latch.{operation, decision, error-code}; - authorize: func(operation: operation) -> result, error-code>; + authorize: func(operation: operation) -> result; } interface latch1 { use latch.{operation, decision, error-code}; - authorize: func(operation: operation) -> result, error-code>; + authorize: func(operation: operation) -> result; } interface latch2 { use latch.{operation, decision, error-code}; - authorize: func(operation: operation) -> result, error-code>; + authorize: func(operation: operation) -> result; } interface latch3 { use latch.{operation, decision, error-code}; - authorize: func(operation: operation) -> result, error-code>; + authorize: func(operation: operation) -> result; } interface latch4 { use latch.{operation, decision, error-code}; - authorize: func(operation: operation) -> result, error-code>; + authorize: func(operation: operation) -> result; } world imports { diff --git a/crates/latch-n/src/lib.rs b/crates/latch-n/src/lib.rs index 49af068..4fea8f8 100644 --- a/crates/latch-n/src/lib.rs +++ b/crates/latch-n/src/lib.rs @@ -4,16 +4,15 @@ use crate::bindings::exports::componentized::http::latch::{Decision, ErrorCode, pub fn authorize( operation: Operation, - authorizers: Vec) -> Result, ErrorCode>>, -) -> Result, ErrorCode> { + authorizers: Vec) -> Result>, +) -> Result { for authorize in authorizers { match authorize(&operation)? { - None => {} - Some(Decision::Granted) => return Ok(Some(Decision::Granted)), - Some(Decision::Denied(error_code)) => return Ok(Some(Decision::Denied(error_code))), + Decision::Abstained => {} + Decision::Denied(error_code) => return Ok(Decision::Denied(error_code)), } } - Ok(None) + Ok(Decision::Abstained) } pub mod bindings { diff --git a/wit/latch.wit b/wit/latch.wit index 37f2cd5..434bb52 100644 --- a/wit/latch.wit +++ b/wit/latch.wit @@ -24,7 +24,7 @@ interface latch { } variant decision { - granted, + abstained, denied(http-error-code), } @@ -33,35 +33,35 @@ interface latch { other(option), } - authorize: func(operation: operation) -> result, error-code>; + authorize: func(operation: operation) -> result; } interface latch0 { use latch.{operation, decision, error-code}; - authorize: func(operation: operation) -> result, error-code>; + authorize: func(operation: operation) -> result; } interface latch1 { use latch.{operation, decision, error-code}; - authorize: func(operation: operation) -> result, error-code>; + authorize: func(operation: operation) -> result; } interface latch2 { use latch.{operation, decision, error-code}; - authorize: func(operation: operation) -> result, error-code>; + authorize: func(operation: operation) -> result; } interface latch3 { use latch.{operation, decision, error-code}; - authorize: func(operation: operation) -> result, error-code>; + authorize: func(operation: operation) -> result; } interface latch4 { use latch.{operation, decision, error-code}; - authorize: func(operation: operation) -> result, error-code>; + authorize: func(operation: operation) -> result; } From 872c5e8b0f6fcb03a6aeb92ee2950e3eddb23f27 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 9 Sep 2026 21:36:16 -0400 Subject: [PATCH 3/3] latch-grant-all -> latch-abstain-all Signed-off-by: Scott Andrews --- Cargo.lock | 4 ++-- README.md | 2 +- .../{latch-grant-all => latch-abstain-all}/Cargo.toml | 2 +- components/latch-abstain-all/README.md | 7 +++++++ .../{latch-grant-all => latch-abstain-all}/src/lib.rs | 6 +++--- components/latch-grant-all/README.md | 7 ------- 6 files changed, 14 insertions(+), 14 deletions(-) rename components/{latch-grant-all => latch-abstain-all}/Cargo.toml (84%) create mode 100644 components/latch-abstain-all/README.md rename components/{latch-grant-all => latch-abstain-all}/src/lib.rs (80%) delete mode 100644 components/latch-grant-all/README.md diff --git a/Cargo.lock b/Cargo.lock index c5eddd1..1bf3384 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,14 +272,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "latch-deny-all" +name = "latch-abstain-all" version = "0.1.0" dependencies = [ "wit-bindgen", ] [[package]] -name = "latch-grant-all" +name = "latch-deny-all" version = "0.1.0" dependencies = [ "wit-bindgen", diff --git a/README.md b/README.md index 2732673..bf37860 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ make components - [`gate-client`](./components/gate-client/) - [`gate-handler`](./components/gate-handler/) - [`http-client`](./components/http-client/) +- [`latch-abstain-all`](./components/latch-abstain-all/) - [`latch-deny-all`](./components/latch-deny-all/) -- [`latch-grant-all`](./components/latch-grant-all/) - [`latch-method`](./components/latch-method/) - [`latch-method-readonly`](./components/latch-method-readonly/) - [`latch-method-readonly-config`](./components/latch-method-readonly-config/) diff --git a/components/latch-grant-all/Cargo.toml b/components/latch-abstain-all/Cargo.toml similarity index 84% rename from components/latch-grant-all/Cargo.toml rename to components/latch-abstain-all/Cargo.toml index e37c55f..9633bbd 100644 --- a/components/latch-grant-all/Cargo.toml +++ b/components/latch-abstain-all/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "latch-grant-all" +name = "latch-abstain-all" version = "0.1.0" edition = "2021" license = "Apache-2.0" diff --git a/components/latch-abstain-all/README.md b/components/latch-abstain-all/README.md new file mode 100644 index 0000000..0d24a19 --- /dev/null +++ b/components/latch-abstain-all/README.md @@ -0,0 +1,7 @@ +# `latch-abstain-all` + +HTTP latch that abstains from all decisions. + +## The `latch-abstain-all` World + +- exports `componentized:http/latch` diff --git a/components/latch-grant-all/src/lib.rs b/components/latch-abstain-all/src/lib.rs similarity index 80% rename from components/latch-grant-all/src/lib.rs rename to components/latch-abstain-all/src/lib.rs index 678f5c4..79d1c00 100644 --- a/components/latch-grant-all/src/lib.rs +++ b/components/latch-abstain-all/src/lib.rs @@ -2,9 +2,9 @@ use crate::exports::componentized::http::latch::{Decision, ErrorCode, Guest as Latch, Operation}; -struct GrantAllLatch {} +struct AbstainAllLatch {} -impl Latch for GrantAllLatch { +impl Latch for AbstainAllLatch { fn authorize(_: Operation) -> Result { Ok(Decision::Abstained) } @@ -17,4 +17,4 @@ wit_bindgen::generate!({ generate_all }); -export!(GrantAllLatch); +export!(AbstainAllLatch); diff --git a/components/latch-grant-all/README.md b/components/latch-grant-all/README.md deleted file mode 100644 index 9a90838..0000000 --- a/components/latch-grant-all/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# `latch-grant-all` - -HTTP latch that implicitly grants all operations. - -## The `latch-grant-all` World - -- exports `componentized:http/latch`