Skip to content

Commit e47c737

Browse files
committed
Added pip style keyring password lookup as a fallback.
This way Poetry gives keyring backends like Microsoft's `artifacts-keyring` or Google's `keyrings.google-artifactregistry-auth` a change to retrieve the credentials. Since Microsoft Azure DevOps Personal Access Tokens will expire after some time the `artifacts-keyring` implementation will generate a new one for you to counteract that.
1 parent 4ec09d4 commit e47c737

File tree

3 files changed

+111
-6
lines changed

3 files changed

+111
-6
lines changed

docs/docs/repositories.md

+14
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ If a system keyring is available and supported, the password is stored to and re
6363

6464
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).
6565

66+
!!!note
67+
68+
```
69+
Poetry will fallback to Pip style use of keyring so that backends like
70+
Microsoft's [artifacts-keyring](https://pypi.org/project/artifacts-keyring/). It will need to be properly
71+
installed into Poetry's virtualenv, preferrably by installing a plugin.
72+
73+
If you are letting Poetry manage your virtual environments you will want a virtualenv
74+
seeder installed in Poetry's virtualenv that installs the desired keyring backend
75+
during `poetry install`. To again use Azure DecOps as an example: [azure-devops-artifacts-helpers](https://pypi.org/project/azure-devops-artifacts-helpers/)
76+
provides such a seeder. This would of course best achieved by installing a Poetry plugin
77+
if it exists for you use case instead of doing it yourself.
78+
```
79+
6680
Alternatively, you can use environment variables to provide the credentials:
6781

6882
```bash

poetry/utils/password_manager.py

+58-6
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,48 @@ def get_password(self, name: str, username: str) -> Optional[str]:
4747
f"Unable to retrieve the password for {name} from the key ring"
4848
)
4949

50+
def get_credential(self, url: str, username: str) -> Optional[Dict[str, str]]:
51+
# Based on how Pip does things
52+
if not self.is_available():
53+
return
54+
55+
import urllib.parse
56+
57+
import keyring
58+
import keyring.errors
59+
60+
try:
61+
netloc = urllib.parse.urlparse(url).netloc
62+
63+
try:
64+
get_credential = keyring.get_credential
65+
except AttributeError:
66+
pass
67+
else:
68+
cred = get_credential(url, username)
69+
if cred is not None:
70+
return cred.username, cred.password
71+
72+
cred = get_credential(netloc, username)
73+
if cred is not None:
74+
return cred.username, cred.password
75+
76+
return None
77+
78+
if username:
79+
password = keyring.get_password(url, username)
80+
if password:
81+
return username, password
82+
83+
password = keyring.get_password(netloc, username)
84+
if password:
85+
return username, password
86+
87+
except (RuntimeError, keyring.errors.KeyringError):
88+
raise KeyRingError(
89+
f"Unable to retrieve the password for {url} from the key ring"
90+
)
91+
5092
def set_password(self, name: str, username: str, password: str) -> None:
5193
if not self.is_available():
5294
return
@@ -156,19 +198,29 @@ def delete_pypi_token(self, name: str) -> None:
156198
def get_http_auth(self, name: str) -> Optional[Dict[str, str]]:
157199
auth = self._config.get(f"http-basic.{name}")
158200
if not auth:
201+
# maybe environment variables are set
159202
username = self._config.get(f"http-basic.{name}.username")
160203
password = self._config.get(f"http-basic.{name}.password")
161-
if not username and not password:
162-
return None
163204
else:
164205
username, password = auth["username"], auth.get("password")
165206
if password is None:
166207
password = self.keyring.get_password(name, username)
167208

168-
return {
169-
"username": username,
170-
"password": password,
171-
}
209+
if not password:
210+
# nothing found the poetry way
211+
# let's give pip style keyring backends a go
212+
# like `artifacts-keyring` for Azure DevOps
213+
# or `keyrings.google-artifactregistry-auth`
214+
url = self._config.get(f"repositories.{name}.url")
215+
cred = self.keyring.get_credential(url, username)
216+
if cred:
217+
username, password = cred
218+
219+
if username and password:
220+
return {
221+
"username": username,
222+
"password": password,
223+
}
172224

173225
def set_http_password(self, name: str, username: str, password: str) -> None:
174226
auth = {"username": username}

tests/utils/test_password_manager.py

+39
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
from poetry.utils.password_manager import PasswordManager
1010

1111

12+
class SimpleCredential:
13+
def __init__(self, username, password):
14+
self.username = username
15+
self.password = password
16+
17+
1218
class DummyBackend(KeyringBackend):
1319
def __init__(self):
1420
self._passwords = {}
@@ -23,6 +29,9 @@ def set_password(self, service, username, password):
2329
def get_password(self, service, username):
2430
return self._passwords.get(service, {}).get(username)
2531

32+
def get_credential(self, service, username):
33+
return self._passwords.get(service, {}).get(username)
34+
2635
def delete_password(self, service, username):
2736
if service in self._passwords and username in self._passwords[service]:
2837
del self._passwords[service][username]
@@ -86,6 +95,36 @@ def test_get_http_auth(config, mock_available_backend, backend):
8695
assert "baz" == auth["password"]
8796

8897

98+
def test_get_http_auth_pip_fallback_url(config, mock_available_backend, backend):
99+
backend.set_password(
100+
"https://poetry.test/pypi/simple", None, SimpleCredential("bar", "baz")
101+
)
102+
config.auth_config_source.add_property(
103+
"repositories.foo.url", "https://poetry.test/pypi/simple"
104+
)
105+
manager = PasswordManager(config)
106+
107+
assert manager.keyring.is_available()
108+
auth = manager.get_http_auth("foo")
109+
110+
assert "bar" == auth["username"]
111+
assert "baz" == auth["password"]
112+
113+
114+
def test_get_http_auth_pip_fallback_netloc(config, mock_available_backend, backend):
115+
backend.set_password("poetry.test", None, SimpleCredential("bar", "baz"))
116+
config.auth_config_source.add_property(
117+
"repositories.foo.url", "https://poetry.test/pypi/simple"
118+
)
119+
manager = PasswordManager(config)
120+
121+
assert manager.keyring.is_available()
122+
auth = manager.get_http_auth("foo")
123+
124+
assert "bar" == auth["username"]
125+
assert "baz" == auth["password"]
126+
127+
89128
def test_delete_http_password(config, mock_available_backend, backend):
90129
backend.set_password("poetry-repository-foo", "bar", "baz")
91130
config.auth_config_source.add_property("http-basic.foo", {"username": "bar"})

0 commit comments

Comments
 (0)