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