s4:ldap.py - add testcase which demonstrates the reset of the "primaryGroupID"
[ira/wip.git] / source4 / lib / ldb / tests / python / passwords.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # This tests the password changes over LDAP for AD implementations
4 #
5 # Copyright Matthias Dieter Wallnoefer 2010
6 #
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
9 # MS-ADTS 3.1.1.3.1.5
10 #
11 # Important: Make sure that the minimum password age is set to "0"!
12
13 import optparse
14 import sys
15 import time
16 import base64
17 import os
18
19 sys.path.append("bin/python")
20
21 import samba.getopt as options
22
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
38 import unittest
39
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()
48
49 if len(args) < 1:
50     parser.print_usage()
51     sys.exit(1)
52
53 host = args[0]
54
55 lp = sambaopts.get_loadparm()
56 creds = credopts.get_credentials(lp)
57
58 # Force an encrypted connection
59 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
60
61 #
62 # Tests start here
63 #
64
65 class PasswordTests(unittest.TestCase):
66     def delete_force(self, ldb, dn):
67         try:
68             ldb.delete(dn)
69         except LdbError, (num, _):
70             self.assertEquals(num, ERR_NO_SUCH_OBJECT)
71
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]
77
78     def setUp(self):
79         self.ldb = ldb
80         self.base_dn = self.find_basedn(ldb)
81
82         # (Re)adds the test user "testuser" with the inital password
83         # "thatsAcomplPASS1"
84         self.delete_force(self.ldb, "cn=testuser,cn=users," + self.base_dn)
85         self.ldb.add({
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)")
91
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)
108
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
112
113         m = Message()
114         m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
115         m["unicodePwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
116           "unicodePwd")
117         try:
118             ldb.modify(m)
119             self.fail()
120         except LdbError, (num, _):
121             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
122
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
126
127         # Hash password changes should never work
128         try:
129             self.ldb2.modify_ldif("""
130 dn: cn=testuser,cn=users,""" + self.base_dn + """
131 changetype: modify
132 delete: unicodePwd
133 unicodePwd: XXXXXXXXXXXXXXXX
134 add: unicodePwd
135 unicodePwd: YYYYYYYYYYYYYYYY
136 """)
137             self.fail()
138         except LdbError, (num, _):
139             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
140
141     def test_unicodePwd_clear_set(self):
142         print "Performs a password cleartext set operation on 'unicodePwd'"
143
144         m = Message()
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")
148         ldb.modify(m)
149
150     def test_unicodePwd_clear_change(self):
151         print "Performs a password cleartext change operation on 'unicodePwd'"
152
153         self.ldb2.modify_ldif("""
154 dn: cn=testuser,cn=users,""" + self.base_dn + """
155 changetype: modify
156 delete: unicodePwd
157 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')) + """
158 add: unicodePwd
159 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
160 """)
161
162         # A change to the same password again will not work (password history)
163         try:
164             self.ldb2.modify_ldif("""
165 dn: cn=testuser,cn=users,""" + self.base_dn + """
166 changetype: modify
167 delete: unicodePwd
168 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
169 add: unicodePwd
170 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
171 """)
172             self.fail()
173         except LdbError, (num, _):
174             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
175
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
179
180         m = Message()
181         m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
182         m["dBCSPwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
183           "dBCSPwd")
184         try:
185             ldb.modify(m)
186             self.fail()
187         except LdbError, (num, _):
188             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
189
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
193
194         try:
195             self.ldb2.modify_ldif("""
196 dn: cn=testuser,cn=users,""" + self.base_dn + """
197 changetype: modify
198 delete: dBCSPwd
199 dBCSPwd: XXXXXXXXXXXXXXXX
200 add: dBCSPwd
201 dBCSPwd: YYYYYYYYYYYYYYYY
202 """)
203             self.fail()
204         except LdbError, (num, _):
205             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
206
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
210         # properly
211
212         m = Message()
213         m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
214         m["userPassword"] = MessageElement("thatsAcomplPASS2", FLAG_MOD_REPLACE,
215           "userPassword")
216         ldb.modify(m)
217
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
221         # properly
222
223         self.ldb2.modify_ldif("""
224 dn: cn=testuser,cn=users,""" + self.base_dn + """
225 changetype: modify
226 delete: userPassword
227 userPassword: thatsAcomplPASS1
228 add: userPassword
229 userPassword: thatsAcomplPASS2
230 """)
231
232         # A change to the same password again will not work (password history)
233         try:
234             self.ldb2.modify_ldif("""
235 dn: cn=testuser,cn=users,""" + self.base_dn + """
236 changetype: modify
237 delete: userPassword
238 userPassword: thatsAcomplPASS2
239 add: userPassword
240 userPassword: thatsAcomplPASS2
241 """)
242             self.fail()
243         except LdbError, (num, _):
244             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
245
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
249
250         try:
251             m = Message()
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")
255             ldb.modify(m)
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)
261
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
265
266         try:
267             self.ldb2.modify_ldif("""
268 dn: cn=testuser,cn=users,""" + self.base_dn + """
269 changetype: modify
270 delete: clearTextPassword
271 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS1".encode('utf-16-le')) + """
272 add: clearTextPassword
273 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
274 """)
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)
280
281         # A change to the same password again will not work (password history)
282         try:
283             self.ldb2.modify_ldif("""
284 dn: cn=testuser,cn=users,""" + self.base_dn + """
285 changetype: modify
286 delete: clearTextPassword
287 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
288 add: clearTextPassword
289 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
290 """)
291             self.fail()
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)
296
297     def test_failures(self):
298         print "Performs some failure testing"
299
300         try:
301             ldb.modify_ldif("""
302 dn: cn=testuser,cn=users,""" + self.base_dn + """
303 changetype: modify
304 delete: userPassword
305 userPassword: thatsAcomplPASS1
306 """)
307             self.fail()
308         except LdbError, (num, _):
309             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
310
311         try:
312             self.ldb2.modify_ldif("""
313 dn: cn=testuser,cn=users,""" + self.base_dn + """
314 changetype: modify
315 delete: userPassword
316 """)
317             self.fail()
318         except LdbError, (num, _):
319             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
320
321         try:
322             ldb.modify_ldif("""
323 dn: cn=testuser,cn=users,""" + self.base_dn + """
324 changetype: modify
325 add: userPassword
326 userPassword: thatsAcomplPASS1
327 """)
328             self.fail()
329         except LdbError, (num, _):
330             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
331
332         try:
333             self.ldb2.modify_ldif("""
334 dn: cn=testuser,cn=users,""" + self.base_dn + """
335 changetype: modify
336 add: userPassword
337 userPassword: thatsAcomplPASS1
338 """)
339             self.fail()
340         except LdbError, (num, _):
341             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
342
343         try:
344             ldb.modify_ldif("""
345 dn: cn=testuser,cn=users,""" + self.base_dn + """
346 changetype: modify
347 delete: userPassword
348 userPassword: thatsAcomplPASS1
349 add: userPassword
350 userPassword: thatsAcomplPASS2
351 userPassword: thatsAcomplPASS2
352 """)
353             self.fail()
354         except LdbError, (num, _):
355             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
356
357         try:
358             self.ldb2.modify_ldif("""
359 dn: cn=testuser,cn=users,""" + self.base_dn + """
360 changetype: modify
361 delete: userPassword
362 userPassword: thatsAcomplPASS1
363 add: userPassword
364 userPassword: thatsAcomplPASS2
365 userPassword: thatsAcomplPASS2
366 """)
367             self.fail()
368         except LdbError, (num, _):
369             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
370
371         try:
372             ldb.modify_ldif("""
373 dn: cn=testuser,cn=users,""" + self.base_dn + """
374 changetype: modify
375 delete: userPassword
376 userPassword: thatsAcomplPASS1
377 userPassword: thatsAcomplPASS1
378 add: userPassword
379 userPassword: thatsAcomplPASS2
380 """)
381             self.fail()
382         except LdbError, (num, _):
383             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
384
385         try:
386             self.ldb2.modify_ldif("""
387 dn: cn=testuser,cn=users,""" + self.base_dn + """
388 changetype: modify
389 delete: userPassword
390 userPassword: thatsAcomplPASS1
391 userPassword: thatsAcomplPASS1
392 add: userPassword
393 userPassword: thatsAcomplPASS2
394 """)
395             self.fail()
396         except LdbError, (num, _):
397             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
398
399         try:
400             ldb.modify_ldif("""
401 dn: cn=testuser,cn=users,""" + self.base_dn + """
402 changetype: modify
403 delete: userPassword
404 userPassword: thatsAcomplPASS1
405 add: userPassword
406 userPassword: thatsAcomplPASS2
407 add: userPassword
408 userPassword: thatsAcomplPASS2
409 """)
410             self.fail()
411         except LdbError, (num, _):
412             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
413
414         try:
415             self.ldb2.modify_ldif("""
416 dn: cn=testuser,cn=users,""" + self.base_dn + """
417 changetype: modify
418 delete: userPassword
419 userPassword: thatsAcomplPASS1
420 add: userPassword
421 userPassword: thatsAcomplPASS2
422 add: userPassword
423 userPassword: thatsAcomplPASS2
424 """)
425             self.fail()
426         except LdbError, (num, _):
427             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
428
429         try:
430             ldb.modify_ldif("""
431 dn: cn=testuser,cn=users,""" + self.base_dn + """
432 changetype: modify
433 delete: userPassword
434 userPassword: thatsAcomplPASS1
435 delete: userPassword
436 userPassword: thatsAcomplPASS1
437 add: userPassword
438 userPassword: thatsAcomplPASS2
439 """)
440             self.fail()
441         except LdbError, (num, _):
442             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
443
444         try:
445             self.ldb2.modify_ldif("""
446 dn: cn=testuser,cn=users,""" + self.base_dn + """
447 changetype: modify
448 delete: userPassword
449 userPassword: thatsAcomplPASS1
450 delete: userPassword
451 userPassword: thatsAcomplPASS1
452 add: userPassword
453 userPassword: thatsAcomplPASS2
454 """)
455             self.fail()
456         except LdbError, (num, _):
457             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
458
459         try:
460             ldb.modify_ldif("""
461 dn: cn=testuser,cn=users,""" + self.base_dn + """
462 changetype: modify
463 delete: userPassword
464 userPassword: thatsAcomplPASS1
465 add: userPassword
466 userPassword: thatsAcomplPASS2
467 replace: userPassword
468 userPassword: thatsAcomplPASS3
469 """)
470             self.fail()
471         except LdbError, (num, _):
472             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
473
474         try:
475             self.ldb2.modify_ldif("""
476 dn: cn=testuser,cn=users,""" + self.base_dn + """
477 changetype: modify
478 delete: userPassword
479 userPassword: thatsAcomplPASS1
480 add: userPassword
481 userPassword: thatsAcomplPASS2
482 replace: userPassword
483 userPassword: thatsAcomplPASS3
484 """)
485             self.fail()
486         except LdbError, (num, _):
487             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
488
489         # Reverse order does work
490         self.ldb2.modify_ldif("""
491 dn: cn=testuser,cn=users,""" + self.base_dn + """
492 changetype: modify
493 add: userPassword
494 userPassword: thatsAcomplPASS2
495 delete: userPassword
496 userPassword: thatsAcomplPASS1
497 """)
498
499         try:
500             self.ldb2.modify_ldif("""
501 dn: cn=testuser,cn=users,""" + self.base_dn + """
502 changetype: modify
503 delete: userPassword
504 userPassword: thatsAcomplPASS2
505 add: unicodePwd
506 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')) + """
507 """)
508              # this passes against s4
509         except LdbError, (num, _):
510             self.assertEquals(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
511
512         try:
513             self.ldb2.modify_ldif("""
514 dn: cn=testuser,cn=users,""" + self.base_dn + """
515 changetype: modify
516 delete: unicodePwd
517 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')) + """
518 add: userPassword
519 userPassword: thatsAcomplPASS4
520 """)
521              # this passes against s4
522         except LdbError, (num, _):
523             self.assertEquals(num, ERR_NO_SUCH_ATTRIBUTE)
524
525         # Several password changes at once are allowed
526         ldb.modify_ldif("""
527 dn: cn=testuser,cn=users,""" + self.base_dn + """
528 changetype: modify
529 replace: userPassword
530 userPassword: thatsAcomplPASS1
531 userPassword: thatsAcomplPASS2
532 """)
533
534         # Several password changes at once are allowed
535         ldb.modify_ldif("""
536 dn: cn=testuser,cn=users,""" + self.base_dn + """
537 changetype: modify
538 replace: userPassword
539 userPassword: thatsAcomplPASS1
540 userPassword: thatsAcomplPASS2
541 replace: userPassword
542 userPassword: thatsAcomplPASS3
543 replace: userPassword
544 userPassword: thatsAcomplPASS4
545 """)
546
547         # This surprisingly should work
548         self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
549         self.ldb.add({
550              "dn": "cn=testuser2,cn=users," + self.base_dn,
551              "objectclass": ["user", "person"],
552              "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS2"] })
553
554         # This surprisingly should work
555         self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
556         self.ldb.add({
557              "dn": "cn=testuser2,cn=users," + self.base_dn,
558              "objectclass": ["user", "person"],
559              "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS1"] })
560
561     def tearDown(self):
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)
565         self.ldb2 = None
566
567 if not "://" in host:
568     if os.path.isfile(host):
569         host = "tdb://%s" % host
570     else:
571         host = "ldap://%s" % host
572
573 ldb = SamDB(url=host, session_info=system_session(), credentials=creds, lp=lp)
574
575 runner = SubunitTestRunner()
576 rc = 0
577 if not runner.run(unittest.makeSuite(PasswordTests)).wasSuccessful():
578     rc = 1
579 sys.exit(rc)