2 # -*- coding: utf-8 -*-
3 # This tests the password changes over LDAP for AD implementations
5 # Copyright Matthias Dieter Wallnoefer 2010
7 # Notice: This tests will also work against Windows Server if the connection is
8 # secured enough (SASL with a minimum of 128 Bit encryption) - consider
16 sys.path.append("bin/python")
18 samba.ensure_external_module("subunit", "subunit/python")
19 samba.ensure_external_module("testtools", "testtools")
21 import samba.getopt as options
23 from samba.auth import system_session
24 from samba.credentials import Credentials
25 from ldb import SCOPE_BASE, LdbError
26 from ldb import ERR_NO_SUCH_OBJECT, ERR_ATTRIBUTE_OR_VALUE_EXISTS
27 from ldb import ERR_UNWILLING_TO_PERFORM, ERR_INSUFFICIENT_ACCESS_RIGHTS
28 from ldb import ERR_NO_SUCH_ATTRIBUTE
29 from ldb import ERR_CONSTRAINT_VIOLATION
30 from ldb import Message, MessageElement, Dn
31 from ldb import FLAG_MOD_REPLACE, FLAG_MOD_DELETE
32 from samba import gensec
33 from samba.samdb import SamDB
35 from subunit.run import SubunitTestRunner
38 parser = optparse.OptionParser("passwords [options] <host>")
39 sambaopts = options.SambaOptions(parser)
40 parser.add_option_group(sambaopts)
41 parser.add_option_group(options.VersionOptions(parser))
42 # use command line creds if available
43 credopts = options.CredentialsOptions(parser)
44 parser.add_option_group(credopts)
45 opts, args = parser.parse_args()
53 lp = sambaopts.get_loadparm()
54 creds = credopts.get_credentials(lp)
56 # Force an encrypted connection
57 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
63 class PasswordTests(samba.tests.TestCase):
65 def delete_force(self, ldb, dn):
68 except LdbError, (num, _):
69 self.assertEquals(num, ERR_NO_SUCH_OBJECT)
71 def find_basedn(self, ldb):
72 res = ldb.search(base="", expression="", scope=SCOPE_BASE,
73 attrs=["defaultNamingContext"])
74 self.assertEquals(len(res), 1)
75 return res[0]["defaultNamingContext"][0]
78 super(PasswordTests, self).setUp()
80 self.base_dn = self.find_basedn(ldb)
82 # (Re)adds the test user "testuser" with the inital password
84 self.delete_force(self.ldb, "cn=testuser,cn=users," + self.base_dn)
86 "dn": "cn=testuser,cn=users," + self.base_dn,
87 "objectclass": ["user", "person"],
88 "sAMAccountName": "testuser",
89 "userPassword": "thatsAcomplPASS1" })
90 self.ldb.enable_account("(sAMAccountName=testuser)")
92 # Open a second LDB connection with the user credentials. Use the
93 # command line credentials for informations like the domain, the realm
94 # and the workstation.
95 creds2 = Credentials()
96 creds2.set_username("testuser")
97 creds2.set_password("thatsAcomplPASS1")
98 creds2.set_domain(creds.get_domain())
99 creds2.set_realm(creds.get_realm())
100 creds2.set_workstation(creds.get_workstation())
101 creds2.set_gensec_features(creds2.get_gensec_features()
102 | gensec.FEATURE_SEAL)
103 self.ldb2 = SamDB(url=host, credentials=creds2, lp=lp)
105 def test_unicodePwd_hash_set(self):
106 print "Performs a password hash set operation on 'unicodePwd' which should be prevented"
107 # Notice: Direct hash password sets should never work
110 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
111 m["unicodePwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
116 except LdbError, (num, _):
117 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
119 def test_unicodePwd_hash_change(self):
120 print "Performs a password hash change operation on 'unicodePwd' which should be prevented"
121 # Notice: Direct hash password changes should never work
123 # Hash password changes should never work
125 self.ldb2.modify_ldif("""
126 dn: cn=testuser,cn=users,""" + self.base_dn + """
129 unicodePwd: XXXXXXXXXXXXXXXX
131 unicodePwd: YYYYYYYYYYYYYYYY
134 except LdbError, (num, _):
135 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
137 def test_unicodePwd_clear_set(self):
138 print "Performs a password cleartext set operation on 'unicodePwd'"
141 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
142 m["unicodePwd"] = MessageElement("\"thatsAcomplPASS2\"".encode('utf-16-le'),
143 FLAG_MOD_REPLACE, "unicodePwd")
146 def test_unicodePwd_clear_change(self):
147 print "Performs a password cleartext change operation on 'unicodePwd'"
149 self.ldb2.modify_ldif("""
150 dn: cn=testuser,cn=users,""" + self.base_dn + """
153 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')) + """
155 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
160 self.ldb2.modify_ldif("""
161 dn: cn=testuser,cn=users,""" + self.base_dn + """
164 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')) + """
166 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS4\"".encode('utf-16-le')) + """
169 except LdbError, (num, _):
170 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
172 # A change to the same password again will not work (password history)
174 self.ldb2.modify_ldif("""
175 dn: cn=testuser,cn=users,""" + self.base_dn + """
178 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
180 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
183 except LdbError, (num, _):
184 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
186 def test_dBCSPwd_hash_set(self):
187 print "Performs a password hash set operation on 'dBCSPwd' which should be prevented"
188 # Notice: Direct hash password sets should never work
191 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
192 m["dBCSPwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
197 except LdbError, (num, _):
198 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
200 def test_dBCSPwd_hash_change(self):
201 print "Performs a password hash change operation on 'dBCSPwd' which should be prevented"
202 # Notice: Direct hash password changes should never work
205 self.ldb2.modify_ldif("""
206 dn: cn=testuser,cn=users,""" + self.base_dn + """
209 dBCSPwd: XXXXXXXXXXXXXXXX
211 dBCSPwd: YYYYYYYYYYYYYYYY
214 except LdbError, (num, _):
215 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
217 def test_userPassword_clear_set(self):
218 print "Performs a password cleartext set operation on 'userPassword'"
219 # Notice: This works only against Windows if "dSHeuristics" has been set
223 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
224 m["userPassword"] = MessageElement("thatsAcomplPASS2", FLAG_MOD_REPLACE,
228 def test_userPassword_clear_change(self):
229 print "Performs a password cleartext change operation on 'userPassword'"
230 # Notice: This works only against Windows if "dSHeuristics" has been set
233 self.ldb2.modify_ldif("""
234 dn: cn=testuser,cn=users,""" + self.base_dn + """
237 userPassword: thatsAcomplPASS1
239 userPassword: thatsAcomplPASS2
244 self.ldb2.modify_ldif("""
245 dn: cn=testuser,cn=users,""" + self.base_dn + """
248 userPassword: thatsAcomplPASS3
250 userPassword: thatsAcomplPASS4
253 except LdbError, (num, _):
254 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
256 # A change to the same password again will not work (password history)
258 self.ldb2.modify_ldif("""
259 dn: cn=testuser,cn=users,""" + self.base_dn + """
262 userPassword: thatsAcomplPASS2
264 userPassword: thatsAcomplPASS2
267 except LdbError, (num, _):
268 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
270 def test_clearTextPassword_clear_set(self):
271 print "Performs a password cleartext set operation on 'clearTextPassword'"
272 # Notice: This never works against Windows - only supported by us
276 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
277 m["clearTextPassword"] = MessageElement("thatsAcomplPASS2".encode('utf-16-le'),
278 FLAG_MOD_REPLACE, "clearTextPassword")
280 # this passes against s4
281 except LdbError, (num, msg):
282 # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
283 if num != ERR_NO_SUCH_ATTRIBUTE:
284 raise LdbError(num, msg)
286 def test_clearTextPassword_clear_change(self):
287 print "Performs a password cleartext change operation on 'clearTextPassword'"
288 # Notice: This never works against Windows - only supported by us
291 self.ldb2.modify_ldif("""
292 dn: cn=testuser,cn=users,""" + self.base_dn + """
294 delete: clearTextPassword
295 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS1".encode('utf-16-le')) + """
296 add: clearTextPassword
297 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
299 # this passes against s4
300 except LdbError, (num, msg):
301 # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
302 if num != ERR_NO_SUCH_ATTRIBUTE:
303 raise LdbError(num, msg)
307 self.ldb2.modify_ldif("""
308 dn: cn=testuser,cn=users,""" + self.base_dn + """
310 delete: clearTextPassword
311 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS3".encode('utf-16-le')) + """
312 add: clearTextPassword
313 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS4".encode('utf-16-le')) + """
316 except LdbError, (num, _):
317 # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
318 if num != ERR_NO_SUCH_ATTRIBUTE:
319 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
321 # A change to the same password again will not work (password history)
323 self.ldb2.modify_ldif("""
324 dn: cn=testuser,cn=users,""" + self.base_dn + """
326 delete: clearTextPassword
327 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
328 add: clearTextPassword
329 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
332 except LdbError, (num, _):
333 # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
334 if num != ERR_NO_SUCH_ATTRIBUTE:
335 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
337 def test_failures(self):
338 print "Performs some failure testing"
342 dn: cn=testuser,cn=users,""" + self.base_dn + """
345 userPassword: thatsAcomplPASS1
348 except LdbError, (num, _):
349 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
352 self.ldb2.modify_ldif("""
353 dn: cn=testuser,cn=users,""" + self.base_dn + """
358 except LdbError, (num, _):
359 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
363 dn: cn=testuser,cn=users,""" + self.base_dn + """
366 userPassword: thatsAcomplPASS1
369 except LdbError, (num, _):
370 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
373 self.ldb2.modify_ldif("""
374 dn: cn=testuser,cn=users,""" + self.base_dn + """
377 userPassword: thatsAcomplPASS1
380 except LdbError, (num, _):
381 self.assertEquals(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
385 dn: cn=testuser,cn=users,""" + self.base_dn + """
388 userPassword: thatsAcomplPASS1
390 userPassword: thatsAcomplPASS2
391 userPassword: thatsAcomplPASS2
394 except LdbError, (num, _):
395 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
398 self.ldb2.modify_ldif("""
399 dn: cn=testuser,cn=users,""" + self.base_dn + """
402 userPassword: thatsAcomplPASS1
404 userPassword: thatsAcomplPASS2
405 userPassword: thatsAcomplPASS2
408 except LdbError, (num, _):
409 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
413 dn: cn=testuser,cn=users,""" + self.base_dn + """
416 userPassword: thatsAcomplPASS1
417 userPassword: thatsAcomplPASS1
419 userPassword: thatsAcomplPASS2
422 except LdbError, (num, _):
423 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
426 self.ldb2.modify_ldif("""
427 dn: cn=testuser,cn=users,""" + self.base_dn + """
430 userPassword: thatsAcomplPASS1
431 userPassword: thatsAcomplPASS1
433 userPassword: thatsAcomplPASS2
436 except LdbError, (num, _):
437 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
441 dn: cn=testuser,cn=users,""" + self.base_dn + """
444 userPassword: thatsAcomplPASS1
446 userPassword: thatsAcomplPASS2
448 userPassword: thatsAcomplPASS2
451 except LdbError, (num, _):
452 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
455 self.ldb2.modify_ldif("""
456 dn: cn=testuser,cn=users,""" + self.base_dn + """
459 userPassword: thatsAcomplPASS1
461 userPassword: thatsAcomplPASS2
463 userPassword: thatsAcomplPASS2
466 except LdbError, (num, _):
467 self.assertEquals(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
471 dn: cn=testuser,cn=users,""" + self.base_dn + """
474 userPassword: thatsAcomplPASS1
476 userPassword: thatsAcomplPASS1
478 userPassword: thatsAcomplPASS2
481 except LdbError, (num, _):
482 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
485 self.ldb2.modify_ldif("""
486 dn: cn=testuser,cn=users,""" + self.base_dn + """
489 userPassword: thatsAcomplPASS1
491 userPassword: thatsAcomplPASS1
493 userPassword: thatsAcomplPASS2
496 except LdbError, (num, _):
497 self.assertEquals(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
501 dn: cn=testuser,cn=users,""" + self.base_dn + """
504 userPassword: thatsAcomplPASS1
506 userPassword: thatsAcomplPASS2
507 replace: userPassword
508 userPassword: thatsAcomplPASS3
511 except LdbError, (num, _):
512 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
515 self.ldb2.modify_ldif("""
516 dn: cn=testuser,cn=users,""" + self.base_dn + """
519 userPassword: thatsAcomplPASS1
521 userPassword: thatsAcomplPASS2
522 replace: userPassword
523 userPassword: thatsAcomplPASS3
526 except LdbError, (num, _):
527 self.assertEquals(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
529 # Reverse order does work
530 self.ldb2.modify_ldif("""
531 dn: cn=testuser,cn=users,""" + self.base_dn + """
534 userPassword: thatsAcomplPASS2
536 userPassword: thatsAcomplPASS1
540 self.ldb2.modify_ldif("""
541 dn: cn=testuser,cn=users,""" + self.base_dn + """
544 userPassword: thatsAcomplPASS2
546 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')) + """
548 # this passes against s4
549 except LdbError, (num, _):
550 self.assertEquals(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
553 self.ldb2.modify_ldif("""
554 dn: cn=testuser,cn=users,""" + self.base_dn + """
557 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')) + """
559 userPassword: thatsAcomplPASS4
561 # this passes against s4
562 except LdbError, (num, _):
563 self.assertEquals(num, ERR_NO_SUCH_ATTRIBUTE)
565 # Several password changes at once are allowed
567 dn: cn=testuser,cn=users,""" + self.base_dn + """
569 replace: userPassword
570 userPassword: thatsAcomplPASS1
571 userPassword: thatsAcomplPASS2
574 # Several password changes at once are allowed
576 dn: cn=testuser,cn=users,""" + self.base_dn + """
578 replace: userPassword
579 userPassword: thatsAcomplPASS1
580 userPassword: thatsAcomplPASS2
581 replace: userPassword
582 userPassword: thatsAcomplPASS3
583 replace: userPassword
584 userPassword: thatsAcomplPASS4
587 # This surprisingly should work
588 self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
590 "dn": "cn=testuser2,cn=users," + self.base_dn,
591 "objectclass": ["user", "person"],
592 "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS2"] })
594 # This surprisingly should work
595 self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
597 "dn": "cn=testuser2,cn=users," + self.base_dn,
598 "objectclass": ["user", "person"],
599 "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS1"] })
602 super(PasswordTests, self).tearDown()
603 self.delete_force(self.ldb, "cn=testuser,cn=users," + self.base_dn)
604 self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
605 # Close the second LDB connection (with the user credentials)
608 if not "://" in host:
609 if os.path.isfile(host):
610 host = "tdb://%s" % host
612 host = "ldap://%s" % host
614 ldb = SamDB(url=host, session_info=system_session(), credentials=creds, lp=lp)
616 # Gets back the configuration basedn
617 res = ldb.search(base="", expression="", scope=SCOPE_BASE,
618 attrs=["configurationNamingContext"])
619 configuration_dn = res[0]["configurationNamingContext"][0]
621 # Gets back the basedn
622 res = ldb.search(base="", expression="", scope=SCOPE_BASE,
623 attrs=["defaultNamingContext"])
624 base_dn = res[0]["defaultNamingContext"][0]
626 # Get the old "dSHeuristics" if it was set
627 res = ldb.search("CN=Directory Service, CN=Windows NT, CN=Services, "
628 + configuration_dn, scope=SCOPE_BASE, attrs=["dSHeuristics"])
629 if "dSHeuristics" in res[0]:
630 dsheuristics = res[0]["dSHeuristics"][0]
634 # Set the "dSHeuristics" to have the tests run against Windows Server
636 m.dn = Dn(ldb, "CN=Directory Service, CN=Windows NT, CN=Services, "
638 m["dSHeuristics"] = MessageElement("000000001", FLAG_MOD_REPLACE,
642 # Get the old "minPwdAge"
643 res = ldb.search(base_dn, scope=SCOPE_BASE, attrs=["minPwdAge"])
644 minPwdAge = res[0]["minPwdAge"][0]
646 # Set it temporarely to "0"
648 m.dn = Dn(ldb, base_dn)
649 m["minPwdAge"] = MessageElement("0", FLAG_MOD_REPLACE, "minPwdAge")
652 runner = SubunitTestRunner()
654 if not runner.run(unittest.makeSuite(PasswordTests)).wasSuccessful():
657 # Reset the "dSHeuristics" as they were before
659 m.dn = Dn(ldb, "CN=Directory Service, CN=Windows NT, CN=Services, "
661 if dsheuristics is not None:
662 m["dSHeuristics"] = MessageElement(dsheuristics, FLAG_MOD_REPLACE,
665 m["dSHeuristics"] = MessageElement([], FLAG_MOD_DELETE, "dsHeuristics")
668 # Reset the "minPwdAge" as it was before
670 m.dn = Dn(ldb, base_dn)
671 m["minPwdAge"] = MessageElement(minPwdAge, FLAG_MOD_REPLACE, "minPwdAge")