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

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