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 owned_bytes(&self) -> &[u8] {
169        self.current.token.owned_bytes()
170    }
171}
172
173impl Clone for BearerCredentialSnapshot {
174    fn clone(&self) -> Self {
175        Self {
176            lineage: Arc::clone(&self.lineage),
177            current: Arc::clone(&self.current),
178        }
179    }
180}
181
182impl fmt::Debug for BearerCredentialSnapshot {
183    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
184        formatter
185            .debug_struct("BearerCredentialSnapshot")
186            .field("generation", &self.generation())
187            .field("credential", &"[redacted]")
188            .finish()
189    }
190}
191
192pub(crate) struct CredentialStore {
193    lineage: Arc<CredentialLineage>,
194    current: RwLock<Arc<VersionedToken>>,
195}
196
197impl CredentialStore {
198    pub(crate) fn new(token: BearerToken) -> Self {
199        Self {
200            lineage: Arc::new(CredentialLineage),
201            current: RwLock::new(Arc::new(VersionedToken {
202                generation: CredentialGeneration::INITIAL,
203                token,
204            })),
205        }
206    }
207
208    pub(crate) fn snapshot(&self) -> Result<BearerCredentialSnapshot, CredentialStateError> {
209        let current = match self.current.read() {
210            Ok(current) => current,
211            Err(poisoned) => {
212                self.current.clear_poison();
213                poisoned.into_inner()
214            }
215        };
216        Ok(BearerCredentialSnapshot {
217            lineage: Arc::clone(&self.lineage),
218            current: Arc::clone(&current),
219        })
220    }
221
222    pub(crate) fn rotate(
223        &self,
224        token: BearerToken,
225    ) -> Result<CredentialGeneration, CredentialUpdateError> {
226        let retired = {
227            let mut current = self.write_current();
228            replace_current(&mut current, token)
229                .map_err(|_| CredentialUpdateError::GenerationExhausted)?
230        };
231        let (retired, generation) = retired;
232        drop(retired);
233        Ok(generation)
234    }
235
236    pub(crate) fn refresh(
237        &self,
238        handoff: BearerRefreshHandoff,
239        token: BearerToken,
240    ) -> Result<CredentialGeneration, TokenRefreshError> {
241        if !Arc::ptr_eq(&self.lineage, &handoff.lineage) {
242            return Err(TokenRefreshError::CredentialMismatch);
243        }
244        let retired = {
245            let mut current = self.write_current();
246            if handoff.expected_generation() != current.generation {
247                return Err(TokenRefreshError::StaleGeneration);
248            }
249            replace_current(&mut current, token)
250                .map_err(|_| TokenRefreshError::GenerationExhausted)?
251        };
252        let (retired, generation) = retired;
253        drop(retired);
254        Ok(generation)
255    }
256
257    fn write_current(&self) -> std::sync::RwLockWriteGuard<'_, Arc<VersionedToken>> {
258        match self.current.write() {
259            Ok(current) => current,
260            Err(poisoned) => {
261                self.current.clear_poison();
262                poisoned.into_inner()
263            }
264        }
265    }
266
267    pub(crate) fn rotate_from_mut_bytes(
268        &self,
269        source: &mut [u8],
270    ) -> Result<CredentialGeneration, TokenRotationError> {
271        let token =
272            BearerToken::from_mut_bytes(source).map_err(TokenRotationError::TokenRejected)?;
273        self.rotate(token).map_err(map_rotation_update)
274    }
275
276    pub(crate) fn rotate_from_secret_buffer(
277        &self,
278        source: SecretBuffer<'_>,
279    ) -> Result<CredentialGeneration, TokenRotationError> {
280        let token =
281            BearerToken::from_secret_buffer(source).map_err(TokenRotationError::TokenRejected)?;
282        self.rotate(token).map_err(map_rotation_update)
283    }
284
285    pub(crate) fn refresh_from_mut_bytes(
286        &self,
287        handoff: BearerRefreshHandoff,
288        source: &mut [u8],
289    ) -> Result<CredentialGeneration, TokenRefreshError> {
290        let token =
291            BearerToken::from_mut_bytes(source).map_err(TokenRefreshError::TokenRejected)?;
292        self.refresh(handoff, token)
293    }
294
295    pub(crate) fn refresh_from_secret_buffer(
296        &self,
297        handoff: BearerRefreshHandoff,
298        source: SecretBuffer<'_>,
299    ) -> Result<CredentialGeneration, TokenRefreshError> {
300        let token =
301            BearerToken::from_secret_buffer(source).map_err(TokenRefreshError::TokenRejected)?;
302        self.refresh(handoff, token)
303    }
304}
305
306fn map_rotation_update(error: CredentialUpdateError) -> TokenRotationError {
307    match error {
308        CredentialUpdateError::StateUnavailable => TokenRotationError::StateUnavailable,
309        CredentialUpdateError::GenerationExhausted => TokenRotationError::GenerationExhausted,
310    }
311}
312
313impl fmt::Debug for CredentialStore {
314    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
315        formatter.write_str("CredentialStore([redacted])")
316    }
317}
318
319fn replace_current(
320    current: &mut Arc<VersionedToken>,
321    token: BearerToken,
322) -> Result<(Arc<VersionedToken>, CredentialGeneration), CredentialGenerationError> {
323    let generation = current.generation.checked_next()?;
324    let replacement = Arc::new(VersionedToken { generation, token });
325    Ok((core::mem::replace(current, replacement), generation))
326}
327
328#[cfg(test)]
329mod tests;