Skip to main content

cloud_sdk_reqwest/shared/
credentials.rs

1use core::fmt;
2use std::sync::{Arc, RwLock};
3
4use cloud_sdk::authentication::{
5    CredentialGeneration, CredentialGenerationError, CredentialLifetime, CredentialLifetimeState,
6    CredentialTimestamp, RefreshHandoff,
7};
8use cloud_sdk_sanitization::SecretBuffer;
9
10use super::BearerToken;
11
12mod error;
13
14pub use error::{
15    CredentialStateError, CredentialUpdateError, RefreshHandoffError, TokenRefreshError,
16    TokenRotationError,
17};
18
19struct VersionedToken {
20    generation: CredentialGeneration,
21    token: BearerToken,
22    lifetime: Option<CredentialLifetime>,
23}
24
25struct CredentialLineage;
26
27/// Store-bound handoff captured before external bearer refresh work.
28///
29/// The lineage is opaque and redacted. A handoff can update only the exact
30/// credential lifecycle whose snapshot created it.
31#[derive(Clone)]
32pub struct BearerRefreshHandoff {
33    lineage: Arc<CredentialLineage>,
34    expected: RefreshHandoff,
35}
36
37impl BearerRefreshHandoff {
38    /// Returns the generation that must still be current.
39    #[must_use]
40    pub fn expected_generation(&self) -> CredentialGeneration {
41        self.expected.expected_generation()
42    }
43}
44
45impl fmt::Debug for BearerRefreshHandoff {
46    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47        formatter
48            .debug_struct("BearerRefreshHandoff")
49            .field("generation", &self.expected_generation())
50            .field("lineage", &"[redacted]")
51            .finish()
52    }
53}
54
55/// Redacted snapshot metadata for an in-flight credential generation.
56///
57/// The internal token remains alive until this snapshot and all transport
58/// copies are dropped, but no secret bytes are exposed through this API.
59pub struct BearerCredentialSnapshot {
60    lineage: Arc<CredentialLineage>,
61    current: Arc<VersionedToken>,
62}
63
64impl BearerCredentialSnapshot {
65    /// Returns the immutable generation captured by this snapshot.
66    #[must_use]
67    pub fn generation(&self) -> CredentialGeneration {
68        self.current.generation
69    }
70
71    /// Creates a refresh handoff tied to this exact snapshot generation.
72    pub fn refresh_handoff(&self) -> Result<BearerRefreshHandoff, RefreshHandoffError> {
73        if self.current.lifetime.is_some() {
74            return Err(RefreshHandoffError::ExplicitTimeRequired);
75        }
76        Ok(self.new_handoff())
77    }
78
79    /// Creates a refresh handoff only inside an expiring token's refresh window.
80    pub fn refresh_handoff_at(
81        &self,
82        now: CredentialTimestamp,
83    ) -> Result<BearerRefreshHandoff, RefreshHandoffError> {
84        let lifetime = self
85            .current
86            .lifetime
87            .ok_or(RefreshHandoffError::LifetimeNotConfigured)?;
88        match lifetime.state_at(now) {
89            CredentialLifetimeState::ClockRollback => Err(RefreshHandoffError::ClockRollback),
90            CredentialLifetimeState::Fresh => Err(RefreshHandoffError::RefreshNotRequired),
91            CredentialLifetimeState::RefreshRequired => Ok(self.new_handoff()),
92            CredentialLifetimeState::Expired => Err(RefreshHandoffError::CredentialExpired),
93        }
94    }
95
96    /// Returns the caller-clock lifetime for an expiring credential.
97    #[must_use]
98    pub fn lifetime(&self) -> Option<CredentialLifetime> {
99        self.current.lifetime
100    }
101
102    fn new_handoff(&self) -> BearerRefreshHandoff {
103        BearerRefreshHandoff {
104            lineage: Arc::clone(&self.lineage),
105            expected: self.generation().refresh_handoff(),
106        }
107    }
108
109    pub(crate) fn header_value(&self) -> Result<reqwest::header::HeaderValue, ()> {
110        self.current.token.header_value()
111    }
112
113    #[cfg(test)]
114    pub(crate) fn header_value_with_drop_probe(&self) -> Result<reqwest::header::HeaderValue, ()> {
115        self.current.token.header_value_with_drop_probe()
116    }
117
118    #[cfg(test)]
119    pub(crate) fn owned_bytes(&self) -> &[u8] {
120        self.current.token.owned_bytes()
121    }
122}
123
124impl Clone for BearerCredentialSnapshot {
125    fn clone(&self) -> Self {
126        Self {
127            lineage: Arc::clone(&self.lineage),
128            current: Arc::clone(&self.current),
129        }
130    }
131}
132
133impl fmt::Debug for BearerCredentialSnapshot {
134    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135        formatter
136            .debug_struct("BearerCredentialSnapshot")
137            .field("generation", &self.generation())
138            .field("credential", &"[redacted]")
139            .finish()
140    }
141}
142
143pub(crate) struct CredentialStore {
144    lineage: Arc<CredentialLineage>,
145    current: RwLock<Arc<VersionedToken>>,
146}
147
148impl CredentialStore {
149    pub(crate) fn new(token: BearerToken, lifetime: Option<CredentialLifetime>) -> Self {
150        Self {
151            lineage: Arc::new(CredentialLineage),
152            current: RwLock::new(Arc::new(VersionedToken {
153                generation: CredentialGeneration::INITIAL,
154                token,
155                lifetime,
156            })),
157        }
158    }
159
160    pub(crate) fn snapshot(&self) -> Result<BearerCredentialSnapshot, CredentialStateError> {
161        let current = match self.current.read() {
162            Ok(current) => current,
163            Err(poisoned) => {
164                self.current.clear_poison();
165                poisoned.into_inner()
166            }
167        };
168        Ok(BearerCredentialSnapshot {
169            lineage: Arc::clone(&self.lineage),
170            current: Arc::clone(&current),
171        })
172    }
173
174    pub(crate) fn rotate(
175        &self,
176        token: BearerToken,
177    ) -> Result<CredentialGeneration, CredentialUpdateError> {
178        let retired = {
179            let mut current = self.write_current();
180            replace_current(&mut current, token, None).map_err(map_update_failure)?
181        };
182        let (retired, generation) = retired;
183        drop(retired);
184        Ok(generation)
185    }
186
187    pub(crate) fn rotate_with_lifetime(
188        &self,
189        token: BearerToken,
190        lifetime: CredentialLifetime,
191    ) -> Result<CredentialGeneration, CredentialUpdateError> {
192        let retired = {
193            let mut current = self.write_current();
194            replace_current(&mut current, token, Some(lifetime)).map_err(map_update_failure)?
195        };
196        let (retired, generation) = retired;
197        drop(retired);
198        Ok(generation)
199    }
200
201    pub(crate) fn refresh(
202        &self,
203        handoff: BearerRefreshHandoff,
204        token: BearerToken,
205    ) -> Result<CredentialGeneration, TokenRefreshError> {
206        if !Arc::ptr_eq(&self.lineage, &handoff.lineage) {
207            return Err(TokenRefreshError::CredentialMismatch);
208        }
209        let retired = {
210            let mut current = self.write_current();
211            if handoff.expected_generation() != current.generation {
212                return Err(TokenRefreshError::StaleGeneration);
213            }
214            replace_current(&mut current, token, None).map_err(map_refresh_update)?
215        };
216        let (retired, generation) = retired;
217        drop(retired);
218        Ok(generation)
219    }
220
221    pub(crate) fn refresh_with_lifetime(
222        &self,
223        handoff: BearerRefreshHandoff,
224        token: BearerToken,
225        lifetime: CredentialLifetime,
226    ) -> Result<CredentialGeneration, TokenRefreshError> {
227        if !Arc::ptr_eq(&self.lineage, &handoff.lineage) {
228            return Err(TokenRefreshError::CredentialMismatch);
229        }
230        let retired = {
231            let mut current = self.write_current();
232            if handoff.expected_generation() != current.generation {
233                return Err(TokenRefreshError::StaleGeneration);
234            }
235            replace_current(&mut current, token, Some(lifetime)).map_err(map_refresh_update)?
236        };
237        let (retired, generation) = retired;
238        drop(retired);
239        Ok(generation)
240    }
241
242    fn write_current(&self) -> std::sync::RwLockWriteGuard<'_, Arc<VersionedToken>> {
243        match self.current.write() {
244            Ok(current) => current,
245            Err(poisoned) => {
246                self.current.clear_poison();
247                poisoned.into_inner()
248            }
249        }
250    }
251
252    pub(crate) fn rotate_from_mut_bytes(
253        &self,
254        source: &mut [u8],
255    ) -> Result<CredentialGeneration, TokenRotationError> {
256        let token =
257            BearerToken::from_mut_bytes(source).map_err(TokenRotationError::TokenRejected)?;
258        self.rotate(token).map_err(map_rotation_update)
259    }
260
261    pub(crate) fn rotate_from_secret_buffer(
262        &self,
263        source: SecretBuffer<'_>,
264    ) -> Result<CredentialGeneration, TokenRotationError> {
265        let token =
266            BearerToken::from_secret_buffer(source).map_err(TokenRotationError::TokenRejected)?;
267        self.rotate(token).map_err(map_rotation_update)
268    }
269
270    pub(crate) fn rotate_from_mut_bytes_with_lifetime(
271        &self,
272        source: &mut [u8],
273        lifetime: CredentialLifetime,
274    ) -> Result<CredentialGeneration, TokenRotationError> {
275        let token =
276            BearerToken::from_mut_bytes(source).map_err(TokenRotationError::TokenRejected)?;
277        self.rotate_with_lifetime(token, lifetime)
278            .map_err(map_rotation_update)
279    }
280
281    pub(crate) fn rotate_from_secret_buffer_with_lifetime(
282        &self,
283        source: SecretBuffer<'_>,
284        lifetime: CredentialLifetime,
285    ) -> Result<CredentialGeneration, TokenRotationError> {
286        let token =
287            BearerToken::from_secret_buffer(source).map_err(TokenRotationError::TokenRejected)?;
288        self.rotate_with_lifetime(token, lifetime)
289            .map_err(map_rotation_update)
290    }
291
292    pub(crate) fn refresh_from_mut_bytes(
293        &self,
294        handoff: BearerRefreshHandoff,
295        source: &mut [u8],
296    ) -> Result<CredentialGeneration, TokenRefreshError> {
297        let token =
298            BearerToken::from_mut_bytes(source).map_err(TokenRefreshError::TokenRejected)?;
299        self.refresh(handoff, token)
300    }
301
302    pub(crate) fn refresh_from_secret_buffer(
303        &self,
304        handoff: BearerRefreshHandoff,
305        source: SecretBuffer<'_>,
306    ) -> Result<CredentialGeneration, TokenRefreshError> {
307        let token =
308            BearerToken::from_secret_buffer(source).map_err(TokenRefreshError::TokenRejected)?;
309        self.refresh(handoff, token)
310    }
311
312    pub(crate) fn refresh_from_mut_bytes_with_lifetime(
313        &self,
314        handoff: BearerRefreshHandoff,
315        source: &mut [u8],
316        lifetime: CredentialLifetime,
317    ) -> Result<CredentialGeneration, TokenRefreshError> {
318        let token =
319            BearerToken::from_mut_bytes(source).map_err(TokenRefreshError::TokenRejected)?;
320        self.refresh_with_lifetime(handoff, token, lifetime)
321    }
322
323    pub(crate) fn refresh_from_secret_buffer_with_lifetime(
324        &self,
325        handoff: BearerRefreshHandoff,
326        source: SecretBuffer<'_>,
327        lifetime: CredentialLifetime,
328    ) -> Result<CredentialGeneration, TokenRefreshError> {
329        let token =
330            BearerToken::from_secret_buffer(source).map_err(TokenRefreshError::TokenRejected)?;
331        self.refresh_with_lifetime(handoff, token, lifetime)
332    }
333}
334
335fn map_rotation_update(error: CredentialUpdateError) -> TokenRotationError {
336    match error {
337        CredentialUpdateError::StateUnavailable => TokenRotationError::StateUnavailable,
338        CredentialUpdateError::GenerationExhausted => TokenRotationError::GenerationExhausted,
339        CredentialUpdateError::LifetimeRequired => TokenRotationError::LifetimeRequired,
340        CredentialUpdateError::LifetimeForbidden => TokenRotationError::LifetimeForbidden,
341    }
342}
343
344impl fmt::Debug for CredentialStore {
345    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
346        formatter.write_str("CredentialStore([redacted])")
347    }
348}
349
350#[derive(Clone, Copy, Debug, Eq, PartialEq)]
351enum CredentialUpdateFailure {
352    GenerationExhausted,
353    LifetimeRequired,
354    LifetimeForbidden,
355}
356
357fn map_update_failure(error: CredentialUpdateFailure) -> CredentialUpdateError {
358    match error {
359        CredentialUpdateFailure::GenerationExhausted => CredentialUpdateError::GenerationExhausted,
360        CredentialUpdateFailure::LifetimeRequired => CredentialUpdateError::LifetimeRequired,
361        CredentialUpdateFailure::LifetimeForbidden => CredentialUpdateError::LifetimeForbidden,
362    }
363}
364
365fn map_refresh_update(error: CredentialUpdateFailure) -> TokenRefreshError {
366    match error {
367        CredentialUpdateFailure::GenerationExhausted => TokenRefreshError::GenerationExhausted,
368        CredentialUpdateFailure::LifetimeRequired => TokenRefreshError::LifetimeRequired,
369        CredentialUpdateFailure::LifetimeForbidden => TokenRefreshError::LifetimeForbidden,
370    }
371}
372
373fn replace_current(
374    current: &mut Arc<VersionedToken>,
375    token: BearerToken,
376    lifetime: Option<CredentialLifetime>,
377) -> Result<(Arc<VersionedToken>, CredentialGeneration), CredentialUpdateFailure> {
378    match (current.lifetime, lifetime) {
379        (Some(_), None) => return Err(CredentialUpdateFailure::LifetimeRequired),
380        (None, Some(_)) => return Err(CredentialUpdateFailure::LifetimeForbidden),
381        (Some(_), Some(_)) | (None, None) => {}
382    }
383    let generation =
384        current
385            .generation
386            .checked_next()
387            .map_err(|_error: CredentialGenerationError| {
388                CredentialUpdateFailure::GenerationExhausted
389            })?;
390    let replacement = Arc::new(VersionedToken {
391        generation,
392        token,
393        lifetime,
394    });
395    Ok((core::mem::replace(current, replacement), generation))
396}
397
398#[cfg(test)]
399mod tests;