Skip to main content

coven_replication/sync/store/membership/
mod.rs

1//! Membership operations: list members, admit, and revoke.
2//!
3//! These are the high-level orchestration functions that download the membership
4//! chain from the storage, perform the operation, and upload the results.
5
6use coven_keys::keys::KeyError;
7use coven_protocol::membership::MembershipConflict;
8use coven_protocol::objects::StorageError;
9use coven_protocol::objects::StoreObjectError;
10use coven_storage::CloudHomeJoinInfo;
11use serde::{Deserialize, Serialize};
12
13#[derive(Clone, Serialize, Deserialize, Debug)]
14#[serde(deny_unknown_fields)]
15pub struct MemberAdmission {
16    pub store_id: String,
17    pub store_name: String,
18    pub join_info: CloudHomeJoinInfo,
19    pub owner_pubkey: String,
20    pub wrapped_key: coven_protocol::wrapped_store_key::WrappedStoreKeyRef,
21    pub store_root: coven_protocol::store_commit::StoreRootRef,
22    pub membership_floor: coven_protocol::membership::MembershipFloor,
23}
24
25/// Why a high-level membership operation (list members, admit, remove, rotate)
26/// failed. The security-critical orchestration layer that downloads the chain,
27/// performs the operation, and uploads the result: it preserves the typed error
28/// each step already produces — [`StorageError`], the owner-anchored
29/// [`AnchoredChainError`], the [`MembershipMutationError`] the admit/revoke path raises,
30/// [`KeyError`] — rather than flattening them into a string,
31/// and names the domain rules it enforces in place as their own variants.
32#[derive(Debug, thiserror::Error)]
33pub enum MembershipOpsError {
34    #[error("membership storage error: {0}")]
35    Storage(#[from] StorageError),
36    #[error("Store protocol object error: {0}")]
37    StoreObject(#[from] coven_protocol::objects::StoreObjectError),
38    #[error("membership database state error: {0}")]
39    Database(#[from] coven_database::DbError),
40    #[error("Store access failed: {0}")]
41    Store(#[from] crate::sync::store::StoreError),
42    #[error("{0}")]
43    Chain(#[from] AnchoredChainError),
44    #[error("{0}")]
45    Mutation(#[from] MembershipMutationError),
46    /// The removal and cloud rotation committed, but this device could not adopt
47    /// the rotated key into custody and its live cipher. The exact removal journal
48    /// and rotation gate remain durable, and retrying the same removal resumes it.
49    #[error(
50        "member removal committed the cloud key rotation, but this device could not \
51         adopt the rotated key locally: {source}; retry the same removal"
52    )]
53    RotationCommittedAdoptionFailed {
54        #[source]
55        source: KeyError,
56    },
57    #[error("cannot admit this device as a new member")]
58    SelfAdmission,
59    #[error("the identity is already a member with different role or provider account")]
60    ExistingMemberMismatch,
61    #[error("the existing member does not have exactly one current wrapped Store key")]
62    ExistingMemberKeyAuthority,
63    /// Admitting into a store whose founder entry is missing (a fresh store
64    /// that never founded, or a wiped `membership/*`). Bootstrapping a founder on
65    /// the spot is the takeover primitive, so admission is refused (issue #104).
66    #[error(
67        "no membership chain to admit into: the store's founder entry is \
68         missing (it is established at store creation)"
69    )]
70    NoFounderChainForAdmission,
71    #[error("membership chain has no founder")]
72    ChainHasNoFounder,
73    #[error("membership has an unresolved semantic conflict: {0:?}")]
74    SemanticConflict(Box<MembershipConflict>),
75    #[error("sharing requires an encrypted cloud home")]
76    NotEncryptedHome,
77}
78
79mod mutation;
80
81/// Why loading an owner-anchored membership chain failed.
82#[derive(Debug, thiserror::Error)]
83pub enum AnchoredChainError {
84    #[error("membership storage unavailable while {operation}: {source}")]
85    StorageUnavailable {
86        operation: String,
87        #[source]
88        source: StorageError,
89    },
90    #[error("membership chain failed to load/validate: {0}")]
91    LoadFailed(String),
92    #[error("membership object: {0}")]
93    Object(#[from] StoreObjectError),
94    #[error("membership database: {0}")]
95    Database(#[from] coven_database::DbError),
96    #[error("membership protocol: {0}")]
97    Membership(#[from] coven_protocol::membership::MembershipError),
98    #[error("membership provider probe: {0}")]
99    ProviderProbe(#[from] coven_protocol::provider::ProviderProbeError),
100    #[error("membership floor failed validation: {0}")]
101    InvalidFloor(#[from] coven_protocol::membership::MembershipFloorError),
102    #[error("membership Store pull: {0}")]
103    StorePull(#[source] Box<crate::sync::store::StorePullError>),
104    #[error("chain founder {founder:?} is not the pinned owner {owner}")]
105    FounderMismatch {
106        founder: Option<String>,
107        owner: String,
108    },
109}
110
111impl From<crate::sync::store::StorePullError> for AnchoredChainError {
112    fn from(error: crate::sync::store::StorePullError) -> Self {
113        Self::StorePull(Box::new(error))
114    }
115}
116
117impl AnchoredChainError {
118    pub(crate) fn from_store_object(error: StoreObjectError) -> Self {
119        match error {
120            StoreObjectError::Storage(source @ StorageError::Storage(_))
121            | StoreObjectError::Storage(source @ StorageError::RotationPending(_)) => {
122                Self::StorageUnavailable {
123                    operation: "discovering immutable membership objects".to_string(),
124                    source,
125                }
126            }
127            error => Self::Object(error),
128        }
129    }
130}
131
132pub use mutation::MembershipMutationError;
133
134#[cfg(test)]
135mod tests;