Skip to main content

cli_engine/auth/
dispatcher.rs

1use std::sync::{Arc, RwLock};
2
3use async_trait::async_trait;
4
5use super::{AuthProvider, Credential};
6use crate::{CliCoreError, Result};
7
8/// Routes auth operations to registered providers by name.
9///
10/// Clones share the same provider registry, so provider facades and transport
11/// injectors see later registration or replacement.
12#[derive(Clone, Debug, Default)]
13pub struct Dispatcher {
14    inner: Arc<RwLock<DispatcherInner>>,
15}
16
17#[derive(Clone, Debug, Default)]
18struct DispatcherInner {
19    providers: Vec<(String, Arc<dyn AuthProvider>)>,
20}
21
22/// Status row produced while querying all providers.
23#[derive(Clone, Debug)]
24pub struct StatusEntry {
25    /// Provider name.
26    pub provider: String,
27    /// Environment name.
28    pub env: String,
29    /// Cached credential when status succeeded.
30    pub credential: Option<Credential>,
31    /// Status error text when status failed.
32    pub error: Option<String>,
33}
34
35impl Dispatcher {
36    /// Creates an empty dispatcher.
37    #[must_use]
38    pub fn new() -> Self {
39        Self {
40            inner: Arc::new(RwLock::new(DispatcherInner::default())),
41        }
42    }
43
44    /// Registers or replaces a provider under its [`AuthProvider::name`].
45    pub fn register(&mut self, provider: Arc<dyn AuthProvider>) {
46        let name = provider.name().to_owned();
47        let mut inner = self.write_inner();
48        if let Some((_, existing)) = inner
49            .providers
50            .iter_mut()
51            .find(|(existing_name, _)| existing_name == &name)
52        {
53            *existing = provider;
54            return;
55        }
56        inner.providers.push((name, provider));
57    }
58
59    /// Returns provider names in registration order.
60    #[must_use]
61    pub fn registered_names(&self) -> Vec<String> {
62        self.read_inner()
63            .providers
64            .iter()
65            .map(|(name, _)| name.clone())
66            .collect()
67    }
68
69    /// Gets a credential from a named provider.
70    pub async fn get_credential(
71        &self,
72        name: &str,
73        env: &str,
74        command: &str,
75        tier: &str,
76    ) -> Result<Credential> {
77        self.get(name)?.get_credential(env, command, tier).await
78    }
79
80    /// Clears any cached credential, ignoring logout failures, then authenticates.
81    pub async fn login(&self, name: &str, env: &str) -> Result<Credential> {
82        let provider = self.get(name)?;
83        if let Err(err) = provider.logout(env).await {
84            tracing::debug!(provider = name, error = %err, "ignoring logout error before login");
85        }
86        provider.get_credential(env, "", "").await
87    }
88
89    /// Gets cached credential status from a named provider.
90    pub async fn status(&self, name: &str, env: &str) -> Result<Credential> {
91        self.get(name)?.status(env).await
92    }
93
94    /// Clears cached credentials for a named provider and environment.
95    pub async fn logout(&self, name: &str, env: &str) -> Result<()> {
96        self.get(name)?.logout(env).await
97    }
98
99    /// Queries every provider for every cached environment it reports.
100    pub async fn all_statuses(&self) -> Vec<StatusEntry> {
101        let mut entries = Vec::new();
102        let providers = self.read_inner().providers.clone();
103        for (name, provider) in providers {
104            let Ok(envs) = provider.list_environments().await else {
105                continue;
106            };
107            for env in envs {
108                match provider.status(&env).await {
109                    Ok(credential) => entries.push(StatusEntry {
110                        provider: name.clone(),
111                        env,
112                        credential: Some(credential),
113                        error: None,
114                    }),
115                    Err(err) => entries.push(StatusEntry {
116                        provider: name.clone(),
117                        env,
118                        credential: None,
119                        error: Some(err.to_string()),
120                    }),
121                }
122            }
123        }
124        entries
125    }
126
127    /// Returns an auth-provider facade backed by this dispatcher.
128    #[must_use]
129    pub fn for_provider(&self, name: impl Into<String>) -> SingleProvider {
130        SingleProvider {
131            dispatcher: self.clone(),
132            name: name.into(),
133        }
134    }
135
136    fn get(&self, name: &str) -> Result<Arc<dyn AuthProvider>> {
137        self.read_inner()
138            .providers
139            .iter()
140            .find(|(existing_name, _)| existing_name == name)
141            .map(|(_, provider)| Arc::clone(provider))
142            .ok_or_else(|| CliCoreError::MissingAuthProvider(name.to_owned()))
143    }
144
145    fn read_inner(&self) -> std::sync::RwLockReadGuard<'_, DispatcherInner> {
146        match self.inner.read() {
147            Ok(guard) => guard,
148            Err(poisoned) => poisoned.into_inner(),
149        }
150    }
151
152    fn write_inner(&self) -> std::sync::RwLockWriteGuard<'_, DispatcherInner> {
153        match self.inner.write() {
154            Ok(guard) => guard,
155            Err(poisoned) => poisoned.into_inner(),
156        }
157    }
158}
159
160/// Single-provider facade over a shared [`Dispatcher`].
161#[derive(Clone, Debug)]
162pub struct SingleProvider {
163    dispatcher: Dispatcher,
164    name: String,
165}
166
167#[async_trait]
168impl AuthProvider for SingleProvider {
169    fn name(&self) -> &str {
170        &self.name
171    }
172
173    async fn get_credential(&self, env: &str, command: &str, tier: &str) -> Result<Credential> {
174        self.dispatcher
175            .get_credential(&self.name, env, command, tier)
176            .await
177    }
178
179    async fn status(&self, env: &str) -> Result<Credential> {
180        self.dispatcher.status(&self.name, env).await
181    }
182
183    async fn logout(&self, env: &str) -> Result<()> {
184        self.dispatcher.logout(&self.name, env).await
185    }
186
187    async fn list_environments(&self) -> Result<Vec<String>> {
188        self.dispatcher.get(&self.name)?.list_environments().await
189    }
190}