use super::address::reject_unsupported_coords;
use super::{Address, ProviderCredentials};
use crate::config::NativeAddress;
use crate::{Result, SecretSpecError};
use secrecy::SecretString;
use std::borrow::Cow;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DiscoveryContext<'a> {
pub project: &'a str,
pub profile: &'a str,
}
impl<'a> DiscoveryContext<'a> {
pub const fn new(project: &'a str, profile: &'a str) -> Self {
Self { project, profile }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProducedValuePersistence {
Persist,
Ephemeral,
}
pub trait Provider: Send + Sync {
fn convention_address(&self, project: &str, profile: &str, key: &str) -> Result<NativeAddress>;
fn supported_coords(&self) -> &'static [&'static str] {
&[]
}
fn resolve_coords<'a>(&self, addr: Address<'a>) -> Result<Cow<'a, NativeAddress>> {
let coords = match addr {
Address::Native(native) => Cow::Borrowed(native),
Address::Convention {
project,
profile,
key,
} => Cow::Owned(self.convention_address(project, profile, key)?),
};
reject_unsupported_coords(self.name(), &coords, self.supported_coords())?;
Ok(coords)
}
fn entry_coordinates<'a>(&self, addr: Address<'a>) -> Result<Cow<'a, NativeAddress>> {
self.resolve_coords(addr)
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretString>>;
fn set(&self, addr: Address<'_>, value: &SecretString) -> Result<()>;
fn set_expiring(
&self,
addr: Address<'_>,
value: &SecretString,
max_age: std::time::Duration,
) -> Result<()> {
let _ = max_age;
self.set(addr, value)
}
fn delete(&self, addr: Address<'_>) -> Result<bool> {
let _ = addr;
Err(self.deletion_unsupported())
}
fn supports_delete(&self) -> bool {
false
}
fn deletion_unsupported(&self) -> SecretSpecError {
SecretSpecError::ProviderOperationFailed(format!(
"provider '{}' does not support deleting secrets",
self.name()
))
}
fn check_deletable(&self, addr: Address<'_>) -> Result<()> {
if !self.supports_delete() {
return Err(self.deletion_unsupported());
}
self.resolve_coords(addr).map(|_| ())
}
fn check_writable(&self, addr: Address<'_>) -> Result<()> {
let _ = addr;
Ok(())
}
fn generated_value_persistence(&self) -> ProducedValuePersistence {
ProducedValuePersistence::Persist
}
fn prompted_value_persistence(&self) -> ProducedValuePersistence {
ProducedValuePersistence::Persist
}
fn describe_write_target(&self, addr: Address<'_>) -> Result<String> {
Ok(self.resolve_coords(addr)?.render())
}
fn auth_scope_key(&self) -> Option<String> {
None
}
fn name(&self) -> &'static str;
fn uri(&self) -> String;
fn storage_identity(&self) -> String {
self.uri()
}
fn entry_container_identity(&self) -> String {
self.storage_identity()
}
fn same_entry(&self, other: &dyn Provider, addr: Address<'_>) -> Result<bool> {
self.same_entries(addr, other, addr)
}
fn same_entries(
&self,
self_addr: Address<'_>,
other: &dyn Provider,
other_addr: Address<'_>,
) -> Result<bool> {
if !same_storage_container(self, other) {
return Ok(false);
}
Ok(self.entry_coordinates(self_addr)? == other.entry_coordinates(other_addr)?)
}
fn physical_store_path(&self) -> Option<&std::path::Path> {
None
}
fn set_reason(&self, _reason: Option<String>) {}
fn set_caller(&self, _caller: Option<crate::CallerContext>) {}
fn set_profile(&self, _profile: &str) {}
fn with_base_dir(&mut self, _base_dir: &std::path::Path) {}
fn with_credentials(&mut self, _credentials: ProviderCredentials) {}
fn reflect(&self, _context: DiscoveryContext<'_>) -> Result<HashMap<String, crate::Secret>> {
Err(SecretSpecError::ProviderOperationFailed(format!(
"Provider '{}' does not support reflection",
self.name()
)))
}
fn get_many(&self, requests: &[(&str, Address<'_>)]) -> Result<HashMap<String, SecretString>> {
get_each(self, requests)
}
}
fn comparable_missing_file_path(path: &std::path::Path) -> std::path::PathBuf {
let absolute = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
let Some(parent) = absolute.parent() else {
return absolute;
};
let Some(file_name) = absolute.file_name() else {
return absolute;
};
std::fs::canonicalize(parent)
.map(|parent| parent.join(file_name))
.unwrap_or(absolute)
}
pub(crate) fn same_storage_container<L, R>(left: &L, right: &R) -> bool
where
L: Provider + ?Sized,
R: Provider + ?Sized,
{
match (left.physical_store_path(), right.physical_store_path()) {
(Some(left), Some(right)) => same_file::is_same_file(left, right).unwrap_or_else(|_| {
let left = comparable_missing_file_path(left);
let right = comparable_missing_file_path(right);
left == right
}),
(None, None) => left.entry_container_identity() == right.entry_container_identity(),
_ => false,
}
}
const DEFAULT_GET_EACH_CONCURRENCY: usize = 8;
pub(crate) const GET_EACH_CONCURRENCY_ENV: &str = "SECRETSPEC_PROVIDER_CONCURRENCY";
pub(crate) fn get_each_concurrency() -> usize {
std::env::var(GET_EACH_CONCURRENCY_ENV)
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|&n| n >= 1)
.unwrap_or(DEFAULT_GET_EACH_CONCURRENCY)
}
pub(crate) fn map_concurrently<T, R, F>(items: &[T], concurrency: usize, map: F) -> Vec<R>
where
T: Sync,
R: Send,
F: Fn(&T) -> R + Sync,
{
let concurrency = concurrency.max(1);
if items.len() <= 1 || concurrency == 1 {
return items.iter().map(map).collect();
}
let mut mapped = Vec::with_capacity(items.len());
for chunk in items.chunks(concurrency) {
std::thread::scope(|scope| {
let handles: Vec<_> = chunk.iter().map(|item| scope.spawn(|| map(item))).collect();
mapped.extend(
handles
.into_iter()
.map(|handle| handle.join().expect("concurrent map thread panicked")),
);
});
}
mapped
}
pub(crate) fn get_each<P: Provider + ?Sized>(
provider: &P,
requests: &[(&str, Address<'_>)],
) -> Result<HashMap<String, SecretString>> {
get_each_with(requests, |addr| provider.get(addr))
}
pub(crate) fn get_each_with<'a, F>(
requests: &[(&str, Address<'a>)],
fetch: F,
) -> Result<HashMap<String, SecretString>>
where
F: Fn(Address<'a>) -> Result<Option<SecretString>> + Sync,
{
let mut groups: HashMap<Address<'_>, Vec<&str>> = HashMap::new();
for (name, addr) in requests {
groups.entry(*addr).or_default().push(name);
}
let groups: Vec<(Address<'_>, Vec<&str>)> = groups.into_iter().collect();
let fetched: Vec<(Vec<&str>, Result<Option<SecretString>>)> =
map_concurrently(&groups, get_each_concurrency(), |(addr, names)| {
(names.clone(), fetch(*addr))
});
let mut results = HashMap::new();
for (names, result) in fetched {
if let Some(value) = result? {
for name in names {
results.insert(name.to_string(), value.clone());
}
}
}
Ok(results)
}
impl<T: Provider> Provider for std::sync::Arc<T> {
fn convention_address(&self, project: &str, profile: &str, key: &str) -> Result<NativeAddress> {
(**self).convention_address(project, profile, key)
}
fn supported_coords(&self) -> &'static [&'static str] {
(**self).supported_coords()
}
fn resolve_coords<'a>(&self, addr: Address<'a>) -> Result<Cow<'a, NativeAddress>> {
(**self).resolve_coords(addr)
}
fn entry_coordinates<'a>(&self, addr: Address<'a>) -> Result<Cow<'a, NativeAddress>> {
(**self).entry_coordinates(addr)
}
fn get(&self, addr: Address<'_>) -> Result<Option<SecretString>> {
(**self).get(addr)
}
fn set(&self, addr: Address<'_>, value: &SecretString) -> Result<()> {
(**self).set(addr, value)
}
fn set_expiring(
&self,
addr: Address<'_>,
value: &SecretString,
max_age: std::time::Duration,
) -> Result<()> {
(**self).set_expiring(addr, value, max_age)
}
fn delete(&self, addr: Address<'_>) -> Result<bool> {
(**self).delete(addr)
}
fn supports_delete(&self) -> bool {
(**self).supports_delete()
}
fn check_deletable(&self, addr: Address<'_>) -> Result<()> {
(**self).check_deletable(addr)
}
fn check_writable(&self, addr: Address<'_>) -> Result<()> {
(**self).check_writable(addr)
}
fn generated_value_persistence(&self) -> ProducedValuePersistence {
(**self).generated_value_persistence()
}
fn prompted_value_persistence(&self) -> ProducedValuePersistence {
(**self).prompted_value_persistence()
}
fn describe_write_target(&self, addr: Address<'_>) -> Result<String> {
(**self).describe_write_target(addr)
}
fn auth_scope_key(&self) -> Option<String> {
(**self).auth_scope_key()
}
fn name(&self) -> &'static str {
(**self).name()
}
fn uri(&self) -> String {
(**self).uri()
}
fn same_entry(&self, other: &dyn Provider, addr: Address<'_>) -> Result<bool> {
(**self).same_entry(other, addr)
}
fn same_entries(
&self,
self_addr: Address<'_>,
other: &dyn Provider,
other_addr: Address<'_>,
) -> Result<bool> {
(**self).same_entries(self_addr, other, other_addr)
}
fn storage_identity(&self) -> String {
(**self).storage_identity()
}
fn entry_container_identity(&self) -> String {
(**self).entry_container_identity()
}
fn physical_store_path(&self) -> Option<&std::path::Path> {
(**self).physical_store_path()
}
fn set_reason(&self, reason: Option<String>) {
(**self).set_reason(reason);
}
fn set_caller(&self, caller: Option<crate::CallerContext>) {
(**self).set_caller(caller);
}
fn set_profile(&self, profile: &str) {
(**self).set_profile(profile);
}
fn reflect(&self, context: DiscoveryContext<'_>) -> Result<HashMap<String, crate::Secret>> {
(**self).reflect(context)
}
fn get_many(&self, requests: &[(&str, Address<'_>)]) -> Result<HashMap<String, SecretString>> {
(**self).get_many(requests)
}
}