diff --git a/docs/advanced/authentication.md b/docs/advanced/authentication.md index 6fb8ee8ef..edec86a6a 100644 --- a/docs/advanced/authentication.md +++ b/docs/advanced/authentication.md @@ -67,3 +67,39 @@ On Linux, one can use `GNOME Keyring` (or just Keyring) to access credentials th If you run on a server with none of the aforementioned keychains available, then pixi falls back to store the credentials in an _insecure_ JSON file. This JSON file is located at `~/.rattler/credentials.json` and contains the credentials. + +## Override the authentication storage + +You can use the `RATTLER_AUTH_FILE` environment variable to override the default location of the credentials file. +When this environment variable is set, it provides the only source of authentication data that is used by pixi. + +E.g. + +```bash +export RATTLER_AUTH_FILE=$HOME/credentials.json +# You can also specify the file in the command line +pixi global install --auth-file $HOME/credentials.json ... +``` + +The JSON should follow the following format: + +```json +{ + "*.prefix.dev": { + "BearerToken": "your_token" + }, + "otherhost.com": { + "BasicHttp": { + "username": "your_username", + "password": "your_password" + } + }, + "conda.anaconda.org": { + "CondaToken": "your_token" + } +} +``` + +Note: if you use a wildcard in the host, any subdomain will match (e.g. `*.prefix.dev` also matches `repo.prefix.dev`). + +Lastly you can set the authentication override file in the [global configuration file](./global_configuration.md). diff --git a/docs/advanced/global_configuration.md b/docs/advanced/global_configuration.md index c27f0b98d..a74c008f6 100644 --- a/docs/advanced/global_configuration.md +++ b/docs/advanced/global_configuration.md @@ -29,4 +29,10 @@ change_ps1 = true # security risk and should only be used for testing purposes or internal networks. # You can override this from the CLI with `--tls-no-verify`. tls_no_verify = false + +# Override from where the authentication information is loaded. +# Usually we try to use the keyring to load authentication data from, and only use a JSON +# file as fallback. This option allows you to force the use of a JSON file. +# Read more in the authentication section. +authentication_override_file = "/path/to/your/override.json" ``` diff --git a/src/cli/global/common.rs b/src/cli/global/common.rs index ec0ce1271..a11f8d3cb 100644 --- a/src/cli/global/common.rs +++ b/src/cli/global/common.rs @@ -1,17 +1,20 @@ use std::path::PathBuf; -use std::sync::Arc; use indexmap::IndexMap; use miette::IntoDiagnostic; use rattler_conda_types::{ Channel, ChannelConfig, MatchSpec, PackageName, Platform, PrefixRecord, RepoDataRecord, }; -use rattler_networking::AuthenticationMiddleware; use rattler_repodata_gateway::sparse::SparseRepoData; use rattler_solve::{resolvo, SolverImpl, SolverTask}; use reqwest_middleware::ClientWithMiddleware; -use crate::{config::home_path, prefix::Prefix, repodata}; +use crate::{ + config::{home_path, Config}, + prefix::Prefix, + repodata, + utils::reqwest::build_reqwest_clients, +}; /// Global binaries directory, default to `$HOME/.pixi/bin` pub struct BinDir(pub PathBuf); @@ -173,13 +176,12 @@ pub fn load_package_records( /// The network client and the fetched sparse repodata pub(super) async fn get_client_and_sparse_repodata( channels: impl IntoIterator, + config: &Config, ) -> miette::Result<( ClientWithMiddleware, IndexMap<(Channel, Platform), SparseRepoData>, )> { - let authenticated_client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new()) - .with_arc(Arc::new(AuthenticationMiddleware::default())) - .build(); + let authenticated_client = build_reqwest_clients(Some(config)).1; let platform_sparse_repodata = repodata::fetch_sparse_repodata(channels, [Platform::current()], &authenticated_client) .await?; diff --git a/src/cli/global/install.rs b/src/cli/global/install.rs index 852daca6e..dd26f2c8b 100644 --- a/src/cli/global/install.rs +++ b/src/cli/global/install.rs @@ -2,7 +2,7 @@ use std::ffi::OsStr; use std::path::{Path, PathBuf}; use std::sync::Arc; -use crate::config::Config; +use crate::config::{Config, ConfigCli}; use crate::install::execute_transaction; use crate::{config, prefix::Prefix, progress::await_in_progress}; use clap::Parser; @@ -43,6 +43,9 @@ pub struct Args { /// By default, if no channel is provided, `conda-forge` is used. #[clap(short, long)] channel: Vec, + + #[clap(flatten)] + config: ConfigCli, } /// Create the environment activation script @@ -231,7 +234,7 @@ pub(super) async fn create_executable_scripts( /// Install a global command pub async fn execute(args: Args) -> miette::Result<()> { // Figure out what channels we are using - let config = Config::load_global(); + let config = Config::with_cli_config(&args.config); let channels = config.compute_channels(&args.channel).into_diagnostic()?; // Find the MatchSpec we want to install @@ -243,7 +246,8 @@ pub async fn execute(args: Args) -> miette::Result<()> { .into_diagnostic()?; // Fetch sparse repodata - let (authenticated_client, sparse_repodata) = get_client_and_sparse_repodata(&channels).await?; + let (authenticated_client, sparse_repodata) = + get_client_and_sparse_repodata(&channels, &config).await?; // Install the package(s) let mut executables = vec![]; diff --git a/src/cli/global/remove.rs b/src/cli/global/remove.rs index b1d981e8e..122095a4b 100644 --- a/src/cli/global/remove.rs +++ b/src/cli/global/remove.rs @@ -19,6 +19,7 @@ pub struct Args { /// Specifies the package(s) that is to be removed. #[arg(num_args = 1..)] package: Vec, + #[command(flatten)] verbose: Verbosity, } diff --git a/src/cli/global/upgrade.rs b/src/cli/global/upgrade.rs index 35956182a..9578d350e 100644 --- a/src/cli/global/upgrade.rs +++ b/src/cli/global/upgrade.rs @@ -84,7 +84,8 @@ pub async fn execute(args: Args) -> miette::Result<()> { channels = channels.into_iter().unique().collect::>(); // Fetch sparse repodata - let (authenticated_client, sparse_repodata) = get_client_and_sparse_repodata(&channels).await?; + let (authenticated_client, sparse_repodata) = + get_client_and_sparse_repodata(&channels, &config).await?; let records = load_package_records(package_matchspec, &sparse_repodata)?; let package_record = records diff --git a/src/cli/global/upgrade_all.rs b/src/cli/global/upgrade_all.rs index 9294bf4de..358a480fd 100644 --- a/src/cli/global/upgrade_all.rs +++ b/src/cli/global/upgrade_all.rs @@ -5,7 +5,7 @@ use itertools::Itertools; use miette::IntoDiagnostic; use rattler_conda_types::{Channel, MatchSpec, ParseStrictness}; -use crate::config::Config; +use crate::config::{Config, ConfigCli}; use super::{ common::{find_installed_package, get_client_and_sparse_repodata, load_package_records}, @@ -27,11 +27,14 @@ pub struct Args { /// the package was installed from will always be used. #[clap(short, long)] channel: Vec, + + #[clap(flatten)] + config: ConfigCli, } pub async fn execute(args: Args) -> miette::Result<()> { let packages = list_global_packages().await?; - let config = Config::load_global(); + let config = Config::with_cli_config(&args.config); let mut channels = config.compute_channels(&args.channel).into_diagnostic()?; let mut installed_versions = HashMap::with_capacity(packages.len()); @@ -58,7 +61,8 @@ pub async fn execute(args: Args) -> miette::Result<()> { channels = channels.into_iter().unique().collect::>(); // Fetch sparse repodata - let (authenticated_client, sparse_repodata) = get_client_and_sparse_repodata(&channels).await?; + let (authenticated_client, sparse_repodata) = + get_client_and_sparse_repodata(&channels, &config).await?; let mut upgraded = false; for package_name in packages.iter() { diff --git a/src/cli/info.rs b/src/cli/info.rs index 7185f7282..718bcddcc 100644 --- a/src/cli/info.rs +++ b/src/cli/info.rs @@ -5,6 +5,7 @@ use clap::Parser; use itertools::Itertools; use miette::IntoDiagnostic; use rattler_conda_types::{GenericVirtualPackage, Platform}; +use rattler_networking::authentication_storage; use rattler_virtual_packages::VirtualPackage; use serde::Serialize; use serde_with::serde_as; @@ -352,15 +353,26 @@ pub async fn execute(args: Args) -> miette::Result<()> { .map(GenericVirtualPackage::from) .collect::>(); + let config = project + .map(|p| p.config().clone()) + .unwrap_or_else(config::Config::load_global); + + let auth_file = config + .authentication_override_file() + .map(|x| x.to_owned()) + .unwrap_or_else(|| { + authentication_storage::backends::file::FileStorage::default() + .path + .clone() + }); + let info = Info { platform: Platform::current().to_string(), virtual_packages, version: env!("CARGO_PKG_VERSION").to_string(), cache_dir: Some(config::get_cache_dir()?), cache_size, - auth_dir: rattler_networking::authentication_storage::backends::file::FileStorage::default( - ) - .path, + auth_dir: auth_file, project_info, environments_info, }; diff --git a/src/cli/install.rs b/src/cli/install.rs index 958c06c56..1e162bb07 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -24,7 +24,8 @@ pub struct Args { } pub async fn execute(args: Args) -> miette::Result<()> { - let project = Project::load_or_else_discover(args.manifest_path.as_deref())?; + let project = + Project::load_or_else_discover(args.manifest_path.as_deref())?.with_cli_config(args.config); let environment_name = args .environment .map_or_else(|| EnvironmentName::Default, EnvironmentName::Named); diff --git a/src/cli/remove.rs b/src/cli/remove.rs index b6db74cc2..38b0c7168 100644 --- a/src/cli/remove.rs +++ b/src/cli/remove.rs @@ -60,8 +60,8 @@ where } pub async fn execute(args: Args) -> miette::Result<()> { - let mut project = Project::load_or_else_discover(args.manifest_path.as_deref())? - .with_cli_config(args.config.clone()); + let mut project = + Project::load_or_else_discover(args.manifest_path.as_deref())?.with_cli_config(args.config); let deps = args.deps; let spec_type = if args.host { SpecType::Host diff --git a/src/cli/run.rs b/src/cli/run.rs index 788f4cf97..3a94da720 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -54,8 +54,8 @@ pub struct Args { /// When running the sigints are ignored and child can react to them. As it pleases. pub async fn execute(args: Args) -> miette::Result<()> { // Load the project - let project = Project::load_or_else_discover(args.manifest_path.as_deref())? - .with_cli_config(args.config.clone()); + let project = + Project::load_or_else_discover(args.manifest_path.as_deref())?.with_cli_config(args.config); // Sanity check of prefix location verify_prefix_location_unchanged(project.default_environment().dir().as_path())?; diff --git a/src/cli/search.rs b/src/cli/search.rs index 6facad793..a7394a112 100644 --- a/src/cli/search.rs +++ b/src/cli/search.rs @@ -7,7 +7,6 @@ use indexmap::IndexMap; use itertools::Itertools; use miette::IntoDiagnostic; use rattler_conda_types::{Channel, PackageName, Platform, RepoDataRecord}; -use rattler_networking::AuthenticationMiddleware; use rattler_repodata_gateway::sparse::SparseRepoData; use regex::Regex; @@ -15,6 +14,7 @@ use strsim::jaro; use tokio::task::spawn_blocking; use crate::config::Config; +use crate::utils::reqwest::build_reqwest_clients; use crate::{progress::await_in_progress, repodata::fetch_sparse_repodata, Project}; /// Search a package, output will list the latest version of package @@ -137,12 +137,14 @@ pub async fn execute(args: Args) -> miette::Result<()> { let package_name_filter = args.package; - let authenticated_client = reqwest_middleware::ClientBuilder::new(reqwest::Client::new()) - .with_arc(Arc::new(AuthenticationMiddleware::default())) - .build(); - let repo_data = Arc::new( - fetch_sparse_repodata(channels.iter(), [args.platform], &authenticated_client).await?, - ); + let client = if let Some(project) = project.as_ref() { + project.authenticated_client().clone() + } else { + build_reqwest_clients(None).1 + }; + + let repo_data = + Arc::new(fetch_sparse_repodata(channels.iter(), [args.platform], &client).await?); // When package name filter contains * (wildcard), it will search and display a list of packages matching this filter if package_name_filter.contains('*') { diff --git a/src/cli/shell.rs b/src/cli/shell.rs index c132441f8..ae7a70daa 100644 --- a/src/cli/shell.rs +++ b/src/cli/shell.rs @@ -199,8 +199,8 @@ async fn start_nu_shell( } pub async fn execute(args: Args) -> miette::Result<()> { - let project = Project::load_or_else_discover(args.manifest_path.as_deref())? - .with_cli_config(args.config.clone()); + let project = + Project::load_or_else_discover(args.manifest_path.as_deref())?.with_cli_config(args.config); let environment_name = args .environment .map_or_else(|| EnvironmentName::Default, EnvironmentName::Named); diff --git a/src/config.rs b/src/config.rs index fc6ad34b8..c466f978a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -70,6 +70,10 @@ pub struct ConfigCli { /// Do not verify the TLS certificate of the server. #[arg(long, action = ArgAction::SetTrue)] tls_no_verify: bool, + + /// Path to the file containing the authentication token. + #[arg(long, env = "RATTLER_AUTH_FILE")] + auth_file: Option, } #[derive(Parser, Debug, Default, Clone)] @@ -91,6 +95,10 @@ pub struct Config { #[serde(default)] change_ps1: Option, + /// Path to the file containing the authentication token. + #[serde(default)] + authentication_override_file: Option, + /// If set to true, pixi will not verify the TLS certificate of the server. #[serde(default)] tls_no_verify: Option, @@ -106,6 +114,7 @@ impl From for Config { fn from(cli: ConfigCli) -> Self { Self { tls_no_verify: if cli.tls_no_verify { Some(true) } else { None }, + authentication_override_file: cli.auth_file, ..Default::default() } } @@ -157,6 +166,13 @@ impl Config { merged_config } + /// Load the global config and layer the given cli config on top of it. + pub fn with_cli_config(cli: &ConfigCli) -> Config { + let mut config = Config::load_global(); + config.merge_config(&cli.clone().into()); + config + } + /// Load the config from the given path pixi folder and merge it with the global config. pub fn load(p: &Path) -> miette::Result { let local_config = p.join(consts::CONFIG_FILE); @@ -190,6 +206,10 @@ impl Config { self.tls_no_verify = other.tls_no_verify; } + if other.authentication_override_file.is_some() { + self.authentication_override_file = other.authentication_override_file.clone(); + } + self.loaded_from.extend(other.loaded_from.iter().cloned()); } @@ -215,6 +235,11 @@ impl Config { self.change_ps1.unwrap_or(true) } + /// Retrieve the value for the auth_file field. + pub fn authentication_override_file(&self) -> Option<&PathBuf> { + self.authentication_override_file.as_ref() + } + pub fn channel_config(&self) -> &ChannelConfig { &self.channel_config } @@ -255,16 +280,22 @@ mod tests { fn test_config_from_cli() { let cli = ConfigCli { tls_no_verify: true, + auth_file: None, }; let config = Config::from(cli); assert_eq!(config.tls_no_verify, Some(true)); let cli = ConfigCli { tls_no_verify: false, + auth_file: Some(PathBuf::from("path.json")), }; let config = Config::from(cli); assert_eq!(config.tls_no_verify, None); + assert_eq!( + config.authentication_override_file, + Some(PathBuf::from("path.json")) + ); } #[test] diff --git a/src/snapshots/pixi__config__tests__config_merge.snap b/src/snapshots/pixi__config__tests__config_merge.snap index 066465599..f2cfffaf5 100644 --- a/src/snapshots/pixi__config__tests__config_merge.snap +++ b/src/snapshots/pixi__config__tests__config_merge.snap @@ -11,6 +11,7 @@ Config { change_ps1: Some( true, ), + authentication_override_file: None, tls_no_verify: Some( false, ), diff --git a/src/utils/reqwest.rs b/src/utils/reqwest.rs index 8874a8844..29f02e612 100644 --- a/src/utils/reqwest.rs +++ b/src/utils/reqwest.rs @@ -1,6 +1,9 @@ -use std::{sync::Arc, time::Duration}; +use std::{path::PathBuf, sync::Arc, time::Duration}; -use rattler_networking::{retry_policies::ExponentialBackoff, AuthenticationMiddleware}; +use rattler_networking::{ + authentication_storage, retry_policies::ExponentialBackoff, AuthenticationMiddleware, + AuthenticationStorage, +}; use reqwest::Client; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; @@ -12,6 +15,25 @@ pub fn default_retry_policy() -> ExponentialBackoff { ExponentialBackoff::builder().build_with_max_retries(3) } +fn auth_middleware(config: &Config) -> AuthenticationMiddleware { + if let Some(auth_file) = config.authentication_override_file() { + tracing::info!("Loading authentication from file: {:?}", auth_file); + + if !auth_file.exists() { + tracing::warn!("Authentication file does not exist: {:?}", auth_file); + } + + let mut store = AuthenticationStorage::new(); + store.add_backend(Arc::from( + authentication_storage::backends::file::FileStorage::new(PathBuf::from(&auth_file)), + )); + + return AuthenticationMiddleware::new(store); + } + + AuthenticationMiddleware::default() +} + pub(crate) fn build_reqwest_clients(config: Option<&Config>) -> (Client, ClientWithMiddleware) { static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); @@ -36,7 +58,7 @@ pub(crate) fn build_reqwest_clients(config: Option<&Config>) -> (Client, ClientW .expect("failed to create reqwest Client"); let authenticated_client = ClientBuilder::new(client.clone()) - .with_arc(Arc::new(AuthenticationMiddleware::default())) + .with_arc(Arc::new(auth_middleware(&config))) .build(); (client, authenticated_client)