1use std::collections::HashMap;
4use std::sync::Arc;
5
6use crate::catalog::{CollectionModel, SchemaMode};
7use crate::physical::{CollectionContract, ContractOrigin};
8use crate::storage::{EntityData, EntityId, EntityKind, RowData, UnifiedEntity};
9use reddb_rql::ast::ConfigValueType;
10
11use super::impl_core::{current_auth_identity, current_connection_id, current_tenant};
12use super::*;
13
14const CONFIG_HISTORY_LIMIT: usize = 16;
15
16#[derive(Clone)]
17struct ConfigVersion {
18 id: EntityId,
19 key: String,
20 version: i64,
21 value: Value,
22 tombstone: bool,
23 created_at_ms: i64,
24 op: String,
25 value_type: Option<ConfigValueType>,
26 schema_version: Option<i64>,
27 tags: Vec<String>,
28}
29
30impl super::keyed_spine::KeyedVersion for ConfigVersion {
31 fn key(&self) -> &str {
32 &self.key
33 }
34
35 fn version(&self) -> i64 {
36 self.version
37 }
38}
39
40impl ConfigVersion {
41 fn from_keyed_row(version: super::keyed_spine::KeyedRowVersion, row: &RowData) -> Self {
42 Self {
43 id: version.id,
44 key: version.key,
45 version: version.version,
46 value: version.value,
47 tombstone: version.tombstone,
48 created_at_ms: version.created_at_ms,
49 op: version.op,
50 value_type: row
51 .get_field("value_type")
52 .and_then(config_value_type_from_value),
53 schema_version: super::keyed_spine::value_i64(row.get_field("schema_version")),
54 tags: config_tags_from_value(row.get_field("tags")),
55 }
56 }
57}
58
59struct ConfigSecretRef {
60 collection: String,
61 key: String,
62}
63
64struct ConfigMutationEvidence {
65 id: String,
66 resource_type: String,
67 managed: bool,
68 mutability: crate::auth::registry::Mutability,
69 matched_action: Option<String>,
70 matched_resource: Option<String>,
71 payload: Option<Value>,
72}
73
74enum ConfigMutationAuthz {
75 Allowed(ConfigMutationEvidence),
76 Denied {
77 reason: String,
78 evidence: ConfigMutationEvidence,
79 },
80}
81
82impl RedDBRuntime {
83 pub fn execute_config_command(
84 &self,
85 raw_query: &str,
86 cmd: &reddb_rql::ast::ConfigCommand,
87 ) -> RedDBResult<RuntimeQueryResult> {
88 use reddb_rql::ast::ConfigCommand;
89
90 match cmd {
91 ConfigCommand::Put {
92 collection,
93 key,
94 value,
95 value_type,
96 tags,
97 } => self.config_write_result(
98 raw_query,
99 collection,
100 key,
101 value.clone(),
102 *value_type,
103 tags,
104 "put",
105 ),
106 ConfigCommand::Rotate {
107 collection,
108 key,
109 value,
110 value_type,
111 tags,
112 } => self.config_write_result(
113 raw_query,
114 collection,
115 key,
116 value.clone(),
117 *value_type,
118 tags,
119 "rotate",
120 ),
121 ConfigCommand::Get { collection, key } => {
122 self.config_get_result(raw_query, collection, key)
123 }
124 ConfigCommand::Resolve { collection, key } => {
125 self.config_resolve_result(raw_query, collection, key)
126 }
127 ConfigCommand::Delete { collection, key } => {
128 self.config_delete_result(raw_query, collection, key)
129 }
130 ConfigCommand::History { collection, key } => {
131 self.config_history_result(raw_query, collection, key)
132 }
133 ConfigCommand::List {
134 collection,
135 prefix,
136 limit,
137 offset,
138 } => self.config_list_result(raw_query, collection, prefix.as_deref(), *limit, *offset),
139 ConfigCommand::Watch {
140 collection,
141 key,
142 prefix,
143 from_lsn,
144 } => self.config_watch_result(raw_query, collection, key, *prefix, *from_lsn),
145 ConfigCommand::InvalidVolatileOperation { operation, .. } => {
146 Err(invalid_config_volatility(operation))
147 }
148 }
149 }
150
151 pub(crate) fn validate_config_command_before_auth(
152 &self,
153 cmd: &reddb_rql::ast::ConfigCommand,
154 ) -> RedDBResult<()> {
155 use reddb_rql::ast::ConfigCommand;
156 match cmd {
157 ConfigCommand::Put {
158 collection, key, ..
159 }
160 | ConfigCommand::Get { collection, key }
161 | ConfigCommand::Resolve { collection, key }
162 | ConfigCommand::Rotate {
163 collection, key, ..
164 }
165 | ConfigCommand::Delete { collection, key }
166 | ConfigCommand::History { collection, key }
167 | ConfigCommand::Watch {
168 collection, key, ..
169 } => validate_red_config_content_key(collection, key)?,
170 ConfigCommand::List {
171 collection,
172 prefix: Some(prefix),
173 ..
174 } => validate_red_config_content_key(collection, prefix)?,
175 ConfigCommand::List { prefix: None, .. }
176 | ConfigCommand::InvalidVolatileOperation { .. } => {}
177 }
178 match cmd {
179 ConfigCommand::InvalidVolatileOperation { operation, .. } => {
180 Err(invalid_config_volatility(operation))
181 }
182 ConfigCommand::Put { collection, .. }
183 | ConfigCommand::Get { collection, .. }
184 | ConfigCommand::Resolve { collection, .. }
185 | ConfigCommand::Rotate { collection, .. }
186 | ConfigCommand::Delete { collection, .. }
187 | ConfigCommand::History { collection, .. }
188 | ConfigCommand::List { collection, .. }
189 | ConfigCommand::Watch { collection, .. } => {
190 let snapshot = self.inner.db.catalog_model_snapshot();
191 let Some(actual_model) = snapshot
192 .collections
193 .iter()
194 .find(|c| c.name == *collection)
195 .map(|c| c.declared_model.unwrap_or(c.model))
196 else {
197 return Ok(());
198 };
199 crate::runtime::ddl::polymorphic_resolver::ensure_model_match(
200 CollectionModel::Config,
201 actual_model,
202 )
203 }
204 }
205 }
206
207 fn config_resolve_result(
208 &self,
209 raw_query: &str,
210 collection: &str,
211 key: &str,
212 ) -> RedDBResult<RuntimeQueryResult> {
213 let latest = self.latest_config_version(collection, key)?;
214 if let Err(reason) = self.check_config_capability("config:read", collection, key) {
215 self.audit_config_resolve(
216 collection,
217 key,
218 None,
219 crate::runtime::audit_log::Outcome::Denied,
220 &reason,
221 );
222 return Err(RedDBError::Query(reason));
223 }
224
225 let Some(version) = latest else {
226 let reason = "not_found";
227 self.audit_config_resolve(
228 collection,
229 key,
230 None,
231 crate::runtime::audit_log::Outcome::Denied,
232 reason,
233 );
234 return Err(RedDBError::NotFound(format!(
235 "config '{}.{}' not found",
236 collection, key
237 )));
238 };
239 if version.tombstone {
240 let reason = "deleted";
241 self.audit_config_resolve(
242 collection,
243 key,
244 None,
245 crate::runtime::audit_log::Outcome::Denied,
246 reason,
247 );
248 return Err(RedDBError::NotFound(format!(
249 "config '{}.{}' is deleted",
250 collection, key
251 )));
252 }
253
254 let secret_ref = parse_config_secret_ref(&version.value).inspect_err(|err| {
255 self.audit_config_resolve(
256 collection,
257 key,
258 None,
259 crate::runtime::audit_log::Outcome::Error,
260 &err.to_string(),
261 );
262 })?;
263
264 match self.resolve_vault_secret_value(&secret_ref.collection, &secret_ref.key) {
265 Ok(value) => {
266 if value_looks_like_secret_ref(&value) {
267 let err = secret_ref_chain_error(
268 collection,
269 key,
270 &secret_ref.collection,
271 &secret_ref.key,
272 );
273 let reason = err.to_string();
274 self.audit_config_resolve(
275 collection,
276 key,
277 Some(&secret_ref),
278 crate::runtime::audit_log::Outcome::Error,
279 &reason,
280 );
281 return Err(err);
282 }
283 self.audit_config_resolve(
284 collection,
285 key,
286 Some(&secret_ref),
287 crate::runtime::audit_log::Outcome::Success,
288 "ok",
289 );
290 let mut result = UnifiedResult::with_columns(vec![
291 "collection".into(),
292 "key".into(),
293 "value".into(),
294 "resolved_store".into(),
295 "resolved_collection".into(),
296 "resolved_key".into(),
297 ]);
298 let mut record = UnifiedRecord::new();
299 record.set("collection", Value::text(collection.to_string()));
300 record.set("key", Value::text(key.to_string()));
301 record.set("value", value);
302 record.set("resolved_store", Value::text("vault"));
303 record.set("resolved_collection", Value::text(secret_ref.collection));
304 record.set("resolved_key", Value::text(secret_ref.key));
305 result.push(record);
306 Ok(RuntimeQueryResult {
307 query: raw_query.to_string(),
308 mode: reddb_rql::modes::QueryMode::Sql,
309 statement: "config_resolve",
310 engine: "config",
311 result,
312 affected_rows: 0,
313 statement_type: "select",
314 bookmark: None,
315 notice: None,
316 })
317 }
318 Err(err) => {
319 let reason = err.to_string();
320 let outcome = if reason.contains("denied") {
321 crate::runtime::audit_log::Outcome::Denied
322 } else {
323 crate::runtime::audit_log::Outcome::Error
324 };
325 self.audit_config_resolve(collection, key, Some(&secret_ref), outcome, &reason);
326 Err(err)
327 }
328 }
329 }
330
331 fn config_write_result(
332 &self,
333 raw_query: &str,
334 collection: &str,
335 key: &str,
336 value: Value,
337 requested_type: Option<ConfigValueType>,
338 tags: &[String],
339 op: &str,
340 ) -> RedDBResult<RuntimeQueryResult> {
341 let mut evidence = match self.authorize_config_write_for_event(collection, key) {
342 ConfigMutationAuthz::Allowed(evidence) => evidence,
343 ConfigMutationAuthz::Denied {
344 reason,
345 mut evidence,
346 } => {
347 evidence.payload = Some(value.clone());
348 let _ = self.emit_config_mutation_event(
349 crate::runtime::control_events::EventKind::ConfigWrite,
350 crate::runtime::control_events::Outcome::Denied,
351 "config:write",
352 collection,
353 key,
354 Some(reason.clone()),
355 &evidence,
356 );
357 return Err(RedDBError::Query(reason));
358 }
359 };
360 if let Err(err) = self.check_write(crate::runtime::write_gate::WriteKind::Dml) {
361 let _ = self.emit_config_mutation_event(
362 crate::runtime::control_events::EventKind::ConfigWrite,
363 crate::runtime::control_events::Outcome::Error,
364 "config:write",
365 collection,
366 key,
367 Some(err.to_string()),
368 &evidence,
369 );
370 return Err(err);
371 }
372 if is_enforcement_mode_config(collection, key) {
377 if let Err(err) = validate_enforcement_mode_value(&value) {
378 let _ = self.emit_config_mutation_event(
379 crate::runtime::control_events::EventKind::ConfigWrite,
380 crate::runtime::control_events::Outcome::Denied,
381 "config:write",
382 collection,
383 key,
384 Some(err.to_string()),
385 &evidence,
386 );
387 return Err(err);
388 }
389 }
390 if let Err(err) = self.ensure_config_collection(collection) {
391 let _ = self.emit_config_mutation_event(
392 crate::runtime::control_events::EventKind::ConfigWrite,
393 crate::runtime::control_events::Outcome::Error,
394 "config:write",
395 collection,
396 key,
397 Some(err.to_string()),
398 &evidence,
399 );
400 return Err(err);
401 }
402 let latest = match self.latest_config_version(collection, key) {
403 Ok(latest) => latest,
404 Err(err) => {
405 let _ = self.emit_config_mutation_event(
406 crate::runtime::control_events::EventKind::ConfigWrite,
407 crate::runtime::control_events::Outcome::Error,
408 "config:write",
409 collection,
410 key,
411 Some(err.to_string()),
412 &evidence,
413 );
414 return Err(err);
415 }
416 };
417 let version = latest.as_ref().map(|version| version.version).unwrap_or(0) + 1;
418 let (value_type, schema_version) = resolve_config_schema(latest.as_ref(), requested_type);
419 if let Some(value_type) = value_type {
420 if let Err(err) = validate_config_value_type(&value, value_type) {
421 let _ = self.emit_config_mutation_event(
422 crate::runtime::control_events::EventKind::ConfigWrite,
423 crate::runtime::control_events::Outcome::Error,
424 "config:write",
425 collection,
426 key,
427 Some(err.to_string()),
428 &evidence,
429 );
430 return Err(err);
431 }
432 }
433 evidence.payload = Some(value.clone());
434 if let Some(reason) = self.secret_ref_guard_write_check(collection, key, &value) {
435 let _ = self.emit_config_mutation_event(
436 crate::runtime::control_events::EventKind::ConfigWrite,
437 crate::runtime::control_events::Outcome::Denied,
438 "config:write",
439 collection,
440 key,
441 Some(reason.to_string()),
442 &evidence,
443 );
444 return Err(reason);
445 }
446 let before = latest.as_ref().and_then(|version| {
447 if version.tombstone {
448 None
449 } else {
450 Some(reddb_types::Value::to_json(&version.value))
451 }
452 });
453 let after = Some(reddb_types::Value::to_json(&value));
454 let change_op = if latest.is_some() {
455 crate::replication::cdc::ChangeOperation::Update
456 } else {
457 crate::replication::cdc::ChangeOperation::Insert
458 };
459 let id = match self.append_config_version(
460 collection,
461 key,
462 value,
463 version,
464 false,
465 op,
466 value_type,
467 schema_version,
468 tags,
469 ) {
470 Ok(id) => id,
471 Err(err) => {
472 let _ = self.emit_config_mutation_event(
473 crate::runtime::control_events::EventKind::ConfigWrite,
474 crate::runtime::control_events::Outcome::Error,
475 "config:write",
476 collection,
477 key,
478 Some(err.to_string()),
479 &evidence,
480 );
481 return Err(err);
482 }
483 };
484 self.record_kv_watch_event(change_op, collection, key, id.raw(), before, after);
485 if let Err(err) = self.prune_config_history(collection, key) {
486 let _ = self.emit_config_mutation_event(
487 crate::runtime::control_events::EventKind::ConfigWrite,
488 crate::runtime::control_events::Outcome::Error,
489 "config:write",
490 collection,
491 key,
492 Some(err.to_string()),
493 &evidence,
494 );
495 return Err(err);
496 }
497 self.invalidate_result_cache();
498 if let Err(err) = self.emit_config_mutation_event(
499 crate::runtime::control_events::EventKind::ConfigWrite,
500 crate::runtime::control_events::Outcome::Allowed,
501 "config:write",
502 collection,
503 key,
504 None,
505 &evidence,
506 ) {
507 let _ = self.inner.db.store().delete(collection, id);
508 self.invalidate_result_cache();
509 return Err(err);
510 }
511 if is_enforcement_mode_config(collection, key) {
515 if let Some(auth_store) = self.inner.auth_store.read().clone() {
516 if let Value::Text(text) =
517 &evidence.payload.as_ref().cloned().unwrap_or(Value::Null)
518 {
519 if let Some(mode) =
520 crate::auth::enforcement_mode::PolicyEnforcementMode::parse(text)
521 {
522 auth_store.set_enforcement_mode(mode);
523 }
524 }
525 }
526 }
527 Ok(config_write_output(
528 raw_query,
529 collection,
530 key,
531 version,
532 id,
533 value_type,
534 schema_version,
535 tags,
536 match op {
537 "rotate" => "config_rotate",
538 _ => "config_put",
539 },
540 1,
541 ))
542 }
543
544 fn config_delete_result(
545 &self,
546 raw_query: &str,
547 collection: &str,
548 key: &str,
549 ) -> RedDBResult<RuntimeQueryResult> {
550 let mut evidence = match self.authorize_config_write_for_event(collection, key) {
551 ConfigMutationAuthz::Allowed(evidence) => evidence,
552 ConfigMutationAuthz::Denied { reason, evidence } => {
553 let _ = self.emit_config_mutation_event(
554 crate::runtime::control_events::EventKind::ConfigDelete,
555 crate::runtime::control_events::Outcome::Denied,
556 "config:delete",
557 collection,
558 key,
559 Some(reason.clone()),
560 &evidence,
561 );
562 return Err(RedDBError::Query(reason));
563 }
564 };
565 if let Err(err) = self.check_write(crate::runtime::write_gate::WriteKind::Dml) {
566 let _ = self.emit_config_mutation_event(
567 crate::runtime::control_events::EventKind::ConfigDelete,
568 crate::runtime::control_events::Outcome::Error,
569 "config:delete",
570 collection,
571 key,
572 Some(err.to_string()),
573 &evidence,
574 );
575 return Err(err);
576 }
577 if let Err(err) = self.ensure_config_collection(collection) {
578 let _ = self.emit_config_mutation_event(
579 crate::runtime::control_events::EventKind::ConfigDelete,
580 crate::runtime::control_events::Outcome::Error,
581 "config:delete",
582 collection,
583 key,
584 Some(err.to_string()),
585 &evidence,
586 );
587 return Err(err);
588 }
589 let latest = match self.latest_config_version(collection, key) {
590 Ok(latest) => latest,
591 Err(err) => {
592 let _ = self.emit_config_mutation_event(
593 crate::runtime::control_events::EventKind::ConfigDelete,
594 crate::runtime::control_events::Outcome::Error,
595 "config:delete",
596 collection,
597 key,
598 Some(err.to_string()),
599 &evidence,
600 );
601 return Err(err);
602 }
603 };
604 evidence.payload = latest.as_ref().map(|version| version.value.clone());
605 let version = latest.as_ref().map(|version| version.version).unwrap_or(0) + 1;
606 let value_type = latest.as_ref().and_then(|version| version.value_type);
607 let schema_version = latest.as_ref().and_then(|version| version.schema_version);
608 let id = match self.append_config_version(
609 collection,
610 key,
611 Value::Null,
612 version,
613 true,
614 "delete",
615 value_type,
616 schema_version,
617 &[],
618 ) {
619 Ok(id) => id,
620 Err(err) => {
621 let _ = self.emit_config_mutation_event(
622 crate::runtime::control_events::EventKind::ConfigDelete,
623 crate::runtime::control_events::Outcome::Error,
624 "config:delete",
625 collection,
626 key,
627 Some(err.to_string()),
628 &evidence,
629 );
630 return Err(err);
631 }
632 };
633 if let Some(before) = latest.as_ref().and_then(|version| {
634 if version.tombstone {
635 None
636 } else {
637 Some(reddb_types::Value::to_json(&version.value))
638 }
639 }) {
640 self.record_kv_watch_event(
641 crate::replication::cdc::ChangeOperation::Delete,
642 collection,
643 key,
644 id.raw(),
645 Some(before),
646 None,
647 );
648 }
649 if let Err(err) = self.prune_config_history(collection, key) {
650 let _ = self.emit_config_mutation_event(
651 crate::runtime::control_events::EventKind::ConfigDelete,
652 crate::runtime::control_events::Outcome::Error,
653 "config:delete",
654 collection,
655 key,
656 Some(err.to_string()),
657 &evidence,
658 );
659 return Err(err);
660 }
661 self.invalidate_result_cache();
662 if let Err(err) = self.emit_config_mutation_event(
663 crate::runtime::control_events::EventKind::ConfigDelete,
664 crate::runtime::control_events::Outcome::Allowed,
665 "config:delete",
666 collection,
667 key,
668 None,
669 &evidence,
670 ) {
671 let _ = self.inner.db.store().delete(collection, id);
672 self.invalidate_result_cache();
673 return Err(err);
674 }
675 Ok(config_write_output(
676 raw_query,
677 collection,
678 key,
679 version,
680 id,
681 value_type,
682 schema_version,
683 &[],
684 "delete",
685 1,
686 ))
687 }
688
689 fn config_get_result(
690 &self,
691 raw_query: &str,
692 collection: &str,
693 key: &str,
694 ) -> RedDBResult<RuntimeQueryResult> {
695 self.check_system_config_capability("config:read", collection, key)
696 .map_err(RedDBError::Query)?;
697 let latest = self.latest_config_version(collection, key)?;
698 let mut result = UnifiedResult::with_columns(vec![
699 "collection".into(),
700 "key".into(),
701 "value".into(),
702 "version".into(),
703 "value_type".into(),
704 "schema_version".into(),
705 "tags".into(),
706 "tombstone".into(),
707 ]);
708 let mut record = UnifiedRecord::new();
709 record.set("collection", Value::text(collection.to_string()));
710 record.set("key", Value::text(key.to_string()));
711 if let Some(version) = latest {
712 record.set("value", version.value);
713 record.set("version", Value::Integer(version.version));
714 record.set("value_type", config_value_type_value(version.value_type));
715 record.set(
716 "schema_version",
717 version
718 .schema_version
719 .map(Value::Integer)
720 .unwrap_or(Value::Null),
721 );
722 record.set("tags", config_tags_value(&version.tags));
723 record.set("tombstone", Value::Boolean(version.tombstone));
724 } else {
725 record.set("value", Value::Null);
726 record.set("version", Value::Null);
727 record.set("value_type", Value::Null);
728 record.set("schema_version", Value::Null);
729 record.set("tags", Value::Null);
730 record.set("tombstone", Value::Boolean(false));
731 }
732 result.push(record);
733 Ok(RuntimeQueryResult {
734 query: raw_query.to_string(),
735 mode: reddb_rql::modes::QueryMode::Sql,
736 statement: "config_get",
737 engine: "config",
738 result,
739 affected_rows: 0,
740 statement_type: "select",
741 bookmark: None,
742 notice: None,
743 })
744 }
745
746 fn config_history_result(
747 &self,
748 raw_query: &str,
749 collection: &str,
750 key: &str,
751 ) -> RedDBResult<RuntimeQueryResult> {
752 self.check_system_config_capability("config:read", collection, key)
753 .map_err(RedDBError::Query)?;
754 let versions = super::keyed_spine::history_versions(self.config_versions(collection, key)?);
755 let mut result = UnifiedResult::with_columns(vec![
756 "collection".into(),
757 "key".into(),
758 "version".into(),
759 "value".into(),
760 "value_type".into(),
761 "schema_version".into(),
762 "tags".into(),
763 "tombstone".into(),
764 "op".into(),
765 "created_at_ms".into(),
766 ]);
767 for version in versions {
768 let mut record = UnifiedRecord::new();
769 record.set("collection", Value::text(collection.to_string()));
770 record.set("key", Value::text(key.to_string()));
771 record.set("version", Value::Integer(version.version));
772 record.set("value", version.value);
773 record.set("value_type", config_value_type_value(version.value_type));
774 record.set(
775 "schema_version",
776 version
777 .schema_version
778 .map(Value::Integer)
779 .unwrap_or(Value::Null),
780 );
781 record.set("tags", Value::Null);
782 record.set("tombstone", Value::Boolean(version.tombstone));
783 record.set("op", Value::text(version.op));
784 record.set("created_at_ms", Value::Integer(version.created_at_ms));
785 result.push(record);
786 }
787 Ok(RuntimeQueryResult {
788 query: raw_query.to_string(),
789 mode: reddb_rql::modes::QueryMode::Sql,
790 statement: "config_history",
791 engine: "config",
792 result,
793 affected_rows: 0,
794 statement_type: "select",
795 bookmark: None,
796 notice: None,
797 })
798 }
799
800 fn config_list_result(
801 &self,
802 raw_query: &str,
803 collection: &str,
804 prefix: Option<&str>,
805 limit: Option<usize>,
806 offset: usize,
807 ) -> RedDBResult<RuntimeQueryResult> {
808 let mut versions = self.latest_config_versions(collection, prefix)?;
809 versions.sort_by(|left, right| left.key.cmp(&right.key));
810 let mut result = UnifiedResult::with_columns(vec![
811 "collection".into(),
812 "key".into(),
813 "value".into(),
814 "version".into(),
815 "value_type".into(),
816 "schema_version".into(),
817 "tags".into(),
818 "tombstone".into(),
819 "op".into(),
820 "created_at_ms".into(),
821 ]);
822 for version in versions
823 .into_iter()
824 .filter(|version| {
825 self.check_config_capability("config:read", collection, &version.key)
826 .is_ok()
827 })
828 .skip(offset)
829 .take(limit.unwrap_or(usize::MAX))
830 {
831 let mut record = UnifiedRecord::new();
832 record.set("collection", Value::text(collection.to_string()));
833 record.set("key", Value::text(version.key));
834 record.set("value", version.value);
835 record.set("version", Value::Integer(version.version));
836 record.set("value_type", config_value_type_value(version.value_type));
837 record.set(
838 "schema_version",
839 version
840 .schema_version
841 .map(Value::Integer)
842 .unwrap_or(Value::Null),
843 );
844 record.set("tags", config_tags_value(&version.tags));
845 record.set("tombstone", Value::Boolean(version.tombstone));
846 record.set("op", Value::text(version.op));
847 record.set("created_at_ms", Value::Integer(version.created_at_ms));
848 result.push(record);
849 }
850 Ok(RuntimeQueryResult {
851 query: raw_query.to_string(),
852 mode: reddb_rql::modes::QueryMode::Sql,
853 statement: "config_list",
854 engine: "config",
855 result,
856 affected_rows: 0,
857 statement_type: "select",
858 bookmark: None,
859 notice: None,
860 })
861 }
862
863 fn config_watch_result(
864 &self,
865 raw_query: &str,
866 collection: &str,
867 key: &str,
868 prefix: bool,
869 from_lsn: Option<u64>,
870 ) -> RedDBResult<RuntimeQueryResult> {
871 let watch_key = if prefix {
872 format!("{key}.*")
873 } else {
874 key.to_string()
875 };
876 let endpoint = match from_lsn {
877 Some(lsn) => {
878 format!("/collections/{collection}/config/{watch_key}/watch?since_lsn={lsn}")
879 }
880 None => format!("/collections/{collection}/config/{watch_key}/watch"),
881 };
882 let mut result = UnifiedResult::with_columns(vec![
883 "collection".into(),
884 "key".into(),
885 "prefix".into(),
886 "from_lsn".into(),
887 "watch_url".into(),
888 "streaming".into(),
889 ]);
890 let mut record = UnifiedRecord::new();
891 record.set("collection", Value::text(collection.to_string()));
892 record.set("key", Value::text(watch_key));
893 record.set("prefix", Value::Boolean(prefix));
894 record.set(
895 "from_lsn",
896 from_lsn
897 .map(Value::UnsignedInteger)
898 .unwrap_or(reddb_types::Value::Null),
899 );
900 record.set("watch_url", Value::text(endpoint));
901 record.set("streaming", Value::Boolean(true));
902 result.push(record);
903 Ok(RuntimeQueryResult {
904 query: raw_query.to_string(),
905 mode: reddb_rql::modes::QueryMode::Sql,
906 statement: "config_watch",
907 engine: "config",
908 result,
909 affected_rows: 0,
910 statement_type: "stream",
911 bookmark: None,
912 notice: None,
913 })
914 }
915
916 fn ensure_config_collection(&self, collection: &str) -> RedDBResult<()> {
917 let store = self.inner.db.store();
918 if store.get_collection(collection).is_none() {
919 store
920 .create_collection(collection)
921 .map_err(|err| RedDBError::Internal(err.to_string()))?;
922 }
923 if let Some(contract) = self.inner.db.collection_contract(collection) {
924 crate::runtime::ddl::polymorphic_resolver::ensure_model_match(
925 CollectionModel::Config,
926 contract.declared_model,
927 )?;
928 return Ok(());
929 }
930 let now = current_unix_ms();
931 self.inner
932 .db
933 .save_collection_contract(CollectionContract {
934 name: collection.to_string(),
935 description: None,
936 description_generated: false,
937 declared_model: CollectionModel::Config,
938 schema_mode: SchemaMode::Dynamic,
939 origin: ContractOrigin::Explicit,
940 version: 1,
941 created_at_unix_ms: now as u128,
942 updated_at_unix_ms: now as u128,
943 default_ttl_ms: None,
944 vector_dimension: None,
945 vector_metric: None,
946 context_index_fields: Vec::new(),
947 declared_columns: Vec::new(),
948 table_def: None,
949 timestamps_enabled: false,
950 context_index_enabled: false,
951 metrics_raw_retention_ms: None,
952 metrics_rollup_policies: Vec::new(),
953 metrics_tenant_identity: None,
954 metrics_namespace: None,
955 append_only: false,
956 subscriptions: Vec::new(),
957 analytics_config: Vec::new(),
958 session_key: None,
959 session_gap_ms: None,
960 retention_duration_ms: None,
961 analytical_storage: None,
962
963 ai_policy: None,
964 })
965 .map(|_| ())
966 .map_err(|err| RedDBError::Internal(err.to_string()))
967 }
968
969 fn append_config_version(
970 &self,
971 collection: &str,
972 key: &str,
973 value: Value,
974 version: i64,
975 tombstone: bool,
976 op: &str,
977 value_type: Option<ConfigValueType>,
978 schema_version: Option<i64>,
979 tags: &[String],
980 ) -> RedDBResult<EntityId> {
981 let now = current_unix_ms() as i64;
982 let fields = vec![
983 ("key".to_string(), Value::text(key.to_string())),
984 ("value".to_string(), value),
985 ("version".to_string(), Value::Integer(version)),
986 (
987 "value_type".to_string(),
988 config_value_type_value(value_type),
989 ),
990 (
991 "schema_version".to_string(),
992 schema_version.map(Value::Integer).unwrap_or(Value::Null),
993 ),
994 ("tombstone".to_string(), Value::Boolean(tombstone)),
995 ("op".to_string(), Value::text(op.to_string())),
996 ("created_at_ms".to_string(), Value::Integer(now)),
997 ("tags".to_string(), config_tags_value(tags)),
998 ];
999 let mut row = RowData::new(Vec::new());
1000 row.named = Some(fields.into_iter().collect());
1001 let entity = UnifiedEntity::new(
1002 EntityId::new(0),
1003 EntityKind::TableRow {
1004 table: Arc::from(collection),
1005 row_id: 0,
1006 },
1007 EntityData::Row(row),
1008 );
1009 self.inner
1010 .db
1011 .store()
1012 .insert(collection, entity)
1013 .map_err(|err| RedDBError::Internal(err.to_string()))
1014 }
1015
1016 fn latest_config_version(
1017 &self,
1018 collection: &str,
1019 key: &str,
1020 ) -> RedDBResult<Option<ConfigVersion>> {
1021 let mut latest: Option<ConfigVersion> = None;
1022 self.visit_config_versions(collection, Some(key), |version| {
1023 super::keyed_spine::select_latest_version(&mut latest, version);
1024 })?;
1025 Ok(latest)
1026 }
1027
1028 fn config_versions(&self, collection: &str, key: &str) -> RedDBResult<Vec<ConfigVersion>> {
1029 let mut versions = Vec::new();
1030 self.visit_config_versions(collection, Some(key), |version| versions.push(version))?;
1031 Ok(versions)
1032 }
1033
1034 fn latest_config_versions(
1035 &self,
1036 collection: &str,
1037 prefix: Option<&str>,
1038 ) -> RedDBResult<Vec<ConfigVersion>> {
1039 let mut latest = std::collections::BTreeMap::new();
1040 self.visit_config_versions(collection, None, |version| {
1041 if prefix.is_none_or(|prefix| version.key.starts_with(prefix)) {
1042 super::keyed_spine::retain_latest_version(&mut latest, version);
1043 }
1044 })?;
1045 Ok(latest.into_values().collect())
1046 }
1047
1048 fn visit_config_versions(
1049 &self,
1050 collection: &str,
1051 key: Option<&str>,
1052 mut consume: impl FnMut(ConfigVersion),
1053 ) -> RedDBResult<()> {
1054 let store = self.inner.db.store();
1055 let Some(manager) = store.get_collection(collection) else {
1056 return Ok(());
1057 };
1058 let _publication = manager
1059 .has_backing()
1060 .then(|| store.vault_publication_lock.lock());
1061 super::function_budget::try_scan(
1062 |visit| match key {
1063 Some(key) => manager.try_for_each_key_entity(key, visit),
1064 None => manager.try_for_each_entity(usize::MAX, visit),
1065 },
1066 |entity| {
1067 let EntityData::Row(row) = &entity.data else {
1068 return true;
1069 };
1070 if key.is_some_and(|key| !matches!(row.get_field("key"), Some(Value::Text(stored)) if stored.as_ref() == key)) {
1071 return true;
1072 }
1073 if let Some(version) = super::keyed_spine::row_version(entity.id, row, 0) {
1074 consume(ConfigVersion::from_keyed_row(version, row));
1075 }
1076 true
1077 },
1078 )?;
1079 Ok(())
1080 }
1081
1082 fn prune_config_history(&self, collection: &str, key: &str) -> RedDBResult<()> {
1083 let mut versions = Vec::new();
1086 self.visit_config_versions(collection, Some(key), |version| {
1087 versions.push((version.version, version.id));
1088 })?;
1089 if versions.len() <= CONFIG_HISTORY_LIMIT {
1090 return Ok(());
1091 }
1092 versions.sort_by_key(|(version, _)| *version);
1093 let drop_count = versions.len() - CONFIG_HISTORY_LIMIT;
1094 let store = self.inner.db.store();
1095 for (_, id) in versions.into_iter().take(drop_count) {
1096 store
1097 .delete(collection, id)
1098 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1099 }
1100 Ok(())
1101 }
1102
1103 fn authorize_config_write_for_event(&self, collection: &str, key: &str) -> ConfigMutationAuthz {
1104 let default_evidence = self.default_config_mutation_evidence(collection, key);
1105 let Some(auth_store) = self.inner.auth_store.read().clone() else {
1106 return ConfigMutationAuthz::Allowed(default_evidence);
1107 };
1108 if !auth_store.iam_authorization_enabled() {
1109 return ConfigMutationAuthz::Allowed(default_evidence);
1110 }
1111 let Some((principal, role)) = current_auth_identity() else {
1112 return ConfigMutationAuthz::Denied {
1113 reason:
1114 "IAM authorization is enabled; config capability check requires an authenticated principal"
1115 .to_string(),
1116 evidence: default_evidence,
1117 };
1118 };
1119 let tenant = current_tenant();
1120 let principal_id = crate::auth::UserId::from_parts(tenant.as_deref(), &principal);
1121 let ctx = crate::auth::policies::EvalContext {
1122 principal_tenant: tenant.clone(),
1123 current_tenant: tenant.clone(),
1124 peer_ip: None,
1125 mfa_present: false,
1126 now_ms: crate::utils::now_unix_millis() as u128,
1127 principal_is_admin_role: role == crate::auth::Role::Admin,
1128 principal_is_platform_scoped: principal_id.tenant.is_none(),
1129 };
1130 let gate = crate::auth::managed_config::ManagedConfigGate::new(
1131 self.inner.config_registry.as_ref(),
1132 );
1133 let decision = if collection == "red.config" {
1134 gate.check_red_config_write(&auth_store, &principal_id, &ctx, key)
1135 } else {
1136 gate.check_write(&auth_store, &principal_id, &ctx, key)
1137 };
1138 match decision {
1139 crate::auth::managed_config::ManagedConfigDecision::Allow {
1140 entry_id,
1141 resource_type,
1142 managed,
1143 mutability,
1144 matched_action,
1145 matched_resource,
1146 ..
1147 } => {
1148 return ConfigMutationAuthz::Allowed(ConfigMutationEvidence {
1149 id: entry_id,
1150 resource_type,
1151 managed,
1152 mutability,
1153 matched_action: Some(matched_action),
1154 matched_resource: Some(matched_resource),
1155 payload: None,
1156 });
1157 }
1158 crate::auth::managed_config::ManagedConfigDecision::Deny {
1159 entry_id,
1160 resource_type,
1161 managed,
1162 mutability,
1163 matched_action,
1164 matched_resource,
1165 reason,
1166 ..
1167 } => {
1168 return ConfigMutationAuthz::Denied {
1169 reason: format!(
1170 "permission denied: managed config mutation blocked for `{key}`: {reason}"
1171 ),
1172 evidence: ConfigMutationEvidence {
1173 id: entry_id,
1174 resource_type,
1175 managed,
1176 mutability,
1177 matched_action: Some(matched_action),
1178 matched_resource: Some(matched_resource),
1179 payload: None,
1180 },
1181 };
1182 }
1183 crate::auth::managed_config::ManagedConfigDecision::PassThrough { .. } => {}
1184 }
1185
1186 let mut resource = crate::auth::policies::ResourceRef::new(
1187 "config",
1188 config_target_resource(collection, key),
1189 );
1190 if let Some(ref tenant) = tenant {
1191 resource = resource.with_tenant(tenant.clone());
1192 }
1193 if auth_store.check_policy_authz_with_role(
1194 &principal_id,
1195 "config:write",
1196 &resource,
1197 &ctx,
1198 role,
1199 ) {
1200 ConfigMutationAuthz::Allowed(default_evidence)
1201 } else {
1202 ConfigMutationAuthz::Denied {
1203 reason: format!(
1204 "principal=`{}` action=`config:write` resource=`config:{}` denied by IAM policy",
1205 principal,
1206 config_target_resource(collection, key)
1207 ),
1208 evidence: default_evidence,
1209 }
1210 }
1211 }
1212
1213 fn default_config_mutation_evidence(
1214 &self,
1215 collection: &str,
1216 key: &str,
1217 ) -> ConfigMutationEvidence {
1218 let id = if collection == "red.config" {
1219 format!("red.config.{key}")
1220 } else {
1221 key.to_string()
1222 };
1223 ConfigMutationEvidence {
1224 id,
1225 resource_type: crate::auth::managed_config::RESOURCE_TYPE_CONFIG_KEY.to_string(),
1226 managed: false,
1227 mutability: crate::auth::registry::Mutability::MutableViaGovernance,
1228 matched_action: None,
1229 matched_resource: None,
1230 payload: None,
1231 }
1232 }
1233
1234 fn emit_config_mutation_event(
1235 &self,
1236 kind: crate::runtime::control_events::EventKind,
1237 outcome: crate::runtime::control_events::Outcome,
1238 action: &'static str,
1239 collection: &str,
1240 key: &str,
1241 reason: Option<String>,
1242 evidence: &ConfigMutationEvidence,
1243 ) -> RedDBResult<()> {
1244 use crate::runtime::control_events::{
1245 ActorRef, ControlEvent, ControlEventCtx, Sensitivity,
1246 };
1247
1248 let tenant = current_tenant();
1249 let principal = current_auth_identity();
1250 let actor_user = principal
1251 .as_ref()
1252 .map(|(principal, _)| crate::auth::UserId::from_parts(tenant.as_deref(), principal));
1253 let actor = actor_user
1254 .as_ref()
1255 .map(ActorRef::User)
1256 .unwrap_or(ActorRef::Anonymous);
1257 let ctx = ControlEventCtx {
1258 actor,
1259 scope: tenant
1260 .as_ref()
1261 .map(|scope| std::borrow::Cow::Borrowed(scope.as_str())),
1262 request_id: Some(std::borrow::Cow::Owned(format!(
1263 "conn-{}",
1264 current_connection_id()
1265 ))),
1266 trace_id: None,
1267 };
1268
1269 let mut fields = HashMap::new();
1270 fields.insert("id".to_string(), Sensitivity::raw(evidence.id.clone()));
1271 fields.insert(
1272 "resource_type".to_string(),
1273 Sensitivity::raw(evidence.resource_type.clone()),
1274 );
1275 fields.insert(
1276 "managed".to_string(),
1277 Sensitivity::raw(evidence.managed.to_string()),
1278 );
1279 fields.insert(
1280 "mutability".to_string(),
1281 Sensitivity::raw(config_mutability_label(evidence.mutability)),
1282 );
1283 fields.insert("collection".to_string(), Sensitivity::raw(collection));
1284 fields.insert("key".to_string(), Sensitivity::raw(key));
1285 fields.insert(
1286 "connection_id".to_string(),
1287 Sensitivity::raw(current_connection_id().to_string()),
1288 );
1289 if let Some((_, role)) = principal {
1290 fields.insert("actor_role".to_string(), Sensitivity::raw(role.as_str()));
1291 }
1292 if let Some(matched_action) = &evidence.matched_action {
1293 fields.insert(
1294 "matched_action".to_string(),
1295 Sensitivity::raw(matched_action.clone()),
1296 );
1297 }
1298 if let Some(matched_resource) = &evidence.matched_resource {
1299 fields.insert(
1300 "matched_resource".to_string(),
1301 Sensitivity::raw(matched_resource.clone()),
1302 );
1303 }
1304 if let Some(payload) = &evidence.payload {
1305 fields.insert(
1306 "payload".to_string(),
1307 config_payload_sensitivity(&evidence.resource_type, "payload", payload),
1308 );
1309 }
1310
1311 let event = ControlEvent {
1312 kind,
1313 outcome,
1314 action: std::borrow::Cow::Borrowed(action),
1315 resource: Some(format!(
1316 "config:{}",
1317 config_target_resource(collection, key)
1318 )),
1319 reason,
1320 matched_policy_id: None,
1321 fields,
1322 };
1323 let ledger = self.inner.control_event_ledger.read();
1324 match ledger.emit(&ctx, event) {
1325 Ok(_) => Ok(()),
1326 Err(err) if self.inner.control_event_config.require_persistence() => {
1327 Err(RedDBError::Internal(err.to_string()))
1328 }
1329 Err(_) => Ok(()),
1330 }
1331 }
1332
1333 fn check_config_capability(
1334 &self,
1335 action: &str,
1336 collection: &str,
1337 key: &str,
1338 ) -> Result<(), String> {
1339 let Some(auth_store) = self.inner.auth_store.read().clone() else {
1340 return Ok(());
1341 };
1342 if !auth_store.iam_authorization_enabled() {
1343 return Ok(());
1344 }
1345 let Some((principal, role)) = current_auth_identity() else {
1346 return Err(
1347 "IAM authorization is enabled; config capability check requires an authenticated principal"
1348 .to_string(),
1349 );
1350 };
1351 let tenant = current_tenant();
1352 let principal_id = crate::auth::UserId::from_parts(tenant.as_deref(), &principal);
1353 let mut resource = crate::auth::policies::ResourceRef::new(
1354 "config",
1355 config_target_resource(collection, key),
1356 );
1357 if let Some(ref tenant) = tenant {
1358 resource = resource.with_tenant(tenant.clone());
1359 }
1360 let ctx = crate::auth::policies::EvalContext {
1361 principal_tenant: tenant.clone(),
1362 current_tenant: tenant,
1363 peer_ip: None,
1364 mfa_present: false,
1365 now_ms: crate::utils::now_unix_millis() as u128,
1366 principal_is_admin_role: role == crate::auth::Role::Admin,
1367 principal_is_platform_scoped: principal_id.tenant.is_none(),
1368 };
1369 if action == "config:write" {
1370 let gate = crate::auth::managed_config::ManagedConfigGate::new(
1371 self.inner.config_registry.as_ref(),
1372 );
1373 let decision = if collection == "red.config" {
1374 gate.check_red_config_write(&auth_store, &principal_id, &ctx, key)
1375 } else {
1376 gate.check_write(&auth_store, &principal_id, &ctx, key)
1377 };
1378 match decision {
1379 crate::auth::managed_config::ManagedConfigDecision::PassThrough { .. } => {}
1380 crate::auth::managed_config::ManagedConfigDecision::Allow { .. } => return Ok(()),
1381 crate::auth::managed_config::ManagedConfigDecision::Deny { reason, .. } => {
1382 return Err(format!(
1383 "permission denied: managed config mutation blocked for `{key}`: {reason}"
1384 ));
1385 }
1386 }
1387 }
1388 if auth_store.check_policy_authz_with_role(&principal_id, action, &resource, &ctx, role) {
1389 Ok(())
1390 } else {
1391 Err(format!(
1392 "principal=`{}` action=`{}` resource=`config:{}` denied by IAM policy",
1393 principal,
1394 action,
1395 config_target_resource(collection, key)
1396 ))
1397 }
1398 }
1399
1400 fn check_system_config_capability(
1401 &self,
1402 action: &str,
1403 collection: &str,
1404 key: &str,
1405 ) -> Result<(), String> {
1406 if collection != "red.config" {
1407 return Ok(());
1408 }
1409 self.check_config_capability(action, collection, key)
1410 }
1411
1412 pub fn config_watch_events_since(
1413 &self,
1414 collection: &str,
1415 key: &str,
1416 since_lsn: u64,
1417 max_count: usize,
1418 ) -> Vec<crate::replication::cdc::KvWatchEvent> {
1419 self.kv_watch_events_since(collection, key, since_lsn, max_count)
1420 .into_iter()
1421 .map(|event| self.policy_filter_config_watch_event(event))
1422 .collect()
1423 }
1424
1425 pub fn config_watch_events_since_prefix(
1426 &self,
1427 collection: &str,
1428 prefix: &str,
1429 since_lsn: u64,
1430 max_count: usize,
1431 ) -> Vec<crate::replication::cdc::KvWatchEvent> {
1432 self.kv_watch_events_since_prefix(collection, prefix, since_lsn, max_count)
1433 .into_iter()
1434 .map(|event| self.policy_filter_config_watch_event(event))
1435 .collect()
1436 }
1437
1438 fn policy_filter_config_watch_event(
1439 &self,
1440 mut event: crate::replication::cdc::KvWatchEvent,
1441 ) -> crate::replication::cdc::KvWatchEvent {
1442 if self
1443 .check_config_capability("config:read", &event.collection, &event.key)
1444 .is_err()
1445 {
1446 event.before = None;
1447 event.after = None;
1448 }
1449 event
1450 }
1451
1452 fn secret_ref_guard_write_check(
1458 &self,
1459 collection: &str,
1460 key: &str,
1461 value: &Value,
1462 ) -> Option<RedDBError> {
1463 if !value_looks_like_secret_ref(value) {
1464 return None;
1465 }
1466 let Ok(secret_ref) = parse_config_secret_ref(value) else {
1467 return None;
1468 };
1469 let unsealed = match self.peek_vault_unsealed(&secret_ref.collection, &secret_ref.key) {
1470 Ok(Some(value)) => value,
1471 Ok(None) => return None,
1472 Err(_) => return None,
1473 };
1474 if value_looks_like_secret_ref(&unsealed) {
1475 return Some(secret_ref_chain_error(
1476 collection,
1477 key,
1478 &secret_ref.collection,
1479 &secret_ref.key,
1480 ));
1481 }
1482 None
1483 }
1484
1485 fn audit_config_resolve(
1486 &self,
1487 collection: &str,
1488 key: &str,
1489 secret_ref: Option<&ConfigSecretRef>,
1490 outcome: crate::runtime::audit_log::Outcome,
1491 reason: &str,
1492 ) {
1493 let actor = current_auth_identity()
1494 .map(|(principal, _)| principal)
1495 .unwrap_or_else(|| "anonymous".to_string());
1496 let request_id = match current_connection_id() {
1497 0 => "embedded".to_string(),
1498 id => format!("conn-{id}"),
1499 };
1500 let mut builder = crate::runtime::audit_log::AuditEvent::builder("config/resolve")
1501 .principal(actor.clone())
1502 .source(crate::runtime::audit_log::AuditAuthSource::Password)
1503 .resource(format!(
1504 "config:{}",
1505 config_target_resource(collection, key)
1506 ))
1507 .outcome(outcome)
1508 .correlation_id(request_id.clone())
1509 .fields([
1510 crate::runtime::audit_log::AuditFieldEscaper::field("actor", actor),
1511 crate::runtime::audit_log::AuditFieldEscaper::field("collection", collection),
1512 crate::runtime::audit_log::AuditFieldEscaper::field("key", key),
1513 crate::runtime::audit_log::AuditFieldEscaper::field(
1514 "target",
1515 config_target_resource(collection, key),
1516 ),
1517 crate::runtime::audit_log::AuditFieldEscaper::field("reason", reason),
1518 crate::runtime::audit_log::AuditFieldEscaper::field("request_id", request_id),
1519 crate::runtime::audit_log::AuditFieldEscaper::field(
1520 "connection_id",
1521 current_connection_id(),
1522 ),
1523 ]);
1524 if let Some(tenant) = current_tenant() {
1525 builder = builder.tenant(tenant);
1526 }
1527 if let Some(secret_ref) = secret_ref {
1528 builder = builder.fields([
1529 crate::runtime::audit_log::AuditFieldEscaper::field("resolved_store", "vault"),
1530 crate::runtime::audit_log::AuditFieldEscaper::field(
1531 "resolved_collection",
1532 secret_ref.collection.as_str(),
1533 ),
1534 crate::runtime::audit_log::AuditFieldEscaper::field(
1535 "resolved_key",
1536 secret_ref.key.as_str(),
1537 ),
1538 crate::runtime::audit_log::AuditFieldEscaper::field(
1539 "resolved_target",
1540 format!("{}.{}", secret_ref.collection, secret_ref.key),
1541 ),
1542 ]);
1543 }
1544 self.audit_log().record_event(builder.build());
1545 }
1546}
1547
1548pub(super) fn validate_red_config_content_key(collection: &str, key: &str) -> RedDBResult<()> {
1549 let normalized_collection = collection.to_ascii_lowercase();
1550 let is_red_config =
1551 reddb_engine::storage::system_collection_names::canonical_or_self(&normalized_collection)
1552 == "red.config";
1553 if is_red_config && key.starts_with("red.") {
1554 return Err(RedDBError::Query(format!(
1555 "config key `{key}` is invalid inside red.config: omit the redundant `red.` prefix"
1556 )));
1557 }
1558 Ok(())
1559}
1560
1561fn value_looks_like_secret_ref(value: &Value) -> bool {
1566 let Value::Json(bytes) = value else {
1567 return false;
1568 };
1569 let Ok(json) = crate::json::from_slice::<crate::json::Value>(bytes) else {
1570 return false;
1571 };
1572 let Some(object) = json.as_object() else {
1573 return false;
1574 };
1575 object.get("type").and_then(|value| value.as_str()) == Some("secret_ref")
1576}
1577
1578fn secret_ref_chain_error(
1579 source_collection: &str,
1580 source_key: &str,
1581 target_collection: &str,
1582 target_key: &str,
1583) -> RedDBError {
1584 RedDBError::InvalidConfig(format!(
1585 "secret_ref chain rejected: config `{source_collection}.{source_key}` points at vault `{target_collection}.{target_key}` which is itself a secret_ref; depth-1 invariant requires the target to resolve to a non-secret_ref value"
1586 ))
1587}
1588
1589fn parse_config_secret_ref(value: &Value) -> RedDBResult<ConfigSecretRef> {
1590 let Value::Json(bytes) = value else {
1591 return Err(RedDBError::InvalidConfig(
1592 "CONFIG value is not a SecretRef".to_string(),
1593 ));
1594 };
1595 let json = crate::json::from_slice::<crate::json::Value>(bytes).map_err(|err| {
1596 RedDBError::InvalidConfig(format!("CONFIG SecretRef is malformed: {err}"))
1597 })?;
1598 let Some(object) = json.as_object() else {
1599 return Err(RedDBError::InvalidConfig(
1600 "CONFIG SecretRef must be an object".to_string(),
1601 ));
1602 };
1603 let get_str = |field: &str| -> RedDBResult<&str> {
1604 object
1605 .get(field)
1606 .and_then(|value| value.as_str())
1607 .ok_or_else(|| RedDBError::InvalidConfig(format!("CONFIG SecretRef missing {field}")))
1608 };
1609 if get_str("type")? != "secret_ref" {
1610 return Err(RedDBError::InvalidConfig(
1611 "CONFIG value is not a SecretRef".to_string(),
1612 ));
1613 }
1614 if get_str("store")? != "vault" {
1615 return Err(RedDBError::InvalidConfig(
1616 "CONFIG SecretRef store is unsupported".to_string(),
1617 ));
1618 }
1619 Ok(ConfigSecretRef {
1620 collection: get_str("collection")?.to_string(),
1621 key: get_str("key")?.to_string(),
1622 })
1623}
1624
1625fn config_target_resource(collection: &str, key: &str) -> String {
1626 if collection == "red.config" {
1627 format!("red.config/{}", key.to_ascii_lowercase())
1628 } else {
1629 format!("{collection}.{key}")
1630 }
1631}
1632
1633fn config_write_output(
1634 raw_query: &str,
1635 collection: &str,
1636 key: &str,
1637 version: i64,
1638 id: EntityId,
1639 value_type: Option<ConfigValueType>,
1640 schema_version: Option<i64>,
1641 tags: &[String],
1642 statement: &'static str,
1643 affected_rows: u64,
1644) -> RuntimeQueryResult {
1645 let mut result = UnifiedResult::with_columns(vec![
1646 "ok".into(),
1647 "collection".into(),
1648 "key".into(),
1649 "version".into(),
1650 "value_type".into(),
1651 "schema_version".into(),
1652 "tags".into(),
1653 "id".into(),
1654 ]);
1655 let mut record = UnifiedRecord::new();
1656 record.set("ok", Value::Boolean(true));
1657 record.set("collection", Value::text(collection.to_string()));
1658 record.set("key", Value::text(key.to_string()));
1659 record.set("version", Value::Integer(version));
1660 record.set("value_type", config_value_type_value(value_type));
1661 record.set(
1662 "schema_version",
1663 schema_version.map(Value::Integer).unwrap_or(Value::Null),
1664 );
1665 record.set("tags", config_tags_value(tags));
1666 record.set("id", Value::Integer(id.raw() as i64));
1667 result.push(record);
1668 RuntimeQueryResult {
1669 query: raw_query.to_string(),
1670 mode: reddb_rql::modes::QueryMode::Sql,
1671 statement,
1672 engine: "config",
1673 result,
1674 affected_rows,
1675 statement_type: if statement == "delete" {
1676 "delete"
1677 } else {
1678 "update"
1679 },
1680 bookmark: None,
1681 notice: None,
1682 }
1683}
1684
1685fn invalid_config_volatility(operation: &str) -> RedDBError {
1686 RedDBError::InvalidOperation(format!(
1687 "CONFIG does not support KV-only volatility operation {operation}"
1688 ))
1689}
1690
1691fn resolve_config_schema(
1692 latest: Option<&ConfigVersion>,
1693 requested_type: Option<ConfigValueType>,
1694) -> (Option<ConfigValueType>, Option<i64>) {
1695 let previous_type = latest.and_then(|version| version.value_type);
1696 let previous_schema_version = latest.and_then(|version| version.schema_version);
1697 match requested_type {
1698 Some(value_type) if Some(value_type) != previous_type => (
1699 Some(value_type),
1700 Some(previous_schema_version.unwrap_or(0) + 1),
1701 ),
1702 Some(value_type) => (Some(value_type), previous_schema_version.or(Some(1))),
1703 None => (previous_type, previous_schema_version),
1704 }
1705}
1706
1707fn validate_config_value_type(value: &Value, value_type: ConfigValueType) -> RedDBResult<()> {
1708 let valid = match value_type {
1709 ConfigValueType::Bool => matches!(value, Value::Boolean(_)),
1710 ConfigValueType::Int => matches!(
1711 value,
1712 Value::Integer(_) | Value::UnsignedInteger(_) | Value::BigInt(_)
1713 ),
1714 ConfigValueType::String => matches!(value, Value::Text(_)),
1715 ConfigValueType::Url => validate_config_url(value),
1716 ConfigValueType::Object => validate_config_json_shape(value, true),
1717 ConfigValueType::Array => {
1718 matches!(value, Value::Array(_) | Value::Vector(_))
1719 || validate_config_json_shape(value, false)
1720 }
1721 };
1722 if valid {
1723 Ok(())
1724 } else {
1725 Err(RedDBError::InvalidConfig(format!(
1726 "CONFIG value type mismatch: expected {}, got {}",
1727 value_type.as_str(),
1728 config_actual_value_type(value),
1729 )))
1730 }
1731}
1732
1733fn is_enforcement_mode_config(collection: &str, key: &str) -> bool {
1738 collection == "red.config" && key == "policy.enforcement_mode"
1739}
1740
1741fn validate_enforcement_mode_value(value: &Value) -> RedDBResult<()> {
1745 let text = match value {
1746 Value::Text(text) => text.as_ref(),
1747 _ => {
1748 return Err(RedDBError::InvalidConfig(format!(
1749 "config key `{}` must be a string ({} or {}); got {}",
1750 crate::auth::enforcement_mode::ENFORCEMENT_MODE_CONFIG_KEY,
1751 crate::auth::enforcement_mode::PolicyEnforcementMode::LegacyRbac.as_str(),
1752 crate::auth::enforcement_mode::PolicyEnforcementMode::PolicyOnly.as_str(),
1753 config_actual_value_type(value),
1754 )));
1755 }
1756 };
1757 if crate::auth::enforcement_mode::PolicyEnforcementMode::parse(text).is_some() {
1758 Ok(())
1759 } else {
1760 Err(RedDBError::InvalidConfig(format!(
1761 "config key `{}` accepts only `{}` or `{}`, got `{}`",
1762 crate::auth::enforcement_mode::ENFORCEMENT_MODE_CONFIG_KEY,
1763 crate::auth::enforcement_mode::PolicyEnforcementMode::LegacyRbac.as_str(),
1764 crate::auth::enforcement_mode::PolicyEnforcementMode::PolicyOnly.as_str(),
1765 text,
1766 )))
1767 }
1768}
1769
1770fn validate_config_url(value: &Value) -> bool {
1771 let url = match value {
1772 Value::Url(value) => value.as_str(),
1773 Value::Text(value) => value.as_ref(),
1774 _ => return false,
1775 };
1776 url.starts_with("http://") || url.starts_with("https://") || url.starts_with("ftp://")
1777}
1778
1779fn validate_config_json_shape(value: &Value, object: bool) -> bool {
1780 let Value::Json(bytes) = value else {
1781 return false;
1782 };
1783 let Ok(json) = crate::json::from_slice::<crate::json::Value>(bytes) else {
1784 return false;
1785 };
1786 matches!(
1787 (object, json),
1788 (true, crate::json::Value::Object(_)) | (false, crate::json::Value::Array(_))
1789 )
1790}
1791
1792fn config_actual_value_type(value: &Value) -> &'static str {
1793 match value {
1794 Value::Null => "null",
1795 Value::Boolean(_) => "bool",
1796 Value::Integer(_) | Value::UnsignedInteger(_) | Value::BigInt(_) => "int",
1797 Value::Text(_) => "string",
1798 Value::Url(_) => "url",
1799 Value::Json(bytes) => match crate::json::from_slice::<crate::json::Value>(bytes) {
1800 Ok(crate::json::Value::Object(_)) => "object",
1801 Ok(crate::json::Value::Array(_)) => "array",
1802 _ => "json",
1803 },
1804 Value::Array(_) | Value::Vector(_) => "array",
1805 _ => "other",
1806 }
1807}
1808
1809fn config_value_type_value(value_type: Option<ConfigValueType>) -> Value {
1810 value_type
1811 .map(|value_type| Value::text(value_type.as_str()))
1812 .unwrap_or(Value::Null)
1813}
1814
1815fn config_value_type_from_value(value: &Value) -> Option<ConfigValueType> {
1816 match value {
1817 Value::Text(value) => ConfigValueType::parse(value.as_ref()),
1818 _ => None,
1819 }
1820}
1821
1822fn config_tags_value(tags: &[String]) -> Value {
1823 if tags.is_empty() {
1824 return Value::Null;
1825 }
1826 Value::Array(tags.iter().map(|tag| Value::text(tag.clone())).collect())
1827}
1828
1829fn config_tags_from_value(value: Option<&Value>) -> Vec<String> {
1830 match value {
1831 Some(Value::Array(values)) => values
1832 .iter()
1833 .filter_map(|value| match value {
1834 Value::Text(tag) => Some(tag.to_string()),
1835 _ => None,
1836 })
1837 .collect(),
1838 Some(Value::Json(bytes)) => crate::json::from_slice::<crate::json::Value>(bytes)
1839 .ok()
1840 .and_then(|value| value.as_array().map(|values| values.to_vec()))
1841 .map(|values| {
1842 values
1843 .into_iter()
1844 .filter_map(|value| value.as_str().map(ToOwned::to_owned))
1845 .collect()
1846 })
1847 .unwrap_or_default(),
1848 _ => Vec::new(),
1849 }
1850}
1851
1852fn config_payload_sensitivity(
1853 resource_type: &str,
1854 field: &str,
1855 value: &Value,
1856) -> crate::runtime::control_events::Sensitivity {
1857 let payload = config_payload_bytes(value);
1858 if config_payload_raw_allowed(resource_type, field) {
1859 crate::runtime::control_events::Sensitivity::raw(
1860 String::from_utf8_lossy(&payload).into_owned(),
1861 )
1862 } else {
1863 crate::runtime::control_events::Sensitivity::hashed(&payload)
1864 }
1865}
1866
1867fn config_payload_bytes(value: &Value) -> Vec<u8> {
1868 let json = reddb_types::Value::to_json(value);
1869 crate::serde_json::to_vec(&json).unwrap_or_else(|_| value.to_string().into_bytes())
1870}
1871
1872fn config_payload_raw_allowed(resource_type: &str, field: &str) -> bool {
1873 const RAW_PAYLOAD_FIELDS: &[(&str, &str)] = &[("audit_surface", "payload")];
1874 RAW_PAYLOAD_FIELDS
1875 .iter()
1876 .any(|(allowed_type, allowed_field)| {
1877 *allowed_type == resource_type && *allowed_field == field
1878 })
1879}
1880
1881fn config_mutability_label(mutability: crate::auth::registry::Mutability) -> &'static str {
1882 match mutability {
1883 crate::auth::registry::Mutability::Immutable => "immutable",
1884 crate::auth::registry::Mutability::MutableViaGovernance => "mutable_via_governance",
1885 }
1886}
1887
1888fn current_unix_ms() -> u64 {
1889 std::time::SystemTime::now()
1890 .duration_since(std::time::UNIX_EPOCH)
1891 .unwrap_or_default()
1892 .as_millis() as u64
1893}