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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CredentialReadMode {
15 Use,
18 Retry,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub(crate) enum CredentialReadPurpose {
25 Resolve,
26 AuthoritativeResolve,
27 ForceRefresh,
28}
29
30#[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#[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#[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#[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#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
108pub struct CredentialReadStatus {
109 pub generation: u64,
110 pub state: CredentialReadStatusState,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum CredentialReadEventCloseReason {
116 Lagged,
118 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
221pub struct CredentialReadEventHandoff {
227 pub snapshot: Option<CredentialReadStatus>,
228 pub events: CredentialReadEventSubscription,
229}
230
231pub 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 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 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 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 }
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 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 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 inner.publish_status(CredentialReadStatus {
601 generation: owned_flight.generation,
602 state: public_state(owned_flight.purpose, &result),
603 });
604 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 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
691pub async fn resolve_credential(
693 mode: CredentialReadMode,
694) -> Result<Option<ResolvedParsleeCredential>, CredentialReadError> {
695 process_coordinator().resolve(mode).await
696}
697
698pub async fn refresh_credential() -> Result<Option<ResolvedParsleeCredential>, CredentialReadError>
700{
701 process_coordinator().refresh().await
702}
703
704pub fn subscribe_credential_read_updates() -> watch::Receiver<Option<CredentialReadStatus>> {
706 process_coordinator().subscribe()
707}
708
709pub fn subscribe_credential_read_events() -> CredentialReadEventSubscription {
716 process_coordinator().subscribe_events()
717}
718
719pub 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 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}