Skip to main content

reddb_server/runtime/
impl_config_secret.rs

1//! Runtime config / secret / KV resolution.
2//!
3//! Extracted verbatim from `impl_core.rs` (impl_core slice 7/10, issue #1628).
4//! Houses the config-tier readers, secret encrypt/decrypt pipeline, and vault
5//! KV accessors:
6//!
7//! - **Free helpers** — `seed_storage_deploy_config`, `show_secrets_allows_key`,
8//!   `secret_sql_value_to_string`, `insert_config_json_path`,
9//!   `insert_config_json_segments`, `show_config_json_result`.
10//! - **Methods** — `vault_kv_get`, `vault_kv_try_set`, `secret_aes_key`,
11//!   `config_bool`, `binary_document_body_enabled`, `config_u64`, `config_f64`,
12//!   `config_string`, `secret_auto_encrypt`, `secret_auto_decrypt`,
13//!   `apply_secret_decryption`.
14use super::*;
15
16pub(crate) fn seed_storage_deploy_config(
17    store: &crate::storage::UnifiedStore,
18    selection: crate::storage::StorageProfileSelection,
19) {
20    store.set_config_tree(
21        "storage.deploy",
22        &crate::json!({
23            "profile": selection.deploy_profile.as_str(),
24            "packaging": selection.packaging.as_str(),
25            "preset": selection.preset_name(),
26            "replica_count": selection.replica_count,
27            "managed_backup": selection.managed_backup,
28            "wal_retention": selection.wal_retention,
29        }),
30    );
31}
32
33pub(crate) fn show_secrets_allows_key(key: &str) -> bool {
34    !key.starts_with("red.secret.") && !key.starts_with("red.config.")
35}
36
37pub(crate) fn secret_sql_value_to_string(value: &Value) -> RedDBResult<String> {
38    match value {
39        Value::Text(s) => Ok(s.to_string()),
40        Value::Integer(n) => Ok(n.to_string()),
41        Value::UnsignedInteger(n) => Ok(n.to_string()),
42        Value::Float(n) => Ok(n.to_string()),
43        Value::Boolean(b) => Ok(b.to_string()),
44        Value::Null => Err(RedDBError::Query(
45            "SET SECRET key = NULL deletes the secret; use DELETE SECRET for explicit deletes"
46                .to_string(),
47        )),
48        Value::Password(_) | Value::Secret(_) => Err(RedDBError::Query(
49            "SET SECRET accepts plain scalar literals; PASSWORD() and SECRET() are for typed columns"
50                .to_string(),
51        )),
52        _ => Err(RedDBError::Query(format!(
53            "SET SECRET does not support value type {:?} yet",
54            value.data_type()
55        ))),
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn show_secrets_allows_only_user_managed_keys() {
65        assert!(!show_secrets_allows_key("red.secret.aes_key"));
66        assert!(!show_secrets_allows_key(
67            "red.secret.ai.providers.anthropic.tokens.default"
68        ));
69        assert!(!show_secrets_allows_key("red.config.ai.default.provider"));
70        assert!(show_secrets_allows_key("acme.key"));
71    }
72}
73
74pub(crate) fn insert_config_json_path(
75    root: &mut crate::serde_json::Value,
76    path: &str,
77    value: crate::serde_json::Value,
78) {
79    let segments: Vec<&str> = path
80        .split('.')
81        .filter(|segment| !segment.is_empty())
82        .collect();
83    insert_config_json_segments(root, &segments, value);
84}
85
86fn insert_config_json_segments(
87    root: &mut crate::serde_json::Value,
88    segments: &[&str],
89    value: crate::serde_json::Value,
90) {
91    if segments.is_empty() {
92        *root = value;
93        return;
94    }
95
96    if !matches!(root, crate::serde_json::Value::Object(_)) {
97        *root = crate::serde_json::Value::Object(crate::serde_json::Map::new());
98    }
99
100    let crate::serde_json::Value::Object(map) = root else {
101        return;
102    };
103    if segments.len() == 1 {
104        map.insert(segments[0].to_string(), value);
105        return;
106    }
107    let entry = map
108        .entry(segments[0].to_string())
109        .or_insert_with(|| crate::serde_json::Value::Object(crate::serde_json::Map::new()));
110    insert_config_json_segments(entry, &segments[1..], value);
111}
112
113pub(crate) fn show_config_json_result(
114    query: &str,
115    mode: crate::storage::query::modes::QueryMode,
116    prefix: &Option<String>,
117    value: crate::serde_json::Value,
118) -> RuntimeQueryResult {
119    let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
120    let mut record = UnifiedRecord::new();
121    record.set(
122        "key",
123        prefix
124            .as_ref()
125            .map(|key| Value::text(key.clone()))
126            .unwrap_or(Value::Null),
127    );
128    record.set("value", Value::Json(value.to_string_compact().into_bytes()));
129    result.push(record);
130    RuntimeQueryResult {
131        query: query.to_string(),
132        mode,
133        statement: "show_config_json",
134        engine: "runtime-config",
135        result,
136        affected_rows: 0,
137        statement_type: "select",
138        bookmark: None,
139        notice: None,
140    }
141}
142
143impl RedDBRuntime {
144    /// Read a vault KV secret from the configured AuthStore, if present.
145    pub fn vault_kv_get(&self, key: &str) -> Option<String> {
146        self.inner
147            .auth_store
148            .read()
149            .as_ref()
150            .and_then(|store| store.vault_kv_get(key))
151    }
152
153    /// Write a vault KV secret and fail if the encrypted vault write is
154    /// unavailable or cannot be made durable.
155    pub fn vault_kv_try_set(&self, key: String, value: String) -> RedDBResult<()> {
156        let store = self.inner.auth_store.read().clone().ok_or_else(|| {
157            RedDBError::Query("secret storage requires an enabled, unsealed vault".to_string())
158        })?;
159        store
160            .vault_kv_try_set(key, value)
161            .map_err(|err| RedDBError::Query(err.to_string()))
162    }
163
164    /// Returns the vault AES key (`red.secret.aes_key`) if an auth
165    /// store is wired and a key has been generated. Used by the
166    /// `Value::Secret` encrypt/decrypt pipeline.
167    pub(crate) fn secret_aes_key(&self) -> Option<[u8; 32]> {
168        let guard = self.inner.auth_store.read();
169        guard.as_ref().and_then(|s| s.vault_secret_key())
170    }
171
172    /// Resolve a boolean flag from `red_config`. Defaults to `default`
173    /// when the key is missing or not coercible. If the same key has
174    /// been written multiple times (SET CONFIG appends new rows), the
175    /// most recent entity wins. Env-var overrides
176    /// (`REDDB_<UP_DOTTED>`) take highest precedence.
177    pub(crate) fn config_bool(&self, key: &str, default: bool) -> bool {
178        if let Some(raw) = self.inner.env_config_overrides.get(key) {
179            if let Some(crate::storage::schema::Value::Boolean(b)) =
180                crate::runtime::config_overlay::coerce_env_value(key, raw)
181            {
182                return b;
183            }
184        }
185        let store = self.inner.db.store();
186        let Some(manager) = store.get_collection("red_config") else {
187            return default;
188        };
189        let mut result = default;
190        let mut latest_id: u64 = 0;
191        manager.for_each_entity(|entity| {
192            if let Some(row) = entity.data.as_row() {
193                let entry_key = row.get_field("key").and_then(|v| match v {
194                    crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
195                    _ => None,
196                });
197                if entry_key == Some(key) {
198                    let id = entity.id.raw();
199                    if id >= latest_id {
200                        latest_id = id;
201                        result = match row.get_field("value") {
202                            Some(crate::storage::schema::Value::Boolean(b)) => *b,
203                            Some(crate::storage::schema::Value::Text(s)) => {
204                                matches!(s.as_ref(), "true" | "TRUE" | "True" | "1")
205                            }
206                            Some(crate::storage::schema::Value::Integer(n)) => *n != 0,
207                            _ => default,
208                        };
209                    }
210                }
211            }
212            true
213        });
214        result
215    }
216
217    /// Whether DOCUMENT writes should store the body as the native binary
218    /// container (PRD-1398, ADR-0063). On by default after the production
219    /// cutover. Reads decode the container transparently regardless of this
220    /// flag.
221    pub(crate) fn binary_document_body_enabled(&self) -> bool {
222        self.config_bool("storage.binary_document_body", true)
223    }
224
225    pub(crate) fn config_u64(&self, key: &str, default: u64) -> u64 {
226        if let Some(raw) = self.inner.env_config_overrides.get(key) {
227            if let Some(crate::storage::schema::Value::UnsignedInteger(n)) =
228                crate::runtime::config_overlay::coerce_env_value(key, raw)
229            {
230                return n;
231            }
232        }
233        let store = self.inner.db.store();
234        let Some(manager) = store.get_collection("red_config") else {
235            return default;
236        };
237        let mut result = default;
238        let mut latest_id: u64 = 0;
239        manager.for_each_entity(|entity| {
240            if let Some(row) = entity.data.as_row() {
241                let entry_key = row.get_field("key").and_then(|v| match v {
242                    crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
243                    _ => None,
244                });
245                if entry_key == Some(key) {
246                    let id = entity.id.raw();
247                    if id >= latest_id {
248                        latest_id = id;
249                        result = match row.get_field("value") {
250                            Some(crate::storage::schema::Value::Integer(n)) => *n as u64,
251                            Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n,
252                            Some(crate::storage::schema::Value::Text(s)) => {
253                                s.parse::<u64>().unwrap_or(default)
254                            }
255                            _ => default,
256                        };
257                    }
258                }
259            }
260            true
261        });
262        result
263    }
264
265    pub(crate) fn config_f64(&self, key: &str, default: f64) -> f64 {
266        if let Some(raw) = self.inner.env_config_overrides.get(key) {
267            if let Ok(n) = raw.parse::<f64>() {
268                return n;
269            }
270        }
271        let store = self.inner.db.store();
272        let Some(manager) = store.get_collection("red_config") else {
273            return default;
274        };
275        let mut result = default;
276        let mut latest_id: u64 = 0;
277        manager.for_each_entity(|entity| {
278            if let Some(row) = entity.data.as_row() {
279                let entry_key = row.get_field("key").and_then(|v| match v {
280                    crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
281                    _ => None,
282                });
283                if entry_key == Some(key) {
284                    let id = entity.id.raw();
285                    if id >= latest_id {
286                        latest_id = id;
287                        result = match row.get_field("value") {
288                            Some(crate::storage::schema::Value::Float(n)) => *n,
289                            Some(crate::storage::schema::Value::Integer(n)) => *n as f64,
290                            Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n as f64,
291                            Some(crate::storage::schema::Value::Text(s)) => {
292                                s.parse::<f64>().unwrap_or(default)
293                            }
294                            _ => default,
295                        };
296                    }
297                }
298            }
299            true
300        });
301        result
302    }
303
304    pub(crate) fn config_string(&self, key: &str, default: &str) -> String {
305        if let Some(raw) = self.inner.env_config_overrides.get(key) {
306            return raw.clone();
307        }
308        let store = self.inner.db.store();
309        let Some(manager) = store.get_collection("red_config") else {
310            return default.to_string();
311        };
312        let mut result = default.to_string();
313        let mut latest_id: u64 = 0;
314        manager.for_each_entity(|entity| {
315            if let Some(row) = entity.data.as_row() {
316                let entry_key = row.get_field("key").and_then(|v| match v {
317                    crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
318                    _ => None,
319                });
320                if entry_key == Some(key) {
321                    let id = entity.id.raw();
322                    if id >= latest_id {
323                        latest_id = id;
324                        if let Some(crate::storage::schema::Value::Text(value)) =
325                            row.get_field("value")
326                        {
327                            result = value.to_string();
328                        }
329                    }
330                }
331            }
332            true
333        });
334        result
335    }
336
337    /// Whether `SECRET('...')` literals should be encrypted with the
338    /// vault AES key on INSERT. Default `true`.
339    pub(crate) fn secret_auto_encrypt(&self) -> bool {
340        self.config_bool("red.config.secret.auto_encrypt", true)
341    }
342
343    /// Whether `Value::Secret` columns should be decrypted back to
344    /// plaintext on SELECT when the vault is unsealed. Default `true`.
345    /// Turning this off keeps secrets masked as `***` even while the
346    /// vault is open — useful for audit trails or read-only exports.
347    pub(crate) fn secret_auto_decrypt(&self) -> bool {
348        self.config_bool("red.config.secret.auto_decrypt", true)
349    }
350
351    /// Walk every record in `result` and swap `Value::Secret(bytes)`
352    /// for the decrypted plaintext when the runtime has the vault
353    /// AES key AND `red.config.secret.auto_decrypt = true`. If the
354    /// key is missing, the vault is sealed, or auto_decrypt is off,
355    /// secrets are left as `Value::Secret` which every formatter
356    /// (Display, JSON) already masks as `***`.
357    pub(crate) fn apply_secret_decryption(&self, result: &mut RuntimeQueryResult) {
358        if !self.secret_auto_decrypt() {
359            return;
360        }
361        let Some(key) = self.secret_aes_key() else {
362            return;
363        };
364        for record in result.result.records.iter_mut() {
365            for value in record.values_mut() {
366                if let Value::Secret(ref bytes) = value {
367                    if let Some(plain) =
368                        super::impl_dml_crypto::decrypt_secret_payload(&key, bytes.as_slice())
369                    {
370                        if let Ok(text) = String::from_utf8(plain) {
371                            *value = Value::text(text);
372                        }
373                    }
374                }
375            }
376        }
377    }
378}