Skip to main content

systemprompt_sync/local/
access_control_sync.rs

1//! YAML → DB sync for access-control baseline rules.
2//!
3//! Reads an [`AccessControlConfig`] from disk and projects it into
4//! `access_control_rules` via
5//! [`AccessControlIngestionService`](systemprompt_security::authz::AccessControlIngestionService).
6//!
7//! Direction is fixed: YAML drives the DB. The `to_disk` direction does
8//! not exist for ACL — DB→YAML promotion is an operator-explicit one-shot
9//! export from the CLI.
10
11use std::path::PathBuf;
12
13use systemprompt_database::DbPool;
14use systemprompt_security::authz::{
15    AccessControlConfig, AccessControlIngestionService, IngestOptions,
16};
17
18use crate::error::{SyncError, SyncResult};
19use crate::models::{LocalSyncDirection, LocalSyncResult};
20
21#[derive(Debug)]
22pub struct AccessControlLocalSync {
23    db: DbPool,
24    yaml_path: PathBuf,
25}
26
27impl AccessControlLocalSync {
28    pub const fn new(db: DbPool, yaml_path: PathBuf) -> Self {
29        Self { db, yaml_path }
30    }
31
32    pub async fn sync_to_db(
33        &self,
34        override_existing: bool,
35        delete_orphans: bool,
36    ) -> SyncResult<LocalSyncResult> {
37        if !self.yaml_path.exists() {
38            return Err(SyncError::MissingConfig(format!(
39                "Access-control config not found at: {}",
40                self.yaml_path.display()
41            )));
42        }
43
44        let raw = std::fs::read_to_string(&self.yaml_path).map_err(|e| {
45            SyncError::internal(format!(
46                "Failed to read {}: {}",
47                self.yaml_path.display(),
48                e
49            ))
50        })?;
51        let config: AccessControlConfig = serde_yaml::from_str(&raw).map_err(|e| {
52            SyncError::invalid_input(format!(
53                "Failed to parse {} as AccessControlConfig: {}",
54                self.yaml_path.display(),
55                e
56            ))
57        })?;
58
59        let svc = AccessControlIngestionService::new(&self.db).map_err(SyncError::internal)?;
60        let report = svc
61            .ingest_config(
62                &config,
63                IngestOptions {
64                    override_existing,
65                    delete_orphans,
66                },
67            )
68            .await
69            .map_err(SyncError::internal)?;
70
71        Ok(LocalSyncResult {
72            items_synced: report.rules_inserted + report.rules_updated,
73            items_skipped: report.rules_skipped,
74            items_skipped_modified: 0,
75            items_deleted: report.rules_deleted,
76            errors: Vec::new(),
77            direction: LocalSyncDirection::ToDatabase,
78        })
79    }
80}