##// END OF EJS Templates
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
Bartłomiej Wołyńczyk -
r2592:0e0508de default
parent child Browse files
Show More
@@ -109,7 +109,7 b' class TestMyAccountEdit(TestController):'
109 # ('extern_name', {'extern_name': None}),
109 # ('extern_name', {'extern_name': None}),
110 ('active', {'active': False}),
110 ('active', {'active': False}),
111 ('active', {'active': True}),
111 ('active', {'active': True}),
112 ('email', {'email': 'some@email.com'}),
112 ('email', {'email': u'some@email.com'}),
113 ])
113 ])
114 def test_my_account_update(self, name, attrs, user_util):
114 def test_my_account_update(self, name, attrs, user_util):
115 usr = user_util.create_user(password='qweqwe')
115 usr = user_util.create_user(password='qweqwe')
@@ -120,13 +120,17 b' class TestMyAccountEdit(TestController):'
120
120
121 params.update({'password_confirmation': ''})
121 params.update({'password_confirmation': ''})
122 params.update({'new_password': ''})
122 params.update({'new_password': ''})
123 params.update({'extern_type': 'rhodecode'})
123 params.update({'extern_type': u'rhodecode'})
124 params.update({'extern_name': 'rhodecode'})
124 params.update({'extern_name': u'rhodecode'})
125 params.update({'csrf_token': self.csrf_token})
125 params.update({'csrf_token': self.csrf_token})
126
126
127 params.update(attrs)
127 params.update(attrs)
128 # my account page cannot set language param yet, only for admins
128 # my account page cannot set language param yet, only for admins
129 del params['language']
129 del params['language']
130 if name == 'email':
131 uem = user_util.create_additional_user_email(usr, attrs['email'])
132 email_before = User.get(user_id).email
133
130 response = self.app.post(route_path('my_account_update'), params)
134 response = self.app.post(route_path('my_account_update'), params)
131
135
132 assert_session_flash(
136 assert_session_flash(
@@ -146,7 +150,7 b' class TestMyAccountEdit(TestController):'
146 params['language'] = updated_params['language']
150 params['language'] = updated_params['language']
147
151
148 if name == 'email':
152 if name == 'email':
149 params['emails'] = [attrs['email']]
153 params['emails'] = [attrs['email'], email_before]
150 if name == 'extern_type':
154 if name == 'extern_type':
151 # cannot update this via form, expected value is original one
155 # cannot update this via form, expected value is original one
152 params['extern_type'] = "rhodecode"
156 params['extern_type'] = "rhodecode"
@@ -162,10 +166,10 b' class TestMyAccountEdit(TestController):'
162
166
163 assert params == updated_params
167 assert params == updated_params
164
168
165 def test_my_account_update_err_email_exists(self):
169 def test_my_account_update_err_email_not_exists_in_emails(self):
166 self.log_user()
170 self.log_user()
167
171
168 new_email = 'test_regular@mail.com' # already existing email
172 new_email = 'test_regular@mail.com' # not in emails
169 params = {
173 params = {
170 'username': 'test_admin',
174 'username': 'test_admin',
171 'new_password': 'test12',
175 'new_password': 'test12',
@@ -179,7 +183,7 b' class TestMyAccountEdit(TestController):'
179 response = self.app.post(route_path('my_account_update'),
183 response = self.app.post(route_path('my_account_update'),
180 params=params)
184 params=params)
181
185
182 response.mustcontain('This e-mail address is already taken')
186 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
183
187
184 def test_my_account_update_bad_email_address(self):
188 def test_my_account_update_bad_email_address(self):
185 self.log_user('test_regular2', 'test12')
189 self.log_user('test_regular2', 'test12')
@@ -197,7 +201,4 b' class TestMyAccountEdit(TestController):'
197 response = self.app.post(route_path('my_account_update'),
201 response = self.app.post(route_path('my_account_update'),
198 params=params)
202 params=params)
199
203
200 response.mustcontain('An email address must contain a single @')
204 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
201 msg = u'Username "%(username)s" already exists'
202 msg = h.html_escape(msg % {'username': 'test_admin'})
203 response.mustcontain(u"%s" % msg)
@@ -24,7 +24,7 b' from rhodecode.apps._base import ADMIN_P'
24 from rhodecode.model.db import User, UserEmailMap
24 from rhodecode.model.db import User, UserEmailMap
25 from rhodecode.tests import (
25 from rhodecode.tests import (
26 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
26 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
27 assert_session_flash)
27 assert_session_flash, TEST_USER_REGULAR_PASS)
28 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.fixture import Fixture
29
29
30 fixture = Fixture()
30 fixture = Fixture()
@@ -47,30 +47,14 b' class TestMyAccountEmails(TestController'
47 response = self.app.get(route_path('my_account_emails'))
47 response = self.app.get(route_path('my_account_emails'))
48 response.mustcontain('No additional emails specified')
48 response.mustcontain('No additional emails specified')
49
49
50 def test_my_account_my_emails_add_existing_email(self):
51 self.log_user()
52 response = self.app.get(route_path('my_account_emails'))
53 response.mustcontain('No additional emails specified')
54 response = self.app.post(route_path('my_account_emails_add'),
55 {'new_email': TEST_USER_REGULAR_EMAIL,
56 'csrf_token': self.csrf_token})
57 assert_session_flash(response, 'This e-mail address is already taken')
58
59 def test_my_account_my_emails_add_mising_email_in_form(self):
60 self.log_user()
61 response = self.app.get(route_path('my_account_emails'))
62 response.mustcontain('No additional emails specified')
63 response = self.app.post(route_path('my_account_emails_add'),
64 {'csrf_token': self.csrf_token})
65 assert_session_flash(response, 'Please enter an email address')
66
67 def test_my_account_my_emails_add_remove(self):
50 def test_my_account_my_emails_add_remove(self):
68 self.log_user()
51 self.log_user()
69 response = self.app.get(route_path('my_account_emails'))
52 response = self.app.get(route_path('my_account_emails'))
70 response.mustcontain('No additional emails specified')
53 response.mustcontain('No additional emails specified')
71
54
72 response = self.app.post(route_path('my_account_emails_add'),
55 response = self.app.post(route_path('my_account_emails_add'),
73 {'new_email': 'foo@barz.com',
56 {'email': 'foo@barz.com',
57 'current_password': TEST_USER_REGULAR_PASS,
74 'csrf_token': self.csrf_token})
58 'csrf_token': self.csrf_token})
75
59
76 response = self.app.get(route_path('my_account_emails'))
60 response = self.app.get(route_path('my_account_emails'))
@@ -232,40 +232,59 b' class MyAccountView(BaseAppView, DataGri'
232
232
233 c.user_email_map = UserEmailMap.query()\
233 c.user_email_map = UserEmailMap.query()\
234 .filter(UserEmailMap.user == c.user).all()
234 .filter(UserEmailMap.user == c.user).all()
235
236 schema = user_schema.AddEmailSchema().bind(
237 username=c.user.username, user_emails=c.user.emails)
238
239 form = forms.RcForm(schema,
240 action=h.route_path('my_account_emails_add'),
241 buttons=(forms.buttons.save, forms.buttons.reset))
242
243 c.form = form
235 return self._get_template_context(c)
244 return self._get_template_context(c)
236
245
237 @LoginRequired()
246 @LoginRequired()
238 @NotAnonymous()
247 @NotAnonymous()
239 @CSRFRequired()
248 @CSRFRequired()
240 @view_config(
249 @view_config(
241 route_name='my_account_emails_add', request_method='POST')
250 route_name='my_account_emails_add', request_method='POST',
251 renderer='rhodecode:templates/admin/my_account/my_account.mako')
242 def my_account_emails_add(self):
252 def my_account_emails_add(self):
243 _ = self.request.translate
253 _ = self.request.translate
244 c = self.load_default_context()
254 c = self.load_default_context()
255 c.active = 'emails'
245
256
246 email = self.request.POST.get('new_email')
257 schema = user_schema.AddEmailSchema().bind(
258 username=c.user.username, user_emails=c.user.emails)
247
259
260 form = forms.RcForm(
261 schema, action=h.route_path('my_account_emails_add'),
262 buttons=(forms.buttons.save, forms.buttons.reset))
263
264 controls = self.request.POST.items()
248 try:
265 try:
249 form = UserExtraEmailForm(self.request.translate)()
266 valid_data = form.validate(controls)
250 data = form.to_python({'email': email})
267 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
251 email = data['email']
252
253 UserModel().add_extra_email(c.user.user_id, email)
254 audit_logger.store_web(
268 audit_logger.store_web(
255 'user.edit.email.add', action_data={
269 'user.edit.email.add', action_data={
256 'data': {'email': email, 'user': 'self'}},
270 'data': {'email': valid_data['email'], 'user': 'self'}},
257 user=self._rhodecode_user,)
271 user=self._rhodecode_user,)
258
259 Session().commit()
272 Session().commit()
260 h.flash(_("Added new email address `%s` for user account") % email,
261 category='success')
262 except formencode.Invalid as error:
273 except formencode.Invalid as error:
263 h.flash(h.escape(error.error_dict['email']), category='error')
274 h.flash(h.escape(error.error_dict['email']), category='error')
275 except forms.ValidationFailure as e:
276 c.user_email_map = UserEmailMap.query() \
277 .filter(UserEmailMap.user == c.user).all()
278 c.form = e
279 return self._get_template_context(c)
264 except Exception:
280 except Exception:
265 log.exception("Exception in my_account_emails")
281 log.exception("Exception adding email")
266 h.flash(_('An error occurred during email saving'),
282 h.flash(_('Error occurred during adding email'),
267 category='error')
283 category='error')
268 return HTTPFound(h.route_path('my_account_emails'))
284 else:
285 h.flash(_("Successfully added email"), category='success')
286
287 raise HTTPFound(self.request.route_path('my_account_emails'))
269
288
270 @LoginRequired()
289 @LoginRequired()
271 @NotAnonymous()
290 @NotAnonymous()
@@ -414,22 +433,23 b' class MyAccountView(BaseAppView, DataGri'
414 def my_account_edit(self):
433 def my_account_edit(self):
415 c = self.load_default_context()
434 c = self.load_default_context()
416 c.active = 'profile_edit'
435 c.active = 'profile_edit'
417
418 c.perm_user = c.auth_user
419 c.extern_type = c.user.extern_type
436 c.extern_type = c.user.extern_type
420 c.extern_name = c.user.extern_name
437 c.extern_name = c.user.extern_name
421
438
422 defaults = c.user.get_dict()
439 schema = user_schema.UserProfileSchema().bind(
440 username=c.user.username, user_emails=c.user.emails)
441 appstruct = {
442 'username': c.user.username,
443 'email': c.user.email,
444 'firstname': c.user.firstname,
445 'lastname': c.user.lastname,
446 }
447 c.form = forms.RcForm(
448 schema, appstruct=appstruct,
449 action=h.route_path('my_account_update'),
450 buttons=(forms.buttons.save, forms.buttons.reset))
423
451
424 data = render('rhodecode:templates/admin/my_account/my_account.mako',
452 return self._get_template_context(c)
425 self._get_template_context(c), self.request)
426 html = formencode.htmlfill.render(
427 data,
428 defaults=defaults,
429 encoding="UTF-8",
430 force_defaults=False
431 )
432 return Response(html)
433
453
434 @LoginRequired()
454 @LoginRequired()
435 @NotAnonymous()
455 @NotAnonymous()
@@ -442,55 +462,40 b' class MyAccountView(BaseAppView, DataGri'
442 _ = self.request.translate
462 _ = self.request.translate
443 c = self.load_default_context()
463 c = self.load_default_context()
444 c.active = 'profile_edit'
464 c.active = 'profile_edit'
445
446 c.perm_user = c.auth_user
465 c.perm_user = c.auth_user
447 c.extern_type = c.user.extern_type
466 c.extern_type = c.user.extern_type
448 c.extern_name = c.user.extern_name
467 c.extern_name = c.user.extern_name
449
468
450 _form = UserForm(self.request.translate, edit=True,
469 schema = user_schema.UserProfileSchema().bind(
451 old_data={'user_id': self._rhodecode_user.user_id,
470 username=c.user.username, user_emails=c.user.emails)
452 'email': self._rhodecode_user.email})()
471 form = forms.RcForm(
453 form_result = {}
472 schema, buttons=(forms.buttons.save, forms.buttons.reset))
473
474 controls = self.request.POST.items()
454 try:
475 try:
455 post_data = dict(self.request.POST)
476 valid_data = form.validate(controls)
456 post_data['new_password'] = ''
457 post_data['password_confirmation'] = ''
458 form_result = _form.to_python(post_data)
459 # skip updating those attrs for my account
460 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
477 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
461 'new_password', 'password_confirmation']
478 'new_password', 'password_confirmation']
462 # TODO: plugin should define if username can be updated
463 if c.extern_type != "rhodecode":
479 if c.extern_type != "rhodecode":
464 # forbid updating username for external accounts
480 # forbid updating username for external accounts
465 skip_attrs.append('username')
481 skip_attrs.append('username')
466
482 old_email = c.user.email
467 UserModel().update_user(
483 UserModel().update_user(
468 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
484 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
469 **form_result)
485 **valid_data)
470 h.flash(_('Your account was updated successfully'),
486 if old_email != valid_data['email']:
471 category='success')
487 old = UserEmailMap.query() \
488 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
489 old.email = old_email
490 h.flash(_('Your account was updated successfully'), category='success')
472 Session().commit()
491 Session().commit()
473
492 except forms.ValidationFailure as e:
474 except formencode.Invalid as errors:
493 c.form = e
475 data = render(
494 return self._get_template_context(c)
476 'rhodecode:templates/admin/my_account/my_account.mako',
477 self._get_template_context(c), self.request)
478
479 html = formencode.htmlfill.render(
480 data,
481 defaults=errors.value,
482 errors=errors.error_dict or {},
483 prefix_error=False,
484 encoding="UTF-8",
485 force_defaults=False)
486 return Response(html)
487
488 except Exception:
495 except Exception:
489 log.exception("Exception updating user")
496 log.exception("Exception updating user")
490 h.flash(_('Error occurred during update of user %s')
497 h.flash(_('Error occurred during update of user'),
491 % form_result.get('username'), category='error')
498 category='error')
492 raise HTTPFound(h.route_path('my_account_profile'))
493
494 raise HTTPFound(h.route_path('my_account_profile'))
499 raise HTTPFound(h.route_path('my_account_profile'))
495
500
496 def _get_pull_requests_list(self, statuses):
501 def _get_pull_requests_list(self, statuses):
@@ -22,10 +22,11 b' import re'
22 import colander
22 import colander
23
23
24 from rhodecode import forms
24 from rhodecode import forms
25 from rhodecode.model.db import User
25 from rhodecode.model.db import User, UserEmailMap
26 from rhodecode.model.validation_schema import types, validators
26 from rhodecode.model.validation_schema import types, validators
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.lib.auth import check_password
28 from rhodecode.lib.auth import check_password
29 from rhodecode.lib import helpers as h
29
30
30
31
31 @colander.deferred
32 @colander.deferred
@@ -40,6 +41,7 b' def deferred_user_password_validator(nod'
40 return _user_password_validator
41 return _user_password_validator
41
42
42
43
44
43 class ChangePasswordSchema(colander.Schema):
45 class ChangePasswordSchema(colander.Schema):
44
46
45 current_password = colander.SchemaNode(
47 current_password = colander.SchemaNode(
@@ -123,3 +125,64 b' class UserSchema(colander.Schema):'
123
125
124 appstruct = super(UserSchema, self).deserialize(cstruct)
126 appstruct = super(UserSchema, self).deserialize(cstruct)
125 return appstruct
127 return appstruct
128
129
130 @colander.deferred
131 def deferred_user_email_in_emails_validator(node, kw):
132 return colander.OneOf(kw.get('user_emails'))
133
134
135 @colander.deferred
136 def deferred_additional_email_validator(node, kw):
137 emails = kw.get('user_emails')
138
139 def name_validator(node, value):
140 if value in emails:
141 msg = _('This e-mail address is already taken')
142 raise colander.Invalid(node, msg)
143 user = User.get_by_email(value, case_insensitive=True)
144 if user:
145 msg = _(u'This e-mail address is already taken')
146 raise colander.Invalid(node, msg)
147 c = colander.Email()
148 return c(node, value)
149 return name_validator
150
151
152 @colander.deferred
153 def deferred_user_email_in_emails_widget(node, kw):
154 import deform.widget
155 emails = [(email, email) for email in kw.get('user_emails')]
156 return deform.widget.Select2Widget(values=emails)
157
158
159 class UserProfileSchema(colander.Schema):
160 username = colander.SchemaNode(
161 colander.String(),
162 validator=deferred_username_validator)
163
164 firstname = colander.SchemaNode(
165 colander.String(), missing='', title='First name')
166
167 lastname = colander.SchemaNode(
168 colander.String(), missing='', title='Last name')
169
170 email = colander.SchemaNode(
171 colander.String(), widget=deferred_user_email_in_emails_widget,
172 validator=deferred_user_email_in_emails_validator,
173 description=h.literal(
174 _('Additional emails can be specified at <a href="{}">extra emails</a> page.').format(
175 '/_admin/my_account/emails')),
176 )
177
178
179 class AddEmailSchema(colander.Schema):
180 current_password = colander.SchemaNode(
181 colander.String(),
182 missing=colander.required,
183 widget=forms.widget.PasswordWidget(redisplay=True),
184 validator=deferred_user_password_validator)
185
186 email = colander.SchemaNode(
187 colander.String(), title='New Email',
188 validator=deferred_additional_email_validator)
@@ -48,25 +48,7 b''
48 </div>
48 </div>
49
49
50 <div>
50 <div>
51 ${h.secure_form(h.route_path('my_account_emails_add'), request=request)}
51 ${c.form.render() | n}
52 <div class="form">
53 <!-- fields -->
54 <div class="fields">
55 <div class="field">
56 <div class="label">
57 <label for="new_email">${_('New email address')}:</label>
58 </div>
59 <div class="input">
60 ${h.text('new_email', class_='medium')}
61 </div>
62 </div>
63 <div class="buttons">
64 ${h.submit('save',_('Add'),class_="btn")}
65 ${h.reset('reset',_('Reset'),class_="btn")}
66 </div>
67 </div>
68 </div>
69 ${h.end_form()}
70 </div>
52 </div>
71 </div>
53 </div>
72 </div>
54 </div>
@@ -6,11 +6,10 b''
6 </div>
6 </div>
7
7
8 <div class="panel-body">
8 <div class="panel-body">
9 ${h.secure_form(h.route_path('my_account_update'), class_='form', request=request)}
10 <% readonly = None %>
9 <% readonly = None %>
11 <% disabled = "" %>
10 <% disabled = "" %>
12
11
13 % if c.extern_type != 'rhodecode':
12 %if c.extern_type != 'rhodecode':
14 <% readonly = "readonly" %>
13 <% readonly = "readonly" %>
15 <% disabled = "disabled" %>
14 <% disabled = "disabled" %>
16 <div class="infoform">
15 <div class="infoform">
@@ -24,7 +23,7 b''
24 <label for="username">${_('Username')}:</label>
23 <label for="username">${_('Username')}:</label>
25 </div>
24 </div>
26 <div class="input">
25 <div class="input">
27 ${h.text('username', class_='input-valuedisplay', readonly=readonly)}
26 ${c.user.username}
28 </div>
27 </div>
29 </div>
28 </div>
30
29
@@ -33,7 +32,7 b''
33 <label for="name">${_('First Name')}:</label>
32 <label for="name">${_('First Name')}:</label>
34 </div>
33 </div>
35 <div class="input">
34 <div class="input">
36 ${h.text('firstname', class_='input-valuedisplay', readonly=readonly)}
35 ${c.user.firstname}
37 </div>
36 </div>
38 </div>
37 </div>
39
38
@@ -42,7 +41,7 b''
42 <label for="lastname">${_('Last Name')}:</label>
41 <label for="lastname">${_('Last Name')}:</label>
43 </div>
42 </div>
44 <div class="input-valuedisplay">
43 <div class="input-valuedisplay">
45 ${h.text('lastname', class_='input-valuedisplay', readonly=readonly)}
44 ${c.user.lastname}
46 </div>
45 </div>
47 </div>
46 </div>
48 </div>
47 </div>
@@ -64,48 +63,7 b''
64 %endif
63 %endif
65 </div>
64 </div>
66 </div>
65 </div>
67 <div class="field">
66 ${c.form.render()| n}
68 <div class="label">
69 <label for="username">${_('Username')}:</label>
70 </div>
71 <div class="input">
72 ${h.text('username', class_='medium%s' % disabled, readonly=readonly)}
73 ${h.hidden('extern_name', c.extern_name)}
74 ${h.hidden('extern_type', c.extern_type)}
75 </div>
76 </div>
77 <div class="field">
78 <div class="label">
79 <label for="name">${_('First Name')}:</label>
80 </div>
81 <div class="input">
82 ${h.text('firstname', class_="medium")}
83 </div>
84 </div>
85
86 <div class="field">
87 <div class="label">
88 <label for="lastname">${_('Last Name')}:</label>
89 </div>
90 <div class="input">
91 ${h.text('lastname', class_="medium")}
92 </div>
93 </div>
94
95 <div class="field">
96 <div class="label">
97 <label for="email">${_('Email')}:</label>
98 </div>
99 <div class="input">
100 ## we should be able to edit email !
101 ${h.text('email', class_="medium")}
102 </div>
103 </div>
104
105 <div class="buttons">
106 ${h.submit('save', _('Save'), class_="btn")}
107 ${h.reset('reset', _('Reset'), class_="btn")}
108 </div>
109 </div>
67 </div>
110 </div>
68 </div>
111 % endif
69 % endif
@@ -30,7 +30,7 b' import shutil'
30 import configobj
30 import configobj
31
31
32 from rhodecode.tests import *
32 from rhodecode.tests import *
33 from rhodecode.model.db import Repository, User, RepoGroup, UserGroup, Gist
33 from rhodecode.model.db import Repository, User, RepoGroup, UserGroup, Gist, UserEmailMap
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35 from rhodecode.model.repo import RepoModel
35 from rhodecode.model.repo import RepoModel
36 from rhodecode.model.user import UserModel
36 from rhodecode.model.user import UserModel
@@ -275,6 +275,13 b' class Fixture(object):'
275 UserModel().delete(userid)
275 UserModel().delete(userid)
276 Session().commit()
276 Session().commit()
277
277
278 def create_additional_user_email(self, user, email):
279 uem = UserEmailMap()
280 uem.user = user
281 uem.email = email
282 Session().add(uem)
283 return uem
284
278 def destroy_users(self, userid_iter):
285 def destroy_users(self, userid_iter):
279 for user_id in userid_iter:
286 for user_id in userid_iter:
280 if User.get_by_username(user_id):
287 if User.get_by_username(user_id):
@@ -1178,6 +1178,10 b' class UserUtility(object):'
1178 self.user_ids.append(user.user_id)
1178 self.user_ids.append(user.user_id)
1179 return user
1179 return user
1180
1180
1181 def create_additional_user_email(self, user, email):
1182 uem = self.fixture.create_additional_user_email(user=user, email=email)
1183 return uem
1184
1181 def create_user_with_group(self):
1185 def create_user_with_group(self):
1182 user = self.create_user()
1186 user = self.create_user()
1183 user_group = self.create_user_group(members=[user])
1187 user_group = self.create_user_group(members=[user])
General Comments 0
You need to be logged in to leave comments. Login now