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
81pub(crate) fn insert_config_json_path(
82 root: &mut crate::serde_json::Value,
83 path: &str,
84 value: crate::serde_json::Value,
85) {
86 let segments: Vec<&str> = path
87 .split('.')
88 .filter(|segment| !segment.is_empty())
89 .collect();
90 insert_config_json_segments(root, &segments, value);
91}
92
93fn insert_config_json_segments(
94 root: &mut crate::serde_json::Value,
95 segments: &[&str],
96 value: crate::serde_json::Value,
97) {
98 if segments.is_empty() {
99 *root = value;
100 return;
101 }
102
103 if !matches!(root, crate::serde_json::Value::Object(_)) {
104 *root = crate::serde_json::Value::Object(crate::serde_json::Map::new());
105 }
106
107 let crate::serde_json::Value::Object(map) = root else {
108 return;
109 };
110 if segments.len() == 1 {
111 map.insert(segments[0].to_string(), value);
112 return;
113 }
114 let entry = map
115 .entry(segments[0].to_string())
116 .or_insert_with(|| crate::serde_json::Value::Object(crate::serde_json::Map::new()));
117 insert_config_json_segments(entry, &segments[1..], value);
118}
119
120pub(crate) fn show_config_json_result(
121 query: &str,
122 mode: reddb_rql::modes::QueryMode,
123 prefix: &Option<String>,
124 value: crate::serde_json::Value,
125) -> RuntimeQueryResult {
126 let mut result = UnifiedResult::with_columns(vec!["key".into(), "value".into()]);
127 let mut record = UnifiedRecord::new();
128 record.set(
129 "key",
130 prefix
131 .as_ref()
132 .map(|key| Value::text(key.clone()))
133 .unwrap_or(Value::Null),
134 );
135 record.set("value", Value::Json(value.to_string_compact().into_bytes()));
136 result.push(record);
137 RuntimeQueryResult {
138 query: query.to_string(),
139 mode,
140 statement: "show_config_json",
141 engine: "runtime-config",
142 result,
143 affected_rows: 0,
144 statement_type: "select",
145 bookmark: None,
146 notice: None,
147 }
148}
149
150impl RedDBRuntime {
151 pub(crate) fn set_internal_provider_secret(
152 &self,
153 key: &str,
154 value: Option<String>,
155 ) -> RedDBResult<()> {
156 self.reject_vault_transaction()?;
157 if super::execution_context::current_tenant().is_some() {
158 return Err(RedDBError::Query(
159 "system provider credentials are platform-scoped".into(),
160 ));
161 }
162 let auth =
163 self.inner.auth_store.read().clone().ok_or_else(|| {
164 RedDBError::Query("secret storage requires an enabled vault".into())
165 })?;
166 self.check_secret_write_privilege(&auth, key)?;
167 match value {
168 Some(value) => auth.vault_kv_try_set(key.into(), value),
169 None => auth.vault_kv_try_delete(key).map(|_| ()),
170 }
171 .map_err(|err| RedDBError::Query(err.to_string()))?;
172 self.invalidate_result_cache();
173 Ok(())
174 }
175
176 pub fn vault_kv_get(&self, key: &str) -> Option<String> {
178 self.inner
179 .auth_store
180 .read()
181 .as_ref()
182 .and_then(|store| store.vault_kv_get(key))
183 }
184
185 pub fn vault_kv_try_set(&self, key: String, value: String) -> RedDBResult<()> {
188 let store = self.inner.auth_store.read().clone().ok_or_else(|| {
189 RedDBError::Query("secret storage requires an enabled, unsealed vault".to_string())
190 })?;
191 store
192 .vault_kv_try_set(key, value)
193 .map_err(|err| RedDBError::Query(err.to_string()))
194 }
195
196 pub(crate) fn secret_aes_key(&self) -> Option<[u8; 32]> {
200 let guard = self.inner.auth_store.read();
201 guard.as_ref().and_then(|s| s.vault_secret_key())
202 }
203
204 pub(crate) fn config_bool(&self, key: &str, default: bool) -> bool {
210 if let Some(raw) = self.inner.env_config_overrides.get(key) {
211 if let Some(reddb_types::Value::Boolean(b)) =
212 crate::runtime::config_overlay::coerce_env_value(key, raw)
213 {
214 return b;
215 }
216 }
217 let store = self.inner.db.store();
218 let Some(manager) = store.get_collection("red_config") else {
219 return default;
220 };
221 let mut result = default;
222 let mut latest_id: u64 = 0;
223 manager.for_each_entity(|entity| {
224 if let Some(row) = entity.data.as_row() {
225 let entry_key = row.get_field("key").and_then(|v| match v {
226 reddb_types::Value::Text(s) => Some(s.as_ref()),
227 _ => None,
228 });
229 if entry_key == Some(key) {
230 let id = entity.id.raw();
231 if id >= latest_id {
232 latest_id = id;
233 result = match row.get_field("value") {
234 Some(reddb_types::Value::Boolean(b)) => *b,
235 Some(reddb_types::Value::Text(s)) => {
236 matches!(s.as_ref(), "true" | "TRUE" | "True" | "1")
237 }
238 Some(reddb_types::Value::Integer(n)) => *n != 0,
239 _ => default,
240 };
241 }
242 }
243 }
244 true
245 });
246 result
247 }
248
249 pub(crate) fn binary_document_body_enabled(&self) -> bool {
254 self.config_bool("storage.binary_document_body", true)
255 }
256
257 pub(crate) fn config_u64(&self, key: &str, default: u64) -> u64 {
258 if let Some(raw) = self.inner.env_config_overrides.get(key) {
259 if let Some(reddb_types::Value::UnsignedInteger(n)) =
260 crate::runtime::config_overlay::coerce_env_value(key, raw)
261 {
262 return n;
263 }
264 }
265 let store = self.inner.db.store();
266 let Some(manager) = store.get_collection("red_config") else {
267 return default;
268 };
269 let mut result = default;
270 let mut latest_id: u64 = 0;
271 manager.for_each_entity(|entity| {
272 if let Some(row) = entity.data.as_row() {
273 let entry_key = row.get_field("key").and_then(|v| match v {
274 reddb_types::Value::Text(s) => Some(s.as_ref()),
275 _ => None,
276 });
277 if entry_key == Some(key) {
278 let id = entity.id.raw();
279 if id >= latest_id {
280 latest_id = id;
281 result = match row.get_field("value") {
282 Some(reddb_types::Value::Integer(n)) => *n as u64,
283 Some(reddb_types::Value::UnsignedInteger(n)) => *n,
284 Some(reddb_types::Value::Text(s)) => {
285 s.parse::<u64>().unwrap_or(default)
286 }
287 _ => default,
288 };
289 }
290 }
291 }
292 true
293 });
294 result
295 }
296
297 pub(crate) fn config_f64(&self, key: &str, default: f64) -> f64 {
298 if let Some(raw) = self.inner.env_config_overrides.get(key) {
299 if let Ok(n) = raw.parse::<f64>() {
300 return n;
301 }
302 }
303 let store = self.inner.db.store();
304 let Some(manager) = store.get_collection("red_config") else {
305 return default;
306 };
307 let mut result = default;
308 let mut latest_id: u64 = 0;
309 manager.for_each_entity(|entity| {
310 if let Some(row) = entity.data.as_row() {
311 let entry_key = row.get_field("key").and_then(|v| match v {
312 reddb_types::Value::Text(s) => Some(s.as_ref()),
313 _ => None,
314 });
315 if entry_key == Some(key) {
316 let id = entity.id.raw();
317 if id >= latest_id {
318 latest_id = id;
319 result = match row.get_field("value") {
320 Some(reddb_types::Value::Float(n)) => *n,
321 Some(reddb_types::Value::Integer(n)) => *n as f64,
322 Some(reddb_types::Value::UnsignedInteger(n)) => *n as f64,
323 Some(reddb_types::Value::Text(s)) => {
324 s.parse::<f64>().unwrap_or(default)
325 }
326 _ => default,
327 };
328 }
329 }
330 }
331 true
332 });
333 result
334 }
335
336 pub(crate) fn config_string(&self, key: &str, default: &str) -> String {
337 if let Some(raw) = self.inner.env_config_overrides.get(key) {
338 return raw.clone();
339 }
340 let store = self.inner.db.store();
341 let Some(manager) = store.get_collection("red_config") else {
342 return default.to_string();
343 };
344 let mut result = default.to_string();
345 let mut latest_id: u64 = 0;
346 manager.for_each_entity(|entity| {
347 if let Some(row) = entity.data.as_row() {
348 let entry_key = row.get_field("key").and_then(|v| match v {
349 reddb_types::Value::Text(s) => Some(s.as_ref()),
350 _ => None,
351 });
352 if entry_key == Some(key) {
353 let id = entity.id.raw();
354 if id >= latest_id {
355 latest_id = id;
356 if let Some(reddb_types::Value::Text(value)) = row.get_field("value") {
357 result = value.to_string();
358 }
359 }
360 }
361 }
362 true
363 });
364 result
365 }
366
367 pub(crate) fn secret_auto_encrypt(&self) -> bool {
370 self.config_bool("red.config.secret.auto_encrypt", true)
371 }
372}