use std::collections::HashMap;
use crate::compiled_spec::CompiledSecret;
use crate::composition::Template;
use crate::config::NativeAddress;
use crate::config::Secret;
use crate::config::SecretEncoding;
use crate::config::SecretExtract;
use crate::error::MonosecretError;
use crate::error::Result;
use crate::provider::OwnedAddress;
use crate::secrets::Secrets;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ResolvedPrimary {
pub spec: String,
pub uri: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ResolvedCache {
pub spec: String,
pub uri: String,
pub max_age_secs: u64,
pub route_fingerprint: String,
}
#[derive(Debug)]
pub(crate) struct Route {
pub primary: Option<ResolvedPrimary>,
pub fallback: Vec<String>,
pub cache: Option<ResolvedCache>,
}
impl Route {
pub(crate) fn primary(&self) -> Option<&str> {
self.primary.as_ref().map(|primary| primary.uri.as_str())
}
pub(crate) fn group_key(&self) -> Option<&str> {
self.primary.as_ref().map(|primary| primary.spec.as_str())
}
pub(crate) fn fallback_specs(&self) -> Option<&[String]> {
(!self.fallback.is_empty()).then_some(self.fallback.as_slice())
}
#[cfg(test)]
pub(crate) fn specs(&self) -> Option<Vec<String>> {
self.primary.as_ref().map(|primary| {
let mut specs = Vec::with_capacity(1 + self.fallback.len());
specs.push(primary.spec.clone());
specs.extend(self.fallback.iter().cloned());
specs
})
}
pub(crate) fn cache(&self) -> Option<&ResolvedCache> {
self.cache.as_ref()
}
}
#[derive(Debug)]
pub(crate) struct PlannedSecret {
pub name: String,
pub secret: CompiledSecret,
pub route: Option<Route>,
}
impl PlannedSecret {
pub(crate) fn config(&self) -> &Secret {
&self.secret.config
}
pub(crate) fn reference(&self) -> Option<&NativeAddress> {
self.secret.config.reference.as_ref()
}
pub(crate) fn cache_fingerprint(
&self,
cache: &ResolvedCache,
project: &str,
profile: &str,
) -> String {
let address = if let Some(reference) = self.reference() {
coordinate_fingerprint("legacy-ref", reference.coordinates())
} else if let Some(references) = &self.config().refs {
let mut entries = references.iter().collect::<Vec<_>>();
entries.sort_by_key(|(alias, _)| alias.as_str());
let mut fingerprints = vec!["scoped-refs".to_string()];
fingerprints.extend(entries.into_iter().map(|(alias, reference)| {
let coordinates = coordinate_fingerprint("native-ref", reference.coordinates());
stable_fingerprint(["scoped-ref", alias.as_str(), coordinates.as_str()])
}));
stable_fingerprint(fingerprints.iter().map(String::as_str))
} else {
"convention".to_string()
};
stable_fingerprint([
project,
profile,
cache.route_fingerprint.as_str(),
self.name.as_str(),
address.as_str(),
])
}
pub(crate) fn required(&self) -> bool {
self.secret.declared_required
}
pub(crate) fn as_path(&self) -> bool {
self.secret.config.as_path.unwrap_or(false)
}
pub(crate) fn encoding(&self) -> Option<SecretEncoding> {
self.secret.config.encoding
}
pub(crate) fn extract(&self) -> Option<&SecretExtract> {
self.secret.config.extract.as_ref()
}
pub(crate) fn is_composed(&self) -> bool {
self.secret.composition.is_some()
}
pub(crate) fn composition(&self) -> Option<&Template> {
self.secret.composition.as_ref()
}
}
#[derive(Debug)]
pub(crate) struct ResolutionPlan {
pub profile: String,
pub override_uri: Option<String>,
pub secrets: Vec<PlannedSecret>,
}
impl ResolutionPlan {
pub(crate) fn groups(&self) -> Vec<(Option<&str>, Vec<&PlannedSecret>)> {
let mut groups: Vec<(Option<&str>, Vec<&PlannedSecret>)> = Vec::new();
let mut group_index: HashMap<Option<&str>, usize> = HashMap::new();
for secret in &self.secrets {
let Some(route) = &secret.route else {
continue;
};
let primary = route.group_key();
if let Some(&idx) = group_index.get(&primary) {
groups
.get_mut(idx)
.expect("invariant: group index was recorded for an existing group")
.1
.push(secret);
} else {
group_index.insert(primary, groups.len());
groups.push((primary, vec![secret]));
}
}
groups
}
}
impl Secrets {
pub(crate) fn address_for_spec(
&self,
planned: &PlannedSecret,
provider_spec: Option<&str>,
project: &str,
profile: &str,
) -> Result<OwnedAddress> {
if let Some(reference) = planned.reference() {
return Ok(OwnedAddress::Native(reference.clone()));
}
let provider_reference = provider_spec.and_then(|spec| {
planned
.config()
.providers
.as_deref()
.and_then(|references| {
references
.iter()
.find(|reference| reference.provider_alias() == spec)
})
});
let detailed_address = provider_reference
.filter(|reference| matches!(reference, crate::config::ProviderRef::Detail(_)))
.and_then(|reference| {
crate::config::SecretRequest::from_provider_ref(reference)
.to_native_address(&planned.name)
});
if let (Some(spec), Some(reference)) = (provider_spec, provider_reference)
&& self.lookup_provider_alias_entry(spec).is_some()
{
let request = crate::config::SecretRequest::from_provider_ref(reference);
let provider = self.resolve_provider_spec(spec.to_string());
tracing::debug!(
target: "monosecret::secrets",
alias = %crate::audit::redact_uri_strict(spec),
provider = %crate::audit::redact_uri_strict(&provider),
path = ?request.path,
key = ?request.key,
"resolved provider reference"
);
}
let effective_spec = provider_spec
.map(str::to_string)
.or_else(|| self.configured_default_provider_spec());
if let Some(spec) = effective_spec.as_deref()
&& let Some(alias) = self.lookup_provider_alias_entry(spec)
&& !alias.is_cached()
{
if let Some(reference) = planned
.config()
.refs
.as_ref()
.and_then(|refs| refs.get(spec))
{
return Ok(OwnedAddress::Native(reference.clone()));
}
if let Some(address) = detailed_address.as_ref() {
return Ok(OwnedAddress::Native(address.clone()));
}
if let Some(template) = alias.reference_template() {
let reference =
template
.expand(project, profile, &planned.name)
.map_err(|error| {
MonosecretError::ProviderOperationFailed(format!(
"provider alias '{spec}' could not expand its ref template for '{}': {error}",
planned.name
))
})?;
return Ok(OwnedAddress::Native(reference));
}
}
if let Some(address) = detailed_address {
return Ok(OwnedAddress::Native(address));
}
Ok(OwnedAddress::convention(project, profile, &planned.name))
}
fn validate_scoped_refs(&self, name: &str, secret: &Secret) -> Result<()> {
let Some(references) = &secret.refs else {
return Ok(());
};
for alias_name in references.keys() {
let Some(alias) = self.lookup_provider_alias_entry(alias_name) else {
return Err(MonosecretError::ProviderNotFound(format!(
"Secret '{name}' defines `refs.{alias_name}`, but provider alias '{alias_name}' is not defined"
)));
};
if alias.is_cached() {
return Err(MonosecretError::ProviderOperationFailed(format!(
"Secret '{name}' defines `refs.{alias_name}`, but '{alias_name}' is a cached route; scoped refs must name leaf provider aliases"
)));
}
}
Ok(())
}
#[cfg(test)]
pub(crate) fn build_plan(&self, profile: Option<&str>) -> Result<ResolutionPlan> {
let profile_name = self.resolve_profile_name(profile);
let names = self.resolve_profile_secret_names(Some(&profile_name))?;
self.build_plan_from_names(profile_name, names)
}
pub(crate) fn build_plan_from_names(
&self,
profile_name: String,
names: Vec<String>,
) -> Result<ResolutionPlan> {
let override_spec = self.explicit_provider_spec(None);
let mut secrets = Vec::with_capacity(names.len());
let profile = self
.manifest
.profile(&profile_name)
.expect("profile names are validated before planning");
for name in names {
let secret = profile
.secrets
.get(&name)
.expect("planned names come from the compiled profile");
secrets.push(self.plan_one_secret(name, secret, override_spec.as_deref())?);
}
let override_uri = override_spec
.as_deref()
.map(|spec| self.override_display_uri(spec))
.transpose()?;
Ok(ResolutionPlan {
profile: profile_name,
override_uri,
secrets,
})
}
pub(crate) fn plan_secret(
&self,
name: &str,
profile_name: &str,
override_arg: Option<&str>,
) -> Result<Option<PlannedSecret>> {
let override_spec = self.explicit_provider_spec(override_arg);
self.plan_one(name, profile_name, override_spec.as_deref())
}
pub(crate) fn plan_declared_secret(
&self,
name: &str,
profile_name: &str,
) -> Result<Option<PlannedSecret>> {
self.plan_one(name, profile_name, None)
}
fn plan_one(
&self,
name: &str,
profile_name: &str,
override_spec: Option<&str>,
) -> Result<Option<PlannedSecret>> {
let Some(secret) = self
.manifest
.profile(profile_name)
.and_then(|profile| profile.secrets.get(name))
else {
return Ok(None);
};
Ok(Some(self.plan_one_secret(
name.to_string(),
secret,
override_spec,
)?))
}
fn plan_one_secret(
&self,
name: String,
secret: &CompiledSecret,
override_spec: Option<&str>,
) -> Result<PlannedSecret> {
self.validate_scoped_refs(&name, &secret.config)?;
let route = if secret.composition.is_some() {
None
} else {
Some(self.route_for(&secret.config, override_spec)?)
};
Ok(PlannedSecret {
name,
secret: secret.clone(),
route,
})
}
pub(crate) fn route_for(&self, config: &Secret, override_spec: Option<&str>) -> Result<Route> {
if let Some(spec) = override_spec {
if let Some(alias) = self.cached_alias(spec) {
return self.cached_route(spec, &alias);
}
self.validate_credential_sources(spec)?;
return Ok(Route {
primary: Some(ResolvedPrimary {
spec: spec.to_string(),
uri: self.resolve_provider_spec(spec.to_string()),
}),
fallback: Vec::new(),
cache: None,
});
}
if let Some([first, fallback @ ..]) = config.providers.as_deref() {
if let Some(spec) = std::iter::once(first)
.chain(fallback)
.find(|spec| self.cached_alias(spec).is_some())
{
if !fallback.is_empty() {
return Err(MonosecretError::ProviderOperationFailed(format!(
"cached provider alias '{spec}' is a complete route and cannot be \
combined with additional entries in a secret's providers list"
)));
}
let alias = self
.cached_alias(spec)
.expect("the spec was just found to be a cached alias");
return self.cached_route(spec, &alias);
}
let uri = self.resolve_one_provider(first)?;
self.validate_credential_sources(first)?;
Ok(Route {
primary: Some(ResolvedPrimary {
spec: first.provider_alias().to_string(),
uri,
}),
fallback: fallback
.iter()
.map(|reference| reference.provider_alias().to_string())
.collect(),
cache: None,
})
} else {
if let Some(spec) = self.configured_default_provider_spec()
&& let Some(alias) = self.cached_alias(&spec)
{
return self.cached_route(&spec, &alias);
}
Ok(Route {
primary: None,
fallback: Vec::new(),
cache: None,
})
}
}
fn cached_route(&self, name: &str, alias: &crate::config::ProviderAlias) -> Result<Route> {
let cache = alias.cache().ok_or_else(|| {
MonosecretError::ProviderOperationFailed(format!(
"provider alias '{name}' does not declare a cache policy"
))
})?;
if alias.authoritative_uri().is_some() && !alias.fallback().is_empty() {
return Err(MonosecretError::ProviderOperationFailed(format!(
"cached provider alias '{name}' cannot declare both uri and fallback"
)));
}
for spec in alias
.fallback()
.iter()
.map(String::as_str)
.chain(std::iter::once(cache.provider()))
{
if self.cached_alias(spec).is_some() {
return Err(MonosecretError::ProviderOperationFailed(format!(
"cached provider alias '{name}' cannot use cached alias '{spec}' as a \
fallback or cache provider; cached aliases may contain only leaf providers"
)));
}
}
let (source_specs, source_uris) = if let Some(uri) = alias.authoritative_uri() {
self.validate_credential_sources(name)?;
(
vec![name.to_string()],
vec![self.resolve_one_provider(uri)?],
)
} else {
let mut source_uris = Vec::with_capacity(alias.fallback().len());
for spec in alias.fallback() {
source_uris.push(self.resolve_one_provider(spec)?);
self.validate_credential_sources(spec)?;
}
(alias.fallback().to_vec(), source_uris)
};
let cache_uri = self.resolve_one_provider(cache.provider())?;
self.validate_credential_sources(cache.provider())?;
let source_identities = source_uris
.iter()
.map(|uri| self.canonical_storage_identity(uri))
.collect::<Result<Vec<_>>>()?;
let source_route_uris = source_uris
.iter()
.map(|uri| self.canonical_provider_uri(uri))
.collect::<Result<Vec<_>>>()?;
let source_route_fingerprints = alias
.fallback()
.iter()
.zip(&source_route_uris)
.map(|(spec, uri)| {
let template = self
.lookup_provider_alias_entry(spec)
.and_then(|alias| alias.reference_template().cloned())
.map_or_else(
|| "convention".to_string(),
|template| coordinate_fingerprint("ref-template", template.coordinates()),
);
stable_fingerprint(["source-route", spec, uri, template.as_str()])
})
.collect::<Vec<_>>();
let cache_identity = self.canonical_storage_identity(&cache_uri)?;
if let Some(shared) = source_identities
.iter()
.position(|identity| *identity == cache_identity)
{
return Err(MonosecretError::ProviderOperationFailed(format!(
"cached provider alias '{name}' caches into '{uri}', which is also its \
authoritative source '{source}'. A cache must be a distinct store, otherwise \
refreshing it overwrites the secret it caches.",
uri = crate::audit::redact_uri_strict(&cache_uri),
source = crate::audit::redact_uri_strict(alias.authoritative_uri().unwrap_or_else(
|| {
alias
.fallback()
.get(shared)
.expect("invariant: sources mirror the fallback list")
.as_str()
}
),),
)));
}
if !crate::provider::spec_provider_deletes(&cache_uri) {
return Err(MonosecretError::ProviderOperationFailed(format!(
"cached provider alias '{name}' caches into '{uri}', which cannot delete secrets, \
so its entries could never be invalidated. Cache into one of: {supported}.",
uri = crate::audit::redact_uri_strict(&cache_uri),
supported = crate::provider::deleting_provider_names().join(", "),
)));
}
let mut specs = source_specs.into_iter();
let first = specs.next().ok_or_else(|| {
MonosecretError::ProviderOperationFailed(format!(
"cached provider alias '{name}' requires at least one authoritative source"
))
})?;
let first_uri = source_uris
.first()
.expect("invariant: every authoritative source resolved a URI")
.clone();
Ok(Route {
primary: Some(ResolvedPrimary {
spec: first,
uri: first_uri,
}),
fallback: specs.collect(),
cache: Some(ResolvedCache {
spec: cache.provider().to_string(),
uri: cache_uri,
max_age_secs: cache.max_age_secs(),
route_fingerprint: stable_fingerprint(
source_route_fingerprints.iter().map(String::as_str),
),
}),
})
}
fn canonical_provider_uri(&self, uri: &str) -> Result<String> {
let mut provider =
crate::provider::provider_from_spec(uri, crate::provider::ProviderCredentials::new())?;
provider.with_base_dir(&self.config_dir);
Ok(provider.uri())
}
fn canonical_storage_identity(&self, uri: &str) -> Result<String> {
let mut provider =
crate::provider::provider_from_spec(uri, crate::provider::ProviderCredentials::new())?;
provider.with_base_dir(&self.config_dir);
Ok(provider.storage_identity())
}
pub(crate) fn override_display_uri(&self, spec: &str) -> Result<String> {
match self.cached_alias(spec) {
Some(alias) => {
if let Some(uri) = alias.authoritative_uri() {
return self.resolve_one_provider(uri);
}
let first = alias.fallback().first().ok_or_else(|| {
MonosecretError::ProviderOperationFailed(format!(
"cached provider alias '{spec}' requires at least one authoritative source"
))
})?;
self.resolve_one_provider(first)
}
None => Ok(self.resolve_provider_spec(spec.to_string())),
}
}
}
fn stable_fingerprint<'a>(parts: impl IntoIterator<Item = &'a str>) -> String {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for part in parts {
for byte in part.len().to_le_bytes().iter().chain(part.as_bytes()) {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0100_0000_01b3);
}
}
format!("v1-{hash:016x}")
}
fn coordinate_fingerprint<'a>(
kind: &'static str,
coordinates: impl IntoIterator<Item = (&'static str, Option<&'a str>)>,
) -> String {
let mut parts = vec![kind];
for (name, value) in coordinates {
parts.push(name);
match value {
Some(value) => {
parts.push("present");
parts.push(value);
}
None => parts.push("absent"),
}
}
stable_fingerprint(parts)
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::config::NativeAddressTemplate;
use crate::config::ProviderAlias;
use crate::config::ProviderCache;
use crate::config::ProviderConfig;
use crate::config::ProviderRef;
use crate::error::MonosecretError;
use crate::tests::global_config_with_aliases;
use crate::tests::scrub_resolution_env;
fn secret(providers: Option<Vec<&str>>) -> Secret {
Secret {
description: Some("a secret".to_string()),
providers: providers.map(|p| p.into_iter().map(ProviderRef::from).collect()),
..Default::default()
}
}
fn spec(
secrets: HashMap<String, Secret>,
provider: Option<&str>,
aliases: &[(&str, &str)],
) -> Secrets {
let config = crate::tests::resolve_test_config(secrets);
let global_config = (!aliases.is_empty()).then(|| global_config_with_aliases(aliases));
Secrets::new(config, global_config, provider.map(String::from), None)
}
fn plan(spec: &Secrets) -> ResolutionPlan {
spec.build_plan(None).unwrap()
}
fn find<'a>(plan: &'a ResolutionPlan, name: &str) -> &'a PlannedSecret {
plan.secrets
.iter()
.find(|s| s.name == name)
.expect("secret in plan")
}
fn route(planned: &PlannedSecret) -> &Route {
planned.route.as_ref().expect("a provider route")
}
fn group_names(plan: &ResolutionPlan) -> Vec<(Option<&str>, Vec<&str>)> {
plan.groups()
.into_iter()
.map(|(uri, group)| (uri, group.iter().map(|s| s.name.as_str()).collect()))
.collect()
}
#[test]
fn no_routing_plans_the_default_store() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([("DATABASE_URL".to_string(), secret(None))]);
let plan = plan(&spec(secrets, None, &[]));
let planned = find(&plan, "DATABASE_URL");
assert_eq!(route(planned).primary(), None);
assert_eq!(route(planned).fallback, Vec::<String>::new());
assert_eq!(group_names(&plan), vec![(None, vec!["DATABASE_URL"])]);
}
#[test]
fn composed_secrets_have_no_provider_route_or_fetch_group() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([
("PART".to_string(), secret(None)),
(
"RESULT".to_string(),
Secret {
description: Some("derived".to_string()),
composed: Some("prefix-${PART}".to_string()),
..Default::default()
},
),
]);
let plan = plan(&spec(secrets, Some("dotenv://.env.mock"), &[]));
assert!(find(&plan, "RESULT").is_composed());
assert!(find(&plan, "RESULT").route.is_none());
assert_eq!(
group_names(&plan),
vec![(Some("dotenv://.env.mock"), vec!["PART"])]
);
}
#[test]
fn override_collapses_the_chain_to_one_store() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([(
"API_KEY".to_string(),
secret(Some(vec!["onepassword://Production", "keyring://"])),
)]);
let mut spec = spec(secrets, None, &[]);
spec.set_provider("dotenv://.env.mock");
let plan = plan(&spec);
let planned = find(&plan, "API_KEY");
assert_eq!(route(planned).primary(), Some("dotenv://.env.mock"));
assert!(
route(planned).fallback.is_empty(),
"the override must collapse the chain: no fallback survives"
);
assert_eq!(plan.override_uri, Some("dotenv://.env.mock".to_string()));
}
#[test]
fn override_alias_keeps_the_raw_spec_as_build_key() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([("API_KEY".to_string(), secret(None))]);
let spec = spec(secrets, Some("mock"), &[("mock", "dotenv://.env.mock")]);
let plan = plan(&spec);
let planned = find(&plan, "API_KEY");
assert_eq!(route(planned).group_key(), Some("mock"));
assert_eq!(route(planned).primary(), Some("dotenv://.env.mock"));
assert_eq!(plan.override_uri, Some("dotenv://.env.mock".to_string()));
}
#[test]
fn providers_chain_resolves_the_primary_and_carries_the_fallback_raw() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([("API_KEY".to_string(), secret(Some(vec!["shared", "kr"])))]);
let plan = plan(&spec(
secrets,
None,
&[("shared", "onepassword://Shared"), ("kr", "keyring://")],
));
let planned = find(&plan, "API_KEY");
assert_eq!(route(planned).primary(), Some("onepassword://Shared"));
assert_eq!(route(planned).fallback, vec!["kr".to_string()]);
}
#[test]
fn an_undefined_fallback_alias_does_not_fail_the_plan() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([("API_KEY".to_string(), secret(Some(vec!["kr", "ghost"])))]);
let plan = plan(&spec(secrets, None, &[("kr", "keyring://")]));
let planned = find(&plan, "API_KEY");
assert_eq!(route(planned).primary(), Some("keyring://"));
assert_eq!(route(planned).fallback, vec!["ghost".to_string()]);
}
#[test]
fn inline_uri_in_chain_passes_through_without_an_alias() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([(
"API_KEY".to_string(),
secret(Some(vec!["onepassword://Production"])),
)]);
let plan = plan(&spec(secrets, None, &[]));
let planned = find(&plan, "API_KEY");
assert_eq!(route(planned).primary(), Some("onepassword://Production"));
}
#[test]
fn undefined_alias_fails_the_plan() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([("API_KEY".to_string(), secret(Some(vec!["nope"])))]);
let spec = spec(secrets, None, &[]);
let err = spec.build_plan(None).unwrap_err();
assert!(matches!(err, MonosecretError::ProviderNotFound(_)));
}
#[test]
fn bare_provider_name_in_chain_passes_through() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([("API_KEY".to_string(), secret(Some(vec!["keyring"])))]);
let plan = plan(&spec(secrets, None, &[]));
assert_eq!(route(find(&plan, "API_KEY")).primary(), Some("keyring"));
}
#[test]
fn a_ref_addresses_native_coordinates_convention_otherwise() {
let _env = scrub_resolution_env();
let mut referenced = secret(None);
referenced.reference = Some(NativeAddress {
item: "db".to_string(),
field: Some("password".to_string()),
..Default::default()
});
let secrets = HashMap::from([
("REFERENCED".to_string(), referenced),
("PLAIN".to_string(), secret(None)),
]);
let spec = spec(secrets, None, &[]);
let plan = plan(&spec);
let referenced = find(&plan, "REFERENCED");
let referenced_address = spec
.address_for_spec(
referenced,
referenced.route.as_ref().and_then(Route::group_key),
"proj",
"default",
)
.unwrap();
match referenced_address {
OwnedAddress::Native(native) => {
assert_eq!(native.item, "db");
assert_eq!(native.field.as_deref(), Some("password"));
}
OwnedAddress::Convention { .. } => {
panic!("a ref should address native coordinates")
}
}
let plain_secret = find(&plan, "PLAIN");
let plain_address = spec
.address_for_spec(
plain_secret,
plain_secret.route.as_ref().and_then(Route::group_key),
"proj",
"default",
)
.unwrap();
match plain_address {
OwnedAddress::Convention { key, .. } => assert_eq!(key, "PLAIN"),
OwnedAddress::Native(_) => panic!("no ref should address the naming convention"),
}
}
#[test]
fn endpoint_address_prefers_scoped_ref_then_alias_template_then_convention() {
let _env = scrub_resolution_env();
let mut configured = secret(Some(vec!["local"]));
configured.refs = Some(HashMap::from([(
"remote".to_string(),
NativeAddress {
item: "legacy-source".to_string(),
field: Some("token".to_string()),
..Default::default()
},
)]));
let mut config =
crate::tests::resolve_test_config(HashMap::from([("API_KEY".to_string(), configured)]));
config.providers = Some(provider_configs(HashMap::from([
(
"remote".to_string(),
ProviderAlias::from("onepassword://Production"),
),
(
"local".to_string(),
ProviderAlias::from("dotenv://.env")
.with_reference_template(NativeAddressTemplate {
item: "{project}_{profile}_{key}".to_string(),
..Default::default()
})
.unwrap(),
),
])));
let spec = Secrets::new(config, None, None, None);
let plan = spec.build_plan(None).unwrap();
let planned = find(&plan, "API_KEY");
assert_eq!(
spec.address_for_spec(planned, Some("remote"), "app", "prod")
.unwrap(),
OwnedAddress::Native(NativeAddress {
item: "legacy-source".to_string(),
field: Some("token".to_string()),
..Default::default()
})
);
assert_eq!(
spec.address_for_spec(planned, Some("local"), "app", "prod")
.unwrap(),
OwnedAddress::Native(NativeAddress {
item: "app_prod_API_KEY".to_string(),
..Default::default()
})
);
assert_eq!(
spec.address_for_spec(planned, Some("dotenv://other.env"), "app", "prod")
.unwrap(),
OwnedAddress::convention("app", "prod", "API_KEY")
);
}
#[test]
fn secrets_are_sorted_and_grouped_by_primary_store() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([
("B".to_string(), secret(Some(vec!["keyring://"]))),
("A".to_string(), secret(None)),
("C".to_string(), secret(Some(vec!["keyring://"]))),
]);
let plan = plan(&spec(secrets, None, &[]));
let names: Vec<&str> = plan.secrets.iter().map(|s| s.name.as_str()).collect();
assert_eq!(names, vec!["A", "B", "C"]);
assert_eq!(
group_names(&plan),
vec![(None, vec!["A"]), (Some("keyring://"), vec!["B", "C"])]
);
}
#[test]
fn distinct_aliases_sharing_a_uri_do_not_merge() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([
("A".to_string(), secret(Some(vec!["one"]))),
("B".to_string(), secret(Some(vec!["two"]))),
]);
let plan = plan(&spec(
secrets,
None,
&[("one", "keyring://"), ("two", "keyring://")],
));
assert_eq!(
group_names(&plan),
vec![(Some("one"), vec!["A"]), (Some("two"), vec!["B"])]
);
}
#[test]
fn plan_secret_is_none_for_an_undeclared_secret() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([("DECLARED".to_string(), secret(None))]);
let spec = spec(secrets, None, &[]);
assert!(
spec.plan_secret("NOPE", "default", None).unwrap().is_none(),
"an undeclared secret must not plan"
);
}
#[test]
fn plan_secret_matches_the_batch_plan_for_a_declared_secret() {
let _env = scrub_resolution_env();
let secrets = HashMap::from([(
"API_KEY".to_string(),
secret(Some(vec!["onepassword://Production", "keyring://"])),
)]);
let spec = spec(secrets, None, &[]);
let one = spec
.plan_secret("API_KEY", "default", None)
.unwrap()
.unwrap();
assert_eq!(route(&one).primary(), Some("onepassword://Production"));
assert_eq!(route(&one).fallback, vec!["keyring://".to_string()]);
let batch = plan(&spec);
assert_eq!(
route(&one).primary(),
route(find(&batch, "API_KEY")).primary()
);
}
fn cached_alias(sources: &[&str], cache_spec: &str, max_age: &str) -> ProviderAlias {
ProviderAlias::cached(
sources.iter().map(ToString::to_string).collect(),
ProviderCache::new(cache_spec, max_age).unwrap(),
)
.unwrap()
}
fn cached_aliases() -> HashMap<String, ProviderConfig> {
provider_configs(HashMap::from([
("azure".to_string(), ProviderAlias::from("akv://team-vault")),
("env".to_string(), ProviderAlias::from("env://")),
("local".to_string(), ProviderAlias::from("keyring://")),
(
"myprovider".to_string(),
cached_alias(&["azure", "env"], "local", "8h"),
),
]))
}
fn provider_configs(
aliases: HashMap<String, ProviderAlias>,
) -> HashMap<String, ProviderConfig> {
aliases
.into_iter()
.map(|(name, alias)| (name, alias.into()))
.collect()
}
fn cached_spec_with(secret_providers: Vec<&str>, aliases: &[(&str, ProviderAlias)]) -> Secrets {
let mut config = crate::tests::resolve_test_config(HashMap::from([(
"API_KEY".to_string(),
secret(Some(secret_providers)),
)]));
let mut providers = cached_aliases();
for (name, alias) in aliases {
providers.insert(name.to_string(), alias.clone().into());
}
config.providers = Some(providers);
Secrets::new(config, None, None, None)
}
fn cached_spec(secret_providers: Vec<&str>) -> Secrets {
cached_spec_with(secret_providers, &[])
}
#[test]
fn cached_alias_expands_into_authoritative_route_and_cache() {
let _env = scrub_resolution_env();
let plan = plan(&cached_spec(vec!["myprovider"]));
let route = route(find(&plan, "API_KEY"));
assert_eq!(route.group_key(), Some("azure"));
assert_eq!(route.primary(), Some("akv://team-vault"));
assert_eq!(route.fallback, ["env"]);
let cache = route.cache().expect("cached route");
assert_eq!(cache.spec, "local");
assert_eq!(cache.uri, "keyring://");
assert_eq!(cache.max_age_secs, 8 * 60 * 60);
}
#[test]
fn inline_cached_uri_keeps_alias_as_the_authoritative_build_key() {
let _env = scrub_resolution_env();
let inline = ProviderAlias::from("akv://team-vault")
.with_cache(ProviderCache::new("local", "5m").expect("valid cache policy"));
let spec = cached_spec_with(vec!["inline"], &[("inline", inline)]);
let plan = plan(&spec);
let route = route(find(&plan, "API_KEY"));
assert_eq!(route.group_key(), Some("inline"));
assert_eq!(route.primary(), Some("akv://team-vault"));
assert_eq!(route.fallback, Vec::<String>::new());
let cache = route.cache().expect("cached route");
assert_eq!(cache.spec, "local");
assert_eq!(cache.max_age_secs, 5 * 60);
}
#[test]
fn cached_alias_is_a_complete_route_not_a_chain_link() {
let _env = scrub_resolution_env();
for chain in [
vec!["myprovider", "env"],
vec!["env", "myprovider"],
vec!["env", "azure", "myprovider"],
] {
let error = cached_spec(chain.clone()).build_plan(None).unwrap_err();
assert!(
error.to_string().contains("complete route"),
"{chain:?}: {error}"
);
}
}
#[test]
fn a_cache_may_not_share_a_store_with_its_authoritative_sources() {
let _env = scrub_resolution_env();
let spec = cached_spec_with(
vec!["myprovider"],
&[
("mirror", ProviderAlias::from("keyring://")),
(
"myprovider",
cached_alias(&["azure", "mirror"], "local", "8h"),
),
],
);
let error = spec.build_plan(None).unwrap_err();
let message = error.to_string();
assert!(message.contains("distinct store"), "{message}");
assert!(message.contains("mirror"), "{message}");
}
#[test]
fn same_store_error_redacts_inline_authoritative_userinfo() {
let _env = scrub_resolution_env();
let source = "vault://team-user@127.0.0.1:8200/secret?tls=false";
let inline = ProviderAlias::from(source)
.with_cache(ProviderCache::new(source, "8h").expect("valid cache policy"));
let spec = cached_spec_with(vec!["route"], &[("route", inline)]);
let message = spec.build_plan(None).unwrap_err().to_string();
assert!(message.contains("distinct store"), "{message}");
assert!(
message.contains("vault://127.0.0.1:8200/secret"),
"{message}"
);
assert!(!message.contains("team-user"), "{message}");
assert!(!message.contains("tls=false"), "{message}");
}
#[test]
fn planning_refuses_an_inline_credential_without_echoing_it() {
let _env = scrub_resolution_env();
let source = "vault://team-user:super-sensitive-password@127.0.0.1:8200/secret?tls=false";
let inline = ProviderAlias::from(source)
.with_cache(ProviderCache::new(source, "8h").expect("valid cache policy"));
let spec = cached_spec_with(vec!["route"], &[("route", inline)]);
let message = spec.build_plan(None).unwrap_err().to_string();
assert!(message.contains("carries a password"), "{message}");
assert!(
message.contains("monosecret config provider login"),
"{message}"
);
assert!(!message.contains("super-sensitive-password"), "{message}");
}
#[test]
fn equivalent_provider_spellings_cannot_bypass_cache_separation() {
let _env = scrub_resolution_env();
let spec = cached_spec_with(
vec!["myprovider"],
&[(
"myprovider",
cached_alias(&["dotenv:.env"], "dotenv://.env", "8h"),
)],
);
let message = spec.build_plan(None).unwrap_err().to_string();
assert!(message.contains("distinct store"), "{message}");
assert!(message.contains("dotenv:.env"), "{message}");
}
#[test]
fn vault_compatible_spelling_and_auth_cannot_bypass_cache_separation() {
let _env = scrub_resolution_env();
for (source, cache) in [
(
"vault://127.0.0.1:8200/secret?tls=false",
"openbao://127.0.0.1:8200/secret?tls=false",
),
(
"vault://127.0.0.1:8200/secret?tls=false&auth=token",
"vault://127.0.0.1:8200/secret?tls=false&auth=approle",
),
(
"openbao://127.0.0.1:8200/secret?tls=false&auth=jwt&role=read&audience=one",
"openbao://127.0.0.1:8200/secret?tls=false&auth=jwt&role=deploy&audience=two",
),
(
"vault://127.0.0.1:8200/secret?tls=false&kv=1",
"openbao://127.0.0.1:8200/secret?tls=false&kv=2",
),
] {
let spec = cached_spec_with(
vec!["route"],
&[("route", cached_alias(&[source], cache, "8h"))],
);
let message = spec.build_plan(None).unwrap_err().to_string();
assert!(
message.contains("distinct store"),
"{source} / {cache}: {message}"
);
assert!(
message.contains(&crate::audit::redact_uri_strict(source)),
"{source} / {cache}: {message}"
);
}
}
#[test]
fn compatible_providers_may_cache_into_a_different_location() {
let _env = scrub_resolution_env();
for (source, cache) in [
(
"vault://team-a@bao.example.com:8200/secret",
"openbao://team-b@bao.example.com:8200/secret",
),
(
"vault://team-a@bao.example.com:8200/secret",
"openbao://team-a@bao.example.com:8200/cache",
),
(
"vault://team-a@bao.example.com:8200/secret",
"openbao://team-a@other.example.com:8200/secret",
),
] {
let spec = cached_spec_with(
vec!["route"],
&[("route", cached_alias(&[source], cache, "8h"))],
);
assert!(
spec.build_plan(None).is_ok(),
"{source} and {cache} are different stores"
);
}
}
fn fingerprint(source: &str) -> String {
let spec = cached_spec_with(
vec!["route"],
&[("route", cached_alias(&[source], "keyring://", "8h"))],
);
let plan = plan(&spec);
route(find(&plan, "API_KEY"))
.cache()
.expect("cached route")
.route_fingerprint
.clone()
}
#[test]
fn vault_compatible_route_configuration_changes_invalidate_the_cache() {
let _env = scrub_resolution_env();
let baseline = fingerprint("vault://bao.example.com:8200/secret");
for changed in [
"openbao://bao.example.com:8200/secret",
"vault://bao.example.com:8200/secret?auth=approle",
"vault://bao.example.com:8200/secret?auth=jwt&role=ci&audience=deploy",
"vault://bao.example.com:8200/secret?kv=1",
] {
assert_ne!(
baseline,
fingerprint(changed),
"route configuration change must invalidate the cache: {changed}"
);
}
assert_ne!(
fingerprint("vault://bao.example.com:8200/secret?auth=jwt&role=read"),
fingerprint("vault://bao.example.com:8200/secret?auth=jwt&role=deploy"),
"changing the JWT role must invalidate the cache"
);
}
#[test]
fn lastpass_item_template_changes_invalidate_the_cache() {
let _env = scrub_resolution_env();
assert_ne!(
fingerprint("lastpass://Work/TeamA/{project}/{profile}/{key}"),
fingerprint("lastpass://Work/TeamB/{project}/{profile}/{key}"),
"changing the item template must invalidate the cache"
);
}
fn template_fingerprint(template: NativeAddressTemplate) -> String {
let source = ProviderAlias::from("env://")
.with_reference_template(template)
.unwrap();
let spec = cached_spec_with(
vec!["route"],
&[
("source", source),
("route", cached_alias(&["source"], "keyring://", "8h")),
],
);
let plan = plan(&spec);
route(find(&plan, "API_KEY"))
.cache()
.expect("cached route")
.route_fingerprint
.clone()
}
#[test]
fn coordinate_boundaries_distinguish_alias_template_fingerprints() {
let _env = scrub_resolution_env();
let embedded_field = NativeAddressTemplate {
item: "foo field={key}".to_string(),
..Default::default()
};
let separate_field = NativeAddressTemplate {
item: "foo".to_string(),
field: Some("{key}".to_string()),
..Default::default()
};
assert_ne!(
template_fingerprint(embedded_field),
template_fingerprint(separate_field),
"different template coordinates must not collide through display rendering"
);
}
fn scoped_ref_cache_fingerprint(reference: NativeAddress) -> String {
let mut configured = secret(Some(vec!["route"]));
configured.refs = Some(HashMap::from([("source".to_string(), reference)]));
let mut config =
crate::tests::resolve_test_config(HashMap::from([("API_KEY".to_string(), configured)]));
config.providers = Some(provider_configs(HashMap::from([
("source".to_string(), ProviderAlias::from("env://")),
("local".to_string(), ProviderAlias::from("keyring://")),
(
"route".to_string(),
cached_alias(&["source"], "local", "8h"),
),
])));
let spec = Secrets::new(config, None, None, None);
let plan = plan(&spec);
let planned = find(&plan, "API_KEY");
planned.cache_fingerprint(
route(planned).cache().expect("cached route"),
&spec.config().project.name,
"default",
)
}
#[test]
fn coordinate_boundaries_distinguish_scoped_ref_fingerprints() {
let _env = scrub_resolution_env();
let embedded_field = NativeAddress {
item: "foo field=API_KEY".to_string(),
..Default::default()
};
let separate_field = NativeAddress {
item: "foo".to_string(),
field: Some("API_KEY".to_string()),
..Default::default()
};
assert_ne!(
scoped_ref_cache_fingerprint(embedded_field),
scoped_ref_cache_fingerprint(separate_field),
"different scoped-ref coordinates must not collide through display rendering"
);
}
#[test]
fn a_cache_must_be_a_store_that_can_delete() {
let _env = scrub_resolution_env();
let spec = cached_spec_with(
vec!["myprovider"],
&[("myprovider", cached_alias(&["azure"], "env://", "8h"))],
);
let message = spec.build_plan(None).unwrap_err().to_string();
assert!(message.contains("cannot delete secrets"), "{message}");
assert!(message.contains("keyring"), "{message}");
}
#[test]
fn a_cache_may_share_a_store_kind_with_a_different_address() {
let _env = scrub_resolution_env();
let spec = cached_spec_with(
vec!["myprovider"],
&[
(
"sibling",
ProviderAlias::from("keyring://monosecret/source/{project}/{profile}/{key}"),
),
("myprovider", cached_alias(&["sibling"], "local", "8h")),
],
);
let plan = plan(&spec);
assert!(route(find(&plan, "API_KEY")).cache().is_some());
}
#[test]
fn a_cached_override_reports_its_first_authoritative_store() {
let _env = scrub_resolution_env();
for secrets in [
HashMap::new(),
HashMap::from([("API_KEY".to_string(), secret(None))]),
] {
let mut config = crate::tests::resolve_test_config(secrets);
config.providers = Some(cached_aliases());
let mut spec = Secrets::new(config, None, None, None);
spec.set_provider("myprovider");
assert_eq!(
plan(&spec).override_uri,
Some("akv://team-vault".to_string())
);
}
}
#[test]
fn cached_alias_can_be_the_global_default_provider() {
let _env = scrub_resolution_env();
let mut config = crate::tests::resolve_test_config(HashMap::from([(
"API_KEY".to_string(),
secret(None),
)]));
config.providers = Some(cached_aliases());
let mut global = global_config_with_aliases(&[]);
global.defaults.provider = Some("myprovider".to_string());
let spec = Secrets::new(config, Some(global), None, None);
let plan = plan(&spec);
let route = route(find(&plan, "API_KEY"));
assert_eq!(route.group_key(), Some("azure"));
assert!(route.cache().is_some());
}
#[test]
fn cached_alias_can_be_an_explicit_provider_override() {
let _env = scrub_resolution_env();
let spec = cached_spec(vec!["env"]);
let planned = spec
.plan_secret("API_KEY", "default", Some("myprovider"))
.unwrap()
.unwrap();
let route = route(&planned);
assert_eq!(route.group_key(), Some("azure"));
assert!(route.cache().is_some());
}
#[test]
fn cached_aliases_cannot_be_nested() {
let _env = scrub_resolution_env();
let spec = cached_spec_with(
vec!["outer"],
&[("outer", cached_alias(&["myprovider"], "local", "1h"))],
);
let error = spec.build_plan(None).unwrap_err();
assert!(
error.to_string().contains("cannot use cached alias"),
"{error}"
);
}
}