1use std::io::BufRead;
2use std::sync::Arc;
3
4use clap::Subcommand;
5
6use crate::admin::{self, DeleteOutcome};
7use crate::cli::CliError;
8use crate::cli::render;
9use crate::cli::style::Palette;
10use crate::config::Config;
11use crate::signer;
12use crate::sqlite::authz::Authorization;
13use crate::sqlite::db::Database;
14use crate::sqlite::order::{Order, OrderQuery};
15use crate::sqlite::status::OrderStatus;
16
17#[derive(Subcommand)]
18pub enum OrderCommand {
19 List {
21 #[arg(long)]
23 profile: Option<String>,
24 #[arg(long = "account-id")]
25 account_id: Option<String>,
26 #[arg(long)]
27 status: Option<String>,
28 #[arg(long)]
29 json: bool,
30 },
31 Show {
33 id: String,
34 #[arg(long)]
35 json: bool,
36 },
37 Delete { id: String },
39 Revoke {
41 id: String,
42 #[arg(long)]
43 reason: Option<u32>,
44 },
45}
46
47pub async fn run_order_command(
48 command: OrderCommand,
49 yes: bool,
50 palette: Palette,
51 reader: &mut impl BufRead,
52 config: &Config,
53 database: Arc<Database>,
54) -> Result<(), CliError> {
55 match command {
56 OrderCommand::List {
57 profile,
58 account_id,
59 status,
60 json,
61 } => {
62 let status = status
66 .map(|value| value.parse::<OrderStatus>())
67 .transpose()
68 .map_err(|error| CliError(format!("--status: {error}")))?;
69
70 let query = OrderQuery {
79 profile,
80 account_id,
81 status,
82 limit: i64::MAX,
83 offset: 0,
84 };
85 let (orders, _total) = Order::search(&query, &database).await?;
86 if json {
87 let ids: Vec<&str> = orders.iter().map(|o| o.id.as_str()).collect();
93 let mut authz_ids = Authorization::find_ids_by_orders(&ids, &database).await?;
94 let rendered: Vec<_> = orders
95 .iter()
96 .map(|order| {
97 admin::render_order_json(
98 order,
99 &config.server.base_url,
100 &authz_ids.remove(&order.id).unwrap_or_default(),
101 )
102 })
103 .collect();
104 println!("{}", serde_json::Value::Array(rendered));
105 } else {
106 for order in &orders {
107 println!("{}", render::render_order_line(order, palette));
108 }
109 }
110 }
111 OrderCommand::Show { id, json } => match admin::load_order_detail(&id, database).await? {
112 None => return Err(not_found(&id)),
113 Some(detail) if json => {
114 println!(
115 "{}",
116 admin::render_order_detail_json(&detail, &config.server.base_url)
117 );
118 }
119 Some(detail) => print!("{}", render::render_order_detail_text(&detail, palette)),
120 },
121 OrderCommand::Delete { id } => {
122 match admin::confirm_delete_order(&id, yes, reader, database).await? {
123 DeleteOutcome::NotFound => return Err(not_found(&id)),
124 DeleteOutcome::Cancelled => println!("Cancelled."),
125 DeleteOutcome::Deleted => println!("Deleted order {id}."),
126 }
127 }
128 OrderCommand::Revoke { id, reason } => {
129 let Some(order) = Order::find_by_id(&id, &database).await? else {
132 return Err(not_found(&id));
133 };
134 let profiles = config
135 .resolve_profiles()
136 .map_err(|error| CliError(format!("configuration error: {error}")))?;
137 let Some(profile) = profiles.iter().find(|p| p.name == order.profile) else {
138 return Err(CliError(format!(
139 "order {id} was issued by profile `{}`, which this configuration does not \
140 define — revoking it needs the endpoint that signed it",
141 order.profile
142 )));
143 };
144 let egress = Arc::new(
152 crate::Egress::from_config(config)
153 .map_err(|error| CliError(format!("configuration error: {error}")))?,
154 );
155 let jobs = crate::jobs::JobQueue::new(database.clone(), &config.jobs);
160 let metrics = Arc::new(crate::metrics::Metrics::new(database.clone()));
164 let signer = signer::from_config(
165 &profile.sections.signer,
166 vec![profile.name.clone()],
167 &signer::SignerParts {
168 database: database.clone(),
169 notifiers: std::collections::HashMap::new().into(),
170 metrics,
171 egress,
172 jobs,
173 },
174 &signer::CarriedState::new(),
177 )
178 .map_err(|error| CliError(format!("signer error: {error}")))?;
179 match admin::revoke_order(
182 &id,
183 reason,
184 crate::audit::Actor::cli(),
185 crate::audit::ClientContext::default(),
186 database,
187 signer,
188 )
189 .await
190 .map_err(|error| CliError(error.to_string()))?
191 {
192 admin::RevokeOutcome::NotFound => return Err(not_found(&id)),
193 admin::RevokeOutcome::NotIssued => {
194 return Err(CliError(format!("order {id} has no issued certificate")));
195 }
196 admin::RevokeOutcome::AlreadyRevoked => {
197 return Err(CliError(format!(
198 "order {id}'s certificate is already revoked"
199 )));
200 }
201 admin::RevokeOutcome::Revoked(order) => {
202 println!("{}", render::render_order_line(&order, palette));
203 }
204 }
205 }
206 }
207 Ok(())
208}
209
210fn not_found(id: &str) -> CliError {
211 CliError(format!("no such order: {id}"))
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::audit::ClientContext;
218 use crate::signer::{IssueOutcome, RequestedValidity, SignerBackend};
219 use crate::sqlite::account::Account;
220
221 fn config_in(dir: impl AsRef<std::path::Path>, profile: &str) -> Config {
225 let dir = dir.as_ref();
226 let _lock = crate::config::ENV_LOCK
227 .lock()
228 .unwrap_or_else(std::sync::PoisonError::into_inner);
229 let ca = dir.join("ca");
230 std::fs::write(
231 dir.join("config.toml"),
232 format!(
233 r#"
234 [profiles.{profile}]
235 signer.local_ca.cert_path = "{ca}.pem"
236 signer.local_ca.key_path = "{ca}.key"
237 signer.local_ca.crl_path = "{ca}.crl"
238 "#,
239 ca = ca.display(),
240 ),
241 )
242 .unwrap();
243 unsafe {
246 std::env::set_var("ACME_PROXY_CONFIG", dir.join("config").to_str().unwrap());
247 }
248 let config = Config::load().expect("the configuration must load");
249 unsafe {
250 std::env::remove_var("ACME_PROXY_CONFIG");
251 }
252 config
253 }
254
255 fn temp_dir() -> crate::testutil::TempDir {
256 crate::testutil::TempDir::new("cli-order")
257 }
258
259 async fn seed_order(database: &Arc<Database>, profile: &str) -> Order {
260 let (account, _) = Account::find_or_create(
261 profile,
262 &[4, 5, 6],
263 vec![],
264 &ClientContext::default(),
265 database,
266 )
267 .await
268 .unwrap();
269 Order::create(
270 profile,
271 &account.id,
272 vec![crate::sqlite::order::Identifier::dns("example.com")],
273 crate::sqlite::nonce::now_secs() + 3600,
274 None,
275 None,
276 database,
277 )
278 .await
279 .unwrap()
280 }
281
282 async fn issue_onto(order: &mut Order, config: &Config, database: Arc<Database>) {
285 let profile = &config.resolve_profiles().unwrap()[0];
286 let resolver = crate::dns::resolver_addr(&config.dns)
287 .and_then(crate::challenge::build_resolver)
288 .expect("the default dns configuration must build a resolver");
289 let signer: Arc<dyn SignerBackend> = signer::from_config(
290 &profile.sections.signer,
291 vec![profile.name.clone()],
292 &crate::testutil::signer_parts(database.clone(), resolver),
293 &signer::CarriedState::new(),
294 )
295 .unwrap();
296
297 let key_pair = rcgen::KeyPair::generate().unwrap();
298 let params = rcgen::CertificateParams::new(vec!["example.com".to_string()]).unwrap();
299 let csr = params.serialize_request(&key_pair).unwrap();
300 let chain = match signer
301 .issue(
302 &order.id,
303 csr.der(),
304 &order.identifiers,
305 RequestedValidity::default(),
306 )
307 .await
308 .unwrap()
309 {
310 IssueOutcome::Issued(chain) => chain,
311 IssueOutcome::Processing => panic!("the local CA issues synchronously"),
312 };
313 let leaf = crate::cert::leaf_der_from_chain(&chain).unwrap();
314 let (serial, pubkey) = crate::cert::cert_serial_and_spki(&leaf).unwrap();
315 order
316 .finalize(chain, serial, pubkey, &database)
317 .await
318 .unwrap();
319 }
320
321 #[tokio::test]
322 async fn every_arm_refuses_an_unknown_order() {
323 let database = Arc::new(Database::connect_in_memory().await.unwrap());
324 let config = Config::default();
325 let expected = CliError("no such order: ord-nope".to_string());
326
327 let commands = vec![
328 OrderCommand::Show {
329 id: "ord-nope".to_string(),
330 json: false,
331 },
332 OrderCommand::Delete {
333 id: "ord-nope".to_string(),
334 },
335 OrderCommand::Revoke {
336 id: "ord-nope".to_string(),
337 reason: None,
338 },
339 ];
340 for command in commands {
341 let mut reader: &[u8] = &[];
342 let error = run_order_command(
343 command,
344 true,
345 Palette::plain(),
346 &mut reader,
347 &config,
348 database.clone(),
349 )
350 .await
351 .expect_err("an unknown order must fail");
352 assert_eq!(error, expected);
353 }
354 }
355
356 #[tokio::test]
360 async fn revoking_an_order_from_an_undefined_profile_is_refused() {
361 let dir = temp_dir();
362 let database = Arc::new(Database::connect_in_memory().await.unwrap());
363 let order = seed_order(&database, "default").await;
365 let config = config_in(&dir, "other");
366
367 let mut reader: &[u8] = &[];
368 let error = run_order_command(
369 OrderCommand::Revoke {
370 id: order.id.clone(),
371 reason: None,
372 },
373 true,
374 Palette::plain(),
375 &mut reader,
376 &config,
377 database,
378 )
379 .await
380 .expect_err("a profile this configuration does not define must be refused");
381 assert!(
382 error.to_string().contains("which this configuration"),
383 "{error}"
384 );
385 }
386
387 #[tokio::test]
389 async fn revoking_without_a_resolvable_configuration_is_refused() {
390 let database = Arc::new(Database::connect_in_memory().await.unwrap());
391 let order = seed_order(&database, "default").await;
392
393 let mut reader: &[u8] = &[];
394 let error = run_order_command(
395 OrderCommand::Revoke {
396 id: order.id,
397 reason: None,
398 },
399 true,
400 Palette::plain(),
401 &mut reader,
402 &Config::default(),
403 database,
404 )
405 .await
406 .expect_err("a configuration mounting nothing must be refused");
407 assert!(
408 error.to_string().starts_with("configuration error: "),
409 "{error}"
410 );
411 }
412
413 #[tokio::test]
414 async fn revoking_an_order_with_no_certificate_is_refused() {
415 let dir = temp_dir();
416 let database = Arc::new(Database::connect_in_memory().await.unwrap());
417 let order = seed_order(&database, "default").await;
418 let config = config_in(&dir, "default");
419
420 let mut reader: &[u8] = &[];
421 let error = run_order_command(
422 OrderCommand::Revoke {
423 id: order.id.clone(),
424 reason: None,
425 },
426 true,
427 Palette::plain(),
428 &mut reader,
429 &config,
430 database,
431 )
432 .await
433 .expect_err("there is nothing to revoke");
434 assert_eq!(
435 error,
436 CliError(format!("order {} has no issued certificate", order.id))
437 );
438 }
439
440 #[tokio::test]
443 async fn an_issued_order_revokes_once() {
444 let dir = temp_dir();
445 let database = Arc::new(Database::connect_in_memory().await.unwrap());
446 let config = config_in(&dir, "default");
447 let mut order = seed_order(&database, "default").await;
448 issue_onto(&mut order, &config, database.clone()).await;
449
450 let mut reader: &[u8] = &[];
451 run_order_command(
452 OrderCommand::Revoke {
453 id: order.id.clone(),
454 reason: Some(1),
455 },
456 true,
457 Palette::plain(),
458 &mut reader,
459 &config,
460 database.clone(),
461 )
462 .await
463 .expect("a certificate issued by this profile's CA must revoke");
464
465 assert!(
466 Order::find_by_id(&order.id, &database)
467 .await
468 .unwrap()
469 .unwrap()
470 .revoked_at
471 .is_some()
472 );
473
474 let error = run_order_command(
475 OrderCommand::Revoke {
476 id: order.id.clone(),
477 reason: None,
478 },
479 true,
480 Palette::plain(),
481 &mut reader,
482 &config,
483 database.clone(),
484 )
485 .await
486 .expect_err("a second revocation has nothing left to do");
487 assert_eq!(
488 error,
489 CliError(format!(
490 "order {}'s certificate is already revoked",
491 order.id
492 ))
493 );
494 }
495
496 #[tokio::test]
499 async fn an_invalid_revocation_reason_is_refused() {
500 let dir = temp_dir();
501 let database = Arc::new(Database::connect_in_memory().await.unwrap());
502 let config = config_in(&dir, "default");
503 let mut order = seed_order(&database, "default").await;
504 issue_onto(&mut order, &config, database.clone()).await;
505
506 let mut reader: &[u8] = &[];
507 let error = run_order_command(
508 OrderCommand::Revoke {
509 id: order.id.clone(),
510 reason: Some(7),
511 },
512 true,
513 Palette::plain(),
514 &mut reader,
515 &config,
516 database,
517 )
518 .await
519 .expect_err("7 is not a defined CRLReason");
520 assert!(error.to_string().contains('7'), "{error}");
521 }
522
523 #[tokio::test]
525 async fn a_declined_delete_is_not_a_failure() {
526 let database = Arc::new(Database::connect_in_memory().await.unwrap());
527 let order = seed_order(&database, "default").await;
528
529 let mut reader: &[u8] = b"n\n";
530 run_order_command(
531 OrderCommand::Delete {
532 id: order.id.clone(),
533 },
534 false,
535 Palette::plain(),
536 &mut reader,
537 &Config::default(),
538 database.clone(),
539 )
540 .await
541 .unwrap();
542
543 assert!(
544 Order::find_by_id(&order.id, &database)
545 .await
546 .unwrap()
547 .is_some()
548 );
549 }
550
551 #[tokio::test]
554 async fn the_json_arms_render() {
555 let database = Arc::new(Database::connect_in_memory().await.unwrap());
556 let order = seed_order(&database, "default").await;
557
558 let mut reader: &[u8] = &[];
559 for command in [
560 OrderCommand::List {
561 profile: Some("default".to_string()),
562 account_id: None,
563 status: None,
564 json: true,
565 },
566 OrderCommand::Show {
567 id: order.id.clone(),
568 json: true,
569 },
570 OrderCommand::Show {
571 id: order.id.clone(),
572 json: false,
573 },
574 ] {
575 run_order_command(
576 command,
577 true,
578 Palette::plain(),
579 &mut reader,
580 &Config::default(),
581 database.clone(),
582 )
583 .await
584 .unwrap();
585 }
586 }
587
588 #[tokio::test]
594 async fn an_unknown_status_is_refused_by_name_rather_than_matching_nothing() {
595 let database = Arc::new(Database::connect_in_memory().await.unwrap());
596 seed_order(&database, "default").await;
597
598 let mut reader: &[u8] = &[];
599 let error = run_order_command(
600 OrderCommand::List {
601 profile: None,
602 account_id: None,
603 status: Some("readyy".to_string()),
604 json: false,
605 },
606 true,
607 Palette::plain(),
608 &mut reader,
609 &Config::default(),
610 database.clone(),
611 )
612 .await
613 .unwrap_err();
614
615 assert!(error.0.contains("--status"), "{error}");
616 assert!(error.0.contains("`readyy`"), "{error}");
617 assert!(
619 error
620 .0
621 .contains("pending, ready, processing, valid, invalid"),
622 "{error}"
623 );
624 }
625
626 #[tokio::test]
631 async fn every_order_status_is_accepted_as_a_filter() {
632 let database = Arc::new(Database::connect_in_memory().await.unwrap());
633 seed_order(&database, "default").await;
634
635 let mut reader: &[u8] = &[];
636 for status in OrderStatus::ALL {
637 run_order_command(
638 OrderCommand::List {
639 profile: None,
640 account_id: None,
641 status: Some(status.as_str().to_string()),
642 json: false,
643 },
644 true,
645 Palette::plain(),
646 &mut reader,
647 &Config::default(),
648 database.clone(),
649 )
650 .await
651 .unwrap_or_else(|error| panic!("--status {status} was refused: {error}"));
652 }
653 }
654}