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