wasmcloud_secrets_client/
lib.rsuse async_nats::HeaderMap;
use nkeys::XKey;
use wasmcloud_secrets_types::{
Secret, SecretRequest, SecretResponse, RESPONSE_XKEY, WASMCLOUD_HOST_XKEY,
};
const DEFAULT_API_VERSION: &str = "v1alpha1";
#[derive(Debug, thiserror::Error)]
pub enum SecretClientError {
#[error("failed to convert server xkey: {0}")]
ConvertServerXkey(String),
#[error("failed to parse server xkey: {0}")]
ParseServerXkey(nkeys::error::Error),
#[error("failed to fetch server xkey: {0}")]
RequestServerXkey(async_nats::RequestError),
#[error("invalid xkey: {0}")]
InvalidXkey(nkeys::error::Error),
#[error("failed to seal secret request: {0}")]
SealSecretRequest(nkeys::error::Error),
#[error("failed to send secret request: {0}")]
SendSecretRequest(async_nats::RequestError),
#[error("failed to serialize secret request: {0}")]
SerializeSecretRequest(serde_json::error::Error),
#[error("failed to parse xkey from server response: {0}")]
ParseServerResponseXkey(nkeys::error::Error),
#[error("failed to open secret response: {0}")]
OpenSecretResponse(nkeys::error::Error),
#[error("failed to deserialize secret response: {0}")]
DeserializeSecretResponse(serde_json::error::Error),
#[error("server error: {0}")]
Server(String),
#[error("missing secret: {0}")]
MissingSecret(String),
}
#[derive(Debug)]
struct SecretsTopic(String);
impl SecretsTopic {
pub(crate) fn new(prefix: &str, backend: &str, api_version: Option<&str>) -> Self {
let version = api_version.unwrap_or(DEFAULT_API_VERSION);
Self(format!("{}.{}.{}", prefix, version, backend))
}
pub fn get(&self) -> String {
format!("{}.{}", self.0, "get")
}
pub fn server_xkey(&self) -> String {
format!("{}.{}", self.0, "server_xkey")
}
}
#[derive(Debug)]
pub struct Client {
client: async_nats::Client,
topic: SecretsTopic,
server_xkey: XKey,
}
impl Client {
pub async fn new(
backend: &str,
prefix: &str,
nats_client: async_nats::Client,
) -> Result<Self, SecretClientError> {
Self::new_with_version(backend, prefix, nats_client, None).await
}
pub async fn new_with_version(
backend: &str,
prefix: &str,
nats_client: async_nats::Client,
api_version: Option<&str>,
) -> Result<Self, SecretClientError> {
let secrets_topic = SecretsTopic::new(prefix, backend, api_version);
let resp = nats_client
.request(secrets_topic.server_xkey(), "".into())
.await
.map_err(SecretClientError::RequestServerXkey)?;
let s = std::str::from_utf8(&resp.payload)
.map_err(|e| SecretClientError::ConvertServerXkey(e.to_string()))?;
let server_xkey = XKey::from_public_key(s).map_err(SecretClientError::ParseServerXkey)?;
Ok(Self {
client: nats_client,
topic: secrets_topic,
server_xkey,
})
}
pub async fn get(
&self,
secret_request: SecretRequest,
request_xkey: XKey,
) -> Result<Secret, SecretClientError> {
if let Err(e) = request_xkey.seed() {
return Err(SecretClientError::InvalidXkey(e));
}
let request = serde_json::to_string(&secret_request)
.map_err(SecretClientError::SerializeSecretRequest)?;
let encrypted_request = request_xkey
.seal(request.as_bytes(), &self.server_xkey)
.map_err(SecretClientError::SealSecretRequest)?;
let response = self
.client
.request_with_headers(
self.topic.get(),
self.request_headers(request_xkey.public_key()),
encrypted_request.into(),
)
.await
.map_err(SecretClientError::SendSecretRequest)?;
let headers = response.headers.unwrap_or_default();
let Some(response_xkey_header) = headers.get(RESPONSE_XKEY) else {
let sr: SecretResponse = serde_json::from_slice(&response.payload)
.map_err(SecretClientError::DeserializeSecretResponse)?;
if let Some(error) = sr.error {
return Err(SecretClientError::Server(error.to_string()));
}
return Err(SecretClientError::Server(
"unhandled server error (the server errored without explanation)".into(),
));
};
let response_xkey = XKey::from_public_key(response_xkey_header.as_str())
.map_err(SecretClientError::ParseServerResponseXkey)?;
let decrypted = request_xkey
.open(&response.payload, &response_xkey)
.map_err(SecretClientError::OpenSecretResponse)?;
let sr: SecretResponse = serde_json::from_slice(&decrypted)
.map_err(SecretClientError::DeserializeSecretResponse)?;
sr.secret.ok_or_else(|| {
SecretClientError::MissingSecret(format!(
"no secret found with name [{}]",
secret_request.key
))
})
}
fn request_headers(&self, pubkey: String) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(WASMCLOUD_HOST_XKEY, pubkey.as_str());
headers
}
}