use crate::vault::{VaultClient, VaultError};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tracing::{info, warn};
#[derive(Debug, Serialize, Deserialize)]
struct TokenResponse {
request_id: String,
lease_id: String,
renewable: bool,
lease_duration: u64,
auth: TokenAuth,
}
#[derive(Debug, Serialize, Deserialize)]
struct TokenAuth {
client_token: String,
accessor: String,
policies: Vec<String>,
token_policies: Vec<String>,
metadata: Option<serde_json::Map<String, Value>>,
renewable: bool,
lease_duration: u64,
}
#[derive(Debug, Serialize, Deserialize)]
struct WrappedResponse {
request_id: String,
wrap_info: WrapInfo,
}
#[derive(Debug, Serialize, Deserialize)]
struct WrapInfo {
token: String,
ttl: u64,
creation_time: String,
creation_path: String,
}
pub async fn setup_transit_engine(vault_addr: &str, token: &str) -> Result<(), VaultError> {
let client = VaultClient::new(vault_addr, token)?;
let response = client
.post_with_body(
"/v1/sys/mounts/transit",
json!({
"type": "transit"
}),
)
.await;
match response {
Ok(_) => Ok(()),
Err(VaultError::HttpStatus(_status, error_text))
if error_text.contains("path is already in use") =>
{
info!("Transit engine already enabled, continuing");
Ok(())
}
Err(e) => {
warn!("Failed to enable transit engine: {}", e);
Err(e)
}
}
}
pub async fn create_transit_key(
vault_addr: &str,
token: &str,
key_name: &str,
) -> Result<(), VaultError> {
let client = VaultClient::new(vault_addr, token)?;
let response = client
.put_with_body(
&format!("/v1/transit/keys/{}", key_name),
json!({
"derived": false,
"exportable": false
}),
)
.await;
match response {
Ok(_) => Ok(()),
Err(VaultError::HttpStatus(_status, error_text))
if error_text.contains("already exists") =>
{
info!("Transit key '{}' already exists, continuing", key_name);
Ok(())
}
Err(e) => {
warn!("Failed to create transit key '{}': {}", key_name, e);
Err(e)
}
}
}
pub async fn create_transit_unseal_policy(
vault_addr: &str,
token: &str,
policy_name: &str,
key_name: &str,
) -> Result<(), VaultError> {
let client = VaultClient::new(vault_addr, token)?;
let policy_hcl = format!(
r#"
# Allow token creation
path "auth/token/create" {{
capabilities = ["update"]
}}
# Allow encryption operations with the key
path "transit/encrypt/{}" {{
capabilities = ["update"]
}}
# Allow decryption operations with the key
path "transit/decrypt/{}" {{
capabilities = ["update"]
}}
"#,
key_name, key_name
);
let response = client
.put_with_body(
&format!("/v1/sys/policies/acl/{}", policy_name),
json!({
"policy": policy_hcl,
}),
)
.await;
match response {
Ok(_) => {
info!("Created/updated auto-unseal policy: {}", policy_name);
Ok(())
}
Err(e) => {
warn!("Failed to create/update policy '{}': {}", policy_name, e);
Err(e)
}
}
}
#[cfg(any(test, feature = "full-api"))]
pub async fn generate_transit_unseal_token(
vault_addr: &str,
token: &str,
policy_name: &str,
) -> Result<String, VaultError> {
let client = VaultClient::new(vault_addr, token)?;
let response = client
.post_with_body(
"/v1/auth/token/create",
json!({
"policies": [policy_name],
"ttl": "768h", "display_name": "transit-unseal-token",
"renewable": true
}),
)
.await?;
let client_token = response
.get("auth")
.and_then(|auth| auth.get("client_token"))
.and_then(|token| token.as_str())
.ok_or_else(|| VaultError::Api("Failed to extract client token".to_string()))?;
Ok(client_token.to_string())
}
pub async fn generate_wrapped_transit_token(
vault_addr: &str,
token: &str,
policy_name: &str,
wrap_ttl: &str,
) -> Result<String, VaultError> {
let mut client = VaultClient::new(vault_addr, token)?;
client.add_header("X-Vault-Wrap-TTL", wrap_ttl);
let response = client
.post_with_body(
"/v1/auth/token/create-orphan",
json!({
"policies": [policy_name],
"period": "24h",
"display_name": "transit-unseal-token",
"renewable": true
}),
)
.await?;
let wrapped_token = response
.get("wrap_info")
.and_then(|wrap| wrap.get("token"))
.and_then(|token| token.as_str())
.ok_or_else(|| VaultError::Api("Failed to extract wrapped token".to_string()))?;
Ok(wrapped_token.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::init_logging;
use crate::vault::status::get_vault_status;
use crate::vault::test_utils::{setup_vault_container, wait_for_vault_ready, VaultMode};
use tracing::info;
#[tokio::test]
async fn test_transit_invalid_token() -> Result<(), Box<dyn std::error::Error>> {
let invalid_token = "invalid-token";
let result = setup_transit_engine("http://127.0.0.1:8200", invalid_token).await;
assert!(result.is_err(), "Expected error with invalid token");
match result {
Err(VaultError::HttpStatus(status_code, _)) => {
assert_eq!(
status_code, 403,
"Expected 403 Forbidden with invalid token"
);
}
Err(VaultError::Connection(_)) => {
println!(
"Connection error (Vault not running) - this is expected in standalone tests"
);
}
Err(VaultError::Network(_)) => {
println!(
"Network error (Vault not running) - this is expected in standalone tests"
);
}
_ => {
panic!(
"Expected VaultError::HttpStatus, VaultError::Connection, or VaultError::Network, got {:?}",
result
);
}
}
Ok(())
}
#[tokio::test]
async fn test_transit_setup() -> Result<(), Box<dyn std::error::Error>> {
init_logging();
let vault_container = setup_vault_container(VaultMode::Dev).await;
let host = vault_container.get_host().await.unwrap();
let host_port = vault_container.get_host_port_ipv4(8200).await.unwrap();
let vault_url = format!("http://{}:{}", host, host_port);
wait_for_vault_ready(&vault_url, 10, 1000)
.await
.map_err(|e| e.to_string())?;
let root_token = "root".to_string();
info!("Setting up transit engine");
setup_transit_engine(&vault_url, &root_token).await?;
info!("Transit engine setup successful");
let key_name = "test-key";
info!("Creating transit key: {}", key_name);
create_transit_key(&vault_url, &root_token, key_name).await?;
info!("Created transit key: {}", key_name);
let policy_name = "test-policy";
info!("Creating transit policy: {}", policy_name);
create_transit_unseal_policy(&vault_url, &root_token, policy_name, key_name).await?;
info!("Generating transit token");
let token = generate_transit_unseal_token(&vault_url, &root_token, policy_name).await?;
info!("Generated transit token: {}", token);
assert!(!token.is_empty(), "Token should not be empty");
info!("Generating wrapped transit token");
let wrapped_token = generate_wrapped_transit_token(
&vault_url,
&root_token,
policy_name,
"60s", )
.await?;
info!("Generated wrapped transit token");
assert!(
!wrapped_token.is_empty(),
"Wrapped token should not be empty"
);
assert!(!token.is_empty(), "Transit token should not be empty");
assert!(
!wrapped_token.is_empty(),
"Wrapped transit token should not be empty"
);
Ok(())
}
#[tokio::test]
async fn test_transit_with_real_vault() -> Result<(), Box<dyn std::error::Error>> {
init_logging();
let vault_container = setup_vault_container(VaultMode::Dev).await;
let port = vault_container.get_host_port_ipv4(8200).await?;
let vault_addr = format!("http://127.0.0.1:{}", port);
wait_for_vault_ready(&vault_addr, 10, 500).await?;
let status = get_vault_status(&vault_addr).await?;
assert!(
status.initialized,
"Vault should be initialized in dev mode"
);
assert!(!status.sealed, "Vault should not be sealed in dev mode");
let root_token = "root".to_string();
let setup_result = setup_transit_engine(&vault_addr, &root_token).await;
assert!(setup_result.is_ok(), "Transit engine setup should succeed");
let key_name = "test-key";
let create_key_result = create_transit_key(&vault_addr, &root_token, key_name).await;
assert!(
create_key_result.is_ok(),
"Transit key creation should succeed"
);
let policy_name = "test-policy";
let create_policy_result =
create_transit_unseal_policy(&vault_addr, &root_token, policy_name, key_name).await;
assert!(
create_policy_result.is_ok(),
"Transit policy creation should succeed"
);
let token_result =
generate_transit_unseal_token(&vault_addr, &root_token, policy_name).await;
assert!(
token_result.is_ok(),
"Transit token generation should succeed"
);
let token = token_result.unwrap();
assert!(!token.is_empty(), "Token should not be empty");
let wrapped_token_result = generate_wrapped_transit_token(
&vault_addr,
&root_token,
policy_name,
"60s", )
.await;
assert!(
wrapped_token_result.is_ok(),
"Wrapped transit token generation should succeed"
);
let wrapped_token = wrapped_token_result.unwrap();
assert!(
!wrapped_token.is_empty(),
"Wrapped token should not be empty"
);
Ok(())
}
}