Skip to content

Commit

Permalink
Fixes jaraco#350 Add get_username_and_password API
Browse files Browse the repository at this point in the history
Adds a new API to enable backends to return a username along with the password.
The entry points include fallback behavior for backends that do not implement the API.
  • Loading branch information
zooba committed Oct 27, 2018
1 parent 869b2a3 commit 327f4cd
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 2 deletions.
4 changes: 2 additions & 2 deletions keyring/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from __future__ import absolute_import

from .core import (set_keyring, get_keyring, set_password, get_password,
delete_password)
delete_password, get_username_and_password)

__all__ = (
'set_keyring', 'get_keyring', 'set_password', 'get_password',
'delete_password',
'delete_password', 'get_username_and_password'
)
17 changes: 17 additions & 0 deletions keyring/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,23 @@ def delete_password(self, service, username):
"""
raise errors.PasswordDeleteError("reason")

# for backward-compatibility, don't require a backend to implement
# get_username_and_password
# @abc.abstractmethod
def get_username_and_password(self, service, username):
"""Gets the username and password for the service.
Returns (username, password)
The *username* argument is optional and may be omitted by
the caller or ignored by the backend. Callers must use the
returned username.
"""
# The default implementation requires a username here.
if username is not None:
password = self.get_password(service, username)
if password is not None:
return username, password
return None, None

class Crypter:
"""Base class providing encryption and decryption
Expand Down
14 changes: 14 additions & 0 deletions keyring/backends/Windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ def _delete_password(self, target):
TargetName=target,
)

def get_username_and_password(self, service, username):
res = None
# get the credentials associated with the provided username
if username:
res = self._get_password(self._compound_name(username, service))
# get any first password under the service name
if not res:
res = self._get_password(service)
if not res:
return None, None
username = res['UserName']
blob = res['CredentialBlob']
return username, blob.decode('utf-16')


class OldPywinError:
"""
Expand Down
18 changes: 18 additions & 0 deletions keyring/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ def delete_password(service_name, username):
_keyring_backend.delete_password(service_name, username)


def get_username_and_password(service_name, username):
"""Get username and password from the specified service.
"""
try:
call = _keyring_backend.get_username_and_password
except AttributeError:
pass
else:
return call(service_name, username)

# The fallback behavior requires a username.
if username is not None:
password = _keyring_backend.get_password(service_name, username)
if password is not None:
return username, password
return None, None


def recommended(backend):
return backend.priority >= 1

Expand Down
13 changes: 13 additions & 0 deletions keyring/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,16 @@ def test_different_user(self):
assert keyring.get_password('service1', 'user2') == 'password2'
self.set_password('service2', 'user3', 'password3')
assert keyring.get_password('service1', 'user1') == 'password1'

def test_username_and_password(self):
keyring = self.keyring
assert keyring.get_username_and_password('service1', None) == (None, None)
self.set_password('service1', 'user1', 'password1')
self.set_password('service1', 'user2', 'password2')
# Using get_username_and_password may produce any of these results
candidates = frozenset((
('user1', 'password1'),
('user2', 'password2'),
))
assert keyring.get_username_and_password('service1', None) in candidates
assert keyring.get_username_and_password('service1', 'user2') in candidates

0 comments on commit 327f4cd

Please sign in to comment.