use dataflow_rs::datalogic_rs;
use std::sync::Arc;
use serde_json::{Value, json};
use super::ChannelRuntimeConfig;
use super::*;
use crate::errors::OrionError;
use crate::metrics;
pub(super) async fn check_rate_limit(
channel: &str,
channel_config: &Option<Arc<ChannelRuntimeConfig>>,
datalogic: &datalogic_rs::Engine,
caller_identity: &str,
header: HeaderLookup<'_>,
) -> Result<(), OrionError> {
let Some(cfg) = channel_config else {
return Ok(());
};
let Some(ref limiter) = cfg.rate_limiter else {
return Ok(());
};
let key = if let Some(ref compiled) = cfg.rate_limit_key_logic {
let context = rate_limit_context(
caller_identity,
channel,
header,
cfg.rate_limit_key_headers.as_deref(),
);
let unavailable = |reason: &str| {
tracing::warn!(
channel = %channel,
reason = %reason,
"rate_limit.key_logic produced no usable key; rejecting request"
);
metrics::record_rate_limit_rejected(channel);
metrics::record_rate_limit_key_unavailable(channel);
OrionError::RateLimitKeyUnavailable("Too many requests".to_string())
};
match datalogic
.session()
.eval_into::<serde_json::Value, _>(compiled, &context)
{
Ok(Value::Null) => return Err(unavailable("expression resolved to null")),
Ok(Value::String(s)) if s.trim().is_empty() => {
return Err(unavailable("expression resolved to an empty string"));
}
Ok(val) => val
.as_str()
.map(str::to_string)
.unwrap_or_else(|| serde_json::to_string(&val).unwrap_or_default()),
Err(e) => {
let err = unavailable(&format!("evaluation failed: {e}"));
return Err(err);
}
}
} else {
caller_identity.to_string()
};
let policy = cfg
.parsed_config
.rate_limit
.as_ref()
.map(|rl| rl.on_backend_error)
.unwrap_or_default();
match limiter.check(key).await {
Ok(true) => Ok(()),
Ok(false) => {
metrics::record_rate_limit_rejected(channel);
Err(OrionError::RateLimited("Too many requests".to_string()))
}
Err(e) => {
metrics::record_error("rate_limit_backend");
match policy {
crate::channel::BackendErrorPolicy::Allow => {
tracing::warn!(
channel = %channel,
error = %e,
"Rate-limit backend error; failing open (request allowed)"
);
Ok(())
}
crate::channel::BackendErrorPolicy::Deny => {
tracing::warn!(
channel = %channel,
error = %e,
"Rate-limit backend error; failing closed (request refused)"
);
metrics::record_rate_limit_rejected(channel);
Err(OrionError::unavailable(
crate::errors::Unavailable::GuardBackend,
format!(
"Channel '{channel}' cannot check its rate limit: the backend \
is unavailable and the channel is configured to fail closed"
),
))
}
}
}
}
}
pub(crate) const COMMON_KEY_HEADERS: &[&str] = &[
"authorization",
"x-api-key",
"x-forwarded-for",
"x-real-ip",
"user-agent",
"content-type",
"origin",
"x-tenant-id",
];
pub(super) fn rate_limit_context(
caller_identity: &str,
channel: &str,
header: HeaderLookup<'_>,
declared: Option<&[String]>,
) -> Value {
let declared = declared.unwrap_or(&[]);
let mut headers = serde_json::Map::with_capacity(COMMON_KEY_HEADERS.len() + declared.len());
for &name in COMMON_KEY_HEADERS {
if let Some(value) = header(name) {
headers.insert(name.to_string(), Value::String(value));
}
}
for name in declared {
if COMMON_KEY_HEADERS.contains(&name.as_str()) {
continue;
}
if let Some(value) = header(name) {
headers.insert(name.clone(), Value::String(value));
}
}
json!({
"client_ip": caller_identity,
"channel": channel,
"headers": headers,
})
}
pub(crate) fn key_logic_header_paths(logic: &Value) -> Vec<String> {
fn literal_path(arg: &Value) -> Option<&str> {
match arg {
Value::String(s) => Some(s.as_str()),
Value::Array(items) => items.first().and_then(|f| f.as_str()),
_ => None,
}
}
fn walk(node: &Value, out: &mut Vec<String>) {
match node {
Value::Object(map) => {
for (op, arg) in map {
if op == "var"
&& let Some(path) = literal_path(arg)
&& let Some(name) = path.strip_prefix("headers.")
&& !name.is_empty()
{
let name = name.to_ascii_lowercase();
if !out.contains(&name) {
out.push(name);
}
}
walk(arg, out);
}
}
Value::Array(items) => {
for item in items {
walk(item, out);
}
}
_ => {}
}
}
let mut out = Vec::new();
walk(logic, &mut out);
out
}
pub(super) async fn check_principal_rate_limit(
channel: &str,
channel_config: &Option<Arc<ChannelRuntimeConfig>>,
datalogic: &datalogic_rs::Engine,
caller_identity: &str,
header: HeaderLookup<'_>,
claims: Option<&Value>,
) -> Result<(), OrionError> {
let Some(cfg) = channel_config else {
return Ok(());
};
let (Some(limiter), Some(compiled)) = (
cfg.principal_rate_limiter.as_ref(),
cfg.principal_rate_limit_key_logic.as_ref(),
) else {
return Ok(());
};
let mut context = rate_limit_context(
caller_identity,
channel,
header,
cfg.rate_limit_key_headers.as_deref(),
);
if let Some(obj) = context.as_object_mut() {
obj.insert("auth".to_string(), claims.cloned().unwrap_or(Value::Null));
}
let unavailable = |reason: &str| {
tracing::warn!(
channel = %channel,
reason = %reason,
"principal_rate_limit.key_logic produced no usable key; rejecting request"
);
metrics::record_rate_limit_rejected(channel);
metrics::record_rate_limit_key_unavailable(channel);
OrionError::RateLimitKeyUnavailable("Too many requests".to_string())
};
let key = match datalogic
.session()
.eval_into::<Value, _>(compiled, &context)
{
Ok(Value::Null) => return Err(unavailable("expression resolved to null")),
Ok(Value::String(s)) if s.trim().is_empty() => {
return Err(unavailable("expression resolved to an empty string"));
}
Ok(val) => val
.as_str()
.map(str::to_string)
.unwrap_or_else(|| serde_json::to_string(&val).unwrap_or_default()),
Err(e) => return Err(unavailable(&format!("evaluation failed: {e}"))),
};
let policy = cfg
.parsed_config
.principal_rate_limit
.as_ref()
.map(|rl| rl.on_backend_error)
.unwrap_or_default();
match limiter.check(key).await {
Ok(true) => Ok(()),
Ok(false) => {
metrics::record_rate_limit_rejected(channel);
tracing::debug!(channel = %channel, "principal rate limit exceeded");
Err(OrionError::RateLimited("Too many requests".to_string()))
}
Err(e) => {
metrics::record_error("rate_limit_backend");
match policy {
crate::channel::BackendErrorPolicy::Allow => {
tracing::warn!(
channel = %channel,
error = %e,
"Principal rate-limit backend error; failing open (request allowed)"
);
Ok(())
}
crate::channel::BackendErrorPolicy::Deny => {
tracing::warn!(
channel = %channel,
error = %e,
"Principal rate-limit backend error; failing closed (request refused)"
);
metrics::record_rate_limit_rejected(channel);
Err(OrionError::unavailable(
crate::errors::Unavailable::GuardBackend,
format!(
"Channel '{channel}' cannot check its principal rate limit: the \
backend is unavailable and the channel is configured to fail closed"
),
))
}
}
}
}
}