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