use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use super::types::map_http_error;
use crate::common::{Error, Result, error::StringError};
#[cfg_attr(test, mockall::automock)]
#[async_trait]
pub trait HttpOps: Send + Sync {
fn display_name(&self) -> String;
fn debug_info(&self) -> String;
async fn get(&self, name: &str) -> Result<String>;
async fn set(&self, name: &str, value: &str) -> Result<()>;
async fn delete(&self, name: &str) -> Result<()>;
async fn list(&self, prefix: Option<String>) -> Result<Vec<String>>;
}
#[derive(Deserialize)]
struct GetResponse {
value: String,
}
#[derive(Serialize)]
struct SetRequest<'a> {
value: &'a str,
}
#[derive(Deserialize)]
struct ListResponse {
keys: Option<Vec<String>>,
}
pub(super) struct ReqwestHttpClient {
pub base_url: String,
pub auth_token: Option<String>,
pub namespace: Option<String>,
pub http: reqwest::Client,
}
impl ReqwestHttpClient {
fn url_for(&self, name: &str) -> String {
format!("{}/{}", self.base_url, name)
}
fn apply_auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
let req = match &self.auth_token {
Some(token) => req.bearer_auth(token),
None => req,
};
match &self.namespace {
Some(ns) => req.header("X-Vault-Namespace", ns),
None => req,
}
}
}
#[async_trait]
impl HttpOps for ReqwestHttpClient {
fn display_name(&self) -> String {
self.base_url.clone()
}
fn debug_info(&self) -> String {
format!(
"base_url={base_url}, auth={auth}, namespace={ns}, provider=HttpSecretStore",
base_url = self.base_url,
auth = if self.auth_token.is_some() {
"Bearer"
} else {
"none"
},
ns = self.namespace.as_deref().unwrap_or("none"),
)
}
async fn get(&self, name: &str) -> Result<String> {
let resp = self
.apply_auth(self.http.get(self.url_for(name)))
.send()
.await
.map_err(|e| Error::Generic {
store: "HttpSecretStore",
source: Box::new(e),
})?;
let status = resp.status().as_u16();
if !resp.status().is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(map_http_error(name, status, StringError(body)));
}
resp.json::<GetResponse>()
.await
.map(|r| r.value)
.map_err(|e| Error::Generic {
store: "HttpSecretStore",
source: Box::new(e),
})
}
async fn set(&self, name: &str, value: &str) -> Result<()> {
let resp = self
.apply_auth(self.http.post(self.url_for(name)))
.json(&SetRequest { value })
.send()
.await
.map_err(|e| Error::Generic {
store: "HttpSecretStore",
source: Box::new(e),
})?;
let status = resp.status().as_u16();
if !resp.status().is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(map_http_error(name, status, StringError(body)));
}
Ok(())
}
async fn delete(&self, name: &str) -> Result<()> {
let resp = self
.apply_auth(self.http.delete(self.url_for(name)))
.send()
.await
.map_err(|e| Error::Generic {
store: "HttpSecretStore",
source: Box::new(e),
})?;
let status = resp.status().as_u16();
if !resp.status().is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(map_http_error(name, status, StringError(body)));
}
Ok(())
}
async fn list(&self, prefix: Option<String>) -> Result<Vec<String>> {
let url = match &prefix {
Some(p) => format!("{}/{}", self.base_url, p),
None => self.base_url.clone(),
};
let resp = self
.apply_auth(self.http.get(&url).query(&[("list", "true")]))
.send()
.await
.map_err(|e| Error::Generic {
store: "HttpSecretStore",
source: Box::new(e),
})?;
let status = resp.status().as_u16();
if !resp.status().is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(map_http_error("(list)", status, StringError(body)));
}
let list_resp: ListResponse = resp.json().await.map_err(|e| Error::Generic {
store: "HttpSecretStore",
source: Box::new(e),
})?;
let prefix_str = prefix.as_deref().unwrap_or("");
let names = list_resp
.keys
.unwrap_or_default()
.into_iter()
.map(|k| {
if prefix_str.is_empty() {
k
} else {
format!("{prefix_str}{k}")
}
})
.collect();
Ok(names)
}
}