testAuthenticationPolicy.py 33.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2004, 2005, 2006 Nexedi SARL and Contributors.
# All Rights Reserved.
#          Ivan Tyagov <ivan@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################

31
from functools import partial
32
import unittest
33 34
import urllib
from StringIO import StringIO
35
import time
36
import httplib
37
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
38
from Products.Formulator.Errors import ValidationError
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
from Products.ERP5Type.Document import newTempBase

class TestAuthenticationPolicy(ERP5TypeTestCase):
  """
  Test for erp5_authentication_policy business template.
  """
  manager_username = 'zope'
  manager_password = 'zope'

  credential = '%s:%s' % (manager_username, manager_password)
  def getTitle(self):
    return "TestAuthenticationPolicy"

  def getBusinessTemplateList(self):
    """
    Return the list of required business templates.
    """
    return ('erp5_core_proxy_field_legacy',
            'erp5_base',
            'erp5_web',
            'erp5_credential',
60
            'erp5_system_event',
61 62 63 64 65 66 67
            'erp5_authentication_policy',)

  def afterSetUp(self):
    portal = self.getPortal()

    uf = portal.acl_users
    uf._doAddUser(self.manager_username, self.manager_password, ['Manager'], [])
68
    self.loginByUserName(self.manager_username)
69 70 71

    kw = dict(portal_type='ERP5 Login',
              reference='test')
72 73
    if portal.portal_catalog.getResultValue(**kw) is None:
      # add a loggable Person
74 75 76 77 78 79
      person = self.createUser(
        kw['reference'],
        password='test',
        person_kw={'first_name': 'First',
                   'last_name': 'Last'},
      )
80
      person.validate()
81 82
      assignment = person.newContent(portal_type = 'Assignment')
      assignment.open()
83

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
    # Reset and Setup auth policy
    old_preference = portal.portal_catalog.getResultValue(
      portal_type='System Preference',
      title='Authentication')
    if old_preference is not None:
      old_preference.setTitle('disabled authentication preference')
      old_preference.disable()

    preference = portal.portal_preferences.newContent(
                                            portal_type = 'System Preference',
                                            title = 'Authentication',
                                            preferred_max_authentication_failure = 3,
                                            preferred_authentication_failure_check_duration = 600,
                                            preferred_authentication_failure_block_duration = 600,
                                            preferred_authentication_policy_enabled = True)
    preference.enable()
    self.tic()
101 102

  def _clearCache(self):
103
    self.portal.portal_caches.clearCache(
104
      cache_factory_list=('erp5_content_short', # for authentication cache
105
                          ))
106

107
  def _getPasswordEventList(self, login):
108 109
    return [x.getObject() for x in self.portal.portal_catalog(
                                                 portal_type = 'Password Event',
110
                                                 default_destination_uid = login.getUid(),
111 112
                                                 sort_on = (('creation_date', 'DESC',),))]

113 114
  def _cleanUpLogin(self, login):
    self.portal.system_event_module.manage_delObjects([x.getId() for x in self._getPasswordEventList(login)])
115 116


117
  def createUser(self, username, password=None, person_kw=None):
118
    """
119 120 121 122 123
      Modified version from ERP5TypeTestCase, that does not set reference as
      password when password is None and does not set the same reference on
      person, so that we can reveal problems with code assuming that person's
      reference is same as login, which use to be true before ERP5 Login were
      introduced.
124 125 126
    """
    if person_kw is None:
      person_kw = {}
127 128 129
    self.createUser
    person = self.portal.person_module.newContent(
        portal_type='Person', **person_kw)
130
    login = person.newContent(portal_type='ERP5 Login',
131
                              reference=username,
132 133 134 135
                              password=password)
    login.validate()
    return person

136
  def test_BlockLogin(self):
137 138 139 140
    """
      Test that a recataloging works for Web Site documents
    """
    portal = self.getPortal()
141
    self.assertTrue(portal.portal_preferences.isAuthenticationPolicyEnabled())
142

143 144 145 146
    login = portal.portal_catalog.getResultValue(
        portal_type='ERP5 Login',
        reference='test')
    self.assertIsNotNone(login)
147
    preference = portal.portal_catalog.getResultValue(portal_type = 'System Preference',
148
                                                      title = 'Authentication',)
149
    # login should be allowed
150
    self.assertFalse(login.isLoginBlocked())
151

152
    # file some failures so we should detect and block account
153 154 155
    login.notifyLoginFailure()
    login.notifyLoginFailure()
    login.notifyLoginFailure()
156
    self.tic()
157

158
    # should be blocked
159
    self.assertTrue(login.isLoginBlocked())
160

161 162 163
    # set check back interval to actualy disable blocking
    preference.setPreferredAuthenticationFailureCheckDuration(0)
    self._clearCache()
164
    self.tic()
165
    time.sleep(1) # we need to give a moment
166
    self.assertFalse(login.isLoginBlocked())
167

168 169 170
    # .. and revert it back
    preference.setPreferredAuthenticationFailureCheckDuration(600)
    self._clearCache()
171
    self.tic()
172
    self.assertTrue(login.isLoginBlocked())
173

174 175 176
    # increase failures attempts
    preference.setPreferredMaxAuthenticationFailure(4)
    self._clearCache()
177
    self.tic()
178
    self.assertFalse(login.isLoginBlocked())
179

180 181 182
    # .. and revert it back
    preference.setPreferredMaxAuthenticationFailure(3)
    self._clearCache()
183
    self.tic()
184
    self.assertTrue(login.isLoginBlocked())
185

186 187 188
    # set short block interval so we can test it as well
    preference.setPreferredAuthenticationFailureBlockDuration(3)
    self._clearCache()
189
    self.tic()
190
    time.sleep(4)
191
    self.assertFalse(login.isLoginBlocked())
192

193 194
    # test multiple concurrent transactions without waiting for activities to be over
    preference.setPreferredAuthenticationFailureCheckDuration(600)
195 196
    preference.setPreferredAuthenticationFailureBlockDuration(600)
    preference.setPreferredMaxAuthenticationFailure(3)
197
    login.Login_unblockLogin()
198
    self._clearCache()
199
    self.tic()
200

201 202 203
    login.notifyLoginFailure()
    login.notifyLoginFailure()
    login.notifyLoginFailure()
204

205
    self.commit()
206
    self.assertTrue(login.isLoginBlocked())
207
    self.tic()
208
    self.assertTrue(login.isLoginBlocked())
209

210
    # test unblock account
211
    login.Login_unblockLogin()
212
    self.tic()
213
    self.assertFalse(login.isLoginBlocked())
214

215

216
  def test_PasswordHistory(self):
217 218 219 220
    """
      Test password history.
    """
    portal = self.getPortal()
221
    self.assertTrue(portal.portal_preferences.isAuthenticationPolicyEnabled())
222

223 224
    person = self.createUser('test-02')
    login = person.objectValues(portal_type='ERP5 Login')[0]
225 226
    preference = portal.portal_catalog.getResultValue(portal_type = 'System Preference',
                                                      title = 'Authentication',)
227
    self.tic()
228

229
    # Check that last (X where X is set in preferences) passwords are saved.
230
    self.assertEqual([], self._getPasswordEventList(login))
231
    preference.setPreferredNumberOfLastPasswordToCheck(10)
232
    self.tic()
233
    self._clearCache()
234

235
    login.setPassword('12345678')
236
    self.tic()
237

238
    # password change date should be saved as well hashed old password value
239 240
    old_password = login.getPassword()
    self.assertSameSet([old_password], [x.getPassword() for x in self._getPasswordEventList(login)])
241

242
    # .. test one more time to check history of password is saved in a list
243
    login.setPassword('123456789')
244
    self.tic()
245
    old_password1 = login.getPassword()
246

247
    # password change date should be saved as well hashed old password value
248
    self.assertSameSet([old_password1, old_password], [x.getPassword() for x in self._getPasswordEventList(login)])
249

250
    # other methods (_setPassword)...
251
    login._setPassword('123456789-1')
252
    self.tic()
253
    old_password2 = login.getPassword()
254
    self.assertSameSet([old_password2, old_password1, old_password], \
255
                     [x.getPassword() for x in self._getPasswordEventList(login)])
256

257
    # other methods (_forceSetPassword)...
258
    login._forceSetPassword('123456789-2')
259
    self.tic()
260
    old_password3 = login.getPassword()
261
    self.assertSameSet([old_password3, old_password2, old_password1, old_password], \
262
                     [x.getPassword() for x in self._getPasswordEventList(login)])
263

264 265

    # other methods (setEncodedPassword)...
266
    login.setEncodedPassword('123456789-3')
267
    self.tic()
268
    old_password4 = login.getPassword()
269
    self.assertSameSet([old_password4, old_password3, old_password2, old_password1, old_password], \
270
                     [x.getPassword() for x in self._getPasswordEventList(login)])
271 272

    # other methods (edit)...
273
    login.edit(password = '123456789-4')
274
    self.tic()
275
    old_password5 = login.getPassword()
276
    self.assertSameSet([old_password5, old_password4, old_password3, old_password2, old_password1, old_password], \
277
                     [x.getPassword() for x in self._getPasswordEventList(login)])
278

279

280
  def test_PasswordValidity(self):
281 282 283 284
    """
      Test validity of a password.
    """
    portal = self.getPortal()
285 286
    request = self.app.REQUEST

287 288 289 290 291
    regular_expression_list = ['([a-z]+)', # english lowercase
                               '([A-Z]+)', # english uppercase
                               '([0-9]+)', # numerals (0-9)
                               '([\\\\$\\\\!\\\\#\\\\%]+)' # (!, $, #, %)
                              ]
292

293
    self.assertTrue(portal.portal_preferences.isAuthenticationPolicyEnabled())
294

295 296 297 298 299 300 301
    person = self.createUser(
        'test-03',
        password='test',
        person_kw={'first_name': 'First',
                   'last_name': 'Last'},
    )
    login = person.objectValues(portal_type='ERP5 Login')[0]
302 303
    preference = portal.portal_catalog.getResultValue(portal_type = 'System Preference',
                                                      title = 'Authentication',)
304
    self.tic()
305

306
    # by default an empty password if nothing set in preferences is OK
307
    self.assertTrue(login.isPasswordValid(''))
308

309
    # Not long enough passwords used
310
    self._cleanUpLogin(login)
311
    preference.setPreferredMinPasswordLength(8)
312
    preference.setPreferredNumberOfLastPasswordToCheck(0)
313
    self.tic()
314
    self._clearCache()
315

316 317
    self.assertEqual(['Too short.'], [str(msg) for msg in login.analyzePassword('')])
    self.assertEqual(['Too short.'], [str(msg) for msg in login.analyzePassword('1234567')])
318
    self.assertTrue(login.isPasswordValid('12345678'))
319

320
    # not changed in last x days
321
    self._cleanUpLogin(login)
322 323
    preference.setPreferredMinPasswordLifetimeDuration(24)
    preference.setPreferredNumberOfLastPasswordToCheck(3)
324
    self.tic()
325
    self._clearCache()
326

327 328
    self.assertTrue(login.isPasswordValid('12345678'))
    login.setPassword('12345678')
329
    self.tic()
330

331
    # if we try to change now we should fail with any password
332 333 334 335 336 337
    self.assertEqual(
      ['You have changed your password too recently.'],
      [str(msg) for msg in login.analyzePassword('87654321')])
    self.assertSameSet(
      ['Too short.', 'You have changed your password too recently.'],
      [str(msg) for msg in login.analyzePassword('short')]) # multiple failures
338 339
    self.assertFalse(login.isPasswordValid('short')) # multiple failures
    self.assertRaises(ValueError, login.setPassword, '87654321')
340

341
    preference.setPreferredMinPasswordLifetimeDuration(0) # remove restriction
342
    self.tic()
343
    self._clearCache()
344
    self.assertTrue(login.isPasswordValid('87654321')) # it's OK to change
345

346 347
    # password not used in previous X passwords
    preference.setPreferredMinPasswordLength(None) # disable for now
348
    self._cleanUpLogin(login)
349
    self._clearCache()
350
    self.tic()
351

352
    login.setPassword('12345678-new')
353
    self.tic()
354

355 356 357 358
    # if we try to change now we should fail with this EXACT password
    self.assertEqual(
      ['You have already used this password.'],
      [str(msg) for msg in login.analyzePassword('12345678-new')])
359 360
    self.assertRaises(ValueError, login.setPassword, '12345678-new')
    self.assertTrue(login.isPasswordValid('12345678_')) # it's OK with another one not used yet
361
    for password in ['a','b','c','d', 'e', 'f']:
362 363 364 365 366
      # this sleep is not so beautiful, but mysql datetime columns has a
      # precision of one second only, and we use creation_date to order
      # "Password Event" objects. So without this sleep, the test is
      # failing randomly.
      time.sleep(1)
367
      login.setPassword(password)
368
      self.tic()
Ivan Tyagov's avatar
Ivan Tyagov committed
369
    self._clearCache()
370
    self.tic()
371

372 373 374 375
    self.assertTrue(login.isPasswordValid('12345678-new'))
    self.assertTrue(login.isPasswordValid('a'))
    self.assertTrue(login.isPasswordValid('b'))
    self.assertTrue(login.isPasswordValid('c'))
376
    # only last 3 (including current one are invalid)
377 378 379 380 381 382 383 384 385
    self.assertEqual(
      ['You have already used this password.'],
      [str(msg) for msg in login.analyzePassword('d')])
    self.assertEqual(
      ['You have already used this password.'],
      [str(msg) for msg in login.analyzePassword('e')])
    self.assertEqual(
      ['You have already used this password.'],
      [str(msg) for msg in login.analyzePassword('f')])
386

387 388
    # if we remove restricted then all password are usable
    preference.setPreferredNumberOfLastPasswordToCheck(None)
389
    self._clearCache()
390
    self.tic()
391

392 393 394
    self.assertTrue(login.isPasswordValid('d'))
    self.assertTrue(login.isPasswordValid('e'))
    self.assertTrue(login.isPasswordValid('f'))
395

396 397
    # if we set only last password to check
    preference.setPreferredNumberOfLastPasswordToCheck(1)
398
    self._clearCache()
399
    self.tic()
400 401 402
    self.assertTrue(login.isPasswordValid('c'))
    self.assertTrue(login.isPasswordValid('d'))
    self.assertTrue(login.isPasswordValid('e'))
403 404 405
    self.assertEqual(
      ['You have already used this password.'],
      [str(msg) for msg in login.analyzePassword('f')])
406

407 408 409
    preference.setPreferredRegularExpressionGroupList(regular_expression_list)
    preference.setPreferredMinPasswordLength(7)
    preference.setPreferredNumberOfLastPasswordToCheck(None)
410
    self._cleanUpLogin(login)
411
    self._clearCache()
412
    self.tic()
413

414 415 416 417 418 419 420
    four_group_password_list = ['abAB#12', 'ghTK61%', '5Tyui1%','Y22GJ5iu#' ]
    three_group_password_list = ['abAB123 ', 'AB123ab', 'XY123yz', 'dufgQ7xL', 'NAfft8h5', '0LcAiWtT']
    two_group_password_list = ['XY12345', 'yuiffg1', 'abcdef##', '##$aabce']
    one_group_password_list = ['1234567', 'ABZSDFE', '##!!$$%','abzdeffgg']

    # min 4 out of all groups
    preference.setPreferredMinRegularExpressionGroupNumber(4)
421
    self._clearCache()
422
    self.tic()
423
    for password in four_group_password_list:
424
      self.assertTrue(login.isPasswordValid(password))
425
    for password in three_group_password_list+two_group_password_list + one_group_password_list:
426 427 428
      self.assertEqual(
        ['Not complex enough.'],
        [str(msg) for msg in login.analyzePassword(password)])
429 430

    # min 3 out of all groups
431
    preference.setPreferredMinRegularExpressionGroupNumber(3)
432
    self._clearCache()
433
    self._cleanUpLogin(login)
434
    self.tic()
435
    for password in four_group_password_list + three_group_password_list:
436
      self.assertTrue(login.isPasswordValid(password))
437
    for password in two_group_password_list + one_group_password_list:
438 439 440
      self.assertEqual(
        ['Not complex enough.'],
        [str(msg) for msg in login.analyzePassword(password)])
441

442 443 444
    # min 2 out of all groups
    preference.setPreferredMinRegularExpressionGroupNumber(2)
    self._clearCache()
445
    self.tic()
446
    for password in four_group_password_list + three_group_password_list + two_group_password_list:
447
      self.assertTrue(login.isPasswordValid(password))
448
    for password in one_group_password_list:
449 450 451
      self.assertEqual(
        ['Not complex enough.'],
        [str(msg) for msg in login.analyzePassword(password)])
452

453 454 455
    # min 1 out of all groups
    preference.setPreferredMinRegularExpressionGroupNumber(1)
    self._clearCache()
456
    self.tic()
457
    for password in four_group_password_list + three_group_password_list + two_group_password_list+one_group_password_list:
458
      self.assertTrue(login.isPasswordValid(password))
459 460 461 462

    # not contain the full name of the user
    preference.setPrefferedForceUsernameCheckInPassword(1)
    self._clearCache()
463
    self.tic()
464 465 466 467 468 469
    self.assertEqual(
      ['You can not use any parts of your first and last name in password.'],
      [str(msg) for msg in person.Login_analyzePassword('abAB#12_%s' %person.getFirstName())])
    self.assertEqual(
      ['You can not use any parts of your first and last name in password.'],
      [str(msg) for msg in person.Login_analyzePassword('abAB#12_%s' %person.getLastName())])
470 471
    preference.setPrefferedForceUsernameCheckInPassword(0)
    self._clearCache()
472
    self.tic()
473 474
    self.assertTrue(login.isPasswordValid('abAB#12_%s' %person.getFirstName()))
    self.assertTrue(login.isPasswordValid('abAB#12_%s' %person.getLastName()))
475 476 477 478 479 480 481 482

    # check on temp objects just passworrd length( i.e. simulating a new user account creation)
    first_name = 'John'
    last_name = 'Doh'
    kw = {'title': '%s %s' %(first_name, last_name),
          'first_name': first_name,
          'last_name': last_name}
    temp_person = newTempBase(portal, kw['title'], **kw)
483

484 485
    preference.setPreferredMinPasswordLength(10)
    preference.setPreferredRegularExpressionGroupList(None)
486
    self._clearCache()
487
    self.tic()
488
    # in this case which is basically used in new account creation only length of password matters
489 490 491 492
    self.assertEqual(
      ['Too short.'],
      [str(msg) for msg in temp_person.Login_analyzePassword('onlyNine1')])
    self.assertEqual([], temp_person.Login_analyzePassword('longEnough1'))
493

494
    # make sure re check works on temp as well ( i.e. min 3 out of all groups)
495 496 497
    preference.setPreferredRegularExpressionGroupList(regular_expression_list)
    preference.setPreferredMinPasswordLength(7)
    preference.setPreferredMinRegularExpressionGroupNumber(3)
498
    self._clearCache()
499
    self.tic()
500
    for password in four_group_password_list + three_group_password_list:
501
      self.assertSameSet([], temp_person.Login_analyzePassword(password))
502
    for password in two_group_password_list + one_group_password_list:
503 504 505
      self.assertEqual(
        ['Not complex enough.'],
        [str(msg) for msg in temp_person.Login_analyzePassword(password)])
506

507 508 509
    # 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()
510
    self.tic()
511 512 513 514 515 516
    self.assertEqual(
      ['You can not use any parts of your first and last name in password.'],
      [str(msg) for msg in temp_person.Login_analyzePassword('abAB#12_%s' %first_name)])
    self.assertEqual(
      ['You can not use any parts of your first and last name in password.'],
      [str(msg) for msg in temp_person.Login_analyzePassword('abAB#12_%s' %last_name)])
517

518 519
    preference.setPrefferedForceUsernameCheckInPassword(0)
    self._clearCache()
520
    self.tic()
521 522
    self.assertEqual([], temp_person.Login_analyzePassword('abAB#12_%s' %first_name))
    self.assertEqual([], temp_person.Login_analyzePassword('abAB#12_%s' %last_name))
523

524 525 526 527 528 529 530
    # check Base_isPasswordValid is able to work in Anonymous User fashion
    # but with already create Person object (i.e. recover password case)
    preference.setPrefferedForceUsernameCheckInPassword(1)
    preference.setPreferredMinPasswordLength(7)
    preference.setPreferredMinRegularExpressionGroupNumber(3)
    preference.setPreferredNumberOfLastPasswordToCheck(1)
    self._clearCache()
531
    self.tic()
532

533
    login.setPassword('used_ALREADY_1234')
534
    self._clearCache()
535
    self.tic()
536

537
  def test_PasswordExpire(self):
538 539 540 541
    """
      Test password expire.
    """
    portal = self.getPortal()
542
    request = self.app.REQUEST
543

544
    self.assertTrue(portal.portal_preferences.isAuthenticationPolicyEnabled())
545 546 547
    preference = portal.portal_catalog.getResultValue(portal_type = 'System Preference',
                                                      title = 'Authentication',)
    preference.setPreferredMaxPasswordLifetimeDuration(24)
548
    self.tic()
549
    self._clearCache()
550 551 552 553 554

    person = self.createUser('test-04',
                             password='used_ALREADY_1234')
    login = person.objectValues(portal_type='ERP5 Login')[0]

555
    self.assertFalse(login.isPasswordExpired())
556
    self.assertFalse(request['is_user_account_password_expired'])
557

558 559 560 561 562
    # Check password expired
    preference.setPreferredMaxPasswordLifetimeDuration(0) # password expire immediatly (just to check isExpired)
    self.tic()
    self._clearCache()
    self.assertTrue(login.isPasswordExpired())
563

564
    # set longer password validity interval
565
    preference.setPreferredMaxPasswordLifetimeDuration(4*24) # password expire in 4 days
566
    self.tic()
567
    self._clearCache()
568
    self.assertFalse(login.isPasswordExpired())
569
    self.assertFalse(request['is_user_account_password_expired'])
570

571 572
    # test early warning password expire notification is detected
    preference.setPreferredPasswordLifetimeExpireWarningDuration(4*24) # password expire notification appear immediately
573
    self.tic()
574
    self._clearCache()
575
    self.assertFalse(login.isPasswordExpired())
576
    self.assertTrue(request['is_user_account_password_expired_expire_date'])
577

578 579
    # test early warning password expire notification is detected
    preference.setPreferredPasswordLifetimeExpireWarningDuration(4*24-24) # password expire notification appear 3 days befor time
580
    self.tic()
581
    self._clearCache()
582
    self.assertFalse(login.isPasswordExpired())
583
    self.assertFalse(request['is_user_account_password_expired_expire_date'])
584

585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600
  def test_SystemRecoverExpiredPassword(self):
    """
      Test automatic system recover password
    """
    portal = self.portal
    request = self.app.REQUEST

    self.assertTrue(portal.portal_preferences.isAuthenticationPolicyEnabled())
    preference = portal.portal_catalog.getResultValue(portal_type = 'System Preference',
                                                      title = 'Authentication',)
    preference.setPreferredMaxPasswordLifetimeDuration(0) # password expire immediatly
    preference.setPreferredSystemRecoverExpiredPassword(True)
    self._clearCache()
    self.tic()

    person = self.createUser(self.id(), password='password')
601 602
    person.setDefaultEmailCoordinateText('user@example.com')
    assignment = person.newContent(portal_type='Assignment')
603 604 605 606 607 608 609 610
    assignment.open()
    login = person.objectValues(portal_type='ERP5 Login')[0]

    self.tic()
    self._clearCache()
    time.sleep(1)
    self.assertTrue(login.isPasswordExpired())

611 612 613 614 615
    publish = partial(
      self.publish,
      portal.absolute_url_path() + '/view',
      basic=self.id() + ':password',
    )
616
    # User cannot login
617
    response = publish()
618 619 620 621
    self.assertTrue(response.getHeader("Location").endswith("login_form"))
    self.tic()

    # and a credential recovery is created automatically
622
    credential_recovery, = person.getDestinationDecisionRelatedValueList(
623 624 625
        portal_type='Credential Recovery')

    # trying to login again does not create a new credential recovery
626
    response = publish()
627
    self.assertTrue(response.getHeader("Location").endswith("login_form"))
628
    self.tic()
629
    credential_recovery, = person.getDestinationDecisionRelatedValueList(
630 631
        portal_type='Credential Recovery')

632 633 634 635 636 637
    credential_recovery.accept()
    self.tic()
    _, (to,), message = self.portal.MailHost._last_message
    self.assertEqual(to, 'user@example.com')
    self.assertIn('Password Recovery', message)

638
  def test_HttpRequest(self):
639 640 641 642 643
    """
      Check HTTP responses
    """
    portal = self.getPortal()
    request = self.app.REQUEST
644 645 646 647 648 649 650

    preference = portal.portal_catalog.getResultValue(portal_type = 'System Preference',
                                                      title = 'Authentication',)
    preference.setPreferredMaxPasswordLifetimeDuration(24)
    self._clearCache()
    self.tic()

651 652 653 654 655
    person = self.createUser('test-05')
    assignment = person.newContent(portal_type = 'Assignment')
    assignment.open()
    login = person.objectValues(portal_type='ERP5 Login')[0]
    login.setPassword('used_ALREADY_1234')
656
    self.tic()
657

658 659 660 661
    response = self.publish(
      portal.absolute_url_path() + '/view',
      basic='test-05:used_ALREADY_1234',
    )
662
    self.assertTrue('Welcome to ERP5' in response.getBody())
663
    self.assertFalse(login.isLoginBlocked())
664

665 666 667 668 669
    publish = partial(
      self.publish,
      portal.absolute_url_path() + '/view',
      basic='test-05:bad_test',
    )
670
    # fail request #1
671
    response = publish()
672
    self.assertTrue(response.getHeader("Location").endswith("login_form"))
673
    self.assertFalse(login.isLoginBlocked())
674

675
    # fail request #2
676
    response = publish()
677
    self.assertTrue(response.getHeader("Location").endswith("login_form"))
678
    self.assertFalse(login.isLoginBlocked())
679 680

    # fail request #3
681
    response = publish()
682
    self.assertTrue(response.getHeader("Location").endswith("login_form"))
683
    self.assertTrue(login.isLoginBlocked())
684

685
    self.tic()
686

687
    # test message that account is blocked
688
    self.assertTrue(login.isLoginBlocked())
689 690 691 692 693 694
    publish = partial(
      self.publish,
      portal.absolute_url_path() + '/logged_in',
      basic='test-05:used_ALREADY_1234',
    )
    response = publish()
695
    self.assertTrue(response.getHeader("Location").endswith("login_form?portal_status_message=Account is blocked."))
696

697
    # test expire password message, first unblock it
698
    login.Login_unblockLogin()
699
    preference.setPreferredMaxPasswordLifetimeDuration(0)
700
    self.tic()
701
    self._clearCache()
702
    response = publish()
703
    self.assertTrue(response.getHeader("Location").endswith("login_form?portal_status_message=Password is expired."))
704
    self.assertTrue(login.isPasswordExpired())
705

706 707
    # test we're redirected to update password due to soon expire
    preference.setPreferredMaxPasswordLifetimeDuration(24)
708
    preference.setPreferredPasswordLifetimeExpireWarningDuration(24)
709
    self.tic()
710
    self._clearCache()
711
    response = publish()
712

713 714
    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"))
715

716
    # test proper login
717
    preference.setPreferredPasswordLifetimeExpireWarningDuration(12)
718
    self.tic()
719
    self._clearCache()
720 721 722 723
    response = self.publish(
      portal.absolute_url_path() + '/view',
      basic='test-05:used_ALREADY_1234',
    )
724
    self.assertTrue('Welcome to ERP5' in response.getBody())
725

726
  def test_ExpireOldAuthenticationEventList(self):
727 728 729 730
    """
      Check that expiring old Authentication Event list works.
    """
    portal = self.getPortal()
731 732
    person = self.createUser('test-06')
    login = person.objectValues(portal_type='ERP5 Login')[0]
733 734 735
    preference = portal.portal_catalog.getResultValue(portal_type = 'System Preference',
                                                      title = 'Authentication',)
    # file some failures so we should detect and block account
736 737 738
    login.notifyLoginFailure()
    login.notifyLoginFailure()
    login.notifyLoginFailure()
739
    self.tic()
740 741

    # should be blocked
742
    self.assertTrue(login.isLoginBlocked())
743

744 745
    # set 0 check interval
    preference.setPreferredAuthenticationFailureCheckDuration(0)
746
    self.tic()
747
    self._clearCache()
748

749
    time.sleep(1) # we need to give a moment
750
    self.assertFalse(login.isLoginBlocked())
751

752 753
    # expire manually old
    portal.system_event_module.SystemEventModule_expireAuthenticationEventList()
754
    self.tic()
755

756
    self.assertEqual(3, len(portal.portal_catalog(portal_type ="Authentication Event",
757
                                                 default_destination_uid = login.getUid(),
758
                                                 validation_state = "expired")))
759

760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814
  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.'))

815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869
  def test_PreferenceTool_changePassword_checks_policy(self):
    person = self.createUser(self.id(), password='current')
    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',)
    preference.setPreferredMinPasswordLength(10)
    self._clearCache()
    self.tic()

    # too short password is refused
    ret = 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': 'short',
        'field_password_confirm': 'short',
      })),
      request_method="POST",
      handle_errors=False)
    self.assertEqual(httplib.OK, ret.getStatus())
    self.assertIn(
      '<span class="error">Too short.</span>',
      ret.getBody())

    # if for some reason, PreferenceTool_setNewPassword is called directly,
    # the password policy is also checked, so this cause an unhandled exception.
    self.login(person.getUserId())
    self.assertRaises(
      ValueError,
      self.portal.PreferenceTool_setNewPassword,
      current_password='current',
      new_password='short')

    # long enough password is accepted
    ret = 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': 'long_enough_password',
        'field_password_confirm': 'long_enough_password',
      })),
      request_method="POST",
      handle_errors=False)
    # When password reset is succesful, user is logged out
    self.assertEqual(httplib.FOUND, ret.getStatus())
870 871
    self.assertEqual(self.portal.portal_preferences.absolute_url(),
                     ret.getHeader("Location"))
872 873 874

    # password is changed on the login
    self.assertTrue(login.checkPassword('long_enough_password'))
875 876


877 878 879 880
def test_suite():
  suite = unittest.TestSuite()
  suite.addTest(unittest.makeSuite(TestAuthenticationPolicy))
  return suite