Skip to main content

reddb_server/runtime/
impl_config.rs

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