# HG changeset patch # User Bartłomiej Wołyńczyk # Date 2018-02-15 17:40:03 # Node ID 0e0508deee7fa42357f8682258d5ceb91b610ae2 # Parent e793d8c772db8653ba0403e544e2437eaa78e41b my-account: security change, added select filed with email from extra emails while editing user profile, now adding extra emails required type password. Task #5386 diff --git a/rhodecode/apps/my_account/tests/test_my_account_edit.py b/rhodecode/apps/my_account/tests/test_my_account_edit.py --- a/rhodecode/apps/my_account/tests/test_my_account_edit.py +++ b/rhodecode/apps/my_account/tests/test_my_account_edit.py @@ -109,7 +109,7 @@ class TestMyAccountEdit(TestController): # ('extern_name', {'extern_name': None}), ('active', {'active': False}), ('active', {'active': True}), - ('email', {'email': 'some@email.com'}), + ('email', {'email': u'some@email.com'}), ]) def test_my_account_update(self, name, attrs, user_util): usr = user_util.create_user(password='qweqwe') @@ -120,13 +120,17 @@ class TestMyAccountEdit(TestController): params.update({'password_confirmation': ''}) params.update({'new_password': ''}) - params.update({'extern_type': 'rhodecode'}) - params.update({'extern_name': 'rhodecode'}) + params.update({'extern_type': u'rhodecode'}) + params.update({'extern_name': u'rhodecode'}) params.update({'csrf_token': self.csrf_token}) params.update(attrs) # my account page cannot set language param yet, only for admins del params['language'] + if name == 'email': + uem = user_util.create_additional_user_email(usr, attrs['email']) + email_before = User.get(user_id).email + response = self.app.post(route_path('my_account_update'), params) assert_session_flash( @@ -146,7 +150,7 @@ class TestMyAccountEdit(TestController): params['language'] = updated_params['language'] if name == 'email': - params['emails'] = [attrs['email']] + params['emails'] = [attrs['email'], email_before] if name == 'extern_type': # cannot update this via form, expected value is original one params['extern_type'] = "rhodecode" @@ -162,10 +166,10 @@ class TestMyAccountEdit(TestController): assert params == updated_params - def test_my_account_update_err_email_exists(self): + def test_my_account_update_err_email_not_exists_in_emails(self): self.log_user() - new_email = 'test_regular@mail.com' # already existing email + new_email = 'test_regular@mail.com' # not in emails params = { 'username': 'test_admin', 'new_password': 'test12', @@ -179,7 +183,7 @@ class TestMyAccountEdit(TestController): response = self.app.post(route_path('my_account_update'), params=params) - response.mustcontain('This e-mail address is already taken') + response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com') def test_my_account_update_bad_email_address(self): self.log_user('test_regular2', 'test12') @@ -197,7 +201,4 @@ class TestMyAccountEdit(TestController): response = self.app.post(route_path('my_account_update'), params=params) - response.mustcontain('An email address must contain a single @') - msg = u'Username "%(username)s" already exists' - msg = h.html_escape(msg % {'username': 'test_admin'}) - response.mustcontain(u"%s" % msg) + response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com') diff --git a/rhodecode/apps/my_account/tests/test_my_account_emails.py b/rhodecode/apps/my_account/tests/test_my_account_emails.py --- a/rhodecode/apps/my_account/tests/test_my_account_emails.py +++ b/rhodecode/apps/my_account/tests/test_my_account_emails.py @@ -24,7 +24,7 @@ from rhodecode.apps._base import ADMIN_P from rhodecode.model.db import User, UserEmailMap from rhodecode.tests import ( TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL, - assert_session_flash) + assert_session_flash, TEST_USER_REGULAR_PASS) from rhodecode.tests.fixture import Fixture fixture = Fixture() @@ -47,30 +47,14 @@ class TestMyAccountEmails(TestController response = self.app.get(route_path('my_account_emails')) response.mustcontain('No additional emails specified') - def test_my_account_my_emails_add_existing_email(self): - self.log_user() - response = self.app.get(route_path('my_account_emails')) - response.mustcontain('No additional emails specified') - response = self.app.post(route_path('my_account_emails_add'), - {'new_email': TEST_USER_REGULAR_EMAIL, - 'csrf_token': self.csrf_token}) - assert_session_flash(response, 'This e-mail address is already taken') - - def test_my_account_my_emails_add_mising_email_in_form(self): - self.log_user() - response = self.app.get(route_path('my_account_emails')) - response.mustcontain('No additional emails specified') - response = self.app.post(route_path('my_account_emails_add'), - {'csrf_token': self.csrf_token}) - assert_session_flash(response, 'Please enter an email address') - def test_my_account_my_emails_add_remove(self): self.log_user() response = self.app.get(route_path('my_account_emails')) response.mustcontain('No additional emails specified') response = self.app.post(route_path('my_account_emails_add'), - {'new_email': 'foo@barz.com', + {'email': 'foo@barz.com', + 'current_password': TEST_USER_REGULAR_PASS, 'csrf_token': self.csrf_token}) response = self.app.get(route_path('my_account_emails')) diff --git a/rhodecode/apps/my_account/views/my_account.py b/rhodecode/apps/my_account/views/my_account.py --- a/rhodecode/apps/my_account/views/my_account.py +++ b/rhodecode/apps/my_account/views/my_account.py @@ -232,40 +232,59 @@ class MyAccountView(BaseAppView, DataGri c.user_email_map = UserEmailMap.query()\ .filter(UserEmailMap.user == c.user).all() + + schema = user_schema.AddEmailSchema().bind( + username=c.user.username, user_emails=c.user.emails) + + form = forms.RcForm(schema, + action=h.route_path('my_account_emails_add'), + buttons=(forms.buttons.save, forms.buttons.reset)) + + c.form = form return self._get_template_context(c) @LoginRequired() @NotAnonymous() @CSRFRequired() @view_config( - route_name='my_account_emails_add', request_method='POST') + route_name='my_account_emails_add', request_method='POST', + renderer='rhodecode:templates/admin/my_account/my_account.mako') def my_account_emails_add(self): _ = self.request.translate c = self.load_default_context() + c.active = 'emails' - email = self.request.POST.get('new_email') + schema = user_schema.AddEmailSchema().bind( + username=c.user.username, user_emails=c.user.emails) + form = forms.RcForm( + schema, action=h.route_path('my_account_emails_add'), + buttons=(forms.buttons.save, forms.buttons.reset)) + + controls = self.request.POST.items() try: - form = UserExtraEmailForm(self.request.translate)() - data = form.to_python({'email': email}) - email = data['email'] - - UserModel().add_extra_email(c.user.user_id, email) + valid_data = form.validate(controls) + UserModel().add_extra_email(c.user.user_id, valid_data['email']) audit_logger.store_web( 'user.edit.email.add', action_data={ - 'data': {'email': email, 'user': 'self'}}, + 'data': {'email': valid_data['email'], 'user': 'self'}}, user=self._rhodecode_user,) - Session().commit() - h.flash(_("Added new email address `%s` for user account") % email, - category='success') except formencode.Invalid as error: h.flash(h.escape(error.error_dict['email']), category='error') + except forms.ValidationFailure as e: + c.user_email_map = UserEmailMap.query() \ + .filter(UserEmailMap.user == c.user).all() + c.form = e + return self._get_template_context(c) except Exception: - log.exception("Exception in my_account_emails") - h.flash(_('An error occurred during email saving'), + log.exception("Exception adding email") + h.flash(_('Error occurred during adding email'), category='error') - return HTTPFound(h.route_path('my_account_emails')) + else: + h.flash(_("Successfully added email"), category='success') + + raise HTTPFound(self.request.route_path('my_account_emails')) @LoginRequired() @NotAnonymous() @@ -414,22 +433,23 @@ class MyAccountView(BaseAppView, DataGri def my_account_edit(self): c = self.load_default_context() c.active = 'profile_edit' - - c.perm_user = c.auth_user c.extern_type = c.user.extern_type c.extern_name = c.user.extern_name - defaults = c.user.get_dict() + schema = user_schema.UserProfileSchema().bind( + username=c.user.username, user_emails=c.user.emails) + appstruct = { + 'username': c.user.username, + 'email': c.user.email, + 'firstname': c.user.firstname, + 'lastname': c.user.lastname, + } + c.form = forms.RcForm( + schema, appstruct=appstruct, + action=h.route_path('my_account_update'), + buttons=(forms.buttons.save, forms.buttons.reset)) - data = render('rhodecode:templates/admin/my_account/my_account.mako', - self._get_template_context(c), self.request) - html = formencode.htmlfill.render( - data, - defaults=defaults, - encoding="UTF-8", - force_defaults=False - ) - return Response(html) + return self._get_template_context(c) @LoginRequired() @NotAnonymous() @@ -442,55 +462,40 @@ class MyAccountView(BaseAppView, DataGri _ = self.request.translate c = self.load_default_context() c.active = 'profile_edit' - c.perm_user = c.auth_user c.extern_type = c.user.extern_type c.extern_name = c.user.extern_name - _form = UserForm(self.request.translate, edit=True, - old_data={'user_id': self._rhodecode_user.user_id, - 'email': self._rhodecode_user.email})() - form_result = {} + schema = user_schema.UserProfileSchema().bind( + username=c.user.username, user_emails=c.user.emails) + form = forms.RcForm( + schema, buttons=(forms.buttons.save, forms.buttons.reset)) + + controls = self.request.POST.items() try: - post_data = dict(self.request.POST) - post_data['new_password'] = '' - post_data['password_confirmation'] = '' - form_result = _form.to_python(post_data) - # skip updating those attrs for my account + valid_data = form.validate(controls) skip_attrs = ['admin', 'active', 'extern_type', 'extern_name', 'new_password', 'password_confirmation'] - # TODO: plugin should define if username can be updated if c.extern_type != "rhodecode": # forbid updating username for external accounts skip_attrs.append('username') - + old_email = c.user.email UserModel().update_user( - self._rhodecode_user.user_id, skip_attrs=skip_attrs, - **form_result) - h.flash(_('Your account was updated successfully'), - category='success') + self._rhodecode_user.user_id, skip_attrs=skip_attrs, + **valid_data) + if old_email != valid_data['email']: + old = UserEmailMap.query() \ + .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first() + old.email = old_email + h.flash(_('Your account was updated successfully'), category='success') Session().commit() - - except formencode.Invalid as errors: - data = render( - 'rhodecode:templates/admin/my_account/my_account.mako', - self._get_template_context(c), self.request) - - html = formencode.htmlfill.render( - data, - defaults=errors.value, - errors=errors.error_dict or {}, - prefix_error=False, - encoding="UTF-8", - force_defaults=False) - return Response(html) - + except forms.ValidationFailure as e: + c.form = e + return self._get_template_context(c) except Exception: log.exception("Exception updating user") - h.flash(_('Error occurred during update of user %s') - % form_result.get('username'), category='error') - raise HTTPFound(h.route_path('my_account_profile')) - + h.flash(_('Error occurred during update of user'), + category='error') raise HTTPFound(h.route_path('my_account_profile')) def _get_pull_requests_list(self, statuses): diff --git a/rhodecode/model/validation_schema/schemas/user_schema.py b/rhodecode/model/validation_schema/schemas/user_schema.py --- a/rhodecode/model/validation_schema/schemas/user_schema.py +++ b/rhodecode/model/validation_schema/schemas/user_schema.py @@ -22,10 +22,11 @@ import re import colander from rhodecode import forms -from rhodecode.model.db import User +from rhodecode.model.db import User, UserEmailMap from rhodecode.model.validation_schema import types, validators from rhodecode.translation import _ from rhodecode.lib.auth import check_password +from rhodecode.lib import helpers as h @colander.deferred @@ -40,6 +41,7 @@ def deferred_user_password_validator(nod return _user_password_validator + class ChangePasswordSchema(colander.Schema): current_password = colander.SchemaNode( @@ -123,3 +125,64 @@ class UserSchema(colander.Schema): appstruct = super(UserSchema, self).deserialize(cstruct) return appstruct + + +@colander.deferred +def deferred_user_email_in_emails_validator(node, kw): + return colander.OneOf(kw.get('user_emails')) + + +@colander.deferred +def deferred_additional_email_validator(node, kw): + emails = kw.get('user_emails') + + def name_validator(node, value): + if value in emails: + msg = _('This e-mail address is already taken') + raise colander.Invalid(node, msg) + user = User.get_by_email(value, case_insensitive=True) + if user: + msg = _(u'This e-mail address is already taken') + raise colander.Invalid(node, msg) + c = colander.Email() + return c(node, value) + return name_validator + + +@colander.deferred +def deferred_user_email_in_emails_widget(node, kw): + import deform.widget + emails = [(email, email) for email in kw.get('user_emails')] + return deform.widget.Select2Widget(values=emails) + + +class UserProfileSchema(colander.Schema): + username = colander.SchemaNode( + colander.String(), + validator=deferred_username_validator) + + firstname = colander.SchemaNode( + colander.String(), missing='', title='First name') + + lastname = colander.SchemaNode( + colander.String(), missing='', title='Last name') + + email = colander.SchemaNode( + colander.String(), widget=deferred_user_email_in_emails_widget, + validator=deferred_user_email_in_emails_validator, + description=h.literal( + _('Additional emails can be specified at extra emails page.').format( + '/_admin/my_account/emails')), + ) + + +class AddEmailSchema(colander.Schema): + current_password = colander.SchemaNode( + colander.String(), + missing=colander.required, + widget=forms.widget.PasswordWidget(redisplay=True), + validator=deferred_user_password_validator) + + email = colander.SchemaNode( + colander.String(), title='New Email', + validator=deferred_additional_email_validator) diff --git a/rhodecode/templates/admin/my_account/my_account_emails.mako b/rhodecode/templates/admin/my_account/my_account_emails.mako --- a/rhodecode/templates/admin/my_account/my_account_emails.mako +++ b/rhodecode/templates/admin/my_account/my_account_emails.mako @@ -48,25 +48,7 @@
- ${h.secure_form(h.route_path('my_account_emails_add'), request=request)} -
- -
-
-
- -
-
- ${h.text('new_email', class_='medium')} -
-
-
- ${h.submit('save',_('Add'),class_="btn")} - ${h.reset('reset',_('Reset'),class_="btn")} -
-
-
- ${h.end_form()} + ${c.form.render() | n}
diff --git a/rhodecode/templates/admin/my_account/my_account_profile_edit.mako b/rhodecode/templates/admin/my_account/my_account_profile_edit.mako --- a/rhodecode/templates/admin/my_account/my_account_profile_edit.mako +++ b/rhodecode/templates/admin/my_account/my_account_profile_edit.mako @@ -6,11 +6,10 @@
- ${h.secure_form(h.route_path('my_account_update'), class_='form', request=request)} <% readonly = None %> <% disabled = "" %> - % if c.extern_type != 'rhodecode': + %if c.extern_type != 'rhodecode': <% readonly = "readonly" %> <% disabled = "disabled" %>
@@ -24,7 +23,7 @@
- ${h.text('username', class_='input-valuedisplay', readonly=readonly)} + ${c.user.username}
@@ -33,7 +32,7 @@
- ${h.text('firstname', class_='input-valuedisplay', readonly=readonly)} + ${c.user.firstname}
@@ -42,7 +41,7 @@
- ${h.text('lastname', class_='input-valuedisplay', readonly=readonly)} + ${c.user.lastname}
@@ -64,48 +63,7 @@ %endif -
-
- -
-
- ${h.text('username', class_='medium%s' % disabled, readonly=readonly)} - ${h.hidden('extern_name', c.extern_name)} - ${h.hidden('extern_type', c.extern_type)} -
-
-
-
- -
-
- ${h.text('firstname', class_="medium")} -
-
- -
-
- -
-
- ${h.text('lastname', class_="medium")} -
-
- -
-
- -
-
- ## we should be able to edit email ! - ${h.text('email', class_="medium")} -
-
- -
- ${h.submit('save', _('Save'), class_="btn")} - ${h.reset('reset', _('Reset'), class_="btn")} -
+ ${c.form.render()| n} % endif diff --git a/rhodecode/tests/fixture.py b/rhodecode/tests/fixture.py --- a/rhodecode/tests/fixture.py +++ b/rhodecode/tests/fixture.py @@ -30,7 +30,7 @@ import shutil import configobj from rhodecode.tests import * -from rhodecode.model.db import Repository, User, RepoGroup, UserGroup, Gist +from rhodecode.model.db import Repository, User, RepoGroup, UserGroup, Gist, UserEmailMap from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel from rhodecode.model.user import UserModel @@ -275,6 +275,13 @@ class Fixture(object): UserModel().delete(userid) Session().commit() + def create_additional_user_email(self, user, email): + uem = UserEmailMap() + uem.user = user + uem.email = email + Session().add(uem) + return uem + def destroy_users(self, userid_iter): for user_id in userid_iter: if User.get_by_username(user_id): diff --git a/rhodecode/tests/plugin.py b/rhodecode/tests/plugin.py --- a/rhodecode/tests/plugin.py +++ b/rhodecode/tests/plugin.py @@ -1178,6 +1178,10 @@ class UserUtility(object): self.user_ids.append(user.user_id) return user + def create_additional_user_email(self, user, email): + uem = self.fixture.create_additional_user_email(user=user, email=email) + return uem + def create_user_with_group(self): user = self.create_user() user_group = self.create_user_group(members=[user])