diff --git a/src/caosdb/common/models.py b/src/caosdb/common/models.py index 6c4ed8b422354239c2ac7c9bad234f6c9d6773a1..293806a3eccf8a853db97aa3f235e51f806c59e8 100644 --- a/src/caosdb/common/models.py +++ b/src/caosdb/common/models.py @@ -3481,6 +3481,11 @@ class DropOffBox(list): return self +class UserInfo(): + + def __init__(self, xml): + self.roles = [ role.text for role in xml.findall("Roles/Role")] + class Info(): def __init__(self): @@ -3499,7 +3504,10 @@ class Info(): for e in xml: m = _parse_single_xml_element(e) - self.messages.append(m) + if isinstance(m, UserInfo): + self.user_info = m + else: + self.messages.append(m) def __str__(self): if "counts" not in self.messages: @@ -3622,6 +3630,8 @@ def _parse_single_xml_element(elem): return ACL(xml=elem) elif elem.tag == "Permissions": return Permissions(xml=elem) + elif elem.tag == "UserInfo": + return UserInfo(xml=elem) else: return Message(type=elem.tag, code=elem.get( "code"), description=elem.get("description"), body=elem.text) diff --git a/src/caosdb/connection/authentication/auth_token.py b/src/caosdb/connection/authentication/auth_token.py new file mode 100644 index 0000000000000000000000000000000000000000..4cff568d86a7380ef7b447f151df8c1622061a0a --- /dev/null +++ b/src/caosdb/connection/authentication/auth_token.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 +# 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 +# +"""auth_token. + +A 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.exceptions import LoginFailedException + + +def get_authentication_provider(): + """get_authentication_provider. + + Return an authenticator which only uses a pre-supplied authentication + token. + + Returns + ------- + AuthTokenAuthenticator + """ + return AuthTokenAuthenticator() + + +class AuthTokenAuthenticator(AbstractAuthenticator): + """AuthTokenAuthenticator. + + Subclass of AbstractAuthenticator which provides authentication only via + a given authentication token. + + Methods + ------- + login + logout + configure + """ + + def __init__(self): + super(AuthTokenAuthenticator, self).__init__() + self.auth_token = None + self._connection = None + + def login(self): + self._login() + + def _login(self): + raise LoginFailedException("The authentication token is expired or you " + "have been logged out otherwise. The " + "auth_token authenticator cannot log in " + "again. You must provide a new " + "authentication token.") + + def logout(self): + self._logout() + + def _logout(self): + self.logger.debug("[LOGOUT]") + if self.auth_token is not None: + self._connection.request(method="DELETE", path="logout") + self.auth_token = None + + def configure(self, **config): + if "auth_token" in config: + self.auth_token = config["auth_token"] + if "connection" in config: + self._connection = config["connection"] + if not isinstance(self._connection, CaosDBServerConnection): + raise Exception("""Bad configuration of the caosdb connection. + The `connection` must be an instance of + `CaosDBConnection`.""") diff --git a/src/caosdb/connection/authentication/interface.py b/src/caosdb/connection/authentication/interface.py index f156345afcb9d77c118daa7bf53b57b8499d87cc..d9a9b4306b1dbdedbc67dfda81a3ca3d7b4aeb41 100644 --- a/src/caosdb/connection/authentication/interface.py +++ b/src/caosdb/connection/authentication/interface.py @@ -50,6 +50,8 @@ class AbstractAuthenticator(ABC): logger : Logger A logger which should be used for all logging which has to do with authentication. + auth_token : str + A string representation of a CaosDB Auth Token. Methods ------- @@ -59,10 +61,6 @@ class AbstractAuthenticator(ABC): on_request on_response - Attributes - ---------- - auth_token : str - A string representation of a CaosDB Auth Token. """ def __init__(self): @@ -187,7 +185,7 @@ class CredentialsAuthenticator(AbstractAuthenticator): self._logout() def _logout(self): - _LOGGER.debug("[LOGOUT]") + self.logger.debug("[LOGOUT]") if self.auth_token is not None: self._connection.request(method="DELETE", path="logout") self.auth_token = None @@ -195,7 +193,7 @@ class CredentialsAuthenticator(AbstractAuthenticator): def _login(self): username = self._credentials_provider.username password = self._credentials_provider.password - _LOGGER.debug("[LOGIN] %s", username) + self.logger.debug("[LOGIN] %s", username) # we need a username for this: if username is None: diff --git a/src/caosdb/connection/connection.py b/src/caosdb/connection/connection.py index 3553c86ca844ca0d83852985caa67158fdbda36b..5c6d30053993779e226d38c020f5c25d581db425 100644 --- a/src/caosdb/connection/connection.py +++ b/src/caosdb/connection/connection.py @@ -333,6 +333,9 @@ def configure_connection(**kwargs): Whether SSL certificate warnings should be ignored. Only use this for development purposes! (Default: False) + auth_token : str + An authentication token which has been issued by the CaosDB Server. + implementation : CaosDBServerConnection The class which implements the connection. (Default: _DefaultCaosDBServerConnection) @@ -473,9 +476,6 @@ class _Connection(object): # pylint: disable=useless-object-inheritance self._authenticator = _get_authenticator( connection=self._delegate_connection, **config) - if "auth_token" in config: - self._authenticator.auth_token = config["auth_token"] - return self def retrieve(self, entity_uri_segments=None, query_dict=None, **kwargs): diff --git a/unittests/test_add_property.py b/unittests/test_add_property.py index bd68f31b89c439c2bd333586b65d9f012b09d7e3..917ee68b5ae3d8ce738c8dff6dbfdfed9fb75580 100644 --- a/unittests/test_add_property.py +++ b/unittests/test_add_property.py @@ -21,68 +21,82 @@ # # ** end header # +from pytest import raises import caosdb as db -from nose.tools import assert_is, assert_is_none, assert_equals, assert_is_not_none, assert_raises def test_no_parameter(): rec = db.Record() - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) - with assert_raises(UserWarning) as cm: + with raises(UserWarning) as cm: rec.add_property() - assert_equals( - cm.exception.args[0], - "This method expects you to pass at least an entity, a name or an id.") - assert_equals(0, len(rec.get_properties())) + assert cm.value.args[0] == ("This method expects you to pass at " + "least an entity, a name or an id.") + assert 0 == len(rec.get_properties()) def test_only_value_parameter(): rec = db.Record() - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) - with assert_raises(UserWarning) as cm: + with raises(UserWarning) as cm: rec.add_property(value="bla") - assert_equals( - cm.exception.args[0], - "This method expects you to pass at least an entity, a name or an id.") - assert_equals(0, len(rec.get_properties())) + assert cm.value.args[0] == ("This method expects you to pass at " + "least an entity, a name or an id.") + assert 0 == len(rec.get_properties()) def test_property_name_ambiguity_1(): rec = db.Record() - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) - with assert_raises(UserWarning) as cm: + with raises(UserWarning) as cm: rec.add_property("one_name", name="another_name") - assert_equals( - cm.exception.args[0], - "The first parameter was neither an instance of Entity nor an integer. Therefore the string representation of your first parameter would normally be interpreted name of the property which is to be added. But you have also specified a parameter 'name' in the method call. This is ambiguous and cannot be processed.") - assert_equals(0, len(rec.get_properties())) + assert cm.value.args[0] == ("The first parameter was neither an " + "instance of Entity nor an integer. " + "Therefore the string representation of " + "your first parameter would normally be " + "interpreted name of the property which " + "is to be added. But you have also " + "specified a parameter 'name' in the " + "method call. This is ambiguous and " + "cannot be processed.") + assert 0 == len(rec.get_properties()) def test_property_name_ambiguity_2(): rec = db.Record() - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) - with assert_raises(UserWarning) as cm: + with raises(UserWarning) as cm: rec.add_property({}, name="another_name") - assert_equals( - cm.exception.args[0], - "The first parameter was neither an instance of Entity nor an integer. Therefore the string representation of your first parameter would normally be interpreted name of the property which is to be added. But you have also specified a parameter 'name' in the method call. This is ambiguous and cannot be processed.") - assert_equals(0, len(rec.get_properties())) + assert cm.value.args[0] == ("The first parameter was neither an " + "instance of Entity nor an integer. " + "Therefore the string representation of " + "your first parameter would normally be " + "interpreted name of the property which " + "is to be added. But you have also " + "specified a parameter 'name' in the " + "method call. This is ambiguous and " + "cannot be processed.") + assert 0 == len(rec.get_properties()) def test_property_id_ambiguity(): rec = db.Record() - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) - with assert_raises(UserWarning) as cm: + with raises(UserWarning) as cm: rec.add_property(25, id=26) - assert_equals( - cm.exception.args[0], - "The first parameter was an integer which would normally be interpreted as the id of the property which is to be added. But you have also specified a parameter 'id' in the method call. This is ambiguous and cannot be processed.") - assert_equals(0, len(rec.get_properties())) + + assert cm.value.args[0] == ("The first parameter was an integer which " + "would normally be interpreted as the id of " + "the property which is to be added. But you " + "have also specified a parameter 'id' in the " + "method call. This is ambiguous and cannot be " + "processed.") + assert 0 == len(rec.get_properties()) def test_property_parameter_with_entity(): @@ -94,18 +108,17 @@ def test_property_parameter_with_entity(): unit="m", description="This is the length of something.") - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) rec.add_property(abstract_property) - assert_equals(1, len(rec.get_properties())) + assert 1 == len(rec.get_properties()) concrete_property = rec.get_property("length") - assert_is_not_none(concrete_property) - assert_equals(concrete_property.name, "length") - assert_equals(concrete_property.id, 512) - assert_equals(concrete_property.description, - "This is the length of something.") - assert_equals(concrete_property.unit, "m") - assert_equals(concrete_property.datatype, db.DOUBLE) - assert_is(concrete_property._wrapped_entity, abstract_property) + assert concrete_property is not None + assert concrete_property.name == "length" + assert concrete_property.id == 512 + assert concrete_property.description == "This is the length of something." + assert concrete_property.unit == "m" + assert concrete_property.datatype == db.DOUBLE + assert concrete_property._wrapped_entity == abstract_property def test_property_parameter_with_entity_and_value(): @@ -117,54 +130,53 @@ def test_property_parameter_with_entity_and_value(): unit="m", description="This is the length of something.") - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) rec.add_property(abstract_property, 3.14) - assert_equals(1, len(rec.get_properties())) + assert 1 == len(rec.get_properties()) concrete_property = rec.get_property("length") - assert_is_not_none(concrete_property) - assert_equals(concrete_property.name, "length") - assert_equals(concrete_property.id, 512) - assert_equals(concrete_property.description, - "This is the length of something.") - assert_equals(concrete_property.unit, "m") - assert_equals(concrete_property.value, 3.14) - assert_equals(concrete_property.datatype, db.DOUBLE) - assert_is(concrete_property._wrapped_entity, abstract_property) + assert concrete_property is not None + assert concrete_property.name == "length" + assert concrete_property.id == 512 + assert concrete_property.description == "This is the length of something." + assert concrete_property.unit == "m" + assert concrete_property.value == 3.14 + assert concrete_property.datatype == db.DOUBLE + assert concrete_property._wrapped_entity == abstract_property def test_property_parameter_with_id(): rec = db.Record() - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) rec.add_property(512) - assert_equals(1, len(rec.get_properties())) + assert 1 == len(rec.get_properties()) concrete_property = rec.get_property(512) - assert_is_not_none(concrete_property) - assert_equals(concrete_property.id, 512) + assert concrete_property is not None + assert concrete_property.id == 512 def test_property_parameter_with_id_and_value(): rec = db.Record() - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) rec.add_property(512, 3.14) - assert_equals(1, len(rec.get_properties())) + assert 1 == len(rec.get_properties()) concrete_property = rec.get_property(512) - assert_is_not_none(concrete_property) - assert_equals(concrete_property.id, 512) - assert_equals(concrete_property.value, 3.14) + assert concrete_property is not None + assert concrete_property.id == 512 + assert concrete_property.value == 3.14 def test_datatype(): rec = db.Record() - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) rec.add_property(512, 3.14) - assert_equals(1, len(rec.get_properties())) + assert 1 == len(rec.get_properties()) concrete_property = rec.get_property(512) - assert_is_not_none(concrete_property) - assert_equals(concrete_property.id, 512) - assert_equals(concrete_property.value, 3.14) + assert concrete_property is not None + assert concrete_property.id == 512 + assert concrete_property.value == 3.14 def test_property_parameter_with_entity_and_datatype(): @@ -176,41 +188,40 @@ def test_property_parameter_with_entity_and_datatype(): unit="m", description="This is the length of something.") - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) rec.add_property(abstract_property, 3.14, datatype=db.INTEGER) - assert_equals(1, len(rec.get_properties())) + assert 1 == len(rec.get_properties()) concrete_property = rec.get_property("length") - assert_is_not_none(concrete_property) - assert_equals(concrete_property.name, "length") - assert_equals(concrete_property.id, 512) - assert_equals(concrete_property.description, - "This is the length of something.") - assert_equals(concrete_property.unit, "m") - assert_equals(concrete_property.value, 3.14) - assert_equals(concrete_property.datatype, db.INTEGER) - assert_is(concrete_property._wrapped_entity, abstract_property) + assert concrete_property is not None + assert concrete_property.name == "length" + assert concrete_property.id == 512 + assert concrete_property.description == "This is the length of something." + assert concrete_property.unit == "m" + assert concrete_property.value == 3.14 + assert concrete_property.datatype == db.INTEGER + assert concrete_property._wrapped_entity == abstract_property def test_kw_name_and_value(): rec = db.Record() - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) rec.add_property(name="length", value=3.14) - assert_equals(1, len(rec.get_properties())) + assert 1 == len(rec.get_properties()) concrete_property = rec.get_property("length") - assert_is_not_none(concrete_property) - assert_equals(concrete_property.value, 3.14) + assert concrete_property is not None + assert concrete_property.value == 3.14 def test_kw_id_and_value(): rec = db.Record() - assert_equals(0, len(rec.get_properties())) + assert 0 == len(rec.get_properties()) rec.add_property(id=512, value=3.14) - assert_equals(1, len(rec.get_properties())) + assert 1 == len(rec.get_properties()) concrete_property = rec.get_property(512) - assert_is_not_none(concrete_property) - assert_equals(concrete_property.value, 3.14) + assert concrete_property is not None + assert concrete_property.value == 3.14 def test_add_list_of_entitities(): @@ -220,7 +231,7 @@ def test_add_list_of_entitities(): values.append(db.Record(name=str(i))) rec.add_property("listOfEntities", values) for e in rec.get_property("listOfEntities").value: - assert_is_none(e.id) + assert e.id is None i = 0 for val in values: @@ -229,5 +240,5 @@ def test_add_list_of_entitities(): i = 0 for e in rec.get_property("listOfEntities").value: - assert_equals(i, e.id) + assert i == e.id i += 1 diff --git a/unittests/test_connection.py b/unittests/test_connection.py index c1f62088c425b2ef587a6b0ca0028f38180ba2ee..2323ff975259a6a26db271f571cf4c29792c5db3 100644 --- a/unittests/test_connection.py +++ b/unittests/test_connection.py @@ -261,9 +261,28 @@ def test_missing_auth_method(): def test_missing_password(): connection = configure_connection() connection.configure(implementation=setup_two_resources, - password_method="plain", auth_token="[test-auth-token]") + password_method="plain") + connection._authenticator.auth_token="[test-auth-token]" assert connection.retrieve(["some"]).headers["Cookie"] == "SessionToken=%5Btest-auth-token%5D;" + connection.configure(implementation=setup_two_resources, password_method="plain") with raises(LoginFailedException): connection.delete(["401"]) + + +def test_auth_token_connection(): + connection = configure_connection(auth_token="blablabla", + password_method="auth_token", + implementation=setup_two_resources) + connection.retrieve(["some"]).headers["Cookie"] == "SessionToken=blablabla;" + + connection._logout() + with raises(LoginFailedException) as cm: + connection.retrieve(["some"]).headers["Cookie"] == "SessionToken=blablabla;" + assert cm.value.args[0] == ("The authentication token is expired or you " + "have been logged out otherwise. The " + "auth_token authenticator cannot log in " + "again. You must provide a new authentication " + "token.") +