2 # -*- coding: utf-8 -*-
3 # This tests the password lockout behavior for AD implementations
5 # Copyright Matthias Dieter Wallnoefer 2010
6 # Copyright Andrew Bartlett 2013
7 # Copyright Stefan Metzmacher 2014
10 from __future__ import print_function
16 sys.path.insert(0, "bin/python")
19 from samba.tests.subunitrun import TestProgram, SubunitOptions
21 import samba.getopt as options
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
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
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()
58 lp = sambaopts.get_loadparm()
59 global_creds = credopts.get_credentials(lp)
61 import password_lockout_base
68 class PasswordTests(password_lockout_base.BasePasswordTestCase):
71 self.host_url = host_url
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()
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)
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)
92 def _reset_ldap_lockoutTime(self, res):
93 self.ldb.modify_ldif("""
94 dn: """ + str(res[0].dn) + """
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])
104 uac = int(res[0]["userAccountControl"][0])
105 uacc = int(res[0]["msDS-User-Account-Control-Computed"][0])
108 uac = uac & ~dsdb.UF_LOCKOUT
110 self.ldb.modify_ldif("""
111 dn: """ + str(res[0].dn) + """
113 replace: userAccountControl
114 userAccountControl: %d
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)
125 self.assertTrue(False, msg="Invalid reset method[%s]" % method)
127 def _test_userPassword_lockout_with_clear_change(self, creds, other_ldb, method,
128 initial_lastlogon_relation=None):
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)
134 # Notice: This works only against Windows if "dSHeuristics" has been set
136 username = creds.get_username()
137 userpass = creds.get_password()
138 userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
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")
146 logoncount_relation = 'equal'
147 lastlogon_relation = 'equal'
148 print("Performs a password cleartext change operation on 'userPassword' using NTLMSSP")
150 if initial_lastlogon_relation is not None:
151 lastlogon_relation = initial_lastlogon_relation
153 res = self._check_account(userdn,
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)
169 # Change password on a connection as another user
173 other_ldb.modify_ldif("""
174 dn: """ + userdn + """
177 userPassword: thatsAcomplPASS1x
179 userPassword: thatsAcomplPASS2
182 except LdbError as e:
184 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
185 self.assertTrue('00000056' in msg, msg)
187 res = self._check_account(userdn,
189 badPasswordTime=("greater", badPasswordTime),
190 logonCount=logonCount,
192 lastLogonTimestamp=lastLogonTimestamp,
193 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
194 msDSUserAccountControlComputed=0)
195 badPasswordTime = int(res[0]["badPasswordTime"][0])
197 # Correct old password
198 other_ldb.modify_ldif("""
199 dn: """ + userdn + """
202 userPassword: """ + userpass + """
204 userPassword: thatsAcomplPASS2
207 res = self._check_account(userdn,
209 badPasswordTime=badPasswordTime,
210 logonCount=logonCount,
212 lastLogonTimestamp=lastLogonTimestamp,
213 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
214 msDSUserAccountControlComputed=0)
218 other_ldb.modify_ldif("""
219 dn: """ + userdn + """
222 userPassword: thatsAcomplPASS1x
224 userPassword: thatsAcomplPASS2
227 except LdbError as e1:
229 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
230 self.assertTrue('00000056' in msg, msg)
232 res = self._check_account(userdn,
234 badPasswordTime=("greater", badPasswordTime),
235 logonCount=logonCount,
237 lastLogonTimestamp=lastLogonTimestamp,
238 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
239 msDSUserAccountControlComputed=0)
240 badPasswordTime = int(res[0]["badPasswordTime"][0])
242 print("two failed password change")
246 other_ldb.modify_ldif("""
247 dn: """ + userdn + """
250 userPassword: thatsAcomplPASS1x
252 userPassword: thatsAcomplPASS2
255 except LdbError as e2:
257 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
258 self.assertTrue('00000056' in msg, msg)
260 res = self._check_account(userdn,
262 badPasswordTime=("greater", badPasswordTime),
263 logonCount=logonCount,
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])
274 other_ldb.modify_ldif("""
275 dn: """ + userdn + """
278 userPassword: thatsAcomplPASS1x
280 userPassword: thatsAcomplPASS2
283 except LdbError as e3:
285 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
286 self.assertTrue('00000775' in msg, msg)
288 res = self._check_account(userdn,
290 badPasswordTime=badPasswordTime,
291 logonCount=logonCount,
293 lastLogonTimestamp=lastLogonTimestamp,
294 lockoutTime=lockoutTime,
295 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
296 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
300 other_ldb.modify_ldif("""
301 dn: """ + userdn + """
304 userPassword: thatsAcomplPASS1x
306 userPassword: thatsAcomplPASS2
309 except LdbError as e4:
311 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
312 self.assertTrue('00000775' in msg, msg)
314 res = self._check_account(userdn,
316 badPasswordTime=badPasswordTime,
317 logonCount=logonCount,
318 lockoutTime=lockoutTime,
320 lastLogonTimestamp=lastLogonTimestamp,
321 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
322 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
325 # Correct old password
326 other_ldb.modify_ldif("""
327 dn: """ + userdn + """
330 userPassword: thatsAcomplPASS2
332 userPassword: thatsAcomplPASS2x
335 except LdbError as e5:
337 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
338 self.assertTrue('00000775' in msg, msg)
340 res = self._check_account(userdn,
342 badPasswordTime=badPasswordTime,
343 logonCount=logonCount,
345 lastLogonTimestamp=lastLogonTimestamp,
346 lockoutTime=lockoutTime,
347 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
348 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
350 # Now reset the password, which does NOT change the lockout!
351 self.ldb.modify_ldif("""
352 dn: """ + userdn + """
354 replace: userPassword
355 userPassword: thatsAcomplPASS2
358 res = self._check_account(userdn,
360 badPasswordTime=badPasswordTime,
361 logonCount=logonCount,
363 lastLogonTimestamp=lastLogonTimestamp,
364 lockoutTime=lockoutTime,
365 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
366 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
369 # Correct old password
370 other_ldb.modify_ldif("""
371 dn: """ + userdn + """
374 userPassword: thatsAcomplPASS2
376 userPassword: thatsAcomplPASS2x
379 except LdbError as e6:
381 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
382 self.assertTrue('00000775' in msg, msg)
384 res = self._check_account(userdn,
386 badPasswordTime=badPasswordTime,
387 logonCount=logonCount,
389 lastLogonTimestamp=lastLogonTimestamp,
390 lockoutTime=lockoutTime,
391 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
392 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
395 m.dn = Dn(self.ldb, userdn)
396 m["userAccountControl"] = MessageElement(
397 str(dsdb.UF_LOCKOUT),
398 FLAG_MOD_REPLACE, "userAccountControl")
402 # This shows that setting the UF_LOCKOUT flag alone makes no difference
403 res = self._check_account(userdn,
405 badPasswordTime=badPasswordTime,
406 logonCount=logonCount,
408 lastLogonTimestamp=lastLogonTimestamp,
409 lockoutTime=lockoutTime,
410 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
411 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
413 # This shows that setting the UF_LOCKOUT flag makes no difference
415 # Correct old password
416 other_ldb.modify_ldif("""
417 dn: """ + userdn + """
420 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
422 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2x\"".encode('utf-16-le')).decode('utf8') + """
425 except LdbError as e7:
427 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
428 self.assertTrue('00000775' in msg, msg)
430 res = self._check_account(userdn,
432 badPasswordTime=badPasswordTime,
433 logonCount=logonCount,
434 lockoutTime=lockoutTime,
436 lastLogonTimestamp=lastLogonTimestamp,
437 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
438 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
440 self._reset_by_method(res, method)
442 # Here bad password counts are reset without logon success.
443 res = self._check_account(userdn,
445 badPasswordTime=badPasswordTime,
446 logonCount=logonCount,
449 lastLogonTimestamp=lastLogonTimestamp,
450 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
451 msDSUserAccountControlComputed=0)
453 # The correct password after doing the unlock
455 other_ldb.modify_ldif("""
456 dn: """ + userdn + """
459 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
461 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2x\"".encode('utf-16-le')).decode('utf8') + """
463 userpass = "thatsAcomplPASS2x"
464 creds.set_password(userpass)
466 res = self._check_account(userdn,
468 badPasswordTime=badPasswordTime,
469 logonCount=logonCount,
472 lastLogonTimestamp=lastLogonTimestamp,
473 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
474 msDSUserAccountControlComputed=0)
478 other_ldb.modify_ldif("""
479 dn: """ + userdn + """
482 userPassword: thatsAcomplPASS1xyz
484 userPassword: thatsAcomplPASS2XYZ
487 except LdbError as e8:
489 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
490 self.assertTrue('00000056' in msg, msg)
492 res = self._check_account(userdn,
494 badPasswordTime=("greater", badPasswordTime),
495 logonCount=logonCount,
498 lastLogonTimestamp=lastLogonTimestamp,
499 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
500 msDSUserAccountControlComputed=0)
501 badPasswordTime = int(res[0]["badPasswordTime"][0])
505 other_ldb.modify_ldif("""
506 dn: """ + userdn + """
509 userPassword: thatsAcomplPASS1xyz
511 userPassword: thatsAcomplPASS2XYZ
514 except LdbError as e9:
516 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
517 self.assertTrue('00000056' in msg, msg)
519 res = self._check_account(userdn,
521 badPasswordTime=("greater", badPasswordTime),
522 logonCount=logonCount,
525 lastLogonTimestamp=lastLogonTimestamp,
526 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
527 msDSUserAccountControlComputed=0)
528 badPasswordTime = int(res[0]["badPasswordTime"][0])
530 self._reset_ldap_lockoutTime(res)
532 res = self._check_account(userdn,
534 badPasswordTime=badPasswordTime,
535 logonCount=logonCount,
537 lastLogonTimestamp=lastLogonTimestamp,
539 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
540 msDSUserAccountControlComputed=0)
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
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")
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,
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,
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')
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,
573 initial_lastlogon_relation='greater')
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,
579 initial_lastlogon_relation='greater')
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")
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,
593 initial_lastlogon_relation='greater')
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,
600 initial_lastlogon_relation='greater')
602 def use_pso_lockout_settings(self, creds):
604 # create a PSO with the lockout settings the test cases normally expect
606 # Some test cases sleep() for self.account_lockout_duration
607 pso = PasswordSettings("lockout-PSO", self.ldb, lockout_attempts=3,
608 lockout_duration=self.account_lockout_duration)
609 self.addCleanup(self.ldb.delete, pso.dn)
611 userdn = "cn=%s,cn=users,%s" % (creds.get_username(), self.base_dn)
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)
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
628 logoncount_relation = "greater"
630 res = self._check_account(userdn,
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)
645 # Change password on a connection as another user
649 other_ldb.modify_ldif("""
650 dn: """ + userdn + """
653 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1x\"".encode('utf-16-le')).decode('utf8') + """
655 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
658 except LdbError as e10:
659 (num, msg) = e10.args
660 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
661 self.assertTrue('00000056' in msg, msg)
663 res = self._check_account(userdn,
665 badPasswordTime=("greater", badPasswordTime),
666 logonCount=logonCount,
668 lastLogonTimestamp=lastLogonTimestamp,
669 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
670 msDSUserAccountControlComputed=0)
671 badPasswordTime = int(res[0]["badPasswordTime"][0])
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')
680 other_ldb.modify_ldif("""
681 dn: """ + userdn + """
684 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
686 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
689 res = self._check_account(userdn,
691 badPasswordTime=badPasswordTime,
692 logonCount=logonCount,
694 lastLogonTimestamp=lastLogonTimestamp,
695 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
696 msDSUserAccountControlComputed=0)
700 other_ldb.modify_ldif("""
701 dn: """ + userdn + """
704 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
706 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
709 except LdbError as e11:
710 (num, msg) = e11.args
711 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
712 self.assertTrue('00000056' in msg, msg)
714 res = self._check_account(userdn,
716 badPasswordTime=("greater", badPasswordTime),
717 logonCount=logonCount,
719 lastLogonTimestamp=lastLogonTimestamp,
720 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
721 msDSUserAccountControlComputed=0)
722 badPasswordTime = int(res[0]["badPasswordTime"][0])
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)
729 res = self._check_account(userdn,
731 badPasswordTime=badPasswordTime,
732 logonCount=logonCount,
734 lastLogonTimestamp=lastLogonTimestamp,
735 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
736 msDSUserAccountControlComputed=0)
738 print("two failed password change")
742 other_ldb.modify_ldif("""
743 dn: """ + userdn + """
746 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
748 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
751 except LdbError as e12:
752 (num, msg) = e12.args
753 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
754 self.assertTrue('00000056' in msg, msg)
756 # this is strange, why do we have lockoutTime=badPasswordTime here?
757 res = self._check_account(userdn,
759 badPasswordTime=("greater", badPasswordTime),
760 logonCount=logonCount,
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])
771 other_ldb.modify_ldif("""
772 dn: """ + userdn + """
775 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
777 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
780 except LdbError as e13:
781 (num, msg) = e13.args
782 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
783 self.assertTrue('00000775' in msg, msg)
785 res = self._check_account(userdn,
787 badPasswordTime=badPasswordTime,
788 logonCount=logonCount,
790 lastLogonTimestamp=lastLogonTimestamp,
791 lockoutTime=lockoutTime,
792 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
793 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
797 other_ldb.modify_ldif("""
798 dn: """ + userdn + """
801 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
803 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
806 except LdbError as e14:
807 (num, msg) = e14.args
808 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
809 self.assertTrue('00000775' in msg, msg)
811 res = self._check_account(userdn,
813 badPasswordTime=badPasswordTime,
814 logonCount=logonCount,
816 lastLogonTimestamp=lastLogonTimestamp,
817 lockoutTime=lockoutTime,
818 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
819 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
822 # Correct old password
823 other_ldb.modify_ldif("""
824 dn: """ + userdn + """
827 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
829 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
832 except LdbError as e15:
833 (num, msg) = e15.args
834 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
835 self.assertTrue('00000775' in msg, msg)
837 res = self._check_account(userdn,
839 badPasswordTime=badPasswordTime,
840 logonCount=logonCount,
842 lastLogonTimestamp=lastLogonTimestamp,
843 lockoutTime=lockoutTime,
844 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
845 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
847 # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
848 self._reset_samr(res)
850 res = self._check_account(userdn,
852 badPasswordTime=badPasswordTime,
853 logonCount=logonCount,
855 lastLogonTimestamp=lastLogonTimestamp,
857 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
858 msDSUserAccountControlComputed=0)
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')
867 other_ldb.modify_ldif("""
868 dn: """ + userdn + """
871 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
873 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
876 res = self._check_account(userdn,
878 badPasswordTime=badPasswordTime,
879 logonCount=logonCount,
881 lastLogonTimestamp=lastLogonTimestamp,
883 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
884 msDSUserAccountControlComputed=0)
888 other_ldb.modify_ldif("""
889 dn: """ + userdn + """
892 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
894 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
897 except LdbError as e16:
898 (num, msg) = e16.args
899 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
900 self.assertTrue('00000056' in msg, msg)
902 res = self._check_account(userdn,
904 badPasswordTime=("greater", badPasswordTime),
905 logonCount=logonCount,
907 lastLogonTimestamp=lastLogonTimestamp,
909 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
910 msDSUserAccountControlComputed=0)
911 badPasswordTime = int(res[0]["badPasswordTime"][0])
915 other_ldb.modify_ldif("""
916 dn: """ + userdn + """
919 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
921 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
924 except LdbError as e17:
925 (num, msg) = e17.args
926 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
927 self.assertTrue('00000056' in msg, msg)
929 res = self._check_account(userdn,
931 badPasswordTime=("greater", badPasswordTime),
932 logonCount=logonCount,
934 lastLogonTimestamp=lastLogonTimestamp,
936 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
937 msDSUserAccountControlComputed=0)
938 badPasswordTime = int(res[0]["badPasswordTime"][0])
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)
944 res = self._check_account(userdn,
946 badPasswordTime=badPasswordTime,
947 logonCount=logonCount,
949 lastLogonTimestamp=lastLogonTimestamp,
951 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
952 msDSUserAccountControlComputed=0)
956 other_ldb.modify_ldif("""
957 dn: """ + userdn + """
960 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
962 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
965 except LdbError as e18:
966 (num, msg) = e18.args
967 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
968 self.assertTrue('00000056' in msg, msg)
970 res = self._check_account(userdn,
972 badPasswordTime=("greater", badPasswordTime),
973 logonCount=logonCount,
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])
982 time.sleep(self.account_lockout_duration + 1)
984 res = self._check_account(userdn,
985 badPwdCount=3, effective_bad_password_count=0,
986 badPasswordTime=badPasswordTime,
987 logonCount=logonCount,
989 lastLogonTimestamp=lastLogonTimestamp,
990 lockoutTime=lockoutTime,
991 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
992 msDSUserAccountControlComputed=0)
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)
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)
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)
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")
1018 def test_login_lockout_krb5(self):
1019 self._test_login_lockout(self.lockout1krb5_creds)
1021 def test_login_lockout_ntlm(self):
1022 self._test_login_lockout(self.lockout1ntlm_creds)
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)
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)
1035 def test_multiple_logon_krb5(self):
1036 self._test_multiple_logon(self.lockout1krb5_creds)
1038 def test_multiple_logon_ntlm(self):
1039 self._test_multiple_logon(self.lockout1ntlm_creds)
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)
1046 use_kerberos = creds.get_kerberos_state()
1047 if use_kerberos == MUST_USE_KERBEROS:
1048 logoncount_relation = 'greater'
1049 lastlogon_relation = 'greater'
1051 logoncount_relation = 'equal'
1052 if lockOutObservationWindow == 0:
1053 lastlogon_relation = 'greater'
1055 lastlogon_relation = 'equal'
1057 delete_force(self.ldb, userdn)
1060 "objectclass": "user",
1061 "sAMAccountName": username})
1063 self.addCleanup(delete_force, self.ldb, userdn)
1065 res = self._check_account(userdn,
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)
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)
1080 res = self._check_account(userdn,
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)
1091 # Tests a password change when we don't have any password yet with a
1092 # wrong old password
1094 self.ldb.modify_ldif("""
1095 dn: """ + userdn + """
1097 delete: userPassword
1098 userPassword: noPassword
1100 userPassword: thatsAcomplPASS2
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)
1110 res = self._check_account(userdn,
1112 badPasswordTime=("greater", 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])
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 + """
1130 delete: userPassword
1132 userPassword: """ + userpass + """
1135 res = self._check_account(userdn,
1136 badPwdCount=badPwdCount,
1137 badPasswordTime=badPasswordTime,
1140 lastLogonTimestamp=('absent', None),
1141 userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
1142 dsdb.UF_ACCOUNTDISABLE |
1143 dsdb.UF_PASSWD_NOTREQD),
1144 msDSUserAccountControlComputed=0)
1146 # Enables the user account
1147 self.ldb.enable_account("(sAMAccountName=%s)" % username)
1149 res = self._check_account(userdn,
1150 badPwdCount=badPwdCount,
1151 badPasswordTime=badPasswordTime,
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
1161 effective_bad_password_count = badPwdCount
1163 res = self._check_account(userdn,
1164 badPwdCount=badPwdCount,
1165 effective_bad_password_count=effective_bad_password_count,
1166 badPasswordTime=badPasswordTime,
1169 lastLogonTimestamp=('absent', None),
1170 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1171 msDSUserAccountControlComputed=0)
1173 ldb = SamDB(url=self.host_url, credentials=creds, lp=self.lp)
1175 if lockOutObservationWindow == 0:
1177 effective_bad_password_count = 0
1178 if use_kerberos == MUST_USE_KERBEROS:
1180 effective_bad_password_count = 0
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)
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)
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)
1210 def _reset_samr(self, res):
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)
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)
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)
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)
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"""
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)
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,
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])
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'),
1268 oldpassword=creds.get_password())
1269 creds.set_password(new_password)
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):
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)
1285 # check the status of the account is updated after each bad attempt
1288 if badPwdCount >= lockout_threshold:
1289 account_flags = dsdb.UF_LOCKOUT
1290 lockoutTime = ("greater", badPasswordTime)
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])
1303 # the user is now locked out
1304 lockoutTime = int(res[0]["lockoutTime"][0])
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"):
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)
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)
1329 # reset the user account lockout
1330 self._reset_samr(res)
1332 # check bad password counts are reset
1333 res = self._check_account(userdn,
1335 badPasswordTime=badPasswordTime,
1336 logonCount=logonCount,
1338 lastLogon=lastLogon,
1339 lastLogonTimestamp=lastLogonTimestamp,
1340 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1341 msDSUserAccountControlComputed=0)
1343 # check we can change the user password successfully now
1344 net.change_password(newpassword=new_password.encode('utf-8'),
1346 oldpassword=creds.get_password())
1347 creds.set_password(new_password)
1349 def test_samr_change_password(self):
1350 self._test_samr_password_change(self.lockout1ntlm_creds,
1351 other_creds=self.lockout2ntlm_creds)
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)
1360 host_url = "ldap://%s" % host
1362 TestProgram(module=__name__, opts=subunitopts)