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