feat: integrate tracing for structured logging

- Added `tracing` and `tracing-subscriber` for improved logging, replacing `println` statements with `info`, `debug`, `warn`, and `error`.
- Annotated key methods with `#[instrument]` for better tracing of function calls and arguments.
- Configured logging initialization in `main.rs` with `EnvFilter` to control log verbosity.
This commit is contained in:
Harald Hoyer 2025-03-20 14:59:22 +01:00
parent f11b83ddf4
commit 8f28cc1af2
5 changed files with 243 additions and 81 deletions

View file

@ -13,6 +13,5 @@ axum = { version = "0.6.18", features = ["multipart"] }
uuid = { version = "1.3.0", features = ["v4", "serde"] }
sha2 = "0.10.6"
base64 = "0.21.0"
tower = "0.4.13"
tower-http = { version = "0.4.0", features = ["cors"] }
futures = "0.3.28"
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }

View file

@ -10,6 +10,7 @@ use axum::{
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::net::TcpListener;
use tracing::{info, error, debug, instrument};
use crate::document_service::{Document, DocumentService, SignatureVerification};
use crate::vault_setup::VaultClient;
@ -48,6 +49,7 @@ pub struct SignDocumentRequest {
// API response implementations
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
error!("API error: {}", self.0);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error: {}", self.0),
@ -66,12 +68,13 @@ where
}
// Start the API server
#[instrument(skip(vault_addr, root_token))]
pub async fn start_api(
vault_addr: &str,
root_token: &str,
api_port: u16,
) -> Result<()> {
println!("Starting API server on port {}...", api_port);
info!("Starting API server on port {}...", api_port);
// Initialize Vault client
let vault_client = VaultClient::new(vault_addr, root_token);
@ -101,14 +104,17 @@ pub async fn start_api(
.route("/api/documents/:id/verify", get(verify_document))
.with_state(state);
info!("API routes configured");
// Start server
let listener = TcpListener::bind(format!("0.0.0.0:{}", api_port)).await?;
println!("API server started on port {}", api_port);
info!("API server started on port {}", api_port);
// Get the socket address
let addr = listener.local_addr()?;
// Bind and serve
info!("Serving API at {}", addr);
Server::bind(&addr)
.serve(app.into_make_service())
.await?;
@ -117,27 +123,36 @@ pub async fn start_api(
}
// Health check endpoint
#[instrument]
async fn health_check() -> &'static str {
debug!("Health check endpoint called");
"OK"
}
// Login endpoint
#[instrument(skip(state, request), fields(username = %request.username))]
async fn login(
State(state): State<ApiState>,
Json(request): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, ApiError> {
info!("Login attempt for user: {}", request.username);
let token = state.vault_client
.login_user(&request.username, &request.password)
.await?;
info!("User {} successfully authenticated", request.username);
Ok(Json(LoginResponse { token }))
}
// Upload document endpoint
#[instrument(skip(state, multipart))]
async fn upload_document(
State(state): State<ApiState>,
mut multipart: Multipart,
) -> Result<Json<Document>, ApiError> {
info!("Document upload request received");
let mut document_name = String::new();
let mut document_content = Vec::new();
@ -147,12 +162,15 @@ async fn upload_document(
if name == "name" {
document_name = field.text().await?;
debug!("Received document name: {}", document_name);
} else if name == "file" {
document_content = field.bytes().await?.to_vec();
debug!("Received document content: {} bytes", document_content.len());
}
}
if document_name.is_empty() || document_content.is_empty() {
error!("Missing document name or content");
return Err(anyhow::anyhow!("Missing document name or content").into());
}
@ -166,27 +184,37 @@ async fn upload_document(
.get_document(&document_id)
.await?;
info!("Document uploaded successfully with ID: {}", document_id);
Ok(Json(document))
}
// Get document endpoint
#[instrument(skip(state))]
async fn get_document(
State(state): State<ApiState>,
Path(document_id): Path<String>,
) -> Result<Json<Document>, ApiError> {
info!("Fetching document: {}", document_id);
let document = state.document_service
.get_document(&document_id)
.await?;
debug!("Retrieved document {} with {} signatures",
document.id, document.signatures.len());
Ok(Json(document))
}
// Sign document endpoint
#[instrument(skip(state, request), fields(document_id = %document_id, username = %request.username))]
async fn sign_document(
State(state): State<ApiState>,
Path(document_id): Path<String>,
Json(request): Json<SignDocumentRequest>,
) -> Result<Json<Document>, ApiError> {
info!("Signing request for document {} by user {}", document_id, request.username);
state.document_service
.sign_document(&document_id, &request.username, &request.token)
.await?;
@ -195,17 +223,24 @@ async fn sign_document(
.get_document(&document_id)
.await?;
info!("Document {} successfully signed by {}", document_id, request.username);
Ok(Json(document))
}
// Verify document endpoint
#[instrument(skip(state))]
async fn verify_document(
State(state): State<ApiState>,
Path(document_id): Path<String>,
) -> Result<Json<SignatureVerification>, ApiError> {
info!("Verifying document signatures: {}", document_id);
let verification = state.document_service
.verify_document_signatures(&document_id)
.await?;
info!("Document {} verification result: {}",
document_id, if verification.is_verified { "VERIFIED" } else { "PENDING" });
Ok(Json(verification))
}

View file

@ -6,6 +6,7 @@ use sha2::{Sha256, Digest};
use std::collections::HashMap;
use uuid::Uuid;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use tracing::{info, error, debug, instrument};
use crate::vault_setup::{Department, User, VaultClient};
@ -52,14 +53,19 @@ impl DocumentService {
}
// Upload a new document and store its metadata
#[instrument(skip(self, content), fields(document_name = %name))]
pub async fn upload_document(&self, name: &str, content: &[u8]) -> Result<String> {
info!("Uploading new document: {}", name);
// Generate a unique ID
let id = Uuid::new_v4().to_string();
debug!("Generated document ID: {}", id);
// Calculate document hash
let mut hasher = Sha256::new();
hasher.update(content);
let hash = format!("{:x}", hasher.finalize());
debug!("Document hash: {}", hash);
// Create document metadata
let document = Document {
@ -73,12 +79,15 @@ impl DocumentService {
// Store document metadata in Vault
self.store_document_metadata(&document).await?;
println!("Document uploaded with ID: {}", id);
info!("Document uploaded with ID: {}", id);
Ok(id)
}
// Store document metadata in Vault
#[instrument(skip(self, document), fields(document_id = %document.id))]
async fn store_document_metadata(&self, document: &Document) -> Result<()> {
debug!("Storing document metadata for {}", document.id);
let url = format!("{}/v1/documents/data/docs/{}",
self.vault_client.addr, document.id);
@ -104,18 +113,22 @@ impl DocumentService {
match response.status() {
StatusCode::OK | StatusCode::NO_CONTENT => {
println!("Successfully stored document metadata for {}", document.id);
info!("Successfully stored document metadata for {}", document.id);
Ok(())
}
status => {
let error_text = response.text().await?;
error!("Failed to store document metadata: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to store document metadata: {} - {}", status, error_text))
}
}
}
// Get document metadata
#[instrument(skip(self))]
pub async fn get_document(&self, document_id: &str) -> Result<Document> {
debug!("Getting document metadata for {}", document_id);
let url = format!("{}/v1/documents/data/docs/{}",
self.vault_client.addr, document_id);
@ -137,7 +150,10 @@ impl DocumentService {
let status = match status_str {
"pending" => DocumentStatus::Pending,
"verified" => DocumentStatus::Verified,
_ => return Err(anyhow::anyhow!("Unknown status: {}", status_str)),
_ => {
error!("Unknown document status: {}", status_str);
return Err(anyhow::anyhow!("Unknown status: {}", status_str));
}
};
// Extract signatures
@ -160,17 +176,22 @@ impl DocumentService {
signatures,
};
debug!("Retrieved document: {} with {} signatures", document.id, document.signatures.len());
Ok(document)
}
status => {
let error_text = response.text().await?;
error!("Failed to get document: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to get document: {} - {}", status, error_text))
}
}
}
// Sign a document with the user's key
#[instrument(skip(self, user_token), fields(document_id = %document_id, username = %username))]
pub async fn sign_document(&self, document_id: &str, username: &str, user_token: &str) -> Result<()> {
info!("Signing document {} by user {}", document_id, username);
// Get document metadata
let document = self.get_document(document_id).await?;
@ -200,6 +221,8 @@ impl DocumentService {
.context("Failed to extract signature")?
.to_string();
debug!("Generated signature for document {}", document_id);
// Update document with signature
self.add_signature(document_id, username, &signature).await?;
@ -209,18 +232,22 @@ impl DocumentService {
// Check if document now has enough signatures
self.update_document_status(document_id).await?;
println!("Document {} signed by {}", document_id, username);
info!("Document {} signed by {}", document_id, username);
Ok(())
}
status => {
let error_text = response.text().await?;
error!("Failed to sign document: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to sign document: {} - {}", status, error_text))
}
}
}
// Add a signature to a document
#[instrument(skip(self, signature), fields(document_id = %document_id, username = %username))]
async fn add_signature(&self, document_id: &str, username: &str, signature: &str) -> Result<()> {
debug!("Adding signature from {} to document {}", username, document_id);
// Get current document
let mut document = self.get_document(document_id).await?;
@ -230,16 +257,20 @@ impl DocumentService {
// Store updated document
self.store_document_metadata(&document).await?;
debug!("Added signature from {} to document {}", username, document_id);
Ok(())
}
// Record department signature
#[instrument(skip(self), fields(document_id = %document_id, department = ?user.department, username = %user.username))]
async fn record_department_signature(&self, document_id: &str, user: &User) -> Result<()> {
let dept_str = match user.department {
Department::Legal => "legal",
Department::Finance => "finance",
};
debug!("Recording {} department signature for document {}", dept_str, document_id);
let url = format!("{}/v1/documents/data/dept/{}/signatures/{}",
self.vault_client.addr, dept_str, document_id);
@ -287,18 +318,22 @@ impl DocumentService {
match response.status() {
StatusCode::OK | StatusCode::NO_CONTENT => {
println!("Recorded signature for {} in {} department", user.username, dept_str);
info!("Recorded signature for {} in {} department", user.username, dept_str);
Ok(())
}
status => {
let error_text = response.text().await?;
error!("Failed to record department signature: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to record department signature: {} - {}", status, error_text))
}
}
}
// Update document status if it has enough signatures
#[instrument(skip(self))]
async fn update_document_status(&self, document_id: &str) -> Result<()> {
debug!("Checking if document {} has enough signatures", document_id);
// Verify signatures
let verification = self.verify_document_signatures(document_id).await?;
@ -312,14 +347,26 @@ impl DocumentService {
// Store updated document
self.store_document_metadata(&document).await?;
println!("Document {} marked as verified", document_id);
info!("Document {} marked as verified", document_id);
} else {
debug!(
"Document {} not yet verified. Has {}/{} signatures ({} legal, {} finance)",
document_id,
verification.signatures_count,
verification.required_signatures,
verification.legal_signatures,
verification.required_finance
);
}
Ok(())
}
// Verify document signatures
#[instrument(skip(self))]
pub async fn verify_document_signatures(&self, document_id: &str) -> Result<SignatureVerification> {
info!("Verifying signatures for document {}", document_id);
// Get document
let document = self.get_document(document_id).await?;
@ -337,6 +384,7 @@ impl DocumentService {
StatusCode::OK => response.json::<serde_json::Value>().await?,
status => {
let error_text = response.text().await?;
error!("Failed to get signing requirements: {} - {}", status, error_text);
return Err(anyhow::anyhow!("Failed to get signing requirements: {} - {}", status, error_text));
}
};
@ -384,11 +432,26 @@ impl DocumentService {
required_finance,
};
info!(
"Verification result for document {}: verified={}, signatures={}/{}, legal={}/{}, finance={}/{}",
document_id,
verification.is_verified,
verification.signatures_count,
verification.required_signatures,
verification.legal_signatures,
verification.required_legal,
verification.finance_signatures,
verification.required_finance
);
Ok(verification)
}
// Get department signatures for a document
#[instrument(skip(self))]
async fn get_department_signatures(&self, document_id: &str, department: &str) -> Result<Vec<String>> {
debug!("Getting {} department signatures for document {}", department, document_id);
let url = format!("{}/v1/documents/data/dept/{}/signatures/{}",
self.vault_client.addr, department, document_id);
@ -414,6 +477,9 @@ impl DocumentService {
}
}
debug!("Found {} signatures for {} department on document {}",
signatures.len(), department, document_id);
Ok(signatures)
}
}

View file

@ -9,6 +9,8 @@ use std::{
time::Duration,
};
use tokio::time::sleep;
use tracing::{info, warn, error, debug, instrument};
use tracing_subscriber::{fmt, EnvFilter};
// Import our library
use vault_hier::start_api;
@ -52,7 +54,7 @@ fn save_credentials(response: &InitResponse, file_path: &str) -> Result<()> {
let mut file = File::create(Path::new(file_path))?;
file.write_all(serde_json::to_string_pretty(&json)?.as_bytes())?;
println!("Credentials saved to JSON file: {}", file_path);
info!("Credentials saved to JSON file: {}", file_path);
return Ok(());
}
@ -69,13 +71,14 @@ fn save_credentials(response: &InitResponse, file_path: &str) -> Result<()> {
writeln!(file)?;
writeln!(file, "Root Token: {}", response.root_token)?;
println!("Credentials saved to {}", file_path);
info!("Credentials saved to {}", file_path);
Ok(())
}
// Wait for Vault to become available
#[instrument(skip(addr))]
async fn wait_for_vault(addr: &str) -> Result<()> {
println!("Waiting for Vault to be ready...");
info!("Waiting for Vault to be ready...");
let client = Client::new();
@ -87,30 +90,32 @@ async fn wait_for_vault(addr: &str) -> Result<()> {
let status = response.status().as_u16();
// Accept any of these status codes as "available"
if matches!(status, 200 | 429 | 472 | 473 | 501 | 503) {
println!("Vault is available! Status code: {}", status);
info!("Vault is available! Status code: {}", status);
return Ok(());
}
println!("Vault returned unexpected status code: {}", status);
debug!("Vault returned unexpected status code: {}", status);
},
Err(e) => {
println!("Error connecting to Vault: {}", e);
debug!("Error connecting to Vault: {}", e);
}
}
if i == 30 {
error!("Timed out waiting for Vault to become available");
return Err(anyhow::anyhow!("Timed out waiting for Vault to become available"));
}
println!("Vault is unavailable - sleeping (attempt {}/30)", i);
info!("Vault is unavailable - sleeping (attempt {}/30)", i);
sleep(Duration::from_secs(2)).await;
}
Ok(())
}
#[instrument(skip(client, addr))]
async fn check_init_status(client: &Client, addr: &str) -> Result<bool> {
println!("Checking if Vault is already initialized...");
info!("Checking if Vault is already initialized...");
let response = client
.get(format!("{}/v1/sys/init", addr))
@ -128,8 +133,9 @@ async fn check_init_status(client: &Client, addr: &str) -> Result<bool> {
Ok(false)
}
#[instrument(skip(client, addr))]
async fn check_seal_status(client: &Client, addr: &str) -> Result<SealStatusResponse> {
println!("Checking Vault seal status...");
info!("Checking Vault seal status...");
let response = client
.get(format!("{}/v1/sys/seal-status", addr))
@ -138,24 +144,27 @@ async fn check_seal_status(client: &Client, addr: &str) -> Result<SealStatusResp
if response.status().is_success() {
let status = response.json::<SealStatusResponse>().await?;
println!("Seal status: sealed={}, threshold={}, shares={}, progress={}",
info!("Seal status: sealed={}, threshold={}, shares={}, progress={}",
status.sealed, status.t, status.n, status.progress);
return Ok(status);
} else {
let error_text = response.text().await?;
error!("Failed to get seal status: {}", error_text);
anyhow::bail!("Failed to get seal status: {}", error_text);
}
}
#[instrument(skip(client, addr))]
async fn init_vault(client: &Client, addr: &str) -> Result<InitResponse> {
// First check if already initialized
let initialized = check_init_status(client, addr).await?;
if initialized {
error!("Vault is already initialized. Cannot re-initialize.");
anyhow::bail!("Vault is already initialized. Cannot re-initialize.");
}
println!("Initializing Vault...");
info!("Initializing Vault...");
// Configure with 5 key shares and a threshold of 3
// This is a standard production configuration, requiring 3 out of 5 keys to unseal
@ -173,32 +182,39 @@ async fn init_vault(client: &Client, addr: &str) -> Result<InitResponse> {
match response.status() {
StatusCode::OK => {
let init_response = response.json::<InitResponse>().await?;
println!("Vault initialized successfully!");
info!("Vault initialized successfully!");
Ok(init_response)
}
status => {
let error_text = response.text().await?;
error!("Failed to initialize Vault: {} - {}", status, error_text);
anyhow::bail!("Failed to initialize Vault: {} - {}", status, error_text);
}
}
}
#[instrument(skip(client, addr, unseal_keys))]
async fn unseal_vault(client: &Client, addr: &str, unseal_keys: &[String]) -> Result<()> {
// First check the current seal status
let mut seal_status = check_seal_status(client, addr).await?;
if !seal_status.sealed {
println!("Vault is already unsealed!");
info!("Vault is already unsealed!");
return Ok(());
}
println!("Unsealing Vault...");
info!("Unsealing Vault...");
// We need to provide enough keys to meet the threshold
// The threshold is in seal_status.t
let required_keys = seal_status.t as usize;
if unseal_keys.len() < required_keys {
error!(
"Not enough unseal keys provided. Need {} keys, but only have {}",
required_keys,
unseal_keys.len()
);
anyhow::bail!(
"Not enough unseal keys provided. Need {} keys, but only have {}",
required_keys,
@ -208,7 +224,7 @@ async fn unseal_vault(client: &Client, addr: &str, unseal_keys: &[String]) -> Re
// Apply each key one at a time until unsealed
for (i, key) in unseal_keys.iter().take(required_keys).enumerate() {
println!("Applying unseal key {}/{}...", i + 1, required_keys);
info!("Applying unseal key {}/{}...", i + 1, required_keys);
let unseal_req = UnsealRequest {
key: key.clone(),
@ -222,6 +238,7 @@ async fn unseal_vault(client: &Client, addr: &str, unseal_keys: &[String]) -> Re
if !response.status().is_success() {
let error_text = response.text().await?;
error!("Failed to apply unseal key: {}", error_text);
anyhow::bail!("Failed to apply unseal key: {}", error_text);
}
@ -229,13 +246,14 @@ async fn unseal_vault(client: &Client, addr: &str, unseal_keys: &[String]) -> Re
seal_status = check_seal_status(client, addr).await?;
if !seal_status.sealed {
println!("Vault unsealed successfully after applying {} keys!", i + 1);
info!("Vault unsealed successfully after applying {} keys!", i + 1);
return Ok(());
}
}
// If we get here, we've applied all keys but Vault is still sealed
if seal_status.sealed {
error!("Applied all available unseal keys, but Vault is still sealed");
anyhow::bail!("Applied all available unseal keys, but Vault is still sealed");
}
@ -244,6 +262,12 @@ async fn unseal_vault(client: &Client, addr: &str, unseal_keys: &[String]) -> Re
#[tokio::main]
async fn main() -> Result<()> {
// Initialize tracing
fmt()
.with_env_filter(EnvFilter::from_default_env().add_directive("vault_hier=info".parse()?))
.with_target(false)
.init();
// Get Vault address from env var or use default
let vault_addr = env::var("VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string());
let client = Client::new();
@ -254,8 +278,8 @@ async fn main() -> Result<()> {
.parse::<u16>()
.unwrap_or(3000);
println!("Vault address: {}", vault_addr);
println!("Connecting to Vault at: {}", vault_addr);
info!("Vault address: {}", vault_addr);
info!("Connecting to Vault at: {}", vault_addr);
// Wait for Vault to be available
wait_for_vault(&vault_addr).await?;
@ -266,10 +290,10 @@ async fn main() -> Result<()> {
Ok(response) => {
if response.status().is_success() {
let status_text = response.text().await?;
println!("Vault status: {}", status_text);
info!("Vault status: {}", status_text);
}
},
Err(e) => println!("Error getting Vault status: {}", e),
Err(e) => warn!("Error getting Vault status: {}", e),
}
// First check if Vault is already initialized
@ -277,44 +301,44 @@ async fn main() -> Result<()> {
let mut root_token = String::new();
if initialized {
println!("Vault is already initialized.");
info!("Vault is already initialized.");
// Check if Vault is sealed
let seal_status = check_seal_status(&client, &vault_addr).await?;
if seal_status.sealed {
println!("Vault is sealed. Looking for unseal keys...");
info!("Vault is sealed. Looking for unseal keys...");
// Try to load unseal keys from environment variables
let mut unseal_keys = Vec::new();
for i in 1..=5 {
match env::var(format!("VAULT_UNSEAL_KEY_{}", i)) {
Ok(key) => {
println!("Found unseal key {} from environment", i);
info!("Found unseal key {} from environment", i);
unseal_keys.push(key);
},
Err(_) => {
println!("Unseal key {} not found in environment", i);
debug!("Unseal key {} not found in environment", i);
}
}
}
// If we have unseal keys, try to unseal
if !unseal_keys.is_empty() {
println!("Found {} unseal keys. Attempting to unseal...", unseal_keys.len());
info!("Found {} unseal keys. Attempting to unseal...", unseal_keys.len());
unseal_vault(&client, &vault_addr, &unseal_keys).await?;
} else {
println!("No unseal keys found. Vault remains sealed.");
println!("To unseal, set VAULT_UNSEAL_KEY_1, VAULT_UNSEAL_KEY_2, etc. environment variables.");
warn!("No unseal keys found. Vault remains sealed.");
info!("To unseal, set VAULT_UNSEAL_KEY_1, VAULT_UNSEAL_KEY_2, etc. environment variables.");
}
} else {
println!("Vault is already unsealed.");
info!("Vault is already unsealed.");
}
// Try to load root token from environment or credentials file
match env::var("VAULT_TOKEN") {
Ok(token) => {
println!("Found root token from environment");
info!("Found root token from environment");
root_token = token;
},
Err(_) => {
@ -322,7 +346,7 @@ async fn main() -> Result<()> {
if let Ok(contents) = std::fs::read_to_string("vault-credentials.json") {
if let Ok(creds) = serde_json::from_str::<serde_json::Value>(&contents) {
if let Some(token) = creds["root_token"].as_str() {
println!("Found root token from credentials file");
info!("Found root token from credentials file");
root_token = token.to_string();
}
}
@ -331,47 +355,48 @@ async fn main() -> Result<()> {
}
if root_token.is_empty() {
error!("Unable to find root token. Please set VAULT_TOKEN environment variable or provide vault-credentials.json file.");
anyhow::bail!("Unable to find root token. Please set VAULT_TOKEN environment variable or provide vault-credentials.json file.");
}
} else {
// Initialize Vault
println!("Vault is not initialized. Proceeding with initialization...");
info!("Vault is not initialized. Proceeding with initialization...");
let init_response = init_vault(&client, &vault_addr).await?;
// Save credentials to files
println!("Saving credentials to files...");
info!("Saving credentials to files...");
let current_dir = std::env::current_dir().context("Failed to get current directory")?;
// Save as JSON (new format)
let json_path = current_dir.join("vault-credentials.json");
save_credentials(&init_response, json_path.to_str().unwrap())?;
println!("JSON credentials saved to: {}", json_path.display());
info!("JSON credentials saved to: {}", json_path.display());
// Save as text (for backward compatibility)
let text_path = current_dir.join("vault-credentials.txt");
save_credentials(&init_response, text_path.to_str().unwrap())?;
println!("Text credentials saved to: {}", text_path.display());
info!("Text credentials saved to: {}", text_path.display());
// Also save to /app/data as a backup for Docker volume mounting
if let Ok(()) = std::fs::create_dir_all("/app/data") {
let docker_json_path = "/app/data/vault-credentials.json";
save_credentials(&init_response, docker_json_path)?;
println!("Backup JSON credentials saved to Docker volume at: {}", docker_json_path);
info!("Backup JSON credentials saved to Docker volume at: {}", docker_json_path);
let docker_text_path = "/app/data/vault-credentials.txt";
save_credentials(&init_response, docker_text_path)?;
println!("Backup text credentials saved to Docker volume at: {}", docker_text_path);
info!("Backup text credentials saved to Docker volume at: {}", docker_text_path);
}
println!("=========================================");
println!("IMPORTANT: SAVE THESE CREDENTIALS SECURELY");
println!("=========================================");
println!("Root Token: {}", init_response.root_token);
println!("Unseal Keys (first 3 of 5 needed to unseal):");
info!("=========================================");
info!("IMPORTANT: SAVE THESE CREDENTIALS SECURELY");
info!("=========================================");
info!("Root Token: {}", init_response.root_token);
info!("Unseal Keys (first 3 of 5 needed to unseal):");
for (i, key) in init_response.keys_base64.iter().enumerate() {
println!("Key {}: {}", i + 1, key);
info!("Key {}: {}", i + 1, key);
}
println!("=========================================");
info!("=========================================");
// Unseal Vault using the first three keys
let unseal_keys = init_response.keys_base64.iter()
@ -381,7 +406,7 @@ async fn main() -> Result<()> {
unseal_vault(&client, &vault_addr, &unseal_keys).await?;
println!("Vault initialization and unseal complete!");
info!("Vault initialization and unseal complete!");
// Set root token
root_token = init_response.root_token;
@ -390,11 +415,11 @@ async fn main() -> Result<()> {
// Look for any existing credentials and copy them to the mounted volume
if let Ok(metadata) = std::fs::metadata("vault-credentials.json") {
if metadata.is_file() {
println!("Found JSON credentials file, ensuring it's saved to Docker volume...");
info!("Found JSON credentials file, ensuring it's saved to Docker volume...");
if let Ok(()) = std::fs::create_dir_all("/app/data") {
match std::fs::copy("vault-credentials.json", "/app/data/vault-credentials.json") {
Ok(_) => println!("JSON credentials saved to Docker volume"),
Err(e) => println!("Failed to copy JSON credentials: {}", e),
Ok(_) => info!("JSON credentials saved to Docker volume"),
Err(e) => warn!("Failed to copy JSON credentials: {}", e),
}
}
}
@ -402,23 +427,23 @@ async fn main() -> Result<()> {
if let Ok(metadata) = std::fs::metadata("vault-credentials.txt") {
if metadata.is_file() {
println!("Found text credentials file, ensuring it's saved to Docker volume...");
info!("Found text credentials file, ensuring it's saved to Docker volume...");
if let Ok(()) = std::fs::create_dir_all("/app/data") {
match std::fs::copy("vault-credentials.txt", "/app/data/vault-credentials.txt") {
Ok(_) => println!("Text credentials saved to Docker volume"),
Err(e) => println!("Failed to copy text credentials: {}", e),
Ok(_) => info!("Text credentials saved to Docker volume"),
Err(e) => warn!("Failed to copy text credentials: {}", e),
}
}
}
}
println!("Vault setup complete!");
println!("Starting hierarchical document signing API...");
info!("Vault setup complete!");
info!("Starting hierarchical document signing API...");
// Start the hierarchical signing API
start_api(&vault_addr, &root_token, api_port).await?;
println!("API server shutdown. Exiting.");
info!("API server shutdown. Exiting.");
Ok(())
}

View file

@ -2,6 +2,7 @@ use anyhow::{Context, Result};
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{info, error, debug, instrument};
// Department types for organizational structure
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -35,8 +36,9 @@ impl VaultClient {
}
// Enable required secrets engines
#[instrument(skip(self))]
pub async fn setup_secrets_engines(&self) -> Result<()> {
println!("Setting up Vault secrets engines...");
info!("Setting up Vault secrets engines...");
// Enable Transit for document signing
self.enable_secrets_engine("transit", "transit").await?;
@ -47,13 +49,14 @@ impl VaultClient {
// Enable userpass for authentication
self.enable_auth_method("userpass").await?;
println!("Secrets engines setup complete!");
info!("Secrets engines setup complete!");
Ok(())
}
// Enable a secrets engine
#[instrument(skip(self))]
async fn enable_secrets_engine(&self, engine_type: &str, path: &str) -> Result<()> {
println!("Enabling {} secrets engine at {}", engine_type, path);
info!("Enabling {} secrets engine at {}", engine_type, path);
let url = format!("{}/v1/sys/mounts/{}", self.addr, path);
let payload = json!({
@ -69,29 +72,32 @@ impl VaultClient {
match response.status() {
StatusCode::NO_CONTENT | StatusCode::OK => {
println!("Successfully enabled {} engine at {}", engine_type, path);
info!("Successfully enabled {} engine at {}", engine_type, path);
Ok(())
}
StatusCode::BAD_REQUEST => {
// Check if already exists
let error_text = response.text().await?;
if error_text.contains("path is already in use") {
println!("Secrets engine already enabled at {}", path);
info!("Secrets engine already enabled at {}", path);
Ok(())
} else {
error!("Failed to enable secrets engine: {}", error_text);
Err(anyhow::anyhow!("Failed to enable secrets engine: {}", error_text))
}
}
status => {
let error_text = response.text().await?;
error!("Failed to enable secrets engine: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to enable secrets engine: {} - {}", status, error_text))
}
}
}
// Enable an auth method
#[instrument(skip(self))]
async fn enable_auth_method(&self, method: &str) -> Result<()> {
println!("Enabling {} auth method", method);
info!("Enabling {} auth method", method);
let url = format!("{}/v1/sys/auth/{}", self.addr, method);
let payload = json!({
@ -107,29 +113,32 @@ impl VaultClient {
match response.status() {
StatusCode::NO_CONTENT | StatusCode::OK => {
println!("Successfully enabled {} auth method", method);
info!("Successfully enabled {} auth method", method);
Ok(())
}
StatusCode::BAD_REQUEST => {
// Check if already exists
let error_text = response.text().await?;
if error_text.contains("path is already in use") {
println!("Auth method already enabled at {}", method);
info!("Auth method already enabled at {}", method);
Ok(())
} else {
error!("Failed to enable auth method: {}", error_text);
Err(anyhow::anyhow!("Failed to enable auth method: {}", error_text))
}
}
status => {
let error_text = response.text().await?;
error!("Failed to enable auth method: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to enable auth method: {} - {}", status, error_text))
}
}
}
// Create a new user in Vault and associate with department
#[instrument(skip(self, password))]
pub async fn create_user(&self, username: &str, password: &str, department: Department) -> Result<()> {
println!("Creating user {} in department {:?}", username, department);
info!("Creating user {} in department {:?}", username, department);
// Step 1: Create a policy for the user
let policy_name = format!("{}-policy", username);
@ -151,7 +160,7 @@ impl VaultClient {
match response.status() {
StatusCode::NO_CONTENT | StatusCode::OK => {
println!("Successfully created user {}", username);
info!("Successfully created user {}", username);
// Step 3: Create a signing key for the user
self.create_signing_key(username).await?;
@ -163,12 +172,14 @@ impl VaultClient {
}
status => {
let error_text = response.text().await?;
error!("Failed to create user: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to create user: {} - {}", status, error_text))
}
}
}
// Create a signing policy for a user based on their department
#[instrument(skip(self))]
async fn create_signing_policy(&self, policy_name: &str, department: Department) -> Result<()> {
let dept_name = match department {
Department::Legal => "legal",
@ -212,17 +223,19 @@ impl VaultClient {
match response.status() {
StatusCode::NO_CONTENT | StatusCode::OK => {
println!("Successfully created policy {}", policy_name);
info!("Successfully created policy {}", policy_name);
Ok(())
}
status => {
let error_text = response.text().await?;
error!("Failed to create policy: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to create policy: {} - {}", status, error_text))
}
}
}
// Create a signing key for a user in the Transit engine
#[instrument(skip(self))]
async fn create_signing_key(&self, username: &str) -> Result<()> {
let url = format!("{}/v1/transit/keys/{}", self.addr, username);
let payload = json!({
@ -238,17 +251,19 @@ impl VaultClient {
match response.status() {
StatusCode::NO_CONTENT | StatusCode::OK => {
println!("Successfully created signing key for {}", username);
info!("Successfully created signing key for {}", username);
Ok(())
}
status => {
let error_text = response.text().await?;
error!("Failed to create signing key: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to create signing key: {} - {}", status, error_text))
}
}
}
// Store user metadata in the KV store
#[instrument(skip(self))]
async fn store_user_metadata(&self, username: &str, department: Department) -> Result<()> {
let dept_str = match department {
Department::Legal => "legal",
@ -272,22 +287,27 @@ impl VaultClient {
match response.status() {
StatusCode::NO_CONTENT | StatusCode::OK => {
println!("Successfully stored metadata for user {}", username);
info!("Successfully stored metadata for user {}", username);
Ok(())
}
status => {
let error_text = response.text().await?;
error!("Failed to store user metadata: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to store user metadata: {} - {}", status, error_text))
}
}
}
// Create 10 users with departmental hierarchy - 5 in each department
#[instrument(skip(self))]
pub async fn setup_hierarchical_users(&self) -> Result<()> {
info!("Setting up hierarchical user structure");
// Create 5 users in Legal department
for i in 1..=5 {
let username = format!("legal{}", i);
let password = format!("legal{}pass", i);
debug!(username, "Creating Legal department user");
self.create_user(&username, &password, Department::Legal).await?;
}
@ -295,18 +315,22 @@ impl VaultClient {
for i in 1..=5 {
let username = format!("finance{}", i);
let password = format!("finance{}pass", i);
debug!(username, "Creating Finance department user");
self.create_user(&username, &password, Department::Finance).await?;
}
// Setup document signing requirements
self.setup_signing_requirements().await?;
println!("Successfully created 10 users in hierarchical structure!");
info!("Successfully created 10 users in hierarchical structure!");
Ok(())
}
// Configure document signing requirements
#[instrument(skip(self))]
async fn setup_signing_requirements(&self) -> Result<()> {
info!("Setting up document signing requirements");
let url = format!("{}/v1/documents/data/config/signing_requirements", self.addr);
let payload = json!({
"data": {
@ -336,18 +360,22 @@ impl VaultClient {
match response.status() {
StatusCode::NO_CONTENT | StatusCode::OK => {
println!("Successfully configured signing requirements");
info!("Successfully configured signing requirements");
Ok(())
}
status => {
let error_text = response.text().await?;
error!("Failed to configure signing requirements: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to configure signing requirements: {} - {}", status, error_text))
}
}
}
// Login a user and get their token
#[instrument(skip(self, password))]
pub async fn login_user(&self, username: &str, password: &str) -> Result<String> {
info!("Attempting user login: {}", username);
let url = format!("{}/v1/auth/userpass/login/{}", self.addr, username);
let payload = json!({
"password": password,
@ -367,18 +395,22 @@ impl VaultClient {
.context("Failed to extract client token")?
.to_string();
println!("User {} successfully logged in", username);
info!("User {} successfully logged in", username);
Ok(token)
}
status => {
let error_text = response.text().await?;
error!("Failed to login: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to login: {} - {}", status, error_text))
}
}
}
// Get user info including department
#[instrument(skip(self))]
pub async fn get_user_info(&self, username: &str) -> Result<User> {
debug!("Getting user info for: {}", username);
let url = format!("{}/v1/documents/data/users/{}", self.addr, username);
let response = self.client
@ -397,9 +429,13 @@ impl VaultClient {
let department = match department_str {
"legal" => Department::Legal,
"finance" => Department::Finance,
_ => return Err(anyhow::anyhow!("Unknown department: {}", department_str)),
_ => {
error!("Unknown department: {}", department_str);
return Err(anyhow::anyhow!("Unknown department: {}", department_str));
}
};
debug!("Retrieved user info for {} in {:?} department", username, department);
Ok(User {
username: username.to_string(),
department,
@ -407,6 +443,7 @@ impl VaultClient {
}
status => {
let error_text = response.text().await?;
error!("Failed to get user info: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to get user info: {} - {}", status, error_text))
}
}