use super::execution_context::current_auth_identity;
use super::*;
pub(crate) fn rls_policy_filter(
runtime: &RedDBRuntime,
table: &str,
action: crate::storage::query::ast::PolicyAction,
) -> Option<crate::storage::query::ast::Filter> {
rls_policy_filter_for_kind(
runtime,
table,
action,
crate::storage::query::ast::PolicyTargetKind::Table,
)
}
pub(crate) fn rls_policy_filter_for_kind(
runtime: &RedDBRuntime,
table: &str,
action: crate::storage::query::ast::PolicyAction,
kind: crate::storage::query::ast::PolicyTargetKind,
) -> Option<crate::storage::query::ast::Filter> {
use crate::storage::query::ast::Filter;
if !runtime.inner.rls_enabled_tables.read().contains(table) {
return None;
}
let role = current_auth_identity().map(|(_, role)| role);
let role_str = role.map(|r| r.as_str().to_string());
let policies = runtime.matching_rls_policies_for_kind(table, role_str.as_deref(), action, kind);
if policies.is_empty() {
return None;
}
policies
.into_iter()
.reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
}
pub(crate) fn rls_is_enabled(runtime: &RedDBRuntime, table: &str) -> bool {
runtime.inner.rls_enabled_tables.read().contains(table)
}
pub(crate) fn node_passes_rls(
runtime: &RedDBRuntime,
collection: &str,
role: Option<&str>,
cache: &mut std::collections::HashMap<String, Option<crate::storage::query::ast::Filter>>,
entity: &crate::storage::unified::entity::UnifiedEntity,
) -> bool {
use crate::storage::query::ast::{Filter, PolicyAction, PolicyTargetKind};
if !runtime.inner.rls_enabled_tables.read().contains(collection) {
return true;
}
let filter = cache.entry(collection.to_string()).or_insert_with(|| {
let policies = runtime.matching_rls_policies_for_kind(
collection,
role,
PolicyAction::Select,
PolicyTargetKind::Nodes,
);
if policies.is_empty() {
None
} else {
policies
.into_iter()
.reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
}
});
let Some(filter) = filter else {
return false;
};
crate::runtime::query_exec::evaluate_entity_filter_with_db(
Some(&runtime.inner.db),
entity,
filter,
collection,
collection,
)
}
pub(crate) fn edge_passes_rls(
runtime: &RedDBRuntime,
collection: &str,
role: Option<&str>,
cache: &mut std::collections::HashMap<String, Option<crate::storage::query::ast::Filter>>,
entity: &crate::storage::unified::entity::UnifiedEntity,
) -> bool {
use crate::storage::query::ast::{Filter, PolicyAction, PolicyTargetKind};
if !runtime.inner.rls_enabled_tables.read().contains(collection) {
return true;
}
let filter = cache.entry(collection.to_string()).or_insert_with(|| {
let policies = runtime.matching_rls_policies_for_kind(
collection,
role,
PolicyAction::Select,
PolicyTargetKind::Edges,
);
if policies.is_empty() {
None
} else {
policies
.into_iter()
.reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
}
});
let Some(filter) = filter else {
return false;
};
crate::runtime::query_exec::evaluate_entity_filter_with_db(
Some(&runtime.inner.db),
entity,
filter,
collection,
collection,
)
}
pub(crate) fn inject_rls_filters(
runtime: &RedDBRuntime,
frame: &dyn super::statement_frame::ReadFrame,
mut table: crate::storage::query::ast::TableQuery,
) -> Option<crate::storage::query::ast::TableQuery> {
use crate::storage::query::ast::{Filter, PolicyAction};
let role = frame.identity().map(|(_, role)| role);
let role_str = role.map(|r| r.as_str().to_string());
let policies =
runtime.matching_rls_policies(&table.table, role_str.as_deref(), PolicyAction::Select);
if policies.is_empty() {
return None;
}
let combined = policies
.into_iter()
.reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
.expect("policies non-empty");
use crate::storage::query::sql_lowering::{expr_to_filter, filter_to_expr};
let had_where_expr = table.where_expr.is_some();
let existing = table
.filter
.take()
.or_else(|| table.where_expr.as_ref().map(expr_to_filter));
let new_filter = match existing {
Some(existing) => Filter::And(Box::new(existing), Box::new(combined)),
None => combined,
};
if had_where_expr {
table.where_expr = Some(filter_to_expr(&new_filter));
}
table.filter = Some(new_filter);
Some(table)
}
pub(crate) fn inject_rls_into_join(
runtime: &RedDBRuntime,
frame: &dyn super::statement_frame::ReadFrame,
mut join: crate::storage::query::ast::JoinQuery,
) -> Option<crate::storage::query::ast::JoinQuery> {
use crate::storage::query::ast::Filter;
let mut policy_filters: Vec<Filter> = Vec::new();
if !collect_join_side_policy(runtime, frame, join.left.as_ref(), &mut policy_filters) {
return None;
}
if !collect_join_side_policy(runtime, frame, join.right.as_ref(), &mut policy_filters) {
return None;
}
if policy_filters.is_empty() {
return Some(join);
}
let combined = policy_filters
.into_iter()
.reduce(|acc, f| Filter::And(Box::new(acc), Box::new(f)))
.expect("policy_filters non-empty");
join.filter = Some(match join.filter.take() {
Some(existing) => Filter::And(Box::new(existing), Box::new(combined)),
None => combined,
});
Some(join)
}
fn collect_join_side_policy(
runtime: &RedDBRuntime,
frame: &dyn super::statement_frame::ReadFrame,
expr: &crate::storage::query::ast::QueryExpr,
out: &mut Vec<crate::storage::query::ast::Filter>,
) -> bool {
use crate::storage::query::ast::{Filter, PolicyAction, QueryExpr};
match expr {
QueryExpr::Table(t) => {
if !runtime.inner.rls_enabled_tables.read().contains(&t.table) {
return true;
}
let role = frame.identity().map(|(_, role)| role);
let role_str = role.map(|r| r.as_str().to_string());
let policies =
runtime.matching_rls_policies(&t.table, role_str.as_deref(), PolicyAction::Select);
if policies.is_empty() {
return false;
}
let combined = policies
.into_iter()
.reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
.expect("policies non-empty");
out.push(combined);
true
}
QueryExpr::Join(inner) => {
collect_join_side_policy(runtime, frame, inner.left.as_ref(), out)
&& collect_join_side_policy(runtime, frame, inner.right.as_ref(), out)
}
_ => true,
}
}
pub(crate) fn apply_foreign_table_filters(
records: Vec<crate::storage::query::unified::UnifiedRecord>,
query: &crate::storage::query::ast::TableQuery,
) -> crate::storage::query::unified::UnifiedResult {
use crate::storage::query::sql_lowering::{
effective_table_filter, effective_table_projections,
};
use crate::storage::query::unified::UnifiedResult;
let filter = effective_table_filter(query);
let projections = effective_table_projections(query);
let mut filtered: Vec<_> = records
.into_iter()
.filter(|record| match &filter {
Some(f) => {
super::join_filter::evaluate_runtime_filter_with_db(None, record, f, None, None)
}
None => true,
})
.collect();
if let Some(offset) = query.offset {
let offset = offset as usize;
if offset >= filtered.len() {
filtered.clear();
} else {
filtered.drain(0..offset);
}
}
if let Some(limit) = query.limit {
filtered.truncate(limit as usize);
}
let columns: Vec<String> = if projections.is_empty() {
filtered
.first()
.map(|r| r.column_names().iter().map(|k| k.to_string()).collect())
.unwrap_or_default()
} else {
projections
.iter()
.map(super::join_filter::projection_name)
.collect()
};
let mut result = UnifiedResult::empty();
result.columns = columns;
result.records = filtered;
result
}
impl RedDBRuntime {
pub fn foreign_tables(&self) -> Arc<crate::storage::fdw::ForeignTableRegistry> {
Arc::clone(&self.inner.foreign_tables)
}
pub fn is_rls_enabled(&self, table: &str) -> bool {
self.inner.rls_enabled_tables.read().contains(table)
}
pub fn matching_rls_policies(
&self,
table: &str,
role: Option<&str>,
action: crate::storage::query::ast::PolicyAction,
) -> Vec<crate::storage::query::ast::Filter> {
self.matching_rls_policies_for_kind(
table,
role,
action,
crate::storage::query::ast::PolicyTargetKind::Table,
)
}
pub fn matching_rls_policies_for_kind(
&self,
table: &str,
role: Option<&str>,
action: crate::storage::query::ast::PolicyAction,
kind: crate::storage::query::ast::PolicyTargetKind,
) -> Vec<crate::storage::query::ast::Filter> {
if !self.is_rls_enabled(table) {
return Vec::new();
}
let policies = self.inner.rls_policies.read();
policies
.iter()
.filter_map(|((t, _), p)| {
if t != table {
return None;
}
if p.target_kind != kind
&& p.target_kind != crate::storage::query::ast::PolicyTargetKind::Table
{
return None;
}
if let Some(a) = p.action {
if a != action {
return None;
}
}
if let Some(p_role) = p.role.as_deref() {
match role {
Some(r) if r == p_role => {}
_ => return None,
}
}
Some((*p.using).clone())
})
.collect()
}
}