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 # the test cases should sleep() for the PSO's lockoutDuration/obsvWindow
625 self.account_lockout_duration = 3
626 self.lockout_observation_window = 3
628 userdn = "cn=%s,cn=users,%s" % (creds.get_username(), self.base_dn)
631 # update the global lockout settings to be wildly different to what
632 # the test cases normally expect
633 self.update_lockout_settings(threshold=10, duration=600,
634 observation_window=600)
636 def _test_unicodePwd_lockout_with_clear_change(self, creds, other_ldb,
637 initial_logoncount_relation=None):
638 print("Performs a password cleartext change operation on 'unicodePwd'")
639 username = creds.get_username()
640 userpass = creds.get_password()
641 userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
642 if initial_logoncount_relation is not None:
643 logoncount_relation = initial_logoncount_relation
645 logoncount_relation = "greater"
647 res = self._check_account(userdn,
649 badPasswordTime=("greater", 0),
650 logonCount=(logoncount_relation, 0),
651 lastLogon=("greater", 0),
652 lastLogonTimestamp=("greater", 0),
654 dsdb.UF_NORMAL_ACCOUNT,
655 msDSUserAccountControlComputed=0)
656 badPasswordTime = int(res[0]["badPasswordTime"][0])
657 logonCount = int(res[0]["logonCount"][0])
658 lastLogon = int(res[0]["lastLogon"][0])
659 lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
660 self.assertGreater(lastLogonTimestamp, badPasswordTime)
661 self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
663 # Change password on a connection as another user
667 other_ldb.modify_ldif("""
668 dn: """ + userdn + """
671 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1x\"".encode('utf-16-le')).decode('utf8') + """
673 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
676 except LdbError as e10:
677 (num, msg) = e10.args
678 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
679 self.assertTrue('00000056' in msg, msg)
681 res = self._check_account(userdn,
683 badPasswordTime=("greater", badPasswordTime),
684 logonCount=logonCount,
686 lastLogonTimestamp=lastLogonTimestamp,
688 dsdb.UF_NORMAL_ACCOUNT,
689 msDSUserAccountControlComputed=0)
690 badPasswordTime = int(res[0]["badPasswordTime"][0])
692 # Correct old password
693 old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
694 invalid_utf16 = "\"thatsAcomplPASSX\"".encode('utf-16-le')
695 userpass = "thatsAcomplPASS2"
696 creds.set_password(userpass)
697 new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
699 other_ldb.modify_ldif("""
700 dn: """ + userdn + """
703 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
705 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
708 res = self._check_account(userdn,
710 badPasswordTime=badPasswordTime,
711 logonCount=logonCount,
713 lastLogonTimestamp=lastLogonTimestamp,
715 dsdb.UF_NORMAL_ACCOUNT,
716 msDSUserAccountControlComputed=0)
720 other_ldb.modify_ldif("""
721 dn: """ + userdn + """
724 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
726 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
729 except LdbError as e11:
730 (num, msg) = e11.args
731 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
732 self.assertTrue('00000056' in msg, msg)
734 res = self._check_account(userdn,
736 badPasswordTime=("greater", badPasswordTime),
737 logonCount=logonCount,
739 lastLogonTimestamp=lastLogonTimestamp,
741 dsdb.UF_NORMAL_ACCOUNT,
742 msDSUserAccountControlComputed=0)
743 badPasswordTime = int(res[0]["badPasswordTime"][0])
745 # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
746 # It doesn't create "lockoutTime" = 0 and doesn't
747 # reset "badPwdCount" = 0.
748 self._reset_samr(res)
750 res = self._check_account(userdn,
752 badPasswordTime=badPasswordTime,
753 logonCount=logonCount,
755 lastLogonTimestamp=lastLogonTimestamp,
757 dsdb.UF_NORMAL_ACCOUNT,
758 msDSUserAccountControlComputed=0)
760 print("two failed password change")
764 other_ldb.modify_ldif("""
765 dn: """ + userdn + """
768 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
770 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
773 except LdbError as e12:
774 (num, msg) = e12.args
775 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
776 self.assertTrue('00000056' in msg, msg)
778 # this is strange, why do we have lockoutTime=badPasswordTime here?
779 res = self._check_account(userdn,
781 badPasswordTime=("greater", badPasswordTime),
782 logonCount=logonCount,
784 lastLogonTimestamp=lastLogonTimestamp,
785 lockoutTime=("greater", badPasswordTime),
787 dsdb.UF_NORMAL_ACCOUNT,
788 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
789 badPasswordTime = int(res[0]["badPasswordTime"][0])
790 lockoutTime = int(res[0]["lockoutTime"][0])
794 other_ldb.modify_ldif("""
795 dn: """ + userdn + """
798 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
800 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
803 except LdbError as e13:
804 (num, msg) = e13.args
805 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
806 self.assertTrue('00000775' in msg, msg)
808 res = self._check_account(userdn,
810 badPasswordTime=badPasswordTime,
811 logonCount=logonCount,
813 lastLogonTimestamp=lastLogonTimestamp,
814 lockoutTime=lockoutTime,
816 dsdb.UF_NORMAL_ACCOUNT,
817 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
821 other_ldb.modify_ldif("""
822 dn: """ + userdn + """
825 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
827 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
830 except LdbError as e14:
831 (num, msg) = e14.args
832 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
833 self.assertTrue('00000775' in msg, msg)
835 res = self._check_account(userdn,
837 badPasswordTime=badPasswordTime,
838 logonCount=logonCount,
840 lastLogonTimestamp=lastLogonTimestamp,
841 lockoutTime=lockoutTime,
843 dsdb.UF_NORMAL_ACCOUNT,
844 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
847 # Correct old password
848 other_ldb.modify_ldif("""
849 dn: """ + userdn + """
852 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
854 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
857 except LdbError as e15:
858 (num, msg) = e15.args
859 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
860 self.assertTrue('00000775' in msg, msg)
862 res = self._check_account(userdn,
864 badPasswordTime=badPasswordTime,
865 logonCount=logonCount,
867 lastLogonTimestamp=lastLogonTimestamp,
868 lockoutTime=lockoutTime,
870 dsdb.UF_NORMAL_ACCOUNT,
871 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
873 # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
874 self._reset_samr(res);
876 res = self._check_account(userdn,
878 badPasswordTime=badPasswordTime,
879 logonCount=logonCount,
881 lastLogonTimestamp=lastLogonTimestamp,
884 dsdb.UF_NORMAL_ACCOUNT,
885 msDSUserAccountControlComputed=0)
887 # Correct old password
888 old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
889 invalid_utf16 = "\"thatsAcomplPASSiX\"".encode('utf-16-le')
890 userpass = "thatsAcomplPASS2x"
891 creds.set_password(userpass)
892 new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
894 other_ldb.modify_ldif("""
895 dn: """ + userdn + """
898 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
900 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
903 res = self._check_account(userdn,
905 badPasswordTime=badPasswordTime,
906 logonCount=logonCount,
908 lastLogonTimestamp=lastLogonTimestamp,
911 dsdb.UF_NORMAL_ACCOUNT,
912 msDSUserAccountControlComputed=0)
916 other_ldb.modify_ldif("""
917 dn: """ + userdn + """
920 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
922 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
925 except LdbError as e16:
926 (num, msg) = e16.args
927 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
928 self.assertTrue('00000056' in msg, msg)
930 res = self._check_account(userdn,
932 badPasswordTime=("greater", badPasswordTime),
933 logonCount=logonCount,
935 lastLogonTimestamp=lastLogonTimestamp,
938 dsdb.UF_NORMAL_ACCOUNT,
939 msDSUserAccountControlComputed=0)
940 badPasswordTime = int(res[0]["badPasswordTime"][0])
944 other_ldb.modify_ldif("""
945 dn: """ + userdn + """
948 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
950 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
953 except LdbError as e17:
954 (num, msg) = e17.args
955 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
956 self.assertTrue('00000056' in msg, msg)
958 res = self._check_account(userdn,
960 badPasswordTime=("greater", badPasswordTime),
961 logonCount=logonCount,
963 lastLogonTimestamp=lastLogonTimestamp,
966 dsdb.UF_NORMAL_ACCOUNT,
967 msDSUserAccountControlComputed=0)
968 badPasswordTime = int(res[0]["badPasswordTime"][0])
970 # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
971 # It doesn't reset "badPwdCount" = 0.
972 self._reset_samr(res)
974 res = self._check_account(userdn,
976 badPasswordTime=badPasswordTime,
977 logonCount=logonCount,
979 lastLogonTimestamp=lastLogonTimestamp,
982 dsdb.UF_NORMAL_ACCOUNT,
983 msDSUserAccountControlComputed=0)
987 other_ldb.modify_ldif("""
988 dn: """ + userdn + """
991 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
993 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
996 except LdbError as e18:
997 (num, msg) = e18.args
998 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
999 self.assertTrue('00000056' in msg, msg)
1001 res = self._check_account(userdn,
1003 badPasswordTime=("greater", badPasswordTime),
1004 logonCount=logonCount,
1005 lastLogon=lastLogon,
1006 lastLogonTimestamp=lastLogonTimestamp,
1007 lockoutTime=("greater", badPasswordTime),
1009 dsdb.UF_NORMAL_ACCOUNT,
1010 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
1011 badPasswordTime = int(res[0]["badPasswordTime"][0])
1012 lockoutTime = int(res[0]["lockoutTime"][0])
1014 time.sleep(self.account_lockout_duration + 1)
1016 res = self._check_account(userdn,
1017 badPwdCount=3, effective_bad_password_count=0,
1018 badPasswordTime=badPasswordTime,
1019 logonCount=logonCount,
1020 lastLogon=lastLogon,
1021 lastLogonTimestamp=lastLogonTimestamp,
1022 lockoutTime=lockoutTime,
1024 dsdb.UF_NORMAL_ACCOUNT,
1025 msDSUserAccountControlComputed=0)
1027 # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
1028 # It doesn't reset "lockoutTime" = 0 and doesn't
1029 # reset "badPwdCount" = 0.
1030 self._reset_samr(res)
1032 res = self._check_account(userdn,
1033 badPwdCount=3, effective_bad_password_count=0,
1034 badPasswordTime=badPasswordTime,
1035 logonCount=logonCount,
1036 lockoutTime=lockoutTime,
1037 lastLogon=lastLogon,
1038 lastLogonTimestamp=lastLogonTimestamp,
1040 dsdb.UF_NORMAL_ACCOUNT,
1041 msDSUserAccountControlComputed=0)
1043 def test_unicodePwd_lockout_with_clear_change_krb5(self):
1044 self._test_unicodePwd_lockout_with_clear_change(self.lockout1krb5_creds,
1045 self.lockout2krb5_ldb)
1047 def test_unicodePwd_lockout_with_clear_change_ntlm(self):
1048 self._test_unicodePwd_lockout_with_clear_change(self.lockout1ntlm_creds,
1049 self.lockout2ntlm_ldb,
1050 initial_logoncount_relation="equal")
1052 def test_login_lockout_krb5(self):
1053 self._test_login_lockout(self.lockout1krb5_creds)
1055 def test_login_lockout_ntlm(self):
1056 self._test_login_lockout(self.lockout1ntlm_creds)
1058 # Repeat the login lockout tests using PSOs
1059 def test_pso_login_lockout_krb5(self):
1060 """Check the PSO lockout settings get applied to the user correctly"""
1061 self.use_pso_lockout_settings(self.lockout1krb5_creds)
1062 self._test_login_lockout(self.lockout1krb5_creds)
1064 def test_pso_login_lockout_ntlm(self):
1065 """Check the PSO lockout settings get applied to the user correctly"""
1066 self.use_pso_lockout_settings(self.lockout1ntlm_creds)
1067 self._test_login_lockout(self.lockout1ntlm_creds)
1069 def test_multiple_logon_krb5(self):
1070 self._test_multiple_logon(self.lockout1krb5_creds)
1072 def test_multiple_logon_ntlm(self):
1073 self._test_multiple_logon(self.lockout1ntlm_creds)
1075 def _testing_add_user(self, creds, lockOutObservationWindow=0):
1076 username = creds.get_username()
1077 userpass = creds.get_password()
1078 userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
1080 use_kerberos = creds.get_kerberos_state()
1081 if use_kerberos == MUST_USE_KERBEROS:
1082 logoncount_relation = 'greater'
1083 lastlogon_relation = 'greater'
1085 logoncount_relation = 'equal'
1086 if lockOutObservationWindow == 0:
1087 lastlogon_relation = 'greater'
1089 lastlogon_relation = 'equal'
1091 delete_force(self.ldb, userdn)
1094 "objectclass": "user",
1095 "sAMAccountName": username})
1097 self.addCleanup(delete_force, self.ldb, userdn)
1099 res = self._check_account(userdn,
1104 lastLogonTimestamp=('absent', None),
1106 dsdb.UF_NORMAL_ACCOUNT |
1107 dsdb.UF_ACCOUNTDISABLE |
1108 dsdb.UF_PASSWD_NOTREQD,
1109 msDSUserAccountControlComputed=
1110 dsdb.UF_PASSWORD_EXPIRED)
1112 # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
1113 # It doesn't create "lockoutTime" = 0.
1114 self._reset_samr(res)
1116 res = self._check_account(userdn,
1121 lastLogonTimestamp=('absent', None),
1123 dsdb.UF_NORMAL_ACCOUNT |
1124 dsdb.UF_ACCOUNTDISABLE |
1125 dsdb.UF_PASSWD_NOTREQD,
1126 msDSUserAccountControlComputed=
1127 dsdb.UF_PASSWORD_EXPIRED)
1129 # Tests a password change when we don't have any password yet with a
1130 # wrong old password
1132 self.ldb.modify_ldif("""
1133 dn: """ + userdn + """
1135 delete: userPassword
1136 userPassword: noPassword
1138 userPassword: thatsAcomplPASS2
1141 except LdbError as e19:
1142 (num, msg) = e19.args
1143 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
1144 # Windows (2008 at least) seems to have some small bug here: it
1145 # returns "0000056A" on longer (always wrong) previous passwords.
1146 self.assertTrue('00000056' in msg, msg)
1148 res = self._check_account(userdn,
1150 badPasswordTime=("greater", 0),
1153 lastLogonTimestamp=('absent', None),
1155 dsdb.UF_NORMAL_ACCOUNT |
1156 dsdb.UF_ACCOUNTDISABLE |
1157 dsdb.UF_PASSWD_NOTREQD,
1158 msDSUserAccountControlComputed=
1159 dsdb.UF_PASSWORD_EXPIRED)
1160 badPwdCount = int(res[0]["badPwdCount"][0])
1161 badPasswordTime = int(res[0]["badPasswordTime"][0])
1163 # Sets the initial user password with a "special" password change
1164 # I think that this internally is a password set operation and it can
1165 # only be performed by someone which has password set privileges on the
1166 # account (at least in s4 we do handle it like that).
1167 self.ldb.modify_ldif("""
1168 dn: """ + userdn + """
1170 delete: userPassword
1172 userPassword: """ + userpass + """
1175 res = self._check_account(userdn,
1176 badPwdCount=badPwdCount,
1177 badPasswordTime=badPasswordTime,
1180 lastLogonTimestamp=('absent', None),
1182 dsdb.UF_NORMAL_ACCOUNT |
1183 dsdb.UF_ACCOUNTDISABLE |
1184 dsdb.UF_PASSWD_NOTREQD,
1185 msDSUserAccountControlComputed=0)
1187 # Enables the user account
1188 self.ldb.enable_account("(sAMAccountName=%s)" % username)
1190 res = self._check_account(userdn,
1191 badPwdCount=badPwdCount,
1192 badPasswordTime=badPasswordTime,
1195 lastLogonTimestamp=('absent', None),
1197 dsdb.UF_NORMAL_ACCOUNT,
1198 msDSUserAccountControlComputed=0)
1199 if lockOutObservationWindow != 0:
1200 time.sleep(lockOutObservationWindow + 1)
1201 effective_bad_password_count = 0
1203 effective_bad_password_count = badPwdCount
1205 res = self._check_account(userdn,
1206 badPwdCount=badPwdCount,
1207 effective_bad_password_count=effective_bad_password_count,
1208 badPasswordTime=badPasswordTime,
1211 lastLogonTimestamp=('absent', None),
1213 dsdb.UF_NORMAL_ACCOUNT,
1214 msDSUserAccountControlComputed=0)
1216 ldb = SamDB(url=self.host_url, credentials=creds, lp=self.lp)
1218 if lockOutObservationWindow == 0:
1220 effective_bad_password_count = 0
1221 if use_kerberos == MUST_USE_KERBEROS:
1223 effective_bad_password_count = 0
1225 res = self._check_account(userdn,
1226 badPwdCount=badPwdCount,
1227 effective_bad_password_count=effective_bad_password_count,
1228 badPasswordTime=badPasswordTime,
1229 logonCount=(logoncount_relation, 0),
1230 lastLogon=(lastlogon_relation, 0),
1231 lastLogonTimestamp=('greater', badPasswordTime),
1233 dsdb.UF_NORMAL_ACCOUNT,
1234 msDSUserAccountControlComputed=0)
1236 logonCount = int(res[0]["logonCount"][0])
1237 lastLogon = int(res[0]["lastLogon"][0])
1238 lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
1239 if lastlogon_relation == 'greater':
1240 self.assertGreater(lastLogon, badPasswordTime)
1241 self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
1243 res = self._check_account(userdn,
1244 badPwdCount=badPwdCount,
1245 effective_bad_password_count=effective_bad_password_count,
1246 badPasswordTime=badPasswordTime,
1247 logonCount=logonCount,
1248 lastLogon=lastLogon,
1249 lastLogonTimestamp=lastLogonTimestamp,
1251 dsdb.UF_NORMAL_ACCOUNT,
1252 msDSUserAccountControlComputed=0)
1255 def _reset_samr(self, res):
1257 # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
1258 samr_user = self._open_samr_user(res)
1259 acb_info = self.samr.QueryUserInfo(samr_user, 16)
1260 acb_info.acct_flags &= ~samr.ACB_AUTOLOCK
1261 self.samr.SetUserInfo(samr_user, 16, acb_info)
1262 self.samr.Close(samr_user)
1264 def test_lockout_observation_window(self):
1265 lockout3krb5_creds = self.insta_creds(self.template_creds,
1266 username="lockout3krb5",
1267 userpass="thatsAcomplPASS0",
1268 kerberos_state=MUST_USE_KERBEROS)
1269 self._testing_add_user(lockout3krb5_creds)
1271 lockout4krb5_creds = self.insta_creds(self.template_creds,
1272 username="lockout4krb5",
1273 userpass="thatsAcomplPASS0",
1274 kerberos_state=MUST_USE_KERBEROS)
1275 self._testing_add_user(lockout4krb5_creds,
1276 lockOutObservationWindow=self.lockout_observation_window)
1278 lockout3ntlm_creds = self.insta_creds(self.template_creds,
1279 username="lockout3ntlm",
1280 userpass="thatsAcomplPASS0",
1281 kerberos_state=DONT_USE_KERBEROS)
1282 self._testing_add_user(lockout3ntlm_creds)
1283 lockout4ntlm_creds = self.insta_creds(self.template_creds,
1284 username="lockout4ntlm",
1285 userpass="thatsAcomplPASS0",
1286 kerberos_state=DONT_USE_KERBEROS)
1287 self._testing_add_user(lockout4ntlm_creds,
1288 lockOutObservationWindow=self.lockout_observation_window)
1290 def _test_samr_password_change(self, creds, other_creds, lockout_threshold=3):
1291 """Tests user lockout by using bad password in SAMR password_change"""
1293 # create a connection for SAMR using another user's credentials
1294 lp = self.get_loadparm()
1295 net = Net(other_creds, lp, server=self.host)
1297 # work out the initial account values for this user
1298 username = creds.get_username()
1299 userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
1300 res = self._check_account(userdn,
1302 badPasswordTime=("greater", 0),
1303 badPwdCountOnly=True)
1304 badPasswordTime = int(res[0]["badPasswordTime"][0])
1305 logonCount = int(res[0]["logonCount"][0])
1306 lastLogon = int(res[0]["lastLogon"][0])
1307 lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
1309 # prove we can change the user password (using the correct password)
1310 new_password = "thatsAcomplPASS2"
1311 net.change_password(newpassword=new_password.encode('utf-8'),
1313 oldpassword=creds.get_password())
1314 creds.set_password(new_password)
1316 # try entering 'x' many bad passwords in a row to lock the user out
1317 new_password = "thatsAcomplPASS3"
1318 for i in range(lockout_threshold):
1321 print("Trying bad password, attempt #%u" % badPwdCount)
1322 net.change_password(newpassword=new_password.encode('utf-8'),
1323 username=creds.get_username(),
1324 oldpassword="bad-password")
1325 self.fail("Invalid SAMR change_password accepted")
1326 except NTSTATUSError as e:
1327 enum = ctypes.c_uint32(e[0]).value
1328 self.assertEquals(enum, ntstatus.NT_STATUS_WRONG_PASSWORD)
1330 # check the status of the account is updated after each bad attempt
1333 if badPwdCount >= lockout_threshold:
1334 account_flags = dsdb.UF_LOCKOUT
1335 lockoutTime = ("greater", badPasswordTime)
1337 res = self._check_account(userdn,
1338 badPwdCount=badPwdCount,
1339 badPasswordTime=("greater", badPasswordTime),
1340 logonCount=logonCount,
1341 lastLogon=lastLogon,
1342 lastLogonTimestamp=lastLogonTimestamp,
1343 lockoutTime=lockoutTime,
1344 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1345 msDSUserAccountControlComputed=account_flags)
1346 badPasswordTime = int(res[0]["badPasswordTime"][0])
1348 # the user is now locked out
1349 lockoutTime = int(res[0]["lockoutTime"][0])
1351 # check the user remains locked out regardless of whether they use a
1352 # good or a bad password now
1353 for password in (creds.get_password(), "bad-password"):
1355 print("Trying password %s" % password)
1356 net.change_password(newpassword=new_password.encode('utf-8'),
1357 username=creds.get_username(),
1358 oldpassword=password)
1359 self.fail("Invalid SAMR change_password accepted")
1360 except NTSTATUSError as e:
1361 enum = ctypes.c_uint32(e[0]).value
1362 self.assertEquals(enum, ntstatus.NT_STATUS_ACCOUNT_LOCKED_OUT)
1364 res = self._check_account(userdn,
1365 badPwdCount=lockout_threshold,
1366 badPasswordTime=badPasswordTime,
1367 logonCount=logonCount,
1368 lastLogon=lastLogon,
1369 lastLogonTimestamp=lastLogonTimestamp,
1370 lockoutTime=lockoutTime,
1371 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1372 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
1374 # reset the user account lockout
1375 self._reset_samr(res)
1377 # check bad password counts are reset
1378 res = self._check_account(userdn,
1380 badPasswordTime=badPasswordTime,
1381 logonCount=logonCount,
1383 lastLogon=lastLogon,
1384 lastLogonTimestamp=lastLogonTimestamp,
1385 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1386 msDSUserAccountControlComputed=0)
1388 # check we can change the user password successfully now
1389 net.change_password(newpassword=new_password.encode('utf-8'),
1391 oldpassword=creds.get_password())
1392 creds.set_password(new_password)
1394 def test_samr_change_password(self):
1395 self._test_samr_password_change(self.lockout1ntlm_creds,
1396 other_creds=self.lockout2ntlm_creds)
1398 # same as above, but use a PSO to enforce the lockout
1399 def test_pso_samr_change_password(self):
1400 self.use_pso_lockout_settings(self.lockout1ntlm_creds)
1401 self._test_samr_password_change(self.lockout1ntlm_creds,
1402 other_creds=self.lockout2ntlm_creds)
1404 host_url = "ldap://%s" % host
1406 TestProgram(module=__name__, opts=subunitopts)