1 # Test 'samba-tool domain passwordsettings' sub-commands
3 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 from samba
.tests
.samba_tool
.base
import SambaToolCmdTest
22 from samba
.tests
.pso
import PasswordSettings
, TestUser
25 class PwdSettingsCmdTestCase(SambaToolCmdTest
):
26 """Tests for 'samba-tool domain passwordsettings' subcommands"""
30 self
.server
= "ldap://%s" % os
.environ
["DC_SERVER"]
31 self
.user_auth
= "-U%s%%%s" % (os
.environ
["DC_USERNAME"],
32 os
.environ
["DC_PASSWORD"])
33 self
.ldb
= self
.getSamDB("-H", self
.server
, self
.user_auth
)
34 system_dn
= "CN=System,%s" % self
.ldb
.domain_dn()
35 self
.pso_container
= "CN=Password Settings Container,%s" % system_dn
40 # clean-up any objects the test has created
41 for dn
in self
.obj_cleanup
:
44 def check_pso(self
, pso_name
, pso
):
45 """Checks the PSO info in the DB matches what's expected"""
47 # lookup the PSO in the DB
48 dn
= "CN=%s,%s" % (pso_name
, self
.pso_container
)
49 pso_attrs
= ['name', 'msDS-PasswordSettingsPrecedence',
50 'msDS-PasswordReversibleEncryptionEnabled',
51 'msDS-PasswordHistoryLength',
52 'msDS-MinimumPasswordLength',
53 'msDS-PasswordComplexityEnabled',
54 'msDS-MinimumPasswordAge',
55 'msDS-MaximumPasswordAge',
56 'msDS-LockoutObservationWindow',
57 'msDS-LockoutThreshold', 'msDS-LockoutDuration']
58 res
= self
.ldb
.search(dn
, scope
=ldb
.SCOPE_BASE
, attrs
=pso_attrs
)
59 self
.assertEqual(len(res
), 1, "PSO lookup failed")
61 # convert types in the PSO-settings to what the search returns, i.e.
62 # boolean --> string, seconds --> timestamps in -100 nanosecond units
63 complexity_str
= "TRUE" if pso
.complexity
else "FALSE"
64 plaintext_str
= "TRUE" if pso
.store_plaintext
else "FALSE"
65 lockout_duration
= -int(pso
.lockout_duration
* (1e7
))
66 lockout_window
= -int(pso
.lockout_window
* (1e7
))
67 min_age
= -int(pso
.password_age_min
* (1e7
))
68 max_age
= -int(pso
.password_age_max
* (1e7
))
70 # check the PSO's settings match the search results
71 self
.assertEqual(str(res
[0]['msDS-PasswordComplexityEnabled'][0]),
73 plaintext_res
= res
[0]['msDS-PasswordReversibleEncryptionEnabled'][0]
74 self
.assertEqual(str(plaintext_res
), plaintext_str
)
75 self
.assertEqual(int(res
[0]['msDS-PasswordHistoryLength'][0]),
77 self
.assertEqual(int(res
[0]['msDS-MinimumPasswordLength'][0]),
79 self
.assertEqual(int(res
[0]['msDS-MinimumPasswordAge'][0]), min_age
)
80 self
.assertEqual(int(res
[0]['msDS-MaximumPasswordAge'][0]), max_age
)
81 self
.assertEqual(int(res
[0]['msDS-LockoutObservationWindow'][0]),
83 self
.assertEqual(int(res
[0]['msDS-LockoutDuration'][0]),
85 self
.assertEqual(int(res
[0]['msDS-LockoutThreshold'][0]),
87 self
.assertEqual(int(res
[0]['msDS-PasswordSettingsPrecedence'][0]),
90 # check we can also display the PSO via the show command
91 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
92 "pso", "show"), pso_name
,
95 self
.assertTrue(len(out
.split(":")) >= 10,
96 "Expect 10 fields displayed")
98 # for a few settings, sanity-check the display is what we expect
99 self
.assertIn("Minimum password length: %u" % pso
.password_len
, out
)
100 self
.assertIn("Password history length: %u" % pso
.history_len
, out
)
101 lockout_str
= "lockout threshold (attempts): %u" % pso
.lockout_attempts
102 self
.assertIn(lockout_str
, out
)
104 def test_pso_create(self
):
105 """Tests basic PSO creation using the samba-tool"""
107 # we expect the PSO to take the current domain settings by default
108 # (we'll set precedence/complexity, the rest should be the defaults)
109 expected_pso
= PasswordSettings(None, self
.ldb
)
110 expected_pso
.complexity
= False
111 expected_pso
.precedence
= 100
113 # check basic PSO creation works
114 pso_name
= "test-create-PSO"
115 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
116 "pso", "create"), pso_name
,
117 "100", "--complexity=off",
120 # make sure we clean-up after the test completes
121 self
.obj_cleanup
.append("CN=%s,%s" % (pso_name
, self
.pso_container
))
123 self
.assertCmdSuccess(result
, out
, err
)
124 self
.assertEqual(err
, "", "Shouldn't be any error messages")
125 self
.assertIn("successfully created", out
)
126 self
.check_pso(pso_name
, expected_pso
)
128 # check creating a PSO with the same name fails
129 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
130 "pso", "create"), pso_name
,
131 "100", "--complexity=off",
134 self
.assertCmdFail(result
, "Ensure that create for existing PSO fails")
135 self
.assertIn("already exists", err
)
137 # check we need to specify at least one password policy argument
138 pso_name
= "test-create-PSO2"
139 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
140 "pso", "create"), pso_name
,
141 "100", "-H", self
.server
,
143 self
.assertCmdFail(result
, "Ensure that create for existing PSO fails")
144 self
.assertIn("specify at least one password policy setting", err
)
146 # create a PSO with different settings and check they match
147 expected_pso
.complexity
= True
148 expected_pso
.store_plaintext
= True
149 expected_pso
.precedence
= 50
150 expected_pso
.password_len
= 12
151 day_in_secs
= 60 * 60 * 24
152 expected_pso
.password_age_min
= 11 * day_in_secs
153 expected_pso
.password_age_max
= 50 * day_in_secs
155 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
156 "pso", "create"), pso_name
,
157 "50", "--complexity=on",
158 "--store-plaintext=on",
159 "--min-pwd-length=12",
164 self
.obj_cleanup
.append("CN=%s,%s" % (pso_name
, self
.pso_container
))
165 self
.assertCmdSuccess(result
, out
, err
)
166 self
.assertEqual(err
, "", "Shouldn't be any error messages")
167 self
.assertIn("successfully created", out
)
168 self
.check_pso(pso_name
, expected_pso
)
170 # check the PSOs we created are present in the 'list' command
171 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
175 self
.assertCmdSuccess(result
, out
, err
)
176 self
.assertIn("test-create-PSO", out
)
177 self
.assertIn("test-create-PSO2", out
)
179 def _create_pso(self
, pso_name
):
180 """Creates a PSO for use in other tests"""
181 # the new PSO will take the current domain settings by default
182 pso_settings
= PasswordSettings(None, self
.ldb
)
183 pso_settings
.name
= pso_name
184 pso_settings
.password_len
= 10
185 pso_settings
.precedence
= 200
187 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
188 "pso", "create"), pso_name
,
189 "200", "--min-pwd-length=10",
192 # make sure we clean-up after the test completes
193 pso_settings
.dn
= "CN=%s,%s" % (pso_name
, self
.pso_container
)
194 self
.obj_cleanup
.append(pso_settings
.dn
)
196 # sanity-check the cmd was successful
197 self
.assertCmdSuccess(result
, out
, err
)
198 self
.assertEqual(err
, "", "Shouldn't be any error messages")
199 self
.assertIn("successfully created", out
)
200 self
.check_pso(pso_name
, pso_settings
)
204 def test_pso_set(self
):
205 """Tests we can modify a PSO using the samba-tool"""
207 pso_name
= "test-set-PSO"
208 pso_settings
= self
._create
_pso
(pso_name
)
210 # check we can update a PSO's settings
211 pso_settings
.precedence
= 99
212 pso_settings
.lockout_attempts
= 10
213 pso_settings
.lockout_duration
= 60 * 17
214 (res
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
215 "pso", "set"), pso_name
,
217 "--account-lockout-threshold=10",
218 "--account-lockout-duration=17",
221 self
.assertCmdSuccess(res
, out
, err
)
222 self
.assertEqual(err
, "", "Shouldn't be any error messages")
223 self
.assertIn("Successfully updated", out
)
225 # check the PSO's settings now reflect the new values
226 self
.check_pso(pso_name
, pso_settings
)
228 def test_pso_delete(self
):
229 """Tests we can delete a PSO using the samba-tool"""
231 pso_name
= "test-delete-PSO"
232 self
._create
_pso
(pso_name
)
234 # check we can successfully delete the PSO
235 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
236 "pso", "delete"), pso_name
,
239 self
.assertCmdSuccess(result
, out
, err
)
240 self
.assertEqual(err
, "", "Shouldn't be any error messages")
241 self
.assertIn("Deleted PSO", out
)
242 dn
= "CN=%s,%s" % (pso_name
, self
.pso_container
)
243 self
.obj_cleanup
.remove(dn
)
245 # check the object no longer exists in the DB
247 self
.ldb
.search(dn
, scope
=ldb
.SCOPE_BASE
, attrs
=['name'])
248 self
.fail("PSO shouldn't exist")
249 except ldb
.LdbError
as e
:
250 (enum
, estr
) = e
.args
251 self
.assertEqual(enum
, ldb
.ERR_NO_SUCH_OBJECT
)
253 # run the same cmd again - it should fail because PSO no longer exists
254 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
255 "pso", "delete"), pso_name
,
258 self
.assertCmdFail(result
, "Deleting a non-existent PSO should fail")
259 self
.assertIn("Unable to find PSO", err
)
261 def check_pso_applied(self
, user
, pso
):
262 """Checks that the correct PSO is applied to a given user"""
264 # first check the samba-tool output tells us the correct PSO is applied
265 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
267 user
.name
, "-H", self
.server
,
269 self
.assertCmdSuccess(result
, out
, err
)
270 self
.assertEqual(err
, "", "Shouldn't be any error messages")
272 self
.assertIn("No PSO applies to user", out
)
274 self
.assertIn(pso
.name
, out
)
276 # then check the DB tells us the same thing
278 self
.assertEqual(user
.get_resultant_PSO(), None)
280 self
.assertEqual(user
.get_resultant_PSO(), pso
.dn
)
282 def test_pso_apply_to_user(self
):
283 """Checks we can apply/unapply a PSO to a user"""
285 pso_name
= "test-apply-PSO"
286 test_pso
= self
._create
_pso
(pso_name
)
288 # check that a new user has no PSO applied by default
289 user
= TestUser("test-PSO-user", self
.ldb
)
290 self
.obj_cleanup
.append(user
.dn
)
291 self
.check_pso_applied(user
, pso
=None)
293 # add the user to a new group
294 group_name
= "test-PSO-group"
295 dn
= "CN=%s,%s" % (group_name
, self
.ldb
.domain_dn())
296 self
.ldb
.add({"dn": dn
, "objectclass": "group",
297 "sAMAccountName": group_name
})
298 self
.obj_cleanup
.append(dn
)
300 m
.dn
= ldb
.Dn(self
.ldb
, dn
)
301 m
["member"] = ldb
.MessageElement(user
.dn
, ldb
.FLAG_MOD_ADD
, "member")
304 # check samba-tool can successfully link a PSO to a group
305 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
306 "pso", "apply"), pso_name
,
307 group_name
, "-H", self
.server
,
309 self
.assertCmdSuccess(result
, out
, err
)
310 self
.assertEqual(err
, "", "Shouldn't be any error messages")
311 self
.check_pso_applied(user
, pso
=test_pso
)
313 # we should fail if we try to apply the same PSO/group twice though
314 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
315 "pso", "apply"), pso_name
,
316 group_name
, "-H", self
.server
,
318 self
.assertCmdFail(result
, "Shouldn't be able to apply PSO twice")
319 self
.assertIn("already applies", err
)
321 # check samba-tool can successfully link a PSO to a user
322 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
323 "pso", "apply"), pso_name
,
324 user
.name
, "-H", self
.server
,
326 self
.assertCmdSuccess(result
, out
, err
)
327 self
.assertEqual(err
, "", "Shouldn't be any error messages")
328 self
.check_pso_applied(user
, pso
=test_pso
)
330 # check samba-tool can successfully unlink a group from a PSO
331 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
332 "pso", "unapply"), pso_name
,
333 group_name
, "-H", self
.server
,
335 self
.assertCmdSuccess(result
, out
, err
)
336 self
.assertEqual(err
, "", "Shouldn't be any error messages")
337 # PSO still applies directly to the user, even though group was removed
338 self
.check_pso_applied(user
, pso
=test_pso
)
340 # check samba-tool can successfully unlink a user from a PSO
341 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
342 "pso", "unapply"), pso_name
,
343 user
.name
, "-H", self
.server
,
345 self
.assertCmdSuccess(result
, out
, err
)
346 self
.assertEqual(err
, "", "Shouldn't be any error messages")
347 self
.check_pso_applied(user
, pso
=None)
349 def test_pso_unpriv(self
):
350 """Checks unprivileged users can't modify PSOs via samba-tool"""
352 # create a dummy PSO and a non-admin user
353 pso_name
= "test-unpriv-PSO"
354 self
._create
_pso
(pso_name
)
355 user
= TestUser("test-unpriv-user", self
.ldb
)
356 self
.obj_cleanup
.append(user
.dn
)
357 unpriv_auth
= "-U%s%%%s" % (user
.name
, user
.get_password())
359 # check we need admin privileges to be able to do anything to PSOs
360 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
361 "pso", "set"), pso_name
,
362 "--complexity=off", "-H",
363 self
.server
, unpriv_auth
)
364 self
.assertCmdFail(result
, "Need admin privileges to modify PSO")
365 self
.assertIn("You may not have permission", err
)
367 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
368 "pso", "create"), "bad-perm",
369 "250", "--complexity=off",
372 self
.assertCmdFail(result
, "Need admin privileges to modify PSO")
373 self
.assertIn("Administrator permissions are needed", err
)
375 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
376 "pso", "delete"), pso_name
,
379 self
.assertCmdFail(result
, "Need admin privileges to delete PSO")
380 self
.assertIn("You may not have permission", err
)
382 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
383 "pso", "show"), pso_name
,
386 self
.assertCmdFail(result
, "Need admin privileges to view PSO")
387 self
.assertIn("You may not have permission", err
)
389 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
390 "pso", "apply"), pso_name
,
391 user
.name
, "-H", self
.server
,
393 self
.assertCmdFail(result
, "Need admin privileges to modify PSO")
394 self
.assertIn("You may not have permission", err
)
396 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
397 "pso", "unapply"), pso_name
,
398 user
.name
, "-H", self
.server
,
400 self
.assertCmdFail(result
, "Need admin privileges to modify PSO")
401 self
.assertIn("You may not have permission", err
)
403 # The 'list' command actually succeeds because it's not easy to tell
404 # whether we got no results due to lack of permissions, or because
405 # there were no PSOs to display
406 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
407 "pso", "list"), "-H",
408 self
.server
, unpriv_auth
)
409 self
.assertCmdSuccess(result
, out
, err
)
410 self
.assertIn("No PSOs", out
)
411 self
.assertIn("permission", out
)
413 def test_domain_passwordsettings(self
):
414 """Checks the 'set/show' commands for the domain settings (non-PSO)"""
416 # check the 'show' cmd for the domain settings
417 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
418 "show"), "-H", self
.server
,
420 self
.assertCmdSuccess(result
, out
, err
)
421 self
.assertEqual(err
, "", "Shouldn't be any error messages")
423 # check an arbitrary setting is displayed correctly
424 min_pwd_len
= self
.ldb
.get_minPwdLength()
425 self
.assertIn("Minimum password length: %s" % min_pwd_len
, out
)
427 # check we can change the domain setting
428 self
.addCleanup(self
.ldb
.set_minPwdLength
, min_pwd_len
)
429 new_len
= int(min_pwd_len
) + 3
430 min_pwd_args
= "--min-pwd-length=%u" % new_len
431 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
432 "set"), min_pwd_args
,
435 self
.assertCmdSuccess(result
, out
, err
)
436 self
.assertEqual(err
, "", "Shouldn't be any error messages")
437 self
.assertIn("successful", out
)
438 self
.assertEqual(new_len
, self
.ldb
.get_minPwdLength())
440 # check the updated value is now displayed
441 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
442 "show"), "-H", self
.server
,
444 self
.assertCmdSuccess(result
, out
, err
)
445 self
.assertEqual(err
, "", "Shouldn't be any error messages")
446 self
.assertIn("Minimum password length: %u" % new_len
, out
)
448 def test_domain_passwordsettings_pwdage(self
):
449 """Checks the 'set' command for the domain password age (non-PSO)"""
451 # check we can set the domain max password age
452 max_pwd_age
= self
.ldb
.get_maxPwdAge()
453 self
.addCleanup(self
.ldb
.set_maxPwdAge
, max_pwd_age
)
454 max_pwd_args
= "--max-pwd-age=270"
455 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
456 "set"), max_pwd_args
,
459 self
.assertCmdSuccess(result
, out
, err
)
460 self
.assertEqual(err
, "", "Shouldn't be any error messages")
461 self
.assertIn("successful", out
)
462 self
.assertNotEqual(max_pwd_age
, self
.ldb
.get_maxPwdAge())
464 # check we can't set the domain min password age to more than the max
465 min_pwd_age
= self
.ldb
.get_minPwdAge()
466 self
.addCleanup(self
.ldb
.set_minPwdAge
, min_pwd_age
)
467 min_pwd_args
= "--min-pwd-age=271"
468 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
469 "set"), min_pwd_args
,
472 self
.assertCmdFail(result
, "minPwdAge > maxPwdAge should be rejected")
473 self
.assertIn("Maximum password age", err
)
475 # check we can set the domain min password age to less than the max
476 min_pwd_args
= "--min-pwd-age=269"
477 (result
, out
, err
) = self
.runsublevelcmd("domain", ("passwordsettings",
478 "set"), min_pwd_args
,
481 self
.assertCmdSuccess(result
, out
, err
)
482 self
.assertEqual(err
, "", "Shouldn't be any error messages")
483 self
.assertIn("successful", out
)
484 self
.assertNotEqual(min_pwd_age
, self
.ldb
.get_minPwdAge())