Skip to main content

acme_proxy/cli/
eab.rs

1use std::sync::Arc;
2
3use clap::Subcommand;
4
5use crate::admin;
6use crate::cli::CliError;
7use crate::cli::render;
8use crate::cli::style::Palette;
9use crate::sqlite::db::Database;
10use crate::sqlite::eab::Eab;
11
12#[derive(Subcommand)]
13pub enum EabCommand {
14    /// Generate a new EAB key and print its kid + secret ONCE.
15    Create {
16        #[arg(long)]
17        label: Option<String>,
18        /// Bind the credential to one ACME endpoint. Omitted, it is accepted
19        /// at every profile — which is what an unscoped credential means.
20        #[arg(long)]
21        profile: Option<String>,
22        #[arg(long)]
23        json: bool,
24    },
25    /// List every EAB key. Never shows the secret.
26    List {
27        #[arg(long)]
28        json: bool,
29    },
30    /// Show one EAB key. Never shows the secret.
31    Show {
32        kid: String,
33        #[arg(long)]
34        json: bool,
35    },
36    /// Revoke a key.
37    Revoke { kid: String },
38}
39
40pub async fn run_eab_command(
41    command: EabCommand,
42    palette: Palette,
43    database: Arc<Database>,
44) -> Result<(), CliError> {
45    match command {
46        EabCommand::Create {
47            label,
48            profile,
49            json,
50        } => {
51            let eab = Eab::create(label, profile, &database).await?;
52            if json {
53                println!("{}", admin::render_eab_created_json(&eab));
54            } else {
55                print!("{}", render::render_eab_created_text(&eab, palette));
56            }
57        }
58        EabCommand::List { json } => {
59            let keys = Eab::list_all(&database).await?;
60            render::print_rows(&keys, json, admin::render_eab_json, |eab| {
61                render::render_eab_line(eab, palette)
62            });
63        }
64        EabCommand::Show { kid, json } => match Eab::find_any_by_kid(&kid, &database).await? {
65            None => return Err(not_found(&kid)),
66            Some(eab) if json => println!("{}", admin::render_eab_json(&eab)),
67            Some(eab) => println!("{}", render::render_eab_line(&eab, palette)),
68        },
69        EabCommand::Revoke { kid } => {
70            if !Eab::revoke(&kid, &database).await? {
71                return Err(not_found(&kid));
72            }
73            println!("Revoked EAB key {kid}.");
74        }
75    }
76    Ok(())
77}
78
79fn not_found(kid: &str) -> CliError {
80    CliError(format!("no such EAB credential: {kid}"))
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[tokio::test]
88    async fn show_and_revoke_refuse_an_unknown_kid() {
89        let database = Arc::new(Database::connect_in_memory().await.unwrap());
90        let expected = CliError("no such EAB credential: kid-nope".to_string());
91
92        for command in [
93            EabCommand::Show {
94                kid: "kid-nope".to_string(),
95                json: false,
96            },
97            EabCommand::Revoke {
98                kid: "kid-nope".to_string(),
99            },
100        ] {
101            let error = run_eab_command(command, Palette::plain(), database.clone())
102                .await
103                .expect_err("an unknown kid must fail");
104            assert_eq!(error, expected);
105        }
106    }
107
108    /// `revoke` matches on the `kid` alone, so revoking twice is idempotent
109    /// and still reports success — only an unknown `kid` is an error.
110    #[tokio::test]
111    async fn a_created_key_shows_lists_and_revokes() {
112        let database = Arc::new(Database::connect_in_memory().await.unwrap());
113        let eab = Eab::create(Some("test".to_string()), None, &database)
114            .await
115            .unwrap();
116
117        for command in [
118            EabCommand::Create {
119                label: None,
120                profile: Some("default".to_string()),
121                json: true,
122            },
123            EabCommand::List { json: true },
124            EabCommand::Show {
125                kid: eab.kid.clone(),
126                json: true,
127            },
128            EabCommand::Show {
129                kid: eab.kid.clone(),
130                json: false,
131            },
132            EabCommand::Revoke {
133                kid: eab.kid.clone(),
134            },
135        ] {
136            run_eab_command(command, Palette::plain(), database.clone())
137                .await
138                .unwrap();
139        }
140
141        run_eab_command(
142            EabCommand::Revoke {
143                kid: eab.kid.clone(),
144            },
145            Palette::plain(),
146            database.clone(),
147        )
148        .await
149        .expect("revoking an already-revoked key is a no-op, not a failure");
150
151        assert_eq!(
152            Eab::find_any_by_kid(&eab.kid, &database)
153                .await
154                .unwrap()
155                .unwrap()
156                .status,
157            "revoked"
158        );
159    }
160}