This repository has no description
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}