4ba534f956d87e52471560ffddb52b1874fdaa45
[samba.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 = self.ou.split(',')[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         # temporarily returning early here will just test the resultant-PSO
133         # constructed attribute. Remove this return to also test min password
134         # length, complexity, and password-history
135         return
136
137         # we're mirroring the pwd_history for the user, so make sure this is
138         # up-to-date, before we start making password changes
139         if user.last_pso:
140             user.pwd_history_change(user.last_pso.history_len, pso.history_len)
141         user.last_pso = pso
142
143         # check if we can set a sufficiently long, but non-complex, password.
144         # (We use the history-size to generate a unique password for each
145         # assertion - otherwise, if the password is already in the history,
146         # then it'll be rejected)
147         unique_char = chr(ord('a') + len(user.all_old_passwords))
148         noncomplex_pwd = "%cabcdefghijklmnopqrst" % unique_char
149
150         if pso.complexity:
151             self.assert_password_invalid(user, noncomplex_pwd)
152         else:
153             self.assert_password_valid(user, noncomplex_pwd)
154
155         # use a unique and sufficiently complex base-string to check pwd-length
156         pass_phrase = "%d#AaBbCcDdEeFfGgHhIi" % len(user.all_old_passwords)
157
158         # check that passwords less than the specified length are rejected
159         for i in range(3, pso.password_len):
160             self.assert_password_invalid(user, pass_phrase[:i])
161
162         # check we can set a password that's exactly the minimum length
163         self.assert_password_valid(user, pass_phrase[:pso.password_len])
164
165         # check the password history is enforced correctly.
166         # first, check the last n items in the password history are invalid
167         invalid_passwords = user.old_invalid_passwords(pso.history_len)
168         for pwd in invalid_passwords:
169             self.assert_password_invalid(user, pwd)
170
171         # next, check any passwords older than the history-len can be re-used
172         valid_passwords = user.old_valid_passwords(pso.history_len)
173         for pwd in valid_passwords:
174             self.assert_set_old_password(user, pwd, pso)
175
176     def password_is_complex(self, password):
177         # non-complex passwords used in the tests are all lower-case letters
178         # If it's got a number in the password, assume it's complex
179         return any(c.isdigit() for c in password)
180
181     def assert_set_old_password(self, user, password, pso):
182         """
183         Checks a user password can be set (if the password conforms to the PSO
184         settings). Used to check an old password that falls outside the history
185         length, but might still be invalid for other reasons.
186         """
187         if self.password_is_complex(password):
188             # check password conforms to length requirements
189             if len(password) < pso.password_len:
190                 self.assert_password_invalid(user, password)
191             else:
192                 self.assert_password_valid(user, password)
193         else:
194             # password is not complex, check PSO handles it appropriately
195             if pso.complexity:
196                 self.assert_password_invalid(user, password)
197             else:
198                 self.assert_password_valid(user, password)
199
200     def test_pso_basics(self):
201         """Simple tests that a PSO takes effect when applied to a group or user"""
202
203         # create some PSOs that vary in priority and basic password-len
204         best_pso = PasswordSettings("highest-priority-PSO", self.ldb,
205                                     precedence=5, password_len=16,
206                                     history_len=6)
207         medium_pso = PasswordSettings("med-priority-PSO", self.ldb,
208                                       precedence=15, password_len=10,
209                                       history_len=4)
210         worst_pso = PasswordSettings("lowest-priority-PSO", self.ldb,
211                                      precedence=100, complexity=False,
212                                      password_len=4, history_len=2)
213
214         # handle PSO clean-up (as they're outside the top-level test OU)
215         self.add_obj_cleanup([worst_pso.dn, medium_pso.dn, best_pso.dn])
216
217         # create some groups and apply the PSOs to the groups
218         group1 = self.add_group("Group-1")
219         group2 = self.add_group("Group-2")
220         group3 = self.add_group("Group-3")
221         group4 = self.add_group("Group-4")
222         worst_pso.apply_to(group1)
223         medium_pso.apply_to(group2)
224         best_pso.apply_to(group3)
225         worst_pso.apply_to(group4)
226
227         # create a user and check the default settings apply to it
228         user = self.add_user("testuser")
229         self.assert_PSO_applied(user, self.pwd_defaults)
230
231         # add user to a group. Check that the group's PSO applies to the user
232         self.set_attribute(group1, "member", user.dn)
233         self.assert_PSO_applied(user, worst_pso)
234
235         # add the user to a group with a higher precedence PSO and and check
236         # that now trumps the previous PSO
237         self.set_attribute(group2, "member", user.dn)
238         self.assert_PSO_applied(user, medium_pso)
239
240         # add the user to the remaining groups. The highest precedence PSO
241         # should now take effect
242         self.set_attribute(group3, "member", user.dn)
243         self.set_attribute(group4, "member", user.dn)
244         self.assert_PSO_applied(user, best_pso)
245
246         # delete a group membership and check the PSO changes
247         self.set_attribute(group3, "member", user.dn, operation=FLAG_MOD_DELETE)
248         self.assert_PSO_applied(user, medium_pso)
249
250         # apply the low-precedence PSO directly to the user
251         # (directly applied PSOs should trump higher precedence group PSOs)
252         worst_pso.apply_to(user.dn)
253         self.assert_PSO_applied(user, worst_pso)
254
255         # remove applying the PSO directly to the user and check PSO changes
256         worst_pso.unapply(user.dn)
257         self.assert_PSO_applied(user, medium_pso)
258
259         # remove all appliesTo and check we have the default settings again
260         worst_pso.unapply(group1)
261         medium_pso.unapply(group2)
262         worst_pso.unapply(group4)
263         self.assert_PSO_applied(user, self.pwd_defaults)
264
265     def test_pso_nested_groups(self):
266         """PSOs operate correctly when applied to nested groups"""
267
268         # create some PSOs that vary in priority and basic password-len
269         group1_pso = PasswordSettings("group1-PSO", self.ldb, precedence=50,
270                                       password_len=12, history_len=3)
271         group2_pso = PasswordSettings("group2-PSO", self.ldb, precedence=25,
272                                       password_len=10, history_len=5,
273                                       complexity=False)
274         group3_pso = PasswordSettings("group3-PSO", self.ldb, precedence=10,
275                                       password_len=6, history_len=2)
276
277         # create some groups and apply the PSOs to the groups
278         group1 = self.add_group("Group-1")
279         group2 = self.add_group("Group-2")
280         group3 = self.add_group("Group-3")
281         group4 = self.add_group("Group-4")
282         group1_pso.apply_to(group1)
283         group2_pso.apply_to(group2)
284         group3_pso.apply_to(group3)
285
286         # create a PSO and apply it to a group that the user is not a member
287         # of - it should not have any effect on the user
288         unused_pso = PasswordSettings("unused-PSO", self.ldb, precedence=1,
289                                       password_len=20)
290         unused_pso.apply_to(group4)
291
292         # handle PSO clean-up (as they're outside the top-level test OU)
293         self.add_obj_cleanup([group1_pso.dn, group2_pso.dn, group3_pso.dn,
294                               unused_pso.dn])
295
296         # create a user and check the default settings apply to it
297         user = self.add_user("testuser")
298         self.assert_PSO_applied(user, self.pwd_defaults)
299
300         # add user to a group. Check that the group's PSO applies to the user
301         self.set_attribute(group1, "member", user.dn)
302         self.set_attribute(group2, "member", group1)
303         self.assert_PSO_applied(user, group2_pso)
304
305         # add another level to the group heirachy & check this PSO takes effect
306         self.set_attribute(group3, "member", group2)
307         self.assert_PSO_applied(user, group3_pso)
308
309         # invert the PSO precedence and check the new lowest value takes effect
310         group1_pso.set_precedence(3)
311         group2_pso.set_precedence(13)
312         group3_pso.set_precedence(33)
313         self.assert_PSO_applied(user, group1_pso)
314
315         # delete a PSO and check it no longer applies
316         self.ldb.delete(group1_pso.dn)
317         self.test_objs.remove(group1_pso.dn)
318         self.assert_PSO_applied(user, group2_pso)
319
320     def get_guid(self, dn):
321         res = self.ldb.search(base=dn, attrs=["objectGUID"], scope=ldb.SCOPE_BASE)
322         return res[0]['objectGUID'][0]
323
324     def guid_string(self, guid):
325         return self.ldb.schema_format_value("objectGUID", guid)
326
327     def PSO_with_lowest_GUID(self, pso_list):
328         """Returns the PSO object in the list with the lowest GUID"""
329         # go through each PSO and fetch its GUID
330         guid_list = []
331         mapping = {}
332         for pso in pso_list:
333             guid = self.get_guid(pso.dn)
334             guid_list.append(guid)
335             # remember which GUID maps to what PSO
336             mapping[guid] = pso
337
338         # sort the GUID list to work out the lowest/best GUID
339         guid_list.sort()
340         best_guid = guid_list[0]
341
342         # sanity-check the mapping between GUID and DN is correct
343         self.assertEqual(self.guid_string(self.get_guid(mapping[best_guid].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=1, 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 password is valid
468         valid_password = "min-age-passwd"
469         self.assert_password_invalid(user, valid_password)
470         # then trying the same password later (min-age=1sec) should succeed
471         time.sleep(1.5)
472         self.assert_password_valid(user, valid_password)
473
474     def test_pso_max_age(self):
475         """Tests that a PSO's max-age is used"""
476
477         # create PSOs that use the domain's max-age +/- 1 day
478         domain_max_age = self.pwd_defaults.password_age_max
479         day_in_secs = 60 * 60 * 24
480         higher_max_age = domain_max_age + day_in_secs
481         lower_max_age = domain_max_age - day_in_secs
482         longer_pso = PasswordSettings("longer-age-PSO", self.ldb, precedence=5,
483                                       password_age_max=higher_max_age)
484         shorter_pso = PasswordSettings("shorter-age-PSO", self.ldb,
485                                        precedence=1,
486                                        password_age_max=lower_max_age)
487         self.add_obj_cleanup([longer_pso.dn, shorter_pso.dn])
488
489         user = self.add_user("testuser")
490
491         # we can't wait around long enough for the max-age to expire, so instead
492         # just check the msDS-UserPasswordExpiryTimeComputed for the user
493         attrs=['msDS-UserPasswordExpiryTimeComputed']
494         res = self.ldb.search(user.dn, attrs=attrs)
495         domain_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
496
497         # apply the longer PSO and check the expiry-time becomes longer
498         longer_pso.apply_to(user.dn)
499         self.assertTrue(user.get_resultant_PSO() == longer_pso.dn)
500         res = self.ldb.search(user.dn, attrs=attrs)
501         new_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
502
503         # use timestamp diff of 1 day - 1 minute. The new expiry should still
504         # be greater than this, without getting into nano-second granularity
505         approx_timestamp_diff = (day_in_secs - 60) * (1e7)
506         self.assertTrue(new_expiry > domain_expiry + approx_timestamp_diff)
507
508         # apply the shorter PSO and check the expiry-time is shorter
509         shorter_pso.apply_to(user.dn)
510         self.assertTrue(user.get_resultant_PSO() == shorter_pso.dn)
511         res = self.ldb.search(user.dn, attrs=attrs)
512         new_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
513         self.assertTrue(new_expiry < domain_expiry - approx_timestamp_diff)
514
515     def test_pso_special_groups(self):
516         """Checks applying a PSO to built-in AD groups takes effect"""
517
518         # create some PSOs that will apply to special groups
519         default_pso = PasswordSettings("default-PSO", self.ldb, precedence=20,
520                                        password_len=8, complexity=False)
521         guest_pso = PasswordSettings("guest-PSO", self.ldb, history_len=4,
522                                      precedence=5, password_len=5)
523         builtin_pso = PasswordSettings("builtin-PSO", self.ldb, history_len=9,
524                                        precedence=1, password_len=9)
525         admin_pso = PasswordSettings("admin-PSO", self.ldb, history_len=0,
526                                      precedence=2, password_len=10)
527         self.add_obj_cleanup([default_pso.dn, guest_pso.dn, admin_pso.dn,
528                               builtin_pso.dn])
529         domain_users = "CN=Domain Users,CN=Users,%s" % self.ldb.domain_dn()
530         domain_guests = "CN=Domain Guests,CN=Users,%s" % self.ldb.domain_dn()
531         admin_users = "CN=Domain Admins,CN=Users,%s" % self.ldb.domain_dn()
532
533         # if we apply a PSO to Domain Users (which all users are a member of)
534         # then that PSO should take effect on a new user
535         default_pso.apply_to(domain_users)
536         user = self.add_user("testuser")
537         self.assert_PSO_applied(user, default_pso)
538
539         # Apply a PSO to a builtin group. 'Domain Users' should be a member of
540         # Builtin/Users, but builtin groups should be excluded from the PSO
541         # calculation, so this should have no effect
542         builtin_pso.apply_to("CN=Users,CN=Builtin,%s" % self.ldb.domain_dn())
543         builtin_pso.apply_to("CN=Administrators,CN=Builtin,%s" % self.ldb.domain_dn())
544         self.assert_PSO_applied(user, default_pso)
545
546         # change the user's primary group to another group (the primaryGroupID
547         # is a little odd in that there's no memberOf backlink for it)
548         self.set_attribute(domain_guests, "member", user.dn)
549         user.set_primary_group(domain_guests)
550         # No PSO is applied to the Domain Guests yet, so the default PSO should
551         # still apply
552         self.assert_PSO_applied(user, default_pso)
553
554         # now apply a PSO to the guests group, which should trump the default
555         # PSO (because the guest PSO has a better precedence)
556         guest_pso.apply_to(domain_guests)
557         self.assert_PSO_applied(user, guest_pso)
558
559         # create a new group that's a member of Admin Users
560         nested_group = self.add_group("nested-group")
561         self.set_attribute(admin_users, "member", nested_group)
562         # set the user's primary-group to be the new group
563         self.set_attribute(nested_group, "member", user.dn)
564         user.set_primary_group(nested_group)
565         # we've only changed group membership so far, not the PSO
566         self.assert_PSO_applied(user, guest_pso)
567
568         # now apply the best-precedence PSO to Admin Users and check it applies
569         # to the user (via the nested-group's membership)
570         admin_pso.apply_to(admin_users)
571         self.assert_PSO_applied(user, admin_pso)
572
573     def test_pso_none_applied(self):
574         """Tests cases where no Resultant PSO should be returned"""
575
576         # create a PSO that we will check *doesn't* get returned
577         dummy_pso = PasswordSettings("dummy-PSO", self.ldb, password_len=20)
578         self.add_obj_cleanup([dummy_pso.dn])
579
580         # you can apply a PSO to other objects (like OUs), but the resultantPSO
581         # attribute should only be returned for users
582         dummy_pso.apply_to(self.ou)
583         res = self.ldb.search(self.ou, attrs=['msDS-ResultantPSO'])
584         self.assertFalse('msDS-ResultantPSO' in res[0])
585
586         # create a dummy user and apply the PSO
587         user = self.add_user("testuser")
588         dummy_pso.apply_to(user.dn)
589         self.assertTrue(user.get_resultant_PSO() == dummy_pso.dn)
590
591         # now clear the ADS_UF_NORMAL_ACCOUNT flag for the user, which should
592         # mean a resultant PSO is no longer returned (we're essentially turning
593         # the user into a DC here, which is a little overkill but tests
594         # behaviour as per the Windows specification)
595         self.set_attribute(user.dn, "userAccountControl",
596                            str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
597                            operation=FLAG_MOD_REPLACE)
598         self.assertTrue(user.get_resultant_PSO() == None)
599
600         # reset it back to a normal user account
601         self.set_attribute(user.dn, "userAccountControl",
602                            str(dsdb.UF_NORMAL_ACCOUNT),
603                            operation=FLAG_MOD_REPLACE)
604         self.assertTrue(user.get_resultant_PSO() == dummy_pso.dn)
605
606         # no PSO should be returned if RID is equal to DOMAIN_USER_RID_KRBTGT
607         # (note this currently fails against Windows due to a Windows bug)
608         krbtgt_user = "CN=krbtgt,CN=Users,%s" % self.ldb.domain_dn()
609         dummy_pso.apply_to(krbtgt_user)
610         res = self.ldb.search(krbtgt_user, attrs=['msDS-ResultantPSO'])
611         self.assertFalse('msDS-ResultantPSO' in res[0])
612
613     def get_ldb_connection(self, username, password, ldaphost):
614         """Returns an LDB connection using the specified user's credentials"""
615         creds = self.get_credentials()
616         creds_tmp = Credentials()
617         creds_tmp.set_username(username)
618         creds_tmp.set_password(password)
619         creds_tmp.set_domain(creds.get_domain())
620         creds_tmp.set_realm(creds.get_realm())
621         creds_tmp.set_workstation(creds.get_workstation())
622         creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
623                                       | gensec.FEATURE_SEAL)
624         return samba.tests.connect_samdb(ldaphost, credentials=creds_tmp)
625
626     def test_pso_permissions(self):
627         """Checks that regular users can't modify/view PSO objects"""
628
629         user = self.add_user("testuser")
630
631         # get an ldb connection with the new user's privileges
632         user_ldb = self.get_ldb_connection("testuser", user.get_password(),
633                                            self.host_url)
634
635         # regular users should not be able to create a PSO (at least, not in
636         # the default Password Settings container)
637         try:
638             priv_pso = PasswordSettings("priv-PSO", user_ldb, password_len=20)
639             self.fail()
640         except ldb.LdbError as e:
641             (num, msg) = e.args
642             self.assertEquals(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
643
644         # create a PSO as the admin user
645         priv_pso = PasswordSettings("priv-PSO", self.ldb, password_len=20)
646         self.add_obj_cleanup([priv_pso.dn])
647
648         # regular users should not be able to apply a PSO to a user
649         try:
650             self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
651                                samdb=user_ldb)
652             self.fail()
653         except ldb.LdbError as e:
654             (num, msg) = e.args
655             self.assertEquals(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
656             self.assertTrue('00002098' in msg, msg)
657
658         self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
659                            samdb=self.ldb)
660
661         # regular users should not be able to change a PSO's precedence
662         try:
663             priv_pso.set_precedence(100, samdb=user_ldb)
664             self.fail()
665         except ldb.LdbError as e:
666             (num, msg) = e.args
667             self.assertEquals(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
668             self.assertTrue('00002098' in msg, msg)
669
670         priv_pso.set_precedence(100, samdb=self.ldb)
671
672         # regular users should not be able to view a PSO's settings
673         pso_attrs = ["msDS-PSOAppliesTo", "msDS-PasswordSettingsPrecedence",
674                      "msDS-PasswordHistoryLength", "msDS-LockoutThreshold",
675                      "msDS-PasswordComplexityEnabled"]
676
677         # users can see the PSO object's DN, but not its attributes
678         res = user_ldb.search(priv_pso.dn, scope=ldb.SCOPE_BASE,
679                               attrs=pso_attrs)
680         self.assertTrue(str(priv_pso.dn) == str(res[0].dn))
681         for attr in pso_attrs:
682             self.assertFalse(attr in res[0])
683
684         # whereas admin users can see everything
685         res = self.ldb.search(priv_pso.dn, scope=ldb.SCOPE_BASE,
686                               attrs=pso_attrs)
687         for attr in pso_attrs:
688             self.assertTrue(attr in res[0])
689
690         # check replace/delete operations can't be performed by regular users
691         operations = [ FLAG_MOD_REPLACE, FLAG_MOD_DELETE ]
692
693         for oper in operations:
694             try:
695                 self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
696                                    samdb=user_ldb, operation=oper)
697                 self.fail()
698             except ldb.LdbError as e:
699                 (num, msg) = e.args
700                 self.assertEquals(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
701                 self.assertTrue('00002098' in msg, msg)
702
703             # ...but can be performed by the admin user
704             self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
705                                samdb=self.ldb, operation=oper)
706
707     # The 'user add' case is a bit more complicated as you can't really query
708     # the msDS-ResultantPSO attribute on a user that doesn't exist yet (it
709     # won't have any group membership or PSOs applied directly against it yet).
710     # In theory it's possible to still get an applicable PSO via the user's
711     # primaryGroupID (i.e. 'Domain Users' by default). However, testing aginst
712     # Windows shows that the PSO doesn't take effect during the user add
713     # operation. (However, the Windows GUI tools presumably adds the user in 2
714     # steps, because it does enforce the PSO for users added via the GUI).
715     def test_pso_add_user(self):
716         """Tests against a 'Domain Users' PSO taking effect on a new user"""
717
718         # create a PSO that will apply to users by default
719         default_pso = PasswordSettings("default-PSO", self.ldb, precedence=20,
720                                        password_len=12, complexity=False)
721         self.add_obj_cleanup([default_pso.dn])
722
723         # apply the PSO to Domain Users (which all users are a member of). In
724         # theory, this PSO *could* take effect on a new user (but it doesn't)
725         domain_users = "CN=Domain Users,CN=Users,%s" % self.ldb.domain_dn()
726         default_pso.apply_to(domain_users)
727
728         # first try to add a user with a password that doesn't meet the domain
729         # defaults, to prove that the DC will reject bad passwords during a
730         # user add
731         userdn = "CN=testuser,%s" % self.ou
732         password = base64.b64encode("\"abcdef\"".encode('utf-16-le'))
733
734         # Note we use an LDIF operation to ensure that the password gets set
735         # as part of the 'add' operation (whereas self.add_user() adds the user
736         # first, then sets the password later in a 2nd step)
737         try:
738             ldif = """
739 dn: %s
740 objectClass: user
741 sAMAccountName: testuser
742 unicodePwd:: %s
743 """ % (userdn, password)
744             self.ldb.add_ldif(ldif)
745             self.fail()
746         except ldb.LdbError as e:
747                 (num, msg) = e.args
748                 # error codes differ between Samba and Windows
749                 self.assertTrue(num == ldb.ERR_UNWILLING_TO_PERFORM or
750                                 num == ldb.ERR_CONSTRAINT_VIOLATION, msg)
751                 self.assertTrue('0000052D' in msg, msg)
752
753         # now use a password that meets the domain defaults, but doesn't meet
754         # the PSO requirements. Note that Windows allows this, i.e. it doesn't
755         # honour the PSO during the add operation
756         password = base64.b64encode("\"abcde12#\"".encode('utf-16-le'))
757         ldif = """
758 dn: %s
759 objectClass: user
760 sAMAccountName: testuser
761 unicodePwd:: %s
762 """ % (userdn, password)
763         self.ldb.add_ldif(ldif)
764
765         # Now do essentially the same thing, but set the password in a 2nd step
766         # which proves that the same password doesn't meet the PSO requirements
767         userdn = "CN=testuser2,%s" % self.ou
768         ldif = """
769 dn: %s
770 objectClass: user
771 sAMAccountName: testuser2
772 """ % userdn
773         self.ldb.add_ldif(ldif)
774
775         # now that the user exists, assert that the PSO is honoured
776         try:
777             ldif = """
778 dn: %s
779 changetype: modify
780 delete: unicodePwd
781 add: unicodePwd
782 unicodePwd:: %s
783 """ % (userdn, password)
784             self.ldb.modify_ldif(ldif)
785             self.fail()
786         except ldb.LdbError as e:
787                 (num, msg) = e.args
788                 self.assertEquals(num, ldb.ERR_CONSTRAINT_VIOLATION, msg)
789                 self.assertTrue('0000052D' in msg, msg)
790
791         # check setting a password that meets the PSO settings works
792         password = base64.b64encode("\"abcdefghijkl\"".encode('utf-16-le'))
793         ldif = """
794 dn: %s
795 changetype: modify
796 delete: unicodePwd
797 add: unicodePwd
798 unicodePwd:: %s
799 """ % (userdn, password)
800         self.ldb.modify_ldif(ldif)
801
802