diff --git a/Cargo.lock b/Cargo.lock index 49b09ca5..935a159d 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,30 @@ dependencies = [ "libc", ] +[[package]] +name = "cel-interpreter" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121ecc72c18bacd45494c0758bd2228d26764454d9b6cffe3ab88a850e5f568e" +dependencies = [ + "cel-parser", + "chrono", + "nom", + "paste", + "thiserror", +] + +[[package]] +name = "cel-parser" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a85ba148abbc551f1c32c8c0cff279dba234f6d38324bf372ba2395690879e" +dependencies = [ + "lalrpop", + "lalrpop-util", + "regex", +] + [[package]] name = "cexpr" version = "0.6.0" @@ -557,7 +620,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 +919,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 +935,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 +968,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 +1464,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 +1616,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 +1681,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 +1724,8 @@ version = "0.8.0-dev" dependencies = [ "async-trait", "base64 0.22.1", + "cel-interpreter", + "cel-parser", "cfg-if", "criterion", "dashmap", @@ -1829,6 +1999,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 +2375,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 +2480,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 +2721,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 +3038,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 +3090,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 +3189,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 +3261,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/limitador/src/limit.rs b/limitador/src/limit.rs index 66b4eb1a..17d8de8d 100644 --- a/limitador/src/limit.rs +++ b/limitador/src/limit.rs @@ -1,12 +1,14 @@ -use crate::limit::conditions::{ErrorType, SyntaxError, Token}; -use cel_interpreter::{Context, Expression, Value}; -use cel_parser::{parse, ParseError}; -use serde::{Deserialize, Serialize, Serializer}; +use crate::limit::conditions::{ErrorType, Literal, SyntaxError, Token, TokenType}; +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}; +use cel_parser::{Atom, parse}; +use cel_parser::RelationOp::{Equals, NotEquals}; +use serde::{Deserialize, Serialize, Serializer}; #[cfg(feature = "lenient_conditions")] mod deprecated { use std::sync::atomic::{AtomicBool, Ordering}; @@ -115,13 +117,12 @@ impl TryFrom<&str> for Condition { } } -impl TryFrom for Condition { - type Error = ConditionParsingError; +impl Condition { - fn try_from(source: String) -> Result { + fn try_from_cel(source: String) -> Result { match parse(&source) { Ok(expression) => Ok(Condition { source, expression }), - Err(err) => Err(ConditionParsingError { + Err(_err) => Err(ConditionParsingError { error: SyntaxError { pos: 0, error: ErrorType::MissingToken, @@ -131,6 +132,173 @@ impl TryFrom for Condition { }), } } + + fn try_from_simple(source: String) -> Result { + match conditions::Scanner::scan(source.clone()) { + Ok(tokens) => match tokens.len().cmp(&(3_usize)) { + Ordering::Equal => { + match ( + &tokens[0].token_type, + &tokens[1].token_type, + &tokens[2].token_type, + ) { + ( + TokenType::Identifier, + TokenType::EqualEqual | TokenType::NotEqual, + TokenType::String, + ) => { + if let ( + Some(Literal::Identifier(var_name)), + Some(Literal::String(operand)), + ) = (&tokens[0].literal, &tokens[2].literal) + { + let predicate = match &tokens[1].token_type { + TokenType::EqualEqual => Equals, + TokenType::NotEqual => NotEquals, + _ => unreachable!(), + }; + Ok(Condition { + source, + expression: Expression::Relation( + Box::new(Expression::Ident(var_name.clone().into())), + predicate, + Box::new(Expression::Atom(Atom::String(operand.clone().into()))), + ), + }) + } else { + panic!( + "Unexpected state {tokens:?} returned from Scanner for: `{source}`" + ) + } + } + ( + TokenType::String, + TokenType::EqualEqual | TokenType::NotEqual, + TokenType::Identifier, + ) => { + if let ( + Some(Literal::String(operand)), + Some(Literal::Identifier(var_name)), + ) = (&tokens[0].literal, &tokens[2].literal) + { + let predicate = match &tokens[1].token_type { + TokenType::EqualEqual => Equals, + TokenType::NotEqual => NotEquals, + _ => unreachable!(), + }; + Ok(Condition { + source, + expression: Expression::Relation( + Box::new(Expression::Atom(Atom::String(operand.clone().into()))), + predicate, + Box::new(Expression::Ident(var_name.clone().into())), + ), + }) + } else { + panic!( + "Unexpected state {tokens:?} returned from Scanner for: `{source}`" + ) + } + } + #[cfg(feature = "lenient_conditions")] + (TokenType::Identifier, TokenType::EqualEqual, TokenType::Identifier) => { + if let ( + Some(Literal::Identifier(var_name)), + Some(Literal::Identifier(operand)), + ) = (&tokens[0].literal, &tokens[2].literal) + { + deprecated::deprecated_syntax_used(); + Ok(Condition { + source, + expression: Expression::Relation( + Box::new(Expression::Ident(var_name.clone().into())), + Equals, + Box::new(Expression::Atom(Atom::String(operand.clone().into()))), + ), + }) + } else { + panic!( + "Unexpected state {tokens:?} returned from Scanner for: `{source}`" + ) + } + } + #[cfg(feature = "lenient_conditions")] + (TokenType::Identifier, TokenType::EqualEqual, TokenType::Number) => { + if let ( + Some(Literal::Identifier(var_name)), + Some(Literal::Number(operand)), + ) = (&tokens[0].literal, &tokens[2].literal) + { + deprecated::deprecated_syntax_used(); + Ok(Condition { + source, + expression: Expression::Relation( + Box::new(Expression::Ident(var_name.clone().into())), + Equals, + Box::new(Expression::Atom(Atom::String(operand.clone().into()))), + ), + }) + } else { + panic!( + "Unexpected state {tokens:?} returned from Scanner for: `{source}`" + ) + } + } + (t1, t2, _) => { + let faulty = match (t1, t2) { + ( + TokenType::Identifier | TokenType::String, + TokenType::EqualEqual | TokenType::NotEqual, + ) => 2, + (TokenType::Identifier | TokenType::String, _) => 1, + (_, _) => 0, + }; + Err(ConditionParsingError { + error: SyntaxError { + pos: tokens[faulty].pos, + error: ErrorType::UnexpectedToken(tokens[faulty].clone()), + }, + tokens, + condition: source, + }) + } + } + } + Ordering::Less => Err(ConditionParsingError { + error: SyntaxError { + pos: source.len(), + error: ErrorType::MissingToken, + }, + tokens, + condition: source, + }), + Ordering::Greater => Err(ConditionParsingError { + error: SyntaxError { + pos: tokens[3].pos, + error: ErrorType::UnexpectedToken(tokens[3].clone()), + }, + tokens, + condition: source, + }), + }, + Err(err) => Err(ConditionParsingError { + error: err, + tokens: Vec::new(), + condition: source, + }), + } + } +} + +impl TryFrom for Condition { + type Error = ConditionParsingError; + + fn try_from(value: String) -> Result { + if value.clone().starts_with("cel:") { + return Condition::try_from_cel(value.strip_prefix("cel:").unwrap().to_string()); + } + return Condition::try_from_simple(value); + } } impl From for String { @@ -798,7 +966,7 @@ mod tests { "test_namespace", 10, 60, - vec!["x == string((11 - 1) / 2)", "y == \"2\""], + vec!["cel:x == string((11 - 1) / 2)", "y == \"2\""], vec!["z"], ); @@ -860,7 +1028,6 @@ mod tests { ); } - #[ignore] #[test] #[cfg(not(feature = "lenient_conditions"))] fn invalid_deprecated_condition_parsing() { @@ -869,10 +1036,9 @@ mod tests { .expect("Should fail!"); } - #[ignore] #[test] fn invalid_condition_parsing() { - let result = serde_json::from_str::(r#""x != 5 ` x > 12""#) + let result = serde_json::from_str::(r#""x != 5 && x > 12""#) .expect_err("should fail parsing"); assert_eq!( result.to_string(), diff --git a/limitador/src/storage/keys.rs b/limitador/src/storage/keys.rs index 6d32977c..8a26a0f7 100644 --- a/limitador/src/storage/keys.rs +++ b/limitador/src/storage/keys.rs @@ -99,7 +99,7 @@ mod tests { vec!["app_id"], ); assert_eq!( - "namespace:{example.com},counters_of_limit:{\"namespace\":\"example.com\",\"seconds\":60,\"conditions\":[\"req.method == \\\"GET\\\"\"],\"variables\":[\"app_id\"]}", + "namespace:{example.com},counters_of_limit:{\"namespace\":\"example.com\",\"seconds\":60,\"conditions\":[\"req.method == 'GET'\"],\"variables\":[\"app_id\"]}", key_for_counters_of_limit(&limit)) }