From baa8ed28b0df1f0669a4edb60aa6461f26101d77 Mon Sep 17 00:00:00 2001 From: Popkornium18 Date: Fri, 26 Jan 2024 17:18:08 +0100 Subject: [PATCH] add config option keyring.enabled to allow disabling the keyring --- docs/configuration.md | 12 +++++++++++ docs/faq.md | 9 +++++++++ docs/repositories.md | 6 ++++++ src/poetry/config/config.py | 4 ++++ src/poetry/console/commands/config.py | 1 + src/poetry/utils/password_manager.py | 2 +- tests/config/test_config.py | 20 ++++++++++++++++++ tests/console/commands/test_config.py | 6 ++++++ tests/utils/test_password_manager.py | 29 +++++++++++++++++++++++++++ 9 files changed, 88 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 67a0301c3cd..6e2a7fe1923 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -506,3 +506,15 @@ repository. Set client certificate for repository ``. See [Repositories - Configuring credentials - Custom certificate authority]({{< relref "repositories#custom-certificate-authority-and-mutual-tls-authentication" >}}) for more information. + +### `keyring.enabled`: + +**Type**: `boolean` + +**Default**: `true` + +**Environment Variable**: `POETRY_KEYRING_ENABLED` + +Enable the system keyring for storing credentials. +See [Repositories - Configuring credentials]({{< relref "repositories#configuring-credentials" >}}) +for more information. diff --git a/docs/faq.md b/docs/faq.md index 549bc609978..45e7fa2bd30 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -154,6 +154,15 @@ required variables explicitly or `passenv = "*"` to forward all of them. Linux systems may require forwarding the `DBUS_SESSION_BUS_ADDRESS` variable to allow access to the system keyring, though this may vary between desktop environments. +Alternatively, you can disable the keyring completely: + +```bash +poetry config keyring.enabled false +``` + +Be aware that this will cause Poetry to write passwords to plaintext config files. +You will need to set the credentials again after changing this setting. + ### Is Nox supported? Use the [`nox-poetry`](https://github.com/cjolowicz/nox-poetry) package to install locked versions of diff --git a/docs/repositories.md b/docs/repositories.md index 4e57bff8d10..710cf7e7525 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -500,6 +500,12 @@ If a system keyring is available and supported, the password is stored to and re Keyring support is enabled using the [keyring library](https://pypi.org/project/keyring/). For more information on supported backends refer to the [library documentation](https://keyring.readthedocs.io/en/latest/?badge=latest). +If you do not want to use the keyring, you can tell Poetry to disable it and store the credentials in plaintext config files: + +```bash +poetry config keyring.enabled false +``` + {{% note %}} Poetry will fallback to Pip style use of keyring so that backends like diff --git a/src/poetry/config/config.py b/src/poetry/config/config.py index 5bafb90218b..42d6d088bc2 100644 --- a/src/poetry/config/config.py +++ b/src/poetry/config/config.py @@ -137,6 +137,9 @@ class Config: "warnings": { "export": True, }, + "keyring": { + "enabled": True, + }, } def __init__( @@ -301,6 +304,7 @@ def _get_normalizer(name: str) -> Callable[[str], Any]: "installer.parallel", "solver.lazy-wheel", "warnings.export", + "keyring.enabled", }: return boolean_normalizer diff --git a/src/poetry/console/commands/config.py b/src/poetry/console/commands/config.py index 13e8b2262a0..3d0162e12c0 100644 --- a/src/poetry/console/commands/config.py +++ b/src/poetry/console/commands/config.py @@ -79,6 +79,7 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any]]: ), "solver.lazy-wheel": (boolean_validator, boolean_normalizer), "warnings.export": (boolean_validator, boolean_normalizer), + "keyring.enabled": (boolean_validator, boolean_normalizer), } return unique_config_values diff --git a/src/poetry/utils/password_manager.py b/src/poetry/utils/password_manager.py index 4ca2ac8caea..f4ab1232694 100644 --- a/src/poetry/utils/password_manager.py +++ b/src/poetry/utils/password_manager.py @@ -145,7 +145,7 @@ def __init__(self, config: Config) -> None: @functools.cached_property def use_keyring(self) -> bool: - return PoetryKeyring.is_available() + return self._config.get("keyring.enabled") and PoetryKeyring.is_available() @functools.cached_property def keyring(self) -> PoetryKeyring: diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 35c2a16753e..df95df6a9d6 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -12,6 +12,8 @@ from poetry.config.config import Config from poetry.config.config import boolean_normalizer from poetry.config.config import int_normalizer +from poetry.utils.password_manager import PasswordManager +from poetry.utils.password_manager import PoetryKeyringError from tests.helpers import flatten_dict @@ -19,6 +21,8 @@ from collections.abc import Callable from collections.abc import Iterator + from tests.conftest import DummyBackend + Normalizer = Callable[[str], Any] @@ -81,3 +85,19 @@ def test_config_expands_tilde_for_virtualenvs_path( ) -> None: config.merge({"virtualenvs": {"path": path_config}}) assert config.virtualenvs_path == expected + + +def test_disabled_keyring_is_unavailable( + config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend +) -> None: + manager = PasswordManager(config) + assert manager.use_keyring + + config.config["keyring"]["enabled"] = False + manager = PasswordManager(config) + assert not manager.use_keyring + + with pytest.raises(PoetryKeyringError) as e: + _ = manager.keyring + + assert str(e.value) == "Access to keyring was requested, but it is not available" diff --git a/tests/console/commands/test_config.py b/tests/console/commands/test_config.py index ee54963ac1d..d96077cc307 100644 --- a/tests/console/commands/test_config.py +++ b/tests/console/commands/test_config.py @@ -59,6 +59,7 @@ def test_list_displays_default_value_if_not_set( installer.modern-installation = true installer.no-binary = null installer.parallel = true +keyring.enabled = true solver.lazy-wheel = true virtualenvs.create = true virtualenvs.in-project = null @@ -90,6 +91,7 @@ def test_list_displays_set_get_setting( installer.modern-installation = true installer.no-binary = null installer.parallel = true +keyring.enabled = true solver.lazy-wheel = true virtualenvs.create = false virtualenvs.in-project = null @@ -142,6 +144,7 @@ def test_unset_setting( installer.modern-installation = true installer.no-binary = null installer.parallel = true +keyring.enabled = true solver.lazy-wheel = true virtualenvs.create = true virtualenvs.in-project = null @@ -172,6 +175,7 @@ def test_unset_repo_setting( installer.modern-installation = true installer.no-binary = null installer.parallel = true +keyring.enabled = true solver.lazy-wheel = true virtualenvs.create = true virtualenvs.in-project = null @@ -300,6 +304,7 @@ def test_list_displays_set_get_local_setting( installer.modern-installation = true installer.no-binary = null installer.parallel = true +keyring.enabled = true solver.lazy-wheel = true virtualenvs.create = false virtualenvs.in-project = null @@ -338,6 +343,7 @@ def test_list_must_not_display_sources_from_pyproject_toml( installer.modern-installation = true installer.no-binary = null installer.parallel = true +keyring.enabled = true repositories.foo.url = "https://foo.bar/simple/" solver.lazy-wheel = true virtualenvs.create = true diff --git a/tests/utils/test_password_manager.py b/tests/utils/test_password_manager.py index bf942da233e..f254ebe61ec 100644 --- a/tests/utils/test_password_manager.py +++ b/tests/utils/test_password_manager.py @@ -330,3 +330,32 @@ def test_get_pypi_token_with_env_var_not_available( result_token = manager.get_pypi_token(repo_name) assert result_token is None + + +def test_disabled_keyring_never_called( + config: Config, with_simple_keyring: None, dummy_keyring: DummyBackend +) -> None: + config.config["keyring"]["enabled"] = False + config.config["http-basic"] = {"onlyuser": {"username": "user"}} + + manager = PasswordManager(config) + num_public_functions = len([f for f in dir(manager) if not f.startswith("_")]) + if num_public_functions != 10: + pytest.fail( + f"A function was added to or removed from the {PasswordManager.__name__} " + "class without reflecting this change in this test." + ) + + manager.set_pypi_token(repo_name="exists", token="token") + manager.get_pypi_token(repo_name="exists") + manager.get_pypi_token(repo_name="doesn't exist") + manager.delete_pypi_token(repo_name="exists") + manager.delete_pypi_token(repo_name="doesn't exist") + manager.set_http_password(repo_name="exists", username="user", password="password") + manager.get_http_auth(repo_name="exists") + manager.get_http_auth(repo_name="doesn't exist") + manager.get_http_auth(repo_name="onlyuser") + manager.delete_http_password(repo_name="exits") + manager.delete_http_password(repo_name="doesn't exist") + manager.delete_http_password(repo_name="onlyuser") + manager.get_credential("a", "b", "c", username="user")