Skip to content

Commit b8c1120

Browse files
committed
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Implement RFC7617-compliant multi-domain basic authentication
1 parent 43d9404 commit b8c1120

File tree

3 files changed

+44
-22
lines changed

3 files changed

+44
-22
lines changed

src/pip/_internal/network/auth.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"""
66

77
import urllib.parse
8-
from typing import Any, List, Optional, Tuple
8+
from collections import defaultdict
9+
from typing import Any, Dict, List, Optional, Tuple
910

1011
from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth
1112
from pip._vendor.requests.models import Request, Response
@@ -16,6 +17,7 @@
1617
ask,
1718
ask_input,
1819
ask_password,
20+
get_scheme_netloc_no_auth,
1921
remove_auth_from_url,
2022
split_auth_netloc_from_url,
2123
)
@@ -76,14 +78,22 @@ def __init__(
7678
) -> None:
7779
self.prompting = prompting
7880
self.index_urls = index_urls
79-
self.passwords: List[Tuple[str, AuthInfo]] = []
81+
# we store the AuthInfo lists in dict indexed by the url,
82+
# which is both domain (netloc) and scheme.
83+
self.passwords: Dict[str, List[Tuple[str, AuthInfo]]] = defaultdict(list)
8084
# When the user is prompted to enter credentials and keyring is
8185
# available, we will offer to save them. If the user accepts,
8286
# this value is set to the credentials they entered. After the
8387
# request authenticates, the caller should call
8488
# ``save_credentials`` to save these.
8589
self._credentials_to_save: Optional[Credentials] = None
8690

91+
def _add_auth_info_to_dict(self, url: str, auth_info: AuthInfo) -> None:
92+
scheme_netloc_no_auth = get_scheme_netloc_no_auth(url)
93+
self.passwords[scheme_netloc_no_auth].append(
94+
(remove_auth_from_url(url), auth_info)
95+
)
96+
8797
def _get_index_url(self, url: str) -> Optional[str]:
8898
"""Return the original index URL matching the requested URL.
8999
@@ -191,7 +201,8 @@ def _get_matching_credentials(self, original_url: str) -> Optional[AuthInfo]:
191201
matched_auth_info = None
192202
no_auth_url = remove_auth_from_url(original_url)
193203
parsed_original = urllib.parse.urlparse(no_auth_url)
194-
for prefix, auth_info in self.passwords:
204+
scheme_netloc_no_auth = get_scheme_netloc_no_auth(original_url)
205+
for prefix, auth_info in self.passwords[scheme_netloc_no_auth]:
195206
parsed_stored = urllib.parse.urlparse(prefix)
196207
if (
197208
parsed_stored.netloc != parsed_original.netloc
@@ -244,7 +255,7 @@ def _get_url_and_credentials(
244255
password = password or ""
245256

246257
# Store any acquired credentials.
247-
self.passwords.append((remove_auth_from_url(url), (username, password)))
258+
self._add_auth_info_to_dict(url, (username, password))
248259

249260
assert (
250261
# Credentials were found
@@ -317,7 +328,7 @@ def handle_401(self, resp: Response, **kwargs: Any) -> Response:
317328
# Store the new username and password to use for future requests
318329
self._credentials_to_save = None
319330
if username is not None and password is not None:
320-
self.passwords.append((remove_auth_from_url(resp.url), (username, password)))
331+
self._add_auth_info_to_dict(resp.url, (username, password))
321332

322333
# Prompt to save the password to keyring
323334
if save and self._should_save_password_to_keyring():

src/pip/_internal/utils/misc.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,17 @@ def split_auth_netloc_from_url(url: str) -> Tuple[str, str, Tuple[str, str]]:
507507
return url_without_auth, netloc, auth
508508

509509

510+
def get_scheme_netloc_no_auth(url: str) -> str:
511+
"""
512+
Retrieve scheme + netloc with auth info removed
513+
514+
Returns: str(scheme + netloc without auth info)
515+
"""
516+
url_no_auth = remove_auth_from_url(url)
517+
purl = urllib.parse.urlsplit(url_no_auth)
518+
return f"{purl.scheme}://{purl.netloc}"
519+
520+
510521
def remove_auth_from_url(url: str) -> str:
511522
"""Return a copy of url with 'username:password@' removed."""
512523
# username/pass params are passed to subversion through flags

tests/unit/test_network_auth.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def test_get_credentials_parses_correctly(
5252
(username is None and password is None)
5353
or
5454
# Credentials were found and "cached" appropriately
55-
(url, (username, password)) in auth.passwords
55+
(url, (username, password)) in auth.passwords["http://example.com"]
5656
)
5757

5858

@@ -72,7 +72,7 @@ def test_handle_prompt_for_password_successful() -> None:
7272
auth._prompt_for_password.assert_called_with("example.com")
7373
expected = ("http://example.com", ("user", "password"))
7474
assert len(auth.passwords) == 1
75-
assert auth.passwords[0] == expected
75+
assert auth.passwords["http://example.com"][0] == expected
7676

7777

7878
def test_handle_prompt_for_password_unsuccessful() -> None:
@@ -94,7 +94,7 @@ def test_handle_prompt_for_password_unsuccessful() -> None:
9494

9595
def test_get_credentials_not_to_uses_cached_credentials() -> None:
9696
auth = MultiDomainBasicAuth()
97-
auth.passwords.append(("http://example.com", ("user", "pass")))
97+
auth._add_auth_info_to_dict("http://example.com", ("user", "pass"))
9898

9999
got = auth._get_url_and_credentials("http://foo:[email protected]/path")
100100
expected = ("http://example.com/path", "foo", "bar")
@@ -103,7 +103,7 @@ def test_get_credentials_not_to_uses_cached_credentials() -> None:
103103

104104
def test_get_credentials_not_to_use_cached_credentials_only_username() -> None:
105105
auth = MultiDomainBasicAuth()
106-
auth.passwords.append(("https://example.com", ("user", "pass")))
106+
auth._add_auth_info_to_dict("https://example.com", ("user", "pass"))
107107

108108
got = auth._get_url_and_credentials("https://[email protected]/path")
109109
expected = ("https://example.com/path", "foo", "")
@@ -112,8 +112,8 @@ def test_get_credentials_not_to_use_cached_credentials_only_username() -> None:
112112

113113
def test_multi_domain_credentials_match() -> None:
114114
auth = MultiDomainBasicAuth()
115-
auth.passwords.append(("http://example.com", ("user", "pass")))
116-
auth.passwords.append(("http://example.com/path", ("user", "pass2")))
115+
auth._add_auth_info_to_dict("http://example.com", ("user", "pass"))
116+
auth._add_auth_info_to_dict("http://example.com/path", ("user", "pass2"))
117117

118118
got = auth._get_url_and_credentials("http://[email protected]/path")
119119
expected = ("http://example.com/path", "user", "pass2")
@@ -122,9 +122,9 @@ def test_multi_domain_credentials_match() -> None:
122122

123123
def test_multi_domain_credentials_longest_match() -> None:
124124
auth = MultiDomainBasicAuth()
125-
auth.passwords.append(("http://example.com", ("user", "pass")))
126-
auth.passwords.append(("http://example.com/path", ("user", "pass2")))
127-
auth.passwords.append(("http://example.com/path/subpath", ("user", "pass3")))
125+
auth._add_auth_info_to_dict("http://example.com", ("user", "pass"))
126+
auth._add_auth_info_to_dict("http://example.com/path", ("user", "pass2"))
127+
auth._add_auth_info_to_dict("http://example.com/path/subpath", ("user", "pass3"))
128128

129129
got = auth._get_url_and_credentials("http://[email protected]/path")
130130
expected = ("http://example.com/path", "user", "pass2")
@@ -133,7 +133,7 @@ def test_multi_domain_credentials_longest_match() -> None:
133133

134134
def test_multi_domain_credentials_partial_match_only() -> None:
135135
auth = MultiDomainBasicAuth()
136-
auth.passwords.append(("http://example.com/path1", ("user", "pass")))
136+
auth._add_auth_info_to_dict("http://example.com/path1", ("user", "pass"))
137137

138138
got = auth._get_url_and_credentials("http://example.com/path2")
139139
expected = ("http://example.com/path2", None, None)
@@ -142,7 +142,7 @@ def test_multi_domain_credentials_partial_match_only() -> None:
142142

143143
def test_get_credentials_uses_cached_credentials() -> None:
144144
auth = MultiDomainBasicAuth()
145-
auth.passwords.append(("https://example.com", ("user", "pass")))
145+
auth._add_auth_info_to_dict("https://example.com", ("user", "pass"))
146146

147147
got = auth._get_url_and_credentials("https://example.com/path")
148148
expected = ("https://example.com/path", "user", "pass")
@@ -151,7 +151,7 @@ def test_get_credentials_uses_cached_credentials() -> None:
151151

152152
def test_get_credentials_not_uses_cached_credentials_different_scheme_http() -> None:
153153
auth = MultiDomainBasicAuth()
154-
auth.passwords.append(("http://example.com", ("user", "pass")))
154+
auth._add_auth_info_to_dict("http://example.com", ("user", "pass"))
155155

156156
got = auth._get_url_and_credentials("https://example.com/path")
157157
expected = ("https://example.com/path", None, None)
@@ -160,7 +160,7 @@ def test_get_credentials_not_uses_cached_credentials_different_scheme_http() ->
160160

161161
def test_get_credentials_not_uses_cached_credentials_different_scheme_https() -> None:
162162
auth = MultiDomainBasicAuth()
163-
auth.passwords.append(("https://example.com", ("user", "pass")))
163+
auth._add_auth_info_to_dict("https://example.com", ("user", "pass"))
164164

165165
got = auth._get_url_and_credentials("http://example.com/path")
166166
expected = ("http://example.com/path", None, None)
@@ -169,7 +169,7 @@ def test_get_credentials_not_uses_cached_credentials_different_scheme_https() ->
169169

170170
def test_get_credentials_uses_cached_credentials_only_username() -> None:
171171
auth = MultiDomainBasicAuth()
172-
auth.passwords.append(("http://example.com", ("user", "pass")))
172+
auth._add_auth_info_to_dict("http://example.com", ("user", "pass"))
173173

174174
got = auth._get_url_and_credentials("http://[email protected]/path")
175175
expected = ("http://example.com/path", "user", "pass")
@@ -178,7 +178,7 @@ def test_get_credentials_uses_cached_credentials_only_username() -> None:
178178

179179
def test_get_credentials_uses_cached_credentials_wrong_username() -> None:
180180
auth = MultiDomainBasicAuth()
181-
auth.passwords.append(("http://example.com", ("user", "pass")))
181+
auth._add_auth_info_to_dict("http://example.com", ("user", "pass"))
182182

183183
got = auth._get_url_and_credentials("http://[email protected]/path")
184184
expected = ("http://example.com/path", "user2", "")
@@ -187,8 +187,8 @@ def test_get_credentials_uses_cached_credentials_wrong_username() -> None:
187187

188188
def test_get_credentials_added_multiple_times() -> None:
189189
auth = MultiDomainBasicAuth()
190-
auth.passwords.append(("http://example.com", ("user", "pass")))
191-
auth.passwords.append(("http://example.com", ("user", "pass2")))
190+
auth._add_auth_info_to_dict("http://example.com", ("user", "pass"))
191+
auth._add_auth_info_to_dict("http://example.com", ("user", "pass2"))
192192

193193
got = auth._get_url_and_credentials("http://[email protected]/path")
194194
expected = ("http://example.com/path", "user", "pass")

0 commit comments

Comments
 (0)