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 AutomaticRefresh(super::RejectedBearerKey),
32}
33
34#[derive(Clone, PartialEq, Eq)]
36pub struct ResolvedParsleeCredential {
37 pub access_token: String,
38 pub api_base: String,
39 pub expires_at: u64,
40}
41
42impl std::fmt::Debug for ResolvedParsleeCredential {
43 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 formatter
45 .debug_struct("ResolvedParsleeCredential")
46 .field("access_token", &"[REDACTED]")
47 .field("api_base", &self.api_base)
48 .field("expires_at", &self.expires_at)
49 .finish()
50 }
51}
52
53#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
55#[serde(rename_all = "snake_case")]
56pub enum CredentialReadFailureKind {
57 Denied,
58 Cancelled,
59 TimedOut,
60 Unreadable,
61 Cooldown,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct CredentialReadError {
67 pub kind: CredentialReadFailureKind,
68 pub message: String,
69}
70
71impl std::fmt::Display for CredentialReadError {
72 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 formatter.write_str(&self.message)
74 }
75}
76
77impl std::error::Error for CredentialReadError {}
78
79impl From<SecretError> for CredentialReadError {
80 fn from(error: SecretError) -> Self {
81 let kind = match error {
82 SecretError::AccessDenied { .. } => CredentialReadFailureKind::Denied,
83 SecretError::UserCancelled { .. } => CredentialReadFailureKind::Cancelled,
84 SecretError::HelperTimedOut { .. } => CredentialReadFailureKind::TimedOut,
85 SecretError::Unavailable(_)
86 | SecretError::NotFound { .. }
87 | SecretError::Backend(_)
88 | SecretError::InvalidJson(_) => CredentialReadFailureKind::Unreadable,
89 };
90 Self {
91 kind,
92 message: error.to_string(),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
99#[serde(rename_all = "snake_case")]
100pub enum CredentialReadStatusState {
101 Pending,
102 Configured,
103 SignedOut,
104 Denied,
105 Cancelled,
106 TimedOut,
107 Unreadable,
108}
109
110#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
112pub struct CredentialReadStatus {
113 pub generation: u64,
114 pub state: CredentialReadStatusState,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum CredentialReadEventCloseReason {
120 Lagged,
122 Closed,
124}
125
126struct CredentialReadEventSubscribers {
127 state: StdMutex<CredentialReadEventSubscriberState>,
128}
129
130struct CredentialReadEventSubscriberState {
131 next_id: u64,
132 latest: Option<CredentialReadStatus>,
133 subscribers: HashMap<u64, CredentialReadEventSubscriber>,
134}
135
136struct CredentialReadEventSubscriber {
137 sender: mpsc::Sender<CredentialReadStatus>,
138 close_reason: watch::Sender<Option<CredentialReadEventCloseReason>>,
139}
140
141impl CredentialReadEventSubscribers {
142 fn new() -> Self {
143 Self {
144 state: StdMutex::new(CredentialReadEventSubscriberState {
145 next_id: 0,
146 latest: None,
147 subscribers: HashMap::new(),
148 }),
149 }
150 }
151
152 fn subscribe(self: &Arc<Self>) -> CredentialReadEventSubscription {
153 self.subscribe_with_snapshot().events
154 }
155
156 fn subscribe_with_snapshot(self: &Arc<Self>) -> CredentialReadEventHandoff {
157 let (sender, receiver) = mpsc::channel(CREDENTIAL_READ_EVENT_QUEUE_CAPACITY);
158 let (close_reason, close_updates) = watch::channel(None);
159 let mut state = self
160 .state
161 .lock()
162 .expect("credential event subscribers mutex poisoned");
163 let snapshot = state.latest;
164 let id = state.next_id;
165 state.next_id = state
166 .next_id
167 .checked_add(1)
168 .expect("credential event subscription id exhausted");
169 state.subscribers.insert(
170 id,
171 CredentialReadEventSubscriber {
172 sender,
173 close_reason,
174 },
175 );
176 CredentialReadEventHandoff {
177 snapshot,
178 events: CredentialReadEventSubscription {
179 id,
180 receiver,
181 close_updates,
182 subscribers: Arc::downgrade(self),
183 },
184 }
185 }
186
187 fn publish(&self, status: CredentialReadStatus) {
188 let mut state = self
189 .state
190 .lock()
191 .expect("credential event subscribers mutex poisoned");
192 state.latest = Some(status);
193 state
194 .subscribers
195 .retain(|_, subscriber| match subscriber.sender.try_send(status) {
196 Ok(()) => true,
197 Err(mpsc::error::TrySendError::Full(_)) => {
198 subscriber
199 .close_reason
200 .send_replace(Some(CredentialReadEventCloseReason::Lagged));
201 false
202 }
203 Err(mpsc::error::TrySendError::Closed(_)) => false,
204 });
205 }
206
207 fn unsubscribe(&self, id: u64) {
208 self.state
209 .lock()
210 .expect("credential event subscribers mutex poisoned")
211 .subscribers
212 .remove(&id);
213 }
214
215 #[cfg(test)]
216 fn count(&self) -> usize {
217 self.state
218 .lock()
219 .expect("credential event subscribers mutex poisoned")
220 .subscribers
221 .len()
222 }
223}
224
225pub struct CredentialReadEventHandoff {
231 pub snapshot: Option<CredentialReadStatus>,
232 pub events: CredentialReadEventSubscription,
233}
234
235pub struct CredentialReadEventSubscription {
241 id: u64,
242 receiver: mpsc::Receiver<CredentialReadStatus>,
243 close_updates: watch::Receiver<Option<CredentialReadEventCloseReason>>,
244 subscribers: Weak<CredentialReadEventSubscribers>,
245}
246
247impl CredentialReadEventSubscription {
248 pub async fn recv(&mut self) -> Result<CredentialReadStatus, CredentialReadEventCloseReason> {
249 match self.receiver.recv().await {
250 Some(status) => Ok(status),
251 None => {
252 Err((*self.close_updates.borrow())
253 .unwrap_or(CredentialReadEventCloseReason::Closed))
254 }
255 }
256 }
257
258 pub fn closed(&self) -> impl Future<Output = CredentialReadEventCloseReason> + Send + 'static {
263 let mut updates = self.close_updates.clone();
264 async move {
265 loop {
266 if let Some(reason) = *updates.borrow_and_update() {
267 return reason;
268 }
269 if updates.changed().await.is_err() {
270 return CredentialReadEventCloseReason::Closed;
271 }
272 }
273 }
274 }
275}
276
277impl Drop for CredentialReadEventSubscription {
278 fn drop(&mut self) {
279 if let Some(subscribers) = self.subscribers.upgrade() {
280 subscribers.unsubscribe(self.id);
281 }
282 }
283}
284
285type CredentialReadResult = Result<Option<ResolvedParsleeCredential>, CredentialReadError>;
286type ReaderFuture = Pin<Box<dyn Future<Output = CredentialReadResult> + Send + 'static>>;
287
288trait CredentialReader: Clone + Send + Sync + 'static {
289 fn read(&self, purpose: CredentialReadPurpose) -> ReaderFuture;
290}
291
292#[derive(Clone, Copy)]
293struct SystemCredentialReader;
294
295impl CredentialReader for SystemCredentialReader {
296 fn read(&self, purpose: CredentialReadPurpose) -> ReaderFuture {
297 Box::pin(async move { super::resolve_credential_once(purpose).await })
298 }
299}
300
301#[derive(Debug, Clone)]
302enum FlightState {
303 Pending,
304 Terminal(CredentialReadResult),
305}
306
307struct Flight {
308 generation: u64,
309 purpose: CredentialReadPurpose,
310 retry_cutoff: AtomicU64,
311 refresh_cutoff: AtomicU64,
312 updates: watch::Sender<FlightState>,
313}
314
315impl Flight {
316 fn new(
317 generation: u64,
318 purpose: CredentialReadPurpose,
319 retry_cutoff: u64,
320 refresh_cutoff: u64,
321 ) -> Self {
322 let (updates, _initial_receiver) = watch::channel(FlightState::Pending);
323 Self {
324 generation,
325 purpose,
326 retry_cutoff: AtomicU64::new(retry_cutoff),
327 refresh_cutoff: AtomicU64::new(refresh_cutoff),
328 updates,
329 }
330 }
331
332 fn terminal_result(&self) -> Option<CredentialReadResult> {
333 match &*self.updates.borrow() {
334 FlightState::Pending => None,
335 FlightState::Terminal(result) => Some(result.clone()),
336 }
337 }
338
339 fn publish_terminal(&self, result: CredentialReadResult) {
340 self.updates.send_replace(FlightState::Terminal(result));
341 }
342
343 async fn wait(&self) -> CredentialReadResult {
344 let mut updates = self.updates.subscribe();
348 loop {
349 if let FlightState::Terminal(result) = &*updates.borrow_and_update() {
350 return result.clone();
351 }
352 updates
353 .changed()
354 .await
355 .expect("credential flight sender is owned by the flight");
356 }
357 }
358}
359
360struct CoordinatorState {
361 generation: u64,
362 flight: Option<Arc<Flight>>,
363 cooldown: Option<CredentialReadError>,
364}
365
366struct CoordinatorInner<R: CredentialReader> {
367 reader: R,
368 retry_intents: AtomicU64,
369 refresh_intents: AtomicU64,
370 state: Mutex<CoordinatorState>,
371 public_updates: watch::Sender<Option<CredentialReadStatus>>,
372 event_subscribers: Arc<CredentialReadEventSubscribers>,
373}
374
375impl<R: CredentialReader> CoordinatorInner<R> {
376 fn publish_status(&self, status: CredentialReadStatus) {
377 self.public_updates.send_replace(Some(status));
378 self.event_subscribers.publish(status);
379 }
380}
381
382struct CredentialReadCoordinator<R: CredentialReader> {
383 inner: Arc<CoordinatorInner<R>>,
384}
385
386impl<R: CredentialReader> Clone for CredentialReadCoordinator<R> {
387 fn clone(&self) -> Self {
388 Self {
389 inner: Arc::clone(&self.inner),
390 }
391 }
392}
393
394impl<R: CredentialReader> CredentialReadCoordinator<R> {
395 fn new(reader: R) -> Self {
396 let (public_updates, _initial_receiver) = watch::channel(None);
397 Self {
398 inner: Arc::new(CoordinatorInner {
399 reader,
400 retry_intents: AtomicU64::new(0),
401 refresh_intents: AtomicU64::new(0),
402 state: Mutex::new(CoordinatorState {
403 generation: 0,
404 flight: None,
405 cooldown: None,
406 }),
407 public_updates,
408 event_subscribers: Arc::new(CredentialReadEventSubscribers::new()),
409 }),
410 }
411 }
412
413 fn subscribe(&self) -> watch::Receiver<Option<CredentialReadStatus>> {
414 self.inner.public_updates.subscribe()
415 }
416
417 fn subscribe_events(&self) -> CredentialReadEventSubscription {
418 self.inner.event_subscribers.subscribe()
419 }
420
421 fn subscribe_events_with_snapshot(&self) -> CredentialReadEventHandoff {
422 self.inner.event_subscribers.subscribe_with_snapshot()
423 }
424
425 #[cfg(test)]
426 fn event_subscriber_count(&self) -> usize {
427 self.inner.event_subscribers.count()
428 }
429
430 #[cfg(test)]
431 fn event_queue_capacity(&self) -> usize {
432 CREDENTIAL_READ_EVENT_QUEUE_CAPACITY
433 }
434
435 fn resolve(
436 &self,
437 mode: CredentialReadMode,
438 ) -> impl Future<Output = CredentialReadResult> + Send + 'static {
439 let purpose = match mode {
440 CredentialReadMode::Use => CredentialReadPurpose::Resolve,
441 CredentialReadMode::Retry => CredentialReadPurpose::AuthoritativeResolve,
442 };
443 self.resolve_for(mode, purpose)
444 }
445
446 fn refresh(&self) -> impl Future<Output = CredentialReadResult> + Send + 'static {
447 self.resolve_for(CredentialReadMode::Use, CredentialReadPurpose::ForceRefresh)
448 }
449
450 fn refresh_after_rejection(
451 &self,
452 rejected_bearer: super::RejectedBearerKey,
453 ) -> impl Future<Output = CredentialReadResult> + Send + 'static {
454 self.resolve_for(
455 CredentialReadMode::Use,
456 CredentialReadPurpose::AutomaticRefresh(rejected_bearer),
457 )
458 }
459
460 fn resolve_for(
461 &self,
462 mode: CredentialReadMode,
463 purpose: CredentialReadPurpose,
464 ) -> impl Future<Output = CredentialReadResult> + Send + 'static {
465 let coordinator = self.clone();
466 let retry_intent = coordinator.register_retry_intent(mode);
471 let refresh_intent = coordinator.register_refresh_intent(purpose);
472 async move {
473 loop {
474 let flight = coordinator
475 .acquire_flight(mode, purpose, retry_intent, refresh_intent)
476 .await?;
477 let result = flight.wait().await;
478 if flight_result_satisfies(purpose, flight.purpose, &result) {
479 return result;
480 }
481 }
487 }
488 }
489
490 fn register_retry_intent(&self, mode: CredentialReadMode) -> u64 {
491 match mode {
492 CredentialReadMode::Use => 0,
493 CredentialReadMode::Retry => self
494 .inner
495 .retry_intents
496 .fetch_add(1, AtomicOrdering::SeqCst)
497 .saturating_add(1),
498 }
499 }
500
501 fn register_refresh_intent(&self, purpose: CredentialReadPurpose) -> u64 {
502 match purpose {
503 CredentialReadPurpose::Resolve | CredentialReadPurpose::AuthoritativeResolve => 0,
504 CredentialReadPurpose::ForceRefresh | CredentialReadPurpose::AutomaticRefresh(_) => {
505 self.inner
506 .refresh_intents
507 .fetch_add(1, AtomicOrdering::SeqCst)
508 .saturating_add(1)
509 }
510 }
511 }
512
513 async fn acquire_flight(
514 &self,
515 mode: CredentialReadMode,
516 purpose: CredentialReadPurpose,
517 retry_intent: u64,
518 refresh_intent: u64,
519 ) -> Result<Arc<Flight>, CredentialReadError> {
520 let mut state = self.inner.state.lock().await;
521 if let Some(flight) = state.flight.as_ref() {
522 if flight.terminal_result().is_none() {
523 if mode == CredentialReadMode::Retry {
524 flight
525 .retry_cutoff
526 .fetch_max(retry_intent, AtomicOrdering::SeqCst);
527 }
528 if matches!(
529 purpose,
530 CredentialReadPurpose::ForceRefresh
531 | CredentialReadPurpose::AutomaticRefresh(_)
532 ) {
533 flight
534 .refresh_cutoff
535 .fetch_max(refresh_intent, AtomicOrdering::SeqCst);
536 }
537 return Ok(Arc::clone(flight));
538 }
539 if mode == CredentialReadMode::Retry
540 && retry_intent <= flight.retry_cutoff.load(AtomicOrdering::SeqCst)
541 && flight
542 .terminal_result()
543 .as_ref()
544 .is_some_and(|result| flight_result_satisfies(purpose, flight.purpose, result))
545 {
546 return Ok(Arc::clone(flight));
547 }
548 if matches!(
549 (purpose, flight.purpose),
550 (
551 CredentialReadPurpose::ForceRefresh,
552 CredentialReadPurpose::ForceRefresh
553 )
554 ) && refresh_intent <= flight.refresh_cutoff.load(AtomicOrdering::SeqCst)
555 {
556 return Ok(Arc::clone(flight));
557 }
558 if matches!(
559 (purpose, flight.purpose),
560 (
561 CredentialReadPurpose::AutomaticRefresh(requested),
562 CredentialReadPurpose::AutomaticRefresh(completed)
563 ) if requested == completed
564 ) && refresh_intent <= flight.refresh_cutoff.load(AtomicOrdering::SeqCst)
565 {
566 return Ok(Arc::clone(flight));
567 }
568 }
569
570 if let Some(failure) = state.cooldown.as_ref() {
571 if mode == CredentialReadMode::Use && purpose != CredentialReadPurpose::ForceRefresh {
577 return Err(CredentialReadError {
578 kind: CredentialReadFailureKind::Cooldown,
579 message: format!(
580 "credential access is in cooldown after {}; explicitly retry Keychain access",
581 failure.kind.label()
582 ),
583 });
584 }
585 state.cooldown = None;
586 }
587
588 state.generation = state.generation.saturating_add(1);
589 let retry_cutoff = self.inner.retry_intents.load(AtomicOrdering::SeqCst);
590 let refresh_cutoff = self.inner.refresh_intents.load(AtomicOrdering::SeqCst);
591 let flight = Arc::new(Flight::new(
592 state.generation,
593 purpose,
594 retry_cutoff,
595 refresh_cutoff,
596 ));
597 state.flight = Some(Arc::clone(&flight));
598 self.inner.publish_status(CredentialReadStatus {
599 generation: flight.generation,
600 state: CredentialReadStatusState::Pending,
601 });
602 drop(state);
603
604 let inner = Arc::clone(&self.inner);
605 let owned_flight = Arc::clone(&flight);
606 tokio::spawn(async move {
607 let result = inner.reader.read(owned_flight.purpose).await;
608 if let Err(error) = &result {
609 let mut state = inner.state.lock().await;
613 if state
614 .flight
615 .as_ref()
616 .is_some_and(|flight| Arc::ptr_eq(flight, &owned_flight))
617 {
618 state.cooldown = Some(error.clone());
619 }
620 }
621 owned_flight.retry_cutoff.fetch_max(
627 inner.retry_intents.load(AtomicOrdering::SeqCst),
628 AtomicOrdering::SeqCst,
629 );
630 owned_flight.refresh_cutoff.fetch_max(
631 inner.refresh_intents.load(AtomicOrdering::SeqCst),
632 AtomicOrdering::SeqCst,
633 );
634 inner.publish_status(CredentialReadStatus {
639 generation: owned_flight.generation,
640 state: public_state(owned_flight.purpose, &result),
641 });
642 owned_flight.publish_terminal(result);
645 });
646 Ok(flight)
647 }
648
649 #[cfg(test)]
650 async fn resolve_after_terminal_publish_for_test(
651 &self,
652 mode: CredentialReadMode,
653 ) -> CredentialReadResult {
654 let retry_intent = self.register_retry_intent(mode);
655 let flight = self
656 .acquire_flight(mode, CredentialReadPurpose::Resolve, retry_intent, 0)
657 .await?;
658 while flight.terminal_result().is_none() {
659 tokio::task::yield_now().await;
660 }
661 flight.wait().await
662 }
663}
664
665fn flight_result_satisfies(
666 requested: CredentialReadPurpose,
667 completed: CredentialReadPurpose,
668 result: &CredentialReadResult,
669) -> bool {
670 if let CredentialReadPurpose::AutomaticRefresh(requested_bearer) = requested {
671 return matches!(
672 completed,
673 CredentialReadPurpose::AutomaticRefresh(completed_bearer)
674 if requested_bearer == completed_bearer
675 );
676 }
677 if result.is_err() {
678 return true;
679 }
680 match requested {
681 CredentialReadPurpose::Resolve => {
682 !matches!(
683 completed,
684 CredentialReadPurpose::ForceRefresh | CredentialReadPurpose::AutomaticRefresh(_)
685 ) || matches!(result, Ok(Some(_)))
686 }
687 CredentialReadPurpose::AuthoritativeResolve => match completed {
688 CredentialReadPurpose::Resolve => false,
689 CredentialReadPurpose::AuthoritativeResolve => true,
690 CredentialReadPurpose::ForceRefresh | CredentialReadPurpose::AutomaticRefresh(_) => {
691 matches!(result, Ok(Some(_)))
692 }
693 },
694 CredentialReadPurpose::ForceRefresh => completed == CredentialReadPurpose::ForceRefresh,
695 CredentialReadPurpose::AutomaticRefresh(_) => unreachable!("handled above"),
696 }
697}
698
699impl CredentialReadFailureKind {
700 fn label(self) -> &'static str {
701 match self {
702 Self::Denied => "access was denied",
703 Self::Cancelled => "access was cancelled",
704 Self::TimedOut => "the credential helper timed out",
705 Self::Unreadable => "the credential store was unreadable",
706 Self::Cooldown => "a previous credential read failed",
707 }
708 }
709}
710
711fn public_state(
712 purpose: CredentialReadPurpose,
713 result: &CredentialReadResult,
714) -> CredentialReadStatusState {
715 match result {
716 Ok(Some(_)) => CredentialReadStatusState::Configured,
717 Ok(None)
722 if matches!(
723 purpose,
724 CredentialReadPurpose::ForceRefresh | CredentialReadPurpose::AutomaticRefresh(_)
725 ) =>
726 {
727 CredentialReadStatusState::Configured
728 }
729 Ok(None) => CredentialReadStatusState::SignedOut,
730 Err(error) => match error.kind {
731 CredentialReadFailureKind::Denied => CredentialReadStatusState::Denied,
732 CredentialReadFailureKind::Cancelled => CredentialReadStatusState::Cancelled,
733 CredentialReadFailureKind::TimedOut => CredentialReadStatusState::TimedOut,
734 CredentialReadFailureKind::Unreadable | CredentialReadFailureKind::Cooldown => {
735 CredentialReadStatusState::Unreadable
736 }
737 },
738 }
739}
740
741fn process_coordinator() -> &'static CredentialReadCoordinator<SystemCredentialReader> {
742 static COORDINATOR: OnceLock<CredentialReadCoordinator<SystemCredentialReader>> =
743 OnceLock::new();
744 COORDINATOR.get_or_init(|| CredentialReadCoordinator::new(SystemCredentialReader))
745}
746
747pub async fn resolve_credential(
749 mode: CredentialReadMode,
750) -> Result<Option<ResolvedParsleeCredential>, CredentialReadError> {
751 process_coordinator().resolve(mode).await
752}
753
754pub async fn refresh_credential() -> Result<Option<ResolvedParsleeCredential>, CredentialReadError>
756{
757 process_coordinator().refresh().await
758}
759
760pub(crate) fn refresh_credential_after_rejection(
766 rejected_bearer: super::RejectedBearerKey,
767) -> impl Future<Output = CredentialReadResult> + Send + 'static {
768 process_coordinator().refresh_after_rejection(rejected_bearer)
769}
770
771pub fn subscribe_credential_read_updates() -> watch::Receiver<Option<CredentialReadStatus>> {
773 process_coordinator().subscribe()
774}
775
776pub fn subscribe_credential_read_events() -> CredentialReadEventSubscription {
783 process_coordinator().subscribe_events()
784}
785
786pub fn subscribe_credential_read_event_handoff() -> CredentialReadEventHandoff {
793 process_coordinator().subscribe_events_with_snapshot()
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799 use std::collections::VecDeque;
800 use std::sync::atomic::{AtomicUsize, Ordering};
801 use std::sync::{Arc, Mutex};
802 use tokio::sync::Notify;
803
804 #[derive(Clone)]
805 struct CountingReader {
806 inner: Arc<CountingReaderInner>,
807 }
808
809 struct CountingReaderInner {
810 calls: AtomicUsize,
811 outcomes: Mutex<VecDeque<CredentialReadResult>>,
812 blocked: bool,
813 release: Notify,
814 }
815
816 impl CountingReader {
817 fn blocked() -> Self {
818 Self {
819 inner: Arc::new(CountingReaderInner {
820 calls: AtomicUsize::new(0),
821 outcomes: Mutex::new(VecDeque::new()),
822 blocked: true,
823 release: Notify::new(),
824 }),
825 }
826 }
827
828 fn sequence(outcomes: impl IntoIterator<Item = CredentialReadResult>) -> Self {
829 Self {
830 inner: Arc::new(CountingReaderInner {
831 calls: AtomicUsize::new(0),
832 outcomes: Mutex::new(outcomes.into_iter().collect()),
833 blocked: false,
834 release: Notify::new(),
835 }),
836 }
837 }
838
839 fn immediate_success(credential: ResolvedParsleeCredential) -> Self {
840 Self::sequence([Ok(Some(credential))])
841 }
842
843 fn release_success(&self, credential: ResolvedParsleeCredential) {
844 self.release(Ok(Some(credential)));
845 }
846
847 fn release(&self, outcome: CredentialReadResult) {
848 self.inner.outcomes.lock().unwrap().push_back(outcome);
849 self.inner.release.notify_one();
853 }
854
855 fn calls(&self) -> usize {
856 self.inner.calls.load(Ordering::SeqCst)
857 }
858 }
859
860 #[derive(Clone)]
861 struct CachedThenPhysicalReader {
862 cached: ResolvedParsleeCredential,
863 physical_calls: Arc<AtomicUsize>,
864 outcomes: Arc<Mutex<VecDeque<CredentialReadResult>>>,
865 }
866
867 impl CachedThenPhysicalReader {
868 fn new(
869 cached: ResolvedParsleeCredential,
870 outcomes: impl IntoIterator<Item = CredentialReadResult>,
871 ) -> Self {
872 Self {
873 cached,
874 physical_calls: Arc::new(AtomicUsize::new(0)),
875 outcomes: Arc::new(Mutex::new(outcomes.into_iter().collect())),
876 }
877 }
878
879 fn physical_calls(&self) -> usize {
880 self.physical_calls.load(Ordering::SeqCst)
881 }
882 }
883
884 impl CredentialReader for CachedThenPhysicalReader {
885 fn read(&self, purpose: CredentialReadPurpose) -> ReaderFuture {
886 let reader = self.clone();
887 Box::pin(async move {
888 if purpose == CredentialReadPurpose::Resolve {
889 return Ok(Some(reader.cached));
890 }
891 reader.physical_calls.fetch_add(1, Ordering::SeqCst);
892 reader
893 .outcomes
894 .lock()
895 .unwrap()
896 .pop_front()
897 .expect("injected physical credential reader exhausted")
898 })
899 }
900 }
901
902 impl CredentialReader for CountingReader {
903 fn read(&self, _purpose: CredentialReadPurpose) -> ReaderFuture {
904 let reader = self.clone();
905 Box::pin(async move {
906 reader.inner.calls.fetch_add(1, Ordering::SeqCst);
907 if reader.inner.blocked {
908 loop {
909 if let Some(outcome) = reader.inner.outcomes.lock().unwrap().pop_front() {
910 return outcome;
911 }
912 reader.inner.release.notified().await;
913 }
914 }
915 reader
916 .inner
917 .outcomes
918 .lock()
919 .unwrap()
920 .pop_front()
921 .expect("injected credential reader exhausted")
922 })
923 }
924 }
925
926 fn fixture_credential() -> ResolvedParsleeCredential {
927 ResolvedParsleeCredential {
928 access_token: "fixture-access-token".into(),
929 api_base: "https://fixture.parslee.test".into(),
930 expires_at: 1_800_000_000,
931 }
932 }
933
934 fn denied() -> CredentialReadResult {
935 Err(CredentialReadError {
936 kind: CredentialReadFailureKind::Denied,
937 message: "credential access denied".into(),
938 })
939 }
940
941 fn success() -> CredentialReadResult {
942 Ok(Some(fixture_credential()))
943 }
944
945 fn replacement_credential() -> ResolvedParsleeCredential {
946 ResolvedParsleeCredential {
947 access_token: "replacement-access-token".into(),
948 api_base: "https://replacement.parslee.test".into(),
949 expires_at: 1_900_000_000,
950 }
951 }
952
953 #[tokio::test]
954 async fn concurrent_reads_share_one_cancellation_safe_flight() {
955 let reader = CountingReader::blocked();
956 let coordinator = CredentialReadCoordinator::new(reader.clone());
957 let mut public_updates = coordinator.subscribe();
958 let first = tokio::spawn(coordinator.clone().resolve(CredentialReadMode::Use));
959 let second = tokio::spawn(coordinator.clone().resolve(CredentialReadMode::Use));
960
961 public_updates.changed().await.unwrap();
962 assert_eq!(
963 *public_updates.borrow_and_update(),
964 Some(CredentialReadStatus {
965 generation: 1,
966 state: CredentialReadStatusState::Pending,
967 })
968 );
969 first.abort();
970 reader.release_success(fixture_credential());
971
972 assert_eq!(second.await.unwrap(), Ok(Some(fixture_credential())));
973 public_updates.changed().await.unwrap();
974 assert_eq!(
975 *public_updates.borrow_and_update(),
976 Some(CredentialReadStatus {
977 generation: 1,
978 state: CredentialReadStatusState::Configured,
979 })
980 );
981 assert_eq!(reader.calls(), 1);
982 }
983
984 #[tokio::test]
985 async fn denial_requires_explicit_retry() {
986 let reader = CountingReader::sequence([denied(), success()]);
987 let coordinator = CredentialReadCoordinator::new(reader.clone());
988
989 assert_eq!(
990 coordinator
991 .resolve(CredentialReadMode::Use)
992 .await
993 .unwrap_err()
994 .kind,
995 CredentialReadFailureKind::Denied
996 );
997 assert_eq!(
998 coordinator
999 .resolve(CredentialReadMode::Use)
1000 .await
1001 .unwrap_err()
1002 .kind,
1003 CredentialReadFailureKind::Cooldown
1004 );
1005 assert_eq!(
1006 coordinator.resolve(CredentialReadMode::Retry).await,
1007 success()
1008 );
1009 assert_eq!(reader.calls(), 2);
1010 }
1011
1012 #[tokio::test]
1013 async fn terminal_publish_before_waiter_subscription_cannot_lose_wakeup() {
1014 let reader = CountingReader::immediate_success(fixture_credential());
1015 let coordinator = CredentialReadCoordinator::new(reader.clone());
1016
1017 let result = coordinator
1018 .resolve_after_terminal_publish_for_test(CredentialReadMode::Use)
1019 .await;
1020
1021 assert_eq!(result, success());
1022 assert_eq!(reader.calls(), 1);
1023 }
1024
1025 #[tokio::test]
1026 async fn immediately_completed_read_preserves_pending_before_terminal_for_event_consumers() {
1027 let reader = CountingReader::immediate_success(fixture_credential());
1028 let coordinator = CredentialReadCoordinator::new(reader.clone());
1029 let mut events = coordinator.subscribe_events();
1030
1031 assert_eq!(
1032 coordinator.resolve(CredentialReadMode::Use).await,
1033 success()
1034 );
1035
1036 assert_eq!(
1037 events.recv().await,
1038 Ok(CredentialReadStatus {
1039 generation: 1,
1040 state: CredentialReadStatusState::Pending,
1041 }),
1042 "a stalled event consumer must still observe pending before terminal"
1043 );
1044 assert_eq!(
1045 events.recv().await,
1046 Ok(CredentialReadStatus {
1047 generation: 1,
1048 state: CredentialReadStatusState::Configured,
1049 })
1050 );
1051 assert!(
1052 tokio::time::timeout(std::time::Duration::from_millis(25), events.recv())
1053 .await
1054 .is_err(),
1055 "one generation must publish exactly one terminal event"
1056 );
1057 assert_eq!(reader.calls(), 1);
1058 }
1059
1060 #[tokio::test]
1061 async fn atomic_handoff_queues_complete_lifecycle_started_after_subscription() {
1062 let reader = CountingReader::immediate_success(fixture_credential());
1063 let coordinator = CredentialReadCoordinator::new(reader);
1064 let CredentialReadEventHandoff {
1065 snapshot,
1066 mut events,
1067 } = coordinator.subscribe_events_with_snapshot();
1068
1069 assert_eq!(snapshot, None);
1070 assert_eq!(
1071 coordinator.resolve(CredentialReadMode::Use).await,
1072 success()
1073 );
1074 assert_eq!(
1075 events.recv().await,
1076 Ok(CredentialReadStatus {
1077 generation: 1,
1078 state: CredentialReadStatusState::Pending,
1079 })
1080 );
1081 assert_eq!(
1082 events.recv().await,
1083 Ok(CredentialReadStatus {
1084 generation: 1,
1085 state: CredentialReadStatusState::Configured,
1086 })
1087 );
1088 }
1089
1090 #[tokio::test]
1091 async fn atomic_handoff_reconciles_preexisting_terminal_without_inventing_pending() {
1092 let reader = CountingReader::immediate_success(fixture_credential());
1093 let coordinator = CredentialReadCoordinator::new(reader);
1094 assert_eq!(
1095 coordinator.resolve(CredentialReadMode::Use).await,
1096 success()
1097 );
1098
1099 let CredentialReadEventHandoff {
1100 snapshot,
1101 mut events,
1102 } = coordinator.subscribe_events_with_snapshot();
1103 assert_eq!(
1104 snapshot,
1105 Some(CredentialReadStatus {
1106 generation: 1,
1107 state: CredentialReadStatusState::Configured,
1108 })
1109 );
1110 assert!(
1111 tokio::time::timeout(std::time::Duration::from_millis(25), events.recv())
1112 .await
1113 .is_err(),
1114 "pre-subscription lifecycle must not be replayed into the future queue"
1115 );
1116 }
1117
1118 #[test]
1119 fn dropped_event_subscriptions_unregister_without_waiting_for_publication() {
1120 let coordinator = CredentialReadCoordinator::new(CountingReader::sequence([]));
1121 let baseline = coordinator.event_subscriber_count();
1122
1123 for _ in 0..64 {
1124 let subscription = coordinator.subscribe_events();
1125 assert_eq!(coordinator.event_subscriber_count(), baseline + 1);
1126 drop(subscription);
1127 }
1128
1129 assert_eq!(
1130 coordinator.event_subscriber_count(),
1131 baseline,
1132 "dropping receivers must promptly unregister their global senders"
1133 );
1134 }
1135
1136 #[tokio::test]
1137 async fn stalled_event_subscription_is_closed_at_bounded_capacity() {
1138 let coordinator =
1139 CredentialReadCoordinator::new(CountingReader::sequence((0..3).map(|_| success())));
1140 let baseline = coordinator.event_subscriber_count();
1141 let capacity = coordinator.event_queue_capacity();
1142 let mut subscription = coordinator.subscribe_events();
1143
1144 for generation in 0..3 {
1145 let mode = if generation == 0 {
1146 CredentialReadMode::Use
1147 } else {
1148 CredentialReadMode::Retry
1149 };
1150 assert_eq!(coordinator.resolve(mode).await, success());
1151 }
1152
1153 assert_eq!(
1154 coordinator.event_subscriber_count(),
1155 baseline,
1156 "overflow must remove the lagging subscriber immediately"
1157 );
1158 let mut retained = Vec::new();
1159 for _ in 0..capacity {
1160 retained.push(subscription.recv().await.unwrap());
1161 }
1162 assert_eq!(retained.len(), capacity);
1163 assert_eq!(
1164 retained,
1165 vec![
1166 CredentialReadStatus {
1167 generation: 1,
1168 state: CredentialReadStatusState::Pending,
1169 },
1170 CredentialReadStatus {
1171 generation: 1,
1172 state: CredentialReadStatusState::Configured,
1173 },
1174 CredentialReadStatus {
1175 generation: 2,
1176 state: CredentialReadStatusState::Pending,
1177 },
1178 CredentialReadStatus {
1179 generation: 2,
1180 state: CredentialReadStatusState::Configured,
1181 },
1182 ],
1183 "bounded retention must preserve complete lifecycle ordering"
1184 );
1185 assert_eq!(
1186 subscription.recv().await,
1187 Err(CredentialReadEventCloseReason::Lagged)
1188 );
1189 assert_eq!(
1190 subscription.closed().await,
1191 CredentialReadEventCloseReason::Lagged
1192 );
1193 }
1194
1195 #[tokio::test]
1196 async fn simultaneous_explicit_retries_create_one_new_flight() {
1197 let reader = CountingReader::sequence([denied(), success()]);
1198 let coordinator = CredentialReadCoordinator::new(reader.clone());
1199 assert_eq!(
1200 coordinator
1201 .resolve(CredentialReadMode::Use)
1202 .await
1203 .unwrap_err()
1204 .kind,
1205 CredentialReadFailureKind::Denied
1206 );
1207
1208 let (first, second) = tokio::join!(
1209 coordinator.resolve(CredentialReadMode::Retry),
1210 coordinator.resolve(CredentialReadMode::Retry),
1211 );
1212
1213 assert_eq!(first, success());
1214 assert_eq!(second, success());
1215 assert_eq!(reader.calls(), 2);
1216 }
1217
1218 #[tokio::test]
1219 async fn failed_retry_can_be_explicitly_retried_again() {
1220 let reader = CountingReader::sequence([denied(), denied(), success()]);
1221 let coordinator = CredentialReadCoordinator::new(reader.clone());
1222
1223 assert_eq!(
1224 coordinator
1225 .resolve(CredentialReadMode::Use)
1226 .await
1227 .unwrap_err()
1228 .kind,
1229 CredentialReadFailureKind::Denied
1230 );
1231 assert_eq!(
1232 coordinator
1233 .resolve(CredentialReadMode::Retry)
1234 .await
1235 .unwrap_err()
1236 .kind,
1237 CredentialReadFailureKind::Denied
1238 );
1239 assert_eq!(
1240 coordinator
1241 .resolve(CredentialReadMode::Use)
1242 .await
1243 .unwrap_err()
1244 .kind,
1245 CredentialReadFailureKind::Cooldown
1246 );
1247 assert_eq!(
1248 coordinator.resolve(CredentialReadMode::Retry).await,
1249 success()
1250 );
1251 assert_eq!(reader.calls(), 3);
1252 }
1253
1254 #[tokio::test]
1255 async fn failed_reactive_refresh_installs_cooldown_for_later_use() {
1256 let reader = CountingReader::sequence([denied(), success()]);
1257 let coordinator = CredentialReadCoordinator::new(reader.clone());
1258
1259 assert_eq!(
1260 coordinator.refresh().await.unwrap_err().kind,
1261 CredentialReadFailureKind::Denied
1262 );
1263 assert_eq!(
1264 coordinator
1265 .resolve(CredentialReadMode::Use)
1266 .await
1267 .unwrap_err()
1268 .kind,
1269 CredentialReadFailureKind::Cooldown
1270 );
1271 assert_eq!(reader.calls(), 1);
1272 }
1273
1274 fn refresh_commit_failure() -> CredentialReadResult {
1275 Err(CredentialReadError {
1278 kind: CredentialReadFailureKind::Unreadable,
1279 message: "reactive Parslee refresh commit failed: injected publish failure".into(),
1280 })
1281 }
1282
1283 #[tokio::test]
1284 async fn forced_refresh_after_failed_forced_refresh_commit_reaches_reader() {
1285 let reader = CountingReader::sequence([refresh_commit_failure(), success()]);
1286 let coordinator = CredentialReadCoordinator::new(reader.clone());
1287
1288 assert_eq!(
1289 coordinator.refresh().await.unwrap_err().kind,
1290 CredentialReadFailureKind::Unreadable
1291 );
1292 assert_eq!(
1293 coordinator
1294 .resolve(CredentialReadMode::Use)
1295 .await
1296 .unwrap_err()
1297 .kind,
1298 CredentialReadFailureKind::Cooldown,
1299 "an ordinary Use stays in cooldown after a failed forced refresh"
1300 );
1301 assert_eq!(reader.calls(), 1);
1302
1303 assert_eq!(
1304 coordinator.refresh().await,
1305 success(),
1306 "a later explicit ForceRefresh must not be blocked by a prior commit failure"
1307 );
1308 assert_eq!(
1309 reader.calls(),
1310 2,
1311 "the second forced refresh must reach the reader, not short-circuit on Cooldown"
1312 );
1313 }
1314
1315 #[tokio::test]
1316 async fn ordinary_use_stays_in_cooldown_when_bypassing_forced_refresh_fails() {
1317 let reader = CountingReader::sequence([denied(), denied()]);
1318 let coordinator = CredentialReadCoordinator::new(reader.clone());
1319 let rejected = super::super::rejected_bearer_key("fixture-access-token");
1320
1321 assert_eq!(
1322 coordinator
1323 .resolve(CredentialReadMode::Use)
1324 .await
1325 .unwrap_err()
1326 .kind,
1327 CredentialReadFailureKind::Denied
1328 );
1329 assert_eq!(
1330 coordinator
1331 .resolve(CredentialReadMode::Use)
1332 .await
1333 .unwrap_err()
1334 .kind,
1335 CredentialReadFailureKind::Cooldown
1336 );
1337 assert_eq!(reader.calls(), 1);
1338
1339 assert_eq!(
1340 coordinator.refresh().await.unwrap_err().kind,
1341 CredentialReadFailureKind::Denied,
1342 "an explicit forced refresh makes one new read instead of returning Cooldown"
1343 );
1344 assert_eq!(reader.calls(), 2);
1345 assert_eq!(
1346 coordinator
1347 .resolve(CredentialReadMode::Use)
1348 .await
1349 .unwrap_err()
1350 .kind,
1351 CredentialReadFailureKind::Cooldown,
1352 "the forced refresh's own failure re-arms cooldown for ordinary Use"
1353 );
1354 assert_eq!(
1355 coordinator
1356 .refresh_after_rejection(rejected)
1357 .await
1358 .unwrap_err()
1359 .kind,
1360 CredentialReadFailureKind::Cooldown,
1361 "automatic rejected-bearer recovery does not bypass cooldown"
1362 );
1363 assert_eq!(
1364 reader.calls(),
1365 2,
1366 "neither cooled-down caller may start a read"
1367 );
1368 }
1369
1370 #[tokio::test]
1371 async fn forced_refresh_after_keychain_denial_reaches_reader_and_success_clears_cooldown() {
1372 let reader = CountingReader::sequence([denied(), success(), success()]);
1373 let coordinator = CredentialReadCoordinator::new(reader.clone());
1374
1375 assert_eq!(
1376 coordinator
1377 .resolve(CredentialReadMode::Use)
1378 .await
1379 .unwrap_err()
1380 .kind,
1381 CredentialReadFailureKind::Denied
1382 );
1383 assert_eq!(
1384 coordinator.refresh().await,
1385 success(),
1386 "an explicit forced refresh after a Keychain denial makes one new read"
1387 );
1388 assert_eq!(reader.calls(), 2);
1389 assert_eq!(
1390 coordinator.resolve(CredentialReadMode::Use).await,
1391 success(),
1392 "a successful forced read clears the cooldown the same way Retry does"
1393 );
1394 assert_eq!(reader.calls(), 3);
1395 }
1396
1397 #[tokio::test]
1398 async fn simultaneous_reactive_refreshes_share_one_forced_flight() {
1399 let reader = CountingReader::sequence([success()]);
1400 let coordinator = CredentialReadCoordinator::new(reader.clone());
1401
1402 let (first, second) = tokio::join!(coordinator.refresh(), coordinator.refresh());
1403
1404 assert_eq!(first, success());
1405 assert_eq!(second, success());
1406 assert_eq!(reader.calls(), 1);
1407 }
1408
1409 #[tokio::test]
1410 async fn ordinary_resolution_does_not_inherit_none_from_forced_refresh() {
1411 let reader = CountingReader::blocked();
1412 let coordinator = CredentialReadCoordinator::new(reader.clone());
1413
1414 let release = async {
1415 while reader.calls() == 0 {
1416 tokio::task::yield_now().await;
1417 }
1418 reader.release(Ok(None));
1419 tokio::task::yield_now().await;
1420 reader.release_success(fixture_credential());
1421 };
1422 let (refreshed, resolved, ()) = tokio::join!(
1423 coordinator.refresh(),
1424 coordinator.resolve(CredentialReadMode::Use),
1425 release,
1426 );
1427
1428 assert_eq!(refreshed, Ok(None));
1429 assert_eq!(resolved, success());
1430 assert_eq!(reader.calls(), 2);
1431 }
1432
1433 #[tokio::test]
1434 async fn explicit_retries_after_failed_force_bypass_cached_resolution_and_coalesce() {
1435 let replacement = replacement_credential();
1436 let reader = CachedThenPhysicalReader::new(
1437 fixture_credential(),
1438 [denied(), Ok(Some(replacement.clone()))],
1439 );
1440 let coordinator = CredentialReadCoordinator::new(reader.clone());
1441
1442 assert_eq!(
1443 coordinator.resolve(CredentialReadMode::Use).await,
1444 success()
1445 );
1446 assert_eq!(reader.physical_calls(), 0);
1447 assert_eq!(
1448 coordinator.refresh().await.unwrap_err().kind,
1449 CredentialReadFailureKind::Denied
1450 );
1451
1452 let (first, second) = tokio::join!(
1453 coordinator.resolve(CredentialReadMode::Retry),
1454 coordinator.resolve(CredentialReadMode::Retry),
1455 );
1456
1457 assert_eq!(first, Ok(Some(replacement.clone())));
1458 assert_eq!(second, Ok(Some(replacement)));
1459 assert_eq!(reader.physical_calls(), 2);
1460 }
1461
1462 #[tokio::test]
1463 async fn common_v2_resolution_flight_performs_one_physical_get() {
1464 const CHILD_MARKER: &str = "CAR_AUTH_ONE_GET_CHILD";
1465 if std::env::var(CHILD_MARKER).as_deref() != Ok("1") {
1466 let status = std::process::Command::new(std::env::current_exe().unwrap())
1467 .args([
1468 "--exact",
1469 "credential_read::tests::common_v2_resolution_flight_performs_one_physical_get",
1470 "--nocapture",
1471 "--test-threads=1",
1472 ])
1473 .env(CHILD_MARKER, "1")
1474 .status()
1475 .unwrap();
1476 assert!(status.success(), "isolated one-get assertion failed");
1477 return;
1478 }
1479
1480 let directory = tempfile::tempdir().unwrap();
1481 std::env::set_var("CAR_SECRETS_FILE_DIR", directory.path());
1482 std::env::remove_var(super::super::PARSLEE_ACCESS_TOKEN_KEY);
1483 std::env::remove_var(super::super::PARSLEE_API_BASE_KEY);
1484 super::super::invalidate_access_token_cache();
1485 car_secrets::SecretStore::new()
1486 .publish(
1487 &car_secrets::SecretRef::with_default_service(
1488 car_secrets::PARSLEE_AUTH_STATE_V2_KEY,
1489 ),
1490 &serde_json::json!({
1491 "schema": 2,
1492 "revision": 7,
1493 "generation": 3,
1494 "active": {
1495 "account_id": "one-get-account",
1496 "access_token": "one-get-access",
1497 "expires_at": 9_999_999_999_u64,
1498 "api_base": "https://one-get.example"
1499 },
1500 "accounts": [{
1501 "account_id": "one-get-account",
1502 "access_token": "one-get-access",
1503 "expires_at": 9_999_999_999_u64,
1504 "api_base": "https://one-get.example"
1505 }]
1506 })
1507 .to_string(),
1508 )
1509 .unwrap();
1510
1511 let before = car_secrets::secret_store_activity();
1512 let resolved = CredentialReadCoordinator::new(SystemCredentialReader)
1513 .resolve(CredentialReadMode::Use)
1514 .await
1515 .unwrap()
1516 .unwrap();
1517 let after = car_secrets::secret_store_activity();
1518
1519 assert_eq!(resolved.api_base, "https://one-get.example");
1520 assert_eq!(after.get_attempts - before.get_attempts, 1);
1521 }
1522}