From 1987aeb45aef64a29565f8bb76ae56a102745ed6 Mon Sep 17 00:00:00 2001 From: dal Date: Mon, 5 May 2025 18:24:05 -0600 Subject: [PATCH] auth, init --- cli/cli/src/commands/auth.rs | 192 +++++++++++++++++++++------------ cli/cli/src/commands/deploy.rs | 10 +- cli/cli/src/commands/init.rs | 46 ++++---- cli/cli/src/main.rs | 1 + 4 files changed, 155 insertions(+), 94 deletions(-) diff --git a/cli/cli/src/commands/auth.rs b/cli/cli/src/commands/auth.rs index 223b7f650..cbf3d2288 100644 --- a/cli/cli/src/commands/auth.rs +++ b/cli/cli/src/commands/auth.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{Result}; use async_trait::async_trait; use clap::Parser; use inquire::{Confirm, Password, Text}; @@ -19,6 +19,12 @@ pub enum AuthError { InvalidApiKey, #[error("Failed to validate credentials: {0}")] ValidationError(String), + #[error("Authentication cancelled by user")] + ClearCredentialsFailed(String), + #[error("Failed to save credentials: {0}")] + SaveCredentialsFailed(String), + #[error("Failed to get user input: {0}")] + UserInputFailed(String), } #[derive(Parser, Debug)] @@ -136,21 +142,21 @@ pub async fn check_authentication() -> Result<()> { check_authentication_inner(cached_credentials_result, &validator).await } -pub async fn auth_with_args(args: AuthArgs) -> Result<()> { - // Handle --clear flag first - if args.clear { - match delete_buster_credentials().await { - Ok(_) => { - println!("Saved credentials cleared successfully."); - return Ok(()); - } - Err(e) => { - return Err(anyhow::anyhow!("Failed to clear credentials: {}", e)); - } - } +/// Handles the --clear flag logic. +async fn handle_clear_flag(clear: bool) -> Result { + if clear { + delete_buster_credentials().await + .map_err(|e| AuthError::ClearCredentialsFailed(e.to_string()))?; + println!("Saved credentials cleared successfully."); + Ok(true) // Indicate that the command should exit + } else { + Ok(false) // Indicate that the command should continue } +} - // Get existing credentials or create default +/// Loads existing credentials or initializes default ones, handling overrides from args/env. +/// Also prompts for overwrite confirmation if necessary. +async fn load_and_confirm_credentials(args: &AuthArgs) -> Result> { let mut buster_creds = match get_buster_credentials().await { Ok(creds) => creds, Err(_) => BusterCredentials { @@ -160,99 +166,147 @@ pub async fn auth_with_args(args: AuthArgs) -> Result<()> { }; let existing_creds_present = !buster_creds.url.is_empty() && !buster_creds.api_key.is_empty(); + // Apply args overrides early - host + if let Some(host) = &args.host { + buster_creds.url = host.clone(); + } + // Apply args overrides early - api key + if let Some(api_key) = &args.api_key { + buster_creds.api_key = api_key.clone(); + } + let host_provided = args.host.is_some(); let api_key_provided = args.api_key.is_some(); let fully_provided_via_args = host_provided && api_key_provided; - // If existing credentials are found and the user hasn't provided everything via args/env, - // prompt for confirmation before proceeding with potential overwrites. + // Prompt for overwrite confirmation only if existing creds are present *and* not fully overridden by args if existing_creds_present && !fully_provided_via_args { let confirm = Confirm::new("Existing credentials found. Do you want to overwrite them?") .with_default(false) .with_help_message("Select 'y' to proceed with entering new credentials, or 'n' to cancel.") - .prompt()?; + .prompt() + .map_err(|e| AuthError::UserInputFailed(e.to_string()))?; if !confirm { println!("Authentication cancelled."); - return Ok(()); + return Ok(None); // Signal cancellation } - // If confirmed, we will proceed, potentially overwriting existing values below. + // If confirmed, proceed with the potentially modified buster_creds } - // Apply host from args or use default - if let Some(host) = args.host { - buster_creds.url = host; - } + Ok(Some(buster_creds)) +} - // Check if API key was provided via args or environment - let api_key_from_env_or_args = args.api_key.is_some(); +/// Prompts the user interactively for missing host and API key information. +async fn prompt_for_missing_credentials( + creds: &mut BusterCredentials, + args: &AuthArgs, + existing_creds_present: bool, // Needed to adjust prompt text +) -> Result<()> { + let host_provided = args.host.is_some(); + let api_key_provided = args.api_key.is_some(); - // Apply API key from args or environment - if let Some(api_key) = args.api_key { - buster_creds.api_key = api_key; - } - - // Interactive mode for missing values - // Only prompt if the value wasn't provided via args/env - if !host_provided && buster_creds.url.is_empty() { + // Prompt for URL if not provided via args and current URL is default or empty + if !host_provided && (creds.url.is_empty() || creds.url == DEFAULT_HOST) { + let default_url_to_show = if creds.url.is_empty() { DEFAULT_HOST } else { &creds.url }; let url_input = Text::new("Enter the URL of your Buster API") - .with_default(DEFAULT_HOST) - .with_help_message("Press Enter to use the default URL") + .with_default(default_url_to_show) + .with_help_message("Press Enter to use the displayed default/current URL") .prompt() - .context("Failed to get URL input")?; + .map_err(|e| AuthError::UserInputFailed(e.to_string()))?; - if url_input.is_empty() { - buster_creds.url = DEFAULT_HOST.to_string(); - } else { - buster_creds.url = url_input; - } + // Update only if input is not empty and different from default/current + if !url_input.is_empty() && url_input != default_url_to_show { + creds.url = url_input; + } else if creds.url.is_empty() { // Ensure default is set if prompt skipped with empty initial value + creds.url = DEFAULT_HOST.to_string(); + } } - // Always prompt for API key if it wasn't found in environment variables or args - // unless it's already present from the loaded credentials - if !api_key_from_env_or_args { - let obfuscated_api_key = if buster_creds.api_key.is_empty() { - String::from("None") + // Prompt for API key if not provided via args + if !api_key_provided { + let obfuscated_api_key = if creds.api_key.is_empty() { + String::from("[Not Set]") } else { - format!("{}...", &buster_creds.api_key[0..std::cmp::min(4, buster_creds.api_key.len())]) // Ensure safe slicing + format!("{}...", &creds.api_key[0..std::cmp::min(4, creds.api_key.len())]) }; - let prompt_message = if existing_creds_present && !fully_provided_via_args { - format!("Enter new API key (current: [{obfuscated_api_key}]):") + let prompt_message = if existing_creds_present { + format!("Enter new API key (current: {obfuscated_api_key}):") } else { - format!("Enter your API key [{obfuscated_api_key}]:") + format!("Enter your API key (current: {obfuscated_api_key}):") }; let api_key_input = Password::new(&prompt_message) .without_confirmation() - .with_help_message("Your API key can be found in your Buster dashboard. Leave blank to keep current key.") + .with_help_message("Your API key can be found in your Buster dashboard. Leave blank to keep the current key.") .prompt() - .context("Failed to get API key input")?; + .map_err(|e| AuthError::UserInputFailed(e.to_string()))?; - if api_key_input.is_empty() && buster_creds.api_key.is_empty() { - // Only error if no key exists *and* none was entered - return Err(AuthError::MissingApiKey.into()); - } else if !api_key_input.is_empty() { - // Update only if new input was provided - buster_creds.api_key = api_key_input; + // Update only if new input was provided + if !api_key_input.is_empty() { + creds.api_key = api_key_input; } } - // Validate credentials using the trait - let validator = RealCredentialsValidator; - validator.validate(&buster_creds.url, &buster_creds.api_key).await?; + // Final check: Ensure API key is present after args and prompts + if creds.api_key.is_empty() { + return Err(AuthError::MissingApiKey.into()); + } - // Save credentials unless --no-save is specified - if !args.no_save { - set_buster_credentials(buster_creds).await - .context("Failed to save credentials")?; + Ok(()) +} + +/// Validates the provided credentials using the validator trait. +async fn validate_credentials( + creds: &BusterCredentials, + validator: &dyn CredentialsValidator, +) -> Result<()> { + validator.validate(&creds.url, &creds.api_key).await?; + Ok(()) +} + +/// Saves credentials to disk or prints a success message. +async fn save_credentials_or_notify( + creds: BusterCredentials, + no_save: bool, +) -> Result<()> { + if !no_save { + set_buster_credentials(creds).await + .map_err(|e| AuthError::SaveCredentialsFailed(e.to_string()))?; println!("Credentials saved successfully!"); } else { - // Only print success if we actually went through validation. - // If validation failed, error would have been returned above. - println!("Authentication successful!"); - println!("Note: Credentials were not saved due to --no-save flag"); + println!("Authentication successful!"); + println!("Note: Credentials were not saved due to --no-save flag"); } + Ok(()) +} + +/// Main function orchestrating the authentication flow. +pub async fn auth_with_args(args: AuthArgs) -> Result<()> { + // 1. Handle --clear flag + if handle_clear_flag(args.clear).await? { + return Ok(()); // Exit early if credentials were cleared + } + + // 2. Load existing credentials or initialize defaults, confirm overwrite if needed + let mut opt_buster_creds = match load_and_confirm_credentials(&args).await? { + Some(creds) => creds, + None => return Ok(()), // User cancelled overwrite prompt + }; + let existing_creds_present = !opt_buster_creds.url.is_empty() && !opt_buster_creds.api_key.is_empty(); + + + // 3. Prompt for missing credentials interactively + prompt_for_missing_credentials(&mut opt_buster_creds, &args, existing_creds_present).await?; + + + // 4. Validate the final credentials + let validator = RealCredentialsValidator; + validate_credentials(&opt_buster_creds, &validator).await?; + + // 5. Save credentials or notify + save_credentials_or_notify(opt_buster_creds, args.no_save).await?; Ok(()) } diff --git a/cli/cli/src/commands/deploy.rs b/cli/cli/src/commands/deploy.rs index 3e6014ef5..e0a2a0a84 100644 --- a/cli/cli/src/commands/deploy.rs +++ b/cli/cli/src/commands/deploy.rs @@ -19,6 +19,7 @@ use crate::utils::{ config::BusterConfig, file::buster_credentials::get_and_validate_buster_credentials, }; +use super::auth::check_authentication; // Use the unified BusterConfig from exclusion.rs instead // This BusterConfig struct is now deprecated and replaced by the one from utils::exclusion @@ -815,7 +816,12 @@ impl ModelFile { } pub async fn deploy(path: Option<&str>, dry_run: bool, recursive: bool) -> Result<()> { - let target_path = PathBuf::from(path.unwrap_or(".")); + check_authentication().await?; + + let current_dir = std::env::current_dir()?; + let target_path = path + .map(|p| PathBuf::from(p)) + .unwrap_or_else(|| current_dir); let mut progress = DeployProgress::new(0); let mut result = DeployResult::default(); @@ -876,7 +882,7 @@ pub async fn deploy(path: Option<&str>, dry_run: bool, recursive: bool) -> Resul find_yml_files_recursively(&target_path, Some(config), Some(&mut progress))? } else { println!("No model_paths specified in buster.yml, using target path"); - find_yml_files_recursively(&target_path, Some(config), Some(&mut progress))? + find_yml_files_recursively(&target_path, None, Some(&mut progress))? } } else { find_yml_files_recursively(&target_path, None, Some(&mut progress))? diff --git a/cli/cli/src/commands/init.rs b/cli/cli/src/commands/init.rs index c2a01e3ce..2f4f9f673 100644 --- a/cli/cli/src/commands/init.rs +++ b/cli/cli/src/commands/init.rs @@ -225,6 +225,29 @@ async fn create_data_source_with_progress( pub async fn init(destination_path: Option<&str>) -> Result<()> { println!("{}", "Initializing Buster...".bold().green()); + // Check for Buster credentials with progress indicator + let spinner = ProgressBar::new_spinner(); + spinner.set_style( + ProgressStyle::default_spinner() + .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ") + .template("{spinner:.green} {msg}") + .unwrap(), + ); + spinner.set_message("Checking for Buster credentials..."); + spinner.enable_steady_tick(Duration::from_millis(100)); + + let buster_creds = match get_and_validate_buster_credentials().await { + Ok(creds) => { + spinner.finish_with_message("✓ Buster credentials found".green().to_string()); + creds + } + Err(_) => { + spinner.finish_with_message("✗ No valid Buster credentials found".red().to_string()); + println!("Please run {} first.", "buster auth".cyan()); + return Err(anyhow::anyhow!("No valid Buster credentials found")); + } + }; + // Determine the destination path for buster.yml let dest_path = match destination_path { Some(path) => PathBuf::from(path), @@ -271,29 +294,6 @@ pub async fn init(destination_path: Option<&str>) -> Result<()> { } // --- End dbt_project.yml parsing --- - // Check for Buster credentials with progress indicator - let spinner = ProgressBar::new_spinner(); - spinner.set_style( - ProgressStyle::default_spinner() - .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ") - .template("{spinner:.green} {msg}") - .unwrap(), - ); - spinner.set_message("Checking for Buster credentials..."); - spinner.enable_steady_tick(Duration::from_millis(100)); - - let buster_creds = match get_and_validate_buster_credentials().await { - Ok(creds) => { - spinner.finish_with_message("✓ Buster credentials found".green().to_string()); - creds - } - Err(_) => { - spinner.finish_with_message("✗ No valid Buster credentials found".red().to_string()); - println!("Please run {} first.", "buster auth".cyan()); - return Err(anyhow::anyhow!("No valid Buster credentials found")); - } - }; - // Select database type // Sort database types alphabetically by display name let mut db_types = vec![ diff --git a/cli/cli/src/main.rs b/cli/cli/src/main.rs index 6117594ed..dd75e8813 100644 --- a/cli/cli/src/main.rs +++ b/cli/cli/src/main.rs @@ -16,6 +16,7 @@ pub const GIT_HASH: &str = env!("GIT_HASH"); #[derive(Subcommand)] #[clap(rename_all = "kebab-case")] pub enum Commands { + /// Initialize a new Buster project Init { /// Path to create the buster.yml file (defaults to current directory) #[arg(long)]