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