Commit 964d004b by Arun Babu Neelicattu Committed by Steph Samson

Use system keyring for passwords when available (#774)

This change introduces the use of system keyring to store repository
passwords when one is available. Passwords are added to and removed from
the keyring using the config command.

Resolves: #210
parent 83f3d1ff
......@@ -49,6 +49,10 @@ If you do not specify the password you will be prompted to write it.
You can also specify the username and password when using the `publish` command
with the `--username` and `--password` options.
If a system keyring is available and supported, the password is stored to and retrieved from the keyring. In the above example, the credential will be stored using the name `poetry-repository-pypi`. If access to keyring fails or is unsupported, this will fall back to writing the password to the `auth.toml` file along with the username.
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).
### Install dependencies from a private repository
Now that you can publish to your private repository, you need to be able to
......
......@@ -4,9 +4,12 @@ import re
from cleo import argument
from cleo import option
from poetry.utils.helpers import (
keyring_repository_password_del,
keyring_repository_password_set,
)
from .command import Command
TEMPLATE = """[settings]
[repositories]
......@@ -198,6 +201,7 @@ To remove a repository (repo is a short alias for repositories):
"There is no {} {} defined".format(m.group(2), m.group(1))
)
keyring_repository_password_del(self._auth_config, m.group(2))
self._auth_config.remove_property(
"{}.{}".format(m.group(1), m.group(2))
)
......@@ -218,9 +222,14 @@ To remove a repository (repo is a short alias for repositories):
username = values[0]
password = values[1]
property_value = dict(username=username)
try:
keyring_repository_password_set(m.group(2), username, password)
except RuntimeError:
property_value.update(password=password)
self._auth_config.add_property(
"{}.{}".format(m.group(1), m.group(2)),
{"username": username, "password": password},
"{}.{}".format(m.group(1), m.group(2)), property_value
)
return 0
......
......@@ -6,8 +6,11 @@ import tempfile
from contextlib import contextmanager
from typing import List
from typing import NoReturn
from typing import Optional
from typing import Union
from keyring import delete_password, set_password, get_password
from keyring.errors import KeyringError
from poetry.config import Config
from poetry.version import Version
......@@ -83,13 +86,50 @@ def parse_requires(requires): # type: (str) -> List[str]
return requires_dist
def keyring_service_name(repository_name): # type: (str) -> str
return "{}-{}".format("poetry-repository", repository_name)
def keyring_repository_password_get(
repository_name, username
): # type: (str, str) -> Optional[str]
try:
return get_password(keyring_service_name(repository_name), username)
except (RuntimeError, KeyringError):
return None
def keyring_repository_password_set(
repository_name, username, password
): # type: (str, str, str) -> NoReturn
try:
set_password(keyring_service_name(repository_name), username, password)
except (RuntimeError, KeyringError):
raise RuntimeError("Failed to store password in keyring")
def keyring_repository_password_del(
config, repository_name
): # type: (Config, str) -> NoReturn
try:
repo_auth = config.setting("http-basic.{}".format(repository_name))
if repo_auth and "username" in repo_auth:
delete_password(
keyring_service_name(repository_name), repo_auth["username"]
)
except (RuntimeError, KeyringError):
pass
def get_http_basic_auth(
config, repository_name
): # type: (Config, str) -> Optional[tuple]
repo_auth = config.setting("http-basic.{}".format(repository_name))
if repo_auth:
return repo_auth["username"], repo_auth.get("password")
username, password = repo_auth["username"], repo_auth.get("password")
if password is None:
password = keyring_repository_password_get(repository_name, username)
return username, password
return None
......
......@@ -47,6 +47,10 @@ glob2 = { version = "^0.6", python = "~2.7 || ~3.4" }
virtualenv = { version = "^16.0", python = "~2.7" }
# functools32 is needed for Python 2.7
functools32 = { version = "^3.2.3", python = "~2.7" }
keyring = [
{ version = "^18.0", python = "~2.7 || ~3.4" },
{ version = "^19.0", python = "^3.5" }
]
[tool.poetry.dev-dependencies]
pytest = "^4.1"
......
from uuid import uuid4
import pytest
from keyring import set_keyring, get_keyring
from keyring.backend import KeyringBackend
from keyring.errors import KeyringError
from poetry.utils.helpers import (
keyring_service_name,
keyring_repository_password_get,
keyring_repository_password_set,
keyring_repository_password_del,
)
class DictKeyring(KeyringBackend):
priority = 1
def __init__(self):
self._storage = dict()
def set_password(self, servicename, username, password):
if servicename not in self._storage:
self._storage[servicename] = dict()
self._storage[servicename][username] = password
def get_password(self, servicename, username):
if servicename in self._storage:
return self._storage[servicename].get(username)
def delete_password(self, servicename, username):
if servicename in self._storage:
if username in self._storage[servicename]:
del self._storage[servicename][username]
if not self._storage[servicename]:
del self._storage[servicename]
class BrokenKeyring(KeyringBackend):
priority = 1
def set_password(self, servicename, username, password):
raise KeyringError()
def get_password(self, servicename, username):
raise KeyringError()
def delete_password(self, servicename, username):
raise KeyringError()
@pytest.fixture
def keyring(): # type: () -> KeyringBackend
k = DictKeyring()
set_keyring(k)
return k
@pytest.fixture
def broken_keyring(): # type: () -> KeyringBackend
k = BrokenKeyring()
set_keyring(k)
return k
@pytest.fixture
def repository(): # type: () -> str
return "test"
@pytest.fixture
def username(): # type: () -> str
return "username"
@pytest.fixture
def password(): # type: () -> str
return str(uuid4())
def test_keyring_repository_password_get(keyring, repository, username, password):
keyring.set_password(keyring_service_name(repository), username, password)
assert keyring_repository_password_get(repository, username) == password
def test_keyring_repository_password_get_not_set(keyring, repository, username):
assert keyring.get_password(keyring_service_name(repository), username) is None
assert keyring_repository_password_get(repository, username) is None
def test_keyring_repository_password_get_broken(broken_keyring):
assert get_keyring() == broken_keyring
assert keyring_repository_password_get("repository", "username") is None
def test_keyring_repository_password_set(keyring, repository, username, password):
keyring_repository_password_set(repository, username, password)
assert keyring.get_password(keyring_service_name(repository), username) == password
def test_keyring_repository_password_set_broken(broken_keyring):
assert get_keyring() == broken_keyring
with pytest.raises(RuntimeError):
keyring_repository_password_set(repository, "username", "password")
def test_keyring_repository_password_del(
keyring, config, repository, username, password
):
keyring.set_password(keyring_service_name(repository), username, password)
config.add_property("http-basic.{}.username".format(repository), username)
keyring_repository_password_del(config, repository)
assert keyring.get_password(keyring_service_name(repository), username) is None
def test_keyring_repository_password_del_not_set(keyring, config, repository, username):
config.add_property("http-basic.{}.username".format(repository), username)
keyring_repository_password_del(config, repository)
assert keyring.get_password(keyring_service_name(repository), username) is None
def test_keyring_repository_password_del_broken(broken_keyring, config):
assert get_keyring() == broken_keyring
keyring_repository_password_del(config, "repository")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment