use std::path::Path;
use std::sync::Arc;
use systemprompt_database::{Database, DbPool};
use systemprompt_identifiers::RouteId;
use systemprompt_models::{Config, Profile};
use systemprompt_security::authz::{
AccessControlIngestionService, AccessControlRepository, IngestOptions,
reconcile_gateway_entities,
};
use systemprompt_sync::AccessControlLocalSync;
const ROLES_YAML_RELATIVE: &str = "access-control/roles.yaml";
pub(super) enum ReconcileOutcome {
Reconciled,
Deferred(String),
}
pub(super) async fn reconcile_authz(profile: &Profile, profile_path: &str) -> ReconcileOutcome {
match try_reconcile(profile, profile_path).await {
Ok(()) => ReconcileOutcome::Reconciled,
Err(err) => {
tracing::warn!(
error = %err,
"profile saved, but the authz catalog could not be reconciled now; it will be \
reconciled on the next app start"
);
ReconcileOutcome::Deferred(err.to_string())
},
}
}
pub(super) fn append_reconcile_notice(message: String, outcome: &ReconcileOutcome) -> String {
match outcome {
ReconcileOutcome::Reconciled => message,
ReconcileOutcome::Deferred(reason) => format!(
"{message}\n\n⚠ authz reconcile deferred: {reason}\nThe profile was saved; the authz \
catalog will be reconciled on the next app start."
),
}
}
async fn try_reconcile(profile: &Profile, profile_path: &str) -> anyhow::Result<()> {
let cfg = Config::get()?;
let database: DbPool = Arc::new(
Database::from_config_with_write(
&cfg.database_type,
&cfg.database_url,
cfg.database_write_url.as_deref(),
)
.await?,
);
let repo = AccessControlRepository::new(&database)?;
let route_ids = profile
.gateway
.as_ref()
.map(systemprompt_models::profile::GatewayState::resolved_route_ids)
.unwrap_or_default();
let id_refs: Vec<&str> = route_ids.iter().map(RouteId::as_str).collect();
let source = format!("profile:{profile_path}");
reconcile_gateway_entities(&repo, &id_refs, &source).await?;
let roles_yaml = Path::new(&profile.paths.services).join(ROLES_YAML_RELATIVE);
if roles_yaml.exists() {
AccessControlLocalSync::new(Arc::clone(&database), roles_yaml)
.sync_to_db(true, false)
.await?;
let services = systemprompt_loader::ConfigLoader::load()?;
let svc = AccessControlIngestionService::new(&database)?;
svc.ingest_marketplace_access(
&services.marketplaces,
IngestOptions {
override_existing: true,
delete_orphans: false,
},
)
.await?;
}
Ok(())
}