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