Skip to main content

cloud_sdk/authentication/
attempt.rs

1use core::sync::atomic::{AtomicU32, Ordering};
2
3const REJECTED: u32 = 1;
4const GENERATION_SHIFT: u32 = 1;
5const MAX_GENERATION: u32 = u32::MAX >> GENERATION_SHIFT;
6
7/// Monotonic identity of one credential-attempt generation.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct CredentialAttemptGeneration(u32);
10
11impl CredentialAttemptGeneration {
12    /// Initial generation assigned to newly admitted credentials.
13    pub const INITIAL: Self = Self(1);
14
15    /// Returns the generation as a nonzero integer.
16    #[must_use]
17    pub const fn get(self) -> u32 {
18        self.0
19    }
20}
21
22/// Proof that one credential generation was open when execution began.
23///
24/// Owner identity is deliberately not hashable because exposing it to a
25/// caller-supplied hasher could disclose a process address.
26///
27/// ```compile_fail
28/// use cloud_sdk::authentication::CredentialAttempt;
29/// fn require_hash<T: core::hash::Hash>() {}
30/// require_hash::<CredentialAttempt<'static>>();
31/// ```
32#[derive(Clone, Copy)]
33pub struct CredentialAttempt<'a> {
34    owner: &'a SharedCredentialAttemptState,
35    generation: CredentialAttemptGeneration,
36}
37
38impl CredentialAttempt<'_> {
39    /// Returns the generation used by this attempt.
40    #[must_use]
41    pub const fn generation(self) -> CredentialAttemptGeneration {
42        self.generation
43    }
44}
45
46impl core::fmt::Debug for CredentialAttempt<'_> {
47    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
48        formatter
49            .debug_struct("CredentialAttempt")
50            .field("owner", &"[bound]")
51            .field("generation", &self.generation)
52            .finish()
53    }
54}
55
56impl PartialEq for CredentialAttempt<'_> {
57    fn eq(&self, other: &Self) -> bool {
58        core::ptr::eq(self.owner, other.owner) && self.generation == other.generation
59    }
60}
61
62impl Eq for CredentialAttempt<'_> {}
63
64/// Explicit caller acknowledgement for retrying unchanged credentials.
65///
66/// Constructing this token must be an operator-level decision. Automatic
67/// retry, pagination, polling, and client policy must not create it.
68#[derive(Debug)]
69pub struct CredentialReconfirmation {
70    _private: (),
71}
72
73impl CredentialReconfirmation {
74    /// Explicitly acknowledges reuse of the same credential material.
75    #[must_use]
76    pub const fn acknowledge_same_credentials() -> Self {
77        Self { _private: () }
78    }
79}
80
81/// Observable credential-attempt lifecycle state.
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum CredentialAttemptStatus {
84    /// The current generation may begin new executions.
85    Open,
86    /// Authentication rejection closed the current generation.
87    Rejected,
88}
89
90/// Credential-attempt lifecycle transition failure.
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub enum CredentialAttemptError {
93    /// The attempt belongs to another credential lifecycle.
94    ForeignState,
95    /// Authentication rejection closed the current generation.
96    GenerationRejected,
97    /// The supplied generation is no longer current.
98    StaleGeneration,
99    /// Explicit reconfirmation is valid only after authentication rejection.
100    ReconfirmationNotRequired,
101    /// The bounded monotonic generation cannot advance without wrapping.
102    GenerationExhausted,
103}
104
105impl_static_error!(CredentialAttemptError,
106    Self::ForeignState => "credential attempt belongs to another state",
107    Self::GenerationRejected => "credential attempt generation was rejected",
108    Self::StaleGeneration => "credential attempt generation is stale",
109    Self::ReconfirmationNotRequired => "credential attempt generation is still open",
110    Self::GenerationExhausted => "credential attempt generation is exhausted",
111);
112
113/// Caller-owned concurrent lockout state for one credential lifecycle.
114///
115/// Cloned clients share this object by reference. Multiple attempts may begin
116/// on one open generation, but the first authentication rejection closes that
117/// generation for every later execution. Only replacement credentials or an
118/// explicit [`CredentialReconfirmation`] advance to a new open generation.
119pub struct SharedCredentialAttemptState {
120    packed: AtomicU32,
121}
122
123impl SharedCredentialAttemptState {
124    /// Creates one open initial credential generation.
125    #[must_use]
126    pub const fn new() -> Self {
127        Self {
128            packed: AtomicU32::new(pack(CredentialAttemptGeneration::INITIAL, false)),
129        }
130    }
131
132    /// Returns the current generation and status.
133    #[must_use]
134    pub fn observe(&self) -> (CredentialAttemptGeneration, CredentialAttemptStatus) {
135        unpack(self.packed.load(Ordering::Acquire))
136    }
137
138    /// Begins execution only when the current generation remains open.
139    pub fn begin(&self) -> Result<CredentialAttempt<'_>, CredentialAttemptError> {
140        let (generation, status) = self.observe();
141        if status == CredentialAttemptStatus::Rejected {
142            return Err(CredentialAttemptError::GenerationRejected);
143        }
144        Ok(CredentialAttempt {
145            owner: self,
146            generation,
147        })
148    }
149
150    /// Revalidates an attempt immediately before credential use.
151    pub fn validate(&self, attempt: CredentialAttempt<'_>) -> Result<(), CredentialAttemptError> {
152        self.validate_owner(attempt)?;
153        self.validate_generation(attempt.generation)
154    }
155
156    pub(crate) fn validate_generation(
157        &self,
158        expected: CredentialAttemptGeneration,
159    ) -> Result<(), CredentialAttemptError> {
160        let (generation, status) = self.observe();
161        if generation != expected {
162            return Err(CredentialAttemptError::StaleGeneration);
163        }
164        if status == CredentialAttemptStatus::Rejected {
165            return Err(CredentialAttemptError::GenerationRejected);
166        }
167        Ok(())
168    }
169
170    /// Closes the exact generation that received authentication rejection.
171    ///
172    /// Repeated concurrent rejection reports for the same generation are
173    /// idempotent. A stale report cannot close replacement credentials.
174    pub fn reject(&self, attempt: CredentialAttempt<'_>) -> Result<(), CredentialAttemptError> {
175        self.validate_owner(attempt)?;
176        self.reject_generation(attempt.generation)
177    }
178
179    pub(crate) fn reject_generation(
180        &self,
181        expected: CredentialAttemptGeneration,
182    ) -> Result<(), CredentialAttemptError> {
183        loop {
184            let current = self.packed.load(Ordering::Acquire);
185            let (generation, status) = unpack(current);
186            if generation != expected {
187                return Err(CredentialAttemptError::StaleGeneration);
188            }
189            if status == CredentialAttemptStatus::Rejected {
190                return Ok(());
191            }
192            let next = pack(generation, true);
193            if self
194                .packed
195                .compare_exchange(current, next, Ordering::AcqRel, Ordering::Acquire)
196                .is_ok()
197            {
198                return Ok(());
199            }
200        }
201    }
202
203    /// Opens a new generation after replacement credentials were admitted.
204    pub fn replace(
205        &self,
206        expected: CredentialAttemptGeneration,
207    ) -> Result<CredentialAttemptGeneration, CredentialAttemptError> {
208        self.advance(expected)
209    }
210
211    /// Opens a new generation after explicit unchanged-credential confirmation.
212    pub fn reconfirm(
213        &self,
214        expected: CredentialAttemptGeneration,
215        _acknowledgement: CredentialReconfirmation,
216    ) -> Result<CredentialAttemptGeneration, CredentialAttemptError> {
217        loop {
218            let current = self.packed.load(Ordering::Acquire);
219            let (generation, status) = unpack(current);
220            if generation != expected {
221                return Err(CredentialAttemptError::StaleGeneration);
222            }
223            if status != CredentialAttemptStatus::Rejected {
224                return Err(CredentialAttemptError::ReconfirmationNotRequired);
225            }
226            let next_generation = checked_next(generation)?;
227            let next = pack(next_generation, false);
228            if self
229                .packed
230                .compare_exchange(current, next, Ordering::AcqRel, Ordering::Acquire)
231                .is_ok()
232            {
233                return Ok(next_generation);
234            }
235        }
236    }
237
238    fn advance(
239        &self,
240        expected: CredentialAttemptGeneration,
241    ) -> Result<CredentialAttemptGeneration, CredentialAttemptError> {
242        loop {
243            let current = self.packed.load(Ordering::Acquire);
244            let (generation, _) = unpack(current);
245            if generation != expected {
246                return Err(CredentialAttemptError::StaleGeneration);
247            }
248            let next_generation = checked_next(generation)?;
249            let next = pack(next_generation, false);
250            if self
251                .packed
252                .compare_exchange(current, next, Ordering::AcqRel, Ordering::Acquire)
253                .is_ok()
254            {
255                return Ok(next_generation);
256            }
257        }
258    }
259
260    fn validate_owner(&self, attempt: CredentialAttempt<'_>) -> Result<(), CredentialAttemptError> {
261        if !core::ptr::eq(self, attempt.owner) {
262            return Err(CredentialAttemptError::ForeignState);
263        }
264        Ok(())
265    }
266
267    #[cfg(test)]
268    fn set_generation_for_test(&mut self, generation: u32, rejected: bool) {
269        *self.packed.get_mut() = pack(CredentialAttemptGeneration(generation), rejected);
270    }
271}
272
273fn checked_next(
274    generation: CredentialAttemptGeneration,
275) -> Result<CredentialAttemptGeneration, CredentialAttemptError> {
276    generation
277        .0
278        .checked_add(1)
279        .filter(|candidate| *candidate <= MAX_GENERATION)
280        .map(CredentialAttemptGeneration)
281        .ok_or(CredentialAttemptError::GenerationExhausted)
282}
283
284impl Default for SharedCredentialAttemptState {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290impl core::fmt::Debug for SharedCredentialAttemptState {
291    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
292        let (generation, status) = self.observe();
293        formatter
294            .debug_struct("SharedCredentialAttemptState")
295            .field("generation", &generation)
296            .field("status", &status)
297            .finish()
298    }
299}
300
301const fn pack(generation: CredentialAttemptGeneration, rejected: bool) -> u32 {
302    (generation.0 << GENERATION_SHIFT) | (rejected as u32)
303}
304
305const fn unpack(value: u32) -> (CredentialAttemptGeneration, CredentialAttemptStatus) {
306    let generation = CredentialAttemptGeneration(value >> GENERATION_SHIFT);
307    let status = if value & REJECTED == 0 {
308        CredentialAttemptStatus::Open
309    } else {
310        CredentialAttemptStatus::Rejected
311    };
312    (generation, status)
313}
314
315#[cfg(test)]
316mod tests {
317    use super::{
318        CredentialAttemptError, CredentialAttemptGeneration, CredentialAttemptStatus,
319        CredentialReconfirmation, MAX_GENERATION, SharedCredentialAttemptState,
320    };
321
322    #[test]
323    fn rejection_closes_one_generation_until_replaced_or_reconfirmed() {
324        let state = SharedCredentialAttemptState::new();
325        let first = state
326            .begin()
327            .unwrap_or_else(|_| unreachable!("initial credential generation was closed"));
328        assert_eq!(first.generation(), CredentialAttemptGeneration::INITIAL);
329        assert_eq!(
330            state.reconfirm(
331                first.generation(),
332                CredentialReconfirmation::acknowledge_same_credentials(),
333            ),
334            Err(CredentialAttemptError::ReconfirmationNotRequired)
335        );
336        assert_eq!(state.reject(first), Ok(()));
337        assert_eq!(
338            state.validate(first),
339            Err(CredentialAttemptError::GenerationRejected)
340        );
341        assert_eq!(state.reject(first), Ok(()));
342        assert_eq!(
343            state.begin(),
344            Err(CredentialAttemptError::GenerationRejected)
345        );
346
347        let second = state
348            .reconfirm(
349                first.generation(),
350                CredentialReconfirmation::acknowledge_same_credentials(),
351            )
352            .unwrap_or_else(|_| unreachable!("explicit reconfirmation was rejected"));
353        assert_eq!(second.get(), 2);
354        assert!(state.begin().is_ok());
355
356        let third = state
357            .replace(second)
358            .unwrap_or_else(|_| unreachable!("replacement generation was rejected"));
359        assert_eq!(third.get(), 3);
360        assert!(state.begin().is_ok());
361    }
362
363    #[test]
364    fn stale_transitions_cannot_close_or_reopen_replacement_credentials() {
365        let state = SharedCredentialAttemptState::new();
366        let stale = state
367            .begin()
368            .unwrap_or_else(|_| unreachable!("initial credential generation was closed"));
369        let current = state
370            .replace(stale.generation())
371            .unwrap_or_else(|_| unreachable!("replacement generation was rejected"));
372        assert_eq!(
373            state.reject(stale),
374            Err(CredentialAttemptError::StaleGeneration)
375        );
376        assert_eq!(
377            state.validate(stale),
378            Err(CredentialAttemptError::StaleGeneration)
379        );
380        assert_eq!(
381            state.replace(stale.generation()),
382            Err(CredentialAttemptError::StaleGeneration)
383        );
384        assert_eq!(
385            state.reconfirm(
386                stale.generation(),
387                CredentialReconfirmation::acknowledge_same_credentials(),
388            ),
389            Err(CredentialAttemptError::StaleGeneration)
390        );
391        assert_eq!(state.observe(), (current, CredentialAttemptStatus::Open));
392    }
393
394    #[test]
395    fn foreign_attempts_never_validate_or_close_equal_generations() {
396        let owner_a = SharedCredentialAttemptState::new();
397        let owner_b = SharedCredentialAttemptState::new();
398        let foreign = owner_a
399            .begin()
400            .unwrap_or_else(|_| unreachable!("owner A generation was closed"));
401
402        assert_eq!(
403            owner_b.validate(foreign),
404            Err(CredentialAttemptError::ForeignState)
405        );
406        assert_eq!(
407            owner_b.reject(foreign),
408            Err(CredentialAttemptError::ForeignState)
409        );
410        assert_eq!(
411            owner_b.observe(),
412            (
413                CredentialAttemptGeneration::INITIAL,
414                CredentialAttemptStatus::Open
415            )
416        );
417
418        let generation_a = owner_a
419            .replace(CredentialAttemptGeneration::INITIAL)
420            .unwrap_or_else(|_| unreachable!("owner A replacement failed"));
421        let generation_b = owner_b
422            .replace(CredentialAttemptGeneration::INITIAL)
423            .unwrap_or_else(|_| unreachable!("owner B replacement failed"));
424        assert_eq!(generation_a, generation_b);
425        let foreign_replacement = owner_a
426            .begin()
427            .unwrap_or_else(|_| unreachable!("owner A replacement was closed"));
428        assert_eq!(
429            owner_b.reject(foreign_replacement),
430            Err(CredentialAttemptError::ForeignState)
431        );
432        assert_eq!(
433            owner_b.observe(),
434            (generation_b, CredentialAttemptStatus::Open)
435        );
436    }
437
438    #[test]
439    fn generation_exhaustion_fails_closed_without_wrapping() {
440        let mut state = SharedCredentialAttemptState::new();
441        state.set_generation_for_test(MAX_GENERATION, true);
442        let generation = state.observe().0;
443        assert_eq!(
444            state.replace(generation),
445            Err(CredentialAttemptError::GenerationExhausted)
446        );
447        assert_eq!(
448            state.reconfirm(
449                generation,
450                CredentialReconfirmation::acknowledge_same_credentials(),
451            ),
452            Err(CredentialAttemptError::GenerationExhausted)
453        );
454        assert_eq!(
455            state.observe(),
456            (generation, CredentialAttemptStatus::Rejected)
457        );
458    }
459}