libndr: Avoid assigning duplicate versions to symbols
[amitay/samba.git] / source4 / dsdb / tests / python / password_lockout.py
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 # This tests the password lockout behavior for AD implementations
4 #
5 # Copyright Matthias Dieter Wallnoefer 2010
6 # Copyright Andrew Bartlett 2013
7 # Copyright Stefan Metzmacher 2014
8 #
9
10 from __future__ import print_function
11 import optparse
12 import sys
13 import base64
14 import time
15
16 sys.path.insert(0, "bin/python")
17 import samba
18
19 from samba.tests.subunitrun import TestProgram, SubunitOptions
20
21 import samba.getopt as options
22
23 from samba.auth import system_session
24 from samba.credentials import Credentials, DONT_USE_KERBEROS, MUST_USE_KERBEROS
25 from ldb import SCOPE_BASE, LdbError
26 from ldb import ERR_CONSTRAINT_VIOLATION
27 from ldb import ERR_INVALID_CREDENTIALS
28 from ldb import Message, MessageElement, Dn
29 from ldb import FLAG_MOD_REPLACE
30 from samba import gensec, dsdb
31 from samba.samdb import SamDB
32 import samba.tests
33 from samba.tests import delete_force
34 from samba.dcerpc import security, samr
35 from samba.ndr import ndr_unpack
36 from samba.tests.pso import PasswordSettings
37 from samba.net import Net
38 from samba import NTSTATUSError, ntstatus
39 import ctypes
40
41 parser = optparse.OptionParser("password_lockout.py [options] <host>")
42 sambaopts = options.SambaOptions(parser)
43 parser.add_option_group(sambaopts)
44 parser.add_option_group(options.VersionOptions(parser))
45 # use command line creds if available
46 credopts = options.CredentialsOptions(parser)
47 parser.add_option_group(credopts)
48 subunitopts = SubunitOptions(parser)
49 parser.add_option_group(subunitopts)
50 opts, args = parser.parse_args()
51
52 if len(args) < 1:
53     parser.print_usage()
54     sys.exit(1)
55
56 host = args[0]
57
58 lp = sambaopts.get_loadparm()
59 global_creds = credopts.get_credentials(lp)
60
61 import password_lockout_base
62
63 #
64 # Tests start here
65 #
66
67
68 class PasswordTests(password_lockout_base.BasePasswordTestCase):
69     def setUp(self):
70         self.host = host
71         self.host_url = host_url
72         self.lp = lp
73         self.global_creds = global_creds
74         self.ldb = SamDB(url=self.host_url, session_info=system_session(self.lp),
75                          credentials=self.global_creds, lp=self.lp)
76         super(PasswordTests, self).setUp()
77
78         self.lockout2krb5_creds = self.insta_creds(self.template_creds,
79                                                    username="lockout2krb5",
80                                                    userpass="thatsAcomplPASS0",
81                                                    kerberos_state=MUST_USE_KERBEROS)
82         self.lockout2krb5_ldb = self._readd_user(self.lockout2krb5_creds,
83                                                  lockOutObservationWindow=self.lockout_observation_window)
84
85         self.lockout2ntlm_creds = self.insta_creds(self.template_creds,
86                                                    username="lockout2ntlm",
87                                                    userpass="thatsAcomplPASS0",
88                                                    kerberos_state=DONT_USE_KERBEROS)
89         self.lockout2ntlm_ldb = self._readd_user(self.lockout2ntlm_creds,
90                                                  lockOutObservationWindow=self.lockout_observation_window)
91
92
93     def use_pso_lockout_settings(self, creds):
94
95         # create a PSO with the lockout settings the test cases normally expect
96         #
97         # Some test cases sleep() for self.account_lockout_duration
98         pso = PasswordSettings("lockout-PSO", self.ldb, lockout_attempts=3,
99                                lockout_duration=self.account_lockout_duration)
100         self.addCleanup(self.ldb.delete, pso.dn)
101
102         userdn = "cn=%s,cn=users,%s" % (creds.get_username(), self.base_dn)
103         pso.apply_to(userdn)
104
105         # update the global lockout settings to be wildly different to what
106         # the test cases normally expect
107         self.update_lockout_settings(threshold=10, duration=600,
108                                      observation_window=600)
109
110     def _reset_samr(self, res):
111
112         # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
113         samr_user = self._open_samr_user(res)
114         acb_info = self.samr.QueryUserInfo(samr_user, 16)
115         acb_info.acct_flags &= ~samr.ACB_AUTOLOCK
116         self.samr.SetUserInfo(samr_user, 16, acb_info)
117         self.samr.Close(samr_user)
118
119
120 class PasswordTestsWithoutSleep(PasswordTests):
121     def setUp(self):
122         # The tests in this class do not sleep, so we can have a
123         # longer window and not flap on slower hosts
124         self.account_lockout_duration = 30
125         self.lockout_observation_window = 30
126         super(PasswordTestsWithoutSleep, self).setUp()
127
128     def _reset_ldap_lockoutTime(self, res):
129         self.ldb.modify_ldif("""
130 dn: """ + str(res[0].dn) + """
131 changetype: modify
132 replace: lockoutTime
133 lockoutTime: 0
134 """)
135
136     def _reset_ldap_userAccountControl(self, res):
137         self.assertTrue("userAccountControl" in res[0])
138         self.assertTrue("msDS-User-Account-Control-Computed" in res[0])
139
140         uac = int(res[0]["userAccountControl"][0])
141         uacc = int(res[0]["msDS-User-Account-Control-Computed"][0])
142
143         uac |= uacc
144         uac = uac & ~dsdb.UF_LOCKOUT
145
146         self.ldb.modify_ldif("""
147 dn: """ + str(res[0].dn) + """
148 changetype: modify
149 replace: userAccountControl
150 userAccountControl: %d
151 """ % uac)
152
153     def _reset_by_method(self, res, method):
154         if method == "ldap_userAccountControl":
155             self._reset_ldap_userAccountControl(res)
156         elif method == "ldap_lockoutTime":
157             self._reset_ldap_lockoutTime(res)
158         elif method == "samr":
159             self._reset_samr(res)
160         else:
161             self.assertTrue(False, msg="Invalid reset method[%s]" % method)
162
163     def _test_userPassword_lockout_with_clear_change(self, creds, other_ldb, method,
164                                                      initial_lastlogon_relation=None):
165         """
166         Tests user lockout behaviour when we try to change the user's password
167         but specify an incorrect old-password. The method parameter specifies
168         how to reset the locked out account (e.g. by resetting lockoutTime)
169         """
170         # Notice: This works only against Windows if "dSHeuristics" has been set
171         # properly
172         username = creds.get_username()
173         userpass = creds.get_password()
174         userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
175
176         use_kerberos = creds.get_kerberos_state()
177         if use_kerberos == MUST_USE_KERBEROS:
178             logoncount_relation = 'greater'
179             lastlogon_relation = 'greater'
180             print("Performs a password cleartext change operation on 'userPassword' using Kerberos")
181         else:
182             logoncount_relation = 'equal'
183             lastlogon_relation = 'equal'
184             print("Performs a password cleartext change operation on 'userPassword' using NTLMSSP")
185
186         if initial_lastlogon_relation is not None:
187             lastlogon_relation = initial_lastlogon_relation
188
189         res = self._check_account(userdn,
190                                   badPwdCount=0,
191                                   badPasswordTime=("greater", 0),
192                                   logonCount=(logoncount_relation, 0),
193                                   lastLogon=(lastlogon_relation, 0),
194                                   lastLogonTimestamp=('greater', 0),
195                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
196                                   msDSUserAccountControlComputed=0)
197         badPasswordTime = int(res[0]["badPasswordTime"][0])
198         logonCount = int(res[0]["logonCount"][0])
199         lastLogon = int(res[0]["lastLogon"][0])
200         lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
201         if lastlogon_relation == 'greater':
202             self.assertGreater(lastLogon, badPasswordTime)
203             self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
204
205         # Change password on a connection as another user
206
207         # Wrong old password
208         try:
209             other_ldb.modify_ldif("""
210 dn: """ + userdn + """
211 changetype: modify
212 delete: userPassword
213 userPassword: thatsAcomplPASS1x
214 add: userPassword
215 userPassword: thatsAcomplPASS2
216 """)
217             self.fail()
218         except LdbError as e:
219             (num, msg) = e.args
220             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
221             self.assertTrue('00000056' in msg, msg)
222
223         res = self._check_account(userdn,
224                                   badPwdCount=1,
225                                   badPasswordTime=("greater", badPasswordTime),
226                                   logonCount=logonCount,
227                                   lastLogon=lastLogon,
228                                   lastLogonTimestamp=lastLogonTimestamp,
229                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
230                                   msDSUserAccountControlComputed=0)
231         badPasswordTime = int(res[0]["badPasswordTime"][0])
232
233         # Correct old password
234         other_ldb.modify_ldif("""
235 dn: """ + userdn + """
236 changetype: modify
237 delete: userPassword
238 userPassword: """ + userpass + """
239 add: userPassword
240 userPassword: thatsAcomplPASS2
241 """)
242
243         res = self._check_account(userdn,
244                                   badPwdCount=1,
245                                   badPasswordTime=badPasswordTime,
246                                   logonCount=logonCount,
247                                   lastLogon=lastLogon,
248                                   lastLogonTimestamp=lastLogonTimestamp,
249                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
250                                   msDSUserAccountControlComputed=0)
251
252         # Wrong old password
253         try:
254             other_ldb.modify_ldif("""
255 dn: """ + userdn + """
256 changetype: modify
257 delete: userPassword
258 userPassword: thatsAcomplPASS1x
259 add: userPassword
260 userPassword: thatsAcomplPASS2
261 """)
262             self.fail()
263         except LdbError as e1:
264             (num, msg) = e1.args
265             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
266             self.assertTrue('00000056' in msg, msg)
267
268         res = self._check_account(userdn,
269                                   badPwdCount=2,
270                                   badPasswordTime=("greater", badPasswordTime),
271                                   logonCount=logonCount,
272                                   lastLogon=lastLogon,
273                                   lastLogonTimestamp=lastLogonTimestamp,
274                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
275                                   msDSUserAccountControlComputed=0)
276         badPasswordTime = int(res[0]["badPasswordTime"][0])
277
278         print("two failed password change")
279
280         # Wrong old password
281         try:
282             other_ldb.modify_ldif("""
283 dn: """ + userdn + """
284 changetype: modify
285 delete: userPassword
286 userPassword: thatsAcomplPASS1x
287 add: userPassword
288 userPassword: thatsAcomplPASS2
289 """)
290             self.fail()
291         except LdbError as e2:
292             (num, msg) = e2.args
293             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
294             self.assertTrue('00000056' in msg, msg)
295
296         res = self._check_account(userdn,
297                                   badPwdCount=3,
298                                   badPasswordTime=("greater", badPasswordTime),
299                                   logonCount=logonCount,
300                                   lastLogon=lastLogon,
301                                   lastLogonTimestamp=lastLogonTimestamp,
302                                   lockoutTime=("greater", badPasswordTime),
303                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
304                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
305         badPasswordTime = int(res[0]["badPasswordTime"][0])
306         lockoutTime = int(res[0]["lockoutTime"][0])
307
308         # Wrong old password
309         try:
310             other_ldb.modify_ldif("""
311 dn: """ + userdn + """
312 changetype: modify
313 delete: userPassword
314 userPassword: thatsAcomplPASS1x
315 add: userPassword
316 userPassword: thatsAcomplPASS2
317 """)
318             self.fail()
319         except LdbError as e3:
320             (num, msg) = e3.args
321             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
322             self.assertTrue('00000775' in msg, msg)
323
324         res = self._check_account(userdn,
325                                   badPwdCount=3,
326                                   badPasswordTime=badPasswordTime,
327                                   logonCount=logonCount,
328                                   lastLogon=lastLogon,
329                                   lastLogonTimestamp=lastLogonTimestamp,
330                                   lockoutTime=lockoutTime,
331                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
332                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
333
334         # Wrong old password
335         try:
336             other_ldb.modify_ldif("""
337 dn: """ + userdn + """
338 changetype: modify
339 delete: userPassword
340 userPassword: thatsAcomplPASS1x
341 add: userPassword
342 userPassword: thatsAcomplPASS2
343 """)
344             self.fail()
345         except LdbError as e4:
346             (num, msg) = e4.args
347             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
348             self.assertTrue('00000775' in msg, msg)
349
350         res = self._check_account(userdn,
351                                   badPwdCount=3,
352                                   badPasswordTime=badPasswordTime,
353                                   logonCount=logonCount,
354                                   lockoutTime=lockoutTime,
355                                   lastLogon=lastLogon,
356                                   lastLogonTimestamp=lastLogonTimestamp,
357                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
358                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
359
360         try:
361             # Correct old password
362             other_ldb.modify_ldif("""
363 dn: """ + userdn + """
364 changetype: modify
365 delete: userPassword
366 userPassword: thatsAcomplPASS2
367 add: userPassword
368 userPassword: thatsAcomplPASS2x
369 """)
370             self.fail()
371         except LdbError as e5:
372             (num, msg) = e5.args
373             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
374             self.assertTrue('00000775' in msg, msg)
375
376         res = self._check_account(userdn,
377                                   badPwdCount=3,
378                                   badPasswordTime=badPasswordTime,
379                                   logonCount=logonCount,
380                                   lastLogon=lastLogon,
381                                   lastLogonTimestamp=lastLogonTimestamp,
382                                   lockoutTime=lockoutTime,
383                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
384                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
385
386         # Now reset the password, which does NOT change the lockout!
387         self.ldb.modify_ldif("""
388 dn: """ + userdn + """
389 changetype: modify
390 replace: userPassword
391 userPassword: thatsAcomplPASS2
392 """)
393
394         res = self._check_account(userdn,
395                                   badPwdCount=3,
396                                   badPasswordTime=badPasswordTime,
397                                   logonCount=logonCount,
398                                   lastLogon=lastLogon,
399                                   lastLogonTimestamp=lastLogonTimestamp,
400                                   lockoutTime=lockoutTime,
401                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
402                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
403
404         try:
405             # Correct old password
406             other_ldb.modify_ldif("""
407 dn: """ + userdn + """
408 changetype: modify
409 delete: userPassword
410 userPassword: thatsAcomplPASS2
411 add: userPassword
412 userPassword: thatsAcomplPASS2x
413 """)
414             self.fail()
415         except LdbError as e6:
416             (num, msg) = e6.args
417             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
418             self.assertTrue('00000775' in msg, msg)
419
420         res = self._check_account(userdn,
421                                   badPwdCount=3,
422                                   badPasswordTime=badPasswordTime,
423                                   logonCount=logonCount,
424                                   lastLogon=lastLogon,
425                                   lastLogonTimestamp=lastLogonTimestamp,
426                                   lockoutTime=lockoutTime,
427                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
428                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
429
430         m = Message()
431         m.dn = Dn(self.ldb, userdn)
432         m["userAccountControl"] = MessageElement(
433             str(dsdb.UF_LOCKOUT),
434           FLAG_MOD_REPLACE, "userAccountControl")
435
436         self.ldb.modify(m)
437
438         # This shows that setting the UF_LOCKOUT flag alone makes no difference
439         res = self._check_account(userdn,
440                                   badPwdCount=3,
441                                   badPasswordTime=badPasswordTime,
442                                   logonCount=logonCount,
443                                   lastLogon=lastLogon,
444                                   lastLogonTimestamp=lastLogonTimestamp,
445                                   lockoutTime=lockoutTime,
446                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
447                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
448
449         # This shows that setting the UF_LOCKOUT flag makes no difference
450         try:
451             # Correct old password
452             other_ldb.modify_ldif("""
453 dn: """ + userdn + """
454 changetype: modify
455 delete: unicodePwd
456 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
457 add: unicodePwd
458 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2x\"".encode('utf-16-le')).decode('utf8') + """
459 """)
460             self.fail()
461         except LdbError as e7:
462             (num, msg) = e7.args
463             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
464             self.assertTrue('00000775' in msg, msg)
465
466         res = self._check_account(userdn,
467                                   badPwdCount=3,
468                                   badPasswordTime=badPasswordTime,
469                                   logonCount=logonCount,
470                                   lockoutTime=lockoutTime,
471                                   lastLogon=lastLogon,
472                                   lastLogonTimestamp=lastLogonTimestamp,
473                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
474                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
475
476         self._reset_by_method(res, method)
477
478         # Here bad password counts are reset without logon success.
479         res = self._check_account(userdn,
480                                   badPwdCount=0,
481                                   badPasswordTime=badPasswordTime,
482                                   logonCount=logonCount,
483                                   lockoutTime=0,
484                                   lastLogon=lastLogon,
485                                   lastLogonTimestamp=lastLogonTimestamp,
486                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
487                                   msDSUserAccountControlComputed=0)
488
489         # The correct password after doing the unlock
490
491         other_ldb.modify_ldif("""
492 dn: """ + userdn + """
493 changetype: modify
494 delete: unicodePwd
495 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
496 add: unicodePwd
497 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2x\"".encode('utf-16-le')).decode('utf8') + """
498 """)
499         userpass = "thatsAcomplPASS2x"
500         creds.set_password(userpass)
501
502         res = self._check_account(userdn,
503                                   badPwdCount=0,
504                                   badPasswordTime=badPasswordTime,
505                                   logonCount=logonCount,
506                                   lockoutTime=0,
507                                   lastLogon=lastLogon,
508                                   lastLogonTimestamp=lastLogonTimestamp,
509                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
510                                   msDSUserAccountControlComputed=0)
511
512         # Wrong old password
513         try:
514             other_ldb.modify_ldif("""
515 dn: """ + userdn + """
516 changetype: modify
517 delete: userPassword
518 userPassword: thatsAcomplPASS1xyz
519 add: userPassword
520 userPassword: thatsAcomplPASS2XYZ
521 """)
522             self.fail()
523         except LdbError as e8:
524             (num, msg) = e8.args
525             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
526             self.assertTrue('00000056' in msg, msg)
527
528         res = self._check_account(userdn,
529                                   badPwdCount=1,
530                                   badPasswordTime=("greater", badPasswordTime),
531                                   logonCount=logonCount,
532                                   lockoutTime=0,
533                                   lastLogon=lastLogon,
534                                   lastLogonTimestamp=lastLogonTimestamp,
535                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
536                                   msDSUserAccountControlComputed=0)
537         badPasswordTime = int(res[0]["badPasswordTime"][0])
538
539         # Wrong old password
540         try:
541             other_ldb.modify_ldif("""
542 dn: """ + userdn + """
543 changetype: modify
544 delete: userPassword
545 userPassword: thatsAcomplPASS1xyz
546 add: userPassword
547 userPassword: thatsAcomplPASS2XYZ
548 """)
549             self.fail()
550         except LdbError as e9:
551             (num, msg) = e9.args
552             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
553             self.assertTrue('00000056' in msg, msg)
554
555         res = self._check_account(userdn,
556                                   badPwdCount=2,
557                                   badPasswordTime=("greater", badPasswordTime),
558                                   logonCount=logonCount,
559                                   lockoutTime=0,
560                                   lastLogon=lastLogon,
561                                   lastLogonTimestamp=lastLogonTimestamp,
562                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
563                                   msDSUserAccountControlComputed=0)
564         badPasswordTime = int(res[0]["badPasswordTime"][0])
565
566         self._reset_ldap_lockoutTime(res)
567
568         res = self._check_account(userdn,
569                                   badPwdCount=0,
570                                   badPasswordTime=badPasswordTime,
571                                   logonCount=logonCount,
572                                   lastLogon=lastLogon,
573                                   lastLogonTimestamp=lastLogonTimestamp,
574                                   lockoutTime=0,
575                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
576                                   msDSUserAccountControlComputed=0)
577
578     # The following test lockout behaviour when modifying a user's password
579     # and specifying an invalid old password. There are variants for both
580     # NTLM and kerberos user authentication. As well as that, there are 3 ways
581     # to reset the locked out account: by clearing the lockout bit for
582     # userAccountControl (via LDAP), resetting it via SAMR, and by resetting
583     # the lockoutTime.
584     def test_userPassword_lockout_with_clear_change_krb5_ldap_userAccountControl(self):
585         self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
586                                                           self.lockout2krb5_ldb,
587                                                           "ldap_userAccountControl")
588
589     def test_userPassword_lockout_with_clear_change_krb5_ldap_lockoutTime(self):
590         self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
591                                                           self.lockout2krb5_ldb,
592                                                           "ldap_lockoutTime")
593
594     def test_userPassword_lockout_with_clear_change_krb5_samr(self):
595         self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
596                                                           self.lockout2krb5_ldb,
597                                                           "samr")
598
599     def test_userPassword_lockout_with_clear_change_ntlm_ldap_userAccountControl(self):
600         self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
601                                                           self.lockout2ntlm_ldb,
602                                                           "ldap_userAccountControl",
603                                                           initial_lastlogon_relation='greater')
604
605     def test_userPassword_lockout_with_clear_change_ntlm_ldap_lockoutTime(self):
606         self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
607                                                           self.lockout2ntlm_ldb,
608                                                           "ldap_lockoutTime",
609                                                           initial_lastlogon_relation='greater')
610
611     def test_userPassword_lockout_with_clear_change_ntlm_samr(self):
612         self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
613                                                           self.lockout2ntlm_ldb,
614                                                           "samr",
615                                                           initial_lastlogon_relation='greater')
616
617     # For PSOs, just test a selection of the above combinations
618     def test_pso_userPassword_lockout_with_clear_change_krb5_ldap_userAccountControl(self):
619         self.use_pso_lockout_settings(self.lockout1krb5_creds)
620         self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
621                                                           self.lockout2krb5_ldb,
622                                                           "ldap_userAccountControl")
623
624     def test_pso_userPassword_lockout_with_clear_change_ntlm_ldap_lockoutTime(self):
625         self.use_pso_lockout_settings(self.lockout1ntlm_creds)
626         self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
627                                                           self.lockout2ntlm_ldb,
628                                                           "ldap_lockoutTime",
629                                                           initial_lastlogon_relation='greater')
630
631     def test_pso_userPassword_lockout_with_clear_change_ntlm_samr(self):
632         self.use_pso_lockout_settings(self.lockout1ntlm_creds)
633         self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
634                                                           self.lockout2ntlm_ldb,
635                                                           "samr",
636                                                           initial_lastlogon_relation='greater')
637
638     def test_multiple_logon_krb5(self):
639         self._test_multiple_logon(self.lockout1krb5_creds)
640
641     def test_multiple_logon_ntlm(self):
642         self._test_multiple_logon(self.lockout1ntlm_creds)
643
644     def _test_samr_password_change(self, creds, other_creds, lockout_threshold=3):
645         """Tests user lockout by using bad password in SAMR password_change"""
646
647         # create a connection for SAMR using another user's credentials
648         lp = self.get_loadparm()
649         net = Net(other_creds, lp, server=self.host)
650
651         # work out the initial account values for this user
652         username = creds.get_username()
653         userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
654         res = self._check_account(userdn,
655                                   badPwdCount=0,
656                                   badPasswordTime=("greater", 0),
657                                   badPwdCountOnly=True)
658         badPasswordTime = int(res[0]["badPasswordTime"][0])
659         logonCount = int(res[0]["logonCount"][0])
660         lastLogon = int(res[0]["lastLogon"][0])
661         lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
662
663         # prove we can change the user password (using the correct password)
664         new_password = "thatsAcomplPASS2"
665         net.change_password(newpassword=new_password,
666                             username=username,
667                             oldpassword=creds.get_password())
668         creds.set_password(new_password)
669
670         # try entering 'x' many bad passwords in a row to lock the user out
671         new_password = "thatsAcomplPASS3"
672         for i in range(lockout_threshold):
673             badPwdCount = i + 1
674             try:
675                 print("Trying bad password, attempt #%u" % badPwdCount)
676                 net.change_password(newpassword=new_password,
677                                     username=creds.get_username(),
678                                     oldpassword="bad-password")
679                 self.fail("Invalid SAMR change_password accepted")
680             except NTSTATUSError as e:
681                 enum = ctypes.c_uint32(e.args[0]).value
682                 self.assertEqual(enum, ntstatus.NT_STATUS_WRONG_PASSWORD)
683
684             # check the status of the account is updated after each bad attempt
685             account_flags = 0
686             lockoutTime = None
687             if badPwdCount >= lockout_threshold:
688                 account_flags = dsdb.UF_LOCKOUT
689                 lockoutTime = ("greater", badPasswordTime)
690
691             res = self._check_account(userdn,
692                                       badPwdCount=badPwdCount,
693                                       badPasswordTime=("greater", badPasswordTime),
694                                       logonCount=logonCount,
695                                       lastLogon=lastLogon,
696                                       lastLogonTimestamp=lastLogonTimestamp,
697                                       lockoutTime=lockoutTime,
698                                       userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
699                                       msDSUserAccountControlComputed=account_flags)
700             badPasswordTime = int(res[0]["badPasswordTime"][0])
701
702         # the user is now locked out
703         lockoutTime = int(res[0]["lockoutTime"][0])
704
705         # check the user remains locked out regardless of whether they use a
706         # good or a bad password now
707         for password in (creds.get_password(), "bad-password"):
708             try:
709                 print("Trying password %s" % password)
710                 net.change_password(newpassword=new_password,
711                                     username=creds.get_username(),
712                                     oldpassword=password)
713                 self.fail("Invalid SAMR change_password accepted")
714             except NTSTATUSError as e:
715                 enum = ctypes.c_uint32(e.args[0]).value
716                 self.assertEqual(enum, ntstatus.NT_STATUS_ACCOUNT_LOCKED_OUT)
717
718             res = self._check_account(userdn,
719                                       badPwdCount=lockout_threshold,
720                                       badPasswordTime=badPasswordTime,
721                                       logonCount=logonCount,
722                                       lastLogon=lastLogon,
723                                       lastLogonTimestamp=lastLogonTimestamp,
724                                       lockoutTime=lockoutTime,
725                                       userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
726                                       msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
727
728         # reset the user account lockout
729         self._reset_samr(res)
730
731         # check bad password counts are reset
732         res = self._check_account(userdn,
733                                   badPwdCount=0,
734                                   badPasswordTime=badPasswordTime,
735                                   logonCount=logonCount,
736                                   lockoutTime=0,
737                                   lastLogon=lastLogon,
738                                   lastLogonTimestamp=lastLogonTimestamp,
739                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
740                                   msDSUserAccountControlComputed=0)
741
742         # check we can change the user password successfully now
743         net.change_password(newpassword=new_password,
744                             username=username,
745                             oldpassword=creds.get_password())
746         creds.set_password(new_password)
747
748     def test_samr_change_password(self):
749         self._test_samr_password_change(self.lockout1ntlm_creds,
750                                         other_creds=self.lockout2ntlm_creds)
751
752     # same as above, but use a PSO to enforce the lockout
753     def test_pso_samr_change_password(self):
754         self.use_pso_lockout_settings(self.lockout1ntlm_creds)
755         self._test_samr_password_change(self.lockout1ntlm_creds,
756                                         other_creds=self.lockout2ntlm_creds)
757
758
759 class PasswordTestsWithSleep(PasswordTests):
760     def setUp(self):
761         super(PasswordTestsWithSleep, self).setUp()
762
763     def _test_unicodePwd_lockout_with_clear_change(self, creds, other_ldb,
764                                                    initial_logoncount_relation=None):
765         print("Performs a password cleartext change operation on 'unicodePwd'")
766         username = creds.get_username()
767         userpass = creds.get_password()
768         userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
769         if initial_logoncount_relation is not None:
770             logoncount_relation = initial_logoncount_relation
771         else:
772             logoncount_relation = "greater"
773
774         res = self._check_account(userdn,
775                                   badPwdCount=0,
776                                   badPasswordTime=("greater", 0),
777                                   logonCount=(logoncount_relation, 0),
778                                   lastLogon=("greater", 0),
779                                   lastLogonTimestamp=("greater", 0),
780                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
781                                   msDSUserAccountControlComputed=0)
782         badPasswordTime = int(res[0]["badPasswordTime"][0])
783         logonCount = int(res[0]["logonCount"][0])
784         lastLogon = int(res[0]["lastLogon"][0])
785         lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
786         self.assertGreater(lastLogonTimestamp, badPasswordTime)
787         self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
788
789         # Change password on a connection as another user
790
791         # Wrong old password
792         try:
793             other_ldb.modify_ldif("""
794 dn: """ + userdn + """
795 changetype: modify
796 delete: unicodePwd
797 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1x\"".encode('utf-16-le')).decode('utf8') + """
798 add: unicodePwd
799 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
800 """)
801             self.fail()
802         except LdbError as e10:
803             (num, msg) = e10.args
804             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
805             self.assertTrue('00000056' in msg, msg)
806
807         res = self._check_account(userdn,
808                                   badPwdCount=1,
809                                   badPasswordTime=("greater", badPasswordTime),
810                                   logonCount=logonCount,
811                                   lastLogon=lastLogon,
812                                   lastLogonTimestamp=lastLogonTimestamp,
813                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
814                                   msDSUserAccountControlComputed=0)
815         badPasswordTime = int(res[0]["badPasswordTime"][0])
816
817         # Correct old password
818         old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
819         invalid_utf16 = "\"thatsAcomplPASSX\"".encode('utf-16-le')
820         userpass = "thatsAcomplPASS2"
821         creds.set_password(userpass)
822         new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
823
824         other_ldb.modify_ldif("""
825 dn: """ + userdn + """
826 changetype: modify
827 delete: unicodePwd
828 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
829 add: unicodePwd
830 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
831 """)
832
833         res = self._check_account(userdn,
834                                   badPwdCount=1,
835                                   badPasswordTime=badPasswordTime,
836                                   logonCount=logonCount,
837                                   lastLogon=lastLogon,
838                                   lastLogonTimestamp=lastLogonTimestamp,
839                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
840                                   msDSUserAccountControlComputed=0)
841
842         # Wrong old password
843         try:
844             other_ldb.modify_ldif("""
845 dn: """ + userdn + """
846 changetype: modify
847 delete: unicodePwd
848 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
849 add: unicodePwd
850 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
851 """)
852             self.fail()
853         except LdbError as e11:
854             (num, msg) = e11.args
855             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
856             self.assertTrue('00000056' in msg, msg)
857
858         res = self._check_account(userdn,
859                                   badPwdCount=2,
860                                   badPasswordTime=("greater", badPasswordTime),
861                                   logonCount=logonCount,
862                                   lastLogon=lastLogon,
863                                   lastLogonTimestamp=lastLogonTimestamp,
864                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
865                                   msDSUserAccountControlComputed=0)
866         badPasswordTime = int(res[0]["badPasswordTime"][0])
867
868         # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
869         # It doesn't create "lockoutTime" = 0 and doesn't
870         # reset "badPwdCount" = 0.
871         self._reset_samr(res)
872
873         res = self._check_account(userdn,
874                                   badPwdCount=2,
875                                   badPasswordTime=badPasswordTime,
876                                   logonCount=logonCount,
877                                   lastLogon=lastLogon,
878                                   lastLogonTimestamp=lastLogonTimestamp,
879                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
880                                   msDSUserAccountControlComputed=0)
881
882         print("two failed password change")
883
884         # Wrong old password
885         try:
886             other_ldb.modify_ldif("""
887 dn: """ + userdn + """
888 changetype: modify
889 delete: unicodePwd
890 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
891 add: unicodePwd
892 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
893 """)
894             self.fail()
895         except LdbError as e12:
896             (num, msg) = e12.args
897             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
898             self.assertTrue('00000056' in msg, msg)
899
900         # this is strange, why do we have lockoutTime=badPasswordTime here?
901         res = self._check_account(userdn,
902                                   badPwdCount=3,
903                                   badPasswordTime=("greater", badPasswordTime),
904                                   logonCount=logonCount,
905                                   lastLogon=lastLogon,
906                                   lastLogonTimestamp=lastLogonTimestamp,
907                                   lockoutTime=("greater", badPasswordTime),
908                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
909                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
910         badPasswordTime = int(res[0]["badPasswordTime"][0])
911         lockoutTime = int(res[0]["lockoutTime"][0])
912
913         # Wrong old password
914         try:
915             other_ldb.modify_ldif("""
916 dn: """ + userdn + """
917 changetype: modify
918 delete: unicodePwd
919 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
920 add: unicodePwd
921 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
922 """)
923             self.fail()
924         except LdbError as e13:
925             (num, msg) = e13.args
926             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
927             self.assertTrue('00000775' in msg, msg)
928
929         res = self._check_account(userdn,
930                                   badPwdCount=3,
931                                   badPasswordTime=badPasswordTime,
932                                   logonCount=logonCount,
933                                   lastLogon=lastLogon,
934                                   lastLogonTimestamp=lastLogonTimestamp,
935                                   lockoutTime=lockoutTime,
936                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
937                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
938
939         # Wrong old password
940         try:
941             other_ldb.modify_ldif("""
942 dn: """ + userdn + """
943 changetype: modify
944 delete: unicodePwd
945 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
946 add: unicodePwd
947 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
948 """)
949             self.fail()
950         except LdbError as e14:
951             (num, msg) = e14.args
952             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
953             self.assertTrue('00000775' in msg, msg)
954
955         res = self._check_account(userdn,
956                                   badPwdCount=3,
957                                   badPasswordTime=badPasswordTime,
958                                   logonCount=logonCount,
959                                   lastLogon=lastLogon,
960                                   lastLogonTimestamp=lastLogonTimestamp,
961                                   lockoutTime=lockoutTime,
962                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
963                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
964
965         try:
966             # Correct old password
967             other_ldb.modify_ldif("""
968 dn: """ + userdn + """
969 changetype: modify
970 delete: unicodePwd
971 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
972 add: unicodePwd
973 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
974 """)
975             self.fail()
976         except LdbError as e15:
977             (num, msg) = e15.args
978             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
979             self.assertTrue('00000775' in msg, msg)
980
981         res = self._check_account(userdn,
982                                   badPwdCount=3,
983                                   badPasswordTime=badPasswordTime,
984                                   logonCount=logonCount,
985                                   lastLogon=lastLogon,
986                                   lastLogonTimestamp=lastLogonTimestamp,
987                                   lockoutTime=lockoutTime,
988                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
989                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
990
991         # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
992         self._reset_samr(res)
993
994         res = self._check_account(userdn,
995                                   badPwdCount=0,
996                                   badPasswordTime=badPasswordTime,
997                                   logonCount=logonCount,
998                                   lastLogon=lastLogon,
999                                   lastLogonTimestamp=lastLogonTimestamp,
1000                                   lockoutTime=0,
1001                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1002                                   msDSUserAccountControlComputed=0)
1003
1004         # Correct old password
1005         old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
1006         invalid_utf16 = "\"thatsAcomplPASSiX\"".encode('utf-16-le')
1007         userpass = "thatsAcomplPASS2x"
1008         creds.set_password(userpass)
1009         new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
1010
1011         other_ldb.modify_ldif("""
1012 dn: """ + userdn + """
1013 changetype: modify
1014 delete: unicodePwd
1015 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
1016 add: unicodePwd
1017 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
1018 """)
1019
1020         res = self._check_account(userdn,
1021                                   badPwdCount=0,
1022                                   badPasswordTime=badPasswordTime,
1023                                   logonCount=logonCount,
1024                                   lastLogon=lastLogon,
1025                                   lastLogonTimestamp=lastLogonTimestamp,
1026                                   lockoutTime=0,
1027                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1028                                   msDSUserAccountControlComputed=0)
1029
1030         # Wrong old password
1031         try:
1032             other_ldb.modify_ldif("""
1033 dn: """ + userdn + """
1034 changetype: modify
1035 delete: unicodePwd
1036 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
1037 add: unicodePwd
1038 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
1039 """)
1040             self.fail()
1041         except LdbError as e16:
1042             (num, msg) = e16.args
1043             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
1044             self.assertTrue('00000056' in msg, msg)
1045
1046         res = self._check_account(userdn,
1047                                   badPwdCount=1,
1048                                   badPasswordTime=("greater", badPasswordTime),
1049                                   logonCount=logonCount,
1050                                   lastLogon=lastLogon,
1051                                   lastLogonTimestamp=lastLogonTimestamp,
1052                                   lockoutTime=0,
1053                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1054                                   msDSUserAccountControlComputed=0)
1055         badPasswordTime = int(res[0]["badPasswordTime"][0])
1056
1057         # Wrong old password
1058         try:
1059             other_ldb.modify_ldif("""
1060 dn: """ + userdn + """
1061 changetype: modify
1062 delete: unicodePwd
1063 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
1064 add: unicodePwd
1065 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
1066 """)
1067             self.fail()
1068         except LdbError as e17:
1069             (num, msg) = e17.args
1070             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
1071             self.assertTrue('00000056' in msg, msg)
1072
1073         res = self._check_account(userdn,
1074                                   badPwdCount=2,
1075                                   badPasswordTime=("greater", badPasswordTime),
1076                                   logonCount=logonCount,
1077                                   lastLogon=lastLogon,
1078                                   lastLogonTimestamp=lastLogonTimestamp,
1079                                   lockoutTime=0,
1080                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1081                                   msDSUserAccountControlComputed=0)
1082         badPasswordTime = int(res[0]["badPasswordTime"][0])
1083
1084         # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
1085         # It doesn't reset "badPwdCount" = 0.
1086         self._reset_samr(res)
1087
1088         res = self._check_account(userdn,
1089                                   badPwdCount=2,
1090                                   badPasswordTime=badPasswordTime,
1091                                   logonCount=logonCount,
1092                                   lastLogon=lastLogon,
1093                                   lastLogonTimestamp=lastLogonTimestamp,
1094                                   lockoutTime=0,
1095                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1096                                   msDSUserAccountControlComputed=0)
1097
1098         # Wrong old password
1099         try:
1100             other_ldb.modify_ldif("""
1101 dn: """ + userdn + """
1102 changetype: modify
1103 delete: unicodePwd
1104 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
1105 add: unicodePwd
1106 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
1107 """)
1108             self.fail()
1109         except LdbError as e18:
1110             (num, msg) = e18.args
1111             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
1112             self.assertTrue('00000056' in msg, msg)
1113
1114         res = self._check_account(userdn,
1115                                   badPwdCount=3,
1116                                   badPasswordTime=("greater", badPasswordTime),
1117                                   logonCount=logonCount,
1118                                   lastLogon=lastLogon,
1119                                   lastLogonTimestamp=lastLogonTimestamp,
1120                                   lockoutTime=("greater", badPasswordTime),
1121                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1122                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
1123         badPasswordTime = int(res[0]["badPasswordTime"][0])
1124         lockoutTime = int(res[0]["lockoutTime"][0])
1125
1126         time.sleep(self.account_lockout_duration + 1)
1127
1128         res = self._check_account(userdn,
1129                                   badPwdCount=3, effective_bad_password_count=0,
1130                                   badPasswordTime=badPasswordTime,
1131                                   logonCount=logonCount,
1132                                   lastLogon=lastLogon,
1133                                   lastLogonTimestamp=lastLogonTimestamp,
1134                                   lockoutTime=lockoutTime,
1135                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1136                                   msDSUserAccountControlComputed=0)
1137
1138         # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
1139         # It doesn't reset "lockoutTime" = 0 and doesn't
1140         # reset "badPwdCount" = 0.
1141         self._reset_samr(res)
1142
1143         res = self._check_account(userdn,
1144                                   badPwdCount=3, effective_bad_password_count=0,
1145                                   badPasswordTime=badPasswordTime,
1146                                   logonCount=logonCount,
1147                                   lockoutTime=lockoutTime,
1148                                   lastLogon=lastLogon,
1149                                   lastLogonTimestamp=lastLogonTimestamp,
1150                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1151                                   msDSUserAccountControlComputed=0)
1152
1153     def test_unicodePwd_lockout_with_clear_change_krb5(self):
1154         self._test_unicodePwd_lockout_with_clear_change(self.lockout1krb5_creds,
1155                                                         self.lockout2krb5_ldb)
1156
1157     def test_unicodePwd_lockout_with_clear_change_ntlm(self):
1158         self._test_unicodePwd_lockout_with_clear_change(self.lockout1ntlm_creds,
1159                                                         self.lockout2ntlm_ldb,
1160                                                         initial_logoncount_relation="equal")
1161
1162     def test_login_lockout_krb5(self):
1163         self._test_login_lockout(self.lockout1krb5_creds)
1164
1165     def test_login_lockout_ntlm(self):
1166         self._test_login_lockout(self.lockout1ntlm_creds)
1167
1168     # Repeat the login lockout tests using PSOs
1169     def test_pso_login_lockout_krb5(self):
1170         """Check the PSO lockout settings get applied to the user correctly"""
1171         self.use_pso_lockout_settings(self.lockout1krb5_creds)
1172         self._test_login_lockout(self.lockout1krb5_creds)
1173
1174     def test_pso_login_lockout_ntlm(self):
1175         """Check the PSO lockout settings get applied to the user correctly"""
1176         self.use_pso_lockout_settings(self.lockout1ntlm_creds)
1177         self._test_login_lockout(self.lockout1ntlm_creds)
1178
1179     def _testing_add_user(self, creds, lockOutObservationWindow=0):
1180         username = creds.get_username()
1181         userpass = creds.get_password()
1182         userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
1183
1184         use_kerberos = creds.get_kerberos_state()
1185         if use_kerberos == MUST_USE_KERBEROS:
1186             logoncount_relation = 'greater'
1187             lastlogon_relation = 'greater'
1188         else:
1189             logoncount_relation = 'equal'
1190             if lockOutObservationWindow == 0:
1191                 lastlogon_relation = 'greater'
1192             else:
1193                 lastlogon_relation = 'equal'
1194
1195         delete_force(self.ldb, userdn)
1196         self.ldb.add({
1197              "dn": userdn,
1198              "objectclass": "user",
1199              "sAMAccountName": username})
1200
1201         self.addCleanup(delete_force, self.ldb, userdn)
1202
1203         res = self._check_account(userdn,
1204                                   badPwdCount=0,
1205                                   badPasswordTime=0,
1206                                   logonCount=0,
1207                                   lastLogon=0,
1208                                   lastLogonTimestamp=('absent', None),
1209                                   userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
1210                                                       dsdb.UF_ACCOUNTDISABLE |
1211                                                       dsdb.UF_PASSWD_NOTREQD),
1212                                   msDSUserAccountControlComputed=dsdb.UF_PASSWORD_EXPIRED)
1213
1214         # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
1215         # It doesn't create "lockoutTime" = 0.
1216         self._reset_samr(res)
1217
1218         res = self._check_account(userdn,
1219                                   badPwdCount=0,
1220                                   badPasswordTime=0,
1221                                   logonCount=0,
1222                                   lastLogon=0,
1223                                   lastLogonTimestamp=('absent', None),
1224                                   userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
1225                                                       dsdb.UF_ACCOUNTDISABLE |
1226                                                       dsdb.UF_PASSWD_NOTREQD),
1227                                   msDSUserAccountControlComputed=dsdb.UF_PASSWORD_EXPIRED)
1228
1229         # Tests a password change when we don't have any password yet with a
1230         # wrong old password
1231         try:
1232             self.ldb.modify_ldif("""
1233 dn: """ + userdn + """
1234 changetype: modify
1235 delete: userPassword
1236 userPassword: noPassword
1237 add: userPassword
1238 userPassword: thatsAcomplPASS2
1239 """)
1240             self.fail()
1241         except LdbError as e19:
1242             (num, msg) = e19.args
1243             self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
1244             # Windows (2008 at least) seems to have some small bug here: it
1245             # returns "0000056A" on longer (always wrong) previous passwords.
1246             self.assertTrue('00000056' in msg, msg)
1247
1248         res = self._check_account(userdn,
1249                                   badPwdCount=1,
1250                                   badPasswordTime=("greater", 0),
1251                                   logonCount=0,
1252                                   lastLogon=0,
1253                                   lastLogonTimestamp=('absent', None),
1254                                   userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
1255                                                       dsdb.UF_ACCOUNTDISABLE |
1256                                                       dsdb.UF_PASSWD_NOTREQD),
1257                                   msDSUserAccountControlComputed=dsdb.UF_PASSWORD_EXPIRED)
1258         badPwdCount = int(res[0]["badPwdCount"][0])
1259         badPasswordTime = int(res[0]["badPasswordTime"][0])
1260
1261         # Sets the initial user password with a "special" password change
1262         # I think that this internally is a password set operation and it can
1263         # only be performed by someone which has password set privileges on the
1264         # account (at least in s4 we do handle it like that).
1265         self.ldb.modify_ldif("""
1266 dn: """ + userdn + """
1267 changetype: modify
1268 delete: userPassword
1269 add: userPassword
1270 userPassword: """ + userpass + """
1271 """)
1272
1273         res = self._check_account(userdn,
1274                                   badPwdCount=badPwdCount,
1275                                   badPasswordTime=badPasswordTime,
1276                                   logonCount=0,
1277                                   lastLogon=0,
1278                                   lastLogonTimestamp=('absent', None),
1279                                   userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
1280                                                       dsdb.UF_ACCOUNTDISABLE |
1281                                                       dsdb.UF_PASSWD_NOTREQD),
1282                                   msDSUserAccountControlComputed=0)
1283
1284         # Enables the user account
1285         self.ldb.enable_account("(sAMAccountName=%s)" % username)
1286
1287         res = self._check_account(userdn,
1288                                   badPwdCount=badPwdCount,
1289                                   badPasswordTime=badPasswordTime,
1290                                   logonCount=0,
1291                                   lastLogon=0,
1292                                   lastLogonTimestamp=('absent', None),
1293                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1294                                   msDSUserAccountControlComputed=0)
1295         if lockOutObservationWindow != 0:
1296             time.sleep(lockOutObservationWindow + 1)
1297             effective_bad_password_count = 0
1298         else:
1299             effective_bad_password_count = badPwdCount
1300
1301         res = self._check_account(userdn,
1302                                   badPwdCount=badPwdCount,
1303                                   effective_bad_password_count=effective_bad_password_count,
1304                                   badPasswordTime=badPasswordTime,
1305                                   logonCount=0,
1306                                   lastLogon=0,
1307                                   lastLogonTimestamp=('absent', None),
1308                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1309                                   msDSUserAccountControlComputed=0)
1310
1311         ldb = SamDB(url=self.host_url, credentials=creds, lp=self.lp)
1312
1313         if lockOutObservationWindow == 0:
1314             badPwdCount = 0
1315             effective_bad_password_count = 0
1316         if use_kerberos == MUST_USE_KERBEROS:
1317             badPwdCount = 0
1318             effective_bad_password_count = 0
1319
1320         res = self._check_account(userdn,
1321                                   badPwdCount=badPwdCount,
1322                                   effective_bad_password_count=effective_bad_password_count,
1323                                   badPasswordTime=badPasswordTime,
1324                                   logonCount=(logoncount_relation, 0),
1325                                   lastLogon=(lastlogon_relation, 0),
1326                                   lastLogonTimestamp=('greater', badPasswordTime),
1327                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1328                                   msDSUserAccountControlComputed=0)
1329
1330         logonCount = int(res[0]["logonCount"][0])
1331         lastLogon = int(res[0]["lastLogon"][0])
1332         lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
1333         if lastlogon_relation == 'greater':
1334             self.assertGreater(lastLogon, badPasswordTime)
1335             self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
1336
1337         res = self._check_account(userdn,
1338                                   badPwdCount=badPwdCount,
1339                                   effective_bad_password_count=effective_bad_password_count,
1340                                   badPasswordTime=badPasswordTime,
1341                                   logonCount=logonCount,
1342                                   lastLogon=lastLogon,
1343                                   lastLogonTimestamp=lastLogonTimestamp,
1344                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1345                                   msDSUserAccountControlComputed=0)
1346         return ldb
1347
1348     def test_lockout_observation_window(self):
1349         lockout3krb5_creds = self.insta_creds(self.template_creds,
1350                                               username="lockout3krb5",
1351                                               userpass="thatsAcomplPASS0",
1352                                               kerberos_state=MUST_USE_KERBEROS)
1353         self._testing_add_user(lockout3krb5_creds)
1354
1355         lockout4krb5_creds = self.insta_creds(self.template_creds,
1356                                               username="lockout4krb5",
1357                                               userpass="thatsAcomplPASS0",
1358                                               kerberos_state=MUST_USE_KERBEROS)
1359         self._testing_add_user(lockout4krb5_creds,
1360                                lockOutObservationWindow=self.lockout_observation_window)
1361
1362         lockout3ntlm_creds = self.insta_creds(self.template_creds,
1363                                               username="lockout3ntlm",
1364                                               userpass="thatsAcomplPASS0",
1365                                               kerberos_state=DONT_USE_KERBEROS)
1366         self._testing_add_user(lockout3ntlm_creds)
1367         lockout4ntlm_creds = self.insta_creds(self.template_creds,
1368                                               username="lockout4ntlm",
1369                                               userpass="thatsAcomplPASS0",
1370                                               kerberos_state=DONT_USE_KERBEROS)
1371         self._testing_add_user(lockout4ntlm_creds,
1372                                lockOutObservationWindow=self.lockout_observation_window)
1373
1374 class PasswordTestsWithDefaults(PasswordTests):
1375     def setUp(self):
1376         # The tests in this class do not sleep, so we can use the default
1377         # timeout windows here
1378         self.account_lockout_duration = 30 * 60
1379         self.lockout_observation_window = 30 * 60
1380         super(PasswordTestsWithDefaults, self).setUp()
1381
1382     # sanity-check that user lockout works with the default settings (we just
1383     # check the user is locked out - we don't wait for the lockout to expire)
1384     def test_login_lockout_krb5(self):
1385         self._test_login_lockout(self.lockout1krb5_creds,
1386                                  wait_lockout_duration=False)
1387
1388     def test_login_lockout_ntlm(self):
1389         self._test_login_lockout(self.lockout1ntlm_creds,
1390                                  wait_lockout_duration=False)
1391
1392     # Repeat the login lockout tests using PSOs
1393     def test_pso_login_lockout_krb5(self):
1394         """Check the PSO lockout settings get applied to the user correctly"""
1395         self.use_pso_lockout_settings(self.lockout1krb5_creds)
1396         self._test_login_lockout(self.lockout1krb5_creds,
1397                                  wait_lockout_duration=False)
1398
1399     def test_pso_login_lockout_ntlm(self):
1400         """Check the PSO lockout settings get applied to the user correctly"""
1401         self.use_pso_lockout_settings(self.lockout1ntlm_creds)
1402         self._test_login_lockout(self.lockout1ntlm_creds,
1403                                  wait_lockout_duration=False)
1404
1405 host_url = "ldap://%s" % host
1406
1407 TestProgram(module=__name__, opts=subunitopts)