]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
samba-tool: validate password early in `domain provision`
authorJamie McClymont <jamiemcclymont@catalyst.net.nz>
Tue, 28 Nov 2017 02:45:30 +0000 (15:45 +1300)
committerAndrew Bartlett <abartlet@samba.org>
Sat, 9 Dec 2017 23:47:30 +0000 (00:47 +0100)
Checks password against default quality and length standards when it is entered,
allowing a second chance to enter one (if interactive), rather than running
through the provisioning process and bailing on an exception

Includes unit tests for the newly-added python wrapper of check_password_quality
plus black-box tests for the checks in samba-tool.

Breaks an openldap test which uses an invalid password.

BUG: https://bugzilla.samba.org/show_bug.cgi?id=9710
BUG: https://bugzilla.samba.org/show_bug.cgi?id=12235

Signed-off-by: Jamie McClymont <jamiemcclymont@catalyst.net.nz>
Reviewed-by: Gary Lockyer <gary@catalyst.net.nz>
python/pyglue.c
python/samba/__init__.py
python/samba/netcmd/domain.py
python/samba/provision/__init__.py
python/samba/tests/password_quality.py [new file with mode: 0644]
python/samba/tests/samba_tool/provision_password_check.py [new file with mode: 0644]
selftest/knownfail.d/validate-password-early [new file with mode: 0644]
selftest/tests.py
source4/selftest/tests.py
source4/setup/provision_basedn_modify.ldif

index 07cde49e16bda6ce0e0bea0b67e2bcdda8d8fad1..d928b4cda41f3f80bf390e63c4a42eb47495117f 100644 (file)
@@ -78,6 +78,17 @@ static PyObject *py_generate_random_machine_password(PyObject *self, PyObject *a
        return ret;
 }
 
+static PyObject *py_check_password_quality(PyObject *self, PyObject *args)
+{
+       char *pass;
+
+       if (!PyArg_ParseTuple(args, "s", &pass)) {
+               return NULL;
+       }
+
+       return PyBool_FromLong(check_password_quality(pass));
+}
+
 static PyObject *py_unix2nttime(PyObject *self, PyObject *args)
 {
        time_t t;
@@ -296,6 +307,12 @@ static PyMethodDef py_misc_methods[] = {
                "(based on random utf16 characters converted to utf8 or "
                "random ascii characters if 'unix charset' is not 'utf8')"
                "with a length >= min (at least 14) and <= max (at most 255)." },
+       { "check_password_quality", (PyCFunction)py_check_password_quality,
+               METH_VARARGS, "check_password_quality(pass) -> bool\n"
+               "Check password quality against Samba's check_password_quality,"
+               "the implementation of Microsoft's rules:"
+               "http://msdn.microsoft.com/en-us/subscriptions/cc786468%28v=ws.10%29.aspx"
+       },
        { "unix2nttime", (PyCFunction)py_unix2nttime, METH_VARARGS,
                "unix2nttime(timestamp) -> nttime" },
        { "nttime2unix", (PyCFunction)py_nttime2unix, METH_VARARGS,
index 6f79b3cc960e8b0e03d37c6d9d3cc1663d1a716f..6ba7c9980747216d9df46b95292affda160956f6 100644 (file)
@@ -388,6 +388,7 @@ nttime2unix = _glue.nttime2unix
 unix2nttime = _glue.unix2nttime
 generate_random_password = _glue.generate_random_password
 generate_random_machine_password = _glue.generate_random_machine_password
+check_password_quality = _glue.check_password_quality
 strcasecmp_m = _glue.strcasecmp_m
 strstr_m = _glue.strstr_m
 is_ntvfs_fileserver_built = _glue.is_ntvfs_fileserver_built
index e3a0e4921f26af09a14c201da20c0ef5e7f0c98b..f54b4045e933cf7e928c5374a86472f25f5fbe62 100644 (file)
@@ -84,7 +84,8 @@ from samba.dsdb import (
 
 from samba.provision import (
     provision,
-    ProvisioningError
+    ProvisioningError,
+    DEFAULT_MIN_PWD_LENGTH
     )
 
 from samba.provision.common import (
@@ -370,8 +371,9 @@ class cmd_domain_provision(Command):
 
             while True:
                 adminpassplain = getpass("Administrator password: ")
-                if not adminpassplain:
-                    self.errf.write("Invalid administrator password.\n")
+                issue = self._adminpass_issue(adminpassplain)
+                if issue:
+                    self.errf.write("%s.\n" % issue)
                 else:
                     adminpassverify = getpass("Retype password: ")
                     if not adminpassplain == adminpassverify:
@@ -387,7 +389,11 @@ class cmd_domain_provision(Command):
             if domain is None:
                 raise CommandError("No domain set!")
 
-        if not adminpass:
+        if adminpass:
+            issue = self._adminpass_issue(adminpass)
+            if issue:
+                raise CommandError(issue)
+        else:
             self.logger.info("Administrator password will be set randomly!")
 
         if function_level == "2000":
@@ -501,6 +507,20 @@ class cmd_domain_provision(Command):
 
         self.logger.warning("No nameserver found in %s" % RESOLV_CONF)
 
+    def _adminpass_issue(self, adminpass):
+        """Returns error string for a bad administrator password,
+        or None if acceptable"""
+
+        if len(adminpass.decode('utf-8')) < DEFAULT_MIN_PWD_LENGTH:
+            return "Administrator password does not meet the default minimum" \
+                " password length requirement (%d characters)" \
+                % DEFAULT_MIN_PWD_LENGTH
+        elif not samba.check_password_quality(adminpass):
+            return "Administrator password does not meet the default" \
+                " quality standards"
+        else:
+            return None
+
 
 class cmd_domain_dcpromo(Command):
     """Promote an existing domain member or NT4 PDC to an AD DC."""
index 07c24795477df471e3f88f77d82596ad981e95eb..ad1430534c2d4afd603a7005b62e76dc1591dc3a 100644 (file)
@@ -127,6 +127,8 @@ DEFAULT_DC_POLICY_GUID = "6AC1786C-016F-11D2-945F-00C04FB984F9"
 DEFAULTSITE = "Default-First-Site-Name"
 LAST_PROVISION_USN_ATTRIBUTE = "lastProvisionUSN"
 
+DEFAULT_MIN_PWD_LENGTH = 7
+
 
 class ProvisionPaths(object):
 
@@ -1309,7 +1311,8 @@ def fill_samdb(samdb, lp, names, logger, policyguid,
         "CONFIGDN": names.configdn,
         "POLICYGUID": policyguid,
         "DOMAIN_FUNCTIONALITY": str(domainFunctionality),
-        "SAMBA_VERSION_STRING": version
+        "SAMBA_VERSION_STRING": version,
+        "MIN_PWD_LENGTH": str(DEFAULT_MIN_PWD_LENGTH)
         })
 
     # If we are setting up a subdomain, then this has been replicated in, so we don't need to add it
diff --git a/python/samba/tests/password_quality.py b/python/samba/tests/password_quality.py
new file mode 100644 (file)
index 0000000..890060e
--- /dev/null
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+
+# Unix SMB/CIFS implementation.
+# Copyright (C) Catalyst IT Ltd. 2017
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+"""Tests for the python wrapper of the check_password_quality function
+"""
+
+from samba import check_password_quality
+from samba.tests import TestCase, TestCaseInTempDir
+
+class PasswordQualityTests(TestCase):
+    def test_check_password_quality(self):
+        self.assertFalse(check_password_quality(""),
+                         "empty password")
+        self.assertFalse(check_password_quality("a"),
+                         "one char password")
+        self.assertFalse(check_password_quality("aaaaaaaaaaaa"),
+                         "same char password")
+        self.assertFalse(check_password_quality("BLA"),
+                         "multiple upcases password")
+        self.assertFalse(check_password_quality("123"),
+                         "digits only")
+        self.assertFalse(check_password_quality("matthiéu"),
+                         "not enough high symbols")
+        self.assertFalse(check_password_quality("abcdééàçè"),
+                         "only lower case")
+        self.assertFalse(check_password_quality("abcdééàçè+"),
+                         "only lower and symbols")
+        self.assertTrue(check_password_quality("abcdééàçè+ढ"),
+                        "valid")
+        self.assertTrue(check_password_quality("ç+ढ"),
+                        "valid")
+        self.assertTrue(check_password_quality("A2e"),
+                        "valid")
+        self.assertTrue(check_password_quality("BA2eLi443"),
+                        "valid")
diff --git a/python/samba/tests/samba_tool/provision_password_check.py b/python/samba/tests/samba_tool/provision_password_check.py
new file mode 100644 (file)
index 0000000..b24e580
--- /dev/null
@@ -0,0 +1,56 @@
+# Unix SMB/CIFS implementation.
+# Copyright (C) Catalyst IT Ltd. 2017
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+from samba.tests.samba_tool.base import SambaToolCmdTest
+import os
+import shutil
+
+class ProvisionPasswordTestCase(SambaToolCmdTest):
+    """Test for password validation in domain provision subcommand"""
+
+    def setUp(self):
+        super(SambaToolCmdTest, self).setUp()
+        self.tempsambadir = os.path.join(self.tempdir, "samba")
+        os.mkdir(self.tempsambadir)
+
+    def _provision_with_password(self, password):
+        return self.runsubcmd(
+            "domain", "provision", "--realm=foo.example.com", "--domain=FOO",
+            "--targetdir=%s" % self.tempsambadir, "--adminpass=%s" % password,
+            "--use-ntvfs")
+
+    def test_short_and_low_quality(self):
+        (result, out, err) = self._provision_with_password("foo")
+        self.assertCmdFail(result)
+
+    def test_short(self):
+        (result, out, err) = self._provision_with_password("Fo0!_9")
+        self.assertCmdFail(result)
+        self.assertRegexpMatches(err, r"minimum password length")
+
+    def test_low_quality(self):
+        (result, out, err) = self._provision_with_password("aaaaaaaaaaaaaaaaa")
+        self.assertCmdFail(result)
+        self.assertRegexpMatches(err, r"quality standards")
+
+    def test_good(self):
+        (result, out, err) = self._provision_with_password("Fo0!_9.")
+        self.assertCmdSuccess(result, out, err)
+
+    def tearDown(self):
+        super(SambaToolCmdTest, self).tearDown()
+        shutil.rmtree(self.tempsambadir)
diff --git a/selftest/knownfail.d/validate-password-early b/selftest/knownfail.d/validate-password-early
new file mode 100644 (file)
index 0000000..76f48ac
--- /dev/null
@@ -0,0 +1 @@
+^samba4.blackbox.provision-backend.openldap-mmr-backend
\ No newline at end of file
index 209800c8ba42b58a28c37a555204ce1a0425d0e9..e65d63d8c6a55ff53aebdf121cdcb12b51ff1874 100644 (file)
@@ -66,6 +66,7 @@ planpythontestsuite("none", "samba.tests.param", py3_compatible=True)
 planpythontestsuite("none", "samba.tests.upgrade")
 planpythontestsuite("none", "samba.tests.core", py3_compatible=True)
 planpythontestsuite("none", "samba.tests.provision")
+planpythontestsuite("none", "samba.tests.password_quality")
 planpythontestsuite("none", "samba.tests.samba3")
 planpythontestsuite("none", "samba.tests.strings")
 planpythontestsuite("none", "samba.tests.netcmd")
index a9ed46f5c8ec7cf49b0ecac836a51919f780fd2b..ec9f5c0d403a28285be85b72712e2a206a1321be 100755 (executable)
@@ -609,6 +609,7 @@ planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.user_virtualCryptSHA"
 planpythontestsuite("chgdcpass:local", "samba.tests.samba_tool.user_check_password_script")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.group")
 planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.ntacl")
+planpythontestsuite("none", "samba.tests.samba_tool.provision_password_check")
 
 planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.sites")
 planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.dnscmd")
index a5e704769db780e7e65d4a5552f520a278af7e84..7d852ba33a73de5ee6c58b9de88ad81cb3cbd1c3 100644 (file)
@@ -39,7 +39,7 @@ replace: minPwdAge
 minPwdAge: -864000000000
 -
 replace: minPwdLength
-minPwdLength: 7
+minPwdLength: ${MIN_PWD_LENGTH}
 -
 replace: modifiedCount
 modifiedCount: 1