1use axum::Json;
9use axum::extract::{Path, Query, State};
10use axum::http::StatusCode;
11use axum::response::{IntoResponse, Response};
12use serde::Deserialize;
13use serde_json::{Value, json};
14use uuid::Uuid;
15
16use crate::admin;
17use crate::admin::ops::{RevokeError, RevokeOutcome};
18use crate::webadmin::AdminState;
19use crate::webadmin::error::AdminError;
20use crate::webadmin::handlers::Caller;
21use crate::webadmin::handlers::paging::{PageParams, page_envelope};
22use crate::webadmin::handlers::params::{bad_status, empty_is_absent, empty_is_absent_serial};
23use crate::webadmin::session::{Authenticated, AuthenticatedWrite};
24use acme_proxy_store::authz::Authorization;
25use acme_proxy_store::order::Order;
26use acme_proxy_store::order::OrderQuery;
27use acme_proxy_store::status::OrderStatus;
28use acme_proxy_store::status::UnknownStatus;
29
30#[derive(Debug, Deserialize, Default)]
33pub struct OrderListParams {
34 #[serde(default, deserialize_with = "empty_is_absent")]
35 pub profile: Option<String>,
36 #[serde(rename = "accountId", default, deserialize_with = "empty_is_absent")]
37 pub account_id: Option<String>,
38 #[serde(default, deserialize_with = "empty_is_absent")]
39 pub status: Option<String>,
40 #[serde(default, deserialize_with = "empty_is_absent")]
42 pub identifier: Option<String>,
43 #[serde(
46 rename = "identifierContains",
47 default,
48 deserialize_with = "empty_is_absent"
49 )]
50 pub identifier_contains: Option<String>,
51 #[serde(
53 rename = "certSerial",
54 default,
55 deserialize_with = "empty_is_absent_serial"
56 )]
57 pub cert_serial: Option<String>,
58 pub limit: Option<i64>,
59 pub offset: Option<i64>,
60}
61
62impl OrderListParams {
63 pub fn parsed_status(&self) -> Result<Option<OrderStatus>, UnknownStatus> {
70 self.status.as_deref().map(str::parse).transpose()
71 }
72
73 pub fn check_identifier_filters(&self) -> Result<(), &'static str> {
78 if self.identifier.is_some() && self.identifier_contains.is_some() {
79 return Err("give either identifier or identifierContains, not both");
80 }
81 Ok(())
82 }
83}
84
85fn bad_identifier_filters(message: &'static str) -> AdminError {
87 AdminError::with_code(
88 StatusCode::BAD_REQUEST,
89 "conflicting_identifier_filter",
90 message,
91 )
92}
93
94#[derive(Debug, Deserialize, Default)]
96pub struct RevokeRequest {
97 #[serde(default)]
99 pub reason: Option<u32>,
100}
101
102pub(crate) fn order_query(
111 params: OrderListParams,
112 page: crate::webadmin::handlers::paging::Page,
113) -> Result<OrderQuery, AdminError> {
114 let status = params.parsed_status().map_err(bad_status)?;
116 params
117 .check_identifier_filters()
118 .map_err(bad_identifier_filters)?;
119 Ok(OrderQuery {
120 profile: params.profile,
121 account_id: params.account_id,
122 status,
123 identifier: params.identifier,
124 identifier_contains: params.identifier_contains,
125 cert_serial: params.cert_serial,
126 limit: page.limit,
127 offset: page.offset,
128 })
129}
130
131pub async fn list_orders(
133 State(state): State<AdminState>,
134 Query(params): Query<OrderListParams>,
135 _auth: Authenticated,
136) -> Result<Json<Value>, AdminError> {
137 let page = PageParams::from(params.limit, params.offset).resolve(&state.config);
138 let query = order_query(params, page)?;
139 let (orders, total) = Order::search(&query, &state.database).await?;
140
141 let items = render_orders(&orders, &state).await?;
142 Ok(Json(page_envelope(items, total, page)))
143}
144
145pub async fn get_order(
147 State(state): State<AdminState>,
148 Path(id): Path<String>,
149 _auth: Authenticated,
150) -> Result<Json<Value>, AdminError> {
151 let detail = admin::load_order_detail(&id, state.database.clone())
152 .await?
153 .ok_or_else(|| not_found(&id))?;
154 Ok(Json(admin::render_order_detail_json(
155 &detail,
156 &state.config.server.base_url,
157 )))
158}
159
160pub async fn revoke_order(
172 State(state): State<AdminState>,
173 Path(id): Path<String>,
174 request_context: acme_proxy_core::audit::RequestContext,
175 AuthenticatedWrite(auth): AuthenticatedWrite,
176 body: Option<Json<RevokeRequest>>,
177) -> Result<Response, AdminError> {
178 let reason = body.and_then(|Json(body)| body.reason);
179 match apply_revoke_order(&state, &Caller::api(&auth, &request_context), &id, reason).await? {
180 Revoked::Now(order) => {
181 let authz_ids = authz_ids(order.id, &state).await?;
182 Ok(Json(admin::render_order_json(
183 &order,
184 &state.config.server.base_url,
185 &authz_ids,
186 ))
187 .into_response())
188 }
189 Revoked::Queued(job) => Ok((
192 StatusCode::ACCEPTED,
193 Json(serde_json::json!({ "status": "queued", "job": job.to_string() })),
194 )
195 .into_response()),
196 }
197}
198
199pub(crate) enum Revoked {
201 Now(Box<Order>),
203 Queued(Uuid),
206}
207
208pub(crate) async fn apply_revoke_order(
216 state: &AdminState,
217 caller: &Caller<'_>,
218 id: &str,
219 reason: Option<u32>,
220) -> Result<Revoked, AdminError> {
221 let profile = resolve_order_profile(state, id).await?;
225
226 let route = profile.signer_info.revocation_route();
230 let outcome = admin::revoke_order(
231 id,
232 reason,
233 acme_proxy_core::audit::Actor::admin(caller.username()),
234 state.audit.client(caller.request).await,
235 acme_proxy_protocol::acme::revoke::Revocations {
236 database: &state.database,
237 audit: &state.audit,
238 notify: Some(&profile.notify),
239 revoker: revoker(state, &route),
240 },
241 )
242 .await
243 .map_err(revoke_error)?;
244
245 match outcome {
246 RevokeOutcome::NotFound => Err(not_found(id)),
247 RevokeOutcome::NotIssued => Err(AdminError::conflict(
248 "order_not_issued",
249 format!("order {id} has no certificate to revoke"),
250 )),
251 RevokeOutcome::AlreadyRevoked => Err(AdminError::conflict(
252 "already_revoked",
253 format!("order {id} was already revoked"),
254 )),
255 RevokeOutcome::Revoked(order) => {
256 tracing::info!(event = "admin_order_revoked",
257 outcome = "success",
258 surface = caller.surface,
259 order_id = %id,
260 profile = %order.profile,
261 reason = ?reason,
262 username = %caller.username());
263 Ok(Revoked::Now(order))
264 }
265 RevokeOutcome::Queued(job) => {
266 tracing::info!(event = "admin_order_revoke_queued",
267 outcome = "progress",
268 surface = caller.surface,
269 order_id = %id,
270 job_id = %job,
271 username = %caller.username());
272 Ok(Revoked::Queued(job))
273 }
274 }
275}
276
277pub(crate) fn revoker<'a>(
281 state: &'a AdminState,
282 route: &'a acme_proxy_signer::RevocationRoute,
283) -> acme_proxy_protocol::acme::revoke::Revoker<'a> {
284 acme_proxy_protocol::acme::revoke::Revoker::for_route(
285 route,
286 &state.jobs,
287 acme_proxy_protocol::acme::revoke::request_wait(state.config.server.request_timeout_ms),
288 )
289}
290
291pub async fn delete_order(
293 State(state): State<AdminState>,
294 Path(id): Path<String>,
295 AuthenticatedWrite(auth): AuthenticatedWrite,
296 request_context: acme_proxy_core::audit::RequestContext,
297) -> Result<Response, AdminError> {
298 let cascaded = apply_delete_order(&state, &Caller::api(&auth, &request_context), &id).await?;
299 Ok((
300 StatusCode::OK,
301 Json(json!({ "deleted": { "authorizations": cascaded } })),
302 )
303 .into_response())
304}
305
306pub(crate) async fn apply_delete_order(
309 state: &AdminState,
310 caller: &Caller<'_>,
311 id: &str,
312) -> Result<u64, AdminError> {
313 let subject = Order::find_by_id(id, &state.database).await?;
314 let deleted = match admin::delete_order(id, state.database.clone()).await? {
315 admin::Deletion::NotFound => return Err(not_found(id)),
316 admin::Deletion::LiveCertificates(live) => {
317 return Err(AdminError::conflict(
318 "live_certificates",
319 admin::live_certificates_refusal(&format!("order {id}"), live),
320 ));
321 }
322 admin::Deletion::Deleted(deleted) => deleted,
323 };
324
325 if let Some(order) = subject {
326 state
327 .record_admin_action(caller.request, caller.username(), |actor, client| {
328 acme_proxy_jobs::auditor::admin::order_deleted(
329 actor,
330 client,
331 &order,
332 deleted.cascaded,
333 )
334 })
335 .await;
336 }
337 tracing::info!(event = "admin_order_deleted",
338 outcome = "success",
339 surface = caller.surface,
340 order_id = %id,
341 username = %caller.username(),
342 cascaded_authorizations = deleted.cascaded);
343 Ok(deleted.cascaded)
344}
345
346pub(crate) async fn render_orders(
349 orders: &[Order],
350 state: &AdminState,
351) -> Result<Vec<Value>, AdminError> {
352 Ok(admin::orders_json(orders, &state.config.server.base_url, &state.database).await?)
353}
354
355async fn authz_ids(order_id: Uuid, state: &AdminState) -> Result<Vec<Uuid>, AdminError> {
358 Ok(Authorization::find_by_order(order_id, &state.database)
359 .await?
360 .into_iter()
361 .map(|authz| authz.id)
362 .collect())
363}
364
365pub(crate) fn revoke_error(error: RevokeError) -> AdminError {
373 match error {
374 RevokeError::BadReason(reason) => AdminError::bad_request(format!(
375 "unsupported revocation reason code {reason} (RFC 5280 §5.3.1)"
376 )),
377 RevokeError::Signer(_) | RevokeError::Abandoned { .. } => {
378 tracing::error!(event = "admin_revoke_signer_failed", outcome = "failure", error = %error);
379 AdminError::signer_failed("the signer backend refused the revocation; retry")
380 }
381 RevokeError::Database(inner) => AdminError::from(inner),
382 RevokeError::Internal(detail) => {
383 tracing::error!(event = "admin_revoke_internal_error", outcome = "failure", error = %detail);
384 AdminError::internal()
385 }
386 }
387}
388
389pub(crate) async fn resolve_order_profile(
401 state: &AdminState,
402 id: &str,
403) -> Result<std::sync::Arc<acme_proxy_protocol::profile::Profile>, AdminError> {
404 let order = Order::find_by_id(id, &state.database)
405 .await?
406 .ok_or_else(|| not_found(id))?;
407 let profile = state.profiles.get(&order.profile).ok_or_else(|| {
408 AdminError::conflict(
409 "profile_not_mounted",
410 format!(
411 "order {id} belongs to profile `{}`, which this configuration does not mount",
412 order.profile
413 ),
414 )
415 })?;
416 Ok(profile.clone())
417}
418
419fn not_found(id: &str) -> AdminError {
420 AdminError::not_found(crate::admin::subject::Subject::Order.missing(id))
421}