From 44e583771f12b889d0c10b15e05b488ab0a45883 Mon Sep 17 00:00:00 2001 From: Eguzki Astiz Lezaun Date: Wed, 20 Nov 2024 23:45:45 +0100 Subject: [PATCH] Fold subsequent calls to limitador into a single one Signed-off-by: Eguzki Astiz Lezaun --- src/auth_action.rs | 29 ++++ src/ratelimit_action.rs | 71 +++++++++ src/runtime_action.rs | 112 ++++++++++++++ src/runtime_action_set.rs | 202 +++++++++++++++++++++++- tests/auth.rs | 314 ++++++++++++++++++++++++++++++++++++++ tests/multi.rs | 4 - tests/rate_limited.rs | 149 ++++++++++++++++++ 7 files changed, 872 insertions(+), 9 deletions(-) diff --git a/src/auth_action.rs b/src/auth_action.rs index 4031b62..94af576 100644 --- a/src/auth_action.rs +++ b/src/auth_action.rs @@ -79,4 +79,33 @@ mod test { let auth_action = build_auth_action_with_predicates(Vec::default()); assert!(auth_action.conditions_apply()); } + + #[test] + fn when_all_predicates_are_truthy_action_apply() { + let auth_action = build_auth_action_with_predicates(vec!["true".into(), "true".into()]); + assert!(auth_action.conditions_apply()); + } + + #[test] + fn when_not_all_predicates_are_truthy_action_does_not_apply() { + let auth_action = build_auth_action_with_predicates(vec![ + "true".into(), + "true".into(), + "true".into(), + "false".into(), + ]); + assert!(!auth_action.conditions_apply()); + } + + #[test] + #[should_panic] + fn when_a_cel_expression_does_not_evaluate_to_bool_panics() { + let auth_action = build_auth_action_with_predicates(vec![ + "true".into(), + "true".into(), + "true".into(), + "1".into(), + ]); + auth_action.conditions_apply(); + } } diff --git a/src/ratelimit_action.rs b/src/ratelimit_action.rs index 62aef21..d093396 100644 --- a/src/ratelimit_action.rs +++ b/src/ratelimit_action.rs @@ -106,6 +106,7 @@ impl ConditionalData { pub struct RateLimitAction { grpc_service: Rc, scope: String, + service_name: String, conditional_data_sets: Vec, } @@ -114,6 +115,7 @@ impl RateLimitAction { Ok(Self { grpc_service: Rc::new(GrpcService::new(Rc::new(service.clone()))), scope: action.scope.clone(), + service_name: action.service.clone(), conditional_data_sets: vec![ConditionalData::new(action)?], }) } @@ -148,6 +150,16 @@ impl RateLimitAction { pub fn get_failure_mode(&self) -> FailureMode { self.grpc_service.get_failure_mode() } + + #[must_use] + pub fn merge(&mut self, other: RateLimitAction) -> Option { + if self.scope == other.scope && self.service_name == other.service_name { + self.conditional_data_sets + .extend(other.conditional_data_sets); + return None; + } + Some(other) + } } #[cfg(test)] @@ -185,6 +197,15 @@ mod test { assert!(rl_action.conditions_apply()); } + #[test] + fn even_with_falsy_predicates_conditions_apply() { + let action = build_action(vec!["false".into()], Vec::default()); + let service = build_service(); + let rl_action = RateLimitAction::new(&action, &service) + .expect("action building failed. Maybe predicates compilation?"); + assert!(rl_action.conditions_apply()); + } + #[test] fn empty_data_generates_empty_descriptor() { let action = build_action(Vec::default(), Vec::default()); @@ -246,4 +267,54 @@ mod test { .expect("action building failed. Maybe predicates compilation?"); assert_eq!(rl_action.build_descriptor(), RateLimitDescriptor::default()); } + + #[test] + fn merged_actions_generate_descriptor_entries_for_truthy_predicates() { + let service = build_service(); + + let data_1 = vec![DataItem { + item: DataType::Expression(ExpressionItem { + key: "key_1".into(), + value: "'value_1'".into(), + }), + }]; + let predicates_1 = vec!["true".into()]; + let action_1 = build_action(predicates_1, data_1); + let mut rl_action_1 = RateLimitAction::new(&action_1, &service) + .expect("action building failed. Maybe predicates compilation?"); + + let data_2 = vec![DataItem { + item: DataType::Expression(ExpressionItem { + key: "key_2".into(), + value: "'value_2'".into(), + }), + }]; + let predicates_2 = vec!["false".into()]; + let action_2 = build_action(predicates_2, data_2); + let rl_action_2 = RateLimitAction::new(&action_2, &service) + .expect("action building failed. Maybe predicates compilation?"); + + let data_3 = vec![DataItem { + item: DataType::Expression(ExpressionItem { + key: "key_3".into(), + value: "'value_3'".into(), + }), + }]; + let predicates_3 = vec!["true".into()]; + let action_3 = build_action(predicates_3, data_3); + let rl_action_3 = RateLimitAction::new(&action_3, &service) + .expect("action building failed. Maybe predicates compilation?"); + + assert!(rl_action_1.merge(rl_action_2).is_none()); + assert!(rl_action_1.merge(rl_action_3).is_none()); + + // it should generate descriptor entries from action 1 and action 3 + + let descriptor = rl_action_1.build_descriptor(); + assert_eq!(descriptor.get_entries().len(), 2); + assert_eq!(descriptor.get_entries()[0].key, String::from("key_1")); + assert_eq!(descriptor.get_entries()[0].value, String::from("value_1")); + assert_eq!(descriptor.get_entries()[1].key, String::from("key_3")); + assert_eq!(descriptor.get_entries()[1].value, String::from("value_3")); + } } diff --git a/src/runtime_action.rs b/src/runtime_action.rs index fa14b19..464b04f 100644 --- a/src/runtime_action.rs +++ b/src/runtime_action.rs @@ -52,4 +52,116 @@ impl RuntimeAction { pub fn get_service_type(&self) -> ServiceType { self.grpc_service().get_service_type() } + + #[must_use] + pub fn merge(&mut self, other: RuntimeAction) -> Option { + // only makes sense for rate limiting actions + if let Self::RateLimit(self_rl_action) = self { + if let Self::RateLimit(other_rl_action) = other { + return self_rl_action.merge(other_rl_action).map(Self::RateLimit); + } + } + Some(other) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::configuration::{Action, FailureMode, ServiceType, Timeout}; + + fn build_rl_service() -> Service { + Service { + service_type: ServiceType::RateLimit, + endpoint: "limitador".into(), + failure_mode: FailureMode::default(), + timeout: Timeout::default(), + } + } + + fn build_auth_service() -> Service { + Service { + service_type: ServiceType::Auth, + endpoint: "authorino".into(), + failure_mode: FailureMode::default(), + timeout: Timeout::default(), + } + } + + fn build_action(service: &str, scope: &str) -> Action { + Action { + service: service.into(), + scope: scope.into(), + predicates: Vec::default(), + data: Vec::default(), + } + } + + #[test] + fn only_rl_actions_are_merged() { + let mut services = HashMap::new(); + services.insert(String::from("service_rl"), build_rl_service()); + + let rl_action_0 = build_action("service_rl", "scope"); + let rl_action_1 = build_action("service_rl", "scope"); + + let mut rl_r_action_0 = RuntimeAction::new(&rl_action_0, &services) + .expect("action building failed. Maybe predicates compilation?"); + let rl_r_action_1 = RuntimeAction::new(&rl_action_1, &services) + .expect("action building failed. Maybe predicates compilation?"); + + assert!(rl_r_action_0.merge(rl_r_action_1).is_none()); + } + + #[test] + fn auth_actions_are_not_merged() { + let mut services = HashMap::new(); + services.insert(String::from("service_auth"), build_auth_service()); + + let auth_action_0 = build_action("service_auth", "scope"); + let auth_action_1 = build_action("service_auth", "scope"); + + let mut auth_r_action_0 = RuntimeAction::new(&auth_action_0, &services) + .expect("action building failed. Maybe predicates compilation?"); + let auth_r_action_1 = RuntimeAction::new(&auth_action_1, &services) + .expect("action building failed. Maybe predicates compilation?"); + + assert!(auth_r_action_0.merge(auth_r_action_1).is_some()); + } + + #[test] + fn auth_actions_do_not_merge_rl() { + let mut services = HashMap::new(); + services.insert(String::from("service_rl"), build_rl_service()); + services.insert(String::from("service_auth"), build_auth_service()); + + let rl_action_0 = build_action("service_rl", "scope"); + let auth_action_0 = build_action("service_auth", "scope"); + + let mut rl_r_action_0 = RuntimeAction::new(&rl_action_0, &services) + .expect("action building failed. Maybe predicates compilation?"); + + let auth_r_action_0 = RuntimeAction::new(&auth_action_0, &services) + .expect("action building failed. Maybe predicates compilation?"); + + assert!(rl_r_action_0.merge(auth_r_action_0).is_some()); + } + + #[test] + fn rl_actions_do_not_merge_auth() { + let mut services = HashMap::new(); + services.insert(String::from("service_rl"), build_rl_service()); + services.insert(String::from("service_auth"), build_auth_service()); + + let rl_action_0 = build_action("service_rl", "scope"); + let auth_action_0 = build_action("service_auth", "scope"); + + let rl_r_action_0 = RuntimeAction::new(&rl_action_0, &services) + .expect("action building failed. Maybe predicates compilation?"); + + let mut auth_r_action_0 = RuntimeAction::new(&auth_action_0, &services) + .expect("action building failed. Maybe predicates compilation?"); + + assert!(auth_r_action_0.merge(rl_r_action_0).is_some()); + } } diff --git a/src/runtime_action_set.rs b/src/runtime_action_set.rs index 03dbb03..840ed6e 100644 --- a/src/runtime_action_set.rs +++ b/src/runtime_action_set.rs @@ -25,18 +25,37 @@ impl RuntimeActionSet { } // actions - let mut runtime_actions = Vec::default(); - for action in &action_set.actions { - runtime_actions.push(Rc::new(RuntimeAction::new(action, services)?)); + let mut all_runtime_actions = Vec::default(); + for action in action_set.actions.iter() { + all_runtime_actions.push(RuntimeAction::new(action, services)?); } + let runtime_actions = Self::merge_subsequent_actions_of_a_kind(all_runtime_actions); Ok(Self { name: action_set.name.clone(), route_rule_predicates, - runtime_actions, + runtime_actions: runtime_actions.into_iter().map(Rc::new).collect(), }) } + fn merge_subsequent_actions_of_a_kind( + runtime_actions: Vec, + ) -> Vec { + // fold subsequent actions of a kind (kind being defined in the action) + let mut folded_actions: Vec = Vec::default(); + for r_action in runtime_actions { + match folded_actions.last_mut() { + Some(existing) => { + if let Some(action) = existing.merge(r_action) { + folded_actions.push(action); + } + } + None => folded_actions.push(r_action), + } + } + folded_actions + } + pub fn conditions_apply(&self) -> bool { let predicates = &self.route_rule_predicates; predicates.is_empty() @@ -53,7 +72,9 @@ impl RuntimeActionSet { #[cfg(test)] mod test { use super::*; - use crate::configuration::ActionSet; + use crate::configuration::{ + Action, ActionSet, FailureMode, RouteRuleConditions, ServiceType, Timeout, + }; #[test] fn empty_route_rule_predicates_do_apply() { @@ -64,4 +85,175 @@ mod test { assert!(runtime_action_set.conditions_apply()) } + + #[test] + fn when_all_predicates_are_truthy_conditions_apply() { + let action_set = ActionSet::new( + "some_name".to_owned(), + RouteRuleConditions { + hostnames: Vec::default(), + predicates: vec!["true".into(), "true".into()], + }, + Vec::new(), + ); + + let runtime_action_set = RuntimeActionSet::new(&action_set, &HashMap::default()) + .expect("should not happen from an empty set of actions"); + + assert!(runtime_action_set.conditions_apply()) + } + + #[test] + fn when_not_all_predicates_are_truthy_action_does_not_apply() { + let action_set = ActionSet::new( + "some_name".to_owned(), + RouteRuleConditions { + hostnames: Vec::default(), + predicates: vec!["true".into(), "true".into(), "true".into(), "false".into()], + }, + Vec::new(), + ); + + let runtime_action_set = RuntimeActionSet::new(&action_set, &HashMap::default()) + .expect("should not happen from an empty set of actions"); + + assert!(!runtime_action_set.conditions_apply()) + } + + #[test] + #[should_panic] + fn when_a_cel_expression_does_not_evaluate_to_bool_panics() { + let action_set = ActionSet::new( + "some_name".to_owned(), + RouteRuleConditions { + hostnames: Vec::default(), + predicates: vec!["true".into(), "true".into(), "true".into(), "1".into()], + }, + Vec::new(), + ); + + let runtime_action_set = RuntimeActionSet::new(&action_set, &HashMap::default()) + .expect("should not happen from an empty set of actions"); + runtime_action_set.conditions_apply(); + } + + fn build_rl_service() -> Service { + Service { + service_type: ServiceType::RateLimit, + endpoint: "limitador".into(), + failure_mode: FailureMode::default(), + timeout: Timeout::default(), + } + } + + fn build_auth_service() -> Service { + Service { + service_type: ServiceType::Auth, + endpoint: "authorino".into(), + failure_mode: FailureMode::default(), + timeout: Timeout::default(), + } + } + + fn build_action(service: &str, scope: &str) -> Action { + Action { + service: service.into(), + scope: scope.into(), + predicates: Vec::default(), + data: Vec::default(), + } + } + + #[test] + fn simple_folding() { + let action_a = build_action("rl_service_common", "scope_common"); + let action_b = build_action("rl_service_common", "scope_common"); + + let action_set = ActionSet::new( + "some_name".to_owned(), + Default::default(), + vec![action_a, action_b], + ); + + let mut services = HashMap::new(); + services.insert(String::from("rl_service_common"), build_rl_service()); + let runtime_action_set = RuntimeActionSet::new(&action_set, &services) + .expect("should not happen for simple actions"); + + assert_eq!(runtime_action_set.runtime_actions.len(), 1); + } + + #[test] + fn unrelated_actions_by_kind_are_not_folded() { + let red_action_0 = build_action("service_red", "scope_red"); + let blue_action_1 = build_action("service_blue", "scope_blue"); + + let action_set = ActionSet::new( + "some_name".to_owned(), + Default::default(), + vec![red_action_0, blue_action_1], + ); + + let mut services = HashMap::new(); + services.insert(String::from("service_red"), build_rl_service()); + services.insert(String::from("service_blue"), build_auth_service()); + + let runtime_action_set = RuntimeActionSet::new(&action_set, &services) + .expect("should not happen from simple actions"); + + assert_eq!(runtime_action_set.runtime_actions.len(), 2); + } + + #[test] + fn unrelated_rl_actions_are_not_folded() { + let red_action_0 = build_action("service_red", "scope_red"); + let blue_action_1 = build_action("service_blue", "scope_blue"); + let green_action_2 = build_action("service_green", "scope_green"); + + let action_set = ActionSet::new( + "some_name".to_owned(), + Default::default(), + vec![red_action_0, blue_action_1, green_action_2], + ); + + let mut services = HashMap::new(); + services.insert(String::from("service_red"), build_rl_service()); + services.insert(String::from("service_blue"), build_rl_service()); + services.insert(String::from("service_green"), build_rl_service()); + + let runtime_action_set = RuntimeActionSet::new(&action_set, &services) + .expect("should not happen from simple actions"); + + assert_eq!(runtime_action_set.runtime_actions.len(), 3); + } + + #[test] + fn only_subsequent_actions_are_folded() { + let red_action_0 = build_action("service_red", "common"); + let red_action_1 = build_action("service_red", "common"); + let blue_action_2 = build_action("service_blue", "common"); + let red_action_3 = build_action("service_red", "common"); + let red_action_4 = build_action("service_red", "common"); + + let action_set = ActionSet::new( + "some_name".to_owned(), + Default::default(), + vec![ + red_action_0, + red_action_1, + blue_action_2, + red_action_3, + red_action_4, + ], + ); + + let mut services = HashMap::new(); + services.insert(String::from("service_red"), build_rl_service()); + services.insert(String::from("service_blue"), build_rl_service()); + + let runtime_action_set = RuntimeActionSet::new(&action_set, &services) + .expect("should not happen from simple actions"); + + assert_eq!(runtime_action_set.runtime_actions.len(), 3); + } } diff --git a/tests/auth.rs b/tests/auth.rs index ea1cfe6..d0ded73 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -436,3 +436,317 @@ fn it_denies() { .execute_and_expect(ReturnType::Action(Action::Continue)) .unwrap(); } + +#[test] +#[serial] +fn it_does_not_fold_auth_actions() { + let args = tester::MockSettings { + wasm_path: wasm_module(), + quiet: false, + allow_unexpected: false, + }; + let mut module = tester::mock(args).unwrap(); + + module + .call_start() + .execute_and_expect(ReturnType::None) + .unwrap(); + + let root_context = 1; + let cfg = r#"{ + "services": { + "auth": { + "type": "auth", + "endpoint": "authorino-cluster", + "failureMode": "deny", + "timeout": "5s" + } + }, + "actionSets": [ + { + "name": "some-name", + "routeRuleConditions": { + "hostnames": ["*.com"] + }, + "actions": [ + { + "service": "auth", + "scope": "auth-scope", + "predicates" : [] + }, + { + "service": "auth", + "scope": "auth-scope", + "predicates" : [] + }] + }] + }"#; + + module + .call_proxy_on_context_create(root_context, 0) + .expect_log(Some(LogLevel::Info), Some("#1 set_root_context")) + .execute_and_expect(ReturnType::None) + .unwrap(); + + module + .call_proxy_on_configure(root_context, 0) + .expect_log(Some(LogLevel::Info), Some("#1 on_configure")) + .expect_get_buffer_bytes(Some(BufferType::PluginConfiguration)) + .returning(Some(cfg.as_bytes())) + .expect_log(Some(LogLevel::Info), None) + .execute_and_expect(ReturnType::Bool(true)) + .unwrap(); + + let http_context = 2; + module + .call_proxy_on_context_create(http_context, root_context) + .expect_log(Some(LogLevel::Debug), Some("#2 create_http_context")) + .execute_and_expect(ReturnType::None) + .unwrap(); + + 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("example.com")) + .expect_log( + Some(LogLevel::Debug), + Some("#2 action_set selected some-name"), + ) + // retrieving properties for CheckRequest + .expect_get_header_map_pairs(Some(MapType::HttpRequestHeaders)) + .returning(None) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"request\", \"host\"]"), + ) + .expect_get_property(Some(vec!["request", "host"])) + .returning(Some(data::request::HOST)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"request\", \"method\"]"), + ) + .expect_get_property(Some(vec!["request", "method"])) + .returning(Some(data::request::method::GET)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"request\", \"scheme\"]"), + ) + .expect_get_property(Some(vec!["request", "scheme"])) + .returning(Some(data::request::scheme::HTTP)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"request\", \"path\"]"), + ) + .expect_get_property(Some(vec!["request", "path"])) + .returning(Some(data::request::path::ADMIN_TOY)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"request\", \"protocol\"]"), + ) + .expect_get_property(Some(vec!["request", "protocol"])) + .returning(Some(data::request::protocol::HTTP_1_1)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"request\", \"time\"]"), + ) + .expect_get_property(Some(vec!["request", "time"])) + .returning(Some(data::request::TIME)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"destination\", \"address\"]"), + ) + .expect_get_property(Some(vec!["destination", "address"])) + .returning(Some(data::destination::ADDRESS)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"destination\", \"port\"]"), + ) + .expect_get_property(Some(vec!["destination", "port"])) + .returning(Some(data::destination::port::P_8000)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"source\", \"address\"]"), + ) + .expect_get_property(Some(vec!["source", "address"])) + .returning(Some(data::source::ADDRESS)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"source\", \"port\"]"), + ) + .expect_get_property(Some(vec!["source", "port"])) + .returning(Some(data::source::port::P_45000)) + // retrieving tracing 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_grpc_call( + Some("authorino-cluster"), + Some("envoy.service.auth.v3.Authorization"), + Some("Check"), + Some(&[0, 0, 0, 0]), + Some(&[ + 10, 234, 1, 10, 25, 10, 23, 10, 21, 18, 15, 49, 50, 55, 46, 48, 46, 48, 46, 49, 58, + 52, 53, 48, 48, 48, 24, 200, 223, 2, 18, 23, 10, 21, 10, 19, 18, 14, 49, 50, 55, + 46, 48, 46, 48, 46, 49, 58, 56, 48, 48, 48, 24, 192, 62, 34, 157, 1, 10, 12, 8, + 146, 140, 179, 185, 6, 16, 240, 213, 233, 163, 3, 18, 140, 1, 18, 3, 71, 69, 84, + 26, 14, 10, 7, 58, 109, 101, 116, 104, 111, 100, 18, 3, 71, 69, 84, 26, 38, 10, 5, + 58, 112, 97, 116, 104, 18, 29, 47, 100, 101, 102, 97, 117, 108, 116, 47, 114, 101, + 113, 117, 101, 115, 116, 47, 104, 101, 97, 100, 101, 114, 115, 47, 112, 97, 116, + 104, 26, 30, 10, 10, 58, 97, 117, 116, 104, 111, 114, 105, 116, 121, 18, 16, 97, + 98, 105, 95, 116, 101, 115, 116, 95, 104, 97, 114, 110, 101, 115, 115, 34, 10, 47, + 97, 100, 109, 105, 110, 47, 116, 111, 121, 42, 17, 99, 97, 114, 115, 46, 116, 111, + 121, 115, 116, 111, 114, 101, 46, 99, 111, 109, 50, 4, 104, 116, 116, 112, 82, 8, + 72, 84, 84, 80, 47, 49, 46, 49, 82, 18, 10, 4, 104, 111, 115, 116, 18, 10, 97, 117, + 116, 104, 45, 115, 99, 111, 112, 101, 90, 0, + ]), + Some(5000), + ) + .returning(Some(42)) + .expect_log( + Some(LogLevel::Debug), + Some("#2 initiated gRPC call (id# 42)"), + ) + .execute_and_expect(ReturnType::Action(Action::Pause)) + .unwrap(); + + // TODO: response containing dynamic metadata + // set_property is panicking with proxy-wasm-test-framework + // because the `expect_set_property` is not yet implemented neither on original repo nor our fork + // let grpc_response: [u8; 41] = [ + // 10, 0, 34, 35, 10, 33, 10, 8, 105, 100, 101, 110, 116, 105, 116, 121, 18, 21, 42, 19, 10, + // 17, 10, 6, 117, 115, 101, 114, 105, 100, 18, 7, 26, 5, 97, 108, 105, 99, 101, 26, 0, + // ]; + let grpc_response: [u8; 6] = [10, 0, 34, 0, 26, 0]; + module + .call_proxy_on_grpc_receive(http_context, 42, grpc_response.len() as i32) + .expect_log( + Some(LogLevel::Debug), + Some("#2 on_grpc_call_response: received gRPC call response: token: 42, status: 0"), + ) + .expect_get_buffer_bytes(Some(BufferType::GrpcReceiveBuffer)) + .returning(Some(&grpc_response)) + .expect_log( + Some(LogLevel::Debug), + Some("process_auth_grpc_response: received OkHttpResponse"), + ) + .expect_get_header_map_pairs(Some(MapType::HttpRequestHeaders)) + .returning(None) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"request\", \"host\"]"), + ) + .expect_get_property(Some(vec!["request", "host"])) + .returning(Some(data::request::HOST)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"request\", \"method\"]"), + ) + .expect_get_property(Some(vec!["request", "method"])) + .returning(Some(data::request::method::GET)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"request\", \"scheme\"]"), + ) + .expect_get_property(Some(vec!["request", "scheme"])) + .returning(Some(data::request::scheme::HTTP)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"request\", \"path\"]"), + ) + .expect_get_property(Some(vec!["request", "path"])) + .returning(Some(data::request::path::ADMIN_TOY)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"request\", \"protocol\"]"), + ) + .expect_get_property(Some(vec!["request", "protocol"])) + .returning(Some(data::request::protocol::HTTP_1_1)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"request\", \"time\"]"), + ) + .expect_get_property(Some(vec!["request", "time"])) + .returning(Some(data::request::TIME)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"destination\", \"address\"]"), + ) + .expect_get_property(Some(vec!["destination", "address"])) + .returning(Some(data::destination::ADDRESS)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"destination\", \"port\"]"), + ) + .expect_get_property(Some(vec!["destination", "port"])) + .returning(Some(data::destination::port::P_8000)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"source\", \"address\"]"), + ) + .expect_get_property(Some(vec!["source", "address"])) + .returning(Some(data::source::ADDRESS)) + .expect_log( + Some(LogLevel::Debug), + Some("get_property: path: [\"source\", \"port\"]"), + ) + .expect_get_property(Some(vec!["source", "port"])) + .returning(Some(data::source::port::P_45000)) + .expect_grpc_call( + Some("authorino-cluster"), + Some("envoy.service.auth.v3.Authorization"), + Some("Check"), + Some(&[0, 0, 0, 0]), + Some(&[ + 10, 234, 1, 10, 25, 10, 23, 10, 21, 18, 15, 49, 50, 55, 46, 48, 46, 48, 46, 49, 58, + 52, 53, 48, 48, 48, 24, 200, 223, 2, 18, 23, 10, 21, 10, 19, 18, 14, 49, 50, 55, + 46, 48, 46, 48, 46, 49, 58, 56, 48, 48, 48, 24, 192, 62, 34, 157, 1, 10, 12, 8, + 146, 140, 179, 185, 6, 16, 240, 213, 233, 163, 3, 18, 140, 1, 18, 3, 71, 69, 84, + 26, 14, 10, 7, 58, 109, 101, 116, 104, 111, 100, 18, 3, 71, 69, 84, 26, 30, 10, 10, + 58, 97, 117, 116, 104, 111, 114, 105, 116, 121, 18, 16, 97, 98, 105, 95, 116, 101, + 115, 116, 95, 104, 97, 114, 110, 101, 115, 115, 26, 38, 10, 5, 58, 112, 97, 116, + 104, 18, 29, 47, 100, 101, 102, 97, 117, 108, 116, 47, 114, 101, 113, 117, 101, + 115, 116, 47, 104, 101, 97, 100, 101, 114, 115, 47, 112, 97, 116, 104, 34, 10, 47, + 97, 100, 109, 105, 110, 47, 116, 111, 121, 42, 17, 99, 97, 114, 115, 46, 116, 111, + 121, 115, 116, 111, 114, 101, 46, 99, 111, 109, 50, 4, 104, 116, 116, 112, 82, 8, + 72, 84, 84, 80, 47, 49, 46, 49, 82, 18, 10, 4, 104, 111, 115, 116, 18, 10, 97, 117, + 116, 104, 45, 115, 99, 111, 112, 101, 90, 0, + ]), + Some(5000), + ) + .returning(Some(42)) + .execute_and_expect(ReturnType::None) + .unwrap(); + + // TODO: response containing dynamic metadata + // set_property is panicking with proxy-wasm-test-framework + // because the `expect_set_property` is not yet implemented neither on original repo nor our fork + // let grpc_response: [u8; 41] = [ + // 10, 0, 34, 35, 10, 33, 10, 8, 105, 100, 101, 110, 116, 105, 116, 121, 18, 21, 42, 19, 10, + // 17, 10, 6, 117, 115, 101, 114, 105, 100, 18, 7, 26, 5, 97, 108, 105, 99, 101, 26, 0, + // ]; + let grpc_response: [u8; 6] = [10, 0, 34, 0, 26, 0]; + module + .call_proxy_on_grpc_receive(http_context, 42, grpc_response.len() as i32) + .expect_log( + Some(LogLevel::Debug), + Some("#2 on_grpc_call_response: received gRPC call response: token: 42, status: 0"), + ) + .expect_get_buffer_bytes(Some(BufferType::GrpcReceiveBuffer)) + .returning(Some(&grpc_response)) + .expect_log( + Some(LogLevel::Debug), + Some("process_auth_grpc_response: received OkHttpResponse"), + ) + .execute_and_expect(ReturnType::None) + .unwrap(); + + module + .call_proxy_on_response_headers(http_context, 0, false) + .expect_log(Some(LogLevel::Debug), Some("#2 on_http_response_headers")) + .execute_and_expect(ReturnType::Action(Action::Continue)) + .unwrap(); +} diff --git a/tests/multi.rs b/tests/multi.rs index 22d1a0e..20a11bf 100644 --- a/tests/multi.rs +++ b/tests/multi.rs @@ -726,10 +726,6 @@ fn authenticated_one_ratelimit_action_matches() { ) .expect_get_property(Some(vec!["source", "address"])) .returning(Some("1.2.3.4:80".as_bytes())) - .expect_log( - Some(LogLevel::Debug), - Some("grpc_message_request: empty descriptors"), - ) .expect_log( Some(LogLevel::Debug), Some("get_property: path: [\"source\", \"address\"]"), diff --git a/tests/rate_limited.rs b/tests/rate_limited.rs index bf6321b..1586160 100644 --- a/tests/rate_limited.rs +++ b/tests/rate_limited.rs @@ -588,3 +588,152 @@ fn it_does_not_rate_limits_when_predicates_does_not_match() { .execute_and_expect(ReturnType::Action(Action::Continue)) .unwrap(); } + +#[test] +#[serial] +fn it_folds_subsequent_actions_to_limitador_into_a_single_one() { + let args = tester::MockSettings { + wasm_path: wasm_module(), + quiet: false, + allow_unexpected: false, + }; + let mut module = tester::mock(args).unwrap(); + + module + .call_start() + .execute_and_expect(ReturnType::None) + .unwrap(); + + let root_context = 1; + let cfg = r#"{ + "services": { + "limitador": { + "type": "ratelimit", + "endpoint": "limitador-cluster", + "failureMode": "allow", + "timeout": "5s" + } + }, + "actionSets": [ + { + "name": "some-name", + "routeRuleConditions": { + "hostnames": ["*.example.com"] + }, + "actions": [ + { + "service": "limitador", + "scope": "RLS-domain", + "data": [ + { + "expression": { + "key": "key_1", + "value": "'value_1'" + } + } + ] + }, + { + "service": "limitador", + "scope": "RLS-domain", + "data": [ + { + "expression": { + "key": "key_2", + "value": "'value_2'" + } + } + ] + }, + { + "service": "limitador", + "scope": "RLS-domain", + "data": [ + { + "expression": { + "key": "key_3", + "value": "'value_3'" + } + } + ] + } + ] + }] + }"#; + + module + .call_proxy_on_context_create(root_context, 0) + .expect_log(Some(LogLevel::Info), Some("#1 set_root_context")) + .execute_and_expect(ReturnType::None) + .unwrap(); + module + .call_proxy_on_configure(root_context, 0) + .expect_log(Some(LogLevel::Info), Some("#1 on_configure")) + .expect_get_buffer_bytes(Some(BufferType::PluginConfiguration)) + .returning(Some(cfg.as_bytes())) + .expect_log(Some(LogLevel::Info), None) + .execute_and_expect(ReturnType::Bool(true)) + .unwrap(); + + let http_context = 2; + module + .call_proxy_on_context_create(http_context, root_context) + .expect_log(Some(LogLevel::Debug), Some("#2 create_http_context")) + .execute_and_expect(ReturnType::None) + .unwrap(); + + 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.example.com")) + .expect_log( + Some(LogLevel::Debug), + Some("#2 action_set selected some-name"), + ) + // retrieving tracing 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_grpc_call( + Some("limitador-cluster"), + Some("envoy.service.ratelimit.v3.RateLimitService"), + Some("ShouldRateLimit"), + Some(&[0, 0, 0, 0]), + Some(&[ + 10, 10, 82, 76, 83, 45, 100, 111, 109, 97, 105, 110, 18, 54, 10, 16, 10, 5, 107, + 101, 121, 95, 49, 18, 7, 118, 97, 108, 117, 101, 95, 49, 10, 16, 10, 5, 107, 101, + 121, 95, 50, 18, 7, 118, 97, 108, 117, 101, 95, 50, 10, 16, 10, 5, 107, 101, 121, + 95, 51, 18, 7, 118, 97, 108, 117, 101, 95, 51, 24, 1, + ]), + Some(5000), + ) + .returning(Some(42)) + .expect_log( + Some(LogLevel::Debug), + Some("#2 initiated gRPC call (id# 42)"), + ) + .execute_and_expect(ReturnType::Action(Action::Pause)) + .unwrap(); + + let grpc_response: [u8; 2] = [8, 1]; + module + .call_proxy_on_grpc_receive(http_context, 42, grpc_response.len() as i32) + .expect_log( + Some(LogLevel::Debug), + Some("#2 on_grpc_call_response: received gRPC call response: token: 42, status: 0"), + ) + .expect_get_buffer_bytes(Some(BufferType::GrpcReceiveBuffer)) + .returning(Some(&grpc_response)) + .execute_and_expect(ReturnType::None) + .unwrap(); + + module + .call_proxy_on_response_headers(http_context, 0, false) + .expect_log(Some(LogLevel::Debug), Some("#2 on_http_response_headers")) + .execute_and_expect(ReturnType::Action(Action::Continue)) + .unwrap(); +}