diff --git a/Cargo.lock b/Cargo.lock index 49b09ca5..1dc783c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -234,6 +234,21 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -300,6 +315,15 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -440,6 +464,21 @@ dependencies = [ "syn 2.0.59", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -536,6 +575,29 @@ dependencies = [ "libc", ] +[[package]] +name = "cel-interpreter" +version = "0.7.0" +source = "git+https://github.com/chirino/cel-rust.git?branch=no-panic-compare#0cf3d6bdee9feb9a6a2107dc6795d09de6fff4cf" +dependencies = [ + "cel-parser", + "chrono", + "nom", + "paste", + "serde", + "thiserror", +] + +[[package]] +name = "cel-parser" +version = "0.6.0" +source = "git+https://github.com/chirino/cel-rust.git?branch=no-panic-compare#0cf3d6bdee9feb9a6a2107dc6795d09de6fff4cf" +dependencies = [ + "lalrpop", + "lalrpop-util", + "regex", +] + [[package]] name = "cexpr" version = "0.6.0" @@ -557,7 +619,12 @@ version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", + "windows-targets 0.52.5", ] [[package]] @@ -851,6 +918,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -861,6 +934,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "either" version = "1.11.0" @@ -873,6 +967,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + [[package]] name = "encoding_rs" version = "0.8.34" @@ -1360,6 +1463,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1489,6 +1615,37 @@ dependencies = [ "libc", ] +[[package]] +name = "lalrpop" +version = "0.19.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b" +dependencies = [ + "ascii-canvas", + "bit-set", + "diff", + "ena", + "is-terminal", + "itertools 0.10.5", + "lalrpop-util", + "petgraph", + "regex", + "regex-syntax 0.6.29", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.19.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed" +dependencies = [ + "regex", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1523,6 +1680,16 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] + [[package]] name = "librocksdb-sys" version = "0.16.0+8.10.0" @@ -1556,6 +1723,8 @@ version = "0.8.0-dev" dependencies = [ "async-trait", "base64 0.22.1", + "cel-interpreter", + "cel-parser", "cfg-if", "criterion", "dashmap", @@ -1829,6 +1998,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nom" version = "7.1.3" @@ -2199,6 +2374,15 @@ dependencies = [ "indexmap 2.2.6", ] +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -2295,6 +2479,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.19" @@ -2530,6 +2720,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.4" @@ -2836,6 +3037,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "sketches-ddsketch" version = "0.2.2" @@ -2882,6 +3089,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2968,6 +3188,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.58" @@ -3029,6 +3260,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinytemplate" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index 817ad31c..a24e8ab6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,11 @@ members = ["limitador", "limitador-server"] resolver = "2" + +[patch.crates-io] +cel-interpreter = { git = "https://github.com/chirino/cel-rust.git", branch = "no-panic-compare"} +cel-parser = { git = "https://github.com/chirino/cel-rust.git", branch = "no-panic-compare"} + [profile.release] lto = true codegen-units = 1 diff --git a/limitador/Cargo.toml b/limitador/Cargo.toml index 0bb26894..942b93f5 100644 --- a/limitador/Cargo.toml +++ b/limitador/Cargo.toml @@ -18,6 +18,7 @@ disk_storage = ["rocksdb"] distributed_storage = ["tokio", "tokio-stream", "h2", "base64", "uuid", "tonic", "tonic-reflection", "prost", "prost-types"] redis_storage = ["redis", "r2d2", "tokio"] lenient_conditions = [] +cel_conditions = [] [dependencies] moka = { version = "0.12", features = ["sync"] } @@ -33,6 +34,8 @@ async-trait = "0.1" cfg-if = "1" tracing = "0.1.40" metrics = "0.22.3" +cel-parser = "0.6.0" +cel-interpreter = "0.7.0" # Optional dependencies rocksdb = { version = "0.22", optional = true, features = ["multi-threaded-cf"] } @@ -85,3 +88,4 @@ tonic-build = "0.11" name = "bench" path = "benches/bench.rs" harness = false + diff --git a/limitador/src/limit.rs b/limitador/src/limit.rs index 12adb7ff..88b133ef 100644 --- a/limitador/src/limit.rs +++ b/limitador/src/limit.rs @@ -1,11 +1,21 @@ -use crate::limit::conditions::{ErrorType, Literal, SyntaxError, Token, TokenType}; -use serde::{Deserialize, Serialize, Serializer}; use std::cmp::Ordering; use std::collections::{BTreeSet, HashMap, HashSet}; use std::error::Error; use std::fmt::{Debug, Display, Formatter}; use std::hash::{Hash, Hasher}; +use cel_interpreter::{Context, Expression, Value}; +#[cfg(feature = "cel_conditions")] +use cel_parser::parse; +use cel_parser::RelationOp::{Equals, NotEquals}; +use cel_parser::{Atom, Member, RelationOp}; +use serde::{Deserialize, Serialize, Serializer}; + +#[cfg(feature = "lenient_conditions")] +pub use deprecated::check_deprecated_syntax_usages_and_reset; + +use crate::limit::conditions::{ErrorType, Literal, SyntaxError, Token, TokenType}; + #[cfg(feature = "lenient_conditions")] mod deprecated { use std::sync::atomic::{AtomicBool, Ordering}; @@ -25,9 +35,6 @@ mod deprecated { } } -#[cfg(feature = "lenient_conditions")] -pub use deprecated::check_deprecated_syntax_usages_and_reset; - #[derive(Debug, Hash, Eq, PartialEq, Clone, Serialize, Deserialize)] pub struct Namespace(String); @@ -66,12 +73,25 @@ pub struct Limit { variables: HashSet, } -#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Hash)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[serde(try_from = "String", into = "String")] pub struct Condition { - var_name: String, - predicate: Predicate, - operand: String, + source: String, + expression: Expression, +} + +impl PartialEq for Condition { + fn eq(&self, other: &Self) -> bool { + self.expression == other.expression + } +} + +impl Eq for Condition {} + +impl Hash for Condition { + fn hash(&self, state: &mut H) { + self.source.hash(state) + } } #[derive(Debug)] @@ -101,11 +121,34 @@ impl TryFrom<&str> for Condition { } } -impl TryFrom for Condition { - type Error = ConditionParsingError; +impl Condition { + #[cfg(feature = "cel_conditions")] + fn try_from_cel(source: String) -> Result { + match parse(source.strip_prefix("cel:").unwrap()) { + Ok(expression) => Ok(Condition { source, expression }), + Err(_err) => Err(ConditionParsingError { + error: SyntaxError { + pos: 0, + error: ErrorType::MissingToken, + }, + tokens: vec![], + condition: source, + }), + } + } - fn try_from(value: String) -> Result { - match conditions::Scanner::scan(value.clone()) { + fn simple_source(var_name: String, op: RelationOp, lit: String) -> String { + let predicate = match op { + Equals => "==", + NotEquals => "!=", + _ => unreachable!(), + }; + let quotes = if lit.contains('"') { '\'' } else { '"' }; + format!("{} {} {}{}{}", var_name, predicate, quotes, lit, quotes) + } + + fn try_from_simple(source: String) -> Result { + match conditions::Scanner::scan(source.clone()) { Ok(tokens) => match tokens.len().cmp(&(3_usize)) { Ordering::Equal => { match ( @@ -124,18 +167,25 @@ impl TryFrom for Condition { ) = (&tokens[0].literal, &tokens[2].literal) { let predicate = match &tokens[1].token_type { - TokenType::EqualEqual => Predicate::Equal, - TokenType::NotEqual => Predicate::NotEqual, + TokenType::EqualEqual => Equals, + TokenType::NotEqual => NotEquals, _ => unreachable!(), }; Ok(Condition { - var_name: var_name.clone(), - predicate, - operand: operand.clone(), + source: Condition::simple_source( + var_name.clone(), + predicate.clone(), + operand.clone(), + ), + expression: Self::simple_expression( + var_name.as_str(), + predicate, + operand.as_str(), + ), }) } else { panic!( - "Unexpected state {tokens:?} returned from Scanner for: `{value}`" + "Unexpected state {tokens:?} returned from Scanner for: `{source}`" ) } } @@ -150,18 +200,25 @@ impl TryFrom for Condition { ) = (&tokens[0].literal, &tokens[2].literal) { let predicate = match &tokens[1].token_type { - TokenType::EqualEqual => Predicate::Equal, - TokenType::NotEqual => Predicate::NotEqual, + TokenType::EqualEqual => Equals, + TokenType::NotEqual => NotEquals, _ => unreachable!(), }; Ok(Condition { - var_name: var_name.clone(), - predicate, - operand: operand.clone(), + source: Condition::simple_source( + var_name.clone(), + predicate.clone(), + operand.clone(), + ), + expression: Self::simple_expression( + var_name.as_str(), + predicate, + operand.as_str(), + ), }) } else { panic!( - "Unexpected state {tokens:?} returned from Scanner for: `{value}`" + "Unexpected state {tokens:?} returned from Scanner for: `{source}`" ) } } @@ -174,13 +231,20 @@ impl TryFrom for Condition { { deprecated::deprecated_syntax_used(); Ok(Condition { - var_name: var_name.clone(), - predicate: Predicate::Equal, - operand: operand.clone(), + source: Condition::simple_source( + var_name.clone(), + Equals, + operand.clone(), + ), + expression: Self::simple_expression( + var_name.as_str(), + Equals, + operand.as_str(), + ), }) } else { panic!( - "Unexpected state {tokens:?} returned from Scanner for: `{value}`" + "Unexpected state {tokens:?} returned from Scanner for: `{source}`" ) } } @@ -193,13 +257,20 @@ impl TryFrom for Condition { { deprecated::deprecated_syntax_used(); Ok(Condition { - var_name: var_name.clone(), - predicate: Predicate::Equal, - operand: operand.to_string(), + source: Condition::simple_source( + var_name.clone(), + Equals, + operand.to_string(), + ), + expression: Self::simple_expression( + var_name.as_str(), + Equals, + operand.to_string().as_str(), + ), }) } else { panic!( - "Unexpected state {tokens:?} returned from Scanner for: `{value}`" + "Unexpected state {tokens:?} returned from Scanner for: `{source}`" ) } } @@ -218,18 +289,18 @@ impl TryFrom for Condition { error: ErrorType::UnexpectedToken(tokens[faulty].clone()), }, tokens, - condition: value, + condition: source, }) } } } Ordering::Less => Err(ConditionParsingError { error: SyntaxError { - pos: value.len(), + pos: source.len(), error: ErrorType::MissingToken, }, tokens, - condition: value, + condition: source, }), Ordering::Greater => Err(ConditionParsingError { error: SyntaxError { @@ -237,55 +308,46 @@ impl TryFrom for Condition { error: ErrorType::UnexpectedToken(tokens[3].clone()), }, tokens, - condition: value, + condition: source, }), }, Err(err) => Err(ConditionParsingError { error: err, tokens: Vec::new(), - condition: value, + condition: source, }), } } -} -impl From for String { - fn from(condition: Condition) -> Self { - let p = &condition.predicate; - let predicate: String = p.clone().into(); - let quotes = if condition.operand.contains('"') { - '\'' - } else { - '"' - }; - format!( - "{} {} {}{}{}", - condition.var_name, predicate, quotes, condition.operand, quotes + fn simple_expression(ident: &str, op: RelationOp, lit: &str) -> Expression { + Expression::Relation( + Box::new(Expression::Member( + Box::new(Expression::Ident("vars".to_string().into())), + Box::new(Member::Index(Box::new(Expression::Atom(Atom::String( + ident.to_string().into(), + ))))), + )), + op, + Box::new(Expression::Atom(Atom::String(lit.to_string().into()))), ) } } -#[derive(PartialEq, Eq, Debug, Clone, Hash)] -pub enum Predicate { - Equal, - NotEqual, -} +impl TryFrom for Condition { + type Error = ConditionParsingError; -impl Predicate { - fn test(&self, lhs: &str, rhs: &str) -> bool { - match self { - Predicate::Equal => lhs == rhs, - Predicate::NotEqual => lhs != rhs, + fn try_from(value: String) -> Result { + #[cfg(feature = "cel_conditions")] + if value.clone().starts_with("cel:") { + return Condition::try_from_cel(value); } + Condition::try_from_simple(value) } } -impl From for String { - fn from(op: Predicate) -> Self { - match op { - Predicate::Equal => "==".to_string(), - Predicate::NotEqual => "!=".to_string(), - } +impl From for String { + fn from(condition: Condition) -> Self { + condition.source.clone() } } @@ -392,12 +454,21 @@ impl Limit { } fn condition_applies(condition: &Condition, values: &HashMap) -> bool { - let left_operand = condition.var_name.as_str(); - let right_operand = condition.operand.as_str(); + let mut context = Context::default(); + + for (key, value) in values { + if key.starts_with('_') { + // reserve _* identifiers for future use. + continue; + } + context.add_variable_from_value(key, value.clone()); + } - match values.get(left_operand) { - Some(val) => condition.predicate.test(val, right_operand), - None => false, + context.add_variable_from_value("vars", values.clone()); + + match Value::resolve(&condition.expression, &context) { + Ok(val) => val == true.into(), + Err(_err) => false, } } } @@ -819,6 +890,14 @@ mod conditions { mod tests { use super::*; + macro_rules! assert_false { + ($cond:expr $(,)?) => { + paste::item! { + assert!(!$cond, "assertion failed: assert_false!({}) was true!", stringify!($cond)) + } + }; + } + #[test] fn limit_can_have_an_optional_name() { let mut limit = Limit::new("test_namespace", 10, 60, vec!["x == \"5\""], vec!["y"]); @@ -848,7 +927,7 @@ mod tests { values.insert("x".into(), "1".into()); values.insert("y".into(), "1".into()); - assert!(!limit.applies(&values)) + assert_false!(limit.applies(&values)) } #[test] @@ -860,9 +939,9 @@ mod tests { values.insert("x".into(), "1".into()); values.insert("y".into(), "1".into()); - assert!(!limit.applies(&values)); + assert_false!(limit.applies(&values)); assert!(check_deprecated_syntax_usages_and_reset()); - assert!(!check_deprecated_syntax_usages_and_reset()); + assert_false!(check_deprecated_syntax_usages_and_reset()); let limit = Limit::new("test_namespace", 10, 60, vec!["x == foobar"], vec!["y"]); @@ -872,7 +951,7 @@ mod tests { assert!(limit.applies(&values)); assert!(check_deprecated_syntax_usages_and_reset()); - assert!(!check_deprecated_syntax_usages_and_reset()); + assert_false!(check_deprecated_syntax_usages_and_reset()); } #[test] @@ -884,7 +963,7 @@ mod tests { values.insert("a".into(), "1".into()); values.insert("y".into(), "1".into()); - assert!(!limit.applies(&values)) + assert_false!(limit.applies(&values)) } #[test] @@ -895,7 +974,7 @@ mod tests { let mut values: HashMap = HashMap::new(); values.insert("x".into(), "5".into()); - assert!(!limit.applies(&values)) + assert_false!(limit.applies(&values)) } #[test] @@ -931,7 +1010,7 @@ mod tests { values.insert("y".into(), "2".into()); values.insert("z".into(), "1".into()); - assert!(!limit.applies(&values)) + assert_false!(limit.applies(&values)) } #[test] @@ -940,9 +1019,8 @@ mod tests { assert_eq!( result, Condition { - var_name: "x".to_string(), - predicate: Predicate::Equal, - operand: "5".to_string(), + source: "x == '5'".to_string(), + expression: Condition::simple_expression("x", Equals, "5"), } ); @@ -951,9 +1029,8 @@ mod tests { assert_eq!( result, Condition { - var_name: "foobar".to_string(), - predicate: Predicate::Equal, - operand: "ok".to_string(), + source: " foobar=='ok' ".to_string(), + expression: Condition::simple_expression("foobar", Equals, "ok"), } ); @@ -962,9 +1039,8 @@ mod tests { assert_eq!( result, Condition { - var_name: "foobar".to_string(), - predicate: Predicate::Equal, - operand: "ok".to_string(), + source: " foobar == 'ok' ".to_string(), + expression: Condition::simple_expression("foobar", Equals, "ok"), } ); } @@ -991,11 +1067,103 @@ mod tests { #[test] fn condition_serialization() { let condition = Condition { - var_name: "foobar".to_string(), - predicate: Predicate::Equal, - operand: "ok".to_string(), + source: "foobar == \"ok\"".to_string(), + expression: Condition::simple_expression("foobar", Equals, "ok"), }; let result = serde_json::to_string(&condition).expect("Should serialize"); assert_eq!(result, r#""foobar == \"ok\"""#.to_string()); } + + #[cfg(feature = "cel_conditions")] + mod cel { + use super::*; + + fn limit_with_condition(conditions: Vec<&str>) -> Limit { + Limit::new("test_namespace", 10, 60, conditions, >::new()) + } + + #[test] + fn limit_applies_when_all_its_conditions_apply_with_subexpression() { + let limit = limit_with_condition(vec![ + r#"cel: x == string((11 - 1) / 2) "#, + r#" y == "2" "#, + ]); + + let values = HashMap::from([ + ("x".to_string(), "5".to_string()), + ("y".to_string(), "2".to_string()), + ]); + + assert!(limit.applies(&values)) + } + + #[test] + fn vars_with_dot_names() { + let values = HashMap::from([("req.method".to_string(), "GET".to_string())]); + + // we can't access complex variables via simple names.... + let limit = limit_with_condition(vec![r#"cel: req.method == "GET" "#]); + assert_false!(limit.applies(&values)); + + // But we can access it via the vars map. + let limit = limit_with_condition(vec![r#"cel: vars["req.method"] == "GET" "#]); + assert!(limit.applies(&values)); + } + + #[test] + fn size_function() { + let values = HashMap::from([("method".to_string(), "GET".to_string())]); + let limit = limit_with_condition(vec![r#"cel: size(vars["method"]) == 3 "#]); + assert!(limit.applies(&values)); + } + + #[test] + fn size_function_and_size_var() { + let values = HashMap::from([ + ("method".to_string(), "GET".to_string()), + ("size".to_string(), "50".to_string()), + ]); + + let limit = limit_with_condition(vec![r#"cel: size(method) == 3 "#]); + assert!(limit.applies(&values)); + + // we can't access simple variables that conflict with built-ins + let limit = limit_with_condition(vec![r#"cel: size == "50" "#]); + assert_false!(limit.applies(&values)); + + // But we can access it via the vars map. + let limit = limit_with_condition(vec![r#"cel: vars["size"] == "50" "#]); + assert!(limit.applies(&values)); + } + + #[test] + fn vars_var() { + let values = HashMap::from([("vars".to_string(), "hello".to_string())]); + + // we can't access simple variables that conflict with built-ins (the vars map) + let limit = limit_with_condition(vec![r#"cel: vars == "hello" "#]); + assert_false!(limit.applies(&values)); + + // But we can access it via the vars map. + let limit = limit_with_condition(vec![r#"cel: vars["vars"] == "hello" "#]); + assert!(limit.applies(&values)); + + // Or via the vars map with dot notation. + let limit = limit_with_condition(vec![r#"cel: vars.vars == "hello" "#]); + assert!(limit.applies(&values)); + } + + #[test] + fn underscore_var() { + let values = HashMap::from([("_hello".to_string(), "world".to_string())]); + + // _* variables are reserved for future use + let limit = limit_with_condition(vec![r#"cel: _hello == "world" "#]); + assert_false!(limit.applies(&values)); + + // But we can access it via the vars map. + let limit = limit_with_condition(vec![r#"cel: vars["_hello"] == "world" "#]); + assert!(limit.applies(&values)); + } + } }