Skip to main content

vcon_server/
commands.rs

1//! The commands other than `serve`: tenants, keys and the audit logs. Each
2//! takes the data directory as its work shares it, opens the registry with
3//! recovery when it runs alone and without when it may run beside a server,
4//! and prints what the matching admin route answers: one JSON object a
5//! line.
6
7use std::fmt;
8use std::io::{self, Write};
9use std::sync::Arc;
10
11use crate::access::{Scope, Scopes};
12use crate::audit::Actor;
13use crate::audit::AuditError;
14use crate::clock::Clock;
15use crate::clock::SystemClock;
16use crate::config::Config;
17use crate::datadir::{self, LockError, Sharing, Work};
18use crate::envelope::{Key, KeyError};
19use crate::fs::OsFs;
20use crate::ids::{KeyId, TenantId};
21use crate::retention::Retention;
22use crate::tenants::{
23    self, BackupError, OpenFailure, Removal, TenantError, TenantExamination, Tenants,
24};
25use time::OffsetDateTime;
26use time::format_description::well_known::Rfc3339;
27
28/// A command, as written on the command line.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum Command {
31    TenantCreate {
32        name: String,
33        retention: Option<String>,
34    },
35    TenantList,
36    /// `retention` is a period, or `default` for the server's.
37    TenantSetRetention {
38        id: String,
39        retention: String,
40    },
41    TenantRemove {
42        id: String,
43        force: bool,
44        override_holds: bool,
45    },
46    KeyCreate {
47        owner: Owner,
48        label: String,
49    },
50    KeyList {
51        tenant: Option<String>,
52    },
53    KeyRevoke {
54        id: String,
55    },
56    AuditExport {
57        log: Log,
58        since: Option<i64>,
59    },
60    /// Rotates a tenant's key.
61    RekeyTenant {
62        id: String,
63    },
64    /// Rotates the master key to the one in `new_master`.
65    RekeyMaster {
66        new_master: std::path::PathBuf,
67    },
68    /// Examines every tenant and reports what needs attention.
69    Check,
70    /// Writes a consistent copy of the data directory to `to`, which must
71    /// not exist, beside a running server.
72    Backup {
73        to: std::path::PathBuf,
74    },
75}
76
77/// Whose key `key create` makes.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum Owner {
80    Tenant { id: String, scopes: Vec<String> },
81    Operator,
82}
83
84/// Which audit log `audit export` writes.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum Log {
87    Tenant(String),
88    Admin,
89}
90
91impl Command {
92    /// The work this command does in the data directory.
93    pub fn work(&self) -> Work {
94        match self {
95            Command::TenantCreate { .. } => Work::TenantCreate,
96            Command::TenantList => Work::TenantList,
97            Command::TenantSetRetention { .. } => Work::TenantSetRetention,
98            Command::TenantRemove { .. } => Work::TenantRemove,
99            Command::KeyCreate { .. } => Work::KeyCreate,
100            Command::KeyList { .. } => Work::KeyList,
101            Command::KeyRevoke { .. } => Work::KeyRevoke,
102            Command::AuditExport { .. } => Work::AuditExport,
103            Command::RekeyTenant { .. } => Work::RekeyTenant,
104            Command::RekeyMaster { .. } => Work::RekeyMaster,
105            Command::Check => Work::Check,
106            Command::Backup { .. } => Work::Backup,
107        }
108    }
109}
110
111/// Why a command did not complete.
112#[derive(Debug)]
113pub enum CommandError {
114    DataDir(LockError),
115    MasterKey(KeyError),
116    /// server.sqlite was made under another master key.
117    WrongMasterKey,
118    /// A period that is neither like 7y, 18m or 90d nor, where allowed,
119    /// `default`.
120    BadPeriod(String),
121    Tenant(TenantError),
122    /// No key has this id.
123    UnknownKey,
124    /// The output could not be written.
125    Output(io::Error),
126    /// The new master key is the one already in use.
127    SameMasterKey,
128    /// `check` found this many problems; its report says which.
129    CheckFound(usize),
130    Backup(BackupError),
131}
132
133impl fmt::Display for CommandError {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        match self {
136            CommandError::DataDir(e) => write!(f, "{e}"),
137            CommandError::MasterKey(e) => write!(f, "the master key cannot be read: {e:?}"),
138            CommandError::WrongMasterKey | CommandError::Tenant(TenantError::WrongKey) => {
139                f.write_str("the master key is not the one server.sqlite was made under")
140            }
141            CommandError::BadPeriod(text) => write!(
142                f,
143                "{text:?} is not a period such as 7y, 18m or 90d (or, for set-retention, default)"
144            ),
145            CommandError::UnknownKey => f.write_str("no key has this id"),
146            CommandError::Output(e) => write!(f, "the output could not be written: {e}"),
147            CommandError::SameMasterKey => {
148                f.write_str("the new master key is the one already in use")
149            }
150            CommandError::CheckFound(n) => write!(f, "check found {n} problem(s); see its report"),
151            CommandError::Backup(e) => f.write_str(match e {
152                BackupError::Exists => {
153                    "the backup's path already exists; a backup is written to a new directory"
154                }
155                BackupError::KeptChanging => {
156                    "tenants were removed or versions purged during each try; try again"
157                }
158                BackupError::Io => "the backup could not be written, or the data could not be read",
159                BackupError::AuditFailed => "the audit record could not be written",
160            }),
161            CommandError::Tenant(e) => f.write_str(match e {
162                TenantError::NotFound => "no tenant has this id",
163                TenantError::NameTaken => "another tenant has this name",
164                TenantError::BadName => {
165                    "a tenant name is 1 to 128 characters, with no control characters and no space at either end"
166                }
167                TenantError::HoldsVcons => "the tenant holds vCons; add --force to remove them too",
168                TenantError::UnderHold => {
169                    "a vCon of the tenant is under a legal hold; only --override-holds removes it"
170                }
171                TenantError::BadScopes => {
172                    "a tenant key's scopes are one or more of write, read, delete, legal and audit, each named once"
173                }
174                TenantError::LimitReached => {
175                    "the master key has wrapped all the tenant keys it may; run rekey master"
176                }
177                TenantError::AuditFailed => "the audit record could not be written",
178                TenantError::Closing => "the tenant is being removed",
179                TenantError::CliOnly => "only the command line may do this",
180                TenantError::WrongKey => unreachable!("matched above"),
181                TenantError::Io => "the data could not be read or written",
182            }),
183        }
184    }
185}
186
187/// Runs `command` against the data directory `config` names, writing its
188/// answer to `out`.
189pub fn run(config: &Config, command: &Command, out: &mut dyn Write) -> Result<(), CommandError> {
190    let sharing = command.work().sharing();
191    let _lock = datadir::lock(&config.data_dir, sharing).map_err(CommandError::DataDir)?;
192    let master = Key::load_master(&config.master_key).map_err(CommandError::MasterKey)?;
193    // A master rotation renames over server.sqlite, so nothing may hold it
194    // open: it runs before the registry is opened.
195    if let Command::RekeyMaster { new_master } = command {
196        return rekey_master(config, &master, new_master, out);
197    }
198    // A command beside a running server must not run start-up recovery: it
199    // would take the server's work in progress for a crash's leftovers. Nor
200    // does `check`, which reports what recovery would remove.
201    let open = if sharing == Sharing::Beside || matches!(command, Command::Check) {
202        Tenants::open_beside
203    } else {
204        Tenants::open
205    };
206    let tenants = open(
207        &config.data_dir,
208        master,
209        Arc::new(OsFs),
210        Arc::new(SystemClock),
211    )
212    .map_err(|e| match e {
213        TenantError::WrongKey => CommandError::WrongMasterKey,
214        other => CommandError::Tenant(other),
215    })?;
216    let who = Actor::Cli;
217    match command {
218        Command::TenantCreate { name, retention } => {
219            let retention = retention.as_deref().map(period).transpose()?;
220            let id = tenants.create(who, name, retention)?;
221            line(out, &serde_json::json!({ "id": id.to_string() }))
222        }
223        Command::TenantList => {
224            for tenant in tenants.list(who)? {
225                line(out, &tenant.to_json())?;
226            }
227            Ok(())
228        }
229        Command::TenantSetRetention { id, retention } => {
230            let retention = match retention.as_str() {
231                "default" => None,
232                text => Some(period(text)?),
233            };
234            let id = tenant_id(id)?;
235            tenants.set_retention(who, id, retention)?;
236            line(
237                out,
238                &serde_json::json!({
239                    "id": id.to_string(),
240                    "retention": retention.map(|r| r.to_string()),
241                }),
242            )
243        }
244        Command::TenantRemove {
245            id,
246            force,
247            override_holds,
248        } => {
249            let removal = match (force, override_holds) {
250                (_, true) => Removal::ForceOverridingHolds,
251                (true, false) => Removal::Force,
252                (false, false) => Removal::Empty,
253            };
254            Ok(tenants.remove(who, tenant_id(id)?, removal)?)
255        }
256        Command::KeyCreate { owner, label } => {
257            let (tenant, scopes) = match owner {
258                Owner::Tenant { id, scopes } => (
259                    Some(tenant_id(id)?),
260                    Scopes::named(scopes).ok_or(CommandError::Tenant(TenantError::BadScopes))?,
261                ),
262                Owner::Operator => (None, Scopes::of(&[Scope::Admin])),
263            };
264            let made = tenants.create_key(who, tenant, scopes, label)?;
265            line(
266                out,
267                &serde_json::json!({ "id": made.id.to_string(), "token": made.token }),
268            )
269        }
270        Command::KeyList { tenant } => {
271            let tenant = tenant.as_deref().map(tenant_id).transpose()?;
272            for key in tenants.list_keys(who, tenant)? {
273                let shown = key.to_json().ok_or(CommandError::Tenant(TenantError::Io))?;
274                line(out, &shown)?;
275            }
276            Ok(())
277        }
278        Command::KeyRevoke { id } => {
279            let id = KeyId::parse(id).ok_or(CommandError::UnknownKey)?;
280            tenants.revoke_key(who, id).map_err(|e| match e {
281                TenantError::NotFound => CommandError::UnknownKey,
282                other => CommandError::Tenant(other),
283            })
284        }
285        Command::RekeyTenant { id } => Ok(tenants.rekey_tenant(who, tenant_id(id)?)?),
286        Command::RekeyMaster { .. } => unreachable!("rotated before the registry is opened"),
287        Command::Check => check(config, &tenants, out),
288        Command::Backup { to } => {
289            let done = tenants.backup(who, to).map_err(CommandError::Backup)?;
290            line(
291                out,
292                &serde_json::json!({
293                    "tenants": done.tenants,
294                    "versions": done.versions,
295                    "tries": done.tries,
296                }),
297            )
298        }
299        Command::AuditExport { log, since } => {
300            let exported = match log {
301                Log::Admin => tenants.export_admin(*since, out),
302                Log::Tenant(id) => tenants.store(tenant_id(id)?)?.export(*since, out),
303            };
304            exported.map_err(|e| match e {
305                AuditError::Write(e) => CommandError::Output(e),
306                AuditError::Sqlite(_) => CommandError::Tenant(TenantError::Io),
307            })
308        }
309    }
310}
311
312impl From<TenantError> for CommandError {
313    fn from(e: TenantError) -> CommandError {
314        CommandError::Tenant(e)
315    }
316}
317
318/// Rotates the master key and says what the operator does next.
319fn rekey_master(
320    config: &Config,
321    master: &Key,
322    new_master: &std::path::Path,
323    out: &mut dyn Write,
324) -> Result<(), CommandError> {
325    let new = Key::load_master(new_master).map_err(CommandError::MasterKey)?;
326    if new.id() == master.id() {
327        return Err(CommandError::SameMasterKey);
328    }
329    tenants::rekey_master(&config.data_dir, master, &new, SystemClock.now()).map_err(
330        |e| match e {
331            TenantError::WrongKey => CommandError::WrongMasterKey,
332            other => CommandError::Tenant(other),
333        },
334    )?;
335    line(
336        out,
337        &serde_json::json!({
338            "master_key_id": new.id().to_string(),
339            "next": "point master_key in the configuration at the new key file, start the server, then destroy every copy of the old master key",
340        }),
341    )
342}
343
344/// Examines every tenant, prints a line for each and a summary, and fails
345/// when anything needs the operator.
346fn check(config: &Config, tenants: &Tenants, out: &mut dyn Write) -> Result<(), CommandError> {
347    let now = SystemClock.now();
348    let found = tenants.examine(config.retention_default, now)?;
349    let time = |at: Option<OffsetDateTime>| at.and_then(|at| at.format(&Rfc3339).ok());
350    let mut total = 0;
351    for tenant in &found.tenants {
352        let named = problems(tenant, config.purge_interval, now);
353        total += named.len();
354        let store = tenant.store.as_ref().ok();
355        line(
356            out,
357            &serde_json::json!({
358                "tenant": tenant.id.to_string(),
359                "name": tenant.name,
360                "versions": store.map(|s| s.versions),
361                "damaged": store.map(|s| s.damaged.iter().map(ToString::to_string).collect::<Vec<_>>()),
362                "leftovers": store.map(|s| &s.leftovers),
363                "past_due": store.map(|s| s.due),
364                "oldest_past_due": time(store.and_then(|s| s.oldest_due_since)),
365                "purges_awaiting_rotation": store.map(|s| s.purges_since_key),
366                "oldest_purge_awaiting_rotation": time(store.and_then(|s| s.oldest_purge_since_key)),
367                "rotation_stopped": tenant.rotation_stopped,
368                "problems": named,
369            }),
370        )?;
371    }
372    line(
373        out,
374        &serde_json::json!({
375            "tenants": found.tenants.len(),
376            "removed_tenants_awaiting_master_rotation": found.removed_since_master,
377            "oldest_removal_awaiting_master_rotation": time(found.oldest_removed_since_master),
378            "problems": total,
379        }),
380    )?;
381    match total {
382        0 => Ok(()),
383        n => Err(CommandError::CheckFound(n)),
384    }
385}
386
387/// What in one tenant's examination needs the operator. A stalled sweep is
388/// one whose oldest due version has waited longer than `purge_interval`;
389/// a shorter wait is the next sweep's work. Leftover files and work waiting
390/// for rotation are reported but are not problems.
391pub fn problems(
392    tenant: &TenantExamination,
393    purge_interval: std::time::Duration,
394    now: OffsetDateTime,
395) -> Vec<String> {
396    let mut found = Vec::new();
397    if tenant.rotation_stopped {
398        found.push("a key rotation was stopped part way; run rekey tenant again".to_owned());
399    }
400    match &tenant.store {
401        Err(OpenFailure::WrongKey) => {
402            found.push("its catalog does not open under its tenant key".to_owned());
403        }
404        Err(OpenFailure::Access { path, error }) => {
405            found.push(format!(
406                "its catalog cannot be opened: {}: {error}",
407                path.display()
408            ));
409        }
410        Err(OpenFailure::Refused(e)) => {
411            found.push(format!("its catalog cannot be opened: {e:?}"));
412        }
413        Ok(store) => {
414            for version in &store.damaged {
415                found.push(format!("version {version} is missing or does not open"));
416            }
417            if let Some(since) = store.oldest_due_since
418                && now - since > purge_interval
419            {
420                found.push(format!(
421                    "{} version(s) due for purge, the oldest longer than purge_interval: the sweep is not keeping up",
422                    store.due
423                ));
424            }
425        }
426    }
427    found
428}
429
430/// A period as a command line writes it.
431fn period(text: &str) -> Result<Retention, CommandError> {
432    Retention::parse(text).ok_or_else(|| CommandError::BadPeriod(text.to_owned()))
433}
434
435/// A tenant id as a command line writes it; one that does not parse names
436/// no tenant.
437fn tenant_id(text: &str) -> Result<TenantId, CommandError> {
438    TenantId::parse(text).ok_or(CommandError::Tenant(TenantError::NotFound))
439}
440
441/// Writes one JSON object as a line.
442fn line(out: &mut dyn Write, value: &serde_json::Value) -> Result<(), CommandError> {
443    writeln!(out, "{value}").map_err(CommandError::Output)
444}
445
446#[cfg(test)]
447mod tests {
448    use serde_json::{Value, json};
449
450    use super::*;
451    use crate::store::Caller;
452    use crate::store::testing::vcon;
453
454    const UUID: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
455
456    struct Setup {
457        _dir: tempfile::TempDir,
458        config: Config,
459    }
460
461    fn setup() -> Setup {
462        let dir = tempfile::tempdir().expect("temp dir");
463        let data = dir.path().join("data");
464        std::fs::create_dir(&data).expect("data dir");
465        let master = dir.path().join("master.key");
466        std::fs::write(&master, [0x5a; 32]).expect("master key file");
467        let config = crate::config::parse(&format!(
468            "listen = \"127.0.0.1:1\"\ndata_dir = {data:?}\nmaster_key = {master:?}\n\
469             [tls]\ncert = \"c\"\nkey = \"k\"\n"
470        ))
471        .expect("a configuration");
472        Setup { _dir: dir, config }
473    }
474
475    impl Setup {
476        /// Runs `command` and parses each line it prints.
477        fn run(&self, command: Command) -> Result<Vec<Value>, CommandError> {
478            let mut out = Vec::new();
479            run(&self.config, &command, &mut out)?;
480            Ok(String::from_utf8(out)
481                .expect("text")
482                .lines()
483                .map(|line| serde_json::from_str(line).expect("a JSON line"))
484                .collect())
485        }
486
487        fn create(&self, name: &str) -> String {
488            let out = self
489                .run(Command::TenantCreate {
490                    name: name.to_owned(),
491                    retention: None,
492                })
493                .expect("made");
494            out[0]["id"].as_str().expect("an id").to_owned()
495        }
496
497        /// The registry as a test reads it, with the command's locks free.
498        fn tenants(&self) -> Tenants {
499            Tenants::open_beside(
500                &self.config.data_dir,
501                Key::load_master(&self.config.master_key).expect("master"),
502                Arc::new(OsFs),
503                Arc::new(SystemClock),
504            )
505            .expect("opens")
506        }
507
508        fn tenant_dirs(&self) -> Vec<String> {
509            let mut names: Vec<String> = std::fs::read_dir(self.config.data_dir.join("tenants"))
510                .expect("tenants dir")
511                .map(|e| e.expect("entry").file_name().to_string_lossy().into_owned())
512                .collect();
513            names.sort();
514            names
515        }
516    }
517
518    fn tenant_error(result: Result<Vec<Value>, CommandError>) -> TenantError {
519        match result {
520            Err(CommandError::Tenant(e)) => e,
521            other => panic!("not a tenant error: {other:?}"),
522        }
523    }
524
525    #[test]
526    fn each_command_does_its_own_work() {
527        let id = || "x".to_owned();
528        for (command, work) in [
529            (
530                Command::TenantCreate {
531                    name: id(),
532                    retention: None,
533                },
534                Work::TenantCreate,
535            ),
536            (Command::TenantList, Work::TenantList),
537            (
538                Command::TenantSetRetention {
539                    id: id(),
540                    retention: id(),
541                },
542                Work::TenantSetRetention,
543            ),
544            (
545                Command::TenantRemove {
546                    id: id(),
547                    force: false,
548                    override_holds: false,
549                },
550                Work::TenantRemove,
551            ),
552            (
553                Command::KeyCreate {
554                    owner: Owner::Operator,
555                    label: id(),
556                },
557                Work::KeyCreate,
558            ),
559            (Command::KeyList { tenant: None }, Work::KeyList),
560            (Command::KeyRevoke { id: id() }, Work::KeyRevoke),
561            (
562                Command::AuditExport {
563                    log: Log::Admin,
564                    since: None,
565                },
566                Work::AuditExport,
567            ),
568            (Command::RekeyTenant { id: id() }, Work::RekeyTenant),
569            (Command::Check, Work::Check),
570            (Command::Backup { to: id().into() }, Work::Backup),
571            (
572                Command::RekeyMaster {
573                    new_master: id().into(),
574                },
575                Work::RekeyMaster,
576            ),
577        ] {
578            assert_eq!(command.work(), work, "{command:?}");
579        }
580    }
581
582    #[test]
583    fn tenant_create_prints_the_new_id_and_list_shows_it() {
584        let s = setup();
585        let out = s
586            .run(Command::TenantCreate {
587                name: "acme".to_owned(),
588                retention: Some("18m".to_owned()),
589            })
590            .expect("made");
591        assert_eq!(out.len(), 1);
592        let id = out[0]["id"].as_str().expect("an id").to_owned();
593        assert!(TenantId::parse(&id).is_some(), "{id}");
594        assert_eq!(out[0], json!({ "id": id }));
595        let beta = s.create("beta");
596        assert_eq!(
597            s.run(Command::TenantList).expect("listed"),
598            [
599                json!({ "id": id, "name": "acme", "retention": "18m" }),
600                json!({ "id": beta, "name": "beta", "retention": null }),
601            ]
602        );
603    }
604
605    #[test]
606    fn a_bad_period_is_refused_before_anything_is_made() {
607        let s = setup();
608        let error = s
609            .run(Command::TenantCreate {
610                name: "acme".to_owned(),
611                retention: Some("7w".to_owned()),
612            })
613            .expect_err("refused");
614        assert!(
615            matches!(&error, CommandError::BadPeriod(p) if p == "7w"),
616            "{error:?}"
617        );
618        assert!(error.to_string().contains("7w"), "{error}");
619        assert_eq!(
620            s.run(Command::TenantList).expect("listed"),
621            Vec::<Value>::new()
622        );
623        let id = s.create("acme");
624        let error = s
625            .run(Command::TenantSetRetention {
626                id,
627                retention: "forever".to_owned(),
628            })
629            .expect_err("refused");
630        assert!(matches!(&error, CommandError::BadPeriod(p) if p == "forever"));
631    }
632
633    #[test]
634    fn set_retention_takes_a_period_or_default() {
635        let s = setup();
636        let id = s.create("acme");
637        let set = |retention: &str| {
638            s.run(Command::TenantSetRetention {
639                id: id.clone(),
640                retention: retention.to_owned(),
641            })
642            .expect("set")
643        };
644        assert_eq!(set("90d"), [json!({ "id": id, "retention": "90d" })]);
645        assert_eq!(
646            s.run(Command::TenantList).expect("listed")[0]["retention"],
647            "90d"
648        );
649        assert_eq!(set("default"), [json!({ "id": id, "retention": null })]);
650        assert_eq!(
651            s.run(Command::TenantList).expect("listed")[0]["retention"],
652            Value::Null
653        );
654    }
655
656    #[test]
657    fn an_unknown_or_malformed_tenant_is_not_found() {
658        let s = setup();
659        for id in [TenantId::generate().to_string(), "not-an-id".to_owned()] {
660            assert_eq!(
661                tenant_error(s.run(Command::TenantSetRetention {
662                    id: id.clone(),
663                    retention: "7y".to_owned()
664                })),
665                TenantError::NotFound
666            );
667            assert_eq!(
668                tenant_error(s.run(Command::TenantRemove {
669                    id: id.clone(),
670                    force: true,
671                    override_holds: false
672                })),
673                TenantError::NotFound
674            );
675            assert_eq!(
676                tenant_error(s.run(Command::KeyCreate {
677                    owner: Owner::Tenant {
678                        id: id.clone(),
679                        scopes: vec!["read".to_owned()]
680                    },
681                    label: "l".to_owned()
682                })),
683                TenantError::NotFound
684            );
685            assert_eq!(
686                tenant_error(s.run(Command::KeyList {
687                    tenant: Some(id.clone())
688                })),
689                TenantError::NotFound
690            );
691            assert_eq!(
692                tenant_error(s.run(Command::AuditExport {
693                    log: Log::Tenant(id.clone()),
694                    since: None
695                })),
696                TenantError::NotFound
697            );
698        }
699    }
700
701    /// Without --force a tenant holding vCons stays; --force does not pass
702    /// a legal hold; --override-holds does.
703    #[test]
704    fn remove_goes_as_far_as_its_flags_allow() {
705        let s = setup();
706        let empty = s.create("empty");
707        let full = s.create("full");
708        {
709            let tenants = s.tenants();
710            let store = tenants
711                .store(TenantId::parse(&full).expect("id"))
712                .expect("store");
713            let caller = Caller { key: KeyId([1; 8]) };
714            let (found, body) = vcon(UUID, "kept");
715            store.create(&caller, &found, &body).expect("stored");
716            store.set_hold(&caller, found.uuid, true).expect("held");
717        }
718        let remove = |id: &str, force, override_holds| {
719            s.run(Command::TenantRemove {
720                id: id.to_owned(),
721                force,
722                override_holds,
723            })
724        };
725        assert_eq!(
726            remove(&empty, false, false).expect("removed"),
727            Vec::<Value>::new()
728        );
729        assert_eq!(
730            tenant_error(remove(&full, false, false)),
731            TenantError::HoldsVcons
732        );
733        assert_eq!(
734            tenant_error(remove(&full, true, false)),
735            TenantError::UnderHold
736        );
737        assert_eq!(
738            remove(&full, true, true).expect("removed"),
739            Vec::<Value>::new()
740        );
741        assert_eq!(
742            s.run(Command::TenantList).expect("listed"),
743            Vec::<Value>::new()
744        );
745        assert_eq!(s.tenant_dirs(), Vec::<String>::new());
746    }
747
748    #[test]
749    fn keys_are_made_listed_and_revoked() {
750        let s = setup();
751        let tenant = s.create("acme");
752        let made = s
753            .run(Command::KeyCreate {
754                owner: Owner::Tenant {
755                    id: tenant.clone(),
756                    scopes: vec!["write".to_owned(), "read".to_owned()],
757                },
758                label: "producer".to_owned(),
759            })
760            .expect("made");
761        let id = made[0]["id"].as_str().expect("id").to_owned();
762        let token = made[0]["token"].as_str().expect("token").to_owned();
763        assert_eq!(made.len(), 1);
764        assert_eq!(made[0].as_object().expect("object").len(), 2);
765        assert!(token.starts_with(&format!("vcs_{id}_")), "{token}");
766        let operator = s
767            .run(Command::KeyCreate {
768                owner: Owner::Operator,
769                label: "ops".to_owned(),
770            })
771            .expect("made")[0]["id"]
772            .as_str()
773            .expect("id")
774            .to_owned();
775
776        let listed = s.run(Command::KeyList { tenant: None }).expect("listed");
777        assert_eq!(listed.len(), 2);
778        assert_eq!(listed[0]["id"], id.as_str());
779        assert_eq!(listed[0]["tenant"], tenant.as_str());
780        assert_eq!(listed[0]["scopes"], json!(["write", "read"]));
781        assert_eq!(listed[0]["label"], "producer");
782        assert_eq!(listed[0]["revoked_at"], Value::Null);
783        assert!(
784            listed[0]["created_at"]
785                .as_str()
786                .is_some_and(|t| t.ends_with('Z'))
787        );
788        assert!(
789            listed.iter().all(|k| k.get("token").is_none()),
790            "never a token"
791        );
792        assert_eq!(listed[1]["id"], operator.as_str());
793        assert_eq!(listed[1]["tenant"], Value::Null);
794        assert_eq!(listed[1]["scopes"], json!(["admin"]));
795
796        let own = s
797            .run(Command::KeyList {
798                tenant: Some(tenant.clone()),
799            })
800            .expect("listed");
801        assert_eq!(own, listed[..1]);
802
803        assert_eq!(
804            s.run(Command::KeyRevoke { id: id.clone() })
805                .expect("revoked"),
806            Vec::<Value>::new()
807        );
808        let listed = s.run(Command::KeyList { tenant: None }).expect("listed");
809        assert!(listed[0]["revoked_at"].as_str().is_some(), "{listed:?}");
810        assert_eq!(listed[1]["revoked_at"], Value::Null);
811    }
812
813    #[test]
814    fn bad_scopes_and_unknown_keys_are_refused() {
815        let s = setup();
816        let tenant = s.create("acme");
817        for scopes in [
818            vec!["superuser"],
819            vec!["read", "superuser"],
820            vec!["read", "read"],
821            vec!["admin"],
822            vec![],
823        ] {
824            let error = s.run(Command::KeyCreate {
825                owner: Owner::Tenant {
826                    id: tenant.clone(),
827                    scopes: scopes.iter().map(|s| (*s).to_owned()).collect(),
828                },
829                label: "l".to_owned(),
830            });
831            assert_eq!(tenant_error(error), TenantError::BadScopes, "{scopes:?}");
832        }
833        assert_eq!(
834            s.run(Command::KeyList { tenant: None }).expect("listed"),
835            Vec::<Value>::new()
836        );
837        for id in ["0123456789abcdef".to_owned(), "nope".to_owned()] {
838            assert!(
839                matches!(
840                    s.run(Command::KeyRevoke { id }),
841                    Err(CommandError::UnknownKey)
842                ),
843                "unknown"
844            );
845        }
846    }
847
848    #[test]
849    fn audit_export_writes_either_log_from_a_sequence_number() {
850        let s = setup();
851        let tenant = s.create("acme");
852        s.run(Command::TenantList).expect("listed");
853        s.run(Command::KeyCreate {
854            owner: Owner::Tenant {
855                id: tenant.clone(),
856                scopes: vec!["read".to_owned()],
857            },
858            label: "l".to_owned(),
859        })
860        .expect("made");
861
862        let admin = s
863            .run(Command::AuditExport {
864                log: Log::Admin,
865                since: None,
866            })
867            .expect("exported");
868        let actions: Vec<&str> = admin
869            .iter()
870            .map(|r| r["action"].as_str().expect("action"))
871            .collect();
872        assert_eq!(actions, ["tenant_create", "tenant_list", "key_create"]);
873        assert!(admin.iter().all(|r| r["actor"] == "cli"));
874        let since = admin[0]["seq"].as_i64().expect("seq");
875        let later = s
876            .run(Command::AuditExport {
877                log: Log::Admin,
878                since: Some(since),
879            })
880            .expect("exported");
881        assert_eq!(later, admin[1..]);
882
883        let own = s
884            .run(Command::AuditExport {
885                log: Log::Tenant(tenant),
886                since: None,
887            })
888            .expect("exported");
889        let actions: Vec<&str> = own
890            .iter()
891            .map(|r| r["action"].as_str().expect("action"))
892            .collect();
893        assert_eq!(actions, ["key_create"]);
894    }
895
896    #[test]
897    fn rekey_tenant_rotates_the_key_and_prints_nothing() {
898        let s = setup();
899        let id = s.create("acme");
900        let tenant = TenantId::parse(&id).expect("id");
901        let before = s.tenants().tenant_key_id_for_test(tenant);
902        assert_eq!(
903            s.run(Command::RekeyTenant { id: id.clone() })
904                .expect("rekeyed"),
905            Vec::<Value>::new()
906        );
907        assert_ne!(s.tenants().tenant_key_id_for_test(tenant), before);
908        assert_eq!(
909            tenant_error(s.run(Command::RekeyTenant {
910                id: TenantId::generate().to_string()
911            })),
912            TenantError::NotFound
913        );
914        let server = datadir::lock(&s.config.data_dir, Sharing::Serve).expect("a server");
915        assert!(matches!(
916            s.run(Command::RekeyTenant { id }),
917            Err(CommandError::DataDir(LockError::InUse(Sharing::Alone)))
918        ));
919        drop(server);
920    }
921
922    #[test]
923    fn rekey_master_moves_the_data_to_the_new_master_key() {
924        let mut s = setup();
925        s.create("acme");
926        let new_file = s.config.master_key.with_file_name("new.key");
927        std::fs::write(&new_file, [0x5c; 32]).expect("new key");
928        assert!(matches!(
929            s.run(Command::RekeyMaster {
930                new_master: s.config.master_key.clone()
931            }),
932            Err(CommandError::SameMasterKey)
933        ));
934        let server = datadir::lock(&s.config.data_dir, Sharing::Serve).expect("a server");
935        assert!(matches!(
936            s.run(Command::RekeyMaster {
937                new_master: new_file.clone()
938            }),
939            Err(CommandError::DataDir(LockError::InUse(Sharing::Alone)))
940        ));
941        drop(server);
942        let out = s
943            .run(Command::RekeyMaster {
944                new_master: new_file.clone(),
945            })
946            .expect("rotated");
947        let new_id = Key::load_master(&new_file).expect("key").id().to_string();
948        assert_eq!(out.len(), 1);
949        assert_eq!(out[0]["master_key_id"], new_id.as_str());
950        assert!(
951            out[0]["next"]
952                .as_str()
953                .is_some_and(|n| n.contains("destroy"))
954        );
955        assert!(matches!(
956            s.run(Command::TenantList),
957            Err(CommandError::WrongMasterKey)
958        ));
959        s.config.master_key = new_file;
960        assert_eq!(s.run(Command::TenantList).expect("listed").len(), 1);
961    }
962
963    mod check {
964        use time::macros::datetime;
965
966        use super::*;
967        use crate::ids::VersionId;
968        use crate::store::Examination;
969
970        const NOW: OffsetDateTime = datetime!(2026-10-25 12:00 UTC);
971        const HOUR: std::time::Duration = std::time::Duration::from_secs(3600);
972
973        fn sound() -> Examination {
974            Examination {
975                versions: 3,
976                damaged: vec![],
977                leftovers: vec!["a.tmp".to_owned()],
978                due: 0,
979                oldest_due_since: None,
980                purges_since_key: 4,
981                oldest_purge_since_key: Some(datetime!(2026-10-01 00:00 UTC)),
982            }
983        }
984
985        fn tenant(store: Result<Examination, OpenFailure>, stopped: bool) -> TenantExamination {
986            TenantExamination {
987                id: TenantId([1; 16]),
988                name: "acme".to_owned(),
989                rotation_stopped: stopped,
990                store,
991            }
992        }
993
994        #[test]
995        fn leftovers_and_work_waiting_for_rotation_are_not_problems() {
996            assert_eq!(
997                problems(&tenant(Ok(sound()), false), HOUR, NOW),
998                Vec::<String>::new()
999            );
1000        }
1001
1002        #[test]
1003        fn each_problem_is_named() {
1004            let damaged = Examination {
1005                damaged: vec![VersionId([2; 16]), VersionId([3; 16])],
1006                ..sound()
1007            };
1008            assert_eq!(problems(&tenant(Ok(damaged), false), HOUR, NOW).len(), 2);
1009            let stopped = problems(&tenant(Ok(sound()), true), HOUR, NOW);
1010            assert_eq!(stopped.len(), 1);
1011            assert!(stopped[0].contains("rekey tenant"), "{stopped:?}");
1012            let closed = problems(
1013                &tenant(Err(OpenFailure::Refused(TenantError::Io)), false),
1014                HOUR,
1015                NOW,
1016            );
1017            assert_eq!(closed.len(), 1);
1018            assert!(closed[0].contains("catalog"), "{closed:?}");
1019            assert!(!closed[0].contains("key"), "{closed:?}");
1020        }
1021
1022        /// Only a catalog its key does not open is a key problem; one the
1023        /// process may not open is named with its path and the system's
1024        /// error.
1025        #[test]
1026        fn a_key_problem_and_an_access_problem_are_told_apart() {
1027            let key = problems(&tenant(Err(OpenFailure::WrongKey), false), HOUR, NOW);
1028            assert_eq!(key, ["its catalog does not open under its tenant key"]);
1029            let access = OpenFailure::Access {
1030                path: std::path::PathBuf::from("/srv/data/tenants/01/catalog.sqlite"),
1031                error: std::io::Error::from_raw_os_error(libc::EACCES),
1032            };
1033            assert_eq!(
1034                problems(&tenant(Err(access), false), HOUR, NOW),
1035                [
1036                    "its catalog cannot be opened: /srv/data/tenants/01/catalog.sqlite: Permission denied (os error 13)"
1037                ]
1038            );
1039        }
1040
1041        #[test]
1042        fn a_sweep_is_stalled_only_once_a_version_waited_longer_than_the_interval() {
1043            let waited = |since| Examination {
1044                due: 1,
1045                oldest_due_since: Some(since),
1046                ..sound()
1047            };
1048            let ok = waited(datetime!(2026-10-25 11:00 UTC));
1049            assert_eq!(
1050                problems(&tenant(Ok(ok), false), HOUR, NOW),
1051                Vec::<String>::new()
1052            );
1053            let stalled = waited(datetime!(2026-10-25 10:59:59 UTC));
1054            let found = problems(&tenant(Ok(stalled), false), HOUR, NOW);
1055            assert_eq!(found.len(), 1);
1056            assert!(found[0].contains("sweep"), "{found:?}");
1057        }
1058
1059        #[test]
1060        fn check_reports_each_tenant_and_a_summary_and_exits_1_on_a_problem() {
1061            let s = setup();
1062            let id = s.create("acme");
1063            let out = s.run(Command::Check).expect("nothing wrong");
1064            assert_eq!(out.len(), 2);
1065            assert_eq!(out[0]["tenant"], id.as_str());
1066            assert_eq!(out[0]["problems"], json!([]));
1067            assert_eq!(out[1]["tenants"], 1);
1068            assert_eq!(out[1]["problems"], 0);
1069
1070            let tenant = TenantId::parse(&id).expect("id");
1071            let version = {
1072                let tenants = s.tenants();
1073                let store = tenants.store(tenant).expect("store");
1074                let (found, body) = vcon(UUID, "kept");
1075                let Ok(crate::store::Created::New(v)) =
1076                    store.create(&Caller { key: KeyId([1; 8]) }, &found, &body)
1077                else {
1078                    panic!("stored")
1079                };
1080                v
1081            };
1082            let objects = s.config.data_dir.join("tenants").join(&id).join("objects");
1083            std::fs::remove_file(objects.join(version.to_string())).expect("remove");
1084            std::fs::write(objects.join(format!("{version}.tmp")), b"x").expect("leftover");
1085            let mut printed = Vec::new();
1086            let found = run(&s.config, &Command::Check, &mut printed);
1087            assert!(
1088                matches!(found, Err(CommandError::CheckFound(1))),
1089                "{found:?}"
1090            );
1091            let lines: Vec<Value> = String::from_utf8(printed)
1092                .expect("text")
1093                .lines()
1094                .map(|l| serde_json::from_str(l).expect("json"))
1095                .collect();
1096            assert_eq!(lines[0]["damaged"], json!([version.to_string()]));
1097            assert_eq!(lines[0]["leftovers"], json!([format!("{version}.tmp")]));
1098            assert_eq!(lines[1]["problems"], 1);
1099            assert!(
1100                objects.join(format!("{version}.tmp")).exists(),
1101                "check reports leftovers; it does not remove them"
1102            );
1103            let server = datadir::lock(&s.config.data_dir, Sharing::Serve).expect("a server");
1104            assert!(matches!(
1105                s.run(Command::Check),
1106                Err(CommandError::DataDir(LockError::InUse(Sharing::Alone)))
1107            ));
1108            drop(server);
1109        }
1110    }
1111
1112    /// The commands that run alone are refused while a server holds the
1113    /// data directory; the others run beside it.
1114    #[test]
1115    fn beside_a_server_only_the_beside_commands_run() {
1116        let s = setup();
1117        let tenant = s.create("acme");
1118        let server = datadir::lock(&s.config.data_dir, Sharing::Serve).expect("a server");
1119        for command in [
1120            Command::TenantCreate {
1121                name: "beta".to_owned(),
1122                retention: None,
1123            },
1124            Command::TenantRemove {
1125                id: tenant.clone(),
1126                force: true,
1127                override_holds: false,
1128            },
1129        ] {
1130            assert!(
1131                matches!(
1132                    s.run(command.clone()),
1133                    Err(CommandError::DataDir(LockError::InUse(Sharing::Alone)))
1134                ),
1135                "{command:?}"
1136            );
1137        }
1138        s.run(Command::TenantList).expect("beside");
1139        s.run(Command::TenantSetRetention {
1140            id: tenant.clone(),
1141            retention: "1y".to_owned(),
1142        })
1143        .expect("beside");
1144        s.run(Command::KeyList { tenant: None }).expect("beside");
1145        s.run(Command::AuditExport {
1146            log: Log::Admin,
1147            since: None,
1148        })
1149        .expect("beside");
1150        s.run(Command::Backup {
1151            to: s._dir.path().join("backup"),
1152        })
1153        .expect("beside");
1154        drop(server);
1155        assert_eq!(s.run(Command::TenantList).expect("listed").len(), 1);
1156    }
1157
1158    /// A command beside a server opens without recovery, so a create the
1159    /// server has in progress stays; one that runs alone recovers.
1160    #[test]
1161    fn only_a_command_that_runs_alone_recovers() {
1162        let s = setup();
1163        s.create("acme");
1164        let in_progress = TenantId::generate().to_string();
1165        std::fs::create_dir(s.config.data_dir.join("tenants").join(&in_progress))
1166            .expect("a directory with no row");
1167        s.run(Command::TenantList).expect("listed");
1168        s.run(Command::KeyList { tenant: None }).expect("listed");
1169        assert!(
1170            s.tenant_dirs().contains(&in_progress),
1171            "left by beside commands"
1172        );
1173        s.create("beta");
1174        assert!(
1175            !s.tenant_dirs().contains(&in_progress),
1176            "removed by an alone command"
1177        );
1178    }
1179
1180    #[test]
1181    fn a_wrong_master_key_is_named() {
1182        let s = setup();
1183        s.create("acme");
1184        std::fs::write(&s.config.master_key, [0x5b; 32]).expect("another key");
1185        let error = s.run(Command::TenantList).expect_err("refused");
1186        assert!(matches!(error, CommandError::WrongMasterKey), "{error:?}");
1187        assert!(error.to_string().contains("master key"), "{error}");
1188        std::fs::write(&s.config.master_key, [0x5a; 31]).expect("short key");
1189        let error = s.run(Command::TenantList).expect_err("refused");
1190        assert!(matches!(error, CommandError::MasterKey(_)), "{error:?}");
1191    }
1192
1193    #[test]
1194    fn backup_prints_what_it_copied_and_check_finds_the_copy_whole() {
1195        let s = setup();
1196        s.create("acme");
1197        s.create("globex");
1198        let to = s._dir.path().join("backup");
1199        let out = s.run(Command::Backup { to: to.clone() }).expect("backs up");
1200        assert_eq!(
1201            out,
1202            [serde_json::json!({ "tenants": 2, "versions": 0, "tries": 1 })]
1203        );
1204        let copy = Setup {
1205            _dir: tempfile::tempdir().expect("temp dir"),
1206            config: Config {
1207                data_dir: to,
1208                ..s.config.clone()
1209            },
1210        };
1211        let checked = copy.run(Command::Check).expect("check finds no problem");
1212        assert_eq!(checked.last().expect("a summary")["problems"], 0);
1213        let names: Vec<_> = copy
1214            .run(Command::TenantList)
1215            .expect("lists")
1216            .iter()
1217            .map(|t| t["name"].clone())
1218            .collect();
1219        assert_eq!(names, ["acme", "globex"]);
1220    }
1221
1222    #[test]
1223    fn a_backup_path_already_there_is_refused() {
1224        let s = setup();
1225        let to = s._dir.path().join("backup");
1226        std::fs::create_dir(&to).expect("dir");
1227        let error = s.run(Command::Backup { to }).expect_err("refused");
1228        assert!(
1229            matches!(error, CommandError::Backup(BackupError::Exists)),
1230            "{error:?}"
1231        );
1232    }
1233
1234    #[test]
1235    fn each_error_says_what_went_wrong() {
1236        for (error, words) in [
1237            (CommandError::WrongMasterKey, "master key"),
1238            (CommandError::BadPeriod("7w".to_owned()), "7w"),
1239            (CommandError::UnknownKey, "no key"),
1240            (CommandError::Tenant(TenantError::NotFound), "no tenant"),
1241            (CommandError::Tenant(TenantError::NameTaken), "name"),
1242            (CommandError::Tenant(TenantError::BadName), "name"),
1243            (CommandError::Tenant(TenantError::HoldsVcons), "--force"),
1244            (
1245                CommandError::Tenant(TenantError::UnderHold),
1246                "--override-holds",
1247            ),
1248            (CommandError::Tenant(TenantError::BadScopes), "scope"),
1249            (
1250                CommandError::Tenant(TenantError::LimitReached),
1251                "rekey master",
1252            ),
1253            (CommandError::Tenant(TenantError::AuditFailed), "audit"),
1254            (
1255                CommandError::DataDir(LockError::InUse(Sharing::Alone)),
1256                "in use",
1257            ),
1258            (CommandError::Output(io::Error::other("closed")), "output"),
1259            (CommandError::Backup(BackupError::Exists), "already exists"),
1260            (CommandError::Backup(BackupError::KeptChanging), "try again"),
1261            (
1262                CommandError::Backup(BackupError::Io),
1263                "could not be written",
1264            ),
1265            (CommandError::Backup(BackupError::AuditFailed), "audit"),
1266        ] {
1267            let text = error.to_string();
1268            assert!(text.contains(words), "{error:?}: {text}");
1269        }
1270    }
1271}