1use super::execution_context::current_auth_identity;
20use super::*;
21
22pub(crate) fn rls_policy_filter(
30 runtime: &RedDBRuntime,
31 table: &str,
32 action: reddb_rql::ast::PolicyAction,
33) -> Option<reddb_rql::ast::Filter> {
34 rls_policy_filter_for_kind(
35 runtime,
36 table,
37 action,
38 reddb_rql::ast::PolicyTargetKind::Table,
39 )
40}
41
42pub(crate) fn rls_policy_filter_for_kind(
48 runtime: &RedDBRuntime,
49 table: &str,
50 action: reddb_rql::ast::PolicyAction,
51 kind: reddb_rql::ast::PolicyTargetKind,
52) -> Option<reddb_rql::ast::Filter> {
53 use reddb_rql::ast::Filter;
54
55 if !runtime.inner.rls_enabled_tables.read().contains(table) {
56 return None;
57 }
58 let role = current_auth_identity().map(|(_, role)| role);
59 let role_str = role.map(|r| r.as_str().to_string());
60 let policies = runtime.matching_rls_policies_for_kind(table, role_str.as_deref(), action, kind);
61 if policies.is_empty() {
62 return None;
63 }
64 policies
65 .into_iter()
66 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
67}
68
69pub(crate) fn rls_is_enabled(runtime: &RedDBRuntime, table: &str) -> bool {
73 runtime.inner.rls_enabled_tables.read().contains(table)
74}
75
76pub(crate) fn entity_passes_rls_action(
77 runtime: &RedDBRuntime,
78 collection: &str,
79 action: reddb_rql::ast::PolicyAction,
80 entity: &crate::storage::unified::entity::UnifiedEntity,
81) -> bool {
82 if current_auth_identity().is_none() || !rls_is_enabled(runtime, collection) {
83 return true;
84 }
85 let kind = entity_policy_target_kind(runtime, collection, entity);
86 let Some(filter) = rls_policy_filter_for_kind(runtime, collection, action, kind) else {
87 return false;
88 };
89 crate::runtime::query_exec::evaluate_entity_filter_with_db(
90 Some(&runtime.inner.db),
91 entity,
92 &filter,
93 collection,
94 collection,
95 )
96}
97
98fn entity_policy_target_kind(
99 runtime: &RedDBRuntime,
100 collection: &str,
101 entity: &crate::storage::unified::entity::UnifiedEntity,
102) -> reddb_rql::ast::PolicyTargetKind {
103 use crate::storage::unified::entity::EntityKind;
104 use reddb_rql::ast::PolicyTargetKind;
105
106 match &entity.kind {
107 EntityKind::GraphNode(_) => PolicyTargetKind::Nodes,
108 EntityKind::GraphEdge(_) => PolicyTargetKind::Edges,
109 EntityKind::Vector { .. } => PolicyTargetKind::Vectors,
110 EntityKind::TimeSeriesPoint(_) => PolicyTargetKind::Points,
111 EntityKind::QueueMessage { .. } => PolicyTargetKind::Messages,
112 EntityKind::TableRow { .. }
113 if runtime
114 .db()
115 .collection_contract_arc(collection)
116 .is_some_and(|contract| {
117 contract.declared_model == crate::catalog::CollectionModel::Document
118 }) =>
119 {
120 PolicyTargetKind::Documents
121 }
122 EntityKind::TableRow { .. } => PolicyTargetKind::Table,
123 }
124}
125
126pub(crate) fn kv_watch_event_passes_rls(
127 runtime: &RedDBRuntime,
128 event: &crate::replication::cdc::KvWatchEvent,
129) -> bool {
130 use crate::storage::unified::entity::{EntityData, EntityKind, RowData, UnifiedEntity};
131 use crate::storage::EntityId;
132 use reddb_rql::ast::PolicyAction;
133 use reddb_types::Value;
134 use std::sync::Arc;
135
136 if current_auth_identity().is_none() || !rls_is_enabled(runtime, &event.collection) {
137 return true;
138 }
139
140 let table: Arc<str> = Arc::from(event.collection.as_str());
141 let visible = |value: &crate::json::Value| {
142 let Ok(value) = crate::application::entity::json_to_storage_value(value) else {
143 return false;
144 };
145 let entity = UnifiedEntity::new(
146 EntityId::new(0),
147 EntityKind::TableRow {
148 table: Arc::clone(&table),
149 row_id: 0,
150 },
151 EntityData::Row(RowData::with_names(
152 vec![Value::text(event.key.clone()), value],
153 vec!["key".to_string(), "value".to_string()],
154 )),
155 );
156 entity_passes_rls_action(runtime, &event.collection, PolicyAction::Select, &entity)
157 };
158
159 let mut values = event.before.iter().chain(event.after.iter());
160 let Some(first) = values.next() else {
161 return false;
162 };
163 visible(first) && values.all(visible)
164}
165
166pub(crate) fn node_passes_rls(
173 runtime: &RedDBRuntime,
174 collection: &str,
175 role: Option<&str>,
176 cache: &mut std::collections::HashMap<String, Option<reddb_rql::ast::Filter>>,
177 entity: &crate::storage::unified::entity::UnifiedEntity,
178) -> bool {
179 use reddb_rql::ast::{Filter, PolicyAction, PolicyTargetKind};
180
181 if !runtime.inner.rls_enabled_tables.read().contains(collection) {
182 return true;
183 }
184 let filter = cache.entry(collection.to_string()).or_insert_with(|| {
185 let policies = runtime.matching_rls_policies_for_kind(
186 collection,
187 role,
188 PolicyAction::Select,
189 PolicyTargetKind::Nodes,
190 );
191 if policies.is_empty() {
192 None
193 } else {
194 policies
195 .into_iter()
196 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
197 }
198 });
199 let Some(filter) = filter else {
200 return false;
201 };
202 crate::runtime::query_exec::evaluate_entity_filter_with_db(
203 Some(&runtime.inner.db),
204 entity,
205 filter,
206 collection,
207 collection,
208 )
209}
210
211pub(crate) fn edge_passes_rls(
214 runtime: &RedDBRuntime,
215 collection: &str,
216 role: Option<&str>,
217 cache: &mut std::collections::HashMap<String, Option<reddb_rql::ast::Filter>>,
218 entity: &crate::storage::unified::entity::UnifiedEntity,
219) -> bool {
220 use reddb_rql::ast::{Filter, PolicyAction, PolicyTargetKind};
221
222 if !runtime.inner.rls_enabled_tables.read().contains(collection) {
223 return true;
224 }
225 let filter = cache.entry(collection.to_string()).or_insert_with(|| {
226 let policies = runtime.matching_rls_policies_for_kind(
227 collection,
228 role,
229 PolicyAction::Select,
230 PolicyTargetKind::Edges,
231 );
232 if policies.is_empty() {
233 None
234 } else {
235 policies
236 .into_iter()
237 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
238 }
239 });
240 let Some(filter) = filter else {
241 return false;
242 };
243 crate::runtime::query_exec::evaluate_entity_filter_with_db(
244 Some(&runtime.inner.db),
245 entity,
246 filter,
247 collection,
248 collection,
249 )
250}
251
252pub(crate) fn inject_rls_filters(
273 runtime: &RedDBRuntime,
274 frame: &dyn super::statement_frame::ReadFrame,
275 mut table: reddb_rql::ast::TableQuery,
276) -> Option<reddb_rql::ast::TableQuery> {
277 use reddb_rql::ast::Filter;
278
279 let role = frame.identity().map(|(_, role)| role);
281 let role_str = role.map(|r| r.as_str().to_string());
282 let policies = runtime.matching_select_rls_policies(&table.table, role_str.as_deref());
283
284 if policies.is_empty() {
285 return None;
288 }
289
290 let combined = policies
292 .into_iter()
293 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
294 .expect("policies non-empty");
295
296 use reddb_rql::sql_lowering::{expr_to_filter, filter_to_expr};
305 let had_where_expr = table.where_expr.is_some();
306 let existing = table
307 .filter
308 .take()
309 .or_else(|| table.where_expr.as_ref().map(expr_to_filter));
310 let new_filter = match existing {
311 Some(existing) => Filter::And(Box::new(existing), Box::new(combined)),
312 None => combined,
313 };
314 if had_where_expr {
317 table.where_expr = Some(filter_to_expr(&new_filter));
318 }
319 table.filter = Some(new_filter);
320 Some(table)
321}
322
323pub(crate) fn inject_rls_into_join(
333 runtime: &RedDBRuntime,
334 frame: &dyn super::statement_frame::ReadFrame,
335 mut join: reddb_rql::ast::JoinQuery,
336) -> Option<reddb_rql::ast::JoinQuery> {
337 use reddb_rql::ast::Filter;
338
339 let mut policy_filters: Vec<Filter> = Vec::new();
340 if !collect_join_side_policy(runtime, frame, join.left.as_ref(), &mut policy_filters) {
341 return None;
342 }
343 if !collect_join_side_policy(runtime, frame, join.right.as_ref(), &mut policy_filters) {
344 return None;
345 }
346
347 if policy_filters.is_empty() {
348 return Some(join);
349 }
350
351 let combined = policy_filters
352 .into_iter()
353 .reduce(|acc, f| Filter::And(Box::new(acc), Box::new(f)))
354 .expect("policy_filters non-empty");
355
356 join.filter = Some(match join.filter.take() {
357 Some(existing) => Filter::And(Box::new(existing), Box::new(combined)),
358 None => combined,
359 });
360
361 Some(join)
362}
363
364fn collect_join_side_policy(
369 runtime: &RedDBRuntime,
370 frame: &dyn super::statement_frame::ReadFrame,
371 expr: &reddb_rql::ast::QueryExpr,
372 out: &mut Vec<reddb_rql::ast::Filter>,
373) -> bool {
374 use reddb_rql::ast::{Filter, QueryExpr};
375 match expr {
376 QueryExpr::Table(t) => {
377 if !runtime.inner.rls_enabled_tables.read().contains(&t.table) {
378 return true;
379 }
380 let role = frame.identity().map(|(_, role)| role);
381 let role_str = role.map(|r| r.as_str().to_string());
382 let policies = runtime.matching_select_rls_policies(&t.table, role_str.as_deref());
383 if policies.is_empty() {
384 return false;
385 }
386 let combined = policies
387 .into_iter()
388 .reduce(|acc, f| Filter::Or(Box::new(acc), Box::new(f)))
389 .expect("policies non-empty");
390 out.push(combined);
391 true
392 }
393 QueryExpr::Join(inner) => {
394 collect_join_side_policy(runtime, frame, inner.left.as_ref(), out)
395 && collect_join_side_policy(runtime, frame, inner.right.as_ref(), out)
396 }
397 _ => true,
398 }
399}
400
401pub(crate) fn apply_foreign_table_filters(
412 records: Vec<crate::storage::query::unified::UnifiedRecord>,
413 query: &reddb_rql::ast::TableQuery,
414) -> crate::storage::query::unified::UnifiedResult {
415 use crate::storage::query::unified::UnifiedResult;
416 use reddb_rql::sql_lowering::{effective_table_filter, effective_table_projections};
417
418 let filter = effective_table_filter(query);
419 let projections = effective_table_projections(query);
420
421 let mut filtered: Vec<_> = records
424 .into_iter()
425 .filter(|record| match &filter {
426 Some(f) => {
427 super::join_filter::evaluate_runtime_filter_with_db(None, record, f, None, None)
428 }
429 None => true,
430 })
431 .collect();
432
433 if let Some(offset) = query.offset {
435 let offset = offset as usize;
436 if offset >= filtered.len() {
437 filtered.clear();
438 } else {
439 filtered.drain(0..offset);
440 }
441 }
442 if let Some(limit) = query.limit {
443 filtered.truncate(limit as usize);
444 }
445
446 let columns: Vec<String> = if projections.is_empty() {
449 filtered
450 .first()
451 .map(|r| r.column_names().iter().map(|k| k.to_string()).collect())
452 .unwrap_or_default()
453 } else {
454 projections
455 .iter()
456 .map(super::join_filter::projection_name)
457 .collect()
458 };
459
460 let mut result = UnifiedResult::empty();
461 result.columns = columns;
462 result.records = filtered;
463 result
464}
465
466impl RedDBRuntime {
467 fn matching_select_rls_policies(
470 &self,
471 table: &str,
472 role: Option<&str>,
473 ) -> Vec<reddb_rql::ast::Filter> {
474 use reddb_rql::ast::{PolicyAction, PolicyTargetKind};
475
476 let kind = if self
477 .db()
478 .collection_contract_arc(table)
479 .is_some_and(|contract| {
480 contract.declared_model == crate::catalog::CollectionModel::Document
481 }) {
482 PolicyTargetKind::Documents
483 } else {
484 PolicyTargetKind::Table
485 };
486 self.matching_rls_policies_for_kind(table, role, PolicyAction::Select, kind)
487 }
488
489 pub fn foreign_tables(&self) -> Arc<crate::storage::fdw::ForeignTableRegistry> {
496 Arc::clone(&self.inner.foreign_tables)
497 }
498
499 pub fn is_rls_enabled(&self, table: &str) -> bool {
501 self.inner.rls_enabled_tables.read().contains(table)
502 }
503
504 pub fn matching_rls_policies(
511 &self,
512 table: &str,
513 role: Option<&str>,
514 action: reddb_rql::ast::PolicyAction,
515 ) -> Vec<reddb_rql::ast::Filter> {
516 self.matching_rls_policies_for_kind(
521 table,
522 role,
523 action,
524 reddb_rql::ast::PolicyTargetKind::Table,
525 )
526 }
527
528 pub fn matching_rls_policies_for_kind(
536 &self,
537 table: &str,
538 role: Option<&str>,
539 action: reddb_rql::ast::PolicyAction,
540 kind: reddb_rql::ast::PolicyTargetKind,
541 ) -> Vec<reddb_rql::ast::Filter> {
542 if !self.is_rls_enabled(table) {
543 return Vec::new();
544 }
545 let policies = self.inner.rls_policies.read();
546 policies
547 .iter()
548 .filter_map(|((t, _), p)| {
549 if t != table {
550 return None;
551 }
552 if p.target_kind != kind && p.target_kind != reddb_rql::ast::PolicyTargetKind::Table
561 {
562 return None;
563 }
564 if let Some(a) = p.action {
566 if a != action {
567 return None;
568 }
569 }
570 if let Some(p_role) = p.role.as_deref() {
572 match role {
573 Some(r) if r == p_role => {}
574 _ => return None,
575 }
576 }
577 Some((*p.using).clone())
578 })
579 .collect()
580 }
581}