coven_protocol/membership/
authority.rs1use super::*;
2
3impl MembershipChain {
4 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 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 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}