cloud_sdk_reqwest/shared/
credentials.rs1use core::fmt;
2use std::sync::{Arc, RwLock};
3
4use cloud_sdk::authentication::{CredentialGeneration, CredentialGenerationError, RefreshHandoff};
5use cloud_sdk_sanitization::SecretBuffer;
6
7use super::{BearerToken, BearerTokenError};
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum CredentialStateError {
12 Unavailable,
14}
15
16impl_static_error!(CredentialStateError,
17 Self::Unavailable => "credential state is unavailable",
18);
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum CredentialUpdateError {
23 StateUnavailable,
25 GenerationExhausted,
27}
28
29impl_static_error!(CredentialUpdateError,
30 Self::StateUnavailable => "credential state is unavailable",
31 Self::GenerationExhausted => "credential generation is exhausted",
32);
33
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum TokenRotationError {
37 TokenRejected(BearerTokenError),
39 StateUnavailable,
41 GenerationExhausted,
43}
44
45impl fmt::Display for TokenRotationError {
46 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47 formatter.write_str(match self {
48 Self::TokenRejected(_) => "replacement bearer token was rejected",
49 Self::StateUnavailable => "credential state is unavailable",
50 Self::GenerationExhausted => "credential generation is exhausted",
51 })
52 }
53}
54
55impl core::error::Error for TokenRotationError {
56 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
57 match self {
58 Self::TokenRejected(error) => Some(error),
59 Self::StateUnavailable | Self::GenerationExhausted => None,
60 }
61 }
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub enum TokenRefreshError {
67 TokenRejected(BearerTokenError),
69 StaleGeneration,
71 CredentialMismatch,
73 StateUnavailable,
75 GenerationExhausted,
77}
78
79impl fmt::Display for TokenRefreshError {
80 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81 formatter.write_str(match self {
82 Self::TokenRejected(_) => "refreshed bearer token was rejected",
83 Self::StaleGeneration => "credential refresh generation is stale",
84 Self::CredentialMismatch => "credential refresh handoff belongs to another credential",
85 Self::StateUnavailable => "credential state is unavailable",
86 Self::GenerationExhausted => "credential generation is exhausted",
87 })
88 }
89}
90
91impl core::error::Error for TokenRefreshError {
92 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
93 match self {
94 Self::TokenRejected(error) => Some(error),
95 Self::StaleGeneration
96 | Self::CredentialMismatch
97 | Self::StateUnavailable
98 | Self::GenerationExhausted => None,
99 }
100 }
101}
102
103struct VersionedToken {
104 generation: CredentialGeneration,
105 token: BearerToken,
106}
107
108struct CredentialLineage;
109
110#[derive(Clone)]
115pub struct BearerRefreshHandoff {
116 lineage: Arc<CredentialLineage>,
117 expected: RefreshHandoff,
118}
119
120impl BearerRefreshHandoff {
121 #[must_use]
123 pub fn expected_generation(&self) -> CredentialGeneration {
124 self.expected.expected_generation()
125 }
126}
127
128impl fmt::Debug for BearerRefreshHandoff {
129 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130 formatter
131 .debug_struct("BearerRefreshHandoff")
132 .field("generation", &self.expected_generation())
133 .field("lineage", &"[redacted]")
134 .finish()
135 }
136}
137
138pub struct BearerCredentialSnapshot {
143 lineage: Arc<CredentialLineage>,
144 current: Arc<VersionedToken>,
145}
146
147impl BearerCredentialSnapshot {
148 #[must_use]
150 pub fn generation(&self) -> CredentialGeneration {
151 self.current.generation
152 }
153
154 #[must_use]
156 pub fn refresh_handoff(&self) -> BearerRefreshHandoff {
157 BearerRefreshHandoff {
158 lineage: Arc::clone(&self.lineage),
159 expected: self.generation().refresh_handoff(),
160 }
161 }
162
163 pub(crate) fn header_value(&self) -> Result<reqwest::header::HeaderValue, ()> {
164 self.current.token.header_value()
165 }
166
167 #[cfg(test)]
168 pub(crate) fn header_value_with_drop_probe(&self) -> Result<reqwest::header::HeaderValue, ()> {
169 self.current.token.header_value_with_drop_probe()
170 }
171
172 #[cfg(test)]
173 pub(crate) fn owned_bytes(&self) -> &[u8] {
174 self.current.token.owned_bytes()
175 }
176}
177
178impl Clone for BearerCredentialSnapshot {
179 fn clone(&self) -> Self {
180 Self {
181 lineage: Arc::clone(&self.lineage),
182 current: Arc::clone(&self.current),
183 }
184 }
185}
186
187impl fmt::Debug for BearerCredentialSnapshot {
188 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189 formatter
190 .debug_struct("BearerCredentialSnapshot")
191 .field("generation", &self.generation())
192 .field("credential", &"[redacted]")
193 .finish()
194 }
195}
196
197pub(crate) struct CredentialStore {
198 lineage: Arc<CredentialLineage>,
199 current: RwLock<Arc<VersionedToken>>,
200}
201
202impl CredentialStore {
203 pub(crate) fn new(token: BearerToken) -> Self {
204 Self {
205 lineage: Arc::new(CredentialLineage),
206 current: RwLock::new(Arc::new(VersionedToken {
207 generation: CredentialGeneration::INITIAL,
208 token,
209 })),
210 }
211 }
212
213 pub(crate) fn snapshot(&self) -> Result<BearerCredentialSnapshot, CredentialStateError> {
214 let current = match self.current.read() {
215 Ok(current) => current,
216 Err(poisoned) => {
217 self.current.clear_poison();
218 poisoned.into_inner()
219 }
220 };
221 Ok(BearerCredentialSnapshot {
222 lineage: Arc::clone(&self.lineage),
223 current: Arc::clone(¤t),
224 })
225 }
226
227 pub(crate) fn rotate(
228 &self,
229 token: BearerToken,
230 ) -> Result<CredentialGeneration, CredentialUpdateError> {
231 let retired = {
232 let mut current = self.write_current();
233 replace_current(&mut current, token)
234 .map_err(|_| CredentialUpdateError::GenerationExhausted)?
235 };
236 let (retired, generation) = retired;
237 drop(retired);
238 Ok(generation)
239 }
240
241 pub(crate) fn refresh(
242 &self,
243 handoff: BearerRefreshHandoff,
244 token: BearerToken,
245 ) -> Result<CredentialGeneration, TokenRefreshError> {
246 if !Arc::ptr_eq(&self.lineage, &handoff.lineage) {
247 return Err(TokenRefreshError::CredentialMismatch);
248 }
249 let retired = {
250 let mut current = self.write_current();
251 if handoff.expected_generation() != current.generation {
252 return Err(TokenRefreshError::StaleGeneration);
253 }
254 replace_current(&mut current, token)
255 .map_err(|_| TokenRefreshError::GenerationExhausted)?
256 };
257 let (retired, generation) = retired;
258 drop(retired);
259 Ok(generation)
260 }
261
262 fn write_current(&self) -> std::sync::RwLockWriteGuard<'_, Arc<VersionedToken>> {
263 match self.current.write() {
264 Ok(current) => current,
265 Err(poisoned) => {
266 self.current.clear_poison();
267 poisoned.into_inner()
268 }
269 }
270 }
271
272 pub(crate) fn rotate_from_mut_bytes(
273 &self,
274 source: &mut [u8],
275 ) -> Result<CredentialGeneration, TokenRotationError> {
276 let token =
277 BearerToken::from_mut_bytes(source).map_err(TokenRotationError::TokenRejected)?;
278 self.rotate(token).map_err(map_rotation_update)
279 }
280
281 pub(crate) fn rotate_from_secret_buffer(
282 &self,
283 source: SecretBuffer<'_>,
284 ) -> Result<CredentialGeneration, TokenRotationError> {
285 let token =
286 BearerToken::from_secret_buffer(source).map_err(TokenRotationError::TokenRejected)?;
287 self.rotate(token).map_err(map_rotation_update)
288 }
289
290 pub(crate) fn refresh_from_mut_bytes(
291 &self,
292 handoff: BearerRefreshHandoff,
293 source: &mut [u8],
294 ) -> Result<CredentialGeneration, TokenRefreshError> {
295 let token =
296 BearerToken::from_mut_bytes(source).map_err(TokenRefreshError::TokenRejected)?;
297 self.refresh(handoff, token)
298 }
299
300 pub(crate) fn refresh_from_secret_buffer(
301 &self,
302 handoff: BearerRefreshHandoff,
303 source: SecretBuffer<'_>,
304 ) -> Result<CredentialGeneration, TokenRefreshError> {
305 let token =
306 BearerToken::from_secret_buffer(source).map_err(TokenRefreshError::TokenRejected)?;
307 self.refresh(handoff, token)
308 }
309}
310
311fn map_rotation_update(error: CredentialUpdateError) -> TokenRotationError {
312 match error {
313 CredentialUpdateError::StateUnavailable => TokenRotationError::StateUnavailable,
314 CredentialUpdateError::GenerationExhausted => TokenRotationError::GenerationExhausted,
315 }
316}
317
318impl fmt::Debug for CredentialStore {
319 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
320 formatter.write_str("CredentialStore([redacted])")
321 }
322}
323
324fn replace_current(
325 current: &mut Arc<VersionedToken>,
326 token: BearerToken,
327) -> Result<(Arc<VersionedToken>, CredentialGeneration), CredentialGenerationError> {
328 let generation = current.generation.checked_next()?;
329 let replacement = Arc::new(VersionedToken { generation, token });
330 Ok((core::mem::replace(current, replacement), generation))
331}
332
333#[cfg(test)]
334mod tests;