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)]
26pub(crate) struct SubjectInput {
27 pub(crate) 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 grant_membership(
44 &state,
45 Grant {
46 subject,
47 added_by: actor,
48 created_at: state.now(),
49 },
50 )
51 .await
52}
53
54pub(crate) async fn grant_membership<H: HttpTransport, C: Clock>(
55 state: &Arc<XrpcState<H, C>>,
56 grant: Grant,
57) -> Result<Response, XrpcError> {
58 if state.admins.contains(&grant.subject)
59 || matches!(state.index.is_member(&grant.subject), Resolved::Ready(true))
60 {
61 return Ok(ok_empty());
62 }
63
64 let now = grant.created_at;
65 let event_subject = grant.subject.clone();
66 let signer = state.secrets.signer(&state.knot_did)?;
67 let meta_path = state.meta_path.clone();
68 let index = Arc::clone(&state.index);
69 let cob_locks = Arc::clone(&state.cob_locks);
70 let events = Arc::clone(&state.events);
71 let home = CobHome::from(&state.knot_did);
72 run_blocking(move || {
73 let _guard = cob_locks.meta();
74 let meta = Repo::open(&meta_path)?;
75 let changed = grant_set_apply::<MembersCob>(
76 &CobStore::new(&meta),
77 &home,
78 MembersChange::Add(grant),
79 &signer,
80 now,
81 true,
82 )?;
83 index.refresh_members()?;
84 if changed {
85 events.publish(&KnotMemberUpdate::added(event_subject));
86 }
87 Ok(())
88 })
89 .await?;
90
91 Ok(ok_empty())
92}
93
94pub(crate) async fn remove_member<H: HttpTransport, C: Clock>(
95 State(state): State<Arc<XrpcState<H, C>>>,
96 headers: HeaderMap,
97 method: crate::Method,
98 body: Bytes,
99) -> Result<Response, XrpcError> {
100 let actor = state.authenticate(&headers, &method).await?;
101 let acl = KnotAcl::new(&state.admins, state.admission, &state.index);
102 if !can_admin_knot(&acl, &actor).is_allowed() {
103 return Err(XrpcError::forbidden("only knot admin may remove members"));
104 }
105
106 let SubjectInput { subject } = decode(&body)?;
107 if matches!(state.index.is_member(&subject), Resolved::Ready(false)) {
108 return Ok(ok_empty());
109 }
110
111 let now = state.now();
112 let event_subject = subject.clone();
113 let removal = Removal { subject };
114 let signer = state.secrets.signer(&state.knot_did)?;
115 let meta_path = state.meta_path.clone();
116 let index = Arc::clone(&state.index);
117 let cob_locks = Arc::clone(&state.cob_locks);
118 let events = Arc::clone(&state.events);
119 let home = CobHome::from(&state.knot_did);
120 run_blocking(move || {
121 let _guard = cob_locks.meta();
122 let meta = Repo::open(&meta_path)?;
123 let changed = grant_set_apply::<MembersCob>(
124 &CobStore::new(&meta),
125 &home,
126 MembersChange::Remove(removal),
127 &signer,
128 now,
129 false,
130 )?;
131 index.refresh_members()?;
132 if changed {
133 events.publish(&KnotMemberUpdate::removed(event_subject));
134 }
135 Ok(())
136 })
137 .await?;
138
139 Ok(ok_empty())
140}