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//!
11//! Copyright (c) systemprompt.io — Business Source License 1.1.
12//! See <https://systemprompt.io> for licensing details.
13
14use std::path::PathBuf;
15
16use systemprompt_database::DbPool;
17use systemprompt_security::authz::{
18    AccessControlConfig, AccessControlIngestionService, IngestOptions,
19};
20
21use crate::error::{SyncError, SyncResult};
22use crate::models::{LocalSyncDirection, LocalSyncResult};
23
24#[derive(Debug)]
25pub struct AccessControlLocalSync {
26    db: DbPool,
27    yaml_path: PathBuf,
28}
29
30impl AccessControlLocalSync {
31    pub const fn new(db: DbPool, yaml_path: PathBuf) -> Self {
32        Self { db, yaml_path }
33    }
34
35    pub async fn sync_to_db(
36        &self,
37        override_existing: bool,
38        delete_orphans: bool,
39    ) -> SyncResult<LocalSyncResult> {
40        if !self.yaml_path.exists() {
41            return Err(SyncError::MissingConfig(format!(
42                "Access-control config not found at: {}",
43                self.yaml_path.display()
44            )));
45        }
46
47        let raw = std::fs::read_to_string(&self.yaml_path).map_err(|e| {
48            SyncError::internal(format!(
49                "Failed to read {}: {}",
50                self.yaml_path.display(),
51                e
52            ))
53        })?;
54        let config: AccessControlConfig = serde_yaml::from_str(&raw).map_err(|e| {
55            SyncError::invalid_input(format!(
56                "Failed to parse {} as AccessControlConfig: {}",
57                self.yaml_path.display(),
58                e
59            ))
60        })?;
61
62        let options = IngestOptions {
63            override_existing,
64            delete_orphans,
65        };
66
67        let svc = AccessControlIngestionService::new(&self.db).map_err(SyncError::internal)?;
68        let report = svc
69            .ingest_config(&config, options)
70            .await
71            .map_err(SyncError::internal)?;
72
73        Ok(LocalSyncResult {
74            items_synced: report.inserted + report.updated,
75            items_skipped: report.skipped,
76            items_skipped_modified: 0,
77            items_deleted: report.deleted,
78            errors: Vec::new(),
79            direction: LocalSyncDirection::ToDatabase,
80        })
81    }
82}