acme_proxy/webadmin/handlers/
orders.rs1use axum::Json;
4use axum::extract::{Path, Query, State};
5use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use serde::Deserialize;
8use serde_json::{Value, json};
9use uuid::Uuid;
10
11use crate::admin;
12use crate::admin::ops::{RevokeError, RevokeOutcome};
13use crate::sqlite::authz::Authorization;
14use crate::sqlite::order::{Order, OrderQuery};
15use crate::sqlite::status::{OrderStatus, UnknownStatus};
16use crate::webadmin::AdminState;
17use crate::webadmin::error::AdminError;
18use crate::webadmin::handlers::paging::{PageParams, page_envelope};
19use crate::webadmin::handlers::params::empty_is_absent;
20use crate::webadmin::session::{Authenticated, AuthenticatedWrite};
21
22#[derive(Debug, Deserialize, Default)]
25pub struct OrderListParams {
26 #[serde(default, deserialize_with = "empty_is_absent")]
27 pub profile: Option<String>,
28 #[serde(rename = "accountId", default, deserialize_with = "empty_is_absent")]
29 pub account_id: Option<String>,
30 #[serde(default, deserialize_with = "empty_is_absent")]
31 pub status: Option<String>,
32 pub limit: Option<i64>,
33 pub offset: Option<i64>,
34}
35
36impl OrderListParams {
37 pub fn parsed_status(&self) -> Result<Option<OrderStatus>, UnknownStatus> {
44 self.status.as_deref().map(str::parse).transpose()
45 }
46}
47
48#[derive(Debug, Deserialize, Default)]
49pub struct RevokeRequest {
50 #[serde(default)]
52 pub reason: Option<u32>,
53}
54
55fn bad_status(error: UnknownStatus) -> AdminError {
57 AdminError::with_code(StatusCode::BAD_REQUEST, "invalid_status", error.to_string())
58}
59
60pub async fn list_orders(
62 State(state): State<AdminState>,
63 Query(params): Query<OrderListParams>,
64 _auth: Authenticated,
65) -> Result<Json<Value>, AdminError> {
66 let page = PageParams::from(params.limit, params.offset).resolve(&state.config);
67 let status = params.parsed_status().map_err(bad_status)?;
69 let query = OrderQuery {
70 profile: params.profile,
71 account_id: params.account_id,
72 status,
73 limit: page.limit,
74 offset: page.offset,
75 };
76 let (orders, total) = Order::search(&query, &state.database).await?;
77
78 let items = render_orders(&orders, &state).await?;
79 Ok(Json(page_envelope(items, total, page)))
80}
81
82pub async fn get_order(
84 State(state): State<AdminState>,
85 Path(id): Path<String>,
86 _auth: Authenticated,
87) -> Result<Json<Value>, AdminError> {
88 let detail = admin::load_order_detail(&id, state.database.clone())
89 .await?
90 .ok_or_else(|| not_found(&id))?;
91 Ok(Json(admin::render_order_detail_json(
92 &detail,
93 &state.config.server.base_url,
94 )))
95}
96
97pub async fn revoke_order(
107 State(state): State<AdminState>,
108 Path(id): Path<String>,
109 request_context: crate::audit::RequestContext,
110 AuthenticatedWrite(auth): AuthenticatedWrite,
111 body: Option<Json<RevokeRequest>>,
112) -> Result<Json<Value>, AdminError> {
113 let reason = body.and_then(|Json(body)| body.reason);
114
115 let signer = resolve_order_signer(&state, &id).await?;
119
120 let outcome = admin::revoke_order(
124 &id,
125 reason,
126 crate::audit::Actor::admin(&auth.user.username),
127 state.audit.client(&request_context).await,
128 state.database.clone(),
129 signer,
130 )
131 .await
132 .map_err(revoke_error)?;
133
134 match outcome {
135 RevokeOutcome::NotFound => Err(not_found(&id)),
136 RevokeOutcome::NotIssued => Err(AdminError::conflict(
137 "order_not_issued",
138 format!("order {id} has no certificate to revoke"),
139 )),
140 RevokeOutcome::AlreadyRevoked => Err(AdminError::conflict(
141 "already_revoked",
142 format!("order {id} was already revoked"),
143 )),
144 RevokeOutcome::Revoked(order) => {
145 tracing::info!(event = "admin_order_revoked",
146 outcome = "success",
147 surface = "api",
148 order_id = %id,
149 profile = %order.profile,
150 reason = ?reason,
151 username = %auth.user.username);
152 let authz_ids = authz_ids(order.id, &state).await?;
153 Ok(Json(admin::render_order_json(
154 &order,
155 &state.config.server.base_url,
156 &authz_ids,
157 )))
158 }
159 }
160}
161
162pub async fn delete_order(
164 State(state): State<AdminState>,
165 Path(id): Path<String>,
166 AuthenticatedWrite(auth): AuthenticatedWrite,
167) -> Result<Response, AdminError> {
168 let deleted = admin::delete_order(&id, state.database.clone())
169 .await?
170 .ok_or_else(|| not_found(&id))?;
171
172 tracing::info!(event = "admin_order_deleted",
173 outcome = "success",
174 surface = "api",
175 order_id = %id,
176 username = %auth.user.username,
177 cascaded_authorizations = deleted.cascaded);
178 Ok((
179 StatusCode::OK,
180 Json(json!({ "deleted": { "authorizations": deleted.cascaded } })),
181 )
182 .into_response())
183}
184
185pub(crate) async fn render_orders(
190 orders: &[Order],
191 state: &AdminState,
192) -> Result<Vec<Value>, AdminError> {
193 let ids: Vec<Uuid> = orders.iter().map(|order| order.id).collect();
196 let mut grouped = Authorization::find_ids_by_orders(&ids, &state.database).await?;
197
198 let items = orders
199 .iter()
200 .map(|order| {
201 admin::render_order_json(
202 order,
203 &state.config.server.base_url,
204 &grouped.remove(&order.id).unwrap_or_default(),
205 )
206 })
207 .collect();
208 Ok(items)
209}
210
211async fn authz_ids(order_id: Uuid, state: &AdminState) -> Result<Vec<Uuid>, AdminError> {
214 Ok(Authorization::find_by_order(order_id, &state.database)
215 .await?
216 .into_iter()
217 .map(|authz| authz.id)
218 .collect())
219}
220
221pub(crate) fn revoke_error(error: RevokeError) -> AdminError {
228 match error {
229 RevokeError::BadReason(reason) => AdminError::bad_request(format!(
230 "unsupported revocation reason code {reason} (RFC 5280 §5.3.1)"
231 )),
232 RevokeError::Signer(_) => {
233 tracing::error!(event = "admin_revoke_signer_failed", outcome = "failure", error = %error);
234 AdminError::signer_failed("the signer backend refused the revocation; retry")
235 }
236 RevokeError::Database(inner) => AdminError::from(inner),
237 RevokeError::Internal(detail) => {
238 tracing::error!(event = "admin_revoke_internal_error", outcome = "failure", error = %detail);
239 AdminError::internal()
240 }
241 }
242}
243
244pub(crate) async fn resolve_order_signer(
255 state: &AdminState,
256 id: &str,
257) -> Result<std::sync::Arc<dyn crate::signer::SignerBackend>, AdminError> {
258 let order = Order::find_by_id(id, &state.database)
259 .await?
260 .ok_or_else(|| not_found(id))?;
261 let profile = state.profiles.get(&order.profile).ok_or_else(|| {
262 AdminError::conflict(
263 "profile_not_mounted",
264 format!(
265 "order {id} belongs to profile `{}`, which this configuration does not mount",
266 order.profile
267 ),
268 )
269 })?;
270 Ok(profile.signer.clone())
271}
272
273fn not_found(id: &str) -> AdminError {
274 AdminError::not_found(format!("no such order: {id}"))
275}