Skip to main content

coven_protocol/membership/
authority.rs

1use super::*;
2
3impl MembershipChain {
4    /// Membership changes consume the resolved grant and key authority they
5    /// were prepared against. Unrelated device controls do not change that
6    /// authority, but an accepted grant change requires a new candidate.
7    pub fn validate_publication_predecessor(
8        &self,
9        entry: &MembershipEntry,
10    ) -> Result<(), MembershipError> {
11        match &entry.change {
12            StoreAuthorityChange::SetMember { .. }
13            | StoreAuthorityChange::RemoveMember { .. }
14            | StoreAuthorityChange::ProviderAdmin => {}
15            StoreAuthorityChange::Founder { .. } => return Err(MembershipError::InvalidFounder),
16            StoreAuthorityChange::DeviceRegistrationActivation { .. }
17            | StoreAuthorityChange::DeviceExclusionProposal { .. }
18            | StoreAuthorityChange::DeviceExclusionOutcome { .. } => return Ok(()),
19        }
20        let current = self.resolved();
21        let included = causal_grants::history_closure(&self.entries, &entry.dependencies);
22        let causal_past = self
23            .entries
24            .iter()
25            .filter(|prior| included.contains(&prior.coord()))
26            .cloned()
27            .collect::<Vec<_>>();
28        let reduced = reduce_store_membership(&causal_past)?;
29        let provider = crate::provider::ProviderAdminState::reduce_merge(
30            &self.provider_admin_genesis,
31            &causal_past,
32            &reduced.included,
33        )?;
34        let prepared = resolved_store_membership(&reduced, provider, &causal_past)?;
35        if prepared.state_hash != current.state_hash {
36            return Err(MembershipError::PublicationPredecessorChanged {
37                coord: Box::new(entry.coord()),
38            });
39        }
40        Ok(())
41    }
42
43    pub fn can_write_now(&self, pubkey: &str) -> bool {
44        self.active_grants_for(pubkey)
45            .iter()
46            .any(|(_, record)| record.role.can_write())
47    }
48
49    /// Whether `pubkey` holds any active grant — a member of this Store now,
50    /// whatever role it has.
51    ///
52    /// Distinct from any question about that member's devices. Removing a
53    /// member ends its grants here and rotates the key; it does not mark the
54    /// devices it registered inactive, because device status tracks a device's
55    /// own lifecycle — a lost laptop belonging to a member in good standing.
56    /// Anything asking "could this principal still be owed history" has to ask
57    /// membership, not device status.
58    pub fn is_member_now(&self, pubkey: &str) -> bool {
59        !self.active_grants_for(pubkey).is_empty()
60    }
61
62    pub fn is_owner_now(&self, pubkey: &str) -> bool {
63        self.active_grants_for(pubkey)
64            .iter()
65            .any(|(_, record)| record.role.is_owner())
66    }
67
68    pub fn authorizes_write_authority(&self, authority: &MembershipCoord, pubkey: &str) -> bool {
69        let resolved = self.resolved();
70        resolved.active_grants().any(|(_, record)| {
71            record.member_pubkey == pubkey
72                && record.role.can_write()
73                && &record.creation_authority == authority
74        })
75    }
76
77    /// The permanent retirement of this exact grant.
78    pub fn write_authority_retirement(
79        &self,
80        authority: &MembershipCoord,
81        pubkey: &str,
82    ) -> Option<&MembershipGrantRetirement> {
83        let resolved = self.resolved();
84        let grant = resolved.grants.values().find(|grant| {
85            grant.record().creation_authority == *authority
86                && grant.record().member_pubkey == pubkey
87        })?;
88        Some(
89            grant
90                .retirements()?
91                .iter()
92                .next()
93                .expect("grant retirements are nonempty"),
94        )
95    }
96
97    pub fn active_grant(&self, grant_id: &MembershipGrantId) -> Option<&MembershipGrantRecord> {
98        let resolved = self.resolved();
99        resolved.active_grant(grant_id)
100    }
101
102    pub fn current_members(&self) -> Vec<(String, MemberRole)> {
103        let mut members = BTreeMap::new();
104        for state in self.resolved().grants.values() {
105            let Some(record) = state.active() else {
106                continue;
107            };
108            members.insert(record.member_pubkey.clone(), record.role.role());
109        }
110        members.into_iter().collect()
111    }
112
113    pub fn active_wrapped_keys_for(&self, recipient_pubkey: &str) -> Vec<WrappedStoreKeyRef> {
114        let active_grants = self.active_grant_ids(recipient_pubkey);
115        self.entries_with_coords()
116            .filter(|(coord, _)| self.included.contains(*coord))
117            .flat_map(|(_, entry)| match &entry.change {
118                StoreAuthorityChange::SetMember {
119                    grant_id,
120                    wrapped_key,
121                    ..
122                } if active_grants.contains(grant_id) => std::slice::from_ref(wrapped_key),
123                StoreAuthorityChange::RemoveMember { wrapped_keys, .. } => wrapped_keys.as_slice(),
124                StoreAuthorityChange::Founder { .. }
125                | StoreAuthorityChange::SetMember { .. }
126                | StoreAuthorityChange::DeviceRegistrationActivation { .. }
127                | StoreAuthorityChange::DeviceExclusionProposal { .. }
128                | StoreAuthorityChange::DeviceExclusionOutcome { .. }
129                | StoreAuthorityChange::ProviderAdmin => &[],
130            })
131            .filter(|reference| reference.recipient_pubkey == recipient_pubkey)
132            .cloned()
133            .collect::<BTreeSet<_>>()
134            .into_iter()
135            .collect()
136    }
137
138    pub fn wrapped_key_authority_for(
139        &self,
140        recipient_pubkey: &str,
141    ) -> Result<Vec<WrappedStoreKeyRef>, MembershipError> {
142        let active_grants = self.active_grants_for(recipient_pubkey);
143        for (index, (rotation_coord, entry)) in self
144            .entries_with_coords()
145            .enumerate()
146            .filter(|(_, (coord, _))| self.included.contains(*coord))
147        {
148            let StoreAuthorityChange::RemoveMember { wrapped_keys, .. } = &entry.change else {
149                continue;
150            };
151            if wrapped_keys
152                .iter()
153                .any(|reference| reference.recipient_pubkey == recipient_pubkey)
154            {
155                continue;
156            }
157            let rotation_generation = wrapped_keys
158                .first()
159                .ok_or(MembershipError::InvalidWrappedKeys(index))?
160                .generation;
161            let covered_by_later_grant = !active_grants.is_empty()
162                && active_grants.iter().all(|(active_grant, _)| {
163                    let Some((_, creation)) = self.entries_with_coords().find(|(_, entry)| {
164                        matches!(
165                            &entry.change,
166                            StoreAuthorityChange::SetMember { grant_id, .. }
167                                if grant_id == *active_grant
168                        )
169                    }) else {
170                        return false;
171                    };
172                    let StoreAuthorityChange::SetMember { wrapped_key, .. } = &creation.change
173                    else {
174                        return false;
175                    };
176                    wrapped_key.generation >= rotation_generation
177                        && causal_grants::history_closure(&self.entries, &creation.dependencies)
178                            .contains(rotation_coord)
179                });
180            if !covered_by_later_grant {
181                return Err(MembershipError::MissingWrappedKeyCoverage {
182                    recipient_pubkey: recipient_pubkey.to_string(),
183                    rotation: Box::new(rotation_coord.clone()),
184                });
185            }
186        }
187        Ok(self.active_wrapped_keys_for(recipient_pubkey))
188    }
189
190    pub fn current_member_provider_email(&self, pubkey: &str) -> Option<&str> {
191        self.active_grants_for(pubkey)
192            .into_iter()
193            .next()
194            .and_then(|(_, record)| record.provider_account_email.as_deref())
195    }
196
197    pub fn write_grant_authority(&self, pubkey: &str) -> Option<MembershipCoord> {
198        self.active_grants_for(pubkey)
199            .into_iter()
200            .find(|(_, record)| record.role.can_write())
201            .map(|(_, record)| record.creation_authority.clone())
202    }
203
204    pub fn active_grant_ids(&self, pubkey: &str) -> BTreeSet<MembershipGrantId> {
205        self.active_grants_for(pubkey)
206            .into_iter()
207            .map(|(grant, _)| grant.clone())
208            .collect()
209    }
210
211    pub fn active_owner_grant(&self, pubkey: &str) -> Option<MembershipGrantId> {
212        self.active_grants_for(pubkey)
213            .into_iter()
214            .find(|(_, record)| record.role.is_owner())
215            .map(|(grant, _)| grant.clone())
216    }
217
218    pub(super) fn membership_retirement_barriers(
219        &self,
220        grants: &BTreeSet<MembershipGrantId>,
221        device_state: Option<&StoreDeviceStateRef>,
222    ) -> Result<BTreeMap<MembershipGrantId, MergeMembershipGrantRetirementBarrier>, MembershipError>
223    {
224        let retires_owner = grants.iter().any(|grant| {
225            self.resolved()
226                .grants
227                .get(grant)
228                .and_then(GrantState::active)
229                .is_some_and(|record| record.role.is_owner())
230        });
231        if retires_owner && device_state.is_none() {
232            return Err(MembershipError::MissingOwnerRecoveryState);
233        }
234        if !retires_owner && device_state.is_some() {
235            return Err(MembershipError::UnexpectedOwnerRecoveryState);
236        }
237        let recovery = match device_state {
238            Some(state) => state.recovery(),
239            None => &[],
240        };
241        grants
242            .iter()
243            .map(|grant| {
244                let record = self
245                    .resolved()
246                    .grants
247                    .get(grant)
248                    .and_then(GrantState::active)
249                    .ok_or_else(|| MembershipError::NotAMember(grant.to_string()))?;
250                let author_streams = StoreGrantStreamBarrier {
251                    observed_streams: self
252                        .effective_frontier()
253                        .into_iter()
254                        .filter(|coord| coord.author_owner_grant == *grant)
255                        .collect(),
256                };
257                let barrier = if record.role.is_owner() {
258                    let cursor = recovery
259                        .iter()
260                        .find(|cursor| cursor.owner_grant == *grant)
261                        .cloned()
262                        .ok_or(MembershipError::MissingOwnerRecoveryState)?;
263                    MergeMembershipGrantRetirementBarrier::Owner {
264                        barrier: MergeStoreOwnerGrantBarrier {
265                            author_streams,
266                            recovery: cursor,
267                        },
268                    }
269                } else {
270                    MergeMembershipGrantRetirementBarrier::NonOwner { author_streams }
271                };
272                Ok((grant.clone(), barrier))
273            })
274            .collect()
275    }
276
277    pub(super) fn active_grants_for(
278        &self,
279        pubkey: &str,
280    ) -> Vec<(&MembershipGrantId, &MembershipGrantRecord)> {
281        self.resolved()
282            .grants
283            .iter()
284            .filter_map(|(grant, state)| {
285                state
286                    .active()
287                    .filter(|record| record.member_pubkey == pubkey)
288                    .map(|record| (grant, record))
289            })
290            .collect()
291    }
292}