Skip to content

Commit c62f8d7

Browse files
Redact index credentials from lockfile sources (#8307)
## Summary Closes #8296.
1 parent 7beb5eb commit c62f8d7

File tree

4 files changed

+240
-16
lines changed

4 files changed

+240
-16
lines changed

crates/uv-pypi-types/src/requirement.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@ impl Requirement {
8282
url,
8383
} => {
8484
// Redact the repository URL, but allow `git@`.
85-
redact_git_credentials(&mut repository);
85+
redact_credentials(&mut repository);
8686

8787
// Redact the PEP 508 URL.
8888
let mut url = url.to_url();
89-
redact_git_credentials(&mut url);
89+
redact_credentials(&mut url);
9090
let url = VerbatimUrl::from_url(url);
9191

9292
Self {
@@ -637,7 +637,7 @@ impl From<RequirementSource> for RequirementSourceWire {
637637
let mut url = repository;
638638

639639
// Redact the credentials.
640-
redact_git_credentials(&mut url);
640+
redact_credentials(&mut url);
641641

642642
// Clear out any existing state.
643643
url.set_fragment(None);
@@ -740,7 +740,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
740740
repository.set_query(None);
741741

742742
// Redact the credentials.
743-
redact_git_credentials(&mut repository);
743+
redact_credentials(&mut repository);
744744

745745
// Create a PEP 508-compatible URL.
746746
let mut url = Url::parse(&format!("git+{repository}"))?;
@@ -814,9 +814,9 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
814814
}
815815
}
816816

817-
/// Remove the credentials from a Git URL, allowing the generic `git` username (without a password)
817+
/// Remove the credentials from a URL, allowing the generic `git` username (without a password)
818818
/// in SSH URLs, as in, `ssh://[email protected]/...`.
819-
pub fn redact_git_credentials(url: &mut Url) {
819+
pub fn redact_credentials(url: &mut Url) {
820820
// For URLs that use the `git` convention (i.e., `ssh://[email protected]/...`), avoid dropping the
821821
// username.
822822
if url.scheme() == "ssh" && url.username() == "git" && url.password().is_none() {

crates/uv-resolver/src/lock/mod.rs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ use uv_pep440::Version;
3131
use uv_pep508::{split_scheme, MarkerEnvironment, MarkerTree, VerbatimUrl, VerbatimUrlError};
3232
use uv_platform_tags::{TagCompatibility, TagPriority, Tags};
3333
use uv_pypi_types::{
34-
redact_git_credentials, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement,
35-
RequirementSource, ResolverMarkerEnvironment,
34+
redact_credentials, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement, RequirementSource,
35+
ResolverMarkerEnvironment,
3636
};
3737
use uv_types::{BuildContext, HashStrategy};
3838
use uv_workspace::{InstallTarget, Workspace};
@@ -3097,7 +3097,7 @@ fn locked_git_url(git_dist: &GitSourceDist) -> Url {
30973097
let mut url = git_dist.git.repository().clone();
30983098

30993099
// Redact the credentials.
3100-
redact_git_credentials(&mut url);
3100+
redact_credentials(&mut url);
31013101

31023102
// Clear out any existing state.
31033103
url.set_fragment(None);
@@ -3686,11 +3686,11 @@ fn normalize_requirement(
36863686
url,
36873687
} => {
36883688
// Redact the credentials.
3689-
redact_git_credentials(&mut repository);
3689+
redact_credentials(&mut repository);
36903690

36913691
// Redact the PEP 508 URL.
36923692
let mut url = url.to_url();
3693-
redact_git_credentials(&mut url);
3693+
redact_credentials(&mut url);
36943694
let url = VerbatimUrl::from_url(url);
36953695

36963696
Ok(Requirement {
@@ -3751,11 +3751,36 @@ fn normalize_requirement(
37513751
origin: None,
37523752
})
37533753
}
3754-
_ => Ok(Requirement {
3754+
RequirementSource::Registry {
3755+
specifier,
3756+
mut index,
3757+
} => {
3758+
if let Some(index) = index.as_mut() {
3759+
redact_credentials(index);
3760+
}
3761+
Ok(Requirement {
3762+
name: requirement.name,
3763+
extras: requirement.extras,
3764+
marker: requirement.marker,
3765+
source: RequirementSource::Registry { specifier, index },
3766+
origin: None,
3767+
})
3768+
}
3769+
RequirementSource::Url {
3770+
location,
3771+
subdirectory,
3772+
ext,
3773+
url,
3774+
} => Ok(Requirement {
37553775
name: requirement.name,
37563776
extras: requirement.extras,
37573777
marker: requirement.marker,
3758-
source: requirement.source,
3778+
source: RequirementSource::Url {
3779+
location,
3780+
subdirectory,
3781+
ext,
3782+
url,
3783+
},
37593784
origin: None,
37603785
}),
37613786
}

crates/uv/src/commands/project/add.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use uv_fs::Simplified;
2323
use uv_git::{GitReference, GIT_STORE};
2424
use uv_normalize::PackageName;
2525
use uv_pep508::{ExtraName, Requirement, UnnamedRequirement, VersionOrUrl};
26-
use uv_pypi_types::{redact_git_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl};
26+
use uv_pypi_types::{redact_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl};
2727
use uv_python::{
2828
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
2929
PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, VersionRequest,
@@ -448,7 +448,7 @@ pub(crate) async fn add(
448448
GIT_STORE.insert(RepositoryUrl::new(&git), credentials);
449449

450450
// Redact the credentials.
451-
redact_git_credentials(&mut git);
451+
redact_credentials(&mut git);
452452
};
453453
Some(Source::Git {
454454
git,

crates/uv/tests/it/lock.rs

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6342,7 +6342,6 @@ fn lock_redact_git_pep508() -> Result<()> {
63426342
Ok(())
63436343
}
63446344

6345-
/// However, we don't currently avoid persisting Git credentials in `uv.lock`.
63466345
#[test]
63476346
fn lock_redact_git_sources() -> Result<()> {
63486347
let context = TestContext::new("3.12").with_filtered_link_mode_warning();
@@ -6439,6 +6438,206 @@ fn lock_redact_git_sources() -> Result<()> {
64396438
Ok(())
64406439
}
64416440

6441+
#[test]
6442+
fn lock_redact_index_sources() -> Result<()> {
6443+
let context = TestContext::new("3.12").with_filtered_link_mode_warning();
6444+
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
6445+
6446+
let filters: Vec<_> = [(token.as_str(), "***")]
6447+
.into_iter()
6448+
.chain(context.filters())
6449+
.collect();
6450+
6451+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
6452+
pyproject_toml.write_str(
6453+
r#"
6454+
[project]
6455+
name = "foo"
6456+
version = "0.1.0"
6457+
requires-python = ">=3.12"
6458+
dependencies = ["iniconfig>=2"]
6459+
6460+
[build-system]
6461+
requires = ["setuptools>=42"]
6462+
build-backend = "setuptools.build_meta"
6463+
6464+
[[tool.uv.index]]
6465+
name = "private"
6466+
url = "https://public:[email protected]/basic-auth/simple"
6467+
6468+
[tool.uv.sources]
6469+
iniconfig = { index = "private" }
6470+
"#,
6471+
)?;
6472+
6473+
uv_snapshot!(&filters, context.lock(), @r###"
6474+
success: true
6475+
exit_code: 0
6476+
----- stdout -----
6477+
6478+
----- stderr -----
6479+
Resolved 2 packages in [TIME]
6480+
"###);
6481+
6482+
let lock = context.read("uv.lock");
6483+
6484+
insta::with_settings!({
6485+
filters => filters.clone(),
6486+
}, {
6487+
assert_snapshot!(
6488+
lock, @r###"
6489+
version = 1
6490+
requires-python = ">=3.12"
6491+
6492+
[options]
6493+
exclude-newer = "2024-03-25T00:00:00Z"
6494+
6495+
[[package]]
6496+
name = "foo"
6497+
version = "0.1.0"
6498+
source = { editable = "." }
6499+
dependencies = [
6500+
{ name = "iniconfig" },
6501+
]
6502+
6503+
[package.metadata]
6504+
requires-dist = [{ name = "iniconfig", specifier = ">=2", index = "https://public:[email protected]/basic-auth/simple" }]
6505+
6506+
[[package]]
6507+
name = "iniconfig"
6508+
version = "2.0.0"
6509+
source = { registry = "https://pypi-proxy.fly.dev/basic-auth/simple" }
6510+
sdist = { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
6511+
wheels = [
6512+
{ url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
6513+
]
6514+
"###
6515+
);
6516+
});
6517+
6518+
// Re-run with `--locked`.
6519+
uv_snapshot!(&filters, context.lock().arg("--locked"), @r###"
6520+
success: true
6521+
exit_code: 0
6522+
----- stdout -----
6523+
6524+
----- stderr -----
6525+
Resolved 2 packages in [TIME]
6526+
"###);
6527+
6528+
// Install from the lockfile.
6529+
uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###"
6530+
success: true
6531+
exit_code: 0
6532+
----- stdout -----
6533+
6534+
----- stderr -----
6535+
Prepared 2 packages in [TIME]
6536+
Installed 2 packages in [TIME]
6537+
+ foo==0.1.0 (from file://[TEMP_DIR]/)
6538+
+ iniconfig==2.0.0
6539+
"###);
6540+
6541+
Ok(())
6542+
}
6543+
6544+
/// We don't currently redact credentials from direct URLs, though.
6545+
#[test]
6546+
fn lock_redact_url_sources() -> Result<()> {
6547+
let context = TestContext::new("3.12").with_filtered_link_mode_warning();
6548+
let token = decode_token(common::READ_ONLY_GITHUB_TOKEN);
6549+
6550+
let filters: Vec<_> = [(token.as_str(), "***")]
6551+
.into_iter()
6552+
.chain(context.filters())
6553+
.collect();
6554+
6555+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
6556+
pyproject_toml.write_str(r#"
6557+
[project]
6558+
name = "foo"
6559+
version = "0.1.0"
6560+
requires-python = ">=3.12"
6561+
dependencies = ["iniconfig>=2"]
6562+
6563+
[build-system]
6564+
requires = ["setuptools>=42"]
6565+
build-backend = "setuptools.build_meta"
6566+
6567+
[tool.uv.sources]
6568+
iniconfig = { url = "https://public:[email protected]/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }
6569+
"#)?;
6570+
6571+
uv_snapshot!(&filters, context.lock(), @r###"
6572+
success: true
6573+
exit_code: 0
6574+
----- stdout -----
6575+
6576+
----- stderr -----
6577+
Resolved 2 packages in [TIME]
6578+
"###);
6579+
6580+
let lock = context.read("uv.lock");
6581+
6582+
insta::with_settings!({
6583+
filters => filters.clone(),
6584+
}, {
6585+
assert_snapshot!(
6586+
lock, @r###"
6587+
version = 1
6588+
requires-python = ">=3.12"
6589+
6590+
[options]
6591+
exclude-newer = "2024-03-25T00:00:00Z"
6592+
6593+
[[package]]
6594+
name = "foo"
6595+
version = "0.1.0"
6596+
source = { editable = "." }
6597+
dependencies = [
6598+
{ name = "iniconfig" },
6599+
]
6600+
6601+
[package.metadata]
6602+
requires-dist = [{ name = "iniconfig", url = "https://public:[email protected]/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }]
6603+
6604+
[[package]]
6605+
name = "iniconfig"
6606+
version = "2.0.0"
6607+
source = { url = "https://public:[email protected]/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }
6608+
wheels = [
6609+
{ url = "https://public:[email protected]/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" },
6610+
]
6611+
"###
6612+
);
6613+
});
6614+
6615+
// Re-run with `--locked`.
6616+
uv_snapshot!(&filters, context.lock().arg("--locked"), @r###"
6617+
success: true
6618+
exit_code: 0
6619+
----- stdout -----
6620+
6621+
----- stderr -----
6622+
Resolved 2 packages in [TIME]
6623+
"###);
6624+
6625+
// Install from the lockfile.
6626+
uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###"
6627+
success: true
6628+
exit_code: 0
6629+
----- stdout -----
6630+
6631+
----- stderr -----
6632+
Prepared 2 packages in [TIME]
6633+
Installed 2 packages in [TIME]
6634+
+ foo==0.1.0 (from file://[TEMP_DIR]/)
6635+
+ iniconfig==2.0.0 (from https://public:[email protected]/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
6636+
"###);
6637+
6638+
Ok(())
6639+
}
6640+
64426641
/// Pass credentials for a named index via environment variables.
64436642
#[test]
64446643
fn lock_env_credentials() -> Result<()> {

0 commit comments

Comments
 (0)