Repository: pypa/pip (python)

Issue:
Add flag to force pip to always fetch password from keyring
**What's the problem this feature will solve?**

I am trying to fetch modules from a private server. In order to avoid storing this password in filesystem or environment, I have added it to keyring instead. I have also set `https://username@my-private-server.com` as global.index-url in my pip.conf.

Based upon the verbose logs from `pip install`, it seems that pip will first do the request without a password, get a 401, and then fetch the password from keyring.

**Describe the solution you'd like**

There should be a way to force pip to always fetch the password from keyring in the first place. This would prevent it from spamming the server with unauthenticated requests.

**Alternative Solutions**

I don't want to put the password in plaintext in my pip.conf or environment variables, so I'm not aware of any alternative solution.

**Additional context**

n/a


Pull request description:
fix auth cache to allow for username in index url
Currently, if the index url itself contains a username, and the password comes from keyring, then the auth cache is never consulted on subsequent requests. This leads to the undesirable behavior of continually spamming the repository with unauthenticated requests, and also spamming keyring with password lookups. This fixes the auth cache lookup logic to work for this case as well.

Fixes #10269 (in spirit).


Patch:
diff --git a/news/10269.bugfix.rst b/news/10269.bugfix.rst
new file mode 100644
index 000000000..45aee70d8
--- /dev/null
+++ b/news/10269.bugfix.rst
@@ -0,0 +1,3 @@
+Fix the auth credential cache to allow for the case in which
+the index url contains the username, but the password comes
+from an external source, such as keyring.
diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py
index ee27fb67a..ca42798bd 100644
--- a/src/pip/_internal/network/auth.py
+++ b/src/pip/_internal/network/auth.py
@@ -179,9 +179,16 @@ class MultiDomainBasicAuth(AuthBase):
         # Try to get credentials from original url
         username, password = self._get_new_credentials(original_url)
 
-        # If credentials not found, use any stored credentials for this netloc
-        if username is None and password is None:
-            username, password = self.passwords.get(netloc, (None, None))
+        # If credentials not found, use any stored credentials for this netloc.
+        # Do this if either the username or the password is missing.
+        # This accounts for the situation in which the user has specified
+        # the username in the index url, but the password comes from keyring.
+        if (username is None or password is None) and netloc in self.passwords:
+            un, pw = self.passwords[netloc]
+            # It is possible that the cached credentials are for a different username,
+            # in which case the cache should be ignored.
+            if username is None or username == un:
+                username, password = un, pw
 
         if username is not None or password is not None:
             # Convert the username and password if they're None, so that


Test patch:
diff --git a/tests/unit/test_network_auth.py b/tests/unit/test_network_auth.py
index 300c953b0..7be8941fe 100644
--- a/tests/unit/test_network_auth.py
+++ b/tests/unit/test_network_auth.py
@@ -78,6 +78,15 @@ def test_get_credentials_uses_cached_credentials():
     assert got == expected
 
 
+def test_get_credentials_uses_cached_credentials_only_username():
+    auth = MultiDomainBasicAuth()
+    auth.passwords["example.com"] = ("user", "pass")
+
+    got = auth._get_url_and_credentials("http://user@example.com/path")
+    expected = ("http://example.com/path", "user", "pass")
+    assert got == expected
+
+
 def test_get_index_url_credentials():
     auth = MultiDomainBasicAuth(index_urls=["http://foo:bar@example.com/path"])
     get = functools.partial(