Fix PEP8 warning E711 comparison to None
[nivanova/samba-autobuild/.git] / source4 / dsdb / tests / python / password_settings.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # Tests for Password Settings Objects.
5 #
6 # This also tests the default password complexity (i.e. pwdProperties),
7 # minPwdLength, pwdHistoryLength settings as a side-effect.
8 #
9 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
10 #
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
23 #
24
25 #
26 # Usage:
27 #  export SERVER_IP=target_dc
28 #  export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun
29 #  PYTHONPATH="$PYTHONPATH:$samba4srcdir/dsdb/tests/python" $SUBUNITRUN password_settings -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD"
30 #
31
32 import samba.tests
33 import ldb
34 from ldb import SCOPE_BASE, FLAG_MOD_DELETE, FLAG_MOD_ADD, FLAG_MOD_REPLACE
35 from samba import dsdb
36 import time
37 from samba.tests.password_test import PasswordTestCase
38 from samba.tests.pso import TestUser
39 from samba.tests.pso import PasswordSettings
40 from samba.credentials import Credentials
41 from samba.samdb import SamDB
42 from samba import gensec
43 from samba.dcerpc.samr import DOMAIN_PASSWORD_STORE_CLEARTEXT
44 from samba.auth import system_session
45 import base64
46
47 class PasswordSettingsTestCase(PasswordTestCase):
48     def setUp(self):
49         super(PasswordSettingsTestCase, self).setUp()
50
51         self.host_url = "ldap://%s" % samba.tests.env_get_var_value("SERVER_IP")
52         self.ldb = samba.tests.connect_samdb(self.host_url)
53
54         # create a temp OU to put this test's users into
55         self.ou = samba.tests.create_test_ou(self.ldb, "password_settings")
56
57         # update DC to allow password changes for the duration of this test
58         self.allow_password_changes()
59
60         # store the current password-settings for the domain
61         self.pwd_defaults = PasswordSettings(None, self.ldb)
62         self.test_objs = []
63
64     def tearDown(self):
65         super(PasswordSettingsTestCase, self).tearDown()
66
67         # remove all objects under the top-level OU
68         self.ldb.delete(self.ou, ["tree_delete:1"])
69
70         # PSOs can't reside within an OU so they need to be cleaned up separately
71         for obj in self.test_objs:
72             self.ldb.delete(obj)
73
74     def add_obj_cleanup(self, dn_list):
75         """Handles cleanup of objects outside of the test OU in the tearDown"""
76         self.test_objs.extend(dn_list)
77
78     def add_group(self, group_name):
79         """Creates a new group"""
80         dn = "CN=%s,%s" %(group_name, self.ou)
81         self.ldb.add({"dn": dn, "objectclass": "group"})
82         return dn
83
84     def set_attribute(self, dn, attr, value, operation=FLAG_MOD_ADD, samdb=None):
85         """Modifies an attribute for an object"""
86         if samdb is None:
87             samdb = self.ldb
88         m = ldb.Message()
89         m.dn = ldb.Dn(samdb, dn)
90         m[attr] = ldb.MessageElement(value, operation, attr)
91         samdb.modify(m)
92
93     def add_user(self, username):
94         # add a new user to the DB under our top-level OU
95         userou = "ou=%s" % self.ou.get_component_value(0)
96         return TestUser(username, self.ldb, userou=userou)
97
98     def assert_password_invalid(self, user, password):
99         """
100         Check we can't set a password that violates complexity or length
101         constraints
102         """
103         try:
104             user.set_password(password)
105             # fail the test if no exception was encountered
106             self.fail("Password '%s' should have been rejected" % password)
107         except ldb.LdbError as e:
108             (num, msg) = e.args
109             self.assertEquals(num, ldb.ERR_CONSTRAINT_VIOLATION, msg)
110             self.assertTrue('0000052D' in msg, msg)
111
112     def assert_password_valid(self, user, password):
113         """Checks that we can set a password successfully"""
114         try:
115             user.set_password(password)
116         except ldb.LdbError as e:
117             (num, msg) = e.args
118             # fail the test (rather than throw an error)
119             self.fail("Password '%s' unexpectedly rejected: %s" %(password, msg))
120
121     def assert_PSO_applied(self, user, pso):
122         """
123         Asserts the expected PSO is applied by checking the msDS-ResultantPSO
124         attribute, as well as checking the corresponding password-length,
125         complexity, and history are enforced correctly
126         """
127         resultant_pso = user.get_resultant_PSO()
128         self.assertTrue(resultant_pso == pso.dn,
129                         "Expected PSO %s, not %s" %(pso.name,
130                                                     str(resultant_pso)))
131
132         # we're mirroring the pwd_history for the user, so make sure this is
133         # up-to-date, before we start making password changes
134         if user.last_pso:
135             user.pwd_history_change(user.last_pso.history_len, pso.history_len)
136         user.last_pso = pso
137
138         # check if we can set a sufficiently long, but non-complex, password.
139         # (We use the history-size to generate a unique password for each
140         # assertion - otherwise, if the password is already in the history,
141         # then it'll be rejected)
142         unique_char = chr(ord('a') + len(user.all_old_passwords))
143         noncomplex_pwd = "%cabcdefghijklmnopqrst" % unique_char
144
145         if pso.complexity:
146             self.assert_password_invalid(user, noncomplex_pwd)
147         else:
148             self.assert_password_valid(user, noncomplex_pwd)
149
150         # use a unique and sufficiently complex base-string to check pwd-length
151         pass_phrase = "%d#AaBbCcDdEeFfGgHhIi" % len(user.all_old_passwords)
152
153         # check that passwords less than the specified length are rejected
154         for i in range(3, pso.password_len):
155             self.assert_password_invalid(user, pass_phrase[:i])
156
157         # check we can set a password that's exactly the minimum length
158         self.assert_password_valid(user, pass_phrase[:pso.password_len])
159
160         # check the password history is enforced correctly.
161         # first, check the last n items in the password history are invalid
162         invalid_passwords = user.old_invalid_passwords(pso.history_len)
163         for pwd in invalid_passwords:
164             self.assert_password_invalid(user, pwd)
165
166         # next, check any passwords older than the history-len can be re-used
167         valid_passwords = user.old_valid_passwords(pso.history_len)
168         for pwd in valid_passwords:
169             self.assert_set_old_password(user, pwd, pso)
170
171     def password_is_complex(self, password):
172         # non-complex passwords used in the tests are all lower-case letters
173         # If it's got a number in the password, assume it's complex
174         return any(c.isdigit() for c in password)
175
176     def assert_set_old_password(self, user, password, pso):
177         """
178         Checks a user password can be set (if the password conforms to the PSO
179         settings). Used to check an old password that falls outside the history
180         length, but might still be invalid for other reasons.
181         """
182         if self.password_is_complex(password):
183             # check password conforms to length requirements
184             if len(password) < pso.password_len:
185                 self.assert_password_invalid(user, password)
186             else:
187                 self.assert_password_valid(user, password)
188         else:
189             # password is not complex, check PSO handles it appropriately
190             if pso.complexity:
191                 self.assert_password_invalid(user, password)
192             else:
193                 self.assert_password_valid(user, password)
194
195     def test_pso_basics(self):
196         """Simple tests that a PSO takes effect when applied to a group or user"""
197
198         # create some PSOs that vary in priority and basic password-len
199         best_pso = PasswordSettings("highest-priority-PSO", self.ldb,
200                                     precedence=5, password_len=16,
201                                     history_len=6)
202         medium_pso = PasswordSettings("med-priority-PSO", self.ldb,
203                                       precedence=15, password_len=10,
204                                       history_len=4)
205         worst_pso = PasswordSettings("lowest-priority-PSO", self.ldb,
206                                      precedence=100, complexity=False,
207                                      password_len=4, history_len=2)
208
209         # handle PSO clean-up (as they're outside the top-level test OU)
210         self.add_obj_cleanup([worst_pso.dn, medium_pso.dn, best_pso.dn])
211
212         # create some groups and apply the PSOs to the groups
213         group1 = self.add_group("Group-1")
214         group2 = self.add_group("Group-2")
215         group3 = self.add_group("Group-3")
216         group4 = self.add_group("Group-4")
217         worst_pso.apply_to(group1)
218         medium_pso.apply_to(group2)
219         best_pso.apply_to(group3)
220         worst_pso.apply_to(group4)
221
222         # create a user and check the default settings apply to it
223         user = self.add_user("testuser")
224         self.assert_PSO_applied(user, self.pwd_defaults)
225
226         # add user to a group. Check that the group's PSO applies to the user
227         self.set_attribute(group1, "member", user.dn)
228         self.assert_PSO_applied(user, worst_pso)
229
230         # add the user to a group with a higher precedence PSO and and check
231         # that now trumps the previous PSO
232         self.set_attribute(group2, "member", user.dn)
233         self.assert_PSO_applied(user, medium_pso)
234
235         # add the user to the remaining groups. The highest precedence PSO
236         # should now take effect
237         self.set_attribute(group3, "member", user.dn)
238         self.set_attribute(group4, "member", user.dn)
239         self.assert_PSO_applied(user, best_pso)
240
241         # delete a group membership and check the PSO changes
242         self.set_attribute(group3, "member", user.dn, operation=FLAG_MOD_DELETE)
243         self.assert_PSO_applied(user, medium_pso)
244
245         # apply the low-precedence PSO directly to the user
246         # (directly applied PSOs should trump higher precedence group PSOs)
247         worst_pso.apply_to(user.dn)
248         self.assert_PSO_applied(user, worst_pso)
249
250         # remove applying the PSO directly to the user and check PSO changes
251         worst_pso.unapply(user.dn)
252         self.assert_PSO_applied(user, medium_pso)
253
254         # remove all appliesTo and check we have the default settings again
255         worst_pso.unapply(group1)
256         medium_pso.unapply(group2)
257         worst_pso.unapply(group4)
258         self.assert_PSO_applied(user, self.pwd_defaults)
259
260     def test_pso_nested_groups(self):
261         """PSOs operate correctly when applied to nested groups"""
262
263         # create some PSOs that vary in priority and basic password-len
264         group1_pso = PasswordSettings("group1-PSO", self.ldb, precedence=50,
265                                       password_len=12, history_len=3)
266         group2_pso = PasswordSettings("group2-PSO", self.ldb, precedence=25,
267                                       password_len=10, history_len=5,
268                                       complexity=False)
269         group3_pso = PasswordSettings("group3-PSO", self.ldb, precedence=10,
270                                       password_len=6, history_len=2)
271
272         # create some groups and apply the PSOs to the groups
273         group1 = self.add_group("Group-1")
274         group2 = self.add_group("Group-2")
275         group3 = self.add_group("Group-3")
276         group4 = self.add_group("Group-4")
277         group1_pso.apply_to(group1)
278         group2_pso.apply_to(group2)
279         group3_pso.apply_to(group3)
280
281         # create a PSO and apply it to a group that the user is not a member
282         # of - it should not have any effect on the user
283         unused_pso = PasswordSettings("unused-PSO", self.ldb, precedence=1,
284                                       password_len=20)
285         unused_pso.apply_to(group4)
286
287         # handle PSO clean-up (as they're outside the top-level test OU)
288         self.add_obj_cleanup([group1_pso.dn, group2_pso.dn, group3_pso.dn,
289                               unused_pso.dn])
290
291         # create a user and check the default settings apply to it
292         user = self.add_user("testuser")
293         self.assert_PSO_applied(user, self.pwd_defaults)
294
295         # add user to a group. Check that the group's PSO applies to the user
296         self.set_attribute(group1, "member", user.dn)
297         self.set_attribute(group2, "member", group1)
298         self.assert_PSO_applied(user, group2_pso)
299
300         # add another level to the group heirachy & check this PSO takes effect
301         self.set_attribute(group3, "member", group2)
302         self.assert_PSO_applied(user, group3_pso)
303
304         # invert the PSO precedence and check the new lowest value takes effect
305         group1_pso.set_precedence(3)
306         group2_pso.set_precedence(13)
307         group3_pso.set_precedence(33)
308         self.assert_PSO_applied(user, group1_pso)
309
310         # delete a PSO and check it no longer applies
311         self.ldb.delete(group1_pso.dn)
312         self.test_objs.remove(group1_pso.dn)
313         self.assert_PSO_applied(user, group2_pso)
314
315     def get_guid(self, dn):
316         res = self.ldb.search(base=dn, attrs=["objectGUID"], scope=ldb.SCOPE_BASE)
317         return res[0]['objectGUID'][0]
318
319     def guid_string(self, guid):
320         return self.ldb.schema_format_value("objectGUID", guid)
321
322     def PSO_with_lowest_GUID(self, pso_list):
323         """Returns the PSO object in the list with the lowest GUID"""
324         # go through each PSO and fetch its GUID
325         guid_list = []
326         mapping = {}
327         for pso in pso_list:
328             guid = self.get_guid(pso.dn)
329             guid_list.append(guid)
330             # remember which GUID maps to what PSO
331             mapping[guid] = pso
332
333         # sort the GUID list to work out the lowest/best GUID
334         guid_list.sort()
335         best_guid = guid_list[0]
336
337         # sanity-check the mapping between GUID and DN is correct
338         self.assertEqual(self.guid_string(self.get_guid(mapping[best_guid].dn)),
339                          self.guid_string(best_guid))
340
341         # return the PSO that this GUID corresponds to
342         return mapping[best_guid]
343
344     def test_pso_equal_precedence(self):
345         """Tests expected PSO wins when several have the same precedence"""
346
347         # create some PSOs that vary in priority and basic password-len
348         pso1 = PasswordSettings("PSO-1", self.ldb, precedence=5, history_len=1,
349                                 password_len=11)
350         pso2 = PasswordSettings("PSO-2", self.ldb, precedence=5, history_len=2,
351                                 password_len=8)
352         pso3 = PasswordSettings("PSO-3", self.ldb, precedence=5, history_len=3,
353                                 password_len=5, complexity=False)
354         pso4 = PasswordSettings("PSO-4", self.ldb, precedence=5, history_len=4,
355                                 password_len=13, complexity=False)
356
357         # handle PSO clean-up (as they're outside the top-level test OU)
358         self.add_obj_cleanup([pso1.dn, pso2.dn, pso3.dn, pso4.dn])
359
360         # create some groups and apply the PSOs to the groups
361         group1 = self.add_group("Group-1")
362         group2 = self.add_group("Group-2")
363         group3 = self.add_group("Group-3")
364         group4 = self.add_group("Group-4")
365         pso1.apply_to(group1)
366         pso2.apply_to(group2)
367         pso3.apply_to(group3)
368         pso4.apply_to(group4)
369
370         # create a user and check the default settings apply to it
371         user = self.add_user("testuser")
372         self.assert_PSO_applied(user, self.pwd_defaults)
373
374         # add the user to all the groups
375         self.set_attribute(group1, "member", user.dn)
376         self.set_attribute(group2, "member", user.dn)
377         self.set_attribute(group3, "member", user.dn)
378         self.set_attribute(group4, "member", user.dn)
379
380         # precedence is equal, so the PSO with lowest GUID gets applied
381         pso_list = [pso1, pso2, pso3, pso4]
382         best_pso = self.PSO_with_lowest_GUID(pso_list)
383         self.assert_PSO_applied(user, best_pso)
384
385         # excluding the winning PSO, apply the other PSOs directly to the user
386         pso_list.remove(best_pso)
387         for pso in pso_list:
388             pso.apply_to(user.dn)
389
390         # we should now have a different PSO applied (the 2nd lowest GUID)
391         next_best_pso = self.PSO_with_lowest_GUID(pso_list)
392         self.assertTrue(next_best_pso is not best_pso)
393         self.assert_PSO_applied(user, next_best_pso)
394
395         # bump the precedence of another PSO and it should now win
396         pso_list.remove(next_best_pso)
397         best_pso = pso_list[0]
398         best_pso.set_precedence(4)
399         self.assert_PSO_applied(user, best_pso)
400
401     def test_pso_invalid_location(self):
402         """Tests that PSOs in an invalid location have no effect"""
403
404         # PSOs should only be able to be created within a Password Settings
405         # Container object. Trying to create one under an OU should fail
406         try:
407             rogue_pso = PasswordSettings("rogue-PSO", self.ldb, precedence=1,
408                                          complexity=False, password_len=20,
409                                          container=self.ou)
410             self.fail()
411         except ldb.LdbError as e:
412             (num, msg) = e.args
413             self.assertEquals(num, ldb.ERR_NAMING_VIOLATION, msg)
414             # Windows returns 2099 (Illegal superior), Samba returns 2037
415             # (Naming violation - "not a valid child class")
416             self.assertTrue('00002099' in msg or '00002037' in msg, msg)
417
418         # we can't create Password Settings Containers under an OU either
419         try:
420             rogue_psc = "CN=Rogue-PSO-container,%s" % self.ou
421             self.ldb.add({"dn": rogue_psc,
422                           "objectclass": "msDS-PasswordSettingsContainer"})
423             self.fail()
424         except ldb.LdbError as e:
425             (num, msg) = e.args
426             self.assertEquals(num, ldb.ERR_NAMING_VIOLATION, msg)
427             self.assertTrue('00002099' in msg or '00002037' in msg, msg)
428
429         base_dn = self.ldb.get_default_basedn()
430         rogue_psc = "CN=Rogue-PSO-container,CN=Computers,%s" % base_dn
431         self.ldb.add({"dn": rogue_psc,
432                       "objectclass": "msDS-PasswordSettingsContainer"})
433
434         rogue_pso = PasswordSettings("rogue-PSO", self.ldb, precedence=1,
435                                      container=rogue_psc, password_len=20)
436         self.add_obj_cleanup([rogue_pso.dn, rogue_psc])
437
438         # apply the PSO to a group and check it has no effect on the user
439         user = self.add_user("testuser")
440         group = self.add_group("Group-1")
441         rogue_pso.apply_to(group)
442         self.set_attribute(group, "member", user.dn)
443         self.assert_PSO_applied(user, self.pwd_defaults)
444
445         # apply the PSO directly to the user and check it has no effect
446         rogue_pso.apply_to(user.dn)
447         self.assert_PSO_applied(user, self.pwd_defaults)
448
449     # the PSOs created in these test-cases all use a default min-age of zero.
450     # This is the only test case that checks the PSO's min-age is enforced
451     def test_pso_min_age(self):
452         """Tests that a PSO's min-age is enforced"""
453         pso = PasswordSettings("min-age-PSO", self.ldb, password_len=10,
454                                password_age_min=2, complexity=False)
455         self.add_obj_cleanup([pso.dn])
456
457         # create a user and apply the PSO
458         user = self.add_user("testuser")
459         pso.apply_to(user.dn)
460         self.assertTrue(user.get_resultant_PSO() == pso.dn)
461
462         # changing the password immediately should fail, even if password is valid
463         valid_password = "min-age-passwd"
464         self.assert_password_invalid(user, valid_password)
465         # then trying the same password later should succeed
466         time.sleep(pso.password_age_min + 0.5)
467         self.assert_password_valid(user, valid_password)
468
469     def test_pso_max_age(self):
470         """Tests that a PSO's max-age is used"""
471
472         # create PSOs that use the domain's max-age +/- 1 day
473         domain_max_age = self.pwd_defaults.password_age_max
474         day_in_secs = 60 * 60 * 24
475         higher_max_age = domain_max_age + day_in_secs
476         lower_max_age = domain_max_age - day_in_secs
477         longer_pso = PasswordSettings("longer-age-PSO", self.ldb, precedence=5,
478                                       password_age_max=higher_max_age)
479         shorter_pso = PasswordSettings("shorter-age-PSO", self.ldb,
480                                        precedence=1,
481                                        password_age_max=lower_max_age)
482         self.add_obj_cleanup([longer_pso.dn, shorter_pso.dn])
483
484         user = self.add_user("testuser")
485
486         # we can't wait around long enough for the max-age to expire, so instead
487         # just check the msDS-UserPasswordExpiryTimeComputed for the user
488         attrs=['msDS-UserPasswordExpiryTimeComputed']
489         res = self.ldb.search(user.dn, attrs=attrs)
490         domain_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
491
492         # apply the longer PSO and check the expiry-time becomes longer
493         longer_pso.apply_to(user.dn)
494         self.assertTrue(user.get_resultant_PSO() == longer_pso.dn)
495         res = self.ldb.search(user.dn, attrs=attrs)
496         new_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
497
498         # use timestamp diff of 1 day - 1 minute. The new expiry should still
499         # be greater than this, without getting into nano-second granularity
500         approx_timestamp_diff = (day_in_secs - 60) * (1e7)
501         self.assertTrue(new_expiry > domain_expiry + approx_timestamp_diff)
502
503         # apply the shorter PSO and check the expiry-time is shorter
504         shorter_pso.apply_to(user.dn)
505         self.assertTrue(user.get_resultant_PSO() == shorter_pso.dn)
506         res = self.ldb.search(user.dn, attrs=attrs)
507         new_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
508         self.assertTrue(new_expiry < domain_expiry - approx_timestamp_diff)
509
510     def test_pso_special_groups(self):
511         """Checks applying a PSO to built-in AD groups takes effect"""
512
513         # create some PSOs that will apply to special groups
514         default_pso = PasswordSettings("default-PSO", self.ldb, precedence=20,
515                                        password_len=8, complexity=False)
516         guest_pso = PasswordSettings("guest-PSO", self.ldb, history_len=4,
517                                      precedence=5, password_len=5)
518         builtin_pso = PasswordSettings("builtin-PSO", self.ldb, history_len=9,
519                                        precedence=1, password_len=9)
520         admin_pso = PasswordSettings("admin-PSO", self.ldb, history_len=0,
521                                      precedence=2, password_len=10)
522         self.add_obj_cleanup([default_pso.dn, guest_pso.dn, admin_pso.dn,
523                               builtin_pso.dn])
524         domain_users = "CN=Domain Users,CN=Users,%s" % self.ldb.domain_dn()
525         domain_guests = "CN=Domain Guests,CN=Users,%s" % self.ldb.domain_dn()
526         admin_users = "CN=Domain Admins,CN=Users,%s" % self.ldb.domain_dn()
527
528         # if we apply a PSO to Domain Users (which all users are a member of)
529         # then that PSO should take effect on a new user
530         default_pso.apply_to(domain_users)
531         user = self.add_user("testuser")
532         self.assert_PSO_applied(user, default_pso)
533
534         # Apply a PSO to a builtin group. 'Domain Users' should be a member of
535         # Builtin/Users, but builtin groups should be excluded from the PSO
536         # calculation, so this should have no effect
537         builtin_pso.apply_to("CN=Users,CN=Builtin,%s" % self.ldb.domain_dn())
538         builtin_pso.apply_to("CN=Administrators,CN=Builtin,%s" % self.ldb.domain_dn())
539         self.assert_PSO_applied(user, default_pso)
540
541         # change the user's primary group to another group (the primaryGroupID
542         # is a little odd in that there's no memberOf backlink for it)
543         self.set_attribute(domain_guests, "member", user.dn)
544         user.set_primary_group(domain_guests)
545         # No PSO is applied to the Domain Guests yet, so the default PSO should
546         # still apply
547         self.assert_PSO_applied(user, default_pso)
548
549         # now apply a PSO to the guests group, which should trump the default
550         # PSO (because the guest PSO has a better precedence)
551         guest_pso.apply_to(domain_guests)
552         self.assert_PSO_applied(user, guest_pso)
553
554         # create a new group that's a member of Admin Users
555         nested_group = self.add_group("nested-group")
556         self.set_attribute(admin_users, "member", nested_group)
557         # set the user's primary-group to be the new group
558         self.set_attribute(nested_group, "member", user.dn)
559         user.set_primary_group(nested_group)
560         # we've only changed group membership so far, not the PSO
561         self.assert_PSO_applied(user, guest_pso)
562
563         # now apply the best-precedence PSO to Admin Users and check it applies
564         # to the user (via the nested-group's membership)
565         admin_pso.apply_to(admin_users)
566         self.assert_PSO_applied(user, admin_pso)
567
568         # restore the default primaryGroupID so we can safely delete the group
569         user.set_primary_group(domain_users)
570
571     def test_pso_none_applied(self):
572         """Tests cases where no Resultant PSO should be returned"""
573
574         # create a PSO that we will check *doesn't* get returned
575         dummy_pso = PasswordSettings("dummy-PSO", self.ldb, password_len=20)
576         self.add_obj_cleanup([dummy_pso.dn])
577
578         # you can apply a PSO to other objects (like OUs), but the resultantPSO
579         # attribute should only be returned for users
580         dummy_pso.apply_to(str(self.ou))
581         res = self.ldb.search(self.ou, attrs=['msDS-ResultantPSO'])
582         self.assertFalse('msDS-ResultantPSO' in res[0])
583
584         # create a dummy user and apply the PSO
585         user = self.add_user("testuser")
586         dummy_pso.apply_to(user.dn)
587         self.assertTrue(user.get_resultant_PSO() == dummy_pso.dn)
588
589         # now clear the ADS_UF_NORMAL_ACCOUNT flag for the user, which should
590         # mean a resultant PSO is no longer returned (we're essentially turning
591         # the user into a DC here, which is a little overkill but tests
592         # behaviour as per the Windows specification)
593         self.set_attribute(user.dn, "userAccountControl",
594                            str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
595                            operation=FLAG_MOD_REPLACE)
596         self.assertIsNone(user.get_resultant_PSO())
597
598         # reset it back to a normal user account
599         self.set_attribute(user.dn, "userAccountControl",
600                            str(dsdb.UF_NORMAL_ACCOUNT),
601                            operation=FLAG_MOD_REPLACE)
602         self.assertTrue(user.get_resultant_PSO() == dummy_pso.dn)
603
604         # no PSO should be returned if RID is equal to DOMAIN_USER_RID_KRBTGT
605         # (note this currently fails against Windows due to a Windows bug)
606         krbtgt_user = "CN=krbtgt,CN=Users,%s" % self.ldb.domain_dn()
607         dummy_pso.apply_to(krbtgt_user)
608         res = self.ldb.search(krbtgt_user, attrs=['msDS-ResultantPSO'])
609         self.assertFalse('msDS-ResultantPSO' in res[0])
610
611     def get_ldb_connection(self, username, password, ldaphost):
612         """Returns an LDB connection using the specified user's credentials"""
613         creds = self.get_credentials()
614         creds_tmp = Credentials()
615         creds_tmp.set_username(username)
616         creds_tmp.set_password(password)
617         creds_tmp.set_domain(creds.get_domain())
618         creds_tmp.set_realm(creds.get_realm())
619         creds_tmp.set_workstation(creds.get_workstation())
620         creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
621                                       | gensec.FEATURE_SEAL)
622         return samba.tests.connect_samdb(ldaphost, credentials=creds_tmp)
623
624     def test_pso_permissions(self):
625         """Checks that regular users can't modify/view PSO objects"""
626
627         user = self.add_user("testuser")
628
629         # get an ldb connection with the new user's privileges
630         user_ldb = self.get_ldb_connection("testuser", user.get_password(),
631                                            self.host_url)
632
633         # regular users should not be able to create a PSO (at least, not in
634         # the default Password Settings container)
635         try:
636             priv_pso = PasswordSettings("priv-PSO", user_ldb, password_len=20)
637             self.fail()
638         except ldb.LdbError as e:
639             (num, msg) = e.args
640             self.assertEquals(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
641
642         # create a PSO as the admin user
643         priv_pso = PasswordSettings("priv-PSO", self.ldb, password_len=20)
644         self.add_obj_cleanup([priv_pso.dn])
645
646         # regular users should not be able to apply a PSO to a user
647         try:
648             self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
649                                samdb=user_ldb)
650             self.fail()
651         except ldb.LdbError as e:
652             (num, msg) = e.args
653             self.assertEquals(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
654             self.assertTrue('00002098' in msg, msg)
655
656         self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
657                            samdb=self.ldb)
658
659         # regular users should not be able to change a PSO's precedence
660         try:
661             priv_pso.set_precedence(100, samdb=user_ldb)
662             self.fail()
663         except ldb.LdbError as e:
664             (num, msg) = e.args
665             self.assertEquals(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
666             self.assertTrue('00002098' in msg, msg)
667
668         priv_pso.set_precedence(100, samdb=self.ldb)
669
670         # regular users should not be able to view a PSO's settings
671         pso_attrs = ["msDS-PSOAppliesTo", "msDS-PasswordSettingsPrecedence",
672                      "msDS-PasswordHistoryLength", "msDS-LockoutThreshold",
673                      "msDS-PasswordComplexityEnabled"]
674
675         # users can see the PSO object's DN, but not its attributes
676         res = user_ldb.search(priv_pso.dn, scope=ldb.SCOPE_BASE,
677                               attrs=pso_attrs)
678         self.assertTrue(str(priv_pso.dn) == str(res[0].dn))
679         for attr in pso_attrs:
680             self.assertFalse(attr in res[0])
681
682         # whereas admin users can see everything
683         res = self.ldb.search(priv_pso.dn, scope=ldb.SCOPE_BASE,
684                               attrs=pso_attrs)
685         for attr in pso_attrs:
686             self.assertTrue(attr in res[0])
687
688         # check replace/delete operations can't be performed by regular users
689         operations = [ FLAG_MOD_REPLACE, FLAG_MOD_DELETE ]
690
691         for oper in operations:
692             try:
693                 self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
694                                    samdb=user_ldb, operation=oper)
695                 self.fail()
696             except ldb.LdbError as e:
697                 (num, msg) = e.args
698                 self.assertEquals(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
699                 self.assertTrue('00002098' in msg, msg)
700
701             # ...but can be performed by the admin user
702             self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
703                                samdb=self.ldb, operation=oper)
704
705     # The 'user add' case is a bit more complicated as you can't really query
706     # the msDS-ResultantPSO attribute on a user that doesn't exist yet (it
707     # won't have any group membership or PSOs applied directly against it yet).
708     # In theory it's possible to still get an applicable PSO via the user's
709     # primaryGroupID (i.e. 'Domain Users' by default). However, testing aginst
710     # Windows shows that the PSO doesn't take effect during the user add
711     # operation. (However, the Windows GUI tools presumably adds the user in 2
712     # steps, because it does enforce the PSO for users added via the GUI).
713     def test_pso_add_user(self):
714         """Tests against a 'Domain Users' PSO taking effect on a new user"""
715
716         # create a PSO that will apply to users by default
717         default_pso = PasswordSettings("default-PSO", self.ldb, precedence=20,
718                                        password_len=12, complexity=False)
719         self.add_obj_cleanup([default_pso.dn])
720
721         # apply the PSO to Domain Users (which all users are a member of). In
722         # theory, this PSO *could* take effect on a new user (but it doesn't)
723         domain_users = "CN=Domain Users,CN=Users,%s" % self.ldb.domain_dn()
724         default_pso.apply_to(domain_users)
725
726         # first try to add a user with a password that doesn't meet the domain
727         # defaults, to prove that the DC will reject bad passwords during a
728         # user add
729         userdn = "CN=testuser,%s" % self.ou
730         password = base64.b64encode('"abcdef"'.encode('utf-16-le')).decode('utf8')
731
732         # Note we use an LDIF operation to ensure that the password gets set
733         # as part of the 'add' operation (whereas self.add_user() adds the user
734         # first, then sets the password later in a 2nd step)
735         try:
736             ldif = """
737 dn: %s
738 objectClass: user
739 sAMAccountName: testuser
740 unicodePwd:: %s
741 """ % (userdn, password)
742             self.ldb.add_ldif(ldif)
743             self.fail()
744         except ldb.LdbError as e:
745                 (num, msg) = e.args
746                 # error codes differ between Samba and Windows
747                 self.assertTrue(num == ldb.ERR_UNWILLING_TO_PERFORM or
748                                 num == ldb.ERR_CONSTRAINT_VIOLATION, msg)
749                 self.assertTrue('0000052D' in msg, msg)
750
751         # now use a password that meets the domain defaults, but doesn't meet
752         # the PSO requirements. Note that Windows allows this, i.e. it doesn't
753         # honour the PSO during the add operation
754         password = base64.b64encode('"abcde12#"'.encode('utf-16-le')).decode('utf8')
755         ldif = """
756 dn: %s
757 objectClass: user
758 sAMAccountName: testuser
759 unicodePwd:: %s
760 """ % (userdn, password)
761         self.ldb.add_ldif(ldif)
762
763         # Now do essentially the same thing, but set the password in a 2nd step
764         # which proves that the same password doesn't meet the PSO requirements
765         userdn = "CN=testuser2,%s" % self.ou
766         ldif = """
767 dn: %s
768 objectClass: user
769 sAMAccountName: testuser2
770 """ % userdn
771         self.ldb.add_ldif(ldif)
772
773         # now that the user exists, assert that the PSO is honoured
774         try:
775             ldif = """
776 dn: %s
777 changetype: modify
778 delete: unicodePwd
779 add: unicodePwd
780 unicodePwd:: %s
781 """ % (userdn, password)
782             self.ldb.modify_ldif(ldif)
783             self.fail()
784         except ldb.LdbError as e:
785                 (num, msg) = e.args
786                 self.assertEquals(num, ldb.ERR_CONSTRAINT_VIOLATION, msg)
787                 self.assertTrue('0000052D' in msg, msg)
788
789         # check setting a password that meets the PSO settings works
790         password = base64.b64encode('"abcdefghijkl"'.encode('utf-16-le')).decode('utf8')
791         ldif = """
792 dn: %s
793 changetype: modify
794 delete: unicodePwd
795 add: unicodePwd
796 unicodePwd:: %s
797 """ % (userdn, password)
798         self.ldb.modify_ldif(ldif)
799
800     def set_domain_pwdHistoryLength(self, value):
801         m = ldb.Message()
802         m.dn = ldb.Dn(self.ldb, self.ldb.domain_dn())
803         m["pwdHistoryLength"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "pwdHistoryLength")
804         self.ldb.modify(m)
805
806     def test_domain_pwd_history(self):
807         """Non-PSO test for domain's pwdHistoryLength setting"""
808
809         # restore the current pwdHistoryLength setting after the test completes
810         curr_hist_len = str(self.pwd_defaults.history_len)
811         self.addCleanup(self.set_domain_pwdHistoryLength, curr_hist_len)
812
813         self.set_domain_pwdHistoryLength("4")
814         user = self.add_user("testuser")
815
816         initial_pwd = user.get_password()
817         passwords = ["First12#", "Second12#", "Third12#", "Fourth12#"]
818
819         # we should be able to set the password to new values OK
820         for pwd in passwords:
821             self.assert_password_valid(user, pwd)
822
823         # the 2nd time round it should fail because they're in the history now
824         for pwd in passwords:
825             self.assert_password_invalid(user, pwd)
826
827         # but the initial password is now outside the history, so should be OK
828         self.assert_password_valid(user, initial_pwd)
829
830         # if we set the history to zero, all the old passwords should now be OK
831         self.set_domain_pwdHistoryLength("0")
832         for pwd in passwords:
833             self.assert_password_valid(user, pwd)
834
835     def test_domain_pwd_history_zero(self):
836         """Non-PSO test for pwdHistoryLength going from zero to non-zero"""
837
838         # restore the current pwdHistoryLength setting after the test completes
839         curr_hist_len = str(self.pwd_defaults.history_len)
840         self.addCleanup(self.set_domain_pwdHistoryLength, curr_hist_len)
841
842         self.set_domain_pwdHistoryLength("0")
843         user = self.add_user("testuser")
844
845         initial_pwd = user.get_password()
846         self.assert_password_valid(user, "NewPwd12#")
847         # we can set the exact same password again because there's no history
848         self.assert_password_valid(user, "NewPwd12#")
849
850         # There is a difference in behaviour here between Windows and Samba.
851         # When going from zero to non-zero password-history, Windows treats
852         # the current user's password as invalid (even though the password has
853         # not been altered since the setting changed). Whereas Samba accepts
854         # the current password (because it's not in the history until the
855         # *next* time the user's password changes.
856         self.set_domain_pwdHistoryLength("1")
857         self.assert_password_invalid(user, "NewPwd12#")
858
859
860
861
862