s4/dsdb/tests: py2/py3 compatability always decode result of b64encode
[metze/samba/wip.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=2)
622         self.addCleanup(self.ldb.delete, pso.dn)
623
624         userdn = "cn=%s,cn=users,%s" % (creds.get_username(), self.base_dn)
625         pso.apply_to(userdn)
626
627         # update the global lockout settings to be wildly different to what
628         # the test cases normally expect
629         self.update_lockout_settings(threshold=10, duration=600,
630                                      observation_window=600)
631
632     def _test_unicodePwd_lockout_with_clear_change(self, creds, other_ldb,
633                                                    initial_logoncount_relation=None):
634         print("Performs a password cleartext change operation on 'unicodePwd'")
635         username = creds.get_username()
636         userpass = creds.get_password()
637         userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
638         if initial_logoncount_relation is not None:
639             logoncount_relation = initial_logoncount_relation
640         else:
641             logoncount_relation = "greater"
642
643         res = self._check_account(userdn,
644                                   badPwdCount=0,
645                                   badPasswordTime=("greater", 0),
646                                   logonCount=(logoncount_relation, 0),
647                                   lastLogon=("greater", 0),
648                                   lastLogonTimestamp=("greater", 0),
649                                   userAccountControl=
650                                     dsdb.UF_NORMAL_ACCOUNT,
651                                   msDSUserAccountControlComputed=0)
652         badPasswordTime = int(res[0]["badPasswordTime"][0])
653         logonCount = int(res[0]["logonCount"][0])
654         lastLogon = int(res[0]["lastLogon"][0])
655         lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
656         self.assertGreater(lastLogonTimestamp, badPasswordTime)
657         self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
658
659         # Change password on a connection as another user
660
661         # Wrong old password
662         try:
663             other_ldb.modify_ldif("""
664 dn: """ + userdn + """
665 changetype: modify
666 delete: unicodePwd
667 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1x\"".encode('utf-16-le')).decode('utf8') + """
668 add: unicodePwd
669 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
670 """)
671             self.fail()
672         except LdbError as e10:
673             (num, msg) = e10.args
674             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
675             self.assertTrue('00000056' in msg, msg)
676
677         res = self._check_account(userdn,
678                                   badPwdCount=1,
679                                   badPasswordTime=("greater", badPasswordTime),
680                                   logonCount=logonCount,
681                                   lastLogon=lastLogon,
682                                   lastLogonTimestamp=lastLogonTimestamp,
683                                   userAccountControl=
684                                     dsdb.UF_NORMAL_ACCOUNT,
685                                   msDSUserAccountControlComputed=0)
686         badPasswordTime = int(res[0]["badPasswordTime"][0])
687
688         # Correct old password
689         old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
690         invalid_utf16 = "\"thatsAcomplPASSX\"".encode('utf-16-le')
691         userpass = "thatsAcomplPASS2"
692         creds.set_password(userpass)
693         new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
694
695         other_ldb.modify_ldif("""
696 dn: """ + userdn + """
697 changetype: modify
698 delete: unicodePwd
699 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
700 add: unicodePwd
701 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
702 """)
703
704         res = self._check_account(userdn,
705                                   badPwdCount=1,
706                                   badPasswordTime=badPasswordTime,
707                                   logonCount=logonCount,
708                                   lastLogon=lastLogon,
709                                   lastLogonTimestamp=lastLogonTimestamp,
710                                   userAccountControl=
711                                     dsdb.UF_NORMAL_ACCOUNT,
712                                   msDSUserAccountControlComputed=0)
713
714         # Wrong old password
715         try:
716             other_ldb.modify_ldif("""
717 dn: """ + userdn + """
718 changetype: modify
719 delete: unicodePwd
720 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
721 add: unicodePwd
722 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
723 """)
724             self.fail()
725         except LdbError as e11:
726             (num, msg) = e11.args
727             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
728             self.assertTrue('00000056' in msg, msg)
729
730         res = self._check_account(userdn,
731                                   badPwdCount=2,
732                                   badPasswordTime=("greater", badPasswordTime),
733                                   logonCount=logonCount,
734                                   lastLogon=lastLogon,
735                                   lastLogonTimestamp=lastLogonTimestamp,
736                                   userAccountControl=
737                                     dsdb.UF_NORMAL_ACCOUNT,
738                                   msDSUserAccountControlComputed=0)
739         badPasswordTime = int(res[0]["badPasswordTime"][0])
740
741         # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
742         # It doesn't create "lockoutTime" = 0 and doesn't
743         # reset "badPwdCount" = 0.
744         self._reset_samr(res)
745
746         res = self._check_account(userdn,
747                                   badPwdCount=2,
748                                   badPasswordTime=badPasswordTime,
749                                   logonCount=logonCount,
750                                   lastLogon=lastLogon,
751                                   lastLogonTimestamp=lastLogonTimestamp,
752                                   userAccountControl=
753                                     dsdb.UF_NORMAL_ACCOUNT,
754                                   msDSUserAccountControlComputed=0)
755
756         print("two failed password change")
757
758         # Wrong old password
759         try:
760             other_ldb.modify_ldif("""
761 dn: """ + userdn + """
762 changetype: modify
763 delete: unicodePwd
764 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
765 add: unicodePwd
766 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
767 """)
768             self.fail()
769         except LdbError as e12:
770             (num, msg) = e12.args
771             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
772             self.assertTrue('00000056' in msg, msg)
773
774         # this is strange, why do we have lockoutTime=badPasswordTime here?
775         res = self._check_account(userdn,
776                                   badPwdCount=3,
777                                   badPasswordTime=("greater", badPasswordTime),
778                                   logonCount=logonCount,
779                                   lastLogon=lastLogon,
780                                   lastLogonTimestamp=lastLogonTimestamp,
781                                   lockoutTime=("greater", badPasswordTime),
782                                   userAccountControl=
783                                     dsdb.UF_NORMAL_ACCOUNT,
784                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
785         badPasswordTime = int(res[0]["badPasswordTime"][0])
786         lockoutTime = int(res[0]["lockoutTime"][0])
787
788         # Wrong old password
789         try:
790             other_ldb.modify_ldif("""
791 dn: """ + userdn + """
792 changetype: modify
793 delete: unicodePwd
794 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
795 add: unicodePwd
796 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
797 """)
798             self.fail()
799         except LdbError as e13:
800             (num, msg) = e13.args
801             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
802             self.assertTrue('00000775' in msg, msg)
803
804         res = self._check_account(userdn,
805                                   badPwdCount=3,
806                                   badPasswordTime=badPasswordTime,
807                                   logonCount=logonCount,
808                                   lastLogon=lastLogon,
809                                   lastLogonTimestamp=lastLogonTimestamp,
810                                   lockoutTime=lockoutTime,
811                                   userAccountControl=
812                                     dsdb.UF_NORMAL_ACCOUNT,
813                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
814
815         # Wrong old password
816         try:
817             other_ldb.modify_ldif("""
818 dn: """ + userdn + """
819 changetype: modify
820 delete: unicodePwd
821 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
822 add: unicodePwd
823 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
824 """)
825             self.fail()
826         except LdbError as e14:
827             (num, msg) = e14.args
828             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
829             self.assertTrue('00000775' in msg, msg)
830
831         res = self._check_account(userdn,
832                                   badPwdCount=3,
833                                   badPasswordTime=badPasswordTime,
834                                   logonCount=logonCount,
835                                   lastLogon=lastLogon,
836                                   lastLogonTimestamp=lastLogonTimestamp,
837                                   lockoutTime=lockoutTime,
838                                   userAccountControl=
839                                     dsdb.UF_NORMAL_ACCOUNT,
840                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
841
842         try:
843             # Correct old password
844             other_ldb.modify_ldif("""
845 dn: """ + userdn + """
846 changetype: modify
847 delete: unicodePwd
848 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
849 add: unicodePwd
850 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
851 """)
852             self.fail()
853         except LdbError as e15:
854             (num, msg) = e15.args
855             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
856             self.assertTrue('00000775' in msg, msg)
857
858         res = self._check_account(userdn,
859                                   badPwdCount=3,
860                                   badPasswordTime=badPasswordTime,
861                                   logonCount=logonCount,
862                                   lastLogon=lastLogon,
863                                   lastLogonTimestamp=lastLogonTimestamp,
864                                   lockoutTime=lockoutTime,
865                                   userAccountControl=
866                                     dsdb.UF_NORMAL_ACCOUNT,
867                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
868
869         # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
870         self._reset_samr(res);
871
872         res = self._check_account(userdn,
873                                   badPwdCount=0,
874                                   badPasswordTime=badPasswordTime,
875                                   logonCount=logonCount,
876                                   lastLogon=lastLogon,
877                                   lastLogonTimestamp=lastLogonTimestamp,
878                                   lockoutTime=0,
879                                   userAccountControl=
880                                     dsdb.UF_NORMAL_ACCOUNT,
881                                   msDSUserAccountControlComputed=0)
882
883         # Correct old password
884         old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
885         invalid_utf16 = "\"thatsAcomplPASSiX\"".encode('utf-16-le')
886         userpass = "thatsAcomplPASS2x"
887         creds.set_password(userpass)
888         new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
889
890         other_ldb.modify_ldif("""
891 dn: """ + userdn + """
892 changetype: modify
893 delete: unicodePwd
894 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
895 add: unicodePwd
896 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
897 """)
898
899         res = self._check_account(userdn,
900                                   badPwdCount=0,
901                                   badPasswordTime=badPasswordTime,
902                                   logonCount=logonCount,
903                                   lastLogon=lastLogon,
904                                   lastLogonTimestamp=lastLogonTimestamp,
905                                   lockoutTime=0,
906                                   userAccountControl=
907                                     dsdb.UF_NORMAL_ACCOUNT,
908                                   msDSUserAccountControlComputed=0)
909
910         # Wrong old password
911         try:
912             other_ldb.modify_ldif("""
913 dn: """ + userdn + """
914 changetype: modify
915 delete: unicodePwd
916 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
917 add: unicodePwd
918 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
919 """)
920             self.fail()
921         except LdbError as e16:
922             (num, msg) = e16.args
923             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
924             self.assertTrue('00000056' in msg, msg)
925
926         res = self._check_account(userdn,
927                                   badPwdCount=1,
928                                   badPasswordTime=("greater", badPasswordTime),
929                                   logonCount=logonCount,
930                                   lastLogon=lastLogon,
931                                   lastLogonTimestamp=lastLogonTimestamp,
932                                   lockoutTime=0,
933                                   userAccountControl=
934                                     dsdb.UF_NORMAL_ACCOUNT,
935                                   msDSUserAccountControlComputed=0)
936         badPasswordTime = int(res[0]["badPasswordTime"][0])
937
938         # Wrong old password
939         try:
940             other_ldb.modify_ldif("""
941 dn: """ + userdn + """
942 changetype: modify
943 delete: unicodePwd
944 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
945 add: unicodePwd
946 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
947 """)
948             self.fail()
949         except LdbError as e17:
950             (num, msg) = e17.args
951             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
952             self.assertTrue('00000056' in msg, msg)
953
954         res = self._check_account(userdn,
955                                   badPwdCount=2,
956                                   badPasswordTime=("greater", badPasswordTime),
957                                   logonCount=logonCount,
958                                   lastLogon=lastLogon,
959                                   lastLogonTimestamp=lastLogonTimestamp,
960                                   lockoutTime=0,
961                                   userAccountControl=
962                                     dsdb.UF_NORMAL_ACCOUNT,
963                                   msDSUserAccountControlComputed=0)
964         badPasswordTime = int(res[0]["badPasswordTime"][0])
965
966         # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
967         # It doesn't reset "badPwdCount" = 0.
968         self._reset_samr(res)
969
970         res = self._check_account(userdn,
971                                   badPwdCount=2,
972                                   badPasswordTime=badPasswordTime,
973                                   logonCount=logonCount,
974                                   lastLogon=lastLogon,
975                                   lastLogonTimestamp=lastLogonTimestamp,
976                                   lockoutTime=0,
977                                   userAccountControl=
978                                     dsdb.UF_NORMAL_ACCOUNT,
979                                   msDSUserAccountControlComputed=0)
980
981         # Wrong old password
982         try:
983             other_ldb.modify_ldif("""
984 dn: """ + userdn + """
985 changetype: modify
986 delete: unicodePwd
987 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
988 add: unicodePwd
989 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
990 """)
991             self.fail()
992         except LdbError as e18:
993             (num, msg) = e18.args
994             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
995             self.assertTrue('00000056' in msg, msg)
996
997         res = self._check_account(userdn,
998                                   badPwdCount=3,
999                                   badPasswordTime=("greater", badPasswordTime),
1000                                   logonCount=logonCount,
1001                                   lastLogon=lastLogon,
1002                                   lastLogonTimestamp=lastLogonTimestamp,
1003                                   lockoutTime=("greater", badPasswordTime),
1004                                   userAccountControl=
1005                                     dsdb.UF_NORMAL_ACCOUNT,
1006                                   msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
1007         badPasswordTime = int(res[0]["badPasswordTime"][0])
1008         lockoutTime = int(res[0]["lockoutTime"][0])
1009
1010         time.sleep(self.account_lockout_duration + 1)
1011
1012         res = self._check_account(userdn,
1013                                   badPwdCount=3, effective_bad_password_count=0,
1014                                   badPasswordTime=badPasswordTime,
1015                                   logonCount=logonCount,
1016                                   lastLogon=lastLogon,
1017                                   lastLogonTimestamp=lastLogonTimestamp,
1018                                   lockoutTime=lockoutTime,
1019                                   userAccountControl=
1020                                     dsdb.UF_NORMAL_ACCOUNT,
1021                                   msDSUserAccountControlComputed=0)
1022
1023         # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
1024         # It doesn't reset "lockoutTime" = 0 and doesn't
1025         # reset "badPwdCount" = 0.
1026         self._reset_samr(res)
1027
1028         res = self._check_account(userdn,
1029                                   badPwdCount=3, effective_bad_password_count=0,
1030                                   badPasswordTime=badPasswordTime,
1031                                   logonCount=logonCount,
1032                                   lockoutTime=lockoutTime,
1033                                   lastLogon=lastLogon,
1034                                   lastLogonTimestamp=lastLogonTimestamp,
1035                                   userAccountControl=
1036                                     dsdb.UF_NORMAL_ACCOUNT,
1037                                   msDSUserAccountControlComputed=0)
1038
1039     def test_unicodePwd_lockout_with_clear_change_krb5(self):
1040         self._test_unicodePwd_lockout_with_clear_change(self.lockout1krb5_creds,
1041                                                         self.lockout2krb5_ldb)
1042
1043     def test_unicodePwd_lockout_with_clear_change_ntlm(self):
1044         self._test_unicodePwd_lockout_with_clear_change(self.lockout1ntlm_creds,
1045                                                         self.lockout2ntlm_ldb,
1046                                                         initial_logoncount_relation="equal")
1047
1048     def test_login_lockout_krb5(self):
1049         self._test_login_lockout(self.lockout1krb5_creds)
1050
1051     def test_login_lockout_ntlm(self):
1052         self._test_login_lockout(self.lockout1ntlm_creds)
1053
1054     # Repeat the login lockout tests using PSOs
1055     def test_pso_login_lockout_krb5(self):
1056         """Check the PSO lockout settings get applied to the user correctly"""
1057         self.use_pso_lockout_settings(self.lockout1krb5_creds)
1058         self._test_login_lockout(self.lockout1krb5_creds)
1059
1060     def test_pso_login_lockout_ntlm(self):
1061         """Check the PSO lockout settings get applied to the user correctly"""
1062         self.use_pso_lockout_settings(self.lockout1ntlm_creds)
1063         self._test_login_lockout(self.lockout1ntlm_creds)
1064
1065     def test_multiple_logon_krb5(self):
1066         self._test_multiple_logon(self.lockout1krb5_creds)
1067
1068     def test_multiple_logon_ntlm(self):
1069         self._test_multiple_logon(self.lockout1ntlm_creds)
1070
1071     def _testing_add_user(self, creds, lockOutObservationWindow=0):
1072         username = creds.get_username()
1073         userpass = creds.get_password()
1074         userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
1075
1076         use_kerberos = creds.get_kerberos_state()
1077         if use_kerberos == MUST_USE_KERBEROS:
1078             logoncount_relation = 'greater'
1079             lastlogon_relation = 'greater'
1080         else:
1081             logoncount_relation = 'equal'
1082             if lockOutObservationWindow == 0:
1083                 lastlogon_relation = 'greater'
1084             else:
1085                 lastlogon_relation = 'equal'
1086
1087         delete_force(self.ldb, userdn)
1088         self.ldb.add({
1089              "dn": userdn,
1090              "objectclass": "user",
1091              "sAMAccountName": username})
1092
1093         self.addCleanup(delete_force, self.ldb, userdn)
1094
1095         res = self._check_account(userdn,
1096                                   badPwdCount=0,
1097                                   badPasswordTime=0,
1098                                   logonCount=0,
1099                                   lastLogon=0,
1100                                   lastLogonTimestamp=('absent', None),
1101                                   userAccountControl=
1102                                     dsdb.UF_NORMAL_ACCOUNT |
1103                                     dsdb.UF_ACCOUNTDISABLE |
1104                                     dsdb.UF_PASSWD_NOTREQD,
1105                                   msDSUserAccountControlComputed=
1106                                     dsdb.UF_PASSWORD_EXPIRED)
1107
1108         # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
1109         # It doesn't create "lockoutTime" = 0.
1110         self._reset_samr(res)
1111
1112         res = self._check_account(userdn,
1113                                   badPwdCount=0,
1114                                   badPasswordTime=0,
1115                                   logonCount=0,
1116                                   lastLogon=0,
1117                                   lastLogonTimestamp=('absent', None),
1118                                   userAccountControl=
1119                                     dsdb.UF_NORMAL_ACCOUNT |
1120                                     dsdb.UF_ACCOUNTDISABLE |
1121                                     dsdb.UF_PASSWD_NOTREQD,
1122                                   msDSUserAccountControlComputed=
1123                                     dsdb.UF_PASSWORD_EXPIRED)
1124
1125         # Tests a password change when we don't have any password yet with a
1126         # wrong old password
1127         try:
1128             self.ldb.modify_ldif("""
1129 dn: """ + userdn + """
1130 changetype: modify
1131 delete: userPassword
1132 userPassword: noPassword
1133 add: userPassword
1134 userPassword: thatsAcomplPASS2
1135 """)
1136             self.fail()
1137         except LdbError as e19:
1138             (num, msg) = e19.args
1139             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
1140             # Windows (2008 at least) seems to have some small bug here: it
1141             # returns "0000056A" on longer (always wrong) previous passwords.
1142             self.assertTrue('00000056' in msg, msg)
1143
1144         res = self._check_account(userdn,
1145                                   badPwdCount=1,
1146                                   badPasswordTime=("greater", 0),
1147                                   logonCount=0,
1148                                   lastLogon=0,
1149                                   lastLogonTimestamp=('absent', None),
1150                                   userAccountControl=
1151                                     dsdb.UF_NORMAL_ACCOUNT |
1152                                     dsdb.UF_ACCOUNTDISABLE |
1153                                     dsdb.UF_PASSWD_NOTREQD,
1154                                   msDSUserAccountControlComputed=
1155                                     dsdb.UF_PASSWORD_EXPIRED)
1156         badPwdCount = int(res[0]["badPwdCount"][0])
1157         badPasswordTime = int(res[0]["badPasswordTime"][0])
1158
1159         # Sets the initial user password with a "special" password change
1160         # I think that this internally is a password set operation and it can
1161         # only be performed by someone which has password set privileges on the
1162         # account (at least in s4 we do handle it like that).
1163         self.ldb.modify_ldif("""
1164 dn: """ + userdn + """
1165 changetype: modify
1166 delete: userPassword
1167 add: userPassword
1168 userPassword: """ + userpass + """
1169 """)
1170
1171         res = self._check_account(userdn,
1172                                   badPwdCount=badPwdCount,
1173                                   badPasswordTime=badPasswordTime,
1174                                   logonCount=0,
1175                                   lastLogon=0,
1176                                   lastLogonTimestamp=('absent', None),
1177                                   userAccountControl=
1178                                     dsdb.UF_NORMAL_ACCOUNT |
1179                                     dsdb.UF_ACCOUNTDISABLE |
1180                                     dsdb.UF_PASSWD_NOTREQD,
1181                                   msDSUserAccountControlComputed=0)
1182
1183         # Enables the user account
1184         self.ldb.enable_account("(sAMAccountName=%s)" % username)
1185
1186         res = self._check_account(userdn,
1187                                   badPwdCount=badPwdCount,
1188                                   badPasswordTime=badPasswordTime,
1189                                   logonCount=0,
1190                                   lastLogon=0,
1191                                   lastLogonTimestamp=('absent', None),
1192                                   userAccountControl=
1193                                     dsdb.UF_NORMAL_ACCOUNT,
1194                                   msDSUserAccountControlComputed=0)
1195         if lockOutObservationWindow != 0:
1196             time.sleep(lockOutObservationWindow + 1)
1197             effective_bad_password_count = 0
1198         else:
1199             effective_bad_password_count = badPwdCount
1200
1201         res = self._check_account(userdn,
1202                                   badPwdCount=badPwdCount,
1203                                   effective_bad_password_count=effective_bad_password_count,
1204                                   badPasswordTime=badPasswordTime,
1205                                   logonCount=0,
1206                                   lastLogon=0,
1207                                   lastLogonTimestamp=('absent', None),
1208                                   userAccountControl=
1209                                     dsdb.UF_NORMAL_ACCOUNT,
1210                                   msDSUserAccountControlComputed=0)
1211
1212         ldb = SamDB(url=self.host_url, credentials=creds, lp=self.lp)
1213
1214         if lockOutObservationWindow == 0:
1215             badPwdCount = 0
1216             effective_bad_password_count = 0
1217         if use_kerberos == MUST_USE_KERBEROS:
1218             badPwdCount = 0
1219             effective_bad_password_count = 0
1220
1221         res = self._check_account(userdn,
1222                                   badPwdCount=badPwdCount,
1223                                   effective_bad_password_count=effective_bad_password_count,
1224                                   badPasswordTime=badPasswordTime,
1225                                   logonCount=(logoncount_relation, 0),
1226                                   lastLogon=(lastlogon_relation, 0),
1227                                   lastLogonTimestamp=('greater', badPasswordTime),
1228                                   userAccountControl=
1229                                     dsdb.UF_NORMAL_ACCOUNT,
1230                                   msDSUserAccountControlComputed=0)
1231
1232         logonCount = int(res[0]["logonCount"][0])
1233         lastLogon = int(res[0]["lastLogon"][0])
1234         lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
1235         if lastlogon_relation == 'greater':
1236             self.assertGreater(lastLogon, badPasswordTime)
1237             self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
1238
1239         res = self._check_account(userdn,
1240                                   badPwdCount=badPwdCount,
1241                                   effective_bad_password_count=effective_bad_password_count,
1242                                   badPasswordTime=badPasswordTime,
1243                                   logonCount=logonCount,
1244                                   lastLogon=lastLogon,
1245                                   lastLogonTimestamp=lastLogonTimestamp,
1246                                   userAccountControl=
1247                                     dsdb.UF_NORMAL_ACCOUNT,
1248                                   msDSUserAccountControlComputed=0)
1249         return ldb
1250
1251     def _reset_samr(self, res):
1252
1253         # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
1254         samr_user = self._open_samr_user(res)
1255         acb_info = self.samr.QueryUserInfo(samr_user, 16)
1256         acb_info.acct_flags &= ~samr.ACB_AUTOLOCK
1257         self.samr.SetUserInfo(samr_user, 16, acb_info)
1258         self.samr.Close(samr_user)
1259
1260     def test_lockout_observation_window(self):
1261         lockout3krb5_creds = self.insta_creds(self.template_creds,
1262                                               username="lockout3krb5",
1263                                               userpass="thatsAcomplPASS0",
1264                                               kerberos_state=MUST_USE_KERBEROS)
1265         self._testing_add_user(lockout3krb5_creds)
1266
1267         lockout4krb5_creds = self.insta_creds(self.template_creds,
1268                                               username="lockout4krb5",
1269                                               userpass="thatsAcomplPASS0",
1270                                               kerberos_state=MUST_USE_KERBEROS)
1271         self._testing_add_user(lockout4krb5_creds,
1272                                lockOutObservationWindow=self.lockout_observation_window)
1273
1274         lockout3ntlm_creds = self.insta_creds(self.template_creds,
1275                                               username="lockout3ntlm",
1276                                               userpass="thatsAcomplPASS0",
1277                                               kerberos_state=DONT_USE_KERBEROS)
1278         self._testing_add_user(lockout3ntlm_creds)
1279         lockout4ntlm_creds = self.insta_creds(self.template_creds,
1280                                               username="lockout4ntlm",
1281                                               userpass="thatsAcomplPASS0",
1282                                               kerberos_state=DONT_USE_KERBEROS)
1283         self._testing_add_user(lockout4ntlm_creds,
1284                                lockOutObservationWindow=self.lockout_observation_window)
1285
1286     def _test_samr_password_change(self, creds, other_creds, lockout_threshold=3):
1287         """Tests user lockout by using bad password in SAMR password_change"""
1288
1289         # create a connection for SAMR using another user's credentials
1290         lp = self.get_loadparm()
1291         net = Net(other_creds, lp, server=self.host)
1292
1293         # work out the initial account values for this user
1294         username = creds.get_username()
1295         userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
1296         res = self._check_account(userdn,
1297                                   badPwdCount=0,
1298                                   badPasswordTime=("greater", 0),
1299                                   badPwdCountOnly=True)
1300         badPasswordTime = int(res[0]["badPasswordTime"][0])
1301         logonCount = int(res[0]["logonCount"][0])
1302         lastLogon = int(res[0]["lastLogon"][0])
1303         lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
1304
1305         # prove we can change the user password (using the correct password)
1306         new_password = "thatsAcomplPASS2"
1307         net.change_password(newpassword=new_password.encode('utf-8'),
1308                             username=username,
1309                             oldpassword=creds.get_password())
1310         creds.set_password(new_password)
1311
1312         # try entering 'x' many bad passwords in a row to lock the user out
1313         new_password = "thatsAcomplPASS3"
1314         for i in range(lockout_threshold):
1315             badPwdCount = i + 1
1316             try:
1317                 print("Trying bad password, attempt #%u" % badPwdCount)
1318                 net.change_password(newpassword=new_password.encode('utf-8'),
1319                                     username=creds.get_username(),
1320                                     oldpassword="bad-password")
1321                 self.fail("Invalid SAMR change_password accepted")
1322             except NTSTATUSError as e:
1323                 enum = ctypes.c_uint32(e[0]).value
1324                 self.assertEquals(enum, ntstatus.NT_STATUS_WRONG_PASSWORD)
1325
1326             # check the status of the account is updated after each bad attempt
1327             account_flags = 0
1328             lockoutTime = None
1329             if badPwdCount >= lockout_threshold:
1330                 account_flags = dsdb.UF_LOCKOUT
1331                 lockoutTime = ("greater", badPasswordTime)
1332
1333             res = self._check_account(userdn,
1334                                       badPwdCount=badPwdCount,
1335                                       badPasswordTime=("greater", badPasswordTime),
1336                                       logonCount=logonCount,
1337                                       lastLogon=lastLogon,
1338                                       lastLogonTimestamp=lastLogonTimestamp,
1339                                       lockoutTime=lockoutTime,
1340                                       userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1341                                       msDSUserAccountControlComputed=account_flags)
1342             badPasswordTime = int(res[0]["badPasswordTime"][0])
1343
1344         # the user is now locked out
1345         lockoutTime = int(res[0]["lockoutTime"][0])
1346
1347         # check the user remains locked out regardless of whether they use a
1348         # good or a bad password now
1349         for password in (creds.get_password(), "bad-password"):
1350             try:
1351                 print("Trying password %s" % password)
1352                 net.change_password(newpassword=new_password.encode('utf-8'),
1353                                     username=creds.get_username(),
1354                                     oldpassword=password)
1355                 self.fail("Invalid SAMR change_password accepted")
1356             except NTSTATUSError as e:
1357                 enum = ctypes.c_uint32(e[0]).value
1358                 self.assertEquals(enum, ntstatus.NT_STATUS_ACCOUNT_LOCKED_OUT)
1359
1360             res = self._check_account(userdn,
1361                                       badPwdCount=lockout_threshold,
1362                                       badPasswordTime=badPasswordTime,
1363                                       logonCount=logonCount,
1364                                       lastLogon=lastLogon,
1365                                       lastLogonTimestamp=lastLogonTimestamp,
1366                                       lockoutTime=lockoutTime,
1367                                       userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1368                                       msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
1369
1370         # reset the user account lockout
1371         self._reset_samr(res)
1372
1373         # check bad password counts are reset
1374         res = self._check_account(userdn,
1375                                   badPwdCount=0,
1376                                   badPasswordTime=badPasswordTime,
1377                                   logonCount=logonCount,
1378                                   lockoutTime=0,
1379                                   lastLogon=lastLogon,
1380                                   lastLogonTimestamp=lastLogonTimestamp,
1381                                   userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1382                                   msDSUserAccountControlComputed=0)
1383
1384         # check we can change the user password successfully now
1385         net.change_password(newpassword=new_password.encode('utf-8'),
1386                             username=username,
1387                             oldpassword=creds.get_password())
1388         creds.set_password(new_password)
1389
1390     def test_samr_change_password(self):
1391         self._test_samr_password_change(self.lockout1ntlm_creds,
1392                                         other_creds=self.lockout2ntlm_creds)
1393
1394     # same as above, but use a PSO to enforce the lockout
1395     def test_pso_samr_change_password(self):
1396         self.use_pso_lockout_settings(self.lockout1ntlm_creds)
1397         self._test_samr_password_change(self.lockout1ntlm_creds,
1398                                         other_creds=self.lockout2ntlm_creds)
1399
1400 host_url = "ldap://%s" % host
1401
1402 TestProgram(module=__name__, opts=subunitopts)