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.providers.anthropic.tokens.default"
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 notice: None,
140 }
141}
142
143impl RedDBRuntime {
144 pub fn vault_kv_get(&self, key: &str) -> Option<String> {
146 self.inner
147 .auth_store
148 .read()
149 .as_ref()
150 .and_then(|store| store.vault_kv_get(key))
151 }
152
153 pub fn vault_kv_try_set(&self, key: String, value: String) -> RedDBResult<()> {
156 let store = self.inner.auth_store.read().clone().ok_or_else(|| {
157 RedDBError::Query("secret storage requires an enabled, unsealed vault".to_string())
158 })?;
159 store
160 .vault_kv_try_set(key, value)
161 .map_err(|err| RedDBError::Query(err.to_string()))
162 }
163
164 pub(crate) fn secret_aes_key(&self) -> Option<[u8; 32]> {
168 let guard = self.inner.auth_store.read();
169 guard.as_ref().and_then(|s| s.vault_secret_key())
170 }
171
172 pub(crate) fn config_bool(&self, key: &str, default: bool) -> bool {
178 if let Some(raw) = self.inner.env_config_overrides.get(key) {
179 if let Some(crate::storage::schema::Value::Boolean(b)) =
180 crate::runtime::config_overlay::coerce_env_value(key, raw)
181 {
182 return b;
183 }
184 }
185 let store = self.inner.db.store();
186 let Some(manager) = store.get_collection("red_config") else {
187 return default;
188 };
189 let mut result = default;
190 let mut latest_id: u64 = 0;
191 manager.for_each_entity(|entity| {
192 if let Some(row) = entity.data.as_row() {
193 let entry_key = row.get_field("key").and_then(|v| match v {
194 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
195 _ => None,
196 });
197 if entry_key == Some(key) {
198 let id = entity.id.raw();
199 if id >= latest_id {
200 latest_id = id;
201 result = match row.get_field("value") {
202 Some(crate::storage::schema::Value::Boolean(b)) => *b,
203 Some(crate::storage::schema::Value::Text(s)) => {
204 matches!(s.as_ref(), "true" | "TRUE" | "True" | "1")
205 }
206 Some(crate::storage::schema::Value::Integer(n)) => *n != 0,
207 _ => default,
208 };
209 }
210 }
211 }
212 true
213 });
214 result
215 }
216
217 pub(crate) fn binary_document_body_enabled(&self) -> bool {
222 self.config_bool("storage.binary_document_body", true)
223 }
224
225 pub(crate) fn config_u64(&self, key: &str, default: u64) -> u64 {
226 if let Some(raw) = self.inner.env_config_overrides.get(key) {
227 if let Some(crate::storage::schema::Value::UnsignedInteger(n)) =
228 crate::runtime::config_overlay::coerce_env_value(key, raw)
229 {
230 return n;
231 }
232 }
233 let store = self.inner.db.store();
234 let Some(manager) = store.get_collection("red_config") else {
235 return default;
236 };
237 let mut result = default;
238 let mut latest_id: u64 = 0;
239 manager.for_each_entity(|entity| {
240 if let Some(row) = entity.data.as_row() {
241 let entry_key = row.get_field("key").and_then(|v| match v {
242 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
243 _ => None,
244 });
245 if entry_key == Some(key) {
246 let id = entity.id.raw();
247 if id >= latest_id {
248 latest_id = id;
249 result = match row.get_field("value") {
250 Some(crate::storage::schema::Value::Integer(n)) => *n as u64,
251 Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n,
252 Some(crate::storage::schema::Value::Text(s)) => {
253 s.parse::<u64>().unwrap_or(default)
254 }
255 _ => default,
256 };
257 }
258 }
259 }
260 true
261 });
262 result
263 }
264
265 pub(crate) fn config_f64(&self, key: &str, default: f64) -> f64 {
266 if let Some(raw) = self.inner.env_config_overrides.get(key) {
267 if let Ok(n) = raw.parse::<f64>() {
268 return n;
269 }
270 }
271 let store = self.inner.db.store();
272 let Some(manager) = store.get_collection("red_config") else {
273 return default;
274 };
275 let mut result = default;
276 let mut latest_id: u64 = 0;
277 manager.for_each_entity(|entity| {
278 if let Some(row) = entity.data.as_row() {
279 let entry_key = row.get_field("key").and_then(|v| match v {
280 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
281 _ => None,
282 });
283 if entry_key == Some(key) {
284 let id = entity.id.raw();
285 if id >= latest_id {
286 latest_id = id;
287 result = match row.get_field("value") {
288 Some(crate::storage::schema::Value::Float(n)) => *n,
289 Some(crate::storage::schema::Value::Integer(n)) => *n as f64,
290 Some(crate::storage::schema::Value::UnsignedInteger(n)) => *n as f64,
291 Some(crate::storage::schema::Value::Text(s)) => {
292 s.parse::<f64>().unwrap_or(default)
293 }
294 _ => default,
295 };
296 }
297 }
298 }
299 true
300 });
301 result
302 }
303
304 pub(crate) fn config_string(&self, key: &str, default: &str) -> String {
305 if let Some(raw) = self.inner.env_config_overrides.get(key) {
306 return raw.clone();
307 }
308 let store = self.inner.db.store();
309 let Some(manager) = store.get_collection("red_config") else {
310 return default.to_string();
311 };
312 let mut result = default.to_string();
313 let mut latest_id: u64 = 0;
314 manager.for_each_entity(|entity| {
315 if let Some(row) = entity.data.as_row() {
316 let entry_key = row.get_field("key").and_then(|v| match v {
317 crate::storage::schema::Value::Text(s) => Some(s.as_ref()),
318 _ => None,
319 });
320 if entry_key == Some(key) {
321 let id = entity.id.raw();
322 if id >= latest_id {
323 latest_id = id;
324 if let Some(crate::storage::schema::Value::Text(value)) =
325 row.get_field("value")
326 {
327 result = value.to_string();
328 }
329 }
330 }
331 }
332 true
333 });
334 result
335 }
336
337 pub(crate) fn secret_auto_encrypt(&self) -> bool {
340 self.config_bool("red.config.secret.auto_encrypt", true)
341 }
342
343 pub(crate) fn secret_auto_decrypt(&self) -> bool {
348 self.config_bool("red.config.secret.auto_decrypt", true)
349 }
350
351 pub(crate) fn apply_secret_decryption(&self, result: &mut RuntimeQueryResult) {
358 if !self.secret_auto_decrypt() {
359 return;
360 }
361 let Some(key) = self.secret_aes_key() else {
362 return;
363 };
364 for record in result.result.records.iter_mut() {
365 for value in record.values_mut() {
366 if let Value::Secret(ref bytes) = value {
367 if let Some(plain) =
368 super::impl_dml_crypto::decrypt_secret_payload(&key, bytes.as_slice())
369 {
370 if let Ok(text) = String::from_utf8(plain) {
371 *value = Value::text(text);
372 }
373 }
374 }
375 }
376 }
377 }
378}