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.anthropic.default.api_key"
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    }
140}
141
142impl RedDBRuntime {
143    /// Read a vault KV secret from the configured AuthStore, if present.
144    pub fn vault_kv_get(&self, key: &str) -> Option<String> {
145        self.inner
146            .auth_store
147            .read()
148            .as_ref()
149            .and_then(|store| store.vault_kv_get(key))
150    }
151
152    /// Write a vault KV secret and fail if the encrypted vault write is
153    /// unavailable or cannot be made durable.
154    pub fn vault_kv_try_set(&self, key: String, value: String) -> RedDBResult<()> {
155        let store = self.inner.auth_store.read().clone().ok_or_else(|| {
156            RedDBError::Query("secret storage requires an enabled, unsealed vault".to_string())
157        })?;
158        store
159            .vault_kv_try_set(key, value)
160            .map_err(|err| RedDBError::Query(err.to_string()))
161    }
162
163    /// Returns the vault AES key (`red.secret.aes_key`) if an auth
164    /// store is wired and a key has been generated. Used by the
165    /// `Value::Secret` encrypt/decrypt pipeline.
166    pub(crate) fn secret_aes_key(&self) -> Option<[u8; 32]> {
167        let guard = self.inner.auth_store.read();
168        guard.as_ref().and_then(|s| s.vault_secret_key())
169    }
170
171    /// Resolve a boolean flag from `red_config`. Defaults to `default`
172    /// when the key is missing or not coercible. If the same key has
173    /// been written multiple times (SET CONFIG appends new rows), the
174    /// most recent entity wins. Env-var overrides
175    /// (`REDDB_<UP_DOTTED>`) take highest precedence.
176    pub(crate) fn config_bool(&self, key: &str, default: bool) -> bool {
177        if let Some(raw) = self.inner.env_config_overrides.get(key) {
178            if let Some(crate::storage::schema::Value::Boolean(b)) =
179                crate::runtime::config_overlay::coerce_env_value(key, raw)
180            {
181                return b;
182            }
183        }
184        let store = self.inner.db.store();
185        let Some(manager) = store.get_collection("red_config") else {
186            return default;
187        };
188        let mut result = default;
189        let mut latest_id: u64 = 0;
190        manager.for_each_entity(|entity| {
191            if let Some(row) = entity.data.as_row() {
192                let entry_key = row.get_field("key").and_then(|v| match v {
193                    crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
194                    _ => None,
195                });
196                if entry_key == Some(key) {
197                    let id = entity.id.raw();
198                    if id >= latest_id {
199                        latest_id = id;
200                        result = match row.get_field("value") {
201                            Some(crate::storage::schema::Value::Boolean(b)) => *b,
202                            Some(crate::storage::schema::Value::Text(s)) => {
203                                matches!(s.as_ref(), "true" | "TRUE" | "True" | "1")
204                            }
205                            Some(crate::storage::schema::Value::Integer(n)) => *n != 0,
206                            _ => default,
207                        };
208                    }
209                }
210            }
211            true
212        });
213        result
214    }
215
216    /// Whether DOCUMENT writes should store the body as the native binary
217    /// container (PRD-1398, ADR-0063). On by default after the production
218    /// cutover. Reads decode the container transparently regardless of this
219    /// flag.
220    pub(crate) fn binary_document_body_enabled(&self) -> bool {
221        self.config_bool("storage.binary_document_body", true)
222    }
223
224    pub(crate) fn config_u64(&self, key: &str, default: u64) -> u64 {
225        if let Some(raw) = self.inner.env_config_overrides.get(key) {
226            if let Some(crate::storage::schema::Value::UnsignedInteger(n)) =
227                crate::runtime::config_overlay::coerce_env_value(key, raw)
228            {
229                return n;
230            }
231        }
232        let store = self.inner.db.store();
233        let Some(manager) = store.get_collection("red_config") else {
234            return default;
235        };
236        let mut result = default;
237        let mut latest_id: u64 = 0;
238        manager.for_each_entity(|entity| {
239            if let Some(row) = entity.data.as_row() {
240                let entry_key = row.get_field("key").and_then(|v| match v {
241                    crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
242                    _ => None,
243                });
244                if entry_key == Some(key) {
245                    let id = entity.id.raw();
246                    if id >= latest_id {
247                        latest_id = id;
248                        result = match row.get_field("value") {
249                            Some(crate::storage::schema::Value::Integer(n)) => *n as u64,
250                            Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n,
251                            Some(crate::storage::schema::Value::Text(s)) => {
252                                s.parse::<u64>().unwrap_or(default)
253                            }
254                            _ => default,
255                        };
256                    }
257                }
258            }
259            true
260        });
261        result
262    }
263
264    pub(crate) fn config_f64(&self, key: &str, default: f64) -> f64 {
265        if let Some(raw) = self.inner.env_config_overrides.get(key) {
266            if let Ok(n) = raw.parse::<f64>() {
267                return n;
268            }
269        }
270        let store = self.inner.db.store();
271        let Some(manager) = store.get_collection("red_config") else {
272            return default;
273        };
274        let mut result = default;
275        let mut latest_id: u64 = 0;
276        manager.for_each_entity(|entity| {
277            if let Some(row) = entity.data.as_row() {
278                let entry_key = row.get_field("key").and_then(|v| match v {
279                    crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
280                    _ => None,
281                });
282                if entry_key == Some(key) {
283                    let id = entity.id.raw();
284                    if id >= latest_id {
285                        latest_id = id;
286                        result = match row.get_field("value") {
287                            Some(crate::storage::schema::Value::Float(n)) => *n,
288                            Some(crate::storage::schema::Value::Integer(n)) => *n as f64,
289                            Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n as f64,
290                            Some(crate::storage::schema::Value::Text(s)) => {
291                                s.parse::<f64>().unwrap_or(default)
292                            }
293                            _ => default,
294                        };
295                    }
296                }
297            }
298            true
299        });
300        result
301    }
302
303    pub(crate) fn config_string(&self, key: &str, default: &str) -> String {
304        if let Some(raw) = self.inner.env_config_overrides.get(key) {
305            return raw.clone();
306        }
307        let store = self.inner.db.store();
308        let Some(manager) = store.get_collection("red_config") else {
309            return default.to_string();
310        };
311        let mut result = default.to_string();
312        let mut latest_id: u64 = 0;
313        manager.for_each_entity(|entity| {
314            if let Some(row) = entity.data.as_row() {
315                let entry_key = row.get_field("key").and_then(|v| match v {
316                    crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
317                    _ => None,
318                });
319                if entry_key == Some(key) {
320                    let id = entity.id.raw();
321                    if id >= latest_id {
322                        latest_id = id;
323                        if let Some(crate::storage::schema::Value::Text(value)) =
324                            row.get_field("value")
325                        {
326                            result = value.to_string();
327                        }
328                    }
329                }
330            }
331            true
332        });
333        result
334    }
335
336    /// Whether `SECRET('...')` literals should be encrypted with the
337    /// vault AES key on INSERT. Default `true`.
338    pub(crate) fn secret_auto_encrypt(&self) -> bool {
339        self.config_bool("red.config.secret.auto_encrypt", true)
340    }
341
342    /// Whether `Value::Secret` columns should be decrypted back to
343    /// plaintext on SELECT when the vault is unsealed. Default `true`.
344    /// Turning this off keeps secrets masked as `***` even while the
345    /// vault is open — useful for audit trails or read-only exports.
346    pub(crate) fn secret_auto_decrypt(&self) -> bool {
347        self.config_bool("red.config.secret.auto_decrypt", true)
348    }
349
350    /// Walk every record in `result` and swap `Value::Secret(bytes)`
351    /// for the decrypted plaintext when the runtime has the vault
352    /// AES key AND `red.config.secret.auto_decrypt = true`. If the
353    /// key is missing, the vault is sealed, or auto_decrypt is off,
354    /// secrets are left as `Value::Secret` which every formatter
355    /// (Display, JSON) already masks as `***`.
356    pub(crate) fn apply_secret_decryption(&self, result: &mut RuntimeQueryResult) {
357        if !self.secret_auto_decrypt() {
358            return;
359        }
360        let Some(key) = self.secret_aes_key() else {
361            return;
362        };
363        for record in result.result.records.iter_mut() {
364            for value in record.values_mut() {
365                if let Value::Secret(ref bytes) = value {
366                    if let Some(plain) =
367                        super::impl_dml_crypto::decrypt_secret_payload(&key, bytes.as_slice())
368                    {
369                        if let Ok(text) = String::from_utf8(plain) {
370                            *value = Value::text(text);
371                        }
372                    }
373                }
374            }
375        }
376    }
377}