use std::collections::HashMap;
use zeroize::Zeroizing;
use crate::audit::{AuditAction, AuditEvent, AuditSink};
use crate::clock::Clock;
use crate::coordinate::{Coordinate, EnvSegment, KeyHalf};
use crate::env_source::EnvSource;
use crate::envrefs::{EnvRefs, Source};
use crate::error::CoreError;
use crate::keyring::Keyring;
use crate::policy::prod_forbids_fallback;
use crate::provider::{SecretProvider, reference_scheme};
use crate::record::SecretRecord;
use crate::registry::{Registry, Resolution, VaultOrigin};
use crate::scope::Origin;
use crate::secret::SecretValue;
use crate::sensitivity::Sensitivity;
#[derive(Debug)]
pub struct ResolvedVar {
pub name: String,
pub value: SecretValue,
pub sensitivity: Option<Sensitivity>,
pub environment: String,
pub coordinate: Option<String>,
pub origin: Option<VaultOrigin>,
pub reference: Option<String>,
}
impl ResolvedVar {
fn plain(
name: &str,
value: SecretValue,
environment: String,
coordinate: Option<String>,
) -> Self {
Self {
name: name.to_string(),
value,
sensitivity: None,
environment,
coordinate,
origin: None,
reference: None,
}
}
}
#[derive(Debug)]
pub struct Resolved {
pub vars: Vec<ResolvedVar>,
}
#[allow(clippy::too_many_arguments)]
pub fn resolve(
refs: &EnvRefs,
env: &str,
registry: &Registry,
keyring: &dyn Keyring,
env_source: &dyn EnvSource,
provider: &dyn SecretProvider,
audit: &dyn AuditSink,
clock: &dyn Clock,
origin: Origin,
project_override: Option<&str>,
) -> Result<Resolved, CoreError> {
let project = project_override.or(refs.project.as_deref());
let master = keyring.get_master_key()?;
let mut cache: HashMap<String, Zeroizing<Vec<u8>>> = HashMap::new();
let mut out = Vec::with_capacity(refs.vars.len());
for (name, source) in &refs.vars {
let resolved = match source {
Source::Literal(v) => {
ResolvedVar::plain(name, SecretValue::from(v.as_str()), env.to_string(), None)
}
Source::EnvPassthrough { var, fallback } => {
let value = env_source
.get(var)
.or_else(|| fallback.clone())
.ok_or_else(|| {
CoreError::EnvRefs(format!(
"`{name}`: env var `{var}` is unset and has no fallback"
))
})?;
ResolvedVar::plain(name, SecretValue::from(value), env.to_string(), None)
}
Source::Uri { uri, fallback } => {
let coord = uri.parse::<Coordinate>()?.with_env(env);
let coord_env = literal_env(&coord, name)?;
if prod_forbids_fallback(&coord_env) && fallback.is_some() {
return Err(CoreError::EnvRefs(format!(
"`{name}`: a `| fallback` is forbidden for prod (I4c)"
)));
}
let coordinate = coord.canonical_path()?;
match registry.resolve_with_key(&coord, project, master.expose())? {
Resolution::Found {
record,
origin: vault_origin,
} => materialize_found(
name,
record,
coord_env,
coordinate,
vault_origin,
coord.half,
provider,
&mut cache,
audit,
clock,
origin,
)?,
Resolution::NotFound => match fallback {
Some(fb) => ResolvedVar::plain(
name,
SecretValue::from(fb.as_str()),
coord_env,
Some(coordinate),
),
None => {
return Err(CoreError::EnvRefs(format!(
"`{name}`: coordinate `{coord}` did not resolve and has no fallback"
)));
}
},
}
}
};
out.push(resolved);
}
Ok(Resolved { vars: out })
}
#[allow(clippy::too_many_arguments)]
fn materialize_found(
name: &str,
record: SecretRecord,
environment: String,
coordinate: String,
origin: VaultOrigin,
half: KeyHalf,
provider: &dyn SecretProvider,
cache: &mut HashMap<String, Zeroizing<Vec<u8>>>,
audit: &dyn AuditSink,
clock: &dyn Clock,
run_origin: Origin,
) -> Result<ResolvedVar, CoreError> {
match record {
SecretRecord::Literal {
value, sensitivity, ..
} => Ok(ResolvedVar {
name: name.to_string(),
value,
sensitivity: Some(sensitivity),
environment,
coordinate: Some(coordinate),
origin: Some(origin),
reference: None,
}),
SecretRecord::Reference {
reference,
sensitivity,
..
} => {
let bytes = match cache.get(&reference) {
Some(b) => b.clone(),
None => {
let scheme = reference_scheme(&reference).unwrap_or("unknown");
let _ = audit.record(
&AuditEvent::new(
clock,
AuditAction::ProviderInvocation,
format!("scheme:{scheme}"),
)
.at(&coordinate, &environment)
.by(run_origin),
);
let materialized = provider.materialize(&reference)?;
let b = Zeroizing::new(materialized.expose().to_vec());
cache.insert(reference.clone(), b.clone());
b
}
};
Ok(ResolvedVar {
name: name.to_string(),
value: SecretValue::new(bytes.to_vec()),
sensitivity: Some(sensitivity),
environment,
coordinate: Some(coordinate),
origin: Some(origin),
reference: Some(reference),
})
}
SecretRecord::Keypair {
private,
public,
sensitivity,
..
} => {
match half {
KeyHalf::Private => {
let private = private.ok_or_else(|| {
CoreError::EnvRefs(format!(
"`{name}`: coordinate selects `#private` but this is a public-only keypair"
))
})?;
Ok(ResolvedVar {
name: name.to_string(),
value: private,
sensitivity: Some(sensitivity),
environment,
coordinate: Some(coordinate),
origin: Some(origin),
reference: None,
})
}
KeyHalf::Public | KeyHalf::Unspecified => {
Ok(ResolvedVar::plain(
name,
SecretValue::from(public.as_str()),
environment,
None,
))
}
}
}
SecretRecord::Totp { .. } => {
Err(CoreError::EnvRefs(format!(
"`{name}`: coordinate `{coordinate}` is a TOTP enrollment — its code is time-varying and single-use, so it cannot be injected via `.env.refs`; produce one on demand with `kovra code`"
)))
}
}
}
fn literal_env(coord: &Coordinate, name: &str) -> Result<String, CoreError> {
match &coord.environment {
EnvSegment::Literal(e) if !e.is_empty() => Ok(e.clone()),
_ => Err(CoreError::EnvRefs(format!(
"`{name}`: a `${{ENV}}` coordinate needs a non-empty --env"
))),
}
}