- Add badges, competitive comparison, and 30-sec demo to README - Add Production Status section showing OQS backend is production-ready - Mark PQC KEM/signing operations complete in roadmap - Fix GitHub URL - Create CHANGELOG.md documenting all recent changes Positions SecretumVault as first Rust vault with production PQC.
229 lines
8.5 KiB
Rust
229 lines
8.5 KiB
Rust
use std::sync::Arc;
|
|
|
|
#[cfg(feature = "server")]
|
|
use axum::{
|
|
extract::{Extension, Path},
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
Json,
|
|
};
|
|
use serde_json::{json, Value};
|
|
|
|
use super::ApiResponse;
|
|
use crate::core::VaultCore;
|
|
|
|
/// Helper: Try reading with fallback path reconstruction
|
|
#[cfg(feature = "server")]
|
|
async fn try_fallback_read(
|
|
vault: &Arc<VaultCore>,
|
|
full_path: &str,
|
|
) -> Option<axum::response::Response> {
|
|
for (mount_path, _) in vault.engines.iter() {
|
|
let slash = if full_path.starts_with('/') { "" } else { "/" };
|
|
let reconstructed = format!("{}{}{}", mount_path, slash, full_path);
|
|
|
|
let (_, relative_path) = vault.split_path(&reconstructed)?;
|
|
let engine = vault.route_to_engine(&reconstructed)?;
|
|
let engine_path = relative_path.trim_start_matches('/');
|
|
|
|
if let Ok(Some(data)) = engine.read(engine_path).await {
|
|
let response = ApiResponse::success(data);
|
|
return Some((StatusCode::OK, Json(response)).into_response());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// GET /v1/* - Read a secret from any mounted engine
|
|
#[cfg(feature = "server")]
|
|
pub async fn read_secret(
|
|
Extension(vault): Extension<Arc<VaultCore>>,
|
|
Path(path): Path<String>,
|
|
) -> impl IntoResponse {
|
|
let full_path = path;
|
|
|
|
if let Some((_mount_path, relative_path)) = vault.split_path(&full_path) {
|
|
if let Some(engine) = vault.route_to_engine(&full_path) {
|
|
return match engine.read(&relative_path).await {
|
|
Ok(Some(data)) => {
|
|
let response = ApiResponse::success(data);
|
|
(StatusCode::OK, Json(response)).into_response()
|
|
}
|
|
Ok(None) => {
|
|
let response = ApiResponse::<Value>::error("Secret not found");
|
|
(StatusCode::NOT_FOUND, Json(response)).into_response()
|
|
}
|
|
Err(e) => {
|
|
let response = ApiResponse::<Value>::error(format!("Failed to read: {}", e));
|
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(response)).into_response()
|
|
}
|
|
};
|
|
}
|
|
let response = ApiResponse::<Value>::error("No engine mounted at this path");
|
|
return (StatusCode::NOT_FOUND, Json(response)).into_response();
|
|
}
|
|
|
|
// Try fallback path reconstruction
|
|
if let Some(response) = try_fallback_read(&vault, &full_path).await {
|
|
return response;
|
|
}
|
|
|
|
let response = ApiResponse::<Value>::error("Path not found");
|
|
(StatusCode::NOT_FOUND, Json(response)).into_response()
|
|
}
|
|
|
|
/// Helper: Try writing with fallback path reconstruction
|
|
#[cfg(feature = "server")]
|
|
async fn try_fallback_write(
|
|
vault: &Arc<VaultCore>,
|
|
full_path: &str,
|
|
payload: &Value,
|
|
) -> Option<axum::response::Response> {
|
|
for (mount_path, _) in vault.engines.iter() {
|
|
let slash = if full_path.starts_with('/') { "" } else { "/" };
|
|
let reconstructed = format!("{}{}{}", mount_path, slash, full_path);
|
|
|
|
let (_, relative_path) = vault.split_path(&reconstructed)?;
|
|
let engine = vault.route_to_engine(&reconstructed)?;
|
|
let engine_path = relative_path.trim_start_matches('/');
|
|
|
|
if engine.write(engine_path, payload).await.is_ok() {
|
|
let response = ApiResponse::success(json!({"path": full_path}));
|
|
return Some((StatusCode::OK, Json(response)).into_response());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// POST /v1/* - Write a secret to any mounted engine
|
|
#[cfg(feature = "server")]
|
|
pub async fn write_secret(
|
|
Extension(vault): Extension<Arc<VaultCore>>,
|
|
Path(path): Path<String>,
|
|
Json(payload): Json<Value>,
|
|
) -> impl IntoResponse {
|
|
let full_path = path;
|
|
|
|
if let Some((_mount_path, relative_path)) = vault.split_path(&full_path) {
|
|
if let Some(engine) = vault.route_to_engine(&full_path) {
|
|
return match engine.write(&relative_path, &payload).await {
|
|
Ok(()) => {
|
|
let response = ApiResponse::success(json!({"path": full_path}));
|
|
(StatusCode::OK, Json(response)).into_response()
|
|
}
|
|
Err(e) => {
|
|
let response = ApiResponse::<Value>::error(format!("Failed to write: {}", e));
|
|
(StatusCode::BAD_REQUEST, Json(response)).into_response()
|
|
}
|
|
};
|
|
}
|
|
let response = ApiResponse::<Value>::error("No engine mounted at this path");
|
|
return (StatusCode::NOT_FOUND, Json(response)).into_response();
|
|
}
|
|
|
|
// Try fallback path reconstruction
|
|
if let Some(response) = try_fallback_write(&vault, &full_path, &payload).await {
|
|
return response;
|
|
}
|
|
|
|
let response = ApiResponse::<Value>::error("Path not found");
|
|
(StatusCode::NOT_FOUND, Json(response)).into_response()
|
|
}
|
|
|
|
/// PUT /v1/* - Update a secret in any mounted engine
|
|
#[cfg(feature = "server")]
|
|
pub async fn update_secret(
|
|
Extension(vault): Extension<Arc<VaultCore>>,
|
|
Path(path): Path<String>,
|
|
Json(payload): Json<Value>,
|
|
) -> impl IntoResponse {
|
|
let full_path = path;
|
|
|
|
match vault.split_path(&full_path) {
|
|
Some((_mount_path, relative_path)) => match vault.route_to_engine(&full_path) {
|
|
Some(engine) => match engine.write(&relative_path, &payload).await {
|
|
Ok(()) => {
|
|
let response = ApiResponse::success(json!({"path": full_path}));
|
|
(StatusCode::OK, Json(response)).into_response()
|
|
}
|
|
Err(e) => {
|
|
let response = ApiResponse::<Value>::error(format!("Failed to update: {}", e));
|
|
(StatusCode::BAD_REQUEST, Json(response)).into_response()
|
|
}
|
|
},
|
|
None => {
|
|
let response = ApiResponse::<Value>::error("No engine mounted at this path");
|
|
(StatusCode::NOT_FOUND, Json(response)).into_response()
|
|
}
|
|
},
|
|
None => {
|
|
let response = ApiResponse::<Value>::error("Path not found");
|
|
(StatusCode::NOT_FOUND, Json(response)).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// DELETE /v1/* - Delete a secret from any mounted engine
|
|
#[cfg(feature = "server")]
|
|
pub async fn delete_secret(
|
|
Extension(vault): Extension<Arc<VaultCore>>,
|
|
Path(path): Path<String>,
|
|
) -> impl IntoResponse {
|
|
let full_path = path;
|
|
|
|
match vault.split_path(&full_path) {
|
|
Some((_mount_path, relative_path)) => match vault.route_to_engine(&full_path) {
|
|
Some(engine) => match engine.delete(&relative_path).await {
|
|
Ok(()) => {
|
|
let response: ApiResponse<Value> = ApiResponse::success(json!({}));
|
|
(StatusCode::NO_CONTENT, Json(response)).into_response()
|
|
}
|
|
Err(e) => {
|
|
let response = ApiResponse::<Value>::error(format!("Failed to delete: {}", e));
|
|
(StatusCode::BAD_REQUEST, Json(response)).into_response()
|
|
}
|
|
},
|
|
None => {
|
|
let response = ApiResponse::<Value>::error("No engine mounted at this path");
|
|
(StatusCode::NOT_FOUND, Json(response)).into_response()
|
|
}
|
|
},
|
|
None => {
|
|
let response = ApiResponse::<Value>::error("Path not found");
|
|
(StatusCode::NOT_FOUND, Json(response)).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// LIST /v1/* - List secrets at a path prefix
|
|
#[cfg(feature = "server")]
|
|
pub async fn list_secrets(
|
|
Extension(vault): Extension<Arc<VaultCore>>,
|
|
Path(path): Path<String>,
|
|
) -> impl IntoResponse {
|
|
let full_path = path;
|
|
|
|
match vault.split_path(&full_path) {
|
|
Some((_mount_path, relative_path)) => match vault.route_to_engine(&full_path) {
|
|
Some(engine) => match engine.list(&relative_path).await {
|
|
Ok(items) => {
|
|
let response = ApiResponse::success(json!({"keys": items}));
|
|
(StatusCode::OK, Json(response)).into_response()
|
|
}
|
|
Err(e) => {
|
|
let response = ApiResponse::<Value>::error(format!("Failed to list: {}", e));
|
|
(StatusCode::BAD_REQUEST, Json(response)).into_response()
|
|
}
|
|
},
|
|
None => {
|
|
let response = ApiResponse::<Value>::error("No engine mounted at this path");
|
|
(StatusCode::NOT_FOUND, Json(response)).into_response()
|
|
}
|
|
},
|
|
None => {
|
|
let response = ApiResponse::<Value>::error("Path not found");
|
|
(StatusCode::NOT_FOUND, Json(response)).into_response()
|
|
}
|
|
}
|
|
}
|