kipuka/routes/admin/mod.rs
1//! Admin API router with separate authentication.
2//!
3//! The admin interface is independent of the EST enrollment endpoints
4//! and uses its own authentication (Bearer token, admin mTLS, or GSSAPI).
5//!
6//! Admin endpoints provide:
7//! - OTP management for EST enrollment
8//! - CA health monitoring and management
9//! - Certificate listing and revocation
10//! - System health checks
11
12pub mod cas;
13pub mod certs;
14pub mod health;
15pub mod otp;
16
17use std::sync::Arc;
18
19use axum::Router;
20use axum::extract::{FromRef, FromRequestParts};
21use axum::http::StatusCode;
22use axum::http::request::Parts;
23use axum::response::{IntoResponse, Response};
24use axum::routing::{delete, get, post};
25
26use crate::state::AppState;
27
28/// Build the admin API sub-router.
29///
30/// All admin routes require admin authentication, which is separate
31/// from the EST enrollment authentication.
32///
33/// # Route structure
34///
35/// ```text
36/// /admin/
37/// health GET — overall system health
38/// health/db GET — database connectivity
39/// health/hsm GET — HSM connectivity
40/// health/ca GET — CA backend health
41/// cas GET — list configured CAs
42/// cas/{id} GET — CA details
43/// cas/{id}/health GET — CA health check
44/// otp/generate POST — generate new OTP
45/// otp GET — list active OTPs
46/// otp/{id} DELETE — revoke OTP
47/// certs GET — list issued certificates
48/// certs/{serial} GET — certificate details
49/// certs/{serial}/revoke POST — revoke certificate
50/// ```
51pub fn admin_router() -> Router<Arc<AppState>> {
52 Router::new()
53 // Health checks
54 .route("/health", get(health::get_health))
55 .route("/health/db", get(health::get_health_db))
56 .route("/health/hsm", get(health::get_health_hsm))
57 .route("/health/ca", get(health::get_health_ca))
58 // CA management
59 .route("/cas", get(cas::list_cas))
60 .route("/cas/{id}", get(cas::get_ca))
61 .route("/cas/{id}/health", get(cas::get_ca_health))
62 // OTP management
63 .route("/otp/generate", post(otp::generate_otp))
64 .route("/otp", get(otp::list_otps))
65 .route("/otp/{id}", delete(otp::revoke_otp))
66 // Certificate management
67 .route("/certs", get(certs::list_certs))
68 .route("/certs/{serial}", get(certs::get_cert))
69 .route("/certs/{serial}/revoke", post(certs::revoke_cert))
70}
71
72/// Authenticated admin context extracted from request headers.
73///
74/// Verifies admin credentials (Bearer token or admin mTLS) before
75/// the handler runs. On failure, returns 401 or 403.
76///
77/// This is intentionally simpler than Akamu's `OperatorContext` since
78/// Kipuka's admin model is less complex (no RBAC roles yet).
79#[derive(Debug, Clone)]
80pub struct AdminAuth {
81 /// The authenticated admin identity (username or cert subject).
82 pub identity: String,
83}
84
85impl<S> FromRequestParts<S> for AdminAuth
86where
87 S: Send + Sync,
88 Arc<AppState>: FromRef<S>,
89{
90 type Rejection = Response;
91
92 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Response> {
93 let _app = Arc::<AppState>::from_ref(state);
94
95 // Check for Bearer token in the Authorization header.
96 if let Some(auth_header) = parts
97 .headers
98 .get(axum::http::header::AUTHORIZATION)
99 .and_then(|v| v.to_str().ok())
100 && let Some(token) = auth_header.strip_prefix("Bearer ")
101 {
102 // TODO: Validate admin bearer token against a configured
103 // admin token or session store.
104 //
105 // For now, accept any non-empty token as a placeholder.
106 if !token.is_empty() {
107 return Ok(AdminAuth {
108 identity: "admin".to_string(),
109 });
110 }
111 }
112
113 // Check for admin mTLS client certificate.
114 if let Some(cert) = parts.extensions.get::<crate::auth::mtls::PeerCertificate>()
115 && !cert.0.is_empty()
116 {
117 // TODO: Validate the cert against the admin truststore
118 // (separate from the EST truststore per RHELBU-3536 R18).
119 return Ok(AdminAuth {
120 identity: "admin-cert".to_string(),
121 });
122 }
123
124 Err((
125 StatusCode::UNAUTHORIZED,
126 "admin authentication required: Bearer token or mTLS certificate",
127 )
128 .into_response())
129 }
130}