1use clap::Arg;
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4
5use super::Dispatcher;
6use crate::{
7 CommandResult, CommandSpec, Credential, GroupSpec, Result, RuntimeCommandSpec,
8 RuntimeGroupSpec, Tier,
9};
10
11#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
13pub struct AuthLoginResult {
14 pub provider: String,
16 pub env: String,
18 pub identity: String,
20 pub expires_at: String,
22}
23
24#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
26pub struct AuthStatusEntry {
27 pub provider: String,
29 pub env: String,
31 pub identity: String,
33 pub expires_at: String,
35 pub expired: bool,
37}
38
39#[must_use]
41pub fn auth_command_group(default_provider: &str, registered_names: &[String]) -> RuntimeGroupSpec {
42 let effective_default = effective_default_provider(default_provider, registered_names);
43 RuntimeGroupSpec::new(GroupSpec::new("auth", "Manage authentication credentials"))
44 .with_command(RuntimeCommandSpec::new_with_context(
45 CommandSpec::new("login", "Authenticate and cache credentials")
46 .with_system("auth")
47 .with_tier(Tier::Mutate)
48 .mutates(true)
49 .no_auth(true)
50 .with_arg(provider_arg(&effective_default, registered_names))
51 .with_arg(Arg::new("env").long("env").value_name("ENV").required(true)),
52 async |context| {
53 let provider = string_arg(&context.args, "provider");
54 let env = string_arg(&context.args, "env");
55 serde_json::to_value(
56 login_and_build(&context.middleware.auth, &provider, &env).await?,
57 )
58 .map(CommandResult::new)
59 .map_err(Into::into)
60 },
61 ))
62 .with_command(RuntimeCommandSpec::new_with_context(
63 CommandSpec::new("status", "Show cached credential status")
64 .with_system("auth")
65 .no_auth(true)
66 .with_arg(provider_arg(&effective_default, registered_names))
67 .with_arg(Arg::new("env").long("env").value_name("ENV")),
68 async |context| {
69 let provider = string_arg(&context.args, "provider");
70 let env = string_arg(&context.args, "env");
71 status_result(&context.middleware.auth, &provider, &env)
72 .await
73 .map(CommandResult::new)
74 },
75 ))
76 .with_command(RuntimeCommandSpec::new_with_context(
77 CommandSpec::new("logout", "Clear cached credentials")
78 .with_system("auth")
79 .with_tier(Tier::Mutate)
80 .mutates(true)
81 .no_auth(true)
82 .with_arg(provider_arg(&effective_default, registered_names))
83 .with_arg(Arg::new("env").long("env").value_name("ENV").required(true)),
84 async |context| {
85 let provider = string_arg(&context.args, "provider");
86 let env = string_arg(&context.args, "env");
87 logout_result(&context.middleware.auth, &provider, &env)
88 .await
89 .map(CommandResult::new)
90 },
91 ))
92}
93
94fn effective_default_provider(default_provider: &str, registered_names: &[String]) -> String {
95 if default_provider.is_empty() {
96 registered_names.first().cloned().unwrap_or_default()
97 } else {
98 default_provider.to_owned()
99 }
100}
101
102fn string_arg(args: &serde_json::Map<String, Value>, name: &str) -> String {
103 args.get(name)
104 .and_then(Value::as_str)
105 .unwrap_or_default()
106 .to_owned()
107}
108
109fn provider_arg(default_provider: &str, registered_names: &[String]) -> Arg {
110 let names = registered_names.join(", ");
111 let help = format!("Auth provider name (one of: [{names}])");
112 let mut arg = Arg::new("provider")
113 .long("provider")
114 .value_name("NAME")
115 .help(help);
116 if !default_provider.is_empty() {
117 arg = arg.default_value(default_provider.to_owned());
118 }
119 arg
120}
121
122pub async fn login_and_build(
124 dispatcher: &Dispatcher,
125 provider: &str,
126 env: &str,
127) -> Result<AuthLoginResult> {
128 let credential = dispatcher.login(provider, env).await?;
129 Ok(AuthLoginResult {
130 provider: provider.to_owned(),
131 env: env.to_owned(),
132 identity: credential.identity,
133 expires_at: credential.expires_at,
134 })
135}
136
137pub async fn status_result(dispatcher: &Dispatcher, provider: &str, env: &str) -> Result<Value> {
139 if !provider.is_empty() && !env.is_empty() {
140 let credential = dispatcher.status(provider, env).await?;
141 return serde_json::to_value(to_status_entry(provider, env, Some(&credential)))
142 .map_err(Into::into);
143 }
144
145 let out = dispatcher
146 .all_statuses()
147 .await
148 .iter()
149 .map(|entry| {
150 if entry.error.is_some() {
151 AuthStatusEntry {
152 provider: entry.provider.clone(),
153 env: entry.env.clone(),
154 identity: String::new(),
155 expires_at: String::new(),
156 expired: true,
157 }
158 } else {
159 to_status_entry(&entry.provider, &entry.env, entry.credential.as_ref())
160 }
161 })
162 .collect::<Vec<_>>();
163 serde_json::to_value(out).map_err(Into::into)
164}
165
166pub async fn logout_result(dispatcher: &Dispatcher, provider: &str, env: &str) -> Result<Value> {
168 dispatcher.logout(provider, env).await?;
169 Ok(json!({
170 "provider": provider,
171 "env": env,
172 "status": "logged out",
173 }))
174}
175
176#[must_use]
178pub fn to_status_entry(
179 provider: &str,
180 env: &str,
181 credential: Option<&Credential>,
182) -> AuthStatusEntry {
183 credential.map_or_else(
184 || AuthStatusEntry {
185 provider: provider.to_owned(),
186 env: env.to_owned(),
187 identity: String::new(),
188 expires_at: String::new(),
189 expired: true,
190 },
191 |credential| AuthStatusEntry {
192 provider: provider.to_owned(),
193 env: env.to_owned(),
194 identity: credential.identity.clone(),
195 expires_at: credential.expires_at.clone(),
196 expired: credential.is_expired(),
197 },
198 )
199}