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 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(¤t),
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;