Skip to content
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

Issue #3483 Add cert retrieval for requests #3490

Closed
wants to merge 16 commits into from
Closed

Issue #3483 Add cert retrieval for requests #3490

wants to merge 16 commits into from

Conversation

dusktreader
Copy link
Contributor

The authenticator.py code already retrieves credentials from the config
for every request based on url matching. This change makes the
authenticator also retrieve certs from the config for each request based
on url matching.

Also includes unit tests.

Pull Request Check List

Resolves: #3483

  • Added tests for changed code.
  • Updated documentation for changed code.

@tbennett6421
Copy link

Forgive my ignorance. Would this work for poetry publish?

@dusktreader
Copy link
Contributor Author

@tbennett6421 Yes, that's specifically what this is for.

@GooseYArd
Copy link

I just noticed that when cert_verify is called by _download_archive, cert's type is Path(), so the test

if not isinstance(cert, basestring):

yields False, and we mistakenly treat the cert as a list.

Oddly, when cert_verify winds up being called by legacy_repository.find_links_for_package, the certs name is passed as a string.

I'll see if I can figure out a fix

@GooseYArd
Copy link

GooseYArd commented Feb 2, 2021

This seems to do the trick (apologies for the edits, I found an issue with the previous patch)

diff --git a/poetry/installation/authenticator.py b/poetry/installation/authenticator.py
index 8b57c9c..f671809 100644
--- a/poetry/installation/authenticator.py
+++ b/poetry/installation/authenticator.py
@@ -75,6 +75,12 @@ class Authenticator(object):
         verify = kwargs.get("verify", certs.get("verify"))
         cert = kwargs.get("cert", certs.get("cert"))

+        if isinstance(cert, pathlib.PurePath):
+            cert = str(cert)
+
+        if isinstance(verify, pathlib.PurePath):
+            verify = str(verify)
+
         settings = session.merge_environment_settings(
             prepared_request.url, proxies, stream, verify, cert
         )

the requests module will not accept Path instances for either cert or verify, but it will accept cert as a tuple of (cert, key), in cases where the cert and key are not combined into a single file. I'm assuming that poetry expects the combined format since pip has the same expectation. I'm guessing that if poetry ever opts to support separate cert/key files, it would entail a different key in auth.yaml, so I doubt we'll need to check here whether cert is a tuple or not.

@dusktreader
Copy link
Contributor Author

@GooseYArd it seems like you could just apply str() to any value of cert and verify, so maybe the if statements aren't necessary?

@GooseYArd
Copy link

@GooseYArd it seems like you could just apply str() to any value of cert and verify, so maybe the if statements aren't necessary?

hah funny you mention it- that was what I did in the initial patch without realizing that the values will be None if no cert/key or calist are specified, and request's cert verification was trying to locate the literal 'None' :)

@dusktreader
Copy link
Contributor Author

@GooseYArd good point there. i'll apply this patch

@BrutalSimplicity
Copy link

Any chance this will be merged anytime soon? I'd love to promote Poetry as the default package manager, but work with enterprises that have an internal PyPi repository, and am unable to pull those packages without hacky workarounds.

@astro-josh
Copy link

The issue resolved here is also blocking adoption for our project

@GooseYArd
Copy link

@abn this was the second of the two private repo + cert related issues that were preventing us from using Artifactory pypi servers. I'm hoping it can be reviewed for 1.2, but am not sure who to ping. We've been using this PR internally for the past few months without issue, fwiw.

@abn abn self-requested a review April 13, 2021 18:42
Copy link
Member

@abn abn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Over all looks okay; can we get this rebased and compatibility code removed please?

self._get_certs_for_netloc_from_config(netloc),
)

def _get_repository_netlocs(self): # type: () -> Generator[[str, str], None, None]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can drop compatibily type hints now.

@abn
Copy link
Member

abn commented Apr 13, 2021

@GooseYArd @dusktreader apologies this slipped through the radar. If you can get it rebased and updated, I can get it reviewed and merged.

@abn abn added this to the 1.2 milestone Apr 13, 2021
@abn abn added the kind/bug Something isn't working as expected label Apr 13, 2021
@GooseYArd
Copy link

@abn thanks a bunch, if @dusktreader doesn't beat me to it I'll make the updates you requested tomorrow AM

@dusktreader
Copy link
Contributor Author

@abn Rebased and force-pushed! thanks

@GooseYArd got it covered, I think

Copy link
Member

@abn abn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor suggestions

verify = kwargs.get("verify", certs.get("verify"))
cert = kwargs.get("cert", certs.get("cert"))

if isinstance(cert, pathlib.PurePath):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PurePath might be problematic. Since the only case we want to prevent here is str(None) I'd probably suggest we update the check to be an is none check.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this check to if cert: and things seem to work as expected for me on Ubuntu with only a client cert specified (verify should be None for me).

continue
def _get_certs_for_netloc_from_config(
self, netloc: str
) -> Dict[str, pathlib.PosixPath]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be pathlib.Path?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also made this change locally and also on line 167 for get_certs_for_url.

@surry
Copy link

surry commented Apr 27, 2021

@dusktreader thanks for this PR, also a blocker for us since we use a private repository with client certificates. I manually made these changes to my Poetry 1.1.6 install and my private repo is working now. Are you able to update and merge this soon? Would love to get this fix into a version I can update to with poetry self update.

@dusktreader
Copy link
Contributor Author

@abn Applied suggested changes and pushed commit. Thanks!

Copy link

@surry surry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor suggestions along the lines of @abn's ones. Also, I quickly looked at why the Python 3.10 tests failed and it might be due to this:

pytest-dev/pytest#8539

Not sure what the fix is here, if any...

@abn would it be possible to merge this in for the upcoming 1.2.0a1 release?

poetry/installation/authenticator.py Outdated Show resolved Hide resolved
poetry/installation/authenticator.py Outdated Show resolved Hide resolved
Copy link
Member

@abn abn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functionally looks okay. Some minor suggestions added.

poetry/installation/authenticator.py Outdated Show resolved Hide resolved
poetry/installation/authenticator.py Outdated Show resolved Hide resolved
poetry/installation/authenticator.py Outdated Show resolved Hide resolved
Comment on lines 207 to 251
def test_authenticator_uses_certs_from_config_if_not_provided(
config, mock_remote, http, mocker
):
config.merge(
{
"repositories": {"foo": {"url": "https://foo.bar/simple/"}},
"http-basic": {"foo": {"username": "bar", "password": "baz"}},
"certificates": {
"foo": {"cert": "/path/to/cert", "client-cert": "/path/to/client-cert"}
},
}
)

authenticator = Authenticator(config, NullIO())
session_send = mocker.patch.object(authenticator.session, "send")
authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz")
kwargs = session_send.call_args[1]
assert pathlib.Path(kwargs["verify"]) == pathlib.Path("/path/to/cert")
assert pathlib.Path(kwargs["cert"]) == pathlib.Path("/path/to/client-cert")


def test_authenticator_uses_provided_certs_instead_of_config_certs(
config, mock_remote, http, mocker
):
config.merge(
{
"repositories": {"foo": {"url": "https://foo.bar/simple/"}},
"http-basic": {"foo": {"username": "bar", "password": "baz"}},
"certificates": {
"foo": {"cert": "/path/to/cert", "client-cert": "/path/to/client-cert"}
},
}
)

authenticator = Authenticator(config, NullIO())
session_send = mocker.patch.object(authenticator.session, "send")
authenticator.request(
"get",
"https://foo.bar/files/foo-0.1.0.tar.gz",
verify="/path/to/provided/cert",
cert="/path/to/provided/client-cert",
)
kwargs = session_send.call_args[1]
assert pathlib.Path(kwargs["verify"]) == pathlib.Path("/path/to/provided/cert")
assert pathlib.Path(kwargs["cert"]) == pathlib.Path("/path/to/provided/client-cert")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_authenticator_uses_certs_from_config_if_not_provided(
config, mock_remote, http, mocker
):
config.merge(
{
"repositories": {"foo": {"url": "https://foo.bar/simple/"}},
"http-basic": {"foo": {"username": "bar", "password": "baz"}},
"certificates": {
"foo": {"cert": "/path/to/cert", "client-cert": "/path/to/client-cert"}
},
}
)
authenticator = Authenticator(config, NullIO())
session_send = mocker.patch.object(authenticator.session, "send")
authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz")
kwargs = session_send.call_args[1]
assert pathlib.Path(kwargs["verify"]) == pathlib.Path("/path/to/cert")
assert pathlib.Path(kwargs["cert"]) == pathlib.Path("/path/to/client-cert")
def test_authenticator_uses_provided_certs_instead_of_config_certs(
config, mock_remote, http, mocker
):
config.merge(
{
"repositories": {"foo": {"url": "https://foo.bar/simple/"}},
"http-basic": {"foo": {"username": "bar", "password": "baz"}},
"certificates": {
"foo": {"cert": "/path/to/cert", "client-cert": "/path/to/client-cert"}
},
}
)
authenticator = Authenticator(config, NullIO())
session_send = mocker.patch.object(authenticator.session, "send")
authenticator.request(
"get",
"https://foo.bar/files/foo-0.1.0.tar.gz",
verify="/path/to/provided/cert",
cert="/path/to/provided/client-cert",
)
kwargs = session_send.call_args[1]
assert pathlib.Path(kwargs["verify"]) == pathlib.Path("/path/to/provided/cert")
assert pathlib.Path(kwargs["cert"]) == pathlib.Path("/path/to/provided/client-cert")
@pytest.mark.parametrize("cert,client_cert", [
(None, None),
("/path/to/provided/cert", "/path/to/provided/client-cert")
])
def test_authenticator_uses_certs_from_config_if_not_provided(
cert, client_cert, config, mock_remote, http, mocker
):
configured_cert = "/path/to/cert"
configured_client_cert = "/path/to/client-cert"
config.merge(
{
"repositories": {"foo": {"url": "https://foo.bar/simple/"}},
"http-basic": {"foo": {"username": "bar", "password": "baz"}},
"certificates": {
"foo": {"cert": configured_cert, "client-cert": configured_client_cert}
},
}
)
authenticator = Authenticator(config, NullIO())
session_send = mocker.patch.object(authenticator.session, "send")
authenticator.request("get", "https://foo.bar/files/foo-0.1.0.tar.gz", verify=cert, cert=client_cert)
kwargs = session_send.call_args[1]
assert Path(kwargs["verify"]) == Path(cert or configured_cert)
assert Path(kwargs["cert"]) == Path(client_cert or configured_client_cert)

Nothing wroing with the implementation, just wanted to avoid some duplicated code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to have the tests separated since the test name clearly defines the conditions under test. In this suggestion, the parameterization makes the test cases less clear.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not wedded to parametrised tests. But I am much more keen on adding to the already large sets of duplicated code in our test suites. It is becoming harder to maintain. Additionally you can add a comment next to the parameters if further clarity is required. Personally, I do think the parameters are clear enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Will update

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been updated as requested

@@ -1,3 +1,4 @@
import pathlib
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import pathlib
from pathlib import Path

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a few reasons to prefer import pathlib to from pathlib import Path. The primary one is that it follows the import pattern used in this module already with requests, pytest, and other. Secondly, pathlib.Path is more clear and explicit the type of Path object being used since the name Path is common and can be overloaded. The last reason, though it isn't really relevant here, is that using the import path style makes it much easier to mock internal functions in pathlib if necessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say those reasons are rather subjective and not necessarily universal. Further, I do not think we use anything other than pathlib.Path as Path through the code base. I recommended the change as this is how Path is import through the rest of the code. As for mocking, I suspect we will have to patch anyway within poetry.* in most cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Will update

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since 1.2.0 looks imminent I thought I'd bump this thread- this is the last cert-related issue that is blocking my company from being able to use poetry generally for our internal packaging, and since I've already converted a number of our packages (and have been using a private fork with a couple of cert fixes) I'm desperate to get this change merged. Is there a compromise we could reach here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to get some cycles to work on this today. The latest requests for changes broke a bunch of unit tests, so I will have to figure out what happened there. Sorry it's been slow to merge.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updates pushed. Tests passed locally; will see what the pipeline does when it's greenlit to run

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The python3.6 tests on FreeBSD failed with a strange (or missing) error: https://github.com/python-poetry/poetry/pull/3490/checks?check_run_id=2657871763

Maybe that test just needs to be re-run and the other workflows could be approved by a maintainer see if there are other issues this PR needs to address? @abn

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that failure seems to be unrelated to my change. I will try to act on any more feedback on this quickly, since folks are waiting on this feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been updated as requested.

@@ -1,9 +1,12 @@
import logging
import pathlib
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import pathlib
from pathlib import Path

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been adjusted as requested

@dusktreader
Copy link
Contributor Author

@abn Can you trigger the pipelines, please? Also, it seems like the "Tests / FreeBSD / PYTHON:python3.6" is failing due to an unrelated issue. Also, I've addressed your comments above

@dusktreader
Copy link
Contributor Author

Can we get a maintainer to look at this please? Just waiting for pipelines

@surry
Copy link

surry commented Jun 8, 2021

I pinged some of the devs on Discord to get the pipelines going, so you might be able to get more traction there.

@abn would you be able to take another look now that the checks pass? This PR would really help users that rely on client certificates to access private Pypi repositories.

@astro-josh
Copy link

Any idea when this would be implemented? Project stuck at 1.0.10 until this is fixed for use of our private repo.

@jakeri
Copy link

jakeri commented Oct 4, 2021

Any updates on this one?

We wanted to use poetry for our internal build but we need the client cert functional. No we have to do a really convoluted build step with extracting requirements.txt, etc.

@dusktreader
Copy link
Contributor Author

Rebased to fix merge conflict.

@dusktreader
Copy link
Contributor Author

Rebased and force pushed again.

@abn or anyone else: does this have any traction?

@LarsStegman
Copy link

@abn @finswimmer @neersighted @sdispater @stephsamson Could one of the maintainers please look at this? I'm running into problems with multiple of our projects due to being stuck at an old version of Poetry.

@neersighted
Copy link
Member

@abn @finswimmer @neersighted @sdispater @stephsamson Could one of the maintainers please look at this? I'm running into problems with multiple of our projects due to being stuck at an old version of Poetry.

This needs another rebase from @dusktreader, but I am happy to land it once it's rebased onto current master and gets another review pass.

@dusktreader
Copy link
Contributor Author

@abn @finswimmer @neersighted @sdispater @stephsamson Could one of the maintainers please look at this? I'm running into problems with multiple of our projects due to being stuck at an old version of Poetry.

This needs another rebase from @dusktreader, but I am happy to land it once it's rebased onto current master and gets another review pass.

Yes, I noticed some of the underlying logic changed a bit. I'm going to work on getting it rebased eventually, but I've been busy lately. Also, it seemed like wasted effort rebasing over and over. Now that a maintainer is looking at it, I'll try to get it done.

@neersighted neersighted self-assigned this Jan 20, 2022
@BrutalSimplicity
Copy link

The completion of this PR is holding up full adoption for 100s of developers at my company. It's been in-progress for over a year. Any chance we can see this completed soon?

Is there some method to the prioritization of these requests? It seems like being able to use a certificate for requests made by Poetry would be a major blocker to widespread adoption for many professional developers.

@ITProKyle
Copy link
Contributor

Would it be possible to release this as a patch to 1.1 to get it out the door faster? It is a major blocker for many and is desperately needed ASAP.

@dusktreader
Copy link
Contributor Author

I simply don't have time to do this right now, and it's not in my current critical path. If someone else has time to pick up the mantle and rebase it, I (and apparently many others) would greatly appreciate it.

@chriswhite199
Copy link

For those blocked by this, can you not just set CURL_CA_BUNDLE env variable before running poetry to your CA bundle file?

This is what I'm doing for hosted Artifactory pypi mirror and private repos.

@ITProKyle
Copy link
Contributor

@chriswhite199 - yes and no.

Yes, that workaround works for me as someone that is familiar with poetry and its quirks.

No, that workaround will not work for the >100 developers I work with that have never used poetry.

@chriswhite199
Copy link

@ITProKyle - I hear you, and we just add a poetry wrapper script in the base of our repositories to do this - It handles detection of whether poetry is installed globally (and if not pip installs it into a .poetry-venv folder). Developers are taught to use it (in the same way they might use a maven or gradle wrapper if they were coding in Java).

@radoering
Copy link
Member

Superseded by: #5320

@radoering radoering closed this Apr 14, 2022
@papb
Copy link

papb commented Aug 1, 2022

For those blocked by this, can you not just set CURL_CA_BUNDLE env variable before running poetry to your CA bundle file?

This is what I'm doing for hosted Artifactory pypi mirror and private repos.

Oohhhhhhh thank you for teaching me this. Where did you find about this CURL_CA_BUNDLE env variable? Where is this documented? @chriswhite199

Copy link

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 29, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
kind/bug Something isn't working as expected
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Upgrading packages from private repositories fails using cert and client-cert