Commit 28ef4724 authored by Jérome Perrin's avatar Jérome Perrin

authentication_policy: fix UnicodeDecodeError with invalid password messages

When new password does not match policy, in reset password and change
password dialogs, we used u' '.join([str(message) ...]) to join all
translated messages in a string, but this construct will decode the
str(message) to unicode using ascii, so it will fail when these messages
contain some multi bytes characters.

Extend test coverage to check that these dialogs uses translations and use
non ascii messages in the tests, to make sure we don't have regressions
with this issue.
parent 0aeea3c4
Pipeline #13043 failed with stage
in 0 seconds
...@@ -34,6 +34,7 @@ import urllib ...@@ -34,6 +34,7 @@ import urllib
from StringIO import StringIO from StringIO import StringIO
import time import time
import httplib import httplib
import mock
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.Document import newTempBase from Products.ERP5Type.Document import newTempBase
...@@ -793,39 +794,45 @@ class TestAuthenticationPolicy(ERP5TypeTestCase): ...@@ -793,39 +794,45 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
self.tic() self.tic()
reset_key = self.portal.portal_password.getResetPasswordKey(user_login=self.id()) reset_key = self.portal.portal_password.getResetPasswordKey(user_login=self.id())
ret = self.publish( def submit_reset_password_dialog(new_password):
'%s/portal_password' % self.portal.getPath(), return self.publish(
stdin=StringIO(urllib.urlencode({ '%s/portal_password' % self.portal.getPath(),
'Base_callDialogMethod:method': '', stdin=StringIO(urllib.urlencode({
'dialog_id': 'PasswordTool_viewResetPassword', 'Base_callDialogMethod:method': '',
'dialog_method': 'PasswordTool_changeUserPassword', 'dialog_id': 'PasswordTool_viewResetPassword',
'field_user_login': self.id(), 'dialog_method': 'PasswordTool_changeUserPassword',
'field_your_password': 'alice', 'field_user_login': self.id(),
'field_password_confirm': 'alice', 'field_your_password': new_password,
'field_your_password_key': reset_key, 'field_password_confirm': new_password,
})), 'field_your_password_key': reset_key,
request_method="POST", })),
handle_errors=False) request_method="POST",
handle_errors=False)
ret = submit_reset_password_dialog('alice')
self.assertEqual(httplib.OK, ret.getStatus()) self.assertEqual(httplib.OK, ret.getStatus())
self.assertIn( self.assertIn(
'<span class="error">You can not use any parts of your ' '<span class="error">You can not use any parts of your '
'first and last name in password.</span>', 'first and last name in password.</span>',
ret.getBody()) ret.getBody())
# the messages are translated
default_gettext = self.portal.Localizer.erp5_ui.gettext
def gettext(message, **kw):
if message == 'You can not use any parts of your first and last name in password.':
return u'Yöü can not ... translated'
assert message != u'Yöü can not ... translated'
return default_gettext(message, **kw)
with mock.patch.object(self.portal.Localizer.erp5_ui.__class__, 'gettext', side_effect=gettext):
ret = submit_reset_password_dialog('alice')
self.assertEqual(httplib.OK, ret.getStatus())
self.assertIn(
'<span class="error">Yöü can not ... translated</span>',
ret.getBody())
# now with a password complying to the policy # now with a password complying to the policy
ret = self.publish( ret = submit_reset_password_dialog('ok')
'%s/portal_password' % self.portal.getPath(),
stdin=StringIO(urllib.urlencode({
'Base_callDialogMethod:method': '',
'dialog_id': 'PasswordTool_viewResetPassword',
'dialog_method': 'PasswordTool_changeUserPassword',
'field_user_login': self.id(),
'field_your_password': 'ok',
'field_password_confirm': 'ok',
'field_your_password_key': reset_key,
})),
request_method="POST",
handle_errors=False)
self.assertEqual(httplib.FOUND, ret.getStatus()) self.assertEqual(httplib.FOUND, ret.getStatus())
self.assertTrue(ret.getHeader('Location').endswith( self.assertTrue(ret.getHeader('Location').endswith(
'/login_form?portal_status_message=Password+changed.')) '/login_form?portal_status_message=Password+changed.'))
...@@ -841,25 +848,43 @@ class TestAuthenticationPolicy(ERP5TypeTestCase): ...@@ -841,25 +848,43 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
self._clearCache() self._clearCache()
self.tic() self.tic()
def submit_change_password_dialog(new_password):
return self.publish(
'%s/portal_preferences' % self.portal.getPath(),
basic='%s:current' % self.id(),
stdin=StringIO(urllib.urlencode({
'Base_callDialogMethod:method': '',
'dialog_id': 'PreferenceTool_viewChangePasswordDialog',
'dialog_method': 'PreferenceTool_setNewPassword',
'field_your_current_password': 'current',
'field_your_new_password': new_password,
'field_password_confirm': new_password,
})),
request_method="POST",
handle_errors=False)
# too short password is refused # too short password is refused
ret = self.publish( ret = submit_change_password_dialog('short')
'%s/portal_preferences' % self.portal.getPath(),
basic='%s:current' % self.id(),
stdin=StringIO(urllib.urlencode({
'Base_callDialogMethod:method': '',
'dialog_id': 'PreferenceTool_viewChangePasswordDialog',
'dialog_method': 'PreferenceTool_setNewPassword',
'field_your_current_password': 'current',
'field_your_new_password': 'short',
'field_password_confirm': 'short',
})),
request_method="POST",
handle_errors=False)
self.assertEqual(httplib.OK, ret.getStatus()) self.assertEqual(httplib.OK, ret.getStatus())
self.assertIn( self.assertIn(
'<span class="error">Too short.</span>', '<span class="error">Too short.</span>',
ret.getBody()) ret.getBody())
# the messages are translated
default_gettext = self.portal.Localizer.erp5_ui.gettext
def gettext(message, **kw):
if message == 'Too short.':
return u'Töü short ... translated'
assert message != u'Töü short ... translated'
return default_gettext(message, **kw)
with mock.patch.object(self.portal.Localizer.erp5_ui.__class__, 'gettext', side_effect=gettext):
ret = submit_change_password_dialog('short')
self.assertEqual(httplib.OK, ret.getStatus())
self.assertIn(
'<span class="error">Töü short ... translated</span>',
ret.getBody())
# if for some reason, PreferenceTool_setNewPassword is called directly, # if for some reason, PreferenceTool_setNewPassword is called directly,
# the password policy is also checked, so this cause an unhandled exception. # the password policy is also checked, so this cause an unhandled exception.
self.login(person.getUserId()) self.login(person.getUserId())
...@@ -870,19 +895,7 @@ class TestAuthenticationPolicy(ERP5TypeTestCase): ...@@ -870,19 +895,7 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
new_password='short') new_password='short')
# long enough password is accepted # long enough password is accepted
ret = self.publish( ret = submit_change_password_dialog('long_enough_password')
'%s/portal_preferences' % self.portal.getPath(),
basic='%s:current' % self.id(),
stdin=StringIO(urllib.urlencode({
'Base_callDialogMethod:method': '',
'dialog_id': 'PreferenceTool_viewChangePasswordDialog',
'dialog_method': 'PreferenceTool_setNewPassword',
'field_your_current_password': 'current',
'field_your_new_password': 'long_enough_password',
'field_password_confirm': 'long_enough_password',
})),
request_method="POST",
handle_errors=False)
# When password reset is succesful, user is logged out # When password reset is succesful, user is logged out
self.assertEqual(httplib.FOUND, ret.getStatus()) self.assertEqual(httplib.FOUND, ret.getStatus())
self.assertEqual(self.portal.portal_preferences.absolute_url(), self.assertEqual(self.portal.portal_preferences.absolute_url(),
......
...@@ -16,7 +16,7 @@ login = getSecurityManager().getUser().getLoginValue() ...@@ -16,7 +16,7 @@ login = getSecurityManager().getUser().getLoginValue()
if login is not None: if login is not None:
validation_message_list = login.analyzePassword(editor) validation_message_list = login.analyzePassword(editor)
if validation_message_list: if validation_message_list:
message = u' '.join([str(x) for x in validation_message_list]) message = ' '.join([str(x) for x in validation_message_list])
raise ValidationError('external_validator_failed', context, error_text=message) raise ValidationError('external_validator_failed', context, error_text=message)
return 1 return 1
...@@ -21,7 +21,7 @@ assert password_key ...@@ -21,7 +21,7 @@ assert password_key
# duplicate code). # duplicate code).
validation_message_list = context.getPortalObject().portal_password.analyzePassword(editor, password_key) validation_message_list = context.getPortalObject().portal_password.analyzePassword(editor, password_key)
if validation_message_list: if validation_message_list:
message = u' '.join([str(x) for x in validation_message_list]) message = ' '.join([str(x) for x in validation_message_list])
raise ValidationError('external_validator_failed', context, error_text=message) raise ValidationError('external_validator_failed', context, error_text=message)
return 1 return 1
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment