secretumvault/src/api/server.rs

222 lines
6.5 KiB
Rust
Raw Normal View History

2025-12-22 21:34:01 +00:00
#[cfg(feature = "server")]
use axum::{
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use std::sync::Arc;
use super::handlers;
use super::{ApiResponse, HealthResponse, SealRequest, SealStatus};
use crate::core::VaultCore;
/// Build the API router with all mounted engines and system endpoints
#[cfg(feature = "server")]
pub fn build_router(vault: Arc<VaultCore>) -> Router<Arc<VaultCore>> {
let mut router = Router::new()
// System endpoints
.route("/v1/sys/health", get(sys_health))
.route("/v1/sys/status", get(sys_status))
.route("/v1/sys/seal", post(sys_seal))
.route("/v1/sys/unseal", post(sys_unseal))
.route("/v1/sys/mounts", get(sys_list_mounts))
.route("/v1/sys/init", get(sys_init_status))
// Metrics endpoint (Prometheus format)
.route("/metrics", get(metrics_endpoint))
.with_state(vault.clone());
// Dynamically mount routes for each registered engine
for (mount_path, _engine) in vault.engines.iter() {
let mount_clean = mount_path.trim_end_matches('/');
let wildcard_path = format!("/v1{mount_clean}/*path");
router = router.route(
&wildcard_path,
get(handlers::read_secret)
.post(handlers::write_secret)
.delete(handlers::delete_secret)
.put(handlers::update_secret),
);
// Also add route without trailing path
let base_path = format!("/v1{mount_clean}");
router = router.route(
&base_path,
get(handlers::read_secret)
.post(handlers::write_secret)
.delete(handlers::delete_secret)
.put(handlers::update_secret),
);
}
router
}
/// GET /v1/sys/health - Health check endpoint
#[cfg(feature = "server")]
async fn sys_health(State(vault): State<Arc<VaultCore>>) -> impl IntoResponse {
let sealed = {
let seal = vault.seal.blocking_lock();
seal.is_sealed()
};
let response = ApiResponse::success(HealthResponse {
sealed,
initialized: true,
});
(StatusCode::OK, Json(response))
}
/// POST /v1/sys/seal - Seal the vault
#[cfg(feature = "server")]
async fn sys_seal(State(vault): State<Arc<VaultCore>>) -> impl IntoResponse {
let mut seal = vault.seal.lock().await;
seal.seal();
let response = ApiResponse::success(SealStatus {
sealed: true,
shares_needed: None,
});
(StatusCode::OK, Json(response))
}
/// POST /v1/sys/unseal - Unseal the vault with shares
#[cfg(feature = "server")]
async fn sys_unseal(
State(vault): State<Arc<VaultCore>>,
Json(payload): Json<SealRequest>,
) -> impl IntoResponse {
if let Some(shares) = payload.shares {
let shares_data: Vec<&[u8]> = shares.iter().map(|s| s.as_bytes()).collect();
let mut seal = vault.seal.lock().await;
match seal.unseal(&shares_data) {
Ok(_) => {
let response = ApiResponse::success(SealStatus {
sealed: seal.is_sealed(),
shares_needed: None,
});
(StatusCode::OK, Json(response)).into_response()
}
Err(e) => {
let response =
ApiResponse::<serde_json::Value>::error(format!("Unseal failed: {}", e));
(StatusCode::BAD_REQUEST, Json(response)).into_response()
}
}
} else {
let response = ApiResponse::<()>::error("Missing shares in request");
(StatusCode::BAD_REQUEST, Json(response)).into_response()
}
}
/// GET /v1/sys/status - Get vault status
#[cfg(feature = "server")]
async fn sys_status(State(vault): State<Arc<VaultCore>>) -> impl IntoResponse {
let sealed = {
let seal = vault.seal.blocking_lock();
seal.is_sealed()
};
let response = ApiResponse::success(serde_json::json!({
"sealed": sealed,
"initialized": true,
"engines": vault.engines.keys().collect::<Vec<_>>(),
}));
(StatusCode::OK, Json(response))
}
/// GET /v1/sys/mounts - List all mounted engines
#[cfg(feature = "server")]
async fn sys_list_mounts(State(vault): State<Arc<VaultCore>>) -> impl IntoResponse {
let mut mounts = serde_json::Map::new();
for (path, engine) in vault.engines.iter() {
let mount_info = serde_json::json!({
"type": engine.engine_type(),
"name": engine.name(),
"path": path,
});
mounts.insert(path.clone(), mount_info);
}
let response = ApiResponse::success(serde_json::Value::Object(mounts));
(StatusCode::OK, Json(response))
}
/// GET /v1/sys/init - Get initialization status
#[cfg(feature = "server")]
async fn sys_init_status(State(vault): State<Arc<VaultCore>>) -> impl IntoResponse {
let _seal = vault.seal.blocking_lock();
let response = ApiResponse::success(serde_json::json!({
"initialized": true,
}));
(StatusCode::OK, Json(response))
}
/// GET /metrics - Prometheus metrics endpoint
#[cfg(feature = "server")]
async fn metrics_endpoint(State(vault): State<Arc<VaultCore>>) -> impl IntoResponse {
let snapshot = vault.metrics.snapshot();
let metrics_text = snapshot.to_prometheus_text();
(
StatusCode::OK,
[("Content-Type", "text/plain; version=0.0.4")],
metrics_text,
)
}
#[cfg(not(feature = "server"))]
pub fn build_router(_vault: Arc<VaultCore>) -> Router<()> {
Router::new()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_api_response_success() {
let response = ApiResponse::success(json!({"key": "value"}));
assert_eq!(response.status, "success");
assert!(response.error.is_none());
}
#[test]
fn test_api_response_error() {
let response = ApiResponse::<serde_json::Value>::error("Something went wrong");
assert_eq!(response.status, "error");
assert!(response.data.is_none());
assert!(response.error.is_some());
}
#[test]
fn test_health_response() {
let health = HealthResponse {
sealed: false,
initialized: true,
};
assert!(!health.sealed);
assert!(health.initialized);
}
#[test]
fn test_seal_status() {
let status = SealStatus {
sealed: true,
shares_needed: Some(2),
};
assert!(status.sealed);
assert_eq!(status.shares_needed, Some(2));
}
}