Skip to main content

p2panda_auth/group/crdt/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3pub(crate) mod state;
4
5use std::collections::{HashMap, HashSet};
6use std::fmt::Debug;
7use std::marker::PhantomData;
8
9use petgraph::prelude::DiGraphMap;
10use petgraph::visit::{Bfs, DfsPostOrder, IntoNodeIdentifiers, NodeIndexable, Reversed};
11#[cfg(any(test, feature = "serde"))]
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15use crate::access::Access;
16use crate::group::{GroupAction, GroupMember, GroupMembersState, GroupMembershipError};
17use crate::traits::{Conditions, IdentityHandle, Operation, OperationId, Resolver};
18
19/// Max depth of group nesting allowed.
20///
21/// Depth is checked during group state queries and if the depth is exceeded further additions are
22/// ignored. The main reason for this check is to protect against accidental group nesting cycles
23/// which may occur as a result of concurrent operations.
24const MAX_NESTED_DEPTH: u32 = 1000;
25
26/// Inner error types for GroupCrdt.
27#[derive(Debug, Error)]
28pub enum GroupCrdtInnerError<OP> {
29    #[error("states {0:?} not found")]
30    StatesNotFound(Vec<OP>),
31}
32
33/// Error types for GroupCrdt.
34#[derive(Debug, Error)]
35pub enum GroupCrdtError<ID, OP, M, C, RS>
36where
37    ID: IdentityHandle,
38    OP: OperationId + Ord,
39    RS: Resolver<ID, OP, M, C>,
40{
41    #[error(transparent)]
42    Inner(#[from] GroupCrdtInnerError<OP>),
43
44    #[error("duplicate operation {0} processed in group {1}")]
45    DuplicateOperation(OP, ID),
46
47    #[error("group cycle detected adding {0} to {1} operation={2}")]
48    GroupCycle(ID, ID, OP),
49
50    #[error("state change error processing operation {0}: {1:?}")]
51    StateChangeError(OP, GroupMembershipError<GroupMember<ID>>),
52
53    #[error("attempted to add group {0} with manage access")]
54    ManagerGroupsNotAllowed(ID),
55
56    #[error("resolver error: {0}")]
57    Resolver(RS::Error),
58}
59
60pub(crate) type GroupStates<ID, C> = HashMap<ID, GroupMembersState<GroupMember<ID>, C>>;
61
62/// Inner state object for `GroupCrdt` which contains the actual groups state,
63/// including operation graph and membership snapshots.
64#[derive(Debug)]
65#[cfg_attr(
66    any(test, feature = "test_utils", feature = "processor"),
67    derive(Clone)
68)]
69#[cfg_attr(any(test, feature = "serde"), derive(Deserialize, Serialize))]
70pub struct GroupCrdtInnerState<ID, OP, M, C>
71where
72    ID: IdentityHandle,
73    OP: OperationId + Ord,
74{
75    /// All operations processed by this group.
76    pub operations: HashMap<OP, M>,
77
78    /// All operations who's actions should be ignored.
79    pub ignore: HashSet<OP>,
80
81    /// All operations which are part of a mutual remove cycle.
82    pub mutual_removes: HashSet<OP>,
83
84    /// All resolved states.
85    pub states: HashMap<OP, GroupStates<ID, C>>,
86
87    /// Operation graph of all auth operations.
88    pub graph: DiGraphMap<OP, ()>,
89}
90
91impl<ID, OP, M, C> Default for GroupCrdtInnerState<ID, OP, M, C>
92where
93    ID: IdentityHandle,
94    OP: OperationId + Ord,
95{
96    fn default() -> Self {
97        Self {
98            operations: Default::default(),
99            ignore: Default::default(),
100            mutual_removes: Default::default(),
101            states: Default::default(),
102            graph: Default::default(),
103        }
104    }
105}
106
107impl<ID, OP, M, C> GroupCrdtInnerState<ID, OP, M, C>
108where
109    ID: IdentityHandle,
110    OP: OperationId + Ord,
111    M: Operation<ID, OP, C>,
112    C: Conditions,
113{
114    /// Current tips for the groups operation graph.
115    pub fn heads(&self) -> HashSet<OP> {
116        self.graph
117            // TODO: clone required here when converting the GraphMap into a Graph. We do this
118            // because the GraphMap api does not include the "externals" method, where as the
119            // Graph api does. We use GraphMap as we can then access nodes by the id we assign
120            // them rather than the internally assigned id generated when using Graph. We can use
121            // Graph and track the indexes ourselves in order to avoid this conversion, or maybe
122            // there is a way to get "externals" on GraphMap (which I didn't find yet). More
123            // investigation required.
124            .clone()
125            .into_graph::<usize>()
126            .externals(petgraph::Direction::Outgoing)
127            .map(|idx| self.graph.from_index(idx.index()))
128            .collect::<HashSet<_>>()
129    }
130
131    /// Get graph tips filtered to only those which included "create" operation for passed group
132    /// ids in their causal history.
133    pub fn heads_filtered(&self, groups: &[ID]) -> HashSet<OP> {
134        let global_heads = self.heads();
135        global_heads
136            .into_iter()
137            .filter(|id| {
138                let reversed = Reversed(&self.graph);
139                let mut bfs = Bfs::new(&reversed, *id);
140                while let Some(inner_id) = bfs.next(&reversed) {
141                    let operation = self
142                        .operations
143                        .get(&inner_id)
144                        .expect("operation is present in map");
145                    if operation.action().is_create() && groups.contains(&operation.group_id()) {
146                        return true;
147                    }
148                }
149                false
150            })
151            .collect()
152    }
153
154    /// Current group states.
155    ///
156    /// This method gets the state at all graph tips and then merges them together into one new
157    /// state which represents the current state of the groups.
158    pub fn current_state(&self) -> GroupStates<ID, C> {
159        self.merge_states(&self.heads())
160            .expect("states exist for processed operations")
161    }
162
163    /// Get the state at a certain point in history.
164    pub fn state_at(
165        &self,
166        dependencies: &HashSet<OP>,
167    ) -> Result<GroupStates<ID, C>, GroupCrdtInnerError<OP>> {
168        self.merge_states(dependencies)
169    }
170
171    /// Merge multiple states together.
172    fn merge_states(
173        &self,
174        ids: &HashSet<OP>,
175    ) -> Result<GroupStates<ID, C>, GroupCrdtInnerError<OP>> {
176        let mut current_state = HashMap::new();
177        for id in ids {
178            // Unwrap as this method is only used internally where all requested states should exist.
179            let group_states = match self.states.get(id) {
180                Some(group_states) => group_states.clone(),
181                None => {
182                    return Err(GroupCrdtInnerError::StatesNotFound(
183                        ids.iter().cloned().collect(),
184                    ));
185                }
186            };
187            for (id, state) in group_states.into_iter() {
188                current_state
189                    .entry(id)
190                    .and_modify(
191                        |current_state: &mut GroupMembersState<GroupMember<ID>, C>| {
192                            *current_state = state::merge(state.clone(), current_state.clone())
193                        },
194                    )
195                    .or_insert(state);
196            }
197        }
198        Ok(current_state)
199    }
200
201    fn members_inner(
202        &self,
203        group_id: ID,
204        members: &mut HashMap<ID, Access<C>>,
205        root_access: Option<Access<C>>,
206        mut depth: u32,
207    ) {
208        // If we reached max nesting depth exit from the traversal.
209        if depth == MAX_NESTED_DEPTH {
210            return;
211        }
212        depth += 1;
213
214        let current_states = self.current_state();
215        let Some(group_state) = current_states.get(&group_id) else {
216            return;
217        };
218
219        for (member, access) in group_state.access_levels() {
220            // As we recurse into sub-groups we must assure that the newly
221            // assignable access level is never higher than the previous root
222            // access level. To do this we take whichever is less.
223            let next_access = match root_access.clone() {
224                Some(root_access) => {
225                    if access <= root_access {
226                        access.clone()
227                    } else {
228                        root_access
229                    }
230                }
231                None => access.clone(),
232            };
233
234            match member {
235                GroupMember::Individual(id) => {
236                    // If this is an individual member, then add them straight to the members map.
237                    members
238                        .entry(id)
239                        .and_modify(|current_access| {
240                            // If the transitive access level this member holds (the access
241                            // level the member has in it's sub-group) is greater than it's
242                            // current access level, but not greater than the root access
243                            // level (the access level initially assigned from the parent
244                            // group) then update the access level.
245
246                            // @TODO: we need to combine access levels here,
247                            // which requires adding a trait bound to conditions
248                            // which allows combining them as well. Or we return
249                            // an array of access levels for each peer.
250                            if *current_access < next_access {
251                                *current_access = next_access.clone();
252                            }
253                        })
254                        .or_insert_with(|| next_access);
255                }
256                GroupMember::Group(id) => self.members_inner(id, members, Some(next_access), depth),
257            }
258        }
259    }
260
261    /// Get all current members of a group.
262    pub fn members(&self, group_id: ID) -> Vec<(ID, Access<C>)> {
263        let mut members = HashMap::new();
264        self.members_inner(group_id, &mut members, None, 0);
265        members.into_iter().collect()
266    }
267
268    pub(crate) fn would_create_cycle(&self, operation: &M) -> bool {
269        let parent_group_id = operation.group_id();
270
271        if let GroupAction::Add {
272            member: GroupMember::Group(child_group_id),
273            ..
274        } = &operation.action()
275        {
276            let states = self.current_state();
277            let mut stack = vec![*child_group_id];
278            let mut visited = HashSet::new();
279
280            while let Some(child_group_id) = stack.pop() {
281                if !visited.insert(child_group_id) {
282                    continue;
283                }
284                if child_group_id == parent_group_id {
285                    // Found a path from child group to parent.
286                    return true;
287                }
288                if let Some(group_state) = states.get(&child_group_id) {
289                    for (member, _) in group_state.access_levels() {
290                        if let GroupMember::Group(id) = member {
291                            stack.push(id);
292                        }
293                    }
294                }
295            }
296        }
297
298        false
299    }
300}
301
302/// State object for `GroupCrdt` containing an orderer state and the inner
303/// state.
304#[derive(Debug)]
305#[cfg_attr(
306    any(test, feature = "test_utils", feature = "processor"),
307    derive(Clone)
308)]
309#[cfg_attr(
310    any(test, feature = "serde"),
311    derive(Deserialize, Serialize),
312    serde(bound(
313        deserialize = "
314            ID: Deserialize<'de>, 
315            OP: Deserialize<'de>, 
316            M: Deserialize<'de>, 
317            C: Deserialize<'de>, 
318        ",
319        serialize = "
320            ID: Serialize, 
321            OP: Serialize, 
322            M: Serialize, 
323            C: Serialize, 
324        "
325    ))
326)]
327pub struct GroupCrdtState<ID, OP, M, C>
328where
329    ID: IdentityHandle,
330    OP: OperationId + Ord,
331{
332    /// Inner groups state.
333    pub inner: GroupCrdtInnerState<ID, OP, M, C>,
334}
335
336impl<ID, OP, M, C> Default for GroupCrdtState<ID, OP, M, C>
337where
338    ID: IdentityHandle,
339    OP: OperationId + Ord,
340    M: Operation<ID, OP, C>,
341    C: Conditions,
342{
343    fn default() -> Self {
344        Self {
345            inner: Default::default(),
346        }
347    }
348}
349
350impl<ID, OP, M, C> GroupCrdtState<ID, OP, M, C>
351where
352    ID: IdentityHandle,
353    OP: OperationId + Ord,
354    M: Operation<ID, OP, C>,
355    C: Conditions,
356{
357    /// Instantiate a new state.
358    pub fn new() -> Self {
359        Self::default()
360    }
361
362    /// Get all direct members of a group.
363    ///
364    /// This method does not recurse into sub-groups, but rather returns only
365    /// the direct group members and their access levels.
366    pub fn root_members(&self, group_id: ID) -> Vec<(GroupMember<ID>, Access<C>)> {
367        match self.inner.current_state().get(&group_id) {
368            Some(group_y) => group_y.access_levels(),
369            None => vec![],
370        }
371    }
372
373    /// Get all transitive members of a group.
374    ///
375    /// This method recurses into all sub-groups and returns a resolved list of
376    /// individual group members and their access levels.
377    pub fn members(&self, group_id: ID) -> Vec<(ID, Access<C>)> {
378        self.inner.members(group_id)
379    }
380
381    /// Returns `true` if the passed group exists in the current state.
382    pub fn has_group(&self, group_id: ID) -> bool {
383        self.inner.current_state().contains_key(&group_id)
384    }
385
386    /// Current tips for the groups operation graph.
387    pub fn heads(&self) -> Vec<OP> {
388        self.inner.heads().into_iter().collect()
389    }
390
391    /// Get graph tips filtered to only those which included "create" operation for passed group
392    /// ids in their causal history.
393    pub fn heads_filtered(&self, groups: &[ID]) -> Vec<OP> {
394        self.inner.heads_filtered(groups).into_iter().collect()
395    }
396}
397
398/// Core group CRDT for maintaining group membership state in a decentralized
399/// system.
400///
401/// Group members can be assigned different access levels, where only a sub-set
402/// of members can mutate the state of the group itself. Group members can be
403/// (immutable) individuals or (mutable) sub-groups.
404///
405/// The core data type is a Directed Acyclic Graph of all operations containing
406/// group management actions. Operations refer to the previous global state (set
407/// of graph tips) in their "dependencies" field, this is the local state when
408/// an actor creates a new auth action; these references make up the edges in
409/// the graph.
410///
411/// A requirement of the protocol is that all messages are processed in
412/// partial-order. When using a dependency graph structure (as is the case in
413/// this implementation) it is possible to achieve partial-ordering by only
414/// processing a message once all it's dependencies have themselves been
415/// processed.
416///
417/// Group state is maintained using the state object `GroupMembersState`. Every
418/// time an action is processed, a new state is generated and added to the map
419/// of all states. When a new operation is received, it's previous state is
420/// calculated and then the message applied, resulting in a new state.
421///
422/// Group membership rules are checked when an action is applied to the previous
423/// state, read more in the `crdt::state` module.
424///
425/// The struct has several generic parameters which allow users to specify their
426/// own core types and to customise behavior when handling concurrent changes
427/// when resolving a graph to it's final state.
428///
429/// - ID : identifier for both an individual actor and group.
430/// - OP : identifier for an operation.
431/// - C  : conditions which restrict an access level.
432/// - RS : generic resolver which contains logic for deciding when group state
433///   rebuilds are required, and how concurrent actions are handled. See the
434///   `resolver` module for different implementations.
435/// - ORD: orderer which exposes an API for creating and processing operations
436///   with meta-data which allow them to be processed in partial order.
437#[derive(Clone, Debug, Default)]
438pub struct GroupCrdt<ID, OP, M, C, RS> {
439    _phantom: PhantomData<(ID, OP, M, C, RS)>,
440}
441
442impl<ID, OP, M, C, RS> GroupCrdt<ID, OP, M, C, RS>
443where
444    ID: IdentityHandle,
445    OP: OperationId + Ord,
446    M: Operation<ID, OP, C> + Clone,
447    C: Conditions,
448    RS: Resolver<ID, OP, M, C, State = GroupCrdtInnerState<ID, OP, M, C>>,
449{
450    pub fn init() -> GroupCrdtState<ID, OP, M, C> {
451        GroupCrdtState {
452            inner: GroupCrdtInnerState::default(),
453        }
454    }
455
456    /// Process an operation created locally or received from a remote peer.
457    #[allow(clippy::type_complexity)]
458    pub fn process(
459        mut y: GroupCrdtState<ID, OP, M, C>,
460        operation: &M,
461    ) -> Result<GroupCrdtState<ID, OP, M, C>, GroupCrdtError<ID, OP, M, C, RS>> {
462        let operation_id = operation.id();
463        let actor = operation.author();
464        let dependencies = HashSet::from_iter(operation.dependencies().clone());
465        let group_id = operation.group_id();
466        let rebuild_required =
467            RS::rebuild_required(&y.inner, operation).map_err(GroupCrdtError::Resolver)?;
468
469        // Validate that the author of this operation had the required access rights at the point
470        // in the auth graph which they claim as their last state (the state at "dependencies").
471        // It could be that they had access at this point but concurrent changes (which we know
472        // about) mean that they have lost that access level. This case is dealt with later, here
473        // we want to catch malicious or invalid operations which should _never_ be attached to
474        // the graph.
475        y = GroupCrdt::validate(y, operation)?;
476        y = Self::add_operation(y, operation);
477
478        if rebuild_required {
479            y.inner = RS::process(y.inner).map_err(GroupCrdtError::Resolver)?;
480            return Ok(y);
481        }
482
483        // We don't need to check the state change result as validation was already performed
484        // above.
485        let mut groups_y = y.inner.state_at(&dependencies)?;
486        groups_y = apply_action(
487            groups_y,
488            group_id,
489            operation_id,
490            actor,
491            &operation.action(),
492            &y.inner.ignore,
493        )
494        .state()
495        .to_owned();
496
497        y.inner.states.insert(operation_id, groups_y);
498
499        Ok(y)
500    }
501
502    /// Validate an action by applying it to the group state build to it's previous pointers.
503    ///
504    /// When processing a new operation we need to validate that the contained action is valid
505    /// before including it in the graph. By valid we mean that the author who composed the action
506    /// had authority to perform the claimed action, and that the action fulfils all group change
507    /// requirements. To check this we need to re-build the group state to the operations claimed
508    /// previous state. This process involves pruning any operations which are not predecessors of
509    /// the new operation resolving the group state again.
510    ///
511    /// This is a relatively expensive computation and should only be used when a re-build is
512    /// actually required.
513    #[allow(clippy::type_complexity)]
514    pub(crate) fn validate(
515        y: GroupCrdtState<ID, OP, M, C>,
516        operation: &M,
517    ) -> Result<GroupCrdtState<ID, OP, M, C>, GroupCrdtError<ID, OP, M, C, RS>> {
518        // Detect already processed operations.
519        if y.inner.operations.contains_key(&operation.id()) {
520            // The operation has already been processed.
521            return Err(GroupCrdtError::DuplicateOperation(
522                operation.id(),
523                operation.group_id(),
524            ));
525        }
526
527        // Adding a group as a manager of another group is currently not
528        // supported.
529        //
530        // @TODO: To support this behavior updates in the StrongRemove resolver
531        // so that cross-group concurrent remove cycles are detected. Related to
532        // issue: https://github.com/p2panda/p2panda/issues/779
533        match &operation.action() {
534            GroupAction::Add { member, access } | GroupAction::Promote { member, access }
535                if member.is_group() && access.is_manage() =>
536            {
537                return Err(GroupCrdtError::ManagerGroupsNotAllowed(member.id()));
538            }
539            _ => (),
540        };
541
542        let last_graph = y.inner.graph.clone();
543        let last_ignore = y.inner.ignore.clone();
544        let last_mutual_removes = y.inner.mutual_removes.clone();
545        let last_states = y.inner.states.clone();
546
547        let dependencies = HashSet::from_iter(operation.dependencies().clone());
548
549        // If this operation is concurrent to our current local state we need to rebuild the graph
550        // to the operations' claimed dependencies in order to validate it correctly.
551        let temp_y = if y.inner.heads() != dependencies {
552            let mut temp_y = y;
553
554            // Collect predecessors of the new operation.
555            let mut predecessors = HashSet::new();
556            for dependency in operation.dependencies() {
557                let reversed = Reversed(&temp_y.inner.graph);
558                let mut dfs_rev = DfsPostOrder::new(&reversed, dependency);
559                while let Some(id) = dfs_rev.next(&reversed) {
560                    predecessors.insert(id);
561                }
562            }
563
564            // Remove all other nodes from the graph.
565            let to_remove: Vec<_> = temp_y
566                .inner
567                .graph
568                .node_identifiers()
569                .filter(|n| !predecessors.contains(n))
570                .collect();
571
572            for node in &to_remove {
573                temp_y.inner.graph.remove_node(*node);
574            }
575
576            temp_y.inner = RS::process(temp_y.inner).map_err(GroupCrdtError::Resolver)?;
577            temp_y
578        } else {
579            y
580        };
581
582        // Detect if this operation would cause a nested group cycle.
583        if temp_y.inner.would_create_cycle(operation) {
584            let parent_group = operation.group_id();
585
586            // Only adds cause a cycle, we just access the member id here.
587            let GroupAction::Add {
588                member: sub_group, ..
589            } = operation.action()
590            else {
591                unreachable!()
592            };
593
594            return Err(GroupCrdtError::GroupCycle(
595                parent_group,
596                sub_group.id(),
597                operation.id(),
598            ));
599        }
600
601        // Apply the operation onto the temporary state.
602        let result = apply_action(
603            temp_y.inner.current_state(),
604            operation.group_id(),
605            operation.id(),
606            operation.author(),
607            &operation.action(),
608            &temp_y.inner.ignore,
609        );
610
611        match result {
612            StateChangeResult::Ok { state } => state,
613            StateChangeResult::Error { error, .. } => {
614                // Noop shouldn't happen when processing new operations as the
615                // rebuild logic should have occurred instead.
616                return Err(GroupCrdtError::StateChangeError(operation.id(), error));
617            }
618            StateChangeResult::Filtered { .. } => {
619                // Operations can't be filtered out before they were processed.
620                unreachable!();
621            }
622        };
623
624        let mut y = temp_y;
625        y.inner.graph = last_graph;
626        y.inner.ignore = last_ignore;
627        y.inner.mutual_removes = last_mutual_removes;
628        y.inner.states = last_states;
629
630        Ok(y)
631    }
632
633    /// Add an operation to the auth graph and operation map.
634    ///
635    /// NOTE: this method _does not_ process the operation so no new state is derived.
636    fn add_operation(
637        mut y: GroupCrdtState<ID, OP, M, C>,
638        operation: &M,
639    ) -> GroupCrdtState<ID, OP, M, C> {
640        let operation_id = operation.id();
641        let dependencies = operation.dependencies();
642
643        // Add operation to the global auth graph.
644        y.inner.graph.add_node(operation_id);
645        for dependency in &dependencies {
646            y.inner.graph.add_edge(*dependency, operation_id, ());
647        }
648
649        // Insert operation into all operations map.
650        y.inner.operations.insert(operation_id, operation.clone());
651
652        y
653    }
654}
655
656/// Apply an action to a single group state.
657pub(crate) fn apply_action<ID, OP, C>(
658    mut groups_y: GroupStates<ID, C>,
659    group_id: ID,
660    id: OP,
661    actor: ID,
662    action: &GroupAction<ID, C>,
663    filter: &HashSet<OP>,
664) -> StateChangeResult<ID, C>
665where
666    ID: IdentityHandle,
667    OP: OperationId + Ord,
668    C: Conditions,
669{
670    let members_y = if action.is_create() {
671        GroupMembersState::default()
672    } else {
673        groups_y
674            .remove(&group_id)
675            .expect("group already present in states map")
676    };
677
678    if filter.contains(&id) {
679        groups_y.insert(group_id, members_y);
680        return StateChangeResult::Filtered { state: groups_y };
681    }
682
683    let result = match action.clone() {
684        GroupAction::Add { member, access, .. } => state::add(
685            members_y.clone(),
686            GroupMember::Individual(actor),
687            member,
688            access,
689        ),
690        GroupAction::Remove { member, .. } => {
691            state::remove(members_y.clone(), GroupMember::Individual(actor), member)
692        }
693        GroupAction::Promote { member, access } => state::promote(
694            members_y.clone(),
695            GroupMember::Individual(actor),
696            member,
697            access,
698        ),
699        GroupAction::Demote { member, access } => state::demote(
700            members_y.clone(),
701            GroupMember::Individual(actor),
702            member,
703            access,
704        ),
705        GroupAction::Create { initial_members } => Ok(state::create(&initial_members)),
706    };
707
708    match result {
709        Ok(members_y_i) => {
710            groups_y.insert(group_id, members_y_i);
711            StateChangeResult::Ok { state: groups_y }
712        }
713        Err(err) => {
714            // Errors occur here because the member attempting to perform an action
715            // doesn't have a suitable access level, or that the action itself is invalid
716            // (eg. promoting a non-existent member).
717            //
718            // 1) We expect some errors to occur when when intentionally filtered out
719            //    actions cause later operations to become invalid.
720            //
721            // 2) Operations which other peers accepted into their graph _before_
722            //    receiving some concurrent operation which caused them to be invalid.
723            //
724            // In both cases it's critical that the action does not cause any state
725            // change, however we do want to accept them into our graph so as to ensure
726            // consistency consistency across peers.
727            groups_y.insert(group_id, members_y);
728            StateChangeResult::Error {
729                state: groups_y,
730                error: err,
731            }
732        }
733    }
734}
735
736/// Apply a remove operation without validating it against state change rules. This is required
737/// when retaining mutual-remove operations which may have lost their delegated access rights.
738pub(crate) fn apply_remove_unsafe<ID, C>(
739    mut groups_y: GroupStates<ID, C>,
740    group_id: ID,
741    removed: GroupMember<ID>,
742) -> GroupStates<ID, C>
743where
744    ID: IdentityHandle,
745    C: Conditions,
746{
747    let mut members_y = groups_y
748        .remove(&group_id)
749        .expect("group already present in states map");
750
751    members_y.members.entry(removed).and_modify(|state| {
752        if state.member_counter % 2 != 0 {
753            state.member_counter += 1
754        }
755    });
756    groups_y.insert(group_id, members_y);
757    groups_y
758}
759
760/// Return types expected from applying an action to group state.
761pub enum StateChangeResult<ID, C>
762where
763    ID: IdentityHandle,
764    C: Conditions,
765{
766    /// Action was applied and no error occurred.
767    Ok { state: GroupStates<ID, C> },
768
769    /// Action was not applied because it failed internal validation.
770    Error {
771        state: GroupStates<ID, C>,
772        #[allow(unused)]
773        error: GroupMembershipError<GroupMember<ID>>,
774    },
775
776    /// Action was not applied because it has been filtered out.
777    Filtered { state: GroupStates<ID, C> },
778}
779
780impl<ID, C> StateChangeResult<ID, C>
781where
782    ID: IdentityHandle,
783    C: Conditions,
784{
785    pub fn state(&self) -> &GroupStates<ID, C> {
786        match self {
787            StateChangeResult::Ok { state }
788            | StateChangeResult::Error { state, .. }
789            | StateChangeResult::Filtered { state } => state,
790        }
791    }
792}
793
794#[cfg(test)]
795pub(crate) mod tests {
796    use crate::Access;
797    use crate::group::{GroupCrdtError, GroupMember, GroupMembershipError};
798    use crate::test_utils::{
799        TestGroup, TestGroupState, add_member, create_group, demote_member, promote_member,
800        remove_member,
801    };
802    use crate::traits::Operation;
803
804    const G1: char = '1';
805    const G2: char = '2';
806    const G3: char = '3';
807    const G4: char = '4';
808
809    const ALICE: char = 'A';
810    const BOB: char = 'B';
811    const CLAIRE: char = 'C';
812    const DAN: char = 'D';
813    const EVE: char = 'E';
814
815    #[test]
816    fn group_operations() {
817        let y = TestGroupState::new();
818
819        let op1 = create_group(
820            ALICE,
821            0,
822            G1,
823            vec![(GroupMember::Individual(ALICE), Access::manage())],
824            vec![],
825        );
826
827        let y_i = TestGroup::process(y, &op1).unwrap();
828        let mut members = y_i.members(G1);
829        members.sort();
830        assert_eq!(members, vec![(ALICE, Access::manage())]);
831
832        let op2 = add_member(
833            ALICE,
834            1,
835            G1,
836            GroupMember::Individual(BOB),
837            Access::read(),
838            vec![op1.id()],
839        );
840
841        let y_ii = TestGroup::process(y_i, &op2).unwrap();
842        let mut members = y_ii.members(G1);
843        members.sort();
844        assert_eq!(
845            members,
846            vec![(ALICE, Access::manage()), (BOB, Access::read())]
847        );
848
849        let op3 = add_member(
850            ALICE,
851            2,
852            G1,
853            GroupMember::Individual(CLAIRE),
854            Access::write(),
855            vec![op2.id()],
856        );
857
858        let y_iii = TestGroup::process(y_ii, &op3).unwrap();
859        let mut members = y_iii.members(G1);
860        members.sort();
861        assert_eq!(
862            members,
863            vec![
864                (ALICE, Access::manage()),
865                (BOB, Access::read()),
866                (CLAIRE, Access::write())
867            ]
868        );
869
870        let op4 = remove_member(ALICE, 3, G1, GroupMember::Individual(BOB), vec![op3.id()]);
871
872        let y_iv = TestGroup::process(y_iii, &op4).unwrap();
873        let mut members = y_iv.members(G1);
874        members.sort();
875        assert_eq!(
876            members,
877            vec![(ALICE, Access::manage()), (CLAIRE, Access::write())]
878        );
879    }
880
881    #[test]
882    fn concurrent_removal() {
883        let y = TestGroupState::new();
884
885        let op1 = create_group(
886            ALICE,
887            0,
888            G1,
889            vec![(GroupMember::Individual(ALICE), Access::manage())],
890            vec![],
891        );
892
893        let y_i = TestGroup::process(y, &op1).unwrap();
894        let mut members = y_i.members(G1);
895        members.sort();
896        assert_eq!(members, vec![(ALICE, Access::manage())]);
897
898        let op2 = add_member(
899            ALICE,
900            1,
901            G1,
902            GroupMember::Individual(BOB),
903            Access::manage(),
904            vec![op1.id()],
905        );
906
907        let y_ii = TestGroup::process(y_i, &op2).unwrap();
908        let mut members = y_ii.members(G1);
909        members.sort();
910        assert_eq!(
911            members,
912            vec![(ALICE, Access::manage()), (BOB, Access::manage())]
913        );
914
915        let op3 = add_member(
916            BOB,
917            2,
918            G1,
919            GroupMember::Individual(CLAIRE),
920            Access::write(),
921            vec![op2.id()],
922        );
923
924        let y_iii = TestGroup::process(y_ii, &op3).unwrap();
925        let mut members = y_iii.members(G1);
926        members.sort();
927        assert_eq!(
928            members,
929            vec![
930                (ALICE, Access::manage()),
931                (BOB, Access::manage()),
932                (CLAIRE, Access::write())
933            ]
934        );
935
936        let op4 = remove_member(ALICE, 3, G1, GroupMember::Individual(BOB), vec![op2.id()]);
937
938        let y_iv = TestGroup::process(y_iii, &op4).unwrap();
939        let mut members = y_iv.members(G1);
940        members.sort();
941        assert_eq!(members, vec![(ALICE, Access::manage())]);
942    }
943
944    #[test]
945    fn mutual_concurrent_removal() {
946        let y = TestGroupState::new();
947
948        let op1 = create_group(
949            ALICE,
950            0,
951            G1,
952            vec![(GroupMember::Individual(ALICE), Access::manage())],
953            vec![],
954        );
955
956        let y_i = TestGroup::process(y, &op1).unwrap();
957        let mut members = y_i.members(G1);
958        members.sort();
959        assert_eq!(members, vec![(ALICE, Access::manage())]);
960
961        let op2 = add_member(
962            ALICE,
963            1,
964            G1,
965            GroupMember::Individual(BOB),
966            Access::manage(),
967            vec![op1.id()],
968        );
969
970        let y_ii = TestGroup::process(y_i, &op2).unwrap();
971        let mut members = y_ii.members(G1);
972        members.sort();
973        assert_eq!(
974            members,
975            vec![(ALICE, Access::manage()), (BOB, Access::manage())]
976        );
977
978        let op3 = add_member(
979            BOB,
980            2,
981            G1,
982            GroupMember::Individual(CLAIRE),
983            Access::manage(),
984            vec![op2.id()],
985        );
986
987        let y_iii = TestGroup::process(y_ii, &op3).unwrap();
988        let mut members = y_iii.members(G1);
989        members.sort();
990        assert_eq!(
991            members,
992            vec![
993                (ALICE, Access::manage()),
994                (BOB, Access::manage()),
995                (CLAIRE, Access::manage())
996            ]
997        );
998
999        let op4 = remove_member(BOB, 3, G1, GroupMember::Individual(CLAIRE), vec![op3.id()]);
1000
1001        let y_iv = TestGroup::process(y_iii, &op4).unwrap();
1002        let mut members = y_iv.members(G1);
1003        members.sort();
1004        assert_eq!(
1005            members,
1006            vec![(ALICE, Access::manage()), (BOB, Access::manage())]
1007        );
1008
1009        let op5 = remove_member(CLAIRE, 4, G1, GroupMember::Individual(BOB), vec![op3.id()]);
1010
1011        let y_v = TestGroup::process(y_iv, &op5).unwrap();
1012        let mut members = y_v.members(G1);
1013        members.sort();
1014        assert_eq!(members, vec![(ALICE, Access::manage())]);
1015    }
1016
1017    #[test]
1018    fn nested_groups() {
1019        let y = TestGroupState::new();
1020
1021        let op1 = create_group(
1022            ALICE,
1023            0,
1024            G1,
1025            vec![(GroupMember::Individual(ALICE), Access::manage())],
1026            vec![],
1027        );
1028
1029        let y_i = TestGroup::process(y, &op1).unwrap();
1030        let mut members = y_i.members(G1);
1031        members.sort();
1032        assert_eq!(members, vec![(ALICE, Access::manage())]);
1033
1034        let op2 = create_group(
1035            BOB,
1036            1,
1037            G2,
1038            vec![(GroupMember::Individual(BOB), Access::manage())],
1039            vec![op1.id()],
1040        );
1041
1042        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1043        let mut members = y_ii.members(G2);
1044        members.sort();
1045        assert_eq!(members, vec![(BOB, Access::manage())]);
1046
1047        let op3 = add_member(
1048            ALICE,
1049            2,
1050            G1,
1051            GroupMember::Group(G2),
1052            Access::read(),
1053            vec![op2.id()],
1054        );
1055
1056        let y_iii = TestGroup::process(y_ii, &op3).unwrap();
1057        let mut members = y_iii.members(G1);
1058        members.sort();
1059        assert_eq!(
1060            members,
1061            vec![(ALICE, Access::manage()), (BOB, Access::read())]
1062        );
1063    }
1064
1065    #[test]
1066    fn error_on_unauthorized_add() {
1067        let y = TestGroupState::new();
1068
1069        let op1 = create_group(
1070            ALICE,
1071            0,
1072            G1,
1073            vec![(GroupMember::Individual(ALICE), Access::manage())],
1074            vec![],
1075        );
1076
1077        let y_i = TestGroup::process(y, &op1).unwrap();
1078
1079        let op2 = add_member(
1080            ALICE,
1081            1,
1082            G1,
1083            GroupMember::Individual(BOB),
1084            Access::read(),
1085            vec![op1.id()],
1086        );
1087
1088        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1089
1090        let op3 = add_member(
1091            BOB,
1092            2,
1093            G1,
1094            GroupMember::Individual(CLAIRE),
1095            Access::read(),
1096            vec![op2.id()],
1097        );
1098
1099        assert!(TestGroup::process(y_ii, &op3).is_err());
1100    }
1101
1102    #[test]
1103    fn error_on_remove_non_member() {
1104        let y = TestGroupState::new();
1105
1106        let op1 = create_group(
1107            ALICE,
1108            0,
1109            G1,
1110            vec![(GroupMember::Individual(ALICE), Access::manage())],
1111            vec![],
1112        );
1113
1114        let y_i = TestGroup::process(y, &op1).unwrap();
1115
1116        let op2 = remove_member(ALICE, 1, G1, GroupMember::Individual(BOB), vec![op1.id()]);
1117
1118        assert!(TestGroup::process(y_i, &op2).is_err());
1119    }
1120
1121    #[test]
1122    fn error_on_promote_non_member() {
1123        let y = TestGroupState::new();
1124
1125        let op1 = create_group(
1126            ALICE,
1127            0,
1128            G1,
1129            vec![(GroupMember::Individual(ALICE), Access::manage())],
1130            vec![],
1131        );
1132
1133        let y_i = TestGroup::process(y, &op1).unwrap();
1134
1135        let op2 = promote_member(
1136            ALICE,
1137            1,
1138            G1,
1139            GroupMember::Individual(BOB),
1140            Access::manage(),
1141            vec![op1.id()],
1142        );
1143
1144        assert!(TestGroup::process(y_i, &op2).is_err());
1145    }
1146
1147    #[test]
1148    fn error_on_add_manager_group() {
1149        let y = TestGroupState::new();
1150
1151        let op1 = create_group(
1152            ALICE,
1153            0,
1154            G1,
1155            vec![(GroupMember::Individual(ALICE), Access::manage())],
1156            vec![],
1157        );
1158
1159        let y_i = TestGroup::process(y, &op1).unwrap();
1160
1161        let op2 = add_member(
1162            ALICE,
1163            1,
1164            G1,
1165            GroupMember::Group(BOB),
1166            Access::manage(),
1167            vec![op1.id()],
1168        );
1169
1170        assert!(TestGroup::process(y_i, &op2).is_err());
1171    }
1172
1173    #[test]
1174    fn error_on_demote_non_member() {
1175        let y = TestGroupState::new();
1176
1177        let op1 = create_group(
1178            ALICE,
1179            0,
1180            G1,
1181            vec![(GroupMember::Individual(ALICE), Access::manage())],
1182            vec![],
1183        );
1184
1185        let y_i = TestGroup::process(y, &op1).unwrap();
1186
1187        let op2 = demote_member(
1188            ALICE,
1189            1,
1190            G1,
1191            GroupMember::Individual(BOB),
1192            Access::read(),
1193            vec![op1.id()],
1194        );
1195
1196        assert!(TestGroup::process(y_i, &op2).is_err());
1197    }
1198
1199    #[test]
1200    fn error_on_add_existing_member() {
1201        let y = TestGroupState::new();
1202
1203        let op1 = create_group(
1204            ALICE,
1205            0,
1206            G1,
1207            vec![(GroupMember::Individual(ALICE), Access::manage())],
1208            vec![],
1209        );
1210
1211        let y_i = TestGroup::process(y, &op1).unwrap();
1212
1213        let op2 = add_member(
1214            ALICE,
1215            1,
1216            G1,
1217            GroupMember::Individual(ALICE),
1218            Access::manage(),
1219            vec![op1.id()],
1220        );
1221
1222        assert!(TestGroup::process(y_i, &op2).is_err());
1223    }
1224
1225    #[test]
1226    fn error_on_remove_nonexistent_subgroup() {
1227        let y = TestGroupState::new();
1228
1229        let op1 = create_group(
1230            ALICE,
1231            0,
1232            G1,
1233            vec![(GroupMember::Individual(ALICE), Access::manage())],
1234            vec![],
1235        );
1236        let y_i = TestGroup::process(y, &op1).unwrap();
1237
1238        // Attempt to remove a subgroup that was never added
1239        let op2 = remove_member(ALICE, 1, G1, GroupMember::Group(G2), vec![op1.id()]);
1240
1241        assert!(TestGroup::process(y_i, &op2).is_err());
1242    }
1243
1244    #[test]
1245    fn deeply_nested_groups_with_removals() {
1246        let y = TestGroupState::new();
1247
1248        // Create G1
1249        let op1 = create_group(
1250            ALICE,
1251            0,
1252            G1,
1253            vec![(GroupMember::Individual(ALICE), Access::manage())],
1254            vec![],
1255        );
1256        let y_i = TestGroup::process(y, &op1).unwrap();
1257
1258        // Create G2
1259        let op2 = create_group(
1260            BOB,
1261            1,
1262            G2,
1263            vec![(GroupMember::Individual(BOB), Access::manage())],
1264            vec![op1.id()],
1265        );
1266        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1267
1268        // Create G3
1269        let op3 = create_group(
1270            CLAIRE,
1271            2,
1272            G3,
1273            vec![(GroupMember::Individual(CLAIRE), Access::manage())],
1274            vec![op2.id()],
1275        );
1276        let y_iii = TestGroup::process(y_ii, &op3).unwrap();
1277
1278        // Create G4
1279        let op4 = create_group(
1280            DAN,
1281            3,
1282            G4,
1283            vec![(GroupMember::Individual(DAN), Access::write())],
1284            vec![op3.id()],
1285        );
1286        let y_iv = TestGroup::process(y_iii, &op4).unwrap();
1287
1288        // Nest G4 into G3
1289        let op5 = add_member(
1290            CLAIRE,
1291            4,
1292            G3,
1293            GroupMember::Group(G4),
1294            Access::read(),
1295            vec![op4.id()],
1296        );
1297        let y_v = TestGroup::process(y_iv, &op5).unwrap();
1298
1299        // Nest G3 into G2
1300        let op6 = add_member(
1301            BOB,
1302            5,
1303            G2,
1304            GroupMember::Group(G3),
1305            Access::write(),
1306            vec![op5.id()],
1307        );
1308        let y_vi = TestGroup::process(y_v, &op6).unwrap();
1309
1310        // Nest G2 into G1
1311        let op7 = add_member(
1312            ALICE,
1313            6,
1314            G1,
1315            GroupMember::Group(G2),
1316            Access::read(),
1317            vec![op6.id()],
1318        );
1319        let y_vii = TestGroup::process(y_vi, &op7).unwrap();
1320
1321        let mut members = y_vii.members(G1);
1322        members.sort();
1323        assert_eq!(
1324            members,
1325            vec![
1326                (ALICE, Access::manage()),
1327                (BOB, Access::read()),
1328                (CLAIRE, Access::read()),
1329                (DAN, Access::read()),
1330            ]
1331        );
1332
1333        // Remove G3 from G2
1334        let op8 = remove_member(BOB, 7, G2, GroupMember::Group(G3), vec![op7.id()]);
1335        let y_viii = TestGroup::process(y_vii, &op8).unwrap();
1336
1337        let mut members_after_removal = y_viii.members(G1);
1338        members_after_removal.sort();
1339        assert_eq!(
1340            members_after_removal,
1341            vec![(ALICE, Access::manage()), (BOB, Access::read()),]
1342        );
1343    }
1344
1345    #[test]
1346    fn nested_groups_with_concurrent_removal_and_promotion() {
1347        let y = TestGroupState::new();
1348
1349        // Create G1
1350        let op1 = create_group(
1351            ALICE,
1352            0,
1353            G1,
1354            vec![(GroupMember::Individual(ALICE), Access::manage())],
1355            vec![],
1356        );
1357        let y_i = TestGroup::process(y, &op1).unwrap();
1358
1359        // Create G2
1360        let op2 = create_group(
1361            BOB,
1362            1,
1363            G2,
1364            vec![(GroupMember::Individual(BOB), Access::manage())],
1365            vec![op1.id()],
1366        );
1367        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1368
1369        // Create G3
1370        let op3 = create_group(
1371            CLAIRE,
1372            2,
1373            G3,
1374            vec![(GroupMember::Individual(CLAIRE), Access::manage())],
1375            vec![op2.id()],
1376        );
1377        let y_iii = TestGroup::process(y_ii, &op3).unwrap();
1378
1379        // G3 includes Dan
1380        let op4 = add_member(
1381            CLAIRE,
1382            3,
1383            G3,
1384            GroupMember::Individual(DAN),
1385            Access::write(),
1386            vec![op3.id()],
1387        );
1388        let y_iv = TestGroup::process(y_iii, &op4).unwrap();
1389
1390        // G2 includes G3
1391        let op5 = add_member(
1392            BOB,
1393            4,
1394            G2,
1395            GroupMember::Group(G3),
1396            Access::write(),
1397            vec![op4.id()],
1398        );
1399        let y_v = TestGroup::process(y_iv, &op5).unwrap();
1400
1401        // G2 includes Claire
1402        let op6 = add_member(
1403            BOB,
1404            5,
1405            G2,
1406            GroupMember::Individual(CLAIRE),
1407            Access::read(),
1408            vec![op5.id()],
1409        );
1410        let y_vi = TestGroup::process(y_v, &op6).unwrap();
1411
1412        // G1 includes G2
1413        let op7 = add_member(
1414            ALICE,
1415            6,
1416            G1,
1417            GroupMember::Group(G2),
1418            Access::read(),
1419            vec![op6.id()],
1420        );
1421        let y_vii = TestGroup::process(y_vi, &op7).unwrap();
1422
1423        let mut members = y_vii.members(G1);
1424        members.sort();
1425        assert_eq!(
1426            members,
1427            vec![
1428                (ALICE, Access::manage()),
1429                (BOB, Access::read()),
1430                (CLAIRE, Access::read()),
1431                (DAN, Access::read()),
1432            ]
1433        );
1434
1435        // Concurrent ops from same parent state
1436        let op8_remove_g2 = remove_member(ALICE, 7, G1, GroupMember::Group(G2), vec![op7.id()]);
1437        let op9_promote_claire = promote_member(
1438            BOB,
1439            8,
1440            G2,
1441            GroupMember::Individual(CLAIRE),
1442            Access::manage(),
1443            vec![op7.id()],
1444        );
1445
1446        // Remove first
1447        let y_after_remove = TestGroup::process(y_vii.clone(), &op8_remove_g2).unwrap();
1448        let mut members = y_after_remove.members(G1);
1449        members.sort();
1450        assert_eq!(members, vec![(ALICE, Access::manage())]);
1451
1452        // Then promote
1453        let y_after_both = TestGroup::process(y_after_remove, &op9_promote_claire).unwrap();
1454        let mut g1_members = y_after_both.members(G1);
1455        g1_members.sort();
1456        assert_eq!(g1_members, vec![(ALICE, Access::manage())]);
1457
1458        let mut g2_members = y_after_both.members(G2);
1459        g2_members.sort();
1460        assert_eq!(
1461            g2_members,
1462            vec![
1463                (BOB, Access::manage()),
1464                (CLAIRE, Access::manage()),
1465                (DAN, Access::write()),
1466            ]
1467        );
1468    }
1469
1470    #[test]
1471    fn concurrent_removal_ooo_processing() {
1472        let y = TestGroupState::new();
1473
1474        // Alice creates group
1475        let op1 = create_group(
1476            ALICE,
1477            0,
1478            G1,
1479            vec![(GroupMember::Individual(ALICE), Access::manage())],
1480            vec![],
1481        );
1482        let y_i = TestGroup::process(y, &op1).unwrap();
1483
1484        // Alice adds Bob as manager
1485        let op2 = add_member(
1486            ALICE,
1487            1,
1488            G1,
1489            GroupMember::Individual(BOB),
1490            Access::manage(),
1491            vec![op1.id()],
1492        );
1493        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1494
1495        // Bob adds Claire (Read)
1496        let op3 = add_member(
1497            BOB,
1498            2,
1499            G1,
1500            GroupMember::Individual(CLAIRE),
1501            Access::read(),
1502            vec![op2.id()],
1503        );
1504
1505        // Alice removes Bob
1506        let op4 = remove_member(ALICE, 3, G1, GroupMember::Individual(BOB), vec![op2.id()]);
1507
1508        // Apply in Order A: Add Claire, then Remove Bob
1509        let y_iii_a = TestGroup::process(y_ii.clone(), &op3).unwrap();
1510        let y_iv_a = TestGroup::process(y_iii_a, &op4).unwrap();
1511
1512        // Apply in Order B: Remove Bob, then Add Claire
1513        let y_iii_b = TestGroup::process(y_ii.clone(), &op4).unwrap();
1514        let y_iv_b = TestGroup::process(y_iii_b, &op3).unwrap();
1515
1516        for (_, y) in [y_iv_a, y_iv_b].into_iter().enumerate() {
1517            let mut members = y.members(G1);
1518            members.sort();
1519            assert_eq!(members, vec![(ALICE, Access::manage())],);
1520        }
1521    }
1522
1523    #[test]
1524    fn concurrent_add_with_insufficient_access() {
1525        let y0 = TestGroupState::new();
1526
1527        // Alice creates the group
1528        let op1 = create_group(
1529            ALICE,
1530            0,
1531            G1,
1532            vec![(GroupMember::Individual(ALICE), Access::manage())],
1533            vec![],
1534        );
1535        let y1 = TestGroup::process(y0, &op1).unwrap();
1536
1537        // Alice adds Bob as manager
1538        let op2 = add_member(
1539            ALICE,
1540            1,
1541            G1,
1542            GroupMember::Individual(BOB),
1543            Access::manage(),
1544            vec![op1.id()],
1545        );
1546
1547        // Bob concurrently tries to add Eve
1548        let op3 = add_member(
1549            BOB,
1550            2,
1551            G1,
1552            GroupMember::Individual(EVE),
1553            Access::read(),
1554            vec![op1.id()],
1555        );
1556
1557        // Case 1: Apply Bob's operation first - should fail
1558        let result = TestGroup::process(y1.clone(), &op3);
1559        assert!(matches!(
1560            result,
1561            Err(GroupCrdtError::StateChangeError(
1562                _,
1563                GroupMembershipError::UnrecognisedActor(_)
1564            ))
1565        ));
1566
1567        // Case 2: Apply Alice’s op first, then Bob's - still must fail
1568        let y1_alt = TestGroup::process(y1, &op2).unwrap();
1569        let result = TestGroup::process(y1_alt.clone(), &op3);
1570        assert!(matches!(
1571            result,
1572            Err(GroupCrdtError::StateChangeError(
1573                _,
1574                GroupMembershipError::UnrecognisedActor(_)
1575            ))
1576        ));
1577
1578        // Confirm final state: Bob is a member, Eve is not
1579        let mut members = y1_alt.members(G1);
1580        members.sort();
1581        assert_eq!(
1582            members,
1583            vec![(ALICE, Access::manage()), (BOB, Access::manage())]
1584        );
1585    }
1586
1587    #[test]
1588    fn add_group_with_concurrent_change() {
1589        let y = TestGroupState::new();
1590
1591        // Create Group 1 with Alice as manager
1592        let op1 = create_group(
1593            ALICE,
1594            0,
1595            G1,
1596            vec![(GroupMember::Individual(ALICE), Access::manage())],
1597            vec![],
1598        );
1599        let y_i = TestGroup::process(y, &op1).unwrap();
1600
1601        // Create Group 2 with Bob as manager
1602        let op2 = create_group(
1603            BOB,
1604            1,
1605            G2,
1606            vec![(GroupMember::Individual(BOB), Access::manage())],
1607            vec![op1.id()],
1608        );
1609        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1610
1611        // Alice adds Group 2 to Group 1
1612        let op3a = add_member(
1613            ALICE,
1614            2,
1615            G1,
1616            GroupMember::Group(G2),
1617            Access::read(),
1618            vec![op2.id()],
1619        );
1620
1621        // Concurrently, Bob adds Claire to Group 2
1622        let op3b = add_member(
1623            BOB,
1624            3,
1625            G2,
1626            GroupMember::Individual(CLAIRE),
1627            Access::write(),
1628            vec![op2.id()],
1629        );
1630
1631        // Order 1: Add group, then add member
1632        let y_iii = TestGroup::process(y_ii.clone(), &op3a).unwrap();
1633        let y_iv = TestGroup::process(y_iii, &op3b).unwrap();
1634
1635        let mut members_1 = y_iv.members(G1);
1636        members_1.sort();
1637        assert_eq!(
1638            members_1,
1639            vec![
1640                (ALICE, Access::manage()),
1641                (BOB, Access::read()),
1642                (CLAIRE, Access::read())
1643            ]
1644        );
1645
1646        // Order 2: Add member, then add group
1647        let y_iii_alt = TestGroup::process(y_ii.clone(), &op3b).unwrap();
1648        let y_iv_alt = TestGroup::process(y_iii_alt, &op3a).unwrap();
1649
1650        let mut members_1 = y_iv_alt.members(G1);
1651        members_1.sort();
1652        assert_eq!(
1653            members_1,
1654            vec![
1655                (ALICE, Access::manage()),
1656                (BOB, Access::read()),
1657                (CLAIRE, Access::read())
1658            ]
1659        );
1660    }
1661
1662    #[test]
1663    fn nested_group_cycle_error() {
1664        let y = TestGroupState::new();
1665
1666        // Create group G1 with ALICE as manager
1667        let op1 = create_group(
1668            ALICE,
1669            0,
1670            G1,
1671            vec![(GroupMember::Individual(ALICE), Access::manage())],
1672            vec![],
1673        );
1674        let y_i = TestGroup::process(y, &op1).unwrap();
1675
1676        // Create group G2 with BOB as manager, with G1 as a member
1677        let op2 = create_group(
1678            BOB,
1679            1,
1680            G2,
1681            vec![
1682                (GroupMember::Individual(BOB), Access::manage()),
1683                (GroupMember::Group(G1), Access::read()),
1684            ],
1685            vec![op1.id()],
1686        );
1687        let y_ii = TestGroup::process(y_i, &op2).unwrap();
1688
1689        // Attempt to add G2 as a member of G1, which creates a cycle (G1 -> G2 -> G1)
1690        let op3 = add_member(
1691            ALICE,
1692            2,
1693            G1,
1694            GroupMember::Group(G2),
1695            Access::read(),
1696            vec![op2.id()],
1697        );
1698
1699        // This should fail due to cycle detection
1700        let result = TestGroup::process(y_ii, &op3);
1701        assert!(
1702            result.is_err(),
1703            "Creating a group cycle should cause an error"
1704        );
1705    }
1706
1707    #[test]
1708    fn serde_to_from_bytes() {
1709        let y = TestGroupState::new();
1710        let op1 = create_group(
1711            ALICE,
1712            0,
1713            G1,
1714            vec![(GroupMember::Individual(ALICE), Access::manage())],
1715            vec![],
1716        );
1717        let y_i = TestGroup::process(y, &op1).unwrap();
1718        let members = y_i.members(G1);
1719        assert_eq!(members, vec![(ALICE, Access::manage())]);
1720
1721        // Serialize auth state to cbor bytes.
1722        let mut bytes = vec![];
1723        ciborium::ser::into_writer(&y_i, &mut bytes).unwrap();
1724
1725        // Deserialize auth state from cbor bytes.
1726        let y_i_de: TestGroupState = ciborium::from_reader(&bytes[..]).unwrap();
1727
1728        // Assert members are the same.
1729        let members = y_i_de.members(G1);
1730        assert_eq!(members, vec![(ALICE, Access::manage())]);
1731    }
1732}