From f32f39c133c1a7720936f4fc3faf94559f44fcee Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Sat, 27 Oct 2018 11:57:07 -0400 Subject: [PATCH 1/2] Fixes #350 Add get_credential() API 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. --- keyring/__init__.py | 4 ++-- keyring/backend.py | 23 ++++++++++++++++++++++- keyring/backends/Windows.py | 16 ++++++++++++++++ keyring/core.py | 6 ++++++ keyring/tests/test_backend.py | 24 ++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 3 deletions(-) diff --git a/keyring/__init__.py b/keyring/__init__.py index eadd788e..2694f6d9 100644 --- a/keyring/__init__.py +++ b/keyring/__init__.py @@ -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_credential) __all__ = ( 'set_keyring', 'get_keyring', 'set_password', 'get_password', - 'delete_password', + 'delete_password', 'get_credential', ) diff --git a/keyring/backend.py b/keyring/backend.py index 72099440..c3ed066a 100644 --- a/keyring/backend.py +++ b/keyring/backend.py @@ -10,7 +10,7 @@ import entrypoints -from . import errors, util +from . import credentials, errors, util from .util import properties from .py27compat import add_metaclass, filter @@ -108,6 +108,27 @@ def delete_password(self, service, username): """ raise errors.PasswordDeleteError("reason") + # for backward-compatibility, don't require a backend to implement + # get_credential + # @abc.abstractmethod + def get_credential(self, service, username): + """Gets the username and password for the service. + Returns a Credential instance. + + 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 credentials.SimpleCredential( + username, + password, + ) + return None + class Crypter: """Base class providing encryption and decryption diff --git a/keyring/backends/Windows.py b/keyring/backends/Windows.py index 3d7c862d..ac5fc28b 100644 --- a/keyring/backends/Windows.py +++ b/keyring/backends/Windows.py @@ -5,6 +5,7 @@ from ..py27compat import text_type from ..util import properties from ..backend import KeyringBackend +from ..credentials import SimpleCredential from ..errors import PasswordDeleteError, ExceptionRaisedContext try: @@ -127,6 +128,21 @@ def _delete_password(self, target): TargetName=target, ) + def get_credential(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 + return SimpleCredential( + res['UserName'], + res['CredentialBlob'].decode('utf-16'), + ) + class OldPywinError: """ diff --git a/keyring/core.py b/keyring/core.py index fd5ba84d..e6cd2fb5 100644 --- a/keyring/core.py +++ b/keyring/core.py @@ -70,6 +70,12 @@ def delete_password(service_name, username): _keyring_backend.delete_password(service_name, username) +def get_credential(service_name, username): + """Get a Credential for the specified service. + """ + return _keyring_backend.get_credential(service_name, username) + + def recommended(backend): return backend.priority >= 1 diff --git a/keyring/tests/test_backend.py b/keyring/tests/test_backend.py index 5eeec144..b4d095b2 100644 --- a/keyring/tests/test_backend.py +++ b/keyring/tests/test_backend.py @@ -134,3 +134,27 @@ 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_credential(self): + keyring = self.keyring + + cred = keyring.get_credential('service', None) + assert cred is None + + self.set_password('service1', 'user1', 'password1') + self.set_password('service1', 'user2', 'password2') + + cred = keyring.get_credential('service1', None) + assert cred is not None + assert (cred.username, cred.password) in ( + ('user1', 'password1'), + ('user2', 'password2'), + (None, None), + ) + + cred = keyring.get_credential('service1', 'user2') + assert cred is not None + assert (cred.username, cred.password) in ( + ('user1', 'password1'), + ('user2', 'password2'), + ) From ca2982b92a72bc1cacacd3c5b949feb5bf41faf5 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Sat, 27 Oct 2018 17:27:30 -0400 Subject: [PATCH 2/2] Fix handling of None case on macOS --- keyring/tests/test_backend.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/keyring/tests/test_backend.py b/keyring/tests/test_backend.py index b4d095b2..c90ea0d7 100644 --- a/keyring/tests/test_backend.py +++ b/keyring/tests/test_backend.py @@ -145,11 +145,9 @@ def test_credential(self): self.set_password('service1', 'user2', 'password2') cred = keyring.get_credential('service1', None) - assert cred is not None - assert (cred.username, cred.password) in ( + assert cred is None or (cred.username, cred.password) in ( ('user1', 'password1'), ('user2', 'password2'), - (None, None), ) cred = keyring.get_credential('service1', 'user2')