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