use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case", tag = "shape")]
pub enum InjectionShape {
EnvVar { name: String },
Header { name: String },
Arg { position: usize },
}
#[derive(Clone, Zeroize)]
#[zeroize(drop)]
pub struct CredentialValue(String);
impl CredentialValue {
pub fn new(value: String) -> Self {
Self(value)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl std::fmt::Debug for CredentialValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CredentialValue(<redacted, {} bytes>)", self.0.len())
}
}
impl From<String> for CredentialValue {
fn from(value: String) -> Self {
Self(value)
}
}
#[derive(Debug, Clone)]
pub struct InjectedCredential {
pub shape: InjectionShape,
pub value: CredentialValue,
}
impl InjectedCredential {
pub fn env(name: impl Into<String>, value: impl Into<CredentialValue>) -> Self {
Self {
shape: InjectionShape::EnvVar { name: name.into() },
value: value.into(),
}
}
pub fn header(name: impl Into<String>, value: impl Into<CredentialValue>) -> Self {
Self {
shape: InjectionShape::Header { name: name.into() },
value: value.into(),
}
}
pub fn arg(position: usize, value: impl Into<CredentialValue>) -> Self {
Self {
shape: InjectionShape::Arg { position },
value: value.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
fn injection_shape() -> impl Strategy<Value = InjectionShape> {
prop_oneof![
".*".prop_map(|name| InjectionShape::EnvVar { name }),
".*".prop_map(|name| InjectionShape::Header { name }),
any::<usize>().prop_map(|position| InjectionShape::Arg { position }),
]
}
proptest! {
#![proptest_config(ProptestConfig { cases: 512, .. ProptestConfig::default() })]
#[test]
fn debug_never_prints_secret(s in ".*") {
let cred = CredentialValue::new(s.clone());
prop_assert_eq!(
format!("{cred:?}"),
format!("CredentialValue(<redacted, {} bytes>)", s.len())
);
prop_assert_eq!(cred.len(), s.len());
prop_assert_eq!(cred.is_empty(), s.is_empty());
}
#[test]
fn injection_shape_json_round_trips(shape in injection_shape()) {
let json = serde_json::to_string(&shape).unwrap();
let back: InjectionShape = serde_json::from_str(&json).unwrap();
prop_assert_eq!(back, shape);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CredentialMetadata {
pub tool: String,
pub key: String,
pub backend: String, pub created_at: String,
pub last_used_at: Option<String>,
pub shape: InjectionShape,
}