1use std::io::BufRead;
11use std::sync::Arc;
12
13use clap::{Args, Subcommand};
14
15use crate::cli::CliError;
16use crate::cli::render;
17use crate::cli::window::{DEFAULT_LIMIT, Window};
18use acme_proxy_admin::admin;
19use acme_proxy_admin::admin::DeleteOutcome;
20use acme_proxy_core::config::Config;
21use acme_proxy_core::palette::Palette;
22use acme_proxy_signer as signer;
23use acme_proxy_store::db::Database;
24use acme_proxy_store::order::Order;
25use acme_proxy_store::order::OrderQuery;
26use acme_proxy_store::status::OrderStatus;
27
28#[derive(Args)]
31pub struct OrderListArgs {
32 #[arg(long)]
34 pub profile: Option<String>,
35 #[arg(long = "account-id")]
37 pub account_id: Option<String>,
38 #[arg(long)]
41 pub status: Option<String>,
42 #[arg(long)]
44 pub identifier: Option<String>,
45 #[arg(long = "identifier-contains", conflicts_with = "identifier")]
48 pub identifier_contains: Option<String>,
49 #[arg(long = "cert-serial")]
53 pub cert_serial: Option<String>,
54 #[arg(long = "expiring-in")]
57 pub expiring_in: Option<u64>,
58 #[arg(long = "hide-superseded")]
61 pub hide_superseded: bool,
62 #[arg(long, default_value_t = DEFAULT_LIMIT)]
64 pub limit: i64,
65 #[arg(long, default_value_t = 0)]
67 pub offset: i64,
68 #[arg(long)]
70 pub json: bool,
71}
72
73#[derive(Subcommand)]
74pub enum OrderCommand {
75 List(OrderListArgs),
77 Show {
79 id: String,
81 #[arg(long)]
83 json: bool,
84 },
85 Chain {
87 id: String,
89 },
90 Delete {
92 id: String,
94 },
95 Revoke {
97 id: String,
99 #[arg(long)]
102 reason: Option<u32>,
103 #[arg(long, default_value_t = DEFAULT_REVOKE_WAIT_SECONDS)]
108 wait: u64,
109 },
110}
111
112pub async fn run_order_command(
113 command: OrderCommand,
114 yes: bool,
115 palette: Palette,
116 reader: &mut impl BufRead,
117 config: &Config,
118 database: Arc<Database>,
119) -> Result<(), CliError> {
120 match command {
121 OrderCommand::List(args) => run_list(args, palette, config, database).await?,
122 OrderCommand::Show { id, json } => match admin::load_order_detail(&id, database).await? {
123 None => return Err(not_found(&id)),
124 Some(detail) if json => {
125 println!(
126 "{}",
127 admin::render_order_detail_json(&detail, &config.server.base_url)
128 );
129 }
130 Some(detail) => print!("{}", render::render_order_detail_text(&detail, palette)),
131 },
132 OrderCommand::Chain { id } => {
133 let Some(order) = Order::find_by_id(&id, &database).await? else {
139 return Err(not_found(&id));
140 };
141 let Some(pem) = order.certificate else {
145 return Err(CliError::bad_request(format!(
146 "order {id} has no certificate: it has not been finalized"
147 )));
148 };
149 print!("{pem}");
152 }
153 OrderCommand::Delete { id } => {
154 let doomed = Order::find_by_id(&id, &database).await?;
159 match admin::confirm_delete_order(&id, yes, reader, database.clone()).await? {
160 DeleteOutcome::NotFound => return Err(not_found(&id)),
161 DeleteOutcome::LiveCertificates(live) => {
162 return Err(CliError::bad_request(admin::live_certificates_refusal(
163 &format!("order {id}"),
164 live,
165 )));
166 }
167 DeleteOutcome::Cancelled => println!("Cancelled."),
168 DeleteOutcome::Deleted(deleted) => {
169 if let Some(order) = doomed {
170 acme_proxy_jobs::auditor::admin::record_cli_action(
171 &database,
172 |actor, client| {
173 acme_proxy_jobs::auditor::admin::order_deleted(
174 actor,
175 client,
176 &order,
177 deleted.cascaded,
178 )
179 },
180 )
181 .await;
182 }
183 println!(
184 "Deleted order {id} ({} authorization(s) cascaded).",
185 deleted.cascaded
186 );
187 }
188 }
189 }
190 OrderCommand::Revoke { id, reason, wait } => {
191 let Some(order) = Order::find_by_id(&id, &database).await? else {
194 return Err(not_found(&id));
195 };
196 let profiles = config
197 .resolve_profiles()
198 .map_err(|error| CliError::failed(format!("configuration error: {error}")))?;
199 let Some(profile) = profiles.iter().find(|p| p.name == order.profile) else {
200 return Err(CliError::bad_request(format!(
201 "order {id} was issued by profile `{}`, which this configuration does not \
202 define — revoking it needs the endpoint that signed it",
203 order.profile
204 )));
205 };
206 if order.certificate.is_none() {
211 return Err(CliError::bad_request(format!(
212 "order {id} has no issued certificate"
213 )));
214 }
215 let notifiers = super::offline_notifiers(config, database.clone())?;
218 let notify = notifiers
219 .get(&order.profile)
220 .map(|dispatcher| dispatcher.as_ref());
221 let audit = acme_proxy_jobs::auditor::Auditor::offline(database.clone());
222 let jobs = acme_proxy_jobs::jobs::JobQueue::new(database.clone(), &config.jobs);
225 let route = signer::revocation_route(&profile.sections.signer)
226 .map_err(|error| CliError::failed(format!("signer error: {error}")))?;
227 let (actor, client) = (
230 acme_proxy_core::audit::Actor::cli(),
231 acme_proxy_core::audit::ClientContext::default(),
232 );
233
234 let revoker = acme_proxy_protocol::acme::revoke::Revoker::for_route(
238 &route,
239 &jobs,
240 std::time::Duration::from_secs(wait),
241 );
242 let outcome = admin::revoke_order(
243 &id,
244 reason,
245 actor,
246 client,
247 acme_proxy_protocol::acme::revoke::Revocations {
248 database: &database,
249 audit: &audit,
250 notify,
251 revoker,
252 },
253 )
254 .await;
255 match outcome.map_err(|error| match error {
258 admin::RevokeError::BadReason(_) => CliError::bad_request(error.to_string()),
259 admin::RevokeError::Abandoned { job, reason } => CliError::failed(format!(
260 "the revocation of order {id} failed (job {job}): {reason}"
261 )),
262 other => CliError::failed(other.to_string()),
263 })? {
264 admin::RevokeOutcome::NotFound => return Err(not_found(&id)),
265 admin::RevokeOutcome::NotIssued => {
266 return Err(CliError::bad_request(format!(
267 "order {id} has no issued certificate"
268 )));
269 }
270 admin::RevokeOutcome::AlreadyRevoked => {
271 return Err(CliError::bad_request(format!(
272 "order {id}'s certificate is already revoked"
273 )));
274 }
275 admin::RevokeOutcome::Queued(job) => println!(
278 "Revocation of order {id} queued as job {job}; a running server performs it \
279 (acme-proxy jobs show {job})."
280 ),
281 admin::RevokeOutcome::Revoked(order) => {
282 println!("{}", render::render_order_line(&order, palette));
283 if let signer::RevocationRoute::Ledger { issuer } = &route {
284 let job = acme_proxy_store::job::Job::find_live(
285 acme_proxy_signer::local_ca::sweep::CRL_REGENERATE_KIND,
286 issuer,
287 &database,
288 )
289 .await?;
290 match job {
291 Some(job) => println!(
292 "CRL regeneration queued (job {}); a running server signs it.",
293 job.id
294 ),
295 None => println!("CRL regeneration already done."),
296 }
297 }
298 }
299 }
300 }
301 }
302 Ok(())
303}
304
305const DEFAULT_REVOKE_WAIT_SECONDS: u64 = 30;
307
308async fn run_list(
310 args: OrderListArgs,
311 palette: Palette,
312 config: &Config,
313 database: Arc<Database>,
314) -> Result<(), CliError> {
315 let OrderListArgs {
316 profile,
317 account_id,
318 status,
319 identifier,
320 identifier_contains,
321 cert_serial,
322 expiring_in,
323 hide_superseded,
324 limit,
325 offset,
326 json,
327 } = args;
328 let window = Window::resolve(limit, offset);
329
330 if let Some(days) = expiring_in {
338 refuse_plain_listing_filters(
339 account_id.as_deref(),
340 status.as_deref(),
341 identifier.as_deref(),
342 identifier_contains.as_deref(),
343 cert_serial.as_deref(),
344 )?;
345 return run_expiring(
346 days,
347 profile,
348 hide_superseded,
349 window,
350 json,
351 palette,
352 database,
353 )
354 .await;
355 }
356 if hide_superseded {
357 return Err(CliError::bad_request(
358 "--hide-superseded needs --expiring-in: it filters on the supersession \
359 annotation, which only the expiry listing carries"
360 .to_string(),
361 ));
362 }
363
364 let status = super::parse_flag::<OrderStatus>("--status", status)?;
368
369 let query = OrderQuery {
374 profile,
375 account_id,
376 status,
377 identifier,
378 identifier_contains,
379 cert_serial: cert_serial
383 .as_deref()
384 .map(acme_proxy_core::cert::normalize_serial),
385 limit: window.limit,
386 offset: window.offset,
387 };
388 let (orders, total) = Order::search(&query, &database).await?;
389 if json {
397 let rendered = admin::orders_json(&orders, &config.server.base_url, &database).await?;
398 println!("{}", render::json_page(rendered, total, window));
399 } else {
400 for order in &orders {
401 println!("{}", render::render_order_line(order, palette));
402 }
403 render::print_footer(orders.len(), total);
404 }
405 Ok(())
406}
407
408async fn run_expiring(
423 days: u64,
424 profile: Option<String>,
425 hide_superseded: bool,
426 window: Window,
427 json: bool,
428 palette: Palette,
429 database: Arc<Database>,
430) -> Result<(), CliError> {
431 let query = acme_proxy_store::expiring::ExpiringQuery {
432 profile,
433 before: acme_proxy_store::expiring::expiring_horizon(days),
434 include_superseded: !hide_superseded,
435 limit: window.limit,
436 offset: window.offset,
437 };
438 let (entries, total, hidden) =
439 acme_proxy_store::expiring::list_expiring(&query, database).await?;
440 if json {
441 let items = entries.iter().map(admin::render_expiring_json).collect();
442 let mut envelope = render::json_page(items, total, window);
443 if let Some(object) = envelope.as_object_mut() {
444 object.insert("hidden".to_string(), serde_json::json!(hidden));
448 object.insert("days".to_string(), serde_json::json!(days));
449 }
450 println!("{envelope}");
451 } else {
452 for entry in &entries {
453 println!("{}", render::render_expiring_line(entry, palette));
454 }
455 render::print_expiring_footer(entries.len(), total, hidden);
456 }
457 Ok(())
458}
459
460fn refuse_plain_listing_filters(
464 account_id: Option<&str>,
465 status: Option<&str>,
466 identifier: Option<&str>,
467 identifier_contains: Option<&str>,
468 cert_serial: Option<&str>,
469) -> Result<(), CliError> {
470 if status.is_some() {
471 return Err(CliError::bad_request(
472 "--status does not apply with --expiring-in: the expiry listing is issued, \
473 unrevoked certificates by definition, so a status filter here would mean \
474 something other than it does everywhere else"
475 .to_string(),
476 ));
477 }
478 if account_id.is_some() {
479 return Err(CliError::bad_request(
480 "--account-id does not apply with --expiring-in: the expiry listing has no \
481 account predicate, and answering as though it did would report one \
482 subscriber's certificates as every subscriber's"
483 .to_string(),
484 ));
485 }
486 if identifier.is_some() || identifier_contains.is_some() || cert_serial.is_some() {
487 return Err(CliError::bad_request(
488 "--identifier, --identifier-contains and --cert-serial do not apply with \
489 --expiring-in: the expiry listing is ordered by expiry over a fixed status \
490 set, so a name or serial filter here would mean something other than it \
491 does on the plain listing"
492 .to_string(),
493 ));
494 }
495 Ok(())
496}
497
498fn not_found(id: &str) -> CliError {
499 CliError::bad_request(acme_proxy_admin::admin::subject::Subject::Order.missing(id))
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505 use crate::cli::CliErrorKind;
506 use acme_proxy_core::audit::ClientContext;
507 use acme_proxy_signer::IssueOutcome;
508 use acme_proxy_signer::RequestedValidity;
509 use acme_proxy_signer::SignerBackend;
510 use acme_proxy_store::account::Account;
511
512 fn config_in(dir: impl AsRef<std::path::Path>, profile: &str) -> Config {
516 let dir = dir.as_ref();
517 let _lock = acme_proxy_core::config::ENV_LOCK
518 .lock()
519 .unwrap_or_else(std::sync::PoisonError::into_inner);
520 let ca = dir.join("ca");
521 std::fs::write(
522 dir.join("config.toml"),
523 format!(
524 r#"
525 [profiles.{profile}]
526 signer.local_ca.cert_path = "{ca}.pem"
527 signer.local_ca.key_path = "{ca}.key"
528 signer.local_ca.crl_path = "{ca}.crl"
529 "#,
530 ca = ca.display(),
531 ),
532 )
533 .unwrap();
534 unsafe {
537 std::env::set_var("ACME_PROXY_CONFIG", dir.join("config").to_str().unwrap());
538 }
539 let config = Config::load().expect("the configuration must load");
540 unsafe {
541 std::env::remove_var("ACME_PROXY_CONFIG");
542 }
543 config
544 }
545
546 fn temp_dir() -> acme_proxy_core::testutil::TempDir {
547 acme_proxy_core::testutil::TempDir::new("cli-order")
548 }
549
550 #[tokio::test]
557 async fn deleting_an_order_writes_a_row_and_a_decline_does_not() {
558 use acme_proxy_store::audit::AuditEntry;
559 use acme_proxy_store::audit::AuditQuery;
560
561 let database = Arc::new(Database::connect_in_memory().await.unwrap());
562 let config = Config::default();
563 let order = seed_order(&database, "default").await;
564 let id = order.id.to_string();
565
566 let mut declined: &[u8] = b"n\n";
567 run_order_command(
568 OrderCommand::Delete { id: id.clone() },
569 false,
570 Palette::plain(),
571 &mut declined,
572 &config,
573 database.clone(),
574 )
575 .await
576 .unwrap();
577 assert_eq!(
578 AuditEntry::search(&AuditQuery::default(), &database)
579 .await
580 .unwrap()
581 .1,
582 0,
583 "a declined delete is not an administrative action"
584 );
585
586 let mut reader: &[u8] = &[];
587 run_order_command(
588 OrderCommand::Delete { id: id.clone() },
589 true,
590 Palette::plain(),
591 &mut reader,
592 &config,
593 database.clone(),
594 )
595 .await
596 .unwrap();
597
598 let (rows, total) = AuditEntry::search(
599 &AuditQuery {
600 limit: 5,
601 ..AuditQuery::default()
602 },
603 &database,
604 )
605 .await
606 .unwrap();
607 assert_eq!(total, 1);
608 assert_eq!(rows[0].event, "order_deleted");
609 assert_eq!(rows[0].actor_kind, "cli");
610 assert_eq!(rows[0].profile, "default");
611 assert_eq!(rows[0].order_id.as_deref(), Some(id.as_str()));
612 assert!(
615 Order::find_by_id(&id, &database).await.unwrap().is_none(),
616 "the order really went"
617 );
618 }
619
620 async fn seed_order(database: &Arc<Database>, profile: &str) -> Order {
621 let (account, _) = Account::find_or_create(
622 profile,
623 &[4, 5, 6],
624 vec![],
625 &ClientContext::default(),
626 database,
627 )
628 .await
629 .unwrap();
630 Order::create(
631 profile,
632 account.id,
633 vec![acme_proxy_core::identifier::Identifier::dns("example.com")],
634 acme_proxy_store::nonce::now_secs() + 3600,
635 None,
636 None,
637 database,
638 )
639 .await
640 .unwrap()
641 }
642
643 async fn issue_onto(
648 order: &mut Order,
649 config: &Config,
650 database: Arc<Database>,
651 ) -> Arc<dyn SignerBackend> {
652 let profile = &config.resolve_profiles().unwrap()[0];
653 let resolver = acme_proxy_net::dns::resolver_addr(&config.dns)
654 .and_then(acme_proxy_net::challenge::build_resolver)
655 .expect("the default dns configuration must build a resolver");
656 let signer: Arc<dyn SignerBackend> = signer::from_config(
657 &profile.sections.signer,
658 &acme_proxy_signer::testutil::signer_parts(database.clone(), resolver),
659 )
660 .unwrap();
661
662 let key_pair = rcgen::KeyPair::generate().unwrap();
663 let params = rcgen::CertificateParams::new(vec!["example.com".to_string()]).unwrap();
664 let csr = params.serialize_request(&key_pair).unwrap();
665 let chain = match signer
666 .issue(
667 order.id.to_string().as_str(),
668 csr.der(),
669 &order.identifiers,
670 RequestedValidity::default(),
671 )
672 .await
673 .unwrap()
674 {
675 IssueOutcome::Issued(chain) => chain,
676 IssueOutcome::Processing => panic!("the local CA issues synchronously"),
677 };
678 let leaf = acme_proxy_core::cert::leaf_der_from_chain(&chain).unwrap();
679 let (serial, pubkey) = acme_proxy_core::cert::cert_serial_and_spki(&leaf).unwrap();
680 let not_after = acme_proxy_core::cert::cert_validity(&leaf)
681 .ok()
682 .map(|(_, na)| na);
683 order
684 .finalize(chain, serial, pubkey, not_after, &database)
685 .await
686 .unwrap();
687 signer
688 }
689
690 #[tokio::test]
691 async fn every_arm_refuses_an_unknown_order() {
692 let database = Arc::new(Database::connect_in_memory().await.unwrap());
693 let config = Config::default();
694 let expected = CliError::bad_request("no such order: ord-nope".to_string());
695
696 let commands = vec![
697 OrderCommand::Show {
698 id: "ord-nope".to_string(),
699 json: false,
700 },
701 OrderCommand::Chain {
702 id: "ord-nope".to_string(),
703 },
704 OrderCommand::Delete {
705 id: "ord-nope".to_string(),
706 },
707 OrderCommand::Revoke {
708 id: "ord-nope".to_string(),
709 reason: None,
710 wait: 0,
711 },
712 ];
713 for command in commands {
714 let mut reader: &[u8] = &[];
715 let error = run_order_command(
716 command,
717 true,
718 Palette::plain(),
719 &mut reader,
720 &config,
721 database.clone(),
722 )
723 .await
724 .expect_err("an unknown order must fail");
725 assert_eq!(error, expected);
726 }
727 }
728
729 #[tokio::test]
734 async fn chain_prints_the_issued_pem_and_refuses_an_order_with_none() {
735 let dir = temp_dir();
736 let config = config_in(&dir, "default");
737 let database = Arc::new(Database::connect_in_memory().await.unwrap());
738 let mut order = seed_order(&database, "default").await;
739
740 let mut reader: &[u8] = &[];
741 let error = run_order_command(
742 OrderCommand::Chain {
743 id: order.id.to_string(),
744 },
745 true,
746 Palette::plain(),
747 &mut reader,
748 &config,
749 database.clone(),
750 )
751 .await
752 .expect_err("an order with no certificate has no chain to print");
753 assert_eq!(
754 error,
755 CliError::bad_request(format!(
756 "order {} has no certificate: it has not been finalized",
757 order.id
758 ))
759 );
760
761 issue_onto(&mut order, &config, database.clone()).await;
762
763 let stored = Order::find_by_id(order.id.to_string().as_str(), &database)
767 .await
768 .unwrap()
769 .unwrap()
770 .certificate
771 .expect("finalize stored the chain");
772 assert!(stored.starts_with("-----BEGIN CERTIFICATE-----"));
773
774 let mut reader: &[u8] = &[];
775 run_order_command(
776 OrderCommand::Chain {
777 id: order.id.to_string(),
778 },
779 true,
780 Palette::plain(),
781 &mut reader,
782 &config,
783 database,
784 )
785 .await
786 .unwrap();
787 }
788
789 #[tokio::test]
793 async fn revoking_an_order_from_an_undefined_profile_is_refused() {
794 let dir = temp_dir();
795 let database = Arc::new(Database::connect_in_memory().await.unwrap());
796 let order = seed_order(&database, "default").await;
798 let config = config_in(&dir, "other");
799
800 let mut reader: &[u8] = &[];
801 let error = run_order_command(
802 OrderCommand::Revoke {
803 id: order.id.to_string(),
804 reason: None,
805 wait: 0,
806 },
807 true,
808 Palette::plain(),
809 &mut reader,
810 &config,
811 database,
812 )
813 .await
814 .expect_err("a profile this configuration does not define must be refused");
815 assert!(
816 error.to_string().contains("which this configuration"),
817 "{error}"
818 );
819 }
820
821 #[tokio::test]
823 async fn revoking_without_a_resolvable_configuration_is_refused() {
824 let database = Arc::new(Database::connect_in_memory().await.unwrap());
825 let order = seed_order(&database, "default").await;
826
827 let mut reader: &[u8] = &[];
828 let error = run_order_command(
829 OrderCommand::Revoke {
830 id: order.id.to_string(),
831 reason: None,
832 wait: 0,
833 },
834 true,
835 Palette::plain(),
836 &mut reader,
837 &Config::default(),
838 database,
839 )
840 .await
841 .expect_err("a configuration mounting nothing must be refused");
842 assert!(
843 error.to_string().starts_with("configuration error: "),
844 "{error}"
845 );
846 }
847
848 #[tokio::test]
849 async fn revoking_an_order_with_no_certificate_is_refused() {
850 let dir = temp_dir();
851 let database = Arc::new(Database::connect_in_memory().await.unwrap());
852 let order = seed_order(&database, "default").await;
853 let config = config_in(&dir, "default");
854
855 let mut reader: &[u8] = &[];
856 let error = run_order_command(
857 OrderCommand::Revoke {
858 id: order.id.to_string(),
859 reason: None,
860 wait: 0,
861 },
862 true,
863 Palette::plain(),
864 &mut reader,
865 &config,
866 database,
867 )
868 .await
869 .expect_err("there is nothing to revoke");
870 assert_eq!(
871 error,
872 CliError::bad_request(format!("order {} has no issued certificate", order.id))
873 );
874 }
875
876 #[tokio::test]
881 async fn an_issued_order_revokes_once() {
882 use acme_proxy_jobs::jobs::JobHandler;
883 use acme_proxy_signer::local_ca::sweep::CRL_REGENERATE_KIND;
884 use acme_proxy_signer::local_ca::sweep::CrlRegenerateJob;
885 use acme_proxy_store::job::Job;
886
887 let dir = temp_dir();
888 let database = Arc::new(Database::connect_in_memory().await.unwrap());
889 let config = config_in(&dir, "default");
890 let mut order = seed_order(&database, "default").await;
891 let ca = issue_onto(&mut order, &config, database.clone()).await;
892 ca.crl_refresher().unwrap().refresh().await.unwrap();
894
895 let mut reader: &[u8] = &[];
896 run_order_command(
897 OrderCommand::Revoke {
898 id: order.id.to_string(),
899 reason: Some(1),
900 wait: 0,
901 },
902 true,
903 Palette::plain(),
904 &mut reader,
905 &config,
906 database.clone(),
907 )
908 .await
909 .expect("a certificate issued by this profile's CA must revoke");
910
911 let revoked = Order::find_by_id(order.id.to_string().as_str(), &database)
912 .await
913 .unwrap()
914 .unwrap();
915 assert!(revoked.revoked_at.is_some());
916
917 let serial = revoked.cert_serial.clone().unwrap();
919 let lists = |der: &[u8]| {
920 use x509_parser::prelude::FromDer;
921 let (_, crl) =
922 x509_parser::revocation_list::CertificateRevocationList::from_der(der).unwrap();
923 crl.iter_revoked_certificates()
924 .any(|entry| hex::encode(entry.raw_serial()).eq_ignore_ascii_case(&serial))
925 };
926 assert!(!lists(&ca.info().crl_der().await.unwrap().unwrap()));
927 let refresher = ca.crl_refresher().unwrap();
928 let job = Job::find_live(CRL_REGENERATE_KIND, refresher.issuer(), &database)
929 .await
930 .unwrap()
931 .expect("the revocation queued its CRL");
932 let handler = CrlRegenerateJob::new(vec![refresher]);
933 assert!(matches!(
934 handler.run(&job).await,
935 acme_proxy_jobs::jobs::JobOutcome::Done
936 ));
937 assert!(lists(&ca.info().crl_der().await.unwrap().unwrap()));
938
939 let error = run_order_command(
940 OrderCommand::Revoke {
941 id: order.id.to_string(),
942 reason: None,
943 wait: 0,
944 },
945 true,
946 Palette::plain(),
947 &mut reader,
948 &config,
949 database.clone(),
950 )
951 .await
952 .expect_err("a second revocation has nothing left to do");
953 assert_eq!(
954 error,
955 CliError::bad_request(format!(
956 "order {}'s certificate is already revoked",
957 order.id
958 ))
959 );
960 }
961
962 #[tokio::test]
966 async fn revoking_against_a_ca_no_server_has_initialised_is_refused() {
967 let dir = temp_dir();
968 let database = Arc::new(Database::connect_in_memory().await.unwrap());
969 let config = config_in(&dir, "default");
970 let mut order = seed_order(&database, "default").await;
971 issue_onto(&mut order, &config, database.clone()).await;
972
973 let mut reader: &[u8] = &[];
974 let error = run_order_command(
975 OrderCommand::Revoke {
976 id: order.id.to_string(),
977 reason: None,
978 wait: 0,
979 },
980 true,
981 Palette::plain(),
982 &mut reader,
983 &config,
984 database.clone(),
985 )
986 .await
987 .expect_err("an uninitialised CA must not take a revocation");
988 assert_eq!(error.kind(), crate::cli::CliErrorKind::Failed);
989 assert!(error.to_string().contains("acme-proxy serve"), "{error}");
990 assert!(
991 Order::find_by_id(order.id.to_string().as_str(), &database)
992 .await
993 .unwrap()
994 .unwrap()
995 .revoked_at
996 .is_none()
997 );
998 }
999
1000 #[tokio::test]
1006 async fn a_delegated_revocation_is_queued_for_the_server() {
1007 use acme_proxy_jobs::jobs::JobHandler;
1008 use acme_proxy_jobs::jobs::JobOutcome;
1009 use acme_proxy_protocol::acme::revoke::SIGNER_REVOKE_KIND;
1010 use acme_proxy_protocol::acme::revoke::SignerRevokeJob;
1011 use acme_proxy_store::job::Job;
1012
1013 let dir = temp_dir();
1014 let marker = dir.join("revoked");
1015 let script = acme_proxy_core::testutil::write_script(
1016 &dir,
1017 "signer.sh",
1018 &format!(
1019 "#!/bin/sh\n[ \"$ACME_SIGNER_HOOK\" = revoke ] && echo once >> {}\nexit 0\n",
1020 marker.display()
1021 ),
1022 );
1023 let config = {
1024 let _lock = acme_proxy_core::config::ENV_LOCK
1025 .lock()
1026 .unwrap_or_else(std::sync::PoisonError::into_inner);
1027 std::fs::write(
1028 dir.join("config.toml"),
1029 format!(
1030 "[profiles.default]\nsigner.backend = \"custom\"\nsigner.custom.script_path = \"{}\"\n",
1031 script.display()
1032 ),
1033 )
1034 .unwrap();
1035 unsafe {
1038 std::env::set_var("ACME_PROXY_CONFIG", dir.join("config").to_str().unwrap());
1039 }
1040 let config = Config::load().expect("the configuration must load");
1041 unsafe {
1042 std::env::remove_var("ACME_PROXY_CONFIG");
1043 }
1044 config
1045 };
1046 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1047 let account = seed_order(&database, "default").await.account_id;
1048 let order = acme_proxy_store::testutil::issued_order(
1049 &database,
1050 "default",
1051 account,
1052 &["example.com"],
1053 30,
1054 )
1055 .await;
1056 let id = order.id.to_string();
1057
1058 for _ in 0..2 {
1059 let mut reader: &[u8] = &[];
1060 run_order_command(
1061 OrderCommand::Revoke {
1062 id: id.clone(),
1063 reason: Some(4),
1064 wait: 0,
1065 },
1066 true,
1067 Palette::plain(),
1068 &mut reader,
1069 &config,
1070 database.clone(),
1071 )
1072 .await
1073 .expect("a queued revocation is not a failure");
1074 }
1075 assert_eq!(
1076 Job::count_live(SIGNER_REVOKE_KIND, &database)
1077 .await
1078 .unwrap(),
1079 1
1080 );
1081 assert!(
1082 Order::find_by_id(&id, &database)
1083 .await
1084 .unwrap()
1085 .unwrap()
1086 .revoked_at
1087 .is_none()
1088 );
1089 assert!(!marker.exists(), "the CLI must not run the script itself");
1090
1091 let profile = &config.resolve_profiles().unwrap()[0];
1092 let resolver = acme_proxy_net::dns::resolver_addr(&config.dns)
1093 .and_then(acme_proxy_net::challenge::build_resolver)
1094 .unwrap();
1095 let signer = signer::from_config(
1096 &profile.sections.signer,
1097 &acme_proxy_signer::testutil::signer_parts(database.clone(), resolver),
1098 )
1099 .unwrap();
1100 let handler = SignerRevokeJob::new(
1101 database.clone(),
1102 Arc::new(acme_proxy_jobs::auditor::Auditor::offline(database.clone())),
1103 vec![("default".to_string(), signer)],
1104 std::collections::HashMap::new().into(),
1105 );
1106 let job = Job::find_live(SIGNER_REVOKE_KIND, &id, &database)
1107 .await
1108 .unwrap()
1109 .unwrap();
1110 assert!(matches!(handler.run(&job).await, JobOutcome::Done));
1111
1112 let revoked = Order::find_by_id(&id, &database).await.unwrap().unwrap();
1113 assert_eq!(revoked.revocation_reason, Some(4));
1114 assert_eq!(std::fs::read_to_string(&marker).unwrap().lines().count(), 1);
1115 let (rows, _) = acme_proxy_store::audit::AuditEntry::search(
1117 &acme_proxy_store::audit::AuditQuery {
1118 limit: 5,
1119 ..acme_proxy_store::audit::AuditQuery::default()
1120 },
1121 &database,
1122 )
1123 .await
1124 .unwrap();
1125 assert_eq!(rows[0].event, "certificate_revoked");
1126 assert_eq!(rows[0].actor_kind, "cli");
1127 }
1128
1129 #[tokio::test]
1132 async fn an_invalid_revocation_reason_is_refused() {
1133 let dir = temp_dir();
1134 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1135 let config = config_in(&dir, "default");
1136 let mut order = seed_order(&database, "default").await;
1137 issue_onto(&mut order, &config, database.clone()).await;
1138
1139 let mut reader: &[u8] = &[];
1140 let error = run_order_command(
1141 OrderCommand::Revoke {
1142 id: order.id.to_string(),
1143 reason: Some(7),
1144 wait: 0,
1145 },
1146 true,
1147 Palette::plain(),
1148 &mut reader,
1149 &config,
1150 database,
1151 )
1152 .await
1153 .expect_err("7 is not a defined CRLReason");
1154 assert!(error.to_string().contains('7'), "{error}");
1155 assert_eq!(error.kind(), CliErrorKind::BadRequest);
1156 }
1157
1158 #[tokio::test]
1160 async fn a_declined_delete_is_not_a_failure() {
1161 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1162 let order = seed_order(&database, "default").await;
1163
1164 let mut reader: &[u8] = b"n\n";
1165 run_order_command(
1166 OrderCommand::Delete {
1167 id: order.id.to_string(),
1168 },
1169 false,
1170 Palette::plain(),
1171 &mut reader,
1172 &Config::default(),
1173 database.clone(),
1174 )
1175 .await
1176 .unwrap();
1177
1178 assert!(
1179 Order::find_by_id(order.id.to_string().as_str(), &database)
1180 .await
1181 .unwrap()
1182 .is_some()
1183 );
1184 }
1185
1186 #[tokio::test]
1189 async fn the_json_arms_render() {
1190 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1191 let order = seed_order(&database, "default").await;
1192
1193 let mut reader: &[u8] = &[];
1194 for command in [
1195 OrderCommand::List(OrderListArgs {
1196 profile: Some("default".to_string()),
1197 account_id: None,
1198 status: None,
1199 identifier: None,
1200 identifier_contains: None,
1201 cert_serial: None,
1202 expiring_in: None,
1203 hide_superseded: false,
1204 limit: DEFAULT_LIMIT,
1205 offset: 0,
1206 json: true,
1207 }),
1208 OrderCommand::List(OrderListArgs {
1210 profile: None,
1211 account_id: None,
1212 status: None,
1213 identifier: Some("seeded.example.com".to_string()),
1214 identifier_contains: None,
1215 cert_serial: None,
1216 expiring_in: None,
1217 hide_superseded: false,
1218 limit: DEFAULT_LIMIT,
1219 offset: 0,
1220 json: true,
1221 }),
1222 OrderCommand::List(OrderListArgs {
1223 profile: None,
1224 account_id: None,
1225 status: None,
1226 identifier: None,
1227 identifier_contains: Some("example".to_string()),
1228 cert_serial: Some("deadbeef".to_string()),
1229 expiring_in: None,
1230 hide_superseded: false,
1231 limit: DEFAULT_LIMIT,
1232 offset: 0,
1233 json: false,
1234 }),
1235 OrderCommand::Show {
1236 id: order.id.to_string(),
1237 json: true,
1238 },
1239 OrderCommand::Show {
1240 id: order.id.to_string(),
1241 json: false,
1242 },
1243 ] {
1244 run_order_command(
1245 command,
1246 true,
1247 Palette::plain(),
1248 &mut reader,
1249 &Config::default(),
1250 database.clone(),
1251 )
1252 .await
1253 .unwrap();
1254 }
1255 }
1256
1257 #[tokio::test]
1263 async fn an_unknown_status_is_refused_by_name_rather_than_matching_nothing() {
1264 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1265 seed_order(&database, "default").await;
1266
1267 let mut reader: &[u8] = &[];
1268 let error = run_order_command(
1269 OrderCommand::List(OrderListArgs {
1270 profile: None,
1271 account_id: None,
1272 status: Some("readyy".to_string()),
1273 identifier: None,
1274 identifier_contains: None,
1275 cert_serial: None,
1276 expiring_in: None,
1277 hide_superseded: false,
1278 limit: DEFAULT_LIMIT,
1279 offset: 0,
1280 json: false,
1281 }),
1282 true,
1283 Palette::plain(),
1284 &mut reader,
1285 &Config::default(),
1286 database.clone(),
1287 )
1288 .await
1289 .unwrap_err();
1290
1291 assert!(error.message.contains("--status"), "{error}");
1292 assert!(error.message.contains("`readyy`"), "{error}");
1293 assert_eq!(error.kind(), CliErrorKind::BadRequest);
1295 assert!(
1297 error
1298 .message
1299 .contains("pending, ready, processing, valid, invalid"),
1300 "{error}"
1301 );
1302 }
1303
1304 #[tokio::test]
1309 async fn every_order_status_is_accepted_as_a_filter() {
1310 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1311 seed_order(&database, "default").await;
1312
1313 let mut reader: &[u8] = &[];
1314 for status in OrderStatus::ALL {
1315 run_order_command(
1316 OrderCommand::List(OrderListArgs {
1317 profile: None,
1318 account_id: None,
1319 status: Some(status.as_str().to_string()),
1320 identifier: None,
1321 identifier_contains: None,
1322 cert_serial: None,
1323 expiring_in: None,
1324 hide_superseded: false,
1325 limit: DEFAULT_LIMIT,
1326 offset: 0,
1327 json: false,
1328 }),
1329 true,
1330 Palette::plain(),
1331 &mut reader,
1332 &Config::default(),
1333 database.clone(),
1334 )
1335 .await
1336 .unwrap_or_else(|error| panic!("--status {status} was refused: {error}"));
1337 }
1338 }
1339
1340 #[allow(clippy::too_many_arguments)]
1343 async fn list_with(
1344 expiring_in: Option<u64>,
1345 account_id: Option<&str>,
1346 status: Option<&str>,
1347 identifier: Option<&str>,
1348 identifier_contains: Option<&str>,
1349 cert_serial: Option<&str>,
1350 hide_superseded: bool,
1351 json: bool,
1352 database: Arc<Database>,
1353 ) -> Result<(), CliError> {
1354 let mut reader: &[u8] = &[];
1355 run_order_command(
1356 OrderCommand::List(OrderListArgs {
1357 profile: None,
1358 account_id: account_id.map(str::to_string),
1359 status: status.map(str::to_string),
1360 identifier: identifier.map(str::to_string),
1361 identifier_contains: identifier_contains.map(str::to_string),
1362 cert_serial: cert_serial.map(str::to_string),
1363 expiring_in,
1364 hide_superseded,
1365 limit: DEFAULT_LIMIT,
1366 offset: 0,
1367 json,
1368 }),
1369 true,
1370 Palette::plain(),
1371 &mut reader,
1372 &Config::default(),
1373 database,
1374 )
1375 .await
1376 }
1377
1378 #[tokio::test]
1381 async fn the_expiring_arm_lists_and_renders_both_ways() {
1382 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1383 let acct = acme_proxy_store::testutil::account_id(&database).await;
1384 acme_proxy_store::testutil::issued_order(&database, "default", acct, &["a.example.com"], 3)
1385 .await;
1386 acme_proxy_store::testutil::issued_order(&database, "default", acct, &["b.example.com"], 5)
1387 .await;
1388 acme_proxy_store::testutil::issued_order(
1390 &database,
1391 "default",
1392 acct,
1393 &["a.example.com"],
1394 90,
1395 )
1396 .await;
1397
1398 for json in [false, true] {
1399 list_with(
1400 Some(30),
1401 None,
1402 None,
1403 None,
1404 None,
1405 None,
1406 false,
1407 json,
1408 database.clone(),
1409 )
1410 .await
1411 .unwrap();
1412 list_with(
1414 Some(30),
1415 None,
1416 None,
1417 None,
1418 None,
1419 None,
1420 true,
1421 json,
1422 database.clone(),
1423 )
1424 .await
1425 .unwrap();
1426 }
1427 }
1428
1429 #[tokio::test]
1435 async fn both_listings_take_a_window_and_clamp_a_nonsense_one() {
1436 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1437 seed_order(&database, "default").await;
1438
1439 let mut reader: &[u8] = &[];
1440 for expiring_in in [None, Some(30)] {
1441 for (limit, offset, json) in
1442 [(1, 0, false), (1, 1, false), (1, 0, true), (0, -5, false)]
1443 {
1444 run_order_command(
1445 OrderCommand::List(OrderListArgs {
1446 profile: None,
1447 account_id: None,
1448 status: None,
1449 identifier: None,
1450 identifier_contains: None,
1451 cert_serial: None,
1452 expiring_in,
1453 hide_superseded: false,
1454 limit,
1455 offset,
1456 json,
1457 }),
1458 true,
1459 Palette::plain(),
1460 &mut reader,
1461 &Config::default(),
1462 database.clone(),
1463 )
1464 .await
1465 .unwrap_or_else(|error| {
1466 panic!(
1467 "--expiring-in {expiring_in:?} --limit {limit} --offset {offset}: {error}"
1468 )
1469 });
1470 }
1471 }
1472 }
1473
1474 #[tokio::test]
1482 async fn the_flags_that_do_not_compose_with_expiring_in_are_refused_by_name() {
1483 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1484 seed_order(&database, "default").await;
1485
1486 let error = list_with(
1487 Some(30),
1488 None,
1489 Some("valid"),
1490 None,
1491 None,
1492 None,
1493 false,
1494 false,
1495 database.clone(),
1496 )
1497 .await
1498 .unwrap_err();
1499 assert!(error.message.contains("--status"), "{error}");
1500 assert!(error.message.contains("--expiring-in"), "{error}");
1501 assert_eq!(error.kind(), CliErrorKind::BadRequest);
1503
1504 let error = list_with(
1505 Some(30),
1506 Some("acct-1"),
1507 None,
1508 None,
1509 None,
1510 None,
1511 false,
1512 false,
1513 database.clone(),
1514 )
1515 .await
1516 .unwrap_err();
1517 assert!(error.message.contains("--account-id"), "{error}");
1518 assert!(error.message.contains("--expiring-in"), "{error}");
1519
1520 let error = list_with(
1521 None,
1522 None,
1523 None,
1524 None,
1525 None,
1526 None,
1527 true,
1528 false,
1529 database.clone(),
1530 )
1531 .await
1532 .unwrap_err();
1533 assert!(error.message.contains("--hide-superseded"), "{error}");
1534 assert!(error.message.contains("--expiring-in"), "{error}");
1535
1536 for (identifier, contains, serial, needle) in [
1538 (Some("a.example.com"), None, None, "--identifier"),
1539 (None, Some("example"), None, "--identifier-contains"),
1540 (None, None, Some("deadbeef"), "--cert-serial"),
1541 ] {
1542 let error = list_with(
1543 Some(30),
1544 None,
1545 None,
1546 identifier,
1547 contains,
1548 serial,
1549 false,
1550 false,
1551 database.clone(),
1552 )
1553 .await
1554 .unwrap_err();
1555 assert!(error.message.contains(needle), "{error}");
1556 assert!(error.message.contains("--expiring-in"), "{error}");
1557 }
1558
1559 list_with(
1561 None,
1562 None,
1563 Some("valid"),
1564 Some("a.example.com"),
1565 None,
1566 None,
1567 false,
1568 false,
1569 database,
1570 )
1571 .await
1572 .unwrap();
1573 }
1574
1575 #[tokio::test]
1577 async fn delete_refuses_an_order_holding_a_live_certificate() {
1578 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1579 let account = acme_proxy_store::testutil::account_id(&database).await;
1580 let order = acme_proxy_store::testutil::certified_order(&database, account, None).await;
1581
1582 let error = run_order_command(
1583 OrderCommand::Delete {
1584 id: order.id.to_string(),
1585 },
1586 true,
1587 Palette::plain(),
1588 &mut &b""[..],
1589 &Config::default(),
1590 database.clone(),
1591 )
1592 .await
1593 .expect_err("a live certificate must refuse the delete");
1594 assert_eq!(
1595 error,
1596 CliError::bad_request(admin::live_certificates_refusal(
1597 &format!("order {}", order.id),
1598 1
1599 ))
1600 );
1601 }
1602}