Skip to main content

reddb_server/runtime/
execution_context.rs

1//! Per-thread runtime execution context.
2//!
3//! This module owns connection identity, auth/tenant scope, statement-local
4//! config/secret resolvers, and MVCC snapshot visibility helpers.
5
6use 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    /// Current connection id for the executing statement. Set by the
15    /// per-connection wrapper (stdio/gRPC handlers) before dispatching
16    /// into `execute_query`; falls back to `0` for embedded callers.
17    static CURRENT_CONN_ID: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
18
19    /// Authenticated user + role for the executing statement (Phase 2.5.2
20    /// RLS enforcement). Set by the transport middleware after validating
21    /// credentials (password / cert / oauth); unset means "anonymous" /
22    /// "embedded" — RLS policies degrade to the role-agnostic subset.
23    ///
24    /// `None` skips RLS injection entirely; `Some((username, role))`
25    /// passes `role` to `matching_rls_policies(table, Some(role), action)`.
26    static CURRENT_AUTH_IDENTITY: std::cell::RefCell<Option<(String, crate::auth::Role)>> =
27        const { std::cell::RefCell::new(None) };
28
29    /// MVCC snapshot scoped to the currently-executing statement (Phase
30    /// 2.3.2d PG parity). `execute_query` captures it on entry and drops
31    /// it on exit; every scan consults it via
32    /// `entity_visible_under_current_snapshot` to hide tuples whose xmin
33    /// hasn't committed or whose xmax already has.
34    ///
35    /// `None` means "pre-MVCC semantics" — the read path returns every
36    /// tuple regardless of xmin/xmax. All embedded callers that bypass
37    /// `execute_query` see this default.
38    static CURRENT_SNAPSHOT: std::cell::RefCell<Option<SnapshotContext>> =
39        const { std::cell::RefCell::new(None) };
40
41    /// Cheap presence flag for `CURRENT_SNAPSHOT`. Scan hot paths
42    /// poll this instead of `borrow()`-ing the RefCell on every
43    /// row — the common case (autocommit / no MVCC session) reads
44    /// one atomic `Cell<bool>` and short-circuits, saving ~10ns × N
45    /// rows on aggregate_group / select_range scans.
46    static HAS_SNAPSHOT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
47
48    /// Session-scoped tenant id for the current connection (Phase 2.5.3
49    /// multi-tenancy). Populated by `SET TENANT 'id'` or by transport
50    /// middleware after resolving tenant from auth claims. Read by the
51    /// `CURRENT_TENANT()` scalar function — RLS policies typically
52    /// combine it as `USING (tenant_id = CURRENT_TENANT())` to scope
53    /// every query to one tenant.
54    ///
55    /// `None` means "no tenant bound" — `CURRENT_TENANT()` returns
56    /// NULL, and RLS policies that gate on it hide every row.
57    static CURRENT_TENANT_ID: std::cell::RefCell<Option<String>> =
58        const { std::cell::RefCell::new(None) };
59
60    /// Statement-local config resolver. SQL expressions materialize the
61    /// `red_config` snapshot lazily on the first `$config.*`/`CONFIG()`
62    /// access, keeping ordinary statements on the zero-scan path.
63    static CURRENT_CONFIG_RESOLVER: std::cell::RefCell<Option<ConfigResolver>> =
64        const { std::cell::RefCell::new(None) };
65
66    /// Statement-local secret resolver. SQL expressions materialize the
67    /// vault KV snapshot lazily on first `$secret.*` access, then use
68    /// lock-free map reads for the rest of the statement.
69    static CURRENT_SECRET_RESOLVER: std::cell::RefCell<Option<SecretResolver>> =
70        const { std::cell::RefCell::new(None) };
71
72    /// Statement-local plain-KV resolver (#1602). Mirrors the secret
73    /// resolver but reads the non-encrypted `plain_kv` store and gates on
74    /// `kv:read`. Materialized lazily on the first `$kv.*` access.
75    static CURRENT_KV_RESOLVER: std::cell::RefCell<Option<KvResolver>> =
76        const { std::cell::RefCell::new(None) };
77}
78
79/// Snapshot + manager pair used for read-path visibility checks.
80///
81/// The manager is needed in addition to the snapshot because `aborted`
82/// state mutates after the snapshot is captured — a ROLLBACK by a
83/// committed-at-capture-time writer must still hide its tuples. Keeping
84/// the Arc around is O(pointer) and the RwLock reads on `is_aborted`
85/// are cheap (HashSet lookup under a parking_lot read guard).
86///
87/// `own_xids` (Phase 2.3.2e) lists the xids belonging to the current
88/// connection's transaction — the parent xid plus open and released
89/// savepoint sub-xids. The visibility rule promotes rows stamped with
90/// these xids to "always visible (unless aborted)" so the writer sees
91/// its own nested-savepoint writes even though their xids exceed
92/// `snapshot.xid`.
93#[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
102/// Install a connection id on the current thread for the duration of a
103/// statement. The Transaction State module is keyed
104/// by this id so different connections can hold independent BEGINs.
105///
106/// Pub so transports (PG wire, gRPC, HTTP per-request spawners) and
107/// tests can emulate per-connection isolation. Call it once when
108/// binding the connection's worker thread; pair with
109/// `clear_current_connection_id` on teardown.
110pub fn set_current_connection_id(id: u64) {
111    CURRENT_CONN_ID.with(|c| c.set(id));
112}
113
114/// Reset the thread's connection id back to `0` (autocommit).
115pub fn clear_current_connection_id() {
116    CURRENT_CONN_ID.with(|c| c.set(0));
117}
118
119/// Read the connection id set by `set_current_connection_id`. Returns
120/// `0` when no wrapper installed one — auto-commit path.
121pub fn current_connection_id() -> u64 {
122    CURRENT_CONN_ID.with(|c| c.get())
123}
124
125/// Install the authenticated identity for the current thread (Phase 2.5.2
126/// RLS enforcement). Transport layers call this right after resolving
127/// auth so the query dispatch can fold RLS policies into the filter.
128pub 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
132/// Clear the thread-local auth identity. Transports call this after the
133/// statement completes so pooled threads don't leak identities across
134/// requests.
135pub fn clear_current_auth_identity() {
136    CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = None);
137}
138
139/// Read the current-thread auth identity. `None` when no transport
140/// installed one (embedded mode / anonymous access).
141pub(crate) fn current_auth_identity() -> Option<(String, crate::auth::Role)> {
142    CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone())
143}
144
145/// Public probe of the thread-local auth identity for callers outside
146/// the `runtime` module (e.g. the AI credential resolver, which audits
147/// who triggered a secret read on behalf of a query).
148pub fn current_auth_identity_for_audit() -> Option<(String, crate::auth::Role)> {
149    current_auth_identity()
150}
151
152/// Install the session tenant id for the current thread (Phase 2.5.3
153/// multi-tenancy). Called by `SET TENANT 'id'` dispatch and by
154/// transport middleware that resolves tenant from auth claims (e.g.
155/// JWT `tenant` claim, HTTP header, subdomain).
156pub fn set_current_tenant(tenant_id: String) {
157    CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = Some(tenant_id));
158}
159
160/// Clear the current-thread tenant — `CURRENT_TENANT()` will then
161/// return NULL and any RLS policy gated on it will hide every row.
162pub fn clear_current_tenant() {
163    CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = None);
164}
165
166/// Read the current-thread tenant id, applying overrides in priority order:
167///   1. `WITHIN TENANT '<id>' …` per-statement override (highest)
168///   2. `SET LOCAL TENANT '<id>'` transaction-local override (consulted
169///      only when the current connection has an open transaction)
170///   3. `SET TENANT '<id>'` session-level thread-local
171///   4. `None` (deny-default for RLS).
172///
173/// The transaction-local layer is read through the runtime; an embedded
174/// helper crate that has no `RedDBRuntime` access still gets correct
175/// behaviour for layers 1, 3, and 4.
176pub 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    /// Snapshot of the active connection's transaction-local tenant for
191    /// the current `execute_query` call. Outer `Some(_)` means "a
192    /// transaction-local tenant override is active for this call";
193    /// inner is the override's value (`Some(s)` overrides to `s`,
194    /// `None` overrides to NULL/cleared). Refreshed at the top of every
195    /// `execute_query` invocation and cleared by the RAII guard on
196    /// return so pooled connections cannot leak the override past the
197    /// statement that owns it.
198    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
206/// Recognise `SET LOCAL TENANT '<id>'` / `SET LOCAL TENANT NULL` —
207/// returns `Ok(Some(Some(id)))` for an explicit value, `Ok(Some(None))`
208/// for an explicit NULL clear, `Ok(None)` when the input is not a
209/// `SET LOCAL TENANT` statement at all, and `Err` when the prefix
210/// matches but the value is malformed.
211pub(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    /// Stack of `WITHIN ... <stmt>` overrides active on the current
268    /// thread. Every entry corresponds to one in-flight `execute_query`
269    /// call that started with a `WITHIN` prefix; the entry is pushed
270    /// before dispatch and popped before the call returns. The stack
271    /// shape supports nested invocations (e.g. a view body that itself
272    /// re-enters execute_query).
273    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
291/// Cheap probe: is any `WITHIN …` scope override active on this
292/// thread? The fast-path needs to know without paying for the full
293/// `.last().cloned()` allocation — just peek at stack length.
294pub(crate) fn has_scope_override_active() -> bool {
295    SCOPE_OVERRIDES.with(|cell| !cell.borrow().is_empty())
296}
297
298/// RAII guard pairing `push_scope_override` with the matching pop, so
299/// the stack stays balanced even when the inner `execute_query` returns
300/// early via `?`.
301pub(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
316/// Read the current-thread auth identity, honouring per-statement
317/// `WITHIN ... USER '<u>' AS ROLE '<r>'` overrides. The override only
318/// supplies projected strings — it never grants additional privilege —
319/// so callers that need to make authorisation decisions must read from
320/// the underlying `current_auth_identity()` directly.
321pub(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<Value> {
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 let Some(value) = resolver.values.get(&key) {
347            return value.clone();
348        }
349        let value = resolver.runtime.query_secret_value(&key).ok().flatten();
350        resolver.values.insert(key, value.clone());
351        value
352    })
353}
354
355/// Decrypt only while evaluating a query. Stored and projected values retain
356/// their Secret representation, so formatters cannot expose plaintext.
357pub(crate) fn secret_query_input(value: Value) -> Option<Value> {
358    if !matches!(value, Value::Secret(_)) {
359        return Some(value);
360    }
361    CURRENT_SECRET_RESOLVER.with(|cell| {
362        let resolver = cell.borrow();
363        resolver.as_ref()?.runtime.secret_query_input(value)
364    })
365}
366
367pub(crate) fn secret_query_output(value: Value) -> crate::RedDBResult<Value> {
368    if value.is_null() || matches!(value, Value::Secret(_)) {
369        return Ok(value);
370    }
371    CURRENT_SECRET_RESOLVER.with(|cell| {
372        let resolver = cell.borrow();
373        let resolver = resolver.as_ref().ok_or_else(|| {
374            RedDBError::Query("secret expression requires an active statement".into())
375        })?;
376        resolver.runtime.secret_query_output(value)
377    })
378}
379
380#[cfg(test)]
381fn secret_value_from_snapshot(values: &HashMap<String, String>, key: &str) -> Option<String> {
382    if key.starts_with("red.secret.") {
383        return None;
384    }
385    values.get(key).cloned()
386}
387
388struct SecretResolver {
389    runtime: super::RedDBRuntime,
390    values: HashMap<String, Option<Value>>,
391}
392
393pub(crate) struct SecretStoreGuard {
394    previous: Option<SecretResolver>,
395}
396
397impl SecretStoreGuard {
398    pub(super) fn install(runtime: &super::RedDBRuntime) -> Self {
399        let previous = CURRENT_SECRET_RESOLVER.with(|cell| {
400            cell.replace(Some(SecretResolver {
401                runtime: runtime.clone(),
402                values: HashMap::new(),
403            }))
404        });
405        Self { previous }
406    }
407}
408
409impl Drop for SecretStoreGuard {
410    fn drop(&mut self) {
411        let previous = self.previous.take();
412        CURRENT_SECRET_RESOLVER.with(|cell| {
413            cell.replace(previous);
414        });
415    }
416}
417
418/// Resolve `$kv.<path>` from the plain (non-encrypted) KV store (#1602).
419///
420/// The parser desugars `$kv.<path>` to `__KV_REF("red.kv/<path>")`; this
421/// strips the `red.kv/` prefix and looks up the bare key in the
422/// statement-local snapshot, gating each hit on `kv:read`. Returns `None`
423/// (→ SQL NULL) when the key is absent or the principal is denied — never
424/// an error, mirroring the secret resolver's deny-to-NULL behaviour.
425pub(crate) fn current_kv_value(path: &str) -> Option<String> {
426    let key = path.to_ascii_lowercase();
427    CURRENT_KV_RESOLVER.with(|cell| {
428        let mut resolver = cell.borrow_mut();
429        let resolver = resolver.as_mut()?;
430        if resolver.values.is_none() {
431            resolver.values = resolver
432                .store
433                .as_ref()
434                .map(|store| store.plain_kv_snapshot());
435        }
436        let values = resolver.values.as_ref()?;
437        // Accept both the namespaced `red.kv/<key>` form (from the
438        // `$kv.*` desugar) and the bare key (from `update_current_kv_value`
439        // after a `SET KV`).
440        let found = values
441            .get(&key)
442            .map(|value| (key.as_str(), value))
443            .or_else(|| {
444                key.strip_prefix("red.kv/")
445                    .and_then(|rest| values.get(rest).map(|value| (rest, value)))
446            })?;
447        if !resolver.can_read(found.0) {
448            return None;
449        }
450        Some(found.1.clone())
451    })
452}
453
454struct KvResolver {
455    store: Option<Arc<crate::auth::store::AuthStore>>,
456    values: Option<HashMap<String, String>>,
457    identity: Option<(String, crate::auth::Role, Option<String>)>,
458}
459
460impl KvResolver {
461    fn can_read(&self, key: &str) -> bool {
462        let Some(store) = &self.store else {
463            return true;
464        };
465        let Some((username, role, tenant)) = &self.identity else {
466            return true;
467        };
468        let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
469        let mut resource =
470            crate::auth::policies::ResourceRef::new("kv".to_string(), key.to_string());
471        if let Some(tenant) = tenant {
472            resource = resource.with_tenant(tenant.clone());
473        }
474        let ctx = crate::auth::policies::EvalContext {
475            principal_tenant: tenant.clone(),
476            current_tenant: tenant.clone(),
477            peer_ip: None,
478            mfa_present: false,
479            now_ms: crate::auth::now_ms(),
480            principal_is_admin_role: *role == crate::auth::Role::Admin,
481            principal_is_platform_scoped: tenant.is_none(),
482        };
483        store.check_policy_authz_with_role(&principal, "kv:read", &resource, &ctx, *role)
484    }
485}
486
487pub(crate) struct KvStoreGuard {
488    // Boxed to keep the stack footprint of nested guards small. Each
489    // StatementFrameGuards is live for the full recursive call depth of a
490    // materialized-view refresh chain; unboxed KvResolver (≈120 B) would
491    // push those deep paths past the 8 MB stack limit.
492    previous: Option<Box<KvResolver>>,
493}
494
495impl KvStoreGuard {
496    pub(super) fn install(store: Option<Arc<crate::auth::store::AuthStore>>) -> Self {
497        let previous = CURRENT_KV_RESOLVER.with(|cell| {
498            cell.replace(Some(KvResolver {
499                store,
500                values: None,
501                identity: current_auth_identity().map(|(username, role)| {
502                    let tenant = current_tenant();
503                    (username, role, tenant)
504                }),
505            }))
506        });
507        Self {
508            previous: previous.map(Box::new),
509        }
510    }
511}
512
513impl Drop for KvStoreGuard {
514    fn drop(&mut self) {
515        let previous = self.previous.take().map(|b| *b);
516        CURRENT_KV_RESOLVER.with(|cell| {
517            cell.replace(previous);
518        });
519    }
520}
521
522/// Update the statement-local KV snapshot after a `SET KV` / `DELETE KV`
523/// so a subsequent `$kv.*` read in the same statement observes the write.
524pub(crate) fn update_current_kv_value(path: &str, value: Option<String>) {
525    let key = path.to_ascii_lowercase();
526    CURRENT_KV_RESOLVER.with(|cell| {
527        if let Some(resolver) = cell.borrow_mut().as_mut() {
528            let Some(values) = resolver.values.as_mut() else {
529                return;
530            };
531            match value {
532                Some(value) => {
533                    values.insert(key, value);
534                }
535                None => {
536                    values.remove(&key);
537                }
538            }
539        }
540    });
541}
542
543pub(crate) fn current_config_value(path: &str) -> Option<Value> {
544    let key = path.to_ascii_lowercase();
545    CURRENT_CONFIG_RESOLVER.with(|cell| {
546        let mut resolver = cell.borrow_mut();
547        let resolver = resolver.as_mut()?;
548        if resolver.values.is_none() {
549            resolver.values = Some(latest_config_snapshot(&resolver.db));
550        }
551        // #1370 — `$config.<path>` desugars to `CONFIG("red.config/<path>")`,
552        // but `SET CONFIG <path>` stores under the bare key. Mirror the secret
553        // resolver: after the namespaced key, fall back to the stripped bare
554        // key, then the dotted `red.config.<rest>` legacy form. Track the key
555        // that matched so the `config:read` gate authorizes the real target.
556        let found = {
557            let values = resolver.values.as_ref()?;
558            values
559                .get(&key)
560                .map(|value| (key.clone(), value.clone()))
561                .or_else(|| {
562                    key.strip_prefix("red.config/").and_then(|rest| {
563                        values
564                            .get(rest)
565                            .map(|value| (rest.to_string(), value.clone()))
566                            .or_else(|| {
567                                let legacy = format!("red.config.{rest}");
568                                values.get(&legacy).map(|value| (legacy, value.clone()))
569                            })
570                    })
571                })
572        }?;
573        // #1743 — hard-block the reserved `red.config.*` namespace and gate the
574        // read on `config:read`, mirroring the `$secret` sibling. Denied reads
575        // resolve to `None` (→ SQL NULL), never an error.
576        if !resolver.can_read(&found.0) {
577            return None;
578        }
579        Some(found.1)
580    })
581}
582
583pub(crate) fn update_current_config_value(path: &str, value: Value) {
584    let key = path.to_ascii_lowercase();
585    CURRENT_CONFIG_RESOLVER.with(|cell| {
586        if let Some(resolver) = cell.borrow_mut().as_mut() {
587            if let Some(values) = resolver.values.as_mut() {
588                values.insert(key, value);
589            }
590        }
591    });
592}
593
594pub(crate) fn update_current_secret_value(_path: &str, _value: Option<String>) {
595    CURRENT_SECRET_RESOLVER.with(|cell| {
596        if let Some(resolver) = cell.borrow_mut().as_mut() {
597            resolver.values.clear();
598        }
599    });
600}
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605
606    /// Companion to `raw_iteration_confined_to_maintenance_allowlist`:
607    /// the snapshot thread-local is an implementation detail of the
608    /// statement frame (issue #2138). It must stay private to this module,
609    /// so the only way any other module can observe ambient snapshot state
610    /// is `capture_current_snapshot`, which hands back an owned `Option`
611    /// the caller then passes explicitly into the scan API.
612    #[test]
613    fn snapshot_thread_local_stays_private_to_the_statement_frame() {
614        let src = include_str!("execution_context.rs");
615
616        for name in ["CURRENT_SNAPSHOT", "HAS_SNAPSHOT"] {
617            assert!(
618                !src.contains(&format!("pub static {name}"))
619                    && !src.contains(&format!("pub(crate) static {name}")),
620                "{name} must stay private to execution_context; expose an explicit \
621                 Option<SnapshotContext> via capture_current_snapshot instead"
622            );
623        }
624
625        assert!(
626            src.contains("the only sanctioned ambient read"),
627            "capture_current_snapshot must keep documenting that it is the single \
628             sanctioned ambient read of the snapshot thread-local"
629        );
630    }
631
632    #[test]
633    fn secret_snapshot_hard_blocks_system_keys() {
634        let values = HashMap::from([
635            ("red.secret.aes_key".into(), "internal".into()),
636            ("acme.key".into(), "user-value".into()),
637        ]);
638        assert_eq!(
639            secret_value_from_snapshot(&values, "red.secret.aes_key"),
640            None
641        );
642        assert_eq!(
643            secret_value_from_snapshot(&values, "acme.key").as_deref(),
644            Some("user-value")
645        );
646    }
647
648    fn with_kv_values<T>(values: HashMap<String, String>, f: impl FnOnce() -> T) -> T {
649        CURRENT_KV_RESOLVER.with(|cell| {
650            cell.replace(Some(KvResolver {
651                store: None,
652                values: Some(values),
653                identity: None,
654            }));
655        });
656        let result = f();
657        CURRENT_KV_RESOLVER.with(|cell| {
658            cell.replace(None);
659        });
660        result
661    }
662
663    #[test]
664    fn current_kv_value_resolves_namespaced_and_bare_keys() {
665        let values = HashMap::from([("acme.key".to_string(), "plain-value".to_string())]);
666
667        with_kv_values(values, || {
668            // `$kv.acme.key` desugars to `red.kv/acme.key`.
669            assert_eq!(
670                current_kv_value("red.kv/acme.key").as_deref(),
671                Some("plain-value")
672            );
673            // Bare-key lookup (post-`SET KV` snapshot update) also resolves.
674            assert_eq!(current_kv_value("acme.key").as_deref(), Some("plain-value"));
675            // Unknown keys resolve to None (→ SQL NULL), never an error.
676            assert_eq!(current_kv_value("red.kv/missing.key"), None);
677        });
678    }
679
680    #[test]
681    fn config_key_read_allowed_hard_blocks_reserved_namespace() {
682        // The reserved `red.config.*` system namespace is denied role- and
683        // store-independently, mirroring the `red.secret.*` block (#1743).
684        assert!(!config_key_read_allowed(
685            None,
686            None,
687            "red.config/red.config.aes_key"
688        ));
689        assert!(!config_key_read_allowed(None, None, "red.config.aes_key"));
690        // Ordinary config keys pass when no store is installed (non-IAM path).
691        assert!(config_key_read_allowed(
692            None,
693            None,
694            "red.config/ai.openrouter.default.key"
695        ));
696        assert!(config_key_read_allowed(None, None, "acme.flags.beta"));
697    }
698
699    #[test]
700    fn is_config_collection_matches_system_config_stores() {
701        assert!(is_config_collection("red_config"));
702        assert!(is_config_collection("RED_CONFIG"));
703        assert!(is_config_collection("red.config"));
704        assert!(!is_config_collection("users"));
705        assert!(!is_config_collection("red_kv"));
706    }
707
708    #[test]
709    fn update_current_kv_value_reflects_in_resolver() {
710        with_kv_values(HashMap::new(), || {
711            assert_eq!(current_kv_value("red.kv/feature.flag"), None);
712            update_current_kv_value("feature.flag", Some("on".to_string()));
713            assert_eq!(
714                current_kv_value("red.kv/feature.flag").as_deref(),
715                Some("on")
716            );
717            update_current_kv_value("feature.flag", None);
718            assert_eq!(current_kv_value("red.kv/feature.flag"), None);
719        });
720    }
721}
722
723fn latest_config_snapshot(db: &RedDB) -> HashMap<String, Value> {
724    let mut latest: HashMap<String, (u64, Value)> = HashMap::new();
725
726    if let Some(manager) = db.store().get_collection("red_config") {
727        manager.for_each_entity(|entity| {
728            let Some(row) = entity.data.as_row() else {
729                return true;
730            };
731            let Some(Value::Text(key)) = row.get_field("key") else {
732                return true;
733            };
734            let value = row.get_field("value").cloned().unwrap_or(Value::Null);
735            let id = entity.id.raw();
736            let key = key.to_ascii_lowercase();
737            insert_latest_config_value(&mut latest, key.clone(), id, value.clone());
738            if let Some(rest) = key.strip_prefix("red.config.") {
739                insert_latest_config_value(&mut latest, format!("red.config/{rest}"), id, value);
740            }
741            true
742        });
743    }
744
745    if let Some(manager) = db.store().get_collection("red.config") {
746        manager.for_each_entity(|entity| {
747            let Some(row) = entity.data.as_row() else {
748                return true;
749            };
750            if matches!(row.get_field("tombstone"), Some(Value::Boolean(true))) {
751                return true;
752            }
753            let Some(Value::Text(key)) = row.get_field("key") else {
754                return true;
755            };
756            let value = row.get_field("value").cloned().unwrap_or(Value::Null);
757            insert_latest_config_value(
758                &mut latest,
759                format!("red.config/{}", key.to_ascii_lowercase()),
760                entity.id.raw(),
761                value,
762            );
763            true
764        });
765    }
766
767    latest
768        .into_iter()
769        .map(|(key, (_, value))| (key, value))
770        .collect()
771}
772
773fn insert_latest_config_value(
774    latest: &mut HashMap<String, (u64, Value)>,
775    key: String,
776    id: u64,
777    value: Value,
778) {
779    match latest.get(&key) {
780        Some((prev_id, _)) if *prev_id > id => {}
781        _ => {
782            latest.insert(key, (id, value));
783        }
784    }
785}
786
787struct ConfigResolver {
788    db: Arc<RedDB>,
789    store: Option<Arc<crate::auth::store::AuthStore>>,
790    identity: Option<(String, crate::auth::Role, Option<String>)>,
791    values: Option<HashMap<String, Value>>,
792}
793
794impl ConfigResolver {
795    fn can_read(&self, key: &str) -> bool {
796        config_key_read_allowed(self.store.as_ref(), self.identity.as_ref(), key)
797    }
798}
799
800/// The reserved `red.config.*` system-config namespace. Never expose it via
801/// the inline `$config`/`CONFIG()`/`KV()` resolver regardless of IAM role,
802/// mirroring the role-independent `red.secret.*` hard-block (#1743).
803fn is_reserved_config_key(key: &str) -> bool {
804    let key = key.strip_prefix("red.config/").unwrap_or(key);
805    key.starts_with("red.config.")
806        || key == "red.config"
807        || key == super::function_catalog::REGISTRY_KEY
808}
809
810/// The system config-store collections (`red_config` / `red.config`) reached
811/// by the inline resolver. A `KV(<collection>, key)` read of one of these is
812/// gated exactly like `$config`; any other collection is unaffected (#1743).
813fn is_config_collection(collection: &str) -> bool {
814    matches!(
815        collection.to_ascii_lowercase().as_str(),
816        "red_config" | "red.config"
817    )
818}
819
820fn config_resource_target(key: &str) -> String {
821    let bare = key.strip_prefix("red.config/").unwrap_or(key);
822    format!("red.config/{bare}")
823}
824
825/// Shared inline-config read gate: a role-independent hard-block on the
826/// reserved `red.config.*` namespace, then a `config:read` capability check
827/// mirroring `SHOW CONFIG` / the `$secret` sibling (#1743). Absent a store or
828/// with IAM authorization disabled, only the hard-block applies so existing
829/// non-IAM `$config` reads keep working.
830fn config_key_read_allowed(
831    store: Option<&Arc<crate::auth::store::AuthStore>>,
832    identity: Option<&(String, crate::auth::Role, Option<String>)>,
833    key: &str,
834) -> bool {
835    if is_reserved_config_key(key) {
836        return false;
837    }
838    let Some(store) = store else {
839        return true;
840    };
841    if !store.iam_authorization_enabled() {
842        return true;
843    }
844    let Some((username, role, tenant)) = identity else {
845        return true;
846    };
847    let principal = crate::auth::UserId::from_parts(tenant.as_deref(), username);
848    let mut resource =
849        crate::auth::policies::ResourceRef::new("config".to_string(), config_resource_target(key));
850    if let Some(tenant) = tenant {
851        resource = resource.with_tenant(tenant.clone());
852    }
853    let ctx = crate::auth::policies::EvalContext {
854        principal_tenant: tenant.clone(),
855        current_tenant: tenant.clone(),
856        peer_ip: None,
857        mfa_present: false,
858        now_ms: crate::auth::now_ms(),
859        principal_is_admin_role: *role == crate::auth::Role::Admin,
860        principal_is_platform_scoped: tenant.is_none(),
861    };
862    store.check_policy_authz_with_role(&principal, "config:read", &resource, &ctx, *role)
863}
864
865/// Whether the current principal may read config `key` through the inline
866/// `$config`/`CONFIG()`/`KV('red_config', …)` resolver. Used by the db-fallback
867/// lookup paths that bypass [`current_config_value`] (#1743).
868pub(crate) fn config_read_permitted(key: &str) -> bool {
869    let key = key.to_ascii_lowercase();
870    CURRENT_CONFIG_RESOLVER.with(|cell| match cell.borrow().as_ref() {
871        Some(resolver) => resolver.can_read(&key),
872        None => !is_reserved_config_key(&key),
873    })
874}
875
876/// Gate a raw `KV(collection, key)` read: config system collections go through
877/// the `$config` gate; every other collection is unaffected (#1743).
878pub(crate) fn kv_read_permitted(collection: &str, key: &str) -> bool {
879    if is_config_collection(collection) {
880        config_read_permitted(key)
881    } else {
882        true
883    }
884}
885
886pub(crate) struct ConfigSnapshotGuard {
887    previous: Option<ConfigResolver>,
888}
889
890impl ConfigSnapshotGuard {
891    pub(super) fn install(
892        db: Arc<RedDB>,
893        store: Option<Arc<crate::auth::store::AuthStore>>,
894    ) -> Self {
895        let identity = current_auth_identity().map(|(username, role)| {
896            let tenant = current_tenant();
897            (username, role, tenant)
898        });
899        let previous = CURRENT_CONFIG_RESOLVER.with(|cell| {
900            cell.replace(Some(ConfigResolver {
901                db,
902                store,
903                identity,
904                values: None,
905            }))
906        });
907        Self { previous }
908    }
909}
910
911impl Drop for ConfigSnapshotGuard {
912    fn drop(&mut self) {
913        let previous = self.previous.take();
914        CURRENT_CONFIG_RESOLVER.with(|cell| {
915            cell.replace(previous);
916        });
917    }
918}
919
920/// Install the MVCC snapshot used by the current thread for the duration
921/// of one statement. Paired with `clear_current_snapshot()` — callers
922/// should prefer the `CurrentSnapshotGuard` RAII wrapper so early returns
923/// still clean up.
924pub fn set_current_snapshot(ctx: SnapshotContext) {
925    CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = Some(ctx));
926    HAS_SNAPSHOT.with(|c| c.set(true));
927}
928
929pub fn clear_current_snapshot() {
930    CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = None);
931    HAS_SNAPSHOT.with(|c| c.set(false));
932}
933
934/// Drop-guard that restores the previous snapshot on scope exit. Safe to
935/// nest — each statement saves the caller's snapshot and puts it back
936/// instead of blindly clearing, so a top-level `execute_query` called
937/// from inside another statement dispatch (e.g. vector source subqueries)
938/// doesn't strip visibility from the outer scan.
939pub(crate) struct CurrentSnapshotGuard {
940    previous: Option<SnapshotContext>,
941}
942
943impl CurrentSnapshotGuard {
944    pub(crate) fn install(ctx: SnapshotContext) -> Self {
945        let previous = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
946        set_current_snapshot(ctx);
947        Self { previous }
948    }
949}
950
951impl Drop for CurrentSnapshotGuard {
952    fn drop(&mut self) {
953        let prev = self.previous.take();
954        let has = prev.is_some();
955        CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = prev);
956        HAS_SNAPSHOT.with(|c| c.set(has));
957    }
958}
959
960/// Is this entity visible under the current thread's MVCC snapshot?
961///
962/// Returns `true` (no filtering) when no snapshot is installed — that
963/// path is used by embedded callers and by operations that intentionally
964/// bypass MVCC (VACUUM, snapshot export, admin introspection).
965///
966/// When a snapshot is installed the result is
967///   `snapshot.sees(xmin, xmax) && !mgr.is_aborted(xmin) && !xmax_half_abort`
968/// where `xmax_half_abort` re-grants visibility for tuples whose
969/// deleting transaction rolled back.
970#[inline]
971pub fn entity_visible_under_current_snapshot(
972    entity: &crate::storage::unified::entity::UnifiedEntity,
973) -> bool {
974    // Moderation visibility gate (#1274, ADR 0057). A row carrying the
975    // moderation status marker — quarantine-pending or rejected-tombstone
976    // — is hidden from every normal read, on top of MVCC visibility. The
977    // marker lives on the row itself, so the check is a single field probe
978    // and rides the existing per-row visibility chokepoint rather than
979    // adding a separate filter pass to each scan call-site.
980    if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
981        return false;
982    }
983    // Fast path — one `Cell<bool>` read, no RefCell borrow. Autocommit
984    // reads (no active MVCC transaction) still hide superseded physical
985    // versions while avoiding a full snapshot-context lookup.
986    // This runs on every row of every scan; the slow path only fires
987    // inside an explicit transaction.
988    if !HAS_SNAPSHOT.with(|c| c.get()) {
989        return entity.xmax == 0;
990    }
991    CURRENT_SNAPSHOT.with(|cell| {
992        let guard = cell.borrow();
993        let Some(ctx) = guard.as_ref() else {
994            return true;
995        };
996        let visible = visibility_check(ctx, entity.xmin, entity.xmax);
997        if visible {
998            record_serializable_read(ctx, entity);
999        }
1000        visible
1001    })
1002}
1003
1004/// Direct visibility check from raw `(xmin, xmax)` — bypasses the
1005/// entity borrow for callers that already decomposed the tuple (e.g.
1006/// pre-materialized scan caches). Same semantics as
1007/// `entity_visible_under_current_snapshot`.
1008#[inline]
1009pub(crate) fn xids_visible_under_current_snapshot(xmin: u64, xmax: u64) -> bool {
1010    if !HAS_SNAPSHOT.with(|c| c.get()) {
1011        return true;
1012    }
1013    CURRENT_SNAPSHOT.with(|cell| {
1014        let guard = cell.borrow();
1015        let Some(ctx) = guard.as_ref() else {
1016            return true;
1017        };
1018        visibility_check(ctx, xmin, xmax)
1019    })
1020}
1021
1022/// Clone the current thread's snapshot context. Parallel scan paths
1023/// (`query_all_zoned` with `std::thread::scope`) call this on the main
1024/// thread *before* spawning workers so the captured `SnapshotContext`
1025/// can be moved into every worker closure. Worker threads do not
1026/// inherit thread-locals, so calling `entity_visible_under_current_snapshot`
1027/// from inside a spawned closure would silently skip the filter.
1028///
1029/// This is the only sanctioned ambient read of the snapshot thread-local
1030/// (issue #2138). `CURRENT_SNAPSHOT` and `HAS_SNAPSHOT` are private to this
1031/// module and stay that way: they exist for statement-frame entry and for
1032/// detecting a frameless read, nothing else. Everything downstream must
1033/// take the resulting `Option<SnapshotContext>` as an explicit argument —
1034/// that is what makes `SegmentManager::scan*` thread-independent.
1035///
1036/// Statement-frame build also reads it, so a nested sub-execution inside
1037/// one statement (CTE prelude, materialized-view refresh, inline graph
1038/// TVF) inherits the enclosing snapshot instead of minting a fresh one
1039/// (#2183).
1040///
1041/// The `Option` is load-bearing: `None` means no statement frame is
1042/// installed — the low-level callers that intentionally bypass MVCC
1043/// (VACUUM, snapshot export, admin introspection) — and the scan API
1044/// keeps a documented fallback for that case rather than assuming a frame
1045/// always exists.
1046pub fn capture_current_snapshot() -> Option<SnapshotContext> {
1047    CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone())
1048}
1049
1050/// Whether the active read snapshot may need historical tuple versions
1051/// that the current secondary indexes cannot prove. Index paths can still
1052/// recheck visible candidates, but only a heap scan can discover versions
1053/// whose indexed value was changed or deleted after this snapshot.
1054pub(crate) fn current_snapshot_requires_index_fallback() -> bool {
1055    if !HAS_SNAPSHOT.with(|c| c.get()) {
1056        return false;
1057    }
1058    CURRENT_SNAPSHOT.with(|cell| {
1059        cell.borrow()
1060            .as_ref()
1061            .is_some_and(|ctx| ctx.requires_index_fallback)
1062    })
1063}
1064
1065/// Frozen MVCC + identity context for callers that need to reinstall
1066/// the same view across thread-local boundaries — long-lived cursors,
1067/// background batchers, anything that detaches from the dispatch path
1068/// and re-enters later.
1069///
1070/// The bundle bakes in the three thread-locals every read path
1071/// consults: `SnapshotContext` (MVCC visibility), the auth identity
1072/// (RLS policy gate), and the tenant id (RLS scope). A FETCH that
1073/// reinstalls the bundle sees exactly the same rows as the DECLARE
1074/// would have, regardless of writes that landed in between.
1075///
1076/// Cheap to clone — `SnapshotContext` is a clone of three
1077/// `Arc`-backed fields, identity is a `(String, Role)`, tenant is a
1078/// `String`. None of these contend with the read path.
1079#[derive(Clone, Default)]
1080pub struct SnapshotBundle {
1081    pub snapshot: Option<SnapshotContext>,
1082    pub auth: Option<(String, crate::auth::Role)>,
1083    pub tenant: Option<String>,
1084}
1085
1086/// Capture the three read-path thread-locals into a `SnapshotBundle`.
1087/// Pairs with `with_snapshot_bundle` for re-entry.
1088pub fn snapshot_bundle() -> SnapshotBundle {
1089    SnapshotBundle {
1090        snapshot: capture_current_snapshot(),
1091        auth: current_auth_identity(),
1092        tenant: CURRENT_TENANT_ID.with(|cell| cell.borrow().clone()),
1093    }
1094}
1095
1096/// Reinstall a captured `SnapshotBundle` for the duration of `f`.
1097/// Restores the caller's previous thread-locals on exit (panic-safe via
1098/// the explicit guard struct so a panic in `f` cannot leak the
1099/// installed identity into the worker's next request).
1100pub fn with_snapshot_bundle<R>(bundle: &SnapshotBundle, f: impl FnOnce() -> R) -> R {
1101    struct Guard {
1102        prev_snapshot: Option<SnapshotContext>,
1103        prev_auth: Option<(String, crate::auth::Role)>,
1104        prev_tenant: Option<String>,
1105    }
1106    impl Drop for Guard {
1107        fn drop(&mut self) {
1108            let snap = self.prev_snapshot.take();
1109            let has = snap.is_some();
1110            CURRENT_SNAPSHOT.with(|cell| *cell.borrow_mut() = snap);
1111            HAS_SNAPSHOT.with(|c| c.set(has));
1112            CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = self.prev_auth.take());
1113            CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = self.prev_tenant.take());
1114        }
1115    }
1116
1117    let _guard = {
1118        let prev_snapshot = CURRENT_SNAPSHOT.with(|cell| cell.borrow().clone());
1119        let prev_auth = CURRENT_AUTH_IDENTITY.with(|cell| cell.borrow().clone());
1120        let prev_tenant = CURRENT_TENANT_ID.with(|cell| cell.borrow().clone());
1121
1122        match bundle.snapshot.clone() {
1123            Some(ctx) => set_current_snapshot(ctx),
1124            None => clear_current_snapshot(),
1125        }
1126        CURRENT_AUTH_IDENTITY.with(|cell| *cell.borrow_mut() = bundle.auth.clone());
1127        CURRENT_TENANT_ID.with(|cell| *cell.borrow_mut() = bundle.tenant.clone());
1128
1129        Guard {
1130            prev_snapshot,
1131            prev_auth,
1132            prev_tenant,
1133        }
1134    };
1135    f()
1136}
1137
1138/// Apply the same visibility rules used by the thread-local helpers
1139/// against a caller-provided context. Intended for parallel workers
1140/// that captured the snapshot with `capture_current_snapshot()`.
1141#[inline]
1142pub fn entity_visible_with_context(
1143    ctx: Option<&SnapshotContext>,
1144    entity: &crate::storage::unified::entity::UnifiedEntity,
1145) -> bool {
1146    // Same moderation visibility gate as the thread-local path (#1274):
1147    // parallel scan workers capture the snapshot context but must apply
1148    // the moderation marker check identically.
1149    if crate::runtime::ai::moderation::entity_moderation_hidden(entity) {
1150        return false;
1151    }
1152    match ctx {
1153        Some(ctx) => {
1154            let visible = visibility_check(ctx, entity.xmin, entity.xmax);
1155            if visible {
1156                record_serializable_read(ctx, entity);
1157            }
1158            visible
1159        }
1160        None => true,
1161    }
1162}
1163
1164fn record_serializable_read(
1165    ctx: &SnapshotContext,
1166    entity: &crate::storage::unified::entity::UnifiedEntity,
1167) {
1168    let Some(reader) = ctx.serializable_reader else {
1169        return;
1170    };
1171    if !matches!(
1172        &entity.kind,
1173        crate::storage::unified::entity::EntityKind::TableRow { .. }
1174    ) {
1175        return;
1176    }
1177    ctx.manager
1178        .record_serializable_read(reader, entity.kind.collection(), entity.logical_id());
1179}
1180
1181#[inline]
1182fn visibility_check(ctx: &SnapshotContext, xmin: u64, xmax: u64) -> bool {
1183    // Writer aborted → tuple never existed from any future reader's view.
1184    // Checked *before* the own-xids fast path so an aborted own-sub-xid
1185    // (rolled-back savepoint) stays hidden from the parent.
1186    if xmin != 0 && ctx.manager.is_aborted(xmin) {
1187        return false;
1188    }
1189    // Deleter aborted → treat xmax as unset; fall back to xmin-only check.
1190    let effective_xmax = if xmax != 0 && ctx.manager.is_aborted(xmax) {
1191        0
1192    } else {
1193        xmax
1194    };
1195    // Phase 2.3.2e: own-tx writes are always visible to the connection
1196    // that stamped them, even when xmin/xmax exceed `snapshot.xid` (as
1197    // happens for sub-xids allocated by SAVEPOINT after BEGIN).
1198    let own_xmin = xmin != 0 && ctx.own_xids.contains(&xmin);
1199    let own_xmax = effective_xmax != 0 && ctx.own_xids.contains(&effective_xmax);
1200    if own_xmax {
1201        // This connection deleted the row via this xid — hide it from self.
1202        return false;
1203    }
1204    if own_xmin {
1205        return true;
1206    }
1207    ctx.snapshot.sees(xmin, effective_xmax)
1208}