1use crate::{
5 AzureCliCredential, AzureCliCredentialOptions, AzureDeveloperCliCredential,
6 AzureDeveloperCliCredentialOptions, Executor,
7};
8use azure_core::{
9 credentials::{AccessToken, TokenCredential, TokenRequestOptions},
10 error::{Error, ErrorKind},
11};
12use std::{
13 any::type_name,
14 fmt,
15 sync::{
16 atomic::{AtomicUsize, Ordering},
17 Arc,
18 },
19};
20
21#[derive(Clone, Default)]
23pub struct DeveloperToolsCredentialOptions {
24 pub executor: Option<Arc<dyn Executor>>,
29}
30
31impl fmt::Debug for DeveloperToolsCredentialOptions {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.debug_struct(type_name::<Self>()).finish_non_exhaustive()
34 }
35}
36
37pub struct DeveloperToolsCredential {
46 sources: Vec<Arc<dyn TokenCredential>>,
47 cached_source_index: AtomicUsize,
49}
50
51impl DeveloperToolsCredential {
52 pub fn new(
57 options: Option<DeveloperToolsCredentialOptions>,
58 ) -> azure_core::Result<Arc<DeveloperToolsCredential>> {
59 let options = options.unwrap_or_default();
60 let sources: Vec<Arc<dyn TokenCredential>> = vec![
61 AzureCliCredential::new(Some(AzureCliCredentialOptions {
62 executor: options.executor.clone(),
63 ..Default::default()
64 }))?,
65 AzureDeveloperCliCredential::new(Some(AzureDeveloperCliCredentialOptions {
66 executor: options.executor,
67 ..Default::default()
68 }))?,
69 ];
70 Ok(Arc::new(Self {
71 sources,
72 cached_source_index: AtomicUsize::new(usize::MAX),
73 }))
74 }
75
76 #[cfg(test)]
77 pub(crate) fn new_with_sources(
78 sources: Vec<Arc<dyn TokenCredential>>,
79 ) -> azure_core::Result<Arc<DeveloperToolsCredential>> {
80 Ok(Arc::new(Self {
81 sources,
82 cached_source_index: AtomicUsize::new(usize::MAX),
83 }))
84 }
85}
86
87impl fmt::Debug for DeveloperToolsCredential {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 f.debug_struct(type_name::<Self>()).finish_non_exhaustive()
90 }
91}
92
93#[async_trait::async_trait]
94impl TokenCredential for DeveloperToolsCredential {
95 async fn get_token(
96 &self,
97 scopes: &[&str],
98 options: Option<TokenRequestOptions<'_>>,
99 ) -> azure_core::Result<AccessToken> {
100 let cached_index = self.cached_source_index.load(Ordering::Relaxed);
101 if cached_index != usize::MAX {
102 if let Some(source) = self.sources.get(cached_index) {
103 return source.get_token(scopes, options).await;
104 }
105 panic!("DeveloperToolsCredential source index {cached_index} is out of bounds")
107 }
108
109 let mut errors = Vec::new();
110 for (index, source) in self.sources.iter().enumerate() {
111 match source.get_token(scopes, options.clone()).await {
112 Ok(token) => {
113 self.cached_source_index.store(index, Ordering::Relaxed);
114 return Ok(token);
115 }
116 Err(error) => errors.push(error),
117 }
118 }
119 Err(Error::with_message_fn(ErrorKind::Credential, || {
120 format!(
121 "Multiple errors were encountered while attempting to authenticate:\n{}",
122 format_aggregate_error(&errors)
123 )
124 }))
125 }
126}
127
128fn format_aggregate_error(errors: &[Error]) -> String {
129 use std::error::Error;
130 errors
131 .iter()
132 .map(|e| {
133 let mut current: Option<&dyn Error> = Some(e);
134 let mut stack = vec![];
135 while let Some(err) = current.take() {
136 stack.push(err.to_string());
137 current = err.source();
138 }
139 stack.join(" - ")
140 })
141 .collect::<Vec<String>>()
142 .join("\n")
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::tests::MockExecutor;
149 use azure_core::credentials::AccessToken;
150 use std::sync::atomic::{AtomicUsize, Ordering};
151 use std::time::{Duration, SystemTime};
152
153 #[derive(Debug)]
154 struct MockCredential {
155 call_count: AtomicUsize,
156 id: String,
157 succeed: bool,
158 }
159
160 impl MockCredential {
161 fn new(id: &str, succeed: bool) -> Arc<Self> {
162 Arc::new(Self {
163 call_count: AtomicUsize::new(0),
164 id: id.to_string(),
165 succeed,
166 })
167 }
168
169 fn call_count(&self) -> usize {
170 self.call_count.load(Ordering::SeqCst)
171 }
172 }
173
174 #[async_trait::async_trait]
175 impl TokenCredential for MockCredential {
176 async fn get_token(
177 &self,
178 _scopes: &[&str],
179 _options: Option<TokenRequestOptions<'_>>,
180 ) -> azure_core::Result<AccessToken> {
181 self.call_count.fetch_add(1, Ordering::SeqCst);
182 if self.succeed {
183 Ok(AccessToken {
184 token: self.id.clone().into(),
185 expires_on: (SystemTime::now() + Duration::from_secs(3600)).into(),
186 })
187 } else {
188 Err(Error::with_message_fn(ErrorKind::Credential, || {
189 format!("{} failed", self.id)
190 }))
191 }
192 }
193 }
194
195 #[tokio::test]
196 async fn cached_credential() {
197 let mock1 = MockCredential::new("mock1", false);
198 let mock2 = MockCredential::new("mock2", false);
199 let mock3 = MockCredential::new("mock3", true);
200 let mock4 = MockCredential::new("mock4", true);
201 let sources: Vec<Arc<dyn TokenCredential>> =
202 vec![mock1.clone(), mock2.clone(), mock3.clone(), mock4.clone()];
203
204 let credential = DeveloperToolsCredential::new_with_sources(sources).unwrap();
205
206 for i in 1..=5 {
207 let token = credential
208 .get_token(&["scope"], None)
209 .await
210 .expect("authentication success");
211 assert_eq!(token.token.secret(), "mock3");
212 assert_eq!(mock1.call_count(), 1);
213 assert_eq!(mock2.call_count(), 1);
214 assert_eq!(mock3.call_count(), i);
215 assert_eq!(mock4.call_count(), 0);
216 }
217 }
218
219 #[tokio::test]
220 async fn error_message() {
221 let mock1 = MockCredential::new("mock1", false);
222 let mock2 = MockCredential::new("mock2", false);
223 let mock3 = MockCredential::new("mock3", false);
224 let sources: Vec<Arc<dyn TokenCredential>> =
225 vec![mock1.clone(), mock2.clone(), mock3.clone()];
226
227 let credential = DeveloperToolsCredential::new_with_sources(sources).unwrap();
228
229 let error_msg = credential
230 .get_token(&["scope"], None)
231 .await
232 .expect_err("authentication error")
233 .to_string();
234
235 assert_eq!(mock1.call_count(), 1);
236 assert_eq!(mock2.call_count(), 1);
237 assert_eq!(mock3.call_count(), 1);
238 assert_eq!(
239 "Multiple errors were encountered while attempting to authenticate:\nmock1 failed\nmock2 failed\nmock3 failed",
240 error_msg
241 );
242 }
243
244 #[tokio::test]
245 async fn executor() {
246 let err = std::io::Error::other("something went wrong");
247 let executor = MockExecutor::with_error(err);
248 let options = DeveloperToolsCredentialOptions {
249 executor: Some(executor.clone()),
250 };
251 let err = DeveloperToolsCredential::new(Some(options))
252 .expect("valid credential")
253 .get_token(&["scope"], None)
254 .await
255 .expect_err("expected error");
256 assert_eq!(
257 "Multiple errors were encountered while attempting to authenticate:\n\
258 AzureCliCredential authentication failed. other error error: something went wrong\n\
259 To troubleshoot, visit https://aka.ms/azsdk/rust/identity/troubleshoot#azure-cli - other error error: something went wrong - something went wrong\n\
260 AzureDeveloperCliCredential authentication failed. other error error: something went wrong\n\
261 To troubleshoot, visit https://aka.ms/azsdk/rust/identity/troubleshoot#azd - other error error: something went wrong - something went wrong",
262 err.to_string()
263 );
264 assert_eq!(
265 2,
266 executor.call_count(),
267 "Executor should have been called once for each inner credential"
268 );
269 }
270}