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