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

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