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_admin_knot};
10use knot_cob::{CobHome, CobStore};
11use knot_cobs::{Grant, MembersChange, MembersCob, Removal};
12use knot_events::KnotMemberUpdate;
13use knot_git::Repo;
14use knot_index::Resolved;
15use knot_runtime::{Clock, HttpTransport};
16use knot_types::AccountDid;
17
18use crate::cob::grant_set_apply;
19use crate::error::XrpcError;
20use crate::{XrpcState, decode, ok_empty, run_blocking};
21
22pub(crate) const ADD_ROUTE: &str = "/xrpc/sh.tangled.knot.addMember";
23pub(crate) const REMOVE_ROUTE: &str = "/xrpc/sh.tangled.knot.removeMember";
24
25#[derive(Deserialize)]
26struct SubjectInput {
27 subject: AccountDid,
28}
29
30pub(crate) async fn add_member<H: HttpTransport, C: Clock>(
31 State(state): State<Arc<XrpcState<H, C>>>,
32 headers: HeaderMap,
33 method: crate::Method,
34 body: Bytes,
35) -> Result<Response, XrpcError> {
36 let actor = state.authenticate(&headers, &method).await?;
37 let acl = KnotAcl::new(&state.admins, state.admission, &state.index);
38 if !can_admin_knot(&acl, &actor).is_allowed() {
39 return Err(XrpcError::forbidden("only knot admin may add members"));
40 }
41
42 let SubjectInput { subject } = decode(&body)?;
43 if state.admins.contains(&subject)
44 || matches!(state.index.is_member(&subject), Resolved::Ready(true))
45 {
46 return Ok(ok_empty());
47 }
48
49 let now = state.now();
50 let event_subject = subject.clone();
51 let grant = Grant {
52 subject,
53 added_by: actor,
54 created_at: now,
55 };
56 let signer = state.secrets.signer(&state.knot_did)?;
57 let meta_path = state.meta_path.clone();
58 let index = Arc::clone(&state.index);
59 let cob_locks = Arc::clone(&state.cob_locks);
60 let events = Arc::clone(&state.events);
61 let home = CobHome::from(&state.knot_did);
62 run_blocking(move || {
63 let _guard = cob_locks.meta();
64 let meta = Repo::open(&meta_path)?;
65 let changed = grant_set_apply::<MembersCob>(
66 &CobStore::new(&meta),
67 &home,
68 MembersChange::Add(grant),
69 &signer,
70 now,
71 true,
72 )?;
73 index.refresh_members()?;
74 if changed {
75 events.publish(&KnotMemberUpdate::added(event_subject));
76 }
77 Ok(())
78 })
79 .await?;
80
81 Ok(ok_empty())
82}
83
84pub(crate) async fn remove_member<H: HttpTransport, C: Clock>(
85 State(state): State<Arc<XrpcState<H, C>>>,
86 headers: HeaderMap,
87 method: crate::Method,
88 body: Bytes,
89) -> Result<Response, XrpcError> {
90 let actor = state.authenticate(&headers, &method).await?;
91 let acl = KnotAcl::new(&state.admins, state.admission, &state.index);
92 if !can_admin_knot(&acl, &actor).is_allowed() {
93 return Err(XrpcError::forbidden("only knot admin may remove members"));
94 }
95
96 let SubjectInput { subject } = decode(&body)?;
97 if matches!(state.index.is_member(&subject), Resolved::Ready(false)) {
98 return Ok(ok_empty());
99 }
100
101 let now = state.now();
102 let event_subject = subject.clone();
103 let removal = Removal { subject };
104 let signer = state.secrets.signer(&state.knot_did)?;
105 let meta_path = state.meta_path.clone();
106 let index = Arc::clone(&state.index);
107 let cob_locks = Arc::clone(&state.cob_locks);
108 let events = Arc::clone(&state.events);
109 let home = CobHome::from(&state.knot_did);
110 run_blocking(move || {
111 let _guard = cob_locks.meta();
112 let meta = Repo::open(&meta_path)?;
113 let changed = grant_set_apply::<MembersCob>(
114 &CobStore::new(&meta),
115 &home,
116 MembersChange::Remove(removal),
117 &signer,
118 now,
119 false,
120 )?;
121 index.refresh_members()?;
122 if changed {
123 events.publish(&KnotMemberUpdate::removed(event_subject));
124 }
125 Ok(())
126 })
127 .await?;
128
129 Ok(ok_empty())
130}