#![allow(dead_code)]
use anyhow::{Context, Result};
use clap::Args;
use colored::*;
use std::collections::HashMap;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::core::api::ApiError;
use crate::core::spec::UnifiedSpec;
#[derive(Debug, Args)]
pub struct ConnectCommand {
pub scheme: Option<String>,
#[arg(long, value_enum)]
pub auth_type: Option<AuthType>,
#[arg(long)]
pub non_interactive: bool,
#[arg(long)]
pub discover: bool,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub keychain: bool,
#[arg(long)]
pub spec: Option<String>,
pub env: Option<String>,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum AuthType {
ApiKey,
Bearer,
Basic,
OAuth2,
OpenIdConnect,
MutualTls,
}
impl ConnectCommand {
pub async fn execute(&self) -> Result<()> {
println!("{}", "Authentication Setup".bold().cyan());
println!("{}", "═".repeat(60).cyan());
let spec = self.load_spec().await.ok();
let schemes = spec.as_ref().map(|s| &s.security_schemes);
let scheme_name = if let Some(name) = &self.scheme {
name.clone()
} else {
self.select_scheme(schemes)?
};
let scheme_details = schemes.and_then(|s| s.get(&scheme_name));
if self.non_interactive {
self.configure_non_interactive(&scheme_name, scheme_details)
.await?;
} else {
self.configure_interactive(&scheme_name, scheme_details)
.await?;
}
println!(
"\n{} Authentication configured successfully!",
"✓".green().bold()
);
let env_name = self.env.as_deref().unwrap_or("local");
let env_file = if env_name == "local" {
".env.local".to_string()
} else if PathBuf::from("env").exists() {
format!("env/.env.{}", env_name)
} else {
format!(".env.{}", env_name)
};
println!(
" Credentials saved to {} (use {})",
env_file.green(),
format!("${}_*", scheme_name.to_uppercase()).cyan()
);
self.print_next_steps(&scheme_name);
Ok(())
}
async fn load_spec(&self) -> Result<UnifiedSpec> {
let spec_path = self
.spec
.as_ref()
.map(Path::new)
.or_else(|| {
for path in &[
"openapi.yaml",
"openapi.json",
"swagger.yaml",
"swagger.json",
] {
if Path::new(path).exists() {
return Some(Path::new(path));
}
}
None
})
.context("No OpenAPI specification found")?;
UnifiedSpec::from_file(spec_path)
}
fn select_scheme(
&self,
schemes: Option<&HashMap<String, crate::core::spec::UnifiedSecurityScheme>>,
) -> Result<String> {
if let Some(schemes) = schemes {
if schemes.is_empty() {
return Err(ApiError::AuthError(
"No authentication schemes found in specification".to_string(),
)
.into());
}
if schemes.len() == 1 {
return Ok(schemes.keys().next().unwrap().clone());
}
println!("\nAvailable authentication schemes:");
let mut options: Vec<_> = schemes.keys().collect();
options.sort();
for (i, name) in options.iter().enumerate() {
println!(" {}. {}", i + 1, name.bold());
}
print!("\nSelect scheme (1-{}): ", options.len());
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let index: usize = input.trim().parse().context("Invalid selection")?;
if index == 0 || index > options.len() {
return Err(ApiError::ValidationError("Invalid selection".to_string()).into());
}
Ok(options[index - 1].to_string())
} else {
return Err(ApiError::ValidationError(
"Please specify a scheme name with --scheme".to_string(),
)
.into());
}
}
async fn configure_interactive(
&self,
scheme_name: &str,
details: Option<&crate::core::spec::UnifiedSecurityScheme>,
) -> Result<()> {
let auth_type = self.determine_auth_type(details)?;
match auth_type {
AuthType::ApiKey => self.setup_api_key_interactive(scheme_name, details).await,
AuthType::Bearer => self.setup_bearer_interactive(scheme_name, details).await,
AuthType::Basic => self.setup_basic_interactive(scheme_name).await,
AuthType::OAuth2 => self.setup_oauth2_interactive(scheme_name, details).await,
AuthType::OpenIdConnect => self.setup_oidc_interactive(scheme_name, details).await,
AuthType::MutualTls => self.setup_mtls_interactive(scheme_name).await,
}
}
async fn configure_non_interactive(
&self,
scheme_name: &str,
details: Option<&crate::core::spec::UnifiedSecurityScheme>,
) -> Result<()> {
let auth_type = self.determine_auth_type(details)?;
match auth_type {
AuthType::ApiKey => self.setup_api_key_env(scheme_name, details),
AuthType::Bearer => self.setup_bearer_env(scheme_name),
AuthType::Basic => self.setup_basic_env(scheme_name),
AuthType::OAuth2 => self.setup_oauth2_env(scheme_name),
AuthType::OpenIdConnect => self.setup_oidc_env(scheme_name),
AuthType::MutualTls => self.setup_mtls_env(scheme_name),
}
}
fn determine_auth_type(
&self,
details: Option<&crate::core::spec::UnifiedSecurityScheme>,
) -> Result<AuthType> {
if let Some(auth_type) = self.auth_type {
return Ok(auth_type);
}
if let Some(details) = details {
match details.scheme_type.as_str() {
"apiKey" => Ok(AuthType::ApiKey),
"http" => {
if details.scheme.as_deref() == Some("bearer") {
Ok(AuthType::Bearer)
} else if details.scheme.as_deref() == Some("basic") {
Ok(AuthType::Basic)
} else {
Ok(AuthType::Bearer) }
}
"oauth2" => Ok(AuthType::OAuth2),
"openIdConnect" => Ok(AuthType::OpenIdConnect),
"mutualTLS" => Ok(AuthType::MutualTls),
_ => Err(ApiError::ValidationError(format!(
"Unknown auth type: {}",
details.scheme_type
))
.into()),
}
} else {
Err(ApiError::ValidationError(
"Cannot determine auth type. Please specify with --auth-type".to_string(),
)
.into())
}
}
async fn setup_api_key_interactive(
&self,
scheme_name: &str,
details: Option<&crate::core::spec::UnifiedSecurityScheme>,
) -> Result<()> {
println!("\n{}", "API Key Configuration".yellow());
let location = details
.and_then(|d| d.location.as_deref())
.unwrap_or("header");
let param_name = details
.and_then(|d| d.name.as_deref())
.unwrap_or("X-API-Key");
println!("Location: {}", location.bold());
println!("Parameter: {}", param_name.bold());
let api_key = if let Ok(key) = std::env::var("API_KEY") {
std::env::remove_var("API_KEY"); key
} else {
print!("\nEnter API Key: ");
std::io::stdout().flush()?;
let key = rpassword::read_password().context("Failed to read API key")?;
if key.trim().is_empty() {
return Err(ApiError::AuthError("API key cannot be empty".to_string()).into());
}
key
};
self.save_api_key_config(scheme_name, &api_key, location, param_name)?;
Ok(())
}
async fn setup_bearer_interactive(
&self,
scheme_name: &str,
details: Option<&crate::core::spec::UnifiedSecurityScheme>,
) -> Result<()> {
println!("\n{}", "Bearer Token Configuration".yellow());
if let Some(format) = details.and_then(|d| d.bearer_format.as_deref()) {
println!("Format: {}", format.bold());
}
let token = if let Ok(t) = std::env::var("BEARER_TOKEN") {
std::env::remove_var("BEARER_TOKEN"); t
} else {
print!("\nEnter Bearer Token: ");
std::io::stdout().flush()?;
let t = rpassword::read_password().context("Failed to read token")?;
if t.trim().is_empty() {
return Err(ApiError::AuthError("Token cannot be empty".to_string()).into());
}
t
};
self.save_bearer_config(scheme_name, &token)?;
Ok(())
}
async fn setup_basic_interactive(&self, scheme_name: &str) -> Result<()> {
println!("\n{}", "Basic Authentication Configuration".yellow());
let (username, password) = if let (Ok(u), Ok(p)) = (
std::env::var("BASIC_USERNAME"),
std::env::var("BASIC_PASSWORD"),
) {
std::env::remove_var("BASIC_USERNAME"); std::env::remove_var("BASIC_PASSWORD");
(u, p)
} else {
print!("Username: ");
std::io::stdout().flush()?;
let mut username = String::new();
std::io::stdin().read_line(&mut username)?;
let username = username.trim().to_string();
print!("Password: ");
std::io::stdout().flush()?;
let password = rpassword::read_password().context("Failed to read password")?;
if username.is_empty() || password.trim().is_empty() {
return Err(ApiError::AuthError(
"Username and password cannot be empty".to_string(),
)
.into());
}
(username, password)
};
self.save_basic_config(scheme_name, &username, &password)?;
Ok(())
}
async fn setup_oauth2_interactive(
&self,
scheme_name: &str,
details: Option<&crate::core::spec::UnifiedSecurityScheme>,
) -> Result<()> {
println!("\n{}", "OAuth2 Configuration".yellow());
let flows = self.get_oauth2_flows(details);
if flows.is_empty() {
return Err(ApiError::AuthError(
"No OAuth2 flows configured in specification".to_string(),
)
.into());
}
println!("\nAvailable flows:");
for (i, flow) in flows.iter().enumerate() {
println!(" {}. {}", i + 1, flow.bold());
}
print!("\nSelect flow (1-{}): ", flows.len());
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let index: usize = input.trim().parse().context("Invalid selection")?;
if index == 0 || index > flows.len() {
return Err(ApiError::ValidationError("Invalid selection".to_string()).into());
}
let selected_flow = &flows[index - 1];
match selected_flow.as_str() {
"client_credentials" => {
self.setup_oauth2_client_credentials(scheme_name, details)
.await
}
"authorization_code" => self.setup_oauth2_auth_code(scheme_name, details).await,
"device_code" => self.setup_oauth2_device_code(scheme_name, details).await,
_ => {
return Err(ApiError::ValidationError(format!(
"Flow {} not yet implemented",
selected_flow
))
.into())
}
}
}
async fn setup_oauth2_client_credentials(
&self,
_scheme_name: &str,
details: Option<&crate::core::spec::UnifiedSecurityScheme>,
) -> Result<()> {
println!("\n{}", "Client Credentials Flow".cyan());
print!("Client ID: ");
std::io::stdout().flush()?;
let mut client_id = String::new();
std::io::stdin().read_line(&mut client_id)?;
let _client_id = client_id.trim();
print!("Client Secret: ");
std::io::stdout().flush()?;
let _client_secret = rpassword::read_password().context("Failed to read client secret")?;
let token_url = details
.and_then(|d| d.token_url.as_deref())
.context("No token URL found in specification")?;
println!("\nToken URL: {}", token_url.green());
println!("\n{} Would fetch token from: {}", "→".yellow(), token_url);
println!(" (Token fetching not yet implemented)");
Ok(())
}
async fn setup_oauth2_auth_code(
&self,
_scheme_name: &str,
details: Option<&crate::core::spec::UnifiedSecurityScheme>,
) -> Result<()> {
println!("\n{}", "Authorization Code Flow".cyan());
let auth_url = details
.and_then(|d| d.authorization_url.as_deref())
.context("No authorization URL found in specification")?;
let token_url = details
.and_then(|d| d.token_url.as_deref())
.context("No token URL found in specification")?;
println!("\nAuthorization URL: {}", auth_url.green());
println!("Token URL: {}", token_url.green());
print!("\nClient ID: ");
std::io::stdout().flush()?;
let mut client_id = String::new();
std::io::stdin().read_line(&mut client_id)?;
let _client_id = client_id.trim();
print!("Client Secret (if required): ");
std::io::stdout().flush()?;
let _client_secret = rpassword::read_password().context("Failed to read client secret")?;
println!("\n{} Would open browser to: {}", "→".yellow(), auth_url);
println!(" (Browser flow not yet implemented)");
Ok(())
}
async fn setup_oauth2_device_code(
&self,
_scheme_name: &str,
_details: Option<&crate::core::spec::UnifiedSecurityScheme>,
) -> Result<()> {
println!("\n{}", "Device Code Flow".cyan());
println!(" (Not yet implemented)");
return Err(
ApiError::ValidationError("Device code flow not yet implemented".to_string()).into(),
);
}
async fn setup_oidc_interactive(
&self,
scheme_name: &str,
details: Option<&crate::core::spec::UnifiedSecurityScheme>,
) -> Result<()> {
println!("\n{}", "OpenID Connect Configuration".yellow());
if self.discover {
if let Some(url) = details.and_then(|d| d.openid_connect_url.as_deref()) {
println!("Discovering from: {}", url.green());
println!(" (OIDC discovery not yet implemented)");
}
}
self.setup_oauth2_interactive(scheme_name, details).await
}
async fn setup_mtls_interactive(&self, scheme_name: &str) -> Result<()> {
println!("\n{}", "Mutual TLS Configuration".yellow());
print!("Client certificate path: ");
std::io::stdout().flush()?;
let mut cert_path = String::new();
std::io::stdin().read_line(&mut cert_path)?;
let cert_path = cert_path.trim();
print!("Client key path: ");
std::io::stdout().flush()?;
let mut key_path = String::new();
std::io::stdin().read_line(&mut key_path)?;
let key_path = key_path.trim();
if !Path::new(cert_path).exists() {
return Err(
ApiError::AuthError(format!("Certificate file not found: {}", cert_path)).into(),
);
}
if !Path::new(key_path).exists() {
return Err(ApiError::AuthError(format!("Key file not found: {}", key_path)).into());
}
self.save_mtls_config(scheme_name, cert_path, key_path)?;
Ok(())
}
fn setup_api_key_env(
&self,
scheme_name: &str,
details: Option<&crate::core::spec::UnifiedSecurityScheme>,
) -> Result<()> {
let env_var = format!("{}_API_KEY", scheme_name.to_uppercase());
let api_key = std::env::var(&env_var)
.with_context(|| format!("Environment variable {} not set", env_var))?;
let location = details
.and_then(|d| d.location.as_deref())
.unwrap_or("header");
let param_name = details
.and_then(|d| d.name.as_deref())
.unwrap_or("X-API-Key");
self.save_api_key_config(scheme_name, &api_key, location, param_name)?;
println!("✓ Configured {} from ${}", scheme_name, env_var);
Ok(())
}
fn setup_bearer_env(&self, scheme_name: &str) -> Result<()> {
let env_var = format!("{}_TOKEN", scheme_name.to_uppercase());
let token = std::env::var(&env_var)
.with_context(|| format!("Environment variable {} not set", env_var))?;
self.save_bearer_config(scheme_name, &token)?;
println!("✓ Configured {} from ${}", scheme_name, env_var);
Ok(())
}
fn setup_basic_env(&self, scheme_name: &str) -> Result<()> {
let user_var = format!("{}_USERNAME", scheme_name.to_uppercase());
let pass_var = format!("{}_PASSWORD", scheme_name.to_uppercase());
let username = std::env::var(&user_var)
.with_context(|| format!("Environment variable {} not set", user_var))?;
let password = std::env::var(&pass_var)
.with_context(|| format!("Environment variable {} not set", pass_var))?;
self.save_basic_config(scheme_name, &username, &password)?;
println!(
"✓ Configured {} from ${} and ${}",
scheme_name, user_var, pass_var
);
Ok(())
}
fn setup_oauth2_env(&self, scheme_name: &str) -> Result<()> {
let id_var = format!("{}_CLIENT_ID", scheme_name.to_uppercase());
let secret_var = format!("{}_CLIENT_SECRET", scheme_name.to_uppercase());
let client_id = std::env::var(&id_var)
.with_context(|| format!("Environment variable {} not set", id_var))?;
let client_secret = std::env::var(&secret_var)
.with_context(|| format!("Environment variable {} not set", secret_var))?;
let token_url = std::env::var(format!("{}_TOKEN_URL", scheme_name.to_uppercase()))
.unwrap_or_else(|_| "https://oauth.provider.com/token".to_string());
self.save_oauth2_config(scheme_name, &client_id, &client_secret, &token_url)?;
println!(
"✓ Configured {} from ${} and ${}",
scheme_name, id_var, secret_var
);
Ok(())
}
fn setup_oidc_env(&self, scheme_name: &str) -> Result<()> {
self.setup_oauth2_env(scheme_name)
}
fn setup_mtls_env(&self, scheme_name: &str) -> Result<()> {
let cert_var = format!("{}_CLIENT_CERT", scheme_name.to_uppercase());
let key_var = format!("{}_CLIENT_KEY", scheme_name.to_uppercase());
let cert_path = std::env::var(&cert_var)
.with_context(|| format!("Environment variable {} not set", cert_var))?;
let key_path = std::env::var(&key_var)
.with_context(|| format!("Environment variable {} not set", key_var))?;
self.save_mtls_config(scheme_name, &cert_path, &key_path)?;
println!(
"✓ Configured {} from ${} and ${}",
scheme_name, cert_var, key_var
);
Ok(())
}
fn save_api_key_config(
&self,
scheme_name: &str,
api_key: &str,
_location: &str,
_param_name: &str,
) -> Result<()> {
self.update_env_file(&format!("{}_API_KEY", scheme_name.to_uppercase()), api_key)?;
println!(" → API key saved for scheme: {}", scheme_name.green());
Ok(())
}
fn save_bearer_config(&self, scheme_name: &str, token: &str) -> Result<()> {
self.update_env_file(&format!("{}_TOKEN", scheme_name.to_uppercase()), token)?;
println!(" → Bearer token saved for scheme: {}", scheme_name.green());
Ok(())
}
fn save_basic_config(&self, scheme_name: &str, username: &str, password: &str) -> Result<()> {
self.update_env_file(
&format!("{}_USERNAME", scheme_name.to_uppercase()),
username,
)?;
self.update_env_file(
&format!("{}_PASSWORD", scheme_name.to_uppercase()),
password,
)?;
println!(
" → Basic auth credentials saved for scheme: {}",
scheme_name.green()
);
Ok(())
}
fn save_oauth2_config(
&self,
scheme_name: &str,
client_id: &str,
client_secret: &str,
token_url: &str,
) -> Result<()> {
self.update_env_file(
&format!("{}_CLIENT_ID", scheme_name.to_uppercase()),
client_id,
)?;
self.update_env_file(
&format!("{}_CLIENT_SECRET", scheme_name.to_uppercase()),
client_secret,
)?;
self.update_env_file(
&format!("{}_TOKEN_URL", scheme_name.to_uppercase()),
token_url,
)?;
println!(
" → OAuth2 credentials saved for scheme: {}",
scheme_name.green()
);
Ok(())
}
fn save_mtls_config(&self, scheme_name: &str, cert_path: &str, key_path: &str) -> Result<()> {
self.update_env_file(
&format!("{}_CLIENT_CERT", scheme_name.to_uppercase()),
cert_path,
)?;
self.update_env_file(
&format!("{}_CLIENT_KEY", scheme_name.to_uppercase()),
key_path,
)?;
println!(
" → mTLS configuration saved for scheme: {}",
scheme_name.green()
);
Ok(())
}
#[allow(dead_code)]
fn write_auth_config(&self, _scheme_name: &str, _content: &str) -> Result<()> {
Ok(())
}
fn update_env_file(&self, key: &str, value: &str) -> Result<()> {
if self.keychain {
println!(" → Would store {} in keychain (not implemented)", key);
} else {
let env_name = self.env.as_deref().unwrap_or("local");
let env_file = if env_name == "local" {
PathBuf::from(".env.local")
} else {
let env_dir = PathBuf::from("env");
if env_dir.exists() {
env_dir.join(format!(".env.{}", env_name))
} else {
PathBuf::from(format!(".env.{}", env_name))
}
};
let mut content = if env_file.exists() {
fs::read_to_string(&env_file)?
} else {
String::new()
};
let key_pattern = format!("{}=", key);
if !content.contains(&key_pattern) {
if !content.is_empty() && !content.ends_with('\n') {
content.push('\n');
}
content.push_str(&format!("{}={}\n", key, value));
if let Some(parent) = env_file.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&env_file, content)?;
println!(" → Saved to {}", env_file.display());
}
}
Ok(())
}
fn get_oauth2_flows(
&self,
details: Option<&crate::core::spec::UnifiedSecurityScheme>,
) -> Vec<String> {
let mut flows = Vec::new();
if let Some(details) = details {
if details.token_url.is_some() {
flows.push("client_credentials".to_string());
}
if details.authorization_url.is_some() && details.token_url.is_some() {
flows.push("authorization_code".to_string());
}
}
if flows.is_empty() {
flows.push("client_credentials".to_string());
flows.push("authorization_code".to_string());
}
flows
}
fn print_next_steps(&self, scheme_name: &str) {
println!("\n{}", "Next Steps:".bold().cyan());
println!(" 1. Validate configuration:");
println!(
" {}",
format!("mrapids auth validate --scheme {}", scheme_name).green()
);
println!(" 2. Test with an API operation:");
println!(" {}", "mrapids run <operation>".green());
println!(" (Auth will be automatically used from environment variables)");
println!(" 3. View auth status:");
println!(" {}", "mrapids auth detect --format table".green());
}
}