Fix PEP8 warning F841 local variable 'blah' is assigned to but never used
[samba.git] / python / samba / tests / samba_tool / passwordsettings.py
1 # Test 'samba-tool domain passwordsettings' sub-commands
2 #
3 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018
4 #
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.
9 #
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.
14 #
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/>.
17 #
18
19 import os
20 import ldb
21 from samba.tests.samba_tool.base import SambaToolCmdTest
22 from samba.tests.pso import PasswordSettings, TestUser
23
24 class PwdSettingsCmdTestCase(SambaToolCmdTest):
25     """Tests for 'samba-tool domain passwordsettings' subcommands"""
26
27     def setUp(self):
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()
35         self.obj_cleanup = []
36
37     def tearDown(self):
38         super(PwdSettingsCmdTestCase, self).tearDown()
39         # clean-up any objects the test has created
40         for dn in self.obj_cleanup:
41             self.ldb.delete(dn)
42
43     def check_pso(self, pso_name, pso):
44         """Checks the PSO info in the DB matches what's expected"""
45
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")
56
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))
65
66         # check the PSO's settings match the search results
67         self.assertEquals(str(res[0]['msDS-PasswordComplexityEnabled'][0]),
68                           complexity_str)
69         self.assertEquals(str(res[0]['msDS-PasswordReversibleEncryptionEnabled'][0]),
70                           plaintext_str)
71         self.assertEquals(int(res[0]['msDS-PasswordHistoryLength'][0]),
72                           pso.history_len)
73         self.assertEquals(int(res[0]['msDS-MinimumPasswordLength'][0]),
74                           pso.password_len)
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]),
78                           lockout_window)
79         self.assertEquals(int(res[0]['msDS-LockoutDuration'][0]),
80                           lockout_duration)
81         self.assertEquals(int(res[0]['msDS-LockoutThreshold'][0]),
82                           pso.lockout_attempts)
83         self.assertEquals(int(res[0]['msDS-PasswordSettingsPrecedence'][0]),
84                           pso.precedence)
85
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,
89                                                  "-H", self.server,
90                                                  self.user_auth)
91         self.assertTrue(len(out.split(":")) >= 10, "Expect 10 fields displayed")
92
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,
97                       out)
98
99     def test_pso_create(self):
100         """Tests basic PSO creation using the samba-tool"""
101
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
107
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",
113                                                  "-H", self.server,
114                                                  self.user_auth)
115         # make sure we clean-up after the test completes
116         self.obj_cleanup.append("CN=%s,%s" %(pso_name, self.pso_container))
117
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)
122
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",
127                                                  "-H", self.server,
128                                                  self.user_auth)
129         self.assertCmdFail(result, "Ensure that create for existing PSO fails")
130         self.assertIn("already exists", err)
131
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,
137                                                  self.user_auth)
138         self.assertCmdFail(result, "Ensure that create for existing PSO fails")
139         self.assertIn("specify at least one password policy setting", err)
140
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
149
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",
155                                                  "--min-pwd-age=11",
156                                                  "--max-pwd-age=50",
157                                                  "-H", self.server,
158                                                  self.user_auth)
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)
164
165         # check the PSOs we created are present in the 'list' command
166         (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
167                                                  "pso", "list"),
168                                                  "-H", self.server,
169                                                  self.user_auth)
170         self.assertCmdSuccess(result, out, err)
171         self.assertIn("test-create-PSO", out)
172         self.assertIn("test-create-PSO2", out)
173
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
181
182         (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
183                                                  "pso", "create"), pso_name,
184                                                  "200", "--min-pwd-length=10",
185                                                  "-H", self.server,
186                                                  self.user_auth)
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)
190
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)
196
197         return pso_settings
198
199     def test_pso_set(self):
200         """Tests we can modify a PSO using the samba-tool"""
201
202         pso_name = "test-set-PSO"
203         pso_settings = self._create_pso(pso_name)
204
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,
211                                                  "--precedence=99",
212                                                  "--account-lockout-threshold=10",
213                                                  "--account-lockout-duration=17",
214                                                  "-H", self.server,
215                                                  self.user_auth)
216         self.assertCmdSuccess(result, out, err)
217         self.assertEquals(err,"","Shouldn't be any error messages")
218         self.assertIn("Successfully updated", out)
219
220         # check the PSO's settings now reflect the new values
221         self.check_pso(pso_name, pso_settings)
222
223     def test_pso_delete(self):
224         """Tests we can delete a PSO using the samba-tool"""
225
226         pso_name = "test-delete-PSO"
227         self._create_pso(pso_name)
228
229         # check we can successfully delete the PSO
230         (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
231                                                  "pso", "delete"), pso_name,
232                                                  "-H", self.server,
233                                                  self.user_auth)
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)
239
240         # check the object no longer exists in the DB
241         try:
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)
247
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,
251                                                  "-H", self.server,
252                                                  self.user_auth)
253         self.assertCmdFail(result, "Deleteing a non-existent PSO should fail")
254         self.assertIn("Unable to find PSO", err)
255
256     def check_pso_applied(self, user, pso):
257         """Checks that the correct PSO is applied to a given user"""
258
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,
262                                                  "-H", self.server,
263                                                  self.user_auth)
264         self.assertCmdSuccess(result, out, err)
265         self.assertEquals(err,"","Shouldn't be any error messages")
266         if pso is None:
267             self.assertIn("No PSO applies to user", out)
268         else:
269             self.assertIn(pso.name, out)
270
271         # then check the DB tells us the same thing
272         if pso is None:
273             self.assertEquals(user.get_resultant_PSO(), None)
274         else:
275             self.assertEquals(user.get_resultant_PSO(), pso.dn)
276
277     def test_pso_apply_to_user(self):
278         """Checks we can apply/unapply a PSO to a user"""
279
280         pso_name = "test-apply-PSO"
281         test_pso = self._create_pso(pso_name)
282
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)
287
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)
294         m = ldb.Message()
295         m.dn = ldb.Dn(self.ldb, dn)
296         m["member"] = ldb.MessageElement(user.dn, ldb.FLAG_MOD_ADD, "member")
297         self.ldb.modify(m)
298
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,
303                                                  self.user_auth)
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)
307
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,
312                                                  self.user_auth)
313         self.assertCmdFail(result, "Shouldn't be able to apply PSO twice")
314         self.assertIn("already applies", err)
315
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,
320                                                  self.user_auth)
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)
324
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,
329                                                  self.user_auth)
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)
334
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,
339                                                  self.user_auth)
340         self.assertCmdSuccess(result, out, err)
341         self.assertEquals(err,"","Shouldn't be any error messages")
342         self.check_pso_applied(user, pso=None)
343
344     def test_pso_unpriv(self):
345         """Checks unprivileged users can't modify PSOs via samba-tool"""
346
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())
353
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)
361
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)
368
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)
374
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)
380
381         (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
382                                                  "pso", "apply"), pso_name,
383                                                  user.name, "-H", self.server,
384                                                  unpriv_auth)
385         self.assertCmdFail(result, "Need admin privileges to modify PSO")
386         self.assertIn("You may not have permission", err)
387
388         (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
389                                                  "pso", "unapply"), pso_name,
390                                                  user.name, "-H", self.server,
391                                                  unpriv_auth)
392         self.assertCmdFail(result, "Need admin privileges to modify PSO")
393         self.assertIn("You may not have permission", err)
394
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)
404
405     def test_domain_passwordsettings(self):
406         """Checks the 'set/show' commands for the domain settings (non-PSO)"""
407
408         # check the 'show' cmd for the domain settings
409         (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
410                                                  "show"), "-H", self.server,
411                                                  self.user_auth)
412         self.assertCmdSuccess(result, out, err)
413         self.assertEquals(err,"","Shouldn't be any error messages")
414
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)
418
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",
423                                                  "set"),
424                                                  "--min-pwd-length=%u" % new_len,
425                                                  "-H", self.server,
426                                                  self.user_auth)
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())
431
432         # check the updated value is now displayed
433         (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
434                                                  "show"), "-H", self.server,
435                                                  self.user_auth)
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)
439