1use std::{
2 collections::{HashMap, HashSet},
3 time::Duration,
4};
5
6use async_recursion::async_recursion;
7use audi::ListenerSet;
8use futures::{FutureExt, StreamExt};
9use mofo::Mofo;
10use objt::objt;
11use ridl::symm_encr::KeySecret;
12use tracing::debug;
13
14use crate::{
15 changes::RevealSecretToParent,
16 credential_source::CredentialSource,
17 crew::{Crew, CrewInner, Invitation, WeakCrew},
18 member::Member,
19 timeline::UncheckedTimeline,
20 AddRole, CredentialSourceHash, CrewChange, CrewID, CrewState, EntrustInfo, MemberCredential,
21 RevealSecret, SimpleCredentialSource, ADMIN_ROLE, CONTENT_SECRET, PARTICIPATION_SECRET,
22 READER_ROLE, READ_INVITATION_ROLE, SET_WRITE_ACCESS_INFO_ID, WRITER_ROLE,
23 WRITE_INVITATION_ROLE,
24};
25
26pub struct CrewManagerInner {
27 pub(crate) endr: endr::Node,
28 pub(crate) crews: HashMap<(CrewID, CredentialSourceHash), WeakCrew>,
29 pub(crate) credential_source: Box<dyn CredentialSource>,
30 pub(crate) background: Mofo,
31}
32
33objt!(
34 CrewManager,
35 WeakCrewManager,
36 CrewManagerInner,
37 endr: endr::Node,
38 background: Mofo
39);
40
41impl CrewManager {
42 pub fn new(
43 endr: endr::Node,
44 background: Mofo,
45 credential_source: Box<dyn CredentialSource>,
46 ) -> Self {
47 CrewManager::from_inner(CrewManagerInner {
48 endr,
49 crews: HashMap::new(),
50 credential_source,
51 background,
52 })
53 }
54
55 pub async fn create_crew(&self) -> (Crew, MemberCredential) {
56 self.create_crew_with_parents(std::iter::empty(), false)
57 .await
58 }
59
60 pub async fn create_crew_with_parents<I: IntoIterator<Item = CrewID>>(
61 &self,
62 parents: I,
63 use_admin_credential_from_parents: bool,
64 ) -> (Crew, MemberCredential) {
65 let parents: HashSet<_> = parents.into_iter().collect();
66
67 let (initial_state, own_admin) = if use_admin_credential_from_parents {
68 let initial_state = CrewState {
69 parents: parents.clone(),
70 roles: HashSet::new(),
71 shared_secrets: HashMap::new(),
72 entrusted_info: HashMap::new(),
73 statements: HashSet::new(),
74 parent_secret_map: HashMap::new(),
75 };
76 (initial_state, None)
77 } else {
78 let admin = MemberCredential::new_random();
79 let initial_state = CrewState {
80 parents: parents.clone(),
81 roles: HashSet::from_iter([(admin.pub_id(), ADMIN_ROLE.to_owned())]),
82 shared_secrets: HashMap::new(),
83 entrusted_info: HashMap::new(),
84 statements: HashSet::new(),
85 parent_secret_map: HashMap::new(),
86 };
87 (initial_state, Some(admin))
88 };
89
90 let (set_id, set_write_access) = self
91 .endr()
92 .create_set(Some(litl::Val::object([
93 ("type", litl::Val::str("crew")),
94 ("initialState", litl::to_val(initial_state).unwrap()),
95 ])))
96 .await;
97
98 let credential_source = self.borrow().credential_source.clone_ref();
99
100 if let Some(admin) = &own_admin {
101 credential_source
102 .add_credential(CrewID(set_id), admin.clone())
103 .await;
104 assert!(credential_source
105 .current_credentials_for(&CrewID(set_id))
106 .iter()
107 .any(|cred| cred.pub_id() == admin.pub_id()));
108 }
109
110 let parent_crews: HashMap<_, _> = parents
111 .into_iter()
112 .map(|parent_id| {
113 (
114 parent_id,
115 self.borrow()
116 .crews
117 .get(&(parent_id, credential_source.id_hash()))
118 .and_then(|weak| weak.upgrade())
119 .expect("Expected parent crew to be loaded on crew creation")
120 .clone_ref(),
121 )
122 })
123 .collect();
124
125 let crew = Crew::from_inner(CrewInner {
126 endr: self.endr(),
127 id: CrewID(set_id),
128 unchecked_timeline: UncheckedTimeline::default(),
129 last_update: None,
130 credential_source: credential_source.clone_ref(),
131 listeners: ListenerSet::new(),
132 background: self.background(),
133 manager: self.clone_ref(),
134 parents_unchecked: HashMap::new(),
135 shared_secret_cache: HashMap::new(),
136 });
137
138 let endr = self.endr();
139 let crew_for_updates = crew.clone();
140
141 self.background().add_background_task(
142 async move {
143 endr.diffs(set_id, "Update crew".to_owned())
144 .for_each(|diff| crew_for_updates.handle_set_diff(diff))
145 .await
146 }
147 .boxed_local(),
148 );
149
150 let participation_secret = KeySecret::new_random();
151 let content_secret = KeySecret::new_random();
152
153
154 crew.wait_for_state(|state| {
155 !crew.get_credentials_with_role(&HashSet::from_iter([ADMIN_ROLE.to_owned()])).is_empty()
156 })
157 .await;
158
159 let admin = own_admin.unwrap_or_else(|| {
160 crew.get_credentials_with_role(&HashSet::from_iter([ADMIN_ROLE.to_owned()]))
161 .into_iter().next().expect("Expected admin credential from parent").0
162 });
163
164 let first_entry = crew
165 .make_changes_with_write_access(
166 [
167 CrewChange::RevealSecret(RevealSecret {
168 secret_kind: PARTICIPATION_SECRET.to_owned(),
169 to: admin.signer().pub_id(),
170 encr: admin.pub_id().encrypt_from_anon(&participation_secret),
171 }),
172 CrewChange::RevealSecret(RevealSecret {
173 secret_kind: CONTENT_SECRET.to_owned(),
174 to: admin.signer().pub_id(),
175 encr: admin.pub_id().encrypt_from_anon(&content_secret),
176 }),
177 CrewChange::EntrustInfo(EntrustInfo {
178 to_secret_kind: PARTICIPATION_SECRET.to_owned(),
179 info_id: SET_WRITE_ACCESS_INFO_ID.to_owned(),
180 info: participation_secret
181 .encrypt(&set_write_access)
182 .as_encrypted_value(),
183 }),
184 ].into_iter().chain(parent_crews.iter().flat_map(|(parent_id, parent_crew)| {
185 let parent_content_secret = parent_crew.get_shared_secret(CONTENT_SECRET).expect("Expected to have content secret of parent when creating child crew");
186 let parent_participation_secret = parent_crew.get_shared_secret(PARTICIPATION_SECRET).expect("Expected to have participation secret of parent when creating child crew");
187
188 [CrewChange::RevealSecretToParent(RevealSecretToParent {
189 parent_id: *parent_id,
190 secret_kind: CONTENT_SECRET.to_owned(),
191 parent_secret_id: parent_content_secret.id,
192 encr: parent_content_secret.encrypt(&content_secret),
193 }),
194 CrewChange::RevealSecretToParent(RevealSecretToParent {
195 parent_id: *parent_id,
196 secret_kind: PARTICIPATION_SECRET.to_owned(),
197 parent_secret_id: parent_participation_secret.id,
198 encr: parent_participation_secret.encrypt(&participation_secret),
199 }),
200 ]
201 })),
202 set_write_access,
203 )
204 .await
205 .unwrap();
206
207 self.borrow_mut()
208 .crews
209 .insert((crew.id(), credential_source.id_hash()), crew.as_weak());
210
211 (crew, admin)
212 }
213
214 pub async fn load_crew(&self, id: CrewID) -> Crew {
215 let credential_source = self.borrow().credential_source.clone_ref();
216 self.load_crew_with_credential_source(id, credential_source)
217 .await
218 }
219
220 #[async_recursion(?Send)]
221 pub async fn load_crew_with_credential_source(
222 &self,
223 id: CrewID,
224 credential_source: impl CredentialSource + 'static,
225 ) -> Crew {
226 if let Some(existing) = self
227 .borrow()
228 .crews
229 .get(&(id, credential_source.id_hash()))
230 .and_then(|weak| weak.upgrade())
231 {
232 return existing.clone_ref();
233 }
234
235 let crew = Crew::from_inner(CrewInner {
236 endr: self.endr(),
237 id,
238 unchecked_timeline: UncheckedTimeline::default(),
239 last_update: None,
240 credential_source: credential_source.clone_ref(),
241 listeners: ListenerSet::new(),
242 background: self.background(),
243 manager: self.clone_ref(),
244 parents_unchecked: HashMap::new(),
245 shared_secret_cache: HashMap::new(),
246 });
247
248 let endr = self.endr();
249 let crew_for_updates = crew.clone();
250
251 self.borrow_mut()
252 .crews
253 .insert((crew.id(), credential_source.id_hash()), crew.as_weak());
254
255 let _loaded = endr.load_object(id.0).await;
256
257 self.background().add_background_task(
258 async move {
259 endr.diffs(id.0, "Update crew".to_owned())
260 .for_each(|diff| crew_for_updates.handle_set_diff(diff))
261 .await
262 }
263 .boxed_local(),
264 );
265
266
267
268 crew
269 }
270
271 pub fn get_loaded_crew(&self, id: CrewID) -> Option<Crew> {
272 let credential_source_id = self.borrow().credential_source.id_hash();
273 self.borrow()
274 .crews
275 .get(&(id, credential_source_id))
276 .and_then(|crew| match crew.upgrade() {
277 Some(crew) => Some(crew),
278 None => {
279 debug!("Crew was loaded but is now gone");
280 None
281 }
282 })
283 }
284
285 pub fn get_loaded_crew_with_credential_source(
286 &self,
287 id: CrewID,
288 credential_source: impl CredentialSource + 'static,
289 ) -> Option<Crew> {
290 self.borrow()
291 .crews
292 .get(&(id, credential_source.id_hash()))
293 .and_then(|crew| crew.upgrade())
294 }
295
296 pub async fn add_credential(&self, crew_id: CrewID, credential: MemberCredential) {
297 let credential_source = self.0.borrow().credential_source.clone_ref();
298 credential_source.add_credential(crew_id, credential).await;
299 }
300
301 pub async fn join_crew(&self, invitation: &Invitation) -> (Crew, MemberCredential) {
302 let invitation_credential_source = SimpleCredentialSource::new();
303 invitation_credential_source
304 .add_credential(invitation.crew_id, invitation.invitation_credential.clone())
305 .await;
306 let crew_for_invitation = self
307 .load_crew_with_credential_source(invitation.crew_id, invitation_credential_source)
308 .await;
309
310 crew_for_invitation
311 .wait_for_state(|state| {
312 !state
313 .roles_of(&invitation.invitation_credential.signer().pub_id())
314 .is_empty()
315 })
316 .await;
317
318 let new_member = MemberCredential::new_random();
319
320 let content_secret = crew_for_invitation
321 .get_shared_secret(CONTENT_SECRET)
322 .expect("Expected invitation to have access to content secret");
323 let maybe_participation_secret =
324 crew_for_invitation.get_shared_secret(PARTICIPATION_SECRET);
325
326 crew_for_invitation
327 .make_changes(
328 Some(CrewChange::AddRole(AddRole {
329 to: new_member.pub_id(),
330 role: match invitation.invitation_role.as_str() {
331 crate::READ_INVITATION_ROLE => READER_ROLE,
332 crate::WRITE_INVITATION_ROLE => WRITER_ROLE,
333 crate::ADMIN_INVITATION_ROLE => ADMIN_ROLE,
334 _ => panic!("Unknown invitation role"),
335 }
336 .to_owned(),
337 }))
338 .into_iter()
339 .chain(Some(CrewChange::RevealSecret(RevealSecret {
340 secret_kind: CONTENT_SECRET.to_owned(),
341 to: new_member.signer().pub_id(),
342 encr: new_member.pub_id().encrypt_from_anon(&content_secret),
343 })))
344 .chain(match invitation.invitation_role.as_str() {
345 crate::READ_INVITATION_ROLE => None,
346 crate::WRITE_INVITATION_ROLE | crate::ADMIN_INVITATION_ROLE => {
347 Some(CrewChange::RevealSecret(RevealSecret {
348 secret_kind: PARTICIPATION_SECRET.to_owned(),
349 to: new_member.signer().pub_id(),
350 encr: new_member.pub_id().encrypt_from_anon(
351 &maybe_participation_secret.expect(
352 "Expected invitation to have access to participation secret",
353 ),
354 ),
355 }))
356 }
357 _ => panic!("Unknown invitation role"),
358 }),
359 )
360 .await
361 .unwrap();
362
363 let credential_source = self.borrow().credential_source.clone_ref();
364 credential_source
365 .add_credential(invitation.crew_id, new_member.clone())
366 .await;
367
368 let crew = self.load_crew(invitation.crew_id).await;
369
370 crew.wait_for_state(|state| {
371 state.roles_of(&new_member.signer().pub_id()).contains(
372 match invitation.invitation_role.as_str() {
373 crate::READ_INVITATION_ROLE => READER_ROLE,
374 crate::WRITE_INVITATION_ROLE => WRITER_ROLE,
375 crate::ADMIN_INVITATION_ROLE => ADMIN_ROLE,
376 _ => panic!("Unknown invitation role"),
377 },
378 )
379 })
380 .await;
381
382 (crew, new_member)
383 }
384}