1use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use crate::api::{RedDBError, RedDBResult};
10use crate::storage::RedDB;
11use reddb_types::Value;
12
13thread_local! {
14 static CURRENT_CONN_ID: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
18
19 static CURRENT_AUTH_IDENTITY: std::cell::RefCell<Option<(String, crate::auth::Role)>> =
27 const { std::cell::RefCell::new(None) };
28
29 static CURRENT_SNAPSHOT: std::cell::RefCell<Option<SnapshotContext>> =
39 const { std::cell::RefCell::new(None) };
40
41 static HAS_SNAPSHOT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
47
48 static CURRENT_TENANT_ID: std::cell::RefCell<Option<String>> =
58 const { std::cell::RefCell::new(None) };
59
60 static CURRENT_CONFIG_RESOLVER: std::cell::RefCell<Option<ConfigResolver>> =
64 const { std::cell::RefCell::new(None) };
65
66 static CURRENT_SECRET_RESOLVER: std::cell::RefCell<Option<SecretResolver>> =
70 const { std::cell::RefCell::new(None) };
71
72 static CURRENT_KV_RESOLVER: std::cell::RefCell<Option<KvResolver>> =
76 const { std::cell::RefCell::new(None) };
77}
78
79pub use reddb_engine::transaction::read::SnapshotContext;
80use reddb_engine::transaction::read::{record_serializable_read, visibility_check};
81
82pub fn set_current_connection_id(id: u64) {
91 CURRENT_CONN_ID.with(|c| c.set(id));
92}
93
94pub fn clear_current_connection_id() {
96 CURRENT_CONN_ID.with(|c| c.set(0));
97}
98
99pub fn current_connection_id() -> u64 {
102 CURRENT_CONN_ID.with(|c| c.get())
103}
104
105pub fn set_current_auth_identity(username: String, role: crate::auth::Role) {
109 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = Some((username, role)));
110}
111
112pub fn clear_current_auth_identity() {
116 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = None);
117}
118
119pub(crate) fn current_auth_identity() -> Option<(String, crate::auth::Role)> {
122 CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone())
123}
124
125pub fn current_auth_identity_for_audit() -> Option<(String, crate::auth::Role)> {
129 current_auth_identity()
130}
131
132pub fn set_current_tenant(tenant_id: String) {
137 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = Some(tenant_id));
138}
139
140pub fn clear_current_tenant() {
143 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = None);
144}
145
146pub fn current_tenant() -> Option<String> {
157 let inherited = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
158 if let Some(over) = current_scope_override() {
159 if over.tenant.is_active() {
160 return over.tenant.resolve(inherited);
161 }
162 }
163 if let Some(tx_local) = current_tx_local_tenant() {
164 return tx_local;
165 }
166 inherited
167}
168
169thread_local! {
170 static TX_LOCAL_TENANT: std::cell::RefCell<Option<Option<String>>> =
179 const { std::cell::RefCell::new(None) };
180}
181
182fn current_tx_local_tenant() -> Option<Option<String>> {
183 TX_LOCAL_TENANT.with(|cell| cell.borrow().clone())
184}
185
186pub(crate) fn parse_set_local_tenant(query: &str) -> RedDBResult<Option<Option<String>>> {
192 let mut tokens = query.split_ascii_whitespace();
193 let Some(w1) = tokens.next() else {
194 return Ok(None);
195 };
196 if !w1.eq_ignore_ascii_case("SET") {
197 return Ok(None);
198 }
199 let Some(w2) = tokens.next() else {
200 return Ok(None);
201 };
202 if !w2.eq_ignore_ascii_case("LOCAL") {
203 return Ok(None);
204 }
205 let Some(w3) = tokens.next() else {
206 return Ok(None);
207 };
208 if !w3.eq_ignore_ascii_case("TENANT") {
209 return Ok(None);
210 }
211 let rest: String = tokens.collect::<Vec<_>>().join(" ");
212 let rest = rest.trim().trim_end_matches(';').trim();
213 let value_str = rest.strip_prefix('=').map(|s| s.trim()).unwrap_or(rest);
214 if value_str.is_empty() {
215 return Err(RedDBError::Query(
216 "SET LOCAL TENANT expects a string literal or NULL".to_string(),
217 ));
218 }
219 if value_str.eq_ignore_ascii_case("NULL") {
220 return Ok(Some(None));
221 }
222 if value_str.starts_with('\'') && value_str.ends_with('\'') && value_str.len() >= 2 {
223 let inner = &value_str[1..value_str.len() - 1];
224 return Ok(Some(Some(inner.to_string())));
225 }
226 Err(RedDBError::Query(format!(
227 "SET LOCAL TENANT expects a string literal or NULL, got `{value_str}`"
228 )))
229}
230
231pub(crate) struct TxLocalTenantGuard;
232
233impl TxLocalTenantGuard {
234 pub fn install(value: Option<Option<String>>) -> Self {
235 TX_LOCAL_TENANT.with(|cell| *cell.borrow_mut() = value);
236 Self
237 }
238}
239
240impl Drop for TxLocalTenantGuard {
241 fn drop(&mut self) {
242 TX_LOCAL_TENANT.with(|cell| *cell.borrow_mut() = None);
243 }
244}
245
246thread_local! {
247 static SCOPE_OVERRIDES: std::cell::RefCell<Vec<crate::runtime::within_clause::ScopeOverride>> =
254 const { std::cell::RefCell::new(Vec::new()) };
255}
256
257pub(crate) fn push_scope_override(over: crate::runtime::within_clause::ScopeOverride) {
258 SCOPE_OVERRIDES.with(|cell| cell.borrow_mut().push(over));
259}
260
261pub(crate) fn pop_scope_override() {
262 SCOPE_OVERRIDES.with(|cell| {
263 cell.borrow_mut().pop();
264 });
265}
266
267pub(crate) fn current_scope_override() -> Option<crate::runtime::within_clause::ScopeOverride> {
268 SCOPE_OVERRIDES.with(|cell| cell.borrow().last().cloned())
269}
270
271pub(crate) fn has_scope_override_active() -> bool {
275 SCOPE_OVERRIDES.with(|cell| !cell.borrow().is_empty())
276}
277
278pub(crate) struct ScopeOverrideGuard;
282
283impl ScopeOverrideGuard {
284 pub fn install(over: crate::runtime::within_clause::ScopeOverride) -> Self {
285 push_scope_override(over);
286 Self
287 }
288}
289
290impl Drop for ScopeOverrideGuard {
291 fn drop(&mut self) {
292 pop_scope_override();
293 }
294}
295
296pub(crate) fn current_user_projected() -> Option<String> {
302 let inherited = current_auth_identity().map(|(u, _)| u);
303 if let Some(over) = current_scope_override() {
304 if over.user.is_active() {
305 return over.user.resolve(inherited);
306 }
307 }
308 inherited
309}
310
311pub(crate) fn current_role_projected() -> Option<String> {
312 let inherited = current_auth_identity().map(|(_, r)| format!("{r:?}").to_lowercase());
313 if let Some(over) = current_scope_override() {
314 if over.role.is_active() {
315 return over.role.resolve(inherited);
316 }
317 }
318 inherited
319}
320
321pub(crate) fn current_secret_value(path: &str) -> Option<Value> {
322 try_current_secret_value(path).ok().flatten()
323}
324
325pub(crate) fn try_current_secret_value(path: &str) -> RedDBResult<Option<Value>> {
326 let key = path.to_ascii_lowercase();
327 CURRENT_SECRET_RESOLVER.with(|cell| {
328 let mut resolver = cell.borrow_mut();
329 let Some(resolver) = resolver.as_mut() else {
330 return Ok(None);
331 };
332 if let Some(value) = resolver.values.get(&key) {
333 return Ok(value.clone());
334 }
335 let value = resolver.runtime.query_secret_value(&key)?;
336 resolver.values.insert(key, value.clone());
337 Ok(value)
338 })
339}
340
341pub(crate) fn secret_query_input(value: Value) -> Option<Value> {
344 if !matches!(value, Value::Secret(_)) {
345 return Some(value);
346 }
347 CURRENT_SECRET_RESOLVER.with(|cell| {
348 let resolver = cell.borrow();
349 resolver.as_ref()?.runtime.secret_query_input(value)
350 })
351}
352
353pub(crate) fn try_secret_query_input(
354 value: Value,
355) -> Result<Option<Value>, super::impl_dml_crypto::SecretInputReadLimit> {
356 if !matches!(value, Value::Secret(_)) {
357 return Ok(Some(value));
358 }
359 CURRENT_SECRET_RESOLVER.with(|cell| {
360 let resolver = cell.borrow();
361 match resolver.as_ref() {
362 Some(resolver) => resolver.runtime.try_secret_query_input(value),
363 None => Ok(None),
364 }
365 })
366}
367
368pub(crate) fn secret_query_output(value: Value) -> crate::RedDBResult<Value> {
369 if value.is_null() || matches!(value, Value::Secret(_)) {
370 return Ok(value);
371 }
372 CURRENT_SECRET_RESOLVER.with(|cell| {
373 let resolver = cell.borrow();
374 let resolver = resolver.as_ref().ok_or_else(|| {
375 RedDBError::Query("secret expression requires an active statement".into())
376 })?;
377 resolver.runtime.secret_query_output(value)
378 })
379}
380
381#[cfg(test)]
382fn secret_value_from_snapshot(values: &HashMap<String, String>, key: &str) -> Option<String> {
383 if key.starts_with("red.secret.") {
384 return None;
385 }
386 values.get(key).cloned()
387}
388
389struct SecretResolver {
390 runtime: super::RedDBRuntime,
391 values: HashMap<String, Option<Value>>,
392}
393
394pub(crate) struct SecretStoreGuard {
395 previous: Option<SecretResolver>,
396}
397
398impl SecretStoreGuard {
399 pub(super) fn install(runtime: &super::RedDBRuntime) -> Self {
400 let previous = CURRENT_SECRET_RESOLVER.with(|cell| {
401 cell.replace(Some(SecretResolver {
402 runtime: runtime.clone(),
403 values: HashMap::new(),
404 }))
405 });
406 Self { previous }
407 }
408}
409
410impl Drop for SecretStoreGuard {
411 fn drop(&mut self) {
412 let previous = self.previous.take();
413 CURRENT_SECRET_RESOLVER.with(|cell| {
414 cell.replace(previous);
415 });
416 }
417}
418
419pub(crate) fn current_kv_value(path: &str) -> Option<String> {
427 let key = path.to_ascii_lowercase();
428 CURRENT_KV_RESOLVER.with(|cell| {
429 let mut resolver = cell.borrow_mut();
430 let resolver = resolver.as_mut()?;
431 if resolver.values.is_none() {
432 resolver.values = resolver
433 .store
434 .as_ref()
435 .map(|store| store.plain_kv_snapshot());
436 }
437 let values = resolver.values.as_ref()?;
438 let found = values
442 .get(&key)
443 .map(|value| (key.as_str(), value))
444 .or_else(|| {
445 key.strip_prefix("red.kv/")
446 .and_then(|rest| values.get(rest).map(|value| (rest, value)))
447 })?;
448 if !resolver.can_read(found.0) {
449 return None;
450 }
451 Some(found.1.clone())
452 })
453}
454
455struct KvResolver {
456 store: Option<Arc<crate::auth::store::AuthStore>>,
457 values: Option<HashMap<String, String>>,
458 identity: Option<(String, crate::auth::Role, Option<String>)>,
459}
460
461impl KvResolver {
462 fn can_read(&self, key: &str) -> bool {
463 let Some(store) = &self.store else {
464 return true;
465 };
466 let Some((username, role, tenant)) = &self.identity else {
467 return true;
468 };
469 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
470 let mut resource =
471 crate::auth::policies::ResourceRef::new("kv".to_string(), key.to_string());
472 if let Some(tenant) = tenant {
473 resource = resource.with_tenant(tenant.clone());
474 }
475 let ctx = crate::auth::policies::EvalContext {
476 principal_tenant: tenant.clone(),
477 current_tenant: tenant.clone(),
478 peer_ip: None,
479 mfa_present: false,
480 now_ms: crate::auth::now_ms(),
481 principal_is_admin_role: *role == crate::auth::Role::Admin,
482 principal_is_platform_scoped: tenant.is_none(),
483 };
484 store.check_policy_authz_with_role(&principal, "kv:read", &resource, &ctx, *role)
485 }
486}
487
488pub(crate) struct KvStoreGuard {
489 previous: Option<Box<KvResolver>>,
494}
495
496impl KvStoreGuard {
497 pub(super) fn install(store: Option<Arc<crate::auth::store::AuthStore>>) -> Self {
498 let previous = CURRENT_KV_RESOLVER.with(|cell| {
499 cell.replace(Some(KvResolver {
500 store,
501 values: None,
502 identity: current_auth_identity().map(|(username, role)| {
503 let tenant = current_tenant();
504 (username, role, tenant)
505 }),
506 }))
507 });
508 Self {
509 previous: previous.map(Box::new),
510 }
511 }
512}
513
514impl Drop for KvStoreGuard {
515 fn drop(&mut self) {
516 let previous = self.previous.take().map(|b| *b);
517 CURRENT_KV_RESOLVER.with(|cell| {
518 cell.replace(previous);
519 });
520 }
521}
522
523pub(crate) fn update_current_kv_value(path: &str, value: Option<String>) {
526 let key = path.to_ascii_lowercase();
527 CURRENT_KV_RESOLVER.with(|cell| {
528 if let Some(resolver) = cell.borrow_mut().as_mut() {
529 let Some(values) = resolver.values.as_mut() else {
530 return;
531 };
532 match value {
533 Some(value) => {
534 values.insert(key, value);
535 }
536 None => {
537 values.remove(&key);
538 }
539 }
540 }
541 });
542}
543
544pub(crate) fn current_config_value(path: &str) -> Option<Value> {
545 try_current_config_value(path).ok().flatten()
546}
547
548pub(crate) fn try_current_config_value(path: &str) -> RedDBResult<Option<Value>> {
549 let key = path.to_ascii_lowercase();
550 CURRENT_CONFIG_RESOLVER.with(|cell| {
551 let mut resolver = cell.borrow_mut();
552 let Some(resolver) = resolver.as_mut() else {
553 return Ok(None);
554 };
555 if !resolver.can_read(&key) {
556 return Ok(None);
557 }
558 if resolver.values.is_none() {
559 resolver.values = Some(latest_config_snapshot(&resolver.db)?);
560 }
561 let found = {
567 let values = resolver
568 .values
569 .as_ref()
570 .expect("invariant: successful snapshot is installed");
571 values
572 .get(&key)
573 .map(|value| (key.clone(), value.clone()))
574 .or_else(|| {
575 key.strip_prefix("red.config/").and_then(|rest| {
576 values
577 .get(rest)
578 .map(|value| (rest.to_string(), value.clone()))
579 .or_else(|| {
580 let legacy = format!("red.config.{rest}");
581 values.get(&legacy).map(|value| (legacy, value.clone()))
582 })
583 })
584 })
585 };
586 let Some(found) = found else { return Ok(None) };
587 if !resolver.can_read(&found.0) {
591 return Ok(None);
592 }
593 Ok(Some(found.1))
594 })
595}
596
597pub(crate) fn update_current_config_value(path: &str, value: Value) {
598 let key = path.to_ascii_lowercase();
599 CURRENT_CONFIG_RESOLVER.with(|cell| {
600 if let Some(resolver) = cell.borrow_mut().as_mut() {
601 if let Some(values) = resolver.values.as_mut() {
602 values.insert(key, value);
603 }
604 }
605 });
606}
607
608pub(crate) fn update_current_secret_value(_path: &str, _value: Option<String>) {
609 CURRENT_SECRET_RESOLVER.with(|cell| {
610 if let Some(resolver) = cell.borrow_mut().as_mut() {
611 resolver.values.clear();
612 }
613 });
614}
615
616#[cfg(test)]
617mod tests {
618 use super::*;
619
620 #[test]
627 fn snapshot_thread_local_stays_private_to_the_statement_frame() {
628 let src = include_str!("execution_context.rs");
629
630 for name in ["CURRENT_SNAPSHOT", "HAS_SNAPSHOT"] {
631 assert!(
632 !src.contains(&format!("pub static {name}"))
633 && !src.contains(&format!("pub(crate) static {name}")),
634 "{name} must stay private to execution_context; expose an explicit \
635 Option<SnapshotContext> via capture_current_snapshot instead"
636 );
637 }
638
639 assert!(
640 src.contains("the only sanctioned ambient read"),
641 "capture_current_snapshot must keep documenting that it is the single \
642 sanctioned ambient read of the snapshot thread-local"
643 );
644 }
645
646 #[test]
647 fn secret_snapshot_hard_blocks_system_keys() {
648 let values = HashMap::from([
649 ("red.secret.aes_key".into(), "internal".into()),
650 ("acme.key".into(), "user-value".into()),
651 ]);
652 assert_eq!(
653 secret_value_from_snapshot(&values, "red.secret.aes_key"),
654 None
655 );
656 assert_eq!(
657 secret_value_from_snapshot(&values, "acme.key").as_deref(),
658 Some("user-value")
659 );
660 }
661
662 fn with_kv_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
663 CURRENT_KV_RESOLVER.with(|cell| {
664 cell.replace(Some(KvResolver {
665 store: None,
666 values: Some(values),
667 identity: None,
668 }));
669 });
670 let result = f();
671 CURRENT_KV_RESOLVER.with(|cell| {
672 cell.replace(None);
673 });
674 result
675 }
676
677 #[test]
678 fn current_kv_value_resolves_namespaced_and_bare_keys() {
679 let values = HashMap::from([("acme.key".to_string(), "plain-value".to_string())]);
680
681 with_kv_values(values, || {
682 assert_eq!(
684 current_kv_value("red.kv/acme.key").as_deref(),
685 Some("plain-value")
686 );
687 assert_eq!(current_kv_value("acme.key").as_deref(), Some("plain-value"));
689 assert_eq!(current_kv_value("red.kv/missing.key"), None);
691 });
692 }
693
694 #[test]
695 fn config_key_read_allowed_hard_blocks_reserved_namespace() {
696 assert!(!config_key_read_allowed(
699 None,
700 None,
701 "red.config/red.config.aes_key"
702 ));
703 assert!(!config_key_read_allowed(None, None, "red.config.aes_key"));
704 assert!(config_key_read_allowed(
706 None,
707 None,
708 "red.config/ai.openrouter.default.key"
709 ));
710 assert!(config_key_read_allowed(None, None, "acme.flags.beta"));
711 }
712
713 #[test]
714 fn is_config_collection_matches_system_config_stores() {
715 assert!(is_config_collection("red_config"));
716 assert!(is_config_collection("RED_CONFIG"));
717 assert!(is_config_collection("red.config"));
718 assert!(!is_config_collection("users"));
719 assert!(!is_config_collection("red_kv"));
720 }
721
722 #[test]
723 fn update_current_kv_value_reflects_in_resolver() {
724 with_kv_values(HashMap::new(), || {
725 assert_eq!(current_kv_value("red.kv/feature.flag"), None);
726 update_current_kv_value("feature.flag", Some("on".to_string()));
727 assert_eq!(
728 current_kv_value("red.kv/feature.flag").as_deref(),
729 Some("on")
730 );
731 update_current_kv_value("feature.flag", None);
732 assert_eq!(current_kv_value("red.kv/feature.flag"), None);
733 });
734 }
735}
736
737fn latest_config_snapshot(db: &RedDB) -> RedDBResult<HashMap<String, Value>> {
738 let mut latest: HashMap<String, (u64, Value)> = HashMap::new();
739
740 if let Some(manager) = db.store().get_collection("red_config") {
741 super::function_budget::try_scan(
742 |visit| manager.try_for_each_entity(usize::MAX, visit),
743 |entity| {
744 let Some(row) = entity.data.as_row() else {
745 return true;
746 };
747 let Some(Value::Text(key)) = row.get_field("key") else {
748 return true;
749 };
750 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
751 let id = entity.id.raw();
752 let key = key.to_ascii_lowercase();
753 insert_latest_config_value(&mut latest, key.clone(), id, value.clone());
754 if let Some(rest) = key.strip_prefix("red.config.") {
755 insert_latest_config_value(
756 &mut latest,
757 format!("red.config/{rest}"),
758 id,
759 value,
760 );
761 }
762 true
763 },
764 )?;
765 }
766
767 if let Some(manager) = db.store().get_collection("red.config") {
768 super::function_budget::try_scan(
769 |visit| manager.try_for_each_entity(usize::MAX, visit),
770 |entity| {
771 let Some(row) = entity.data.as_row() else {
772 return true;
773 };
774 if matches!(row.get_field("tombstone"), Some(Value::Boolean(true))) {
775 return true;
776 }
777 let Some(Value::Text(key)) = row.get_field("key") else {
778 return true;
779 };
780 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
781 insert_latest_config_value(
782 &mut latest,
783 format!("red.config/{}", key.to_ascii_lowercase()),
784 entity.id.raw(),
785 value,
786 );
787 true
788 },
789 )?;
790 }
791
792 Ok(latest
793 .into_iter()
794 .map(|(key, (_, value))| (key, value))
795 .collect())
796}
797
798fn insert_latest_config_value(
799 latest: &mut HashMap<String, (u64, Value)>,
800 key: String,
801 id: u64,
802 value: Value,
803) {
804 match latest.get(&key) {
805 Some((prev_id, _)) if *prev_id > id => {}
806 _ => {
807 latest.insert(key, (id, value));
808 }
809 }
810}
811
812struct ConfigResolver {
813 db: Arc<RedDB>,
814 store: Option<Arc<crate::auth::store::AuthStore>>,
815 identity: Option<(String, crate::auth::Role, Option<String>)>,
816 values: Option<HashMap<String, Value>>,
817}
818
819impl ConfigResolver {
820 fn can_read(&self, key: &str) -> bool {
821 config_key_read_allowed(self.store.as_ref(), self.identity.as_ref(), key)
822 }
823}
824
825fn is_reserved_config_key(key: &str) -> bool {
829 let key = key.strip_prefix("red.config/").unwrap_or(key);
830 key.starts_with("red.config.")
831 || key == "red.config"
832 || key == super::function_catalog::REGISTRY_KEY
833}
834
835fn is_config_collection(collection: &str) -> bool {
839 matches!(
840 collection.to_ascii_lowercase().as_str(),
841 "red_config" | "red.config"
842 )
843}
844
845fn config_resource_target(key: &str) -> String {
846 let bare = key.strip_prefix("red.config/").unwrap_or(key);
847 format!("red.config/{bare}")
848}
849
850fn config_key_read_allowed(
856 store: Option<&Arc<crate::auth::store::AuthStore>>,
857 identity: Option<&(String, crate::auth::Role, Option<String>)>,
858 key: &str,
859) -> bool {
860 if is_reserved_config_key(key) {
861 return false;
862 }
863 let Some(store) = store else {
864 return true;
865 };
866 if !store.iam_authorization_enabled() {
867 return true;
868 }
869 let Some((username, role, tenant)) = identity else {
870 return true;
871 };
872 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
873 let mut resource =
874 crate::auth::policies::ResourceRef::new("config".to_string(), config_resource_target(key));
875 if let Some(tenant) = tenant {
876 resource = resource.with_tenant(tenant.clone());
877 }
878 let ctx = crate::auth::policies::EvalContext {
879 principal_tenant: tenant.clone(),
880 current_tenant: tenant.clone(),
881 peer_ip: None,
882 mfa_present: false,
883 now_ms: crate::auth::now_ms(),
884 principal_is_admin_role: *role == crate::auth::Role::Admin,
885 principal_is_platform_scoped: tenant.is_none(),
886 };
887 store.check_policy_authz_with_role(&principal, "config:read", &resource, &ctx, *role)
888}
889
890pub(crate) fn config_read_permitted(key: &str) -> bool {
894 let key = key.to_ascii_lowercase();
895 CURRENT_CONFIG_RESOLVER.with(|cell| match cell.borrow().as_ref() {
896 Some(resolver) => resolver.can_read(&key),
897 None => !is_reserved_config_key(&key),
898 })
899}
900
901pub(crate) fn kv_read_permitted(collection: &str, key: &str) -> bool {
904 if is_config_collection(collection) {
905 config_read_permitted(key)
906 } else {
907 true
908 }
909}
910
911pub(crate) struct ConfigSnapshotGuard {
912 previous: Option<ConfigResolver>,
913}
914
915impl ConfigSnapshotGuard {
916 pub(super) fn install(
917 db: Arc<RedDB>,
918 store: Option<Arc<crate::auth::store::AuthStore>>,
919 ) -> Self {
920 let identity = current_auth_identity().map(|(username, role)| {
921 let tenant = current_tenant();
922 (username, role, tenant)
923 });
924 let previous = CURRENT_CONFIG_RESOLVER.with(|cell| {
925 cell.replace(Some(ConfigResolver {
926 db,
927 store,
928 identity,
929 values: None,
930 }))
931 });
932 Self { previous }
933 }
934}
935
936impl Drop for ConfigSnapshotGuard {
937 fn drop(&mut self) {
938 let previous = self.previous.take();
939 CURRENT_CONFIG_RESOLVER.with(|cell| {
940 cell.replace(previous);
941 });
942 }
943}
944
945pub fn set_current_snapshot(ctx: SnapshotContext) {
950 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = Some(ctx));
951 HAS_SNAPSHOT.with(|c| c.set(true));
952}
953
954pub fn clear_current_snapshot() {
955 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = None);
956 HAS_SNAPSHOT.with(|c| c.set(false));
957}
958
959pub(crate) struct CurrentSnapshotGuard {
965 previous: Option<SnapshotContext>,
966}
967
968impl CurrentSnapshotGuard {
969 pub(crate) fn install(ctx: SnapshotContext) -> Self {
970 let previous = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
971 set_current_snapshot(ctx);
972 Self { previous }
973 }
974}
975
976impl Drop for CurrentSnapshotGuard {
977 fn drop(&mut self) {
978 let prev = self.previous.take();
979 let has = prev.is_some();
980 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = prev);
981 HAS_SNAPSHOT.with(|c| c.set(has));
982 }
983}
984
985#[inline]
996pub fn entity_visible_under_current_snapshot(
997 entity: &crate::storage::unified::entity::UnifiedEntity,
998) -> bool {
999 if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
1006 return false;
1007 }
1008 if !HAS_SNAPSHOT.with(|c| c.get()) {
1014 return entity.xmax == 0;
1015 }
1016 CURRENT_SNAPSHOT.with(|cell| {
1017 let guard = cell.borrow();
1018 let Some(ctx) = guard.as_ref() else {
1019 return true;
1020 };
1021 let visible = visibility_check(ctx, entity.xmin, entity.xmax);
1022 if visible {
1023 record_serializable_read(ctx, entity);
1024 }
1025 visible
1026 })
1027}
1028
1029#[inline]
1034pub(crate) fn xids_visible_under_current_snapshot(xmin: u64, xmax: u64) -> bool {
1035 if !HAS_SNAPSHOT.with(|c| c.get()) {
1036 return true;
1037 }
1038 CURRENT_SNAPSHOT.with(|cell| {
1039 let guard = cell.borrow();
1040 let Some(ctx) = guard.as_ref() else {
1041 return true;
1042 };
1043 visibility_check(ctx, xmin, xmax)
1044 })
1045}
1046
1047pub fn capture_current_snapshot() -> Option<SnapshotContext> {
1072 CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone())
1073}
1074
1075pub(crate) fn current_snapshot_requires_index_fallback() -> bool {
1080 if !HAS_SNAPSHOT.with(|c| c.get()) {
1081 return false;
1082 }
1083 CURRENT_SNAPSHOT.with(|cell| {
1084 cell.borrow()
1085 .as_ref()
1086 .is_some_and(|ctx| ctx.requires_index_fallback)
1087 })
1088}
1089
1090#[derive(Clone, Default)]
1105pub struct SnapshotBundle {
1106 pub snapshot: Option<SnapshotContext>,
1107 pub auth: Option<(String, crate::auth::Role)>,
1108 pub tenant: Option<String>,
1109}
1110
1111pub fn snapshot_bundle() -> SnapshotBundle {
1114 SnapshotBundle {
1115 snapshot: capture_current_snapshot(),
1116 auth: current_auth_identity(),
1117 tenant: CURRENT_TENANT_ID.with(|cell| cell.borrow().clone()),
1118 }
1119}
1120
1121pub fn with_snapshot_bundle<R>(bundle: &SnapshotBundle, f: impl FnOnce() -> R) -> R {
1126 struct Guard {
1127 prev_snapshot: Option<SnapshotContext>,
1128 prev_auth: Option<(String, crate::auth::Role)>,
1129 prev_tenant: Option<String>,
1130 }
1131 impl Drop for Guard {
1132 fn drop(&mut self) {
1133 let snap = self.prev_snapshot.take();
1134 let has = snap.is_some();
1135 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = snap);
1136 HAS_SNAPSHOT.with(|c| c.set(has));
1137 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = self.prev_auth.take());
1138 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = self.prev_tenant.take());
1139 }
1140 }
1141
1142 let _guard = {
1143 let prev_snapshot = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
1144 let prev_auth = CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone());
1145 let prev_tenant = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
1146
1147 match bundle.snapshot.clone() {
1148 Some(ctx) => set_current_snapshot(ctx),
1149 None => clear_current_snapshot(),
1150 }
1151 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = bundle.auth.clone());
1152 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = bundle.tenant.clone());
1153
1154 Guard {
1155 prev_snapshot,
1156 prev_auth,
1157 prev_tenant,
1158 }
1159 };
1160 f()
1161}
1162
1163#[inline]
1167pub fn entity_visible_with_context(
1168 ctx: Option<&SnapshotContext>,
1169 entity: &crate::storage::unified::entity::UnifiedEntity,
1170) -> bool {
1171 reddb_engine::storage::visibility::entity_visible_with_context(ctx, entity)
1172}