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/>.
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"
35 from ldb import FLAG_MOD_DELETE, FLAG_MOD_ADD, FLAG_MOD_REPLACE
36 from samba import dsdb
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
47 class PasswordSettingsTestCase(PasswordTestCase):
49 super(PasswordSettingsTestCase, self).setUp()
51 self.host_url = "ldap://%s" % env_get_var_value("SERVER_IP")
52 self.ldb = samba.tests.connect_samdb(self.host_url)
54 # create a temp OU to put this test's users into
55 self.ou = samba.tests.create_test_ou(self.ldb, "password_settings")
57 # update DC to allow password changes for the duration of this test
58 self.allow_password_changes()
60 # store the current password-settings for the domain
61 self.pwd_defaults = PasswordSettings(None, self.ldb)
65 super(PasswordSettingsTestCase, self).tearDown()
67 # remove all objects under the top-level OU
68 self.ldb.delete(self.ou, ["tree_delete:1"])
70 # PSOs can't reside within an OU so they get cleaned up separately
71 for obj in self.test_objs:
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)
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"})
84 def set_attribute(self, dn, attr, value, operation=FLAG_MOD_ADD,
86 """Modifies an attribute for an object"""
90 m.dn = ldb.Dn(samdb, dn)
91 m[attr] = ldb.MessageElement(value, operation, attr)
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)
99 def assert_password_invalid(self, user, password):
101 Check we can't set a password that violates complexity or length
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:
110 self.assertEquals(num, ldb.ERR_CONSTRAINT_VIOLATION, msg)
111 self.assertTrue('0000052D' in msg, msg)
113 def assert_password_valid(self, user, password):
114 """Checks that we can set a password successfully"""
116 user.set_password(password)
117 except ldb.LdbError as e:
119 # fail the test (rather than throw an error)
120 self.fail("Password '%s' unexpectedly rejected: %s" % (password,
123 def assert_PSO_applied(self, user, pso):
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
129 resultant_pso = user.get_resultant_PSO()
130 self.assertTrue(resultant_pso == pso.dn,
131 "Expected PSO %s, not %s" % (pso.name,
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
137 user.pwd_history_change(user.last_pso.history_len, pso.history_len)
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
148 self.assert_password_invalid(user, noncomplex_pwd)
150 self.assert_password_valid(user, noncomplex_pwd)
152 # use a unique and sufficiently complex base-string to check pwd-length
153 pass_phrase = "%d#AaBbCcDdEeFfGgHhIi" % len(user.all_old_passwords)
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])
159 # check we can set a password that's exactly the minimum length
160 self.assert_password_valid(user, pass_phrase[:pso.password_len])
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)
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)
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)
178 def assert_set_old_password(self, user, password, pso):
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.
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)
189 self.assert_password_valid(user, password)
191 # password is not complex, check PSO handles it appropriately
193 self.assert_password_invalid(user, password)
195 self.assert_password_valid(user, password)
197 def test_pso_basics(self):
198 """Simple tests that a PSO takes effect when applied to a group/user"""
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,
204 medium_pso = PasswordSettings("med-priority-PSO", self.ldb,
205 precedence=15, password_len=10,
207 worst_pso = PasswordSettings("lowest-priority-PSO", self.ldb,
208 precedence=100, complexity=False,
209 password_len=4, history_len=2)
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])
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)
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)
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)
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)
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)
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)
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)
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)
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)
263 def test_pso_nested_groups(self):
264 """PSOs operate correctly when applied to nested groups"""
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,
272 group3_pso = PasswordSettings("group3-PSO", self.ldb, precedence=10,
273 password_len=6, history_len=2)
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)
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,
288 unused_pso.apply_to(group4)
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,
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)
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)
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)
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)
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)
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]
323 def guid_string(self, guid):
324 return self.ldb.schema_format_value("objectGUID", guid)
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
332 guid = self.get_guid(pso.dn)
333 guid_list.append(guid)
334 # remember which GUID maps to what PSO
337 # sort the GUID list to work out the lowest/best GUID
339 best_guid = guid_list[0]
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))
346 # return the PSO that this GUID corresponds to
347 return mapping[best_guid]
349 def test_pso_equal_precedence(self):
350 """Tests expected PSO wins when several have the same precedence"""
352 # create some PSOs that vary in priority and basic password-len
353 pso1 = PasswordSettings("PSO-1", self.ldb, precedence=5, history_len=1,
355 pso2 = PasswordSettings("PSO-2", self.ldb, precedence=5, history_len=2,
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)
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])
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)
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)
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)
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)
390 # excluding the winning PSO, apply the other PSOs directly to the user
391 pso_list.remove(best_pso)
393 pso.apply_to(user.dn)
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)
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)
406 def test_pso_invalid_location(self):
407 """Tests that PSOs in an invalid location have no effect"""
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
412 rogue_pso = PasswordSettings("rogue-PSO", self.ldb, precedence=1,
413 complexity=False, password_len=20,
416 except ldb.LdbError as e:
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)
423 # we can't create Password Settings Containers under an OU either
425 rogue_psc = "CN=Rogue-PSO-container,%s" % self.ou
426 self.ldb.add({"dn": rogue_psc,
427 "objectclass": "msDS-PasswordSettingsContainer"})
429 except ldb.LdbError as e:
431 self.assertEquals(num, ldb.ERR_NAMING_VIOLATION, msg)
432 self.assertTrue('00002099' in msg or '00002037' in msg, msg)
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"})
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])
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)
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)
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])
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)
467 # changing the password immediately should fail, even if the password
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)
475 def test_pso_max_age(self):
476 """Tests that a PSO's max-age is used"""
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,
487 password_age_max=lower_max_age)
488 self.add_obj_cleanup([longer_pso.dn, shorter_pso.dn])
490 user = self.add_user("testuser")
492 # we can't wait around long enough for the max-age to expire, so
493 # instead just check the msDS-UserPasswordExpiryTimeComputed for
495 attrs = ['msDS-UserPasswordExpiryTimeComputed']
496 res = self.ldb.search(user.dn, attrs=attrs)
497 domain_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
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])
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)
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)
517 def test_pso_special_groups(self):
518 """Checks applying a PSO to built-in AD groups takes effect"""
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,
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
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)
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)
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
555 self.assert_PSO_applied(user, default_pso)
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)
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)
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)
576 # restore the default primaryGroupID so we can safely delete the group
577 user.set_primary_group(domain_users)
579 def test_pso_none_applied(self):
580 """Tests cases where no Resultant PSO should be returned"""
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])
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])
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)
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())
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)
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])
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)
632 def test_pso_permissions(self):
633 """Checks that regular users can't modify/view PSO objects"""
635 user = self.add_user("testuser")
637 # get an ldb connection with the new user's privileges
638 user_ldb = self.get_ldb_connection("testuser", user.get_password(),
641 # regular users should not be able to create a PSO (at least, not in
642 # the default Password Settings container)
644 priv_pso = PasswordSettings("priv-PSO", user_ldb, password_len=20)
646 except ldb.LdbError as e:
648 self.assertEquals(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
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])
654 # regular users should not be able to apply a PSO to a user
656 self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
659 except ldb.LdbError as e:
661 self.assertEquals(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
662 self.assertTrue('00002098' in msg, msg)
664 self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
667 # regular users should not be able to change a PSO's precedence
669 priv_pso.set_precedence(100, samdb=user_ldb)
671 except ldb.LdbError as e:
673 self.assertEquals(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
674 self.assertTrue('00002098' in msg, msg)
676 priv_pso.set_precedence(100, samdb=self.ldb)
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"]
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,
686 self.assertTrue(str(priv_pso.dn) == str(res[0].dn))
687 for attr in pso_attrs:
688 self.assertFalse(attr in res[0])
690 # whereas admin users can see everything
691 res = self.ldb.search(priv_pso.dn, scope=ldb.SCOPE_BASE,
693 for attr in pso_attrs:
694 self.assertTrue(attr in res[0])
696 # check replace/delete operations can't be performed by regular users
697 operations = [FLAG_MOD_REPLACE, FLAG_MOD_DELETE]
699 for oper in operations:
701 self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
702 samdb=user_ldb, operation=oper)
704 except ldb.LdbError as e:
706 self.assertEquals(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
707 self.assertTrue('00002098' in msg, msg)
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)
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')
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"""
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])
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)
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
742 userdn = "CN=testuser,%s" % self.ou
743 password = self.format_password_for_ldif('abcdef')
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)
752 sAMAccountName: testuser
754 """ % (userdn, password)
755 self.ldb.add_ldif(ldif)
757 except ldb.LdbError as e:
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)
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#')
771 sAMAccountName: testuser
773 """ % (userdn, password)
774 self.ldb.add_ldif(ldif)
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
782 sAMAccountName: testuser2
784 self.ldb.add_ldif(ldif)
786 # now that the user exists, assert that the PSO is honoured
794 """ % (userdn, password)
795 self.ldb.modify_ldif(ldif)
797 except ldb.LdbError as e:
799 self.assertEquals(num, ldb.ERR_CONSTRAINT_VIOLATION, msg)
800 self.assertTrue('0000052D' in msg, msg)
802 # check setting a password that meets the PSO settings works
803 password = self.format_password_for_ldif('abcdefghijkl')
810 """ % (userdn, password)
811 self.ldb.modify_ldif(ldif)
813 def set_domain_pwdHistoryLength(self, value):
815 m.dn = ldb.Dn(self.ldb, self.ldb.domain_dn())
816 m["pwdHistoryLength"] = ldb.MessageElement(value,
817 ldb.FLAG_MOD_REPLACE,
821 def test_domain_pwd_history(self):
822 """Non-PSO test for domain's pwdHistoryLength setting"""
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)
828 self.set_domain_pwdHistoryLength("4")
829 user = self.add_user("testuser")
831 initial_pwd = user.get_password()
832 passwords = ["First12#", "Second12#", "Third12#", "Fourth12#"]
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)
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)
842 # but the initial password is now outside the history, so should be OK
843 self.assert_password_valid(user, initial_pwd)
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)
850 def test_domain_pwd_history_zero(self):
851 """Non-PSO test for pwdHistoryLength going from zero to non-zero"""
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)
857 self.set_domain_pwdHistoryLength("0")
858 user = self.add_user("testuser")
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#")
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#")