1use crate::{
2 cli::{
3 args::{AuthArgs, ProviderKind},
4 history::{HistoryKey, HistoryManager, file::FileHistoryManager},
5 },
6 config::cli_config::Profile,
7 services::authentication::urls::IdentityProvider,
8};
9use dialoguer::{Input, Select, theme::ColorfulTheme};
10use miette::{IntoDiagnostic, Result};
11
12#[derive(Debug, PartialEq)]
21pub struct AuthParams {
22 pub provider: IdentityProvider,
24
25 pub client_id: String,
27
28 pub scopes: Vec<String>,
33}
34
35impl AuthParams {
36 pub fn new(profile: &Profile, args: AuthArgs, default_scope_prompt: &str) -> Result<Self> {
50 let theme = ColorfulTheme::default();
51 let manager = FileHistoryManager;
52
53 let provider_kind = match args.provider.or(profile.provider.clone()) {
54 Some(p) => p,
55 None => Self::prompt_provider(&theme)?,
56 };
57
58 let provider = match provider_kind {
59 ProviderKind::Microsoft => {
60 let tenant_id = match args.tenant_id.or(profile.tenant_id.clone()) {
61 Some(t) => t,
62 None => Self::prompt_or_history(
63 &theme,
64 "Enter Tenant ID",
65 HistoryKey::Tenant,
66 None,
67 &manager,
68 )?,
69 };
70 IdentityProvider::Microsoft { tenant_id }
71 }
72 ProviderKind::Auth0 => {
73 let domain = match args.domain.or(profile.domain.clone()) {
74 Some(d) => d,
75 None => Self::prompt_or_history(
76 &theme,
77 "Enter Auth0 Domain (e.g. my-org.eu.auth0.com)",
78 HistoryKey::Domain,
79 None,
80 &manager,
81 )?,
82 };
83 let audience = match args.audience.or(profile.audience.clone()) {
84 Some(a) => a,
85 None => Self::prompt_or_history(
86 &theme,
87 "Enter Auth0 Audience (e.g. api://ez-token)",
88 HistoryKey::Audience,
89 None,
90 &manager,
91 )?,
92 };
93 IdentityProvider::Auth0 { domain, audience }
94 }
95 };
96
97 let client_id = match args.client_id.or(profile.client_id.clone()) {
98 Some(c) => c,
99 None => Self::prompt_or_history(
100 &theme,
101 "Enter Client ID",
102 HistoryKey::Client,
103 None,
104 &manager,
105 )?,
106 };
107
108 let scopes_str = match args.scopes.or(profile.default_scopes.clone()) {
109 Some(s) => s,
110 None => Self::prompt_or_history(
111 &theme,
112 &format!("Enter Scopes (e.g. {})", default_scope_prompt),
113 HistoryKey::Scopes,
114 Some(default_scope_prompt),
115 &manager,
116 )?,
117 };
118
119 let scopes = scopes_str
120 .split_whitespace()
121 .map(|s| s.to_string())
122 .collect();
123
124 Ok(Self {
125 provider,
126 client_id,
127 scopes,
128 })
129 }
130
131 fn prompt_or_history(
141 theme: &ColorfulTheme,
142 prompt: &str,
143 key: HistoryKey,
144 default: Option<&str>,
145 manager: &dyn HistoryManager,
146 ) -> Result<String> {
147 let mut history = manager.load(key);
148 let mut input = Input::with_theme(theme)
149 .with_prompt(prompt)
150 .history_with(&mut history);
151
152 if let Some(d) = default {
153 input = input.default(d.to_string());
154 }
155
156 let result = input.interact_text().into_diagnostic()?;
157 manager.save(key, &history);
158 Ok(result)
159 }
160
161 fn prompt_provider(theme: &ColorfulTheme) -> Result<ProviderKind> {
166 let options = vec!["Microsoft Entra ID (Azure AD)", "Auth0"];
167 let selection = Select::with_theme(theme)
168 .with_prompt("Select Identity Provider")
169 .items(&options)
170 .default(0)
171 .interact()
172 .into_diagnostic()?;
173
174 match selection {
175 0 => Ok(ProviderKind::Microsoft),
176 _ => Ok(ProviderKind::Auth0),
177 }
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 fn empty_profile() -> Profile {
187 Profile {
188 provider: None,
189 tenant_id: None,
190 domain: None,
191 audience: None,
192 client_id: None,
193 default_scopes: None,
194 }
195 }
196
197 #[test]
198 fn test_cli_args_override_profile_microsoft() {
199 let profile = Profile {
200 provider: Some(ProviderKind::Auth0),
201 tenant_id: Some("profile-tenant".to_string()),
202 client_id: Some("profile-client".to_string()),
203 default_scopes: Some("profile.read".to_string()),
204 ..empty_profile()
205 };
206
207 let args = AuthArgs {
208 provider: Some(ProviderKind::Microsoft),
209 tenant_id: Some("cli-tenant".to_string()),
210 client_id: Some("cli-client".to_string()),
211 scopes: Some("cli.read cli.write".to_string()),
212 ..AuthArgs::default()
213 };
214
215 let params = AuthParams::new(&profile, args, "default.scope").unwrap();
216
217 assert_eq!(
218 params,
219 AuthParams {
220 provider: IdentityProvider::Microsoft {
221 tenant_id: "cli-tenant".to_string(),
222 },
223 client_id: "cli-client".to_string(),
224 scopes: vec!["cli.read".to_string(), "cli.write".to_string()],
225 }
226 );
227 }
228
229 #[test]
230 fn test_profile_fallback_when_cli_args_missing_auth0() {
231 let profile = Profile {
232 provider: Some(ProviderKind::Auth0),
233 domain: Some("my-org.auth0.com".to_string()),
234 audience: Some("api://my-api".to_string()),
235 client_id: Some("profile-client".to_string()),
236 default_scopes: Some("openid profile".to_string()),
237 ..empty_profile()
238 };
239
240 let args = AuthArgs::default();
241
242 let params = AuthParams::new(&profile, args, "default.scope").unwrap();
243
244 assert_eq!(
245 params,
246 AuthParams {
247 provider: IdentityProvider::Auth0 {
248 domain: "my-org.auth0.com".to_string(),
249 audience: "api://my-api".to_string(),
250 },
251 client_id: "profile-client".to_string(),
252 scopes: vec!["openid".to_string(), "profile".to_string()],
253 }
254 );
255 }
256
257 #[test]
258 fn test_mixed_resolution() {
259 let profile = Profile {
260 provider: Some(ProviderKind::Microsoft),
261 tenant_id: Some("common".to_string()),
262 ..empty_profile()
263 };
264
265 let args = AuthArgs {
266 client_id: Some("cli-client".to_string()),
267 scopes: Some("api://ez/.default".to_string()),
268 ..AuthArgs::default()
269 };
270
271 let params = AuthParams::new(&profile, args, "default").unwrap();
272
273 assert_eq!(
274 params,
275 AuthParams {
276 provider: IdentityProvider::Microsoft {
277 tenant_id: "common".to_string(),
278 },
279 client_id: "cli-client".to_string(),
280 scopes: vec!["api://ez/.default".to_string()],
281 }
282 );
283 }
284
285 #[test]
286 fn test_scopes_whitespace_splitting() {
287 let profile = empty_profile();
288
289 let args = AuthArgs {
290 provider: Some(ProviderKind::Microsoft),
291 tenant_id: Some("common".to_string()),
292 client_id: Some("12345".to_string()),
293
294 scopes: Some("scope1 scope2\tscope3".to_string()),
295 ..AuthArgs::default()
296 };
297
298 let params = AuthParams::new(&profile, args, "default").unwrap();
299
300 assert_eq!(
301 params.scopes,
302 vec![
303 "scope1".to_string(),
304 "scope2".to_string(),
305 "scope3".to_string()
306 ]
307 );
308 }
309}