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