>From 7fc416d5d7392508ecdb440fdef8839201613b58 Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Thu, 17 Sep 2015 18:30:28 +1200 Subject: [PATCH 01/12] sambatool sites: PEP8/flake8 improvements We were nearly there, so lets make the jump. This involves removing some unused variables. Signed-off-by: Douglas Bagnall Reviewed-by: Andrew Bartlett --- python/samba/netcmd/sites.py | 9 +++++---- python/samba/sites.py | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/python/samba/netcmd/sites.py b/python/samba/netcmd/sites.py index 09df55e..2d802c9 100644 --- a/python/samba/netcmd/sites.py +++ b/python/samba/netcmd/sites.py @@ -16,7 +16,6 @@ # along with this program. If not, see . # -import os from samba import sites from samba.samdb import SamDB import samba.getopt as options @@ -53,14 +52,16 @@ class cmd_sites_create(Command): samdb.transaction_start() try: - ok = sites.create_site(samdb, samdb.get_config_basedn(), sitename) + sites.create_site(samdb, samdb.get_config_basedn(), sitename) samdb.transaction_commit() except sites.SiteAlreadyExistsException, e: samdb.transaction_cancel() - raise CommandError("Error while creating site %s, error: %s" % (sitename, str(e))) + raise CommandError("Error while creating site %s, error: %s" % + (sitename, str(e))) self.outf.write("Site %s created !\n" % sitename) + class cmd_sites_delete(Command): """Delete an existing site.""" @@ -86,7 +87,7 @@ class cmd_sites_delete(Command): samdb.transaction_start() try: - ok = sites.delete_site(samdb, samdb.get_config_basedn(), sitename) + sites.delete_site(samdb, samdb.get_config_basedn(), sitename) samdb.transaction_commit() except sites.SiteException, e: samdb.transaction_cancel() diff --git a/python/samba/sites.py b/python/samba/sites.py index 76c57dd..05e5340 100644 --- a/python/samba/sites.py +++ b/python/samba/sites.py @@ -94,6 +94,7 @@ def create_site(samdb, configDn, siteName): return True + def delete_site(samdb, configDn, siteName): """ Delete a site -- 2.1.4 >From a2be9b6b6c665bba445235c36029b6ff35ec93be Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Wed, 16 Sep 2015 14:17:25 +1200 Subject: [PATCH 02/12] samba-tool sites: use -H to set URL with standard handling samba-tool sites was defaulting to the local database, but we might want to use another URL. This allows that case while defaulting to the old behaviour. Signed-off-by: Douglas Bagnall Reviewed-by: Garming Sam Reviewed-by: Andrew Bartlett --- python/samba/netcmd/sites.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/python/samba/netcmd/sites.py b/python/samba/netcmd/sites.py index 2d802c9..53091a2 100644 --- a/python/samba/netcmd/sites.py +++ b/python/samba/netcmd/sites.py @@ -23,7 +23,8 @@ from samba.auth import system_session from samba.netcmd import ( Command, CommandError, - SuperCommand + SuperCommand, + Option, ) @@ -40,14 +41,16 @@ class cmd_sites_create(Command): "credopts": options.CredentialsOptions, } - def run(self, sitename, sambaopts=None, credopts=None, versionopts=None): + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + ] + + def run(self, sitename, H=None, sambaopts=None, credopts=None, + versionopts=None): lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp, fallback_machine=True) - url = lp.private_path("sam.ldb") - - if not os.path.exists(url): - raise CommandError("secret database not found at %s " % url) - samdb = SamDB(url=url, session_info=system_session(), + samdb = SamDB(url=H, session_info=system_session(), credentials=creds, lp=lp) samdb.transaction_start() @@ -75,15 +78,17 @@ class cmd_sites_delete(Command): "credopts": options.CredentialsOptions, } - def run(self, sitename, sambaopts=None, credopts=None, versionopts=None): + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + ] + + def run(self, sitename, H=None, sambaopts=None, credopts=None, + versionopts=None): lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp, fallback_machine=True) - url = lp.private_path("sam.ldb") - - if not os.path.exists(url): - raise CommandError("secret database not found at %s " % url) - samdb = SamDB(url=url, session_info=system_session(), - credentials=creds, lp=lp) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) samdb.transaction_start() try: -- 2.1.4 >From 2a0f25ccc11854dd4c0cdc8262300ca498e17f5d Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Thu, 17 Sep 2015 11:35:55 +1200 Subject: [PATCH 03/12] dsdb.tests.sites: merge interdependent tests The delete test deleted the site made by the create test, which worked because "delete" sorts after "create" alphabetically. By themselves, "delete" would fail and "create" would neglect its duty to clean up. This would be an issue if the order of tests changes, if one of the tests is not run, or if another test appears in between. Everything is fine if they give up the pretense of independence. Signed-off-by: Douglas Bagnall Reviewed-by: Garming Sam Reviewed-by: Andrew Bartlett --- source4/dsdb/tests/python/sites.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/source4/dsdb/tests/python/sites.py b/source4/dsdb/tests/python/sites.py index 402d676..4fff77e 100755 --- a/source4/dsdb/tests/python/sites.py +++ b/source4/dsdb/tests/python/sites.py @@ -81,8 +81,8 @@ class SitesBaseTests(samba.tests.TestCase): #tests on sites class SimpleSitesTests(SitesBaseTests): - def test_create(self): - """test creation of 1 site""" + def test_create_and_delete(self): + """test creation and deletion of 1 site""" self.ldb_admin.transaction_start() ok = sites.create_site(self.ldb_admin, self.ldb_admin.get_config_basedn(), @@ -93,9 +93,6 @@ class SimpleSitesTests(SitesBaseTests): sites.create_site, self.ldb_admin, self.ldb_admin.get_config_basedn(), "testsamba") - def test_delete(self): - """test removal of 1 site""" - self.ldb_admin.transaction_start() ok = sites.delete_site(self.ldb_admin, self.ldb_admin.get_config_basedn(), "testsamba") -- 2.1.4 >From c5d897b2e13cd899d09369daf1768053bfea0546 Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Thu, 17 Sep 2015 18:10:03 +1200 Subject: [PATCH 04/12] samba.sites: improve grammar in an error message Signed-off-by: Douglas Bagnall Reviewed-by: Garming Sam Reviewed-by: Andrew Bartlett --- python/samba/sites.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/samba/sites.py b/python/samba/sites.py index 05e5340..113b47f 100644 --- a/python/samba/sites.py +++ b/python/samba/sites.py @@ -114,7 +114,7 @@ def delete_site(samdb, configDn, siteName): ret = samdb.search(base=dnsites, scope=ldb.SCOPE_ONELEVEL, expression='(dn=%s)' % str(dnsite)) if len(ret) != 1: - raise SiteNotFoundException('Site %s do not exists' % siteName) + raise SiteNotFoundException('Site %s does not exist' % siteName) ret = samdb.search(base=dnserver, scope=ldb.SCOPE_ONELEVEL, expression='(objectclass=server)') -- 2.1.4 >From 0d8dfa89a6314c40d997d1d30949e71d164d0f42 Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Thu, 17 Sep 2015 18:28:09 +1200 Subject: [PATCH 05/12] dsdb.tests.sites: don't use global database, tidy long lines Signed-off-by: Douglas Bagnall Reviewed-by: Garming Sam Reviewed-by: Andrew Bartlett --- source4/dsdb/tests/python/sites.py | 45 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/source4/dsdb/tests/python/sites.py b/source4/dsdb/tests/python/sites.py index 4fff77e..912dfc4 100755 --- a/source4/dsdb/tests/python/sites.py +++ b/source4/dsdb/tests/python/sites.py @@ -32,7 +32,7 @@ from samba.samdb import SamDB import samba.tests from samba.dcerpc import security -parser = optparse.OptionParser("dirsync.py [options] ") +parser = optparse.OptionParser(__file__ + " [options] ") sambaopts = options.SambaOptions(parser) parser.add_option_group(sambaopts) parser.add_option_group(options.VersionOptions(parser)) @@ -69,10 +69,11 @@ class SitesBaseTests(samba.tests.TestCase): def setUp(self): super(SitesBaseTests, self).setUp() - self.ldb_admin = ldb - self.base_dn = ldb.domain_dn() - self.domain_sid = security.dom_sid(ldb.get_domain_sid()) - self.configuration_dn = self.ldb_admin.get_config_basedn().get_linearized() + self.ldb = SamDB(ldapshost, credentials=creds, + session_info=system_session(lp), lp=lp) + self.base_dn = self.ldb.domain_dn() + self.domain_sid = security.dom_sid(self.ldb.get_domain_sid()) + self.configuration_dn = self.ldb.get_config_basedn().get_linearized() def get_user_dn(self, name): return "CN=%s,CN=Users,%s" % (name, self.base_dn) @@ -84,34 +85,34 @@ class SimpleSitesTests(SitesBaseTests): def test_create_and_delete(self): """test creation and deletion of 1 site""" - self.ldb_admin.transaction_start() - ok = sites.create_site(self.ldb_admin, self.ldb_admin.get_config_basedn(), - "testsamba") - self.ldb_admin.transaction_commit() + self.ldb.transaction_start() + sites.create_site(self.ldb, self.ldb.get_config_basedn(), + "testsamba") + self.ldb.transaction_commit() self.assertRaises(sites.SiteAlreadyExistsException, - sites.create_site, self.ldb_admin, self.ldb_admin.get_config_basedn(), - "testsamba") + sites.create_site, self.ldb, + self.ldb.get_config_basedn(), + "testsamba") - self.ldb_admin.transaction_start() - ok = sites.delete_site(self.ldb_admin, self.ldb_admin.get_config_basedn(), - "testsamba") - - self.ldb_admin.transaction_commit() + self.ldb.transaction_start() + sites.delete_site(self.ldb, self.ldb.get_config_basedn(), + "testsamba") + self.ldb.transaction_commit() self.assertRaises(sites.SiteNotFoundException, - sites.delete_site, self.ldb_admin, self.ldb_admin.get_config_basedn(), - "testsamba") - + sites.delete_site, self.ldb, + self.ldb.get_config_basedn(), + "testsamba") def test_delete_not_empty(self): """test removal of 1 site with servers""" self.assertRaises(sites.SiteServerNotEmptyException, - sites.delete_site, self.ldb_admin, self.ldb_admin.get_config_basedn(), - "Default-First-Site-Name") + sites.delete_site, self.ldb, + self.ldb.get_config_basedn(), + "Default-First-Site-Name") -ldb = SamDB(ldapshost, credentials=creds, session_info=system_session(lp), lp=lp) TestProgram(module=__name__, opts=subunitopts) -- 2.1.4 >From 579192dc724af108d65a2ddaee7cdb78d13dde85 Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Thu, 17 Sep 2015 18:07:32 +1200 Subject: [PATCH 06/12] samba.sites: reduce code duplication in Exception classes Signed-off-by: Douglas Bagnall Reviewed-by: Garming Sam Reviewed-by: Andrew Bartlett --- python/samba/sites.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/python/samba/sites.py b/python/samba/sites.py index 113b47f..9bce998 100644 --- a/python/samba/sites.py +++ b/python/samba/sites.py @@ -28,35 +28,20 @@ class SiteException(Exception): self.value = value def __str__(self): - return "SiteException: " + self.value + return "%s: %s" % (self.__class__.__name__, self.value) class SiteNotFoundException(SiteException): """Raised when the site is not found and it's expected to exists.""" - def __init__(self, value): - self.value = value - - def __str__(self): - return "SiteNotFoundException: " + self.value class SiteAlreadyExistsException(SiteException): """Raised when the site is not found and it's expected not to exists.""" - def __init__(self, value): - self.value = value - - def __str__(self): - return "SiteAlreadyExists: " + self.value class SiteServerNotEmptyException(SiteException): """Raised when the site still has servers attached.""" - def __init__(self, value): - self.value = value - - def __str__(self): - return "SiteServerNotEmpty: " + self.value def create_site(samdb, configDn, siteName): """ -- 2.1.4 >From 33b52cee54ae8bfe431493633fb607b132f7abbe Mon Sep 17 00:00:00 2001 From: Andrew Bartlett Date: Wed, 28 Oct 2015 15:26:12 +1300 Subject: [PATCH 07/12] selftest: Allow sites test to run against a remote ldap:// host The previous code was just broken Signed-off-by: Andrew Bartlett --- source4/dsdb/tests/python/sites.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/source4/dsdb/tests/python/sites.py b/source4/dsdb/tests/python/sites.py index 912dfc4..71725bf 100755 --- a/source4/dsdb/tests/python/sites.py +++ b/source4/dsdb/tests/python/sites.py @@ -52,11 +52,8 @@ if len(args) < 1: host = args[0] if not "://" in host: ldaphost = "ldap://%s" % host - ldapshost = "ldaps://%s" % host else: ldaphost = host - start = host.rindex("://") - host = host.lstrip(start+3) lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp) @@ -69,7 +66,7 @@ class SitesBaseTests(samba.tests.TestCase): def setUp(self): super(SitesBaseTests, self).setUp() - self.ldb = SamDB(ldapshost, credentials=creds, + self.ldb = SamDB(ldaphost, credentials=creds, session_info=system_session(lp), lp=lp) self.base_dn = self.ldb.domain_dn() self.domain_sid = security.dom_sid(self.ldb.get_domain_sid()) -- 2.1.4 >From ef4b6fff3bb07984b307ce761f2400533eece85b Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Thu, 29 Oct 2015 14:54:15 +1300 Subject: [PATCH 08/12] python.sites tests: remove excessive transaction management These are atomic anyway. Signed-off-by: Douglas Bagnall Reviewed-by: Andrew Bartlett --- source4/dsdb/tests/python/sites.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/source4/dsdb/tests/python/sites.py b/source4/dsdb/tests/python/sites.py index 71725bf..f42e7bf 100755 --- a/source4/dsdb/tests/python/sites.py +++ b/source4/dsdb/tests/python/sites.py @@ -82,20 +82,16 @@ class SimpleSitesTests(SitesBaseTests): def test_create_and_delete(self): """test creation and deletion of 1 site""" - self.ldb.transaction_start() sites.create_site(self.ldb, self.ldb.get_config_basedn(), "testsamba") - self.ldb.transaction_commit() self.assertRaises(sites.SiteAlreadyExistsException, sites.create_site, self.ldb, self.ldb.get_config_basedn(), "testsamba") - self.ldb.transaction_start() sites.delete_site(self.ldb, self.ldb.get_config_basedn(), "testsamba") - self.ldb.transaction_commit() self.assertRaises(sites.SiteNotFoundException, sites.delete_site, self.ldb, -- 2.1.4 >From 10a2df4c6e57b9354b92582472b010393e544645 Mon Sep 17 00:00:00 2001 From: Andrew Bartlett Date: Thu, 29 Oct 2015 16:34:27 +1300 Subject: [PATCH 09/12] python/sites: Rework site DN construction This new routine is safe against escape characters and works against Windows 2012R2. The dn= filter in the old code was samba-specific. Andrew Bartlett Signed-off-by: Andrew Bartlett --- python/samba/sites.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/python/samba/sites.py b/python/samba/sites.py index 9bce998..7111cfa 100644 --- a/python/samba/sites.py +++ b/python/samba/sites.py @@ -18,7 +18,7 @@ """Manipulating sites.""" import ldb -from ldb import FLAG_MOD_ADD +from ldb import FLAG_MOD_ADD, LdbError class SiteException(Exception): @@ -92,17 +92,27 @@ def delete_site(samdb, configDn, siteName): :raise SiteServerNotEmpty: if the site has still servers in it. """ - dnsites = ldb.Dn(samdb, "CN=Sites,%s" % (str(configDn))) - dnsite = ldb.Dn(samdb, "Cn=%s,CN=Sites,%s" % (siteName, str(configDn))) - dnserver = ldb.Dn(samdb, "Cn=Servers,%s" % str(dnsite)) - - ret = samdb.search(base=dnsites, scope=ldb.SCOPE_ONELEVEL, - expression='(dn=%s)' % str(dnsite)) - if len(ret) != 1: - raise SiteNotFoundException('Site %s does not exist' % siteName) - - ret = samdb.search(base=dnserver, scope=ldb.SCOPE_ONELEVEL, - expression='(objectclass=server)') + dnsite = ldb.Dn(samdb, "CN=Sites") + if dnsite.add_base(configDn) == False: + raise SiteException("dnsites.add_base() failed") + if dnsite.add_child("CN=X") == False: + raise SiteException("dnsites.add_child() failed") + dnsite.set_component(0, "CN", siteName) + + dnservers = ldb.Dn(samdb, "CN=Servers") + dnservers.add_base(dnsite) + + try: + ret = samdb.search(base=dnsite, scope=ldb.SCOPE_BASE, + expression="objectClass=site") + if len(ret) != 1: + raise SiteNotFoundException('Site %s does not exist' % siteName) + except LdbError as (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + raise SiteNotFoundException('Site %s does not exist' % siteName) + + ret = samdb.search(base=dnservers, scope=ldb.SCOPE_ONELEVEL, + expression='(objectclass=server)') if len(ret) != 0: raise SiteServerNotEmptyException('Site %s still has servers in it, move them before removal' % siteName) -- 2.1.4 >From c84ed802909f85a65fa661bacbd7732ab4263467 Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Thu, 17 Sep 2015 18:16:49 +1200 Subject: [PATCH 10/12] samba-tool tests: Add command line tests for sites Signed-off-by: Douglas Bagnall Reviewed-by: Garming Sam Reviewed-by: Andrew Bartlett --- python/samba/tests/samba_tool/sites.py | 57 ++++++++++++++++++++++++++++++++++ source4/selftest/tests.py | 2 ++ 2 files changed, 59 insertions(+) create mode 100644 python/samba/tests/samba_tool/sites.py diff --git a/python/samba/tests/samba_tool/sites.py b/python/samba/tests/samba_tool/sites.py new file mode 100644 index 0000000..212df92 --- /dev/null +++ b/python/samba/tests/samba_tool/sites.py @@ -0,0 +1,57 @@ +# Unix SMB/CIFS implementation. +# Copyright (C) Catalyst.Net LTD 2015 +# Copyright (C) Sean Dague 2011 +# +# Catalyst.Net's contribution was written by Douglas Bagnall +# . +# +# 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 . +# + +import os +import ldb +from samba.tests.samba_tool.base import SambaToolCmdTest +from samba import sites + + +class BaseSitesCmdTestCase(SambaToolCmdTest): + """Tests for samba-tool sites subnets""" + def setUp(self): + super(BaseSitesCmdTestCase, self).setUp() + self.dburl = "ldap://%s" % os.environ["DC_SERVER"] + self.creds_string = "-U%s%%%s" % (os.environ["DC_USERNAME"], + os.environ["DC_PASSWORD"]) + + self.samdb = self.getSamDB("-H", self.dburl, self.creds_string) + self.config_dn = str(self.samdb.get_config_basedn()) + + +class SitesCmdTestCase(BaseSitesCmdTestCase): + + def test_site_create(self): + sitename = 'new_site' + + result, out, err = self.runsubcmd("sites", "create", sitename, + "-H", self.dburl, self.creds_string) + self.assertCmdSuccess(result) + + dnsites = ldb.Dn(self.samdb, "CN=Sites,%s" % self.config_dn) + dnsite = ldb.Dn(self.samdb, "CN=%s,%s" % (sitename, dnsites)) + + ret = self.samdb.search(base=dnsites, scope=ldb.SCOPE_ONELEVEL, + expression='(dn=%s)' % str(dnsite)) + self.assertEquals(len(ret), 1) + + # now delete it + self.samdb.delete(dnsite, ["tree_delete:0"]) diff --git a/source4/selftest/tests.py b/source4/selftest/tests.py index 1425323..8586531 100755 --- a/source4/selftest/tests.py +++ b/source4/selftest/tests.py @@ -479,6 +479,8 @@ planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.user") planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.group") planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.ntacl") +planpythontestsuite("ad_dc:local", "samba.tests.samba_tool.sites") + planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.dcerpc.rpcecho") planoldpythontestsuite("ad_dc_ntvfs:local", "samba.tests.dcerpc.registry", extra_args=['-U"$USERNAME%$PASSWORD"']) planoldpythontestsuite("ad_dc_ntvfs", "samba.tests.dcerpc.dnsserver", extra_args=['-U"$USERNAME%$PASSWORD"']) -- 2.1.4 >From 88c5055d914c950e27c14ca190c75f37fbf6a8f0 Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Wed, 28 Oct 2015 12:20:37 +1300 Subject: [PATCH 11/12] samba-tool: add sites subnet subcommands This allows you to add, remove, or shift subnets. Signed-off-by: Douglas Bagnall Signed-off-by: Andrew Bartlett Reviewed-by: Andrew Bartlett --- python/samba/netcmd/sites.py | 121 ++++++++++++++++++++- python/samba/subnets.py | 186 +++++++++++++++++++++++++++++++++ python/samba/tests/samba_tool/sites.py | 55 ++++++++++ source4/dsdb/tests/python/sites.py | 76 ++++++++++++++ 4 files changed, 436 insertions(+), 2 deletions(-) create mode 100644 python/samba/subnets.py diff --git a/python/samba/netcmd/sites.py b/python/samba/netcmd/sites.py index 53091a2..f0c792d 100644 --- a/python/samba/netcmd/sites.py +++ b/python/samba/netcmd/sites.py @@ -16,7 +16,7 @@ # along with this program. If not, see . # -from samba import sites +from samba import sites, subnets from samba.samdb import SamDB import samba.getopt as options from samba.auth import system_session @@ -102,10 +102,127 @@ class cmd_sites_delete(Command): self.outf.write("Site %s removed!\n" % sitename) +class cmd_sites_subnet_create(Command): + """Create a new subnet.""" + synopsis = "%prog [options]" + takes_args = ["subnetname", "site_of_subnet"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + ] + + def run(self, subnetname, site_of_subnet, H=None, sambaopts=None, + credopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + samdb.transaction_start() + try: + subnets.create_subnet(samdb, samdb.get_config_basedn(), subnetname, + site_of_subnet) + samdb.transaction_commit() + except subnets.SubnetException, e: + samdb.transaction_cancel() + raise CommandError("Error while creating subnet %s: %s" % + (subnetname, e)) + + self.outf.write("Subnet %s created !\n" % subnetname) + + +class cmd_sites_subnet_delete(Command): + """Delete an existing subnet.""" + + synopsis = "%prog [options]" + + takes_args = ["subnetname"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + ] + + def run(self, subnetname, H=None, sambaopts=None, credopts=None, + versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + samdb.transaction_start() + try: + subnets.delete_subnet(samdb, samdb.get_config_basedn(), subnetname) + samdb.transaction_commit() + except subnets.SubnetException, e: + samdb.transaction_cancel() + raise CommandError("Error while removing subnet %s, error: %s" % + (subnetname, e)) + + self.outf.write("Subnet %s removed!\n" % subnetname) + + +class cmd_sites_subnet_set_site(Command): + """Assign a subnet to a site.""" + synopsis = "%prog [options]" + takes_args = ["subnetname", "site_of_subnet"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + ] + + def run(self, subnetname, site_of_subnet, H=None, sambaopts=None, + credopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + samdb.transaction_start() + try: + subnets.set_subnet_site(samdb, samdb.get_config_basedn(), + subnetname, site_of_subnet) + samdb.transaction_commit() + except subnets.SubnetException, e: + samdb.transaction_cancel() + raise CommandError("Error assigning subnet %s to site %s: %s" % + (subnetname, site_of_subnet, e)) + + print >> self.outf, ("Subnet %s shifted to site %s" % + (subnet_name, site_of_subnet)) + + +class cmd_sites_subnet(SuperCommand): + """Subnet management subcommands.""" + subcommands = { + "create": cmd_sites_subnet_create(), + "remove": cmd_sites_subnet_delete(), + "set-site": cmd_sites_subnet_set_site(), + } class cmd_sites(SuperCommand): """Sites management.""" - subcommands = {} subcommands["create"] = cmd_sites_create() subcommands["remove"] = cmd_sites_delete() + subcommands["subnet"] = cmd_sites_subnet() diff --git a/python/samba/subnets.py b/python/samba/subnets.py new file mode 100644 index 0000000..e859f06 --- /dev/null +++ b/python/samba/subnets.py @@ -0,0 +1,186 @@ +# Add/remove subnets to sites. +# +# Copyright (C) Catalyst.Net Ltd 2015 +# Copyright Matthieu Patou 2011 +# +# Catalyst.Net's contribution was written by Douglas Bagnall +# . +# +# 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 . +# + +import ldb +from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, LdbError +from sites import SiteNotFoundException + +class SubnetException(Exception): + """Base element for Subnet errors""" + pass + + +class SubnetNotFound(SubnetException): + """The subnet requested does not exist.""" + pass + + +class SubnetAlreadyExists(SubnetException): + """The subnet being added already exists.""" + pass + + +class SubnetInvalid(SubnetException): + """The subnet CIDR is invalid.""" + pass + + +class SiteNotFound(SubnetException): + """The site to be used for the subnet does not exist.""" + pass + + +def create_subnet(samdb, configDn, subnet_name, site_name): + """Create a subnet and associate it with a site. + + :param samdb: A samdb connection + :param configDn: The DN of the configuration partition + :param subnet_name: name of the subnet to create (a CIDR range) + :return: None + :raise SubnetAlreadyExists: if the subnet to be created already exists. + :raise SiteNotFound: if the site does not exist. + """ + ret = samdb.search(base=configDn, scope=ldb.SCOPE_SUBTREE, + expression='(&(objectclass=Site)(cn=%s))' % + ldb.binary_encode(site_name)) + if len(ret) != 1: + raise SiteNotFound('A site with the name %s does not exist' % + site_name) + dn_site = ret[0].dn + + if not isinstance(subnet_name, str): + raise SubnetInvalid("%s is not a valid subnet (not a string)" % subnet_name) + + dnsubnet = ldb.Dn(samdb, "CN=Subnets,CN=Sites") + if dnsubnet.add_base(configDn) == False: + raise SubnetException("dnsubnet.add_base() failed") + if dnsubnet.add_child("CN=X") == False: + raise SubnetException("dnsubnet.add_child() failed") + dnsubnet.set_component(0, "CN", subnet_name) + + try: + m = ldb.Message() + m.dn = dnsubnet + m["objectclass"] = ldb.MessageElement("subnet", FLAG_MOD_ADD, + "objectclass") + m["siteObject"] = ldb.MessageElement(str(dn_site), FLAG_MOD_ADD, + "siteObject") + samdb.add(m) + except ldb.LdbError as (enum, estr): + if enum == ldb.ERR_INVALID_DN_SYNTAX: + raise SubnetInvalid("%s is not a valid subnet: %s" % (subnet_name, estr)) + elif enum == ldb.ERR_ENTRY_ALREADY_EXISTS: + # Subnet collisions are checked by exact match only, not + # overlapping range. This won't stop you creating + # 10.1.1.0/24 when there is already 10.1.0.0/16, or + # prevent you from having numerous IPv6 subnets that refer + # to the same range (e.g 5::0/16, 5::/16, 5:0:0::/16). + raise SubnetAlreadyExists('A subnet with the CIDR %s already exists' + % subnet_name) + else: + raise + + +def delete_subnet(samdb, configDn, subnet_name): + """Delete a subnet. + + :param samdb: A samdb connection + :param configDn: The DN of the configuration partition + :param subnet_name: Name of the subnet to delete + :return: None + :raise SubnetNotFound: if the subnet to be deleted does not exist. + """ + dnsubnet = ldb.Dn(samdb, "CN=Subnets,CN=Sites") + if dnsubnet.add_base(configDn) == False: + raise SubnetException("dnsubnet.add_base() failed") + if dnsubnet.add_child("CN=X") == False: + raise SubnetException("dnsubnet.add_child() failed") + dnsubnet.set_component(0, "CN", subnet_name) + + try: + ret = samdb.search(base=dnsubnet, scope=ldb.SCOPE_BASE, + expression="objectClass=subnet") + if len(ret) != 1: + raise SubnetNotFound('Subnet %s does not exist' % subnet_name) + except LdbError as (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + raise SubnetNotFound('Subnet %s does not exist' % subnet_name) + + samdb.delete(dnsubnet) + + +def set_subnet_site(samdb, configDn, subnet_name, site_name): + """Assign a subnet to a site. + + This dissociates the subnet from its previous site. + + :param samdb: A samdb connection + :param configDn: The DN of the configuration partition + :param subnet_name: Name of the subnet + :param site_name: Name of the site + :return: None + :raise SubnetNotFound: if the subnet does not exist. + :raise SiteNotFound: if the site does not exist. + """ + + dnsubnet = ldb.Dn(samdb, "CN=Subnets,CN=Sites") + if dnsubnet.add_base(configDn) == False: + raise SubnetException("dnsubnet.add_base() failed") + if dnsubnet.add_child("CN=X") == False: + raise SubnetException("dnsubnet.add_child() failed") + dnsubnet.set_component(0, "CN", subnet_name) + + try: + ret = samdb.search(base=dnsubnet, scope=ldb.SCOPE_BASE, + expression="objectClass=subnet") + if len(ret) != 1: + raise SubnetNotFound('Subnet %s does not exist' % subnet_name) + except LdbError as (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + raise SubnetNotFound('Subnet %s does not exist' % subnet_name) + + dnsite = ldb.Dn(samdb, "CN=Sites") + if dnsite.add_base(configDn) == False: + raise SubnetException("dnsites.add_base() failed") + if dnsite.add_child("CN=X") == False: + raise SubnetException("dnsites.add_child() failed") + dnsite.set_component(0, "CN", site_name) + + dnservers = ldb.Dn(samdb, "CN=Servers") + dnservers.add_base(dnsite) + + try: + ret = samdb.search(base=dnsite, scope=ldb.SCOPE_BASE, + expression="objectClass=site") + if len(ret) != 1: + raise SiteNotFoundException('Site %s does not exist' % site_name) + except LdbError as (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + raise SiteNotFoundException('Site %s does not exist' % site_name) + + siteDn = str(ret[0].dn) + + m = ldb.Message() + m.dn = dnsubnet + m["siteObject"] = ldb.MessageElement(siteDn, FLAG_MOD_REPLACE, + "siteObject") + samdb.modify(m) diff --git a/python/samba/tests/samba_tool/sites.py b/python/samba/tests/samba_tool/sites.py index 212df92..81cc66d 100644 --- a/python/samba/tests/samba_tool/sites.py +++ b/python/samba/tests/samba_tool/sites.py @@ -55,3 +55,58 @@ class SitesCmdTestCase(BaseSitesCmdTestCase): # now delete it self.samdb.delete(dnsite, ["tree_delete:0"]) + + +class SitesSubnetCmdTestCase(BaseSitesCmdTestCase): + def setUp(self): + super(SitesSubnetCmdTestCase, self).setUp() + self.sitename = "testsite" + self.sitename2 = "testsite2" + self.samdb.transaction_start() + sites.create_site(self.samdb, self.config_dn, self.sitename) + sites.create_site(self.samdb, self.config_dn, self.sitename2) + self.samdb.transaction_commit() + + def tearDown(self): + self.samdb.transaction_start() + sites.delete_site(self.samdb, self.config_dn, self.sitename) + sites.delete_site(self.samdb, self.config_dn, self.sitename2) + self.samdb.transaction_commit() + super(SitesSubnetCmdTestCase, self).tearDown() + + def test_site_subnet_create(self): + cidrs = (("10.9.8.0/24", self.sitename), + ("50.60.0.0/16", self.sitename2), + ("50.61.0.0/16", self.sitename2), # second subnet on the site + ("50.0.0.0/8", self.sitename), # overlapping subnet, other site + ("50.62.1.2/32", self.sitename), # single IP + ("aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/120", + self.sitename2), + ) + + for cidr, sitename in cidrs: + result, out, err = self.runsubcmd("sites", "subnet", "create", + cidr, sitename, + "-H", self.dburl, + self.creds_string) + self.assertCmdSuccess(result) + + ret = self.samdb.search(base=self.config_dn, + scope=ldb.SCOPE_SUBTREE, + expression=('(&(objectclass=subnet)(cn=%s))' + % cidr)) + self.assertIsNotNone(ret) + self.assertEqual(len(ret), 1) + + dnsubnets = ldb.Dn(self.samdb, + "CN=Subnets,CN=Sites,%s" % self.config_dn) + + for cidr, sitename in cidrs: + dnsubnet = ldb.Dn(self.samdb, ("Cn=%s,CN=Subnets,CN=Sites,%s" % + (cidr, self.config_dn))) + + ret = self.samdb.search(base=dnsubnets, scope=ldb.SCOPE_ONELEVEL, + expression='(dn=%s)' % dnsubnet) + self.assertIsNotNone(ret) + self.assertEqual(len(ret), 1) + self.samdb.delete(dnsubnet, ["tree_delete:0"]) diff --git a/source4/dsdb/tests/python/sites.py b/source4/dsdb/tests/python/sites.py index f42e7bf..6242a9d 100755 --- a/source4/dsdb/tests/python/sites.py +++ b/source4/dsdb/tests/python/sites.py @@ -27,10 +27,12 @@ from samba.tests.subunitrun import TestProgram, SubunitOptions import samba.getopt as options from samba import sites +from samba import subnets from samba.auth import system_session from samba.samdb import SamDB import samba.tests from samba.dcerpc import security +from ldb import SCOPE_SUBTREE parser = optparse.OptionParser(__file__ + " [options] ") sambaopts = options.SambaOptions(parser) @@ -107,5 +109,79 @@ class SimpleSitesTests(SitesBaseTests): "Default-First-Site-Name") +# tests for subnets +class SimpleSubnetTests(SitesBaseTests): + + def setUp(self): + super(SimpleSubnetTests, self).setUp() + self.basedn = self.ldb.get_config_basedn() + self.sitename = "testsite" + self.sitename2 = "testsite2" + self.ldb.transaction_start() + sites.create_site(self.ldb, self.basedn, self.sitename) + sites.create_site(self.ldb, self.basedn, self.sitename2) + self.ldb.transaction_commit() + + def tearDown(self): + self.ldb.transaction_start() + sites.delete_site(self.ldb, self.basedn, self.sitename) + sites.delete_site(self.ldb, self.basedn, self.sitename2) + self.ldb.transaction_commit() + super(SimpleSubnetTests, self).tearDown() + + def test_create_delete(self): + """Create a subnet and delete it again.""" + basedn = self.ldb.get_config_basedn() + cidr = "10.11.12.0/24" + + subnets.create_subnet(self.ldb, basedn, cidr, self.sitename) + + self.assertRaises(subnets.SubnetAlreadyExists, + subnets.create_subnet, self.ldb, basedn, cidr, + self.sitename) + + subnets.delete_subnet(self.ldb, basedn, cidr) + + ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE, + expression='(&(objectclass=subnet)(cn=%s))' % cidr) + + self.assertEqual(len(ret), 0, 'Failed to delete subnet %s' % cidr) + + def test_create_shift_delete(self): + """Create a subnet, shift it to another site, then delete it.""" + basedn = self.ldb.get_config_basedn() + cidr = "10.11.12.0/24" + + subnets.create_subnet(self.ldb, basedn, cidr, self.sitename) + + subnets.set_subnet_site(self.ldb, basedn, cidr, self.sitename2) + + ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE, + expression='(&(objectclass=subnet)(cn=%s))' % cidr) + + sites = ret[0]['siteObject'] + self.assertEqual(len(sites), 1) + self.assertEqual(sites[0], + 'CN=testsite2,CN=Sites,%s' % self.ldb.get_config_basedn()) + + self.assertRaises(subnets.SubnetAlreadyExists, + subnets.create_subnet, self.ldb, basedn, cidr, + self.sitename) + + subnets.delete_subnet(self.ldb, basedn, cidr) + + ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE, + expression='(&(objectclass=subnet)(cn=%s))' % cidr) + + self.assertEqual(len(ret), 0, 'Failed to delete subnet %s' % cidr) + + def test_delete_subnet_that_does_not_exist(self): + """Ensure we can't delete a site that isn't there.""" + basedn = self.ldb.get_config_basedn() + cidr = "10.15.0.0/16" + + self.assertRaises(subnets.SubnetNotFound, + subnets.delete_subnet, self.ldb, basedn, cidr) + TestProgram(module=__name__, opts=subunitopts) -- 2.1.4 >From 9e4e4b5cd2caee91f36d6588ea1efbde7e68105e Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Wed, 23 Sep 2015 15:10:56 +1200 Subject: [PATCH 12/12] samldb: ensure subnets have proper net ranges A subnet name needs to be a valid CIDR address range -- that's the ones that look like 10.9.8.0/22, where the number after the / determines how many bits are in the address suffix. It can be IPv4 or IPv6. There are a few odd constraints (see MS-ADTS v20150630 6.1.1.2.2.2.1 "Subnet Object") -- for example, with IPv4, the implied bit mask can't equal the address. That is, you can't have a subnet named "255.255.255.0/24" in a Windows subnet. This rule does not apply to IPv6. Windows and Samba both make some ensure that subnets have a unique valid name, though unfortunately Windows 2008R2 is rather slack when it comes to IPv6. We follow Windows 2012R2, which roughly follows RFC5952 -- with one caveat: Windows will allow an address like "::ffff:0:1:2", which translates to the IPv4 address "0.1.0.2" using the SIIT translation scheme, and which inet_ntop() would render as "::ffff:0:0.1.0.2". In the Samba implementation we use an inet_pton()/ inet_ntop() round-trip to establish canonicality, so these addresses fail. Windows wisely does not allow the SIIT style addresses (the acronym is widely agreed to be off-by-one in the second letter), and it will regard "::ffff:0:1:2" as simply "::ffff:0:1:2" and allow it. We would like to do that too. Signed-off-by: Douglas Bagnall --- python/samba/tests/samba_tool/sites.py | 24 +++ source4/dsdb/samdb/ldb_modules/samldb.c | 257 ++++++++++++++++++++++++++++ source4/dsdb/tests/python/sites.py | 288 ++++++++++++++++++++++++++++++++ 3 files changed, 569 insertions(+) diff --git a/python/samba/tests/samba_tool/sites.py b/python/samba/tests/samba_tool/sites.py index 81cc66d..ee28109 100644 --- a/python/samba/tests/samba_tool/sites.py +++ b/python/samba/tests/samba_tool/sites.py @@ -110,3 +110,27 @@ class SitesSubnetCmdTestCase(BaseSitesCmdTestCase): self.assertIsNotNone(ret) self.assertEqual(len(ret), 1) self.samdb.delete(dnsubnet, ["tree_delete:0"]) + + def test_site_subnet_create_should_fail(self): + cidrs = (("10.9.8.0/33", self.sitename), # mask too big + ("50.60.0.0/8", self.sitename2), # insufficient zeros + ("50.261.0.0/16", self.sitename2), # bad octet + ("7.0.0.0.0/0", self.sitename), # insufficient zeros + ("aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/119", + self.sitename), # insufficient zeros + ) + + for cidr, sitename in cidrs: + result, out, err = self.runsubcmd("sites", "subnet", "create", + cidr, sitename, + "-H", self.dburl, + self.creds_string) + self.assertCmdFail(result) + + ret = self.samdb.search(base=self.config_dn, + scope=ldb.SCOPE_SUBTREE, + expression=('(&(objectclass=subnet)(cn=%s))' + % cidr)) + + self.assertIsNotNone(ret) + self.assertEqual(len(ret), 0) diff --git a/source4/dsdb/samdb/ldb_modules/samldb.c b/source4/dsdb/samdb/ldb_modules/samldb.c index 153c85e..194ad41 100644 --- a/source4/dsdb/samdb/ldb_modules/samldb.c +++ b/source4/dsdb/samdb/ldb_modules/samldb.c @@ -42,6 +42,7 @@ #include "ldb_wrap.h" #include "param/param.h" #include "libds/common/flag_mapping.h" +#include "system/network.h" struct samldb_ctx; enum samldb_add_type { @@ -2644,6 +2645,242 @@ static int samldb_fsmo_role_owner_check(struct samldb_ctx *ac) return LDB_SUCCESS; } +/* + * Return zero if the number of zero bits in the address (looking from low to + * high) is equal to or greater than the length minus the mask. Otherwise it + * returns -1. + */ +static int check_cidr_zero_bits(uint8_t *address, unsigned int len, + unsigned int mask) +{ + /*
is an integer in big-endian form, bits long. All + bits between and must be zero. */ + int i; + unsigned int byte_len; + unsigned int byte_mask; + unsigned int bit_mask; + if (len == 32) { + DBG_INFO("Looking at address %02x%02x%02x%02x, mask %u\n", + address[0], address[1], address[2], address[3], + mask); + } else if (len == 128){ + DBG_INFO("Looking at address " + "%02x%02x-%02x%02x-%02x%02x-%02x%02x-" + "%02x%02x-%02x%02x-%02x%02x-%02x%02x, mask %u\n", + address[0], address[1], address[2], address[3], + address[4], address[5], address[6], address[7], + address[8], address[9], address[10], address[11], + address[12], address[13], address[14], address[15], + mask); + } + + if (mask > len){ + DBG_INFO("mask %u is too big (> %u)\n", mask, len); + return -1; + } + if (mask == len){ + /* single address subnet. + * In IPv4 all 255s is invalid by the bitmask != address rule + * in MS-ADTS. IPv6 does not suffer. + */ + if (len == 32){ + if (address[0] == 255 && + address[1] == 255 && + address[2] == 255 && + address[3] == 255){ + return -1; + } + } + return 0; + } + + byte_len = len / 8; + byte_mask = mask / 8; + + for (i = byte_len - 1; i > byte_mask; i--){ + DBG_DEBUG("checking byte %d %02x\n", i, address[i]); + if (address[i] != 0){ + return -1; + } + } + bit_mask = (1 << (8 - (mask & 7))) - 1; + DBG_DEBUG("checking bitmask %02x & %02x overlap %02x\n", bit_mask, address[byte_mask], + bit_mask & address[byte_mask]); + if (address[byte_mask] & bit_mask){ + return -1; + } + + /* According to MS-ADTS, the mask can't exactly equal the bitmask for + * IPv4 (but this is fine for v6). That is 255.255.80.0/17 is bad, + * because the bitmask implied by "/17" is 255.255.80.0. + * + * The bit_mask used in the previous check is the complement of what + * we want here. + */ + if (len == 32 && address[byte_mask] == (uint8_t)~bit_mask){ + bool ok = false; + for (i = 0; i < byte_mask; i++){ + if (address[i] != 255){ + ok = true; + break; + } + } + if (ok == false){ + return -1; + } + } + return 0; +} + + + +static int check_address_roundtrip(const char *address, int family, + const uint8_t *address_bytes, + char *buffer, int buffer_len) +{ + /* + * Check that the address is in the canonical RFC5952 format for IPv6, + * and lacks extra leading zeros for each dotted decimal for IPv4. + * Handily this is what inet_ntop() gives you. + */ + const char *address_redux = inet_ntop(family, address_bytes, + buffer, buffer_len); + if (address_redux == NULL){ + DBG_INFO("Address round trip %s failed unexpectedly" + " with errno %d\n", address, errno); + return -1; + } + if (strcasecmp(address, address_redux) != 0){ + DBG_INFO("Address %s round trips to %s; fail!\n", + address, address_redux); + return -1; + } + return 0; +} + + + +/* + * MS-ADTS v20150630 6.1.1.2.2.2.1 Subnet Object, refers to RFC1166 and + * RFC2373. It specifies something seemingly indistinguishable from an RFC4632 + * CIDR address range without saying so explicitly. Here we follow the CIDR + * spec. + * + * Return 0 on success, -1 on error. + */ +static int verify_cidr(const char *cidr) +{ + char *address = NULL, *slash = NULL, *endptr = NULL; + bool has_colon, has_dot; + int res, ret; + unsigned long mask; + uint8_t *address_bytes = NULL; + char *address_redux = NULL; + unsigned int address_len; + TALLOC_CTX *frame = NULL; + + DBG_DEBUG("CIDR is %s\n", cidr); + frame = talloc_stackframe(); + address = talloc_strdup(frame, cidr); + if (address == NULL){ + goto error; + } + + /* there must be a '/' */ + slash = strchr(address, '/'); + if (slash == NULL){ + goto error; + } + /* terminate the address for strchr, inet_pton */ + *slash = '\0'; + + mask = strtoul(slash + 1, &endptr, 10); + if (*endptr != '\0' || endptr == slash + 1){ + DBG_INFO("CIDR mask is not a proper integer: %s\n", cidr); + goto error; + } + + address_bytes = talloc_size(frame, sizeof(struct in6_addr)); + if (address_bytes == NULL){ + goto error; + } + + address_redux = talloc_size(frame, INET6_ADDRSTRLEN); + if (address_redux == NULL){ + goto error; + } + + DBG_INFO("found address %s, mask %lu\n", address, mask); + has_colon = (strchr(address, ':') == NULL) ? false : true; + has_dot = (strchr(address, '.') == NULL) ? false : true; + if (has_dot && has_colon){ + /* This seems to be an IPv4 address embedded in IPv6, which is + icky. We don't support it. */ + DBG_INFO("Refusing to consider cidr '%s' with dots and colons\n", + cidr); + goto error; + } else if (has_colon){ /* looks like IPv6 */ + res = inet_pton(AF_INET6, address, address_bytes); + if (res != 1) { + DBG_INFO("Address in %s fails to parse as IPv6\n", cidr); + goto error; + } + address_len = 128; + if (check_address_roundtrip(address, AF_INET6, address_bytes, + address_redux, INET6_ADDRSTRLEN)){ + goto error; + } + } else if (has_dot) { + /* looks like IPv4 */ + res = inet_pton(AF_INET, address, address_bytes); + if (res != 1) { + DBG_INFO("Address in %s fails to parse as IPv4\n", cidr); + goto error; + } + address_len = 32; + + if (check_address_roundtrip(address, AF_INET, address_bytes, + address_redux, INET_ADDRSTRLEN)){ + goto error; + } + } else { + /* This doesn't look like an IP address at all. */ + goto error; + } + + ret = check_cidr_zero_bits(address_bytes, address_len, mask); + talloc_free(frame); + return ret; + error: + talloc_free(frame); + return -1; +} + + +static int samldb_verify_subnet(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + const char *cidr = NULL; + const struct ldb_val *rdn_value = NULL; + + rdn_value = ldb_dn_get_rdn_val(ac->msg->dn); + cidr = ldb_dn_escape_value(ac, *rdn_value); + DBG_INFO("looking at cidr '%s'\n", cidr); + if (cidr == NULL) { + ldb_set_errstring(ldb, + "samldb: adding an empty subnet cidr seems wrong"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + if (verify_cidr(cidr)){ + ldb_set_errstring(ldb, + "samldb: subnet value is invalid"); + return LDB_ERR_INVALID_DN_SYNTAX; + } + + return LDB_SUCCESS; +} + /* add */ static int samldb_add(struct ldb_module *module, struct ldb_request *req) @@ -2752,6 +2989,17 @@ static int samldb_add(struct ldb_module *module, struct ldb_request *req) return samldb_fill_object(ac); } + if (samdb_find_attribute(ldb, ac->msg, + "objectclass", "subnet") != NULL) { + ret = samldb_verify_subnet(ac); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + /* We are just checking the value is valid, and there are no + values to fill in. */ + } + talloc_free(ac); /* nothing matched, go on */ @@ -3099,6 +3347,15 @@ static int check_rename_constraints(struct ldb_message *msg, return LDB_ERR_UNWILLING_TO_PERFORM; } + /* subnet objects */ + if (samdb_find_attribute(ldb, msg, "objectclass", "subnet") != NULL) { + ret = samldb_verify_subnet(ac); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } + /* systemFlags */ dn1 = ldb_dn_get_parent(ac, olddn); diff --git a/source4/dsdb/tests/python/sites.py b/source4/dsdb/tests/python/sites.py index 6242a9d..0fbb768 100755 --- a/source4/dsdb/tests/python/sites.py +++ b/source4/dsdb/tests/python/sites.py @@ -183,5 +183,293 @@ class SimpleSubnetTests(SitesBaseTests): self.assertRaises(subnets.SubnetNotFound, subnets.delete_subnet, self.ldb, basedn, cidr) + def test_create_bad_ranges(self): + """These CIDR ranges all have something wrong with them, and they + should all fail.""" + basedn = self.ldb.get_config_basedn() + + cidrs = [ + # IPv4 + # insufficient zeros + "10.11.12.0/14", + "110.0.0.0/6", + "1.0.0.0/0", + "10.11.13.1/24", + "1.2.3.4/29", + "10.11.12.0/21", + # out of range mask + "110.0.0.0/33", + "110.0.0.0/-1", + "4.0.0.0/111", + # out of range address + "310.0.0.0/24", + "10.0.0.256/32", + "1.1.-20.0/24", + # badly formed + "1.0.0.0/1e", + "1.0.0.0/24.0", + "1.0.0.0/1/1", + "1.0.0.0", + "1.c.0.0/24", + "1.2.0.0.0/27", + "1.23.0/24", + "1.23.0.-7/24", + "1.-23.0.7/24", + "1.23.-0.7/24", + "1.23.0.0/0x10", + # IPv6 insufficient zeros -- this could be a subtle one + # due to the vagaries of endianness in the 16 bit groups. + "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/119", + "aaaa:bbbb::/31", + "a:b::/31", + "c000::/1", + "a::b00/119", + "1::1/127", + "1::2/126", + "1::100/119", + "1::8000/112", + # out of range mask + "a:b::/130", + "a:b::/-1", + "::/129", + # An IPv4 address can't be exactly the bitmask (MS ADTS) + "128.0.0.0/1", + "192.0.0.0/2", + "255.192.0.0/10", + "255.255.255.0/24", + "255.255.255.255/32", + "0.0.0.0/0", + # The address can't have leading zeros (not RFC 4632, but MS ADTS) + "00.1.2.0/24", + "003.1.2.0/24", + "022.1.0.0/16", + "00000000000000000000000003.1.2.0/24", + "09876::abfc/126", + "0aaaa:bbbb::/32", + "009876::abfc/126", + "000a:bbbb::/32", + + # How about extraneous zeros later on + "3.01.2.0/24", + "3.1.2.00/24", + "22.001.0.0/16", + "3.01.02.0/24", + "100a:0bbb:0023::/48", + "100a::0023/128", + + # Windows doesn't like the zero IPv4 address + "0.0.0.0/8", + # or the zero mask on IPv6 + "::/0", + + # various violations of RFC5952 + "0:0:0:0:0:0:0:0/8", + "0::0/0", + "::0:0/48", + "::0:4/128", + "0::/8", + "0::4f/128", + "0::42:0:0:0:0/64", + "4f::0/48", + + # badly formed -- mostly the wrong arrangement of colons + "a::b::0/120", + "a::abcdf:0/120", + "a::g:0/120", + "::0::3/48", + "2001:3::110::3/118", + "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1111:0000/128", + "a:::5:0/120", + + # non-canonical representations (vs RFC 5952) + # "2001:0:c633:63::1:0/120" is correct + "2001:0:c633:63:0:0:1:0/120", + "2001::c633:63:0:0:1:0/120", + "2001:0:c633:63:0:0:1::/120", + + # "10:0:0:42::/64" is correct + "10::42:0:0:0:0/64", + "10:0:0:42:0:0:0:0/64", + + # "1::4:5:0:0:8/127" is correct + "1:0:0:4:5:0:0:8/127", + "1:0:0:4:5::8/127", + + # "2001:db8:0:1:1:1:1:1/128" is correct + "2001:db8::1:1:1:1:1/128", + + # IP4 embedded - rejected + "a::10.0.0.0/120", + "a::10.9.8.7/128", + # completely wrong + None, + "bob", + 3.1415, + False, + "10.11.16.0/24\x00hidden bytes past a zero", + self, + ] + + failures = [] + for cidr in cidrs: + try: + subnets.create_subnet(self.ldb, basedn, cidr, self.sitename) + except subnets.SubnetInvalid: + print >> sys.stderr, "%s fails properly" % (cidr,) + continue + + # we are here because it succeeded when it shouldn't have. + print >> sys.stderr, "CIDR %s fails to fail" % (cidr,) + failures.append(cidr) + subnets.delete_subnet(self.ldb, basedn, cidr) + + if failures: + print "These bad subnet names were accepted:" + for cidr in failures: + print " \033[01;34m%s\033[00m" % cidr + self.fail() + + def test_create_good_ranges(self): + """All of these CIDRs are good, and the subnet creation should + succeed.""" + basedn = self.ldb.get_config_basedn() + + cidrs = [ + # IPv4 + "10.11.12.0/24", + "10.11.12.0/23", + "10.11.12.0/25", + "110.0.0.0/7", + "1.0.0.0/32", + "10.11.13.0/32", + "10.11.13.1/32", + "99.0.97.0/24", + "1.2.3.4/30", + "10.11.12.0/22", + "0.12.13.0/24", + # IPv6 + "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/120", + "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:11f0/124", + "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:11fc/126", + # don't forget upper case + "FFFF:FFFF:FFFF:FFFF:ABCD:EfFF:FFFF:FFeF/128", + "9876::ab00/120", + "9876::abf0/124", + "9876::abfc/126", + "aaaa:bbbb::/32", + "aaaa:bbba::/31", + "aaaa:ba00::/23", + "aaaa:bb00::/24", + "aaaa:bb00::/77", + "::/48", + "a:b::/32", + "c000::/2", + "a::b00/120", + "1::2/127", + # this pattern of address suffix == mask is forbidden with + # IPv4 but OK for IPv6. + "8000::/1", + "c000::/2", + "ffff:ffff:ffc0::/42", + "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF/128", + # leading zeros are forbidden, but implicit IPv6 zeros + # (via "::") are OK. + "::1000/116", + "::8000/113", + # taken to the logical conclusion, "::/0" should be OK, but no. + "::/48", + + # Try some reserved ranges, which it might be reasonable + # to exclude, but which are not excluded in practice. + "129.0.0.0/16", + "129.255.0.0/16", + "100.64.0.0/10", + "127.0.0.0/8", + "127.0.0.0/24", + "169.254.0.0/16", + "169.254.1.0/24", + "192.0.0.0/24", + "192.0.2.0/24", + "198.18.0.0/15", + "198.51.100.0/24", + "203.0.113.0/24", + "224.0.0.0/4", + "130.129.0.0/16", + "130.255.0.0/16", + "192.12.0.0/24", + "223.255.255.0/24", + "240.255.255.0/24", + "224.0.0.0/8", + "::ffff:0:0/96", + "::/96", + "100::/64", + "2001:10::/28", + "fec0::/10", + "ff00::/8", + "::1/128", + "2001:db8::/32", + "2001:10::/28", + "2002::/24", + "2002:a00::/24", + "2002:7f00::/24", + "2002:a9fe::/32", + "2002:ac10::/28", + "2002:c000::/40", + "2002:c000:200::/40", + "2002:c0a8::/32", + "2002:c612::/31", + "2002:c633:6400::/40", + "2002:cb00:7100::/40", + "2002:e000::/20", + "2002:f000::/20", + "2002:ffff:ffff::/48", + "2001::/40", + "2001:0:a00::/40", + "2001:0:7f00::/40", + "2001:0:a9fe::/48", + "2001:0:ac10::/44", + "2001:0:c000::/56", + "2001:0:c000:200::/56", + "2001:0:c0a8::/48", + "2001:0:c612::/47", + "2001:0:c633:6400::/56", + "2001:0:cb00:7100::/56", + "2001:0:e000::/36", + "2001:0:f000::/36", + "2001:0:ffff:ffff::/64", + + # non-RFC-5952 versions of these are tested in create_bad_ranges + "2001:0:c633:63::1:0/120", + "10:0:0:42::/64", + "1::4:5:0:0:8/127", + "2001:db8:0:1:1:1:1:1/128", + ] + failures = [] + + for cidr in cidrs: + try: + subnets.create_subnet(self.ldb, basedn, cidr, self.sitename) + except subnets.SubnetInvalid, e: + print e + failures.append(cidr) + continue + + ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE, + expression=('(&(objectclass=subnet)(cn=%s))' % + cidr)) + + if len(ret) != 1: + print "%s was not created" % cidr + failures.append(cidr) + continue + subnets.delete_subnet(self.ldb, basedn, cidr) + + if failures: + print "These good subnet names were not accepted:" + for cidr in failures: + print " \033[01;34m%s\033[00m" % cidr + self.fail() + + TestProgram(module=__name__, opts=subunitopts) -- 2.1.4