Blame SOURCES/0081-Ticket-49671-Readonly-replicas-should-not-write-inte.patch

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