ctdb-scripts: Improve update and listing code
[samba4-gss.git] / python / samba / tests / samba_tool / passwordsettings.py
blob6db7a585b82ab756299ed505caf6058bf43f13b8
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/>.
19 import os
20 import ldb
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"""
28 def setUp(self):
29 super().setUp()
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
36 self.obj_cleanup = []
38 def tearDown(self):
39 super().tearDown()
40 # clean-up any objects the test has created
41 for dn in self.obj_cleanup:
42 self.ldb.delete(dn)
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]),
72 complexity_str)
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]),
76 pso.history_len)
77 self.assertEqual(int(res[0]['msDS-MinimumPasswordLength'][0]),
78 pso.password_len)
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]),
82 lockout_window)
83 self.assertEqual(int(res[0]['msDS-LockoutDuration'][0]),
84 lockout_duration)
85 self.assertEqual(int(res[0]['msDS-LockoutThreshold'][0]),
86 pso.lockout_attempts)
87 self.assertEqual(int(res[0]['msDS-PasswordSettingsPrecedence'][0]),
88 pso.precedence)
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,
93 "-H", self.server,
94 self.user_auth)
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",
118 "-H", self.server,
119 self.user_auth)
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",
132 "-H", self.server,
133 self.user_auth)
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,
142 self.user_auth)
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",
160 "--min-pwd-age=11",
161 "--max-pwd-age=50",
162 "-H", self.server,
163 self.user_auth)
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",
172 "pso", "list"),
173 "-H", self.server,
174 self.user_auth)
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",
190 "-H", self.server,
191 self.user_auth)
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)
202 return 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,
216 "--precedence=99",
217 "--account-lockout-threshold=10",
218 "--account-lockout-duration=17",
219 "-H", self.server,
220 self.user_auth)
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,
237 "-H", self.server,
238 self.user_auth)
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
246 try:
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,
256 "-H", self.server,
257 self.user_auth)
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",
266 "pso", "show-user"),
267 user.name, "-H", self.server,
268 self.user_auth)
269 self.assertCmdSuccess(result, out, err)
270 self.assertEqual(err, "", "Shouldn't be any error messages")
271 if pso is None:
272 self.assertIn("No PSO applies to user", out)
273 else:
274 self.assertIn(pso.name, out)
276 # then check the DB tells us the same thing
277 if pso is None:
278 self.assertEqual(user.get_resultant_PSO(), None)
279 else:
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)
299 m = ldb.Message()
300 m.dn = ldb.Dn(self.ldb, dn)
301 m["member"] = ldb.MessageElement(user.dn, ldb.FLAG_MOD_ADD, "member")
302 self.ldb.modify(m)
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,
308 self.user_auth)
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,
317 self.user_auth)
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,
325 self.user_auth)
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,
334 self.user_auth)
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,
344 self.user_auth)
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",
370 "-H", self.server,
371 unpriv_auth)
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,
377 "-H", self.server,
378 unpriv_auth)
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,
384 "-H", self.server,
385 unpriv_auth)
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,
392 unpriv_auth)
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,
399 unpriv_auth)
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,
419 self.user_auth)
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,
433 "-H", self.server,
434 self.user_auth)
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,
443 self.user_auth)
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,
457 "-H", self.server,
458 self.user_auth)
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,
470 "-H", self.server,
471 self.user_auth)
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,
479 "-H", self.server,
480 self.user_auth)
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())