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