Repository: pypa/pip

Issue:
Pip never asks for password when connecting to private repo
**Environment**

* pip version: 20.0.2
* Python version: 3.8.2
* OS: Arch Linux
* Keyring in use: gnome keyring
<!-- Feel free to add more information about your environment here -->


**Description**
<!-- A clear and concise description of what the bug is. -->
When fetching a package from a private repo for the first time, pip asks
for the username, then fails without asking for the password.

**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
Pip should ask for the password.

**How to Reproduce**
<!-- Describe the steps to reproduce this bug. -->

1. Add `https://py.briq.it/simple/` as extra index url 
2. Run `pip install -v briq_utils`

**Output**

```
$ pip install -v briq_utils                                                                                                                                                       
Defaulting to user installation because normal site-packages is not writeable
Created temporary directory: /tmp/pip-ephem-wheel-cache-h0go_59q
Created temporary directory: /tmp/pip-req-tracker-_zrb96hk
Initialized build tracking at /tmp/pip-req-tracker-_zrb96hk
Created build tracker: /tmp/pip-req-tracker-_zrb96hk
Entered build tracker: /tmp/pip-req-tracker-_zrb96hk
Created temporary directory: /tmp/pip-install-cjulioxj
Looking in indexes: https://pypi.org/simple, https://py.briq.it/simple/
2 location(s) to search for versions of briq-utils:
* https://pypi.org/simple/briq-utils/
* https://py.briq.it/simple/briq-utils/
Fetching project page and analyzing links: https://pypi.org/simple/briq-utils/
Getting page https://pypi.org/simple/briq-utils/
Found index url https://pypi.org/simple
Getting credentials from keyring for https://pypi.org/simple
Getting credentials from keyring for pypi.org
Looking up "https://pypi.org/simple/briq-utils/" in the cache
Request header has "max_age" as 0, cache bypassed
Starting new HTTPS connection (1): pypi.org:443
https://pypi.org:443 "GET /simple/briq-utils/ HTTP/1.1" 404 13
Status code 404 not in (200, 203, 300, 301)
Could not fetch URL https://pypi.org/simple/briq-utils/: 404 Client Error: Not Found for url: https://pypi.org/simple/briq-utils/ - skipping
Fetching project page and analyzing links: https://py.briq.it/simple/briq-utils/
Getting page https://py.briq.it/simple/briq-utils/
Found index url https://py.briq.it/simple/
Getting credentials from keyring for https://py.briq.it/simple/
Getting credentials from keyring for py.briq.it
Looking up "https://py.briq.it/simple/briq-utils/" in the cache
Request header has "max_age" as 0, cache bypassed
Starting new HTTPS connection (1): py.briq.it:443
https://py.briq.it:443 "GET /simple/briq-utils/ HTTP/1.1" 401 727
User for py.briq.it: [type username]
Getting credentials from keyring for py.briq.it
Status code 401 not in (200, 203, 300, 301)
Looking up "https://py.briq.it/simple/briq-utils/" in the cache
Request header has "max_age" as 0, cache bypassed
https://py.briq.it:443 "GET /simple/briq-utils/ HTTP/1.1" 403 721
Status code 403 not in (200, 203, 300, 301)
Could not fetch URL https://py.briq.it/simple/briq-utils/: 403 Client Error: Forbidden for url: https://py.briq.it/simple/briq-utils/ - skipping
[...]
```

**Workaround**

```
>>> import keyring
>>> keyring.set_password("py.briq.it", "myusername", "mypassword")
```


Discussion:
The problem seems to be in `_internal/network/auth.py`, where function `_prompt_for_password` calls `get_keyring_auth` after reading the username; however, at least with my keyring, an object that is not None is returned even if there is no credential (`auth.password` is None). 

It is a matter of opinion if `get_keyring_auth` should be called at all: one would expect the program to always ask for the password after the username.  
> one would expect the program to always ask for the password after the username.

I’m less sure about that; it seems natural to me for a program to auto-fill the password if it “knows” it already.

Do you happen to know what is returned by `keyring.get_password()` in this case? Maybe it’s just a matter of improving detection.
> I’m less sure about that; it seems natural to me for a program to auto-fill the password if it “knows” it already.

For CLI, I don't think that's a good idea, since it's difficult to make good UX for accessing (changing/deleting) the saved pasword.
> Do you happen to know what is returned by `keyring.get_password()` in this case? Maybe it’s just a matter of improving detection.

In my case, `keyring.get_password` returns `None`, so that in `_prompt_for_password`, `auth[0] == "myusername"` and `auth[1] is None`

> In my case, `keyring.get_password` returns None, so that in `_prompt_for_password`, `auth[0] == "myusername"` and `auth[1]` is None

Sounds like we just need to `get_keyring_auth()` to detect this.
I would like to resolve this issue.

To me, it seems that using the condition as `if auth[0] and auth[1]` in place of the currently used condition i.e. `if auth` here https://github.com/pypa/pip/blob/master/src/pip/_internal/network/auth.py#L222, would resolve the problem. Am I seeking the correct approach?


Resolving patch:
diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py
--- a/src/pip/_internal/network/auth.py
+++ b/src/pip/_internal/network/auth.py
@@ -219,7 +219,7 @@ def _prompt_for_password(self, netloc):
         if not username:
             return None, None
         auth = get_keyring_auth(netloc, username)
-        if auth:
+        if auth and auth[0] is not None and auth[1] is not None:
             return auth[0], auth[1], False
         password = ask_password("Password: ")
         return username, password, True


Test patch:
diff --git a/tests/unit/test_network_auth.py b/tests/unit/test_network_auth.py
--- a/tests/unit/test_network_auth.py
+++ b/tests/unit/test_network_auth.py
@@ -123,6 +123,26 @@ def ask_input(prompt):
     assert actual == ("user", "user!netloc", False)
 
 
+def test_keyring_get_password_after_prompt_when_none(monkeypatch):
+    keyring = KeyringModuleV1()
+    monkeypatch.setattr('pip._internal.network.auth.keyring', keyring)
+    auth = MultiDomainBasicAuth()
+
+    def ask_input(prompt):
+        assert prompt == "User for unknown.com: "
+        return "user"
+
+    def ask_password(prompt):
+        assert prompt == "Password: "
+        return "fake_password"
+
+    monkeypatch.setattr('pip._internal.network.auth.ask_input', ask_input)
+    monkeypatch.setattr(
+        'pip._internal.network.auth.ask_password', ask_password)
+    actual = auth._prompt_for_password("unknown.com")
+    assert actual == ("user", "fake_password", True)
+
+
 def test_keyring_get_password_username_in_index(monkeypatch):
     keyring = KeyringModuleV1()
     monkeypatch.setattr('pip._internal.network.auth.keyring', keyring)