diff --git a/Cargo.lock b/Cargo.lock index 9c5c5b5..0e98929 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1907,6 +1907,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "uuid" version = "1.10.0" @@ -1958,6 +1964,7 @@ dependencies = [ "serde_json", "serial_test", "thiserror", + "urlencoding", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9042e8b..f462287 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ const_format = "0.2.31" chrono = { version = "0.4.38", default-features = false, features = ["alloc", "std"] } cel-interpreter = "0.8.1" cel-parser = "0.7.1" +urlencoding = "2.1.3" [dev-dependencies] proxy-wasm-test-framework = { git = "https://github.com/Kuadrant/wasm-test-framework.git", branch = "kuadrant" } diff --git a/README.md b/README.md index cb52533..a638a36 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ actionSets: - service: ratelimit-service scope: ratelimit-scope-a predicates: - - auth.identity.anonymous == "true" + - auth.identity.anonymous == true data: - expression: key: my_header diff --git a/src/configuration.rs b/src/configuration.rs index cbaa6ef..6605f9a 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -468,7 +468,7 @@ impl TryFrom for FilterConfig { } let mut predicates = Vec::default(); for predicate in &action_set.route_rule_conditions.predicates { - predicates.push(Predicate::new(predicate).map_err(|e| e.to_string())?); + predicates.push(Predicate::route_rule(predicate).map_err(|e| e.to_string())?); } action_set .route_rule_conditions diff --git a/src/data/cel.rs b/src/data/cel.rs index 09e1aba..0f71ce1 100644 --- a/src/data/cel.rs +++ b/src/data/cel.rs @@ -1,24 +1,27 @@ use crate::data::get_attribute; use crate::data::property::{host_get_map, Path}; -use cel_interpreter::extractors::This; -use cel_interpreter::objects::{Map, ValueType}; +use cel_interpreter::extractors::{Arguments, This}; +use cel_interpreter::objects::{Key, Map, ValueType}; use cel_interpreter::{Context, ExecutionError, ResolveResult, Value}; use cel_parser::{parse, Expression as CelExpression, Member, ParseError}; use chrono::{DateTime, FixedOffset}; use proxy_wasm::types::{Bytes, Status}; use serde_json::Value as JsonValue; +use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; -use std::sync::OnceLock; +use std::sync::{Arc, OnceLock}; +use urlencoding::decode; #[derive(Clone, Debug)] pub struct Expression { attributes: Vec, expression: CelExpression, + extended: bool, } impl Expression { - pub fn new(expression: &str) -> Result { + pub fn new_expression(expression: &str, extended: bool) -> Result { let expression = parse(expression)?; let mut props = Vec::with_capacity(5); @@ -40,19 +43,27 @@ impl Expression { Ok(Self { attributes, expression, + extended, }) } + pub fn new(expression: &str) -> Result { + Self::new_expression(expression, false) + } + + pub fn new_extended(expression: &str) -> Result { + Self::new_expression(expression, true) + } + pub fn eval(&self) -> Value { let mut ctx = create_context(); + if self.extended { + Self::add_extended_capabilities(&mut ctx) + } let Map { map } = self.build_data_map(); ctx.add_function("getHostProperty", get_host_property); - // if expression was "auth.identity.anonymous", - // { - // "auth": { "identity": { "anonymous": true } } - // } for binding in ["request", "metadata", "source", "destination", "auth"] { ctx.add_variable_from_value( binding, @@ -62,11 +73,57 @@ impl Expression { Value::resolve(&self.expression, &ctx).expect("Cel expression couldn't be evaluated") } + /// Add support for `queryMap`, see [`decode_query_string`] + fn add_extended_capabilities(ctx: &mut Context) { + ctx.add_function("queryMap", decode_query_string); + } + fn build_data_map(&self) -> Map { data::AttributeMap::new(self.attributes.clone()).into() } } +/// Decodes the query string and returns a Map where the key is the parameter's name and +/// the value is either a [`Value::String`] or a [`Value::List`] if the parameter's name is repeated +/// and the second arg is set not set to `false`. +/// see [`tests::decodes_query_string`] +fn decode_query_string(This(s): This>, Arguments(args): Arguments) -> ResolveResult { + let allow_repeats = if args.len() == 2 { + match &args[1] { + Value::Bool(b) => *b, + _ => false, + } + } else { + false + }; + let mut map: HashMap = HashMap::default(); + for part in s.split('&') { + let mut kv = part.split('='); + if let (Some(key), Some(value)) = (kv.next(), kv.next().or(Some(""))) { + let new_v: Value = decode(value).unwrap().into_owned().into(); + match map.entry(decode(key).unwrap().into_owned().into()) { + Entry::Occupied(mut e) => { + if allow_repeats { + if let Value::List(ref mut list) = e.get_mut() { + Arc::get_mut(list) + .expect("This isn't ever shared!") + .push(new_v); + } else { + let v = e.get().clone(); + let list = Value::List([v, new_v].to_vec().into()); + e.insert(list); + } + } + } + Entry::Vacant(e) => { + e.insert(decode(value).unwrap().into_owned().into()); + } + } + } + } + Ok(map.into()) +} + #[cfg(test)] pub fn inner_host_get_property(path: Vec<&str>) -> Result, Status> { super::property::host_get_property(&Path::new(path)) @@ -132,6 +189,15 @@ impl Predicate { }) } + /// Unlike with [`Predicate::new`], a `Predicate::route_rule` is backed by an + /// `Expression` that has extended capabilities enabled. + /// See [`Expression::add_extended_capabilities`] + pub fn route_rule(predicate: &str) -> Result { + Ok(Self { + expression: Expression::new_extended(predicate)?, + }) + } + pub fn test(&self) -> bool { match self.expression.eval() { Value::Bool(result) => result, @@ -578,6 +644,48 @@ mod tests { assert_eq!(value, "some random crap".into()); } + #[test] + fn decodes_query_string() { + property::test::TEST_PROPERTY_VALUE.set(Some(( + "request.query".into(), + "param1=%F0%9F%91%BE%20¶m2=Exterminate%21&%F0%9F%91%BE=123&%F0%9F%91%BE=456&%F0%9F%91%BE" + .bytes() + .collect(), + ))); + let predicate = Predicate::route_rule( + "queryMap(request.query, true)['param1'] == '👾 ' && \ + queryMap(request.query, true)['param2'] == 'Exterminate!' && \ + queryMap(request.query, true)['👾'][0] == '123' && \ + queryMap(request.query, true)['👾'][1] == '456' && \ + queryMap(request.query, true)['👾'][2] == '' \ + ", + ) + .expect("This is valid!"); + assert!(predicate.test()); + + property::test::TEST_PROPERTY_VALUE.set(Some(( + "request.query".into(), + "param1=%F0%9F%91%BE%20¶m2=Exterminate%21&%F0%9F%91%BE=123&%F0%9F%91%BE=456&%F0%9F%91%BE" + .bytes() + .collect(), + ))); + let predicate = Predicate::route_rule( + "queryMap(request.query, false)['param2'] == 'Exterminate!' && \ + queryMap(request.query, false)['👾'] == '123' \ + ", + ) + .expect("This is valid!"); + assert!(predicate.test()); + + property::test::TEST_PROPERTY_VALUE.set(Some(( + "request.query".into(), + "%F0%9F%91%BE".bytes().collect(), + ))); + let predicate = + Predicate::route_rule("queryMap(request.query) == {'👾': ''}").expect("This is valid!"); + assert!(predicate.test()); + } + #[test] fn attribute_resolve() { property::test::TEST_PROPERTY_VALUE.set(Some((