Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support request.query decoding #126

Merged
merged 8 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ impl TryFrom<PluginConfiguration> 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
Expand Down
124 changes: 116 additions & 8 deletions src/data/cel.rs
Original file line number Diff line number Diff line change
@@ -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<Attribute>,
expression: CelExpression,
extended: bool,
}

impl Expression {
pub fn new(expression: &str) -> Result<Self, ParseError> {
pub fn new_expression(expression: &str, extended: bool) -> Result<Self, ParseError> {
let expression = parse(expression)?;

let mut props = Vec::with_capacity(5);
Expand All @@ -40,19 +43,27 @@ impl Expression {
Ok(Self {
attributes,
expression,
extended,
})
}

pub fn new(expression: &str) -> Result<Self, ParseError> {
Self::new_expression(expression, false)
}

pub fn new_extended(expression: &str) -> Result<Self, ParseError> {
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,
Expand All @@ -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<Arc<String>>, 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<Key, Value> = 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<Option<Bytes>, Status> {
super::property::host_get_property(&Path::new(path))
Expand Down Expand Up @@ -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<Self, ParseError> {
Ok(Self {
expression: Expression::new_extended(predicate)?,
})
}

pub fn test(&self) -> bool {
match self.expression.eval() {
Value::Bool(result) => result,
Expand Down Expand Up @@ -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&param2=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&param2=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((
Expand Down
Loading