From 56bbf74788b010bd24e6c483d0e7f3743487959b Mon Sep 17 00:00:00 2001
From: "Thierry bordaz (tbordaz)" <tbordaz@redhat.com>
Date: Wed, 22 Jan 2014 15:45:58 +0100
Subject: [PATCH] Ticket 47676 - Replication of the schema fails 'master
 branch' -> 1.2.11/1.3.1

Bug Description:

	Since https://fedorahosted.org/389/ticket/47490 and https://fedorahosted.org/389/ticket/47541 a supplier schema is
	pushed to the consumer at the condition that the consumer schema is not a superset of the supplier schema.

	With https://fedorahosted.org/389/ticket/47647, the objectclass 'print-uri' has been removed from master.
	Starting in 1.3.1, unhashed#user#password pseudo attribute as been removed from the schema.

	A consequence is that replication of the schema fails: master->1.2.11 and master->1.3.1

Fix Description:
	The fix handles 'unhashed#user#password' and 'printer-uri' as exception to the checking that a consumer schema
	superseeds a supplier schema

https://fedorahosted.org/389/ticket/47676

Reviewed by: ?

Platforms tested: F17/F19 (jenkins)

Flag Day: no

Doc impact: no
---
 dirsrvtests/tickets/ticket47676_test.py | 375 ++++++++++++++++++++++++++++++++
 ldap/servers/slapd/schema.c             |  92 ++++++++
 2 files changed, 467 insertions(+)
 create mode 100644 dirsrvtests/tickets/ticket47676_test.py

diff --git a/dirsrvtests/tickets/ticket47676_test.py b/dirsrvtests/tickets/ticket47676_test.py
new file mode 100644
index 0000000..576e52f
--- /dev/null
+++ b/dirsrvtests/tickets/ticket47676_test.py
@@ -0,0 +1,375 @@
+'''
+Created on Nov 7, 2013
+
+@author: tbordaz
+'''
+import os
+import sys
+import time
+import ldap
+import logging
+import socket
+import time
+import logging
+import pytest
+import re
+from lib389 import DirSrv, Entry, tools
+from lib389.tools import DirSrvTools
+from lib389._constants import *
+from lib389.properties import *
+from constants import *
+from lib389._constants import REPLICAROLE_MASTER
+
+logging.getLogger(__name__).setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+#
+# important part. We can deploy Master1 and Master2 on different versions
+#
+installation1_prefix = None
+installation2_prefix = None
+
+TEST_REPL_DN = "cn=test_repl, %s" % SUFFIX
+OC_NAME = 'OCticket47676'
+MUST = "(postalAddress $ postalCode)"
+MAY  = "(member $ street)"
+
+OTHER_NAME = 'other_entry'
+MAX_OTHERS = 10
+
+BIND_NAME  = 'bind_entry'
+BIND_DN    = 'cn=%s, %s' % (BIND_NAME, SUFFIX)
+BIND_PW    = 'password'
+
+ENTRY_NAME = 'test_entry'
+ENTRY_DN   = 'cn=%s, %s' % (ENTRY_NAME, SUFFIX)
+ENTRY_OC   = "top person %s" % OC_NAME
+    
+def _oc_definition(oid_ext, name, must=None, may=None):
+    oid  = "1.2.3.4.5.6.7.8.9.10.%d" % oid_ext
+    desc = 'To test ticket 47490'
+    sup  = 'person'
+    if not must:
+        must = MUST
+    if not may:
+        may = MAY
+    
+    new_oc = "( %s  NAME '%s' DESC '%s' SUP %s AUXILIARY MUST %s MAY %s )" % (oid, name, desc, sup, must, may)
+    return new_oc
+class TopologyMaster1Master2(object):
+    def __init__(self, master1, master2):
+        master1.open()
+        self.master1 = master1
+        
+        master2.open()
+        self.master2 = master2
+
+
+@pytest.fixture(scope="module")
+def topology(request):
+    '''
+        This fixture is used to create a replicated topology for the 'module'.
+        The replicated topology is MASTER1 <-> Master2.
+        At the beginning, It may exists a master2 instance and/or a master2 instance.
+        It may also exists a backup for the master1 and/or the master2.
+    
+        Principle:
+            If master1 instance exists:
+                restart it
+            If master2 instance exists:
+                restart it
+            If backup of master1 AND backup of master2 exists:
+                create or rebind to master1
+                create or rebind to master2
+
+                restore master1 from backup
+                restore master2 from backup
+            else:
+                Cleanup everything
+                    remove instances
+                    remove backups
+                Create instances
+                Initialize replication
+                Create backups
+    '''
+    global installation1_prefix
+    global installation2_prefix
+
+    # allocate master1 on a given deployement
+    master1   = DirSrv(verbose=False)
+    if installation1_prefix:
+        args_instance[SER_DEPLOYED_DIR] = installation1_prefix
+        
+    # Args for the master1 instance
+    args_instance[SER_HOST] = HOST_MASTER_1
+    args_instance[SER_PORT] = PORT_MASTER_1
+    args_instance[SER_SERVERID_PROP] = SERVERID_MASTER_1
+    args_master = args_instance.copy()
+    master1.allocate(args_master)
+    
+    # allocate master1 on a given deployement
+    master2 = DirSrv(verbose=False)
+    if installation2_prefix:
+        args_instance[SER_DEPLOYED_DIR] = installation2_prefix
+        
+    # Args for the consumer instance
+    args_instance[SER_HOST] = HOST_MASTER_2
+    args_instance[SER_PORT] = PORT_MASTER_2
+    args_instance[SER_SERVERID_PROP] = SERVERID_MASTER_2
+    args_master = args_instance.copy()
+    master2.allocate(args_master)
+
+    
+    # Get the status of the backups
+    backup_master1 = master1.checkBackupFS()
+    backup_master2 = master2.checkBackupFS()
+    
+    # Get the status of the instance and restart it if it exists
+    instance_master1   = master1.exists()
+    if instance_master1:
+        master1.stop(timeout=10)
+        master1.start(timeout=10)
+        
+    instance_master2 = master2.exists()
+    if instance_master2:
+        master2.stop(timeout=10)
+        master2.start(timeout=10)
+    
+    if backup_master1 and backup_master2:
+        # The backups exist, assuming they are correct 
+        # we just re-init the instances with them
+        if not instance_master1:
+            master1.create()
+            # Used to retrieve configuration information (dbdir, confdir...)
+            master1.open()
+        
+        if not instance_master2:
+            master2.create()
+            # Used to retrieve configuration information (dbdir, confdir...)
+            master2.open()
+        
+        # restore master1 from backup
+        master1.stop(timeout=10)
+        master1.restoreFS(backup_master1)
+        master1.start(timeout=10)
+        
+        # restore master2 from backup
+        master2.stop(timeout=10)
+        master2.restoreFS(backup_master2)
+        master2.start(timeout=10)
+    else:
+        # We should be here only in two conditions
+        #      - This is the first time a test involve master-consumer
+        #        so we need to create everything
+        #      - Something weird happened (instance/backup destroyed)
+        #        so we discard everything and recreate all
+        
+        # Remove all the backups. So even if we have a specific backup file
+        # (e.g backup_master) we clear all backups that an instance my have created
+        if backup_master1:
+            master1.clearBackupFS()
+        if backup_master2:
+            master2.clearBackupFS()
+        
+        # Remove all the instances
+        if instance_master1:
+            master1.delete()
+        if instance_master2:
+            master2.delete()
+                        
+        # Create the instances
+        master1.create()
+        master1.open()
+        master2.create()
+        master2.open()
+    
+        # 
+        # Now prepare the Master-Consumer topology
+        #
+        # First Enable replication
+        master1.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_MASTER, replicaId=REPLICAID_MASTER_1)
+        master2.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_MASTER, replicaId=REPLICAID_MASTER_2)
+        
+        # Initialize the supplier->consumer
+        
+        properties = {RA_NAME:      r'meTo_$host:$port',
+                      RA_BINDDN:    defaultProperties[REPLICATION_BIND_DN],
+                      RA_BINDPW:    defaultProperties[REPLICATION_BIND_PW],
+                      RA_METHOD:    defaultProperties[REPLICATION_BIND_METHOD],
+                      RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
+        repl_agreement = master1.agreement.create(suffix=SUFFIX, host=master2.host, port=master2.port, properties=properties)
+    
+        if not repl_agreement:
+            log.fatal("Fail to create a replica agreement")
+            sys.exit(1)
+            
+        log.debug("%s created" % repl_agreement)
+        
+        properties = {RA_NAME:      r'meTo_$host:$port',
+                      RA_BINDDN:    defaultProperties[REPLICATION_BIND_DN],
+                      RA_BINDPW:    defaultProperties[REPLICATION_BIND_PW],
+                      RA_METHOD:    defaultProperties[REPLICATION_BIND_METHOD],
+                      RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
+        master2.agreement.create(suffix=SUFFIX, host=master1.host, port=master1.port, properties=properties)
+
+        master1.agreement.init(SUFFIX, HOST_MASTER_2, PORT_MASTER_2)
+        master1.waitForReplInit(repl_agreement)
+        
+        # Check replication is working fine
+        master1.add_s(Entry((TEST_REPL_DN, {
+                                                'objectclass': "top person".split(),
+                                                'sn': 'test_repl',
+                                                'cn': 'test_repl'})))
+        loop = 0
+        while loop <= 10:
+            try:
+                ent = master2.getEntry(TEST_REPL_DN, ldap.SCOPE_BASE, "(objectclass=*)")
+                break
+            except ldap.NO_SUCH_OBJECT:
+                time.sleep(1)
+                loop += 1
+                
+        # Time to create the backups
+        master1.stop(timeout=10)
+        master1.backupfile = master1.backupFS()
+        master1.start(timeout=10)
+        
+        master2.stop(timeout=10)
+        master2.backupfile = master2.backupFS()
+        master2.start(timeout=10)
+    
+    # 
+    # Here we have two instances master and consumer
+    # with replication working. Either coming from a backup recovery
+    # or from a fresh (re)init
+    # Time to return the topology
+    return TopologyMaster1Master2(master1, master2)
+
+
+def test_ticket47676_init(topology):
+    """
+        It adds
+           - Objectclass with MAY 'member'
+           - an entry ('bind_entry') with which we bind to test the 'SELFDN' operation
+        It deletes the anonymous aci
+        
+    """
+        
+    
+    topology.master1.log.info("Add %s that allows 'member' attribute" % OC_NAME)
+    new_oc = _oc_definition(2, OC_NAME, must = MUST, may  = MAY) 
+    topology.master1.addSchema('objectClasses', new_oc)
+    
+    
+    # entry used to bind with
+    topology.master1.log.info("Add %s" % BIND_DN)
+    topology.master1.add_s(Entry((BIND_DN, {
+                                            'objectclass': "top person".split(),
+                                            'sn':           BIND_NAME,
+                                            'cn':           BIND_NAME,
+                                            'userpassword': BIND_PW})))
+    
+    # enable acl error logging
+    mod = [(ldap.MOD_REPLACE, 'nsslapd-errorlog-level', str(128+8192))] # ACL + REPL
+    topology.master1.modify_s(DN_CONFIG, mod)
+    topology.master2.modify_s(DN_CONFIG, mod)
+    
+    # add dummy entries
+    for cpt in range(MAX_OTHERS):
+        name = "%s%d" % (OTHER_NAME, cpt)
+        topology.master1.add_s(Entry(("cn=%s,%s" % (name, SUFFIX), {
+                                            'objectclass': "top person".split(),
+                                            'sn': name,
+                                            'cn': name})))
+
+def test_ticket47676_skip_oc_at(topology):
+    '''
+        This test ADD an entry on MASTER1 where 47676 is fixed. Then it checks that entry is replicated
+        on MASTER2 (even if on MASTER2 47676 is NOT fixed). Then update on MASTER2.
+        If the schema has successfully been pushed, updating Master2 should succeed
+    '''
+    topology.master1.log.info("\n\n######################### ADD ######################\n")
+    
+    # bind as 'cn=Directory manager'
+    topology.master1.log.info("Bind as %s and add the add the entry with specific oc" % DN_DM)
+    topology.master1.simple_bind_s(DN_DM, PASSWORD)
+    
+    # Prepare the entry with multivalued members
+    entry = Entry(ENTRY_DN)
+    entry.setValues('objectclass', 'top', 'person', 'OCticket47676')
+    entry.setValues('sn', ENTRY_NAME)
+    entry.setValues('cn', ENTRY_NAME)
+    entry.setValues('postalAddress', 'here')
+    entry.setValues('postalCode', '1234')
+    members = []
+    for cpt in range(MAX_OTHERS):
+        name = "%s%d" % (OTHER_NAME, cpt)
+        members.append("cn=%s,%s" % (name, SUFFIX))
+    members.append(BIND_DN)
+    entry.setValues('member', members)
+    
+    topology.master1.log.info("Try to add Add  %s should be successful" % ENTRY_DN)
+    topology.master1.add_s(entry)
+    
+    #
+    # Now check the entry as been replicated
+    #
+    topology.master2.simple_bind_s(DN_DM, PASSWORD)
+    topology.master1.log.info("Try to retrieve %s from Master2" % ENTRY_DN)
+    loop = 0
+    while loop <= 10:
+        try:
+            ent = topology.master2.getEntry(ENTRY_DN, ldap.SCOPE_BASE, "(objectclass=*)")
+            break
+        except ldap.NO_SUCH_OBJECT:
+            time.sleep(2)
+            loop += 1
+    assert loop <= 10
+    
+    # Now update the entry on Master2 (as DM because 47676 is possibly not fixed on M2)
+    topology.master1.log.info("Update  %s on M2" % ENTRY_DN)
+    mod = [(ldap.MOD_REPLACE, 'description', 'test_add')]
+    topology.master2.modify_s(ENTRY_DN, mod)
+    
+    topology.master1.simple_bind_s(DN_DM, PASSWORD)
+    loop = 0
+    while loop <= 10:
+        ent = topology.master1.getEntry(ENTRY_DN, ldap.SCOPE_BASE, "(objectclass=*)")
+        if ent.hasAttr('description') and (ent.getValue('description') == 'test_add'):
+            break
+        time.sleep(1)
+        loop += 1
+    
+    assert ent.getValue('description') == 'test_add'
+    
+        
+def test_ticket47676_final(topology):
+    topology.master1.stop(timeout=10) 
+    topology.master2.stop(timeout=10)   
+
+def run_isolated():
+    '''
+        run_isolated is used to run these test cases independently of a test scheduler (xunit, py.test..)
+        To run isolated without py.test, you need to 
+            - edit this file and comment '@pytest.fixture' line before 'topology' function.
+            - set the installation prefix
+            - run this program
+    '''
+    global installation1_prefix
+    global installation2_prefix
+    installation1_prefix = None
+    installation2_prefix = None
+        
+    topo = topology(True)
+    test_ticket47676_init(topo)
+    
+    test_ticket47676_skip_oc_at(topo)
+    
+    test_ticket47676_final(topo)
+    
+
+
+
+if __name__ == '__main__':
+    run_isolated()
+
diff --git a/ldap/servers/slapd/schema.c b/ldap/servers/slapd/schema.c
index 3ec76b4..4aa62b1 100644
--- a/ldap/servers/slapd/schema.c
+++ b/ldap/servers/slapd/schema.c
@@ -55,6 +55,23 @@
 #include <ldap_schema.h> /* openldap schema parser */
 #endif
 
+/*
+ * Before pushing the schema to a consumer, it is checked that consumer schema
+ * is not a superset of the supplier schema.
+ * This checking need to be relaxed for specific OC and AT that are not longer needed and defined
+ */
+#define OC_PRINTER_URI_NAME "printer-uri"
+#define OC_PRINTER_URI_OID  "printer-uri-oid"
+char *oc_name_check_skipped[] = { OC_PRINTER_URI_NAME, NULL};
+char *oc_oid_check_skipped[]  = { OC_PRINTER_URI_OID,  NULL};
+
+char *at_name_check_skipped[] = { PSEUDO_ATTR_UNHASHEDUSERPASSWORD, NULL};
+#if defined(USE_OLD_UNHASHED)
+char *at_oid_check_skipped[]  = { PSEUDO_ATTR_UNHASHEDUSERPASSWORD_OID, NULL};
+#else
+char *at_oid_check_skipped[]  = { "2.16.840.1.113730.3.1.2110", NULL};
+#endif
+
 typedef struct sizedbuffer
 {
     char *buffer;
@@ -5908,6 +5925,33 @@ slapi_schema_get_superior_name(const char *ocname_or_oid)
 }
 
 
+/*
+ * It checks if the provided objectclass 'oc' is present in
+ * the array of objectclasses to skip.
+ * It is looking up with name and oid
+ * 
+ * If it is present in the array -> return PR_TRUE
+ * else it returns PR_FALSE
+ */
+static PRBool
+oc_check_to_skip(struct objclass *oc)
+{
+        PRBool skip = PR_FALSE;
+        int i;
+        /* Check the oc name is not listed as objectclass to skip */
+        for (i = 0; !skip && oc_name_check_skipped[i]; i++) {
+                if ((strcasecmp( oc->oc_name, oc_name_check_skipped[i]) == 0)) {
+                        skip = PR_TRUE;
+                }
+        }      
+        /* Check the oc oid is not listed as objectclass to skip */
+        for (i = 0; !skip && oc_oid_check_skipped[i]; i++) {
+                if ((strcmp( oc->oc_oid, oc_oid_check_skipped[i]) == 0)) {
+                        skip = PR_TRUE;
+                }
+        }
+        return skip;
+}
 
 /* Check if the oc_list1 is a superset of oc_list2.
  * oc_list1 is a superset if it exists objectclass in oc_list1 that
@@ -5948,6 +5992,16 @@ schema_oc_superset_check(struct objclass *oc_list1, struct objclass *oc_list2, c
          */
         for (oc_1 = oc_list1; oc_1 != NULL; oc_1 = oc_1->oc_next) {
 
+                /* some objectclass may have disappeared without being a concern, just skip them */          
+                if (oc_check_to_skip(oc_1)) {
+                        if (debug_logging) {
+                                slapi_log_error(SLAPI_LOG_REPL, "schema", "Do not check if this oc is missing on the remote schema [%s or %s]\n",
+                                        oc_1->oc_name,
+                                        oc_1->oc_oid);
+                        }
+                        continue;
+                }
+                
                 /* Retrieve the remote objectclass in our local schema */
                 oc_2 = oc_find_nolock(oc_1->oc_oid, oc_list2, PR_TRUE);
                 if (oc_2 == NULL) {
@@ -6046,6 +6100,34 @@ schema_oc_superset_check(struct objclass *oc_list1, struct objclass *oc_list2, c
         return rc;
 }
 
+/*
+ * It checks if the provided attribute 'at' is present in
+ * the array of attributes to skip.
+ * It is looking up with name and oid
+ * 
+ * If it is present in the array -> return PR_TRUE
+ * else it returns PR_FALSE
+ */
+static PRBool
+at_check_to_skip(struct asyntaxinfo *at)
+{
+        PRBool skip = PR_FALSE;
+        int i;
+        /* Check the oc name is not listed as objectclass to skip */
+        for (i = 0; !skip && at_name_check_skipped[i]; i++) {
+                if ((strcasecmp( at->asi_name, at_name_check_skipped[i]) == 0)) {
+                        skip = PR_TRUE;
+                }
+        }      
+        /* Check the oc oid is not listed as objectclass to skip */
+        for (i = 0; !skip && at_oid_check_skipped[i]; i++) {
+                if ((strcmp( at->asi_oid, at_oid_check_skipped[i]) == 0)) {
+                        skip = PR_TRUE;
+                }
+        }
+        return skip;
+}
+
 static int
 schema_at_superset_check(struct asyntaxinfo *at_list1, struct asyntaxinfo *at_list2, char *message)
 {
@@ -6065,6 +6147,16 @@ schema_at_superset_check(struct asyntaxinfo *at_list1, struct asyntaxinfo *at_li
 
     for (at_1 = at_list1; at_1 != NULL; at_1 = at_1->asi_next){
 
+        /* some objectclass may have disappeared without being a concern, just skip them */
+        if (at_check_to_skip(at_1)) {
+                if (debug_logging) {
+                        slapi_log_error(SLAPI_LOG_REPL, "schema", "Do not check if this attribute is missing on the remote schema [%s or %s]\n",
+                                at_1->asi_name,
+                                at_1->asi_oid);
+                }
+                continue;
+        }
+                
         /* check if at_1 exists in at_list2 */
         if((at_2 = attr_syntax_find(at_1, at_list2))){
             /*
-- 
1.7.11.7

