crew_rs/
crew_manager.rs

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}