auth.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. """Network Authentication Helpers
  2. Contains interface (MultiDomainBasicAuth) and associated glue code for
  3. providing credentials in the context of network requests.
  4. """
  5. import logging
  6. from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth
  7. from pip._vendor.requests.utils import get_netrc_auth
  8. from pip._vendor.six.moves.urllib import parse as urllib_parse
  9. from pip._internal.utils.misc import (
  10. ask,
  11. ask_input,
  12. ask_password,
  13. remove_auth_from_url,
  14. split_auth_netloc_from_url,
  15. )
  16. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  17. if MYPY_CHECK_RUNNING:
  18. from typing import Dict, Optional, Tuple, List, Any
  19. from pip._internal.vcs.versioncontrol import AuthInfo
  20. from pip._vendor.requests.models import Response, Request
  21. Credentials = Tuple[str, str, str]
  22. logger = logging.getLogger(__name__)
  23. try:
  24. import keyring # noqa
  25. except ImportError:
  26. keyring = None
  27. except Exception as exc:
  28. logger.warning(
  29. "Keyring is skipped due to an exception: %s", str(exc),
  30. )
  31. keyring = None
  32. def get_keyring_auth(url, username):
  33. # type: (str, str) -> Optional[AuthInfo]
  34. """Return the tuple auth for a given url from keyring."""
  35. global keyring
  36. if not url or not keyring:
  37. return None
  38. try:
  39. try:
  40. get_credential = keyring.get_credential
  41. except AttributeError:
  42. pass
  43. else:
  44. logger.debug("Getting credentials from keyring for %s", url)
  45. cred = get_credential(url, username)
  46. if cred is not None:
  47. return cred.username, cred.password
  48. return None
  49. if username:
  50. logger.debug("Getting password from keyring for %s", url)
  51. password = keyring.get_password(url, username)
  52. if password:
  53. return username, password
  54. except Exception as exc:
  55. logger.warning(
  56. "Keyring is skipped due to an exception: %s", str(exc),
  57. )
  58. keyring = None
  59. return None
  60. class MultiDomainBasicAuth(AuthBase):
  61. def __init__(self, prompting=True, index_urls=None):
  62. # type: (bool, Optional[List[str]]) -> None
  63. self.prompting = prompting
  64. self.index_urls = index_urls
  65. self.passwords = {} # type: Dict[str, AuthInfo]
  66. # When the user is prompted to enter credentials and keyring is
  67. # available, we will offer to save them. If the user accepts,
  68. # this value is set to the credentials they entered. After the
  69. # request authenticates, the caller should call
  70. # ``save_credentials`` to save these.
  71. self._credentials_to_save = None # type: Optional[Credentials]
  72. def _get_index_url(self, url):
  73. # type: (str) -> Optional[str]
  74. """Return the original index URL matching the requested URL.
  75. Cached or dynamically generated credentials may work against
  76. the original index URL rather than just the netloc.
  77. The provided url should have had its username and password
  78. removed already. If the original index url had credentials then
  79. they will be included in the return value.
  80. Returns None if no matching index was found, or if --no-index
  81. was specified by the user.
  82. """
  83. if not url or not self.index_urls:
  84. return None
  85. for u in self.index_urls:
  86. prefix = remove_auth_from_url(u).rstrip("/") + "/"
  87. if url.startswith(prefix):
  88. return u
  89. return None
  90. def _get_new_credentials(self, original_url, allow_netrc=True,
  91. allow_keyring=True):
  92. # type: (str, bool, bool) -> AuthInfo
  93. """Find and return credentials for the specified URL."""
  94. # Split the credentials and netloc from the url.
  95. url, netloc, url_user_password = split_auth_netloc_from_url(
  96. original_url,
  97. )
  98. # Start with the credentials embedded in the url
  99. username, password = url_user_password
  100. if username is not None and password is not None:
  101. logger.debug("Found credentials in url for %s", netloc)
  102. return url_user_password
  103. # Find a matching index url for this request
  104. index_url = self._get_index_url(url)
  105. if index_url:
  106. # Split the credentials from the url.
  107. index_info = split_auth_netloc_from_url(index_url)
  108. if index_info:
  109. index_url, _, index_url_user_password = index_info
  110. logger.debug("Found index url %s", index_url)
  111. # If an index URL was found, try its embedded credentials
  112. if index_url and index_url_user_password[0] is not None:
  113. username, password = index_url_user_password
  114. if username is not None and password is not None:
  115. logger.debug("Found credentials in index url for %s", netloc)
  116. return index_url_user_password
  117. # Get creds from netrc if we still don't have them
  118. if allow_netrc:
  119. netrc_auth = get_netrc_auth(original_url)
  120. if netrc_auth:
  121. logger.debug("Found credentials in netrc for %s", netloc)
  122. return netrc_auth
  123. # If we don't have a password and keyring is available, use it.
  124. if allow_keyring:
  125. # The index url is more specific than the netloc, so try it first
  126. kr_auth = (
  127. get_keyring_auth(index_url, username) or
  128. get_keyring_auth(netloc, username)
  129. )
  130. if kr_auth:
  131. logger.debug("Found credentials in keyring for %s", netloc)
  132. return kr_auth
  133. return username, password
  134. def _get_url_and_credentials(self, original_url):
  135. # type: (str) -> Tuple[str, Optional[str], Optional[str]]
  136. """Return the credentials to use for the provided URL.
  137. If allowed, netrc and keyring may be used to obtain the
  138. correct credentials.
  139. Returns (url_without_credentials, username, password). Note
  140. that even if the original URL contains credentials, this
  141. function may return a different username and password.
  142. """
  143. url, netloc, _ = split_auth_netloc_from_url(original_url)
  144. # Use any stored credentials that we have for this netloc
  145. username, password = self.passwords.get(netloc, (None, None))
  146. if username is None and password is None:
  147. # No stored credentials. Acquire new credentials without prompting
  148. # the user. (e.g. from netrc, keyring, or the URL itself)
  149. username, password = self._get_new_credentials(original_url)
  150. if username is not None or password is not None:
  151. # Convert the username and password if they're None, so that
  152. # this netloc will show up as "cached" in the conditional above.
  153. # Further, HTTPBasicAuth doesn't accept None, so it makes sense to
  154. # cache the value that is going to be used.
  155. username = username or ""
  156. password = password or ""
  157. # Store any acquired credentials.
  158. self.passwords[netloc] = (username, password)
  159. assert (
  160. # Credentials were found
  161. (username is not None and password is not None) or
  162. # Credentials were not found
  163. (username is None and password is None)
  164. ), "Could not load credentials from url: {}".format(original_url)
  165. return url, username, password
  166. def __call__(self, req):
  167. # type: (Request) -> Request
  168. # Get credentials for this request
  169. url, username, password = self._get_url_and_credentials(req.url)
  170. # Set the url of the request to the url without any credentials
  171. req.url = url
  172. if username is not None and password is not None:
  173. # Send the basic auth with this request
  174. req = HTTPBasicAuth(username, password)(req)
  175. # Attach a hook to handle 401 responses
  176. req.register_hook("response", self.handle_401)
  177. return req
  178. # Factored out to allow for easy patching in tests
  179. def _prompt_for_password(self, netloc):
  180. # type: (str) -> Tuple[Optional[str], Optional[str], bool]
  181. username = ask_input("User for {}: ".format(netloc))
  182. if not username:
  183. return None, None, False
  184. auth = get_keyring_auth(netloc, username)
  185. if auth and auth[0] is not None and auth[1] is not None:
  186. return auth[0], auth[1], False
  187. password = ask_password("Password: ")
  188. return username, password, True
  189. # Factored out to allow for easy patching in tests
  190. def _should_save_password_to_keyring(self):
  191. # type: () -> bool
  192. if not keyring:
  193. return False
  194. return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y"
  195. def handle_401(self, resp, **kwargs):
  196. # type: (Response, **Any) -> Response
  197. # We only care about 401 responses, anything else we want to just
  198. # pass through the actual response
  199. if resp.status_code != 401:
  200. return resp
  201. # We are not able to prompt the user so simply return the response
  202. if not self.prompting:
  203. return resp
  204. parsed = urllib_parse.urlparse(resp.url)
  205. # Prompt the user for a new username and password
  206. username, password, save = self._prompt_for_password(parsed.netloc)
  207. # Store the new username and password to use for future requests
  208. self._credentials_to_save = None
  209. if username is not None and password is not None:
  210. self.passwords[parsed.netloc] = (username, password)
  211. # Prompt to save the password to keyring
  212. if save and self._should_save_password_to_keyring():
  213. self._credentials_to_save = (parsed.netloc, username, password)
  214. # Consume content and release the original connection to allow our new
  215. # request to reuse the same one.
  216. resp.content
  217. resp.raw.release_conn()
  218. # Add our new username and password to the request
  219. req = HTTPBasicAuth(username or "", password or "")(resp.request)
  220. req.register_hook("response", self.warn_on_401)
  221. # On successful request, save the credentials that were used to
  222. # keyring. (Note that if the user responded "no" above, this member
  223. # is not set and nothing will be saved.)
  224. if self._credentials_to_save:
  225. req.register_hook("response", self.save_credentials)
  226. # Send our new request
  227. new_resp = resp.connection.send(req, **kwargs)
  228. new_resp.history.append(resp)
  229. return new_resp
  230. def warn_on_401(self, resp, **kwargs):
  231. # type: (Response, **Any) -> None
  232. """Response callback to warn about incorrect credentials."""
  233. if resp.status_code == 401:
  234. logger.warning(
  235. '401 Error, Credentials not correct for %s', resp.request.url,
  236. )
  237. def save_credentials(self, resp, **kwargs):
  238. # type: (Response, **Any) -> None
  239. """Response callback to save credentials on success."""
  240. assert keyring is not None, "should never reach here without keyring"
  241. if not keyring:
  242. return
  243. creds = self._credentials_to_save
  244. self._credentials_to_save = None
  245. if creds and resp.status_code < 400:
  246. try:
  247. logger.info('Saving credentials to keyring')
  248. keyring.set_password(*creds)
  249. except Exception:
  250. logger.exception('Failed to save credentials')