feat: add CLI commands and server default behavior
- Introduced CLI commands for server, login, upload, sign, verify, and more using `clap`. - Updated Dockerfile and docker-compose to default to `server` command on startup. - Enhanced `test_local.sh` for testing the server and client operations. - Added multipart support to `reqwest` and new CLI documentation in `README.md`. - Updated `Cargo.toml` with new dependencies to support CLI and multipart uploads.
This commit is contained in:
parent
c662dfbfd8
commit
26e81cef17
7 changed files with 461 additions and 29 deletions
323
src/main.rs
323
src/main.rs
|
@ -1,11 +1,111 @@
|
|||
use anyhow::Result;
|
||||
use std::env;
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
use tracing::{info};
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
// Import our library
|
||||
use vault_hier::{start_api, initialize_vault};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about = "Hierarchical Document Signing with HashiCorp Vault")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Start the vault-hier server
|
||||
Server {
|
||||
/// Address of the Vault server
|
||||
#[arg(long, env = "VAULT_ADDR", default_value = "http://127.0.0.1:8200")]
|
||||
vault_addr: String,
|
||||
|
||||
/// Port for the API server
|
||||
#[arg(long, env = "API_PORT", default_value_t = 3000)]
|
||||
api_port: u16,
|
||||
},
|
||||
|
||||
/// Login to get an authentication token
|
||||
Login {
|
||||
/// Username for authentication
|
||||
#[arg(short, long)]
|
||||
username: String,
|
||||
|
||||
/// Password for authentication
|
||||
#[arg(short, long)]
|
||||
password: String,
|
||||
|
||||
/// API server address
|
||||
#[arg(long, default_value = "http://localhost:3000")]
|
||||
api_url: String,
|
||||
},
|
||||
|
||||
/// Upload a document
|
||||
Upload {
|
||||
/// Name of the document
|
||||
#[arg(short, long)]
|
||||
name: String,
|
||||
|
||||
/// Path to the document file
|
||||
#[arg(short, long)]
|
||||
file: PathBuf,
|
||||
|
||||
/// API server address
|
||||
#[arg(long, default_value = "http://localhost:3000")]
|
||||
api_url: String,
|
||||
},
|
||||
|
||||
/// Sign a document
|
||||
Sign {
|
||||
/// Document ID to sign
|
||||
#[arg(short, long)]
|
||||
document_id: String,
|
||||
|
||||
/// Username for signing
|
||||
#[arg(short, long)]
|
||||
username: String,
|
||||
|
||||
/// Authentication token
|
||||
#[arg(short, long)]
|
||||
token: String,
|
||||
|
||||
/// API server address
|
||||
#[arg(long, default_value = "http://localhost:3000")]
|
||||
api_url: String,
|
||||
},
|
||||
|
||||
/// Verify a document's signatures
|
||||
Verify {
|
||||
/// Document ID to verify
|
||||
#[arg(short, long)]
|
||||
document_id: String,
|
||||
|
||||
/// API server address
|
||||
#[arg(long, default_value = "http://localhost:3000")]
|
||||
api_url: String,
|
||||
},
|
||||
|
||||
/// List all documents
|
||||
List {
|
||||
/// API server address
|
||||
#[arg(long, default_value = "http://localhost:3000")]
|
||||
api_url: String,
|
||||
},
|
||||
|
||||
/// Get document details
|
||||
Get {
|
||||
/// Document ID to retrieve
|
||||
#[arg(short, long)]
|
||||
document_id: String,
|
||||
|
||||
/// API server address
|
||||
#[arg(long, default_value = "http://localhost:3000")]
|
||||
api_url: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize tracing
|
||||
|
@ -14,27 +114,230 @@ async fn main() -> Result<()> {
|
|||
.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 cli = Cli::parse();
|
||||
|
||||
// Get API port from env var or use default
|
||||
let api_port = env::var("API_PORT")
|
||||
.unwrap_or_else(|_| "3000".to_string())
|
||||
.parse::<u16>()
|
||||
.unwrap_or(3000);
|
||||
match cli.command {
|
||||
Commands::Server { vault_addr, api_port } => {
|
||||
run_server(&vault_addr, api_port).await?;
|
||||
},
|
||||
Commands::Login { username, password, api_url } => {
|
||||
login(&username, &password, &api_url).await?;
|
||||
},
|
||||
Commands::Upload { name, file, api_url } => {
|
||||
upload_document(&name, file, &api_url).await?;
|
||||
},
|
||||
Commands::Sign { document_id, username, token, api_url } => {
|
||||
sign_document(&document_id, &username, &token, &api_url).await?;
|
||||
},
|
||||
Commands::Verify { document_id, api_url } => {
|
||||
verify_document(&document_id, &api_url).await?;
|
||||
},
|
||||
Commands::List { api_url } => {
|
||||
list_documents(&api_url).await?;
|
||||
},
|
||||
Commands::Get { document_id, api_url } => {
|
||||
get_document(&document_id, &api_url).await?;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_server(vault_addr: &str, api_port: u16) -> Result<()> {
|
||||
info!("Vault address: {}", vault_addr);
|
||||
info!("Connecting to Vault at: {}", vault_addr);
|
||||
|
||||
// Initialize and unseal Vault, get the root token
|
||||
let root_token = initialize_vault(&vault_addr).await?;
|
||||
let root_token = initialize_vault(vault_addr).await?;
|
||||
|
||||
info!("Starting hierarchical document signing API...");
|
||||
|
||||
// Start the hierarchical signing API
|
||||
start_api(&vault_addr, &root_token, api_port).await?;
|
||||
start_api(vault_addr, &root_token, api_port).await?;
|
||||
|
||||
info!("API server shutdown. Exiting.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn login(username: &str, password: &str, api_url: &str) -> Result<()> {
|
||||
info!("Logging in as user: {}", username);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&format!("{}/api/login", api_url))
|
||||
.json(&serde_json::json!({
|
||||
"username": username,
|
||||
"password": password,
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let data: serde_json::Value = response.json().await?;
|
||||
println!("Login successful!");
|
||||
println!("Token: {}", data["token"]);
|
||||
println!("Department: {}", data["department"]);
|
||||
} else {
|
||||
let error_text = response.text().await?;
|
||||
println!("Login failed: {}", error_text);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upload_document(name: &str, file_path: PathBuf, api_url: &str) -> Result<()> {
|
||||
info!("Uploading document: {}", name);
|
||||
|
||||
let file_content = tokio::fs::read(&file_path).await?;
|
||||
let file_name = file_path.file_name().unwrap().to_string_lossy();
|
||||
|
||||
let form = reqwest::multipart::Form::new()
|
||||
.text("name", name.to_string())
|
||||
.part("file", reqwest::multipart::Part::bytes(file_content)
|
||||
.file_name(file_name.to_string()));
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&format!("{}/api/documents", api_url))
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let data: serde_json::Value = response.json().await?;
|
||||
println!("Document uploaded successfully!");
|
||||
println!("Document ID: {}", data["id"]);
|
||||
} else {
|
||||
let error_text = response.text().await?;
|
||||
println!("Upload failed: {}", error_text);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn sign_document(document_id: &str, username: &str, token: &str, api_url: &str) -> Result<()> {
|
||||
info!("Signing document: {}", document_id);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&format!("{}/api/documents/{}/sign", api_url, document_id))
|
||||
.json(&serde_json::json!({
|
||||
"username": username,
|
||||
"token": token,
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let data: serde_json::Value = response.json().await?;
|
||||
println!("Document signed successfully!");
|
||||
println!("Signature ID: {}", data["signature_id"]);
|
||||
} else {
|
||||
let error_text = response.text().await?;
|
||||
println!("Signing failed: {}", error_text);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn verify_document(document_id: &str, api_url: &str) -> Result<()> {
|
||||
info!("Verifying document: {}", document_id);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&format!("{}/api/documents/{}/verify", api_url, document_id))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let data: serde_json::Value = response.json().await?;
|
||||
println!("Verification result:");
|
||||
println!(" Valid: {}", data["valid"]);
|
||||
println!(" Total signatures: {}", data["signature_count"]);
|
||||
println!(" Departments represented: {}", data["departments_represented"]);
|
||||
|
||||
if let Some(signatures) = data["signatures"].as_array() {
|
||||
println!("\nSignatures:");
|
||||
for (i, sig) in signatures.iter().enumerate() {
|
||||
println!(" {}. User: {}, Department: {}, Time: {}",
|
||||
i+1,
|
||||
sig["username"],
|
||||
sig["department"],
|
||||
sig["timestamp"]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let error_text = response.text().await?;
|
||||
println!("Verification failed: {}", error_text);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_documents(api_url: &str) -> Result<()> {
|
||||
info!("Listing all documents");
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&format!("{}/api/documents", api_url))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let data: Vec<serde_json::Value> = response.json().await?;
|
||||
println!("Documents:");
|
||||
|
||||
for (i, doc) in data.iter().enumerate() {
|
||||
println!(" {}. ID: {}, Name: {}", i+1, doc["id"], doc["name"]);
|
||||
}
|
||||
|
||||
if data.is_empty() {
|
||||
println!(" No documents found.");
|
||||
}
|
||||
} else {
|
||||
let error_text = response.text().await?;
|
||||
println!("Failed to list documents: {}", error_text);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_document(document_id: &str, api_url: &str) -> Result<()> {
|
||||
info!("Getting document: {}", document_id);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(&format!("{}/api/documents/{}", api_url, document_id))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let doc: serde_json::Value = response.json().await?;
|
||||
println!("Document details:");
|
||||
println!(" ID: {}", doc["id"]);
|
||||
println!(" Name: {}", doc["name"]);
|
||||
println!(" Hash: {}", doc["hash"]);
|
||||
println!(" Created: {}", doc["created_at"]);
|
||||
|
||||
if let Some(signatures) = doc["signatures"].as_array() {
|
||||
println!("\nSignatures:");
|
||||
for (i, sig) in signatures.iter().enumerate() {
|
||||
println!(" {}. User: {}, Department: {}, Time: {}",
|
||||
i+1,
|
||||
sig["username"],
|
||||
sig["department"],
|
||||
sig["timestamp"]);
|
||||
}
|
||||
|
||||
if signatures.is_empty() {
|
||||
println!(" No signatures yet.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let error_text = response.text().await?;
|
||||
println!("Failed to get document: {}", error_text);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue