Strip the trailing /simple from index URLs provided to uv auth login (#15591)
I'm pretty sure we need to do this for robust lookups of URLs served by indexes.
This commit is contained in:
@@ -7,6 +7,8 @@ use owo_colors::OwoColorize;
|
||||
use uv_auth::Service;
|
||||
use uv_auth::{Credentials, TextCredentialStore};
|
||||
use uv_configuration::KeyringProviderType;
|
||||
use uv_distribution_types::IndexUrl;
|
||||
use uv_pep508::VerbatimUrl;
|
||||
use uv_preview::Preview;
|
||||
|
||||
use crate::commands::auth::AuthBackend;
|
||||
@@ -22,11 +24,19 @@ pub(crate) async fn login(
|
||||
printer: Printer,
|
||||
preview: Preview,
|
||||
) -> Result<ExitStatus> {
|
||||
let url = service.url();
|
||||
let service_clone = service.clone();
|
||||
let url = service_clone.url();
|
||||
let backend = AuthBackend::from_settings(keyring_provider.as_ref(), preview)?;
|
||||
|
||||
// If the URL includes a known index URL suffix, strip it
|
||||
// TODO(zanieb): Use a shared abstraction across `login` and `logout`?
|
||||
let (service, url) = match IndexUrl::from(VerbatimUrl::from_url(url.clone())).root() {
|
||||
Some(root) => (Service::try_from(root.clone())?, root),
|
||||
None => (service, url.clone()),
|
||||
};
|
||||
|
||||
// Extract credentials from URL if present
|
||||
let url_credentials = Credentials::from_url(url);
|
||||
let url_credentials = Credentials::from_url(&url);
|
||||
let url_username = url_credentials.as_ref().and_then(|c| c.username());
|
||||
let url_password = url_credentials.as_ref().and_then(|c| c.password());
|
||||
|
||||
@@ -104,7 +114,7 @@ pub(crate) async fn login(
|
||||
let credentials = Credentials::basic(Some(username), Some(password));
|
||||
match backend {
|
||||
AuthBackend::Keyring(provider) => {
|
||||
provider.store(url, &credentials).await?;
|
||||
provider.store(&url, &credentials).await?;
|
||||
}
|
||||
AuthBackend::TextStore(mut text_store) => {
|
||||
text_store.insert(service.clone(), credentials);
|
||||
|
||||
@@ -6,6 +6,8 @@ use owo_colors::OwoColorize;
|
||||
use uv_auth::Service;
|
||||
use uv_auth::{Credentials, TextCredentialStore};
|
||||
use uv_configuration::KeyringProviderType;
|
||||
use uv_distribution_types::IndexUrl;
|
||||
use uv_pep508::VerbatimUrl;
|
||||
use uv_preview::Preview;
|
||||
|
||||
use crate::commands::auth::AuthBackend;
|
||||
@@ -21,11 +23,18 @@ pub(crate) async fn logout(
|
||||
printer: Printer,
|
||||
preview: Preview,
|
||||
) -> Result<ExitStatus> {
|
||||
let url = service.url();
|
||||
let service_clone = service.clone();
|
||||
let url = service_clone.url();
|
||||
let backend = AuthBackend::from_settings(keyring_provider.as_ref(), preview)?;
|
||||
|
||||
// TODO(zanieb): Use a shared abstraction across `login` and `logout`?
|
||||
let (service, url) = match IndexUrl::from(VerbatimUrl::from_url(url.clone())).root() {
|
||||
Some(root) => (Service::try_from(root.clone())?, root),
|
||||
None => (service, url.clone()),
|
||||
};
|
||||
|
||||
// Extract credentials from URL if present
|
||||
let url_credentials = Credentials::from_url(url);
|
||||
let url_credentials = Credentials::from_url(&url);
|
||||
let url_username = url_credentials.as_ref().and_then(|c| c.username());
|
||||
|
||||
let username = match (username, url_username) {
|
||||
@@ -49,7 +58,7 @@ pub(crate) async fn logout(
|
||||
match backend {
|
||||
AuthBackend::Keyring(provider) => {
|
||||
provider
|
||||
.remove(url, &username)
|
||||
.remove(&url, &username)
|
||||
.await
|
||||
.with_context(|| format!("Unable to remove credentials for {display_url}"))?;
|
||||
}
|
||||
|
||||
+226
-36
@@ -66,25 +66,24 @@ fn add_package_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||||
"
|
||||
);
|
||||
|
||||
// Try to add the original package without credentials again. This should use
|
||||
// credentials storied in the system keyring.
|
||||
uv_snapshot!(context.add().arg("anyio").arg("--default-index").arg("https://public@pypi-proxy.fly.dev/basic-auth/simple"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because anyio was not found in the package registry and your project depends on anyio, we can conclude that your project's requirements are unsatisfiable.
|
||||
|
||||
hint: An index URL (https://pypi-proxy.fly.dev/basic-auth/simple) could not be queried due to a lack of valid authentication credentials (401 Unauthorized).
|
||||
help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.
|
||||
"
|
||||
);
|
||||
|
||||
@@ -99,7 +98,7 @@ fn add_package_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Removed credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Removed credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||||
"
|
||||
);
|
||||
|
||||
@@ -182,7 +181,7 @@ fn token_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||||
"
|
||||
);
|
||||
|
||||
@@ -193,13 +192,13 @@ fn token_native_keyring() -> Result<()> {
|
||||
.arg("public")
|
||||
.arg("--keyring-provider")
|
||||
.arg("native"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
heron
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
error: Failed to fetch credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
");
|
||||
|
||||
// Without the username
|
||||
@@ -247,7 +246,7 @@ fn token_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Stored credentials for https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Stored credentials for https://pypi-proxy.fly.dev/basic-auth
|
||||
"
|
||||
);
|
||||
|
||||
@@ -256,13 +255,13 @@ fn token_native_keyring() -> Result<()> {
|
||||
.arg("https://pypi-proxy.fly.dev/basic-auth/simple")
|
||||
.arg("--keyring-provider")
|
||||
.arg("native"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
heron
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
error: Failed to fetch credentials for https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
");
|
||||
|
||||
context
|
||||
@@ -462,7 +461,7 @@ fn login_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||||
"
|
||||
);
|
||||
|
||||
@@ -497,7 +496,7 @@ fn login_token_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Stored credentials for https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Stored credentials for https://pypi-proxy.fly.dev/basic-auth
|
||||
"
|
||||
);
|
||||
|
||||
@@ -545,7 +544,7 @@ fn logout_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Removed credentials for https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Removed credentials for https://pypi-proxy.fly.dev/basic-auth
|
||||
");
|
||||
|
||||
// Logout before logging in (with a username)
|
||||
@@ -561,7 +560,7 @@ fn logout_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
error: Unable to remove credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
error: Unable to remove credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||||
Caused by: No matching entry found in secure storage
|
||||
");
|
||||
|
||||
@@ -580,7 +579,7 @@ fn logout_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||||
"
|
||||
);
|
||||
|
||||
@@ -596,7 +595,7 @@ fn logout_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
error: Unable to remove credentials for https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
error: Unable to remove credentials for https://pypi-proxy.fly.dev/basic-auth
|
||||
Caused by: No matching entry found in secure storage
|
||||
");
|
||||
|
||||
@@ -613,7 +612,7 @@ fn logout_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Removed credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Removed credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||||
");
|
||||
|
||||
// Login again
|
||||
@@ -640,7 +639,7 @@ fn logout_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Removed credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Removed credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||||
");
|
||||
|
||||
// Conflict between --username and a URL username is rejected
|
||||
@@ -704,7 +703,7 @@ fn logout_token_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Stored credentials for https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Stored credentials for https://pypi-proxy.fly.dev/basic-auth
|
||||
"
|
||||
);
|
||||
|
||||
@@ -719,7 +718,7 @@ fn logout_token_native_keyring() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Removed credentials for https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Removed credentials for https://pypi-proxy.fly.dev/basic-auth
|
||||
");
|
||||
|
||||
Ok(())
|
||||
@@ -799,7 +798,7 @@ fn login_native_keyring_url() {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Stored credentials for test@https://example.com/simple
|
||||
Stored credentials for test@https://example.com/
|
||||
");
|
||||
|
||||
// An invalid URL is rejected
|
||||
@@ -832,7 +831,7 @@ fn login_native_keyring_url() {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Stored credentials for test@https://example.com/simple
|
||||
Stored credentials for test@https://example.com/
|
||||
");
|
||||
|
||||
// URL with embedded username and separate password works
|
||||
@@ -848,7 +847,7 @@ fn login_native_keyring_url() {
|
||||
|
||||
----- stderr -----
|
||||
warning: The native keyring provider is experimental and may change without warning. Pass `--preview-features native-keyring` to disable this warning.
|
||||
Stored credentials for test@https://example.com/simple
|
||||
Stored credentials for test@https://example.com/
|
||||
");
|
||||
|
||||
// Conflict between --username and URL username is rejected
|
||||
@@ -918,7 +917,7 @@ fn login_text_store() {
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||||
"
|
||||
);
|
||||
|
||||
@@ -932,7 +931,7 @@ fn login_text_store() {
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Stored credentials for https://example.com/simple
|
||||
Stored credentials for https://example.com/
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -1013,7 +1012,7 @@ fn logout_text_store() {
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Removed credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Removed credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||||
"
|
||||
);
|
||||
|
||||
@@ -1033,7 +1032,7 @@ fn logout_text_store() {
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Removed credentials for https://example.com/simple
|
||||
Removed credentials for https://example.com/
|
||||
"
|
||||
);
|
||||
}
|
||||
@@ -1056,7 +1055,7 @@ fn auth_disabled_provider_uses_text_store() {
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth/simple
|
||||
Stored credentials for public@https://pypi-proxy.fly.dev/basic-auth
|
||||
"
|
||||
);
|
||||
|
||||
@@ -1076,3 +1075,194 @@ fn auth_disabled_provider_uses_text_store() {
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_text_store_strips_simple_suffix() {
|
||||
let context = TestContext::new_with_versions(&[]);
|
||||
|
||||
// Login with `/simple` suffix - should strip it and store credentials for the root URL
|
||||
uv_snapshot!(context.auth_login()
|
||||
.arg("https://example.com/simple")
|
||||
.arg("--username")
|
||||
.arg("testuser")
|
||||
.arg("--password")
|
||||
.arg("testpass"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Stored credentials for testuser@https://example.com/
|
||||
"
|
||||
);
|
||||
|
||||
// Login with `/+simple` suffix (devpi format) - should also strip it
|
||||
uv_snapshot!(context.auth_login()
|
||||
.arg("https://devpi.example.com/root/+simple")
|
||||
.arg("--username")
|
||||
.arg("devpiuser")
|
||||
.arg("--password")
|
||||
.arg("devpipass"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Stored credentials for devpiuser@https://devpi.example.com/root
|
||||
"
|
||||
);
|
||||
|
||||
// Login with `/Simple` (case insensitive) - should strip it
|
||||
uv_snapshot!(context.auth_login()
|
||||
.arg("https://registry.example.com/Simple")
|
||||
.arg("--username")
|
||||
.arg("caseuser")
|
||||
.arg("--password")
|
||||
.arg("casepass"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Stored credentials for caseuser@https://registry.example.com/
|
||||
"
|
||||
);
|
||||
|
||||
// Login without `/simple` suffix - should store as-is
|
||||
uv_snapshot!(context.auth_login()
|
||||
.arg("https://custom.example.com/api/v1")
|
||||
.arg("--username")
|
||||
.arg("apiuser")
|
||||
.arg("--password")
|
||||
.arg("apipass"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Stored credentials for apiuser@https://custom.example.com/api/v1
|
||||
"
|
||||
);
|
||||
|
||||
// Login with trailing slash and `/simple` - should strip both
|
||||
uv_snapshot!(context.auth_login()
|
||||
.arg("https://trailing.example.com/simple/")
|
||||
.arg("--username")
|
||||
.arg("slashuser")
|
||||
.arg("--password")
|
||||
.arg("slashpass"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Stored credentials for slashuser@https://trailing.example.com/
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logout_text_store_strips_simple_suffix() {
|
||||
let context = TestContext::new_with_versions(&[]);
|
||||
|
||||
// Login with `/simple` suffix first
|
||||
context
|
||||
.auth_login()
|
||||
.arg("https://example.com/simple")
|
||||
.arg("--username")
|
||||
.arg("testuser")
|
||||
.arg("--password")
|
||||
.arg("testpass")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Logout using the same URL with `/simple` - should work
|
||||
uv_snapshot!(context.auth_logout()
|
||||
.arg("https://example.com/simple")
|
||||
.arg("--username")
|
||||
.arg("testuser"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Removed credentials for testuser@https://example.com/
|
||||
"
|
||||
);
|
||||
|
||||
// Login with `/+simple` suffix
|
||||
context
|
||||
.auth_login()
|
||||
.arg("https://devpi.example.com/root/+simple")
|
||||
.arg("--username")
|
||||
.arg("devpiuser")
|
||||
.arg("--password")
|
||||
.arg("devpipass")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Logout using URL with `/+simple` - should work
|
||||
uv_snapshot!(context.auth_logout()
|
||||
.arg("https://devpi.example.com/root/+simple")
|
||||
.arg("--username")
|
||||
.arg("devpiuser"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Removed credentials for devpiuser@https://devpi.example.com/root
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_text_store_strips_simple_suffix() {
|
||||
let context = TestContext::new_with_versions(&[]);
|
||||
|
||||
// Login with `/simple` suffix
|
||||
context
|
||||
.auth_login()
|
||||
.arg("https://example.com/simple")
|
||||
.arg("--username")
|
||||
.arg("testuser")
|
||||
.arg("--password")
|
||||
.arg("testpass")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Retrieve token using URL with `/simple` - should work
|
||||
uv_snapshot!(context.auth_token()
|
||||
.arg("https://example.com/simple")
|
||||
.arg("--username")
|
||||
.arg("testuser"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
testpass
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
);
|
||||
|
||||
// Login with token and `/simple` suffix
|
||||
context
|
||||
.auth_login()
|
||||
.arg("https://token.example.com/simple")
|
||||
.arg("--token")
|
||||
.arg("secret-token")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Retrieve token using URL with `/simple` - should work
|
||||
uv_snapshot!(context.auth_token()
|
||||
.arg("https://token.example.com/simple"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
secret-token
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user