use std::collections::HashSet;
use std::path::Path;
use systemprompt_database::DbPool;
use systemprompt_identifiers::RouteId;
use systemprompt_models::services::{BundleOwnership, ServicesBundleManifest, ServicesConfig};
use super::error::AuthzResult;
use super::gateway_entities::{GatewayReconcileReport, reconcile_gateway_entities_exact};
use super::ingestion::{
AccessControlIngestionService, IngestOptions, IngestReport, IngestScope, RegisteredEntities,
};
use super::repository::AccessControlRepository;
use super::types::EntityKind;
const ROLES_YAML_RELATIVE: &str = "access-control/roles.yaml";
#[derive(Debug, Clone, Default)]
pub struct ReconcileReport {
pub gateway: Option<GatewayReconcileReport>,
pub roles: Option<IngestReport>,
pub marketplaces: Option<IngestReport>,
}
struct Pass<'a> {
source: &'a str,
scope: IngestScope,
platform_dirs: bool,
delete_orphans: bool,
}
pub async fn reconcile_services_authz(
db: &DbPool,
services: &ServicesConfig,
services_root: &Path,
source: &str,
scope: Option<IngestScope>,
) -> AuthzResult<ReconcileReport> {
let pass = Pass {
source,
scope: scope.unwrap_or_default(),
platform_dirs: true,
delete_orphans: false,
};
run_pass(db, services, services_root, &pass).await
}
pub async fn reconcile_composed_bundles(
db: &DbPool,
services: &ServicesConfig,
composed_root: &Path,
bundles: &[(&str, &ServicesBundleManifest)],
) -> AuthzResult<Vec<(String, ReconcileReport)>> {
let mut out = Vec::with_capacity(bundles.len());
for (index, (name, manifest)) in bundles.iter().enumerate() {
let owns = &manifest.owns;
let view = bundle_view(services, owns, index == 0);
let source = format!("bundle:{name}");
let pass = Pass {
source: &source,
scope: bundle_scope(owns, &view, index == 0),
platform_dirs: index == 0,
delete_orphans: true,
};
let report = run_pass(db, &view, composed_root, &pass).await?;
out.push(((*name).to_owned(), report));
}
Ok(out)
}
async fn run_pass(
db: &DbPool,
services: &ServicesConfig,
services_root: &Path,
pass: &Pass<'_>,
) -> AuthzResult<ReconcileReport> {
let repo = AccessControlRepository::new(db)?;
let svc = AccessControlIngestionService::new(db)?;
let route_ids = services
.gateway
.as_ref()
.map(|gateway| gateway.dispatchable_route_ids(&services.providers))
.unwrap_or_default();
let id_refs: Vec<&str> = route_ids.iter().map(RouteId::as_str).collect();
let mut report = ReconcileReport::default();
let registered = if id_refs.is_empty() {
RegisteredEntities::default()
} else {
report.gateway =
Some(reconcile_gateway_entities_exact(&repo, &id_refs, pass.source).await?);
RegisteredEntities::new().with_kind(EntityKind::GatewayRoute, id_refs.iter().copied())
};
let options = IngestOptions {
override_existing: true,
delete_orphans: pass.delete_orphans,
source: pass.source.to_owned(),
scope: pass.scope.clone(),
};
let roles_yaml = services_root.join(ROLES_YAML_RELATIVE);
if pass.platform_dirs && roles_yaml.exists() {
report.roles = Some(
svc.ingest_config_from_yaml_path(&roles_yaml, options.clone(), ®istered)
.await?,
);
}
report.marketplaces = Some(
svc.ingest_marketplace_access(&services.marketplaces, options)
.await?,
);
Ok(report)
}
fn bundle_view(services: &ServicesConfig, owns: &BundleOwnership, is_base: bool) -> ServicesConfig {
let owned: HashSet<&str> = owns.marketplaces.iter().map(String::as_str).collect();
let mut view = services.clone();
view.marketplaces
.retain(|id, _| owned.contains(id.as_str()));
if !is_base {
view.gateway = None;
}
view
}
fn bundle_scope(owns: &BundleOwnership, view: &ServicesConfig, is_base: bool) -> IngestScope {
let mut scope = IngestScope::new()
.with_kind(EntityKind::Marketplace, owns.marketplaces.clone())
.with_kind(EntityKind::Plugin, owns.plugins.clone())
.with_kind(EntityKind::Skill, owns.skills.clone())
.with_kind(EntityKind::Hook, owns.hooks.clone());
if is_base {
let route_ids = view
.gateway
.as_ref()
.map(|gateway| gateway.dispatchable_route_ids(&view.providers))
.unwrap_or_default();
scope = scope.with_kind(
EntityKind::GatewayRoute,
route_ids.iter().map(|id| id.as_str().to_owned()),
);
}
scope
}