diff --git a/src/caosdb/configuration.py b/src/caosdb/configuration.py index 8b4af56eb45561526bf8f45d9e4708631901ce6e..9f6b558abd7bd5e291eaa8bd89ca41ebe0c73bf8 100644 --- a/src/caosdb/configuration.py +++ b/src/caosdb/configuration.py @@ -24,20 +24,20 @@ """Created on 20.09.2016.""" -from sys import hexversion -if hexversion < 0x03000000: - from ConfigParser import ConfigParser # @UnusedImport @UnresolvedImport -else: - from configparser import ConfigParser # @UnresolvedImport @Reimport +try: + # python2 + from ConfigParser import ConfigParser +except ImportError: + # python3 + from configparser import ConfigParser _DEFAULTS = {"Connection": {"url": None, - "timeout": "200", - "username": None, - "password": None, - "password_method": "plain", - "debug": "0", - "cacert": None}, + "timeout": "200", + "username": None, + "password_method": "plain", + "debug": "0", + "cacert": None}, "Container": {"debug": "0"}} diff --git a/src/caosdb/connection/authentication/external_credentials_provider.py b/src/caosdb/connection/authentication/external_credentials_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..bebe43fedf215dfc2b744fbc0c96d7e9d50ba3e5 --- /dev/null +++ b/src/caosdb/connection/authentication/external_credentials_provider.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2018 Research Group Biomedical Physics, +# Max-Planck-Institute for Dynamics and Self-Organization Göttingen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +# +"""external_credentials_provider. +""" +from __future__ import absolute_import, unicode_literals +from abc import ABCMeta +import logging +from .plain import PlainTextCredentialsProvider + +# meta class compatible with Python 2 *and* 3: +ABC = ABCMeta(str('ABC'), (object, ), {str('__slots__'): ()}) + + +class ExternalCredentialsProvider(PlainTextCredentialsProvider, ABC): + """ExternalCredentialsProvider. + + Abstract subclass of PlainTextCredentialsProvider which should be used to + implement external credentials provider (e.g. pass, keyring, or any other call + to an external program, which presents the plain text password, which is to be + used for the authentication. + + Parameters + ---------- + callback: Function + A function which has **kwargs argument. This funktion will be called + each time a password is needed with the current connection + configuration as parameters. + """ + + def __init__(self, callback): + super(ExternalCredentialsProvider, self).__init__() + self._callback = callback + self._config = None + + def configure(self, **config): + """configure. + + Parameters + ---------- + **config + Keyword arguments containing the necessary arguments for the + concrete implementation of this class. + + Attributes + ---------- + password : str + The password. This password is not stored in this class. A callback + is called to provide the password each time this property is + called. + + Returns + ------- + None + """ + if "password" in config: + if "password_method" in config: + authm = "`{}`".format(config["password_method"]) + else: + authm = "an external credentials provider" + self.logger.log(logging.WARNING, + ("`password` defined. You configured caosdb to " + "use %s as authentication method and yet " + "provided a password yourself. This indicates " + "a misconfiguration (e.g. in your " + "pycaosdb.ini) and should be avoided."), + authm) + self._config = dict(config) + super(ExternalCredentialsProvider, self).configure(**config) + + @property + def password(self): + return self._callback(**self._config) diff --git a/src/caosdb/connection/authentication/interface.py b/src/caosdb/connection/authentication/interface.py index 975c43a4b8070944ec4a33fb2993a27b3047f3cb..008933caa37e1c0e5fe65ac12721df32fd0f2480 100644 --- a/src/caosdb/connection/authentication/interface.py +++ b/src/caosdb/connection/authentication/interface.py @@ -25,10 +25,6 @@ caosdb server.""" from abc import ABCMeta, abstractmethod, abstractproperty import logging -try: - from urllib.parse import quote -except ImportError: - from urllib import quote from caosdb.connection.utils import urlencode from caosdb.connection.interface import CaosDBServerConnection from caosdb.connection.utils import parse_auth_token @@ -46,11 +42,17 @@ class AbstractAuthenticator(ABC): Interface for different authentication mechanisms. e.g. username/password authentication or SSH key authentication. + Attributes + ---------- + logger : Logger + A logger which should be used for all logging which has to do with + authentication. + Methods ------- - login - logout - configure + login (abstract) + logout (abstract) + configure (abstract) on_request on_response @@ -62,6 +64,7 @@ class AbstractAuthenticator(ABC): def __init__(self): self.auth_token = None + self.logger = _LOGGER @abstractmethod def login(self): @@ -225,10 +228,21 @@ class CredentialsProvider(ABC): Attributes ---------- - password - username + password (abstract) + username (abstract) + logger : Logger + A logger which should be used for all logging which has to do with the + provision of credentials. This is usually just the "authentication" + logger. + + Methods + ------- + configure (abstract) """ + def __init__(self): + self.logger = _LOGGER + @abstractmethod def configure(self, **config): """configure. diff --git a/src/caosdb/connection/authentication/keyring.py b/src/caosdb/connection/authentication/keyring.py index da9e7d41000009c1ba26c1faa080f84a5ab2e91f..abdbf112e20279d9c7211d01bebd296869edea5d 100644 --- a/src/caosdb/connection/authentication/keyring.py +++ b/src/caosdb/connection/authentication/keyring.py @@ -30,7 +30,8 @@ retrieve the password. import sys import imp from getpass import getpass -from .plain import PlainTextCredentialsProvider +from caosdb.exceptions import ConfigurationException +from .external_credentials_provider import ExternalCredentialsProvider from .interface import CredentialsAuthenticator @@ -46,7 +47,7 @@ def get_authentication_provider(): CredentialsAuthenticator with a 'KeyringCaller' as back-end. """ - return CredentialsAuthenticator(KeyringCaller()) + return CredentialsAuthenticator(KeyringCaller(callback=_call_keyring)) def _get_external_keyring(): @@ -64,7 +65,16 @@ def _get_external_keyring(): fil.close() -def _call_keyring(app, username): +def _call_keyring(**config): + if "username" not in config: + raise ConfigurationException("Your configuration did not provide a " + "`username` which is needed by the " + "`KeyringCaller` to retrieve the " + "password in question.") + url = config.get("url") + username = config.get("username") + app = "PyCaosDB — {}".format(url) + password = _call_keyring(app=app, username=username) external_keyring = _get_external_keyring() password = external_keyring.get_password(app, username) if password is None: @@ -76,7 +86,7 @@ def _call_keyring(app, username): return password -class KeyringCaller(PlainTextCredentialsProvider): +class KeyringCaller(ExternalCredentialsProvider): """KeyringCaller. A class for retrieving the password from the external 'gnome keyring' and @@ -91,22 +101,3 @@ class KeyringCaller(PlainTextCredentialsProvider): password username """ - - def configure(self, **config): - """configure. - - Parameters - ---------- - **config - Keyword arguments which contain at least the keywords "username" and - "url". - - Returns - ------- - None - """ - url = config.get("url") - username = config.get("username") - app = "PyCaosDB — {}".format(url) - password = _call_keyring(app=app, username=username) - super(KeyringCaller, self).configure(password=password, **config) diff --git a/src/caosdb/connection/authentication/pass.py b/src/caosdb/connection/authentication/pass.py index 5aa9a81a24ed02584b51ae30cb1de0d69e11d9f4..9399fc4f4a76407ad94618785adcfbb945d4c788 100644 --- a/src/caosdb/connection/authentication/pass.py +++ b/src/caosdb/connection/authentication/pass.py @@ -28,8 +28,9 @@ password. """ from subprocess import check_output, CalledProcessError +from caosdb.exceptions import ConfigurationException from .interface import CredentialsAuthenticator -from .plain import PlainTextCredentialsProvider +from .external_credentials_provider import ExternalCredentialsProvider def get_authentication_provider(): @@ -44,13 +45,19 @@ def get_authentication_provider(): CredentialsAuthenticator with a 'PassCaller' as back-end. """ - return CredentialsAuthenticator(PassCaller()) + return CredentialsAuthenticator(PassCaller(callback=_call_pass)) -def _call_pass(password_identifier): +def _call_pass(**config): + if "password_identifier" not in config: + raise ConfigurationException("Your configuration did not provide a " + "`password_identifier` which is needed " + "by the `PassCaller` to retrieve the " + "password in question.") + try: return check_output( - "pass " + password_identifier, + "pass " + config["password_identifier"], shell=True).splitlines()[0].decode("UTF-8") except CalledProcessError as exc: raise RuntimeError( @@ -59,7 +66,7 @@ def _call_pass(password_identifier): "incorrect or missing.".format(exc.returncode)) -class PassCaller(PlainTextCredentialsProvider): +class PassCaller(ExternalCredentialsProvider): """PassCaller. A class for retrieving the password from the external program 'pass' and @@ -74,20 +81,5 @@ class PassCaller(PlainTextCredentialsProvider): password username """ - - def configure(self, **config): - """configure. - - Parameters - ---------- - **config - Keyword arguments which contain at least the keywords "username" and - "password_identifier". - - Returns - ------- - None - """ - if "password_identifier" in config: - password = _call_pass(config["password_identifier"]) - super(PassCaller, self).configure(password=password, **config) + # all the work is done in _call_pass and the super class + pass diff --git a/src/caosdb/connection/authentication/plain.py b/src/caosdb/connection/authentication/plain.py index 6cb7b1b896d8d32f71437c527385f269b37cdb51..83dd592940a7010d07112f73b9bd5bcf3741a168 100644 --- a/src/caosdb/connection/authentication/plain.py +++ b/src/caosdb/connection/authentication/plain.py @@ -59,6 +59,7 @@ class PlainTextCredentialsProvider(CredentialsProvider): """ def __init__(self): + super(PlainTextCredentialsProvider, self).__init__() self._password = None self._username = None diff --git a/unittests/test_authentication_external.py b/unittests/test_authentication_external.py new file mode 100644 index 0000000000000000000000000000000000000000..a8fc6f79578c9812dab52dd1fa3807f62fd710fb --- /dev/null +++ b/unittests/test_authentication_external.py @@ -0,0 +1,61 @@ +#! -*- coding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2018 Research Group Biomedical Physics, +# Max-Planck-Institute for Dynamics and Self-Organization Göttingen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +# +"""test_authentication_external. + +Tests for the external_credentials_provider modul. +""" + +from __future__ import unicode_literals +import logging +from pytest import raises +from caosdb.connection.authentication import ( + external_credentials_provider as ecp +) + + +class _TestCaller(ecp.ExternalCredentialsProvider): + pass + + +def test_callback(): + def _callback(**config): + assert "opt" in config + return "secret" + t = _TestCaller(callback=_callback) + t.configure(opt=None) + assert t.password == "secret" + + +def test_log_password_incident(): + class _mylogger(logging.Logger): + def log(self, level, msg, *args, **kwargs): + assert level == logging.WARNING + assert "`password` defined." in msg + raise Exception("log") + + t = _TestCaller(callback=None) + t.logger = _mylogger("mylogger") + with raises(Exception) as exc_info: + t.configure(password="password") + assert exc_info.value.args[0] == "log" diff --git a/unittests/test_authentication_keyring.py b/unittests/test_authentication_keyring.py new file mode 100644 index 0000000000000000000000000000000000000000..1105b9d29604e7809c1f6ab6d6a6d027c5115025 --- /dev/null +++ b/unittests/test_authentication_keyring.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2018 Research Group Biomedical Physics, +# Max-Planck-Institute for Dynamics and Self-Organization Göttingen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +# +"""test_authentication_keyring + +Tests for the caosdb.connection.authentication.keyring module. +""" +import sys +from pytest import raises +from caosdb.connection.authentication.keyring import KeyringCaller + + +def test_initialization(): + def _callback(**config): + assert not config + raise Exception("_callback") + k = KeyringCaller(callback=_callback) + k.configure() + with raises(Exception) as exc_info: + assert k.password is None + assert exc_info.value.args[0] == "_callback" diff --git a/unittests/test_authentication_pass.py b/unittests/test_authentication_pass.py new file mode 100644 index 0000000000000000000000000000000000000000..e5e4870c890a6a19effe672a3fde127cb4ea1fd7 --- /dev/null +++ b/unittests/test_authentication_pass.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2018 Research Group Biomedical Physics, +# Max-Planck-Institute for Dynamics and Self-Organization Göttingen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +# +"""test_authentication_pass + +Tests for the caosdb.connection.authentication.pass module. +""" +import sys +from pytest import raises +_PASSCALLER = "caosdb.connection.authentication.pass" +__import__(_PASSCALLER) +PassCaller = sys.modules[_PASSCALLER].PassCaller + + +def test_initialization(): + def _callback(**config): + assert not config + raise Exception("_callback") + p = PassCaller(callback=_callback) + p.configure() + with raises(Exception) as exc_info: + p.password + assert exc_info.value.args[0] == "_callback" diff --git a/unittests/test_authentication_plain.py b/unittests/test_authentication_plain.py new file mode 100644 index 0000000000000000000000000000000000000000..26c57e953f28a2a0ab80e35026c27ff74f18d370 --- /dev/null +++ b/unittests/test_authentication_plain.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2018 Research Group Biomedical Physics, +# Max-Planck-Institute for Dynamics and Self-Organization Göttingen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +# +"""test_authentication_plain. + +Unit tests for the modul caosdb.connection.authentication.plain. +""" + +from __future__ import unicode_literals +from pytest import raises +from caosdb.connection.authentication.plain import PlainTextCredentialsProvider + + +def test_subclass_configure(): + """Test the correct passing of the password argument.""" + class SubClassOf(PlainTextCredentialsProvider): + """A simple subclass of PlainTextCredentialsProvider.""" + + def configure(self, **config): + super(SubClassOf, self).configure(password="added in subclass", **config) + + instance = SubClassOf() + instance.configure() + assert instance.password == "added in subclass" + + instance.configure(**{"somearg": "BLA"}) + assert instance.password == "added in subclass" + + instance.configure(somearg="BLA") + assert instance.password == "added in subclass" + + with raises(TypeError) as exc_info: + instance.configure(password="OH NO!") + assert exc_info.value.args[0] == ("configure() got multiple values for " + "keyword argument 'password'") + + +def test_plain_has_logger(): + p = PlainTextCredentialsProvider() + assert hasattr(p, "logger") + assert p.logger.name == "authentication"