diff --git a/gix-config/src/file/init/comfort.rs b/gix-config/src/file/init/comfort.rs index 235a02d61d8..f5b8ae9838b 100644 --- a/gix-config/src/file/init/comfort.rs +++ b/gix-config/src/file/init/comfort.rs @@ -17,31 +17,35 @@ use crate::{ impl File<'static> { /// Open all global configuration files which involves the following sources: /// - /// * [system][crate::Source::System] - /// * [git][crate::Source::Git] - /// * [user][crate::Source::User] + /// * [git-installation](source::Kind::GitInstallation) + /// * [system](source::Kind::System) + /// * [globals](source::Kind::Global) /// /// which excludes repository local configuration, as well as override-configuration from environment variables. /// /// Note that the file might [be empty][File::is_void()] in case no configuration file was found. pub fn from_globals() -> Result, init::from_paths::Error> { - let metas = [source::Kind::System, source::Kind::Global] - .iter() - .flat_map(|kind| kind.sources()) - .filter_map(|source| { - let path = source - .storage_location(&mut gix_path::env::var) - .and_then(|p| p.is_file().then_some(p)) - .map(Cow::into_owned); + let metas = [ + source::Kind::GitInstallation, + source::Kind::System, + source::Kind::Global, + ] + .iter() + .flat_map(|kind| kind.sources()) + .filter_map(|source| { + let path = source + .storage_location(&mut gix_path::env::var) + .and_then(|p| p.is_file().then_some(p)) + .map(Cow::into_owned); - Metadata { - path, - source: *source, - level: 0, - trust: gix_sec::Trust::Full, - } - .into() - }); + Metadata { + path, + source: *source, + level: 0, + trust: gix_sec::Trust::Full, + } + .into() + }); let home = gix_path::env::home_dir(); let options = init::Options { diff --git a/gix-credentials/src/protocol/context/mod.rs b/gix-credentials/src/protocol/context/mod.rs index 1c578c046d1..c25bf04dec1 100644 --- a/gix-credentials/src/protocol/context/mod.rs +++ b/gix-credentials/src/protocol/context/mod.rs @@ -59,6 +59,7 @@ mod mutate { let url = gix_url::parse(self.url.as_ref().ok_or(protocol::Error::UrlMissing)?.as_ref())?; self.protocol = Some(url.scheme.as_str().into()); self.username = url.user().map(ToOwned::to_owned); + self.password = url.password().map(ToOwned::to_owned); self.host = url.host().map(ToOwned::to_owned).map(|mut host| { if let Some(port) = url.port { use std::fmt::Write; diff --git a/gix-credentials/tests/protocol/context.rs b/gix-credentials/tests/protocol/context.rs index 3cfd850a363..e9e03b1f0f9 100644 --- a/gix-credentials/tests/protocol/context.rs +++ b/gix-credentials/tests/protocol/context.rs @@ -36,6 +36,15 @@ mod destructure_url_in_place { assert_eq_parts("ssh://user@host:21/path", "ssh", "user", "host:21", "path", false); assert_eq_parts("ssh://host.org/path", "ssh", None, "host.org", "path", true); } + + #[test] + fn passwords_are_placed_in_context_too() -> crate::Result { + let mut ctx = url_ctx("http://user:password@host/path"); + ctx.destructure_url_in_place(false)?; + assert_eq!(ctx.password.as_deref(), Some("password")); + Ok(()) + } + #[test] fn http_and_https_ignore_the_path_by_default() { assert_eq_parts( diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index f2892c730e2..2ebf2c5c06b 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -212,25 +212,13 @@ impl Cache { &self, key: impl gix_config::AsKey, ) -> Option, gix_config::path::interpolate::Error>> { - let path = self - .resolved - .path_filter(&key, &mut self.filter_config_section.clone())?; - - if self.lenient_config && path.is_empty() { - let _key = key.as_key(); - gix_trace::info!( - "Ignored empty path at {section_name}.{subsection_name:?}.{name} due to lenient configuration", - section_name = _key.section_name, - subsection_name = _key.subsection_name, - name = _key.value_name - ); - return None; - } - - let install_dir = crate::path::install_dir().ok(); - let home = self.home_dir(); - let ctx = config::cache::interpolate_context(install_dir.as_deref(), home.as_deref()); - Some(path.interpolate(ctx)) + trusted_file_path( + &self.resolved, + key, + &mut self.filter_config_section.clone(), + self.lenient_config, + self.environment, + ) } pub(crate) fn apply_leniency(&self, res: Option>) -> Result, E> { @@ -465,11 +453,45 @@ impl Cache { /// /// We never fail for here even if the permission is set to deny as we `gix-config` will fail later /// if it actually wants to use the home directory - we don't want to fail prematurely. + #[cfg(any( + feature = "blocking-http-transport-reqwest", + feature = "blocking-http-transport-curl" + ))] pub(crate) fn home_dir(&self) -> Option { - gix_path::env::home_dir().and_then(|path| self.environment.home.check_opt(path)) + home_dir(self.environment) } } +pub(crate) fn trusted_file_path<'config>( + config: &'config gix_config::File<'_>, + key: impl gix_config::AsKey, + filter: &mut gix_config::file::MetadataFilter, + lenient_config: bool, + environment: crate::open::permissions::Environment, +) -> Option, gix_config::path::interpolate::Error>> { + let path = config.path_filter(&key, filter)?; + + if lenient_config && path.is_empty() { + let _key = key.as_key(); + gix_trace::info!( + "Ignored empty path at {section_name}.{subsection_name:?}.{name} due to lenient configuration", + section_name = _key.section_name, + subsection_name = _key.subsection_name, + name = _key.value_name + ); + return None; + } + + let install_dir = crate::path::install_dir().ok(); + let home = home_dir(environment); + let ctx = config::cache::interpolate_context(install_dir.as_deref(), home.as_deref()); + Some(path.interpolate(ctx)) +} + +pub(crate) fn home_dir(environment: crate::open::permissions::Environment) -> Option { + gix_path::env::home_dir().and_then(|path| environment.home.check_opt(path)) +} + fn boolean( me: &Cache, full_key: &str, diff --git a/gix/src/config/cache/mod.rs b/gix/src/config/cache/mod.rs index 1904c5ea91e..825711938eb 100644 --- a/gix/src/config/cache/mod.rs +++ b/gix/src/config/cache/mod.rs @@ -11,7 +11,7 @@ impl std::fmt::Debug for Cache { } } -mod access; +pub(crate) mod access; pub(crate) mod util; diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index f7b8860d1fd..e4953f9fa1e 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -42,7 +42,11 @@ pub struct CommitAutoRollback<'repo> { pub(crate) prev_config: crate::Config, } -pub(crate) mod section { +/// +#[allow(clippy::empty_docs)] +pub mod section { + /// A filter that returns `true` for `meta` if the meta-data attached to a configuration section can be trusted. + /// This is either the case if its file is fully trusted, or if it's a section from a system-wide file. pub fn is_trusted(meta: &gix_config::file::Metadata) -> bool { meta.trust == gix_sec::Trust::Full || meta.source.kind() != gix_config::source::Kind::Repository } @@ -627,7 +631,7 @@ pub(crate) struct Cache { /// If true, we are on a case-insensitive file system. pub ignore_case: bool, /// If true, we should default what's possible if something is misconfigured, on case by case basis, to be more resilient. - /// Also available in options! Keep in sync! + /// Also, available in options! Keep in sync! pub lenient_config: bool, #[cfg_attr(not(feature = "worktree-mutation"), allow(dead_code))] attributes: crate::open::permissions::Attributes, diff --git a/gix/src/config/snapshot/credential_helpers.rs b/gix/src/config/snapshot/credential_helpers.rs index f84efa896e8..26613ef1a4e 100644 --- a/gix/src/config/snapshot/credential_helpers.rs +++ b/gix/src/config/snapshot/credential_helpers.rs @@ -1,15 +1,6 @@ -use std::borrow::Cow; - pub use error::Error; -use crate::config::cache::util::ApplyLeniency; -use crate::{ - bstr::{ByteSlice, ByteVec}, - config::{ - tree::{credential, gitoxide::Credentials, Core, Credential, Key}, - Snapshot, - }, -}; +use crate::config::Snapshot; mod error { use crate::bstr::BString; @@ -33,11 +24,55 @@ mod error { impl Snapshot<'_> { /// Returns the configuration for all git-credential helpers from trusted configuration that apply /// to the given `url` along with an action preconfigured to invoke the cascade with. + /// For details, please see [this function](function::credential_helpers). + pub fn credential_helpers( + &self, + url: gix_url::Url, + ) -> Result< + ( + gix_credentials::helper::Cascade, + gix_credentials::helper::Action, + gix_prompt::Options<'static>, + ), + Error, + > { + let repo = self.repo; + function::credential_helpers( + url, + &repo.config.resolved, + repo.config.lenient_config, + &mut repo.filter_config_section(), + repo.config.environment, + ) + } +} + +pub(super) mod function { + use crate::bstr::{ByteSlice, ByteVec}; + use crate::config::cache::util::ApplyLeniency; + use crate::config::credential_helpers::Error; + use crate::config::tree::gitoxide::Credentials; + use crate::config::tree::{credential, Core, Credential}; + use std::borrow::Cow; + + /// Returns the configuration for all git-credential helpers from trusted configuration that apply + /// to the given `url` along with an action preconfigured to invoke the cascade with to retrieve it. /// This includes `url` which may be altered to contain a user-name as configured. /// /// These can be invoked to obtain credentials. Note that the `url` is expected to be the one used /// to connect to a remote, and thus should already have passed the url-rewrite engine. /// + /// * `config` + /// - the configuration to obtain credential helper configuration from. + /// * `is_lenient_config` + /// - if `true`, minor configuration errors will be ignored. + /// * `filter` + /// - A way to choose which sections in `config` can be trusted. This is important as we will execute programs + /// from the paths contained within. + /// * `environment` + /// - Determines how environment variables can be used. + /// - Actually used are `GIT_*` and `SSH_*` environment variables to configure git prompting capabilities. + /// /// # Deviation /// /// - Invalid urls can't be used to obtain credential helpers as they are rejected early when creating a valid `url` here. @@ -48,8 +83,11 @@ impl Snapshot<'_> { /// a feature or a bug. // TODO: when dealing with `http.*.*` configuration, generalize this algorithm as needed and support precedence. pub fn credential_helpers( - &self, mut url: gix_url::Url, + config: &gix_config::File<'_>, + is_lenient_config: bool, + filter: &mut gix_config::file::MetadataFilter, + environment: crate::open::permissions::Environment, ) -> Result< ( gix_credentials::helper::Cascade, @@ -63,12 +101,7 @@ impl Snapshot<'_> { let url_had_user_initially = url.user().is_some(); normalize(&mut url); - if let Some(credential_sections) = self - .repo - .config - .resolved - .sections_by_name_and_filter("credential", &mut self.repo.filter_config_section()) - { + if let Some(credential_sections) = config.sections_by_name_and_filter("credential", filter) { for section in credential_sections { let section = match section.header().subsection_name() { Some(pattern) => gix_url::parse(pattern).ok().and_then(|mut pattern| { @@ -138,19 +171,24 @@ impl Snapshot<'_> { } } - let allow_git_env = self.repo.options.permissions.env.git_prefix.is_allowed(); - let allow_ssh_env = self.repo.options.permissions.env.ssh_prefix.is_allowed(); + let allow_git_env = environment.git_prefix.is_allowed(); + let allow_ssh_env = environment.ssh_prefix.is_allowed(); let prompt_options = gix_prompt::Options { - askpass: self - .trusted_path(Core::ASKPASS.logical_name().as_str()) - .transpose() - .ignore_empty()? - .map(|c| Cow::Owned(c.into_owned())), - mode: self - .try_boolean(Credentials::TERMINAL_PROMPT.logical_name().as_str()) + askpass: crate::config::cache::access::trusted_file_path( + config, + &Core::ASKPASS, + filter, + is_lenient_config, + environment, + ) + .transpose() + .ignore_empty()? + .map(|c| Cow::Owned(c.into_owned())), + mode: config + .boolean(&Credentials::TERMINAL_PROMPT) .map(|val| Credentials::TERMINAL_PROMPT.enrich_error(val)) .transpose() - .with_leniency(self.repo.config.lenient_config)? + .with_leniency(is_lenient_config)? .and_then(|val| (!val).then_some(gix_prompt::Mode::Disable)) .unwrap_or_default(), } @@ -161,52 +199,52 @@ impl Snapshot<'_> { use_http_path, // The default ssh implementation uses binaries that do their own auth, so our passwords aren't used. query_user_only: url.scheme == gix_url::Scheme::Ssh, - stderr: self - .try_boolean(Credentials::HELPER_STDERR.logical_name().as_str()) + stderr: config + .boolean(&Credentials::HELPER_STDERR) .map(|val| Credentials::HELPER_STDERR.enrich_error(val)) .transpose() - .with_leniency(self.repo.options.lenient_config)? + .with_leniency(is_lenient_config)? .unwrap_or(true), }, gix_credentials::helper::Action::get_for_url(url.to_bstring()), prompt_options, )) } -} -fn host_matches(pattern: Option<&str>, host: Option<&str>) -> bool { - match (pattern, host) { - (Some(pattern), Some(host)) => { - let lfields = pattern.split('.'); - let rfields = host.split('.'); - if lfields.clone().count() != rfields.clone().count() { - return false; + fn host_matches(pattern: Option<&str>, host: Option<&str>) -> bool { + match (pattern, host) { + (Some(pattern), Some(host)) => { + let lfields = pattern.split('.'); + let rfields = host.split('.'); + if lfields.clone().count() != rfields.clone().count() { + return false; + } + lfields.zip(rfields).all(|(pat, value)| { + gix_glob::wildmatch(pat.into(), value.into(), gix_glob::wildmatch::Mode::empty()) + }) } - lfields - .zip(rfields) - .all(|(pat, value)| gix_glob::wildmatch(pat.into(), value.into(), gix_glob::wildmatch::Mode::empty())) + (None, None) => true, + (Some(_), None) | (None, Some(_)) => false, } - (None, None) => true, - (Some(_), None) | (None, Some(_)) => false, } -} -fn normalize(url: &mut gix_url::Url) { - if !url.path_is_root() && url.path.ends_with(b"/") { - url.path.pop(); + fn normalize(url: &mut gix_url::Url) { + if !url.path_is_root() && url.path.ends_with(b"/") { + url.path.pop(); + } } -} -trait IgnoreEmptyPath { - fn ignore_empty(self) -> Self; -} + trait IgnoreEmptyPath { + fn ignore_empty(self) -> Self; + } -impl IgnoreEmptyPath for Result>, gix_config::path::interpolate::Error> { - fn ignore_empty(self) -> Self { - match self { - Ok(maybe_path) => Ok(maybe_path), - Err(gix_config::path::interpolate::Error::Missing { .. }) => Ok(None), - Err(err) => Err(err), + impl IgnoreEmptyPath for Result>, gix_config::path::interpolate::Error> { + fn ignore_empty(self) -> Self { + match self { + Ok(maybe_path) => Ok(maybe_path), + Err(gix_config::path::interpolate::Error::Missing { .. }) => Ok(None), + Err(err) => Err(err), + } } } } diff --git a/gix/src/config/snapshot/mod.rs b/gix/src/config/snapshot/mod.rs index de143ea1f07..173f73bc361 100644 --- a/gix/src/config/snapshot/mod.rs +++ b/gix/src/config/snapshot/mod.rs @@ -4,3 +4,5 @@ mod access; /// #[cfg(feature = "credentials")] pub mod credential_helpers; +#[cfg(feature = "credentials")] +pub use credential_helpers::function::credential_helpers;