Skip to main content

car_auth/
credential_read.rs

1use car_secrets::SecretError;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
7use std::sync::{Arc, Mutex as StdMutex, OnceLock, Weak};
8use tokio::sync::{mpsc, watch, Mutex};
9
10const CREDENTIAL_READ_EVENT_QUEUE_CAPACITY: usize = 4;
11
12/// Whether an authoritative credential use may start a new physical read.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CredentialReadMode {
15    /// Join an in-flight read or start one when the coordinator is not cooling
16    /// down from a terminal store failure.
17    Use,
18    /// Join an in-flight read or explicitly clear cooldown and start one new
19    /// generation.
20    Retry,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub(crate) enum CredentialReadPurpose {
25    Resolve,
26    AuthoritativeResolve,
27    ForceRefresh,
28}
29
30/// Parslee request authority resolved from one credential snapshot.
31#[derive(Clone, PartialEq, Eq)]
32pub struct ResolvedParsleeCredential {
33    pub access_token: String,
34    pub api_base: String,
35    pub expires_at: u64,
36}
37
38impl std::fmt::Debug for ResolvedParsleeCredential {
39    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        formatter
41            .debug_struct("ResolvedParsleeCredential")
42            .field("access_token", &"[REDACTED]")
43            .field("api_base", &self.api_base)
44            .field("expires_at", &self.expires_at)
45            .finish()
46    }
47}
48
49/// Stable recovery classes. No variant contains secret material.
50#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
51#[serde(rename_all = "snake_case")]
52pub enum CredentialReadFailureKind {
53    Denied,
54    Cancelled,
55    TimedOut,
56    Unreadable,
57    Cooldown,
58}
59
60/// One terminal authoritative-read failure.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct CredentialReadError {
63    pub kind: CredentialReadFailureKind,
64    pub message: String,
65}
66
67impl std::fmt::Display for CredentialReadError {
68    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        formatter.write_str(&self.message)
70    }
71}
72
73impl std::error::Error for CredentialReadError {}
74
75impl From<SecretError> for CredentialReadError {
76    fn from(error: SecretError) -> Self {
77        let kind = match error {
78            SecretError::AccessDenied { .. } => CredentialReadFailureKind::Denied,
79            SecretError::UserCancelled { .. } => CredentialReadFailureKind::Cancelled,
80            SecretError::HelperTimedOut { .. } => CredentialReadFailureKind::TimedOut,
81            SecretError::Unavailable(_)
82            | SecretError::NotFound { .. }
83            | SecretError::Backend(_)
84            | SecretError::InvalidJson(_) => CredentialReadFailureKind::Unreadable,
85        };
86        Self {
87            kind,
88            message: error.to_string(),
89        }
90    }
91}
92
93/// Secret-free state suitable for daemon event fanout.
94#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
95#[serde(rename_all = "snake_case")]
96pub enum CredentialReadStatusState {
97    Pending,
98    Configured,
99    SignedOut,
100    Denied,
101    Cancelled,
102    TimedOut,
103    Unreadable,
104}
105
106/// Latest process-owned credential-read generation and its public state.
107#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
108pub struct CredentialReadStatus {
109    pub generation: u64,
110    pub state: CredentialReadStatusState,
111}
112
113/// Why an ordered credential-event subscription stopped.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum CredentialReadEventCloseReason {
116    /// The bounded subscriber queue filled before its consumer kept pace.
117    Lagged,
118    /// The process-owned publisher closed unexpectedly.
119    Closed,
120}
121
122struct CredentialReadEventSubscribers {
123    state: StdMutex<CredentialReadEventSubscriberState>,
124}
125
126struct CredentialReadEventSubscriberState {
127    next_id: u64,
128    latest: Option<CredentialReadStatus>,
129    subscribers: HashMap<u64, CredentialReadEventSubscriber>,
130}
131
132struct CredentialReadEventSubscriber {
133    sender: mpsc::Sender<CredentialReadStatus>,
134    close_reason: watch::Sender<Option<CredentialReadEventCloseReason>>,
135}
136
137impl CredentialReadEventSubscribers {
138    fn new() -> Self {
139        Self {
140            state: StdMutex::new(CredentialReadEventSubscriberState {
141                next_id: 0,
142                latest: None,
143                subscribers: HashMap::new(),
144            }),
145        }
146    }
147
148    fn subscribe(self: &Arc<Self>) -> CredentialReadEventSubscription {
149        self.subscribe_with_snapshot().events
150    }
151
152    fn subscribe_with_snapshot(self: &Arc<Self>) -> CredentialReadEventHandoff {
153        let (sender, receiver) = mpsc::channel(CREDENTIAL_READ_EVENT_QUEUE_CAPACITY);
154        let (close_reason, close_updates) = watch::channel(None);
155        let mut state = self
156            .state
157            .lock()
158            .expect("credential event subscribers mutex poisoned");
159        let snapshot = state.latest;
160        let id = state.next_id;
161        state.next_id = state
162            .next_id
163            .checked_add(1)
164            .expect("credential event subscription id exhausted");
165        state.subscribers.insert(
166            id,
167            CredentialReadEventSubscriber {
168                sender,
169                close_reason,
170            },
171        );
172        CredentialReadEventHandoff {
173            snapshot,
174            events: CredentialReadEventSubscription {
175                id,
176                receiver,
177                close_updates,
178                subscribers: Arc::downgrade(self),
179            },
180        }
181    }
182
183    fn publish(&self, status: CredentialReadStatus) {
184        let mut state = self
185            .state
186            .lock()
187            .expect("credential event subscribers mutex poisoned");
188        state.latest = Some(status);
189        state
190            .subscribers
191            .retain(|_, subscriber| match subscriber.sender.try_send(status) {
192                Ok(()) => true,
193                Err(mpsc::error::TrySendError::Full(_)) => {
194                    subscriber
195                        .close_reason
196                        .send_replace(Some(CredentialReadEventCloseReason::Lagged));
197                    false
198                }
199                Err(mpsc::error::TrySendError::Closed(_)) => false,
200            });
201    }
202
203    fn unsubscribe(&self, id: u64) {
204        self.state
205            .lock()
206            .expect("credential event subscribers mutex poisoned")
207            .subscribers
208            .remove(&id);
209    }
210
211    #[cfg(test)]
212    fn count(&self) -> usize {
213        self.state
214            .lock()
215            .expect("credential event subscribers mutex poisoned")
216            .subscribers
217            .len()
218    }
219}
220
221/// Atomic starting point for an ordered credential-event consumer.
222///
223/// `snapshot` contains only the last status published before `events` was
224/// registered. Every publication after registration is queued in `events`, so
225/// a terminal snapshot can never overtake that generation's queued Pending.
226pub struct CredentialReadEventHandoff {
227    pub snapshot: Option<CredentialReadStatus>,
228    pub events: CredentialReadEventSubscription,
229}
230
231/// One owned subscription to future credential-read lifecycle events.
232///
233/// Dropping this value unregisters it immediately. A subscriber that falls
234/// behind the small bounded queue is also unregistered and observes the stream
235/// close after draining the events already retained in order.
236pub struct CredentialReadEventSubscription {
237    id: u64,
238    receiver: mpsc::Receiver<CredentialReadStatus>,
239    close_updates: watch::Receiver<Option<CredentialReadEventCloseReason>>,
240    subscribers: Weak<CredentialReadEventSubscribers>,
241}
242
243impl CredentialReadEventSubscription {
244    pub async fn recv(&mut self) -> Result<CredentialReadStatus, CredentialReadEventCloseReason> {
245        match self.receiver.recv().await {
246            Some(status) => Ok(status),
247            None => {
248                Err((*self.close_updates.borrow())
249                    .unwrap_or(CredentialReadEventCloseReason::Closed))
250            }
251        }
252    }
253
254    /// Wait independently for this subscription to become unusable.
255    ///
256    /// This wakes even when the event consumer is blocked writing a prior
257    /// status to its downstream transport.
258    pub fn closed(&self) -> impl Future<Output = CredentialReadEventCloseReason> + Send + 'static {
259        let mut updates = self.close_updates.clone();
260        async move {
261            loop {
262                if let Some(reason) = *updates.borrow_and_update() {
263                    return reason;
264                }
265                if updates.changed().await.is_err() {
266                    return CredentialReadEventCloseReason::Closed;
267                }
268            }
269        }
270    }
271}
272
273impl Drop for CredentialReadEventSubscription {
274    fn drop(&mut self) {
275        if let Some(subscribers) = self.subscribers.upgrade() {
276            subscribers.unsubscribe(self.id);
277        }
278    }
279}
280
281type CredentialReadResult = Result<Option<ResolvedParsleeCredential>, CredentialReadError>;
282type ReaderFuture = Pin<Box<dyn Future<Output = CredentialReadResult> + Send + 'static>>;
283
284trait CredentialReader: Clone + Send + Sync + 'static {
285    fn read(&self, purpose: CredentialReadPurpose) -> ReaderFuture;
286}
287
288#[derive(Clone, Copy)]
289struct SystemCredentialReader;
290
291impl CredentialReader for SystemCredentialReader {
292    fn read(&self, purpose: CredentialReadPurpose) -> ReaderFuture {
293        Box::pin(async move { super::resolve_credential_once(purpose).await })
294    }
295}
296
297#[derive(Debug, Clone)]
298enum FlightState {
299    Pending,
300    Terminal(CredentialReadResult),
301}
302
303struct Flight {
304    generation: u64,
305    purpose: CredentialReadPurpose,
306    retry_cutoff: AtomicU64,
307    refresh_cutoff: AtomicU64,
308    updates: watch::Sender<FlightState>,
309}
310
311impl Flight {
312    fn new(
313        generation: u64,
314        purpose: CredentialReadPurpose,
315        retry_cutoff: u64,
316        refresh_cutoff: u64,
317    ) -> Self {
318        let (updates, _initial_receiver) = watch::channel(FlightState::Pending);
319        Self {
320            generation,
321            purpose,
322            retry_cutoff: AtomicU64::new(retry_cutoff),
323            refresh_cutoff: AtomicU64::new(refresh_cutoff),
324            updates,
325        }
326    }
327
328    fn terminal_result(&self) -> Option<CredentialReadResult> {
329        match &*self.updates.borrow() {
330            FlightState::Pending => None,
331            FlightState::Terminal(result) => Some(result.clone()),
332        }
333    }
334
335    fn publish_terminal(&self, result: CredentialReadResult) {
336        self.updates.send_replace(FlightState::Terminal(result));
337    }
338
339    async fn wait(&self) -> CredentialReadResult {
340        // Subscribe before rechecking the current value. If the reader
341        // publishes between these operations, watch retains the terminal value
342        // and changed() cannot be the only wakeup path.
343        let mut updates = self.updates.subscribe();
344        loop {
345            if let FlightState::Terminal(result) = &*updates.borrow_and_update() {
346                return result.clone();
347            }
348            updates
349                .changed()
350                .await
351                .expect("credential flight sender is owned by the flight");
352        }
353    }
354}
355
356struct CoordinatorState {
357    generation: u64,
358    flight: Option<Arc<Flight>>,
359    cooldown: Option<CredentialReadError>,
360}
361
362struct CoordinatorInner<R: CredentialReader> {
363    reader: R,
364    retry_intents: AtomicU64,
365    refresh_intents: AtomicU64,
366    state: Mutex<CoordinatorState>,
367    public_updates: watch::Sender<Option<CredentialReadStatus>>,
368    event_subscribers: Arc<CredentialReadEventSubscribers>,
369}
370
371impl<R: CredentialReader> CoordinatorInner<R> {
372    fn publish_status(&self, status: CredentialReadStatus) {
373        self.public_updates.send_replace(Some(status));
374        self.event_subscribers.publish(status);
375    }
376}
377
378struct CredentialReadCoordinator<R: CredentialReader> {
379    inner: Arc<CoordinatorInner<R>>,
380}
381
382impl<R: CredentialReader> Clone for CredentialReadCoordinator<R> {
383    fn clone(&self) -> Self {
384        Self {
385            inner: Arc::clone(&self.inner),
386        }
387    }
388}
389
390impl<R: CredentialReader> CredentialReadCoordinator<R> {
391    fn new(reader: R) -> Self {
392        let (public_updates, _initial_receiver) = watch::channel(None);
393        Self {
394            inner: Arc::new(CoordinatorInner {
395                reader,
396                retry_intents: AtomicU64::new(0),
397                refresh_intents: AtomicU64::new(0),
398                state: Mutex::new(CoordinatorState {
399                    generation: 0,
400                    flight: None,
401                    cooldown: None,
402                }),
403                public_updates,
404                event_subscribers: Arc::new(CredentialReadEventSubscribers::new()),
405            }),
406        }
407    }
408
409    fn subscribe(&self) -> watch::Receiver<Option<CredentialReadStatus>> {
410        self.inner.public_updates.subscribe()
411    }
412
413    fn subscribe_events(&self) -> CredentialReadEventSubscription {
414        self.inner.event_subscribers.subscribe()
415    }
416
417    fn subscribe_events_with_snapshot(&self) -> CredentialReadEventHandoff {
418        self.inner.event_subscribers.subscribe_with_snapshot()
419    }
420
421    #[cfg(test)]
422    fn event_subscriber_count(&self) -> usize {
423        self.inner.event_subscribers.count()
424    }
425
426    #[cfg(test)]
427    fn event_queue_capacity(&self) -> usize {
428        CREDENTIAL_READ_EVENT_QUEUE_CAPACITY
429    }
430
431    fn resolve(
432        &self,
433        mode: CredentialReadMode,
434    ) -> impl Future<Output = CredentialReadResult> + Send + 'static {
435        let purpose = match mode {
436            CredentialReadMode::Use => CredentialReadPurpose::Resolve,
437            CredentialReadMode::Retry => CredentialReadPurpose::AuthoritativeResolve,
438        };
439        self.resolve_for(mode, purpose)
440    }
441
442    fn refresh(&self) -> impl Future<Output = CredentialReadResult> + Send + 'static {
443        self.resolve_for(CredentialReadMode::Use, CredentialReadPurpose::ForceRefresh)
444    }
445
446    fn resolve_for(
447        &self,
448        mode: CredentialReadMode,
449        purpose: CredentialReadPurpose,
450    ) -> impl Future<Output = CredentialReadResult> + Send + 'static {
451        let coordinator = self.clone();
452        // Allocate Retry intent when the future is created, not when it first
453        // gets polled. A flight records every intent created before terminal
454        // publication, so simultaneous callers still join it even when the
455        // injected/store reader completes before a follower acquires the lock.
456        let retry_intent = coordinator.register_retry_intent(mode);
457        let refresh_intent = coordinator.register_refresh_intent(purpose);
458        async move {
459            loop {
460                let flight = coordinator
461                    .acquire_flight(mode, purpose, retry_intent, refresh_intent)
462                    .await?;
463                let result = flight.wait().await;
464                if flight_result_satisfies(purpose, flight.purpose, &result) {
465                    return result;
466                }
467                // Incompatible purposes may join a pending flight so physical
468                // reads never overlap. Once that flight succeeds, loop to
469                // create/join the requested generation. In particular, a
470                // forced `None` cannot sign out an ordinary waiter, and a
471                // Retry cannot accept a cache-eligible ordinary resolution.
472            }
473        }
474    }
475
476    fn register_retry_intent(&self, mode: CredentialReadMode) -> u64 {
477        match mode {
478            CredentialReadMode::Use => 0,
479            CredentialReadMode::Retry => self
480                .inner
481                .retry_intents
482                .fetch_add(1, AtomicOrdering::SeqCst)
483                .saturating_add(1),
484        }
485    }
486
487    fn register_refresh_intent(&self, purpose: CredentialReadPurpose) -> u64 {
488        match purpose {
489            CredentialReadPurpose::Resolve | CredentialReadPurpose::AuthoritativeResolve => 0,
490            CredentialReadPurpose::ForceRefresh => self
491                .inner
492                .refresh_intents
493                .fetch_add(1, AtomicOrdering::SeqCst)
494                .saturating_add(1),
495        }
496    }
497
498    async fn acquire_flight(
499        &self,
500        mode: CredentialReadMode,
501        purpose: CredentialReadPurpose,
502        retry_intent: u64,
503        refresh_intent: u64,
504    ) -> Result<Arc<Flight>, CredentialReadError> {
505        let mut state = self.inner.state.lock().await;
506        if let Some(flight) = state.flight.as_ref() {
507            if flight.terminal_result().is_none() {
508                if mode == CredentialReadMode::Retry {
509                    flight
510                        .retry_cutoff
511                        .fetch_max(retry_intent, AtomicOrdering::SeqCst);
512                }
513                if purpose == CredentialReadPurpose::ForceRefresh {
514                    flight
515                        .refresh_cutoff
516                        .fetch_max(refresh_intent, AtomicOrdering::SeqCst);
517                }
518                return Ok(Arc::clone(flight));
519            }
520            if mode == CredentialReadMode::Retry
521                && retry_intent <= flight.retry_cutoff.load(AtomicOrdering::SeqCst)
522                && flight
523                    .terminal_result()
524                    .as_ref()
525                    .is_some_and(|result| flight_result_satisfies(purpose, flight.purpose, result))
526            {
527                return Ok(Arc::clone(flight));
528            }
529            if purpose == CredentialReadPurpose::ForceRefresh
530                && flight.purpose == CredentialReadPurpose::ForceRefresh
531                && refresh_intent <= flight.refresh_cutoff.load(AtomicOrdering::SeqCst)
532            {
533                return Ok(Arc::clone(flight));
534            }
535        }
536
537        if let Some(failure) = state.cooldown.as_ref() {
538            if mode == CredentialReadMode::Use {
539                return Err(CredentialReadError {
540                    kind: CredentialReadFailureKind::Cooldown,
541                    message: format!(
542                        "credential access is in cooldown after {}; explicitly retry Keychain access",
543                        failure.kind.label()
544                    ),
545                });
546            }
547            state.cooldown = None;
548        }
549
550        state.generation = state.generation.saturating_add(1);
551        let retry_cutoff = self.inner.retry_intents.load(AtomicOrdering::SeqCst);
552        let refresh_cutoff = self.inner.refresh_intents.load(AtomicOrdering::SeqCst);
553        let flight = Arc::new(Flight::new(
554            state.generation,
555            purpose,
556            retry_cutoff,
557            refresh_cutoff,
558        ));
559        state.flight = Some(Arc::clone(&flight));
560        self.inner.publish_status(CredentialReadStatus {
561            generation: flight.generation,
562            state: CredentialReadStatusState::Pending,
563        });
564        drop(state);
565
566        let inner = Arc::clone(&self.inner);
567        let owned_flight = Arc::clone(&flight);
568        tokio::spawn(async move {
569            let result = inner.reader.read(owned_flight.purpose).await;
570            if let Err(error) = &result {
571                // Install cooldown before waking any waiter with the terminal
572                // failure. Otherwise an immediate ordinary Use could race the
573                // background task and start another prompt in the gap.
574                let mut state = inner.state.lock().await;
575                if state
576                    .flight
577                    .as_ref()
578                    .is_some_and(|flight| Arc::ptr_eq(flight, &owned_flight))
579                {
580                    state.cooldown = Some(error.clone());
581                }
582            }
583            // Linearize terminal publication after every Retry future already
584            // created for this generation. Those callers may not have polled
585            // yet; their intent IDs let them join this result without turning
586            // an immediate completion into a second physical read. A later
587            // explicit Retry receives a larger ID and may create a generation.
588            owned_flight.retry_cutoff.fetch_max(
589                inner.retry_intents.load(AtomicOrdering::SeqCst),
590                AtomicOrdering::SeqCst,
591            );
592            owned_flight.refresh_cutoff.fetch_max(
593                inner.refresh_intents.load(AtomicOrdering::SeqCst),
594                AtomicOrdering::SeqCst,
595            );
596            // Queue the public terminal before waking flight waiters. A waiter
597            // may immediately start the next generation on another executor
598            // thread; publishing in this order prevents that generation's
599            // Pending event from overtaking this terminal event.
600            inner.publish_status(CredentialReadStatus {
601                generation: owned_flight.generation,
602                state: public_state(owned_flight.purpose, &result),
603            });
604            // The independently owned flight receives exactly one terminal
605            // publication even when the request that created it is gone.
606            owned_flight.publish_terminal(result);
607        });
608        Ok(flight)
609    }
610
611    #[cfg(test)]
612    async fn resolve_after_terminal_publish_for_test(
613        &self,
614        mode: CredentialReadMode,
615    ) -> CredentialReadResult {
616        let retry_intent = self.register_retry_intent(mode);
617        let flight = self
618            .acquire_flight(mode, CredentialReadPurpose::Resolve, retry_intent, 0)
619            .await?;
620        while flight.terminal_result().is_none() {
621            tokio::task::yield_now().await;
622        }
623        flight.wait().await
624    }
625}
626
627fn flight_result_satisfies(
628    requested: CredentialReadPurpose,
629    completed: CredentialReadPurpose,
630    result: &CredentialReadResult,
631) -> bool {
632    if result.is_err() {
633        return true;
634    }
635    match requested {
636        CredentialReadPurpose::Resolve => {
637            completed != CredentialReadPurpose::ForceRefresh || matches!(result, Ok(Some(_)))
638        }
639        CredentialReadPurpose::AuthoritativeResolve => match completed {
640            CredentialReadPurpose::Resolve => false,
641            CredentialReadPurpose::AuthoritativeResolve => true,
642            CredentialReadPurpose::ForceRefresh => matches!(result, Ok(Some(_))),
643        },
644        CredentialReadPurpose::ForceRefresh => completed == CredentialReadPurpose::ForceRefresh,
645    }
646}
647
648impl CredentialReadFailureKind {
649    fn label(self) -> &'static str {
650        match self {
651            Self::Denied => "access was denied",
652            Self::Cancelled => "access was cancelled",
653            Self::TimedOut => "the credential helper timed out",
654            Self::Unreadable => "the credential store was unreadable",
655            Self::Cooldown => "a previous credential read failed",
656        }
657    }
658}
659
660fn public_state(
661    purpose: CredentialReadPurpose,
662    result: &CredentialReadResult,
663) -> CredentialReadStatusState {
664    match result {
665        Ok(Some(_)) => CredentialReadStatusState::Configured,
666        // A forced refresh may be unavailable because the configured account
667        // has no refresh token or the token endpoint is offline. `None` tells
668        // the caller not to retry the rejected bearer; it is not evidence that
669        // the credential observed at the start of this generation disappeared.
670        Ok(None) if purpose == CredentialReadPurpose::ForceRefresh => {
671            CredentialReadStatusState::Configured
672        }
673        Ok(None) => CredentialReadStatusState::SignedOut,
674        Err(error) => match error.kind {
675            CredentialReadFailureKind::Denied => CredentialReadStatusState::Denied,
676            CredentialReadFailureKind::Cancelled => CredentialReadStatusState::Cancelled,
677            CredentialReadFailureKind::TimedOut => CredentialReadStatusState::TimedOut,
678            CredentialReadFailureKind::Unreadable | CredentialReadFailureKind::Cooldown => {
679                CredentialReadStatusState::Unreadable
680            }
681        },
682    }
683}
684
685fn process_coordinator() -> &'static CredentialReadCoordinator<SystemCredentialReader> {
686    static COORDINATOR: OnceLock<CredentialReadCoordinator<SystemCredentialReader>> =
687        OnceLock::new();
688    COORDINATOR.get_or_init(|| CredentialReadCoordinator::new(SystemCredentialReader))
689}
690
691/// Resolve one Parslee credential through the process-owned flight.
692pub async fn resolve_credential(
693    mode: CredentialReadMode,
694) -> Result<Option<ResolvedParsleeCredential>, CredentialReadError> {
695    process_coordinator().resolve(mode).await
696}
697
698/// Force one coordinator-owned Parslee refresh after a server auth rejection.
699pub async fn refresh_credential() -> Result<Option<ResolvedParsleeCredential>, CredentialReadError>
700{
701    process_coordinator().refresh().await
702}
703
704/// Subscribe to the latest secret-free credential-read state snapshot.
705pub fn subscribe_credential_read_updates() -> watch::Receiver<Option<CredentialReadStatus>> {
706    process_coordinator().subscribe()
707}
708
709/// Subscribe to future secret-free credential-read events without coalescing.
710///
711/// The stream does not replay the current snapshot. For every generation that
712/// starts after subscription, a consumer that keeps pace receives `Pending`
713/// followed by exactly one terminal state in publication order. A lagging
714/// subscription closes instead of dropping or reordering lifecycle events.
715pub fn subscribe_credential_read_events() -> CredentialReadEventSubscription {
716    process_coordinator().subscribe_events()
717}
718
719/// Atomically capture the last secret-free status and subscribe to later ones.
720///
721/// The snapshot and ordered queue are linearized under one credential-layer
722/// lock. A lifecycle completed before subscription appears only as the
723/// snapshot; a lifecycle started after subscription appears in exact
724/// `Pending`-then-terminal queue order.
725pub fn subscribe_credential_read_event_handoff() -> CredentialReadEventHandoff {
726    process_coordinator().subscribe_events_with_snapshot()
727}
728
729#[cfg(test)]
730mod tests {
731    use super::*;
732    use std::collections::VecDeque;
733    use std::sync::atomic::{AtomicUsize, Ordering};
734    use std::sync::{Arc, Mutex};
735    use tokio::sync::Notify;
736
737    #[derive(Clone)]
738    struct CountingReader {
739        inner: Arc<CountingReaderInner>,
740    }
741
742    struct CountingReaderInner {
743        calls: AtomicUsize,
744        outcomes: Mutex<VecDeque<CredentialReadResult>>,
745        blocked: bool,
746        release: Notify,
747    }
748
749    impl CountingReader {
750        fn blocked() -> Self {
751            Self {
752                inner: Arc::new(CountingReaderInner {
753                    calls: AtomicUsize::new(0),
754                    outcomes: Mutex::new(VecDeque::new()),
755                    blocked: true,
756                    release: Notify::new(),
757                }),
758            }
759        }
760
761        fn sequence(outcomes: impl IntoIterator<Item = CredentialReadResult>) -> Self {
762            Self {
763                inner: Arc::new(CountingReaderInner {
764                    calls: AtomicUsize::new(0),
765                    outcomes: Mutex::new(outcomes.into_iter().collect()),
766                    blocked: false,
767                    release: Notify::new(),
768                }),
769            }
770        }
771
772        fn immediate_success(credential: ResolvedParsleeCredential) -> Self {
773            Self::sequence([Ok(Some(credential))])
774        }
775
776        fn release_success(&self, credential: ResolvedParsleeCredential) {
777            self.release(Ok(Some(credential)));
778        }
779
780        fn release(&self, outcome: CredentialReadResult) {
781            self.inner.outcomes.lock().unwrap().push_back(outcome);
782            // Retain a permit if the independently owned reader has not
783            // reached `notified()` yet; the cancellation test must not depend
784            // on scheduler timing.
785            self.inner.release.notify_one();
786        }
787
788        fn calls(&self) -> usize {
789            self.inner.calls.load(Ordering::SeqCst)
790        }
791    }
792
793    #[derive(Clone)]
794    struct CachedThenPhysicalReader {
795        cached: ResolvedParsleeCredential,
796        physical_calls: Arc<AtomicUsize>,
797        outcomes: Arc<Mutex<VecDeque<CredentialReadResult>>>,
798    }
799
800    impl CachedThenPhysicalReader {
801        fn new(
802            cached: ResolvedParsleeCredential,
803            outcomes: impl IntoIterator<Item = CredentialReadResult>,
804        ) -> Self {
805            Self {
806                cached,
807                physical_calls: Arc::new(AtomicUsize::new(0)),
808                outcomes: Arc::new(Mutex::new(outcomes.into_iter().collect())),
809            }
810        }
811
812        fn physical_calls(&self) -> usize {
813            self.physical_calls.load(Ordering::SeqCst)
814        }
815    }
816
817    impl CredentialReader for CachedThenPhysicalReader {
818        fn read(&self, purpose: CredentialReadPurpose) -> ReaderFuture {
819            let reader = self.clone();
820            Box::pin(async move {
821                if purpose == CredentialReadPurpose::Resolve {
822                    return Ok(Some(reader.cached));
823                }
824                reader.physical_calls.fetch_add(1, Ordering::SeqCst);
825                reader
826                    .outcomes
827                    .lock()
828                    .unwrap()
829                    .pop_front()
830                    .expect("injected physical credential reader exhausted")
831            })
832        }
833    }
834
835    impl CredentialReader for CountingReader {
836        fn read(&self, _purpose: CredentialReadPurpose) -> ReaderFuture {
837            let reader = self.clone();
838            Box::pin(async move {
839                reader.inner.calls.fetch_add(1, Ordering::SeqCst);
840                if reader.inner.blocked {
841                    loop {
842                        if let Some(outcome) = reader.inner.outcomes.lock().unwrap().pop_front() {
843                            return outcome;
844                        }
845                        reader.inner.release.notified().await;
846                    }
847                }
848                reader
849                    .inner
850                    .outcomes
851                    .lock()
852                    .unwrap()
853                    .pop_front()
854                    .expect("injected credential reader exhausted")
855            })
856        }
857    }
858
859    fn fixture_credential() -> ResolvedParsleeCredential {
860        ResolvedParsleeCredential {
861            access_token: "fixture-access-token".into(),
862            api_base: "https://fixture.parslee.test".into(),
863            expires_at: 1_800_000_000,
864        }
865    }
866
867    fn denied() -> CredentialReadResult {
868        Err(CredentialReadError {
869            kind: CredentialReadFailureKind::Denied,
870            message: "credential access denied".into(),
871        })
872    }
873
874    fn success() -> CredentialReadResult {
875        Ok(Some(fixture_credential()))
876    }
877
878    fn replacement_credential() -> ResolvedParsleeCredential {
879        ResolvedParsleeCredential {
880            access_token: "replacement-access-token".into(),
881            api_base: "https://replacement.parslee.test".into(),
882            expires_at: 1_900_000_000,
883        }
884    }
885
886    #[tokio::test]
887    async fn concurrent_reads_share_one_cancellation_safe_flight() {
888        let reader = CountingReader::blocked();
889        let coordinator = CredentialReadCoordinator::new(reader.clone());
890        let mut public_updates = coordinator.subscribe();
891        let first = tokio::spawn(coordinator.clone().resolve(CredentialReadMode::Use));
892        let second = tokio::spawn(coordinator.clone().resolve(CredentialReadMode::Use));
893
894        public_updates.changed().await.unwrap();
895        assert_eq!(
896            *public_updates.borrow_and_update(),
897            Some(CredentialReadStatus {
898                generation: 1,
899                state: CredentialReadStatusState::Pending,
900            })
901        );
902        first.abort();
903        reader.release_success(fixture_credential());
904
905        assert_eq!(second.await.unwrap(), Ok(Some(fixture_credential())));
906        public_updates.changed().await.unwrap();
907        assert_eq!(
908            *public_updates.borrow_and_update(),
909            Some(CredentialReadStatus {
910                generation: 1,
911                state: CredentialReadStatusState::Configured,
912            })
913        );
914        assert_eq!(reader.calls(), 1);
915    }
916
917    #[tokio::test]
918    async fn denial_requires_explicit_retry() {
919        let reader = CountingReader::sequence([denied(), success()]);
920        let coordinator = CredentialReadCoordinator::new(reader.clone());
921
922        assert_eq!(
923            coordinator
924                .resolve(CredentialReadMode::Use)
925                .await
926                .unwrap_err()
927                .kind,
928            CredentialReadFailureKind::Denied
929        );
930        assert_eq!(
931            coordinator
932                .resolve(CredentialReadMode::Use)
933                .await
934                .unwrap_err()
935                .kind,
936            CredentialReadFailureKind::Cooldown
937        );
938        assert_eq!(
939            coordinator.resolve(CredentialReadMode::Retry).await,
940            success()
941        );
942        assert_eq!(reader.calls(), 2);
943    }
944
945    #[tokio::test]
946    async fn terminal_publish_before_waiter_subscription_cannot_lose_wakeup() {
947        let reader = CountingReader::immediate_success(fixture_credential());
948        let coordinator = CredentialReadCoordinator::new(reader.clone());
949
950        let result = coordinator
951            .resolve_after_terminal_publish_for_test(CredentialReadMode::Use)
952            .await;
953
954        assert_eq!(result, success());
955        assert_eq!(reader.calls(), 1);
956    }
957
958    #[tokio::test]
959    async fn immediately_completed_read_preserves_pending_before_terminal_for_event_consumers() {
960        let reader = CountingReader::immediate_success(fixture_credential());
961        let coordinator = CredentialReadCoordinator::new(reader.clone());
962        let mut events = coordinator.subscribe_events();
963
964        assert_eq!(
965            coordinator.resolve(CredentialReadMode::Use).await,
966            success()
967        );
968
969        assert_eq!(
970            events.recv().await,
971            Ok(CredentialReadStatus {
972                generation: 1,
973                state: CredentialReadStatusState::Pending,
974            }),
975            "a stalled event consumer must still observe pending before terminal"
976        );
977        assert_eq!(
978            events.recv().await,
979            Ok(CredentialReadStatus {
980                generation: 1,
981                state: CredentialReadStatusState::Configured,
982            })
983        );
984        assert!(
985            tokio::time::timeout(std::time::Duration::from_millis(25), events.recv())
986                .await
987                .is_err(),
988            "one generation must publish exactly one terminal event"
989        );
990        assert_eq!(reader.calls(), 1);
991    }
992
993    #[tokio::test]
994    async fn atomic_handoff_queues_complete_lifecycle_started_after_subscription() {
995        let reader = CountingReader::immediate_success(fixture_credential());
996        let coordinator = CredentialReadCoordinator::new(reader);
997        let CredentialReadEventHandoff {
998            snapshot,
999            mut events,
1000        } = coordinator.subscribe_events_with_snapshot();
1001
1002        assert_eq!(snapshot, None);
1003        assert_eq!(
1004            coordinator.resolve(CredentialReadMode::Use).await,
1005            success()
1006        );
1007        assert_eq!(
1008            events.recv().await,
1009            Ok(CredentialReadStatus {
1010                generation: 1,
1011                state: CredentialReadStatusState::Pending,
1012            })
1013        );
1014        assert_eq!(
1015            events.recv().await,
1016            Ok(CredentialReadStatus {
1017                generation: 1,
1018                state: CredentialReadStatusState::Configured,
1019            })
1020        );
1021    }
1022
1023    #[tokio::test]
1024    async fn atomic_handoff_reconciles_preexisting_terminal_without_inventing_pending() {
1025        let reader = CountingReader::immediate_success(fixture_credential());
1026        let coordinator = CredentialReadCoordinator::new(reader);
1027        assert_eq!(
1028            coordinator.resolve(CredentialReadMode::Use).await,
1029            success()
1030        );
1031
1032        let CredentialReadEventHandoff {
1033            snapshot,
1034            mut events,
1035        } = coordinator.subscribe_events_with_snapshot();
1036        assert_eq!(
1037            snapshot,
1038            Some(CredentialReadStatus {
1039                generation: 1,
1040                state: CredentialReadStatusState::Configured,
1041            })
1042        );
1043        assert!(
1044            tokio::time::timeout(std::time::Duration::from_millis(25), events.recv())
1045                .await
1046                .is_err(),
1047            "pre-subscription lifecycle must not be replayed into the future queue"
1048        );
1049    }
1050
1051    #[test]
1052    fn dropped_event_subscriptions_unregister_without_waiting_for_publication() {
1053        let coordinator = CredentialReadCoordinator::new(CountingReader::sequence([]));
1054        let baseline = coordinator.event_subscriber_count();
1055
1056        for _ in 0..64 {
1057            let subscription = coordinator.subscribe_events();
1058            assert_eq!(coordinator.event_subscriber_count(), baseline + 1);
1059            drop(subscription);
1060        }
1061
1062        assert_eq!(
1063            coordinator.event_subscriber_count(),
1064            baseline,
1065            "dropping receivers must promptly unregister their global senders"
1066        );
1067    }
1068
1069    #[tokio::test]
1070    async fn stalled_event_subscription_is_closed_at_bounded_capacity() {
1071        let coordinator =
1072            CredentialReadCoordinator::new(CountingReader::sequence((0..3).map(|_| success())));
1073        let baseline = coordinator.event_subscriber_count();
1074        let capacity = coordinator.event_queue_capacity();
1075        let mut subscription = coordinator.subscribe_events();
1076
1077        for generation in 0..3 {
1078            let mode = if generation == 0 {
1079                CredentialReadMode::Use
1080            } else {
1081                CredentialReadMode::Retry
1082            };
1083            assert_eq!(coordinator.resolve(mode).await, success());
1084        }
1085
1086        assert_eq!(
1087            coordinator.event_subscriber_count(),
1088            baseline,
1089            "overflow must remove the lagging subscriber immediately"
1090        );
1091        let mut retained = Vec::new();
1092        for _ in 0..capacity {
1093            retained.push(subscription.recv().await.unwrap());
1094        }
1095        assert_eq!(retained.len(), capacity);
1096        assert_eq!(
1097            retained,
1098            vec![
1099                CredentialReadStatus {
1100                    generation: 1,
1101                    state: CredentialReadStatusState::Pending,
1102                },
1103                CredentialReadStatus {
1104                    generation: 1,
1105                    state: CredentialReadStatusState::Configured,
1106                },
1107                CredentialReadStatus {
1108                    generation: 2,
1109                    state: CredentialReadStatusState::Pending,
1110                },
1111                CredentialReadStatus {
1112                    generation: 2,
1113                    state: CredentialReadStatusState::Configured,
1114                },
1115            ],
1116            "bounded retention must preserve complete lifecycle ordering"
1117        );
1118        assert_eq!(
1119            subscription.recv().await,
1120            Err(CredentialReadEventCloseReason::Lagged)
1121        );
1122        assert_eq!(
1123            subscription.closed().await,
1124            CredentialReadEventCloseReason::Lagged
1125        );
1126    }
1127
1128    #[tokio::test]
1129    async fn simultaneous_explicit_retries_create_one_new_flight() {
1130        let reader = CountingReader::sequence([denied(), success()]);
1131        let coordinator = CredentialReadCoordinator::new(reader.clone());
1132        assert_eq!(
1133            coordinator
1134                .resolve(CredentialReadMode::Use)
1135                .await
1136                .unwrap_err()
1137                .kind,
1138            CredentialReadFailureKind::Denied
1139        );
1140
1141        let (first, second) = tokio::join!(
1142            coordinator.resolve(CredentialReadMode::Retry),
1143            coordinator.resolve(CredentialReadMode::Retry),
1144        );
1145
1146        assert_eq!(first, success());
1147        assert_eq!(second, success());
1148        assert_eq!(reader.calls(), 2);
1149    }
1150
1151    #[tokio::test]
1152    async fn failed_retry_can_be_explicitly_retried_again() {
1153        let reader = CountingReader::sequence([denied(), denied(), success()]);
1154        let coordinator = CredentialReadCoordinator::new(reader.clone());
1155
1156        assert_eq!(
1157            coordinator
1158                .resolve(CredentialReadMode::Use)
1159                .await
1160                .unwrap_err()
1161                .kind,
1162            CredentialReadFailureKind::Denied
1163        );
1164        assert_eq!(
1165            coordinator
1166                .resolve(CredentialReadMode::Retry)
1167                .await
1168                .unwrap_err()
1169                .kind,
1170            CredentialReadFailureKind::Denied
1171        );
1172        assert_eq!(
1173            coordinator
1174                .resolve(CredentialReadMode::Use)
1175                .await
1176                .unwrap_err()
1177                .kind,
1178            CredentialReadFailureKind::Cooldown
1179        );
1180        assert_eq!(
1181            coordinator.resolve(CredentialReadMode::Retry).await,
1182            success()
1183        );
1184        assert_eq!(reader.calls(), 3);
1185    }
1186
1187    #[tokio::test]
1188    async fn failed_reactive_refresh_installs_cooldown_for_later_use() {
1189        let reader = CountingReader::sequence([denied(), success()]);
1190        let coordinator = CredentialReadCoordinator::new(reader.clone());
1191
1192        assert_eq!(
1193            coordinator.refresh().await.unwrap_err().kind,
1194            CredentialReadFailureKind::Denied
1195        );
1196        assert_eq!(
1197            coordinator
1198                .resolve(CredentialReadMode::Use)
1199                .await
1200                .unwrap_err()
1201                .kind,
1202            CredentialReadFailureKind::Cooldown
1203        );
1204        assert_eq!(reader.calls(), 1);
1205    }
1206
1207    #[tokio::test]
1208    async fn simultaneous_reactive_refreshes_share_one_forced_flight() {
1209        let reader = CountingReader::sequence([success()]);
1210        let coordinator = CredentialReadCoordinator::new(reader.clone());
1211
1212        let (first, second) = tokio::join!(coordinator.refresh(), coordinator.refresh());
1213
1214        assert_eq!(first, success());
1215        assert_eq!(second, success());
1216        assert_eq!(reader.calls(), 1);
1217    }
1218
1219    #[tokio::test]
1220    async fn ordinary_resolution_does_not_inherit_none_from_forced_refresh() {
1221        let reader = CountingReader::blocked();
1222        let coordinator = CredentialReadCoordinator::new(reader.clone());
1223
1224        let release = async {
1225            while reader.calls() == 0 {
1226                tokio::task::yield_now().await;
1227            }
1228            reader.release(Ok(None));
1229            tokio::task::yield_now().await;
1230            reader.release_success(fixture_credential());
1231        };
1232        let (refreshed, resolved, ()) = tokio::join!(
1233            coordinator.refresh(),
1234            coordinator.resolve(CredentialReadMode::Use),
1235            release,
1236        );
1237
1238        assert_eq!(refreshed, Ok(None));
1239        assert_eq!(resolved, success());
1240        assert_eq!(reader.calls(), 2);
1241    }
1242
1243    #[tokio::test]
1244    async fn explicit_retries_after_failed_force_bypass_cached_resolution_and_coalesce() {
1245        let replacement = replacement_credential();
1246        let reader = CachedThenPhysicalReader::new(
1247            fixture_credential(),
1248            [denied(), Ok(Some(replacement.clone()))],
1249        );
1250        let coordinator = CredentialReadCoordinator::new(reader.clone());
1251
1252        assert_eq!(
1253            coordinator.resolve(CredentialReadMode::Use).await,
1254            success()
1255        );
1256        assert_eq!(reader.physical_calls(), 0);
1257        assert_eq!(
1258            coordinator.refresh().await.unwrap_err().kind,
1259            CredentialReadFailureKind::Denied
1260        );
1261
1262        let (first, second) = tokio::join!(
1263            coordinator.resolve(CredentialReadMode::Retry),
1264            coordinator.resolve(CredentialReadMode::Retry),
1265        );
1266
1267        assert_eq!(first, Ok(Some(replacement.clone())));
1268        assert_eq!(second, Ok(Some(replacement)));
1269        assert_eq!(reader.physical_calls(), 2);
1270    }
1271
1272    #[tokio::test]
1273    async fn common_v2_resolution_flight_performs_one_physical_get() {
1274        const CHILD_MARKER: &str = "CAR_AUTH_ONE_GET_CHILD";
1275        if std::env::var(CHILD_MARKER).as_deref() != Ok("1") {
1276            let status = std::process::Command::new(std::env::current_exe().unwrap())
1277                .args([
1278                    "--exact",
1279                    "credential_read::tests::common_v2_resolution_flight_performs_one_physical_get",
1280                    "--nocapture",
1281                    "--test-threads=1",
1282                ])
1283                .env(CHILD_MARKER, "1")
1284                .status()
1285                .unwrap();
1286            assert!(status.success(), "isolated one-get assertion failed");
1287            return;
1288        }
1289
1290        let directory = tempfile::tempdir().unwrap();
1291        std::env::set_var("CAR_SECRETS_FILE_DIR", directory.path());
1292        std::env::remove_var(super::super::PARSLEE_ACCESS_TOKEN_KEY);
1293        std::env::remove_var(super::super::PARSLEE_API_BASE_KEY);
1294        super::super::invalidate_access_token_cache();
1295        car_secrets::SecretStore::new()
1296            .publish(
1297                &car_secrets::SecretRef::with_default_service(
1298                    car_secrets::PARSLEE_AUTH_STATE_V2_KEY,
1299                ),
1300                &serde_json::json!({
1301                    "schema": 2,
1302                    "revision": 7,
1303                    "generation": 3,
1304                    "active": {
1305                        "account_id": "one-get-account",
1306                        "access_token": "one-get-access",
1307                        "expires_at": 9_999_999_999_u64,
1308                        "api_base": "https://one-get.example"
1309                    },
1310                    "accounts": [{
1311                        "account_id": "one-get-account",
1312                        "access_token": "one-get-access",
1313                        "expires_at": 9_999_999_999_u64,
1314                        "api_base": "https://one-get.example"
1315                    }]
1316                })
1317                .to_string(),
1318            )
1319            .unwrap();
1320
1321        let before = car_secrets::secret_store_activity();
1322        let resolved = CredentialReadCoordinator::new(SystemCredentialReader)
1323            .resolve(CredentialReadMode::Use)
1324            .await
1325            .unwrap()
1326            .unwrap();
1327        let after = car_secrets::secret_store_activity();
1328
1329        assert_eq!(resolved.api_base, "https://one-get.example");
1330        assert_eq!(after.get_attempts - before.get_attempts, 1);
1331    }
1332}