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):
603 # create a PSO with the lockout settings the test cases normally expect
604 pso = PasswordSettings("lockout-PSO", self.ldb, lockout_attempts=3,
606 self.addCleanup(self.ldb.delete, pso.dn)
608 # the test cases should sleep() for the PSO's lockoutDuration/obsvWindow
609 self.account_lockout_duration = 3
610 self.lockout_observation_window = 3
612 userdn = "cn=%s,cn=users,%s" % (creds.get_username(), self.base_dn)
615 # update the global lockout settings to be wildly different to what
616 # the test cases normally expect
617 self.update_lockout_settings(threshold=10, duration=600,
618 observation_window=600)
620 def _test_unicodePwd_lockout_with_clear_change(self, creds, other_ldb,
621 initial_logoncount_relation=None):
622 print("Performs a password cleartext change operation on 'unicodePwd'")
623 username = creds.get_username()
624 userpass = creds.get_password()
625 userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
626 if initial_logoncount_relation is not None:
627 logoncount_relation = initial_logoncount_relation
629 logoncount_relation = "greater"
631 res = self._check_account(userdn,
633 badPasswordTime=("greater", 0),
634 logonCount=(logoncount_relation, 0),
635 lastLogon=("greater", 0),
636 lastLogonTimestamp=("greater", 0),
637 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
638 msDSUserAccountControlComputed=0)
639 badPasswordTime = int(res[0]["badPasswordTime"][0])
640 logonCount = int(res[0]["logonCount"][0])
641 lastLogon = int(res[0]["lastLogon"][0])
642 lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
643 self.assertGreater(lastLogonTimestamp, badPasswordTime)
644 self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
646 # Change password on a connection as another user
650 other_ldb.modify_ldif("""
651 dn: """ + userdn + """
654 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1x\"".encode('utf-16-le')).decode('utf8') + """
656 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
659 except LdbError as e10:
660 (num, msg) = e10.args
661 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
662 self.assertTrue('00000056' in msg, msg)
664 res = self._check_account(userdn,
666 badPasswordTime=("greater", badPasswordTime),
667 logonCount=logonCount,
669 lastLogonTimestamp=lastLogonTimestamp,
670 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
671 msDSUserAccountControlComputed=0)
672 badPasswordTime = int(res[0]["badPasswordTime"][0])
674 # Correct old password
675 old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
676 invalid_utf16 = "\"thatsAcomplPASSX\"".encode('utf-16-le')
677 userpass = "thatsAcomplPASS2"
678 creds.set_password(userpass)
679 new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
681 other_ldb.modify_ldif("""
682 dn: """ + userdn + """
685 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
687 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
690 res = self._check_account(userdn,
692 badPasswordTime=badPasswordTime,
693 logonCount=logonCount,
695 lastLogonTimestamp=lastLogonTimestamp,
696 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
697 msDSUserAccountControlComputed=0)
701 other_ldb.modify_ldif("""
702 dn: """ + userdn + """
705 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
707 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
710 except LdbError as e11:
711 (num, msg) = e11.args
712 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
713 self.assertTrue('00000056' in msg, msg)
715 res = self._check_account(userdn,
717 badPasswordTime=("greater", badPasswordTime),
718 logonCount=logonCount,
720 lastLogonTimestamp=lastLogonTimestamp,
721 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
722 msDSUserAccountControlComputed=0)
723 badPasswordTime = int(res[0]["badPasswordTime"][0])
725 # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
726 # It doesn't create "lockoutTime" = 0 and doesn't
727 # reset "badPwdCount" = 0.
728 self._reset_samr(res)
730 res = self._check_account(userdn,
732 badPasswordTime=badPasswordTime,
733 logonCount=logonCount,
735 lastLogonTimestamp=lastLogonTimestamp,
736 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
737 msDSUserAccountControlComputed=0)
739 print("two failed password change")
743 other_ldb.modify_ldif("""
744 dn: """ + userdn + """
747 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
749 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
752 except LdbError as e12:
753 (num, msg) = e12.args
754 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
755 self.assertTrue('00000056' in msg, msg)
757 # this is strange, why do we have lockoutTime=badPasswordTime here?
758 res = self._check_account(userdn,
760 badPasswordTime=("greater", badPasswordTime),
761 logonCount=logonCount,
763 lastLogonTimestamp=lastLogonTimestamp,
764 lockoutTime=("greater", badPasswordTime),
765 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
766 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
767 badPasswordTime = int(res[0]["badPasswordTime"][0])
768 lockoutTime = int(res[0]["lockoutTime"][0])
772 other_ldb.modify_ldif("""
773 dn: """ + userdn + """
776 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
778 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
781 except LdbError as e13:
782 (num, msg) = e13.args
783 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
784 self.assertTrue('00000775' in msg, msg)
786 res = self._check_account(userdn,
788 badPasswordTime=badPasswordTime,
789 logonCount=logonCount,
791 lastLogonTimestamp=lastLogonTimestamp,
792 lockoutTime=lockoutTime,
793 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
794 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
798 other_ldb.modify_ldif("""
799 dn: """ + userdn + """
802 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
804 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
807 except LdbError as e14:
808 (num, msg) = e14.args
809 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
810 self.assertTrue('00000775' in msg, msg)
812 res = self._check_account(userdn,
814 badPasswordTime=badPasswordTime,
815 logonCount=logonCount,
817 lastLogonTimestamp=lastLogonTimestamp,
818 lockoutTime=lockoutTime,
819 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
820 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
823 # Correct old password
824 other_ldb.modify_ldif("""
825 dn: """ + userdn + """
828 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
830 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
833 except LdbError as e15:
834 (num, msg) = e15.args
835 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
836 self.assertTrue('00000775' in msg, msg)
838 res = self._check_account(userdn,
840 badPasswordTime=badPasswordTime,
841 logonCount=logonCount,
843 lastLogonTimestamp=lastLogonTimestamp,
844 lockoutTime=lockoutTime,
845 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
846 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
848 # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
849 self._reset_samr(res);
851 res = self._check_account(userdn,
853 badPasswordTime=badPasswordTime,
854 logonCount=logonCount,
856 lastLogonTimestamp=lastLogonTimestamp,
858 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
859 msDSUserAccountControlComputed=0)
861 # Correct old password
862 old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
863 invalid_utf16 = "\"thatsAcomplPASSiX\"".encode('utf-16-le')
864 userpass = "thatsAcomplPASS2x"
865 creds.set_password(userpass)
866 new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
868 other_ldb.modify_ldif("""
869 dn: """ + userdn + """
872 unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
874 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
877 res = self._check_account(userdn,
879 badPasswordTime=badPasswordTime,
880 logonCount=logonCount,
882 lastLogonTimestamp=lastLogonTimestamp,
884 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
885 msDSUserAccountControlComputed=0)
889 other_ldb.modify_ldif("""
890 dn: """ + userdn + """
893 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
895 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
898 except LdbError as e16:
899 (num, msg) = e16.args
900 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
901 self.assertTrue('00000056' in msg, msg)
903 res = self._check_account(userdn,
905 badPasswordTime=("greater", badPasswordTime),
906 logonCount=logonCount,
908 lastLogonTimestamp=lastLogonTimestamp,
910 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
911 msDSUserAccountControlComputed=0)
912 badPasswordTime = int(res[0]["badPasswordTime"][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 e17:
926 (num, msg) = e17.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,
937 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
938 msDSUserAccountControlComputed=0)
939 badPasswordTime = int(res[0]["badPasswordTime"][0])
941 # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
942 # It doesn't reset "badPwdCount" = 0.
943 self._reset_samr(res)
945 res = self._check_account(userdn,
947 badPasswordTime=badPasswordTime,
948 logonCount=logonCount,
950 lastLogonTimestamp=lastLogonTimestamp,
952 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
953 msDSUserAccountControlComputed=0)
957 other_ldb.modify_ldif("""
958 dn: """ + userdn + """
961 unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
963 unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
966 except LdbError as e18:
967 (num, msg) = e18.args
968 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
969 self.assertTrue('00000056' in msg, msg)
971 res = self._check_account(userdn,
973 badPasswordTime=("greater", badPasswordTime),
974 logonCount=logonCount,
976 lastLogonTimestamp=lastLogonTimestamp,
977 lockoutTime=("greater", badPasswordTime),
978 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
979 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
980 badPasswordTime = int(res[0]["badPasswordTime"][0])
981 lockoutTime = int(res[0]["lockoutTime"][0])
983 time.sleep(self.account_lockout_duration + 1)
985 res = self._check_account(userdn,
986 badPwdCount=3, effective_bad_password_count=0,
987 badPasswordTime=badPasswordTime,
988 logonCount=logonCount,
990 lastLogonTimestamp=lastLogonTimestamp,
991 lockoutTime=lockoutTime,
992 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
993 msDSUserAccountControlComputed=0)
995 # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
996 # It doesn't reset "lockoutTime" = 0 and doesn't
997 # reset "badPwdCount" = 0.
998 self._reset_samr(res)
1000 res = self._check_account(userdn,
1001 badPwdCount=3, effective_bad_password_count=0,
1002 badPasswordTime=badPasswordTime,
1003 logonCount=logonCount,
1004 lockoutTime=lockoutTime,
1005 lastLogon=lastLogon,
1006 lastLogonTimestamp=lastLogonTimestamp,
1007 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1008 msDSUserAccountControlComputed=0)
1010 def test_unicodePwd_lockout_with_clear_change_krb5(self):
1011 self._test_unicodePwd_lockout_with_clear_change(self.lockout1krb5_creds,
1012 self.lockout2krb5_ldb)
1014 def test_unicodePwd_lockout_with_clear_change_ntlm(self):
1015 self._test_unicodePwd_lockout_with_clear_change(self.lockout1ntlm_creds,
1016 self.lockout2ntlm_ldb,
1017 initial_logoncount_relation="equal")
1019 def test_login_lockout_krb5(self):
1020 self._test_login_lockout(self.lockout1krb5_creds)
1022 def test_login_lockout_ntlm(self):
1023 self._test_login_lockout(self.lockout1ntlm_creds)
1025 # Repeat the login lockout tests using PSOs
1026 def test_pso_login_lockout_krb5(self):
1027 """Check the PSO lockout settings get applied to the user correctly"""
1028 self.use_pso_lockout_settings(self.lockout1krb5_creds)
1029 self._test_login_lockout(self.lockout1krb5_creds)
1031 def test_pso_login_lockout_ntlm(self):
1032 """Check the PSO lockout settings get applied to the user correctly"""
1033 self.use_pso_lockout_settings(self.lockout1ntlm_creds)
1034 self._test_login_lockout(self.lockout1ntlm_creds)
1036 def test_multiple_logon_krb5(self):
1037 self._test_multiple_logon(self.lockout1krb5_creds)
1039 def test_multiple_logon_ntlm(self):
1040 self._test_multiple_logon(self.lockout1ntlm_creds)
1042 def _testing_add_user(self, creds, lockOutObservationWindow=0):
1043 username = creds.get_username()
1044 userpass = creds.get_password()
1045 userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
1047 use_kerberos = creds.get_kerberos_state()
1048 if use_kerberos == MUST_USE_KERBEROS:
1049 logoncount_relation = 'greater'
1050 lastlogon_relation = 'greater'
1052 logoncount_relation = 'equal'
1053 if lockOutObservationWindow == 0:
1054 lastlogon_relation = 'greater'
1056 lastlogon_relation = 'equal'
1058 delete_force(self.ldb, userdn)
1061 "objectclass": "user",
1062 "sAMAccountName": username})
1064 self.addCleanup(delete_force, self.ldb, userdn)
1066 res = self._check_account(userdn,
1071 lastLogonTimestamp=('absent', None),
1072 userAccountControl=dsdb.UF_NORMAL_ACCOUNT |
1073 dsdb.UF_ACCOUNTDISABLE |
1074 dsdb.UF_PASSWD_NOTREQD,
1075 msDSUserAccountControlComputed=dsdb.UF_PASSWORD_EXPIRED)
1077 # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
1078 # It doesn't create "lockoutTime" = 0.
1079 self._reset_samr(res)
1081 res = self._check_account(userdn,
1086 lastLogonTimestamp=('absent', None),
1087 userAccountControl=dsdb.UF_NORMAL_ACCOUNT |
1088 dsdb.UF_ACCOUNTDISABLE |
1089 dsdb.UF_PASSWD_NOTREQD,
1090 msDSUserAccountControlComputed=dsdb.UF_PASSWORD_EXPIRED)
1092 # Tests a password change when we don't have any password yet with a
1093 # wrong old password
1095 self.ldb.modify_ldif("""
1096 dn: """ + userdn + """
1098 delete: userPassword
1099 userPassword: noPassword
1101 userPassword: thatsAcomplPASS2
1104 except LdbError as e19:
1105 (num, msg) = e19.args
1106 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
1107 # Windows (2008 at least) seems to have some small bug here: it
1108 # returns "0000056A" on longer (always wrong) previous passwords.
1109 self.assertTrue('00000056' in msg, msg)
1111 res = self._check_account(userdn,
1113 badPasswordTime=("greater", 0),
1116 lastLogonTimestamp=('absent', None),
1117 userAccountControl=dsdb.UF_NORMAL_ACCOUNT |
1118 dsdb.UF_ACCOUNTDISABLE |
1119 dsdb.UF_PASSWD_NOTREQD,
1120 msDSUserAccountControlComputed=dsdb.UF_PASSWORD_EXPIRED)
1121 badPwdCount = int(res[0]["badPwdCount"][0])
1122 badPasswordTime = int(res[0]["badPasswordTime"][0])
1124 # Sets the initial user password with a "special" password change
1125 # I think that this internally is a password set operation and it can
1126 # only be performed by someone which has password set privileges on the
1127 # account (at least in s4 we do handle it like that).
1128 self.ldb.modify_ldif("""
1129 dn: """ + userdn + """
1131 delete: userPassword
1133 userPassword: """ + userpass + """
1136 res = self._check_account(userdn,
1137 badPwdCount=badPwdCount,
1138 badPasswordTime=badPasswordTime,
1141 lastLogonTimestamp=('absent', None),
1142 userAccountControl=dsdb.UF_NORMAL_ACCOUNT |
1143 dsdb.UF_ACCOUNTDISABLE |
1144 dsdb.UF_PASSWD_NOTREQD,
1145 msDSUserAccountControlComputed=0)
1147 # Enables the user account
1148 self.ldb.enable_account("(sAMAccountName=%s)" % username)
1150 res = self._check_account(userdn,
1151 badPwdCount=badPwdCount,
1152 badPasswordTime=badPasswordTime,
1155 lastLogonTimestamp=('absent', None),
1156 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1157 msDSUserAccountControlComputed=0)
1158 if lockOutObservationWindow != 0:
1159 time.sleep(lockOutObservationWindow + 1)
1160 effective_bad_password_count = 0
1162 effective_bad_password_count = badPwdCount
1164 res = self._check_account(userdn,
1165 badPwdCount=badPwdCount,
1166 effective_bad_password_count=effective_bad_password_count,
1167 badPasswordTime=badPasswordTime,
1170 lastLogonTimestamp=('absent', None),
1171 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1172 msDSUserAccountControlComputed=0)
1174 ldb = SamDB(url=self.host_url, credentials=creds, lp=self.lp)
1176 if lockOutObservationWindow == 0:
1178 effective_bad_password_count = 0
1179 if use_kerberos == MUST_USE_KERBEROS:
1181 effective_bad_password_count = 0
1183 res = self._check_account(userdn,
1184 badPwdCount=badPwdCount,
1185 effective_bad_password_count=effective_bad_password_count,
1186 badPasswordTime=badPasswordTime,
1187 logonCount=(logoncount_relation, 0),
1188 lastLogon=(lastlogon_relation, 0),
1189 lastLogonTimestamp=('greater', badPasswordTime),
1190 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1191 msDSUserAccountControlComputed=0)
1193 logonCount = int(res[0]["logonCount"][0])
1194 lastLogon = int(res[0]["lastLogon"][0])
1195 lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
1196 if lastlogon_relation == 'greater':
1197 self.assertGreater(lastLogon, badPasswordTime)
1198 self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
1200 res = self._check_account(userdn,
1201 badPwdCount=badPwdCount,
1202 effective_bad_password_count=effective_bad_password_count,
1203 badPasswordTime=badPasswordTime,
1204 logonCount=logonCount,
1205 lastLogon=lastLogon,
1206 lastLogonTimestamp=lastLogonTimestamp,
1207 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1208 msDSUserAccountControlComputed=0)
1211 def _reset_samr(self, res):
1213 # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
1214 samr_user = self._open_samr_user(res)
1215 acb_info = self.samr.QueryUserInfo(samr_user, 16)
1216 acb_info.acct_flags &= ~samr.ACB_AUTOLOCK
1217 self.samr.SetUserInfo(samr_user, 16, acb_info)
1218 self.samr.Close(samr_user)
1220 def test_lockout_observation_window(self):
1221 lockout3krb5_creds = self.insta_creds(self.template_creds,
1222 username="lockout3krb5",
1223 userpass="thatsAcomplPASS0",
1224 kerberos_state=MUST_USE_KERBEROS)
1225 self._testing_add_user(lockout3krb5_creds)
1227 lockout4krb5_creds = self.insta_creds(self.template_creds,
1228 username="lockout4krb5",
1229 userpass="thatsAcomplPASS0",
1230 kerberos_state=MUST_USE_KERBEROS)
1231 self._testing_add_user(lockout4krb5_creds,
1232 lockOutObservationWindow=self.lockout_observation_window)
1234 lockout3ntlm_creds = self.insta_creds(self.template_creds,
1235 username="lockout3ntlm",
1236 userpass="thatsAcomplPASS0",
1237 kerberos_state=DONT_USE_KERBEROS)
1238 self._testing_add_user(lockout3ntlm_creds)
1239 lockout4ntlm_creds = self.insta_creds(self.template_creds,
1240 username="lockout4ntlm",
1241 userpass="thatsAcomplPASS0",
1242 kerberos_state=DONT_USE_KERBEROS)
1243 self._testing_add_user(lockout4ntlm_creds,
1244 lockOutObservationWindow=self.lockout_observation_window)
1246 def _test_samr_password_change(self, creds, other_creds, lockout_threshold=3):
1247 """Tests user lockout by using bad password in SAMR password_change"""
1249 # create a connection for SAMR using another user's credentials
1250 lp = self.get_loadparm()
1251 net = Net(other_creds, lp, server=self.host)
1253 # work out the initial account values for this user
1254 username = creds.get_username()
1255 userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
1256 res = self._check_account(userdn,
1258 badPasswordTime=("greater", 0),
1259 badPwdCountOnly=True)
1260 badPasswordTime = int(res[0]["badPasswordTime"][0])
1261 logonCount = int(res[0]["logonCount"][0])
1262 lastLogon = int(res[0]["lastLogon"][0])
1263 lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
1265 # prove we can change the user password (using the correct password)
1266 new_password = "thatsAcomplPASS2"
1267 net.change_password(newpassword=new_password.encode('utf-8'),
1269 oldpassword=creds.get_password())
1270 creds.set_password(new_password)
1272 # try entering 'x' many bad passwords in a row to lock the user out
1273 new_password = "thatsAcomplPASS3"
1274 for i in range(lockout_threshold):
1277 print("Trying bad password, attempt #%u" % badPwdCount)
1278 net.change_password(newpassword=new_password.encode('utf-8'),
1279 username=creds.get_username(),
1280 oldpassword="bad-password")
1281 self.fail("Invalid SAMR change_password accepted")
1282 except NTSTATUSError as e:
1283 enum = ctypes.c_uint32(e[0]).value
1284 self.assertEquals(enum, ntstatus.NT_STATUS_WRONG_PASSWORD)
1286 # check the status of the account is updated after each bad attempt
1289 if badPwdCount >= lockout_threshold:
1290 account_flags = dsdb.UF_LOCKOUT
1291 lockoutTime = ("greater", badPasswordTime)
1293 res = self._check_account(userdn,
1294 badPwdCount=badPwdCount,
1295 badPasswordTime=("greater", badPasswordTime),
1296 logonCount=logonCount,
1297 lastLogon=lastLogon,
1298 lastLogonTimestamp=lastLogonTimestamp,
1299 lockoutTime=lockoutTime,
1300 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1301 msDSUserAccountControlComputed=account_flags)
1302 badPasswordTime = int(res[0]["badPasswordTime"][0])
1304 # the user is now locked out
1305 lockoutTime = int(res[0]["lockoutTime"][0])
1307 # check the user remains locked out regardless of whether they use a
1308 # good or a bad password now
1309 for password in (creds.get_password(), "bad-password"):
1311 print("Trying password %s" % password)
1312 net.change_password(newpassword=new_password.encode('utf-8'),
1313 username=creds.get_username(),
1314 oldpassword=password)
1315 self.fail("Invalid SAMR change_password accepted")
1316 except NTSTATUSError as e:
1317 enum = ctypes.c_uint32(e[0]).value
1318 self.assertEquals(enum, ntstatus.NT_STATUS_ACCOUNT_LOCKED_OUT)
1320 res = self._check_account(userdn,
1321 badPwdCount=lockout_threshold,
1322 badPasswordTime=badPasswordTime,
1323 logonCount=logonCount,
1324 lastLogon=lastLogon,
1325 lastLogonTimestamp=lastLogonTimestamp,
1326 lockoutTime=lockoutTime,
1327 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1328 msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
1330 # reset the user account lockout
1331 self._reset_samr(res)
1333 # check bad password counts are reset
1334 res = self._check_account(userdn,
1336 badPasswordTime=badPasswordTime,
1337 logonCount=logonCount,
1339 lastLogon=lastLogon,
1340 lastLogonTimestamp=lastLogonTimestamp,
1341 userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
1342 msDSUserAccountControlComputed=0)
1344 # check we can change the user password successfully now
1345 net.change_password(newpassword=new_password.encode('utf-8'),
1347 oldpassword=creds.get_password())
1348 creds.set_password(new_password)
1350 def test_samr_change_password(self):
1351 self._test_samr_password_change(self.lockout1ntlm_creds,
1352 other_creds=self.lockout2ntlm_creds)
1354 # same as above, but use a PSO to enforce the lockout
1355 def test_pso_samr_change_password(self):
1356 self.use_pso_lockout_settings(self.lockout1ntlm_creds)
1357 self._test_samr_password_change(self.lockout1ntlm_creds,
1358 other_creds=self.lockout2ntlm_creds)
1360 host_url = "ldap://%s" % host
1362 TestProgram(module=__name__, opts=subunitopts)