1use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use crate::api::{RedDBError, RedDBResult};
10use crate::storage::schema::Value;
11use crate::storage::RedDB;
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 fn with_secret_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
647 CURRENT_SECRET_RESOLVER.with(|cell| {
648 cell.replace(Some(SecretResolver {
649 store: None,
650 values: Some(values),
651 identity: None,
652 }));
653 });
654 let result = f();
655 CURRENT_SECRET_RESOLVER.with(|cell| {
656 cell.replace(None);
657 });
658 result
659 }
660
661 #[test]
662 fn current_secret_value_does_not_fall_back_to_reserved_red_secret_namespace() {
663 let values = HashMap::from([
664 (
665 "red.secret.aes_key".to_string(),
666 "vault-aes-key".to_string(),
667 ),
668 (
669 "red.secret.ai.providers.anthropic.tokens.default".to_string(),
670 "provider-key".to_string(),
671 ),
672 ("acme.key".to_string(), "user-value".to_string()),
673 ]);
674
675 with_secret_values(values, || {
676 assert_eq!(current_secret_value("red.vault/aes_key"), None);
677 assert_eq!(current_secret_value("red.vault/red.secret.aes_key"), None);
678 assert_eq!(
681 current_secret_value("red.vault/ai.providers.anthropic.tokens.default"),
682 None
683 );
684 assert_eq!(
685 current_secret_value("red.vault/red.secret.ai.providers.anthropic.tokens.default"),
686 None
687 );
688 assert_eq!(
689 current_secret_value("red.vault/acme.key").as_deref(),
690 Some("user-value")
691 );
692 });
693 }
694
695 fn with_kv_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
696 CURRENT_KV_RESOLVER.with(|cell| {
697 cell.replace(Some(KvResolver {
698 store: None,
699 values: Some(values),
700 identity: None,
701 }));
702 });
703 let result = f();
704 CURRENT_KV_RESOLVER.with(|cell| {
705 cell.replace(None);
706 });
707 result
708 }
709
710 #[test]
711 fn current_kv_value_resolves_namespaced_and_bare_keys() {
712 let values = HashMap::from([("acme.key".to_string(), "plain-value".to_string())]);
713
714 with_kv_values(values, || {
715 assert_eq!(
717 current_kv_value("red.kv/acme.key").as_deref(),
718 Some("plain-value")
719 );
720 assert_eq!(current_kv_value("acme.key").as_deref(), Some("plain-value"));
722 assert_eq!(current_kv_value("red.kv/missing.key"), None);
724 });
725 }
726
727 #[test]
728 fn config_key_read_allowed_hard_blocks_reserved_namespace() {
729 assert!(!config_key_read_allowed(
732 None,
733 None,
734 "red.config/red.config.aes_key"
735 ));
736 assert!(!config_key_read_allowed(None, None, "red.config.aes_key"));
737 assert!(config_key_read_allowed(
739 None,
740 None,
741 "red.config/ai.openrouter.default.key"
742 ));
743 assert!(config_key_read_allowed(None, None, "acme.flags.beta"));
744 }
745
746 #[test]
747 fn is_config_collection_matches_system_config_stores() {
748 assert!(is_config_collection("red_config"));
749 assert!(is_config_collection("RED_CONFIG"));
750 assert!(is_config_collection("red.config"));
751 assert!(!is_config_collection("users"));
752 assert!(!is_config_collection("red_kv"));
753 }
754
755 #[test]
756 fn update_current_kv_value_reflects_in_resolver() {
757 with_kv_values(HashMap::new(), || {
758 assert_eq!(current_kv_value("red.kv/feature.flag"), None);
759 update_current_kv_value("feature.flag", Some("on".to_string()));
760 assert_eq!(
761 current_kv_value("red.kv/feature.flag").as_deref(),
762 Some("on")
763 );
764 update_current_kv_value("feature.flag", None);
765 assert_eq!(current_kv_value("red.kv/feature.flag"), None);
766 });
767 }
768}
769
770fn latest_config_snapshot(db: &RedDB) -> HashMap<String, Value> {
771 let mut latest: HashMap<String, (u64, Value)> = HashMap::new();
772
773 if let Some(manager) = db.store().get_collection("red_config") {
774 manager.for_each_entity(|entity| {
775 let Some(row) = entity.data.as_row() else {
776 return true;
777 };
778 let Some(Value::Text(key)) = row.get_field("key") else {
779 return true;
780 };
781 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
782 let id = entity.id.raw();
783 let key = key.to_ascii_lowercase();
784 insert_latest_config_value(&mut latest, key.clone(), id, value.clone());
785 if let Some(rest) = key.strip_prefix("red.config.") {
786 insert_latest_config_value(&mut latest, format!("red.config/{rest}"), id, value);
787 }
788 true
789 });
790 }
791
792 if let Some(manager) = db.store().get_collection("red.config") {
793 manager.for_each_entity(|entity| {
794 let Some(row) = entity.data.as_row() else {
795 return true;
796 };
797 if matches!(row.get_field("tombstone"), Some(Value::Boolean(true))) {
798 return true;
799 }
800 let Some(Value::Text(key)) = row.get_field("key") else {
801 return true;
802 };
803 let value = row.get_field("value").cloned().unwrap_or(Value::Null);
804 insert_latest_config_value(
805 &mut latest,
806 format!("red.config/{}", key.to_ascii_lowercase()),
807 entity.id.raw(),
808 value,
809 );
810 true
811 });
812 }
813
814 latest
815 .into_iter()
816 .map(|(key, (_, value))| (key, value))
817 .collect()
818}
819
820fn insert_latest_config_value(
821 latest: &mut HashMap<String, (u64, Value)>,
822 key: String,
823 id: u64,
824 value: Value,
825) {
826 match latest.get(&key) {
827 Some((prev_id, _)) if *prev_id > id => {}
828 _ => {
829 latest.insert(key, (id, value));
830 }
831 }
832}
833
834struct ConfigResolver {
835 db: Arc<RedDB>,
836 store: Option<Arc<crate::auth::store::AuthStore>>,
837 identity: Option<(String, crate::auth::Role, Option<String>)>,
838 values: Option<HashMap<String, Value>>,
839}
840
841impl ConfigResolver {
842 fn can_read(&self, key: &str) -> bool {
843 config_key_read_allowed(self.store.as_ref(), self.identity.as_ref(), key)
844 }
845}
846
847fn is_reserved_config_key(key: &str) -> bool {
851 let key = key.strip_prefix("red.config/").unwrap_or(key);
852 key.starts_with("red.config.") || key == "red.config"
853}
854
855fn is_config_collection(collection: &str) -> bool {
859 matches!(
860 collection.to_ascii_lowercase().as_str(),
861 "red_config" | "red.config"
862 )
863}
864
865fn config_resource_target(key: &str) -> String {
866 let bare = key.strip_prefix("red.config/").unwrap_or(key);
867 format!("red.config/{bare}")
868}
869
870fn config_key_read_allowed(
876 store: Option<&Arc<crate::auth::store::AuthStore>>,
877 identity: Option<&(String, crate::auth::Role, Option<String>)>,
878 key: &str,
879) -> bool {
880 if is_reserved_config_key(key) {
881 return false;
882 }
883 let Some(store) = store else {
884 return true;
885 };
886 if !store.iam_authorization_enabled() {
887 return true;
888 }
889 let Some((username, role, tenant)) = identity else {
890 return true;
891 };
892 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
893 let mut resource =
894 crate::auth::policies::ResourceRef::new("config".to_string(), config_resource_target(key));
895 if let Some(tenant) = tenant {
896 resource = resource.with_tenant(tenant.clone());
897 }
898 let ctx = crate::auth::policies::EvalContext {
899 principal_tenant: tenant.clone(),
900 current_tenant: tenant.clone(),
901 peer_ip: None,
902 mfa_present: false,
903 now_ms: crate::auth::now_ms(),
904 principal_is_admin_role: *role == crate::auth::Role::Admin,
905 principal_is_platform_scoped: tenant.is_none(),
906 };
907 store.check_policy_authz_with_role(&principal, "config:read", &resource, &ctx, *role)
908}
909
910pub(crate) fn config_read_permitted(key: &str) -> bool {
914 let key = key.to_ascii_lowercase();
915 CURRENT_CONFIG_RESOLVER.with(|cell| match cell.borrow().as_ref() {
916 Some(resolver) => resolver.can_read(&key),
917 None => !is_reserved_config_key(&key),
918 })
919}
920
921pub(crate) fn kv_read_permitted(collection: &str, key: &str) -> bool {
924 if is_config_collection(collection) {
925 config_read_permitted(key)
926 } else {
927 true
928 }
929}
930
931pub(crate) struct ConfigSnapshotGuard {
932 previous: Option<ConfigResolver>,
933}
934
935impl ConfigSnapshotGuard {
936 pub(super) fn install(
937 db: Arc<RedDB>,
938 store: Option<Arc<crate::auth::store::AuthStore>>,
939 ) -> Self {
940 let identity = current_auth_identity().map(|(username, role)| {
941 let tenant = current_tenant();
942 (username, role, tenant)
943 });
944 let previous = CURRENT_CONFIG_RESOLVER.with(|cell| {
945 cell.replace(Some(ConfigResolver {
946 db,
947 store,
948 identity,
949 values: None,
950 }))
951 });
952 Self { previous }
953 }
954}
955
956impl Drop for ConfigSnapshotGuard {
957 fn drop(&mut self) {
958 let previous = self.previous.take();
959 CURRENT_CONFIG_RESOLVER.with(|cell| {
960 cell.replace(previous);
961 });
962 }
963}
964
965pub fn set_current_snapshot(ctx: SnapshotContext) {
970 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = Some(ctx));
971 HAS_SNAPSHOT.with(|c| c.set(true));
972}
973
974pub fn clear_current_snapshot() {
975 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = None);
976 HAS_SNAPSHOT.with(|c| c.set(false));
977}
978
979pub(crate) struct CurrentSnapshotGuard {
985 previous: Option<SnapshotContext>,
986}
987
988impl CurrentSnapshotGuard {
989 pub(crate) fn install(ctx: SnapshotContext) -> Self {
990 let previous = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
991 set_current_snapshot(ctx);
992 Self { previous }
993 }
994}
995
996impl Drop for CurrentSnapshotGuard {
997 fn drop(&mut self) {
998 let prev = self.previous.take();
999 let has = prev.is_some();
1000 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = prev);
1001 HAS_SNAPSHOT.with(|c| c.set(has));
1002 }
1003}
1004
1005#[inline]
1016pub fn entity_visible_under_current_snapshot(
1017 entity: &crate::storage::unified::entity::UnifiedEntity,
1018) -> bool {
1019 if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
1026 return false;
1027 }
1028 if !HAS_SNAPSHOT.with(|c| c.get()) {
1034 return entity.xmax == 0;
1035 }
1036 CURRENT_SNAPSHOT.with(|cell| {
1037 let guard = cell.borrow();
1038 let Some(ctx) = guard.as_ref() else {
1039 return true;
1040 };
1041 let visible = visibility_check(ctx, entity.xmin, entity.xmax);
1042 if visible {
1043 record_serializable_read(ctx, entity);
1044 }
1045 visible
1046 })
1047}
1048
1049#[inline]
1054pub(crate) fn xids_visible_under_current_snapshot(xmin: u64, xmax: u64) -> bool {
1055 if !HAS_SNAPSHOT.with(|c| c.get()) {
1056 return true;
1057 }
1058 CURRENT_SNAPSHOT.with(|cell| {
1059 let guard = cell.borrow();
1060 let Some(ctx) = guard.as_ref() else {
1061 return true;
1062 };
1063 visibility_check(ctx, xmin, xmax)
1064 })
1065}
1066
1067pub fn capture_current_snapshot() -> Option<SnapshotContext> {
1074 CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone())
1075}
1076
1077pub(crate) fn current_snapshot_requires_index_fallback() -> bool {
1082 if !HAS_SNAPSHOT.with(|c| c.get()) {
1083 return false;
1084 }
1085 CURRENT_SNAPSHOT.with(|cell| {
1086 cell.borrow()
1087 .as_ref()
1088 .is_some_and(|ctx| ctx.requires_index_fallback)
1089 })
1090}
1091
1092#[derive(Clone, Default)]
1107pub struct SnapshotBundle {
1108 pub snapshot: Option<SnapshotContext>,
1109 pub auth: Option<(String, crate::auth::Role)>,
1110 pub tenant: Option<String>,
1111}
1112
1113pub fn snapshot_bundle() -> SnapshotBundle {
1116 SnapshotBundle {
1117 snapshot: capture_current_snapshot(),
1118 auth: current_auth_identity(),
1119 tenant: CURRENT_TENANT_ID.with(|cell| cell.borrow().clone()),
1120 }
1121}
1122
1123pub fn with_snapshot_bundle<R>(bundle: &SnapshotBundle, f: impl FnOnce() -> R) -> R {
1128 struct Guard {
1129 prev_snapshot: Option<SnapshotContext>,
1130 prev_auth: Option<(String, crate::auth::Role)>,
1131 prev_tenant: Option<String>,
1132 }
1133 impl Drop for Guard {
1134 fn drop(&mut self) {
1135 let snap = self.prev_snapshot.take();
1136 let has = snap.is_some();
1137 CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = snap);
1138 HAS_SNAPSHOT.with(|c| c.set(has));
1139 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = self.prev_auth.take());
1140 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = self.prev_tenant.take());
1141 }
1142 }
1143
1144 let _guard = {
1145 let prev_snapshot = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
1146 let prev_auth = CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone());
1147 let prev_tenant = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
1148
1149 match bundle.snapshot.clone() {
1150 Some(ctx) => set_current_snapshot(ctx),
1151 None => clear_current_snapshot(),
1152 }
1153 CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = bundle.auth.clone());
1154 CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = bundle.tenant.clone());
1155
1156 Guard {
1157 prev_snapshot,
1158 prev_auth,
1159 prev_tenant,
1160 }
1161 };
1162 f()
1163}
1164
1165#[inline]
1169pub fn entity_visible_with_context(
1170 ctx: Option<&SnapshotContext>,
1171 entity: &crate::storage::unified::entity::UnifiedEntity,
1172) -> bool {
1173 if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
1177 return false;
1178 }
1179 match ctx {
1180 Some(ctx) => {
1181 let visible = visibility_check(ctx, entity.xmin, entity.xmax);
1182 if visible {
1183 record_serializable_read(ctx, entity);
1184 }
1185 visible
1186 }
1187 None => true,
1188 }
1189}
1190
1191fn record_serializable_read(
1192 ctx: &SnapshotContext,
1193 entity: &crate::storage::unified::entity::UnifiedEntity,
1194) {
1195 let Some(reader) = ctx.serializable_reader else {
1196 return;
1197 };
1198 if !matches!(
1199 &entity.kind,
1200 crate::storage::unified::entity::EntityKind::TableRow { .. }
1201 ) {
1202 return;
1203 }
1204 ctx.manager
1205 .record_serializable_read(reader, entity.kind.collection(), entity.logical_id());
1206}
1207
1208#[inline]
1209fn visibility_check(ctx: &SnapshotContext, xmin: u64, xmax: u64) -> bool {
1210 if xmin != 0 && ctx.manager.is_aborted(xmin) {
1214 return false;
1215 }
1216 let effective_xmax = if xmax != 0 && ctx.manager.is_aborted(xmax) {
1218 0
1219 } else {
1220 xmax
1221 };
1222 let own_xmin = xmin != 0 && ctx.own_xids.contains(&xmin);
1226 let own_xmax = effective_xmax != 0 && ctx.own_xids.contains(&effective_xmax);
1227 if own_xmax {
1228 return false;
1230 }
1231 if own_xmin {
1232 return true;
1233 }
1234 ctx.snapshot.sees(xmin, effective_xmax)
1235}