1use 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 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 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 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 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 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 pub(crate) fn secret_auto_encrypt(&self) -> bool {
356 self.config_bool("red.config.secret.auto_encrypt", true)
357 }
358}