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    #[test]
81    fn typed_config_readers_preserve_cold_values_and_live_updates() {
82        let directory = tempfile::tempdir().expect("config fixture");
83        let runtime = RedDBRuntime::with_options(
84            RedDBOptions::persistent(directory.path().join("config.rdb"))
85                .with_memory_budget(32 * 1024 * 1024)
86                .with_bounded_disk_residency(true),
87        )
88        .expect("runtime");
89        let store = runtime.db().store();
90        store
91            .try_set_config_tree(
92                "probe",
93                &crate::json!({"boolean": false, "integer": 42, "float": 1.5, "string": "saved"}),
94            )
95            .expect("persist configuration");
96        store
97            .try_set_config_tree("red.config.queue.max_wait_ms", &crate::json!(1))
98            .expect("queue wait limit");
99        store.persist().expect("checkpoint");
100        let manager = store
101            .get_collection("red_config")
102            .expect("config collection");
103        assert!(manager.has_backing(), "fixture must read persisted rows");
104        assert!(
105            manager.query_all(|_| true).is_empty(),
106            "fixture must be cold"
107        );
108        assert_eq!(
109            store.try_get_config("probe.integer").unwrap(),
110            Some(Value::UnsignedInteger(42))
111        );
112        assert!(!runtime.config_bool("probe.boolean", true));
113        assert_eq!(runtime.config_u64("probe.integer", 9), 42);
114        assert_eq!(runtime.config_f64("probe.float", 9.0), 1.5);
115        assert_eq!(runtime.config_string("probe.string", "default"), "saved");
116        assert!(runtime.redwire_queue_wait_cap_check(5).is_err());
117        store
118            .try_set_config_tree("probe.integer", &crate::json!(73))
119            .expect("live replacement");
120        assert_eq!(runtime.config_u64("probe.integer", 9), 73);
121        assert_eq!(runtime.config_u64("probe.missing", 9), 9);
122    }
123
124    #[test]
125    fn boot_defaults_and_overlay_preserve_cold_operator_configuration() {
126        let directory = tempfile::tempdir().expect("config fixture");
127        let runtime = RedDBRuntime::with_options(
128            RedDBOptions::persistent(directory.path().join("config.rdb"))
129                .with_memory_budget(32 * 1024 * 1024)
130                .with_bounded_disk_residency(true),
131        )
132        .expect("runtime");
133        let store = runtime.db().store();
134        store
135            .try_set_config_tree("concurrency.locking.enabled", &crate::json!(false))
136            .expect("operator configuration");
137        store.persist().expect("checkpoint");
138        super::super::config_matrix::heal_critical_keys(store.as_ref());
139        assert_eq!(
140            store.try_get_config("concurrency.locking.enabled").unwrap(),
141            Some(Value::Boolean(false))
142        );
143        let overlay = directory.path().join("overlay.json");
144        std::fs::write(&overlay, r#"{"concurrency":{"locking":{"enabled":true}}}"#)
145            .expect("overlay fixture");
146        assert_eq!(
147            super::super::config_overlay::apply_config_file(
148                store.as_ref(),
149                overlay.to_str().unwrap()
150            ),
151            0
152        );
153        assert_eq!(
154            store.try_get_config("concurrency.locking.enabled").unwrap(),
155            Some(Value::Boolean(false))
156        );
157    }
158}
159
160pub(crate) fn insert_config_json_path(
161    root: &mut crate::serde_json::Value,
162    path: &str,
163    value: crate::serde_json::Value,
164) {
165    let segments: Vec<&str> = path
166        .split('.')
167        .filter(|segment| !segment.is_empty())
168        .collect();
169    insert_config_json_segments(root, &segments, value);
170}
171
172fn insert_config_json_segments(
173    root: &mut crate::serde_json::Value,
174    segments: &[&str],
175    value: crate::serde_json::Value,
176) {
177    if segments.is_empty() {
178        *root = value;
179        return;
180    }
181
182    if !matches!(root, crate::serde_json::Value::Object(_)) {
183        *root = crate::serde_json::Value::Object(crate::serde_json::Map::new());
184    }
185
186    let crate::serde_json::Value::Object(map) = root else {
187        return;
188    };
189    if segments.len() == 1 {
190        map.insert(segments[0].to_string(), value);
191        return;
192    }
193    let entry = map
194        .entry(segments[0].to_string())
195        .or_insert_with(|| crate::serde_json::Value::Object(crate::serde_json::Map::new()));
196    insert_config_json_segments(entry, &segments[1..], value);
197}
198
199pub(crate) fn show_config_json_result(
200    query: &str,
201    mode: reddb_rql::modes::QueryMode,
202    prefix: &Option<String>,
203    value: crate::serde_json::Value,
204) -> RuntimeQueryResult {
205    let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
206    let mut record = UnifiedRecord::new();
207    record.set(
208        "key",
209        prefix
210            .as_ref()
211            .map(|key| Value::text(key.clone()))
212            .unwrap_or(Value::Null),
213    );
214    record.set("value", Value::Json(value.to_string_compact().into_bytes()));
215    result.push(record);
216    RuntimeQueryResult {
217        query: query.to_string(),
218        mode,
219        statement: "show_config_json",
220        engine: "runtime-config",
221        result,
222        affected_rows: 0,
223        statement_type: "select",
224        bookmark: None,
225        notice: None,
226    }
227}
228
229impl RedDBRuntime {
230    pub(crate) fn set_internal_provider_secret(
231        &self,
232        key: &str,
233        value: Option<String>,
234    ) -> RedDBResult<()> {
235        self.reject_vault_transaction()?;
236        if super::execution_context::current_tenant().is_some() {
237            return Err(RedDBError::Query(
238                "system provider credentials are platform-scoped".into(),
239            ));
240        }
241        let auth =
242            self.inner.auth_store.read().clone().ok_or_else(|| {
243                RedDBError::Query("secret storage requires an enabled vault".into())
244            })?;
245        self.check_secret_write_privilege(&auth, key)?;
246        match value {
247            Some(value) => auth.vault_kv_try_set(key.into(), value),
248            None => auth.vault_kv_try_delete(key).map(|_| ()),
249        }
250        .map_err(|err| RedDBError::Query(err.to_string()))?;
251        self.invalidate_result_cache();
252        Ok(())
253    }
254
255    /// Read a vault KV secret from the configured AuthStore, if present.
256    pub fn vault_kv_get(&self, key: &str) -> Option<String> {
257        self.inner
258            .auth_store
259            .read()
260            .as_ref()
261            .and_then(|store| store.vault_kv_get(key))
262    }
263
264    /// Write a vault KV secret and fail if the encrypted vault write is
265    /// unavailable or cannot be made durable.
266    pub fn vault_kv_try_set(&self, key: String, value: String) -> RedDBResult<()> {
267        let store = self.inner.auth_store.read().clone().ok_or_else(|| {
268            RedDBError::Query("secret storage requires an enabled, unsealed vault".to_string())
269        })?;
270        store
271            .vault_kv_try_set(key, value)
272            .map_err(|err| RedDBError::Query(err.to_string()))
273    }
274
275    /// Returns the vault AES key (`red.secret.aes_key`) if an auth
276    /// store is wired and a key has been generated. Used by the
277    /// `Value::Secret` encrypt/decrypt pipeline.
278    pub(crate) fn secret_aes_key(&self) -> Option<[u8; 32]> {
279        let guard = self.inner.auth_store.read();
280        guard.as_ref().and_then(|s| s.vault_secret_key())
281    }
282
283    /// Resolve a boolean flag from `red_config`. Defaults to `default`
284    /// when the key is missing or not coercible. If the same key has
285    /// been written multiple times (SET CONFIG appends new rows), the
286    /// most recent entity wins. Env-var overrides
287    /// (`REDDB_<UP_DOTTED>`) take highest precedence.
288    pub(crate) fn config_bool(&self, key: &str, default: bool) -> bool {
289        if let Some(raw) = self.inner.env_config_overrides.get(key) {
290            if let Some(reddb_types::Value::Boolean(b)) =
291                crate::runtime::config_overlay::coerce_env_value(key, raw)
292            {
293                return b;
294            }
295        }
296        match self.inner.db.store().get_config(key) {
297            Some(Value::Boolean(value)) => value,
298            Some(Value::Text(value)) => matches!(value.as_ref(), "true" | "TRUE" | "True" | "1"),
299            Some(Value::Integer(value)) => value != 0,
300            _ => default,
301        }
302    }
303
304    /// Whether DOCUMENT writes should store the body as the native binary
305    /// container (PRD-1398, ADR-0063). On by default after the production
306    /// cutover. Reads decode the container transparently regardless of this
307    /// flag.
308    pub(crate) fn binary_document_body_enabled(&self) -> bool {
309        self.config_bool("storage.binary_document_body", true)
310    }
311
312    pub(crate) fn config_u64(&self, key: &str, default: u64) -> u64 {
313        if let Some(raw) = self.inner.env_config_overrides.get(key) {
314            if let Some(Value::UnsignedInteger(value)) =
315                crate::runtime::config_overlay::coerce_env_value(key, raw)
316            {
317                return value;
318            }
319        }
320        match self.inner.db.store().get_config(key) {
321            Some(Value::Integer(value)) => value as u64,
322            Some(Value::UnsignedInteger(value)) => value,
323            Some(Value::Text(value)) => value.parse::<u64>().unwrap_or(default),
324            _ => default,
325        }
326    }
327
328    pub(crate) fn config_f64(&self, key: &str, default: f64) -> f64 {
329        if let Some(raw) = self.inner.env_config_overrides.get(key) {
330            if let Ok(value) = raw.parse::<f64>() {
331                return value;
332            }
333        }
334        match self.inner.db.store().get_config(key) {
335            Some(Value::Float(value)) => value,
336            Some(Value::Integer(value)) => value as f64,
337            Some(Value::UnsignedInteger(value)) => value as f64,
338            Some(Value::Text(value)) => value.parse::<f64>().unwrap_or(default),
339            _ => default,
340        }
341    }
342
343    pub(crate) fn config_string(&self, key: &str, default: &str) -> String {
344        if let Some(raw) = self.inner.env_config_overrides.get(key) {
345            return raw.clone();
346        }
347        match self.inner.db.store().get_config(key) {
348            Some(Value::Text(value)) => value.to_string(),
349            _ => default.to_string(),
350        }
351    }
352
353    /// Whether `SECRET('...')` literals should be encrypted with the
354    /// vault AES key on INSERT. Default `true`.
355    pub(crate) fn secret_auto_encrypt(&self) -> bool {
356        self.config_bool("red.config.secret.auto_encrypt", true)
357    }
358}