Skip to main content

cloud_sdk/authentication/
attempt.rs

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