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 options = IngestOptions {
60            override_existing,
61            delete_orphans,
62        };
63
64        let svc = AccessControlIngestionService::new(&self.db).map_err(SyncError::internal)?;
65        let report = svc
66            .ingest_config(&config, options)
67            .await
68            .map_err(SyncError::internal)?;
69
70        let services = systemprompt_loader::ConfigLoader::load().map_err(|e| {
71            SyncError::invalid_input(format!("Failed to load services config: {e}"))
72        })?;
73        let marketplace_report = svc
74            .ingest_marketplace_access(&services.marketplaces, options)
75            .await
76            .map_err(SyncError::internal)?;
77
78        Ok(LocalSyncResult {
79            items_synced: report.inserted
80                + report.updated
81                + marketplace_report.inserted
82                + marketplace_report.updated,
83            items_skipped: report.skipped + marketplace_report.skipped,
84            items_skipped_modified: 0,
85            items_deleted: report.deleted + marketplace_report.deleted,
86            errors: Vec::new(),
87            direction: LocalSyncDirection::ToDatabase,
88        })
89    }
90}