aven_core/sync/client/engine/
devices.rs1use anyhow::{Result, bail, ensure};
7
8use super::{
9 NOT_SET_UP, REFUSED, associated_server, explain_change_limit, is_set_up, track_access_result,
10};
11use crate::db::Database;
12use crate::sync::client::coordination;
13use crate::sync::client::enrollment::{self, RemovalStatus};
14use crate::sync::client::errors::{code, has_code};
15use crate::sync::client::exchange::Link;
16use crate::sync::client::host::{ClientHost, key_store};
17use crate::sync::client::keys::ProtectedLocalKeyStore;
18
19const REMOVED: &str = "error sync-device-removed hint=\"another device removed this device from sync; its local tasks and images stay available here but can no longer sync\"";
20
21fn explain_revoked(error: anyhow::Error) -> anyhow::Error {
22 if has_code(&error, "enrollment-revoked") {
23 error.context(REMOVED)
24 } else {
25 error
26 }
27}
28
29struct Session {
30 _guard: coordination::SyncProcessGuard,
31 store: ProtectedLocalKeyStore,
32 client: enrollment::Client,
33 server: String,
34}
35
36async fn open(link: Link, database: &Database, host: &dyn ClientHost) -> Result<Session> {
38 host.ensure_sync_allowed()?;
39 ensure!(is_set_up(database).await?, NOT_SET_UP);
40 let store = key_store(host, database).await?;
41 let guard = coordination::acquire(database).await?;
42 let server = associated_server(&store, database).await?;
43 Ok(Session {
44 _guard: guard,
45 client: enrollment::Client::new(&server, link)?,
46 store,
47 server,
48 })
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct Device {
54 pub id: [u8; 32],
55 pub label: Option<String>,
56 pub current: bool,
57 pub admission_sequence: u64,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct DeviceListing {
65 pub server: String,
66 pub key_rotation_pending: bool,
68 pub devices: Vec<Device>,
69}
70
71pub async fn load_devices(
72 link: Link,
73 database: &Database,
74 host: &dyn ClientHost,
75) -> Result<DeviceListing> {
76 let Session {
77 _guard,
78 store,
79 client,
80 server,
81 } = open(link, database, host).await?;
82 let mut inputs = store.active_inputs(database, &server).await?;
83 let refreshed = client
84 .refresh_inputs(&store, database, &mut inputs)
85 .await
86 .map_err(|error| match code(&error).as_deref() {
87 Some("enrollment-unauthorized") => error.context(REFUSED),
88 _ => explain_revoked(error),
89 });
90 track_access_result(database, refreshed).await?;
91 let current = inputs.device();
92 let labels = database.device_labels().await?;
93 Ok(DeviceListing {
94 server,
95 key_rotation_pending: inputs.membership.rotation_pending(),
96 devices: inputs
97 .membership
98 .admissions()
99 .map(|(device, sequence)| Device {
100 id: device,
101 label: labels.get(&device).cloned(),
102 current: device == current,
103 admission_sequence: sequence,
104 })
105 .collect(),
106 })
107}
108
109async fn explain_removal_error(
112 store: &ProtectedLocalKeyStore,
113 database: &Database,
114 server: &str,
115 target: [u8; 32],
116 error: anyhow::Error,
117) -> anyhow::Error {
118 let hint = match code(&error).as_deref() {
119 Some("enrollment-revoked") => return error.context(REMOVED),
120 Some("membership-signer" | "membership-invalid") => {
122 match store.active_inputs(database, server).await {
123 Ok(inputs) if !inputs.membership.has_device(target) => {
124 "error sync-device-not-found hint=\"the device is not in sync; list devices with `aven sync device list`\""
125 }
126 _ => return error,
127 }
128 }
129 Some("membership-change-limit") => return explain_change_limit(error),
130 Some("management-unfinished") => {
131 "error sync-device-removal-unfinished hint=\"an earlier device removal from this device is unfinished; run `aven sync` to finish it, then retry\""
132 }
133 Some("enrollment-unauthorized") => {
134 "error sync-device-removal-incomplete hint=\"rerun the same command; it resumes this removal. If the server keeps refusing, another device may have removed this device from sync\""
135 }
136 _ => {
137 "error sync-device-removal-incomplete hint=\"rerun the same command; it resumes this removal\""
138 }
139 };
140 error.context(hint)
141}
142
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub struct Removal {
146 pub device: [u8; 32],
147 pub access_revoked: bool,
149 pub key_rotation_pending: bool,
151}
152
153pub async fn remove_other_device(
156 link: Link,
157 database: &Database,
158 host: &dyn ClientHost,
159 target: [u8; 32],
160) -> Result<Removal> {
161 let Session {
162 _guard,
163 store,
164 client,
165 server,
166 } = open(link, database, host).await?;
167 let current = store.active_inputs(database, &server).await?.device();
170 ensure!(
171 target != current,
172 "error sync-device-current hint=\"this is the current device; remove it from another device in sync\""
173 );
174 let status = match client.remove_device(&store, database, target).await {
175 Ok(status) => status,
176 Err(error) => {
177 let error = explain_removal_error(&store, database, &server, target, error).await;
178 return track_access_result(database, Err(error)).await;
179 }
180 };
181 let key_rotation_pending = match status {
182 RemovalStatus::Complete => false,
183 RemovalStatus::Pending => true,
184 RemovalStatus::SelfRevoked => bail!("error management-self-revoke"),
185 };
186 let membership = store.active_inputs(database, &server).await?.membership;
187 Ok(Removal {
188 device: target,
189 access_revoked: !membership.has_device(target),
190 key_rotation_pending,
191 })
192}
193
194pub async fn finish_removal(
197 link: Link,
198 database: &Database,
199 host: &dyn ClientHost,
200) -> Result<bool> {
201 let Session {
202 _guard,
203 store,
204 client,
205 server,
206 } = open(link, database, host).await?;
207 let finished = client
208 .finish_pending_management(&store, database)
209 .await
210 .map_err(|error| match code(&error).as_deref() {
211 Some("enrollment-unauthorized") => error.context(REFUSED),
212 _ => explain_revoked(error),
213 });
214 track_access_result(database, finished).await?;
215 Ok(store
216 .active_inputs(database, &server)
217 .await?
218 .membership
219 .rotation_pending())
220}