use serde_json::json;
use trust_tasks_rs::specs::git_ns::roles::reproject::v0_1 as reproject;
use crate::server::AppState;
use super::bridge;
use super::model::{Mode, RepoState, Right};
use super::ops::{
self, Audit, OpError, OpResult, PolicyInput, audit, check_policy, consent_gate, declared, now,
standing,
};
use super::rules;
use super::store::{self, Snapshot};
use super::wire;
pub const MANUAL_MODE: &str = reproject::error_codes::MANUAL_MODE.code;
pub const NO_FORGE_ACCESS: &str = reproject::error_codes::NO_FORGE_ACCESS.code;
pub async fn roles_reproject(
state: &AppState,
actor_did: &str,
p: reproject::Payload,
) -> OpResult<reproject::Response> {
let actor = standing(state, actor_did).await?;
let reason = p.reason.as_ref().map(|r| r.to_string());
let (ns, repos) = {
let _guard = store::write_lock().await;
let snap = Snapshot::load(&state.git_ns).await?;
let t = now();
let resource = ops::parse_resource(&p.resource)?;
let ns = ops::bound_namespace_for(&snap, &resource)?.clone();
let explicit_admin = rules::admins(&snap, &ns.id, t).contains(&actor.did);
let owner = !resource.is_namespace()
&& rules::effective_on(&snap, &actor.did, &resource, t).contains(&Right::RepoOwn);
let passed =
rules::reproject_admitted(actor.community_admin, actor.member, explicit_admin, owner)
.ok_or_else(|| {
OpError::PermissionDenied(if resource.is_namespace() {
format!(
"re-projecting every repository in {} needs the community-administrator \
capability or git.ns.admin on it by explicit record",
ns.resource()
)
} else {
format!(
"re-projecting {resource} needs git.repo.own on it, git.ns.admin on {} by \
explicit record, or the community-administrator capability",
ns.resource()
)
})
})?;
if ns.mode != Mode::Bridge || ns.bridge_did.is_none() {
return Err(declared(
MANUAL_MODE,
format!(
"{} is governed in manual mode: no bridge projects its roles",
ns.resource()
),
));
}
if ns.installation_removed {
return Err(declared(
NO_FORGE_ACCESS,
format!(
"the bridge reported losing its access to {}; re-project once a forge owner \
restores it",
ns.resource()
),
));
}
let covered: Vec<_> = if resource.is_namespace() {
snap.repos
.iter()
.filter(|r| {
r.namespace_id == ns.id
&& matches!(r.state, RepoState::Active | RepoState::Orphaned)
})
.cloned()
.collect()
} else {
let repo = ops::repo_at(&snap, &resource)?;
if !matches!(repo.state, RepoState::Active | RepoState::Orphaned) {
return Err(declared(
ops::REPO_NOT_ACTIVE,
format!(
"{resource} is {}; roles are projected on an active or orphaned \
repository",
repo.state.as_str()
),
));
}
vec![repo.clone()]
};
consent_gate(state, &actor, "roles.reproject", None).await?;
let version = check_policy(
state,
PolicyInput {
action: "roles.reproject",
actor: &actor,
actor_rights: rules::effective_on(&snap, &actor.did, &resource, t)
.into_iter()
.collect(),
resource: &resource,
right: None,
subject: None,
visibility: None,
expires_at: None,
namespace: Some(&ns),
passed,
},
)
.await?;
for repo in &covered {
let mut updated = repo.clone();
updated.roles_digest = None;
store::put_repo(&state.git_ns.ks, &updated).await?;
}
let repos: Vec<String> = covered.iter().map(|r| r.resource.clone()).collect();
audit(
state,
&actor.did,
None,
Audit {
action: "gitNs.roles.reprojected",
namespace: Some(&ns.id),
resource: Some(resource.to_string()),
right: None,
policy_version: version,
detail: Some(json!({ "repos": repos, "reason": reason }).to_string()),
},
)
.await;
(ns, repos)
};
if !repos.is_empty() {
bridge::project_roles(state, false).await?;
}
tracing::info!(
namespace = %ns.id,
repos = repos.len(),
"git-ns roles re-projected"
);
Ok(wire::into(json!({ "repos": repos }))?)
}