# -*- coding: utf-8 -*- # # Tests a corner-case involving the fromServer attribute, which is slightly # unique: it's an Object(DS-DN) (like a one-way link), but it is also a # mandatory attribute (for nTDSConnection). The corner-case is that the # fromServer can potentially end up pointing to a non-existent object. # This can happen with other one-way links, but these other one-way links # are not mandatory attributes. # # Copyright (C) Andrew Bartlett 2018 # # 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 optparse import sys sys.path.insert(0, "bin/python") import samba import os import time import ldb import samba.tests from samba.tests.subunitrun import TestProgram, SubunitOptions from samba.dcerpc import misc from samba.provision import DEFAULTSITE # note we must connect to the local ldb file on disk, in order to # add system-only nTDSDSA objects parser = optparse.OptionParser("attr_from_server.py ") subunitopts = SubunitOptions(parser) parser.add_option_group(subunitopts) opts, args = parser.parse_args() if len(args) < 1: parser.print_usage() sys.exit(1) ldb_path = args[0] class FromServerAttrTest(samba.tests.TestCase): def setUp(self): super(FromServerAttrTest, self).setUp() self.ldb = samba.tests.connect_samdb(ldb_path) def tearDown(self): super(FromServerAttrTest, self).tearDown() def set_attribute(self, dn, attr, value, operation=ldb.FLAG_MOD_ADD): """Modifies an attribute for an object""" m = ldb.Message() m.dn = ldb.Dn(self.ldb, dn) m[attr] = ldb.MessageElement(value, operation, attr) self.ldb.modify(m) def get_object_guid(self, dn): res = self.ldb.search(base=dn, attrs=["objectGUID"], scope=ldb.SCOPE_BASE) self.assertTrue(len(res) == 1) return str(misc.GUID(res[0]['objectGUID'][0])) def test_dangling_server_attr(self): """ Tests a scenario where an object has a fromServer attribute that points to an object that no longer exists. """ # add a temporary server and its associated NTDS Settings object config_dn = self.ldb.get_config_basedn() sites_dn = "CN=Sites,{0}".format(config_dn) servers_dn = "CN=Servers,CN={0},{1}".format(DEFAULTSITE, sites_dn) tmp_server = "CN=TMPSERVER,{0}".format(servers_dn) self.ldb.add({"dn": tmp_server, "objectclass": "server"}) server_guid = self.get_object_guid(tmp_server) tmp_ntds_settings = "CN=NTDS Settings,{0}".format(tmp_server) self.ldb.add({"dn": tmp_ntds_settings, "objectClass": "nTDSDSA"}, ["relax:0"]) # add an NTDS connection under the testenv DC that points to the tmp DC testenv_dc = "CN={0},{1}".format(os.environ["SERVER"], servers_dn) ntds_conn = "CN=Test-NTDS-Conn,CN=NTDS Settings,{0}".format(testenv_dc) ldif = """ dn: {dn} objectClass: nTDSConnection fromServer: CN=NTDS Settings,{fromServer} options: 1 enabledConnection: TRUE """.format(dn=ntds_conn, fromServer=tmp_server) self.ldb.add_ldif(ldif) self.addCleanup(self.ldb.delete, ntds_conn) # sanity-check we can modify the NTDS Connection object self.set_attribute(ntds_conn, 'description', 'Test-value') # sanity-check we can't modify the fromServer to point to a bad DN try: bad_dn = "CN=NTDS Settings,CN=BAD-DC,{0}".format(servers_dn) self.set_attribute(ntds_conn, 'fromServer', bad_dn, operation=ldb.FLAG_MOD_REPLACE) self.fail("Successfully set fromServer to bad DN") except ldb.LdbError as err: enum = err.args[0] self.assertEqual(enum, ldb.ERR_CONSTRAINT_VIOLATION) # delete the tmp server, i.e. pretend we demoted it self.ldb.delete(tmp_server, ["tree_delete:1"]) # check we can still see the deleted server object search_expr = '(objectGUID={0})'.format(server_guid) res = self.ldb.search(config_dn, scope=ldb.SCOPE_SUBTREE, expression=search_expr, controls=["show_deleted:1"]) self.assertTrue(len(res) == 1, "Could not find deleted server entry") # now pretend some time has passed and the deleted server object # has been tombstone-expunged from the DB time.sleep(1) current_time = int(time.time()) self.ldb.garbage_collect_tombstones([str(config_dn)], current_time, tombstone_lifetime=0) # repeat the search to sanity-check the deleted object is really gone res = self.ldb.search(config_dn, scope=ldb.SCOPE_SUBTREE, expression=search_expr, controls=["show_deleted:1"]) self.assertTrue(len(res) == 0, "Did not expunge deleted server") # the nTDSConnection now has a (mandatory) fromServer attribute that # points to an object that no longer exists. Now try to modify an # unrelated attribute on the nTDSConnection try: self.set_attribute(ntds_conn, 'description', 'Test-value-2', operation=ldb.FLAG_MOD_REPLACE) except ldb.LdbError as err: print(err) self.fail("Could not modify NTDS connection") TestProgram(module=__name__, opts=subunitopts)