Selaa lähdekoodia

Ticket 47553: Enhance ACIs to have more control over MODRDN operations

Bug Description:
	This is enhancement of the access control related to the MODDN/MODRDN operation.
	We need the ability to specify a 'source tree' and a 'destination tree' where MODDN/MODRDN
	are allowed/denied.
	It is a requirement that 'source' and 'destination' targets can be specified in the same ACI.

Fix Description:
	Please refer to http://directory.fedoraproject.org/wiki/Access_control_on_trees_specified_in_MODDN_operation.
	Mainly:
		- parse new aci syntax
		- support of new 'moddn' right in aci
		- Check 'moddn' right in ldbm_back_modrdn
		- filter source/destination when looking for matching ressources
		- support of 'moddn' rights in GER control
		- new nsslapd-moddn-aci compatibility config flag
		- aci logging summary (display source/destination)
		- Skip readonly backend

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

Reviewed by: Rich Megginson (Big thanks Rich !!)

Platforms tested: F17

Flag Day: no

Doc impact: yes
Thierry bordaz (tbordaz) 11 vuotta sitten
vanhempi
sitoutus
dc17d63368

+ 1187 - 0
dirsrvtests/tickets/ticket47553_single_aci_test.py

@@ -0,0 +1,1187 @@
+'''
+Created on Nov 7, 2013
+
+@author: tbordaz
+'''
+import os
+import sys
+import time
+import ldap
+import logging
+import socket
+import time
+import logging
+import pytest
+import re
+from lib389 import DirSrv, Entry, tools
+from lib389.tools import DirSrvTools
+from lib389._constants import *
+from lib389.properties import *
+from constants import *
+from lib389._constants import REPLICAROLE_MASTER
+
+logging.getLogger(__name__).setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+#
+# important part. We can deploy Master1 and Master2 on different versions
+#
+installation1_prefix = None
+installation2_prefix = None
+
+TEST_REPL_DN = "cn=test_repl, %s" % SUFFIX
+
+STAGING_CN     = "staged user"
+PRODUCTION_CN  = "accounts"
+EXCEPT_CN      = "excepts"
+
+STAGING_DN    = "cn=%s,%s" % (STAGING_CN, SUFFIX)
+PRODUCTION_DN = "cn=%s,%s" % (PRODUCTION_CN, SUFFIX)
+PROD_EXCEPT_DN = "cn=%s,%s" % (EXCEPT_CN, PRODUCTION_DN)
+
+STAGING_PATTERN    = "cn=%s*,%s" % (STAGING_CN[:2],    SUFFIX)
+PRODUCTION_PATTERN = "cn=%s*,%s" % (PRODUCTION_CN[:2], SUFFIX)
+BAD_STAGING_PATTERN    = "cn=bad*,%s" % (SUFFIX)
+BAD_PRODUCTION_PATTERN = "cn=bad*,%s" % (SUFFIX)
+
+BIND_CN        = "bind_entry"
+BIND_DN        = "cn=%s,%s" % (BIND_CN, SUFFIX)
+BIND_PW        = "password"
+
+NEW_ACCOUNT    = "new_account"
+MAX_ACCOUNTS   = 20
+
+CONFIG_MODDN_ACI_ATTR = "nsslapd-moddn-aci"
+
+class TopologyMaster1Master2(object):
+    def __init__(self, master1, master2):
+        master1.open()
+        self.master1 = master1
+        
+        master2.open()
+        self.master2 = master2
+
+
+#@pytest.fixture(scope="module")
+def topology(request):
+    '''
+        This fixture is used to create a replicated topology for the 'module'.
+        The replicated topology is MASTER1 <-> Master2.
+        At the beginning, It may exists a master2 instance and/or a master2 instance.
+        It may also exists a backup for the master1 and/or the master2.
+    
+        Principle:
+            If master1 instance exists:
+                restart it
+            If master2 instance exists:
+                restart it
+            If backup of master1 AND backup of master2 exists:
+                create or rebind to master1
+                create or rebind to master2
+
+                restore master1 from backup
+                restore master2 from backup
+            else:
+                Cleanup everything
+                    remove instances
+                    remove backups
+                Create instances
+                Initialize replication
+                Create backups
+    '''
+    global installation1_prefix
+    global installation2_prefix
+
+    # allocate master1 on a given deployement
+    master1   = DirSrv(verbose=False)
+    if installation1_prefix:
+        args_instance[SER_DEPLOYED_DIR] = installation1_prefix
+        
+    # Args for the master1 instance
+    args_instance[SER_HOST] = HOST_MASTER_1
+    args_instance[SER_PORT] = PORT_MASTER_1
+    args_instance[SER_SERVERID_PROP] = SERVERID_MASTER_1
+    args_master = args_instance.copy()
+    master1.allocate(args_master)
+    
+    # allocate master1 on a given deployement
+    master2 = DirSrv(verbose=False)
+    if installation2_prefix:
+        args_instance[SER_DEPLOYED_DIR] = installation2_prefix
+        
+    # Args for the consumer instance
+    args_instance[SER_HOST] = HOST_MASTER_2
+    args_instance[SER_PORT] = PORT_MASTER_2
+    args_instance[SER_SERVERID_PROP] = SERVERID_MASTER_2
+    args_master = args_instance.copy()
+    master2.allocate(args_master)
+
+    
+    # Get the status of the backups
+    backup_master1 = master1.checkBackupFS()
+    backup_master2 = master2.checkBackupFS()
+    
+    # Get the status of the instance and restart it if it exists
+    instance_master1   = master1.exists()
+    if instance_master1:
+        master1.stop(timeout=10)
+        master1.start(timeout=10)
+        
+    instance_master2 = master2.exists()
+    if instance_master2:
+        master2.stop(timeout=10)
+        master2.start(timeout=10)
+    
+    if backup_master1 and backup_master2:
+        # The backups exist, assuming they are correct 
+        # we just re-init the instances with them
+        if not instance_master1:
+            master1.create()
+            # Used to retrieve configuration information (dbdir, confdir...)
+            master1.open()
+        
+        if not instance_master2:
+            master2.create()
+            # Used to retrieve configuration information (dbdir, confdir...)
+            master2.open()
+        
+        # restore master1 from backup
+        master1.stop(timeout=10)
+        master1.restoreFS(backup_master1)
+        master1.start(timeout=10)
+        
+        # restore master2 from backup
+        master2.stop(timeout=10)
+        master2.restoreFS(backup_master2)
+        master2.start(timeout=10)
+    else:
+        # We should be here only in two conditions
+        #      - This is the first time a test involve master-consumer
+        #        so we need to create everything
+        #      - Something weird happened (instance/backup destroyed)
+        #        so we discard everything and recreate all
+        
+        # Remove all the backups. So even if we have a specific backup file
+        # (e.g backup_master) we clear all backups that an instance my have created
+        if backup_master1:
+            master1.clearBackupFS()
+        if backup_master2:
+            master2.clearBackupFS()
+        
+        # Remove all the instances
+        if instance_master1:
+            master1.delete()
+        if instance_master2:
+            master2.delete()
+                        
+        # Create the instances
+        master1.create()
+        master1.open()
+        master2.create()
+        master2.open()
+    
+        # 
+        # Now prepare the Master-Consumer topology
+        #
+        # First Enable replication
+        master1.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_MASTER, replicaId=REPLICAID_MASTER_1)
+        master2.replica.enableReplication(suffix=SUFFIX, role=REPLICAROLE_MASTER, replicaId=REPLICAID_MASTER_2)
+        
+        # Initialize the supplier->consumer
+        
+        properties = {RA_NAME:      r'meTo_$host:$port',
+                      RA_BINDDN:    defaultProperties[REPLICATION_BIND_DN],
+                      RA_BINDPW:    defaultProperties[REPLICATION_BIND_PW],
+                      RA_METHOD:    defaultProperties[REPLICATION_BIND_METHOD],
+                      RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
+        repl_agreement = master1.agreement.create(suffix=SUFFIX, host=master2.host, port=master2.port, properties=properties)
+    
+        if not repl_agreement:
+            log.fatal("Fail to create a replica agreement")
+            sys.exit(1)
+            
+        log.debug("%s created" % repl_agreement)
+        
+        properties = {RA_NAME:      r'meTo_$host:$port',
+                      RA_BINDDN:    defaultProperties[REPLICATION_BIND_DN],
+                      RA_BINDPW:    defaultProperties[REPLICATION_BIND_PW],
+                      RA_METHOD:    defaultProperties[REPLICATION_BIND_METHOD],
+                      RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
+        master2.agreement.create(suffix=SUFFIX, host=master1.host, port=master1.port, properties=properties)
+
+        master1.agreement.init(SUFFIX, HOST_MASTER_2, PORT_MASTER_2)
+        master1.waitForReplInit(repl_agreement)
+        
+        # Check replication is working fine
+        master1.add_s(Entry((TEST_REPL_DN, {
+                                                'objectclass': "top person".split(),
+                                                'sn': 'test_repl',
+                                                'cn': 'test_repl'})))
+        loop = 0
+        while loop <= 10:
+            try:
+                ent = master2.getEntry(TEST_REPL_DN, ldap.SCOPE_BASE, "(objectclass=*)")
+                break
+            except ldap.NO_SUCH_OBJECT:
+                time.sleep(1)
+                loop += 1
+                
+        # Time to create the backups
+        master1.stop(timeout=10)
+        master1.backupfile = master1.backupFS()
+        master1.start(timeout=10)
+        
+        master2.stop(timeout=10)
+        master2.backupfile = master2.backupFS()
+        master2.start(timeout=10)
+    
+    # 
+    # Here we have two instances master and consumer
+    # with replication working. Either coming from a backup recovery
+    # or from a fresh (re)init
+    # Time to return the topology
+    return TopologyMaster1Master2(master1, master2)
+
+
+
+def _bind_manager(topology):
+    topology.master1.log.info("Bind as %s " % DN_DM)
+    topology.master1.simple_bind_s(DN_DM, PASSWORD)
+    
+def _bind_normal(topology):
+    # bind as bind_entry
+    topology.master1.log.info("Bind as %s" % BIND_DN)
+    topology.master1.simple_bind_s(BIND_DN, BIND_PW)
+    
+def _moddn_aci_deny_tree(topology, mod_type=None, target_from=STAGING_DN, target_to=PROD_EXCEPT_DN):
+    '''
+    It denies the access moddn_to in cn=except,cn=accounts,SUFFIX
+    '''
+    assert mod_type != None
+    
+    ACI_TARGET_FROM = ""
+    ACI_TARGET_TO   = ""
+    if target_from:
+        ACI_TARGET_FROM = "(target_from = \"ldap:///%s\")" % (target_from)
+    if target_to:
+        ACI_TARGET_TO   = "(target_to   = \"ldap:///%s\")" % (target_to)
+        
+    ACI_ALLOW        = "(version 3.0; acl \"Deny MODDN to prod_except\"; deny (moddn)"
+    ACI_SUBJECT      = " userdn = \"ldap:///%s\";)" % BIND_DN
+    ACI_BODY         = ACI_TARGET_TO + ACI_TARGET_FROM + ACI_ALLOW + ACI_SUBJECT
+    mod = [(mod_type, 'aci', ACI_BODY)]
+    #topology.master1.modify_s(SUFFIX, mod)
+    topology.master1.log.info("Add a DENY aci under %s " % PROD_EXCEPT_DN)
+    topology.master1.modify_s(PROD_EXCEPT_DN, mod)
+    
+def _moddn_aci_staging_to_production(topology, mod_type=None, target_from=STAGING_DN, target_to=PRODUCTION_DN):
+    assert mod_type != None
+
+
+    ACI_TARGET_FROM = ""
+    ACI_TARGET_TO   = ""
+    if target_from:
+        ACI_TARGET_FROM = "(target_from = \"ldap:///%s\")" % (target_from)
+    if target_to:
+        ACI_TARGET_TO   = "(target_to   = \"ldap:///%s\")" % (target_to)
+
+    ACI_ALLOW        = "(version 3.0; acl \"MODDN from staging to production\"; allow (moddn)"
+    ACI_SUBJECT      = " userdn = \"ldap:///%s\";)" % BIND_DN
+    ACI_BODY         = ACI_TARGET_FROM + ACI_TARGET_TO + ACI_ALLOW + ACI_SUBJECT
+    mod = [(mod_type, 'aci', ACI_BODY)]
+    topology.master1.modify_s(SUFFIX, mod)
+
+def _moddn_aci_from_production_to_staging(topology, mod_type=None):
+    assert mod_type != None
+    
+    ACI_TARGET       = "(target_from = \"ldap:///%s\") (target_to = \"ldap:///%s\")" % (PRODUCTION_DN, STAGING_DN)
+    ACI_ALLOW        = "(version 3.0; acl \"MODDN from production to staging\"; allow (moddn)"
+    ACI_SUBJECT      = " userdn = \"ldap:///%s\";)" % BIND_DN
+    ACI_BODY         = ACI_TARGET + ACI_ALLOW + ACI_SUBJECT
+    mod = [(mod_type, 'aci', ACI_BODY)]
+    topology.master1.modify_s(SUFFIX, mod)
+
+
+def test_ticket47553_init(topology):
+    """
+        Creates
+            - a staging DIT
+            - a production DIT
+            - add accounts in staging DIT
+            - enable ACL logging (commented for performance reason)
+        
+    """
+    
+    topology.master1.log.info("\n\n######################### INITIALIZATION ######################\n")
+    
+    # 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_CN,
+                                            'cn':           BIND_CN,
+                                            'userpassword': BIND_PW})))
+    
+    # DIT for staging
+    topology.master1.log.info("Add %s" % STAGING_DN)
+    topology.master1.add_s(Entry((STAGING_DN, {
+                                            'objectclass': "top organizationalRole".split(),
+                                            'cn':           STAGING_CN,
+                                            'description': "staging DIT"})))
+    
+    # DIT for production
+    topology.master1.log.info("Add %s" % PRODUCTION_DN)
+    topology.master1.add_s(Entry((PRODUCTION_DN, {
+                                            'objectclass': "top organizationalRole".split(),
+                                            'cn':           PRODUCTION_CN,
+                                            'description': "production DIT"})))
+    
+    # DIT for production/except
+    topology.master1.log.info("Add %s" % PROD_EXCEPT_DN)
+    topology.master1.add_s(Entry((PROD_EXCEPT_DN, {
+                                            'objectclass': "top organizationalRole".split(),
+                                            'cn':           EXCEPT_CN,
+                                            'description': "production except DIT"})))
+    
+    # enable acl error logging
+    #mod = [(ldap.MOD_REPLACE, 'nsslapd-errorlog-level', '128')]
+    #topology.master1.modify_s(DN_CONFIG, mod)
+    #topology.master2.modify_s(DN_CONFIG, mod)
+    
+
+    
+    
+    
+    # add dummy entries in the staging DIT
+    for cpt in range(MAX_ACCOUNTS):
+        name = "%s%d" % (NEW_ACCOUNT, cpt)
+        topology.master1.add_s(Entry(("cn=%s,%s" % (name, STAGING_DN), {
+                                            'objectclass': "top person".split(),
+                                            'sn': name,
+                                            'cn': name})))
+
+
+def test_ticket47553_add(topology):
+    '''
+    This test case checks that the ADD operation fails (no ADD aci on production)
+    '''
+    
+    topology.master1.log.info("\n\n######################### ADD (should fail) ######################\n")
+    
+    _bind_normal(topology)
+    
+    #
+    # First try to add an entry in production => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to add %s" % PRODUCTION_DN)
+        name = "%s%d" % (NEW_ACCOUNT, 0)
+        topology.master1.add_s(Entry(("cn=%s,%s" % (name, PRODUCTION_DN), {
+                                                'objectclass': "top person".split(),
+                                                'sn': name,
+                                                'cn': name})))
+        assert 0  # this is an error, we should not be allowed to add an entry in production
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+
+def test_ticket47553_delete(topology):
+    '''
+    This test case checks that the DEL operation fails (no 'delete' aci on production)
+    '''
+    
+    topology.master1.log.info("\n\n######################### DELETE (should fail) ######################\n")
+    
+    _bind_normal(topology)
+    #
+    # Second try to delete an entry in staging => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to delete %s" % STAGING_DN)
+        name = "%s%d" % (NEW_ACCOUNT, 0)
+        topology.master1.delete_s("cn=%s,%s" % (name, STAGING_DN))
+        assert 0  # this is an error, we should not be allowed to add an entry in production
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+        
+    
+def test_ticket47553_moddn_staging_prod_0(topology):
+    '''
+        This test case MOVE entry NEW_ACCOUNT0 from staging to prod
+        target_to/target_from: equality filter
+    '''
+    
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (0) ######################\n")
+    _bind_normal(topology)
+    
+    old_rdn = "cn=%s0" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    
+    # successfull MOD with the ACI
+    topology.master1.log.info("\n\n######################### MOVE to and from equality filter ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=STAGING_DN, target_to=PRODUCTION_DN)
+    _bind_normal(topology)
+    
+    topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+    topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    
+    # successfull MOD with the both ACI
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=STAGING_DN, target_to=PRODUCTION_DN)
+    _bind_normal(topology)
+    
+    
+def test_ticket47553_moddn_staging_prod_1(topology):
+    '''
+        This test case MOVE entry NEW_ACCOUNT1 from staging to prod
+        target_to/target_from: substring/equality filter
+    '''
+    
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (1) ######################\n")
+    _bind_normal(topology)
+    
+    old_rdn = "cn=%s1" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    
+    # successfull MOD with the ACI
+    topology.master1.log.info("\n\n######################### MOVE to substring/ from equality filter ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=STAGING_DN, target_to=PRODUCTION_PATTERN)
+    _bind_normal(topology)
+    
+    
+    topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+    topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    
+    # successfull MOD with the both ACI
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=STAGING_DN, target_to=PRODUCTION_PATTERN)
+    _bind_normal(topology)
+
+def test_ticket47553_moddn_staging_prod_2(topology):
+    '''
+        This test case fails to MOVE entry NEW_ACCOUNT2 from staging to prod
+        because of bad pattern
+    '''
+    
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (2) ######################\n")
+    _bind_normal(topology)
+    
+    old_rdn = "cn=%s2" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    
+    # successfull MOD with the ACI
+    topology.master1.log.info("\n\n######################### MOVE to substring (BAD)/ from equality filter ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=STAGING_DN, target_to=BAD_PRODUCTION_PATTERN)
+    _bind_normal(topology)
+    
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    # successfull MOD with the both ACI
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=STAGING_DN, target_to=BAD_PRODUCTION_PATTERN)
+    _bind_normal(topology)
+    
+def test_ticket47553_moddn_staging_prod_3(topology):
+    '''
+        This test case MOVE entry NEW_ACCOUNT3 from staging to prod
+        target_to/target_from: equality/substring filter
+    '''
+    
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (3) ######################\n")
+    _bind_normal(topology)
+    
+    old_rdn = "cn=%s3" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    
+    # successfull MOD with the ACI
+    topology.master1.log.info("\n\n######################### MOVE to:equality filter / from substring filter ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=STAGING_PATTERN, target_to=PRODUCTION_DN)
+    _bind_normal(topology)
+    
+    
+    topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+    topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    
+    # successfull MOD with the both ACI
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=STAGING_PATTERN, target_to=PRODUCTION_DN)
+    _bind_normal(topology)
+    
+def test_ticket47553_moddn_staging_prod_4(topology):
+    '''
+        This test case fails to MOVE entry NEW_ACCOUNT4 from staging to prod
+        because of bad pattern
+    '''
+    
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (4) ######################\n")
+    _bind_normal(topology)
+    
+    old_rdn = "cn=%s4" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    
+    # successfull MOD with the ACI
+    topology.master1.log.info("\n\n######################### MOVE to: equality filter/ from: substring (BAD) ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=BAD_STAGING_PATTERN, target_to=PRODUCTION_DN)
+    _bind_normal(topology)
+    
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    # successfull MOD with the both ACI
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=BAD_STAGING_PATTERN, target_to=PRODUCTION_DN)
+    _bind_normal(topology)
+    
+def test_ticket47553_moddn_staging_prod_5(topology):
+    '''
+        This test case MOVE entry NEW_ACCOUNT5 from staging to prod
+        target_to/target_from: substring/substring filter
+    '''
+    
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (5) ######################\n")
+    _bind_normal(topology)
+    
+    old_rdn = "cn=%s5" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    
+    # successfull MOD with the ACI
+    topology.master1.log.info("\n\n######################### MOVE to:substring filter / from: substring filter ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=STAGING_PATTERN, target_to=PRODUCTION_PATTERN)
+    _bind_normal(topology)
+    
+    
+    topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+    topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    
+    # successfull MOD with the both ACI
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=STAGING_PATTERN, target_to=PRODUCTION_PATTERN)
+    _bind_normal(topology)
+    
+def test_ticket47553_moddn_staging_prod_6(topology):
+    '''
+        This test case MOVE entry NEW_ACCOUNT6 from staging to prod
+        target_to/target_from: substring/<enmpty> filter
+    '''
+    
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (6) ######################\n")
+    _bind_normal(topology)
+    
+    old_rdn = "cn=%s6" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    
+    # successfull MOD with the ACI
+    topology.master1.log.info("\n\n######################### MOVE to:substring filter / from: empty ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=None, target_to=PRODUCTION_PATTERN)
+    _bind_normal(topology)
+    
+    
+    topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+    topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    
+    # successfull MOD with the both ACI
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=None, target_to=PRODUCTION_PATTERN)
+    _bind_normal(topology)
+    
+def test_ticket47553_moddn_staging_prod_7(topology):
+    '''
+        This test case MOVE entry NEW_ACCOUNT7 from staging to prod
+        target_to/target_from: <empty>/substring filter
+    '''
+    
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (7) ######################\n")
+    _bind_normal(topology)
+    
+    old_rdn = "cn=%s7" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    
+    # successfull MOD with the ACI
+    topology.master1.log.info("\n\n######################### MOVE to: empty/ from: substring filter ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=STAGING_PATTERN, target_to=None)
+    _bind_normal(topology)
+    
+    
+    topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+    topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    
+    # successfull MOD with the both ACI
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=STAGING_PATTERN, target_to=None)
+    _bind_normal(topology)
+    
+    
+def test_ticket47553_moddn_staging_prod_8(topology):
+    '''
+        This test case MOVE entry NEW_ACCOUNT8 from staging to prod
+        target_to/target_from: <empty>/<empty> filter
+    '''
+    
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (8) ######################\n")
+    _bind_normal(topology)
+    
+    old_rdn = "cn=%s8" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    
+    # successfull MOD with the ACI
+    topology.master1.log.info("\n\n######################### MOVE to: empty/ from: empty ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=None, target_to=None)
+    _bind_normal(topology)
+    
+    
+    topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+    topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    
+    # successfull MOD with the both ACI
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=None, target_to=None)
+    _bind_normal(topology)
+    
+def test_ticket47553_moddn_staging_prod_9(topology):
+    '''
+        This test case disable the 'moddn' right so a MODDN requires a 'add' right
+        to be successfull.
+        It fails to MOVE entry NEW_ACCOUNT9 from staging to prod.
+        Add a 'add' right to prod.
+        Then it succeeds to MOVE NEW_ACCOUNT9 from staging to prod.
+        
+        Then enable the 'moddn' right so a MODDN requires a 'moddn' right
+        It fails to MOVE entry NEW_ACCOUNT10 from staging to prod.
+        Add a 'moddn' right to prod.
+        Then it succeeds to MOVE NEW_ACCOUNT10 from staging to prod.
+    '''
+    
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (9) ######################\n")
+    
+    _bind_normal(topology)   
+    old_rdn = "cn=%s9" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+        
+    ############################################
+    # Now do tests with no support of moddn aci
+    ############################################
+    topology.master1.log.info("Disable the moddn right" )
+    _bind_manager(topology)
+    mod = [(ldap.MOD_REPLACE, CONFIG_MODDN_ACI_ATTR, 'off')]
+    topology.master1.modify_s(DN_CONFIG, mod)
+        
+    # Add the moddn aci that will not be evaluated because of the config flag
+    topology.master1.log.info("\n\n######################### MOVE to and from equality filter ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=STAGING_DN, target_to=PRODUCTION_DN)
+    _bind_normal(topology)
+    
+    # It will fail because it will test the ADD right
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    # remove the moddn aci
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=STAGING_DN, target_to=PRODUCTION_DN)
+    _bind_normal(topology)
+    
+    # 
+    # add the 'add' right to the production DN
+    # Then do a successfull moddn
+    #
+    ACI_ALLOW        = "(version 3.0; acl \"ADD rights to allow moddn\"; allow (add)"
+    ACI_SUBJECT      = " userdn = \"ldap:///%s\";)" % BIND_DN
+    ACI_BODY         = ACI_ALLOW + ACI_SUBJECT
+    
+    _bind_manager(topology)
+    mod = [(ldap.MOD_ADD, 'aci', ACI_BODY)]
+    topology.master1.modify_s(PRODUCTION_DN, mod)
+    _bind_normal(topology)
+    
+    topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+    topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    
+    _bind_manager(topology)
+    mod = [(ldap.MOD_DELETE, 'aci', ACI_BODY)]
+    topology.master1.modify_s(PRODUCTION_DN, mod)
+    _bind_normal(topology)
+    
+    
+    ############################################
+    # Now do tests with support of moddn aci
+    ############################################
+    topology.master1.log.info("Enable the moddn right" )
+    _bind_manager(topology)
+    mod = [(ldap.MOD_REPLACE, CONFIG_MODDN_ACI_ATTR, 'on')]
+    topology.master1.modify_s(DN_CONFIG, mod)
+    
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (10) ######################\n")
+    
+    _bind_normal(topology)   
+    old_rdn = "cn=%s10" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    # 
+    # add the 'add' right to the production DN
+    # Then do a failing moddn
+    #
+    ACI_ALLOW        = "(version 3.0; acl \"ADD rights to allow moddn\"; allow (add)"
+    ACI_SUBJECT      = " userdn = \"ldap:///%s\";)" % BIND_DN
+    ACI_BODY         = ACI_ALLOW + ACI_SUBJECT
+    
+    _bind_manager(topology)
+    mod = [(ldap.MOD_ADD, 'aci', ACI_BODY)]
+    topology.master1.modify_s(PRODUCTION_DN, mod)
+    _bind_normal(topology)
+    
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    _bind_manager(topology)
+    mod = [(ldap.MOD_DELETE, 'aci', ACI_BODY)]
+    topology.master1.modify_s(PRODUCTION_DN, mod)
+    _bind_normal(topology)
+    
+    # Add the moddn aci that will be evaluated because of the config flag
+    topology.master1.log.info("\n\n######################### MOVE to and from equality filter ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=STAGING_DN, target_to=PRODUCTION_DN)
+    _bind_normal(topology)
+    
+    topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+    topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    
+    # remove the moddn aci
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=STAGING_DN, target_to=PRODUCTION_DN)
+    _bind_normal(topology)
+    
+def test_ticket47553_moddn_prod_staging(topology):
+    '''
+        This test checks that we can move ACCOUNT11 from staging to prod
+        but not move back ACCOUNT11 from prod to staging
+    '''
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (11) ######################\n")
+    
+    _bind_normal(topology)
+    
+    old_rdn = "cn=%s11" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    
+    # successfull MOD with the ACI
+    topology.master1.log.info("\n\n######################### MOVE to and from equality filter ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=STAGING_DN, target_to=PRODUCTION_DN)
+    _bind_normal(topology)
+    
+    topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+    topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    
+    
+    #
+    # Now check we can not move back the entry to staging
+    old_rdn = "cn=%s11" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, PRODUCTION_DN)
+    new_rdn = old_rdn
+    new_superior = STAGING_DN
+    
+    try:
+        topology.master1.log.info("Try to move back MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    # successfull MOD with the both ACI
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=STAGING_DN, target_to=PRODUCTION_DN)
+    _bind_normal(topology)
+
+
+def test_ticket47553_check_repl_M2_to_M1(topology):
+    '''
+        Checks that replication is still working M2->M1, using ACCOUNT12
+    '''
+    
+    topology.master1.log.info("Bind as %s (M2)" % DN_DM)
+    topology.master2.simple_bind_s(DN_DM, PASSWORD)
+    
+    rdn = "cn=%s12" % NEW_ACCOUNT
+    dn  = "%s,%s" % (rdn, STAGING_DN)
+    
+        # First wait for the ACCOUNT19 entry being replicated on M2
+    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
+    
+    
+    attribute = 'description'
+    tested_value = 'Hello world'
+    mod = [(ldap.MOD_ADD, attribute, tested_value)]
+    topology.master1.log.info("Update (M2) %s (%s)" % (dn, attribute))
+    topology.master2.modify_s(dn, mod)
+    
+    loop = 0
+    while loop <= 10:
+        ent = topology.master1.getEntry(dn, ldap.SCOPE_BASE, "(objectclass=*)")
+        assert ent != None
+        if ent.hasAttr(attribute) and (ent.getValue(attribute) == tested_value):
+            break
+        
+        time.sleep(1)
+        loop += 1
+    assert loop < 10
+    topology.master1.log.info("Update %s (%s) replicated on M1" % (dn, attribute))
+
+def test_ticket47553_moddn_staging_prod_except(topology):
+    '''
+        This test case MOVE entry NEW_ACCOUNT13 from staging to prod
+        but fails to move entry NEW_ACCOUNT14 from staging to prod_except
+    '''
+    
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod (13) ######################\n")
+    _bind_normal(topology)
+    
+    old_rdn = "cn=%s13" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PRODUCTION_DN
+    
+    # 
+    # Try to rename without the apropriate ACI  => INSUFFICIENT_ACCESS
+    #
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    
+    # successfull MOD with the ACI
+    topology.master1.log.info("\n\n######################### MOVE to and from equality filter ######################\n")
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_ADD, target_from=STAGING_DN, target_to=PRODUCTION_DN)
+    _moddn_aci_deny_tree(topology, mod_type=ldap.MOD_ADD)
+    _bind_normal(topology)
+    
+    topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+    topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+    
+    #
+    # Now try to move an entry  under except
+    #
+    topology.master1.log.info("\n\n######################### MOVE staging -> Prod/Except (14) ######################\n")
+    old_rdn = "cn=%s14" % NEW_ACCOUNT
+    old_dn  = "%s,%s" % (old_rdn, STAGING_DN)
+    new_rdn = old_rdn
+    new_superior = PROD_EXCEPT_DN
+    try:
+        topology.master1.log.info("Try to MODDN %s -> %s,%s" % (old_dn, new_rdn, new_superior))
+        topology.master1.rename_s(old_dn, new_rdn, newsuperior=new_superior)
+        assert 0
+    except AssertionError:
+        topology.master1.log.info("Exception (not really expected exception but that is fine as it fails to rename)")
+    except Exception as e:
+        topology.master1.log.info("Exception (expected): %s" % type(e).__name__)
+        assert isinstance(e, ldap.INSUFFICIENT_ACCESS)
+    
+    # successfull MOD with the both ACI
+    _bind_manager(topology)
+    _moddn_aci_staging_to_production(topology, mod_type=ldap.MOD_DELETE, target_from=STAGING_DN, target_to=PRODUCTION_DN)
+    _moddn_aci_deny_tree(topology, mod_type=ldap.MOD_DELETE)
+    _bind_normal(topology)
+        
+def test_ticket47553_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 = '/home/tbordaz/install'
+    installation2_prefix = '/home/tbordaz/install'
+        
+    topo = topology(True)
+    topo.master1.log.info("\n\n######################### Ticket 47553 ######################\n")
+    test_ticket47553_init(topo)
+
+    
+    # Check that without appropriate aci we are not allowed to add/delete
+    test_ticket47553_add(topo)
+    test_ticket47553_delete(topo)
+
+    # tests the ACI as equality/substring filter
+    test_ticket47553_moddn_staging_prod_0(topo)
+    test_ticket47553_moddn_staging_prod_1(topo)
+    test_ticket47553_moddn_staging_prod_2(topo)
+    test_ticket47553_moddn_staging_prod_3(topo)
+    test_ticket47553_moddn_staging_prod_4(topo)
+    test_ticket47553_moddn_staging_prod_5(topo)
+      
+    # tests the ACI with undefined 'target_to'/'target_from'
+    test_ticket47553_moddn_staging_prod_6(topo)
+    test_ticket47553_moddn_staging_prod_7(topo)
+    test_ticket47553_moddn_staging_prod_8(topo)
+      
+    # Check we can control the behavior with nsslapd-moddn-aci
+    test_ticket47553_moddn_staging_prod_9(topo)
+      
+    # Check we can move entry 'from' -> 'to' but not 'to' -> 'from'
+    test_ticket47553_moddn_prod_staging(topo)
+    
+    # check replication is still working
+    test_ticket47553_check_repl_M2_to_M1(topo)
+    
+    # check DENY rule is working
+    test_ticket47553_moddn_staging_prod_except(topo)
+    
+    test_ticket47553_final(topo)
+    
+
+
+
+if __name__ == '__main__':
+    run_isolated()
+

+ 1 - 0
ldap/schema/01core389.ldif

@@ -295,6 +295,7 @@ attributeTypes: ( 2.16.840.1.113730.3.1.2300 NAME 'nsslapd-connection-nocanon' D
 attributeTypes: ( 2.16.840.1.113730.3.1.2301 NAME 'nsslapd-plugin-logging' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
 attributeTypes: ( 2.16.840.1.113730.3.1.2302 NAME 'nsslapd-listen-backlog-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.2303 NAME 'nsslapd-ignore-time-skew' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
+attributeTypes: ( 2.16.840.1.113730.3.1.2305 NAME 'nsslapd-moddn-aci' DESC 'Netscape defined attribute type' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'Netscape Directory Server' )
 #
 # objectclasses
 #

+ 206 - 44
ldap/servers/plugins/acl/acl.c

@@ -282,7 +282,7 @@ acl_access_allowed(
 	if ( access & (ACLPB_SLAPI_ACL_WRITE_ADD | ACLPB_SLAPI_ACL_WRITE_DEL) )	{
 		access |= SLAPI_ACL_WRITE;
 	}
-
+        
 	n_edn =	slapi_entry_get_ndn ( e	);
 	e_sdn =	slapi_entry_get_sdn ( e	);
 
@@ -290,7 +290,7 @@ acl_access_allowed(
 	/* No one, even	the rootdn should be allowed to	write to the database */
 	/* jcm:	ReadOnly only applies to the public backends, the private ones */
 	/* (the	DSEs) should still be writable for configuration. */
-	if ( access & (	SLAPI_ACL_WRITE	| SLAPI_ACL_ADD	 | SLAPI_ACL_DELETE )) {
+	if ( access & (	SLAPI_ACL_WRITE	| SLAPI_ACL_ADD	 | SLAPI_ACL_DELETE | SLAPI_ACL_MODDN)) {
 		int			be_readonly, privateBackend;
 		Slapi_Backend		*be;
 
@@ -351,6 +351,20 @@ acl_access_allowed(
 	TNF_PROBE_0_DEBUG(acl_aclpbinit_start,"ACL","");
 	acl_init_aclpb ( pb, aclpb, clientDn, 0	);
 	TNF_PROBE_0_DEBUG(acl_aclpbinit_end,"ACL","");
+        
+        if ( access & SLAPI_ACL_MODDN) {
+                /* with MODDN, the entry 'e' is the destination entry.
+                 * This destination entry will be checked against a possible 'target_to'
+                 * The source entry  will be checked against a possible 'target_from'.
+                 * 'e' will be given down to acl_resource_match_aci, so no pb on this side.
+                 * For the source entry we need to keep its dn into the aclpb.
+                 */
+                entry_address *old_addr;
+                slapi_pblock_get(pb, SLAPI_TARGET_ADDRESS, &old_addr);
+                aclpb->aclpb_moddn_source_sdn = old_addr->sdn;
+
+        }
+
 
 	/* Here	we mean	if "I am trying	to add/delete "myself" to a group, etc." We
 	 * basically just want to see if the value matches the DN of the user that
@@ -767,48 +781,95 @@ print_access_control_summary( char *source, int ret_val, char *clientDn,
 
 		if ( aclpb->aclpb_authorization_sdn != NULL ) {
 
-				proxy_user = 
-					(char *)(slapi_sdn_get_ndn(aclpb->aclpb_authorization_sdn)?
-					slapi_sdn_get_ndn(aclpb->aclpb_authorization_sdn):
-					null_user);
-
-				slapi_log_error(loglevel, plugin_name, 
-		"conn=%" NSPRIu64 " op=%d (%s): %s %s on entry(%s).attr(%s) to proxy (%s)"
-		": %s\n",
-				(long long unsigned int)op->o_connid, op->o_opid,
-				source,
-				access_status,
-				right, 
-				edn,
-				attr ? attr: "NULL",
-				proxy_user,
-				acl_info[0] ? acl_info : access_reason);									
+                        proxy_user = 
+                                (char *)(slapi_sdn_get_ndn(aclpb->aclpb_authorization_sdn)?
+                                slapi_sdn_get_ndn(aclpb->aclpb_authorization_sdn):
+                                null_user);
+                        if (strcasecmp(right, access_str_moddn) == 0) {
+                                 slapi_log_error(loglevel, plugin_name,                                                
+                                        "conn=%" NSPRIu64 " op=%d (%s): %s %s on entry(%s).attr(%s) [from %s] to proxy (%s)"
+                                         ": %s\n",
+                                        (long long unsigned int)op->o_connid, op->o_opid,
+                                        source,
+                                        access_status,
+                                        right, 
+                                        edn,
+                                        attr ? attr: "NULL",
+                                        aclpb->aclpb_moddn_source_sdn ? slapi_sdn_get_dn(aclpb->aclpb_moddn_source_sdn) : "NULL", /* from entry - stored in  pblock */
+                                        proxy_user,
+                                        acl_info[0] ? acl_info : access_reason);
+
+                        } else {
+                                slapi_log_error(loglevel, plugin_name, 
+                                        "conn=%" NSPRIu64 " op=%d (%s): %s %s on entry(%s).attr(%s) to proxy (%s)"
+                                         ": %s\n",
+                                        (long long unsigned int)op->o_connid, op->o_opid,
+                                        source,
+                                        access_status,
+                                        right, 
+                                        edn,
+                                        attr ? attr: "NULL",
+                                        proxy_user,
+                                        acl_info[0] ? acl_info : access_reason);
+                        }
 		} else {
-					proxy_user = null_user;
-					slapi_log_error(loglevel, plugin_name, 
-		"conn=%" NSPRIu64 " op=%d (%s): %s %s on entry(%s).attr(%s) to proxy (%s)"
-		": %s\n",
-				(long long unsigned int)op->o_connid, op->o_opid,
-				source,
-				access_status,
-				right, 
-				edn,
-				attr ? attr: "NULL",
-				proxy_user,
-				acl_info[0] ? acl_info : access_reason);								
+                        proxy_user = null_user;
+                        if (strcasecmp(right, access_str_moddn) == 0) {
+                                slapi_log_error(loglevel, plugin_name, 
+                                        "conn=%" NSPRIu64 " op=%d (%s): %s %s on entry(%s).attr(%s) [from %s] to proxy (%s)"
+                                        ": %s\n",
+                                        (long long unsigned int)op->o_connid, op->o_opid,
+                                        source,
+                                        access_status,
+                                        right, 
+                                        edn,
+                                        attr ? attr: "NULL",
+                                        aclpb->aclpb_moddn_source_sdn ? slapi_sdn_get_dn(aclpb->aclpb_moddn_source_sdn) : "NULL", /* from entry - stored in  pblock */
+                                        proxy_user,
+                                        acl_info[0] ? acl_info : access_reason);
+                                
+                        } else {
+                                slapi_log_error(loglevel, plugin_name, 
+                                        "conn=%" NSPRIu64 " op=%d (%s): %s %s on entry(%s).attr(%s) to proxy (%s)"
+                                        ": %s\n",
+                                        (long long unsigned int)op->o_connid, op->o_opid,
+                                        source,
+                                        access_status,
+                                        right, 
+                                        edn,
+                                        attr ? attr: "NULL",
+                                        proxy_user,
+                                        acl_info[0] ? acl_info : access_reason);
+                        }
 		}
-	} else{
-		slapi_log_error(loglevel, plugin_name, 
-			"conn=%" NSPRIu64 " op=%d (%s): %s %s on entry(%s).attr(%s) to %s"
-			": %s\n",
-				(long long unsigned int)op->o_connid, op->o_opid,
-				source,
-				access_status,
-				right, 
-				edn,
-				attr ? attr: "NULL",
-				real_user,
-				acl_info[0] ? acl_info : access_reason);									
+	} else {
+                if (strcasecmp(right, access_str_moddn) == 0) {
+                        slapi_log_error(loglevel, plugin_name, 
+                                "conn=%" NSPRIu64 " op=%d (%s): %s %s on entry(%s).attr(%s) [from %s] to %s"
+                                ": %s\n",
+                                (long long unsigned int)op->o_connid, op->o_opid,
+                                source,
+                                access_status,
+                                right, 
+                                edn,
+                                attr ? attr: "NULL",
+                                aclpb->aclpb_moddn_source_sdn ? slapi_sdn_get_dn(aclpb->aclpb_moddn_source_sdn) : "NULL", /* from entry - stored in  pblock */
+                                real_user,
+                                acl_info[0] ? acl_info : access_reason);
+                        
+                } else {
+                        slapi_log_error(loglevel, plugin_name, 
+                                "conn=%" NSPRIu64 " op=%d (%s): %s %s on entry(%s).attr(%s) to %s"
+                                ": %s\n",
+                                (long long unsigned int)op->o_connid, op->o_opid,
+                                source,
+                                access_status,
+                                right, 
+                                edn,
+                                attr ? attr: "NULL",
+                                real_user,
+                                acl_info[0] ? acl_info : access_reason);
+                }
 	}
 	
 
@@ -2006,7 +2067,7 @@ acl__scan_for_acis(Acl_PBlock *aclpb, int *err)
 *	None.
 *
 **************************************************************************/
-#define ACL_RIGHTS_TARGETATTR_NOT_NEEDED  ( SLAPI_ACL_ADD | SLAPI_ACL_DELETE | SLAPI_ACL_PROXY)
+#define ACL_RIGHTS_TARGETATTR_NOT_NEEDED  ( SLAPI_ACL_ADD | SLAPI_ACL_DELETE | SLAPI_ACL_PROXY | SLAPI_ACL_MODDN)
 static int
 acl__resource_match_aci( Acl_PBlock *aclpb, aci_t *aci, int skip_attrEval, int *a_matched)
 {
@@ -2093,7 +2154,108 @@ acl__resource_match_aci( Acl_PBlock *aclpb, aci_t *aci, int skip_attrEval, int *
 			matches = (dn_matched ? ACL_TRUE: ACL_FALSE);
 		}
 	}
-	
+        
+        /* No need to look further */
+	if (matches == ACL_FALSE) {
+		goto acl__resource_match_aci_EXIT;	
+	}
+        
+	if ((aci->aci_type & ACI_TARGET_MODDN) && (res_right & SLAPI_ACL_MODDN) && (aclpb->aclpb_curr_entry_sdn)) {
+		char		*avaType;
+		struct berval	*avaValue;
+                char		 logbuf[1024];		
+
+                /* We are evaluating the moddn permission. 
+                 * The aci contains target_to and target_from 
+                 * 
+                 * target_to filter must be checked against the resource ndn that was stored in
+                 * aclpb->aclpb_curr_entry_sdn
+                 * 
+                 * target_from filter must be check against the entry ndn that is in aclpb->aclpb_moddn_source_sdn
+                 * (sdn was stored in the pblock)
+                 */
+		if (aci->target_to) { 
+                        f = aci->target_to;
+                        dn_matched = ACL_TRUE;
+                        
+                        /* Now check if the filter is a simple or substring filter */
+                        if (aci->aci_type & ACI_TARGET_MODDN_TO_PATTERN) {
+                                /* This is a filter with substring
+                                 * e.g. ldap:///uid=*,cn=accounts,dc=example,dc=com
+                                 */
+                                slapi_log_error( SLAPI_LOG_ACL, plugin_name, "moddn target_to substring: %s\n",
+                                        slapi_filter_to_string(f, logbuf, sizeof(logbuf)));
+                                if ((rv = acl_match_substring(f, (char *) res_ndn, 0 /* match suffix */)) != ACL_TRUE) {
+                                        dn_matched = ACL_FALSE;
+                                        if (rv == ACL_ERR) {
+                                                slapi_log_error(SLAPI_LOG_ACL, plugin_name,
+                                                        "acl__resource_match_aci:pattern err\n");
+                                                matches = ACL_FALSE;
+                                                goto acl__resource_match_aci_EXIT;
+                                        }
+                                }
+                        } else {
+                                /* This is a filter without substring
+                                 * e.g. ldap:///cn=accounts,dc=example,dc=com
+                                 */
+                                slapi_log_error( SLAPI_LOG_ACL, plugin_name, "moddn target_to: %s\n",
+                                        slapi_filter_to_string(f, logbuf, sizeof(logbuf)));
+                                slapi_filter_get_ava(f, &avaType, &avaValue);
+
+                                if (!slapi_dn_issuffix(res_ndn, avaValue->bv_val)) {
+                                        dn_matched = ACL_FALSE;
+                                }
+
+                        }
+                        if (aci->aci_type & ACI_TARGET_NOT) {
+                                matches = (dn_matched ? ACL_FALSE : ACL_TRUE);
+                        } else {
+                                matches = (dn_matched ? ACL_TRUE : ACL_FALSE);
+                        }
+                } /* target_to */
+                
+                if ((matches == ACL_TRUE) && (aci->target_from) && aclpb->aclpb_moddn_source_sdn) {
+                        f = aci->target_from;
+                        dn_matched = ACL_TRUE;
+                        slapi_filter_get_ava ( f, &avaType, &avaValue );
+                        
+                        /* Now check if the filter is a simple or substring filter */
+                        if (aci->aci_type & ACI_TARGET_MODDN_FROM_PATTERN) {
+                                /* This is a filter with substring
+                                 * e.g. ldap:///uid=*,cn=accounts,dc=example,dc=com
+                                 */
+                                slapi_log_error( SLAPI_LOG_ACL, plugin_name, "moddn target_from substring: %s\n",
+                                        slapi_filter_to_string(f, logbuf, sizeof(logbuf)));
+                                if ((rv = acl_match_substring(f, (char *) slapi_sdn_get_dn(aclpb->aclpb_moddn_source_sdn), 0 /* match suffix */)) != ACL_TRUE) {
+                                        dn_matched = ACL_FALSE;
+                                        if (rv == ACL_ERR) {
+                                                slapi_log_error(SLAPI_LOG_ACL, plugin_name,
+                                                        "acl__resource_match_aci:pattern err\n");
+                                                matches = ACL_FALSE;
+                                                goto acl__resource_match_aci_EXIT;
+                                        }
+                                }
+                                
+                        } else {
+                                /* This is a filter without substring
+                                 * e.g. ldap:///cn=accounts,dc=example,dc=com
+                                 */
+                                slapi_log_error( SLAPI_LOG_ACL, plugin_name, "moddn target_from: %s\n",
+                                        slapi_filter_to_string(f, logbuf, sizeof(logbuf)));
+                                if (!slapi_dn_issuffix(slapi_sdn_get_dn(aclpb->aclpb_moddn_source_sdn), avaValue->bv_val)) {
+                                        dn_matched = ACL_FALSE;
+                                }
+                                
+                        }
+
+                        if (aci->aci_type & ACI_TARGET_NOT) {
+                                matches = (dn_matched ? ACL_FALSE : ACL_TRUE);
+                        } else {
+                                matches = (dn_matched ? ACL_TRUE: ACL_FALSE);
+                        }
+                }
+	}
+        
 	/* No need to look further */
 	if (matches == ACL_FALSE) {
 		goto acl__resource_match_aci_EXIT;	

+ 29 - 18
ldap/servers/plugins/acl/acl.h

@@ -117,6 +117,8 @@ static char* const aci_targetdn 			= "target";
 static char* const aci_targetattr 			= "targetattr";
 static char* const aci_targetattrfilters 	= "targattrfilters";
 static char* const aci_targetfilter 		= "targetfilter";
+static char* const aci_target_to                = "target_to";
+static char* const aci_target_from              = "target_from";
 
 static char* const LDAP_URL_prefix_core 	= "ldap://";
 static char* const LDAPS_URL_prefix_core 	= "ldaps://";
@@ -132,6 +134,7 @@ static char* const access_str_delete	= "delete";
 static char* const access_str_add		= "add";
 static char* const access_str_selfwrite = "selfwrite";
 static char* const access_str_proxy 	= "proxy";
+static char* const access_str_moddn 	= "moddn";
 
 #define ACL_INIT_ATTR_ARRAY 5 
 
@@ -256,24 +259,27 @@ typedef struct aci {
 ** 
 */
 
-#define ACI_TARGET_MACRO_DN			(int)0x000001
-#define ACI_TARGET_FILTER_MACRO_DN	(int)0x000002
-#define ACI_TARGET_DN				(int)0x000100	/* target has DN */
-#define ACI_TARGET_ATTR				(int)0x000200	/* target is an attr */
-#define ACI_TARGET_PATTERN			(int)0x000400	/* target has some patt */
-#define ACI_TARGET_FILTER			(int)0x000800	/* target has a filter */
-#define	ACI_ACLTXT					(int)0x001000	/* ACI has text only */
-#define	ACI_TARGET_NOT				(int)0x002000	/* it's a !=  */
-#define	ACI_TARGET_ATTR_NOT			(int)0x004000	/* It's a != manager */
-#define ACI_TARGET_FILTER_NOT		(int)0x008000	/* It's a != filter */
-#define ACI_UNUSED2					(int)0x010000    /* Unused */
-#define ACI_HAS_ALLOW_RULE			(int)0x020000	/* allow (...) */
-#define ACI_HAS_DENY_RULE			(int)0x040000	/* deny (...) */
-#define ACI_CONTAIN_NOT_USERDN		(int)0x080000	/* userdn != blah */
-#define ACI_TARGET_ATTR_ADD_FILTERS	(int)0x100000
-#define ACI_TARGET_ATTR_DEL_FILTERS	(int)0x200000
-#define ACI_CONTAIN_NOT_GROUPDN		(int)0x400000	/* groupdn != blah */
-#define ACI_CONTAIN_NOT_ROLEDN		(int)0x800000
+#define ACI_TARGET_MACRO_DN		(int)0x0000001
+#define ACI_TARGET_FILTER_MACRO_DN	(int)0x0000002
+#define ACI_TARGET_DN			(int)0x0000100	/* target has DN */
+#define ACI_TARGET_ATTR			(int)0x0000200	/* target is an attr */
+#define ACI_TARGET_PATTERN		(int)0x0000400	/* target has some patt */
+#define ACI_TARGET_FILTER		(int)0x0000800	/* target has a filter */
+#define	ACI_ACLTXT			(int)0x0001000	/* ACI has text only */
+#define	ACI_TARGET_NOT			(int)0x0002000	/* it's a !=  */
+#define	ACI_TARGET_ATTR_NOT		(int)0x0004000	/* It's a != manager */
+#define ACI_TARGET_FILTER_NOT		(int)0x0008000	/* It's a != filter */
+#define ACI_UNUSED2			(int)0x0010000    /* Unused */
+#define ACI_HAS_ALLOW_RULE		(int)0x0020000	/* allow (...) */
+#define ACI_HAS_DENY_RULE		(int)0x0040000	/* deny (...) */
+#define ACI_CONTAIN_NOT_USERDN		(int)0x0080000	/* userdn != blah */
+#define ACI_TARGET_ATTR_ADD_FILTERS	(int)0x0100000
+#define ACI_TARGET_ATTR_DEL_FILTERS	(int)0x0200000
+#define ACI_CONTAIN_NOT_GROUPDN		(int)0x0400000	/* groupdn != blah */
+#define ACI_CONTAIN_NOT_ROLEDN		(int)0x0800000
+#define ACI_TARGET_MODDN                (int)0x1000000
+#define ACI_TARGET_MODDN_FROM_PATTERN   (int)0x2000000
+#define ACI_TARGET_MODDN_TO_PATTERN     (int)0x4000000
 
 	int				aci_access;
 
@@ -320,6 +326,8 @@ typedef struct aci {
 	struct slapi_filter		*targetFilter;  /* Target has a filter */
 	Targetattrfilter		**targetAttrAddFilters;
 	Targetattrfilter		**targetAttrDelFilters;
+        Slapi_Filter                    *target_to;             /* Target is the destination DN of moddn */
+        Slapi_Filter                    *target_from;           /* Target is the source DN of moddn */
 	char					*aclName;		/* ACL name */
 	struct ACLListHandle	*aci_handle;	/*handle of the ACL */
 	aciMacro				*aci_macro;
@@ -520,6 +528,9 @@ struct acl_pblock {
 	Slapi_Entry				*aclpb_filter_test_entry;	/* Scratch entry */
 	aci_t					*aclpb_curr_aci;
 	char					*aclpb_Evalattr;	/* The last attr evaluated  */
+        
+        /* Source entry (MODDN) */
+        Slapi_DN                                *aclpb_moddn_source_sdn; /* This is a pointer into the pb, do not free it */
 
 	/* Plist and eval info */
 	ACLEvalHandle_t			*aclpb_acleval;		/* acleval handle for evaluation */

+ 29 - 11
ldap/servers/plugins/acl/aclanom.c

@@ -544,18 +544,36 @@ aclanom_match_profile (Slapi_PBlock *pb, struct acl_pblock *aclpb, Slapi_Entry *
 		if ( result == LDAP_SUCCESS) {
 			const char				*aci_ndn;
 			aci_ndn = slapi_sdn_get_ndn (acl_anom_profile->anom_targetinfo[i].anom_target);
-
-			slapi_log_error(loglevel, plugin_name, 
-				"conn=%" NSPRIu64 " op=%d: Allow access on entry(%s).attr(%s) to anonymous: acidn=\"%s\"\n",
-				(long long unsigned int)op->o_connid, op->o_opid,
-				ndn,
-				attr ? attr:"NULL",
-				aci_ndn);
+                        if (access & SLAPI_ACL_MODDN) {
+                                slapi_log_error(loglevel, plugin_name, 
+                                        "conn=%" NSPRIu64 " op=%d: Allow access on entry(%s).attr(%s) (from %s) to anonymous: acidn=\"%s\"\n",
+                                        (long long unsigned int)op->o_connid, op->o_opid,
+                                        ndn,
+                                        attr ? attr:"NULL",
+                                        aclpb->aclpb_moddn_source_sdn ? slapi_sdn_get_dn(aclpb->aclpb_moddn_source_sdn) : "NULL",
+                                        aci_ndn);
+                                
+                        } else {
+                                slapi_log_error(loglevel, plugin_name, 
+                                        "conn=%" NSPRIu64 " op=%d: Allow access on entry(%s).attr(%s) to anonymous: acidn=\"%s\"\n",
+                                        (long long unsigned int)op->o_connid, op->o_opid,
+                                        ndn,
+                                        attr ? attr:"NULL",
+                                        aci_ndn);
+                        }			
 		} else {
-			slapi_log_error(loglevel, plugin_name,
-				"conn=%" NSPRIu64 " op=%d: Deny access on entry(%s).attr(%s) to anonymous\n",
-				(long long unsigned int)op->o_connid, op->o_opid,
-				ndn, attr ? attr:"NULL" );
+                        if (access & SLAPI_ACL_MODDN) {
+                                slapi_log_error(loglevel, plugin_name,
+                                        "conn=%" NSPRIu64 " op=%d: Deny access on entry(%s).attr(%s) (from %s) to anonymous\n",
+                                        (long long unsigned int)op->o_connid, op->o_opid,
+                                        ndn, attr ? attr:"NULL" ,
+                                        aclpb->aclpb_moddn_source_sdn ? slapi_sdn_get_dn(aclpb->aclpb_moddn_source_sdn) : "NULL");
+                        } else {
+                                slapi_log_error(loglevel, plugin_name,
+                                        "conn=%" NSPRIu64 " op=%d: Deny access on entry(%s).attr(%s) to anonymous\n",
+                                        (long long unsigned int)op->o_connid, op->o_opid,
+                                        ndn, attr ? attr:"NULL" );
+                        }			
 		}
 	}
 

+ 7 - 0
ldap/servers/plugins/acl/acleffectiverights.c

@@ -481,6 +481,13 @@ _ger_get_entry_rights (
 	}
 	slapi_rdn_free ( &rdn );
 
+         if (acl_access_allowed(gerpb, e, NULL, NULL, SLAPI_ACL_MODDN) == LDAP_SUCCESS) {
+                slapi_log_error (SLAPI_LOG_ACL, plugin_name,
+			"_ger_get_entry_rights: SLAPI_ACL_MODDN %s\n", slapi_entry_get_ndn (e) );
+                /* n - rename e */
+		entryrights |= SLAPI_ACL_MODDN;
+		_append_gerstr(gerstr, gerstrsize, gerstrcap, "n", NULL);
+        }
 	if ( entryrights == 0 )
 	{
 		_append_gerstr(gerstr, gerstrsize, gerstrcap, "none", NULL);

+ 109 - 8
ldap/servers/plugins/acl/aclparse.c

@@ -254,6 +254,9 @@ __aclp__parse_aci(char *str, aci_t  *aci_item, char **errbuf)
 	int		targetdnlen = strlen (aci_targetdn);
 	int		tfilterlen = strlen(aci_targetfilter);
    	int 	targetattrfilterslen = strlen(aci_targetattrfilters);
+        int     target_to_len        = strlen(aci_target_to);
+        int     target_from_len      = strlen(aci_target_from);
+        PRBool  is_target_to;
 
 	__acl_strip_leading_space( &str );
 
@@ -348,7 +351,72 @@ __aclp__parse_aci(char *str, aci_t  *aci_item, char **errbuf)
 			/* save the filter string */
             aci_item->targetFilterStr = tmpstr;
 
-		} else if (strncmp(str, aci_targetdn, targetdnlen) == 0) {
+		} else if ((strncmp(str, aci_target_to, target_to_len) == 0) || (strncmp(str, aci_target_from, target_from_len) == 0)) {
+                        /* This is important to make this test before aci_targetdn
+                         * because aci_targetdn also match aci_target_to/aci_target_from
+                         *  */
+                        char		*tstr = NULL;
+			size_t    LDAP_URL_prefix_len = 0;
+			size_t	tmplen = 0;
+
+                        /* Keep a copy of the target attr */
+                        type = ACI_TARGET_MODDN;
+			if (strncmp(str, aci_target_to, target_to_len) == 0) {
+                                if (aci_item->target_to) {
+                                        return (ACL_SYNTAX_ERR);
+                                }
+                                is_target_to = PR_TRUE;
+                        } else {
+                                if (aci_item->target_from) {
+                                        return (ACL_SYNTAX_ERR);
+                                }
+                                is_target_to = PR_FALSE;
+			}
+			if ( (s = strstr( str, "!=" )) != NULL ) {
+				type |= ACI_TARGET_NOT;
+				strncpy(s, " ", 1);
+			}			
+			if ( (s = strchr( str, '=' )) != NULL ) {
+				value = s + 1;
+				__acl_strip_leading_space(&value);
+				__acl_strip_trailing_space(value);
+				len =  strlen ( value );
+				/* strip double quotes */
+				if (*value == '"' &&  value[len-1] == '"') {
+					value[len-1] = '\0';
+					value++;
+				}	
+				__acl_strip_leading_space(&value);
+			} else {
+				return ( ACL_SYNTAX_ERR );
+			}
+			if (0 ==
+				strncasecmp(value, LDAP_URL_prefix, strlen(LDAP_URL_prefix))) {
+				LDAP_URL_prefix_len = strlen(LDAP_URL_prefix);
+			} else if (0 == strncasecmp(value, LDAPS_URL_prefix,
+										strlen(LDAPS_URL_prefix))) {
+				LDAP_URL_prefix_len = strlen(LDAPS_URL_prefix);
+			} else {
+				return ( ACL_SYNTAX_ERR );
+			}
+
+			value += LDAP_URL_prefix_len;
+			rv = slapi_dn_normalize_case_ext(value, 0, &tmpstr, &tmplen);
+			if (rv < 0) {
+				return ACL_SYNTAX_ERR;
+			} else if (rv == 0) { /* value passed in; not null terminated */
+				*(tmpstr + tmplen) = '\0';
+			}
+                        
+                        /* Now prepare the filter */
+                        if (strncmp(str, aci_target_to, target_to_len) == 0) {
+                                tstr = slapi_ch_smprintf("(%s=%s)", aci_target_to, tmpstr);
+                        } else {
+                                tstr = slapi_ch_smprintf("(%s=%s)", aci_target_from, tmpstr);
+                        }
+                        f = slapi_str2filter ( tstr );
+			slapi_ch_free_string ( &tstr );
+                } else if (strncmp(str, aci_targetdn, targetdnlen) == 0) {
 			char		*tstr = NULL;
 			size_t    LDAP_URL_prefix_len = 0;
 			size_t	tmplen = 0;
@@ -407,7 +475,7 @@ __aclp__parse_aci(char *str, aci_t  *aci_item, char **errbuf)
 				f = slapi_str2filter ( tstr );				
 			}
 			slapi_ch_free_string ( &tstr );
-		} else {
+                } else {
 			/* did start with a 't' but was not a recognsied keyword */
 			return(ACL_SYNTAX_ERR);
 		}
@@ -419,7 +487,7 @@ __aclp__parse_aci(char *str, aci_t  *aci_item, char **errbuf)
 		*/
 		if (f == NULL) {
 			/* The following types require a filter to have been created */
-			if (type & ACI_TARGET_DN)
+			if (type & (ACI_TARGET_DN | ACI_TARGET_MODDN))
 				return ACL_TARGET_FILTER_ERR;
 			else if (type & ACI_TARGET_FILTER) 
 				return ACL_TARGETFILTER_ERR;
@@ -427,16 +495,23 @@ __aclp__parse_aci(char *str, aci_t  *aci_item, char **errbuf)
 			int	filterChoice;
 
 			filterChoice = slapi_filter_get_choice ( f );
-			if ( (type & ACI_TARGET_DN) &&
+			if ( (type & (ACI_TARGET_DN | ACI_TARGET_MODDN)) &&
 				( filterChoice == LDAP_FILTER_PRESENT)) {
 					slapi_log_error(SLAPI_LOG_ACL, plugin_name,
 					"acl__parse_aci: Unsupported filter type:%d\n", filterChoice);
 				return(ACL_SYNTAX_ERR);
 			} else if (( filterChoice == LDAP_FILTER_SUBSTRINGS) &&
 					(type & ACI_TARGET_DN)) {
-				type &= ~ACI_TARGET_DN;
+                                type &= ~ACI_TARGET_DN;
 				type |= ACI_TARGET_PATTERN;
-			}
+			} else if (( filterChoice == LDAP_FILTER_SUBSTRINGS) &&
+                                        (type & ACI_TARGET_MODDN)) {
+                                if (is_target_to) {
+                                        type |= ACI_TARGET_MODDN_TO_PATTERN;
+                                } else {
+                                        type |= ACI_TARGET_MODDN_FROM_PATTERN;
+                                }
+                        }  
 		}
 
 		if ((type & ACI_TARGET_DN) ||
@@ -459,7 +534,29 @@ __aclp__parse_aci(char *str, aci_t  *aci_item, char **errbuf)
 			} else {
 				aci_item->targetFilter = f;
 			}
-		}
+		} else if (type & ACI_TARGET_MODDN) {
+                        if (is_target_to) {
+                                if (aci_item->target_to) {
+                                        /* There is something already. ERROR */
+                                        slapi_log_error(SLAPI_LOG_ACL, plugin_name,
+                                                "Multiple targets (target_to) in the ACL syntax\n");
+                                        slapi_filter_free(f, 1);
+                                        return(ACL_SYNTAX_ERR);
+                                } else {
+                                        aci_item->target_to = f;
+                                }                               
+                        } else {
+                                if (aci_item->target_from) {
+                                        /* There is something already. ERROR */
+                                        slapi_log_error(SLAPI_LOG_ACL, plugin_name,
+					 "Multiple targets (target_from) in the ACL syntax\n");
+                                        slapi_filter_free(f, 1);
+                                        return(ACL_SYNTAX_ERR);
+                                } else {
+                                        aci_item->target_from = f;
+                                }                               
+                        }     
+                } 
 		break; /* 't' */
 		default:
 			/* Here the keyword did not start with 'v' ot 't' so error */
@@ -1346,6 +1443,8 @@ static int get_acl_rights_as_int( char * strValue)
 		return (SLAPI_ACL_SELF | SLAPI_ACL_WRITE);
 	else if (strcasecmp (strValue, "all") == 0 )
 		return SLAPI_ACL_ALL;
+        else if (strcasecmp (strValue, "moddn") == 0)
+                return SLAPI_ACL_MODDN;
 	else
 		return -1; /* error */
 }
@@ -1389,7 +1488,9 @@ acl_access2str(int access)
 		return access_str_write;
 	} else if (access & SLAPI_ACL_PROXY ) {
 		return access_str_proxy;
-	}
+	} else if (access & SLAPI_ACL_MODDN) {
+                return access_str_moddn;
+        }
 
 	return NULL;
 }

+ 1 - 1
ldap/servers/plugins/acl/aclplugin.c

@@ -393,7 +393,7 @@ acl_access_allowed_main ( Slapi_PBlock *pb, Slapi_Entry *e, char **attrs,
 	/* generate the appropriate error message */
 	if ( ( rc != LDAP_SUCCESS ) && errbuf && 
 		 ( ACLPLUGIN_ACCESS_GET_EFFECTIVE_RIGHTS != flags ) &&
-		 ( access & ( SLAPI_ACL_WRITE | SLAPI_ACL_ADD | SLAPI_ACL_DELETE ))) {
+		 ( access & ( SLAPI_ACL_WRITE | SLAPI_ACL_ADD | SLAPI_ACL_DELETE | SLAPI_ACL_MODDN ))) {
 
 		char	*edn  = slapi_entry_get_dn ( e );
 

+ 4 - 1
ldap/servers/plugins/acl/aclutil.c

@@ -475,7 +475,10 @@ acl_gen_err_msg(int access, char *edn, char *attr, char **errbuf)
 	} else if ( access & SLAPI_ACL_DELETE ) {
 		line = PR_smprintf(
 			"Insufficient 'delete' privilege to delete the entry '%s'.\n",edn);
-	}
+	} else if ( access & SLAPI_ACL_MODDN ) {
+                line = PR_smprintf(
+			"Insufficient 'moddn' privilege to move an entry to '%s'.\n",edn);
+        }
 	aclutil_str_append(errbuf, line );
 
 	if (line) {

+ 46 - 13
ldap/servers/slapd/back-ldbm/ldbm_modrdn.c

@@ -121,6 +121,7 @@ ldbm_back_modrdn( Slapi_PBlock *pb )
     int opreturn = 0;
     int free_modrdn_existing_entry = 0;
     int not_an_error = 0;
+    int support_moddn_aci;
 
     /* sdn & parentsdn need to be initialized before "goto *_return" */
     slapi_sdn_init(&dn_newdn);
@@ -143,6 +144,11 @@ ldbm_back_modrdn( Slapi_PBlock *pb )
         return( -1 );
     } 
 
+    /* In case of support of 'moddn' permission in aci, this permission will
+     * be tested rather than the SLAPI_ACL_ADD
+     */
+    support_moddn_aci = config_get_moddn_aci();
+    
     /* dblayer_txn_init needs to be called before "goto error_return" */
     dblayer_txn_init(li,&txn);
     /* the calls to perform searches require the parent txn if any
@@ -594,12 +600,22 @@ ldbm_back_modrdn( Slapi_PBlock *pb )
             }
             else
             {
-                ldap_result_code= plugin_call_acl_plugin (pb, newparententry->ep_entry, NULL, NULL, SLAPI_ACL_ADD, ACLPLUGIN_ACCESS_DEFAULT, &errbuf );
-                if ( ldap_result_code != LDAP_SUCCESS )
-                {
-                    ldap_result_message= errbuf;
-                    LDAPDebug( LDAP_DEBUG_TRACE, "No access to new superior.\n", 0, 0, 0 );
-                    goto error_return;
+                if (support_moddn_aci) {
+                    /* aci permission requires 'moddn' right to allow a MODDN */
+                    ldap_result_code = plugin_call_acl_plugin(pb, newparententry->ep_entry, NULL, NULL, SLAPI_ACL_MODDN, ACLPLUGIN_ACCESS_DEFAULT, &errbuf);
+                    if (ldap_result_code != LDAP_SUCCESS) {
+                            ldap_result_message = errbuf;
+                            LDAPDebug(LDAP_DEBUG_TRACE, "No 'moddn' access to new superior.\n", 0, 0, 0);
+                            goto error_return;
+                    }
+                } else {
+                    /* aci permission requires 'add' right to allow a MODDN (old style) */
+                    ldap_result_code = plugin_call_acl_plugin (pb, newparententry->ep_entry, NULL, NULL, SLAPI_ACL_ADD, ACLPLUGIN_ACCESS_DEFAULT, &errbuf );
+                    if (ldap_result_code != LDAP_SUCCESS) {
+                                ldap_result_message = errbuf;
+                                LDAPDebug(LDAP_DEBUG_TRACE, "No 'add' access to new superior.\n", 0, 0, 0);
+                                goto error_return;
+                        }
                 }
             }
         
@@ -641,16 +657,33 @@ ldbm_back_modrdn( Slapi_PBlock *pb )
                 ldap_result_code= LDAP_OPERATIONS_ERROR;
                 goto error_return;
             }
-        
+            
             /* JCMACL - Should be performed before the child check. */
             /* JCMACL - Why is the check performed against the copy, rather than the existing entry? */
-            ldap_result_code = plugin_call_acl_plugin (pb, ec->ep_entry,
-                                    NULL /*attr*/, NULL /*value*/, SLAPI_ACL_WRITE,
-                                    ACLPLUGIN_ACCESS_MODRDN,  &errbuf );
-            if ( ldap_result_code != LDAP_SUCCESS )
             {
-                goto error_return;
-            }
+                Slapi_RDN *new_rdn;
+                Slapi_RDN *old_rdn;
+
+                /* Taken from the entry */
+                old_rdn = slapi_entry_get_srdn(ec->ep_entry);
+
+                /* Taken from the request */
+                new_rdn = slapi_rdn_new();
+                slapi_sdn_get_rdn(&dn_newrdn, new_rdn);
+
+                /* Only if we change the RDN value, we need the write access to the entry */
+                if (slapi_rdn_compare(old_rdn, new_rdn)) {
+                        ldap_result_code = plugin_call_acl_plugin(pb, ec->ep_entry,
+                                NULL /*attr*/, NULL /*value*/, SLAPI_ACL_WRITE,
+                                ACLPLUGIN_ACCESS_MODRDN, &errbuf);
+                }
+
+                slapi_rdn_free(&new_rdn);
+
+                if (ldap_result_code != LDAP_SUCCESS) {
+                        goto error_return;
+                }
+            } 
         
             /* Set the new dn to the copy of the entry */
             slapi_entry_set_sdn( ec->ep_entry, &dn_newdn );

+ 16 - 1
ldap/servers/slapd/config.c

@@ -225,12 +225,13 @@ slapd_bootstrap_config(const char *configdir)
 			char syntaxlogging[BUFSIZ];
 			char plugintracking[BUFSIZ];
 			char dn_validate_strict[BUFSIZ];
+                        char moddn_aci[BUFSIZ];
 			Slapi_DN plug_dn;
 
 			workpath[0] = loglevel[0] = maxdescriptors[0] = '\0';
 			val[0] = logenabled[0] = schemacheck[0] = syntaxcheck[0] = '\0';
 			syntaxlogging[0] = _localuser[0] = '\0';
-			plugintracking [0] = dn_validate_strict[0] = '\0';
+			plugintracking [0] = dn_validate_strict[0] = moddn_aci[0] ='\0';
 
 			/* Convert LDIF to entry structures */
 			slapi_sdn_init_ndn_byref(&plug_dn, PLUGIN_BASE_DN);
@@ -460,6 +461,20 @@ slapd_bootstrap_config(const char *configdir)
 								CONFIG_PLUGIN_BINDDN_TRACKING_ATTRIBUTE, errorbuf);
 					}
 				}
+                                
+                                /* see if we allow moddn aci */
+                                if (!moddn_aci[0] &&
+					entry_has_attr_and_value(e, CONFIG_MODDN_ACI_ATTRIBUTE,
+											 moddn_aci, sizeof(moddn_aci)))
+				{
+					if (config_set_moddn_aci(CONFIG_MODDN_ACI_ATTRIBUTE,
+								moddn_aci, errorbuf, CONFIG_APPLY)
+								!= LDAP_SUCCESS)
+					{
+						LDAPDebug(LDAP_DEBUG_ANY, "%s: %s: %s\n", configfile,
+								  CONFIG_MODDN_ACI_ATTRIBUTE, errorbuf);
+					}
+				}
 
 				/* see if we need to enable syntax checking */
 				if (!syntaxcheck[0] &&

+ 33 - 0
ldap/servers/slapd/libglobs.c

@@ -243,6 +243,7 @@ slapi_onoff_t init_attrname_exceptions;
 slapi_onoff_t init_return_exact_case;
 slapi_onoff_t init_result_tweak;
 slapi_onoff_t init_plugin_track;
+slapi_onoff_t init_moddn_aci;
 slapi_onoff_t init_lastmod;
 slapi_onoff_t init_readonly;
 slapi_onoff_t init_accesscontrol;
@@ -841,6 +842,11 @@ static struct config_get_and_set {
 		NULL, 0,
 		(void**)&global_slapdFrontendConfig.plugin_track,
 		CONFIG_ON_OFF, NULL, &init_plugin_track},
+        {CONFIG_MODDN_ACI_ATTRIBUTE, config_set_moddn_aci,
+		NULL, 0,
+		(void**)&global_slapdFrontendConfig.moddn_aci,
+		CONFIG_ON_OFF, (ConfigGetFunc)config_get_moddn_aci,
+                &init_moddn_aci},                
 	{CONFIG_ATTRIBUTE_NAME_EXCEPTION_ATTRIBUTE, config_set_attrname_exceptions,
 		NULL, 0,
 		(void**)&global_slapdFrontendConfig.attrname_exceptions,
@@ -1424,6 +1430,7 @@ FrontendConfig_init () {
   init_schemamod = cfg->schemamod = LDAP_ON;
   init_syntaxcheck = cfg->syntaxcheck = LDAP_OFF;
   init_plugin_track = cfg->plugin_track = LDAP_OFF;
+  init_moddn_aci = cfg->moddn_aci = LDAP_ON;
   init_syntaxlogging = cfg->syntaxlogging = LDAP_OFF;
   init_dn_validate_strict = cfg->dn_validate_strict = LDAP_OFF;
   init_ds4_compatible_schema = cfg->ds4_compatible_schema = LDAP_OFF;
@@ -3249,6 +3256,20 @@ config_set_plugin_tracking( const char *attrname, char *value, char *errorbuf, i
   return retVal;
 }
 
+int
+config_set_moddn_aci( const char *attrname, char *value, char *errorbuf, int apply ) {
+  int retVal = LDAP_SUCCESS;
+  slapdFrontendConfig_t *slapdFrontendConfig = getFrontendConfig();
+
+  retVal = config_set_onoff ( attrname,
+							  value,
+							  &(slapdFrontendConfig->moddn_aci),
+							  errorbuf,
+							  apply);
+
+  return retVal;
+}
+
 int
 config_set_security( const char *attrname, char *value, char *errorbuf, int apply ) {
   int retVal = LDAP_SUCCESS;
@@ -5061,6 +5082,18 @@ config_get_result_tweak() {
   return retVal; 
 }
 
+int
+config_get_moddn_aci() {
+  slapdFrontendConfig_t *slapdFrontendConfig = getFrontendConfig();
+  int retVal;
+  
+  CFG_ONOFF_LOCK_READ(slapdFrontendConfig);
+  retVal = (int)slapdFrontendConfig->moddn_aci;
+  CFG_ONOFF_UNLOCK_READ(slapdFrontendConfig);
+
+  return retVal;
+}
+
 int
 config_get_security() {
   slapdFrontendConfig_t *slapdFrontendConfig = getFrontendConfig();

+ 2 - 0
ldap/servers/slapd/proto-slap.h

@@ -289,6 +289,7 @@ int config_set_pagedsizelimit( const char *attrname, char *value, char *errorbuf
 int config_set_lastmod( const char *attrname, char *value, char *errorbuf, int apply );
 int config_set_nagle( const char *attrname, char *value, char *errorbuf, int apply );
 int config_set_accesscontrol( const char *attrname, char *value, char *errorbuf, int apply );
+int config_set_moddn_aci( const char *attrname, char *value, char *errorbuf, int apply );
 int config_set_security( const char *attrname, char *value, char *errorbuf, int apply );
 int config_set_readonly( const char *attrname, char *value, 	char *errorbuf, int apply );
 int config_set_schemacheck( const char *attrname, char *value, char *errorbuf, int apply );
@@ -470,6 +471,7 @@ int config_get_nagle();
 int config_get_accesscontrol();
 int config_get_return_exact_case();
 int config_get_result_tweak();
+int config_get_moddn_aci();
 int config_get_security();
 int config_get_schemacheck();
 int config_get_syntaxcheck();

+ 2 - 0
ldap/servers/slapd/slap.h

@@ -2102,6 +2102,7 @@ typedef struct _slapdEntryPoints {
 #define CONFIG_AUDITLOG_LIST_ATTRIBUTE "nsslapd-auditlog-list"
 #define CONFIG_REWRITE_RFC1274_ATTRIBUTE "nsslapd-rewrite-rfc1274"
 #define CONFIG_PLUGIN_BINDDN_TRACKING_ATTRIBUTE "nsslapd-plugin-binddn-tracking"
+#define CONFIG_MODDN_ACI_ATTRIBUTE "nsslapd-moddn-aci"
 
 #define CONFIG_CONFIG_ATTRIBUTE "nsslapd-config"
 #define CONFIG_INSTDIR_ATTRIBUTE "nsslapd-instancedir"
@@ -2257,6 +2258,7 @@ typedef struct _slapdFrontendConfig {
   char **include;
   char **plugin;
   slapi_onoff_t plugin_track;
+  slapi_onoff_t moddn_aci;
   struct pw_scheme *pw_storagescheme;
 
   slapi_onoff_t pwpolicy_local;

+ 6 - 1
ldap/servers/slapd/slapi-plugin.h

@@ -237,7 +237,12 @@ NSPR_API(PRUint32) PR_fprintf(struct PRFileDesc* fd, const char *fmt, ...)
 #define SLAPI_ACL_SELF		0x40
 #define SLAPI_ACL_PROXY		0x80
 #define SLAPI_ACL_ALL		0x7f
-
+/* Values 0x200 and 0x400 are booked (acl.h) by
+ * ACLPB_SLAPI_ACL_WRITE_ADD
+ * ACLPB_SLAPI_ACL_WRITE_DEL
+ */
+#define SLAPI_ACL_MODDN         0x0800
+        
 
 /*
  * filter types