From 839e329adfab8d427ba71a542ddd65db2f41292a Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 16 Aug 2024 13:59:05 +0100 Subject: [PATCH 01/66] Remove use of filter in get_attribute Signed-off-by: Adam Cattermole Signed-off-by: dd di cesare --- src/attribute.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/attribute.rs b/src/attribute.rs index aada6176..357e9f83 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -1,7 +1,6 @@ use crate::configuration::Path; -use crate::filter::http_context::Filter; use chrono::{DateTime, FixedOffset}; -use proxy_wasm::traits::Context; +use proxy_wasm::hostcalls; pub trait Attribute { fn parse(raw_attribute: Vec) -> Result @@ -105,15 +104,12 @@ impl Attribute for DateTime { } #[allow(dead_code)] -pub fn get_attribute(f: &Filter, attr: &str) -> Result +pub fn get_attribute(attr: &str) -> Result where T: Attribute, { - match f.get_property(Path::from(attr).tokens()) { - None => Err(format!( - "#{} get_attribute: not found: {}", - f.context_id, attr - )), - Some(attribute_bytes) => T::parse(attribute_bytes), + match hostcalls::get_property(Path::from(attr).tokens()) { + Ok(Some(attribute_bytes)) => T::parse(attribute_bytes), + _ => Err(format!("get_attribute: not found: {}", attr)), } } From 2428cb04686a672eb26f99e36f4432c27270bb80 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 16 Aug 2024 14:39:37 +0100 Subject: [PATCH 02/66] Minor refactor of rate-limit service Signed-off-by: Adam Cattermole Signed-off-by: dd di cesare --- src/service/rate_limit.rs | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src/service/rate_limit.rs b/src/service/rate_limit.rs index 6c4726c5..5fe03b41 100644 --- a/src/service/rate_limit.rs +++ b/src/service/rate_limit.rs @@ -14,12 +14,13 @@ pub struct RateLimitService { } impl RateLimitService { - pub fn new(endpoint: &str, metadata: Vec<(TracingHeader, Bytes)>) -> RateLimitService { + pub fn new(endpoint: &str, metadata: Vec<(TracingHeader, Bytes)>) -> Self { Self { endpoint: String::from(endpoint), tracing_headers: metadata, } } + pub fn message( domain: String, descriptors: RepeatedField, @@ -34,31 +35,22 @@ impl RateLimitService { } } -fn grpc_call( - upstream_name: &str, - initial_metadata: Vec<(&str, &[u8])>, - message: RateLimitRequest, -) -> Result { - let msg = Message::write_to_bytes(&message).unwrap(); // TODO(didierofrivia): Error Handling - dispatch_grpc_call( - upstream_name, - RATELIMIT_SERVICE_NAME, - RATELIMIT_METHOD_NAME, - initial_metadata, - Some(&msg), - Duration::from_secs(5), - ) -} - impl Service for RateLimitService { fn send(&self, message: RateLimitRequest) -> Result { - grpc_call( + let msg = Message::write_to_bytes(&message).unwrap(); // TODO(didierofrivia): Error Handling + let metadata = self + .tracing_headers + .iter() + .map(|(header, value)| (header.as_str(), value.as_slice())) + .collect(); + + dispatch_grpc_call( self.endpoint.as_str(), - self.tracing_headers - .iter() - .map(|(header, value)| (header.as_str(), value.as_slice())) - .collect(), - message, + RATELIMIT_SERVICE_NAME, + RATELIMIT_METHOD_NAME, + metadata, + Some(&msg), + Duration::from_secs(5), ) } } From 4e59d1716d0c36b917978e9f8a0664529d4cb1a7 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 16 Aug 2024 14:41:06 +0100 Subject: [PATCH 03/66] Add initial implementation of auth service Signed-off-by: Adam Cattermole Signed-off-by: dd di cesare --- src/envoy/mod.rs | 7 +++ src/service.rs | 1 + src/service/auth.rs | 119 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 src/service/auth.rs diff --git a/src/envoy/mod.rs b/src/envoy/mod.rs index 68e18d60..db527204 100644 --- a/src/envoy/mod.rs +++ b/src/envoy/mod.rs @@ -31,6 +31,13 @@ mod token_bucket; mod value; pub use { + address::{Address, SocketAddress}, + attribute_context::{ + AttributeContext, AttributeContext_HttpRequest, AttributeContext_Peer, + AttributeContext_Request, + }, + base::Metadata, + external_auth::CheckRequest, ratelimit::{RateLimitDescriptor, RateLimitDescriptor_Entry}, rls::{RateLimitRequest, RateLimitResponse, RateLimitResponse_Code}, }; diff --git a/src/service.rs b/src/service.rs index b63bb827..3cad9530 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,3 +1,4 @@ +pub(crate) mod auth; pub(crate) mod rate_limit; use protobuf::Message; diff --git a/src/service/auth.rs b/src/service/auth.rs new file mode 100644 index 00000000..a623bc8e --- /dev/null +++ b/src/service/auth.rs @@ -0,0 +1,119 @@ +use crate::attribute::get_attribute; +use crate::envoy::{ + Address, AttributeContext, AttributeContext_HttpRequest, AttributeContext_Peer, + AttributeContext_Request, CheckRequest, Metadata, SocketAddress, +}; +use crate::filter::http_context::TracingHeader; +use crate::service::Service; +use chrono::{DateTime, FixedOffset, Timelike}; +use protobuf::well_known_types::Timestamp; +use protobuf::Message; +use proxy_wasm::hostcalls; +use proxy_wasm::hostcalls::dispatch_grpc_call; +use proxy_wasm::types::{Bytes, MapType, Status}; +use std::collections::HashMap; +use std::time::Duration; + +const AUTH_SERVICE_NAME: &str = "envoy.service.auth.v3.Authorization"; +const AUTH_METHOD_NAME: &str = "Check"; + +pub struct AuthService { + endpoint: String, + tracing_headers: Vec<(TracingHeader, Bytes)>, +} + +impl AuthService { + pub fn new(endpoint: &str, metadata: Vec<(TracingHeader, Bytes)>) -> Self { + Self { + endpoint: String::from(endpoint), + tracing_headers: metadata, + } + } + + pub fn message() -> CheckRequest { + AuthService::build_check_req() + } + + fn build_check_req() -> CheckRequest { + let mut auth_req = CheckRequest::default(); + let mut attr = AttributeContext::default(); + attr.set_request(AuthService::build_request()); + attr.set_destination(AuthService::build_peer( + get_attribute::("destination.address").unwrap_or_default(), + get_attribute::("destination.port").unwrap_or_default() as u32, + )); + attr.set_source(AuthService::build_peer( + get_attribute::("source.address").unwrap_or_default(), + get_attribute::("source.port").unwrap_or_default() as u32, + )); + // todo(adam-cattermole): for now we set the context_extensions to the request host + // but this should take other info into account + let context_extensions = HashMap::from([( + "host".to_string(), + attr.get_request().get_http().host.to_owned(), + )]); + attr.set_context_extensions(context_extensions); + attr.set_metadata_context(Metadata::default()); + auth_req.set_attributes(attr); + auth_req + } + + fn build_request() -> AttributeContext_Request { + let mut request = AttributeContext_Request::default(); + let mut http = AttributeContext_HttpRequest::default(); + let headers: HashMap = hostcalls::get_map(MapType::HttpRequestHeaders) + .unwrap() + .into_iter() + .collect(); + + http.set_host(get_attribute::("request.host").unwrap_or_default()); + http.set_method(get_attribute::("request.method").unwrap_or_default()); + http.set_scheme(get_attribute::("request.scheme").unwrap_or_default()); + http.set_path(get_attribute::("request.path").unwrap_or_default()); + http.set_protocol(get_attribute::("request.protocol").unwrap_or_default()); + + http.set_headers(headers); + request.set_time(get_attribute("request.time").map_or( + Timestamp::new(), + |date_time: DateTime| Timestamp { + nanos: date_time.nanosecond() as i32, + seconds: date_time.second() as i64, + unknown_fields: Default::default(), + cached_size: Default::default(), + }, + )); + request.set_http(http); + request + } + + fn build_peer(host: String, port: u32) -> AttributeContext_Peer { + let mut peer = AttributeContext_Peer::default(); + let mut address = Address::default(); + let mut socket_address = SocketAddress::default(); + socket_address.set_address(host); + socket_address.set_port_value(port); + address.set_socket_address(socket_address); + peer.set_address(address); + peer + } +} + +impl Service for AuthService { + fn send(&self, message: CheckRequest) -> Result { + let msg = Message::write_to_bytes(&message).unwrap(); // TODO(adam-cattermole): Error Handling + let metadata = self + .tracing_headers + .iter() + .map(|(header, value)| (header.as_str(), value.as_slice())) + .collect(); + + dispatch_grpc_call( + self.endpoint.as_str(), + AUTH_SERVICE_NAME, + AUTH_METHOD_NAME, + metadata, + Some(&msg), + Duration::from_secs(5), + ) + } +} From 3eb9974ab3045e176125e4c79a320f1d62601bed Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 16 Aug 2024 16:00:29 +0100 Subject: [PATCH 04/66] Pass in the host to set in context_extensions Signed-off-by: Adam Cattermole Signed-off-by: dd di cesare --- src/service/auth.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/service/auth.rs b/src/service/auth.rs index a623bc8e..35a82cd9 100644 --- a/src/service/auth.rs +++ b/src/service/auth.rs @@ -30,11 +30,11 @@ impl AuthService { } } - pub fn message() -> CheckRequest { - AuthService::build_check_req() + pub fn message(ce_host: String) -> CheckRequest { + AuthService::build_check_req(ce_host) } - fn build_check_req() -> CheckRequest { + fn build_check_req(ce_host: String) -> CheckRequest { let mut auth_req = CheckRequest::default(); let mut attr = AttributeContext::default(); attr.set_request(AuthService::build_request()); @@ -46,12 +46,8 @@ impl AuthService { get_attribute::("source.address").unwrap_or_default(), get_attribute::("source.port").unwrap_or_default() as u32, )); - // todo(adam-cattermole): for now we set the context_extensions to the request host - // but this should take other info into account - let context_extensions = HashMap::from([( - "host".to_string(), - attr.get_request().get_http().host.to_owned(), - )]); + // the ce_host is the identifier for authorino to determine which authconfig to use + let context_extensions = HashMap::from([("host".to_string(), ce_host)]); attr.set_context_extensions(context_extensions); attr.set_metadata_context(Metadata::default()); auth_req.set_attributes(attr); From 5a2e2b1f51d9ca3eb7a67bd83e3b736db96d1139 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Wed, 21 Aug 2024 11:37:09 +0100 Subject: [PATCH 05/66] Genericise the service to send any type of M Signed-off-by: Adam Cattermole Signed-off-by: dd di cesare --- src/configuration.rs | 7 +++ src/filter/http_context.rs | 12 +++-- src/service.rs | 93 ++++++++++++++++++++++++++++++++++++-- src/service/auth.rs | 42 ++--------------- src/service/rate_limit.rs | 44 ++---------------- 5 files changed, 114 insertions(+), 84 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index d7495ef0..9526f8cd 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -494,6 +494,13 @@ pub enum FailureMode { Allow, } +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum ExtensionType { + Auth, + RateLimit, +} + #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct PluginConfiguration { diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index 281eab93..9feedb7a 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -1,9 +1,9 @@ -use crate::configuration::{FailureMode, FilterConfig}; +use crate::configuration::{ExtensionType, FailureMode, FilterConfig}; use crate::envoy::{RateLimitResponse, RateLimitResponse_Code}; use crate::filter::http_context::TracingHeader::{Baggage, Traceparent, Tracestate}; use crate::policy::Policy; use crate::service::rate_limit::RateLimitService; -use crate::service::Service; +use crate::service::GrpcServiceHandler; use log::{debug, warn}; use protobuf::Message; use proxy_wasm::traits::{Context, HttpContext}; @@ -19,7 +19,7 @@ pub enum TracingHeader { } impl TracingHeader { - fn all() -> [Self; 3] { + pub fn all() -> [Self; 3] { [Traceparent, Tracestate, Baggage] } @@ -63,7 +63,11 @@ impl Filter { return Action::Continue; } - let rls = RateLimitService::new(rlp.service.as_str(), self.tracing_headers.clone()); + let rls = GrpcServiceHandler::new( + ExtensionType::RateLimit, + rlp.service.clone(), + self.tracing_headers.clone(), + ); let message = RateLimitService::message(rlp.domain.clone(), descriptors); match rls.send(message) { diff --git a/src/service.rs b/src/service.rs index 3cad9530..99424631 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,9 +1,96 @@ pub(crate) mod auth; pub(crate) mod rate_limit; +use crate::configuration::ExtensionType; +use crate::filter::http_context::TracingHeader; +use crate::service::auth::{AUTH_METHOD_NAME, AUTH_SERVICE_NAME}; +use crate::service::rate_limit::{RATELIMIT_METHOD_NAME, RATELIMIT_SERVICE_NAME}; use protobuf::Message; -use proxy_wasm::types::Status; +use proxy_wasm::hostcalls; +use proxy_wasm::hostcalls::dispatch_grpc_call; +use proxy_wasm::types::{Bytes, MapType, Status}; +use std::cell::OnceCell; +use std::time::Duration; -pub trait Service { - fn send(&self, message: M) -> Result; +pub struct GrpcServiceHandler { + endpoint: String, + service_name: String, + method_name: String, + tracing_headers: Vec<(TracingHeader, Bytes)>, +} + +impl GrpcServiceHandler { + fn build( + endpoint: String, + service_name: &str, + method_name: &str, + tracing_headers: Vec<(TracingHeader, Bytes)>, + ) -> Self { + Self { + endpoint: endpoint.to_owned(), + service_name: service_name.to_owned(), + method_name: method_name.to_owned(), + tracing_headers, + } + } + + pub fn new( + extension_type: ExtensionType, + endpoint: String, + tracing_headers: Vec<(TracingHeader, Bytes)>, + ) -> Self { + match extension_type { + ExtensionType::Auth => Self::build( + endpoint, + AUTH_SERVICE_NAME, + AUTH_METHOD_NAME, + tracing_headers, + ), + ExtensionType::RateLimit => Self::build( + endpoint, + RATELIMIT_SERVICE_NAME, + RATELIMIT_METHOD_NAME, + tracing_headers, + ), + } + } + + pub fn send(&self, message: M) -> Result { + let msg = Message::write_to_bytes(&message).unwrap(); + let metadata = self + .tracing_headers + .iter() + .map(|(header, value)| (header.as_str(), value.as_slice())) + .collect(); + + dispatch_grpc_call( + self.endpoint.as_str(), + self.service_name.as_str(), + self.method_name.as_str(), + metadata, + Some(&msg), + Duration::from_secs(5), + ) + } +} + +pub struct TracingHeaderResolver { + tracing_headers: OnceCell>, +} + +impl TracingHeaderResolver { + pub fn get(&self) -> &Vec<(TracingHeader, Bytes)> { + self.tracing_headers.get_or_init(|| { + let mut headers = Vec::new(); + for header in TracingHeader::all() { + if let Some(value) = + hostcalls::get_map_value_bytes(MapType::HttpRequestHeaders, header.as_str()) + .unwrap() + { + headers.push((header, value)); + } + } + headers + }) + } } diff --git a/src/service/auth.rs b/src/service/auth.rs index 35a82cd9..36220f59 100644 --- a/src/service/auth.rs +++ b/src/service/auth.rs @@ -3,33 +3,19 @@ use crate::envoy::{ Address, AttributeContext, AttributeContext_HttpRequest, AttributeContext_Peer, AttributeContext_Request, CheckRequest, Metadata, SocketAddress, }; -use crate::filter::http_context::TracingHeader; -use crate::service::Service; use chrono::{DateTime, FixedOffset, Timelike}; use protobuf::well_known_types::Timestamp; use protobuf::Message; use proxy_wasm::hostcalls; -use proxy_wasm::hostcalls::dispatch_grpc_call; -use proxy_wasm::types::{Bytes, MapType, Status}; +use proxy_wasm::types::MapType; use std::collections::HashMap; -use std::time::Duration; -const AUTH_SERVICE_NAME: &str = "envoy.service.auth.v3.Authorization"; -const AUTH_METHOD_NAME: &str = "Check"; +pub const AUTH_SERVICE_NAME: &str = "envoy.service.auth.v3.Authorization"; +pub const AUTH_METHOD_NAME: &str = "Check"; -pub struct AuthService { - endpoint: String, - tracing_headers: Vec<(TracingHeader, Bytes)>, -} +pub struct AuthService; impl AuthService { - pub fn new(endpoint: &str, metadata: Vec<(TracingHeader, Bytes)>) -> Self { - Self { - endpoint: String::from(endpoint), - tracing_headers: metadata, - } - } - pub fn message(ce_host: String) -> CheckRequest { AuthService::build_check_req(ce_host) } @@ -93,23 +79,3 @@ impl AuthService { peer } } - -impl Service for AuthService { - fn send(&self, message: CheckRequest) -> Result { - let msg = Message::write_to_bytes(&message).unwrap(); // TODO(adam-cattermole): Error Handling - let metadata = self - .tracing_headers - .iter() - .map(|(header, value)| (header.as_str(), value.as_slice())) - .collect(); - - dispatch_grpc_call( - self.endpoint.as_str(), - AUTH_SERVICE_NAME, - AUTH_METHOD_NAME, - metadata, - Some(&msg), - Duration::from_secs(5), - ) - } -} diff --git a/src/service/rate_limit.rs b/src/service/rate_limit.rs index 5fe03b41..6dfc3c89 100644 --- a/src/service/rate_limit.rs +++ b/src/service/rate_limit.rs @@ -1,26 +1,12 @@ use crate::envoy::{RateLimitDescriptor, RateLimitRequest}; -use crate::filter::http_context::TracingHeader; -use crate::service::Service; -use protobuf::{Message, RepeatedField}; -use proxy_wasm::hostcalls::dispatch_grpc_call; -use proxy_wasm::types::{Bytes, Status}; -use std::time::Duration; +use protobuf::RepeatedField; -const RATELIMIT_SERVICE_NAME: &str = "envoy.service.ratelimit.v3.RateLimitService"; -const RATELIMIT_METHOD_NAME: &str = "ShouldRateLimit"; -pub struct RateLimitService { - endpoint: String, - tracing_headers: Vec<(TracingHeader, Bytes)>, -} +pub const RATELIMIT_SERVICE_NAME: &str = "envoy.service.ratelimit.v3.RateLimitService"; +pub const RATELIMIT_METHOD_NAME: &str = "ShouldRateLimit"; -impl RateLimitService { - pub fn new(endpoint: &str, metadata: Vec<(TracingHeader, Bytes)>) -> Self { - Self { - endpoint: String::from(endpoint), - tracing_headers: metadata, - } - } +pub struct RateLimitService; +impl RateLimitService { pub fn message( domain: String, descriptors: RepeatedField, @@ -35,26 +21,6 @@ impl RateLimitService { } } -impl Service for RateLimitService { - fn send(&self, message: RateLimitRequest) -> Result { - let msg = Message::write_to_bytes(&message).unwrap(); // TODO(didierofrivia): Error Handling - let metadata = self - .tracing_headers - .iter() - .map(|(header, value)| (header.as_str(), value.as_slice())) - .collect(); - - dispatch_grpc_call( - self.endpoint.as_str(), - RATELIMIT_SERVICE_NAME, - RATELIMIT_METHOD_NAME, - metadata, - Some(&msg), - Duration::from_secs(5), - ) - } -} - #[cfg(test)] mod tests { use crate::envoy::{RateLimitDescriptor, RateLimitDescriptor_Entry, RateLimitRequest}; From ebfa96e3c0bf0fb714efdf424b615dd8deec03cd Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 22 Aug 2024 11:40:42 +0100 Subject: [PATCH 06/66] Use new header resolver in the service Signed-off-by: Adam Cattermole Signed-off-by: dd di cesare --- src/filter/http_context.rs | 37 +++------------------- src/filter/root_context.rs | 3 +- src/service.rs | 64 +++++++++++++++++++++++++++----------- src/service/auth.rs | 1 - 4 files changed, 52 insertions(+), 53 deletions(-) diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index 9feedb7a..ba2fb690 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -1,42 +1,19 @@ use crate::configuration::{ExtensionType, FailureMode, FilterConfig}; use crate::envoy::{RateLimitResponse, RateLimitResponse_Code}; -use crate::filter::http_context::TracingHeader::{Baggage, Traceparent, Tracestate}; use crate::policy::Policy; use crate::service::rate_limit::RateLimitService; -use crate::service::GrpcServiceHandler; +use crate::service::{GrpcServiceHandler, HeaderResolver}; use log::{debug, warn}; use protobuf::Message; use proxy_wasm::traits::{Context, HttpContext}; -use proxy_wasm::types::{Action, Bytes}; +use proxy_wasm::types::Action; use std::rc::Rc; -// tracing headers -#[derive(Clone)] -pub enum TracingHeader { - Traceparent, - Tracestate, - Baggage, -} - -impl TracingHeader { - pub fn all() -> [Self; 3] { - [Traceparent, Tracestate, Baggage] - } - - pub fn as_str(&self) -> &'static str { - match self { - Traceparent => "traceparent", - Tracestate => "tracestate", - Baggage => "baggage", - } - } -} - pub struct Filter { pub context_id: u32, pub config: Rc, pub response_headers_to_add: Vec<(String, String)>, - pub tracing_headers: Vec<(TracingHeader, Bytes)>, + pub header_resolver: Rc, } impl Filter { @@ -66,7 +43,7 @@ impl Filter { let rls = GrpcServiceHandler::new( ExtensionType::RateLimit, rlp.service.clone(), - self.tracing_headers.clone(), + Rc::clone(&self.header_resolver), ); let message = RateLimitService::message(rlp.domain.clone(), descriptors); @@ -102,12 +79,6 @@ impl HttpContext for Filter { fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { debug!("#{} on_http_request_headers", self.context_id); - for header in TracingHeader::all() { - if let Some(value) = self.get_http_request_header_bytes(header.as_str()) { - self.tracing_headers.push((header, value)) - } - } - match self .config .index diff --git a/src/filter/root_context.rs b/src/filter/root_context.rs index ab28c72c..90774e1c 100644 --- a/src/filter/root_context.rs +++ b/src/filter/root_context.rs @@ -1,5 +1,6 @@ use crate::configuration::{FilterConfig, PluginConfiguration}; use crate::filter::http_context::Filter; +use crate::service::HeaderResolver; use const_format::formatcp; use log::{debug, error, info}; use proxy_wasm::traits::{Context, HttpContext, RootContext}; @@ -40,7 +41,7 @@ impl RootContext for FilterRoot { context_id, config: Rc::clone(&self.config), response_headers_to_add: Vec::default(), - tracing_headers: Vec::default(), + header_resolver: Rc::new(HeaderResolver::new()), })) } diff --git a/src/service.rs b/src/service.rs index 99424631..1f16198a 100644 --- a/src/service.rs +++ b/src/service.rs @@ -2,21 +2,22 @@ pub(crate) mod auth; pub(crate) mod rate_limit; use crate::configuration::ExtensionType; -use crate::filter::http_context::TracingHeader; use crate::service::auth::{AUTH_METHOD_NAME, AUTH_SERVICE_NAME}; use crate::service::rate_limit::{RATELIMIT_METHOD_NAME, RATELIMIT_SERVICE_NAME}; +use crate::service::TracingHeader::{Baggage, Traceparent, Tracestate}; use protobuf::Message; use proxy_wasm::hostcalls; use proxy_wasm::hostcalls::dispatch_grpc_call; use proxy_wasm::types::{Bytes, MapType, Status}; use std::cell::OnceCell; +use std::rc::Rc; use std::time::Duration; pub struct GrpcServiceHandler { endpoint: String, service_name: String, method_name: String, - tracing_headers: Vec<(TracingHeader, Bytes)>, + header_resolver: Rc, } impl GrpcServiceHandler { @@ -24,33 +25,33 @@ impl GrpcServiceHandler { endpoint: String, service_name: &str, method_name: &str, - tracing_headers: Vec<(TracingHeader, Bytes)>, + header_resolver: Rc, ) -> Self { Self { endpoint: endpoint.to_owned(), service_name: service_name.to_owned(), method_name: method_name.to_owned(), - tracing_headers, + header_resolver, } } pub fn new( extension_type: ExtensionType, endpoint: String, - tracing_headers: Vec<(TracingHeader, Bytes)>, + header_resolver: Rc, ) -> Self { match extension_type { ExtensionType::Auth => Self::build( endpoint, AUTH_SERVICE_NAME, AUTH_METHOD_NAME, - tracing_headers, + header_resolver, ), ExtensionType::RateLimit => Self::build( endpoint, RATELIMIT_SERVICE_NAME, RATELIMIT_METHOD_NAME, - tracing_headers, + header_resolver, ), } } @@ -58,9 +59,10 @@ impl GrpcServiceHandler { pub fn send(&self, message: M) -> Result { let msg = Message::write_to_bytes(&message).unwrap(); let metadata = self - .tracing_headers + .header_resolver + .get() .iter() - .map(|(header, value)| (header.as_str(), value.as_slice())) + .map(|(header, value)| (*header, value.as_slice())) .collect(); dispatch_grpc_call( @@ -74,23 +76,49 @@ impl GrpcServiceHandler { } } -pub struct TracingHeaderResolver { - tracing_headers: OnceCell>, +pub struct HeaderResolver { + headers: OnceCell>, } -impl TracingHeaderResolver { - pub fn get(&self) -> &Vec<(TracingHeader, Bytes)> { - self.tracing_headers.get_or_init(|| { +impl HeaderResolver { + pub fn new() -> Self { + Self { + headers: OnceCell::new(), + } + } + + pub fn get(&self) -> &Vec<(&'static str, Bytes)> { + self.headers.get_or_init(|| { let mut headers = Vec::new(); for header in TracingHeader::all() { - if let Some(value) = - hostcalls::get_map_value_bytes(MapType::HttpRequestHeaders, header.as_str()) - .unwrap() + if let Ok(Some(value)) = + hostcalls::get_map_value_bytes(MapType::HttpRequestHeaders, (*header).as_str()) { - headers.push((header, value)); + headers.push(((*header).as_str(), value)); } } headers }) } } + +// tracing headers +pub enum TracingHeader { + Traceparent, + Tracestate, + Baggage, +} + +impl TracingHeader { + fn all() -> &'static [Self; 3] { + &[Traceparent, Tracestate, Baggage] + } + + pub fn as_str(&self) -> &'static str { + match self { + Traceparent => "traceparent", + Tracestate => "tracestate", + Baggage => "baggage", + } + } +} diff --git a/src/service/auth.rs b/src/service/auth.rs index 36220f59..1e7c7344 100644 --- a/src/service/auth.rs +++ b/src/service/auth.rs @@ -5,7 +5,6 @@ use crate::envoy::{ }; use chrono::{DateTime, FixedOffset, Timelike}; use protobuf::well_known_types::Timestamp; -use protobuf::Message; use proxy_wasm::hostcalls; use proxy_wasm::types::MapType; use std::collections::HashMap; From d5d6ad77c5e239e01d6d44ec533b2678a4f18573 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 22 Aug 2024 14:08:40 +0100 Subject: [PATCH 07/66] Fix tests using header resolver Signed-off-by: Adam Cattermole Signed-off-by: dd di cesare --- tests/rate_limited.rs | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/tests/rate_limited.rs b/tests/rate_limited.rs index 44bfec3c..6a0412c8 100644 --- a/tests/rate_limited.rs +++ b/tests/rate_limited.rs @@ -57,12 +57,6 @@ fn it_loads() { module .call_proxy_on_request_headers(http_context, 0, false) .expect_log(Some(LogLevel::Debug), Some("#2 on_http_request_headers")) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("traceparent")) - .returning(None) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("tracestate")) - .returning(None) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("baggage")) - .returning(None) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some(":authority")) .returning(Some("cars.toystore.com")) .expect_log( @@ -161,14 +155,14 @@ fn it_limits() { module .call_proxy_on_request_headers(http_context, 0, false) .expect_log(Some(LogLevel::Debug), Some("#2 on_http_request_headers")) + .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some(":authority")) + .returning(Some("cars.toystore.com")) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("traceparent")) .returning(None) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("tracestate")) .returning(None) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("baggage")) .returning(None) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some(":authority")) - .returning(Some("cars.toystore.com")) .expect_get_property(Some(vec!["request", "url_path"])) .returning(Some("/admin/toy".as_bytes())) .expect_get_property(Some(vec!["request", "host"])) @@ -311,14 +305,14 @@ fn it_passes_additional_headers() { module .call_proxy_on_request_headers(http_context, 0, false) .expect_log(Some(LogLevel::Debug), Some("#2 on_http_request_headers")) + .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some(":authority")) + .returning(Some("cars.toystore.com")) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("traceparent")) .returning(None) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("tracestate")) .returning(None) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("baggage")) .returning(None) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some(":authority")) - .returning(Some("cars.toystore.com")) .expect_get_property(Some(vec!["request", "url_path"])) .returning(Some("/admin/toy".as_bytes())) .expect_get_property(Some(vec!["request", "host"])) @@ -455,14 +449,14 @@ fn it_rate_limits_with_empty_conditions() { module .call_proxy_on_request_headers(http_context, 0, false) .expect_log(Some(LogLevel::Debug), Some("#2 on_http_request_headers")) + .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some(":authority")) + .returning(Some("a.com")) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("traceparent")) .returning(None) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("tracestate")) .returning(None) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("baggage")) .returning(None) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some(":authority")) - .returning(Some("a.com")) .expect_log( Some(LogLevel::Debug), Some("#2 ratelimitpolicy selected some-name"), @@ -566,12 +560,6 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value module .call_proxy_on_request_headers(http_context, 0, false) .expect_log(Some(LogLevel::Debug), Some("#2 on_http_request_headers")) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("traceparent")) - .returning(None) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("tracestate")) - .returning(None) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("baggage")) - .returning(None) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some(":authority")) .returning(Some("a.com")) .expect_get_property(Some(vec!["unknown", "path"])) From db504ba6fe29b5f29976b08c634f70fa5b96bae2 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Wed, 28 Aug 2024 15:25:11 +0100 Subject: [PATCH 08/66] Ignore authservice until used Signed-off-by: Adam Cattermole Signed-off-by: dd di cesare --- src/service/auth.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/service/auth.rs b/src/service/auth.rs index 1e7c7344..0831cd6c 100644 --- a/src/service/auth.rs +++ b/src/service/auth.rs @@ -14,6 +14,7 @@ pub const AUTH_METHOD_NAME: &str = "Check"; pub struct AuthService; +#[allow(dead_code)] impl AuthService { pub fn message(ce_host: String) -> CheckRequest { AuthService::build_check_req(ce_host) From cb114c8714fc263b0e3765d154365e3f4e2d06ee Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 29 Aug 2024 11:19:12 +0100 Subject: [PATCH 09/66] Create service at configuration level Signed-off-by: Adam Cattermole Signed-off-by: dd di cesare --- src/configuration.rs | 9 +++++ src/filter/http_context.rs | 5 +-- src/service.rs | 75 ++++++++++++++++++++------------------ 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index 9526f8cd..a3436dde 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,5 +1,6 @@ use std::cell::OnceCell; use std::fmt::{Debug, Display, Formatter}; +use std::rc::Rc; use std::sync::Arc; use cel_interpreter::objects::ValueType; @@ -10,6 +11,7 @@ use serde::Deserialize; use crate::attribute::Attribute; use crate::policy::Policy; use crate::policy_index::PolicyIndex; +use crate::service::GrpcService; #[derive(Deserialize, Debug, Clone)] pub struct SelectorItem { @@ -441,6 +443,7 @@ pub struct FilterConfig { pub index: PolicyIndex, // Deny/Allow request when faced with an irrecoverable failure. pub failure_mode: FailureMode, + pub service: Rc, } impl Default for FilterConfig { @@ -448,6 +451,7 @@ impl Default for FilterConfig { Self { index: PolicyIndex::new(), failure_mode: FailureMode::Deny, + service: Rc::new(GrpcService::default()), } } } @@ -480,9 +484,14 @@ impl TryFrom for FilterConfig { } } + // todo(adam-cattermole): retrieve from config + let rl_service = + GrpcService::new(ExtensionType::RateLimit, config.policies[0].service.clone()); + Ok(Self { index, failure_mode: config.failure_mode, + service: Rc::new(rl_service), }) } } diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index ba2fb690..531ab7f8 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -1,4 +1,4 @@ -use crate::configuration::{ExtensionType, FailureMode, FilterConfig}; +use crate::configuration::{FailureMode, FilterConfig}; use crate::envoy::{RateLimitResponse, RateLimitResponse_Code}; use crate::policy::Policy; use crate::service::rate_limit::RateLimitService; @@ -41,8 +41,7 @@ impl Filter { } let rls = GrpcServiceHandler::new( - ExtensionType::RateLimit, - rlp.service.clone(), + Rc::clone(&self.config.service), Rc::clone(&self.header_resolver), ); let message = RateLimitService::message(rlp.domain.clone(), descriptors); diff --git a/src/service.rs b/src/service.rs index 1f16198a..e0b01681 100644 --- a/src/service.rs +++ b/src/service.rs @@ -13,49 +13,52 @@ use std::cell::OnceCell; use std::rc::Rc; use std::time::Duration; -pub struct GrpcServiceHandler { +#[derive(Default)] +pub struct GrpcService { endpoint: String, - service_name: String, - method_name: String, + name: &'static str, + method: &'static str, +} + +impl GrpcService { + pub fn new(extension_type: ExtensionType, endpoint: String) -> Self { + match extension_type { + ExtensionType::Auth => Self { + endpoint, + name: AUTH_SERVICE_NAME, + method: AUTH_METHOD_NAME, + }, + ExtensionType::RateLimit => Self { + endpoint, + name: RATELIMIT_SERVICE_NAME, + method: RATELIMIT_METHOD_NAME, + }, + } + } + fn endpoint(&self) -> &str { + &self.endpoint + } + fn name(&self) -> &str { + self.name + } + fn method(&self) -> &str { + self.method + } +} + +pub struct GrpcServiceHandler { + service: Rc, header_resolver: Rc, } impl GrpcServiceHandler { - fn build( - endpoint: String, - service_name: &str, - method_name: &str, - header_resolver: Rc, - ) -> Self { + pub fn new(service: Rc, header_resolver: Rc) -> Self { Self { - endpoint: endpoint.to_owned(), - service_name: service_name.to_owned(), - method_name: method_name.to_owned(), + service, header_resolver, } } - pub fn new( - extension_type: ExtensionType, - endpoint: String, - header_resolver: Rc, - ) -> Self { - match extension_type { - ExtensionType::Auth => Self::build( - endpoint, - AUTH_SERVICE_NAME, - AUTH_METHOD_NAME, - header_resolver, - ), - ExtensionType::RateLimit => Self::build( - endpoint, - RATELIMIT_SERVICE_NAME, - RATELIMIT_METHOD_NAME, - header_resolver, - ), - } - } - pub fn send(&self, message: M) -> Result { let msg = Message::write_to_bytes(&message).unwrap(); let metadata = self @@ -66,9 +69,9 @@ impl GrpcServiceHandler { .collect(); dispatch_grpc_call( - self.endpoint.as_str(), - self.service_name.as_str(), - self.method_name.as_str(), + self.service.endpoint(), + self.service.name(), + self.service.method(), metadata, Some(&msg), Duration::from_secs(5), From b9f979a7ae0b5c2b041be50c1a07356d2e832589 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 29 Aug 2024 13:30:12 +0100 Subject: [PATCH 10/66] Add and use extensions from configuration Signed-off-by: Adam Cattermole Signed-off-by: dd di cesare --- src/configuration.rs | 122 ++++++++++++++++++++++++++----------- src/filter/http_context.rs | 28 ++++++--- src/policy.rs | 10 +-- src/policy_index.rs | 8 +-- src/service.rs | 17 +++++- 5 files changed, 126 insertions(+), 59 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index a3436dde..554c086d 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,4 +1,5 @@ use std::cell::OnceCell; +use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter}; use std::rc::Rc; use std::sync::Arc; @@ -441,17 +442,14 @@ pub fn type_of(path: &str) -> Option { pub struct FilterConfig { pub index: PolicyIndex, - // Deny/Allow request when faced with an irrecoverable failure. - pub failure_mode: FailureMode, - pub service: Rc, + pub services: Rc>>, } impl Default for FilterConfig { fn default() -> Self { Self { index: PolicyIndex::new(), - failure_mode: FailureMode::Deny, - service: Rc::new(GrpcService::default()), + services: Rc::new(HashMap::new()), } } } @@ -484,21 +482,33 @@ impl TryFrom for FilterConfig { } } - // todo(adam-cattermole): retrieve from config - let rl_service = - GrpcService::new(ExtensionType::RateLimit, config.policies[0].service.clone()); + // configure grpc services from the extensions in config + let services = config + .extensions + .into_iter() + .map(|(name, ext)| { + ( + name, + Rc::new(GrpcService::new( + ext.extension_type, + ext.endpoint, + ext.failure_mode, + )), + ) + }) + .collect(); Ok(Self { index, - failure_mode: config.failure_mode, - service: Rc::new(rl_service), + services: Rc::new(services), }) } } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, Default)] #[serde(rename_all = "lowercase")] pub enum FailureMode { + #[default] Deny, Allow, } @@ -513,8 +523,16 @@ pub enum ExtensionType { #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct PluginConfiguration { - #[serde(rename = "rateLimitPolicies")] + pub extensions: HashMap, pub policies: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Extension { + #[serde(rename = "type")] + pub extension_type: ExtensionType, + pub endpoint: String, // Deny/Allow request when faced with an irrecoverable failure. pub failure_mode: FailureMode, } @@ -524,12 +542,17 @@ mod test { use super::*; const CONFIG: &str = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "limitador": { + "type": "ratelimit", + "endpoint": "limitador-cluster", + "failureMode": "deny" + } + }, + "policies": [ { "name": "rlp-ns-A/rlp-name-A", "domain": "rlp-ns-A/rlp-name-A", - "service": "limitador-cluster", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -621,8 +644,8 @@ mod test { #[test] fn parse_config_min() { let config = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [] + "extensions": {}, + "policies": [] }"#; let res = serde_json::from_str::(config); if let Err(ref e) = res { @@ -637,12 +660,17 @@ mod test { #[test] fn parse_config_data_selector() { let config = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "limitador": { + "type": "ratelimit", + "endpoint": "limitador-cluster", + "failureMode": "deny" + } + }, + "policies": [ { "name": "rlp-ns-A/rlp-name-A", "domain": "rlp-ns-A/rlp-name-A", - "service": "limitador-cluster", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -687,12 +715,17 @@ mod test { #[test] fn parse_config_condition_selector_operators() { let config = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "limitador": { + "type": "ratelimit", + "endpoint": "limitador-cluster", + "failureMode": "deny" + } + }, + "policies": [ { "name": "rlp-ns-A/rlp-name-A", "domain": "rlp-ns-A/rlp-name-A", - "service": "limitador-cluster", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -766,12 +799,17 @@ mod test { #[test] fn parse_config_conditions_optional() { let config = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "limitador": { + "type": "ratelimit", + "endpoint": "limitador-cluster", + "failureMode": "deny" + } + }, + "policies": [ { "name": "rlp-ns-A/rlp-name-A", "domain": "rlp-ns-A/rlp-name-A", - "service": "limitador-cluster", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -810,12 +848,17 @@ mod test { fn parse_config_invalid_data() { // data item fields are mutually exclusive let bad_config = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "limitador": { + "type": "ratelimit", + "endpoint": "limitador-cluster", + "failureMode": "deny" + } + }, + "policies": [ { "name": "rlp-ns-A/rlp-name-A", "domain": "rlp-ns-A/rlp-name-A", - "service": "limitador-cluster", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -837,8 +880,14 @@ mod test { // data item unknown fields are forbidden let bad_config = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "limitador": { + "type": "ratelimit", + "endpoint": "limitador-cluster", + "failureMode": "deny" + } + }, + "policies": [ { "name": "rlp-ns-A/rlp-name-A", "domain": "rlp-ns-A/rlp-name-A", @@ -861,12 +910,17 @@ mod test { // condition selector operator unknown let bad_config = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "limitador": { + "type": "ratelimit", + "endpoint": "limitador-cluster", + "failureMode": "deny" + } + }, + "policies": [ { "name": "rlp-ns-A/rlp-name-A", "domain": "rlp-ns-A/rlp-name-A", - "service": "limitador-cluster", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index 531ab7f8..fa80e75c 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -40,13 +40,19 @@ impl Filter { return Action::Continue; } - let rls = GrpcServiceHandler::new( - Rc::clone(&self.config.service), - Rc::clone(&self.header_resolver), - ); + // todo(adam-cattermole): For now we just get the first GrpcService but we expect to have + // an action which links to the service that should be used + let rls = self + .config + .services + .values() + .next() + .expect("expect a value"); + + let handler = GrpcServiceHandler::new(Rc::clone(rls), Rc::clone(&self.header_resolver)); let message = RateLimitService::message(rlp.domain.clone(), descriptors); - match rls.send(message) { + match handler.send(message) { Ok(call_id) => { debug!( "#{} initiated gRPC call (id# {}) to Limitador", @@ -56,7 +62,7 @@ impl Filter { } Err(e) => { warn!("gRPC call to Limitador failed! {e:?}"); - if let FailureMode::Deny = self.config.failure_mode { + if let FailureMode::Deny = rls.failure_mode() { self.send_http_response(500, vec![], Some(b"Internal Server Error.\n")) } Action::Continue @@ -65,7 +71,15 @@ impl Filter { } fn handle_error_on_grpc_response(&self) { - match &self.config.failure_mode { + // todo(adam-cattermole): We need a method of knowing which service is the one currently + // being used (the current action) so that we can get the failure mode + let rls = self + .config + .services + .values() + .next() + .expect("expect a value"); + match rls.failure_mode() { FailureMode::Deny => { self.send_http_response(500, vec![], Some(b"Internal Server Error.\n")) } diff --git a/src/policy.rs b/src/policy.rs index 6788b74c..f9368bbe 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -26,24 +26,16 @@ pub struct Rule { pub struct Policy { pub name: String, pub domain: String, - pub service: String, pub hostnames: Vec, pub rules: Vec, } impl Policy { #[cfg(test)] - pub fn new( - name: String, - domain: String, - service: String, - hostnames: Vec, - rules: Vec, - ) -> Self { + pub fn new(name: String, domain: String, hostnames: Vec, rules: Vec) -> Self { Policy { name, domain, - service, hostnames, rules, } diff --git a/src/policy_index.rs b/src/policy_index.rs index 4a1bf607..58b31d94 100644 --- a/src/policy_index.rs +++ b/src/policy_index.rs @@ -41,13 +41,7 @@ mod tests { use crate::policy_index::PolicyIndex; fn build_ratelimit_policy(name: &str) -> Policy { - Policy::new( - name.to_owned(), - "".to_owned(), - "".to_owned(), - Vec::new(), - Vec::new(), - ) + Policy::new(name.to_owned(), "".to_owned(), Vec::new(), Vec::new()) } #[test] diff --git a/src/service.rs b/src/service.rs index e0b01681..e6b13d61 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,7 +1,7 @@ pub(crate) mod auth; pub(crate) mod rate_limit; -use crate::configuration::ExtensionType; +use crate::configuration::{ExtensionType, FailureMode}; use crate::service::auth::{AUTH_METHOD_NAME, AUTH_SERVICE_NAME}; use crate::service::rate_limit::{RATELIMIT_METHOD_NAME, RATELIMIT_SERVICE_NAME}; use crate::service::TracingHeader::{Baggage, Traceparent, Tracestate}; @@ -18,20 +18,23 @@ pub struct GrpcService { endpoint: String, name: &'static str, method: &'static str, + failure_mode: FailureMode, } impl GrpcService { - pub fn new(extension_type: ExtensionType, endpoint: String) -> Self { + pub fn new(extension_type: ExtensionType, endpoint: String, failure_mode: FailureMode) -> Self { match extension_type { ExtensionType::Auth => Self { endpoint, name: AUTH_SERVICE_NAME, method: AUTH_METHOD_NAME, + failure_mode, }, ExtensionType::RateLimit => Self { endpoint, name: RATELIMIT_SERVICE_NAME, method: RATELIMIT_METHOD_NAME, + failure_mode, }, } } @@ -44,8 +47,12 @@ impl GrpcService { fn method(&self) -> &str { self.method } + pub fn failure_mode(&self) -> &FailureMode { + &self.failure_mode + } } +#[derive(Default)] pub struct GrpcServiceHandler { service: Rc, header_resolver: Rc, @@ -83,6 +90,12 @@ pub struct HeaderResolver { headers: OnceCell>, } +impl Default for HeaderResolver { + fn default() -> Self { + Self::new() + } +} + impl HeaderResolver { pub fn new() -> Self { Self { From 6a6f6cb4fbf185827b3297a226913adcef5ac4c0 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 29 Aug 2024 14:10:46 +0100 Subject: [PATCH 11/66] Update tests to use new config Signed-off-by: Adam Cattermole Signed-off-by: dd di cesare --- tests/rate_limited.rs | 48 ++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/tests/rate_limited.rs b/tests/rate_limited.rs index 6a0412c8..ccf17048 100644 --- a/tests/rate_limited.rs +++ b/tests/rate_limited.rs @@ -29,8 +29,8 @@ fn it_loads() { let root_context = 1; let cfg = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [] + "extensions": {}, + "policies": [] }"#; module @@ -90,12 +90,17 @@ fn it_limits() { let root_context = 1; let cfg = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "limitador": { + "type": "ratelimit", + "endpoint": "limitador-cluster", + "failureMode": "deny" + } + }, + "policies": [ { "name": "some-name", "domain": "RLS-domain", - "service": "limitador-cluster", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -240,12 +245,17 @@ fn it_passes_additional_headers() { let root_context = 1; let cfg = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "limitador": { + "type": "ratelimit", + "endpoint": "limitador-cluster", + "failureMode": "deny" + } + }, + "policies": [ { "name": "some-name", "domain": "RLS-domain", - "service": "limitador-cluster", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -404,12 +414,17 @@ fn it_rate_limits_with_empty_conditions() { let root_context = 1; let cfg = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "limitador": { + "type": "ratelimit", + "endpoint": "limitador-cluster", + "failureMode": "deny" + } + }, + "policies": [ { "name": "some-name", "domain": "RLS-domain", - "service": "limitador-cluster", "hostnames": ["*.com"], "rules": [ { @@ -516,12 +531,17 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value let root_context = 1; let cfg = r#"{ - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "limitador": { + "type": "ratelimit", + "endpoint": "limitador-cluster", + "failureMode": "deny" + } + }, + "policies": [ { "name": "some-name", "domain": "RLS-domain", - "service": "limitador-cluster", "hostnames": ["*.com"], "rules": [ { From 3fa4ddd51ecc296a1ad2b750bfa1179948c383b8 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 27 Aug 2024 16:40:04 +0200 Subject: [PATCH 12/66] [feat] Action dispatcher state machine, naive impl Signed-off-by: dd di cesare --- src/action_dispatcher.rs | 201 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 202 insertions(+) create mode 100644 src/action_dispatcher.rs diff --git a/src/action_dispatcher.rs b/src/action_dispatcher.rs new file mode 100644 index 00000000..d22550d0 --- /dev/null +++ b/src/action_dispatcher.rs @@ -0,0 +1,201 @@ +use std::cell::RefCell; + +#[derive(PartialEq, Debug, Clone)] +pub(crate) enum State { + Pending, + Waiting, + Done, +} + +impl State { + fn next(&mut self) { + match self { + State::Pending => *self = State::Waiting, + State::Waiting => *self = State::Done, + _ => {} + } + } +} +#[derive(PartialEq, Clone)] +pub(crate) enum Action { + Auth { state: State }, + RateLimit { state: State }, +} + +impl Action { + pub fn trigger(&mut self) { + match self { + Action::Auth { .. } => self.auth(), + Action::RateLimit { .. } => self.rate_limit(), + } + } + + fn get_state(&self) -> &State { + match self { + Action::Auth { state } => state, + Action::RateLimit { state } => state, + } + } + + fn rate_limit(&mut self) { + // Specifics for RL, returning State + if let Action::RateLimit { state } = self { + match state { + State::Pending => { + println!("Trigger the request and return State::Waiting"); + state.next(); + } + State::Waiting => { + println!( + "When got on_grpc_response, process RL response and return State::Done" + ); + state.next(); + } + State::Done => { + println!("Done for RL... calling next action (?)"); + } + } + } + } + + fn auth(&mut self) { + // Specifics for Auth, returning State + if let Action::Auth { state } = self { + match state { + State::Pending => { + println!("Trigger the request and return State::Waiting"); + state.next(); + } + State::Waiting => { + println!( + "When got on_grpc_response, process Auth response and return State::Done" + ); + state.next(); + } + State::Done => { + println!("Done for Auth... calling next action (?)"); + } + } + } + } +} + +pub struct ActionDispatcher { + actions: RefCell>, +} + +impl ActionDispatcher { + pub fn default() -> ActionDispatcher { + ActionDispatcher { + actions: RefCell::new(vec![]), + } + } + + pub fn new(/*vec of PluginConfig actions*/) -> ActionDispatcher { + ActionDispatcher::default() + } + + pub fn push_actions(&self, actions: Vec) { + self.actions.borrow_mut().extend(actions); + } + + pub fn get_current_action_state(&self) -> Option { + self.actions + .borrow() + .first() + .map(|action| action.get_state().clone()) + } + + pub fn next(&self) -> bool { + let mut actions = self.actions.borrow_mut(); + if let Some((i, action)) = actions.iter_mut().enumerate().next() { + if let State::Done = action.get_state() { + actions.remove(i); + actions.len() > 0 + } else { + action.trigger(); + true + } + } else { + false + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn action_transition() { + let mut action = Action::Auth { + state: State::Pending, + }; + assert_eq!(*action.get_state(), State::Pending); + action.trigger(); + assert_eq!(*action.get_state(), State::Waiting); + action.trigger(); + assert_eq!(*action.get_state(), State::Done); + } + + #[test] + fn action_dispatcher_push_actions() { + let mut action_dispatcher = ActionDispatcher { + actions: RefCell::new(vec![Action::RateLimit { + state: State::Pending, + }]), + }; + + assert_eq!(action_dispatcher.actions.borrow().len(), 1); + + action_dispatcher.push_actions(vec![Action::Auth { + state: State::Pending, + }]); + + assert_eq!(action_dispatcher.actions.borrow().len(), 2); + } + + #[test] + fn action_dispatcher_get_current_action_state() { + let action_dispatcher = ActionDispatcher { + actions: RefCell::new(vec![Action::RateLimit { + state: State::Waiting, + }]), + }; + + assert_eq!( + action_dispatcher.get_current_action_state(), + Some(State::Waiting) + ); + + let action_dispatcher2 = ActionDispatcher::default(); + + assert_eq!(action_dispatcher2.get_current_action_state(), None); + } + + #[test] + fn action_dispatcher_next() { + let mut action_dispatcher = ActionDispatcher { + actions: RefCell::new(vec![Action::RateLimit { + state: State::Pending, + }]), + }; + let mut res = action_dispatcher.next(); + assert_eq!(res, true); + assert_eq!( + action_dispatcher.get_current_action_state(), + Some(State::Waiting) + ); + + res = action_dispatcher.next(); + assert_eq!(res, true); + assert_eq!( + action_dispatcher.get_current_action_state(), + Some(State::Done) + ); + + res = action_dispatcher.next(); + assert_eq!(res, false); + assert_eq!(action_dispatcher.get_current_action_state(), None); + } +} diff --git a/src/lib.rs b/src/lib.rs index 179174b3..7c9d267d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +mod action_dispatcher; mod attribute; mod configuration; #[allow(renamed_and_removed_lints)] From 2aacc8957cd50e131661d2d06eb7dbf0a128ad2e Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Fri, 30 Aug 2024 20:15:48 +0200 Subject: [PATCH 13/66] [feat] A simplistic approach, agnostic to extension type Signed-off-by: dd di cesare --- src/action_dispatcher.rs | 129 ++++++++++++++------------------------- 1 file changed, 47 insertions(+), 82 deletions(-) diff --git a/src/action_dispatcher.rs b/src/action_dispatcher.rs index d22550d0..2e93a6cc 100644 --- a/src/action_dispatcher.rs +++ b/src/action_dispatcher.rs @@ -1,3 +1,4 @@ +use proxy_wasm::types::Status; use std::cell::RefCell; #[derive(PartialEq, Debug, Clone)] @@ -16,67 +17,40 @@ impl State { } } } -#[derive(PartialEq, Clone)] -pub(crate) enum Action { - Auth { state: State }, - RateLimit { state: State }, +#[derive(Clone)] +pub(crate) struct Action { + state: State, + result: Result, + operation: Option Result>, } impl Action { - pub fn trigger(&mut self) { - match self { - Action::Auth { .. } => self.auth(), - Action::RateLimit { .. } => self.rate_limit(), + pub fn default() -> Self { + Self { + state: State::Pending, + result: Err(Status::Empty), + operation: None, } } - fn get_state(&self) -> &State { - match self { - Action::Auth { state } => state, - Action::RateLimit { state } => state, - } + pub fn set_operation(&mut self, operation: fn() -> Result) { + self.operation = Some(operation); } - fn rate_limit(&mut self) { - // Specifics for RL, returning State - if let Action::RateLimit { state } = self { - match state { - State::Pending => { - println!("Trigger the request and return State::Waiting"); - state.next(); - } - State::Waiting => { - println!( - "When got on_grpc_response, process RL response and return State::Done" - ); - state.next(); - } - State::Done => { - println!("Done for RL... calling next action (?)"); - } - } + pub fn trigger(&mut self) { + if let State::Done = self.state { + } else if let Some(operation) = self.operation { + self.result = operation(); + self.state.next(); } } - fn auth(&mut self) { - // Specifics for Auth, returning State - if let Action::Auth { state } = self { - match state { - State::Pending => { - println!("Trigger the request and return State::Waiting"); - state.next(); - } - State::Waiting => { - println!( - "When got on_grpc_response, process Auth response and return State::Done" - ); - state.next(); - } - State::Done => { - println!("Done for Auth... calling next action (?)"); - } - } - } + fn get_state(&self) -> State { + self.state.clone() + } + + fn get_result(&self) -> Result { + self.result } } @@ -91,10 +65,6 @@ impl ActionDispatcher { } } - pub fn new(/*vec of PluginConfig actions*/) -> ActionDispatcher { - ActionDispatcher::default() - } - pub fn push_actions(&self, actions: Vec) { self.actions.borrow_mut().extend(actions); } @@ -106,6 +76,10 @@ impl ActionDispatcher { .map(|action| action.get_state().clone()) } + pub fn get_current_action_result(&self) -> Result { + self.actions.borrow().first().unwrap().get_result() + } + pub fn next(&self) -> bool { let mut actions = self.actions.borrow_mut(); if let Some((i, action)) = actions.iter_mut().enumerate().next() { @@ -128,29 +102,25 @@ mod tests { #[test] fn action_transition() { - let mut action = Action::Auth { - state: State::Pending, - }; - assert_eq!(*action.get_state(), State::Pending); + let mut action = Action::default(); + action.set_operation(|| -> Result { Ok(200) }); + assert_eq!(action.get_state(), State::Pending); action.trigger(); - assert_eq!(*action.get_state(), State::Waiting); + assert_eq!(action.get_state(), State::Waiting); action.trigger(); - assert_eq!(*action.get_state(), State::Done); + assert_eq!(action.result, Ok(200)); + assert_eq!(action.get_state(), State::Done); } #[test] fn action_dispatcher_push_actions() { - let mut action_dispatcher = ActionDispatcher { - actions: RefCell::new(vec![Action::RateLimit { - state: State::Pending, - }]), + let action_dispatcher = ActionDispatcher { + actions: RefCell::new(vec![Action::default()]), }; assert_eq!(action_dispatcher.actions.borrow().len(), 1); - action_dispatcher.push_actions(vec![Action::Auth { - state: State::Pending, - }]); + action_dispatcher.push_actions(vec![Action::default()]); assert_eq!(action_dispatcher.actions.borrow().len(), 2); } @@ -158,44 +128,39 @@ mod tests { #[test] fn action_dispatcher_get_current_action_state() { let action_dispatcher = ActionDispatcher { - actions: RefCell::new(vec![Action::RateLimit { - state: State::Waiting, - }]), + actions: RefCell::new(vec![Action::default()]), }; assert_eq!( action_dispatcher.get_current_action_state(), - Some(State::Waiting) + Some(State::Pending) ); - - let action_dispatcher2 = ActionDispatcher::default(); - - assert_eq!(action_dispatcher2.get_current_action_state(), None); } #[test] fn action_dispatcher_next() { - let mut action_dispatcher = ActionDispatcher { - actions: RefCell::new(vec![Action::RateLimit { - state: State::Pending, - }]), + let mut action = Action::default(); + action.set_operation(|| -> Result { Ok(200) }); + let action_dispatcher = ActionDispatcher { + actions: RefCell::new(vec![action]), }; let mut res = action_dispatcher.next(); - assert_eq!(res, true); + assert!(res); assert_eq!( action_dispatcher.get_current_action_state(), Some(State::Waiting) ); res = action_dispatcher.next(); - assert_eq!(res, true); + assert!(res); assert_eq!( action_dispatcher.get_current_action_state(), Some(State::Done) ); + assert_eq!(action_dispatcher.get_current_action_result(), Ok(200)); res = action_dispatcher.next(); - assert_eq!(res, false); + assert!(!res); assert_eq!(action_dispatcher.get_current_action_state(), None); } } From 65786d5cd20b31bde666fb1b4abc864011d9e27f Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Mon, 2 Sep 2024 10:49:52 +0200 Subject: [PATCH 14/66] [tmp] Allowing dead code Signed-off-by: dd di cesare --- src/action_dispatcher.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/action_dispatcher.rs b/src/action_dispatcher.rs index 2e93a6cc..dd43912d 100644 --- a/src/action_dispatcher.rs +++ b/src/action_dispatcher.rs @@ -1,6 +1,7 @@ use proxy_wasm::types::Status; use std::cell::RefCell; +#[allow(dead_code)] #[derive(PartialEq, Debug, Clone)] pub(crate) enum State { Pending, @@ -8,6 +9,7 @@ pub(crate) enum State { Done, } +#[allow(dead_code)] impl State { fn next(&mut self) { match self { @@ -17,6 +19,8 @@ impl State { } } } + +#[allow(dead_code)] #[derive(Clone)] pub(crate) struct Action { state: State, @@ -24,6 +28,7 @@ pub(crate) struct Action { operation: Option Result>, } +#[allow(dead_code)] impl Action { pub fn default() -> Self { Self { @@ -54,10 +59,12 @@ impl Action { } } +#[allow(dead_code)] pub struct ActionDispatcher { actions: RefCell>, } +#[allow(dead_code)] impl ActionDispatcher { pub fn default() -> ActionDispatcher { ActionDispatcher { From 8d4ee0ebb14bce31d5b6d2e6ecdc97754d7c0bc7 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Mon, 2 Sep 2024 15:51:55 +0200 Subject: [PATCH 15/66] [refactor] Changing name to `Operation` instead of `Action` * Could get confusing with proxy_wasm `Actions` * Also with plugin configuration `Action` Signed-off-by: dd di cesare --- src/action_dispatcher.rs | 173 ------------------------------------ src/lib.rs | 2 +- src/operation_dispatcher.rs | 173 ++++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 174 deletions(-) delete mode 100644 src/action_dispatcher.rs create mode 100644 src/operation_dispatcher.rs diff --git a/src/action_dispatcher.rs b/src/action_dispatcher.rs deleted file mode 100644 index dd43912d..00000000 --- a/src/action_dispatcher.rs +++ /dev/null @@ -1,173 +0,0 @@ -use proxy_wasm::types::Status; -use std::cell::RefCell; - -#[allow(dead_code)] -#[derive(PartialEq, Debug, Clone)] -pub(crate) enum State { - Pending, - Waiting, - Done, -} - -#[allow(dead_code)] -impl State { - fn next(&mut self) { - match self { - State::Pending => *self = State::Waiting, - State::Waiting => *self = State::Done, - _ => {} - } - } -} - -#[allow(dead_code)] -#[derive(Clone)] -pub(crate) struct Action { - state: State, - result: Result, - operation: Option Result>, -} - -#[allow(dead_code)] -impl Action { - pub fn default() -> Self { - Self { - state: State::Pending, - result: Err(Status::Empty), - operation: None, - } - } - - pub fn set_operation(&mut self, operation: fn() -> Result) { - self.operation = Some(operation); - } - - pub fn trigger(&mut self) { - if let State::Done = self.state { - } else if let Some(operation) = self.operation { - self.result = operation(); - self.state.next(); - } - } - - fn get_state(&self) -> State { - self.state.clone() - } - - fn get_result(&self) -> Result { - self.result - } -} - -#[allow(dead_code)] -pub struct ActionDispatcher { - actions: RefCell>, -} - -#[allow(dead_code)] -impl ActionDispatcher { - pub fn default() -> ActionDispatcher { - ActionDispatcher { - actions: RefCell::new(vec![]), - } - } - - pub fn push_actions(&self, actions: Vec) { - self.actions.borrow_mut().extend(actions); - } - - pub fn get_current_action_state(&self) -> Option { - self.actions - .borrow() - .first() - .map(|action| action.get_state().clone()) - } - - pub fn get_current_action_result(&self) -> Result { - self.actions.borrow().first().unwrap().get_result() - } - - pub fn next(&self) -> bool { - let mut actions = self.actions.borrow_mut(); - if let Some((i, action)) = actions.iter_mut().enumerate().next() { - if let State::Done = action.get_state() { - actions.remove(i); - actions.len() > 0 - } else { - action.trigger(); - true - } - } else { - false - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn action_transition() { - let mut action = Action::default(); - action.set_operation(|| -> Result { Ok(200) }); - assert_eq!(action.get_state(), State::Pending); - action.trigger(); - assert_eq!(action.get_state(), State::Waiting); - action.trigger(); - assert_eq!(action.result, Ok(200)); - assert_eq!(action.get_state(), State::Done); - } - - #[test] - fn action_dispatcher_push_actions() { - let action_dispatcher = ActionDispatcher { - actions: RefCell::new(vec![Action::default()]), - }; - - assert_eq!(action_dispatcher.actions.borrow().len(), 1); - - action_dispatcher.push_actions(vec![Action::default()]); - - assert_eq!(action_dispatcher.actions.borrow().len(), 2); - } - - #[test] - fn action_dispatcher_get_current_action_state() { - let action_dispatcher = ActionDispatcher { - actions: RefCell::new(vec![Action::default()]), - }; - - assert_eq!( - action_dispatcher.get_current_action_state(), - Some(State::Pending) - ); - } - - #[test] - fn action_dispatcher_next() { - let mut action = Action::default(); - action.set_operation(|| -> Result { Ok(200) }); - let action_dispatcher = ActionDispatcher { - actions: RefCell::new(vec![action]), - }; - let mut res = action_dispatcher.next(); - assert!(res); - assert_eq!( - action_dispatcher.get_current_action_state(), - Some(State::Waiting) - ); - - res = action_dispatcher.next(); - assert!(res); - assert_eq!( - action_dispatcher.get_current_action_state(), - Some(State::Done) - ); - assert_eq!(action_dispatcher.get_current_action_result(), Ok(200)); - - res = action_dispatcher.next(); - assert!(!res); - assert_eq!(action_dispatcher.get_current_action_state(), None); - } -} diff --git a/src/lib.rs b/src/lib.rs index 7c9d267d..4e4569a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -mod action_dispatcher; +mod operation_dispatcher; mod attribute; mod configuration; #[allow(renamed_and_removed_lints)] diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs new file mode 100644 index 00000000..86c6deac --- /dev/null +++ b/src/operation_dispatcher.rs @@ -0,0 +1,173 @@ +use proxy_wasm::types::Status; +use std::cell::RefCell; + +#[allow(dead_code)] +#[derive(PartialEq, Debug, Clone)] +pub(crate) enum State { + Pending, + Waiting, + Done, +} + +#[allow(dead_code)] +impl State { + fn next(&mut self) { + match self { + State::Pending => *self = State::Waiting, + State::Waiting => *self = State::Done, + _ => {} + } + } +} + +#[allow(dead_code)] +#[derive(Clone)] +pub(crate) struct Operation { + state: State, + result: Result, + action: Option Result>, +} + +#[allow(dead_code)] +impl Operation { + pub fn default() -> Self { + Self { + state: State::Pending, + result: Err(Status::Empty), + action: None, + } + } + + pub fn set_action(&mut self, action: fn() -> Result) { + self.action = Some(action); + } + + pub fn trigger(&mut self) { + if let State::Done = self.state { + } else if let Some(action) = self.action { + self.result = action(); + self.state.next(); + } + } + + fn get_state(&self) -> State { + self.state.clone() + } + + fn get_result(&self) -> Result { + self.result + } +} + +#[allow(dead_code)] +pub struct OperationDispatcher { + operations: RefCell>, +} + +#[allow(dead_code)] +impl OperationDispatcher { + pub fn default() -> OperationDispatcher { + OperationDispatcher { + operations: RefCell::new(vec![]), + } + } + + pub fn push_operations(&self, operations: Vec) { + self.operations.borrow_mut().extend(operations); + } + + pub fn get_current_operation_state(&self) -> Option { + self.operations + .borrow() + .first() + .map(|operation| operation.get_state().clone()) + } + + pub fn get_current_operation_result(&self) -> Result { + self.operations.borrow().first().unwrap().get_result() + } + + pub fn next(&self) -> bool { + let mut operations = self.operations.borrow_mut(); + if let Some((i, operation)) = operations.iter_mut().enumerate().next() { + if let State::Done = operation.get_state() { + operations.remove(i); + operations.len() > 0 + } else { + operation.trigger(); + true + } + } else { + false + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn operation_transition() { + let mut operation = Operation::default(); + operation.set_action(|| -> Result { Ok(200) }); + assert_eq!(operation.get_state(), State::Pending); + operation.trigger(); + assert_eq!(operation.get_state(), State::Waiting); + operation.trigger(); + assert_eq!(operation.result, Ok(200)); + assert_eq!(operation.get_state(), State::Done); + } + + #[test] + fn operation_dispatcher_push_actions() { + let operation_dispatcher = OperationDispatcher { + operations: RefCell::new(vec![Operation::default()]), + }; + + assert_eq!(operation_dispatcher.operations.borrow().len(), 1); + + operation_dispatcher.push_operations(vec![Operation::default()]); + + assert_eq!(operation_dispatcher.operations.borrow().len(), 2); + } + + #[test] + fn operation_dispatcher_get_current_action_state() { + let operation_dispatcher = OperationDispatcher { + operations: RefCell::new(vec![Operation::default()]), + }; + + assert_eq!( + operation_dispatcher.get_current_operation_state(), + Some(State::Pending) + ); + } + + #[test] + fn operation_dispatcher_next() { + let mut operation = Operation::default(); + operation.set_action(|| -> Result { Ok(200) }); + let operation_dispatcher = OperationDispatcher { + operations: RefCell::new(vec![operation]), + }; + let mut res = operation_dispatcher.next(); + assert!(res); + assert_eq!( + operation_dispatcher.get_current_operation_state(), + Some(State::Waiting) + ); + + res = operation_dispatcher.next(); + assert!(res); + assert_eq!( + operation_dispatcher.get_current_operation_state(), + Some(State::Done) + ); + assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(200)); + + res = operation_dispatcher.next(); + assert!(!res); + assert_eq!(operation_dispatcher.get_current_operation_state(), None); + } +} From 94d006b0e4ae4158e190e5594b6cfe453afdff24 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 3 Sep 2024 14:53:38 +0200 Subject: [PATCH 16/66] [refactor] Configuration, adding Actions Signed-off-by: dd di cesare --- src/configuration.rs | 102 ++++++++++++++++++++++++++++++++++++++---- src/policy.rs | 12 ++++- src/policy_index.rs | 8 +++- tests/rate_limited.rs | 52 +++++++++++++++++++-- 4 files changed, 159 insertions(+), 15 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index 554c086d..174b7e83 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -513,10 +513,11 @@ pub enum FailureMode { Allow, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, Default)] #[serde(rename_all = "lowercase")] pub enum ExtensionType { Auth, + #[default] RateLimit, } @@ -537,6 +538,14 @@ pub struct Extension { pub failure_mode: FailureMode, } +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Action { + pub extension: String, + #[allow(dead_code)] + pub data: DataType, +} + #[cfg(test)] mod test { use super::*; @@ -587,7 +596,18 @@ mod test { "selector": "auth.metadata.username" } }] - }] + }], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] }] }"#; @@ -682,7 +702,18 @@ mod test { "default": "my_selector_default_value" } }] - }] + }], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] }] }"#; let res = serde_json::from_str::(config); @@ -759,7 +790,18 @@ mod test { }] }], "data": [ { "selector": { "selector": "my.selector.path" } }] - }] + }], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] }] }"#; let res = serde_json::from_str::(config); @@ -825,7 +867,18 @@ mod test { "selector": "auth.metadata.username" } }] - }] + }], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] }] }"#; let res = serde_json::from_str::(config); @@ -872,7 +925,18 @@ mod test { "selector": "auth.metadata.username" } }] - }] + }], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] }] }"#; let res = serde_json::from_str::(bad_config); @@ -902,7 +966,18 @@ mod test { "value": "1" } }] - }] + }], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] }] }"#; let res = serde_json::from_str::(bad_config); @@ -934,7 +1009,18 @@ mod test { }] }], "data": [ { "selector": { "selector": "my.selector.path" } }] - }] + }], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] }] }"#; let res = serde_json::from_str::(bad_config); diff --git a/src/policy.rs b/src/policy.rs index f9368bbe..353928ea 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -1,5 +1,5 @@ use crate::attribute::Attribute; -use crate::configuration::{DataItem, DataType, PatternExpression}; +use crate::configuration::{Action, DataItem, DataType, PatternExpression}; use crate::envoy::{RateLimitDescriptor, RateLimitDescriptor_Entry}; use crate::filter::http_context::Filter; use log::debug; @@ -28,16 +28,24 @@ pub struct Policy { pub domain: String, pub hostnames: Vec, pub rules: Vec, + pub actions: Vec, } impl Policy { #[cfg(test)] - pub fn new(name: String, domain: String, hostnames: Vec, rules: Vec) -> Self { + pub fn new( + name: String, + domain: String, + hostnames: Vec, + rules: Vec, + actions: Vec, + ) -> Self { Policy { name, domain, hostnames, rules, + actions, } } diff --git a/src/policy_index.rs b/src/policy_index.rs index 58b31d94..3620179e 100644 --- a/src/policy_index.rs +++ b/src/policy_index.rs @@ -41,7 +41,13 @@ mod tests { use crate::policy_index::PolicyIndex; fn build_ratelimit_policy(name: &str) -> Policy { - Policy::new(name.to_owned(), "".to_owned(), Vec::new(), Vec::new()) + Policy::new( + name.to_owned(), + "".to_owned(), + Vec::new(), + Vec::new(), + Vec::new(), + ) } #[test] diff --git a/tests/rate_limited.rs b/tests/rate_limited.rs index ccf17048..6b70d85e 100644 --- a/tests/rate_limited.rs +++ b/tests/rate_limited.rs @@ -132,7 +132,18 @@ fn it_limits() { } } ] - }] + }], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] }] }"#; @@ -287,7 +298,18 @@ fn it_passes_additional_headers() { } } ] - }] + }], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] }] }"#; @@ -436,7 +458,18 @@ fn it_rate_limits_with_empty_conditions() { } } ] - }] + }], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] }] }"#; @@ -552,7 +585,18 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value } } ] - }] + }], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] }] }"#; From 914b1d89c6c6cc8712f40455964c97608c287dba Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 3 Sep 2024 14:55:09 +0200 Subject: [PATCH 17/66] [wip, refactor] GrpcServiceHandler builds message * GrpcMessage type created Signed-off-by: dd di cesare --- src/service.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/service.rs b/src/service.rs index e6b13d61..4d553b77 100644 --- a/src/service.rs +++ b/src/service.rs @@ -2,8 +2,9 @@ pub(crate) mod auth; pub(crate) mod rate_limit; use crate::configuration::{ExtensionType, FailureMode}; +use crate::envoy::{RateLimitDescriptor, RateLimitRequest}; use crate::service::auth::{AUTH_METHOD_NAME, AUTH_SERVICE_NAME}; -use crate::service::rate_limit::{RATELIMIT_METHOD_NAME, RATELIMIT_SERVICE_NAME}; +use crate::service::rate_limit::{RateLimitService, RATELIMIT_METHOD_NAME, RATELIMIT_SERVICE_NAME}; use crate::service::TracingHeader::{Baggage, Traceparent, Tracestate}; use protobuf::Message; use proxy_wasm::hostcalls; @@ -13,9 +14,26 @@ use std::cell::OnceCell; use std::rc::Rc; use std::time::Duration; +#[derive(Clone)] +pub enum GrpcMessage { + //Auth(CheckRequest), + RateLimit(RateLimitRequest), +} + +impl GrpcMessage { + pub fn get_message(&self) -> &RateLimitRequest { + //TODO(didierofrivia): Should return Message + match self { + GrpcMessage::RateLimit(message) => message, + } + } +} + #[derive(Default)] pub struct GrpcService { endpoint: String, + #[allow(dead_code)] + extension_type: ExtensionType, name: &'static str, method: &'static str, failure_mode: FailureMode, @@ -26,18 +44,21 @@ impl GrpcService { match extension_type { ExtensionType::Auth => Self { endpoint, + extension_type, name: AUTH_SERVICE_NAME, method: AUTH_METHOD_NAME, failure_mode, }, ExtensionType::RateLimit => Self { endpoint, + extension_type, name: RATELIMIT_SERVICE_NAME, method: RATELIMIT_METHOD_NAME, failure_mode, }, } } + fn endpoint(&self) -> &str { &self.endpoint } @@ -52,22 +73,36 @@ impl GrpcService { } } -#[derive(Default)] +type GrpcCall = fn( + upstream_name: &str, + service_name: &str, + method_name: &str, + initial_metadata: Vec<(&str, &[u8])>, + message: Option<&[u8]>, + timeout: Duration, +) -> Result; + pub struct GrpcServiceHandler { service: Rc, header_resolver: Rc, + grpc_call: GrpcCall, } impl GrpcServiceHandler { - pub fn new(service: Rc, header_resolver: Rc) -> Self { + pub fn new( + service: Rc, + header_resolver: Rc, + grpc_call: Option, + ) -> Self { Self { service, header_resolver, + grpc_call: grpc_call.unwrap_or(dispatch_grpc_call), } } - pub fn send(&self, message: M) -> Result { - let msg = Message::write_to_bytes(&message).unwrap(); + pub fn send(&self, message: GrpcMessage) -> Result { + let msg = Message::write_to_bytes(message.get_message()).unwrap(); let metadata = self .header_resolver .get() @@ -75,7 +110,7 @@ impl GrpcServiceHandler { .map(|(header, value)| (*header, value.as_slice())) .collect(); - dispatch_grpc_call( + (self.grpc_call)( self.service.endpoint(), self.service.name(), self.service.method(), @@ -84,6 +119,20 @@ impl GrpcServiceHandler { Duration::from_secs(5), ) } + + // Using domain as ce_host for the time being, we might pass a DataType in the future. + //TODO(didierofrivia): Make it work with Message. for both Auth and RL + pub fn build_message( + &self, + domain: String, + descriptors: protobuf::RepeatedField, + ) -> GrpcMessage { + /*match self.service.extension_type { + //ExtensionType::Auth => GrpcMessage::Auth(AuthService::message(domain.clone())), + //ExtensionType::RateLimit => GrpcMessage::RateLimit(RateLimitService::message(domain.clone(), descriptors)), + }*/ + GrpcMessage::RateLimit(RateLimitService::message(domain.clone(), descriptors)) + } } pub struct HeaderResolver { From c938e6b7c4d5dea2c72a9f16659d8331d02bdd18 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 3 Sep 2024 14:56:09 +0200 Subject: [PATCH 18/66] [clean] Removing obsolete code Signed-off-by: dd di cesare --- src/service/rate_limit.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/service/rate_limit.rs b/src/service/rate_limit.rs index 6dfc3c89..b6b0357c 100644 --- a/src/service/rate_limit.rs +++ b/src/service/rate_limit.rs @@ -27,8 +27,6 @@ mod tests { use crate::service::rate_limit::RateLimitService; //use crate::service::Service; use protobuf::{CachedSize, RepeatedField, UnknownFields}; - //use proxy_wasm::types::Status; - //use crate::filter::http_context::{Filter}; fn build_message() -> RateLimitRequest { let domain = "rlp1"; @@ -52,20 +50,4 @@ mod tests { assert_eq!(msg.unknown_fields, UnknownFields::default()); assert_eq!(msg.cached_size, CachedSize::default()); } - /*#[test] - fn sends_message() { - let msg = build_message(); - let metadata = vec![("header-1", "value-1".as_bytes())]; - let rls = RateLimitService::new("limitador-cluster", metadata); - - // TODO(didierofrivia): When we have a grpc response type, assert the async response - } - - fn grpc_call( - _upstream_name: &str, - _initial_metadata: Vec<(&str, &[u8])>, - _message: RateLimitRequest, - ) -> Result { - Ok(1) - } */ } From e6460e75d1bf6ca5804c6a6424c597f756dc37b5 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 3 Sep 2024 14:56:49 +0200 Subject: [PATCH 19/66] [refactor] OperationDispatcher triggering procedures Signed-off-by: dd di cesare --- src/lib.rs | 2 +- src/operation_dispatcher.rs | 126 +++++++++++++++++++++++++++--------- 2 files changed, 97 insertions(+), 31 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4e4569a7..279ed839 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,10 @@ -mod operation_dispatcher; mod attribute; mod configuration; #[allow(renamed_and_removed_lints)] mod envoy; mod filter; mod glob; +mod operation_dispatcher; mod policy; mod policy_index; mod service; diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 86c6deac..91efc538 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -1,5 +1,11 @@ +use crate::envoy::RateLimitDescriptor; +use crate::policy::Policy; +use crate::service::{GrpcMessage, GrpcServiceHandler}; +use protobuf::RepeatedField; use proxy_wasm::types::Status; use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; #[allow(dead_code)] #[derive(PartialEq, Debug, Clone)] @@ -20,32 +26,33 @@ impl State { } } +type Procedure = (Rc, GrpcMessage); + #[allow(dead_code)] -#[derive(Clone)] pub(crate) struct Operation { state: State, result: Result, - action: Option Result>, + procedure: Procedure, } #[allow(dead_code)] impl Operation { - pub fn default() -> Self { + pub fn new(procedure: Procedure) -> Self { Self { state: State::Pending, result: Err(Status::Empty), - action: None, + procedure, } } - pub fn set_action(&mut self, action: fn() -> Result) { - self.action = Some(action); + pub fn set_action(&mut self, procedure: Procedure) { + self.procedure = procedure; } pub fn trigger(&mut self) { if let State::Done = self.state { - } else if let Some(action) = self.action { - self.result = action(); + } else { + self.result = self.procedure.0.send(self.procedure.1.clone()); self.state.next(); } } @@ -62,15 +69,39 @@ impl Operation { #[allow(dead_code)] pub struct OperationDispatcher { operations: RefCell>, + service_handlers: HashMap>, } #[allow(dead_code)] impl OperationDispatcher { - pub fn default() -> OperationDispatcher { + pub fn default() -> Self { OperationDispatcher { operations: RefCell::new(vec![]), + service_handlers: HashMap::default(), } } + pub fn new(service_handlers: HashMap>) -> Self { + Self { + service_handlers, + operations: RefCell::new(vec![]), + } + } + + pub fn build_operations( + &self, + policy: &Policy, + descriptors: RepeatedField, + ) { + let mut operations: Vec = vec![]; + policy.actions.iter().for_each(|action| { + // TODO(didierofrivia): Error handling + if let Some(service) = self.service_handlers.get(&action.extension) { + let message = service.build_message(policy.domain.clone(), descriptors.clone()); + operations.push(Operation::new((service.clone(), message))) + } + }); + self.push_operations(operations); + } pub fn push_operations(&self, operations: Vec) { self.operations.borrow_mut().extend(operations); @@ -87,18 +118,19 @@ impl OperationDispatcher { self.operations.borrow().first().unwrap().get_result() } - pub fn next(&self) -> bool { + pub fn next(&self) -> Option<(State, Result)> { let mut operations = self.operations.borrow_mut(); if let Some((i, operation)) = operations.iter_mut().enumerate().next() { if let State::Done = operation.get_state() { + let res = operation.get_result(); operations.remove(i); - operations.len() > 0 + Some((State::Done, res)) } else { operation.trigger(); - true + Some((operation.state.clone(), operation.result)) } } else { - false + None } } } @@ -106,11 +138,44 @@ impl OperationDispatcher { #[cfg(test)] mod tests { use super::*; + use crate::envoy::RateLimitRequest; + use std::time::Duration; + + fn grpc_call( + _upstream_name: &str, + _service_name: &str, + _method_name: &str, + _initial_metadata: Vec<(&str, &[u8])>, + _message: Option<&[u8]>, + _timeout: Duration, + ) -> Result { + Ok(1) + } + + fn build_grpc_service_handler() -> GrpcServiceHandler { + GrpcServiceHandler::new( + Rc::new(Default::default()), + Rc::new(Default::default()), + Some(grpc_call), + ) + } + + fn build_message() -> RateLimitRequest { + RateLimitRequest { + domain: "example.org".to_string(), + descriptors: RepeatedField::new(), + hits_addend: 1, + unknown_fields: Default::default(), + cached_size: Default::default(), + } + } #[test] fn operation_transition() { - let mut operation = Operation::default(); - operation.set_action(|| -> Result { Ok(200) }); + let mut operation = Operation::new(( + Rc::new(build_grpc_service_handler()), + GrpcMessage::RateLimit(build_message()), + )); assert_eq!(operation.get_state(), State::Pending); operation.trigger(); assert_eq!(operation.get_state(), State::Waiting); @@ -121,22 +186,21 @@ mod tests { #[test] fn operation_dispatcher_push_actions() { - let operation_dispatcher = OperationDispatcher { - operations: RefCell::new(vec![Operation::default()]), - }; + let operation_dispatcher = OperationDispatcher::default(); assert_eq!(operation_dispatcher.operations.borrow().len(), 1); - operation_dispatcher.push_operations(vec![Operation::default()]); + operation_dispatcher.push_operations(vec![Operation::new(( + Rc::new(build_grpc_service_handler()), + GrpcMessage::RateLimit(build_message()), + ))]); assert_eq!(operation_dispatcher.operations.borrow().len(), 2); } #[test] fn operation_dispatcher_get_current_action_state() { - let operation_dispatcher = OperationDispatcher { - operations: RefCell::new(vec![Operation::default()]), - }; + let operation_dispatcher = OperationDispatcher::default(); assert_eq!( operation_dispatcher.get_current_operation_state(), @@ -146,20 +210,22 @@ mod tests { #[test] fn operation_dispatcher_next() { - let mut operation = Operation::default(); - operation.set_action(|| -> Result { Ok(200) }); - let operation_dispatcher = OperationDispatcher { - operations: RefCell::new(vec![operation]), - }; + let operation = Operation::new(( + Rc::new(build_grpc_service_handler()), + GrpcMessage::RateLimit(build_message()), + )); + let operation_dispatcher = OperationDispatcher::default(); + operation_dispatcher.push_operations(vec![operation]); + let mut res = operation_dispatcher.next(); - assert!(res); + assert_eq!(res, Some((State::Waiting, Ok(200)))); assert_eq!( operation_dispatcher.get_current_operation_state(), Some(State::Waiting) ); res = operation_dispatcher.next(); - assert!(res); + assert_eq!(res, Some((State::Done, Ok(200)))); assert_eq!( operation_dispatcher.get_current_operation_state(), Some(State::Done) @@ -167,7 +233,7 @@ mod tests { assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(200)); res = operation_dispatcher.next(); - assert!(!res); + assert_eq!(res, None); assert_eq!(operation_dispatcher.get_current_operation_state(), None); } } From 888e983402fd22414263c76ca37aa7c4dbd0c71c Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 3 Sep 2024 14:57:26 +0200 Subject: [PATCH 20/66] [refactor] Wiring up altogether Signed-off-by: dd di cesare --- src/filter/http_context.rs | 51 ++++++++++++++++++-------------------- src/filter/root_context.rs | 19 ++++++++++++-- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index fa80e75c..1d2a140b 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -1,8 +1,7 @@ use crate::configuration::{FailureMode, FilterConfig}; use crate::envoy::{RateLimitResponse, RateLimitResponse_Code}; +use crate::operation_dispatcher::OperationDispatcher; use crate::policy::Policy; -use crate::service::rate_limit::RateLimitService; -use crate::service::{GrpcServiceHandler, HeaderResolver}; use log::{debug, warn}; use protobuf::Message; use proxy_wasm::traits::{Context, HttpContext}; @@ -13,7 +12,7 @@ pub struct Filter { pub context_id: u32, pub config: Rc, pub response_headers_to_add: Vec<(String, String)>, - pub header_resolver: Rc, + pub operation_dispatcher: OperationDispatcher, } impl Filter { @@ -40,33 +39,31 @@ impl Filter { return Action::Continue; } - // todo(adam-cattermole): For now we just get the first GrpcService but we expect to have - // an action which links to the service that should be used - let rls = self - .config - .services - .values() - .next() - .expect("expect a value"); - - let handler = GrpcServiceHandler::new(Rc::clone(rls), Rc::clone(&self.header_resolver)); - let message = RateLimitService::message(rlp.domain.clone(), descriptors); + // Build Actions from config actions + self.operation_dispatcher.build_operations(rlp, descriptors); + // populate actions in the dispatcher + // call the next on the match - match handler.send(message) { - Ok(call_id) => { - debug!( - "#{} initiated gRPC call (id# {}) to Limitador", - self.context_id, call_id - ); - Action::Pause - } - Err(e) => { - warn!("gRPC call to Limitador failed! {e:?}"); - if let FailureMode::Deny = rls.failure_mode() { - self.send_http_response(500, vec![], Some(b"Internal Server Error.\n")) + if let Some(result) = self.operation_dispatcher.next() { + match result { + (_state, Ok(call_id)) => { + debug!( + "#{} initiated gRPC call (id# {}) to Limitador", + self.context_id, call_id + ); + Action::Pause + } + (_state, Err(e)) => { + warn!("gRPC call to Limitador failed! {e:?}"); + // TODO(didierofrivia): Get the failure_mode + /*if let FailureMode::Deny = rls.failure_mode() { + self.send_http_response(500, vec![], Some(b"Internal Server Error.\n")) + } */ + Action::Continue } - Action::Continue } + } else { + Action::Continue } } diff --git a/src/filter/root_context.rs b/src/filter/root_context.rs index 90774e1c..b4fafb77 100644 --- a/src/filter/root_context.rs +++ b/src/filter/root_context.rs @@ -1,10 +1,12 @@ use crate::configuration::{FilterConfig, PluginConfiguration}; use crate::filter::http_context::Filter; -use crate::service::HeaderResolver; +use crate::operation_dispatcher::OperationDispatcher; +use crate::service::{GrpcServiceHandler, HeaderResolver}; use const_format::formatcp; use log::{debug, error, info}; use proxy_wasm::traits::{Context, HttpContext, RootContext}; use proxy_wasm::types::ContextType; +use std::collections::HashMap; use std::rc::Rc; const WASM_SHIM_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -37,11 +39,24 @@ impl RootContext for FilterRoot { fn create_http_context(&self, context_id: u32) -> Option> { debug!("#{} create_http_context", context_id); + let mut service_handlers: HashMap> = HashMap::new(); + self.config + .services + .iter() + .for_each(|(extension, service)| { + service_handlers + .entry(extension.clone()) + .or_insert(Rc::from(GrpcServiceHandler::new( + Rc::clone(service), + Rc::new(HeaderResolver::new()), + None, + ))); + }); Some(Box::new(Filter { context_id, config: Rc::clone(&self.config), response_headers_to_add: Vec::default(), - header_resolver: Rc::new(HeaderResolver::new()), + operation_dispatcher: OperationDispatcher::new(service_handlers), })) } From 5f73f0555237dd704377ed1067676899ad58b9ba Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Wed, 4 Sep 2024 15:11:19 +0200 Subject: [PATCH 21/66] [refactor] Implementing own Message for GrpcMessage Signed-off-by: dd di cesare --- src/operation_dispatcher.rs | 6 +- src/service.rs | 150 ++++++++++++++++++++++++++++++------ 2 files changed, 133 insertions(+), 23 deletions(-) diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 91efc538..0b104402 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -96,7 +96,11 @@ impl OperationDispatcher { policy.actions.iter().for_each(|action| { // TODO(didierofrivia): Error handling if let Some(service) = self.service_handlers.get(&action.extension) { - let message = service.build_message(policy.domain.clone(), descriptors.clone()); + let message = GrpcMessage::new( + service.get_extension_type(), + policy.domain.clone(), + descriptors.clone(), + ); operations.push(Operation::new((service.clone(), message))) } }); diff --git a/src/service.rs b/src/service.rs index 4d553b77..a28ecfde 100644 --- a/src/service.rs +++ b/src/service.rs @@ -2,29 +2,145 @@ pub(crate) mod auth; pub(crate) mod rate_limit; use crate::configuration::{ExtensionType, FailureMode}; -use crate::envoy::{RateLimitDescriptor, RateLimitRequest}; -use crate::service::auth::{AUTH_METHOD_NAME, AUTH_SERVICE_NAME}; +use crate::envoy::{CheckRequest, RateLimitDescriptor, RateLimitRequest}; +use crate::service::auth::{AuthService, AUTH_METHOD_NAME, AUTH_SERVICE_NAME}; use crate::service::rate_limit::{RateLimitService, RATELIMIT_METHOD_NAME, RATELIMIT_SERVICE_NAME}; use crate::service::TracingHeader::{Baggage, Traceparent, Tracestate}; -use protobuf::Message; +use protobuf::reflect::MessageDescriptor; +use protobuf::{ + Clear, CodedInputStream, CodedOutputStream, Message, ProtobufResult, UnknownFields, +}; use proxy_wasm::hostcalls; use proxy_wasm::hostcalls::dispatch_grpc_call; use proxy_wasm::types::{Bytes, MapType, Status}; +use std::any::Any; use std::cell::OnceCell; +use std::fmt::Debug; use std::rc::Rc; use std::time::Duration; -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum GrpcMessage { - //Auth(CheckRequest), + Auth(CheckRequest), RateLimit(RateLimitRequest), } -impl GrpcMessage { - pub fn get_message(&self) -> &RateLimitRequest { - //TODO(didierofrivia): Should return Message +impl Default for GrpcMessage { + fn default() -> Self { + GrpcMessage::RateLimit(RateLimitRequest::new()) + } +} + +impl Clear for GrpcMessage { + fn clear(&mut self) { match self { - GrpcMessage::RateLimit(message) => message, + GrpcMessage::Auth(msg) => msg.clear(), + GrpcMessage::RateLimit(msg) => msg.clear(), + } + } +} + +impl Message for GrpcMessage { + fn descriptor(&self) -> &'static MessageDescriptor { + match self { + GrpcMessage::Auth(msg) => msg.descriptor(), + GrpcMessage::RateLimit(msg) => msg.descriptor(), + } + } + + fn is_initialized(&self) -> bool { + match self { + GrpcMessage::Auth(msg) => msg.is_initialized(), + GrpcMessage::RateLimit(msg) => msg.is_initialized(), + } + } + + fn merge_from(&mut self, is: &mut CodedInputStream) -> ProtobufResult<()> { + match self { + GrpcMessage::Auth(msg) => msg.merge_from(is), + GrpcMessage::RateLimit(msg) => msg.merge_from(is), + } + } + + fn write_to_with_cached_sizes(&self, os: &mut CodedOutputStream) -> ProtobufResult<()> { + match self { + GrpcMessage::Auth(msg) => msg.write_to_with_cached_sizes(os), + GrpcMessage::RateLimit(msg) => msg.write_to_with_cached_sizes(os), + } + } + + fn write_to_bytes(&self) -> ProtobufResult> { + match self { + GrpcMessage::Auth(msg) => msg.write_to_bytes(), + GrpcMessage::RateLimit(msg) => msg.write_to_bytes(), + } + } + + fn compute_size(&self) -> u32 { + match self { + GrpcMessage::Auth(msg) => msg.compute_size(), + GrpcMessage::RateLimit(msg) => msg.compute_size(), + } + } + + fn get_cached_size(&self) -> u32 { + match self { + GrpcMessage::Auth(msg) => msg.get_cached_size(), + GrpcMessage::RateLimit(msg) => msg.get_cached_size(), + } + } + + fn get_unknown_fields(&self) -> &UnknownFields { + match self { + GrpcMessage::Auth(msg) => msg.get_unknown_fields(), + GrpcMessage::RateLimit(msg) => msg.get_unknown_fields(), + } + } + + fn mut_unknown_fields(&mut self) -> &mut UnknownFields { + match self { + GrpcMessage::Auth(msg) => msg.mut_unknown_fields(), + GrpcMessage::RateLimit(msg) => msg.mut_unknown_fields(), + } + } + + fn as_any(&self) -> &dyn Any { + match self { + GrpcMessage::Auth(msg) => msg.as_any(), + GrpcMessage::RateLimit(msg) => msg.as_any(), + } + } + + fn new() -> Self + where + Self: Sized, + { + // Returning default value + GrpcMessage::default() + } + + fn default_instance() -> &'static Self + where + Self: Sized, + { + #[allow(non_upper_case_globals)] + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(|| GrpcMessage::RateLimit(RateLimitRequest::new())) + } +} + +impl GrpcMessage { + // Using domain as ce_host for the time being, we might pass a DataType in the future. + pub fn new( + extension_type: ExtensionType, + domain: String, + descriptors: protobuf::RepeatedField, + ) -> Self { + match extension_type { + ExtensionType::RateLimit => { + GrpcMessage::RateLimit(RateLimitService::message(domain.clone(), descriptors)) + } + ExtensionType::Auth => GrpcMessage::Auth(AuthService::message(domain.clone())), } } } @@ -102,7 +218,7 @@ impl GrpcServiceHandler { } pub fn send(&self, message: GrpcMessage) -> Result { - let msg = Message::write_to_bytes(message.get_message()).unwrap(); + let msg = Message::write_to_bytes(&message).unwrap(); let metadata = self .header_resolver .get() @@ -120,18 +236,8 @@ impl GrpcServiceHandler { ) } - // Using domain as ce_host for the time being, we might pass a DataType in the future. - //TODO(didierofrivia): Make it work with Message. for both Auth and RL - pub fn build_message( - &self, - domain: String, - descriptors: protobuf::RepeatedField, - ) -> GrpcMessage { - /*match self.service.extension_type { - //ExtensionType::Auth => GrpcMessage::Auth(AuthService::message(domain.clone())), - //ExtensionType::RateLimit => GrpcMessage::RateLimit(RateLimitService::message(domain.clone(), descriptors)), - }*/ - GrpcMessage::RateLimit(RateLimitService::message(domain.clone(), descriptors)) + pub fn get_extension_type(&self) -> ExtensionType { + self.service.extension_type.clone() } } From 87afee3fe3f15404550bfe2608a35156ff42c960 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Wed, 4 Sep 2024 16:37:15 +0200 Subject: [PATCH 22/66] [refactor] Inlucing Extension within Service and Operation as Rc Signed-off-by: dd di cesare --- src/configuration.rs | 17 ++------- src/operation_dispatcher.rs | 76 ++++++++++++++++++++++++++++--------- src/service.rs | 28 +++++++------- 3 files changed, 76 insertions(+), 45 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index 174b7e83..e026254d 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -486,16 +486,7 @@ impl TryFrom for FilterConfig { let services = config .extensions .into_iter() - .map(|(name, ext)| { - ( - name, - Rc::new(GrpcService::new( - ext.extension_type, - ext.endpoint, - ext.failure_mode, - )), - ) - }) + .map(|(name, ext)| (name, Rc::new(GrpcService::new(Rc::new(ext))))) .collect(); Ok(Self { @@ -505,7 +496,7 @@ impl TryFrom for FilterConfig { } } -#[derive(Deserialize, Debug, Clone, Default)] +#[derive(Deserialize, Debug, Clone, Default, PartialEq)] #[serde(rename_all = "lowercase")] pub enum FailureMode { #[default] @@ -513,7 +504,7 @@ pub enum FailureMode { Allow, } -#[derive(Deserialize, Debug, Clone, Default)] +#[derive(Deserialize, Debug, Clone, Default, PartialEq)] #[serde(rename_all = "lowercase")] pub enum ExtensionType { Auth, @@ -528,7 +519,7 @@ pub struct PluginConfiguration { pub policies: Vec, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug, Clone, Default)] #[serde(rename_all = "camelCase")] pub struct Extension { #[serde(rename = "type")] diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 0b104402..1584dc05 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -1,3 +1,4 @@ +use crate::configuration::{Extension, ExtensionType, FailureMode}; use crate::envoy::RateLimitDescriptor; use crate::policy::Policy; use crate::service::{GrpcMessage, GrpcServiceHandler}; @@ -32,15 +33,17 @@ type Procedure = (Rc, GrpcMessage); pub(crate) struct Operation { state: State, result: Result, + extension: Rc, procedure: Procedure, } #[allow(dead_code)] impl Operation { - pub fn new(procedure: Procedure) -> Self { + pub fn new(extension: Rc, procedure: Procedure) -> Self { Self { state: State::Pending, result: Err(Status::Empty), + extension, procedure, } } @@ -57,13 +60,21 @@ impl Operation { } } - fn get_state(&self) -> State { + pub fn get_state(&self) -> State { self.state.clone() } - fn get_result(&self) -> Result { + pub fn get_result(&self) -> Result { self.result } + + pub fn get_extension_type(&self) -> ExtensionType { + self.extension.extension_type.clone() + } + + pub fn get_failure_mode(&self) -> FailureMode { + self.extension.failure_mode.clone() + } } #[allow(dead_code)] @@ -101,7 +112,10 @@ impl OperationDispatcher { policy.domain.clone(), descriptors.clone(), ); - operations.push(Operation::new((service.clone(), message))) + operations.push(Operation::new( + service.get_extension(), + (Rc::clone(service), message), + )) } }); self.push_operations(operations); @@ -174,12 +188,33 @@ mod tests { } } + #[test] + fn operation_getters() { + let extension = Rc::new(Extension::default()); + let operation = Operation::new( + extension, + ( + Rc::new(build_grpc_service_handler()), + GrpcMessage::RateLimit(build_message()), + ), + ); + + assert_eq!(operation.get_state(), State::Pending); + assert_eq!(operation.get_extension_type(), ExtensionType::RateLimit); + assert_eq!(operation.get_failure_mode(), FailureMode::Deny); + assert_eq!(operation.get_result(), Result::Ok(1)); + } + #[test] fn operation_transition() { - let mut operation = Operation::new(( - Rc::new(build_grpc_service_handler()), - GrpcMessage::RateLimit(build_message()), - )); + let extension = Rc::new(Extension::default()); + let mut operation = Operation::new( + extension, + ( + Rc::new(build_grpc_service_handler()), + GrpcMessage::RateLimit(build_message()), + ), + ); assert_eq!(operation.get_state(), State::Pending); operation.trigger(); assert_eq!(operation.get_state(), State::Waiting); @@ -193,11 +228,14 @@ mod tests { let operation_dispatcher = OperationDispatcher::default(); assert_eq!(operation_dispatcher.operations.borrow().len(), 1); - - operation_dispatcher.push_operations(vec![Operation::new(( - Rc::new(build_grpc_service_handler()), - GrpcMessage::RateLimit(build_message()), - ))]); + let extension = Rc::new(Extension::default()); + operation_dispatcher.push_operations(vec![Operation::new( + extension, + ( + Rc::new(build_grpc_service_handler()), + GrpcMessage::RateLimit(build_message()), + ), + )]); assert_eq!(operation_dispatcher.operations.borrow().len(), 2); } @@ -214,10 +252,14 @@ mod tests { #[test] fn operation_dispatcher_next() { - let operation = Operation::new(( - Rc::new(build_grpc_service_handler()), - GrpcMessage::RateLimit(build_message()), - )); + let extension = Rc::new(Extension::default()); + let operation = Operation::new( + extension, + ( + Rc::new(build_grpc_service_handler()), + GrpcMessage::RateLimit(build_message()), + ), + ); let operation_dispatcher = OperationDispatcher::default(); operation_dispatcher.push_operations(vec![operation]); diff --git a/src/service.rs b/src/service.rs index a28ecfde..4bd77c08 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,7 +1,7 @@ pub(crate) mod auth; pub(crate) mod rate_limit; -use crate::configuration::{ExtensionType, FailureMode}; +use crate::configuration::{Extension, ExtensionType, FailureMode}; use crate::envoy::{CheckRequest, RateLimitDescriptor, RateLimitRequest}; use crate::service::auth::{AuthService, AUTH_METHOD_NAME, AUTH_SERVICE_NAME}; use crate::service::rate_limit::{RateLimitService, RATELIMIT_METHOD_NAME, RATELIMIT_SERVICE_NAME}; @@ -147,36 +147,30 @@ impl GrpcMessage { #[derive(Default)] pub struct GrpcService { - endpoint: String, #[allow(dead_code)] - extension_type: ExtensionType, + extension: Rc, name: &'static str, method: &'static str, - failure_mode: FailureMode, } impl GrpcService { - pub fn new(extension_type: ExtensionType, endpoint: String, failure_mode: FailureMode) -> Self { - match extension_type { + pub fn new(extension: Rc) -> Self { + match extension.extension_type { ExtensionType::Auth => Self { - endpoint, - extension_type, + extension, name: AUTH_SERVICE_NAME, method: AUTH_METHOD_NAME, - failure_mode, }, ExtensionType::RateLimit => Self { - endpoint, - extension_type, + extension, name: RATELIMIT_SERVICE_NAME, method: RATELIMIT_METHOD_NAME, - failure_mode, }, } } fn endpoint(&self) -> &str { - &self.endpoint + &self.extension.endpoint } fn name(&self) -> &str { self.name @@ -185,7 +179,7 @@ impl GrpcService { self.method } pub fn failure_mode(&self) -> &FailureMode { - &self.failure_mode + &self.extension.failure_mode } } @@ -236,8 +230,12 @@ impl GrpcServiceHandler { ) } + pub fn get_extension(&self) -> Rc { + Rc::clone(&self.service.extension) + } + pub fn get_extension_type(&self) -> ExtensionType { - self.service.extension_type.clone() + self.service.extension.extension_type.clone() } } From bec2faa7c78ed60f08a68613dbaca9bea034f5c5 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Wed, 4 Sep 2024 16:51:46 +0200 Subject: [PATCH 23/66] [refactor] OperationDispatcher.next() returns Option Signed-off-by: dd di cesare --- src/operation_dispatcher.rs | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 1584dc05..41480f91 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -30,6 +30,7 @@ impl State { type Procedure = (Rc, GrpcMessage); #[allow(dead_code)] +#[derive(Clone)] pub(crate) struct Operation { state: State, result: Result, @@ -136,16 +137,14 @@ impl OperationDispatcher { self.operations.borrow().first().unwrap().get_result() } - pub fn next(&self) -> Option<(State, Result)> { + pub fn next(&self) -> Option { let mut operations = self.operations.borrow_mut(); if let Some((i, operation)) = operations.iter_mut().enumerate().next() { if let State::Done = operation.get_state() { - let res = operation.get_result(); - operations.remove(i); - Some((State::Done, res)) + Some(operations.remove(i)) } else { operation.trigger(); - Some((operation.state.clone(), operation.result)) + Some(operation.clone()) } } else { None @@ -167,7 +166,7 @@ mod tests { _message: Option<&[u8]>, _timeout: Duration, ) -> Result { - Ok(1) + Ok(200) } fn build_grpc_service_handler() -> GrpcServiceHandler { @@ -263,23 +262,16 @@ mod tests { let operation_dispatcher = OperationDispatcher::default(); operation_dispatcher.push_operations(vec![operation]); - let mut res = operation_dispatcher.next(); - assert_eq!(res, Some((State::Waiting, Ok(200)))); - assert_eq!( - operation_dispatcher.get_current_operation_state(), - Some(State::Waiting) - ); - - res = operation_dispatcher.next(); - assert_eq!(res, Some((State::Done, Ok(200)))); - assert_eq!( - operation_dispatcher.get_current_operation_state(), - Some(State::Done) - ); - assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(200)); + if let Some(operation) = operation_dispatcher.next() { + assert_eq!(operation.get_result(), Ok(200)); + assert_eq!(operation.get_state(), State::Waiting); + } - res = operation_dispatcher.next(); - assert_eq!(res, None); + if let Some(operation) = operation_dispatcher.next() { + assert_eq!(operation.get_result(), Ok(200)); + assert_eq!(operation.get_state(), State::Done); + } + operation_dispatcher.next(); assert_eq!(operation_dispatcher.get_current_operation_state(), None); } } From ea028abb86270458b4c079c1c0e7ec456c4c1d32 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Wed, 4 Sep 2024 16:52:32 +0200 Subject: [PATCH 24/66] [refactor] Wiring up with the new API Signed-off-by: dd di cesare --- src/filter/http_context.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index 1d2a140b..2290266d 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -39,26 +39,22 @@ impl Filter { return Action::Continue; } - // Build Actions from config actions self.operation_dispatcher.build_operations(rlp, descriptors); - // populate actions in the dispatcher - // call the next on the match - if let Some(result) = self.operation_dispatcher.next() { - match result { - (_state, Ok(call_id)) => { + if let Some(operation) = self.operation_dispatcher.next() { + match operation.get_result() { + Ok(call_id) => { debug!( "#{} initiated gRPC call (id# {}) to Limitador", self.context_id, call_id ); Action::Pause } - (_state, Err(e)) => { + Err(e) => { warn!("gRPC call to Limitador failed! {e:?}"); - // TODO(didierofrivia): Get the failure_mode - /*if let FailureMode::Deny = rls.failure_mode() { + if let FailureMode::Deny = operation.get_failure_mode() { self.send_http_response(500, vec![], Some(b"Internal Server Error.\n")) - } */ + } Action::Continue } } From edc0ef966c861dd308458b1e1748e2dd91f72ffa Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Wed, 4 Sep 2024 17:47:38 +0200 Subject: [PATCH 25/66] [refactor] grpc_call function delegated to the caller Signed-off-by: dd di cesare --- src/filter/root_context.rs | 1 - src/operation_dispatcher.rs | 28 ++++++++++++++++++++++------ src/service.rs | 13 +++---------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/filter/root_context.rs b/src/filter/root_context.rs index b4fafb77..6dcd4bdc 100644 --- a/src/filter/root_context.rs +++ b/src/filter/root_context.rs @@ -49,7 +49,6 @@ impl RootContext for FilterRoot { .or_insert(Rc::from(GrpcServiceHandler::new( Rc::clone(service), Rc::new(HeaderResolver::new()), - None, ))); }); Some(Box::new(Filter { diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 41480f91..61e12b8a 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -3,10 +3,12 @@ use crate::envoy::RateLimitDescriptor; use crate::policy::Policy; use crate::service::{GrpcMessage, GrpcServiceHandler}; use protobuf::RepeatedField; +use proxy_wasm::hostcalls; use proxy_wasm::types::Status; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +use std::time::Duration; #[allow(dead_code)] #[derive(PartialEq, Debug, Clone)] @@ -27,6 +29,24 @@ impl State { } } +fn grpc_call( + upstream_name: &str, + service_name: &str, + method_name: &str, + initial_metadata: Vec<(&str, &[u8])>, + message: Option<&[u8]>, + timeout: Duration, +) -> Result { + hostcalls::dispatch_grpc_call( + upstream_name, + service_name, + method_name, + initial_metadata, + message, + timeout, + ) +} + type Procedure = (Rc, GrpcMessage); #[allow(dead_code)] @@ -56,7 +76,7 @@ impl Operation { pub fn trigger(&mut self) { if let State::Done = self.state { } else { - self.result = self.procedure.0.send(self.procedure.1.clone()); + self.result = self.procedure.0.send(grpc_call, self.procedure.1.clone()); self.state.next(); } } @@ -170,11 +190,7 @@ mod tests { } fn build_grpc_service_handler() -> GrpcServiceHandler { - GrpcServiceHandler::new( - Rc::new(Default::default()), - Rc::new(Default::default()), - Some(grpc_call), - ) + GrpcServiceHandler::new(Rc::new(Default::default()), Rc::new(Default::default())) } fn build_message() -> RateLimitRequest { diff --git a/src/service.rs b/src/service.rs index 4bd77c08..38fe2fce 100644 --- a/src/service.rs +++ b/src/service.rs @@ -11,7 +11,6 @@ use protobuf::{ Clear, CodedInputStream, CodedOutputStream, Message, ProtobufResult, UnknownFields, }; use proxy_wasm::hostcalls; -use proxy_wasm::hostcalls::dispatch_grpc_call; use proxy_wasm::types::{Bytes, MapType, Status}; use std::any::Any; use std::cell::OnceCell; @@ -195,23 +194,17 @@ type GrpcCall = fn( pub struct GrpcServiceHandler { service: Rc, header_resolver: Rc, - grpc_call: GrpcCall, } impl GrpcServiceHandler { - pub fn new( - service: Rc, - header_resolver: Rc, - grpc_call: Option, - ) -> Self { + pub fn new(service: Rc, header_resolver: Rc) -> Self { Self { service, header_resolver, - grpc_call: grpc_call.unwrap_or(dispatch_grpc_call), } } - pub fn send(&self, message: GrpcMessage) -> Result { + pub fn send(&self, grpc_call: GrpcCall, message: GrpcMessage) -> Result { let msg = Message::write_to_bytes(&message).unwrap(); let metadata = self .header_resolver @@ -220,7 +213,7 @@ impl GrpcServiceHandler { .map(|(header, value)| (*header, value.as_slice())) .collect(); - (self.grpc_call)( + grpc_call( self.service.endpoint(), self.service.name(), self.service.method(), From 502d62bd6f506e8f43ebc6f9b26944928178b30f Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Wed, 4 Sep 2024 19:05:17 +0200 Subject: [PATCH 26/66] [refactor] Operation responsible of providing hostcalls fns * Easier to test, mocking fn * Assigned fn on creation, default hostcall and mock on tests Signed-off-by: dd di cesare --- src/operation_dispatcher.rs | 120 ++++++++++++++++++------------------ src/service.rs | 18 ++++-- 2 files changed, 71 insertions(+), 67 deletions(-) diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 61e12b8a..1d855a8f 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -1,10 +1,10 @@ use crate::configuration::{Extension, ExtensionType, FailureMode}; use crate::envoy::RateLimitDescriptor; use crate::policy::Policy; -use crate::service::{GrpcMessage, GrpcServiceHandler}; +use crate::service::{GetMapValuesBytes, GrpcCall, GrpcMessage, GrpcServiceHandler}; use protobuf::RepeatedField; use proxy_wasm::hostcalls; -use proxy_wasm::types::Status; +use proxy_wasm::types::{Bytes, MapType, Status}; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; @@ -29,24 +29,6 @@ impl State { } } -fn grpc_call( - upstream_name: &str, - service_name: &str, - method_name: &str, - initial_metadata: Vec<(&str, &[u8])>, - message: Option<&[u8]>, - timeout: Duration, -) -> Result { - hostcalls::dispatch_grpc_call( - upstream_name, - service_name, - method_name, - initial_metadata, - message, - timeout, - ) -} - type Procedure = (Rc, GrpcMessage); #[allow(dead_code)] @@ -56,6 +38,8 @@ pub(crate) struct Operation { result: Result, extension: Rc, procedure: Procedure, + grpc_call: GrpcCall, + get_map_values_bytes: GetMapValuesBytes, } #[allow(dead_code)] @@ -66,17 +50,19 @@ impl Operation { result: Err(Status::Empty), extension, procedure, + grpc_call, + get_map_values_bytes, } } - pub fn set_action(&mut self, procedure: Procedure) { - self.procedure = procedure; - } - - pub fn trigger(&mut self) { + fn trigger(&mut self) { if let State::Done = self.state { } else { - self.result = self.procedure.0.send(grpc_call, self.procedure.1.clone()); + self.result = self.procedure.0.send( + self.get_map_values_bytes, + self.grpc_call, + self.procedure.1.clone(), + ); self.state.next(); } } @@ -172,6 +158,28 @@ impl OperationDispatcher { } } +fn grpc_call( + upstream_name: &str, + service_name: &str, + method_name: &str, + initial_metadata: Vec<(&str, &[u8])>, + message: Option<&[u8]>, + timeout: Duration, +) -> Result { + hostcalls::dispatch_grpc_call( + upstream_name, + service_name, + method_name, + initial_metadata, + message, + timeout, + ) +} + +fn get_map_values_bytes(map_type: MapType, key: &str) -> Result, Status> { + hostcalls::get_map_value_bytes(map_type, key) +} + #[cfg(test)] mod tests { use super::*; @@ -189,6 +197,10 @@ mod tests { Ok(200) } + fn get_map_values_bytes(_map_type: MapType, _key: &str) -> Result, Status> { + Ok(Some(Vec::new())) + } + fn build_grpc_service_handler() -> GrpcServiceHandler { GrpcServiceHandler::new(Rc::new(Default::default()), Rc::new(Default::default())) } @@ -203,33 +215,33 @@ mod tests { } } - #[test] - fn operation_getters() { - let extension = Rc::new(Extension::default()); - let operation = Operation::new( - extension, - ( + fn build_operation() -> Operation { + Operation { + state: State::Pending, + result: Ok(200), + extension: Rc::new(Extension::default()), + procedure: ( Rc::new(build_grpc_service_handler()), GrpcMessage::RateLimit(build_message()), ), - ); + grpc_call, + get_map_values_bytes, + } + } + + #[test] + fn operation_getters() { + let operation = build_operation(); assert_eq!(operation.get_state(), State::Pending); assert_eq!(operation.get_extension_type(), ExtensionType::RateLimit); assert_eq!(operation.get_failure_mode(), FailureMode::Deny); - assert_eq!(operation.get_result(), Result::Ok(1)); + assert_eq!(operation.get_result(), Ok(200)); } #[test] fn operation_transition() { - let extension = Rc::new(Extension::default()); - let mut operation = Operation::new( - extension, - ( - Rc::new(build_grpc_service_handler()), - GrpcMessage::RateLimit(build_message()), - ), - ); + let mut operation = build_operation(); assert_eq!(operation.get_state(), State::Pending); operation.trigger(); assert_eq!(operation.get_state(), State::Waiting); @@ -242,23 +254,16 @@ mod tests { fn operation_dispatcher_push_actions() { let operation_dispatcher = OperationDispatcher::default(); - assert_eq!(operation_dispatcher.operations.borrow().len(), 1); - let extension = Rc::new(Extension::default()); - operation_dispatcher.push_operations(vec![Operation::new( - extension, - ( - Rc::new(build_grpc_service_handler()), - GrpcMessage::RateLimit(build_message()), - ), - )]); + assert_eq!(operation_dispatcher.operations.borrow().len(), 0); + operation_dispatcher.push_operations(vec![build_operation()]); - assert_eq!(operation_dispatcher.operations.borrow().len(), 2); + assert_eq!(operation_dispatcher.operations.borrow().len(), 1); } #[test] fn operation_dispatcher_get_current_action_state() { let operation_dispatcher = OperationDispatcher::default(); - + operation_dispatcher.push_operations(vec![build_operation()]); assert_eq!( operation_dispatcher.get_current_operation_state(), Some(State::Pending) @@ -267,14 +272,7 @@ mod tests { #[test] fn operation_dispatcher_next() { - let extension = Rc::new(Extension::default()); - let operation = Operation::new( - extension, - ( - Rc::new(build_grpc_service_handler()), - GrpcMessage::RateLimit(build_message()), - ), - ); + let operation = build_operation(); let operation_dispatcher = OperationDispatcher::default(); operation_dispatcher.push_operations(vec![operation]); diff --git a/src/service.rs b/src/service.rs index 38fe2fce..aec2aff0 100644 --- a/src/service.rs +++ b/src/service.rs @@ -10,7 +10,6 @@ use protobuf::reflect::MessageDescriptor; use protobuf::{ Clear, CodedInputStream, CodedOutputStream, Message, ProtobufResult, UnknownFields, }; -use proxy_wasm::hostcalls; use proxy_wasm::types::{Bytes, MapType, Status}; use std::any::Any; use std::cell::OnceCell; @@ -182,7 +181,7 @@ impl GrpcService { } } -type GrpcCall = fn( +pub type GrpcCall = fn( upstream_name: &str, service_name: &str, method_name: &str, @@ -191,6 +190,8 @@ type GrpcCall = fn( timeout: Duration, ) -> Result; +pub type GetMapValuesBytes = fn(map_type: MapType, key: &str) -> Result, Status>; + pub struct GrpcServiceHandler { service: Rc, header_resolver: Rc, @@ -204,11 +205,16 @@ impl GrpcServiceHandler { } } - pub fn send(&self, grpc_call: GrpcCall, message: GrpcMessage) -> Result { + pub fn send( + &self, + get_map_values_bytes: GetMapValuesBytes, + grpc_call: GrpcCall, + message: GrpcMessage, + ) -> Result { let msg = Message::write_to_bytes(&message).unwrap(); let metadata = self .header_resolver - .get() + .get(get_map_values_bytes) .iter() .map(|(header, value)| (*header, value.as_slice())) .collect(); @@ -249,12 +255,12 @@ impl HeaderResolver { } } - pub fn get(&self) -> &Vec<(&'static str, Bytes)> { + pub fn get(&self, get_map_values_bytes: GetMapValuesBytes) -> &Vec<(&'static str, Bytes)> { self.headers.get_or_init(|| { let mut headers = Vec::new(); for header in TracingHeader::all() { if let Ok(Some(value)) = - hostcalls::get_map_value_bytes(MapType::HttpRequestHeaders, (*header).as_str()) + get_map_values_bytes(MapType::HttpRequestHeaders, (*header).as_str()) { headers.push(((*header).as_str(), value)); } From 312710e84937549ec86c4bd9cc2a97d44c43e8e5 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Fri, 6 Sep 2024 11:29:10 +0200 Subject: [PATCH 27/66] [refactor] Fix `OperationDispatcher.next()` behaviour * Bonus: Addressed review regarding testing and Fn types Signed-off-by: dd di cesare --- src/operation_dispatcher.rs | 77 +++++++++++++++++++++++-------------- src/service.rs | 16 ++++---- 2 files changed, 57 insertions(+), 36 deletions(-) diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 1d855a8f..a9ab3c18 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -1,7 +1,7 @@ use crate::configuration::{Extension, ExtensionType, FailureMode}; use crate::envoy::RateLimitDescriptor; use crate::policy::Policy; -use crate::service::{GetMapValuesBytes, GrpcCall, GrpcMessage, GrpcServiceHandler}; +use crate::service::{GetMapValuesBytesFn, GrpcCallFn, GrpcMessage, GrpcServiceHandler}; use protobuf::RepeatedField; use proxy_wasm::hostcalls; use proxy_wasm::types::{Bytes, MapType, Status}; @@ -38,8 +38,8 @@ pub(crate) struct Operation { result: Result, extension: Rc, procedure: Procedure, - grpc_call: GrpcCall, - get_map_values_bytes: GetMapValuesBytes, + grpc_call_fn: GrpcCallFn, + get_map_values_bytes_fn: GetMapValuesBytesFn, } #[allow(dead_code)] @@ -50,8 +50,8 @@ impl Operation { result: Err(Status::Empty), extension, procedure, - grpc_call, - get_map_values_bytes, + grpc_call_fn, + get_map_values_bytes_fn, } } @@ -59,8 +59,8 @@ impl Operation { if let State::Done = self.state { } else { self.result = self.procedure.0.send( - self.get_map_values_bytes, - self.grpc_call, + self.get_map_values_bytes_fn, + self.grpc_call_fn, self.procedure.1.clone(), ); self.state.next(); @@ -147,7 +147,8 @@ impl OperationDispatcher { let mut operations = self.operations.borrow_mut(); if let Some((i, operation)) = operations.iter_mut().enumerate().next() { if let State::Done = operation.get_state() { - Some(operations.remove(i)) + operations.remove(i); + operations.get(i).cloned() // The next op is now at `i` } else { operation.trigger(); Some(operation.clone()) @@ -158,7 +159,7 @@ impl OperationDispatcher { } } -fn grpc_call( +fn grpc_call_fn( upstream_name: &str, service_name: &str, method_name: &str, @@ -176,7 +177,7 @@ fn grpc_call( ) } -fn get_map_values_bytes(map_type: MapType, key: &str) -> Result, Status> { +fn get_map_values_bytes_fn(map_type: MapType, key: &str) -> Result, Status> { hostcalls::get_map_value_bytes(map_type, key) } @@ -186,7 +187,7 @@ mod tests { use crate::envoy::RateLimitRequest; use std::time::Duration; - fn grpc_call( + fn grpc_call_fn_stub( _upstream_name: &str, _service_name: &str, _method_name: &str, @@ -197,7 +198,10 @@ mod tests { Ok(200) } - fn get_map_values_bytes(_map_type: MapType, _key: &str) -> Result, Status> { + fn get_map_values_bytes_fn_stub( + _map_type: MapType, + _key: &str, + ) -> Result, Status> { Ok(Some(Vec::new())) } @@ -218,14 +222,14 @@ mod tests { fn build_operation() -> Operation { Operation { state: State::Pending, - result: Ok(200), + result: Ok(1), extension: Rc::new(Extension::default()), procedure: ( Rc::new(build_grpc_service_handler()), GrpcMessage::RateLimit(build_message()), ), - grpc_call, - get_map_values_bytes, + grpc_call_fn: grpc_call_fn_stub, + get_map_values_bytes_fn: get_map_values_bytes_fn_stub, } } @@ -236,7 +240,7 @@ mod tests { assert_eq!(operation.get_state(), State::Pending); assert_eq!(operation.get_extension_type(), ExtensionType::RateLimit); assert_eq!(operation.get_failure_mode(), FailureMode::Deny); - assert_eq!(operation.get_result(), Ok(200)); + assert_eq!(operation.get_result(), Ok(1)); } #[test] @@ -272,20 +276,37 @@ mod tests { #[test] fn operation_dispatcher_next() { - let operation = build_operation(); let operation_dispatcher = OperationDispatcher::default(); - operation_dispatcher.push_operations(vec![operation]); + operation_dispatcher.push_operations(vec![build_operation(), build_operation()]); - if let Some(operation) = operation_dispatcher.next() { - assert_eq!(operation.get_result(), Ok(200)); - assert_eq!(operation.get_state(), State::Waiting); - } + assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(1)); + assert_eq!( + operation_dispatcher.get_current_operation_state(), + Some(State::Pending) + ); - if let Some(operation) = operation_dispatcher.next() { - assert_eq!(operation.get_result(), Ok(200)); - assert_eq!(operation.get_state(), State::Done); - } - operation_dispatcher.next(); - assert_eq!(operation_dispatcher.get_current_operation_state(), None); + let mut op = operation_dispatcher.next(); + assert_eq!(op.clone().unwrap().get_result(), Ok(200)); + assert_eq!(op.unwrap().get_state(), State::Waiting); + + op = operation_dispatcher.next(); + assert_eq!(op.clone().unwrap().get_result(), Ok(200)); + assert_eq!(op.unwrap().get_state(), State::Done); + + op = operation_dispatcher.next(); + assert_eq!(op.clone().unwrap().get_result(), Ok(1)); + assert_eq!(op.unwrap().get_state(), State::Pending); + + op = operation_dispatcher.next(); + assert_eq!(op.clone().unwrap().get_result(), Ok(200)); + assert_eq!(op.unwrap().get_state(), State::Waiting); + + op = operation_dispatcher.next(); + assert_eq!(op.clone().unwrap().get_result(), Ok(200)); + assert_eq!(op.unwrap().get_state(), State::Done); + + op = operation_dispatcher.next(); + assert!(op.is_none()); + assert!(operation_dispatcher.get_current_operation_state().is_none()); } } diff --git a/src/service.rs b/src/service.rs index aec2aff0..e89077f2 100644 --- a/src/service.rs +++ b/src/service.rs @@ -181,7 +181,7 @@ impl GrpcService { } } -pub type GrpcCall = fn( +pub type GrpcCallFn = fn( upstream_name: &str, service_name: &str, method_name: &str, @@ -190,7 +190,7 @@ pub type GrpcCall = fn( timeout: Duration, ) -> Result; -pub type GetMapValuesBytes = fn(map_type: MapType, key: &str) -> Result, Status>; +pub type GetMapValuesBytesFn = fn(map_type: MapType, key: &str) -> Result, Status>; pub struct GrpcServiceHandler { service: Rc, @@ -207,19 +207,19 @@ impl GrpcServiceHandler { pub fn send( &self, - get_map_values_bytes: GetMapValuesBytes, - grpc_call: GrpcCall, + get_map_values_bytes_fn: GetMapValuesBytesFn, + grpc_call_fn: GrpcCallFn, message: GrpcMessage, ) -> Result { let msg = Message::write_to_bytes(&message).unwrap(); let metadata = self .header_resolver - .get(get_map_values_bytes) + .get(get_map_values_bytes_fn) .iter() .map(|(header, value)| (*header, value.as_slice())) .collect(); - grpc_call( + grpc_call_fn( self.service.endpoint(), self.service.name(), self.service.method(), @@ -255,12 +255,12 @@ impl HeaderResolver { } } - pub fn get(&self, get_map_values_bytes: GetMapValuesBytes) -> &Vec<(&'static str, Bytes)> { + pub fn get(&self, get_map_values_bytes_fn: GetMapValuesBytesFn) -> &Vec<(&'static str, Bytes)> { self.headers.get_or_init(|| { let mut headers = Vec::new(); for header in TracingHeader::all() { if let Ok(Some(value)) = - get_map_values_bytes(MapType::HttpRequestHeaders, (*header).as_str()) + get_map_values_bytes_fn(MapType::HttpRequestHeaders, (*header).as_str()) { headers.push(((*header).as_str(), value)); } From 9293e03b1e3ebc02d258eee81089b55ab4363c37 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 1 Aug 2024 14:23:52 +0100 Subject: [PATCH 28/66] Local auth dev environment Signed-off-by: Adam Cattermole --- Makefile | 18 +++ make/auth.mk | 101 +++++++++++++++ utils/deploy/envoy.yaml | 279 ++++++++++++++++++++++++++++++++++++++++ utils/kind/cluster.yaml | 9 ++ 4 files changed, 407 insertions(+) create mode 100644 make/auth.mk create mode 100644 utils/deploy/envoy.yaml create mode 100644 utils/kind/cluster.yaml diff --git a/Makefile b/Makefile index 5362a265..e7944622 100644 --- a/Makefile +++ b/Makefile @@ -79,3 +79,21 @@ chmod a+x $(1)/bin/protoc ;\ rm -rf $$TMP_DIR ;\ } endef + +##@ Util + +# go-install-tool will 'go install' any package $2 and install it to $1. +PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) +define go-install-tool +@[ -f $(1) ] || { \ +set -e ;\ +TMP_DIR=$$(mktemp -d) ;\ +cd $$TMP_DIR ;\ +go mod init tmp ;\ +echo "Downloading $(2)" ;\ +GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ +rm -rf $$TMP_DIR ;\ +} +endef + +include ./make/*.mk diff --git a/make/auth.mk b/make/auth.mk new file mode 100644 index 00000000..478c214c --- /dev/null +++ b/make/auth.mk @@ -0,0 +1,101 @@ +##@ Kind + +.PHONY: kind kind-create-cluster kind-delete-cluster + +KIND = $(PROJECT_PATH)/bin/kind +KIND_VERSION = v0.23.0 +$(KIND): + $(call go-install-tool,$(KIND),sigs.k8s.io/kind@$(KIND_VERSION)) + +kind: $(KIND) ## Download kind locally if necessary. + +KIND_CLUSTER_NAME ?= wasm-auth-local + +kind-create-cluster: BUILD?=debug +kind-create-cluster: WASM_PATH=$(subst /,\/,$(PROJECT_PATH)/target/wasm32-unknown-unknown/$(BUILD)) +kind-create-cluster: kind ## Create the "wasm-auth-local" kind cluster. + @{ \ + TEMP_FILE=/tmp/kind-cluster-$$(openssl rand -hex 4).yaml ;\ + cp utils/kind/cluster.yaml $$TEMP_FILE ;\ + $(SED) -i "s/\$$(WASM_PATH)/$(WASM_PATH)/g" $$TEMP_FILE ;\ + KIND_EXPERIMENTAL_PROVIDER=$(CONTAINER_ENGINE) $(KIND) create cluster --name $(KIND_CLUSTER_NAME) --config $$TEMP_FILE ;\ + rm -rf $$TEMP_FILE ;\ + } + +kind-delete-cluster: ## Delete the "wasm-auth-local" kind cluster. + - KIND_EXPERIMENTAL_PROVIDER=$(CONTAINER_ENGINE) $(KIND) delete cluster --name $(KIND_CLUSTER_NAME) + + +##@ Authorino + +.PHONY: install-authorino-operator certs deploy-authorino + +AUTHORINO_IMAGE ?= quay.io/kuadrant/authorino:latest +AUTHORINO_OPERATOR_NAMESPACE ?= authorino-operator +install-authorino-operator: ## Installs Authorino Operator and dependencies into the Kubernetes cluster configured in ~/.kube/config + curl -sL https://raw.githubusercontent.com/Kuadrant/authorino-operator/main/utils/install.sh | bash -s -- --git-ref main + kubectl patch deployment/authorino-webhooks -n $(AUTHORINO_OPERATOR_NAMESPACE) -p '{"spec":{"template":{"spec":{"containers":[{"name":"webhooks","image":"$(AUTHORINO_IMAGE)","imagePullPolicy":"IfNotPresent"}]}}}}' + kubectl -n $(AUTHORINO_OPERATOR_NAMESPACE) wait --timeout=300s --for=condition=Available deployments --all + +TLS_ENABLED ?= true +AUTHORINO_INSTANCE ?= authorino +NAMESPACE ?= default +certs: sed ## Requests TLS certificates for the Authorino instance if TLS is enabled, cert-manager.io is installed, and the secret is not already present +ifeq (true,$(TLS_ENABLED)) +ifeq (,$(shell kubectl -n $(NAMESPACE) get secret/authorino-oidc-server-cert 2>/dev/null)) + curl -sl https://raw.githubusercontent.com/kuadrant/authorino/main/deploy/certs.yaml | $(SED) "s/\$$(AUTHORINO_INSTANCE)/$(AUTHORINO_INSTANCE)/g;s/\$$(NAMESPACE)/$(NAMESPACE)/g" | kubectl -n $(NAMESPACE) apply -f - +else + echo "tls cert secret found." +endif +else + echo "tls disabled." +endif + +deploy-authorino: certs sed ## Deploys an instance of Authorino into the Kubernetes cluster configured in ~/.kube/config + @{ \ + set -e ;\ + TEMP_FILE=/tmp/authorino-deploy-$$(openssl rand -hex 4).yaml ;\ + curl -sl https://raw.githubusercontent.com/kuadrant/authorino/main/deploy/authorino.yaml > $$TEMP_FILE ;\ + $(SED) -i "s/\$$(AUTHORINO_INSTANCE)/$(AUTHORINO_INSTANCE)/g;s/\$$(TLS_ENABLED)/$(TLS_ENABLED)/g" $$TEMP_FILE ;\ + kubectl -n $(NAMESPACE) apply -f $$TEMP_FILE ;\ + kubectl patch -n $(NAMESPACE) authorino/$(AUTHORINO_INSTANCE) --type='merge' -p '{"spec":{"image": "$(AUTHORINO_IMAGE)"}}' ;\ + rm -rf $$TEMP_FILE ;\ + } + + +##@ User Apps + +.PHONY: user-apps + +user-apps: ## Deploys talker API and envoy + kubectl -n $(NAMESPACE) apply -f https://raw.githubusercontent.com/kuadrant/authorino-examples/main/talker-api/talker-api-deploy.yaml + kubectl -n $(NAMESPACE) apply -f utils/deploy/envoy.yaml + + +##@ Util + +.PHONY: local-setup local-env-setup local-cleanup sed + +local-setup: local-env-setup + kubectl -n $(NAMESPACE) wait --timeout=300s --for=condition=Available deployments --all + +local-env-setup: + $(MAKE) kind-delete-cluster + $(MAKE) kind-create-cluster + $(MAKE) install-authorino-operator + $(MAKE) deploy-authorino + $(MAKE) user-apps + +local-cleanup: kind ## Delete the "wasm-auth-local" kind cluster. + $(MAKE) kind-delete-cluster + +ifeq ($(shell uname),Darwin) +SED=$(shell which gsed) +else +SED=$(shell which sed) +endif +sed: ## Checks if GNU sed is installed +ifeq ($(SED),) + @echo "Cannot find GNU sed installed." + exit 1 +endif diff --git a/utils/deploy/envoy.yaml b/utils/deploy/envoy.yaml new file mode 100644 index 00000000..fd5f3b20 --- /dev/null +++ b/utils/deploy/envoy.yaml @@ -0,0 +1,279 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app: envoy + name: envoy +data: + envoy.yaml: | + static_resources: + clusters: + - name: authorino + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + http2_protocol_options: { } + load_assignment: + cluster_name: authorino + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: authorino-authorino-authorization + port_value: 50051 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + validation_context: + trusted_ca: + filename: /etc/ssl/certs/authorino-ca-cert.crt + - name: limitador + connect_timeout: 1s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + http2_protocol_options: { } + load_assignment: + cluster_name: limitador + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: limitador + port_value: 8081 + - name: talker-api + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: talker-api + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: talker-api + port_value: 3000 + - name: talker-web + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: talker-web + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: talker-web + port_value: 8888 + - name: opentelemetry + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + http2_protocol_options: { } + load_assignment: + cluster_name: opentelemetry + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: otel-collector + port_value: 4317 + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 8000 + filter_chains: + - filters: + - name: envoy.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: local + use_remote_address: true + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: [ '*' ] + routes: + - match: { prefix: /web } + route: + cluster: talker-web + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + disabled: true + - match: { prefix: / } + route: + cluster: talker-api + rate_limits: + - actions: + - metadata: + metadata_key: + key: "envoy.filters.http.ext_authz" + path: + - key: ext_auth_data + - key: username + descriptor_key: user_id + http_filters: + - name: envoy.filters.http.header_to_metadata + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_to_metadata.v3.Config + request_rules: + - header: x-dyn-user-id + on_header_present: + key: user_id + type: STRING + remove: false + - name: envoy.filters.http.wasm + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm + config: + name: kuadrant_wasm + root_id: kuadrant_wasm + vm_config: + vm_id: vm.sentinel.kuadrant_wasm + runtime: envoy.wasm.runtime.v8 + code: + local: + filename: /opt/kuadrant/wasm/wasm_shim.wasm + allow_precompiled: true + configuration: + "@type": "type.googleapis.com/google.protobuf.StringValue" + value: > + { + "failureMode": "deny", + "rateLimitPolicies": [ + { + "name": "rlp-ns-A/rlp-name-A", + "domain": "rlp-ns-A/rlp-name-A", + "service": "authorino", + "hostnames": ["*.a.com"], + "rules": [ + { + "data": [ + { + "selector": { + "selector": "unknown.path" + } + } + ] + } + ] + } + ] + } + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # # Uncomment to enable tracing + # tracing: + # provider: + # name: envoy.tracers.opentelemetry + # typed_config: + # "@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig + # grpc_service: + # envoy_grpc: + # cluster_name: opentelemetry + # timeout: 1s + # service_name: envoy + admin: + access_log_path: "/tmp/admin_access.log" + address: + socket_address: + address: 0.0.0.0 + port_value: 8001 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: envoy + name: envoy +spec: + replicas: 1 + selector: + matchLabels: + app: envoy + template: + metadata: + labels: + app: envoy + spec: + containers: + - args: + - --config-path /usr/local/etc/envoy/envoy.yaml + - --service-cluster front-proxy + - --log-level info + - --component-log-level filter:trace,http:debug,router:debug + command: + - /usr/local/bin/envoy + image: envoyproxy/envoy:v1.25-latest + name: envoy + ports: + - containerPort: 8000 + name: web + - containerPort: 8001 + name: admin + volumeMounts: + - mountPath: /usr/local/etc/envoy + name: config + readOnly: true + - mountPath: /etc/ssl/certs/authorino-ca-cert.crt + name: authorino-ca-cert + readOnly: true + subPath: ca.crt + - mountPath: /opt/kuadrant/wasm + name: wasm + volumes: + - configMap: + items: + - key: envoy.yaml + path: envoy.yaml + name: envoy + name: config + - name: authorino-ca-cert + secret: + defaultMode: 420 + secretName: authorino-ca-cert + - name: wasm + hostPath: + path: /opt/kuadrant/wasm +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: envoy + name: envoy +spec: + ports: + - name: web + port: 8000 + protocol: TCP + selector: + app: envoy +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-wildcard-host +spec: + rules: + - host: talker-api.127.0.0.1.nip.io + http: + paths: + - backend: + service: + name: envoy + port: + number: 8000 + path: / + pathType: Prefix diff --git a/utils/kind/cluster.yaml b/utils/kind/cluster.yaml new file mode 100644 index 00000000..9ec18f83 --- /dev/null +++ b/utils/kind/cluster.yaml @@ -0,0 +1,9 @@ +--- +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: + - role: control-plane + image: kindest/node:v1.30.0 + extraMounts: + - hostPath: $(WASM_PATH) + containerPath: /opt/kuadrant/wasm From ee3997d3696272af3414f899cb8b652c241b74e4 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 8 Aug 2024 14:34:35 +0100 Subject: [PATCH 29/66] Deploy limitador as part of the kind cluster Signed-off-by: Adam Cattermole --- make/auth.mk | 24 ++++++++++++++-- utils/deploy/limitador.yaml | 57 +++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 utils/deploy/limitador.yaml diff --git a/make/auth.mk b/make/auth.mk index 478c214c..a92cb7df 100644 --- a/make/auth.mk +++ b/make/auth.mk @@ -16,7 +16,7 @@ kind-create-cluster: WASM_PATH=$(subst /,\/,$(PROJECT_PATH)/target/wasm32-unknow kind-create-cluster: kind ## Create the "wasm-auth-local" kind cluster. @{ \ TEMP_FILE=/tmp/kind-cluster-$$(openssl rand -hex 4).yaml ;\ - cp utils/kind/cluster.yaml $$TEMP_FILE ;\ + cp $(PROJECT_PATH)/utils/kind/cluster.yaml $$TEMP_FILE ;\ $(SED) -i "s/\$$(WASM_PATH)/$(WASM_PATH)/g" $$TEMP_FILE ;\ KIND_EXPERIMENTAL_PROVIDER=$(CONTAINER_ENGINE) $(KIND) create cluster --name $(KIND_CLUSTER_NAME) --config $$TEMP_FILE ;\ rm -rf $$TEMP_FILE ;\ @@ -63,32 +63,50 @@ deploy-authorino: certs sed ## Deploys an instance of Authorino into the Kuberne } +##@ Limitador + +deploy-limitador: + kubectl create configmap limits --from-file=$(PROJECT_PATH)/utils/docker-compose/limits.yaml + kubectl -n $(NAMESPACE) apply -f $(PROJECT_PATH)/utils/deploy/limitador.yaml + + ##@ User Apps .PHONY: user-apps user-apps: ## Deploys talker API and envoy kubectl -n $(NAMESPACE) apply -f https://raw.githubusercontent.com/kuadrant/authorino-examples/main/talker-api/talker-api-deploy.yaml - kubectl -n $(NAMESPACE) apply -f utils/deploy/envoy.yaml + kubectl -n $(NAMESPACE) apply -f $(PROJECT_PATH)/utils/deploy/envoy-tls.yaml ##@ Util -.PHONY: local-setup local-env-setup local-cleanup sed +.PHONY: local-setup local-env-setup local-cleanup local-rollout sed local-setup: local-env-setup kubectl -n $(NAMESPACE) wait --timeout=300s --for=condition=Available deployments --all + @{ \ + echo "Now you can export the envoy service by doing:"; \ + echo "kubectl port-forward --namespace $(NAMESPACE) deployment/envoy 8000:8000"; \ + echo "After that, you can curl -H \"Host: myhost.com\" localhost:8000"; \ + } local-env-setup: $(MAKE) kind-delete-cluster $(MAKE) kind-create-cluster $(MAKE) install-authorino-operator $(MAKE) deploy-authorino + $(MAKE) deploy-limitador $(MAKE) user-apps local-cleanup: kind ## Delete the "wasm-auth-local" kind cluster. $(MAKE) kind-delete-cluster +local-rollout: + $(MAKE) user-apps + kubectl rollout restart -n $(NAMESPACE) deployment/envoy + kubectl -n $(NAMESPACE) wait --timeout=300s --for=condition=Available deployments --all + ifeq ($(shell uname),Darwin) SED=$(shell which gsed) else diff --git a/utils/deploy/limitador.yaml b/utils/deploy/limitador.yaml new file mode 100644 index 00000000..51f231e9 --- /dev/null +++ b/utils/deploy/limitador.yaml @@ -0,0 +1,57 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: limitador + name: limitador +spec: + replicas: 1 + selector: + matchLabels: + app: limitador + template: + metadata: + labels: + app: limitador + spec: + containers: + - args: + - -vvv + - /opt/kuadrant/limits/limits.yaml + command: + - limitador-server + image: quay.io/kuadrant/limitador:latest + name: limitador + ports: + - containerPort: 8080 + name: http + - containerPort: 8081 + name: grpc + volumeMounts: + - mountPath: /opt/kuadrant/limits + name: limits + volumes: + - configMap: + items: + - key: limits.yaml + path: limits.yaml + name: limits + name: limits +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: limitador + name: limitador +spec: + ports: + - name: http + port: 8080 + protocol: TCP + - name: grpc + port: 8081 + protocol: TCP + selector: + app: limitador From 5f0ff7c94a639a3d78a6b35575f3acc7d97c04c4 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 8 Aug 2024 14:39:35 +0100 Subject: [PATCH 30/66] Add tls and notls envoy configuration files Signed-off-by: Adam Cattermole --- make/auth.mk | 8 +- utils/deploy/envoy-notls.yaml | 269 ++++++++++++++++++++ utils/deploy/{envoy.yaml => envoy-tls.yaml} | 65 ++--- 3 files changed, 312 insertions(+), 30 deletions(-) create mode 100644 utils/deploy/envoy-notls.yaml rename utils/deploy/{envoy.yaml => envoy-tls.yaml} (80%) diff --git a/make/auth.mk b/make/auth.mk index a92cb7df..e5d57cc2 100644 --- a/make/auth.mk +++ b/make/auth.mk @@ -74,9 +74,15 @@ deploy-limitador: .PHONY: user-apps + +ifeq (true,$(TLS_ENABLED)) +ENVOY_OVERLAY = tls +else +ENVOY_OVERLAY = notls +endif user-apps: ## Deploys talker API and envoy kubectl -n $(NAMESPACE) apply -f https://raw.githubusercontent.com/kuadrant/authorino-examples/main/talker-api/talker-api-deploy.yaml - kubectl -n $(NAMESPACE) apply -f $(PROJECT_PATH)/utils/deploy/envoy-tls.yaml + kubectl -n $(NAMESPACE) apply -f $(PROJECT_PATH)/utils/deploy/envoy-$(ENVOY_OVERLAY).yaml ##@ Util diff --git a/utils/deploy/envoy-notls.yaml b/utils/deploy/envoy-notls.yaml new file mode 100644 index 00000000..263d3652 --- /dev/null +++ b/utils/deploy/envoy-notls.yaml @@ -0,0 +1,269 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app: envoy + name: envoy +data: + envoy.yaml: | + static_resources: + clusters: + - name: authorino_wasm + connect_timeout: 1s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: { } + load_assignment: + cluster_name: authorino_wasm + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: authorino-authorino-authorization + port_value: 50051 + - name: limitador + connect_timeout: 1s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: { } + load_assignment: + cluster_name: limitador + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: limitador + port_value: 8081 + - name: talker-api + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: talker-api + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: talker-api + port_value: 3000 + - name: talker-web + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: talker-web + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: talker-web + port_value: 8888 + - name: opentelemetry + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: { } + load_assignment: + cluster_name: opentelemetry + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: otel-collector + port_value: 4317 + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 8000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: local + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: [ '*' ] + routes: + - match: { prefix: / } + route: + cluster: talker-api + http_filters: + - name: envoy.filters.http.header_to_metadata + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_to_metadata.v3.Config + request_rules: + - header: x-dyn-user-id + on_header_present: + key: user_id + type: STRING + remove: false + - name: envoy.filters.http.wasm + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm + config: + name: kuadrant_wasm + root_id: kuadrant_wasm + vm_config: + vm_id: vm.sentinel.kuadrant_wasm + runtime: envoy.wasm.runtime.v8 + code: + local: + filename: /opt/kuadrant/wasm/wasm_shim.wasm + allow_precompiled: true + configuration: + "@type": "type.googleapis.com/google.protobuf.StringValue" + value: > + { + "failureMode": "deny", + "rateLimitPolicies": [ + { + "name": "rlp-ns-A/rlp-name-A", + "domain": "rlp-ns-A/rlp-name-A", + "service": "authorino_wasm", + "hostnames": ["*.a.com"], + "rules": [ + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.host", + "operator": "eq", + "value": "test.a.com" + } + ] + } + ], + "data": [ + { + "static": { + "key": "limit_to_be_activated", + "value": "1" + } + } + ] + } + ] + } + ] + } + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + # # Uncomment to enable tracing + # tracing: + # provider: + # name: envoy.tracers.opentelemetry + # typed_config: + # "@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig + # grpc_service: + # envoy_grpc: + # cluster_name: opentelemetry + # timeout: 1s + # service_name: envoy + admin: + address: + socket_address: + address: 0.0.0.0 + port_value: 8001 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: envoy + name: envoy +spec: + replicas: 1 + selector: + matchLabels: + app: envoy + template: + metadata: + labels: + app: envoy + spec: + containers: + - args: + - --config-path /usr/local/etc/envoy/envoy.yaml + - --service-cluster front-proxy + - --log-level info + - --component-log-level wasm:debug,filter:trace,http:debug,router:debug + command: + - /usr/local/bin/envoy + image: envoyproxy/envoy:v1.31-latest + name: envoy + ports: + - containerPort: 8000 + name: web + - containerPort: 8001 + name: admin + volumeMounts: + - mountPath: /usr/local/etc/envoy + name: config + readOnly: true + - mountPath: /opt/kuadrant/wasm + name: wasm + volumes: + - configMap: + items: + - key: envoy.yaml + path: envoy.yaml + name: envoy + name: config + - name: wasm + hostPath: + path: /opt/kuadrant/wasm +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: envoy + name: envoy +spec: + ports: + - name: web + port: 8000 + protocol: TCP + selector: + app: envoy +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-wildcard-host +spec: + rules: + - host: talker-api.127.0.0.1.nip.io + http: + paths: + - backend: + service: + name: envoy + port: + number: 8000 + path: / + pathType: Prefix diff --git a/utils/deploy/envoy.yaml b/utils/deploy/envoy-tls.yaml similarity index 80% rename from utils/deploy/envoy.yaml rename to utils/deploy/envoy-tls.yaml index fd5f3b20..3b4d76b1 100644 --- a/utils/deploy/envoy.yaml +++ b/utils/deploy/envoy-tls.yaml @@ -8,13 +8,17 @@ data: envoy.yaml: | static_resources: clusters: - - name: authorino - connect_timeout: 0.25s + - name: authorino_wasm + connect_timeout: 1s type: STRICT_DNS lb_policy: ROUND_ROBIN - http2_protocol_options: { } + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: { } load_assignment: - cluster_name: authorino + cluster_name: authorino_wasm endpoints: - lb_endpoints: - endpoint: @@ -34,7 +38,11 @@ data: connect_timeout: 1s type: STRICT_DNS lb_policy: ROUND_ROBIN - http2_protocol_options: { } + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: { } load_assignment: cluster_name: limitador endpoints: @@ -74,7 +82,11 @@ data: connect_timeout: 0.25s type: STRICT_DNS lb_policy: ROUND_ROBIN - http2_protocol_options: { } + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http2_protocol_options: { } load_assignment: cluster_name: opentelemetry endpoints: @@ -91,7 +103,7 @@ data: port_value: 8000 filter_chains: - filters: - - name: envoy.http_connection_manager + - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: local @@ -102,25 +114,9 @@ data: - name: local_service domains: [ '*' ] routes: - - match: { prefix: /web } - route: - cluster: talker-web - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - disabled: true - match: { prefix: / } route: cluster: talker-api - rate_limits: - - actions: - - metadata: - metadata_key: - key: "envoy.filters.http.ext_authz" - path: - - key: ext_auth_data - - key: username - descriptor_key: user_id http_filters: - name: envoy.filters.http.header_to_metadata typed_config: @@ -153,14 +149,26 @@ data: { "name": "rlp-ns-A/rlp-name-A", "domain": "rlp-ns-A/rlp-name-A", - "service": "authorino", + "service": "authorino_wasm", "hostnames": ["*.a.com"], "rules": [ { + "conditions": [ + { + "allOf": [ + { + "selector": "request.host", + "operator": "eq", + "value": "test.a.com" + } + ] + } + ], "data": [ { - "selector": { - "selector": "unknown.path" + "static": { + "key": "limit_to_be_activated", + "value": "1" } } ] @@ -185,7 +193,6 @@ data: # timeout: 1s # service_name: envoy admin: - access_log_path: "/tmp/admin_access.log" address: socket_address: address: 0.0.0.0 @@ -212,10 +219,10 @@ spec: - --config-path /usr/local/etc/envoy/envoy.yaml - --service-cluster front-proxy - --log-level info - - --component-log-level filter:trace,http:debug,router:debug + - --component-log-level wasm:debug,filter:trace,http:debug,router:debug command: - /usr/local/bin/envoy - image: envoyproxy/envoy:v1.25-latest + image: envoyproxy/envoy:v1.31-latest name: envoy ports: - containerPort: 8000 From 8dd4c97df6b862a2bdbdcc4d4b31252ec3009e7e Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 9 Aug 2024 14:31:24 +0100 Subject: [PATCH 31/66] Add simple authconfig to local environment Signed-off-by: Adam Cattermole --- make/auth.mk | 1 + utils/deploy/authconfig.yaml | 28 ++++++++++++++++++++++++++++ utils/deploy/envoy-notls.yaml | 6 +++--- utils/deploy/envoy-tls.yaml | 6 +++--- 4 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 utils/deploy/authconfig.yaml diff --git a/make/auth.mk b/make/auth.mk index e5d57cc2..64ba2ed6 100644 --- a/make/auth.mk +++ b/make/auth.mk @@ -83,6 +83,7 @@ endif user-apps: ## Deploys talker API and envoy kubectl -n $(NAMESPACE) apply -f https://raw.githubusercontent.com/kuadrant/authorino-examples/main/talker-api/talker-api-deploy.yaml kubectl -n $(NAMESPACE) apply -f $(PROJECT_PATH)/utils/deploy/envoy-$(ENVOY_OVERLAY).yaml + kubectl -n $(NAMESPACE) apply -f $(PROJECT_PATH)/utils/deploy/authconfig.yaml ##@ Util diff --git a/utils/deploy/authconfig.yaml b/utils/deploy/authconfig.yaml new file mode 100644 index 00000000..eb01b060 --- /dev/null +++ b/utils/deploy/authconfig.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: authorino.kuadrant.io/v1beta2 +kind: AuthConfig +metadata: + name: talker-api-protection +spec: + hosts: + - talker-api.127.0.0.1.nip.io + authentication: + "friends": + apiKey: + selector: + matchLabels: + group: friends + credentials: + authorizationHeader: + prefix: APIKEY +--- +apiVersion: v1 +kind: Secret +metadata: + name: api-key-1 + labels: + authorino.kuadrant.io/managed-by: authorino + group: friends +stringData: + api_key: "ndyBzreUzF4zqDQsqSPMHkRhriEOtcRx" +type: Opaque diff --git a/utils/deploy/envoy-notls.yaml b/utils/deploy/envoy-notls.yaml index 263d3652..9e27d821 100644 --- a/utils/deploy/envoy-notls.yaml +++ b/utils/deploy/envoy-notls.yaml @@ -141,16 +141,16 @@ data: "name": "rlp-ns-A/rlp-name-A", "domain": "rlp-ns-A/rlp-name-A", "service": "authorino_wasm", - "hostnames": ["*.a.com"], + "hostnames": ["talker-api.127.0.0.1.nip.io"], "rules": [ { "conditions": [ { "allOf": [ { - "selector": "request.host", + "selector": "request.path", "operator": "eq", - "value": "test.a.com" + "value": "/hello" } ] } diff --git a/utils/deploy/envoy-tls.yaml b/utils/deploy/envoy-tls.yaml index 3b4d76b1..b6f90ffe 100644 --- a/utils/deploy/envoy-tls.yaml +++ b/utils/deploy/envoy-tls.yaml @@ -150,16 +150,16 @@ data: "name": "rlp-ns-A/rlp-name-A", "domain": "rlp-ns-A/rlp-name-A", "service": "authorino_wasm", - "hostnames": ["*.a.com"], + "hostnames": ["talker-api.127.0.0.1.nip.io"], "rules": [ { "conditions": [ { "allOf": [ { - "selector": "request.host", + "selector": "request.path", "operator": "eq", - "value": "test.a.com" + "value": "/hello" } ] } From a43f39336ec8f94247bec12c39159902e4c4362a Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 15 Aug 2024 11:55:08 +0100 Subject: [PATCH 32/66] Rename make/auth.mk to make/deploy.mk Signed-off-by: Adam Cattermole --- make/{auth.mk => deploy.mk} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename make/{auth.mk => deploy.mk} (100%) diff --git a/make/auth.mk b/make/deploy.mk similarity index 100% rename from make/auth.mk rename to make/deploy.mk From 7be2e291abff6699156a0015ecbaa0ee4eaba063 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 16 Aug 2024 15:49:35 +0100 Subject: [PATCH 33/66] Set the host in authconfig to an arbitrary string Signed-off-by: Adam Cattermole --- utils/deploy/authconfig.yaml | 2 +- utils/deploy/envoy-notls.yaml | 4 ++-- utils/deploy/envoy-tls.yaml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/utils/deploy/authconfig.yaml b/utils/deploy/authconfig.yaml index eb01b060..fa9b8d6b 100644 --- a/utils/deploy/authconfig.yaml +++ b/utils/deploy/authconfig.yaml @@ -5,7 +5,7 @@ metadata: name: talker-api-protection spec: hosts: - - talker-api.127.0.0.1.nip.io + - effective-route-1 authentication: "friends": apiKey: diff --git a/utils/deploy/envoy-notls.yaml b/utils/deploy/envoy-notls.yaml index 9e27d821..af1d5b7d 100644 --- a/utils/deploy/envoy-notls.yaml +++ b/utils/deploy/envoy-notls.yaml @@ -158,8 +158,8 @@ data: "data": [ { "static": { - "key": "limit_to_be_activated", - "value": "1" + "key": "host", + "value": "effective-route-1" } } ] diff --git a/utils/deploy/envoy-tls.yaml b/utils/deploy/envoy-tls.yaml index b6f90ffe..6b91fe09 100644 --- a/utils/deploy/envoy-tls.yaml +++ b/utils/deploy/envoy-tls.yaml @@ -167,8 +167,8 @@ data: "data": [ { "static": { - "key": "limit_to_be_activated", - "value": "1" + "key": "host", + "value": "effective-route-1" } } ] From 7ad0a00fb3a3bd4a3e91bbb300793511fd291edd Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Mon, 16 Sep 2024 10:45:11 +0100 Subject: [PATCH 34/66] Update plugin config for envoy Signed-off-by: Adam Cattermole --- utils/deploy/envoy-notls.yaml | 31 +++++++++++++++++++++++++++---- utils/deploy/envoy-tls.yaml | 31 +++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/utils/deploy/envoy-notls.yaml b/utils/deploy/envoy-notls.yaml index af1d5b7d..ac54b81b 100644 --- a/utils/deploy/envoy-notls.yaml +++ b/utils/deploy/envoy-notls.yaml @@ -135,13 +135,25 @@ data: "@type": "type.googleapis.com/google.protobuf.StringValue" value: > { - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "authorino": { + "type": "auth", + "endpoint": "authorino_wasm", + "failureMode": "deny" + }, + "limitador": { + "type": "ratelimit", + "endpoint": "limitador", + "failureMode": "deny" + } + }, + "policies": [ { "name": "rlp-ns-A/rlp-name-A", "domain": "rlp-ns-A/rlp-name-A", - "service": "authorino_wasm", - "hostnames": ["talker-api.127.0.0.1.nip.io"], + "hostnames": [ + "talker-api.127.0.0.1.nip.io" + ], "rules": [ { "conditions": [ @@ -164,6 +176,17 @@ data: } ] } + ], + "actions": [ + { + "extension": "authorino", + "data": { + "static": { + "key": "host", + "value": "effective-route-1" + } + } + } ] } ] diff --git a/utils/deploy/envoy-tls.yaml b/utils/deploy/envoy-tls.yaml index 6b91fe09..7416fbbc 100644 --- a/utils/deploy/envoy-tls.yaml +++ b/utils/deploy/envoy-tls.yaml @@ -144,13 +144,25 @@ data: "@type": "type.googleapis.com/google.protobuf.StringValue" value: > { - "failureMode": "deny", - "rateLimitPolicies": [ + "extensions": { + "authorino": { + "type": "auth", + "endpoint": "authorino_wasm", + "failureMode": "deny" + }, + "limitador": { + "type": "ratelimit", + "endpoint": "limitador", + "failureMode": "deny" + } + }, + "policies": [ { "name": "rlp-ns-A/rlp-name-A", "domain": "rlp-ns-A/rlp-name-A", - "service": "authorino_wasm", - "hostnames": ["talker-api.127.0.0.1.nip.io"], + "hostnames": [ + "talker-api.127.0.0.1.nip.io" + ], "rules": [ { "conditions": [ @@ -173,6 +185,17 @@ data: } ] } + ], + "actions": [ + { + "extension": "authorino", + "data": { + "static": { + "key": "host", + "value": "effective-route-1" + } + } + } ] } ] From b78bbc91f52f8463b8da24a88f956eee78ff722e Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Mon, 16 Sep 2024 11:15:38 +0100 Subject: [PATCH 35/66] Add original limitador examples to plugin config Signed-off-by: Adam Cattermole --- utils/deploy/envoy-notls.yaml | 220 +++++++++++++++++++++++++++++++++- utils/deploy/envoy-tls.yaml | 220 +++++++++++++++++++++++++++++++++- 2 files changed, 432 insertions(+), 8 deletions(-) diff --git a/utils/deploy/envoy-notls.yaml b/utils/deploy/envoy-notls.yaml index ac54b81b..e3f8146c 100644 --- a/utils/deploy/envoy-notls.yaml +++ b/utils/deploy/envoy-notls.yaml @@ -149,10 +149,10 @@ data: }, "policies": [ { - "name": "rlp-ns-A/rlp-name-A", - "domain": "rlp-ns-A/rlp-name-A", + "name": "auth-ns-A/auth-name-A", + "domain": "auth-ns-A/auth-name-A", "hostnames": [ - "talker-api.127.0.0.1.nip.io" + "*.a.auth.com" ], "rules": [ { @@ -162,7 +162,7 @@ data: { "selector": "request.path", "operator": "eq", - "value": "/hello" + "value": "/get" } ] } @@ -188,6 +188,218 @@ data: } } ] + }, + { + "name": "rlp-ns-A/rlp-name-A", + "domain": "rlp-ns-A/rlp-name-A", + "hostnames": [ + "*.a.rlp.com" + ], + "rules": [ + { + "data": [ + { + "selector": { + "selector": "unknown.path" + } + } + ] + } + ], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] + }, + { + "name": "rlp-ns-B/rlp-name-B", + "domain": "rlp-ns-B/rlp-name-B", + "hostnames": [ + "*.b.rlp.com" + ], + "rules": [ + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/unknown-path" + } + ] + } + ], + "data": [ + { + "static": { + "key": "rlp-ns-B/rlp-name-B/limit-not-to-be-activated", + "value": "1" + } + } + ] + } + ], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-B/rlp-name-B", + "value": "1" + } + } + } + ] + }, + { + "name": "rlp-ns-C/rlp-name-C", + "domain": "rlp-ns-C/rlp-name-C", + "hostnames": [ + "*.c.rlp.com" + ], + "rules": [ + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/get" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "test.c.rlp.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "GET" + } + ] + } + ], + "data": [ + { + "static": { + "key": "limit_to_be_activated", + "value": "1" + } + } + ] + }, + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/get" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "test.c.rlp.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "GET" + } + ] + } + ], + "data": [ + { + "selector": { + "selector": "source.address" + } + } + ] + }, + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/get" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "test.c.rlp.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "GET" + } + ] + } + ], + "data": [ + { + "selector": { + "selector": "request.headers.My-Custom-Header-01" + } + } + ] + }, + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/get" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "test.c.rlp.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "GET" + } + ] + } + ], + "data": [ + { + "selector": { + "selector": "metadata.filter_metadata.envoy\\.filters\\.http\\.header_to_metadata.user_id", + "key": "user_id" + } + } + ] + } + ], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-C/rlp-name-C", + "value": "1" + } + } + } + ] } ] } diff --git a/utils/deploy/envoy-tls.yaml b/utils/deploy/envoy-tls.yaml index 7416fbbc..dbdd4500 100644 --- a/utils/deploy/envoy-tls.yaml +++ b/utils/deploy/envoy-tls.yaml @@ -158,10 +158,10 @@ data: }, "policies": [ { - "name": "rlp-ns-A/rlp-name-A", - "domain": "rlp-ns-A/rlp-name-A", + "name": "auth-ns-A/auth-name-A", + "domain": "auth-ns-A/auth-name-A", "hostnames": [ - "talker-api.127.0.0.1.nip.io" + "*.a.auth.com" ], "rules": [ { @@ -171,7 +171,7 @@ data: { "selector": "request.path", "operator": "eq", - "value": "/hello" + "value": "/get" } ] } @@ -197,6 +197,218 @@ data: } } ] + }, + { + "name": "rlp-ns-A/rlp-name-A", + "domain": "rlp-ns-A/rlp-name-A", + "hostnames": [ + "*.a.rlp.com" + ], + "rules": [ + { + "data": [ + { + "selector": { + "selector": "unknown.path" + } + } + ] + } + ], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + } + } + ] + }, + { + "name": "rlp-ns-B/rlp-name-B", + "domain": "rlp-ns-B/rlp-name-B", + "hostnames": [ + "*.b.rlp.com" + ], + "rules": [ + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/unknown-path" + } + ] + } + ], + "data": [ + { + "static": { + "key": "rlp-ns-B/rlp-name-B/limit-not-to-be-activated", + "value": "1" + } + } + ] + } + ], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-B/rlp-name-B", + "value": "1" + } + } + } + ] + }, + { + "name": "rlp-ns-C/rlp-name-C", + "domain": "rlp-ns-C/rlp-name-C", + "hostnames": [ + "*.c.rlp.com" + ], + "rules": [ + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/get" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "test.c.rlp.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "GET" + } + ] + } + ], + "data": [ + { + "static": { + "key": "limit_to_be_activated", + "value": "1" + } + } + ] + }, + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/get" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "test.c.rlp.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "GET" + } + ] + } + ], + "data": [ + { + "selector": { + "selector": "source.address" + } + } + ] + }, + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/get" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "test.c.rlp.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "GET" + } + ] + } + ], + "data": [ + { + "selector": { + "selector": "request.headers.My-Custom-Header-01" + } + } + ] + }, + { + "conditions": [ + { + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/get" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "test.c.rlp.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "GET" + } + ] + } + ], + "data": [ + { + "selector": { + "selector": "metadata.filter_metadata.envoy\\.filters\\.http\\.header_to_metadata.user_id", + "key": "user_id" + } + } + ] + } + ], + "actions": [ + { + "extension": "limitador", + "data": { + "static": { + "key": "rlp-ns-C/rlp-name-C", + "value": "1" + } + } + } + ] } ] } From cb74375a75065ade871d4f02ea0262a992848829 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Mon, 16 Sep 2024 12:06:38 +0100 Subject: [PATCH 36/66] Add some documentation for auth development Signed-off-by: Adam Cattermole --- README.md | 116 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 106 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 46587c3a..4bb803ac 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,23 @@ A Proxy-Wasm module written in Rust, acting as a shim between Envoy and Limitador. ## Sample configuration + Following is a sample configuration used by the shim. ```yaml -failureMode: deny -rateLimitPolicies: +extensions: + auth-ext: + type: auth + endpoint: auth-cluster + failureMode: deny + ratelimit-ext: + type: ratelimit + endpoint: ratelimit-cluster + failureMode: deny +policies: - name: rlp-ns-A/rlp-name-A domain: rlp-ns-A/rlp-name-A - service: rate-limit-cluster - hostnames: ["*.toystore.com"] + hostnames: [ "*.toystore.com" ] rules: - conditions: - allOf: @@ -33,6 +41,12 @@ rateLimitPolicies: - static: key: admin value: "1" + actions: + - extension: ratelimit-ext + data: + static: + key: host + value: rlp-ns-A/rlp-name-A ``` ## Features @@ -57,6 +71,7 @@ pub enum WhenConditionOperator { The `matches` operator is a a simple globbing pattern implementation based on regular expressions. The only characters taken into account are: + * `?`: 0 or 1 characters * `*`: 0 or more characters * `+`: 1 or more characters @@ -95,10 +110,10 @@ Example: Input: this.is.a.exam\.ple -> Retuns: ["this", "is", "a", "exam.ple"]. ``` -Some path segments include dot `.` char in them. For instance envoy filter names: `envoy.filters.http.header_to_metadata`. +Some path segments include dot `.` char in them. For instance envoy filter +names: `envoy.filters.http.header_to_metadata`. In that particular cases, the dot chat (separator), needs to be escaped. - ## Building Prerequisites: @@ -127,7 +142,85 @@ make build BUILD=release cargo test ``` -## Running local development environment +## Running local development environment (kind) + +`docker` is required. + +Run local development environment + +```sh +make local-setup +``` + +This deploys a local kubernetes cluster using kind, with the local build of wasm-shim mapped to the envoy container. An +echo API as well as limitador, authorino, and some test policies are configured. + +To expose the envoy endpoint run the following: + +```sh +kubectl port-forward --namespace default deployment/envoy 8000:8000 +``` + +There is then a single auth policy defined for e2e testing: + +* `auth-a` which defines auth is required for requests to `/get` for the `AuthConfig` with `effective-route-1` + +```sh +curl -H "Host: test.a.auth.com" http://127.0.0.1:8000/get -i +``` + +And three rate limit policies defined for e2e testing: + +* `rlp-a`: Only one data item. Data selector should not generate return any value. Thus, descriptor should be empty and + rate limiting service should **not** be called. + +```sh +curl -H "Host: test.a.rlp.com" http://127.0.0.1:8000/get -i +``` + +* `rlp-b`: Conditions do not match. Hence, rate limiting service should **not** be called. + +```sh +curl -H "Host: test.b.rlp.com" http://127.0.0.1:8000/get -i +``` + +* `rlp-c`: Four descriptors from multiple rules should be generated. Hence, rate limiting service should be called. + +```sh +curl -H "Host: test.c.rlp.com" -H "x-forwarded-for: 127.0.0.1" -H "My-Custom-Header-01: my-custom-header-value-01" -H "x-dyn-user-id: bob" http://127.0.0.1:8000/get -i +``` + +The expected descriptors: + +``` +RateLimitDescriptor { entries: [Entry { key: "limit_to_be_activated", value: "1" }], limit: None } +``` + +``` +RateLimitDescriptor { entries: [Entry { key: "source.address", value: "127.0.0.1:0" }], limit: None } +``` + +``` +RateLimitDescriptor { entries: [Entry { key: "request.headers.My-Custom-Header-01", value: "my-custom-header-value-01" }], limit: None } +``` + +``` +RateLimitDescriptor { entries: [Entry { key: "user_id", value: "bob" }], limit: None } +``` + +To rebuild and deploy to the cluster: + +```sh +make build local-rollout +``` + +Stop and clean up resources: + +```sh +make local-cleanup +``` + +## Running local development environment (docker-compose legacy) `docker` and `docker-compose` required. @@ -139,7 +232,8 @@ make development Three rate limit policies defined for e2e testing: -* `rlp-a`: Only one data item. Data selector should not generate return any value. Thus, descriptor should be empty and rate limiting service should **not** be called. +* `rlp-a`: Only one data item. Data selector should not generate return any value. Thus, descriptor should be empty and + rate limiting service should **not** be called. ``` curl -H "Host: test.a.com" http://127.0.0.1:18000/get @@ -175,7 +269,9 @@ RateLimitDescriptor { entries: [Entry { key: "request.headers.My-Custom-Header-0 RateLimitDescriptor { entries: [Entry { key: "user_id", value: "bob" }], limit: None } ``` -**Note:** Using [Header-To-Metadata filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/header_to_metadata_filter#config-http-filters-header-to-metadata), `x-dyn-user-id` header value is available in the metadata struct with the `user-id` key. +**Note:** +Using [Header-To-Metadata filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/header_to_metadata_filter#config-http-filters-header-to-metadata), `x-dyn-user-id` +header value is available in the metadata struct with the `user-id` key. According to the defined limits: @@ -187,7 +283,7 @@ According to the defined limits: conditions: - "limit_to_be_activated == '1'" - "user_id == 'bob'" - variables: [] + variables: [ ] ``` The third request in less than 10 seconds should return `429 Too Many Requests`. From 625b205acbbe21eeee90fc69bfc093a34bbf3e70 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 27 Aug 2024 16:40:04 +0200 Subject: [PATCH 37/66] [feat] Action dispatcher state machine, naive impl Signed-off-by: dd di cesare --- src/action_dispatcher.rs | 201 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 202 insertions(+) create mode 100644 src/action_dispatcher.rs diff --git a/src/action_dispatcher.rs b/src/action_dispatcher.rs new file mode 100644 index 00000000..d22550d0 --- /dev/null +++ b/src/action_dispatcher.rs @@ -0,0 +1,201 @@ +use std::cell::RefCell; + +#[derive(PartialEq, Debug, Clone)] +pub(crate) enum State { + Pending, + Waiting, + Done, +} + +impl State { + fn next(&mut self) { + match self { + State::Pending => *self = State::Waiting, + State::Waiting => *self = State::Done, + _ => {} + } + } +} +#[derive(PartialEq, Clone)] +pub(crate) enum Action { + Auth { state: State }, + RateLimit { state: State }, +} + +impl Action { + pub fn trigger(&mut self) { + match self { + Action::Auth { .. } => self.auth(), + Action::RateLimit { .. } => self.rate_limit(), + } + } + + fn get_state(&self) -> &State { + match self { + Action::Auth { state } => state, + Action::RateLimit { state } => state, + } + } + + fn rate_limit(&mut self) { + // Specifics for RL, returning State + if let Action::RateLimit { state } = self { + match state { + State::Pending => { + println!("Trigger the request and return State::Waiting"); + state.next(); + } + State::Waiting => { + println!( + "When got on_grpc_response, process RL response and return State::Done" + ); + state.next(); + } + State::Done => { + println!("Done for RL... calling next action (?)"); + } + } + } + } + + fn auth(&mut self) { + // Specifics for Auth, returning State + if let Action::Auth { state } = self { + match state { + State::Pending => { + println!("Trigger the request and return State::Waiting"); + state.next(); + } + State::Waiting => { + println!( + "When got on_grpc_response, process Auth response and return State::Done" + ); + state.next(); + } + State::Done => { + println!("Done for Auth... calling next action (?)"); + } + } + } + } +} + +pub struct ActionDispatcher { + actions: RefCell>, +} + +impl ActionDispatcher { + pub fn default() -> ActionDispatcher { + ActionDispatcher { + actions: RefCell::new(vec![]), + } + } + + pub fn new(/*vec of PluginConfig actions*/) -> ActionDispatcher { + ActionDispatcher::default() + } + + pub fn push_actions(&self, actions: Vec) { + self.actions.borrow_mut().extend(actions); + } + + pub fn get_current_action_state(&self) -> Option { + self.actions + .borrow() + .first() + .map(|action| action.get_state().clone()) + } + + pub fn next(&self) -> bool { + let mut actions = self.actions.borrow_mut(); + if let Some((i, action)) = actions.iter_mut().enumerate().next() { + if let State::Done = action.get_state() { + actions.remove(i); + actions.len() > 0 + } else { + action.trigger(); + true + } + } else { + false + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn action_transition() { + let mut action = Action::Auth { + state: State::Pending, + }; + assert_eq!(*action.get_state(), State::Pending); + action.trigger(); + assert_eq!(*action.get_state(), State::Waiting); + action.trigger(); + assert_eq!(*action.get_state(), State::Done); + } + + #[test] + fn action_dispatcher_push_actions() { + let mut action_dispatcher = ActionDispatcher { + actions: RefCell::new(vec![Action::RateLimit { + state: State::Pending, + }]), + }; + + assert_eq!(action_dispatcher.actions.borrow().len(), 1); + + action_dispatcher.push_actions(vec![Action::Auth { + state: State::Pending, + }]); + + assert_eq!(action_dispatcher.actions.borrow().len(), 2); + } + + #[test] + fn action_dispatcher_get_current_action_state() { + let action_dispatcher = ActionDispatcher { + actions: RefCell::new(vec![Action::RateLimit { + state: State::Waiting, + }]), + }; + + assert_eq!( + action_dispatcher.get_current_action_state(), + Some(State::Waiting) + ); + + let action_dispatcher2 = ActionDispatcher::default(); + + assert_eq!(action_dispatcher2.get_current_action_state(), None); + } + + #[test] + fn action_dispatcher_next() { + let mut action_dispatcher = ActionDispatcher { + actions: RefCell::new(vec![Action::RateLimit { + state: State::Pending, + }]), + }; + let mut res = action_dispatcher.next(); + assert_eq!(res, true); + assert_eq!( + action_dispatcher.get_current_action_state(), + Some(State::Waiting) + ); + + res = action_dispatcher.next(); + assert_eq!(res, true); + assert_eq!( + action_dispatcher.get_current_action_state(), + Some(State::Done) + ); + + res = action_dispatcher.next(); + assert_eq!(res, false); + assert_eq!(action_dispatcher.get_current_action_state(), None); + } +} diff --git a/src/lib.rs b/src/lib.rs index 279ed839..4ef8a50b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +mod action_dispatcher; mod attribute; mod configuration; #[allow(renamed_and_removed_lints)] From 360c967a955a896f542dbf7d746bcbaed39878d3 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Mon, 2 Sep 2024 15:51:55 +0200 Subject: [PATCH 38/66] [refactor] Changing name to `Operation` instead of `Action` * Could get confusing with proxy_wasm `Actions` * Also with plugin configuration `Action` Signed-off-by: dd di cesare --- src/action_dispatcher.rs | 201 ------------------------------ src/lib.rs | 2 +- src/operation_dispatcher.rs | 237 ++++++++---------------------------- 3 files changed, 50 insertions(+), 390 deletions(-) delete mode 100644 src/action_dispatcher.rs diff --git a/src/action_dispatcher.rs b/src/action_dispatcher.rs deleted file mode 100644 index d22550d0..00000000 --- a/src/action_dispatcher.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::cell::RefCell; - -#[derive(PartialEq, Debug, Clone)] -pub(crate) enum State { - Pending, - Waiting, - Done, -} - -impl State { - fn next(&mut self) { - match self { - State::Pending => *self = State::Waiting, - State::Waiting => *self = State::Done, - _ => {} - } - } -} -#[derive(PartialEq, Clone)] -pub(crate) enum Action { - Auth { state: State }, - RateLimit { state: State }, -} - -impl Action { - pub fn trigger(&mut self) { - match self { - Action::Auth { .. } => self.auth(), - Action::RateLimit { .. } => self.rate_limit(), - } - } - - fn get_state(&self) -> &State { - match self { - Action::Auth { state } => state, - Action::RateLimit { state } => state, - } - } - - fn rate_limit(&mut self) { - // Specifics for RL, returning State - if let Action::RateLimit { state } = self { - match state { - State::Pending => { - println!("Trigger the request and return State::Waiting"); - state.next(); - } - State::Waiting => { - println!( - "When got on_grpc_response, process RL response and return State::Done" - ); - state.next(); - } - State::Done => { - println!("Done for RL... calling next action (?)"); - } - } - } - } - - fn auth(&mut self) { - // Specifics for Auth, returning State - if let Action::Auth { state } = self { - match state { - State::Pending => { - println!("Trigger the request and return State::Waiting"); - state.next(); - } - State::Waiting => { - println!( - "When got on_grpc_response, process Auth response and return State::Done" - ); - state.next(); - } - State::Done => { - println!("Done for Auth... calling next action (?)"); - } - } - } - } -} - -pub struct ActionDispatcher { - actions: RefCell>, -} - -impl ActionDispatcher { - pub fn default() -> ActionDispatcher { - ActionDispatcher { - actions: RefCell::new(vec![]), - } - } - - pub fn new(/*vec of PluginConfig actions*/) -> ActionDispatcher { - ActionDispatcher::default() - } - - pub fn push_actions(&self, actions: Vec) { - self.actions.borrow_mut().extend(actions); - } - - pub fn get_current_action_state(&self) -> Option { - self.actions - .borrow() - .first() - .map(|action| action.get_state().clone()) - } - - pub fn next(&self) -> bool { - let mut actions = self.actions.borrow_mut(); - if let Some((i, action)) = actions.iter_mut().enumerate().next() { - if let State::Done = action.get_state() { - actions.remove(i); - actions.len() > 0 - } else { - action.trigger(); - true - } - } else { - false - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn action_transition() { - let mut action = Action::Auth { - state: State::Pending, - }; - assert_eq!(*action.get_state(), State::Pending); - action.trigger(); - assert_eq!(*action.get_state(), State::Waiting); - action.trigger(); - assert_eq!(*action.get_state(), State::Done); - } - - #[test] - fn action_dispatcher_push_actions() { - let mut action_dispatcher = ActionDispatcher { - actions: RefCell::new(vec![Action::RateLimit { - state: State::Pending, - }]), - }; - - assert_eq!(action_dispatcher.actions.borrow().len(), 1); - - action_dispatcher.push_actions(vec![Action::Auth { - state: State::Pending, - }]); - - assert_eq!(action_dispatcher.actions.borrow().len(), 2); - } - - #[test] - fn action_dispatcher_get_current_action_state() { - let action_dispatcher = ActionDispatcher { - actions: RefCell::new(vec![Action::RateLimit { - state: State::Waiting, - }]), - }; - - assert_eq!( - action_dispatcher.get_current_action_state(), - Some(State::Waiting) - ); - - let action_dispatcher2 = ActionDispatcher::default(); - - assert_eq!(action_dispatcher2.get_current_action_state(), None); - } - - #[test] - fn action_dispatcher_next() { - let mut action_dispatcher = ActionDispatcher { - actions: RefCell::new(vec![Action::RateLimit { - state: State::Pending, - }]), - }; - let mut res = action_dispatcher.next(); - assert_eq!(res, true); - assert_eq!( - action_dispatcher.get_current_action_state(), - Some(State::Waiting) - ); - - res = action_dispatcher.next(); - assert_eq!(res, true); - assert_eq!( - action_dispatcher.get_current_action_state(), - Some(State::Done) - ); - - res = action_dispatcher.next(); - assert_eq!(res, false); - assert_eq!(action_dispatcher.get_current_action_state(), None); - } -} diff --git a/src/lib.rs b/src/lib.rs index 4ef8a50b..35f49e07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -mod action_dispatcher; +mod operation_dispatcher; mod attribute; mod configuration; #[allow(renamed_and_removed_lints)] diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index a9ab3c18..86c6deac 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -1,14 +1,5 @@ -use crate::configuration::{Extension, ExtensionType, FailureMode}; -use crate::envoy::RateLimitDescriptor; -use crate::policy::Policy; -use crate::service::{GetMapValuesBytesFn, GrpcCallFn, GrpcMessage, GrpcServiceHandler}; -use protobuf::RepeatedField; -use proxy_wasm::hostcalls; -use proxy_wasm::types::{Bytes, MapType, Status}; +use proxy_wasm::types::Status; use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; -use std::time::Duration; #[allow(dead_code)] #[derive(PartialEq, Debug, Clone)] @@ -29,105 +20,58 @@ impl State { } } -type Procedure = (Rc, GrpcMessage); - #[allow(dead_code)] #[derive(Clone)] pub(crate) struct Operation { state: State, result: Result, - extension: Rc, - procedure: Procedure, - grpc_call_fn: GrpcCallFn, - get_map_values_bytes_fn: GetMapValuesBytesFn, + action: Option Result>, } #[allow(dead_code)] impl Operation { - pub fn new(extension: Rc, procedure: Procedure) -> Self { + pub fn default() -> Self { Self { state: State::Pending, result: Err(Status::Empty), - extension, - procedure, - grpc_call_fn, - get_map_values_bytes_fn, + action: None, } } - fn trigger(&mut self) { + pub fn set_action(&mut self, action: fn() -> Result) { + self.action = Some(action); + } + + pub fn trigger(&mut self) { if let State::Done = self.state { - } else { - self.result = self.procedure.0.send( - self.get_map_values_bytes_fn, - self.grpc_call_fn, - self.procedure.1.clone(), - ); + } else if let Some(action) = self.action { + self.result = action(); self.state.next(); } } - pub fn get_state(&self) -> State { + fn get_state(&self) -> State { self.state.clone() } - pub fn get_result(&self) -> Result { + fn get_result(&self) -> Result { self.result } - - pub fn get_extension_type(&self) -> ExtensionType { - self.extension.extension_type.clone() - } - - pub fn get_failure_mode(&self) -> FailureMode { - self.extension.failure_mode.clone() - } } #[allow(dead_code)] pub struct OperationDispatcher { operations: RefCell>, - service_handlers: HashMap>, } #[allow(dead_code)] impl OperationDispatcher { - pub fn default() -> Self { + pub fn default() -> OperationDispatcher { OperationDispatcher { operations: RefCell::new(vec![]), - service_handlers: HashMap::default(), - } - } - pub fn new(service_handlers: HashMap>) -> Self { - Self { - service_handlers, - operations: RefCell::new(vec![]), } } - pub fn build_operations( - &self, - policy: &Policy, - descriptors: RepeatedField, - ) { - let mut operations: Vec = vec![]; - policy.actions.iter().for_each(|action| { - // TODO(didierofrivia): Error handling - if let Some(service) = self.service_handlers.get(&action.extension) { - let message = GrpcMessage::new( - service.get_extension_type(), - policy.domain.clone(), - descriptors.clone(), - ); - operations.push(Operation::new( - service.get_extension(), - (Rc::clone(service), message), - )) - } - }); - self.push_operations(operations); - } - pub fn push_operations(&self, operations: Vec) { self.operations.borrow_mut().extend(operations); } @@ -143,109 +87,30 @@ impl OperationDispatcher { self.operations.borrow().first().unwrap().get_result() } - pub fn next(&self) -> Option { + pub fn next(&self) -> bool { let mut operations = self.operations.borrow_mut(); if let Some((i, operation)) = operations.iter_mut().enumerate().next() { if let State::Done = operation.get_state() { operations.remove(i); - operations.get(i).cloned() // The next op is now at `i` + operations.len() > 0 } else { operation.trigger(); - Some(operation.clone()) + true } } else { - None + false } } } -fn grpc_call_fn( - upstream_name: &str, - service_name: &str, - method_name: &str, - initial_metadata: Vec<(&str, &[u8])>, - message: Option<&[u8]>, - timeout: Duration, -) -> Result { - hostcalls::dispatch_grpc_call( - upstream_name, - service_name, - method_name, - initial_metadata, - message, - timeout, - ) -} - -fn get_map_values_bytes_fn(map_type: MapType, key: &str) -> Result, Status> { - hostcalls::get_map_value_bytes(map_type, key) -} - #[cfg(test)] mod tests { use super::*; - use crate::envoy::RateLimitRequest; - use std::time::Duration; - - fn grpc_call_fn_stub( - _upstream_name: &str, - _service_name: &str, - _method_name: &str, - _initial_metadata: Vec<(&str, &[u8])>, - _message: Option<&[u8]>, - _timeout: Duration, - ) -> Result { - Ok(200) - } - - fn get_map_values_bytes_fn_stub( - _map_type: MapType, - _key: &str, - ) -> Result, Status> { - Ok(Some(Vec::new())) - } - - fn build_grpc_service_handler() -> GrpcServiceHandler { - GrpcServiceHandler::new(Rc::new(Default::default()), Rc::new(Default::default())) - } - - fn build_message() -> RateLimitRequest { - RateLimitRequest { - domain: "example.org".to_string(), - descriptors: RepeatedField::new(), - hits_addend: 1, - unknown_fields: Default::default(), - cached_size: Default::default(), - } - } - - fn build_operation() -> Operation { - Operation { - state: State::Pending, - result: Ok(1), - extension: Rc::new(Extension::default()), - procedure: ( - Rc::new(build_grpc_service_handler()), - GrpcMessage::RateLimit(build_message()), - ), - grpc_call_fn: grpc_call_fn_stub, - get_map_values_bytes_fn: get_map_values_bytes_fn_stub, - } - } - - #[test] - fn operation_getters() { - let operation = build_operation(); - - assert_eq!(operation.get_state(), State::Pending); - assert_eq!(operation.get_extension_type(), ExtensionType::RateLimit); - assert_eq!(operation.get_failure_mode(), FailureMode::Deny); - assert_eq!(operation.get_result(), Ok(1)); - } #[test] fn operation_transition() { - let mut operation = build_operation(); + let mut operation = Operation::default(); + operation.set_action(|| -> Result { Ok(200) }); assert_eq!(operation.get_state(), State::Pending); operation.trigger(); assert_eq!(operation.get_state(), State::Waiting); @@ -256,18 +121,23 @@ mod tests { #[test] fn operation_dispatcher_push_actions() { - let operation_dispatcher = OperationDispatcher::default(); - - assert_eq!(operation_dispatcher.operations.borrow().len(), 0); - operation_dispatcher.push_operations(vec![build_operation()]); + let operation_dispatcher = OperationDispatcher { + operations: RefCell::new(vec![Operation::default()]), + }; assert_eq!(operation_dispatcher.operations.borrow().len(), 1); + + operation_dispatcher.push_operations(vec![Operation::default()]); + + assert_eq!(operation_dispatcher.operations.borrow().len(), 2); } #[test] fn operation_dispatcher_get_current_action_state() { - let operation_dispatcher = OperationDispatcher::default(); - operation_dispatcher.push_operations(vec![build_operation()]); + let operation_dispatcher = OperationDispatcher { + operations: RefCell::new(vec![Operation::default()]), + }; + assert_eq!( operation_dispatcher.get_current_operation_state(), Some(State::Pending) @@ -276,37 +146,28 @@ mod tests { #[test] fn operation_dispatcher_next() { - let operation_dispatcher = OperationDispatcher::default(); - operation_dispatcher.push_operations(vec![build_operation(), build_operation()]); - - assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(1)); + let mut operation = Operation::default(); + operation.set_action(|| -> Result { Ok(200) }); + let operation_dispatcher = OperationDispatcher { + operations: RefCell::new(vec![operation]), + }; + let mut res = operation_dispatcher.next(); + assert!(res); assert_eq!( operation_dispatcher.get_current_operation_state(), - Some(State::Pending) + Some(State::Waiting) ); - let mut op = operation_dispatcher.next(); - assert_eq!(op.clone().unwrap().get_result(), Ok(200)); - assert_eq!(op.unwrap().get_state(), State::Waiting); - - op = operation_dispatcher.next(); - assert_eq!(op.clone().unwrap().get_result(), Ok(200)); - assert_eq!(op.unwrap().get_state(), State::Done); - - op = operation_dispatcher.next(); - assert_eq!(op.clone().unwrap().get_result(), Ok(1)); - assert_eq!(op.unwrap().get_state(), State::Pending); - - op = operation_dispatcher.next(); - assert_eq!(op.clone().unwrap().get_result(), Ok(200)); - assert_eq!(op.unwrap().get_state(), State::Waiting); - - op = operation_dispatcher.next(); - assert_eq!(op.clone().unwrap().get_result(), Ok(200)); - assert_eq!(op.unwrap().get_state(), State::Done); + res = operation_dispatcher.next(); + assert!(res); + assert_eq!( + operation_dispatcher.get_current_operation_state(), + Some(State::Done) + ); + assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(200)); - op = operation_dispatcher.next(); - assert!(op.is_none()); - assert!(operation_dispatcher.get_current_operation_state().is_none()); + res = operation_dispatcher.next(); + assert!(!res); + assert_eq!(operation_dispatcher.get_current_operation_state(), None); } } From 1fc0e1a984b64ad5d7581860abb8f26ce17d20bf Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 3 Sep 2024 14:56:49 +0200 Subject: [PATCH 39/66] [refactor] OperationDispatcher triggering procedures Signed-off-by: dd di cesare --- src/lib.rs | 1 - src/operation_dispatcher.rs | 126 +++++++++++++++++++++++++++--------- 2 files changed, 96 insertions(+), 31 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 35f49e07..279ed839 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -mod operation_dispatcher; mod attribute; mod configuration; #[allow(renamed_and_removed_lints)] diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 86c6deac..91efc538 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -1,5 +1,11 @@ +use crate::envoy::RateLimitDescriptor; +use crate::policy::Policy; +use crate::service::{GrpcMessage, GrpcServiceHandler}; +use protobuf::RepeatedField; use proxy_wasm::types::Status; use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; #[allow(dead_code)] #[derive(PartialEq, Debug, Clone)] @@ -20,32 +26,33 @@ impl State { } } +type Procedure = (Rc, GrpcMessage); + #[allow(dead_code)] -#[derive(Clone)] pub(crate) struct Operation { state: State, result: Result, - action: Option Result>, + procedure: Procedure, } #[allow(dead_code)] impl Operation { - pub fn default() -> Self { + pub fn new(procedure: Procedure) -> Self { Self { state: State::Pending, result: Err(Status::Empty), - action: None, + procedure, } } - pub fn set_action(&mut self, action: fn() -> Result) { - self.action = Some(action); + pub fn set_action(&mut self, procedure: Procedure) { + self.procedure = procedure; } pub fn trigger(&mut self) { if let State::Done = self.state { - } else if let Some(action) = self.action { - self.result = action(); + } else { + self.result = self.procedure.0.send(self.procedure.1.clone()); self.state.next(); } } @@ -62,15 +69,39 @@ impl Operation { #[allow(dead_code)] pub struct OperationDispatcher { operations: RefCell>, + service_handlers: HashMap>, } #[allow(dead_code)] impl OperationDispatcher { - pub fn default() -> OperationDispatcher { + pub fn default() -> Self { OperationDispatcher { operations: RefCell::new(vec![]), + service_handlers: HashMap::default(), } } + pub fn new(service_handlers: HashMap>) -> Self { + Self { + service_handlers, + operations: RefCell::new(vec![]), + } + } + + pub fn build_operations( + &self, + policy: &Policy, + descriptors: RepeatedField, + ) { + let mut operations: Vec = vec![]; + policy.actions.iter().for_each(|action| { + // TODO(didierofrivia): Error handling + if let Some(service) = self.service_handlers.get(&action.extension) { + let message = service.build_message(policy.domain.clone(), descriptors.clone()); + operations.push(Operation::new((service.clone(), message))) + } + }); + self.push_operations(operations); + } pub fn push_operations(&self, operations: Vec) { self.operations.borrow_mut().extend(operations); @@ -87,18 +118,19 @@ impl OperationDispatcher { self.operations.borrow().first().unwrap().get_result() } - pub fn next(&self) -> bool { + pub fn next(&self) -> Option<(State, Result)> { let mut operations = self.operations.borrow_mut(); if let Some((i, operation)) = operations.iter_mut().enumerate().next() { if let State::Done = operation.get_state() { + let res = operation.get_result(); operations.remove(i); - operations.len() > 0 + Some((State::Done, res)) } else { operation.trigger(); - true + Some((operation.state.clone(), operation.result)) } } else { - false + None } } } @@ -106,11 +138,44 @@ impl OperationDispatcher { #[cfg(test)] mod tests { use super::*; + use crate::envoy::RateLimitRequest; + use std::time::Duration; + + fn grpc_call( + _upstream_name: &str, + _service_name: &str, + _method_name: &str, + _initial_metadata: Vec<(&str, &[u8])>, + _message: Option<&[u8]>, + _timeout: Duration, + ) -> Result { + Ok(1) + } + + fn build_grpc_service_handler() -> GrpcServiceHandler { + GrpcServiceHandler::new( + Rc::new(Default::default()), + Rc::new(Default::default()), + Some(grpc_call), + ) + } + + fn build_message() -> RateLimitRequest { + RateLimitRequest { + domain: "example.org".to_string(), + descriptors: RepeatedField::new(), + hits_addend: 1, + unknown_fields: Default::default(), + cached_size: Default::default(), + } + } #[test] fn operation_transition() { - let mut operation = Operation::default(); - operation.set_action(|| -> Result { Ok(200) }); + let mut operation = Operation::new(( + Rc::new(build_grpc_service_handler()), + GrpcMessage::RateLimit(build_message()), + )); assert_eq!(operation.get_state(), State::Pending); operation.trigger(); assert_eq!(operation.get_state(), State::Waiting); @@ -121,22 +186,21 @@ mod tests { #[test] fn operation_dispatcher_push_actions() { - let operation_dispatcher = OperationDispatcher { - operations: RefCell::new(vec![Operation::default()]), - }; + let operation_dispatcher = OperationDispatcher::default(); assert_eq!(operation_dispatcher.operations.borrow().len(), 1); - operation_dispatcher.push_operations(vec![Operation::default()]); + operation_dispatcher.push_operations(vec![Operation::new(( + Rc::new(build_grpc_service_handler()), + GrpcMessage::RateLimit(build_message()), + ))]); assert_eq!(operation_dispatcher.operations.borrow().len(), 2); } #[test] fn operation_dispatcher_get_current_action_state() { - let operation_dispatcher = OperationDispatcher { - operations: RefCell::new(vec![Operation::default()]), - }; + let operation_dispatcher = OperationDispatcher::default(); assert_eq!( operation_dispatcher.get_current_operation_state(), @@ -146,20 +210,22 @@ mod tests { #[test] fn operation_dispatcher_next() { - let mut operation = Operation::default(); - operation.set_action(|| -> Result { Ok(200) }); - let operation_dispatcher = OperationDispatcher { - operations: RefCell::new(vec![operation]), - }; + let operation = Operation::new(( + Rc::new(build_grpc_service_handler()), + GrpcMessage::RateLimit(build_message()), + )); + let operation_dispatcher = OperationDispatcher::default(); + operation_dispatcher.push_operations(vec![operation]); + let mut res = operation_dispatcher.next(); - assert!(res); + assert_eq!(res, Some((State::Waiting, Ok(200)))); assert_eq!( operation_dispatcher.get_current_operation_state(), Some(State::Waiting) ); res = operation_dispatcher.next(); - assert!(res); + assert_eq!(res, Some((State::Done, Ok(200)))); assert_eq!( operation_dispatcher.get_current_operation_state(), Some(State::Done) @@ -167,7 +233,7 @@ mod tests { assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(200)); res = operation_dispatcher.next(); - assert!(!res); + assert_eq!(res, None); assert_eq!(operation_dispatcher.get_current_operation_state(), None); } } From 190b59704a38d28c21343e5b56d302a5c6864af4 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Mon, 9 Sep 2024 11:35:51 +0200 Subject: [PATCH 40/66] [refactor] Specifying that it's about request messages Signed-off-by: dd di cesare --- src/operation_dispatcher.rs | 184 +++++++++++++++++++++++++----------- src/service.rs | 142 ++-------------------------- src/service/grpc_message.rs | 136 ++++++++++++++++++++++++++ 3 files changed, 271 insertions(+), 191 deletions(-) create mode 100644 src/service/grpc_message.rs diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 91efc538..fa0b5a86 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -1,11 +1,15 @@ +use crate::configuration::{Extension, ExtensionType, FailureMode}; use crate::envoy::RateLimitDescriptor; use crate::policy::Policy; -use crate::service::{GrpcMessage, GrpcServiceHandler}; +use crate::service::grpc_message::GrpcMessageRequest; +use crate::service::{GetMapValuesBytesFn, GrpcCallFn, GrpcServiceHandler}; use protobuf::RepeatedField; -use proxy_wasm::types::Status; +use proxy_wasm::hostcalls; +use proxy_wasm::types::{Bytes, MapType, Status}; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +use std::time::Duration; #[allow(dead_code)] #[derive(PartialEq, Debug, Clone)] @@ -26,44 +30,59 @@ impl State { } } -type Procedure = (Rc, GrpcMessage); +type Procedure = (Rc, GrpcMessageRequest); #[allow(dead_code)] +#[derive(Clone)] pub(crate) struct Operation { state: State, result: Result, + extension: Rc, procedure: Procedure, + grpc_call_fn: GrpcCallFn, + get_map_values_bytes_fn: GetMapValuesBytesFn, } #[allow(dead_code)] impl Operation { - pub fn new(procedure: Procedure) -> Self { + pub fn new(extension: Rc, procedure: Procedure) -> Self { Self { state: State::Pending, result: Err(Status::Empty), + extension, procedure, + grpc_call_fn, + get_map_values_bytes_fn, } } - pub fn set_action(&mut self, procedure: Procedure) { - self.procedure = procedure; - } - - pub fn trigger(&mut self) { + fn trigger(&mut self) { if let State::Done = self.state { } else { - self.result = self.procedure.0.send(self.procedure.1.clone()); + self.result = self.procedure.0.send( + self.get_map_values_bytes_fn, + self.grpc_call_fn, + self.procedure.1.clone(), + ); self.state.next(); } } - fn get_state(&self) -> State { + pub fn get_state(&self) -> State { self.state.clone() } - fn get_result(&self) -> Result { + pub fn get_result(&self) -> Result { self.result } + + pub fn get_extension_type(&self) -> ExtensionType { + self.extension.extension_type.clone() + } + + pub fn get_failure_mode(&self) -> FailureMode { + self.extension.failure_mode.clone() + } } #[allow(dead_code)] @@ -96,8 +115,15 @@ impl OperationDispatcher { policy.actions.iter().for_each(|action| { // TODO(didierofrivia): Error handling if let Some(service) = self.service_handlers.get(&action.extension) { - let message = service.build_message(policy.domain.clone(), descriptors.clone()); - operations.push(Operation::new((service.clone(), message))) + let message = GrpcMessageRequest::new( + service.get_extension_type(), + policy.domain.clone(), + descriptors.clone(), + ); + operations.push(Operation::new( + service.get_extension(), + (Rc::clone(service), message), + )) } }); self.push_operations(operations); @@ -118,16 +144,15 @@ impl OperationDispatcher { self.operations.borrow().first().unwrap().get_result() } - pub fn next(&self) -> Option<(State, Result)> { + pub fn next(&self) -> Option { let mut operations = self.operations.borrow_mut(); if let Some((i, operation)) = operations.iter_mut().enumerate().next() { if let State::Done = operation.get_state() { - let res = operation.get_result(); operations.remove(i); - Some((State::Done, res)) + operations.get(i).cloned() // The next op is now at `i` } else { operation.trigger(); - Some((operation.state.clone(), operation.result)) + Some(operation.clone()) } } else { None @@ -135,13 +160,35 @@ impl OperationDispatcher { } } +fn grpc_call_fn( + upstream_name: &str, + service_name: &str, + method_name: &str, + initial_metadata: Vec<(&str, &[u8])>, + message: Option<&[u8]>, + timeout: Duration, +) -> Result { + hostcalls::dispatch_grpc_call( + upstream_name, + service_name, + method_name, + initial_metadata, + message, + timeout, + ) +} + +fn get_map_values_bytes_fn(map_type: MapType, key: &str) -> Result, Status> { + hostcalls::get_map_value_bytes(map_type, key) +} + #[cfg(test)] mod tests { use super::*; use crate::envoy::RateLimitRequest; use std::time::Duration; - fn grpc_call( + fn grpc_call_fn_stub( _upstream_name: &str, _service_name: &str, _method_name: &str, @@ -149,15 +196,18 @@ mod tests { _message: Option<&[u8]>, _timeout: Duration, ) -> Result { - Ok(1) + Ok(200) + } + + fn get_map_values_bytes_fn_stub( + _map_type: MapType, + _key: &str, + ) -> Result, Status> { + Ok(Some(Vec::new())) } fn build_grpc_service_handler() -> GrpcServiceHandler { - GrpcServiceHandler::new( - Rc::new(Default::default()), - Rc::new(Default::default()), - Some(grpc_call), - ) + GrpcServiceHandler::new(Rc::new(Default::default()), Rc::new(Default::default())) } fn build_message() -> RateLimitRequest { @@ -170,12 +220,33 @@ mod tests { } } + fn build_operation() -> Operation { + Operation { + state: State::Pending, + result: Ok(1), + extension: Rc::new(Extension::default()), + procedure: ( + Rc::new(build_grpc_service_handler()), + GrpcMessageRequest::RateLimit(build_message()), + ), + grpc_call_fn: grpc_call_fn_stub, + get_map_values_bytes_fn: get_map_values_bytes_fn_stub, + } + } + + #[test] + fn operation_getters() { + let operation = build_operation(); + + assert_eq!(operation.get_state(), State::Pending); + assert_eq!(operation.get_extension_type(), ExtensionType::RateLimit); + assert_eq!(operation.get_failure_mode(), FailureMode::Deny); + assert_eq!(operation.get_result(), Ok(1)); + } + #[test] fn operation_transition() { - let mut operation = Operation::new(( - Rc::new(build_grpc_service_handler()), - GrpcMessage::RateLimit(build_message()), - )); + let mut operation = build_operation(); assert_eq!(operation.get_state(), State::Pending); operation.trigger(); assert_eq!(operation.get_state(), State::Waiting); @@ -188,20 +259,16 @@ mod tests { fn operation_dispatcher_push_actions() { let operation_dispatcher = OperationDispatcher::default(); - assert_eq!(operation_dispatcher.operations.borrow().len(), 1); - - operation_dispatcher.push_operations(vec![Operation::new(( - Rc::new(build_grpc_service_handler()), - GrpcMessage::RateLimit(build_message()), - ))]); + assert_eq!(operation_dispatcher.operations.borrow().len(), 0); + operation_dispatcher.push_operations(vec![build_operation()]); - assert_eq!(operation_dispatcher.operations.borrow().len(), 2); + assert_eq!(operation_dispatcher.operations.borrow().len(), 1); } #[test] fn operation_dispatcher_get_current_action_state() { let operation_dispatcher = OperationDispatcher::default(); - + operation_dispatcher.push_operations(vec![build_operation()]); assert_eq!( operation_dispatcher.get_current_operation_state(), Some(State::Pending) @@ -210,30 +277,37 @@ mod tests { #[test] fn operation_dispatcher_next() { - let operation = Operation::new(( - Rc::new(build_grpc_service_handler()), - GrpcMessage::RateLimit(build_message()), - )); let operation_dispatcher = OperationDispatcher::default(); - operation_dispatcher.push_operations(vec![operation]); + operation_dispatcher.push_operations(vec![build_operation(), build_operation()]); - let mut res = operation_dispatcher.next(); - assert_eq!(res, Some((State::Waiting, Ok(200)))); + assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(1)); assert_eq!( operation_dispatcher.get_current_operation_state(), - Some(State::Waiting) + Some(State::Pending) ); - res = operation_dispatcher.next(); - assert_eq!(res, Some((State::Done, Ok(200)))); - assert_eq!( - operation_dispatcher.get_current_operation_state(), - Some(State::Done) - ); - assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(200)); + let mut op = operation_dispatcher.next(); + assert_eq!(op.clone().unwrap().get_result(), Ok(200)); + assert_eq!(op.unwrap().get_state(), State::Waiting); + + op = operation_dispatcher.next(); + assert_eq!(op.clone().unwrap().get_result(), Ok(200)); + assert_eq!(op.unwrap().get_state(), State::Done); + + op = operation_dispatcher.next(); + assert_eq!(op.clone().unwrap().get_result(), Ok(1)); + assert_eq!(op.unwrap().get_state(), State::Pending); + + op = operation_dispatcher.next(); + assert_eq!(op.clone().unwrap().get_result(), Ok(200)); + assert_eq!(op.unwrap().get_state(), State::Waiting); + + op = operation_dispatcher.next(); + assert_eq!(op.clone().unwrap().get_result(), Ok(200)); + assert_eq!(op.unwrap().get_state(), State::Done); - res = operation_dispatcher.next(); - assert_eq!(res, None); - assert_eq!(operation_dispatcher.get_current_operation_state(), None); + op = operation_dispatcher.next(); + assert!(op.is_none()); + assert!(operation_dispatcher.get_current_operation_state().is_none()); } } diff --git a/src/service.rs b/src/service.rs index e89077f2..ef0ae9a9 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,148 +1,18 @@ pub(crate) mod auth; +pub(crate) mod grpc_message; pub(crate) mod rate_limit; use crate::configuration::{Extension, ExtensionType, FailureMode}; -use crate::envoy::{CheckRequest, RateLimitDescriptor, RateLimitRequest}; -use crate::service::auth::{AuthService, AUTH_METHOD_NAME, AUTH_SERVICE_NAME}; -use crate::service::rate_limit::{RateLimitService, RATELIMIT_METHOD_NAME, RATELIMIT_SERVICE_NAME}; +use crate::service::auth::{AUTH_METHOD_NAME, AUTH_SERVICE_NAME}; +use crate::service::grpc_message::GrpcMessageRequest; +use crate::service::rate_limit::{RATELIMIT_METHOD_NAME, RATELIMIT_SERVICE_NAME}; use crate::service::TracingHeader::{Baggage, Traceparent, Tracestate}; -use protobuf::reflect::MessageDescriptor; -use protobuf::{ - Clear, CodedInputStream, CodedOutputStream, Message, ProtobufResult, UnknownFields, -}; +use protobuf::Message; use proxy_wasm::types::{Bytes, MapType, Status}; -use std::any::Any; use std::cell::OnceCell; -use std::fmt::Debug; use std::rc::Rc; use std::time::Duration; -#[derive(Clone, Debug)] -pub enum GrpcMessage { - Auth(CheckRequest), - RateLimit(RateLimitRequest), -} - -impl Default for GrpcMessage { - fn default() -> Self { - GrpcMessage::RateLimit(RateLimitRequest::new()) - } -} - -impl Clear for GrpcMessage { - fn clear(&mut self) { - match self { - GrpcMessage::Auth(msg) => msg.clear(), - GrpcMessage::RateLimit(msg) => msg.clear(), - } - } -} - -impl Message for GrpcMessage { - fn descriptor(&self) -> &'static MessageDescriptor { - match self { - GrpcMessage::Auth(msg) => msg.descriptor(), - GrpcMessage::RateLimit(msg) => msg.descriptor(), - } - } - - fn is_initialized(&self) -> bool { - match self { - GrpcMessage::Auth(msg) => msg.is_initialized(), - GrpcMessage::RateLimit(msg) => msg.is_initialized(), - } - } - - fn merge_from(&mut self, is: &mut CodedInputStream) -> ProtobufResult<()> { - match self { - GrpcMessage::Auth(msg) => msg.merge_from(is), - GrpcMessage::RateLimit(msg) => msg.merge_from(is), - } - } - - fn write_to_with_cached_sizes(&self, os: &mut CodedOutputStream) -> ProtobufResult<()> { - match self { - GrpcMessage::Auth(msg) => msg.write_to_with_cached_sizes(os), - GrpcMessage::RateLimit(msg) => msg.write_to_with_cached_sizes(os), - } - } - - fn write_to_bytes(&self) -> ProtobufResult> { - match self { - GrpcMessage::Auth(msg) => msg.write_to_bytes(), - GrpcMessage::RateLimit(msg) => msg.write_to_bytes(), - } - } - - fn compute_size(&self) -> u32 { - match self { - GrpcMessage::Auth(msg) => msg.compute_size(), - GrpcMessage::RateLimit(msg) => msg.compute_size(), - } - } - - fn get_cached_size(&self) -> u32 { - match self { - GrpcMessage::Auth(msg) => msg.get_cached_size(), - GrpcMessage::RateLimit(msg) => msg.get_cached_size(), - } - } - - fn get_unknown_fields(&self) -> &UnknownFields { - match self { - GrpcMessage::Auth(msg) => msg.get_unknown_fields(), - GrpcMessage::RateLimit(msg) => msg.get_unknown_fields(), - } - } - - fn mut_unknown_fields(&mut self) -> &mut UnknownFields { - match self { - GrpcMessage::Auth(msg) => msg.mut_unknown_fields(), - GrpcMessage::RateLimit(msg) => msg.mut_unknown_fields(), - } - } - - fn as_any(&self) -> &dyn Any { - match self { - GrpcMessage::Auth(msg) => msg.as_any(), - GrpcMessage::RateLimit(msg) => msg.as_any(), - } - } - - fn new() -> Self - where - Self: Sized, - { - // Returning default value - GrpcMessage::default() - } - - fn default_instance() -> &'static Self - where - Self: Sized, - { - #[allow(non_upper_case_globals)] - static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; - instance.get(|| GrpcMessage::RateLimit(RateLimitRequest::new())) - } -} - -impl GrpcMessage { - // Using domain as ce_host for the time being, we might pass a DataType in the future. - pub fn new( - extension_type: ExtensionType, - domain: String, - descriptors: protobuf::RepeatedField, - ) -> Self { - match extension_type { - ExtensionType::RateLimit => { - GrpcMessage::RateLimit(RateLimitService::message(domain.clone(), descriptors)) - } - ExtensionType::Auth => GrpcMessage::Auth(AuthService::message(domain.clone())), - } - } -} - #[derive(Default)] pub struct GrpcService { #[allow(dead_code)] @@ -209,7 +79,7 @@ impl GrpcServiceHandler { &self, get_map_values_bytes_fn: GetMapValuesBytesFn, grpc_call_fn: GrpcCallFn, - message: GrpcMessage, + message: GrpcMessageRequest, ) -> Result { let msg = Message::write_to_bytes(&message).unwrap(); let metadata = self diff --git a/src/service/grpc_message.rs b/src/service/grpc_message.rs new file mode 100644 index 00000000..ad41fab6 --- /dev/null +++ b/src/service/grpc_message.rs @@ -0,0 +1,136 @@ +use crate::configuration::ExtensionType; +use crate::envoy::{CheckRequest, RateLimitDescriptor, RateLimitRequest}; +use crate::service::auth::AuthService; +use crate::service::rate_limit::RateLimitService; +use protobuf::reflect::MessageDescriptor; +use protobuf::{ + Clear, CodedInputStream, CodedOutputStream, Message, ProtobufResult, UnknownFields, +}; +use std::any::Any; + +#[derive(Clone, Debug)] +pub enum GrpcMessageRequest { + Auth(CheckRequest), + RateLimit(RateLimitRequest), +} + +impl Default for GrpcMessageRequest { + fn default() -> Self { + GrpcMessageRequest::RateLimit(RateLimitRequest::new()) + } +} + +impl Clear for GrpcMessageRequest { + fn clear(&mut self) { + match self { + GrpcMessageRequest::Auth(msg) => msg.clear(), + GrpcMessageRequest::RateLimit(msg) => msg.clear(), + } + } +} + +impl Message for GrpcMessageRequest { + fn descriptor(&self) -> &'static MessageDescriptor { + match self { + GrpcMessageRequest::Auth(msg) => msg.descriptor(), + GrpcMessageRequest::RateLimit(msg) => msg.descriptor(), + } + } + + fn is_initialized(&self) -> bool { + match self { + GrpcMessageRequest::Auth(msg) => msg.is_initialized(), + GrpcMessageRequest::RateLimit(msg) => msg.is_initialized(), + } + } + + fn merge_from(&mut self, is: &mut CodedInputStream) -> ProtobufResult<()> { + match self { + GrpcMessageRequest::Auth(msg) => msg.merge_from(is), + GrpcMessageRequest::RateLimit(msg) => msg.merge_from(is), + } + } + + fn write_to_with_cached_sizes(&self, os: &mut CodedOutputStream) -> ProtobufResult<()> { + match self { + GrpcMessageRequest::Auth(msg) => msg.write_to_with_cached_sizes(os), + GrpcMessageRequest::RateLimit(msg) => msg.write_to_with_cached_sizes(os), + } + } + + fn write_to_bytes(&self) -> ProtobufResult> { + match self { + GrpcMessageRequest::Auth(msg) => msg.write_to_bytes(), + GrpcMessageRequest::RateLimit(msg) => msg.write_to_bytes(), + } + } + + fn compute_size(&self) -> u32 { + match self { + GrpcMessageRequest::Auth(msg) => msg.compute_size(), + GrpcMessageRequest::RateLimit(msg) => msg.compute_size(), + } + } + + fn get_cached_size(&self) -> u32 { + match self { + GrpcMessageRequest::Auth(msg) => msg.get_cached_size(), + GrpcMessageRequest::RateLimit(msg) => msg.get_cached_size(), + } + } + + fn get_unknown_fields(&self) -> &UnknownFields { + match self { + GrpcMessageRequest::Auth(msg) => msg.get_unknown_fields(), + GrpcMessageRequest::RateLimit(msg) => msg.get_unknown_fields(), + } + } + + fn mut_unknown_fields(&mut self) -> &mut UnknownFields { + match self { + GrpcMessageRequest::Auth(msg) => msg.mut_unknown_fields(), + GrpcMessageRequest::RateLimit(msg) => msg.mut_unknown_fields(), + } + } + + fn as_any(&self) -> &dyn Any { + match self { + GrpcMessageRequest::Auth(msg) => msg.as_any(), + GrpcMessageRequest::RateLimit(msg) => msg.as_any(), + } + } + + fn new() -> Self + where + Self: Sized, + { + // Returning default value + GrpcMessageRequest::default() + } + + fn default_instance() -> &'static Self + where + Self: Sized, + { + #[allow(non_upper_case_globals)] + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(|| GrpcMessageRequest::RateLimit(RateLimitRequest::new())) + } +} + +impl GrpcMessageRequest { + // Using domain as ce_host for the time being, we might pass a DataType in the future. + pub fn new( + extension_type: ExtensionType, + domain: String, + descriptors: protobuf::RepeatedField, + ) -> Self { + match extension_type { + ExtensionType::RateLimit => GrpcMessageRequest::RateLimit(RateLimitService::message( + domain.clone(), + descriptors, + )), + ExtensionType::Auth => GrpcMessageRequest::Auth(AuthService::message(domain.clone())), + } + } +} From dbba0f5491d09eebfa99fe117745aab1d0964f3b Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 10 Sep 2024 16:46:38 +0200 Subject: [PATCH 41/66] [refactor, feature] Adding a new GrpcResponse type * Services also building the response Message Signed-off-by: dd di cesare --- src/envoy/mod.rs | 2 +- src/service/auth.rs | 31 +++++++- src/service/grpc_message.rs | 153 ++++++++++++++++++++++++++++++++++-- src/service/rate_limit.rs | 15 +++- 4 files changed, 188 insertions(+), 13 deletions(-) diff --git a/src/envoy/mod.rs b/src/envoy/mod.rs index db527204..6195cfae 100644 --- a/src/envoy/mod.rs +++ b/src/envoy/mod.rs @@ -37,7 +37,7 @@ pub use { AttributeContext_Request, }, base::Metadata, - external_auth::CheckRequest, + external_auth::{CheckRequest, DeniedHttpResponse, OkHttpResponse}, ratelimit::{RateLimitDescriptor, RateLimitDescriptor_Entry}, rls::{RateLimitRequest, RateLimitResponse, RateLimitResponse_Code}, }; diff --git a/src/service/auth.rs b/src/service/auth.rs index 0831cd6c..ece695cb 100644 --- a/src/service/auth.rs +++ b/src/service/auth.rs @@ -3,10 +3,12 @@ use crate::envoy::{ Address, AttributeContext, AttributeContext_HttpRequest, AttributeContext_Peer, AttributeContext_Request, CheckRequest, Metadata, SocketAddress, }; +use crate::service::grpc_message::{GrpcMessageResponse, GrpcMessageResult}; use chrono::{DateTime, FixedOffset, Timelike}; use protobuf::well_known_types::Timestamp; +use protobuf::Message; use proxy_wasm::hostcalls; -use proxy_wasm::types::MapType; +use proxy_wasm::types::{Bytes, MapType}; use std::collections::HashMap; pub const AUTH_SERVICE_NAME: &str = "envoy.service.auth.v3.Authorization"; @@ -16,10 +18,35 @@ pub struct AuthService; #[allow(dead_code)] impl AuthService { - pub fn message(ce_host: String) -> CheckRequest { + pub fn request_message(ce_host: String) -> CheckRequest { AuthService::build_check_req(ce_host) } + pub fn response_message( + res_body_bytes: &Bytes, + status_code: u32, + ) -> GrpcMessageResult { + if status_code % 2 == 0 { + AuthService::response_message_ok(res_body_bytes) + } else { + AuthService::response_message_denied(res_body_bytes) + } + } + + fn response_message_ok(res_body_bytes: &Bytes) -> GrpcMessageResult { + match Message::parse_from_bytes(res_body_bytes) { + Ok(res) => Ok(GrpcMessageResponse::AuthOk(res)), + Err(e) => Err(e), + } + } + + fn response_message_denied(res_body_bytes: &Bytes) -> GrpcMessageResult { + match Message::parse_from_bytes(res_body_bytes) { + Ok(res) => Ok(GrpcMessageResponse::AuthDenied(res)), + Err(e) => Err(e), + } + } + fn build_check_req(ce_host: String) -> CheckRequest { let mut auth_req = CheckRequest::default(); let mut attr = AttributeContext::default(); diff --git a/src/service/grpc_message.rs b/src/service/grpc_message.rs index ad41fab6..0ed70c96 100644 --- a/src/service/grpc_message.rs +++ b/src/service/grpc_message.rs @@ -1,11 +1,16 @@ use crate::configuration::ExtensionType; -use crate::envoy::{CheckRequest, RateLimitDescriptor, RateLimitRequest}; +use crate::envoy::{ + CheckRequest, DeniedHttpResponse, OkHttpResponse, RateLimitDescriptor, RateLimitRequest, + RateLimitResponse, +}; use crate::service::auth::AuthService; use crate::service::rate_limit::RateLimitService; use protobuf::reflect::MessageDescriptor; use protobuf::{ - Clear, CodedInputStream, CodedOutputStream, Message, ProtobufResult, UnknownFields, + Clear, CodedInputStream, CodedOutputStream, Message, ProtobufError, ProtobufResult, + UnknownFields, }; +use proxy_wasm::types::Bytes; use std::any::Any; #[derive(Clone, Debug)] @@ -126,11 +131,145 @@ impl GrpcMessageRequest { descriptors: protobuf::RepeatedField, ) -> Self { match extension_type { - ExtensionType::RateLimit => GrpcMessageRequest::RateLimit(RateLimitService::message( - domain.clone(), - descriptors, - )), - ExtensionType::Auth => GrpcMessageRequest::Auth(AuthService::message(domain.clone())), + ExtensionType::RateLimit => GrpcMessageRequest::RateLimit( + RateLimitService::request_message(domain.clone(), descriptors), + ), + ExtensionType::Auth => { + GrpcMessageRequest::Auth(AuthService::request_message(domain.clone())) + } } } } + +#[derive(Clone, Debug)] +pub enum GrpcMessageResponse { + AuthOk(OkHttpResponse), + AuthDenied(DeniedHttpResponse), + RateLimit(RateLimitResponse), +} + +impl Default for GrpcMessageResponse { + fn default() -> Self { + GrpcMessageResponse::RateLimit(RateLimitResponse::new()) + } +} + +impl Clear for GrpcMessageResponse { + fn clear(&mut self) { + todo!() + } +} + +impl Message for GrpcMessageResponse { + fn descriptor(&self) -> &'static MessageDescriptor { + match self { + GrpcMessageResponse::AuthOk(res) => res.descriptor(), + GrpcMessageResponse::AuthDenied(res) => res.descriptor(), + GrpcMessageResponse::RateLimit(res) => res.descriptor(), + } + } + + fn is_initialized(&self) -> bool { + match self { + GrpcMessageResponse::AuthOk(res) => res.is_initialized(), + GrpcMessageResponse::AuthDenied(res) => res.is_initialized(), + GrpcMessageResponse::RateLimit(res) => res.is_initialized(), + } + } + + fn merge_from(&mut self, is: &mut CodedInputStream) -> ProtobufResult<()> { + match self { + GrpcMessageResponse::AuthOk(res) => res.merge_from(is), + GrpcMessageResponse::AuthDenied(res) => res.merge_from(is), + GrpcMessageResponse::RateLimit(res) => res.merge_from(is), + } + } + + fn write_to_with_cached_sizes(&self, os: &mut CodedOutputStream) -> ProtobufResult<()> { + match self { + GrpcMessageResponse::AuthOk(res) => res.write_to_with_cached_sizes(os), + GrpcMessageResponse::AuthDenied(res) => res.write_to_with_cached_sizes(os), + GrpcMessageResponse::RateLimit(res) => res.write_to_with_cached_sizes(os), + } + } + + fn write_to_bytes(&self) -> ProtobufResult> { + match self { + GrpcMessageResponse::AuthOk(res) => res.write_to_bytes(), + GrpcMessageResponse::AuthDenied(res) => res.write_to_bytes(), + GrpcMessageResponse::RateLimit(res) => res.write_to_bytes(), + } + } + + fn compute_size(&self) -> u32 { + match self { + GrpcMessageResponse::AuthOk(res) => res.compute_size(), + GrpcMessageResponse::AuthDenied(res) => res.compute_size(), + GrpcMessageResponse::RateLimit(res) => res.compute_size(), + } + } + + fn get_cached_size(&self) -> u32 { + match self { + GrpcMessageResponse::AuthOk(res) => res.get_cached_size(), + GrpcMessageResponse::AuthDenied(res) => res.get_cached_size(), + GrpcMessageResponse::RateLimit(res) => res.get_cached_size(), + } + } + + fn get_unknown_fields(&self) -> &UnknownFields { + match self { + GrpcMessageResponse::AuthOk(res) => res.get_unknown_fields(), + GrpcMessageResponse::AuthDenied(res) => res.get_unknown_fields(), + GrpcMessageResponse::RateLimit(res) => res.get_unknown_fields(), + } + } + + fn mut_unknown_fields(&mut self) -> &mut UnknownFields { + match self { + GrpcMessageResponse::AuthOk(res) => res.mut_unknown_fields(), + GrpcMessageResponse::AuthDenied(res) => res.mut_unknown_fields(), + GrpcMessageResponse::RateLimit(res) => res.mut_unknown_fields(), + } + } + + fn as_any(&self) -> &dyn Any { + match self { + GrpcMessageResponse::AuthOk(res) => res.as_any(), + GrpcMessageResponse::AuthDenied(res) => res.as_any(), + GrpcMessageResponse::RateLimit(res) => res.as_any(), + } + } + + fn new() -> Self + where + Self: Sized, + { + // Returning default value + GrpcMessageResponse::default() + } + + fn default_instance() -> &'static Self + where + Self: Sized, + { + #[allow(non_upper_case_globals)] + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(|| GrpcMessageResponse::RateLimit(RateLimitResponse::new())) + } +} + +impl GrpcMessageResponse { + pub fn new( + extension_type: ExtensionType, + res_body_bytes: &Bytes, + status_code: u32, + ) -> GrpcMessageResult { + match extension_type { + ExtensionType::RateLimit => RateLimitService::response_message(res_body_bytes), + ExtensionType::Auth => AuthService::response_message(res_body_bytes, status_code), + } + } +} + +pub type GrpcMessageResult = Result; diff --git a/src/service/rate_limit.rs b/src/service/rate_limit.rs index b6b0357c..4a81884a 100644 --- a/src/service/rate_limit.rs +++ b/src/service/rate_limit.rs @@ -1,5 +1,7 @@ use crate::envoy::{RateLimitDescriptor, RateLimitRequest}; -use protobuf::RepeatedField; +use crate::service::grpc_message::{GrpcMessageResponse, GrpcMessageResult}; +use protobuf::{Message, RepeatedField}; +use proxy_wasm::types::Bytes; pub const RATELIMIT_SERVICE_NAME: &str = "envoy.service.ratelimit.v3.RateLimitService"; pub const RATELIMIT_METHOD_NAME: &str = "ShouldRateLimit"; @@ -7,7 +9,7 @@ pub const RATELIMIT_METHOD_NAME: &str = "ShouldRateLimit"; pub struct RateLimitService; impl RateLimitService { - pub fn message( + pub fn request_message( domain: String, descriptors: RepeatedField, ) -> RateLimitRequest { @@ -19,6 +21,13 @@ impl RateLimitService { cached_size: Default::default(), } } + + pub fn response_message(res_body_bytes: &Bytes) -> GrpcMessageResult { + match Message::parse_from_bytes(res_body_bytes) { + Ok(res) => Ok(GrpcMessageResponse::RateLimit(res)), + Err(e) => Err(e), + } + } } #[cfg(test)] @@ -37,7 +46,7 @@ mod tests { field.set_entries(RepeatedField::from_vec(vec![entry])); let descriptors = RepeatedField::from_vec(vec![field]); - RateLimitService::message(domain.to_string(), descriptors.clone()) + RateLimitService::request_message(domain.to_string(), descriptors.clone()) } #[test] fn builds_correct_message() { From d70d038ea5f4df9ddc6e73575af8284eecdd7547 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Thu, 12 Sep 2024 16:58:38 +0200 Subject: [PATCH 42/66] [refactor] Indexing waiting operations by `token_id` Signed-off-by: dd di cesare --- src/operation_dispatcher.rs | 79 +++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index fa0b5a86..e838e066 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -48,7 +48,7 @@ impl Operation { pub fn new(extension: Rc, procedure: Procedure) -> Self { Self { state: State::Pending, - result: Err(Status::Empty), + result: Ok(0), // Heuristics: zero represents that it's not been triggered, following `hostcalls` example extension, procedure, grpc_call_fn, @@ -56,15 +56,22 @@ impl Operation { } } - fn trigger(&mut self) { - if let State::Done = self.state { - } else { - self.result = self.procedure.0.send( - self.get_map_values_bytes_fn, - self.grpc_call_fn, - self.procedure.1.clone(), - ); - self.state.next(); + fn trigger(&mut self) -> Result { + match self.state { + State::Pending => { + self.result = self.procedure.0.send( + self.get_map_values_bytes_fn, + self.grpc_call_fn, + self.procedure.1.clone(), + ); + self.state.next(); + self.result + } + State::Waiting => { + self.state.next(); + self.result + } + State::Done => self.result, } } @@ -88,6 +95,7 @@ impl Operation { #[allow(dead_code)] pub struct OperationDispatcher { operations: RefCell>, + waiting_operations: RefCell>, // TODO(didierofrivia): Maybe keep references or Rc service_handlers: HashMap>, } @@ -96,6 +104,7 @@ impl OperationDispatcher { pub fn default() -> Self { OperationDispatcher { operations: RefCell::new(vec![]), + waiting_operations: RefCell::new(HashMap::default()), service_handlers: HashMap::default(), } } @@ -103,9 +112,14 @@ impl OperationDispatcher { Self { service_handlers, operations: RefCell::new(vec![]), + waiting_operations: RefCell::new(HashMap::new()), } } + pub fn get_operation(&self, token_id: u32) -> Option { + self.waiting_operations.borrow_mut().get(&token_id).cloned() + } + pub fn build_operations( &self, policy: &Policy, @@ -148,10 +162,18 @@ impl OperationDispatcher { let mut operations = self.operations.borrow_mut(); if let Some((i, operation)) = operations.iter_mut().enumerate().next() { if let State::Done = operation.get_state() { + if let Ok(token_id) = operation.result { + self.waiting_operations.borrow_mut().remove(&token_id); + } // If result was Err, means the operation wasn't indexed + operations.remove(i); operations.get(i).cloned() // The next op is now at `i` } else { - operation.trigger(); + if let Ok(token_id) = operation.trigger() { + self.waiting_operations + .borrow_mut() + .insert(token_id, operation.clone()); + } // TODO(didierofrivia): Decide on indexing the failed operations. Some(operation.clone()) } } else { @@ -223,7 +245,7 @@ mod tests { fn build_operation() -> Operation { Operation { state: State::Pending, - result: Ok(1), + result: Ok(0), extension: Rc::new(Extension::default()), procedure: ( Rc::new(build_grpc_service_handler()), @@ -241,16 +263,19 @@ mod tests { assert_eq!(operation.get_state(), State::Pending); assert_eq!(operation.get_extension_type(), ExtensionType::RateLimit); assert_eq!(operation.get_failure_mode(), FailureMode::Deny); - assert_eq!(operation.get_result(), Ok(1)); + assert_eq!(operation.get_result(), Ok(0)); } #[test] fn operation_transition() { let mut operation = build_operation(); + assert_eq!(operation.result, Ok(0)); assert_eq!(operation.get_state(), State::Pending); - operation.trigger(); + let mut res = operation.trigger(); + assert_eq!(res, Ok(200)); assert_eq!(operation.get_state(), State::Waiting); - operation.trigger(); + res = operation.trigger(); + assert_eq!(res, Ok(200)); assert_eq!(operation.result, Ok(200)); assert_eq!(operation.get_state(), State::Done); } @@ -280,27 +305,43 @@ mod tests { let operation_dispatcher = OperationDispatcher::default(); operation_dispatcher.push_operations(vec![build_operation(), build_operation()]); - assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(1)); + assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(0)); assert_eq!( operation_dispatcher.get_current_operation_state(), Some(State::Pending) ); + assert_eq!( + operation_dispatcher.waiting_operations.borrow_mut().len(), + 0 + ); let mut op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(200)); assert_eq!(op.unwrap().get_state(), State::Waiting); + assert_eq!( + operation_dispatcher.waiting_operations.borrow_mut().len(), + 1 + ); op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(200)); assert_eq!(op.unwrap().get_state(), State::Done); op = operation_dispatcher.next(); - assert_eq!(op.clone().unwrap().get_result(), Ok(1)); + assert_eq!(op.clone().unwrap().get_result(), Ok(0)); assert_eq!(op.unwrap().get_state(), State::Pending); + assert_eq!( + operation_dispatcher.waiting_operations.borrow_mut().len(), + 0 + ); op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(200)); assert_eq!(op.unwrap().get_state(), State::Waiting); + assert_eq!( + operation_dispatcher.waiting_operations.borrow_mut().len(), + 1 + ); op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(200)); @@ -309,5 +350,9 @@ mod tests { op = operation_dispatcher.next(); assert!(op.is_none()); assert!(operation_dispatcher.get_current_operation_state().is_none()); + assert_eq!( + operation_dispatcher.waiting_operations.borrow_mut().len(), + 0 + ); } } From 93c980d8b7b666e7d4e79ef9a72db0cf935fc187 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Fri, 13 Sep 2024 12:58:06 +0200 Subject: [PATCH 43/66] [refactor] Wiring filter with dispatcher Signed-off-by: dd di cesare --- src/filter/http_context.rs | 99 +++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 39 deletions(-) diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index 2290266d..b4430ea8 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -1,9 +1,9 @@ -use crate::configuration::{FailureMode, FilterConfig}; +use crate::configuration::{ExtensionType, FailureMode, FilterConfig}; use crate::envoy::{RateLimitResponse, RateLimitResponse_Code}; use crate::operation_dispatcher::OperationDispatcher; use crate::policy::Policy; +use crate::service::grpc_message::GrpcMessageResponse; use log::{debug, warn}; -use protobuf::Message; use proxy_wasm::traits::{Context, HttpContext}; use proxy_wasm::types::Action; use std::rc::Rc; @@ -79,6 +79,40 @@ impl Filter { FailureMode::Allow => self.resume_http_request(), } } + + fn process_ratelimit_grpc_response(&mut self, rl_resp: GrpcMessageResponse) { + match rl_resp { + GrpcMessageResponse::RateLimit(RateLimitResponse { + overall_code: RateLimitResponse_Code::UNKNOWN, + .. + }) => { + self.handle_error_on_grpc_response(); + } + GrpcMessageResponse::RateLimit(RateLimitResponse { + overall_code: RateLimitResponse_Code::OVER_LIMIT, + response_headers_to_add: rl_headers, + .. + }) => { + let mut response_headers = vec![]; + for header in &rl_headers { + response_headers.push((header.get_key(), header.get_value())); + } + self.send_http_response(429, response_headers, Some(b"Too Many Requests\n")); + } + GrpcMessageResponse::RateLimit(RateLimitResponse { + overall_code: RateLimitResponse_Code::OK, + response_headers_to_add: additional_headers, + .. + }) => { + for header in additional_headers { + self.response_headers_to_add + .push((header.key, header.value)); + } + } + _ => {} + } + self.operation_dispatcher.next(); + } } impl HttpContext for Filter { @@ -133,46 +167,33 @@ impl Context for Filter { } }; - let rl_resp: RateLimitResponse = match Message::parse_from_bytes(&res_body_bytes) { - Ok(res) => res, - Err(e) => { - warn!("failed to parse grpc response body into RateLimitResponse message: {e}"); - self.handle_error_on_grpc_response(); - return; - } - }; - - match rl_resp { - RateLimitResponse { - overall_code: RateLimitResponse_Code::UNKNOWN, - .. - } => { - self.handle_error_on_grpc_response(); - return; - } - RateLimitResponse { - overall_code: RateLimitResponse_Code::OVER_LIMIT, - response_headers_to_add: rl_headers, - .. - } => { - let mut response_headers = vec![]; - for header in &rl_headers { - response_headers.push((header.get_key(), header.get_value())); + if let Some(operation) = self.operation_dispatcher.get_operation(token_id) { + let res = match GrpcMessageResponse::new( + operation.get_extension_type(), + &res_body_bytes, + status_code, + ) { + Ok(res) => res, + Err(e) => { + warn!( + "failed to parse grpc response body into GrpcMessageResponse message: {e}" + ); + self.handle_error_on_grpc_response(); + return; } - self.send_http_response(429, response_headers, Some(b"Too Many Requests\n")); - return; + }; + match operation.get_extension_type() { + ExtensionType::Auth => {} + ExtensionType::RateLimit => self.process_ratelimit_grpc_response(res), } - RateLimitResponse { - overall_code: RateLimitResponse_Code::OK, - response_headers_to_add: additional_headers, - .. - } => { - for header in additional_headers { - self.response_headers_to_add - .push((header.key, header.value)); - } + + if let Some(_op) = self.operation_dispatcher.next() { + } else { + self.resume_http_request() } + } else { + warn!("No Operation found with token_id: {token_id}"); + self.handle_error_on_grpc_response(); } - self.resume_http_request(); } } From 98d3754e7f6ebee9dd79fed375ac2ba52ccfe82a Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Fri, 13 Sep 2024 18:41:29 +0200 Subject: [PATCH 44/66] [refactor] Passing by references instead of cloning Signed-off-by: dd di cesare --- src/operation_dispatcher.rs | 34 +++++++++++++++++----------------- src/service/grpc_message.rs | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index e838e066..ce4143c3 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -75,20 +75,20 @@ impl Operation { } } - pub fn get_state(&self) -> State { - self.state.clone() + pub fn get_state(&self) -> &State { + &self.state } pub fn get_result(&self) -> Result { self.result } - pub fn get_extension_type(&self) -> ExtensionType { - self.extension.extension_type.clone() + pub fn get_extension_type(&self) -> &ExtensionType { + &self.extension.extension_type } - pub fn get_failure_mode(&self) -> FailureMode { - self.extension.failure_mode.clone() + pub fn get_failure_mode(&self) -> &FailureMode { + &self.extension.failure_mode } } @@ -260,9 +260,9 @@ mod tests { fn operation_getters() { let operation = build_operation(); - assert_eq!(operation.get_state(), State::Pending); - assert_eq!(operation.get_extension_type(), ExtensionType::RateLimit); - assert_eq!(operation.get_failure_mode(), FailureMode::Deny); + assert_eq!(*operation.get_state(), State::Pending); + assert_eq!(*operation.get_extension_type(), ExtensionType::RateLimit); + assert_eq!(*operation.get_failure_mode(), FailureMode::Deny); assert_eq!(operation.get_result(), Ok(0)); } @@ -270,14 +270,14 @@ mod tests { fn operation_transition() { let mut operation = build_operation(); assert_eq!(operation.result, Ok(0)); - assert_eq!(operation.get_state(), State::Pending); + assert_eq!(*operation.get_state(), State::Pending); let mut res = operation.trigger(); assert_eq!(res, Ok(200)); - assert_eq!(operation.get_state(), State::Waiting); + assert_eq!(*operation.get_state(), State::Waiting); res = operation.trigger(); assert_eq!(res, Ok(200)); assert_eq!(operation.result, Ok(200)); - assert_eq!(operation.get_state(), State::Done); + assert_eq!(*operation.get_state(), State::Done); } #[test] @@ -317,7 +317,7 @@ mod tests { let mut op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(200)); - assert_eq!(op.unwrap().get_state(), State::Waiting); + assert_eq!(*op.unwrap().get_state(), State::Waiting); assert_eq!( operation_dispatcher.waiting_operations.borrow_mut().len(), 1 @@ -325,11 +325,11 @@ mod tests { op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(200)); - assert_eq!(op.unwrap().get_state(), State::Done); + assert_eq!(*op.unwrap().get_state(), State::Done); op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(0)); - assert_eq!(op.unwrap().get_state(), State::Pending); + assert_eq!(*op.unwrap().get_state(), State::Pending); assert_eq!( operation_dispatcher.waiting_operations.borrow_mut().len(), 0 @@ -337,7 +337,7 @@ mod tests { op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(200)); - assert_eq!(op.unwrap().get_state(), State::Waiting); + assert_eq!(*op.unwrap().get_state(), State::Waiting); assert_eq!( operation_dispatcher.waiting_operations.borrow_mut().len(), 1 @@ -345,7 +345,7 @@ mod tests { op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(200)); - assert_eq!(op.unwrap().get_state(), State::Done); + assert_eq!(*op.unwrap().get_state(), State::Done); op = operation_dispatcher.next(); assert!(op.is_none()); diff --git a/src/service/grpc_message.rs b/src/service/grpc_message.rs index 0ed70c96..0a6f514f 100644 --- a/src/service/grpc_message.rs +++ b/src/service/grpc_message.rs @@ -261,7 +261,7 @@ impl Message for GrpcMessageResponse { impl GrpcMessageResponse { pub fn new( - extension_type: ExtensionType, + extension_type: &ExtensionType, res_body_bytes: &Bytes, status_code: u32, ) -> GrpcMessageResult { From 49e4fec036ca9031f7672512140263fefd6e78e4 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Fri, 13 Sep 2024 18:41:56 +0200 Subject: [PATCH 45/66] [refactor] Using action `failure_mode` when processing grpc error Signed-off-by: dd di cesare --- src/filter/http_context.rs | 46 +++++++++++++++++--------------------- src/service.rs | 1 + 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index b4430ea8..28153680 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -63,16 +63,8 @@ impl Filter { } } - fn handle_error_on_grpc_response(&self) { - // todo(adam-cattermole): We need a method of knowing which service is the one currently - // being used (the current action) so that we can get the failure mode - let rls = self - .config - .services - .values() - .next() - .expect("expect a value"); - match rls.failure_mode() { + fn handle_error_on_grpc_response(&self, failure_mode: &FailureMode) { + match failure_mode { FailureMode::Deny => { self.send_http_response(500, vec![], Some(b"Internal Server Error.\n")) } @@ -80,13 +72,17 @@ impl Filter { } } - fn process_ratelimit_grpc_response(&mut self, rl_resp: GrpcMessageResponse) { + fn process_ratelimit_grpc_response( + &mut self, + rl_resp: GrpcMessageResponse, + failure_mode: &FailureMode, + ) { match rl_resp { GrpcMessageResponse::RateLimit(RateLimitResponse { overall_code: RateLimitResponse_Code::UNKNOWN, .. }) => { - self.handle_error_on_grpc_response(); + self.handle_error_on_grpc_response(failure_mode); } GrpcMessageResponse::RateLimit(RateLimitResponse { overall_code: RateLimitResponse_Code::OVER_LIMIT, @@ -158,16 +154,16 @@ impl Context for Filter { self.context_id ); - let res_body_bytes = match self.get_grpc_call_response_body(0, resp_size) { - Some(bytes) => bytes, - None => { - warn!("grpc response body is empty!"); - self.handle_error_on_grpc_response(); - return; - } - }; - if let Some(operation) = self.operation_dispatcher.get_operation(token_id) { + let failure_mode = &operation.get_failure_mode(); + let res_body_bytes = match self.get_grpc_call_response_body(0, resp_size) { + Some(bytes) => bytes, + None => { + warn!("grpc response body is empty!"); + self.handle_error_on_grpc_response(failure_mode); + return; + } + }; let res = match GrpcMessageResponse::new( operation.get_extension_type(), &res_body_bytes, @@ -178,13 +174,13 @@ impl Context for Filter { warn!( "failed to parse grpc response body into GrpcMessageResponse message: {e}" ); - self.handle_error_on_grpc_response(); + self.handle_error_on_grpc_response(failure_mode); return; } }; match operation.get_extension_type() { - ExtensionType::Auth => {} - ExtensionType::RateLimit => self.process_ratelimit_grpc_response(res), + ExtensionType::Auth => {} // TODO(didierofrivia): Process auth grpc response. + ExtensionType::RateLimit => self.process_ratelimit_grpc_response(res, failure_mode), } if let Some(_op) = self.operation_dispatcher.next() { @@ -193,7 +189,7 @@ impl Context for Filter { } } else { warn!("No Operation found with token_id: {token_id}"); - self.handle_error_on_grpc_response(); + self.handle_error_on_grpc_response(&FailureMode::Deny); // TODO(didierofrivia): Decide on what's the default failure mode } } } diff --git a/src/service.rs b/src/service.rs index ef0ae9a9..e712dfdb 100644 --- a/src/service.rs +++ b/src/service.rs @@ -46,6 +46,7 @@ impl GrpcService { fn method(&self) -> &str { self.method } + #[allow(dead_code)] pub fn failure_mode(&self) -> &FailureMode { &self.extension.failure_mode } From b0329d46c8e67435b965d365d3ed9152499a0b7a Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Wed, 18 Sep 2024 16:04:12 +0200 Subject: [PATCH 46/66] [fix] Changed behaviour, removing and triggering next op at same step * When Operation `n` is Done, it's removed and it triggers Op n+1 Signed-off-by: dd di cesare --- src/operation_dispatcher.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index ce4143c3..88c399f1 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -165,16 +165,18 @@ impl OperationDispatcher { if let Ok(token_id) = operation.result { self.waiting_operations.borrow_mut().remove(&token_id); } // If result was Err, means the operation wasn't indexed - operations.remove(i); - operations.get(i).cloned() // The next op is now at `i` - } else { + // The next op is now at `i` + } + if let Some(operation) = operations.get_mut(i) { if let Ok(token_id) = operation.trigger() { self.waiting_operations .borrow_mut() .insert(token_id, operation.clone()); } // TODO(didierofrivia): Decide on indexing the failed operations. Some(operation.clone()) + } else { + None } } else { None @@ -327,14 +329,6 @@ mod tests { assert_eq!(op.clone().unwrap().get_result(), Ok(200)); assert_eq!(*op.unwrap().get_state(), State::Done); - op = operation_dispatcher.next(); - assert_eq!(op.clone().unwrap().get_result(), Ok(0)); - assert_eq!(*op.unwrap().get_state(), State::Pending); - assert_eq!( - operation_dispatcher.waiting_operations.borrow_mut().len(), - 0 - ); - op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(200)); assert_eq!(*op.unwrap().get_state(), State::Waiting); @@ -346,6 +340,10 @@ mod tests { op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(200)); assert_eq!(*op.unwrap().get_state(), State::Done); + assert_eq!( + operation_dispatcher.waiting_operations.borrow_mut().len(), + 1 + ); op = operation_dispatcher.next(); assert!(op.is_none()); From c262342e3b914f26a1a4265ef5f8f152c45f1a90 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Wed, 18 Sep 2024 16:05:46 +0200 Subject: [PATCH 47/66] [refactor] Renaming rlp to policy for consistency Signed-off-by: dd di cesare --- src/filter/http_context.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index 28153680..fe4832a2 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -29,8 +29,8 @@ impl Filter { } } - fn process_rate_limit_policy(&self, rlp: &Policy) -> Action { - let descriptors = rlp.build_descriptors(self); + fn process_policy(&self, policy: &Policy) -> Action { + let descriptors = policy.build_descriptors(self); if descriptors.is_empty() { debug!( "#{} process_rate_limit_policy: empty descriptors", @@ -39,7 +39,8 @@ impl Filter { return Action::Continue; } - self.operation_dispatcher.build_operations(rlp, descriptors); + self.operation_dispatcher + .build_operations(policy, descriptors); if let Some(operation) = self.operation_dispatcher.next() { match operation.get_result() { @@ -127,9 +128,12 @@ impl HttpContext for Filter { ); Action::Continue } - Some(rlp) => { - debug!("#{} ratelimitpolicy selected {}", self.context_id, rlp.name); - self.process_rate_limit_policy(rlp) + Some(policy) => { + debug!( + "#{} ratelimitpolicy selected {}", + self.context_id, policy.name + ); + self.process_policy(policy) } } } From 4272ff78c6b7202c9b76a5c2b9ce1f0b494106c0 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Wed, 18 Sep 2024 17:03:09 +0200 Subject: [PATCH 48/66] [refactor] Explicit testing Signed-off-by: dd di cesare --- src/operation_dispatcher.rs | 68 ++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 88c399f1..807b8222 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -212,7 +212,7 @@ mod tests { use crate::envoy::RateLimitRequest; use std::time::Duration; - fn grpc_call_fn_stub( + fn default_grpc_call_fn_stub( _upstream_name: &str, _service_name: &str, _method_name: &str, @@ -244,11 +244,15 @@ mod tests { } } - fn build_operation() -> Operation { + fn build_operation(grpc_call_fn_stub: GrpcCallFn, extension_type: ExtensionType) -> Operation { Operation { state: State::Pending, result: Ok(0), - extension: Rc::new(Extension::default()), + extension: Rc::new(Extension { + extension_type, + endpoint: "local".to_string(), + failure_mode: FailureMode::Deny, + }), procedure: ( Rc::new(build_grpc_service_handler()), GrpcMessageRequest::RateLimit(build_message()), @@ -260,7 +264,7 @@ mod tests { #[test] fn operation_getters() { - let operation = build_operation(); + let operation = build_operation(default_grpc_call_fn_stub, ExtensionType::RateLimit); assert_eq!(*operation.get_state(), State::Pending); assert_eq!(*operation.get_extension_type(), ExtensionType::RateLimit); @@ -270,7 +274,7 @@ mod tests { #[test] fn operation_transition() { - let mut operation = build_operation(); + let mut operation = build_operation(default_grpc_call_fn_stub, ExtensionType::RateLimit); assert_eq!(operation.result, Ok(0)); assert_eq!(*operation.get_state(), State::Pending); let mut res = operation.trigger(); @@ -287,7 +291,10 @@ mod tests { let operation_dispatcher = OperationDispatcher::default(); assert_eq!(operation_dispatcher.operations.borrow().len(), 0); - operation_dispatcher.push_operations(vec![build_operation()]); + operation_dispatcher.push_operations(vec![build_operation( + default_grpc_call_fn_stub, + ExtensionType::RateLimit, + )]); assert_eq!(operation_dispatcher.operations.borrow().len(), 1); } @@ -295,7 +302,10 @@ mod tests { #[test] fn operation_dispatcher_get_current_action_state() { let operation_dispatcher = OperationDispatcher::default(); - operation_dispatcher.push_operations(vec![build_operation()]); + operation_dispatcher.push_operations(vec![build_operation( + default_grpc_call_fn_stub, + ExtensionType::RateLimit, + )]); assert_eq!( operation_dispatcher.get_current_operation_state(), Some(State::Pending) @@ -305,7 +315,33 @@ mod tests { #[test] fn operation_dispatcher_next() { let operation_dispatcher = OperationDispatcher::default(); - operation_dispatcher.push_operations(vec![build_operation(), build_operation()]); + + fn grpc_call_fn_stub_66( + _upstream_name: &str, + _service_name: &str, + _method_name: &str, + _initial_metadata: Vec<(&str, &[u8])>, + _message: Option<&[u8]>, + _timeout: Duration, + ) -> Result { + Ok(66) + } + + fn grpc_call_fn_stub_77( + _upstream_name: &str, + _service_name: &str, + _method_name: &str, + _initial_metadata: Vec<(&str, &[u8])>, + _message: Option<&[u8]>, + _timeout: Duration, + ) -> Result { + Ok(77) + } + + operation_dispatcher.push_operations(vec![ + build_operation(grpc_call_fn_stub_66, ExtensionType::RateLimit), + build_operation(grpc_call_fn_stub_77, ExtensionType::Auth), + ]); assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(0)); assert_eq!( @@ -318,7 +354,11 @@ mod tests { ); let mut op = operation_dispatcher.next(); - assert_eq!(op.clone().unwrap().get_result(), Ok(200)); + assert_eq!(op.clone().unwrap().get_result(), Ok(66)); + assert_eq!( + *op.clone().unwrap().get_extension_type(), + ExtensionType::RateLimit + ); assert_eq!(*op.unwrap().get_state(), State::Waiting); assert_eq!( operation_dispatcher.waiting_operations.borrow_mut().len(), @@ -326,11 +366,15 @@ mod tests { ); op = operation_dispatcher.next(); - assert_eq!(op.clone().unwrap().get_result(), Ok(200)); + assert_eq!(op.clone().unwrap().get_result(), Ok(66)); assert_eq!(*op.unwrap().get_state(), State::Done); op = operation_dispatcher.next(); - assert_eq!(op.clone().unwrap().get_result(), Ok(200)); + assert_eq!(op.clone().unwrap().get_result(), Ok(77)); + assert_eq!( + *op.clone().unwrap().get_extension_type(), + ExtensionType::Auth + ); assert_eq!(*op.unwrap().get_state(), State::Waiting); assert_eq!( operation_dispatcher.waiting_operations.borrow_mut().len(), @@ -338,7 +382,7 @@ mod tests { ); op = operation_dispatcher.next(); - assert_eq!(op.clone().unwrap().get_result(), Ok(200)); + assert_eq!(op.clone().unwrap().get_result(), Ok(77)); assert_eq!(*op.unwrap().get_state(), State::Done); assert_eq!( operation_dispatcher.waiting_operations.borrow_mut().len(), From 71257542a49fdd9b50e2cc84df8b148043345473 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Thu, 19 Sep 2024 16:18:42 +0200 Subject: [PATCH 49/66] [fix] Indexing waiting operations only when triggered Pending => Waiting Signed-off-by: dd di cesare --- src/operation_dispatcher.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 807b8222..764a3d08 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -170,10 +170,13 @@ impl OperationDispatcher { } if let Some(operation) = operations.get_mut(i) { if let Ok(token_id) = operation.trigger() { - self.waiting_operations - .borrow_mut() - .insert(token_id, operation.clone()); - } // TODO(didierofrivia): Decide on indexing the failed operations. + if *operation.get_state() == State::Waiting { + // We index only if it was just transitioned to Waiting after triggering + self.waiting_operations + .borrow_mut() + .insert(token_id, operation.clone()); + } // TODO(didierofrivia): Decide on indexing the failed operations. + } Some(operation.clone()) } else { None From 6675cab8834a4d0acb3beb136431e543b30c8649 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 20 Sep 2024 10:56:53 +0100 Subject: [PATCH 50/66] Process auth CheckResponse Signed-off-by: Adam Cattermole --- src/envoy/mod.rs | 2 +- src/filter/http_context.rs | 78 ++++++++++++++++++++++++++--------- src/service/auth.rs | 22 +--------- src/service/grpc_message.rs | 39 ++++++------------ utils/deploy/envoy-notls.yaml | 2 +- utils/deploy/envoy-tls.yaml | 2 +- 6 files changed, 77 insertions(+), 68 deletions(-) diff --git a/src/envoy/mod.rs b/src/envoy/mod.rs index 6195cfae..60810273 100644 --- a/src/envoy/mod.rs +++ b/src/envoy/mod.rs @@ -37,7 +37,7 @@ pub use { AttributeContext_Request, }, base::Metadata, - external_auth::{CheckRequest, DeniedHttpResponse, OkHttpResponse}, + external_auth::{CheckRequest, CheckResponse, CheckResponse_oneof_http_response}, ratelimit::{RateLimitDescriptor, RateLimitDescriptor_Entry}, rls::{RateLimitRequest, RateLimitResponse, RateLimitResponse_Code}, }; diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index fe4832a2..a658b8b0 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -1,5 +1,5 @@ use crate::configuration::{ExtensionType, FailureMode, FilterConfig}; -use crate::envoy::{RateLimitResponse, RateLimitResponse_Code}; +use crate::envoy::{CheckResponse_oneof_http_response, RateLimitResponse, RateLimitResponse_Code}; use crate::operation_dispatcher::OperationDispatcher; use crate::policy::Policy; use crate::service::grpc_message::GrpcMessageResponse; @@ -45,14 +45,11 @@ impl Filter { if let Some(operation) = self.operation_dispatcher.next() { match operation.get_result() { Ok(call_id) => { - debug!( - "#{} initiated gRPC call (id# {}) to Limitador", - self.context_id, call_id - ); + debug!("#{} initiated gRPC call (id# {})", self.context_id, call_id); Action::Pause } Err(e) => { - warn!("gRPC call to Limitador failed! {e:?}"); + warn!("gRPC call failed! {e:?}"); if let FailureMode::Deny = operation.get_failure_mode() { self.send_http_response(500, vec![], Some(b"Internal Server Error.\n")) } @@ -110,6 +107,52 @@ impl Filter { } self.operation_dispatcher.next(); } + + fn process_auth_grpc_response( + &mut self, + auth_resp: GrpcMessageResponse, + failure_mode: &FailureMode, + ) { + if let GrpcMessageResponse::Auth(check_response) = auth_resp { + match check_response.http_response { + Some(CheckResponse_oneof_http_response::ok_response(ok_response)) => { + debug!("Handling OkHttpResponse..."); + + ok_response + .get_response_headers_to_add() + .iter() + .for_each(|header| { + self.add_http_response_header( + header.get_header().get_key(), + header.get_header().get_value(), + ) + }); + } + Some(CheckResponse_oneof_http_response::denied_response(denied_response)) => { + debug!("Handling DeniedHttpResponse..."); + + let mut response_headers = vec![]; + denied_response.get_headers().iter().for_each(|header| { + response_headers.push(( + header.get_header().get_key(), + header.get_header().get_value(), + )) + }); + self.send_http_response( + denied_response.get_status().code as u32, + response_headers, + Some(denied_response.get_body().as_ref()), + ); + return; + } + None => { + self.handle_error_on_grpc_response(failure_mode); + return; + } + } + } + self.operation_dispatcher.next(); + } } impl HttpContext for Filter { @@ -168,22 +211,19 @@ impl Context for Filter { return; } }; - let res = match GrpcMessageResponse::new( - operation.get_extension_type(), - &res_body_bytes, - status_code, - ) { - Ok(res) => res, - Err(e) => { - warn!( + let res = + match GrpcMessageResponse::new(operation.get_extension_type(), &res_body_bytes) { + Ok(res) => res, + Err(e) => { + warn!( "failed to parse grpc response body into GrpcMessageResponse message: {e}" ); - self.handle_error_on_grpc_response(failure_mode); - return; - } - }; + self.handle_error_on_grpc_response(failure_mode); + return; + } + }; match operation.get_extension_type() { - ExtensionType::Auth => {} // TODO(didierofrivia): Process auth grpc response. + ExtensionType::Auth => self.process_auth_grpc_response(res, failure_mode), ExtensionType::RateLimit => self.process_ratelimit_grpc_response(res, failure_mode), } diff --git a/src/service/auth.rs b/src/service/auth.rs index ece695cb..b2261ac1 100644 --- a/src/service/auth.rs +++ b/src/service/auth.rs @@ -22,27 +22,9 @@ impl AuthService { AuthService::build_check_req(ce_host) } - pub fn response_message( - res_body_bytes: &Bytes, - status_code: u32, - ) -> GrpcMessageResult { - if status_code % 2 == 0 { - AuthService::response_message_ok(res_body_bytes) - } else { - AuthService::response_message_denied(res_body_bytes) - } - } - - fn response_message_ok(res_body_bytes: &Bytes) -> GrpcMessageResult { - match Message::parse_from_bytes(res_body_bytes) { - Ok(res) => Ok(GrpcMessageResponse::AuthOk(res)), - Err(e) => Err(e), - } - } - - fn response_message_denied(res_body_bytes: &Bytes) -> GrpcMessageResult { + pub fn response_message(res_body_bytes: &Bytes) -> GrpcMessageResult { match Message::parse_from_bytes(res_body_bytes) { - Ok(res) => Ok(GrpcMessageResponse::AuthDenied(res)), + Ok(res) => Ok(GrpcMessageResponse::Auth(res)), Err(e) => Err(e), } } diff --git a/src/service/grpc_message.rs b/src/service/grpc_message.rs index 0a6f514f..6396c5f5 100644 --- a/src/service/grpc_message.rs +++ b/src/service/grpc_message.rs @@ -1,7 +1,6 @@ use crate::configuration::ExtensionType; use crate::envoy::{ - CheckRequest, DeniedHttpResponse, OkHttpResponse, RateLimitDescriptor, RateLimitRequest, - RateLimitResponse, + CheckRequest, CheckResponse, RateLimitDescriptor, RateLimitRequest, RateLimitResponse, }; use crate::service::auth::AuthService; use crate::service::rate_limit::RateLimitService; @@ -143,8 +142,7 @@ impl GrpcMessageRequest { #[derive(Clone, Debug)] pub enum GrpcMessageResponse { - AuthOk(OkHttpResponse), - AuthDenied(DeniedHttpResponse), + Auth(CheckResponse), RateLimit(RateLimitResponse), } @@ -163,80 +161,70 @@ impl Clear for GrpcMessageResponse { impl Message for GrpcMessageResponse { fn descriptor(&self) -> &'static MessageDescriptor { match self { - GrpcMessageResponse::AuthOk(res) => res.descriptor(), - GrpcMessageResponse::AuthDenied(res) => res.descriptor(), + GrpcMessageResponse::Auth(res) => res.descriptor(), GrpcMessageResponse::RateLimit(res) => res.descriptor(), } } fn is_initialized(&self) -> bool { match self { - GrpcMessageResponse::AuthOk(res) => res.is_initialized(), - GrpcMessageResponse::AuthDenied(res) => res.is_initialized(), + GrpcMessageResponse::Auth(res) => res.is_initialized(), GrpcMessageResponse::RateLimit(res) => res.is_initialized(), } } fn merge_from(&mut self, is: &mut CodedInputStream) -> ProtobufResult<()> { match self { - GrpcMessageResponse::AuthOk(res) => res.merge_from(is), - GrpcMessageResponse::AuthDenied(res) => res.merge_from(is), + GrpcMessageResponse::Auth(res) => res.merge_from(is), GrpcMessageResponse::RateLimit(res) => res.merge_from(is), } } fn write_to_with_cached_sizes(&self, os: &mut CodedOutputStream) -> ProtobufResult<()> { match self { - GrpcMessageResponse::AuthOk(res) => res.write_to_with_cached_sizes(os), - GrpcMessageResponse::AuthDenied(res) => res.write_to_with_cached_sizes(os), + GrpcMessageResponse::Auth(res) => res.write_to_with_cached_sizes(os), GrpcMessageResponse::RateLimit(res) => res.write_to_with_cached_sizes(os), } } fn write_to_bytes(&self) -> ProtobufResult> { match self { - GrpcMessageResponse::AuthOk(res) => res.write_to_bytes(), - GrpcMessageResponse::AuthDenied(res) => res.write_to_bytes(), + GrpcMessageResponse::Auth(res) => res.write_to_bytes(), GrpcMessageResponse::RateLimit(res) => res.write_to_bytes(), } } fn compute_size(&self) -> u32 { match self { - GrpcMessageResponse::AuthOk(res) => res.compute_size(), - GrpcMessageResponse::AuthDenied(res) => res.compute_size(), + GrpcMessageResponse::Auth(res) => res.compute_size(), GrpcMessageResponse::RateLimit(res) => res.compute_size(), } } fn get_cached_size(&self) -> u32 { match self { - GrpcMessageResponse::AuthOk(res) => res.get_cached_size(), - GrpcMessageResponse::AuthDenied(res) => res.get_cached_size(), + GrpcMessageResponse::Auth(res) => res.get_cached_size(), GrpcMessageResponse::RateLimit(res) => res.get_cached_size(), } } fn get_unknown_fields(&self) -> &UnknownFields { match self { - GrpcMessageResponse::AuthOk(res) => res.get_unknown_fields(), - GrpcMessageResponse::AuthDenied(res) => res.get_unknown_fields(), + GrpcMessageResponse::Auth(res) => res.get_unknown_fields(), GrpcMessageResponse::RateLimit(res) => res.get_unknown_fields(), } } fn mut_unknown_fields(&mut self) -> &mut UnknownFields { match self { - GrpcMessageResponse::AuthOk(res) => res.mut_unknown_fields(), - GrpcMessageResponse::AuthDenied(res) => res.mut_unknown_fields(), + GrpcMessageResponse::Auth(res) => res.mut_unknown_fields(), GrpcMessageResponse::RateLimit(res) => res.mut_unknown_fields(), } } fn as_any(&self) -> &dyn Any { match self { - GrpcMessageResponse::AuthOk(res) => res.as_any(), - GrpcMessageResponse::AuthDenied(res) => res.as_any(), + GrpcMessageResponse::Auth(res) => res.as_any(), GrpcMessageResponse::RateLimit(res) => res.as_any(), } } @@ -263,11 +251,10 @@ impl GrpcMessageResponse { pub fn new( extension_type: &ExtensionType, res_body_bytes: &Bytes, - status_code: u32, ) -> GrpcMessageResult { match extension_type { ExtensionType::RateLimit => RateLimitService::response_message(res_body_bytes), - ExtensionType::Auth => AuthService::response_message(res_body_bytes, status_code), + ExtensionType::Auth => AuthService::response_message(res_body_bytes), } } } diff --git a/utils/deploy/envoy-notls.yaml b/utils/deploy/envoy-notls.yaml index e3f8146c..6c4cc277 100644 --- a/utils/deploy/envoy-notls.yaml +++ b/utils/deploy/envoy-notls.yaml @@ -150,7 +150,7 @@ data: "policies": [ { "name": "auth-ns-A/auth-name-A", - "domain": "auth-ns-A/auth-name-A", + "domain": "effective-route-1", "hostnames": [ "*.a.auth.com" ], diff --git a/utils/deploy/envoy-tls.yaml b/utils/deploy/envoy-tls.yaml index dbdd4500..faba5e26 100644 --- a/utils/deploy/envoy-tls.yaml +++ b/utils/deploy/envoy-tls.yaml @@ -159,7 +159,7 @@ data: "policies": [ { "name": "auth-ns-A/auth-name-A", - "domain": "auth-ns-A/auth-name-A", + "domain": "effective-route-1", "hostnames": [ "*.a.auth.com" ], From 41f85eaf5f7e7bcb894c6869628b8dd07b0c9056 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 20 Sep 2024 11:03:02 +0100 Subject: [PATCH 51/66] Add auth examples to the doc Signed-off-by: Adam Cattermole --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 4bb803ac..73fffee2 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,12 @@ There is then a single auth policy defined for e2e testing: ```sh curl -H "Host: test.a.auth.com" http://127.0.0.1:8000/get -i +# HTTP/1.1 401 Unauthorized +``` + +```sh +curl -H "Host: test.a.auth.com" -H "Authorization: APIKEY ndyBzreUzF4zqDQsqSPMHkRhriEOtcRx" http://127.0.0.1:8000/get -i +# HTTP/1.1 200 OK ``` And three rate limit policies defined for e2e testing: From a2977fd54dff7cdb010470fac3274d1efeedf915 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 20 Sep 2024 11:08:36 +0100 Subject: [PATCH 52/66] Update debug logs Signed-off-by: Adam Cattermole --- src/filter/http_context.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index a658b8b0..1680540c 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -116,7 +116,10 @@ impl Filter { if let GrpcMessageResponse::Auth(check_response) = auth_resp { match check_response.http_response { Some(CheckResponse_oneof_http_response::ok_response(ok_response)) => { - debug!("Handling OkHttpResponse..."); + debug!( + "#{} process_auth_grpc_response: received OkHttpResponse", + self.context_id + ); ok_response .get_response_headers_to_add() @@ -129,7 +132,10 @@ impl Filter { }); } Some(CheckResponse_oneof_http_response::denied_response(denied_response)) => { - debug!("Handling DeniedHttpResponse..."); + debug!( + "#{} process_auth_grpc_response: received DeniedHttpResponse", + self.context_id + ); let mut response_headers = vec![]; denied_response.get_headers().iter().for_each(|header| { From efa4b908c88d602f61a76f0b49a6a3405a912b9d Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 20 Sep 2024 11:23:03 +0100 Subject: [PATCH 53/66] Update test expected logs Signed-off-by: Adam Cattermole --- tests/rate_limited.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/rate_limited.rs b/tests/rate_limited.rs index 6b70d85e..47ec8f99 100644 --- a/tests/rate_limited.rs +++ b/tests/rate_limited.rs @@ -215,7 +215,7 @@ fn it_limits() { .returning(Some(42)) .expect_log( Some(LogLevel::Debug), - Some("#2 initiated gRPC call (id# 42) to Limitador"), + Some("#2 initiated gRPC call (id# 42)"), ) .execute_and_expect(ReturnType::Action(Action::Pause)) .unwrap(); @@ -381,7 +381,7 @@ fn it_passes_additional_headers() { .returning(Some(42)) .expect_log( Some(LogLevel::Debug), - Some("#2 initiated gRPC call (id# 42) to Limitador"), + Some("#2 initiated gRPC call (id# 42)"), ) .execute_and_expect(ReturnType::Action(Action::Pause)) .unwrap(); @@ -523,7 +523,7 @@ fn it_rate_limits_with_empty_conditions() { .returning(Some(42)) .expect_log( Some(LogLevel::Debug), - Some("#2 initiated gRPC call (id# 42) to Limitador"), + Some("#2 initiated gRPC call (id# 42)"), ) .execute_and_expect(ReturnType::Action(Action::Pause)) .unwrap(); From 80cde651e7af0aae2d1c827e3c698cc91e7f6b50 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 20 Sep 2024 11:37:16 +0100 Subject: [PATCH 54/66] Rename domain -> scope and move to action Signed-off-by: Adam Cattermole --- src/configuration.rs | 15 ++++++++------- src/operation_dispatcher.rs | 2 +- src/policy.rs | 3 --- src/policy_index.rs | 8 +------- tests/rate_limited.rs | 8 ++++---- utils/deploy/envoy-notls.yaml | 8 ++++---- utils/deploy/envoy-tls.yaml | 8 ++++---- 7 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index e026254d..f4e603b5 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -533,6 +533,7 @@ pub struct Extension { #[serde(rename_all = "camelCase")] pub struct Action { pub extension: String, + pub scope: String, #[allow(dead_code)] pub data: DataType, } @@ -552,7 +553,6 @@ mod test { "policies": [ { "name": "rlp-ns-A/rlp-name-A", - "domain": "rlp-ns-A/rlp-name-A", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -591,6 +591,7 @@ mod test { "actions": [ { "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", @@ -681,7 +682,6 @@ mod test { "policies": [ { "name": "rlp-ns-A/rlp-name-A", - "domain": "rlp-ns-A/rlp-name-A", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -697,6 +697,7 @@ mod test { "actions": [ { "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", @@ -747,7 +748,6 @@ mod test { "policies": [ { "name": "rlp-ns-A/rlp-name-A", - "domain": "rlp-ns-A/rlp-name-A", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -785,6 +785,7 @@ mod test { "actions": [ { "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", @@ -842,7 +843,6 @@ mod test { "policies": [ { "name": "rlp-ns-A/rlp-name-A", - "domain": "rlp-ns-A/rlp-name-A", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -862,6 +862,7 @@ mod test { "actions": [ { "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", @@ -902,7 +903,6 @@ mod test { "policies": [ { "name": "rlp-ns-A/rlp-name-A", - "domain": "rlp-ns-A/rlp-name-A", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -920,6 +920,7 @@ mod test { "actions": [ { "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", @@ -945,7 +946,6 @@ mod test { "policies": [ { "name": "rlp-ns-A/rlp-name-A", - "domain": "rlp-ns-A/rlp-name-A", "service": "limitador-cluster", "hostnames": ["*.toystore.com", "example.com"], "rules": [ @@ -961,6 +961,7 @@ mod test { "actions": [ { "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", @@ -986,7 +987,6 @@ mod test { "policies": [ { "name": "rlp-ns-A/rlp-name-A", - "domain": "rlp-ns-A/rlp-name-A", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -1004,6 +1004,7 @@ mod test { "actions": [ { "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 764a3d08..fcd83a70 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -131,7 +131,7 @@ impl OperationDispatcher { if let Some(service) = self.service_handlers.get(&action.extension) { let message = GrpcMessageRequest::new( service.get_extension_type(), - policy.domain.clone(), + action.scope.clone(), descriptors.clone(), ); operations.push(Operation::new( diff --git a/src/policy.rs b/src/policy.rs index 353928ea..1585e267 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -25,7 +25,6 @@ pub struct Rule { #[serde(rename_all = "camelCase")] pub struct Policy { pub name: String, - pub domain: String, pub hostnames: Vec, pub rules: Vec, pub actions: Vec, @@ -35,14 +34,12 @@ impl Policy { #[cfg(test)] pub fn new( name: String, - domain: String, hostnames: Vec, rules: Vec, actions: Vec, ) -> Self { Policy { name, - domain, hostnames, rules, actions, diff --git a/src/policy_index.rs b/src/policy_index.rs index 3620179e..30adf4c7 100644 --- a/src/policy_index.rs +++ b/src/policy_index.rs @@ -41,13 +41,7 @@ mod tests { use crate::policy_index::PolicyIndex; fn build_ratelimit_policy(name: &str) -> Policy { - Policy::new( - name.to_owned(), - "".to_owned(), - Vec::new(), - Vec::new(), - Vec::new(), - ) + Policy::new(name.to_owned(), Vec::new(), Vec::new(), Vec::new()) } #[test] diff --git a/tests/rate_limited.rs b/tests/rate_limited.rs index 47ec8f99..0baad090 100644 --- a/tests/rate_limited.rs +++ b/tests/rate_limited.rs @@ -100,7 +100,6 @@ fn it_limits() { "policies": [ { "name": "some-name", - "domain": "RLS-domain", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -136,6 +135,7 @@ fn it_limits() { "actions": [ { "extension": "limitador", + "scope": "RLS-domain", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", @@ -266,7 +266,6 @@ fn it_passes_additional_headers() { "policies": [ { "name": "some-name", - "domain": "RLS-domain", "hostnames": ["*.toystore.com", "example.com"], "rules": [ { @@ -302,6 +301,7 @@ fn it_passes_additional_headers() { "actions": [ { "extension": "limitador", + "scope": "RLS-domain", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", @@ -446,7 +446,6 @@ fn it_rate_limits_with_empty_conditions() { "policies": [ { "name": "some-name", - "domain": "RLS-domain", "hostnames": ["*.com"], "rules": [ { @@ -462,6 +461,7 @@ fn it_rate_limits_with_empty_conditions() { "actions": [ { "extension": "limitador", + "scope": "RLS-domain", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", @@ -574,7 +574,6 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value "policies": [ { "name": "some-name", - "domain": "RLS-domain", "hostnames": ["*.com"], "rules": [ { @@ -589,6 +588,7 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value "actions": [ { "extension": "limitador", + "scope": "RLS-domain", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", diff --git a/utils/deploy/envoy-notls.yaml b/utils/deploy/envoy-notls.yaml index 6c4cc277..1846e2a4 100644 --- a/utils/deploy/envoy-notls.yaml +++ b/utils/deploy/envoy-notls.yaml @@ -150,7 +150,6 @@ data: "policies": [ { "name": "auth-ns-A/auth-name-A", - "domain": "effective-route-1", "hostnames": [ "*.a.auth.com" ], @@ -180,6 +179,7 @@ data: "actions": [ { "extension": "authorino", + "scope": "effective-route-1", "data": { "static": { "key": "host", @@ -191,7 +191,6 @@ data: }, { "name": "rlp-ns-A/rlp-name-A", - "domain": "rlp-ns-A/rlp-name-A", "hostnames": [ "*.a.rlp.com" ], @@ -209,6 +208,7 @@ data: "actions": [ { "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", @@ -220,7 +220,6 @@ data: }, { "name": "rlp-ns-B/rlp-name-B", - "domain": "rlp-ns-B/rlp-name-B", "hostnames": [ "*.b.rlp.com" ], @@ -250,6 +249,7 @@ data: "actions": [ { "extension": "limitador", + "scope": "rlp-ns-B/rlp-name-B", "data": { "static": { "key": "rlp-ns-B/rlp-name-B", @@ -261,7 +261,6 @@ data: }, { "name": "rlp-ns-C/rlp-name-C", - "domain": "rlp-ns-C/rlp-name-C", "hostnames": [ "*.c.rlp.com" ], @@ -392,6 +391,7 @@ data: "actions": [ { "extension": "limitador", + "scope": "rlp-ns-C/rlp-name-C", "data": { "static": { "key": "rlp-ns-C/rlp-name-C", diff --git a/utils/deploy/envoy-tls.yaml b/utils/deploy/envoy-tls.yaml index faba5e26..8f0fe791 100644 --- a/utils/deploy/envoy-tls.yaml +++ b/utils/deploy/envoy-tls.yaml @@ -159,7 +159,6 @@ data: "policies": [ { "name": "auth-ns-A/auth-name-A", - "domain": "effective-route-1", "hostnames": [ "*.a.auth.com" ], @@ -189,6 +188,7 @@ data: "actions": [ { "extension": "authorino", + "scope": "effective-route-1", "data": { "static": { "key": "host", @@ -200,7 +200,6 @@ data: }, { "name": "rlp-ns-A/rlp-name-A", - "domain": "rlp-ns-A/rlp-name-A", "hostnames": [ "*.a.rlp.com" ], @@ -218,6 +217,7 @@ data: "actions": [ { "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", "data": { "static": { "key": "rlp-ns-A/rlp-name-A", @@ -229,7 +229,6 @@ data: }, { "name": "rlp-ns-B/rlp-name-B", - "domain": "rlp-ns-B/rlp-name-B", "hostnames": [ "*.b.rlp.com" ], @@ -259,6 +258,7 @@ data: "actions": [ { "extension": "limitador", + "scope": "rlp-ns-B/rlp-name-B", "data": { "static": { "key": "rlp-ns-B/rlp-name-B", @@ -270,7 +270,6 @@ data: }, { "name": "rlp-ns-C/rlp-name-C", - "domain": "rlp-ns-C/rlp-name-C", "hostnames": [ "*.c.rlp.com" ], @@ -401,6 +400,7 @@ data: "actions": [ { "extension": "limitador", + "scope": "rlp-ns-C/rlp-name-C", "data": { "static": { "key": "rlp-ns-C/rlp-name-C", From ab2eed019b68214836838c5729c2689e0b00cce9 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 20 Sep 2024 15:08:14 +0100 Subject: [PATCH 55/66] Move data from rules to actions Signed-off-by: Adam Cattermole --- src/configuration.rs | 369 ++++++++++++++++++++-------------- src/filter/http_context.rs | 12 +- src/operation_dispatcher.rs | 30 ++- src/policy.rs | 145 +------------ src/service/grpc_message.rs | 11 +- tests/rate_limited.rs | 144 ++++++------- utils/deploy/envoy-notls.yaml | 205 ++++--------------- utils/deploy/envoy-tls.yaml | 205 ++++--------------- 8 files changed, 397 insertions(+), 724 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index f4e603b5..4eee9b3a 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -7,10 +7,14 @@ use std::sync::Arc; use cel_interpreter::objects::ValueType; use cel_interpreter::{Context, Expression, Value}; use cel_parser::{Atom, RelationOp}; +use log::debug; +use proxy_wasm::traits::Context as _; use serde::Deserialize; use crate::attribute::Attribute; -use crate::policy::Policy; +use crate::envoy::{RateLimitDescriptor, RateLimitDescriptor_Entry}; +use crate::filter::http_context::Filter; +use crate::policy::{Policy, Rule}; use crate::policy_index::PolicyIndex; use crate::service::GrpcService; @@ -462,18 +466,18 @@ impl TryFrom for FilterConfig { for rlp in config.policies.iter() { for rule in &rlp.rules { - for datum in &rule.data { - let result = datum.item.compile(); + for pe in &rule.conditions { + let result = pe.compile(); if result.is_err() { return Err(result.err().unwrap()); } } - for condition in &rule.conditions { - for pe in &condition.all_of { - let result = pe.compile(); - if result.is_err() { - return Err(result.err().unwrap()); - } + } + for action in &rlp.actions { + for datum in &action.data { + let result = datum.item.compile(); + if result.is_err() { + return Err(result.err().unwrap()); } } } @@ -535,7 +539,129 @@ pub struct Action { pub extension: String, pub scope: String, #[allow(dead_code)] - pub data: DataType, + pub data: Vec, +} + +impl Action { + pub fn build_descriptors( + &self, + rules: Vec, + filter: &Filter, + ) -> protobuf::RepeatedField { + rules + .iter() + .filter(|rule: &&Rule| self.filter_rule_by_conditions(filter, &rule.conditions)) + // Mapping 1 Rule -> 1 Descriptor + // Filter out empty descriptors + .filter_map(|_| self.build_single_descriptor(filter, &self.data)) + .collect() + } + + fn filter_rule_by_conditions(&self, filter: &Filter, conditions: &[PatternExpression]) -> bool { + if conditions.is_empty() { + // no conditions is equivalent to matching all the requests. + return true; + } + + conditions + .iter() + .all(|pattern_expression| self.pattern_expression_applies(filter, pattern_expression)) + } + + fn pattern_expression_applies(&self, filter: &Filter, p_e: &PatternExpression) -> bool { + let attribute_path = p_e.path(); + debug!( + "#{} get_property: selector: {} path: {:?}", + filter.context_id, p_e.selector, attribute_path + ); + let attribute_value = match filter.get_property(attribute_path) { + None => { + debug!( + "#{} pattern_expression_applies: selector not found: {}, defaulting to ``", + filter.context_id, p_e.selector + ); + b"".to_vec() + } + Some(attribute_bytes) => attribute_bytes, + }; + match p_e.eval(attribute_value) { + Err(e) => { + debug!( + "#{} pattern_expression_applies failed: {}", + filter.context_id, e + ); + false + } + Ok(result) => result, + } + } + + fn build_single_descriptor( + &self, + filter: &Filter, + data_list: &[DataItem], + ) -> Option { + let mut entries = ::protobuf::RepeatedField::default(); + + // iterate over data items to allow any data item to skip the entire descriptor + for data in data_list.iter() { + match &data.item { + DataType::Static(static_item) => { + let mut descriptor_entry = RateLimitDescriptor_Entry::new(); + descriptor_entry.set_key(static_item.key.to_owned()); + descriptor_entry.set_value(static_item.value.to_owned()); + entries.push(descriptor_entry); + } + DataType::Selector(selector_item) => { + let descriptor_key = match &selector_item.key { + None => selector_item.path().to_string(), + Some(key) => key.to_owned(), + }; + + let attribute_path = selector_item.path(); + debug!( + "#{} get_property: selector: {} path: {:?}", + filter.context_id, selector_item.selector, attribute_path + ); + let value = match filter.get_property(attribute_path.tokens()) { + None => { + debug!( + "#{} build_single_descriptor: selector not found: {}", + filter.context_id, attribute_path + ); + match &selector_item.default { + None => return None, // skipping the entire descriptor + Some(default_value) => default_value.clone(), + } + } + // TODO(eastizle): not all fields are strings + // https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes + Some(attribute_bytes) => match Attribute::parse(attribute_bytes) { + Ok(attr_str) => attr_str, + Err(e) => { + debug!("#{} build_single_descriptor: failed to parse selector value: {}, error: {}", + filter.context_id, attribute_path, e); + return None; + } + }, + // Alternative implementation (for rust >= 1.76) + // Attribute::parse(attribute_bytes) + // .inspect_err(|e| debug!("#{} build_single_descriptor: failed to parse selector value: {}, error: {}", + // filter.context_id, attribute_path, e)) + // .ok()?, + }; + let mut descriptor_entry = RateLimitDescriptor_Entry::new(); + descriptor_entry.set_key(descriptor_key); + descriptor_entry.set_value(value); + entries.push(descriptor_entry); + } + } + } + + let mut res = RateLimitDescriptor::new(); + res.set_entries(entries); + Some(res) + } } #[cfg(test)] @@ -558,46 +684,37 @@ mod test { { "conditions": [ { - "allOf": [ - { - "selector": "request.path", - "operator": "eq", - "value": "/admin/toy" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "POST" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "cars.toystore.com" - }] - }], - "data": [ + "selector": "request.path", + "operator": "eq", + "value": "/admin/toy" + }, { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" - } + "selector": "request.method", + "operator": "eq", + "value": "POST" }, { - "selector": { - "selector": "auth.metadata.username" - } + "selector": "request.host", + "operator": "eq", + "value": "cars.toystore.com" }] }], "actions": [ { "extension": "limitador", "scope": "rlp-ns-A/rlp-name-A", - "data": { + "data": [ + { "static": { "key": "rlp-ns-A/rlp-name-A", "value": "1" } - } + }, + { + "selector": { + "selector": "auth.metadata.username" + } + }] } ] }] @@ -618,12 +735,12 @@ mod test { assert_eq!(rules.len(), 1); let conditions = &rules[0].conditions; - assert_eq!(conditions.len(), 1); + assert_eq!(conditions.len(), 3); - let all_of_conditions = &conditions[0].all_of; - assert_eq!(all_of_conditions.len(), 3); + let actions = &filter_config.policies[0].actions; + assert_eq!(actions.len(), 1); - let data_items = &rules[0].data; + let data_items = &actions[0].data; assert_eq!(data_items.len(), 2); // TODO(eastizle): DataItem does not implement PartialEq, add it only for testing? @@ -683,8 +800,11 @@ mod test { { "name": "rlp-ns-A/rlp-name-A", "hostnames": ["*.toystore.com", "example.com"], - "rules": [ + "rules": [], + "actions": [ { + "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", "data": [ { "selector": { @@ -693,17 +813,6 @@ mod test { "default": "my_selector_default_value" } }] - }], - "actions": [ - { - "extension": "limitador", - "scope": "rlp-ns-A/rlp-name-A", - "data": { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" - } - } } ] }] @@ -718,9 +827,12 @@ mod test { assert_eq!(filter_config.policies.len(), 1); let rules = &filter_config.policies[0].rules; - assert_eq!(rules.len(), 1); + assert_eq!(rules.len(), 0); + + let actions = &filter_config.policies[0].actions; + assert_eq!(actions.len(), 1); - let data_items = &rules[0].data; + let data_items = &actions[0].data; assert_eq!(data_items.len(), 1); if let DataType::Selector(selector_item) = &data_items[0].item { @@ -753,45 +865,36 @@ mod test { { "conditions": [ { - "allOf": [ - { - "selector": "request.path", - "operator": "eq", - "value": "/admin/toy" - }, - { - "selector": "request.method", - "operator": "neq", - "value": "POST" - }, - { - "selector": "request.host", - "operator": "startswith", - "value": "cars." - }, - { - "selector": "request.host", - "operator": "endswith", - "value": ".com" - }, - { - "selector": "request.host", - "operator": "matches", - "value": "*.com" - }] - }], - "data": [ { "selector": { "selector": "my.selector.path" } }] + "selector": "request.path", + "operator": "eq", + "value": "/admin/toy" + }, + { + "selector": "request.method", + "operator": "neq", + "value": "POST" + }, + { + "selector": "request.host", + "operator": "startswith", + "value": "cars." + }, + { + "selector": "request.host", + "operator": "endswith", + "value": ".com" + }, + { + "selector": "request.host", + "operator": "matches", + "value": "*.com" + }] }], "actions": [ { "extension": "limitador", "scope": "rlp-ns-A/rlp-name-A", - "data": { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" - } - } + "data": [ { "selector": { "selector": "my.selector.path" } }] } ] }] @@ -809,10 +912,7 @@ mod test { assert_eq!(rules.len(), 1); let conditions = &rules[0].conditions; - assert_eq!(conditions.len(), 1); - - let all_of_conditions = &conditions[0].all_of; - assert_eq!(all_of_conditions.len(), 5); + assert_eq!(conditions.len(), 5); let expected_conditions = [ // selector, value, operator @@ -824,9 +924,9 @@ mod test { ]; for i in 0..expected_conditions.len() { - assert_eq!(all_of_conditions[i].selector, expected_conditions[i].0); - assert_eq!(all_of_conditions[i].value, expected_conditions[i].1); - assert_eq!(all_of_conditions[i].operator, expected_conditions[i].2); + assert_eq!(conditions[i].selector, expected_conditions[i].0); + assert_eq!(conditions[i].value, expected_conditions[i].1); + assert_eq!(conditions[i].operator, expected_conditions[i].2); } } @@ -844,8 +944,13 @@ mod test { { "name": "rlp-ns-A/rlp-name-A", "hostnames": ["*.toystore.com", "example.com"], - "rules": [ + "rules": [{ + "conditions": [] + }], + "actions": [ { + "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", "data": [ { "static": { @@ -858,17 +963,6 @@ mod test { "selector": "auth.metadata.username" } }] - }], - "actions": [ - { - "extension": "limitador", - "scope": "rlp-ns-A/rlp-name-A", - "data": { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" - } - } } ] }] @@ -904,29 +998,21 @@ mod test { { "name": "rlp-ns-A/rlp-name-A", "hostnames": ["*.toystore.com", "example.com"], - "rules": [ - { - "data": [ - { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" - }, - "selector": { - "selector": "auth.metadata.username" - } - }] - }], + "rules": [], "actions": [ { "extension": "limitador", "scope": "rlp-ns-A/rlp-name-A", - "data": { + "data": [ + { "static": { "key": "rlp-ns-A/rlp-name-A", "value": "1" + }, + "selector": { + "selector": "auth.metadata.username" } - } + }] } ] }] @@ -948,26 +1034,18 @@ mod test { "name": "rlp-ns-A/rlp-name-A", "service": "limitador-cluster", "hostnames": ["*.toystore.com", "example.com"], - "rules": [ - { - "data": [ - { - "unknown": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" - } - }] - }], + "rules": [], "actions": [ { "extension": "limitador", "scope": "rlp-ns-A/rlp-name-A", - "data": { - "static": { + "data": [ + { + "unknown": { "key": "rlp-ns-A/rlp-name-A", "value": "1" } - } + }] } ] }] @@ -992,25 +1070,16 @@ mod test { { "conditions": [ { - "allOf": [ - { - "selector": "request.path", - "operator": "unknown", - "value": "/admin/toy" - }] - }], - "data": [ { "selector": { "selector": "my.selector.path" } }] + "selector": "request.path", + "operator": "unknown", + "value": "/admin/toy" + }] }], "actions": [ { "extension": "limitador", "scope": "rlp-ns-A/rlp-name-A", - "data": { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" - } - } + "data": [ { "selector": { "selector": "my.selector.path" } }] } ] }] diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index 1680540c..1e38123b 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -30,17 +30,7 @@ impl Filter { } fn process_policy(&self, policy: &Policy) -> Action { - let descriptors = policy.build_descriptors(self); - if descriptors.is_empty() { - debug!( - "#{} process_rate_limit_policy: empty descriptors", - self.context_id - ); - return Action::Continue; - } - - self.operation_dispatcher - .build_operations(policy, descriptors); + self.operation_dispatcher.build_operations(policy, self); if let Some(operation) = self.operation_dispatcher.next() { match operation.get_result() { diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index fcd83a70..37f911d6 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -1,9 +1,9 @@ use crate::configuration::{Extension, ExtensionType, FailureMode}; -use crate::envoy::RateLimitDescriptor; +use crate::filter::http_context::Filter; use crate::policy::Policy; use crate::service::grpc_message::GrpcMessageRequest; use crate::service::{GetMapValuesBytesFn, GrpcCallFn, GrpcServiceHandler}; -use protobuf::RepeatedField; +use log::debug; use proxy_wasm::hostcalls; use proxy_wasm::types::{Bytes, MapType, Status}; use std::cell::RefCell; @@ -120,15 +120,26 @@ impl OperationDispatcher { self.waiting_operations.borrow_mut().get(&token_id).cloned() } - pub fn build_operations( - &self, - policy: &Policy, - descriptors: RepeatedField, - ) { + pub fn build_operations(&self, policy: &Policy, filter: &Filter) { let mut operations: Vec = vec![]; - policy.actions.iter().for_each(|action| { + for action in policy.actions.iter() { // TODO(didierofrivia): Error handling if let Some(service) = self.service_handlers.get(&action.extension) { + let descriptors = match service.get_extension_type() { + ExtensionType::Auth => None, + ExtensionType::RateLimit => { + let desc = action.build_descriptors(policy.rules.clone(), filter); + if desc.is_empty() { + debug!( + "#{} process_rate_limit_policy: empty descriptors", + filter.context_id + ); + continue; + } + Some(desc) + } + }; + let message = GrpcMessageRequest::new( service.get_extension_type(), action.scope.clone(), @@ -139,7 +150,7 @@ impl OperationDispatcher { (Rc::clone(service), message), )) } - }); + } self.push_operations(operations); } @@ -213,6 +224,7 @@ fn get_map_values_bytes_fn(map_type: MapType, key: &str) -> Result mod tests { use super::*; use crate::envoy::RateLimitRequest; + use protobuf::RepeatedField; use std::time::Duration; fn default_grpc_call_fn_stub( diff --git a/src/policy.rs b/src/policy.rs index 1585e267..0437ddd6 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -1,24 +1,9 @@ -use crate::attribute::Attribute; -use crate::configuration::{Action, DataItem, DataType, PatternExpression}; -use crate::envoy::{RateLimitDescriptor, RateLimitDescriptor_Entry}; -use crate::filter::http_context::Filter; -use log::debug; -use proxy_wasm::traits::Context; +use crate::configuration::{Action, PatternExpression}; use serde::Deserialize; -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Condition { - pub all_of: Vec, -} - #[derive(Deserialize, Debug, Clone)] pub struct Rule { - // - #[serde(default)] - pub conditions: Vec, - // - pub data: Vec, + pub conditions: Vec, } #[derive(Deserialize, Debug, Clone)] @@ -45,130 +30,4 @@ impl Policy { actions, } } - - pub fn build_descriptors( - &self, - filter: &Filter, - ) -> protobuf::RepeatedField { - self.rules - .iter() - .filter(|rule: &&Rule| self.filter_rule_by_conditions(filter, &rule.conditions)) - // Mapping 1 Rule -> 1 Descriptor - // Filter out empty descriptors - .filter_map(|rule| self.build_single_descriptor(filter, &rule.data)) - .collect() - } - - fn filter_rule_by_conditions(&self, filter: &Filter, conditions: &[Condition]) -> bool { - if conditions.is_empty() { - // no conditions is equivalent to matching all the requests. - return true; - } - - conditions - .iter() - .any(|condition| self.condition_applies(filter, condition)) - } - - fn condition_applies(&self, filter: &Filter, condition: &Condition) -> bool { - condition - .all_of - .iter() - .all(|pattern_expression| self.pattern_expression_applies(filter, pattern_expression)) - } - - fn pattern_expression_applies(&self, filter: &Filter, p_e: &PatternExpression) -> bool { - let attribute_path = p_e.path(); - debug!( - "#{} get_property: selector: {} path: {:?}", - filter.context_id, p_e.selector, attribute_path - ); - let attribute_value = match filter.get_property(attribute_path) { - None => { - debug!( - "#{} pattern_expression_applies: selector not found: {}, defaulting to ``", - filter.context_id, p_e.selector - ); - b"".to_vec() - } - Some(attribute_bytes) => attribute_bytes, - }; - match p_e.eval(attribute_value) { - Err(e) => { - debug!( - "#{} pattern_expression_applies failed: {}", - filter.context_id, e - ); - false - } - Ok(result) => result, - } - } - - fn build_single_descriptor( - &self, - filter: &Filter, - data_list: &[DataItem], - ) -> Option { - let mut entries = ::protobuf::RepeatedField::default(); - - // iterate over data items to allow any data item to skip the entire descriptor - for data in data_list.iter() { - match &data.item { - DataType::Static(static_item) => { - let mut descriptor_entry = RateLimitDescriptor_Entry::new(); - descriptor_entry.set_key(static_item.key.to_owned()); - descriptor_entry.set_value(static_item.value.to_owned()); - entries.push(descriptor_entry); - } - DataType::Selector(selector_item) => { - let descriptor_key = match &selector_item.key { - None => selector_item.path().to_string(), - Some(key) => key.to_owned(), - }; - - let attribute_path = selector_item.path(); - debug!( - "#{} get_property: selector: {} path: {:?}", - filter.context_id, selector_item.selector, attribute_path - ); - let value = match filter.get_property(attribute_path.tokens()) { - None => { - debug!( - "#{} build_single_descriptor: selector not found: {}", - filter.context_id, attribute_path - ); - match &selector_item.default { - None => return None, // skipping the entire descriptor - Some(default_value) => default_value.clone(), - } - } - // TODO(eastizle): not all fields are strings - // https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes - Some(attribute_bytes) => match Attribute::parse(attribute_bytes) { - Ok(attr_str) => attr_str, - Err(e) => { - debug!("#{} build_single_descriptor: failed to parse selector value: {}, error: {}", - filter.context_id, attribute_path, e); - return None; - } - }, - // Alternative implementation (for rust >= 1.76) - // Attribute::parse(attribute_bytes) - // .inspect_err(|e| debug!("#{} build_single_descriptor: failed to parse selector value: {}, error: {}", - // filter.context_id, attribute_path, e)) - // .ok()?, - }; - let mut descriptor_entry = RateLimitDescriptor_Entry::new(); - descriptor_entry.set_key(descriptor_key); - descriptor_entry.set_value(value); - entries.push(descriptor_entry); - } - } - } - - let mut res = RateLimitDescriptor::new(); - res.set_entries(entries); - Some(res) - } } diff --git a/src/service/grpc_message.rs b/src/service/grpc_message.rs index 6396c5f5..40df200b 100644 --- a/src/service/grpc_message.rs +++ b/src/service/grpc_message.rs @@ -127,12 +127,15 @@ impl GrpcMessageRequest { pub fn new( extension_type: ExtensionType, domain: String, - descriptors: protobuf::RepeatedField, + descriptors: Option>, ) -> Self { match extension_type { - ExtensionType::RateLimit => GrpcMessageRequest::RateLimit( - RateLimitService::request_message(domain.clone(), descriptors), - ), + ExtensionType::RateLimit => { + GrpcMessageRequest::RateLimit(RateLimitService::request_message( + domain.clone(), + descriptors.expect("rate limit message expects descriptors"), + )) + } ExtensionType::Auth => { GrpcMessageRequest::Auth(AuthService::request_message(domain.clone())) } diff --git a/tests/rate_limited.rs b/tests/rate_limited.rs index 0baad090..4b6e4b82 100644 --- a/tests/rate_limited.rs +++ b/tests/rate_limited.rs @@ -105,43 +105,34 @@ fn it_limits() { { "conditions": [ { - "allOf": [ - { - "selector": "request.url_path", - "operator": "startswith", - "value": "/admin/toy" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "cars.toystore.com" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "POST" - }] - } - ], - "data": [ - { - "static": { - "key": "admin", - "value": "1" + "selector": "request.url_path", + "operator": "startswith", + "value": "/admin/toy" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "cars.toystore.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "POST" } - } ] }], "actions": [ { "extension": "limitador", "scope": "RLS-domain", - "data": { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" + "data": [ + { + "static": { + "key": "admin", + "value": "1" + } } - } + ] } ] }] @@ -271,43 +262,34 @@ fn it_passes_additional_headers() { { "conditions": [ { - "allOf": [ - { - "selector": "request.url_path", - "operator": "startswith", - "value": "/admin/toy" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "cars.toystore.com" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "POST" - }] - } - ], - "data": [ - { - "static": { - "key": "admin", - "value": "1" + "selector": "request.url_path", + "operator": "startswith", + "value": "/admin/toy" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "cars.toystore.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "POST" } - } ] }], "actions": [ { "extension": "limitador", "scope": "RLS-domain", - "data": { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" + "data": [ + { + "static": { + "key": "admin", + "value": "1" + } } - } + ] } ] }] @@ -448,26 +430,22 @@ fn it_rate_limits_with_empty_conditions() { "name": "some-name", "hostnames": ["*.com"], "rules": [ - { - "data": [ - { - "static": { - "key": "admin", - "value": "1" - } - } - ] - }], + { + "conditions": [] + } + ], "actions": [ { "extension": "limitador", "scope": "RLS-domain", - "data": { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" + "data": [ + { + "static": { + "key": "admin", + "value": "1" + } } - } + ] } ] }] @@ -576,25 +554,21 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value "name": "some-name", "hostnames": ["*.com"], "rules": [ - { - "data": [ { - "selector": { - "selector": "unknown.path" - } + "conditions": [] } - ] - }], + ], "actions": [ { "extension": "limitador", "scope": "RLS-domain", - "data": { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" + "data": [ + { + "selector": { + "selector": "unknown.path" + } } - } + ] } ] }] diff --git a/utils/deploy/envoy-notls.yaml b/utils/deploy/envoy-notls.yaml index 1846e2a4..70a7b4c9 100644 --- a/utils/deploy/envoy-notls.yaml +++ b/utils/deploy/envoy-notls.yaml @@ -157,21 +157,9 @@ data: { "conditions": [ { - "allOf": [ - { - "selector": "request.path", - "operator": "eq", - "value": "/get" - } - ] - } - ], - "data": [ - { - "static": { - "key": "host", - "value": "effective-route-1" - } + "selector": "request.path", + "operator": "eq", + "value": "/get" } ] } @@ -180,12 +168,7 @@ data: { "extension": "authorino", "scope": "effective-route-1", - "data": { - "static": { - "key": "host", - "value": "effective-route-1" - } - } + "data": [] } ] }, @@ -196,25 +179,20 @@ data: ], "rules": [ { - "data": [ - { - "selector": { - "selector": "unknown.path" - } - } - ] + "conditions": [] } ], "actions": [ { "extension": "limitador", "scope": "rlp-ns-A/rlp-name-A", - "data": { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" + "data": [ + { + "selector": { + "selector": "unknown.path" + } } - } + ] } ] }, @@ -227,21 +205,9 @@ data: { "conditions": [ { - "allOf": [ - { - "selector": "request.url_path", - "operator": "startswith", - "value": "/unknown-path" - } - ] - } - ], - "data": [ - { - "static": { - "key": "rlp-ns-B/rlp-name-B/limit-not-to-be-activated", - "value": "1" - } + "selector": "request.url_path", + "operator": "startswith", + "value": "/unknown-path" } ] } @@ -250,12 +216,14 @@ data: { "extension": "limitador", "scope": "rlp-ns-B/rlp-name-B", - "data": { - "static": { - "key": "rlp-ns-B/rlp-name-B", - "value": "1" + "data": [ + { + "static": { + "key": "rlp-ns-B/rlp-name-B/limit-not-to-be-activated", + "value": "1" + } } - } + ] } ] }, @@ -268,117 +236,44 @@ data: { "conditions": [ { - "allOf": [ - { - "selector": "request.url_path", - "operator": "startswith", - "value": "/get" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "test.c.rlp.com" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "GET" - } - ] + "selector": "request.url_path", + "operator": "startswith", + "value": "/get" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "test.c.rlp.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "GET" } - ], + ] + } + ], + "actions": [ + { + "extension": "limitador", + "scope": "rlp-ns-C/rlp-name-C", "data": [ { "static": { "key": "limit_to_be_activated", "value": "1" } - } - ] - }, - { - "conditions": [ - { - "allOf": [ - { - "selector": "request.url_path", - "operator": "startswith", - "value": "/get" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "test.c.rlp.com" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "GET" - } - ] - } - ], - "data": [ + }, { "selector": { "selector": "source.address" } - } - ] - }, - { - "conditions": [ - { - "allOf": [ - { - "selector": "request.url_path", - "operator": "startswith", - "value": "/get" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "test.c.rlp.com" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "GET" - } - ] - } - ], - "data": [ + }, { "selector": { "selector": "request.headers.My-Custom-Header-01" } - } - ] - }, - { - "conditions": [ - { - "allOf": [ - { - "selector": "request.url_path", - "operator": "startswith", - "value": "/get" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "test.c.rlp.com" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "GET" - } - ] - } - ], - "data": [ + }, { "selector": { "selector": "metadata.filter_metadata.envoy\\.filters\\.http\\.header_to_metadata.user_id", @@ -387,18 +282,6 @@ data: } ] } - ], - "actions": [ - { - "extension": "limitador", - "scope": "rlp-ns-C/rlp-name-C", - "data": { - "static": { - "key": "rlp-ns-C/rlp-name-C", - "value": "1" - } - } - } ] } ] diff --git a/utils/deploy/envoy-tls.yaml b/utils/deploy/envoy-tls.yaml index 8f0fe791..234fd926 100644 --- a/utils/deploy/envoy-tls.yaml +++ b/utils/deploy/envoy-tls.yaml @@ -166,21 +166,9 @@ data: { "conditions": [ { - "allOf": [ - { - "selector": "request.path", - "operator": "eq", - "value": "/get" - } - ] - } - ], - "data": [ - { - "static": { - "key": "host", - "value": "effective-route-1" - } + "selector": "request.path", + "operator": "eq", + "value": "/get" } ] } @@ -189,12 +177,7 @@ data: { "extension": "authorino", "scope": "effective-route-1", - "data": { - "static": { - "key": "host", - "value": "effective-route-1" - } - } + "data": [] } ] }, @@ -205,25 +188,20 @@ data: ], "rules": [ { - "data": [ - { - "selector": { - "selector": "unknown.path" - } - } - ] + "conditions": [] } ], "actions": [ { "extension": "limitador", "scope": "rlp-ns-A/rlp-name-A", - "data": { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" + "data": [ + { + "selector": { + "selector": "unknown.path" + } } - } + ] } ] }, @@ -236,21 +214,9 @@ data: { "conditions": [ { - "allOf": [ - { - "selector": "request.url_path", - "operator": "startswith", - "value": "/unknown-path" - } - ] - } - ], - "data": [ - { - "static": { - "key": "rlp-ns-B/rlp-name-B/limit-not-to-be-activated", - "value": "1" - } + "selector": "request.url_path", + "operator": "startswith", + "value": "/unknown-path" } ] } @@ -259,12 +225,14 @@ data: { "extension": "limitador", "scope": "rlp-ns-B/rlp-name-B", - "data": { - "static": { - "key": "rlp-ns-B/rlp-name-B", - "value": "1" + "data": [ + { + "static": { + "key": "rlp-ns-B/rlp-name-B/limit-not-to-be-activated", + "value": "1" + } } - } + ] } ] }, @@ -277,117 +245,44 @@ data: { "conditions": [ { - "allOf": [ - { - "selector": "request.url_path", - "operator": "startswith", - "value": "/get" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "test.c.rlp.com" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "GET" - } - ] + "selector": "request.url_path", + "operator": "startswith", + "value": "/get" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "test.c.rlp.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "GET" } - ], + ] + } + ], + "actions": [ + { + "extension": "limitador", + "scope": "rlp-ns-C/rlp-name-C", "data": [ { "static": { "key": "limit_to_be_activated", "value": "1" } - } - ] - }, - { - "conditions": [ - { - "allOf": [ - { - "selector": "request.url_path", - "operator": "startswith", - "value": "/get" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "test.c.rlp.com" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "GET" - } - ] - } - ], - "data": [ + }, { "selector": { "selector": "source.address" } - } - ] - }, - { - "conditions": [ - { - "allOf": [ - { - "selector": "request.url_path", - "operator": "startswith", - "value": "/get" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "test.c.rlp.com" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "GET" - } - ] - } - ], - "data": [ + }, { "selector": { "selector": "request.headers.My-Custom-Header-01" } - } - ] - }, - { - "conditions": [ - { - "allOf": [ - { - "selector": "request.url_path", - "operator": "startswith", - "value": "/get" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "test.c.rlp.com" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "GET" - } - ] - } - ], - "data": [ + }, { "selector": { "selector": "metadata.filter_metadata.envoy\\.filters\\.http\\.header_to_metadata.user_id", @@ -396,18 +291,6 @@ data: } ] } - ], - "actions": [ - { - "extension": "limitador", - "scope": "rlp-ns-C/rlp-name-C", - "data": { - "static": { - "key": "rlp-ns-C/rlp-name-C", - "value": "1" - } - } - } ] } ] From 91da4439005653b4351b434d6a8a816886035219 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Tue, 24 Sep 2024 12:41:01 +0100 Subject: [PATCH 56/66] Update examples to include authenticated rate limiting Signed-off-by: Adam Cattermole --- README.md | 19 ++++++++++++++++- utils/deploy/authconfig.yaml | 33 ++++++++++++++++++++++++----- utils/deploy/envoy-notls.yaml | 36 ++++++++++++++++++++++++++++++++ utils/deploy/envoy-tls.yaml | 36 ++++++++++++++++++++++++++++++++ utils/docker-compose/limits.yaml | 12 +++++++++++ 5 files changed, 130 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 73fffee2..60ea9155 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ curl -H "Host: test.a.auth.com" http://127.0.0.1:8000/get -i ``` ```sh -curl -H "Host: test.a.auth.com" -H "Authorization: APIKEY ndyBzreUzF4zqDQsqSPMHkRhriEOtcRx" http://127.0.0.1:8000/get -i +curl -H "Host: test.a.auth.com" -H "Authorization: APIKEY IAMALICE" http://127.0.0.1:8000/get -i # HTTP/1.1 200 OK ``` @@ -196,6 +196,23 @@ curl -H "Host: test.b.rlp.com" http://127.0.0.1:8000/get -i curl -H "Host: test.c.rlp.com" -H "x-forwarded-for: 127.0.0.1" -H "My-Custom-Header-01: my-custom-header-value-01" -H "x-dyn-user-id: bob" http://127.0.0.1:8000/get -i ``` +* `multi-a` which defines two actions for authenticated ratelimiting. + +```sh +curl -H "Host: test.a.multi.com" http://127.0.0.1:8000/get -i +# HTTP/1.1 401 Unauthorized +``` + +Alice has 5 requests per 10 seconds: +```sh +while :; do curl --write-out '%{http_code}\n' --silent --output /dev/null -H "Authorization: APIKEY IAMALICE" -H "Host: test.a.multi.com" http://127.0.0.1:8000/get | grep -E --color "\b(429)\b|$"; sleep 1; done +``` + +Bob has 2 requests per 10 seconds: +```sh +while :; do curl --write-out '%{http_code}\n' --silent --output /dev/null -H "Authorization: APIKEY IAMBOB" -H "Host: test.a.multi.com" http://127.0.0.1:8000/get | grep -E --color "\b(429)\b|$"; sleep 1; done +``` + The expected descriptors: ``` diff --git a/utils/deploy/authconfig.yaml b/utils/deploy/authconfig.yaml index fa9b8d6b..74efaef6 100644 --- a/utils/deploy/authconfig.yaml +++ b/utils/deploy/authconfig.yaml @@ -7,22 +7,45 @@ spec: hosts: - effective-route-1 authentication: - "friends": + "api-key-users": apiKey: selector: matchLabels: - group: friends + app: toystore credentials: authorizationHeader: prefix: APIKEY + response: + success: + dynamicMetadata: + "identity": + json: + properties: + "userid": + selector: auth.identity.metadata.annotations.secret\.kuadrant\.io/user-id --- apiVersion: v1 kind: Secret metadata: - name: api-key-1 + name: bob-key labels: authorino.kuadrant.io/managed-by: authorino - group: friends + app: toystore + annotations: + secret.kuadrant.io/user-id: bob stringData: - api_key: "ndyBzreUzF4zqDQsqSPMHkRhriEOtcRx" + api_key: "IAMBOB" +type: Opaque +--- +apiVersion: v1 +kind: Secret +metadata: + name: alice-key + labels: + authorino.kuadrant.io/managed-by: authorino + app: toystore + annotations: + secret.kuadrant.io/user-id: alice +stringData: + api_key: "IAMALICE" type: Opaque diff --git a/utils/deploy/envoy-notls.yaml b/utils/deploy/envoy-notls.yaml index 70a7b4c9..31848620 100644 --- a/utils/deploy/envoy-notls.yaml +++ b/utils/deploy/envoy-notls.yaml @@ -283,6 +283,42 @@ data: ] } ] + }, + { + "name": "multi-ns-A/multi-name-A", + "hostnames": [ + "*.a.multi.com" + ], + "rules": [ + { + "conditions": [ + { + "selector": "request.path", + "operator": "eq", + "value": "/get" + } + ] + } + ], + "actions": [ + { + "extension": "authorino", + "scope": "effective-route-1", + "data": [] + }, + { + "extension": "limitador", + "scope": "multi-ns-A/multi-name-A", + "data": [ + { + "selector": { + "selector": "filter_state.wasm\\.kuadrant\\.identity\\.userid", + "key": "user_id" + } + } + ] + } + ] } ] } diff --git a/utils/deploy/envoy-tls.yaml b/utils/deploy/envoy-tls.yaml index 234fd926..d420595f 100644 --- a/utils/deploy/envoy-tls.yaml +++ b/utils/deploy/envoy-tls.yaml @@ -292,6 +292,42 @@ data: ] } ] + }, + { + "name": "multi-ns-A/multi-name-A", + "hostnames": [ + "*.a.multi.com" + ], + "rules": [ + { + "conditions": [ + { + "selector": "request.path", + "operator": "eq", + "value": "/get" + } + ] + } + ], + "actions": [ + { + "extension": "authorino", + "scope": "effective-route-1", + "data": [] + }, + { + "extension": "limitador", + "scope": "multi-ns-A/multi-name-A", + "data": [ + { + "selector": { + "selector": "filter_state.wasm\\.kuadrant\\.identity\\.userid", + "key": "user_id" + } + } + ] + } + ] } ] } diff --git a/utils/docker-compose/limits.yaml b/utils/docker-compose/limits.yaml index 31a68f61..f2f447bb 100644 --- a/utils/docker-compose/limits.yaml +++ b/utils/docker-compose/limits.yaml @@ -6,3 +6,15 @@ - "limit_to_be_activated == '1'" - "user_id == 'bob'" variables: [] +- namespace: multi-ns-A/multi-name-A + max_value: 5 + seconds: 10 + conditions: + - "user_id == 'alice'" + variables: [] +- namespace: multi-ns-A/multi-name-A + max_value: 2 + seconds: 10 + conditions: + - "user_id == 'bob'" + variables: [] From 9f8257443eb499be95e396c9251796fb18242d25 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Tue, 24 Sep 2024 12:53:52 +0100 Subject: [PATCH 57/66] Store dynamic metadata in filter_state Signed-off-by: Adam Cattermole --- src/attribute.rs | 130 ++++++++++++++++++++++++++++++++++++- src/filter/http_context.rs | 4 ++ 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/attribute.rs b/src/attribute.rs index 357e9f83..cd88625c 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -1,7 +1,11 @@ use crate::configuration::Path; use chrono::{DateTime, FixedOffset}; +use log::{debug, error}; +use protobuf::well_known_types::Struct; use proxy_wasm::hostcalls; +pub const KUADRANT_NAMESPACE: &str = "kuadrant"; + pub trait Attribute { fn parse(raw_attribute: Vec) -> Result where @@ -103,13 +107,135 @@ impl Attribute for DateTime { } } -#[allow(dead_code)] pub fn get_attribute(attr: &str) -> Result where T: Attribute, { match hostcalls::get_property(Path::from(attr).tokens()) { Ok(Some(attribute_bytes)) => T::parse(attribute_bytes), - _ => Err(format!("get_attribute: not found: {}", attr)), + Ok(None) => Err(format!("get_attribute: not found or null: {attr}")), + Err(e) => Err(format!("get_attribute: error: {e:?}")), + } +} + +pub fn set_attribute(attr: &str, value: &[u8]) { + match hostcalls::set_property(Path::from(attr).tokens(), Some(value)) { + Ok(_) => (), + Err(_) => error!("set_attribute: failed to set property {attr}"), + }; +} + +pub fn store_metadata(metastruct: &Struct) { + let metadata = process_metadata(metastruct, String::new()); + for (key, value) in metadata { + let attr = format!("{KUADRANT_NAMESPACE}\\.{key}"); + debug!("set_attribute: {attr} = {value}"); + set_attribute(attr.as_str(), value.into_bytes().as_slice()); + } +} + +fn process_metadata(s: &Struct, prefix: String) -> Vec<(String, String)> { + let mut result = Vec::new(); + for (key, value) in s.get_fields() { + let current_prefix = if prefix.is_empty() { + key.clone() + } else { + format!("{prefix}\\.{key}") + }; + + if value.has_string_value() { + result.push((current_prefix, value.get_string_value().to_string())); + } else if value.has_struct_value() { + let nested_struct = value.get_struct_value(); + result.extend(process_metadata(nested_struct, current_prefix)); + } + } + result +} + +#[cfg(test)] +mod tests { + use crate::attribute::process_metadata; + use protobuf::well_known_types::{Struct, Value, Value_oneof_kind}; + use std::collections::HashMap; + + pub fn struct_from(values: Vec<(String, Value)>) -> Struct { + let mut hm = HashMap::new(); + for (key, value) in values { + hm.insert(key, value); + } + Struct { + fields: hm, + unknown_fields: Default::default(), + cached_size: Default::default(), + } + } + + pub fn string_value_from(value: String) -> Value { + Value { + kind: Some(Value_oneof_kind::string_value(value)), + unknown_fields: Default::default(), + cached_size: Default::default(), + } + } + + pub fn struct_value_from(value: Struct) -> Value { + Value { + kind: Some(Value_oneof_kind::struct_value(value)), + unknown_fields: Default::default(), + cached_size: Default::default(), + } + } + #[test] + fn get_metadata_one() { + let metadata = struct_from(vec![( + "identity".to_string(), + struct_value_from(struct_from(vec![( + "userid".to_string(), + string_value_from("bob".to_string()), + )])), + )]); + let output = process_metadata(&metadata, String::new()); + assert_eq!(output.len(), 1); + assert_eq!( + output, + vec![("identity\\.userid".to_string(), "bob".to_string())] + ); + } + + #[test] + fn get_metadata_two() { + let metadata = struct_from(vec![( + "identity".to_string(), + struct_value_from(struct_from(vec![ + ("userid".to_string(), string_value_from("bob".to_string())), + ("type".to_string(), string_value_from("test".to_string())), + ])), + )]); + let output = process_metadata(&metadata, String::new()); + assert_eq!(output.len(), 2); + assert!(output.contains(&("identity\\.userid".to_string(), "bob".to_string()))); + assert!(output.contains(&("identity\\.type".to_string(), "test".to_string()))); + } + + #[test] + fn get_metadata_three() { + let metadata = struct_from(vec![ + ( + "identity".to_string(), + struct_value_from(struct_from(vec![( + "userid".to_string(), + string_value_from("bob".to_string()), + )])), + ), + ( + "other_data".to_string(), + string_value_from("other_value".to_string()), + ), + ]); + let output = process_metadata(&metadata, String::new()); + assert_eq!(output.len(), 2); + assert!(output.contains(&("identity\\.userid".to_string(), "bob".to_string()))); + assert!(output.contains(&("other_data".to_string(), "other_value".to_string()))); } } diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index 1e38123b..1d61b432 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -1,3 +1,4 @@ +use crate::attribute::store_metadata; use crate::configuration::{ExtensionType, FailureMode, FilterConfig}; use crate::envoy::{CheckResponse_oneof_http_response, RateLimitResponse, RateLimitResponse_Code}; use crate::operation_dispatcher::OperationDispatcher; @@ -104,6 +105,9 @@ impl Filter { failure_mode: &FailureMode, ) { if let GrpcMessageResponse::Auth(check_response) = auth_resp { + // store dynamic metadata in filter state + store_metadata(check_response.get_dynamic_metadata()); + match check_response.http_response { Some(CheckResponse_oneof_http_response::ok_response(ok_response)) => { debug!( From 57cb6d6a8f347a25a509785300891761bd555955 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Thu, 26 Sep 2024 16:19:17 +0100 Subject: [PATCH 58/66] Refactor actions into rules Co-authored-by: dd di cesare Signed-off-by: Adam Cattermole --- src/configuration.rs | 225 ++++++++++++++-------------------- src/filter/http_context.rs | 12 +- src/operation_dispatcher.rs | 14 +-- src/policy.rs | 52 ++++++-- src/policy_index.rs | 2 +- tests/rate_limited.rs | 77 +++++------- utils/deploy/envoy-notls.yaml | 150 +++++++++++------------ utils/deploy/envoy-tls.yaml | 150 +++++++++++------------ 8 files changed, 327 insertions(+), 355 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index 4eee9b3a..78a7f8e8 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -8,13 +8,13 @@ use cel_interpreter::objects::ValueType; use cel_interpreter::{Context, Expression, Value}; use cel_parser::{Atom, RelationOp}; use log::debug; -use proxy_wasm::traits::Context as _; +use protobuf::RepeatedField; +use proxy_wasm::hostcalls; use serde::Deserialize; use crate::attribute::Attribute; use crate::envoy::{RateLimitDescriptor, RateLimitDescriptor_Entry}; -use crate::filter::http_context::Filter; -use crate::policy::{Policy, Rule}; +use crate::policy::Policy; use crate::policy_index::PolicyIndex; use crate::service::GrpcService; @@ -472,15 +472,16 @@ impl TryFrom for FilterConfig { return Err(result.err().unwrap()); } } - } - for action in &rlp.actions { - for datum in &action.data { - let result = datum.item.compile(); - if result.is_err() { - return Err(result.err().unwrap()); + for action in &rule.actions { + for datum in &action.data { + let result = datum.item.compile(); + if result.is_err() { + return Err(result.err().unwrap()); + } } } } + for hostname in rlp.hostnames.iter() { index.insert(hostname, rlp.clone()); } @@ -543,68 +544,19 @@ pub struct Action { } impl Action { - pub fn build_descriptors( - &self, - rules: Vec, - filter: &Filter, - ) -> protobuf::RepeatedField { - rules - .iter() - .filter(|rule: &&Rule| self.filter_rule_by_conditions(filter, &rule.conditions)) - // Mapping 1 Rule -> 1 Descriptor - // Filter out empty descriptors - .filter_map(|_| self.build_single_descriptor(filter, &self.data)) - .collect() - } - - fn filter_rule_by_conditions(&self, filter: &Filter, conditions: &[PatternExpression]) -> bool { - if conditions.is_empty() { - // no conditions is equivalent to matching all the requests. - return true; + pub fn build_descriptors(&self) -> RepeatedField { + let mut entries = RepeatedField::new(); + if let Some(desc) = self.build_single_descriptor() { + entries.push(desc); } - - conditions - .iter() - .all(|pattern_expression| self.pattern_expression_applies(filter, pattern_expression)) + entries } - fn pattern_expression_applies(&self, filter: &Filter, p_e: &PatternExpression) -> bool { - let attribute_path = p_e.path(); - debug!( - "#{} get_property: selector: {} path: {:?}", - filter.context_id, p_e.selector, attribute_path - ); - let attribute_value = match filter.get_property(attribute_path) { - None => { - debug!( - "#{} pattern_expression_applies: selector not found: {}, defaulting to ``", - filter.context_id, p_e.selector - ); - b"".to_vec() - } - Some(attribute_bytes) => attribute_bytes, - }; - match p_e.eval(attribute_value) { - Err(e) => { - debug!( - "#{} pattern_expression_applies failed: {}", - filter.context_id, e - ); - false - } - Ok(result) => result, - } - } - - fn build_single_descriptor( - &self, - filter: &Filter, - data_list: &[DataItem], - ) -> Option { - let mut entries = ::protobuf::RepeatedField::default(); + fn build_single_descriptor(&self) -> Option { + let mut entries = RepeatedField::default(); // iterate over data items to allow any data item to skip the entire descriptor - for data in data_list.iter() { + for data in self.data.iter() { match &data.item { DataType::Static(static_item) => { let mut descriptor_entry = RateLimitDescriptor_Entry::new(); @@ -620,14 +572,15 @@ impl Action { let attribute_path = selector_item.path(); debug!( - "#{} get_property: selector: {} path: {:?}", - filter.context_id, selector_item.selector, attribute_path + "get_property: selector: {} path: {:?}", + selector_item.selector, attribute_path ); - let value = match filter.get_property(attribute_path.tokens()) { + let value = match hostcalls::get_property(attribute_path.tokens()).unwrap() { + //TODO(didierofrivia): Replace hostcalls by DI None => { debug!( - "#{} build_single_descriptor: selector not found: {}", - filter.context_id, attribute_path + "build_single_descriptor: selector not found: {}", + attribute_path ); match &selector_item.default { None => return None, // skipping the entire descriptor @@ -639,8 +592,8 @@ impl Action { Some(attribute_bytes) => match Attribute::parse(attribute_bytes) { Ok(attr_str) => attr_str, Err(e) => { - debug!("#{} build_single_descriptor: failed to parse selector value: {}, error: {}", - filter.context_id, attribute_path, e); + debug!("build_single_descriptor: failed to parse selector value: {}, error: {}", + attribute_path, e); return None; } }, @@ -657,7 +610,6 @@ impl Action { } } } - let mut res = RateLimitDescriptor::new(); res.set_entries(entries); Some(res) @@ -697,9 +649,8 @@ mod test { "selector": "request.host", "operator": "eq", "value": "cars.toystore.com" - }] - }], - "actions": [ + }], + "actions": [ { "extension": "limitador", "scope": "rlp-ns-A/rlp-name-A", @@ -715,8 +666,8 @@ mod test { "selector": "auth.metadata.username" } }] - } - ] + }] + }] }] }"#; @@ -737,7 +688,7 @@ mod test { let conditions = &rules[0].conditions; assert_eq!(conditions.len(), 3); - let actions = &filter_config.policies[0].actions; + let actions = &rules[0].actions; assert_eq!(actions.len(), 1); let data_items = &actions[0].data; @@ -800,21 +751,23 @@ mod test { { "name": "rlp-ns-A/rlp-name-A", "hostnames": ["*.toystore.com", "example.com"], - "rules": [], - "actions": [ + "rules": [ { - "extension": "limitador", - "scope": "rlp-ns-A/rlp-name-A", - "data": [ + "conditions": [], + "actions": [ { - "selector": { - "selector": "my.selector.path", - "key": "mykey", - "default": "my_selector_default_value" - } + "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", + "data": [ + { + "selector": { + "selector": "my.selector.path", + "key": "mykey", + "default": "my_selector_default_value" + } + }] }] - } - ] + }] }] }"#; let res = serde_json::from_str::(config); @@ -827,9 +780,9 @@ mod test { assert_eq!(filter_config.policies.len(), 1); let rules = &filter_config.policies[0].rules; - assert_eq!(rules.len(), 0); + assert_eq!(rules.len(), 1); - let actions = &filter_config.policies[0].actions; + let actions = &rules[0].actions; assert_eq!(actions.len(), 1); let data_items = &actions[0].data; @@ -888,15 +841,14 @@ mod test { "selector": "request.host", "operator": "matches", "value": "*.com" + }], + "actions": [ + { + "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", + "data": [ { "selector": { "selector": "my.selector.path" } }] }] - }], - "actions": [ - { - "extension": "limitador", - "scope": "rlp-ns-A/rlp-name-A", - "data": [ { "selector": { "selector": "my.selector.path" } }] - } - ] + }] }] }"#; let res = serde_json::from_str::(config); @@ -944,27 +896,27 @@ mod test { { "name": "rlp-ns-A/rlp-name-A", "hostnames": ["*.toystore.com", "example.com"], - "rules": [{ - "conditions": [] - }], - "actions": [ + "rules": [ { - "extension": "limitador", - "scope": "rlp-ns-A/rlp-name-A", - "data": [ - { - "static": { - "key": "rlp-ns-A/rlp-name-A", - "value": "1" - } - }, + "conditions": [], + "actions": [ { - "selector": { - "selector": "auth.metadata.username" - } + "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", + "data": [ + { + "static": { + "key": "rlp-ns-A/rlp-name-A", + "value": "1" + } + }, + { + "selector": { + "selector": "auth.metadata.username" + } + }] }] - } - ] + }] }] }"#; let res = serde_json::from_str::(config); @@ -998,8 +950,9 @@ mod test { { "name": "rlp-ns-A/rlp-name-A", "hostnames": ["*.toystore.com", "example.com"], - "rules": [], - "actions": [ + "rules": [ + { + "actions": [ { "extension": "limitador", "scope": "rlp-ns-A/rlp-name-A", @@ -1013,8 +966,8 @@ mod test { "selector": "auth.metadata.username" } }] - } - ] + }] + }] }] }"#; let res = serde_json::from_str::(bad_config); @@ -1034,8 +987,9 @@ mod test { "name": "rlp-ns-A/rlp-name-A", "service": "limitador-cluster", "hostnames": ["*.toystore.com", "example.com"], - "rules": [], - "actions": [ + "rules": [ + { + "actions": [ { "extension": "limitador", "scope": "rlp-ns-A/rlp-name-A", @@ -1046,8 +1000,8 @@ mod test { "value": "1" } }] - } - ] + }] + }] }] }"#; let res = serde_json::from_str::(bad_config); @@ -1073,15 +1027,14 @@ mod test { "selector": "request.path", "operator": "unknown", "value": "/admin/toy" + }], + "actions": [ + { + "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", + "data": [ { "selector": { "selector": "my.selector.path" } }] }] - }], - "actions": [ - { - "extension": "limitador", - "scope": "rlp-ns-A/rlp-name-A", - "data": [ { "selector": { "selector": "my.selector.path" } }] - } - ] + }] }] }"#; let res = serde_json::from_str::(bad_config); diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index 1d61b432..e28e5f6d 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -31,7 +31,12 @@ impl Filter { } fn process_policy(&self, policy: &Policy) -> Action { - self.operation_dispatcher.build_operations(policy, self); + if let Some(rule) = policy.find_rule_that_applies() { + self.operation_dispatcher.build_operations(rule); + } else { + debug!("#{} process_policy: no rule applied", self.context_id); + return Action::Continue; + } if let Some(operation) = self.operation_dispatcher.next() { match operation.get_result() { @@ -172,10 +177,7 @@ impl HttpContext for Filter { Action::Continue } Some(policy) => { - debug!( - "#{} ratelimitpolicy selected {}", - self.context_id, policy.name - ); + debug!("#{} policy selected {}", self.context_id, policy.name); self.process_policy(policy) } } diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 37f911d6..b89785ae 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -1,6 +1,5 @@ use crate::configuration::{Extension, ExtensionType, FailureMode}; -use crate::filter::http_context::Filter; -use crate::policy::Policy; +use crate::policy::Rule; use crate::service::grpc_message::GrpcMessageRequest; use crate::service::{GetMapValuesBytesFn, GrpcCallFn, GrpcServiceHandler}; use log::debug; @@ -120,20 +119,17 @@ impl OperationDispatcher { self.waiting_operations.borrow_mut().get(&token_id).cloned() } - pub fn build_operations(&self, policy: &Policy, filter: &Filter) { + pub fn build_operations(&self, rule: &Rule) { let mut operations: Vec = vec![]; - for action in policy.actions.iter() { + for action in rule.actions.iter() { // TODO(didierofrivia): Error handling if let Some(service) = self.service_handlers.get(&action.extension) { let descriptors = match service.get_extension_type() { ExtensionType::Auth => None, ExtensionType::RateLimit => { - let desc = action.build_descriptors(policy.rules.clone(), filter); + let desc = action.build_descriptors(); if desc.is_empty() { - debug!( - "#{} process_rate_limit_policy: empty descriptors", - filter.context_id - ); + debug!("process_policy: empty descriptors"); continue; } Some(desc) diff --git a/src/policy.rs b/src/policy.rs index 0437ddd6..c85eae8c 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -1,9 +1,12 @@ use crate::configuration::{Action, PatternExpression}; +use log::debug; +use proxy_wasm::hostcalls; use serde::Deserialize; #[derive(Deserialize, Debug, Clone)] pub struct Rule { pub conditions: Vec, + pub actions: Vec, } #[derive(Deserialize, Debug, Clone)] @@ -12,22 +15,55 @@ pub struct Policy { pub name: String, pub hostnames: Vec, pub rules: Vec, - pub actions: Vec, } impl Policy { #[cfg(test)] - pub fn new( - name: String, - hostnames: Vec, - rules: Vec, - actions: Vec, - ) -> Self { + pub fn new(name: String, hostnames: Vec, rules: Vec) -> Self { Policy { name, hostnames, rules, - actions, } } + + pub fn find_rule_that_applies(&self) -> Option<&Rule> { + self.rules + .iter() + .find(|rule: &&Rule| self.filter_rule_by_conditions(&rule.conditions)) + } + + fn filter_rule_by_conditions(&self, conditions: &[PatternExpression]) -> bool { + if conditions.is_empty() { + // no conditions is equivalent to matching all the requests. + return true; + } + + conditions + .iter() + .any(|condition| self.pattern_expression_applies(condition)) + } + + fn pattern_expression_applies(&self, p_e: &PatternExpression) -> bool { + let attribute_path = p_e.path(); + debug!( + "get_property: selector: {} path: {:?}", + p_e.selector, attribute_path + ); + let attribute_value = match hostcalls::get_property(attribute_path).unwrap() { + //TODO(didierofrivia): Replace hostcalls by DI + None => { + debug!( + "pattern_expression_applies: selector not found: {}, defaulting to ``", + p_e.selector + ); + b"".to_vec() + } + Some(attribute_bytes) => attribute_bytes, + }; + p_e.eval(attribute_value).unwrap_or_else(|e| { + debug!("pattern_expression_applies failed: {}", e); + false + }) + } } diff --git a/src/policy_index.rs b/src/policy_index.rs index 30adf4c7..0c0759a9 100644 --- a/src/policy_index.rs +++ b/src/policy_index.rs @@ -41,7 +41,7 @@ mod tests { use crate::policy_index::PolicyIndex; fn build_ratelimit_policy(name: &str) -> Policy { - Policy::new(name.to_owned(), Vec::new(), Vec::new(), Vec::new()) + Policy::new(name.to_owned(), Vec::new(), Vec::new()) } #[test] diff --git a/tests/rate_limited.rs b/tests/rate_limited.rs index 4b6e4b82..dda0702b 100644 --- a/tests/rate_limited.rs +++ b/tests/rate_limited.rs @@ -119,9 +119,8 @@ fn it_limits() { "operator": "eq", "value": "POST" } - ] - }], - "actions": [ + ], + "actions": [ { "extension": "limitador", "scope": "RLS-domain", @@ -133,8 +132,8 @@ fn it_limits() { } } ] - } - ] + }] + }] }] }"#; @@ -176,21 +175,18 @@ fn it_limits() { .returning(Some("cars.toystore.com".as_bytes())) .expect_get_property(Some(vec!["request", "method"])) .returning(Some("POST".as_bytes())) + .expect_log(Some(LogLevel::Debug), Some("#2 policy selected some-name")) .expect_log( Some(LogLevel::Debug), - Some("#2 ratelimitpolicy selected some-name"), + Some("get_property: selector: request.url_path path: [\"request\", \"url_path\"]"), ) .expect_log( Some(LogLevel::Debug), - Some("#2 get_property: selector: request.url_path path: [\"request\", \"url_path\"]"), + Some("get_property: selector: request.host path: [\"request\", \"host\"]"), ) .expect_log( Some(LogLevel::Debug), - Some("#2 get_property: selector: request.host path: [\"request\", \"host\"]"), - ) - .expect_log( - Some(LogLevel::Debug), - Some("#2 get_property: selector: request.method path: [\"request\", \"method\"]"), + Some("get_property: selector: request.method path: [\"request\", \"method\"]"), ) .expect_grpc_call( Some("limitador-cluster"), @@ -276,9 +272,8 @@ fn it_passes_additional_headers() { "operator": "eq", "value": "POST" } - ] - }], - "actions": [ + ], + "actions": [ { "extension": "limitador", "scope": "RLS-domain", @@ -290,8 +285,8 @@ fn it_passes_additional_headers() { } } ] - } - ] + }] + }] }] }"#; @@ -333,21 +328,18 @@ fn it_passes_additional_headers() { .returning(Some("cars.toystore.com".as_bytes())) .expect_get_property(Some(vec!["request", "method"])) .returning(Some("POST".as_bytes())) + .expect_log(Some(LogLevel::Debug), Some("#2 policy selected some-name")) .expect_log( Some(LogLevel::Debug), - Some("#2 ratelimitpolicy selected some-name"), + Some("get_property: selector: request.url_path path: [\"request\", \"url_path\"]"), ) .expect_log( Some(LogLevel::Debug), - Some("#2 get_property: selector: request.url_path path: [\"request\", \"url_path\"]"), + Some("get_property: selector: request.host path: [\"request\", \"host\"]"), ) .expect_log( Some(LogLevel::Debug), - Some("#2 get_property: selector: request.host path: [\"request\", \"host\"]"), - ) - .expect_log( - Some(LogLevel::Debug), - Some("#2 get_property: selector: request.method path: [\"request\", \"method\"]"), + Some("get_property: selector: request.method path: [\"request\", \"method\"]"), ) .expect_grpc_call( Some("limitador-cluster"), @@ -430,11 +422,9 @@ fn it_rate_limits_with_empty_conditions() { "name": "some-name", "hostnames": ["*.com"], "rules": [ - { - "conditions": [] - } - ], - "actions": [ + { + "conditions": [], + "actions": [ { "extension": "limitador", "scope": "RLS-domain", @@ -446,8 +436,8 @@ fn it_rate_limits_with_empty_conditions() { } } ] - } - ] + }] + }] }] }"#; @@ -483,10 +473,7 @@ fn it_rate_limits_with_empty_conditions() { .returning(None) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("baggage")) .returning(None) - .expect_log( - Some(LogLevel::Debug), - Some("#2 ratelimitpolicy selected some-name"), - ) + .expect_log(Some(LogLevel::Debug), Some("#2 policy selected some-name")) .expect_grpc_call( Some("limitador-cluster"), Some("envoy.service.ratelimit.v3.RateLimitService"), @@ -554,11 +541,9 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value "name": "some-name", "hostnames": ["*.com"], "rules": [ - { - "conditions": [] - } - ], - "actions": [ + { + "conditions": [], + "actions": [ { "extension": "limitador", "scope": "RLS-domain", @@ -569,8 +554,8 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value } } ] - } - ] + }] + }] }] }"#; @@ -604,19 +589,19 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value .returning(None) .expect_log( Some(LogLevel::Debug), - Some("#2 ratelimitpolicy selected some-name"), + Some("#2 policy selected some-name"), ) .expect_log( Some(LogLevel::Debug), - Some("#2 get_property: selector: unknown.path path: Path { tokens: [\"unknown\", \"path\"] }"), + Some("get_property: selector: unknown.path path: Path { tokens: [\"unknown\", \"path\"] }"), ) .expect_log( Some(LogLevel::Debug), - Some("#2 build_single_descriptor: selector not found: unknown.path"), + Some("build_single_descriptor: selector not found: unknown.path"), ) .expect_log( Some(LogLevel::Debug), - Some("#2 process_rate_limit_policy: empty descriptors"), + Some("process_policy: empty descriptors"), ) .execute_and_expect(ReturnType::Action(Action::Continue)) .unwrap(); diff --git a/utils/deploy/envoy-notls.yaml b/utils/deploy/envoy-notls.yaml index 31848620..e73be330 100644 --- a/utils/deploy/envoy-notls.yaml +++ b/utils/deploy/envoy-notls.yaml @@ -161,15 +161,15 @@ data: "operator": "eq", "value": "/get" } + ], + "actions": [ + { + "extension": "authorino", + "scope": "effective-route-1", + "data": [] + } ] } - ], - "actions": [ - { - "extension": "authorino", - "scope": "effective-route-1", - "data": [] - } ] }, { @@ -179,18 +179,18 @@ data: ], "rules": [ { - "conditions": [] - } - ], - "actions": [ - { - "extension": "limitador", - "scope": "rlp-ns-A/rlp-name-A", - "data": [ + "conditions": [], + "actions": [ { - "selector": { - "selector": "unknown.path" - } + "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", + "data": [ + { + "selector": { + "selector": "unknown.path" + } + } + ] } ] } @@ -209,19 +209,19 @@ data: "operator": "startswith", "value": "/unknown-path" } - ] - } - ], - "actions": [ - { - "extension": "limitador", - "scope": "rlp-ns-B/rlp-name-B", - "data": [ + ], + "actions": [ { - "static": { - "key": "rlp-ns-B/rlp-name-B/limit-not-to-be-activated", - "value": "1" - } + "extension": "limitador", + "scope": "rlp-ns-B/rlp-name-B", + "data": [ + { + "static": { + "key": "rlp-ns-B/rlp-name-B/limit-not-to-be-activated", + "value": "1" + } + } + ] } ] } @@ -250,35 +250,35 @@ data: "operator": "eq", "value": "GET" } - ] - } - ], - "actions": [ - { - "extension": "limitador", - "scope": "rlp-ns-C/rlp-name-C", - "data": [ - { - "static": { - "key": "limit_to_be_activated", - "value": "1" - } - }, - { - "selector": { - "selector": "source.address" - } - }, - { - "selector": { - "selector": "request.headers.My-Custom-Header-01" - } - }, + ], + "actions": [ { - "selector": { - "selector": "metadata.filter_metadata.envoy\\.filters\\.http\\.header_to_metadata.user_id", - "key": "user_id" - } + "extension": "limitador", + "scope": "rlp-ns-C/rlp-name-C", + "data": [ + { + "static": { + "key": "limit_to_be_activated", + "value": "1" + } + }, + { + "selector": { + "selector": "source.address" + } + }, + { + "selector": { + "selector": "request.headers.My-Custom-Header-01" + } + }, + { + "selector": { + "selector": "metadata.filter_metadata.envoy\\.filters\\.http\\.header_to_metadata.user_id", + "key": "user_id" + } + } + ] } ] } @@ -297,24 +297,24 @@ data: "operator": "eq", "value": "/get" } - ] - } - ], - "actions": [ - { - "extension": "authorino", - "scope": "effective-route-1", - "data": [] - }, - { - "extension": "limitador", - "scope": "multi-ns-A/multi-name-A", - "data": [ + ], + "actions": [ + { + "extension": "authorino", + "scope": "effective-route-1", + "data": [] + }, { - "selector": { - "selector": "filter_state.wasm\\.kuadrant\\.identity\\.userid", - "key": "user_id" - } + "extension": "limitador", + "scope": "multi-ns-A/multi-name-A", + "data": [ + { + "selector": { + "selector": "filter_state.wasm\\.kuadrant\\.identity\\.userid", + "key": "user_id" + } + } + ] } ] } diff --git a/utils/deploy/envoy-tls.yaml b/utils/deploy/envoy-tls.yaml index d420595f..86264f2e 100644 --- a/utils/deploy/envoy-tls.yaml +++ b/utils/deploy/envoy-tls.yaml @@ -170,15 +170,15 @@ data: "operator": "eq", "value": "/get" } + ], + "actions": [ + { + "extension": "authorino", + "scope": "effective-route-1", + "data": [] + } ] } - ], - "actions": [ - { - "extension": "authorino", - "scope": "effective-route-1", - "data": [] - } ] }, { @@ -188,18 +188,18 @@ data: ], "rules": [ { - "conditions": [] - } - ], - "actions": [ - { - "extension": "limitador", - "scope": "rlp-ns-A/rlp-name-A", - "data": [ + "conditions": [], + "actions": [ { - "selector": { - "selector": "unknown.path" - } + "extension": "limitador", + "scope": "rlp-ns-A/rlp-name-A", + "data": [ + { + "selector": { + "selector": "unknown.path" + } + } + ] } ] } @@ -218,19 +218,19 @@ data: "operator": "startswith", "value": "/unknown-path" } - ] - } - ], - "actions": [ - { - "extension": "limitador", - "scope": "rlp-ns-B/rlp-name-B", - "data": [ + ], + "actions": [ { - "static": { - "key": "rlp-ns-B/rlp-name-B/limit-not-to-be-activated", - "value": "1" - } + "extension": "limitador", + "scope": "rlp-ns-B/rlp-name-B", + "data": [ + { + "static": { + "key": "rlp-ns-B/rlp-name-B/limit-not-to-be-activated", + "value": "1" + } + } + ] } ] } @@ -259,35 +259,35 @@ data: "operator": "eq", "value": "GET" } - ] - } - ], - "actions": [ - { - "extension": "limitador", - "scope": "rlp-ns-C/rlp-name-C", - "data": [ - { - "static": { - "key": "limit_to_be_activated", - "value": "1" - } - }, - { - "selector": { - "selector": "source.address" - } - }, - { - "selector": { - "selector": "request.headers.My-Custom-Header-01" - } - }, + ], + "actions": [ { - "selector": { - "selector": "metadata.filter_metadata.envoy\\.filters\\.http\\.header_to_metadata.user_id", - "key": "user_id" - } + "extension": "limitador", + "scope": "rlp-ns-C/rlp-name-C", + "data": [ + { + "static": { + "key": "limit_to_be_activated", + "value": "1" + } + }, + { + "selector": { + "selector": "source.address" + } + }, + { + "selector": { + "selector": "request.headers.My-Custom-Header-01" + } + }, + { + "selector": { + "selector": "metadata.filter_metadata.envoy\\.filters\\.http\\.header_to_metadata.user_id", + "key": "user_id" + } + } + ] } ] } @@ -306,24 +306,24 @@ data: "operator": "eq", "value": "/get" } - ] - } - ], - "actions": [ - { - "extension": "authorino", - "scope": "effective-route-1", - "data": [] - }, - { - "extension": "limitador", - "scope": "multi-ns-A/multi-name-A", - "data": [ + ], + "actions": [ + { + "extension": "authorino", + "scope": "effective-route-1", + "data": [] + }, { - "selector": { - "selector": "filter_state.wasm\\.kuadrant\\.identity\\.userid", - "key": "user_id" - } + "extension": "limitador", + "scope": "multi-ns-A/multi-name-A", + "data": [ + { + "selector": { + "selector": "filter_state.wasm\\.kuadrant\\.identity\\.userid", + "key": "user_id" + } + } + ] } ] } From 68c25af3c9209aee84f5c1f3199586fb395fc507 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Mon, 30 Sep 2024 11:20:15 +0100 Subject: [PATCH 59/66] Match on all conditions not any Signed-off-by: Adam Cattermole --- src/policy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/policy.rs b/src/policy.rs index c85eae8c..26bc6dcd 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -41,7 +41,7 @@ impl Policy { conditions .iter() - .any(|condition| self.pattern_expression_applies(condition)) + .all(|condition| self.pattern_expression_applies(condition)) } fn pattern_expression_applies(&self, p_e: &PatternExpression) -> bool { From 2dc5d1b5b31f75e3cad600b9393925534ddf85b5 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Mon, 30 Sep 2024 11:27:59 +0100 Subject: [PATCH 60/66] Re-order test expectations to follow exactly the execution order Signed-off-by: Adam Cattermole --- tests/rate_limited.rs | 54 +++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/rate_limited.rs b/tests/rate_limited.rs index dda0702b..cac1a730 100644 --- a/tests/rate_limited.rs +++ b/tests/rate_limited.rs @@ -163,31 +163,31 @@ fn it_limits() { .expect_log(Some(LogLevel::Debug), Some("#2 on_http_request_headers")) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some(":authority")) .returning(Some("cars.toystore.com")) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("traceparent")) - .returning(None) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("tracestate")) - .returning(None) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("baggage")) - .returning(None) - .expect_get_property(Some(vec!["request", "url_path"])) - .returning(Some("/admin/toy".as_bytes())) - .expect_get_property(Some(vec!["request", "host"])) - .returning(Some("cars.toystore.com".as_bytes())) - .expect_get_property(Some(vec!["request", "method"])) - .returning(Some("POST".as_bytes())) .expect_log(Some(LogLevel::Debug), Some("#2 policy selected some-name")) .expect_log( Some(LogLevel::Debug), Some("get_property: selector: request.url_path path: [\"request\", \"url_path\"]"), ) + .expect_get_property(Some(vec!["request", "url_path"])) + .returning(Some("/admin/toy".as_bytes())) .expect_log( Some(LogLevel::Debug), Some("get_property: selector: request.host path: [\"request\", \"host\"]"), ) + .expect_get_property(Some(vec!["request", "host"])) + .returning(Some("cars.toystore.com".as_bytes())) .expect_log( Some(LogLevel::Debug), Some("get_property: selector: request.method path: [\"request\", \"method\"]"), ) + .expect_get_property(Some(vec!["request", "method"])) + .returning(Some("POST".as_bytes())) + .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("traceparent")) + .returning(None) + .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("tracestate")) + .returning(None) + .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("baggage")) + .returning(None) .expect_grpc_call( Some("limitador-cluster"), Some("envoy.service.ratelimit.v3.RateLimitService"), @@ -316,31 +316,31 @@ fn it_passes_additional_headers() { .expect_log(Some(LogLevel::Debug), Some("#2 on_http_request_headers")) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some(":authority")) .returning(Some("cars.toystore.com")) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("traceparent")) - .returning(None) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("tracestate")) - .returning(None) - .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("baggage")) - .returning(None) - .expect_get_property(Some(vec!["request", "url_path"])) - .returning(Some("/admin/toy".as_bytes())) - .expect_get_property(Some(vec!["request", "host"])) - .returning(Some("cars.toystore.com".as_bytes())) - .expect_get_property(Some(vec!["request", "method"])) - .returning(Some("POST".as_bytes())) .expect_log(Some(LogLevel::Debug), Some("#2 policy selected some-name")) .expect_log( Some(LogLevel::Debug), Some("get_property: selector: request.url_path path: [\"request\", \"url_path\"]"), ) + .expect_get_property(Some(vec!["request", "url_path"])) + .returning(Some("/admin/toy".as_bytes())) .expect_log( Some(LogLevel::Debug), Some("get_property: selector: request.host path: [\"request\", \"host\"]"), ) + .expect_get_property(Some(vec!["request", "host"])) + .returning(Some("cars.toystore.com".as_bytes())) .expect_log( Some(LogLevel::Debug), Some("get_property: selector: request.method path: [\"request\", \"method\"]"), ) + .expect_get_property(Some(vec!["request", "method"])) + .returning(Some("POST".as_bytes())) + .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("traceparent")) + .returning(None) + .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("tracestate")) + .returning(None) + .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("baggage")) + .returning(None) .expect_grpc_call( Some("limitador-cluster"), Some("envoy.service.ratelimit.v3.RateLimitService"), @@ -467,13 +467,13 @@ fn it_rate_limits_with_empty_conditions() { .expect_log(Some(LogLevel::Debug), Some("#2 on_http_request_headers")) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some(":authority")) .returning(Some("a.com")) + .expect_log(Some(LogLevel::Debug), Some("#2 policy selected some-name")) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("traceparent")) .returning(None) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("tracestate")) .returning(None) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some("baggage")) .returning(None) - .expect_log(Some(LogLevel::Debug), Some("#2 policy selected some-name")) .expect_grpc_call( Some("limitador-cluster"), Some("envoy.service.ratelimit.v3.RateLimitService"), @@ -585,8 +585,6 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value .expect_log(Some(LogLevel::Debug), Some("#2 on_http_request_headers")) .expect_get_header_map_value(Some(MapType::HttpRequestHeaders), Some(":authority")) .returning(Some("a.com")) - .expect_get_property(Some(vec!["unknown", "path"])) - .returning(None) .expect_log( Some(LogLevel::Debug), Some("#2 policy selected some-name"), @@ -595,6 +593,8 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value Some(LogLevel::Debug), Some("get_property: selector: unknown.path path: Path { tokens: [\"unknown\", \"path\"] }"), ) + .expect_get_property(Some(vec!["unknown", "path"])) + .returning(None) .expect_log( Some(LogLevel::Debug), Some("build_single_descriptor: selector not found: unknown.path"), From a5c47e03b71cbe76606dcfe9e23eb07d3d5cbdc1 Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Mon, 30 Sep 2024 15:23:07 +0100 Subject: [PATCH 61/66] Move building of descriptors to action time Signed-off-by: Adam Cattermole --- src/operation_dispatcher.rs | 148 ++++++++++++++++++++++-------------- src/service.rs | 9 +-- src/service/grpc_message.rs | 32 ++++---- tests/rate_limited.rs | 2 +- 4 files changed, 112 insertions(+), 79 deletions(-) diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index b89785ae..792bbca8 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -1,11 +1,11 @@ -use crate::configuration::{Extension, ExtensionType, FailureMode}; +use crate::configuration::{Action, Extension, ExtensionType, FailureMode}; use crate::policy::Rule; use crate::service::grpc_message::GrpcMessageRequest; -use crate::service::{GetMapValuesBytesFn, GrpcCallFn, GrpcServiceHandler}; -use log::debug; +use crate::service::{GetMapValuesBytesFn, GrpcCallFn, GrpcMessageBuildFn, GrpcServiceHandler}; +use log::error; use proxy_wasm::hostcalls; use proxy_wasm::types::{Bytes, MapType, Status}; -use std::cell::RefCell; +use std::cell::{RefCell, RefMut}; use std::collections::HashMap; use std::rc::Rc; use std::time::Duration; @@ -27,9 +27,11 @@ impl State { _ => {} } } -} -type Procedure = (Rc, GrpcMessageRequest); + fn done(&mut self) { + *self = State::Done + } +} #[allow(dead_code)] #[derive(Clone)] @@ -37,34 +39,44 @@ pub(crate) struct Operation { state: State, result: Result, extension: Rc, - procedure: Procedure, + action: Action, + service: Rc, grpc_call_fn: GrpcCallFn, get_map_values_bytes_fn: GetMapValuesBytesFn, + grpc_message_build_fn: GrpcMessageBuildFn, } #[allow(dead_code)] impl Operation { - pub fn new(extension: Rc, procedure: Procedure) -> Self { + pub fn new(extension: Rc, action: Action, service: Rc) -> Self { Self { state: State::Pending, result: Ok(0), // Heuristics: zero represents that it's not been triggered, following `hostcalls` example extension, - procedure, + action, + service, grpc_call_fn, get_map_values_bytes_fn, + grpc_message_build_fn, } } fn trigger(&mut self) -> Result { match self.state { State::Pending => { - self.result = self.procedure.0.send( - self.get_map_values_bytes_fn, - self.grpc_call_fn, - self.procedure.1.clone(), - ); - self.state.next(); - self.result + if let Some(message) = + (self.grpc_message_build_fn)(self.get_extension_type(), &self.action) + { + self.result = + self.service + .send(self.get_map_values_bytes_fn, self.grpc_call_fn, message); + self.state.next(); + self.result + } else { + //todo: we need to move to and start the next action + self.state.done(); + Ok(1234) + } } State::Waiting => { self.state.next(); @@ -124,26 +136,10 @@ impl OperationDispatcher { for action in rule.actions.iter() { // TODO(didierofrivia): Error handling if let Some(service) = self.service_handlers.get(&action.extension) { - let descriptors = match service.get_extension_type() { - ExtensionType::Auth => None, - ExtensionType::RateLimit => { - let desc = action.build_descriptors(); - if desc.is_empty() { - debug!("process_policy: empty descriptors"); - continue; - } - Some(desc) - } - }; - - let message = GrpcMessageRequest::new( - service.get_extension_type(), - action.scope.clone(), - descriptors.clone(), - ); operations.push(Operation::new( service.get_extension(), - (Rc::clone(service), message), + action.clone(), + Rc::clone(service), )) } } @@ -166,27 +162,48 @@ impl OperationDispatcher { } pub fn next(&self) -> Option { - let mut operations = self.operations.borrow_mut(); + let operations = self.operations.borrow_mut(); + self.step(operations) + } + + fn step(&self, mut operations: RefMut>) -> Option { if let Some((i, operation)) = operations.iter_mut().enumerate().next() { - if let State::Done = operation.get_state() { - if let Ok(token_id) = operation.result { - self.waiting_operations.borrow_mut().remove(&token_id); - } // If result was Err, means the operation wasn't indexed - operations.remove(i); - // The next op is now at `i` - } - if let Some(operation) = operations.get_mut(i) { - if let Ok(token_id) = operation.trigger() { - if *operation.get_state() == State::Waiting { - // We index only if it was just transitioned to Waiting after triggering - self.waiting_operations - .borrow_mut() - .insert(token_id, operation.clone()); - } // TODO(didierofrivia): Decide on indexing the failed operations. + match operation.get_state() { + State::Pending => { + match operation.trigger() { + Ok(token_id) => { + match operation.get_state() { + State::Pending => { + panic!("Operation dispatcher reached an undefined state"); + } + State::Waiting => { + // We index only if it was just transitioned to Waiting after triggering + self.waiting_operations + .borrow_mut() + .insert(token_id, operation.clone()); + // TODO(didierofrivia): Decide on indexing the failed operations. + Some(operation.clone()) + } + State::Done => self.step(operations), + } + } + Err(status) => { + error!("{status:?}"); + None + } + } + } + State::Waiting => { + let _ = operation.trigger(); + Some(operation.clone()) + } + State::Done => { + if let Ok(token_id) = operation.result { + self.waiting_operations.borrow_mut().remove(&token_id); + } // If result was Err, means the operation wasn't indexed + operations.remove(i); + self.step(operations) } - Some(operation.clone()) - } else { - None } } else { None @@ -216,6 +233,13 @@ fn get_map_values_bytes_fn(map_type: MapType, key: &str) -> Result hostcalls::get_map_value_bytes(map_type, key) } +fn grpc_message_build_fn( + extension_type: &ExtensionType, + action: &Action, +) -> Option { + GrpcMessageRequest::new(extension_type, action) +} + #[cfg(test)] mod tests { use super::*; @@ -241,6 +265,13 @@ mod tests { Ok(Some(Vec::new())) } + fn grpc_message_build_fn_stub( + _extension_type: &ExtensionType, + _action: &Action, + ) -> Option { + Some(GrpcMessageRequest::RateLimit(build_message())) + } + fn build_grpc_service_handler() -> GrpcServiceHandler { GrpcServiceHandler::new(Rc::new(Default::default()), Rc::new(Default::default())) } @@ -264,12 +295,15 @@ mod tests { endpoint: "local".to_string(), failure_mode: FailureMode::Deny, }), - procedure: ( - Rc::new(build_grpc_service_handler()), - GrpcMessageRequest::RateLimit(build_message()), - ), + action: Action { + extension: "local".to_string(), + scope: "".to_string(), + data: vec![], + }, + service: Rc::new(build_grpc_service_handler()), grpc_call_fn: grpc_call_fn_stub, get_map_values_bytes_fn: get_map_values_bytes_fn_stub, + grpc_message_build_fn: grpc_message_build_fn_stub, } } diff --git a/src/service.rs b/src/service.rs index e712dfdb..9e988a1a 100644 --- a/src/service.rs +++ b/src/service.rs @@ -2,7 +2,7 @@ pub(crate) mod auth; pub(crate) mod grpc_message; pub(crate) mod rate_limit; -use crate::configuration::{Extension, ExtensionType, FailureMode}; +use crate::configuration::{Action, Extension, ExtensionType, FailureMode}; use crate::service::auth::{AUTH_METHOD_NAME, AUTH_SERVICE_NAME}; use crate::service::grpc_message::GrpcMessageRequest; use crate::service::rate_limit::{RATELIMIT_METHOD_NAME, RATELIMIT_SERVICE_NAME}; @@ -63,6 +63,9 @@ pub type GrpcCallFn = fn( pub type GetMapValuesBytesFn = fn(map_type: MapType, key: &str) -> Result, Status>; +pub type GrpcMessageBuildFn = + fn(extension_type: &ExtensionType, action: &Action) -> Option; + pub struct GrpcServiceHandler { service: Rc, header_resolver: Rc, @@ -103,10 +106,6 @@ impl GrpcServiceHandler { pub fn get_extension(&self) -> Rc { Rc::clone(&self.service.extension) } - - pub fn get_extension_type(&self) -> ExtensionType { - self.service.extension.extension_type.clone() - } } pub struct HeaderResolver { diff --git a/src/service/grpc_message.rs b/src/service/grpc_message.rs index 40df200b..6654c84a 100644 --- a/src/service/grpc_message.rs +++ b/src/service/grpc_message.rs @@ -1,9 +1,8 @@ -use crate::configuration::ExtensionType; -use crate::envoy::{ - CheckRequest, CheckResponse, RateLimitDescriptor, RateLimitRequest, RateLimitResponse, -}; +use crate::configuration::{Action, ExtensionType}; +use crate::envoy::{CheckRequest, CheckResponse, RateLimitRequest, RateLimitResponse}; use crate::service::auth::AuthService; use crate::service::rate_limit::RateLimitService; +use log::debug; use protobuf::reflect::MessageDescriptor; use protobuf::{ Clear, CodedInputStream, CodedOutputStream, Message, ProtobufError, ProtobufResult, @@ -124,21 +123,22 @@ impl Message for GrpcMessageRequest { impl GrpcMessageRequest { // Using domain as ce_host for the time being, we might pass a DataType in the future. - pub fn new( - extension_type: ExtensionType, - domain: String, - descriptors: Option>, - ) -> Self { + pub fn new(extension_type: &ExtensionType, action: &Action) -> Option { match extension_type { ExtensionType::RateLimit => { - GrpcMessageRequest::RateLimit(RateLimitService::request_message( - domain.clone(), - descriptors.expect("rate limit message expects descriptors"), - )) - } - ExtensionType::Auth => { - GrpcMessageRequest::Auth(AuthService::request_message(domain.clone())) + let descriptors = action.build_descriptors(); + if descriptors.is_empty() { + debug!("grpc_message_request: empty descriptors"); + None + } else { + Some(GrpcMessageRequest::RateLimit( + RateLimitService::request_message(action.scope.clone(), descriptors), + )) + } } + ExtensionType::Auth => Some(GrpcMessageRequest::Auth(AuthService::request_message( + action.scope.clone(), + ))), } } } diff --git a/tests/rate_limited.rs b/tests/rate_limited.rs index cac1a730..bd3acc19 100644 --- a/tests/rate_limited.rs +++ b/tests/rate_limited.rs @@ -601,7 +601,7 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value ) .expect_log( Some(LogLevel::Debug), - Some("process_policy: empty descriptors"), + Some("grpc_message_request: empty descriptors"), ) .execute_and_expect(ReturnType::Action(Action::Continue)) .unwrap(); From ea58e1dd813fc12951a611da9a66ddaf228824fb Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Tue, 1 Oct 2024 10:48:00 +0100 Subject: [PATCH 62/66] Revert removal of allOf within conditions Signed-off-by: Adam Cattermole --- src/configuration.rs | 115 +++++++++++++++++++--------------- src/policy.rs | 20 +++++- tests/rate_limited.rs | 14 +++-- utils/deploy/envoy-notls.yaml | 61 +++++++++++------- utils/deploy/envoy-tls.yaml | 61 +++++++++++------- 5 files changed, 166 insertions(+), 105 deletions(-) diff --git a/src/configuration.rs b/src/configuration.rs index 78a7f8e8..c7fa9b8f 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -466,10 +466,12 @@ impl TryFrom for FilterConfig { for rlp in config.policies.iter() { for rule in &rlp.rules { - for pe in &rule.conditions { - let result = pe.compile(); - if result.is_err() { - return Err(result.err().unwrap()); + for condition in &rule.conditions { + for pe in &condition.all_of { + let result = pe.compile(); + if result.is_err() { + return Err(result.err().unwrap()); + } } } for action in &rule.actions { @@ -636,19 +638,22 @@ mod test { { "conditions": [ { - "selector": "request.path", - "operator": "eq", - "value": "/admin/toy" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "POST" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "cars.toystore.com" + "allOf": [ + { + "selector": "request.path", + "operator": "eq", + "value": "/admin/toy" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "POST" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "cars.toystore.com" + }] }], "actions": [ { @@ -686,7 +691,10 @@ mod test { assert_eq!(rules.len(), 1); let conditions = &rules[0].conditions; - assert_eq!(conditions.len(), 3); + assert_eq!(conditions.len(), 1); + + let all_of_conditions = &conditions[0].all_of; + assert_eq!(all_of_conditions.len(), 3); let actions = &rules[0].actions; assert_eq!(actions.len(), 1); @@ -753,7 +761,6 @@ mod test { "hostnames": ["*.toystore.com", "example.com"], "rules": [ { - "conditions": [], "actions": [ { "extension": "limitador", @@ -818,29 +825,32 @@ mod test { { "conditions": [ { - "selector": "request.path", - "operator": "eq", - "value": "/admin/toy" - }, - { - "selector": "request.method", - "operator": "neq", - "value": "POST" - }, - { - "selector": "request.host", - "operator": "startswith", - "value": "cars." - }, - { - "selector": "request.host", - "operator": "endswith", - "value": ".com" - }, - { - "selector": "request.host", - "operator": "matches", - "value": "*.com" + "allOf": [ + { + "selector": "request.path", + "operator": "eq", + "value": "/admin/toy" + }, + { + "selector": "request.method", + "operator": "neq", + "value": "POST" + }, + { + "selector": "request.host", + "operator": "startswith", + "value": "cars." + }, + { + "selector": "request.host", + "operator": "endswith", + "value": ".com" + }, + { + "selector": "request.host", + "operator": "matches", + "value": "*.com" + }] }], "actions": [ { @@ -864,7 +874,10 @@ mod test { assert_eq!(rules.len(), 1); let conditions = &rules[0].conditions; - assert_eq!(conditions.len(), 5); + assert_eq!(conditions.len(), 1); + + let all_of_conditions = &conditions[0].all_of; + assert_eq!(all_of_conditions.len(), 5); let expected_conditions = [ // selector, value, operator @@ -876,9 +889,9 @@ mod test { ]; for i in 0..expected_conditions.len() { - assert_eq!(conditions[i].selector, expected_conditions[i].0); - assert_eq!(conditions[i].value, expected_conditions[i].1); - assert_eq!(conditions[i].operator, expected_conditions[i].2); + assert_eq!(all_of_conditions[i].selector, expected_conditions[i].0); + assert_eq!(all_of_conditions[i].value, expected_conditions[i].1); + assert_eq!(all_of_conditions[i].operator, expected_conditions[i].2); } } @@ -898,7 +911,6 @@ mod test { "hostnames": ["*.toystore.com", "example.com"], "rules": [ { - "conditions": [], "actions": [ { "extension": "limitador", @@ -1024,9 +1036,12 @@ mod test { { "conditions": [ { - "selector": "request.path", - "operator": "unknown", - "value": "/admin/toy" + "allOf": [ + { + "selector": "request.path", + "operator": "unknown", + "value": "/admin/toy" + }] }], "actions": [ { diff --git a/src/policy.rs b/src/policy.rs index 26bc6dcd..db9ed9fe 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -3,9 +3,16 @@ use log::debug; use proxy_wasm::hostcalls; use serde::Deserialize; +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Condition { + pub all_of: Vec, +} + #[derive(Deserialize, Debug, Clone)] pub struct Rule { - pub conditions: Vec, + #[serde(default)] + pub conditions: Vec, pub actions: Vec, } @@ -33,7 +40,7 @@ impl Policy { .find(|rule: &&Rule| self.filter_rule_by_conditions(&rule.conditions)) } - fn filter_rule_by_conditions(&self, conditions: &[PatternExpression]) -> bool { + fn filter_rule_by_conditions(&self, conditions: &[Condition]) -> bool { if conditions.is_empty() { // no conditions is equivalent to matching all the requests. return true; @@ -41,7 +48,14 @@ impl Policy { conditions .iter() - .all(|condition| self.pattern_expression_applies(condition)) + .any(|condition| self.condition_applies(condition)) + } + + fn condition_applies(&self, condition: &Condition) -> bool { + condition + .all_of + .iter() + .all(|pattern_expression| self.pattern_expression_applies(pattern_expression)) } fn pattern_expression_applies(&self, p_e: &PatternExpression) -> bool { diff --git a/tests/rate_limited.rs b/tests/rate_limited.rs index bd3acc19..21ba4988 100644 --- a/tests/rate_limited.rs +++ b/tests/rate_limited.rs @@ -104,6 +104,8 @@ fn it_limits() { "rules": [ { "conditions": [ + { + "allOf": [ { "selector": "request.url_path", "operator": "startswith", @@ -118,8 +120,8 @@ fn it_limits() { "selector": "request.method", "operator": "eq", "value": "POST" - } - ], + }] + }], "actions": [ { "extension": "limitador", @@ -257,6 +259,8 @@ fn it_passes_additional_headers() { "rules": [ { "conditions": [ + { + "allOf": [ { "selector": "request.url_path", "operator": "startswith", @@ -271,8 +275,8 @@ fn it_passes_additional_headers() { "selector": "request.method", "operator": "eq", "value": "POST" - } - ], + }] + }], "actions": [ { "extension": "limitador", @@ -423,7 +427,6 @@ fn it_rate_limits_with_empty_conditions() { "hostnames": ["*.com"], "rules": [ { - "conditions": [], "actions": [ { "extension": "limitador", @@ -542,7 +545,6 @@ fn it_does_not_rate_limits_when_selector_does_not_exist_and_misses_default_value "hostnames": ["*.com"], "rules": [ { - "conditions": [], "actions": [ { "extension": "limitador", diff --git a/utils/deploy/envoy-notls.yaml b/utils/deploy/envoy-notls.yaml index e73be330..d116429b 100644 --- a/utils/deploy/envoy-notls.yaml +++ b/utils/deploy/envoy-notls.yaml @@ -157,9 +157,13 @@ data: { "conditions": [ { - "selector": "request.path", - "operator": "eq", - "value": "/get" + "allOf": [ + { + "selector": "request.path", + "operator": "eq", + "value": "/get" + } + ] } ], "actions": [ @@ -179,7 +183,6 @@ data: ], "rules": [ { - "conditions": [], "actions": [ { "extension": "limitador", @@ -205,9 +208,13 @@ data: { "conditions": [ { - "selector": "request.url_path", - "operator": "startswith", - "value": "/unknown-path" + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/unknown-path" + } + ] } ], "actions": [ @@ -236,19 +243,23 @@ data: { "conditions": [ { - "selector": "request.url_path", - "operator": "startswith", - "value": "/get" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "test.c.rlp.com" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "GET" + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/get" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "test.c.rlp.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "GET" + } + ] } ], "actions": [ @@ -293,9 +304,13 @@ data: { "conditions": [ { - "selector": "request.path", - "operator": "eq", - "value": "/get" + "allOf": [ + { + "selector": "request.path", + "operator": "eq", + "value": "/get" + } + ] } ], "actions": [ diff --git a/utils/deploy/envoy-tls.yaml b/utils/deploy/envoy-tls.yaml index 86264f2e..2e46c9b5 100644 --- a/utils/deploy/envoy-tls.yaml +++ b/utils/deploy/envoy-tls.yaml @@ -166,9 +166,13 @@ data: { "conditions": [ { - "selector": "request.path", - "operator": "eq", - "value": "/get" + "allOf": [ + { + "selector": "request.path", + "operator": "eq", + "value": "/get" + } + ] } ], "actions": [ @@ -188,7 +192,6 @@ data: ], "rules": [ { - "conditions": [], "actions": [ { "extension": "limitador", @@ -214,9 +217,13 @@ data: { "conditions": [ { - "selector": "request.url_path", - "operator": "startswith", - "value": "/unknown-path" + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/unknown-path" + } + ] } ], "actions": [ @@ -245,19 +252,23 @@ data: { "conditions": [ { - "selector": "request.url_path", - "operator": "startswith", - "value": "/get" - }, - { - "selector": "request.host", - "operator": "eq", - "value": "test.c.rlp.com" - }, - { - "selector": "request.method", - "operator": "eq", - "value": "GET" + "allOf": [ + { + "selector": "request.url_path", + "operator": "startswith", + "value": "/get" + }, + { + "selector": "request.host", + "operator": "eq", + "value": "test.c.rlp.com" + }, + { + "selector": "request.method", + "operator": "eq", + "value": "GET" + } + ] } ], "actions": [ @@ -302,9 +313,13 @@ data: { "conditions": [ { - "selector": "request.path", - "operator": "eq", - "value": "/get" + "allOf": [ + { + "selector": "request.path", + "operator": "eq", + "value": "/get" + } + ] } ], "actions": [ From 76c9bea1926f7b82a23d57e8dca8e660b6fad8f3 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 1 Oct 2024 15:47:03 +0200 Subject: [PATCH 63/66] [refactor] OperationDispatcher not using RefCell for storing operations * Operations store its status and result in RefCell for interior mut * OperationDispatcher keeps a Vec of Rc, then indexes cloning the Rc instead of cloning the entire object. Signed-off-by: dd di cesare --- src/operation_dispatcher.rs | 180 +++++++++++++++++------------------- 1 file changed, 84 insertions(+), 96 deletions(-) diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 792bbca8..65b9d064 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -5,13 +5,13 @@ use crate::service::{GetMapValuesBytesFn, GrpcCallFn, GrpcMessageBuildFn, GrpcSe use log::error; use proxy_wasm::hostcalls; use proxy_wasm::types::{Bytes, MapType, Status}; -use std::cell::{RefCell, RefMut}; +use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use std::time::Duration; #[allow(dead_code)] -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone, Copy)] pub(crate) enum State { Pending, Waiting, @@ -36,8 +36,8 @@ impl State { #[allow(dead_code)] #[derive(Clone)] pub(crate) struct Operation { - state: State, - result: Result, + state: RefCell, + result: RefCell>, extension: Rc, action: Action, service: Rc, @@ -50,8 +50,8 @@ pub(crate) struct Operation { impl Operation { pub fn new(extension: Rc, action: Action, service: Rc) -> Self { Self { - state: State::Pending, - result: Ok(0), // Heuristics: zero represents that it's not been triggered, following `hostcalls` example + state: RefCell::new(State::Pending), + result: RefCell::new(Ok(0)), // Heuristics: zero represents that it's not been triggered, following `hostcalls` example extension, action, service, @@ -61,37 +61,50 @@ impl Operation { } } - fn trigger(&mut self) -> Result { - match self.state { + fn trigger(&self) -> Result { + match self.get_state() { State::Pending => { if let Some(message) = (self.grpc_message_build_fn)(self.get_extension_type(), &self.action) { - self.result = + let res = self.service .send(self.get_map_values_bytes_fn, self.grpc_call_fn, message); - self.state.next(); - self.result + self.set_result(res); + self.next_state(); + res } else { //todo: we need to move to and start the next action - self.state.done(); - Ok(1234) + self.done(); + self.get_result() } } State::Waiting => { - self.state.next(); - self.result + self.next_state(); + self.get_result() } - State::Done => self.result, + State::Done => self.get_result(), } } - pub fn get_state(&self) -> &State { - &self.state + fn next_state(&self) { + self.state.borrow_mut().next() + } + + fn done(&self) { + self.state.borrow_mut().done() + } + + pub fn get_state(&self) -> State { + *self.state.borrow() } pub fn get_result(&self) -> Result { - self.result + *self.result.borrow() + } + + fn set_result(&self, result: Result) { + *self.result.borrow_mut() = result; } pub fn get_extension_type(&self) -> &ExtensionType { @@ -105,8 +118,8 @@ impl Operation { #[allow(dead_code)] pub struct OperationDispatcher { - operations: RefCell>, - waiting_operations: RefCell>, // TODO(didierofrivia): Maybe keep references or Rc + operations: Vec>, + waiting_operations: HashMap>, service_handlers: HashMap>, } @@ -114,60 +127,50 @@ pub struct OperationDispatcher { impl OperationDispatcher { pub fn default() -> Self { OperationDispatcher { - operations: RefCell::new(vec![]), - waiting_operations: RefCell::new(HashMap::default()), + operations: vec![], + waiting_operations: HashMap::default(), service_handlers: HashMap::default(), } } pub fn new(service_handlers: HashMap>) -> Self { Self { service_handlers, - operations: RefCell::new(vec![]), - waiting_operations: RefCell::new(HashMap::new()), + operations: vec![], + waiting_operations: HashMap::new(), } } - pub fn get_operation(&self, token_id: u32) -> Option { - self.waiting_operations.borrow_mut().get(&token_id).cloned() + pub fn get_operation(&self, token_id: u32) -> Option> { + self.waiting_operations.get(&token_id).cloned() } - pub fn build_operations(&self, rule: &Rule) { - let mut operations: Vec = vec![]; + pub fn build_operations(&mut self, rule: &Rule) { + let mut operations: Vec> = vec![]; for action in rule.actions.iter() { // TODO(didierofrivia): Error handling if let Some(service) = self.service_handlers.get(&action.extension) { - operations.push(Operation::new( + operations.push(Rc::new(Operation::new( service.get_extension(), action.clone(), Rc::clone(service), - )) + ))) } } self.push_operations(operations); } - pub fn push_operations(&self, operations: Vec) { - self.operations.borrow_mut().extend(operations); + pub fn push_operations(&mut self, operations: Vec>) { + self.operations.extend(operations); } pub fn get_current_operation_state(&self) -> Option { self.operations - .borrow() .first() - .map(|operation| operation.get_state().clone()) + .map(|operation| operation.get_state()) } - pub fn get_current_operation_result(&self) -> Result { - self.operations.borrow().first().unwrap().get_result() - } - - pub fn next(&self) -> Option { - let operations = self.operations.borrow_mut(); - self.step(operations) - } - - fn step(&self, mut operations: RefMut>) -> Option { - if let Some((i, operation)) = operations.iter_mut().enumerate().next() { + pub fn next(&mut self) -> Option> { + if let Some((i, operation)) = self.operations.iter_mut().enumerate().next() { match operation.get_state() { State::Pending => { match operation.trigger() { @@ -178,13 +181,11 @@ impl OperationDispatcher { } State::Waiting => { // We index only if it was just transitioned to Waiting after triggering - self.waiting_operations - .borrow_mut() - .insert(token_id, operation.clone()); + self.waiting_operations.insert(token_id, operation.clone()); // TODO(didierofrivia): Decide on indexing the failed operations. Some(operation.clone()) } - State::Done => self.step(operations), + State::Done => self.next(), } } Err(status) => { @@ -198,11 +199,11 @@ impl OperationDispatcher { Some(operation.clone()) } State::Done => { - if let Ok(token_id) = operation.result { - self.waiting_operations.borrow_mut().remove(&token_id); + if let Ok(token_id) = operation.get_result() { + self.waiting_operations.remove(&token_id); } // If result was Err, means the operation wasn't indexed - operations.remove(i); - self.step(operations) + self.operations.remove(i); + self.next() } } } else { @@ -286,10 +287,13 @@ mod tests { } } - fn build_operation(grpc_call_fn_stub: GrpcCallFn, extension_type: ExtensionType) -> Operation { - Operation { - state: State::Pending, - result: Ok(0), + fn build_operation( + grpc_call_fn_stub: GrpcCallFn, + extension_type: ExtensionType, + ) -> Rc { + Rc::new(Operation { + state: RefCell::from(State::Pending), + result: RefCell::new(Ok(0)), extension: Rc::new(Extension { extension_type, endpoint: "local".to_string(), @@ -304,14 +308,14 @@ mod tests { grpc_call_fn: grpc_call_fn_stub, get_map_values_bytes_fn: get_map_values_bytes_fn_stub, grpc_message_build_fn: grpc_message_build_fn_stub, - } + }) } #[test] fn operation_getters() { let operation = build_operation(default_grpc_call_fn_stub, ExtensionType::RateLimit); - assert_eq!(*operation.get_state(), State::Pending); + assert_eq!(operation.get_state(), State::Pending); assert_eq!(*operation.get_extension_type(), ExtensionType::RateLimit); assert_eq!(*operation.get_failure_mode(), FailureMode::Deny); assert_eq!(operation.get_result(), Ok(0)); @@ -319,34 +323,34 @@ mod tests { #[test] fn operation_transition() { - let mut operation = build_operation(default_grpc_call_fn_stub, ExtensionType::RateLimit); - assert_eq!(operation.result, Ok(0)); - assert_eq!(*operation.get_state(), State::Pending); + let operation = build_operation(default_grpc_call_fn_stub, ExtensionType::RateLimit); + assert_eq!(operation.get_result(), Ok(0)); + assert_eq!(operation.get_state(), State::Pending); let mut res = operation.trigger(); assert_eq!(res, Ok(200)); - assert_eq!(*operation.get_state(), State::Waiting); + assert_eq!(operation.get_state(), State::Waiting); res = operation.trigger(); assert_eq!(res, Ok(200)); - assert_eq!(operation.result, Ok(200)); - assert_eq!(*operation.get_state(), State::Done); + assert_eq!(operation.get_result(), Ok(200)); + assert_eq!(operation.get_state(), State::Done); } #[test] fn operation_dispatcher_push_actions() { - let operation_dispatcher = OperationDispatcher::default(); + let mut operation_dispatcher = OperationDispatcher::default(); - assert_eq!(operation_dispatcher.operations.borrow().len(), 0); + assert_eq!(operation_dispatcher.operations.len(), 0); operation_dispatcher.push_operations(vec![build_operation( default_grpc_call_fn_stub, ExtensionType::RateLimit, )]); - assert_eq!(operation_dispatcher.operations.borrow().len(), 1); + assert_eq!(operation_dispatcher.operations.len(), 1); } #[test] fn operation_dispatcher_get_current_action_state() { - let operation_dispatcher = OperationDispatcher::default(); + let mut operation_dispatcher = OperationDispatcher::default(); operation_dispatcher.push_operations(vec![build_operation( default_grpc_call_fn_stub, ExtensionType::RateLimit, @@ -359,7 +363,7 @@ mod tests { #[test] fn operation_dispatcher_next() { - let operation_dispatcher = OperationDispatcher::default(); + let mut operation_dispatcher = OperationDispatcher::default(); fn grpc_call_fn_stub_66( _upstream_name: &str, @@ -388,15 +392,11 @@ mod tests { build_operation(grpc_call_fn_stub_77, ExtensionType::Auth), ]); - assert_eq!(operation_dispatcher.get_current_operation_result(), Ok(0)); assert_eq!( operation_dispatcher.get_current_operation_state(), Some(State::Pending) ); - assert_eq!( - operation_dispatcher.waiting_operations.borrow_mut().len(), - 0 - ); + assert_eq!(operation_dispatcher.waiting_operations.len(), 0); let mut op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(66)); @@ -404,15 +404,12 @@ mod tests { *op.clone().unwrap().get_extension_type(), ExtensionType::RateLimit ); - assert_eq!(*op.unwrap().get_state(), State::Waiting); - assert_eq!( - operation_dispatcher.waiting_operations.borrow_mut().len(), - 1 - ); + assert_eq!(op.unwrap().get_state(), State::Waiting); + assert_eq!(operation_dispatcher.waiting_operations.len(), 1); op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(66)); - assert_eq!(*op.unwrap().get_state(), State::Done); + assert_eq!(op.unwrap().get_state(), State::Done); op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(77)); @@ -420,26 +417,17 @@ mod tests { *op.clone().unwrap().get_extension_type(), ExtensionType::Auth ); - assert_eq!(*op.unwrap().get_state(), State::Waiting); - assert_eq!( - operation_dispatcher.waiting_operations.borrow_mut().len(), - 1 - ); + assert_eq!(op.unwrap().get_state(), State::Waiting); + assert_eq!(operation_dispatcher.waiting_operations.len(), 1); op = operation_dispatcher.next(); assert_eq!(op.clone().unwrap().get_result(), Ok(77)); - assert_eq!(*op.unwrap().get_state(), State::Done); - assert_eq!( - operation_dispatcher.waiting_operations.borrow_mut().len(), - 1 - ); + assert_eq!(op.unwrap().get_state(), State::Done); + assert_eq!(operation_dispatcher.waiting_operations.len(), 1); op = operation_dispatcher.next(); assert!(op.is_none()); assert!(operation_dispatcher.get_current_operation_state().is_none()); - assert_eq!( - operation_dispatcher.waiting_operations.borrow_mut().len(), - 0 - ); + assert_eq!(operation_dispatcher.waiting_operations.len(), 0); } } From 249d3cd1c1055d47238d7475f128ada5d66073b9 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 1 Oct 2024 15:49:39 +0200 Subject: [PATCH 64/66] [refactor] OperationDispatcher within a RefCell for interior mut Signed-off-by: dd di cesare --- src/filter/http_context.rs | 21 +++++++++++++-------- src/filter/root_context.rs | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/filter/http_context.rs b/src/filter/http_context.rs index e28e5f6d..cbc7eea9 100644 --- a/src/filter/http_context.rs +++ b/src/filter/http_context.rs @@ -7,13 +7,14 @@ use crate::service::grpc_message::GrpcMessageResponse; use log::{debug, warn}; use proxy_wasm::traits::{Context, HttpContext}; use proxy_wasm::types::Action; +use std::cell::RefCell; use std::rc::Rc; pub struct Filter { pub context_id: u32, pub config: Rc, pub response_headers_to_add: Vec<(String, String)>, - pub operation_dispatcher: OperationDispatcher, + pub operation_dispatcher: RefCell, } impl Filter { @@ -32,13 +33,15 @@ impl Filter { fn process_policy(&self, policy: &Policy) -> Action { if let Some(rule) = policy.find_rule_that_applies() { - self.operation_dispatcher.build_operations(rule); + self.operation_dispatcher + .borrow_mut() + .build_operations(rule); } else { debug!("#{} process_policy: no rule applied", self.context_id); return Action::Continue; } - if let Some(operation) = self.operation_dispatcher.next() { + if let Some(operation) = self.operation_dispatcher.borrow_mut().next() { match operation.get_result() { Ok(call_id) => { debug!("#{} initiated gRPC call (id# {})", self.context_id, call_id); @@ -101,11 +104,11 @@ impl Filter { } _ => {} } - self.operation_dispatcher.next(); + self.operation_dispatcher.borrow_mut().next(); } fn process_auth_grpc_response( - &mut self, + &self, auth_resp: GrpcMessageResponse, failure_mode: &FailureMode, ) { @@ -156,7 +159,7 @@ impl Filter { } } } - self.operation_dispatcher.next(); + self.operation_dispatcher.borrow_mut().next(); } } @@ -203,7 +206,9 @@ impl Context for Filter { self.context_id ); - if let Some(operation) = self.operation_dispatcher.get_operation(token_id) { + let some_op = self.operation_dispatcher.borrow().get_operation(token_id); + + if let Some(operation) = some_op { let failure_mode = &operation.get_failure_mode(); let res_body_bytes = match self.get_grpc_call_response_body(0, resp_size) { Some(bytes) => bytes, @@ -229,7 +234,7 @@ impl Context for Filter { ExtensionType::RateLimit => self.process_ratelimit_grpc_response(res, failure_mode), } - if let Some(_op) = self.operation_dispatcher.next() { + if let Some(_op) = self.operation_dispatcher.borrow_mut().next() { } else { self.resume_http_request() } diff --git a/src/filter/root_context.rs b/src/filter/root_context.rs index 6dcd4bdc..5e5a8aa6 100644 --- a/src/filter/root_context.rs +++ b/src/filter/root_context.rs @@ -55,7 +55,7 @@ impl RootContext for FilterRoot { context_id, config: Rc::clone(&self.config), response_headers_to_add: Vec::default(), - operation_dispatcher: OperationDispatcher::new(service_handlers), + operation_dispatcher: OperationDispatcher::new(service_handlers).into(), })) } From 5434eb9b33c2c4af6750c9c403fb88f9e62a9d37 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Tue, 1 Oct 2024 18:00:06 +0200 Subject: [PATCH 65/66] [refactor] Simplifying Operation state transtion and exec of req msg Signed-off-by: dd di cesare --- src/operation_dispatcher.rs | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/src/operation_dispatcher.rs b/src/operation_dispatcher.rs index 65b9d064..c9c6b0ce 100644 --- a/src/operation_dispatcher.rs +++ b/src/operation_dispatcher.rs @@ -62,28 +62,17 @@ impl Operation { } fn trigger(&self) -> Result { - match self.get_state() { - State::Pending => { - if let Some(message) = - (self.grpc_message_build_fn)(self.get_extension_type(), &self.action) - { - let res = - self.service - .send(self.get_map_values_bytes_fn, self.grpc_call_fn, message); - self.set_result(res); - self.next_state(); - res - } else { - //todo: we need to move to and start the next action - self.done(); - self.get_result() - } - } - State::Waiting => { - self.next_state(); - self.get_result() - } - State::Done => self.get_result(), + if let Some(message) = (self.grpc_message_build_fn)(self.get_extension_type(), &self.action) + { + let res = self + .service + .send(self.get_map_values_bytes_fn, self.grpc_call_fn, message); + self.set_result(res); + self.next_state(); + res + } else { + self.done(); + self.get_result() } } @@ -195,7 +184,7 @@ impl OperationDispatcher { } } State::Waiting => { - let _ = operation.trigger(); + operation.next_state(); Some(operation.clone()) } State::Done => { From 4222ffb1afd318f18914ea83f984263a71054698 Mon Sep 17 00:00:00 2001 From: dd di cesare Date: Wed, 2 Oct 2024 10:26:50 +0200 Subject: [PATCH 66/66] [readme] Updating new sample config Signed-off-by: dd di cesare --- README.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 60ea9155..9ab09747 100644 --- a/README.md +++ b/README.md @@ -35,18 +35,15 @@ policies: - selector: request.method operator: eq value: GET - data: - - selector: - selector: request.headers.My-Custom-Header - - static: - key: admin - value: "1" - actions: - - extension: ratelimit-ext - data: - static: - key: host - value: rlp-ns-A/rlp-name-A + actions: + - extension: ratelimit-ext + scope: rlp-ns-A/rlp-name-A + data: + - selector: + selector: request.headers.My-Custom-Header + - static: + key: admin + value: "1" ``` ## Features