cloud_sdk_reqwest/shared/
credentials.rs1use core::fmt;
2use std::sync::{Arc, RwLock};
3
4use cloud_sdk_sanitization::SecretBuffer;
5
6use super::{BearerToken, BearerTokenError};
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum CredentialStateError {
11 Unavailable,
13}
14
15impl_static_error!(CredentialStateError,
16 Self::Unavailable => "credential state is unavailable",
17);
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum TokenRotationError {
22 TokenRejected(BearerTokenError),
24 StateUnavailable,
26}
27
28impl fmt::Display for TokenRotationError {
29 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30 formatter.write_str(match self {
31 Self::TokenRejected(_) => "replacement bearer token was rejected",
32 Self::StateUnavailable => "credential state is unavailable",
33 })
34 }
35}
36
37impl core::error::Error for TokenRotationError {
38 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
39 match self {
40 Self::TokenRejected(error) => Some(error),
41 Self::StateUnavailable => None,
42 }
43 }
44}
45
46pub(crate) struct CredentialStore {
47 current: RwLock<Arc<BearerToken>>,
48}
49
50impl CredentialStore {
51 pub(crate) fn new(token: BearerToken) -> Self {
52 Self {
53 current: RwLock::new(Arc::new(token)),
54 }
55 }
56
57 pub(crate) fn snapshot(&self) -> Result<Arc<BearerToken>, CredentialStateError> {
58 let current = match self.current.read() {
59 Ok(current) => current,
60 Err(poisoned) => {
61 self.current.clear_poison();
62 poisoned.into_inner()
63 }
64 };
65 Ok(Arc::clone(¤t))
66 }
67
68 pub(crate) fn rotate(&self, token: BearerToken) -> Result<(), CredentialStateError> {
69 let replacement = Arc::new(token);
70 let retired = {
71 let mut current = match self.current.write() {
72 Ok(current) => current,
73 Err(poisoned) => {
74 self.current.clear_poison();
75 poisoned.into_inner()
76 }
77 };
78 core::mem::replace(&mut *current, replacement)
79 };
80 drop(retired);
81 Ok(())
82 }
83
84 pub(crate) fn rotate_from_mut_bytes(
85 &self,
86 source: &mut [u8],
87 ) -> Result<(), TokenRotationError> {
88 let token =
89 BearerToken::from_mut_bytes(source).map_err(TokenRotationError::TokenRejected)?;
90 self.rotate(token)
91 .map_err(|_| TokenRotationError::StateUnavailable)
92 }
93
94 pub(crate) fn rotate_from_secret_buffer(
95 &self,
96 source: SecretBuffer<'_>,
97 ) -> Result<(), TokenRotationError> {
98 let token =
99 BearerToken::from_secret_buffer(source).map_err(TokenRotationError::TokenRejected)?;
100 self.rotate(token)
101 .map_err(|_| TokenRotationError::StateUnavailable)
102 }
103}
104
105impl fmt::Debug for CredentialStore {
106 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
107 formatter.write_str("CredentialStore([redacted])")
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use std::boxed::Box;
114 use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
115 use std::sync::{
116 Arc,
117 atomic::{AtomicUsize, Ordering},
118 };
119
120 use cloud_sdk_sanitization::SecretBuffer;
121
122 use super::{BearerToken, CredentialStore, TokenRotationError};
123
124 #[test]
125 fn mutable_and_guarded_sources_clear_on_success_and_failure() {
126 let mut valid = *b"replacement";
127 let token = BearerToken::from_mut_bytes(&mut valid);
128 assert!(token.is_ok());
129 assert_eq!(valid, [0; 11]);
130
131 let mut invalid = *b"bad token";
132 assert!(BearerToken::from_mut_bytes(&mut invalid).is_err());
133 assert_eq!(invalid, [0; 9]);
134
135 let mut guarded = *b"guarded-token";
136 let token = BearerToken::from_secret_buffer(SecretBuffer::new(&mut guarded));
137 assert!(token.is_ok());
138 assert_eq!(guarded, [0; 13]);
139 }
140
141 #[test]
142 fn rejected_rotation_preserves_the_active_token_and_clears_input() {
143 let Ok(active) = BearerToken::new("active-token") else {
144 return;
145 };
146 let store = CredentialStore::new(active);
147 let mut rejected = *b"bad token";
148 assert!(matches!(
149 store.rotate_from_mut_bytes(&mut rejected),
150 Err(TokenRotationError::TokenRejected(_))
151 ));
152 assert_eq!(rejected, [0; 9]);
153 let snapshot = store.snapshot();
154 assert!(snapshot.is_ok());
155 if let Ok(snapshot) = snapshot {
156 assert_eq!(snapshot.owned_bytes(), b"Bearer active-token");
157 }
158 }
159
160 #[test]
161 fn retired_token_drops_only_after_the_last_in_flight_snapshot() {
162 let drops = Arc::new(AtomicUsize::new(0));
163 let active = BearerToken::with_drop_probe("old-token", Arc::clone(&drops));
164 let Ok(active) = active else { return };
165 let store = CredentialStore::new(active);
166 let old_snapshot = store.snapshot();
167 let Ok(old_snapshot) = old_snapshot else {
168 return;
169 };
170 let Ok(replacement) = BearerToken::new("new-token") else {
171 return;
172 };
173
174 assert!(store.rotate(replacement).is_ok());
175 assert_eq!(drops.load(Ordering::SeqCst), 0);
176 assert_eq!(old_snapshot.owned_bytes(), b"Bearer old-token");
177 let new_snapshot = store.snapshot();
178 assert!(new_snapshot.is_ok());
179 if let Ok(new_snapshot) = new_snapshot {
180 assert_eq!(new_snapshot.owned_bytes(), b"Bearer new-token");
181 }
182 drop(old_snapshot);
183 assert_eq!(drops.load(Ordering::SeqCst), 1);
184 }
185
186 #[test]
187 fn poisoned_state_recovers_for_snapshots_and_rotations() {
188 let Ok(active) = BearerToken::new("active-token") else {
189 return;
190 };
191 let store = CredentialStore::new(active);
192
193 poison_state(&store);
194 let snapshot = store.snapshot();
195 assert!(snapshot.is_ok());
196 assert!(!store.current.is_poisoned());
197 if let Ok(snapshot) = snapshot {
198 assert_eq!(snapshot.owned_bytes(), b"Bearer active-token");
199 }
200
201 poison_state(&store);
202 let Ok(replacement) = BearerToken::new("replacement-token") else {
203 return;
204 };
205 assert!(store.rotate(replacement).is_ok());
206 assert!(!store.current.is_poisoned());
207 let snapshot = store.snapshot();
208 assert!(snapshot.is_ok());
209 if let Ok(snapshot) = snapshot {
210 assert_eq!(snapshot.owned_bytes(), b"Bearer replacement-token");
211 }
212 }
213
214 fn poison_state(store: &CredentialStore) {
215 let result = catch_unwind(AssertUnwindSafe(|| {
216 let guard = store.current.write();
217 let Ok(_guard) = guard else { return };
218 resume_unwind(Box::new(()));
219 }));
220 assert!(result.is_err());
221 assert!(store.current.is_poisoned());
222 }
223}