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