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
11 # Important: Make sure that the minimum password age is set to "0"!
19 sys.path.append("bin/python")
21 import samba.getopt as options
23 from samba.auth import system_session
24 from samba.credentials import Credentials
25 from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, LdbError
26 from ldb import ERR_NO_SUCH_OBJECT, ERR_ATTRIBUTE_OR_VALUE_EXISTS
27 from ldb import ERR_ENTRY_ALREADY_EXISTS, ERR_UNWILLING_TO_PERFORM
28 from ldb import ERR_NOT_ALLOWED_ON_NON_LEAF, ERR_OTHER, ERR_INVALID_DN_SYNTAX
29 from ldb import ERR_NO_SUCH_ATTRIBUTE
30 from ldb import ERR_OBJECT_CLASS_VIOLATION, ERR_NOT_ALLOWED_ON_RDN
31 from ldb import ERR_NAMING_VIOLATION, ERR_CONSTRAINT_VIOLATION
32 from ldb import ERR_UNDEFINED_ATTRIBUTE_TYPE
33 from ldb import Message, MessageElement, Dn
34 from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
35 from samba import gensec
36 from samba.samdb import SamDB
37 from subunit.run import SubunitTestRunner
40 parser = optparse.OptionParser("passwords [options] <host>")
41 sambaopts = options.SambaOptions(parser)
42 parser.add_option_group(sambaopts)
43 parser.add_option_group(options.VersionOptions(parser))
44 # use command line creds if available
45 credopts = options.CredentialsOptions(parser)
46 parser.add_option_group(credopts)
47 opts, args = parser.parse_args()
55 lp = sambaopts.get_loadparm()
56 creds = credopts.get_credentials(lp)
58 # Force an encrypted connection
59 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
65 class PasswordTests(unittest.TestCase):
66 def delete_force(self, ldb, dn):
69 except LdbError, (num, _):
70 self.assertEquals(num, ERR_NO_SUCH_OBJECT)
72 def find_basedn(self, ldb):
73 res = ldb.search(base="", expression="", scope=SCOPE_BASE,
74 attrs=["defaultNamingContext"])
75 self.assertEquals(len(res), 1)
76 return res[0]["defaultNamingContext"][0]
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 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 # FIXME: Reactivate the user credentials when we have user password
97 # change support also on the ACL level in s4
98 creds2.set_username(creds.get_username())
99 creds2.set_password(creds.get_password())
100 #creds2.set_username("testuser")
101 #creds2.set_password("thatsAcomplPASS1")
102 creds2.set_domain(creds.get_domain())
103 creds2.set_realm(creds.get_realm())
104 creds2.set_workstation(creds.get_workstation())
105 creds2.set_gensec_features(creds2.get_gensec_features()
106 | gensec.FEATURE_SEAL)
107 self.ldb2 = SamDB(url=host, credentials=creds2, lp=lp)
109 def test_unicodePwd_hash_set(self):
110 print "Performs a password hash set operation on 'unicodePwd' which should be prevented"
111 # Notice: Direct hash password sets should never work
114 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
115 m["unicodePwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
120 except LdbError, (num, _):
121 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
123 def test_unicodePwd_hash_change(self):
124 print "Performs a password hash change operation on 'unicodePwd' which should be prevented"
125 # Notice: Direct hash password changes should never work
127 # Hash password changes should never work
129 self.ldb2.modify_ldif("""
130 dn: cn=testuser,cn=users,""" + self.base_dn + """
133 unicodePwd: XXXXXXXXXXXXXXXX
135 unicodePwd: YYYYYYYYYYYYYYYY
138 except LdbError, (num, _):
139 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
141 def test_unicodePwd_clear_set(self):
142 print "Performs a password cleartext set operation on 'unicodePwd'"
145 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
146 m["unicodePwd"] = MessageElement("\"thatsAcomplPASS2\"".encode('utf-16-le'),
147 FLAG_MOD_REPLACE, "unicodePwd")
150 def test_unicodePwd_clear_change(self):
151 print "Performs a password cleartext change operation on 'unicodePwd'"
153 self.ldb2.modify_ldif("""
154 dn: cn=testuser,cn=users,""" + self.base_dn + """
157 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')) + """
159 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
162 # A change to the same password again will not work (password history)
164 self.ldb2.modify_ldif("""
165 dn: cn=testuser,cn=users,""" + self.base_dn + """
168 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
170 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
173 except LdbError, (num, _):
174 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
176 def test_dBCSPwd_hash_set(self):
177 print "Performs a password hash set operation on 'dBCSPwd' which should be prevented"
178 # Notice: Direct hash password sets should never work
181 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
182 m["dBCSPwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
187 except LdbError, (num, _):
188 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
190 def test_dBCSPwd_hash_change(self):
191 print "Performs a password hash change operation on 'dBCSPwd' which should be prevented"
192 # Notice: Direct hash password changes should never work
195 self.ldb2.modify_ldif("""
196 dn: cn=testuser,cn=users,""" + self.base_dn + """
199 dBCSPwd: XXXXXXXXXXXXXXXX
201 dBCSPwd: YYYYYYYYYYYYYYYY
204 except LdbError, (num, _):
205 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
207 def test_userPassword_clear_set(self):
208 print "Performs a password cleartext set operation on 'userPassword'"
209 # Notice: This works only against Windows if "dSHeuristics" has been set
213 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
214 m["userPassword"] = MessageElement("thatsAcomplPASS2", FLAG_MOD_REPLACE,
218 def test_userPassword_clear_change(self):
219 print "Performs a password cleartext change operation on 'userPassword'"
220 # Notice: This works only against Windows if "dSHeuristics" has been set
223 self.ldb2.modify_ldif("""
224 dn: cn=testuser,cn=users,""" + self.base_dn + """
227 userPassword: thatsAcomplPASS1
229 userPassword: thatsAcomplPASS2
232 # A change to the same password again will not work (password history)
234 self.ldb2.modify_ldif("""
235 dn: cn=testuser,cn=users,""" + self.base_dn + """
238 userPassword: thatsAcomplPASS2
240 userPassword: thatsAcomplPASS2
243 except LdbError, (num, _):
244 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
246 def test_clearTextPassword_clear_set(self):
247 print "Performs a password cleartext set operation on 'clearTextPassword'"
248 # Notice: This never works against Windows - only supported by us
252 m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
253 m["clearTextPassword"] = MessageElement("thatsAcomplPASS2".encode('utf-16-le'),
254 FLAG_MOD_REPLACE, "clearTextPassword")
256 # this passes against s4
257 except LdbError, (num, msg):
258 # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
259 if num != ERR_NO_SUCH_ATTRIBUTE:
260 raise LdbError(num, msg)
262 def test_clearTextPassword_clear_change(self):
263 print "Performs a password cleartext change operation on 'clearTextPassword'"
264 # Notice: This never works against Windows - only supported by us
267 self.ldb2.modify_ldif("""
268 dn: cn=testuser,cn=users,""" + self.base_dn + """
270 delete: clearTextPassword
271 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS1".encode('utf-16-le')) + """
272 add: clearTextPassword
273 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
275 # this passes against s4
276 except LdbError, (num, msg):
277 # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
278 if num != ERR_NO_SUCH_ATTRIBUTE:
279 raise LdbError(num, msg)
281 # A change to the same password again will not work (password history)
283 self.ldb2.modify_ldif("""
284 dn: cn=testuser,cn=users,""" + self.base_dn + """
286 delete: clearTextPassword
287 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
288 add: clearTextPassword
289 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
292 except LdbError, (num, _):
293 # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
294 if num != ERR_NO_SUCH_ATTRIBUTE:
295 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
297 def test_failures(self):
298 print "Performs some failure testing"
302 dn: cn=testuser,cn=users,""" + self.base_dn + """
305 userPassword: thatsAcomplPASS1
308 except LdbError, (num, _):
309 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
312 self.ldb2.modify_ldif("""
313 dn: cn=testuser,cn=users,""" + self.base_dn + """
318 except LdbError, (num, _):
319 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
323 dn: cn=testuser,cn=users,""" + self.base_dn + """
326 userPassword: thatsAcomplPASS1
329 except LdbError, (num, _):
330 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
333 self.ldb2.modify_ldif("""
334 dn: cn=testuser,cn=users,""" + self.base_dn + """
337 userPassword: thatsAcomplPASS1
340 except LdbError, (num, _):
341 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
345 dn: cn=testuser,cn=users,""" + self.base_dn + """
348 userPassword: thatsAcomplPASS1
350 userPassword: thatsAcomplPASS2
351 userPassword: thatsAcomplPASS2
354 except LdbError, (num, _):
355 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
358 self.ldb2.modify_ldif("""
359 dn: cn=testuser,cn=users,""" + self.base_dn + """
362 userPassword: thatsAcomplPASS1
364 userPassword: thatsAcomplPASS2
365 userPassword: thatsAcomplPASS2
368 except LdbError, (num, _):
369 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
373 dn: cn=testuser,cn=users,""" + self.base_dn + """
376 userPassword: thatsAcomplPASS1
377 userPassword: thatsAcomplPASS1
379 userPassword: thatsAcomplPASS2
382 except LdbError, (num, _):
383 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
386 self.ldb2.modify_ldif("""
387 dn: cn=testuser,cn=users,""" + self.base_dn + """
390 userPassword: thatsAcomplPASS1
391 userPassword: thatsAcomplPASS1
393 userPassword: thatsAcomplPASS2
396 except LdbError, (num, _):
397 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
401 dn: cn=testuser,cn=users,""" + self.base_dn + """
404 userPassword: thatsAcomplPASS1
406 userPassword: thatsAcomplPASS2
408 userPassword: thatsAcomplPASS2
411 except LdbError, (num, _):
412 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
415 self.ldb2.modify_ldif("""
416 dn: cn=testuser,cn=users,""" + self.base_dn + """
419 userPassword: thatsAcomplPASS1
421 userPassword: thatsAcomplPASS2
423 userPassword: thatsAcomplPASS2
426 except LdbError, (num, _):
427 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
431 dn: cn=testuser,cn=users,""" + self.base_dn + """
434 userPassword: thatsAcomplPASS1
436 userPassword: thatsAcomplPASS1
438 userPassword: thatsAcomplPASS2
441 except LdbError, (num, _):
442 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
445 self.ldb2.modify_ldif("""
446 dn: cn=testuser,cn=users,""" + self.base_dn + """
449 userPassword: thatsAcomplPASS1
451 userPassword: thatsAcomplPASS1
453 userPassword: thatsAcomplPASS2
456 except LdbError, (num, _):
457 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
461 dn: cn=testuser,cn=users,""" + self.base_dn + """
464 userPassword: thatsAcomplPASS1
466 userPassword: thatsAcomplPASS2
467 replace: userPassword
468 userPassword: thatsAcomplPASS3
471 except LdbError, (num, _):
472 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
475 self.ldb2.modify_ldif("""
476 dn: cn=testuser,cn=users,""" + self.base_dn + """
479 userPassword: thatsAcomplPASS1
481 userPassword: thatsAcomplPASS2
482 replace: userPassword
483 userPassword: thatsAcomplPASS3
486 except LdbError, (num, _):
487 self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
489 # Reverse order does work
490 self.ldb2.modify_ldif("""
491 dn: cn=testuser,cn=users,""" + self.base_dn + """
494 userPassword: thatsAcomplPASS2
496 userPassword: thatsAcomplPASS1
500 self.ldb2.modify_ldif("""
501 dn: cn=testuser,cn=users,""" + self.base_dn + """
504 userPassword: thatsAcomplPASS2
506 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')) + """
508 # this passes against s4
509 except LdbError, (num, _):
510 self.assertEquals(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
513 self.ldb2.modify_ldif("""
514 dn: cn=testuser,cn=users,""" + self.base_dn + """
517 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')) + """
519 userPassword: thatsAcomplPASS4
521 # this passes against s4
522 except LdbError, (num, _):
523 self.assertEquals(num, ERR_NO_SUCH_ATTRIBUTE)
525 # Several password changes at once are allowed
527 dn: cn=testuser,cn=users,""" + self.base_dn + """
529 replace: userPassword
530 userPassword: thatsAcomplPASS1
531 userPassword: thatsAcomplPASS2
534 # Several password changes at once are allowed
536 dn: cn=testuser,cn=users,""" + self.base_dn + """
538 replace: userPassword
539 userPassword: thatsAcomplPASS1
540 userPassword: thatsAcomplPASS2
541 replace: userPassword
542 userPassword: thatsAcomplPASS3
543 replace: userPassword
544 userPassword: thatsAcomplPASS4
547 # This surprisingly should work
548 self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
550 "dn": "cn=testuser2,cn=users," + self.base_dn,
551 "objectclass": ["user", "person"],
552 "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS2"] })
554 # This surprisingly should work
555 self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
557 "dn": "cn=testuser2,cn=users," + self.base_dn,
558 "objectclass": ["user", "person"],
559 "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS1"] })
562 self.delete_force(self.ldb, "cn=testuser,cn=users," + self.base_dn)
563 self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
564 # Close the second LDB connection (with the user credentials)
567 if not "://" in host:
568 if os.path.isfile(host):
569 host = "tdb://%s" % host
571 host = "ldap://%s" % host
573 ldb = SamDB(url=host, session_info=system_session(), credentials=creds, lp=lp)
575 runner = SubunitTestRunner()
577 if not runner.run(unittest.makeSuite(PasswordTests)).wasSuccessful():