coven_format/admin.rs
1//! Admin changes in sequence slots (D6). The enclosing slot supplies identity and author.
2
3use crate::error::{require, Error, Rule};
4use crate::value::{name, Slot};
5use crate::wire::{wire_struct, Decoder, Encoder, Wire};
6pub use coven_crypto::MemberPublicKeys;
7use coven_foundation::id_source::{CircleId, DeviceId, InviteId, KeyId, MemberId, StoreId};
8use coven_merge::Audience;
9
10/// A member's role in the store (§10).
11#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
12pub enum MemberRole {
13 /// Can add and remove members and change roles.
14 Admin,
15 /// Can write data and manage their own devices.
16 Member,
17}
18impl Wire for MemberRole {
19 fn put(&self, out: &mut Encoder) -> Result<(), Error> {
20 match self {
21 Self::Admin => 0u8,
22 Self::Member => 1,
23 }
24 .put(out)
25 }
26 fn get(input: &mut Decoder<'_>) -> Result<Self, Error> {
27 match u8::get(input)? {
28 0 => Ok(Self::Admin),
29 1 => Ok(Self::Member),
30 tag => Err(Error::UnknownTag { field: "role", tag }),
31 }
32 }
33}
34
35/// A snapshot's identity without a storage path (§16.2).
36#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
37pub struct SnapshotId {
38 /// The rows the snapshot covers.
39 pub audience: Audience,
40 /// The device that wrote the snapshot.
41 pub device: DeviceId,
42 /// The positive number in this device's snapshot sequence.
43 pub number: u64,
44}
45wire_struct!(SnapshotId, audience, device, number);
46
47/// Every admin operation listed in §10, including store creation.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum StoreChange {
50 /// Create a store, name its first admin and register the writing device.
51 /// The entry's author is the first admin's stable id; `admin` supplies
52 /// that member's initial pair, which verifies this entry (D6).
53 CreateStore {
54 /// The store's id.
55 store: StoreId,
56 /// The store's name.
57 name: String,
58 /// The first admin's initial public pair.
59 admin: MemberPublicKeys,
60 /// The owner’s provider account or public S3 key id.
61 access: crate::MemberAccess,
62 /// The first store key, introduced by this entry.
63 key: KeyId,
64 /// SHA-256 of the exact 32 store-key bytes.
65 key_hash: [u8; 32],
66 /// The name of the device that wrote this entry, belonging to its author.
67 device_name: String,
68 },
69 /// Set the store name, as an admin in the preceding state (§10).
70 RenameStore {
71 /// The new name.
72 name: String,
73 },
74 /// Admit a joining person: add their stable id with both public keys,
75 /// their initial role and access, and register the device their request
76 /// names. Only an admission slot holds it, beside the member's key copies
77 /// (§13.2, D9).
78 AdmitMember {
79 /// The added member's stable id, fixed by the admin's invite.
80 member: MemberId,
81 /// The member's initial public pair.
82 keys: MemberPublicKeys,
83 /// The initial role.
84 role: MemberRole,
85 /// Storage access granted by the inviting owner.
86 access: crate::MemberAccess,
87 /// The joining installation, registered for the member. Never the
88 /// writing device.
89 device: DeviceId,
90 /// The joining installation's name, from its request.
91 device_name: String,
92 },
93 /// Recover an active member on a new device: rebind their pair, as
94 /// replace-member-keys does, set their access when the invite carried a
95 /// new S3 key, and register the device their request names. Only an
96 /// admission slot holds it (§13.2, D9).
97 RecoverMember {
98 /// The recovered member.
99 member: MemberId,
100 /// The binding which must still be current before this slot.
101 expected: MemberPublicKeys,
102 /// The member's new public pair.
103 keys: MemberPublicKeys,
104 /// The member's new storage access, as set-access records it: the
105 /// S3 key the recovery invite carries. `None` keeps their access.
106 access: Option<crate::MemberAccess>,
107 /// The new installation, registered for the member. Never the writing
108 /// device.
109 device: DeviceId,
110 /// The new installation's name, from its request.
111 device_name: String,
112 },
113 /// Record a member's current storage access, used by a later removal (§10):
114 /// the author's own, or an admin's replacement S3 key for another member.
115 SetAccess {
116 /// The member whose access this records.
117 member: MemberId,
118 /// The provider account, or the S3 key id and its principal; no credentials.
119 access: crate::MemberAccess,
120 },
121 /// Remove a member and delete circles left empty (§14.1).
122 RemoveMember {
123 /// The removed member.
124 member: MemberId,
125 },
126 /// Set a member's role.
127 ChangeRole {
128 /// The affected member.
129 member: MemberId,
130 /// The target role.
131 role: MemberRole,
132 },
133 /// Register the writing install for the entry's author: an install
134 /// registers only itself (§10.2).
135 AddDevice {
136 /// The device's name.
137 name: String,
138 /// The older install of the same member being replaced, if any (§19.3).
139 /// Must differ from the writing device.
140 replaces: Option<DeviceId>,
141 },
142 /// Remove a device's access.
143 RemoveDevice {
144 /// The removed install.
145 device: DeviceId,
146 },
147 /// Make a circle with its creator as its first member.
148 CreateCircle {
149 /// The new circle's id.
150 circle: CircleId,
151 /// The circle's name.
152 name: String,
153 /// The newest breaking migration its maker's app had run, zero if
154 /// none: the circle's schema version until a raise of its own goes
155 /// past it (§15.5, §16.3).
156 breaking_version: u32,
157 /// The first circle key, introduced by this entry.
158 key: KeyId,
159 /// SHA-256 of the exact 32 circle-key bytes.
160 key_hash: [u8; 32],
161 },
162 /// Rename a circle without changing its members, keys or rows.
163 RenameCircle {
164 /// The circle being renamed.
165 circle: CircleId,
166 /// Its new name.
167 name: String,
168 },
169 /// Delete a circle (§15.9).
170 DeleteCircle {
171 /// The deleted circle.
172 circle: CircleId,
173 },
174 /// Add a store member to a circle (§15.5).
175 AddCircleMember {
176 /// The circle being joined.
177 circle: CircleId,
178 /// The added member.
179 member: MemberId,
180 },
181 /// Remove a circle member, deleting the circle if left empty (§15.8).
182 RemoveCircleMember {
183 /// The circle being left.
184 circle: CircleId,
185 /// The removed member.
186 member: MemberId,
187 },
188 /// Introduce a key without changing audience membership (§12.1).
189 RotateKey {
190 /// The audience whose member authored the rotation.
191 audience: Audience,
192 /// The introduced key's random id.
193 key: KeyId,
194 /// SHA-256 of the exact 32 key bytes.
195 key_hash: [u8; 32],
196 },
197 /// Raise an audience's app schema version with its replacement snapshot (§18.3).
198 RaiseSchema {
199 /// The new schema version.
200 version: u32,
201 /// The earlier announcement of the prepared snapshot.
202 announcement: Slot,
203 },
204 /// Reset the store's or a circle's rows to a snapshot (§19.4).
205 Reset {
206 /// The earlier announcement of the prepared reset snapshot.
207 announcement: Slot,
208 },
209 /// Replace an active member's public pair, keeping their id, role, access,
210 /// circles and devices (§10). It carries no audience keys.
211 ReplaceMemberKeys {
212 /// The member whose keys change.
213 member: MemberId,
214 /// The binding which must still be current before this slot.
215 expected: MemberPublicKeys,
216 /// The member's new public pair.
217 keys: MemberPublicKeys,
218 },
219 /// Record that the writing device's invite shares the store with a
220 /// provider account (§13.2, §14.2). Claimed before the grant, so the
221 /// owner's other devices know of the share and take it back once the
222 /// invite ends or its device is gone for good.
223 AddInviteShare {
224 /// The invite, fixed when it was made.
225 invite: InviteId,
226 /// The provider account the invite shares the store with.
227 account: String,
228 },
229 /// End the share the writing device recorded for its invite (§13.2).
230 EndInviteShare {
231 /// The invite whose share ends.
232 invite: InviteId,
233 },
234}
235
236impl StoreChange {
237 pub(crate) fn validate(&self) -> Result<(), Error> {
238 match self {
239 Self::CreateStore { name: n, device_name, .. } => {
240 name(n)?;
241 name(device_name)
242 }
243 Self::RenameStore { name: n }
244 | Self::AdmitMember { device_name: n, .. }
245 | Self::RecoverMember { device_name: n, .. }
246 | Self::AddDevice { name: n, .. }
247 | Self::CreateCircle { name: n, .. }
248 | Self::RenameCircle { name: n, .. } => name(n),
249 Self::RaiseSchema { version, .. } => {
250 require(*version > 0, "schema version", Rule::Required)
251 }
252 Self::Reset { .. } => Ok(()),
253 Self::AddInviteShare { account, .. } => {
254 require(!account.trim().is_empty(), "invite share account", Rule::Required)?;
255 crate::error::bound(
256 account.len(),
257 crate::member_access::MAX_TEXT,
258 "invite share account",
259 )
260 }
261 Self::RemoveMember { .. }
262 | Self::RotateKey { .. }
263 | Self::SetAccess { .. }
264 | Self::ChangeRole { .. }
265 | Self::RemoveDevice { .. }
266 | Self::DeleteCircle { .. }
267 | Self::AddCircleMember { .. }
268 | Self::RemoveCircleMember { .. }
269 | Self::ReplaceMemberKeys { .. }
270 | Self::EndInviteShare { .. } => Ok(()),
271 }
272 }
273}
274
275// The tag and named fields are the whole wire layout of each store change.
276macro_rules! store_changes {
277 ($($tag:literal => $variant:ident { $($field:ident $(=> $get:path)?),+ }),+ $(,)?) => {
278 impl Wire for StoreChange {
279 fn put(&self, out: &mut Encoder) -> Result<(), Error> {
280 match self { $(Self::$variant { $($field),+ } => {
281 ($tag as u8).put(out)?; $($field.put(out)?;)+ Ok(())
282 }),+ }
283 }
284 fn get(input: &mut Decoder<'_>) -> Result<Self, Error> {
285 match u8::get(input)? {
286 $($tag => Ok(Self::$variant { $($field: crate::wire::wire_struct!(@get input $(, $get)?)?),+ }),)+
287 tag => Err(Error::UnknownTag { field: "store change", tag }),
288 }
289 }
290 }
291 };
292}
293store_changes!(
294 0 => CreateStore { store, name => crate::wire::get_name, admin, access, device_name => crate::wire::get_name, key, key_hash },
295 1 => AdmitMember { member, keys, role, access, device, device_name => crate::wire::get_name },
296 2 => RemoveMember { member },
297 3 => ChangeRole { member, role },
298 4 => AddDevice { name => crate::wire::get_name, replaces },
299 5 => RemoveDevice { device },
300 6 => CreateCircle { circle, name => crate::wire::get_name, breaking_version, key, key_hash },
301 7 => RenameCircle { circle, name => crate::wire::get_name },
302 8 => DeleteCircle { circle },
303 9 => AddCircleMember { circle, member },
304 10 => RemoveCircleMember { circle, member },
305 11 => RaiseSchema { version, announcement },
306 13 => Reset { announcement },
307 14 => SetAccess { member, access },
308 15 => RotateKey { audience, key, key_hash },
309 16 => ReplaceMemberKeys { member, expected, keys },
310 17 => RenameStore { name => crate::wire::get_name },
311 18 => AddInviteShare { invite, account => get_account },
312 19 => EndInviteShare { invite },
313 20 => RecoverMember { member, expected, keys, access, device, device_name => crate::wire::get_name },
314);
315
316/// An invite share's provider account, bounded as member access bounds an
317/// account (D6).
318fn get_account(input: &mut Decoder<'_>) -> Result<String, Error> {
319 crate::wire::get_text(input, crate::member_access::MAX_TEXT, "invite share account")
320}
321
322impl StoreChange {
323 /// Encode the one bounded kind-4 admin frame (D6).
324 pub fn encode(&self) -> Result<Vec<u8>, Error> {
325 self.validate()?;
326 crate::encode_frame(crate::kinds::ADMIN, self)
327 }
328
329 /// Decode exactly one admin frame. Slot-dependent authorization is separate.
330 pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
331 let change: Self =
332 crate::wire::decode_frame_value(bytes, crate::kinds::ADMIN, "admin frame")?;
333 change.validate()?;
334 Ok(change)
335 }
336}
337
338#[cfg(test)]
339#[path = "admin_tests.rs"]
340mod tests;