Skip to main content

kipuka/routes/admin/
cas.rs

1//! CA management endpoints for the admin API.
2//!
3//! Provides visibility into configured CA backends, their health status,
4//! and key material metadata (without exposing private keys).
5
6use std::sync::Arc;
7
8use axum::Json;
9use axum::extract::{Path, State};
10use axum::http::StatusCode;
11use axum::response::{IntoResponse, Response};
12use serde::Serialize;
13
14use super::AdminAuth;
15use crate::state::AppState;
16
17/// CA summary returned by `GET /admin/cas`.
18#[derive(Serialize)]
19pub struct CaSummary {
20    /// Unique CA identifier.
21    pub id: String,
22    /// Whether this is the default CA.
23    pub is_default: bool,
24    /// Key type (e.g., "ec:P-256", "rsa:2048").
25    pub key_type: String,
26    /// Hash algorithm (e.g., "sha256").
27    pub hash_algorithm: String,
28    /// Default validity period in days.
29    pub validity_days: u32,
30    /// Health status from the HA subsystem.
31    pub health: String,
32    /// Whether the CA uses an HSM-backed key.
33    pub hsm_backed: bool,
34}
35
36/// CA detail returned by `GET /admin/cas/{id}`.
37#[derive(Serialize)]
38pub struct CaDetail {
39    #[serde(flatten)]
40    pub summary: CaSummary,
41    /// Subject CN of the CA certificate.
42    pub subject_cn: String,
43    /// CRL distribution point URL (if configured).
44    pub crl_url: Option<String>,
45    /// OCSP responder URL (if configured).
46    pub ocsp_url: Option<String>,
47    /// CA/B Forum compliance mode.
48    pub cab_forum_compliant: bool,
49}
50
51/// `GET /admin/cas` — list all configured CAs with health status.
52///
53/// Returns an array of CA summaries including health status from the
54/// HA subsystem.
55pub async fn list_cas(_admin: AdminAuth, State(state): State<Arc<AppState>>) -> Response {
56    let mut cas = Vec::new();
57
58    for ca_config in &state.config.cas {
59        // Determine health status from the HA pool if available.
60        let health = state
61            .ha_manager
62            .as_ref()
63            .and_then(|ha| {
64                let ca_id = crate::ha::CaId(ca_config.id.clone());
65                ha.pool()
66                    .status_snapshot()
67                    .get(&ca_id)
68                    .map(|s| format!("{:?}", s.health))
69            })
70            .unwrap_or_else(|| "unknown".to_string());
71
72        cas.push(CaSummary {
73            id: ca_config.id.clone(),
74            is_default: ca_config.is_default,
75            key_type: ca_config.key_type.clone(),
76            hash_algorithm: ca_config.hash_algorithm.clone(),
77            validity_days: ca_config.validity_days,
78            health,
79            hsm_backed: ca_config.is_hsm_backed(),
80        });
81    }
82
83    (StatusCode::OK, Json(cas)).into_response()
84}
85
86/// `GET /admin/cas/{id}` — CA details.
87///
88/// Returns detailed information about a specific CA, including
89/// certificate metadata and configuration.
90pub async fn get_ca(
91    _admin: AdminAuth,
92    Path(id): Path<String>,
93    State(state): State<Arc<AppState>>,
94) -> Response {
95    let ca_config = match state.config.cas.iter().find(|c| c.id == id) {
96        Some(c) => c,
97        None => return (StatusCode::NOT_FOUND, "CA not found").into_response(),
98    };
99
100    let health = state
101        .ha_manager
102        .as_ref()
103        .and_then(|ha| {
104            ha.pool()
105                .status_snapshot()
106                .get(&id)
107                .map(|s| format!("{:?}", s.health))
108        })
109        .unwrap_or_else(|| "unknown".to_string());
110
111    let detail = CaDetail {
112        summary: CaSummary {
113            id: ca_config.id.clone(),
114            is_default: ca_config.is_default,
115            key_type: ca_config.key_type.clone(),
116            hash_algorithm: ca_config.hash_algorithm.clone(),
117            validity_days: ca_config.validity_days,
118            health,
119            hsm_backed: ca_config.is_hsm_backed(),
120        },
121        subject_cn: ca_config.common_name.clone(),
122        crl_url: ca_config.crl_url.clone(),
123        ocsp_url: ca_config.ocsp_url.clone(),
124        cab_forum_compliant: ca_config.cab_forum_compliant,
125    };
126
127    (StatusCode::OK, Json(detail)).into_response()
128}
129
130/// `GET /admin/cas/{id}/health` — CA health check.
131///
132/// Performs or retrieves an on-demand health check for the specified CA
133/// backend.
134pub async fn get_ca_health(
135    _admin: AdminAuth,
136    Path(id): Path<String>,
137    State(state): State<Arc<AppState>>,
138) -> Response {
139    // Verify the CA exists.
140    if !state.config.cas.iter().any(|c| c.id == id) {
141        return (StatusCode::NOT_FOUND, "CA not found").into_response();
142    }
143
144    let (health, latency_ms) = match state.ha_manager.as_ref() {
145        Some(ha) => {
146            let ca_id_key = crate::ha::CaId(id.clone());
147            let snapshot = ha.pool().status_snapshot();
148            match snapshot.get(&ca_id_key) {
149                Some(status) => {
150                    let health = format!("{:?}", status.health);
151                    let latency = status.latency_ema_ms as u64;
152                    (health, latency)
153                }
154                None => ("unknown".to_string(), 0),
155            }
156        }
157        None => ("not_monitored".to_string(), 0),
158    };
159
160    (
161        StatusCode::OK,
162        Json(serde_json::json!({
163            "ca_id": id,
164            "health": health,
165            "latency_ms": latency_ms,
166        })),
167    )
168        .into_response()
169}