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("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 #[test]
160 fn fresh_boot_never_persists_a_redundant_red_config_prefix() {
161 let runtime = RedDBRuntime::in_memory().expect("runtime");
162 let manager = runtime
163 .db()
164 .store()
165 .get_collection("red.config")
166 .expect("red.config");
167 let mut invalid = Vec::new();
168 manager.for_each_entity(|entity| {
169 if let Some(Value::Text(key)) =
170 entity.data.as_row().and_then(|row| row.get_field("key"))
171 {
172 if key.starts_with("red.") {
173 invalid.push(key.to_string());
174 }
175 }
176 true
177 });
178 assert!(
179 invalid.is_empty(),
180 "fresh red.config contains redundant prefixes: {invalid:?}"
181 );
182 }
183
184 #[test]
185 fn sql_config_surfaces_reject_redundant_red_prefixes() {
186 let runtime = RedDBRuntime::in_memory().expect("runtime");
187 for query in ["SET CONFIG red.auth.enabled = true", "SHOW CONFIG red.auth"] {
188 let error = match runtime.execute_query(query) {
189 Ok(_) => panic!("legacy config key unexpectedly accepted: {query}"),
190 Err(error) => error,
191 };
192 assert!(
193 error
194 .to_string()
195 .contains("omit the redundant `red.` prefix"),
196 "unexpected error for {query}: {error}"
197 );
198 }
199 }
200}
201
202pub(crate) fn insert_config_json_path(
203 root: &mut crate::serde_json::Value,
204 path: &str,
205 value: crate::serde_json::Value,
206) {
207 let segments: Vec<&str> = path
208 .split('.')
209 .filter(|segment| !segment.is_empty())
210 .collect();
211 insert_config_json_segments(root, &segments, value);
212}
213
214fn insert_config_json_segments(
215 root: &mut crate::serde_json::Value,
216 segments: &[&str],
217 value: crate::serde_json::Value,
218) {
219 if segments.is_empty() {
220 *root = value;
221 return;
222 }
223
224 if !matches!(root, crate::serde_json::Value::Object(_)) {
225 *root = crate::serde_json::Value::Object(crate::serde_json::Map::new());
226 }
227
228 let crate::serde_json::Value::Object(map) = root else {
229 return;
230 };
231 if segments.len() == 1 {
232 map.insert(segments[0].to_string(), value);
233 return;
234 }
235 let entry = map
236 .entry(segments[0].to_string())
237 .or_insert_with(|| crate::serde_json::Value::Object(crate::serde_json::Map::new()));
238 insert_config_json_segments(entry, &segments[1..], value);
239}
240
241pub(crate) fn show_config_json_result(
242 query: &str,
243 mode: reddb_rql::modes::QueryMode,
244 prefix: &Option<String>,
245 value: crate::serde_json::Value,
246) -> RuntimeQueryResult {
247 let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
248 let mut record = UnifiedRecord::new();
249 record.set(
250 "key",
251 prefix
252 .as_ref()
253 .map(|key| Value::text(key.clone()))
254 .unwrap_or(Value::Null),
255 );
256 record.set("value", Value::Json(value.to_string_compact().into_bytes()));
257 result.push(record);
258 RuntimeQueryResult {
259 query: query.to_string(),
260 mode,
261 statement: "show_config_json",
262 engine: "runtime-config",
263 result,
264 affected_rows: 0,
265 statement_type: "select",
266 bookmark: None,
267 notice: None,
268 }
269}
270
271impl RedDBRuntime {
272 pub(crate) fn set_internal_provider_secret(
273 &self,
274 key: &str,
275 value: Option<String>,
276 ) -> RedDBResult<()> {
277 self.reject_vault_transaction()?;
278 if super::execution_context::current_tenant().is_some() {
279 return Err(RedDBError::Query(
280 "system provider credentials are platform-scoped".into(),
281 ));
282 }
283 let auth =
284 self.inner.auth_store.read().clone().ok_or_else(|| {
285 RedDBError::Query("secret storage requires an enabled vault".into())
286 })?;
287 self.check_secret_write_privilege(&auth, key)?;
288 match value {
289 Some(value) => auth.vault_kv_try_set(key.into(), value),
290 None => auth.vault_kv_try_delete(key).map(|_| ()),
291 }
292 .map_err(|err| RedDBError::Query(err.to_string()))?;
293 self.invalidate_result_cache();
294 Ok(())
295 }
296
297 pub fn vault_kv_get(&self, key: &str) -> Option<String> {
299 self.inner
300 .auth_store
301 .read()
302 .as_ref()
303 .and_then(|store| store.vault_kv_get(key))
304 }
305
306 pub fn vault_kv_try_set(&self, key: String, value: String) -> RedDBResult<()> {
309 let store = self.inner.auth_store.read().clone().ok_or_else(|| {
310 RedDBError::Query("secret storage requires an enabled, unsealed vault".to_string())
311 })?;
312 store
313 .vault_kv_try_set(key, value)
314 .map_err(|err| RedDBError::Query(err.to_string()))
315 }
316
317 pub(crate) fn secret_aes_key(&self) -> Option<[u8; 32]> {
321 let guard = self.inner.auth_store.read();
322 guard.as_ref().and_then(|s| s.vault_secret_key())
323 }
324
325 pub(crate) fn config_bool(&self, key: &str, default: bool) -> bool {
331 if let Some(raw) = self.inner.env_config_overrides.get(key) {
332 if let Some(reddb_types::Value::Boolean(b)) =
333 crate::runtime::config_overlay::coerce_env_value(key, raw)
334 {
335 return b;
336 }
337 }
338 match self.inner.db.store().get_config(key) {
339 Some(Value::Boolean(value)) => value,
340 Some(Value::Text(value)) => matches!(value.as_ref(), "true" | "TRUE" | "True" | "1"),
341 Some(Value::Integer(value)) => value != 0,
342 _ => default,
343 }
344 }
345
346 pub(crate) fn binary_document_body_enabled(&self) -> bool {
351 self.config_bool("storage.binary_document_body", true)
352 }
353
354 pub(crate) fn config_u64(&self, key: &str, default: u64) -> u64 {
355 if let Some(raw) = self.inner.env_config_overrides.get(key) {
356 if let Some(Value::UnsignedInteger(value)) =
357 crate::runtime::config_overlay::coerce_env_value(key, raw)
358 {
359 return value;
360 }
361 }
362 match self.inner.db.store().get_config(key) {
363 Some(Value::Integer(value)) => value as u64,
364 Some(Value::UnsignedInteger(value)) => value,
365 Some(Value::Text(value)) => value.parse::<u64>().unwrap_or(default),
366 _ => default,
367 }
368 }
369
370 pub(crate) fn config_f64(&self, key: &str, default: f64) -> f64 {
371 if let Some(raw) = self.inner.env_config_overrides.get(key) {
372 if let Ok(value) = raw.parse::<f64>() {
373 return value;
374 }
375 }
376 match self.inner.db.store().get_config(key) {
377 Some(Value::Float(value)) => value,
378 Some(Value::Integer(value)) => value as f64,
379 Some(Value::UnsignedInteger(value)) => value as f64,
380 Some(Value::Text(value)) => value.parse::<f64>().unwrap_or(default),
381 _ => default,
382 }
383 }
384
385 pub(crate) fn config_string(&self, key: &str, default: &str) -> String {
386 if let Some(raw) = self.inner.env_config_overrides.get(key) {
387 return raw.clone();
388 }
389 match self.inner.db.store().get_config(key) {
390 Some(Value::Text(value)) => value.to_string(),
391 _ => default.to_string(),
392 }
393 }
394
395 pub(crate) fn secret_auto_encrypt(&self) -> bool {
398 self.config_bool("secret.auto_encrypt", true)
399 }
400}