-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Implement RFC7617-compliant multi-domain basic authentication #10904
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,54 @@ token as the "username" and do not provide a password: | |
https://[email protected]/simple | ||
``` | ||
|
||
When you specify several indexes, each of them can come with its own | ||
authentication information. When the domains and schemes of multiple | ||
indexes partially overlap, you can specify different authentication for each of them | ||
For example you can have: | ||
|
||
``` | ||
PIP_INDEX_URL=https://build:[email protected]/feed1 | ||
PIP_EXTRA_INDEX_URL=https://build:[email protected]/feed2 | ||
``` | ||
|
||
If you specify multiple identical index URLs with different authentication information, | ||
authentication from the first index will be used. | ||
|
||
```{versionchanged} 22.1 | ||
The basic authentication is now compliant with RFC 7617 | ||
``` | ||
|
||
In compliance with [RFC7617](https://datatracker.ietf.org/doc/html/rfc7617#section-2.2) if the indexes | ||
overlap, the authentication information from the prefix-match will be reused for the longer index if | ||
the longer index does not contain the authentication information. In case multiple indexes are | ||
prefix-matching, the authentication of the first of the longest matching prefixes is used. | ||
|
||
For example in this case, build:password authentication will be used when authenticating with the extra | ||
index URL. | ||
|
||
``` | ||
PIP_INDEX_URL=https://build:[email protected]/ | ||
PIP_EXTRA_INDEX_URL=https://pkgs.dev.azure.com/feed1 | ||
``` | ||
|
||
```{note} | ||
Prior to version 22.1 reusing of basic authentication between URLs was not RFC7617 compliant. | ||
This could lead to the situation that custom-built indexes could receive the authentication | ||
provided for the index path, to download files outside fof the security domain of the path. | ||
|
||
For example if your index at https://username:[email protected]/simple served files from | ||
https://pypi.company.com/file.tar.gz - the username and password provided for the "/simple" path | ||
was also used to authenticate download of the `file.tar.gz`. This is not RFC7617 compliant and as of | ||
version 22.1 it will not work automatically. If you encounter a problem where your files are being | ||
served from different security domain than your index and authentication is not used for them, you | ||
should (ideally) fix it on your server side or (as temporary workaround) | ||
specify your file download location as extra index url: | ||
|
||
PIP_EXTRA_INDEX_URL=https://username:[email protected]/ | ||
|
||
``` | ||
|
||
|
||
### Percent-encoding special characters | ||
|
||
```{versionadded} 10.0 | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
When you specify multiple indexes with common domains and schemes, basic authentication for each of | ||
the indexes can be different - compliant with RFC 7617. Previous versions of pip reused basic | ||
authentication credentials for all urls within the same domain. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,12 @@ | ||
import functools | ||
from typing import Any, List, Optional, Tuple | ||
from unittest.mock import Mock, PropertyMock | ||
|
||
import pytest | ||
|
||
import pip._internal.network.auth | ||
from pip._internal.network.auth import MultiDomainBasicAuth | ||
from src.pip._vendor.requests import PreparedRequest | ||
from tests.lib.requests_mocks import MockConnection, MockRequest, MockResponse | ||
|
||
|
||
|
@@ -50,40 +52,143 @@ def test_get_credentials_parses_correctly( | |
(username is None and password is None) | ||
or | ||
# Credentials were found and "cached" appropriately | ||
auth.passwords["example.com"] == (username, password) | ||
(url, (username, password)) in auth.passwords | ||
) | ||
|
||
|
||
def test_handle_prompt_for_password_successful() -> None: | ||
auth = MultiDomainBasicAuth() | ||
resp = Mock() | ||
resp.status_code = 401 | ||
resp.url = "http://example.com" | ||
resp.raw = Mock() | ||
resp.content = PropertyMock() | ||
resp.content = "" | ||
resp.request = PreparedRequest() | ||
resp.request.headers = {} | ||
auth._prompt_for_password = Mock() # type: ignore[assignment] | ||
auth._prompt_for_password.return_value = ("user", "password", True) | ||
auth.handle_401(resp) | ||
auth._prompt_for_password.assert_called_with("example.com") | ||
expected = ("http://example.com", ("user", "password")) | ||
assert len(auth.passwords) == 1 | ||
assert auth.passwords[0] == expected | ||
|
||
|
||
def test_handle_prompt_for_password_unsuccessful() -> None: | ||
auth = MultiDomainBasicAuth() | ||
resp = Mock() | ||
resp.status_code = 401 | ||
resp.url = "http://example.com" | ||
resp.raw = Mock() | ||
resp.content = PropertyMock() | ||
resp.content = "" | ||
resp.request = PreparedRequest() | ||
resp.request.headers = {} | ||
auth._prompt_for_password = Mock() # type: ignore[assignment] | ||
auth._prompt_for_password.return_value = (None, None, False) | ||
auth.handle_401(resp) | ||
auth._prompt_for_password.assert_called_with("example.com") | ||
assert len(auth.passwords) == 0 | ||
|
||
|
||
def test_get_credentials_not_to_uses_cached_credentials() -> None: | ||
auth = MultiDomainBasicAuth() | ||
auth.passwords["example.com"] = ("user", "pass") | ||
auth.passwords.append(("http://example.com", ("user", "pass"))) | ||
|
||
got = auth._get_url_and_credentials("http://foo:[email protected]/path") | ||
expected = ("http://example.com/path", "foo", "bar") | ||
assert got == expected | ||
|
||
|
||
def test_get_credentials_not_to_uses_cached_credentials_only_username() -> None: | ||
def test_get_credentials_not_to_use_cached_credentials_only_username() -> None: | ||
auth = MultiDomainBasicAuth() | ||
auth.passwords.append(("https://example.com", ("user", "pass"))) | ||
|
||
got = auth._get_url_and_credentials("https://[email protected]/path") | ||
expected = ("https://example.com/path", "foo", "") | ||
assert got == expected | ||
|
||
|
||
def test_multi_domain_credentials_match() -> None: | ||
auth = MultiDomainBasicAuth() | ||
auth.passwords.append(("http://example.com", ("user", "pass"))) | ||
auth.passwords.append(("http://example.com/path", ("user", "pass2"))) | ||
|
||
got = auth._get_url_and_credentials("http://[email protected]/path") | ||
expected = ("http://example.com/path", "user", "pass2") | ||
assert got == expected | ||
|
||
|
||
def test_multi_domain_credentials_longest_match() -> None: | ||
auth = MultiDomainBasicAuth() | ||
auth.passwords.append(("http://example.com", ("user", "pass"))) | ||
auth.passwords.append(("http://example.com/path", ("user", "pass2"))) | ||
auth.passwords.append(("http://example.com/path/subpath", ("user", "pass3"))) | ||
|
||
got = auth._get_url_and_credentials("http://[email protected]/path") | ||
expected = ("http://example.com/path", "user", "pass2") | ||
assert got == expected | ||
|
||
|
||
def test_multi_domain_credentials_partial_match_only() -> None: | ||
auth = MultiDomainBasicAuth() | ||
auth.passwords["example.com"] = ("user", "pass") | ||
auth.passwords.append(("http://example.com/path1", ("user", "pass"))) | ||
|
||
got = auth._get_url_and_credentials("http://foo@example.com/path") | ||
expected = ("http://example.com/path", "foo", "") | ||
got = auth._get_url_and_credentials("http://example.com/path2") | ||
expected = ("http://example.com/path2", None, None) | ||
assert got == expected | ||
|
||
|
||
def test_get_credentials_uses_cached_credentials() -> None: | ||
auth = MultiDomainBasicAuth() | ||
auth.passwords["example.com"] = ("user", "pass") | ||
auth.passwords.append(("https://example.com", ("user", "pass"))) | ||
|
||
got = auth._get_url_and_credentials("https://example.com/path") | ||
expected = ("https://example.com/path", "user", "pass") | ||
assert got == expected | ||
|
||
|
||
def test_get_credentials_not_uses_cached_credentials_different_scheme_http() -> None: | ||
auth = MultiDomainBasicAuth() | ||
auth.passwords.append(("http://example.com", ("user", "pass"))) | ||
|
||
got = auth._get_url_and_credentials("https://example.com/path") | ||
expected = ("https://example.com/path", None, None) | ||
assert got == expected | ||
|
||
|
||
def test_get_credentials_not_uses_cached_credentials_different_scheme_https() -> None: | ||
auth = MultiDomainBasicAuth() | ||
auth.passwords.append(("https://example.com", ("user", "pass"))) | ||
|
||
got = auth._get_url_and_credentials("http://example.com/path") | ||
expected = ("http://example.com/path", "user", "pass") | ||
expected = ("http://example.com/path", None, None) | ||
assert got == expected | ||
|
||
|
||
def test_get_credentials_uses_cached_credentials_only_username() -> None: | ||
auth = MultiDomainBasicAuth() | ||
auth.passwords["example.com"] = ("user", "pass") | ||
auth.passwords.append(("http://example.com", ("user", "pass"))) | ||
|
||
got = auth._get_url_and_credentials("http://[email protected]/path") | ||
expected = ("http://example.com/path", "user", "pass") | ||
assert got == expected | ||
|
||
|
||
def test_get_credentials_uses_cached_credentials_wrong_username() -> None: | ||
auth = MultiDomainBasicAuth() | ||
auth.passwords.append(("http://example.com", ("user", "pass"))) | ||
|
||
got = auth._get_url_and_credentials("http://[email protected]/path") | ||
expected = ("http://example.com/path", "user2", "") | ||
assert got == expected | ||
|
||
|
||
def test_get_credentials_added_multiple_times() -> None: | ||
auth = MultiDomainBasicAuth() | ||
auth.passwords.append(("http://example.com", ("user", "pass"))) | ||
auth.passwords.append(("http://example.com", ("user", "pass2"))) | ||
|
||
got = auth._get_url_and_credentials("http://[email protected]/path") | ||
expected = ("http://example.com/path", "user", "pass") | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.