Blob Blame Raw
From 279489884f56cfc97d1ad9afdf92da3ad3b05b70 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Fri, 11 May 2018 10:53:06 -0400
Subject: [PATCH] Ticket 49671 - Readonly replicas should not write internal
 ops to changelog

Bug Description:  When a hub receives an update that triggers the memberOf
                  plugin, but that interal operation has no csn and that
                  causes the update to the changelog to fail and break
                  replication.

Fix Description:  Do not write internal updates with no csns to the changelog
                  on read-only replicas.

https://pagure.io/389-ds-base/issue/49671

Reviewed by: simon, tbordaz, and lkrispen (Thanks!!!)

(cherry picked from commit afb755bd95f1643665ea34c5a5fa2bb26bfa21b9)
---
 .../tests/suites/replication/cascading_test.py     | 150 +++++++++++++++++++++
 ldap/servers/plugins/replication/repl5_plugins.c   |  10 ++
 2 files changed, 160 insertions(+)
 create mode 100644 dirsrvtests/tests/suites/replication/cascading_test.py

diff --git a/dirsrvtests/tests/suites/replication/cascading_test.py b/dirsrvtests/tests/suites/replication/cascading_test.py
new file mode 100644
index 000000000..7331f20e9
--- /dev/null
+++ b/dirsrvtests/tests/suites/replication/cascading_test.py
@@ -0,0 +1,150 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2018 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import logging
+import pytest
+import os
+import ldap
+from lib389._constants import *
+from lib389.replica import ReplicationManager
+from lib389.plugins import MemberOfPlugin
+from lib389.agreement import Agreements
+from lib389.idm.user import UserAccount, TEST_USER_PROPERTIES
+from lib389.idm.group import Groups
+from lib389.topologies import topology_m1h1c1 as topo
+
+DEBUGGING = os.getenv("DEBUGGING", default=False)
+if DEBUGGING:
+    logging.getLogger(__name__).setLevel(logging.DEBUG)
+else:
+    logging.getLogger(__name__).setLevel(logging.INFO)
+log = logging.getLogger(__name__)
+
+BIND_DN = 'uid=tuser1,ou=People,dc=example,dc=com'
+BIND_RDN = 'tuser1'
+
+
+def config_memberof(server):
+    """Configure memberOf plugin and configure fractional
+    to prevent total init to send memberof
+    """
+
+    memberof = MemberOfPlugin(server)
+    memberof.enable()
+    memberof.set_autoaddoc('nsMemberOf')
+    server.restart()
+    agmts = Agreements(server)
+    for agmt in agmts.list():
+        log.info('update %s to add nsDS5ReplicatedAttributeListTotal' % agmt.dn)
+        agmt.replace_many(('nsDS5ReplicatedAttributeListTotal', '(objectclass=*) $ EXCLUDE '),
+                          ('nsDS5ReplicatedAttributeList', '(objectclass=*) $ EXCLUDE memberOf'))
+
+
+def test_basic_with_hub(topo):
+    """Check that basic operations work in cascading replication, this includes
+    testing plugins that perform internal operatons, and replicated password
+    policy state attributes.
+
+    :id: 4ac85552-45bc-477b-89a4-226dfff8c6cc
+    :setup: 1 master, 1 hub, 1 consumer
+    :steps:
+        1. Enable memberOf plugin and set password account lockout settings
+        2. Restart the instance
+        3. Add a user
+        4. Add a group
+        5. Test that the replication works
+        6. Add the user as a member to the group
+        7. Test that the replication works
+        8. Issue bad binds to update passwordRetryCount
+        9. Test that replicaton works
+        10. Check that passwordRetyCount was replicated
+    :expectedresults:
+        1. Should be a success
+        2. Should be a success
+        3. Should be a success
+        4. Should be a success
+        5. Should be a success
+        6. Should be a success
+        7. Should be a success
+        8. Should be a success
+        9. Should be a success
+        10. Should be a success
+    """
+
+    repl_manager = ReplicationManager(DEFAULT_SUFFIX)
+    master = topo.ms["master1"]
+    consumer = topo.cs["consumer1"]
+    hub = topo.hs["hub1"]
+
+    for inst in topo:
+        config_memberof(inst)
+        inst.config.set('passwordlockout', 'on')
+        inst.config.set('passwordlockoutduration', '60')
+        inst.config.set('passwordmaxfailure', '3')
+        inst.config.set('passwordIsGlobalPolicy', 'on')
+
+    # Create user
+    user1 = UserAccount(master, BIND_DN)
+    user_props = TEST_USER_PROPERTIES.copy()
+    user_props.update({'sn': BIND_RDN,
+                       'cn': BIND_RDN,
+                       'uid': BIND_RDN,
+                       'inetUserStatus': '1',
+                       'objectclass': 'extensibleObject',
+                       'userpassword': PASSWORD})
+    user1.create(properties=user_props, basedn=SUFFIX)
+
+    # Create group
+    groups = Groups(master, DEFAULT_SUFFIX)
+    group = groups.create(properties={'cn': 'group'})
+
+    # Test replication
+    repl_manager.test_replication(master, consumer)
+
+    # Trigger memberOf plugin by adding user to group
+    group.replace('member', user1.dn)
+
+    # Test replication once more
+    repl_manager.test_replication(master, consumer)
+
+    # Issue bad password to update passwordRetryCount
+    try:
+        master.simple_bind_s(user1.dn, "badpassword")
+    except:
+        pass
+
+    # Test replication one last time
+    master.simple_bind_s(DN_DM, PASSWORD)
+    repl_manager.test_replication(master, consumer)
+
+    # Finally check if passwordRetyCount was replicated to the hub and consumer
+    user1 = UserAccount(hub, BIND_DN)
+    count = user1.get_attr_val_int('passwordRetryCount')
+    if count is None:
+        log.fatal('PasswordRetyCount was not replicated to hub')
+        assert False
+    if int(count) != 1:
+        log.fatal('PasswordRetyCount has unexpected value: {}'.format(count))
+        assert False
+
+    user1 = UserAccount(consumer, BIND_DN)
+    count = user1.get_attr_val_int('passwordRetryCount')
+    if count is None:
+        log.fatal('PasswordRetyCount was not replicated to consumer')
+        assert False
+    if int(count) != 1:
+        log.fatal('PasswordRetyCount has unexpected value: {}'.format(count))
+        assert False
+
+
+if __name__ == '__main__':
+    # Run isolated
+    # -s for DEBUG mode
+    CURRENT_FILE = os.path.realpath(__file__)
+    pytest.main(["-s", CURRENT_FILE])
+
diff --git a/ldap/servers/plugins/replication/repl5_plugins.c b/ldap/servers/plugins/replication/repl5_plugins.c
index 0aee8829a..324e38263 100644
--- a/ldap/servers/plugins/replication/repl5_plugins.c
+++ b/ldap/servers/plugins/replication/repl5_plugins.c
@@ -1059,6 +1059,16 @@ write_changelog_and_ruv(Slapi_PBlock *pb)
             goto common_return;
         }
 
+        /* Skip internal operations with no op csn if this is a read-only replica */
+        if (op_params->csn == NULL &&
+            operation_is_flag_set(op, OP_FLAG_INTERNAL) &&
+            replica_get_type(r) == REPLICA_TYPE_READONLY)
+        {
+            slapi_log_err(SLAPI_LOG_REPL, "write_changelog_and_ruv",
+                          "Skipping internal operation on read-only replica\n");
+            goto common_return;
+        }
+
         /* we might have stripped all the mods - in that case we do not
            log the operation */
         if (op_params->operation_type != SLAPI_OPERATION_MODIFY ||
-- 
2.13.6