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_AUTH_PRINCIPAL: std::cell::RefCell<Option<crate::auth::UserId>> =
32 const { std::cell::RefCell::new(None) };
33
34 static CURRENT_SNAPSHOT: std::cell::RefCell<Option<SnapshotContext>> =
44 const { std::cell::RefCell::new(None) };
45
46 static HAS_SNAPSHOT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
52
53 static CURRENT_TENANT_ID: std::cell::RefCell<Option<String>> =
63 const { std::cell::RefCell::new(None) };
64
65 static CURRENT_CONFIG_RESOLVER: std::cell::RefCell<Option<ConfigResolver>> =
69 const { std::cell::RefCell::new(None) };
70
71 static CURRENT_SECRET_RESOLVER: std::cell::RefCell<Option<SecretResolver>> =
75 const { std::cell::RefCell::new(None) };
76
77 static CURRENT_KV_RESOLVER: std::cell::RefCell<Option<KvResolver>> =
81 const { std::cell::RefCell::new(None) };
82}
83
84pub use reddb_engine::transaction::read::SnapshotContext;
85use reddb_engine::transaction::read::{record_serializable_read, visibility_check};
86
87pub fn set_current_connection_id(id: u64) {
96 CURRENT_CONN_ID.with(|c| c.set(id));
97}
98
99pub fn clear_current_connection_id() {
101 CURRENT_CONN_ID.with(|c| c.set(0));
102}
103
104pub fn current_connection_id() -> u64 {
107 CURRENT_CONN_ID.with(|c| c.get())
108}
109
110pub fn set_current_auth_identity(username: String, role: crate::auth::Role) {
114 set_current_auth_principal(crate::auth::UserId::platform(username), role);
115}
116
117pub fn set_current_auth_principal(principal: crate::auth::UserId, role: crate::auth::Role) {
119 CURRENT_AUTH_IDENTITY.with(|cell| {
120 *cell.borrow_mut() = Some((principal.username.clone(), role));
121 });
122 CURRENT_AUTH_PRINCIPAL.with(|cell| *cell.borrow_mut() = Some(principal));
123}
124
125pub(crate) fn set_current_auth_projection(username: String, role: crate::auth::Role) {
128 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = Some((username, role)));
129}
130
131pub fn clear_current_auth_identity() {
135 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = None);
136 CURRENT_AUTH_PRINCIPAL.with(|cell| *cell.borrow_mut() = None);
137}
138
139pub(crate) fn current_auth_identity() -> Option<(String, crate::auth::Role)> {
142 CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone())
143}
144
145pub(crate) fn current_auth_principal() -> Option<crate::auth::UserId> {
147 CURRENT_AUTH_PRINCIPAL.with(|cell| cell.borrow().clone())
148}
149
150pub fn current_auth_identity_for_audit() -> Option<(String, crate::auth::Role)> {
154 current_auth_identity()
155}
156
157pub fn set_current_tenant(tenant_id: String) {
162 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = Some(tenant_id));
163}
164
165pub fn clear_current_tenant() {
168 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = None);
169}
170
171pub fn current_tenant() -> Option<String> {
182 let inherited = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
183 if let Some(over) = current_scope_override() {
184 if over.tenant.is_active() {
185 return over.tenant.resolve(inherited);
186 }
187 }
188 if let Some(tx_local) = current_tx_local_tenant() {
189 return tx_local;
190 }
191 inherited
192}
193
194thread_local! {
195 static TX_LOCAL_TENANT: std::cell::RefCell<Option<Option<String>>> =
204 const { std::cell::RefCell::new(None) };
205}
206
207fn current_tx_local_tenant() -> Option<Option<String>> {
208 TX_LOCAL_TENANT.with(|cell| cell.borrow().clone())
209}
210
211pub(crate) fn parse_set_local_tenant(query: &str) -> RedDBResult<Option<Option<String>>> {
217 let mut tokens = query.split_ascii_whitespace();
218 let Some(w1) = tokens.next() else {
219 return Ok(None);
220 };
221 if !w1.eq_ignore_ascii_case("SET") {
222 return Ok(None);
223 }
224 let Some(w2) = tokens.next() else {
225 return Ok(None);
226 };
227 if !w2.eq_ignore_ascii_case("LOCAL") {
228 return Ok(None);
229 }
230 let Some(w3) = tokens.next() else {
231 return Ok(None);
232 };
233 if !w3.eq_ignore_ascii_case("TENANT") {
234 return Ok(None);
235 }
236 let rest: String = tokens.collect::<Vec<_>>().join(" ");
237 let rest = rest.trim().trim_end_matches(';').trim();
238 let value_str = rest.strip_prefix('=').map(|s| s.trim()).unwrap_or(rest);
239 if value_str.is_empty() {
240 return Err(RedDBError::Query(
241 "SET LOCAL TENANT expects a string literal or NULL".to_string(),
242 ));
243 }
244 if value_str.eq_ignore_ascii_case("NULL") {
245 return Ok(Some(None));
246 }
247 if value_str.starts_with('\'') && value_str.ends_with('\'') && value_str.len() >= 2 {
248 let inner = &value_str[1..value_str.len() - 1];
249 return Ok(Some(Some(inner.to_string())));
250 }
251 Err(RedDBError::Query(format!(
252 "SET LOCAL TENANT expects a string literal or NULL, got `{value_str}`"
253 )))
254}
255
256pub(crate) struct TxLocalTenantGuard;
257
258impl TxLocalTenantGuard {
259 pub fn install(value: Option<Option<String>>) -> Self {
260 TX_LOCAL_TENANT.with(|cell| *cell.borrow_mut() = value);
261 Self
262 }
263}
264
265impl Drop for TxLocalTenantGuard {
266 fn drop(&mut self) {
267 TX_LOCAL_TENANT.with(|cell| *cell.borrow_mut() = None);
268 }
269}
270
271thread_local! {
272 static SCOPE_OVERRIDES: std::cell::RefCell<Vec<crate::runtime::within_clause::ScopeOverride>> =
279 const { std::cell::RefCell::new(Vec::new()) };
280}
281
282pub(crate) fn push_scope_override(over: crate::runtime::within_clause::ScopeOverride) {
283 SCOPE_OVERRIDES.with(|cell| cell.borrow_mut().push(over));
284}
285
286pub(crate) fn pop_scope_override() {
287 SCOPE_OVERRIDES.with(|cell| {
288 cell.borrow_mut().pop();
289 });
290}
291
292pub(crate) fn current_scope_override() -> Option<crate::runtime::within_clause::ScopeOverride> {
293 SCOPE_OVERRIDES.with(|cell| cell.borrow().last().cloned())
294}
295
296pub(crate) fn has_scope_override_active() -> bool {
300 SCOPE_OVERRIDES.with(|cell| !cell.borrow().is_empty())
301}
302
303pub(crate) struct ScopeOverrideGuard;
307
308impl ScopeOverrideGuard {
309 pub fn install(over: crate::runtime::within_clause::ScopeOverride) -> Self {
310 push_scope_override(over);
311 Self
312 }
313}
314
315impl Drop for ScopeOverrideGuard {
316 fn drop(&mut self) {
317 pop_scope_override();
318 }
319}
320
321pub(crate) fn current_user_projected() -> Option<String> {
327 let inherited = current_auth_identity().map(|(u, _)| u);
328 if let Some(over) = current_scope_override() {
329 if over.user.is_active() {
330 return over.user.resolve(inherited);
331 }
332 }
333 inherited
334}
335
336pub(crate) fn current_role_projected() -> Option<String> {
337 let inherited = current_auth_identity().map(|(_, r)| format!("{r:?}").to_lowercase());
338 if let Some(over) = current_scope_override() {
339 if over.role.is_active() {
340 return over.role.resolve(inherited);
341 }
342 }
343 inherited
344}
345
346pub(crate) fn current_secret_value(path: &str) -> Option<Value> {
347 try_current_secret_value(path).ok().flatten()
348}
349
350pub(crate) fn try_current_secret_value(path: &str) -> RedDBResult<Option<Value>> {
351 let key = path.to_ascii_lowercase();
352 CURRENT_SECRET_RESOLVER.with(|cell| {
353 let mut resolver = cell.borrow_mut();
354 let Some(resolver) = resolver.as_mut() else {
355 return Ok(None);
356 };
357 if let Some(value) = resolver.values.get(&key) {
358 return Ok(value.clone());
359 }
360 let value = resolver.runtime.query_secret_value(&key)?;
361 resolver.values.insert(key, value.clone());
362 Ok(value)
363 })
364}
365
366pub(crate) fn secret_query_input(value: Value) -> Option<Value> {
369 if !matches!(value, Value::Secret(_)) {
370 return Some(value);
371 }
372 CURRENT_SECRET_RESOLVER.with(|cell| {
373 let resolver = cell.borrow();
374 resolver.as_ref()?.runtime.secret_query_input(value)
375 })
376}
377
378pub(crate) fn try_secret_query_input(
379 value: Value,
380) -> Result<Option<Value>, super::impl_dml_crypto::SecretInputReadLimit> {
381 if !matches!(value, Value::Secret(_)) {
382 return Ok(Some(value));
383 }
384 CURRENT_SECRET_RESOLVER.with(|cell| {
385 let resolver = cell.borrow();
386 match resolver.as_ref() {
387 Some(resolver) => resolver.runtime.try_secret_query_input(value),
388 None => Ok(None),
389 }
390 })
391}
392
393pub(crate) fn secret_query_output(value: Value) -> crate::RedDBResult<Value> {
394 if value.is_null() || matches!(value, Value::Secret(_)) {
395 return Ok(value);
396 }
397 CURRENT_SECRET_RESOLVER.with(|cell| {
398 let resolver = cell.borrow();
399 let resolver = resolver.as_ref().ok_or_else(|| {
400 RedDBError::Query("secret expression requires an active statement".into())
401 })?;
402 resolver.runtime.secret_query_output(value)
403 })
404}
405
406#[cfg(test)]
407fn secret_value_from_snapshot(values: &HashMap<String, String>, key: &str) -> Option<String> {
408 if key.starts_with("red.secret.") {
409 return None;
410 }
411 values.get(key).cloned()
412}
413
414struct SecretResolver {
415 runtime: super::RedDBRuntime,
416 values: HashMap<String, Option<Value>>,
417}
418
419pub(crate) struct SecretStoreGuard {
420 previous: Option<SecretResolver>,
421}
422
423impl SecretStoreGuard {
424 pub(super) fn install(runtime: &super::RedDBRuntime) -> Self {
425 let previous = CURRENT_SECRET_RESOLVER.with(|cell| {
426 cell.replace(Some(SecretResolver {
427 runtime: runtime.clone(),
428 values: HashMap::new(),
429 }))
430 });
431 Self { previous }
432 }
433}
434
435impl Drop for SecretStoreGuard {
436 fn drop(&mut self) {
437 let previous = self.previous.take();
438 CURRENT_SECRET_RESOLVER.with(|cell| {
439 cell.replace(previous);
440 });
441 }
442}
443
444pub(crate) fn current_kv_value(path: &str) -> Option<String> {
452 let key = path.to_ascii_lowercase();
453 CURRENT_KV_RESOLVER.with(|cell| {
454 let mut resolver = cell.borrow_mut();
455 let resolver = resolver.as_mut()?;
456 if resolver.values.is_none() {
457 resolver.values = resolver
458 .store
459 .as_ref()
460 .map(|store| store.plain_kv_snapshot());
461 }
462 let values = resolver.values.as_ref()?;
463 let found = values
467 .get(&key)
468 .map(|value| (key.as_str(), value))
469 .or_else(|| {
470 key.strip_prefix("red.kv/")
471 .and_then(|rest| values.get(rest).map(|value| (rest, value)))
472 })?;
473 if !resolver.can_read(found.0) {
474 return None;
475 }
476 Some(found.1.clone())
477 })
478}
479
480struct KvResolver {
481 store: Option<Arc<crate::auth::store::AuthStore>>,
482 values: Option<HashMap<String, String>>,
483 identity: Option<(String, crate::auth::Role, Option<String>)>,
484}
485
486impl KvResolver {
487 fn can_read(&self, key: &str) -> bool {
488 let Some(store) = &self.store else {
489 return true;
490 };
491 let Some((username, role, tenant)) = &self.identity else {
492 return true;
493 };
494 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
495 let mut resource =
496 crate::auth::policies::ResourceRef::new("kv".to_string(), key.to_string());
497 if let Some(tenant) = tenant {
498 resource = resource.with_tenant(tenant.clone());
499 }
500 let ctx = crate::auth::policies::EvalContext {
501 principal_tenant: tenant.clone(),
502 current_tenant: tenant.clone(),
503 peer_ip: None,
504 mfa_present: false,
505 now_ms: crate::auth::now_ms(),
506 principal_is_admin_role: *role == crate::auth::Role::Admin,
507 principal_is_platform_scoped: tenant.is_none(),
508 };
509 store.check_policy_authz_with_role(&principal, "kv:read", &resource, &ctx, *role)
510 }
511}
512
513pub(crate) struct KvStoreGuard {
514 previous: Option<Box<KvResolver>>,
519}
520
521impl KvStoreGuard {
522 pub(super) fn install(store: Option<Arc<crate::auth::store::AuthStore>>) -> Self {
523 let previous = CURRENT_KV_RESOLVER.with(|cell| {
524 cell.replace(Some(KvResolver {
525 store,
526 values: None,
527 identity: current_auth_identity().map(|(username, role)| {
528 let tenant = current_tenant();
529 (username, role, tenant)
530 }),
531 }))
532 });
533 Self {
534 previous: previous.map(Box::new),
535 }
536 }
537}
538
539impl Drop for KvStoreGuard {
540 fn drop(&mut self) {
541 let previous = self.previous.take().map(|b| *b);
542 CURRENT_KV_RESOLVER.with(|cell| {
543 cell.replace(previous);
544 });
545 }
546}
547
548pub(crate) fn update_current_kv_value(path: &str, value: Option<String>) {
551 let key = path.to_ascii_lowercase();
552 CURRENT_KV_RESOLVER.with(|cell| {
553 if let Some(resolver) = cell.borrow_mut().as_mut() {
554 let Some(values) = resolver.values.as_mut() else {
555 return;
556 };
557 match value {
558 Some(value) => {
559 values.insert(key, value);
560 }
561 None => {
562 values.remove(&key);
563 }
564 }
565 }
566 });
567}
568
569pub(crate) fn current_config_value(path: &str) -> Option<Value> {
570 try_current_config_value(path).ok().flatten()
571}
572
573pub(crate) fn try_current_config_value(path: &str) -> RedDBResult<Option<Value>> {
574 let key = path.to_ascii_lowercase();
575 CURRENT_CONFIG_RESOLVER.with(|cell| {
576 let mut resolver = cell.borrow_mut();
577 let Some(resolver) = resolver.as_mut() else {
578 return Ok(None);
579 };
580 if !resolver.can_read(&key) {
581 return Ok(None);
582 }
583 if resolver.values.is_none() {
584 resolver.values = Some(latest_config_snapshot(&resolver.db)?);
585 }
586 let found = {
592 let values = resolver
593 .values
594 .as_ref()
595 .expect("invariant: successful snapshot is installed");
596 values
597 .get(&key)
598 .map(|value| (key.clone(), value.clone()))
599 .or_else(|| {
600 key.strip_prefix("red.config/").and_then(|rest| {
601 values
602 .get(rest)
603 .map(|value| (rest.to_string(), value.clone()))
604 .or_else(|| {
605 let legacy = format!("red.config.{rest}");
606 values.get(&legacy).map(|value| (legacy, value.clone()))
607 })
608 })
609 })
610 };
611 let Some(found) = found else { return Ok(None) };
612 if !resolver.can_read(&found.0) {
616 return Ok(None);
617 }
618 Ok(Some(found.1))
619 })
620}
621
622pub(crate) fn update_current_config_value(path: &str, value: Value) {
623 let key = path.to_ascii_lowercase();
624 CURRENT_CONFIG_RESOLVER.with(|cell| {
625 if let Some(resolver) = cell.borrow_mut().as_mut() {
626 if let Some(values) = resolver.values.as_mut() {
627 values.insert(key, value);
628 }
629 }
630 });
631}
632
633pub(crate) fn update_current_secret_value(_path: &str, _value: Option<String>) {
634 CURRENT_SECRET_RESOLVER.with(|cell| {
635 if let Some(resolver) = cell.borrow_mut().as_mut() {
636 resolver.values.clear();
637 }
638 });
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644
645 #[test]
652 fn snapshot_thread_local_stays_private_to_the_statement_frame() {
653 let src = include_str!("execution_context.rs");
654
655 for name in ["CURRENT_SNAPSHOT", "HAS_SNAPSHOT"] {
656 assert!(
657 !src.contains(&format!("pub static {name}"))
658 && !src.contains(&format!("pub(crate) static {name}")),
659 "{name} must stay private to execution_context; expose an explicit \
660 Option<SnapshotContext> via capture_current_snapshot instead"
661 );
662 }
663
664 assert!(
665 src.contains("the only sanctioned ambient read"),
666 "capture_current_snapshot must keep documenting that it is the single \
667 sanctioned ambient read of the snapshot thread-local"
668 );
669 }
670
671 #[test]
672 fn secret_snapshot_hard_blocks_system_keys() {
673 let values = HashMap::from([
674 ("red.secret.aes_key".into(), "internal".into()),
675 ("acme.key".into(), "user-value".into()),
676 ]);
677 assert_eq!(
678 secret_value_from_snapshot(&values, "red.secret.aes_key"),
679 None
680 );
681 assert_eq!(
682 secret_value_from_snapshot(&values, "acme.key").as_deref(),
683 Some("user-value")
684 );
685 }
686
687 fn with_kv_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
688 CURRENT_KV_RESOLVER.with(|cell| {
689 cell.replace(Some(KvResolver {
690 store: None,
691 values: Some(values),
692 identity: None,
693 }));
694 });
695 let result = f();
696 CURRENT_KV_RESOLVER.with(|cell| {
697 cell.replace(None);
698 });
699 result
700 }
701
702 #[test]
703 fn current_kv_value_resolves_namespaced_and_bare_keys() {
704 let values = HashMap::from([("acme.key".to_string(), "plain-value".to_string())]);
705
706 with_kv_values(values, || {
707 assert_eq!(
709 current_kv_value("red.kv/acme.key").as_deref(),
710 Some("plain-value")
711 );
712 assert_eq!(current_kv_value("acme.key").as_deref(), Some("plain-value"));
714 assert_eq!(current_kv_value("red.kv/missing.key"), None);
716 });
717 }
718
719 #[test]
720 fn config_key_read_allowed_hard_blocks_reserved_namespace() {
721 assert!(!config_key_read_allowed(
724 None,
725 None,
726 "red.config/red.config.aes_key"
727 ));
728 assert!(!config_key_read_allowed(None, None, "red.config.aes_key"));
729 assert!(config_key_read_allowed(
731 None,
732 None,
733 "red.config/ai.openrouter.default.key"
734 ));
735 assert!(config_key_read_allowed(None, None, "acme.flags.beta"));
736 }
737
738 #[test]
739 fn is_config_collection_matches_system_config_stores() {
740 assert!(is_config_collection("red_config"));
741 assert!(is_config_collection("RED_CONFIG"));
742 assert!(is_config_collection("red.config"));
743 assert!(!is_config_collection("users"));
744 assert!(!is_config_collection("red_kv"));
745 }
746
747 #[test]
748 fn update_current_kv_value_reflects_in_resolver() {
749 with_kv_values(HashMap::new(), || {
750 assert_eq!(current_kv_value("red.kv/feature.flag"), None);
751 update_current_kv_value("feature.flag", Some("on".to_string()));
752 assert_eq!(
753 current_kv_value("red.kv/feature.flag").as_deref(),
754 Some("on")
755 );
756 update_current_kv_value("feature.flag", None);
757 assert_eq!(current_kv_value("red.kv/feature.flag"), None);
758 });
759 }
760}
761
762fn latest_config_snapshot(db: &RedDB) -> RedDBResult<HashMap<String, Value>> {
763 let mut latest: HashMap<String, (u64, Value)> = HashMap::new();
764
765 if let Some(manager) = db.store().get_collection("red_config") {
766 super::function_budget::try_scan(
767 |visit| manager.try_for_each_entity(usize::MAX, visit),
768 |entity| {
769 let Some(row) = entity.data.as_row() else {
770 return true;
771 };
772 let Some(Value::Text(key)) = row.get_field("key") else {
773 return true;
774 };
775 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
776 let id = entity.id.raw();
777 let key = key.to_ascii_lowercase();
778 insert_latest_config_value(&mut latest, key.clone(), id, value.clone());
779 if let Some(rest) = key.strip_prefix("red.config.") {
780 insert_latest_config_value(
781 &mut latest,
782 format!("red.config/{rest}"),
783 id,
784 value,
785 );
786 }
787 true
788 },
789 )?;
790 }
791
792 if let Some(manager) = db.store().get_collection("red.config") {
793 super::function_budget::try_scan(
794 |visit| manager.try_for_each_entity(usize::MAX, visit),
795 |entity| {
796 let Some(row) = entity.data.as_row() else {
797 return true;
798 };
799 if matches!(row.get_field("tombstone"), Some(Value::Boolean(true))) {
800 return true;
801 }
802 let Some(Value::Text(key)) = row.get_field("key") else {
803 return true;
804 };
805 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
806 insert_latest_config_value(
807 &mut latest,
808 format!("red.config/{}", key.to_ascii_lowercase()),
809 entity.id.raw(),
810 value,
811 );
812 true
813 },
814 )?;
815 }
816
817 Ok(latest
818 .into_iter()
819 .map(|(key, (_, value))| (key, value))
820 .collect())
821}
822
823fn insert_latest_config_value(
824 latest: &mut HashMap<String, (u64, Value)>,
825 key: String,
826 id: u64,
827 value: Value,
828) {
829 match latest.get(&key) {
830 Some((prev_id, _)) if *prev_id > id => {}
831 _ => {
832 latest.insert(key, (id, value));
833 }
834 }
835}
836
837struct ConfigResolver {
838 db: Arc<RedDB>,
839 store: Option<Arc<crate::auth::store::AuthStore>>,
840 identity: Option<(String, crate::auth::Role, Option<String>)>,
841 values: Option<HashMap<String, Value>>,
842}
843
844impl ConfigResolver {
845 fn can_read(&self, key: &str) -> bool {
846 config_key_read_allowed(self.store.as_ref(), self.identity.as_ref(), key)
847 }
848}
849
850fn is_reserved_config_key(key: &str) -> bool {
854 let key = key.strip_prefix("red.config/").unwrap_or(key);
855 key.starts_with("red.config.")
856 || key == "red.config"
857 || key == super::function_catalog::REGISTRY_KEY
858}
859
860fn is_config_collection(collection: &str) -> bool {
864 matches!(
865 collection.to_ascii_lowercase().as_str(),
866 "red_config" | "red.config"
867 )
868}
869
870fn config_resource_target(key: &str) -> String {
871 let bare = key.strip_prefix("red.config/").unwrap_or(key);
872 format!("red.config/{bare}")
873}
874
875fn config_key_read_allowed(
881 store: Option<&Arc<crate::auth::store::AuthStore>>,
882 identity: Option<&(String, crate::auth::Role, Option<String>)>,
883 key: &str,
884) -> bool {
885 if is_reserved_config_key(key) {
886 return false;
887 }
888 let Some(store) = store else {
889 return true;
890 };
891 if !store.iam_authorization_enabled() {
892 return true;
893 }
894 let Some((username, role, tenant)) = identity else {
895 return true;
896 };
897 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
898 let mut resource =
899 crate::auth::policies::ResourceRef::new("config".to_string(), config_resource_target(key));
900 if let Some(tenant) = tenant {
901 resource = resource.with_tenant(tenant.clone());
902 }
903 let ctx = crate::auth::policies::EvalContext {
904 principal_tenant: tenant.clone(),
905 current_tenant: tenant.clone(),
906 peer_ip: None,
907 mfa_present: false,
908 now_ms: crate::auth::now_ms(),
909 principal_is_admin_role: *role == crate::auth::Role::Admin,
910 principal_is_platform_scoped: tenant.is_none(),
911 };
912 store.check_policy_authz_with_role(&principal, "config:read", &resource, &ctx, *role)
913}
914
915pub(crate) fn config_read_permitted(key: &str) -> bool {
919 let key = key.to_ascii_lowercase();
920 CURRENT_CONFIG_RESOLVER.with(|cell| match cell.borrow().as_ref() {
921 Some(resolver) => resolver.can_read(&key),
922 None => !is_reserved_config_key(&key),
923 })
924}
925
926pub(crate) fn kv_read_permitted(collection: &str, key: &str) -> bool {
929 if is_config_collection(collection) {
930 config_read_permitted(key)
931 } else {
932 true
933 }
934}
935
936pub(crate) struct ConfigSnapshotGuard {
937 previous: Option<ConfigResolver>,
938}
939
940impl ConfigSnapshotGuard {
941 pub(super) fn install(
942 db: Arc<RedDB>,
943 store: Option<Arc<crate::auth::store::AuthStore>>,
944 ) -> Self {
945 let identity = current_auth_identity().map(|(username, role)| {
946 let tenant = current_tenant();
947 (username, role, tenant)
948 });
949 let previous = CURRENT_CONFIG_RESOLVER.with(|cell| {
950 cell.replace(Some(ConfigResolver {
951 db,
952 store,
953 identity,
954 values: None,
955 }))
956 });
957 Self { previous }
958 }
959}
960
961impl Drop for ConfigSnapshotGuard {
962 fn drop(&mut self) {
963 let previous = self.previous.take();
964 CURRENT_CONFIG_RESOLVER.with(|cell| {
965 cell.replace(previous);
966 });
967 }
968}
969
970pub fn set_current_snapshot(ctx: SnapshotContext) {
975 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = Some(ctx));
976 HAS_SNAPSHOT.with(|c| c.set(true));
977}
978
979pub fn clear_current_snapshot() {
980 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = None);
981 HAS_SNAPSHOT.with(|c| c.set(false));
982}
983
984pub(crate) struct CurrentSnapshotGuard {
990 previous: Option<SnapshotContext>,
991}
992
993impl CurrentSnapshotGuard {
994 pub(crate) fn install(ctx: SnapshotContext) -> Self {
995 let previous = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
996 set_current_snapshot(ctx);
997 Self { previous }
998 }
999}
1000
1001impl Drop for CurrentSnapshotGuard {
1002 fn drop(&mut self) {
1003 let prev = self.previous.take();
1004 let has = prev.is_some();
1005 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = prev);
1006 HAS_SNAPSHOT.with(|c| c.set(has));
1007 }
1008}
1009
1010#[inline]
1021pub fn entity_visible_under_current_snapshot(
1022 entity: &crate::storage::unified::entity::UnifiedEntity,
1023) -> bool {
1024 if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
1031 return false;
1032 }
1033 if !HAS_SNAPSHOT.with(|c| c.get()) {
1039 return entity.xmax == 0;
1040 }
1041 CURRENT_SNAPSHOT.with(|cell| {
1042 let guard = cell.borrow();
1043 let Some(ctx) = guard.as_ref() else {
1044 return true;
1045 };
1046 let visible = visibility_check(ctx, entity.xmin, entity.xmax);
1047 if visible {
1048 record_serializable_read(ctx, entity);
1049 }
1050 visible
1051 })
1052}
1053
1054#[inline]
1059pub(crate) fn xids_visible_under_current_snapshot(xmin: u64, xmax: u64) -> bool {
1060 if !HAS_SNAPSHOT.with(|c| c.get()) {
1061 return true;
1062 }
1063 CURRENT_SNAPSHOT.with(|cell| {
1064 let guard = cell.borrow();
1065 let Some(ctx) = guard.as_ref() else {
1066 return true;
1067 };
1068 visibility_check(ctx, xmin, xmax)
1069 })
1070}
1071
1072pub fn capture_current_snapshot() -> Option<SnapshotContext> {
1097 CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone())
1098}
1099
1100pub(crate) fn current_snapshot_requires_index_fallback() -> bool {
1105 if !HAS_SNAPSHOT.with(|c| c.get()) {
1106 return false;
1107 }
1108 CURRENT_SNAPSHOT.with(|cell| {
1109 cell.borrow()
1110 .as_ref()
1111 .is_some_and(|ctx| ctx.requires_index_fallback)
1112 })
1113}
1114
1115#[derive(Clone, Default)]
1130pub struct SnapshotBundle {
1131 pub snapshot: Option<SnapshotContext>,
1132 pub auth: Option<(String, crate::auth::Role)>,
1133 pub auth_principal: Option<crate::auth::UserId>,
1134 pub tenant: Option<String>,
1135}
1136
1137pub fn snapshot_bundle() -> SnapshotBundle {
1140 SnapshotBundle {
1141 snapshot: capture_current_snapshot(),
1142 auth: current_auth_identity(),
1143 auth_principal: current_auth_principal(),
1144 tenant: CURRENT_TENANT_ID.with(|cell| cell.borrow().clone()),
1145 }
1146}
1147
1148pub fn with_snapshot_bundle<R>(bundle: &SnapshotBundle, f: impl FnOnce() -> R) -> R {
1153 struct Guard {
1154 prev_snapshot: Option<SnapshotContext>,
1155 prev_auth: Option<(String, crate::auth::Role)>,
1156 prev_auth_principal: Option<crate::auth::UserId>,
1157 prev_tenant: Option<String>,
1158 }
1159 impl Drop for Guard {
1160 fn drop(&mut self) {
1161 let snap = self.prev_snapshot.take();
1162 let has = snap.is_some();
1163 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = snap);
1164 HAS_SNAPSHOT.with(|c| c.set(has));
1165 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = self.prev_auth.take());
1166 CURRENT_AUTH_PRINCIPAL
1167 .with(|cell| *cell.borrow_mut() = self.prev_auth_principal.take());
1168 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = self.prev_tenant.take());
1169 }
1170 }
1171
1172 let _guard = {
1173 let prev_snapshot = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
1174 let prev_auth = CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone());
1175 let prev_auth_principal = CURRENT_AUTH_PRINCIPAL.with(|cell| cell.borrow().clone());
1176 let prev_tenant = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
1177
1178 match bundle.snapshot.clone() {
1179 Some(ctx) => set_current_snapshot(ctx),
1180 None => clear_current_snapshot(),
1181 }
1182 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = bundle.auth.clone());
1183 CURRENT_AUTH_PRINCIPAL.with(|cell| *cell.borrow_mut() = bundle.auth_principal.clone());
1184 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = bundle.tenant.clone());
1185
1186 Guard {
1187 prev_snapshot,
1188 prev_auth,
1189 prev_auth_principal,
1190 prev_tenant,
1191 }
1192 };
1193 f()
1194}
1195
1196#[inline]
1200pub fn entity_visible_with_context(
1201 ctx: Option<&SnapshotContext>,
1202 entity: &crate::storage::unified::entity::UnifiedEntity,
1203) -> bool {
1204 reddb_engine::storage::visibility::entity_visible_with_context(ctx, entity)
1205}