use super::{Secret, SecretError, SecretStore, BoxedAuditSink, SecretAuditEvent};
use crate::secrets::config::{VaultAuthConfig, VaultConfig};
use async_trait::async_trait;
use std::time::Duration;
use vaultrs::client::{VaultClient, VaultClientSettingsBuilder, Client};
use vaultrs::error::ClientError;
use vaultrs::kv2;
use vaultrs::token;
pub struct VaultSecretStore {
client: VaultClient,
config: VaultConfig,
agent_id: String,
audit_sink: Option<BoxedAuditSink>,
}
impl VaultSecretStore {
pub async fn new(
config: VaultConfig,
agent_id: String,
audit_sink: Option<BoxedAuditSink>,
) -> Result<Self, SecretError> {
let client = Self::create_vault_client(&config).await?;
Ok(Self {
client,
config,
agent_id,
audit_sink,
})
}
async fn create_vault_client(config: &VaultConfig) -> Result<VaultClient, SecretError> {
let mut settings_builder = VaultClientSettingsBuilder::default();
settings_builder.address(&config.url);
if let Some(namespace) = &config.namespace {
settings_builder.namespace(Some(namespace.clone()));
}
if config.tls.skip_verify {
settings_builder.verify(false);
}
let connection_timeout = Duration::from_secs(config.connection.connection_timeout_seconds);
settings_builder.timeout(Some(connection_timeout));
let settings = settings_builder.build()
.map_err(|e| SecretError::ConfigurationError {
message: format!("Failed to build Vault client settings: {}", e),
})?;
let client = VaultClient::new(settings)
.map_err(|e| SecretError::ConnectionError {
message: format!("Failed to create Vault client: {}", e),
})?;
Ok(client)
}
pub async fn authenticate(&mut self) -> Result<(), SecretError> {
let auth_config = self.config.auth.clone();
match auth_config {
VaultAuthConfig::Token { token } => {
self.client.set_token(&token);
self.verify_token().await
}
VaultAuthConfig::Kubernetes { token_path, role, mount_path } => {
self.authenticate_kubernetes(&token_path, &role, &mount_path).await
}
VaultAuthConfig::Aws { region, role, mount_path } => {
self.authenticate_aws(®ion, &role, &mount_path).await
}
VaultAuthConfig::AppRole { role_id, secret_id, mount_path } => {
self.authenticate_approle(&role_id, &secret_id, &mount_path).await
}
}
}
async fn verify_token(&self) -> Result<(), SecretError> {
match token::lookup_self(&self.client).await {
Ok(_) => Ok(()),
Err(e) => Err(self.map_vault_error(e)),
}
}
async fn authenticate_kubernetes(&mut self, token_path: &str, role: &str, mount_path: &str) -> Result<(), SecretError> {
let jwt = tokio::fs::read_to_string(token_path)
.await
.map_err(|e| SecretError::AuthenticationFailed {
message: format!("Failed to read Kubernetes token from {}: {}", token_path, e),
})?;
let auth_info = vaultrs::auth::kubernetes::login(&self.client, mount_path, role, &jwt)
.await
.map_err(|e| SecretError::AuthenticationFailed {
message: format!("Kubernetes authentication failed: {}", e),
})?;
self.client.set_token(&auth_info.client_token);
Ok(())
}
async fn authenticate_aws(&mut self, _region: &str, role: &str, _mount_path: &str) -> Result<(), SecretError> {
Err(SecretError::UnsupportedOperation {
operation: format!("AWS IAM authentication not yet implemented for role: {}", role),
})
}
async fn authenticate_approle(&mut self, role_id: &str, secret_id: &str, mount_path: &str) -> Result<(), SecretError> {
let auth_info = vaultrs::auth::approle::login(&self.client, mount_path, role_id, secret_id)
.await
.map_err(|e| SecretError::AuthenticationFailed {
message: format!("AppRole authentication failed: {}", e),
})?;
self.client.set_token(&auth_info.client_token);
Ok(())
}
fn get_secret_path(&self, key: &str) -> String {
format!("agents/{}/secrets/{}", self.agent_id, key)
}
fn get_base_path(&self) -> String {
format!("agents/{}/secrets", self.agent_id)
}
fn map_vault_error(&self, error: ClientError) -> SecretError {
match error {
ClientError::RestClientError { .. } => SecretError::ConnectionError {
message: error.to_string(),
},
ClientError::APIError { code: 404, .. } => SecretError::NotFound {
key: "unknown".to_string(), },
ClientError::APIError { code: 403, .. } => SecretError::PermissionDenied {
key: "unknown".to_string(),
},
ClientError::APIError { code: 401, .. } => SecretError::AuthenticationFailed {
message: "Vault authentication failed".to_string(),
},
ClientError::APIError { code: 429, .. } => SecretError::RateLimitExceeded {
message: "Vault rate limit exceeded".to_string(),
},
_ => SecretError::BackendError {
message: error.to_string(),
},
}
}
async fn log_audit_event(&self, event: SecretAuditEvent) {
if let Some(audit_sink) = &self.audit_sink {
if let Err(e) = audit_sink.log_event(event).await {
eprintln!("Audit logging failed: {}", e);
}
}
}
}
#[async_trait]
impl SecretStore for VaultSecretStore {
async fn get_secret(&self, key: &str) -> Result<Secret, SecretError> {
let path = self.get_secret_path(key);
let result: Result<Secret, SecretError> = async {
match kv2::read::<serde_json::Value>(&self.client, &self.config.mount_path, &path).await {
Ok(secret_response) => {
let data = secret_response
.get("data")
.and_then(|d| d.get("data"))
.ok_or_else(|| SecretError::BackendError {
message: "Invalid Vault response structure".to_string(),
})?;
let secret_value = data
.get("value")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| {
data.as_object()
.and_then(|obj| obj.values().next())
.and_then(|v| v.as_str())
.map(|s| s.to_string())
})
.ok_or_else(|| SecretError::BackendError {
message: format!("No string value found for key '{}'", key),
})?;
let mut metadata = std::collections::HashMap::new();
if let Some(metadata_obj) = secret_response.get("data").and_then(|d| d.get("metadata")) {
if let Some(created_time) = metadata_obj.get("created_time").and_then(|v| v.as_str()) {
metadata.insert("created_time".to_string(), created_time.to_string());
}
if let Some(version) = metadata_obj.get("version").and_then(|v| v.as_u64()) {
metadata.insert("version".to_string(), version.to_string());
}
if let Some(destroyed) = metadata_obj.get("destroyed").and_then(|v| v.as_bool()) {
metadata.insert("destroyed".to_string(), destroyed.to_string());
}
if let Some(deletion_time) = metadata_obj.get("deletion_time").and_then(|v| v.as_str()) {
if deletion_time != "" && deletion_time != "null" {
metadata.insert("deletion_time".to_string(), deletion_time.to_string());
}
}
}
let created_at = metadata
.get("created_time")
.map(|ts| ts.clone());
let version = metadata
.get("version")
.map(|v| v.clone());
Ok(Secret {
key: key.to_string(),
value: secret_value,
metadata: Some(metadata),
created_at,
version,
})
}
Err(e) => {
let mapped_error = self.map_vault_error(e);
match mapped_error {
SecretError::NotFound { .. } => Err(SecretError::NotFound {
key: key.to_string(),
}),
SecretError::PermissionDenied { .. } => Err(SecretError::PermissionDenied {
key: key.to_string(),
}),
other => Err(other),
}
}
}
}.await;
let audit_event = match &result {
Ok(_) => SecretAuditEvent::success(
self.agent_id.clone(),
"get_secret".to_string(),
Some(key.to_string()),
),
Err(e) => SecretAuditEvent::failure(
self.agent_id.clone(),
"get_secret".to_string(),
Some(key.to_string()),
e.to_string(),
),
};
self.log_audit_event(audit_event).await;
result
}
async fn list_secrets(&self) -> Result<Vec<String>, SecretError> {
let base_path = self.get_base_path();
let result: Result<Vec<String>, SecretError> = async {
match kv2::list(&self.client, &self.config.mount_path, &base_path).await {
Ok(list_response) => {
Ok(list_response)
}
Err(e) => {
let mapped_error = self.map_vault_error(e);
match mapped_error {
SecretError::NotFound { .. } => Ok(vec![]),
other => Err(other),
}
}
}
}.await;
let audit_event = match &result {
Ok(keys) => SecretAuditEvent::success(
self.agent_id.clone(),
"list_secrets".to_string(),
None,
).with_metadata(serde_json::json!({
"secrets_count": keys.len()
})),
Err(e) => SecretAuditEvent::failure(
self.agent_id.clone(),
"list_secrets".to_string(),
None,
e.to_string(),
),
};
self.log_audit_event(audit_event).await;
result
}
}
impl VaultSecretStore {
pub async fn list_secrets_with_prefix(&self, prefix: &str) -> Result<Vec<String>, SecretError> {
let all_keys = self.list_secrets().await?;
Ok(all_keys
.into_iter()
.filter(|key| key.starts_with(prefix))
.collect())
}
pub fn agent_id(&self) -> &str {
&self.agent_id
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::secrets::config::{VaultConnectionConfig, VaultTlsConfig};
fn create_test_config() -> VaultConfig {
VaultConfig {
url: "http://localhost:8200".to_string(),
auth: VaultAuthConfig::Token {
token: "test-token".to_string(),
},
namespace: None,
mount_path: "secret".to_string(),
api_version: "v2".to_string(),
tls: VaultTlsConfig::default(),
connection: VaultConnectionConfig::default(),
}
}
#[test]
fn test_secret_path_generation() {
let _config = create_test_config();
let agent_id = "test-agent-123";
let expected_path = format!("agents/{}/secrets/my-key", agent_id);
let path = format!("agents/{}/secrets/{}", agent_id, "my-key");
assert_eq!(path, expected_path);
}
#[test]
fn test_base_path_generation() {
let agent_id = "test-agent-123";
let expected_base = format!("agents/{}/secrets", agent_id);
let base_path = format!("agents/{}/secrets", agent_id);
assert_eq!(base_path, expected_base);
}
}