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