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