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