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