ez_token/cli/args.rs
1use clap::{Args, Parser, Subcommand};
2use serde::{Deserialize, Serialize};
3
4/// The main CLI entry point for `ez-token`.
5///
6/// Parses command-line arguments and dispatches to the appropriate
7/// subcommand. If no subcommand is given, the default profile is used
8/// to run an interactive login via the PKCE flow.
9#[derive(Parser, PartialEq, Debug)]
10#[command(author, version, about, long_about = None)]
11pub struct Cli {
12 /// The configuration profile to use.
13 ///
14 /// Profiles store Tenant ID, Client ID, and Scopes so you don't
15 /// have to re-enter them every time. Defaults to `"default"`.
16 ///
17 /// Example: `ez-token --profile prod login`
18 #[arg(long, global = true, default_value = "default")]
19 pub profile: String,
20
21 /// The subcommand to execute.
22 #[command(subcommand)]
23 pub command: Option<Commands>,
24}
25
26/// The identity provider to authenticate against.
27#[derive(Clone, Default, Serialize, PartialEq, Deserialize, Debug, clap::ValueEnum)]
28pub enum ProviderKind {
29 /// Microsoft Entra ID (Azure AD). Requires `--tenant-id`.
30 #[default]
31 Microsoft,
32 /// Auth0. Requires `--domain` (e.g. `my-org.eu.auth0.com`).
33 Auth0,
34}
35
36/// Shared authentication arguments used across multiple subcommands.
37///
38/// These fields are optional — any value not provided via the command line
39/// will be resolved from the active profile, or prompted interactively.
40#[derive(Args, Default, PartialEq, Debug)]
41pub struct AuthArgs {
42 /// The identity provider to authenticate against.
43 ///
44 /// If not provided, resolved from the active profile or prompted interactively.
45 /// Accepted values: `microsoft`, `auth0`
46 #[arg(long)]
47 pub provider: Option<ProviderKind>,
48
49 /// Tenant ID — required for Microsoft (GUID, domain, or `"common"`).
50 #[arg(long)]
51 pub tenant_id: Option<String>,
52
53 /// Domain — required for Auth0 (e.g. `my-org.eu.auth0.com`).
54 #[arg(long)]
55 pub domain: Option<String>,
56
57 /// Audience — required for Auth0 (e.g. `api://ez-token`).
58 ///
59 /// Tells Auth0 which API the token is intended for.
60 /// Ignored for Microsoft flows.
61 #[arg(long)]
62 pub audience: Option<String>,
63
64 /// The Application (Client) ID.
65 #[arg(long)]
66 pub client_id: Option<String>,
67
68 /// Space-separated list of OAuth2 scopes to request.
69 ///
70 /// Example: `"read:ez write:ez"`
71 /// For Microsoft M2M flows use: `"api://YOUR_API/.default"`
72 #[arg(long)]
73 pub scopes: Option<String>,
74}
75
76/// Available subcommands for `ez-token`.
77#[derive(Subcommand, PartialEq, Debug)]
78pub enum Commands {
79 /// Authenticate interactively via the browser using the PKCE flow.
80 ///
81 /// Opens your default browser to complete the OAuth2 Authorization
82 /// Code flow with PKCE. The resulting access token is copied to
83 /// your clipboard automatically.
84 ///
85 /// Example: `ez-token login --tenant-id common --client-id YOUR_ID`
86 Login {
87 /// The authentication parameters.
88 #[command(flatten)]
89 auth: AuthArgs,
90
91 /// The local port to use for the OAuth2 callback server.
92 #[arg(long, default_value = "3000")]
93 port: u16,
94 },
95
96 /// Fetch a token using the Client Credentials (machine-to-machine) flow.
97 ///
98 /// Does not open a browser. Authenticates using a client secret,
99 /// suitable for services, scripts, and CI/CD pipelines.
100 ///
101 /// Example: `ez-token m2m --client-secret YOUR_SECRET`
102 ///
103 /// Note: The client secret is never saved to disk. If not provided
104 /// via `--client-secret`, it will be prompted securely.
105 M2m {
106 /// The authentication parameters.
107 #[command(flatten)]
108 auth: AuthArgs,
109
110 /// The client secret for the registered application.
111 ///
112 /// If omitted, the value will be prompted interactively
113 /// and is never persisted to disk.
114 #[arg(long)]
115 client_secret: Option<String>,
116 },
117
118 /// Manage configuration profiles.
119 ///
120 /// Profiles store Tenant ID, Client ID, and Scopes so you don't
121 /// have to re-enter them on every invocation.
122 ///
123 /// Example: `ez-token config set --tenant-id common --client-id YOUR_ID`
124 Config {
125 /// The specific configuration action to perform.
126 #[command(subcommand)]
127 action: ConfigAction,
128 },
129}
130
131/// Subcommands for managing configuration profiles.
132#[derive(Subcommand, PartialEq, Debug)]
133pub enum ConfigAction {
134 /// Save or update the active profile's configuration.
135 ///
136 /// Only the provided fields are updated — omitted fields retain
137 /// their existing values.
138 ///
139 /// Example: `ez-token --profile prod config set --tenant-id YOUR_TENANT`
140 Set {
141 /// The configuration parameters to update.
142 #[command(flatten)]
143 auth: AuthArgs,
144 },
145
146 /// Display the active profile's current configuration.
147 ///
148 /// Example: `ez-token --profile prod config show`
149 Show,
150
151 /// List all saved profiles.
152 ///
153 /// The active profile is highlighted with an asterisk (*).
154 List,
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use clap::CommandFactory;
161
162 #[test]
163 fn verify_cli() {
164 Cli::command().debug_assert();
165 }
166
167 #[test]
168 fn test_no_command_uses_default_profile() {
169 let cli = Cli::try_parse_from(["ez-token"]).unwrap();
170
171 assert_eq!(
172 cli,
173 Cli {
174 profile: "default".to_string(),
175 command: None,
176 }
177 );
178 }
179
180 #[test]
181 fn test_global_profile_flag() {
182 let cli = Cli::try_parse_from(["ez-token", "--profile", "prod", "config", "show"]).unwrap();
183
184 assert_eq!(
185 cli,
186 Cli {
187 profile: "prod".to_string(),
188 command: Some(Commands::Config {
189 action: ConfigAction::Show,
190 }),
191 }
192 );
193 }
194
195 #[test]
196 fn test_login_command_with_defaults() {
197 let cli = Cli::try_parse_from(["ez-token", "login"]).unwrap();
198
199 assert_eq!(
200 cli.command,
201 Some(Commands::Login {
202 auth: AuthArgs::default(),
203 port: 3000,
204 })
205 );
206 }
207
208 #[test]
209 fn test_login_command_with_auth_args_and_custom_port() {
210 let cli = Cli::try_parse_from([
211 "ez-token",
212 "login",
213 "--provider",
214 "auth0",
215 "--domain",
216 "my-org.eu.auth0.com",
217 "--client-id",
218 "12345",
219 "--port",
220 "8080",
221 ])
222 .unwrap();
223
224 assert_eq!(
225 cli.command,
226 Some(Commands::Login {
227 auth: AuthArgs {
228 provider: Some(ProviderKind::Auth0),
229 domain: Some("my-org.eu.auth0.com".to_string()),
230 client_id: Some("12345".to_string()),
231 ..Default::default()
232 },
233 port: 8080,
234 })
235 );
236 }
237
238 #[test]
239 fn test_m2m_command() {
240 let cli = Cli::try_parse_from([
241 "ez-token",
242 "m2m",
243 "--tenant-id",
244 "common",
245 "--client-secret",
246 "super-secret",
247 ])
248 .unwrap();
249
250 assert_eq!(
251 cli.command,
252 Some(Commands::M2m {
253 auth: AuthArgs {
254 tenant_id: Some("common".to_string()),
255 ..Default::default()
256 },
257 client_secret: Some("super-secret".to_string()),
258 })
259 );
260 }
261
262 #[test]
263 fn test_config_set_command() {
264 let cli = Cli::try_parse_from([
265 "ez-token",
266 "--profile",
267 "dev",
268 "config",
269 "set",
270 "--scopes",
271 "api://ez/.default",
272 ])
273 .unwrap();
274
275 assert_eq!(cli.profile, "dev");
276 assert_eq!(
277 cli.command,
278 Some(Commands::Config {
279 action: ConfigAction::Set {
280 auth: AuthArgs {
281 scopes: Some("api://ez/.default".to_string()),
282 ..Default::default()
283 }
284 }
285 })
286 );
287 }
288
289 #[test]
290 fn test_config_list_command() {
291 let cli = Cli::try_parse_from(["ez-token", "config", "list"]).unwrap();
292
293 assert_eq!(
294 cli.command,
295 Some(Commands::Config {
296 action: ConfigAction::List,
297 })
298 );
299 }
300}