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
79#[derive(Clone)]
94pub struct SnapshotContext {
95 pub snapshot: crate::storage::transaction::snapshot::Snapshot,
96 pub manager: Arc<crate::storage::transaction::snapshot::SnapshotManager>,
97 pub own_xids: std::collections::HashSet<crate::storage::transaction::snapshot::Xid>,
98 pub requires_index_fallback: bool,
99 pub serializable_reader: Option<crate::storage::transaction::snapshot::Xid>,
100}
101
102pub fn set_current_connection_id(id: u64) {
111 CURRENT_CONN_ID.with(|c| c.set(id));
112}
113
114pub fn clear_current_connection_id() {
116 CURRENT_CONN_ID.with(|c| c.set(0));
117}
118
119pub fn current_connection_id() -> u64 {
122 CURRENT_CONN_ID.with(|c| c.get())
123}
124
125pub fn set_current_auth_identity(username: String, role: crate::auth::Role) {
129 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = Some((username, role)));
130}
131
132pub fn clear_current_auth_identity() {
136 CURRENT_AUTH_IDENTITY.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 fn current_auth_identity_for_audit() -> Option<(String, crate::auth::Role)> {
149 current_auth_identity()
150}
151
152pub fn set_current_tenant(tenant_id: String) {
157 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = Some(tenant_id));
158}
159
160pub fn clear_current_tenant() {
163 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = None);
164}
165
166pub fn current_tenant() -> Option<String> {
177 let inherited = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
178 if let Some(over) = current_scope_override() {
179 if over.tenant.is_active() {
180 return over.tenant.resolve(inherited);
181 }
182 }
183 if let Some(tx_local) = current_tx_local_tenant() {
184 return tx_local;
185 }
186 inherited
187}
188
189thread_local! {
190 static TX_LOCAL_TENANT: std::cell::RefCell<Option<Option<String>>> =
199 const { std::cell::RefCell::new(None) };
200}
201
202fn current_tx_local_tenant() -> Option<Option<String>> {
203 TX_LOCAL_TENANT.with(|cell| cell.borrow().clone())
204}
205
206pub(crate) fn parse_set_local_tenant(query: &str) -> RedDBResult<Option<Option<String>>> {
212 let mut tokens = query.split_ascii_whitespace();
213 let Some(w1) = tokens.next() else {
214 return Ok(None);
215 };
216 if !w1.eq_ignore_ascii_case("SET") {
217 return Ok(None);
218 }
219 let Some(w2) = tokens.next() else {
220 return Ok(None);
221 };
222 if !w2.eq_ignore_ascii_case("LOCAL") {
223 return Ok(None);
224 }
225 let Some(w3) = tokens.next() else {
226 return Ok(None);
227 };
228 if !w3.eq_ignore_ascii_case("TENANT") {
229 return Ok(None);
230 }
231 let rest: String = tokens.collect::<Vec<_>>().join(" ");
232 let rest = rest.trim().trim_end_matches(';').trim();
233 let value_str = rest.strip_prefix('=').map(|s| s.trim()).unwrap_or(rest);
234 if value_str.is_empty() {
235 return Err(RedDBError::Query(
236 "SET LOCAL TENANT expects a string literal or NULL".to_string(),
237 ));
238 }
239 if value_str.eq_ignore_ascii_case("NULL") {
240 return Ok(Some(None));
241 }
242 if value_str.starts_with('\'') && value_str.ends_with('\'') && value_str.len() >= 2 {
243 let inner = &value_str[1..value_str.len() - 1];
244 return Ok(Some(Some(inner.to_string())));
245 }
246 Err(RedDBError::Query(format!(
247 "SET LOCAL TENANT expects a string literal or NULL, got `{value_str}`"
248 )))
249}
250
251pub(crate) struct TxLocalTenantGuard;
252
253impl TxLocalTenantGuard {
254 pub fn install(value: Option<Option<String>>) -> Self {
255 TX_LOCAL_TENANT.with(|cell| *cell.borrow_mut() = value);
256 Self
257 }
258}
259
260impl Drop for TxLocalTenantGuard {
261 fn drop(&mut self) {
262 TX_LOCAL_TENANT.with(|cell| *cell.borrow_mut() = None);
263 }
264}
265
266thread_local! {
267 static SCOPE_OVERRIDES: std::cell::RefCell<Vec<crate::runtime::within_clause::ScopeOverride>> =
274 const { std::cell::RefCell::new(Vec::new()) };
275}
276
277pub(crate) fn push_scope_override(over: crate::runtime::within_clause::ScopeOverride) {
278 SCOPE_OVERRIDES.with(|cell| cell.borrow_mut().push(over));
279}
280
281pub(crate) fn pop_scope_override() {
282 SCOPE_OVERRIDES.with(|cell| {
283 cell.borrow_mut().pop();
284 });
285}
286
287pub(crate) fn current_scope_override() -> Option<crate::runtime::within_clause::ScopeOverride> {
288 SCOPE_OVERRIDES.with(|cell| cell.borrow().last().cloned())
289}
290
291pub(crate) fn has_scope_override_active() -> bool {
295 SCOPE_OVERRIDES.with(|cell| !cell.borrow().is_empty())
296}
297
298pub(crate) struct ScopeOverrideGuard;
302
303impl ScopeOverrideGuard {
304 pub fn install(over: crate::runtime::within_clause::ScopeOverride) -> Self {
305 push_scope_override(over);
306 Self
307 }
308}
309
310impl Drop for ScopeOverrideGuard {
311 fn drop(&mut self) {
312 pop_scope_override();
313 }
314}
315
316pub(crate) fn current_user_projected() -> Option<String> {
322 let inherited = current_auth_identity().map(|(u, _)| u);
323 if let Some(over) = current_scope_override() {
324 if over.user.is_active() {
325 return over.user.resolve(inherited);
326 }
327 }
328 inherited
329}
330
331pub(crate) fn current_role_projected() -> Option<String> {
332 let inherited = current_auth_identity().map(|(_, r)| format!("{r:?}").to_lowercase());
333 if let Some(over) = current_scope_override() {
334 if over.role.is_active() {
335 return over.role.resolve(inherited);
336 }
337 }
338 inherited
339}
340
341pub(crate) fn current_secret_value(path: &str) -> Option<String> {
342 let key = path.to_ascii_lowercase();
343 CURRENT_SECRET_RESOLVER.with(|cell| {
344 let mut resolver = cell.borrow_mut();
345 let resolver = resolver.as_mut()?;
346 if resolver.values.is_none() {
347 resolver.values = resolver
348 .store
349 .as_ref()
350 .map(|store| store.vault_kv_snapshot());
351 }
352 let values = resolver.values.as_ref()?;
353 let found = values
354 .get(&key)
355 .map(|value| (key.as_str(), value))
356 .or_else(|| {
357 key.strip_prefix("red.vault/").and_then(|rest| {
358 values.get(rest).map(|value| (rest, value)).or_else(|| {
359 let red_secret_key = format!("red.secret.{rest}");
360 values
361 .get_key_value(&red_secret_key)
362 .map(|(key, value)| (key.as_str(), value))
363 })
364 })
365 })?;
366 if !resolver.can_read(found.0) {
367 return None;
368 }
369 Some(found.1.clone())
370 })
371}
372
373fn secret_value_from_snapshot(values: &HashMap<String, String>, key: &str) -> Option<String> {
374 if key.starts_with("red.secret.") {
375 return None;
376 }
377 values.get(key).cloned()
378}
379
380struct SecretResolver {
381 store: Option<Arc<crate::auth::store::AuthStore>>,
382 values: Option<HashMap<String, String>>,
383 identity: Option<(String, crate::auth::Role, Option<String>)>,
384}
385
386impl SecretResolver {
387 fn can_read(&self, key: &str) -> bool {
388 if key.starts_with("red.secret.") {
391 return false;
392 }
393 let Some(store) = &self.store else {
394 return true;
395 };
396 let Some((username, role, tenant)) = &self.identity else {
397 return true;
398 };
399 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
400 let mut resource =
401 crate::auth::policies::ResourceRef::new("secret".to_string(), key.to_string());
402 if let Some(tenant) = tenant {
403 resource = resource.with_tenant(tenant.clone());
404 }
405 let ctx = crate::auth::policies::EvalContext {
406 principal_tenant: tenant.clone(),
407 current_tenant: tenant.clone(),
408 peer_ip: None,
409 mfa_present: false,
410 now_ms: crate::auth::now_ms(),
411 principal_is_admin_role: *role == crate::auth::Role::Admin,
412 principal_is_platform_scoped: tenant.is_none(),
413 };
414 store.check_policy_authz_with_role(&principal, "secret:read", &resource, &ctx, *role)
415 }
416}
417
418pub(crate) struct SecretStoreGuard {
419 previous: Option<SecretResolver>,
420}
421
422impl SecretStoreGuard {
423 pub(super) fn install(store: Option<Arc<crate::auth::store::AuthStore>>) -> Self {
424 let previous = CURRENT_SECRET_RESOLVER.with(|cell| {
425 cell.replace(Some(SecretResolver {
426 store,
427 values: None,
428 identity: current_auth_identity().map(|(username, role)| {
429 let tenant = current_tenant();
430 (username, role, tenant)
431 }),
432 }))
433 });
434 Self { previous }
435 }
436}
437
438impl Drop for SecretStoreGuard {
439 fn drop(&mut self) {
440 let previous = self.previous.take();
441 CURRENT_SECRET_RESOLVER.with(|cell| {
442 cell.replace(previous);
443 });
444 }
445}
446
447pub(crate) fn current_kv_value(path: &str) -> Option<String> {
455 let key = path.to_ascii_lowercase();
456 CURRENT_KV_RESOLVER.with(|cell| {
457 let mut resolver = cell.borrow_mut();
458 let resolver = resolver.as_mut()?;
459 if resolver.values.is_none() {
460 resolver.values = resolver
461 .store
462 .as_ref()
463 .map(|store| store.plain_kv_snapshot());
464 }
465 let values = resolver.values.as_ref()?;
466 let found = values
470 .get(&key)
471 .map(|value| (key.as_str(), value))
472 .or_else(|| {
473 key.strip_prefix("red.kv/")
474 .and_then(|rest| values.get(rest).map(|value| (rest, value)))
475 })?;
476 if !resolver.can_read(found.0) {
477 return None;
478 }
479 Some(found.1.clone())
480 })
481}
482
483struct KvResolver {
484 store: Option<Arc<crate::auth::store::AuthStore>>,
485 values: Option<HashMap<String, String>>,
486 identity: Option<(String, crate::auth::Role, Option<String>)>,
487}
488
489impl KvResolver {
490 fn can_read(&self, key: &str) -> bool {
491 let Some(store) = &self.store else {
492 return true;
493 };
494 let Some((username, role, tenant)) = &self.identity else {
495 return true;
496 };
497 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
498 let mut resource =
499 crate::auth::policies::ResourceRef::new("kv".to_string(), key.to_string());
500 if let Some(tenant) = tenant {
501 resource = resource.with_tenant(tenant.clone());
502 }
503 let ctx = crate::auth::policies::EvalContext {
504 principal_tenant: tenant.clone(),
505 current_tenant: tenant.clone(),
506 peer_ip: None,
507 mfa_present: false,
508 now_ms: crate::auth::now_ms(),
509 principal_is_admin_role: *role == crate::auth::Role::Admin,
510 principal_is_platform_scoped: tenant.is_none(),
511 };
512 store.check_policy_authz_with_role(&principal, "kv:read", &resource, &ctx, *role)
513 }
514}
515
516pub(crate) struct KvStoreGuard {
517 previous: Option<Box<KvResolver>>,
522}
523
524impl KvStoreGuard {
525 pub(super) fn install(store: Option<Arc<crate::auth::store::AuthStore>>) -> Self {
526 let previous = CURRENT_KV_RESOLVER.with(|cell| {
527 cell.replace(Some(KvResolver {
528 store,
529 values: None,
530 identity: current_auth_identity().map(|(username, role)| {
531 let tenant = current_tenant();
532 (username, role, tenant)
533 }),
534 }))
535 });
536 Self {
537 previous: previous.map(Box::new),
538 }
539 }
540}
541
542impl Drop for KvStoreGuard {
543 fn drop(&mut self) {
544 let previous = self.previous.take().map(|b| *b);
545 CURRENT_KV_RESOLVER.with(|cell| {
546 cell.replace(previous);
547 });
548 }
549}
550
551pub(crate) fn update_current_kv_value(path: &str, value: Option<String>) {
554 let key = path.to_ascii_lowercase();
555 CURRENT_KV_RESOLVER.with(|cell| {
556 if let Some(resolver) = cell.borrow_mut().as_mut() {
557 let Some(values) = resolver.values.as_mut() else {
558 return;
559 };
560 match value {
561 Some(value) => {
562 values.insert(key, value);
563 }
564 None => {
565 values.remove(&key);
566 }
567 }
568 }
569 });
570}
571
572pub(crate) fn current_config_value(path: &str) -> Option<Value> {
573 let key = path.to_ascii_lowercase();
574 CURRENT_CONFIG_RESOLVER.with(|cell| {
575 let mut resolver = cell.borrow_mut();
576 let resolver = resolver.as_mut()?;
577 if resolver.values.is_none() {
578 resolver.values = Some(latest_config_snapshot(&resolver.db));
579 }
580 let found = {
586 let values = resolver.values.as_ref()?;
587 values
588 .get(&key)
589 .map(|value| (key.clone(), value.clone()))
590 .or_else(|| {
591 key.strip_prefix("red.config/").and_then(|rest| {
592 values
593 .get(rest)
594 .map(|value| (rest.to_string(), value.clone()))
595 .or_else(|| {
596 let legacy = format!("red.config.{rest}");
597 values.get(&legacy).map(|value| (legacy, value.clone()))
598 })
599 })
600 })
601 }?;
602 if !resolver.can_read(&found.0) {
606 return None;
607 }
608 Some(found.1)
609 })
610}
611
612pub(crate) fn update_current_config_value(path: &str, value: Value) {
613 let key = path.to_ascii_lowercase();
614 CURRENT_CONFIG_RESOLVER.with(|cell| {
615 if let Some(resolver) = cell.borrow_mut().as_mut() {
616 if let Some(values) = resolver.values.as_mut() {
617 values.insert(key, value);
618 }
619 }
620 });
621}
622
623pub(crate) fn update_current_secret_value(path: &str, value: Option<String>) {
624 let key = path.to_ascii_lowercase();
625 CURRENT_SECRET_RESOLVER.with(|cell| {
626 if let Some(resolver) = cell.borrow_mut().as_mut() {
627 let Some(values) = resolver.values.as_mut() else {
628 return;
629 };
630 match value {
631 Some(value) => {
632 values.insert(key, value);
633 }
634 None => {
635 values.remove(&key);
636 }
637 }
638 }
639 });
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645
646 #[test]
653 fn snapshot_thread_local_stays_private_to_the_statement_frame() {
654 let src = include_str!("execution_context.rs");
655
656 for name in ["CURRENT_SNAPSHOT", "HAS_SNAPSHOT"] {
657 assert!(
658 !src.contains(&format!("pub static {name}"))
659 && !src.contains(&format!("pub(crate) static {name}")),
660 "{name} must stay private to execution_context; expose an explicit \
661 Option<SnapshotContext> via capture_current_snapshot instead"
662 );
663 }
664
665 assert!(
666 src.contains("the only sanctioned ambient read"),
667 "capture_current_snapshot must keep documenting that it is the single \
668 sanctioned ambient read of the snapshot thread-local"
669 );
670 }
671
672 fn with_secret_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
673 CURRENT_SECRET_RESOLVER.with(|cell| {
674 cell.replace(Some(SecretResolver {
675 store: None,
676 values: Some(values),
677 identity: None,
678 }));
679 });
680 let result = f();
681 CURRENT_SECRET_RESOLVER.with(|cell| {
682 cell.replace(None);
683 });
684 result
685 }
686
687 #[test]
688 fn current_secret_value_does_not_fall_back_to_reserved_red_secret_namespace() {
689 let values = HashMap::from([
690 (
691 "red.secret.aes_key".to_string(),
692 "vault-aes-key".to_string(),
693 ),
694 (
695 "red.secret.ai.providers.anthropic.tokens.default".to_string(),
696 "provider-key".to_string(),
697 ),
698 ("acme.key".to_string(), "user-value".to_string()),
699 ]);
700
701 with_secret_values(values, || {
702 assert_eq!(current_secret_value("red.vault/aes_key"), None);
703 assert_eq!(current_secret_value("red.vault/red.secret.aes_key"), None);
704 assert_eq!(
707 current_secret_value("red.vault/ai.providers.anthropic.tokens.default"),
708 None
709 );
710 assert_eq!(
711 current_secret_value("red.vault/red.secret.ai.providers.anthropic.tokens.default"),
712 None
713 );
714 assert_eq!(
715 current_secret_value("red.vault/acme.key").as_deref(),
716 Some("user-value")
717 );
718 });
719 }
720
721 fn with_kv_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
722 CURRENT_KV_RESOLVER.with(|cell| {
723 cell.replace(Some(KvResolver {
724 store: None,
725 values: Some(values),
726 identity: None,
727 }));
728 });
729 let result = f();
730 CURRENT_KV_RESOLVER.with(|cell| {
731 cell.replace(None);
732 });
733 result
734 }
735
736 #[test]
737 fn current_kv_value_resolves_namespaced_and_bare_keys() {
738 let values = HashMap::from([("acme.key".to_string(), "plain-value".to_string())]);
739
740 with_kv_values(values, || {
741 assert_eq!(
743 current_kv_value("red.kv/acme.key").as_deref(),
744 Some("plain-value")
745 );
746 assert_eq!(current_kv_value("acme.key").as_deref(), Some("plain-value"));
748 assert_eq!(current_kv_value("red.kv/missing.key"), None);
750 });
751 }
752
753 #[test]
754 fn config_key_read_allowed_hard_blocks_reserved_namespace() {
755 assert!(!config_key_read_allowed(
758 None,
759 None,
760 "red.config/red.config.aes_key"
761 ));
762 assert!(!config_key_read_allowed(None, None, "red.config.aes_key"));
763 assert!(config_key_read_allowed(
765 None,
766 None,
767 "red.config/ai.openrouter.default.key"
768 ));
769 assert!(config_key_read_allowed(None, None, "acme.flags.beta"));
770 }
771
772 #[test]
773 fn is_config_collection_matches_system_config_stores() {
774 assert!(is_config_collection("red_config"));
775 assert!(is_config_collection("RED_CONFIG"));
776 assert!(is_config_collection("red.config"));
777 assert!(!is_config_collection("users"));
778 assert!(!is_config_collection("red_kv"));
779 }
780
781 #[test]
782 fn update_current_kv_value_reflects_in_resolver() {
783 with_kv_values(HashMap::new(), || {
784 assert_eq!(current_kv_value("red.kv/feature.flag"), None);
785 update_current_kv_value("feature.flag", Some("on".to_string()));
786 assert_eq!(
787 current_kv_value("red.kv/feature.flag").as_deref(),
788 Some("on")
789 );
790 update_current_kv_value("feature.flag", None);
791 assert_eq!(current_kv_value("red.kv/feature.flag"), None);
792 });
793 }
794}
795
796fn latest_config_snapshot(db: &RedDB) -> HashMap<String, Value> {
797 let mut latest: HashMap<String, (u64, Value)> = HashMap::new();
798
799 if let Some(manager) = db.store().get_collection("red_config") {
800 manager.for_each_entity(|entity| {
801 let Some(row) = entity.data.as_row() else {
802 return true;
803 };
804 let Some(Value::Text(key)) = row.get_field("key") else {
805 return true;
806 };
807 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
808 let id = entity.id.raw();
809 let key = key.to_ascii_lowercase();
810 insert_latest_config_value(&mut latest, key.clone(), id, value.clone());
811 if let Some(rest) = key.strip_prefix("red.config.") {
812 insert_latest_config_value(&mut latest, format!("red.config/{rest}"), id, value);
813 }
814 true
815 });
816 }
817
818 if let Some(manager) = db.store().get_collection("red.config") {
819 manager.for_each_entity(|entity| {
820 let Some(row) = entity.data.as_row() else {
821 return true;
822 };
823 if matches!(row.get_field("tombstone"), Some(Value::Boolean(true))) {
824 return true;
825 }
826 let Some(Value::Text(key)) = row.get_field("key") else {
827 return true;
828 };
829 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
830 insert_latest_config_value(
831 &mut latest,
832 format!("red.config/{}", key.to_ascii_lowercase()),
833 entity.id.raw(),
834 value,
835 );
836 true
837 });
838 }
839
840 latest
841 .into_iter()
842 .map(|(key, (_, value))| (key, value))
843 .collect()
844}
845
846fn insert_latest_config_value(
847 latest: &mut HashMap<String, (u64, Value)>,
848 key: String,
849 id: u64,
850 value: Value,
851) {
852 match latest.get(&key) {
853 Some((prev_id, _)) if *prev_id > id => {}
854 _ => {
855 latest.insert(key, (id, value));
856 }
857 }
858}
859
860struct ConfigResolver {
861 db: Arc<RedDB>,
862 store: Option<Arc<crate::auth::store::AuthStore>>,
863 identity: Option<(String, crate::auth::Role, Option<String>)>,
864 values: Option<HashMap<String, Value>>,
865}
866
867impl ConfigResolver {
868 fn can_read(&self, key: &str) -> bool {
869 config_key_read_allowed(self.store.as_ref(), self.identity.as_ref(), key)
870 }
871}
872
873fn is_reserved_config_key(key: &str) -> bool {
877 let key = key.strip_prefix("red.config/").unwrap_or(key);
878 key.starts_with("red.config.")
879 || key == "red.config"
880 || key == super::function_catalog::REGISTRY_KEY
881}
882
883fn is_config_collection(collection: &str) -> bool {
887 matches!(
888 collection.to_ascii_lowercase().as_str(),
889 "red_config" | "red.config"
890 )
891}
892
893fn config_resource_target(key: &str) -> String {
894 let bare = key.strip_prefix("red.config/").unwrap_or(key);
895 format!("red.config/{bare}")
896}
897
898fn config_key_read_allowed(
904 store: Option<&Arc<crate::auth::store::AuthStore>>,
905 identity: Option<&(String, crate::auth::Role, Option<String>)>,
906 key: &str,
907) -> bool {
908 if is_reserved_config_key(key) {
909 return false;
910 }
911 let Some(store) = store else {
912 return true;
913 };
914 if !store.iam_authorization_enabled() {
915 return true;
916 }
917 let Some((username, role, tenant)) = identity else {
918 return true;
919 };
920 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
921 let mut resource =
922 crate::auth::policies::ResourceRef::new("config".to_string(), config_resource_target(key));
923 if let Some(tenant) = tenant {
924 resource = resource.with_tenant(tenant.clone());
925 }
926 let ctx = crate::auth::policies::EvalContext {
927 principal_tenant: tenant.clone(),
928 current_tenant: tenant.clone(),
929 peer_ip: None,
930 mfa_present: false,
931 now_ms: crate::auth::now_ms(),
932 principal_is_admin_role: *role == crate::auth::Role::Admin,
933 principal_is_platform_scoped: tenant.is_none(),
934 };
935 store.check_policy_authz_with_role(&principal, "config:read", &resource, &ctx, *role)
936}
937
938pub(crate) fn config_read_permitted(key: &str) -> bool {
942 let key = key.to_ascii_lowercase();
943 CURRENT_CONFIG_RESOLVER.with(|cell| match cell.borrow().as_ref() {
944 Some(resolver) => resolver.can_read(&key),
945 None => !is_reserved_config_key(&key),
946 })
947}
948
949pub(crate) fn kv_read_permitted(collection: &str, key: &str) -> bool {
952 if is_config_collection(collection) {
953 config_read_permitted(key)
954 } else {
955 true
956 }
957}
958
959pub(crate) struct ConfigSnapshotGuard {
960 previous: Option<ConfigResolver>,
961}
962
963impl ConfigSnapshotGuard {
964 pub(super) fn install(
965 db: Arc<RedDB>,
966 store: Option<Arc<crate::auth::store::AuthStore>>,
967 ) -> Self {
968 let identity = current_auth_identity().map(|(username, role)| {
969 let tenant = current_tenant();
970 (username, role, tenant)
971 });
972 let previous = CURRENT_CONFIG_RESOLVER.with(|cell| {
973 cell.replace(Some(ConfigResolver {
974 db,
975 store,
976 identity,
977 values: None,
978 }))
979 });
980 Self { previous }
981 }
982}
983
984impl Drop for ConfigSnapshotGuard {
985 fn drop(&mut self) {
986 let previous = self.previous.take();
987 CURRENT_CONFIG_RESOLVER.with(|cell| {
988 cell.replace(previous);
989 });
990 }
991}
992
993pub fn set_current_snapshot(ctx: SnapshotContext) {
998 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = Some(ctx));
999 HAS_SNAPSHOT.with(|c| c.set(true));
1000}
1001
1002pub fn clear_current_snapshot() {
1003 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = None);
1004 HAS_SNAPSHOT.with(|c| c.set(false));
1005}
1006
1007pub(crate) struct CurrentSnapshotGuard {
1013 previous: Option<SnapshotContext>,
1014}
1015
1016impl CurrentSnapshotGuard {
1017 pub(crate) fn install(ctx: SnapshotContext) -> Self {
1018 let previous = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
1019 set_current_snapshot(ctx);
1020 Self { previous }
1021 }
1022}
1023
1024impl Drop for CurrentSnapshotGuard {
1025 fn drop(&mut self) {
1026 let prev = self.previous.take();
1027 let has = prev.is_some();
1028 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = prev);
1029 HAS_SNAPSHOT.with(|c| c.set(has));
1030 }
1031}
1032
1033#[inline]
1044pub fn entity_visible_under_current_snapshot(
1045 entity: &crate::storage::unified::entity::UnifiedEntity,
1046) -> bool {
1047 if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
1054 return false;
1055 }
1056 if !HAS_SNAPSHOT.with(|c| c.get()) {
1062 return entity.xmax == 0;
1063 }
1064 CURRENT_SNAPSHOT.with(|cell| {
1065 let guard = cell.borrow();
1066 let Some(ctx) = guard.as_ref() else {
1067 return true;
1068 };
1069 let visible = visibility_check(ctx, entity.xmin, entity.xmax);
1070 if visible {
1071 record_serializable_read(ctx, entity);
1072 }
1073 visible
1074 })
1075}
1076
1077#[inline]
1082pub(crate) fn xids_visible_under_current_snapshot(xmin: u64, xmax: u64) -> bool {
1083 if !HAS_SNAPSHOT.with(|c| c.get()) {
1084 return true;
1085 }
1086 CURRENT_SNAPSHOT.with(|cell| {
1087 let guard = cell.borrow();
1088 let Some(ctx) = guard.as_ref() else {
1089 return true;
1090 };
1091 visibility_check(ctx, xmin, xmax)
1092 })
1093}
1094
1095pub fn capture_current_snapshot() -> Option<SnapshotContext> {
1120 CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone())
1121}
1122
1123pub(crate) fn current_snapshot_requires_index_fallback() -> bool {
1128 if !HAS_SNAPSHOT.with(|c| c.get()) {
1129 return false;
1130 }
1131 CURRENT_SNAPSHOT.with(|cell| {
1132 cell.borrow()
1133 .as_ref()
1134 .is_some_and(|ctx| ctx.requires_index_fallback)
1135 })
1136}
1137
1138#[derive(Clone, Default)]
1153pub struct SnapshotBundle {
1154 pub snapshot: Option<SnapshotContext>,
1155 pub auth: Option<(String, crate::auth::Role)>,
1156 pub tenant: Option<String>,
1157}
1158
1159pub fn snapshot_bundle() -> SnapshotBundle {
1162 SnapshotBundle {
1163 snapshot: capture_current_snapshot(),
1164 auth: current_auth_identity(),
1165 tenant: CURRENT_TENANT_ID.with(|cell| cell.borrow().clone()),
1166 }
1167}
1168
1169pub fn with_snapshot_bundle<R>(bundle: &SnapshotBundle, f: impl FnOnce() -> R) -> R {
1174 struct Guard {
1175 prev_snapshot: Option<SnapshotContext>,
1176 prev_auth: Option<(String, crate::auth::Role)>,
1177 prev_tenant: Option<String>,
1178 }
1179 impl Drop for Guard {
1180 fn drop(&mut self) {
1181 let snap = self.prev_snapshot.take();
1182 let has = snap.is_some();
1183 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = snap);
1184 HAS_SNAPSHOT.with(|c| c.set(has));
1185 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = self.prev_auth.take());
1186 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = self.prev_tenant.take());
1187 }
1188 }
1189
1190 let _guard = {
1191 let prev_snapshot = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
1192 let prev_auth = CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone());
1193 let prev_tenant = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
1194
1195 match bundle.snapshot.clone() {
1196 Some(ctx) => set_current_snapshot(ctx),
1197 None => clear_current_snapshot(),
1198 }
1199 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = bundle.auth.clone());
1200 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = bundle.tenant.clone());
1201
1202 Guard {
1203 prev_snapshot,
1204 prev_auth,
1205 prev_tenant,
1206 }
1207 };
1208 f()
1209}
1210
1211#[inline]
1215pub fn entity_visible_with_context(
1216 ctx: Option<&SnapshotContext>,
1217 entity: &crate::storage::unified::entity::UnifiedEntity,
1218) -> bool {
1219 if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
1223 return false;
1224 }
1225 match ctx {
1226 Some(ctx) => {
1227 let visible = visibility_check(ctx, entity.xmin, entity.xmax);
1228 if visible {
1229 record_serializable_read(ctx, entity);
1230 }
1231 visible
1232 }
1233 None => true,
1234 }
1235}
1236
1237fn record_serializable_read(
1238 ctx: &SnapshotContext,
1239 entity: &crate::storage::unified::entity::UnifiedEntity,
1240) {
1241 let Some(reader) = ctx.serializable_reader else {
1242 return;
1243 };
1244 if !matches!(
1245 &entity.kind,
1246 crate::storage::unified::entity::EntityKind::TableRow { .. }
1247 ) {
1248 return;
1249 }
1250 ctx.manager
1251 .record_serializable_read(reader, entity.kind.collection(), entity.logical_id());
1252}
1253
1254#[inline]
1255fn visibility_check(ctx: &SnapshotContext, xmin: u64, xmax: u64) -> bool {
1256 if xmin != 0 && ctx.manager.is_aborted(xmin) {
1260 return false;
1261 }
1262 let effective_xmax = if xmax != 0 && ctx.manager.is_aborted(xmax) {
1264 0
1265 } else {
1266 xmax
1267 };
1268 let own_xmin = xmin != 0 && ctx.own_xids.contains(&xmin);
1272 let own_xmax = effective_xmax != 0 && ctx.own_xids.contains(&effective_xmax);
1273 if own_xmax {
1274 return false;
1276 }
1277 if own_xmin {
1278 return true;
1279 }
1280 ctx.snapshot.sees(xmin, effective_xmax)
1281}