Skip to content

Commit 6885e52

Browse files
author
Aleksandr Mangin
committed
keyring2.0
1 parent 3817aef commit 6885e52

File tree

8 files changed

+94
-520
lines changed

8 files changed

+94
-520
lines changed

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
@@ -256,6 +256,7 @@ def add_options(self) -> None:
256256
self.cmd_opts.add_option(cmdoptions.require_hashes())
257257
self.cmd_opts.add_option(cmdoptions.progress_bar())
258258
self.cmd_opts.add_option(cmdoptions.root_user_action())
259+
self.cmd_opts.add_option(cmdoptions.default_key_ring_user())
259260

260261
index_opts = cmdoptions.make_option_group(
261262
cmdoptions.index_group,

src/pip/_internal/network/auth.py

Lines changed: 69 additions & 145 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__)
@@ -190,11 +183,15 @@ def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[Au
190183

191184
class MultiDomainBasicAuth(AuthBase):
192185
def __init__(
193-
self, prompting: bool = True, index_urls: Optional[List[str]] = None
186+
self,
187+
prompting: bool = True,
188+
index_urls: Optional[List[str]] = None,
189+
default_key_ring_user: Optional[str] = None,
194190
) -> None:
195191
self.prompting = prompting
196192
self.index_urls = index_urls
197193
self.passwords: Dict[str, AuthInfo] = {}
194+
self.default_key_ring_user = default_key_ring_user
198195
# When the user is prompted to enter credentials and keyring is
199196
# available, we will offer to save them. If the user accepts,
200197
# this value is set to the credentials they entered. After the
@@ -227,29 +224,31 @@ def _get_index_url(self, url: str) -> Optional[str]:
227224
def _get_new_credentials(
228225
self,
229226
original_url: str,
230-
allow_netrc: bool = True,
231-
allow_keyring: bool = False,
232227
) -> AuthInfo:
233228
"""Find and return credentials for the specified URL."""
234229
# Split the credentials and netloc from the url.
235230
url, netloc, url_user_password = split_auth_netloc_from_url(
236231
original_url,
237232
)
238233

239-
# Start with the credentials embedded in the url
240234
username, password = url_user_password
241235
if username is not None and password is not None:
242236
logger.debug("Found credentials in url for %s", netloc)
243237
return url_user_password
244238

245-
# Find a matching index url for this request
246-
index_url = self._get_index_url(url)
247-
if index_url:
248-
# Split the credentials from the url.
249-
index_info = split_auth_netloc_from_url(index_url)
250-
if index_info:
251-
index_url, _, index_url_user_password = index_info
252-
logger.debug("Found index url %s", index_url)
239+
def split_index_url_on_url_and_credentials(url):
240+
if not url:
241+
return url, None
242+
index_info = split_auth_netloc_from_url(url)
243+
if not index_info:
244+
return url, None
245+
index_url, _, index_url_user_password = index_info
246+
logger.debug("Found index url %s", index_url)
247+
return index_url, index_url_user_password
248+
249+
index_url, index_url_user_password = split_index_url_on_url_and_credentials(
250+
self._get_index_url(url)
251+
)
253252

254253
# If an index URL was found, try its embedded credentials
255254
if index_url and index_url_user_password[0] is not None:
@@ -258,27 +257,57 @@ def _get_new_credentials(
258257
logger.debug("Found credentials in index url for %s", netloc)
259258
return index_url_user_password
260259

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

281-
return username, password
305+
kr_auth = get_keyring_auth(netloc, key_ring_user)
306+
if kr_auth:
307+
logger.debug("Found credentials in keyring for %s", netloc)
308+
return kr_auth
309+
310+
return None
282311

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