tests: Extend passwordsettings tests to cover PSO command options
authorTim Beale <timbeale@catalyst.net.nz>
Thu, 10 May 2018 23:49:23 +0000 (11:49 +1200)
committerGarming Sam <garming@samba.org>
Wed, 23 May 2018 04:55:32 +0000 (06:55 +0200)
Add test cases for the new PSO samba-tool command options.

Signed-off-by: Tim Beale <timbeale@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Garming Sam <garming@catalyst.net.nz>
python/samba/tests/samba_tool/passwordsettings.py

index 5766352772d6c978b782ca83031af29e8739cb72..7c1afc8f51bf6b3d73c3ec13cf295ffff27436f3 100644 (file)
@@ -19,6 +19,7 @@
 import os
 import ldb
 from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba.tests.pso import PasswordSettings, TestUser
 
 class PwdSettingsCmdTestCase(SambaToolCmdTest):
     """Tests for 'samba-tool domain passwordsettings' subcommands"""
@@ -29,9 +30,377 @@ class PwdSettingsCmdTestCase(SambaToolCmdTest):
         self.user_auth = "-U%s%%%s" % (os.environ["DC_USERNAME"],
                                        os.environ["DC_PASSWORD"])
         self.ldb = self.getSamDB("-H", self.server, self.user_auth)
+        self.pso_container = \
+             "CN=Password Settings Container,CN=System,%s" % self.ldb.domain_dn()
+        self.obj_cleanup = []
 
     def tearDown(self):
         super(PwdSettingsCmdTestCase, self).tearDown()
+        # clean-up any objects the test has created
+        for dn in self.obj_cleanup:
+            self.ldb.delete(dn)
+
+    def check_pso(self, pso_name, pso):
+        """Checks the PSO info in the DB matches what's expected"""
+
+        # lookup the PSO in the DB
+        dn = "CN=%s,%s" %(pso_name, self.pso_container)
+        pso_attrs = ['name', 'msDS-PasswordSettingsPrecedence',
+                     'msDS-PasswordReversibleEncryptionEnabled',
+                     'msDS-PasswordHistoryLength', 'msDS-MinimumPasswordLength',
+                     'msDS-PasswordComplexityEnabled', 'msDS-MinimumPasswordAge',
+                     'msDS-MaximumPasswordAge', 'msDS-LockoutObservationWindow',
+                     'msDS-LockoutThreshold', 'msDS-LockoutDuration']
+        res = self.ldb.search(dn, scope=ldb.SCOPE_BASE, attrs=pso_attrs)
+        self.assertEquals(len(res), 1, "PSO lookup failed")
+
+        # convert types in the PSO-settings to what the search returns, i.e.
+        # boolean --> string, seconds --> timestamps in -100 nanosecond units
+        complexity_str = "TRUE" if pso.complexity else "FALSE"
+        plaintext_str = "TRUE" if pso.store_plaintext else "FALSE"
+        lockout_duration = -int(pso.lockout_duration * (1e7))
+        lockout_window = -int(pso.lockout_window * (1e7))
+        min_age = -int(pso.password_age_min * (1e7))
+        max_age = -int(pso.password_age_max * (1e7))
+
+        # check the PSO's settings match the search results
+        self.assertEquals(str(res[0]['msDS-PasswordComplexityEnabled'][0]),
+                          complexity_str)
+        self.assertEquals(str(res[0]['msDS-PasswordReversibleEncryptionEnabled'][0]),
+                          plaintext_str)
+        self.assertEquals(int(res[0]['msDS-PasswordHistoryLength'][0]),
+                          pso.history_len)
+        self.assertEquals(int(res[0]['msDS-MinimumPasswordLength'][0]),
+                          pso.password_len)
+        self.assertEquals(int(res[0]['msDS-MinimumPasswordAge'][0]), min_age)
+        self.assertEquals(int(res[0]['msDS-MaximumPasswordAge'][0]), max_age)
+        self.assertEquals(int(res[0]['msDS-LockoutObservationWindow'][0]),
+                          lockout_window)
+        self.assertEquals(int(res[0]['msDS-LockoutDuration'][0]),
+                          lockout_duration)
+        self.assertEquals(int(res[0]['msDS-LockoutThreshold'][0]),
+                          pso.lockout_attempts)
+        self.assertEquals(int(res[0]['msDS-PasswordSettingsPrecedence'][0]),
+                          pso.precedence)
+
+        # check we can also display the PSO via the show command
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "show"), pso_name,
+                                                 "-H", self.server,
+                                                 self.user_auth)
+        self.assertTrue(len(out.split(":")) >= 10, "Expect 10 fields displayed")
+
+        # for a few settings, sanity-check the display is what we expect
+        self.assertIn("Minimum password length: %u" % pso.password_len, out)
+        self.assertIn("Password history length: %u" % pso.history_len, out)
+        self.assertIn("lockout threshold (attempts): %u" % pso.lockout_attempts,
+                      out)
+
+    def test_pso_create(self):
+        """Tests basic PSO creation using the samba-tool"""
+
+        # we expect the PSO to take the current domain settings by default
+        # (we'll set precedence/complexity, the rest should be the defaults)
+        expected_pso = PasswordSettings(None, self.ldb)
+        expected_pso.complexity = False
+        expected_pso.precedence = 100
+
+        # check basic PSO creation works
+        pso_name = "test-create-PSO"
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "create"), pso_name,
+                                                 "100", "--complexity=off",
+                                                 "-H", self.server,
+                                                 self.user_auth)
+        # make sure we clean-up after the test completes
+        self.obj_cleanup.append("CN=%s,%s" %(pso_name, self.pso_container))
+
+        self.assertCmdSuccess(result, out, err)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("successfully created", out)
+        self.check_pso(pso_name, expected_pso)
+
+        # check creating a PSO with the same name fails
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "create"), pso_name,
+                                                 "100", "--complexity=off",
+                                                 "-H", self.server,
+                                                 self.user_auth)
+        self.assertCmdFail(result, "Ensure that create for existing PSO fails")
+        self.assertIn("already exists", err)
+
+        # check we need to specify at least one password policy argument
+        pso_name = "test-create-PSO2"
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "create"), pso_name,
+                                                 "100", "-H", self.server,
+                                                 self.user_auth)
+        self.assertCmdFail(result, "Ensure that create for existing PSO fails")
+        self.assertIn("specify at least one password policy setting", err)
+
+        # create a PSO with different settings and check they match
+        expected_pso.complexity = True
+        expected_pso.store_plaintext = True
+        expected_pso.precedence = 50
+        expected_pso.password_len = 12
+        day_in_secs = 60 * 60 * 24
+        expected_pso.password_age_min = 11 * day_in_secs
+        expected_pso.password_age_max = 50 * day_in_secs
+
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "create"), pso_name,
+                                                 "50", "--complexity=on",
+                                                 "--store-plaintext=on",
+                                                 "--min-pwd-length=12",
+                                                 "--min-pwd-age=11",
+                                                 "--max-pwd-age=50",
+                                                 "-H", self.server,
+                                                 self.user_auth)
+        self.obj_cleanup.append("CN=%s,%s" %(pso_name, self.pso_container))
+        self.assertCmdSuccess(result, out, err)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("successfully created", out)
+        self.check_pso(pso_name, expected_pso)
+
+        # check the PSOs we created are present in the 'list' command
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "list"),
+                                                 "-H", self.server,
+                                                 self.user_auth)
+        self.assertCmdSuccess(result, out, err)
+        self.assertIn("test-create-PSO", out)
+        self.assertIn("test-create-PSO2", out)
+
+    def _create_pso(self, pso_name):
+        """Creates a PSO for use in other tests"""
+        # the new PSO will take the current domain settings by default
+        pso_settings = PasswordSettings(None, self.ldb)
+        pso_settings.name = pso_name
+        pso_settings.password_len = 10
+        pso_settings.precedence = 200
+
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "create"), pso_name,
+                                                 "200", "--min-pwd-length=10",
+                                                 "-H", self.server,
+                                                 self.user_auth)
+        # make sure we clean-up after the test completes
+        pso_settings.dn = "CN=%s,%s" %(pso_name, self.pso_container)
+        self.obj_cleanup.append(pso_settings.dn)
+
+        # sanity-check the cmd was successful
+        self.assertCmdSuccess(result, out, err)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("successfully created", out)
+        self.check_pso(pso_name, pso_settings)
+
+        return pso_settings
+
+    def test_pso_set(self):
+        """Tests we can modify a PSO using the samba-tool"""
+
+        pso_name = "test-set-PSO"
+        pso_settings = self._create_pso(pso_name)
+
+        # check we can update a PSO's settings
+        pso_settings.precedence = 99
+        pso_settings.lockout_attempts = 10
+        pso_settings.lockout_duration = 60 * 17
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "set"), pso_name,
+                                                 "--precedence=99",
+                                                 "--account-lockout-threshold=10",
+                                                 "--account-lockout-duration=17",
+                                                 "-H", self.server,
+                                                 self.user_auth)
+        self.assertCmdSuccess(result, out, err)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("Successfully updated", out)
+
+        # check the PSO's settings now reflect the new values
+        self.check_pso(pso_name, pso_settings)
+
+    def test_pso_delete(self):
+        """Tests we can delete a PSO using the samba-tool"""
+
+        pso_name = "test-delete-PSO"
+        pso_settings = self._create_pso(pso_name)
+
+        # check we can successfully delete the PSO
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "delete"), pso_name,
+                                                 "-H", self.server,
+                                                 self.user_auth)
+        self.assertCmdSuccess(result, out, err)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.assertIn("Deleted PSO", out)
+        dn = "CN=%s,%s" %(pso_name, self.pso_container)
+        self.obj_cleanup.remove(dn)
+
+        # check the object no longer exists in the DB
+        try:
+            res = self.ldb.search(dn, scope=ldb.SCOPE_BASE, attrs=['name'])
+            self.fail("PSO shouldn't exist")
+        except ldb.LdbError as e:
+            (enum, estr) = e.args
+            self.assertEquals(enum, ldb.ERR_NO_SUCH_OBJECT)
+
+        # run the same cmd again - it should fail because PSO no longer exists
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "delete"), pso_name,
+                                                 "-H", self.server,
+                                                 self.user_auth)
+        self.assertCmdFail(result, "Deleteing a non-existent PSO should fail")
+        self.assertIn("Unable to find PSO", err)
+
+    def check_pso_applied(self, user, pso):
+        """Checks that the correct PSO is applied to a given user"""
+
+        # first check the samba-tool output tells us the correct PSO is applied
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "show-user"), user.name,
+                                                 "-H", self.server,
+                                                 self.user_auth)
+        self.assertCmdSuccess(result, out, err)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        if pso is None:
+            self.assertIn("No PSO applies to user", out)
+        else:
+            self.assertIn(pso.name, out)
+
+        # then check the DB tells us the same thing
+        if pso is None:
+            self.assertEquals(user.get_resultant_PSO(), None)
+        else:
+            self.assertEquals(user.get_resultant_PSO(), pso.dn)
+
+    def test_pso_apply_to_user(self):
+        """Checks we can apply/unapply a PSO to a user"""
+
+        pso_name = "test-apply-PSO"
+        test_pso = self._create_pso(pso_name)
+
+        # check that a new user has no PSO applied by default
+        user = TestUser("test-PSO-user", self.ldb)
+        self.obj_cleanup.append(user.dn)
+        self.check_pso_applied(user, pso=None)
+
+        # add the user to a new group
+        group_name = "test-PSO-group"
+        dn = "CN=%s,%s" %(group_name, self.ldb.domain_dn())
+        self.ldb.add({"dn": dn, "objectclass": "group",
+                      "sAMAccountName": group_name})
+        self.obj_cleanup.append(dn)
+        m = ldb.Message()
+        m.dn = ldb.Dn(self.ldb, dn)
+        m["member"] = ldb.MessageElement(user.dn, ldb.FLAG_MOD_ADD, "member")
+        self.ldb.modify(m)
+
+        # check samba-tool can successfully link a PSO to a group
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "apply"), pso_name,
+                                                 group_name, "-H", self.server,
+                                                 self.user_auth)
+        self.assertCmdSuccess(result, out, err)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.check_pso_applied(user, pso=test_pso)
+
+        # we should fail if we try to apply the same PSO/group twice though
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "apply"), pso_name,
+                                                 group_name, "-H", self.server,
+                                                 self.user_auth)
+        self.assertCmdFail(result, "Shouldn't be able to apply PSO twice")
+        self.assertIn("already applies", err)
+
+        # check samba-tool can successfully link a PSO to a user
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "apply"), pso_name,
+                                                 user.name, "-H", self.server,
+                                                 self.user_auth)
+        self.assertCmdSuccess(result, out, err)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.check_pso_applied(user, pso=test_pso)
+
+        # check samba-tool can successfully unlink a group from a PSO
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "unapply"), pso_name,
+                                                 group_name, "-H", self.server,
+                                                 self.user_auth)
+        self.assertCmdSuccess(result, out, err)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        # PSO still applies directly to the user, even though group was removed
+        self.check_pso_applied(user, pso=test_pso)
+
+        # check samba-tool can successfully unlink a user from a PSO
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "unapply"), pso_name,
+                                                 user.name, "-H", self.server,
+                                                 self.user_auth)
+        self.assertCmdSuccess(result, out, err)
+        self.assertEquals(err,"","Shouldn't be any error messages")
+        self.check_pso_applied(user, pso=None)
+
+    def test_pso_unpriv(self):
+        """Checks unprivileged users can't modify PSOs via samba-tool"""
+
+        # create a dummy PSO and a non-admin user
+        pso_name = "test-unpriv-PSO"
+        test_pso = self._create_pso(pso_name)
+        user = TestUser("test-unpriv-user", self.ldb)
+        self.obj_cleanup.append(user.dn)
+        unpriv_auth = "-U%s%%%s" %(user.name, user.get_password())
+
+        # check we need admin privileges to be able to do anything to PSOs
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "set"), pso_name,
+                                                 "--complexity=off", "-H",
+                                                 self.server, unpriv_auth)
+        self.assertCmdFail(result, "Need admin privileges to modify PSO")
+        self.assertIn("You may not have permission", err)
+
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "create"), "bad-perm",
+                                                 "250", "--complexity=off",
+                                                 "-H", self.server, unpriv_auth)
+        self.assertCmdFail(result, "Need admin privileges to modify PSO")
+        self.assertIn("Administrator permissions are needed", err)
+
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "delete"), pso_name,
+                                                 "-H", self.server, unpriv_auth)
+        self.assertCmdFail(result, "Need admin privileges to delete PSO")
+        self.assertIn("You may not have permission", err)
+
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "show"), pso_name,
+                                                 "-H", self.server, unpriv_auth)
+        self.assertCmdFail(result, "Need admin privileges to view PSO")
+        self.assertIn("You may not have permission", err)
+
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "apply"), pso_name,
+                                                 user.name, "-H", self.server,
+                                                 unpriv_auth)
+        self.assertCmdFail(result, "Need admin privileges to modify PSO")
+        self.assertIn("You may not have permission", err)
+
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "unapply"), pso_name,
+                                                 user.name, "-H", self.server,
+                                                 unpriv_auth)
+        self.assertCmdFail(result, "Need admin privileges to modify PSO")
+        self.assertIn("You may not have permission", err)
+
+        # The 'list' command actually succeeds because it's not easy to tell
+        # whether we got no results due to lack of permissions, or because
+        # there were no PSOs to display
+        (result, out, err) = self.runsublevelcmd("domain", ("passwordsettings",
+                                                 "pso", "list"), "-H",
+                                                 self.server, unpriv_auth)
+        self.assertCmdSuccess(result, out, err)
+        self.assertIn("No PSOs", out)
+        self.assertIn("permission", out)
 
     def test_domain_passwordsettings(self):
         """Checks the 'set/show' commands for the domain settings (non-PSO)"""