Skip to main content

cli_engine/auth/
commands.rs

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/// Data rendered after a successful `auth login`.
12#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
13pub struct AuthLoginResult {
14    /// Provider used for login.
15    pub provider: String,
16    /// Environment used for login.
17    pub env: String,
18    /// Authenticated identity.
19    pub identity: String,
20    /// Credential expiration timestamp.
21    pub expires_at: String,
22}
23
24/// Data rendered by `auth status`.
25#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
26pub struct AuthStatusEntry {
27    /// Provider name.
28    pub provider: String,
29    /// Environment name.
30    pub env: String,
31    /// Cached identity, empty when missing or unavailable.
32    pub identity: String,
33    /// Credential expiration timestamp, empty when missing or unavailable.
34    pub expires_at: String,
35    /// Whether the cached credential is expired or unavailable.
36    pub expired: bool,
37}
38
39/// Builds the built-in runtime `auth` command group.
40#[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
122/// Runs dispatcher login and converts the credential to renderable output.
123pub 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
137/// Builds the JSON value rendered by `auth status`.
138pub 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
166/// Runs dispatcher logout and builds the renderable result.
167pub 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/// Converts an optional credential into an auth status row.
177#[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}