p2panda_auth/group/
mod.rs

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