p2panda_auth/group/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Group membership and authorisation.
4
5mod action;
6pub(crate) mod crdt;
7#[cfg(any(test, feature = "test_utils"))]
8mod display;
9mod member;
10mod message;
11pub mod resolver;
12
13pub use action::GroupAction;
14pub use crdt::state::{GroupMembersState, GroupMembershipError, MemberState};
15pub use crdt::{GroupCrdt, GroupCrdtError, GroupCrdtState, StateChangeResult};
16pub use member::GroupMember;
17pub use message::GroupControlMessage;
18
19use std::fmt::{Debug, Display};
20use std::marker::PhantomData;
21
22use thiserror::Error;
23
24use crate::Access;
25use crate::traits::{
26    Group as GroupTrait, GroupMembership, GroupStore, IdentityHandle, Operation, OperationId,
27    Orderer, Resolver,
28};
29
30#[derive(Debug, Error)]
31/// All possible errors that can occur when creating or updating a group.
32pub enum GroupError<ID, OP, C, RS, ORD, GS>
33where
34    ID: IdentityHandle,
35    OP: OperationId + Ord,
36    RS: Resolver<ID, OP, C, ORD, GS>,
37    ORD: Orderer<ID, OP, GroupControlMessage<ID, C>>,
38    GS: GroupStore<ID, OP, C, RS, ORD>,
39{
40    #[error(transparent)]
41    Group(#[from] GroupCrdtError<ID, OP, C, RS, ORD, GS>),
42
43    #[error("group must be created with at least one initial member")]
44    EmptyGroup,
45
46    #[error("actor {0} is already a member of group {1}")]
47    GroupMember(ID, ID),
48
49    #[error("actor {0} is not a member of group {1}")]
50    NotGroupMember(ID, ID),
51
52    #[error("action requires manager access but actor {0} is {1} in group {2}")]
53    InsufficientAccess(ID, Access<C>, ID),
54
55    #[error("actor {0} already has access level {1} in group {2}")]
56    SameAccessLevel(ID, Access<C>, ID),
57}
58
59/// Decentralised Group Management (DGM).
60///
61/// The `Group` provides a high-level interface for creating and updating groups. These groups
62/// provide a means for restricting access to application data and resources. Groups are
63/// comprised of members, which may be individuals or groups, and are assigned a user-chosen
64/// identity. Each member is assigned a unique user-chosen identifier and access level. Access
65/// levels are used to enforce restrictions over access to data and the mutation of that data.
66/// They are also used to grant permissions which allow for mutating the group state by adding,
67/// removing and modifying the access level of other members.
68///
69/// Each `Group` method performs internal validation to ensure that the desired group action is
70/// valid in light of the current group state. Attempting to perform an invalid action results in a
71/// `GroupError`. For example, attempting to remove a member who is not currently part of the
72/// group.
73pub struct Group<ID, OP, C, RS, ORD, GS>
74where
75    ID: IdentityHandle,
76    OP: OperationId + Ord,
77    RS: Resolver<ID, OP, C, ORD, GS> + Debug,
78    ORD: Orderer<ID, OP, GroupControlMessage<ID, C>>,
79    GS: GroupStore<ID, OP, C, RS, ORD>,
80{
81    /// ID of the local actor.
82    my_id: ID,
83
84    /// Store for all known groups.
85    store: GS,
86
87    /// Message orderer state.
88    orderer: ORD::State,
89
90    _phantom: PhantomData<(ID, OP, C, RS, ORD, GS)>,
91}
92
93impl<ID, OP, C, RS, ORD, GS> Group<ID, OP, C, RS, ORD, GS>
94where
95    ID: IdentityHandle,
96    OP: OperationId + Ord,
97    RS: Resolver<ID, OP, C, ORD, GS> + Debug,
98    ORD: Orderer<ID, OP, GroupControlMessage<ID, C>>,
99    GS: GroupStore<ID, OP, C, RS, ORD>,
100{
101    /// Initialise the `Group` state so that groups can be created and updated.
102    ///
103    /// Requires the identifier of the local actor, as well as a group store and orderer.
104    pub fn init(my_id: ID, store: GS, orderer: ORD::State) -> Self {
105        Self {
106            _phantom: PhantomData,
107            my_id,
108            store,
109            orderer,
110        }
111    }
112}
113
114impl<ID, OP, C, RS, ORD, GS> GroupTrait<ID, OP, C, ORD> for Group<ID, OP, C, RS, ORD, GS>
115where
116    ID: IdentityHandle + Display,
117    OP: OperationId + Ord + Display,
118    C: Clone + Debug + PartialEq + PartialOrd,
119    RS: Resolver<ID, OP, C, ORD, GS> + Debug,
120    ORD: Orderer<ID, OP, GroupControlMessage<ID, C>> + Clone + Debug,
121    ORD::Operation: Clone,
122    ORD::State: Clone,
123    GS: GroupStore<ID, OP, C, RS, ORD> + Clone + Debug,
124{
125    type State = GroupCrdtState<ID, OP, C, RS, ORD, GS>;
126    type Action = GroupControlMessage<ID, C>;
127    type Error = GroupError<ID, OP, C, RS, ORD, GS>;
128
129    /// Create a group.
130    ///
131    /// The creator of the group is automatically added as a manager.
132    ///
133    /// The caller of this method must ensure that the given `group_id` is globally unique. For
134    /// example, using a collision-resistant hash.
135    fn create(
136        &self,
137        group_id: ID,
138        members: Vec<(GroupMember<ID>, Access<C>)>,
139    ) -> Result<(Self::State, ORD::Operation), Self::Error> {
140        // The creator of the group is automatically added as a manager.
141        let creator = (GroupMember::Individual(self.my_id), Access::manage());
142
143        let mut initial_members = Vec::new();
144        initial_members.push(creator);
145        initial_members.extend(members);
146
147        let y = GroupCrdtState::new(
148            self.my_id,
149            group_id,
150            self.store.clone(),
151            self.orderer.clone(),
152        );
153
154        let action = GroupControlMessage {
155            group_id: y.group_id,
156            action: GroupAction::Create { initial_members },
157        };
158
159        let (y, operation) = GroupCrdt::prepare(y, &action)?;
160        let y = GroupCrdt::process(y, &operation)?;
161
162        Ok((y, operation))
163    }
164
165    /// Create a group by processing a remote operation.
166    fn create_from_remote(
167        &self,
168        remote_operation: ORD::Operation,
169    ) -> Result<Self::State, Self::Error> {
170        let group_id = remote_operation.payload().group_id();
171
172        let y = GroupCrdtState::new(
173            self.my_id,
174            group_id,
175            self.store.clone(),
176            self.orderer.clone(),
177        );
178
179        let y = GroupCrdt::process(y, &remote_operation)?;
180
181        Ok(y)
182    }
183
184    /// Update the group by processing a remotely-authored action.
185    ///
186    /// The `group_id` of the given operation must be the same as that of the given `y`; failure to
187    /// meet this condition will result in an error.
188    fn receive_from_remote(
189        y: Self::State,
190        remote_operation: ORD::Operation,
191    ) -> Result<Self::State, Self::Error> {
192        // Validation is performed internally by `process()`.
193        let y = GroupCrdt::process(y, &remote_operation)?;
194
195        Ok(y)
196    }
197
198    /// Add a group member.
199    ///
200    /// The `adder` must be a manager and the `added` identity must not already be a member of
201    /// the group; failure to meet these conditions will result in an error.
202    fn add(
203        y: Self::State,
204        adder: ID,
205        added: ID,
206        access: Access<C>,
207    ) -> Result<(Self::State, ORD::Operation), Self::Error> {
208        if !Self::State::is_manager(&y, &adder)? {
209            let adder_access = Self::State::access(&y, &adder)?;
210            return Err(GroupError::InsufficientAccess(
211                adder,
212                adder_access,
213                y.group_id,
214            ));
215        }
216
217        if Self::State::is_member(&y, &added)? {
218            return Err(GroupError::GroupMember(added, y.group_id));
219        }
220
221        let action = GroupControlMessage {
222            group_id: y.group_id,
223            action: GroupAction::Add {
224                member: GroupMember::Individual(added),
225                access,
226            },
227        };
228
229        let (y, operation) = GroupCrdt::prepare(y, &action)?;
230        let y = GroupCrdt::process(y, &operation)?;
231
232        Ok((y, operation))
233    }
234
235    /// Remove a group member.
236    ///
237    /// The `remover` must be a manager and the `removed` identity must already be a member
238    /// of the group; failure to meet these conditions will result in an error. A member can only
239    /// remove themself from the group if they are a manager.
240    // TODO: Consider introducing self-removal for non-manager members:
241    //
242    // https://github.com/p2panda/p2panda/issues/759
243    fn remove(
244        y: Self::State,
245        remover: ID,
246        removed: ID,
247    ) -> Result<(Self::State, ORD::Operation), Self::Error> {
248        if !Self::State::is_manager(&y, &remover)? {
249            let remover_access = Self::State::access(&y, &remover)?;
250            return Err(GroupError::InsufficientAccess(
251                remover,
252                remover_access,
253                y.group_id,
254            ));
255        }
256
257        if !Self::State::is_member(&y, &removed)? {
258            return Err(GroupError::NotGroupMember(removed, y.group_id));
259        }
260
261        let action = GroupControlMessage {
262            group_id: y.group_id,
263            action: GroupAction::Remove {
264                member: GroupMember::Individual(removed),
265            },
266        };
267
268        let (y, operation) = GroupCrdt::prepare(y, &action)?;
269        let y = GroupCrdt::process(y, &operation)?;
270
271        Ok((y, operation))
272    }
273
274    /// Promote a group member to the given access level.
275    ///
276    /// The `promoter` must be a manager and the `promoted` identity must already be a member of
277    /// the group; failure to meet these conditions will result in an error. A redundant access
278    /// level assignment will also result in an error; for example, if the `promoted` member
279    /// currently has `Read` access and the given access is also `Read`.
280    fn promote(
281        y: Self::State,
282        promoter: ID,
283        promoted: ID,
284        access: Access<C>,
285    ) -> Result<(Self::State, ORD::Operation), Self::Error> {
286        if !Self::State::is_manager(&y, &promoter)? {
287            let promoter_access = Self::State::access(&y, &promoter)?;
288            return Err(GroupError::InsufficientAccess(
289                promoter,
290                promoter_access,
291                y.group_id,
292            ));
293        }
294
295        if !Self::State::is_member(&y, &promoted)? {
296            return Err(GroupError::NotGroupMember(promoted, y.group_id));
297        }
298
299        // Prevent redundant access level assignment.
300        if Self::State::access(&y, &promoted)? == access {
301            return Err(GroupError::SameAccessLevel(promoted, access, y.group_id));
302        }
303
304        let action = GroupControlMessage {
305            group_id: y.group_id,
306            action: GroupAction::Promote {
307                member: GroupMember::Individual(promoted),
308                access,
309            },
310        };
311
312        let (y, operation) = GroupCrdt::prepare(y, &action)?;
313        let y = GroupCrdt::process(y, &operation)?;
314
315        Ok((y, operation))
316    }
317
318    /// Demote a group member to the given access level.
319    ///
320    /// The `demoter` must be a manager and the `demoted` identity must already be a member of
321    /// the group; failure to meet these conditions will result in an error. A redundant access
322    /// level assignment will also result in an error; for example, if the `demoted` member
323    /// currently has `Manage` access and the given access is also `Manage`.
324    fn demote(
325        y: Self::State,
326        demoter: ID,
327        demoted: ID,
328        access: Access<C>,
329    ) -> Result<(Self::State, ORD::Operation), Self::Error> {
330        if !Self::State::is_manager(&y, &demoter)? {
331            let demoter_access = Self::State::access(&y, &demoter)?;
332            return Err(GroupError::InsufficientAccess(
333                demoter,
334                demoter_access,
335                y.group_id,
336            ));
337        }
338
339        if !Self::State::is_member(&y, &demoted)? {
340            return Err(GroupError::NotGroupMember(demoted, y.group_id));
341        }
342
343        if Self::State::access(&y, &demoted)? == access {
344            return Err(GroupError::SameAccessLevel(demoted, access, y.group_id));
345        }
346
347        let action = GroupControlMessage {
348            group_id: y.group_id,
349            action: GroupAction::Demote {
350                member: GroupMember::Individual(demoted),
351                access,
352            },
353        };
354
355        let (y, operation) = GroupCrdt::prepare(y, &action)?;
356        let y = GroupCrdt::process(y, &operation)?;
357
358        Ok((y, operation))
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use rand::SeedableRng;
365    use rand::rngs::StdRng;
366
367    use crate::test_utils::{TestGroupState, TestGroupStore, TestOrdererState};
368
369    use super::*;
370
371    const ALICE: char = 'A';
372    const BOB: char = 'B';
373    const CLAIRE: char = 'C';
374    const DAVE: char = 'D';
375
376    const MY_ID: char = 'T';
377    const GROUP_ID: char = 'G';
378
379    // Initialise the group manager and create a group with three initial members.
380    fn setup() -> TestGroupState {
381        let rng = StdRng::from_os_rng();
382
383        let store = TestGroupStore::default();
384        let orderer = TestOrdererState::new(MY_ID, store.clone(), rng);
385
386        let initial_members = [
387            (GroupMember::Individual(ALICE), Access::manage()),
388            (GroupMember::Individual(BOB), Access::read()),
389            (GroupMember::Individual(CLAIRE), Access::write()),
390        ]
391        .to_vec();
392
393        let group = Group::init(MY_ID, store, orderer);
394
395        let (group_y, _operation) = group.create(GROUP_ID, initial_members).unwrap();
396
397        group_y
398    }
399
400    // The following tests are all focused on ensuring correct validation and returned error
401    // variants. `GroupCrdt::prepare()` and `GroupCrdt::process()` are tested elsewhere and are therefore
402    // excluded from explicit testing here.
403    #[test]
404    fn add_validation_errors() {
405        let y = setup();
406
407        // Bob is not a manager.
408        let _expected_access = <Access>::read();
409        assert!(matches!(
410            Group::add(y.clone(), BOB, DAVE, Access::pull()),
411            Err(GroupError::InsufficientAccess(
412                BOB,
413                _expected_access,
414                GROUP_ID
415            ))
416        ));
417
418        // Claire is already a group member.
419        assert!(matches!(
420            Group::add(y, ALICE, CLAIRE, Access::pull()),
421            Err(GroupError::GroupMember(CLAIRE, GROUP_ID))
422        ));
423    }
424
425    #[test]
426    fn remove_validation_errors() {
427        let y = setup();
428
429        // Bob is not a manager.
430        let _expected_access = <Access>::read();
431        assert!(matches!(
432            Group::remove(y.clone(), BOB, CLAIRE),
433            Err(GroupError::InsufficientAccess(
434                BOB,
435                _expected_access,
436                GROUP_ID
437            ))
438        ));
439
440        // Dave is not a group member.
441        assert!(matches!(
442            Group::remove(y, ALICE, DAVE),
443            Err(GroupError::NotGroupMember(DAVE, GROUP_ID))
444        ));
445    }
446
447    #[test]
448    fn promote_validation_errors() {
449        let y = setup();
450
451        // Bob is not a manager.
452        let _expected_access = <Access>::read();
453        assert!(matches!(
454            Group::promote(y.clone(), BOB, CLAIRE, Access::manage()),
455            Err(GroupError::InsufficientAccess(
456                BOB,
457                _expected_access,
458                GROUP_ID
459            ))
460        ));
461
462        // Dave is not a group member.
463        assert!(matches!(
464            Group::promote(y.clone(), ALICE, DAVE, Access::read()),
465            Err(GroupError::NotGroupMember(DAVE, GROUP_ID))
466        ));
467
468        // Bob already has `Read` access.
469        let _expected_access = <Access>::read();
470        assert!(matches!(
471            Group::promote(y, ALICE, BOB, Access::read()),
472            Err(GroupError::SameAccessLevel(BOB, _expected_access, GROUP_ID))
473        ));
474    }
475
476    #[test]
477    fn demote_validation_errors() {
478        let y = setup();
479
480        // Bob is not a manager.
481        let _expected_access = <Access>::read();
482        assert!(matches!(
483            Group::demote(y.clone(), BOB, CLAIRE, Access::pull()),
484            Err(GroupError::InsufficientAccess(
485                BOB,
486                _expected_access,
487                GROUP_ID
488            ))
489        ));
490
491        // Dave is not a group member.
492        assert!(matches!(
493            Group::demote(y.clone(), ALICE, DAVE, Access::read()),
494            Err(GroupError::NotGroupMember(DAVE, GROUP_ID))
495        ));
496
497        // Bob already has `Read` access.
498        let _expected_access = <Access>::read();
499        assert!(matches!(
500            Group::demote(y, ALICE, BOB, Access::read()),
501            Err(GroupError::SameAccessLevel(BOB, _expected_access, GROUP_ID))
502        ));
503    }
504}