Skip to content

Commit

Permalink
Add an interface to allow calling system keyring
Browse files Browse the repository at this point in the history
  • Loading branch information
judahrand committed Nov 9, 2022
1 parent 90f51db commit b87ddb9
Showing 1 changed file with 60 additions and 4 deletions.
64 changes: 60 additions & 4 deletions src/pip/_internal/network/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
providing credentials in the context of network requests.
"""

import shutil
import subprocess
import urllib.parse
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, NamedTuple, Optional, Tuple

from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth
from pip._vendor.requests.models import Request, Response
Expand All @@ -23,11 +25,61 @@

logger = getLogger(__name__)

Credentials = Tuple[str, str, str]

class Credentials(NamedTuple):
service_name: str
username: str
password: str


class KeyRingCredential(NamedTuple):
username: str
password: str


class KeyRingCli:
"""Mirror the parts of keyring's API which pip uses
Instead of calling the keyring package installed alongside pip
we call keyring on the command line which will enable pip to
use which ever installation of keyring is available first in
PATH.
"""

@staticmethod
def _quote(string: Optional[str]) -> str:
return f"'{string}'"

def get_credential(
self, service_name: str, username: Optional[str]
) -> Optional[KeyRingCredential]:
cmd = ["keyring", "get", self._quote(service_name), self._quote(username)]
res = subprocess.run(cmd)
if res.returncode:
return None
return KeyRingCredential(username=username, password=res.stdout)

def set_password(self, service_name: str, username: str, password: str) -> None:
cmd = [
"echo",
self._quote(password),
"|",
"keyring",
"set",
self._quote(service_name),
self._quote(username),
]
res = subprocess.run(cmd)
if res.returncode:
raise RuntimeError(res.stderr)
return None


try:
import keyring
except ImportError:
if shutil.which("keyring") is not None:
keyring = KeyRingCli()
keyring = None # type: ignore[assignment]
except Exception as exc:
logger.warning(
Expand Down Expand Up @@ -276,7 +328,11 @@ def handle_401(self, resp: Response, **kwargs: Any) -> Response:

# Prompt to save the password to keyring
if save and self._should_save_password_to_keyring():
self._credentials_to_save = (parsed.netloc, username, password)
self._credentials_to_save = Credentials(
service_name=parsed.netloc,
username=username,
password=password,
)

# Consume content and release the original connection to allow our new
# request to reuse the same one.
Expand Down Expand Up @@ -318,6 +374,6 @@ def save_credentials(self, resp: Response, **kwargs: Any) -> None:
if creds and resp.status_code < 400:
try:
logger.info("Saving credentials to keyring")
keyring.set_password(*creds)
keyring.set_password(creds.service_name, creds.username, creds.password)
except Exception:
logger.exception("Failed to save credentials")

0 comments on commit b87ddb9

Please sign in to comment.