use super::vault_common::{KvConfig, KvProvider, Product, ROLE_ID, SECRET_ID, TOKEN};
use super::{Address, Provider, ProviderCredentials, ProviderUrl};
use crate::config::NativeAddress;
use crate::{Result, SecretSpecError};
use secrecy::SecretString;
#[derive(Debug, Clone, Default)]
pub struct VaultConfig(KvConfig);
impl TryFrom<&ProviderUrl> for VaultConfig {
type Error = SecretSpecError;
fn try_from(url: &ProviderUrl) -> Result<Self> {
KvConfig::parse(url, Product::Vault).map(Self)
}
}
pub struct VaultProvider {
core: KvProvider,
}
crate::register_provider! {
struct: VaultProvider,
config: VaultConfig,
name: "vault",
description: "HashiCorp Vault secret management",
schemes: ["vault"],
examples: ["vault://vault.example.com:8200/secret"],
credential_names: [ROLE_ID, SECRET_ID, TOKEN],
deletes: true,
}
impl VaultProvider {
pub fn new(config: VaultConfig) -> Self {
Self {
core: KvProvider::new(config.0, Product::Vault),
}
}
}
impl Provider for VaultProvider {
fn convention_address(&self, project: &str, profile: &str, key: &str) -> Result<NativeAddress> {
self.core.convention_address(project, profile, key)
}
fn with_credentials(&mut self, credentials: ProviderCredentials) {
self.core.with_credentials(credentials);
}
fn name(&self) -> &'static str {
Self::PROVIDER_NAME
}
fn uri(&self) -> String {
self.core.uri()
}
fn storage_identity(&self) -> String {
self.core.storage_identity()
}
fn supported_coords(&self) -> &'static [&'static str] {
self.core.supported_coords()
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretString>> {
let coords = self.resolve_coords(addr)?;
self.core.get(&coords)
}
fn get_many(
&self,
requests: &[(&str, Address<'_>)],
) -> Result<std::collections::HashMap<String, SecretString>> {
self.core.get_many(requests)
}
fn set(&self, addr: Address<'_>, value: &SecretString) -> Result<()> {
self.check_writable(addr)?;
let coords = self.resolve_coords(addr)?;
self.core.set(&coords, value)
}
fn set_expiring(
&self,
addr: Address<'_>,
value: &SecretString,
max_age: std::time::Duration,
) -> Result<()> {
self.check_writable(addr)?;
let coords = self.resolve_coords(addr)?;
self.core.set_expiring(&coords, value, max_age)
}
fn delete(&self, addr: Address<'_>) -> Result<bool> {
self.core.check_deletable(addr)?;
let coords = self.resolve_coords(addr)?;
self.core.delete(&coords)
}
fn check_writable(&self, addr: Address<'_>) -> Result<()> {
self.core.check_writable(addr)
}
}
#[cfg(test)]
mod tests {
use super::*;
use url::Url;
fn config(spec: &str) -> VaultConfig {
VaultConfig::try_from(&ProviderUrl::new(Url::parse(spec).unwrap())).unwrap()
}
#[test]
fn field_query_is_rejected_in_favour_of_a_ref() {
let err = VaultConfig::try_from(&ProviderUrl::new(
Url::parse("vault://vault.example.com:8200/secret?field=x").unwrap(),
))
.unwrap_err();
assert!(err.to_string().contains("ref = { item ="), "{err}");
}
#[test]
fn convention_address_is_the_writable_value_field() {
let provider = VaultProvider::new(config("vault://vault.example.com:8200/secret"));
let address = provider
.resolve_coords(Address::convention("app", "prod", "DATABASE_URL"))
.unwrap();
assert_eq!(address.item, "secretspec/app/prod/DATABASE_URL");
assert_eq!(address.field.as_deref(), Some("value"));
assert!(
provider
.check_writable(Address::convention("app", "prod", "DATABASE_URL"))
.is_ok()
);
}
#[test]
fn native_address_requires_a_field() {
let provider = VaultProvider::new(config("vault://vault.example.com:8200/secret"));
let address = NativeAddress {
item: "myapp/config".into(),
..Default::default()
};
let error = provider.get(Address::Native(&address)).unwrap_err();
assert!(error.to_string().contains("need a `field`"), "{error}");
}
#[test]
fn native_address_is_read_only() {
let provider = VaultProvider::new(config("vault://vault.example.com:8200/secret"));
let address = NativeAddress {
item: "myapp/config".into(),
field: Some("db_password".into()),
..Default::default()
};
let refusal = provider
.check_writable(Address::Native(&address))
.unwrap_err();
assert!(refusal.to_string().contains("read-only"), "{refusal}");
let error = provider
.set(Address::Native(&address), &SecretString::new("v".into()))
.unwrap_err();
assert_eq!(error.to_string(), refusal.to_string());
}
#[test]
fn native_address_rejects_version() {
let provider = VaultProvider::new(config("vault://vault.example.com:8200/secret"));
let address = NativeAddress {
item: "myapp/config".into(),
field: Some("db_password".into()),
version: Some("3".into()),
..Default::default()
};
let error = provider.get(Address::Native(&address)).unwrap_err();
assert!(error.to_string().contains("`version`"), "{error}");
}
}