use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Deserialize;
use serde_json::{Value, json};
use uuid::Uuid;
use crate::admin;
use crate::admin::ops::{RevokeError, RevokeOutcome};
use crate::sqlite::authz::Authorization;
use crate::sqlite::order::{Order, OrderQuery};
use crate::sqlite::status::{OrderStatus, UnknownStatus};
use crate::webadmin::AdminState;
use crate::webadmin::error::AdminError;
use crate::webadmin::handlers::paging::{PageParams, page_envelope};
use crate::webadmin::handlers::params::{empty_is_absent, empty_is_absent_serial};
use crate::webadmin::session::{Authenticated, AuthenticatedWrite};
#[derive(Debug, Deserialize, Default)]
pub struct OrderListParams {
#[serde(default, deserialize_with = "empty_is_absent")]
pub profile: Option<String>,
#[serde(rename = "accountId", default, deserialize_with = "empty_is_absent")]
pub account_id: Option<String>,
#[serde(default, deserialize_with = "empty_is_absent")]
pub status: Option<String>,
#[serde(default, deserialize_with = "empty_is_absent")]
pub identifier: Option<String>,
#[serde(
rename = "identifierContains",
default,
deserialize_with = "empty_is_absent"
)]
pub identifier_contains: Option<String>,
#[serde(
rename = "certSerial",
default,
deserialize_with = "empty_is_absent_serial"
)]
pub cert_serial: Option<String>,
pub limit: Option<i64>,
pub offset: Option<i64>,
}
impl OrderListParams {
pub fn parsed_status(&self) -> Result<Option<OrderStatus>, UnknownStatus> {
self.status.as_deref().map(str::parse).transpose()
}
pub fn check_identifier_filters(&self) -> Result<(), &'static str> {
if self.identifier.is_some() && self.identifier_contains.is_some() {
return Err("give either identifier or identifierContains, not both");
}
Ok(())
}
}
fn bad_identifier_filters(message: &'static str) -> AdminError {
AdminError::with_code(
StatusCode::BAD_REQUEST,
"conflicting_identifier_filter",
message,
)
}
#[derive(Debug, Deserialize, Default)]
pub struct RevokeRequest {
#[serde(default)]
pub reason: Option<u32>,
}
fn bad_status(error: UnknownStatus) -> AdminError {
AdminError::with_code(StatusCode::BAD_REQUEST, "invalid_status", error.to_string())
}
pub async fn list_orders(
State(state): State<AdminState>,
Query(params): Query<OrderListParams>,
_auth: Authenticated,
) -> Result<Json<Value>, AdminError> {
let page = PageParams::from(params.limit, params.offset).resolve(&state.config);
let status = params.parsed_status().map_err(bad_status)?;
params
.check_identifier_filters()
.map_err(bad_identifier_filters)?;
let query = OrderQuery {
profile: params.profile,
account_id: params.account_id,
status,
identifier: params.identifier,
identifier_contains: params.identifier_contains,
cert_serial: params.cert_serial,
limit: page.limit,
offset: page.offset,
};
let (orders, total) = Order::search(&query, &state.database).await?;
let items = render_orders(&orders, &state).await?;
Ok(Json(page_envelope(items, total, page)))
}
pub async fn get_order(
State(state): State<AdminState>,
Path(id): Path<String>,
_auth: Authenticated,
) -> Result<Json<Value>, AdminError> {
let detail = admin::load_order_detail(&id, state.database.clone())
.await?
.ok_or_else(|| not_found(&id))?;
Ok(Json(admin::render_order_detail_json(
&detail,
&state.config.server.base_url,
)))
}
pub async fn revoke_order(
State(state): State<AdminState>,
Path(id): Path<String>,
request_context: crate::audit::RequestContext,
AuthenticatedWrite(auth): AuthenticatedWrite,
body: Option<Json<RevokeRequest>>,
) -> Result<Json<Value>, AdminError> {
let reason = body.and_then(|Json(body)| body.reason);
let signer = resolve_order_signer(&state, &id).await?;
let outcome = admin::revoke_order(
&id,
reason,
crate::audit::Actor::admin(&auth.user.username),
state.audit.client(&request_context).await,
state.database.clone(),
signer,
)
.await
.map_err(revoke_error)?;
match outcome {
RevokeOutcome::NotFound => Err(not_found(&id)),
RevokeOutcome::NotIssued => Err(AdminError::conflict(
"order_not_issued",
format!("order {id} has no certificate to revoke"),
)),
RevokeOutcome::AlreadyRevoked => Err(AdminError::conflict(
"already_revoked",
format!("order {id} was already revoked"),
)),
RevokeOutcome::Revoked(order) => {
tracing::info!(event = "admin_order_revoked",
outcome = "success",
surface = "api",
order_id = %id,
profile = %order.profile,
reason = ?reason,
username = %auth.user.username);
let authz_ids = authz_ids(order.id, &state).await?;
Ok(Json(admin::render_order_json(
&order,
&state.config.server.base_url,
&authz_ids,
)))
}
}
}
pub async fn delete_order(
State(state): State<AdminState>,
Path(id): Path<String>,
AuthenticatedWrite(auth): AuthenticatedWrite,
request_context: crate::audit::RequestContext,
) -> Result<Response, AdminError> {
let subject = Order::find_by_id(&id, &state.database).await?;
let deleted = admin::delete_order(&id, state.database.clone())
.await?
.ok_or_else(|| not_found(&id))?;
if let Some(order) = subject {
state
.record_admin_action(&request_context, &auth.user.username, |actor, client| {
crate::audit::admin::order_deleted(actor, client, &order, deleted.cascaded)
})
.await;
}
tracing::info!(event = "admin_order_deleted",
outcome = "success",
surface = "api",
order_id = %id,
username = %auth.user.username,
cascaded_authorizations = deleted.cascaded);
Ok((
StatusCode::OK,
Json(json!({ "deleted": { "authorizations": deleted.cascaded } })),
)
.into_response())
}
pub(crate) async fn render_orders(
orders: &[Order],
state: &AdminState,
) -> Result<Vec<Value>, AdminError> {
let ids: Vec<Uuid> = orders.iter().map(|order| order.id).collect();
let mut grouped = Authorization::find_ids_by_orders(&ids, &state.database).await?;
let items = orders
.iter()
.map(|order| {
admin::render_order_json(
order,
&state.config.server.base_url,
&grouped.remove(&order.id).unwrap_or_default(),
)
})
.collect();
Ok(items)
}
async fn authz_ids(order_id: Uuid, state: &AdminState) -> Result<Vec<Uuid>, AdminError> {
Ok(Authorization::find_by_order(order_id, &state.database)
.await?
.into_iter()
.map(|authz| authz.id)
.collect())
}
pub(crate) fn revoke_error(error: RevokeError) -> AdminError {
match error {
RevokeError::BadReason(reason) => AdminError::bad_request(format!(
"unsupported revocation reason code {reason} (RFC 5280 §5.3.1)"
)),
RevokeError::Signer(_) => {
tracing::error!(event = "admin_revoke_signer_failed", outcome = "failure", error = %error);
AdminError::signer_failed("the signer backend refused the revocation; retry")
}
RevokeError::Database(inner) => AdminError::from(inner),
RevokeError::Internal(detail) => {
tracing::error!(event = "admin_revoke_internal_error", outcome = "failure", error = %detail);
AdminError::internal()
}
}
}
pub(crate) async fn resolve_order_signer(
state: &AdminState,
id: &str,
) -> Result<std::sync::Arc<dyn crate::signer::SignerBackend>, AdminError> {
let order = Order::find_by_id(id, &state.database)
.await?
.ok_or_else(|| not_found(id))?;
let profile = state.profiles.get(&order.profile).ok_or_else(|| {
AdminError::conflict(
"profile_not_mounted",
format!(
"order {id} belongs to profile `{}`, which this configuration does not mount",
order.profile
),
)
})?;
Ok(profile.signer.clone())
}
fn not_found(id: &str) -> AdminError {
AdminError::not_found(format!("no such order: {id}"))
}