diff --git a/python/pyglue.c b/python/pyglue.c index 07cde49e16b..d928b4cda41 100644 --- a/python/pyglue.c +++ b/python/pyglue.c @@ -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, diff --git a/python/samba/__init__.py b/python/samba/__init__.py index 6f79b3cc960..6ba7c998074 100644 --- a/python/samba/__init__.py +++ b/python/samba/__init__.py @@ -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 diff --git a/python/samba/netcmd/domain.py b/python/samba/netcmd/domain.py index e3a0e4921f2..f54b4045e93 100644 --- a/python/samba/netcmd/domain.py +++ b/python/samba/netcmd/domain.py @@ -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.""" diff --git a/python/samba/provision/__init__.py b/python/samba/provision/__init__.py index 07c24795477..ad1430534c2 100644 --- a/python/samba/provision/__init__.py +++ b/python/samba/provision/__init__.py @@ -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 index 00000000000..890060e2796 --- /dev/null +++ b/python/samba/tests/password_quality.py @@ -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 . +# + +"""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 index 00000000000..b24e5804072 --- /dev/null +++ b/python/samba/tests/samba_tool/provision_password_check.py @@ -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 . +# + +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 index 00000000000..76f48aca260 --- /dev/null +++ b/selftest/knownfail.d/validate-password-early @@ -0,0 +1 @@ +^samba4.blackbox.provision-backend.openldap-mmr-backend \ No newline at end of file diff --git a/selftest/tests.py b/selftest/tests.py index 209800c8ba4..e65d63d8c6a 100644 --- a/selftest/tests.py +++ b/selftest/tests.py @@ -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") diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py index a9ed46f5c8e..ec9f5c0d403 100755 --- a/source4/selftest/tests.py +++ b/source4/selftest/tests.py @@ -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") diff --git a/source4/setup/provision_basedn_modify.ldif b/source4/setup/provision_basedn_modify.ldif index a5e704769db..7d852ba33a7 100644 --- a/source4/setup/provision_basedn_modify.ldif +++ b/source4/setup/provision_basedn_modify.ldif @@ -39,7 +39,7 @@ replace: minPwdAge minPwdAge: -864000000000 - replace: minPwdLength -minPwdLength: 7 +minPwdLength: ${MIN_PWD_LENGTH} - replace: modifiedCount modifiedCount: 1