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 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
import gensec
45 class PasswordSettingsTestCase(PasswordTestCase
):
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
)
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
:
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"})
82 def set_attribute(self
, dn
, attr
, value
, operation
=FLAG_MOD_ADD
, samdb
=None):
83 """Modifies an attribute for an object"""
87 m
.dn
= ldb
.Dn(samdb
, dn
)
88 m
[attr
] = ldb
.MessageElement(value
, operation
, attr
)
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
):
98 Check we can't set a password that violates complexity or length
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
:
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"""
113 user
.set_password(password
)
114 except ldb
.LdbError
as e
:
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
,
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
133 user
.pwd_history_change(user
.last_pso
.history_len
, pso
.history_len
)
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
144 self
.assert_password_invalid(user
, noncomplex_pwd
)
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
)
185 self
.assert_password_valid(user
, password
)
187 # password is not complex, check PSO handles it appropriately
189 self
.assert_password_invalid(user
, password
)
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,
200 medium_pso
= PasswordSettings("med-priority-PSO", self
.ldb
,
201 precedence
=15, password_len
=10,
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,
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,
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
,
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
326 guid
= self
.get_guid(pso
.dn
)
327 guid_list
.append(guid
)
328 # remember which GUID maps to what PSO
331 # sort the GUID list to work out the lowest/best GUID
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,
348 pso2
= PasswordSettings("PSO-2", self
.ldb
, precedence
=5, history_len
=2,
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
)
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
405 rogue_pso
= PasswordSettings("rogue-PSO", self
.ldb
, precedence
=1,
406 complexity
=False, password_len
=20,
409 except ldb
.LdbError
as e
:
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
418 rogue_psc
= "CN=Rogue-PSO-container,%s" % self
.ou
419 self
.ldb
.add({"dn": rogue_psc
,
420 "objectclass": "msDS-PasswordSettingsContainer"})
422 except ldb
.LdbError
as e
:
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
,
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
,
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
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(),
631 # regular users should not be able to create a PSO (at least, not in
632 # the default Password Settings container)
634 priv_pso
= PasswordSettings("priv-PSO", user_ldb
, password_len
=20)
636 except ldb
.LdbError
as e
:
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
646 self
.set_attribute(priv_pso
.dn
, "msDS-PSOAppliesTo", user
.dn
,
649 except ldb
.LdbError
as e
:
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
,
657 # regular users should not be able to change a PSO's precedence
659 priv_pso
.set_precedence(100, samdb
=user_ldb
)
661 except ldb
.LdbError
as e
:
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
,
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
,
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
:
691 self
.set_attribute(priv_pso
.dn
, "msDS-PSOAppliesTo", user
.dn
,
692 samdb
=user_ldb
, operation
=oper
)
694 except ldb
.LdbError
as e
:
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
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)
737 sAMAccountName: testuser
739 """ % (userdn
, password
)
740 self
.ldb
.add_ldif(ldif
)
742 except ldb
.LdbError
as e
:
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')
756 sAMAccountName: testuser
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
767 sAMAccountName: testuser2
769 self
.ldb
.add_ldif(ldif
)
771 # now that the user exists, assert that the PSO is honoured
779 """ % (userdn
, password
)
780 self
.ldb
.modify_ldif(ldif
)
782 except ldb
.LdbError
as e
:
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')
795 """ % (userdn
, password
)
796 self
.ldb
.modify_ldif(ldif
)
798 def set_domain_pwdHistoryLength(self
, value
):
800 m
.dn
= ldb
.Dn(self
.ldb
, self
.ldb
.domain_dn())
801 m
["pwdHistoryLength"] = ldb
.MessageElement(value
, ldb
.FLAG_MOD_REPLACE
, "pwdHistoryLength")
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#")