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