feat: add hierarchical document signing with Vault API
- Introduced a new hierarchical signing system using HashiCorp Vault. - Added Rust modules for user management, secrets setup, and document API. - Implemented API endpoints for login, document upload, signing, and verification. - Updated README with features, usage, and API examples.
This commit is contained in:
parent
0dc662865f
commit
f11b83ddf4
|
@ -9,4 +9,10 @@ tokio = { version = "1.28.0", features = ["full"] }
|
|||
serde = { version = "1.0.160", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
anyhow = "1.0.70"
|
||||
|
||||
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"
|
||||
|
|
137
README.md
137
README.md
|
@ -1,93 +1,90 @@
|
|||
# Vault Hierarchical Initializer
|
||||
# Hierarchical Document Signing with HashiCorp Vault
|
||||
|
||||
A Rust-based utility for initializing and unsealing HashiCorp Vault in non-dev (production) mode.
|
||||
This project implements a hierarchical document signing system using HashiCorp Vault. It allows for secure document signing with a requirement of a specific number of signatures from different departmental groups.
|
||||
|
||||
## Overview
|
||||
## Features
|
||||
|
||||
This project provides a Docker-based solution for:
|
||||
- **Hierarchical Signing**: Requires 3 of 5 signatures to validate a document, with at least 1 signature from each department
|
||||
- **Department Structure**: Two departments (Legal and Finance) with 5 users each
|
||||
- **Document API**: Upload, sign, and verify documents through a RESTful API
|
||||
- **Vault Integration**: Leverages HashiCorp Vault's Transit engine for cryptographic operations
|
||||
|
||||
1. Running a HashiCorp Vault server in non-dev (production) mode
|
||||
2. Automatically initializing the Vault instance
|
||||
3. Unsealing the Vault after initialization
|
||||
4. Storing unseal keys and root token securely
|
||||
## System Architecture
|
||||
|
||||
## Prerequisites
|
||||
The system consists of:
|
||||
|
||||
- Docker and Docker Compose installed on your system
|
||||
- Rust (if you want to build the project locally)
|
||||
1. **Vault Server**: Provides secure storage and cryptographic operations
|
||||
2. **Rust Application**: Initializes Vault and provides a REST API for document operations
|
||||
3. **User Hierarchy**: 10 users organized into 2 departments
|
||||
4. **Signature Requirements**: 3 of 5 signatures needed, with at least 1 from each department
|
||||
|
||||
## Configuration
|
||||
## API Endpoints
|
||||
|
||||
In production mode, Vault:
|
||||
- Starts sealed and requires a threshold of unseal keys to unseal
|
||||
- Stores data persistently in mounted volumes
|
||||
- Requires explicit initialization
|
||||
- Needs manual unsealing after restarts
|
||||
- **POST /api/login**: Authenticate with username/password and get a token
|
||||
- **POST /api/documents**: Upload a new document for signing
|
||||
- **GET /api/documents/:id**: Retrieve document metadata
|
||||
- **POST /api/documents/:id/sign**: Sign a document with your user credentials
|
||||
- **GET /api/documents/:id/verify**: Check if a document has sufficient signatures
|
||||
|
||||
The implementation uses:
|
||||
- 5 key shares with a threshold of 3 keys needed for unsealing
|
||||
- Persistent volume storage for Vault data
|
||||
## Getting Started
|
||||
|
||||
## Usage
|
||||
### Prerequisites
|
||||
|
||||
### Starting Vault with Docker Compose
|
||||
- Docker and Docker Compose
|
||||
- Rust development environment (if building from source)
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
### Running with Docker
|
||||
|
||||
This will:
|
||||
1. Start a Vault server in production mode
|
||||
2. Run the vault-hier utility to initialize Vault if needed
|
||||
3. Automatically unseal Vault using the threshold number of keys
|
||||
4. Save the unseal keys and root token to `vault-credentials.txt` in the mounted volume
|
||||
1. Start the Vault server and initialization program:
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Getting Vault Credentials
|
||||
2. The service will automatically:
|
||||
- Initialize Vault (if needed)
|
||||
- Unseal Vault
|
||||
- Create 10 users in a hierarchical structure
|
||||
- Start the API server on port 3000
|
||||
|
||||
After initialization, you can find the unseal keys and root token in:
|
||||
3. User credentials:
|
||||
- Legal department: legal1/legal1pass through legal5/legal5pass
|
||||
- Finance department: finance1/finance1pass through finance5/finance5pass
|
||||
|
||||
```
|
||||
./vault-credentials.txt
|
||||
```
|
||||
### API Usage Examples
|
||||
|
||||
Keep these credentials safe! They provide full access to your Vault instance.
|
||||
1. **Login**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"legal1","password":"legal1pass"}'
|
||||
```
|
||||
|
||||
### Restarting a Sealed Vault
|
||||
2. **Upload Document**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/documents \
|
||||
-F "name=Contract" \
|
||||
-F "file=@/path/to/document.pdf"
|
||||
```
|
||||
|
||||
If your Vault instance restarts, it will start in a sealed state. To unseal it automatically:
|
||||
3. **Sign Document**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/documents/DOCUMENT_ID/sign \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"legal1","token":"USER_TOKEN"}'
|
||||
```
|
||||
|
||||
```bash
|
||||
# Set the unseal keys as environment variables
|
||||
export VAULT_UNSEAL_KEY_1="your-first-key"
|
||||
export VAULT_UNSEAL_KEY_2="your-second-key"
|
||||
export VAULT_UNSEAL_KEY_3="your-third-key"
|
||||
|
||||
# Restart the vault-init container to trigger unsealing
|
||||
docker-compose restart vault-init
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Building the Project Locally
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
### Custom Configuration
|
||||
|
||||
To modify the key sharing threshold:
|
||||
1. Edit the `init_req` struct in `src/main.rs`
|
||||
2. Rebuild the Docker image
|
||||
4. **Verify Document**:
|
||||
```bash
|
||||
curl -X GET http://localhost:3000/api/documents/DOCUMENT_ID/verify
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- In a production environment, never store unseal keys on the same machine as Vault
|
||||
- Consider using a key management solution like Shamir's Secret Sharing
|
||||
- Rotate root tokens regularly and use appropriate authentication methods
|
||||
- All cryptographic operations are performed by Vault's Transit engine
|
||||
- Each user has their own signing key
|
||||
- Root token should be secured in production environments
|
||||
- Consider adding TLS for production deployments
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
211
src/api.rs
Normal file
211
src/api.rs
Normal file
|
@ -0,0 +1,211 @@
|
|||
use anyhow::Result;
|
||||
use axum::{
|
||||
extract::{Multipart, Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
Server,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use crate::document_service::{Document, DocumentService, SignatureVerification};
|
||||
use crate::vault_setup::VaultClient;
|
||||
|
||||
// API state containing services
|
||||
#[derive(Clone)]
|
||||
pub struct ApiState {
|
||||
document_service: Arc<DocumentService>,
|
||||
vault_client: Arc<VaultClient>,
|
||||
}
|
||||
|
||||
// API error
|
||||
#[derive(Debug)]
|
||||
pub struct ApiError(anyhow::Error);
|
||||
|
||||
// Login request
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct LoginRequest {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
// Login response
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct LoginResponse {
|
||||
token: String,
|
||||
}
|
||||
|
||||
// Sign document request
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SignDocumentRequest {
|
||||
username: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
// API response implementations
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Error: {}", self.0),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> From<E> for ApiError
|
||||
where
|
||||
E: Into<anyhow::Error>,
|
||||
{
|
||||
fn from(err: E) -> Self {
|
||||
Self(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
// Start the API server
|
||||
pub async fn start_api(
|
||||
vault_addr: &str,
|
||||
root_token: &str,
|
||||
api_port: u16,
|
||||
) -> Result<()> {
|
||||
println!("Starting API server on port {}...", api_port);
|
||||
|
||||
// Initialize Vault client
|
||||
let vault_client = VaultClient::new(vault_addr, root_token);
|
||||
|
||||
// Setup required secrets engines and auth methods
|
||||
vault_client.setup_secrets_engines().await?;
|
||||
|
||||
// Setup 10 users in hierarchical structure
|
||||
vault_client.setup_hierarchical_users().await?;
|
||||
|
||||
// Initialize document service
|
||||
let document_service = DocumentService::new(vault_client.clone());
|
||||
|
||||
// Create API state
|
||||
let state = ApiState {
|
||||
document_service: Arc::new(document_service),
|
||||
vault_client: Arc::new(vault_client),
|
||||
};
|
||||
|
||||
// Setup router
|
||||
let app = Router::new()
|
||||
.route("/health", get(health_check))
|
||||
.route("/api/login", post(login))
|
||||
.route("/api/documents", post(upload_document))
|
||||
.route("/api/documents/:id", get(get_document))
|
||||
.route("/api/documents/:id/sign", post(sign_document))
|
||||
.route("/api/documents/:id/verify", get(verify_document))
|
||||
.with_state(state);
|
||||
|
||||
// Start server
|
||||
let listener = TcpListener::bind(format!("0.0.0.0:{}", api_port)).await?;
|
||||
println!("API server started on port {}", api_port);
|
||||
|
||||
// Get the socket address
|
||||
let addr = listener.local_addr()?;
|
||||
|
||||
// Bind and serve
|
||||
Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
async fn health_check() -> &'static str {
|
||||
"OK"
|
||||
}
|
||||
|
||||
// Login endpoint
|
||||
async fn login(
|
||||
State(state): State<ApiState>,
|
||||
Json(request): Json<LoginRequest>,
|
||||
) -> Result<Json<LoginResponse>, ApiError> {
|
||||
let token = state.vault_client
|
||||
.login_user(&request.username, &request.password)
|
||||
.await?;
|
||||
|
||||
Ok(Json(LoginResponse { token }))
|
||||
}
|
||||
|
||||
// Upload document endpoint
|
||||
async fn upload_document(
|
||||
State(state): State<ApiState>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<Document>, ApiError> {
|
||||
let mut document_name = String::new();
|
||||
let mut document_content = Vec::new();
|
||||
|
||||
// Process multipart form
|
||||
while let Some(field) = multipart.next_field().await? {
|
||||
let name = field.name().unwrap_or("").to_string();
|
||||
|
||||
if name == "name" {
|
||||
document_name = field.text().await?;
|
||||
} else if name == "file" {
|
||||
document_content = field.bytes().await?.to_vec();
|
||||
}
|
||||
}
|
||||
|
||||
if document_name.is_empty() || document_content.is_empty() {
|
||||
return Err(anyhow::anyhow!("Missing document name or content").into());
|
||||
}
|
||||
|
||||
// Upload document
|
||||
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?;
|
||||
|
||||
Ok(Json(document))
|
||||
}
|
||||
|
||||
// Get document endpoint
|
||||
async fn get_document(
|
||||
State(state): State<ApiState>,
|
||||
Path(document_id): Path<String>,
|
||||
) -> Result<Json<Document>, ApiError> {
|
||||
let document = state.document_service
|
||||
.get_document(&document_id)
|
||||
.await?;
|
||||
|
||||
Ok(Json(document))
|
||||
}
|
||||
|
||||
// Sign document endpoint
|
||||
async fn sign_document(
|
||||
State(state): State<ApiState>,
|
||||
Path(document_id): Path<String>,
|
||||
Json(request): Json<SignDocumentRequest>,
|
||||
) -> Result<Json<Document>, ApiError> {
|
||||
state.document_service
|
||||
.sign_document(&document_id, &request.username, &request.token)
|
||||
.await?;
|
||||
|
||||
let document = state.document_service
|
||||
.get_document(&document_id)
|
||||
.await?;
|
||||
|
||||
Ok(Json(document))
|
||||
}
|
||||
|
||||
// Verify document endpoint
|
||||
async fn verify_document(
|
||||
State(state): State<ApiState>,
|
||||
Path(document_id): Path<String>,
|
||||
) -> Result<Json<SignatureVerification>, ApiError> {
|
||||
let verification = state.document_service
|
||||
.verify_document_signatures(&document_id)
|
||||
.await?;
|
||||
|
||||
Ok(Json(verification))
|
||||
}
|
419
src/document_service.rs
Normal file
419
src/document_service.rs
Normal file
|
@ -0,0 +1,419 @@
|
|||
use anyhow::{Context, Result};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sha2::{Sha256, Digest};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
|
||||
use crate::vault_setup::{Department, User, VaultClient};
|
||||
|
||||
// Document status
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum DocumentStatus {
|
||||
#[serde(rename = "pending")]
|
||||
Pending,
|
||||
#[serde(rename = "verified")]
|
||||
Verified,
|
||||
}
|
||||
|
||||
// Document metadata
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Document {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub hash: String,
|
||||
pub status: DocumentStatus,
|
||||
pub signatures: HashMap<String, String>, // username -> signature
|
||||
}
|
||||
|
||||
// Signature verification response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SignatureVerification {
|
||||
pub document_id: String,
|
||||
pub is_verified: bool,
|
||||
pub signatures_count: usize,
|
||||
pub legal_signatures: usize,
|
||||
pub finance_signatures: usize,
|
||||
pub required_signatures: usize,
|
||||
pub required_legal: usize,
|
||||
pub required_finance: usize,
|
||||
}
|
||||
|
||||
// Document service to handle document operations
|
||||
pub struct DocumentService {
|
||||
vault_client: VaultClient,
|
||||
}
|
||||
|
||||
impl DocumentService {
|
||||
pub fn new(vault_client: VaultClient) -> Self {
|
||||
DocumentService { vault_client }
|
||||
}
|
||||
|
||||
// Upload a new document and store its metadata
|
||||
pub async fn upload_document(&self, name: &str, content: &[u8]) -> Result<String> {
|
||||
// Generate a unique ID
|
||||
let id = Uuid::new_v4().to_string();
|
||||
|
||||
// Calculate document hash
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(content);
|
||||
let hash = format!("{:x}", hasher.finalize());
|
||||
|
||||
// Create document metadata
|
||||
let document = Document {
|
||||
id: id.clone(),
|
||||
name: name.to_string(),
|
||||
hash,
|
||||
status: DocumentStatus::Pending,
|
||||
signatures: HashMap::new(),
|
||||
};
|
||||
|
||||
// Store document metadata in Vault
|
||||
self.store_document_metadata(&document).await?;
|
||||
|
||||
println!("Document uploaded with ID: {}", id);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
// Store document metadata in Vault
|
||||
async fn store_document_metadata(&self, document: &Document) -> Result<()> {
|
||||
let url = format!("{}/v1/documents/data/docs/{}",
|
||||
self.vault_client.addr, document.id);
|
||||
|
||||
let payload = json!({
|
||||
"data": {
|
||||
"id": document.id,
|
||||
"name": document.name,
|
||||
"hash": document.hash,
|
||||
"status": match document.status {
|
||||
DocumentStatus::Pending => "pending",
|
||||
DocumentStatus::Verified => "verified",
|
||||
},
|
||||
"signatures": document.signatures,
|
||||
}
|
||||
});
|
||||
|
||||
let response = self.vault_client.client
|
||||
.post(&url)
|
||||
.header("X-Vault-Token", &self.vault_client.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK | StatusCode::NO_CONTENT => {
|
||||
println!("Successfully stored document metadata for {}", document.id);
|
||||
Ok(())
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to store document metadata: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get document metadata
|
||||
pub async fn get_document(&self, document_id: &str) -> Result<Document> {
|
||||
let url = format!("{}/v1/documents/data/docs/{}",
|
||||
self.vault_client.addr, document_id);
|
||||
|
||||
let response = self.vault_client.client
|
||||
.get(&url)
|
||||
.header("X-Vault-Token", &self.vault_client.token)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => {
|
||||
let json: serde_json::Value = response.json().await?;
|
||||
|
||||
// Extract status
|
||||
let status_str = json["data"]["data"]["status"]
|
||||
.as_str()
|
||||
.context("Failed to extract status")?;
|
||||
|
||||
let status = match status_str {
|
||||
"pending" => DocumentStatus::Pending,
|
||||
"verified" => DocumentStatus::Verified,
|
||||
_ => return Err(anyhow::anyhow!("Unknown status: {}", status_str)),
|
||||
};
|
||||
|
||||
// Extract signatures
|
||||
let signatures_value = &json["data"]["data"]["signatures"];
|
||||
let mut signatures = HashMap::new();
|
||||
|
||||
if let Some(obj) = signatures_value.as_object() {
|
||||
for (username, sig) in obj {
|
||||
if let Some(sig_str) = sig.as_str() {
|
||||
signatures.insert(username.clone(), sig_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
status,
|
||||
signatures,
|
||||
};
|
||||
|
||||
Ok(document)
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to get document: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sign a document with the user's key
|
||||
pub async fn sign_document(&self, document_id: &str, username: &str, user_token: &str) -> Result<()> {
|
||||
// Get document metadata
|
||||
let document = self.get_document(document_id).await?;
|
||||
|
||||
// Get user info to verify department
|
||||
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 payload = json!({
|
||||
"input": BASE64.encode(document.hash.as_bytes()),
|
||||
});
|
||||
|
||||
let response = self.vault_client.client
|
||||
.post(&url)
|
||||
.header("X-Vault-Token", user_token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => {
|
||||
let json: serde_json::Value = response.json().await?;
|
||||
let signature = json["data"]["signature"]
|
||||
.as_str()
|
||||
.context("Failed to extract signature")?
|
||||
.to_string();
|
||||
|
||||
// Update document with signature
|
||||
self.add_signature(document_id, username, &signature).await?;
|
||||
|
||||
// Update department signature record
|
||||
self.record_department_signature(document_id, &user).await?;
|
||||
|
||||
// Check if document now has enough signatures
|
||||
self.update_document_status(document_id).await?;
|
||||
|
||||
println!("Document {} signed by {}", document_id, username);
|
||||
Ok(())
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to sign document: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a signature to a document
|
||||
async fn add_signature(&self, document_id: &str, username: &str, signature: &str) -> Result<()> {
|
||||
// Get current document
|
||||
let mut document = self.get_document(document_id).await?;
|
||||
|
||||
// Add signature
|
||||
document.signatures.insert(username.to_string(), signature.to_string());
|
||||
|
||||
// Store updated document
|
||||
self.store_document_metadata(&document).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Record department signature
|
||||
async fn record_department_signature(&self, document_id: &str, user: &User) -> Result<()> {
|
||||
let dept_str = match user.department {
|
||||
Department::Legal => "legal",
|
||||
Department::Finance => "finance",
|
||||
};
|
||||
|
||||
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
|
||||
.get(&url)
|
||||
.header("X-Vault-Token", &self.vault_client.token)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let mut signatures = Vec::new();
|
||||
|
||||
// If record exists, get current signatures
|
||||
if let Ok(resp) = response {
|
||||
if resp.status() == StatusCode::OK {
|
||||
let json: serde_json::Value = resp.json().await?;
|
||||
if let Some(array) = json["data"]["data"]["signatures"].as_array() {
|
||||
for sig in array {
|
||||
if let Some(sig_str) = sig.as_str() {
|
||||
signatures.push(sig_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add user to signatures if not already present
|
||||
if !signatures.contains(&user.username) {
|
||||
signatures.push(user.username.clone());
|
||||
}
|
||||
|
||||
// Store updated signatures
|
||||
let payload = json!({
|
||||
"data": {
|
||||
"signatures": signatures,
|
||||
}
|
||||
});
|
||||
|
||||
let response = self.vault_client.client
|
||||
.post(&url)
|
||||
.header("X-Vault-Token", &self.vault_client.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK | StatusCode::NO_CONTENT => {
|
||||
println!("Recorded signature for {} in {} department", user.username, dept_str);
|
||||
Ok(())
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to record department signature: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update document status if it has enough signatures
|
||||
async fn update_document_status(&self, document_id: &str) -> Result<()> {
|
||||
// Verify signatures
|
||||
let verification = self.verify_document_signatures(document_id).await?;
|
||||
|
||||
if verification.is_verified {
|
||||
// Get current document
|
||||
let mut document = self.get_document(document_id).await?;
|
||||
|
||||
// Update status
|
||||
document.status = DocumentStatus::Verified;
|
||||
|
||||
// Store updated document
|
||||
self.store_document_metadata(&document).await?;
|
||||
|
||||
println!("Document {} marked as verified", document_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Verify document signatures
|
||||
pub async fn verify_document_signatures(&self, document_id: &str) -> Result<SignatureVerification> {
|
||||
// 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 response = self.vault_client.client
|
||||
.get(&url)
|
||||
.header("X-Vault-Token", &self.vault_client.token)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let config = match response.status() {
|
||||
StatusCode::OK => response.json::<serde_json::Value>().await?,
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
return Err(anyhow::anyhow!("Failed to get signing requirements: {} - {}", status, error_text));
|
||||
}
|
||||
};
|
||||
|
||||
// Get required signatures
|
||||
let required_signatures = config["data"]["data"]["total_required"]
|
||||
.as_u64()
|
||||
.context("Missing total_required")? as usize;
|
||||
|
||||
// Get department requirements
|
||||
let mut required_legal = 0;
|
||||
let mut required_finance = 0;
|
||||
|
||||
if let Some(departments) = config["data"]["data"]["departments"].as_array() {
|
||||
for dept in departments {
|
||||
let name = dept["name"].as_str().context("Missing department name")?;
|
||||
let required = dept["required"].as_u64().context("Missing required")? as usize;
|
||||
|
||||
match name {
|
||||
"legal" => required_legal = required,
|
||||
"finance" => required_finance = required,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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?;
|
||||
|
||||
// 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 verification = SignatureVerification {
|
||||
document_id: document_id.to_string(),
|
||||
is_verified,
|
||||
signatures_count: total_signatures,
|
||||
legal_signatures: legal_signatures.len(),
|
||||
finance_signatures: finance_signatures.len(),
|
||||
required_signatures,
|
||||
required_legal,
|
||||
required_finance,
|
||||
};
|
||||
|
||||
Ok(verification)
|
||||
}
|
||||
|
||||
// Get department signatures for a document
|
||||
async fn get_department_signatures(&self, document_id: &str, department: &str) -> Result<Vec<String>> {
|
||||
let url = format!("{}/v1/documents/data/dept/{}/signatures/{}",
|
||||
self.vault_client.addr, department, document_id);
|
||||
|
||||
let response = self.vault_client.client
|
||||
.get(&url)
|
||||
.header("X-Vault-Token", &self.vault_client.token)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let mut signatures = Vec::new();
|
||||
|
||||
// If record exists, get signatures
|
||||
if let Ok(resp) = response {
|
||||
if resp.status() == StatusCode::OK {
|
||||
let json: serde_json::Value = resp.json().await?;
|
||||
if let Some(array) = json["data"]["data"]["signatures"].as_array() {
|
||||
for sig in array {
|
||||
if let Some(sig_str) = sig.as_str() {
|
||||
signatures.push(sig_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(signatures)
|
||||
}
|
||||
}
|
9
src/lib.rs
Normal file
9
src/lib.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Modules that implement our hierarchical signing system
|
||||
pub mod vault_setup;
|
||||
pub mod document_service;
|
||||
pub mod api;
|
||||
|
||||
// Re-export main components for easier access
|
||||
pub use vault_setup::VaultClient;
|
||||
pub use document_service::DocumentService;
|
||||
pub use api::start_api;
|
58
src/main.rs
58
src/main.rs
|
@ -10,6 +10,9 @@ use std::{
|
|||
};
|
||||
use tokio::time::sleep;
|
||||
|
||||
// Import our library
|
||||
use vault_hier::start_api;
|
||||
|
||||
// Vault API response structures
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct InitResponse {
|
||||
|
@ -46,13 +49,13 @@ fn save_credentials(response: &InitResponse, file_path: &str) -> Result<()> {
|
|||
"keys_base64": response.keys_base64,
|
||||
"root_token": response.root_token
|
||||
});
|
||||
|
||||
|
||||
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);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
// For plaintext output (legacy format)
|
||||
let mut file = File::create(Path::new(file_path))?;
|
||||
writeln!(file, "Unseal Keys:")?;
|
||||
|
@ -245,6 +248,12 @@ async fn main() -> Result<()> {
|
|||
let vault_addr = env::var("VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string());
|
||||
let client = Client::new();
|
||||
|
||||
// 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);
|
||||
|
||||
println!("Vault address: {}", vault_addr);
|
||||
println!("Connecting to Vault at: {}", vault_addr);
|
||||
|
||||
|
@ -265,6 +274,7 @@ async fn main() -> Result<()> {
|
|||
|
||||
// First check if Vault is already initialized
|
||||
let initialized = check_init_status(&client, &vault_addr).await?;
|
||||
let mut root_token = String::new();
|
||||
|
||||
if initialized {
|
||||
println!("Vault is already initialized.");
|
||||
|
@ -300,6 +310,29 @@ async fn main() -> Result<()> {
|
|||
} else {
|
||||
println!("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");
|
||||
root_token = token;
|
||||
},
|
||||
Err(_) => {
|
||||
// Try to load from credentials file
|
||||
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");
|
||||
root_token = token.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if root_token.is_empty() {
|
||||
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...");
|
||||
|
@ -308,12 +341,12 @@ async fn main() -> Result<()> {
|
|||
// Save credentials to files
|
||||
println!("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());
|
||||
|
||||
|
||||
// Save as text (for backward compatibility)
|
||||
let text_path = current_dir.join("vault-credentials.txt");
|
||||
save_credentials(&init_response, text_path.to_str().unwrap())?;
|
||||
|
@ -324,7 +357,7 @@ async fn main() -> Result<()> {
|
|||
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);
|
||||
|
||||
|
||||
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);
|
||||
|
@ -349,8 +382,11 @@ async fn main() -> Result<()> {
|
|||
unseal_vault(&client, &vault_addr, &unseal_keys).await?;
|
||||
|
||||
println!("Vault initialization and unseal complete!");
|
||||
|
||||
// Set root token
|
||||
root_token = init_response.root_token;
|
||||
}
|
||||
|
||||
|
||||
// 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() {
|
||||
|
@ -363,7 +399,7 @@ 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...");
|
||||
|
@ -376,7 +412,13 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
println!("Operation complete!");
|
||||
println!("Vault setup complete!");
|
||||
println!("Starting hierarchical document signing API...");
|
||||
|
||||
// Start the hierarchical signing API
|
||||
start_api(&vault_addr, &root_token, api_port).await?;
|
||||
|
||||
println!("API server shutdown. Exiting.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
414
src/vault_setup.rs
Normal file
414
src/vault_setup.rs
Normal file
|
@ -0,0 +1,414 @@
|
|||
use anyhow::{Context, Result};
|
||||
use reqwest::{Client, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
// Department types for organizational structure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Department {
|
||||
Legal,
|
||||
Finance,
|
||||
}
|
||||
|
||||
// User representation with department info
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub username: String,
|
||||
pub department: Department,
|
||||
}
|
||||
|
||||
// VaultClient to interact with Vault API
|
||||
#[derive(Clone)]
|
||||
pub struct VaultClient {
|
||||
pub client: Client,
|
||||
pub addr: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl VaultClient {
|
||||
pub fn new(addr: &str, token: &str) -> Self {
|
||||
VaultClient {
|
||||
client: Client::new(),
|
||||
addr: addr.to_string(),
|
||||
token: token.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
// Enable required secrets engines
|
||||
pub async fn setup_secrets_engines(&self) -> Result<()> {
|
||||
println!("Setting up Vault secrets engines...");
|
||||
|
||||
// Enable Transit for document signing
|
||||
self.enable_secrets_engine("transit", "transit").await?;
|
||||
|
||||
// Enable KV v2 for document storage
|
||||
self.enable_secrets_engine("kv-v2", "documents").await?;
|
||||
|
||||
// Enable userpass for authentication
|
||||
self.enable_auth_method("userpass").await?;
|
||||
|
||||
println!("Secrets engines setup complete!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Enable a secrets engine
|
||||
async fn enable_secrets_engine(&self, engine_type: &str, path: &str) -> Result<()> {
|
||||
println!("Enabling {} secrets engine at {}", engine_type, path);
|
||||
|
||||
let url = format!("{}/v1/sys/mounts/{}", self.addr, path);
|
||||
let payload = json!({
|
||||
"type": engine_type,
|
||||
});
|
||||
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.header("X-Vault-Token", &self.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::NO_CONTENT | StatusCode::OK => {
|
||||
println!("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);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Failed to enable secrets engine: {}", error_text))
|
||||
}
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to enable secrets engine: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable an auth method
|
||||
async fn enable_auth_method(&self, method: &str) -> Result<()> {
|
||||
println!("Enabling {} auth method", method);
|
||||
|
||||
let url = format!("{}/v1/sys/auth/{}", self.addr, method);
|
||||
let payload = json!({
|
||||
"type": method,
|
||||
});
|
||||
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.header("X-Vault-Token", &self.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::NO_CONTENT | StatusCode::OK => {
|
||||
println!("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);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Failed to enable auth method: {}", error_text))
|
||||
}
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to enable auth method: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new user in Vault and associate with department
|
||||
pub async fn create_user(&self, username: &str, password: &str, department: Department) -> Result<()> {
|
||||
println!("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?;
|
||||
|
||||
// Step 2: Create the user with userpass auth
|
||||
let url = format!("{}/v1/auth/userpass/users/{}", self.addr, username);
|
||||
let payload = json!({
|
||||
"password": password,
|
||||
"policies": [policy_name],
|
||||
});
|
||||
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.header("X-Vault-Token", &self.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::NO_CONTENT | StatusCode::OK => {
|
||||
println!("Successfully created user {}", username);
|
||||
|
||||
// Step 3: Create a signing key for the user
|
||||
self.create_signing_key(username).await?;
|
||||
|
||||
// Step 4: Store user metadata in KV store
|
||||
self.store_user_metadata(username, department).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to create user: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a signing policy for a user based on their department
|
||||
async fn create_signing_policy(&self, policy_name: &str, department: Department) -> Result<()> {
|
||||
let dept_name = match department {
|
||||
Department::Legal => "legal",
|
||||
Department::Finance => "finance",
|
||||
};
|
||||
|
||||
// Policy content with specific paths for the department
|
||||
let policy = format!(r#"
|
||||
# Allow reading document metadata
|
||||
path "documents/data/docs/*" {{
|
||||
capabilities = ["read"]
|
||||
}}
|
||||
|
||||
# Allow signing with user's key
|
||||
path "transit/sign/{{{{identity.entity.name}}}}" {{
|
||||
capabilities = ["update"]
|
||||
}}
|
||||
|
||||
# Allow signature verification
|
||||
path "transit/verify/*" {{
|
||||
capabilities = ["update"]
|
||||
}}
|
||||
|
||||
# Department-specific path
|
||||
path "documents/data/dept/{}/signatures/*" {{
|
||||
capabilities = ["create", "read", "update"]
|
||||
}}
|
||||
"#, dept_name);
|
||||
|
||||
let url = format!("{}/v1/sys/policies/acl/{}", self.addr, policy_name);
|
||||
let payload = json!({
|
||||
"policy": policy,
|
||||
});
|
||||
|
||||
let response = self.client
|
||||
.put(&url)
|
||||
.header("X-Vault-Token", &self.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::NO_CONTENT | StatusCode::OK => {
|
||||
println!("Successfully created policy {}", policy_name);
|
||||
Ok(())
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to create policy: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a signing key for a user in the Transit engine
|
||||
async fn create_signing_key(&self, username: &str) -> Result<()> {
|
||||
let url = format!("{}/v1/transit/keys/{}", self.addr, username);
|
||||
let payload = json!({
|
||||
"type": "ed25519",
|
||||
});
|
||||
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.header("X-Vault-Token", &self.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::NO_CONTENT | StatusCode::OK => {
|
||||
println!("Successfully created signing key for {}", username);
|
||||
Ok(())
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to create signing key: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store user metadata in the KV store
|
||||
async fn store_user_metadata(&self, username: &str, department: Department) -> Result<()> {
|
||||
let dept_str = match department {
|
||||
Department::Legal => "legal",
|
||||
Department::Finance => "finance",
|
||||
};
|
||||
|
||||
let url = format!("{}/v1/documents/data/users/{}", self.addr, username);
|
||||
let payload = json!({
|
||||
"data": {
|
||||
"username": username,
|
||||
"department": dept_str,
|
||||
}
|
||||
});
|
||||
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.header("X-Vault-Token", &self.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::NO_CONTENT | StatusCode::OK => {
|
||||
println!("Successfully stored metadata for user {}", username);
|
||||
Ok(())
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to store user metadata: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create 10 users with departmental hierarchy - 5 in each department
|
||||
pub async fn setup_hierarchical_users(&self) -> Result<()> {
|
||||
// Create 5 users in Legal department
|
||||
for i in 1..=5 {
|
||||
let username = format!("legal{}", i);
|
||||
let password = format!("legal{}pass", i);
|
||||
self.create_user(&username, &password, Department::Legal).await?;
|
||||
}
|
||||
|
||||
// Create 5 users in Finance department
|
||||
for i in 1..=5 {
|
||||
let username = format!("finance{}", i);
|
||||
let password = format!("finance{}pass", i);
|
||||
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!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Configure document signing requirements
|
||||
async fn setup_signing_requirements(&self) -> Result<()> {
|
||||
let url = format!("{}/v1/documents/data/config/signing_requirements", self.addr);
|
||||
let payload = json!({
|
||||
"data": {
|
||||
"total_required": 3,
|
||||
"departments": [
|
||||
{
|
||||
"name": "legal",
|
||||
"required": 1,
|
||||
"total_users": 5
|
||||
},
|
||||
{
|
||||
"name": "finance",
|
||||
"required": 1,
|
||||
"total_users": 5
|
||||
}
|
||||
],
|
||||
"description": "Requires 3 signatures total, with at least 1 from Legal and 1 from Finance departments"
|
||||
}
|
||||
});
|
||||
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.header("X-Vault-Token", &self.token)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::NO_CONTENT | StatusCode::OK => {
|
||||
println!("Successfully configured signing requirements");
|
||||
Ok(())
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to configure signing requirements: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Login a user and get their token
|
||||
pub async fn login_user(&self, username: &str, password: &str) -> Result<String> {
|
||||
let url = format!("{}/v1/auth/userpass/login/{}", self.addr, username);
|
||||
let payload = json!({
|
||||
"password": password,
|
||||
});
|
||||
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => {
|
||||
let json: serde_json::Value = response.json().await?;
|
||||
let token = json["auth"]["client_token"]
|
||||
.as_str()
|
||||
.context("Failed to extract client token")?
|
||||
.to_string();
|
||||
|
||||
println!("User {} successfully logged in", username);
|
||||
Ok(token)
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to login: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get user info including department
|
||||
pub async fn get_user_info(&self, username: &str) -> Result<User> {
|
||||
let url = format!("{}/v1/documents/data/users/{}", self.addr, username);
|
||||
|
||||
let response = self.client
|
||||
.get(&url)
|
||||
.header("X-Vault-Token", &self.token)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => {
|
||||
let json: serde_json::Value = response.json().await?;
|
||||
let department_str = json["data"]["data"]["department"]
|
||||
.as_str()
|
||||
.context("Failed to extract department")?;
|
||||
|
||||
let department = match department_str {
|
||||
"legal" => Department::Legal,
|
||||
"finance" => Department::Finance,
|
||||
_ => return Err(anyhow::anyhow!("Unknown department: {}", department_str)),
|
||||
};
|
||||
|
||||
Ok(User {
|
||||
username: username.to_string(),
|
||||
department,
|
||||
})
|
||||
}
|
||||
status => {
|
||||
let error_text = response.text().await?;
|
||||
Err(anyhow::anyhow!("Failed to get user info: {} - {}", status, error_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue