acme_proxy_admin/webadmin/handlers/
eab.rs1use axum::Json;
9use axum::extract::{Path, Query, State};
10use axum::http::StatusCode;
11use axum::response::{IntoResponse, Response};
12use serde::Deserialize;
13use serde_json::Value;
14
15use crate::admin;
16use crate::webadmin::AdminState;
17use crate::webadmin::error::AdminError;
18use crate::webadmin::handlers::Caller;
19use crate::webadmin::handlers::paging::{PageParams, page_envelope};
20use crate::webadmin::handlers::params::empty_is_absent;
21use crate::webadmin::session::{Authenticated, AuthenticatedWrite};
22use acme_proxy_store::eab::BoundAccounts;
23use acme_proxy_store::eab::DeletedEab;
24use acme_proxy_store::eab::Eab;
25use acme_proxy_store::eab::EabDeletion;
26
27#[derive(Debug, Deserialize, Default)]
29pub struct CreateEab {
30 #[serde(default, deserialize_with = "empty_is_absent")]
31 pub label: Option<String>,
32 #[serde(default, deserialize_with = "empty_is_absent")]
34 pub profile: Option<String>,
35}
36
37#[derive(Debug, Deserialize, Default)]
39pub struct DeleteEabParams {
40 #[serde(default, deserialize_with = "empty_is_absent")]
43 pub accounts: Option<String>,
44}
45
46impl DeleteEabParams {
47 pub(crate) fn resolve(&self) -> Result<BoundAccounts, AdminError> {
51 let Some(value) = self.accounts.as_deref() else {
52 return Ok(BoundAccounts::Keep);
53 };
54 BoundAccounts::parse(value).ok_or_else(|| {
55 let known: Vec<&str> = BoundAccounts::ALL
56 .iter()
57 .map(|mode| mode.as_str())
58 .collect();
59 AdminError::bad_request(format!(
60 "unknown accounts mode `{value}`; expected one of: {}",
61 known.join(", ")
62 ))
63 })
64 }
65}
66
67pub async fn list_eab(
75 State(state): State<AdminState>,
76 Query(params): Query<PageParams>,
77 _auth: Authenticated,
78) -> Result<Json<Value>, AdminError> {
79 let page = params.resolve(&state.config);
80 let (keys, total) = Eab::search(page.limit, page.offset, &state.database).await?;
81 let items: Vec<Value> = keys.iter().map(admin::render_eab_json).collect();
82 Ok(Json(page_envelope(items, total, page)))
83}
84
85pub async fn get_eab(
87 State(state): State<AdminState>,
88 Path(kid): Path<String>,
89 _auth: Authenticated,
90) -> Result<Json<Value>, AdminError> {
91 let eab = Eab::find_any_by_kid(&kid, &state.database)
92 .await?
93 .ok_or_else(|| not_found(&kid))?;
94 Ok(Json(admin::render_eab_json(&eab)))
95}
96
97pub async fn create_eab(
104 State(state): State<AdminState>,
105 AuthenticatedWrite(auth): AuthenticatedWrite,
106 request_context: acme_proxy_core::audit::RequestContext,
107 body: Option<Json<CreateEab>>,
108) -> Result<Response, AdminError> {
109 let Json(body) = body.unwrap_or_default();
110 let eab = apply_create_eab(
111 &state,
112 &Caller::api(&auth, &request_context),
113 body.label,
114 body.profile,
115 "omit `profile`",
116 )
117 .await?;
118
119 Ok((
120 StatusCode::CREATED,
121 Json(admin::render_eab_created_json(&eab)),
122 )
123 .into_response())
124}
125
126pub async fn revoke_eab(
133 State(state): State<AdminState>,
134 Path(kid): Path<String>,
135 AuthenticatedWrite(auth): AuthenticatedWrite,
136 request_context: acme_proxy_core::audit::RequestContext,
137) -> Result<StatusCode, AdminError> {
138 apply_revoke_eab(&state, &Caller::api(&auth, &request_context), &kid).await?;
139 Ok(StatusCode::NO_CONTENT)
140}
141
142pub async fn delete_eab(
152 State(state): State<AdminState>,
153 Path(kid): Path<String>,
154 Query(params): Query<DeleteEabParams>,
155 AuthenticatedWrite(auth): AuthenticatedWrite,
156 request_context: acme_proxy_core::audit::RequestContext,
157) -> Result<Json<Value>, AdminError> {
158 let accounts = params.resolve()?;
159 let deleted = apply_delete_eab(
160 &state,
161 &Caller::api(&auth, &request_context),
162 &kid,
163 accounts,
164 )
165 .await?;
166
167 let orders: u64 = deleted.deleted.iter().map(|(_, orders)| orders).sum();
168 Ok(Json(serde_json::json!({
169 "deleted": { "accounts": deleted.deleted.len(), "orders": orders },
170 "deactivatedAccounts": deleted.deactivated.len(),
171 "keptAccounts": deleted.remaining,
172 })))
173}
174
175pub(crate) async fn apply_create_eab(
179 state: &AdminState,
180 caller: &Caller<'_>,
181 label: Option<String>,
182 profile: Option<String>,
183 hint: &str,
184) -> Result<Eab, AdminError> {
185 require_mounted_profile(state, profile.as_deref(), hint)?;
186
187 let eab = Eab::create(label, profile, &state.database).await?;
188 state
189 .record_admin_action(caller.request, caller.username(), |actor, client| {
190 acme_proxy_jobs::auditor::admin::eab_created(
191 actor,
192 client,
193 &eab.kid.to_string(),
194 eab.profile.as_deref(),
195 eab.label.as_deref(),
196 )
197 })
198 .await;
199 tracing::info!(event = "admin_eab_created",
200 outcome = "success",
201 surface = caller.surface,
202 kid = %eab.kid,
203 profile = ?eab.profile,
204 username = %caller.username());
205 Ok(eab)
206}
207
208pub(crate) async fn apply_revoke_eab(
212 state: &AdminState,
213 caller: &Caller<'_>,
214 kid: &str,
215) -> Result<(), AdminError> {
216 let subject = Eab::find_any_by_kid(kid, &state.database).await?;
217 if !Eab::revoke(kid, &state.database).await? {
218 return Err(not_found(kid));
219 }
220 if let Some(eab) = subject.as_ref().filter(|eab| eab.status == "active") {
221 state
222 .record_admin_action(caller.request, caller.username(), |actor, client| {
223 acme_proxy_jobs::auditor::admin::eab_revoked(
224 actor,
225 client,
226 kid,
227 eab.profile.as_deref(),
228 )
229 })
230 .await;
231 }
232 tracing::info!(event = "admin_eab_revoked",
233 outcome = "success",
234 surface = caller.surface,
235 kid = %kid,
236 username = %caller.username());
237 Ok(())
238}
239
240pub(crate) async fn apply_delete_eab(
244 state: &AdminState,
245 caller: &Caller<'_>,
246 kid: &str,
247 accounts: BoundAccounts,
248) -> Result<DeletedEab, AdminError> {
249 let deleted = deleted_or_refused(
250 kid,
251 admin::delete_eab(kid, accounts, state.database.clone()).await?,
252 )?;
253
254 state
255 .record_admin_actions(caller.request, caller.username(), |actor, client| {
256 acme_proxy_jobs::auditor::admin::eab_deleted_records(actor, client, &deleted)
257 })
258 .await;
259 tracing::info!(event = "admin_eab_deleted",
260 outcome = "success",
261 surface = caller.surface,
262 kid = %kid,
263 accounts = accounts.as_str(),
264 username = %caller.username());
265 Ok(deleted)
266}
267
268pub(crate) fn deleted_or_refused(
271 kid: &str,
272 deletion: EabDeletion,
273) -> Result<DeletedEab, AdminError> {
274 match deletion {
275 EabDeletion::NotFound => Err(not_found(kid)),
276 EabDeletion::LiveCertificates {
277 accounts,
278 certificates,
279 } => Err(AdminError::conflict(
280 "live_certificates",
281 admin::eab_live_certificates_refusal(kid, accounts, certificates),
282 )),
283 EabDeletion::Deleted(deleted) => Ok(deleted),
284 }
285}
286
287pub(crate) fn require_mounted_profile(
292 state: &AdminState,
293 profile: Option<&str>,
294 hint: &str,
295) -> Result<(), AdminError> {
296 match crate::admin::ops::unmounted_profile_refusal(
297 |name| state.profiles.contains_key(name),
298 profile,
299 hint,
300 ) {
301 Some(message) => Err(AdminError::bad_request(message)),
302 None => Ok(()),
303 }
304}
305
306fn not_found(kid: &str) -> AdminError {
307 AdminError::not_found(crate::admin::subject::Subject::EabCredential.missing(kid))
308}