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