This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-xrpc / src / collaborators.rs
5.6 kB 177 lines
1use std::sync::Arc; 2 3use axum::body::Bytes; 4use axum::extract::State; 5use axum::response::Response; 6use http::HeaderMap; 7use serde::Deserialize; 8 9use knot_acl::{KnotAcl, can_manage_collaborators}; 10use knot_cob::{CobHome, CobStore}; 11use knot_cobs::{CollaboratorsChange, CollaboratorsCob, Grant, Removal}; 12use knot_events::RepoCollaboratorUpdate; 13use knot_index::Resolved; 14use knot_runtime::{Clock, HttpTransport}; 15use knot_types::{AccountDid, RepoDid}; 16 17use crate::cob::grant_set_apply; 18use crate::error::XrpcError; 19use crate::{XrpcState, decode, ok_empty, run_blocking}; 20 21pub(crate) const ADD_ROUTE: &str = "/xrpc/sh.tangled.repo.addCollaborator"; 22pub(crate) const REMOVE_ROUTE: &str = "/xrpc/sh.tangled.repo.removeCollaborator"; 23 24#[derive(Deserialize)] 25struct CollaboratorInput { 26 repo: RepoDid, 27 subject: AccountDid, 28} 29 30fn require_owner<H: HttpTransport, C: Clock>( 31 state: &XrpcState<H, C>, 32 actor: &AccountDid, 33 repo: &RepoDid, 34) -> Result<knot_types::OwnerDid, XrpcError> { 35 let owner = match state.index.owner_of(repo) { 36 Resolved::Ready(Some(owner)) => owner, 37 Resolved::Ready(None) => { 38 return Err(XrpcError::not_found( 39 "repository isn't registered on this knot", 40 )); 41 } 42 Resolved::Warming => { 43 return Err(XrpcError::warming("registry projection is still warming")); 44 } 45 }; 46 let acl = KnotAcl::new(&state.admins, state.admission, &state.index); 47 if can_manage_collaborators(&acl, actor, repo).is_allowed() { 48 Ok(owner) 49 } else { 50 Err(XrpcError::forbidden( 51 "only repository owner may manage collaborators", 52 )) 53 } 54} 55 56pub(crate) async fn add_collaborator<H: HttpTransport, C: Clock>( 57 State(state): State<Arc<XrpcState<H, C>>>, 58 headers: HeaderMap, 59 method: crate::Method, 60 body: Bytes, 61) -> Result<Response, XrpcError> { 62 let actor = state.authenticate(&headers, &method).await?; 63 let CollaboratorInput { repo, subject } = decode(&body)?; 64 let owner = require_owner(&state, &actor, &repo)?; 65 66 crate::fold_collaborators(&state, &repo).await; 67 if owner.is(&subject) 68 || matches!( 69 state.index.is_collaborator(&repo, &subject), 70 Resolved::Ready(true) 71 ) 72 { 73 return Ok(ok_empty()); 74 } 75 76 let now = state.now(); 77 let event_subject = subject.clone(); 78 let event_repo = repo.clone(); 79 let grant = Grant { 80 subject, 81 added_by: actor, 82 created_at: now, 83 }; 84 let signer = state.secrets.signer(&state.knot_did).map_err(|error| { 85 XrpcError::internal(format!("knot signing key is unavailable: {error}")) 86 })?; 87 let layout = state.layout.clone(); 88 let index = Arc::clone(&state.index); 89 let cob_locks = Arc::clone(&state.cob_locks); 90 let events = Arc::clone(&state.events); 91 run_blocking(move || { 92 let _guard = cob_locks.repo(&repo); 93 owner_unmoved(&index, &repo, &owner)?; 94 let git = layout.open(&repo)?; 95 let changed = grant_set_apply::<CollaboratorsCob>( 96 &CobStore::new(&git), 97 &CobHome::from(&repo), 98 CollaboratorsChange::Add(grant), 99 &signer, 100 now, 101 true, 102 )?; 103 index.refresh_collaborators(&repo)?; 104 if changed { 105 events.publish(&RepoCollaboratorUpdate::added(event_subject, event_repo)); 106 } 107 Ok(()) 108 }) 109 .await?; 110 111 Ok(ok_empty()) 112} 113 114fn owner_unmoved( 115 index: &knot_index::Index, 116 repo: &RepoDid, 117 owner: &knot_types::OwnerDid, 118) -> Result<(), XrpcError> { 119 match index.owner_of(repo) { 120 Resolved::Ready(Some(current)) if current == *owner => Ok(()), 121 _ => Err(XrpcError::conflict( 122 "repository is no longer registered to owner who authorized this request", 123 )), 124 } 125} 126 127pub(crate) async fn remove_collaborator<H: HttpTransport, C: Clock>( 128 State(state): State<Arc<XrpcState<H, C>>>, 129 headers: HeaderMap, 130 method: crate::Method, 131 body: Bytes, 132) -> Result<Response, XrpcError> { 133 let actor = state.authenticate(&headers, &method).await?; 134 let CollaboratorInput { repo, subject } = decode(&body)?; 135 let owner = require_owner(&state, &actor, &repo)?; 136 137 crate::fold_collaborators(&state, &repo).await; 138 if matches!( 139 state.index.is_collaborator(&repo, &subject), 140 Resolved::Ready(false) 141 ) { 142 return Ok(ok_empty()); 143 } 144 145 let now = state.now(); 146 let event_subject = subject.clone(); 147 let event_repo = repo.clone(); 148 let removal = Removal { subject }; 149 let signer = state.secrets.signer(&state.knot_did).map_err(|error| { 150 XrpcError::internal(format!("knot signing key is unavailable: {error}")) 151 })?; 152 let layout = state.layout.clone(); 153 let index = Arc::clone(&state.index); 154 let cob_locks = Arc::clone(&state.cob_locks); 155 let events = Arc::clone(&state.events); 156 run_blocking(move || { 157 let _guard = cob_locks.repo(&repo); 158 owner_unmoved(&index, &repo, &owner)?; 159 let git = layout.open(&repo)?; 160 let changed = grant_set_apply::<CollaboratorsCob>( 161 &CobStore::new(&git), 162 &CobHome::from(&repo), 163 CollaboratorsChange::Remove(removal), 164 &signer, 165 now, 166 false, 167 )?; 168 index.refresh_collaborators(&repo)?; 169 if changed { 170 events.publish(&RepoCollaboratorUpdate::removed(event_subject, event_repo)); 171 } 172 Ok(()) 173 }) 174 .await?; 175 176 Ok(ok_empty()) 177}