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