Compare commits

..

No commits in common. "c1f76f4c8b0c11ab0fef2a553a0717f450ce9761" and "c65ae95b4365ce3eace88dffb679b261e8de7ff6" have entirely different histories.

19 changed files with 235 additions and 2709 deletions

1
.gitignore vendored
View file

@ -2,6 +2,7 @@
/target/
**/*.rs.bk
*.pdb
Cargo.lock
# Generated by Cargo
.cargo/

2026
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,20 +1,5 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1742394900,
"narHash": "sha256-vVOAp9ahvnU+fQoKd4SEXB2JG2wbENkpqcwlkIXgUC0=",
"owner": "ipetkov",
"repo": "crane",
"rev": "70947c1908108c0c551ddfd73d4f750ff2ea67cd",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
@ -35,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1742751704,
"narHash": "sha256-rBfc+H1dDBUQ2mgVITMGBPI1PGuCznf9rcWX/XIULyE=",
"lastModified": 1742268799,
"narHash": "sha256-IhnK4LhkBlf14/F8THvUy3xi/TxSQkp9hikfDZRD4Ic=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f0946fa5f1fb876a9dc2e1850d9d3a4e3f914092",
"rev": "da044451c6a70518db5b730fe277b70f494188f1",
"type": "github"
},
"original": {
@ -67,7 +52,6 @@
},
"root": {
"inputs": {
"crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
@ -78,11 +62,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1742783666,
"narHash": "sha256-IwdSl51NL6V0f+mYXZR0UTKaGleOsk9zV3l6kt5SUWw=",
"lastModified": 1742437918,
"narHash": "sha256-Vflb6KJVDikFcM9E231mRN88uk4+jo7BWtaaQMifthI=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "60766d63c227d576510ecfb5edd3a687d56f6bc7",
"rev": "f03085549609e49c7bcbbee86a1949057d087199",
"type": "github"
},
"original": {

100
flake.nix
View file

@ -3,18 +3,10 @@
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
crane.url = "github:ipetkov/crane";
};
outputs =
{
self,
nixpkgs,
flake-utils,
rust-overlay,
crane,
}:
flake-utils.lib.eachDefaultSystem (
system:
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
flake-utils.lib.eachDefaultSystem
(system:
let
overlays = [
rust-overlay.overlays.default
@ -25,83 +17,27 @@
allowUnfree = true;
};
};
# Import rust setup
rustSetup = import ./nix/rust-setup.nix { inherit pkgs crane; };
inherit (rustSetup) rustVersion rustPlatform craneLib;
src = craneLib.cleanCargoSource ./.;
commonArgs = {
inherit src;
# Add any build inputs needed
buildInputs = with pkgs; [
openssl
pkg-config
];
# Add any native build inputs
nativeBuildInputs = with pkgs; [
rustPlatform.bindgenHook
pkg-config
];
checkType = "debug";
in
with pkgs;
{
devShells.default = mkShell {
env = {
OPENSSL_NO_VENDOR = "1";
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
};
};
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
individualCrateArgs = commonArgs // {
inherit cargoArtifacts;
inherit (craneLib.crateNameFromCargoToml { inherit src; }) version;
# NB: we disable tests since we'll run them all via cargo-nextest
doCheck = false;
packages = [
pkg-config
vault
(rust-bin.stable.latest.default.override {
extensions = [ "rust-src" ];
})
rustc
cargo
rustfmt
clippy
];
};
# Import vault-hier package
vault-hier = import ./nix/packages/vault-hier.nix {
inherit
pkgs
craneLib
individualCrateArgs
;
};
# Import devshell
devshell_with_src = import ./nix/devshell.nix {
inherit pkgs vault-hier rustVersion;
};
in
{
# Import checks
checks = import ./nix/checks {
inherit
craneLib
src
commonArgs
cargoArtifacts
vault-hier
pkgs
;
};
# Add packages output
packages = {
inherit vault-hier;
default = vault-hier;
};
devShells = {
inherit devshell_with_src;
default = devshell_with_src;
};
# Add formatter for `nix fmt`
formatter = pkgs.nixfmt-rfc-style;
}
);
}

View file

@ -1,13 +0,0 @@
{
craneLib,
commonArgs,
cargoArtifacts,
}:
craneLib.cargoClippy (
commonArgs
// {
inherit cargoArtifacts;
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
}
)

View file

@ -1,30 +0,0 @@
{
craneLib,
src,
commonArgs,
cargoArtifacts,
vault-hier,
pkgs,
}:
{
inherit vault-hier;
my-workspace-clippy = import ./clippy.nix {
inherit craneLib commonArgs cargoArtifacts;
};
my-workspace-doc = import ./doc.nix {
inherit craneLib commonArgs cargoArtifacts;
};
# Check formatting
my-workspace-fmt = import ./fmt.nix {
inherit craneLib src;
};
my-workspace-toml-fmt = import ./toml-fmt.nix {
inherit craneLib src;
lib = pkgs.lib;
};
}

View file

@ -1,12 +0,0 @@
{
craneLib,
commonArgs,
cargoArtifacts,
}:
craneLib.cargoDoc (
commonArgs
// {
inherit cargoArtifacts;
}
)

View file

@ -1,5 +0,0 @@
{ craneLib, src }:
craneLib.cargoFmt {
inherit src;
}

View file

@ -1,11 +0,0 @@
{
craneLib,
src,
lib,
}:
craneLib.taploFmt {
src = lib.sources.sourceFilesBySuffices src [ ".toml" ];
# taplo arguments can be further customized below as needed
# taploExtraArgs = "--config ./taplo.toml";
}

View file

@ -1,26 +0,0 @@
{
pkgs,
vault-hier,
rustVersion,
}:
let
toolchain_with_src = (
rustVersion.override {
extensions = [
"rustfmt"
"clippy"
"rust-src"
];
}
);
in
pkgs.mkShell {
packages = with pkgs; [
vault-hier # Add the vault-hier package to the dev shell
toolchain_with_src # Add the custom Rust toolchain with source code to the dev shell
];
nativeBuildInputs = [
vault-hier
];
}

View file

@ -1,12 +0,0 @@
{
pkgs,
craneLib,
individualCrateArgs,
}:
craneLib.buildPackage (
individualCrateArgs
// {
pname = "vault-hier";
cargoExtraArgs = "-p vault-hier";
}
)

View file

@ -1,13 +0,0 @@
{ pkgs, crane }:
let
rustVersion = pkgs.rust-bin.fromRustupToolchainFile (../rust-toolchain.toml);
rustPlatform = pkgs.makeRustPlatform {
cargo = rustVersion;
rustc = rustVersion;
};
craneLib = (crane.mkLib pkgs).overrideToolchain rustVersion;
in
{
inherit rustVersion rustPlatform craneLib;
}

View file

@ -1,3 +0,0 @@
[toolchain]
channel = "1.85.1"
components = ["rustfmt", "clippy", "rust-src"]

View file

@ -1,14 +1,15 @@
use anyhow::Result;
use axum::{
Json, Router, Server,
extract::{Multipart, Path, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
Server,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::{debug, error, info, instrument};
use tracing::{info, error, debug, instrument};
use crate::document_service::{Document, DocumentService, SignatureVerification};
use crate::vault_setup::VaultClient;
@ -67,7 +68,11 @@ 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<()> {
pub async fn start_api(
vault_addr: &str,
root_token: &str,
api_port: u16,
) -> Result<()> {
info!("Starting API server on port {}...", api_port);
// Initialize Vault client
@ -105,7 +110,9 @@ pub async fn start_api(vault_addr: &str, root_token: &str, api_port: u16) -> Res
// Bind and serve
info!("Serving API at {}", addr);
Server::bind(&addr).serve(app.into_make_service()).await?;
Server::bind(&addr)
.serve(app.into_make_service())
.await?;
Ok(())
}
@ -125,8 +132,7 @@ async fn login(
) -> Result<Json<LoginResponse>, ApiError> {
info!("Login attempt for user: {}", request.username);
let token = state
.vault_client
let token = state.vault_client
.login_user(&request.username, &request.password)
.await?;
@ -154,10 +160,7 @@ async fn upload_document(
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()
);
debug!("Received document content: {} bytes", document_content.len());
}
}
@ -167,13 +170,14 @@ async fn upload_document(
}
// Upload document
let document_id = state
.document_service
let document_id = state.document_service
.upload_document(&document_name, &document_content)
.await?;
// Return document metadata
let document = state.document_service.get_document(&document_id).await?;
let document = state.document_service
.get_document(&document_id)
.await?;
info!("Document uploaded successfully with ID: {}", document_id);
Ok(Json(document))
@ -187,13 +191,12 @@ async fn get_document(
) -> Result<Json<Document>, ApiError> {
info!("Fetching document: {}", document_id);
let document = state.document_service.get_document(&document_id).await?;
let document = state.document_service
.get_document(&document_id)
.await?;
debug!(
"Retrieved document {} with {} signatures",
document.id,
document.signatures.len()
);
debug!("Retrieved document {} with {} signatures",
document.id, document.signatures.len());
Ok(Json(document))
}
@ -205,22 +208,17 @@ async fn sign_document(
Path(document_id): Path<String>,
Json(request): Json<SignDocumentRequest>,
) -> Result<Json<Document>, ApiError> {
info!(
"Signing request for document {} by user {}",
document_id, request.username
);
info!("Signing request for document {} by user {}", document_id, request.username);
state
.document_service
state.document_service
.sign_document(&document_id, &request.username, &request.token)
.await?;
let document = state.document_service.get_document(&document_id).await?;
let document = state.document_service
.get_document(&document_id)
.await?;
info!(
"Document {} successfully signed by {}",
document_id, request.username
);
info!("Document {} successfully signed by {}", document_id, request.username);
Ok(Json(document))
}
@ -232,20 +230,12 @@ async fn verify_document(
) -> Result<Json<SignatureVerification>, ApiError> {
info!("Verifying document signatures: {}", document_id);
let verification = state
.document_service
let verification = state.document_service
.verify_document_signatures(&document_id)
.await?;
info!(
"Document {} verification result: {}",
document_id,
if verification.is_verified {
"VERIFIED"
} else {
"PENDING"
}
);
info!("Document {} verification result: {}",
document_id, if verification.is_verified { "VERIFIED" } else { "PENDING" });
Ok(Json(verification))
}

View file

@ -1,12 +1,12 @@
use anyhow::{Context, Result};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::{Digest, Sha256};
use sha2::{Sha256, Digest};
use std::collections::HashMap;
use tracing::{debug, error, info, instrument};
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};
@ -88,10 +88,8 @@ impl DocumentService {
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
);
let url = format!("{}/v1/documents/data/docs/{}",
self.vault_client.addr, document.id);
let payload = json!({
"data": {
@ -106,9 +104,7 @@ impl DocumentService {
}
});
let response = self
.vault_client
.client
let response = self.vault_client.client
.post(&url)
.header("X-Vault-Token", &self.vault_client.token)
.json(&payload)
@ -122,15 +118,8 @@ impl DocumentService {
}
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
))
error!("Failed to store document metadata: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to store document metadata: {} - {}", status, error_text))
}
}
}
@ -140,14 +129,10 @@ impl DocumentService {
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
);
let url = format!("{}/v1/documents/data/docs/{}",
self.vault_client.addr, document_id);
let response = self
.vault_client
.client
let response = self.vault_client.client
.get(&url)
.header("X-Vault-Token", &self.vault_client.token)
.send()
@ -184,49 +169,27 @@ impl DocumentService {
}
let document = Document {
id: json["data"]["data"]["id"]
.as_str()
.context("Missing id")?
.to_string(),
name: json["data"]["data"]["name"]
.as_str()
.context("Missing name")?
.to_string(),
hash: json["data"]["data"]["hash"]
.as_str()
.context("Missing hash")?
.to_string(),
id: json["data"]["data"]["id"].as_str().context("Missing id")?.to_string(),
name: json["data"]["data"]["name"].as_str().context("Missing name")?.to_string(),
hash: json["data"]["data"]["hash"].as_str().context("Missing hash")?.to_string(),
status,
signatures,
};
debug!(
"Retrieved document: {} with {} signatures",
document.id,
document.signatures.len()
);
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
))
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<()> {
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
@ -236,15 +199,14 @@ impl DocumentService {
let user = self.vault_client.get_user_info(username).await?;
// Sign the document hash with user's key
let url = format!("{}/v1/transit/sign/{}", self.vault_client.addr, username);
let url = format!("{}/v1/transit/sign/{}",
self.vault_client.addr, username);
let payload = json!({
"input": BASE64.encode(document.hash.as_bytes()),
});
let response = self
.vault_client
.client
let response = self.vault_client.client
.post(&url)
.header("X-Vault-Token", user_token)
.json(&payload)
@ -262,8 +224,7 @@ impl DocumentService {
debug!("Generated signature for document {}", document_id);
// Update document with signature
self.add_signature(document_id, username, &signature)
.await?;
self.add_signature(document_id, username, &signature).await?;
// Update department signature record
self.record_department_signature(document_id, &user).await?;
@ -277,43 +238,26 @@ impl DocumentService {
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
))
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
);
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?;
// Add signature
document
.signatures
.insert(username.to_string(), signature.to_string());
document.signatures.insert(username.to_string(), signature.to_string());
// Store updated document
self.store_document_metadata(&document).await?;
debug!(
"Added signature from {} to document {}",
username, document_id
);
debug!("Added signature from {} to document {}", username, document_id);
Ok(())
}
@ -325,20 +269,13 @@ impl DocumentService {
Department::Finance => "finance",
};
debug!(
"Recording {} department signature for document {}",
dept_str, document_id
);
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
);
let url = format!("{}/v1/documents/data/dept/{}/signatures/{}",
self.vault_client.addr, dept_str, document_id);
// Check if department signatures already exist
let response = self
.vault_client
.client
let response = self.vault_client.client
.get(&url)
.header("X-Vault-Token", &self.vault_client.token)
.send()
@ -372,9 +309,7 @@ impl DocumentService {
}
});
let response = self
.vault_client
.client
let response = self.vault_client.client
.post(&url)
.header("X-Vault-Token", &self.vault_client.token)
.json(&payload)
@ -383,23 +318,13 @@ impl DocumentService {
match response.status() {
StatusCode::OK | StatusCode::NO_CONTENT => {
info!(
"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
))
error!("Failed to record department signature: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to record department signature: {} - {}", status, error_text))
}
}
}
@ -439,24 +364,17 @@ impl DocumentService {
// Verify document signatures
#[instrument(skip(self))]
pub async fn verify_document_signatures(
&self,
document_id: &str,
) -> Result<SignatureVerification> {
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?;
// Get signing requirements
let url = format!(
"{}/v1/documents/data/config/signing_requirements",
self.vault_client.addr
);
let url = format!("{}/v1/documents/data/config/signing_requirements",
self.vault_client.addr);
let response = self
.vault_client
.client
let response = self.vault_client.client
.get(&url)
.header("X-Vault-Token", &self.vault_client.token)
.send()
@ -466,15 +384,8 @@ 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
));
error!("Failed to get signing requirements: {} - {}", status, error_text);
return Err(anyhow::anyhow!("Failed to get signing requirements: {} - {}", status, error_text));
}
};
@ -502,15 +413,13 @@ impl DocumentService {
// Get department signatures
let legal_signatures = self.get_department_signatures(document_id, "legal").await?;
let finance_signatures = self
.get_department_signatures(document_id, "finance")
.await?;
let finance_signatures = self.get_department_signatures(document_id, "finance").await?;
// Check if requirements are met
let total_signatures = document.signatures.len();
let is_verified = total_signatures >= required_signatures
&& legal_signatures.len() >= required_legal
&& finance_signatures.len() >= required_finance;
let is_verified = total_signatures >= required_signatures &&
legal_signatures.len() >= required_legal &&
finance_signatures.len() >= required_finance;
let verification = SignatureVerification {
document_id: document_id.to_string(),
@ -540,24 +449,13 @@ impl DocumentService {
// 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
);
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
);
let url = format!("{}/v1/documents/data/dept/{}/signatures/{}",
self.vault_client.addr, department, document_id);
let response = self
.vault_client
.client
let response = self.vault_client.client
.get(&url)
.header("X-Vault-Token", &self.vault_client.token)
.send()
@ -579,12 +477,8 @@ impl DocumentService {
}
}
debug!(
"Found {} signatures for {} department on document {}",
signatures.len(),
department,
document_id
);
debug!("Found {} signatures for {} department on document {}",
signatures.len(), department, document_id);
Ok(signatures)
}

View file

@ -1,11 +1,11 @@
// Modules that implement our hierarchical signing system
pub mod api;
pub mod document_service;
pub mod vault_init;
pub mod vault_setup;
pub mod vault_init;
pub mod document_service;
pub mod api;
// Re-export main components for easier access
pub use api::start_api;
pub use document_service::DocumentService;
pub use vault_init::initialize_vault;
pub use vault_setup::VaultClient;
pub use vault_init::initialize_vault;
pub use document_service::DocumentService;
pub use api::start_api;

View file

@ -1,18 +1,14 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use tracing::info;
use tracing_subscriber::{EnvFilter, fmt};
use tracing::{info};
use tracing_subscriber::{fmt, EnvFilter};
// Import our library
use vault_hier::{initialize_vault, start_api};
use vault_hier::{start_api, initialize_vault};
#[derive(Parser)]
#[command(
author,
version,
about = "Hierarchical Document Signing with HashiCorp Vault"
)]
#[command(author, version, about = "Hierarchical Document Signing with HashiCorp Vault")]
struct Cli {
#[command(subcommand)]
command: Commands,
@ -121,49 +117,27 @@ async fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Server {
vault_addr,
api_port,
} => {
Commands::Server { vault_addr, api_port } => {
run_server(&vault_addr, api_port).await?;
}
Commands::Login {
username,
password,
api_url,
} => {
},
Commands::Login { username, password, api_url } => {
login(&username, &password, &api_url).await?;
}
Commands::Upload {
name,
file,
api_url,
} => {
},
Commands::Upload { name, file, api_url } => {
upload_document(&name, file, &api_url).await?;
}
Commands::Sign {
document_id,
username,
token,
api_url,
} => {
},
Commands::Sign { document_id, username, token, api_url } => {
sign_document(&document_id, &username, &token, &api_url).await?;
}
Commands::Verify {
document_id,
api_url,
} => {
},
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,
} => {
},
Commands::Get { document_id, api_url } => {
get_document(&document_id, &api_url).await?;
}
},
}
Ok(())
@ -191,7 +165,7 @@ async fn login(username: &str, password: &str, api_url: &str) -> Result<()> {
let client = reqwest::Client::new();
let response = client
.post(format!("{}/api/login", api_url))
.post(&format!("{}/api/login", api_url))
.json(&serde_json::json!({
"username": username,
"password": password,
@ -220,14 +194,12 @@ async fn upload_document(name: &str, file_path: PathBuf, api_url: &str) -> Resul
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()),
);
.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))
.post(&format!("{}/api/documents", api_url))
.multipart(form)
.send()
.await?;
@ -244,17 +216,12 @@ async fn upload_document(name: &str, file_path: PathBuf, api_url: &str) -> Resul
Ok(())
}
async fn sign_document(
document_id: &str,
username: &str,
token: &str,
api_url: &str,
) -> Result<()> {
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))
.post(&format!("{}/api/documents/{}/sign", api_url, document_id))
.json(&serde_json::json!({
"username": username,
"token": token,
@ -279,7 +246,7 @@ async fn verify_document(document_id: &str, api_url: &str) -> Result<()> {
let client = reqwest::Client::new();
let response = client
.get(format!("{}/api/documents/{}/verify", api_url, document_id))
.get(&format!("{}/api/documents/{}/verify", api_url, document_id))
.send()
.await?;
@ -288,21 +255,16 @@ async fn verify_document(document_id: &str, api_url: &str) -> Result<()> {
println!("Verification result:");
println!(" Valid: {}", data["valid"]);
println!(" Total signatures: {}", data["signature_count"]);
println!(
" Departments represented: {}",
data["departments_represented"]
);
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,
println!(" {}. User: {}, Department: {}, Time: {}",
i+1,
sig["username"],
sig["department"],
sig["timestamp"]
);
sig["timestamp"]);
}
}
} else {
@ -318,7 +280,7 @@ async fn list_documents(api_url: &str) -> Result<()> {
let client = reqwest::Client::new();
let response = client
.get(format!("{}/api/documents", api_url))
.get(&format!("{}/api/documents", api_url))
.send()
.await?;
@ -327,7 +289,7 @@ async fn list_documents(api_url: &str) -> Result<()> {
println!("Documents:");
for (i, doc) in data.iter().enumerate() {
println!(" {}. ID: {}, Name: {}", i + 1, doc["id"], doc["name"]);
println!(" {}. ID: {}, Name: {}", i+1, doc["id"], doc["name"]);
}
if data.is_empty() {
@ -346,7 +308,7 @@ async fn get_document(document_id: &str, api_url: &str) -> Result<()> {
let client = reqwest::Client::new();
let response = client
.get(format!("{}/api/documents/{}", api_url, document_id))
.get(&format!("{}/api/documents/{}", api_url, document_id))
.send()
.await?;
@ -361,13 +323,11 @@ async fn get_document(document_id: &str, api_url: &str) -> Result<()> {
if let Some(signatures) = doc["signatures"].as_array() {
println!("\nSignatures:");
for (i, sig) in signatures.iter().enumerate() {
println!(
" {}. User: {}, Department: {}, Time: {}",
i + 1,
println!(" {}. User: {}, Department: {}, Time: {}",
i+1,
sig["username"],
sig["department"],
sig["timestamp"]
);
sig["timestamp"]);
}
if signatures.is_empty() {

View file

@ -1,7 +1,10 @@
use anyhow::{Context, Result};
use reqwest::Client;
use std::{env, fs};
use tracing::{debug, error, info, warn};
use std::{
env,
fs,
};
use tracing::{info, warn, error, debug};
use crate::vault_setup::VaultClient;
@ -13,17 +16,14 @@ pub async fn initialize_vault(vault_addr: &str) -> Result<String> {
VaultClient::wait_for_vault(vault_addr).await?;
// Display Vault health status
let health_url = format!(
"{}/v1/sys/health?standbyok=true&sealedok=true&uninitok=true",
vault_addr
);
let health_url = format!("{}/v1/sys/health?standbyok=true&sealedok=true&uninitok=true", vault_addr);
match client.get(&health_url).send().await {
Ok(response) => {
if response.status().is_success() {
let status_text = response.text().await?;
info!("Vault status: {}", status_text);
}
}
},
Err(e) => warn!("Error getting Vault status: {}", e),
}
@ -47,7 +47,7 @@ pub async fn initialize_vault(vault_addr: &str) -> Result<String> {
Ok(key) => {
info!("Found unseal key {} from environment", i);
unseal_keys.push(key);
}
},
Err(_) => {
debug!("Unseal key {} not found in environment", i);
}
@ -56,16 +56,11 @@ pub async fn initialize_vault(vault_addr: &str) -> Result<String> {
// If we have unseal keys, try to unseal
if !unseal_keys.is_empty() {
info!(
"Found {} unseal keys. Attempting to unseal...",
unseal_keys.len()
);
info!("Found {} unseal keys. Attempting to unseal...", unseal_keys.len());
VaultClient::unseal_vault(&client, vault_addr, &unseal_keys).await?;
} else {
warn!("No unseal keys found. Vault remains sealed.");
info!(
"To unseal, set VAULT_UNSEAL_KEY_1, VAULT_UNSEAL_KEY_2, etc. environment variables."
);
info!("To unseal, set VAULT_UNSEAL_KEY_1, VAULT_UNSEAL_KEY_2, etc. environment variables.");
}
} else {
info!("Vault is already unsealed.");
@ -76,7 +71,7 @@ pub async fn initialize_vault(vault_addr: &str) -> Result<String> {
Ok(token) => {
info!("Found root token from environment");
root_token = token;
}
},
Err(_) => {
// Try to load from credentials file
if let Ok(contents) = fs::read_to_string("vault-credentials.json") {
@ -91,12 +86,8 @@ pub async fn initialize_vault(vault_addr: &str) -> Result<String> {
}
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."
);
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
@ -121,17 +112,11 @@ pub async fn initialize_vault(vault_addr: &str) -> Result<String> {
if let Ok(()) = std::fs::create_dir_all("/app/data") {
let docker_json_path = "/app/data/vault-credentials.json";
VaultClient::save_credentials(&init_response, docker_json_path)?;
info!(
"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";
VaultClient::save_credentials(&init_response, docker_text_path)?;
info!(
"Backup text credentials saved to Docker volume at: {}",
docker_text_path
);
info!("Backup text credentials saved to Docker volume at: {}", docker_text_path);
}
info!("=========================================");
@ -145,9 +130,7 @@ pub async fn initialize_vault(vault_addr: &str) -> Result<String> {
info!("=========================================");
// Unseal Vault using the first three keys
let unseal_keys = init_response
.keys_base64
.iter()
let unseal_keys = init_response.keys_base64.iter()
.take(3) // We only need threshold number of keys (3)
.cloned()
.collect::<Vec<String>>();

View file

@ -2,9 +2,14 @@ use anyhow::{Context, Result};
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::{fs::File, io::Write, path::Path, time::Duration};
use std::{
fs::File,
io::Write,
path::Path,
time::Duration,
};
use tokio::time::sleep;
use tracing::{debug, error, info, instrument};
use tracing::{info, error, debug, instrument};
// Vault API response structures
#[derive(Debug, Deserialize)]
@ -72,17 +77,9 @@ impl VaultClient {
let client = Client::new();
for i in 1..=30 {
let health_url = format!(
"{}/v1/sys/health?standbyok=true&sealedok=true&uninitok=true",
addr
);
let health_url = format!("{}/v1/sys/health?standbyok=true&sealedok=true&uninitok=true", addr);
match client
.get(&health_url)
.timeout(Duration::from_secs(1))
.send()
.await
{
match client.get(&health_url).timeout(Duration::from_secs(1)).send().await {
Ok(response) => {
let status = response.status().as_u16();
// Accept any of these status codes as "available"
@ -92,7 +89,7 @@ impl VaultClient {
}
debug!("Vault returned unexpected status code: {}", status);
}
},
Err(e) => {
debug!("Error connecting to Vault: {}", e);
}
@ -100,9 +97,7 @@ impl VaultClient {
if i == 30 {
error!("Timed out waiting for Vault to become available");
return Err(anyhow::anyhow!(
"Timed out waiting for Vault to become available"
));
return Err(anyhow::anyhow!("Timed out waiting for Vault to become available"));
}
info!("Vault is unavailable - sleeping (attempt {}/30)", i);
@ -117,7 +112,10 @@ impl VaultClient {
pub async fn check_init_status(client: &Client, addr: &str) -> Result<bool> {
info!("Checking if Vault is already initialized...");
let response = client.get(format!("{}/v1/sys/init", addr)).send().await?;
let response = client
.get(format!("{}/v1/sys/init", addr))
.send()
.await?;
if response.status().is_success() {
let status = response.json::<serde_json::Value>().await?;
@ -142,10 +140,8 @@ impl VaultClient {
if response.status().is_success() {
let status = response.json::<SealStatusResponse>().await?;
info!(
"Seal status: sealed={}, threshold={}, shares={}, progress={}",
status.sealed, status.t, status.n, status.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?;
@ -228,7 +224,9 @@ impl VaultClient {
for (i, key) in unseal_keys.iter().take(required_keys).enumerate() {
info!("Applying unseal key {}/{}...", i + 1, required_keys);
let unseal_req = UnsealRequest { key: key.clone() };
let unseal_req = UnsealRequest {
key: key.clone(),
};
let response = client
.put(format!("{}/v1/sys/unseal", addr))
@ -321,8 +319,7 @@ impl VaultClient {
"type": engine_type,
});
let response = self
.client
let response = self.client
.post(&url)
.header("X-Vault-Token", &self.token)
.json(&payload)
@ -342,23 +339,13 @@ impl VaultClient {
Ok(())
} else {
error!("Failed to enable secrets engine: {}", error_text);
Err(anyhow::anyhow!(
"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
))
error!("Failed to enable secrets engine: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to enable secrets engine: {} - {}", status, error_text))
}
}
}
@ -373,8 +360,7 @@ impl VaultClient {
"type": method,
});
let response = self
.client
let response = self.client
.post(&url)
.header("X-Vault-Token", &self.token)
.json(&payload)
@ -394,38 +380,25 @@ impl VaultClient {
Ok(())
} else {
error!("Failed to enable auth method: {}", error_text);
Err(anyhow::anyhow!(
"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
))
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<()> {
pub async fn create_user(&self, username: &str, password: &str, department: Department) -> Result<()> {
info!("Creating user {} in department {:?}", username, department);
// Step 1: Create a policy for the user
let policy_name = format!("{}-policy", username);
self.create_signing_policy(&policy_name, department.clone())
.await?;
self.create_signing_policy(&policy_name, department.clone()).await?;
// Step 2: Create the user with userpass auth
let url = format!("{}/v1/auth/userpass/users/{}", self.addr, username);
@ -434,8 +407,7 @@ impl VaultClient {
"policies": [policy_name],
});
let response = self
.client
let response = self.client
.post(&url)
.header("X-Vault-Token", &self.token)
.json(&payload)
@ -457,11 +429,7 @@ 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
))
Err(anyhow::anyhow!("Failed to create user: {} - {}", status, error_text))
}
}
}
@ -478,8 +446,7 @@ impl VaultClient {
let username = policy_name.trim_end_matches("-policy");
// Policy content with specific paths for the department
let policy = format!(
r#"
let policy = format!(r#"
# Allow reading document metadata
path "documents/data/docs/*" {{
capabilities = ["read"]
@ -499,17 +466,14 @@ impl VaultClient {
path "documents/data/dept/{}/signatures/*" {{
capabilities = ["create", "read", "update"]
}}
"#,
username, dept_name
);
"#, username, dept_name);
let url = format!("{}/v1/sys/policies/acl/{}", self.addr, policy_name);
let payload = json!({
"policy": policy,
});
let response = self
.client
let response = self.client
.put(&url)
.header("X-Vault-Token", &self.token)
.json(&payload)
@ -524,11 +488,7 @@ impl VaultClient {
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
))
Err(anyhow::anyhow!("Failed to create policy: {} - {}", status, error_text))
}
}
}
@ -541,8 +501,7 @@ impl VaultClient {
"type": "ed25519",
});
let response = self
.client
let response = self.client
.post(&url)
.header("X-Vault-Token", &self.token)
.json(&payload)
@ -557,11 +516,7 @@ impl VaultClient {
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
))
Err(anyhow::anyhow!("Failed to create signing key: {} - {}", status, error_text))
}
}
}
@ -582,8 +537,7 @@ impl VaultClient {
}
});
let response = self
.client
let response = self.client
.post(&url)
.header("X-Vault-Token", &self.token)
.json(&payload)
@ -598,11 +552,7 @@ impl VaultClient {
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
))
Err(anyhow::anyhow!("Failed to store user metadata: {} - {}", status, error_text))
}
}
}
@ -617,8 +567,7 @@ impl VaultClient {
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?;
self.create_user(&username, &password, Department::Legal).await?;
}
// Create 5 users in Finance department
@ -626,8 +575,7 @@ impl VaultClient {
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?;
self.create_user(&username, &password, Department::Finance).await?;
}
// Setup document signing requirements
@ -642,10 +590,7 @@ impl VaultClient {
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 url = format!("{}/v1/documents/data/config/signing_requirements", self.addr);
let payload = json!({
"data": {
"total_required": 3,
@ -665,8 +610,7 @@ impl VaultClient {
}
});
let response = self
.client
let response = self.client
.post(&url)
.header("X-Vault-Token", &self.token)
.json(&payload)
@ -680,15 +624,8 @@ impl VaultClient {
}
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
))
error!("Failed to configure signing requirements: {} - {}", status, error_text);
Err(anyhow::anyhow!("Failed to configure signing requirements: {} - {}", status, error_text))
}
}
}
@ -703,7 +640,11 @@ impl VaultClient {
"password": password,
});
let response = self.client.post(&url).json(&payload).send().await?;
let response = self.client
.post(&url)
.json(&payload)
.send()
.await?;
match response.status() {
StatusCode::OK => {
@ -719,11 +660,7 @@ impl VaultClient {
status => {
let error_text = response.text().await?;
error!("Failed to login: {} - {}", status, error_text);
Err(anyhow::anyhow!(
"Failed to login: {} - {}",
status,
error_text
))
Err(anyhow::anyhow!("Failed to login: {} - {}", status, error_text))
}
}
}
@ -735,8 +672,7 @@ impl VaultClient {
let url = format!("{}/v1/documents/data/users/{}", self.addr, username);
let response = self
.client
let response = self.client
.get(&url)
.header("X-Vault-Token", &self.token)
.send()
@ -758,10 +694,7 @@ impl VaultClient {
}
};
debug!(
"Retrieved user info for {} in {:?} department",
username, department
);
debug!("Retrieved user info for {} in {:?} department", username, department);
Ok(User {
username: username.to_string(),
department,
@ -770,11 +703,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
))
Err(anyhow::anyhow!("Failed to get user info: {} - {}", status, error_text))
}
}
}