auth, init

This commit is contained in:
dal 2025-05-05 18:24:05 -06:00
parent ef07010ff5
commit 1987aeb45a
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
4 changed files with 155 additions and 94 deletions

View File

@ -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<bool> {
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<Option<BusterCredentials>> {
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(())
}

View File

@ -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))?

View File

@ -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![

View File

@ -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)]