1use 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#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum Command {
29 TenantCreate {
30 name: String,
31 retention: Option<String>,
32 },
33 TenantList,
34 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 RekeyTenant {
60 id: String,
61 },
62 RekeyMaster {
64 new_master: std::path::PathBuf,
65 },
66 Check,
68 Backup {
71 to: std::path::PathBuf,
72 },
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum Owner {
78 Tenant { id: String, scopes: Vec<String> },
79 Operator,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum Log {
85 Tenant(String),
86 Admin,
87}
88
89impl Command {
90 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#[derive(Debug)]
111pub enum CommandError {
112 DataDir(LockError),
113 MasterKey(KeyError),
114 WrongMasterKey,
116 BadPeriod(String),
119 Tenant(TenantError),
120 Open(RegistryError),
122 UnknownKey,
124 Output(io::Error),
126 SameMasterKey,
128 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
192pub 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 if let Command::RekeyMaster { new_master } = command {
201 return rekey_master(config, &master, new_master, out);
202 }
203 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
323fn 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
349fn 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
392pub 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
435fn period(text: &str) -> Result<Retention, CommandError> {
437 Retention::parse(text).ok_or_else(|| CommandError::BadPeriod(text.to_owned()))
438}
439
440fn tenant_id(text: &str) -> Result<TenantId, CommandError> {
443 TenantId::parse(text).ok_or(CommandError::Tenant(TenantError::NotFound))
444}
445
446fn 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 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 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 #[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 #[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 #[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 #[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 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 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 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}