Commit 7e9f03f3 authored by Ivan Tyagov's avatar Ivan Tyagov

Allow UI to show all password validation error messages by extending

API.
Adjust test accordingly.
parent 65610e6b
......@@ -59,6 +59,13 @@ class ILoginAccountProvider(Interface):
"""
Is password valid?
"""
def analyzePassword(password, **kw):
"""
Analyze password validity.
Return status code indicating if password is acceptable and if not status code
for reason for not being a valid one (i.e. too short, not complex, etc ...)
"""
def isPasswordAlreadyUsed(self, password):
"""
......
......@@ -82,10 +82,19 @@ class LoginAccountProviderMixin:
"""
Is password valid?
"""
method = self._getTypeBasedMethod('isPasswordValid')
if method is not None:
return method(password, **kw)
return True
result_code_list = self.analyzePassword(password, **kw)
if not len(result_code_list):
return True
return False
def analyzePassword(self, password, **kw):
"""
Analyze password validity.
Return status code indicating if password is acceptable and if not status code
for reason for not being a valid one (i.e. too short, not complex, etc ...)
"""
method = self._getTypeBasedMethod('analyzePassword')
return method(password, **kw)
security.declareProtected(Permissions.SetOwnPassword, 'isPasswordAlreadyUsed')
def isPasswordAlreadyUsed(self, password):
......@@ -93,9 +102,13 @@ class LoginAccountProviderMixin:
Return if password has already been used.
"""
preferred_number_of_last_password_to_check = self.portal_preferences.getPreferredNumberOfLastPasswordToCheck()
password_list = self.getLastChangedPasswordValueList()
password_list.reverse()
for encoded_password in password_list[:preferred_number_of_last_password_to_check]:
password_event_list = self.getPortalObject().portal_catalog(
portal_type = "Password Event",
default_destination_uid = self.getUid(),
sort_on = (('creation_date', 'DESC',),),
limit = preferred_number_of_last_password_to_check)
password_list = [x.getPassword() for x in password_event_list]
for encoded_password in password_list:
if pw_validate(encoded_password, password):
return True
return False
......@@ -30,6 +30,7 @@
import unittest
import time
import transaction
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.backportUnittest import expectedFailure
from Products.Formulator.Errors import ValidationError
......@@ -55,6 +56,7 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
'erp5_base',
'erp5_web',
'erp5_credential',
'erp5_system_event',
'erp5_authentication_policy',)
def afterSetUp(self):
......@@ -90,10 +92,14 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
for cache_factory in [x for x in self.portal.portal_caches.getCacheFactoryList() if x!="erp5_session_cache"]:
self.portal.portal_caches.clearCacheFactory(cache_factory)
def _getPasswordEventList(self, person):
return [x.getObject() for x in self.portal.portal_catalog(
portal_type = 'Password Event',
default_destination_uid = person.getUid(),
sort_on = (('creation_date', 'DESC',),))]
def _cleanUpPerson(self, person):
# remove all traces from password changes
person.setLastPasswordModificationDate(None)
person.setLastChangedPasswordValueList([])
self.portal.system_event_module.manage_delObjects([x.getId() for x in self._getPasswordEventList(person)])
def test_01_BlockLogin(self):
......@@ -108,24 +114,22 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
preference = portal.portal_catalog.getResultValue(portal_type = 'System Preference',
title = 'Authentication',)
# login should be allowed
self.assertEqual(1, len(person.notifyLoginFailure())) # just to init structure
self.assertFalse(person.isLoginBlocked())
# file two more failures so we should detect and block account
self.assertEqual(2, len(person.notifyLoginFailure()))
self.assertEqual(3, len(person.notifyLoginFailure()))
# we do not need to store more than max allowed failures so check it here
# this way a bot can not brute force us by filling up session storage backend
for i in range (0, 1000):
self.assertEqual(3, len(person.notifyLoginFailure()))
# file some failures so we should detect and block account
person.notifyLoginFailure()
person.notifyLoginFailure()
person.notifyLoginFailure()
self.stepTic()
# should be blocked
self.assertTrue(person.isLoginBlocked())
# set check back interval to actualy disable blocking
preference.setPreferredAuthenticationFailureCheckDuration(0)
self._clearCache()
self.stepTic()
time.sleep(1) # we need to give a moment
self.assertFalse(person.isLoginBlocked())
# .. and revert it back
......@@ -152,6 +156,29 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
self.stepTic()
time.sleep(4)
self.assertFalse(person.isLoginBlocked())
# test multiple concurrent transactions without waiting for activities to be over
preference.setPreferredAuthenticationFailureCheckDuration(600)
preference.setPreferredAuthenticationFailureBlockDuration(600)
preference.setPreferredMaxAuthenticationFailure(3)
person.Person_unblockLogin()
self._clearCache()
self.stepTic()
person.notifyLoginFailure()
person.notifyLoginFailure()
person.notifyLoginFailure()
transaction.commit()
self.assertTrue(person.isLoginBlocked())
self.stepTic()
self.assertTrue(person.isLoginBlocked())
# test unblock account
person.Person_unblockLogin()
self.stepTic()
self.assertFalse(person.isLoginBlocked())
def test_02_PasswordHistory(self):
"""
......@@ -160,70 +187,61 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
portal = self.getPortal()
self.assertTrue(portal.portal_preferences.isAuthenticationPolicyEnabled())
person = portal.portal_catalog.getResultValue(portal_type = 'Person',
reference = 'test')
person = portal.person_module.newContent(portal_type = 'Person',
reference = 'test-02')
preference = portal.portal_catalog.getResultValue(portal_type = 'System Preference',
title = 'Authentication',)
self.stepTic()
# Check that last (X where X is set in preferences) passwords are saved.
self.assertEqual(None, person.getLastPasswordModificationDate())
self.assertEqual([], person.getLastChangedPasswordValueList())
self.assertEqual([], self._getPasswordEventList(person))
preference.setPreferredNumberOfLastPasswordToCheck(10)
self.stepTic()
self._clearCache()
before = DateTime()
person.setPassword('12345678')
self.stepTic()
# password change date should be saved as well hashed old password value
old_password = person.getPassword()
self.assertTrue(person.getLastPasswordModificationDate() > before)
self.assertEqual([old_password], person.getLastChangedPasswordValueList())
self.assertSameSet([old_password], [x.getPassword() for x in self._getPasswordEventList(person)])
# .. test one more time to check history of password is saved in a list
before = DateTime()
person.setPassword('123456789')
self.stepTic()
old_password1 = person.getPassword()
# password change date should be saved as well hashed old password value
self.assertTrue(person.getLastPasswordModificationDate() > before)
self.assertEqual([old_password, old_password1], person.getLastChangedPasswordValueList())
self.assertSameSet([old_password1, old_password], [x.getPassword() for x in self._getPasswordEventList(person)])
# other methods (_setPassword)...
before = DateTime()
person._setPassword('123456789-1')
self.stepTic()
old_password2 = person.getPassword()
self.assertTrue(person.getLastPasswordModificationDate() > before)
self.assertEqual([old_password, old_password1, old_password2], person.getLastChangedPasswordValueList())
self.assertSameSet([old_password2, old_password1, old_password], \
[x.getPassword() for x in self._getPasswordEventList(person)])
# other methods (_forceSetPassword)...
before = DateTime()
person._forceSetPassword('123456789-2')
self.stepTic()
old_password3 = person.getPassword()
self.assertTrue(person.getLastPasswordModificationDate() > before)
self.assertEqual([old_password, old_password1, old_password2, old_password3], person.getLastChangedPasswordValueList())
self.assertSameSet([old_password3, old_password2, old_password1, old_password], \
[x.getPassword() for x in self._getPasswordEventList(person)])
# other methods (setEncodedPassword)...
before = DateTime()
person.setEncodedPassword('123456789-3')
self.stepTic()
old_password4 = person.getPassword()
self.assertTrue(person.getLastPasswordModificationDate() > before)
self.assertEqual([old_password, old_password1, old_password2, old_password3, old_password4], \
person.getLastChangedPasswordValueList())
self.assertSameSet([old_password4, old_password3, old_password2, old_password1, old_password], \
[x.getPassword() for x in self._getPasswordEventList(person)])
# other methods (edit)...
before = DateTime()
person.edit(password = '123456789-4')
self.stepTic()
old_password5 = person.getPassword()
self.assertTrue(person.getLastPasswordModificationDate() > before)
self.assertEqual([old_password, old_password1, old_password2, old_password3, old_password4, old_password5], \
person.getLastChangedPasswordValueList())
self.assertSameSet([old_password5, old_password4, old_password3, old_password2, old_password1, old_password], \
[x.getPassword() for x in self._getPasswordEventList(person)])
def test_03_PasswordValidity(self):
......@@ -240,12 +258,16 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
]
self.assertTrue(portal.portal_preferences.isAuthenticationPolicyEnabled())
person = portal.portal_catalog.getResultValue(portal_type = 'Person',
reference = 'test')
person = portal.person_module.newContent(portal_type = 'Person',
reference = 'test-03',
password = 'test',
first_name = 'First',
last_name = 'Last')
preference = portal.portal_catalog.getResultValue(portal_type = 'System Preference',
title = 'Authentication',)
self.stepTic()
# by default an empty password if nothing set in preferences is OK
self.assertTrue(person.isPasswordValid(''))
......@@ -256,8 +278,8 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
self.stepTic()
self._clearCache()
self.assertEqual(-1, person.isPasswordValid(''))
self.assertEqual(-1, person.isPasswordValid('1234567'))
self.assertEqual([-1], person.analyzePassword(''))
self.assertEqual([-1], person.analyzePassword('1234567'))
self.assertTrue(person.isPasswordValid('12345678'))
# not changed in last x days
......@@ -267,12 +289,14 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
self.stepTic()
self._clearCache()
self.assertEqual(1, person.isPasswordValid('12345678'))
self.assertTrue(person.isPasswordValid('12345678'))
person.setPassword('12345678')
self.stepTic()
# if we try to change now we should fail with any password
self.assertEqual(-3, person.isPasswordValid('87654321'))
self.assertSameSet([-3], person.analyzePassword('87654321'))
self.assertSameSet([-1, -3], person.analyzePassword('short')) # multiple failures
self.assertFalse(person.isPasswordValid('short')) # multiple failures
self.assertRaises(ValueError, person.setPassword, '87654321')
preference.setPreferredMinPasswordLifetimeDuration(0) # remove restriction
......@@ -289,38 +313,38 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
person.setPassword('12345678-new')
self.stepTic()
self.assertEqual(-4, person.isPasswordValid('12345678-new')) # if we try to change now we should fail with this EXACT password
self.assertSameSet([-4], person.analyzePassword('12345678-new')) # if we try to change now we should fail with this EXACT password
self.assertRaises(ValueError, person.setPassword, '12345678-new')
self.assertTrue(person.isPasswordValid('12345678_')) # it's OK with another one not used yet
for password in ['a','b','c','d', 'e', 'f']:
person.setPassword(password)
self.stepTic()
self.assertEqual(1, person.isPasswordValid('12345678-new'))
self.assertEqual(1, person.isPasswordValid('a'))
self.assertEqual(1, person.isPasswordValid('b'))
self.assertTrue(person.isPasswordValid('12345678-new'))
self.assertTrue(person.isPasswordValid('a'))
self.assertTrue(person.isPasswordValid('b'))
# only last 3 (including current one are invalid)
self.assertEqual(-4, person.isPasswordValid('d'))
self.assertEqual(-4, person.isPasswordValid('e'))
self.assertEqual(-4, person.isPasswordValid('f'))
self.assertSameSet([-4], person.analyzePassword('d'))
self.assertSameSet([-4], person.analyzePassword('e'))
self.assertSameSet([-4], person.analyzePassword('f'))
# if we remove restricted then all password are usable
preference.setPreferredNumberOfLastPasswordToCheck(None)
self._clearCache()
self.stepTic()
self.assertEqual(1, person.isPasswordValid('d'))
self.assertEqual(1, person.isPasswordValid('e'))
self.assertEqual(1, person.isPasswordValid('f'))
self.assertTrue(person.isPasswordValid('d'))
self.assertTrue(person.isPasswordValid('e'))
self.assertTrue(person.isPasswordValid('f'))
# if we set only last password to check
preference.setPreferredNumberOfLastPasswordToCheck(1)
self._clearCache()
self.stepTic()
self.assertEqual(1, person.isPasswordValid('c'))
self.assertEqual(1, person.isPasswordValid('d'))
self.assertEqual(1, person.isPasswordValid('e'))
self.assertEqual(-4, person.isPasswordValid('f'))
self.assertTrue(person.isPasswordValid('c'))
self.assertTrue(person.isPasswordValid('d'))
self.assertTrue(person.isPasswordValid('e'))
self.assertSameSet([-4], person.analyzePassword('f'))
preference.setPreferredRegularExpressionGroupList(regular_expression_list)
preference.setPreferredMinPasswordLength(7)
......@@ -341,7 +365,7 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
for password in four_group_password_list:
self.assertTrue(person.isPasswordValid(password))
for password in three_group_password_list+two_group_password_list + one_group_password_list:
self.assertEqual(-2, person.isPasswordValid(password))
self.assertSameSet([-2], person.analyzePassword(password))
# min 3 out of all groups
preference.setPreferredMinRegularExpressionGroupNumber(3)
......@@ -351,7 +375,7 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
for password in four_group_password_list + three_group_password_list:
self.assertTrue(person.isPasswordValid(password))
for password in two_group_password_list + one_group_password_list:
self.assertEqual(-2, person.isPasswordValid(password))
self.assertSameSet([-2], person.analyzePassword(password))
# min 2 out of all groups
preference.setPreferredMinRegularExpressionGroupNumber(2)
......@@ -360,7 +384,7 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
for password in four_group_password_list + three_group_password_list + two_group_password_list:
self.assertTrue(person.isPasswordValid(password))
for password in one_group_password_list:
self.assertEqual(-2, person.isPasswordValid(password))
self.assertSameSet([-2], person.analyzePassword(password))
# min 1 out of all groups
preference.setPreferredMinRegularExpressionGroupNumber(1)
......@@ -373,8 +397,8 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
preference.setPrefferedForceUsernameCheckInPassword(1)
self._clearCache()
self.stepTic()
self.assertEqual(-5, person.isPasswordValid('abAB#12_%s' %person.getFirstName()))
self.assertEqual(-5, person.isPasswordValid('abAB#12_%s' %person.getLastName()))
self.assertSameSet([-5], person.analyzePassword('abAB#12_%s' %person.getFirstName()))
self.assertSameSet([-5], person.analyzePassword('abAB#12_%s' %person.getLastName()))
preference.setPrefferedForceUsernameCheckInPassword(0)
self._clearCache()
self.stepTic()
......@@ -394,8 +418,8 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
self._clearCache()
self.stepTic()
# in this case which is basically used in new account creation only length of password matters
self.assertEqual(-1, temp_person.Person_isPasswordValid('onlyNine1'))
self.assertTrue(temp_person.Person_isPasswordValid('longEnough1'))
self.assertSameSet([-1], temp_person.Person_analyzePassword('onlyNine1'))
self.assertSameSet([], temp_person.Person_analyzePassword('longEnough1'))
# make sure re check works on temp as well ( i.e. min 3 out of all groups)
preference.setPreferredRegularExpressionGroupList(regular_expression_list)
......@@ -404,22 +428,22 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
self._clearCache()
self.stepTic()
for password in four_group_password_list + three_group_password_list:
self.assertTrue(temp_person.Person_isPasswordValid(password))
self.assertSameSet([], temp_person.Person_analyzePassword(password))
for password in two_group_password_list + one_group_password_list:
self.assertEqual(-2, temp_person.Person_isPasswordValid(password))
self.assertSameSet([-2], temp_person.Person_analyzePassword(password))
# make sure peron's check on username works on temp as well (i.e. not contain the full name of the user)
preference.setPrefferedForceUsernameCheckInPassword(1)
self._clearCache()
self.stepTic()
self.assertEqual(-5, temp_person.Person_isPasswordValid('abAB#12_%s' %first_name))
self.assertEqual(-5, temp_person.Person_isPasswordValid('abAB#12_%s' %last_name))
self.assertSameSet([-5], temp_person.Person_analyzePassword('abAB#12_%s' %first_name))
self.assertSameSet([-5], temp_person.Person_analyzePassword('abAB#12_%s' %last_name))
preference.setPrefferedForceUsernameCheckInPassword(0)
self._clearCache()
self.stepTic()
self.assertTrue(temp_person.Person_isPasswordValid('abAB#12_%s' %first_name))
self.assertTrue(temp_person.Person_isPasswordValid('abAB#12_%s' %last_name))
self.assertSameSet([], temp_person.Person_analyzePassword('abAB#12_%s' %first_name))
self.assertSameSet([], temp_person.Person_analyzePassword('abAB#12_%s' %last_name))
# check Base_isPasswordValid is able to work in Anonymous User fashion
# but with already create Person object (i.e. recover password case)
......@@ -454,8 +478,9 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
self.assertTrue(portal.portal_preferences.isAuthenticationPolicyEnabled())
person = portal.portal_catalog.getResultValue(portal_type = 'Person',
reference = 'test')
person = portal.person_module.newContent(portal_type = 'Person',
reference = 'test-04',
password = 'used_ALREADY_1234')
preference = portal.portal_catalog.getResultValue(portal_type = 'System Preference',
title = 'Authentication',)
......@@ -465,16 +490,8 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
self.assertFalse(person.isPasswordExpired())
self.assertFalse(request['is_user_account_password_expired'])
# set older last password modification date just for test
now = DateTime()
person.setLastPasswordModificationDate(now - 2)
self.stepTic()
self._clearCache()
self.assertTrue(person.isPasswordExpired())
self.assertTrue(request['is_user_account_password_expired'])
# set longer password validity interval
person.setLastPasswordModificationDate(now)
preference.setPreferredMaxPasswordLifetimeDuration(4*24) # password expire in 4 days
self.stepTic()
self._clearCache()
......@@ -486,17 +503,16 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
self.stepTic()
self._clearCache()
self.assertFalse(person.isPasswordExpired())
self.assertTrue(request['is_user_account_password_expired_warning_on'])
self.assertTrue(request['is_user_account_password_expired_expire_date'])
# test early warning password expire notification is detected
preference.setPreferredPasswordLifetimeExpireWarningDuration(4*24-24) # password expire notification appear 3 days befor time
self.stepTic()
self._clearCache()
self.assertFalse(person.isPasswordExpired())
#import pdb; pdb.set_trace()
self.assertFalse(request['is_user_account_password_expired_warning_on'])
self.assertFalse(request['is_user_account_password_expired_expire_date'])
def test_05_HttpResponse(self):
def test_05_HttpRequest(self):
"""
Check HTTP responses
"""
......@@ -504,11 +520,66 @@ class TestAuthenticationPolicy(ERP5TypeTestCase):
request = self.app.REQUEST
person = portal.portal_catalog.getResultValue(portal_type = 'Person',
reference = 'test')
# XXX: finish
path = portal.absolute_url_path() + '/view?__ac_name=%s&__ac_password=%s' %('test', 'test')
preference = portal.portal_catalog.getResultValue(portal_type = 'System Preference',
title = 'Authentication',)
person.setPassword('used_ALREADY_1234')
self.stepTic()
path = portal.absolute_url_path() + '/view?__ac_name=%s&__ac_password=%s' %('test', 'used_ALREADY_1234')
response = self.publish(path)
self.assertTrue('Welcome to ERP5' in response.getBody())
self.assertFalse(person.isLoginBlocked())
# fail request #1
path = portal.absolute_url_path() + '/view?__ac_name=%s&__ac_password=%s' %('test', 'bad_test')
response = self.publish(path)
print path
print response
self.assertTrue(response.getHeader("Location").endswith("login_form"))
self.assertFalse(person.isLoginBlocked())
# fail request #2
response = self.publish(path)
self.assertTrue(response.getHeader("Location").endswith("login_form"))
self.assertFalse(person.isLoginBlocked())
# fail request #3
response = self.publish(path)
self.assertTrue(response.getHeader("Location").endswith("login_form"))
self.assertTrue(person.isLoginBlocked())
self.stepTic()
# test message that account is blocked
self.assertTrue(person.isLoginBlocked())
path = portal.absolute_url_path() + '/logged_in?__ac_name=%s&__ac_password=%s' %('test', 'used_ALREADY_1234')
response = self.publish(path)
self.assertTrue(response.getHeader("Location").endswith("login_form?portal_status_message=Account is blocked."))
# test expire password message, first unblock it
person.Person_unblockLogin()
preference.setPreferredMaxPasswordLifetimeDuration(0)
self.stepTic()
self._clearCache()
response = self.publish(path)
self.assertTrue(response.getHeader("Location").endswith("login_form?portal_status_message=Password is expired."))
self.assertTrue(person.isPasswordExpired())
# test we're redirected to update password due to soon expire
preference.setPreferredMaxPasswordLifetimeDuration(24)
preference.setPreferredPasswordLifetimeExpireWarningDuration(24)
self.stepTic()
self._clearCache()
response = self.publish(path)
self.assertTrue('Your password will expire' in response.getHeader("Location"))
self.assertTrue('You are advised to change it as soon as possible' in response.getHeader("Location"))
# test proper login
preference.setPreferredPasswordLifetimeExpireWarningDuration(12)
self.stepTic()
self._clearCache()
path = portal.absolute_url_path() + '/view?__ac_name=%s&__ac_password=%s' %('test', 'used_ALREADY_1234')
response = self.publish(path)
self.assertTrue('Welcome to ERP5' in response.getBody())
def test_suite():
suite = unittest.TestSuite()
......
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