1use std::sync::Arc;
2
3use clap::Subcommand;
4
5use crate::admin;
6use crate::cli::CliError;
7use crate::cli::render;
8use crate::cli::style::Palette;
9use crate::sqlite::db::Database;
10use crate::sqlite::eab::Eab;
11
12#[derive(Subcommand)]
13pub enum EabCommand {
14 Create {
16 #[arg(long)]
17 label: Option<String>,
18 #[arg(long)]
21 profile: Option<String>,
22 #[arg(long)]
23 json: bool,
24 },
25 List {
27 #[arg(long)]
28 json: bool,
29 },
30 Show {
32 kid: String,
33 #[arg(long)]
34 json: bool,
35 },
36 Revoke { kid: String },
38}
39
40pub async fn run_eab_command(
41 command: EabCommand,
42 palette: Palette,
43 database: Arc<Database>,
44) -> Result<(), CliError> {
45 match command {
46 EabCommand::Create {
47 label,
48 profile,
49 json,
50 } => {
51 let eab = Eab::create(label, profile, &database).await?;
52 if json {
53 println!("{}", admin::render_eab_created_json(&eab));
54 } else {
55 print!("{}", render::render_eab_created_text(&eab, palette));
56 }
57 }
58 EabCommand::List { json } => {
59 let keys = Eab::list_all(&database).await?;
60 render::print_rows(&keys, json, admin::render_eab_json, |eab| {
61 render::render_eab_line(eab, palette)
62 });
63 }
64 EabCommand::Show { kid, json } => match Eab::find_any_by_kid(&kid, &database).await? {
65 None => return Err(not_found(&kid)),
66 Some(eab) if json => println!("{}", admin::render_eab_json(&eab)),
67 Some(eab) => println!("{}", render::render_eab_line(&eab, palette)),
68 },
69 EabCommand::Revoke { kid } => {
70 if !Eab::revoke(&kid, &database).await? {
71 return Err(not_found(&kid));
72 }
73 println!("Revoked EAB key {kid}.");
74 }
75 }
76 Ok(())
77}
78
79fn not_found(kid: &str) -> CliError {
80 CliError(format!("no such EAB credential: {kid}"))
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[tokio::test]
88 async fn show_and_revoke_refuse_an_unknown_kid() {
89 let database = Arc::new(Database::connect_in_memory().await.unwrap());
90 let expected = CliError("no such EAB credential: kid-nope".to_string());
91
92 for command in [
93 EabCommand::Show {
94 kid: "kid-nope".to_string(),
95 json: false,
96 },
97 EabCommand::Revoke {
98 kid: "kid-nope".to_string(),
99 },
100 ] {
101 let error = run_eab_command(command, Palette::plain(), database.clone())
102 .await
103 .expect_err("an unknown kid must fail");
104 assert_eq!(error, expected);
105 }
106 }
107
108 #[tokio::test]
111 async fn a_created_key_shows_lists_and_revokes() {
112 let database = Arc::new(Database::connect_in_memory().await.unwrap());
113 let eab = Eab::create(Some("test".to_string()), None, &database)
114 .await
115 .unwrap();
116
117 for command in [
118 EabCommand::Create {
119 label: None,
120 profile: Some("default".to_string()),
121 json: true,
122 },
123 EabCommand::List { json: true },
124 EabCommand::Show {
125 kid: eab.kid.clone(),
126 json: true,
127 },
128 EabCommand::Show {
129 kid: eab.kid.clone(),
130 json: false,
131 },
132 EabCommand::Revoke {
133 kid: eab.kid.clone(),
134 },
135 ] {
136 run_eab_command(command, Palette::plain(), database.clone())
137 .await
138 .unwrap();
139 }
140
141 run_eab_command(
142 EabCommand::Revoke {
143 kid: eab.kid.clone(),
144 },
145 Palette::plain(),
146 database.clone(),
147 )
148 .await
149 .expect("revoking an already-revoked key is a no-op, not a failure");
150
151 assert_eq!(
152 Eab::find_any_by_kid(&eab.kid, &database)
153 .await
154 .unwrap()
155 .unwrap()
156 .status,
157 "revoked"
158 );
159 }
160}