diff --git a/README.md b/README.md
index 62ad7980..f3666936 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,50 @@ A Proxy-Wasm module written in Rust, acting as a shim between Envoy and Limitado
Following is a sample configuration used by the shim.
+```yaml
+services:
+ auth-service:
+ type: auth
+ endpoint: auth-cluster
+ failureMode: deny
+ timeout: 10ms
+ ratelimit-service:
+ type: ratelimit
+ endpoint: ratelimit-cluster
+ failureMode: deny
+actionSets:
+ - name: rlp-ns-A/rlp-name-A
+ routeRuleConditions:
+ hostnames: [ "*.toystore.com" ]
+ predicates:
+ - request.url_path.startsWith("/get")
+ - request.host == "test.toystore.com"
+ - request.method == "GET"
+ actions:
+ - service: ratelimit-service
+ scope: rlp-ns-A/rlp-name-A
+ conditions: []
+ data:
+ - expression:
+ key: my_header
+ value: request.headers["My-Custom-Header"]
+```
+
+## Features
+
+### CEL Predicates and Expression
+
+`routeRuleConditions`'s `predicate`s are expressed in [Common Expression Language (CEL)](https://cel.dev). `Predicate`s
+evaluating to a `bool` value, while `Expression`, used for passing data to a service, evaluate to some `Value`.
+
+These expression can operate on the data made available to them through the Well Known Attributes, see below
+
+#### Conditions, Selectors and Operators (deprecated!)
+
+
+
+While still supported, these will eventually disappear. For now though, you still can express them as such:
+
```yaml
services:
auth-service:
@@ -46,10 +90,6 @@ actionSets:
value: "1"
```
-## Features
-
-#### Condition operators implemented
-
```Rust
#[derive(Deserialize, PartialEq, Debug, Clone)]
pub enum WhenConditionOperator {
@@ -66,15 +106,6 @@ pub enum WhenConditionOperator {
}
```
-The `matches` operator is a a simple globbing pattern implementation based on regular expressions.
-The only characters taken into account are:
-
-* `?`: 0 or 1 characters
-* `*`: 0 or more characters
-* `+`: 1 or more characters
-
-#### Selectors
-
Selector of an attribute from the contextual properties provided by kuadrant.
See [Well Known Attributes](#Well-Known-Attributes) for more info about available attributes.
@@ -109,12 +140,16 @@ Some path segments include dot `.` char in them. For instance envoy filter
names: `envoy.filters.http.header_to_metadata`.
In that particular cases, the dot chat (separator), needs to be escaped.
+
+
+
### Well Known Attributes
-| Attribute | Description |
-| --- | --- |
-| [Envoy Attributes](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes) | Contextual properties provided by Envoy during request and connection processing |
-| `source.remote_address` | This attribute evaluates to the `trusted client address` (IP address without port) as it is being defined by [Envoy Doc](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for) |
+| Attribute | Description |
+|---------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [Envoy Attributes](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes) | Contextual properties provided by Envoy during request and connection processing |
+| `source.remote_address` | This attribute evaluates to the `trusted client address` (IP address without port) as it is being defined by [Envoy Doc](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for) |
+| `auth.*` | Data made available by the authentication service to the `ActionSet`'s pipeline |
## Building
diff --git a/src/configuration.rs b/src/configuration.rs
index fb239820..22c23380 100644
--- a/src/configuration.rs
+++ b/src/configuration.rs
@@ -4,10 +4,11 @@ use std::fmt::{Debug, Formatter};
use std::rc::Rc;
use std::sync::Arc;
-use crate::attribute::Attribute;
use crate::configuration::action_set::ActionSet;
use crate::configuration::action_set_index::ActionSetIndex;
-use crate::property_path::Path;
+use crate::data;
+use crate::data::PropertyPath;
+use crate::data::{AttributeValue, Predicate};
use crate::service::GrpcService;
use cel_interpreter::functions::duration;
use cel_interpreter::objects::ValueType;
@@ -22,6 +23,23 @@ pub mod action;
pub mod action_set;
mod action_set_index;
+#[derive(Deserialize, Debug, Clone)]
+pub struct ExpressionItem {
+ pub key: String,
+ pub value: String,
+ #[serde(skip_deserializing)]
+ pub compiled: OnceCell,
+}
+
+impl ExpressionItem {
+ pub fn compile(&self) -> Result<(), String> {
+ self.compiled
+ .set(data::Expression::new(&self.value).map_err(|e| e.to_string())?)
+ .expect("Expression must not be compiled yet!");
+ Ok(())
+ }
+}
+
#[derive(Deserialize, Debug, Clone)]
pub struct SelectorItem {
// Selector of an attribute from the contextual properties provided by kuadrant
@@ -38,7 +56,7 @@ pub struct SelectorItem {
pub default: Option,
#[serde(skip_deserializing)]
- path: OnceCell,
+ path: OnceCell,
}
impl SelectorItem {
@@ -48,7 +66,7 @@ impl SelectorItem {
.map_err(|p| format!("Err on {p:?}"))
}
- pub fn path(&self) -> &Path {
+ pub fn path(&self) -> &PropertyPath {
self.path
.get()
.expect("SelectorItem wasn't previously compiled!")
@@ -67,6 +85,7 @@ pub struct StaticItem {
pub enum DataType {
Static(StaticItem),
Selector(SelectorItem),
+ Expression(ExpressionItem),
}
impl DataType {
@@ -74,6 +93,7 @@ impl DataType {
match self {
DataType::Static(_) => Ok(()),
DataType::Selector(selector) => selector.compile(),
+ DataType::Expression(exp) => exp.compile(),
}
}
}
@@ -107,7 +127,7 @@ pub struct PatternExpression {
pub value: String,
#[serde(skip_deserializing)]
- path: OnceCell,
+ path: OnceCell,
#[serde(skip_deserializing)]
compiled: OnceCell,
}
@@ -121,23 +141,22 @@ impl PatternExpression {
.set(self.try_into()?)
.map_err(|_| "Ooops".to_string())
}
- pub fn path(&self) -> Vec<&str> {
+ pub fn path(&self) -> &PropertyPath {
self.path
.get()
.expect("PatternExpression wasn't previously compiled!")
- .tokens()
}
pub fn eval(&self, raw_attribute: Vec) -> Result {
let cel_type = &self.compiled.get().unwrap().cel_type;
let value = match cel_type {
- ValueType::String => Value::String(Arc::new(Attribute::parse(raw_attribute)?)),
- ValueType::Int => Value::Int(Attribute::parse(raw_attribute)?),
- ValueType::UInt => Value::UInt(Attribute::parse(raw_attribute)?),
- ValueType::Float => Value::Float(Attribute::parse(raw_attribute)?),
- ValueType::Bytes => Value::Bytes(Arc::new(Attribute::parse(raw_attribute)?)),
- ValueType::Bool => Value::Bool(Attribute::parse(raw_attribute)?),
- ValueType::Timestamp => Value::Timestamp(Attribute::parse(raw_attribute)?),
+ ValueType::String => Value::String(Arc::new(AttributeValue::parse(raw_attribute)?)),
+ ValueType::Int => Value::Int(AttributeValue::parse(raw_attribute)?),
+ ValueType::UInt => Value::UInt(AttributeValue::parse(raw_attribute)?),
+ ValueType::Float => Value::Float(AttributeValue::parse(raw_attribute)?),
+ ValueType::Bytes => Value::Bytes(Arc::new(AttributeValue::parse(raw_attribute)?)),
+ ValueType::Bool => Value::Bool(AttributeValue::parse(raw_attribute)?),
+ ValueType::Timestamp => Value::Timestamp(AttributeValue::parse(raw_attribute)?),
// todo: Impl support for parsing these two types… Tho List/Map of what?
// ValueType::List => {}
// ValueType::Map => {}
@@ -157,8 +176,7 @@ impl PatternExpression {
}
fn applies(&self) -> bool {
- let attribute_path = self.path();
- let attribute_value = match crate::property::get_property(attribute_path).unwrap() {
+ let attribute_value = match crate::data::get_property(self.path()).unwrap() {
//TODO(didierofrivia): Replace hostcalls by DI
None => {
debug!(
@@ -169,6 +187,11 @@ impl PatternExpression {
}
Some(attribute_bytes) => attribute_bytes,
};
+
+ // if someone would have the P_E be:
+ // selector: auth.identity.anonymous
+ // operator: eq
+ // value: \""true"\"
self.eval(attribute_value).unwrap_or_else(|e| {
debug!("pattern_expression_applies failed: {}", e);
false
@@ -443,6 +466,15 @@ impl TryFrom for FilterConfig {
return Err(result.err().unwrap());
}
}
+ 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())?);
+ }
+ action_set
+ .route_rule_conditions
+ .compiled_predicates
+ .set(predicates)
+ .expect("Predicates must not be compiled yet!");
for action in &action_set.actions {
for condition in &action.conditions {
let result = condition.compile();
@@ -450,6 +482,15 @@ impl TryFrom for FilterConfig {
return Err(result.err().unwrap());
}
}
+ let mut predicates = Vec::default();
+ for predicate in &action.predicates {
+ predicates.push(Predicate::new(predicate).map_err(|e| e.to_string())?);
+ }
+ action
+ .compiled_predicates
+ .set(predicates)
+ .expect("Predicates must not be compiled yet!");
+
for datum in &action.data {
let result = datum.item.compile();
if result.is_err() {
@@ -619,8 +660,9 @@ mod test {
}
},
{
- "selector": {
- "selector": "auth.metadata.username"
+ "expression": {
+ "key": "username",
+ "value": "auth.metadata.username"
}
}]
}]
@@ -700,10 +742,9 @@ mod test {
panic!();
}
- if let DataType::Selector(selector_item) = &rl_data_items[1].item {
- assert_eq!(selector_item.selector, "auth.metadata.username");
- assert!(selector_item.key.is_none());
- assert!(selector_item.default.is_none());
+ if let DataType::Expression(exp) = &rl_data_items[1].item {
+ assert_eq!(exp.key, "username");
+ assert_eq!(exp.value, "auth.metadata.username");
} else {
panic!();
}
diff --git a/src/configuration/action.rs b/src/configuration/action.rs
index 32f538d9..4cbfe31b 100644
--- a/src/configuration/action.rs
+++ b/src/configuration/action.rs
@@ -1,9 +1,11 @@
-use crate::attribute::Attribute;
use crate::configuration::{DataItem, DataType, PatternExpression};
+use crate::data::Predicate;
use crate::envoy::{RateLimitDescriptor, RateLimitDescriptor_Entry};
+use cel_interpreter::Value;
use log::debug;
use protobuf::RepeatedField;
use serde::Deserialize;
+use std::cell::OnceCell;
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
@@ -13,12 +15,24 @@ pub struct Action {
#[serde(default)]
pub conditions: Vec,
#[serde(default)]
+ pub predicates: Vec,
+ #[serde(skip_deserializing)]
+ pub compiled_predicates: OnceCell>,
+ #[serde(default)]
pub data: Vec,
}
impl Action {
pub fn conditions_apply(&self) -> bool {
- self.conditions.is_empty() || self.conditions.iter().all(|m| m.applies())
+ let predicates = self
+ .compiled_predicates
+ .get()
+ .expect("predicates must be compiled by now");
+ if predicates.is_empty() {
+ self.conditions.is_empty() || self.conditions.iter().all(PatternExpression::applies)
+ } else {
+ predicates.iter().all(Predicate::test)
+ }
}
pub fn build_descriptors(&self) -> RepeatedField {
@@ -34,28 +48,42 @@ impl Action {
// iterate over data items to allow any data item to skip the entire descriptor
for data in self.data.iter() {
- match &data.item {
+ let (key, value) = match &data.item {
DataType::Static(static_item) => {
- let mut descriptor_entry = RateLimitDescriptor_Entry::new();
- descriptor_entry.set_key(static_item.key.to_owned());
- descriptor_entry.set_value(static_item.value.to_owned());
- entries.push(descriptor_entry);
+ (static_item.key.to_owned(), static_item.value.to_owned())
}
+ DataType::Expression(cel) => (
+ cel.key.clone(),
+ match cel
+ .compiled
+ .get()
+ .expect("Expression must be compiled by now")
+ .eval()
+ {
+ Value::Int(n) => format!("{n}"),
+ Value::UInt(n) => format!("{n}"),
+ Value::Float(n) => format!("{n}"),
+ // todo this probably should be a proper string literal!
+ Value::String(s) => (*s).clone(),
+ Value::Bool(b) => format!("{b}"),
+ Value::Null => "null".to_owned(),
+ _ => panic!("Only scalar values can be sent as data"),
+ },
+ ),
DataType::Selector(selector_item) => {
let descriptor_key = match &selector_item.key {
None => selector_item.path().to_string(),
Some(key) => key.to_owned(),
};
- let attribute_path = selector_item.path();
- let value = match crate::property::get_property(attribute_path.tokens())
- .unwrap()
+ let value = match crate::data::get_attribute::(selector_item.path())
+ .expect("Error!")
{
//TODO(didierofrivia): Replace hostcalls by DI
None => {
debug!(
"build_single_descriptor: selector not found: {}",
- attribute_path
+ selector_item.path()
);
match &selector_item.default {
None => return None, // skipping the entire descriptor
@@ -64,26 +92,20 @@ impl Action {
}
// TODO(eastizle): not all fields are strings
// https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes
- Some(attribute_bytes) => match Attribute::parse(attribute_bytes) {
- Ok(attr_str) => attr_str,
- Err(e) => {
- debug!("build_single_descriptor: failed to parse selector value: {}, error: {}",
- attribute_path, e);
- return None;
- }
- },
+ Some(attr_str) => attr_str,
// Alternative implementation (for rust >= 1.76)
// Attribute::parse(attribute_bytes)
// .inspect_err(|e| debug!("#{} build_single_descriptor: failed to parse selector value: {}, error: {}",
// filter.context_id, attribute_path, e))
// .ok()?,
};
- let mut descriptor_entry = RateLimitDescriptor_Entry::new();
- descriptor_entry.set_key(descriptor_key);
- descriptor_entry.set_value(value);
- entries.push(descriptor_entry);
+ (descriptor_key, value)
}
- }
+ };
+ let mut descriptor_entry = RateLimitDescriptor_Entry::new();
+ descriptor_entry.set_key(key);
+ descriptor_entry.set_value(value);
+ entries.push(descriptor_entry);
}
let mut res = RateLimitDescriptor::new();
res.set_entries(entries);
diff --git a/src/configuration/action_set.rs b/src/configuration/action_set.rs
index 6448dadb..c2ebba8d 100644
--- a/src/configuration/action_set.rs
+++ b/src/configuration/action_set.rs
@@ -1,12 +1,18 @@
use crate::configuration::action::Action;
use crate::configuration::PatternExpression;
+use crate::data::Predicate;
use serde::Deserialize;
+use std::cell::OnceCell;
#[derive(Deserialize, Debug, Clone, Default)]
pub struct RouteRuleConditions {
pub hostnames: Vec,
#[serde(default)]
pub matches: Vec,
+ #[serde(default)]
+ pub predicates: Vec,
+ #[serde(skip_deserializing)]
+ pub compiled_predicates: OnceCell>,
}
#[derive(Default, Deserialize, Debug, Clone)]
@@ -32,11 +38,20 @@ impl ActionSet {
}
pub fn conditions_apply(&self) -> bool {
- self.route_rule_conditions.matches.is_empty()
- || self
- .route_rule_conditions
- .matches
- .iter()
- .all(|m| m.applies())
+ let predicates = self
+ .route_rule_conditions
+ .compiled_predicates
+ .get()
+ .expect("predicates must be compiled by now");
+ if predicates.is_empty() {
+ self.route_rule_conditions.matches.is_empty()
+ || self
+ .route_rule_conditions
+ .matches
+ .iter()
+ .all(|m| m.applies())
+ } else {
+ predicates.iter().all(Predicate::test)
+ }
}
}
diff --git a/src/attribute.rs b/src/data/attribute.rs
similarity index 76%
rename from src/attribute.rs
rename to src/data/attribute.rs
index 130af5f0..9e65caa9 100644
--- a/src/attribute.rs
+++ b/src/data/attribute.rs
@@ -1,18 +1,19 @@
-use crate::property_path::Path;
+use crate::data::PropertyPath;
use chrono::{DateTime, FixedOffset};
-use log::{debug, error};
+use log::{debug, error, warn};
use protobuf::well_known_types::Struct;
use proxy_wasm::hostcalls;
+use serde_json::Value;
pub const KUADRANT_NAMESPACE: &str = "kuadrant";
-pub trait Attribute {
+pub trait AttributeValue {
fn parse(raw_attribute: Vec) -> Result
where
Self: Sized;
}
-impl Attribute for String {
+impl AttributeValue for String {
fn parse(raw_attribute: Vec) -> Result {
String::from_utf8(raw_attribute).map_err(|err| {
format!(
@@ -23,7 +24,7 @@ impl Attribute for String {
}
}
-impl Attribute for i64 {
+impl AttributeValue for i64 {
fn parse(raw_attribute: Vec) -> Result {
if raw_attribute.len() != 8 {
return Err(format!(
@@ -39,7 +40,7 @@ impl Attribute for i64 {
}
}
-impl Attribute for u64 {
+impl AttributeValue for u64 {
fn parse(raw_attribute: Vec) -> Result {
if raw_attribute.len() != 8 {
return Err(format!(
@@ -55,7 +56,7 @@ impl Attribute for u64 {
}
}
-impl Attribute for f64 {
+impl AttributeValue for f64 {
fn parse(raw_attribute: Vec) -> Result {
if raw_attribute.len() != 8 {
return Err(format!(
@@ -71,13 +72,13 @@ impl Attribute for f64 {
}
}
-impl Attribute for Vec {
+impl AttributeValue for Vec {
fn parse(raw_attribute: Vec) -> Result {
Ok(raw_attribute)
}
}
-impl Attribute for bool {
+impl AttributeValue for bool {
fn parse(raw_attribute: Vec) -> Result {
if raw_attribute.len() != 1 {
return Err(format!(
@@ -89,7 +90,7 @@ impl Attribute for bool {
}
}
-impl Attribute for DateTime {
+impl AttributeValue for DateTime {
fn parse(raw_attribute: Vec) -> Result {
if raw_attribute.len() != 8 {
return Err(format!(
@@ -107,19 +108,19 @@ impl Attribute for DateTime {
}
}
-pub fn get_attribute(attr: &str) -> Result
+pub fn get_attribute(path: &PropertyPath) -> Result