use super::{BoxedAuditSink, Secret, SecretAuditEvent, SecretError, SecretStore};
use crate::secrets::config::{VaultAuthConfig, VaultConfig};
use async_trait::async_trait;
use std::time::Duration;
use vaultrs::client::{Client, VaultClient, VaultClientSettingsBuilder};
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.is_empty() && deletion_time != "null" {
metadata
.insert("deletion_time".to_string(), deletion_time.to_string());
}
}
}
let created_at = metadata.get("created_time").cloned();
let version = metadata.get("version").cloned();
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);
}
}