diff --git a/compose/control-plane/config.json b/compose/control-plane/config.json index aa4d3bc..1b1dea0 100644 --- a/compose/control-plane/config.json +++ b/compose/control-plane/config.json @@ -5,6 +5,20 @@ "policies": [], "target_domain": "http://web.app:80", "oidc_issuer": "http://keycloak:8080/auth/realms/ostia", + "hosts": [ + "web", + "web.app" + ], + "policies": [ + { + "name": "jwt", + "configuration": { + "rules": [ + "AA" + ] + } + } + ], "proxy_rules": [ { "pattern": "/", diff --git a/src/oidc.rs b/src/oidc.rs index c3a5142..d1abce1 100644 --- a/src/oidc.rs +++ b/src/oidc.rs @@ -78,6 +78,7 @@ impl OIDCConfig { let provider = JwtProvider { issuer: self.issuer.clone(), + payload_in_metadata: "jwt_payload".to_string(), from_headers: vec![JwtHeader { name: "Authorization".to_string(), value_prefix: "Bearer ".to_string(), diff --git a/src/service.rs b/src/service.rs index 000932e..4341cca 100644 --- a/src/service.rs +++ b/src/service.rs @@ -52,11 +52,17 @@ pub struct MappingRules { delta: u32, } +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct PoliciyConfig { + pub name: String, + configuration: serde_json::Value, +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Service { pub id: u32, pub hosts: Vec, - pub policies: Vec, + pub policies: Vec, pub target_domain: std::string::String, pub proxy_rules: Vec, pub oidc_issuer: std::string::String, diff --git a/wasm_filter/Cargo.lock b/wasm_filter/Cargo.lock index 377d564..1740464 100644 --- a/wasm_filter/Cargo.lock +++ b/wasm_filter/Cargo.lock @@ -6,6 +6,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" +[[package]] +name = "anyhow" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7" + [[package]] name = "autocfg" version = "1.0.1" @@ -18,6 +24,12 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + [[package]] name = "cfg-if" version = "0.1.10" @@ -37,12 +49,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "filter" version = "0.1.0" dependencies = [ + "anyhow", "chrono", "log", + "prost", + "prost-types", "proxy-wasm", "serde", "serde_json", @@ -60,6 +81,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.6" @@ -121,6 +151,39 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "prost" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce49aefe0a6144a45de32927c77bd2859a5f7677b55f220ae5b744e87389c212" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537aa19b95acde10a12fec4301466386f757403de4cd4e5b4fa78fb5ecb18f72" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1834f67c0697c001304b75be76f67add9c89742eda3a085ad8ee0bb38c3417aa" +dependencies = [ + "bytes", + "prost", +] + [[package]] name = "proxy-wasm" version = "0.1.2" diff --git a/wasm_filter/Cargo.toml b/wasm_filter/Cargo.toml index b5daf7b..38f5a4c 100644 --- a/wasm_filter/Cargo.toml +++ b/wasm_filter/Cargo.toml @@ -13,5 +13,10 @@ wasm-bindgen-macro = "0.2.60" serde_json = "^1" serde = { version = "^1", features = ["derive"] } +anyhow = "^1" + +prost = { version = "^0", default-features = false, features = ["prost-derive"] } +prost-types = { version = "^0", default-features = false } + [lib] crate-type = ["cdylib"] diff --git a/wasm_filter/src/config.rs b/wasm_filter/src/config.rs index bf802fd..a4fa3bc 100644 --- a/wasm_filter/src/config.rs +++ b/wasm_filter/src/config.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::cell::RefCell; use std::collections::HashMap; @@ -30,11 +31,17 @@ impl MappingRule { } } +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct PoliciyConfig { + pub name: String, + configuration: serde_json::Value, +} + #[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct Service { pub id: u32, pub hosts: Vec, - pub policies: Vec, + pub policies: Vec, pub target_domain: String, pub proxy_rules: Vec, } diff --git a/wasm_filter/src/jwt.rs b/wasm_filter/src/jwt.rs new file mode 100644 index 0000000..b00c471 --- /dev/null +++ b/wasm_filter/src/jwt.rs @@ -0,0 +1,151 @@ +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +use prost::Message; + +#[derive(Debug, Default)] +pub struct Rules { + path: String, + claim: String, // Move to liquid + claim_value: String, // Move to liquid + allow: bool, +} +impl Rules { + pub fn matches(&self, path: String, jwt_claim_value: &str) -> bool { + if self.path != path { + log::debug!( + "PATH failed match request_path='{}' with path='{}'", + self.path, + path + ); + return false; + } + if self.claim_value.as_str() != jwt_claim_value { + log::debug!( + "Matches claim_value='{}' with jwt_value='{}' failed", + self.claim_value, + jwt_claim_value, + ); + return false; + } + + true + } +} + +#[derive(Debug, Default)] +pub struct JWTConfig { + pub rules: Vec, +} + +#[derive(Debug, Default)] +pub struct JWT { + pub context_id: u32, + pub config: JWTConfig, +} + +impl JWT { + pub fn new(context_id: u32) -> JWT { + JWT { + context_id, + config: JWTConfig { + ..Default::default() + }, + } + } + + pub fn config(&mut self) { + self.config.rules = Vec::new(); + self.config.rules.push(Rules { + path: "/headers".to_string(), + claim: "name".to_string(), + claim_value: "Jane Smith".to_string(), + allow: false, + }); + } + pub fn get_jwt_claim(&self, claim: &str) -> Result { + let key = vec![ + "metadata", + "filter_metadata", + "envoy.filters.http.jwt_authn", + "jwt_payload", + claim, + ]; + + let data = self.get_property(key); + if data.is_none() { + return Err(anyhow::Error::msg("Failed to get JWT payload")); + } + let tmp = data.clone().unwrap(); + + let ret = std::str::from_utf8(tmp.as_slice()); + if ret.is_err() { + return Err(anyhow::Error::msg("Failed to get decode JWT payload")); + } + Ok(ret.unwrap().to_string()) + } + + pub fn get_jwt_token(&self) -> Result, anyhow::Error> { + let data = self.get_property(vec![ + "metadata", + "filter_metadata", + "envoy.filters.http.jwt_authn", + "jwt_payload", + ]); + + if data.is_none() { + return Err(anyhow::Error::msg("Failed to get JWT payload")); + } + log::info!("Bytes --> {:?}", data.clone().unwrap().as_slice()); + log::info!( + "STR --> {:?}", + std::str::from_utf8(data.clone().unwrap().as_slice()) + ); + let msg: prost_types::Struct = + Message::decode_length_delimited(data.clone().unwrap().as_slice()) + .expect("cannot decode message"); + log::info!("MSG--> {:?}", msg); + return Ok(data.unwrap()); + } + + fn get_path(&self) -> Option { + return self.get_http_request_header(":path"); + } +} + +impl Context for JWT {} + +impl HttpContext for JWT { + fn on_http_request_headers(&mut self, _: usize) -> Action { + // @TODO to be removed until HTTP_CONTEXT can have metadata attached + self.config(); + + let jwt_token = self.get_jwt_token(); + if jwt_token.is_err() { + log::warn!("Error on JWT auth: '{:?}'", jwt_token); + self.send_http_response(403, vec![], Some(b"Access forbidden.\n")); + return Action::Pause; + } + + let mut result = false; + + for rule in &self.config.rules { + let claim_value = self.get_jwt_claim(rule.claim.as_str()); + if claim_value.is_err() { + log::info!("Cannot retrieve {}", rule.claim.as_str()); + continue; + } + + if rule.matches(self.get_path().unwrap(), claim_value.unwrap().as_str()) { + result = true; + } + } + + if result == true { + return Action::Continue; + } + + self.send_http_response(403, vec![], Some(b"Access forbidden.\n")); + return Action::Pause; + } +} diff --git a/wasm_filter/src/lib.rs b/wasm_filter/src/lib.rs index 611f5c4..2143b72 100644 --- a/wasm_filter/src/lib.rs +++ b/wasm_filter/src/lib.rs @@ -5,6 +5,7 @@ use proxy_wasm::types::*; use std::time::Duration; mod config; +mod jwt; const AUTH_BACKEND: &str = "httpbin"; @@ -28,7 +29,16 @@ impl Context for ConfigContext {} impl RootContext for ConfigContext { fn on_vm_start(&mut self, _: usize) -> bool { let config = self.get_configuration(); - config::import_config(std::str::from_utf8(&config.unwrap()).unwrap()); + + let service = config::import_config(std::str::from_utf8(&config.unwrap()).unwrap()); + for policy in &service.policies { + if policy.name.as_str() == "jwt" { + let cb = + |context_id, _| -> Box { Box::new(jwt::JWT::new(context_id)) }; + proxy_wasm::set_http_context(cb); + } + } + self.set_tick_period(Duration::from_secs(20)); true }