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