Skip to main content

coven_protocol/membership/
authority.rs

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