1use time::OffsetDateTime;
6use time::format_description::well_known::Rfc3339;
7
8use super::{OpenFailure, TenantError, Tenants, io, stored_retention};
9use crate::audit::Action;
10use crate::clock::from_nanos;
11use crate::envelope::{self, Context};
12use crate::ids::TenantId;
13use crate::retention::Retention;
14use crate::store::Examination;
15
16#[derive(Debug)]
18pub struct TenantExamination {
19 pub id: TenantId,
20 pub name: String,
21 pub rotation_stopped: bool,
23 pub store: Result<Examination, OpenFailure>,
25}
26
27#[derive(Debug)]
29pub struct RegistryExamination {
30 pub tenants: Vec<TenantExamination>,
31 pub removed_since_master: u64,
34 pub oldest_removed_since_master: Option<OffsetDateTime>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Default)]
42pub struct KeyOrder {
43 pub wrapped_elsewhere: u64,
45 pub rotations_stopped: u64,
47 pub data_under_other_keys: u64,
50 pub catalogs_unopened: u64,
53 pub unopened_error: Option<String>,
56}
57
58impl KeyOrder {
59 pub fn is_sound(&self) -> bool {
60 *self == KeyOrder::default()
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68pub struct RotationBacklog {
69 pub tenants_with_purges: u64,
70 pub purges: u64,
71 pub oldest_purge: Option<OffsetDateTime>,
72 pub removals: u64,
73 pub oldest_removal: Option<OffsetDateTime>,
74}
75
76impl Tenants {
77 pub fn key_order(&self) -> Result<KeyOrder, TenantError> {
79 let rows = self.registry.snapshot()?.tenants()?;
80 let mut order = KeyOrder::default();
81 for row in rows {
82 let (id, wrapped, pending) = (row.id, row.wrapped, row.pending.is_some());
83 if envelope::unwrap(&self.master, &Context::Tenant { tenant: id }, &wrapped).is_err() {
84 order.wrapped_elsewhere += 1;
85 continue;
86 }
87 if pending {
88 order.rotations_stopped += 1;
89 }
90 let elsewhere = match self.store_or_why(id) {
91 Ok(store) => store.rows_under_other_keys().map_err(io)? > 0,
92 Err(OpenFailure::WrongKey) => true,
93 Err(failure) => {
94 order.catalogs_unopened += 1;
95 if let OpenFailure::Access { error, .. } = failure {
96 order.unopened_error.get_or_insert(error.to_string());
97 }
98 false
99 }
100 };
101 if elsewhere {
102 order.data_under_other_keys += 1;
103 }
104 }
105 Ok(order)
106 }
107
108 pub fn rotation_backlog(&self) -> Result<RotationBacklog, TenantError> {
111 let rows = self.registry.snapshot()?.tenants()?;
112 let mut backlog = RotationBacklog::default();
113 for row in rows {
114 let key_since = from_nanos(row.key_since).ok_or(TenantError::Io)?;
115 let (purges, oldest) = self.store(row.id)?.purges_since(key_since).map_err(io)?;
116 if purges > 0 {
117 backlog.tenants_with_purges += 1;
118 backlog.purges += purges;
119 backlog.oldest_purge = earliest(backlog.oldest_purge, oldest);
120 }
121 }
122 (backlog.removals, backlog.oldest_removal) = self.removals_since_master()?;
123 Ok(backlog)
124 }
125
126 fn removals_since_master(&self) -> Result<(u64, Option<OffsetDateTime>), TenantError> {
128 let (since, removals) = {
129 let mut tx = self.registry.snapshot()?;
130 let since = tx.master_since(self.master.id())?;
131 let removals = tx.record_times(Action::TenantRemove, 204)?;
132 (since, removals)
133 };
134 let since = since
135 .map(|s| from_nanos(s).ok_or(TenantError::Io))
136 .transpose()?;
137 let (mut count, mut oldest) = (0, None);
138 for at in removals {
139 let at = OffsetDateTime::parse(&at, &Rfc3339).map_err(io)?;
140 if since.is_none_or(|since| at >= since) {
141 count += 1;
142 oldest = earliest(oldest, Some(at));
143 }
144 }
145 Ok((count, oldest))
146 }
147
148 pub fn examine(
151 &self,
152 default: Retention,
153 now: OffsetDateTime,
154 ) -> Result<RegistryExamination, TenantError> {
155 let rows = self.registry.snapshot()?.tenants()?;
156 let time = |nanos| from_nanos(nanos).ok_or(TenantError::Io);
157 let mut tenants = Vec::new();
158 for row in rows {
159 let (id, name, retention, key_since) = (row.id, row.name, row.retention, row.key_since);
160 let rotation_stopped = row.pending.is_some();
161 let retention = super::kept_for(stored_retention(retention)?, default);
162 let key_since = time(key_since)?;
163 let store = self.store_or_why(id).and_then(|store| {
164 store
165 .examine(retention, key_since, now)
166 .map_err(|e| OpenFailure::Refused(io(e)))
167 });
168 tenants.push(TenantExamination {
169 id,
170 name,
171 rotation_stopped,
172 store,
173 });
174 }
175 let (removed_since_master, oldest_removed_since_master) = self.removals_since_master()?;
176 let found = RegistryExamination {
177 tenants,
178 removed_since_master,
179 oldest_removed_since_master,
180 };
181 Ok(found)
182 }
183}
184
185fn earliest(a: Option<OffsetDateTime>, b: Option<OffsetDateTime>) -> Option<OffsetDateTime> {
187 match (a, b) {
188 (Some(a), Some(b)) => Some(a.min(b)),
189 (a, b) => a.or(b),
190 }
191}
192
193#[cfg(test)]
194mod examine_tests {
195 use time::macros::datetime;
196
197 use super::super::tests::fixture;
198 use super::*;
199 use crate::audit::Actor;
200 use crate::store::Caller;
201 use crate::store::testing::vcon;
202
203 const CALLER: Caller = Caller {
204 key: crate::ids::KeyId([7; 8]),
205 };
206
207 #[test]
208 fn each_tenant_is_examined_under_its_own_period_or_the_default() {
209 let f = fixture();
210 let own = f
211 .tenants
212 .create(Actor::Cli, "own", Some(Retention::Days(1)))
213 .expect("made");
214 let default = f.tenants.create(Actor::Cli, "default", None).expect("made");
215 for id in [own, default] {
216 let store = f.tenants.store(id).expect("store");
217 let (found, body) = vcon("01a07da8-c2bb-83e5-b9a2-279e0d16bc46", "kept");
218 store.create(&CALLER, &found, &body).expect("stored");
219 store
220 .delete(&CALLER, found.uuid, Retention::Years(7))
221 .expect("deleted");
222 }
223 let found = f
224 .tenants
225 .examine(Retention::Years(7), datetime!(2026-10-30 12:00 UTC))
226 .expect("examined");
227 let by_name = |name: &str| found.tenants.iter().find(|t| t.name == name).expect(name);
228 let own = by_name("own");
229 assert_eq!(
230 own.store.as_ref().expect("opens").due,
231 1,
232 "a day has passed"
233 );
234 assert!(!own.rotation_stopped);
235 assert_eq!(by_name("default").store.as_ref().expect("opens").due, 0);
236 assert_eq!(found.tenants.len(), 2);
237 }
238
239 #[test]
240 fn a_rotation_stopped_part_way_is_reported_and_a_catalog_that_does_not_open_too() {
241 let f = fixture();
242 let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
243 *f.tenants.hooks.rekey_stop.lock().expect("hook") =
244 Some(super::super::rekey::RekeyStep::Reencrypted);
245 assert_eq!(f.tenants.rekey_tenant(Actor::Cli, id), Err(TenantError::Io));
246 let found = f
247 .tenants
248 .examine(Retention::Years(7), datetime!(2026-10-23 12:00 UTC))
249 .expect("examined");
250 let acme = &found.tenants[0];
251 assert!(acme.rotation_stopped);
252 assert!(
253 matches!(acme.store, Err(OpenFailure::WrongKey)),
254 "its catalog is under the pending key: {:?}",
255 acme.store
256 );
257 }
258
259 #[test]
264 fn a_catalog_the_process_may_not_open_is_an_access_failure_not_a_key_one() {
265 use std::os::unix::fs::PermissionsExt;
266 let f = fixture();
267 let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
269 let dir = f.dir.path().join("tenants").join(id.to_string());
270 let mode =
271 |m| std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(m)).expect("chmod");
272 mode(0o500);
273 let order = f.tenants.key_order();
274 let found = f
275 .tenants
276 .examine(Retention::Years(7), datetime!(2026-10-23 12:00 UTC));
277 mode(0o700);
278 let order = order.expect("read");
279 assert_eq!(order.data_under_other_keys, 0, "{order:?}");
280 assert_eq!(order.catalogs_unopened, 1, "{order:?}");
281 assert_eq!(
282 order.unopened_error.as_deref(),
283 Some("Permission denied (os error 13)")
284 );
285 assert!(!order.is_sound());
286 match &found.expect("examined").tenants[0].store {
287 Err(OpenFailure::Access { path, error }) => {
288 assert_eq!(path, &dir);
289 assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
290 }
291 other => panic!("{other:?}"),
292 }
293 assert!(
294 f.tenants.key_order().expect("read").is_sound(),
295 "once allowed"
296 );
297 }
298
299 mod key_order {
300 use super::*;
301 use crate::envelope::{self, Context, Key};
302 use crate::tenants::rekey::RekeyStep;
303
304 fn with_vcons(f: &super::super::super::tests::Fixture) -> TenantId {
305 let id = f.tenants.create(Actor::Cli, "acme", None).expect("made");
306 let store = f.tenants.store(id).expect("store");
307 for n in 0..3 {
308 let (found, body) = vcon(&format!("01a07da8-c2bb-83e5-b9a2-279e0d16bc4{n}"), "k");
309 store.create(&CALLER, &found, &body).expect("stored");
310 }
311 id
312 }
313
314 #[test]
315 fn a_sound_registry_is_in_order() {
316 let f = fixture();
317 with_vcons(&f);
318 assert!(f.tenants.key_order().expect("read").is_sound());
319 }
320
321 #[test]
322 fn a_rotation_stopped_at_any_step_is_out_of_order() {
323 for (step, data) in [
324 (RekeyStep::Pending, 0),
325 (RekeyStep::Batch, 1),
326 (RekeyStep::Rewrapped, 1),
327 (RekeyStep::Reencrypted, 1),
328 ] {
329 let f = fixture();
330 let id = with_vcons(&f);
331 *f.tenants.hooks.rekey_stop.lock().expect("hook") = Some(step);
332 assert!(f.tenants.rekey_in_batches(Actor::Cli, id, 2).is_err());
333 assert_eq!(
334 f.tenants.key_order().expect("read"),
335 KeyOrder {
336 wrapped_elsewhere: 0,
337 rotations_stopped: 1,
338 data_under_other_keys: data,
339 ..KeyOrder::default()
340 },
341 "{step:?}"
342 );
343 f.tenants.rekey_tenant(Actor::Cli, id).expect("finished");
344 assert!(f.tenants.key_order().expect("read").is_sound(), "{step:?}");
345 }
346 }
347
348 #[test]
349 fn a_tenant_key_wrapped_under_another_key_is_out_of_order() {
350 let f = fixture();
351 let id = with_vcons(&f);
352 let other = Key::generate().expect("key");
353 let foreign =
354 envelope::wrap(&other, &Context::Tenant { tenant: id }, &f.tenant_key(id));
355 f.conn()
356 .execute(
357 "UPDATE tenant SET wrapped_tenant_key = ?1 WHERE id = ?2",
358 rusqlite::params![foreign, id.0],
359 )
360 .expect("rewritten");
361 f.tenants.close(id);
362 assert_eq!(f.tenants.key_order().expect("read").wrapped_elsewhere, 1);
363 }
364 }
365
366 #[test]
367 fn the_rotation_backlog_counts_purges_and_removals_from_the_logs() {
368 let f = fixture();
369 let purging = f
370 .tenants
371 .create(Actor::Cli, "purging", Some(Retention::Days(0)))
372 .expect("made");
373 f.tenants.create(Actor::Cli, "quiet", None).expect("made");
374 let gone = f.tenants.create(Actor::Cli, "gone", None).expect("made");
375 let store = f.tenants.store(purging).expect("store");
376 for (n, at) in [
377 (0, datetime!(2026-10-24 10:00 UTC)),
378 (1, datetime!(2026-10-24 11:00 UTC)),
379 ] {
380 f.clock.set(at);
381 let (found, body) = vcon(&format!("01a07da8-c2bb-83e5-b9a2-279e0d16bc4{n}"), "k");
382 store.create(&CALLER, &found, &body).expect("stored");
383 store
384 .delete(&CALLER, found.uuid, Retention::Days(0))
385 .expect("deleted and purged");
386 }
387 drop(store);
388 f.clock.set(datetime!(2026-10-24 12:00 UTC));
389 f.tenants
390 .remove(Actor::Cli, gone, crate::tenants::Removal::Empty)
391 .expect("removed");
392 assert_eq!(
393 f.tenants.rotation_backlog().expect("read"),
394 RotationBacklog {
395 tenants_with_purges: 1,
396 purges: 2,
397 oldest_purge: Some(datetime!(2026-10-24 10:00 UTC)),
398 removals: 1,
399 oldest_removal: Some(datetime!(2026-10-24 12:00 UTC)),
400 }
401 );
402 }
403
404 #[test]
405 fn removals_since_the_master_key_was_rotated_are_counted() {
406 let f = fixture();
407 let gone = f.tenants.create(Actor::Cli, "gone", None).expect("made");
408 f.tenants.create(Actor::Cli, "kept", None).expect("made");
409 f.clock.set(datetime!(2026-10-24 08:00 UTC));
410 f.tenants
411 .remove(Actor::Cli, gone, crate::tenants::Removal::Empty)
412 .expect("removed");
413 f.conn()
415 .execute_batch(
416 "INSERT INTO audit (at, actor, action, target, version, status, count)
417 VALUES ('2026-10-01T00:00:00Z', 'cli', 'tenant_remove', NULL, NULL, 204, 1),
418 ('2026-10-24T09:00:00Z', 'cli', 'tenant_remove', NULL, NULL, 409, 1);",
419 )
420 .expect("older records");
421 let found = f
422 .tenants
423 .examine(Retention::Years(7), datetime!(2026-10-25 12:00 UTC))
424 .expect("examined");
425 assert_eq!(found.removed_since_master, 1);
426 assert_eq!(
427 found.oldest_removed_since_master,
428 Some(datetime!(2026-10-24 08:00 UTC))
429 );
430 assert_eq!(found.tenants.len(), 1);
431 }
432}