Skip to main content

systemprompt_sync/jobs/
access_control_sync.rs

1//! Bootstrap job that ingests the access-control YAML baseline into the
2//! database.
3//!
4//! Mirrors [`super::ContentSyncJob`] but with a fixed direction
5//! (YAML → DB). Disabled by default; operators wire it in via
6//! `scheduler_config.bootstrap_jobs` so it runs once at startup.
7
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use async_trait::async_trait;
12use systemprompt_database::{Database, DbPool};
13use systemprompt_models::AppPaths;
14use systemprompt_traits::{Job, JobContext, JobResult, ProviderError, ProviderResult};
15
16use crate::local::AccessControlLocalSync;
17
18const DEFAULT_YAML_RELATIVE: &str = "access-control/config.yaml";
19
20#[derive(Debug, Clone, Copy)]
21pub struct AccessControlSyncJob;
22
23#[async_trait]
24impl Job for AccessControlSyncJob {
25    fn name(&self) -> &'static str {
26        "access_control_sync"
27    }
28
29    fn description(&self) -> &'static str {
30        "Project services/access-control YAML into access_control_rules"
31    }
32
33    fn schedule(&self) -> &'static str {
34        ""
35    }
36
37    fn tags(&self) -> Vec<&'static str> {
38        vec!["access-control", "sync", "bootstrap"]
39    }
40
41    fn enabled(&self) -> bool {
42        false
43    }
44
45    async fn execute(&self, ctx: &JobContext) -> ProviderResult<JobResult> {
46        let start = std::time::Instant::now();
47
48        let db_pool: &DbPool = ctx.db_pool::<DbPool>().ok_or_else(|| {
49            ProviderError::Configuration("DbPool not available in job context".into())
50        })?;
51
52        let paths = ctx
53            .app_paths::<Arc<AppPaths>>()
54            .ok_or_else(|| {
55                ProviderError::Configuration("AppPaths not available in job context".into())
56            })?
57            .as_ref();
58
59        let yaml_path = resolve_yaml_path(ctx, paths.system().services());
60        let override_existing = bool_param(ctx, "override_existing", true);
61        let delete_orphans = bool_param(ctx, "delete_orphans", true);
62
63        tracing::info!(
64            yaml_path = %yaml_path.display(),
65            override_existing,
66            delete_orphans,
67            "access_control_sync job started",
68        );
69
70        let sync = AccessControlLocalSync::new(Arc::<Database>::clone(db_pool), yaml_path);
71        let result = sync
72            .sync_to_db(override_existing, delete_orphans)
73            .await
74            .map_err(|e| ProviderError::RenderFailed(e.to_string()))?;
75
76        let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
77        tracing::info!(
78            items_synced = result.items_synced,
79            items_skipped = result.items_skipped,
80            items_deleted = result.items_deleted,
81            duration_ms,
82            "access_control_sync job completed",
83        );
84
85        Ok(JobResult::success()
86            .with_stats(result.items_synced as u64, result.errors.len() as u64)
87            .with_duration(duration_ms))
88    }
89}
90
91fn resolve_yaml_path(ctx: &JobContext, services_path: &std::path::Path) -> PathBuf {
92    ctx.parameters().get("yaml_path").map_or_else(
93        || services_path.join(DEFAULT_YAML_RELATIVE),
94        |raw| {
95            let p = std::path::Path::new(raw);
96            if p.is_absolute() {
97                p.to_path_buf()
98            } else {
99                services_path.join(p)
100            }
101        },
102    )
103}
104
105fn bool_param(ctx: &JobContext, key: &str, default: bool) -> bool {
106    ctx.parameters().get(key).map_or(default, |v| {
107        matches!(v.as_str(), "true" | "1" | "yes" | "TRUE" | "True")
108    })
109}
110
111systemprompt_provider_contracts::submit_job!(&AccessControlSyncJob);