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
24 class PwdSettingsCmdTestCase(SambaToolCmdTest):
25 """Tests for 'samba-tool domain passwordsettings' subcommands"""
28 super(PwdSettingsCmdTestCase, self).setUp()
29 self.server = "ldap://%s" % os.environ["DC_SERVER"]
30 self.user_auth = "-U%s%%%s" % (os.environ["DC_USERNAME"],
31 os.environ["DC_PASSWORD"])
32 self.ldb = self.getSamDB("-H", self.server, self.user_auth)
33 self.pso_container = \
34 "CN=Password Settings Container,CN=System,%s" % self.ldb.domain_dn()
38 super(PwdSettingsCmdTestCase, self).tearDown()
39 # clean-up any objects the test has created
40 for dn in self.obj_cleanup:
43 def check_pso(self, pso_name, pso):
44 """Checks the PSO info in the DB matches what's expected"""
46 # lookup the PSO in the DB
47 dn = "CN=%s,%s" %(pso_name, self.pso_container)
48 pso_attrs = ['name', 'msDS-PasswordSettingsPrecedence',
49 'msDS-PasswordReversibleEncryptionEnabled',
50 'msDS-PasswordHistoryLength', 'msDS-MinimumPasswordLength',
51 'msDS-PasswordComplexityEnabled', 'msDS-MinimumPasswordAge',
52 'msDS-MaximumPasswordAge', 'msDS-LockoutObservationWindow',
53 'msDS-LockoutThreshold', 'msDS-LockoutDuration']
54 res = self.ldb.search(dn, scope=ldb.SCOPE_BASE, attrs=pso_attrs)
55 self.assertEquals(len(res), 1, "PSO lookup failed")
57 # convert types in the PSO-settings to what the search returns, i.e.
58 # boolean --> string, seconds --> timestamps in -100 nanosecond units
59 complexity_str = "TRUE" if pso.complexity else "FALSE"
60 plaintext_str = "TRUE" if pso.store_plaintext else "FALSE"
61 lockout_duration = -int(pso.lockout_duration * (1e7))
62 lockout_window = -int(pso.lockout_window * (1e7))
63 min_age = -int(pso.password_age_min * (1e7))
64 max_age = -int(pso.password_age_max * (1e7))
66 # check the PSO's settings match the search results
67 self.assertEquals(str(res[0]['msDS-PasswordComplexityEnabled'][0]),
69 self.assertEquals(str(res[0]['msDS-PasswordReversibleEncryptionEnabled'][0]),
71 self.assertEquals(int(res[0]['msDS-PasswordHistoryLength'][0]),
73 self.assertEquals(int(res[0]['msDS-MinimumPasswordLength'][0]),
75 self.assertEquals(int(res[0]['msDS-MinimumPasswordAge'][0]), min_age)
76 self.assertEquals(int(res[0]['msDS-MaximumPasswordAge'][0]), max_age)
77 self.assertEquals(int(res[0]['msDS-LockoutObservationWindow'][0]),
79 self.assertEquals(int(res[0]['msDS-LockoutDuration'][0]),
81 self.assertEquals(int(res[0]['msDS-LockoutThreshold'][0]),
83 self.assertEquals(int(res[0]['msDS-PasswordSettingsPrecedence'][0]),
86 # check we can also display the PSO via the show command
87 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
88 "pso", "show"), pso_name,
91 self.assertTrue(len(out.split(":")) >= 10, "Expect 10 fields displayed")
93 # for a few settings, sanity-check the display is what we expect
94 self.assertIn("Minimum password length: %u" % pso.password_len, out)
95 self.assertIn("Password history length: %u" % pso.history_len, out)
96 self.assertIn("lockout threshold (attempts): %u" % pso.lockout_attempts,
99 def test_pso_create(self):
100 """Tests basic PSO creation using the samba-tool"""
102 # we expect the PSO to take the current domain settings by default
103 # (we'll set precedence/complexity, the rest should be the defaults)
104 expected_pso = PasswordSettings(None, self.ldb)
105 expected_pso.complexity = False
106 expected_pso.precedence = 100
108 # check basic PSO creation works
109 pso_name = "test-create-PSO"
110 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
111 "pso", "create"), pso_name,
112 "100", "--complexity=off",
115 # make sure we clean-up after the test completes
116 self.obj_cleanup.append("CN=%s,%s" %(pso_name, self.pso_container))
118 self.assertCmdSuccess(result, out, err)
119 self.assertEquals(err,"","Shouldn't be any error messages")
120 self.assertIn("successfully created", out)
121 self.check_pso(pso_name, expected_pso)
123 # check creating a PSO with the same name fails
124 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
125 "pso", "create"), pso_name,
126 "100", "--complexity=off",
129 self.assertCmdFail(result, "Ensure that create for existing PSO fails")
130 self.assertIn("already exists", err)
132 # check we need to specify at least one password policy argument
133 pso_name = "test-create-PSO2"
134 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
135 "pso", "create"), pso_name,
136 "100", "-H", self.server,
138 self.assertCmdFail(result, "Ensure that create for existing PSO fails")
139 self.assertIn("specify at least one password policy setting", err)
141 # create a PSO with different settings and check they match
142 expected_pso.complexity = True
143 expected_pso.store_plaintext = True
144 expected_pso.precedence = 50
145 expected_pso.password_len = 12
146 day_in_secs = 60 * 60 * 24
147 expected_pso.password_age_min = 11 * day_in_secs
148 expected_pso.password_age_max = 50 * day_in_secs
150 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
151 "pso", "create"), pso_name,
152 "50", "--complexity=on",
153 "--store-plaintext=on",
154 "--min-pwd-length=12",
159 self.obj_cleanup.append("CN=%s,%s" %(pso_name, self.pso_container))
160 self.assertCmdSuccess(result, out, err)
161 self.assertEquals(err,"","Shouldn't be any error messages")
162 self.assertIn("successfully created", out)
163 self.check_pso(pso_name, expected_pso)
165 # check the PSOs we created are present in the 'list' command
166 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
170 self.assertCmdSuccess(result, out, err)
171 self.assertIn("test-create-PSO", out)
172 self.assertIn("test-create-PSO2", out)
174 def _create_pso(self, pso_name):
175 """Creates a PSO for use in other tests"""
176 # the new PSO will take the current domain settings by default
177 pso_settings = PasswordSettings(None, self.ldb)
178 pso_settings.name = pso_name
179 pso_settings.password_len = 10
180 pso_settings.precedence = 200
182 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
183 "pso", "create"), pso_name,
184 "200", "--min-pwd-length=10",
187 # make sure we clean-up after the test completes
188 pso_settings.dn = "CN=%s,%s" %(pso_name, self.pso_container)
189 self.obj_cleanup.append(pso_settings.dn)
191 # sanity-check the cmd was successful
192 self.assertCmdSuccess(result, out, err)
193 self.assertEquals(err,"","Shouldn't be any error messages")
194 self.assertIn("successfully created", out)
195 self.check_pso(pso_name, pso_settings)
199 def test_pso_set(self):
200 """Tests we can modify a PSO using the samba-tool"""
202 pso_name = "test-set-PSO"
203 pso_settings = self._create_pso(pso_name)
205 # check we can update a PSO's settings
206 pso_settings.precedence = 99
207 pso_settings.lockout_attempts = 10
208 pso_settings.lockout_duration = 60 * 17
209 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
210 "pso", "set"), pso_name,
212 "--account-lockout-threshold=10",
213 "--account-lockout-duration=17",
216 self.assertCmdSuccess(result, out, err)
217 self.assertEquals(err,"","Shouldn't be any error messages")
218 self.assertIn("Successfully updated", out)
220 # check the PSO's settings now reflect the new values
221 self.check_pso(pso_name, pso_settings)
223 def test_pso_delete(self):
224 """Tests we can delete a PSO using the samba-tool"""
226 pso_name = "test-delete-PSO"
227 self._create_pso(pso_name)
229 # check we can successfully delete the PSO
230 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
231 "pso", "delete"), pso_name,
234 self.assertCmdSuccess(result, out, err)
235 self.assertEquals(err,"","Shouldn't be any error messages")
236 self.assertIn("Deleted PSO", out)
237 dn = "CN=%s,%s" %(pso_name, self.pso_container)
238 self.obj_cleanup.remove(dn)
240 # check the object no longer exists in the DB
242 self.ldb.search(dn, scope=ldb.SCOPE_BASE, attrs=['name'])
243 self.fail("PSO shouldn't exist")
244 except ldb.LdbError as e:
245 (enum, estr) = e.args
246 self.assertEquals(enum, ldb.ERR_NO_SUCH_OBJECT)
248 # run the same cmd again - it should fail because PSO no longer exists
249 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
250 "pso", "delete"), pso_name,
253 self.assertCmdFail(result, "Deleteing a non-existent PSO should fail")
254 self.assertIn("Unable to find PSO", err)
256 def check_pso_applied(self, user, pso):
257 """Checks that the correct PSO is applied to a given user"""
259 # first check the samba-tool output tells us the correct PSO is applied
260 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
261 "pso", "show-user"), user.name,
264 self.assertCmdSuccess(result, out, err)
265 self.assertEquals(err,"","Shouldn't be any error messages")
267 self.assertIn("No PSO applies to user", out)
269 self.assertIn(pso.name, out)
271 # then check the DB tells us the same thing
273 self.assertEquals(user.get_resultant_PSO(), None)
275 self.assertEquals(user.get_resultant_PSO(), pso.dn)
277 def test_pso_apply_to_user(self):
278 """Checks we can apply/unapply a PSO to a user"""
280 pso_name = "test-apply-PSO"
281 test_pso = self._create_pso(pso_name)
283 # check that a new user has no PSO applied by default
284 user = TestUser("test-PSO-user", self.ldb)
285 self.obj_cleanup.append(user.dn)
286 self.check_pso_applied(user, pso=None)
288 # add the user to a new group
289 group_name = "test-PSO-group"
290 dn = "CN=%s,%s" %(group_name, self.ldb.domain_dn())
291 self.ldb.add({"dn": dn, "objectclass": "group",
292 "sAMAccountName": group_name})
293 self.obj_cleanup.append(dn)
295 m.dn = ldb.Dn(self.ldb, dn)
296 m["member"] = ldb.MessageElement(user.dn, ldb.FLAG_MOD_ADD, "member")
299 # check samba-tool can successfully link a PSO to a group
300 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
301 "pso", "apply"), pso_name,
302 group_name, "-H", self.server,
304 self.assertCmdSuccess(result, out, err)
305 self.assertEquals(err,"","Shouldn't be any error messages")
306 self.check_pso_applied(user, pso=test_pso)
308 # we should fail if we try to apply the same PSO/group twice though
309 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
310 "pso", "apply"), pso_name,
311 group_name, "-H", self.server,
313 self.assertCmdFail(result, "Shouldn't be able to apply PSO twice")
314 self.assertIn("already applies", err)
316 # check samba-tool can successfully link a PSO to a user
317 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
318 "pso", "apply"), pso_name,
319 user.name, "-H", self.server,
321 self.assertCmdSuccess(result, out, err)
322 self.assertEquals(err,"","Shouldn't be any error messages")
323 self.check_pso_applied(user, pso=test_pso)
325 # check samba-tool can successfully unlink a group from a PSO
326 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
327 "pso", "unapply"), pso_name,
328 group_name, "-H", self.server,
330 self.assertCmdSuccess(result, out, err)
331 self.assertEquals(err,"","Shouldn't be any error messages")
332 # PSO still applies directly to the user, even though group was removed
333 self.check_pso_applied(user, pso=test_pso)
335 # check samba-tool can successfully unlink a user from a PSO
336 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
337 "pso", "unapply"), pso_name,
338 user.name, "-H", self.server,
340 self.assertCmdSuccess(result, out, err)
341 self.assertEquals(err,"","Shouldn't be any error messages")
342 self.check_pso_applied(user, pso=None)
344 def test_pso_unpriv(self):
345 """Checks unprivileged users can't modify PSOs via samba-tool"""
347 # create a dummy PSO and a non-admin user
348 pso_name = "test-unpriv-PSO"
349 self._create_pso(pso_name)
350 user = TestUser("test-unpriv-user", self.ldb)
351 self.obj_cleanup.append(user.dn)
352 unpriv_auth = "-U%s%%%s" %(user.name, user.get_password())
354 # check we need admin privileges to be able to do anything to PSOs
355 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
356 "pso", "set"), pso_name,
357 "--complexity=off", "-H",
358 self.server, unpriv_auth)
359 self.assertCmdFail(result, "Need admin privileges to modify PSO")
360 self.assertIn("You may not have permission", err)
362 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
363 "pso", "create"), "bad-perm",
364 "250", "--complexity=off",
365 "-H", self.server, unpriv_auth)
366 self.assertCmdFail(result, "Need admin privileges to modify PSO")
367 self.assertIn("Administrator permissions are needed", err)
369 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
370 "pso", "delete"), pso_name,
371 "-H", self.server, unpriv_auth)
372 self.assertCmdFail(result, "Need admin privileges to delete PSO")
373 self.assertIn("You may not have permission", err)
375 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
376 "pso", "show"), pso_name,
377 "-H", self.server, unpriv_auth)
378 self.assertCmdFail(result, "Need admin privileges to view PSO")
379 self.assertIn("You may not have permission", err)
381 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
382 "pso", "apply"), pso_name,
383 user.name, "-H", self.server,
385 self.assertCmdFail(result, "Need admin privileges to modify PSO")
386 self.assertIn("You may not have permission", err)
388 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
389 "pso", "unapply"), pso_name,
390 user.name, "-H", self.server,
392 self.assertCmdFail(result, "Need admin privileges to modify PSO")
393 self.assertIn("You may not have permission", err)
395 # The 'list' command actually succeeds because it's not easy to tell
396 # whether we got no results due to lack of permissions, or because
397 # there were no PSOs to display
398 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
399 "pso", "list"), "-H",
400 self.server, unpriv_auth)
401 self.assertCmdSuccess(result, out, err)
402 self.assertIn("No PSOs", out)
403 self.assertIn("permission", out)
405 def test_domain_passwordsettings(self):
406 """Checks the 'set/show' commands for the domain settings (non-PSO)"""
408 # check the 'show' cmd for the domain settings
409 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
410 "show"), "-H", self.server,
412 self.assertCmdSuccess(result, out, err)
413 self.assertEquals(err,"","Shouldn't be any error messages")
415 # check an arbitrary setting is displayed correctly
416 min_pwd_len = self.ldb.get_minPwdLength()
417 self.assertIn("Minimum password length: %s" % min_pwd_len, out)
419 # check we can change the domain setting
420 self.addCleanup(self.ldb.set_minPwdLength, min_pwd_len)
421 new_len = int(min_pwd_len) + 3
422 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
424 "--min-pwd-length=%u" % new_len,
427 self.assertCmdSuccess(result, out, err)
428 self.assertEquals(err,"","Shouldn't be any error messages")
429 self.assertIn("successful", out)
430 self.assertEquals(new_len, self.ldb.get_minPwdLength())
432 # check the updated value is now displayed
433 (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
434 "show"), "-H", self.server,
436 self.assertCmdSuccess(result, out, err)
437 self.assertEquals(err,"","Shouldn't be any error messages")
438 self.assertIn("Minimum password length: %u" % new_len, out)