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')) + """
158 # A change to the same password again will not work (password history)
160 self.ldb2.modify_ldif("""
161 dn: cn=testuser,cn=users,""" + self.base_dn + """
164 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
166 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
169 except LdbError, (num, _):
170 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
172 def test_dBCSPwd_hash_set(self):
173 print "Performs a password hash set operation on 'dBCSPwd' which should be prevented"
174 # Notice: Direct hash password sets should never work
177 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
178 m["dBCSPwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
183 except LdbError, (num, _):
184 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
186 def test_dBCSPwd_hash_change(self):
187 print "Performs a password hash change operation on 'dBCSPwd' which should be prevented"
188 # Notice: Direct hash password changes should never work
191 self.ldb2.modify_ldif("""
192 dn: cn=testuser,cn=users,""" + self.base_dn + """
195 dBCSPwd: XXXXXXXXXXXXXXXX
197 dBCSPwd: YYYYYYYYYYYYYYYY
200 except LdbError, (num, _):
201 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
203 def test_userPassword_clear_set(self):
204 print "Performs a password cleartext set operation on 'userPassword'"
205 # Notice: This works only against Windows if "dSHeuristics" has been set
209 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
210 m["userPassword"] = MessageElement("thatsAcomplPASS2", FLAG_MOD_REPLACE,
214 def test_userPassword_clear_change(self):
215 print "Performs a password cleartext change operation on 'userPassword'"
216 # Notice: This works only against Windows if "dSHeuristics" has been set
219 self.ldb2.modify_ldif("""
220 dn: cn=testuser,cn=users,""" + self.base_dn + """
223 userPassword: thatsAcomplPASS1
225 userPassword: thatsAcomplPASS2
228 # A change to the same password again will not work (password history)
230 self.ldb2.modify_ldif("""
231 dn: cn=testuser,cn=users,""" + self.base_dn + """
234 userPassword: thatsAcomplPASS2
236 userPassword: thatsAcomplPASS2
239 except LdbError, (num, _):
240 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
242 def test_clearTextPassword_clear_set(self):
243 print "Performs a password cleartext set operation on 'clearTextPassword'"
244 # Notice: This never works against Windows - only supported by us
248 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
249 m["clearTextPassword"] = MessageElement("thatsAcomplPASS2".encode('utf-16-le'),
250 FLAG_MOD_REPLACE, "clearTextPassword")
252 # this passes against s4
253 except LdbError, (num, msg):
254 # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
255 if num != ERR_NO_SUCH_ATTRIBUTE:
256 raise LdbError(num, msg)
258 def test_clearTextPassword_clear_change(self):
259 print "Performs a password cleartext change operation on 'clearTextPassword'"
260 # Notice: This never works against Windows - only supported by us
263 self.ldb2.modify_ldif("""
264 dn: cn=testuser,cn=users,""" + self.base_dn + """
266 delete: clearTextPassword
267 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS1".encode('utf-16-le')) + """
268 add: clearTextPassword
269 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
271 # this passes against s4
272 except LdbError, (num, msg):
273 # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
274 if num != ERR_NO_SUCH_ATTRIBUTE:
275 raise LdbError(num, msg)
277 # A change to the same password again will not work (password history)
279 self.ldb2.modify_ldif("""
280 dn: cn=testuser,cn=users,""" + self.base_dn + """
282 delete: clearTextPassword
283 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
284 add: clearTextPassword
285 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
288 except LdbError, (num, _):
289 # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
290 if num != ERR_NO_SUCH_ATTRIBUTE:
291 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
293 def test_failures(self):
294 print "Performs some failure testing"
298 dn: cn=testuser,cn=users,""" + self.base_dn + """
301 userPassword: thatsAcomplPASS1
304 except LdbError, (num, _):
305 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
308 self.ldb2.modify_ldif("""
309 dn: cn=testuser,cn=users,""" + self.base_dn + """
314 except LdbError, (num, _):
315 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
319 dn: cn=testuser,cn=users,""" + self.base_dn + """
322 userPassword: thatsAcomplPASS1
325 except LdbError, (num, _):
326 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
329 self.ldb2.modify_ldif("""
330 dn: cn=testuser,cn=users,""" + self.base_dn + """
333 userPassword: thatsAcomplPASS1
336 except LdbError, (num, _):
337 self.assertEquals(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
341 dn: cn=testuser,cn=users,""" + self.base_dn + """
344 userPassword: thatsAcomplPASS1
346 userPassword: thatsAcomplPASS2
347 userPassword: thatsAcomplPASS2
350 except LdbError, (num, _):
351 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
354 self.ldb2.modify_ldif("""
355 dn: cn=testuser,cn=users,""" + self.base_dn + """
358 userPassword: thatsAcomplPASS1
360 userPassword: thatsAcomplPASS2
361 userPassword: thatsAcomplPASS2
364 except LdbError, (num, _):
365 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
369 dn: cn=testuser,cn=users,""" + self.base_dn + """
372 userPassword: thatsAcomplPASS1
373 userPassword: thatsAcomplPASS1
375 userPassword: thatsAcomplPASS2
378 except LdbError, (num, _):
379 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
382 self.ldb2.modify_ldif("""
383 dn: cn=testuser,cn=users,""" + self.base_dn + """
386 userPassword: thatsAcomplPASS1
387 userPassword: thatsAcomplPASS1
389 userPassword: thatsAcomplPASS2
392 except LdbError, (num, _):
393 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
397 dn: cn=testuser,cn=users,""" + self.base_dn + """
400 userPassword: thatsAcomplPASS1
402 userPassword: thatsAcomplPASS2
404 userPassword: thatsAcomplPASS2
407 except LdbError, (num, _):
408 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
411 self.ldb2.modify_ldif("""
412 dn: cn=testuser,cn=users,""" + self.base_dn + """
415 userPassword: thatsAcomplPASS1
417 userPassword: thatsAcomplPASS2
419 userPassword: thatsAcomplPASS2
422 except LdbError, (num, _):
423 self.assertEquals(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
427 dn: cn=testuser,cn=users,""" + self.base_dn + """
430 userPassword: thatsAcomplPASS1
432 userPassword: thatsAcomplPASS1
434 userPassword: thatsAcomplPASS2
437 except LdbError, (num, _):
438 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
441 self.ldb2.modify_ldif("""
442 dn: cn=testuser,cn=users,""" + self.base_dn + """
445 userPassword: thatsAcomplPASS1
447 userPassword: thatsAcomplPASS1
449 userPassword: thatsAcomplPASS2
452 except LdbError, (num, _):
453 self.assertEquals(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
457 dn: cn=testuser,cn=users,""" + self.base_dn + """
460 userPassword: thatsAcomplPASS1
462 userPassword: thatsAcomplPASS2
463 replace: userPassword
464 userPassword: thatsAcomplPASS3
467 except LdbError, (num, _):
468 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
471 self.ldb2.modify_ldif("""
472 dn: cn=testuser,cn=users,""" + self.base_dn + """
475 userPassword: thatsAcomplPASS1
477 userPassword: thatsAcomplPASS2
478 replace: userPassword
479 userPassword: thatsAcomplPASS3
482 except LdbError, (num, _):
483 self.assertEquals(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
485 # Reverse order does work
486 self.ldb2.modify_ldif("""
487 dn: cn=testuser,cn=users,""" + self.base_dn + """
490 userPassword: thatsAcomplPASS2
492 userPassword: thatsAcomplPASS1
496 self.ldb2.modify_ldif("""
497 dn: cn=testuser,cn=users,""" + self.base_dn + """
500 userPassword: thatsAcomplPASS2
502 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')) + """
504 # this passes against s4
505 except LdbError, (num, _):
506 self.assertEquals(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
509 self.ldb2.modify_ldif("""
510 dn: cn=testuser,cn=users,""" + self.base_dn + """
513 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')) + """
515 userPassword: thatsAcomplPASS4
517 # this passes against s4
518 except LdbError, (num, _):
519 self.assertEquals(num, ERR_NO_SUCH_ATTRIBUTE)
521 # Several password changes at once are allowed
523 dn: cn=testuser,cn=users,""" + self.base_dn + """
525 replace: userPassword
526 userPassword: thatsAcomplPASS1
527 userPassword: thatsAcomplPASS2
530 # Several password changes at once are allowed
532 dn: cn=testuser,cn=users,""" + self.base_dn + """
534 replace: userPassword
535 userPassword: thatsAcomplPASS1
536 userPassword: thatsAcomplPASS2
537 replace: userPassword
538 userPassword: thatsAcomplPASS3
539 replace: userPassword
540 userPassword: thatsAcomplPASS4
543 # This surprisingly should work
544 self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
546 "dn": "cn=testuser2,cn=users," + self.base_dn,
547 "objectclass": ["user", "person"],
548 "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS2"] })
550 # This surprisingly should work
551 self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
553 "dn": "cn=testuser2,cn=users," + self.base_dn,
554 "objectclass": ["user", "person"],
555 "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS1"] })
558 super(PasswordTests, self).tearDown()
559 self.delete_force(self.ldb, "cn=testuser,cn=users," + self.base_dn)
560 self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
561 # Close the second LDB connection (with the user credentials)
564 if not "://" in host:
565 if os.path.isfile(host):
566 host = "tdb://%s" % host
568 host = "ldap://%s" % host
570 ldb = SamDB(url=host, session_info=system_session(), credentials=creds, lp=lp)
572 # Gets back the configuration basedn
573 res = ldb.search(base="", expression="", scope=SCOPE_BASE,
574 attrs=["configurationNamingContext"])
575 configuration_dn = res[0]["configurationNamingContext"][0]
577 # Gets back the basedn
578 res = ldb.search(base="", expression="", scope=SCOPE_BASE,
579 attrs=["defaultNamingContext"])
580 base_dn = res[0]["defaultNamingContext"][0]
582 # Get the old "dSHeuristics" if it was set
583 res = ldb.search("CN=Directory Service, CN=Windows NT, CN=Services, "
584 + configuration_dn, scope=SCOPE_BASE, attrs=["dSHeuristics"])
585 if "dSHeuristics" in res[0]:
586 dsheuristics = res[0]["dSHeuristics"][0]
590 # Set the "dSHeuristics" to have the tests run against Windows Server
592 m.dn = Dn(ldb, "CN=Directory Service, CN=Windows NT, CN=Services, "
594 m["dSHeuristics"] = MessageElement("000000001", FLAG_MOD_REPLACE,
598 # Get the old "minPwdAge"
599 res = ldb.search(base_dn, scope=SCOPE_BASE, attrs=["minPwdAge"])
600 minPwdAge = res[0]["minPwdAge"][0]
602 # Set it temporarely to "0"
604 m.dn = Dn(ldb, base_dn)
605 m["minPwdAge"] = MessageElement("0", FLAG_MOD_REPLACE, "minPwdAge")
608 runner = SubunitTestRunner()
610 if not runner.run(unittest.makeSuite(PasswordTests)).wasSuccessful():
613 # Reset the "dSHeuristics" as they were before
615 m.dn = Dn(ldb, "CN=Directory Service, CN=Windows NT, CN=Services, "
617 if dsheuristics is not None:
618 m["dSHeuristics"] = MessageElement(dsheuristics, FLAG_MOD_REPLACE,
621 m["dSHeuristics"] = MessageElement([], FLAG_MOD_DELETE, "dsHeuristics")
624 # Reset the "minPwdAge" as it was before
626 m.dn = Dn(ldb, base_dn)
627 m["minPwdAge"] = MessageElement(minPwdAge, FLAG_MOD_REPLACE, "minPwdAge")