Skip to main content

cloud_sdk_reqwest/shared/
credentials.rs

1use 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/// Credential-state access failure.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum CredentialStateError {
12    /// The short-lived credential-state lock could not be recovered.
13    Unavailable,
14}
15
16impl_static_error!(CredentialStateError,
17    Self::Unavailable => "credential state is unavailable",
18);
19
20/// Validated token rotation failure.
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum CredentialUpdateError {
23    /// The credential state could not be changed.
24    StateUnavailable,
25    /// The monotonic credential generation cannot advance.
26    GenerationExhausted,
27}
28
29impl_static_error!(CredentialUpdateError,
30    Self::StateUnavailable => "credential state is unavailable",
31    Self::GenerationExhausted => "credential generation is exhausted",
32);
33
34/// Bearer-token validation or rotation failure.
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum TokenRotationError {
37    /// The replacement bearer token was rejected before state changed.
38    TokenRejected(BearerTokenError),
39    /// The credential state could not be changed.
40    StateUnavailable,
41    /// The monotonic credential generation cannot advance.
42    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/// Compare-and-swap bearer refresh failure.
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub enum TokenRefreshError {
67    /// The replacement bearer token was rejected before state changed.
68    TokenRejected(BearerTokenError),
69    /// A newer rotation or refresh superseded this handoff.
70    StaleGeneration,
71    /// The refresh handoff belongs to a different credential lifecycle.
72    CredentialMismatch,
73    /// The credential state could not be changed.
74    StateUnavailable,
75    /// The monotonic credential generation cannot advance.
76    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/// Store-bound handoff captured before external bearer refresh work.
111///
112/// The lineage is opaque and redacted. A handoff can update only the exact
113/// credential lifecycle whose snapshot created it.
114#[derive(Clone)]
115pub struct BearerRefreshHandoff {
116    lineage: Arc<CredentialLineage>,
117    expected: RefreshHandoff,
118}
119
120impl BearerRefreshHandoff {
121    /// Returns the generation that must still be current.
122    #[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
138/// Redacted snapshot metadata for an in-flight credential generation.
139///
140/// The internal token remains alive until this snapshot and all transport
141/// copies are dropped, but no secret bytes are exposed through this API.
142pub struct BearerCredentialSnapshot {
143    lineage: Arc<CredentialLineage>,
144    current: Arc<VersionedToken>,
145}
146
147impl BearerCredentialSnapshot {
148    /// Returns the immutable generation captured by this snapshot.
149    #[must_use]
150    pub fn generation(&self) -> CredentialGeneration {
151        self.current.generation
152    }
153
154    /// Creates a refresh handoff tied to this exact snapshot generation.
155    #[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(&current),
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;