From 2774595bc15bfe2a4530ec2ca0e9276bd172abc3 Mon Sep 17 00:00:00 2001 From: Aiden Fujiwara <106630142+afujiwara-roblox@users.noreply.github.com> Date: Tue, 5 Mar 2024 10:58:22 -0800 Subject: [PATCH] Add command to add Artifactory Auth (#91) We can already read Artifactory tokens, but Foreman has no way of adding the tokens them itself. This PR looks to add the command `artifactory-auth`, that places the tokens in the expected format in the expected location. This PR also includes a minor version bump since we are adding behavior. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/artifactory_auth_store.rs | 72 +++++++++++++++++++++++++++++++ src/main.rs | 62 +++++++++++++++++++++++++- src/tool_provider/artifactory.rs | 2 +- tests/snapshots/help_command.snap | 4 +- 6 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 src/artifactory_auth_store.rs diff --git a/Cargo.lock b/Cargo.lock index 5178c82..0b85d3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -390,7 +390,7 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "foreman" -version = "1.5.0" +version = "1.6.0" dependencies = [ "artiaa_auth", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index c65a4dc..cdb1834 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ default-members = [".", "artiaa_auth"] [package] name = "foreman" description = "Toolchain manager for simple binary tools" -version = "1.5.0" +version = "1.6.0" authors = [ "Lucien Greathouse ", "Matt Hargett ", diff --git a/src/artifactory_auth_store.rs b/src/artifactory_auth_store.rs new file mode 100644 index 0000000..f327fe5 --- /dev/null +++ b/src/artifactory_auth_store.rs @@ -0,0 +1,72 @@ +use crate::error::ForemanError; +use crate::{error::ForemanResult, fs}; +use artiaa_auth::{error::ArtifactoryAuthError, Credentials}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use std::{ + ops::{Deref, DerefMut}, + path::Path, +}; +/// Contains stored user tokens that Foreman can use to download tools. +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct ArtifactoryAuthStore { + tokens: HashMap, +} + +impl Deref for ArtifactoryAuthStore { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.tokens + } +} + +impl DerefMut for ArtifactoryAuthStore { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.tokens + } +} + +impl ArtifactoryAuthStore { + pub fn set_token(auth_file: &Path, key: &str, token: &str) -> ForemanResult<()> { + let contents = fs::try_read_to_string(auth_file)?; + + let mut store: ArtifactoryAuthStore = if let Some(contents) = contents { + serde_json::from_str(&contents).map_err(|err: serde_json::Error| { + ForemanError::ArtiAAError { + error: ArtifactoryAuthError::auth_parsing(auth_file, err.to_string()), + } + })? + } else { + ArtifactoryAuthStore::default() + }; + + store.insert( + key.to_owned(), + Credentials { + username: "".to_owned(), + token: token.to_owned(), + }, + ); + + let serialized = + serde_json::to_string_pretty(&store).map_err(|err: serde_json::Error| { + ForemanError::ArtiAAError { + error: ArtifactoryAuthError::auth_parsing(auth_file, err.to_string()), + } + })?; + + if let Some(dir) = auth_file.parent() { + fs::create_dir_all(dir)?; + fs::write(auth_file, serialized) + } else { + Err(ForemanError::ArtiAAError { + error: ArtifactoryAuthError::auth_parsing( + auth_file, + "Could not find parent directory of auth file".to_owned(), + ), + }) + } + } +} diff --git a/src/main.rs b/src/main.rs index dd39664..a836bb7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod aliaser; mod artifact_choosing; +mod artifactory_auth_store; mod artifactory_path; mod auth_store; mod ci_string; @@ -11,8 +12,13 @@ mod process; mod tool_cache; mod tool_provider; -use std::{env, ffi::OsStr}; +use std::{ + env, + ffi::OsStr, + io::{stdout, Write}, +}; +use artifactory_auth_store::ArtifactoryAuthStore; use paths::ForemanPaths; use structopt::StructOpt; @@ -153,6 +159,11 @@ enum Subcommand { #[structopt(name = "gitlab-auth")] GitLabAuth(GitLabAuthCommand), + /// Set the Artifactory Token that Foreman should use with the + /// Artifactory API. + #[structopt(name = "artifactory-auth")] + ArtifactoryAuth(ArtifactoryAuthCommand), + /// Create a path to publish to artifactory /// /// Foreman does not support uploading binaries to artifactory directly, but it can generate the path where it would expect to find a given artifact. Use this command to generate paths that can be input to generic artifactory upload solutions. @@ -176,6 +187,12 @@ struct GitLabAuthCommand { token: Option, } +#[derive(Debug, StructOpt)] +struct ArtifactoryAuthCommand { + url: Option, + token: Option, +} + #[derive(Debug, StructOpt)] struct GenerateArtifactoryPathCommand { repo: String, @@ -297,11 +314,54 @@ fn actual_main(paths: ForemanPaths) -> ForemanResult<()> { )?; println!("{}", artifactory_path); } + Subcommand::ArtifactoryAuth(subcommand) => { + let url = prompt_url(subcommand.url)?; + + let token = prompt_auth_token( + subcommand.token, + "Artifactory", + "https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-tokens", + )?; + + ArtifactoryAuthStore::set_token(&paths.artiaa_path()?, &url, &token)?; + } } Ok(()) } +fn prompt_url(url: Option) -> Result { + match url { + Some(url) => Ok(url), + None => { + println!("Artifactory auth saved successfully."); + println!("Foreman requires a specific URL to authenticate to Artifactory."); + println!(); + + loop { + let mut input = String::new(); + + print!("Artifactory URL: "); + stdout().flush().map_err(|err| { + ForemanError::io_error_with_context( + err, + "an error happened trying to flush stdout", + ) + })?; + std::io::stdin().read_line(&mut input).map_err(|err| { + ForemanError::io_error_with_context(err, "an error happened trying to read url") + })?; + + if input.is_empty() { + println!("Token must be non-empty."); + } else { + break Ok(input); + } + } + } + } +} + fn prompt_auth_token( token: Option, provider: &str, diff --git a/src/tool_provider/artifactory.rs b/src/tool_provider/artifactory.rs index f9e22b5..2757809 100644 --- a/src/tool_provider/artifactory.rs +++ b/src/tool_provider/artifactory.rs @@ -1,4 +1,4 @@ -//! Slice of GitHub's API that Foreman consumes. +//! Slice of Artifactory's API that Foreman consumes. use super::{Release, ReleaseAsset, ToolProviderImpl}; use crate::{ diff --git a/tests/snapshots/help_command.snap b/tests/snapshots/help_command.snap index 08cd5cf..6abdecf 100644 --- a/tests/snapshots/help_command.snap +++ b/tests/snapshots/help_command.snap @@ -1,8 +1,9 @@ --- source: tests/cli.rs +assertion_line: 100 expression: content --- -foreman 1.5.0 +foreman 1.6.0 USAGE: foreman [FLAGS] @@ -13,6 +14,7 @@ FLAGS: -v Logging verbosity. Supply multiple for more verbosity, up to -vvv SUBCOMMANDS: + artifactory-auth Set the Artifactory Token that Foreman should use with the Artifactory API generate-artifactory-path Create a path to publish to artifactory github-auth Set the GitHub Personal Access Token that Foreman should use with the GitHub API gitlab-auth Set the GitLab Personal Access Token that Foreman should use with the GitLab API