Skip to main content

acme_proxy/cli/
nonce.rs

1//! `acme-proxy nonce` — count the replay-nonce table, and sweep it by hand.
2//!
3//! The server already sweeps it on an interval; these exist to debug a table
4//! that is growing. Nonce values are bearer credentials and are never listed.
5
6use std::io::BufRead;
7use std::sync::Arc;
8use std::time::Duration;
9
10use clap::Subcommand;
11
12use crate::cli::CliError;
13use acme_proxy_admin::admin;
14use acme_proxy_core::config::Config;
15use acme_proxy_jobs::auditor::admin as audit_admin;
16use acme_proxy_store::db::Database;
17use acme_proxy_store::nonce::Nonce;
18
19#[derive(Subcommand)]
20pub enum NonceCommand {
21    /// Delete nonces older than the TTL.
22    Cleanup {
23        /// The age past which a nonce is deleted. Defaults to `nonce.ttl_seconds`.
24        #[arg(long = "ttl-seconds")]
25        ttl_seconds: Option<u64>,
26    },
27    /// Print how many nonces the table holds, and the window they are fresh for.
28    Count {
29        /// Print it as JSON.
30        #[arg(long)]
31        json: bool,
32    },
33}
34
35pub async fn run_nonce_command(
36    command: NonceCommand,
37    yes: bool,
38    reader: &mut impl BufRead,
39    config: &Config,
40    database: Arc<Database>,
41) -> Result<(), CliError> {
42    match command {
43        NonceCommand::Cleanup { ttl_seconds } => {
44            let ttl = Duration::from_secs(ttl_seconds.unwrap_or(config.nonce.ttl_seconds));
45            match admin::confirm_cleanup_nonces(ttl, yes, reader, database.clone()).await? {
46                None => println!("Cancelled."),
47                Some(removed) => {
48                    // Only when it actually removed something, the rule
49                    // `audit cleanup` already follows: a sweep that changed
50                    // nothing is not an administrative action worth a row.
51                    if removed > 0 {
52                        audit_admin::record_cli_action(&database, |actor, client| {
53                            audit_admin::nonce_cleanup_completed(actor, client, removed)
54                        })
55                        .await;
56                    }
57                    println!("Removed {removed} nonce(s).");
58                }
59            }
60        }
61        NonceCommand::Count { json } => {
62            // No `Palette`: a count is data, and the only thing here that could
63            // be painted -- "the reaper is not running" -- is a judgement this
64            // command deliberately leaves to the operator reading the two
65            // numbers together.
66            let count = Nonce::count(&database).await?;
67            let ttl = config.nonce.ttl_seconds;
68            if json {
69                println!("{}", admin::render_nonce_stats_json(count, ttl));
70            } else {
71                println!("{count} nonce(s), ttl {ttl}s.");
72            }
73        }
74    }
75    Ok(())
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    /// Both shapes, and the pair the count is only meaningful as: the number on
83    /// its own says nothing without the window it is a count over.
84    #[tokio::test]
85    async fn count_reports_the_table_and_the_configured_ttl() {
86        let database = Arc::new(Database::connect_in_memory().await.unwrap());
87        let mut config = Config::default();
88        config.nonce.ttl_seconds = 42;
89
90        for _ in 0..3 {
91            Nonce::new().save(&database).await.unwrap();
92        }
93        assert_eq!(Nonce::count(&database).await.unwrap(), 3);
94
95        assert_eq!(
96            admin::render_nonce_stats_json(3, config.nonce.ttl_seconds),
97            serde_json::json!({ "count": 3, "ttlSeconds": 42 }),
98        );
99
100        for json in [false, true] {
101            let mut reader: &[u8] = &[];
102            run_nonce_command(
103                NonceCommand::Count { json },
104                true,
105                &mut reader,
106                &config,
107                database.clone(),
108            )
109            .await
110            .unwrap();
111        }
112    }
113
114    /// Omitting `--ttl-seconds` falls back to `nonce.ttl_seconds`, and a
115    /// declined confirmation sweeps nothing without being a failure.
116    #[tokio::test]
117    async fn cleanup_honours_the_configured_ttl_and_the_prompt() {
118        let database = Arc::new(
119            acme_proxy_store::db::Database::connect_in_memory()
120                .await
121                .unwrap(),
122        );
123        let mut config = Config::default();
124        config.nonce.ttl_seconds = 1;
125
126        let mut declined: &[u8] = b"n\n";
127        run_nonce_command(
128            NonceCommand::Cleanup { ttl_seconds: None },
129            false,
130            &mut declined,
131            &config,
132            database.clone(),
133        )
134        .await
135        .unwrap();
136
137        let mut reader: &[u8] = &[];
138        run_nonce_command(
139            NonceCommand::Cleanup {
140                ttl_seconds: Some(60),
141            },
142            true,
143            &mut reader,
144            &config,
145            database,
146        )
147        .await
148        .unwrap();
149    }
150}