222 lines
6.5 KiB
Rust
222 lines
6.5 KiB
Rust
|
|
#[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));
|
||
|
|
}
|
||
|
|
}
|