use clap::{Arg, ArgAction};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use super::Dispatcher;
use crate::{
CliCoreError, CommandContext, CommandResult, CommandSpec, Credential, GroupSpec, Result,
RuntimeCommandSpec, RuntimeGroupSpec, Tier,
};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct AuthLoginResult {
pub provider: String,
pub env: String,
pub identity: String,
pub expires_at: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct AuthStatusEntry {
pub provider: String,
pub env: String,
pub identity: String,
pub expires_at: String,
pub expired: bool,
}
#[must_use]
pub fn auth_command_group(default_provider: &str, registered_names: &[String]) -> RuntimeGroupSpec {
let effective_default = effective_default_provider(default_provider, registered_names);
RuntimeGroupSpec::new(GroupSpec::new("auth", "Manage authentication credentials"))
.with_command(RuntimeCommandSpec::new_with_context(
CommandSpec::new("login", "Authenticate and cache credentials")
.with_system("auth")
.with_tier(Tier::Mutate)
.mutates(true)
.no_auth(true)
.with_arg(provider_arg(&effective_default, registered_names))
.with_arg(Arg::new("env").long("env").value_name("ENV"))
.with_arg(
Arg::new("scope")
.long("scope")
.short('s')
.value_name("SCOPE")
.action(ArgAction::Append)
.help("Additional OAuth scope to request (repeatable, one per flag)"),
),
async |context| {
let provider = string_arg(&context.args, "provider");
let env = env_arg(&context)?;
let scopes = string_vec_arg(&context.args, "scope");
serde_json::to_value(
login_and_build_with_scopes(&context.middleware.auth, &provider, &env, &scopes)
.await?,
)
.map(CommandResult::new)
.map_err(Into::into)
},
))
.with_command(RuntimeCommandSpec::new_with_context(
CommandSpec::new("status", "Show cached credential status")
.with_system("auth")
.no_auth(true)
.with_arg(provider_arg(&effective_default, registered_names))
.with_arg(Arg::new("env").long("env").value_name("ENV")),
async |context| {
let provider = string_arg(&context.args, "provider");
let env = string_arg(&context.args, "env");
status_result(&context.middleware.auth, &provider, &env)
.await
.map(CommandResult::new)
},
))
.with_command(RuntimeCommandSpec::new_with_context(
CommandSpec::new("logout", "Clear cached credentials")
.with_system("auth")
.with_tier(Tier::Mutate)
.mutates(true)
.no_auth(true)
.with_arg(provider_arg(&effective_default, registered_names))
.with_arg(Arg::new("env").long("env").value_name("ENV")),
async |context| {
let provider = string_arg(&context.args, "provider");
let env = env_arg(&context)?;
logout_result(&context.middleware.auth, &provider, &env)
.await
.map(CommandResult::new)
},
))
}
fn effective_default_provider(default_provider: &str, registered_names: &[String]) -> String {
if default_provider.is_empty() {
registered_names.first().cloned().unwrap_or_default()
} else {
default_provider.to_owned()
}
}
fn string_arg(args: &serde_json::Map<String, Value>, name: &str) -> String {
args.get(name)
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned()
}
fn env_arg(context: &CommandContext) -> Result<String> {
if let Some(env) = context.user_args.get("env").and_then(Value::as_str) {
if env.is_empty() {
return Err(missing_env_error());
}
return Ok(env.to_owned());
}
if !context.middleware.env.is_empty() {
return Ok(context.middleware.env.clone());
}
Err(missing_env_error())
}
fn missing_env_error() -> CliCoreError {
CliCoreError::message(
"auth: missing environment; pass --env or configure a default environment",
)
}
fn string_vec_arg(args: &serde_json::Map<String, Value>, name: &str) -> Vec<String> {
match args.get(name) {
Some(Value::Array(items)) => items
.iter()
.filter_map(Value::as_str)
.filter(|value| !value.is_empty())
.map(str::to_owned)
.collect(),
Some(Value::String(value)) if !value.is_empty() => vec![value.clone()],
_ => Vec::new(),
}
}
fn provider_arg(default_provider: &str, registered_names: &[String]) -> Arg {
let names = registered_names.join(", ");
let help = format!("Auth provider name (one of: [{names}])");
let mut arg = Arg::new("provider")
.long("provider")
.value_name("NAME")
.help(help);
if !default_provider.is_empty() {
arg = arg.default_value(default_provider.to_owned());
}
arg
}
pub async fn login_and_build(
dispatcher: &Dispatcher,
provider: &str,
env: &str,
) -> Result<AuthLoginResult> {
login_and_build_with_scopes(dispatcher, provider, env, &[]).await
}
pub async fn login_and_build_with_scopes(
dispatcher: &Dispatcher,
provider: &str,
env: &str,
additional_scopes: &[String],
) -> Result<AuthLoginResult> {
let credential = dispatcher
.login_with_scopes(provider, env, additional_scopes)
.await?;
Ok(AuthLoginResult {
provider: provider.to_owned(),
env: env.to_owned(),
identity: credential.identity,
expires_at: credential.expires_at,
})
}
pub async fn status_result(dispatcher: &Dispatcher, provider: &str, env: &str) -> Result<Value> {
if !provider.is_empty() && !env.is_empty() {
let credential = dispatcher.status(provider, env).await?;
return serde_json::to_value(to_status_entry(provider, env, Some(&credential)))
.map_err(Into::into);
}
let out = dispatcher
.all_statuses()
.await
.iter()
.map(|entry| {
if entry.error.is_some() {
AuthStatusEntry {
provider: entry.provider.clone(),
env: entry.env.clone(),
identity: String::new(),
expires_at: String::new(),
expired: true,
}
} else {
to_status_entry(&entry.provider, &entry.env, entry.credential.as_ref())
}
})
.collect::<Vec<_>>();
serde_json::to_value(out).map_err(Into::into)
}
pub async fn logout_result(dispatcher: &Dispatcher, provider: &str, env: &str) -> Result<Value> {
dispatcher.logout(provider, env).await?;
Ok(json!({
"provider": provider,
"env": env,
"status": "logged out",
}))
}
#[must_use]
pub fn to_status_entry(
provider: &str,
env: &str,
credential: Option<&Credential>,
) -> AuthStatusEntry {
credential.map_or_else(
|| AuthStatusEntry {
provider: provider.to_owned(),
env: env.to_owned(),
identity: String::new(),
expires_at: String::new(),
expired: true,
},
|credential| AuthStatusEntry {
provider: provider.to_owned(),
env: env.to_owned(),
identity: credential.identity.clone(),
expires_at: credential.expires_at.clone(),
expired: credential.is_expired(),
},
)
}