s3:libsmb: allow store_cldap_reply() to work with a ipv6 response
[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
25 class PwdSettingsCmdTestCase(SambaToolCmdTest):
26     """Tests for 'samba-tool domain passwordsettings' subcommands"""
27
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 = []
37
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)
43
44     def check_pso(self, pso_name, pso):
45         """Checks the PSO info in the DB matches what's expected"""
46
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")
60
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))
69
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)
89
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")
97
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)
103
104     def test_pso_create(self):
105         """Tests basic PSO creation using the samba-tool"""
106
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
112
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))
122
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)
127
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)
136
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)
145
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
154
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)
169
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)
178
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
186
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)
195
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)
201
202         return pso_settings
203
204     def test_pso_set(self):
205         """Tests we can modify a PSO using the samba-tool"""
206
207         pso_name = "test-set-PSO"
208         pso_settings = self._create_pso(pso_name)
209
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)
224
225         # check the PSO's settings now reflect the new values
226         self.check_pso(pso_name, pso_settings)
227
228     def test_pso_delete(self):
229         """Tests we can delete a PSO using the samba-tool"""
230
231         pso_name = "test-delete-PSO"
232         self._create_pso(pso_name)
233
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)
244
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)
252
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)
260
261     def check_pso_applied(self, user, pso):
262         """Checks that the correct PSO is applied to a given user"""
263
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)
275
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)
281
282     def test_pso_apply_to_user(self):
283         """Checks we can apply/unapply a PSO to a user"""
284
285         pso_name = "test-apply-PSO"
286         test_pso = self._create_pso(pso_name)
287
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)
292
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)
303
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)
312
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)
320
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)
329
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)
339
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)
348
349     def test_pso_unpriv(self):
350         """Checks unprivileged users can't modify PSOs via samba-tool"""
351
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())
358
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)
366
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)
374
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)
381
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)
388
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)
395
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)
402
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)
412
413     def test_domain_passwordsettings(self):
414         """Checks the 'set/show' commands for the domain settings (non-PSO)"""
415
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")
422
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)
426
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())
439
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)
447
448     def test_domain_passwordsettings_pwdage(self):
449         """Checks the 'set' command for the domain password age (non-PSO)"""
450
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())
463
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)
474
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())