Skip to main content

vcon_server/store/
legal.rs

1//! Legal access: reading a deleted or replaced vCon, and listing what is
2//! kept of a uuid, for a legal key. Access decides who holds a legal key;
3//! these calls trust their caller and record each answer.
4
5use time::OffsetDateTime;
6use uuid::Uuid;
7
8use super::{Caller, Store, StoreError, Stored, Tx, commit, version_row};
9use crate::audit::{Action, Actor, Entry};
10use crate::clock::from_nanos;
11use crate::ids::VersionId;
12
13/// A version as a legal read returns it, and whether its uuid is deleted.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Legal {
16    pub stored: Stored,
17    /// The uuid is deleted; otherwise it is live.
18    pub deleted: bool,
19}
20
21/// One kept version of a uuid, as the legal version list shows it.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct VersionInfo {
24    pub version: VersionId,
25    pub stored_at: OffsetDateTime,
26    /// When it was replaced, or its uuid deleted; None while current.
27    pub hidden_at: Option<OffsetDateTime>,
28    /// The uuid's current version.
29    pub current: bool,
30}
31
32/// What a legal call finds of a uuid.
33enum Standing {
34    Purged,
35    Absent,
36    Live(VersionId),
37    Deleted,
38}
39
40fn standing(tx: &mut Tx, uuid: Uuid) -> Result<Standing, StoreError> {
41    if tx.tombstoned(uuid)? {
42        return Ok(Standing::Purged);
43    }
44    match tx.vcon_row(uuid)? {
45        None => Ok(Standing::Absent),
46        Some(row) if row.deleted_at.is_some() => Ok(Standing::Deleted),
47        Some(row) => row.current.map(Standing::Live).ok_or(StoreError::Damaged),
48    }
49}
50
51/// The status a legal call records for `answer`, `ok` when it succeeded.
52fn status<T>(answer: &Result<T, StoreError>, ok: u16) -> u16 {
53    match answer {
54        Ok(_) => ok,
55        Err(StoreError::Gone) => 410,
56        Err(StoreError::NotFound) => 404,
57        Err(StoreError::NotDeleted) => 409,
58        Err(_) => 500,
59    }
60}
61
62/// The latest kept version of a deleted uuid. A deleted uuid left with no
63/// version is purged, with a tombstone, so a deleted row always keeps one.
64fn latest(tx: &mut Tx, uuid: Uuid) -> Result<VersionId, StoreError> {
65    tx.latest(uuid)?.ok_or(StoreError::Damaged)
66}
67
68impl Store {
69    /// Reads `version` of `uuid`, or with None its current version, or its
70    /// latest kept one when the uuid is deleted. A version purged, never
71    /// stored or of another uuid is NotFound; a purged uuid is Gone. The
72    /// whole read, object included, is one immediate transaction, so a
73    /// purge cannot remove what is being read. Each answer is recorded.
74    pub fn legal_get(
75        &self,
76        who: &Caller,
77        uuid: Uuid,
78        version: Option<VersionId>,
79    ) -> Result<Legal, StoreError> {
80        let mut tx = self.catalog.immediate()?;
81        let answer = self.legal_read(&mut tx, uuid, version);
82        let entry = Entry {
83            actor: Actor::Key(who.key),
84            action: Action::LegalRead,
85            target: Some(uuid.to_string()),
86            version: answer.as_ref().ok().map(|read| read.stored.version),
87            status: status(&answer, 200),
88        };
89        self.record(&mut tx, &entry)?;
90        commit(tx)?;
91        answer
92    }
93
94    fn legal_read(
95        &self,
96        tx: &mut Tx,
97        uuid: Uuid,
98        asked: Option<VersionId>,
99    ) -> Result<Legal, StoreError> {
100        let (current, deleted) = match standing(tx, uuid)? {
101            Standing::Purged => return Err(StoreError::Gone),
102            Standing::Absent => return Err(StoreError::NotFound),
103            Standing::Live(current) => (Some(current), false),
104            Standing::Deleted => (None, true),
105        };
106        let version = match (asked, current) {
107            (Some(asked), _) if tx.has_version(asked, uuid)? => asked,
108            (Some(_), _) => return Err(StoreError::NotFound),
109            (None, Some(current)) => current,
110            (None, None) => latest(tx, uuid)?,
111        };
112        let row = version_row(tx, version)?;
113        let body = self.open_version(uuid, version, &row.wrapped)?;
114        Ok(Legal {
115            stored: Stored {
116                version,
117                form: row.form,
118                gzip: row.gzip,
119                body,
120            },
121            deleted,
122        })
123    }
124
125    /// Every kept version of `uuid`, oldest first, with its times. A purged
126    /// uuid is Gone and one never stored NotFound. Each answer is recorded.
127    pub fn legal_versions(&self, who: &Caller, uuid: Uuid) -> Result<Vec<VersionInfo>, StoreError> {
128        let mut tx = self.catalog.immediate()?;
129        let answer = versions(&mut tx, uuid);
130        let entry = Entry {
131            actor: Actor::Key(who.key),
132            action: Action::LegalVersions,
133            target: Some(uuid.to_string()),
134            version: None,
135            status: status(&answer, 200),
136        };
137        self.record(&mut tx, &entry)?;
138        commit(tx)?;
139        answer
140    }
141
142    /// Makes the latest kept version of a deleted `uuid` current again: its
143    /// hidden time and the uuid's deleted time are cleared, so no sweep
144    /// purges it. Older hidden versions stay hidden. A live uuid is
145    /// NotDeleted, a purged one Gone, one never stored NotFound. Recorded.
146    pub fn restore(&self, who: &Caller, uuid: Uuid) -> Result<VersionId, StoreError> {
147        let mut tx = self.catalog.immediate()?;
148        // The writes go in a part of their own, so a failure undoes them
149        // and still leaves the record.
150        let answer = in_part(&mut tx, |tx| restored(tx, uuid))?;
151        let entry = Entry {
152            actor: Actor::Key(who.key),
153            action: Action::Restore,
154            target: Some(uuid.to_string()),
155            version: answer.as_ref().ok().copied(),
156            status: status(&answer, 200),
157        };
158        self.record(&mut tx, &entry)?;
159        commit(tx)?;
160        answer
161    }
162
163    /// Turns the legal hold on `uuid` on or off. A held uuid is never
164    /// purged; a delete still hides it. A purged uuid is Gone, one never
165    /// stored NotFound. Recorded.
166    pub fn set_hold(&self, who: &Caller, uuid: Uuid, on: bool) -> Result<(), StoreError> {
167        let mut tx = self.catalog.immediate()?;
168        let answer = in_part(&mut tx, |tx| held(tx, uuid, on))?;
169        let entry = Entry {
170            actor: Actor::Key(who.key),
171            action: if on { Action::Hold } else { Action::Release },
172            target: Some(uuid.to_string()),
173            version: None,
174            status: status(&answer, 204),
175        };
176        self.record(&mut tx, &entry)?;
177        commit(tx)?;
178        answer
179    }
180}
181
182/// Runs `writes` in a part of `tx` of its own: kept if they succeed, undone
183/// if they fail. The outer error is the part's own failure; the inner one
184/// is `writes`'s answer.
185fn in_part<T>(
186    tx: &mut Tx,
187    writes: impl FnOnce(&mut Tx) -> Result<T, StoreError>,
188) -> Result<Result<T, StoreError>, StoreError> {
189    tx.begin_part()?;
190    let answer = writes(tx);
191    if answer.is_ok() {
192        tx.release_part()?;
193    } else {
194        tx.rollback_part()?;
195    }
196    Ok(answer)
197}
198
199fn restored(tx: &mut Tx, uuid: Uuid) -> Result<VersionId, StoreError> {
200    match standing(tx, uuid)? {
201        Standing::Purged => return Err(StoreError::Gone),
202        Standing::Absent => return Err(StoreError::NotFound),
203        Standing::Live(_) => return Err(StoreError::NotDeleted),
204        Standing::Deleted => {}
205    }
206    let version = latest(tx, uuid)?;
207    tx.unhide(version)?;
208    tx.undelete(uuid, version)?;
209    Ok(version)
210}
211
212fn held(tx: &mut Tx, uuid: Uuid, on: bool) -> Result<(), StoreError> {
213    match standing(tx, uuid)? {
214        Standing::Purged => Err(StoreError::Gone),
215        Standing::Absent => Err(StoreError::NotFound),
216        Standing::Live(_) | Standing::Deleted => tx.set_hold(uuid, on),
217    }
218}
219
220fn versions(tx: &mut Tx, uuid: Uuid) -> Result<Vec<VersionInfo>, StoreError> {
221    let current = match standing(tx, uuid)? {
222        Standing::Purged => return Err(StoreError::Gone),
223        Standing::Absent => return Err(StoreError::NotFound),
224        Standing::Live(current) => Some(current),
225        Standing::Deleted => None,
226    };
227    let time = |nanos: i64| from_nanos(nanos).ok_or(StoreError::Damaged);
228    tx.kept(uuid)?
229        .into_iter()
230        .map(|row| {
231            Ok(VersionInfo {
232                version: row.version,
233                stored_at: time(row.stored_at)?,
234                hidden_at: row.hidden_at.map(time).transpose()?,
235                current: Some(row.version) == current,
236            })
237        })
238        .collect()
239}
240
241#[cfg(test)]
242mod legal_tests {
243    use serde_json::Value;
244    use time::Duration;
245    use time::macros::datetime;
246    use uuid::Uuid;
247
248    use super::super::testing::vcon;
249    use super::super::tests::{Fixture, fixture};
250    use super::*;
251    use crate::clock::Clock;
252    use crate::fs::Fs;
253    use crate::ids::VersionId;
254    use crate::retention::Retention;
255    use crate::store::{Created, StoreError, Stored};
256
257    const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
258    const OTHER: &str = "0199e5c6-0d7a-7c11-8000-000000000001";
259
260    /// Stores a vCon of `uuid` and returns its version and body.
261    fn stored(f: &Fixture, uuid: &str, subject: &str) -> (VersionId, Vec<u8>) {
262        let (found, body) = vcon(uuid, subject);
263        let Ok(Created::New(version)) = f.store.create(&f.caller, &found, &body) else {
264            panic!("a new vCon")
265        };
266        (version, body)
267    }
268
269    /// Replaces the current version of `UUID` and returns the new one.
270    fn replaced(f: &Fixture, current: VersionId, subject: &str) -> (VersionId, Vec<u8>) {
271        let (found, body) = vcon(UUID, subject);
272        let version = f
273            .store
274            .replace(&f.caller, current, &found, &body)
275            .expect("replaced");
276        (version, body)
277    }
278
279    fn uuid() -> Uuid {
280        UUID.parse().expect("uuid")
281    }
282
283    fn later(f: &Fixture, by: Duration) {
284        f.clock.set(f.clock.now() + by);
285    }
286
287    /// The last record of `action`.
288    fn last(f: &Fixture, action: &str) -> Value {
289        f.store
290            .records()
291            .into_iter()
292            .rfind(|r| r["action"] == action)
293            .expect("a record")
294    }
295
296    #[test]
297    fn a_deleted_vcon_reads_back_byte_for_byte_and_says_it_is_deleted() {
298        let f = fixture();
299        let (version, body) = stored(&f, UUID, "first");
300        f.store
301            .delete(&f.caller, uuid(), Retention::Years(7))
302            .expect("deleted");
303
304        let read = f.store.legal_get(&f.caller, uuid(), None).expect("read");
305
306        assert_eq!(
307            read,
308            Legal {
309                stored: Stored {
310                    version,
311                    form: vcon::Form::Unsigned,
312                    gzip: false,
313                    body,
314                },
315                deleted: true,
316            }
317        );
318        let record = last(&f, "legal_read");
319        assert_eq!(record["status"], 200);
320        assert_eq!(record["target"], UUID);
321        assert_eq!(record["version"], version.to_string());
322    }
323
324    #[test]
325    fn a_deleted_vcon_reads_its_latest_kept_version() {
326        let f = fixture();
327        let (first, _) = stored(&f, UUID, "first");
328        later(&f, Duration::hours(1));
329        let (second, body) = replaced(&f, first, "second");
330        f.store
331            .delete(&f.caller, uuid(), Retention::Years(7))
332            .expect("deleted");
333
334        let read = f.store.legal_get(&f.caller, uuid(), None).expect("read");
335
336        assert_eq!((read.stored.version, read.stored.body), (second, body));
337    }
338
339    #[test]
340    fn a_live_vcon_reads_its_current_version() {
341        let f = fixture();
342        let (first, _) = stored(&f, UUID, "first");
343        let (second, body) = replaced(&f, first, "second");
344
345        let read = f.store.legal_get(&f.caller, uuid(), None).expect("read");
346
347        assert_eq!((read.stored.version, read.stored.body), (second, body));
348        assert!(!read.deleted);
349    }
350
351    #[test]
352    fn a_named_kept_version_reads_back_until_it_is_purged() {
353        let f = fixture();
354        let (first, body) = stored(&f, UUID, "first");
355        replaced(&f, first, "second");
356
357        let read = f
358            .store
359            .legal_get(&f.caller, uuid(), Some(first))
360            .expect("read");
361        assert_eq!((read.stored.version, read.stored.body), (first, body));
362        assert!(!read.deleted, "the uuid is live");
363
364        f.store.purge_due(Retention::Days(0)).expect("purged");
365        assert_eq!(
366            f.store.legal_get(&f.caller, uuid(), Some(first)),
367            Err(StoreError::NotFound)
368        );
369        assert_eq!(last(&f, "legal_read")["status"], 404);
370    }
371
372    #[test]
373    fn a_version_of_another_uuid_is_not_found() {
374        let f = fixture();
375        stored(&f, UUID, "first");
376        let (theirs, _) = stored(&f, OTHER, "other");
377
378        assert_eq!(
379            f.store.legal_get(&f.caller, uuid(), Some(theirs)),
380            Err(StoreError::NotFound)
381        );
382    }
383
384    #[test]
385    fn a_purged_uuid_is_gone_and_one_never_stored_is_not_found() {
386        let f = fixture();
387        stored(&f, UUID, "first");
388        f.store
389            .delete(&f.caller, uuid(), Retention::Days(0))
390            .expect("deleted and purged");
391
392        assert_eq!(
393            f.store.legal_get(&f.caller, uuid(), None),
394            Err(StoreError::Gone)
395        );
396        assert_eq!(last(&f, "legal_read")["status"], 410);
397        assert_eq!(
398            f.store.legal_versions(&f.caller, uuid()),
399            Err(StoreError::Gone)
400        );
401        assert_eq!(last(&f, "legal_versions")["status"], 410);
402
403        let never: Uuid = OTHER.parse().expect("uuid");
404        assert_eq!(
405            f.store.legal_get(&f.caller, never, None),
406            Err(StoreError::NotFound)
407        );
408        assert_eq!(
409            f.store.legal_versions(&f.caller, never),
410            Err(StoreError::NotFound)
411        );
412        assert_eq!(last(&f, "legal_versions")["status"], 404);
413    }
414
415    #[test]
416    fn the_version_list_gives_every_kept_version_with_its_times() {
417        let f = fixture();
418        let (first, _) = stored(&f, UUID, "first");
419        f.clock.set(datetime!(2026-10-23 13:00 UTC));
420        let (second, _) = replaced(&f, first, "second");
421
422        let live = f.store.legal_versions(&f.caller, uuid()).expect("list");
423        assert_eq!(
424            live,
425            [
426                VersionInfo {
427                    version: first,
428                    stored_at: datetime!(2026-10-23 12:00 UTC),
429                    hidden_at: Some(datetime!(2026-10-23 13:00 UTC)),
430                    current: false,
431                },
432                VersionInfo {
433                    version: second,
434                    stored_at: datetime!(2026-10-23 13:00 UTC),
435                    hidden_at: None,
436                    current: true,
437                },
438            ]
439        );
440        let record = last(&f, "legal_versions");
441        assert_eq!(
442            (record["status"].clone(), record["target"].clone()),
443            (200.into(), UUID.into())
444        );
445
446        f.clock.set(datetime!(2026-10-23 14:00 UTC));
447        f.store
448            .delete(&f.caller, uuid(), Retention::Years(7))
449            .expect("deleted");
450        let deleted = f.store.legal_versions(&f.caller, uuid()).expect("list");
451        assert_eq!(deleted.len(), 2);
452        assert_eq!(deleted[1].hidden_at, Some(datetime!(2026-10-23 14:00 UTC)));
453        assert!(deleted.iter().all(|v| !v.current), "nothing is current");
454    }
455
456    #[test]
457    fn a_damaged_kept_version_is_a_500_never_a_body() {
458        let f = fixture();
459        let (version, _) = stored(&f, UUID, "first");
460        f.store
461            .delete(&f.caller, uuid(), Retention::Years(7))
462            .expect("deleted");
463        let file = f.store.objects_dir.join(version.to_string());
464        let mut blob = f.fs.read(&file).expect("file");
465        let last_byte = blob.len() - 1;
466        blob[last_byte] ^= 1;
467        f.fs.remove(&file).expect("remove");
468        f.fs.write_new(&file, &blob).expect("write");
469
470        assert_eq!(
471            f.store.legal_get(&f.caller, uuid(), None),
472            Err(StoreError::Damaged)
473        );
474        assert_eq!(last(&f, "legal_read")["status"], 500);
475    }
476
477    #[test]
478    fn a_restored_vcon_is_current_again_and_a_sweep_leaves_it() {
479        let f = fixture();
480        let (first, _) = stored(&f, UUID, "first");
481        later(&f, Duration::hours(1));
482        let (second, body) = replaced(&f, first, "second");
483        f.store
484            .delete(&f.caller, uuid(), Retention::Years(7))
485            .expect("deleted");
486        later(&f, Duration::days(2));
487
488        assert_eq!(f.store.restore(&f.caller, uuid()), Ok(second));
489        let record = last(&f, "restore");
490        assert_eq!(record["status"], 200);
491        assert_eq!(record["version"], second.to_string());
492
493        let read = f.store.get(&f.caller, uuid()).expect("live again");
494        assert_eq!((read.version, read.body), (second, body));
495        f.store.purge_due(Retention::Days(1)).expect("swept");
496        assert_eq!(
497            f.store.get(&f.caller, uuid()).map(|s| s.version),
498            Ok(second),
499            "the sweep leaves it current"
500        );
501        let kept = f.store.legal_versions(&f.caller, uuid()).expect("list");
502        assert_eq!(
503            kept.iter().map(|v| v.version).collect::<Vec<_>>(),
504            [second],
505            "the older hidden version was due and is gone"
506        );
507    }
508
509    #[test]
510    fn restore_refuses_a_live_a_purged_and_a_never_stored_uuid() {
511        let f = fixture();
512        stored(&f, UUID, "first");
513        assert_eq!(
514            f.store.restore(&f.caller, uuid()),
515            Err(StoreError::NotDeleted)
516        );
517        assert_eq!(last(&f, "restore")["status"], 409);
518
519        f.store
520            .delete(&f.caller, uuid(), Retention::Days(0))
521            .expect("deleted and purged");
522        assert_eq!(f.store.restore(&f.caller, uuid()), Err(StoreError::Gone));
523        assert_eq!(last(&f, "restore")["status"], 410);
524
525        let never: Uuid = OTHER.parse().expect("uuid");
526        assert_eq!(f.store.restore(&f.caller, never), Err(StoreError::NotFound));
527        assert_eq!(last(&f, "restore")["status"], 404);
528    }
529
530    #[test]
531    fn a_hold_blocks_the_purge_until_it_is_released() {
532        let f = fixture();
533        stored(&f, UUID, "first");
534        assert_eq!(f.store.set_hold(&f.caller, uuid(), true), Ok(()));
535        assert_eq!(last(&f, "hold")["status"], 204);
536        f.store
537            .delete(&f.caller, uuid(), Retention::Days(0))
538            .expect("deleted, but held");
539        f.store.purge_due(Retention::Days(0)).expect("swept");
540        assert!(
541            f.store.legal_get(&f.caller, uuid(), None).is_ok(),
542            "held, so kept"
543        );
544
545        assert_eq!(f.store.set_hold(&f.caller, uuid(), false), Ok(()));
546        f.store.purge_due(Retention::Days(0)).expect("swept");
547        assert_eq!(
548            f.store.legal_get(&f.caller, uuid(), None),
549            Err(StoreError::Gone),
550            "released, so purged"
551        );
552    }
553
554    /// A release is recorded as a release, so the log tells it from a hold.
555    #[test]
556    fn a_release_is_recorded_as_release_not_as_hold() {
557        let f = fixture();
558        stored(&f, UUID, "first");
559        assert_eq!(f.store.set_hold(&f.caller, uuid(), true), Ok(()));
560        assert_eq!(f.store.set_hold(&f.caller, uuid(), false), Ok(()));
561        let actions: Vec<_> = f
562            .store
563            .records()
564            .into_iter()
565            .filter(|r| r["target"] == UUID)
566            .map(|r| r["action"].as_str().expect("an action").to_owned())
567            .collect();
568        assert_eq!(actions, ["create", "hold", "release"]);
569        assert_eq!(last(&f, "release")["status"], 204);
570        let never: Uuid = OTHER.parse().expect("uuid");
571        assert_eq!(
572            f.store.set_hold(&f.caller, never, false),
573            Err(StoreError::NotFound)
574        );
575        assert_eq!(last(&f, "release")["status"], 404, "a refused release too");
576    }
577
578    #[test]
579    fn a_hold_refuses_a_purged_and_a_never_stored_uuid() {
580        let f = fixture();
581        stored(&f, UUID, "first");
582        f.store
583            .delete(&f.caller, uuid(), Retention::Days(0))
584            .expect("deleted and purged");
585        assert_eq!(
586            f.store.set_hold(&f.caller, uuid(), true),
587            Err(StoreError::Gone)
588        );
589        assert_eq!(last(&f, "hold")["status"], 410);
590        let never: Uuid = OTHER.parse().expect("uuid");
591        assert_eq!(
592            f.store.set_hold(&f.caller, never, true),
593            Err(StoreError::NotFound)
594        );
595        assert_eq!(last(&f, "hold")["status"], 404);
596    }
597
598    #[test]
599    fn a_restore_that_fails_partway_changes_nothing_and_is_recorded() {
600        let f = fixture();
601        stored(&f, UUID, "first");
602        f.store
603            .delete(&f.caller, uuid(), Retention::Years(7))
604            .expect("deleted");
605        f.store
606            .conn()
607            .execute_batch(
608                "CREATE TEMP TRIGGER no_restore BEFORE UPDATE OF current ON vcon
609                 BEGIN SELECT RAISE(ABORT, 'the disk has failed'); END;",
610            )
611            .expect("trigger");
612
613        assert!(f.store.restore(&f.caller, uuid()).is_err());
614
615        assert_eq!(last(&f, "restore")["status"], 500);
616        let kept = f.store.legal_versions(&f.caller, uuid()).expect("list");
617        assert!(
618            kept.iter().all(|v| v.hidden_at.is_some()),
619            "the version written first is hidden again"
620        );
621    }
622}