Skip to main content

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;