1use std::borrow::Borrow;
4use std::cell::RefCell;
5use std::collections::VecDeque;
6use std::fmt::Debug;
7use std::marker::PhantomData;
8
9use p2panda_auth::group;
10use p2panda_auth::traits::{Conditions, Operation as GroupsOperationTrait};
11use p2panda_core::{Extensions, Hash, LogId, VerifyingKey};
12use p2panda_store::groups::GroupsStore;
13use p2panda_store::{SqliteError, SqliteStore, Transaction};
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16use tokio::sync::Notify;
17use tracing::debug;
18
19use crate::Processor;
20use crate::groups::{GroupsArgs, GroupsOperation};
21
22type GroupsCrdt<C> = group::GroupCrdt<VerifyingKey, Hash, GroupsOperation<C>, C, StrongRemove<C>>;
23
24type GroupsCrdtError<C> =
25 group::GroupCrdtError<VerifyingKey, Hash, GroupsOperation<C>, C, StrongRemove<C>>;
26
27type StrongRemove<C> = group::resolver::StrongRemove<VerifyingKey, Hash, GroupsOperation<C>, C>;
28
29#[derive(Clone)]
30pub enum GroupsResult {
31 Processed,
32 Noop,
33}
34
35impl GroupsResult {
36 pub fn was_processed(self) -> bool {
37 match self {
38 Self::Processed => true,
39 Self::Noop => false,
40 }
41 }
42}
43
44pub struct Groups<T, E, L, C = ()> {
46 store: SqliteStore,
47 notify: Notify,
48 queue: RefCell<VecDeque<(T, GroupsResult)>>,
49 _marker: PhantomData<(E, L, C)>,
50}
51
52impl<T, E, L, C> Groups<T, E, L, C>
53where
54 E: Extensions,
55 L: LogId,
56 C: Conditions + Serialize + for<'a> Deserialize<'a>,
57{
58 pub fn new(store: SqliteStore) -> Self {
59 Self {
60 store,
61 notify: Notify::new(),
62 queue: RefCell::new(VecDeque::new()),
63 _marker: PhantomData,
64 }
65 }
66}
67
68impl<T, E, L, C> Processor<T> for Groups<T, E, L, C>
69where
70 T: Borrow<GroupsArgs<C>>,
71 E: Extensions,
72 L: LogId,
73 C: Conditions + Serialize + for<'a> Deserialize<'a>,
74{
75 type Output = (T, GroupsResult);
76
77 type Error = (T, GroupsError<C>);
78
79 async fn process(&self, input: T) -> Result<(), Self::Error> {
80 let input_args: &GroupsArgs<C> = input.borrow();
81
82 let result = if let GroupsArgs::Process {
87 state_id,
88 operation,
89 } = input_args
90 {
91 let permit = match self.store.begin().await {
92 Ok(permit) => permit,
93 Err(err) => return Err((input, err.into())),
94 };
95
96 let mut y = match GroupsStore::<GroupsOperation<C>, C>::get_groups_state_tx(
97 &self.store,
98 *state_id,
99 )
100 .await
101 {
102 Err(err) => return Err((input, err.into())),
103 Ok(Some(y)) => y,
104 Ok(None) => Default::default(),
105 };
106
107 debug!(
108 group_id = %operation.group_id(),
109 "current group membership: {:?}",
110 y.members(operation.group_id())
111 );
112
113 debug!(id = %operation.id, "apply operation to group state");
114
115 y = match GroupsCrdt::process(y, operation) {
116 Ok(y) => y,
117 Err(err) => return Err((input, err.into())),
118 };
119
120 if let Err(err) = self.store.set_groups_state_tx(*state_id, &y).await {
121 return Err((input, err.into()));
122 }
123
124 if let Err(err) = self.store.commit(permit).await {
125 return Err((input, err.into()));
126 }
127
128 debug!(
129 group_id = %operation.group_id(),
130 "new group membership: {:?}",
131 y.members(operation.group_id())
132 );
133
134 (input, GroupsResult::Processed)
135 } else {
136 (input, GroupsResult::Noop)
137 };
138
139 self.queue.borrow_mut().push_back(result);
140 self.notify.notify_one(); Ok(())
143 }
144
145 async fn next(&self) -> Result<Self::Output, Self::Error> {
146 loop {
147 if let Some(item) = self.queue.borrow_mut().pop_front() {
148 return Ok(item);
149 }
150
151 self.notify.notified().await;
153 }
154 }
155}
156
157#[allow(clippy::large_enum_variant)]
159#[derive(Debug, Error)]
160pub enum GroupsError<C>
161where
162 C: Conditions,
163{
164 #[error(transparent)]
165 Store(#[from] SqliteError),
166
167 #[error(transparent)]
168 Groups(#[from] GroupsCrdtError<C>),
169}
170
171#[cfg(test)]
172mod tests {
173 use std::borrow::Borrow;
174
175 use p2panda_auth::group::{GroupAction, GroupCrdtState, GroupMember};
176 use p2panda_auth::{Access, GroupsExtensionArgs};
177 use p2panda_core::test_utils::{TestLog, setup_logging};
178 use p2panda_core::traits::{Digest, Provenance};
179 use p2panda_core::{Hash, Operation, SigningKey, Topic, VerifyingKey};
180 use p2panda_store::groups::GroupsStore;
181 use p2panda_store::{SqliteStore, Transaction, tx_unwrap};
182 use serde::{Deserialize, Serialize};
183
184 use crate::Processor;
185 use crate::groups::{GroupsArgs, GroupsOperation};
186 use crate::ingest::{Ingest, IngestArgs};
187 use crate::orderer::Ordering;
188
189 type LogId = usize;
190
191 type GroupsState = GroupCrdtState<VerifyingKey, Hash, GroupsOperation, ()>;
192
193 type Groups = crate::groups::Groups<Event, TestExtensions, LogId, ()>;
194
195 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196 struct TestExtensions {
197 log_id: LogId,
198 dependencies: Vec<Hash>,
199 groups: Option<GroupsExtensionArgs>,
200 }
201
202 impl From<GroupsExtensionArgs> for TestExtensions {
203 fn from(args: GroupsExtensionArgs) -> Self {
204 TestExtensions {
205 log_id: 0,
206 dependencies: args.dependencies.clone(),
207 groups: Some(args),
208 }
209 }
210 }
211
212 #[derive(Clone, Debug, PartialEq, Eq)]
213 struct Event {
214 pub operation: Operation<TestExtensions>,
215 pub ingest_args: IngestArgs<LogId, Topic>,
216 pub groups_args: GroupsArgs<()>,
217 }
218
219 impl From<Operation<TestExtensions>> for Event {
220 fn from(operation: Operation<TestExtensions>) -> Self {
221 Self {
222 ingest_args: IngestArgs {
223 log_id: operation.header.extensions.log_id,
224 topic: Hash::digest("test").into(),
225 prune_flag: false,
226 },
227 groups_args: match operation.header.extensions.groups {
228 Some(ref groups) => GroupsArgs::Process {
229 state_id: Hash::digest("default"),
231 operation: GroupsOperation {
232 id: operation.hash(),
233 author: operation.author(),
234 dependencies: operation.header.extensions.dependencies.clone(),
235 group_id: groups.group_id,
236 action: groups.action.clone(),
237 },
238 },
239 None => GroupsArgs::Ignore,
240 },
241 operation,
242 }
243 }
244 }
245
246 impl Borrow<IngestArgs<LogId, Topic>> for Event {
249 fn borrow(&self) -> &IngestArgs<LogId, Topic> {
250 &self.ingest_args
251 }
252 }
253
254 impl Borrow<Operation<TestExtensions>> for Event {
255 fn borrow(&self) -> &Operation<TestExtensions> {
256 &self.operation
257 }
258 }
259
260 impl Ordering<Hash> for Operation<TestExtensions> {
263 fn dependencies(&self) -> &[Hash] {
264 &self.header.extensions.dependencies
265 }
266 }
267
268 impl Borrow<GroupsArgs<()>> for Event {
271 fn borrow(&self) -> &GroupsArgs<()> {
272 &self.groups_args
273 }
274 }
275
276 #[tokio::test]
277 async fn basic_processing() {
278 setup_logging();
279
280 let store = SqliteStore::temporary().await;
281
282 let state_id = Hash::digest(b"default");
283 let group_id = SigningKey::generate().verifying_key();
284
285 let alice_log = TestLog::new();
286 let bobby_log = TestLog::new();
287
288 let alice = alice_log.author();
289 let bobby = bobby_log.author();
290
291 let args = GroupsExtensionArgs {
292 group_id,
293 action: GroupAction::Create {
294 initial_members: vec![
295 (GroupMember::Individual(alice), <Access>::manage()),
296 (GroupMember::Individual(bobby), <Access>::manage()),
297 ],
298 },
299 dependencies: vec![],
300 };
301
302 let operation: Operation<TestExtensions> =
303 alice_log.operation(&[], TestExtensions::from(args.clone()));
304
305 let event = Event::from(operation);
306
307 let ingest = Ingest::new(store.clone());
309 ingest.process(event.clone()).await.unwrap();
310
311 let groups = Groups::new(store.clone());
312 groups.process(event).await.unwrap();
313
314 let (_processed_op, result) = groups.next().await.unwrap();
315 assert!(result.was_processed());
316
317 let permit = store.begin().await.unwrap();
318 let y: GroupsState = store.get_groups_state_tx(state_id).await.unwrap().unwrap();
319 store.commit(permit).await.unwrap();
320
321 let members = y.members(group_id);
322 assert_eq!(members.len(), 2);
323 assert!(members.contains(&(alice, Access::manage())));
324 assert!(members.contains(&(bobby, Access::manage())));
325 }
326
327 #[tokio::test]
328 async fn device_groups_single_context() {
329 let alice_log = TestLog::new();
330 let bobby_log = TestLog::new();
331 let cathy_log = TestLog::new();
332
333 let alice = alice_log.author();
334 let bobby = bobby_log.author();
335 let cathy = cathy_log.author();
336
337 let alice_store = SqliteStore::temporary().await;
338 let bobby_store = SqliteStore::temporary().await;
339 let cathy_store = SqliteStore::temporary().await;
340
341 let alice_device_group = SigningKey::generate().verifying_key();
342 let bobby_device_group = SigningKey::generate().verifying_key();
343 let cathy_device_group = SigningKey::generate().verifying_key();
344
345 let ab_chat = SigningKey::generate().verifying_key();
346 let bc_chat = SigningKey::generate().verifying_key();
347
348 let alice_ingest = Ingest::new(alice_store.clone());
349 let bobby_ingest = Ingest::new(bobby_store.clone());
350 let cathy_ingest = Ingest::new(cathy_store.clone());
351
352 let alice_groups = Groups::new(alice_store.clone());
353 let bobby_groups = Groups::new(bobby_store.clone());
354 let cathy_groups = Groups::new(cathy_store.clone());
355
356 let args = GroupsExtensionArgs {
359 group_id: alice_device_group,
360 action: GroupAction::Create {
361 initial_members: vec![(GroupMember::Individual(alice), Access::manage())],
362 },
363 dependencies: vec![],
364 };
365
366 let create_alice_device_00: Operation<TestExtensions> =
367 alice_log.operation(&[], TestExtensions::from(args));
368
369 let event_00 = Event::from(create_alice_device_00.clone());
370
371 alice_ingest.process(event_00.clone()).await.unwrap();
372 alice_groups.process(event_00).await.unwrap();
373
374 let args = GroupsExtensionArgs {
375 group_id: bobby_device_group,
376 action: GroupAction::Create {
377 initial_members: vec![(GroupMember::Individual(bobby), Access::manage())],
378 },
379 dependencies: vec![],
380 };
381 let create_bobby_device_01: Operation<TestExtensions> =
382 bobby_log.operation(&[], TestExtensions::from(args));
383 let event_01 = Event::from(create_bobby_device_01.clone());
384
385 bobby_ingest.process(event_01.clone()).await.unwrap();
386 bobby_groups.process(event_01.clone()).await.unwrap();
387
388 let args = GroupsExtensionArgs {
389 group_id: cathy_device_group,
390 action: GroupAction::Create {
391 initial_members: vec![(GroupMember::Individual(cathy), Access::manage())],
392 },
393 dependencies: vec![],
394 };
395 let create_cathy_device_02: Operation<TestExtensions> =
396 cathy_log.operation(&[], TestExtensions::from(args));
397 let event_02 = Event::from(create_cathy_device_02.clone());
398
399 cathy_ingest.process(event_02.clone()).await.unwrap();
400 cathy_groups.process(event_02).await.unwrap();
401
402 alice_ingest.process(event_01.clone()).await.unwrap();
406 alice_groups.process(event_01.clone()).await.unwrap();
407
408 let y: GroupsState = tx_unwrap!(alice_store, {
410 let state_id = Hash::digest("default");
411 alice_store
412 .get_groups_state_tx(state_id)
413 .await
414 .unwrap()
415 .unwrap()
416 });
417
418 let args = GroupsExtensionArgs {
419 group_id: ab_chat,
420 action: GroupAction::Create {
421 initial_members: vec![
422 (GroupMember::Group(alice_device_group), Access::write()),
423 (GroupMember::Group(bobby_device_group), Access::write()),
424 ],
425 },
426 dependencies: y.heads_filtered(&[alice_device_group, bobby_device_group]),
427 };
428 let create_alice_bobby_chat_03: Operation<TestExtensions> =
429 alice_log.operation(&[], TestExtensions::from(args));
430 let event_03 = Event::from(create_alice_bobby_chat_03.clone());
431
432 alice_ingest.process(event_03.clone()).await.unwrap();
433 alice_groups.process(event_03).await.unwrap();
434
435 for op in [create_alice_device_00.clone(), create_alice_bobby_chat_03] {
437 let event: Event = op.into();
438
439 bobby_ingest.process(event.clone()).await.unwrap();
440 bobby_groups.process(event).await.unwrap();
441 }
442
443 for store in [alice_store.clone(), bobby_store.clone()] {
445 let y: GroupsState = tx_unwrap!(store, {
446 let state_id = Hash::digest("default");
447 store.get_groups_state_tx(state_id).await.unwrap().unwrap()
448 });
449
450 let mut members = y.members(ab_chat);
451 members.sort();
452
453 assert_eq!(members.len(), 2);
454 assert!(members.contains(&(alice, Access::write())));
455 assert!(members.contains(&(bobby, Access::write())));
456 }
457
458 cathy_ingest.process(event_01.clone()).await.unwrap();
462 cathy_groups.process(event_01).await.unwrap();
463
464 let y: GroupsState = tx_unwrap!(cathy_store, {
466 let state_id = Hash::digest("default");
467 cathy_store
468 .get_groups_state_tx(state_id)
469 .await
470 .unwrap()
471 .unwrap()
472 });
473
474 let args = GroupsExtensionArgs {
475 group_id: bc_chat,
476 action: GroupAction::Create {
477 initial_members: vec![
478 (GroupMember::Group(bobby_device_group), Access::write()),
479 (GroupMember::Group(cathy_device_group), Access::write()),
480 ],
481 },
482 dependencies: y.heads_filtered(&[bobby_device_group, cathy_device_group]),
483 };
484 let create_bobby_cathy_chat_04: Operation<TestExtensions> =
485 cathy_log.operation(&[], TestExtensions::from(args));
486 let event_04 = Event::from(create_bobby_cathy_chat_04.clone());
487
488 cathy_ingest.process(event_04.clone()).await.unwrap();
489 cathy_groups.process(event_04).await.unwrap();
490
491 for op in [create_cathy_device_02.clone(), create_bobby_cathy_chat_04] {
493 let event: Event = op.into();
494
495 bobby_ingest.process(event.clone()).await.unwrap();
496 bobby_groups.process(event).await.unwrap();
497 }
498
499 for store in [cathy_store, bobby_store] {
501 let y: GroupsState = tx_unwrap!(store, {
502 let state_id = Hash::digest("default");
503 store.get_groups_state_tx(state_id).await.unwrap().unwrap()
504 });
505 let mut members = y.members(bc_chat);
506 members.sort();
507
508 assert_eq!(members.len(), 2);
509 assert!(members.contains(&(bobby, Access::write())));
510 assert!(members.contains(&(cathy, Access::write())));
511 }
512 }
513}