diff --git a/src/caosdb/connection/authentication/auth_token.py b/src/caosdb/connection/authentication/auth_token.py index 4cff568d86a7380ef7b447f151df8c1622061a0a..fbce78fb86d78e06d97ef00dd162c1ed57f7560d 100644 --- a/src/caosdb/connection/authentication/auth_token.py +++ b/src/caosdb/connection/authentication/auth_token.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +#! -*- coding: utf-8 -*- # # ** header v3.0 # This file is a part of the CaosDB Project. @@ -25,10 +25,11 @@ # """auth_token. -A Authentictor which only uses only a pre-supplied authentication token. +An Authentictor which only uses only a pre-supplied authentication token. """ from __future__ import absolute_import, unicode_literals, print_function from .interface import AbstractAuthenticator, CaosDBServerConnection +from caosdb.connection.utils import auth_token_to_cookie from caosdb.exceptions import LoginFailedException @@ -79,7 +80,9 @@ class AuthTokenAuthenticator(AbstractAuthenticator): def _logout(self): self.logger.debug("[LOGOUT]") if self.auth_token is not None: - self._connection.request(method="DELETE", path="logout") + headers = {'Cookie': auth_token_to_cookie(self.auth_token)} + self._connection.request(method="DELETE", path="logout", + headers=headers) self.auth_token = None def configure(self, **config): diff --git a/src/caosdb/connection/authentication/unauthenticated.py b/src/caosdb/connection/authentication/unauthenticated.py new file mode 100644 index 0000000000000000000000000000000000000000..fefa069371cb33f401178d19ca8efa13c5a0611f --- /dev/null +++ b/src/caosdb/connection/authentication/unauthenticated.py @@ -0,0 +1,119 @@ +#! -*- 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 +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Timm Fitschen <f.fitschen@indiscale.com> +# +# 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 +# +"""unauthenticated. + +An Authentictor which suppresses any authentication and also ignores auth_token +cookies. +""" +from __future__ import absolute_import, unicode_literals, print_function +from .interface import AbstractAuthenticator, CaosDBServerConnection +from caosdb.exceptions import LoginFailedException + + +def get_authentication_provider(): + """get_authentication_provider. + + Return an authenticator which only uses a pre-supplied authentication + token. + + Returns + ------- + AuthTokenAuthenticator + """ + return Unauthenticated() + + +class Unauthenticated(AbstractAuthenticator): + """Unauthenticated. + + Subclass of AbstractAuthenticator which suppresses any authentication and + also ignores auth_token cookies. + + Methods + ------- + login + logout + configure + on_request + on_response + """ + + def __init__(self): + super(Unauthenticated, self).__init__() + self.auth_token = None + self._connection = None + + def login(self): + self._login() + + def _login(self): + raise LoginFailedException("This caosdb client is configured to stay " + "unauthenticated. Change your " + "`password_method` and provide an " + "`auth_token` or credentials if your want " + "to authenticate this client.") + + def logout(self): + self._logout() + + def _logout(self): + self.auth_token = None + + def configure(self, **config): + self.auth_token = None + + def on_request(self, method, path, headers, **kwargs): + # pylint: disable=unused-argument + """on_request. + + This implementation does not attempt to login or authenticate in any + form. + + Parameters + ---------- + method + unused + path + unused + headers + unused + **kwargs + unused + """ + pass + + def on_response(self, response): + # pylint: disable=unused-argument + """on_response. + + This implementation ignores any auth_token cookie sent by the server. + + Parameters + ---------- + response + unused + """ + pass diff --git a/src/caosdb/connection/connection.py b/src/caosdb/connection/connection.py index 5c6d30053993779e226d38c020f5c25d581db425..94ee3d0817d448285d5f828ecb8d0ac27079024a 100644 --- a/src/caosdb/connection/connection.py +++ b/src/caosdb/connection/connection.py @@ -262,8 +262,8 @@ def _get_authenticator(**config): ---------- password_method : str The simple name of a submodule of caosdb.connection.authentication. - Currently, there are three valid values for this parameter: 'plain', - 'pass', and 'keyring'. + Currently, there are four valid values for this parameter: 'plain', + 'pass', 'keyring' and 'auth_token'. **config : Any other keyword arguments are passed the configre method of the password_method. @@ -293,7 +293,8 @@ def _get_authenticator(**config): except ImportError: raise ConfigurationException("Password method \"{}\" not implemented. " - "Valid methods: plain, pass, or keyring." + "Try `plain`, `pass`, `keyring`, or " + "`auth_token`." .format(config["password_method"])) @@ -325,6 +326,7 @@ def configure_connection(**kwargs): - "input" Asks for the password. - "pass" Uses the `pass` password manager. - "keyring" Uses the `keyring` library. + - "auth_token" Uses only a given auth_token. timeout : int A connection timeout in seconds. (Default: 210) @@ -333,8 +335,9 @@ def configure_connection(**kwargs): Whether SSL certificate warnings should be ignored. Only use this for development purposes! (Default: False) - auth_token : str + auth_token : str (optional) An authentication token which has been issued by the CaosDB Server. + Implies `password_method="auth_token"` if set. implementation : CaosDBServerConnection The class which implements the connection. (Default: @@ -469,6 +472,9 @@ class _Connection(object): # pylint: disable=useless-object-inheritance "or a factory).", type_err) self._delegate_connection.configure(**config) + if "auth_token" in config: + # deprecated, needed for older scripts + config["password_method"] = "auth_token" if "password_method" not in config: raise ConfigurationException("Missing password_method. You did " "not specify a `password_method` for" diff --git a/src/caosdb/utils/caosdb_admin.py b/src/caosdb/utils/caosdb_admin.py index 46ca9b1baaf66e3380af342d3c0e8e3a489b21e7..6f0041925b0b073f797f767aace4174e660494cc 100755 --- a/src/caosdb/utils/caosdb_admin.py +++ b/src/caosdb/utils/caosdb_admin.py @@ -284,6 +284,14 @@ USAGE formatter_class=RawDescriptionHelpFormatter) parser.add_argument('-V', '--version', action='version', version=program_version_message) + parser.add_argument("--auth-token", metavar="AUTH_TOKEN", + dest="auth_token", + help=("A CaosDB authentication token (default: None). " + "If the authentication token is passed, the " + "`password_method` of the connection is set to " + "`auth_token` and the respective configuration " + "from the pycaosdb.ini is effectively being " + "overridden.")) subparsers = parser.add_subparsers( title="commands", metavar="COMMAND", @@ -600,8 +608,12 @@ USAGE # Process arguments args = parser.parse_args() - - db.configure_connection()._login() + auth_token = args.auth_token + if auth_token is not None: + db.configure_connection(password_method = "auth_token", + auth_token = auth_token) + else: + db.configure_connection() return args.call(args) diff --git a/unittests/test_authentication_auth_token.py b/unittests/test_authentication_auth_token.py new file mode 100644 index 0000000000000000000000000000000000000000..5b91a7c78fe95b868b1dd49a43d5fa76c263bed6 --- /dev/null +++ b/unittests/test_authentication_auth_token.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# +# 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_auth_token + +Unit tests for the module caosdb.connection.authentication.auth_token +""" + +from __future__ import unicode_literals +from pytest import raises +from unittest.mock import Mock +from caosdb.connection.authentication import auth_token as at +from caosdb.connection.mockup import MockUpServerConnection, MockUpResponse +from caosdb.connection.utils import parse_auth_token +from caosdb.exceptions import LoginFailedException +from caosdb import configure_connection + +def test_get_authentication_provider(): + ap = at.get_authentication_provider() + assert isinstance(ap, at.AuthTokenAuthenticator) + +def response_with_auth_token(): + token = "SessionToken=[response token];" + assert parse_auth_token(token) is not None, "cookie not ok" + + return MockUpResponse(200, {"Set-Cookie": token}, "ok") + +def test_configure_connection(): + def request_has_auth_token(**kwargs): + """test resources""" + cookie = kwargs["headers"]["Cookie"] + assert cookie is not None + assert cookie == "SessionToken=%5Brequest%20token%5D;" + + return response_with_auth_token() + + c = configure_connection(password_method="auth_token", + auth_token="[request token]", + implementation=MockUpServerConnection) + assert isinstance(c._authenticator, at.AuthTokenAuthenticator) + + c._delegate_connection.resources.append(request_has_auth_token) + assert c._authenticator.auth_token == "[request token]" + response = c._http_request(method="GET", path="test") + assert response.read() == "ok" + assert c._authenticator.auth_token == "[response token]" + +def test_login_raises(): + c = configure_connection(password_method="auth_token", + auth_token="[auth_token]") + with raises(LoginFailedException): + c._login() + + +def test_logout_calls_delete(): + mock = Mock() + + def logout_resource(**kwargs): + """logout with auth_token""" + mock.method() + assert kwargs["path"] == "logout" + assert kwargs["method"] == "DELETE" + + cookie = kwargs["headers"]["Cookie"] + assert cookie is not None + assert cookie == "SessionToken=%5Brequest%20token%5D;" + + return MockUpResponse(200, {}, "ok") + + c = configure_connection(password_method="auth_token", + auth_token="[request token]", + implementation=MockUpServerConnection) + + c._delegate_connection.resources.append(logout_resource) + c._logout() + mock.method.assert_called_once() + diff --git a/unittests/test_authentication_unauthenticated.py b/unittests/test_authentication_unauthenticated.py new file mode 100644 index 0000000000000000000000000000000000000000..54f091b175214b36895956a4a27c2871b3a0206b --- /dev/null +++ b/unittests/test_authentication_unauthenticated.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# +# 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_unauthenticated + +Unit tests for the module caosdb.connection.authentication.unauthenticated. +""" + +from __future__ import unicode_literals +from pytest import raises +from unittest.mock import Mock +from caosdb.connection.authentication import unauthenticated +from caosdb.connection.mockup import MockUpServerConnection, MockUpResponse +from caosdb.connection.utils import parse_auth_token +from caosdb.exceptions import LoginFailedException +from caosdb import configure_connection +from .test_authentication_auth_token import response_with_auth_token + + +def test_get_authentication_provider(): + ap = unauthenticated.get_authentication_provider() + assert isinstance(ap, unauthenticated.Unauthenticated) + +def test_configure_connection(): + mock = Mock() + + def request_has_no_auth_token(**kwargs): + """test resource""" + assert "Cookie" not in kwargs["headers"] + mock.method() + return response_with_auth_token() + + c = configure_connection(password_method="unauthenticated", + implementation=MockUpServerConnection) + assert isinstance(c._authenticator, unauthenticated.Unauthenticated) + + c._delegate_connection.resources.append(request_has_no_auth_token) + + assert c._authenticator.auth_token is None + response = c._http_request(method="GET", path="test") + assert response.read() == "ok" + mock.method.assert_called_once() + assert c._authenticator.auth_token is None + + +def test_login_raises(): + c = configure_connection(password_method="unauthenticated") + with raises(LoginFailedException): + c._login() + + + + + + +