diff --git a/lib/hawat/blueprints/auth_dev/__init__.py b/lib/hawat/blueprints/auth_dev/__init__.py index e3e198424b4d8cc7a7591fd1991366e1c18ea2be..91d01fd4d88ada89295645b787ce0a09121a8633 100644 --- a/lib/hawat/blueprints/auth_dev/__init__.py +++ b/lib/hawat/blueprints/auth_dev/__init__.py @@ -92,6 +92,10 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): def dbmodel(self): return self.get_model(vial.const.MODEL_USER) + @property + def search_by(self): + return self.dbmodel.login + def dispatch_request(self): """ Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`. @@ -101,47 +105,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): if form.validate_on_submit(): try: - user = self.dbquery(self.dbmodel).filter(self.dbmodel.login == form.login.data).one() - - if not user.enabled: - self.flash( - flask.Markup(gettext( - 'Please be aware, that the account for user <strong>%(login)s (%(name)s)</strong> is currently disabled.', - login = user.login, - name = user.fullname - )), - vial.const.FLASH_FAILURE - ) - - flask_login.login_user(user) - - # Tell Flask-Principal the identity changed. Access to private method - # _get_current_object is according to the Flask documentation: - # http://flask.pocoo.org/docs/1.0/reqcontext/#notes-on-proxies - flask_principal.identity_changed.send( - flask.current_app._get_current_object(), # pylint: disable=locally-disabled,protected-access - identity = flask_principal.Identity(user.get_id()) - ) - - self.flash( - flask.Markup(gettext( - 'You have been successfully logged in as <strong>%(user)s</strong>.', - user = str(user) - )), - vial.const.FLASH_SUCCESS - ) - self.logger.info( - "User '{}' successfully logged in with 'auth_dev'.".format( - user.login - ) - ) - - # Redirect user back to original page. - return self.redirect( - default_url = flask.url_for( - flask.current_app.config['ENDPOINT_LOGIN_REDIRECT'] - ) - ) + user = self.dbsession().query(self.dbmodel).filter(self.dbmodel.login == form.login.data).one() except sqlalchemy.orm.exc.MultipleResultsFound: self.logger.error( @@ -149,13 +113,14 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): form.login.data ) ) - self.abort(500) + flask.abort(500) except sqlalchemy.orm.exc.NoResultFound: self.flash( gettext('You have entered wrong login credentials.'), vial.const.FLASH_FAILURE ) + self.abort(403) except Exception: # pylint: disable=locally-disabled,broad-except self.flash( @@ -169,6 +134,49 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): traceback.TracebackException(*sys.exc_info()), 'Unable to perform developer login.', ) + self.abort(500) + + if not user.enabled: + self.flash( + flask.Markup(gettext( + 'Your user account <strong>%(login)s (%(name)s)</strong> is currently disabled, you are not permitted to log in.', + login = user.login, + name = user.fullname + )), + vial.const.FLASH_FAILURE + ) + self.abort(403) + + flask_login.login_user(user) + + # Tell Flask-Principal the identity changed. Access to private method + # _get_current_object is according to the Flask documentation: + # http://flask.pocoo.org/docs/1.0/reqcontext/#notes-on-proxies + flask_principal.identity_changed.send( + flask.current_app._get_current_object(), # pylint: disable=locally-disabled,protected-access + identity = flask_principal.Identity(user.get_id()) + ) + + self.flash( + flask.Markup(gettext( + 'You have been successfully logged in as <strong>%(user)s</strong>.', + user = str(user) + )), + vial.const.FLASH_SUCCESS + ) + self.logger.info( + "User '{}' successfully logged in with '{}'.".format( + user.login, + self.module_name + ) + ) + + # Redirect user back to original page. + return self.redirect( + default_url = flask.url_for( + flask.current_app.config['ENDPOINT_LOGIN_REDIRECT'] + ) + ) self.response_context.update( form = form, @@ -357,28 +365,14 @@ class DevAuthBlueprint(VialBlueprint): app.menu_anon.add_entry( 'view', 'login_dev', - position = 20, + position = 30, view = LoginView, hidelegend = True ) app.menu_anon.add_entry( 'view', 'register_dev', - position = 60, - view = RegisterView, - hidelegend = True - ) - app.menu_auth.add_entry( - 'view', - 'login_dev', - position = 60, - view = LoginView, - hidelegend = True - ) - app.menu_auth.add_entry( - 'view', - 'register_dev', - position = 70, + position = 130, view = RegisterView, hidelegend = True ) diff --git a/lib/hawat/blueprints/auth_dev/forms.py b/lib/hawat/blueprints/auth_dev/forms.py index fffb9a754e50cfbaa7a3c2d8fcd0090b4ad9f32f..a72dd0aa30ed1aabfa5845126d25a2347dffba1a 100644 --- a/lib/hawat/blueprints/auth_dev/forms.py +++ b/lib/hawat/blueprints/auth_dev/forms.py @@ -22,25 +22,18 @@ __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea # import wtforms from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField +import flask import flask_wtf from flask_babel import lazy_gettext # # Custom modules. # -from mentat.datatype.sqldb import UserModel, GroupModel - import vial.forms import vial.db +from vial.forms import check_login, check_unique_login, get_available_groups -from hawat.blueprints.users.forms import check_id_existence, BaseUserAccountForm - - -def get_available_groups(): - """ - Query the database for list of all available groups. - """ - return vial.db.db_query(GroupModel).order_by(GroupModel.name).all() +from hawat.blueprints.users.forms import BaseUserAccountForm class LoginForm(flask_wtf.FlaskForm): @@ -52,7 +45,8 @@ class LoginForm(flask_wtf.FlaskForm): login = wtforms.SelectField( lazy_gettext('User account:'), validators = [ - wtforms.validators.DataRequired() + wtforms.validators.DataRequired(), + check_login ] ) submit = wtforms.SubmitField( @@ -73,7 +67,8 @@ class LoginForm(flask_wtf.FlaskForm): the ``login`` selectbox. """ dbsess = vial.db.db_get().session - users = dbsess.query(UserModel).order_by(UserModel.login).all() + user_model = flask.current_app.get_model(vial.const.MODEL_USER) + users = dbsess.query(user_model).order_by(user_model.login).all() choices = [] for usr in users: @@ -91,14 +86,13 @@ class RegisterUserAccountForm(BaseUserAccountForm): validators = [ wtforms.validators.DataRequired(), wtforms.validators.Length(min = 3, max = 50), - vial.forms.check_login, - check_id_existence + check_login, + check_unique_login ] ) memberships_wanted = QuerySelectMultipleField( lazy_gettext('Requested group memberships:'), - query_factory = get_available_groups, - allow_blank = True + query_factory = get_available_groups ) justification = wtforms.TextAreaField( lazy_gettext('Justification:'), diff --git a/lib/hawat/blueprints/auth_dev/templates/auth_dev/login.html b/lib/hawat/blueprints/auth_dev/templates/auth_dev/login.html index 607d4069ba5cd342d1830edbfb9d79817eb2797c..02d7658890da4da4a8cd345ff17586d07a05b882 100644 --- a/lib/hawat/blueprints/auth_dev/templates/auth_dev/login.html +++ b/lib/hawat/blueprints/auth_dev/templates/auth_dev/login.html @@ -1,5 +1,7 @@ {%- extends "_layout.html" %} +{% block title %}{{ vial_current_view.get_view_title() }}{% endblock %} + {%- block content %} <div class="row"> diff --git a/lib/hawat/blueprints/auth_dev/test/__init__.py b/lib/hawat/blueprints/auth_dev/test/__init__.py index 1329cdd8f707c3604a6406f0b6e8148533cbd0c4..582e349b9c57bbc1833bc7e28df3a5dead6478c5 100644 --- a/lib/hawat/blueprints/auth_dev/test/__init__.py +++ b/lib/hawat/blueprints/auth_dev/test/__init__.py @@ -64,7 +64,7 @@ class AuthDevTestCase(RegistrationTestCase): def test_04_register(self): """ - Test registration with *auth_dev* module. + Test registration with *auth_dev* module - new user 'test'. """ self.assertRegister( '/auth_dev/register', @@ -78,6 +78,22 @@ class AuthDevTestCase(RegistrationTestCase): ] ) + def test_05_register_fail(self): + """ + Test registration with *auth_dev* module - existing user 'user'. + """ + self.assertRegisterFail( + '/auth_dev/register', + [ + ('submit', 'Register'), + ('login', 'user'), + ('fullname', 'Demo User'), + ('email', 'demo.user@domain.org'), + ('organization', 'TEST, org.'), + ('justification', 'I really want in.') + ] + ) + #------------------------------------------------------------------------------- diff --git a/lib/hawat/blueprints/users/forms.py b/lib/hawat/blueprints/users/forms.py index 922f029cca4f6318fa10f4e9f8ed3cd88fc84101..d37660e3e2382872e6f36136e939ce6579c94317 100644 --- a/lib/hawat/blueprints/users/forms.py +++ b/lib/hawat/blueprints/users/forms.py @@ -96,21 +96,23 @@ class BaseUserAccountForm(vial.forms.BaseItemForm): wtforms.validators.Length(min = 3, max = 250) ] ) - locale = wtforms.SelectField( + locale = vial.forms.SelectFieldWithNone( lazy_gettext('Prefered locale:'), validators = [ wtforms.validators.Optional() ], choices = [('', lazy_gettext('<< no preference >>'))], - filters = [lambda x: x or None] + filters = [lambda x: x or None], + default = '' ) - timezone = wtforms.SelectField( + timezone = vial.forms.SelectFieldWithNone( lazy_gettext('Prefered timezone:'), validators = [ wtforms.validators.Optional(), ], choices = [('', lazy_gettext('<< no preference >>'))] + list(zip(pytz.common_timezones, pytz.common_timezones)), - filters = [lambda x: x or None] + filters = [lambda x: x or None], + default = '' ) submit = wtforms.SubmitField( lazy_gettext('Submit') diff --git a/lib/vial/forms.py b/lib/vial/forms.py index a099ad502e1167099540fe687325e04c2a981ff1..aef9d2c0196a2a61268fc6766e8a12b7e1da6925 100644 --- a/lib/vial/forms.py +++ b/lib/vial/forms.py @@ -145,7 +145,7 @@ def check_unique_login(form, field): # pylint: disable=locally-disabled,unused- """ Callback for validating of uniqueness of user login. """ - user_model = flask.current_app.config[vial.const.MODEL_USER] + user_model = flask.current_app.get_model(vial.const.MODEL_USER) user = vial.db.db_session().query(user_model).filter_by(login = field.data).first() if user is not None: raise wtforms.validators.ValidationError( @@ -302,6 +302,14 @@ def check_int_list(form, field): # pylint: disable=locally-disabled,unused-argu ) +def get_available_groups(): + """ + Query the database for list of all available groups. + """ + group_model = flask.current_app.get_model(vial.const.MODEL_GROUP) + return vial.db.db_query(group_model).order_by(group_model.name).all() + + #------------------------------------------------------------------------------- @@ -491,7 +499,7 @@ class SelectFieldWithNone(wtforms.SelectField): def process_formdata(self, valuelist): if valuelist: try: - self.data = self.coerce(valuelist[0]) if valuelist[0] != 'None' else None # pylint: disable=locally-disabled,attribute-defined-outside-init + self.data = self.coerce(valuelist[0]) if valuelist[0].lower() != 'none' else None # pylint: disable=locally-disabled,attribute-defined-outside-init except ValueError: raise ValueError(self.gettext("Invalid Choice: could not coerce")) diff --git a/lib/vial/test/__init__.py b/lib/vial/test/__init__.py index 7d853f5dd5bc5469aacbf4a07705754e58b037f9..abbfc36c99c071d9193a8c8486ddcb3ff64ac7dd 100644 --- a/lib/vial/test/__init__.py +++ b/lib/vial/test/__init__.py @@ -250,11 +250,17 @@ class VialTestCase(unittest.TestCase): def user_get(self, user_type): """ - Get given user + Get given user. """ user_model = self.app.get_model(vial.const.MODEL_USER) - with self.app.app_context(): - return vial.db.db_session().query(user_model).filter(user_model.login == user_type).one_or_none() + return vial.db.db_session().query(user_model).filter(user_model.login == user_type).one_or_none() + + def user_save(self, user_object): + """ + Update given user. + """ + vial.db.db_session().add(user_object) + vial.db.db_session().commit() def assertMailbox(self, checklist): # pylint: disable=locally-disabled,invalid-name """ @@ -276,10 +282,41 @@ class RegistrationVialTestCase(VialTestCase): Class for testing :py:class:`vial.app.Vial` application registration views. """ + def assertRegisterFail(self, url, data): # pylint: disable=locally-disabled,invalid-name + response = response = self.client.get( + url, + follow_redirects = True + ) + self.assertEqual(response.status_code, 200) + self.assertTrue(b'User account registration' in response.data) + + for idx, param in enumerate(data): + if idx == len(data) - 1: + break + response = response = self.client.post( + url, + follow_redirects = True, + data = { + i[0]: i[1] for i in data[0:idx+1] + } + ) + self.assertEqual(response.status_code, 200) + self.assertTrue(b'This field is required.' in response.data) + self.assertTrue(b'help-block form-error' in response.data) + + response = response = self.client.post( + url, + follow_redirects = True, + data = { + i[0]: i[1] for i in data + } + ) + self.assertEqual(response.status_code, 200) + self.assertTrue(b'Please use different login, the "user" is already taken.' in response.data) + + def assertRegister(self, url, data): # pylint: disable=locally-disabled,invalid-name - """ - Test registration with *auth_dev* module. - """ + uname = 'test' self.mailbox_monitoring('on') response = response = self.client.get( @@ -312,13 +349,14 @@ class RegistrationVialTestCase(VialTestCase): ) self.assertEqual(response.status_code, 200) self.assertTrue(b'User account <strong>test (Test User)</strong> was successfully registered.' in response.data) - uobj = self.user_get('test') - self.assertTrue(uobj) + with self.app.app_context(): + uobj = self.user_get(uname) + self.assertTrue(uobj) self.assertMailbox( { 'subject': [ - '[{}] Account registration - test'.format(self.app.config['APPLICATION_NAME']), - '[{}] Account registration - test'.format(self.app.config['APPLICATION_NAME']) + '[{}] Account registration - {}'.format(self.app.config['APPLICATION_NAME'], uname), + '[{}] Account registration - {}'.format(self.app.config['APPLICATION_NAME'], uname) ], 'sender': [ 'root@unittest', @@ -334,3 +372,66 @@ class RegistrationVialTestCase(VialTestCase): ) self.mailbox_monitoring('off') + + with self.app.app_context(): + user = self.user_get(uname) + user_dict = user.to_dict() + del user_dict['createtime'] + self.assertEqual( + user_dict, + { + 'apikey': 'None', + 'email': 'test.user@domain.org', + 'enabled': False, + 'fullname': 'Test User', + 'id': 5, + 'locale': 'None', + 'login': 'test', + 'logintime': 'None', + 'managements': [], + 'memberships': [], + 'memberships_wanted': [], + 'organization': 'TEST, org.', + 'roles': ['user'], + 'timezone': 'None' + } + ) + response = self.login_dev(uname) + self.assertEqual(response.status_code, 403) + #self.assertTrue(b'is currently disabled, you are not permitted to log in.' in response.data) + + with self.app.app_context(): + user = self.user_get(uname) + user.set_state_enabled() + self.user_save(user) + + with self.app.app_context(): + user = self.user_get(uname) + user_dict = user.to_dict() + del user_dict['createtime'] + self.assertEqual( + user_dict, + { + 'apikey': 'None', + 'email': 'test.user@domain.org', + 'enabled': True, + 'fullname': 'Test User', + 'id': 5, + 'locale': 'None', + 'login': 'test', + 'logintime': 'None', + 'managements': [], + 'memberships': [], + 'memberships_wanted': [], + 'organization': 'TEST, org.', + 'roles': ['user'], + 'timezone': 'None' + } + ) + response = self.login_dev(uname) + self.assertEqual(response.status_code, 200) + self.assertTrue(b'You have been successfully logged in as' in response.data) + + response = self.logout() + self.assertEqual(response.status_code, 200) + self.assertTrue(b'You have been successfully logged out' in response.data)