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
67 class PasswordTests(password_lockout_base.BasePasswordTestCase):
70 self.host_url = host_url
72 self.global_creds = global_creds
73 self.ldb = SamDB(url=self.host_url, session_info=system_session(self.lp),
74 credentials=self.global_creds, lp=self.lp)
75 super(PasswordTests, self).setUp()
77 self.lockout2krb5_creds = self.insta_creds(self.template_creds,
78 username="lockout2krb5",
79 userpass="thatsAcomplPASS0",
80 kerberos_state=MUST_USE_KERBEROS)
81 self.lockout2krb5_ldb = self._readd_user(self.lockout2krb5_creds,
82 lockOutObservationWindow=self.lockout_observation_window)
84 self.lockout2ntlm_creds = self.insta_creds(self.template_creds,
85 username="lockout2ntlm",
86 userpass="thatsAcomplPASS0",
87 kerberos_state=DONT_USE_KERBEROS)
88 self.lockout2ntlm_ldb = self._readd_user(self.lockout2ntlm_creds,
89 lockOutObservationWindow=self.lockout_observation_window)
91 def _reset_ldap_lockoutTime(self, res):
92 self.ldb.modify_ldif("""
93 dn: """ + str(res[0].dn) + """
99 def _reset_ldap_userAccountControl(self, res):
100 self.assertTrue("userAccountControl" in res[0])
101 self.assertTrue("msDS-User-Account-Control-Computed" in res[0])
103 uac = int(res[0]["userAccountControl"][0])
104 uacc = int(res[0]["msDS-User-Account-Control-Computed"][0])
107 uac = uac & ~dsdb.UF_LOCKOUT
109 self.ldb.modify_ldif("""
110 dn: """ + str(res[0].dn) + """
112 replace: userAccountControl
113 userAccountControl: %d
116 def _reset_by_method(self, res, method):
117 if method is "ldap_userAccountControl":
118 self._reset_ldap_userAccountControl(res)
119 elif method is "ldap_lockoutTime":
120 self._reset_ldap_lockoutTime(res)
121 elif method is "samr":
122 self._reset_samr(res)
124 self.assertTrue(False, msg="Invalid reset method[%s]" % method)
126 def _test_userPassword_lockout_with_clear_change(self, creds, other_ldb, method,
127 initial_lastlogon_relation=None):
129 Tests user lockout behaviour when we try to change the user's password
130 but specify an incorrect old-password. The method parameter specifies
131 how to reset the locked out account (e.g. by resetting lockoutTime)
133 # Notice: This works only against Windows if "dSHeuristics" has been set
135 username = creds.get_username()
136 userpass = creds.get_password()
137 userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
139 use_kerberos = creds.get_kerberos_state()
140 if use_kerberos == MUST_USE_KERBEROS:
141 logoncount_relation = 'greater'
142 lastlogon_relation = 'greater'
143 print("Performs a password cleartext change operation on 'userPassword' using Kerberos")
145 logoncount_relation = 'equal'
146 lastlogon_relation = 'equal'
147 print("Performs a password cleartext change operation on 'userPassword' using NTLMSSP")
149 if initial_lastlogon_relation is not None:
150 lastlogon_relation = initial_lastlogon_relation
152 res = self._check_account(userdn,
154 badPasswordTime=("greater", 0),
155 logonCount=(logoncount_relation, 0),
156 lastLogon=(lastlogon_relation, 0),
157 lastLogonTimestamp=('greater', 0),
159 dsdb.UF_NORMAL_ACCOUNT,
160 msDSUserAccountControlComputed=0)
161 badPasswordTime = int(res[0]["badPasswordTime"][0])
162 logonCount = int(res[0]["logonCount"][0])
163 lastLogon = int(res[0]["lastLogon"][0])
164 lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
165 if lastlogon_relation == 'greater':
166 self.assertGreater(lastLogon, badPasswordTime)
167 self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
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,
194 dsdb.UF_NORMAL_ACCOUNT,
195 msDSUserAccountControlComputed=0)
196 badPasswordTime = int(res[0]["badPasswordTime"][0])
198 # Correct old password
199 other_ldb.modify_ldif("""
200 dn: """ + userdn + """
203 userPassword: """ + userpass + """
205 userPassword: thatsAcomplPASS2
208 res = self._check_account(userdn,
210 badPasswordTime=badPasswordTime,
211 logonCount=logonCount,
213 lastLogonTimestamp=lastLogonTimestamp,
215 dsdb.UF_NORMAL_ACCOUNT,
216 msDSUserAccountControlComputed=0)
220 other_ldb.modify_ldif("""
221 dn: """ + userdn + """
224 userPassword: thatsAcomplPASS1x
226 userPassword: thatsAcomplPASS2
229 except LdbError as e1:
231 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
232 self.assertTrue('00000056' in msg, msg)
234 res = self._check_account(userdn,
236 badPasswordTime=("greater", badPasswordTime),
237 logonCount=logonCount,
239 lastLogonTimestamp=lastLogonTimestamp,
241 dsdb.UF_NORMAL_ACCOUNT,
242 msDSUserAccountControlComputed=0)
243 badPasswordTime = int(res[0]["badPasswordTime"][0])
245 print("two failed password change")
249 other_ldb.modify_ldif("""
250 dn: """ + userdn + """
253 userPassword: thatsAcomplPASS1x
255 userPassword: thatsAcomplPASS2
258 except LdbError as e2:
260 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
261 self.assertTrue('00000056' in msg, msg)
263 res = self._check_account(userdn,
265 badPasswordTime=("greater", badPasswordTime),
266 logonCount=logonCount,
268 lastLogonTimestamp=lastLogonTimestamp,
269 lockoutTime=("greater", badPasswordTime),
271 dsdb.UF_NORMAL_ACCOUNT,
272 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
273 badPasswordTime = int(res[0]["badPasswordTime"][0])
274 lockoutTime = int(res[0]["lockoutTime"][0])
278 other_ldb.modify_ldif("""
279 dn: """ + userdn + """
282 userPassword: thatsAcomplPASS1x
284 userPassword: thatsAcomplPASS2
287 except LdbError as e3:
289 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
290 self.assertTrue('00000775' in msg, msg)
292 res = self._check_account(userdn,
294 badPasswordTime=badPasswordTime,
295 logonCount=logonCount,
297 lastLogonTimestamp=lastLogonTimestamp,
298 lockoutTime=lockoutTime,
300 dsdb.UF_NORMAL_ACCOUNT,
301 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
305 other_ldb.modify_ldif("""
306 dn: """ + userdn + """
309 userPassword: thatsAcomplPASS1x
311 userPassword: thatsAcomplPASS2
314 except LdbError as e4:
316 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
317 self.assertTrue('00000775' in msg, msg)
319 res = self._check_account(userdn,
321 badPasswordTime=badPasswordTime,
322 logonCount=logonCount,
323 lockoutTime=lockoutTime,
325 lastLogonTimestamp=lastLogonTimestamp,
327 dsdb.UF_NORMAL_ACCOUNT,
328 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
331 # Correct old password
332 other_ldb.modify_ldif("""
333 dn: """ + userdn + """
336 userPassword: thatsAcomplPASS2
338 userPassword: thatsAcomplPASS2x
341 except LdbError as e5:
343 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
344 self.assertTrue('00000775' in msg, msg)
346 res = self._check_account(userdn,
348 badPasswordTime=badPasswordTime,
349 logonCount=logonCount,
351 lastLogonTimestamp=lastLogonTimestamp,
352 lockoutTime=lockoutTime,
354 dsdb.UF_NORMAL_ACCOUNT,
355 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
357 # Now reset the password, which does NOT change the lockout!
358 self.ldb.modify_ldif("""
359 dn: """ + userdn + """
361 replace: userPassword
362 userPassword: thatsAcomplPASS2
365 res = self._check_account(userdn,
367 badPasswordTime=badPasswordTime,
368 logonCount=logonCount,
370 lastLogonTimestamp=lastLogonTimestamp,
371 lockoutTime=lockoutTime,
373 dsdb.UF_NORMAL_ACCOUNT,
374 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
377 # Correct old password
378 other_ldb.modify_ldif("""
379 dn: """ + userdn + """
382 userPassword: thatsAcomplPASS2
384 userPassword: thatsAcomplPASS2x
387 except LdbError as e6:
389 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
390 self.assertTrue('00000775' in msg, msg)
392 res = self._check_account(userdn,
394 badPasswordTime=badPasswordTime,
395 logonCount=logonCount,
397 lastLogonTimestamp=lastLogonTimestamp,
398 lockoutTime=lockoutTime,
400 dsdb.UF_NORMAL_ACCOUNT,
401 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
404 m.dn = Dn(self.ldb, userdn)
405 m["userAccountControl"] = MessageElement(
406 str(dsdb.UF_LOCKOUT),
407 FLAG_MOD_REPLACE, "userAccountControl")
411 # This shows that setting the UF_LOCKOUT flag alone makes no difference
412 res = self._check_account(userdn,
414 badPasswordTime=badPasswordTime,
415 logonCount=logonCount,
417 lastLogonTimestamp=lastLogonTimestamp,
418 lockoutTime=lockoutTime,
420 dsdb.UF_NORMAL_ACCOUNT,
421 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
423 # This shows that setting the UF_LOCKOUT flag makes no difference
425 # Correct old password
426 other_ldb.modify_ldif("""
427 dn: """ + userdn + """
430 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
432 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2x\"".encode('utf-16-le')).decode('utf8') + """
435 except LdbError as e7:
437 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
438 self.assertTrue('00000775' in msg, msg)
440 res = self._check_account(userdn,
442 badPasswordTime=badPasswordTime,
443 logonCount=logonCount,
444 lockoutTime=lockoutTime,
446 lastLogonTimestamp=lastLogonTimestamp,
448 dsdb.UF_NORMAL_ACCOUNT,
449 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
451 self._reset_by_method(res, method)
453 # Here bad password counts are reset without logon success.
454 res = self._check_account(userdn,
456 badPasswordTime=badPasswordTime,
457 logonCount=logonCount,
460 lastLogonTimestamp=lastLogonTimestamp,
462 dsdb.UF_NORMAL_ACCOUNT,
463 msDSUserAccountControlComputed=0)
465 # The correct password after doing the unlock
467 other_ldb.modify_ldif("""
468 dn: """ + userdn + """
471 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
473 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2x\"".encode('utf-16-le')).decode('utf8') + """
475 userpass = "thatsAcomplPASS2x"
476 creds.set_password(userpass)
478 res = self._check_account(userdn,
480 badPasswordTime=badPasswordTime,
481 logonCount=logonCount,
484 lastLogonTimestamp=lastLogonTimestamp,
486 dsdb.UF_NORMAL_ACCOUNT,
487 msDSUserAccountControlComputed=0)
491 other_ldb.modify_ldif("""
492 dn: """ + userdn + """
495 userPassword: thatsAcomplPASS1xyz
497 userPassword: thatsAcomplPASS2XYZ
500 except LdbError as e8:
502 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
503 self.assertTrue('00000056' in msg, msg)
505 res = self._check_account(userdn,
507 badPasswordTime=("greater", badPasswordTime),
508 logonCount=logonCount,
511 lastLogonTimestamp=lastLogonTimestamp,
513 dsdb.UF_NORMAL_ACCOUNT,
514 msDSUserAccountControlComputed=0)
515 badPasswordTime = int(res[0]["badPasswordTime"][0])
519 other_ldb.modify_ldif("""
520 dn: """ + userdn + """
523 userPassword: thatsAcomplPASS1xyz
525 userPassword: thatsAcomplPASS2XYZ
528 except LdbError as e9:
530 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
531 self.assertTrue('00000056' in msg, msg)
533 res = self._check_account(userdn,
535 badPasswordTime=("greater", badPasswordTime),
536 logonCount=logonCount,
539 lastLogonTimestamp=lastLogonTimestamp,
541 dsdb.UF_NORMAL_ACCOUNT,
542 msDSUserAccountControlComputed=0)
543 badPasswordTime = int(res[0]["badPasswordTime"][0])
545 self._reset_ldap_lockoutTime(res)
547 res = self._check_account(userdn,
549 badPasswordTime=badPasswordTime,
550 logonCount=logonCount,
552 lastLogonTimestamp=lastLogonTimestamp,
555 dsdb.UF_NORMAL_ACCOUNT,
556 msDSUserAccountControlComputed=0)
558 # The following test lockout behaviour when modifying a user's password
559 # and specifying an invalid old password. There are variants for both
560 # NTLM and kerberos user authentication. As well as that, there are 3 ways
561 # to reset the locked out account: by clearing the lockout bit for
562 # userAccountControl (via LDAP), resetting it via SAMR, and by resetting
564 def test_userPassword_lockout_with_clear_change_krb5_ldap_userAccountControl(self):
565 self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
566 self.lockout2krb5_ldb,
567 "ldap_userAccountControl")
569 def test_userPassword_lockout_with_clear_change_krb5_ldap_lockoutTime(self):
570 self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
571 self.lockout2krb5_ldb,
574 def test_userPassword_lockout_with_clear_change_krb5_samr(self):
575 self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
576 self.lockout2krb5_ldb,
579 def test_userPassword_lockout_with_clear_change_ntlm_ldap_userAccountControl(self):
580 self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
581 self.lockout2ntlm_ldb,
582 "ldap_userAccountControl",
583 initial_lastlogon_relation='greater')
585 def test_userPassword_lockout_with_clear_change_ntlm_ldap_lockoutTime(self):
586 self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
587 self.lockout2ntlm_ldb,
589 initial_lastlogon_relation='greater')
591 def test_userPassword_lockout_with_clear_change_ntlm_samr(self):
592 self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
593 self.lockout2ntlm_ldb,
595 initial_lastlogon_relation='greater')
597 # For PSOs, just test a selection of the above combinations
598 def test_pso_userPassword_lockout_with_clear_change_krb5_ldap_userAccountControl(self):
599 self.use_pso_lockout_settings(self.lockout1krb5_creds)
600 self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
601 self.lockout2krb5_ldb,
602 "ldap_userAccountControl")
604 def test_pso_userPassword_lockout_with_clear_change_ntlm_ldap_lockoutTime(self):
605 self.use_pso_lockout_settings(self.lockout1ntlm_creds)
606 self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
607 self.lockout2ntlm_ldb,
609 initial_lastlogon_relation='greater')
611 def test_pso_userPassword_lockout_with_clear_change_ntlm_samr(self):
612 self.use_pso_lockout_settings(self.lockout1ntlm_creds)
613 self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
614 self.lockout2ntlm_ldb,
616 initial_lastlogon_relation='greater')
618 def use_pso_lockout_settings(self, creds):
619 # create a PSO with the lockout settings the test cases normally expect
620 pso = PasswordSettings("lockout-PSO", self.ldb, lockout_attempts=3,
622 self.addCleanup(self.ldb.delete, pso.dn)
624 userdn = "cn=%s,cn=users,%s" % (creds.get_username(), self.base_dn)
627 # update the global lockout settings to be wildly different to what
628 # the test cases normally expect
629 self.update_lockout_settings(threshold=10, duration=600,
630 observation_window=600)
632 def _test_unicodePwd_lockout_with_clear_change(self, creds, other_ldb,
633 initial_logoncount_relation=None):
634 print("Performs a password cleartext change operation on 'unicodePwd'")
635 username = creds.get_username()
636 userpass = creds.get_password()
637 userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
638 if initial_logoncount_relation is not None:
639 logoncount_relation = initial_logoncount_relation
641 logoncount_relation = "greater"
643 res = self._check_account(userdn,
645 badPasswordTime=("greater", 0),
646 logonCount=(logoncount_relation, 0),
647 lastLogon=("greater", 0),
648 lastLogonTimestamp=("greater", 0),
650 dsdb.UF_NORMAL_ACCOUNT,
651 msDSUserAccountControlComputed=0)
652 badPasswordTime = int(res[0]["badPasswordTime"][0])
653 logonCount = int(res[0]["logonCount"][0])
654 lastLogon = int(res[0]["lastLogon"][0])
655 lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
656 self.assertGreater(lastLogonTimestamp, badPasswordTime)
657 self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
659 # Change password on a connection as another user
663 other_ldb.modify_ldif("""
664 dn: """ + userdn + """
667 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1x\"".encode('utf-16-le')).decode('utf8') + """
669 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
672 except LdbError as e10:
673 (num, msg) = e10.args
674 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
675 self.assertTrue('00000056' in msg, msg)
677 res = self._check_account(userdn,
679 badPasswordTime=("greater", badPasswordTime),
680 logonCount=logonCount,
682 lastLogonTimestamp=lastLogonTimestamp,
684 dsdb.UF_NORMAL_ACCOUNT,
685 msDSUserAccountControlComputed=0)
686 badPasswordTime = int(res[0]["badPasswordTime"][0])
688 # Correct old password
689 old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
690 invalid_utf16 = "\"thatsAcomplPASSX\"".encode('utf-16-le')
691 userpass = "thatsAcomplPASS2"
692 creds.set_password(userpass)
693 new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
695 other_ldb.modify_ldif("""
696 dn: """ + userdn + """
699 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
701 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
704 res = self._check_account(userdn,
706 badPasswordTime=badPasswordTime,
707 logonCount=logonCount,
709 lastLogonTimestamp=lastLogonTimestamp,
711 dsdb.UF_NORMAL_ACCOUNT,
712 msDSUserAccountControlComputed=0)
716 other_ldb.modify_ldif("""
717 dn: """ + userdn + """
720 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
722 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
725 except LdbError as e11:
726 (num, msg) = e11.args
727 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
728 self.assertTrue('00000056' in msg, msg)
730 res = self._check_account(userdn,
732 badPasswordTime=("greater", badPasswordTime),
733 logonCount=logonCount,
735 lastLogonTimestamp=lastLogonTimestamp,
737 dsdb.UF_NORMAL_ACCOUNT,
738 msDSUserAccountControlComputed=0)
739 badPasswordTime = int(res[0]["badPasswordTime"][0])
741 # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
742 # It doesn't create "lockoutTime" = 0 and doesn't
743 # reset "badPwdCount" = 0.
744 self._reset_samr(res)
746 res = self._check_account(userdn,
748 badPasswordTime=badPasswordTime,
749 logonCount=logonCount,
751 lastLogonTimestamp=lastLogonTimestamp,
753 dsdb.UF_NORMAL_ACCOUNT,
754 msDSUserAccountControlComputed=0)
756 print("two failed password change")
760 other_ldb.modify_ldif("""
761 dn: """ + userdn + """
764 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
766 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
769 except LdbError as e12:
770 (num, msg) = e12.args
771 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
772 self.assertTrue('00000056' in msg, msg)
774 # this is strange, why do we have lockoutTime=badPasswordTime here?
775 res = self._check_account(userdn,
777 badPasswordTime=("greater", badPasswordTime),
778 logonCount=logonCount,
780 lastLogonTimestamp=lastLogonTimestamp,
781 lockoutTime=("greater", badPasswordTime),
783 dsdb.UF_NORMAL_ACCOUNT,
784 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
785 badPasswordTime = int(res[0]["badPasswordTime"][0])
786 lockoutTime = int(res[0]["lockoutTime"][0])
790 other_ldb.modify_ldif("""
791 dn: """ + userdn + """
794 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
796 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
799 except LdbError as e13:
800 (num, msg) = e13.args
801 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
802 self.assertTrue('00000775' in msg, msg)
804 res = self._check_account(userdn,
806 badPasswordTime=badPasswordTime,
807 logonCount=logonCount,
809 lastLogonTimestamp=lastLogonTimestamp,
810 lockoutTime=lockoutTime,
812 dsdb.UF_NORMAL_ACCOUNT,
813 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
817 other_ldb.modify_ldif("""
818 dn: """ + userdn + """
821 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
823 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
826 except LdbError as e14:
827 (num, msg) = e14.args
828 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
829 self.assertTrue('00000775' in msg, msg)
831 res = self._check_account(userdn,
833 badPasswordTime=badPasswordTime,
834 logonCount=logonCount,
836 lastLogonTimestamp=lastLogonTimestamp,
837 lockoutTime=lockoutTime,
839 dsdb.UF_NORMAL_ACCOUNT,
840 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
843 # Correct old password
844 other_ldb.modify_ldif("""
845 dn: """ + userdn + """
848 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
850 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
853 except LdbError as e15:
854 (num, msg) = e15.args
855 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
856 self.assertTrue('00000775' in msg, msg)
858 res = self._check_account(userdn,
860 badPasswordTime=badPasswordTime,
861 logonCount=logonCount,
863 lastLogonTimestamp=lastLogonTimestamp,
864 lockoutTime=lockoutTime,
866 dsdb.UF_NORMAL_ACCOUNT,
867 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
869 # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
870 self._reset_samr(res);
872 res = self._check_account(userdn,
874 badPasswordTime=badPasswordTime,
875 logonCount=logonCount,
877 lastLogonTimestamp=lastLogonTimestamp,
880 dsdb.UF_NORMAL_ACCOUNT,
881 msDSUserAccountControlComputed=0)
883 # Correct old password
884 old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
885 invalid_utf16 = "\"thatsAcomplPASSiX\"".encode('utf-16-le')
886 userpass = "thatsAcomplPASS2x"
887 creds.set_password(userpass)
888 new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
890 other_ldb.modify_ldif("""
891 dn: """ + userdn + """
894 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
896 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
899 res = self._check_account(userdn,
901 badPasswordTime=badPasswordTime,
902 logonCount=logonCount,
904 lastLogonTimestamp=lastLogonTimestamp,
907 dsdb.UF_NORMAL_ACCOUNT,
908 msDSUserAccountControlComputed=0)
912 other_ldb.modify_ldif("""
913 dn: """ + userdn + """
916 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
918 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
921 except LdbError as e16:
922 (num, msg) = e16.args
923 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
924 self.assertTrue('00000056' in msg, msg)
926 res = self._check_account(userdn,
928 badPasswordTime=("greater", badPasswordTime),
929 logonCount=logonCount,
931 lastLogonTimestamp=lastLogonTimestamp,
934 dsdb.UF_NORMAL_ACCOUNT,
935 msDSUserAccountControlComputed=0)
936 badPasswordTime = int(res[0]["badPasswordTime"][0])
940 other_ldb.modify_ldif("""
941 dn: """ + userdn + """
944 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
946 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
949 except LdbError as e17:
950 (num, msg) = e17.args
951 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
952 self.assertTrue('00000056' in msg, msg)
954 res = self._check_account(userdn,
956 badPasswordTime=("greater", badPasswordTime),
957 logonCount=logonCount,
959 lastLogonTimestamp=lastLogonTimestamp,
962 dsdb.UF_NORMAL_ACCOUNT,
963 msDSUserAccountControlComputed=0)
964 badPasswordTime = int(res[0]["badPasswordTime"][0])
966 # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
967 # It doesn't reset "badPwdCount" = 0.
968 self._reset_samr(res)
970 res = self._check_account(userdn,
972 badPasswordTime=badPasswordTime,
973 logonCount=logonCount,
975 lastLogonTimestamp=lastLogonTimestamp,
978 dsdb.UF_NORMAL_ACCOUNT,
979 msDSUserAccountControlComputed=0)
983 other_ldb.modify_ldif("""
984 dn: """ + userdn + """
987 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
989 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
992 except LdbError as e18:
993 (num, msg) = e18.args
994 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
995 self.assertTrue('00000056' in msg, msg)
997 res = self._check_account(userdn,
999 badPasswordTime=("greater", badPasswordTime),
1000 logonCount=logonCount,
1001 lastLogon=lastLogon,
1002 lastLogonTimestamp=lastLogonTimestamp,
1003 lockoutTime=("greater", badPasswordTime),
1005 dsdb.UF_NORMAL_ACCOUNT,
1006 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
1007 badPasswordTime = int(res[0]["badPasswordTime"][0])
1008 lockoutTime = int(res[0]["lockoutTime"][0])
1010 time.sleep(self.account_lockout_duration + 1)
1012 res = self._check_account(userdn,
1013 badPwdCount=3, effective_bad_password_count=0,
1014 badPasswordTime=badPasswordTime,
1015 logonCount=logonCount,
1016 lastLogon=lastLogon,
1017 lastLogonTimestamp=lastLogonTimestamp,
1018 lockoutTime=lockoutTime,
1020 dsdb.UF_NORMAL_ACCOUNT,
1021 msDSUserAccountControlComputed=0)
1023 # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
1024 # It doesn't reset "lockoutTime" = 0 and doesn't
1025 # reset "badPwdCount" = 0.
1026 self._reset_samr(res)
1028 res = self._check_account(userdn,
1029 badPwdCount=3, effective_bad_password_count=0,
1030 badPasswordTime=badPasswordTime,
1031 logonCount=logonCount,
1032 lockoutTime=lockoutTime,
1033 lastLogon=lastLogon,
1034 lastLogonTimestamp=lastLogonTimestamp,
1036 dsdb.UF_NORMAL_ACCOUNT,
1037 msDSUserAccountControlComputed=0)
1039 def test_unicodePwd_lockout_with_clear_change_krb5(self):
1040 self._test_unicodePwd_lockout_with_clear_change(self.lockout1krb5_creds,
1041 self.lockout2krb5_ldb)
1043 def test_unicodePwd_lockout_with_clear_change_ntlm(self):
1044 self._test_unicodePwd_lockout_with_clear_change(self.lockout1ntlm_creds,
1045 self.lockout2ntlm_ldb,
1046 initial_logoncount_relation="equal")
1048 def test_login_lockout_krb5(self):
1049 self._test_login_lockout(self.lockout1krb5_creds)
1051 def test_login_lockout_ntlm(self):
1052 self._test_login_lockout(self.lockout1ntlm_creds)
1054 # Repeat the login lockout tests using PSOs
1055 def test_pso_login_lockout_krb5(self):
1056 """Check the PSO lockout settings get applied to the user correctly"""
1057 self.use_pso_lockout_settings(self.lockout1krb5_creds)
1058 self._test_login_lockout(self.lockout1krb5_creds)
1060 def test_pso_login_lockout_ntlm(self):
1061 """Check the PSO lockout settings get applied to the user correctly"""
1062 self.use_pso_lockout_settings(self.lockout1ntlm_creds)
1063 self._test_login_lockout(self.lockout1ntlm_creds)
1065 def test_multiple_logon_krb5(self):
1066 self._test_multiple_logon(self.lockout1krb5_creds)
1068 def test_multiple_logon_ntlm(self):
1069 self._test_multiple_logon(self.lockout1ntlm_creds)
1071 def _testing_add_user(self, creds, lockOutObservationWindow=0):
1072 username = creds.get_username()
1073 userpass = creds.get_password()
1074 userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
1076 use_kerberos = creds.get_kerberos_state()
1077 if use_kerberos == MUST_USE_KERBEROS:
1078 logoncount_relation = 'greater'
1079 lastlogon_relation = 'greater'
1081 logoncount_relation = 'equal'
1082 if lockOutObservationWindow == 0:
1083 lastlogon_relation = 'greater'
1085 lastlogon_relation = 'equal'
1087 delete_force(self.ldb, userdn)
1090 "objectclass": "user",
1091 "sAMAccountName": username})
1093 self.addCleanup(delete_force, self.ldb, userdn)
1095 res = self._check_account(userdn,
1100 lastLogonTimestamp=('absent', None),
1102 dsdb.UF_NORMAL_ACCOUNT |
1103 dsdb.UF_ACCOUNTDISABLE |
1104 dsdb.UF_PASSWD_NOTREQD,
1105 msDSUserAccountControlComputed=
1106 dsdb.UF_PASSWORD_EXPIRED)
1108 # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
1109 # It doesn't create "lockoutTime" = 0.
1110 self._reset_samr(res)
1112 res = self._check_account(userdn,
1117 lastLogonTimestamp=('absent', None),
1119 dsdb.UF_NORMAL_ACCOUNT |
1120 dsdb.UF_ACCOUNTDISABLE |
1121 dsdb.UF_PASSWD_NOTREQD,
1122 msDSUserAccountControlComputed=
1123 dsdb.UF_PASSWORD_EXPIRED)
1125 # Tests a password change when we don't have any password yet with a
1126 # wrong old password
1128 self.ldb.modify_ldif("""
1129 dn: """ + userdn + """
1131 delete: userPassword
1132 userPassword: noPassword
1134 userPassword: thatsAcomplPASS2
1137 except LdbError as e19:
1138 (num, msg) = e19.args
1139 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
1140 # Windows (2008 at least) seems to have some small bug here: it
1141 # returns "0000056A" on longer (always wrong) previous passwords.
1142 self.assertTrue('00000056' in msg, msg)
1144 res = self._check_account(userdn,
1146 badPasswordTime=("greater", 0),
1149 lastLogonTimestamp=('absent', None),
1151 dsdb.UF_NORMAL_ACCOUNT |
1152 dsdb.UF_ACCOUNTDISABLE |
1153 dsdb.UF_PASSWD_NOTREQD,
1154 msDSUserAccountControlComputed=
1155 dsdb.UF_PASSWORD_EXPIRED)
1156 badPwdCount = int(res[0]["badPwdCount"][0])
1157 badPasswordTime = int(res[0]["badPasswordTime"][0])
1159 # Sets the initial user password with a "special" password change
1160 # I think that this internally is a password set operation and it can
1161 # only be performed by someone which has password set privileges on the
1162 # account (at least in s4 we do handle it like that).
1163 self.ldb.modify_ldif("""
1164 dn: """ + userdn + """
1166 delete: userPassword
1168 userPassword: """ + userpass + """
1171 res = self._check_account(userdn,
1172 badPwdCount=badPwdCount,
1173 badPasswordTime=badPasswordTime,
1176 lastLogonTimestamp=('absent', None),
1178 dsdb.UF_NORMAL_ACCOUNT |
1179 dsdb.UF_ACCOUNTDISABLE |
1180 dsdb.UF_PASSWD_NOTREQD,
1181 msDSUserAccountControlComputed=0)
1183 # Enables the user account
1184 self.ldb.enable_account("(sAMAccountName=%s)" % username)
1186 res = self._check_account(userdn,
1187 badPwdCount=badPwdCount,
1188 badPasswordTime=badPasswordTime,
1191 lastLogonTimestamp=('absent', None),
1193 dsdb.UF_NORMAL_ACCOUNT,
1194 msDSUserAccountControlComputed=0)
1195 if lockOutObservationWindow != 0:
1196 time.sleep(lockOutObservationWindow + 1)
1197 effective_bad_password_count = 0
1199 effective_bad_password_count = badPwdCount
1201 res = self._check_account(userdn,
1202 badPwdCount=badPwdCount,
1203 effective_bad_password_count=effective_bad_password_count,
1204 badPasswordTime=badPasswordTime,
1207 lastLogonTimestamp=('absent', None),
1209 dsdb.UF_NORMAL_ACCOUNT,
1210 msDSUserAccountControlComputed=0)
1212 ldb = SamDB(url=self.host_url, credentials=creds, lp=self.lp)
1214 if lockOutObservationWindow == 0:
1216 effective_bad_password_count = 0
1217 if use_kerberos == MUST_USE_KERBEROS:
1219 effective_bad_password_count = 0
1221 res = self._check_account(userdn,
1222 badPwdCount=badPwdCount,
1223 effective_bad_password_count=effective_bad_password_count,
1224 badPasswordTime=badPasswordTime,
1225 logonCount=(logoncount_relation, 0),
1226 lastLogon=(lastlogon_relation, 0),
1227 lastLogonTimestamp=('greater', badPasswordTime),
1229 dsdb.UF_NORMAL_ACCOUNT,
1230 msDSUserAccountControlComputed=0)
1232 logonCount = int(res[0]["logonCount"][0])
1233 lastLogon = int(res[0]["lastLogon"][0])
1234 lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
1235 if lastlogon_relation == 'greater':
1236 self.assertGreater(lastLogon, badPasswordTime)
1237 self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
1239 res = self._check_account(userdn,
1240 badPwdCount=badPwdCount,
1241 effective_bad_password_count=effective_bad_password_count,
1242 badPasswordTime=badPasswordTime,
1243 logonCount=logonCount,
1244 lastLogon=lastLogon,
1245 lastLogonTimestamp=lastLogonTimestamp,
1247 dsdb.UF_NORMAL_ACCOUNT,
1248 msDSUserAccountControlComputed=0)
1251 def _reset_samr(self, res):
1253 # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
1254 samr_user = self._open_samr_user(res)
1255 acb_info = self.samr.QueryUserInfo(samr_user, 16)
1256 acb_info.acct_flags &= ~samr.ACB_AUTOLOCK
1257 self.samr.SetUserInfo(samr_user, 16, acb_info)
1258 self.samr.Close(samr_user)
1260 def test_lockout_observation_window(self):
1261 lockout3krb5_creds = self.insta_creds(self.template_creds,
1262 username="lockout3krb5",
1263 userpass="thatsAcomplPASS0",
1264 kerberos_state=MUST_USE_KERBEROS)
1265 self._testing_add_user(lockout3krb5_creds)
1267 lockout4krb5_creds = self.insta_creds(self.template_creds,
1268 username="lockout4krb5",
1269 userpass="thatsAcomplPASS0",
1270 kerberos_state=MUST_USE_KERBEROS)
1271 self._testing_add_user(lockout4krb5_creds,
1272 lockOutObservationWindow=self.lockout_observation_window)
1274 lockout3ntlm_creds = self.insta_creds(self.template_creds,
1275 username="lockout3ntlm",
1276 userpass="thatsAcomplPASS0",
1277 kerberos_state=DONT_USE_KERBEROS)
1278 self._testing_add_user(lockout3ntlm_creds)
1279 lockout4ntlm_creds = self.insta_creds(self.template_creds,
1280 username="lockout4ntlm",
1281 userpass="thatsAcomplPASS0",
1282 kerberos_state=DONT_USE_KERBEROS)
1283 self._testing_add_user(lockout4ntlm_creds,
1284 lockOutObservationWindow=self.lockout_observation_window)
1286 def _test_samr_password_change(self, creds, other_creds, lockout_threshold=3):
1287 """Tests user lockout by using bad password in SAMR password_change"""
1289 # create a connection for SAMR using another user's credentials
1290 lp = self.get_loadparm()
1291 net = Net(other_creds, lp, server=self.host)
1293 # work out the initial account values for this user
1294 username = creds.get_username()
1295 userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
1296 res = self._check_account(userdn,
1298 badPasswordTime=("greater", 0),
1299 badPwdCountOnly=True)
1300 badPasswordTime = int(res[0]["badPasswordTime"][0])
1301 logonCount = int(res[0]["logonCount"][0])
1302 lastLogon = int(res[0]["lastLogon"][0])
1303 lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
1305 # prove we can change the user password (using the correct password)
1306 new_password = "thatsAcomplPASS2"
1307 net.change_password(newpassword=new_password.encode('utf-8'),
1309 oldpassword=creds.get_password())
1310 creds.set_password(new_password)
1312 # try entering 'x' many bad passwords in a row to lock the user out
1313 new_password = "thatsAcomplPASS3"
1314 for i in range(lockout_threshold):
1317 print("Trying bad password, attempt #%u" % badPwdCount)
1318 net.change_password(newpassword=new_password.encode('utf-8'),
1319 username=creds.get_username(),
1320 oldpassword="bad-password")
1321 self.fail("Invalid SAMR change_password accepted")
1322 except NTSTATUSError as e:
1323 enum = ctypes.c_uint32(e[0]).value
1324 self.assertEquals(enum, ntstatus.NT_STATUS_WRONG_PASSWORD)
1326 # check the status of the account is updated after each bad attempt
1329 if badPwdCount >= lockout_threshold:
1330 account_flags = dsdb.UF_LOCKOUT
1331 lockoutTime = ("greater", badPasswordTime)
1333 res = self._check_account(userdn,
1334 badPwdCount=badPwdCount,
1335 badPasswordTime=("greater", badPasswordTime),
1336 logonCount=logonCount,
1337 lastLogon=lastLogon,
1338 lastLogonTimestamp=lastLogonTimestamp,
1339 lockoutTime=lockoutTime,
1340 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1341 msDSUserAccountControlComputed=account_flags)
1342 badPasswordTime = int(res[0]["badPasswordTime"][0])
1344 # the user is now locked out
1345 lockoutTime = int(res[0]["lockoutTime"][0])
1347 # check the user remains locked out regardless of whether they use a
1348 # good or a bad password now
1349 for password in (creds.get_password(), "bad-password"):
1351 print("Trying password %s" % password)
1352 net.change_password(newpassword=new_password.encode('utf-8'),
1353 username=creds.get_username(),
1354 oldpassword=password)
1355 self.fail("Invalid SAMR change_password accepted")
1356 except NTSTATUSError as e:
1357 enum = ctypes.c_uint32(e[0]).value
1358 self.assertEquals(enum, ntstatus.NT_STATUS_ACCOUNT_LOCKED_OUT)
1360 res = self._check_account(userdn,
1361 badPwdCount=lockout_threshold,
1362 badPasswordTime=badPasswordTime,
1363 logonCount=logonCount,
1364 lastLogon=lastLogon,
1365 lastLogonTimestamp=lastLogonTimestamp,
1366 lockoutTime=lockoutTime,
1367 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1368 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
1370 # reset the user account lockout
1371 self._reset_samr(res)
1373 # check bad password counts are reset
1374 res = self._check_account(userdn,
1376 badPasswordTime=badPasswordTime,
1377 logonCount=logonCount,
1379 lastLogon=lastLogon,
1380 lastLogonTimestamp=lastLogonTimestamp,
1381 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1382 msDSUserAccountControlComputed=0)
1384 # check we can change the user password successfully now
1385 net.change_password(newpassword=new_password.encode('utf-8'),
1387 oldpassword=creds.get_password())
1388 creds.set_password(new_password)
1390 def test_samr_change_password(self):
1391 self._test_samr_password_change(self.lockout1ntlm_creds,
1392 other_creds=self.lockout2ntlm_creds)
1394 # same as above, but use a PSO to enforce the lockout
1395 def test_pso_samr_change_password(self):
1396 self.use_pso_lockout_settings(self.lockout1ntlm_creds)
1397 self._test_samr_password_change(self.lockout1ntlm_creds,
1398 other_creds=self.lockout2ntlm_creds)
1400 host_url = "ldap://%s" % host
1402 TestProgram(module=__name__, opts=subunitopts)