dirsrvtests/tickets/ticket47721_test.py | 492 ++++++++++
ldap/servers/plugins/replication/repl5_connection.c | 191 ++-
ldap/servers/slapd/proto-slap.h | 1
ldap/servers/slapd/schema.c | 977 ++++++++++++++++----
4 files changed, 1436 insertions(+), 225 deletions(-)
New commits:
commit 568f4a9d48dc215ac77a8250a3dc942a26d41276
Author: Thierry bordaz (tbordaz) <tbordaz(a)redhat.com>
Date: Tue Mar 25 18:35:46 2014 +0100
Ticket 47721 - Schema Replication Issue
Bug Description:
During a replication session, a supplier may send its schema in order to overwrite a consumer schema.
Since https://fedorahosted.org/389/ticket/47490, a replica acting as a consumer rejects the schema
if it evaluates that its own schema is a superset of the received schema. Also with 47490, a replica
acting as a supplier will not send its schema if it evaluates that the consumer schema is a superset
of its own schema.
This creates an issue if the schema on both side contains definitions that the other side ignore, or
that extend the other side definition. In that case each side will keep its own schema.
Fix Description:
http://directory.fedoraproject.org/wiki/Replication_of_custom_schema_(ticke…
When acting as a consumer, it may receive a schema during a replication session.
It then evaluates each definition (objectclasses/attritetypes) and if a definition
is new or extend a current definition, then it add the definitions in its schema (in 99user.ldif)
When acting as a supplier, it looks up the consumer schema. Whether it decides to send
its own schema or not, it first evaluates each definition (objectclasses/attritetypes) and
if a definition is new or extend a current definition, then it add the definitions in its schema (in 99user.ldif)
https://fedorahosted.org/389/ticket/47721
Reviewed by: Rich Megginson (Big thanks Rich for the review)
Platforms tested:
for 389-DS CI tests: F17, F19 (jenkins)
for Dogtag: manual test case master/clone - F19 (10.0.7) / F20 (10.1.1)
for IPA: F20 3.3.5 manual test case and unit tests
Flag Day: no
diff --git a/dirsrvtests/tickets/ticket47721_test.py b/dirsrvtests/tickets/ticket47721_test.py
new file mode 100644
index 0000000..0d6cd8b
--- /dev/null
+++ b/dirsrvtests/tickets/ticket47721_test.py
@@ -0,0 +1,492 @@
+'''
+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
+
+SCHEMA_DN = "cn=schema"
+TEST_REPL_DN = "cn=test_repl, %s" % SUFFIX
+OC_NAME = 'OCticket47721'
+OC_OID_EXT = 2
+MUST = "(postalAddress $ postalCode)"
+MAY = "(member $ street)"
+
+OC2_NAME = 'OC2ticket47721'
+OC2_OID_EXT = 3
+MUST_2 = "(postalAddress $ postalCode)"
+MAY_2 = "(member $ street)"
+
+REPL_SCHEMA_POLICY_CONSUMER = "cn=consumerUpdatePolicy,cn=replSchema,cn=config"
+REPL_SCHEMA_POLICY_SUPPLIER = "cn=supplierUpdatePolicy,cn=replSchema,cn=config"
+
+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
+
+BASE_OID = "1.2.3.4.5.6.7.8.9.10"
+
+def _add_custom_at_definition(name='ATticket47721'):
+ new_at = "( %s-oid NAME '%s' DESC 'test AT ticket 47721' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN ( 'Test 47721' 'user defined' ) )" % (name, name)
+ return new_at
+
+def _chg_std_at_defintion():
+ new_at = "( 2.16.840.1.113730.3.1.569 NAME 'cosPriority' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 X-ORIGIN 'Netscape Directory Server' )"
+ return new_at
+
+def _add_custom_oc_defintion(name='OCticket47721'):
+ new_oc = "( %s-oid NAME '%s' DESC 'An group of related automount objects' SUP top STRUCTURAL MUST ou X-ORIGIN 'draft-howard-rfc2307bis' )" % (name, name)
+ return new_oc
+
+def _chg_std_oc_defintion():
+ new_oc = "( 5.3.6.1.1.1.2.0 NAME 'trustAccount' DESC 'Sets trust accounts information' SUP top AUXILIARY MUST trustModel MAY ( accessTo $ ou ) X-ORIGIN 'nss_ldap/pam_ldap' )"
+ return new_oc
+
+
+class TopologyMaster1Master2(object):
+ def __init__(self, master1, master2):
+ master1.open()
+ self.master1 = master1
+
+ master2.open()
+ self.master2 = master2
+
+
+(a)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_ticket47721_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
+
+ """
+
+
+
+ # 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(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_ticket47721_0(topology):
+ dn = "cn=%s0,%s" % (OTHER_NAME, SUFFIX)
+ loop = 0
+ while loop <= 10:
+ try:
+ ent = topology.master2.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)")
+ break
+ except ldap.NO_SUCH_OBJECT:
+ time.sleep(1)
+ loop += 1
+ assert loop <= 10
+
+def test_ticket47721_1(topology):
+ topology.master1.log.info("Attach debugger\n\n" )
+ #time.sleep(30)
+
+ new = _add_custom_at_definition()
+ topology.master1.log.info("Add (M2) %s " % new)
+ topology.master2.schema.add_schema('attributetypes', new)
+
+ new = _chg_std_at_defintion()
+ topology.master1.log.info("Chg (M2) %s " % new)
+ topology.master2.schema.add_schema('attributetypes', new)
+
+ new = _add_custom_oc_defintion()
+ topology.master1.log.info("Add (M2) %s " % new)
+ topology.master2.schema.add_schema('objectClasses', new)
+
+ new = _chg_std_oc_defintion()
+ topology.master1.log.info("Chg (M2) %s " % new)
+ topology.master2.schema.add_schema('objectClasses', new)
+
+ mod = [(ldap.MOD_REPLACE, 'description', 'Hello world 1')]
+ dn = "cn=%s0,%s" % (OTHER_NAME, SUFFIX)
+ topology.master2.modify_s(dn, mod)
+
+ loop = 0
+ while loop <= 10:
+ try:
+ ent = topology.master1.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)")
+ if ent.hasAttr('description') and (ent.getValue('description') == 'Hello world 1'):
+ break
+ except ldap.NO_SUCH_OBJECT:
+ loop += 1
+ time.sleep(1)
+ assert loop <= 10
+
+def test_ticket47721_2(topology):
+ mod = [(ldap.MOD_REPLACE, 'description', 'Hello world 2')]
+ dn = "cn=%s0,%s" % (OTHER_NAME, SUFFIX)
+ topology.master1.modify_s(dn, mod)
+
+ loop = 0
+ while loop <= 10:
+ try:
+ ent = topology.master2.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)")
+ if ent.hasAttr('description') and (ent.getValue('description') == 'Hello world 2'):
+ break
+ except ldap.NO_SUCH_OBJECT:
+ loop += 1
+ time.sleep(1)
+ assert loop <= 10
+
+ schema_csn_master1 = topology.master1.schema.get_schema_csn()
+ schema_csn_master2 = topology.master2.schema.get_schema_csn()
+ assert schema_csn_master1 != None
+ assert schema_csn_master1 == schema_csn_master2
+
+def test_ticket47721_3(topology):
+ '''
+ Check that the supplier can update its schema from consumer schema
+ Update M2 schema, then trigger a replication M1->M2
+ '''
+ # stop RA M2->M1, so that M1 can only learn being a supplier
+ ents = topology.master2.agreement.list(suffix=SUFFIX)
+ assert len(ents) == 1
+ topology.master2.agreement.pause(ents[0].dn)
+
+ new = _add_custom_at_definition('ATtest3')
+ topology.master1.log.info("Update schema (M2) %s " % new)
+ topology.master2.schema.add_schema('attributetypes', new)
+
+ new = _add_custom_oc_defintion('OCtest3')
+ topology.master1.log.info("Update schema (M2) %s " % new)
+ topology.master2.schema.add_schema('objectClasses', new)
+
+ mod = [(ldap.MOD_REPLACE, 'description', 'Hello world 3')]
+ dn = "cn=%s0,%s" % (OTHER_NAME, SUFFIX)
+ topology.master1.modify_s(dn, mod)
+
+ loop = 0
+ while loop <= 10:
+ try:
+ ent = topology.master2.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)")
+ if ent.hasAttr('description') and (ent.getValue('description') == 'Hello world 3'):
+ break
+ except ldap.NO_SUCH_OBJECT:
+ loop += 1
+ time.sleep(1)
+ assert loop <= 10
+
+ schema_csn_master1 = topology.master1.schema.get_schema_csn()
+ schema_csn_master2 = topology.master2.schema.get_schema_csn()
+ assert schema_csn_master1 != None
+ # schema csn on M2 is larger that on M1. M1 only took the new definitions
+ assert schema_csn_master1 != schema_csn_master2
+
+def test_ticket47721_4(topology):
+ '''
+ Here M2->M1 agreement is disabled.
+ with test_ticket47721_3, M1 schema and M2 should be identical BUT
+ the nsschemacsn is M2>M1. But as the RA M2->M1 is disabled, M1 keeps its schemacsn.
+ Update schema on M2 (nsschemaCSN update), update M2. Check they have the same schemacsn
+ '''
+ new = _add_custom_at_definition('ATtest4')
+ topology.master1.log.info("Update schema (M1) %s " % new)
+ topology.master1.schema.add_schema('attributetypes', new)
+
+ new = _add_custom_oc_defintion('OCtest4')
+ topology.master1.log.info("Update schema (M1) %s " % new)
+ topology.master1.schema.add_schema('objectClasses', new)
+
+ topology.master1.log.info("trigger replication M1->M2: to update the schema")
+ mod = [(ldap.MOD_REPLACE, 'description', 'Hello world 4')]
+ dn = "cn=%s0,%s" % (OTHER_NAME, SUFFIX)
+ topology.master1.modify_s(dn, mod)
+
+ loop = 0
+ while loop <= 10:
+ try:
+ ent = topology.master2.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)")
+ if ent.hasAttr('description') and (ent.getValue('description') == 'Hello world 4'):
+ break
+ except ldap.NO_SUCH_OBJECT:
+ loop += 1
+ time.sleep(1)
+ assert loop <= 10
+
+ topology.master1.log.info("trigger replication M1->M2: to push the schema")
+ mod = [(ldap.MOD_REPLACE, 'description', 'Hello world 5')]
+ dn = "cn=%s0,%s" % (OTHER_NAME, SUFFIX)
+ topology.master1.modify_s(dn, mod)
+
+ loop = 0
+ while loop <= 10:
+ try:
+ ent = topology.master2.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)")
+ if ent.hasAttr('description') and (ent.getValue('description') == 'Hello world 5'):
+ break
+ except ldap.NO_SUCH_OBJECT:
+ loop += 1
+ time.sleep(1)
+ assert loop <= 10
+
+ schema_csn_master1 = topology.master1.schema.get_schema_csn()
+ schema_csn_master2 = topology.master2.schema.get_schema_csn()
+ assert schema_csn_master1 != None
+ assert schema_csn_master1 == schema_csn_master2
+
+def test_ticket47721_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)
+ topo.master1.log.info("\n\n######################### Ticket 47721 ######################\n")
+ test_ticket47721_init(topo)
+
+ test_ticket47721_0(topo)
+ test_ticket47721_1(topo)
+ test_ticket47721_2(topo)
+ test_ticket47721_3(topo)
+ test_ticket47721_4(topo)
+ sys.exit(0)
+
+ test_ticket47721_final(topo)
+
+
+
+
+if __name__ == '__main__':
+ run_isolated()
+
diff --git a/ldap/servers/plugins/replication/repl5_connection.c b/ldap/servers/plugins/replication/repl5_connection.c
index 3d29a79..c004bfb 100644
--- a/ldap/servers/plugins/replication/repl5_connection.c
+++ b/ldap/servers/plugins/replication/repl5_connection.c
@@ -99,6 +99,7 @@ typedef struct repl_connection
/*** from proto-slap.h ***/
int schema_objectclasses_superset_check(struct berval **remote_schema, char *type);
int schema_attributetypes_superset_check(struct berval **remote_schema, char *type);
+void supplier_learn_new_definitions(struct berval **objectclasses, struct berval **attributetypes);
/* Controls we add on every outbound operation */
static LDAPControl manageDSAITControl = {LDAP_CONTROL_MANAGEDSAIT, {0, ""}, '\0'};
@@ -1518,6 +1519,84 @@ attribute_string_value_present(LDAP *ld, LDAPMessage *entry, const char *type,
return return_value;
}
+/* It returns the objectclasses and attributetypes of the remote schema
+ * in the form of berval arrays.
+ * In case of success, the caller must free those berval arrays with ber_bvecfree
+ * */
+static ConnResult
+supplier_read_consumer_definitions(Repl_Connection *conn, struct berval ***remote_objectclasses, struct berval ***remote_attributetypes)
+{
+ ConnResult return_value = CONN_OPERATION_SUCCESS;
+ struct berval **remote_schema_objectclasses_bervals = NULL;
+ struct berval **remote_schema_attributetypes_bervals = NULL;
+
+ *remote_objectclasses = NULL;
+ *remote_attributetypes = NULL;
+
+ /* read the objectclass then the attribytetype from the remote schema */
+ return_value = conn_read_entry_attribute(conn, "cn=schema", "objectclasses", &remote_schema_objectclasses_bervals);
+ if (return_value == CONN_OPERATION_SUCCESS) {
+ *remote_objectclasses = remote_schema_objectclasses_bervals;
+
+ return_value = conn_read_entry_attribute(conn, "cn=schema", "attributetypes", &remote_schema_attributetypes_bervals);
+ if (return_value == CONN_OPERATION_SUCCESS) {
+ *remote_attributetypes = remote_schema_attributetypes_bervals;
+ } else {
+ slapi_log_error(SLAPI_LOG_FATAL, repl_plugin_name,
+ "%s: Fail to retrieve the remote schema attributetypes\n",
+ agmt_get_long_name(conn->agmt));
+ }
+ } else {
+ slapi_log_error(SLAPI_LOG_FATAL, repl_plugin_name,
+ "%s: Fail to retrieve the remote schema objectclasses\n",
+ agmt_get_long_name(conn->agmt));
+ }
+
+ if (return_value != CONN_OPERATION_SUCCESS) {
+ /* in case of failure free everything */
+ *remote_objectclasses = NULL;
+ if (remote_schema_objectclasses_bervals) {
+ ber_bvecfree(remote_schema_objectclasses_bervals);
+ }
+
+ *remote_attributetypes = NULL;
+ if (remote_schema_attributetypes_bervals) {
+ ber_bvecfree(remote_schema_attributetypes_bervals);
+ }
+ }
+ return return_value;
+
+}
+//
+
+static PRBool
+update_consumer_schema(Repl_Connection *conn)
+{
+ struct berval **remote_schema_objectclasses_bervals = NULL;
+ struct berval **remote_schema_attributetypes_bervals = NULL;
+ PRBool ok_to_send_schema = PR_TRUE;
+
+ if (supplier_read_consumer_definitions(conn, &remote_schema_objectclasses_bervals, &remote_schema_attributetypes_bervals) == CONN_OPERATION_SUCCESS) {
+ if (schema_objectclasses_superset_check(remote_schema_objectclasses_bervals, OC_SUPPLIER) ||
+ schema_attributetypes_superset_check(remote_schema_attributetypes_bervals, OC_SUPPLIER)) {
+
+ /* The consumer contains definitions that needs to be learned */
+ supplier_learn_new_definitions(remote_schema_objectclasses_bervals, remote_schema_attributetypes_bervals);
+ slapi_log_error(SLAPI_LOG_FATAL, repl_plugin_name,
+ "[S] Schema %s must not be overwritten (set replication log for additional info)\n",
+ agmt_get_long_name(conn->agmt));
+ ok_to_send_schema = PR_FALSE;
+ }
+ ber_bvecfree(remote_schema_objectclasses_bervals);
+ ber_bvecfree(remote_schema_attributetypes_bervals);
+ } else {
+ /* We can not be sure, be conservative and not send the schema */
+ ok_to_send_schema = PR_FALSE;
+ }
+ return ok_to_send_schema;
+
+}
+
/*
* Read the remote server's schema entry, then read the local schema entry,
* and compare the nsschemacsn attribute. If the local csn is newer, or
@@ -1568,79 +1647,45 @@ conn_push_schema(Repl_Connection *conn, CSN **remotecsn)
return_value = CONN_SCHEMA_NO_UPDATE_NEEDED;
}
else
- {
- struct berval **remote_schema_csn_bervals = NULL;
- /* Get remote server's schema */
- return_value = conn_read_entry_attribute(conn, "cn=schema", nsschemacsn,
- &remote_schema_csn_bervals);
- if (CONN_OPERATION_SUCCESS == return_value)
- {
- if (NULL != remote_schema_csn_bervals && NULL != remote_schema_csn_bervals[0])
- {
- char remotecsnstr[CSN_STRSIZE + 1] = {0};
- memcpy(remotecsnstr, remote_schema_csn_bervals[0]->bv_val,
- remote_schema_csn_bervals[0]->bv_len);
- remotecsnstr[remote_schema_csn_bervals[0]->bv_len] = '\0';
- *remotecsn = csn_new_by_string(remotecsnstr);
- if (*remotecsn && (csn_compare(localcsn, *remotecsn) <= 0))
- {
- return_value = CONN_SCHEMA_NO_UPDATE_NEEDED;
- }
- /* Need to free the remote_schema_csn_bervals */
- ber_bvecfree(remote_schema_csn_bervals);
- }
- if (return_value != CONN_SCHEMA_NO_UPDATE_NEEDED) {
- struct berval **remote_schema_objectclasses_bervals = NULL;
- struct berval **remote_schema_attributetypes_bervals = NULL;
- /* before pushing the schema do some checking */
-
- /* First objectclasses */
- return_value = conn_read_entry_attribute(conn, "cn=schema", "objectclasses",
- &remote_schema_objectclasses_bervals);
- if (return_value == CONN_OPERATION_SUCCESS) {
- /* Check if the consumer objectclasses are a superset of the local supplier schema */
- if (schema_objectclasses_superset_check(remote_schema_objectclasses_bervals, OC_SUPPLIER)) {
- slapi_log_error(SLAPI_LOG_FATAL, repl_plugin_name,
- "Schema %s must not be overwritten (set replication log for additional info)\n",
- agmt_get_long_name(conn->agmt));
- return_value = CONN_OPERATION_FAILED;
- }
- if(remote_schema_objectclasses_bervals){
- ber_bvecfree(remote_schema_objectclasses_bervals);
- }
- } else {
- slapi_log_error(SLAPI_LOG_FATAL, repl_plugin_name,
- "%s: Fail to retrieve the remote schema objectclasses\n",
- agmt_get_long_name(conn->agmt));
- }
- if (return_value == CONN_OPERATION_SUCCESS) {
- /* Next attribute types */
- return_value = conn_read_entry_attribute(conn, "cn=schema", "attributetypes",
- &remote_schema_attributetypes_bervals);
- if (return_value == CONN_OPERATION_SUCCESS) {
- /* Check if the consumer attributes are a superset of the local supplier schema */
- if (schema_attributetypes_superset_check(remote_schema_attributetypes_bervals, OC_SUPPLIER)) {
- slapi_log_error(SLAPI_LOG_FATAL, repl_plugin_name,
- "Schema %s must not be overwritten (set replication log for additional info)\n",
- agmt_get_long_name(conn->agmt));
- return_value = CONN_OPERATION_FAILED;
- }
- if(remote_schema_attributetypes_bervals){
- ber_bvecfree(remote_schema_attributetypes_bervals);
- }
- } else {
- slapi_log_error(SLAPI_LOG_FATAL, repl_plugin_name,
- "%s: Fail to retrieve the remote schema attribute types\n",
- agmt_get_long_name(conn->agmt));
- }
- }
- /* In case of success, possibly log a message */
- if (return_value == CONN_OPERATION_SUCCESS) {
- slapi_log_error(SLAPI_LOG_REPL, repl_plugin_name,
- "Schema checking successful: ok to push the schema (%s)\n", agmt_get_long_name(conn->agmt));
- }
- }
- }
+ {
+ if (!update_consumer_schema(conn)) {
+ /* At least one schema definition (attributetypes/objectclasses) of the consumer
+ * is a superset of the supplier.
+ * It is not possible push the schema immediately.
+ * Note: in update_consumer_schema, it may update the local supplier schema.
+ * So it could be possible that a second attempt (right now) of update_consumer_schema
+ * would be successful
+ */
+ if (!update_consumer_schema(conn)) {
+ return_value = CONN_OPERATION_FAILED;
+ }
+ }
+ if (return_value == CONN_OPERATION_SUCCESS) {
+ struct berval **remote_schema_csn_bervals = NULL;
+
+ /* Get remote server's schema */
+ return_value = conn_read_entry_attribute(conn, "cn=schema", nsschemacsn,
+ &remote_schema_csn_bervals);
+ if (CONN_OPERATION_SUCCESS == return_value) {
+ if (NULL != remote_schema_csn_bervals && NULL != remote_schema_csn_bervals[0]) {
+ char remotecsnstr[CSN_STRSIZE + 1] = {0};
+ memcpy(remotecsnstr, remote_schema_csn_bervals[0]->bv_val,
+ remote_schema_csn_bervals[0]->bv_len);
+ remotecsnstr[remote_schema_csn_bervals[0]->bv_len] = '\0';
+ *remotecsn = csn_new_by_string(remotecsnstr);
+ if (*remotecsn && (csn_compare(localcsn, *remotecsn) <= 0)) {
+ return_value = CONN_SCHEMA_NO_UPDATE_NEEDED;
+ }
+ /* Need to free the remote_schema_csn_bervals */
+ ber_bvecfree(remote_schema_csn_bervals);
+ }
+ if (return_value == CONN_OPERATION_SUCCESS) {
+ slapi_log_error(SLAPI_LOG_REPL, repl_plugin_name,
+ "Schema checking successful: ok to push the schema (%s)\n", agmt_get_long_name(conn->agmt));
+ }
+
+ }
+ }
}
}
if (CONN_OPERATION_SUCCESS == return_value)
diff --git a/ldap/servers/slapd/proto-slap.h b/ldap/servers/slapd/proto-slap.h
index 3aa2170..14ed6f7 100644
--- a/ldap/servers/slapd/proto-slap.h
+++ b/ldap/servers/slapd/proto-slap.h
@@ -1016,6 +1016,7 @@ void schema_free_extensions(schemaext *extensions);
schemaext *schema_copy_extensions(schemaext *extensions);
int schema_objectclasses_superset_check(struct berval **remote_schema, char *type);
int schema_attributetypes_superset_check(struct berval **remote_schema, char *type);
+void supplier_learn_new_definitions(struct berval **objectclasses, struct berval **attributetypes);
/*
* schemaparse.c
diff --git a/ldap/servers/slapd/schema.c b/ldap/servers/slapd/schema.c
index bd0e006..50faef7 100644
--- a/ldap/servers/slapd/schema.c
+++ b/ldap/servers/slapd/schema.c
@@ -61,6 +61,7 @@ typedef struct sizedbuffer
size_t size;
} sizedbuffer;
+
typedef char *(*schema_strstr_fn_t)( const char *big, const char *little);
/*
@@ -135,6 +136,12 @@ typedef struct repl_schema_policy {
schema_item_t *attributes;
} repl_schema_policy_t;
+struct schema_mods_indexes {
+ int index;
+ char *new_value;
+ struct schema_mods_indexes *next;
+};
+
/*
* pschemadse is based on the general implementation in dse
*/
@@ -194,6 +201,12 @@ static void schema_create_errormsg( char *errorbuf, size_t errorbufsize,
#else
;
#endif
+static PRBool check_replicated_schema(LDAPMod **mods, char *replica_role, char **attr_name);
+static void modify_schema_get_new_definitions(Slapi_PBlock *pb, LDAPMod **mods, struct schema_mods_indexes **at_list, struct schema_mods_indexes **oc_list);
+static void modify_schema_apply_new_definitions(char *attr_name, struct schema_mods_indexes *list);
+static void modify_schema_free_new_definitions(struct schema_mods_indexes *def_list);
+static int schema_oc_compare(struct objclass *oc_1, struct objclass *oc_2, char *description);
+static int schema_at_compare(struct asyntaxinfo *at_1, struct asyntaxinfo *at_2, char *message, int debug_logging);
static int schema_at_superset_check(struct asyntaxinfo *at_list1, struct asyntaxinfo *at_list2, char *message, int replica_role);
static int schema_at_superset_check_syntax_oids(char *oid1, char *oid2);
static int schema_at_superset_check_mr(struct asyntaxinfo *a1, struct asyntaxinfo *a2, char *info);
@@ -2063,6 +2076,46 @@ modify_schema_dse (Slapi_PBlock *pb, Slapi_Entry *entryBefore, Slapi_Entry *entr
slapi_pblock_get( pb, SLAPI_MODIFY_MODS, &mods );
slapi_pblock_get( pb, SLAPI_IS_REPLICATED_OPERATION, &is_replicated_operation);
+
+ /* In case we receive a schema from a supplier, check if we can accept it
+ * (it is a superset of our own schema).
+ * If it is not a superset, pick up what could extend our schema and return
+ */
+ if (is_replicated_operation) {
+ char *attr_name = NULL;
+ struct schema_mods_indexes *at_list = NULL;
+ struct schema_mods_indexes *oc_list = NULL;
+
+ if (!check_replicated_schema(mods, OC_CONSUMER, &attr_name)) {
+
+ /* we will refuse to apply this schema
+ * Try to capture in it what would extends our own schema
+ */
+ modify_schema_get_new_definitions(pb, mods, &at_list, &oc_list);
+ if (at_list) {
+ modify_schema_apply_new_definitions("attributetypes", at_list);
+ }
+ if (oc_list) {
+ modify_schema_apply_new_definitions("objectclasses", oc_list);
+ }
+
+ /* No need to hold the lock for these list that are local */
+ modify_schema_free_new_definitions(at_list);
+ modify_schema_free_new_definitions(oc_list);
+
+ /* now return, we will not apply that schema */
+ schema_create_errormsg( returntext, SLAPI_DSE_RETURNTEXT_SIZE,
+ schema_errprefix_generic, attr_name,
+ "Replace is not possible, local consumer schema is a superset of the supplier" );
+ slapi_log_error(SLAPI_LOG_FATAL, "schema",
+ "[C] Local %s must not be overwritten (set replication log for additional info)\n",
+ attr_name);
+ *returncode = LDAP_UNWILLING_TO_PERFORM;
+ return (SLAPI_DSE_CALLBACK_ERROR);
+ }
+ }
+
+
schema_dse_lock_write();
/*
@@ -2136,62 +2189,23 @@ modify_schema_dse (Slapi_PBlock *pb, Slapi_Entry *entryBefore, Slapi_Entry *entr
"Replace is not allowed on the subschema subentry" );
rc = SLAPI_DSE_CALLBACK_ERROR;
} else {
- if (strcasecmp (mods[i]->mod_type, "attributetypes") == 0) {
- if (is_replicated_operation) {
+ if (strcasecmp (mods[i]->mod_type, "attributetypes") == 0) {
/*
- * before accepting the schema checks if the local consumer schema is not
- * a superset of the supplier schema
- */
- if (schema_attributetypes_superset_check(mods[i]->mod_bvalues, OC_CONSUMER)) {
- schema_create_errormsg( returntext, SLAPI_DSE_RETURNTEXT_SIZE,
- schema_errprefix_generic, mods[i]->mod_type,
- "Replace is not possible, local consumer schema is a superset of the supplier" );
- slapi_log_error(SLAPI_LOG_FATAL, "schema",
- "Local %s must not be overwritten (set replication log for additional info)\n",
- mods[i]->mod_type);
- *returncode = LDAP_UNWILLING_TO_PERFORM;
- } else {
- /*
- * Replace all attributes
- */
- *returncode = schema_replace_attributes( pb, mods[i], returntext,
- SLAPI_DSE_RETURNTEXT_SIZE );
- }
- } else {
- /*
- * Replace all objectclasses
+ * Replace all attributetypes
+ * It has already been checked that if it was a replicated schema
+ * it is a superset of the current schema. That is fine to apply the mods
*/
*returncode = schema_replace_attributes( pb, mods[i], returntext,
SLAPI_DSE_RETURNTEXT_SIZE );
- }
} else if (strcasecmp (mods[i]->mod_type, "objectclasses") == 0) {
- if (is_replicated_operation) {
- /* before accepting the schema checks if the local consumer schema is not
- * a superset of the supplier schema
- */
- if (schema_objectclasses_superset_check(mods[i]->mod_bvalues, OC_CONSUMER)) {
-
- schema_create_errormsg( returntext, SLAPI_DSE_RETURNTEXT_SIZE,
- schema_errprefix_generic, mods[i]->mod_type,
- "Replace is not possible, local consumer schema is a superset of the supplier" );
- slapi_log_error(SLAPI_LOG_FATAL, "schema",
- "Local %s must not be overwritten (set replication log for additional info)\n",
- mods[i]->mod_type);
- *returncode = LDAP_UNWILLING_TO_PERFORM;
- } else {
- /*
- * Replace all objectclasses
- */
- *returncode = schema_replace_objectclasses(pb, mods[i],
- returntext, SLAPI_DSE_RETURNTEXT_SIZE);
- }
- } else {
/*
* Replace all objectclasses
+ * It has already been checked that if it was a replicated schema
+ * it is a superset of the current schema. That is fine to apply the mods
*/
*returncode = schema_replace_objectclasses(pb, mods[i],
returntext, SLAPI_DSE_RETURNTEXT_SIZE);
- }
+
} else if (strcasecmp (mods[i]->mod_type, "nsschemacsn") == 0) {
if (is_replicated_operation) {
/* Update the schema CSN */
@@ -6139,8 +6153,6 @@ slapi_schema_get_superior_name(const char *ocname_or_oid)
return superior;
}
-
-
/* Check if the oc_list1 is a superset of oc_list2.
* oc_list1 is a superset if it exists objectclass in oc_list1 that
* do not exist in oc_list2. Or if a OC in oc_list1 required more attributes
@@ -6156,9 +6168,8 @@ schema_oc_superset_check(struct objclass *oc_list1, struct objclass *oc_list2, c
struct objclass *oc_1, *oc_2;
char *description;
int debug_logging = 0;
- int rc, i, j;
+ int rc;
int repl_schema_policy;
- int found;
if (message == NULL) {
description = "";
@@ -6209,12 +6220,28 @@ schema_oc_superset_check(struct objclass *oc_list1, struct objclass *oc_list2, c
/* try to retrieve it with the name*/
oc_2 = oc_find_nolock(oc_1->oc_name, oc_list2, PR_TRUE);
}
- if (oc_2 == NULL) {
+ if (oc_2) {
+ if (schema_oc_compare(oc_1, oc_2, description) > 0) {
+ rc = 1;
+ if (debug_logging) {
+ if (replica_role == REPL_SCHEMA_AS_CONSUMER) {
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "Local %s schema objectclasses is a superset of"
+ " the received one.\n", oc_1->oc_name);
+ } else {
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "Remote %s schema objectclasses is a superset of"
+ " the received one.\n", oc_1->oc_name);
+ }
+ continue;
+ } else {
+ break;
+ }
+ }
+ } else {
slapi_log_error(SLAPI_LOG_REPL, "schema", "Fail to retrieve in the %s schema [%s or %s]\n",
description,
oc_1->oc_name,
oc_1->oc_oid);
-
+
/* The oc_1 objectclasses is supperset */
rc = 1;
if(debug_logging){
@@ -6224,25 +6251,176 @@ schema_oc_superset_check(struct objclass *oc_list1, struct objclass *oc_list2, c
break;
}
}
+ }
+ slapi_rwlock_unlock( schema_policy_lock );
+
+ return rc;
+}
+/* call must hold oc_lock at least in read */
+static struct schema_mods_indexes *
+schema_list_oc2learn(struct objclass *oc_remote_list, struct objclass *oc_local_list, int replica_role) {
+ struct objclass *oc_remote, *oc_local;
+ struct schema_mods_indexes *head = NULL, *mods_index;
+ int index = 0;
+ int repl_schema_policy;
+ char *message;
+
+ if (replica_role == REPL_SCHEMA_AS_SUPPLIER) {
+ message = "remote consumer";
+ } else {
+ message = "remote supplier";
+ }
+
+ slapi_rwlock_rdlock( schema_policy_lock );
+ for (oc_remote = oc_remote_list; oc_remote != NULL; oc_remote = oc_remote->oc_next, index++) {
+
+ /* If this objectclass is not checked (accept) or rejects schema update */
+ repl_schema_policy = schema_check_policy(replica_role, REPL_SCHEMA_OBJECTCLASS, oc_remote->oc_name, oc_remote->oc_oid);
+ if ((repl_schema_policy == REPL_SCHEMA_UPDATE_ACCEPT_VALUE) || (repl_schema_policy == REPL_SCHEMA_UPDATE_REJECT_VALUE)) {
+ continue;
+ }
+
+
+ oc_local = oc_find_nolock(oc_remote->oc_oid, oc_local_list, PR_TRUE);
+ if (oc_local == NULL) {
+ /* try to retrieve it with the name*/
+ oc_local = oc_find_nolock(oc_remote->oc_name, oc_local_list, PR_TRUE);
+ }
+ if ((oc_local == NULL) ||
+ (schema_oc_compare(oc_local, oc_remote, message) < 0)) {
+ /* This replica does not know this objectclass, It needs to be added */
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "Add that unknown/extended objectclass %s (%s)\n",
+ oc_remote->oc_name,
+ oc_remote->oc_oid);
+
+ if ((mods_index = (struct schema_mods_indexes *) slapi_ch_calloc(1, sizeof (struct schema_mods_indexes))) == NULL) {
+ slapi_log_error(SLAPI_LOG_FATAL, "schema", "Fail to Add (no memory) objectclass %s (%s)\n",
+ oc_remote->oc_name,
+ oc_remote->oc_oid);
+ continue;
+ }
+
+ /* insert it in the list */
+ mods_index->index = index;
+ mods_index->next = head;
+ mods_index->new_value = NULL;
+ head = mods_index;
+ }
+ }
+ slapi_rwlock_unlock( schema_policy_lock );
+ return head;
+}
+static struct schema_mods_indexes *
+schema_list_attr2learn(struct asyntaxinfo *at_list_local, struct asyntaxinfo *at_list_remote, int replica_role)
+{
+ struct asyntaxinfo *at_remote, *at_local;
+ struct schema_mods_indexes *head = NULL, *mods_index;
+ int index = 0;
+ int repl_schema_policy;
+ int debug_logging = 0;
+ char *message;
+
+ if (slapi_is_loglevel_set(SLAPI_LOG_REPL)) {
+ debug_logging = 1;
+ }
+
+ if (replica_role == REPL_SCHEMA_AS_SUPPLIER) {
+ message = "remote consumer";
+ } else {
+ message = "remote supplier";
+ }
+
+ slapi_rwlock_rdlock(schema_policy_lock);
+ for (at_remote = at_list_remote; at_remote != NULL; at_remote = at_remote->asi_next, index++) {
+ /* If this objectclass is not checked (accept) or rejects schema update */
+ repl_schema_policy = schema_check_policy(replica_role, REPL_SCHEMA_ATTRIBUTE, at_remote->asi_name, at_remote->asi_oid);;
+ if ((repl_schema_policy == REPL_SCHEMA_UPDATE_ACCEPT_VALUE) || (repl_schema_policy == REPL_SCHEMA_UPDATE_REJECT_VALUE)) {
+ continue;
+ }
+
+ if (((at_local = attr_syntax_find(at_remote, at_list_local)) == NULL) ||
+ (schema_at_compare(at_local, at_remote, message, debug_logging) < 0)) {
+ /* This replica does not know this attribute, It needs to be added */
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "Add that unknown/extended attribute %s (%s)\n",
+ at_remote->asi_name,
+ at_remote->asi_oid);
+
+ if ((mods_index = (struct schema_mods_indexes *) slapi_ch_calloc(1, sizeof (struct schema_mods_indexes))) == NULL) {
+ slapi_log_error(SLAPI_LOG_FATAL, "schema", "Fail to Add (no memory) attribute %s (%s)\n",
+ at_remote->asi_name,
+ at_remote->asi_oid);
+ continue;
+ }
+
+ /* insert it in the list */
+ mods_index->index = index;
+ mods_index->next = head;
+ mods_index->new_value = NULL;
+ head = mods_index;
+
+ }
+ }
+ slapi_rwlock_unlock(schema_policy_lock);
+ return head;
+}
+
+/* If oc_1 > oc2 returns 1
+ * else it returns 0
+ */
+static PRBool
+schema_oc_compare_strict(struct objclass *oc_1, struct objclass *oc_2, char *description)
+{
+ int found;
+ int i,j;
+ PRBool moved_must_to_may;
+
+ /* safety checking */
+ if (!oc_1) {
+ return 0;
+ } else if (!oc_2) {
+ return 1;
+ }
+
- /* First check the MUST */
- if (oc_1->oc_orig_required) {
- for (i = 0; oc_1->oc_orig_required[i] != NULL; i++) {
- /* For each required attribute from the remote schema check that
- * it is also required in the local schema
+ /* First check the MUST */
+ if (oc_1->oc_orig_required) {
+ for (i = 0; oc_1->oc_orig_required[i] != NULL; i++) {
+ /* For each required attribute from oc1 schema check that
+ * it is also required in the oc2 schema
+ */
+ found = 0;
+ if (oc_2->oc_orig_required) {
+ for (j = 0; oc_2->oc_orig_required[j] != NULL; j++) {
+ if (strcasecmp(oc_2->oc_orig_required[j], oc_1->oc_orig_required[i]) == 0) {
+ found = 1;
+ break;
+ }
+ }
+ }
+ if (!found) {
+ /* Before stating that oc1 is a superset of oc2, we need to verify that the 'required'
+ * attribute (from oc1) is missing in 'required' oc2 because it is
+ * now 'allowed' in oc2
*/
- found = 0;
- if (oc_2->oc_orig_required) {
- for (j = 0; oc_2->oc_orig_required[j] != NULL; j++) {
- if (strcasecmp(oc_2->oc_orig_required[j], oc_1->oc_orig_required[i]) == 0) {
- found = 1;
+ moved_must_to_may = PR_FALSE;
+ if (oc_2->oc_orig_allowed) {
+ for (j = 0; oc_2->oc_orig_allowed[j] != NULL; j++) {
+ if (strcasecmp(oc_2->oc_orig_allowed[j], oc_1->oc_orig_required[i]) == 0) {
+ moved_must_to_may = PR_TRUE;
break;
}
}
}
- if (!found) {
- /* The required attribute in the remote protocol (remote_oc->oc_orig_required[i])
- * is not required in the local protocol
+
+ if (moved_must_to_may) {
+ /* This is a special case where oc1 is actually NOT a superset of oc2 */
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "Attribute %s is no longer 'required' in '%s' of the %s schema but is now 'allowed'\n",
+ oc_1->oc_orig_required[i],
+ oc_1->oc_name,
+ description);
+ } else {
+ /* The required attribute in the oc1
+ * is not required in the oc2
*/
slapi_log_error(SLAPI_LOG_REPL, "schema", "Attribute %s is not required in '%s' of the %s schema\n",
oc_1->oc_orig_required[i],
@@ -6250,63 +6428,161 @@ schema_oc_superset_check(struct objclass *oc_list1, struct objclass *oc_list2, c
description);
/* The oc_1 objectclasses is supperset */
- rc = 1;
- if(debug_logging){
- /* we continue to check all attributes so we log what is wrong */
- continue;
- } else {
- break;
- }
+ return 1;
}
}
}
+ }
- /* Second check the MAY */
- if (oc_1->oc_orig_allowed) {
- for (i = 0; oc_1->oc_orig_allowed[i] != NULL; i++) {
- /* For each required attribute from the remote schema check that
- * it is also required in the local schema
- */
- found = 0;
- if (oc_2->oc_orig_allowed) {
- for (j = 0; oc_2->oc_orig_allowed[j] != NULL; j++) {
- if (strcasecmp(oc_2->oc_orig_allowed[j], oc_1->oc_orig_allowed[i]) == 0) {
- found = 1;
- break;
- }
+ /* Second check the MAY */
+ if (oc_1->oc_orig_allowed) {
+ for (i = 0; oc_1->oc_orig_allowed[i] != NULL; i++) {
+ /* For each required attribute from the remote schema check that
+ * it is also required in the local schema
+ */
+ found = 0;
+ if (oc_2->oc_orig_allowed) {
+ for (j = 0; oc_2->oc_orig_allowed[j] != NULL; j++) {
+ if (strcasecmp(oc_2->oc_orig_allowed[j], oc_1->oc_orig_allowed[i]) == 0) {
+ found = 1;
+ break;
}
}
- if (!found) {
- /* The required attribute in the remote protocol (remote_oc->oc_orig_allowed[i])
- * is not required in the local protocol
- */
- slapi_log_error(SLAPI_LOG_REPL, "schema", "Attribute %s is not allowed in '%s' of the %s schema\n",
- oc_1->oc_orig_allowed[i],
- oc_1->oc_name,
- description);
+ }
+ if (!found) {
+ /* The allowed attribute in the remote schema (remote_oc->oc_orig_allowed[i])
+ * is not allowed in the local schema
+ */
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "Attribute %s is not allowed in '%s' of the %s schema\n",
+ oc_1->oc_orig_allowed[i],
+ oc_1->oc_name,
+ description);
- /* The oc_1 objectclasses is superset */
- rc = 1;
- if(debug_logging){
- /* we continue to check all attributes so we log what is wrong */
- continue;
- } else {
- break;
- }
- }
+ /* The oc_1 objectclasses is superset */
+ return 1;
}
}
}
- slapi_rwlock_unlock( schema_policy_lock );
+
+
+ return 0;
+}
+
+
+/* Compare two objectclass definitions
+ * it compares:
+
+ * It returns:
+ * 1: if oc_1 is a superset of oc_2
+ * -1: if oc_2 is a superset of oc_1
+ * 0: if oc_1 and at_2 are equivalent
+ */
+static int
+schema_oc_compare(struct objclass *oc_1, struct objclass *oc_2, char *description)
+{
+ if (schema_oc_compare_strict(oc_1, oc_2, description) > 0) {
+ return 1;
+ } else if (schema_oc_compare_strict(oc_2, oc_1, description) > 0) {
+ return -1;
+ } else {
+ return 0;
+ }
+}
+
+/* Compare two attributes definitions
+ * it compares:
+ * - single/multi value
+ * - syntax
+ * - matching rules
+ * It returns:
+ * 1: if at_1 is a superset of at_2
+ * -1: if at_2 is a superset of at_1
+ * 0: if at_1 and at_2 are equivalent
+ */
+static int
+schema_at_compare(struct asyntaxinfo *at_1, struct asyntaxinfo *at_2, char *message, int debug_logging)
+{
+ char *info = NULL;
- return rc;
+ /* safety checking */
+ if (! at_1) {
+ if (!at_2) {
+ return 0;
+ } else {
+ return -1;
+ }
+ } else if (!at_2) {
+ return 1;
+ }
+
+ /*
+ * Check for single vs. multi value
+ */
+ if (!(at_1->asi_flags & SLAPI_ATTR_FLAG_SINGLE) && (at_2->asi_flags & SLAPI_ATTR_FLAG_SINGLE)) {
+
+ /* at_1 is a superset */
+ if (debug_logging) {
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "%s schema attribute [%s] is not "
+ "\"single-valued\" \n", message, at_1->asi_name);
+ }
+ return 1;
+ }
+ if ((at_1->asi_flags & SLAPI_ATTR_FLAG_SINGLE) && !(at_2->asi_flags & SLAPI_ATTR_FLAG_SINGLE)) {
+ /* at_2 is a superset */
+ if (debug_logging) {
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "%s schema attribute [%s] is not "
+ "\"single-valued\" \n", message, at_1->asi_name);
+ }
+ return -1;
+ }
+
+ /*
+ * Check the syntaxes
+ */
+ if (schema_at_superset_check_syntax_oids(at_1->asi_syntax_oid, at_2->asi_syntax_oid)) {
+ /* at_1 is a superset */
+ if (debug_logging) {
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "%s schema attribute [%s] syntax "
+ "can not be overwritten\n", message, at_1->asi_name);
+ }
+ return 1;
+ }
+ if (schema_at_superset_check_syntax_oids(at_2->asi_syntax_oid, at_1->asi_syntax_oid)) {
+ /* at_2 is a superset */
+ if (debug_logging) {
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "%s schema attribute [%s] syntax "
+ "can not be overwritten\n", message, at_2->asi_name);
+ }
+ return -1;
+ }
+
+ /*
+ * Check some matching rules - not finished yet...
+ *
+ * For now, skip the matching rule check (rc is never equal to -1)
+ */
+ if (schema_at_superset_check_mr(at_1, at_2, info)) {
+ if (debug_logging) {
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "%s schema attribute [%s] matching "
+ "rule can not be overwritten\n", message, at_1->asi_name);
+ }
+ return 1;
+ }
+ if (schema_at_superset_check_mr(at_2, at_1, info)) {
+ if (debug_logging) {
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "%s schema attribute [%s] matching "
+ "rule can not be overwritten\n", message, at_2->asi_name);
+ }
+ return -1;
+ }
+
+ return 0;
}
static int
schema_at_superset_check(struct asyntaxinfo *at_list1, struct asyntaxinfo *at_list2, char *message, int replica_role)
{
struct asyntaxinfo *at_1, *at_2;
- char *info = NULL;
int debug_logging = 0;
int repl_schema_policy;
int rc = 0;
@@ -6344,51 +6620,22 @@ schema_at_superset_check(struct asyntaxinfo *at_list1, struct asyntaxinfo *at_li
}
/* check if at_1 exists in at_list2 */
- if((at_2 = attr_syntax_find(at_1, at_list2))){
- /*
- * Check for single vs. multi value
- */
- if(!(at_1->asi_flags & SLAPI_ATTR_FLAG_SINGLE) && (at_2->asi_flags & SLAPI_ATTR_FLAG_SINGLE)){
- /* at_list 1 is a superset */
- rc = 1;
- if(debug_logging){
- slapi_log_error(SLAPI_LOG_REPL, "schema", "%s schema attribute [%s] is not "
- "\"single-valued\" \n",message, at_1->asi_name);
- continue;
- } else {
- break;
+ if((at_2 = attr_syntax_find(at_1, at_list2))) {
+ if (schema_at_compare(at_1, at_2, message, debug_logging) > 0) {
+ rc = 1;
+ if (debug_logging) {
+ if (replica_role == REPL_SCHEMA_AS_CONSUMER) {
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "Local %s schema attributetypes is a superset of"
+ " the received one.\n", at_1->asi_name);
+ } else {
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "Remote %s schema attributetypes is a superset of"
+ " the received one.\n", at_1->asi_name);
+ }
+ continue;
+ } else {
+ break;
+ }
}
- }
-
- /*
- * Check the syntaxes
- */
- if(schema_at_superset_check_syntax_oids(at_1->asi_syntax_oid, at_2->asi_syntax_oid)){
- /* at_list 1 is a superset */
- rc = 1;
- if(debug_logging){
- slapi_log_error(SLAPI_LOG_REPL, "schema", "%s schema attribute [%s] syntax "
- "can not be overwritten\n",message, at_1->asi_name);
- continue;
- } else {
- break;
- }
- }
- /*
- * Check some matching rules - not finished yet...
- *
- * For now, skip the matching rule check (rc is never equal to -1)
- */
- if(rc == -1 && schema_at_superset_check_mr(at_1, at_2, info)){
- rc = 1;
- if(debug_logging){
- slapi_log_error(SLAPI_LOG_REPL, "schema", "%s schema attribute [%s] matching "
- "rule can not be overwritten\n",message, at_1->asi_name);
- continue;
- } else {
- break;
- }
- }
} else {
rc = 1;
if(debug_logging){
@@ -6802,6 +7049,7 @@ schema_berval_to_atlist(struct berval **at_berval)
return head;
}
+
int
schema_objectclasses_superset_check(struct berval **remote_schema, char *type)
{
@@ -6827,19 +7075,19 @@ schema_objectclasses_superset_check(struct berval **remote_schema, char *type)
*/
if (remote_oc_list) {
- oc_lock_read();
+ oc_lock_read();
if (strcmp(type, OC_SUPPLIER) == 0) {
/* Check if the remote_oc_list from a consumer are or not
* a superset of the objectclasses of the local supplier schema
*/
- rc = schema_oc_superset_check(remote_oc_list, g_get_global_oc_nolock(), "local supplier", REPL_SCHEMA_AS_SUPPLIER);
+ rc = schema_oc_superset_check(remote_oc_list, g_get_global_oc_nolock(), "remote consumer", REPL_SCHEMA_AS_SUPPLIER);
} else {
/* Check if the objectclasses of the local consumer schema are or not
* a superset of the remote_oc_list from a supplier
*/
rc = schema_oc_superset_check(g_get_global_oc_nolock(), remote_oc_list, "remote supplier", REPL_SCHEMA_AS_CONSUMER);
}
-
+
oc_unlock();
}
@@ -6890,3 +7138,428 @@ schema_attributetypes_superset_check(struct berval **remote_schema, char *type)
}
return rc;
}
+
+/* Do the internal MOD and update the local "nsSchemaCSN" with a local timestamp
+ * It could be a good idea to set the 'nsSchemaCSN' with the maximum of local time and
+ * the CSN received with the remote schema
+ */
+static void
+modify_schema_internal_mod(Slapi_DN *sdn, Slapi_Mods *smods)
+{
+ Slapi_PBlock *newpb;
+ int op_result;
+ CSN *schema_csn;
+
+ /* allocate internal mod components: pblock*/
+ newpb = slapi_pblock_new();
+
+ slapi_modify_internal_set_pb_ext (
+ newpb,
+ sdn,
+ slapi_mods_get_ldapmods_byref (smods),
+ NULL, /* Controls */
+ NULL,
+ (void *)plugin_get_default_component_id(),
+ 0);
+
+ /* do modify */
+ slapi_modify_internal_pb (newpb);
+ slapi_pblock_get (newpb, SLAPI_PLUGIN_INTOP_RESULT, &op_result);
+ if (op_result == LDAP_SUCCESS) {
+ /* Update the schema csn if the operation succeeded */
+ schema_csn = csn_new();
+ if (NULL != schema_csn) {
+ csn_set_replicaid(schema_csn, 0);
+ csn_set_time(schema_csn, current_time());
+ g_set_global_schema_csn(schema_csn);
+ }
+ }
+
+ slapi_pblock_destroy(newpb);
+}
+
+/* Prepare slapi_mods for the internal mod
+ * Caller must free smods with slapi_mods_done
+ */
+static void
+modify_schema_prepare_mods(Slapi_Mods *smods, char *type, struct schema_mods_indexes *values)
+{
+ struct schema_mods_indexes *object;
+ struct berval *bv;
+ struct berval **bvps;
+ int nb_values, i;
+
+ for (object = values, nb_values = 0; object != NULL; object = object->next, nb_values++);
+ bvps = (struct berval **) slapi_ch_calloc(1, (nb_values + 1) * sizeof(struct berval *));
+
+
+ for (i = 0, object = values; object != NULL; i++, object = object->next) {
+ bv = (struct berval *) slapi_ch_malloc(sizeof(struct berval));
+ bv->bv_len = strlen(object->new_value);
+ bv->bv_val = (void*) object->new_value;
+ bvps[i] = bv;
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "MOD[%d] add (%s): %s\n", i, type, object->new_value);
+ }
+ bvps[nb_values] = NULL;
+ slapi_mods_init (smods, 2);
+ slapi_mods_add_modbvps( smods, LDAP_MOD_ADD, type, bvps );
+ for (i = 0; bvps[i] != NULL; i++) {
+ /* bv_val should not be free. It belongs to the incoming MOD */
+ slapi_ch_free((void **) &bvps[i]);
+ }
+ slapi_ch_free((void **) &bvps);
+
+}
+
+/* called by modify_schema_dse/supplier_learn_new_definitions to learn new
+ * definitions via internal mod.
+ * Internal mod is important, because we want those definitions to be updated in 99user.ldif
+ * and we are not sure that the current operation will succeeds or not.
+ */
+static void
+modify_schema_apply_new_definitions(char *attr_name, struct schema_mods_indexes *list)
+{
+ Slapi_Mods smods = {0};
+ Slapi_DN *sdn = NULL;
+
+ if (list == NULL)
+ return;
+
+ /* Then the sdn */
+ sdn = slapi_sdn_new();
+ if (!sdn) {
+ slapi_log_error( SLAPI_LOG_FATAL, "schema", "modify_schema_apply_new_definitions Out of memory \n");
+ goto done;
+ }
+ slapi_sdn_set_dn_byval(sdn, SLAPD_SCHEMA_DN);
+
+ /* prepare the mods */
+ modify_schema_prepare_mods(&smods, attr_name, list);
+
+ /* update the schema with the new attributetypes */
+ /* No need to lock the schema_dse as the internal mod will do */
+ modify_schema_internal_mod(sdn, &smods);
+
+
+done:
+ if (sdn) {
+ slapi_sdn_free(&sdn);
+ }
+ slapi_mods_done (&smods);
+}
+
+/*
+ * This routines retrieves from the remote schema (mods) the
+ * definitions (attributetypes/objectclasses), that are:
+ * - unknown from the local schema
+ * - a superset of the local definition
+ *
+ * It then builds two lists (to be freed by the caller) with those definitions.
+ * Those list contains a duplicate of the definition (string).
+ */
+static void
+modify_schema_get_new_definitions(Slapi_PBlock *pb, LDAPMod **mods, struct schema_mods_indexes **at_list, struct schema_mods_indexes **oc_list)
+{
+ struct asyntaxinfo *remote_at_list;
+ struct objclass *remote_oc_list;
+ int is_replicated_operation = 0;
+ int replace_allowed = 0;
+ slapdFrontendConfig_t *slapdFrontendConfig;
+ int i;
+ struct schema_mods_indexes *at2learn_list = NULL;
+ struct schema_mods_indexes *at2learn;
+ struct schema_mods_indexes *oc2learn_list = NULL;
+ struct schema_mods_indexes *oc2learn;
+
+
+ slapi_pblock_get(pb, SLAPI_IS_REPLICATED_OPERATION, &is_replicated_operation);
+
+ /* by default nothing to learn */
+ *at_list = NULL;
+ *oc_list = NULL;
+
+ /* We are only looking for schema received from a supplier */
+ if (!is_replicated_operation || !mods) {
+ return ;
+ }
+
+ /* Check if we are allowed to update the schema (if needed) */
+ slapdFrontendConfig = getFrontendConfig();
+ CFG_LOCK_READ(slapdFrontendConfig);
+ if ((0 == strcasecmp(slapdFrontendConfig->schemareplace, CONFIG_SCHEMAREPLACE_STR_ON)) ||
+ (0 == strcasecmp(slapdFrontendConfig->schemareplace, CONFIG_SCHEMAREPLACE_STR_REPLICATION_ONLY))) {
+ replace_allowed = 1;
+ }
+ CFG_UNLOCK_READ(slapdFrontendConfig);
+ if (!replace_allowed) {
+ return;
+ }
+
+
+ /* First retrieve unknowns attributetypes because an unknown objectclasses
+ * may be composed of unknown attributetypes
+ */
+ at2learn_list = NULL;
+ oc2learn_list = NULL;
+ schema_dse_lock_read();
+ for (i = 0; mods[i]; i++) {
+ if (SLAPI_IS_MOD_REPLACE(mods[i]->mod_op) && (mods[i]->mod_bvalues)) {
+
+ if (strcasecmp(mods[i]->mod_type, "attributetypes") == 0) {
+ /* we have some MOD_replace of attributetypes*/
+
+ /* First build an attribute list from the remote schema */
+ if ((remote_at_list = schema_berval_to_atlist(mods[i]->mod_bvalues)) == NULL) {
+ /* If we can not build an attributes list from the mods, just skip
+ * it and look for objectclasses
+ */
+ slapi_log_error(SLAPI_LOG_FATAL, "schema",
+ "Not able to build an attributes list (%s) from the schema received from the supplier\n",
+ mods[i]->mod_type);
+ continue;
+ }
+ /* Build a list of attributestype to learn from the remote definitions */
+ attr_syntax_read_lock();
+ at2learn_list = schema_list_attr2learn(attr_syntax_get_global_at(), remote_at_list, REPL_SCHEMA_AS_CONSUMER);
+ attr_syntax_unlock_read();
+
+ /* For each of them copy the value to set */
+ for (at2learn = at2learn_list; at2learn != NULL; at2learn = at2learn->next) {
+ struct berval *bv;
+ bv = mods[i]->mod_bvalues[at2learn->index]; /* takes the berval from the selected mod */
+ at2learn->new_value = (char *) slapi_ch_malloc(bv->bv_len + 1);
+ memcpy(at2learn->new_value, bv->bv_val, bv->bv_len);
+ at2learn->new_value[bv->bv_len] = '\0';
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "take attributetypes: %s\n", at2learn->new_value);
+ }
+
+ /* Free the remote schema list */
+ schema_atlist_free(remote_at_list);
+
+ } else if (strcasecmp(mods[i]->mod_type, "objectclasses") == 0) {
+ /* we have some MOD_replace of objectclasses */
+
+ /* First build an objectclass list from the remote schema */
+ if ((remote_oc_list = schema_berval_to_oclist(mods[i]->mod_bvalues)) == NULL) {
+ /* If we can not build an objectclasses list from the mods, just skip
+ * it and look for attributes
+ */
+ slapi_log_error(SLAPI_LOG_FATAL, "schema",
+ "Not able to build an objectclasses list (%s) from the schema received from the supplier\n",
+ mods[i]->mod_type);
+ continue;
+ }
+ /* Build a list of objectclasses to learn from the remote definitions */
+ oc_lock_read();
+ oc2learn_list = schema_list_oc2learn(remote_oc_list, g_get_global_oc_nolock(), REPL_SCHEMA_AS_CONSUMER);
+ oc_unlock();
+
+ /* For each of them copy the value to set */
+ for (oc2learn = oc2learn_list; oc2learn != NULL; oc2learn = oc2learn->next) {
+ struct berval *bv;
+ bv = mods[i]->mod_bvalues[oc2learn->index]; /* takes the berval from the selected mod */
+ oc2learn->new_value = (char *) slapi_ch_malloc(bv->bv_len + 1);
+ memcpy(oc2learn->new_value, bv->bv_val, bv->bv_len);
+ oc2learn->new_value[bv->bv_len] = '\0';
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "take objectclass: %s\n", oc2learn->new_value);
+ }
+
+ /* Free the remote schema list*/
+ schema_oclist_free(remote_oc_list);
+ }
+ }
+ }
+ schema_dse_unlock();
+
+ *at_list = at2learn_list;
+ *oc_list = oc2learn_list;
+}
+
+/*
+ * It evaluate if the schema in the mods, is a superset of
+ * the local schema.
+ * Called when we know the mods comes from/to a replicated session
+ * Caller must not hold schema_dse lock
+ *
+ * mods: set of mod to apply to the schema
+ * replica_role:
+ * OC_CONSUMER: means the caller is acting as a consumer (receiving a schema)
+ * OC_SUPPLIER: means the caller is acting as a supplier (sending a schema)
+ *
+ * It returns:
+ * - PR_TRUE: if replicated schema is a superset of local schema
+ * - PR_FALSE: if local schema is a superset of local schema
+ */
+static PRBool
+check_replicated_schema(LDAPMod **mods, char *replica_role, char **attr_name)
+{
+ int i;
+ PRBool rc = PR_TRUE;
+
+ schema_dse_lock_read();
+ for (i = 0; mods[i]; i++) {
+ if ((SLAPI_IS_MOD_REPLACE(mods[i]->mod_op)) && strcasecmp (mods[i]->mod_type, "attributetypes") == 0) {
+ if (schema_attributetypes_superset_check(mods[i]->mod_bvalues, replica_role)) {
+ rc = PR_FALSE;
+ *attr_name = mods[i]->mod_type;
+ break;
+ }
+ } else if ((SLAPI_IS_MOD_REPLACE(mods[i]->mod_op)) && strcasecmp (mods[i]->mod_type, "objectclasses") == 0) {
+ if (schema_objectclasses_superset_check(mods[i]->mod_bvalues, replica_role)) {
+ rc = PR_FALSE;
+ *attr_name = mods[i]->mod_type;
+ break;
+ }
+
+ }
+
+ }
+ schema_dse_unlock();
+
+ return rc;
+}
+
+/* Free the list of definitions allocated in modify_schema_get_new_definitions/supplier_get_new_definitions */
+static void
+modify_schema_free_new_definitions(struct schema_mods_indexes *def_list)
+{
+ struct schema_mods_indexes *def, *head;
+
+ for (head = def_list; head != NULL; ) {
+ def = head;
+ head = head->next;
+
+ /* Free the string definition that was copied from the berval */
+ if (def->new_value) {
+ slapi_ch_free((void**) &def->new_value);
+ }
+
+ /* Then the definition cell */
+ slapi_ch_free((void **) &def);
+ }
+}
+
+/* This functions is called by a supplier.
+ * It builds lists of definitions (attributetypes/objectclasses) to learn
+ * objectclasses: received objectclass definitions
+ * attributetypes: received attribute definitions
+ * new_oc: list of definitions to learn (list should be freed by the caller)
+ * new_at: list of definitions to learn (list should be freed by the caller)
+ */
+
+static void
+supplier_get_new_definitions(struct berval **objectclasses, struct berval **attributetypes, struct schema_mods_indexes **new_oc, struct schema_mods_indexes **new_at)
+{
+ int replace_allowed = 0;
+ slapdFrontendConfig_t *slapdFrontendConfig;
+ struct asyntaxinfo *remote_at_list;
+ struct objclass *remote_oc_list;
+ struct schema_mods_indexes *at2learn_list = NULL;
+ struct schema_mods_indexes *at2learn;
+ struct schema_mods_indexes *oc2learn_list = NULL;
+ struct schema_mods_indexes *oc2learn;
+
+ *new_oc = NULL;
+ *new_at = NULL;
+
+ if ((objectclasses == NULL) && (attributetypes == NULL)) {
+ return;
+ }
+ /* Check if we are allowed to update the schema (if needed) */
+ slapdFrontendConfig = getFrontendConfig();
+ CFG_LOCK_READ(slapdFrontendConfig);
+ if ((0 == strcasecmp(slapdFrontendConfig->schemareplace, CONFIG_SCHEMAREPLACE_STR_ON)) ||
+ (0 == strcasecmp(slapdFrontendConfig->schemareplace, CONFIG_SCHEMAREPLACE_STR_REPLICATION_ONLY))) {
+ replace_allowed = 1;
+ }
+ CFG_UNLOCK_READ(slapdFrontendConfig);
+ if (!replace_allowed) {
+ return;
+ }
+
+ schema_dse_lock_read();
+ /*
+ * Build the list of objectclasses
+ */
+ /* from berval to objclass more convenient to compare */
+ if ((remote_oc_list = schema_berval_to_oclist(objectclasses)) != NULL) {
+ /* Build a list of objectclasses to learn from the remote definitions */
+ oc_lock_read();
+ oc2learn_list = schema_list_oc2learn(remote_oc_list, g_get_global_oc_nolock(), REPL_SCHEMA_AS_SUPPLIER);
+ oc_unlock();
+
+ /* For each of them copy the value to set */
+ for (oc2learn = oc2learn_list; oc2learn != NULL; oc2learn = oc2learn->next) {
+ struct berval *bv;
+ bv = objectclasses[oc2learn->index]; /* takes the berval from the selected objectclass */
+ oc2learn->new_value = (char *) slapi_ch_malloc(bv->bv_len + 1);
+ memcpy(oc2learn->new_value, bv->bv_val, bv->bv_len);
+ oc2learn->new_value[bv->bv_len] = '\0';
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "supplier takes objectclass: %s\n", oc2learn->new_value);
+ }
+
+ /* Free the remote schema list*/
+ schema_oclist_free(remote_oc_list);
+ } else {
+ /* If we can not build an objectclasses list */
+ slapi_log_error(SLAPI_LOG_FATAL, "schema",
+ "Not able to build an objectclasses list from the consumer schema\n");
+ }
+
+
+ /*
+ * Build the list of attributetypes
+ */
+ /* First build an attribute list from the remote schema */
+ if ((remote_at_list = schema_berval_to_atlist(attributetypes)) != NULL) {
+ /* Build a list of attributestype to learn from the remote definitions */
+ attr_syntax_read_lock();
+ at2learn_list = schema_list_attr2learn(attr_syntax_get_global_at(), remote_at_list, REPL_SCHEMA_AS_SUPPLIER);
+ attr_syntax_unlock_read();
+
+ /* For each of them copy the value to set */
+ for (at2learn = at2learn_list; at2learn != NULL; at2learn = at2learn->next) {
+ struct berval *bv;
+ bv = attributetypes[at2learn->index]; /* takes the berval from the selected mod */
+ at2learn->new_value = (char *) slapi_ch_malloc(bv->bv_len + 1);
+ memcpy(at2learn->new_value, bv->bv_val, bv->bv_len);
+ at2learn->new_value[bv->bv_len] = '\0';
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "supplier takes attributetypes: %s\n", at2learn->new_value);
+ }
+
+ /* Free the remote schema list */
+ schema_atlist_free(remote_at_list);
+ } else {
+ /* If we can not build an attributes list from the mods, just skip
+ * it and look for objectclasses
+ */
+ slapi_log_error(SLAPI_LOG_FATAL, "schema",
+ "Not able to build an attributes list from the consumer schema");
+ }
+ schema_dse_unlock();
+ *new_oc = oc2learn_list;
+ *new_at = at2learn_list;
+
+}
+
+/* This functions is called by a supplier when it detects new definitions (objectclasses/attributetypes)
+ * or extension of existing definitions in a consumer schema.
+ * This function, build lists of definitions to "learn" and add those definitions in the schema and 99user.ldif
+ */
+void
+supplier_learn_new_definitions(struct berval **objectclasses, struct berval **attributetypes)
+{
+ struct schema_mods_indexes *oc_list = NULL;
+ struct schema_mods_indexes *at_list = NULL;
+
+ supplier_get_new_definitions(objectclasses, attributetypes, &oc_list, &at_list);
+ if (at_list) {
+ modify_schema_apply_new_definitions("attributetypes", at_list);
+ }
+ if (oc_list) {
+ modify_schema_apply_new_definitions("objectclasses", oc_list);
+ }
+ /* No need to hold the lock for these list that are local */
+ modify_schema_free_new_definitions(at_list);
+ modify_schema_free_new_definitions(oc_list);
+}
dirsrvtests/tickets/ticket47676_test.py | 485 ++++++++++++++++++++++++++
ldap/schema/01core389.ldif | 5
ldap/servers/plugins/replication/repl5_init.c | 98 +++++
ldap/servers/slapd/schema.c | 304 +++++++++++++++-
ldap/servers/slapd/slap.h | 36 +
ldap/servers/slapd/slapi-private.h | 3
6 files changed, 922 insertions(+), 9 deletions(-)
New commits:
commit e35d20194b415167ec0fbb0f0ec62e39e3808a32
Author: Thierry bordaz (tbordaz) <tbordaz(a)redhat.com>
Date: Tue Feb 4 11:50:45 2014 +0100
Ticket 47676 : Replication of the schema fails 'master branch' -> 1.2.11 or 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:
Replication plugin initialization (multimaster_start) creates the three following entries
dn: cn=replSchema,cn=config
objectClass: top
objectClass: nsSchemaPolicy
cn: replSchema
dn: cn=consumerUpdatePolicy,cn=replSchema,cn=config
objectClass: top
objectClass: nsSchemaPolicy
cn: consumerUpdatePolicy
schemaUpdateObjectclassAccept: printer-uri-oid
schemaUpdateAttributeAccept: 2.16.840.1.113730.3.1.2110
schemaUpdateObjectclassReject: dummy-objectclass-name
schemaUpdateAttributeReject: dummy-attribute-name
dn: cn=supplierUpdatePolicy,cn=replSchema,cn=config
objectClass: top
objectClass: nsSchemaPolicy
cn: supplierUpdatePolicy
schemaUpdateObjectclassAccept: printer-uri-oid
schemaUpdateAttributeAccept: 2.16.840.1.113730.3.1.2110
schemaUpdateObjectclassReject: dummy-objectclass-name
schemaUpdateAttributeReject: dummy-attribute-name
schemaUpdateObjectclassAccept, schemaUpdateAttributeAccept, schemaUpdateObjectclassReject and schemaUpdateAttributeReject
are optional multi-valued attribute.
The values are strings representing objectclass/attribute name or OID.
During a replication session, if the consumer schema needs to be updated (because nsSchemaCSN differs) the checkings are:
On supplier side:
OBJECTCLASSES
For each objectclass OC in the consumer schema:
if it exists in 'cn=supplierUpdatePolicy,cn=replSchema,cn=config'
schemaUpdateObjectclassAccept: <oc_name or oc_oid>
then this OC is "accepted" without checking that
- OC exists in supplier schema
- supplier's OC >= consumer's OC
schemaUpdateObjectclassReject: <oc_name or oc_oid>
then the supplier schema is not pushed (rejected)
If none of these values exists, then it does the "normal" processing to determine if the schema can be updated:
(if Supplier's OC < consumer's OC then schema is not pushed)
ATTRIBUTES
It does the same processing as above for each attribute, looking for values
schemaUpdateAttributeAccept and schemaUpdateAttributeReject
On consumer side:
OBJECTCLASSES
It does the same processing as above for each OC in the supplier schema, checking against
entry 'cn=consumerUpdatePolicy,cn=replSchema,cn=config'
ATTRIBUTES
It does the same processing as above for each AT in the supplier schema, checking against
entry 'cn=consumerUpdatePolicy,cn=replSchema,cn=config'
https://fedorahosted.org/389/ticket/47676
Reviewed by: Rich Megginson (Thanks Rich!)
Platforms tested: F17/F19(jenkins)
Flag Day: no
Doc impact: no
diff --git a/dirsrvtests/tickets/ticket47676_test.py b/dirsrvtests/tickets/ticket47676_test.py
new file mode 100644
index 0000000..8ba5956
--- /dev/null
+++ b/dirsrvtests/tickets/ticket47676_test.py
@@ -0,0 +1,485 @@
+'''
+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
+
+SCHEMA_DN = "cn=schema"
+TEST_REPL_DN = "cn=test_repl, %s" % SUFFIX
+OC_NAME = 'OCticket47676'
+OC_OID_EXT = 2
+MUST = "(postalAddress $ postalCode)"
+MAY = "(member $ street)"
+
+OC2_NAME = 'OC2ticket47676'
+OC2_OID_EXT = 3
+MUST_2 = "(postalAddress $ postalCode)"
+MAY_2 = "(member $ street)"
+
+REPL_SCHEMA_POLICY_CONSUMER = "cn=consumerUpdatePolicy,cn=replSchema,cn=config"
+REPL_SCHEMA_POLICY_SUPPLIER = "cn=supplierUpdatePolicy,cn=replSchema,cn=config"
+
+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
+
+BASE_OID = "1.2.3.4.5.6.7.8.9.10"
+
+def _oc_definition(oid_ext, name, must=None, may=None):
+ oid = "%s.%d" % (BASE_OID, 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
+
+
+(a)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(OC_OID_EXT, 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_reject_action(topology):
+
+ topology.master1.log.info("\n\n######################### REJECT ACTION ######################\n")
+
+ topology.master1.simple_bind_s(DN_DM, PASSWORD)
+ topology.master2.simple_bind_s(DN_DM, PASSWORD)
+
+ # make master1 to refuse to push the schema if OC_NAME is present in consumer schema
+ mod = [(ldap.MOD_ADD, 'schemaUpdateObjectclassReject', '%s' % (OC_NAME) )] # ACL + REPL
+ topology.master1.modify_s(REPL_SCHEMA_POLICY_SUPPLIER, mod)
+
+ # Restart is required to take into account that policy
+ topology.master1.stop(timeout=10)
+ topology.master1.start(timeout=10)
+
+ # Add a new OC on M1 so that schema CSN will change and M1 will try to push the schema
+ topology.master1.log.info("Add %s on M1" % OC2_NAME)
+ new_oc = _oc_definition(OC2_OID_EXT, OC2_NAME, must = MUST, may = MAY)
+ topology.master1.addSchema('objectClasses', new_oc)
+
+ # Safety checking that the schema has been updated on M1
+ topology.master1.log.info("Check %s is in M1" % OC2_NAME)
+ ent = topology.master1.getEntry(SCHEMA_DN, ldap.SCOPE_BASE, "(objectclass=*)", ["objectclasses"])
+ assert ent.hasAttr('objectclasses')
+ found = False
+ for objectclass in ent.getValues('objectclasses'):
+ if str(objectclass).find(OC2_NAME) >= 0:
+ found = True
+ break
+ assert found
+
+ # Do an update of M1 so that M1 will try to push the schema
+ topology.master1.log.info("Update %s on M1" % ENTRY_DN)
+ mod = [(ldap.MOD_REPLACE, 'description', 'test_reject')]
+ topology.master1.modify_s(ENTRY_DN, mod)
+
+ # Check the replication occured and so also M1 attempted to push the schema
+ topology.master1.log.info("Check updated %s on M2" % ENTRY_DN)
+ loop = 0
+ while loop <= 10:
+ ent = topology.master2.getEntry(ENTRY_DN, ldap.SCOPE_BASE, "(objectclass=*)", ['description'])
+ if ent.hasAttr('description') and ent.getValue('description') == 'test_reject':
+ # update was replicated
+ break
+ time.sleep(2)
+ loop += 1
+ assert loop <= 10
+
+ # Check that the schema has not been pushed
+ topology.master1.log.info("Check %s is not in M2" % OC2_NAME)
+ ent = topology.master2.getEntry(SCHEMA_DN, ldap.SCOPE_BASE, "(objectclass=*)", ["objectclasses"])
+ assert ent.hasAttr('objectclasses')
+ found = False
+ for objectclass in ent.getValues('objectclasses'):
+ if str(objectclass).find(OC2_NAME) >= 0:
+ found = True
+ break
+ assert not found
+
+ topology.master1.log.info("\n\n######################### NO MORE REJECT ACTION ######################\n")
+
+ # make master1 to do no specific action on OC_NAME
+ mod = [(ldap.MOD_DELETE, 'schemaUpdateObjectclassReject', '%s' % (OC_NAME) )] # ACL + REPL
+ topology.master1.modify_s(REPL_SCHEMA_POLICY_SUPPLIER, mod)
+
+ # Restart is required to take into account that policy
+ topology.master1.stop(timeout=10)
+ topology.master1.start(timeout=10)
+
+ # Do an update of M1 so that M1 will try to push the schema
+ topology.master1.log.info("Update %s on M1" % ENTRY_DN)
+ mod = [(ldap.MOD_REPLACE, 'description', 'test_no_more_reject')]
+ topology.master1.modify_s(ENTRY_DN, mod)
+
+ # Check the replication occured and so also M1 attempted to push the schema
+ topology.master1.log.info("Check updated %s on M2" % ENTRY_DN)
+ loop = 0
+ while loop <= 10:
+ ent = topology.master2.getEntry(ENTRY_DN, ldap.SCOPE_BASE, "(objectclass=*)", ['description'])
+ if ent.hasAttr('description') and ent.getValue('description') == 'test_no_more_reject':
+ # update was replicated
+ break
+ time.sleep(2)
+ loop += 1
+ assert loop <= 10
+
+ # Check that the schema has been pushed
+ topology.master1.log.info("Check %s is in M2" % OC2_NAME)
+ ent = topology.master2.getEntry(SCHEMA_DN, ldap.SCOPE_BASE, "(objectclass=*)", ["objectclasses"])
+ assert ent.hasAttr('objectclasses')
+ found = False
+ for objectclass in ent.getValues('objectclasses'):
+ if str(objectclass).find(OC2_NAME) >= 0:
+ found = True
+ break
+ assert found
+
+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)
+ topo.master1.log.info("\n\n######################### Ticket 47676 ######################\n")
+ test_ticket47676_init(topo)
+
+ test_ticket47676_skip_oc_at(topo)
+ test_ticket47676_reject_action(topo)
+
+ test_ticket47676_final(topo)
+
+
+
+
+if __name__ == '__main__':
+ run_isolated()
+
diff --git a/ldap/schema/01core389.ldif b/ldap/schema/01core389.ldif
index b9baae7..85b1860 100644
--- a/ldap/schema/01core389.ldif
+++ b/ldap/schema/01core389.ldif
@@ -154,6 +154,10 @@ attributeTypes: ( 2.16.840.1.113730.3.1.2152 NAME 'nsds5ReplicaProtocolTimeout'
attributeTypes: ( 2.16.840.1.113730.3.1.2154 NAME 'nsds5ReplicaBackoffMin' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
attributeTypes: ( 2.16.840.1.113730.3.1.2155 NAME 'nsds5ReplicaBackoffMax' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
attributeTypes: ( 2.16.840.1.113730.3.1.2156 NAME 'nsslapd-sasl-max-buffer-size' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
+attributeTypes: ( 2.16.840.1.113730.3.1.2165 NAME 'schemaUpdateObjectclassAccept' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Netscape Directory Server' )
+attributeTypes: ( 2.16.840.1.113730.3.1.2166 NAME 'schemaUpdateObjectclassReject' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Netscape Directory Server' )
+attributeTypes: ( 2.16.840.1.113730.3.1.2167 NAME 'schemaUpdateAttributeAccept' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Netscape Directory Server' )
+attributeTypes: ( 2.16.840.1.113730.3.1.2168 NAME 'schemaUpdateAttributeReject' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'Netscape Directory Server' )
#
# objectclasses
#
@@ -165,6 +169,7 @@ objectClasses: ( 2.16.840.1.113730.3.2.110 NAME 'nsMappingTree' DESC 'Netscape d
objectClasses: ( 2.16.840.1.113730.3.2.104 NAME 'nsContainer' DESC 'Netscape defined objectclass' SUP top MUST ( CN ) X-ORIGIN 'Netscape Directory Server' )
objectClasses: ( 2.16.840.1.113730.3.2.108 NAME 'nsDS5Replica' DESC 'Netscape defined objectclass' SUP top MUST ( nsDS5ReplicaRoot $ nsDS5ReplicaId ) MAY (cn $ nsds5ReplicaCleanRUV $ nsds5ReplicaAbortCleanRUV $ nsDS5ReplicaType $ nsDS5ReplicaBindDN $ nsState $ nsDS5ReplicaName $ nsDS5Flags $ nsDS5Task $ nsDS5ReplicaReferral $ nsDS5ReplicaAutoReferral $ nsds5ReplicaPurgeDelay $ nsds5ReplicaTombstonePurgeInterval $ nsds5ReplicaChangeCount $ nsds5ReplicaLegacyConsumer $ nsds5ReplicaProtocolTimeout $ nsds5ReplicaBackoffMin $ nsds5ReplicaBackoffMax ) X-ORIGIN 'Netscape Directory Server' )
objectClasses: ( 2.16.840.1.113730.3.2.113 NAME 'nsTombstone' DESC 'Netscape defined objectclass' SUP top MAY ( nsParentUniqueId $ nscpEntryDN ) X-ORIGIN 'Netscape Directory Server' )
+objectClasses: ( 2.16.840.1.113730.3.2.115 NAME 'nsSchemaPolicy' DESC 'Netscape defined objectclass' SUP top MAY ( cn $ schemaUpdateObjectclassAccept $ schemaUpdateObjectclassReject $ schemaUpdateAttributeAccept $ schemaUpdateAttributeReject) X-ORIGIN 'Netscape Directory Server' )
objectClasses: ( 2.16.840.1.113730.3.2.103 NAME 'nsDS5ReplicationAgreement' DESC 'Netscape defined objectclass' SUP top MUST ( cn ) MAY ( nsds5ReplicaCleanRUVNotified $ nsDS5ReplicaHost $ nsDS5ReplicaPort $ nsDS5ReplicaTransportInfo $ nsDS5ReplicaBindDN $ nsDS5ReplicaCredentials $ nsDS5ReplicaBindMethod $ nsDS5ReplicaRoot $ nsDS5ReplicatedAttributeList $ nsDS5ReplicatedAttributeListTotal $ nsDS5ReplicaUpdateSchedule $ nsds5BeginReplicaRefresh $ description $ nsds50ruv $ nsruvReplicaLastModified $ nsds5ReplicaTimeout $ nsds5replicaChangesSentSinceStartup $ nsds5replicaLastUpdateEnd $ nsds5replicaLastUpdateStart $ nsds5replicaLastUpdateStatus $ nsds5replicaUpdateInProgress $ nsds5replicaLastInitEnd $ nsds5ReplicaEnabled $ nsds5replicaLastInitStart $ nsds5replicaLastInitStatus $ nsds5debugreplicatimeout $ nsds5replicaBusyWaitTime $ nsds5ReplicaStripAttrs $ nsds5replicaSessionPauseTime $ nsds5ReplicaProtocolTimeout ) X-ORIGIN 'Netscape Directory Server' )
objectClasses: ( 2.16.840.1.113730.3.2.39 NAME 'nsslapdConfig' DESC 'Netscape defined objectclass' SUP top MAY ( cn ) X-ORIGIN 'Netscape Directory Server' )
objectClasses: ( 2.16.840.1.113730.3.2.317 NAME 'nsSaslMapping' DESC 'Netscape defined objectclass' SUP top MUST ( cn $ nsSaslMapRegexString $ nsSaslMapBaseDNTemplate $ nsSaslMapFilterTemplate ) MAY ( nsSaslMapPriority ) X-ORIGIN 'Netscape Directory Server' )
diff --git a/ldap/servers/plugins/replication/repl5_init.c b/ldap/servers/plugins/replication/repl5_init.c
index ee923c9..56f01a1 100644
--- a/ldap/servers/plugins/replication/repl5_init.c
+++ b/ldap/servers/plugins/replication/repl5_init.c
@@ -644,7 +644,100 @@ check_for_ldif_dump(Slapi_PBlock *pb)
}
return return_value;
}
-
+/*
+ * If the entries do not exist, it create the entries of the schema replication policies
+ * returns 0 if success
+ */
+static int
+create_repl_schema_policy()
+{
+ /* DN part of this entry_string: no need to be optimized. */
+ char entry_string[1024];
+ Slapi_PBlock *pb;
+ Slapi_Entry *e ;
+ int return_value;
+ char *repl_schema_top, *repl_schema_supplier, *repl_schema_consumer;
+ char *default_supplier_policy = NULL;
+ char *default_consumer_policy = NULL;
+ int rc = 0;
+
+ slapi_schema_get_repl_entries(&repl_schema_top, &repl_schema_supplier, &repl_schema_consumer, &default_supplier_policy, &default_consumer_policy);
+
+ /* Create cn=replSchema,cn=config */
+ PR_snprintf(entry_string, sizeof(entry_string), "dn: %s\nobjectclass: top\nobjectclass: nsSchemaPolicy\ncn: replSchema\n", repl_schema_top);
+ e = slapi_str2entry(entry_string, 0);
+ pb = slapi_pblock_new();
+ slapi_add_entry_internal_set_pb(pb, e, NULL, /* controls */
+ repl_get_plugin_identity(PLUGIN_MULTIMASTER_REPLICATION), 0 /* flags */);
+ slapi_add_internal_pb(pb);
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &return_value);
+ if (return_value != LDAP_SUCCESS && return_value != LDAP_ALREADY_EXISTS) {
+ slapi_log_error(SLAPI_LOG_FATAL, repl_plugin_name, "Warning: unable to "
+ "create configuration entry %s: %s\n", repl_schema_top,
+ ldap_err2string(return_value));
+ rc = -1;
+ slapi_entry_free (e); /* The entry was not consumed */
+ goto done;
+ }
+ slapi_pblock_destroy(pb);
+
+ /* Create cn=supplierUpdatePolicy,cn=replSchema,cn=config */
+ PR_snprintf(entry_string, sizeof(entry_string), "dn: %s\nobjectclass: top\nobjectclass: nsSchemaPolicy\ncn: supplierUpdatePolicy\n%s",
+ repl_schema_supplier,
+ default_supplier_policy ? default_supplier_policy : "");
+ e = slapi_str2entry(entry_string, 0);
+ pb = slapi_pblock_new();
+ slapi_add_entry_internal_set_pb(pb, e, NULL, /* controls */
+ repl_get_plugin_identity(PLUGIN_MULTIMASTER_REPLICATION), 0 /* flags */);
+ slapi_add_internal_pb(pb);
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &return_value);
+ if (return_value != LDAP_SUCCESS && return_value != LDAP_ALREADY_EXISTS) {
+ slapi_log_error(SLAPI_LOG_FATAL, repl_plugin_name, "Warning: unable to "
+ "create configuration entry %s: %s\n", repl_schema_supplier,
+ ldap_err2string(return_value));
+ rc = -1;
+ slapi_entry_free(e); /* The entry was not consumed */
+ goto done;
+ }
+ slapi_pblock_destroy(pb);
+
+ /* Create cn=consumerUpdatePolicy,cn=replSchema,cn=config */
+ PR_snprintf(entry_string, sizeof(entry_string), "dn: %s\nobjectclass: top\nobjectclass: nsSchemaPolicy\ncn: consumerUpdatePolicy\n%s",
+ repl_schema_consumer,
+ default_consumer_policy ? default_consumer_policy : "");
+ e = slapi_str2entry(entry_string, 0);
+ pb = slapi_pblock_new();
+ slapi_add_entry_internal_set_pb(pb, e, NULL, /* controls */
+ repl_get_plugin_identity(PLUGIN_MULTIMASTER_REPLICATION), 0 /* flags */);
+ slapi_add_internal_pb(pb);
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &return_value);
+ if (return_value != LDAP_SUCCESS && return_value != LDAP_ALREADY_EXISTS) {
+ slapi_log_error(SLAPI_LOG_FATAL, repl_plugin_name, "Warning: unable to "
+ "create configuration entry %s: %s\n", repl_schema_consumer,
+ ldap_err2string(return_value));
+ rc = -1;
+ slapi_entry_free(e); /* The entry was not consumed */
+ goto done;
+ }
+ slapi_pblock_destroy(pb);
+ pb = NULL;
+
+
+
+ /* Load the policies of the schema replication */
+ if (slapi_schema_load_repl_policies()) {
+ slapi_log_error(SLAPI_LOG_FATAL, repl_plugin_name, "Warning: unable to "
+ "load the schema replication policies\n");
+ rc = -1;
+ goto done;
+ }
+done:
+ if (pb) {
+ slapi_pblock_destroy(pb);
+ pb = NULL;
+ }
+ return rc;
+}
static PRBool is_ldif_dump = PR_FALSE;
@@ -715,6 +808,9 @@ multimaster_start( Slapi_PBlock *pb )
if (rc != 0)
goto out;
}
+ rc = create_repl_schema_policy();
+ if (rc != 0)
+ goto out;
/* check if the replica's data was reloaded offline and we need
to reinitialize replica's changelog. This should be done
diff --git a/ldap/servers/slapd/schema.c b/ldap/servers/slapd/schema.c
index d7eed74..bd0e006 100644
--- a/ldap/servers/slapd/schema.c
+++ b/ldap/servers/slapd/schema.c
@@ -86,6 +86,55 @@ static char *schema_user_defined_origin[] = {
NULL
};
+/* The policies for the replication of the schema are
+ * - base policy
+ * - extended policies
+ * Those policies are enforced when the server is acting as a supplier and
+ * when it is acting as a consumer
+ *
+ * Base policy:
+ * Supplier: before pushing the schema, the supplier checks that each objectclass/attribute of
+ * the consumer schema is a subset of the objectclass/attribute of the supplier schema
+ * Consumer: before accepting a schema (from replication), the consumer checks that
+ * each objectclass/attribute of the consumer schema is a subset of the objectclass/attribute
+ * of the supplier schema
+ * Extended policies:
+ * They are stored in repl_schema_policy_t and specifies an "action" to be taken
+ * for specific objectclass/attribute.
+ * Supplier: extended policies are stored in entry "cn=supplierUpdatePolicy,cn=replSchema,cn=config"
+ * and uploaded in static variable: supplier_policy
+ * Before pushing the schema, for each objectclass/attribute defined in supplier_policy:
+ * if its "action" is REPL_SCHEMA_UPDATE_ACCEPT_VALUE, it is not checked that the
+ * attribute/objectclass of the consumer is a subset of the attribute/objectclass
+ * of the supplier schema.
+ *
+ * if its "action" is REPL_SCHEMA_UPDATE_REJECT_VALUE and the consumer schema contains
+ * attribute/objectclass, then schema is not pushed
+ *
+ * Consumer: extended policies are stored in entry "cn=consumerUpdatePolicy,cn=replSchema,cn=config"
+ * and uploaded in static variable: consumer_policy
+ * before accepting a schema (from replication), for each objectclass/attribute defined in
+ * consumer_policy:
+ * if its "action" is REPL_SCHEMA_UPDATE_ACCEPT_VALUE, it is not checked that the
+ * attribute/objectclass of the consumer is a subset of the attribute/objectclass
+ * of the supplier schema.
+ *
+ * if its "action" is REPL_SCHEMA_UPDATE_REJECT_VALUE and the consumer schema contains
+ * attribute/objectclass, then schema is not accepted
+ *
+ */
+
+typedef struct schema_item {
+ int action; /* REPL_SCHEMA_UPDATE_ACCEPT_VALUE or REPL_SCHEMA_UPDATE_REJECT_VALUE */
+ char *name_or_oid;
+ struct schema_item *next;
+} schema_item_t;
+
+typedef struct repl_schema_policy {
+ schema_item_t *objectclasses;
+ schema_item_t *attributes;
+} repl_schema_policy_t;
+
/*
* pschemadse is based on the general implementation in dse
*/
@@ -145,7 +194,7 @@ static void schema_create_errormsg( char *errorbuf, size_t errorbufsize,
#else
;
#endif
-static int schema_at_superset_check(struct asyntaxinfo *at_list1, struct asyntaxinfo *at_list2, char *message);
+static int schema_at_superset_check(struct asyntaxinfo *at_list1, struct asyntaxinfo *at_list2, char *message, int replica_role);
static int schema_at_superset_check_syntax_oids(char *oid1, char *oid2);
static int schema_at_superset_check_mr(struct asyntaxinfo *a1, struct asyntaxinfo *a2, char *info);
static int parse_at_str(const char *input, struct asyntaxinfo **asipp, char *errorbuf, size_t errorbufsize,
@@ -200,6 +249,13 @@ static const char *schema_errprefix_oc = "object class %s: ";
static const char *schema_errprefix_at = "attribute type %s: ";
static const char *schema_errprefix_generic = "%s: ";
+/* Defined the policies for the replication of the schema */
+static repl_schema_policy_t supplier_policy = {0};
+static repl_schema_policy_t consumer_policy = {0};
+static Slapi_RWLock *schema_policy_lock = NULL;
+static int schema_check_policy(int replica_role, int schema_item, char *name, char *oid);
+static void schema_load_repl_policy(const char *dn, repl_schema_policy_t *replica);
+
/*
* A "cached" copy of the "ignore trailing spaces" config. setting.
@@ -256,6 +312,191 @@ schema_dse_mandatory_init( void )
}
+void
+slapi_schema_get_repl_entries(char **repl_schema_top, char ** repl_schema_supplier, char **repl_schema_consumer, char **default_supplier_policy, char **default_consumer_policy)
+{
+ *repl_schema_top = ENTRY_REPL_SCHEMA_TOP;
+ *repl_schema_supplier = ENTRY_REPL_SCHEMA_SUPPLIER;
+ *repl_schema_consumer = ENTRY_REPL_SCHEMA_CONSUMER;
+ *default_supplier_policy = DEFAULT_SUPPLIER_POLICY;
+ *default_consumer_policy = DEFAULT_CONSUMER_POLICY;
+}
+
+/* It gets the attributes (see attrName)values in the entry, and add
+ * the policies in the provided list
+ *
+ * Entry: Slapi_entry with DN being ENTRY_REPL_SCHEMA_SUPPLIER or ENTRY_REPL_SCHEMA_CONSUMER
+ * attrName: name defining the policy object (objectclass/attribute) and the action
+ * ATTR_SCHEMA_UPDATE_OBJECTCLASS_ACCEPT
+ * ATTR_SCHEMA_UPDATE_OBJECTCLASS_REJECT
+ * ATTR_SCHEMA_UPDATE_ATTRIBUTE_ACCEPT
+ * ATTR_SCHEMA_UPDATE_ATTRIBUTE_REJECT
+ * *list: is the list of schema_item_t containing the policies (it can be list of objectclasses or attributes)
+ *
+ */
+static
+void schema_policy_add_action(Slapi_Entry *entry, char *attrName, schema_item_t **list)
+{
+ Slapi_Attr *attr = NULL;
+ schema_item_t *schema_item;
+ char *value;
+ int action;
+
+ /* Retrieve the expected action from the attribute name */
+ if ((strcasecmp(attrName, ATTR_SCHEMA_UPDATE_OBJECTCLASS_ACCEPT) == 0) ||
+ (strcasecmp(attrName, ATTR_SCHEMA_UPDATE_ATTRIBUTE_ACCEPT) == 0)) {
+ action = REPL_SCHEMA_UPDATE_ACCEPT_VALUE;
+ } else {
+ action = REPL_SCHEMA_UPDATE_REJECT_VALUE;
+ }
+
+ /* Retrieve the given attribute from the entry */
+ slapi_entry_attr_find(entry, attrName, &attr);
+ if (attr != NULL) {
+ Slapi_Value *sval = NULL;
+ const struct berval *attrVal = NULL;
+ int k = slapi_attr_first_value(attr, &sval);
+
+ /* For each value adds the policy in the list */
+ while (k != -1) {
+ attrVal = slapi_value_get_berval(sval);
+
+ schema_item = (schema_item_t *) slapi_ch_calloc(1, sizeof(schema_item_t));
+
+ /* Get the schema name_or_oid */
+ value = (char *) slapi_ch_malloc(attrVal->bv_len + 1);
+ memcpy(value, attrVal->bv_val, attrVal->bv_len);
+ value[attrVal->bv_len] = '\0';
+ schema_item->name_or_oid = value;
+
+ /* Set the action on that item */
+ schema_item->action = action;
+
+ /* Add it on the head of the list */
+ schema_item->next = *list;
+ *list = schema_item;
+
+ /* Get the next name_or_oid */
+ k = slapi_attr_next_value(attr, k, &sval);
+ }
+ }
+}
+
+/* Caller must hold schema_policy_lock in write */
+static void
+schema_load_repl_policy(const char *dn, repl_schema_policy_t *replica)
+{
+ Slapi_DN sdn;
+ Slapi_Entry *entry = NULL;
+ schema_item_t *schema_item, *next;
+
+ if (replica == NULL) {
+ return;
+ }
+
+ /* Start to free the previous policy */
+ /* first the objectclasses policies */
+ for (schema_item = replica->objectclasses; schema_item; ) {
+ slapi_ch_free((void **) &schema_item->name_or_oid);
+ next = schema_item->next;
+ slapi_ch_free((void **) &schema_item);
+ schema_item = next;
+ }
+ replica->objectclasses = NULL;
+ /* second the attributes policies */
+ for (schema_item = replica->attributes; schema_item; ) {
+ slapi_ch_free((void **) &schema_item->name_or_oid);
+ next = schema_item->next;
+ slapi_ch_free((void **) &schema_item);
+ schema_item = next;
+ }
+ replica->attributes = NULL;
+
+ /* Load the replication policy of the schema */
+ slapi_sdn_init_dn_byref( &sdn, dn );
+ if (slapi_search_internal_get_entry(&sdn, NULL, &entry, plugin_get_default_component_id()) == LDAP_SUCCESS) {
+
+ /* fill the policies (accept/reject) regarding objectclass */
+ schema_policy_add_action(entry, ATTR_SCHEMA_UPDATE_OBJECTCLASS_ACCEPT, &replica->objectclasses);
+ schema_policy_add_action(entry, ATTR_SCHEMA_UPDATE_OBJECTCLASS_REJECT, &replica->objectclasses);
+
+ /* fill the policies (accept/reject) regarding attribute */
+ schema_policy_add_action(entry, ATTR_SCHEMA_UPDATE_ATTRIBUTE_ACCEPT, &replica->attributes);
+ schema_policy_add_action(entry, ATTR_SCHEMA_UPDATE_ATTRIBUTE_REJECT, &replica->attributes);
+
+ slapi_entry_free( entry );
+ }
+ slapi_sdn_done(&sdn);
+}
+
+/* It load the policies (if they are defined) regarding the replication of the schema
+ * depending if the instance behaves as a consumer or a supplier
+ * It returns 0 if success
+ */
+int
+slapi_schema_load_repl_policies()
+{
+ if (schema_policy_lock == NULL) {
+ if (NULL == (schema_policy_lock = slapi_new_rwlock())) {
+ slapi_log_error(SLAPI_LOG_FATAL, "slapi_schema_load_repl_policies",
+ "slapi_new_rwlock() for schema replication policy lock failed\n");
+ return -1;
+ }
+ }
+ slapi_rwlock_wrlock( schema_policy_lock );
+
+ schema_load_repl_policy((const char *) ENTRY_REPL_SCHEMA_SUPPLIER, &supplier_policy);
+ schema_load_repl_policy((const char *) ENTRY_REPL_SCHEMA_CONSUMER, &consumer_policy);
+
+ slapi_rwlock_unlock( schema_policy_lock );
+
+ return 0;
+}
+
+/*
+ * It checks if the name/oid of the provided schema item (objectclass/attribute)
+ * is defined in the schema replication policy.
+ * If the replica role is a supplier, it takes the policy from supplier_policy else
+ * it takes it from the consumer_policy.
+ * Then depending on the schema_item, it takes the objectclasses or attributes policies
+ *
+ * If it find the name/oid in the policies, it returns
+ * REPL_SCHEMA_UPDATE_ACCEPT_VALUE: This schema item is accepted and can not prevent schema update
+ * REPL_SCHEMA_UPDATE_REJECT_VALUE: This schema item is rejected and prevents the schema update
+ * REPL_SCHEMA_UPDATE_UNKNOWN_VALUE: This schema item as no defined policy
+ *
+ * Caller must hold schema_policy_lock in read
+ */
+static int
+schema_check_policy(int replica_role, int schema_item, char *name, char *oid)
+{
+ repl_schema_policy_t *repl_policy;
+ schema_item_t *policy;
+
+ /* depending on the role, we take the supplier or the consumer policy */
+ if (replica_role == REPL_SCHEMA_AS_SUPPLIER) {
+ repl_policy = &supplier_policy;
+ } else {
+ repl_policy = &consumer_policy;
+ }
+
+ /* Now take the correct schema item policy */
+ if (schema_item == REPL_SCHEMA_OBJECTCLASS) {
+ policy = repl_policy->objectclasses;
+ } else {
+ policy = repl_policy->attributes;
+ }
+
+ /* Try to find the name/oid in the defined policies */
+ while (policy) {
+ if ((strcasecmp( name, policy->name_or_oid) == 0) || (strcasecmp( oid, policy->name_or_oid) == 0)) {
+ return policy->action;
+ }
+ policy = policy->next;
+ }
+ return REPL_SCHEMA_UPDATE_UNKNOWN_VALUE;
+}
+
static void
schema_dse_lock_read( void )
{
@@ -5911,11 +6152,12 @@ slapi_schema_get_superior_name(const char *ocname_or_oid)
* If oc_list1 or oc_list2 is global_oc, the caller must hold the oc_lock
*/
static int
-schema_oc_superset_check(struct objclass *oc_list1, struct objclass *oc_list2, char *message) {
+schema_oc_superset_check(struct objclass *oc_list1, struct objclass *oc_list2, char *message, int replica_role) {
struct objclass *oc_1, *oc_2;
char *description;
int debug_logging = 0;
int rc, i, j;
+ int repl_schema_policy;
int found;
if (message == NULL) {
@@ -5937,8 +6179,30 @@ schema_oc_superset_check(struct objclass *oc_list1, struct objclass *oc_list2, c
* - required attributes are also required in oc_2
* - allowed attributes are also allowed in oc_2
*/
+ slapi_rwlock_rdlock( schema_policy_lock );
for (oc_1 = oc_list1; oc_1 != NULL; oc_1 = oc_1->oc_next) {
+
+ /* Check if there is a specific policy for that objectclass */
+ repl_schema_policy = schema_check_policy(replica_role, REPL_SCHEMA_OBJECTCLASS, oc_1->oc_name, oc_1->oc_oid);
+ if (repl_schema_policy == REPL_SCHEMA_UPDATE_ACCEPT_VALUE) {
+ /* We are skipping the superset checking for that objectclass */
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "Do not check if this OBJECTCLASS is missing on local/remote schema [%s or %s]\n", oc_1->oc_name, oc_1->oc_oid);
+ continue;
+ } else if (repl_schema_policy == REPL_SCHEMA_UPDATE_REJECT_VALUE) {
+ /* This objectclass being present, we need to fail as if it was a superset
+ * keep evaluating to have all the objectclass checking
+ */
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "%s objectclass prevents replication of the schema\n", oc_1->oc_name);
+ rc = 1;
+ if(debug_logging){
+ /* we continue to check all the objectclasses so we log what is wrong */
+ continue;
+ } else {
+ break;
+ }
+ }
+
/* 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) {
@@ -6033,16 +6297,18 @@ schema_oc_superset_check(struct objclass *oc_list1, struct objclass *oc_list2, c
}
}
}
+ slapi_rwlock_unlock( schema_policy_lock );
return rc;
}
static int
-schema_at_superset_check(struct asyntaxinfo *at_list1, struct asyntaxinfo *at_list2, char *message)
+schema_at_superset_check(struct asyntaxinfo *at_list1, struct asyntaxinfo *at_list2, char *message, int replica_role)
{
struct asyntaxinfo *at_1, *at_2;
char *info = NULL;
int debug_logging = 0;
+ int repl_schema_policy;
int rc = 0;
if(at_list1 == NULL || at_list2 == NULL){
@@ -6054,8 +6320,29 @@ schema_at_superset_check(struct asyntaxinfo *at_list1, struct asyntaxinfo *at_li
debug_logging = 1;
}
- for (at_1 = at_list1; at_1 != NULL; at_1 = at_1->asi_next){
+ slapi_rwlock_rdlock( schema_policy_lock );
+ for (at_1 = at_list1; at_1 != NULL; at_1 = at_1->asi_next) {
+ /* Check if there is a specific policy for that objectclass */
+ repl_schema_policy = schema_check_policy(replica_role, REPL_SCHEMA_ATTRIBUTE, at_1->asi_name, at_1->asi_oid);
+ if (repl_schema_policy == REPL_SCHEMA_UPDATE_ACCEPT_VALUE) {
+ /* We are skipping the superset checking for that attribute */
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "Do not check if this ATTRIBUTE is missing on local/remote schema [%s or %s]\n", at_1->asi_name, at_1->asi_oid);
+ continue;
+ } else if (repl_schema_policy == REPL_SCHEMA_UPDATE_REJECT_VALUE) {
+ /* This attribute being present, we need to fail as if it was a superset
+ * but keep evaluating to have all the attribute checking
+ */
+ slapi_log_error(SLAPI_LOG_REPL, "schema", "%s attribute prevents replication of the schema\n", at_1->asi_name);
+ rc = 1;
+ if (debug_logging) {
+ /* we continue to check all the objectclasses so we log what is wrong */
+ continue;
+ } else {
+ break;
+ }
+ }
+
/* check if at_1 exists in at_list2 */
if((at_2 = attr_syntax_find(at_1, at_list2))){
/*
@@ -6114,6 +6401,7 @@ schema_at_superset_check(struct asyntaxinfo *at_list1, struct asyntaxinfo *at_li
}
}
}
+ slapi_rwlock_unlock( schema_policy_lock );
return rc;
@@ -6544,12 +6832,12 @@ schema_objectclasses_superset_check(struct berval **remote_schema, char *type)
/* Check if the remote_oc_list from a consumer are or not
* a superset of the objectclasses of the local supplier schema
*/
- rc = schema_oc_superset_check(remote_oc_list, g_get_global_oc_nolock(), "local supplier" );
+ rc = schema_oc_superset_check(remote_oc_list, g_get_global_oc_nolock(), "local supplier", REPL_SCHEMA_AS_SUPPLIER);
} else {
/* Check if the objectclasses of the local consumer schema are or not
* a superset of the remote_oc_list from a supplier
*/
- rc = schema_oc_superset_check(g_get_global_oc_nolock(), remote_oc_list, "remote supplier");
+ rc = schema_oc_superset_check(g_get_global_oc_nolock(), remote_oc_list, "remote supplier", REPL_SCHEMA_AS_CONSUMER);
}
oc_unlock();
@@ -6586,13 +6874,13 @@ schema_attributetypes_superset_check(struct berval **remote_schema, char *type)
* Check if the remote_at_list from a consumer are or not
* a superset of the attributetypes of the local supplier schema
*/
- rc = schema_at_superset_check(remote_at_list, attr_syntax_get_global_at(), "local supplier" );
+ rc = schema_at_superset_check(remote_at_list, attr_syntax_get_global_at(), "local supplier", REPL_SCHEMA_AS_SUPPLIER);
} else {
/*
* Check if the attributeypes of the local consumer schema are or not
* a superset of the remote_at_list from a supplier
*/
- rc = schema_at_superset_check(attr_syntax_get_global_at(), remote_at_list, "remote supplier");
+ rc = schema_at_superset_check(attr_syntax_get_global_at(), remote_at_list, "remote supplier", REPL_SCHEMA_AS_CONSUMER);
}
attr_syntax_unlock_read();
}
diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h
index a03b968..7ef532b 100644
--- a/ldap/servers/slapd/slap.h
+++ b/ldap/servers/slapd/slap.h
@@ -2413,6 +2413,42 @@ typedef struct _slapdFrontendConfig {
#define CONFIG_SCHEMAREPLACE_STR_ON "on"
#define CONFIG_SCHEMAREPLACE_STR_REPLICATION_ONLY "replication-only"
+/* Those define are used when applying the policies for the replication of the schema */
+/* Used to determine if we are looking at the policies from the supplier (push) or consumer (receive) side */
+#define REPL_SCHEMA_AS_CONSUMER 0
+#define REPL_SCHEMA_AS_SUPPLIER 1
+/* Used to determine if we are looking at objectclass/attribute policies */
+#define REPL_SCHEMA_OBJECTCLASS 0
+#define REPL_SCHEMA_ATTRIBUTE 1
+/* Used to determine the schema replication: never or based on values */
+#define REPL_SCHEMA_UPDATE_NEVER 1
+#define REPL_SCHEMA_UPDATE_ON_VALUE 2
+/* Used to determine the action when schema replition is based on values */
+#define REPL_SCHEMA_UPDATE_ACCEPT_VALUE 3
+#define REPL_SCHEMA_UPDATE_REJECT_VALUE 4
+#define REPL_SCHEMA_UPDATE_UNKNOWN_VALUE 5
+
+/* entries containing the policies */
+#define ENTRY_REPL_SCHEMA_TOP "cn=replSchema, cn=config"
+#define ENTRY_REPL_SCHEMA_SUPPLIER "cn=supplierUpdatePolicy," ENTRY_REPL_SCHEMA_TOP
+#define ENTRY_REPL_SCHEMA_CONSUMER "cn=consumerUpdatePolicy," ENTRY_REPL_SCHEMA_TOP
+/*
+ * attribute name containing the policy action for
+ * a given objectclass/attribute
+ */
+#define ATTR_SCHEMA_UPDATE_OBJECTCLASS_ACCEPT "schemaUpdateObjectclassAccept"
+#define ATTR_SCHEMA_UPDATE_OBJECTCLASS_REJECT "schemaUpdateObjectclassReject"
+#define ATTR_SCHEMA_UPDATE_ATTRIBUTE_ACCEPT "schemaUpdateAttributeAccept"
+#define ATTR_SCHEMA_UPDATE_ATTRIBUTE_REJECT "schemaUpdateAttributeReject"
+#define SUPPLIER_POLICY_1 ATTR_SCHEMA_UPDATE_OBJECTCLASS_ACCEPT ": printer-uri-oid\n"
+#if defined(USE_OLD_UNHASHED)
+#define SUPPLIER_POLICY_2 ATTR_SCHEMA_UPDATE_ATTRIBUTE_ACCEPT ": PSEUDO_ATTR_UNHASHEDUSERPASSWORD_OID\n"
+#else
+#define SUPPLIER_POLICY_2 ATTR_SCHEMA_UPDATE_ATTRIBUTE_ACCEPT ": 2.16.840.1.113730.3.1.2110\n"
+#endif
+#define DEFAULT_SUPPLIER_POLICY SUPPLIER_POLICY_1 SUPPLIER_POLICY_2
+#define DEFAULT_CONSUMER_POLICY DEFAULT_SUPPLIER_POLICY
+
#define CONNECTION_BUFFER_OFF 0
#define CONNECTION_BUFFER_ON 1
#define CONNECTION_BUFFER_ADAPT 2
diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h
index 1b22d9e..f2b4fe8 100644
--- a/ldap/servers/slapd/slapi-private.h
+++ b/ldap/servers/slapd/slapi-private.h
@@ -758,6 +758,9 @@ struct slapi_componentid * plugin_get_default_component_id();
#define SLAPI_COMPONENT_NAME_NSPR "Netscape Portable Runtime"
#define SLAPI_COMPONENT_NAME_LDAPSDK "LDAP sdk"
+/* loads the policies related to the replication of the schema */
+int slapi_schema_load_repl_policies();
+void slapi_schema_get_repl_entries(char **repl_schema_top, char ** repl_schema_supplier, char **repl_schema_consumer, char **default_supplier_policy, char **default_consumer_policy);
/* return the list of attr defined in the schema matching the attr flags */
char ** slapi_schema_list_attribute_names(unsigned long flag);
/* return the list of attributes belonging to the objectclass */