1use 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 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 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 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 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 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 pub(crate) fn secret_auto_encrypt(&self) -> bool {
339 self.config_bool("red.config.secret.auto_encrypt", true)
340 }
341
342 pub(crate) fn secret_auto_decrypt(&self) -> bool {
347 self.config_bool("red.config.secret.auto_decrypt", true)
348 }
349
350 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}