Skip to content

Commit b0ff975

Browse files
author
Aleksandr Mangin
committed
keyring2.0
1 parent 5e20a7b commit b0ff975

File tree

9 files changed

+96
-518
lines changed

9 files changed

+96
-518
lines changed

news/11823.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improved integration with keyring by removing logic of promting username.
2+
Now the keyring integration tries to figure out username from environment.

src/pip/_internal/cli/cmdoptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,13 @@ def _handle_config_settings(
883883
help="Don't periodically check PyPI to determine whether a new version "
884884
"of pip is available for download. Implied with --no-index.",
885885
)
886+
default_key_ring_user: Callable[..., Option] = partial(
887+
Option,
888+
"--default-key-ring-user",
889+
dest="default_key_ring_user",
890+
default=None,
891+
help="Default key ring user that pip can use to get credentials from keyring",
892+
)
886893

887894
root_user_action: Callable[..., Option] = partial(
888895
Option,

src/pip/_internal/cli/req_command.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ def _build_session(
128128
trusted_hosts=options.trusted_hosts,
129129
index_urls=self._get_index_urls(options),
130130
ssl_context=ssl_context,
131+
default_key_ring_user=getattr(options, "default_key_ring_user", None),
131132
)
132133

133134
# Handle custom ca-bundles from the user

src/pip/_internal/commands/install.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ def add_options(self) -> None:
261261
self.cmd_opts.add_option(cmdoptions.require_hashes())
262262
self.cmd_opts.add_option(cmdoptions.progress_bar())
263263
self.cmd_opts.add_option(cmdoptions.root_user_action())
264+
self.cmd_opts.add_option(cmdoptions.default_key_ring_user())
264265

265266
index_opts = cmdoptions.make_option_group(
266267
cmdoptions.index_group,

src/pip/_internal/network/auth.py

Lines changed: 69 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,15 @@
77
import os
88
import shutil
99
import subprocess
10-
import urllib.parse
1110
from abc import ABC, abstractmethod
12-
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
11+
from typing import Dict, List, NamedTuple, Optional, Tuple
1312

1413
from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth
15-
from pip._vendor.requests.models import Request, Response
14+
from pip._vendor.requests.models import Request
1615
from pip._vendor.requests.utils import get_netrc_auth
1716

1817
from pip._internal.utils.logging import getLogger
19-
from pip._internal.utils.misc import (
20-
ask,
21-
ask_input,
22-
ask_password,
23-
remove_auth_from_url,
24-
split_auth_netloc_from_url,
25-
)
18+
from pip._internal.utils.misc import remove_auth_from_url, split_auth_netloc_from_url
2619
from pip._internal.vcs.versioncontrol import AuthInfo
2720

2821
logger = getLogger(__name__)
@@ -198,11 +191,15 @@ def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[Au
198191

199192
class MultiDomainBasicAuth(AuthBase):
200193
def __init__(
201-
self, prompting: bool = True, index_urls: Optional[List[str]] = None
194+
self,
195+
prompting: bool = True,
196+
index_urls: Optional[List[str]] = None,
197+
default_key_ring_user: Optional[str] = None,
202198
) -> None:
203199
self.prompting = prompting
204200
self.index_urls = index_urls
205201
self.passwords: Dict[str, AuthInfo] = {}
202+
self.default_key_ring_user = default_key_ring_user
206203
# When the user is prompted to enter credentials and keyring is
207204
# available, we will offer to save them. If the user accepts,
208205
# this value is set to the credentials they entered. After the
@@ -235,29 +232,31 @@ def _get_index_url(self, url: str) -> Optional[str]:
235232
def _get_new_credentials(
236233
self,
237234
original_url: str,
238-
allow_netrc: bool = True,
239-
allow_keyring: bool = False,
240235
) -> AuthInfo:
241236
"""Find and return credentials for the specified URL."""
242237
# Split the credentials and netloc from the url.
243238
url, netloc, url_user_password = split_auth_netloc_from_url(
244239
original_url,
245240
)
246241

247-
# Start with the credentials embedded in the url
248242
username, password = url_user_password
249243
if username is not None and password is not None:
250244
logger.debug("Found credentials in url for %s", netloc)
251245
return url_user_password
252246

253-
# Find a matching index url for this request
254-
index_url = self._get_index_url(url)
255-
if index_url:
256-
# Split the credentials from the url.
257-
index_info = split_auth_netloc_from_url(index_url)
258-
if index_info:
259-
index_url, _, index_url_user_password = index_info
260-
logger.debug("Found index url %s", index_url)
247+
def split_index_url_on_url_and_credentials(url):
248+
if not url:
249+
return url, None
250+
index_info = split_auth_netloc_from_url(url)
251+
if not index_info:
252+
return url, None
253+
index_url, _, index_url_user_password = index_info
254+
logger.debug("Found index url %s", index_url)
255+
return index_url, index_url_user_password
256+
257+
index_url, index_url_user_password = split_index_url_on_url_and_credentials(
258+
self._get_index_url(url)
259+
)
261260

262261
# If an index URL was found, try its embedded credentials
263262
if index_url and index_url_user_password[0] is not None:
@@ -266,27 +265,57 @@ def _get_new_credentials(
266265
logger.debug("Found credentials in index url for %s", netloc)
267266
return index_url_user_password
268267

269-
# Get creds from netrc if we still don't have them
270-
if allow_netrc:
271-
netrc_auth = get_netrc_auth(original_url)
272-
if netrc_auth:
273-
logger.debug("Found credentials in netrc for %s", netloc)
274-
return netrc_auth
275-
276-
# If we don't have a password and keyring is available, use it.
277-
if allow_keyring:
278-
# The index url is more specific than the netloc, so try it first
279-
# fmt: off
280-
kr_auth = (
281-
get_keyring_auth(index_url, username) or
282-
get_keyring_auth(netloc, username)
283-
)
284-
# fmt: on
268+
netrc_auth = get_netrc_auth(original_url)
269+
if netrc_auth is not None:
270+
logger.debug("Found credentials in netrc for %s", netloc)
271+
return netrc_auth
272+
273+
kr_auth = self._find_key_ring_credentials(
274+
index_url, index_url_user_password, netloc, username
275+
)
276+
if kr_auth is not None:
277+
return kr_auth
278+
279+
return username, password
280+
281+
def _find_key_ring_credentials(
282+
self,
283+
index_url: Optional[str],
284+
index_url_user_password: Optional[str],
285+
netloc: str,
286+
artifact_username: str,
287+
) -> Optional[AuthInfo]:
288+
def get_key_ring_user() -> Optional[str]:
289+
if artifact_username is not None:
290+
return artifact_username
291+
if index_url_user_password:
292+
if (
293+
index_url_user_password[0] is not None
294+
and index_url_user_password[1] is None
295+
):
296+
logger.debug("Found key ring username in index_url")
297+
return index_url_user_password[0]
298+
if artifact_username is None and self.default_key_ring_user is not None:
299+
logger.debug("Using default_key_ring_user")
300+
return self.default_key_ring_user
301+
return None
302+
303+
key_ring_user = get_key_ring_user()
304+
if key_ring_user is None:
305+
return None
306+
307+
if index_url is not None:
308+
kr_auth = get_keyring_auth(index_url, key_ring_user)
285309
if kr_auth:
286-
logger.debug("Found credentials in keyring for %s", netloc)
310+
logger.debug("Found credentials in keyring for %s", index_url)
287311
return kr_auth
288312

289-
return username, password
313+
kr_auth = get_keyring_auth(netloc, key_ring_user)
314+
if kr_auth:
315+
logger.debug("Found credentials in keyring for %s", netloc)
316+
return kr_auth
317+
318+
return None
290319

291320
def _get_url_and_credentials(
292321
self, original_url: str
@@ -346,107 +375,4 @@ def __call__(self, req: Request) -> Request:
346375
if username is not None and password is not None:
347376
# Send the basic auth with this request
348377
req = HTTPBasicAuth(username, password)(req)
349-
350-
# Attach a hook to handle 401 responses
351-
req.register_hook("response", self.handle_401)
352-
353378
return req
354-
355-
# Factored out to allow for easy patching in tests
356-
def _prompt_for_password(
357-
self, netloc: str
358-
) -> Tuple[Optional[str], Optional[str], bool]:
359-
username = ask_input(f"User for {netloc}: ")
360-
if not username:
361-
return None, None, False
362-
auth = get_keyring_auth(netloc, username)
363-
if auth and auth[0] is not None and auth[1] is not None:
364-
return auth[0], auth[1], False
365-
password = ask_password("Password: ")
366-
return username, password, True
367-
368-
# Factored out to allow for easy patching in tests
369-
def _should_save_password_to_keyring(self) -> bool:
370-
if not get_keyring_provider().has_keyring:
371-
return False
372-
return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y"
373-
374-
def handle_401(self, resp: Response, **kwargs: Any) -> Response:
375-
# We only care about 401 responses, anything else we want to just
376-
# pass through the actual response
377-
if resp.status_code != 401:
378-
return resp
379-
380-
# We are not able to prompt the user so simply return the response
381-
if not self.prompting:
382-
return resp
383-
384-
parsed = urllib.parse.urlparse(resp.url)
385-
386-
# Query the keyring for credentials:
387-
username, password = self._get_new_credentials(
388-
resp.url,
389-
allow_netrc=False,
390-
allow_keyring=True,
391-
)
392-
393-
# Prompt the user for a new username and password
394-
save = False
395-
if not username and not password:
396-
username, password, save = self._prompt_for_password(parsed.netloc)
397-
398-
# Store the new username and password to use for future requests
399-
self._credentials_to_save = None
400-
if username is not None and password is not None:
401-
self.passwords[parsed.netloc] = (username, password)
402-
403-
# Prompt to save the password to keyring
404-
if save and self._should_save_password_to_keyring():
405-
self._credentials_to_save = Credentials(
406-
url=parsed.netloc,
407-
username=username,
408-
password=password,
409-
)
410-
411-
# Consume content and release the original connection to allow our new
412-
# request to reuse the same one.
413-
resp.content
414-
resp.raw.release_conn()
415-
416-
# Add our new username and password to the request
417-
req = HTTPBasicAuth(username or "", password or "")(resp.request)
418-
req.register_hook("response", self.warn_on_401)
419-
420-
# On successful request, save the credentials that were used to
421-
# keyring. (Note that if the user responded "no" above, this member
422-
# is not set and nothing will be saved.)
423-
if self._credentials_to_save:
424-
req.register_hook("response", self.save_credentials)
425-
426-
# Send our new request
427-
new_resp = resp.connection.send(req, **kwargs)
428-
new_resp.history.append(resp)
429-
430-
return new_resp
431-
432-
def warn_on_401(self, resp: Response, **kwargs: Any) -> None:
433-
"""Response callback to warn about incorrect credentials."""
434-
if resp.status_code == 401:
435-
logger.warning(
436-
"401 Error, Credentials not correct for %s",
437-
resp.request.url,
438-
)
439-
440-
def save_credentials(self, resp: Response, **kwargs: Any) -> None:
441-
"""Response callback to save credentials on success."""
442-
keyring = get_keyring_provider()
443-
assert keyring.has_keyring, "should never reach here without keyring"
444-
445-
creds = self._credentials_to_save
446-
self._credentials_to_save = None
447-
if creds and resp.status_code < 400:
448-
try:
449-
logger.info("Saving credentials to keyring")
450-
keyring.save_auth_info(creds.url, creds.username, creds.password)
451-
except Exception:
452-
logger.exception("Failed to save credentials")

src/pip/_internal/network/session.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ def __init__(
327327
trusted_hosts: Sequence[str] = (),
328328
index_urls: Optional[List[str]] = None,
329329
ssl_context: Optional["SSLContext"] = None,
330+
default_key_ring_user: Optional[str] = None,
330331
**kwargs: Any,
331332
) -> None:
332333
"""
@@ -343,7 +344,10 @@ def __init__(
343344
self.headers["User-Agent"] = user_agent()
344345

345346
# Attach our Authentication handler to the session
346-
self.auth = MultiDomainBasicAuth(index_urls=index_urls)
347+
self.auth = MultiDomainBasicAuth(
348+
index_urls=index_urls,
349+
default_key_ring_user=default_key_ring_user,
350+
)
347351

348352
# Create our urllib3.Retry instance which will allow us to customize
349353
# how we handle retries.

src/pip/_internal/utils/misc.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import contextlib
55
import errno
6-
import getpass
76
import hashlib
87
import io
98
import logging
@@ -199,18 +198,6 @@ def ask(message: str, options: Iterable[str]) -> str:
199198
return response
200199

201200

202-
def ask_input(message: str) -> str:
203-
"""Ask for input interactively."""
204-
_check_no_input(message)
205-
return input(message)
206-
207-
208-
def ask_password(message: str) -> str:
209-
"""Ask for a password interactively."""
210-
_check_no_input(message)
211-
return getpass.getpass(message)
212-
213-
214201
def strtobool(val: str) -> int:
215202
"""Convert a string representation of truth to true (1) or false (0).
216203

0 commit comments

Comments
 (0)