systemprompt_cli/commands/admin/config/
reconcile.rs1use std::path::Path;
15use std::sync::Arc;
16
17use systemprompt_database::{Database, DbPool};
18use systemprompt_identifiers::RouteId;
19use systemprompt_models::{Config, Profile};
20use systemprompt_security::authz::{
21 AccessControlIngestionService, AccessControlRepository, IngestOptions,
22 reconcile_gateway_entities,
23};
24use systemprompt_sync::AccessControlLocalSync;
25
26const ROLES_YAML_RELATIVE: &str = "access-control/roles.yaml";
27
28pub(super) enum ReconcileOutcome {
33 Reconciled,
34 Deferred(String),
35}
36
37pub(super) async fn reconcile_authz(profile: &Profile, profile_path: &str) -> ReconcileOutcome {
38 match try_reconcile(profile, profile_path).await {
39 Ok(()) => ReconcileOutcome::Reconciled,
40 Err(err) => {
41 tracing::warn!(
42 error = %err,
43 "profile saved, but the authz catalog could not be reconciled now; it will be \
44 reconciled on the next app start"
45 );
46 ReconcileOutcome::Deferred(err.to_string())
47 },
48 }
49}
50
51pub(super) fn append_reconcile_notice(message: String, outcome: &ReconcileOutcome) -> String {
55 match outcome {
56 ReconcileOutcome::Reconciled => message,
57 ReconcileOutcome::Deferred(reason) => format!(
58 "{message}\n\nā authz reconcile deferred: {reason}\nThe profile was saved; the authz \
59 catalog will be reconciled on the next app start."
60 ),
61 }
62}
63
64async fn try_reconcile(profile: &Profile, profile_path: &str) -> anyhow::Result<()> {
65 let cfg = Config::get()?;
66 let database: DbPool = Arc::new(
67 Database::from_config_with_write(
68 &cfg.database_type,
69 &cfg.database_url,
70 cfg.database_write_url.as_deref(),
71 )
72 .await?,
73 );
74
75 let repo = AccessControlRepository::new(&database)?;
76 let route_ids = profile
77 .gateway
78 .as_ref()
79 .map(|gateway| gateway.dispatchable_route_ids(&profile.providers))
80 .unwrap_or_default();
81 let id_refs: Vec<&str> = route_ids.iter().map(RouteId::as_str).collect();
82 let source = format!("profile:{profile_path}");
83 reconcile_gateway_entities(&repo, &id_refs, &source).await?;
84
85 let roles_yaml = Path::new(&profile.paths.services).join(ROLES_YAML_RELATIVE);
86 if roles_yaml.exists() {
87 AccessControlLocalSync::new(Arc::clone(&database), roles_yaml)
88 .sync_to_db(true, false)
89 .await?;
90
91 let services = systemprompt_loader::ConfigLoader::load()?;
92 let svc = AccessControlIngestionService::new(&database)?;
93 svc.ingest_marketplace_access(
94 &services.marketplaces,
95 IngestOptions {
96 override_existing: true,
97 delete_orphans: false,
98 },
99 )
100 .await?;
101 }
102 Ok(())
103}