PEP8: fix E127: continuation line over-indented for visual indent
[samba.git] / source4 / torture / drs / python / repl_schema.py
index 16f00acdc56e51948b8c8e2e3fc81eae6bc764d5..1e41a748f89ea49ce24acfdb3c3dab27918932e3 100644 (file)
 #  PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN repl_schema -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD"
 #
 
-import sys
 import time
 import random
-import os
+import ldb
+import drs_base
 
-sys.path.append("bin/python")
-import samba
-samba.ensure_external_module("testtools", "testtools")
-samba.ensure_external_module("subunit", "subunit/python")
+from ldb import (
+    ERR_NO_SUCH_OBJECT,
+    LdbError,
+    SCOPE_BASE,
+    Message,
+    FLAG_MOD_ADD,
+    FLAG_MOD_REPLACE
+)
+from samba.dcerpc import drsuapi, misc
+from samba.drs_utils import drs_DsBind
+from samba import dsdb
 
-from ldb import LdbError, ERR_NO_SUCH_OBJECT
-from ldb import SCOPE_BASE
-from ldb import Message
-from ldb import FLAG_MOD_REPLACE
+class DrsReplSchemaTestCase(drs_base.DrsBaseTestCase):
 
-import samba.tests
+    # prefix for all objects created
+    obj_prefix = None
+    # current Class or Attribute object id
+    obj_id = 0
 
+    def _ds_bind(self, server_name):
+        binding_str = "ncacn_ip_tcp:%s[seal]" % server_name
 
-class DrsReplSchemaTestCase(samba.tests.TestCase):
+        drs = drsuapi.drsuapi(binding_str, self.get_loadparm(), self.get_credentials())
+        (drs_handle, supported_extensions) = drs_DsBind(drs)
+        return (drs, drs_handle)
 
-    # prefix for all objects created
-    obj_prefix = None
+    def _exop_req8(self, dest_dsa, invocation_id, nc_dn_str, exop,
+                   replica_flags=0, max_objects=0):
+        req8 = drsuapi.DsGetNCChangesRequest8()
+
+        req8.destination_dsa_guid = misc.GUID(dest_dsa) if dest_dsa else misc.GUID()
+        req8.source_dsa_invocation_id = misc.GUID(invocation_id)
+        req8.naming_context = drsuapi.DsReplicaObjectIdentifier()
+        req8.naming_context.dn = str(nc_dn_str)
+        req8.highwatermark = drsuapi.DsReplicaHighWaterMark()
+        req8.highwatermark.tmp_highest_usn = 0
+        req8.highwatermark.reserved_usn = 0
+        req8.highwatermark.highest_usn = 0
+        req8.uptodateness_vector = None
+        req8.replica_flags = replica_flags
+        req8.max_object_count = max_objects
+        req8.max_ndr_size = 402116
+        req8.extended_op = exop
+        req8.fsmo_info = 0
+        req8.partial_attribute_set = None
+        req8.partial_attribute_set_ex = None
+        req8.mapping_ctr.num_mappings = 0
+        req8.mapping_ctr.mappings = None
+
+        return req8
 
     def setUp(self):
         super(DrsReplSchemaTestCase, self).setUp()
 
-        # connect to DCs
-        url_dc = samba.tests.env_get_var_value("DC1")
-        (self.ldb_dc1, self.info_dc1) = samba.tests.connect_samdb_ex(url_dc, 
-                                                                     ldap_only=True)
-        url_dc = samba.tests.env_get_var_value("DC2")
-        (self.ldb_dc2, self.info_dc2) = samba.tests.connect_samdb_ex(url_dc, 
-                                                                     ldap_only=True)
+        # disable automatic replication temporary
+        self._disable_all_repl(self.dnsname_dc1)
+        self._disable_all_repl(self.dnsname_dc2)
 
+        # make sure DCs are synchronized before the test
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True)
+        self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True)
         # initialize objects prefix if not done yet
         if self.obj_prefix is None:
             t = time.strftime("%s", time.gmtime())
-            DrsReplSchemaTestCase.obj_prefix = "DrsReplSchema-%s-" % t
-
-        # cache some of RootDSE props
-        self.schema_dn = self.info_dc1["schemaNamingContext"][0]
-        self.domain_dn = self.info_dc1["defaultNamingContext"][0]
-        self.config_dn = self.info_dc1["configurationNamingContext"][0]
-        self.forest_level = int(self.info_dc1["forestFunctionality"][0])
-
-        # we will need DCs DNS names for 'samba-tool drs' command
-        self.dnsname_dc1 = self.info_dc1["dnsHostName"][0]
-        self.dnsname_dc2 = self.info_dc2["dnsHostName"][0]
+            DrsReplSchemaTestCase.obj_prefix = "DrsReplSchema-%s" % t
 
     def tearDown(self):
+        self._enable_all_repl(self.dnsname_dc1)
+        self._enable_all_repl(self.dnsname_dc2)
         super(DrsReplSchemaTestCase, self).tearDown()
 
-    def _net_drs_replicate(self, DC, fromDC, nc_dn):
-        """Triggers replication cycle on 'DC' to
-           replicate from 'fromDC'. Naming context to
-           be replicated is 'nc_dn' dn"""
-        # find out where is net command
-        samba_tool_cmd = os.path.abspath("./bin/samba-tool")
-        # make command line credentials string
-        creds = samba.tests.cmdline_credentials
-        cmd_line_auth = "-U%s/%s%%%s" % (creds.get_domain(),
-                                         creds.get_username(), creds.get_password())
-        # bin/samba-tool drs replicate <Dest_DC_NAME> <Src_DC_NAME> <Naming Context>
-        cmd_line = "%s drs replicate %s %s %s %s" % (samba_tool_cmd, DC, fromDC,
-                                                     nc_dn, cmd_line_auth)
-        ret = os.system(cmd_line)
-        self.assertEquals(ret, 0, "Replicating %s from %s has failed!" % (DC, fromDC))
-
-    def _GUID_string(self, guid):
-        return self.ldb_dc1.schema_format_value("objectGUID", guid)
-
-    def _ldap_schemaUpdateNow(self, sam_db):
-        rec = {"dn": "",
-               "schemaUpdateNow": "1"}
-        m = Message.from_dict(sam_db, rec, FLAG_MOD_REPLACE)
-        sam_db.modify(m)
-
     def _make_obj_names(self, base_name):
         '''Try to create a unique name for an object
            that is to be added to schema'''
-        obj_name = self.obj_prefix + base_name
+        self.obj_id += 1
+        obj_name = "%s-%d-%s" % (self.obj_prefix, self.obj_id, base_name)
         obj_ldn = obj_name.replace("-", "")
-        obj_dn = "CN=%s,%s" % (obj_name, self.schema_dn)
+        obj_dn = ldb.Dn(self.ldb_dc1, "CN=X")
+        obj_dn.add_base(ldb.Dn(self.ldb_dc1, self.schema_dn))
+        obj_dn.set_component(0, "CN", obj_name)
         return (obj_dn, obj_name, obj_ldn)
 
-    def _schema_new_class(self, ldb_ctx, base_name, attrs=None):
+    def _schema_new_class(self, ldb_ctx, base_name, base_int, oc_cat=1, attrs=None):
         (class_dn, class_name, class_ldn) = self._make_obj_names(base_name)
         rec = {"dn": class_dn,
                "objectClass": ["top", "classSchema"],
                "cn": class_name,
                "lDAPDisplayName": class_ldn,
-               "governsId": "1.2.840." + str(random.randint(1,100000)) + ".1.5.13",
+               "governsId": "1.3.6.1.4.1.7165.4.6.2.5." \
+               + str((100000 * base_int) + random.randint(1,100000)) + ".1.5.13",
                "instanceType": "4",
-               "objectClassCategory": "1",
-               "subClassOf": "organizationalPerson",
+               "objectClassCategory": "%d" % oc_cat,
+               "subClassOf": "top",
                "systemOnly": "FALSE"}
         # allow overriding/adding attributes
-        if not attrs is None:
+        if attrs is not None:
             rec.update(attrs)
         # add it to the Schema
-        ldb_ctx.add(rec)
+        try:
+            ldb_ctx.add(rec)
+        except LdbError as e:
+            (enum, estr) = e.args
+            self.fail("Adding record failed with %d/%s" % (enum, estr))
+
         self._ldap_schemaUpdateNow(ldb_ctx)
         return (rec["lDAPDisplayName"], rec["dn"])
 
-    def _schema_new_attr(self, ldb_ctx, base_name, attrs=None):
+    def _schema_new_attr(self, ldb_ctx, base_name, base_int, attrs=None):
         (attr_dn, attr_name, attr_ldn) = self._make_obj_names(base_name)
         rec = {"dn": attr_dn,
                "objectClass": ["top", "attributeSchema"],
                "cn": attr_name,
                "lDAPDisplayName": attr_ldn,
-               "attributeId": "1.2.841." + str(random.randint(1,100000)) + ".1.5.13",
+               "attributeId": "1.3.6.1.4.1.7165.4.6.1.5." \
+               + str((100000 * base_int) + random.randint(1,100000)) + ".1.5.13",
                "attributeSyntax": "2.5.5.12",
                "omSyntax": "64",
                "instanceType": "4",
                "isSingleValued": "TRUE",
                "systemOnly": "FALSE"}
         # allow overriding/adding attributes
-        if not attrs is None:
+        if attrs is not None:
             rec.update(attrs)
         # add it to the Schema
         ldb_ctx.add(rec)
@@ -162,24 +171,280 @@ class DrsReplSchemaTestCase(samba.tests.TestCase):
             res_dc2 = self.ldb_dc2.search(base=obj_dn,
                                           scope=SCOPE_BASE,
                                           attrs=["*"])
-        except LdbError, (ERR_NO_SUCH_OBJECT, _):
-            self.fail("%s doesn't exists on %s" % (obj_dn, self.dnsname_dc2))
+        except LdbError as e1:
+            (enum, estr) = e1.args
+            if enum == ERR_NO_SUCH_OBJECT:
+                self.fail("%s doesn't exists on %s" % (obj_dn, self.dnsname_dc2))
+            raise
         self.assertEquals(len(res_dc2), 1,
                           "%s doesn't exists on %s" % (obj_dn, self.dnsname_dc2))
 
+    def test_class(self):
+        """Simple test for classSchema replication"""
+        # add new classSchema object
+        (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-S", 0)
+        # force replication from DC1 to DC2
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn, forced=True)
+        # check object is replicated
+        self._check_object(c_dn)
+
+    def test_classInheritance(self):
+        """Test inheritance through subClassOf
+           I think 5 levels of inheritance is pretty decent for now."""
+        # add 5 levels deep hierarchy
+        c_dn_list = []
+        c_ldn_last = None
+        for i in range(1, 6):
+            base_name = "cls-I-%02d" % i
+            (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, base_name, i)
+            c_dn_list.append(c_dn)
+            if c_ldn_last:
+                # inherit from last class added
+                m = Message.from_dict(self.ldb_dc1,
+                                      {"dn": c_dn,
+                                       "subClassOf": c_ldn_last},
+                                      FLAG_MOD_REPLACE)
+                self.ldb_dc1.modify(m)
+            # store last class ldapDisplayName
+            c_ldn_last = c_ldn
+        # force replication from DC1 to DC2
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn, forced=True)
+        # check objects are replicated
+        for c_dn in c_dn_list:
+            self._check_object(c_dn)
+
+    def test_classWithCustomAttribute(self):
+        """Create new Attribute and a Class,
+           that has value for newly created attribute.
+           This should check code path that searches for
+           AttributeID_id in Schema cache"""
+        # add new attributeSchema object
+        (a_ldn, a_dn) = self._schema_new_attr(self.ldb_dc1, "attr-A", 7)
+        # add a base classSchema class so we can use our new
+        # attribute in class definition in a sibling class
+        (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-A", 8,
+                                               1,
+                                               {"systemMayContain": a_ldn,
+                                                "subClassOf": "classSchema"})
+        # add new classSchema object with value for a_ldb attribute
+        (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-B", 9,
+                                               1,
+                                               {"objectClass": ["top", "classSchema", c_ldn],
+                                                a_ldn: "test_classWithCustomAttribute"})
+        # force replication from DC1 to DC2
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn, forced=True)
+        # check objects are replicated
+        self._check_object(c_dn)
+        self._check_object(a_dn)
+
+    def test_classWithCustomLinkAttribute(self):
+        """Create new Attribute and a Class,
+           that has value for newly created attribute.
+           This should check code path that searches for
+           AttributeID_id in Schema cache"""
+        # add new attributeSchema object
+        (a_ldn, a_dn) = self._schema_new_attr(self.ldb_dc1, "attr-Link-X", 10,
+                                              attrs={'linkID':"1.2.840.113556.1.2.50",
+                                                     "attributeSyntax": "2.5.5.1",
+                                                     "omSyntax": "127"})
+        # add a base classSchema class so we can use our new
+        # attribute in class definition in a sibling class
+        (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-Link-Y", 11,
+                                               1,
+                                               {"systemMayContain": a_ldn,
+                                                "subClassOf": "classSchema"})
+        # add new classSchema object with value for a_ldb attribute
+        (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-Link-Z", 12,
+                                               1,
+                                               {"objectClass": ["top", "classSchema", c_ldn],
+                                                a_ldn: self.schema_dn})
+        # force replication from DC1 to DC2
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn, forced=True)
+        # check objects are replicated
+        self._check_object(c_dn)
+        self._check_object(a_dn)
+
+        res = self.ldb_dc1.search(base="",
+                                  scope=SCOPE_BASE,
+                                  attrs=["domainFunctionality"])
+
+        if int(res[0]["domainFunctionality"][0]) > dsdb.DS_DOMAIN_FUNCTION_2000:
+            res = self.ldb_dc1.search(base=a_dn,
+                                      scope=SCOPE_BASE,
+                                      attrs=["msDS-IntId"])
+            self.assertEqual(1, len(res))
+            self.assertTrue("msDS-IntId" in res[0])
+            int_id = int(res[0]["msDS-IntId"][0])
+            if int_id < 0:
+                int_id += (1 << 32)
+
+        dc_guid_1 = self.ldb_dc1.get_invocation_id()
+
+        drs, drs_handle = self._ds_bind(self.dnsname_dc1)
+
+        req8 = self._exop_req8(dest_dsa=None,
+                invocation_id=dc_guid_1,
+                nc_dn_str=c_dn,
+                exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
+                replica_flags=drsuapi.DRSUAPI_DRS_SYNC_FORCED)
+
+        (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
+
+        for link in ctr.linked_attributes:
+            self.assertTrue(link.attid != int_id,
+                            'Got %d for both' % link.attid)
+
+    def test_attribute(self):
+        """Simple test for attributeSchema replication"""
+        # add new attributeSchema object
+        (a_ldn, a_dn) = self._schema_new_attr(self.ldb_dc1, "attr-S", 13)
+        # force replication from DC1 to DC2
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn, forced=True)
+        # check object is replicated
+        self._check_object(a_dn)
+
+    def test_attribute_on_ou(self):
+        """Simple test having an OU with a custome attribute replicated correctly
+
+        This ensures that the server
+        """
+
+       # add new attributeSchema object
+        (a_ldn, a_dn) = self._schema_new_attr(self.ldb_dc1, "attr-OU-S", 14)
+        (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-OU-A", 15,
+                                               3,
+                                               {"mayContain": a_ldn})
+        ou_dn = ldb.Dn(self.ldb_dc1, "ou=X")
+        ou_dn.add_base(self.ldb_dc1.get_default_basedn())
+        ou_dn.set_component(0, "OU", a_dn.get_component_value(0))
+        rec = {"dn": ou_dn,
+               "objectClass": ["top", "organizationalUnit", c_ldn],
+               "ou": ou_dn.get_component_value(0),
+               a_ldn: "test OU"}
+        self.ldb_dc1.add(rec)
+
+        # force replication from DC1 to DC2
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.domain_dn, forced=True)
+        # check objects are replicated
+        self._check_object(c_dn)
+        self._check_object(a_dn)
+        self._check_object(ou_dn)
+        self.ldb_dc1.delete(ou_dn)
+
     def test_all(self):
         """Basic plan is to create bunch of classSchema
            and attributeSchema objects, replicate Schema NC
            and then check all objects are replicated correctly"""
 
-        # add new attributeSchema object
-        (a_ldn, a_dn) = self._schema_new_attr(self.ldb_dc1, "attr-A")
         # add new classSchema object
-        (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-A")
+        (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-A", 16)
+        # add new attributeSchema object
+        (a_ldn, a_dn) = self._schema_new_attr(self.ldb_dc1, "attr-A", 17)
+
+        # add attribute to the class we have
+        m = Message.from_dict(self.ldb_dc1,
+                              {"dn": c_dn,
+                               "mayContain": a_ldn},
+                              FLAG_MOD_ADD)
+        self.ldb_dc1.modify(m)
 
         # force replication from DC1 to DC2
-        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn)
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn, forced=True)
         
         # check objects are replicated
         self._check_object(c_dn)
         self._check_object(a_dn)
+
+    def test_classWithCustomBinaryDNLinkAttribute(self):
+        # Add a new attribute to the schema, which has binary DN syntax (2.5.5.7)
+        (bin_ldn, bin_dn) = self._schema_new_attr(self.ldb_dc1, "attr-Link-Bin", 18,
+                                                  attrs={"linkID": "1.2.840.113556.1.2.50",
+                                                         "attributeSyntax": "2.5.5.7",
+                                                         "omSyntax": "127"})
+
+        (bin_ldn_b, bin_dn_b) = self._schema_new_attr(self.ldb_dc1, "attr-Link-Bin-Back", 19,
+                                                      attrs={"linkID": bin_ldn,
+                                                             "attributeSyntax": "2.5.5.1",
+                                                             "omSyntax": "127"})
+
+        # Add a new class to the schema which can have the binary DN attribute
+        (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-Link-Bin", 20,
+                                               3,
+                                               {"mayContain": bin_ldn})
+        (c_ldn_b, c_dn_b) = self._schema_new_class(self.ldb_dc1, "cls-Link-Bin-Back", 21,
+                                                   3,
+                                                   {"mayContain": bin_ldn_b})
+
+        link_end_dn = ldb.Dn(self.ldb_dc1, "ou=X")
+        link_end_dn.add_base(self.ldb_dc1.get_default_basedn())
+        link_end_dn.set_component(0, "OU", bin_dn_b.get_component_value(0))
+
+        ou_dn = ldb.Dn(self.ldb_dc1, "ou=X")
+        ou_dn.add_base(self.ldb_dc1.get_default_basedn())
+        ou_dn.set_component(0, "OU", bin_dn.get_component_value(0))
+
+        # Add an instance of the class to be pointed at
+        rec = {"dn": link_end_dn,
+               "objectClass": ["top", "organizationalUnit", c_ldn_b],
+               "ou": link_end_dn.get_component_value(0)}
+        self.ldb_dc1.add(rec)
+
+        # .. and one that does, and points to the first one
+        rec = {"dn": ou_dn,
+               "objectClass": ["top", "organizationalUnit", c_ldn],
+               "ou": ou_dn.get_component_value(0)}
+        self.ldb_dc1.add(rec)
+
+        m = Message.from_dict(self.ldb_dc1,
+                              {"dn": ou_dn,
+                               bin_ldn: "B:8:1234ABCD:%s" % str(link_end_dn)},
+                              FLAG_MOD_ADD)
+        self.ldb_dc1.modify(m)
+
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1,
+                                nc_dn=self.schema_dn, forced=True)
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1,
+                                nc_dn=self.domain_dn, forced=True)
+
+        self._check_object(c_dn)
+        self._check_object(bin_dn)
+
+        # Make sure we can delete the backlink
+        self.ldb_dc1.delete(link_end_dn)
+
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1,
+                                nc_dn=self.schema_dn, forced=True)
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1,
+                                nc_dn=self.domain_dn, forced=True)
+
+    def test_rename(self):
+        """Basic plan is to create a classSchema
+           and attributeSchema objects, replicate Schema NC
+           and then check all objects are replicated correctly"""
+
+        # add new classSchema object
+        (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-B", 20)
+
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1,
+                                nc_dn=self.schema_dn, forced=True)
+
+        # check objects are replicated
+        self._check_object(c_dn)
+
+        # rename the Class CN
+        c_dn_new = ldb.Dn(self.ldb_dc1, str(c_dn))
+        c_dn_new.set_component(0,
+                               "CN",
+                               c_dn.get_component_value(0) + "-NEW")
+        try:
+            self.ldb_dc1.rename(c_dn, c_dn_new)
+        except LdbError as e2:
+            (num, _) = e2.args
+            self.fail("failed to change CN for %s: %s" % (c_dn, _))
+
+        # force replication from DC1 to DC2
+        self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1,
+                                nc_dn=self.schema_dn, forced=True)
+
+        # check objects are replicated
+        self._check_object(c_dn_new)