Skip to main content

acme_proxy/admin/
ops.rs

1use std::io::BufRead;
2use std::sync::Arc;
3use std::time::Duration;
4
5use crate::admin::prompt::confirm;
6use crate::audit::{Actor, AuditEvent, AuditRecord, ClientContext};
7use crate::signer::{SignerBackend, SignerError};
8use crate::sqlite::account::Account;
9use crate::sqlite::audit::{AuditEntry, AuditQuery};
10use crate::sqlite::authz::{Authorization, Challenge};
11use crate::sqlite::db::Database;
12use crate::sqlite::nonce::Nonce;
13use crate::sqlite::order::Order;
14
15/// Outcome of a confirm-gated hard delete.
16///
17/// `Cancelled` only exists on the `confirm_*` wrappers: a caller with nobody
18/// to ask -- the web admin -- uses the bare function below, whose return type
19/// has no such variant to leave unhandled.
20#[derive(Debug, PartialEq, Eq)]
21pub enum DeleteOutcome {
22    NotFound,
23    Cancelled,
24    Deleted,
25}
26
27/// What a hard delete took with it.
28///
29/// The count is already computed to word the confirmation prompt, so returning
30/// it costs nothing and lets an API caller report what it removed rather than
31/// answering a bare `204`.
32#[derive(Debug, PartialEq, Eq)]
33pub struct Deleted {
34    /// Rows the schema's `ON DELETE CASCADE` removed along with the row named:
35    /// orders for an account, authorizations for an order.
36    pub cascaded: u64,
37}
38
39/// An order plus every authorization (each with its challenges).
40#[derive(Debug)]
41pub struct OrderDetail {
42    pub order: Order,
43    pub authorizations: Vec<(Authorization, Vec<Challenge>)>,
44}
45
46/// Outcome of [`revoke_order`].
47#[derive(Debug)]
48pub enum RevokeOutcome {
49    NotFound,
50    NotIssued,
51    AlreadyRevoked,
52    Revoked(Box<Order>),
53}
54
55/// How a [`SignerError`] reads inside a [`RevokeError`].
56///
57/// `BadCsr` is not a thing `revoke` can legitimately answer — the hook takes a
58/// certificate, not a CSR — so it is reported as the contract violation it is
59/// rather than passed through as if it meant something here.
60fn signer_detail(error: &SignerError) -> String {
61    match error {
62        SignerError::Internal(detail) => detail.clone(),
63        SignerError::BadCsr => "unexpected badCsr from revoke".to_string(),
64    }
65}
66
67/// Why [`revoke_order`] failed.
68#[derive(Debug, thiserror::Error)]
69pub enum RevokeError {
70    #[error("database error: {0}")]
71    Database(sqlx::Error),
72    #[error("signer error: {}", signer_detail(.0))]
73    Signer(SignerError),
74    #[error("internal error: {0}")]
75    Internal(String),
76    #[error("unsupported revocation reason code {0}")]
77    BadReason(u32),
78}
79
80impl From<sqlx::Error> for RevokeError {
81    fn from(error: sqlx::Error) -> Self {
82        Self::Database(error)
83    }
84}
85
86impl From<SignerError> for RevokeError {
87    fn from(error: SignerError) -> Self {
88        Self::Signer(error)
89    }
90}
91
92// Each of the three destructive operations comes in two forms: a bare one that
93// simply does the thing, and a `confirm_*` wrapper that asks first. The split
94// exists because the wrapper's `assume_yes: bool` + `reader: &mut impl BufRead`
95// are a terminal's concerns, and a caller with no terminal -- the web admin --
96// had to pass `true` and an empty reader, asserting a confirmation that never
97// happened. The generic also makes the wrapper non-object-safe for no benefit
98// on that path. The CLI calls the wrapper; everything else calls the bare form.
99
100/// Hard-deletes an account. `None` when there is no such account; otherwise
101/// how many orders cascaded with it.
102pub async fn delete_account(
103    id: &str,
104    database: Arc<Database>,
105) -> Result<Option<Deleted>, sqlx::Error> {
106    let Some(cascaded) = account_cascade(id, database.clone()).await? else {
107        return Ok(None);
108    };
109    Account::delete(id, &database).await?;
110    Ok(Some(Deleted { cascaded }))
111}
112
113/// Looks up the account, shows what will cascade, confirms, then hard-deletes it.
114pub async fn confirm_delete_account(
115    id: &str,
116    assume_yes: bool,
117    reader: &mut impl BufRead,
118    database: Arc<Database>,
119) -> Result<DeleteOutcome, sqlx::Error> {
120    let Some(account) = Account::find_any_by_id(id, &database).await? else {
121        return Ok(DeleteOutcome::NotFound);
122    };
123    let order_count = Order::count_by_account(id, &database).await?;
124    let prompt = format!(
125        "Delete account {id} (status: {}, {order_count} order(s) will cascade)?",
126        account.status
127    );
128    if !confirm(&prompt, assume_yes, reader) {
129        return Ok(DeleteOutcome::Cancelled);
130    }
131    Account::delete(id, &database).await?;
132    Ok(DeleteOutcome::Deleted)
133}
134
135/// Hard-deletes an order. `None` when there is no such order; otherwise how
136/// many authorizations cascaded with it.
137pub async fn delete_order(
138    id: &str,
139    database: Arc<Database>,
140) -> Result<Option<Deleted>, sqlx::Error> {
141    let Some(cascaded) = order_cascade(id, database.clone()).await? else {
142        return Ok(None);
143    };
144    Order::delete(id, &database).await?;
145    Ok(Some(Deleted { cascaded }))
146}
147
148/// Same shape as [`confirm_delete_account`], for an order.
149pub async fn confirm_delete_order(
150    id: &str,
151    assume_yes: bool,
152    reader: &mut impl BufRead,
153    database: Arc<Database>,
154) -> Result<DeleteOutcome, sqlx::Error> {
155    let Some(order) = Order::find_by_id(id, &database).await? else {
156        return Ok(DeleteOutcome::NotFound);
157    };
158    let authz_count = Authorization::count_by_order(id, &database).await?;
159    let prompt = format!(
160        "Delete order {id} (status: {}, {authz_count} authorization(s) will cascade)?",
161        order.status
162    );
163    if !confirm(&prompt, assume_yes, reader) {
164        return Ok(DeleteOutcome::Cancelled);
165    }
166    Order::delete(id, &database).await?;
167    Ok(DeleteOutcome::Deleted)
168}
169
170/// Runs [`Nonce::cleanup`], returning how many were removed.
171pub async fn cleanup_nonces(ttl: Duration, database: Arc<Database>) -> Result<u64, sqlx::Error> {
172    Nonce::cleanup(&database, ttl).await
173}
174
175/// Confirms, then runs [`cleanup_nonces`]. `None` when the operator declined.
176pub async fn confirm_cleanup_nonces(
177    ttl: Duration,
178    assume_yes: bool,
179    reader: &mut impl BufRead,
180    database: Arc<Database>,
181) -> Result<Option<u64>, sqlx::Error> {
182    let prompt = format!("Delete all nonces older than {}s?", ttl.as_secs());
183    if !confirm(&prompt, assume_yes, reader) {
184        return Ok(None);
185    }
186    Ok(Some(cleanup_nonces(ttl, database).await?))
187}
188
189/// How many orders an account delete would cascade, or `None` if there is no
190/// such account. Shared so the prompt and the bare delete agree on the count.
191async fn account_cascade(id: &str, database: Arc<Database>) -> Result<Option<u64>, sqlx::Error> {
192    if Account::find_any_by_id(id, &database).await?.is_none() {
193        return Ok(None);
194    }
195    Ok(Some(Order::count_by_account(id, &database).await? as u64))
196}
197
198/// The [`account_cascade`] counterpart for an order's authorizations.
199async fn order_cascade(id: &str, database: Arc<Database>) -> Result<Option<u64>, sqlx::Error> {
200    if Order::find_by_id(id, &database).await?.is_none() {
201        return Ok(None);
202    }
203    Ok(Some(
204        Authorization::count_by_order(id, &database).await? as u64,
205    ))
206}
207
208/// Updates an account's contact list.
209pub async fn update_account_contact(
210    id: &str,
211    contact: Vec<String>,
212    database: Arc<Database>,
213) -> Result<Option<Account>, sqlx::Error> {
214    let Some(mut account) = Account::find_any_by_id(id, &database).await? else {
215        return Ok(None);
216    };
217    account.update_contact(contact, &database).await?;
218    Ok(Some(account))
219}
220
221/// Deactivates an account.
222pub async fn deactivate_account(
223    id: &str,
224    database: Arc<Database>,
225) -> Result<Option<Account>, sqlx::Error> {
226    let Some(mut account) = Account::find_any_by_id(id, &database).await? else {
227        return Ok(None);
228    };
229    account.deactivate(&database).await?;
230    Ok(Some(account))
231}
232
233/// Revokes an order's issued certificate at the signer backend and records it on the order.
234///
235/// `actor`/`client` say who asked and from where. Both front ends supply their
236/// own: [`Actor::cli`] with an empty [`ClientContext`] from the command line,
237/// [`Actor::admin`] with the operator's address from the web admin. Passed in
238/// rather than derived here because this layer is deliberately front-end
239/// agnostic — it is the same reason the destructive operations come in a bare
240/// and a `confirm_*` form.
241///
242/// The four outcomes that are *not* a revocation (`NotFound`, `NotIssued`,
243/// `AlreadyRevoked`, a bad reason code) write no audit row. They are the
244/// operator being told the state of things, not the CA refusing something it
245/// might have done — unlike `POST /revokeCert`'s refusals, which are a remote
246/// party being turned away and are audited for exactly that reason.
247pub async fn revoke_order(
248    id: &str,
249    reason: Option<u32>,
250    actor: Actor,
251    client: ClientContext,
252    database: Arc<Database>,
253    signer: Arc<dyn SignerBackend>,
254) -> Result<RevokeOutcome, RevokeError> {
255    let Some(mut order) = Order::find_by_id(id, &database).await? else {
256        return Ok(RevokeOutcome::NotFound);
257    };
258    let Some(chain) = order.certificate.clone() else {
259        return Ok(RevokeOutcome::NotIssued);
260    };
261    if order.revoked_at.is_some() {
262        return Ok(RevokeOutcome::AlreadyRevoked);
263    }
264    if let Some(r) = reason
265        && !crate::cert::is_valid_revocation_reason(r)
266    {
267        return Err(RevokeError::BadReason(r));
268    }
269
270    let cert_der = crate::cert::leaf_der_from_chain(&chain).map_err(|error| {
271        RevokeError::Internal(format!("stored certificate chain is unparsable: {error}"))
272    })?;
273    let mut record = AuditRecord::new(
274        AuditEvent::CertificateRevoked,
275        &order.profile,
276        actor.clone(),
277    )
278    .with_order(&order)
279    .with_client(client.clone());
280    if let Some(serial) = order.cert_serial.clone() {
281        record = record.with_serial(serial);
282    }
283    // Absent rather than empty when no reason was given — see the same rule in
284    // `post_revoke_cert`.
285    if let Some(reason) = reason {
286        record = record.with_reason(reason.to_string());
287    }
288
289    // The signer first, as on the ACME path: the CA-side action is
290    // authoritative, so a failure there must leave the order un-revoked for a
291    // retry — and must be audited as the attempt it was.
292    if let Err(error) = signer.revoke(&cert_der, reason).await {
293        crate::audit::write(
294            AuditRecord::new(AuditEvent::CertificateRevokeFailed, &order.profile, actor)
295                .with_order(&order)
296                .with_client(client)
297                .with_reason("serverInternal")
298                .with_detail(error.to_string()),
299            &database,
300        )
301        .await;
302        return Err(error.into());
303    }
304    order.revoke(reason.map(i64::from), &database).await?;
305    crate::audit::write(record, &database).await;
306    Ok(RevokeOutcome::Revoked(Box::new(order)))
307}
308
309/// The `created_at` below which an audit row is past `retention_days`.
310///
311/// One function so `acme-proxy audit cleanup --older-than` and the
312/// `audit.retention_days` sweep delete the identical set — a CLI that computed
313/// its own cutoff would eventually disagree with the timer by a rounding rule.
314#[must_use]
315pub fn audit_cutoff(days: u64) -> i64 {
316    let seconds = i64::try_from(days.saturating_mul(24 * 60 * 60)).unwrap_or(i64::MAX);
317    crate::sqlite::nonce::now_secs().saturating_sub(seconds)
318}
319
320/// One page of audit rows, plus the unpaged total the same filters match.
321pub async fn list_audit(
322    query: &AuditQuery,
323    database: Arc<Database>,
324) -> Result<(Vec<AuditEntry>, i64), sqlx::Error> {
325    AuditEntry::search(query, &database).await
326}
327
328/// One audit row by id.
329pub async fn find_audit(
330    id: i64,
331    database: Arc<Database>,
332) -> Result<Option<AuditEntry>, sqlx::Error> {
333    AuditEntry::find_by_id(id, &database).await
334}
335
336/// Deletes audit rows older than `days`, returning how many went.
337pub async fn cleanup_audit(days: u64, database: Arc<Database>) -> Result<u64, sqlx::Error> {
338    AuditEntry::cleanup(audit_cutoff(days), &database).await
339}
340
341/// Confirms, then runs [`cleanup_audit`]. `None` when the operator declined.
342///
343/// Confirm-gated, unlike `revoke_order`: this is the one operation in the crate
344/// that destroys audit history, and the prompt names how many rows are about to
345/// go — a number the operator usually did not expect.
346pub async fn confirm_cleanup_audit(
347    days: u64,
348    assume_yes: bool,
349    reader: &mut impl BufRead,
350    database: Arc<Database>,
351) -> Result<Option<u64>, sqlx::Error> {
352    let cutoff = audit_cutoff(days);
353    let doomed = AuditEntry::count_older_than(cutoff, &database).await?;
354    let prompt =
355        format!("Delete {doomed} audit row(s) older than {days} day(s)? This cannot be undone.");
356    if !confirm(&prompt, assume_yes, reader) {
357        return Ok(None);
358    }
359    Ok(Some(AuditEntry::cleanup(cutoff, &database).await?))
360}
361
362/// Loads order detail.
363pub async fn load_order_detail(
364    id: &str,
365    database: Arc<Database>,
366) -> Result<Option<OrderDetail>, sqlx::Error> {
367    let Some(order) = Order::find_by_id(id, &database).await? else {
368        return Ok(None);
369    };
370    let authzs = Authorization::find_by_order(&order.id, &database).await?;
371    let mut authorizations = Vec::with_capacity(authzs.len());
372    for authz in authzs {
373        let challenges = Challenge::find_by_authz(&authz.id, &database).await?;
374        authorizations.push((authz, challenges));
375    }
376    Ok(Some(OrderDetail {
377        order,
378        authorizations,
379    }))
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385    use crate::sqlite::order::Identifier;
386    use crate::testutil::account_id;
387
388    /// The actor the CLI supplies, which is what these tests stand in for.
389    /// `Actor::cli` reads `$USER`, so it is called rather than hard-coded — the
390    /// point of the tests below is the revocation, not the name on the row.
391    fn cli_actor() -> Actor {
392        Actor::cli()
393    }
394
395    async fn audit_rows(db: &Arc<Database>) -> Vec<AuditEntry> {
396        AuditEntry::search(
397            &AuditQuery {
398                limit: 50,
399                ..AuditQuery::default()
400            },
401            db,
402        )
403        .await
404        .unwrap()
405        .0
406    }
407
408    /// One cutoff function, so `audit cleanup --older-than` and the
409    /// `audit.retention_days` sweep delete the identical set.
410    #[test]
411    fn the_audit_cutoff_is_days_before_now_and_saturates_rather_than_overflowing() {
412        let now = crate::sqlite::nonce::now_secs();
413        assert!((audit_cutoff(0) - now).abs() <= 1);
414        let week = audit_cutoff(7);
415        assert!((now - week - 7 * 24 * 60 * 60).abs() <= 1, "{week}");
416        // A nonsense retention must not panic in a debug build.
417        assert!(audit_cutoff(u64::MAX) <= now);
418    }
419
420    /// The confirm gate: declined leaves the trail intact, accepted prunes by
421    /// age and nothing else.
422    #[tokio::test]
423    async fn cleaning_the_audit_trail_is_confirm_gated_and_bounded_by_age() {
424        let db = Arc::new(Database::connect_in_memory().await.unwrap());
425        AuditEntry::insert(
426            AuditRecord::new(AuditEvent::CertificateIssued, "default", Actor::system()),
427            &db,
428        )
429        .await
430        .unwrap();
431
432        let mut declined: &[u8] = b"n\n";
433        assert_eq!(
434            confirm_cleanup_audit(0, false, &mut declined, db.clone())
435                .await
436                .unwrap(),
437            None
438        );
439        assert_eq!(audit_rows(&db).await.len(), 1);
440
441        // Nothing is a week old yet.
442        let mut reader: &[u8] = &[];
443        assert_eq!(
444            confirm_cleanup_audit(7, true, &mut reader, db.clone())
445                .await
446                .unwrap(),
447            Some(0)
448        );
449        assert_eq!(audit_rows(&db).await.len(), 1);
450
451        assert_eq!(cleanup_audit(0, db.clone()).await.unwrap(), 0);
452
453        // A cutoff in the future takes it.
454        assert_eq!(
455            AuditEntry::cleanup(audit_cutoff(0) + 3600, &db)
456                .await
457                .unwrap(),
458            1
459        );
460        assert!(audit_rows(&db).await.is_empty());
461    }
462
463    /// `list_audit`/`find_audit` are the thin pass-throughs both front ends
464    /// share; this pins that they page and look up rather than doing anything
465    /// of their own.
466    #[tokio::test]
467    async fn listing_and_finding_audit_rows_pages_and_resolves() {
468        let db = Arc::new(Database::connect_in_memory().await.unwrap());
469        let mut ids = Vec::new();
470        for _ in 0..3 {
471            ids.push(
472                AuditEntry::insert(
473                    AuditRecord::new(AuditEvent::CertificateIssued, "default", Actor::system()),
474                    &db,
475                )
476                .await
477                .unwrap(),
478            );
479        }
480
481        let (page, total) = list_audit(
482            &AuditQuery {
483                limit: 2,
484                ..AuditQuery::default()
485            },
486            db.clone(),
487        )
488        .await
489        .unwrap();
490        assert_eq!(total, 3);
491        assert_eq!(page.len(), 2);
492
493        assert!(find_audit(ids[0], db.clone()).await.unwrap().is_some());
494        assert!(find_audit(9_999, db).await.unwrap().is_none());
495    }
496
497    /// A revocation through this layer writes exactly one row, naming the
498    /// actor the caller supplied rather than the order's own account — which
499    /// is the whole point of the parameter.
500    #[tokio::test]
501    async fn revoking_writes_one_audit_row_naming_the_caller() {
502        let db = Arc::new(Database::connect_in_memory().await.unwrap());
503        let signer = in_memory_ca();
504        let order = finalized_order(db.clone(), &signer).await;
505
506        let outcome = revoke_order(
507            &order.id,
508            Some(1),
509            Actor::admin("root"),
510            ClientContext {
511                ip: Some("203.0.113.7".to_string()),
512                ptr: Some("desk.example.com".to_string()),
513                ..ClientContext::default()
514            },
515            db.clone(),
516            signer.clone(),
517        )
518        .await
519        .unwrap();
520        assert!(matches!(outcome, RevokeOutcome::Revoked(_)));
521
522        let rows = audit_rows(&db).await;
523        assert_eq!(rows.len(), 1, "{rows:?}");
524        let row = &rows[0];
525        assert_eq!(row.event, "certificate_revoked");
526        assert_eq!(row.outcome, "success");
527        assert_eq!(row.actor_kind, "admin");
528        assert_eq!(row.actor_id.as_deref(), Some("root"));
529        assert_eq!(row.account_id.as_deref(), Some(order.account_id.as_str()));
530        assert_eq!(row.order_id.as_deref(), Some(order.id.as_str()));
531        assert_eq!(row.cert_serial, order.cert_serial);
532        assert_eq!(row.client_ip.as_deref(), Some("203.0.113.7"));
533        assert_eq!(row.client_ptr.as_deref(), Some("desk.example.com"));
534        assert_eq!(row.reason.as_deref(), Some("1"));
535
536        // Revoking again is `AlreadyRevoked` and writes nothing: the operator
537        // is being told the state of things, not refused a CA action.
538        let outcome = revoke_order(
539            &order.id,
540            None,
541            Actor::admin("root"),
542            ClientContext::default(),
543            db.clone(),
544            signer,
545        )
546        .await
547        .unwrap();
548        assert!(matches!(outcome, RevokeOutcome::AlreadyRevoked));
549        assert_eq!(audit_rows(&db).await.len(), 1);
550    }
551
552    /// No reason given is an **absent** `reason`, not an empty one: RFC 8555
553    /// §7.6 allows omitting it, and that is not the same as `unspecified` (0).
554    #[tokio::test]
555    async fn a_revocation_with_no_reason_leaves_the_column_absent() {
556        let db = Arc::new(Database::connect_in_memory().await.unwrap());
557        let signer = in_memory_ca();
558        let order = finalized_order(db.clone(), &signer).await;
559
560        revoke_order(
561            &order.id,
562            None,
563            cli_actor(),
564            ClientContext::default(),
565            db.clone(),
566            signer,
567        )
568        .await
569        .unwrap();
570
571        let rows = audit_rows(&db).await;
572        assert_eq!(rows[0].reason, None);
573        assert_eq!(rows[0].actor_kind, "cli");
574        // A CLI revocation genuinely has no client, and says so.
575        assert_eq!(rows[0].client_ip, None);
576        assert_eq!(rows[0].client_ptr, None);
577    }
578
579    #[tokio::test]
580    async fn delete_account_not_found() {
581        let db = Arc::new(Database::connect_in_memory().await.unwrap());
582        let mut reader: &[u8] = &[];
583        let outcome = confirm_delete_account("nope", true, &mut reader, db)
584            .await
585            .unwrap();
586        assert_eq!(outcome, DeleteOutcome::NotFound);
587    }
588
589    #[tokio::test]
590    async fn delete_account_cancelled_leaves_row() {
591        let db = Arc::new(Database::connect_in_memory().await.unwrap());
592        let acct = account_id(&db).await;
593
594        let mut reader = b"n\n".as_slice();
595        let outcome = confirm_delete_account(&acct, false, &mut reader, db.clone())
596            .await
597            .unwrap();
598        assert_eq!(outcome, DeleteOutcome::Cancelled);
599        assert!(
600            Account::find_by_id("default", &acct, &db)
601                .await
602                .unwrap()
603                .is_some()
604        );
605    }
606
607    #[tokio::test]
608    async fn delete_account_confirmed_deletes_and_cascades() {
609        let db = Arc::new(Database::connect_in_memory().await.unwrap());
610        let acct = account_id(&db).await;
611        let order = Order::create(
612            "default",
613            &acct,
614            vec![Identifier::dns("example.com")],
615            crate::sqlite::nonce::now_secs() + 3600,
616            None,
617            None,
618            &db,
619        )
620        .await
621        .unwrap();
622
623        let mut reader: &[u8] = &[];
624        let outcome = confirm_delete_account(&acct, true, &mut reader, db.clone())
625            .await
626            .unwrap();
627        assert_eq!(outcome, DeleteOutcome::Deleted);
628        assert!(
629            Account::find_by_id("default", &acct, &db)
630                .await
631                .unwrap()
632                .is_none()
633        );
634        assert!(Order::find_by_id(&order.id, &db).await.unwrap().is_none());
635    }
636
637    #[tokio::test]
638    async fn delete_order_not_found() {
639        let db = Arc::new(Database::connect_in_memory().await.unwrap());
640        let mut reader: &[u8] = &[];
641        let outcome = confirm_delete_order("nope", true, &mut reader, db)
642            .await
643            .unwrap();
644        assert_eq!(outcome, DeleteOutcome::NotFound);
645    }
646
647    #[tokio::test]
648    async fn delete_order_cancelled_leaves_row() {
649        let db = Arc::new(Database::connect_in_memory().await.unwrap());
650        let acct = account_id(&db).await;
651        let order = Order::create(
652            "default",
653            &acct,
654            vec![Identifier::dns("example.com")],
655            crate::sqlite::nonce::now_secs() + 3600,
656            None,
657            None,
658            &db,
659        )
660        .await
661        .unwrap();
662
663        let mut reader = b"no\n".as_slice();
664        let outcome = confirm_delete_order(&order.id, false, &mut reader, db.clone())
665            .await
666            .unwrap();
667        assert_eq!(outcome, DeleteOutcome::Cancelled);
668        assert!(Order::find_by_id(&order.id, &db).await.unwrap().is_some());
669    }
670
671    #[tokio::test]
672    async fn delete_order_confirmed_deletes_and_cascades() {
673        let db = Arc::new(Database::connect_in_memory().await.unwrap());
674        let acct = account_id(&db).await;
675        let order = Order::create(
676            "default",
677            &acct,
678            vec![Identifier::dns("example.com")],
679            crate::sqlite::nonce::now_secs() + 3600,
680            None,
681            None,
682            &db,
683        )
684        .await
685        .unwrap();
686        let authz = Authorization::create(
687            &order.id,
688            Identifier::dns("example.com"),
689            crate::sqlite::nonce::now_secs() + 3600,
690            &db,
691        )
692        .await
693        .unwrap();
694
695        let mut reader: &[u8] = &[];
696        let outcome = confirm_delete_order(&order.id, true, &mut reader, db.clone())
697            .await
698            .unwrap();
699        assert_eq!(outcome, DeleteOutcome::Deleted);
700        assert!(Order::find_by_id(&order.id, &db).await.unwrap().is_none());
701        assert!(
702            Authorization::find_by_id(&authz.id, &db)
703                .await
704                .unwrap()
705                .is_none()
706        );
707    }
708
709    // The bare forms below are what the web admin calls: no prompt, no reader,
710    // and a cascade count to report back instead of a bare acknowledgement.
711
712    #[tokio::test]
713    async fn bare_delete_account_reports_none_for_an_unknown_id() {
714        let db = Arc::new(Database::connect_in_memory().await.unwrap());
715        assert_eq!(delete_account("nope", db).await.unwrap(), None);
716    }
717
718    #[tokio::test]
719    async fn bare_delete_account_deletes_and_counts_the_cascade() {
720        let db = Arc::new(Database::connect_in_memory().await.unwrap());
721        let acct = account_id(&db).await;
722        for _ in 0..2 {
723            Order::create(
724                "default",
725                &acct,
726                vec![Identifier::dns("example.com")],
727                crate::sqlite::nonce::now_secs() + 3600,
728                None,
729                None,
730                &db,
731            )
732            .await
733            .unwrap();
734        }
735
736        assert_eq!(
737            delete_account(&acct, db.clone()).await.unwrap(),
738            Some(Deleted { cascaded: 2 })
739        );
740        assert!(
741            Account::find_by_id("default", &acct, &db)
742                .await
743                .unwrap()
744                .is_none()
745        );
746    }
747
748    #[tokio::test]
749    async fn bare_delete_order_reports_none_for_an_unknown_id() {
750        let db = Arc::new(Database::connect_in_memory().await.unwrap());
751        assert_eq!(delete_order("nope", db).await.unwrap(), None);
752    }
753
754    #[tokio::test]
755    async fn bare_delete_order_deletes_and_counts_the_cascade() {
756        let db = Arc::new(Database::connect_in_memory().await.unwrap());
757        let acct = account_id(&db).await;
758        let order = Order::create(
759            "default",
760            &acct,
761            vec![Identifier::dns("example.com")],
762            crate::sqlite::nonce::now_secs() + 3600,
763            None,
764            None,
765            &db,
766        )
767        .await
768        .unwrap();
769        Authorization::create(
770            &order.id,
771            Identifier::dns("example.com"),
772            crate::sqlite::nonce::now_secs() + 3600,
773            &db,
774        )
775        .await
776        .unwrap();
777
778        assert_eq!(
779            delete_order(&order.id, db.clone()).await.unwrap(),
780            Some(Deleted { cascaded: 1 })
781        );
782        assert!(Order::find_by_id(&order.id, &db).await.unwrap().is_none());
783    }
784
785    #[tokio::test]
786    async fn bare_cleanup_nonces_removes_stale_rows_without_asking() {
787        let db = Arc::new(Database::connect_in_memory().await.unwrap());
788        let stale = Nonce {
789            value: "stale".to_string(),
790            created_at: crate::sqlite::nonce::now_secs() - 10_000,
791        };
792        stale.save(&db).await.unwrap();
793        Nonce::new().save(&db).await.unwrap();
794
795        assert_eq!(
796            cleanup_nonces(Duration::from_secs(300), db.clone())
797                .await
798                .unwrap(),
799            1
800        );
801        assert!(
802            !Nonce::verify("stale", &db, Duration::from_secs(300))
803                .await
804                .unwrap()
805        );
806    }
807
808    fn in_memory_ca() -> Arc<dyn SignerBackend> {
809        Arc::new(
810            crate::signer::local_ca::LocalCa::generate_in_memory("ecdsa-p256", 90)
811                .expect("in-memory CA"),
812        )
813    }
814
815    async fn finalized_order(db: Arc<Database>, signer: &Arc<dyn SignerBackend>) -> Order {
816        let acct = account_id(&db).await;
817        let mut order = Order::create(
818            "default",
819            &acct,
820            vec![Identifier::dns("example.com")],
821            crate::sqlite::nonce::now_secs() + 3600,
822            None,
823            None,
824            &db,
825        )
826        .await
827        .unwrap();
828
829        let key_pair = rcgen::KeyPair::generate().unwrap();
830        let params = rcgen::CertificateParams::new(vec!["example.com".to_string()]).unwrap();
831        let csr = params.serialize_request(&key_pair).unwrap();
832        let chain = match signer
833            .issue(
834                &order.id,
835                csr.der(),
836                &order.identifiers,
837                crate::signer::RequestedValidity::default(),
838            )
839            .await
840            .unwrap()
841        {
842            crate::signer::IssueOutcome::Issued(chain) => chain,
843            crate::signer::IssueOutcome::Processing => {
844                panic!("the in-memory local CA issues synchronously")
845            }
846        };
847        let leaf = crate::cert::leaf_der_from_chain(&chain).unwrap();
848        let (serial, pubkey) = crate::cert::cert_serial_and_spki(&leaf).unwrap();
849        order.finalize(chain, serial, pubkey, &db).await.unwrap();
850        order
851    }
852
853    #[tokio::test]
854    async fn revoke_order_not_found() {
855        let db = Arc::new(Database::connect_in_memory().await.unwrap());
856        let outcome = revoke_order(
857            "nope",
858            None,
859            cli_actor(),
860            ClientContext::default(),
861            db,
862            in_memory_ca(),
863        )
864        .await
865        .unwrap();
866        assert!(matches!(outcome, RevokeOutcome::NotFound));
867    }
868
869    #[tokio::test]
870    async fn revoke_order_without_a_certificate_is_refused() {
871        let db = Arc::new(Database::connect_in_memory().await.unwrap());
872        let acct = account_id(&db).await;
873        let order = Order::create(
874            "default",
875            &acct,
876            vec![Identifier::dns("example.com")],
877            crate::sqlite::nonce::now_secs() + 3600,
878            None,
879            None,
880            &db,
881        )
882        .await
883        .unwrap();
884
885        let outcome = revoke_order(
886            &order.id,
887            None,
888            cli_actor(),
889            ClientContext::default(),
890            db,
891            in_memory_ca(),
892        )
893        .await
894        .unwrap();
895        assert!(matches!(outcome, RevokeOutcome::NotIssued));
896    }
897
898    #[tokio::test]
899    async fn revoke_order_persists() {
900        let db = Arc::new(Database::connect_in_memory().await.unwrap());
901        let signer = in_memory_ca();
902        let order = finalized_order(db.clone(), &signer).await;
903
904        let outcome = revoke_order(
905            &order.id,
906            Some(1),
907            cli_actor(),
908            ClientContext::default(),
909            db.clone(),
910            signer.clone(),
911        )
912        .await
913        .unwrap();
914        let RevokeOutcome::Revoked(revoked) = outcome else {
915            panic!("expected Revoked, got {outcome:?}");
916        };
917        assert!(revoked.revoked_at.is_some());
918        assert_eq!(revoked.revocation_reason, Some(1));
919
920        let reloaded = Order::find_by_id(&order.id, &db).await.unwrap().unwrap();
921        assert!(reloaded.revoked_at.is_some());
922
923        use x509_parser::prelude::FromDer;
924        let der = signer.crl_der().await.unwrap();
925        let (_, crl) =
926            x509_parser::revocation_list::CertificateRevocationList::from_der(&der).unwrap();
927        assert_eq!(crl.iter_revoked_certificates().count(), 1);
928    }
929
930    #[tokio::test]
931    async fn revoke_order_already_revoked() {
932        let db = Arc::new(Database::connect_in_memory().await.unwrap());
933        let signer = in_memory_ca();
934        let order = finalized_order(db.clone(), &signer).await;
935
936        revoke_order(
937            &order.id,
938            None,
939            cli_actor(),
940            ClientContext::default(),
941            db.clone(),
942            signer.clone(),
943        )
944        .await
945        .unwrap();
946        let outcome = revoke_order(
947            &order.id,
948            None,
949            cli_actor(),
950            ClientContext::default(),
951            db,
952            signer,
953        )
954        .await
955        .unwrap();
956        assert!(matches!(outcome, RevokeOutcome::AlreadyRevoked));
957    }
958
959    #[tokio::test]
960    async fn revoke_order_bad_reason_is_refused() {
961        let db = Arc::new(Database::connect_in_memory().await.unwrap());
962        let signer = in_memory_ca();
963        let order = finalized_order(db.clone(), &signer).await;
964
965        let error = revoke_order(
966            &order.id,
967            Some(999),
968            cli_actor(),
969            ClientContext::default(),
970            db,
971            signer,
972        )
973        .await
974        .unwrap_err();
975        assert!(matches!(error, RevokeError::BadReason(999)));
976    }
977
978    #[tokio::test]
979    async fn cleanup_nonces_cancelled_leaves_nonces() {
980        let db = Arc::new(Database::connect_in_memory().await.unwrap());
981        Nonce {
982            value: "stale".to_string(),
983            created_at: crate::sqlite::nonce::now_secs() - 600,
984        }
985        .save(&db)
986        .await
987        .unwrap();
988
989        let mut reader = b"n\n".as_slice();
990        let outcome =
991            confirm_cleanup_nonces(Duration::from_secs(300), false, &mut reader, db.clone())
992                .await
993                .unwrap();
994        assert_eq!(outcome, None);
995
996        let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM nonces;")
997            .fetch_one(&db.pool)
998            .await
999            .unwrap();
1000        assert_eq!(count, 1);
1001    }
1002
1003    #[tokio::test]
1004    async fn cleanup_nonces_confirmed_removes_stale_and_reports_count() {
1005        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1006        Nonce {
1007            value: "stale".to_string(),
1008            created_at: crate::sqlite::nonce::now_secs() - 600,
1009        }
1010        .save(&db)
1011        .await
1012        .unwrap();
1013
1014        let mut reader: &[u8] = &[];
1015        let outcome =
1016            confirm_cleanup_nonces(Duration::from_secs(300), true, &mut reader, db.clone())
1017                .await
1018                .unwrap();
1019        assert_eq!(outcome, Some(1));
1020
1021        let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM nonces;")
1022            .fetch_one(&db.pool)
1023            .await
1024            .unwrap();
1025        assert_eq!(count, 0);
1026    }
1027
1028    #[tokio::test]
1029    async fn update_account_contact_not_found() {
1030        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1031        assert!(
1032            update_account_contact("nope", vec![], db)
1033                .await
1034                .unwrap()
1035                .is_none()
1036        );
1037    }
1038
1039    #[tokio::test]
1040    async fn update_account_contact_persists() {
1041        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1042        let acct = account_id(&db).await;
1043
1044        let contact = vec!["mailto:a@example.com".to_string()];
1045        let updated = update_account_contact(&acct, contact.clone(), db.clone())
1046            .await
1047            .unwrap()
1048            .unwrap();
1049        assert_eq!(updated.contact, contact);
1050
1051        let reloaded = Account::find_by_id("default", &acct, &db)
1052            .await
1053            .unwrap()
1054            .unwrap();
1055        assert_eq!(reloaded.contact, contact);
1056    }
1057
1058    #[tokio::test]
1059    async fn deactivate_account_not_found() {
1060        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1061        assert!(deactivate_account("nope", db).await.unwrap().is_none());
1062    }
1063
1064    #[tokio::test]
1065    async fn deactivate_account_persists() {
1066        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1067        let acct = account_id(&db).await;
1068
1069        let updated = deactivate_account(&acct, db.clone())
1070            .await
1071            .unwrap()
1072            .unwrap();
1073        assert_eq!(updated.status, "deactivated");
1074
1075        let reloaded = Account::find_by_id("default", &acct, &db)
1076            .await
1077            .unwrap()
1078            .unwrap();
1079        assert_eq!(reloaded.status, "deactivated");
1080    }
1081
1082    #[tokio::test]
1083    async fn load_order_detail_not_found() {
1084        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1085        assert!(load_order_detail("nope", db).await.unwrap().is_none());
1086    }
1087
1088    #[tokio::test]
1089    async fn load_order_detail_nests_authorizations_and_challenges() {
1090        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1091        let acct = account_id(&db).await;
1092        let order = Order::create(
1093            "default",
1094            &acct,
1095            vec![Identifier::dns("example.com")],
1096            crate::sqlite::nonce::now_secs() + 3600,
1097            None,
1098            None,
1099            &db,
1100        )
1101        .await
1102        .unwrap();
1103        let authz = Authorization::create(
1104            &order.id,
1105            Identifier::dns("example.com"),
1106            crate::sqlite::nonce::now_secs() + 3600,
1107            &db,
1108        )
1109        .await
1110        .unwrap();
1111        Challenge::create(&authz.id, "http-01", &db).await.unwrap();
1112
1113        let detail = load_order_detail(&order.id, db).await.unwrap().unwrap();
1114        assert_eq!(detail.order.id, order.id);
1115        assert_eq!(detail.authorizations.len(), 1);
1116        assert_eq!(detail.authorizations[0].0.id, authz.id);
1117        assert_eq!(detail.authorizations[0].1.len(), 1);
1118        assert_eq!(detail.authorizations[0].1[0].typ, "http-01");
1119    }
1120
1121    #[test]
1122    fn revoke_error_display_formatting() {
1123        let db_err: RevokeError = sqlx::Error::RowNotFound.into();
1124        assert!(format!("{db_err}").contains("database error"));
1125
1126        let signer_internal: RevokeError = SignerError::Internal("test".to_string()).into();
1127        assert!(format!("{signer_internal}").contains("signer error: test"));
1128
1129        let signer_bad_csr: RevokeError = SignerError::BadCsr.into();
1130        assert!(format!("{signer_bad_csr}").contains("unexpected badCsr"));
1131
1132        let internal = RevokeError::Internal("detail".to_string());
1133        assert!(format!("{internal}").contains("internal error: detail"));
1134
1135        let bad_reason = RevokeError::BadReason(7);
1136        assert!(format!("{bad_reason}").contains("unsupported revocation reason code 7"));
1137    }
1138}