Commit a9f4cab4 authored by Jérome Perrin's avatar Jérome Perrin

PasswordTool: check new password comply with authentication policy

When reseting password through portal_password, we should check new
password comply with policy.

Because user is not logged in at this stage, we expose a new method
`PasswordTool.analyzePassword` that checks the password is valid for
this reset key.
parent 6da3c680
......@@ -229,6 +229,31 @@ class PasswordTool(BaseTool):
if date < current_date:
del password_request_dict[key]
security.declarePublic('analyzePassword')
def analyzePassword(self, password, password_key):
"""Analyze password validity in the context of the login.
Returns a list of messages as returned by IEncryptedPassword.analyzePassword
"""
portal = self.getPortalObject()
if not portal.portal_preferences.isAuthenticationPolicyEnabled():
return []
try:
register_user_login, expiration_date = self._password_request_dict[
password_key]
except KeyError:
return []
user_dict_list = portal.acl_users.searchUsers(
login=register_user_login,
exact_match=True,
)
if user_dict_list:
user_dict, = user_dict_list
login_dict, = user_dict['login_list']
login = portal.unrestrictedTraverse(login_dict['path'])
return login.analyzePassword(password)
return []
security.declarePublic('changeUserPassword')
def changeUserPassword(self, password, password_key, password_confirm=None,
user_login=None, REQUEST=None, **kw):
......@@ -271,6 +296,7 @@ class PasswordTool(BaseTool):
)
login_dict, = user_dict['login_list']
login = portal.unrestrictedTraverse(login_dict['path'])
login.checkPasswordValueAcceptable(password) # this will raise if password does not match policy
login._forceSetPassword(password)
login.reindexObject()
return redirect(REQUEST, site_url,
......
"""External validator for PasswordTool_viewResetPassword/your_password
Check that the provided password matchs with the confirmation
and if authentication policy is enabled, that the password match the policy.
"""
from Products.Formulator.Errors import ValidationError
password_confirm = request.get('field_password_confirm',
request.get('password_confirm'))
# password does not match confirmation, returns the default external validator message.
if password_confirm != editor:
return 0
password_key = request.get('field_your_password_key',
request.get('your_password_key'))
assert password_key
validation_message_list = context.analyzePassword(editor, password_key)
if validation_message_list:
message = u' '.join([str(x) for x in validation_message_list])
raise ValidationError('external_validator_failed', context, error_text=message)
return 1
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>editor, request</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>PasswordTool_validatePassword</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -217,9 +217,7 @@
</item>
<item>
<key> <string>external_validator</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
<value> <string></string> </value>
</item>
<item>
<key> <string>extra</string> </key>
......@@ -229,6 +227,10 @@
<key> <string>hidden</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>input_type</string> </key>
<value> <string>text</string> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
......@@ -259,17 +261,4 @@
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="Method" module="Products.Formulator.MethodField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>method_name</string> </key>
<value> <string>Base_validatePasswordsMatch</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -229,6 +229,10 @@
<key> <string>hidden</string> </key>
<value> <int>0</int> </value>
</item>
<item>
<key> <string>input_type</string> </key>
<value> <string>text</string> </value>
</item>
<item>
<key> <string>max_length</string> </key>
<value> <string></string> </value>
......@@ -267,7 +271,7 @@
<dictionary>
<item>
<key> <string>method_name</string> </key>
<value> <string>Base_validatePasswordsMatch</string> </value>
<value> <string>PasswordTool_validatePassword</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -742,6 +742,61 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
default_destination_uid = login.getUid(),
validation_state = "expired")))
def test_PasswordTool_resetPassword_checks_policy(self):
person = self.createUser(
self.id(),
password='current',
person_kw={'first_name': 'Alice'})
person.newContent(portal_type = 'Assignment').open()
login = person.objectValues(portal_type='ERP5 Login')[0]
preference = self.portal.portal_catalog.getResultValue(
portal_type='System Preference',
title='Authentication',)
# Here we activate the "password should contain usename" policy
# as a way to check that password reset checks are done in the
# context of the login
preference.setPrefferedForceUsernameCheckInPassword(1)
self._clearCache()
self.tic()
reset_key = self.portal.portal_password.getResetPasswordKey(user_login=self.id())
ret = self.publish(
'%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': 'alice',
'field_password_confirm': 'alice',
'field_your_password_key': reset_key,
})),
request_method="POST",
handle_errors=False)
self.assertEqual(httplib.OK, ret.getStatus())
self.assertIn(
'<span class="error">You can not use any parts of your '
'first and last name in password.</span>',
ret.getBody())
# now with a password complying to the policy
ret = self.publish(
'%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.assertTrue(ret.getHeader('Location').endswith(
'/login_form?portal_status_message=Password+changed.'))
def test_PreferenceTool_changePassword_checks_policy(self):
person = self.createUser(self.id(), password='current')
person.newContent(portal_type = 'Assignment').open()
......
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