use std::sync::Arc; use axum::body::Bytes; use axum::extract::State; use axum::response::Response; use http::HeaderMap; use serde::Deserialize; use knot_acl::{KnotAcl, can_admin_knot}; use knot_cob::{CobHome, CobStore}; use knot_cobs::{Grant, MembersChange, MembersCob, Removal}; use knot_events::KnotMemberUpdate; use knot_git::Repo; use knot_index::Resolved; use knot_runtime::{Clock, HttpTransport}; use knot_types::AccountDid; use crate::cob::grant_set_apply; use crate::error::XrpcError; use crate::{XrpcState, decode, ok_empty, run_blocking}; pub(crate) const ADD_ROUTE: &str = "/xrpc/sh.tangled.knot.addMember"; pub(crate) const REMOVE_ROUTE: &str = "/xrpc/sh.tangled.knot.removeMember"; #[derive(Deserialize)] pub(crate) struct SubjectInput { pub(crate) subject: AccountDid, } pub(crate) async fn add_member( State(state): State>>, headers: HeaderMap, method: crate::Method, body: Bytes, ) -> Result { let actor = state.authenticate(&headers, &method).await?; let acl = KnotAcl::new(&state.admins, state.admission, &state.index); if !can_admin_knot(&acl, &actor).is_allowed() { return Err(XrpcError::forbidden("only knot admin may add members")); } let SubjectInput { subject } = decode(&body)?; grant_membership( &state, Grant { subject, added_by: actor, created_at: state.now(), }, ) .await } pub(crate) async fn grant_membership( state: &Arc>, grant: Grant, ) -> Result { if state.admins.contains(&grant.subject) || matches!(state.index.is_member(&grant.subject), Resolved::Ready(true)) { return Ok(ok_empty()); } let now = grant.created_at; let event_subject = grant.subject.clone(); let signer = state.secrets.signer(&state.knot_did)?; let meta_path = state.meta_path.clone(); let index = Arc::clone(&state.index); let cob_locks = Arc::clone(&state.cob_locks); let events = Arc::clone(&state.events); let home = CobHome::from(&state.knot_did); run_blocking(move || { let _guard = cob_locks.meta(); let meta = Repo::open(&meta_path)?; let changed = grant_set_apply::( &CobStore::new(&meta), &home, MembersChange::Add(grant), &signer, now, true, )?; index.refresh_members()?; if changed { events.publish(&KnotMemberUpdate::added(event_subject)); } Ok(()) }) .await?; Ok(ok_empty()) } pub(crate) async fn remove_member( State(state): State>>, headers: HeaderMap, method: crate::Method, body: Bytes, ) -> Result { let actor = state.authenticate(&headers, &method).await?; let acl = KnotAcl::new(&state.admins, state.admission, &state.index); if !can_admin_knot(&acl, &actor).is_allowed() { return Err(XrpcError::forbidden("only knot admin may remove members")); } let SubjectInput { subject } = decode(&body)?; if matches!(state.index.is_member(&subject), Resolved::Ready(false)) { return Ok(ok_empty()); } let now = state.now(); let event_subject = subject.clone(); let removal = Removal { subject }; let signer = state.secrets.signer(&state.knot_did)?; let meta_path = state.meta_path.clone(); let index = Arc::clone(&state.index); let cob_locks = Arc::clone(&state.cob_locks); let events = Arc::clone(&state.events); let home = CobHome::from(&state.knot_did); run_blocking(move || { let _guard = cob_locks.meta(); let meta = Repo::open(&meta_path)?; let changed = grant_set_apply::( &CobStore::new(&meta), &home, MembersChange::Remove(removal), &signer, now, false, )?; index.refresh_members()?; if changed { events.publish(&KnotMemberUpdate::removed(event_subject)); } Ok(()) }) .await?; Ok(ok_empty()) }