Blame SOURCES/0023-Issue-4443-Internal-unindexed-searches-in-syncrepl-r.patch

b8da0b
From 55a47c1bfe1ce1c27e470384c4f1d50895db25f7 Mon Sep 17 00:00:00 2001
b8da0b
From: Mark Reynolds <mreynolds@redhat.com>
b8da0b
Date: Tue, 13 Jul 2021 14:18:03 -0400
b8da0b
Subject: [PATCH] Issue 4443 - Internal unindexed searches in syncrepl/retro
b8da0b
 changelog
b8da0b
b8da0b
Bug Description:
b8da0b
b8da0b
When a non-system index is added to a backend it is
b8da0b
disabled until the database is initialized or reindexed.
b8da0b
So in the case of the retro changelog the changenumber index
b8da0b
is alway disabled by default since it is never initialized.
b8da0b
This leads to unexpected unindexed searches of the retro
b8da0b
changelog.
b8da0b
b8da0b
Fix Description:
b8da0b
b8da0b
If an index has "nsSystemIndex" set to "true" then enable it
b8da0b
immediately.
b8da0b
b8da0b
relates:  https://github.com/389ds/389-ds-base/issues/4443
b8da0b
b8da0b
Reviewed by: spichugi & tbordaz(Thanks!!)
b8da0b
---
b8da0b
 .../tests/suites/retrocl/basic_test.py        | 53 ++++++++-------
b8da0b
 .../suites/retrocl/retrocl_indexing_test.py   | 68 +++++++++++++++++++
b8da0b
 ldap/servers/plugins/retrocl/retrocl_create.c |  2 +-
b8da0b
 .../slapd/back-ldbm/ldbm_index_config.c       | 25 +++++--
b8da0b
 src/lib389/lib389/_mapped_object.py           | 13 ++++
b8da0b
 5 files changed, 130 insertions(+), 31 deletions(-)
b8da0b
 create mode 100644 dirsrvtests/tests/suites/retrocl/retrocl_indexing_test.py
b8da0b
b8da0b
diff --git a/dirsrvtests/tests/suites/retrocl/basic_test.py b/dirsrvtests/tests/suites/retrocl/basic_test.py
b8da0b
index f3bc50f29..84d513829 100644
b8da0b
--- a/dirsrvtests/tests/suites/retrocl/basic_test.py
b8da0b
+++ b/dirsrvtests/tests/suites/retrocl/basic_test.py
b8da0b
@@ -8,7 +8,6 @@
b8da0b
 
b8da0b
 import logging
b8da0b
 import ldap
b8da0b
-import time
b8da0b
 import pytest
b8da0b
 from lib389.topologies import topology_st
b8da0b
 from lib389.plugins import RetroChangelogPlugin
b8da0b
@@ -18,7 +17,8 @@ from lib389.tasks import *
b8da0b
 from lib389.cli_base import FakeArgs, connect_instance, disconnect_instance
b8da0b
 from lib389.cli_base.dsrc import dsrc_arg_concat
b8da0b
 from lib389.cli_conf.plugins.retrochangelog import retrochangelog_add_attr
b8da0b
-from lib389.idm.user import UserAccount, UserAccounts, nsUserAccounts
b8da0b
+from lib389.idm.user import UserAccount, UserAccounts
b8da0b
+from lib389._mapped_object import DSLdapObjects
b8da0b
 
b8da0b
 pytestmark = pytest.mark.tier1
b8da0b
 
b8da0b
@@ -82,7 +82,7 @@ def test_retrocl_exclude_attr_add(topology_st):
b8da0b
 
b8da0b
     log.info('Adding user1')
b8da0b
     try:
b8da0b
-        user1 = users.create(properties={
b8da0b
+        users.create(properties={
b8da0b
             'sn': '1',
b8da0b
             'cn': 'user 1',
b8da0b
             'uid': 'user1',
b8da0b
@@ -97,17 +97,18 @@ def test_retrocl_exclude_attr_add(topology_st):
b8da0b
     except ldap.ALREADY_EXISTS:
b8da0b
         pass
b8da0b
     except ldap.LDAPError as e:
b8da0b
-        log.error("Failed to add user1")
b8da0b
+        log.error("Failed to add user1: " + str(e))
b8da0b
 
b8da0b
     log.info('Verify homePhone and carLicense attrs are in the changelog changestring')
b8da0b
     try:
b8da0b
-        cllist = st.search_s(RETROCL_SUFFIX, ldap.SCOPE_SUBTREE, '(targetDn=%s)' % USER1_DN)
b8da0b
+        retro_changelog_suffix = DSLdapObjects(st, basedn=RETROCL_SUFFIX)
b8da0b
+        cllist = retro_changelog_suffix.filter(f'(targetDn={USER1_DN})')
b8da0b
     except ldap.LDAPError as e:
b8da0b
-        log.fatal("Changelog search failed, error: " +str(e))
b8da0b
+        log.fatal("Changelog search failed, error: " + str(e))
b8da0b
         assert False
b8da0b
     assert len(cllist) > 0
b8da0b
-    if  cllist[0].hasAttr('changes'):
b8da0b
-        clstr = (cllist[0].getValue('changes')).decode()
b8da0b
+    if  cllist[0].present('changes'):
b8da0b
+        clstr = str(cllist[0].get_attr_vals_utf8('changes'))
b8da0b
         assert ATTR_HOMEPHONE in clstr
b8da0b
         assert ATTR_CARLICENSE in clstr
b8da0b
 
b8da0b
@@ -134,7 +135,7 @@ def test_retrocl_exclude_attr_add(topology_st):
b8da0b
 
b8da0b
     log.info('Adding user2')
b8da0b
     try:
b8da0b
-        user2 = users.create(properties={
b8da0b
+        users.create(properties={
b8da0b
             'sn': '2',
b8da0b
             'cn': 'user 2',
b8da0b
             'uid': 'user2',
b8da0b
@@ -149,18 +150,18 @@ def test_retrocl_exclude_attr_add(topology_st):
b8da0b
     except ldap.ALREADY_EXISTS:
b8da0b
         pass
b8da0b
     except ldap.LDAPError as e:
b8da0b
-        log.error("Failed to add user2")
b8da0b
+        log.error("Failed to add user2: " + str(e))
b8da0b
 
b8da0b
     log.info('Verify homePhone attr is not in the changelog changestring')
b8da0b
     try:
b8da0b
-        cllist = st.search_s(RETROCL_SUFFIX, ldap.SCOPE_SUBTREE, '(targetDn=%s)' % USER2_DN)
b8da0b
+        cllist = retro_changelog_suffix.filter(f'(targetDn={USER2_DN})')
b8da0b
         assert len(cllist) > 0
b8da0b
-        if  cllist[0].hasAttr('changes'):
b8da0b
-            clstr = (cllist[0].getValue('changes')).decode()
b8da0b
+        if  cllist[0].present('changes'):
b8da0b
+            clstr = str(cllist[0].get_attr_vals_utf8('changes'))
b8da0b
             assert ATTR_HOMEPHONE not in clstr
b8da0b
             assert ATTR_CARLICENSE in clstr
b8da0b
     except ldap.LDAPError as e:
b8da0b
-        log.fatal("Changelog search failed, error: " +str(e))
b8da0b
+        log.fatal("Changelog search failed, error: " + str(e))
b8da0b
         assert False
b8da0b
 
b8da0b
 def test_retrocl_exclude_attr_mod(topology_st):
b8da0b
@@ -228,19 +229,20 @@ def test_retrocl_exclude_attr_mod(topology_st):
b8da0b
             'homeDirectory': '/home/user1',
b8da0b
             'userpassword': USER_PW})
b8da0b
     except ldap.ALREADY_EXISTS:
b8da0b
-        pass
b8da0b
+        user1 = UserAccount(st, dn=USER1_DN)
b8da0b
     except ldap.LDAPError as e:
b8da0b
-        log.error("Failed to add user1")
b8da0b
+        log.error("Failed to add user1: " + str(e))
b8da0b
 
b8da0b
     log.info('Verify homePhone and carLicense attrs are in the changelog changestring')
b8da0b
     try:
b8da0b
-        cllist = st.search_s(RETROCL_SUFFIX, ldap.SCOPE_SUBTREE, '(targetDn=%s)' % USER1_DN)
b8da0b
+        retro_changelog_suffix = DSLdapObjects(st, basedn=RETROCL_SUFFIX)
b8da0b
+        cllist = retro_changelog_suffix.filter(f'(targetDn={USER1_DN})')
b8da0b
     except ldap.LDAPError as e:
b8da0b
-        log.fatal("Changelog search failed, error: " +str(e))
b8da0b
+        log.fatal("Changelog search failed, error: " + str(e))
b8da0b
         assert False
b8da0b
     assert len(cllist) > 0
b8da0b
-    if  cllist[0].hasAttr('changes'):
b8da0b
-        clstr = (cllist[0].getValue('changes')).decode()
b8da0b
+    if  cllist[0].present('changes'):
b8da0b
+        clstr = str(cllist[0].get_attr_vals_utf8('changes'))
b8da0b
         assert ATTR_HOMEPHONE in clstr
b8da0b
         assert ATTR_CARLICENSE in clstr
b8da0b
 
b8da0b
@@ -267,24 +269,25 @@ def test_retrocl_exclude_attr_mod(topology_st):
b8da0b
 
b8da0b
     log.info('Modify user1 carLicense attribute')
b8da0b
     try:
b8da0b
-        st.modify_s(USER1_DN, [(ldap.MOD_REPLACE, ATTR_CARLICENSE, b"123WX321")])
b8da0b
+        user1.replace(ATTR_CARLICENSE, "123WX321")
b8da0b
     except ldap.LDAPError as e:
b8da0b
         log.fatal('test_retrocl_exclude_attr_mod: Failed to update user1 attribute: error ' + e.message['desc'])
b8da0b
         assert False
b8da0b
 
b8da0b
     log.info('Verify carLicense attr is not in the changelog changestring')
b8da0b
     try:
b8da0b
-        cllist = st.search_s(RETROCL_SUFFIX, ldap.SCOPE_SUBTREE, '(targetDn=%s)' % USER1_DN)
b8da0b
+        cllist = retro_changelog_suffix.filter(f'(targetDn={USER1_DN})')
b8da0b
         assert len(cllist) > 0
b8da0b
         # There will be 2 entries in the changelog for this user, we are only
b8da0b
         #interested in the second one, the modify operation.
b8da0b
-        if  cllist[1].hasAttr('changes'):
b8da0b
-            clstr = (cllist[1].getValue('changes')).decode()
b8da0b
+        if  cllist[1].present('changes'):
b8da0b
+            clstr = str(cllist[1].get_attr_vals_utf8('changes'))
b8da0b
             assert ATTR_CARLICENSE not in clstr
b8da0b
     except ldap.LDAPError as e:
b8da0b
-        log.fatal("Changelog search failed, error: " +str(e))
b8da0b
+        log.fatal("Changelog search failed, error: " + str(e))
b8da0b
         assert False
b8da0b
 
b8da0b
+
b8da0b
 if __name__ == '__main__':
b8da0b
     # Run isolated
b8da0b
     # -s for DEBUG mode
b8da0b
diff --git a/dirsrvtests/tests/suites/retrocl/retrocl_indexing_test.py b/dirsrvtests/tests/suites/retrocl/retrocl_indexing_test.py
b8da0b
new file mode 100644
b8da0b
index 000000000..b1dfe962c
b8da0b
--- /dev/null
b8da0b
+++ b/dirsrvtests/tests/suites/retrocl/retrocl_indexing_test.py
b8da0b
@@ -0,0 +1,68 @@
b8da0b
+import logging
b8da0b
+import pytest
b8da0b
+import os
b8da0b
+from lib389._constants import RETROCL_SUFFIX, DEFAULT_SUFFIX
b8da0b
+from lib389.topologies import topology_st as topo
b8da0b
+from lib389.plugins import RetroChangelogPlugin
b8da0b
+from lib389.idm.user import UserAccounts
b8da0b
+from lib389._mapped_object import DSLdapObjects
b8da0b
+log = logging.getLogger(__name__)
b8da0b
+
b8da0b
+
b8da0b
+def test_indexing_is_online(topo):
b8da0b
+    """Test that the changenmumber index is online right after enabling the plugin
b8da0b
+
b8da0b
+    :id: 16f4c001-9e0c-4448-a2b3-08ac1e85d40f
b8da0b
+    :setup: Standalone Instance
b8da0b
+    :steps:
b8da0b
+        1. Enable retro cl
b8da0b
+        2. Perform some updates
b8da0b
+        3. Search for "(changenumber>=-1)", and it is not partially unindexed
b8da0b
+        4. Search for "(&(changenumber>=-1)(targetuniqueid=*))", and it is not partially unindexed
b8da0b
+    :expectedresults:
b8da0b
+        1. Success
b8da0b
+        2. Success
b8da0b
+        3. Success
b8da0b
+        4. Success
b8da0b
+    """
b8da0b
+
b8da0b
+    # Enable plugin
b8da0b
+    topo.standalone.config.set('nsslapd-accesslog-logbuffering',  'off')
b8da0b
+    plugin = RetroChangelogPlugin(topo.standalone)
b8da0b
+    plugin.enable()
b8da0b
+    topo.standalone.restart()
b8da0b
+
b8da0b
+    # Do a bunch of updates
b8da0b
+    users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
b8da0b
+    user_entry = users.create(properties={
b8da0b
+        'sn': '1',
b8da0b
+        'cn': 'user 1',
b8da0b
+        'uid': 'user1',
b8da0b
+        'uidNumber': '11',
b8da0b
+        'gidNumber': '111',
b8da0b
+        'givenname': 'user1',
b8da0b
+        'homePhone': '0861234567',
b8da0b
+        'carLicense': '131D16674',
b8da0b
+        'mail': 'user1@whereever.com',
b8da0b
+        'homeDirectory': '/home'
b8da0b
+    })
b8da0b
+    for count in range(0, 10):
b8da0b
+        user_entry.replace('mail', f'test{count}@test.com')
b8da0b
+
b8da0b
+    # Search the retro cl, and check for error messages
b8da0b
+    filter_simple = '(changenumber>=-1)'
b8da0b
+    filter_compound = '(&(changenumber>=-1)(targetuniqueid=*))'
b8da0b
+    retro_changelog_suffix = DSLdapObjects(topo.standalone, basedn=RETROCL_SUFFIX)
b8da0b
+    retro_changelog_suffix.filter(filter_simple)
b8da0b
+    assert not topo.standalone.searchAccessLog('Partially Unindexed Filter')
b8da0b
+
b8da0b
+    # Search the retro cl again with compound filter
b8da0b
+    retro_changelog_suffix.filter(filter_compound)
b8da0b
+    assert not topo.standalone.searchAccessLog('Partially Unindexed Filter')
b8da0b
+
b8da0b
+
b8da0b
+if __name__ == '__main__':
b8da0b
+    # Run isolated
b8da0b
+    # -s for DEBUG mode
b8da0b
+    CURRENT_FILE = os.path.realpath(__file__)
b8da0b
+    pytest.main(["-s", CURRENT_FILE])
b8da0b
diff --git a/ldap/servers/plugins/retrocl/retrocl_create.c b/ldap/servers/plugins/retrocl/retrocl_create.c
b8da0b
index 571e6899f..5bfde7831 100644
b8da0b
--- a/ldap/servers/plugins/retrocl/retrocl_create.c
b8da0b
+++ b/ldap/servers/plugins/retrocl/retrocl_create.c
b8da0b
@@ -133,7 +133,7 @@ retrocl_create_be(const char *bedir)
b8da0b
     val.bv_len = strlen(val.bv_val);
b8da0b
     slapi_entry_add_values(e, "cn", vals);
b8da0b
 
b8da0b
-    val.bv_val = "false";
b8da0b
+    val.bv_val = "true"; /* enables the index */
b8da0b
     val.bv_len = strlen(val.bv_val);
b8da0b
     slapi_entry_add_values(e, "nssystemindex", vals);
b8da0b
 
b8da0b
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_index_config.c b/ldap/servers/slapd/back-ldbm/ldbm_index_config.c
b8da0b
index 9722d0ce7..38e7368e1 100644
b8da0b
--- a/ldap/servers/slapd/back-ldbm/ldbm_index_config.c
b8da0b
+++ b/ldap/servers/slapd/back-ldbm/ldbm_index_config.c
b8da0b
@@ -25,7 +25,7 @@ int ldbm_instance_index_config_delete_callback(Slapi_PBlock *pb, Slapi_Entry *en
b8da0b
 #define INDEXTYPE_NONE 1
b8da0b
 
b8da0b
 static int
b8da0b
-ldbm_index_parse_entry(ldbm_instance *inst, Slapi_Entry *e, const char *trace_string, char **index_name, char *err_buf)
b8da0b
+ldbm_index_parse_entry(ldbm_instance *inst, Slapi_Entry *e, const char *trace_string, char **index_name, PRBool *is_system_index, char *err_buf)
b8da0b
 {
b8da0b
     Slapi_Attr *attr;
b8da0b
     const struct berval *attrValue;
b8da0b
@@ -78,6 +78,15 @@ ldbm_index_parse_entry(ldbm_instance *inst, Slapi_Entry *e, const char *trace_st
b8da0b
         }
b8da0b
     }
b8da0b
 
b8da0b
+    *is_system_index = PR_FALSE;
b8da0b
+    if (0 == slapi_entry_attr_find(e, "nsSystemIndex", &attr)) {
b8da0b
+        slapi_attr_first_value(attr, &sval);
b8da0b
+        attrValue = slapi_value_get_berval(sval);
b8da0b
+        if (strcasecmp(attrValue->bv_val, "true") == 0) {
b8da0b
+            *is_system_index = PR_TRUE;
b8da0b
+        }
b8da0b
+    }
b8da0b
+
b8da0b
     /* ok the entry is good to process, pass it to attr_index_config */
b8da0b
     if (attr_index_config(inst->inst_be, (char *)trace_string, 0, e, 0, 0, err_buf)) {
b8da0b
         slapi_ch_free_string(index_name);
b8da0b
@@ -101,9 +110,10 @@ ldbm_index_init_entry_callback(Slapi_PBlock *pb __attribute__((unused)),
b8da0b
                                void *arg)
b8da0b
 {
b8da0b
     ldbm_instance *inst = (ldbm_instance *)arg;
b8da0b
+    PRBool is_system_index = PR_FALSE;
b8da0b
 
b8da0b
     returntext[0] = '\0';
b8da0b
-    *returncode = ldbm_index_parse_entry(inst, e, "from ldbm instance init", NULL, NULL);
b8da0b
+    *returncode = ldbm_index_parse_entry(inst, e, "from ldbm instance init", NULL, &is_system_index /* not used */, NULL);
b8da0b
     if (*returncode == LDAP_SUCCESS) {
b8da0b
         return SLAPI_DSE_CALLBACK_OK;
b8da0b
     } else {
b8da0b
@@ -126,17 +136,21 @@ ldbm_instance_index_config_add_callback(Slapi_PBlock *pb __attribute__((unused))
b8da0b
 {
b8da0b
     ldbm_instance *inst = (ldbm_instance *)arg;
b8da0b
     char *index_name = NULL;
b8da0b
+    PRBool is_system_index = PR_FALSE;
b8da0b
 
b8da0b
     returntext[0] = '\0';
b8da0b
-    *returncode = ldbm_index_parse_entry(inst, e, "from DSE add", &index_name, returntext);
b8da0b
+    *returncode = ldbm_index_parse_entry(inst, e, "from DSE add", &index_name, &is_system_index, returntext);
b8da0b
     if (*returncode == LDAP_SUCCESS) {
b8da0b
         struct attrinfo *ai = NULL;
b8da0b
         /* if the index is a "system" index, we assume it's being added by
b8da0b
          * by the server, and it's okay for the index to go online immediately.
b8da0b
          * if not, we set the index "offline" so it won't actually be used
b8da0b
          * until someone runs db2index on it.
b8da0b
+         * If caller wants to add an index that they want to be online
b8da0b
+         * immediately they can also set "nsSystemIndex" to "true" in the
b8da0b
+         * index config entry (e.g. is_system_index).
b8da0b
          */
b8da0b
-        if (!ldbm_attribute_always_indexed(index_name)) {
b8da0b
+        if (!is_system_index && !ldbm_attribute_always_indexed(index_name)) {
b8da0b
             ainfo_get(inst->inst_be, index_name, &ai;;
b8da0b
             PR_ASSERT(ai != NULL);
b8da0b
             ai->ai_indexmask |= INDEX_OFFLINE;
b8da0b
@@ -386,13 +400,14 @@ ldbm_instance_index_config_enable_index(ldbm_instance *inst, Slapi_Entry *e)
b8da0b
     char *index_name = NULL;
b8da0b
     int rc = LDAP_SUCCESS;
b8da0b
     struct attrinfo *ai = NULL;
b8da0b
+    PRBool is_system_index = PR_FALSE;
b8da0b
 
b8da0b
     index_name = slapi_entry_attr_get_charptr(e, "cn");
b8da0b
     if (index_name) {
b8da0b
         ainfo_get(inst->inst_be, index_name, &ai;;
b8da0b
     }
b8da0b
     if (!ai) {
b8da0b
-        rc = ldbm_index_parse_entry(inst, e, "from DSE add", &index_name, NULL);
b8da0b
+        rc = ldbm_index_parse_entry(inst, e, "from DSE add", &index_name, &is_system_index /* not used */, NULL);
b8da0b
     }
b8da0b
     if (rc == LDAP_SUCCESS) {
b8da0b
         /* Assume the caller knows if it is OK to go online immediately */
b8da0b
diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py
b8da0b
index b6d778b01..fe610d175 100644
b8da0b
--- a/src/lib389/lib389/_mapped_object.py
b8da0b
+++ b/src/lib389/lib389/_mapped_object.py
b8da0b
@@ -148,6 +148,19 @@ class DSLdapObject(DSLogging, DSLint):
b8da0b
 
b8da0b
         return True
b8da0b
 
b8da0b
+    def search(self, scope="subtree", filter='objectclass=*'):
b8da0b
+        search_scope = ldap.SCOPE_SUBTREE
b8da0b
+        if scope == 'base':
b8da0b
+            search_scope = ldap.SCOPE_BASE
b8da0b
+        elif scope == 'one':
b8da0b
+            search_scope = ldap.SCOPE_ONE
b8da0b
+        elif scope == 'subtree':
b8da0b
+            search_scope = ldap.SCOPE_SUBTREE
b8da0b
+        return self._instance.search_ext_s(self._dn, search_scope, filter,
b8da0b
+                                           serverctrls=self._server_controls,
b8da0b
+                                           clientctrls=self._client_controls,
b8da0b
+                                           escapehatch='i am sure')
b8da0b
+
b8da0b
     def display(self, attrlist=['*']):
b8da0b
         """Get an entry but represent it as a string LDIF
b8da0b
 
b8da0b
-- 
b8da0b
2.31.1
b8da0b