Skip to main content

cloud_sdk_reqwest/shared/
credentials.rs

1use core::fmt;
2use std::sync::{Arc, RwLock};
3
4use cloud_sdk_sanitization::SecretBuffer;
5
6use super::{BearerToken, BearerTokenError};
7
8/// Credential-state access failure.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum CredentialStateError {
11    /// The short-lived credential-state lock was poisoned by a panic.
12    Unavailable,
13}
14
15impl_static_error!(CredentialStateError,
16    Self::Unavailable => "credential state is unavailable",
17);
18
19/// Bearer-token validation or rotation failure.
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum TokenRotationError {
22    /// The replacement bearer token was rejected before state changed.
23    TokenRejected(BearerTokenError),
24    /// The credential state could not be read or changed.
25    StateUnavailable,
26}
27
28impl fmt::Display for TokenRotationError {
29    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30        formatter.write_str(match self {
31            Self::TokenRejected(_) => "replacement bearer token was rejected",
32            Self::StateUnavailable => "credential state is unavailable",
33        })
34    }
35}
36
37impl core::error::Error for TokenRotationError {
38    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
39        match self {
40            Self::TokenRejected(error) => Some(error),
41            Self::StateUnavailable => None,
42        }
43    }
44}
45
46pub(crate) struct CredentialStore {
47    current: RwLock<Arc<BearerToken>>,
48}
49
50impl CredentialStore {
51    pub(crate) fn new(token: BearerToken) -> Self {
52        Self {
53            current: RwLock::new(Arc::new(token)),
54        }
55    }
56
57    pub(crate) fn snapshot(&self) -> Result<Arc<BearerToken>, CredentialStateError> {
58        let current = match self.current.read() {
59            Ok(current) => current,
60            Err(poisoned) => {
61                self.current.clear_poison();
62                poisoned.into_inner()
63            }
64        };
65        Ok(Arc::clone(&current))
66    }
67
68    pub(crate) fn rotate(&self, token: BearerToken) -> Result<(), CredentialStateError> {
69        let replacement = Arc::new(token);
70        let retired = {
71            let mut current = match self.current.write() {
72                Ok(current) => current,
73                Err(poisoned) => {
74                    self.current.clear_poison();
75                    poisoned.into_inner()
76                }
77            };
78            core::mem::replace(&mut *current, replacement)
79        };
80        drop(retired);
81        Ok(())
82    }
83
84    pub(crate) fn rotate_from_mut_bytes(
85        &self,
86        source: &mut [u8],
87    ) -> Result<(), TokenRotationError> {
88        let token =
89            BearerToken::from_mut_bytes(source).map_err(TokenRotationError::TokenRejected)?;
90        self.rotate(token)
91            .map_err(|_| TokenRotationError::StateUnavailable)
92    }
93
94    pub(crate) fn rotate_from_secret_buffer(
95        &self,
96        source: SecretBuffer<'_>,
97    ) -> Result<(), TokenRotationError> {
98        let token =
99            BearerToken::from_secret_buffer(source).map_err(TokenRotationError::TokenRejected)?;
100        self.rotate(token)
101            .map_err(|_| TokenRotationError::StateUnavailable)
102    }
103}
104
105impl fmt::Debug for CredentialStore {
106    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
107        formatter.write_str("CredentialStore([redacted])")
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use std::boxed::Box;
114    use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
115    use std::sync::{
116        Arc,
117        atomic::{AtomicUsize, Ordering},
118    };
119
120    use cloud_sdk_sanitization::SecretBuffer;
121
122    use super::{BearerToken, CredentialStore, TokenRotationError};
123
124    #[test]
125    fn mutable_and_guarded_sources_clear_on_success_and_failure() {
126        let mut valid = *b"replacement";
127        let token = BearerToken::from_mut_bytes(&mut valid);
128        assert!(token.is_ok());
129        assert_eq!(valid, [0; 11]);
130
131        let mut invalid = *b"bad token";
132        assert!(BearerToken::from_mut_bytes(&mut invalid).is_err());
133        assert_eq!(invalid, [0; 9]);
134
135        let mut guarded = *b"guarded-token";
136        let token = BearerToken::from_secret_buffer(SecretBuffer::new(&mut guarded));
137        assert!(token.is_ok());
138        assert_eq!(guarded, [0; 13]);
139    }
140
141    #[test]
142    fn rejected_rotation_preserves_the_active_token_and_clears_input() {
143        let Ok(active) = BearerToken::new("active-token") else {
144            return;
145        };
146        let store = CredentialStore::new(active);
147        let mut rejected = *b"bad token";
148        assert!(matches!(
149            store.rotate_from_mut_bytes(&mut rejected),
150            Err(TokenRotationError::TokenRejected(_))
151        ));
152        assert_eq!(rejected, [0; 9]);
153        let snapshot = store.snapshot();
154        assert!(snapshot.is_ok());
155        if let Ok(snapshot) = snapshot {
156            assert_eq!(snapshot.owned_bytes(), b"Bearer active-token");
157        }
158    }
159
160    #[test]
161    fn retired_token_drops_only_after_the_last_in_flight_snapshot() {
162        let drops = Arc::new(AtomicUsize::new(0));
163        let active = BearerToken::with_drop_probe("old-token", Arc::clone(&drops));
164        let Ok(active) = active else { return };
165        let store = CredentialStore::new(active);
166        let old_snapshot = store.snapshot();
167        let Ok(old_snapshot) = old_snapshot else {
168            return;
169        };
170        let Ok(replacement) = BearerToken::new("new-token") else {
171            return;
172        };
173
174        assert!(store.rotate(replacement).is_ok());
175        assert_eq!(drops.load(Ordering::SeqCst), 0);
176        assert_eq!(old_snapshot.owned_bytes(), b"Bearer old-token");
177        let new_snapshot = store.snapshot();
178        assert!(new_snapshot.is_ok());
179        if let Ok(new_snapshot) = new_snapshot {
180            assert_eq!(new_snapshot.owned_bytes(), b"Bearer new-token");
181        }
182        drop(old_snapshot);
183        assert_eq!(drops.load(Ordering::SeqCst), 1);
184    }
185
186    #[test]
187    fn poisoned_state_recovers_for_snapshots_and_rotations() {
188        let Ok(active) = BearerToken::new("active-token") else {
189            return;
190        };
191        let store = CredentialStore::new(active);
192
193        poison_state(&store);
194        let snapshot = store.snapshot();
195        assert!(snapshot.is_ok());
196        assert!(!store.current.is_poisoned());
197        if let Ok(snapshot) = snapshot {
198            assert_eq!(snapshot.owned_bytes(), b"Bearer active-token");
199        }
200
201        poison_state(&store);
202        let Ok(replacement) = BearerToken::new("replacement-token") else {
203            return;
204        };
205        assert!(store.rotate(replacement).is_ok());
206        assert!(!store.current.is_poisoned());
207        let snapshot = store.snapshot();
208        assert!(snapshot.is_ok());
209        if let Ok(snapshot) = snapshot {
210            assert_eq!(snapshot.owned_bytes(), b"Bearer replacement-token");
211        }
212    }
213
214    fn poison_state(store: &CredentialStore) {
215        let result = catch_unwind(AssertUnwindSafe(|| {
216            let guard = store.current.write();
217            let Ok(_guard) = guard else { return };
218            resume_unwind(Box::new(()));
219        }));
220        assert!(result.is_err());
221        assert!(store.current.is_poisoned());
222    }
223}