1use serde::{Deserialize, Serialize};
9use zeroize::Zeroize;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(rename_all = "snake_case", tag = "shape")]
14pub enum InjectionShape {
15 EnvVar { name: String },
17 Header { name: String },
19 Arg { position: usize },
21}
22
23#[derive(Clone, Zeroize)]
25#[zeroize(drop)]
26pub struct CredentialValue(String);
27
28impl CredentialValue {
29 pub fn new(value: String) -> Self {
30 Self(value)
31 }
32
33 pub fn as_str(&self) -> &str {
34 &self.0
35 }
36
37 pub fn len(&self) -> usize {
38 self.0.len()
39 }
40
41 pub fn is_empty(&self) -> bool {
42 self.0.is_empty()
43 }
44}
45
46impl std::fmt::Debug for CredentialValue {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 write!(f, "CredentialValue(<redacted, {} bytes>)", self.0.len())
49 }
50}
51
52impl From<String> for CredentialValue {
53 fn from(value: String) -> Self {
54 Self(value)
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct InjectedCredential {
61 pub shape: InjectionShape,
62 pub value: CredentialValue,
63}
64
65impl InjectedCredential {
66 pub fn env(name: impl Into<String>, value: impl Into<CredentialValue>) -> Self {
67 Self {
68 shape: InjectionShape::EnvVar { name: name.into() },
69 value: value.into(),
70 }
71 }
72
73 pub fn header(name: impl Into<String>, value: impl Into<CredentialValue>) -> Self {
74 Self {
75 shape: InjectionShape::Header { name: name.into() },
76 value: value.into(),
77 }
78 }
79
80 pub fn arg(position: usize, value: impl Into<CredentialValue>) -> Self {
81 Self {
82 shape: InjectionShape::Arg { position },
83 value: value.into(),
84 }
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use proptest::prelude::*;
92
93 fn injection_shape() -> impl Strategy<Value = InjectionShape> {
94 prop_oneof![
95 ".*".prop_map(|name| InjectionShape::EnvVar { name }),
96 ".*".prop_map(|name| InjectionShape::Header { name }),
97 any::<usize>().prop_map(|position| InjectionShape::Arg { position }),
98 ]
99 }
100
101 proptest! {
102 #![proptest_config(ProptestConfig { cases: 512, .. ProptestConfig::default() })]
103
104 #[test]
108 fn debug_never_prints_secret(s in ".*") {
109 let cred = CredentialValue::new(s.clone());
110 prop_assert_eq!(
111 format!("{cred:?}"),
112 format!("CredentialValue(<redacted, {} bytes>)", s.len())
113 );
114 prop_assert_eq!(cred.len(), s.len());
116 prop_assert_eq!(cred.is_empty(), s.is_empty());
117 }
118
119 #[test]
124 fn injection_shape_json_round_trips(shape in injection_shape()) {
125 let json = serde_json::to_string(&shape).unwrap();
126 let back: InjectionShape = serde_json::from_str(&json).unwrap();
127 prop_assert_eq!(back, shape);
128 }
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
134pub struct CredentialMetadata {
135 pub tool: String,
136 pub key: String,
137 pub backend: String, pub created_at: String,
139 pub last_used_at: Option<String>,
140 pub shape: InjectionShape,
141}