Compare commits

...

No commits in common. "c8-stream-1.4" and "c9-beta" have entirely different histories.

119 changed files with 9728 additions and 24086 deletions

View File

@ -1,3 +1,3 @@
bd9aab32d9cbf9231058d585479813f3420dc872 SOURCES/389-ds-base-1.4.3.39.tar.bz2
14e38ca3a1851bec5023c191a756c3c58b15f5e0 SOURCES/389-ds-base-2.8.0.tar.bz2
1c8f2d0dfbf39fa8cd86363bf3314351ab21f8d4 SOURCES/jemalloc-5.3.0.tar.bz2
8d3275209f2f8e1a69053340930ad1fb037d61fb SOURCES/vendor-1.4.3.39-3.tar.gz
04340b73d0c8dcc03e2fd08948334f51ffbd49d7 SOURCES/vendor-2.8.0-5.tar.gz

4
.gitignore vendored
View File

@ -1,3 +1,3 @@
SOURCES/389-ds-base-1.4.3.39.tar.bz2
SOURCES/389-ds-base-2.8.0.tar.bz2
SOURCES/jemalloc-5.3.0.tar.bz2
SOURCES/vendor-1.4.3.39-3.tar.gz
SOURCES/vendor-2.8.0-5.tar.gz

View File

@ -0,0 +1,80 @@
From 94a52cd03a6fcccf78a4d7e5c4adc65fb777fcfb Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Mon, 10 Nov 2025 13:20:28 +0100
Subject: [PATCH] Issue 7049 - RetroCL plugin generates invalid LDIF
Bug Description:
When a replicated modification marked with LDAP_MOD_IGNORE is logged,
`changes` attribute contains invalid LDIF:
```
replace: modifiersName
modifiersName: cn=MemberOf Plugin,cn=plugins,cn=config
-
modifyTimestamp: 20250903092211Z
-
```
Line `replace: modifyTimestamp` is missing.
A similar issue is present in audit log:
```
time: 20251031064114
dn: ou=tuser,dc=example,dc=com
result: 0
changetype: modify
add: objectClass
objectClass: nsMemberOf
-
replace: modifiersName
modifiersName: cn=MemberOf Plugin,cn=plugins,cn=config
-
-
```
Dash separator is logged, while the operation is not.
This issue is not present wheh JSON format is used.
Fix Description:
* retrocl_po.c: add a default case to skip the entire modification if it
has LDAP_MOD_IGNORE flag.
* auditlog.c: write the dash separator only if operation type is not
LDAP_MOD_IGNORE
Fixes: https://github.com/389ds/389-ds-base/issues/7049
Reviewed by: @progier389 (Thanks!)
---
ldap/servers/plugins/retrocl/retrocl_po.c | 3 +++
ldap/servers/slapd/auditlog.c | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/ldap/servers/plugins/retrocl/retrocl_po.c b/ldap/servers/plugins/retrocl/retrocl_po.c
index e447ccdfb..277711dd3 100644
--- a/ldap/servers/plugins/retrocl/retrocl_po.c
+++ b/ldap/servers/plugins/retrocl/retrocl_po.c
@@ -99,6 +99,9 @@ make_changes_string(LDAPMod **ldm, const char **includeattrs)
addlenstr(l, ldm[i]->mod_type);
addlenstr(l, "\n");
break;
+ default:
+ /* LDAP_MOD_IGNORE or unknown - skip this mod entirely */
+ continue;
}
for (j = 0; ldm[i]->mod_bvalues != NULL &&
ldm[i]->mod_bvalues[j] != NULL;
diff --git a/ldap/servers/slapd/auditlog.c b/ldap/servers/slapd/auditlog.c
index 32fcea38a..e2db40722 100644
--- a/ldap/servers/slapd/auditlog.c
+++ b/ldap/servers/slapd/auditlog.c
@@ -853,8 +853,8 @@ write_audit_file(
slapi_ch_free((void **)&buf);
}
}
+ addlenstr(l, "-\n");
}
- addlenstr(l, "-\n");
}
break;
--
2.52.0

View File

@ -1,119 +0,0 @@
From dddb14210b402f317e566b6387c76a8e659bf7fa Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Tue, 14 Feb 2023 13:34:10 +0100
Subject: [PATCH 1/2] issue 5647 - covscan: memory leak in audit log when
adding entries (#5650)
covscan reported an issue about "vals" variable in auditlog.c:231 and indeed a charray_free is missing.
Issue: 5647
Reviewed by: @mreynolds389, @droideck
---
ldap/servers/slapd/auditlog.c | 71 +++++++++++++++++++----------------
1 file changed, 38 insertions(+), 33 deletions(-)
diff --git a/ldap/servers/slapd/auditlog.c b/ldap/servers/slapd/auditlog.c
index 68cbc674d..3128e0497 100644
--- a/ldap/servers/slapd/auditlog.c
+++ b/ldap/servers/slapd/auditlog.c
@@ -177,6 +177,40 @@ write_auditfail_log_entry(Slapi_PBlock *pb)
slapi_ch_free_string(&audit_config);
}
+/*
+ * Write the attribute values to the audit log as "comments"
+ *
+ * Slapi_Attr *entry - the attribute begin logged.
+ * char *attrname - the attribute name.
+ * lenstr *l - the audit log buffer
+ *
+ * Resulting output in the log:
+ *
+ * #ATTR: VALUE
+ * #ATTR: VALUE
+ */
+static void
+log_entry_attr(Slapi_Attr *entry_attr, char *attrname, lenstr *l)
+{
+ Slapi_Value **vals = attr_get_present_values(entry_attr);
+ for(size_t i = 0; vals && vals[i]; i++) {
+ char log_val[256] = "";
+ const struct berval *bv = slapi_value_get_berval(vals[i]);
+ if (bv->bv_len >= 256) {
+ strncpy(log_val, bv->bv_val, 252);
+ strcpy(log_val+252, "...");
+ } else {
+ strncpy(log_val, bv->bv_val, bv->bv_len);
+ log_val[bv->bv_len] = 0;
+ }
+ addlenstr(l, "#");
+ addlenstr(l, attrname);
+ addlenstr(l, ": ");
+ addlenstr(l, log_val);
+ addlenstr(l, "\n");
+ }
+}
+
/*
* Write "requested" attributes from the entry to the audit log as "comments"
*
@@ -212,21 +246,9 @@ add_entry_attrs(Slapi_Entry *entry, lenstr *l)
for (req_attr = ldap_utf8strtok_r(display_attrs, ", ", &last); req_attr;
req_attr = ldap_utf8strtok_r(NULL, ", ", &last))
{
- char **vals = slapi_entry_attr_get_charray(entry, req_attr);
- for(size_t i = 0; vals && vals[i]; i++) {
- char log_val[256] = {0};
-
- if (strlen(vals[i]) > 256) {
- strncpy(log_val, vals[i], 252);
- strcat(log_val, "...");
- } else {
- strcpy(log_val, vals[i]);
- }
- addlenstr(l, "#");
- addlenstr(l, req_attr);
- addlenstr(l, ": ");
- addlenstr(l, log_val);
- addlenstr(l, "\n");
+ slapi_entry_attr_find(entry, req_attr, &entry_attr);
+ if (entry_attr) {
+ log_entry_attr(entry_attr, req_attr, l);
}
}
} else {
@@ -234,7 +256,6 @@ add_entry_attrs(Slapi_Entry *entry, lenstr *l)
for (; entry_attr; entry_attr = entry_attr->a_next) {
Slapi_Value **vals = attr_get_present_values(entry_attr);
char *attr = NULL;
- const char *val = NULL;
slapi_attr_get_type(entry_attr, &attr);
if (strcmp(attr, PSEUDO_ATTR_UNHASHEDUSERPASSWORD) == 0) {
@@ -251,23 +272,7 @@ add_entry_attrs(Slapi_Entry *entry, lenstr *l)
addlenstr(l, ": ****************************\n");
continue;
}
-
- for(size_t i = 0; vals && vals[i]; i++) {
- char log_val[256] = {0};
-
- val = slapi_value_get_string(vals[i]);
- if (strlen(val) > 256) {
- strncpy(log_val, val, 252);
- strcat(log_val, "...");
- } else {
- strcpy(log_val, val);
- }
- addlenstr(l, "#");
- addlenstr(l, attr);
- addlenstr(l, ": ");
- addlenstr(l, log_val);
- addlenstr(l, "\n");
- }
+ log_entry_attr(entry_attr, attr, l);
}
}
slapi_ch_free_string(&display_attrs);
--
2.43.0

View File

@ -1,27 +0,0 @@
From be7c2b82958e91ce08775bf6b5da3c311d3b00e5 Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Mon, 20 Feb 2023 16:14:05 +0100
Subject: [PATCH 2/2] Issue 5647 - Fix unused variable warning from previous
commit (#5670)
* issue 5647 - memory leak in audit log when adding entries
* Issue 5647 - Fix unused variable warning from previous commit
---
ldap/servers/slapd/auditlog.c | 1 -
1 file changed, 1 deletion(-)
diff --git a/ldap/servers/slapd/auditlog.c b/ldap/servers/slapd/auditlog.c
index 3128e0497..0597ecc6f 100644
--- a/ldap/servers/slapd/auditlog.c
+++ b/ldap/servers/slapd/auditlog.c
@@ -254,7 +254,6 @@ add_entry_attrs(Slapi_Entry *entry, lenstr *l)
} else {
/* Return all attributes */
for (; entry_attr; entry_attr = entry_attr->a_next) {
- Slapi_Value **vals = attr_get_present_values(entry_attr);
char *attr = NULL;
slapi_attr_get_type(entry_attr, &attr);
--
2.43.0

View File

@ -1,4 +1,4 @@
From 2da3ad24ce028c6b68abef0d115dae4215de558d Mon Sep 17 00:00:00 2001
From 67118939bd2b51e53f7284c2058a43744d1dba76 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Wed, 7 Jan 2026 11:21:12 +0100
Subject: [PATCH] Issue 7096 - During replication online total init the
@ -21,15 +21,15 @@ Reviewed by: Mark Reynolds, Pierre Rogier (Thanks !!)
---
ldap/servers/slapd/back-ldbm/back-ldbm.h | 12 ++
ldap/servers/slapd/back-ldbm/idl_common.c | 163 ++++++++++++++++++
ldap/servers/slapd/back-ldbm/idl_new.c | 29 ++--
ldap/servers/slapd/back-ldbm/idl_new.c | 30 ++--
.../servers/slapd/back-ldbm/proto-back-ldbm.h | 3 +
4 files changed, 189 insertions(+), 18 deletions(-)
4 files changed, 189 insertions(+), 19 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/back-ldbm.h b/ldap/servers/slapd/back-ldbm/back-ldbm.h
index d17ec644b..462d1763c 100644
index 36aa432e0..5e4988782 100644
--- a/ldap/servers/slapd/back-ldbm/back-ldbm.h
+++ b/ldap/servers/slapd/back-ldbm/back-ldbm.h
@@ -285,6 +285,18 @@ typedef struct _idlist_set
@@ -281,6 +281,18 @@ typedef struct _idlist_set
#define INDIRECT_BLOCK(idl) ((idl)->b_nids == INDBLOCK)
#define IDL_NIDS(idl) (idl ? (idl)->b_nids : (NIDS)0)
@ -49,7 +49,7 @@ index d17ec644b..462d1763c 100644
/* small hashtable implementation used in the entry cache -- the table
diff --git a/ldap/servers/slapd/back-ldbm/idl_common.c b/ldap/servers/slapd/back-ldbm/idl_common.c
index e609d349e..3827e59ea 100644
index fcb0ece4b..fdc9b4e67 100644
--- a/ldap/servers/slapd/back-ldbm/idl_common.c
+++ b/ldap/servers/slapd/back-ldbm/idl_common.c
@@ -172,6 +172,169 @@ idl_min(IDList *a, IDList *b)
@ -223,18 +223,26 @@ index e609d349e..3827e59ea 100644
idl_id_is_in_idlist(IDList *idl, ID id)
{
diff --git a/ldap/servers/slapd/back-ldbm/idl_new.c b/ldap/servers/slapd/back-ldbm/idl_new.c
index 916e0781e..212f31b7b 100644
index 5fbcaff2e..2d978353f 100644
--- a/ldap/servers/slapd/back-ldbm/idl_new.c
+++ b/ldap/servers/slapd/back-ldbm/idl_new.c
@@ -378,6 +378,7 @@ idl_new_range_fetch(
idl_range_id_pair *leftover = NULL;
@@ -417,7 +417,6 @@ idl_new_range_fetch(
{
int ret = 0;
int ret2 = 0;
- int idl_rc = 0;
dbi_cursor_t cursor = {0};
IDList *idl = NULL;
dbi_val_t cur_key = {0};
@@ -436,6 +435,7 @@ idl_new_range_fetch(
size_t leftoverlen = 32;
size_t leftovercnt = 0;
char *index_id = get_index_name(be, db, ai);
+ IdRange_t *idrange_list = NULL;
if (NULL == flag_err) {
return NULL;
@@ -528,10 +529,12 @@ idl_new_range_fetch(
@@ -578,10 +578,12 @@ idl_new_range_fetch(
* found entry is the one from the suffix
*/
suffix = key;
@ -250,7 +258,7 @@ index 916e0781e..212f31b7b 100644
} else {
/* Otherwise, keep the {key,id} in leftover array */
if (!leftover) {
@@ -546,13 +549,7 @@ idl_new_range_fetch(
@@ -596,13 +598,7 @@ idl_new_range_fetch(
leftovercnt++;
}
} else {
@ -265,7 +273,7 @@ index 916e0781e..212f31b7b 100644
}
count++;
@@ -655,21 +652,17 @@ error:
@@ -695,21 +691,17 @@ error:
while(remaining > 0) {
for (size_t i = 0; i < leftovercnt; i++) {
@ -289,13 +297,13 @@ index 916e0781e..212f31b7b 100644
slapi_ch_free((void **)&leftover);
+ idrange_free(&idrange_list);
}
return idl;
}
slapi_log_err(SLAPI_LOG_FILTER, "idl_new_range_fetch",
"Found %d candidates; error code is: %d\n",
diff --git a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
index 157788fa4..1d93b6402 100644
index 9a7ffee37..f16c37d73 100644
--- a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
+++ b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
@@ -229,6 +229,9 @@ ID idl_firstid(IDList *idl);
@@ -217,6 +217,9 @@ ID idl_firstid(IDList *idl);
ID idl_nextid(IDList *idl, ID id);
int idl_init_private(backend *be, struct attrinfo *a);
int idl_release_private(struct attrinfo *a);

View File

@ -1,147 +0,0 @@
From 692c4cec6cc5c0086cf58f83bcfa690c766c9887 Mon Sep 17 00:00:00 2001
From: Thierry Bordaz <tbordaz@redhat.com>
Date: Fri, 2 Feb 2024 14:14:28 +0100
Subject: [PATCH] Issue 5407 - sync_repl crashes if enabled while dynamic
plugin is enabled (#5411)
Bug description:
When dynamic plugin is enabled, if a MOD enables sync_repl plugin
then sync_repl init function registers the postop callback
that will be called for the MOD itself while the preop
has not been called.
postop expects preop to be called and so primary operation
to be set. When it is not set it crashes
Fix description:
If the primary operation is not set, just return
relates: #5407
---
.../suites/syncrepl_plugin/basic_test.py | 68 +++++++++++++++++++
ldap/servers/plugins/sync/sync_persist.c | 23 ++++++-
2 files changed, 90 insertions(+), 1 deletion(-)
diff --git a/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py b/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py
index eb3770b78..cdf35eeaa 100644
--- a/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py
+++ b/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py
@@ -592,6 +592,74 @@ def test_sync_repl_cenotaph(topo_m2, request):
request.addfinalizer(fin)
+def test_sync_repl_dynamic_plugin(topology, request):
+ """Test sync_repl with dynamic plugin
+
+ :id: d4f84913-c18a-459f-8525-110f610ca9e6
+ :setup: install a standalone instance
+ :steps:
+ 1. reset instance to standard (no retroCL, no sync_repl, no dynamic plugin)
+ 2. Enable dynamic plugin
+ 3. Enable retroCL/content_sync
+ 4. Establish a sync_repl req
+ :expectedresults:
+ 1. Should succeeds
+ 2. Should succeeds
+ 3. Should succeeds
+ 4. Should succeeds
+ """
+
+ # Reset the instance in a default config
+ # Disable content sync plugin
+ topology.standalone.plugins.disable(name=PLUGIN_REPL_SYNC)
+
+ # Disable retro changelog
+ topology.standalone.plugins.disable(name=PLUGIN_RETRO_CHANGELOG)
+
+ # Disable dynamic plugins
+ topology.standalone.modify_s(DN_CONFIG, [(ldap.MOD_REPLACE, 'nsslapd-dynamic-plugins', b'off')])
+ topology.standalone.restart()
+
+ # Now start the test
+ # Enable dynamic plugins
+ try:
+ topology.standalone.modify_s(DN_CONFIG, [(ldap.MOD_REPLACE, 'nsslapd-dynamic-plugins', b'on')])
+ except ldap.LDAPError as e:
+ log.error('Failed to enable dynamic plugin! {}'.format(e.args[0]['desc']))
+ assert False
+
+ # Enable retro changelog
+ topology.standalone.plugins.enable(name=PLUGIN_RETRO_CHANGELOG)
+
+ # Enbale content sync plugin
+ topology.standalone.plugins.enable(name=PLUGIN_REPL_SYNC)
+
+ # create a sync repl client and wait 5 seconds to be sure it is running
+ sync_repl = Sync_persist(topology.standalone)
+ sync_repl.start()
+ time.sleep(5)
+
+ # create users
+ users = UserAccounts(topology.standalone, DEFAULT_SUFFIX)
+ users_set = []
+ for i in range(10001, 10004):
+ users_set.append(users.create_test_user(uid=i))
+
+ time.sleep(10)
+ # delete users, that automember/memberof will generate nested updates
+ for user in users_set:
+ user.delete()
+ # stop the server to get the sync_repl result set (exit from while loop).
+ # Only way I found to acheive that.
+ # and wait a bit to let sync_repl thread time to set its result before fetching it.
+ topology.standalone.stop()
+ sync_repl.get_result()
+ sync_repl.join()
+ log.info('test_sync_repl_dynamic_plugin: PASS\n')
+
+ # Success
+ log.info('Test complete')
+
def test_sync_repl_invalid_cookie(topology, request):
"""Test sync_repl with invalid cookie
diff --git a/ldap/servers/plugins/sync/sync_persist.c b/ldap/servers/plugins/sync/sync_persist.c
index d2210b64c..283607361 100644
--- a/ldap/servers/plugins/sync/sync_persist.c
+++ b/ldap/servers/plugins/sync/sync_persist.c
@@ -156,6 +156,17 @@ ignore_op_pl(Slapi_PBlock *pb)
* This is the same for ident
*/
prim_op = get_thread_primary_op();
+ if (prim_op == NULL) {
+ /* This can happen if the PRE_OP (sync_update_persist_betxn_pre_op) was not called.
+ * The only known case it happens is with dynamic plugin enabled and an
+ * update that enable the sync_repl plugin. In such case sync_repl registers
+ * the postop (sync_update_persist_op) that is called while the preop was not called
+ */
+ slapi_log_err(SLAPI_LOG_PLUGIN, SYNC_PLUGIN_SUBSYSTEM,
+ "ignore_op_pl - Operation without primary op set (0x%lx)\n",
+ (ulong) op);
+ return;
+ }
ident = sync_persist_get_operation_extension(pb);
if (ident) {
@@ -232,8 +243,18 @@ sync_update_persist_op(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eprev, ber
prim_op = get_thread_primary_op();
+ if (prim_op == NULL) {
+ /* This can happen if the PRE_OP (sync_update_persist_betxn_pre_op) was not called.
+ * The only known case it happens is with dynamic plugin enabled and an
+ * update that enable the sync_repl plugin. In such case sync_repl registers
+ * the postop (sync_update_persist_op) that is called while the preop was not called
+ */
+ slapi_log_err(SLAPI_LOG_PLUGIN, SYNC_PLUGIN_SUBSYSTEM,
+ "sync_update_persist_op - Operation without primary op set (0x%lx)\n",
+ (ulong) pb_op);
+ return;
+ }
ident = sync_persist_get_operation_extension(pb);
- PR_ASSERT(prim_op);
if ((ident == NULL) && operation_is_flag_set(pb_op, OP_FLAG_NOOP)) {
/* This happens for URP (add cenotaph, fixup rename, tombstone resurrect)
--
2.43.0

View File

@ -0,0 +1,765 @@
From a0af5ab79ddad7944687e31f16bb2a667c800958 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Wed, 7 Jan 2026 16:55:27 -0500
Subject: [PATCH] Issue - Revise paged result search locking
Description:
Move to a single lock approach verses having two locks. This will impact
concurrency when multiple async paged result searches are done on the same
connection, but it simplifies the code and avoids race conditions and
deadlocks.
Relates: https://github.com/389ds/389-ds-base/issues/7118
Reviewed by: progier & tbordaz (Thanks!!)
---
ldap/servers/slapd/abandon.c | 2 +-
ldap/servers/slapd/opshared.c | 60 ++++----
ldap/servers/slapd/pagedresults.c | 228 +++++++++++++++++++-----------
ldap/servers/slapd/proto-slap.h | 26 ++--
ldap/servers/slapd/slap.h | 5 +-
5 files changed, 187 insertions(+), 134 deletions(-)
diff --git a/ldap/servers/slapd/abandon.c b/ldap/servers/slapd/abandon.c
index 2dd1ee320..d7c1f8868 100644
--- a/ldap/servers/slapd/abandon.c
+++ b/ldap/servers/slapd/abandon.c
@@ -141,7 +141,7 @@ do_abandon(Slapi_PBlock *pb)
}
pthread_mutex_unlock(&(pb_conn->c_mutex));
- if (0 == pagedresults_free_one_msgid(pb_conn, id, pageresult_lock_get_addr(pb_conn))) {
+ if (0 == pagedresults_free_one_msgid(pb_conn, id, PR_NOT_LOCKED)) {
slapi_log_access(LDAP_DEBUG_STATS, "conn=%" PRIu64
" op=%d ABANDON targetop=Simple Paged Results msgid=%d\n",
pb_conn->c_connid, pb_op->o_opid, id);
diff --git a/ldap/servers/slapd/opshared.c b/ldap/servers/slapd/opshared.c
index 03ed60981..bb474ef3d 100644
--- a/ldap/servers/slapd/opshared.c
+++ b/ldap/servers/slapd/opshared.c
@@ -545,8 +545,8 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
be = be_list[index];
}
}
- pr_search_result = pagedresults_get_search_result(pb_conn, operation, 0 /*not locked*/, pr_idx);
- estimate = pagedresults_get_search_result_set_size_estimate(pb_conn, operation, pr_idx);
+ pr_search_result = pagedresults_get_search_result(pb_conn, operation, PR_NOT_LOCKED, pr_idx);
+ estimate = pagedresults_get_search_result_set_size_estimate(pb_conn, operation, PR_NOT_LOCKED, pr_idx);
/* Set operation note flags as required. */
if (pagedresults_get_unindexed(pb_conn, operation, pr_idx)) {
slapi_pblock_set_flag_operation_notes(pb, SLAPI_OP_NOTE_UNINDEXED);
@@ -592,14 +592,7 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
int32_t tlimit;
slapi_pblock_get(pb, SLAPI_SEARCH_TIMELIMIT, &tlimit);
pagedresults_set_timelimit(pb_conn, operation, (time_t)tlimit, pr_idx);
- /* When using this mutex in conjunction with the main paged
- * result lock, you must do so in this order:
- *
- * --> pagedresults_lock()
- * --> pagedresults_mutex
- * <-- pagedresults_mutex
- * <-- pagedresults_unlock()
- */
+ /* IMPORTANT: Never acquire pagedresults_mutex when holding c_mutex. */
pagedresults_mutex = pageresult_lock_get_addr(pb_conn);
}
@@ -716,17 +709,15 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
if (op_is_pagedresults(operation) && pr_search_result) {
void *sr = NULL;
/* PAGED RESULTS and already have the search results from the prev op */
- pagedresults_lock(pb_conn, pr_idx);
/*
* In async paged result case, the search result might be released
* by other theads. We need to double check it in the locked region.
*/
pthread_mutex_lock(pagedresults_mutex);
- pr_search_result = pagedresults_get_search_result(pb_conn, operation, 1 /*locked*/, pr_idx);
+ pr_search_result = pagedresults_get_search_result(pb_conn, operation, PR_LOCKED, pr_idx);
if (pr_search_result) {
- if (pagedresults_is_abandoned_or_notavailable(pb_conn, 1 /*locked*/, pr_idx)) {
+ if (pagedresults_is_abandoned_or_notavailable(pb_conn, PR_LOCKED, pr_idx)) {
pthread_mutex_unlock(pagedresults_mutex);
- pagedresults_unlock(pb_conn, pr_idx);
/* Previous operation was abandoned and the simplepaged object is not in use. */
send_ldap_result(pb, 0, NULL, "Simple Paged Results Search abandoned", 0, NULL);
rc = LDAP_SUCCESS;
@@ -737,14 +728,13 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
/* search result could be reset in the backend/dse */
slapi_pblock_get(pb, SLAPI_SEARCH_RESULT_SET, &sr);
- pagedresults_set_search_result(pb_conn, operation, sr, 1 /*locked*/, pr_idx);
+ pagedresults_set_search_result(pb_conn, operation, sr, PR_LOCKED, pr_idx);
}
} else {
pr_stat = PAGEDRESULTS_SEARCH_END;
rc = LDAP_SUCCESS;
}
pthread_mutex_unlock(pagedresults_mutex);
- pagedresults_unlock(pb_conn, pr_idx);
if ((PAGEDRESULTS_SEARCH_END == pr_stat) || (0 == pnentries)) {
/* no more entries to send in the backend */
@@ -762,22 +752,22 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
}
pagedresults_set_response_control(pb, 0, estimate,
curr_search_count, pr_idx);
- if (pagedresults_get_with_sort(pb_conn, operation, pr_idx)) {
+ if (pagedresults_get_with_sort(pb_conn, operation, PR_NOT_LOCKED, pr_idx)) {
sort_make_sort_response_control(pb, CONN_GET_SORT_RESULT_CODE, NULL);
}
pagedresults_set_search_result_set_size_estimate(pb_conn,
operation,
- estimate, pr_idx);
+ estimate, PR_NOT_LOCKED, pr_idx);
if (PAGEDRESULTS_SEARCH_END == pr_stat) {
- pagedresults_lock(pb_conn, pr_idx);
+ pthread_mutex_lock(pagedresults_mutex);
slapi_pblock_set(pb, SLAPI_SEARCH_RESULT_SET, NULL);
- if (!pagedresults_is_abandoned_or_notavailable(pb_conn, 0 /*not locked*/, pr_idx)) {
- pagedresults_free_one(pb_conn, operation, pr_idx);
+ if (!pagedresults_is_abandoned_or_notavailable(pb_conn, PR_LOCKED, pr_idx)) {
+ pagedresults_free_one(pb_conn, operation, PR_LOCKED, pr_idx);
}
- pagedresults_unlock(pb_conn, pr_idx);
+ pthread_mutex_unlock(pagedresults_mutex);
if (next_be) {
/* no more entries, but at least another backend */
- if (pagedresults_set_current_be(pb_conn, next_be, pr_idx, 0) < 0) {
+ if (pagedresults_set_current_be(pb_conn, next_be, pr_idx, PR_NOT_LOCKED) < 0) {
goto free_and_return;
}
}
@@ -884,7 +874,7 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
}
}
pagedresults_set_search_result(pb_conn, operation, NULL, 1, pr_idx);
- rc = pagedresults_set_current_be(pb_conn, NULL, pr_idx, 1);
+ rc = pagedresults_set_current_be(pb_conn, NULL, pr_idx, PR_LOCKED);
pthread_mutex_unlock(pagedresults_mutex);
}
@@ -922,7 +912,7 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
pthread_mutex_lock(pagedresults_mutex);
pagedresults_set_search_result(pb_conn, operation, NULL, 1, pr_idx);
be->be_search_results_release(&sr);
- rc = pagedresults_set_current_be(pb_conn, next_be, pr_idx, 1);
+ rc = pagedresults_set_current_be(pb_conn, next_be, pr_idx, PR_LOCKED);
pthread_mutex_unlock(pagedresults_mutex);
pr_stat = PAGEDRESULTS_SEARCH_END; /* make sure stat is SEARCH_END */
if (NULL == next_be) {
@@ -935,23 +925,23 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
} else {
curr_search_count = pnentries;
slapi_pblock_get(pb, SLAPI_SEARCH_RESULT_SET_SIZE_ESTIMATE, &estimate);
- pagedresults_lock(pb_conn, pr_idx);
- if ((pagedresults_set_current_be(pb_conn, be, pr_idx, 0) < 0) ||
- (pagedresults_set_search_result(pb_conn, operation, sr, 0, pr_idx) < 0) ||
- (pagedresults_set_search_result_count(pb_conn, operation, curr_search_count, pr_idx) < 0) ||
- (pagedresults_set_search_result_set_size_estimate(pb_conn, operation, estimate, pr_idx) < 0) ||
- (pagedresults_set_with_sort(pb_conn, operation, with_sort, pr_idx) < 0)) {
- pagedresults_unlock(pb_conn, pr_idx);
+ pthread_mutex_lock(pagedresults_mutex);
+ if ((pagedresults_set_current_be(pb_conn, be, pr_idx, PR_LOCKED) < 0) ||
+ (pagedresults_set_search_result(pb_conn, operation, sr, PR_LOCKED, pr_idx) < 0) ||
+ (pagedresults_set_search_result_count(pb_conn, operation, curr_search_count, PR_LOCKED, pr_idx) < 0) ||
+ (pagedresults_set_search_result_set_size_estimate(pb_conn, operation, estimate, PR_LOCKED, pr_idx) < 0) ||
+ (pagedresults_set_with_sort(pb_conn, operation, with_sort, PR_LOCKED, pr_idx) < 0)) {
+ pthread_mutex_unlock(pagedresults_mutex);
cache_return_target_entry(pb, be, operation);
goto free_and_return;
}
- pagedresults_unlock(pb_conn, pr_idx);
+ pthread_mutex_unlock(pagedresults_mutex);
}
slapi_pblock_set(pb, SLAPI_SEARCH_RESULT_SET, NULL);
next_be = NULL; /* to break the loop */
if (operation->o_status & SLAPI_OP_STATUS_ABANDONED) {
/* It turned out this search was abandoned. */
- pagedresults_free_one_msgid(pb_conn, operation->o_msgid, pagedresults_mutex);
+ pagedresults_free_one_msgid(pb_conn, operation->o_msgid, PR_NOT_LOCKED);
/* paged-results-request was abandoned; making an empty cookie. */
pagedresults_set_response_control(pb, 0, estimate, -1, pr_idx);
send_ldap_result(pb, 0, NULL, "Simple Paged Results Search abandoned", 0, NULL);
@@ -961,7 +951,7 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
}
pagedresults_set_response_control(pb, 0, estimate, curr_search_count, pr_idx);
if (curr_search_count == -1) {
- pagedresults_free_one(pb_conn, operation, pr_idx);
+ pagedresults_free_one(pb_conn, operation, PR_NOT_LOCKED, pr_idx);
}
}
diff --git a/ldap/servers/slapd/pagedresults.c b/ldap/servers/slapd/pagedresults.c
index 941ab97e3..0d6c4a1aa 100644
--- a/ldap/servers/slapd/pagedresults.c
+++ b/ldap/servers/slapd/pagedresults.c
@@ -34,9 +34,9 @@ pageresult_lock_cleanup()
slapi_ch_free((void**)&lock_hash);
}
-/* Beware to the lock order with c_mutex:
- * c_mutex is sometime locked while holding pageresult_lock
- * ==> Do not lock pageresult_lock when holing c_mutex
+/* Lock ordering constraint with c_mutex:
+ * c_mutex is sometimes locked while holding pageresult_lock.
+ * Therefore: DO NOT acquire pageresult_lock when holding c_mutex.
*/
pthread_mutex_t *
pageresult_lock_get_addr(Connection *conn)
@@ -44,7 +44,11 @@ pageresult_lock_get_addr(Connection *conn)
return &lock_hash[(((size_t)conn)/sizeof (Connection))%LOCK_HASH_SIZE];
}
-/* helper function to clean up one prp slot */
+/* helper function to clean up one prp slot
+ *
+ * NOTE: This function must be called while holding the pageresult_lock
+ * (via pageresult_lock_get_addr(conn)) to ensure thread-safe cleanup.
+ */
static void
_pr_cleanup_one_slot(PagedResults *prp)
{
@@ -56,7 +60,7 @@ _pr_cleanup_one_slot(PagedResults *prp)
prp->pr_current_be->be_search_results_release(&(prp->pr_search_result_set));
}
- /* clean up the slot except the mutex */
+ /* clean up the slot */
prp->pr_current_be = NULL;
prp->pr_search_result_set = NULL;
prp->pr_search_result_count = 0;
@@ -136,6 +140,8 @@ pagedresults_parse_control_value(Slapi_PBlock *pb,
return LDAP_UNWILLING_TO_PERFORM;
}
+ /* Acquire hash-based lock for paged results list access
+ * IMPORTANT: Never acquire this lock when holding c_mutex */
pthread_mutex_lock(pageresult_lock_get_addr(conn));
/* the ber encoding is no longer needed */
ber_free(ber, 1);
@@ -184,10 +190,6 @@ pagedresults_parse_control_value(Slapi_PBlock *pb,
goto bail;
}
- if ((*index > -1) && (*index < conn->c_pagedresults.prl_maxlen) &&
- !conn->c_pagedresults.prl_list[*index].pr_mutex) {
- conn->c_pagedresults.prl_list[*index].pr_mutex = PR_NewLock();
- }
conn->c_pagedresults.prl_count++;
} else {
/* Repeated paged results request.
@@ -327,8 +329,14 @@ bailout:
"<= idx=%d\n", index);
}
+/*
+ * Free one paged result entry by index.
+ *
+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
+ * caller already holds pageresult_lock. Never call when holding c_mutex.
+ */
int
-pagedresults_free_one(Connection *conn, Operation *op, int index)
+pagedresults_free_one(Connection *conn, Operation *op, bool locked, int index)
{
int rc = -1;
@@ -338,7 +346,9 @@ pagedresults_free_one(Connection *conn, Operation *op, int index)
slapi_log_err(SLAPI_LOG_TRACE, "pagedresults_free_one",
"=> idx=%d\n", index);
if (conn && (index > -1)) {
- pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ }
if (conn->c_pagedresults.prl_count <= 0) {
slapi_log_err(SLAPI_LOG_TRACE, "pagedresults_free_one",
"conn=%" PRIu64 " paged requests list count is %d\n",
@@ -349,7 +359,9 @@ pagedresults_free_one(Connection *conn, Operation *op, int index)
conn->c_pagedresults.prl_count--;
rc = 0;
}
- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ }
}
slapi_log_err(SLAPI_LOG_TRACE, "pagedresults_free_one", "<= %d\n", rc);
@@ -357,21 +369,28 @@ pagedresults_free_one(Connection *conn, Operation *op, int index)
}
/*
- * Used for abandoning - pageresult_lock_get_addr(conn) is already locked in do_abandone.
+ * Free one paged result entry by message ID.
+ *
+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
+ * caller already holds pageresult_lock. Never call when holding c_mutex.
*/
int
-pagedresults_free_one_msgid(Connection *conn, ber_int_t msgid, pthread_mutex_t *mutex)
+pagedresults_free_one_msgid(Connection *conn, ber_int_t msgid, bool locked)
{
int rc = -1;
int i;
+ pthread_mutex_t *lock = NULL;
if (conn && (msgid > -1)) {
if (conn->c_pagedresults.prl_maxlen <= 0) {
; /* Not a paged result. */
} else {
slapi_log_err(SLAPI_LOG_TRACE,
- "pagedresults_free_one_msgid_nolock", "=> msgid=%d\n", msgid);
- pthread_mutex_lock(mutex);
+ "pagedresults_free_one_msgid", "=> msgid=%d\n", msgid);
+ lock = pageresult_lock_get_addr(conn);
+ if (!locked) {
+ pthread_mutex_lock(lock);
+ }
for (i = 0; i < conn->c_pagedresults.prl_maxlen; i++) {
if (conn->c_pagedresults.prl_list[i].pr_msgid == msgid) {
PagedResults *prp = conn->c_pagedresults.prl_list + i;
@@ -390,9 +409,11 @@ pagedresults_free_one_msgid(Connection *conn, ber_int_t msgid, pthread_mutex_t *
break;
}
}
- pthread_mutex_unlock(mutex);
+ if (!locked) {
+ pthread_mutex_unlock(lock);
+ }
slapi_log_err(SLAPI_LOG_TRACE,
- "pagedresults_free_one_msgid_nolock", "<= %d\n", rc);
+ "pagedresults_free_one_msgid", "<= %d\n", rc);
}
}
@@ -418,29 +439,43 @@ pagedresults_get_current_be(Connection *conn, int index)
return be;
}
+/*
+ * Set current backend for a paged result entry.
+ *
+ * Locking: If locked=false, acquires pageresult_lock. If locked=true, assumes
+ * caller already holds pageresult_lock. Never call when holding c_mutex.
+ */
int
-pagedresults_set_current_be(Connection *conn, Slapi_Backend *be, int index, int nolock)
+pagedresults_set_current_be(Connection *conn, Slapi_Backend *be, int index, bool locked)
{
int rc = -1;
slapi_log_err(SLAPI_LOG_TRACE,
"pagedresults_set_current_be", "=> idx=%d\n", index);
if (conn && (index > -1)) {
- if (!nolock)
+ if (!locked) {
pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ }
if (index < conn->c_pagedresults.prl_maxlen) {
conn->c_pagedresults.prl_list[index].pr_current_be = be;
}
rc = 0;
- if (!nolock)
+ if (!locked) {
pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ }
}
slapi_log_err(SLAPI_LOG_TRACE,
"pagedresults_set_current_be", "<= %d\n", rc);
return rc;
}
+/*
+ * Get search result set for a paged result entry.
+ *
+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
+ * caller already holds pageresult_lock. Never call when holding c_mutex.
+ */
void *
-pagedresults_get_search_result(Connection *conn, Operation *op, int locked, int index)
+pagedresults_get_search_result(Connection *conn, Operation *op, bool locked, int index)
{
void *sr = NULL;
if (!op_is_pagedresults(op)) {
@@ -465,8 +500,14 @@ pagedresults_get_search_result(Connection *conn, Operation *op, int locked, int
return sr;
}
+/*
+ * Set search result set for a paged result entry.
+ *
+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
+ * caller already holds pageresult_lock. Never call when holding c_mutex.
+ */
int
-pagedresults_set_search_result(Connection *conn, Operation *op, void *sr, int locked, int index)
+pagedresults_set_search_result(Connection *conn, Operation *op, void *sr, bool locked, int index)
{
int rc = -1;
if (!op_is_pagedresults(op)) {
@@ -494,8 +535,14 @@ pagedresults_set_search_result(Connection *conn, Operation *op, void *sr, int lo
return rc;
}
+/*
+ * Get search result count for a paged result entry.
+ *
+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
+ * caller already holds pageresult_lock. Never call when holding c_mutex.
+ */
int
-pagedresults_get_search_result_count(Connection *conn, Operation *op, int index)
+pagedresults_get_search_result_count(Connection *conn, Operation *op, bool locked, int index)
{
int count = 0;
if (!op_is_pagedresults(op)) {
@@ -504,19 +551,29 @@ pagedresults_get_search_result_count(Connection *conn, Operation *op, int index)
slapi_log_err(SLAPI_LOG_TRACE,
"pagedresults_get_search_result_count", "=> idx=%d\n", index);
if (conn && (index > -1)) {
- pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ }
if (index < conn->c_pagedresults.prl_maxlen) {
count = conn->c_pagedresults.prl_list[index].pr_search_result_count;
}
- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ }
}
slapi_log_err(SLAPI_LOG_TRACE,
"pagedresults_get_search_result_count", "<= %d\n", count);
return count;
}
+/*
+ * Set search result count for a paged result entry.
+ *
+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
+ * caller already holds pageresult_lock. Never call when holding c_mutex.
+ */
int
-pagedresults_set_search_result_count(Connection *conn, Operation *op, int count, int index)
+pagedresults_set_search_result_count(Connection *conn, Operation *op, int count, bool locked, int index)
{
int rc = -1;
if (!op_is_pagedresults(op)) {
@@ -525,11 +582,15 @@ pagedresults_set_search_result_count(Connection *conn, Operation *op, int count,
slapi_log_err(SLAPI_LOG_TRACE,
"pagedresults_set_search_result_count", "=> idx=%d\n", index);
if (conn && (index > -1)) {
- pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ }
if (index < conn->c_pagedresults.prl_maxlen) {
conn->c_pagedresults.prl_list[index].pr_search_result_count = count;
}
- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ }
rc = 0;
}
slapi_log_err(SLAPI_LOG_TRACE,
@@ -537,9 +598,16 @@ pagedresults_set_search_result_count(Connection *conn, Operation *op, int count,
return rc;
}
+/*
+ * Get search result set size estimate for a paged result entry.
+ *
+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
+ * caller already holds pageresult_lock. Never call when holding c_mutex.
+ */
int
pagedresults_get_search_result_set_size_estimate(Connection *conn,
Operation *op,
+ bool locked,
int index)
{
int count = 0;
@@ -550,11 +618,15 @@ pagedresults_get_search_result_set_size_estimate(Connection *conn,
"pagedresults_get_search_result_set_size_estimate",
"=> idx=%d\n", index);
if (conn && (index > -1)) {
- pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ }
if (index < conn->c_pagedresults.prl_maxlen) {
count = conn->c_pagedresults.prl_list[index].pr_search_result_set_size_estimate;
}
- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ }
}
slapi_log_err(SLAPI_LOG_TRACE,
"pagedresults_get_search_result_set_size_estimate", "<= %d\n",
@@ -562,10 +634,17 @@ pagedresults_get_search_result_set_size_estimate(Connection *conn,
return count;
}
+/*
+ * Set search result set size estimate for a paged result entry.
+ *
+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
+ * caller already holds pageresult_lock. Never call when holding c_mutex.
+ */
int
pagedresults_set_search_result_set_size_estimate(Connection *conn,
Operation *op,
int count,
+ bool locked,
int index)
{
int rc = -1;
@@ -576,11 +655,15 @@ pagedresults_set_search_result_set_size_estimate(Connection *conn,
"pagedresults_set_search_result_set_size_estimate",
"=> idx=%d\n", index);
if (conn && (index > -1)) {
- pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ }
if (index < conn->c_pagedresults.prl_maxlen) {
conn->c_pagedresults.prl_list[index].pr_search_result_set_size_estimate = count;
}
- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ }
rc = 0;
}
slapi_log_err(SLAPI_LOG_TRACE,
@@ -589,8 +672,14 @@ pagedresults_set_search_result_set_size_estimate(Connection *conn,
return rc;
}
+/*
+ * Get with_sort flag for a paged result entry.
+ *
+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
+ * caller already holds pageresult_lock. Never call when holding c_mutex.
+ */
int
-pagedresults_get_with_sort(Connection *conn, Operation *op, int index)
+pagedresults_get_with_sort(Connection *conn, Operation *op, bool locked, int index)
{
int flags = 0;
if (!op_is_pagedresults(op)) {
@@ -599,19 +688,29 @@ pagedresults_get_with_sort(Connection *conn, Operation *op, int index)
slapi_log_err(SLAPI_LOG_TRACE,
"pagedresults_get_with_sort", "=> idx=%d\n", index);
if (conn && (index > -1)) {
- pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ }
if (index < conn->c_pagedresults.prl_maxlen) {
flags = conn->c_pagedresults.prl_list[index].pr_flags & CONN_FLAG_PAGEDRESULTS_WITH_SORT;
}
- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ }
}
slapi_log_err(SLAPI_LOG_TRACE,
"pagedresults_get_with_sort", "<= %d\n", flags);
return flags;
}
+/*
+ * Set with_sort flag for a paged result entry.
+ *
+ * Locking: If locked=0, acquires pageresult_lock. If locked=1, assumes
+ * caller already holds pageresult_lock. Never call when holding c_mutex.
+ */
int
-pagedresults_set_with_sort(Connection *conn, Operation *op, int flags, int index)
+pagedresults_set_with_sort(Connection *conn, Operation *op, int flags, bool locked, int index)
{
int rc = -1;
if (!op_is_pagedresults(op)) {
@@ -620,14 +719,18 @@ pagedresults_set_with_sort(Connection *conn, Operation *op, int flags, int index
slapi_log_err(SLAPI_LOG_TRACE,
"pagedresults_set_with_sort", "=> idx=%d\n", index);
if (conn && (index > -1)) {
- pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_lock(pageresult_lock_get_addr(conn));
+ }
if (index < conn->c_pagedresults.prl_maxlen) {
if (flags & OP_FLAG_SERVER_SIDE_SORTING) {
conn->c_pagedresults.prl_list[index].pr_flags |=
CONN_FLAG_PAGEDRESULTS_WITH_SORT;
}
}
- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ if (!locked) {
+ pthread_mutex_unlock(pageresult_lock_get_addr(conn));
+ }
rc = 0;
}
slapi_log_err(SLAPI_LOG_TRACE, "pagedresults_set_with_sort", "<= %d\n", rc);
@@ -802,10 +905,6 @@ pagedresults_cleanup(Connection *conn, int needlock)
rc = 1;
}
prp->pr_current_be = NULL;
- if (prp->pr_mutex) {
- PR_DestroyLock(prp->pr_mutex);
- prp->pr_mutex = NULL;
- }
memset(prp, '\0', sizeof(PagedResults));
}
conn->c_pagedresults.prl_count = 0;
@@ -840,10 +939,6 @@ pagedresults_cleanup_all(Connection *conn, int needlock)
i < conn->c_pagedresults.prl_maxlen;
i++) {
prp = conn->c_pagedresults.prl_list + i;
- if (prp->pr_mutex) {
- PR_DestroyLock(prp->pr_mutex);
- prp->pr_mutex = NULL;
- }
if (prp->pr_current_be && prp->pr_search_result_set &&
prp->pr_current_be->be_search_results_release) {
prp->pr_current_be->be_search_results_release(&(prp->pr_search_result_set));
@@ -1010,43 +1105,8 @@ op_set_pagedresults(Operation *op)
op->o_flags |= OP_FLAG_PAGED_RESULTS;
}
-/*
- * pagedresults_lock/unlock -- introduced to protect search results for the
- * asynchronous searches. Do not call these functions while the PR conn lock
- * is held (e.g. pageresult_lock_get_addr(conn))
- */
-void
-pagedresults_lock(Connection *conn, int index)
-{
- PagedResults *prp;
- if (!conn || (index < 0) || (index >= conn->c_pagedresults.prl_maxlen)) {
- return;
- }
- pthread_mutex_lock(pageresult_lock_get_addr(conn));
- prp = conn->c_pagedresults.prl_list + index;
- if (prp->pr_mutex) {
- PR_Lock(prp->pr_mutex);
- }
- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-}
-
-void
-pagedresults_unlock(Connection *conn, int index)
-{
- PagedResults *prp;
- if (!conn || (index < 0) || (index >= conn->c_pagedresults.prl_maxlen)) {
- return;
- }
- pthread_mutex_lock(pageresult_lock_get_addr(conn));
- prp = conn->c_pagedresults.prl_list + index;
- if (prp->pr_mutex) {
- PR_Unlock(prp->pr_mutex);
- }
- pthread_mutex_unlock(pageresult_lock_get_addr(conn));
-}
-
int
-pagedresults_is_abandoned_or_notavailable(Connection *conn, int locked, int index)
+pagedresults_is_abandoned_or_notavailable(Connection *conn, bool locked, int index)
{
PagedResults *prp;
int32_t result;
@@ -1066,7 +1126,7 @@ pagedresults_is_abandoned_or_notavailable(Connection *conn, int locked, int inde
}
int
-pagedresults_set_search_result_pb(Slapi_PBlock *pb, void *sr, int locked)
+pagedresults_set_search_result_pb(Slapi_PBlock *pb, void *sr, bool locked)
{
int rc = -1;
Connection *conn = NULL;
diff --git a/ldap/servers/slapd/proto-slap.h b/ldap/servers/slapd/proto-slap.h
index 37c643ea9..742e3ee14 100644
--- a/ldap/servers/slapd/proto-slap.h
+++ b/ldap/servers/slapd/proto-slap.h
@@ -1597,20 +1597,22 @@ pthread_mutex_t *pageresult_lock_get_addr(Connection *conn);
int pagedresults_parse_control_value(Slapi_PBlock *pb, struct berval *psbvp, ber_int_t *pagesize, int *index, Slapi_Backend *be);
void pagedresults_set_response_control(Slapi_PBlock *pb, int iscritical, ber_int_t estimate, int curr_search_count, int index);
Slapi_Backend *pagedresults_get_current_be(Connection *conn, int index);
-int pagedresults_set_current_be(Connection *conn, Slapi_Backend *be, int index, int nolock);
-void *pagedresults_get_search_result(Connection *conn, Operation *op, int locked, int index);
-int pagedresults_set_search_result(Connection *conn, Operation *op, void *sr, int locked, int index);
-int pagedresults_get_search_result_count(Connection *conn, Operation *op, int index);
-int pagedresults_set_search_result_count(Connection *conn, Operation *op, int cnt, int index);
+int pagedresults_set_current_be(Connection *conn, Slapi_Backend *be, int index, bool locked);
+void *pagedresults_get_search_result(Connection *conn, Operation *op, bool locked, int index);
+int pagedresults_set_search_result(Connection *conn, Operation *op, void *sr, bool locked, int index);
+int pagedresults_get_search_result_count(Connection *conn, Operation *op, bool locked, int index);
+int pagedresults_set_search_result_count(Connection *conn, Operation *op, int cnt, bool locked, int index);
int pagedresults_get_search_result_set_size_estimate(Connection *conn,
Operation *op,
+ bool locked,
int index);
int pagedresults_set_search_result_set_size_estimate(Connection *conn,
Operation *op,
int cnt,
+ bool locked,
int index);
-int pagedresults_get_with_sort(Connection *conn, Operation *op, int index);
-int pagedresults_set_with_sort(Connection *conn, Operation *op, int flags, int index);
+int pagedresults_get_with_sort(Connection *conn, Operation *op, bool locked, int index);
+int pagedresults_set_with_sort(Connection *conn, Operation *op, int flags, bool locked, int index);
int pagedresults_get_unindexed(Connection *conn, Operation *op, int index);
int pagedresults_set_unindexed(Connection *conn, Operation *op, int index);
int pagedresults_get_sort_result_code(Connection *conn, Operation *op, int index);
@@ -1622,15 +1624,13 @@ int pagedresults_cleanup(Connection *conn, int needlock);
int pagedresults_is_timedout_nolock(Connection *conn);
int pagedresults_reset_timedout_nolock(Connection *conn);
int pagedresults_in_use_nolock(Connection *conn);
-int pagedresults_free_one(Connection *conn, Operation *op, int index);
-int pagedresults_free_one_msgid(Connection *conn, ber_int_t msgid, pthread_mutex_t *mutex);
+int pagedresults_free_one(Connection *conn, Operation *op, bool locked, int index);
+int pagedresults_free_one_msgid(Connection *conn, ber_int_t msgid, bool locked);
int op_is_pagedresults(Operation *op);
int pagedresults_cleanup_all(Connection *conn, int needlock);
void op_set_pagedresults(Operation *op);
-void pagedresults_lock(Connection *conn, int index);
-void pagedresults_unlock(Connection *conn, int index);
-int pagedresults_is_abandoned_or_notavailable(Connection *conn, int locked, int index);
-int pagedresults_set_search_result_pb(Slapi_PBlock *pb, void *sr, int locked);
+int pagedresults_is_abandoned_or_notavailable(Connection *conn, bool locked, int index);
+int pagedresults_set_search_result_pb(Slapi_PBlock *pb, void *sr, bool locked);
/*
* sort.c
diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h
index 9aaa4fb80..f2395cfa2 100644
--- a/ldap/servers/slapd/slap.h
+++ b/ldap/servers/slapd/slap.h
@@ -80,6 +80,10 @@ static char ptokPBE[34] = "Internal (Software) Token ";
#include <stdbool.h>
#include <time.h> /* For timespec definitions */
+/* Macros for paged results lock parameter */
+#define PR_LOCKED true
+#define PR_NOT_LOCKED false
+
/* Provides our int types and platform specific requirements. */
#include <slapi_pal.h>
@@ -1656,7 +1660,6 @@ typedef struct _paged_results
struct timespec pr_timelimit_hr; /* expiry time of this request rel to clock monotonic */
int pr_flags;
ber_int_t pr_msgid; /* msgid of the request; to abandon */
- PRLock *pr_mutex; /* protect each conn structure */
} PagedResults;
/* array of simple paged structure stashed in connection */
--
2.52.0

View File

@ -1,840 +0,0 @@
From 8dc61a176323f0d41df730abd715ccff3034c2be Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Sun, 27 Nov 2022 09:37:19 -0500
Subject: [PATCH] Issue 5547 - automember plugin improvements
Description:
Rebuild task has the following improvements:
- Only one task allowed at a time
- Do not cleanup previous members by default. Add new CLI option to intentionally
cleanup memberships before rebuilding from scratch.
- Add better task logging to show fixup progress
To prevent automember from being called in a nested be_txn loop thread storage is
used to check and skip these loops.
relates: https://github.com/389ds/389-ds-base/issues/5547
Reviewed by: spichugi(Thanks!)
---
.../automember_plugin/automember_mod_test.py | 43 +++-
ldap/servers/plugins/automember/automember.c | 232 ++++++++++++++----
ldap/servers/slapd/back-ldbm/ldbm_add.c | 11 +-
ldap/servers/slapd/back-ldbm/ldbm_delete.c | 10 +-
ldap/servers/slapd/back-ldbm/ldbm_modify.c | 11 +-
.../lib389/cli_conf/plugins/automember.py | 10 +-
src/lib389/lib389/plugins.py | 7 +-
src/lib389/lib389/tasks.py | 9 +-
8 files changed, 250 insertions(+), 83 deletions(-)
diff --git a/dirsrvtests/tests/suites/automember_plugin/automember_mod_test.py b/dirsrvtests/tests/suites/automember_plugin/automember_mod_test.py
index 8d25384bf..7a0ed3275 100644
--- a/dirsrvtests/tests/suites/automember_plugin/automember_mod_test.py
+++ b/dirsrvtests/tests/suites/automember_plugin/automember_mod_test.py
@@ -5,12 +5,13 @@
# License: GPL (version 3 or any later version).
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---
-#
+import ldap
import logging
import pytest
import os
+import time
from lib389.utils import ds_is_older
-from lib389._constants import *
+from lib389._constants import DEFAULT_SUFFIX
from lib389.plugins import AutoMembershipPlugin, AutoMembershipDefinitions
from lib389.idm.user import UserAccounts
from lib389.idm.group import Groups
@@ -41,6 +42,11 @@ def automember_fixture(topo, request):
user_accts = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
user = user_accts.create_test_user()
+ # Create extra users
+ users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
+ for i in range(0, 100):
+ users.create_test_user(uid=i)
+
# Create automember definitions and regex rules
automember_prop = {
'cn': 'testgroup_definition',
@@ -59,7 +65,7 @@ def automember_fixture(topo, request):
automemberplugin.enable()
topo.standalone.restart()
- return (user, groups)
+ return user, groups
def test_mods(automember_fixture, topo):
@@ -72,19 +78,21 @@ def test_mods(automember_fixture, topo):
2. Update user that should add it to group[1]
3. Update user that should add it to group[2]
4. Update user that should add it to group[0]
- 5. Test rebuild task correctly moves user to group[1]
+ 5. Test rebuild task adds user to group[1]
+ 6. Test rebuild task cleanups groups and only adds it to group[1]
:expectedresults:
1. Success
2. Success
3. Success
4. Success
5. Success
+ 6. Success
"""
(user, groups) = automember_fixture
# Update user which should go into group[0]
user.replace('cn', 'whatever')
- groups[0].is_member(user.dn)
+ assert groups[0].is_member(user.dn)
if groups[1].is_member(user.dn):
assert False
if groups[2].is_member(user.dn):
@@ -92,7 +100,7 @@ def test_mods(automember_fixture, topo):
# Update user0 which should go into group[1]
user.replace('cn', 'mark')
- groups[1].is_member(user.dn)
+ assert groups[1].is_member(user.dn)
if groups[0].is_member(user.dn):
assert False
if groups[2].is_member(user.dn):
@@ -100,7 +108,7 @@ def test_mods(automember_fixture, topo):
# Update user which should go into group[2]
user.replace('cn', 'simon')
- groups[2].is_member(user.dn)
+ assert groups[2].is_member(user.dn)
if groups[0].is_member(user.dn):
assert False
if groups[1].is_member(user.dn):
@@ -108,7 +116,7 @@ def test_mods(automember_fixture, topo):
# Update user which should go back into group[0] (full circle)
user.replace('cn', 'whatever')
- groups[0].is_member(user.dn)
+ assert groups[0].is_member(user.dn)
if groups[1].is_member(user.dn):
assert False
if groups[2].is_member(user.dn):
@@ -128,12 +136,24 @@ def test_mods(automember_fixture, topo):
automemberplugin.enable()
topo.standalone.restart()
- # Run rebuild task
+ # Run rebuild task (no cleanup)
task = automemberplugin.fixup(DEFAULT_SUFFIX, "objectclass=posixaccount")
+ with pytest.raises(ldap.UNWILLING_TO_PERFORM):
+ # test only one fixup task is allowed at a time
+ automemberplugin.fixup(DEFAULT_SUFFIX, "objectclass=top")
task.wait()
- # Test membership
- groups[1].is_member(user.dn)
+ # Test membership (user should still be in groups[0])
+ assert groups[1].is_member(user.dn)
+ if not groups[0].is_member(user.dn):
+ assert False
+
+ # Run rebuild task with cleanup
+ task = automemberplugin.fixup(DEFAULT_SUFFIX, "objectclass=posixaccount", cleanup=True)
+ task.wait()
+
+ # Test membership (user should only be in groups[1])
+ assert groups[1].is_member(user.dn)
if groups[0].is_member(user.dn):
assert False
if groups[2].is_member(user.dn):
@@ -148,4 +168,3 @@ if __name__ == '__main__':
# -s for DEBUG mode
CURRENT_FILE = os.path.realpath(__file__)
pytest.main(["-s", CURRENT_FILE])
-
diff --git a/ldap/servers/plugins/automember/automember.c b/ldap/servers/plugins/automember/automember.c
index 3494d0343..419adb052 100644
--- a/ldap/servers/plugins/automember/automember.c
+++ b/ldap/servers/plugins/automember/automember.c
@@ -1,5 +1,5 @@
/** BEGIN COPYRIGHT BLOCK
- * Copyright (C) 2011 Red Hat, Inc.
+ * Copyright (C) 2022 Red Hat, Inc.
* All rights reserved.
*
* License: GPL (version 3 or any later version).
@@ -14,7 +14,7 @@
* Auto Membership Plug-in
*/
#include "automember.h"
-
+#include <pthread.h>
/*
* Plug-in globals
@@ -22,7 +22,9 @@
static PRCList *g_automember_config = NULL;
static Slapi_RWLock *g_automember_config_lock = NULL;
static uint64_t abort_rebuild_task = 0;
-
+static pthread_key_t td_automem_block_nested;
+static PRBool fixup_running = PR_FALSE;
+static PRLock *fixup_lock = NULL;
static void *_PluginID = NULL;
static Slapi_DN *_PluginDN = NULL;
static Slapi_DN *_ConfigAreaDN = NULL;
@@ -93,9 +95,43 @@ static void automember_task_export_destructor(Slapi_Task *task);
static void automember_task_map_destructor(Slapi_Task *task);
#define DEFAULT_FILE_MODE PR_IRUSR | PR_IWUSR
+#define FIXUP_PROGRESS_LIMIT 1000
static uint64_t plugin_do_modify = 0;
static uint64_t plugin_is_betxn = 0;
+/* automember_plugin fixup task and add operations should block other be_txn
+ * plugins from calling automember_post_op_mod() */
+static int32_t
+slapi_td_block_nested_post_op(void)
+{
+ int32_t val = 12345;
+
+ if (pthread_setspecific(td_automem_block_nested, (void *)&val) != 0) {
+ return PR_FAILURE;
+ }
+ return PR_SUCCESS;
+}
+
+static int32_t
+slapi_td_unblock_nested_post_op(void)
+{
+ if (pthread_setspecific(td_automem_block_nested, NULL) != 0) {
+ return PR_FAILURE;
+ }
+ return PR_SUCCESS;
+}
+
+static int32_t
+slapi_td_is_post_op_nested(void)
+{
+ int32_t *value = pthread_getspecific(td_automem_block_nested);
+
+ if (value == NULL) {
+ return 0;
+ }
+ return 1;
+}
+
/*
* Config cache locking functions
*/
@@ -317,6 +353,14 @@ automember_start(Slapi_PBlock *pb)
return -1;
}
+ if (fixup_lock == NULL) {
+ if ((fixup_lock = PR_NewLock()) == NULL) {
+ slapi_log_err(SLAPI_LOG_ERR, AUTOMEMBER_PLUGIN_SUBSYSTEM,
+ "automember_start - Failed to create fixup lock.\n");
+ return -1;
+ }
+ }
+
/*
* Get the plug-in target dn from the system
* and store it for future use. */
@@ -360,6 +404,11 @@ automember_start(Slapi_PBlock *pb)
}
}
+ if (pthread_key_create(&td_automem_block_nested, NULL) != 0) {
+ slapi_log_err(SLAPI_LOG_ERR, AUTOMEMBER_PLUGIN_SUBSYSTEM,
+ "automember_start - pthread_key_create failed\n");
+ }
+
slapi_log_err(SLAPI_LOG_PLUGIN, AUTOMEMBER_PLUGIN_SUBSYSTEM,
"automember_start - ready for service\n");
slapi_log_err(SLAPI_LOG_TRACE, AUTOMEMBER_PLUGIN_SUBSYSTEM,
@@ -394,6 +443,8 @@ automember_close(Slapi_PBlock *pb __attribute__((unused)))
slapi_sdn_free(&_ConfigAreaDN);
slapi_destroy_rwlock(g_automember_config_lock);
g_automember_config_lock = NULL;
+ PR_DestroyLock(fixup_lock);
+ fixup_lock = NULL;
slapi_log_err(SLAPI_LOG_TRACE, AUTOMEMBER_PLUGIN_SUBSYSTEM,
"<-- automember_close\n");
@@ -1619,7 +1670,6 @@ out:
return rc;
}
-
/*
* automember_update_member_value()
*
@@ -1634,7 +1684,7 @@ automember_update_member_value(Slapi_Entry *member_e, const char *group_dn, char
LDAPMod *mods[2];
char *vals[2];
char *member_value = NULL;
- int rc = 0;
+ int rc = LDAP_SUCCESS;
Slapi_DN *group_sdn;
/* First thing check that the group still exists */
@@ -1653,7 +1703,7 @@ automember_update_member_value(Slapi_Entry *member_e, const char *group_dn, char
"automember_update_member_value - group (default or target) can not be retrieved (%s) err=%d\n",
group_dn, rc);
}
- return rc;
+ goto out;
}
/* If grouping_value is dn, we need to fetch the dn instead. */
@@ -1879,6 +1929,13 @@ automember_mod_post_op(Slapi_PBlock *pb)
PRCList *list = NULL;
int rc = SLAPI_PLUGIN_SUCCESS;
+ if (slapi_td_is_post_op_nested()) {
+ /* don't process op twice in the same thread */
+ return rc;
+ } else {
+ slapi_td_block_nested_post_op();
+ }
+
slapi_log_err(SLAPI_LOG_TRACE, AUTOMEMBER_PLUGIN_SUBSYSTEM,
"--> automember_mod_post_op\n");
@@ -2005,6 +2062,7 @@ automember_mod_post_op(Slapi_PBlock *pb)
}
}
}
+ slapi_td_unblock_nested_post_op();
slapi_log_err(SLAPI_LOG_TRACE, AUTOMEMBER_PLUGIN_SUBSYSTEM,
"<-- automember_mod_post_op (%d)\n", rc);
@@ -2024,6 +2082,13 @@ automember_add_post_op(Slapi_PBlock *pb)
slapi_log_err(SLAPI_LOG_TRACE, AUTOMEMBER_PLUGIN_SUBSYSTEM,
"--> automember_add_post_op\n");
+ if (slapi_td_is_post_op_nested()) {
+ /* don't process op twice in the same thread */
+ return rc;
+ } else {
+ slapi_td_block_nested_post_op();
+ }
+
/* Reload config if a config entry was added. */
if ((sdn = automember_get_sdn(pb))) {
if (automember_dn_is_config(sdn)) {
@@ -2039,7 +2104,7 @@ automember_add_post_op(Slapi_PBlock *pb)
/* If replication, just bail. */
if (automember_isrepl(pb)) {
- return SLAPI_PLUGIN_SUCCESS;
+ goto bail;
}
/* Get the newly added entry. */
@@ -2052,7 +2117,7 @@ automember_add_post_op(Slapi_PBlock *pb)
tombstone);
slapi_value_free(&tombstone);
if (is_tombstone) {
- return SLAPI_PLUGIN_SUCCESS;
+ goto bail;
}
/* Check if a config entry applies
@@ -2063,21 +2128,19 @@ automember_add_post_op(Slapi_PBlock *pb)
list = PR_LIST_HEAD(g_automember_config);
while (list != g_automember_config) {
config = (struct configEntry *)list;
-
/* Does the entry meet scope and filter requirements? */
if (slapi_dn_issuffix(slapi_sdn_get_dn(sdn), config->scope) &&
- (slapi_filter_test_simple(e, config->filter) == 0)) {
+ (slapi_filter_test_simple(e, config->filter) == 0))
+ {
/* Find out what membership changes are needed and make them. */
if (automember_update_membership(config, e, NULL) == SLAPI_PLUGIN_FAILURE) {
rc = SLAPI_PLUGIN_FAILURE;
break;
}
}
-
list = PR_NEXT_LINK(list);
}
}
-
automember_config_unlock();
} else {
slapi_log_err(SLAPI_LOG_PLUGIN, AUTOMEMBER_PLUGIN_SUBSYSTEM,
@@ -2098,6 +2161,7 @@ bail:
slapi_pblock_set(pb, SLAPI_RESULT_CODE, &result);
slapi_pblock_set(pb, SLAPI_PB_RESULT_TEXT, &errtxt);
}
+ slapi_td_unblock_nested_post_op();
return rc;
}
@@ -2138,6 +2202,7 @@ typedef struct _task_data
Slapi_DN *base_dn;
char *bind_dn;
int scope;
+ PRBool cleanup;
} task_data;
static void
@@ -2270,6 +2335,7 @@ automember_task_abort_thread(void *arg)
* basedn: dc=example,dc=com
* filter: (uid=*)
* scope: sub
+ * cleanup: yes/on (default is off)
*
* basedn and filter are required. If scope is omitted, the default is sub
*/
@@ -2284,9 +2350,22 @@ automember_task_add(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eAfter __attr
const char *base_dn;
const char *filter;
const char *scope;
+ const char *cleanup_str;
+ PRBool cleanup = PR_FALSE;
*returncode = LDAP_SUCCESS;
+ PR_Lock(fixup_lock);
+ if (fixup_running) {
+ PR_Unlock(fixup_lock);
+ *returncode = LDAP_UNWILLING_TO_PERFORM;
+ slapi_log_err(SLAPI_LOG_ERR, AUTOMEMBER_PLUGIN_SUBSYSTEM,
+ "automember_task_add - there is already a fixup task running\n");
+ rv = SLAPI_DSE_CALLBACK_ERROR;
+ goto out;
+ }
+ PR_Unlock(fixup_lock);
+
/*
* Grab the task params
*/
@@ -2300,6 +2379,12 @@ automember_task_add(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eAfter __attr
rv = SLAPI_DSE_CALLBACK_ERROR;
goto out;
}
+ if ((cleanup_str = slapi_entry_attr_get_ref(e, "cleanup"))) {
+ if (strcasecmp(cleanup_str, "yes") == 0 || strcasecmp(cleanup_str, "on")) {
+ cleanup = PR_TRUE;
+ }
+ }
+
scope = slapi_fetch_attr(e, "scope", "sub");
/*
* setup our task data
@@ -2315,6 +2400,7 @@ automember_task_add(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eAfter __attr
mytaskdata->bind_dn = slapi_ch_strdup(bind_dn);
mytaskdata->base_dn = slapi_sdn_new_dn_byval(base_dn);
mytaskdata->filter_str = slapi_ch_strdup(filter);
+ mytaskdata->cleanup = cleanup;
if (scope) {
if (strcasecmp(scope, "sub") == 0) {
@@ -2334,6 +2420,9 @@ automember_task_add(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eAfter __attr
task = slapi_plugin_new_task(slapi_entry_get_ndn(e), arg);
slapi_task_set_destructor_fn(task, automember_task_destructor);
slapi_task_set_data(task, mytaskdata);
+ PR_Lock(fixup_lock);
+ fixup_running = PR_TRUE;
+ PR_Unlock(fixup_lock);
/*
* Start the task as a separate thread
*/
@@ -2345,6 +2434,9 @@ automember_task_add(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_Entry *eAfter __attr
"automember_task_add - Unable to create task thread!\n");
*returncode = LDAP_OPERATIONS_ERROR;
slapi_task_finish(task, *returncode);
+ PR_Lock(fixup_lock);
+ fixup_running = PR_FALSE;
+ PR_Unlock(fixup_lock);
rv = SLAPI_DSE_CALLBACK_ERROR;
} else {
rv = SLAPI_DSE_CALLBACK_OK;
@@ -2372,6 +2464,9 @@ automember_rebuild_task_thread(void *arg)
PRCList *list = NULL;
PRCList *include_list = NULL;
int result = 0;
+ int64_t fixup_progress_count = 0;
+ int64_t fixup_progress_elapsed = 0;
+ int64_t fixup_start_time = 0;
size_t i = 0;
/* Reset abort flag */
@@ -2380,6 +2475,7 @@ automember_rebuild_task_thread(void *arg)
if (!task) {
return; /* no task */
}
+
slapi_task_inc_refcount(task);
slapi_log_err(SLAPI_LOG_PLUGIN, AUTOMEMBER_PLUGIN_SUBSYSTEM,
"automember_rebuild_task_thread - Refcount incremented.\n");
@@ -2393,9 +2489,11 @@ automember_rebuild_task_thread(void *arg)
slapi_task_log_status(task, "Automember rebuild task starting (base dn: (%s) filter (%s)...",
slapi_sdn_get_dn(td->base_dn), td->filter_str);
/*
- * Set the bind dn in the local thread data
+ * Set the bind dn in the local thread data, and block post op mods
*/
slapi_td_set_dn(slapi_ch_strdup(td->bind_dn));
+ slapi_td_block_nested_post_op();
+ fixup_start_time = slapi_current_rel_time_t();
/*
* Take the config lock now and search the database
*/
@@ -2426,6 +2524,21 @@ automember_rebuild_task_thread(void *arg)
* Loop over the entries
*/
for (i = 0; entries && (entries[i] != NULL); i++) {
+ fixup_progress_count++;
+ if (fixup_progress_count % FIXUP_PROGRESS_LIMIT == 0 ) {
+ slapi_task_log_notice(task,
+ "Processed %ld entries in %ld seconds (+%ld seconds)",
+ fixup_progress_count,
+ slapi_current_rel_time_t() - fixup_start_time,
+ slapi_current_rel_time_t() - fixup_progress_elapsed);
+ slapi_task_log_status(task,
+ "Processed %ld entries in %ld seconds (+%ld seconds)",
+ fixup_progress_count,
+ slapi_current_rel_time_t() - fixup_start_time,
+ slapi_current_rel_time_t() - fixup_progress_elapsed);
+ slapi_task_inc_progress(task);
+ fixup_progress_elapsed = slapi_current_rel_time_t();
+ }
if (slapi_atomic_load_64(&abort_rebuild_task, __ATOMIC_ACQUIRE) == 1) {
/* The task was aborted */
slapi_task_log_notice(task, "Automember rebuild task was intentionally aborted");
@@ -2443,48 +2556,66 @@ automember_rebuild_task_thread(void *arg)
if (slapi_dn_issuffix(slapi_entry_get_dn(entries[i]), config->scope) &&
(slapi_filter_test_simple(entries[i], config->filter) == 0))
{
- /* First clear out all the defaults groups */
- for (size_t ii = 0; config->default_groups && config->default_groups[ii]; ii++) {
- if ((result = automember_update_member_value(entries[i], config->default_groups[ii],
- config->grouping_attr, config->grouping_value, NULL, DEL_MEMBER)))
- {
- slapi_task_log_notice(task, "Automember rebuild membership task unable to delete "
- "member from default group (%s) error (%d)",
- config->default_groups[ii], result);
- slapi_task_log_status(task, "Automember rebuild membership task unable to delete "
- "member from default group (%s) error (%d)",
- config->default_groups[ii], result);
- slapi_log_err(SLAPI_LOG_ERR, AUTOMEMBER_PLUGIN_SUBSYSTEM,
- "automember_rebuild_task_thread - Unable to unable to delete from (%s) error (%d)\n",
- config->default_groups[ii], result);
- goto out;
- }
- }
-
- /* Then clear out the non-default group */
- if (config->inclusive_rules && !PR_CLIST_IS_EMPTY((PRCList *)config->inclusive_rules)) {
- include_list = PR_LIST_HEAD((PRCList *)config->inclusive_rules);
- while (include_list != (PRCList *)config->inclusive_rules) {
- struct automemberRegexRule *curr_rule = (struct automemberRegexRule *)include_list;
- if ((result = automember_update_member_value(entries[i], slapi_sdn_get_dn(curr_rule->target_group_dn),
- config->grouping_attr, config->grouping_value, NULL, DEL_MEMBER)))
+ if (td->cleanup) {
+
+ slapi_log_err(SLAPI_LOG_PLUGIN, AUTOMEMBER_PLUGIN_SUBSYSTEM,
+ "automember_rebuild_task_thread - Cleaning up groups (config %s)\n",
+ config->dn);
+ /* First clear out all the defaults groups */
+ for (size_t ii = 0; config->default_groups && config->default_groups[ii]; ii++) {
+ if ((result = automember_update_member_value(entries[i],
+ config->default_groups[ii],
+ config->grouping_attr,
+ config->grouping_value,
+ NULL, DEL_MEMBER)))
{
slapi_task_log_notice(task, "Automember rebuild membership task unable to delete "
- "member from group (%s) error (%d)",
- slapi_sdn_get_dn(curr_rule->target_group_dn), result);
+ "member from default group (%s) error (%d)",
+ config->default_groups[ii], result);
slapi_task_log_status(task, "Automember rebuild membership task unable to delete "
- "member from group (%s) error (%d)",
- slapi_sdn_get_dn(curr_rule->target_group_dn), result);
+ "member from default group (%s) error (%d)",
+ config->default_groups[ii], result);
slapi_log_err(SLAPI_LOG_ERR, AUTOMEMBER_PLUGIN_SUBSYSTEM,
"automember_rebuild_task_thread - Unable to unable to delete from (%s) error (%d)\n",
- slapi_sdn_get_dn(curr_rule->target_group_dn), result);
+ config->default_groups[ii], result);
goto out;
}
- include_list = PR_NEXT_LINK(include_list);
}
+
+ /* Then clear out the non-default group */
+ if (config->inclusive_rules && !PR_CLIST_IS_EMPTY((PRCList *)config->inclusive_rules)) {
+ include_list = PR_LIST_HEAD((PRCList *)config->inclusive_rules);
+ while (include_list != (PRCList *)config->inclusive_rules) {
+ struct automemberRegexRule *curr_rule = (struct automemberRegexRule *)include_list;
+ if ((result = automember_update_member_value(entries[i],
+ slapi_sdn_get_dn(curr_rule->target_group_dn),
+ config->grouping_attr,
+ config->grouping_value,
+ NULL, DEL_MEMBER)))
+ {
+ slapi_task_log_notice(task, "Automember rebuild membership task unable to delete "
+ "member from group (%s) error (%d)",
+ slapi_sdn_get_dn(curr_rule->target_group_dn), result);
+ slapi_task_log_status(task, "Automember rebuild membership task unable to delete "
+ "member from group (%s) error (%d)",
+ slapi_sdn_get_dn(curr_rule->target_group_dn), result);
+ slapi_log_err(SLAPI_LOG_ERR, AUTOMEMBER_PLUGIN_SUBSYSTEM,
+ "automember_rebuild_task_thread - Unable to unable to delete from (%s) error (%d)\n",
+ slapi_sdn_get_dn(curr_rule->target_group_dn), result);
+ goto out;
+ }
+ include_list = PR_NEXT_LINK(include_list);
+ }
+ }
+ slapi_log_err(SLAPI_LOG_PLUGIN, AUTOMEMBER_PLUGIN_SUBSYSTEM,
+ "automember_rebuild_task_thread - Finished cleaning up groups (config %s)\n",
+ config->dn);
}
/* Update the memberships for this entries */
+ slapi_log_err(SLAPI_LOG_PLUGIN, AUTOMEMBER_PLUGIN_SUBSYSTEM,
+ "automember_rebuild_task_thread - Updating membership (config %s)\n",
+ config->dn);
if (slapi_is_shutting_down() ||
automember_update_membership(config, entries[i], NULL) == SLAPI_PLUGIN_FAILURE)
{
@@ -2508,15 +2639,22 @@ out:
slapi_task_log_notice(task, "Automember rebuild task aborted. Error (%d)", result);
slapi_task_log_status(task, "Automember rebuild task aborted. Error (%d)", result);
} else {
- slapi_task_log_notice(task, "Automember rebuild task finished. Processed (%d) entries.", (int32_t)i);
- slapi_task_log_status(task, "Automember rebuild task finished. Processed (%d) entries.", (int32_t)i);
+ slapi_task_log_notice(task, "Automember rebuild task finished. Processed (%ld) entries in %ld seconds",
+ (int64_t)i, slapi_current_rel_time_t() - fixup_start_time);
+ slapi_task_log_status(task, "Automember rebuild task finished. Processed (%ld) entries in %ld seconds",
+ (int64_t)i, slapi_current_rel_time_t() - fixup_start_time);
}
slapi_task_inc_progress(task);
slapi_task_finish(task, result);
slapi_task_dec_refcount(task);
slapi_atomic_store_64(&abort_rebuild_task, 0, __ATOMIC_RELEASE);
+ slapi_td_unblock_nested_post_op();
+ PR_Lock(fixup_lock);
+ fixup_running = PR_FALSE;
+ PR_Unlock(fixup_lock);
+
slapi_log_err(SLAPI_LOG_PLUGIN, AUTOMEMBER_PLUGIN_SUBSYSTEM,
- "automember_rebuild_task_thread - Refcount decremented.\n");
+ "automember_rebuild_task_thread - task finished, refcount decremented.\n");
}
/*
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_add.c b/ldap/servers/slapd/back-ldbm/ldbm_add.c
index ba2d73a84..ce4c314a1 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_add.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_add.c
@@ -1,6 +1,6 @@
/** BEGIN COPYRIGHT BLOCK
* Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
- * Copyright (C) 2005 Red Hat, Inc.
+ * Copyright (C) 2022 Red Hat, Inc.
* Copyright (C) 2009 Hewlett-Packard Development Company, L.P.
* All rights reserved.
*
@@ -1264,10 +1264,6 @@ ldbm_back_add(Slapi_PBlock *pb)
goto common_return;
error_return:
- /* Revert the caches if this is the parent operation */
- if (parent_op && betxn_callback_fails) {
- revert_cache(inst, &parent_time);
- }
if (addingentry_id_assigned) {
next_id_return(be, addingentry->ep_id);
}
@@ -1376,6 +1372,11 @@ diskfull_return:
if (!not_an_error) {
rc = SLAPI_FAIL_GENERAL;
}
+
+ /* Revert the caches if this is the parent operation */
+ if (parent_op && betxn_callback_fails) {
+ revert_cache(inst, &parent_time);
+ }
}
common_return:
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_delete.c b/ldap/servers/slapd/back-ldbm/ldbm_delete.c
index de23190c3..27f0ac58a 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_delete.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_delete.c
@@ -1407,11 +1407,6 @@ commit_return:
goto common_return;
error_return:
- /* Revert the caches if this is the parent operation */
- if (parent_op && betxn_callback_fails) {
- revert_cache(inst, &parent_time);
- }
-
if (tombstone) {
if (cache_is_in_cache(&inst->inst_cache, tombstone)) {
tomb_ep_id = tombstone->ep_id; /* Otherwise, tombstone might have been freed. */
@@ -1496,6 +1491,11 @@ error_return:
conn_id, op_id, parent_modify_c.old_entry, parent_modify_c.new_entry, myrc);
}
+ /* Revert the caches if this is the parent operation */
+ if (parent_op && betxn_callback_fails) {
+ revert_cache(inst, &parent_time);
+ }
+
common_return:
if (orig_entry) {
/* NOTE: #define SLAPI_DELETE_BEPREOP_ENTRY SLAPI_ENTRY_PRE_OP */
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_modify.c b/ldap/servers/slapd/back-ldbm/ldbm_modify.c
index 537369055..64b293001 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_modify.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_modify.c
@@ -1,6 +1,6 @@
/** BEGIN COPYRIGHT BLOCK
* Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
- * Copyright (C) 2005 Red Hat, Inc.
+ * Copyright (C) 2022 Red Hat, Inc.
* Copyright (C) 2009 Hewlett-Packard Development Company, L.P.
* All rights reserved.
*
@@ -1043,11 +1043,6 @@ ldbm_back_modify(Slapi_PBlock *pb)
goto common_return;
error_return:
- /* Revert the caches if this is the parent operation */
- if (parent_op && betxn_callback_fails) {
- revert_cache(inst, &parent_time);
- }
-
if (postentry != NULL) {
slapi_entry_free(postentry);
postentry = NULL;
@@ -1103,6 +1098,10 @@ error_return:
if (!not_an_error) {
rc = SLAPI_FAIL_GENERAL;
}
+ /* Revert the caches if this is the parent operation */
+ if (parent_op && betxn_callback_fails) {
+ revert_cache(inst, &parent_time);
+ }
}
/* if ec is in cache, remove it, then add back e if we still have it */
diff --git a/src/lib389/lib389/cli_conf/plugins/automember.py b/src/lib389/lib389/cli_conf/plugins/automember.py
index 15b00c633..568586ad8 100644
--- a/src/lib389/lib389/cli_conf/plugins/automember.py
+++ b/src/lib389/lib389/cli_conf/plugins/automember.py
@@ -155,7 +155,7 @@ def fixup(inst, basedn, log, args):
log.info('Attempting to add task entry... This will fail if Automembership plug-in is not enabled.')
if not plugin.status():
log.error("'%s' is disabled. Rebuild membership task can't be executed" % plugin.rdn)
- fixup_task = plugin.fixup(args.DN, args.filter)
+ fixup_task = plugin.fixup(args.DN, args.filter, args.cleanup)
if args.wait:
log.info(f'Waiting for fixup task "{fixup_task.dn}" to complete. You can safely exit by pressing Control C ...')
fixup_task.wait(timeout=args.timeout)
@@ -225,8 +225,8 @@ def create_parser(subparsers):
subcommands = automember.add_subparsers(help='action')
add_generic_plugin_parsers(subcommands, AutoMembershipPlugin)
- list = subcommands.add_parser('list', help='List Automembership definitions or regex rules.')
- subcommands_list = list.add_subparsers(help='action')
+ automember_list = subcommands.add_parser('list', help='List Automembership definitions or regex rules.')
+ subcommands_list = automember_list.add_subparsers(help='action')
list_definitions = subcommands_list.add_parser('definitions', help='Lists Automembership definitions.')
list_definitions.set_defaults(func=definition_list)
list_regexes = subcommands_list.add_parser('regexes', help='List Automembership regex rules.')
@@ -269,6 +269,8 @@ def create_parser(subparsers):
fixup_task.add_argument('-f', '--filter', required=True, help='Sets the LDAP filter for entries to fix up')
fixup_task.add_argument('-s', '--scope', required=True, choices=['sub', 'base', 'one'], type=str.lower,
help='Sets the LDAP search scope for entries to fix up')
+ fixup_task.add_argument('--cleanup', action='store_true',
+ help="Clean up previous group memberships before rebuilding")
fixup_task.add_argument('--wait', action='store_true',
help="Wait for the task to finish, this could take a long time")
fixup_task.add_argument('--timeout', default=0, type=int,
@@ -279,7 +281,7 @@ def create_parser(subparsers):
fixup_status.add_argument('--dn', help="The task entry's DN")
fixup_status.add_argument('--show-log', action='store_true', help="Display the task log")
fixup_status.add_argument('--watch', action='store_true',
- help="Watch the task's status and wait for it to finish")
+ help="Watch the task's status and wait for it to finish")
abort_fixup = subcommands.add_parser('abort-fixup', help='Abort the rebuild membership task.')
abort_fixup.set_defaults(func=abort)
diff --git a/src/lib389/lib389/plugins.py b/src/lib389/lib389/plugins.py
index 52691a44c..a1ad0a45b 100644
--- a/src/lib389/lib389/plugins.py
+++ b/src/lib389/lib389/plugins.py
@@ -1141,13 +1141,15 @@ class AutoMembershipPlugin(Plugin):
def __init__(self, instance, dn="cn=Auto Membership Plugin,cn=plugins,cn=config"):
super(AutoMembershipPlugin, self).__init__(instance, dn)
- def fixup(self, basedn, _filter=None):
+ def fixup(self, basedn, _filter=None, cleanup=False):
"""Create an automember rebuild membership task
:param basedn: Basedn to fix up
:type basedn: str
:param _filter: a filter for entries to fix up
:type _filter: str
+ :param cleanup: cleanup old group memberships
+ :type cleanup: boolean
:returns: an instance of Task(DSLdapObject)
"""
@@ -1156,6 +1158,9 @@ class AutoMembershipPlugin(Plugin):
task_properties = {'basedn': basedn}
if _filter is not None:
task_properties['filter'] = _filter
+ if cleanup:
+ task_properties['cleanup'] = "yes"
+
task.create(properties=task_properties)
return task
diff --git a/src/lib389/lib389/tasks.py b/src/lib389/lib389/tasks.py
index 1a16bbb83..193805780 100644
--- a/src/lib389/lib389/tasks.py
+++ b/src/lib389/lib389/tasks.py
@@ -1006,12 +1006,13 @@ class Tasks(object):
return exitCode
def automemberRebuild(self, suffix=DEFAULT_SUFFIX, scope='sub',
- filterstr='objectclass=top', args=None):
+ filterstr='objectclass=top', cleanup=False, args=None):
'''
- @param suffix - The suffix the task should examine - defualt is
+ @param suffix - The suffix the task should examine - default is
"dc=example,dc=com"
@param scope - The scope of the search to find entries
- @param fitlerstr - THe search filter to find entries
+ @param fitlerstr - The search filter to find entries
+ @param cleanup - reset/clear the old group mmeberships prior to rebuilding
@param args - is a dictionary that contains modifier of the task
wait: True/[False] - If True, waits for the completion of
the task before to return
@@ -1027,6 +1028,8 @@ class Tasks(object):
entry.setValues('basedn', suffix)
entry.setValues('filter', filterstr)
entry.setValues('scope', scope)
+ if cleanup:
+ entry.setValues('cleanup', 'yes')
# start the task and possibly wait for task completion
try:
--
2.43.0

View File

@ -0,0 +1,213 @@
From 5b6211de252f801ffc088703acc242e47f6273ab Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Fri, 9 Jan 2026 11:39:50 +0100
Subject: [PATCH] Issue 7172 - Index ordering mismatch after upgrade (#7173)
Bug Description:
Commit daf731f55071d45eaf403a52b63d35f4e699ff28 introduced a regression.
After upgrading to a version that adds `integerOrderingMatch` matching
rule to `parentid` and `ancestorid` indexes, searches may return empty
or incorrect results.
This happens because the existing index data was created with
lexicographic ordering, but the new compare function expects integer
ordering. Index lookups fail because the compare function doesn't match
the data ordering.
The root cause is that `ldbm_instance_create_default_indexes()` calls
`attr_index_config()` unconditionally for `parentid` and `ancestorid`
indexes, which triggers `ainfo_dup()` to overwrite `ai_key_cmp_fn` on
existing indexes. This breaks indexes that were created without the
`integerOrderingMatch` matching rule.
Fix Description:
* Call `attr_index_config()` for `parentid` and `ancestorid` indexes
only if index config doesn't exist.
* Add `upgrade_check_id_index_matching_rule()` that logs an error on
server startup if `parentid` or `ancestorid` indexes are missing the
integerOrderingMatch matching rule, advising administrators to reindex.
Fixes: https://github.com/389ds/389-ds-base/issues/7172
Reviewed by: @tbordaz, @progier389, @droideck (Thanks!)
---
ldap/servers/slapd/back-ldbm/instance.c | 25 ++++--
ldap/servers/slapd/upgrade.c | 105 ++++++++++++++++++++++++
2 files changed, 122 insertions(+), 8 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/instance.c b/ldap/servers/slapd/back-ldbm/instance.c
index 24c00200b..30db99c81 100644
--- a/ldap/servers/slapd/back-ldbm/instance.c
+++ b/ldap/servers/slapd/back-ldbm/instance.c
@@ -191,6 +191,7 @@ ldbm_instance_create_default_indexes(backend *be)
char *ancestorid_indexes_limit = NULL;
char *parentid_indexes_limit = NULL;
struct attrinfo *ai = NULL;
+ struct attrinfo *index_already_configured = NULL;
struct index_idlistsizeinfo *iter;
int cookie;
int limit;
@@ -255,10 +256,14 @@ ldbm_instance_create_default_indexes(backend *be)
slapi_entry_free(e);
}
- e = ldbm_instance_init_config_entry(LDBM_PARENTID_STR, "eq", 0, 0, 0, "integerOrderingMatch", parentid_indexes_limit);
- ldbm_instance_config_add_index_entry(inst, e, flags);
- attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
- slapi_entry_free(e);
+ ainfo_get(be, (char *)LDBM_PARENTID_STR, &ai);
+ index_already_configured = ai;
+ if (!index_already_configured) {
+ e = ldbm_instance_init_config_entry(LDBM_PARENTID_STR, "eq", 0, 0, 0, "integerOrderingMatch", parentid_indexes_limit);
+ ldbm_instance_config_add_index_entry(inst, e, flags);
+ attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
+ slapi_entry_free(e);
+ }
e = ldbm_instance_init_config_entry("objectclass", "eq", 0, 0, 0, 0, 0);
ldbm_instance_config_add_index_entry(inst, e, flags);
@@ -296,10 +301,14 @@ ldbm_instance_create_default_indexes(backend *be)
* ancestorid is special, there is actually no such attr type
* but we still want to use the attr index file APIs.
*/
- e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, "integerOrderingMatch", ancestorid_indexes_limit);
- ldbm_instance_config_add_index_entry(inst, e, flags);
- attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
- slapi_entry_free(e);
+ ainfo_get(be, (char *)LDBM_ANCESTORID_STR, &ai);
+ index_already_configured = ai;
+ if (!index_already_configured) {
+ e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, "integerOrderingMatch", ancestorid_indexes_limit);
+ ldbm_instance_config_add_index_entry(inst, e, flags);
+ attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
+ slapi_entry_free(e);
+ }
}
slapi_ch_free_string(&ancestorid_indexes_limit);
diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
index 0d1e2abf4..43906c1af 100644
--- a/ldap/servers/slapd/upgrade.c
+++ b/ldap/servers/slapd/upgrade.c
@@ -279,6 +279,107 @@ upgrade_205_fixup_repl_dep(void)
return UPGRADE_SUCCESS;
}
+/*
+ * Check if parentid/ancestorid indexes are missing the integerOrderingMatch
+ * matching rule.
+ *
+ * This function logs a warning if we detect this condition, advising
+ * the administrator to reindex the affected attributes.
+ */
+static upgrade_status
+upgrade_check_id_index_matching_rule(void)
+{
+ struct slapi_pblock *pb = slapi_pblock_new();
+ Slapi_Entry **backends = NULL;
+ const char *be_base_dn = "cn=ldbm database,cn=plugins,cn=config";
+ const char *be_filter = "(objectclass=nsBackendInstance)";
+ const char *attrs_to_check[] = {"parentid", "ancestorid", NULL};
+ upgrade_status uresult = UPGRADE_SUCCESS;
+
+ /* Search for all backend instances */
+ slapi_search_internal_set_pb(
+ pb, be_base_dn,
+ LDAP_SCOPE_ONELEVEL,
+ be_filter, NULL, 0, NULL, NULL,
+ plugin_get_default_component_id(), 0);
+ slapi_search_internal_pb(pb);
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &backends);
+
+ if (backends) {
+ for (size_t be_idx = 0; backends[be_idx] != NULL; be_idx++) {
+ const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
+ if (!be_name) {
+ continue;
+ }
+
+ /* Check each attribute that should have integerOrderingMatch */
+ for (size_t attr_idx = 0; attrs_to_check[attr_idx] != NULL; attr_idx++) {
+ const char *attr_name = attrs_to_check[attr_idx];
+ struct slapi_pblock *idx_pb = slapi_pblock_new();
+ Slapi_Entry **idx_entries = NULL;
+ char *idx_dn = slapi_create_dn_string("cn=%s,cn=index,cn=%s,%s",
+ attr_name, be_name, be_base_dn);
+ char *idx_filter = "(objectclass=nsIndex)";
+ PRBool has_matching_rule = PR_FALSE;
+
+ if (!idx_dn) {
+ slapi_pblock_destroy(idx_pb);
+ continue;
+ }
+
+ slapi_search_internal_set_pb(
+ idx_pb, idx_dn,
+ LDAP_SCOPE_BASE,
+ idx_filter, NULL, 0, NULL, NULL,
+ plugin_get_default_component_id(), 0);
+ slapi_search_internal_pb(idx_pb);
+ slapi_pblock_get(idx_pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &idx_entries);
+
+ if (idx_entries && idx_entries[0]) {
+ /* Index exists, check if it has integerOrderingMatch */
+ Slapi_Attr *mr_attr = NULL;
+ if (slapi_entry_attr_find(idx_entries[0], "nsMatchingRule", &mr_attr) == 0) {
+ Slapi_Value *sval = NULL;
+ int idx;
+ for (idx = slapi_attr_first_value(mr_attr, &sval);
+ idx != -1;
+ idx = slapi_attr_next_value(mr_attr, idx, &sval)) {
+ const struct berval *bval = slapi_value_get_berval(sval);
+ if (bval && bval->bv_val &&
+ strcasecmp(bval->bv_val, "integerOrderingMatch") == 0) {
+ has_matching_rule = PR_TRUE;
+ break;
+ }
+ }
+ }
+
+ if (!has_matching_rule) {
+ /* Index exists but doesn't have integerOrderingMatch, log a warning */
+ slapi_log_err(SLAPI_LOG_ERR, "upgrade_check_id_index_matching_rule",
+ "Index '%s' in backend '%s' is missing 'nsMatchingRule: integerOrderingMatch'. "
+ "Incorrectly configured system indexes can lead to poor search performance, replication issues, and other operational problems. "
+ "To fix this, add the matching rule and reindex: "
+ "dsconf <instance> backend index set --add-mr integerOrderingMatch --attr %s %s && "
+ "dsconf <instance> backend index reindex --attr %s %s. "
+ "WARNING: Reindexing can be resource-intensive and may impact server performance on a live system. "
+ "Consider scheduling reindexing during maintenance windows or periods of low activity.\n",
+ attr_name, be_name, attr_name, be_name, attr_name, be_name);
+ }
+ }
+
+ slapi_ch_free_string(&idx_dn);
+ slapi_free_search_results_internal(idx_pb);
+ slapi_pblock_destroy(idx_pb);
+ }
+ }
+ }
+
+ slapi_free_search_results_internal(pb);
+ slapi_pblock_destroy(pb);
+
+ return uresult;
+}
+
upgrade_status
upgrade_server(void)
{
@@ -306,6 +407,10 @@ upgrade_server(void)
return UPGRADE_FAILURE;
}
+ if (upgrade_check_id_index_matching_rule() != UPGRADE_SUCCESS) {
+ return UPGRADE_FAILURE;
+ }
+
return UPGRADE_SUCCESS;
}
--
2.52.0

View File

@ -1,83 +0,0 @@
From 9319d5b022918f14cacb00e3faef85a6ab730a26 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Tue, 27 Feb 2024 16:30:47 -0800
Subject: [PATCH] Issue 3527 - Support HAProxy and Instance on the same machine
configuration (#6107)
Description: Improve how we handle HAProxy connections to work better when
the DS and HAProxy are on the same machine.
Ensure the client and header destination IPs are checked against the trusted IP list.
Additionally, this change will also allow configuration having
HAProxy is listening on a different subnet than the one used to forward the request.
Related: https://github.com/389ds/389-ds-base/issues/3527
Reviewed by: @progier389, @jchapma (Thanks!)
---
ldap/servers/slapd/connection.c | 35 +++++++++++++++++++++++++--------
1 file changed, 27 insertions(+), 8 deletions(-)
diff --git a/ldap/servers/slapd/connection.c b/ldap/servers/slapd/connection.c
index d28a39bf7..10a8cc577 100644
--- a/ldap/servers/slapd/connection.c
+++ b/ldap/servers/slapd/connection.c
@@ -1187,6 +1187,8 @@ connection_read_operation(Connection *conn, Operation *op, ber_tag_t *tag, int *
char str_ip[INET6_ADDRSTRLEN + 1] = {0};
char str_haproxy_ip[INET6_ADDRSTRLEN + 1] = {0};
char str_haproxy_destip[INET6_ADDRSTRLEN + 1] = {0};
+ int trusted_matches_ip_found = 0;
+ int trusted_matches_destip_found = 0;
struct berval **bvals = NULL;
int proxy_connection = 0;
@@ -1245,21 +1247,38 @@ connection_read_operation(Connection *conn, Operation *op, ber_tag_t *tag, int *
normalize_IPv4(conn->cin_addr, buf_ip, sizeof(buf_ip), str_ip, sizeof(str_ip));
normalize_IPv4(&pr_netaddr_dest, buf_haproxy_destip, sizeof(buf_haproxy_destip),
str_haproxy_destip, sizeof(str_haproxy_destip));
+ size_t ip_len = strlen(buf_ip);
+ size_t destip_len = strlen(buf_haproxy_destip);
/* Now, reset RC and set it to 0 only if a match is found */
haproxy_rc = -1;
- /* Allow only:
- * Trusted IP == Original Client IP == HAProxy Header Destination IP */
+ /*
+ * We need to allow a configuration where DS instance and HAProxy are on the same machine.
+ * In this case, we need to check if
+ * the HAProxy client IP (which will be a loopback address) matches one of the the trusted IP addresses,
+ * while still checking that
+ * the HAProxy header destination IP address matches one of the trusted IP addresses.
+ * Additionally, this change will also allow configuration having
+ * HAProxy listening on a different subnet than one used to forward the request.
+ */
for (size_t i = 0; bvals[i] != NULL; ++i) {
- if ((strlen(bvals[i]->bv_val) == strlen(buf_ip)) &&
- (strlen(bvals[i]->bv_val) == strlen(buf_haproxy_destip)) &&
- (strncasecmp(bvals[i]->bv_val, buf_ip, strlen(buf_ip)) == 0) &&
- (strncasecmp(bvals[i]->bv_val, buf_haproxy_destip, strlen(buf_haproxy_destip)) == 0)) {
- haproxy_rc = 0;
- break;
+ size_t bval_len = strlen(bvals[i]->bv_val);
+
+ /* Check if the Client IP (HAProxy's machine IP) address matches the trusted IP address */
+ if (!trusted_matches_ip_found) {
+ trusted_matches_ip_found = (bval_len == ip_len) && (strncasecmp(bvals[i]->bv_val, buf_ip, ip_len) == 0);
+ }
+ /* Check if the HAProxy header destination IP address matches the trusted IP address */
+ if (!trusted_matches_destip_found) {
+ trusted_matches_destip_found = (bval_len == destip_len) && (strncasecmp(bvals[i]->bv_val, buf_haproxy_destip, destip_len) == 0);
}
}
+
+ if (trusted_matches_ip_found && trusted_matches_destip_found) {
+ haproxy_rc = 0;
+ }
+
if (haproxy_rc == -1) {
slapi_log_err(SLAPI_LOG_CONNS, "connection_read_operation", "HAProxy header received from unknown source.\n");
disconnect_server_nomutex(conn, conn->c_connid, -1, SLAPD_DISCONNECT_PROXY_UNKNOWN, EPROTO);
--
2.45.0

View File

@ -1,7 +1,7 @@
From 2d9618da04161d7aecf2a19f5c06cd0b5749921c Mon Sep 17 00:00:00 2001
From cae81cde000df9a28733c673ee7c9b8292d69ef3 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Mon, 12 Jan 2026 10:58:02 +0100
Subject: [PATCH 2/2] Issue 7172 - (2nd) Index ordering mismatch after upgrade
Subject: [PATCH] Issue 7172 - (2nd) Index ordering mismatch after upgrade
(#7180)
Commit 742c12e0247ab64e87da000a4de2f3e5c99044ab introduced a regression
@ -30,7 +30,7 @@ Reviewed by: @tbordaz (Thanks!)
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/instance.c b/ldap/servers/slapd/back-ldbm/instance.c
index 04340a992..29643f7e4 100644
index 30db99c81..23bd70243 100644
--- a/ldap/servers/slapd/back-ldbm/instance.c
+++ b/ldap/servers/slapd/back-ldbm/instance.c
@@ -191,7 +191,7 @@ ldbm_instance_create_default_indexes(backend *be)
@ -52,7 +52,7 @@ index 04340a992..29643f7e4 100644
if (!index_already_configured) {
e = ldbm_instance_init_config_entry(LDBM_PARENTID_STR, "eq", 0, 0, 0, "integerOrderingMatch", parentid_indexes_limit);
ldbm_instance_config_add_index_entry(inst, e, flags);
@@ -308,7 +309,8 @@ ldbm_instance_create_default_indexes(backend *be)
@@ -302,7 +303,8 @@ ldbm_instance_create_default_indexes(backend *be)
* but we still want to use the attr index file APIs.
*/
ainfo_get(be, (char *)LDBM_ANCESTORID_STR, &ai);

View File

@ -1,108 +0,0 @@
From 016a2b6bd3e27cbff36609824a75b020dfd24823 Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Wed, 1 May 2024 15:01:33 +0100
Subject: [PATCH] CVE-2024-2199
---
.../tests/suites/password/password_test.py | 56 +++++++++++++++++++
ldap/servers/slapd/modify.c | 8 ++-
2 files changed, 62 insertions(+), 2 deletions(-)
diff --git a/dirsrvtests/tests/suites/password/password_test.py b/dirsrvtests/tests/suites/password/password_test.py
index 38079476a..b3ff08904 100644
--- a/dirsrvtests/tests/suites/password/password_test.py
+++ b/dirsrvtests/tests/suites/password/password_test.py
@@ -65,6 +65,62 @@ def test_password_delete_specific_password(topology_st):
log.info('test_password_delete_specific_password: PASSED')
+def test_password_modify_non_utf8(topology_st):
+ """Attempt a modify of the userPassword attribute with
+ an invalid non utf8 value
+
+ :id: a31af9d5-d665-42b9-8d6e-fea3d0837d36
+ :setup: Standalone instance
+ :steps:
+ 1. Add a user if it doesnt exist and set its password
+ 2. Verify password with a bind
+ 3. Modify userPassword attr with invalid value
+ 4. Attempt a bind with invalid password value
+ 5. Verify original password with a bind
+ :expectedresults:
+ 1. The user with userPassword should be added successfully
+ 2. Operation should be successful
+ 3. Server returns ldap.UNWILLING_TO_PERFORM
+ 4. Server returns ldap.INVALID_CREDENTIALS
+ 5. Operation should be successful
+ """
+
+ log.info('Running test_password_modify_non_utf8...')
+
+ # Create user and set password
+ standalone = topology_st.standalone
+ users = UserAccounts(standalone, DEFAULT_SUFFIX)
+ if not users.exists(TEST_USER_PROPERTIES['uid'][0]):
+ user = users.create(properties=TEST_USER_PROPERTIES)
+ else:
+ user = users.get(TEST_USER_PROPERTIES['uid'][0])
+ user.set('userpassword', PASSWORD)
+
+ # Verify password
+ try:
+ user.bind(PASSWORD)
+ except ldap.LDAPError as e:
+ log.fatal('Failed to bind as {}, error: '.format(user.dn) + e.args[0]['desc'])
+ assert False
+
+ # Modify userPassword with an invalid value
+ password = b'tes\x82t-password' # A non UTF-8 encoded password
+ with pytest.raises(ldap.UNWILLING_TO_PERFORM):
+ user.replace('userpassword', password)
+
+ # Verify a bind fails with invalid pasword
+ with pytest.raises(ldap.INVALID_CREDENTIALS):
+ user.bind(password)
+
+ # Verify we can still bind with original password
+ try:
+ user.bind(PASSWORD)
+ except ldap.LDAPError as e:
+ log.fatal('Failed to bind as {}, error: '.format(user.dn) + e.args[0]['desc'])
+ assert False
+
+ log.info('test_password_modify_non_utf8: PASSED')
+
if __name__ == '__main__':
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/modify.c b/ldap/servers/slapd/modify.c
index 5ca78539c..669bb104c 100644
--- a/ldap/servers/slapd/modify.c
+++ b/ldap/servers/slapd/modify.c
@@ -765,8 +765,10 @@ op_shared_modify(Slapi_PBlock *pb, int pw_change, char *old_pw)
* flagged - leave mod attributes alone */
if (!repl_op && !skip_modified_attrs && lastmod) {
modify_update_last_modified_attr(pb, &smods);
+ slapi_pblock_set(pb, SLAPI_MODIFY_MODS, slapi_mods_get_ldapmods_byref(&smods));
}
+
if (0 == slapi_mods_get_num_mods(&smods)) {
/* nothing to do - no mods - this is not an error - just
send back LDAP_SUCCESS */
@@ -933,8 +935,10 @@ op_shared_modify(Slapi_PBlock *pb, int pw_change, char *old_pw)
/* encode password */
if (pw_encodevals_ext(pb, sdn, va)) {
- slapi_log_err(SLAPI_LOG_CRIT, "op_shared_modify", "Unable to hash userPassword attribute for %s.\n", slapi_entry_get_dn_const(e));
- send_ldap_result(pb, LDAP_UNWILLING_TO_PERFORM, NULL, "Unable to store attribute \"userPassword\" correctly\n", 0, NULL);
+ slapi_log_err(SLAPI_LOG_CRIT, "op_shared_modify", "Unable to hash userPassword attribute for %s, "
+ "check value is utf8 string.\n", slapi_entry_get_dn_const(e));
+ send_ldap_result(pb, LDAP_UNWILLING_TO_PERFORM, NULL, "Unable to hash \"userPassword\" attribute, "
+ "check value is utf8 string.\n", 0, NULL);
valuearray_free(&va);
goto free_and_return;
}
--
2.45.0

View File

@ -1,7 +1,7 @@
From 05ce84ae76e0e71381bcc7b8491a74e7edcd888f Mon Sep 17 00:00:00 2001
From c5aab4f8ba822572daa9ef69a0109577bf2147e1 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Tue, 20 Jan 2026 09:52:47 +0100
Subject: [PATCH 2/2] Issue 7189 - DSBLE0007 generates incorrect remediation
Subject: [PATCH] Issue 7189 - DSBLE0007 generates incorrect remediation
commands for scan limits
Bug Description:
@ -32,10 +32,10 @@ Reviewed by: @progier389, @droideck (Thanks!)
2 files changed, 147 insertions(+), 18 deletions(-)
diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
index 1700ba207..f25de4214 100644
index 6293340ca..5eadf6283 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -406,6 +406,132 @@ def test_retrocl_plugin_missing_matching_rule(topology_st, retrocl_plugin_enable
@@ -405,6 +405,132 @@ def test_retrocl_plugin_missing_matching_rule(topology_st, retrocl_plugin_enable
run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
@ -169,10 +169,10 @@ index 1700ba207..f25de4214 100644
"""Check if healthcheck returns DSBLE0007 code when multiple system indexes are missing
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
index 14b64d1d3..3cea0df36 100644
index 024de2adb..d3c9ccf35 100644
--- a/src/lib389/lib389/backend.py
+++ b/src/lib389/lib389/backend.py
@@ -602,7 +602,7 @@ class Backend(DSLdapObject):
@@ -678,7 +678,7 @@ class Backend(DSLdapObject):
if expected_config.get('matching_rule'):
cmd += f" --matching-rule {expected_config['matching_rule']}"
if expected_config.get('scanlimit'):
@ -181,7 +181,7 @@ index 14b64d1d3..3cea0df36 100644
remediation_commands.append(cmd)
reindex_attrs.add(attr_name) # New index needs reindexing
else:
@@ -624,28 +624,31 @@ class Backend(DSLdapObject):
@@ -700,28 +700,31 @@ class Backend(DSLdapObject):
remediation_commands.append(cmd)
reindex_attrs.add(attr_name)

View File

@ -1,213 +0,0 @@
From d5bbe52fbe84a7d3b5938bf82d5c4af15061a8e2 Mon Sep 17 00:00:00 2001
From: Pierre Rogier <progier@redhat.com>
Date: Wed, 17 Apr 2024 18:18:04 +0200
Subject: [PATCH] CVE-2024-3657
---
.../tests/suites/filter/large_filter_test.py | 34 +++++-
ldap/servers/slapd/back-ldbm/index.c | 111 ++++++++++--------
2 files changed, 92 insertions(+), 53 deletions(-)
diff --git a/dirsrvtests/tests/suites/filter/large_filter_test.py b/dirsrvtests/tests/suites/filter/large_filter_test.py
index ecc7bf979..40526bb16 100644
--- a/dirsrvtests/tests/suites/filter/large_filter_test.py
+++ b/dirsrvtests/tests/suites/filter/large_filter_test.py
@@ -13,19 +13,29 @@ verify and testing Filter from a search
import os
import pytest
+import ldap
-from lib389._constants import PW_DM
+from lib389._constants import PW_DM, DEFAULT_SUFFIX, ErrorLog
from lib389.topologies import topology_st as topo
from lib389.idm.user import UserAccounts, UserAccount
from lib389.idm.account import Accounts
from lib389.backend import Backends
from lib389.idm.domain import Domain
+from lib389.utils import get_ldapurl_from_serverid
SUFFIX = 'dc=anuj,dc=com'
pytestmark = pytest.mark.tier1
+def open_new_ldapi_conn(dsinstance):
+ ldapurl, certdir = get_ldapurl_from_serverid(dsinstance)
+ assert 'ldapi://' in ldapurl
+ conn = ldap.initialize(ldapurl)
+ conn.sasl_interactive_bind_s("", ldap.sasl.external())
+ return conn
+
+
@pytest.fixture(scope="module")
def _create_entries(request, topo):
"""
@@ -160,6 +170,28 @@ def test_large_filter(topo, _create_entries, real_value):
assert len(Accounts(conn, SUFFIX).filter(real_value)) == 3
+def test_long_filter_value(topo):
+ """Exercise large eq filter with dn syntax attributes
+
+ :id: b069ef72-fcc3-11ee-981c-482ae39447e5
+ :setup: Standalone
+ :steps:
+ 1. Try to pass filter rules as per the condition.
+ :expectedresults:
+ 1. Pass
+ """
+ inst = topo.standalone
+ conn = open_new_ldapi_conn(inst.serverid)
+ inst.config.loglevel(vals=(ErrorLog.DEFAULT,ErrorLog.TRACE,ErrorLog.SEARCH_FILTER))
+ filter_value = "a\x1Edmin" * 1025
+ conn.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, f'(cn={filter_value})')
+ filter_value = "aAdmin" * 1025
+ conn.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, f'(cn={filter_value})')
+ filter_value = "*"
+ conn.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, f'(cn={filter_value})')
+ inst.config.loglevel(vals=(ErrorLog.DEFAULT,))
+
+
if __name__ == '__main__':
CURRENT_FILE = os.path.realpath(__file__)
pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/ldap/servers/slapd/back-ldbm/index.c b/ldap/servers/slapd/back-ldbm/index.c
index 410db23d1..30fa09ebb 100644
--- a/ldap/servers/slapd/back-ldbm/index.c
+++ b/ldap/servers/slapd/back-ldbm/index.c
@@ -71,6 +71,32 @@ typedef struct _index_buffer_handle index_buffer_handle;
#define INDEX_BUFFER_FLAG_SERIALIZE 1
#define INDEX_BUFFER_FLAG_STATS 2
+/*
+ * space needed to encode a byte:
+ * 0x00-0x31 and 0x7f-0xff requires 3 bytes: \xx
+ * 0x22 and 0x5C requires 2 bytes: \" and \\
+ * other requires 1 byte: c
+ */
+static char encode_size[] = {
+ /* 0x00 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ /* 0x10 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ /* 0x20 */ 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ /* 0x30 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ /* 0x40 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ /* 0x50 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1,
+ /* 0x60 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ /* 0x70 */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3,
+ /* 0x80 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ /* 0x90 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ /* 0xA0 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ /* 0xB0 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ /* 0xC0 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ /* 0xD0 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ /* 0xE0 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ /* 0xF0 */ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+};
+
+
/* Index buffering functions */
static int
@@ -799,65 +825,46 @@ index_add_mods(
/*
* Convert a 'struct berval' into a displayable ASCII string
+ * returns the printable string
*/
-
-#define SPECIAL(c) (c < 32 || c > 126 || c == '\\' || c == '"')
-
const char *
encode(const struct berval *data, char buf[BUFSIZ])
{
- char *s;
- char *last;
- if (data == NULL || data->bv_len == 0)
- return "";
- last = data->bv_val + data->bv_len - 1;
- for (s = data->bv_val; s < last; ++s) {
- if (SPECIAL(*s)) {
- char *first = data->bv_val;
- char *bufNext = buf;
- size_t bufSpace = BUFSIZ - 4;
- while (1) {
- /* printf ("%lu bytes ASCII\n", (unsigned long)(s - first)); */
- if (bufSpace < (size_t)(s - first))
- s = first + bufSpace - 1;
- if (s != first) {
- memcpy(bufNext, first, s - first);
- bufNext += (s - first);
- bufSpace -= (s - first);
- }
- do {
- if (bufSpace) {
- *bufNext++ = '\\';
- --bufSpace;
- }
- if (bufSpace < 2) {
- memcpy(bufNext, "..", 2);
- bufNext += 2;
- goto bail;
- }
- if (*s == '\\' || *s == '"') {
- *bufNext++ = *s;
- --bufSpace;
- } else {
- sprintf(bufNext, "%02x", (unsigned)*(unsigned char *)s);
- bufNext += 2;
- bufSpace -= 2;
- }
- } while (++s <= last && SPECIAL(*s));
- if (s > last)
- break;
- first = s;
- while (!SPECIAL(*s) && s <= last)
- ++s;
- }
- bail:
- *bufNext = '\0';
- /* printf ("%lu chars in buffer\n", (unsigned long)(bufNext - buf)); */
+ if (!data || !data->bv_val) {
+ strcpy(buf, "<NULL>");
+ return buf;
+ }
+ char *endbuff = &buf[BUFSIZ-4]; /* Reserve space to append "...\0" */
+ char *ptout = buf;
+ unsigned char *ptin = (unsigned char*) data->bv_val;
+ unsigned char *endptin = ptin+data->bv_len;
+
+ while (ptin < endptin) {
+ if (ptout >= endbuff) {
+ /*
+ * BUFSIZ(8K) > SLAPI_LOG_BUFSIZ(2K) so the error log message will be
+ * truncated anyway. So there is no real interrest to test if the original
+ * data contains no special characters and return it as is.
+ */
+ strcpy(endbuff, "...");
return buf;
}
+ switch (encode_size[*ptin]) {
+ case 1:
+ *ptout++ = *ptin++;
+ break;
+ case 2:
+ *ptout++ = '\\';
+ *ptout++ = *ptin++;
+ break;
+ case 3:
+ sprintf(ptout, "\\%02x", *ptin++);
+ ptout += 3;
+ break;
+ }
}
- /* printf ("%lu bytes, all ASCII\n", (unsigned long)(s - data->bv_val)); */
- return data->bv_val;
+ *ptout = 0;
+ return buf;
}
static const char *
--
2.45.0

View File

@ -0,0 +1,48 @@
From 327ebf93decaa19bd3919afaeba0dc122eef1990 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Mon, 12 Jan 2026 13:53:05 -0500
Subject: [PATCH] Issue 7184 - argparse.HelpFormatter _format_actions_usage()
is deprecated
Description:
_format_actions_usage() was removed in python 3.15. Instead we can use
_get_actions_usage_parts() but it also behaves differently between
python 3.14 and 3.15 so we need special handling.
Relates: https://github.com/389ds/389-ds-base/issues/7184
Reviewed by: spichugi(Thanks!)
---
src/lib389/lib389/cli_base/__init__.py | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/lib389/lib389/cli_base/__init__.py b/src/lib389/lib389/cli_base/__init__.py
index 06b8f9964..f1055aadc 100644
--- a/src/lib389/lib389/cli_base/__init__.py
+++ b/src/lib389/lib389/cli_base/__init__.py
@@ -413,7 +413,20 @@ class CustomHelpFormatter(argparse.HelpFormatter):
def _format_usage(self, usage, actions, groups, prefix):
usage = super(CustomHelpFormatter, self)._format_usage(usage, actions, groups, prefix)
- formatted_options = self._format_actions_usage(parent_arguments, [])
+
+ if sys.version_info < (3, 13):
+ # Use _format_actions_usage() for Python 3.12 and earlier
+ formatted_options = self._format_actions_usage(parent_arguments, [])
+ else:
+ # Use _get_actions_usage_parts() for Python 3.13 and later
+ action_parts = self._get_actions_usage_parts(parent_arguments, [])
+ if sys.version_info >= (3, 15):
+ # Python 3.15 returns a tuple (list of actions, count of actions)
+ formatted_options = ' '.join(action_parts[0])
+ else:
+ # Python 3.13 and 3.14 return a list of actions
+ formatted_options = ' '.join(action_parts)
+
# If formatted_options already in usage - remove them
if formatted_options in usage:
usage = usage.replace(f' {formatted_options}', '')
--
2.52.0

View File

@ -1,143 +0,0 @@
From 6e5f03d5872129963106024f53765234a282406c Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Fri, 16 Feb 2024 11:13:16 +0000
Subject: [PATCH] Issue 6096 - Improve connection timeout error logging (#6097)
Bug description: When a paged result search is run with a time limit,
if the time limit is exceed the server closes the connection with
closed IO timeout (nsslapd-ioblocktimeout) - T2. This error message
is incorrect as the reason the connection has been closed was because
the specified time limit on a paged result search has been exceeded.
Fix description: Correct error message
Relates: https://github.com/389ds/389-ds-base/issues/6096
Reviewed by: @tbordaz (Thank you)
---
ldap/admin/src/logconv.pl | 24 ++++++++++++++++++-
ldap/servers/slapd/daemon.c | 4 ++--
ldap/servers/slapd/disconnect_error_strings.h | 1 +
ldap/servers/slapd/disconnect_errors.h | 2 +-
4 files changed, 27 insertions(+), 4 deletions(-)
diff --git a/ldap/admin/src/logconv.pl b/ldap/admin/src/logconv.pl
index 7698c383a..2a933c4a3 100755
--- a/ldap/admin/src/logconv.pl
+++ b/ldap/admin/src/logconv.pl
@@ -267,7 +267,7 @@ my $optimeAvg = 0;
my %cipher = ();
my @removefiles = ();
-my @conncodes = qw(A1 B1 B4 T1 T2 B2 B3 R1 P1 P2 U1);
+my @conncodes = qw(A1 B1 B4 T1 T2 T3 B2 B3 R1 P1 P2 U1);
my %conn = ();
map {$conn{$_} = $_} @conncodes;
@@ -355,6 +355,7 @@ $connmsg{"B1"} = "Bad Ber Tag Encountered";
$connmsg{"B4"} = "Server failed to flush data (response) back to Client";
$connmsg{"T1"} = "Idle Timeout Exceeded";
$connmsg{"T2"} = "IO Block Timeout Exceeded or NTSSL Timeout";
+$connmsg{"T3"} = "Paged Search Time Limit Exceeded";
$connmsg{"B2"} = "Ber Too Big";
$connmsg{"B3"} = "Ber Peek";
$connmsg{"R1"} = "Revents";
@@ -1723,6 +1724,10 @@ if ($usage =~ /j/i || $verb eq "yes"){
print "\n $recCount. You have some coonections that are being closed by the ioblocktimeout setting. You may want to increase the ioblocktimeout.\n";
$recCount++;
}
+ if (defined($conncount->{"T3"}) and $conncount->{"T3"} > 0){
+ print "\n $recCount. You have some connections that are being closed because a paged result search limit has been exceeded. You may want to increase the search time limit.\n";
+ $recCount++;
+ }
# compare binds to unbinds, if the difference is more than 30% of the binds, then report a issue
if (($bindCount - $unbindCount) > ($bindCount*.3)){
print "\n $recCount. You have a significant difference between binds and unbinds. You may want to investigate this difference.\n";
@@ -2366,6 +2371,7 @@ sub parseLineNormal
$brokenPipeCount++;
if (m/- T1/){ $hashes->{rc}->{"T1"}++; }
elsif (m/- T2/){ $hashes->{rc}->{"T2"}++; }
+ elsif (m/- T3/){ $hashes->{rc}->{"T3"}++; }
elsif (m/- A1/){ $hashes->{rc}->{"A1"}++; }
elsif (m/- B1/){ $hashes->{rc}->{"B1"}++; }
elsif (m/- B4/){ $hashes->{rc}->{"B4"}++; }
@@ -2381,6 +2387,7 @@ sub parseLineNormal
$connResetByPeerCount++;
if (m/- T1/){ $hashes->{src}->{"T1"}++; }
elsif (m/- T2/){ $hashes->{src}->{"T2"}++; }
+ elsif (m/- T3/){ $hashes->{src}->{"T3"}++; }
elsif (m/- A1/){ $hashes->{src}->{"A1"}++; }
elsif (m/- B1/){ $hashes->{src}->{"B1"}++; }
elsif (m/- B4/){ $hashes->{src}->{"B4"}++; }
@@ -2396,6 +2403,7 @@ sub parseLineNormal
$resourceUnavailCount++;
if (m/- T1/){ $hashes->{rsrc}->{"T1"}++; }
elsif (m/- T2/){ $hashes->{rsrc}->{"T2"}++; }
+ elsif (m/- T3/){ $hashes->{rsrc}->{"T3"}++; }
elsif (m/- A1/){ $hashes->{rsrc}->{"A1"}++; }
elsif (m/- B1/){ $hashes->{rsrc}->{"B1"}++; }
elsif (m/- B4/){ $hashes->{rsrc}->{"B4"}++; }
@@ -2494,6 +2502,20 @@ sub parseLineNormal
}
}
}
+ if (m/- T3/){
+ if ($_ =~ /conn= *([0-9A-Z]+)/i) {
+ $exc = "no";
+ $ip = getIPfromConn($1, $serverRestartCount);
+ for (my $xxx = 0; $xxx < $#excludeIP; $xxx++){
+ if ($ip eq $excludeIP[$xxx]){$exc = "yes";}
+ }
+ if ($exc ne "yes"){
+ $hashes->{T3}->{$ip}++;
+ $hashes->{conncount}->{"T3"}++;
+ $connCodeCount++;
+ }
+ }
+ }
if (m/- B2/){
if ($_ =~ /conn= *([0-9A-Z]+)/i) {
$exc = "no";
diff --git a/ldap/servers/slapd/daemon.c b/ldap/servers/slapd/daemon.c
index 5a48aa66f..bb80dae36 100644
--- a/ldap/servers/slapd/daemon.c
+++ b/ldap/servers/slapd/daemon.c
@@ -1599,9 +1599,9 @@ setup_pr_read_pds(Connection_Table *ct)
int add_fd = 1;
/* check timeout for PAGED RESULTS */
if (pagedresults_is_timedout_nolock(c)) {
- /* Exceeded the timelimit; disconnect the client */
+ /* Exceeded the paged search timelimit; disconnect the client */
disconnect_server_nomutex(c, c->c_connid, -1,
- SLAPD_DISCONNECT_IO_TIMEOUT,
+ SLAPD_DISCONNECT_PAGED_SEARCH_LIMIT,
0);
connection_table_move_connection_out_of_active_list(ct,
c);
diff --git a/ldap/servers/slapd/disconnect_error_strings.h b/ldap/servers/slapd/disconnect_error_strings.h
index f7a31d728..c2d9e283b 100644
--- a/ldap/servers/slapd/disconnect_error_strings.h
+++ b/ldap/servers/slapd/disconnect_error_strings.h
@@ -27,6 +27,7 @@ ER2(SLAPD_DISCONNECT_BER_FLUSH, "B4")
ER2(SLAPD_DISCONNECT_IDLE_TIMEOUT, "T1")
ER2(SLAPD_DISCONNECT_REVENTS, "R1")
ER2(SLAPD_DISCONNECT_IO_TIMEOUT, "T2")
+ER2(SLAPD_DISCONNECT_PAGED_SEARCH_LIMIT, "T3")
ER2(SLAPD_DISCONNECT_PLUGIN, "P1")
ER2(SLAPD_DISCONNECT_UNBIND, "U1")
ER2(SLAPD_DISCONNECT_POLL, "P2")
diff --git a/ldap/servers/slapd/disconnect_errors.h b/ldap/servers/slapd/disconnect_errors.h
index a0484f1c2..e118f674c 100644
--- a/ldap/servers/slapd/disconnect_errors.h
+++ b/ldap/servers/slapd/disconnect_errors.h
@@ -35,6 +35,6 @@
#define SLAPD_DISCONNECT_SASL_FAIL SLAPD_DISCONNECT_ERROR_BASE + 12
#define SLAPD_DISCONNECT_PROXY_INVALID_HEADER SLAPD_DISCONNECT_ERROR_BASE + 13
#define SLAPD_DISCONNECT_PROXY_UNKNOWN SLAPD_DISCONNECT_ERROR_BASE + 14
-
+#define SLAPD_DISCONNECT_PAGED_SEARCH_LIMIT SLAPD_DISCONNECT_ERROR_BASE + 15
#endif /* __DISCONNECT_ERRORS_H_ */
--
2.45.0

View File

@ -0,0 +1,53 @@
From dbabbe6a3c42b979d14960b355925c9748371cad Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Fri, 30 Jan 2026 12:00:13 +0100
Subject: [PATCH] Issue 7027 - (2nd) 389-ds-base OpenScanHub Leaks Detected
(#7211)
Fix Description:
Update coverity annotations.
Relates: https://github.com/389ds/389-ds-base/issues/7027
Reviewed by: @aadhikar (Thanks!)
---
ldap/servers/slapd/log.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c
index 668a02454..6f57a3d9c 100644
--- a/ldap/servers/slapd/log.c
+++ b/ldap/servers/slapd/log.c
@@ -203,8 +203,8 @@ compress_log_file(char *log_name, int32_t mode)
if ((source = fopen(log_name, "r")) == NULL) {
/* Failed to open log file */
- /* coverity[leaked_storage] gzclose does close FD */
gzclose(outfile);
+ /* coverity[leaked_handle] gzclose does close FD */
return -1;
}
@@ -214,17 +214,17 @@ compress_log_file(char *log_name, int32_t mode)
if (bytes_written == 0)
{
fclose(source);
- /* coverity[leaked_storage] gzclose does close FD */
gzclose(outfile);
+ /* coverity[leaked_handle] gzclose does close FD */
return -1;
}
bytes_read = fread(buf, 1, LOG_CHUNK, source);
}
- /* coverity[leaked_storage] gzclose does close FD */
gzclose(outfile);
fclose(source);
PR_Delete(log_name); /* remove the old uncompressed log */
+ /* coverity[leaked_handle] gzclose does close FD */
return 0;
}
--
2.52.0

View File

@ -1,44 +0,0 @@
From a112394af3a20787755029804684d57a9c3ffa9a Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Wed, 21 Feb 2024 12:43:03 +0000
Subject: [PATCH] Issue 6103 - New connection timeout error breaks errormap
(#6104)
Bug description: A recent addition to the connection disconnect error
messaging, conflicts with how errormap.c maps error codes/strings.
Fix description: errormap expects error codes/strings to be in ascending
order. Moved the new error code to the bottom of the list.
Relates: https://github.com/389ds/389-ds-base/issues/6103
Reviewed by: @droideck. @progier389 (Thank you)
---
ldap/servers/slapd/disconnect_error_strings.h | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/ldap/servers/slapd/disconnect_error_strings.h b/ldap/servers/slapd/disconnect_error_strings.h
index c2d9e283b..f603a08ce 100644
--- a/ldap/servers/slapd/disconnect_error_strings.h
+++ b/ldap/servers/slapd/disconnect_error_strings.h
@@ -14,7 +14,8 @@
/* disconnect_error_strings.h
*
* Strings describing the errors used in logging the reason a connection
- * was closed.
+ * was closed. Ensure definitions are in the same order as the error codes
+ * defined in disconnect_errors.h
*/
#ifndef __DISCONNECT_ERROR_STRINGS_H_
#define __DISCONNECT_ERROR_STRINGS_H_
@@ -35,6 +36,6 @@ ER2(SLAPD_DISCONNECT_NTSSL_TIMEOUT, "T2")
ER2(SLAPD_DISCONNECT_SASL_FAIL, "S1")
ER2(SLAPD_DISCONNECT_PROXY_INVALID_HEADER, "P3")
ER2(SLAPD_DISCONNECT_PROXY_UNKNOWN, "P4")
-
+ER2(SLAPD_DISCONNECT_PAGED_SEARCH_LIMIT, "T3")
#endif /* __DISCONNECT_ERROR_STRINGS_H_ */
--
2.45.0

View File

@ -0,0 +1,197 @@
From 39d47ca91e866d30b35dca41d477ad4ee07f8a14 Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Mon, 2 Feb 2026 15:39:18 +0100
Subject: [PATCH] Issue 7213 - MDB_BAD_VALSIZE error while handling VLV (#7214)
* Issue 7213 - MDB_BAD_VALSIZE error while handling VLV
Avoid failing lmdb operation when handling VLV index by truncating the key so that key+data is small enough.
Issue: #7213
Reviewed by: @mreynolds389 , @vashirov (Thanks!)
Assisted by: Claude A/I
(cherry picked from commit 5ebce22d4214bec5ed94ad84c4448164be99389a)
---
.../tests/suites/vlv/regression_test.py | 110 ++++++++++++++++++
.../slapd/back-ldbm/db-mdb/mdb_layer.c | 5 +
ldap/servers/slapd/back-ldbm/vlv.c | 7 +-
3 files changed, 121 insertions(+), 1 deletion(-)
diff --git a/dirsrvtests/tests/suites/vlv/regression_test.py b/dirsrvtests/tests/suites/vlv/regression_test.py
index c8db94b19..1cc03c303 100644
--- a/dirsrvtests/tests/suites/vlv/regression_test.py
+++ b/dirsrvtests/tests/suites/vlv/regression_test.py
@@ -1178,6 +1178,116 @@ def test_vlv_with_mr(vlv_setup_with_uid_mr):
+def test_vlv_long_attribute_value(topology_st, request):
+ """
+ Test VLV with an entry containing a very long attribute value (2K).
+
+ :id: 99126fa4-003e-11f1-b7d6-c85309d5c3e3
+ :setup: Standalone instance.
+ :steps:
+ 1. Cleanup leftover from previous tests
+ 2. Create VLV search and index on cn attribute
+ 3. Reindex VLV
+ 4. Add an entry with a cn attribute having 2K character value
+ 5. Verify the entry was added successfully
+ 6. Perform a VLV search to ensure it still works
+ 7. Add another entry with a cn attribute having 2K character value
+ 8. Verify the entry was added successfully
+ 9. Perform a VLV search to ensure it still works
+ :expectedresults:
+ 1. Should Success.
+ 2. Should Success.
+ 3. Should Success.
+ 4. Should Success.
+ 5. Should Success.
+ 6. Should Success.
+ 7. Should Success.
+ 8. Should Success.
+ 9. Should Success.
+ """
+ inst = topology_st.standalone
+ reindex_task = Tasks(inst)
+
+ users_to_delete = []
+
+ def fin():
+ cleanup(inst)
+ # Clean the added users
+ for user in users_to_delete:
+ user.delete()
+
+ if not DEBUGGING:
+ request.addfinalizer(fin)
+
+ # Clean previous tests leftover
+ fin()
+
+ # Create VLV search and index
+ vlv_search, vlv_index = create_vlv_search_and_index(inst)
+ assert reindex_task.reindex(
+ suffix=DEFAULT_SUFFIX,
+ attrname=vlv_index.rdn,
+ args={TASK_WAIT: True},
+ vlv=True
+ ) == 0
+
+ # Add a few regular users first
+ add_users(inst, 10)
+
+ # Create a very long cn value (2K characters)
+ long_cn_value = 'a' * 2048 + '1'
+
+ # Add an entry with the long cn attribute
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ user_properties = {
+ 'uid': 'longcnuser1',
+ 'cn': long_cn_value,
+ 'sn': 'user1',
+ 'uidNumber': '99999',
+ 'gidNumber': '99999',
+ 'homeDirectory': '/home/longcnuser1'
+ }
+ user = users.create(properties=user_properties)
+ users_to_delete.append(user);
+
+ # Verify the entry was created and has the long cn value
+ entry = user.get_attr_vals_utf8('cn')
+ assert entry[0] == long_cn_value
+ log.info(f'Successfully created user with cn length: {len(entry[0])}')
+
+ # Perform VLV search to ensure VLV still works with long attribute values
+ conn = open_new_ldapi_conn(inst.serverid)
+ count = len(conn.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(uid=*)"))
+ assert count > 0
+ log.info(f'VLV search successful with {count} entries including entry with 2K cn value')
+
+ # Add another entry with the long cn attribute
+ long_cn_value = 'a' * 2048 + '2'
+
+ user_properties = {
+ 'uid': 'longcnuser2',
+ 'cn': long_cn_value,
+ 'sn': 'user2',
+ 'uidNumber': '99998',
+ 'gidNumber': '99998',
+ 'homeDirectory': '/home/longcnuser2'
+ }
+ user = users.create(properties=user_properties)
+ users_to_delete.append(user);
+
+ # Verify the entry was created and has the long cn value
+ entry = user.get_attr_vals_utf8('cn')
+ assert entry[0] == long_cn_value
+ log.info(f'Successfully created user with cn length: {len(entry[0])}')
+
+ # Perform VLV search to ensure VLV still works with long attribute values
+ conn = open_new_ldapi_conn(inst.serverid)
+ count = len(conn.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(uid=*)"))
+ assert count > 1
+ log.info(f'VLV search successful with {count} entries including entry with 2K cn value')
+
+
+
if __name__ == "__main__":
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c
index c6e9f8b01..ffbd6609f 100644
--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c
+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c
@@ -2138,10 +2138,15 @@ void *dbmdb_recno_cache_build(void *arg)
recno = 1;
}
while (rc == 0) {
+ struct ldbminfo *li = (struct ldbminfo *)rcctx->cursor->be->be_database->plg_private;
slapi_log_err(SLAPI_LOG_DEBUG, "dbmdb_recno_cache_build", "recno=%d\n", recno);
if (recno % RECNO_CACHE_INTERVAL == 1) {
/* Prepare the cache data */
len = sizeof(*rce) + data.mv_size + key.mv_size;
+ if (len > li->li_max_key_len) {
+ key.mv_size = li->li_max_key_len - data.mv_size - sizeof(*rce);
+ len = li->li_max_key_len;
+ }
rce = (dbmdb_recno_cache_elmt_t*)slapi_ch_malloc(len);
rce->len = len;
rce->recno = recno;
diff --git a/ldap/servers/slapd/back-ldbm/vlv.c b/ldap/servers/slapd/back-ldbm/vlv.c
index 18431799c..a1cd226f4 100644
--- a/ldap/servers/slapd/back-ldbm/vlv.c
+++ b/ldap/servers/slapd/back-ldbm/vlv.c
@@ -866,6 +866,7 @@ do_vlv_update_index(back_txn *txn, struct ldbminfo *li, Slapi_PBlock *pb, struct
struct vlv_key *key = NULL;
dbi_val_t data = {0};
dblayer_private *priv = NULL;
+ size_t key_size_limit = li->li_max_key_len - sizeof(entry->ep_id);
slapi_pblock_get(pb, SLAPI_BACKEND, &be);
priv = (dblayer_private *)li->li_dblayer_private;
@@ -886,6 +887,10 @@ do_vlv_update_index(back_txn *txn, struct ldbminfo *li, Slapi_PBlock *pb, struct
return rc;
}
+ /* Truncate the key if it is too long */
+ if (key->key.size > key_size_limit) {
+ key->key.size = key_size_limit;
+ }
if (NULL != txn) {
db_txn = txn->back_txn_txn;
} else {
@@ -930,7 +935,7 @@ do_vlv_update_index(back_txn *txn, struct ldbminfo *li, Slapi_PBlock *pb, struct
if (txn && txn->back_special_handling_fn) {
rc = txn->back_special_handling_fn(be, BTXNACT_VLV_DEL, db, &key->key, &data, txn);
} else {
- rc = dblayer_db_op(be, db, db_txn, DBI_OP_DEL, &key->key, NULL);
+ rc = dblayer_db_op(be, db, db_txn, DBI_OP_DEL, &key->key, &data);
}
if (rc == 0) {
if (txn && txn->back_special_handling_fn) {
--
2.52.0

View File

@ -1,30 +0,0 @@
From edd9abc8901604dde1d739d87ca2906734d53dd3 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Thu, 13 Jun 2024 13:35:09 +0200
Subject: [PATCH] Issue 6103 - New connection timeout error breaks errormap
Description:
Remove duplicate SLAPD_DISCONNECT_PAGED_SEARCH_LIMIT error code.
Fixes: https://github.com/389ds/389-ds-base/issues/6103
Reviewed by: @tbordaz (Thanks!)
---
ldap/servers/slapd/disconnect_error_strings.h | 1 -
1 file changed, 1 deletion(-)
diff --git a/ldap/servers/slapd/disconnect_error_strings.h b/ldap/servers/slapd/disconnect_error_strings.h
index f603a08ce..d49cc79a2 100644
--- a/ldap/servers/slapd/disconnect_error_strings.h
+++ b/ldap/servers/slapd/disconnect_error_strings.h
@@ -28,7 +28,6 @@ ER2(SLAPD_DISCONNECT_BER_FLUSH, "B4")
ER2(SLAPD_DISCONNECT_IDLE_TIMEOUT, "T1")
ER2(SLAPD_DISCONNECT_REVENTS, "R1")
ER2(SLAPD_DISCONNECT_IO_TIMEOUT, "T2")
-ER2(SLAPD_DISCONNECT_PAGED_SEARCH_LIMIT, "T3")
ER2(SLAPD_DISCONNECT_PLUGIN, "P1")
ER2(SLAPD_DISCONNECT_UNBIND, "U1")
ER2(SLAPD_DISCONNECT_POLL, "P2")
--
2.45.0

View File

@ -0,0 +1,43 @@
From 70d323d4d9ca4d1d682ab4a273178946922c6929 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Fri, 24 Jan 2025 11:10:45 +0100
Subject: [PATCH] Issue 6542 - RPM build errors on Fedora 42
Bug Description:
Fedora 42 has unified `/bin` and `/sbin`: https://fedoraproject.org/wiki/Changes/Unify_bin_and_sbin
https://docs.fedoraproject.org/en-US/packaging-guidelines/#_merged_file_system_layout
This change causes RPM build to fail with:
```
RPM build errors:
File not found: /builddir/build/BUILD/389-ds-base-3.1.2-build/BUILDROOT/usr/bin/openldap_to_ds
```
Fix Description:
Patch `setup.py.in` based on Fedora or RHEL version that support unified
`/bin` and `/sbin`.
Fixes: https://github.com/389ds/389-ds-base/issues/6542
Reviewed by: @mreynolds389, @droideck (Thanks!)
---
rpm/389-ds-base.spec.in | 3 +++
1 file changed, 3 insertions(+)
diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in
index 258d94698..44a158ce5 100644
--- a/rpm/389-ds-base.spec.in
+++ b/rpm/389-ds-base.spec.in
@@ -522,6 +522,9 @@ autoreconf -fiv
%if 0%{?rhel} > 7 || 0%{?fedora}
# lib389
+%if 0%{?fedora} >= 42 || 0%{?rhel} >= 11
+ sed -i "/prefix/s@sbin@bin@g" src/lib389/setup.py.in
+%endif
make src/lib389/setup.py
pushd ./src/lib389
%py3_build
--
2.52.0

View File

@ -1,220 +0,0 @@
From 8cf981c00ae18d3efaeb10819282cd991621e9a2 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Wed, 22 May 2024 11:29:05 +0200
Subject: [PATCH] Issue 6172 - RFE: improve the performance of evaluation of
filter component when tested against a large valueset (like group members)
(#6173)
Bug description:
Before returning an entry (to a SRCH) the server checks that the entry matches the SRCH filter.
If a filter component (equality) is testing the value (ava) against a
large valueset (like uniquemember values), it takes a long time because
of the large number of values and required normalization of the values.
This can be improved taking benefit of sorted valueset. Those sorted
valueset were created to improve updates of large valueset (groups) but
at that time not implemented in SRCH path.
Fix description:
In case of LDAP_FILTER_EQUALITY component, the server can get
benefit of the sorted valuearray.
To limit the risk of regression, we use the sorted valuearray
only for the DN syntax attribute. Indeed the sorted valuearray was
designed for those type of attribute.
With those two limitations, there is no need of a toggle and
the call to plugin_call_syntax_filter_ava can be replaced by
a call to slapi_valueset_find.
In both cases, sorted valueset and plugin_call_syntax_filter_ava, ava and
values are normalized.
In sorted valueset, the values have been normalized to insert the index
in the sorted array and then comparison is done on normalized values.
In plugin_call_syntax_filter_ava, all values in valuearray (of valueset) are normalized
before comparison.
relates: #6172
Reviewed by: Pierre Rogier, Simon Pichugin (Big Thanks !!!)
---
.../tests/suites/filter/filter_test.py | 125 ++++++++++++++++++
ldap/servers/slapd/filterentry.c | 22 ++-
2 files changed, 146 insertions(+), 1 deletion(-)
diff --git a/dirsrvtests/tests/suites/filter/filter_test.py b/dirsrvtests/tests/suites/filter/filter_test.py
index d6bfa5a3b..4baaf04a7 100644
--- a/dirsrvtests/tests/suites/filter/filter_test.py
+++ b/dirsrvtests/tests/suites/filter/filter_test.py
@@ -9,7 +9,11 @@
import logging
import pytest
+import time
+from lib389.dirsrv_log import DirsrvAccessLog
from lib389.tasks import *
+from lib389.backend import Backends, Backend
+from lib389.dbgen import dbgen_users, dbgen_groups
from lib389.topologies import topology_st
from lib389._constants import PASSWORD, DEFAULT_SUFFIX, DN_DM, SUFFIX
from lib389.utils import *
@@ -304,6 +308,127 @@ def test_extended_search(topology_st):
ents = topology_st.standalone.search_s(SUFFIX, ldap.SCOPE_SUBTREE, myfilter)
assert len(ents) == 1
+def test_match_large_valueset(topology_st):
+ """Test that when returning a big number of entries
+ and that we need to match the filter from a large valueset
+ we get benefit to use the sorted valueset
+
+ :id: 7db5aa88-50e0-4c31-85dd-1d2072cb674c
+
+ :setup: Standalone instance
+
+ :steps:
+ 1. Create a users and groups backends and tune them
+ 2. Generate a test ldif (2k users and 1K groups with all users)
+ 3. Import test ldif file using Offline import (ldif2db).
+ 4. Prim the 'groups' entrycache with a "fast" search
+ 5. Search the 'groups' with a difficult matching value
+ 6. check that etime from step 5 is less than a second
+
+ :expectedresults:
+ 1. Create a users and groups backends should PASS
+ 2. Generate LDIF should PASS.
+ 3. Offline import should PASS.
+ 4. Priming should PASS.
+ 5. Performance search should PASS.
+ 6. Etime of performance search should PASS.
+ """
+
+ log.info('Running test_match_large_valueset...')
+ #
+ # Test online/offline LDIF imports
+ #
+ inst = topology_st.standalone
+ inst.start()
+ backends = Backends(inst)
+ users_suffix = "ou=users,%s" % DEFAULT_SUFFIX
+ users_backend = 'users'
+ users_ldif = 'users_import.ldif'
+ groups_suffix = "ou=groups,%s" % DEFAULT_SUFFIX
+ groups_backend = 'groups'
+ groups_ldif = 'groups_import.ldif'
+ groups_entrycache = '200000000'
+ users_number = 2000
+ groups_number = 1000
+
+
+ # For priming the cache we just want to be fast
+ # taking the first value in the valueset is good
+ # whether the valueset is sorted or not
+ priming_user_rdn = "user0001"
+
+ # For performance testing, this is important to use
+ # user1000 rather then user0001
+ # Because user0001 is the first value in the valueset
+ # whether we use the sorted valuearray or non sorted
+ # valuearray the performance will be similar.
+ # With middle value user1000, the performance boost of
+ # the sorted valuearray will make the difference.
+ perf_user_rdn = "user1000"
+
+ # Step 1. Prepare the backends and tune the groups entrycache
+ try:
+ be_users = backends.create(properties={'parent': DEFAULT_SUFFIX, 'nsslapd-suffix': users_suffix, 'name': users_backend})
+ be_groups = backends.create(properties={'parent': DEFAULT_SUFFIX, 'nsslapd-suffix': groups_suffix, 'name': groups_backend})
+
+ # set the entry cache to 200Mb as the 1K groups of 2K users require at least 170Mb
+ be_groups.replace('nsslapd-cachememsize', groups_entrycache)
+ except:
+ raise
+
+ # Step 2. Generate a test ldif (10k users entries)
+ log.info("Generating users LDIF...")
+ ldif_dir = inst.get_ldif_dir()
+ users_import_ldif = "%s/%s" % (ldif_dir, users_ldif)
+ groups_import_ldif = "%s/%s" % (ldif_dir, groups_ldif)
+ dbgen_users(inst, users_number, users_import_ldif, suffix=users_suffix, generic=True, parent=users_suffix)
+
+ # Generate a test ldif (800 groups with 10k members) that fit in 700Mb entry cache
+ props = {
+ "name": "group",
+ "suffix": groups_suffix,
+ "parent": groups_suffix,
+ "number": groups_number,
+ "numMembers": users_number,
+ "createMembers": False,
+ "memberParent": users_suffix,
+ "membershipAttr": "uniquemember",
+ }
+ dbgen_groups(inst, groups_import_ldif, props)
+
+ # Step 3. Do the both offline imports
+ inst.stop()
+ if not inst.ldif2db(users_backend, None, None, None, users_import_ldif):
+ log.fatal('test_basic_import_export: Offline users import failed')
+ assert False
+ if not inst.ldif2db(groups_backend, None, None, None, groups_import_ldif):
+ log.fatal('test_basic_import_export: Offline groups import failed')
+ assert False
+ inst.start()
+
+ # Step 4. first prime the cache
+ # Just request the 'DN'. We are interested by the time of matching not by the time of transfert
+ entries = topology_st.standalone.search_s(groups_suffix, ldap.SCOPE_SUBTREE, "(&(objectclass=groupOfUniqueNames)(uniquemember=uid=%s,%s))" % (priming_user_rdn, users_suffix), ['dn'])
+ assert len(entries) == groups_number
+
+ # Step 5. Now do the real performance checking it should take less than a second
+ # Just request the 'DN'. We are interested by the time of matching not by the time of transfert
+ search_start = time.time()
+ entries = topology_st.standalone.search_s(groups_suffix, ldap.SCOPE_SUBTREE, "(&(objectclass=groupOfUniqueNames)(uniquemember=uid=%s,%s))" % (perf_user_rdn, users_suffix), ['dn'])
+ duration = time.time() - search_start
+ log.info("Duration of the search was %f", duration)
+
+ # Step 6. Gather the etime from the access log
+ inst.stop()
+ access_log = DirsrvAccessLog(inst)
+ search_result = access_log.match(".*RESULT err=0 tag=101 nentries=%s.*" % groups_number)
+ log.info("Found patterns are %s", search_result[0])
+ log.info("Found patterns are %s", search_result[1])
+ etime = float(search_result[1].split('etime=')[1])
+ log.info("Duration of the search from access log was %f", etime)
+ assert len(entries) == groups_number
+ assert (etime < 1)
+
if __name__ == '__main__':
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/filterentry.c b/ldap/servers/slapd/filterentry.c
index fd8fdda9f..cae5c7edc 100644
--- a/ldap/servers/slapd/filterentry.c
+++ b/ldap/servers/slapd/filterentry.c
@@ -296,7 +296,27 @@ test_ava_filter(
rc = -1;
for (; a != NULL; a = a->a_next) {
if (slapi_attr_type_cmp(ava->ava_type, a->a_type, SLAPI_TYPE_CMP_SUBTYPE) == 0) {
- rc = plugin_call_syntax_filter_ava(a, ftype, ava);
+ if ((ftype == LDAP_FILTER_EQUALITY) &&
+ (slapi_attr_is_dn_syntax_type(a->a_type))) {
+ /* This path is for a performance improvement */
+
+ /* In case of equality filter we can get benefit of the
+ * sorted valuearray (from valueset).
+ * This improvement is limited to DN syntax attributes for
+ * which the sorted valueset was designed.
+ */
+ Slapi_Value *sval = NULL;
+ sval = slapi_value_new_berval(&ava->ava_value);
+ if (slapi_valueset_find((const Slapi_Attr *)a, &a->a_present_values, sval)) {
+ rc = 0;
+ }
+ slapi_value_free(&sval);
+ } else {
+ /* When sorted valuearray optimization cannot be used
+ * lets filter the value according to its syntax
+ */
+ rc = plugin_call_syntax_filter_ava(a, ftype, ava);
+ }
if (rc == 0) {
break;
}
--
2.46.0

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
From 850cb2ae142b1dc31081387e8e997699668e70f4 Mon Sep 17 00:00:00 2001
From 7a45b50bf3d9cfc8f7e805379fcb39d5669d6c8b Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Thu, 5 Feb 2026 12:17:06 +0100
Subject: [PATCH] Issue 7223 - Revert index scan limits for system indexes
@ -9,28 +9,28 @@ c6f458b42 Issue 7189 - DSBLE0007 generates incorrect remediation commands for sc
Relates: https://github.com/389ds/389-ds-base/issues/7223
Reviewed by: @progier389, @tbordaz (Thanks!)
Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
---
.../tests/suites/config/config_test.py | 27 +---
.../healthcheck/health_system_indexes_test.py | 135 +-----------------
.../paged_results/paged_results_test.py | 25 +---
ldap/servers/slapd/back-ldbm/back-ldbm.h | 1 -
ldap/servers/slapd/back-ldbm/index.c | 2 -
ldap/servers/slapd/back-ldbm/instance.c | 108 ++------------
ldap/servers/slapd/back-ldbm/instance.c | 106 +++-----------
ldap/servers/slapd/back-ldbm/ldbm_config.c | 30 ----
ldap/servers/slapd/back-ldbm/ldbm_config.h | 1 -
.../slapd/back-ldbm/ldbm_index_config.c | 8 --
src/lib389/lib389/backend.py | 50 ++-----
src/lib389/lib389/cli_conf/backend.py | 20 ---
11 files changed, 39 insertions(+), 368 deletions(-)
11 files changed, 41 insertions(+), 364 deletions(-)
diff --git a/dirsrvtests/tests/suites/config/config_test.py b/dirsrvtests/tests/suites/config/config_test.py
index 430176602..bbe13d248 100644
index b9ba684d9..6f11e7fc8 100644
--- a/dirsrvtests/tests/suites/config/config_test.py
+++ b/dirsrvtests/tests/suites/config/config_test.py
@@ -514,19 +514,17 @@ def test_ndn_cache_enabled(topo):
topo.standalone.config.set('nsslapd-ndn-cache-max-size', 'invalid_value')
@@ -715,19 +715,17 @@ def test_ndn_cache_size_enforcement(topo, request):
request.addfinalizer(fin)
-def test_require_index(topo, request):
+def test_require_index(topo):
@ -50,7 +50,7 @@ index 430176602..bbe13d248 100644
"""
# Set the config
@@ -537,10 +535,6 @@ def test_require_index(topo, request):
@@ -738,10 +736,6 @@ def test_require_index(topo, request):
db_cfg = DatabaseConfig(topo.standalone)
db_cfg.set([('nsslapd-idlistscanlimit', '100')])
@ -61,7 +61,7 @@ index 430176602..bbe13d248 100644
users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
for i in range(101):
@@ -551,15 +545,10 @@ def test_require_index(topo, request):
@@ -752,15 +746,10 @@ def test_require_index(topo, request):
with pytest.raises(ldap.UNWILLING_TO_PERFORM):
raw_objects.filter("(description=test*)")
@ -78,7 +78,7 @@ index 430176602..bbe13d248 100644
"""Ensure internal operations require indexed attributes
:id: 22b94f30-59e3-4f27-89a1-c4f4be036f7f
@@ -591,10 +580,6 @@ def test_require_internal_index(topo, request):
@@ -791,10 +780,6 @@ def test_require_internal_index(topo, request):
# Create a bunch of users
db_cfg = DatabaseConfig(topo.standalone)
db_cfg.set([('nsslapd-idlistscanlimit', '100')])
@ -89,7 +89,7 @@ index 430176602..bbe13d248 100644
users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
for i in range(102, 202):
users.create_test_user(uid=i)
@@ -619,12 +604,6 @@ def test_require_internal_index(topo, request):
@@ -819,12 +804,6 @@ def test_require_internal_index(topo, request):
with pytest.raises(ldap.UNWILLING_TO_PERFORM):
user.delete()
@ -100,13 +100,13 @@ index 430176602..bbe13d248 100644
-
-
if __name__ == '__main__':
# Run isolated
def get_pstack(pid):
"""Get a pstack of the pid."""
diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
index f25de4214..842f7e8dd 100644
index 5eadf6283..61972d60c 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -172,8 +172,7 @@ def test_missing_parentid(topology_st, log_buffering_enabled):
@@ -171,8 +171,7 @@ def test_missing_parentid(topology_st, log_buffering_enabled):
log.info("Re-add the parentId index")
backend = Backends(standalone).get("userRoot")
@ -116,7 +116,7 @@ index f25de4214..842f7e8dd 100644
run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
@@ -261,8 +260,7 @@ def test_usn_plugin_missing_entryusn(topology_st, usn_plugin_enabled, log_buffer
@@ -260,8 +259,7 @@ def test_usn_plugin_missing_entryusn(topology_st, usn_plugin_enabled, log_buffer
log.info("Re-add the entryusn index")
backend = Backends(standalone).get("userRoot")
@ -126,7 +126,7 @@ index f25de4214..842f7e8dd 100644
run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
@@ -406,132 +404,6 @@ def test_retrocl_plugin_missing_matching_rule(topology_st, retrocl_plugin_enable
@@ -405,132 +403,6 @@ def test_retrocl_plugin_missing_matching_rule(topology_st, retrocl_plugin_enable
run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
@ -259,7 +259,7 @@ index f25de4214..842f7e8dd 100644
def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
"""Check if healthcheck returns DSBLE0007 code when multiple system indexes are missing
@@ -572,8 +444,7 @@ def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
@@ -571,8 +443,7 @@ def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
log.info("Re-add the missing system indexes")
backend = Backends(standalone).get("userRoot")
@ -270,10 +270,10 @@ index f25de4214..842f7e8dd 100644
run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
diff --git a/dirsrvtests/tests/suites/paged_results/paged_results_test.py b/dirsrvtests/tests/suites/paged_results/paged_results_test.py
index 8835be8fa..1ed11c891 100644
index 61d6702da..1bb94b53a 100644
--- a/dirsrvtests/tests/suites/paged_results/paged_results_test.py
+++ b/dirsrvtests/tests/suites/paged_results/paged_results_test.py
@@ -317,19 +317,19 @@ def test_search_success(topology_st, create_user, page_size, users_num):
@@ -306,19 +306,19 @@ def test_search_success(topology_st, create_user, page_size, users_num):
del_users(users_list)
@ -300,7 +300,7 @@ index 8835be8fa..1ed11c891 100644
"""Verify that search with a simple paged results control
throws expected exceptoins when corresponding limits are
exceeded.
@@ -351,15 +351,6 @@ def test_search_limits_fail(topology_st, create_user, page_size, users_num,
@@ -341,15 +341,6 @@ def test_search_limits_fail(topology_st, create_user, page_size, users_num,
users_list = add_users(topology_st, users_num, DEFAULT_SUFFIX)
attr_value_bck = change_conf_attr(topology_st, suffix, attr_name, attr_value)
@ -316,7 +316,7 @@ index 8835be8fa..1ed11c891 100644
conf_param_dict = {attr_name: attr_value}
search_flt = r'(uid=test*)'
searchreq_attrlist = ['dn', 'sn']
@@ -412,8 +403,6 @@ def test_search_limits_fail(topology_st, create_user, page_size, users_num,
@@ -402,8 +393,6 @@ def test_search_limits_fail(topology_st, create_user, page_size, users_num,
else:
break
finally:
@ -326,10 +326,10 @@ index 8835be8fa..1ed11c891 100644
change_conf_attr(topology_st, suffix, attr_name, attr_value_bck)
diff --git a/ldap/servers/slapd/back-ldbm/back-ldbm.h b/ldap/servers/slapd/back-ldbm/back-ldbm.h
index cde30cedd..d17ec644b 100644
index 5e4988782..a4993208c 100644
--- a/ldap/servers/slapd/back-ldbm/back-ldbm.h
+++ b/ldap/servers/slapd/back-ldbm/back-ldbm.h
@@ -554,7 +554,6 @@ struct ldbminfo
@@ -561,7 +561,6 @@ struct ldbminfo
int li_mode;
int li_lookthroughlimit;
int li_allidsthreshold;
@ -338,10 +338,10 @@ index cde30cedd..d17ec644b 100644
int li_reslimit_lookthrough_handle;
uint64_t li_dbcachesize;
diff --git a/ldap/servers/slapd/back-ldbm/index.c b/ldap/servers/slapd/back-ldbm/index.c
index 63f0196c1..30fa09ebb 100644
index e06a8ee5f..90129b682 100644
--- a/ldap/servers/slapd/back-ldbm/index.c
+++ b/ldap/servers/slapd/back-ldbm/index.c
@@ -999,8 +999,6 @@ index_read_ext_allids(
@@ -1007,8 +1007,6 @@ index_read_ext_allids(
}
if (pb) {
slapi_pblock_get(pb, SLAPI_SEARCH_IS_AND, &is_and);
@ -351,7 +351,7 @@ index 63f0196c1..30fa09ebb 100644
ai_flags = is_and ? INDEX_ALLIDS_FLAG_AND : 0;
/* the caller can pass in a value of 0 - just ignore those - but if the index
diff --git a/ldap/servers/slapd/back-ldbm/instance.c b/ldap/servers/slapd/back-ldbm/instance.c
index 29643f7e4..6098e04fc 100644
index 23bd70243..f9a546661 100644
--- a/ldap/servers/slapd/back-ldbm/instance.c
+++ b/ldap/servers/slapd/back-ldbm/instance.c
@@ -16,7 +16,7 @@
@ -445,7 +445,7 @@ index 29643f7e4..6098e04fc 100644
/*
* Always index (entrydn or entryrdn), parentid, objectclass,
@@ -247,59 +190,47 @@ ldbm_instance_create_default_indexes(backend *be)
@@ -247,53 +190,47 @@ ldbm_instance_create_default_indexes(backend *be)
* ACL routines.
*/
if (entryrdn_get_switch()) { /* subtree-rename: on */
@ -469,28 +469,24 @@ index 29643f7e4..6098e04fc 100644
- attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
- slapi_entry_free(e);
- }
-
- e = ldbm_instance_init_config_entry("objectclass", "eq", 0, 0, 0, 0, 0);
+ e = ldbm_instance_init_config_entry(LDBM_PARENTID_STR, "eq", 0, 0, 0, "integerOrderingMatch");
ldbm_instance_config_add_index_entry(inst, e, flags);
slapi_entry_free(e);
+ ldbm_instance_config_add_index_entry(inst, e, flags);
+ slapi_entry_free(e);
- e = ldbm_instance_init_config_entry("aci", "pres", 0, 0, 0, 0, 0);
- e = ldbm_instance_init_config_entry("objectclass", "eq", 0, 0, 0, 0, 0);
+ e = ldbm_instance_init_config_entry("objectclass", "eq", 0, 0, 0, 0);
ldbm_instance_config_add_index_entry(inst, e, flags);
slapi_entry_free(e);
- e = ldbm_instance_init_config_entry(LDBM_NUMSUBORDINATES_STR, "pres", 0, 0, 0, 0, 0);
- e = ldbm_instance_init_config_entry("aci", "pres", 0, 0, 0, 0, 0);
+ e = ldbm_instance_init_config_entry("aci", "pres", 0, 0, 0, 0);
ldbm_instance_config_add_index_entry(inst, e, flags);
slapi_entry_free(e);
-#if 0 /* don't need copiedfrom */
- e = ldbm_instance_init_config_entry("copiedfrom","pres",0 ,0);
- e = ldbm_instance_init_config_entry(LDBM_NUMSUBORDINATES_STR, "pres", 0, 0, 0, 0, 0);
+ e = ldbm_instance_init_config_entry(LDBM_NUMSUBORDINATES_STR, "pres", 0, 0, 0, 0);
ldbm_instance_config_add_index_entry(inst, e, flags);
slapi_entry_free(e);
-#endif
- e = ldbm_instance_init_config_entry(SLAPI_ATTR_UNIQUEID, "eq", 0, 0, 0, 0, 0);
+ e = ldbm_instance_init_config_entry(SLAPI_ATTR_UNIQUEID, "eq", 0, 0, 0, 0);
@ -515,7 +511,7 @@ index 29643f7e4..6098e04fc 100644
attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
slapi_entry_free(e);
@@ -308,20 +239,11 @@ ldbm_instance_create_default_indexes(backend *be)
@@ -302,20 +239,11 @@ ldbm_instance_create_default_indexes(backend *be)
* ancestorid is special, there is actually no such attr type
* but we still want to use the attr index file APIs.
*/
@ -540,10 +536,10 @@ index 29643f7e4..6098e04fc 100644
}
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_config.c b/ldap/servers/slapd/back-ldbm/ldbm_config.c
index f8d8f7474..b7bceabf2 100644
index d8ffc4479..5cce0dbd3 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_config.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_config.c
@@ -366,35 +366,6 @@ ldbm_config_allidsthreshold_set(void *arg, void *value, char *errorbuf __attribu
@@ -386,35 +386,6 @@ ldbm_config_allidsthreshold_set(void *arg, void *value, char *errorbuf __attribu
return retval;
}
@ -579,7 +575,7 @@ index f8d8f7474..b7bceabf2 100644
static void *
ldbm_config_pagedallidsthreshold_get(void *arg)
{
@@ -974,7 +945,6 @@ static config_info ldbm_config[] = {
@@ -1133,7 +1104,6 @@ static config_info ldbm_config[] = {
{CONFIG_LOOKTHROUGHLIMIT, CONFIG_TYPE_INT, "5000", &ldbm_config_lookthroughlimit_get, &ldbm_config_lookthroughlimit_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
{CONFIG_MODE, CONFIG_TYPE_INT_OCTAL, "0600", &ldbm_config_mode_get, &ldbm_config_mode_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
{CONFIG_IDLISTSCANLIMIT, CONFIG_TYPE_INT, "2147483646", &ldbm_config_allidsthreshold_get, &ldbm_config_allidsthreshold_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
@ -588,7 +584,7 @@ index f8d8f7474..b7bceabf2 100644
{CONFIG_MAXPASSBEFOREMERGE, CONFIG_TYPE_INT, "100", &ldbm_config_maxpassbeforemerge_get, &ldbm_config_maxpassbeforemerge_set, 0},
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_config.h b/ldap/servers/slapd/back-ldbm/ldbm_config.h
index 004e5ea7e..48446193e 100644
index 30e3477ed..e1f05d7a2 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_config.h
+++ b/ldap/servers/slapd/back-ldbm/ldbm_config.h
@@ -60,7 +60,6 @@ struct config_info
@ -619,10 +615,10 @@ index bae2a64b9..38e7368e1 100644
if (eBuf) {
PR_smprintf_free(eBuf);
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
index 3cea0df36..4babf6850 100644
index d3c9ccf35..1d9be4683 100644
--- a/src/lib389/lib389/backend.py
+++ b/src/lib389/lib389/backend.py
@@ -539,10 +539,11 @@ class Backend(DSLdapObject):
@@ -615,10 +615,11 @@ class Backend(DSLdapObject):
indexes = self.get_indexes()
# Default system indexes taken from ldap/servers/slapd/back-ldbm/instance.c
@ -637,7 +633,7 @@ index 3cea0df36..4babf6850 100644
'objectClass': {'types': ['eq'], 'matching_rule': None},
'aci': {'types': ['pres'], 'matching_rule': None},
'nscpEntryDN': {'types': ['eq'], 'matching_rule': None},
@@ -599,17 +600,14 @@ class Backend(DSLdapObject):
@@ -675,17 +676,14 @@ class Backend(DSLdapObject):
# Generate remediation command
index_types = ' '.join([f"--index-type {t}" for t in expected_config['types']])
cmd = f"dsconf YOUR_INSTANCE backend index add {bename} --attr {attr_name} {index_types}"
@ -656,7 +652,7 @@ index 3cea0df36..4babf6850 100644
# Normalize to lowercase for comparison
actual_types = [t.lower() for t in actual_types]
@@ -624,31 +622,16 @@ class Backend(DSLdapObject):
@@ -700,31 +698,16 @@ class Backend(DSLdapObject):
remediation_commands.append(cmd)
reindex_attrs.add(attr_name)
@ -693,7 +689,7 @@ index 3cea0df36..4babf6850 100644
except Exception as e:
self._log.debug(f"_lint_system_indexes - Error checking index {attr_name}: {e}")
@@ -879,13 +862,12 @@ class Backend(DSLdapObject):
@@ -963,13 +946,12 @@ class Backend(DSLdapObject):
return
raise ValueError("Can not delete index because it does not exist")
@ -708,7 +704,7 @@ index 3cea0df36..4babf6850 100644
:param reindex - If set to True then index the attribute after creating it.
"""
@@ -915,15 +897,6 @@ class Backend(DSLdapObject):
@@ -999,15 +981,6 @@ class Backend(DSLdapObject):
# Only add if there are actually rules present in the list.
if len(mrs) > 0:
props['nsMatchingRule'] = mrs
@ -724,7 +720,7 @@ index 3cea0df36..4babf6850 100644
new_index.create(properties=props, basedn="cn=index," + self._dn)
if reindex:
@@ -1230,7 +1203,6 @@ class DatabaseConfig(DSLdapObject):
@@ -1314,7 +1287,6 @@ class DatabaseConfig(DSLdapObject):
'nsslapd-lookthroughlimit',
'nsslapd-mode',
'nsslapd-idlistscanlimit',
@ -733,7 +729,7 @@ index 3cea0df36..4babf6850 100644
'nsslapd-import-cachesize',
'nsslapd-idl-switch',
diff --git a/src/lib389/lib389/cli_conf/backend.py b/src/lib389/lib389/cli_conf/backend.py
index d57cb9433..4dc67d563 100644
index dcf57d006..80008d22d 100644
--- a/src/lib389/lib389/cli_conf/backend.py
+++ b/src/lib389/lib389/cli_conf/backend.py
@@ -39,7 +39,6 @@ arg_to_attr = {
@ -744,7 +740,7 @@ index d57cb9433..4dc67d563 100644
'directory': 'nsslapd-directory',
'dbcachesize': 'nsslapd-dbcachesize',
'logdirectory': 'nsslapd-db-logdirectory',
@@ -588,21 +587,6 @@ def backend_set_index(inst, basedn, log, args):
@@ -626,21 +625,6 @@ def backend_set_index(inst, basedn, log, args):
except ldap.NO_SUCH_ATTRIBUTE:
raise ValueError('Can not delete matching rule type because it does not exist')
@ -766,7 +762,7 @@ index d57cb9433..4dc67d563 100644
if args.reindex:
be.reindex(attrs=[args.attr])
log.info("Index successfully updated")
@@ -924,9 +908,6 @@ def create_parser(subparsers):
@@ -962,9 +946,6 @@ def create_parser(subparsers):
edit_index_parser.add_argument('--del-type', action='append', help='Removes an index type from the index: (eq, sub, pres, or approx)')
edit_index_parser.add_argument('--add-mr', action='append', help='Adds a matching-rule to the index')
edit_index_parser.add_argument('--del-mr', action='append', help='Removes a matching-rule from the index')
@ -776,7 +772,7 @@ index d57cb9433..4dc67d563 100644
edit_index_parser.add_argument('--reindex', action='store_true', help='Re-indexes the database after editing the index')
edit_index_parser.add_argument('be_name', help='The backend name or suffix')
@@ -1053,7 +1034,6 @@ def create_parser(subparsers):
@@ -1091,7 +1072,6 @@ def create_parser(subparsers):
'will check when examining candidate entries in response to a search request')
set_db_config_parser.add_argument('--mode', help='Specifies the permissions used for newly created index files')
set_db_config_parser.add_argument('--idlistscanlimit', help='Specifies the number of entry IDs that are searched during a search operation')

View File

@ -1,163 +0,0 @@
From 57051154bafaf50b83fc27dadbd89a49fd1c8c36 Mon Sep 17 00:00:00 2001
From: Pierre Rogier <progier@redhat.com>
Date: Fri, 14 Jun 2024 13:27:10 +0200
Subject: [PATCH] Security fix for CVE-2024-5953
Description:
A denial of service vulnerability was found in the 389 Directory Server.
This issue may allow an authenticated user to cause a server denial
of service while attempting to log in with a user with a malformed hash
in their password.
Fix Description:
To prevent buffer overflow when a bind request is processed, the bind fails
if the hash size is not coherent without even attempting to process further
the hashed password.
References:
- https://nvd.nist.gov/vuln/detail/CVE-2024-5953
- https://access.redhat.com/security/cve/CVE-2024-5953
- https://bugzilla.redhat.com/show_bug.cgi?id=2292104
---
.../tests/suites/password/regression_test.py | 54 ++++++++++++++++++-
ldap/servers/plugins/pwdstorage/md5_pwd.c | 9 +++-
ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c | 6 +++
3 files changed, 66 insertions(+), 3 deletions(-)
diff --git a/dirsrvtests/tests/suites/password/regression_test.py b/dirsrvtests/tests/suites/password/regression_test.py
index 8f1facb6d..1fa581643 100644
--- a/dirsrvtests/tests/suites/password/regression_test.py
+++ b/dirsrvtests/tests/suites/password/regression_test.py
@@ -7,12 +7,14 @@
#
import pytest
import time
+import glob
+import base64
from lib389._constants import PASSWORD, DN_DM, DEFAULT_SUFFIX
from lib389._constants import SUFFIX, PASSWORD, DN_DM, DN_CONFIG, PLUGIN_RETRO_CHANGELOG, DEFAULT_SUFFIX, DEFAULT_CHANGELOG_DB
from lib389 import Entry
from lib389.topologies import topology_m1 as topo_supplier
-from lib389.idm.user import UserAccounts
-from lib389.utils import ldap, os, logging, ensure_bytes, ds_is_newer
+from lib389.idm.user import UserAccounts, UserAccount
+from lib389.utils import ldap, os, logging, ensure_bytes, ds_is_newer, ds_supports_new_changelog
from lib389.topologies import topology_st as topo
from lib389.idm.organizationalunit import OrganizationalUnits
@@ -39,6 +41,13 @@ TEST_PASSWORDS += ['CNpwtest1ZZZZ', 'ZZZZZCNpwtest1',
TEST_PASSWORDS2 = (
'CN12pwtest31', 'SN3pwtest231', 'UID1pwtest123', 'MAIL2pwtest12@redhat.com', '2GN1pwtest123', 'People123')
+SUPPORTED_SCHEMES = (
+ "{SHA}", "{SSHA}", "{SHA256}", "{SSHA256}",
+ "{SHA384}", "{SSHA384}", "{SHA512}", "{SSHA512}",
+ "{crypt}", "{NS-MTA-MD5}", "{clear}", "{MD5}",
+ "{SMD5}", "{PBKDF2_SHA256}", "{PBKDF2_SHA512}",
+ "{GOST_YESCRYPT}", "{PBKDF2-SHA256}", "{PBKDF2-SHA512}" )
+
def _check_unhashed_userpw(inst, user_dn, is_present=False):
"""Check if unhashed#user#password attribute is present or not in the changelog"""
unhashed_pwd_attribute = 'unhashed#user#password'
@@ -319,6 +328,47 @@ def test_unhashed_pw_switch(topo_supplier):
# Add debugging steps(if any)...
pass
+@pytest.mark.parametrize("scheme", SUPPORTED_SCHEMES )
+def test_long_hashed_password(topo, create_user, scheme):
+ """Check that hashed password with very long value does not cause trouble
+
+ :id: 252a1f76-114b-11ef-8a7a-482ae39447e5
+ :setup: standalone Instance
+ :parametrized: yes
+ :steps:
+ 1. Add a test user user
+ 2. Set a long password with requested scheme
+ 3. Bind on that user using a wrong password
+ 4. Check that instance is still alive
+ 5. Remove the added user
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Should get ldap.INVALID_CREDENTIALS exception
+ 4. Success
+ 5. Success
+ """
+ inst = topo.standalone
+ inst.simple_bind_s(DN_DM, PASSWORD)
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ # Make sure that server is started as this test may crash it
+ inst.start()
+ # Adding Test user (It may already exists if previous test failed)
+ user2 = UserAccount(inst, dn='uid=test_user_1002,ou=People,dc=example,dc=com')
+ if not user2.exists():
+ user2 = users.create_test_user(uid=1002, gid=2002)
+ # Setting hashed password
+ passwd = 'A'*4000
+ hashed_passwd = scheme.encode('utf-8') + base64.b64encode(passwd.encode('utf-8'))
+ user2.replace('userpassword', hashed_passwd)
+ # Bind on that user using a wrong password
+ with pytest.raises(ldap.INVALID_CREDENTIALS):
+ conn = user2.bind(PASSWORD)
+ # Check that instance is still alive
+ assert inst.status()
+ # Remove the added user
+ user2.delete()
+
if __name__ == '__main__':
# Run isolated
diff --git a/ldap/servers/plugins/pwdstorage/md5_pwd.c b/ldap/servers/plugins/pwdstorage/md5_pwd.c
index 1e2cf58e7..b9a48d5ca 100644
--- a/ldap/servers/plugins/pwdstorage/md5_pwd.c
+++ b/ldap/servers/plugins/pwdstorage/md5_pwd.c
@@ -37,6 +37,7 @@ md5_pw_cmp(const char *userpwd, const char *dbpwd)
unsigned char hash_out[MD5_HASH_LEN];
unsigned char b2a_out[MD5_HASH_LEN * 2]; /* conservative */
SECItem binary_item;
+ size_t dbpwd_len = strlen(dbpwd);
ctx = PK11_CreateDigestContext(SEC_OID_MD5);
if (ctx == NULL) {
@@ -45,6 +46,12 @@ md5_pw_cmp(const char *userpwd, const char *dbpwd)
goto loser;
}
+ if (dbpwd_len >= sizeof b2a_out) {
+ slapi_log_err(SLAPI_LOG_PLUGIN, MD5_SUBSYSTEM_NAME,
+ "The hashed password stored in the user entry is longer than any valid md5 hash");
+ goto loser;
+ }
+
/* create the hash */
PK11_DigestBegin(ctx);
PK11_DigestOp(ctx, (const unsigned char *)userpwd, strlen(userpwd));
@@ -57,7 +64,7 @@ md5_pw_cmp(const char *userpwd, const char *dbpwd)
bver = NSSBase64_EncodeItem(NULL, (char *)b2a_out, sizeof b2a_out, &binary_item);
/* bver points to b2a_out upon success */
if (bver) {
- rc = slapi_ct_memcmp(bver, dbpwd, strlen(dbpwd));
+ rc = slapi_ct_memcmp(bver, dbpwd, dbpwd_len);
} else {
slapi_log_err(SLAPI_LOG_PLUGIN, MD5_SUBSYSTEM_NAME,
"Could not base64 encode hashed value for password compare");
diff --git a/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c b/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c
index dcac4fcdd..82b8c9501 100644
--- a/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c
+++ b/ldap/servers/plugins/pwdstorage/pbkdf2_pwd.c
@@ -255,6 +255,12 @@ pbkdf2_sha256_pw_cmp(const char *userpwd, const char *dbpwd)
passItem.data = (unsigned char *)userpwd;
passItem.len = strlen(userpwd);
+ if (pwdstorage_base64_decode_len(dbpwd, dbpwd_len) > sizeof dbhash) {
+ /* Hashed value is too long and cannot match any value generated by pbkdf2_sha256_hash */
+ slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Unable to base64 decode dbpwd value. (hashed value is too long)\n");
+ return result;
+ }
+
/* Decode the DBpwd to bytes from b64 */
if (PL_Base64Decode(dbpwd, dbpwd_len, dbhash) == NULL) {
slapi_log_err(SLAPI_LOG_ERR, (char *)schemeName, "Unable to base64 decode dbpwd value\n");
--
2.46.0

View File

@ -1,178 +0,0 @@
From e8a5b1deef1b455aafecb71efc029d2407b1b06f Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Tue, 16 Jul 2024 08:32:21 -0700
Subject: [PATCH] Issue 4778 - Add COMPACT_CL5 task to dsconf replication
(#6260)
Description: In 1.4.3, the changelog is not part of a backend.
It can be compacted with nsds5task: CAMPACT_CL5 as part of the replication entry.
Add the task as a compact-changelog command under the dsconf replication tool.
Add tests for the feature and fix old tests.
Related: https://github.com/389ds/389-ds-base/issues/4778
Reviewed by: @progier389 (Thanks!)
---
.../tests/suites/config/compact_test.py | 36 ++++++++++++++---
src/lib389/lib389/cli_conf/replication.py | 10 +++++
src/lib389/lib389/replica.py | 40 +++++++++++++++++++
3 files changed, 81 insertions(+), 5 deletions(-)
diff --git a/dirsrvtests/tests/suites/config/compact_test.py b/dirsrvtests/tests/suites/config/compact_test.py
index 317258d0e..31d98d10c 100644
--- a/dirsrvtests/tests/suites/config/compact_test.py
+++ b/dirsrvtests/tests/suites/config/compact_test.py
@@ -13,14 +13,14 @@ import time
import datetime
from lib389.tasks import DBCompactTask
from lib389.backend import DatabaseConfig
-from lib389.replica import Changelog5
+from lib389.replica import Changelog5, Replicas
from lib389.topologies import topology_m1 as topo
log = logging.getLogger(__name__)
def test_compact_db_task(topo):
- """Specify a test case purpose or name here
+ """Test compaction of database
:id: 1b3222ef-a336-4259-be21-6a52f76e1859
:setup: Standalone Instance
@@ -48,7 +48,7 @@ def test_compact_db_task(topo):
def test_compaction_interval_and_time(topo):
- """Specify a test case purpose or name here
+ """Test compaction interval and time for database and changelog
:id: f361bee9-d7e7-4569-9255-d7b60dd9d92e
:setup: Supplier Instance
@@ -95,10 +95,36 @@ def test_compaction_interval_and_time(topo):
# Check compaction occurred as expected
time.sleep(45)
- assert not inst.searchErrorsLog("Compacting databases")
+ assert not inst.searchErrorsLog("compacting replication changelogs")
time.sleep(90)
- assert inst.searchErrorsLog("Compacting databases")
+ assert inst.searchErrorsLog("compacting replication changelogs")
+ inst.deleteErrorLogs(restart=False)
+
+
+def test_compact_cl5_task(topo):
+ """Test compaction of changelog5 database
+
+ :id: aadfa9f7-73c0-463a-912c-0a29aa1f8167
+ :setup: Standalone Instance
+ :steps:
+ 1. Run compaction task
+ 2. Check errors log to show task was run
+ :expectedresults:
+ 1. Success
+ 2. Success
+ """
+ inst = topo.ms["supplier1"]
+
+ replicas = Replicas(inst)
+ replicas.compact_changelog(log=log)
+
+ # Check compaction occurred as expected. But instead of time.sleep(5) check 1 sec in loop
+ for _ in range(5):
+ time.sleep(1)
+ if inst.searchErrorsLog("compacting replication changelogs"):
+ break
+ assert inst.searchErrorsLog("compacting replication changelogs")
inst.deleteErrorLogs(restart=False)
diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py
index 352c0ee5b..ccc394255 100644
--- a/src/lib389/lib389/cli_conf/replication.py
+++ b/src/lib389/lib389/cli_conf/replication.py
@@ -1199,6 +1199,11 @@ def restore_cl_dir(inst, basedn, log, args):
replicas.restore_changelog(replica_roots=args.REPLICA_ROOTS, log=log)
+def compact_cl5(inst, basedn, log, args):
+ replicas = Replicas(inst)
+ replicas.compact_changelog(replica_roots=args.REPLICA_ROOTS, log=log)
+
+
def create_parser(subparsers):
############################################
@@ -1326,6 +1331,11 @@ def create_parser(subparsers):
help="Specify one replica root whose changelog you want to restore. "
"The replica root will be consumed from the LDIF file name if the option is omitted.")
+ compact_cl = repl_subcommands.add_parser('compact-changelog', help='Compact the changelog database')
+ compact_cl.set_defaults(func=compact_cl5)
+ compact_cl.add_argument('REPLICA_ROOTS', nargs="+",
+ help="Specify replica roots whose changelog you want to compact.")
+
restore_changelogdir = restore_subcommands.add_parser('from-changelogdir', help='Restore LDIF files from changelogdir.')
restore_changelogdir.set_defaults(func=restore_cl_dir)
restore_changelogdir.add_argument('REPLICA_ROOTS', nargs="+",
diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py
index 94e1fdad5..1f321972d 100644
--- a/src/lib389/lib389/replica.py
+++ b/src/lib389/lib389/replica.py
@@ -1648,6 +1648,11 @@ class Replica(DSLdapObject):
"""
self.replace('nsds5task', 'ldif2cl')
+ def begin_task_compact_cl5(self):
+ """Begin COMPACT_CL5 task
+ """
+ self.replace('nsds5task', 'COMPACT_CL5')
+
def get_suffix(self):
"""Return the suffix
"""
@@ -1829,6 +1834,41 @@ class Replicas(DSLdapObjects):
log.error(f"Changelog LDIF for '{repl_root}' was not found")
continue
+ def compact_changelog(self, replica_roots=[], log=None):
+ """Compact Directory Server replication changelog
+
+ :param replica_roots: Replica suffixes that need to be processed (and optional LDIF file path)
+ :type replica_roots: list of str
+ :param log: The logger object
+ :type log: logger
+ """
+
+ if log is None:
+ log = self._log
+
+ # Check if the changelog entry exists
+ try:
+ cl = Changelog5(self._instance)
+ cl.get_attr_val_utf8_l("nsslapd-changelogdir")
+ except ldap.NO_SUCH_OBJECT:
+ raise ValueError("Changelog entry was not found. Probably, the replication is not enabled on this instance")
+
+ # Get all the replicas on the server if --replica-roots option is not specified
+ repl_roots = []
+ if not replica_roots:
+ for replica in self.list():
+ repl_roots.append(replica.get_attr_val_utf8("nsDS5ReplicaRoot"))
+ else:
+ for repl_root in replica_roots:
+ repl_roots.append(repl_root)
+
+ # Dump the changelog for the replica
+
+ # Dump the changelog for the replica
+ for repl_root in repl_roots:
+ replica = self.get(repl_root)
+ replica.begin_task_compact_cl5()
+
class BootstrapReplicationManager(DSLdapObject):
"""A Replication Manager credential for bootstrapping the repl process.
--
2.47.0

View File

@ -1,4 +1,4 @@
From c6e2911dca08aac40e98999b48019eac72cc74a6 Mon Sep 17 00:00:00 2001
From a58212943da2604091e7d5a20829ab54970d41c9 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Thu, 5 Feb 2026 12:17:06 +0100
Subject: [PATCH] Issue 7223 - Add upgrade function to remove
@ -15,17 +15,17 @@ automatically on server startup and removes the attribute if found.
Relates: https://github.com/389ds/389-ds-base/issues/7223
Reviewed by: @progier389, @tbordaz (Thanks!)
Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
---
.../healthcheck/health_system_indexes_test.py | 52 +++++
ldap/servers/slapd/upgrade.c | 210 ++++++++++++++++++
2 files changed, 262 insertions(+)
.../healthcheck/health_system_indexes_test.py | 52 +++++++++
ldap/servers/slapd/upgrade.c | 105 ++++++++++++++++++
2 files changed, 157 insertions(+)
diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
index 842f7e8dd..72a04fdab 100644
index 61972d60c..b0d7a99ec 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -451,6 +451,58 @@ def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
@@ -450,6 +450,58 @@ def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
@ -85,12 +85,12 @@ index 842f7e8dd..72a04fdab 100644
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
index 2f124afaf..074c15e3c 100644
index 43906c1af..adfec63de 100644
--- a/ldap/servers/slapd/upgrade.c
+++ b/ldap/servers/slapd/upgrade.c
@@ -22,8 +22,218 @@
* or altered.
*/
@@ -279,6 +279,107 @@ upgrade_205_fixup_repl_dep(void)
return UPGRADE_SUCCESS;
}
+/*
+ * Remove nsIndexIDListScanLimit from parentid index configuration.
@ -193,120 +193,20 @@ index 2f124afaf..074c15e3c 100644
+ return uresult;
+}
+
+/*
+ * Check if parentid indexes are missing the integerOrderingMatch
+ * matching rule.
+ *
+ * This function logs a warning if we detect this condition, advising
+ * the administrator to reindex the affected attributes.
+ */
+static upgrade_status
+upgrade_check_id_index_matching_rule(void)
+{
+ struct slapi_pblock *pb = slapi_pblock_new();
+ Slapi_Entry **backends = NULL;
+ const char *be_base_dn = "cn=ldbm database,cn=plugins,cn=config";
+ const char *be_filter = "(objectclass=nsBackendInstance)";
+ const char *attrs_to_check[] = {"parentid", "ancestorid", NULL};
+ upgrade_status uresult = UPGRADE_SUCCESS;
+
+ /* Search for all backend instances */
+ slapi_search_internal_set_pb(
+ pb, be_base_dn,
+ LDAP_SCOPE_ONELEVEL,
+ be_filter, NULL, 0, NULL, NULL,
+ plugin_get_default_component_id(), 0);
+ slapi_search_internal_pb(pb);
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &backends);
+
+ if (backends) {
+ for (size_t be_idx = 0; backends[be_idx] != NULL; be_idx++) {
+ const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
+ if (!be_name) {
+ continue;
+ }
+
+ /* Check each attribute that should have integerOrderingMatch */
+ for (size_t attr_idx = 0; attrs_to_check[attr_idx] != NULL; attr_idx++) {
+ const char *attr_name = attrs_to_check[attr_idx];
+ struct slapi_pblock *idx_pb = slapi_pblock_new();
+ Slapi_Entry **idx_entries = NULL;
+ char *idx_dn = slapi_create_dn_string("cn=%s,cn=index,cn=%s,%s",
+ attr_name, be_name, be_base_dn);
+ char *idx_filter = "(objectclass=nsIndex)";
+ PRBool has_matching_rule = PR_FALSE;
+
+ if (!idx_dn) {
+ slapi_pblock_destroy(idx_pb);
+ continue;
+ }
+
+ slapi_search_internal_set_pb(
+ idx_pb, idx_dn,
+ LDAP_SCOPE_BASE,
+ idx_filter, NULL, 0, NULL, NULL,
+ plugin_get_default_component_id(), 0);
+ slapi_search_internal_pb(idx_pb);
+ slapi_pblock_get(idx_pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &idx_entries);
+
+ if (idx_entries && idx_entries[0]) {
+ /* Index exists, check if it has integerOrderingMatch */
+ Slapi_Attr *mr_attr = NULL;
+ if (slapi_entry_attr_find(idx_entries[0], "nsMatchingRule", &mr_attr) == 0) {
+ Slapi_Value *sval = NULL;
+ int idx;
+ for (idx = slapi_attr_first_value(mr_attr, &sval);
+ idx != -1;
+ idx = slapi_attr_next_value(mr_attr, idx, &sval)) {
+ const struct berval *bval = slapi_value_get_berval(sval);
+ if (bval && bval->bv_val &&
+ strcasecmp(bval->bv_val, "integerOrderingMatch") == 0) {
+ has_matching_rule = PR_TRUE;
+ break;
+ }
+ }
+ }
+
+ if (!has_matching_rule) {
+ /* Index exists but doesn't have integerOrderingMatch, log a warning */
+ slapi_log_err(SLAPI_LOG_ERR, "upgrade_check_id_index_matching_rule",
+ "Index '%s' in backend '%s' is missing 'nsMatchingRule: integerOrderingMatch'. "
+ "Incorrectly configured system indexes can lead to poor search performance, replication issues, and other operational problems. "
+ "To fix this, add the matching rule and reindex: "
+ "dsconf <instance> backend index set --add-mr integerOrderingMatch --attr %s %s && "
+ "dsconf <instance> backend index reindex --attr %s %s. "
+ "WARNING: Reindexing can be resource-intensive and may impact server performance on a live system. "
+ "Consider scheduling reindexing during maintenance windows or periods of low activity.\n",
+ attr_name, be_name, attr_name, be_name, attr_name, be_name);
+ }
+ }
+
+ slapi_ch_free_string(&idx_dn);
+ slapi_free_search_results_internal(idx_pb);
+ slapi_pblock_destroy(idx_pb);
+ }
+ }
+ }
+
+ slapi_free_search_results_internal(pb);
+ slapi_pblock_destroy(pb);
+
+ return uresult;
+}
+
upgrade_status
upgrade_server(void)
{
/*
* Check if parentid/ancestorid indexes are missing the integerOrderingMatch
* matching rule.
@@ -407,6 +508,10 @@ upgrade_server(void)
return UPGRADE_FAILURE;
}
+ if (upgrade_remove_index_scanlimit() != UPGRADE_SUCCESS) {
+ return UPGRADE_FAILURE;
+ }
+
+ if (upgrade_check_id_index_matching_rule() != UPGRADE_SUCCESS) {
+ return UPGRADE_FAILURE;
+ }
+
return UPGRADE_SUCCESS;
}
if (upgrade_check_id_index_matching_rule() != UPGRADE_SUCCESS) {
return UPGRADE_FAILURE;
}
--
2.52.0

View File

@ -1,55 +0,0 @@
From d1cd9a5675e2953b7c8034ebb87a434cdd3ce0c3 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Mon, 2 Dec 2024 17:18:32 +0100
Subject: [PATCH] Issue 6417 - If an entry RDN is identical to the suffix, then
Entryrdn gets broken during a reindex (#6418)
Bug description:
During a reindex, the entryrdn index is built at the end from
each entry in the suffix.
If one entry has a RDN that is identical to the suffix DN,
then entryrdn_lookup_dn may erroneously return the suffix DN
as the DN of the entry.
Fix description:
When the lookup entry has no parent (because index is under
work) the loop lookup the entry using the RDN.
If this RDN matches the suffix DN, then it exits from the loop
with the suffix DN.
Before exiting it checks that the original lookup entryID
is equal to suffix entryID. If it does not match
the function fails and then the DN from the entry will be
built from id2enty
fixes: #6417
Reviewed by: Pierre Rogier, Simon Pichugin (Thanks !!!)
---
ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c b/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c
index 5797dd779..83b041192 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c
@@ -1224,7 +1224,16 @@ entryrdn_lookup_dn(backend *be,
}
goto bail;
}
- maybesuffix = 1;
+ if (workid == 1) {
+ /* The loop (workid) iterates from the starting 'id'
+ * up to the suffix ID (i.e. '1').
+ * A corner case (#6417) is if an entry, on the path
+ * 'id' -> suffix, has the same RDN than the suffix.
+ * In order to erroneously believe the loop hits the suffix
+ * we need to check that 'workid' is '1' (suffix)
+ */
+ maybesuffix = 1;
+ }
} else {
_entryrdn_cursor_print_error("entryrdn_lookup_dn",
key.data, data.size, data.ulen, rc);
--
2.48.0

View File

@ -1,6 +1,6 @@
From 1669e65b36d543fb69ef5503e5072ea37e9a0e19 Mon Sep 17 00:00:00 2001
From 9a9446b9bafe25eacf97039e66d6ef19d366315c Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Mon, 9 Feb 2026 14:12:18 +0100
Date: Thu, 5 Feb 2026 12:17:06 +0100
Subject: [PATCH] Issue 7223 - Add upgrade function to remove ancestorid index
config entry
@ -13,7 +13,7 @@ Also remove ancestorid index configuration from template-dse.ldif.
Relates: https://github.com/389ds/389-ds-base/issues/7223
Reviewed by: @progier389, @tbordaz (Thanks!)
Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
---
.../healthcheck/health_system_indexes_test.py | 85 +++++++++++
ldap/ldif/template-dse.ldif.in | 8 --
@ -21,10 +21,10 @@ Reviewed by: @progier389, @tbordaz (Thanks!)
3 files changed, 214 insertions(+), 12 deletions(-)
diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
index 72a04fdab..db5d13bf8 100644
index b0d7a99ec..4b0c58835 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -502,6 +502,91 @@ def test_upgrade_removes_parentid_scanlimit(topology_st):
@@ -501,6 +501,91 @@ def test_upgrade_removes_parentid_scanlimit(topology_st):
log.info("Upgrade successfully removed nsIndexIDListScanLimit from parentid index")
@ -117,10 +117,10 @@ index 72a04fdab..db5d13bf8 100644
if __name__ == "__main__":
# Run isolated
diff --git a/ldap/ldif/template-dse.ldif.in b/ldap/ldif/template-dse.ldif.in
index c2754adf8..2ddaf5fb3 100644
index 6f97d492b..70e7a85ac 100644
--- a/ldap/ldif/template-dse.ldif.in
+++ b/ldap/ldif/template-dse.ldif.in
@@ -973,14 +973,6 @@ cn: aci
@@ -990,14 +990,6 @@ cn: aci
nssystemindex: true
nsindextype: pres
@ -136,10 +136,10 @@ index c2754adf8..2ddaf5fb3 100644
objectclass: top
objectclass: nsIndex
diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
index 074c15e3c..aa51e72d2 100644
index adfec63de..d9156cae9 100644
--- a/ldap/servers/slapd/upgrade.c
+++ b/ldap/servers/slapd/upgrade.c
@@ -123,6 +123,126 @@ upgrade_remove_index_scanlimit(void)
@@ -380,6 +380,126 @@ upgrade_remove_index_scanlimit(void)
return uresult;
}
@ -264,9 +264,9 @@ index 074c15e3c..aa51e72d2 100644
+}
+
/*
* Check if parentid indexes are missing the integerOrderingMatch
* Check if parentid/ancestorid indexes are missing the integerOrderingMatch
* matching rule.
@@ -137,7 +257,7 @@ upgrade_check_id_index_matching_rule(void)
@@ -394,7 +514,7 @@ upgrade_check_id_index_matching_rule(void)
Slapi_Entry **backends = NULL;
const char *be_base_dn = "cn=ldbm database,cn=plugins,cn=config";
const char *be_filter = "(objectclass=nsBackendInstance)";
@ -275,7 +275,7 @@ index 074c15e3c..aa51e72d2 100644
upgrade_status uresult = UPGRADE_SUCCESS;
/* Search for all backend instances */
@@ -151,8 +271,9 @@ upgrade_check_id_index_matching_rule(void)
@@ -408,8 +528,9 @@ upgrade_check_id_index_matching_rule(void)
if (backends) {
for (size_t be_idx = 0; backends[be_idx] != NULL; be_idx++) {
@ -286,7 +286,7 @@ index 074c15e3c..aa51e72d2 100644
continue;
}
@@ -161,8 +282,8 @@ upgrade_check_id_index_matching_rule(void)
@@ -418,8 +539,8 @@ upgrade_check_id_index_matching_rule(void)
const char *attr_name = attrs_to_check[attr_idx];
struct slapi_pblock *idx_pb = slapi_pblock_new();
Slapi_Entry **idx_entries = NULL;
@ -297,7 +297,7 @@ index 074c15e3c..aa51e72d2 100644
char *idx_filter = "(objectclass=nsIndex)";
PRBool has_matching_rule = PR_FALSE;
@@ -231,6 +352,10 @@ upgrade_server(void)
@@ -512,6 +633,10 @@ upgrade_server(void)
return UPGRADE_FAILURE;
}

View File

@ -1,267 +0,0 @@
From 9b2fc77a36156ea987dcea6e2043f8e4c4a6b259 Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Tue, 18 Jun 2024 14:21:07 +0200
Subject: [PATCH] Issue 6224 - d2entry - Could not open id2entry err 0 - at
startup when having sub-suffixes (#6225)
Problem:: d2entry - Could not open id2entry err 0 is logged at startup when having sub-suffixes
Reason: The slapi_exist_referral internal search access a backend that is not yet started.
Solution: Limit the internal search to a single backend
Issue: #6224
Reviewed by: @droideck Thanks!
(cherry picked from commit 796f703021e961fdd8cbc53b4ad4e20258af0e96)
---
.../tests/suites/ds_logs/ds_logs_test.py | 1 +
.../suites/mapping_tree/regression_test.py | 161 +++++++++++++++++-
ldap/servers/slapd/backend.c | 7 +-
3 files changed, 159 insertions(+), 10 deletions(-)
diff --git a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
index 812936c62..84a9c6ec8 100644
--- a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
+++ b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
@@ -1222,6 +1222,7 @@ def test_referral_check(topology_st, request):
request.addfinalizer(fin)
+<<<<<<< HEAD
def test_referral_subsuffix(topology_st, request):
"""Test the results of an inverted parent suffix definition in the configuration.
diff --git a/dirsrvtests/tests/suites/mapping_tree/regression_test.py b/dirsrvtests/tests/suites/mapping_tree/regression_test.py
index 99d4a1d5f..689ff9f59 100644
--- a/dirsrvtests/tests/suites/mapping_tree/regression_test.py
+++ b/dirsrvtests/tests/suites/mapping_tree/regression_test.py
@@ -11,10 +11,14 @@ import ldap
import logging
import os
import pytest
+import time
from lib389.backend import Backends, Backend
+from lib389._constants import HOST_STANDALONE, PORT_STANDALONE, DN_DM, PW_DM
from lib389.dbgen import dbgen_users
from lib389.mappingTree import MappingTrees
from lib389.topologies import topology_st
+from lib389.referral import Referrals, Referral
+
try:
from lib389.backend import BackendSuffixView
@@ -31,14 +35,26 @@ else:
logging.getLogger(__name__).setLevel(logging.INFO)
log = logging.getLogger(__name__)
+PARENT_SUFFIX = "dc=parent"
+CHILD1_SUFFIX = f"dc=child1,{PARENT_SUFFIX}"
+CHILD2_SUFFIX = f"dc=child2,{PARENT_SUFFIX}"
+
+PARENT_REFERRAL_DN = f"cn=ref,ou=People,{PARENT_SUFFIX}"
+CHILD1_REFERRAL_DN = f"cn=ref,ou=people,{CHILD1_SUFFIX}"
+CHILD2_REFERRAL_DN = f"cn=ref,ou=people,{CHILD2_SUFFIX}"
+
+REFERRAL_CHECK_PEDIOD = 7
+
+
+
BESTRUCT = [
- { "bename" : "parent", "suffix": "dc=parent" },
- { "bename" : "child1", "suffix": "dc=child1,dc=parent" },
- { "bename" : "child2", "suffix": "dc=child2,dc=parent" },
+ { "bename" : "parent", "suffix": PARENT_SUFFIX },
+ { "bename" : "child1", "suffix": CHILD1_SUFFIX },
+ { "bename" : "child2", "suffix": CHILD2_SUFFIX },
]
-@pytest.fixture(scope="function")
+@pytest.fixture(scope="module")
def topo(topology_st, request):
bes = []
@@ -50,6 +66,9 @@ def topo(topology_st, request):
request.addfinalizer(fin)
inst = topology_st.standalone
+ # Reduce nsslapd-referral-check-period to accelerate test
+ topology_st.standalone.config.set("nsslapd-referral-check-period", str(REFERRAL_CHECK_PEDIOD))
+
ldif_files = {}
for d in BESTRUCT:
bename = d['bename']
@@ -76,14 +95,13 @@ def topo(topology_st, request):
inst.start()
return topology_st
-# Parameters for test_change_repl_passwd
-EXPECTED_ENTRIES = (("dc=parent", 39), ("dc=child1,dc=parent", 13), ("dc=child2,dc=parent", 13))
+# Parameters for test_sub_suffixes
@pytest.mark.parametrize(
"orphan_param",
[
- pytest.param( ( True, { "dc=parent": 2, "dc=child1,dc=parent":1, "dc=child2,dc=parent":1}), id="orphan-is-true" ),
- pytest.param( ( False, { "dc=parent": 3, "dc=child1,dc=parent":1, "dc=child2,dc=parent":1}), id="orphan-is-false" ),
- pytest.param( ( None, { "dc=parent": 3, "dc=child1,dc=parent":1, "dc=child2,dc=parent":1}), id="no-orphan" ),
+ pytest.param( ( True, { PARENT_SUFFIX: 2, CHILD1_SUFFIX:1, CHILD2_SUFFIX:1}), id="orphan-is-true" ),
+ pytest.param( ( False, { PARENT_SUFFIX: 3, CHILD1_SUFFIX:1, CHILD2_SUFFIX:1}), id="orphan-is-false" ),
+ pytest.param( ( None, { PARENT_SUFFIX: 3, CHILD1_SUFFIX:1, CHILD2_SUFFIX:1}), id="no-orphan" ),
],
)
@@ -128,3 +146,128 @@ def test_sub_suffixes(topo, orphan_param):
log.info('Test PASSED')
+def test_one_level_search_on_sub_suffixes(topo):
+ """ Perform one level scoped search accross suffix and sub-suffix
+
+ :id: 92f3139e-280e-11ef-a989-482ae39447e5
+ :feature: mapping-tree
+ :setup: Standalone instance with 3 additional backends:
+ dc=parent, dc=child1,dc=parent, dc=childr21,dc=parent
+ :steps:
+ 1. Perform a ONE LEVEL search on dc=parent
+ 2. Check that all expected entries have been returned
+ 3. Check that only the expected entries have been returned
+ :expectedresults:
+ 1. Success
+ 2. each expected dn should be in the result set
+ 3. Number of returned entries should be the same as the number of expected entries
+ """
+ expected_dns = ( 'dc=child1,dc=parent',
+ 'dc=child2,dc=parent',
+ 'ou=accounting,dc=parent',
+ 'ou=product development,dc=parent',
+ 'ou=product testing,dc=parent',
+ 'ou=human resources,dc=parent',
+ 'ou=payroll,dc=parent',
+ 'ou=people,dc=parent',
+ 'ou=groups,dc=parent', )
+ entries = topo.standalone.search_s("dc=parent", ldap.SCOPE_ONELEVEL, "(objectClass=*)",
+ attrlist=("dc","ou"), escapehatch='i am sure')
+ log.info(f'one level search on dc=parent returned the following entries: {entries}')
+ dns = [ entry.dn for entry in entries ]
+ for dn in expected_dns:
+ assert dn in dns
+ assert len(entries) == len(expected_dns)
+
+
+def test_sub_suffixes_errlog(topo):
+ """ check the entries found on suffix/sub-suffix
+ used int
+
+ :id: 1db9d52e-28de-11ef-b286-482ae39447e5
+ :feature: mapping-tree
+ :setup: Standalone instance with 3 additional backends:
+ dc=parent, dc=child1,dc=parent, dc=childr21,dc=parent
+ :steps:
+ 1. Check that id2entry error message is not in the error log.
+ :expectedresults:
+ 1. Success
+ """
+ inst = topo.standalone
+ assert not inst.searchErrorsLog('id2entry - Could not open id2entry err 0')
+
+
+# Parameters for test_referral_subsuffix:
+# a tuple pair containing:
+# - list of referral dn that must be created
+# - dict of searches basedn: expected_number_of_referrals
+@pytest.mark.parametrize(
+ "parameters",
+ [
+ pytest.param( ((PARENT_REFERRAL_DN, CHILD1_REFERRAL_DN), {PARENT_SUFFIX: 2, CHILD1_SUFFIX:1, CHILD2_SUFFIX:0}), id="Both"),
+ pytest.param( ((PARENT_REFERRAL_DN,), {PARENT_SUFFIX: 1, CHILD1_SUFFIX:0, CHILD2_SUFFIX:0}) , id="Parent"),
+ pytest.param( ((CHILD1_REFERRAL_DN,), {PARENT_SUFFIX: 1, CHILD1_SUFFIX:1, CHILD2_SUFFIX:0}) , id="Child"),
+ pytest.param( ((), {PARENT_SUFFIX: 0, CHILD1_SUFFIX:0, CHILD2_SUFFIX:0}), id="None"),
+ ])
+
+def test_referral_subsuffix(topo, request, parameters):
+ """Test the results of an inverted parent suffix definition in the configuration.
+
+ For more details see:
+ https://www.port389.org/docs/389ds/design/mapping_tree_assembly.html
+
+ :id: 4e111a22-2a5d-11ef-a890-482ae39447e5
+ :feature: referrals
+ :setup: Standalone instance with 3 additional backends:
+ dc=parent, dc=child1,dc=parent, dc=childr21,dc=parent
+
+ :setup: Standalone instance
+ :parametrized: yes
+ :steps:
+ refs,searches = referrals
+
+ 1. Create the referrals according to the current parameter
+ 2. Wait enough time so they get detected
+ 3. For each search base dn, in the current parameter, perform the two following steps
+ 4. In 3. loop: Perform a search with provided base dn
+ 5. In 3. loop: Check that the number of returned referrals is the expected one.
+
+ :expectedresults:
+ all steps succeeds
+ """
+ inst = topo.standalone
+
+ def fin():
+ log.info('Deleting all referrals')
+ for ref in Referrals(inst, PARENT_SUFFIX).list():
+ ref.delete()
+
+ # Set cleanup callback
+ if DEBUGGING:
+ request.addfinalizer(fin)
+
+ # Remove all referrals
+ fin()
+ # Add requested referrals
+ for dn in parameters[0]:
+ refs = Referral(inst, dn=dn)
+ refs.create(basedn=dn, properties={ 'cn': 'ref', 'ref': f'ldap://remote/{dn}'})
+ # Wait that the internal search detects the referrals
+ time.sleep(REFERRAL_CHECK_PEDIOD + 1)
+ # Open a test connection
+ ldc = ldap.initialize(f"ldap://{HOST_STANDALONE}:{PORT_STANDALONE}")
+ ldc.set_option(ldap.OPT_REFERRALS,0)
+ ldc.simple_bind_s(DN_DM,PW_DM)
+
+ # For each search base dn:
+ for basedn,nbref in parameters[1].items():
+ log.info(f"Referrals are: {parameters[0]}")
+ # Perform a search with provided base dn
+ result = ldc.search_s(basedn, ldap.SCOPE_SUBTREE, filterstr="(ou=People)")
+ found_dns = [ dn for dn,entry in result if dn is not None ]
+ found_refs = [ entry for dn,entry in result if dn is None ]
+ log.info(f"Search on {basedn} returned {found_dns} and {found_refs}")
+ # Check that the number of returned referrals is the expected one.
+ log.info(f"Search returned {len(found_refs)} referrals. {nbref} are expected.")
+ assert len(found_refs) == nbref
+ ldc.unbind()
diff --git a/ldap/servers/slapd/backend.c b/ldap/servers/slapd/backend.c
index 498f683b1..f86b0b9b6 100644
--- a/ldap/servers/slapd/backend.c
+++ b/ldap/servers/slapd/backend.c
@@ -230,12 +230,17 @@ slapi_exist_referral(Slapi_Backend *be)
/* search for ("smart") referral entries */
search_pb = slapi_pblock_new();
- server_ctrls = (LDAPControl **) slapi_ch_calloc(2, sizeof (LDAPControl *));
+ server_ctrls = (LDAPControl **) slapi_ch_calloc(3, sizeof (LDAPControl *));
server_ctrls[0] = (LDAPControl *) slapi_ch_malloc(sizeof (LDAPControl));
server_ctrls[0]->ldctl_oid = slapi_ch_strdup(LDAP_CONTROL_MANAGEDSAIT);
server_ctrls[0]->ldctl_value.bv_val = NULL;
server_ctrls[0]->ldctl_value.bv_len = 0;
server_ctrls[0]->ldctl_iscritical = '\0';
+ server_ctrls[1] = (LDAPControl *) slapi_ch_malloc(sizeof (LDAPControl));
+ server_ctrls[1]->ldctl_oid = slapi_ch_strdup(MTN_CONTROL_USE_ONE_BACKEND_EXT_OID);
+ server_ctrls[1]->ldctl_value.bv_val = NULL;
+ server_ctrls[1]->ldctl_value.bv_len = 0;
+ server_ctrls[1]->ldctl_iscritical = '\0';
slapi_search_internal_set_pb(search_pb, suffix, LDAP_SCOPE_SUBTREE,
filter, NULL, 0, server_ctrls, NULL,
(void *) plugin_get_default_component_id(), 0);
--
2.48.0

View File

@ -1,4 +1,4 @@
From f6dd2d6dc94290c85d221951b34792443bb08d80 Mon Sep 17 00:00:00 2001
From b5bee921b7f4cfbf7a2edbe55d4291f487f4a140 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Thu, 5 Feb 2026 12:17:06 +0100
Subject: [PATCH] Issue 7223 - Detect and log index ordering mismatch during
@ -10,16 +10,16 @@ index data and logs a message in case of a mismatch with DSE config entry.
Relates: https://github.com/389ds/389-ds-base/issues/7223
Reviewed by: @progier389, @tbordaz (Thanks!)
Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
---
ldap/servers/slapd/back-ldbm/instance.c | 269 ++++++++++++++++++++++++
1 file changed, 269 insertions(+)
ldap/servers/slapd/back-ldbm/instance.c | 262 ++++++++++++++++++++++++
1 file changed, 262 insertions(+)
diff --git a/ldap/servers/slapd/back-ldbm/instance.c b/ldap/servers/slapd/back-ldbm/instance.c
index 6098e04fc..388dd6efb 100644
index f9a546661..3fcdb5554 100644
--- a/ldap/servers/slapd/back-ldbm/instance.c
+++ b/ldap/servers/slapd/back-ldbm/instance.c
@@ -248,6 +248,273 @@ ldbm_instance_create_default_indexes(backend *be)
@@ -248,6 +248,266 @@ ldbm_instance_create_default_indexes(backend *be)
}
@ -107,10 +107,10 @@ index 6098e04fc..388dd6efb 100644
+{
+ ldbm_instance *inst = (ldbm_instance *)be->be_instance_info;
+ struct attrinfo *ai = NULL;
+ DB *db = NULL;
+ DBC *dbc = NULL;
+ DBT key;
+ DBT data;
+ dbi_db_t *db = NULL;
+ dbi_cursor_t dbc = {0};
+ dbi_val_t key = {0};
+ dbi_val_t data = {0};
+ int ret = 0;
+ PRBool config_has_int_order = PR_FALSE;
+ PRBool disk_has_int_order = PR_TRUE; /* Assume integer order until proven otherwise */
@ -147,7 +147,7 @@ index 6098e04fc..388dd6efb 100644
+ }
+
+ /* Create a cursor to read keys */
+ ret = db->cursor(db, NULL, &dbc, 0);
+ ret = dblayer_new_cursor(be, db, NULL, &dbc);
+ if (ret != 0) {
+ slapi_log_err(SLAPI_LOG_ERR, "ldbm_instance_check_index_config",
+ "Backend '%s': could not create cursor on %s index (ret=%d)\n",
@ -156,10 +156,8 @@ index 6098e04fc..388dd6efb 100644
+ return;
+ }
+
+ memset(&key, 0, sizeof(key));
+ memset(&data, 0, sizeof(data));
+ key.flags = DB_DBT_MALLOC;
+ data.flags = DB_DBT_MALLOC;
+ dblayer_value_init(be, &key);
+ dblayer_value_init(be, &data);
+
+ /*
+ * Read up to 100 unique keys and check their ordering.
@ -173,12 +171,7 @@ index 6098e04fc..388dd6efb 100644
+ while (key_count < 100) {
+ ID current_id;
+
+ slapi_ch_free(&(key.data));
+ slapi_ch_free(&(data.data));
+ key.size = 0;
+ data.size = 0;
+
+ ret = dbc->c_get(dbc, &key, &data, first_key ? DB_FIRST : DB_NEXT_NODUP);
+ ret = dblayer_cursor_op(&dbc, first_key ? DBI_OP_MOVE_TO_FIRST : DBI_OP_NEXT_KEY, &key, &data);
+ first_key = PR_FALSE; /* Always advance cursor on next iteration */
+ if (ret != 0) {
+ break; /* No more keys or error */
@ -229,9 +222,9 @@ index 6098e04fc..388dd6efb 100644
+ }
+
+ /* Close the cursor and free values */
+ slapi_ch_free(&(key.data));
+ slapi_ch_free(&(data.data));
+ dbc->c_close(dbc);
+ dblayer_cursor_op(&dbc, DBI_OP_CLOSE, NULL, NULL);
+ dblayer_value_free(be, &key);
+ dblayer_value_free(be, &data);
+
+ /* Release the index file */
+ dblayer_release_index_file(be, ai, db);
@ -293,7 +286,7 @@ index 6098e04fc..388dd6efb 100644
/* Starts a backend instance */
int
ldbm_instance_start(backend *be)
@@ -316,6 +583,8 @@ ldbm_instance_startall(struct ldbminfo *li)
@@ -319,6 +579,8 @@ ldbm_instance_startall(struct ldbminfo *li)
ldbm_instance_register_modify_callback(inst);
vlv_init(inst);
slapi_mtn_be_started(inst->inst_be);

View File

@ -1,32 +0,0 @@
From ab06b3cebbe0287ef557c0307ca2ee86fe8cb761 Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Thu, 21 Nov 2024 16:26:02 +0100
Subject: [PATCH] Issue 6224 - Fix merge issue in 389-ds-base-2.1 for
ds_log_test.py (#6414)
Fix a merge issue during cherry-pick over 389-ds-base-2.1 and 389-ds-base-1.4.3 branches
Issue: #6224
Reviewed by: @mreynolds389
(cherry picked from commit 2b541c64b8317209e4dafa4f82918d714039907c)
---
dirsrvtests/tests/suites/ds_logs/ds_logs_test.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
index 84a9c6ec8..812936c62 100644
--- a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
+++ b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
@@ -1222,7 +1222,6 @@ def test_referral_check(topology_st, request):
request.addfinalizer(fin)
-<<<<<<< HEAD
def test_referral_subsuffix(topology_st, request):
"""Test the results of an inverted parent suffix definition in the configuration.
--
2.48.0

View File

@ -1,4 +1,4 @@
From 99470a4befbea7eda777b9096337e29113e1ddda Mon Sep 17 00:00:00 2001
From 04eca1fe36480561bc2f59440d971e000133d213 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Thu, 5 Feb 2026 12:17:06 +0100
Subject: [PATCH] Issue 7223 - Add dsctl index-check command for offline index
@ -15,19 +15,19 @@ upgrade.
Relates: https://github.com/389ds/389-ds-base/issues/7223
Reviewed by: @progier389, @tbordaz (Thanks!)
Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
---
.../healthcheck/health_system_indexes_test.py | 593 ++++++++++++++++++
rpm/389-ds-base.spec.in | 40 +-
rpm/389-ds-base.spec.in | 51 +-
src/lib389/lib389/cli_ctl/dbtasks.py | 402 ++++++++++++
src/lib389/lib389/dseldif.py | 56 +-
4 files changed, 1085 insertions(+), 6 deletions(-)
src/lib389/lib389/dseldif.py | 51 +-
4 files changed, 1068 insertions(+), 29 deletions(-)
diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
index db5d13bf8..ce86239e5 100644
index 4b0c58835..571465562 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -588,6 +588,599 @@ def test_upgrade_removes_ancestorid_index_config(topology_st):
@@ -587,6 +587,599 @@ def test_upgrade_removes_ancestorid_index_config(topology_st):
log.info(f"Idempotency verified - ancestorid still absent after second restart (got exception: {e})")
@ -628,47 +628,70 @@ index db5d13bf8..ce86239e5 100644
# Run isolated
# -s for DEBUG mode
diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in
index b8c14cd14..16b8d14c5 100644
index 44a158ce5..0175dfa7c 100644
--- a/rpm/389-ds-base.spec.in
+++ b/rpm/389-ds-base.spec.in
@@ -507,7 +507,45 @@ if ! getent passwd $USERNAME >/dev/null ; then
@@ -645,42 +645,45 @@ if ! getent passwd $USERNAME >/dev/null ; then
fi
# Reload our sysctl before we restart (if we can)
-sysctl --system &> $output; true
+sysctl --system &> "$output"; true
+
-# Gather the running instances so we can restart them
+# Gather running instances, stop them, run index-check, then restart
+instbase="%{_sysconfdir}/%{pkgname}"
instbase="%{_sysconfdir}/%{pkgname}"
+instances=""
+ninst=0
ninst=0
-for dir in $instbase/slapd-* ; do
- echo dir = $dir >> $output 2>&1 || :
+
+for dir in "$instbase"/slapd-* ; do
+ echo "dir = $dir" >> "$output" 2>&1 || :
+ if [ ! -d "$dir" ] ; then continue ; fi
+ case "$dir" in *.removed) continue ;; esac
if [ ! -d "$dir" ] ; then continue ; fi
case "$dir" in *.removed) continue ;; esac
- basename=`basename $dir`
- inst="%{pkgname}@`echo $basename | sed -e 's/slapd-//g'`"
- echo found instance $inst - getting status >> $output 2>&1 || :
- if /bin/systemctl -q is-active $inst ; then
- echo instance $inst is running >> $output 2>&1 || :
+ basename=$(basename "$dir")
+ inst="%{pkgname}@${basename#slapd-}"
+ inst_name="${basename#slapd-}"
+ echo "found instance $inst - getting status" >> "$output" 2>&1 || :
+ if /bin/systemctl -q is-active "$inst" ; then
+ echo "instance $inst is running - stopping for upgrade" >> "$output" 2>&1 || :
+ instances="$instances $inst"
instances="$instances $inst"
+ /bin/systemctl stop "$inst" >> "$output" 2>&1 || :
+ else
else
- echo instance $inst is not running >> $output 2>&1 || :
+ echo "instance $inst is not running" >> "$output" 2>&1 || :
+ fi
fi
- ninst=`expr $ninst + 1`
+ # Run index-check on all instances (running or not)
+ # This fixes index ordering mismatches from older versions
+ dsctl "$inst_name" index-check --fix >> "$output2" 2>&1 || :
+ ninst=$((ninst + 1))
+done
done
+
+if [ $ninst -eq 0 ] ; then
if [ $ninst -eq 0 ] ; then
- echo no instances to upgrade >> $output 2>&1 || :
- exit 0 # have no instances to upgrade - just skip the rest
-else
- # restart running instances
- echo shutting down all instances . . . >> $output 2>&1 || :
- for inst in $instances ; do
- echo stopping instance $inst >> $output 2>&1 || :
- /bin/systemctl stop $inst >> $output 2>&1 || :
- done
- for inst in $instances ; do
- echo starting instance $inst >> $output 2>&1 || :
- /bin/systemctl start $inst >> $output 2>&1 || :
- done
+ echo "no instances to upgrade" >> "$output" 2>&1 || :
+ exit 0
+fi
+
fi
+# Restart previously running instances
+for inst in $instances ; do
+ echo "starting instance $inst" >> "$output" 2>&1 || :
@ -1109,19 +1132,14 @@ index 856639672..16da966d1 100644
+ help="Fix mismatches by reindexing affected indexes")
+ index_check_parser.set_defaults(func=dbtasks_index_check)
diff --git a/src/lib389/lib389/dseldif.py b/src/lib389/lib389/dseldif.py
index 3104a7b6f..49efc5dcb 100644
index d12c6424c..7834d9468 100644
--- a/src/lib389/lib389/dseldif.py
+++ b/src/lib389/lib389/dseldif.py
@@ -118,11 +118,19 @@ class DSEldif(DSLint):
with open(self.path, "w") as file_dse:
file_dse.write("".join(self._contents))
@@ -125,11 +125,14 @@ class DSEldif(DSLint):
self._contents[i] = self._contents[i].replace(strfrom, strto)
self._update()
- def _find_attr(self, entry_dn, attr):
+ def globalSubstitute(self, strfrom, strto):
+ for i in range(0, len(self._contents)-1):
+ self._contents[i] = self._contents[i].replace(strfrom, strto)
+ self._update()
+
+ def _find_attr(self, entry_dn, attr, lower=False):
"""Find all attribute values and indexes under a given entry
@ -1133,7 +1151,7 @@ index 3104a7b6f..49efc5dcb 100644
"""
entry_dn_i = self._contents.index("dn: {}\n".format(entry_dn.lower()))
@@ -139,7 +147,11 @@ class DSEldif(DSLint):
@@ -146,7 +149,11 @@ class DSEldif(DSLint):
# Find the attribute
for line in entry_slice:
@ -1146,7 +1164,7 @@ index 3104a7b6f..49efc5dcb 100644
attr_value = line.split(" ", 1)[1][:-1]
attr_data.update({entry_slice.index(line): attr_value})
@@ -148,7 +160,7 @@ class DSEldif(DSLint):
@@ -155,7 +162,7 @@ class DSEldif(DSLint):
return entry_dn_i, attr_data
@ -1155,7 +1173,7 @@ index 3104a7b6f..49efc5dcb 100644
"""Return attribute values under a given entry
:param entry_dn: a DN of entry we want to get attribute from
@@ -156,11 +168,13 @@ class DSEldif(DSLint):
@@ -163,11 +170,13 @@ class DSEldif(DSLint):
:param attr: an attribute name
:type attr: str
:param single: Return a single value instead of a list
@ -1171,7 +1189,7 @@ index 3104a7b6f..49efc5dcb 100644
except ValueError:
return None
@@ -183,6 +197,38 @@ class DSEldif(DSLint):
@@ -190,6 +199,38 @@ class DSEldif(DSLint):
return indexes

View File

@ -1,214 +0,0 @@
From 3fe2cf7cdedcdf5cafb59867e52a1fbe4a643571 Mon Sep 17 00:00:00 2001
From: Masahiro Matsuya <mmatsuya@redhat.com>
Date: Fri, 20 Dec 2024 22:37:15 +0900
Subject: [PATCH] Issue 6224 - Remove test_referral_subsuffix from
ds_logs_test.py (#6456)
Bug Description:
test_referral_subsuffix test was removed from main branch and some other
ones for higher versions. But, it was not removed from 389-ds-base-1.4.3
and 389-ds-base-2.1. The test doesn't work anymore with the fix for
Issue 6224, because the added new control limited one backend for internal
search. The test should be removed.
Fix Description:
remove the test from ds_logs_test.py
relates: https://github.com/389ds/389-ds-base/issues/6224
---
.../tests/suites/ds_logs/ds_logs_test.py | 177 ------------------
1 file changed, 177 deletions(-)
diff --git a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
index 812936c62..84d721756 100644
--- a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
+++ b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
@@ -1222,183 +1222,6 @@ def test_referral_check(topology_st, request):
request.addfinalizer(fin)
-def test_referral_subsuffix(topology_st, request):
- """Test the results of an inverted parent suffix definition in the configuration.
-
- For more details see:
- https://www.port389.org/docs/389ds/design/mapping_tree_assembly.html
-
- :id: 4faf210a-4fde-4e4f-8834-865bdc8f4d37
- :setup: Standalone instance
- :steps:
- 1. First create two Backends, without mapping trees.
- 2. create the mapping trees for these backends
- 3. reduce nsslapd-referral-check-period to accelerate test
- 4. Remove error log file
- 5. Create a referral entry on parent suffix
- 6. Check that the server detected the referral
- 7. Delete the referral entry
- 8. Check that the server detected the deletion of the referral
- 9. Remove error log file
- 10. Create a referral entry on child suffix
- 11. Check that the server detected the referral on both parent and child suffixes
- 12. Delete the referral entry
- 13. Check that the server detected the deletion of the referral on both parent and child suffixes
- 14. Remove error log file
- 15. Create a referral entry on parent suffix
- 16. Check that the server detected the referral on both parent and child suffixes
- 17. Delete the child referral entry
- 18. Check that the server detected the deletion of the referral on child suffix but not on parent suffix
- 19. Delete the parent referral entry
- 20. Check that the server detected the deletion of the referral parent suffix
-
- :expectedresults:
- all steps succeeds
- """
- inst = topology_st.standalone
- # Step 1 First create two Backends, without mapping trees.
- PARENT_SUFFIX='dc=parent,dc=com'
- CHILD_SUFFIX='dc=child,%s' % PARENT_SUFFIX
- be1 = create_backend(inst, 'Parent', PARENT_SUFFIX)
- be2 = create_backend(inst, 'Child', CHILD_SUFFIX)
- # Step 2 create the mapping trees for these backends
- mts = MappingTrees(inst)
- mt1 = mts.create(properties={
- 'cn': PARENT_SUFFIX,
- 'nsslapd-state': 'backend',
- 'nsslapd-backend': 'Parent',
- })
- mt2 = mts.create(properties={
- 'cn': CHILD_SUFFIX,
- 'nsslapd-state': 'backend',
- 'nsslapd-backend': 'Child',
- 'nsslapd-parent-suffix': PARENT_SUFFIX,
- })
-
- dc_ex = Domain(inst, dn=PARENT_SUFFIX)
- assert dc_ex.exists()
-
- dc_st = Domain(inst, dn=CHILD_SUFFIX)
- assert dc_st.exists()
-
- # Step 3 reduce nsslapd-referral-check-period to accelerate test
- # requires a restart done on step 4
- REFERRAL_CHECK=7
- topology_st.standalone.config.set("nsslapd-referral-check-period", str(REFERRAL_CHECK))
-
- # Check that if we create a referral at parent level
- # - referral is detected at parent backend
- # - referral is not detected at child backend
-
- # Step 3 Remove error log file
- topology_st.standalone.stop()
- lpath = topology_st.standalone.ds_error_log._get_log_path()
- os.unlink(lpath)
- topology_st.standalone.start()
-
- # Step 4 Create a referral entry on parent suffix
- rs_parent = Referrals(topology_st.standalone, PARENT_SUFFIX)
-
- referral_entry_parent = rs_parent.create(properties={
- 'cn': 'testref',
- 'ref': 'ldap://localhost:38901/ou=People,dc=example,dc=com'
- })
-
- # Step 5 Check that the server detected the referral
- time.sleep(REFERRAL_CHECK + 1)
- assert topology_st.standalone.ds_error_log.match('.*slapd_daemon - New referral entries are detected under %s.*' % PARENT_SUFFIX)
- assert not topology_st.standalone.ds_error_log.match('.*slapd_daemon - New referral entries are detected under %s.*' % CHILD_SUFFIX)
- assert not topology_st.standalone.ds_error_log.match('.*slapd_daemon - No more referral entry under %s' % PARENT_SUFFIX)
-
- # Step 6 Delete the referral entry
- referral_entry_parent.delete()
-
- # Step 7 Check that the server detected the deletion of the referral
- time.sleep(REFERRAL_CHECK + 1)
- assert topology_st.standalone.ds_error_log.match('.*slapd_daemon - No more referral entry under %s' % PARENT_SUFFIX)
-
- # Check that if we create a referral at child level
- # - referral is detected at parent backend
- # - referral is detected at child backend
-
- # Step 8 Remove error log file
- topology_st.standalone.stop()
- lpath = topology_st.standalone.ds_error_log._get_log_path()
- os.unlink(lpath)
- topology_st.standalone.start()
-
- # Step 9 Create a referral entry on child suffix
- rs_child = Referrals(topology_st.standalone, CHILD_SUFFIX)
- referral_entry_child = rs_child.create(properties={
- 'cn': 'testref',
- 'ref': 'ldap://localhost:38901/ou=People,dc=example,dc=com'
- })
-
- # Step 10 Check that the server detected the referral on both parent and child suffixes
- time.sleep(REFERRAL_CHECK + 1)
- assert topology_st.standalone.ds_error_log.match('.*slapd_daemon - New referral entries are detected under %s.*' % PARENT_SUFFIX)
- assert topology_st.standalone.ds_error_log.match('.*slapd_daemon - New referral entries are detected under %s.*' % CHILD_SUFFIX)
- assert not topology_st.standalone.ds_error_log.match('.*slapd_daemon - No more referral entry under %s' % CHILD_SUFFIX)
-
- # Step 11 Delete the referral entry
- referral_entry_child.delete()
-
- # Step 12 Check that the server detected the deletion of the referral on both parent and child suffixes
- time.sleep(REFERRAL_CHECK + 1)
- assert topology_st.standalone.ds_error_log.match('.*slapd_daemon - No more referral entry under %s' % PARENT_SUFFIX)
- assert topology_st.standalone.ds_error_log.match('.*slapd_daemon - No more referral entry under %s' % CHILD_SUFFIX)
-
- # Check that if we create a referral at child level and parent level
- # - referral is detected at parent backend
- # - referral is detected at child backend
-
- # Step 13 Remove error log file
- topology_st.standalone.stop()
- lpath = topology_st.standalone.ds_error_log._get_log_path()
- os.unlink(lpath)
- topology_st.standalone.start()
-
- # Step 14 Create a referral entry on parent suffix
- # Create a referral entry on child suffix
- referral_entry_parent = rs_parent.create(properties={
- 'cn': 'testref',
- 'ref': 'ldap://localhost:38901/ou=People,dc=example,dc=com'
- })
- referral_entry_child = rs_child.create(properties={
- 'cn': 'testref',
- 'ref': 'ldap://localhost:38901/ou=People,dc=example,dc=com'
- })
-
- # Step 15 Check that the server detected the referral on both parent and child suffixes
- time.sleep(REFERRAL_CHECK + 1)
- assert topology_st.standalone.ds_error_log.match('.*slapd_daemon - New referral entries are detected under %s.*' % PARENT_SUFFIX)
- assert topology_st.standalone.ds_error_log.match('.*slapd_daemon - New referral entries are detected under %s.*' % CHILD_SUFFIX)
- assert not topology_st.standalone.ds_error_log.match('.*slapd_daemon - No more referral entry under %s' % CHILD_SUFFIX)
-
- # Step 16 Delete the child referral entry
- referral_entry_child.delete()
-
- # Step 17 Check that the server detected the deletion of the referral on child suffix but not on parent suffix
- time.sleep(REFERRAL_CHECK + 1)
- assert topology_st.standalone.ds_error_log.match('.*slapd_daemon - No more referral entry under %s' % CHILD_SUFFIX)
- assert not topology_st.standalone.ds_error_log.match('.*slapd_daemon - No more referral entry under %s' % PARENT_SUFFIX)
-
- # Step 18 Delete the parent referral entry
- referral_entry_parent.delete()
-
- # Step 19 Check that the server detected the deletion of the referral parent suffix
- time.sleep(REFERRAL_CHECK + 1)
- assert topology_st.standalone.ds_error_log.match('.*slapd_daemon - No more referral entry under %s' % PARENT_SUFFIX)
-
- def fin():
- log.info('Deleting referral')
- try:
- referral_entry_parent.delete()
- referral.entry_child.delete()
- except:
- pass
-
- request.addfinalizer(fin)
def test_missing_backend_suffix(topology_st, request):
"""Test that the server does not crash if a backend has no suffix
--
2.48.0

View File

@ -0,0 +1,135 @@
From c1f0756b453b7ea219add236f60a72b3fc660af0 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Tue, 27 Jan 2026 14:26:29 +0100
Subject: [PATCH] Issue 7096 - (2nd) During replication online total init the
function idl_id_is_in_idlist is not scaling with large database (#7205)
Bug Description:
The fix for #7096 optimized the BDB backend's `idl_new_range_fetch()`
function to use ID ranges instead of checking the full ID list during
online total initialization. However, the LMDB backend's
`idl_lmdb_range_fetch()` function and its callback
`idl_range_add_id_cb()` were not updated and still use the non-scaling
`idl_id_is_in_idlist()` function.
Fix Description:
Apply the same optimization to the LMDB backend.
Fixes: https://github.com/389ds/389-ds-base/issues/7096
Reviewed by: @tbordaz, @droideck (Thanks!)
---
ldap/servers/slapd/back-ldbm/idl_new.c | 39 ++++++++++----------------
1 file changed, 15 insertions(+), 24 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/idl_new.c b/ldap/servers/slapd/back-ldbm/idl_new.c
index 2d978353f..613d53815 100644
--- a/ldap/servers/slapd/back-ldbm/idl_new.c
+++ b/ldap/servers/slapd/back-ldbm/idl_new.c
@@ -66,6 +66,7 @@ typedef struct {
size_t leftoverlen;
size_t leftovercnt;
IDList *idl;
+ IdRange_t *idrange_list;
int flag_err;
ID lastid;
ID suffix;
@@ -700,9 +701,9 @@ error:
}
}
}
- slapi_ch_free((void **)&leftover);
- idrange_free(&idrange_list);
}
+ slapi_ch_free((void **)&leftover);
+ idrange_free(&idrange_list);
slapi_log_err(SLAPI_LOG_FILTER, "idl_new_range_fetch",
"Found %d candidates; error code is: %d\n",
idl ? idl->b_nids : 0, *flag_err);
@@ -716,7 +717,6 @@ static int
idl_range_add_id_cb(dbi_val_t *key, dbi_val_t *data, void *ctx)
{
idl_range_ctx_t *rctx = ctx;
- int idl_rc = 0;
ID id = 0;
if (key->data == NULL) {
@@ -779,10 +779,12 @@ idl_range_add_id_cb(dbi_val_t *key, dbi_val_t *data, void *ctx)
* found entry is the one from the suffix
*/
rctx->suffix = keyval;
- idl_rc = idl_append_extend(&rctx->idl, id);
- } else if ((keyval == rctx->suffix) || idl_id_is_in_idlist(rctx->idl, keyval)) {
+ idl_append_extend(&rctx->idl, id);
+ idrange_add_id(&rctx->idrange_list, id);
+ } else if ((keyval == rctx->suffix) || idl_id_is_in_idlist_ranges(rctx->idl, rctx->idrange_list, keyval)) {
/* the parent is the suffix or already in idl. */
- idl_rc = idl_append_extend(&rctx->idl, id);
+ idl_append_extend(&rctx->idl, id);
+ idrange_add_id(&rctx->idrange_list, id);
} else {
/* Otherwise, keep the {keyval,id} in leftover array */
if (!rctx->leftover) {
@@ -797,14 +799,7 @@ idl_range_add_id_cb(dbi_val_t *key, dbi_val_t *data, void *ctx)
rctx->leftovercnt++;
}
} else {
- idl_rc = idl_append_extend(&rctx->idl, id);
- }
- if (idl_rc) {
- slapi_log_err(SLAPI_LOG_ERR, "idl_lmdb_range_fetch",
- "Unable to extend id list (err=%d)\n", idl_rc);
- idl_free(&rctx->idl);
- rctx->flag_err = LDAP_UNWILLING_TO_PERFORM;
- return DBI_RC_NOTFOUND;
+ idl_append_extend(&rctx->idl, id);
}
#if defined(DB_ALLIDS_ON_READ)
/* enforce the allids read limit */
@@ -841,7 +836,6 @@ idl_lmdb_range_fetch(
{
int ret = 0;
int ret2 = 0;
- int idl_rc = 0;
dbi_cursor_t cursor = {0};
back_txn s_txn;
struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private;
@@ -891,6 +885,7 @@ idl_lmdb_range_fetch(
idl_range_ctx.lastid = 0;
idl_range_ctx.count = 0;
idl_range_ctx.index_id = index_id;
+ idl_range_ctx.idrange_list = NULL;
if (operator & SLAPI_OP_RANGE_NO_IDL_SORT) {
struct _back_info_index_key bck_info;
/* We are doing a bulk import
@@ -966,22 +961,18 @@ error:
while(remaining > 0) {
for (size_t i = 0; i < idl_range_ctx.leftovercnt; i++) {
if (idl_range_ctx.leftover[i].key > 0 &&
- idl_id_is_in_idlist(idl_range_ctx.idl, idl_range_ctx.leftover[i].key) != 0) {
+ idl_id_is_in_idlist_ranges(idl_range_ctx.idl, idl_range_ctx.idrange_list, idl_range_ctx.leftover[i].key) != 0) {
/* if the leftover key has its parent in the idl */
- idl_rc = idl_append_extend(&idl_range_ctx.idl, idl_range_ctx.leftover[i].id);
- if (idl_rc) {
- slapi_log_err(SLAPI_LOG_ERR, "idl_lmdb_range_fetch",
- "Unable to extend id list (err=%d)\n", idl_rc);
- idl_free(&idl_range_ctx.idl);
- break;
- }
+ idl_append_extend(&idl_range_ctx.idl, idl_range_ctx.leftover[i].id);
+ idrange_add_id(&idl_range_ctx.idrange_list, idl_range_ctx.leftover[i].id);
idl_range_ctx.leftover[i].key = 0;
remaining--;
}
}
}
- slapi_ch_free((void **)&idl_range_ctx.leftover);
}
+ slapi_ch_free((void **)&idl_range_ctx.leftover);
+ idrange_free(&idl_range_ctx.idrange_list);
*flag_err = idl_range_ctx.flag_err;
slapi_log_err(SLAPI_LOG_FILTER, "idl_lmdb_range_fetch",
"Found %d candidates; error code is: %d\n",
--
2.52.0

View File

@ -1,90 +0,0 @@
From 4121ffe7a44fbacf513758661e71e483eb11ee3c Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Mon, 6 Jan 2025 14:00:39 +0100
Subject: [PATCH] Issue 6417 - (2nd) If an entry RDN is identical to the
suffix, then Entryrdn gets broken during a reindex (#6460)
Bug description:
The primary fix has a flaw as it assumes that the
suffix ID is '1'.
If the RUV entry is the first entry of the database
the server loops indefinitely
Fix description:
Read the suffix ID from the entryrdn index
fixes: #6417
Reviewed by: Pierre Rogier (also reviewed the first fix)
---
.../suites/replication/regression_m2_test.py | 9 +++++++++
ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c | 19 ++++++++++++++++++-
2 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/dirsrvtests/tests/suites/replication/regression_m2_test.py b/dirsrvtests/tests/suites/replication/regression_m2_test.py
index abac46ada..72d4b9f89 100644
--- a/dirsrvtests/tests/suites/replication/regression_m2_test.py
+++ b/dirsrvtests/tests/suites/replication/regression_m2_test.py
@@ -1010,6 +1010,15 @@ def test_online_reinit_may_hang(topo_with_sigkill):
"""
M1 = topo_with_sigkill.ms["supplier1"]
M2 = topo_with_sigkill.ms["supplier2"]
+
+ # The RFE 5367 (when enabled) retrieves the DN
+ # from the dncache. This hides an issue
+ # with primary fix for 6417.
+ # We need to disable the RFE to verify that the primary
+ # fix is properly fixed.
+ if ds_is_newer('2.3.1'):
+ M1.config.replace('nsslapd-return-original-entrydn', 'off')
+
M1.stop()
ldif_file = '%s/supplier1.ldif' % M1.get_ldif_dir()
M1.db2ldif(bename=DEFAULT_BENAME, suffixes=[DEFAULT_SUFFIX],
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c b/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c
index 83b041192..1bbb6252a 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c
@@ -1115,6 +1115,7 @@ entryrdn_lookup_dn(backend *be,
rdn_elem *elem = NULL;
int maybesuffix = 0;
int db_retry = 0;
+ ID suffix_id = 1;
slapi_log_err(SLAPI_LOG_TRACE, "entryrdn_lookup_dn",
"--> entryrdn_lookup_dn\n");
@@ -1175,6 +1176,22 @@ entryrdn_lookup_dn(backend *be,
/* Setting the bulk fetch buffer */
data.flags = DB_DBT_MALLOC;
+ /* Just in case the suffix ID is not '1' retrieve it from the database */
+ keybuf = slapi_ch_strdup(slapi_sdn_get_ndn(be->be_suffix));
+ dblayer_value_set(be, &key, keybuf, strlen(keybuf) + 1);
+ rc = dblayer_cursor_op(&ctx.cursor, DBI_OP_MOVE_TO_KEY, &key, &data);
+ if (rc) {
+ slapi_log_err(SLAPI_LOG_WARNING, "entryrdn_lookup_dn",
+ "Fails to retrieve the ID of suffix %s - keep the default value '%d'\n",
+ slapi_sdn_get_ndn(be->be_suffix),
+ suffix_id);
+ } else {
+ elem = (rdn_elem *)data.data;
+ suffix_id = id_stored_to_internal(elem->rdn_elem_id);
+ }
+ dblayer_value_free(be, &data);
+ dblayer_value_free(be, &key);
+
do {
/* Setting up a key for the node to get its parent */
slapi_ch_free_string(&keybuf);
@@ -1224,7 +1241,7 @@ entryrdn_lookup_dn(backend *be,
}
goto bail;
}
- if (workid == 1) {
+ if (workid == suffix_id) {
/* The loop (workid) iterates from the starting 'id'
* up to the suffix ID (i.e. '1').
* A corner case (#6417) is if an entry, on the path
--
2.48.0

View File

@ -0,0 +1,877 @@
From 35237d57daf6fdf20a42c3cef27130ba70f0cdc2 Mon Sep 17 00:00:00 2001
From: Akshay Adhikari <aadhikar@redhat.com>
Date: Tue, 18 Nov 2025 21:57:10 +0530
Subject: [PATCH] Issue 7076, 6992, 6784, 6214 - Fix CI test failures (#7077)
- Fixed import test bugs in regression_test.py (cleanup handler, LDIF permissions) -
https://github.com/389ds/389-ds-base/issues/6992
- Fixed ModRDN cache corruption on failed operations (parent update check, cache cleanup)
- Fixed attribute uniqueness test fixture cleanup in attruniq_test.py
- mproved test stability by fixing race conditions in replication, healthcheck,
web UI, memberOf, and basic tests.
- Fixed entrycache_eviction_test.py to track incremental log counts instead of cumulative -
https://github.com/389ds/389-ds-base/issues/6784
Fixes: https://github.com/389ds/389-ds-base/issues/7076
Relates: https://github.com/389ds/389-ds-base/issues/6992
Relates: https://github.com/389ds/389-ds-base/issues/6784
Fixes: https://github.com/389ds/389-ds-base/issues/6214
Reviewed by: @vashirov, @progier389 (Thanks!)
---
dirsrvtests/tests/suites/basic/basic_test.py | 4 +-
.../healthcheck/health_system_indexes_test.py | 3 +
.../tests/suites/import/regression_test.py | 388 +++++++++++++++++-
.../suites/memberof_plugin/fixup_test.py | 8 +-
.../tests/suites/plugins/attruniq_test.py | 84 ++--
.../suites/replication/regression_m2_test.py | 4 +
.../replication/repl_log_monitoring_test.py | 8 +-
.../suites/webui/database/database_test.py | 1 +
ldap/servers/slapd/back-ldbm/ldbm_modrdn.c | 44 +-
9 files changed, 481 insertions(+), 63 deletions(-)
diff --git a/dirsrvtests/tests/suites/basic/basic_test.py b/dirsrvtests/tests/suites/basic/basic_test.py
index be825efe9..e9b611439 100644
--- a/dirsrvtests/tests/suites/basic/basic_test.py
+++ b/dirsrvtests/tests/suites/basic/basic_test.py
@@ -593,7 +593,7 @@ def test_basic_import_export(topology_st, import_example_ldif):
#
# Test online/offline LDIF imports
#
- topology_st.standalone.start()
+ topology_st.standalone.restart()
# topology_st.standalone.config.set('nsslapd-errorlog-level', '1')
# Generate a test ldif (50k entries)
@@ -691,6 +691,8 @@ def test_basic_backup(topology_st, import_example_ldif):
log.info('Running test_basic_backup...')
+ topology_st.standalone.restart()
+
backup_dir = topology_st.standalone.get_bak_dir() + '/backup_test_online'
log.info(f'Backup directory is {backup_dir}')
diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
index 571465562..4cd4d0c70 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -172,6 +172,7 @@ def test_missing_parentid(topology_st, log_buffering_enabled):
log.info("Re-add the parentId index")
backend = Backends(standalone).get("userRoot")
backend.add_index("parentid", ["eq"], matching_rules=["integerOrderingMatch"])
+ standalone.restart()
run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
@@ -215,6 +216,7 @@ def test_missing_matching_rule(topology_st, log_buffering_enabled):
log.info("Re-add the integerOrderingMatch matching rule")
parentid_index = Index(standalone, PARENTID_DN)
parentid_index.add("nsMatchingRule", "integerOrderingMatch")
+ standalone.restart()
run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
@@ -445,6 +447,7 @@ def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
backend = Backends(standalone).get("userRoot")
backend.add_index("parentid", ["eq"], matching_rules=["integerOrderingMatch"])
backend.add_index("nsuniqueid", ["eq"])
+ standalone.restart()
run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
diff --git a/dirsrvtests/tests/suites/import/regression_test.py b/dirsrvtests/tests/suites/import/regression_test.py
index 18611de35..bdbb516e8 100644
--- a/dirsrvtests/tests/suites/import/regression_test.py
+++ b/dirsrvtests/tests/suites/import/regression_test.py
@@ -5,6 +5,7 @@
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---
#
+from abc import ABC, abstractmethod
from decimal import *
import ldap
import logging
@@ -16,9 +17,9 @@ from lib389.backend import Backends
from lib389.properties import TASK_WAIT
from lib389.topologies import topology_st as topo
from lib389.dbgen import dbgen_users
-from lib389._constants import DEFAULT_SUFFIX
+from lib389._constants import DEFAULT_SUFFIX, DEFAULT_BENAME
from lib389.tasks import *
-from lib389.idm.user import UserAccounts
+from lib389.idm.user import UserAccounts, UserAccount
from lib389.idm.directorymanager import DirectoryManager
from lib389.dbgen import *
from lib389.utils import *
@@ -92,7 +93,203 @@ class AddDelUsers(threading.Thread):
return self._ran
-def test_replay_import_operation(topo):
+def get_backend_by_name(inst, bename):
+ bes = Backends(inst)
+ bename = bename.lower()
+ be = [ be for be in bes if be.get_attr_val_utf8_l('cn') == bename ]
+ return be[0] if len(be) == 1 else None
+
+
+class LogHandler():
+ def __init__(self, logfd, patterns):
+ self.logfd = logfd
+ self.patterns = [ p.lower() for p in patterns ]
+ self.pos = logfd.tell()
+ self.last_result = None
+
+ def zero(self):
+ return [0 for _ in range(len(self.patterns))]
+
+ def countCaptures(self):
+ res = self.zero()
+ self.logfd.seek(self.pos)
+ for line in iter(self.logfd.readline, ''):
+ # Ignore autotune messages that may confuse the counts
+ if 'bdb_start_autotune' in line:
+ continue
+ # Ignore LMDB size warnings that may confuse the counts
+ if 'dbmdb_ctx_t_db_max_size_set' in line:
+ continue
+ log.info(f'ERROR LOG line is {line.strip()}')
+ for idx,pattern in enumerate(self.patterns):
+ if pattern in line.lower():
+ res[idx] += 1
+ self.pos = self.logfd.tell()
+ self.last_result = res
+ log.info(f'ERROR LOG counts are: {res}')
+ return res
+
+ def seek2end(self):
+ self.pos = os.fstat(self.logfd.fileno()).st_size
+
+ def check(self, idx, val):
+ count = self.last_result[idx]
+ assert count == val , f"Should have {val} '{self.patterns[idx]}' messages but got: {count} - idx = {idx}"
+
+
+class IEHandler(ABC):
+ def __init__(self, inst, errlog, ldifname, bename=DEFAULT_BENAME, suffix=None):
+ self.inst = inst
+ self.errlog = errlog
+ self.ldifname = ldifname
+ self.bename = bename
+ self.suffix = suffix
+ self.ldif = ldifname if ldifname.startswith('/') else f'{inst.get_ldif_dir()}/{ldifname}.ldif'
+
+ @abstractmethod
+ def get_name(self):
+ pass
+
+ @abstractmethod
+ def _run_task_b(self):
+ pass
+
+ @abstractmethod
+ def _run_task_s(self):
+ pass
+
+ @abstractmethod
+ def _run_offline(self):
+ pass
+
+ @abstractmethod
+ def _set_log_pattern(self, success):
+ pass
+
+ def run(self, extra_checks, success=True):
+ if self.errlog:
+ self._set_log_pattern(success)
+ self.errlog.seek2end()
+
+ if self.inst.status():
+ if self.bename:
+ log.info(f"Performing online {self.get_name()} of backend {self.bename} into LDIF file {self.ldif}")
+ r = self._run_task_b()
+ else:
+ log.info(f"Performing online {self.get_name()} of suffix {self.suffix} into LDIF file {self.ldif}")
+ r = self._run_task_s()
+ r.wait()
+ time.sleep(1)
+ else:
+ if self.bename:
+ log.info(f"Performing offline {self.get_name()} of backend {self.bename} into LDIF file {self.ldif}")
+ else:
+ log.info(f"Performing offline {self.get_name()} of suffix {self.suffix} into LDIF file {self.ldif}")
+ self._run_offline()
+ if self.errlog:
+ expected_counts = ['*' for _ in range(len(self.errlog.patterns))]
+ for (idx, val) in extra_checks:
+ expected_counts[idx] = val
+ res = self.errlog.countCaptures()
+ log.info(f'Expected errorlog counts are: {expected_counts}')
+ if success is True or success is False:
+ log.info(f'Number of {self.errlog.patterns[0]} in errorlog is: {res[0]}')
+ assert res[0] >= 1
+ for (idx, val) in extra_checks:
+ self.errlog.check(idx, val)
+
+ def check_db(self):
+ assert self.inst.dbscan(bename=self.bename, index='id2entry')
+
+
+class Importer(IEHandler):
+ def get_name(self):
+ return "import"
+
+ def _set_log_pattern(self, success):
+ if success is True:
+ self.errlog.patterns[0] = 'import complete'
+ elif success is False:
+ self.errlog.patterns[0] = 'import failed'
+
+ def _run_task_b(self):
+ bes = Backends(self.inst)
+ r = bes.import_ldif(self.bename, [self.ldif,], include_suffixes=self.suffix)
+ return r
+
+ def _run_task_s(self):
+ r = ImportTask(self.inst)
+ r.import_suffix_from_ldif(self.ldif, self.suffix)
+ return r
+
+ def _run_offline(self):
+ log.info(f'self.inst.ldif2db({self.bename}, {self.suffix}, ...)')
+ if self.suffix is None:
+ self.inst.ldif2db(self.bename, self.suffix, None, False, self.ldif)
+ else:
+ self.inst.ldif2db(self.bename, [self.suffix, ], None, False, self.ldif)
+
+
+class Exporter(IEHandler):
+ def get_name(self):
+ return "export"
+
+ def _set_log_pattern(self, success):
+ if success is True:
+ self.errlog.patterns[0] = 'export finished'
+ elif success is False:
+ self.errlog.patterns[0] = 'export failed'
+
+ def _run_task_b(self):
+ bes = Backends(self.inst)
+ r = bes.export_ldif(self.bename, self.ldif, include_suffixes=self.suffix)
+ return r
+
+ def _run_task_s(self):
+ r = ExportTask(self.inst)
+ r.export_suffix_to_ldif(self.ldif, self.suffix)
+ return r
+
+ def _run_offline(self):
+ self.inst.db2ldif(self.bename, self.suffix, None, False, False, self.ldif)
+
+
+def preserve_func(topo, request, restart):
+ # Ensure that topology get preserved helper
+ inst = topo.standalone
+
+ def fin():
+ if restart:
+ inst.restart()
+ Importer(inst, None, "save").run(())
+
+ r = Exporter(inst, None, "save")
+ if not os.path.isfile(r.ldif):
+ r.run(())
+ request.addfinalizer(fin)
+
+
+@pytest.fixture(scope="function")
+def preserve(topo, request):
+ # Ensure that topology get preserved (no restart)
+ preserve_func(topo, request, False)
+
+
+@pytest.fixture(scope="function")
+def preserve_r(topo, request):
+ # Ensure that topology get preserved (with restart)
+ preserve_func(topo, request, True)
+
+
+@pytest.fixture(scope="function")
+def verify(topo):
+ # Check that backend is not broken
+ inst = topo.standalone
+ dn=f'uid=demo_user,ou=people,{DEFAULT_SUFFIX}'
+ assert UserAccount(inst,dn).exists()
+
+
+def test_replay_import_operation(topo, preserve_r, verify):
""" Check after certain failed import operation, is it
possible to replay an import operation
@@ -487,7 +684,190 @@ def test_ldif2db_after_backend_create(topo):
import_time_2 = create_backend_and_import(instance, ldif_file_2, 'o=test_2', 'test_2')
log.info('Import times should be approximately the same')
- assert abs(import_time_1 - import_time_2) < 5
+ assert abs(import_time_1 - import_time_2) < 15
+
+
+def test_ldif_missing_suffix_entry(topo, request, verify):
+ """Test that ldif2db/import aborts if suffix entry is not in the ldif
+
+ :id: 731bd0d6-8cc8-11f0-8ef2-c85309d5c3e3
+ :setup: Standalone Instance
+ :steps:
+ 1. Prepare final cleanup
+ 2. Add a few users
+ 3. Export ou=people subtree
+ 4. Online import using backend name ou=people subtree
+ 5. Online import using suffix name ou=people subtree
+ 6. Stop the instance
+ 7. Offline import using backend name ou=people subtree
+ 8. Offline import using suffix name ou=people subtree
+ 9. Generate ldif with a far away suffix
+ 10. Offline import using backend name and "far" ldif
+ 11. Offline import using suffix name and "far" ldif
+ 12. Start the instance
+ 13. Online import using backend name and "far" ldif
+ 14. Online import using suffix name and "far" ldif
+ :expectedresults:
+ 1. Operation successful
+ 2. Operation successful
+ 3. Operation successful
+ 4. Import should success, skip all entries, db should exists
+ 5. Import should success, skip all entries, db should exists
+ 6. Operation successful
+ 7. Import should success, skip all entries, db should exists
+ 8. Import should success, skip all entries, db should exists
+ 9. Operation successful
+ 10. Import should success, skip all entries, db should exists
+ 11. Import should success, 10 entries skipped, db should exists
+ 12. Operation successful
+ 13. Import should success, skip all entries, db should exists
+ 14. Import should success, 10 entries skipped, db should exists
+ """
+
+ inst = topo.standalone
+ inst.config.set('nsslapd-errorlog-level', '266354688')
+ no_suffix_on = (
+ (1, 0), # no errors are expected.
+ (2, 1), # 1 warning is expected.
+ (3, 0), # no 'no parent' warning is expected.
+ (4, 1), # 1 'all entries were skipped' warning
+ (5, 0), # no 'returning task warning' info message
+ )
+ no_suffix_off = (
+ (1, 0), # no errors are expected.
+ (2, 1), # 1 warning is expected.
+ (3, 0), # no 'no parent' warning is expected.
+ (4, 1), # 1 'all entries were skipped' warning
+ (5, 1), # 1 'returning task warning' info message
+ )
+
+ far_suffix_on = (
+ (1, 0), # no errors are expected.
+ (2, 1), # 1 warning (consolidated, pre-check aborts after 4 entries)
+ (3, 0), # 0 'no parent' warnings (pre-check aborts before processing)
+ (4, 1), # 1 'all entries were skipped' warning (from pre-check)
+ (5, 0), # 0 'returning task warning' info message (online import)
+ )
+ # Backend-specific behavior for orphan detection when suffix parameter is provided
+ nbw = 0 if get_default_db_lib() == "bdb" else 10
+ far_suffix_with_suffix_on = (
+ (1, 0), # no errors are expected.
+ (2, nbw), # 0 (BDB early filtering) or 10 (LMDB orphan detection) warnings
+ (3, nbw), # 0 (BDB early filtering) or 10 (LMDB orphan detection) 'no parent' warnings
+ (4, 0), # 0 'all entries were skipped' warning (no pre-check abort)
+ (5, 0), # 0 'returning task warning' info message (online import)
+ )
+ far_suffix_off = (
+ (1, 0), # no errors are expected.
+ (2, 1), # 1 warning (consolidated, pre-check detects missing suffix)
+ (3, 0), # 0 'no parent' warnings (pre-check aborts before processing)
+ (4, 1), # 1 'all entries were skipped' warning (from pre-check)
+ (5, 1), # 1 'returning task warning' info message (offline import)
+ )
+ far_suffix_with_suffix_off = (
+ (1, 0), # no errors are expected.
+ (2, nbw), # 0 (BDB early filtering) or 10 (LMDB orphan detection) warnings
+ (3, nbw), # 0 (BDB early filtering) or 10 (LMDB orphan detection) 'no parent' warnings
+ (4, 0), # 0 'all entries were skipped' warning (no pre-check abort)
+ (5, 0), # 0 'returning task warning' (rc=0, successful import of suffix)
+ )
+
+ with open(inst.ds_paths.error_log, 'at+') as fd:
+ patterns = (
+ "Reserved for IEHandler",
+ " ERR ",
+ " WARN ",
+ "has no parent",
+ "all entries were skipped",
+ "returning task warning",
+ )
+ errlog = LogHandler(fd, patterns)
+ no_errors = ((1, 0), (2, 0)) # no errors nor warnings are expected.
+
+
+ # 1. Prepare final cleanup
+ Exporter(inst, errlog, "full").run(no_errors)
+
+ def fin():
+ inst.start()
+ with open(inst.ds_paths.error_log, 'at+') as cleanup_fd:
+ cleanup_errlog = LogHandler(cleanup_fd, patterns)
+ Importer(inst, cleanup_errlog, "full").run(no_errors)
+
+ if not DEBUGGING:
+ request.addfinalizer(fin)
+
+ # 2. Add a few users
+ user = UserAccounts(inst, DEFAULT_SUFFIX)
+ users = [ user.create_test_user(uid=i) for i in range(10) ]
+
+ # 3. Export ou=people subtree
+ e = Exporter(inst, errlog, "people", suffix=f'ou=people,{DEFAULT_SUFFIX}')
+ e.run(no_errors) # no errors nor warnings are expected.
+
+ # 4. Online import using backend name ou=people subtree
+ e = Importer(inst, errlog, "people")
+ e.run(no_suffix_on)
+ e.check_db()
+
+ # 5. Online import using suffix name ou=people subtree
+ e = Importer(inst, errlog, "people", suffix=DEFAULT_SUFFIX)
+ e.run(no_suffix_on)
+ e.check_db()
+
+ # 6. Stop the instance
+ inst.stop()
+
+ # 7. Offline import using backend name ou=people subtree
+ e = Importer(inst, errlog, "people")
+ e.run(no_suffix_off)
+ e.check_db()
+
+ # 8. Offline import using suffix name ou=people subtree
+ e = Importer(inst, errlog, "people", suffix=DEFAULT_SUFFIX)
+ e.run(no_suffix_off)
+ e.check_db()
+
+ # 9. Generate ldif with a far away suffix
+ e = Importer(inst, errlog, "full")
+ people_ldif = e.ldif
+ e = Importer(inst, errlog, "far")
+ with open(e.ldif, "wt") as fout:
+ with open(people_ldif, "rt") as fin:
+ # Copy version
+ line = fin.readline()
+ fout.write(line)
+ line = fin.readline()
+ fout.write(line)
+ # Generate fake entries
+ for idx in range(10):
+ fout.write(f"dn: uid=id{idx},dc=foo\nobjectclasses: extensibleObject\n\n")
+ for line in iter(fin.readline, ''):
+ fout.write(line)
+
+ os.chmod(e.ldif, 0o644)
+
+ # 10. Offline import using backend name ou=people subtree
+ e.run(far_suffix_off)
+ e.check_db()
+
+ # 11. Offline import using suffix name ou=people subtree
+ e = Importer(inst, errlog, "far", suffix=DEFAULT_SUFFIX)
+ e.run(far_suffix_with_suffix_off)
+ e.check_db()
+
+ # 12. Start the instance
+ inst.start()
+
+ # 13. Online import using backend name ou=people subtree
+ e = Importer(inst, errlog, "far")
+ e.run(far_suffix_on)
+ e.check_db()
+
+ # 14. Online import using suffix name ou=people subtree
+ e = Importer(inst, errlog, "far", suffix=DEFAULT_SUFFIX)
+ e.run(far_suffix_with_suffix_on)
+ e.check_db()
if __name__ == '__main__':
diff --git a/dirsrvtests/tests/suites/memberof_plugin/fixup_test.py b/dirsrvtests/tests/suites/memberof_plugin/fixup_test.py
index 5aac40d2b..44804bd1c 100644
--- a/dirsrvtests/tests/suites/memberof_plugin/fixup_test.py
+++ b/dirsrvtests/tests/suites/memberof_plugin/fixup_test.py
@@ -44,7 +44,10 @@ def test_fixup_task_limit(topo):
group = groups.create(properties={'cn': 'test'})
users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
- for idx in range(400):
+ # Turn on access log buffering to speed up user creation
+ buffering = topo.standalone.config.get_attr_val_utf8('nsslapd-accesslog-logbuffering')
+ topo.standalone.config.set('nsslapd-accesslog-logbuffering', 'on')
+ for idx in range(6000):
user = users.create(properties={
'uid': 'testuser%s' % idx,
'cn' : 'testuser%s' % idx,
@@ -55,6 +58,9 @@ def test_fixup_task_limit(topo):
})
group.add('member', user.dn)
+ # Restore access log buffering
+ topo.standalone.config.set('nsslapd-accesslog-logbuffering', buffering)
+
# Configure memberOf plugin
memberof = MemberOfPlugin(topo.standalone)
memberof.enable()
diff --git a/dirsrvtests/tests/suites/plugins/attruniq_test.py b/dirsrvtests/tests/suites/plugins/attruniq_test.py
index 6eaee08a4..a2be413c8 100644
--- a/dirsrvtests/tests/suites/plugins/attruniq_test.py
+++ b/dirsrvtests/tests/suites/plugins/attruniq_test.py
@@ -84,14 +84,23 @@ def containers(topology_st, request):
def attruniq(topology_st, request):
log.info('Setup attribute uniqueness plugin')
attruniq = AttributeUniquenessPlugin(topology_st.standalone, dn="cn=attruniq,cn=plugins,cn=config")
- attruniq.create(properties={'cn': 'attruniq'})
- attruniq.add_unique_attribute('cn')
+
+ if attruniq.exists():
+ attruniq.delete()
+ topology_st.standalone.restart()
+
+ attruniq.create(properties={
+ 'cn': 'attruniq',
+ 'uniqueness-attribute-name': 'cn',
+ 'uniqueness-subtrees': 'cn=config',
+ 'nsslapd-pluginEnabled': 'on'
+ })
topology_st.standalone.restart()
def fin():
if attruniq.exists():
- attruniq.disable()
attruniq.delete()
+ topology_st.standalone.restart()
request.addfinalizer(fin)
@@ -250,8 +259,8 @@ def test_modrdn_attr_uniqueness(topology_st, attruniq):
group1 = groups.create(properties={'cn': 'group1'})
group2 = groups.create(properties={'cn': 'group2'})
- attruniq.add_unique_attribute('mail')
- attruniq.add_unique_subtree(group2.dn)
+ attruniq.replace('uniqueness-attribute-name', 'mail')
+ attruniq.replace('uniqueness-subtrees', group2.dn)
attruniq.enable_all_subtrees()
log.debug(f'Enable PLUGIN_ATTR_UNIQUENESS plugin as "ON"')
attruniq.enable()
@@ -274,9 +283,6 @@ def test_modrdn_attr_uniqueness(topology_st, attruniq):
assert 'attribute value already exist' in str(excinfo.value)
log.debug(excinfo.value)
- log.debug('Move user2 to group1')
- user2.rename(f'uid={user2.rdn}', group1.dn)
-
user1.delete()
user2.delete()
@@ -302,17 +308,11 @@ def test_multiple_attr_uniqueness(topology_st, attruniq):
6. Should raise CONSTRAINT_VIOLATION
"""
- try:
- attruniq.add_unique_attribute('mail')
- attruniq.add_unique_attribute('mailAlternateAddress')
- attruniq.add_unique_subtree(DEFAULT_SUFFIX)
- attruniq.enable_all_subtrees()
- log.debug(f'Enable PLUGIN_ATTR_UNIQUENESS plugin as "ON"')
- attruniq.enable()
- except ldap.LDAPError as e:
- log.fatal('test_multiple_attribute_uniqueness: Failed to configure plugin for "mail": error {}'.format(e.args[0]['desc']))
- assert False
-
+ attruniq.replace('uniqueness-attribute-name', ['mail', 'mailAlternateAddress'])
+ attruniq.replace('uniqueness-subtrees', DEFAULT_SUFFIX)
+ attruniq.enable_all_subtrees()
+ log.debug(f'Enable PLUGIN_ATTR_UNIQUENESS plugin as "ON"')
+ attruniq.enable()
topology_st.standalone.restart()
users = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX)
@@ -383,8 +383,9 @@ def test_exclude_subtrees(topology_st, attruniq):
16. Success
17. Success
"""
- attruniq.add_unique_attribute('telephonenumber')
- attruniq.add_unique_subtree(DEFAULT_SUFFIX)
+ # Replace dummy config with actual test config
+ attruniq.replace('uniqueness-attribute-name', 'telephonenumber')
+ attruniq.replace('uniqueness-subtrees', DEFAULT_SUFFIX)
attruniq.enable_all_subtrees()
attruniq.enable()
topology_st.standalone.restart()
@@ -517,10 +518,18 @@ def test_matchingrule_attr(topology_st):
"""
inst = topology_st.standalone
-
attruniq = AttributeUniquenessPlugin(inst,
dn="cn=attribute uniqueness,cn=plugins,cn=config")
- attruniq.add_unique_attribute('cn:CaseExactMatch:')
+
+ if attruniq.exists():
+ attruniq.delete()
+ inst.restart()
+
+ attruniq.create(properties={
+ 'cn': 'attribute uniqueness',
+ 'uniqueness-attribute-name': 'cn:CaseExactMatch:',
+ 'uniqueness-subtrees': DEFAULT_SUFFIX
+ })
attruniq.enable_all_subtrees()
attruniq.enable()
inst.restart()
@@ -595,7 +604,7 @@ def test_one_container_add(topology_st, attruniq, containers, active_user_1):
active_2.delete()
log.info('Setup attribute uniqueness plugin for "cn" attribute')
- attruniq.add_unique_subtree(ACTIVE_DN)
+ attruniq.replace('uniqueness-subtrees', ACTIVE_DN)
attruniq.enable()
topology_st.standalone.restart()
@@ -628,7 +637,7 @@ def test_one_container_mod(topology_st, attruniq, containers,
"""
log.info('Setup attribute uniqueness plugin for "cn" attribute')
- attruniq.add_unique_subtree(ACTIVE_DN)
+ attruniq.replace('uniqueness-subtrees', ACTIVE_DN)
attruniq.enable()
topology_st.standalone.restart()
@@ -656,7 +665,7 @@ def test_one_container_modrdn(topology_st, attruniq, containers,
"""
log.info('Setup attribute uniqueness plugin for "cn" attribute')
- attruniq.add_unique_subtree(ACTIVE_DN)
+ attruniq.replace('uniqueness-subtrees', ACTIVE_DN)
attruniq.enable()
topology_st.standalone.restart()
@@ -690,8 +699,7 @@ def test_multiple_containers_add(topology_st, attruniq, containers,
"""
log.info('Setup attribute uniqueness plugin for "cn" attribute')
- attruniq.add_unique_subtree(ACTIVE_DN)
- attruniq.add_unique_subtree(STAGE_DN)
+ attruniq.replace('uniqueness-subtrees', [ACTIVE_DN, STAGE_DN])
attruniq.enable()
topology_st.standalone.restart()
@@ -789,8 +797,7 @@ def test_multiple_containers_mod(topology_st, attruniq, containers,
"""
log.info('Setup attribute uniqueness plugin for "cn" attribute')
- attruniq.add_unique_subtree(ACTIVE_DN)
- attruniq.add_unique_subtree(STAGE_DN)
+ attruniq.replace('uniqueness-subtrees', [ACTIVE_DN, STAGE_DN])
attruniq.enable()
topology_st.standalone.restart()
@@ -874,8 +881,8 @@ def test_multiple_containers_modrdn(topology_st, attruniq, containers,
"""
log.info('Setup attribute uniqueness plugin for "cn" attribute')
- attruniq.add_unique_subtree(ACTIVE_DN)
- attruniq.add_unique_subtree(STAGE_DN)
+ # Replace dummy subtree with actual test subtrees
+ attruniq.replace('uniqueness-subtrees', [ACTIVE_DN, STAGE_DN])
attruniq.enable()
topology_st.standalone.restart()
@@ -993,7 +1000,10 @@ def test_invalid_config_missing_attr_name(topology_st):
_config_file(topology_st, action='save')
attruniq = AttributeUniquenessPlugin(topology_st.standalone, dn="cn=attruniq,cn=plugins,cn=config")
- attruniq.create(properties={'cn': 'attruniq'})
+ attruniq.create(properties={
+ 'cn': 'attruniq',
+ 'uniqueness-subtrees': DEFAULT_SUFFIX
+ })
attruniq.enable()
topology_st.standalone.errorlog_file = open(topology_st.standalone.errlog, "r")
@@ -1040,9 +1050,11 @@ def test_invalid_config_invalid_subtree(topology_st):
_config_file(topology_st, action='save')
attruniq = AttributeUniquenessPlugin(topology_st.standalone, dn="cn=attruniq,cn=plugins,cn=config")
- attruniq.create(properties={'cn': 'attruniq'})
- attruniq.add_unique_attribute('cn')
- attruniq.add_unique_subtree('invalid_subtree')
+ attruniq.create(properties={
+ 'cn': 'attruniq',
+ 'uniqueness-attribute-name': 'cn',
+ 'uniqueness-subtrees': 'invalid_subtree'
+ })
attruniq.enable()
topology_st.standalone.errorlog_file = open(topology_st.standalone.errlog, "r")
diff --git a/dirsrvtests/tests/suites/replication/regression_m2_test.py b/dirsrvtests/tests/suites/replication/regression_m2_test.py
index ba1ffcc9c..db5140b0b 100644
--- a/dirsrvtests/tests/suites/replication/regression_m2_test.py
+++ b/dirsrvtests/tests/suites/replication/regression_m2_test.py
@@ -1221,6 +1221,10 @@ def test_rid_starting_with_0(topo_m2, request):
for replica,rid in zip(replicas, ['010', '020']):
replica.replace('nsDS5ReplicaId', rid)
+ # Restart required - replica IDs are loaded at startup and cached in memory
+ S1.restart()
+ S2.restart()
+
# Restore replica id in finalizer
def fin():
for replica,rid in zip(replicas, ['1', '2']):
diff --git a/dirsrvtests/tests/suites/replication/repl_log_monitoring_test.py b/dirsrvtests/tests/suites/replication/repl_log_monitoring_test.py
index 665fcb96f..b2b2d25a2 100644
--- a/dirsrvtests/tests/suites/replication/repl_log_monitoring_test.py
+++ b/dirsrvtests/tests/suites/replication/repl_log_monitoring_test.py
@@ -369,11 +369,13 @@ def test_replication_log_monitoring_multi_suffix(topo_m4):
repl.ensure_agreement(s1, s2)
repl.ensure_agreement(s2, s1)
- # Allow initial topology to settle before capturing metrics
+ # Wait for all the setup replication to settle, then clear the logs
for suffix in all_suffixes:
repl = ReplicationManager(suffix)
- repl.test_replication_topology(topo_m4)
-
+ for s1 in suppliers:
+ for s2 in suppliers:
+ if s1 != s2:
+ repl.wait_for_replication(s1, s2)
for supplier in suppliers:
supplier.deleteAccessLogs(restart=True)
diff --git a/dirsrvtests/tests/suites/webui/database/database_test.py b/dirsrvtests/tests/suites/webui/database/database_test.py
index ef105d262..05ddb6b00 100644
--- a/dirsrvtests/tests/suites/webui/database/database_test.py
+++ b/dirsrvtests/tests/suites/webui/database/database_test.py
@@ -138,6 +138,7 @@ def test_chaining_configuration_availability(topology_st, page, browser_name):
log.info('Click on Chaining Configuration and check if element is loaded.')
frame.get_by_role('tab', name='Database', exact=True).click()
frame.locator('#chaining-config').click()
+ frame.locator('#chaining-page').wait_for()
frame.locator('#defSizeLimit').wait_for()
assert frame.locator('#defSizeLimit').is_visible()
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c b/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c
index e3b7e5783..b1a29ff7f 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c
@@ -1080,7 +1080,9 @@ ldbm_back_modrdn(Slapi_PBlock *pb)
}
}
}
- if (slapi_sdn_get_dn(dn_newsuperiordn) != NULL) {
+ /* Only update parent if we're actually moving to a NEW parent (not the same parent) */
+ if (slapi_sdn_get_dn(dn_newsuperiordn) != NULL &&
+ slapi_sdn_compare(dn_newsuperiordn, &dn_parentdn) != 0) {
/* Push out the db modifications from the parent entry */
retval = modify_update_all(be, pb, &parent_modify_context, &txn);
if (DBI_RC_RETRY == retval) {
@@ -1335,11 +1337,6 @@ ldbm_back_modrdn(Slapi_PBlock *pb)
goto common_return;
error_return:
- /* Revert the caches if this is the parent operation */
- if (parent_op && betxn_callback_fails) {
- revert_cache(inst, &parent_time);
- }
-
/* result already sent above - just free stuff */
if (postentry) {
slapi_entry_free(postentry);
@@ -1417,13 +1414,6 @@ error_return:
slapi_pblock_set(pb, SLAPI_PLUGIN_OPRETURN, ldap_result_code ? &ldap_result_code : &retval);
}
slapi_pblock_get(pb, SLAPI_PB_RESULT_TEXT, &ldap_result_message);
-
- /* As it is a BETXN plugin failure then
- * revert the caches if this is the parent operation
- */
- if (parent_op) {
- revert_cache(inst, &parent_time);
- }
}
retval = plugin_call_mmr_plugin_postop(pb, NULL,SLAPI_PLUGIN_BE_TXN_POST_MODRDN_FN);
@@ -1437,6 +1427,15 @@ error_return:
}
}
+ /* Revert the caches if this is the parent operation and cache modifications were made.
+ * Cache modifications (via modify_switch_entries) only happen after BETXN PRE plugins succeed,
+ * so we should only revert if we got past that point (i.e., BETXN POST plugin failures).
+ * For BETXN PRE failures, no cache modifications were made to parent/newparent entries.
+ */
+ if (parent_op && betxn_callback_fails && postentry) {
+ revert_cache(inst, &parent_time);
+ }
+
common_return:
/* result code could be used in the bepost plugin functions. */
@@ -1482,12 +1481,22 @@ common_return:
"operation failed, the target entry is cleared from dncache (%s)\n", slapi_entry_get_dn(ec->ep_entry));
CACHE_REMOVE(&inst->inst_dncache, bdn);
CACHE_RETURN(&inst->inst_dncache, &bdn);
+
+ /* Also remove ec from entry cache and free it since the operation failed */
+ if (inst && cache_is_in_cache(&inst->inst_cache, ec)) {
+ CACHE_REMOVE(&inst->inst_cache, ec);
+ CACHE_RETURN(&inst->inst_cache, &ec);
+ } else {
+ /* ec was not in cache, just free it */
+ backentry_free(&ec);
+ }
+ ec = NULL;
}
if (ec && inst) {
CACHE_RETURN(&inst->inst_cache, &ec);
+ ec = NULL;
}
- ec = NULL;
}
if (inst) {
@@ -1817,6 +1826,8 @@ moddn_get_newdn(Slapi_PBlock *pb, Slapi_DN *dn_olddn, Slapi_DN *dn_newrdn, Slapi
/*
* Return the entries to the cache.
+ * For the original entry 'e', we should NOT remove it from cache on failure,
+ * as it's still a valid entry in the directory.
*/
static void
moddn_unlock_and_return_entry(
@@ -1825,12 +1836,9 @@ moddn_unlock_and_return_entry(
{
ldbm_instance *inst = (ldbm_instance *)be->be_instance_info;
- /* Something bad happened so we should give back all the entries */
+ /* Unlock and return the entry to the cache */
if (*targetentry != NULL) {
cache_unlock_entry(&inst->inst_cache, *targetentry);
- if (cache_is_in_cache(&inst->inst_cache, *targetentry)) {
- CACHE_REMOVE(&inst->inst_cache, *targetentry);
- }
CACHE_RETURN(&inst->inst_cache, targetentry);
*targetentry = NULL;
}
--
2.52.0

View File

@ -1,40 +0,0 @@
From 1ffcc9aa9a397180fe35283ee61b164471d073fb Mon Sep 17 00:00:00 2001
From: Thierry Bordaz <tbordaz@redhat.com>
Date: Tue, 7 Jan 2025 10:01:51 +0100
Subject: [PATCH] Issue 6417 - (2nd) fix typo
---
ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c b/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c
index 1bbb6252a..e2b8273a2 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c
@@ -1178,8 +1178,10 @@ entryrdn_lookup_dn(backend *be,
/* Just in case the suffix ID is not '1' retrieve it from the database */
keybuf = slapi_ch_strdup(slapi_sdn_get_ndn(be->be_suffix));
- dblayer_value_set(be, &key, keybuf, strlen(keybuf) + 1);
- rc = dblayer_cursor_op(&ctx.cursor, DBI_OP_MOVE_TO_KEY, &key, &data);
+ key.data = keybuf;
+ key.size = key.ulen = strlen(keybuf) + 1;
+ key.flags = DB_DBT_USERMEM;
+ rc = cursor->c_get(cursor, &key, &data, DB_SET);
if (rc) {
slapi_log_err(SLAPI_LOG_WARNING, "entryrdn_lookup_dn",
"Fails to retrieve the ID of suffix %s - keep the default value '%d'\n",
@@ -1189,8 +1191,8 @@ entryrdn_lookup_dn(backend *be,
elem = (rdn_elem *)data.data;
suffix_id = id_stored_to_internal(elem->rdn_elem_id);
}
- dblayer_value_free(be, &data);
- dblayer_value_free(be, &key);
+ slapi_ch_free(&data.data);
+ slapi_ch_free_string(&keybuf);
do {
/* Setting up a key for the node to get its parent */
--
2.48.0

View File

@ -0,0 +1,57 @@
From 04a0f5560ea741972b9c264af23b377893ab7133 Mon Sep 17 00:00:00 2001
From: Akshay Adhikari <aadhikar@redhat.com>
Date: Thu, 5 Feb 2026 15:41:09 +0530
Subject: [PATCH] Issue 7076 - Fix revert_cache() never called in modrdn
(#7220)
Description: The postentry check in PR #7077 was broken - postentry is always NULL
at that point, fixed by removing the check.
Relates: #7076
Reviewed by: @vashirov, @mreynolds389, @droideck (Thanks!)
---
ldap/servers/slapd/back-ldbm/ldbm_modrdn.c | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c b/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c
index b1a29ff7f..36377fc01 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c
@@ -103,6 +103,7 @@ ldbm_back_modrdn(Slapi_PBlock *pb)
Connection *pb_conn = NULL;
int32_t parent_op = 0;
int32_t betxn_callback_fails = 0; /* if a BETXN fails we need to revert entry cache */
+ int32_t cache_mod_phase = 0; /* set when we reach the cache modification phase */
struct timespec parent_time;
Slapi_Mods *smods_add_rdn = NULL;
@@ -1229,6 +1230,8 @@ ldbm_back_modrdn(Slapi_PBlock *pb)
goto error_return;
}
+ /* We're now past the BETXN PRE phase and entering the cache modification phase */
+ cache_mod_phase = 1;
postentry = slapi_entry_dup(ec->ep_entry);
if (parententry != NULL) {
@@ -1427,12 +1430,11 @@ error_return:
}
}
- /* Revert the caches if this is the parent operation and cache modifications were made.
- * Cache modifications (via modify_switch_entries) only happen after BETXN PRE plugins succeed,
- * so we should only revert if we got past that point (i.e., BETXN POST plugin failures).
- * For BETXN PRE failures, no cache modifications were made to parent/newparent entries.
+ /* Revert the caches if this is the parent operation AND we reached the
+ * cache modification phase. If BETXN PRE fails, cache_mod_phase is 0
+ * and we don't need to revert since no cache modifications were made.
*/
- if (parent_op && betxn_callback_fails && postentry) {
+ if (parent_op && betxn_callback_fails && cache_mod_phase) {
revert_cache(inst, &parent_time);
}
--
2.52.0

View File

@ -1,75 +0,0 @@
From 9e1284122a929fe14633a2aa6e2de4d72891f98f Mon Sep 17 00:00:00 2001
From: Thierry Bordaz <tbordaz@redhat.com>
Date: Mon, 13 Jan 2025 17:41:18 +0100
Subject: [PATCH] Issue 6417 - (3rd) If an entry RDN is identical to the
suffix, then Entryrdn gets broken during a reindex (#6480)
Bug description:
The previous fix had a flaw.
In case entryrdn_lookup_dn is called with an undefined suffix
the lookup of the suffix trigger a crash.
For example it can occur during internal search of an
unexisting map (view plugin).
The issue exists in all releases but is hidden since 2.3.
Fix description:
testing the suffix is defined
fixes: #6417
Reviewed by: Pierre Rogier (THnaks !)
---
ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c | 36 +++++++++++---------
1 file changed, 20 insertions(+), 16 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c b/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c
index e2b8273a2..01c77156f 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_entryrdn.c
@@ -1176,23 +1176,27 @@ entryrdn_lookup_dn(backend *be,
/* Setting the bulk fetch buffer */
data.flags = DB_DBT_MALLOC;
- /* Just in case the suffix ID is not '1' retrieve it from the database */
- keybuf = slapi_ch_strdup(slapi_sdn_get_ndn(be->be_suffix));
- key.data = keybuf;
- key.size = key.ulen = strlen(keybuf) + 1;
- key.flags = DB_DBT_USERMEM;
- rc = cursor->c_get(cursor, &key, &data, DB_SET);
- if (rc) {
- slapi_log_err(SLAPI_LOG_WARNING, "entryrdn_lookup_dn",
- "Fails to retrieve the ID of suffix %s - keep the default value '%d'\n",
- slapi_sdn_get_ndn(be->be_suffix),
- suffix_id);
- } else {
- elem = (rdn_elem *)data.data;
- suffix_id = id_stored_to_internal(elem->rdn_elem_id);
+ /* Just in case the suffix ID is not '1' retrieve it from the database
+ * if the suffix is not defined suffix_id remains '1'
+ */
+ if (be->be_suffix) {
+ keybuf = slapi_ch_strdup(slapi_sdn_get_ndn(be->be_suffix));
+ key.data = keybuf;
+ key.size = key.ulen = strlen(keybuf) + 1;
+ key.flags = DB_DBT_USERMEM;
+ rc = cursor->c_get(cursor, &key, &data, DB_SET);
+ if (rc) {
+ slapi_log_err(SLAPI_LOG_WARNING, "entryrdn_lookup_dn",
+ "Fails to retrieve the ID of suffix %s - keep the default value '%d'\n",
+ slapi_sdn_get_ndn(be->be_suffix),
+ suffix_id);
+ } else {
+ elem = (rdn_elem *) data.data;
+ suffix_id = id_stored_to_internal(elem->rdn_elem_id);
+ }
+ slapi_ch_free(&data.data);
+ slapi_ch_free_string(&keybuf);
}
- slapi_ch_free(&data.data);
- slapi_ch_free_string(&keybuf);
do {
/* Setting up a key for the node to get its parent */
--
2.48.0

View File

@ -0,0 +1,24 @@
From fb8acf6141b069e250be3821c1e70ee7ed448720 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Wed, 11 Feb 2026 19:53:44 +0100
Subject: [PATCH] Issue 6947 - Fix health_system_indexes_test.py
---
.../tests/suites/healthcheck/health_system_indexes_test.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
index 4cd4d0c70..3b3651b38 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -99,6 +99,7 @@ def run_healthcheck_and_flush_log(topology, instance, searched_code, json, searc
"memberof",
]
args.dry_run = False
+ args.exclude_check = []
# If we are using BDB as a backend, we will get error DSBLE0006 on new versions
if (
--
2.52.0

View File

@ -1,297 +0,0 @@
From d2f9dd82e3610ee9b73feea981c680c03bb21394 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Thu, 16 Jan 2025 08:42:53 -0500
Subject: [PATCH] Issue 6509 - Race condition with Paged Result searches
Description:
There is a race condition with Paged Result searches when a new operation comes
in while a paged search is finishing. This triggers an invalid time out error
and closes the connection with a T3 code.
The problem is that we do not use the "PagedResult lock" when checking the
connection's paged result data for a timeout event. This causes the paged
result timeout value to change unexpectedly and trigger a false timeout when a
new operation arrives.
Now we check the timeout without hte conn lock, if its expired it could
be a race condition and false positive. Try the lock again and test the
timeout. This also prevents blocking non-paged result searches from
getting held up by the lock when it's not necessary.
This also fixes some memory leaks that occur when an error happens.
Relates: https://github.com/389ds/389-ds-base/issues/6509
Reviewed by: tbordaz & proger (Thanks!!)
---
ldap/servers/slapd/daemon.c | 61 ++++++++++++++++++-------------
ldap/servers/slapd/opshared.c | 58 ++++++++++++++---------------
ldap/servers/slapd/pagedresults.c | 9 +++++
ldap/servers/slapd/slap.h | 2 +-
4 files changed, 75 insertions(+), 55 deletions(-)
diff --git a/ldap/servers/slapd/daemon.c b/ldap/servers/slapd/daemon.c
index bb80dae36..13dfe250d 100644
--- a/ldap/servers/slapd/daemon.c
+++ b/ldap/servers/slapd/daemon.c
@@ -1578,7 +1578,29 @@ setup_pr_read_pds(Connection_Table *ct)
if (c->c_state == CONN_STATE_FREE) {
connection_table_move_connection_out_of_active_list(ct, c);
} else {
- /* we try to acquire the connection mutex, if it is already
+ /* Check for a timeout for PAGED RESULTS */
+ if (pagedresults_is_timedout_nolock(c)) {
+ /*
+ * There could be a race condition so lets try again with the
+ * right lock
+ */
+ pthread_mutex_t *pr_mutex = pageresult_lock_get_addr(c);
+ if (pthread_mutex_trylock(pr_mutex) == EBUSY) {
+ c = next;
+ continue;
+ }
+ if (pagedresults_is_timedout_nolock(c)) {
+ pthread_mutex_unlock(pr_mutex);
+ disconnect_server(c, c->c_connid, -1,
+ SLAPD_DISCONNECT_PAGED_SEARCH_LIMIT,
+ 0);
+ } else {
+ pthread_mutex_unlock(pr_mutex);
+ }
+ }
+
+ /*
+ * we try to acquire the connection mutex, if it is already
* acquired by another thread, don't wait
*/
if (pthread_mutex_trylock(&(c->c_mutex)) == EBUSY) {
@@ -1586,35 +1608,24 @@ setup_pr_read_pds(Connection_Table *ct)
continue;
}
if (c->c_flags & CONN_FLAG_CLOSING) {
- /* A worker thread has marked that this connection
- * should be closed by calling disconnect_server.
- * move this connection out of the active list
- * the last thread to use the connection will close it
+ /*
+ * A worker thread, or paged result timeout, has marked that
+ * this connection should be closed by calling
+ * disconnect_server(). Move this connection out of the active
+ * list then the last thread to use the connection will close
+ * it.
*/
connection_table_move_connection_out_of_active_list(ct, c);
} else if (c->c_sd == SLAPD_INVALID_SOCKET) {
connection_table_move_connection_out_of_active_list(ct, c);
} else if (c->c_prfd != NULL) {
if ((!c->c_gettingber) && (c->c_threadnumber < c->c_max_threads_per_conn)) {
- int add_fd = 1;
- /* check timeout for PAGED RESULTS */
- if (pagedresults_is_timedout_nolock(c)) {
- /* Exceeded the paged search timelimit; disconnect the client */
- disconnect_server_nomutex(c, c->c_connid, -1,
- SLAPD_DISCONNECT_PAGED_SEARCH_LIMIT,
- 0);
- connection_table_move_connection_out_of_active_list(ct,
- c);
- add_fd = 0; /* do not poll on this fd */
- }
- if (add_fd) {
- ct->fd[count].fd = c->c_prfd;
- ct->fd[count].in_flags = SLAPD_POLL_FLAGS;
- /* slot i of the connection table is mapped to slot
- * count of the fds array */
- c->c_fdi = count;
- count++;
- }
+ ct->fd[listnum][count].fd = c->c_prfd;
+ ct->fd[listnum][count].in_flags = SLAPD_POLL_FLAGS;
+ /* slot i of the connection table is mapped to slot
+ * count of the fds array */
+ c->c_fdi = count;
+ count++;
} else {
if (c->c_threadnumber >= c->c_max_threads_per_conn) {
c->c_maxthreadsblocked++;
@@ -1675,7 +1686,7 @@ handle_pr_read_ready(Connection_Table *ct, PRIntn num_poll __attribute__((unused
continue;
}
- /* Try to get connection mutex, if not available just skip the connection and
+ /* Try to get connection mutex, if not available just skip the connection and
* process other connections events. May generates cpu load for listening thread
* if connection mutex is held for a long time
*/
diff --git a/ldap/servers/slapd/opshared.c b/ldap/servers/slapd/opshared.c
index 7ab4117cd..a29eed052 100644
--- a/ldap/servers/slapd/opshared.c
+++ b/ldap/servers/slapd/opshared.c
@@ -250,7 +250,7 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
char *errtext = NULL;
int nentries, pnentries;
int flag_search_base_found = 0;
- int flag_no_such_object = 0;
+ bool flag_no_such_object = false;
int flag_referral = 0;
int flag_psearch = 0;
int err_code = LDAP_SUCCESS;
@@ -315,7 +315,7 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
rc = -1;
goto free_and_return_nolock;
}
-
+
/* Set the time we actually started the operation */
slapi_operation_set_time_started(operation);
@@ -798,11 +798,11 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
}
/* subtree searches :
- * if the search was started above the backend suffix
- * - temporarily set the SLAPI_SEARCH_TARGET_SDN to the
- * base of the node so that we don't get a NO SUCH OBJECT error
- * - do not change the scope
- */
+ * if the search was started above the backend suffix
+ * - temporarily set the SLAPI_SEARCH_TARGET_SDN to the
+ * base of the node so that we don't get a NO SUCH OBJECT error
+ * - do not change the scope
+ */
if (scope == LDAP_SCOPE_SUBTREE) {
if (slapi_sdn_issuffix(be_suffix, basesdn)) {
if (free_sdn) {
@@ -825,53 +825,53 @@ op_shared_search(Slapi_PBlock *pb, int send_result)
switch (rc) {
case 1:
/* if the backend returned LDAP_NO_SUCH_OBJECT for a SEARCH request,
- * it will not have sent back a result - otherwise, it will have
- * sent a result */
+ * it will not have sent back a result - otherwise, it will have
+ * sent a result */
rc = SLAPI_FAIL_GENERAL;
slapi_pblock_get(pb, SLAPI_RESULT_CODE, &err);
if (err == LDAP_NO_SUCH_OBJECT) {
/* may be the object exist somewhere else
- * wait the end of the loop to send back this error
- */
- flag_no_such_object = 1;
+ * wait the end of the loop to send back this error
+ */
+ flag_no_such_object = true;
} else {
/* err something other than LDAP_NO_SUCH_OBJECT, so the backend will
- * have sent the result -
- * Set a flag here so we don't return another result. */
+ * have sent the result -
+ * Set a flag here so we don't return another result. */
sent_result = 1;
}
- /* fall through */
+ /* fall through */
case -1: /* an error occurred */
+ slapi_pblock_get(pb, SLAPI_RESULT_CODE, &err);
/* PAGED RESULTS */
if (op_is_pagedresults(operation)) {
/* cleanup the slot */
pthread_mutex_lock(pagedresults_mutex);
+ if (err != LDAP_NO_SUCH_OBJECT && !flag_no_such_object) {
+ /* Free the results if not "no_such_object" */
+ void *sr = NULL;
+ slapi_pblock_get(pb, SLAPI_SEARCH_RESULT_SET, &sr);
+ be->be_search_results_release(&sr);
+ }
pagedresults_set_search_result(pb_conn, operation, NULL, 1, pr_idx);
rc = pagedresults_set_current_be(pb_conn, NULL, pr_idx, 1);
pthread_mutex_unlock(pagedresults_mutex);
}
- if (1 == flag_no_such_object) {
- break;
- }
- slapi_pblock_get(pb, SLAPI_RESULT_CODE, &err);
- if (err == LDAP_NO_SUCH_OBJECT) {
- /* may be the object exist somewhere else
- * wait the end of the loop to send back this error
- */
- flag_no_such_object = 1;
+
+ if (err == LDAP_NO_SUCH_OBJECT || flag_no_such_object) {
+ /* Maybe the object exists somewhere else, wait to the end
+ * of the loop to send back this error */
+ flag_no_such_object = true;
break;
} else {
- /* for error other than LDAP_NO_SUCH_OBJECT
- * the error has already been sent
- * stop the search here
- */
+ /* For error other than LDAP_NO_SUCH_OBJECT the error has
+ * already been sent stop the search here */
cache_return_target_entry(pb, be, operation);
goto free_and_return;
}
/* when rc == SLAPI_FAIL_DISKFULL this case is executed */
-
case SLAPI_FAIL_DISKFULL:
operation_out_of_disk_space();
cache_return_target_entry(pb, be, operation);
diff --git a/ldap/servers/slapd/pagedresults.c b/ldap/servers/slapd/pagedresults.c
index db87e486e..4aa1fa3e5 100644
--- a/ldap/servers/slapd/pagedresults.c
+++ b/ldap/servers/slapd/pagedresults.c
@@ -121,12 +121,15 @@ pagedresults_parse_control_value(Slapi_PBlock *pb,
if (ber_scanf(ber, "{io}", pagesize, &cookie) == LBER_ERROR) {
slapi_log_err(SLAPI_LOG_ERR, "pagedresults_parse_control_value",
"<= corrupted control value\n");
+ ber_free(ber, 1);
return LDAP_PROTOCOL_ERROR;
}
if (!maxreqs) {
slapi_log_err(SLAPI_LOG_ERR, "pagedresults_parse_control_value",
"Simple paged results requests per conn exceeded the limit: %d\n",
maxreqs);
+ ber_free(ber, 1);
+ slapi_ch_free_string(&cookie.bv_val);
return LDAP_UNWILLING_TO_PERFORM;
}
@@ -376,6 +379,10 @@ pagedresults_free_one_msgid(Connection *conn, ber_int_t msgid, pthread_mutex_t *
}
prp->pr_flags |= CONN_FLAG_PAGEDRESULTS_ABANDONED;
prp->pr_flags &= ~CONN_FLAG_PAGEDRESULTS_PROCESSING;
+ if (conn->c_pagedresults.prl_count > 0) {
+ _pr_cleanup_one_slot(prp);
+ conn->c_pagedresults.prl_count--;
+ }
rc = 0;
break;
}
@@ -940,7 +947,9 @@ pagedresults_is_timedout_nolock(Connection *conn)
return 1;
}
}
+
slapi_log_err(SLAPI_LOG_TRACE, "<-- pagedresults_is_timedout", "<= false 2\n");
+
return 0;
}
diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h
index 072f6f962..469874fd1 100644
--- a/ldap/servers/slapd/slap.h
+++ b/ldap/servers/slapd/slap.h
@@ -74,7 +74,7 @@ static char ptokPBE[34] = "Internal (Software) Token ";
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
-
+#include <stdbool.h>
#include <time.h> /* For timespec definitions */
/* Provides our int types and platform specific requirements. */
--
2.48.0

View File

@ -0,0 +1,71 @@
From 8761b28fd2f2cf3d2e2119bdf1b348bc7d2d1834 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Mon, 9 Feb 2026 13:18:09 +0100
Subject: [PATCH] Issue 7121 - (2nd) LeakSanitizer: various leaks during
replication (#7212)
Bug Description:
With the previous fix 75e0e487545893a7b0d83f94f9264c10f8bb0353 applied,
server can crash in ber_bvcpy.
```
Program terminated with signal SIGSEGV, Segmentation fault.
#0 ber_bvcpy (bvs=0x7f1d00000000, bvd=0x7f1da2cd73c0) at ldap/servers/slapd/value.c:47
47 len = bvs->bv_len;
[Current thread is 1 (Thread 0x7f1db47fe640 (LWP 36576))]
(gdb) bt
#0 ber_bvcpy (bvs=0x7f1d00000000, bvd=0x7f1da2cd73c0) at ldap/servers/slapd/value.c:47
#1 ber_bvcpy (bvs=0x7f1d00000000, bvd=0x7f1da2cd73c0) at ldap/servers/slapd/value.c:40
#2 slapi_value_set_berval (bval=0x7f1d00000000, value=0x7f1da2cd73c0) at ldap/servers/slapd/value.c:322
#3 slapi_value_set_berval (value=value@entry=0x7f1da2cd73c0, bval=bval@entry=0x7f1d00000000) at ldap/servers/slapd/value.c:317
#4 0x00007f1e48b7d787 in value_init (v=v@entry=0x7f1da2cd73c0, bval=bval@entry=0x7f1d00000000, t=t@entry=0 '\000', csn=csn@entry=0x0)
at ldap/servers/slapd/value.c:179
#5 0x00007f1e48b7d884 in value_new (bval=bval@entry=0x7f1d00000000, t=t@entry=0 '\000', csn=csn@entry=0x0) at ldap/servers/slapd/value.c:158
#6 0x00007f1e48b7ddb7 in slapi_value_dup (v=0x7f1d00000000) at ldap/servers/slapd/value.c:147
#7 0x00007f1e48b7e262 in valueset_set_valueset (vs2=0x7f1d502b5218, vs1=0x7f1da2c5b358) at ldap/servers/slapd/valueset.c:1244
#8 valueset_set_valueset (vs1=0x7f1da2c5b358, vs2=0x7f1d502b5218) at ldap/servers/slapd/valueset.c:1220
#9 0x00007f1e48add4af in slapi_attr_dup (attr=0x7f1d502b51e0) at ldap/servers/slapd/attr.c:396
#10 0x00007f1e48af0f60 in slapi_entry_dup (e=0x7f1da2c19000) at ldap/servers/slapd/entry.c:2036
#11 0x00007f1e442c734e in ldbm_back_modify (pb=0x7f1da2c00000) at ldap/servers/slapd/back-ldbm/ldbm_modify.c:741
#12 0x00007f1e48b30076 in op_shared_modify (pb=pb@entry=0x7f1da2c00000, pw_change=pw_change@entry=0, old_pw=0x0)
at ldap/servers/slapd/modify.c:1079
#13 0x00007f1e48b30ced in do_modify (pb=pb@entry=0x7f1da2c00000) at ldap/servers/slapd/modify.c:377
#14 0x000055e990e2fd1c in connection_dispatch_operation (pb=0x7f1da2c00000, op=<optimized out>, conn=<optimized out>)
at ldap/servers/slapd/connection.c:672
#15 connection_threadmain (arg=<optimized out>) at ldap/servers/slapd/connection.c:1955
#16 0x00007f1e48839bd4 in _pt_root (arg=0x7f1e439d9500) at pthreads/../../../../nspr/pr/src/pthreads/ptthread.c:191
#17 0x00007f1e4868a19a in start_thread (arg=<optimized out>) at pthread_create.c:443
#18 0x00007f1e4870f100 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81
```
The fix changed from always setting `v_csnset = NULL` to only freeing it
inside the if-block.
Fix Description:
Keep `csnset_free()` outside the if-block to handle all values, not just
those matching the condtion.
Related: https://github.com/389ds/389-ds-base/issues/7121
Reviewed by: @progier389, @droideck (Thanks!)
---
ldap/servers/slapd/entrywsi.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ldap/servers/slapd/entrywsi.c b/ldap/servers/slapd/entrywsi.c
index e1bdc1bab..0d044092d 100644
--- a/ldap/servers/slapd/entrywsi.c
+++ b/ldap/servers/slapd/entrywsi.c
@@ -1185,8 +1185,8 @@ resolve_attribute_state_deleted_to_present(Slapi_Entry *e, Slapi_Attr *a, Slapi_
if ((csn_compare(vucsn, deletedcsn) >= 0) ||
value_distinguished_at_csn(e, a, valuestoupdate[i], deletedcsn)) {
entry_deleted_value_to_present_value(a, valuestoupdate[i]);
- csnset_free(&valuestoupdate[i]->v_csnset);
}
+ csnset_free(&valuestoupdate[i]->v_csnset);
}
}
}
--
2.52.0

View File

@ -1,29 +0,0 @@
From 27cd055197bc3cae458a1f86621aa5410c66dd2c Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Mon, 20 Jan 2025 15:51:24 -0500
Subject: [PATCH] Issue 6509 - Fix cherry pick issue (race condition in Paged
results)
Relates: https://github.com/389ds/389-ds-base/issues/6509
---
ldap/servers/slapd/daemon.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ldap/servers/slapd/daemon.c b/ldap/servers/slapd/daemon.c
index 13dfe250d..57e07e5f5 100644
--- a/ldap/servers/slapd/daemon.c
+++ b/ldap/servers/slapd/daemon.c
@@ -1620,8 +1620,8 @@ setup_pr_read_pds(Connection_Table *ct)
connection_table_move_connection_out_of_active_list(ct, c);
} else if (c->c_prfd != NULL) {
if ((!c->c_gettingber) && (c->c_threadnumber < c->c_max_threads_per_conn)) {
- ct->fd[listnum][count].fd = c->c_prfd;
- ct->fd[listnum][count].in_flags = SLAPD_POLL_FLAGS;
+ ct->fd[count].fd = c->c_prfd;
+ ct->fd[count].in_flags = SLAPD_POLL_FLAGS;
/* slot i of the connection table is mapped to slot
* count of the fds array */
c->c_fdi = count;
--
2.48.0

View File

@ -0,0 +1,551 @@
From 86b15d688949197a9efe3d5fc7a7eadcdd46e115 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Tue, 16 Dec 2025 15:48:35 -0800
Subject: [PATCH] Issue 7150 - Compressed access log rotations skipped,
accesslog-list out of sync (#7151)
Description: Accept `.gz`-suffixed rotated log filenames when
rebuilding rotation info and checking previous logs, preventing
compressed rotations from being dropped from the internal list.
Add regression tests to stress log rotation with compression,
verify `nsslapd-accesslog-list` stays in sync, and guard against
crashes when flushing buffered logs during rotation.
Minor doc fix in test.
Fixes: https://github.com/389ds/389-ds-base/issues/7150
Reviewed by: @progier389 (Thanks!)
---
.../suites/logging/log_flush_rotation_test.py | 341 +++++++++++++++++-
ldap/servers/slapd/log.c | 99 +++--
2 files changed, 402 insertions(+), 38 deletions(-)
diff --git a/dirsrvtests/tests/suites/logging/log_flush_rotation_test.py b/dirsrvtests/tests/suites/logging/log_flush_rotation_test.py
index b33a622e1..864ba9c5d 100644
--- a/dirsrvtests/tests/suites/logging/log_flush_rotation_test.py
+++ b/dirsrvtests/tests/suites/logging/log_flush_rotation_test.py
@@ -6,6 +6,7 @@
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---
#
+import glob
import os
import logging
import time
@@ -13,14 +14,351 @@ import pytest
from lib389._constants import DEFAULT_SUFFIX, PW_DM
from lib389.tasks import ImportTask
from lib389.idm.user import UserAccounts
+from lib389.idm.domain import Domain
+from lib389.idm.directorymanager import DirectoryManager
from lib389.topologies import topology_st as topo
log = logging.getLogger(__name__)
+def remove_rotated_access_logs(inst):
+ """
+ Remove all rotated access log files to start fresh for each test.
+ This prevents log files from previous tests affecting current test results.
+ """
+ log_dir = inst.get_log_dir()
+ patterns = [
+ f'{log_dir}/access.2*', # Uncompressed rotated logs
+ f'{log_dir}/access.*.gz', # Compressed rotated logs
+ ]
+ for pattern in patterns:
+ for log_file in glob.glob(pattern):
+ try:
+ os.remove(log_file)
+ log.info(f"Removed old log file: {log_file}")
+ except OSError as e:
+ log.warning(f"Could not remove {log_file}: {e}")
+
+
+def reset_access_log_config(inst):
+ """
+ Reset access log configuration to default values.
+ """
+ inst.config.set('nsslapd-accesslog-compress', 'off')
+ inst.config.set('nsslapd-accesslog-maxlogsize', '100')
+ inst.config.set('nsslapd-accesslog-maxlogsperdir', '10')
+ inst.config.set('nsslapd-accesslog-logrotationsync-enabled', 'off')
+ inst.config.set('nsslapd-accesslog-logbuffering', 'on')
+ inst.config.set('nsslapd-accesslog-logexpirationtime', '-1')
+ inst.config.set('nsslapd-accesslog-logminfreediskspace', '5')
+
+
+def generate_heavy_load(inst, suffix, iterations=50):
+ """
+ Generate heavy LDAP load to fill access log quickly.
+ Performs multiple operations: searches, modifies, binds to populate logs.
+ """
+ for i in range(iterations):
+ suffix.replace('description', f'iteration_{i}')
+ suffix.get_attr_val('description')
+
+
+def count_access_logs(log_dir, compressed_only=False):
+ """
+ Count access log files in the log directory.
+ Returns count of rotated access logs (not including the active 'access' file).
+ """
+ if compressed_only:
+ pattern = f'{log_dir}/access.*.gz'
+ else:
+ pattern = f'{log_dir}/access.2*'
+ log_files = glob.glob(pattern)
+ return len(log_files)
+
+
+def test_log_pileup_with_compression(topo):
+ """Test that log rotation properly deletes old logs when compression is enabled.
+
+ :id: fa1bfce8-b6d3-4520-a0a8-bead14fa5838
+ :setup: Standalone Instance
+ :steps:
+ 1. Clean up existing rotated logs and reset configuration
+ 2. Enable access log compression
+ 3. Set strict log limits (small maxlogsperdir)
+ 4. Disable log expiration to test count-based deletion
+ 5. Generate heavy load to create many log rotations
+ 6. Verify log count does not exceed maxlogsperdir limit
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. Success
+ 6. Log count should be at or below maxlogsperdir + small buffer
+ """
+
+ inst = topo.standalone
+ suffix = Domain(inst, DEFAULT_SUFFIX)
+ log_dir = inst.get_log_dir()
+
+ # Clean up before test
+ remove_rotated_access_logs(inst)
+ reset_access_log_config(inst)
+ inst.restart()
+
+ max_logs = 5
+ inst.config.set('nsslapd-accesslog-compress', 'on')
+ inst.config.set('nsslapd-accesslog-maxlogsperdir', str(max_logs))
+ inst.config.set('nsslapd-accesslog-maxlogsize', '1') # 1MB to trigger rotation
+ inst.config.set('nsslapd-accesslog-logrotationsync-enabled', 'off')
+ inst.config.set('nsslapd-accesslog-logbuffering', 'off')
+
+ inst.config.set('nsslapd-accesslog-logexpirationtime', '-1')
+
+ inst.config.set('nsslapd-accesslog-logminfreediskspace', '5')
+
+ inst.restart()
+ time.sleep(2)
+
+ target_logs = max_logs * 3
+ for i in range(target_logs):
+ log.info(f"Generating load for log rotation {i+1}/{target_logs}")
+ generate_heavy_load(inst, suffix, iterations=150)
+ time.sleep(1) # Wait for rotation
+
+ time.sleep(3)
+
+ logs_on_disk = count_access_logs(log_dir)
+ log.info(f"Configured maxlogsperdir: {max_logs}")
+ log.info(f"Actual rotated logs on disk: {logs_on_disk}")
+
+ all_access_logs = glob.glob(f'{log_dir}/access*')
+ log.info(f"All access log files: {all_access_logs}")
+
+ max_allowed = max_logs + 2
+ assert logs_on_disk <= max_allowed, (
+ f"Log rotation failed to delete old files! "
+ f"Expected at most {max_allowed} rotated logs (maxlogsperdir={max_logs} + 2 buffer), "
+ f"but found {logs_on_disk}. The server has lost track of the file list."
+ )
+
+
+@pytest.mark.parametrize("compress_enabled", ["on", "off"])
+def test_accesslog_list_mismatch(topo, compress_enabled):
+ """Test that nsslapd-accesslog-list stays synchronized with actual log files.
+
+ :id: 0a8a46a6-cae7-43bd-8b64-5e3481480cd3
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Clean up existing rotated logs and reset configuration
+ 2. Configure log rotation with compression enabled/disabled
+ 3. Generate activity to trigger multiple rotations
+ 4. Get the nsslapd-accesslog-list attribute
+ 5. Compare with actual files on disk
+ 6. Verify they match (accounting for .gz extension when enabled)
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. Success
+ 6. The list attribute should match actual files on disk
+ """
+
+ inst = topo.standalone
+ suffix = Domain(inst, DEFAULT_SUFFIX)
+ log_dir = inst.get_log_dir()
+ compression_on = compress_enabled == "on"
+
+ # Clean up before test
+ remove_rotated_access_logs(inst)
+ reset_access_log_config(inst)
+ inst.restart()
+
+ inst.config.set('nsslapd-accesslog-compress', compress_enabled)
+ inst.config.set('nsslapd-accesslog-maxlogsize', '1')
+ inst.config.set('nsslapd-accesslog-maxlogsperdir', '10')
+ inst.config.set('nsslapd-accesslog-logrotationsync-enabled', 'off')
+ inst.config.set('nsslapd-accesslog-logbuffering', 'off')
+ inst.config.set('nsslapd-accesslog-logexpirationtime', '-1')
+
+ inst.restart()
+ time.sleep(2)
+
+ for i in range(15):
+ suffix_note = "(no compression)" if not compression_on else ""
+ log.info(f"Generating load for rotation {i+1}/15 {suffix_note}")
+ generate_heavy_load(inst, suffix, iterations=150)
+ time.sleep(1)
+
+ time.sleep(3)
+
+ accesslog_list = inst.config.get_attr_vals_utf8('nsslapd-accesslog-list')
+ log.info(f"nsslapd-accesslog-list entries (compress={compress_enabled}): {len(accesslog_list)}")
+ log.info(f"nsslapd-accesslog-list (compress={compress_enabled}): {accesslog_list}")
+
+ disk_files = glob.glob(f'{log_dir}/access.2*')
+ log.info(f"Actual files on disk (compress={compress_enabled}): {len(disk_files)}")
+ log.info(f"Disk files (compress={compress_enabled}): {disk_files}")
+
+ disk_files_for_compare = set()
+ for fpath in disk_files:
+ if compression_on and fpath.endswith('.gz'):
+ disk_files_for_compare.add(fpath[:-3])
+ else:
+ disk_files_for_compare.add(fpath)
+
+ list_files_set = set(accesslog_list)
+ missing_from_disk = list_files_set - disk_files_for_compare
+ extra_on_disk = disk_files_for_compare - list_files_set
+
+ if missing_from_disk:
+ log.error(
+ f"[compress={compress_enabled}] Files in list but NOT on disk: {missing_from_disk}"
+ )
+ if extra_on_disk:
+ log.warning(
+ f"[compress={compress_enabled}] Files on disk but NOT in list: {extra_on_disk}"
+ )
+
+ assert not missing_from_disk, (
+ f"nsslapd-accesslog-list mismatch (compress={compress_enabled})! "
+ f"Files listed but missing from disk: {missing_from_disk}. "
+ f"This indicates the server's internal list is out of sync with actual files."
+ )
+
+ if len(extra_on_disk) > 2:
+ log.warning(
+ f"Potential log tracking issue (compress={compress_enabled}): "
+ f"{len(extra_on_disk)} files on disk are not tracked in the accesslog-list: "
+ f"{extra_on_disk}"
+ )
+
+
+def test_accesslog_list_mixed_compression(topo):
+ """Test that nsslapd-accesslog-list correctly tracks both compressed and uncompressed logs.
+
+ :id: 11b088cd-23be-407d-ad16-4ce2e12da09e
+ :setup: Standalone Instance
+ :steps:
+ 1. Clean up existing rotated logs and reset configuration
+ 2. Create rotated logs with compression OFF
+ 3. Enable compression and create more rotated logs
+ 4. Get the nsslapd-accesslog-list attribute
+ 5. Compare with actual files on disk
+ 6. Verify all files are correctly tracked (uncompressed and compressed)
+ :expectedresults:
+ 1. Success
+ 2. Success - uncompressed rotated logs created
+ 3. Success - compressed rotated logs created
+ 4. Success
+ 5. Success
+ 6. The list should contain base filenames (without .gz) that
+ correspond to files on disk (either as-is or with .gz suffix)
+ """
+
+ inst = topo.standalone
+ suffix = Domain(inst, DEFAULT_SUFFIX)
+ log_dir = inst.get_log_dir()
+
+ # Clean up before test
+ remove_rotated_access_logs(inst)
+ reset_access_log_config(inst)
+ inst.restart()
+
+ inst.config.set('nsslapd-accesslog-compress', 'off')
+ inst.config.set('nsslapd-accesslog-maxlogsize', '1')
+ inst.config.set('nsslapd-accesslog-maxlogsperdir', '20')
+ inst.config.set('nsslapd-accesslog-logrotationsync-enabled', 'off')
+ inst.config.set('nsslapd-accesslog-logbuffering', 'off')
+ inst.config.set('nsslapd-accesslog-logexpirationtime', '-1')
+
+ inst.restart()
+ time.sleep(2)
+
+ for i in range(15):
+ log.info(f"Generating load for uncompressed rotation {i+1}/15")
+ generate_heavy_load(inst, suffix, iterations=150)
+ time.sleep(1)
+
+ time.sleep(2)
+
+ # Check what we have so far
+ uncompressed_files = glob.glob(f'{log_dir}/access.2*')
+ log.info(f"Files on disk after uncompressed phase: {uncompressed_files}")
+
+ inst.config.set('nsslapd-accesslog-compress', 'on')
+ inst.restart()
+ time.sleep(2)
+
+ for i in range(15):
+ log.info(f"Generating load for compressed rotation {i+1}/15")
+ generate_heavy_load(inst, suffix, iterations=150)
+ time.sleep(1)
+
+ time.sleep(3)
+
+ accesslog_list = inst.config.get_attr_vals_utf8('nsslapd-accesslog-list')
+
+ disk_files = glob.glob(f'{log_dir}/access.2*')
+
+ log.info(f"nsslapd-accesslog-list entries: {len(accesslog_list)}")
+ log.info(f"nsslapd-accesslog-list: {sorted(accesslog_list)}")
+ log.info(f"Actual files on disk: {len(disk_files)}")
+ log.info(f"Disk files: {sorted(disk_files)}")
+
+ compressed_on_disk = [f for f in disk_files if f.endswith('.gz')]
+ uncompressed_on_disk = [f for f in disk_files if not f.endswith('.gz')]
+ log.info(f"Compressed files on disk: {compressed_on_disk}")
+ log.info(f"Uncompressed files on disk: {uncompressed_on_disk}")
+
+ list_files_set = set(accesslog_list)
+
+ disk_files_base = set()
+ for fpath in disk_files:
+ if fpath.endswith('.gz'):
+ disk_files_base.add(fpath[:-3]) # Strip .gz
+ else:
+ disk_files_base.add(fpath)
+
+ missing_from_disk = list_files_set - disk_files_base
+
+ extra_on_disk = disk_files_base - list_files_set
+
+ if missing_from_disk:
+ log.error(f"Files in list but NOT on disk: {missing_from_disk}")
+ if extra_on_disk:
+ log.warning(f"Files on disk but NOT in list: {extra_on_disk}")
+
+ assert not missing_from_disk, (
+ f"nsslapd-accesslog-list contains stale entries! "
+ f"Files in list but not on disk (as base or .gz): {missing_from_disk}"
+ )
+
+ for list_file in accesslog_list:
+ exists_uncompressed = os.path.exists(list_file)
+ exists_compressed = os.path.exists(list_file + '.gz')
+ assert exists_uncompressed or exists_compressed, (
+ f"File in accesslog-list does not exist on disk: {list_file} "
+ f"(checked both {list_file} and {list_file}.gz)"
+ )
+ if exists_compressed and not exists_uncompressed:
+ log.info(f" {list_file} -> exists as .gz (compressed)")
+ elif exists_uncompressed:
+ log.info(f" {list_file} -> exists (uncompressed)")
+
+ if len(extra_on_disk) > 1:
+ log.warning(
+ f"Some files on disk are not tracked in accesslog-list: {extra_on_disk}"
+ )
+
+ log.info("Mixed compression test completed successfully")
+
+
def test_log_flush_and_rotation_crash(topo):
- """Make sure server does not crash whening flushing a buffer and rotating
+ """Make sure server does not crash when flushing a buffer and rotating
the log at the same time
:id: d4b0af2f-48b2-45f5-ae8b-f06f692c3133
@@ -36,6 +374,7 @@ def test_log_flush_and_rotation_crash(topo):
3. Success
4. Success
"""
+ # NOTE: This test is placed last as it may affect the suffix state.
inst = topo.standalone
diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c
index 6f57a3d9c..ca8d481e5 100644
--- a/ldap/servers/slapd/log.c
+++ b/ldap/servers/slapd/log.c
@@ -135,6 +135,7 @@ static void vslapd_log_emergency_error(LOGFD fp, const char *msg, int locked);
static int get_syslog_loglevel(int loglevel);
static void log_external_libs_debug_openldap_print(char *buffer);
static int log__fix_rotationinfof(char *pathname);
+static int log__validate_rotated_logname(const char *timestamp_str, PRBool *is_compressed);
static int
get_syslog_loglevel(int loglevel)
@@ -410,7 +411,7 @@ g_log_init()
loginfo.log_security_fdes = NULL;
loginfo.log_security_file = NULL;
loginfo.log_securityinfo_file = NULL;
- loginfo.log_numof_access_logs = 1;
+ loginfo.log_numof_security_logs = 1;
loginfo.log_security_logchain = NULL;
loginfo.log_security_buffer = log_create_buffer(LOG_BUFFER_MAXSIZE);
loginfo.log_security_compress = cfg->securitylog_compress;
@@ -3311,7 +3312,7 @@ log__open_accesslogfile(int logfile_state, int locked)
}
} else if (loginfo.log_access_compress) {
if (compress_log_file(newfile, loginfo.log_access_mode) != 0) {
- slapi_log_err(SLAPI_LOG_ERR, "log__open_auditfaillogfile",
+ slapi_log_err(SLAPI_LOG_ERR, "log__open_accesslogfile",
"failed to compress rotated access log (%s)\n",
newfile);
} else {
@@ -4710,6 +4711,50 @@ log__delete_rotated_logs()
loginfo.log_error_logchain = NULL;
}
+/*
+ * log__validate_rotated_logname
+ *
+ * Validates that a log filename timestamp suffix matches the expected format:
+ * YYYYMMDD-HHMMSS (15 chars) or YYYYMMDD-HHMMSS.gz (18 chars) for compressed files.
+ * Uses regex pattern: ^[0-9]{8}-[0-9]{6}(\.gz)?$
+ *
+ * \param timestamp_str The timestamp portion of the log filename (after the first '.')
+ * \param is_compressed Output parameter set to PR_TRUE if the file has .gz suffix
+ * \return 1 if valid, 0 if invalid
+ */
+static int
+log__validate_rotated_logname(const char *timestamp_str, PRBool *is_compressed)
+{
+ Slapi_Regex *re = NULL;
+ char *re_error = NULL;
+ int rc = 0;
+
+ /* Match YYYYMMDD-HHMMSS with optional .gz suffix */
+ static const char *pattern = "^[0-9]{8}-[0-9]{6}(\\.gz)?$";
+
+ *is_compressed = PR_FALSE;
+
+ re = slapi_re_comp(pattern, &re_error);
+ if (re == NULL) {
+ slapi_log_err(SLAPI_LOG_ERR, "log__validate_rotated_logname",
+ "Failed to compile regex: %s\n", re_error ? re_error : "unknown error");
+ slapi_ch_free_string(&re_error);
+ return 0;
+ }
+
+ rc = slapi_re_exec_nt(re, timestamp_str);
+ if (rc == 1) {
+ /* Check if compressed by looking for .gz suffix */
+ size_t len = strlen(timestamp_str);
+ if (len >= 3 && strcmp(timestamp_str + len - 3, ".gz") == 0) {
+ *is_compressed = PR_TRUE;
+ }
+ }
+
+ slapi_re_free(re);
+ return rc == 1 ? 1 : 0;
+}
+
#define ERRORSLOG 1
#define ACCESSLOG 2
#define AUDITLOG 3
@@ -4792,31 +4837,19 @@ log__fix_rotationinfof(char *pathname)
}
} else if (0 == strncmp(log_type, dirent->name, strlen(log_type)) &&
(p = strchr(dirent->name, '.')) != NULL &&
- NULL != strchr(p, '-')) /* e.g., errors.20051123-165135 */
+ NULL != strchr(p, '-')) /* e.g., errors.20051123-165135 or errors.20051123-165135.gz */
{
struct logfileinfo *logp;
- char *q;
- int ignoreit = 0;
-
- for (q = ++p; q && *q; q++) {
- if (*q != '-' &&
- *q != '.' && /* .gz */
- *q != 'g' &&
- *q != 'z' &&
- !isdigit(*q))
- {
- ignoreit = 1;
- }
- }
- if (ignoreit || (q - p != 15)) {
+ PRBool is_compressed = PR_FALSE;
+
+ /* Skip the '.' to get the timestamp portion */
+ p++;
+ if (!log__validate_rotated_logname(p, &is_compressed)) {
continue;
}
logp = (struct logfileinfo *)slapi_ch_malloc(sizeof(struct logfileinfo));
logp->l_ctime = log_reverse_convert_time(p);
- logp->l_compressed = PR_FALSE;
- if (strcmp(p + strlen(p) - 3, ".gz") == 0) {
- logp->l_compressed = PR_TRUE;
- }
+ logp->l_compressed = is_compressed;
PR_snprintf(rotated_log, rotated_log_len, "%s/%s",
logsdir, dirent->name);
@@ -4982,23 +5015,15 @@ log__check_prevlogs(FILE *fp, char *pathname)
for (dirent = PR_ReadDir(dirptr, dirflags); dirent;
dirent = PR_ReadDir(dirptr, dirflags)) {
if (0 == strncmp(log_type, dirent->name, strlen(log_type)) &&
- (p = strrchr(dirent->name, '.')) != NULL &&
- NULL != strchr(p, '-')) { /* e.g., errors.20051123-165135 */
- char *q;
- int ignoreit = 0;
-
- for (q = ++p; q && *q; q++) {
- if (*q != '-' &&
- *q != '.' && /* .gz */
- *q != 'g' &&
- *q != 'z' &&
- !isdigit(*q))
- {
- ignoreit = 1;
- }
- }
- if (ignoreit || (q - p != 15))
+ (p = strchr(dirent->name, '.')) != NULL &&
+ NULL != strchr(p, '-')) { /* e.g., errors.20051123-165135 or errors.20051123-165135.gz */
+ PRBool is_compressed = PR_FALSE;
+
+ /* Skip the '.' to get the timestamp portion */
+ p++;
+ if (!log__validate_rotated_logname(p, &is_compressed)) {
continue;
+ }
fseek(fp, 0, SEEK_SET);
buf[BUFSIZ - 1] = '\0';
--
2.52.0

View File

@ -0,0 +1,93 @@
From 524cae18721234e1bc8d958c89684008d62e6b30 Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Thu, 5 Feb 2026 15:33:08 +0000
Subject: [PATCH] Issue 7224 - CI Test - Simplify
test_reserve_descriptor_validation (#7225)
Description:
Previously, the test_reserve_descriptor_validation CItest calculated
the expected number of file descriptors based on backends, indexes,
SSL/FIPS mode, and compared it to the value returned by the server.
This approach is fragile, especially in FIPS mode.
Fix:
The test has been updated to simply verify that the server corrects
the configured nsslapd-reservedescriptors value if it is set too low,
instead of calculating the expected total.
Fixes: https://github.com/389ds/389-ds-base/issues/7224
Reviewed by: @bsimonova (Thank you)
---
.../suites/resource_limits/fdlimits_test.py | 36 +++++++------------
1 file changed, 13 insertions(+), 23 deletions(-)
diff --git a/dirsrvtests/tests/suites/resource_limits/fdlimits_test.py b/dirsrvtests/tests/suites/resource_limits/fdlimits_test.py
index edcae28a5..9d8fdec52 100644
--- a/dirsrvtests/tests/suites/resource_limits/fdlimits_test.py
+++ b/dirsrvtests/tests/suites/resource_limits/fdlimits_test.py
@@ -27,7 +27,7 @@ RESRV_FD_ATTR = "nsslapd-reservedescriptors"
GLOBAL_LIMIT = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
SYSTEMD_LIMIT = ensure_str(check_output("systemctl show -p LimitNOFILE dirsrv@standalone1".split(" ")).strip()).split('=')[1]
CUSTOM_VAL = str(int(SYSTEMD_LIMIT) - 10)
-RESRV_DESC_VAL = str(10)
+RESRV_DESC_VAL_LOW = 10
TOO_HIGH_VAL = str(GLOBAL_LIMIT * 2)
TOO_HIGH_VAL2 = str(int(SYSTEMD_LIMIT) * 2)
TOO_LOW_VAL = "0"
@@ -86,40 +86,30 @@ def test_reserve_descriptor_validation(topology_st):
:id: 9bacdbcc-7754-4955-8a56-1d8c82bce274
:setup: Standalone Instance
:steps:
- 1. Set attr nsslapd-reservedescriptors to a low value of RESRV_DESC_VAL (10)
+ 1. Set attr nsslapd-reservedescriptors to a low value (10)
2. Verify low value has been set
3. Restart instance (On restart the reservedescriptor attr will be validated)
- 4. Check updated value for nsslapd-reservedescriptors attr
+ 4. Verify corrected value for nsslapd-reservedescriptors > low value
:expectedresults:
1. Success
- 2. A value of RESRV_DESC_VAL (10) is returned
+ 2. A value of RESRV_DESC_VAL_LOW (10) is returned
3. Success
- 4. A value of STANDALONE_INST_RESRV_DESCS (55) is returned
+ 4. Corrected value for nsslapd-reservedescriptors > low value
"""
- # Set nsslapd-reservedescriptors to a low value (RESRV_DESC_VAL:10)
- topology_st.standalone.config.set(RESRV_FD_ATTR, RESRV_DESC_VAL)
- resrv_fd = topology_st.standalone.config.get_attr_val_utf8(RESRV_FD_ATTR)
- assert resrv_fd == RESRV_DESC_VAL
+ # Set nsslapd-reservedescriptors to a low value (10)
+ topology_st.standalone.config.set(RESRV_FD_ATTR, str(RESRV_DESC_VAL_LOW))
+ resrv_fd = int(topology_st.standalone.config.get_attr_val_utf8(RESRV_FD_ATTR))
+ assert resrv_fd == RESRV_DESC_VAL_LOW
# An instance restart triggers a validation of the configured nsslapd-reservedescriptors attribute
topology_st.standalone.restart()
- """
- A standalone instance contains a single backend with default indexes
- so we only check these. TODO add tests for repl, chaining, PTA, SSL
- """
- STANDALONE_INST_RESRV_DESCS = 20 # 20 = Reserve descriptor constant
- backends = Backends(topology_st.standalone)
- STANDALONE_INST_RESRV_DESCS += (len(backends.list()) * 4) # 4 = Backend descriptor constant
- for be in backends.list() :
- STANDALONE_INST_RESRV_DESCS += len(be.get_indexes().list())
-
- # Varify reservedescriptors has been updated
- resrv_fd = topology_st.standalone.config.get_attr_val_utf8(RESRV_FD_ATTR)
- assert resrv_fd == str(STANDALONE_INST_RESRV_DESCS)
+ # Get the corrected value
+ corrected_fd = int(topology_st.standalone.config.get_attr_val_utf8(RESRV_FD_ATTR))
+ assert corrected_fd > RESRV_DESC_VAL_LOW
- log.info("test_reserve_descriptor_validation PASSED")
+ log.info(f"test_reserve_descriptor_validation PASSED (corrected from {RESRV_DESC_VAL_LOW} to {corrected_fd})")
@pytest.mark.skipif(ds_is_older("1.4.1.2"), reason="Not implemented")
def test_reserve_descriptors_high(topology_st):
--
2.52.0

View File

@ -1,236 +0,0 @@
From 1845aed98becaba6b975342229cb5e0de79d208d Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Wed, 29 Jan 2025 17:41:55 +0000
Subject: [PATCH] Issue 6436 - MOD on a large group slow if substring index is
present (#6437)
Bug Description: If the substring index is configured for the group
membership attribute ( member or uniqueMember ), the removal of a
member from a large static group is pretty slow.
Fix Description: A solution to this issue would be to introduce
a new index to track a membership atttribute index. In the interm,
we add a check to healthcheck to inform the user of the implications
of this configuration.
Fixes: https://github.com/389ds/389-ds-base/issues/6436
Reviewed by: @Firstyear, @tbordaz, @droideck (Thanks)
---
.../suites/healthcheck/health_config_test.py | 89 ++++++++++++++++++-
src/lib389/lib389/lint.py | 15 ++++
src/lib389/lib389/plugins.py | 37 +++++++-
3 files changed, 137 insertions(+), 4 deletions(-)
diff --git a/dirsrvtests/tests/suites/healthcheck/health_config_test.py b/dirsrvtests/tests/suites/healthcheck/health_config_test.py
index 6d3d08bfa..747699486 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_config_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_config_test.py
@@ -212,6 +212,7 @@ def test_healthcheck_RI_plugin_missing_indexes(topology_st):
MEMBER_DN = 'cn=member,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config'
standalone = topology_st.standalone
+ standalone.config.set("nsslapd-accesslog-logbuffering", "on")
log.info('Enable RI plugin')
plugin = ReferentialIntegrityPlugin(standalone)
@@ -233,7 +234,7 @@ def test_healthcheck_RI_plugin_missing_indexes(topology_st):
def test_healthcheck_MO_plugin_missing_indexes(topology_st):
- """Check if HealthCheck returns DSMOLE0002 code
+ """Check if HealthCheck returns DSMOLE0001 code
:id: 236b0ec2-13da-48fb-b65a-db7406d56d5d
:setup: Standalone instance
@@ -248,8 +249,8 @@ def test_healthcheck_MO_plugin_missing_indexes(topology_st):
:expectedresults:
1. Success
2. Success
- 3. Healthcheck reports DSMOLE0002 code and related details
- 4. Healthcheck reports DSMOLE0002 code and related details
+ 3. Healthcheck reports DSMOLE0001 code and related details
+ 4. Healthcheck reports DSMOLE0001 code and related details
5. Success
6. Healthcheck reports no issue found
7. Healthcheck reports no issue found
@@ -259,6 +260,7 @@ def test_healthcheck_MO_plugin_missing_indexes(topology_st):
MO_GROUP_ATTR = 'creatorsname'
standalone = topology_st.standalone
+ standalone.config.set("nsslapd-accesslog-logbuffering", "on")
log.info('Enable MO plugin')
plugin = MemberOfPlugin(standalone)
@@ -279,6 +281,87 @@ def test_healthcheck_MO_plugin_missing_indexes(topology_st):
run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
+def test_healthcheck_MO_plugin_substring_index(topology_st):
+ """Check if HealthCheck returns DSMOLE0002 code when the
+ member, uniquemember attribute contains a substring index type
+
+ :id: 10954811-24ac-4886-8183-e30892f8e02d
+ :setup: Standalone instance
+ :steps:
+ 1. Create DS instance
+ 2. Configure the instance with MO Plugin
+ 3. Change index type to substring for member attribute
+ 4. Use HealthCheck without --json option
+ 5. Use HealthCheck with --json option
+ 6. Change index type back to equality for member attribute
+ 7. Use HealthCheck without --json option
+ 8. Use HealthCheck with --json option
+ 9. Change index type to substring for uniquemember attribute
+ 10. Use HealthCheck without --json option
+ 11. Use HealthCheck with --json option
+ 12. Change index type back to equality for uniquemember attribute
+ 13. Use HealthCheck without --json option
+ 14. Use HealthCheck with --json option
+
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Healthcheck reports DSMOLE0002 code and related details
+ 5. Healthcheck reports DSMOLE0002 code and related details
+ 6. Success
+ 7. Healthcheck reports no issue found
+ 8. Healthcheck reports no issue found
+ 9. Success
+ 10. Healthcheck reports DSMOLE0002 code and related details
+ 11. Healthcheck reports DSMOLE0002 code and related details
+ 12. Success
+ 13. Healthcheck reports no issue found
+ 14. Healthcheck reports no issue found
+ """
+
+ RET_CODE = 'DSMOLE0002'
+ MEMBER_DN = 'cn=member,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config'
+ UNIQUE_MEMBER_DN = 'cn=uniquemember,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config'
+
+ standalone = topology_st.standalone
+ standalone.config.set("nsslapd-accesslog-logbuffering", "on")
+
+ log.info('Enable MO plugin')
+ plugin = MemberOfPlugin(standalone)
+ plugin.disable()
+ plugin.enable()
+
+ log.info('Change the index type of the member attribute index to substring')
+ index = Index(topology_st.standalone, MEMBER_DN)
+ index.replace('nsIndexType', 'sub')
+
+ run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=RET_CODE)
+ run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=RET_CODE)
+
+ log.info('Set the index type of the member attribute index back to eq')
+ index.replace('nsIndexType', 'eq')
+
+ run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
+ run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
+
+ log.info('Change the index type of the uniquemember attribute index to substring')
+ index = Index(topology_st.standalone, UNIQUE_MEMBER_DN)
+ index.replace('nsIndexType', 'sub')
+
+ run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=RET_CODE)
+ run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=RET_CODE)
+
+ log.info('Set the index type of the uniquemember attribute index back to eq')
+ index.replace('nsIndexType', 'eq')
+
+ run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
+ run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
+
+ # Restart the instance after changing the plugin to avoid breaking the other tests
+ standalone.restart()
+
+
@pytest.mark.ds50873
@pytest.mark.bz1685160
@pytest.mark.xfail(ds_is_older("1.4.1"), reason="Not implemented")
diff --git a/src/lib389/lib389/lint.py b/src/lib389/lib389/lint.py
index 4d9cbb666..3d3c79ea3 100644
--- a/src/lib389/lib389/lint.py
+++ b/src/lib389/lib389/lint.py
@@ -231,6 +231,21 @@ database after adding the missing index type. Here is an example using dsconf:
"""
}
+DSMOLE0002 = {
+ 'dsle': 'DSMOLE0002',
+ 'severity': 'LOW',
+ 'description': 'Removal of a member can be slow ',
+ 'items': ['cn=memberof plugin,cn=plugins,cn=config', ],
+ 'detail': """If the substring index is configured for a membership attribute. The removal of a member
+from the large group can be slow.
+
+""",
+ 'fix': """If not required, you can remove the substring index type using dsconf:
+
+ # dsconf slapd-YOUR_INSTANCE backend index set --attr=ATTR BACKEND --del-type=sub
+"""
+}
+
# Disk Space check. Note - PARTITION is replaced by the calling function
DSDSLE0001 = {
'dsle': 'DSDSLE0001',
diff --git a/src/lib389/lib389/plugins.py b/src/lib389/lib389/plugins.py
index 6bf1843ad..185398e5b 100644
--- a/src/lib389/lib389/plugins.py
+++ b/src/lib389/lib389/plugins.py
@@ -12,7 +12,7 @@ import copy
import os.path
from lib389 import tasks
from lib389._mapped_object import DSLdapObjects, DSLdapObject
-from lib389.lint import DSRILE0001, DSRILE0002, DSMOLE0001
+from lib389.lint import DSRILE0001, DSRILE0002, DSMOLE0001, DSMOLE0002
from lib389.utils import ensure_str, ensure_list_bytes
from lib389.schema import Schema
from lib389._constants import (
@@ -827,6 +827,41 @@ class MemberOfPlugin(Plugin):
report['check'] = f'memberof:attr_indexes'
yield report
+ def _lint_member_substring_index(self):
+ if self.status():
+ from lib389.backend import Backends
+ backends = Backends(self._instance).list()
+ membership_attrs = ['member', 'uniquemember']
+ container = self.get_attr_val_utf8_l("nsslapd-plugincontainerscope")
+ for backend in backends:
+ suffix = backend.get_attr_val_utf8_l('nsslapd-suffix')
+ if suffix == "cn=changelog":
+ # Always skip retro changelog
+ continue
+ if container is not None:
+ # Check if this backend is in the scope
+ if not container.endswith(suffix):
+ # skip this backend that is not in the scope
+ continue
+ indexes = backend.get_indexes()
+ for attr in membership_attrs:
+ report = copy.deepcopy(DSMOLE0002)
+ try:
+ index = indexes.get(attr)
+ types = index.get_attr_vals_utf8_l("nsIndexType")
+ if "sub" in types:
+ report['detail'] = report['detail'].replace('ATTR', attr)
+ report['detail'] = report['detail'].replace('BACKEND', suffix)
+ report['fix'] = report['fix'].replace('ATTR', attr)
+ report['fix'] = report['fix'].replace('BACKEND', suffix)
+ report['fix'] = report['fix'].replace('YOUR_INSTANCE', self._instance.serverid)
+ report['items'].append(suffix)
+ report['items'].append(attr)
+ report['check'] = f'attr:substring_index'
+ yield report
+ except KeyError:
+ continue
+
def get_attr(self):
"""Get memberofattr attribute"""
--
2.48.1

View File

@ -0,0 +1,92 @@
From 91e03f431d8ee0b1d316ebd7add232bf49360db2 Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Thu, 12 Feb 2026 10:42:09 +0000
Subject: [PATCH] Issue 7231 - Sync repl tests fail in FIPS mode due to non
FIPS compliant crypto (#7232)
Description:
Several sync_repl tests fail when running on a FIPS enabled system. The failures
are caused by the sync repl client (Sync_persist), using TLS options and ciphers
that are not FIPS compatible.
Fix:
Update the sync repl client to use FIPS approved TLS version.
Fixes: https://github.com/389ds/389-ds-base/issues/7231
Reviewed by: @progier389, @droideck (Thank you)
---
.../tests/suites/syncrepl_plugin/basic_test.py | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py b/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py
index c22331eb5..1e9a530bb 100644
--- a/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py
+++ b/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py
@@ -20,7 +20,7 @@ from lib389.idm.group import Groups
from lib389.topologies import topology_st as topology
from lib389.topologies import topology_m2 as topo_m2
from lib389.paths import Paths
-from lib389.utils import ds_is_older
+from lib389.utils import ds_is_older, is_fips
from lib389.plugins import RetroChangelogPlugin, ContentSyncPlugin, AutoMembershipPlugin, MemberOfPlugin, MemberOfSharedConfig, AutoMembershipDefinitions, MEPTemplates, MEPConfigs, ManagedEntriesPlugin, MEPTemplate
from lib389._constants import *
@@ -214,6 +214,12 @@ class Sync_persist(threading.Thread, ReconnectLDAPObject, SyncreplConsumer):
def run(self):
"""Start a sync repl client"""
ldap_connection = TestSyncer(self.inst.toLDAPURL())
+ ldap_connection.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND)
+ ldap_connection.set_option(ldap.OPT_X_TLS_CACERTFILE, os.path.join(self.inst.get_config_dir(), "ca.crt"))
+ if is_fips():
+ ldap_connection.set_option(ldap.OPT_X_TLS_PROTOCOL_MIN, ldap.OPT_X_TLS_PROTOCOL_TLS1_2)
+ ldap_connection.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
+
ldap_connection.simple_bind_s('cn=directory manager', 'password')
ldap_search = ldap_connection.syncrepl_search(
"dc=example,dc=com",
@@ -253,6 +259,7 @@ def test_sync_repl_mep(topology, request):
5. Success
"""
inst = topology[0]
+ inst.enable_tls()
# Enable/configure retroCL
plugin = RetroChangelogPlugin(inst)
@@ -338,6 +345,7 @@ def test_sync_repl_cookie(topology, init_sync_repl_plugins, request):
5.: succeeds
"""
inst = topology[0]
+ inst.enable_tls()
# create a sync repl client and wait 5 seconds to be sure it is running
sync_repl = Sync_persist(inst)
@@ -404,6 +412,8 @@ def test_sync_repl_cookie_add_del(topology, init_sync_repl_plugins, request):
6.: succeeds
"""
inst = topology[0]
+ inst.enable_tls()
+
# create a sync repl client and wait 5 seconds to be sure it is running
sync_repl = Sync_persist(inst)
sync_repl.start()
@@ -547,6 +557,7 @@ def test_sync_repl_cenotaph(topo_m2, request):
5. Should succeeds
"""
m1 = topo_m2.ms["supplier1"]
+ m1.enable_tls()
# Enable/configure retroCL
plugin = RetroChangelogPlugin(m1)
plugin.disable()
@@ -605,7 +616,7 @@ def test_sync_repl_dynamic_plugin(topology, request):
3. Should succeeds
4. Should succeeds
"""
-
+ topology.standalone.enable_tls()
# Reset the instance in a default config
# Disable content sync plugin
topology.standalone.plugins.disable(name=PLUGIN_REPL_SYNC)
--
2.52.0

View File

@ -1,651 +0,0 @@
From dba27e56161943fbcf54ecbc28337e2c81b07979 Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Mon, 13 Jan 2025 18:03:07 +0100
Subject: [PATCH] Issue 6494 - Various errors when using extended matching rule
on vlv sort filter (#6495)
* Issue 6494 - Various errors when using extended matching rule on vlv sort filter
Various issues when configuring and using extended matching rule within a vlv sort filter:
Race condition about the keys storage while indexing leading to various heap and data corruption. (lmdb only)
Crash while indexing if vlv are misconfigured because NULL key is not checked.
Read after block because of data type mismatch between SlapiValue and berval
Memory leaks
Solution:
Serialize the vlv index key generation if vlv filter has an extended matching rule.
Check null keys
Always provides SlapiValue even ifg we want to get keys as bervals
Free properly the resources
Issue: #6494
Reviewed by: @mreynolds389 (Thanks!)
(cherry picked from commit 4bd27ecc4e1d21c8af5ab8cad795d70477179a98)
(cherry picked from commit 223a20250cbf29a546dcb398cfc76024d2f91347)
(cherry picked from commit 280043740a525eaf0438129fd8b99ca251c62366)
---
.../tests/suites/indexes/regression_test.py | 29 +++
.../tests/suites/vlv/regression_test.py | 183 ++++++++++++++++++
ldap/servers/slapd/back-ldbm/cleanup.c | 8 +
ldap/servers/slapd/back-ldbm/dblayer.c | 22 ++-
ldap/servers/slapd/back-ldbm/ldbm_attr.c | 2 +-
ldap/servers/slapd/back-ldbm/matchrule.c | 8 +-
.../servers/slapd/back-ldbm/proto-back-ldbm.h | 3 +-
ldap/servers/slapd/back-ldbm/sort.c | 37 ++--
ldap/servers/slapd/back-ldbm/vlv.c | 26 +--
ldap/servers/slapd/back-ldbm/vlv_srch.c | 4 +-
ldap/servers/slapd/generation.c | 5 +
ldap/servers/slapd/plugin_mr.c | 12 +-
src/lib389/lib389/backend.py | 10 +
13 files changed, 292 insertions(+), 57 deletions(-)
diff --git a/dirsrvtests/tests/suites/indexes/regression_test.py b/dirsrvtests/tests/suites/indexes/regression_test.py
index fc6db727f..2196fb2ed 100644
--- a/dirsrvtests/tests/suites/indexes/regression_test.py
+++ b/dirsrvtests/tests/suites/indexes/regression_test.py
@@ -227,6 +227,35 @@ def test_reject_virtual_attr_for_indexing(topo):
break
+def test_reindex_extended_matching_rule(topo, add_backend_and_ldif_50K_users):
+ """Check that index with extended matching rule are reindexed properly.
+
+ :id: 8a3198e8-cc5a-11ef-a3e7-482ae39447e5
+ :setup: Standalone instance + a second backend with 50K users
+ :steps:
+ 1. Configure uid with 2.5.13.2 matching rule
+ 1. Configure cn with 2.5.13.2 matching rule
+ 2. Reindex
+ :expectedresults:
+ 1. Success
+ 2. Success
+ """
+
+ inst = topo.standalone
+ tasks = Tasks(inst)
+ be2 = Backends(topo.standalone).get_backend(SUFFIX2)
+ index = be2.get_index('uid')
+ index.replace('nsMatchingRule', '2.5.13.2')
+ index = be2.get_index('cn')
+ index.replace('nsMatchingRule', '2.5.13.2')
+
+ assert tasks.reindex(
+ suffix=SUFFIX2,
+ args={TASK_WAIT: True}
+ ) == 0
+
+
+
if __name__ == "__main__":
# Run isolated
# -s for DEBUG mode
diff --git a/dirsrvtests/tests/suites/vlv/regression_test.py b/dirsrvtests/tests/suites/vlv/regression_test.py
index 3b66de8b5..6ab709bd3 100644
--- a/dirsrvtests/tests/suites/vlv/regression_test.py
+++ b/dirsrvtests/tests/suites/vlv/regression_test.py
@@ -22,6 +22,146 @@ logging.getLogger(__name__).setLevel(logging.DEBUG)
log = logging.getLogger(__name__)
+class BackendHandler:
+ def __init__(self, inst, bedict, scope=ldap.SCOPE_ONELEVEL):
+ self.inst = inst
+ self.bedict = bedict
+ self.bes = Backends(inst)
+ self.scope = scope
+ self.data = {}
+
+ def find_backend(self, bename):
+ for be in self.bes.list():
+ if be.get_attr_val_utf8_l('cn') == bename:
+ return be
+ return None
+
+ def cleanup(self):
+ benames = list(self.bedict.keys())
+ benames.reverse()
+ for bename in benames:
+ be = self.find_backend(bename)
+ if be:
+ be.delete()
+
+ def setup(self):
+ # Create backends, add vlv index and populate the backends.
+ for bename,suffix in self.bedict.items():
+ be = self.bes.create(properties={
+ 'cn': bename,
+ 'nsslapd-suffix': suffix,
+ })
+ # Add suffix entry
+ Organization(self.inst, dn=suffix).create(properties={ 'o': bename, })
+ # Configure vlv
+ vlv_search, vlv_index = create_vlv_search_and_index(
+ self.inst, basedn=suffix,
+ bename=bename, scope=self.scope,
+ prefix=f'vlv_1lvl_{bename}')
+ # Reindex
+ reindex_task = Tasks(self.inst)
+ assert reindex_task.reindex(
+ suffix=suffix,
+ attrname=vlv_index.rdn,
+ args={TASK_WAIT: True},
+ vlv=True
+ ) == 0
+ # Add ou=People entry
+ OrganizationalUnits(self.inst, suffix).create(properties={'ou': 'People'})
+ # Add another ou that will be deleted before the export
+ # so that import will change the vlv search basedn entryid
+ ou2 = OrganizationalUnits(self.inst, suffix).create(properties={'ou': 'dummy ou'})
+ # Add a demo user so that vlv_check is happy
+ dn = f'uid=demo_user,ou=people,{suffix}'
+ UserAccount(self.inst, dn=dn).create( properties= {
+ 'uid': 'demo_user',
+ 'cn': 'Demo user',
+ 'sn': 'Demo user',
+ 'uidNumber': '99998',
+ 'gidNumber': '99998',
+ 'homeDirectory': '/var/empty',
+ 'loginShell': '/bin/false',
+ 'userpassword': DEMO_PW })
+ # Add regular user
+ add_users(self.inst, 10, suffix=suffix)
+ # Removing ou2
+ ou2.delete()
+ # And export
+ tasks = Tasks(self.inst)
+ ldif = f'{self.inst.get_ldif_dir()}/db-{bename}.ldif'
+ assert tasks.exportLDIF(suffix=suffix,
+ output_file=ldif,
+ args={TASK_WAIT: True}) == 0
+ # Add the various parameters in topology_st.belist
+ self.data[bename] = { 'be': be,
+ 'suffix': suffix,
+ 'ldif': ldif,
+ 'vlv_search' : vlv_search,
+ 'vlv_index' : vlv_index,
+ 'dn' : dn}
+
+
+def create_vlv_search_and_index(inst, basedn=DEFAULT_SUFFIX, bename='userRoot',
+ scope=ldap.SCOPE_SUBTREE, prefix="vlv", vlvsort="cn"):
+ vlv_searches = VLVSearch(inst)
+ vlv_search_properties = {
+ "objectclass": ["top", "vlvSearch"],
+ "cn": f"{prefix}Srch",
+ "vlvbase": basedn,
+ "vlvfilter": "(uid=*)",
+ "vlvscope": str(scope),
+ }
+ vlv_searches.create(
+ basedn=f"cn={bename},cn=ldbm database,cn=plugins,cn=config",
+ properties=vlv_search_properties
+ )
+
+ vlv_index = VLVIndex(inst)
+ vlv_index_properties = {
+ "objectclass": ["top", "vlvIndex"],
+ "cn": f"{prefix}Idx",
+ "vlvsort": vlvsort,
+ }
+ vlv_index.create(
+ basedn=f"cn={prefix}Srch,cn={bename},cn=ldbm database,cn=plugins,cn=config",
+ properties=vlv_index_properties
+ )
+ return vlv_searches, vlv_index
+
+
+@pytest.fixture
+def vlv_setup_with_uid_mr(topology_st, request):
+ inst = topology_st.standalone
+ bename = 'be1'
+ besuffix = f'o={bename}'
+ beh = BackendHandler(inst, { bename: besuffix })
+
+ def fin():
+ # Cleanup function
+ if not DEBUGGING and inst.exists() and inst.status():
+ beh.cleanup()
+
+ request.addfinalizer(fin)
+
+ # Make sure that our backend are not already present.
+ beh.cleanup()
+
+ # Then add the new backend
+ beh.setup()
+
+ index = Index(inst, f'cn=uid,cn=index,cn={bename},cn=ldbm database,cn=plugins,cn=config')
+ index.add('nsMatchingRule', '2.5.13.2')
+ reindex_task = Tasks(inst)
+ assert reindex_task.reindex(
+ suffix=besuffix,
+ attrname='uid',
+ args={TASK_WAIT: True}
+ ) == 0
+
+ topology_st.beh = beh
+ return topology_st
+
+
@pytest.mark.DS47966
def test_bulk_import_when_the_backend_with_vlv_was_recreated(topology_m2):
"""
@@ -105,6 +245,49 @@ def test_bulk_import_when_the_backend_with_vlv_was_recreated(topology_m2):
entries = M2.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(objectclass=*)")
+def test_vlv_with_mr(vlv_setup_with_uid_mr):
+ """
+ Testing vlv having specific matching rule
+
+ :id: 5e04afe2-beec-11ef-aa84-482ae39447e5
+ :setup: Standalone with uid have a matching rule index
+ :steps:
+ 1. Append vlvIndex entries then vlvSearch entry in the dse.ldif
+ 2. Restart the server
+ :expectedresults:
+ 1. Should Success.
+ 2. Should Success.
+ """
+ inst = vlv_setup_with_uid_mr.standalone
+ beh = vlv_setup_with_uid_mr.beh
+ bename, besuffix = next(iter(beh.bedict.items()))
+ vlv_searches, vlv_index = create_vlv_search_and_index(
+ inst, basedn=besuffix, bename=bename,
+ vlvsort="uid:2.5.13.2")
+ # Reindex the vlv
+ reindex_task = Tasks(inst)
+ assert reindex_task.reindex(
+ suffix=besuffix,
+ attrname=vlv_index.rdn,
+ args={TASK_WAIT: True},
+ vlv=True
+ ) == 0
+
+ inst.restart()
+ users = UserAccounts(inst, besuffix)
+ user_properties = {
+ 'uid': f'a new testuser',
+ 'cn': f'a new testuser',
+ 'sn': 'user',
+ 'uidNumber': '0',
+ 'gidNumber': '0',
+ 'homeDirectory': 'foo'
+ }
+ user = users.create(properties=user_properties)
+ user.delete()
+ assert inst.status()
+
+
if __name__ == "__main__":
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/back-ldbm/cleanup.c b/ldap/servers/slapd/back-ldbm/cleanup.c
index 6b2e9faef..939d8bc4f 100644
--- a/ldap/servers/slapd/back-ldbm/cleanup.c
+++ b/ldap/servers/slapd/back-ldbm/cleanup.c
@@ -15,12 +15,14 @@
#include "back-ldbm.h"
#include "dblayer.h"
+#include "vlv_srch.h"
int
ldbm_back_cleanup(Slapi_PBlock *pb)
{
struct ldbminfo *li;
Slapi_Backend *be;
+ struct vlvSearch *nextp;
slapi_log_err(SLAPI_LOG_TRACE, "ldbm_back_cleanup", "ldbm backend cleaning up\n");
slapi_pblock_get(pb, SLAPI_PLUGIN_PRIVATE, &li);
@@ -45,6 +47,12 @@ ldbm_back_cleanup(Slapi_PBlock *pb)
return 0;
}
+ /* Release the vlv list */
+ for (struct vlvSearch *p=be->vlvSearchList; p; p=nextp) {
+ nextp = p->vlv_next;
+ vlvSearch_delete(&p);
+ }
+
/*
* We check if li is NULL. Because of an issue in how we create backends
* we share the li and plugin info between many unique backends. This causes
diff --git a/ldap/servers/slapd/back-ldbm/dblayer.c b/ldap/servers/slapd/back-ldbm/dblayer.c
index 05cc5b891..6b8ce0016 100644
--- a/ldap/servers/slapd/back-ldbm/dblayer.c
+++ b/ldap/servers/slapd/back-ldbm/dblayer.c
@@ -494,8 +494,12 @@ int
dblayer_close(struct ldbminfo *li, int dbmode)
{
dblayer_private *priv = (dblayer_private *)li->li_dblayer_private;
-
- return priv->dblayer_close_fn(li, dbmode);
+ int rc = priv->dblayer_close_fn(li, dbmode);
+ if (rc == 0) {
+ /* Clean thread specific data */
+ dblayer_destroy_txn_stack();
+ }
+ return rc;
}
/* Routines for opening and closing random files in the DB_ENV.
@@ -621,6 +625,9 @@ dblayer_erase_index_file(backend *be, struct attrinfo *a, PRBool use_lock, int n
return 0;
}
struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private;
+ if (NULL == li) {
+ return 0;
+ }
dblayer_private *priv = (dblayer_private *)li->li_dblayer_private;
return priv->dblayer_rm_db_file_fn(be, a, use_lock, no_force_chkpt);
@@ -1382,3 +1389,14 @@ dblayer_pop_pvt_txn(void)
}
return;
}
+
+void
+dblayer_destroy_txn_stack(void)
+{
+ /*
+ * Cleanup for the main thread to avoid false/positive leaks from libasan
+ * Note: data is freed because PR_SetThreadPrivate calls the
+ * dblayer_cleanup_txn_stack callback
+ */
+ PR_SetThreadPrivate(thread_private_txn_stack, NULL);
+}
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_attr.c b/ldap/servers/slapd/back-ldbm/ldbm_attr.c
index 708756d3e..70700ca1d 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_attr.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_attr.c
@@ -54,7 +54,7 @@ attrinfo_delete(struct attrinfo **pp)
idl_release_private(*pp);
(*pp)->ai_key_cmp_fn = NULL;
slapi_ch_free((void **)&((*pp)->ai_type));
- slapi_ch_free((void **)(*pp)->ai_index_rules);
+ charray_free((*pp)->ai_index_rules);
slapi_ch_free((void **)&((*pp)->ai_attrcrypt));
attr_done(&((*pp)->ai_sattr));
attrinfo_delete_idlistinfo(&(*pp)->ai_idlistinfo);
diff --git a/ldap/servers/slapd/back-ldbm/matchrule.c b/ldap/servers/slapd/back-ldbm/matchrule.c
index 5d516b9f8..5365e8acf 100644
--- a/ldap/servers/slapd/back-ldbm/matchrule.c
+++ b/ldap/servers/slapd/back-ldbm/matchrule.c
@@ -107,7 +107,7 @@ destroy_matchrule_indexer(Slapi_PBlock *pb)
* is destroyed
*/
int
-matchrule_values_to_keys(Slapi_PBlock *pb, struct berval **input_values, struct berval ***output_values)
+matchrule_values_to_keys(Slapi_PBlock *pb, Slapi_Value **input_values, struct berval ***output_values)
{
IFP mrINDEX = NULL;
@@ -135,10 +135,8 @@ matchrule_values_to_keys_sv(Slapi_PBlock *pb, Slapi_Value **input_values, Slapi_
slapi_pblock_get(pb, SLAPI_PLUGIN_MR_INDEX_SV_FN, &mrINDEX);
if (NULL == mrINDEX) { /* old school - does not have SV function */
int rc;
- struct berval **bvi = NULL, **bvo = NULL;
- valuearray_get_bervalarray(input_values, &bvi);
- rc = matchrule_values_to_keys(pb, bvi, &bvo);
- ber_bvecfree(bvi);
+ struct berval **bvo = NULL;
+ rc = matchrule_values_to_keys(pb, input_values, &bvo);
/* note - the indexer owns bvo and will free it when destroyed */
valuearray_init_bervalarray(bvo, output_values);
/* store output values in SV form - caller expects SLAPI_PLUGIN_MR_KEYS is Slapi_Value** */
diff --git a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
index d93ff9239..157788fa4 100644
--- a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
+++ b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
@@ -84,6 +84,7 @@ int dblayer_release_index_file(backend *be, struct attrinfo *a, DB *pDB);
int dblayer_erase_index_file(backend *be, struct attrinfo *a, PRBool use_lock, int no_force_chkpt);
int dblayer_get_id2entry(backend *be, DB **ppDB);
int dblayer_release_id2entry(backend *be, DB *pDB);
+void dblayer_destroy_txn_stack(void);
int dblayer_txn_init(struct ldbminfo *li, back_txn *txn);
int dblayer_txn_begin(backend *be, back_txnid parent_txn, back_txn *txn);
int dblayer_txn_begin_ext(struct ldbminfo *li, back_txnid parent_txn, back_txn *txn, PRBool use_lock);
@@ -560,7 +561,7 @@ int compute_allids_limit(Slapi_PBlock *pb, struct ldbminfo *li);
*/
int create_matchrule_indexer(Slapi_PBlock **pb, char *matchrule, char *type);
int destroy_matchrule_indexer(Slapi_PBlock *pb);
-int matchrule_values_to_keys(Slapi_PBlock *pb, struct berval **input_values, struct berval ***output_values);
+int matchrule_values_to_keys(Slapi_PBlock *pb, Slapi_Value **input_values, struct berval ***output_values);
int matchrule_values_to_keys_sv(Slapi_PBlock *pb, Slapi_Value **input_values, Slapi_Value ***output_values);
/*
diff --git a/ldap/servers/slapd/back-ldbm/sort.c b/ldap/servers/slapd/back-ldbm/sort.c
index 70ac60803..196af753f 100644
--- a/ldap/servers/slapd/back-ldbm/sort.c
+++ b/ldap/servers/slapd/back-ldbm/sort.c
@@ -536,30 +536,18 @@ compare_entries_sv(ID *id_a, ID *id_b, sort_spec *s, baggage_carrier *bc, int *e
valuearray_get_bervalarray(valueset_get_valuearray(&attr_b->a_present_values), &value_b);
} else {
/* Match rule case */
- struct berval **actual_value_a = NULL;
- struct berval **actual_value_b = NULL;
- struct berval **temp_value = NULL;
-
- valuearray_get_bervalarray(valueset_get_valuearray(&attr_a->a_present_values), &actual_value_a);
- valuearray_get_bervalarray(valueset_get_valuearray(&attr_b->a_present_values), &actual_value_b);
- matchrule_values_to_keys(this_one->mr_pb, actual_value_a, &temp_value);
- /* Now copy it, so the second call doesn't crap on it */
- value_a = slapi_ch_bvecdup(temp_value); /* Really, we'd prefer to not call the chXXX variant...*/
- matchrule_values_to_keys(this_one->mr_pb, actual_value_b, &value_b);
-
- if ((actual_value_a && !value_a) ||
- (actual_value_b && !value_b)) {
- ber_bvecfree(actual_value_a);
- ber_bvecfree(actual_value_b);
- CACHE_RETURN(&inst->inst_cache, &a);
- CACHE_RETURN(&inst->inst_cache, &b);
- *error = 1;
- return 0;
+ Slapi_Value **va_a = valueset_get_valuearray(&attr_a->a_present_values);
+ Slapi_Value **va_b = valueset_get_valuearray(&attr_b->a_present_values);
+
+ matchrule_values_to_keys(this_one->mr_pb, va_a, &value_a);
+ /* Plugin owns the memory ==> duplicate the key before next call garble it */
+ value_a = slapi_ch_bvecdup(value_a);
+ matchrule_values_to_keys(this_one->mr_pb, va_b, &value_b);
+
+ if ((va_a && !value_a) || (va_b && !value_b)) {
+ result = 0;
+ goto bail;
}
- if (actual_value_a)
- ber_bvecfree(actual_value_a);
- if (actual_value_b)
- ber_bvecfree(actual_value_b);
}
/* Compare them */
if (!order) {
@@ -582,9 +570,10 @@ compare_entries_sv(ID *id_a, ID *id_b, sort_spec *s, baggage_carrier *bc, int *e
}
/* If so, proceed to the next attribute for comparison */
}
+ *error = 0;
+bail:
CACHE_RETURN(&inst->inst_cache, &a);
CACHE_RETURN(&inst->inst_cache, &b);
- *error = 0;
return result;
}
diff --git a/ldap/servers/slapd/back-ldbm/vlv.c b/ldap/servers/slapd/back-ldbm/vlv.c
index 121fb3667..70e0bac85 100644
--- a/ldap/servers/slapd/back-ldbm/vlv.c
+++ b/ldap/servers/slapd/back-ldbm/vlv.c
@@ -605,7 +605,7 @@ vlv_getindices(IFP callback_fn, void *param, backend *be)
* generate the same composite key, so we append the EntryID
* to ensure the uniqueness of the key.
*
- * Always creates a key. Never returns NULL.
+ * May return NULL in case of errors (typically in some configuration error cases)
*/
static struct vlv_key *
vlv_create_key(struct vlvIndex *p, struct backentry *e)
@@ -659,10 +659,8 @@ vlv_create_key(struct vlvIndex *p, struct backentry *e)
/* Matching rule. Do the magic mangling. Plugin owns the memory. */
if (p->vlv_mrpb[sortattr] != NULL) {
/* xxxPINAKI */
- struct berval **bval = NULL;
Slapi_Value **va = valueset_get_valuearray(&attr->a_present_values);
- valuearray_get_bervalarray(va, &bval);
- matchrule_values_to_keys(p->vlv_mrpb[sortattr], bval, &value);
+ matchrule_values_to_keys(p->vlv_mrpb[sortattr], va, &value);
}
}
@@ -779,6 +777,13 @@ do_vlv_update_index(back_txn *txn, struct ldbminfo *li __attribute__((unused)),
}
key = vlv_create_key(pIndex, entry);
+ if (key == NULL) {
+ slapi_log_err(SLAPI_LOG_ERR, "vlv_create_key", "Unable to generate vlv %s index key."
+ " There may be a configuration issue.\n", pIndex->vlv_name);
+ dblayer_release_index_file(be, pIndex->vlv_attrinfo, db);
+ return rc;
+ }
+
if (NULL != txn) {
db_txn = txn->back_txn_txn;
} else {
@@ -949,11 +954,11 @@ vlv_create_matching_rule_value(Slapi_PBlock *pb, struct berval *original_value)
struct berval **value = NULL;
if (pb != NULL) {
struct berval **outvalue = NULL;
- struct berval *invalue[2];
- invalue[0] = original_value; /* jcm: cast away const */
- invalue[1] = NULL;
+ Slapi_Value v_in = {0};
+ Slapi_Value *va_in[2] = { &v_in, NULL };
+ slapi_value_init_berval(&v_in, original_value);
/* The plugin owns the memory it returns in outvalue */
- matchrule_values_to_keys(pb, invalue, &outvalue);
+ matchrule_values_to_keys(pb, va_in, &outvalue);
if (outvalue != NULL) {
value = slapi_ch_bvecdup(outvalue);
}
@@ -1610,11 +1615,8 @@ retry:
PRBool needFree = PR_FALSE;
if (sort_control->mr_pb != NULL) {
- struct berval **tmp_entry_value = NULL;
-
- valuearray_get_bervalarray(csn_value, &tmp_entry_value);
/* Matching rule. Do the magic mangling. Plugin owns the memory. */
- matchrule_values_to_keys(sort_control->mr_pb, /* xxxPINAKI needs modification attr->a_vals */ tmp_entry_value, &entry_value);
+ matchrule_values_to_keys(sort_control->mr_pb, csn_value, &entry_value);
} else {
valuearray_get_bervalarray(csn_value, &entry_value);
needFree = PR_TRUE; /* entry_value is a copy */
diff --git a/ldap/servers/slapd/back-ldbm/vlv_srch.c b/ldap/servers/slapd/back-ldbm/vlv_srch.c
index fe1208d59..11d1c715b 100644
--- a/ldap/servers/slapd/back-ldbm/vlv_srch.c
+++ b/ldap/servers/slapd/back-ldbm/vlv_srch.c
@@ -203,6 +203,9 @@ vlvSearch_delete(struct vlvSearch **ppvs)
{
if (ppvs != NULL && *ppvs != NULL) {
struct vlvIndex *pi, *ni;
+ if ((*ppvs)->vlv_e) {
+ slapi_entry_free((struct slapi_entry *)((*ppvs)->vlv_e));
+ }
slapi_sdn_free(&((*ppvs)->vlv_dn));
slapi_ch_free((void **)&((*ppvs)->vlv_name));
slapi_sdn_free(&((*ppvs)->vlv_base));
@@ -217,7 +220,6 @@ vlvSearch_delete(struct vlvSearch **ppvs)
pi = ni;
}
slapi_ch_free((void **)ppvs);
- *ppvs = NULL;
}
}
diff --git a/ldap/servers/slapd/generation.c b/ldap/servers/slapd/generation.c
index c4f20f793..89f097322 100644
--- a/ldap/servers/slapd/generation.c
+++ b/ldap/servers/slapd/generation.c
@@ -93,9 +93,13 @@ get_server_dataversion()
lenstr *l = NULL;
Slapi_Backend *be;
char *cookie;
+ static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
+ /* Serialize to avoid race condition */
+ pthread_mutex_lock(&mutex);
/* we already cached the copy - just return it */
if (server_dataversion_id != NULL) {
+ pthread_mutex_unlock(&mutex);
return server_dataversion_id;
}
@@ -130,5 +134,6 @@ get_server_dataversion()
server_dataversion_id = slapi_ch_strdup(l->ls_buf);
}
lenstr_free(&l);
+ pthread_mutex_unlock(&mutex);
return server_dataversion_id;
}
diff --git a/ldap/servers/slapd/plugin_mr.c b/ldap/servers/slapd/plugin_mr.c
index 13f76fe52..6cf88b7de 100644
--- a/ldap/servers/slapd/plugin_mr.c
+++ b/ldap/servers/slapd/plugin_mr.c
@@ -391,28 +391,18 @@ mr_wrap_mr_index_sv_fn(Slapi_PBlock *pb)
return rc;
}
-/* this function takes SLAPI_PLUGIN_MR_VALUES as struct berval ** and
+/* this function takes SLAPI_PLUGIN_MR_VALUES as Slapi_Value ** and
returns SLAPI_PLUGIN_MR_KEYS as struct berval **
*/
static int
mr_wrap_mr_index_fn(Slapi_PBlock *pb)
{
int rc = -1;
- struct berval **in_vals = NULL;
struct berval **out_vals = NULL;
struct mr_private *mrpriv = NULL;
- Slapi_Value **in_vals_sv = NULL;
Slapi_Value **out_vals_sv = NULL;
- slapi_pblock_get(pb, SLAPI_PLUGIN_MR_VALUES, &in_vals); /* get bervals */
- /* convert bervals to sv ary */
- valuearray_init_bervalarray(in_vals, &in_vals_sv);
- slapi_pblock_set(pb, SLAPI_PLUGIN_MR_VALUES, in_vals_sv); /* use sv */
rc = mr_wrap_mr_index_sv_fn(pb);
- /* clean up in_vals_sv */
- valuearray_free(&in_vals_sv);
- /* restore old in_vals */
- slapi_pblock_set(pb, SLAPI_PLUGIN_MR_VALUES, in_vals);
/* get result sv keys */
slapi_pblock_get(pb, SLAPI_PLUGIN_MR_KEYS, &out_vals_sv);
/* convert to bvec */
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
index 9acced205..cee073ea7 100644
--- a/src/lib389/lib389/backend.py
+++ b/src/lib389/lib389/backend.py
@@ -1029,6 +1029,16 @@ class Backends(DSLdapObjects):
for be in sorted(self.list(), key=lambda be: len(be.get_suffix()), reverse=True):
be.delete()
+ def get_backend(self, suffix):
+ """
+ Return the backend associated with the provided suffix.
+ """
+ suffix_l = suffix.lower()
+ for be in self.list():
+ if be.get_attr_val_utf8_l('nsslapd-suffix') == suffix_l:
+ return be
+ return None
+
class DatabaseConfig(DSLdapObject):
"""Backend Database configuration
--
2.48.1

View File

@ -0,0 +1,33 @@
From bd57467f6efd8e398eb2e608331711bdd571d3ac Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Thu, 12 Feb 2026 09:58:54 -0500
Subject: [PATCH] Issue 7248 - CLI - attribute uniqueness - fix usage for
exclude subtree option
Description:
Fix typo in usage message for the exclude subtree option
relates: https://github.com/389ds/389-ds-base/issues/7248
Reviewed by: progier (Thanks!)
---
src/lib389/lib389/cli_conf/plugins/attruniq.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/lib389/lib389/cli_conf/plugins/attruniq.py b/src/lib389/lib389/cli_conf/plugins/attruniq.py
index bc925eb1c..26ca5d819 100644
--- a/src/lib389/lib389/cli_conf/plugins/attruniq.py
+++ b/src/lib389/lib389/cli_conf/plugins/attruniq.py
@@ -127,7 +127,7 @@ def _add_parser_args(parser):
help='Sets the DN under which the plug-in checks for uniqueness of '
'the attributes value. This attribute is multi-valued (uniqueness-subtrees)')
parser.add_argument('--exclude-subtree', nargs='+',
- help='Sets subtrees that should not excludedfrom attribute uniqueness. '
+ help='Sets subtrees that should be excluded from attribute uniqueness checks. '
'This attribute is multi-valued (uniqueness-exclude-subtrees)')
parser.add_argument('--across-all-subtrees', choices=['on', 'off'], type=str.lower,
help='If enabled (on), the plug-in checks that the attribute is unique across all subtrees '
--
2.52.0

View File

@ -1,230 +0,0 @@
From bd2829d04491556c35a0b36b591c09a69baf6546 Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Mon, 11 Dec 2023 11:58:40 +0100
Subject: [PATCH] Issue 6004 - idletimeout may be ignored (#6005)
* Issue 6004 - idletimeout may be ignored
Problem: idletimeout is still not handled when binding as non root (unless there are some activity
on another connection)
Fix:
Add a slapi_eq_repeat_rel handler that walks all active connection every seconds and check if the timeout is expired.
Note about CI test:
Notice that idletimeout is never enforced for connections bound as root (i.e cn=directory manager).
Issue #6004
Reviewed by: @droideck, @tbordaz (Thanks!)
(cherry picked from commit 86b5969acbe124eec8c89bcf1ab2156b2b140c17)
(cherry picked from commit bdb0a72b4953678e5418406b3c202dfa2c7469a2)
(cherry picked from commit 61cebc191cd4090072dda691b9956dbde4cf7c48)
---
.../tests/suites/config/regression_test.py | 82 ++++++++++++++++++-
ldap/servers/slapd/daemon.c | 52 +++++++++++-
2 files changed, 128 insertions(+), 6 deletions(-)
diff --git a/dirsrvtests/tests/suites/config/regression_test.py b/dirsrvtests/tests/suites/config/regression_test.py
index 0000dd82d..8dbba8cd2 100644
--- a/dirsrvtests/tests/suites/config/regression_test.py
+++ b/dirsrvtests/tests/suites/config/regression_test.py
@@ -6,20 +6,49 @@
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---
#
+import os
import logging
import pytest
+import time
from lib389.utils import *
from lib389.dseldif import DSEldif
-from lib389.config import LDBMConfig
+from lib389.config import BDB_LDBMConfig, LDBMConfig, Config
from lib389.backend import Backends
from lib389.topologies import topology_st as topo
+from lib389.idm.user import UserAccounts, TEST_USER_PROPERTIES
+from lib389._constants import DEFAULT_SUFFIX, PASSWORD, DN_DM
pytestmark = pytest.mark.tier0
logging.getLogger(__name__).setLevel(logging.INFO)
log = logging.getLogger(__name__)
+DEBUGGING = os.getenv("DEBUGGING", default=False)
CUSTOM_MEM = '9100100100'
+IDLETIMEOUT = 5
+DN_TEST_USER = f'uid={TEST_USER_PROPERTIES["uid"]},ou=People,{DEFAULT_SUFFIX}'
+
+
+@pytest.fixture(scope="module")
+def idletimeout_topo(topo, request):
+ """Create an instance with a test user and set idletimeout"""
+ inst = topo.standalone
+ config = Config(inst)
+
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ user = users.create(properties={
+ **TEST_USER_PROPERTIES,
+ 'userpassword' : PASSWORD,
+ })
+ config.replace('nsslapd-idletimeout', str(IDLETIMEOUT))
+
+ def fin():
+ if not DEBUGGING:
+ config.reset('nsslapd-idletimeout')
+ user.delete()
+
+ request.addfinalizer(fin)
+ return topo
# Function to return value of available memory in kb
@@ -79,7 +108,7 @@ def test_maxbersize_repl(topo):
nsslapd-errorlog-logmaxdiskspace are set in certain order
:id: 743e912c-2be4-4f5f-9c2a-93dcb18f51a0
- :setup: MMR with two suppliers
+ :setup: Standalone Instance
:steps:
1. Stop the instance
2. Set nsslapd-errorlog-maxlogsize before/after
@@ -112,3 +141,52 @@ def test_maxbersize_repl(topo):
log.info("Assert no init_dse_file errors in the error log")
assert not inst.ds_error_log.match('.*ERR - init_dse_file.*')
+
+def test_bdb_config(topo):
+ """Check that bdb config entry exists
+
+ :id: edbc6f54-7c98-11ee-b1c0-482ae39447e5
+ :setup: standalone
+ :steps:
+ 1. Check that bdb config instance exists.
+ :expectedresults:
+ 1. Success
+ """
+
+ inst = topo.standalone
+ assert BDB_LDBMConfig(inst).exists()
+
+
+@pytest.mark.parametrize("dn,expected_result", [(DN_TEST_USER, True), (DN_DM, False)])
+def test_idletimeout(idletimeout_topo, dn, expected_result):
+ """Check that bdb config entry exists
+
+ :id: b20f2826-942a-11ee-827b-482ae39447e5
+ :parametrized: yes
+ :setup: Standalone Instance with test user and idletimeout
+ :steps:
+ 1. Open new ldap connection
+ 2. Bind with the provided dn
+ 3. Wait longer than idletimeout
+ 4. Try to bind again the provided dn and check if
+ connection is closed or not.
+ 5. Check if result is the expected one.
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. Success
+ """
+
+ inst = idletimeout_topo.standalone
+
+ l = ldap.initialize(f'ldap://localhost:{inst.port}')
+ l.bind_s(dn, PASSWORD)
+ time.sleep(IDLETIMEOUT+1)
+ try:
+ l.bind_s(dn, PASSWORD)
+ result = False
+ except ldap.SERVER_DOWN:
+ result = True
+ assert expected_result == result
diff --git a/ldap/servers/slapd/daemon.c b/ldap/servers/slapd/daemon.c
index 57e07e5f5..6df109760 100644
--- a/ldap/servers/slapd/daemon.c
+++ b/ldap/servers/slapd/daemon.c
@@ -68,6 +68,8 @@
#define SLAPD_ACCEPT_WAKEUP_TIMER 250
#endif
+#define MILLISECONDS_PER_SECOND 1000
+
int slapd_wakeup_timer = SLAPD_WAKEUP_TIMER; /* time in ms to wakeup */
int slapd_accept_wakeup_timer = SLAPD_ACCEPT_WAKEUP_TIMER; /* time in ms to wakeup */
#ifdef notdef /* GGOODREPL */
@@ -1045,6 +1047,48 @@ slapd_sockets_ports_free(daemon_ports_t *ports_info)
#endif
}
+/*
+ * Tells if idle timeout has expired
+ */
+static inline int __attribute__((always_inline))
+has_idletimeout_expired(Connection *c, time_t curtime)
+{
+ return (c->c_state != CONN_STATE_FREE && !c->c_gettingber &&
+ c->c_idletimeout > 0 && NULL == c->c_ops &&
+ curtime - c->c_idlesince >= c->c_idletimeout);
+}
+
+/*
+ * slapi_eq_repeat_rel callback that checks that idletimeout has not expired.
+ */
+void
+check_idletimeout(time_t when __attribute__((unused)), void *arg __attribute__((unused)) )
+{
+ Connection_Table *ct = the_connection_table;
+ time_t curtime = slapi_current_rel_time_t();
+ /* Walk all active connections of all connection listeners */
+ for (int list_num = 0; list_num < ct->list_num; list_num++) {
+ for (Connection *c = connection_table_get_first_active_connection(ct, list_num);
+ c != NULL; c = connection_table_get_next_active_connection(ct, c)) {
+ if (!has_idletimeout_expired(c, curtime)) {
+ continue;
+ }
+ /* Looks like idletimeout has expired, lets acquire the lock
+ * and double check.
+ */
+ if (pthread_mutex_trylock(&(c->c_mutex)) == EBUSY) {
+ continue;
+ }
+ if (has_idletimeout_expired(c, curtime)) {
+ /* idle timeout has expired */
+ disconnect_server_nomutex(c, c->c_connid, -1,
+ SLAPD_DISCONNECT_IDLE_TIMEOUT, ETIMEDOUT);
+ }
+ pthread_mutex_unlock(&(c->c_mutex));
+ }
+ }
+}
+
void
slapd_daemon(daemon_ports_t *ports)
{
@@ -1258,7 +1302,9 @@ slapd_daemon(daemon_ports_t *ports)
"MAINPID=%lu",
(unsigned long)getpid());
#endif
-
+ slapi_eq_repeat_rel(check_idletimeout, NULL,
+ slapi_current_rel_time_t(),
+ MILLISECONDS_PER_SECOND);
/* The meat of the operation is in a loop on a call to select */
while (!g_get_shutdown()) {
int select_return = 0;
@@ -1734,9 +1780,7 @@ handle_pr_read_ready(Connection_Table *ct, PRIntn num_poll __attribute__((unused
disconnect_server_nomutex(c, c->c_connid, -1,
SLAPD_DISCONNECT_POLL, EPIPE);
}
- } else if (c->c_idletimeout > 0 &&
- (curtime - c->c_idlesince) >= c->c_idletimeout &&
- NULL == c->c_ops) {
+ } else if (has_idletimeout_expired(c, curtime)) {
/* idle timeout */
disconnect_server_nomutex(c, c->c_connid, -1,
SLAPD_DISCONNECT_IDLE_TIMEOUT, ETIMEDOUT);
--
2.48.1

View File

@ -0,0 +1,201 @@
From 180329d9ca672306f8e90d599890be3a6a8aa95c Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Thu, 12 Feb 2026 11:13:45 -0500
Subject: [PATCH] Issue - CLI - dsctl db2index needs some hardening with MBD
Description:
The usage for dsctl db2index was confusing. The way the attr options and
backend name were displayed it looks like the backend name could come after
the attributes, but instead the backend name was treated as an attribute.
Instead make the backend name required, and change the attribute naming to
require individual options instead of a list of values.
Relates: https://github.com/389ds/389-ds-base/issues/7250
Reviewed by: progier(Thanks!)
---
.../tests/suites/import/import_test.py | 2 +-
src/lib389/lib389/__init__.py | 40 ++++++-------------
src/lib389/lib389/cli_ctl/dbtasks.py | 37 +++++++----------
3 files changed, 27 insertions(+), 52 deletions(-)
diff --git a/dirsrvtests/tests/suites/import/import_test.py b/dirsrvtests/tests/suites/import/import_test.py
index 18caec633..058a4b3c7 100644
--- a/dirsrvtests/tests/suites/import/import_test.py
+++ b/dirsrvtests/tests/suites/import/import_test.py
@@ -512,7 +512,7 @@ def test_entry_with_escaped_characters_fails_to_import_and_index(topo, _import_c
count += 1
# Now re-index the database
topo.standalone.stop()
- topo.standalone.db2index()
+ topo.standalone.db2index(bename="userroot")
topo.standalone.start()
# Should not return error.
assert not topo.standalone.searchErrorsLog('error')
diff --git a/src/lib389/lib389/__init__.py b/src/lib389/lib389/__init__.py
index 39a2852e5..ec47987be 100644
--- a/src/lib389/lib389/__init__.py
+++ b/src/lib389/lib389/__init__.py
@@ -2985,7 +2985,7 @@ class DirSrv(SimpleLDAPObject, object):
return True
- def db2index(self, bename=None, suffixes=None, attrs=None, vlvTag=None):
+ def db2index(self, bename, suffixes=None, attrs=None, vlvTag=None):
"""
@param bename - The backend name to reindex
@param suffixes - List/tuple of suffixes to reindex, currently unused
@@ -2998,34 +2998,18 @@ class DirSrv(SimpleLDAPObject, object):
if self.status():
self.log.error("db2index: Can not operate while directory server is running")
return False
- cmd = [prog, ]
- # No backend specified, do an upgrade on all backends
- # Backend and no attrs specified, reindex with all backend indexes
- # Backend and attr/s specified, reindex backend with attr/s
- if bename:
- cmd.append('db2index')
- cmd.append('-n')
- cmd.append(bename)
- if attrs:
- for attr in attrs:
- cmd.append('-t')
- cmd.append(attr)
- else:
- dse_ldif = DSEldif(self)
- indexes = dse_ldif.get_indexes(bename)
- if indexes:
- for idx in indexes:
- cmd.append('-t')
- cmd.append(idx)
+ cmd = [prog, 'db2index', '-n', bename, '-D', self.get_config_dir()]
+ if attrs:
+ for attr in attrs:
+ cmd.append('-t')
+ cmd.append(attr)
else:
- cmd.append('upgradedb')
- cmd.append('-a')
- now = datetime.now().isoformat()
- cmd.append(os.path.join(self.get_bak_dir(), 'reindex_%s' % now))
- cmd.append('-f')
-
- cmd.append('-D')
- cmd.append(self.get_config_dir())
+ dse_ldif = DSEldif(self)
+ indexes = dse_ldif.get_indexes(bename)
+ if indexes:
+ for idx in indexes:
+ cmd.append('-t')
+ cmd.append(idx)
try:
result = subprocess.check_output(cmd, encoding='utf-8')
diff --git a/src/lib389/lib389/cli_ctl/dbtasks.py b/src/lib389/lib389/cli_ctl/dbtasks.py
index 16da966d1..cd96cdaf7 100644
--- a/src/lib389/lib389/cli_ctl/dbtasks.py
+++ b/src/lib389/lib389/cli_ctl/dbtasks.py
@@ -26,32 +26,18 @@ class IndexOrdering(Enum):
def dbtasks_db2index(inst, log, args):
- rtn = False
- if not args.backend:
- if not inst.db2index():
- rtn = False
- else:
- rtn = True
- elif args.backend and not args.attr:
- if not inst.db2index(bename=args.backend):
- rtn = False
- else:
- rtn = True
+ inst.log = log
+ if not inst.db2index(bename=args.backend, attrs=args.attr):
+ log.fatal("db2index failed")
+ return False
else:
- if not inst.db2index(bename=args.backend, attrs=args.attr):
- rtn = False
- else:
- rtn = True
- if rtn:
log.info("db2index successful")
- return rtn
- else:
- log.fatal("db2index failed")
- return rtn
+ return True
def dbtasks_db2bak(inst, log, args):
# Needs an output name?
+ inst.log = log
if not inst.db2bak(args.archive):
log.fatal("db2bak failed")
return False
@@ -61,6 +47,7 @@ def dbtasks_db2bak(inst, log, args):
def dbtasks_bak2db(inst, log, args):
# Needs the archive to restore.
+ inst.log = log
if not inst.bak2db(args.archive):
log.fatal("bak2db failed")
return False
@@ -70,6 +57,7 @@ def dbtasks_bak2db(inst, log, args):
def dbtasks_db2ldif(inst, log, args):
# If export filename is provided, check if file path exists
+ inst.log = log
if args.ldif:
path = Path(args.ldif)
parent = path.parent.absolute()
@@ -88,6 +76,7 @@ def dbtasks_db2ldif(inst, log, args):
def dbtasks_ldif2db(inst, log, args):
# Check if ldif file exists
+ inst.log = log
if not os.path.exists(args.ldif):
raise ValueError("The LDIF file does not exist: " + args.ldif)
@@ -103,6 +92,7 @@ def dbtasks_ldif2db(inst, log, args):
def dbtasks_backups(inst, log, args):
+ inst.log = log
if args.delete:
# Delete backup
inst.del_backup(args.delete[0])
@@ -117,6 +107,7 @@ def dbtasks_backups(inst, log, args):
def dbtasks_ldifs(inst, log, args):
+ inst.log = log
if args.delete:
# Delete LDIF file
inst.del_ldif(args.delete[0])
@@ -131,6 +122,7 @@ def dbtasks_ldifs(inst, log, args):
def dbtasks_verify(inst, log, args):
+ inst.log = log
if not inst.dbverify(bename=args.backend):
log.fatal("dbverify failed")
return False
@@ -521,9 +513,8 @@ def dbtasks_index_check(inst, log, args):
def create_parser(subcommands):
db2index_parser = subcommands.add_parser('db2index', help="Initialise a reindex of the server database. The server must be stopped for this to proceed.", formatter_class=CustomHelpFormatter)
- # db2index_parser.add_argument('suffix', help="The suffix to reindex. IE dc=example,dc=com.")
- db2index_parser.add_argument('backend', nargs="?", help="The backend to reindex. IE userRoot", default=False)
- db2index_parser.add_argument('--attr', nargs="*", help="The attribute's to reindex. IE --attr aci cn givenname", default=False)
+ db2index_parser.add_argument('backend', help="The backend to reindex. IE userRoot")
+ db2index_parser.add_argument('--attr', action='append', help="An attribute to reindex. IE: --attr member --attr cn ...")
db2index_parser.set_defaults(func=dbtasks_db2index)
db2bak_parser = subcommands.add_parser('db2bak', help="Initialise a BDB backup of the database. The server must be stopped for this to proceed.", formatter_class=CustomHelpFormatter)
--
2.52.0

View File

@ -1,69 +0,0 @@
From e9fe6e074130406328b8e932a5c2efa814d190a0 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Wed, 5 Feb 2025 09:41:30 +0100
Subject: [PATCH] Issue 6004 - (2nd) idletimeout may be ignored (#6569)
Problem:
multiple listener threads was implemented in 2.x and after
This is missing in 1.4.3 so the cherry pick should be adapted
Fix:
skip the loop with listeners
Issue #6004
Reviewed by: Jamie Chapman (Thanks !)
---
ldap/servers/slapd/daemon.c | 36 +++++++++++++++++-------------------
1 file changed, 17 insertions(+), 19 deletions(-)
diff --git a/ldap/servers/slapd/daemon.c b/ldap/servers/slapd/daemon.c
index 6df109760..bef75e4a3 100644
--- a/ldap/servers/slapd/daemon.c
+++ b/ldap/servers/slapd/daemon.c
@@ -1066,26 +1066,24 @@ check_idletimeout(time_t when __attribute__((unused)), void *arg __attribute__((
{
Connection_Table *ct = the_connection_table;
time_t curtime = slapi_current_rel_time_t();
- /* Walk all active connections of all connection listeners */
- for (int list_num = 0; list_num < ct->list_num; list_num++) {
- for (Connection *c = connection_table_get_first_active_connection(ct, list_num);
- c != NULL; c = connection_table_get_next_active_connection(ct, c)) {
- if (!has_idletimeout_expired(c, curtime)) {
- continue;
- }
- /* Looks like idletimeout has expired, lets acquire the lock
- * and double check.
- */
- if (pthread_mutex_trylock(&(c->c_mutex)) == EBUSY) {
- continue;
- }
- if (has_idletimeout_expired(c, curtime)) {
- /* idle timeout has expired */
- disconnect_server_nomutex(c, c->c_connid, -1,
- SLAPD_DISCONNECT_IDLE_TIMEOUT, ETIMEDOUT);
- }
- pthread_mutex_unlock(&(c->c_mutex));
+ /* Walk all active connections */
+ for (Connection *c = connection_table_get_first_active_connection(ct);
+ c != NULL; c = connection_table_get_next_active_connection(ct, c)) {
+ if (!has_idletimeout_expired(c, curtime)) {
+ continue;
+ }
+ /* Looks like idletimeout has expired, lets acquire the lock
+ * and double check.
+ */
+ if (pthread_mutex_trylock(&(c->c_mutex)) == EBUSY) {
+ continue;
+ }
+ if (has_idletimeout_expired(c, curtime)) {
+ /* idle timeout has expired */
+ disconnect_server_nomutex(c, c->c_connid, -1,
+ SLAPD_DISCONNECT_IDLE_TIMEOUT, ETIMEDOUT);
}
+ pthread_mutex_unlock(&(c->c_mutex));
}
}
--
2.48.1

View File

@ -0,0 +1,41 @@
From c831a7af2afb7cfd2eb958476a7a8d421ac5d339 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Fri, 13 Feb 2026 15:38:52 +0100
Subject: [PATCH] Issue 7184 - (2nd) argparse.HelpFormatter
_format_actions_usage() is deprecated (#7257)
Description:
`_format_actions_usage()` was also removed in Python 3.14.3.
Replace version check with `isinstance()` to handle the return type of
`_get_actions_usage_parts()` more robustly across Python versions.
Relates: https://github.com/389ds/389-ds-base/issues/7184
Fixes: https://github.com/389ds/389-ds-base/issues/7253
Reviewed by: @progier389 (Thanks!)
---
src/lib389/lib389/cli_base/__init__.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/lib389/lib389/cli_base/__init__.py b/src/lib389/lib389/cli_base/__init__.py
index f1055aadc..3af8a46e6 100644
--- a/src/lib389/lib389/cli_base/__init__.py
+++ b/src/lib389/lib389/cli_base/__init__.py
@@ -420,11 +420,11 @@ class CustomHelpFormatter(argparse.HelpFormatter):
else:
# Use _get_actions_usage_parts() for Python 3.13 and later
action_parts = self._get_actions_usage_parts(parent_arguments, [])
- if sys.version_info >= (3, 15):
- # Python 3.15 returns a tuple (list of actions, count of actions)
+ if isinstance(action_parts, tuple):
+ # Python 3.14.3+ and 3.15+ return a tuple (list of actions, count of actions)
formatted_options = ' '.join(action_parts[0])
else:
- # Python 3.13 and 3.14 return a list of actions
+ # Earlier versions return a list of actions
formatted_options = ' '.join(action_parts)
# If formatted_options already in usage - remove them
--
2.52.0

View File

@ -1,52 +0,0 @@
From b2edc371c5ca4fd24ef469c64829c48824098e7f Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Wed, 8 Jan 2025 12:57:52 -0500
Subject: [PATCH] Issue 6485 - Fix double free in USN cleanup task
Description:
ASAN report shows double free of bind dn in the USN cleanup task data. The bind
dn was passed as a reference so it should never have to be freed by the cleanup
task.
Relates: https://github.com/389ds/389-ds-base/issues/6485
Reviewed by: tbordaz(Thanks!)
---
ldap/servers/plugins/usn/usn_cleanup.c | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/ldap/servers/plugins/usn/usn_cleanup.c b/ldap/servers/plugins/usn/usn_cleanup.c
index bdb55e6b1..7eaf0f88f 100644
--- a/ldap/servers/plugins/usn/usn_cleanup.c
+++ b/ldap/servers/plugins/usn/usn_cleanup.c
@@ -240,7 +240,7 @@ usn_cleanup_add(Slapi_PBlock *pb,
char *suffix = NULL;
char *backend = NULL;
char *maxusn = NULL;
- char *bind_dn;
+ char *bind_dn = NULL;
struct usn_cleanup_data *cleanup_data = NULL;
int rv = SLAPI_DSE_CALLBACK_OK;
Slapi_Task *task = NULL;
@@ -323,8 +323,7 @@ usn_cleanup_add(Slapi_PBlock *pb,
suffix = NULL; /* don't free in this function */
cleanup_data->maxusn_to_delete = maxusn;
maxusn = NULL; /* don't free in this function */
- cleanup_data->bind_dn = bind_dn;
- bind_dn = NULL; /* don't free in this function */
+ cleanup_data->bind_dn = slapi_ch_strdup(bind_dn);
slapi_task_set_data(task, cleanup_data);
/* start the USN tombstone cleanup task as a separate thread */
@@ -363,7 +362,6 @@ usn_cleanup_task_destructor(Slapi_Task *task)
slapi_ch_free_string(&mydata->suffix);
slapi_ch_free_string(&mydata->maxusn_to_delete);
slapi_ch_free_string(&mydata->bind_dn);
- /* Need to cast to avoid a compiler warning */
slapi_ch_free((void **)&mydata);
}
}
--
2.48.1

View File

@ -0,0 +1,32 @@
From a7f8eb79dde03ac771de2335a2acb4aed48483a2 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Fri, 13 Feb 2026 16:27:25 +0100
Subject: [PATCH] Issue 7213 - (2nd) MDB_BAD_VALSIZE error while handling VLV
(#7258)
Decription:
Disable test_vlv_long_attribute_value on BDB as it hangs sometimes in
CI, blocking other pipelines.
Relates: https://github.com/389ds/389-ds-base/issues/7213
Reviewed by: @progier389 (Thanks!)
---
dirsrvtests/tests/suites/vlv/regression_test.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/dirsrvtests/tests/suites/vlv/regression_test.py b/dirsrvtests/tests/suites/vlv/regression_test.py
index 1cc03c303..94001af62 100644
--- a/dirsrvtests/tests/suites/vlv/regression_test.py
+++ b/dirsrvtests/tests/suites/vlv/regression_test.py
@@ -1178,6 +1178,7 @@ def test_vlv_with_mr(vlv_setup_with_uid_mr):
+@pytest.mark.skipif(get_default_db_lib() == "bdb", reason="Hangs on BDB")
def test_vlv_long_attribute_value(topology_st, request):
"""
Test VLV with an entry containing a very long attribute value (2K).
--
2.52.0

View File

@ -1,7 +1,7 @@
From 384dbcaba418afe9e4de798d16a68d0468632a57 Mon Sep 17 00:00:00 2001
From 75b1213dfb94495160a735c2688cee03d6be5a9a Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Fri, 13 Feb 2026 13:08:40 +0100
Subject: [PATCH] Issue 7223 - Use lexicographical order for ancestorid
Date: Fri, 13 Feb 2026 16:58:24 +0100
Subject: [PATCH] Issue 7223 - Use lexicographical order for ancestorid (#7256)
Description:
`ldbm_instance_create_default_indexes()` configured ancestorid with
@ -11,13 +11,13 @@ ldif2db import).
Relates: https://github.com/389ds/389-ds-base/issues/7223
Reviewed by: @progier389, @tbordaz (Thanks!)
Reviewed by: @tbordaz (Thanks!)
---
ldap/servers/slapd/back-ldbm/instance.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ldap/servers/slapd/back-ldbm/instance.c b/ldap/servers/slapd/back-ldbm/instance.c
index 388dd6efb..f5342b99a 100644
index 3fcdb5554..563cf96db 100644
--- a/ldap/servers/slapd/back-ldbm/instance.c
+++ b/ldap/servers/slapd/back-ldbm/instance.c
@@ -239,7 +239,7 @@ ldbm_instance_create_default_indexes(backend *be)

View File

@ -1,38 +0,0 @@
From 679262c0c292413851d2d004b588ecfd7d91c85a Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Tue, 11 Feb 2025 18:06:34 +0000
Subject: [PATCH] Issue 5841 - dsconf incorrectly setting up Pass-Through
Authentication (#6601)
Bug description:
During init, PAMPassThroughAuthConfigs defines an "objectclass=nsslapdplugin"
plugin object. During filter creation, dsconf fails as objectclass=nsslapdplugin
is not present in the PAM PT config entry. This objectclass has been removed in
all other branches, branch 1.4.3 was skipped as there are cherry pick conflicts.
Fix description:
Remove nsslapdplugin from the plugin objecti, objectclass list.
Fixes: https://github.com/389ds/389-ds-base/issues/5841
Reviewed by: @progier389 (Thank you)
---
src/lib389/lib389/plugins.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/lib389/lib389/plugins.py b/src/lib389/lib389/plugins.py
index 185398e5b..25b49dae4 100644
--- a/src/lib389/lib389/plugins.py
+++ b/src/lib389/lib389/plugins.py
@@ -1579,7 +1579,7 @@ class PAMPassThroughAuthConfigs(DSLdapObjects):
def __init__(self, instance, basedn="cn=PAM Pass Through Auth,cn=plugins,cn=config"):
super(PAMPassThroughAuthConfigs, self).__init__(instance)
- self._objectclasses = ['top', 'extensibleObject', 'nsslapdplugin', 'pamConfig']
+ self._objectclasses = ['top', 'extensibleObject', 'pamConfig']
self._filterattrs = ['cn']
self._scope = ldap.SCOPE_ONELEVEL
self._childobject = PAMPassThroughAuthConfig
--
2.48.1

View File

@ -0,0 +1,89 @@
From 7809104b9edb2d05ceee7ca819152f365cc1beb0 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Wed, 11 Feb 2026 15:51:47 -0500
Subject: [PATCH] Issue 7066/7052 - allow password history to be set to zero
and remove history
Description:
For local password policies the server was incorrectly rejecting updates that
set the value to zero. When password history is set to zero the old passwords
in the entry history are not cleaned as expected.
relates: https://github.com/389ds/389-ds-base/issues/7052
relates: https://github.com/389ds/389-ds-base/issues/7066
Reviewed by: progier(Thanks!)
---
.../tests/suites/password/pwp_history_test.py | 9 ++++++---
ldap/servers/slapd/modify.c | 2 +-
ldap/servers/slapd/pw.c | 13 ++++++++++++-
3 files changed, 19 insertions(+), 5 deletions(-)
diff --git a/dirsrvtests/tests/suites/password/pwp_history_test.py b/dirsrvtests/tests/suites/password/pwp_history_test.py
index 8a15a1986..387824bc5 100644
--- a/dirsrvtests/tests/suites/password/pwp_history_test.py
+++ b/dirsrvtests/tests/suites/password/pwp_history_test.py
@@ -108,8 +108,12 @@ def test_history_is_not_overwritten(topology_st, user):
user.set('userpassword', USER_PWD)
-def test_basic(topology_st, user):
- """Test basic password policy history feature functionality
+@pytest.mark.parametrize('policy',
+ [(pytest.param('global')),
+ (pytest.param('subtree')),
+ (pytest.param('user'))])
+def test_basic(topology_st, user, policy):
+ """Test basic password policy history feature functionality with dynamic count reduction
:id: 83d74f7d-3036-4944-8839-1b40bbf265ff
:setup: Standalone instance, a test user
@@ -238,7 +242,6 @@ def test_basic(topology_st, user):
#
# Reset password by Directory Manager(admin reset)
- #
dm = DirectoryManager(topology_st.standalone)
dm.rebind()
time.sleep(.5)
diff --git a/ldap/servers/slapd/modify.c b/ldap/servers/slapd/modify.c
index b0066faf8..d76427886 100644
--- a/ldap/servers/slapd/modify.c
+++ b/ldap/servers/slapd/modify.c
@@ -87,7 +87,7 @@ static struct attr_value_check
{CONFIG_PW_WARNING_ATTRIBUTE, check_pw_duration_value, 0, -1},
{CONFIG_PW_MINLENGTH_ATTRIBUTE, attr_check_minmax, 2, 512},
{CONFIG_PW_MAXFAILURE_ATTRIBUTE, attr_check_minmax, 1, 32767},
- {CONFIG_PW_INHISTORY_ATTRIBUTE, attr_check_minmax, 1, 24},
+ {CONFIG_PW_INHISTORY_ATTRIBUTE, attr_check_minmax, 0, 24},
{CONFIG_PW_LOCKDURATION_ATTRIBUTE, check_pw_duration_value, -1, -1},
{CONFIG_PW_RESETFAILURECOUNT_ATTRIBUTE, check_pw_resetfailurecount_value, -1, -1},
{CONFIG_PW_GRACELIMIT_ATTRIBUTE, attr_check_minmax, 0, -1},
diff --git a/ldap/servers/slapd/pw.c b/ldap/servers/slapd/pw.c
index cda1c404f..12cf759ce 100644
--- a/ldap/servers/slapd/pw.c
+++ b/ldap/servers/slapd/pw.c
@@ -1532,7 +1532,18 @@ update_pw_history(Slapi_PBlock *pb, const Slapi_DN *sdn, char *old_pw)
pwpolicy = new_passwdPolicy(pb, dn);
if (pwpolicy->pw_inhistory == 0){
- /* We are only enforcing the current password, just return */
+ /* We are only enforcing the current password, just return but first
+ * cleanup any old passwords in the history */
+ attribute.mod_type = "passwordHistory";
+ attribute.mod_op = LDAP_MOD_REPLACE;
+ attribute.mod_values = NULL;
+ list_of_mods[0] = &attribute;
+ list_of_mods[1] = NULL;
+ mod_pb = slapi_pblock_new();
+ slapi_modify_internal_set_pb_ext(mod_pb, sdn, list_of_mods, NULL, NULL, pw_get_componentID(), 0);
+ slapi_modify_internal_pb(mod_pb);
+ slapi_pblock_destroy(mod_pb);
+
return res;
}
--
2.52.0

View File

@ -0,0 +1,260 @@
From 4209ff65310c54b6ab55594984201b1214fb75b2 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Mon, 16 Feb 2026 16:52:52 -0500
Subject: [PATCH] Issue 7243 - UI - fix certificate table and modal
Description:
The certificate table was not handling sorting and pagination correctly, and
the add cert modal was not correctly selecting certs on disk
This was fixed upstream for the hot cetificate features, but only backporting
the table/modal changes to older branches
relates: https://github.com/389ds/389-ds-base/issues/7243
Reviewed by: spichugi(Thanks!)
---
.../src/lib/security/securityModals.jsx | 4 +-
.../src/lib/security/securityTables.jsx | 85 ++++++-------------
2 files changed, 29 insertions(+), 60 deletions(-)
diff --git a/src/cockpit/389-console/src/lib/security/securityModals.jsx b/src/cockpit/389-console/src/lib/security/securityModals.jsx
index a42aa50d6..c4af753c0 100644
--- a/src/cockpit/389-console/src/lib/security/securityModals.jsx
+++ b/src/cockpit/389-console/src/lib/security/securityModals.jsx
@@ -298,7 +298,9 @@ export class SecurityAddCertModal extends React.Component {
<FormSelect
value={selectCertName}
id="selectCertName"
- onChange={handleCertSelect}
+ onChange={(e, str) => {
+ handleCertSelect(str);
+ }}
aria-label="FormSelect Input"
className="ds-cert-select"
validated={selectValidated}
diff --git a/src/cockpit/389-console/src/lib/security/securityTables.jsx b/src/cockpit/389-console/src/lib/security/securityTables.jsx
index fce4cb04e..a6c64e6c1 100644
--- a/src/cockpit/389-console/src/lib/security/securityTables.jsx
+++ b/src/cockpit/389-console/src/lib/security/securityTables.jsx
@@ -4,7 +4,6 @@ import {
Grid,
GridItem,
Pagination,
- PaginationVariant,
SearchInput,
Tooltip,
} from '@patternfly/react-core';
@@ -94,10 +93,10 @@ class KeyTable extends React.Component {
];
handleSort(_event, index, direction) {
- const sortedRows = [...this.state.rows].sort((a, b) =>
+ const sortedRows = [...this.state.rows].sort((a, b) =>
(a[index] < b[index] ? -1 : a[index] > b[index] ? 1 : 0)
);
-
+
this.setState({
sortBy: {
index,
@@ -127,7 +126,7 @@ class KeyTable extends React.Component {
>
<a className="ds-font-size-sm">{_("What is an orphan key?")}</a>
</Tooltip>
- <Table
+ <Table
className="ds-margin-top"
aria-label="orph key table"
variant="compact"
@@ -135,7 +134,7 @@ class KeyTable extends React.Component {
<Thead>
<Tr>
{columns.map((column, idx) => (
- <Th
+ <Th
key={idx}
sort={column.sortable ? {
sortBy,
@@ -163,7 +162,7 @@ class KeyTable extends React.Component {
)}
{hasRows && (
<Td isActionCell>
- <ActionsColumn
+ <ActionsColumn
items={this.getActionsForRow(row)}
/>
</Td>
@@ -224,7 +223,7 @@ class CSRTable extends React.Component {
}
handleSort(_event, index, direction) {
- const sortedRows = [...this.state.rows].sort((a, b) =>
+ const sortedRows = [...this.state.rows].sort((a, b) =>
(a[index] < b[index] ? -1 : a[index] > b[index] ? 1 : 0)
);
this.setState({
@@ -316,7 +315,7 @@ class CSRTable extends React.Component {
onClear={(evt) => this.handleSearchChange(evt, '')}
/>
}
- <Table
+ <Table
className="ds-margin-top"
aria-label="csr table"
variant="compact"
@@ -324,7 +323,7 @@ class CSRTable extends React.Component {
<Thead>
<Tr>
{columns.map((column, idx) => (
- <Th
+ <Th
key={idx}
sort={column.sortable ? {
sortBy,
@@ -352,7 +351,7 @@ class CSRTable extends React.Component {
)}
{hasRows && (
<Td isActionCell>
- <ActionsColumn
+ <ActionsColumn
items={this.getActionsForRow(row)}
/>
</Td>
@@ -418,41 +417,11 @@ class CertTable extends React.Component {
}
handleSort(_event, columnIndex, direction) {
- const sorted_rows = [];
- const rows = [];
- let count = 0;
-
- // Convert the rows pairings into a sortable array
- for (let idx = 0; idx < this.state.rows.length; idx += 2) {
- sorted_rows.push({
- expandedRow: this.state.rows[idx + 1],
- 1: this.state.rows[idx].cells[0].content,
- 2: this.state.rows[idx].cells[1].content,
- 3: this.state.rows[idx].cells[2].content,
- issuer: this.state.rows[idx].issuer,
- flags: this.state.rows[idx].flags
- });
- }
+ const rows = [...this.state.rows];
- sorted_rows.sort((a, b) => (a[columnIndex + 1] > b[columnIndex + 1]) ? 1 : -1);
+ rows.sort((a, b) => (a.cells[columnIndex].content > b.cells[columnIndex].content) ? 1 : -1);
if (direction !== SortByDirection.asc) {
- sorted_rows.reverse();
- }
-
- for (const srow of sorted_rows) {
- rows.push({
- isOpen: false,
- cells: [
- { content: srow[1] },
- { content: srow[2] },
- { content: srow[3] }
- ],
- issuer: srow.issuer,
- flags: srow.flags,
- });
- srow.expandedRow.parent = count;
- rows.push(srow.expandedRow);
- count += 2;
+ rows.reverse();
}
this.setState({
@@ -534,18 +503,16 @@ class CertTable extends React.Component {
rows.push(
{
isOpen: false,
- cells: [cert.attrs.nickname, cert.attrs.subject, cert.attrs.expires],
+ cells: [
+ { content: cert.attrs.nickname },
+ { content: cert.attrs.subject },
+ { content: cert.attrs.expires }
+ ],
issuer: cert.attrs.issuer,
flags: cert.attrs.flags,
-
- },
- {
- parent: count,
- fullWidth: true,
- cells: [{ title: this.getExpandedRow(cert.attrs.issuer, cert.attrs.flags) }]
- },
+ }
);
- count += 2;
+ count += 1;
}
this.setState({
@@ -587,7 +554,7 @@ class CertTable extends React.Component {
onChange={this.handleSearchChange}
onClear={(evt) => this.handleSearchChange(evt, '')}
/>}
- <Table
+ <Table
aria-label="cert table"
variant='compact'
>
@@ -626,7 +593,7 @@ class CertTable extends React.Component {
</Td>
))}
<Td isActionCell>
- <ActionsColumn
+ <ActionsColumn
items={this.getActionsForRow(row)}
/>
</Td>
@@ -646,7 +613,7 @@ class CertTable extends React.Component {
</Table>
{hasRows &&
<Pagination
- itemCount={this.state.rows.length / 2}
+ itemCount={this.state.rows.length}
widgetId="pagination-options-menu-bottom"
perPage={perPage}
page={page}
@@ -728,7 +695,7 @@ class CRLTable extends React.Component {
if (direction !== SortByDirection.asc) {
sorted_rows.reverse();
}
-
+
for (const srow of sorted_rows) {
rows.push({
isOpen: false,
@@ -806,14 +773,14 @@ class CRLTable extends React.Component {
onChange={this.handleSearchChange}
onClear={(evt) => this.handleSearchChange(evt, '')}
/>
- <Table
+ <Table
aria-label="CRL Table"
variant="compact"
>
<Thead>
<Tr>
{this.state.columns.map((column, idx) => (
- <Th
+ <Th
key={idx}
sort={column.sortable ? {
sortBy: this.state.sortBy,
@@ -836,7 +803,7 @@ class CRLTable extends React.Component {
))}
{this.state.hasRows && (
<Td isActionCell>
- <ActionsColumn
+ <ActionsColumn
items={this.getActionsForRow(row)}
/>
</Td>
--
2.52.0

View File

@ -1,319 +0,0 @@
From 7d534efdcd96b13524dae587c3c5994ed01924ab Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Fri, 16 Feb 2024 13:52:36 -0800
Subject: [PATCH] Issue 6067 - Improve dsidm CLI No Such Entry handling (#6079)
Description: Add additional error processing to dsidm CLI tool for when basedn
or OU subentries are absent.
Related: https://github.com/389ds/389-ds-base/issues/6067
Reviewed by: @vashirov (Thanks!)
---
src/lib389/cli/dsidm | 21 ++++++++-------
src/lib389/lib389/cli_idm/__init__.py | 38 ++++++++++++++++++++++++++-
src/lib389/lib389/cli_idm/account.py | 4 +--
src/lib389/lib389/cli_idm/service.py | 4 ++-
src/lib389/lib389/idm/group.py | 10 ++++---
src/lib389/lib389/idm/posixgroup.py | 5 ++--
src/lib389/lib389/idm/services.py | 5 ++--
src/lib389/lib389/idm/user.py | 5 ++--
8 files changed, 67 insertions(+), 25 deletions(-)
diff --git a/src/lib389/cli/dsidm b/src/lib389/cli/dsidm
index 1b739b103..970973f4f 100755
--- a/src/lib389/cli/dsidm
+++ b/src/lib389/cli/dsidm
@@ -2,7 +2,7 @@
# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2016, William Brown <william at blackhats.net.au>
-# Copyright (C) 2023 Red Hat, Inc.
+# Copyright (C) 2024 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
@@ -19,6 +19,7 @@ import argparse
import argcomplete
from lib389.utils import get_instance_list, instance_choices
from lib389._constants import DSRC_HOME
+from lib389.cli_idm import _get_basedn_arg
from lib389.cli_idm import account as cli_account
from lib389.cli_idm import initialise as cli_init
from lib389.cli_idm import organizationalunit as cli_ou
@@ -117,14 +118,6 @@ if __name__ == '__main__':
parser.print_help()
sys.exit(1)
- if dsrc_inst['basedn'] is None:
- errmsg = "Must provide a basedn!"
- if args.json:
- sys.stderr.write('{"desc": "%s"}\n' % errmsg)
- else:
- log.error(errmsg)
- sys.exit(1)
-
if not args.verbose:
signal.signal(signal.SIGINT, signal_handler)
@@ -135,7 +128,15 @@ if __name__ == '__main__':
result = False
try:
inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose, args=args)
- result = args.func(inst, dsrc_inst['basedn'], log, args)
+ basedn = _get_basedn_arg(inst, args, log, msg="Enter basedn")
+ if basedn is None:
+ errmsg = "Must provide a basedn!"
+ if args.json:
+ sys.stderr.write('{"desc": "%s"}\n' % errmsg)
+ else:
+ log.error(errmsg)
+ sys.exit(1)
+ result = args.func(inst, basedn, log, args)
if args.verbose:
log.info("Command successful.")
except Exception as e:
diff --git a/src/lib389/lib389/cli_idm/__init__.py b/src/lib389/lib389/cli_idm/__init__.py
index 0dab54847..e3622246d 100644
--- a/src/lib389/lib389/cli_idm/__init__.py
+++ b/src/lib389/lib389/cli_idm/__init__.py
@@ -1,15 +1,30 @@
# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2016, William Brown <william at blackhats.net.au>
-# Copyright (C) 2023 Red Hat, Inc.
+# Copyright (C) 2024 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---
+import sys
import ldap
from getpass import getpass
import json
+from lib389._mapped_object import DSLdapObject
+from lib389.cli_base import _get_dn_arg
+from lib389.idm.user import DEFAULT_BASEDN_RDN as DEFAULT_BASEDN_RDN_USER
+from lib389.idm.group import DEFAULT_BASEDN_RDN as DEFAULT_BASEDN_RDN_GROUP
+from lib389.idm.posixgroup import DEFAULT_BASEDN_RDN as DEFAULT_BASEDN_RDN_POSIXGROUP
+from lib389.idm.services import DEFAULT_BASEDN_RDN as DEFAULT_BASEDN_RDN_SERVICES
+
+# The key is module name, the value is default RDN
+BASEDN_RDNS = {
+ 'user': DEFAULT_BASEDN_RDN_USER,
+ 'group': DEFAULT_BASEDN_RDN_GROUP,
+ 'posixgroup': DEFAULT_BASEDN_RDN_POSIXGROUP,
+ 'service': DEFAULT_BASEDN_RDN_SERVICES,
+}
def _get_arg(args, msg=None):
@@ -37,6 +52,27 @@ def _get_args(args, kws):
return kwargs
+def _get_basedn_arg(inst, args, log, msg=None):
+ basedn_arg = _get_dn_arg(args.basedn, msg="Enter basedn")
+ if not DSLdapObject(inst, basedn_arg).exists():
+ raise ValueError(f'The base DN "{basedn_arg}" does not exist.')
+
+ # Get the RDN based on the last part of the module name if applicable
+ # (lib389.cli_idm.user -> user)
+ try:
+ command_name = args.func.__module__.split('.')[-1]
+ object_rdn = BASEDN_RDNS[command_name]
+ # Check if the DN for our command exists
+ command_basedn = f'{object_rdn},{basedn_arg}'
+ if not DSLdapObject(inst, command_basedn).exists():
+ errmsg = f'The DN "{command_basedn}" does not exist.'
+ errmsg += f' It is required for "{command_name}" subcommand. Please create it first.'
+ raise ValueError(errmsg)
+ except KeyError:
+ pass
+ return basedn_arg
+
+
# This is really similar to get_args, but generates from an array
def _get_attributes(args, attrs):
kwargs = {}
diff --git a/src/lib389/lib389/cli_idm/account.py b/src/lib389/lib389/cli_idm/account.py
index 5d7b9cc77..15f766588 100644
--- a/src/lib389/lib389/cli_idm/account.py
+++ b/src/lib389/lib389/cli_idm/account.py
@@ -1,5 +1,5 @@
# --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2023, Red Hat inc,
+# Copyright (C) 2024, Red Hat inc,
# Copyright (C) 2018, William Brown <william@blackhats.net.au>
# All rights reserved.
#
@@ -91,7 +91,6 @@ def entry_status(inst, basedn, log, args):
def subtree_status(inst, basedn, log, args):
- basedn = _get_dn_arg(args.basedn, msg="Enter basedn to check")
filter = ""
scope = ldap.SCOPE_SUBTREE
epoch_inactive_time = None
@@ -121,7 +120,6 @@ def subtree_status(inst, basedn, log, args):
def bulk_update(inst, basedn, log, args):
- basedn = _get_dn_arg(args.basedn, msg="Enter basedn to search")
search_filter = "(objectclass=*)"
scope = ldap.SCOPE_SUBTREE
scope_str = "sub"
diff --git a/src/lib389/lib389/cli_idm/service.py b/src/lib389/lib389/cli_idm/service.py
index c62fc12d1..c2b2c8c84 100644
--- a/src/lib389/lib389/cli_idm/service.py
+++ b/src/lib389/lib389/cli_idm/service.py
@@ -57,7 +57,9 @@ def rename(inst, basedn, log, args, warn=True):
_generic_rename(inst, basedn, log.getChild('_generic_rename'), MANY, rdn, args)
def create_parser(subparsers):
- service_parser = subparsers.add_parser('service', help='Manage service accounts', formatter_class=CustomHelpFormatter)
+ service_parser = subparsers.add_parser('service',
+ help='Manage service accounts. The organizationalUnit (by default "ou=Services") '
+ 'needs to exist prior to managing service accounts.', formatter_class=CustomHelpFormatter)
subcommands = service_parser.add_subparsers(help='action')
diff --git a/src/lib389/lib389/idm/group.py b/src/lib389/lib389/idm/group.py
index 1b60a1f51..2cf2c7b23 100644
--- a/src/lib389/lib389/idm/group.py
+++ b/src/lib389/lib389/idm/group.py
@@ -1,6 +1,6 @@
# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2016, William Brown <william at blackhats.net.au>
-# Copyright (C) 2023 Red Hat, Inc.
+# Copyright (C) 2024 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
@@ -16,6 +16,8 @@ MUST_ATTRIBUTES = [
'cn',
]
RDN = 'cn'
+DEFAULT_BASEDN_RDN = 'ou=Groups'
+DEFAULT_BASEDN_RDN_ADMIN_GROUPS = 'ou=People'
class Group(DSLdapObject):
@@ -93,7 +95,7 @@ class Groups(DSLdapObjects):
:type basedn: str
"""
- def __init__(self, instance, basedn, rdn='ou=Groups'):
+ def __init__(self, instance, basedn, rdn=DEFAULT_BASEDN_RDN):
super(Groups, self).__init__(instance)
self._objectclasses = [
'groupOfNames',
@@ -140,7 +142,7 @@ class UniqueGroup(DSLdapObject):
class UniqueGroups(DSLdapObjects):
# WARNING!!!
# Use group, not unique group!!!
- def __init__(self, instance, basedn, rdn='ou=Groups'):
+ def __init__(self, instance, basedn, rdn=DEFAULT_BASEDN_RDN):
super(UniqueGroups, self).__init__(instance)
self._objectclasses = [
'groupOfUniqueNames',
@@ -203,7 +205,7 @@ class nsAdminGroups(DSLdapObjects):
:type rdn: str
"""
- def __init__(self, instance, basedn, rdn='ou=People'):
+ def __init__(self, instance, basedn, rdn=DEFAULT_BASEDN_RDN_ADMIN_GROUPS):
super(nsAdminGroups, self).__init__(instance)
self._objectclasses = [
'nsAdminGroup'
diff --git a/src/lib389/lib389/idm/posixgroup.py b/src/lib389/lib389/idm/posixgroup.py
index d1debcf12..45735c579 100644
--- a/src/lib389/lib389/idm/posixgroup.py
+++ b/src/lib389/lib389/idm/posixgroup.py
@@ -1,6 +1,6 @@
# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2016, William Brown <william at blackhats.net.au>
-# Copyright (C) 2023 Red Hat, Inc.
+# Copyright (C) 2024 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
@@ -17,6 +17,7 @@ MUST_ATTRIBUTES = [
'gidNumber',
]
RDN = 'cn'
+DEFAULT_BASEDN_RDN = 'ou=Groups'
class PosixGroup(DSLdapObject):
@@ -72,7 +73,7 @@ class PosixGroups(DSLdapObjects):
:type basedn: str
"""
- def __init__(self, instance, basedn, rdn='ou=Groups'):
+ def __init__(self, instance, basedn, rdn=DEFAULT_BASEDN_RDN):
super(PosixGroups, self).__init__(instance)
self._objectclasses = [
'groupOfNames',
diff --git a/src/lib389/lib389/idm/services.py b/src/lib389/lib389/idm/services.py
index d1e5b4693..e750a32c4 100644
--- a/src/lib389/lib389/idm/services.py
+++ b/src/lib389/lib389/idm/services.py
@@ -1,6 +1,6 @@
# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2016, William Brown <william at blackhats.net.au>
-# Copyright (C) 2021 Red Hat, Inc.
+# Copyright (C) 2024 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
@@ -16,6 +16,7 @@ RDN = 'cn'
MUST_ATTRIBUTES = [
'cn',
]
+DEFAULT_BASEDN_RDN = 'ou=Services'
class ServiceAccount(Account):
"""A single instance of Service entry
@@ -59,7 +60,7 @@ class ServiceAccounts(DSLdapObjects):
:type basedn: str
"""
- def __init__(self, instance, basedn, rdn='ou=Services'):
+ def __init__(self, instance, basedn, rdn=DEFAULT_BASEDN_RDN):
super(ServiceAccounts, self).__init__(instance)
self._objectclasses = [
'applicationProcess',
diff --git a/src/lib389/lib389/idm/user.py b/src/lib389/lib389/idm/user.py
index 1206a6e08..3b21ccf1c 100644
--- a/src/lib389/lib389/idm/user.py
+++ b/src/lib389/lib389/idm/user.py
@@ -1,6 +1,6 @@
# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2016, William Brown <william at blackhats.net.au>
-# Copyright (C) 2023 Red Hat, Inc.
+# Copyright (C) 2024 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
@@ -23,6 +23,7 @@ MUST_ATTRIBUTES = [
'homeDirectory',
]
RDN = 'uid'
+DEFAULT_BASEDN_RDN = 'ou=People'
TEST_USER_PROPERTIES = {
'uid': 'testuser',
@@ -201,7 +202,7 @@ class UserAccounts(DSLdapObjects):
:type rdn: str
"""
- def __init__(self, instance, basedn, rdn='ou=People'):
+ def __init__(self, instance, basedn, rdn=DEFAULT_BASEDN_RDN):
super(UserAccounts, self).__init__(instance)
self._objectclasses = [
'account',
--
2.48.1

View File

@ -1,4 +1,4 @@
From 67c3183380888f4af093b546e717f3e7451a41d6 Mon Sep 17 00:00:00 2001
From 09b6f770f15ee8d333377b7902e18b78f4f7008c Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Wed, 18 Feb 2026 09:26:57 +0100
Subject: [PATCH] Issue 7223 - Remove integerOrderingMatch requirement for
@ -19,17 +19,17 @@ Relates: https://github.com/389ds/389-ds-base/pull/7223
Reviewed by: @progier389, @tbordaz (Thanks!)
---
.../healthcheck/health_system_indexes_test.py | 83 ++++----------
ldap/servers/slapd/upgrade.c | 106 ------------------
ldap/servers/slapd/upgrade.c | 105 ------------------
rpm/389-ds-base.spec.in | 3 -
src/lib389/lib389/backend.py | 5 +-
src/lib389/lib389/cli_ctl/dbtasks.py | 99 ++++++++--------
5 files changed, 73 insertions(+), 223 deletions(-)
src/lib389/lib389/cli_ctl/dbtasks.py | 99 ++++++++---------
5 files changed, 73 insertions(+), 222 deletions(-)
diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
index ce86239e5..088b48587 100644
index 3b3651b38..71a9f590a 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -179,7 +179,8 @@ def test_missing_parentid(topology_st, log_buffering_enabled):
@@ -180,7 +180,8 @@ def test_missing_parentid(topology_st, log_buffering_enabled):
def test_missing_matching_rule(topology_st, log_buffering_enabled):
@ -39,7 +39,7 @@ index ce86239e5..088b48587 100644
:id: 7ffa71db-8995-430a-bed8-59bce944221c
:setup: Standalone instance
@@ -189,19 +190,14 @@ def test_missing_matching_rule(topology_st, log_buffering_enabled):
@@ -190,19 +191,14 @@ def test_missing_matching_rule(topology_st, log_buffering_enabled):
3. Use healthcheck without --json option
4. Use healthcheck with --json option
5. Re-add the matching rule
@ -61,7 +61,7 @@ index ce86239e5..088b48587 100644
PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
standalone = topology_st.standalone
@@ -210,16 +206,13 @@ def test_missing_matching_rule(topology_st, log_buffering_enabled):
@@ -211,17 +207,14 @@ def test_missing_matching_rule(topology_st, log_buffering_enabled):
parentid_index = Index(standalone, PARENTID_DN)
parentid_index.remove("nsMatchingRule", "integerOrderingMatch")
@ -73,6 +73,7 @@ index ce86239e5..088b48587 100644
log.info("Re-add the integerOrderingMatch matching rule")
parentid_index = Index(standalone, PARENTID_DN)
parentid_index.add("nsMatchingRule", "integerOrderingMatch")
standalone.restart()
- run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
@ -80,7 +81,7 @@ index ce86239e5..088b48587 100644
def test_usn_plugin_missing_entryusn(topology_st, usn_plugin_enabled, log_buffering_enabled):
"""Check if healthcheck returns DSBLE0007 code when USN plugin is enabled but entryusn index is missing
@@ -908,7 +901,9 @@ def test_index_check_fixes_ancestorid_config(topology_st):
@@ -911,7 +904,9 @@ def test_index_check_fixes_ancestorid_config(topology_st):
def test_index_check_fixes_missing_matching_rule(topology_st):
@ -91,7 +92,7 @@ index ce86239e5..088b48587 100644
:id: 6c1d4e9f-0a3b-4d5c-1e7f-8a9b0c2d3e4f
:setup: Standalone instance
@@ -916,18 +911,14 @@ def test_index_check_fixes_missing_matching_rule(topology_st):
@@ -919,18 +914,14 @@ def test_index_check_fixes_missing_matching_rule(topology_st):
1. Create DS instance
2. Stop the server
3. Remove integerOrderingMatch from parentid index using DSEldif
@ -114,7 +115,7 @@ index ce86239e5..088b48587 100644
"""
from lib389.cli_ctl.dbtasks import dbtasks_index_check
from lib389.dseldif import DSEldif
@@ -961,34 +952,20 @@ def test_index_check_fixes_missing_matching_rule(topology_st):
@@ -964,34 +955,20 @@ def test_index_check_fixes_missing_matching_rule(topology_st):
f"integerOrderingMatch should be removed, but found: {mr}"
log.info("integerOrderingMatch removed from parentid index")
@ -156,7 +157,7 @@ index ce86239e5..088b48587 100644
log.info("Start the server")
standalone.start()
@@ -1078,7 +1055,7 @@ def test_index_check_fixes_multiple_issues(topology_st):
@@ -1081,7 +1058,7 @@ def test_index_check_fixes_multiple_issues(topology_st):
:steps:
1. Create DS instance
2. Stop the server
@ -165,7 +166,7 @@ index ce86239e5..088b48587 100644
4. Run dsctl index-check (should detect all issues)
5. Run dsctl index-check --fix
6. Verify all issues were fixed
@@ -1120,14 +1097,6 @@ def test_index_check_fixes_multiple_issues(topology_st):
@@ -1123,14 +1100,6 @@ def test_index_check_fixes_multiple_issues(topology_st):
]
dse_ldif.add_entry(ancestorid_entry)
@ -180,7 +181,7 @@ index ce86239e5..088b48587 100644
log.info("Run index-check without --fix (should detect all issues)")
args = FakeArgs()
args.backend = "userRoot"
@@ -1158,16 +1127,6 @@ def test_index_check_fixes_multiple_issues(topology_st):
@@ -1161,16 +1130,6 @@ def test_index_check_fixes_multiple_issues(topology_st):
cn_value = dse_ldif.get(ancestorid_dn, "cn", single=True)
assert cn_value is None, f"ancestorid config should be removed, got: {cn_value}"
@ -198,15 +199,15 @@ index ce86239e5..088b48587 100644
log.info("Run index-check again to confirm all clear")
diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
index aa51e72d2..4eb09ca38 100644
index d9156cae9..537c38feb 100644
--- a/ldap/servers/slapd/upgrade.c
+++ b/ldap/servers/slapd/upgrade.c
@@ -243,108 +243,6 @@ upgrade_remove_ancestorid_index_config(void)
@@ -500,107 +500,6 @@ upgrade_remove_ancestorid_index_config(void)
return uresult;
}
-/*
- * Check if parentid indexes are missing the integerOrderingMatch
- * Check if parentid/ancestorid indexes are missing the integerOrderingMatch
- * matching rule.
- *
- * This function logs a warning if we detect this condition, advising
@ -306,11 +307,10 @@ index aa51e72d2..4eb09ca38 100644
-
- return uresult;
-}
-
upgrade_status
upgrade_server(void)
{
@@ -356,9 +254,5 @@ upgrade_server(void)
@@ -637,10 +536,6 @@ upgrade_server(void)
return UPGRADE_FAILURE;
}
@ -320,11 +320,12 @@ index aa51e72d2..4eb09ca38 100644
-
return UPGRADE_SUCCESS;
}
diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in
index 16b8d14c5..59e3a748f 100644
index 0175dfa7c..f78258f6a 100644
--- a/rpm/389-ds-base.spec.in
+++ b/rpm/389-ds-base.spec.in
@@ -529,9 +529,6 @@ for dir in "$instbase"/slapd-* ; do
@@ -667,9 +667,6 @@ for dir in "$instbase"/slapd-* ; do
else
echo "instance $inst is not running" >> "$output" 2>&1 || :
fi
@ -335,10 +336,10 @@ index 16b8d14c5..59e3a748f 100644
done
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
index 4babf6850..376596cd6 100644
index 1d9be4683..29a1796ac 100644
--- a/src/lib389/lib389/backend.py
+++ b/src/lib389/lib389/backend.py
@@ -541,9 +541,10 @@ class Backend(DSLdapObject):
@@ -617,9 +617,10 @@ class Backend(DSLdapObject):
# Default system indexes taken from ldap/servers/slapd/back-ldbm/instance.c
# Note: entryrdn and ancestorid are internal system indexes that are not
# exposed in cn=config - they are managed internally by the server.
@ -352,7 +353,7 @@ index 4babf6850..376596cd6 100644
'aci': {'types': ['pres'], 'matching_rule': None},
'nscpEntryDN': {'types': ['eq'], 'matching_rule': None},
diff --git a/src/lib389/lib389/cli_ctl/dbtasks.py b/src/lib389/lib389/cli_ctl/dbtasks.py
index 16da966d1..ea8a00cc3 100644
index cd96cdaf7..b02de203f 100644
--- a/src/lib389/lib389/cli_ctl/dbtasks.py
+++ b/src/lib389/lib389/cli_ctl/dbtasks.py
@@ -10,6 +10,7 @@
@ -363,7 +364,7 @@ index 16da966d1..ea8a00cc3 100644
import subprocess
from enum import Enum
from lib389._constants import TaskWarning
@@ -271,45 +272,53 @@ def _check_disk_ordering(db_dir, backend, index_name, dbscan_path, is_mdb, log):
@@ -263,45 +264,53 @@ def _check_disk_ordering(db_dir, backend, index_name, dbscan_path, is_mdb, log):
if not index_file:
return IndexOrdering.UNKNOWN
@ -430,7 +431,7 @@ index 16da966d1..ea8a00cc3 100644
except OSError as e:
log.warning(" Error running dbscan: %s", e)
return IndexOrdering.UNKNOWN
@@ -383,8 +392,7 @@ def dbtasks_index_check(inst, log, args):
@@ -375,8 +384,7 @@ def dbtasks_index_check(inst, log, args):
# Track all issues found
all_ok = True
@ -440,7 +441,7 @@ index 16da966d1..ea8a00cc3 100644
scan_limits_to_remove = [] # (backend, index_name) tuples with nsIndexIDListScanLimit
ancestorid_configs_to_remove = [] # backend names with ancestorid config entries
remove_ancestorid_from_defaults = False # Flag to remove from cn=default indexes
@@ -417,13 +425,6 @@ def dbtasks_index_check(inst, log, args):
@@ -409,13 +417,6 @@ def dbtasks_index_check(inst, log, args):
if disk_ordering == IndexOrdering.UNKNOWN:
log.info(" %s - could not determine disk ordering, skipping", index_name)
@ -454,7 +455,7 @@ index 16da966d1..ea8a00cc3 100644
continue
config_has_int_order = _has_integer_ordering_match(dse_ldif, backend, index_name)
@@ -431,18 +432,15 @@ def dbtasks_index_check(inst, log, args):
@@ -423,18 +424,15 @@ def dbtasks_index_check(inst, log, args):
log.info(" %s - config: %s, disk: %s",
index_name, config_desc, disk_ordering.value)
@ -480,7 +481,7 @@ index 16da966d1..ea8a00cc3 100644
all_ok = False
# Handle issues
@@ -488,26 +486,27 @@ def dbtasks_index_check(inst, log, args):
@@ -480,26 +478,27 @@ def dbtasks_index_check(inst, log, args):
log.error(" Failed to remove ancestorid config from backend %s: %s", backend, e)
return False
@ -525,7 +526,7 @@ index 16da966d1..ea8a00cc3 100644
log.info("All issues fixed")
return True
@@ -572,5 +571,5 @@ def create_parser(subcommands):
@@ -563,5 +562,5 @@ def create_parser(subcommands):
index_check_parser.add_argument('backend', nargs='?', default=None,
help="Backend to check. If not specified, all backends are checked.")
index_check_parser.add_argument('--fix', action='store_true', default=False,

View File

@ -1,52 +0,0 @@
From ee03e8443a108cff0cc4c7a03962fdc3a1fbf94d Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Wed, 16 Oct 2024 19:24:55 -0700
Subject: [PATCH] Issue 6067 - Update dsidm to prioritize basedn from .dsrc
over interactive input (#6362)
Description: Modifies dsidm CLI tool to check for the basedn in the .dsrc configuration file
when the -b option is not provided.
Previously, users were required to always specify the basedn interactively if -b was omitted,
even if it was available in .dsrc.
Now, the basedn is determined by first checking the -b option, then the .dsrc file, and finally
prompting the user if neither is set.
Related: https://github.com/389ds/389-ds-base/issues/6067
Reviewed by: @Firstyear (Thanks!)
---
src/lib389/cli/dsidm | 2 +-
src/lib389/lib389/cli_idm/__init__.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/lib389/cli/dsidm b/src/lib389/cli/dsidm
index 970973f4f..d318664bc 100755
--- a/src/lib389/cli/dsidm
+++ b/src/lib389/cli/dsidm
@@ -128,7 +128,7 @@ if __name__ == '__main__':
result = False
try:
inst = connect_instance(dsrc_inst=dsrc_inst, verbose=args.verbose, args=args)
- basedn = _get_basedn_arg(inst, args, log, msg="Enter basedn")
+ basedn = _get_basedn_arg(inst, args, dsrc_inst['basedn'], log, msg="Enter basedn")
if basedn is None:
errmsg = "Must provide a basedn!"
if args.json:
diff --git a/src/lib389/lib389/cli_idm/__init__.py b/src/lib389/lib389/cli_idm/__init__.py
index e3622246d..1f3e2dc86 100644
--- a/src/lib389/lib389/cli_idm/__init__.py
+++ b/src/lib389/lib389/cli_idm/__init__.py
@@ -52,8 +52,8 @@ def _get_args(args, kws):
return kwargs
-def _get_basedn_arg(inst, args, log, msg=None):
- basedn_arg = _get_dn_arg(args.basedn, msg="Enter basedn")
+def _get_basedn_arg(inst, args, basedn, log, msg=None):
+ basedn_arg = _get_dn_arg(basedn, msg="Enter basedn")
if not DSLdapObject(inst, basedn_arg).exists():
raise ValueError(f'The base DN "{basedn_arg}" does not exist.')
--
2.48.1

View File

@ -0,0 +1,202 @@
From 31bef3852f7064c73f66974d7c41262d2a724eab Mon Sep 17 00:00:00 2001
From: Alex Kulberg <vectinx@yandex.ru>
Date: Tue, 9 Dec 2025 17:11:56 +0300
Subject: [PATCH] Issue 7053 - Remove memberof_del_dn_from_groups from MemberOf
plugin (#7064)
Bug Description:
The member plugin creates redundant changes to the member attribute
in groups when deleting a user, although the referential integrity
of the member attribute should be controlled by the Referential Integrity plugin.
Furthermore, memberof doesn't take replication of operations into account
and performs the change on every server instance in the topology.
Fix Description:
Remove the `memberof_del_dn_from_groups` function from the MemberOf plugin,
completely transferring responsibility for deleting users from groups
to the Referential Integrity plugin.
Relates: https://github.com/389ds/389-ds-base/issues/7053
Reviewed by: @tbordaz
---
.../memberof_include_scopes_test.py | 13 ++-
ldap/servers/plugins/memberof/memberof.c | 79 -------------------
2 files changed, 9 insertions(+), 83 deletions(-)
diff --git a/dirsrvtests/tests/suites/memberof_plugin/memberof_include_scopes_test.py b/dirsrvtests/tests/suites/memberof_plugin/memberof_include_scopes_test.py
index e1d3b0a96..347eb880f 100644
--- a/dirsrvtests/tests/suites/memberof_plugin/memberof_include_scopes_test.py
+++ b/dirsrvtests/tests/suites/memberof_plugin/memberof_include_scopes_test.py
@@ -13,7 +13,7 @@ import time
from lib389.utils import ensure_str
from lib389.topologies import topology_st as topo
from lib389._constants import *
-from lib389.plugins import MemberOfPlugin
+from lib389.plugins import MemberOfPlugin, ReferentialIntegrityPlugin
from lib389.idm.user import UserAccount, UserAccounts
from lib389.idm.group import Group, Groups
from lib389.idm.nscontainer import nsContainers
@@ -77,6 +77,13 @@ def test_multiple_scopes(topo):
inst = topo.standalone
+ EXCLUDED_SUBTREE = 'cn=exclude,%s' % SUFFIX
+ # enable Referential Integrity plugin
+ # to correctly process the 'member' attribute
+ refint = ReferentialIntegrityPlugin(inst)
+ refint.add_excludescope(EXCLUDED_SUBTREE)
+ refint.enable()
+
# configure plugin
memberof = MemberOfPlugin(inst)
memberof.enable()
@@ -106,7 +113,6 @@ def test_multiple_scopes(topo):
check_membership(inst, f'uid=test_m3,{SUBTREE_3}', f'cn=g3,{SUBTREE_3}', False)
# Set exclude scope
- EXCLUDED_SUBTREE = 'cn=exclude,%s' % SUFFIX
EXCLUDED_USER = f"uid=test_m1,{EXCLUDED_SUBTREE}"
INCLUDED_USER = f"uid=test_m1,{SUBTREE_1}"
GROUP_DN = f'cn=g1,{SUBTREE_1}'
@@ -122,9 +128,8 @@ def test_multiple_scopes(topo):
# Check memberOf and group are cleaned up
check_membership(inst, EXCLUDED_USER, GROUP_DN, False)
group = Group(topo.standalone, dn=GROUP_DN)
- assert not group.present("member", EXCLUDED_USER)
assert not group.present("member", INCLUDED_USER)
-
+ assert not group.present("member", EXCLUDED_USER)
if __name__ == '__main__':
# Run isolated
diff --git a/ldap/servers/plugins/memberof/memberof.c b/ldap/servers/plugins/memberof/memberof.c
index c4b5afee7..5de2e9e72 100644
--- a/ldap/servers/plugins/memberof/memberof.c
+++ b/ldap/servers/plugins/memberof/memberof.c
@@ -151,7 +151,6 @@ static void memberof_set_plugin_id(void *plugin_id);
static int memberof_compare(MemberOfConfig *config, const void *a, const void *b);
static int memberof_qsort_compare(const void *a, const void *b);
static void memberof_load_array(Slapi_Value **array, Slapi_Attr *attr);
-static int memberof_del_dn_from_groups(Slapi_PBlock *pb, MemberOfConfig *config, Slapi_Entry *e, Slapi_DN *sdn);
static int memberof_call_foreach_dn(Slapi_PBlock *pb, Slapi_Entry *e, Slapi_DN *sdn, MemberOfConfig *config, char **types, plugin_search_entry_callback callback, void *callback_data, int *cached, PRBool use_grp_cache);
static int memberof_is_direct_member(MemberOfConfig *config, Slapi_Value *groupdn, Slapi_Value *memberdn);
static int memberof_is_grouping_attr(char *type, MemberOfConfig *config);
@@ -540,21 +539,6 @@ deferred_modrdn_func(MemberofDeferredModrdnTask *task)
* attributes to refer to the new name. */
if (ret == LDAP_SUCCESS && pre_sdn && post_sdn) {
if (!memberof_entry_in_scope(&configCopy, &post_entry_info)) {
- /*
- * After modrdn the group contains both the pre and post DN's as
- * members, so we need to cleanup both in this case.
- */
- if ((ret = memberof_del_dn_from_groups(pb, &configCopy, pre_e, pre_sdn))) {
- slapi_log_err(SLAPI_LOG_ERR, MEMBEROF_PLUGIN_SUBSYSTEM,
- "deferred_modrdn_func - Delete dn failed for preop entry(%s), error (%d)\n",
- slapi_sdn_get_dn(pre_sdn), ret);
- }
- if ((ret = memberof_del_dn_from_groups(pb, &configCopy, post_e, post_sdn))) {
- slapi_log_err(SLAPI_LOG_ERR, MEMBEROF_PLUGIN_SUBSYSTEM,
- "deferred_modrdn_func - Delete dn failed for postop entry(%s), error (%d)\n",
- slapi_sdn_get_dn(post_sdn), ret);
- }
-
if (ret == LDAP_SUCCESS && pre_e && configCopy.group_filter &&
0 == slapi_filter_test_simple(pre_e, configCopy.group_filter))
{
@@ -638,16 +622,6 @@ deferred_del_func(MemberofDeferredDelTask *task)
free_configCopy = PR_TRUE;
memberof_unlock_config();
- /* remove this DN from the
- * membership lists of groups
- */
- if ((ret = memberof_del_dn_from_groups(pb, &configCopy, e, sdn))) {
- slapi_log_err(SLAPI_LOG_ERR, MEMBEROF_PLUGIN_SUBSYSTEM,
- "deferred_del_func - Error deleting dn (%s) from group. Error (%d)\n",
- slapi_sdn_get_dn(sdn), ret);
- goto bail;
- }
-
/* is the entry of interest as a group? */
if (e && configCopy.group_filter && 0 == slapi_filter_test_simple(e, configCopy.group_filter)) {
Slapi_Attr *attr = 0;
@@ -1456,16 +1430,6 @@ memberof_postop_del(Slapi_PBlock *pb)
memberof_copy_config(&configCopy, memberof_get_config());
memberof_unlock_config();
- /* remove this DN from the
- * membership lists of groups
- */
- if ((ret = memberof_del_dn_from_groups(pb, &configCopy, e, sdn))) {
- slapi_log_err(SLAPI_LOG_ERR, MEMBEROF_PLUGIN_SUBSYSTEM,
- "memberof_postop_del - Error deleting dn (%s) from group. Error (%d)\n",
- slapi_sdn_get_dn(sdn), ret);
- goto bail;
- }
-
/* is the entry of interest as a group? */
if (e && configCopy.group_filter && 0 == slapi_filter_test_simple(e, configCopy.group_filter)) {
Slapi_Attr *attr = 0;
@@ -1496,34 +1460,6 @@ done:
}
-/* Deletes a member dn from all groups that refer to it. */
-static int
-memberof_del_dn_from_groups(Slapi_PBlock *pb, MemberOfConfig *config, Slapi_Entry *e, Slapi_DN *sdn)
-{
- char *groupattrs[2] = {0, 0};
- int rc = LDAP_SUCCESS;
- int cached = 0;
-
- /* Loop through each grouping attribute to find groups that have
- * dn as a member. For any matches, delete the dn value from the
- * same grouping attribute. */
- for (size_t i = 0; config->groupattrs && config->groupattrs[i] && rc == LDAP_SUCCESS; i++) {
- memberof_del_dn_data data = {(char *)slapi_sdn_get_dn(sdn),
- config->groupattrs[i], config};
-
- groupattrs[0] = config->groupattrs[i];
-
- slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
- "memberof_del_dn_from_groups: Ancestors of %s attr: %s\n",
- slapi_sdn_get_dn(sdn),
- groupattrs[0]);
- rc = memberof_call_foreach_dn(pb, e, sdn, config, groupattrs,
- memberof_del_dn_type_callback, &data, &cached, PR_FALSE);
- }
-
- return rc;
-}
-
int
memberof_del_dn_type_callback(Slapi_Entry *e, void *callback_data)
{
@@ -1904,21 +1840,6 @@ memberof_postop_modrdn(Slapi_PBlock *pb)
* attributes to refer to the new name. */
if (ret == LDAP_SUCCESS && pre_sdn && post_sdn) {
if (!memberof_entry_in_scope(&configCopy, &post_entry_info)) {
- /*
- * After modrdn the group contains both the pre and post DN's as
- * members, so we need to cleanup both in this case.
- */
- if ((ret = memberof_del_dn_from_groups(pb, &configCopy, pre_e, pre_sdn))) {
- slapi_log_err(SLAPI_LOG_ERR, MEMBEROF_PLUGIN_SUBSYSTEM,
- "memberof_postop_modrdn - Delete dn failed for preop entry(%s), error (%d)\n",
- slapi_sdn_get_dn(pre_sdn), ret);
- }
- if ((ret = memberof_del_dn_from_groups(pb, &configCopy, post_e, post_sdn))) {
- slapi_log_err(SLAPI_LOG_ERR, MEMBEROF_PLUGIN_SUBSYSTEM,
- "memberof_postop_modrdn - Delete dn failed for postop entry(%s), error (%d)\n",
- slapi_sdn_get_dn(post_sdn), ret);
- }
-
if (ret == LDAP_SUCCESS && pre_e && configCopy.group_filter &&
0 == slapi_filter_test_simple(pre_e, configCopy.group_filter)) {
/* is the entry of interest as a group? */
--
2.52.0

View File

@ -0,0 +1,952 @@
From 0c5f7b9909ababc53e73e9469da6e30b81637cd0 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Mon, 23 Feb 2026 09:49:52 +0100
Subject: [PATCH] Issue 5853 - Update concread to 0.5.10
Description:
Update concread to 0.5.10 and update Cargo.lock
Relates: https://github.com/389ds/389-ds-base/issues/5853
Reviewed by: @droideck (Thanks!)
---
src/Cargo.lock | 515 +++++++++++++++++++++++----------------
src/librslapd/Cargo.toml | 2 +-
2 files changed, 312 insertions(+), 205 deletions(-)
diff --git a/src/Cargo.lock b/src/Cargo.lock
index 87aeee852..425371478 100644
--- a/src/Cargo.lock
+++ b/src/Cargo.lock
@@ -2,27 +2,18 @@
# It is not intended for manual editing.
version = 3
-[[package]]
-name = "addr2line"
-version = "0.24.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
-dependencies = [
- "gimli",
-]
-
-[[package]]
-name = "adler2"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
-
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
[[package]]
name = "atty"
version = "0.2.14"
@@ -40,21 +31,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
-[[package]]
-name = "backtrace"
-version = "0.3.75"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
-dependencies = [
- "addr2line",
- "cfg-if",
- "libc",
- "miniz_oxide",
- "object",
- "rustc-demangle",
- "windows-targets",
-]
-
[[package]]
name = "base64"
version = "0.13.1"
@@ -69,9 +45,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
-version = "2.9.1"
+version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "byteorder"
@@ -86,8 +62,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49"
dependencies = [
"clap",
- "heck",
- "indexmap",
+ "heck 0.4.1",
+ "indexmap 1.9.3",
"log",
"proc-macro2",
"quote",
@@ -100,10 +76,11 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.27"
+version = "1.2.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
+checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
dependencies = [
+ "find-msvc-tools",
"jobserver",
"libc",
"shlex",
@@ -111,9 +88,9 @@ dependencies = [
[[package]]
name = "cfg-if"
-version = "1.0.1"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "clap"
@@ -124,7 +101,7 @@ dependencies = [
"atty",
"bitflags 1.3.2",
"clap_lex",
- "indexmap",
+ "indexmap 1.9.3",
"strsim",
"termcolor",
"textwrap",
@@ -141,14 +118,14 @@ dependencies = [
[[package]]
name = "concread"
-version = "0.5.6"
+version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b639eeaa550eba0c8be45b292d5e272e6d29bfdffb4df6925d651ed9ed10fd6"
+checksum = "6588e9e68e11207fb9a5aabd88765187969e6bcba98763c40bcad87b2a73e9f5"
dependencies = [
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
- "foldhash",
+ "foldhash 0.2.0",
"lru",
"smallvec",
"sptr",
@@ -210,9 +187,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
-version = "0.3.12"
+version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
@@ -232,17 +209,29 @@ checksum = "93804560e638370a8be6d59ce71ed803e55e230abdbf42598e666b41adda9b1f"
dependencies = [
"base64",
"byteorder",
- "getrandom 0.2.16",
+ "getrandom 0.2.17",
"openssl",
"zeroize",
]
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
[[package]]
name = "foreign-types"
version = "0.3.2"
@@ -260,32 +249,39 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "getrandom"
-version = "0.2.16"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
- "wasi 0.11.1+wasi-snapshot-preview1",
+ "wasi",
]
[[package]]
name = "getrandom"
-version = "0.3.3"
+version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
- "wasi 0.14.2+wasi-0.2.4",
+ "wasip2",
]
[[package]]
-name = "gimli"
-version = "0.31.1"
+name = "getrandom"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
[[package]]
name = "hashbrown"
@@ -295,13 +291,22 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
-version = "0.15.4"
+version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash 0.1.5",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
- "foldhash",
+ "foldhash 0.2.0",
]
[[package]]
@@ -310,6 +315,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
[[package]]
name = "hermit-abi"
version = "0.1.19"
@@ -319,6 +330,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -329,27 +346,45 @@ dependencies = [
"hashbrown 0.12.3",
]
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
[[package]]
name = "itoa"
-version = "1.0.15"
+version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jobserver"
-version = "0.1.33"
+version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
- "getrandom 0.3.3",
+ "getrandom 0.3.4",
"libc",
]
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
[[package]]
name = "libc"
-version = "0.2.174"
+version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
+checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "librnsslapd"
@@ -372,48 +407,30 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
-version = "0.9.4"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "log"
-version = "0.4.27"
+version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
-version = "0.13.0"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465"
+checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
dependencies = [
- "hashbrown 0.15.4",
+ "hashbrown 0.16.1",
]
[[package]]
name = "memchr"
-version = "2.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
-
-[[package]]
-name = "miniz_oxide"
-version = "0.8.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
-dependencies = [
- "adler2",
-]
-
-[[package]]
-name = "object"
-version = "0.36.7"
+version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
-dependencies = [
- "memchr",
-]
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "once_cell"
@@ -423,11 +440,11 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openssl"
-version = "0.10.73"
+version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
- "bitflags 2.9.1",
+ "bitflags 2.11.0",
"cfg-if",
"foreign-types",
"libc",
@@ -444,14 +461,14 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.117",
]
[[package]]
name = "openssl-sys"
-version = "0.9.109"
+version = "0.9.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
dependencies = [
"cc",
"libc",
@@ -496,6 +513,16 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.117",
+]
+
[[package]]
name = "proc-macro-hack"
version = "0.5.20+deprecated"
@@ -504,9 +531,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
-version = "1.0.95"
+version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
@@ -526,9 +553,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.40"
+version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
@@ -539,19 +566,13 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
-[[package]]
-name = "rustc-demangle"
-version = "0.1.25"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
-
[[package]]
name = "rustix"
-version = "1.0.7"
+version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
- "bitflags 2.9.1",
+ "bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys",
@@ -559,41 +580,52 @@ dependencies = [
]
[[package]]
-name = "ryu"
-version = "1.0.20"
+name = "semver"
+version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
-version = "1.0.219"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.219"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.117",
]
[[package]]
name = "serde_json"
-version = "1.0.140"
+version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
- "ryu",
"serde",
+ "serde_core",
+ "zmij",
]
[[package]]
@@ -649,9 +681,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.103"
+version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
@@ -660,12 +692,12 @@ dependencies = [
[[package]]
name = "tempfile"
-version = "3.20.0"
+version = "3.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
dependencies = [
"fastrand",
- "getrandom 0.3.3",
+ "getrandom 0.4.1",
"once_cell",
"rustix",
"windows-sys",
@@ -688,11 +720,10 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
[[package]]
name = "tokio"
-version = "1.45.1"
+version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
+checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
- "backtrace",
"pin-project-lite",
]
@@ -707,9 +738,9 @@ dependencies = [
[[package]]
name = "tracing"
-version = "0.1.41"
+version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
@@ -718,29 +749,35 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.30"
+version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.117",
]
[[package]]
name = "tracing-core"
-version = "0.1.34"
+version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
]
[[package]]
name = "unicode-ident"
-version = "1.0.18"
+version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "uuid"
@@ -748,7 +785,7 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
- "getrandom 0.2.16",
+ "getrandom 0.2.17",
]
[[package]]
@@ -764,12 +801,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
-name = "wasi"
-version = "0.14.2+wasi-0.2.4"
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap 2.13.0",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
- "wit-bindgen-rt",
+ "bitflags 2.11.0",
+ "hashbrown 0.15.5",
+ "indexmap 2.13.0",
+ "semver",
]
[[package]]
@@ -790,9 +870,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
-version = "0.1.9"
+version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
@@ -804,103 +884,130 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
-name = "windows-sys"
-version = "0.59.0"
+name = "windows-link"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets",
-]
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
-name = "windows-targets"
-version = "0.52.6"
+name = "windows-sys"
+version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows-link",
]
[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
+name = "wit-bindgen"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
+name = "wit-bindgen-core"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "wit-parser",
+]
[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
+name = "wit-bindgen-rust"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "indexmap 2.13.0",
+ "prettyplease",
+ "syn 2.0.117",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
+name = "wit-component"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags 2.11.0",
+ "indexmap 2.13.0",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
[[package]]
-name = "wit-bindgen-rt"
-version = "0.39.0"
+name = "wit-parser"
+version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
- "bitflags 2.9.1",
+ "anyhow",
+ "id-arena",
+ "indexmap 2.13.0",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
]
[[package]]
name = "zeroize"
-version = "1.8.1"
+version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
-version = "1.4.2"
+version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.103",
+ "syn 2.0.117",
]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/src/librslapd/Cargo.toml b/src/librslapd/Cargo.toml
index f38f93f4c..61100f381 100644
--- a/src/librslapd/Cargo.toml
+++ b/src/librslapd/Cargo.toml
@@ -16,7 +16,7 @@ crate-type = ["staticlib", "lib"]
[dependencies]
slapd = { path = "../slapd" }
libc = "0.2"
-concread = "0.5.6"
+concread = "0.5.10"
[build-dependencies]
cbindgen = "0.26"
--
2.53.0

View File

@ -1,520 +0,0 @@
From b8c079c770d3eaa4de49e997d42e1501c28a153b Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Mon, 8 Jul 2024 11:19:09 +0200
Subject: [PATCH] Issue 6155 - ldap-agent fails to start because of permission
error (#6179)
Issue: dirsrv-snmp service fails to starts when SELinux is enforced because of AVC preventing to open some files
One workaround is to use the dac_override capability but it is a bad practice.
Fix: Setting proper permissions:
Running ldap-agent with uid=root and gid=dirsrv to be able to access both snmp and dirsrv resources.
Setting read permission on the group for the dse.ldif file
Setting r/w permissions on the group for the snmp semaphore and mmap file
For that one special care is needed because ns-slapd umask overrides the file creation permission
as is better to avoid changing the umask (changing umask within the code is not thread safe,
and the current 0022 umask value is correct for most of the files) so the safest way is to chmod the snmp file
if the needed permission are not set.
Issue: #6155
Reviewed by: @droideck , @vashirov (Thanks ! )
(cherry picked from commit eb7e57d77b557b63c65fdf38f9069893b021f049)
---
.github/scripts/generate_matrix.py | 4 +-
dirsrvtests/tests/suites/snmp/snmp.py | 214 ++++++++++++++++++++++++++
ldap/servers/slapd/agtmmap.c | 72 ++++++++-
ldap/servers/slapd/agtmmap.h | 13 ++
ldap/servers/slapd/dse.c | 6 +-
ldap/servers/slapd/slap.h | 6 +
ldap/servers/slapd/snmp_collator.c | 4 +-
src/lib389/lib389/instance/setup.py | 5 +
wrappers/systemd-snmp.service.in | 1 +
9 files changed, 313 insertions(+), 12 deletions(-)
create mode 100644 dirsrvtests/tests/suites/snmp/snmp.py
diff --git a/.github/scripts/generate_matrix.py b/.github/scripts/generate_matrix.py
index 584374597..8d67a1dc7 100644
--- a/.github/scripts/generate_matrix.py
+++ b/.github/scripts/generate_matrix.py
@@ -21,8 +21,8 @@ else:
# Use tests from the source
suites = next(os.walk('dirsrvtests/tests/suites/'))[1]
- # Filter out snmp as it is an empty directory:
- suites.remove('snmp')
+ # Filter out webui because of broken tests
+ suites.remove('webui')
# Run each replication test module separately to speed things up
suites.remove('replication')
diff --git a/dirsrvtests/tests/suites/snmp/snmp.py b/dirsrvtests/tests/suites/snmp/snmp.py
new file mode 100644
index 000000000..0952deb40
--- /dev/null
+++ b/dirsrvtests/tests/suites/snmp/snmp.py
@@ -0,0 +1,214 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2024 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import os
+import pytest
+import logging
+import subprocess
+import ldap
+from datetime import datetime
+from shutil import copyfile
+from lib389.topologies import topology_m2 as topo_m2
+from lib389.utils import selinux_present
+
+
+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__)
+
+
+SNMP_USER = 'user_name'
+SNMP_PASSWORD = 'authentication_password'
+SNMP_PRIVATE = 'private_password'
+
+# LDAP OID in MIB
+LDAP_OID = '.1.3.6.1.4.1.2312.6.1.1'
+LDAPCONNECTIONS_OID = f'{LDAP_OID}.21'
+
+
+def run_cmd(cmd, check_returncode=True):
+ """Run a command"""
+
+ log.info(f'Run: {cmd}')
+ result = subprocess.run(cmd, capture_output=True, universal_newlines=True)
+ log.info(f'STDOUT of {cmd} is:\n{result.stdout}')
+ log.info(f'STDERR of {cmd} is:\n{result.stderr}')
+ if check_returncode:
+ result.check_returncode()
+ return result
+
+
+def add_lines(lines, filename):
+ """Add lines that are not already present at the end of a file"""
+
+ log.info(f'add_lines({lines}, {filename})')
+ try:
+ with open(filename, 'r') as fd:
+ for line in fd:
+ try:
+ lines.remove(line.strip())
+ except ValueError:
+ pass
+ except FileNotFoundError:
+ pass
+ if lines:
+ with open(filename, 'a') as fd:
+ for line in lines:
+ fd.write(f'{line}\n')
+
+
+def remove_lines(lines, filename):
+ """Remove lines in a file"""
+
+ log.info(f'remove_lines({lines}, {filename})')
+ file_lines = []
+ with open(filename, 'r') as fd:
+ for line in fd:
+ if not line.strip() in lines:
+ file_lines.append(line)
+ with open(filename, 'w') as fd:
+ for line in file_lines:
+ fd.write(line)
+
+
+@pytest.fixture(scope="module")
+def setup_snmp(topo_m2, request):
+ """Install snmp and configure it
+
+ Returns the time just before dirsrv-snmp get restarted
+ """
+
+ inst1 = topo_m2.ms["supplier1"]
+ inst2 = topo_m2.ms["supplier2"]
+
+ # Check for the test prerequisites
+ if os.getuid() != 0:
+ pytest.skip('This test should be run by root superuser')
+ return None
+ if not inst1.with_systemd_running():
+ pytest.skip('This test requires systemd')
+ return None
+ required_packages = {
+ '389-ds-base-snmp': os.path.join(inst1.get_sbin_dir(), 'ldap-agent'),
+ 'net-snmp': '/etc/snmp/snmpd.conf', }
+ skip_msg = ""
+ for package,file in required_packages.items():
+ if not os.path.exists(file):
+ skip_msg += f"Package {package} is not installed ({file} is missing).\n"
+ if skip_msg != "":
+ pytest.skip(f'This test requires the following package(s): {skip_msg}')
+ return None
+
+ # Install snmp
+ # run_cmd(['/usr/bin/dnf', 'install', '-y', 'net-snmp', 'net-snmp-utils', '389-ds-base-snmp'])
+
+ # Prepare the lines to add/remove in files:
+ # master agentx
+ # snmp user (user_name - authentication_password - private_password)
+ # ldap_agent ds instances
+ #
+ # Adding rwuser and createUser lines is the same as running:
+ # net-snmp-create-v3-user -A authentication_password -a SHA -X private_password -x AES user_name
+ # but has the advantage of removing the user at cleanup phase
+ #
+ agent_cfg = '/etc/dirsrv/config/ldap-agent.conf'
+ lines_dict = { '/etc/snmp/snmpd.conf' : ['master agentx', f'rwuser {SNMP_USER}'],
+ '/var/lib/net-snmp/snmpd.conf' : [
+ f'createUser {SNMP_USER} SHA "{SNMP_PASSWORD}" AES "{SNMP_PRIVATE}"',],
+ agent_cfg : [] }
+ for inst in topo_m2:
+ lines_dict[agent_cfg].append(f'server slapd-{inst.serverid}')
+
+ # Prepare the cleanup
+ def fin():
+ run_cmd(['systemctl', 'stop', 'dirsrv-snmp'])
+ if not DEBUGGING:
+ run_cmd(['systemctl', 'stop', 'snmpd'])
+ try:
+ os.remove('/usr/share/snmp/mibs/redhat-directory.mib')
+ except FileNotFoundError:
+ pass
+ for filename,lines in lines_dict.items():
+ remove_lines(lines, filename)
+ run_cmd(['systemctl', 'start', 'snmpd'])
+
+ request.addfinalizer(fin)
+
+ # Copy RHDS MIB in default MIB search path (Ugly because I have not found how to change the search path)
+ copyfile('/usr/share/dirsrv/mibs/redhat-directory.mib', '/usr/share/snmp/mibs/redhat-directory.mib')
+
+ run_cmd(['systemctl', 'stop', 'snmpd'])
+ for filename,lines in lines_dict.items():
+ add_lines(lines, filename)
+
+ run_cmd(['systemctl', 'start', 'snmpd'])
+
+ curtime = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+
+ run_cmd(['systemctl', 'start', 'dirsrv-snmp'])
+ return curtime
+
+
+@pytest.mark.skipif(not os.path.exists('/usr/bin/snmpwalk'), reason="net-snmp-utils package is not installed")
+def test_snmpwalk(topo_m2, setup_snmp):
+ """snmp smoke tests.
+
+ :id: e5d29998-1c21-11ef-a654-482ae39447e5
+ :setup: Two suppliers replication setup, snmp
+ :steps:
+ 1. use snmpwalk to display LDAP statistics
+ 2. use snmpwalk to get the number of open connections
+ :expectedresults:
+ 1. Success and no messages in stderr
+ 2. The number of open connections should be positive
+ """
+
+ inst1 = topo_m2.ms["supplier1"]
+ inst2 = topo_m2.ms["supplier2"]
+
+
+ cmd = [ '/usr/bin/snmpwalk', '-v3', '-u', SNMP_USER, '-l', 'AuthPriv',
+ '-m', '+RHDS-MIB', '-A', SNMP_PASSWORD, '-a', 'SHA',
+ '-X', SNMP_PRIVATE, '-x', 'AES', 'localhost',
+ LDAP_OID ]
+ result = run_cmd(cmd)
+ assert not result.stderr
+
+ cmd = [ '/usr/bin/snmpwalk', '-v3', '-u', SNMP_USER, '-l', 'AuthPriv',
+ '-m', '+RHDS-MIB', '-A', SNMP_PASSWORD, '-a', 'SHA',
+ '-X', SNMP_PRIVATE, '-x', 'AES', 'localhost',
+ f'{LDAPCONNECTIONS_OID}.{inst1.port}', '-Ov' ]
+ result = run_cmd(cmd)
+ nbconns = int(result.stdout.split()[1])
+ log.info(f'There are {nbconns} open connections on {inst1.serverid}')
+ assert nbconns > 0
+
+
+@pytest.mark.skipif(not selinux_present(), reason="SELinux is not enabled")
+def test_snmp_avc(topo_m2, setup_snmp):
+ """snmp smoke tests.
+
+ :id: fb79728e-1d0d-11ef-9213-482ae39447e5
+ :setup: Two suppliers replication setup, snmp
+ :steps:
+ 1. Get the system journal about ldap-agent
+ :expectedresults:
+ 1. No AVC should be present
+ """
+ result = run_cmd(['journalctl', '-S', setup_snmp, '-g', 'ldap-agent'])
+ assert not 'AVC' in result.stdout
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
diff --git a/ldap/servers/slapd/agtmmap.c b/ldap/servers/slapd/agtmmap.c
index bc5fe1ee1..4dc67dcfb 100644
--- a/ldap/servers/slapd/agtmmap.c
+++ b/ldap/servers/slapd/agtmmap.c
@@ -34,6 +34,70 @@
agt_mmap_context_t mmap_tbl[2] = {{AGT_MAP_UNINIT, -1, (caddr_t)-1},
{AGT_MAP_UNINIT, -1, (caddr_t)-1}};
+#define CHECK_MAP_FAILURE(addr) ((addr)==NULL || (addr) == (caddr_t) -1)
+
+
+/****************************************************************************
+ *
+ * agt_set_fmode () - try to increase file mode if some flags are missing.
+ *
+ *
+ * Inputs:
+ * fd -> The file descriptor.
+ *
+ * mode -> the wanted mode
+ *
+ * Outputs: None
+ * Return Values: None
+ *
+ ****************************************************************************/
+static void
+agt_set_fmode(int fd, mode_t mode)
+{
+ /* ns-slapd umask is 0022 which is usually fine.
+ * but ldap-agen needs S_IWGRP permission on snmp semaphore and mmap file
+ * ( when SELinux is enforced process with uid=0 does not bypass the file permission
+ * (unless the unfamous dac_override capability is set)
+ * Changing umask could lead to race conditions so it is better to check the
+ * file permission and change them if needed and if the process own the file.
+ */
+ struct stat fileinfo = {0};
+ if (fstat(fd, &fileinfo) == 0 && fileinfo.st_uid == getuid() &&
+ (fileinfo.st_mode & mode) != mode) {
+ (void) fchmod(fd, fileinfo.st_mode | mode);
+ }
+}
+
+/****************************************************************************
+ *
+ * agt_sem_open () - Like sem_open but ignores umask
+ *
+ *
+ * Inputs: see sem_open man page.
+ * Outputs: see sem_open man page.
+ * Return Values: see sem_open man page.
+ *
+ ****************************************************************************/
+sem_t *
+agt_sem_open(const char *name, int oflag, mode_t mode, unsigned int value)
+{
+ sem_t *sem = sem_open(name, oflag, mode, value);
+ char *semname = NULL;
+
+ if (sem != NULL) {
+ if (asprintf(&semname, "/dev/shm/sem.%s", name+1) > 0) {
+ int fd = open(semname, O_RDONLY);
+ if (fd >= 0) {
+ agt_set_fmode(fd, mode);
+ (void) close(fd);
+ }
+ free(semname);
+ semname = NULL;
+ }
+ }
+ return sem;
+}
+
/****************************************************************************
*
* agt_mopen_stats () - open and Memory Map the stats file. agt_mclose_stats()
@@ -52,7 +116,6 @@ agt_mmap_context_t mmap_tbl[2] = {{AGT_MAP_UNINIT, -1, (caddr_t)-1},
* as defined in <errno.h>, otherwise.
*
****************************************************************************/
-
int
agt_mopen_stats(char *statsfile, int mode, int *hdl)
{
@@ -64,6 +127,7 @@ agt_mopen_stats(char *statsfile, int mode, int *hdl)
int err;
size_t sz;
struct stat fileinfo;
+ mode_t rw_mode = S_IWUSR | S_IRUSR | S_IRGRP | S_IWGRP | S_IROTH;
switch (mode) {
case O_RDONLY:
@@ -128,10 +192,7 @@ agt_mopen_stats(char *statsfile, int mode, int *hdl)
break;
case O_RDWR:
- fd = open(path,
- O_RDWR | O_CREAT,
- S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH);
-
+ fd = open(path, O_RDWR | O_CREAT, rw_mode);
if (fd < 0) {
err = errno;
#if (0)
@@ -140,6 +201,7 @@ agt_mopen_stats(char *statsfile, int mode, int *hdl)
rc = err;
goto bail;
}
+ agt_set_fmode(fd, rw_mode);
if (fstat(fd, &fileinfo) != 0) {
close(fd);
diff --git a/ldap/servers/slapd/agtmmap.h b/ldap/servers/slapd/agtmmap.h
index fb27ab2c4..99a8584a3 100644
--- a/ldap/servers/slapd/agtmmap.h
+++ b/ldap/servers/slapd/agtmmap.h
@@ -28,6 +28,7 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
+#include <semaphore.h>
#include <errno.h>
#include "nspr.h"
@@ -188,6 +189,18 @@ int agt_mclose_stats(int hdl);
int agt_mread_stats(int hdl, struct hdr_stats_t *, struct ops_stats_t *, struct entries_stats_t *);
+/****************************************************************************
+ *
+ * agt_sem_open () - Like sem_open but ignores umask
+ *
+ *
+ * Inputs: see sem_open man page.
+ * Outputs: see sem_open man page.
+ * Return Values: see sem_open man page.
+ *
+ ****************************************************************************/
+sem_t *agt_sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
+
#ifdef __cplusplus
}
#endif
diff --git a/ldap/servers/slapd/dse.c b/ldap/servers/slapd/dse.c
index b04fafde6..f1e48c6b1 100644
--- a/ldap/servers/slapd/dse.c
+++ b/ldap/servers/slapd/dse.c
@@ -683,7 +683,7 @@ dse_read_one_file(struct dse *pdse, const char *filename, Slapi_PBlock *pb, int
"The configuration file %s could not be accessed, error %d\n",
filename, rc);
rc = 0; /* Fail */
- } else if ((prfd = PR_Open(filename, PR_RDONLY, SLAPD_DEFAULT_FILE_MODE)) == NULL) {
+ } else if ((prfd = PR_Open(filename, PR_RDONLY, SLAPD_DEFAULT_DSE_FILE_MODE)) == NULL) {
slapi_log_err(SLAPI_LOG_ERR, "dse_read_one_file",
"The configuration file %s could not be read. " SLAPI_COMPONENT_NAME_NSPR " %d (%s)\n",
filename,
@@ -871,7 +871,7 @@ dse_rw_permission_to_one_file(const char *name, int loglevel)
PRFileDesc *prfd;
prfd = PR_Open(name, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE,
- SLAPD_DEFAULT_FILE_MODE);
+ SLAPD_DEFAULT_DSE_FILE_MODE);
if (NULL == prfd) {
prerr = PR_GetError();
accesstype = "create";
@@ -970,7 +970,7 @@ dse_write_file_nolock(struct dse *pdse)
fpw.fpw_prfd = NULL;
if (NULL != pdse->dse_filename) {
- if ((fpw.fpw_prfd = PR_Open(pdse->dse_tmpfile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, SLAPD_DEFAULT_FILE_MODE)) == NULL) {
+ if ((fpw.fpw_prfd = PR_Open(pdse->dse_tmpfile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, SLAPD_DEFAULT_DSE_FILE_MODE)) == NULL) {
rc = PR_GetOSError();
slapi_log_err(SLAPI_LOG_ERR, "dse_write_file_nolock", "Cannot open "
"temporary DSE file \"%s\" for update: OS error %d (%s)\n",
diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h
index 469874fd1..927576b70 100644
--- a/ldap/servers/slapd/slap.h
+++ b/ldap/servers/slapd/slap.h
@@ -238,6 +238,12 @@ typedef void (*VFPV)(); /* takes undefined arguments */
*/
#define SLAPD_DEFAULT_FILE_MODE S_IRUSR | S_IWUSR
+/* ldap_agent run as uid=root gid=dirsrv and requires S_IRGRP | S_IWGRP
+ * on semaphore and mmap file if SELinux is enforced.
+ */
+#define SLAPD_DEFAULT_SNMP_FILE_MODE S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP
+/* ldap_agent run as uid=root gid=dirsrv and requires S_IRGRP on dse.ldif if SELinux is enforced. */
+#define SLAPD_DEFAULT_DSE_FILE_MODE S_IRUSR | S_IWUSR | S_IRGRP
#define SLAPD_DEFAULT_DIR_MODE S_IRWXU
#define SLAPD_DEFAULT_IDLE_TIMEOUT 3600 /* seconds - 0 == never */
#define SLAPD_DEFAULT_IDLE_TIMEOUT_STR "3600"
diff --git a/ldap/servers/slapd/snmp_collator.c b/ldap/servers/slapd/snmp_collator.c
index c998d4262..bd7020585 100644
--- a/ldap/servers/slapd/snmp_collator.c
+++ b/ldap/servers/slapd/snmp_collator.c
@@ -474,7 +474,7 @@ static void
snmp_collator_create_semaphore(void)
{
/* First just try to create the semaphore. This should usually just work. */
- if ((stats_sem = sem_open(stats_sem_name, O_CREAT | O_EXCL, SLAPD_DEFAULT_FILE_MODE, 1)) == SEM_FAILED) {
+ if ((stats_sem = agt_sem_open(stats_sem_name, O_CREAT | O_EXCL, SLAPD_DEFAULT_SNMP_FILE_MODE, 1)) == SEM_FAILED) {
if (errno == EEXIST) {
/* It appears that we didn't exit cleanly last time and left the semaphore
* around. Recreate it since we don't know what state it is in. */
@@ -486,7 +486,7 @@ snmp_collator_create_semaphore(void)
exit(1);
}
- if ((stats_sem = sem_open(stats_sem_name, O_CREAT | O_EXCL, SLAPD_DEFAULT_FILE_MODE, 1)) == SEM_FAILED) {
+ if ((stats_sem = agt_sem_open(stats_sem_name, O_CREAT | O_EXCL, SLAPD_DEFAULT_SNMP_FILE_MODE, 1)) == SEM_FAILED) {
/* No dice */
slapi_log_err(SLAPI_LOG_EMERG, "snmp_collator_create_semaphore",
"Failed to create semaphore for stats file (/dev/shm/sem.%s). Error %d (%s).\n",
diff --git a/src/lib389/lib389/instance/setup.py b/src/lib389/lib389/instance/setup.py
index 036664447..fca03383e 100644
--- a/src/lib389/lib389/instance/setup.py
+++ b/src/lib389/lib389/instance/setup.py
@@ -10,6 +10,7 @@
import os
import sys
import shutil
+import stat
import pwd
import grp
import re
@@ -773,6 +774,10 @@ class SetupDs(object):
ldapi_autobind="on",
)
file_dse.write(dse_fmt)
+ # Set minimum permission required by snmp ldap-agent
+ status = os.fstat(file_dse.fileno())
+ os.fchmod(file_dse.fileno(), status.st_mode | stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP)
+ os.chown(os.path.join(slapd['config_dir'], 'dse.ldif'), slapd['user_uid'], slapd['group_gid'])
self.log.info("Create file system structures ...")
# Create all the needed paths
diff --git a/wrappers/systemd-snmp.service.in b/wrappers/systemd-snmp.service.in
index f18766cb4..d344367a0 100644
--- a/wrappers/systemd-snmp.service.in
+++ b/wrappers/systemd-snmp.service.in
@@ -9,6 +9,7 @@ After=network.target
[Service]
Type=forking
+Group=@defaultgroup@
PIDFile=/run/dirsrv/ldap-agent.pid
ExecStart=@sbindir@/ldap-agent @configdir@/ldap-agent.conf
--
2.49.0

View File

@ -1,60 +0,0 @@
From 12870f410545fb055f664b588df2a2b7ab1c228e Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Mon, 4 Mar 2024 07:22:00 +0100
Subject: [PATCH] Issue 5305 - OpenLDAP version autodetection doesn't work
Bug Description:
An error is logged during a build in `mock` with Bash 4.4:
```
checking for --with-libldap-r... ./configure: command substitution: line 22848: syntax error near unexpected token `>'
./configure: command substitution: line 22848: `ldapsearch -VV 2> >(sed -n '/ldapsearch/ s/.*ldapsearch \([0-9]\+\.[0-9]\+\.[0-9]\+\) .*/\1/p')'
no
```
`mock` runs Bash as `sh` (POSIX mode). Support for process substitution
in POSIX mode was added in version 5.1:
https://lists.gnu.org/archive/html/bug-bash/2020-12/msg00002.html
> Process substitution is now available in posix mode.
Fix Description:
* Add missing `BuildRequires` for openldap-clients
* Replace process substitution with a pipe
Fixes: https://github.com/389ds/389-ds-base/issues/5305
Reviewed by: @progier389, @tbordaz (Thanks!)
---
configure.ac | 2 +-
rpm/389-ds-base.spec.in | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/configure.ac b/configure.ac
index ffc2aac14..a690765a3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -912,7 +912,7 @@ AC_ARG_WITH(libldap-r, AS_HELP_STRING([--with-libldap-r],[Use lldap_r shared lib
AC_SUBST(with_libldap_r)
fi
],
-OPENLDAP_VERSION=`ldapsearch -VV 2> >(sed -n '/ldapsearch/ s/.*ldapsearch \([[[0-9]]]\+\.[[[0-9]]]\+\.[[[0-9]]]\+\) .*/\1/p')`
+OPENLDAP_VERSION=`ldapsearch -VV 2>&1 | sed -n '/ldapsearch/ s/.*ldapsearch \([[[0-9]]]\+\.[[[0-9]]]\+\.[[[0-9]]]\+\) .*/\1/p'`
AX_COMPARE_VERSION([$OPENLDAP_VERSION], [lt], [2.5], [ with_libldap_r=yes ], [ with_libldap_r=no ])
AC_MSG_RESULT($with_libldap_r))
diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in
index cd86138ea..b8c14cd14 100644
--- a/rpm/389-ds-base.spec.in
+++ b/rpm/389-ds-base.spec.in
@@ -65,6 +65,7 @@ Provides: ldif2ldbm
# Attach the buildrequires to the top level package:
BuildRequires: nspr-devel
BuildRequires: nss-devel >= 3.34
+BuildRequires: openldap-clients
BuildRequires: openldap-devel
BuildRequires: libdb-devel
BuildRequires: cyrus-sasl-devel
--
2.49.0

View File

@ -0,0 +1,86 @@
From a3fd3897ede4166625194d48b010b6dbdcdf9f70 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Mon, 23 Feb 2026 12:37:26 -0500
Subject: [PATCH] Issue 7271 - plugins that create threads need to update
active thread count
Description:
Plugins that create threads need to up to the global active thread count.
Otherwise when the server is being stopped the plugin's close function gets
called while these threads are still running and still using the plugin
configuration. This can lead to crashes.
relates: https://github.com/389ds/389-ds-base/issues/7271
Reviewed by: progier & tbordaz (Thanks!!)
---
ldap/servers/plugins/replication/repl5_protocol.c | 5 +++++
ldap/servers/plugins/retrocl/retrocl_trim.c | 7 ++++++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/ldap/servers/plugins/replication/repl5_protocol.c b/ldap/servers/plugins/replication/repl5_protocol.c
index 5d4a0e455..bc9580319 100644
--- a/ldap/servers/plugins/replication/repl5_protocol.c
+++ b/ldap/servers/plugins/replication/repl5_protocol.c
@@ -23,6 +23,7 @@
#include "repl5.h"
#include "repl5_prot_private.h"
+#include "slap.h"
#define PROTOCOL_5_INCREMENTAL 1
#define PROTOCOL_5_TOTAL 2
@@ -237,6 +238,8 @@ prot_thread_main(void *arg)
return;
}
+ g_incr_active_threadcnt();
+
set_thread_private_agmtname(agmt_get_long_name(agmt));
done = 0;
@@ -301,6 +304,8 @@ prot_thread_main(void *arg)
done = 1;
}
}
+
+ g_decr_active_threadcnt();
}
/*
diff --git a/ldap/servers/plugins/retrocl/retrocl_trim.c b/ldap/servers/plugins/retrocl/retrocl_trim.c
index 8fcd3d32b..8fdc75c62 100644
--- a/ldap/servers/plugins/retrocl/retrocl_trim.c
+++ b/ldap/servers/plugins/retrocl/retrocl_trim.c
@@ -243,6 +243,8 @@ trim_changelog(void)
now_interval = slapi_current_rel_time_t(); /* monotonic time for interval */
+ g_incr_active_threadcnt();
+
PR_Lock(ts.ts_s_trim_mutex);
max_age = ts.ts_c_max_age;
trim_interval = ts.ts_c_trim_interval;
@@ -257,7 +259,7 @@ trim_changelog(void)
*/
done = 0;
now_maxage = slapi_current_utc_time(); /* real time for trim candidates */
- while (!done && retrocl_trimming == 1) {
+ while (!done && retrocl_trimming == 1 && !slapi_is_shutting_down()) {
int did_delete;
did_delete = 0;
@@ -309,6 +311,9 @@ trim_changelog(void)
"trim_changelog: removed %d change records\n",
num_deleted);
}
+
+ g_decr_active_threadcnt();
+
return rc;
}
--
2.53.0

View File

@ -1,245 +0,0 @@
From eca6f5fe18f768fd407d38c85624a5212bcf16ab Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Wed, 27 Sep 2023 15:40:33 -0700
Subject: [PATCH] Issue 1925 - Add a CI test (#5936)
Description: Verify that the issue is not present. Cover the scenario when
we remove existing VLVs, create new VLVs (with the same name) and then
we do online re-indexing.
Related: https://github.com/389ds/389-ds-base/issues/1925
Reviewed by: @progier389 (Thanks!)
(cherry picked from the 9633e8d32d28345409680f8e462fb4a53d3b4f83)
---
.../tests/suites/vlv/regression_test.py | 175 +++++++++++++++---
1 file changed, 145 insertions(+), 30 deletions(-)
diff --git a/dirsrvtests/tests/suites/vlv/regression_test.py b/dirsrvtests/tests/suites/vlv/regression_test.py
index 6ab709bd3..536fe950f 100644
--- a/dirsrvtests/tests/suites/vlv/regression_test.py
+++ b/dirsrvtests/tests/suites/vlv/regression_test.py
@@ -1,5 +1,5 @@
# --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2018 Red Hat, Inc.
+# Copyright (C) 2023 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
@@ -9,12 +9,16 @@
import pytest, time
from lib389.tasks import *
from lib389.utils import *
-from lib389.topologies import topology_m2
+from lib389.topologies import topology_m2, topology_st
from lib389.replica import *
from lib389._constants import *
+from lib389.properties import TASK_WAIT
from lib389.index import *
from lib389.mappingTree import *
from lib389.backend import *
+from lib389.idm.user import UserAccounts
+from ldap.controls.vlv import VLVRequestControl
+from ldap.controls.sss import SSSRequestControl
pytestmark = pytest.mark.tier1
@@ -22,6 +26,88 @@ logging.getLogger(__name__).setLevel(logging.DEBUG)
log = logging.getLogger(__name__)
+def open_new_ldapi_conn(dsinstance):
+ ldapurl, certdir = get_ldapurl_from_serverid(dsinstance)
+ assert 'ldapi://' in ldapurl
+ conn = ldap.initialize(ldapurl)
+ conn.sasl_interactive_bind_s("", ldap.sasl.external())
+ return conn
+
+
+def check_vlv_search(conn):
+ before_count=1
+ after_count=3
+ offset=3501
+
+ vlv_control = VLVRequestControl(criticality=True,
+ before_count=before_count,
+ after_count=after_count,
+ offset=offset,
+ content_count=0,
+ greater_than_or_equal=None,
+ context_id=None)
+
+ sss_control = SSSRequestControl(criticality=True, ordering_rules=['cn'])
+ result = conn.search_ext_s(
+ base='dc=example,dc=com',
+ scope=ldap.SCOPE_SUBTREE,
+ filterstr='(uid=*)',
+ serverctrls=[vlv_control, sss_control]
+ )
+ imin = offset + 998 - before_count
+ imax = offset + 998 + after_count
+
+ for i, (dn, entry) in enumerate(result, start=imin):
+ assert i <= imax
+ expected_dn = f'uid=testuser{i},ou=People,dc=example,dc=com'
+ log.debug(f'found {dn} expected {expected_dn}')
+ assert dn.lower() == expected_dn.lower()
+
+
+def add_users(inst, users_num):
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ log.info(f'Adding {users_num} users')
+ for i in range(0, users_num):
+ uid = 1000 + i
+ user_properties = {
+ 'uid': f'testuser{uid}',
+ 'cn': f'testuser{uid}',
+ 'sn': 'user',
+ 'uidNumber': str(uid),
+ 'gidNumber': str(uid),
+ 'homeDirectory': f'/home/testuser{uid}'
+ }
+ users.create(properties=user_properties)
+
+
+
+def create_vlv_search_and_index(inst, basedn=DEFAULT_SUFFIX, bename='userRoot',
+ scope=ldap.SCOPE_SUBTREE, prefix="vlv", vlvsort="cn"):
+ vlv_searches = VLVSearch(inst)
+ vlv_search_properties = {
+ "objectclass": ["top", "vlvSearch"],
+ "cn": f"{prefix}Srch",
+ "vlvbase": basedn,
+ "vlvfilter": "(uid=*)",
+ "vlvscope": str(scope),
+ }
+ vlv_searches.create(
+ basedn=f"cn={bename},cn=ldbm database,cn=plugins,cn=config",
+ properties=vlv_search_properties
+ )
+
+ vlv_index = VLVIndex(inst)
+ vlv_index_properties = {
+ "objectclass": ["top", "vlvIndex"],
+ "cn": f"{prefix}Idx",
+ "vlvsort": vlvsort,
+ }
+ vlv_index.create(
+ basedn=f"cn={prefix}Srch,cn={bename},cn=ldbm database,cn=plugins,cn=config",
+ properties=vlv_index_properties
+ )
+ return vlv_searches, vlv_index
+
class BackendHandler:
def __init__(self, inst, bedict, scope=ldap.SCOPE_ONELEVEL):
self.inst = inst
@@ -101,34 +187,6 @@ class BackendHandler:
'dn' : dn}
-def create_vlv_search_and_index(inst, basedn=DEFAULT_SUFFIX, bename='userRoot',
- scope=ldap.SCOPE_SUBTREE, prefix="vlv", vlvsort="cn"):
- vlv_searches = VLVSearch(inst)
- vlv_search_properties = {
- "objectclass": ["top", "vlvSearch"],
- "cn": f"{prefix}Srch",
- "vlvbase": basedn,
- "vlvfilter": "(uid=*)",
- "vlvscope": str(scope),
- }
- vlv_searches.create(
- basedn=f"cn={bename},cn=ldbm database,cn=plugins,cn=config",
- properties=vlv_search_properties
- )
-
- vlv_index = VLVIndex(inst)
- vlv_index_properties = {
- "objectclass": ["top", "vlvIndex"],
- "cn": f"{prefix}Idx",
- "vlvsort": vlvsort,
- }
- vlv_index.create(
- basedn=f"cn={prefix}Srch,cn={bename},cn=ldbm database,cn=plugins,cn=config",
- properties=vlv_index_properties
- )
- return vlv_searches, vlv_index
-
-
@pytest.fixture
def vlv_setup_with_uid_mr(topology_st, request):
inst = topology_st.standalone
@@ -245,6 +303,62 @@ def test_bulk_import_when_the_backend_with_vlv_was_recreated(topology_m2):
entries = M2.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(objectclass=*)")
+def test_vlv_recreation_reindex(topology_st):
+ """Test VLV recreation and reindexing.
+
+ :id: 29f4567f-4ac0-410f-bc99-a32e217a939f
+ :setup: Standalone instance.
+ :steps:
+ 1. Create new VLVs and do the reindex.
+ 2. Test the new VLVs.
+ 3. Remove the existing VLVs.
+ 4. Create new VLVs (with the same name).
+ 5. Perform online re-indexing of the new VLVs.
+ 6. Test the new VLVs.
+ :expectedresults:
+ 1. Should Success.
+ 2. Should Success.
+ 3. Should Success.
+ 4. Should Success.
+ 5. Should Success.
+ 6. Should Success.
+ """
+
+ inst = topology_st.standalone
+ reindex_task = Tasks(inst)
+
+ # Create and test VLVs
+ vlv_search, vlv_index = create_vlv_search_and_index(inst)
+ assert reindex_task.reindex(
+ suffix=DEFAULT_SUFFIX,
+ attrname=vlv_index.rdn,
+ args={TASK_WAIT: True},
+ vlv=True
+ ) == 0
+
+ add_users(inst, 5000)
+
+ conn = open_new_ldapi_conn(inst.serverid)
+ assert len(conn.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(cn=*)")) > 0
+ check_vlv_search(conn)
+
+ # Remove and recreate VLVs
+ vlv_index.delete()
+ vlv_search.delete()
+
+ vlv_search, vlv_index = create_vlv_search_and_index(inst)
+ assert reindex_task.reindex(
+ suffix=DEFAULT_SUFFIX,
+ attrname=vlv_index.rdn,
+ args={TASK_WAIT: True},
+ vlv=True
+ ) == 0
+
+ conn = open_new_ldapi_conn(inst.serverid)
+ assert len(conn.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(cn=*)")) > 0
+ check_vlv_search(conn)
+
+
def test_vlv_with_mr(vlv_setup_with_uid_mr):
"""
Testing vlv having specific matching rule
@@ -288,6 +402,7 @@ def test_vlv_with_mr(vlv_setup_with_uid_mr):
assert inst.status()
+
if __name__ == "__main__":
# Run isolated
# -s for DEBUG mode
--
2.49.0

View File

@ -1,4 +1,4 @@
From 582c4916a80660e3ea876ae4b6355a81c29a791d Mon Sep 17 00:00:00 2001
From f4817944e43e080b1ab15c3fa13359252429e500 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Wed, 25 Feb 2026 14:06:42 +0100
Subject: [PATCH] Security fix for CVE-2025-14905
@ -17,7 +17,7 @@ References:
1 file changed, 38 insertions(+), 9 deletions(-)
diff --git a/ldap/servers/slapd/schema.c b/ldap/servers/slapd/schema.c
index 16f1861cf..ff92d2b33 100644
index 9ef4ee4bf..7712a720d 100644
--- a/ldap/servers/slapd/schema.c
+++ b/ldap/servers/slapd/schema.c
@@ -1410,6 +1410,7 @@ schema_attr_enum_callback(struct asyntaxinfo *asip, void *arg)
@ -89,5 +89,5 @@ index 16f1861cf..ff92d2b33 100644
/*
* Overall strategy is to maintain a pointer to the next location in
--
2.52.0
2.53.0

View File

@ -1,75 +0,0 @@
From af3fa90f91efda86f4337e8823bca6581ab61792 Mon Sep 17 00:00:00 2001
From: Thierry Bordaz <tbordaz@redhat.com>
Date: Fri, 7 Feb 2025 09:43:08 +0100
Subject: [PATCH] Issue 6494 - (2nd) Various errors when using extended
matching rule on vlv sort filter
---
.../tests/suites/indexes/regression_test.py | 40 +++++++++++++++++++
1 file changed, 40 insertions(+)
diff --git a/dirsrvtests/tests/suites/indexes/regression_test.py b/dirsrvtests/tests/suites/indexes/regression_test.py
index 2196fb2ed..b5bcccc8f 100644
--- a/dirsrvtests/tests/suites/indexes/regression_test.py
+++ b/dirsrvtests/tests/suites/indexes/regression_test.py
@@ -11,17 +11,57 @@ import os
import pytest
import ldap
from lib389._constants import DEFAULT_BENAME, DEFAULT_SUFFIX
+from lib389.backend import Backend, Backends, DatabaseConfig
from lib389.cos import CosClassicDefinition, CosClassicDefinitions, CosTemplate
+from lib389.dbgen import dbgen_users
from lib389.index import Indexes
from lib389.backend import Backends
from lib389.idm.user import UserAccounts
from lib389.topologies import topology_st as topo
from lib389.utils import ds_is_older
from lib389.idm.nscontainer import nsContainer
+from lib389.properties import TASK_WAIT
+from lib389.tasks import Tasks, Task
pytestmark = pytest.mark.tier1
+SUFFIX2 = 'dc=example2,dc=com'
+BENAME2 = 'be2'
+
+DEBUGGING = os.getenv("DEBUGGING", default=False)
+
+@pytest.fixture(scope="function")
+def add_backend_and_ldif_50K_users(request, topo):
+ """
+ Add an empty backend and associated 50K users ldif file
+ """
+
+ tasks = Tasks(topo.standalone)
+ import_ldif = f'{topo.standalone.ldifdir}/be2_50K_users.ldif'
+ be2 = Backend(topo.standalone)
+ be2.create(properties={
+ 'cn': BENAME2,
+ 'nsslapd-suffix': SUFFIX2,
+ },
+ )
+
+ def fin():
+ nonlocal be2
+ if not DEBUGGING:
+ be2.delete()
+
+ request.addfinalizer(fin)
+ parent = f'ou=people,{SUFFIX2}'
+ dbgen_users(topo.standalone, 50000, import_ldif, SUFFIX2, generic=True, parent=parent)
+ assert tasks.importLDIF(
+ suffix=SUFFIX2,
+ input_file=import_ldif,
+ args={TASK_WAIT: True}
+ ) == 0
+
+ return import_ldif
+
@pytest.fixture(scope="function")
def add_a_group_with_users(request, topo):
"""
--
2.49.0

View File

@ -0,0 +1,262 @@
From c6bb96c95dcbff0052fc2decab7020b0f833f313 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Mon, 2 Mar 2026 14:31:29 -0500
Subject: [PATCH] Issue 7271 - implement a pre-close plugin function
Description:
replication protocol could benefit from being notifioed the shutdown process
has started before calling the "close" plugin function. This would allow the
replication protocol to wake up and adjust its active thread count to prevent
a hang during shutdown.
relates: https://github.com/389ds/389-ds-base/issues/7271
Reviewed by: progier, spichugi, and vashirov(Thanks!!!)
---
ldap/servers/plugins/replication/repl5_init.c | 19 ++++++++----
ldap/servers/slapd/daemon.c | 6 +++-
ldap/servers/slapd/pblock.c | 30 +++++++++++++------
ldap/servers/slapd/plugin.c | 24 ++++++++++++++-
ldap/servers/slapd/proto-slap.h | 3 +-
ldap/servers/slapd/slap.h | 3 +-
ldap/servers/slapd/slapi-plugin.h | 3 +-
7 files changed, 69 insertions(+), 19 deletions(-)
diff --git a/ldap/servers/plugins/replication/repl5_init.c b/ldap/servers/plugins/replication/repl5_init.c
index 5047fb8dc..395744db8 100644
--- a/ldap/servers/plugins/replication/repl5_init.c
+++ b/ldap/servers/plugins/replication/repl5_init.c
@@ -1,6 +1,6 @@
/** BEGIN COPYRIGHT BLOCK
* Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
- * Copyright (C) 2005 Red Hat, Inc.
+ * Copyright (C) 2026 Red Hat, Inc.
* All rights reserved.
*
* License: GPL (version 3 or any later version).
@@ -122,6 +122,8 @@ static PRUintn thread_private_agmtname; /* thread private index for logging*/
static PRUintn thread_private_cache;
static PRUintn thread_primary_csn;
+static int multisupplier_pre_stop(Slapi_PBlock *pb __attribute__((unused)));
+
char *
get_thread_private_agmtname()
{
@@ -836,10 +838,6 @@ multisupplier_stop(Slapi_PBlock *pb __attribute__((unused)))
int rc = 0; /* OK */
if (!multisupplier_stopped_flag) {
- if (!is_ldif_dump) {
- /* Shut down replication agreements */
- agmtlist_shutdown();
- }
/* if we are cleaning a ruv, stop */
stop_ruv_cleaning();
/* unregister backend state change notification */
@@ -853,6 +851,16 @@ multisupplier_stop(Slapi_PBlock *pb __attribute__((unused)))
return rc;
}
+static int
+multisupplier_pre_stop(Slapi_PBlock *pb __attribute__((unused)))
+{
+ if (!multisupplier_stopped_flag) {
+ /* Shut down replication agreements which will stop all the
+ * replication protocol threads */
+ agmtlist_shutdown();
+ }
+ return 0;
+}
PRBool
multisupplier_started()
@@ -900,6 +908,7 @@ replication_multisupplier_plugin_init(Slapi_PBlock *pb)
rc = slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION, (void *)&multisupplierdesc);
rc = slapi_pblock_set(pb, SLAPI_PLUGIN_START_FN, (void *)multisupplier_start);
rc = slapi_pblock_set(pb, SLAPI_PLUGIN_CLOSE_FN, (void *)multisupplier_stop);
+ rc = slapi_pblock_set(pb, SLAPI_PLUGIN_PRE_CLOSE_FN, (void *)multisupplier_pre_stop);
/* Register the plugin interfaces we implement */
/* preop acquires csn generator handle */
diff --git a/ldap/servers/slapd/daemon.c b/ldap/servers/slapd/daemon.c
index d6911f03c..7fc1a16f1 100644
--- a/ldap/servers/slapd/daemon.c
+++ b/ldap/servers/slapd/daemon.c
@@ -1,6 +1,6 @@
/** BEGIN COPYRIGHT BLOCK
* Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
- * Copyright (C) 2021 Red Hat, Inc.
+ * Copyright (C) 2026 Red Hat, Inc.
* All rights reserved.
*
* License: GPL (version 3 or any later version).
@@ -1216,6 +1216,10 @@ slapd_daemon(daemon_ports_t *ports)
task_cancel_all();
}
+ /* Call plugin pre close functions */
+ plugin_pre_closeall();
+
+ /* Now wait for active threads to terminate */
threads = g_get_active_threadcnt();
if (threads > 0) {
slapi_log_err(SLAPI_LOG_INFO, "slapd_daemon",
diff --git a/ldap/servers/slapd/pblock.c b/ldap/servers/slapd/pblock.c
index 76e26cb86..90114dee1 100644
--- a/ldap/servers/slapd/pblock.c
+++ b/ldap/servers/slapd/pblock.c
@@ -1,12 +1,12 @@
-/** BEGIN COPYRIGHT BLOCK
- * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
- * Copyright (C) 2005-2025 Red Hat, Inc.
- * Copyright (C) 2009 Hewlett-Packard Development Company, L.P.
- * All rights reserved.
- *
- * License: GPL (version 3 or any later version).
- * See LICENSE for details.
- * END COPYRIGHT BLOCK **/
+ /** BEGIN COPYRIGHT BLOCK
+ * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
+ * Copyright (C) 2026 Red Hat, Inc.
+ * Copyright (C) 2009 Hewlett-Packard Development Company, L.P.
+ * All rights reserved.
+ *
+ * License: GPL (version 3 or any later version).
+ * See LICENSE for details.
+ * END COPYRIGHT BLOCK **/
#ifdef HAVE_CONFIG_H
#include <config.h>
@@ -2524,6 +2524,14 @@ slapi_pblock_get(Slapi_PBlock *pblock, int arg, void *value)
}
break;
+ case SLAPI_PLUGIN_PRE_CLOSE_FN:
+ if (pblock->pb_plugin != NULL) {
+ (*(IFP *)value) = pblock->pb_plugin->plg_pre_close;
+ } else {
+ (*(IFP *)value) = NULL;
+ }
+ break;
+
default:
slapi_log_err(SLAPI_LOG_ERR, "slapi_pblock_get", "Unknown parameter block argument %d\n", arg);
PR_ASSERT(0);
@@ -4209,6 +4217,10 @@ slapi_pblock_set(Slapi_PBlock *pblock, int arg, void *value)
pblock->pb_misc->pb_aci_target_check = *((int *)value);
break;
+ case SLAPI_PLUGIN_PRE_CLOSE_FN:
+ pblock->pb_plugin->plg_pre_close = (IFP)value;
+ break;
+
default:
slapi_log_err(SLAPI_LOG_ERR, "slapi_pblock_set",
"Unknown parameter block argument %d\n", arg);
diff --git a/ldap/servers/slapd/plugin.c b/ldap/servers/slapd/plugin.c
index 52d25e19e..95b8de894 100644
--- a/ldap/servers/slapd/plugin.c
+++ b/ldap/servers/slapd/plugin.c
@@ -1,6 +1,6 @@
/** BEGIN COPYRIGHT BLOCK
* Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
- * Copyright (C) 2021 Red Hat, Inc.
+ * Copyright (C) 2026 Red Hat, Inc.
* All rights reserved.
*
* License: GPL (version 3 or any later version).
@@ -1847,6 +1847,28 @@ plugin_dependency_closeall(void)
}
}
+/* Call the pre close functions of all the plugins */
+void
+plugin_pre_closeall(void)
+{
+ Slapi_PBlock *pb = NULL;
+ int plugins_pre_closed = 0;
+ int index = 0;
+
+ while (plugins_pre_closed < global_plugins_started) {
+ if (global_plugin_shutdown_order[index].name) {
+ if (!global_plugin_shutdown_order[index].removed) {
+ pb = slapi_pblock_new();
+ plugin_call_one(global_plugin_shutdown_order[index].plugin,
+ SLAPI_PLUGIN_PRE_CLOSE_FN, pb);
+ slapi_pblock_destroy(pb);
+ }
+ plugins_pre_closed++;
+ }
+ index++;
+ }
+}
+
void
plugin_freeall(void)
{
diff --git a/ldap/servers/slapd/proto-slap.h b/ldap/servers/slapd/proto-slap.h
index 742e3ee14..fd5bc6e71 100644
--- a/ldap/servers/slapd/proto-slap.h
+++ b/ldap/servers/slapd/proto-slap.h
@@ -1,6 +1,6 @@
/** BEGIN COPYRIGHT BLOCK
* Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
- * Copyright (C) 2021-2025 Red Hat, Inc.
+ * Copyright (C) 2026 Red Hat, Inc.
* All rights reserved.
*
* License: GPL (version 3 or any later version).
@@ -982,6 +982,7 @@ int plugin_call_exop_plugins(Slapi_PBlock *pb, struct slapdplugin *p);
Slapi_Backend *plugin_extended_op_getbackend(Slapi_PBlock *pb, struct slapdplugin *p);
const char *plugin_extended_op_oid2string(const char *oid);
void plugin_closeall(int close_backends, int close_globals);
+void plugin_pre_closeall(void);
void plugin_dependency_freeall(void);
void plugin_startall(int argc, char **argv, char **plugin_list);
void plugin_get_plugin_dependencies(char *plugin_name, char ***names);
diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h
index fee5a6ab5..64f9f465b 100644
--- a/ldap/servers/slapd/slap.h
+++ b/ldap/servers/slapd/slap.h
@@ -1,7 +1,7 @@
/** BEGIN COPYRIGHT BLOCK
* Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
* Copyright (C) 2009 Hewlett-Packard Development Company, L.P.
- * Copyright (C) 2009-2025 Red Hat, Inc.
+ * Copyright (C) 2026 Red Hat, Inc.
* All rights reserved.
*
* Contributors:
@@ -1035,6 +1035,7 @@ struct slapdplugin
char *plg_libpath; /* library path for dll/so */
char *plg_initfunc; /* init symbol */
IFP plg_close; /* close function */
+ IFP plg_pre_close; /* pre close function */
Slapi_PluginDesc plg_desc; /* vendor's info */
char *plg_name; /* used for plugin rdn in cn=config */
struct slapdplugin *plg_next; /* for plugin lists */
diff --git a/ldap/servers/slapd/slapi-plugin.h b/ldap/servers/slapd/slapi-plugin.h
index 5d9ef289d..4661d2cf0 100644
--- a/ldap/servers/slapd/slapi-plugin.h
+++ b/ldap/servers/slapd/slapi-plugin.h
@@ -1,6 +1,6 @@
/* BEGIN COPYRIGHT BLOCK
* Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
- * Copyright (C) 2021 Red Hat, Inc.
+ * Copyright (C) 2026 Red Hat, Inc.
* Copyright (C) 2009 Hewlett-Packard Development Company, L.P.
* All rights reserved.
*
@@ -7082,6 +7082,7 @@ typedef struct slapi_plugindesc
/* miscellaneous plugin functions */
#define SLAPI_PLUGIN_CLOSE_FN 210
+#define SLAPI_PLUGIN_PRE_CLOSE_FN 211
#define SLAPI_PLUGIN_START_FN 212
#define SLAPI_PLUGIN_CLEANUP_FN 232
#define SLAPI_PLUGIN_POSTSTART_FN 233
--
2.53.0

View File

@ -1,45 +0,0 @@
From 0ad0eb34972c99f30334d7d420f3056e0e794d74 Mon Sep 17 00:00:00 2001
From: Thierry Bordaz <tbordaz@redhat.com>
Date: Fri, 7 Feb 2025 14:33:46 +0100
Subject: [PATCH] Issue 6494 - (3rd) Various errors when using extended
matching rule on vlv sort filter
(cherry picked from the commit f2f917ca55c34c81b578bce1dd5275abff6abb72)
---
dirsrvtests/tests/suites/vlv/regression_test.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/dirsrvtests/tests/suites/vlv/regression_test.py b/dirsrvtests/tests/suites/vlv/regression_test.py
index 536fe950f..d069fdbaf 100644
--- a/dirsrvtests/tests/suites/vlv/regression_test.py
+++ b/dirsrvtests/tests/suites/vlv/regression_test.py
@@ -16,12 +16,16 @@ from lib389.properties import TASK_WAIT
from lib389.index import *
from lib389.mappingTree import *
from lib389.backend import *
-from lib389.idm.user import UserAccounts
+from lib389.idm.user import UserAccounts, UserAccount
+from lib389.idm.organization import Organization
+from lib389.idm.organizationalunit import OrganizationalUnits
from ldap.controls.vlv import VLVRequestControl
from ldap.controls.sss import SSSRequestControl
pytestmark = pytest.mark.tier1
+DEMO_PW = 'secret12'
+
logging.getLogger(__name__).setLevel(logging.DEBUG)
log = logging.getLogger(__name__)
@@ -169,7 +173,7 @@ class BackendHandler:
'loginShell': '/bin/false',
'userpassword': DEMO_PW })
# Add regular user
- add_users(self.inst, 10, suffix=suffix)
+ add_users(self.inst, 10)
# Removing ou2
ou2.delete()
# And export
--
2.49.0

View File

@ -0,0 +1,34 @@
From 21b9e00264437dff3b6b81355cd159485bfdaf37 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Wed, 4 Mar 2026 14:47:01 -0500
Subject: [PATCH] Issue 7271 - Add new plugin pre-close function check to
plugin_invoke_plugin_pb
Description:
In plugin_invoke_plugin_pb we were not checking for the new pre-close function
which led to an error in the logs: pb_op is NULL. In a debug build this leads
to an assertion error at shutdown.
relates: https://github.com/389ds/389-ds-base/issues/7271
Reviewed by: vashirov(Thanks!)
---
ldap/servers/slapd/plugin.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/ldap/servers/slapd/plugin.c b/ldap/servers/slapd/plugin.c
index 95b8de894..c1b0571eb 100644
--- a/ldap/servers/slapd/plugin.c
+++ b/ldap/servers/slapd/plugin.c
@@ -3647,6 +3647,7 @@ plugin_invoke_plugin_pb(struct slapdplugin *plugin, int operation, Slapi_PBlock
if (operation == SLAPI_PLUGIN_START_FN ||
operation == SLAPI_PLUGIN_POSTSTART_FN ||
operation == SLAPI_PLUGIN_CLOSE_FN ||
+ operation == SLAPI_PLUGIN_PRE_CLOSE_FN ||
operation == SLAPI_PLUGIN_CLEANUP_FN ||
operation == SLAPI_PLUGIN_BE_PRE_CLOSE_FN ||
operation == SLAPI_PLUGIN_BE_POST_OPEN_FN ||
--
2.53.0

View File

@ -1,72 +0,0 @@
From 52041811b200292af6670490c9ebc1f599439a22 Mon Sep 17 00:00:00 2001
From: Masahiro Matsuya <mmatsuya@redhat.com>
Date: Sat, 22 Mar 2025 01:25:25 +0900
Subject: [PATCH] Issue 6494 - (4th) Various errors when using extended
matching rule on vlv sort filter
test_vlv_with_mr uses vlv_setup_with_uid_mr fixture to setup backend
and testusers. add_users function is called in beh.setup without any
suffix for the created backend. As a result, testusers always are
created in the DEFAULT_SUFFIX only by add_users function. Another test
like test_vlv_recreation_reindex can create the same test user in
DEFAULT_SUFFIX, and it caused the ALREADY_EXISTS failure in
test_vlv_with_mr test.
In main branch, add_users have suffix argument. Test users are created
on the specific suffix, and the backend is cleaned up after the test.
This PR is to follow the same implementation.
Also, suppressing ldap.ALREADY_EXISTS makes the add_users func to be
used easily.
Related: https://github.com/389ds/389-ds-base/issues/6494
---
dirsrvtests/tests/suites/vlv/regression_test.py | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/dirsrvtests/tests/suites/vlv/regression_test.py b/dirsrvtests/tests/suites/vlv/regression_test.py
index d069fdbaf..e9408117b 100644
--- a/dirsrvtests/tests/suites/vlv/regression_test.py
+++ b/dirsrvtests/tests/suites/vlv/regression_test.py
@@ -21,6 +21,7 @@ from lib389.idm.organization import Organization
from lib389.idm.organizationalunit import OrganizationalUnits
from ldap.controls.vlv import VLVRequestControl
from ldap.controls.sss import SSSRequestControl
+from contextlib import suppress
pytestmark = pytest.mark.tier1
@@ -68,8 +69,8 @@ def check_vlv_search(conn):
assert dn.lower() == expected_dn.lower()
-def add_users(inst, users_num):
- users = UserAccounts(inst, DEFAULT_SUFFIX)
+def add_users(inst, users_num, suffix=DEFAULT_SUFFIX):
+ users = UserAccounts(inst, suffix)
log.info(f'Adding {users_num} users')
for i in range(0, users_num):
uid = 1000 + i
@@ -81,8 +82,8 @@ def add_users(inst, users_num):
'gidNumber': str(uid),
'homeDirectory': f'/home/testuser{uid}'
}
- users.create(properties=user_properties)
-
+ with suppress(ldap.ALREADY_EXISTS):
+ users.create(properties=user_properties)
def create_vlv_search_and_index(inst, basedn=DEFAULT_SUFFIX, bename='userRoot',
@@ -173,7 +174,7 @@ class BackendHandler:
'loginShell': '/bin/false',
'userpassword': DEMO_PW })
# Add regular user
- add_users(self.inst, 10)
+ add_users(self.inst, 10, suffix=suffix)
# Removing ou2
ou2.delete()
# And export
--
2.49.0

View File

@ -1,357 +0,0 @@
From b812afe4da6db134c1221eb48a6155480e4c2cb3 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Tue, 14 Jan 2025 13:55:03 -0500
Subject: [PATCH] Issue 6497 - lib389 - Configure replication for multiple
suffixes (#6498)
Bug Description: When trying to set up replication across multiple suffixes -
particularly if one of those suffixes is a subsuffix - lib389 fails to properly
configure the replication agreements, service accounts, and required groups.
The references to the replication_managers group and service account
naming do not correctly account for non-default additional suffixes.
Fix Description: Ensure replication DNs and credentials are correctly tied to each suffix.
Enable DSLdapObject.present method to compare values as
a normalized DNs if they are DNs.
Add a test (test_multi_subsuffix_replication) to verify multi-suffix
replication across four suppliers.
Fix tests that are related to repl service accounts.
Fixes: https://github.com/389ds/389-ds-base/issues/6497
Reviewed: @progier389 (Thanks!)
---
.../tests/suites/ds_tools/replcheck_test.py | 4 +-
.../suites/replication/acceptance_test.py | 153 ++++++++++++++++++
.../cleanallruv_shutdown_crash_test.py | 4 +-
.../suites/replication/regression_m2_test.py | 2 +-
.../replication/tls_client_auth_repl_test.py | 4 +-
src/lib389/lib389/_mapped_object.py | 21 ++-
src/lib389/lib389/replica.py | 10 +-
7 files changed, 182 insertions(+), 16 deletions(-)
diff --git a/dirsrvtests/tests/suites/ds_tools/replcheck_test.py b/dirsrvtests/tests/suites/ds_tools/replcheck_test.py
index f61fc432d..dfa1d9423 100644
--- a/dirsrvtests/tests/suites/ds_tools/replcheck_test.py
+++ b/dirsrvtests/tests/suites/ds_tools/replcheck_test.py
@@ -67,10 +67,10 @@ def topo_tls_ldapi(topo):
# Create the replication dns
services = ServiceAccounts(m1, DEFAULT_SUFFIX)
- repl_m1 = services.get('%s:%s' % (m1.host, m1.sslport))
+ repl_m1 = services.get(f'{DEFAULT_SUFFIX}:{m1.host}:{m1.sslport}')
repl_m1.set('nsCertSubjectDN', m1.get_server_tls_subject())
- repl_m2 = services.get('%s:%s' % (m2.host, m2.sslport))
+ repl_m2 = services.get(f'{DEFAULT_SUFFIX}:{m2.host}:{m2.sslport}')
repl_m2.set('nsCertSubjectDN', m2.get_server_tls_subject())
# Check the replication is "done".
diff --git a/dirsrvtests/tests/suites/replication/acceptance_test.py b/dirsrvtests/tests/suites/replication/acceptance_test.py
index d1cfa8bdb..fc8622051 100644
--- a/dirsrvtests/tests/suites/replication/acceptance_test.py
+++ b/dirsrvtests/tests/suites/replication/acceptance_test.py
@@ -9,6 +9,7 @@
import pytest
import logging
import time
+from lib389.backend import Backend
from lib389.replica import Replicas
from lib389.tasks import *
from lib389.utils import *
@@ -325,6 +326,158 @@ def test_modify_stripattrs(topo_m4):
assert attr_value in entries[0].data['nsds5replicastripattrs']
+def test_multi_subsuffix_replication(topo_m4):
+ """Check that replication works with multiple subsuffixes
+
+ :id: ac1aaeae-173e-48e7-847f-03b9867443c4
+ :setup: Four suppliers replication setup
+ :steps:
+ 1. Create additional suffixes
+ 2. Setup replication for all suppliers
+ 3. Generate test data for each suffix (add, modify, remove)
+ 4. Wait for replication to complete across all suppliers for each suffix
+ 5. Check that all expected data is present on all suppliers
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. Success (the data is replicated everywhere)
+ """
+
+ SUFFIX_2 = "dc=test2"
+ SUFFIX_3 = f"dc=test3,{DEFAULT_SUFFIX}"
+ all_suffixes = [DEFAULT_SUFFIX, SUFFIX_2, SUFFIX_3]
+
+ test_users_by_suffix = {suffix: [] for suffix in all_suffixes}
+ created_backends = []
+
+ suppliers = [
+ topo_m4.ms["supplier1"],
+ topo_m4.ms["supplier2"],
+ topo_m4.ms["supplier3"],
+ topo_m4.ms["supplier4"]
+ ]
+
+ try:
+ # Setup additional backends and replication for the new suffixes
+ for suffix in [SUFFIX_2, SUFFIX_3]:
+ repl = ReplicationManager(suffix)
+ for supplier in suppliers:
+ # Create a new backend for this suffix
+ props = {
+ 'cn': f'userRoot_{suffix.split(",")[0][3:]}',
+ 'nsslapd-suffix': suffix
+ }
+ be = Backend(supplier)
+ be.create(properties=props)
+ be.create_sample_entries('001004002')
+
+ # Track the backend so we can remove it later
+ created_backends.append((supplier, props['cn']))
+
+ # Enable replication
+ if supplier == suppliers[0]:
+ repl.create_first_supplier(supplier)
+ else:
+ repl.join_supplier(suppliers[0], supplier)
+
+ # Create a full mesh topology for this suffix
+ for i, supplier_i in enumerate(suppliers):
+ for j, supplier_j in enumerate(suppliers):
+ if i != j:
+ repl.ensure_agreement(supplier_i, supplier_j)
+
+ # Generate test data for each suffix (add, modify, remove)
+ for suffix in all_suffixes:
+ # Create some user entries in supplier1
+ for i in range(20):
+ user_dn = f'uid=test_user_{i},{suffix}'
+ test_user = UserAccount(suppliers[0], user_dn)
+ test_user.create(properties={
+ 'uid': f'test_user_{i}',
+ 'cn': f'Test User {i}',
+ 'sn': f'User{i}',
+ 'userPassword': 'password',
+ 'uidNumber': str(1000 + i),
+ 'gidNumber': '2000',
+ 'homeDirectory': f'/home/test_user_{i}'
+ })
+ test_users_by_suffix[suffix].append(test_user)
+
+ # Perform modifications on these entries
+ for user in test_users_by_suffix[suffix]:
+ # Add some attributes
+ for j in range(3):
+ user.add('description', f'Description {j}')
+ # Replace an attribute
+ user.replace('cn', f'Modified User {user.get_attr_val_utf8("uid")}')
+ # Delete the attributes we added
+ for j in range(3):
+ try:
+ user.remove('description', f'Description {j}')
+ except Exception:
+ pass
+
+ # Wait for replication to complete across all suppliers, for each suffix
+ for suffix in all_suffixes:
+ repl = ReplicationManager(suffix)
+ for i, supplier_i in enumerate(suppliers):
+ for j, supplier_j in enumerate(suppliers):
+ if i != j:
+ repl.wait_for_replication(supplier_i, supplier_j)
+
+ # Verify that each user and modification replicated to all suppliers
+ for suffix in all_suffixes:
+ for i in range(20):
+ user_dn = f'uid=test_user_{i},{suffix}'
+ # Retrieve this user from all suppliers
+ all_user_objs = topo_m4.all_get_dsldapobject(user_dn, UserAccount)
+ # Ensure it exists in all 4 suppliers
+ assert len(all_user_objs) == 4, (
+ f"User {user_dn} not found on all suppliers. "
+ f"Found only on {len(all_user_objs)} suppliers."
+ )
+ # Check modifications: 'cn' should now be 'Modified User test_user_{i}'
+ for user_obj in all_user_objs:
+ expected_cn = f"Modified User test_user_{i}"
+ actual_cn = user_obj.get_attr_val_utf8("cn")
+ assert actual_cn == expected_cn, (
+ f"User {user_dn} has unexpected 'cn': {actual_cn} "
+ f"(expected '{expected_cn}') on supplier {user_obj._instance.serverid}"
+ )
+ # And check that 'description' attributes were removed
+ desc_vals = user_obj.get_attr_vals_utf8('description')
+ for j in range(3):
+ assert f"Description {j}" not in desc_vals, (
+ f"User {user_dn} on supplier {user_obj._instance.serverid} "
+ f"still has 'Description {j}'"
+ )
+ finally:
+ for suffix, test_users in test_users_by_suffix.items():
+ for user in test_users:
+ try:
+ if user.exists():
+ user.delete()
+ except Exception:
+ pass
+
+ for suffix in [SUFFIX_2, SUFFIX_3]:
+ repl = ReplicationManager(suffix)
+ for supplier in suppliers:
+ try:
+ repl.remove_supplier(supplier)
+ except Exception:
+ pass
+
+ for (supplier, backend_name) in created_backends:
+ be = Backend(supplier, backend_name)
+ try:
+ be.delete()
+ except Exception:
+ pass
+
+
def test_new_suffix(topo_m4, new_suffix):
"""Check that we can enable replication on a new suffix
diff --git a/dirsrvtests/tests/suites/replication/cleanallruv_shutdown_crash_test.py b/dirsrvtests/tests/suites/replication/cleanallruv_shutdown_crash_test.py
index b4b74e339..fe9955e7e 100644
--- a/dirsrvtests/tests/suites/replication/cleanallruv_shutdown_crash_test.py
+++ b/dirsrvtests/tests/suites/replication/cleanallruv_shutdown_crash_test.py
@@ -66,10 +66,10 @@ def test_clean_shutdown_crash(topology_m2):
log.info('Creating replication dns')
services = ServiceAccounts(m1, DEFAULT_SUFFIX)
- repl_m1 = services.get('%s:%s' % (m1.host, m1.sslport))
+ repl_m1 = services.get(f'{DEFAULT_SUFFIX}:{m1.host}:{m1.sslport}')
repl_m1.set('nsCertSubjectDN', m1.get_server_tls_subject())
- repl_m2 = services.get('%s:%s' % (m2.host, m2.sslport))
+ repl_m2 = services.get(f'{DEFAULT_SUFFIX}:{m2.host}:{m2.sslport}')
repl_m2.set('nsCertSubjectDN', m2.get_server_tls_subject())
log.info('Changing auth type')
diff --git a/dirsrvtests/tests/suites/replication/regression_m2_test.py b/dirsrvtests/tests/suites/replication/regression_m2_test.py
index 72d4b9f89..9c707615f 100644
--- a/dirsrvtests/tests/suites/replication/regression_m2_test.py
+++ b/dirsrvtests/tests/suites/replication/regression_m2_test.py
@@ -64,7 +64,7 @@ class _AgmtHelper:
self.binddn = f'cn={cn},cn=config'
else:
self.usedn = False
- self.cn = f'{self.from_inst.host}:{self.from_inst.sslport}'
+ self.cn = ldap.dn.escape_dn_chars(f'{DEFAULT_SUFFIX}:{self.from_inst.host}:{self.from_inst.sslport}')
self.binddn = f'cn={self.cn}, ou=Services, {DEFAULT_SUFFIX}'
self.original_state = []
self._pass = False
diff --git a/dirsrvtests/tests/suites/replication/tls_client_auth_repl_test.py b/dirsrvtests/tests/suites/replication/tls_client_auth_repl_test.py
index a00dc5b78..ca17554c7 100644
--- a/dirsrvtests/tests/suites/replication/tls_client_auth_repl_test.py
+++ b/dirsrvtests/tests/suites/replication/tls_client_auth_repl_test.py
@@ -56,10 +56,10 @@ def tls_client_auth(topo_m2):
# Create the replication dns
services = ServiceAccounts(m1, DEFAULT_SUFFIX)
- repl_m1 = services.get('%s:%s' % (m1.host, m1.sslport))
+ repl_m1 = services.get(f'{DEFAULT_SUFFIX}:{m1.host}:{m1.sslport}')
repl_m1.set('nsCertSubjectDN', m1.get_server_tls_subject())
- repl_m2 = services.get('%s:%s' % (m2.host, m2.sslport))
+ repl_m2 = services.get(f'{DEFAULT_SUFFIX}:{m2.host}:{m2.sslport}')
repl_m2.set('nsCertSubjectDN', m2.get_server_tls_subject())
# Check the replication is "done".
diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py
index b7391d8cc..ae00c95d0 100644
--- a/src/lib389/lib389/_mapped_object.py
+++ b/src/lib389/lib389/_mapped_object.py
@@ -19,7 +19,7 @@ from lib389._constants import DIRSRV_STATE_ONLINE
from lib389._mapped_object_lint import DSLint, DSLints
from lib389.utils import (
ensure_bytes, ensure_str, ensure_int, ensure_list_bytes, ensure_list_str,
- ensure_list_int, display_log_value, display_log_data
+ ensure_list_int, display_log_value, display_log_data, is_a_dn, normalizeDN
)
# This function filter and term generation provided thanks to
@@ -292,15 +292,28 @@ class DSLdapObject(DSLogging, DSLint):
_search_ext_s(self._instance,self._dn, ldap.SCOPE_BASE, self._object_filter, attrlist=[attr, ],
serverctrls=self._server_controls, clientctrls=self._client_controls,
escapehatch='i am sure')[0]
- values = self.get_attr_vals_bytes(attr)
+ values = self.get_attr_vals_utf8(attr)
self._log.debug("%s contains %s" % (self._dn, values))
if value is None:
# We are just checking if SOMETHING is present ....
return len(values) > 0
+
+ # Otherwise, we are checking a specific value
+ if is_a_dn(value):
+ normalized_value = normalizeDN(value)
else:
- # Check if a value really does exist.
- return ensure_bytes(value).lower() in [x.lower() for x in values]
+ normalized_value = ensure_bytes(value).lower()
+
+ # Normalize each returned value depending on whether it is a DN
+ normalized_values = []
+ for v in values:
+ if is_a_dn(v):
+ normalized_values.append(normalizeDN(v))
+ else:
+ normalized_values.append(ensure_bytes(v.lower()))
+
+ return normalized_value in normalized_values
def add(self, key, value):
"""Add an attribute with a value
diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py
index 1f321972d..cd46e86d5 100644
--- a/src/lib389/lib389/replica.py
+++ b/src/lib389/lib389/replica.py
@@ -2011,7 +2011,7 @@ class ReplicationManager(object):
return repl_group
else:
try:
- repl_group = groups.get('replication_managers')
+ repl_group = groups.get(dn=f'cn=replication_managers,{self._suffix}')
return repl_group
except ldap.NO_SUCH_OBJECT:
self._log.warning("{} doesn't have cn=replication_managers,{} entry \
@@ -2035,7 +2035,7 @@ class ReplicationManager(object):
services = ServiceAccounts(from_instance, self._suffix)
# Generate the password and save the credentials
# for putting them into agreements in the future
- service_name = '{}:{}'.format(to_instance.host, port)
+ service_name = f'{self._suffix}:{to_instance.host}:{port}'
creds = password_generate()
repl_service = services.ensure_state(properties={
'cn': service_name,
@@ -2299,7 +2299,7 @@ class ReplicationManager(object):
Internal Only.
"""
- rdn = '{}:{}'.format(from_instance.host, from_instance.sslport)
+ rdn = f'{self._suffix}:{from_instance.host}:{from_instance.sslport}'
try:
creds = self._repl_creds[rdn]
except KeyError:
@@ -2499,8 +2499,8 @@ class ReplicationManager(object):
# Touch something then wait_for_replication.
from_groups = Groups(from_instance, basedn=self._suffix, rdn=None)
to_groups = Groups(to_instance, basedn=self._suffix, rdn=None)
- from_group = from_groups.get('replication_managers')
- to_group = to_groups.get('replication_managers')
+ from_group = from_groups.get(dn=f'cn=replication_managers,{self._suffix}')
+ to_group = to_groups.get(dn=f'cn=replication_managers,{self._suffix}')
change = str(uuid.uuid4())
--
2.49.0

View File

@ -1,126 +0,0 @@
From ebe986c78c6cd4e1f10172d8a8a11faf814fbc22 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Thu, 6 Mar 2025 16:49:53 -0500
Subject: [PATCH] Issue 6655 - fix replication release replica decoding error
Description:
When a start replication session extended op is received acquire and
release exclusive access before returning the result to the client.
Otherwise there is a race condition where a "end" replication extended
op can arrive before the replica is released and that leads to a
decoding error on the other replica.
Relates: https://github.com/389ds/389-ds-base/issues/6655
Reviewed by: spichugi, tbordaz, and vashirov(Thanks!!!)
---
.../suites/replication/acceptance_test.py | 12 ++++++++++
ldap/servers/plugins/replication/repl_extop.c | 24 ++++++++++++-------
2 files changed, 27 insertions(+), 9 deletions(-)
diff --git a/dirsrvtests/tests/suites/replication/acceptance_test.py b/dirsrvtests/tests/suites/replication/acceptance_test.py
index fc8622051..0f18edb44 100644
--- a/dirsrvtests/tests/suites/replication/acceptance_test.py
+++ b/dirsrvtests/tests/suites/replication/acceptance_test.py
@@ -1,5 +1,9 @@
# --- BEGIN COPYRIGHT BLOCK ---
+<<<<<<< HEAD
# Copyright (C) 2021 Red Hat, Inc.
+=======
+# Copyright (C) 2025 Red Hat, Inc.
+>>>>>>> a623c3f90 (Issue 6655 - fix replication release replica decoding error)
# All rights reserved.
#
# License: GPL (version 3 or any later version).
@@ -453,6 +457,13 @@ def test_multi_subsuffix_replication(topo_m4):
f"User {user_dn} on supplier {user_obj._instance.serverid} "
f"still has 'Description {j}'"
)
+
+ # Check there are no decoding errors
+ assert not topo_m4.ms["supplier1"].ds_error_log.match('.*decoding failed.*')
+ assert not topo_m4.ms["supplier2"].ds_error_log.match('.*decoding failed.*')
+ assert not topo_m4.ms["supplier3"].ds_error_log.match('.*decoding failed.*')
+ assert not topo_m4.ms["supplier4"].ds_error_log.match('.*decoding failed.*')
+
finally:
for suffix, test_users in test_users_by_suffix.items():
for user in test_users:
@@ -507,6 +518,7 @@ def test_new_suffix(topo_m4, new_suffix):
repl.remove_supplier(m1)
repl.remove_supplier(m2)
+
def test_many_attrs(topo_m4, create_entry):
"""Check a replication with many attributes (add and delete)
diff --git a/ldap/servers/plugins/replication/repl_extop.c b/ldap/servers/plugins/replication/repl_extop.c
index 14b756df1..dacc611c0 100644
--- a/ldap/servers/plugins/replication/repl_extop.c
+++ b/ldap/servers/plugins/replication/repl_extop.c
@@ -1134,6 +1134,12 @@ send_response:
slapi_pblock_set(pb, SLAPI_EXT_OP_RET_OID, REPL_NSDS50_REPLICATION_RESPONSE_OID);
}
+ /* connext (release our hold on it at least) */
+ if (NULL != connext) {
+ /* don't free it, just let go of it */
+ consumer_connection_extension_relinquish_exclusive_access(conn, connid, opid, PR_FALSE);
+ }
+
slapi_pblock_set(pb, SLAPI_EXT_OP_RET_VALUE, resp_bval);
slapi_log_err(SLAPI_LOG_REPL, repl_plugin_name,
"multimaster_extop_StartNSDS50ReplicationRequest - "
@@ -1251,12 +1257,6 @@ send_response:
if (NULL != ruv_bervals) {
ber_bvecfree(ruv_bervals);
}
- /* connext (our hold on it at least) */
- if (NULL != connext) {
- /* don't free it, just let go of it */
- consumer_connection_extension_relinquish_exclusive_access(conn, connid, opid, PR_FALSE);
- connext = NULL;
- }
return return_value;
}
@@ -1389,6 +1389,13 @@ multimaster_extop_EndNSDS50ReplicationRequest(Slapi_PBlock *pb)
}
}
send_response:
+ /* connext (release our hold on it at least) */
+ if (NULL != connext) {
+ /* don't free it, just let go of it */
+ consumer_connection_extension_relinquish_exclusive_access(conn, connid, opid, PR_FALSE);
+ connext = NULL;
+ }
+
/* Send the response code */
if ((resp_bere = der_alloc()) == NULL) {
goto free_and_return;
@@ -1419,11 +1426,10 @@ free_and_return:
if (NULL != resp_bval) {
ber_bvfree(resp_bval);
}
- /* connext (our hold on it at least) */
+ /* connext (release our hold on it if not already released) */
if (NULL != connext) {
/* don't free it, just let go of it */
consumer_connection_extension_relinquish_exclusive_access(conn, connid, opid, PR_FALSE);
- connext = NULL;
}
return return_value;
@@ -1516,7 +1522,7 @@ multimaster_extop_abort_cleanruv(Slapi_PBlock *pb)
rid);
}
/*
- * Get the replica
+ * Get the replica
*/
if ((r = replica_get_replica_from_root(repl_root)) == NULL) {
slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "multimaster_extop_abort_cleanruv - "
--
2.49.0

View File

@ -1,26 +0,0 @@
From 5b12463bfeb518f016acb14bc118b5f8ad3eef5e Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Thu, 15 May 2025 09:22:22 +0200
Subject: [PATCH] Issue 6655 - fix merge conflict
---
dirsrvtests/tests/suites/replication/acceptance_test.py | 4 ----
1 file changed, 4 deletions(-)
diff --git a/dirsrvtests/tests/suites/replication/acceptance_test.py b/dirsrvtests/tests/suites/replication/acceptance_test.py
index 0f18edb44..6b5186127 100644
--- a/dirsrvtests/tests/suites/replication/acceptance_test.py
+++ b/dirsrvtests/tests/suites/replication/acceptance_test.py
@@ -1,9 +1,5 @@
# --- BEGIN COPYRIGHT BLOCK ---
-<<<<<<< HEAD
-# Copyright (C) 2021 Red Hat, Inc.
-=======
# Copyright (C) 2025 Red Hat, Inc.
->>>>>>> a623c3f90 (Issue 6655 - fix replication release replica decoding error)
# All rights reserved.
#
# License: GPL (version 3 or any later version).
--
2.49.0

View File

@ -1,291 +0,0 @@
From 8d62124fb4d0700378b6f0669cc9d47338a8151c Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Tue, 25 Mar 2025 09:20:50 +0100
Subject: [PATCH] Issue 6571 - Nested group does not receive memberOf attribute
(#6679)
Bug description:
There is a risk to create a loop in group membership.
For example G2 is member of G1 and G1 is member of G2.
Memberof plugins iterates from a node to its ancestors
to update the 'memberof' values of the node.
The plugin uses a valueset ('already_seen_ndn_vals')
to keep the track of the nodes it already visited.
It uses this valueset to detect a possible loop and
in that case it does not add the ancestor as the
memberof value of the node.
This is an error in case there are multiples paths
up to an ancestor.
Fix description:
The ancestor should be added to the node systematically,
just in case the ancestor is in 'already_seen_ndn_vals'
it skips the final recursion
fixes: #6571
Reviewed by: Pierre Rogier, Mark Reynolds (Thanks !!!)
---
.../suites/memberof_plugin/regression_test.py | 109 ++++++++++++++++++
.../tests/suites/plugins/memberof_test.py | 5 +
ldap/servers/plugins/memberof/memberof.c | 52 ++++-----
3 files changed, 137 insertions(+), 29 deletions(-)
diff --git a/dirsrvtests/tests/suites/memberof_plugin/regression_test.py b/dirsrvtests/tests/suites/memberof_plugin/regression_test.py
index 4c681a909..dba908975 100644
--- a/dirsrvtests/tests/suites/memberof_plugin/regression_test.py
+++ b/dirsrvtests/tests/suites/memberof_plugin/regression_test.py
@@ -467,6 +467,21 @@ def _find_memberof_ext(server, user_dn=None, group_dn=None, find_result=True):
else:
assert (not found)
+def _check_membership(server, entry, expected_members, expected_memberof):
+ assert server
+ assert entry
+
+ memberof = entry.get_attr_vals('memberof')
+ member = entry.get_attr_vals('member')
+ assert len(member) == len(expected_members)
+ assert len(memberof) == len(expected_memberof)
+ for e in expected_members:
+ server.log.info("Checking %s has member %s" % (entry.dn, e.dn))
+ assert e.dn.encode() in member
+ for e in expected_memberof:
+ server.log.info("Checking %s is member of %s" % (entry.dn, e.dn))
+ assert e.dn.encode() in memberof
+
@pytest.mark.ds49161
def test_memberof_group(topology_st):
@@ -535,6 +550,100 @@ def test_memberof_group(topology_st):
_find_memberof_ext(inst, dn1, g2n, True)
_find_memberof_ext(inst, dn2, g2n, True)
+def test_multipaths(topology_st, request):
+ """Test memberof succeeds to update memberof when
+ there are multiple paths from a leaf to an intermediate node
+
+ :id: 35aa704a-b895-4153-9dcb-1e8a13612ebf
+
+ :setup: Single instance
+
+ :steps:
+ 1. Create a graph G1->U1, G2->G21->U1
+ 2. Add G2 as member of G1: G1->U1, G1->G2->G21->U1
+ 3. Check members and memberof in entries G1,G2,G21,User1
+
+ :expectedresults:
+ 1. Graph should be created
+ 2. succeed
+ 3. Membership is okay
+ """
+
+ inst = topology_st.standalone
+ memberof = MemberOfPlugin(inst)
+ memberof.enable()
+ memberof.replace('memberOfEntryScope', SUFFIX)
+ if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() == "on"):
+ delay = 3
+ else:
+ delay = 0
+ inst.restart()
+
+ #
+ # Create the hierarchy
+ #
+ #
+ # Grp1 ---------------> User1
+ # ^
+ # /
+ # Grp2 ----> Grp21 ------/
+ #
+ users = UserAccounts(inst, SUFFIX, rdn=None)
+ user1 = users.create(properties={'uid': "user1",
+ 'cn': "user1",
+ 'sn': 'SN',
+ 'description': 'leaf',
+ 'uidNumber': '1000',
+ 'gidNumber': '2000',
+ 'homeDirectory': '/home/user1'
+ })
+ group = Groups(inst, SUFFIX, rdn=None)
+ g1 = group.create(properties={'cn': 'group1',
+ 'member': user1.dn,
+ 'description': 'group1'})
+ g21 = group.create(properties={'cn': 'group21',
+ 'member': user1.dn,
+ 'description': 'group21'})
+ g2 = group.create(properties={'cn': 'group2',
+ 'member': [g21.dn],
+ 'description': 'group2'})
+
+ # Enable debug logs if necessary
+ #inst.config.replace('nsslapd-errorlog-level', '65536')
+ #inst.config.set('nsslapd-accesslog-level','260')
+ #inst.config.set('nsslapd-plugin-logging', 'on')
+ #inst.config.set('nsslapd-auditlog-logging-enabled','on')
+ #inst.config.set('nsslapd-auditfaillog-logging-enabled','on')
+
+ #
+ # Update the hierarchy
+ #
+ #
+ # Grp1 ----------------> User1
+ # \ ^
+ # \ /
+ # --> Grp2 --> Grp21 --
+ #
+ g1.add_member(g2.dn)
+ time.sleep(delay)
+
+ #
+ # Check G1, G2, G21 and User1 members and memberof
+ #
+ _check_membership(inst, g1, expected_members=[g2, user1], expected_memberof=[])
+ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[g1])
+ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2, g1])
+ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1])
+
+ def fin():
+ try:
+ user1.delete()
+ g1.delete()
+ g2.delete()
+ g21.delete()
+ except:
+ pass
+ request.addfinalizer(fin)
def _config_memberof_entrycache_on_modrdn_failure(server):
diff --git a/dirsrvtests/tests/suites/plugins/memberof_test.py b/dirsrvtests/tests/suites/plugins/memberof_test.py
index 2de1389fd..621c45daf 100644
--- a/dirsrvtests/tests/suites/plugins/memberof_test.py
+++ b/dirsrvtests/tests/suites/plugins/memberof_test.py
@@ -2168,9 +2168,14 @@ def test_complex_group_scenario_6(topology_st):
# add Grp[1-4] (uniqueMember) to grp5
# it creates a membership loop !!!
+ topology_st.standalone.config.replace('nsslapd-errorlog-level', '65536')
mods = [(ldap.MOD_ADD, 'uniqueMember', memofegrp020_5)]
for grp in [memofegrp020_1, memofegrp020_2, memofegrp020_3, memofegrp020_4]:
topology_st.standalone.modify_s(ensure_str(grp), mods)
+ topology_st.standalone.config.replace('nsslapd-errorlog-level', '0')
+
+ results = topology_st.standalone.ds_error_log.match('.*detecting a loop in group.*')
+ assert results
time.sleep(5)
# assert user[1-4] are member of grp20_[1-4]
diff --git a/ldap/servers/plugins/memberof/memberof.c b/ldap/servers/plugins/memberof/memberof.c
index e75b99b14..32bdcf3f1 100644
--- a/ldap/servers/plugins/memberof/memberof.c
+++ b/ldap/servers/plugins/memberof/memberof.c
@@ -1592,7 +1592,7 @@ memberof_call_foreach_dn(Slapi_PBlock *pb __attribute__((unused)), Slapi_DN *sdn
ht_grp = ancestors_cache_lookup(config, (const void *)ndn);
if (ht_grp) {
#if MEMBEROF_CACHE_DEBUG
- slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_call_foreach_dn: Ancestors of %s already cached (%x)\n", ndn, ht_grp);
+ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_call_foreach_dn: Ancestors of %s already cached (%lx)\n", ndn, (ulong) ht_grp);
#endif
add_ancestors_cbdata(ht_grp, callback_data);
*cached = 1;
@@ -1600,7 +1600,7 @@ memberof_call_foreach_dn(Slapi_PBlock *pb __attribute__((unused)), Slapi_DN *sdn
}
}
#if MEMBEROF_CACHE_DEBUG
- slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_call_foreach_dn: Ancestors of %s not cached\n", ndn);
+ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_call_foreach_dn: Ancestors of %s not cached\n", slapi_sdn_get_ndn(sdn));
#endif
/* Escape the dn, and build the search filter. */
@@ -3233,7 +3233,8 @@ cache_ancestors(MemberOfConfig *config, Slapi_Value **member_ndn_val, memberof_g
return;
}
#if MEMBEROF_CACHE_DEBUG
- if (double_check = ancestors_cache_lookup(config, (const void*) key)) {
+ double_check = ancestors_cache_lookup(config, (const void*) key);
+ if (double_check) {
dump_cache_entry(double_check, "read back");
}
#endif
@@ -3263,13 +3264,13 @@ merge_ancestors(Slapi_Value **member_ndn_val, memberof_get_groups_data *v1, memb
sval_dn = slapi_value_new_string(slapi_value_get_string(sval));
if (sval_dn) {
/* Use the normalized dn from v1 to search it
- * in v2
- */
+ * in v2
+ */
val_sdn = slapi_sdn_new_dn_byval(slapi_value_get_string(sval_dn));
sval_ndn = slapi_value_new_string(slapi_sdn_get_ndn(val_sdn));
if (!slapi_valueset_find(
((memberof_get_groups_data *)v2)->config->group_slapiattrs[0], v2_group_norm_vals, sval_ndn)) {
-/* This ancestor was not already present in v2 => Add it
+ /* This ancestor was not already present in v2 => Add it
* Using slapi_valueset_add_value it consumes val
* so do not free sval
*/
@@ -3318,7 +3319,7 @@ memberof_get_groups_r(MemberOfConfig *config, Slapi_DN *member_sdn, memberof_get
merge_ancestors(&member_ndn_val, &member_data, data);
if (!cached && member_data.use_cache)
- cache_ancestors(config, &member_ndn_val, &member_data);
+ cache_ancestors(config, &member_ndn_val, data);
slapi_value_free(&member_ndn_val);
slapi_valueset_free(groupvals);
@@ -3379,25 +3380,6 @@ memberof_get_groups_callback(Slapi_Entry *e, void *callback_data)
goto bail;
}
- /* Have we been here before? Note that we don't loop through all of the group_slapiattrs
- * in config. We only need this attribute for it's syntax so the comparison can be
- * performed. Since all of the grouping attributes are validated to use the Dinstinguished
- * Name syntax, we can safely just use the first group_slapiattr. */
- if (slapi_valueset_find(
- ((memberof_get_groups_data *)callback_data)->config->group_slapiattrs[0], already_seen_ndn_vals, group_ndn_val)) {
- /* we either hit a recursive grouping, or an entry is
- * a member of a group through multiple paths. Either
- * way, we can just skip processing this entry since we've
- * already gone through this part of the grouping hierarchy. */
- slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
- "memberof_get_groups_callback - Possible group recursion"
- " detected in %s\n",
- group_ndn);
- slapi_value_free(&group_ndn_val);
- ((memberof_get_groups_data *)callback_data)->use_cache = PR_FALSE;
- goto bail;
- }
-
/* if the group does not belong to an excluded subtree, adds it to the valueset */
if (memberof_entry_in_scope(config, group_sdn)) {
/* Push group_dn_val into the valueset. This memory is now owned
@@ -3407,9 +3389,21 @@ memberof_get_groups_callback(Slapi_Entry *e, void *callback_data)
group_dn_val = slapi_value_new_string(group_dn);
slapi_valueset_add_value_ext(groupvals, group_dn_val, SLAPI_VALUE_FLAG_PASSIN);
- /* push this ndn to detect group recursion */
- already_seen_ndn_val = slapi_value_new_string(group_ndn);
- slapi_valueset_add_value_ext(already_seen_ndn_vals, already_seen_ndn_val, SLAPI_VALUE_FLAG_PASSIN);
+ if (slapi_valueset_find(
+ ((memberof_get_groups_data *)callback_data)->config->group_slapiattrs[0], already_seen_ndn_vals, group_ndn_val)) {
+ /* The group group_ndn_val has already been processed
+ * skip the final recursion to prevent infinite loop
+ */
+ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "memberof_get_groups_callback - detecting a loop in group %s (stop building memberof)\n",
+ group_ndn);
+ ((memberof_get_groups_data *)callback_data)->use_cache = PR_FALSE;
+ goto bail;
+ } else {
+ /* keep this ndn to detect a possible group recursion */
+ already_seen_ndn_val = slapi_value_new_string(group_ndn);
+ slapi_valueset_add_value_ext(already_seen_ndn_vals, already_seen_ndn_val, SLAPI_VALUE_FLAG_PASSIN);
+ }
}
if (!config->skip_nested || config->fixup_task) {
/* now recurse to find ancestors groups of e */
--
2.49.0

View File

@ -1,272 +0,0 @@
From 17da0257b24749765777a4e64c3626cb39cca639 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Mon, 31 Mar 2025 11:05:01 +0200
Subject: [PATCH] Issue 6571 - (2nd) Nested group does not receive memberOf
attribute (#6697)
Bug description:
erroneous debug change made in previous fix
where cache_ancestors is called with the wrong parameter
Fix description:
Restore the orginal param 'member_data'
Increase the set of tests around multipaths
fixes: #6571
review by: Simon Pichugin (Thanks !!)
---
.../suites/memberof_plugin/regression_test.py | 154 ++++++++++++++++++
ldap/servers/plugins/memberof/memberof.c | 50 +++++-
2 files changed, 203 insertions(+), 1 deletion(-)
diff --git a/dirsrvtests/tests/suites/memberof_plugin/regression_test.py b/dirsrvtests/tests/suites/memberof_plugin/regression_test.py
index dba908975..9ba40a0c3 100644
--- a/dirsrvtests/tests/suites/memberof_plugin/regression_test.py
+++ b/dirsrvtests/tests/suites/memberof_plugin/regression_test.py
@@ -598,6 +598,8 @@ def test_multipaths(topology_st, request):
'homeDirectory': '/home/user1'
})
group = Groups(inst, SUFFIX, rdn=None)
+ g0 = group.create(properties={'cn': 'group0',
+ 'description': 'group0'})
g1 = group.create(properties={'cn': 'group1',
'member': user1.dn,
'description': 'group1'})
@@ -635,6 +637,158 @@ def test_multipaths(topology_st, request):
_check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2, g1])
_check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1])
+ #inst.config.replace('nsslapd-errorlog-level', '65536')
+ #inst.config.set('nsslapd-accesslog-level','260')
+ #inst.config.set('nsslapd-plugin-logging', 'on')
+ #inst.config.set('nsslapd-auditlog-logging-enabled','on')
+ #inst.config.set('nsslapd-auditfaillog-logging-enabled','on')
+ #
+ # Update the hierarchy
+ #
+ #
+ # Grp1 ----------------> User1
+ # ^
+ # /
+ # Grp2 --> Grp21 --
+ #
+ g1.remove_member(g2.dn)
+ time.sleep(delay)
+
+ #
+ # Check G1, G2, G21 and User1 members and memberof
+ #
+ _check_membership(inst, g1, expected_members=[user1], expected_memberof=[])
+ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[])
+ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2])
+ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1])
+
+ #
+ # Update the hierarchy
+ #
+ #
+ # Grp1 ----------------> User1
+ # \__________ ^
+ # | /
+ # v /
+ # Grp2 --> Grp21 ----
+ #
+ g1.add_member(g21.dn)
+ time.sleep(delay)
+
+ #
+ # Check G1, G2, G21 and User1 members and memberof
+ #
+ _check_membership(inst, g1, expected_members=[user1, g21], expected_memberof=[])
+ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[])
+ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2, g1])
+ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1])
+
+ #
+ # Update the hierarchy
+ #
+ #
+ # Grp1 ----------------> User1
+ # ^
+ # /
+ # Grp2 --> Grp21 --
+ #
+ g1.remove_member(g21.dn)
+ time.sleep(delay)
+
+ #
+ # Check G1, G2, G21 and User1 members and memberof
+ #
+ _check_membership(inst, g1, expected_members=[user1], expected_memberof=[])
+ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[])
+ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2])
+ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1])
+
+ #
+ # Update the hierarchy
+ #
+ #
+ # Grp1 ----------------> User1
+ # ^
+ # /
+ # Grp0 ---> Grp2 ---> Grp21 ---
+ #
+ g0.add_member(g2.dn)
+ time.sleep(delay)
+
+ #
+ # Check G0,G1, G2, G21 and User1 members and memberof
+ #
+ _check_membership(inst, g0, expected_members=[g2], expected_memberof=[])
+ _check_membership(inst, g1, expected_members=[user1], expected_memberof=[])
+ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[g0])
+ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g0, g2])
+ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1, g0])
+
+ #
+ # Update the hierarchy
+ #
+ #
+ # Grp1 ----------------> User1
+ # ^ ^
+ # / /
+ # Grp0 ---> Grp2 ---> Grp21 ---
+ #
+ g0.add_member(g1.dn)
+ time.sleep(delay)
+
+ #
+ # Check G0,G1, G2, G21 and User1 members and memberof
+ #
+ _check_membership(inst, g0, expected_members=[g1,g2], expected_memberof=[])
+ _check_membership(inst, g1, expected_members=[user1], expected_memberof=[g0])
+ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[g0])
+ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g0, g2])
+ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1, g0])
+
+ #
+ # Update the hierarchy
+ #
+ #
+ # Grp1 ----------------> User1
+ # ^ \_____________ ^
+ # / | /
+ # / V /
+ # Grp0 ---> Grp2 ---> Grp21 ---
+ #
+ g1.add_member(g21.dn)
+ time.sleep(delay)
+
+ #
+ # Check G0,G1, G2, G21 and User1 members and memberof
+ #
+ _check_membership(inst, g0, expected_members=[g1, g2], expected_memberof=[])
+ _check_membership(inst, g1, expected_members=[user1, g21], expected_memberof=[g0])
+ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[g0])
+ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g0, g1, g2])
+ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1, g0])
+
+ #
+ # Update the hierarchy
+ #
+ #
+ # Grp1 ----------------> User1
+ # ^ \_____________ ^
+ # / | /
+ # / V /
+ # Grp0 ---> Grp2 Grp21 ---
+ #
+ g2.remove_member(g21.dn)
+ time.sleep(delay)
+
+ #
+ # Check G0,G1, G2, G21 and User1 members and memberof
+ #
+ _check_membership(inst, g0, expected_members=[g1, g2], expected_memberof=[])
+ _check_membership(inst, g1, expected_members=[user1, g21], expected_memberof=[g0])
+ _check_membership(inst, g2, expected_members=[], expected_memberof=[g0])
+ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g0, g1])
+ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g1, g0])
+
def fin():
try:
user1.delete()
diff --git a/ldap/servers/plugins/memberof/memberof.c b/ldap/servers/plugins/memberof/memberof.c
index 32bdcf3f1..f79b083a9 100644
--- a/ldap/servers/plugins/memberof/memberof.c
+++ b/ldap/servers/plugins/memberof/memberof.c
@@ -3258,6 +3258,35 @@ merge_ancestors(Slapi_Value **member_ndn_val, memberof_get_groups_data *v1, memb
Slapi_ValueSet *v2_group_norm_vals = *((memberof_get_groups_data *)v2)->group_norm_vals;
int merged_cnt = 0;
+#if MEMBEROF_CACHE_DEBUG
+ {
+ Slapi_Value *val = 0;
+ int hint = 0;
+ struct berval *bv;
+ hint = slapi_valueset_first_value(v2_groupvals, &val);
+ while (val) {
+ /* this makes a copy of the berval */
+ bv = slapi_value_get_berval(val);
+ if (bv && bv->bv_len) {
+ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "merge_ancestors: V2 contains %s\n",
+ bv->bv_val);
+ }
+ hint = slapi_valueset_next_value(v2_groupvals, hint, &val);
+ }
+ hint = slapi_valueset_first_value(v1_groupvals, &val);
+ while (val) {
+ /* this makes a copy of the berval */
+ bv = slapi_value_get_berval(val);
+ if (bv && bv->bv_len) {
+ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "merge_ancestors: add %s (from V1)\n",
+ bv->bv_val);
+ }
+ hint = slapi_valueset_next_value(v1_groupvals, hint, &val);
+ }
+ }
+#endif
hint = slapi_valueset_first_value(v1_groupvals, &sval);
while (sval) {
if (memberof_compare(config, member_ndn_val, &sval)) {
@@ -3319,7 +3348,7 @@ memberof_get_groups_r(MemberOfConfig *config, Slapi_DN *member_sdn, memberof_get
merge_ancestors(&member_ndn_val, &member_data, data);
if (!cached && member_data.use_cache)
- cache_ancestors(config, &member_ndn_val, data);
+ cache_ancestors(config, &member_ndn_val, &member_data);
slapi_value_free(&member_ndn_val);
slapi_valueset_free(groupvals);
@@ -4285,6 +4314,25 @@ memberof_fix_memberof_callback(Slapi_Entry *e, void *callback_data)
/* get a list of all of the groups this user belongs to */
groups = memberof_get_groups(config, sdn);
+#if MEMBEROF_CACHE_DEBUG
+ {
+ Slapi_Value *val = 0;
+ int hint = 0;
+ struct berval *bv;
+ hint = slapi_valueset_first_value(groups, &val);
+ while (val) {
+ /* this makes a copy of the berval */
+ bv = slapi_value_get_berval(val);
+ if (bv && bv->bv_len) {
+ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "memberof_fix_memberof_callback: %s belongs to %s\n",
+ ndn,
+ bv->bv_val);
+ }
+ hint = slapi_valueset_next_value(groups, hint, &val);
+ }
+ }
+#endif
if (config->group_filter) {
if (slapi_filter_test_simple(e, config->group_filter)) {
--
2.49.0

View File

@ -1,192 +0,0 @@
From ff364a4b1c88e1a8f678e056af88cce50cd8717c Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Fri, 28 Mar 2025 17:32:14 +0100
Subject: [PATCH] Issue 6698 - NPE after configuring invalid filtered role
(#6699)
Server crash when doing search after configuring filtered role with invalid filter.
Reason: The part of the filter that should be overwritten are freed before knowing that the filter is invalid.
Solution: Check first that the filter is valid before freeing the filtere bits
Issue: #6698
Reviewed by: @tbordaz , @mreynolds389 (Thanks!)
(cherry picked from commit 31e120d2349eda7a41380cf78fc04cf41e394359)
---
dirsrvtests/tests/suites/roles/basic_test.py | 80 ++++++++++++++++++--
ldap/servers/slapd/filter.c | 17 ++++-
2 files changed, 88 insertions(+), 9 deletions(-)
diff --git a/dirsrvtests/tests/suites/roles/basic_test.py b/dirsrvtests/tests/suites/roles/basic_test.py
index 875ac47c1..b79816c58 100644
--- a/dirsrvtests/tests/suites/roles/basic_test.py
+++ b/dirsrvtests/tests/suites/roles/basic_test.py
@@ -28,6 +28,7 @@ from lib389.dbgen import dbgen_users
from lib389.tasks import ImportTask
from lib389.utils import get_default_db_lib
from lib389.rewriters import *
+from lib389._mapped_object import DSLdapObject
from lib389.backend import Backends
logging.getLogger(__name__).setLevel(logging.INFO)
@@ -427,7 +428,6 @@ def test_vattr_on_filtered_role_restart(topo, request):
log.info("Check the default value of attribute nsslapd-ignore-virtual-attrs should be OFF")
assert topo.standalone.config.present('nsslapd-ignore-virtual-attrs', 'off')
-
log.info("Check the virtual attribute definition is found (after a required delay)")
topo.standalone.restart()
time.sleep(5)
@@ -541,7 +541,7 @@ def test_managed_and_filtered_role_rewrite(topo, request):
indexes = backend.get_indexes()
try:
index = indexes.create(properties={
- 'cn': attrname,
+ 'cn': attrname,
'nsSystemIndex': 'false',
'nsIndexType': ['eq', 'pres']
})
@@ -593,7 +593,6 @@ def test_managed_and_filtered_role_rewrite(topo, request):
dn = "uid=%s0000%d,%s" % (RDN, i, PARENT)
topo.standalone.modify_s(dn, [(ldap.MOD_REPLACE, 'nsRoleDN', [role.dn.encode()])])
-
# Now check that search is fast, evaluating only 4 entries
search_start = time.time()
entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn)
@@ -676,7 +675,7 @@ def test_not_such_entry_role_rewrite(topo, request):
indexes = backend.get_indexes()
try:
index = indexes.create(properties={
- 'cn': attrname,
+ 'cn': attrname,
'nsSystemIndex': 'false',
'nsIndexType': ['eq', 'pres']
})
@@ -730,7 +729,7 @@ def test_not_such_entry_role_rewrite(topo, request):
# Enable plugin level to check message
topo.standalone.config.loglevel(vals=(ErrorLog.DEFAULT,ErrorLog.PLUGIN))
-
+
# Now check that search is fast, evaluating only 4 entries
search_start = time.time()
entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(|(nsrole=%s)(nsrole=cn=not_such_entry_role,%s))" % (role.dn, DEFAULT_SUFFIX))
@@ -758,6 +757,77 @@ def test_not_such_entry_role_rewrite(topo, request):
request.addfinalizer(fin)
+
+def test_rewriter_with_invalid_filter(topo, request):
+ """Test that server does not crash when having
+ invalid filter in filtered role
+
+ :id: 5013b0b2-0af6-11f0-8684-482ae39447e5
+ :setup: standalone server
+ :steps:
+ 1. Setup filtered role with good filter
+ 2. Setup nsrole rewriter
+ 3. Restart the server
+ 4. Search for entries
+ 5. Setup filtered role with bad filter
+ 6. Search for entries
+ :expectedresults:
+ 1. Operation should succeed
+ 2. Operation should succeed
+ 3. Operation should succeed
+ 4. Operation should succeed
+ 5. Operation should succeed
+ 6. Operation should succeed
+ """
+ inst = topo.standalone
+ entries = []
+
+ def fin():
+ inst.start()
+ for entry in entries:
+ entry.delete()
+ request.addfinalizer(fin)
+
+ # Setup filtered role
+ roles = FilteredRoles(inst, f'ou=people,{DEFAULT_SUFFIX}')
+ filter_ko = '(&((objectClass=top)(objectClass=nsPerson))'
+ filter_ok = '(&(objectClass=top)(objectClass=nsPerson))'
+ role_properties = {
+ 'cn': 'TestFilteredRole',
+ 'nsRoleFilter': filter_ok,
+ 'description': 'Test good filter',
+ }
+ role = roles.create(properties=role_properties)
+ entries.append(role)
+
+ # Setup nsrole rewriter
+ rewriters = Rewriters(inst)
+ rewriter_properties = {
+ "cn": "nsrole",
+ "nsslapd-libpath": 'libroles-plugin',
+ "nsslapd-filterrewriter": 'role_nsRole_filter_rewriter',
+ }
+ rewriter = rewriters.ensure_state(properties=rewriter_properties)
+ entries.append(rewriter)
+
+ # Restart thge instance
+ inst.restart()
+
+ # Search for entries
+ entries = inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn)
+
+ # Set bad filter
+ role_properties = {
+ 'cn': 'TestFilteredRole',
+ 'nsRoleFilter': filter_ko,
+ 'description': 'Test bad filter',
+ }
+ role.ensure_state(properties=role_properties)
+
+ # Search for entries
+ entries = inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn)
+
+
if __name__ == "__main__":
CURRENT_FILE = os.path.realpath(__file__)
pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/ldap/servers/slapd/filter.c b/ldap/servers/slapd/filter.c
index ce09891b8..f541b8fc1 100644
--- a/ldap/servers/slapd/filter.c
+++ b/ldap/servers/slapd/filter.c
@@ -1038,9 +1038,11 @@ slapi_filter_get_subfilt(
}
/*
- * Before calling this function, you must free all the parts
+ * The function does not know how to free all the parts
* which will be overwritten (i.e. slapi_free_the_filter_bits),
- * this function dosn't know how to do that
+ * so the caller must take care of that.
+ * But it must do so AFTER calling slapi_filter_replace_ex to
+ * avoid getting invalid filter if slapi_filter_replace_ex fails.
*/
int
slapi_filter_replace_ex(Slapi_Filter *f, char *s)
@@ -1099,8 +1101,15 @@ slapi_filter_free_bits(Slapi_Filter *f)
int
slapi_filter_replace_strfilter(Slapi_Filter *f, char *strfilter)
{
- slapi_filter_free_bits(f);
- return (slapi_filter_replace_ex(f, strfilter));
+ /* slapi_filter_replace_ex may fail and we cannot
+ * free filter bits before calling it.
+ */
+ Slapi_Filter save_f = *f;
+ int ret = slapi_filter_replace_ex(f, strfilter);
+ if (ret == 0) {
+ slapi_filter_free_bits(&save_f);
+ }
+ return ret;
}
static void
--
2.49.0

View File

@ -1,455 +0,0 @@
From 446a23d0ed2d3ffa76c5fb5e9576d6876bdbf04f Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Fri, 28 Mar 2025 11:28:54 -0700
Subject: [PATCH] Issue 6686 - CLI - Re-enabling user accounts that reached
inactivity limit fails with error (#6687)
Description: When attempting to unlock a user account that has been locked due
to exceeding the Account Policy Plugin's inactivity limit, the dsidm account
unlock command fails with a Python type error: "float() argument must be a
string or a number, not 'NoneType'".
Enhance the unlock method to properly handle different account locking states,
including inactivity limit exceeded states.
Add test cases to verify account inactivity locking/unlocking functionality
with CoS and role-based indirect locking.
Fix CoS template class to include the required 'ldapsubentry' objectClass.
Improv error messages to provide better guidance on unlocking indirectly
locked accounts.
Fixes: https://github.com/389ds/389-ds-base/issues/6686
Reviewed by: @mreynolds389 (Thanks!)
---
.../clu/dsidm_account_inactivity_test.py | 329 ++++++++++++++++++
src/lib389/lib389/cli_idm/account.py | 25 +-
src/lib389/lib389/idm/account.py | 28 +-
3 files changed, 377 insertions(+), 5 deletions(-)
create mode 100644 dirsrvtests/tests/suites/clu/dsidm_account_inactivity_test.py
diff --git a/dirsrvtests/tests/suites/clu/dsidm_account_inactivity_test.py b/dirsrvtests/tests/suites/clu/dsidm_account_inactivity_test.py
new file mode 100644
index 000000000..88a34abf6
--- /dev/null
+++ b/dirsrvtests/tests/suites/clu/dsidm_account_inactivity_test.py
@@ -0,0 +1,329 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2025 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import ldap
+import time
+import pytest
+import logging
+import os
+from datetime import datetime, timedelta
+
+from lib389 import DEFAULT_SUFFIX, DN_PLUGIN, DN_CONFIG
+from lib389.cli_idm.account import entry_status, unlock
+from lib389.topologies import topology_st
+from lib389.cli_base import FakeArgs
+from lib389.utils import ds_is_older
+from lib389.plugins import AccountPolicyPlugin, AccountPolicyConfigs
+from lib389.idm.role import FilteredRoles
+from lib389.idm.user import UserAccounts
+from lib389.cos import CosTemplate, CosPointerDefinition
+from lib389.idm.domain import Domain
+from . import check_value_in_log_and_reset
+
+pytestmark = pytest.mark.tier0
+
+logging.getLogger(__name__).setLevel(logging.DEBUG)
+log = logging.getLogger(__name__)
+
+# Constants
+PLUGIN_ACCT_POLICY = "Account Policy Plugin"
+ACCP_DN = f"cn={PLUGIN_ACCT_POLICY},{DN_PLUGIN}"
+ACCP_CONF = f"{DN_CONFIG},{ACCP_DN}"
+POLICY_NAME = "Account Inactivity Policy"
+POLICY_DN = f"cn={POLICY_NAME},{DEFAULT_SUFFIX}"
+COS_TEMPLATE_NAME = "TemplateCoS"
+COS_TEMPLATE_DN = f"cn={COS_TEMPLATE_NAME},{DEFAULT_SUFFIX}"
+COS_DEFINITION_NAME = "DefinitionCoS"
+COS_DEFINITION_DN = f"cn={COS_DEFINITION_NAME},{DEFAULT_SUFFIX}"
+TEST_USER_NAME = "test_inactive_user"
+TEST_USER_DN = f"uid={TEST_USER_NAME},{DEFAULT_SUFFIX}"
+TEST_USER_PW = "password"
+INACTIVITY_LIMIT = 30
+
+
+@pytest.fixture(scope="function")
+def account_policy_setup(topology_st, request):
+ """Set up account policy plugin, configuration, and CoS objects"""
+ log.info("Setting up Account Policy Plugin and CoS")
+
+ # Enable Account Policy Plugin
+ plugin = AccountPolicyPlugin(topology_st.standalone)
+ if not plugin.status():
+ plugin.enable()
+ plugin.set('nsslapd-pluginarg0', ACCP_CONF)
+
+ # Configure Account Policy
+ accp_configs = AccountPolicyConfigs(topology_st.standalone)
+ accp_config = accp_configs.ensure_state(
+ properties={
+ 'cn': 'config',
+ 'alwaysrecordlogin': 'yes',
+ 'stateattrname': 'lastLoginTime',
+ 'altstateattrname': '1.1',
+ 'specattrname': 'acctPolicySubentry',
+ 'limitattrname': 'accountInactivityLimit'
+ }
+ )
+
+ # Add ACI for anonymous access if it doesn't exist
+ domain = Domain(topology_st.standalone, DEFAULT_SUFFIX)
+ anon_aci = '(targetattr="*")(version 3.0; acl "Anonymous read access"; allow (read,search,compare) userdn="ldap:///anyone";)'
+ domain.ensure_present('aci', anon_aci)
+
+ # Restart the server to apply plugin configuration
+ topology_st.standalone.restart()
+
+ # Create or update account policy entry
+ accp_configs = AccountPolicyConfigs(topology_st.standalone, basedn=DEFAULT_SUFFIX)
+ policy = accp_configs.ensure_state(
+ properties={
+ 'cn': POLICY_NAME,
+ 'objectClass': ['top', 'ldapsubentry', 'extensibleObject', 'accountpolicy'],
+ 'accountInactivityLimit': str(INACTIVITY_LIMIT)
+ }
+ )
+
+ # Create or update CoS template entry
+ cos_template = CosTemplate(topology_st.standalone, dn=COS_TEMPLATE_DN)
+ cos_template.ensure_state(
+ properties={
+ 'cn': COS_TEMPLATE_NAME,
+ 'objectClass': ['top', 'cosTemplate', 'extensibleObject'],
+ 'acctPolicySubentry': policy.dn
+ }
+ )
+
+ # Create or update CoS definition entry
+ cos_def = CosPointerDefinition(topology_st.standalone, dn=COS_DEFINITION_DN)
+ cos_def.ensure_state(
+ properties={
+ 'cn': COS_DEFINITION_NAME,
+ 'objectClass': ['top', 'ldapsubentry', 'cosSuperDefinition', 'cosPointerDefinition'],
+ 'cosTemplateDn': COS_TEMPLATE_DN,
+ 'cosAttribute': 'acctPolicySubentry default operational-default'
+ }
+ )
+
+ # Restart server to ensure CoS is applied
+ topology_st.standalone.restart()
+
+ def fin():
+ log.info('Cleaning up Account Policy settings')
+ try:
+ # Delete CoS and policy entries
+ if cos_def.exists():
+ cos_def.delete()
+ if cos_template.exists():
+ cos_template.delete()
+ if policy.exists():
+ policy.delete()
+
+ # Disable the plugin
+ if plugin.status():
+ plugin.disable()
+ topology_st.standalone.restart()
+ except Exception as e:
+ log.error(f'Failed to clean up: {e}')
+
+ request.addfinalizer(fin)
+
+ return topology_st.standalone
+
+
+@pytest.fixture(scope="function")
+def create_test_user(topology_st, account_policy_setup, request):
+ """Create a test user for the inactivity test"""
+ log.info('Creating test user')
+
+ users = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX)
+ user = users.ensure_state(
+ properties={
+ 'uid': TEST_USER_NAME,
+ 'cn': TEST_USER_NAME,
+ 'sn': TEST_USER_NAME,
+ 'userPassword': TEST_USER_PW,
+ 'uidNumber': '1000',
+ 'gidNumber': '2000',
+ 'homeDirectory': f'/home/{TEST_USER_NAME}'
+ }
+ )
+
+ def fin():
+ log.info('Deleting test user')
+ if user.exists():
+ user.delete()
+
+ request.addfinalizer(fin)
+ return user
+
+
+@pytest.mark.skipif(ds_is_older("1.4.2"), reason="Indirect account locking not implemented")
+def test_dsidm_account_inactivity_lock_unlock(topology_st, create_test_user):
+ """Test dsidm account unlock functionality with indirectly locked accounts
+
+ :id: d7b57083-6111-4dbf-af84-6fca7fc7fb31
+ :setup: Standalone instance with Account Policy Plugin and CoS configured
+ :steps:
+ 1. Create a test user
+ 2. Bind as the test user to set lastLoginTime
+ 3. Check account status - should be active
+ 4. Set user's lastLoginTime to a time in the past that exceeds inactivity limit
+ 5. Check account status - should be locked due to inactivity
+ 6. Attempt to bind as the user - should fail with constraint violation
+ 7. Unlock the account using dsidm account unlock
+ 8. Verify account status is active again
+ 9. Verify the user can bind again
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Account status shows as activated
+ 4. Success
+ 5. Account status shows as inactivity limit exceeded
+ 6. Bind attempt fails with constraint violation
+ 7. Account unlocked successfully
+ 8. Account status shows as activated
+ 9. User can bind successfully
+ """
+ standalone = topology_st.standalone
+ user = create_test_user
+
+ # Set up FakeArgs for dsidm commands
+ args = FakeArgs()
+ args.dn = user.dn
+ args.json = False
+ args.details = False
+
+ # 1. Check initial account status - should be active
+ log.info('Step 1: Checking initial account status')
+ entry_status(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args)
+ check_value_in_log_and_reset(topology_st, check_value='Entry State: activated')
+
+ # 2. Bind as test user to set initial lastLoginTime
+ log.info('Step 2: Binding as test user to set lastLoginTime')
+ try:
+ conn = user.bind(TEST_USER_PW)
+ conn.unbind()
+ log.info("Successfully bound as test user")
+ except ldap.LDAPError as e:
+ pytest.fail(f"Failed to bind as test user: {e}")
+
+ # 3. Set lastLoginTime to a time in the past that exceeds inactivity limit
+ log.info('Step 3: Setting lastLoginTime to the past')
+ past_time = datetime.utcnow() - timedelta(seconds=INACTIVITY_LIMIT * 2)
+ past_time_str = past_time.strftime('%Y%m%d%H%M%SZ')
+ user.replace('lastLoginTime', past_time_str)
+
+ # 4. Check account status - should now be locked due to inactivity
+ log.info('Step 4: Checking account status after setting old lastLoginTime')
+ entry_status(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args)
+ check_value_in_log_and_reset(topology_st, check_value='Entry State: inactivity limit exceeded')
+
+ # 5. Attempt to bind as the user - should fail
+ log.info('Step 5: Attempting to bind as user (should fail)')
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION) as excinfo:
+ conn = user.bind(TEST_USER_PW)
+ assert "Account inactivity limit exceeded" in str(excinfo.value)
+
+ # 6. Unlock the account using dsidm account unlock
+ log.info('Step 6: Unlocking the account with dsidm')
+ unlock(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args)
+ check_value_in_log_and_reset(topology_st,
+ check_value='now unlocked by resetting lastLoginTime')
+
+ # 7. Verify account status is active again
+ log.info('Step 7: Checking account status after unlock')
+ entry_status(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args)
+ check_value_in_log_and_reset(topology_st, check_value='Entry State: activated')
+
+ # 8. Verify the user can bind again
+ log.info('Step 8: Verifying user can bind again')
+ try:
+ conn = user.bind(TEST_USER_PW)
+ conn.unbind()
+ log.info("Successfully bound as test user after unlock")
+ except ldap.LDAPError as e:
+ pytest.fail(f"Failed to bind as test user after unlock: {e}")
+
+
+@pytest.mark.skipif(ds_is_older("1.4.2"), reason="Indirect account locking not implemented")
+def test_dsidm_indirectly_locked_via_role(topology_st, create_test_user):
+ """Test dsidm account unlock functionality with accounts indirectly locked via role
+
+ :id: 7bfe69bb-cf99-4214-a763-051ab2b9cf89
+ :setup: Standalone instance with Role and user configured
+ :steps:
+ 1. Create a test user
+ 2. Create a Filtered Role that includes the test user
+ 3. Lock the role
+ 4. Check account status - should be indirectly locked through the role
+ 5. Attempt to unlock the account - should fail with appropriate message
+ 6. Unlock the role
+ 7. Verify account status is active again
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Account status shows as indirectly locked
+ 5. Unlock attempt fails with appropriate error message
+ 6. Success
+ 7. Account status shows as activated
+ """
+ standalone = topology_st.standalone
+ user = create_test_user
+
+ # Use FilteredRoles and ensure_state for role creation
+ log.info('Step 1: Creating Filtered Role')
+ roles = FilteredRoles(standalone, DEFAULT_SUFFIX)
+ role = roles.ensure_state(
+ properties={
+ 'cn': 'TestFilterRole',
+ 'nsRoleFilter': f'(uid={TEST_USER_NAME})'
+ }
+ )
+
+ # Set up FakeArgs for dsidm commands
+ args = FakeArgs()
+ args.dn = user.dn
+ args.json = False
+ args.details = False
+
+ # 2. Check account status before locking role
+ log.info('Step 2: Checking account status before locking role')
+ entry_status(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args)
+ check_value_in_log_and_reset(topology_st, check_value='Entry State: activated')
+
+ # 3. Lock the role
+ log.info('Step 3: Locking the role')
+ role.lock()
+
+ # 4. Check account status - should be indirectly locked
+ log.info('Step 4: Checking account status after locking role')
+ entry_status(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args)
+ check_value_in_log_and_reset(topology_st, check_value='Entry State: indirectly locked through a Role')
+
+ # 5. Attempt to unlock the account - should fail
+ log.info('Step 5: Attempting to unlock indirectly locked account')
+ unlock(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args)
+ check_value_in_log_and_reset(topology_st,
+ check_value='Account is locked through role')
+
+ # 6. Unlock the role
+ log.info('Step 6: Unlocking the role')
+ role.unlock()
+
+ # 7. Verify account status is active again
+ log.info('Step 7: Checking account status after unlocking role')
+ entry_status(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args)
+ check_value_in_log_and_reset(topology_st, check_value='Entry State: activated')
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main(["-s", CURRENT_FILE])
\ No newline at end of file
diff --git a/src/lib389/lib389/cli_idm/account.py b/src/lib389/lib389/cli_idm/account.py
index 15f766588..a0dfd8f65 100644
--- a/src/lib389/lib389/cli_idm/account.py
+++ b/src/lib389/lib389/cli_idm/account.py
@@ -176,8 +176,29 @@ def unlock(inst, basedn, log, args):
dn = _get_dn_arg(args.dn, msg="Enter dn to unlock")
accounts = Accounts(inst, basedn)
acct = accounts.get(dn=dn)
- acct.unlock()
- log.info(f'Entry {dn} is unlocked')
+
+ try:
+ # Get the account status before attempting to unlock
+ status = acct.status()
+ state = status["state"]
+
+ # Attempt to unlock the account
+ acct.unlock()
+
+ # Success message
+ log.info(f'Entry {dn} is unlocked')
+ if state == AccountState.DIRECTLY_LOCKED:
+ log.info(f'The entry was directly locked')
+ elif state == AccountState.INACTIVITY_LIMIT_EXCEEDED:
+ log.info(f'The entry was locked due to inactivity and is now unlocked by resetting lastLoginTime')
+
+ except ValueError as e:
+ # Provide a more detailed error message based on failure reason
+ if "through role" in str(e):
+ log.error(f"Cannot unlock {dn}: {str(e)}")
+ log.info("To unlock this account, you must modify the role that's locking it.")
+ else:
+ log.error(f"Failed to unlock {dn}: {str(e)}")
def reset_password(inst, basedn, log, args):
diff --git a/src/lib389/lib389/idm/account.py b/src/lib389/lib389/idm/account.py
index 4b823b662..faf6f6f16 100644
--- a/src/lib389/lib389/idm/account.py
+++ b/src/lib389/lib389/idm/account.py
@@ -140,7 +140,8 @@ class Account(DSLdapObject):
"nsAccountLock", state_attr])
last_login_time = self._dict_get_with_ignore_indexerror(account_data, state_attr)
- if not last_login_time:
+ # if last_login_time not exist then check alt_state_attr only if its not disabled and exist
+ if not last_login_time and alt_state_attr in account_data:
last_login_time = self._dict_get_with_ignore_indexerror(account_data, alt_state_attr)
create_time = self._dict_get_with_ignore_indexerror(account_data, "createTimestamp")
@@ -203,12 +204,33 @@ class Account(DSLdapObject):
self.replace('nsAccountLock', 'true')
def unlock(self):
- """Unset nsAccountLock"""
+ """Unset nsAccountLock if it's set and reset lastLoginTime if account is locked due to inactivity"""
current_status = self.status()
+
if current_status["state"] == AccountState.ACTIVATED:
raise ValueError("Account is already active")
- self.remove('nsAccountLock', None)
+
+ if current_status["state"] == AccountState.DIRECTLY_LOCKED:
+ # Account is directly locked with nsAccountLock attribute
+ self.remove('nsAccountLock', None)
+ elif current_status["state"] == AccountState.INACTIVITY_LIMIT_EXCEEDED:
+ # Account is locked due to inactivity - reset lastLoginTime to current time
+ # The lastLoginTime attribute stores its value in GMT/UTC time (Zulu time zone)
+ current_time = time.strftime('%Y%m%d%H%M%SZ', time.gmtime())
+ self.replace('lastLoginTime', current_time)
+ elif current_status["state"] == AccountState.INDIRECTLY_LOCKED:
+ # Account is locked through a role
+ role_dn = current_status.get("role_dn")
+ if role_dn:
+ raise ValueError(f"Account is locked through role {role_dn}. "
+ f"Please modify the role to unlock this account.")
+ else:
+ raise ValueError("Account is locked through an unknown role. "
+ "Please check the roles configuration to unlock this account.")
+ else:
+ # Should not happen, but just in case
+ raise ValueError(f"Unknown lock state: {current_status['state'].value}")
# If the account can be bound to, this will attempt to do so. We don't check
# for exceptions, just pass them back!
--
2.49.0

View File

@ -1,70 +0,0 @@
From 09a284ee43c2b4346da892f8756f97accd15ca68 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Wed, 4 Dec 2024 21:59:40 -0500
Subject: [PATCH] Issue 6302 - Allow to run replication status without a prompt
(#6410)
Description: We should allow running replication status and
other similar commands without requesting a password and bind DN.
This way, the current instance's root DN and root PW will be used on other
instances when requesting CSN info. If they are incorrect,
then the info won't be printed, but otherwise, the agreement status
will be displayed correctly.
Fixes: https://github.com/389ds/389-ds-base/issues/6302
Reviewed by: @progier389 (Thanks!)
---
src/lib389/lib389/cli_conf/replication.py | 15 +++------------
1 file changed, 3 insertions(+), 12 deletions(-)
diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py
index 399d0d2f8..cd4a331a8 100644
--- a/src/lib389/lib389/cli_conf/replication.py
+++ b/src/lib389/lib389/cli_conf/replication.py
@@ -319,12 +319,9 @@ def list_suffixes(inst, basedn, log, args):
def get_repl_status(inst, basedn, log, args):
replicas = Replicas(inst)
replica = replicas.get(args.suffix)
- pw_and_dn_prompt = False
if args.bind_passwd_file is not None:
args.bind_passwd = get_passwd_from_file(args.bind_passwd_file)
- if args.bind_passwd_prompt or args.bind_dn is None or args.bind_passwd is None:
- pw_and_dn_prompt = True
- status = replica.status(binddn=args.bind_dn, bindpw=args.bind_passwd, pwprompt=pw_and_dn_prompt)
+ status = replica.status(binddn=args.bind_dn, bindpw=args.bind_passwd, pwprompt=args.bind_passwd_prompt)
if args.json:
log.info(json.dumps({"type": "list", "items": status}, indent=4))
else:
@@ -335,12 +332,9 @@ def get_repl_status(inst, basedn, log, args):
def get_repl_winsync_status(inst, basedn, log, args):
replicas = Replicas(inst)
replica = replicas.get(args.suffix)
- pw_and_dn_prompt = False
if args.bind_passwd_file is not None:
args.bind_passwd = get_passwd_from_file(args.bind_passwd_file)
- if args.bind_passwd_prompt or args.bind_dn is None or args.bind_passwd is None:
- pw_and_dn_prompt = True
- status = replica.status(binddn=args.bind_dn, bindpw=args.bind_passwd, winsync=True, pwprompt=pw_and_dn_prompt)
+ status = replica.status(binddn=args.bind_dn, bindpw=args.bind_passwd, winsync=True, pwprompt=args.bind_passwd_prompt)
if args.json:
log.info(json.dumps({"type": "list", "items": status}, indent=4))
else:
@@ -874,12 +868,9 @@ def poke_agmt(inst, basedn, log, args):
def get_agmt_status(inst, basedn, log, args):
agmt = get_agmt(inst, args)
- pw_and_dn_prompt = False
if args.bind_passwd_file is not None:
args.bind_passwd = get_passwd_from_file(args.bind_passwd_file)
- if args.bind_passwd_prompt or args.bind_dn is None or args.bind_passwd is None:
- pw_and_dn_prompt = True
- status = agmt.status(use_json=args.json, binddn=args.bind_dn, bindpw=args.bind_passwd, pwprompt=pw_and_dn_prompt)
+ status = agmt.status(use_json=args.json, binddn=args.bind_dn, bindpw=args.bind_passwd, pwprompt=args.bind_passwd_prompt)
log.info(status)
--
2.49.0

View File

@ -1,45 +0,0 @@
From 2b73c3596e724f314b0e09cf6209e0151260f7e5 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy@redhat.com>
Date: Wed, 9 Jul 2025 12:08:09 +0300
Subject: [PATCH] Issue 6857 - uiduniq: allow specifying match rules in the
filter
Allow uniqueness plugin to work with attributes where uniqueness should
be enforced using different matching rule than the one defined for the
attribute itself.
Since uniqueness plugin configuration can contain multiple attributes,
add matching rule right to the attribute as it is used in the LDAP rule
(e.g. 'attribute:caseIgnoreMatch:' to force 'attribute' to be searched
with case-insensitive matching rule instead of the original matching
rule.
Fixes: https://github.com/389ds/389-ds-base/issues/6857
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
---
ldap/servers/plugins/uiduniq/uid.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/ldap/servers/plugins/uiduniq/uid.c b/ldap/servers/plugins/uiduniq/uid.c
index 5b763b551..15cf88477 100644
--- a/ldap/servers/plugins/uiduniq/uid.c
+++ b/ldap/servers/plugins/uiduniq/uid.c
@@ -1031,7 +1031,14 @@ preop_add(Slapi_PBlock *pb)
}
for (i = 0; attrNames && attrNames[i]; i++) {
+ char *attr_match = strchr(attrNames[i], ':');
+ if (attr_match != NULL) {
+ attr_match[0] = '\0';
+ }
err = slapi_entry_attr_find(e, attrNames[i], &attr);
+ if (attr_match != NULL) {
+ attr_match[0] = ':';
+ }
if (!err) {
/*
* Passed all the requirements - this is an operation we
--
2.49.0

View File

@ -1,399 +0,0 @@
From 3ba73d2aa55f18ff73d4b3901ce101133745effc Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Wed, 9 Jul 2025 14:18:50 -0400
Subject: [PATCH] Issue 6859 - str2filter is not fully applying matching rules
Description:
When we have an extended filter, one with a MR applied, it is ignored during
internal searches:
"(cn:CaseExactMatch:=Value)"
For internal searches we use str2filter() and it doesn't fully apply extended
search filter matching rules
Also needed to update attr uniqueness plugin to apply this change for mod
operations (previously only Adds were correctly handling these attribute
filters)
Relates: https://github.com/389ds/389-ds-base/issues/6857
Relates: https://github.com/389ds/389-ds-base/issues/6859
Reviewed by: spichugi & tbordaz(Thanks!!)
---
.../tests/suites/plugins/attruniq_test.py | 295 +++++++++++++++++-
ldap/servers/plugins/uiduniq/uid.c | 7 +
ldap/servers/slapd/plugin_mr.c | 2 +-
ldap/servers/slapd/str2filter.c | 8 +
4 files changed, 309 insertions(+), 3 deletions(-)
diff --git a/dirsrvtests/tests/suites/plugins/attruniq_test.py b/dirsrvtests/tests/suites/plugins/attruniq_test.py
index b190e0ec1..b338f405f 100644
--- a/dirsrvtests/tests/suites/plugins/attruniq_test.py
+++ b/dirsrvtests/tests/suites/plugins/attruniq_test.py
@@ -1,5 +1,5 @@
# --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2021 Red Hat, Inc.
+# Copyright (C) 2025 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
@@ -80,4 +80,295 @@ def test_modrdn_attr_uniqueness(topology_st):
log.debug(excinfo.value)
log.debug('Move user2 to group1')
- user2.rename(f'uid={user2.rdn}', group1.dn)
\ No newline at end of file
+
+ user2.rename(f'uid={user2.rdn}', group1.dn)
+
+ # Cleanup for next test
+ user1.delete()
+ user2.delete()
+ attruniq.disable()
+ attruniq.delete()
+
+
+def test_multiple_attr_uniqueness(topology_st):
+ """ Test that attribute uniqueness works properly with multiple attributes
+
+ :id: c49aa5c1-7e65-45fd-b064-55e0b815e9bc
+ :setup: Standalone instance
+ :steps:
+ 1. Setup attribute uniqueness plugin to ensure uniqueness of attributes 'mail' and 'mailAlternateAddress'
+ 2. Add user with unique 'mail=non-uniq@value.net' and 'mailAlternateAddress=alt-mail@value.net'
+ 3. Try adding another user with 'mail=non-uniq@value.net'
+ 4. Try adding another user with 'mailAlternateAddress=alt-mail@value.net'
+ 5. Try adding another user with 'mail=alt-mail@value.net'
+ 6. Try adding another user with 'mailAlternateAddress=non-uniq@value.net'
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Should raise CONSTRAINT_VIOLATION
+ 4. Should raise CONSTRAINT_VIOLATION
+ 5. Should raise CONSTRAINT_VIOLATION
+ 6. Should raise CONSTRAINT_VIOLATION
+ """
+ attruniq = AttributeUniquenessPlugin(topology_st.standalone, dn="cn=attruniq,cn=plugins,cn=config")
+
+ try:
+ log.debug(f'Setup PLUGIN_ATTR_UNIQUENESS plugin for {MAIL_ATTR_VALUE} attribute for the group2')
+ attruniq.create(properties={'cn': 'attruniq'})
+ attruniq.add_unique_attribute('mail')
+ attruniq.add_unique_attribute('mailAlternateAddress')
+ attruniq.add_unique_subtree(DEFAULT_SUFFIX)
+ attruniq.enable_all_subtrees()
+ log.debug(f'Enable PLUGIN_ATTR_UNIQUENESS plugin as "ON"')
+ attruniq.enable()
+ except ldap.LDAPError as e:
+ log.fatal('test_multiple_attribute_uniqueness: Failed to configure plugin for "mail": error {}'.format(e.args[0]['desc']))
+ assert False
+
+ topology_st.standalone.restart()
+
+ users = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX)
+
+ testuser1 = users.create_test_user(100,100)
+ testuser1.add('objectclass', 'extensibleObject')
+ testuser1.add('mail', MAIL_ATTR_VALUE)
+ testuser1.add('mailAlternateAddress', MAIL_ATTR_VALUE_ALT)
+
+ testuser2 = users.create_test_user(200, 200)
+ testuser2.add('objectclass', 'extensibleObject')
+
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ testuser2.add('mail', MAIL_ATTR_VALUE)
+
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ testuser2.add('mailAlternateAddress', MAIL_ATTR_VALUE_ALT)
+
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ testuser2.add('mail', MAIL_ATTR_VALUE_ALT)
+
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ testuser2.add('mailAlternateAddress', MAIL_ATTR_VALUE)
+
+ # Cleanup
+ testuser1.delete()
+ testuser2.delete()
+ attruniq.disable()
+ attruniq.delete()
+
+
+def test_exclude_subtrees(topology_st):
+ """ Test attribute uniqueness with exclude scope
+
+ :id: 43d29a60-40e1-4ebd-b897-6ef9f20e9f27
+ :setup: Standalone instance
+ :steps:
+ 1. Setup and enable attribute uniqueness plugin for telephonenumber unique attribute
+ 2. Create subtrees and test users
+ 3. Add a unique attribute to a user within uniqueness scope
+ 4. Add exclude subtree
+ 5. Try to add existing value attribute to an entry within uniqueness scope
+ 6. Try to add existing value attribute to an entry within exclude scope
+ 7. Remove the attribute from affected entries
+ 8. Add a unique attribute to a user within exclude scope
+ 9. Try to add existing value attribute to an entry within uniqueness scope
+ 10. Try to add existing value attribute to another entry within uniqueness scope
+ 11. Remove the attribute from affected entries
+ 12. Add another exclude subtree
+ 13. Add a unique attribute to a user within uniqueness scope
+ 14. Try to add existing value attribute to an entry within uniqueness scope
+ 15. Try to add existing value attribute to an entry within exclude scope
+ 16. Try to add existing value attribute to an entry within another exclude scope
+ 17. Clean up entries
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. Should raise CONSTRAINT_VIOLATION
+ 6. Success
+ 7. Success
+ 8. Success
+ 9. Success
+ 10. Should raise CONSTRAINT_VIOLATION
+ 11. Success
+ 12. Success
+ 13. Success
+ 14. Should raise CONSTRAINT_VIOLATION
+ 15. Success
+ 16. Success
+ 17. Success
+ """
+ log.info('Setup attribute uniqueness plugin')
+ attruniq = AttributeUniquenessPlugin(topology_st.standalone, dn="cn=attruniq,cn=plugins,cn=config")
+ attruniq.create(properties={'cn': 'attruniq'})
+ attruniq.add_unique_attribute('telephonenumber')
+ attruniq.add_unique_subtree(DEFAULT_SUFFIX)
+ attruniq.enable_all_subtrees()
+ attruniq.enable()
+ topology_st.standalone.restart()
+
+ log.info('Create subtrees container')
+ containers = nsContainers(topology_st.standalone, DEFAULT_SUFFIX)
+ cont1 = containers.create(properties={'cn': EXCLUDED_CONTAINER_CN})
+ cont2 = containers.create(properties={'cn': EXCLUDED_BIS_CONTAINER_CN})
+ cont3 = containers.create(properties={'cn': ENFORCED_CONTAINER_CN})
+
+ log.info('Create test users')
+ users = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX,
+ rdn='cn={}'.format(ENFORCED_CONTAINER_CN))
+ users_excluded = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX,
+ rdn='cn={}'.format(EXCLUDED_CONTAINER_CN))
+ users_excluded2 = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX,
+ rdn='cn={}'.format(EXCLUDED_BIS_CONTAINER_CN))
+
+ user1 = users.create(properties={'cn': USER_1_CN,
+ 'uid': USER_1_CN,
+ 'sn': USER_1_CN,
+ 'uidNumber': '1',
+ 'gidNumber': '11',
+ 'homeDirectory': '/home/{}'.format(USER_1_CN)})
+ user2 = users.create(properties={'cn': USER_2_CN,
+ 'uid': USER_2_CN,
+ 'sn': USER_2_CN,
+ 'uidNumber': '2',
+ 'gidNumber': '22',
+ 'homeDirectory': '/home/{}'.format(USER_2_CN)})
+ user3 = users_excluded.create(properties={'cn': USER_3_CN,
+ 'uid': USER_3_CN,
+ 'sn': USER_3_CN,
+ 'uidNumber': '3',
+ 'gidNumber': '33',
+ 'homeDirectory': '/home/{}'.format(USER_3_CN)})
+ user4 = users_excluded2.create(properties={'cn': USER_4_CN,
+ 'uid': USER_4_CN,
+ 'sn': USER_4_CN,
+ 'uidNumber': '4',
+ 'gidNumber': '44',
+ 'homeDirectory': '/home/{}'.format(USER_4_CN)})
+
+ UNIQUE_VALUE = '1234'
+
+ try:
+ log.info('Create user with unique attribute')
+ user1.add('telephonenumber', UNIQUE_VALUE)
+ assert user1.present('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Add exclude subtree')
+ attruniq.add_exclude_subtree(EXCLUDED_CONTAINER_DN)
+ topology_st.standalone.restart()
+
+ log.info('Verify an already used attribute value cannot be added within the same subtree')
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ user2.add('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Verify an entry with same attribute value can be added within exclude subtree')
+ user3.add('telephonenumber', UNIQUE_VALUE)
+ assert user3.present('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Cleanup unique attribute values')
+ user1.remove_all('telephonenumber')
+ user3.remove_all('telephonenumber')
+
+ log.info('Add a unique value to an entry in excluded scope')
+ user3.add('telephonenumber', UNIQUE_VALUE)
+ assert user3.present('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Verify the same value can be added to an entry within uniqueness scope')
+ user1.add('telephonenumber', UNIQUE_VALUE)
+ assert user1.present('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Verify that yet another same value cannot be added to another entry within uniqueness scope')
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ user2.add('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Cleanup unique attribute values')
+ user1.remove_all('telephonenumber')
+ user3.remove_all('telephonenumber')
+
+ log.info('Add another exclude subtree')
+ attruniq.add_exclude_subtree(EXCLUDED_BIS_CONTAINER_DN)
+ topology_st.standalone.restart()
+
+ user1.add('telephonenumber', UNIQUE_VALUE)
+ log.info('Verify an already used attribute value cannot be added within the same subtree')
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ user2.add('telephonenumber', UNIQUE_VALUE)
+
+ log.info('Verify an already used attribute can be added to an entry in exclude scope')
+ user3.add('telephonenumber', UNIQUE_VALUE)
+ assert user3.present('telephonenumber', UNIQUE_VALUE)
+ user4.add('telephonenumber', UNIQUE_VALUE)
+ assert user4.present('telephonenumber', UNIQUE_VALUE)
+
+ finally:
+ log.info('Clean up users, containers and attribute uniqueness plugin')
+ user1.delete()
+ user2.delete()
+ user3.delete()
+ user4.delete()
+ cont1.delete()
+ cont2.delete()
+ cont3.delete()
+ attruniq.disable()
+ attruniq.delete()
+
+
+def test_matchingrule_attr(topology_st):
+ """ Test list extension MR attribute. Check for "cn" using CES (versus it
+ being defined as CIS)
+
+ :id: 5cde4342-6fa3-4225-b23d-0af918981075
+ :setup: Standalone instance
+ :steps:
+ 1. Setup and enable attribute uniqueness plugin to use CN attribute
+ with a matching rule of CaseExactMatch.
+ 2. Add user with CN value is lowercase
+ 3. Add second user with same lowercase CN which should be rejected
+ 4. Add second user with same CN value but with mixed case
+ 5. Modify second user replacing CN value to lc which should be rejected
+
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. Success
+ """
+
+ inst = topology_st.standalone
+
+ attruniq = AttributeUniquenessPlugin(inst,
+ dn="cn=attribute uniqueness,cn=plugins,cn=config")
+ attruniq.add_unique_attribute('cn:CaseExactMatch:')
+ attruniq.enable_all_subtrees()
+ attruniq.enable()
+ inst.restart()
+
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ users.create(properties={'cn': "common_name",
+ 'uid': "uid_name",
+ 'sn': "uid_name",
+ 'uidNumber': '1',
+ 'gidNumber': '11',
+ 'homeDirectory': '/home/uid_name'})
+
+ log.info('Add entry with the exact CN value which should be rejected')
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ users.create(properties={'cn': "common_name",
+ 'uid': "uid_name2",
+ 'sn': "uid_name2",
+ 'uidNumber': '11',
+ 'gidNumber': '111',
+ 'homeDirectory': '/home/uid_name2'})
+
+ log.info('Add entry with the mixed case CN value which should be allowed')
+ user = users.create(properties={'cn': "Common_Name",
+ 'uid': "uid_name2",
+ 'sn': "uid_name2",
+ 'uidNumber': '11',
+ 'gidNumber': '111',
+ 'homeDirectory': '/home/uid_name2'})
+
+ log.info('Mod entry with exact case CN value which should be rejected')
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ user.replace('cn', 'common_name')
diff --git a/ldap/servers/plugins/uiduniq/uid.c b/ldap/servers/plugins/uiduniq/uid.c
index 15cf88477..5a0d61b86 100644
--- a/ldap/servers/plugins/uiduniq/uid.c
+++ b/ldap/servers/plugins/uiduniq/uid.c
@@ -1179,6 +1179,10 @@ preop_modify(Slapi_PBlock *pb)
for (; mods && *mods; mods++) {
mod = *mods;
for (i = 0; attrNames && attrNames[i]; i++) {
+ char *attr_match = strchr(attrNames[i], ':');
+ if (attr_match != NULL) {
+ attr_match[0] = '\0';
+ }
if ((slapi_attr_type_cmp(mod->mod_type, attrNames[i], 1) == 0) && /* mod contains target attr */
(mod->mod_op & LDAP_MOD_BVALUES) && /* mod is bval encoded (not string val) */
(mod->mod_bvalues && mod->mod_bvalues[0]) && /* mod actually contains some values */
@@ -1187,6 +1191,9 @@ preop_modify(Slapi_PBlock *pb)
{
addMod(&checkmods, &checkmodsCapacity, &modcount, mod);
}
+ if (attr_match != NULL) {
+ attr_match[0] = ':';
+ }
}
}
if (modcount == 0) {
diff --git a/ldap/servers/slapd/plugin_mr.c b/ldap/servers/slapd/plugin_mr.c
index 6cf88b7de..f40c9a39b 100644
--- a/ldap/servers/slapd/plugin_mr.c
+++ b/ldap/servers/slapd/plugin_mr.c
@@ -624,7 +624,7 @@ attempt_mr_filter_create(mr_filter_t *f, struct slapdplugin *mrp, Slapi_PBlock *
int rc;
IFP mrf_create = NULL;
f->mrf_match = NULL;
- pblock_init(pb);
+ slapi_pblock_init(pb);
if (!(rc = slapi_pblock_set(pb, SLAPI_PLUGIN, mrp)) &&
!(rc = slapi_pblock_get(pb, SLAPI_PLUGIN_MR_FILTER_CREATE_FN, &mrf_create)) &&
mrf_create != NULL &&
diff --git a/ldap/servers/slapd/str2filter.c b/ldap/servers/slapd/str2filter.c
index 9fdc500f7..5620b7439 100644
--- a/ldap/servers/slapd/str2filter.c
+++ b/ldap/servers/slapd/str2filter.c
@@ -344,6 +344,14 @@ str2simple(char *str, int unescape_filter)
return NULL; /* error */
} else {
f->f_choice = LDAP_FILTER_EXTENDED;
+ if (f->f_mr_oid) {
+ /* apply the MR indexers */
+ rc = plugin_mr_filter_create(&f->f_mr);
+ if (rc) {
+ slapi_filter_free(f, 1);
+ return NULL; /* error */
+ }
+ }
}
} else if (str_find_star(value) == NULL) {
f->f_choice = LDAP_FILTER_EQUALITY;
--
2.49.0

View File

@ -1,55 +0,0 @@
From 77cc17e5dfb7ed71a320844d14a90c99c1474cc3 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Tue, 20 May 2025 08:13:24 -0400
Subject: [PATCH] Issue 6787 - Improve error message when bulk import
connection is closed
Description:
If an online replication initialization connection is closed a vague error
message is reported when the init is aborted:
factory_destructor - ERROR bulk import abandoned
It should be clear that the import is being abandoned because the connection
was closed and identify the conn id.
relates: https://github.com/389ds/389-ds-base/issues/6787
Reviewed by: progier(Thanks!)
(cherry picked from commit d472dd83d49f8dce6d71e202cbb4d897218ceffb)
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c
index 67d6e3abc..e433f3db2 100644
--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c
+++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import_threads.c
@@ -3432,9 +3432,10 @@ factory_constructor(void *object __attribute__((unused)), void *parent __attribu
}
void
-factory_destructor(void *extension, void *object __attribute__((unused)), void *parent __attribute__((unused)))
+factory_destructor(void *extension, void *object, void *parent __attribute__((unused)))
{
ImportJob *job = (ImportJob *)extension;
+ Connection *conn = (Connection *)object;
PRThread *thread;
if (extension == NULL)
@@ -3446,7 +3447,8 @@ factory_destructor(void *extension, void *object __attribute__((unused)), void *
*/
thread = job->main_thread;
slapi_log_err(SLAPI_LOG_ERR, "factory_destructor",
- "ERROR bulk import abandoned\n");
+ "ERROR bulk import abandoned: conn=%ld was closed\n",
+ conn->c_connid);
import_abort_all(job, 1);
/* wait for import_main to finish... */
PR_JoinThread(thread);
--
2.51.1

View File

@ -1,536 +0,0 @@
From 8cba0dd699541d562d74502f35176df33f188512 Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Fri, 30 May 2025 11:12:43 +0000
Subject: [PATCH] Issue 6641 - modrdn fails when a user is member of multiple
groups (#6643)
Bug description:
Rename of a user that is member of multiple AM groups fail when MO and
RI plugins are enabled.
Fix description:
MO plugin - After updating the entry member attribute, check the return
value. Retry the delete if the attr value exists and retry the add if the
attr value is missing.
RI plugin - A previous commit checked if the attr value was not present
before adding a mod. This commit was reverted in favour of overriding
the internal op return value, consistent with other plugins.
CI test from Viktor Ashirov <vashirov@redhat.com>
Fixes: https://github.com/389ds/389-ds-base/issues/6641
Relates: https://github.com/389ds/389-ds-base/issues/6566
Reviewed by: @progier389, @tbordaz, @vashirov (Thank you)
(cherry picked from commit 132ce4ab158679475cb83dbe28cc4fd7ced5cd19)
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
.../tests/suites/plugins/modrdn_test.py | 174 ++++++++++++++++++
ldap/servers/plugins/automember/automember.c | 11 +-
ldap/servers/plugins/memberof/memberof.c | 123 +++++--------
ldap/servers/plugins/referint/referint.c | 30 +--
ldap/servers/slapd/modify.c | 51 +++++
ldap/servers/slapd/slapi-plugin.h | 1 +
6 files changed, 301 insertions(+), 89 deletions(-)
create mode 100644 dirsrvtests/tests/suites/plugins/modrdn_test.py
diff --git a/dirsrvtests/tests/suites/plugins/modrdn_test.py b/dirsrvtests/tests/suites/plugins/modrdn_test.py
new file mode 100644
index 000000000..be79b0c3c
--- /dev/null
+++ b/dirsrvtests/tests/suites/plugins/modrdn_test.py
@@ -0,0 +1,174 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2025 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import pytest
+from lib389.topologies import topology_st
+from lib389._constants import DEFAULT_SUFFIX
+from lib389.idm.group import Groups
+from lib389.idm.user import nsUserAccounts
+from lib389.plugins import (
+ AutoMembershipDefinitions,
+ AutoMembershipPlugin,
+ AutoMembershipRegexRules,
+ MemberOfPlugin,
+ ReferentialIntegrityPlugin,
+)
+
+pytestmark = pytest.mark.tier1
+
+USER_PROPERTIES = {
+ "uid": "userwith",
+ "cn": "userwith",
+ "uidNumber": "1000",
+ "gidNumber": "2000",
+ "homeDirectory": "/home/testuser",
+ "displayName": "test user",
+}
+
+
+def test_modrdn_of_a_member_of_2_automember_groups(topology_st):
+ """Test that a member of 2 automember groups can be renamed
+
+ :id: 0e40bdc4-a2d2-4bb8-8368-e02c8920bad2
+
+ :setup: Standalone instance
+
+ :steps:
+ 1. Enable automember plugin
+ 2. Create definiton for users with A in the name
+ 3. Create regex rule for users with A in the name
+ 4. Create definiton for users with Z in the name
+ 5. Create regex rule for users with Z in the name
+ 6. Enable memberof plugin
+ 7. Enable referential integrity plugin
+ 8. Restart the instance
+ 9. Create groups
+ 10. Create users userwitha, userwithz, userwithaz
+ 11. Rename userwithaz
+
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. Success
+ 6. Success
+ 7. Success
+ 8. Success
+ 9. Success
+ 10. Success
+ 11. Success
+ """
+ inst = topology_st.standalone
+
+ # Enable automember plugin
+ automember_plugin = AutoMembershipPlugin(inst)
+ automember_plugin.enable()
+
+ # Create definiton for users with A in the name
+ automembers = AutoMembershipDefinitions(inst)
+ automember = automembers.create(
+ properties={
+ "cn": "userswithA",
+ "autoMemberScope": DEFAULT_SUFFIX,
+ "autoMemberFilter": "objectclass=posixAccount",
+ "autoMemberGroupingAttr": "member:dn",
+ }
+ )
+
+ # Create regex rule for users with A in the name
+ automembers_regex_rule = AutoMembershipRegexRules(inst, f"{automember.dn}")
+ automembers_regex_rule.create(
+ properties={
+ "cn": "userswithA",
+ "autoMemberInclusiveRegex": ["cn=.*a.*"],
+ "autoMemberTargetGroup": [f"cn=userswithA,ou=Groups,{DEFAULT_SUFFIX}"],
+ }
+ )
+
+ # Create definiton for users with Z in the name
+ automember = automembers.create(
+ properties={
+ "cn": "userswithZ",
+ "autoMemberScope": DEFAULT_SUFFIX,
+ "autoMemberFilter": "objectclass=posixAccount",
+ "autoMemberGroupingAttr": "member:dn",
+ }
+ )
+
+ # Create regex rule for users with Z in the name
+ automembers_regex_rule = AutoMembershipRegexRules(inst, f"{automember.dn}")
+ automembers_regex_rule.create(
+ properties={
+ "cn": "userswithZ",
+ "autoMemberInclusiveRegex": ["cn=.*z.*"],
+ "autoMemberTargetGroup": [f"cn=userswithZ,ou=Groups,{DEFAULT_SUFFIX}"],
+ }
+ )
+
+ # Enable memberof plugin
+ memberof_plugin = MemberOfPlugin(inst)
+ memberof_plugin.enable()
+
+ # Enable referential integrity plugin
+ referint_plugin = ReferentialIntegrityPlugin(inst)
+ referint_plugin.enable()
+
+ # Restart the instance
+ inst.restart()
+
+ # Create groups
+ groups = Groups(inst, DEFAULT_SUFFIX)
+ groupA = groups.create(properties={"cn": "userswithA"})
+ groupZ = groups.create(properties={"cn": "userswithZ"})
+
+ # Create users
+ users = nsUserAccounts(inst, DEFAULT_SUFFIX)
+
+ # userwitha
+ user_props = USER_PROPERTIES.copy()
+ user_props.update(
+ {
+ "uid": USER_PROPERTIES["uid"] + "a",
+ "cn": USER_PROPERTIES["cn"] + "a",
+ }
+ )
+ user = users.create(properties=user_props)
+
+ # userwithz
+ user_props.update(
+ {
+ "uid": USER_PROPERTIES["uid"] + "z",
+ "cn": USER_PROPERTIES["cn"] + "z",
+ }
+ )
+ user = users.create(properties=user_props)
+
+ # userwithaz
+ user_props.update(
+ {
+ "uid": USER_PROPERTIES["uid"] + "az",
+ "cn": USER_PROPERTIES["cn"] + "az",
+ }
+ )
+ user = users.create(properties=user_props)
+ user_orig_dn = user.dn
+
+ # Rename userwithaz
+ user.rename(new_rdn="uid=userwith")
+ user_new_dn = user.dn
+
+ assert user.get_attr_val_utf8("uid") != "userwithaz"
+
+ # Check groups contain renamed username
+ assert groupA.is_member(user_new_dn)
+ assert groupZ.is_member(user_new_dn)
+
+ # Check groups dont contain original username
+ assert not groupA.is_member(user_orig_dn)
+ assert not groupZ.is_member(user_orig_dn)
diff --git a/ldap/servers/plugins/automember/automember.c b/ldap/servers/plugins/automember/automember.c
index 419adb052..fde92ee12 100644
--- a/ldap/servers/plugins/automember/automember.c
+++ b/ldap/servers/plugins/automember/automember.c
@@ -1754,13 +1754,12 @@ automember_update_member_value(Slapi_Entry *member_e, const char *group_dn, char
}
mod_pb = slapi_pblock_new();
- slapi_modify_internal_set_pb(mod_pb, group_dn,
- mods, 0, 0, automember_get_plugin_id(), 0);
- slapi_modify_internal_pb(mod_pb);
- slapi_pblock_get(mod_pb, SLAPI_PLUGIN_INTOP_RESULT, &result);
+ /* Do a single mod with error overrides for DEL/ADD */
+ result = slapi_single_modify_internal_override(mod_pb, slapi_sdn_new_dn_byval(group_dn), mods,
+ automember_get_plugin_id(), 0);
if(add){
- if ((result != LDAP_SUCCESS) && (result != LDAP_TYPE_OR_VALUE_EXISTS)) {
+ if (result != LDAP_SUCCESS) {
slapi_log_err(SLAPI_LOG_ERR, AUTOMEMBER_PLUGIN_SUBSYSTEM,
"automember_update_member_value - Unable to add \"%s\" as "
"a \"%s\" value to group \"%s\" (%s).\n",
@@ -1770,7 +1769,7 @@ automember_update_member_value(Slapi_Entry *member_e, const char *group_dn, char
}
} else {
/* delete value */
- if ((result != LDAP_SUCCESS) && (result != LDAP_NO_SUCH_ATTRIBUTE)) {
+ if (result != LDAP_SUCCESS) {
slapi_log_err(SLAPI_LOG_ERR, AUTOMEMBER_PLUGIN_SUBSYSTEM,
"automember_update_member_value - Unable to delete \"%s\" as "
"a \"%s\" value from group \"%s\" (%s).\n",
diff --git a/ldap/servers/plugins/memberof/memberof.c b/ldap/servers/plugins/memberof/memberof.c
index f79b083a9..f3dc7cf00 100644
--- a/ldap/servers/plugins/memberof/memberof.c
+++ b/ldap/servers/plugins/memberof/memberof.c
@@ -1482,18 +1482,9 @@ memberof_del_dn_type_callback(Slapi_Entry *e, void *callback_data)
mod.mod_op = LDAP_MOD_DELETE;
mod.mod_type = ((memberof_del_dn_data *)callback_data)->type;
mod.mod_values = val;
-
- slapi_modify_internal_set_pb_ext(
- mod_pb, slapi_entry_get_sdn(e),
- mods, 0, 0,
- memberof_get_plugin_id(), SLAPI_OP_FLAG_BYPASS_REFERRALS);
-
- slapi_modify_internal_pb(mod_pb);
-
- slapi_pblock_get(mod_pb,
- SLAPI_PLUGIN_INTOP_RESULT,
- &rc);
-
+ /* Internal mod with error overrides for DEL/ADD */
+ rc = slapi_single_modify_internal_override(mod_pb, slapi_entry_get_sdn(e), mods,
+ memberof_get_plugin_id(), SLAPI_OP_FLAG_BYPASS_REFERRALS);
slapi_pblock_destroy(mod_pb);
if (rc == LDAP_NO_SUCH_ATTRIBUTE && val[0] == NULL) {
@@ -1966,6 +1957,7 @@ memberof_replace_dn_type_callback(Slapi_Entry *e, void *callback_data)
return rc;
}
+
LDAPMod **
my_copy_mods(LDAPMod **orig_mods)
{
@@ -2774,33 +2766,6 @@ memberof_modop_one_replace_r(Slapi_PBlock *pb, MemberOfConfig *config, int mod_o
replace_mod.mod_values = replace_val;
}
rc = memberof_add_memberof_attr(mods, op_to, config->auto_add_oc);
- if (rc == LDAP_NO_SUCH_ATTRIBUTE || rc == LDAP_TYPE_OR_VALUE_EXISTS) {
- if (rc == LDAP_TYPE_OR_VALUE_EXISTS) {
- /*
- * For some reason the new modrdn value is present, so retry
- * the delete by itself and ignore the add op by tweaking
- * the mod array.
- */
- mods[1] = NULL;
- rc = memberof_add_memberof_attr(mods, op_to, config->auto_add_oc);
- } else {
- /*
- * The memberof value to be replaced does not exist so just
- * add the new value. Shuffle the mod array to apply only
- * the add operation.
- */
- mods[0] = mods[1];
- mods[1] = NULL;
- rc = memberof_add_memberof_attr(mods, op_to, config->auto_add_oc);
- if (rc == LDAP_TYPE_OR_VALUE_EXISTS) {
- /*
- * The entry already has the expected memberOf value, no
- * problem just return success.
- */
- rc = LDAP_SUCCESS;
- }
- }
- }
}
}
@@ -4454,43 +4419,57 @@ memberof_add_memberof_attr(LDAPMod **mods, const char *dn, char *add_oc)
Slapi_PBlock *mod_pb = NULL;
int added_oc = 0;
int rc = 0;
+ LDAPMod *single_mod[2];
- while (1) {
- mod_pb = slapi_pblock_new();
- slapi_modify_internal_set_pb(
- mod_pb, dn, mods, 0, 0,
- memberof_get_plugin_id(), SLAPI_OP_FLAG_BYPASS_REFERRALS);
- slapi_modify_internal_pb(mod_pb);
-
- slapi_pblock_get(mod_pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
- if (rc == LDAP_OBJECT_CLASS_VIOLATION) {
- if (!add_oc || added_oc) {
- /*
- * We aren't auto adding an objectclass, or we already
- * added the objectclass, and we are still failing.
- */
+ if (!dn || !mods) {
+ slapi_log_err(SLAPI_LOG_ERR, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "Invalid argument: %s%s is NULL\n",
+ !dn ? "dn " : "",
+ !mods ? "mods " : "");
+ return LDAP_PARAM_ERROR;
+ }
+
+
+ mod_pb = slapi_pblock_new();
+ /* Split multiple mods into individual mod operations */
+ for (size_t i = 0; (mods != NULL) && (mods[i] != NULL); i++) {
+ single_mod[0] = mods[i];
+ single_mod[1] = NULL;
+
+ while (1) {
+ slapi_pblock_init(mod_pb);
+
+ /* Internal mod with error overrides for DEL/ADD */
+ rc = slapi_single_modify_internal_override(mod_pb, slapi_sdn_new_normdn_byref(dn), single_mod,
+ memberof_get_plugin_id(), SLAPI_OP_FLAG_BYPASS_REFERRALS);
+ if (rc == LDAP_OBJECT_CLASS_VIOLATION) {
+ if (!add_oc || added_oc) {
+ /*
+ * We aren't auto adding an objectclass, or we already
+ * added the objectclass, and we are still failing.
+ */
+ break;
+ }
+ rc = memberof_add_objectclass(add_oc, dn);
+ slapi_log_err(SLAPI_LOG_WARNING, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "Entry %s - schema violation caught - repair operation %s\n",
+ dn ? dn : "unknown",
+ rc ? "failed" : "succeeded");
+ if (rc) {
+ /* Failed to add objectclass */
+ rc = LDAP_OBJECT_CLASS_VIOLATION;
+ break;
+ }
+ added_oc = 1;
+ } else if (rc) {
+ /* Some other fatal error */
+ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
+ "memberof_add_memberof_attr - Internal modify failed. rc=%d\n", rc);
break;
- }
- rc = memberof_add_objectclass(add_oc, dn);
- slapi_log_err(SLAPI_LOG_WARNING, MEMBEROF_PLUGIN_SUBSYSTEM,
- "Entry %s - schema violation caught - repair operation %s\n",
- dn ? dn : "unknown",
- rc ? "failed" : "succeeded");
- if (rc) {
- /* Failed to add objectclass */
- rc = LDAP_OBJECT_CLASS_VIOLATION;
+ } else {
+ /* success */
break;
}
- added_oc = 1;
- slapi_pblock_destroy(mod_pb);
- } else if (rc) {
- /* Some other fatal error */
- slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM,
- "memberof_add_memberof_attr - Internal modify failed. rc=%d\n", rc);
- break;
- } else {
- /* success */
- break;
}
}
slapi_pblock_destroy(mod_pb);
diff --git a/ldap/servers/plugins/referint/referint.c b/ldap/servers/plugins/referint/referint.c
index 28240c1f6..c5e259d8d 100644
--- a/ldap/servers/plugins/referint/referint.c
+++ b/ldap/servers/plugins/referint/referint.c
@@ -711,19 +711,28 @@ static int
_do_modify(Slapi_PBlock *mod_pb, Slapi_DN *entrySDN, LDAPMod **mods)
{
int rc = 0;
+ LDAPMod *mod[2];
- slapi_pblock_init(mod_pb);
+ /* Split multiple modifications into individual modify operations */
+ for (size_t i = 0; (mods != NULL) && (mods[i] != NULL); i++) {
+ mod[0] = mods[i];
+ mod[1] = NULL;
- if (allow_repl) {
- /* Must set as a replicated operation */
- slapi_modify_internal_set_pb_ext(mod_pb, entrySDN, mods, NULL, NULL,
- referint_plugin_identity, OP_FLAG_REPLICATED);
- } else {
- slapi_modify_internal_set_pb_ext(mod_pb, entrySDN, mods, NULL, NULL,
- referint_plugin_identity, 0);
+ slapi_pblock_init(mod_pb);
+
+ /* Do a single mod with error overrides for DEL/ADD */
+ if (allow_repl) {
+ rc = slapi_single_modify_internal_override(mod_pb, entrySDN, mod,
+ referint_plugin_identity, OP_FLAG_REPLICATED);
+ } else {
+ rc = slapi_single_modify_internal_override(mod_pb, entrySDN, mod,
+ referint_plugin_identity, 0);
+ }
+
+ if (rc != LDAP_SUCCESS) {
+ return rc;
+ }
}
- slapi_modify_internal_pb(mod_pb);
- slapi_pblock_get(mod_pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
return rc;
}
@@ -1033,7 +1042,6 @@ _update_all_per_mod(Slapi_DN *entrySDN, /* DN of the searched entry */
/* (case 1) */
slapi_mods_add_string(smods, LDAP_MOD_DELETE, attrName, sval);
slapi_mods_add_string(smods, LDAP_MOD_ADD, attrName, newDN);
-
} else if (p) {
/* (case 2) */
slapi_mods_add_string(smods, LDAP_MOD_DELETE, attrName, sval);
diff --git a/ldap/servers/slapd/modify.c b/ldap/servers/slapd/modify.c
index 669bb104c..455eb63ec 100644
--- a/ldap/servers/slapd/modify.c
+++ b/ldap/servers/slapd/modify.c
@@ -492,6 +492,57 @@ slapi_modify_internal_set_pb_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, LDAPMod
slapi_pblock_set(pb, SLAPI_PLUGIN_IDENTITY, plugin_identity);
}
+/* Performs a single LDAP modify operation with error overrides.
+ *
+ * If specific errors occur, such as attempting to add an existing attribute or
+ * delete a non-existent one, the function overrides the error and returns success:
+ * - LDAP_MOD_ADD -> LDAP_TYPE_OR_VALUE_EXISTS (ignored)
+ * - LDAP_MOD_DELETE -> LDAP_NO_SUCH_ATTRIBUTE (ignored)
+ *
+ * Any other errors encountered during the operation will be returned as-is.
+ */
+int
+slapi_single_modify_internal_override(Slapi_PBlock *pb, const Slapi_DN *sdn, LDAPMod **mod, Slapi_ComponentId *plugin_id, int op_flags)
+{
+ int rc = 0;
+ int result = 0;
+ int result_reset = 0;
+ int mod_op = 0;
+
+ if (!pb || !sdn || !mod || !mod[0]) {
+ slapi_log_err(SLAPI_LOG_ERR, "slapi_single_modify_internal_override",
+ "Invalid argument: %s%s%s%s is NULL\n",
+ !pb ? "pb " : "",
+ !sdn ? "sdn " : "",
+ !mod ? "mod " : "",
+ !mod[0] ? "mod[0] " : "");
+
+ return LDAP_PARAM_ERROR;
+ }
+
+ slapi_modify_internal_set_pb_ext(pb, sdn, mod, NULL, NULL, plugin_id, op_flags);
+ slapi_modify_internal_pb(pb);
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &result);
+
+ if (result != LDAP_SUCCESS) {
+ mod_op = mod[0]->mod_op & LDAP_MOD_OP;
+ if ((mod_op == LDAP_MOD_ADD && result == LDAP_TYPE_OR_VALUE_EXISTS) ||
+ (mod_op == LDAP_MOD_DELETE && result == LDAP_NO_SUCH_ATTRIBUTE)) {
+ slapi_log_err(SLAPI_LOG_PLUGIN, "slapi_single_modify_internal_override",
+ "Overriding return code - plugin:%s dn:%s mod_op:%d result:%d\n",
+ plugin_id ? plugin_id->sci_component_name : "unknown",
+ sdn ? sdn->udn : "unknown", mod_op, result);
+
+ slapi_pblock_set(pb, SLAPI_PLUGIN_INTOP_RESULT, &result_reset);
+ rc = LDAP_SUCCESS;
+ } else {
+ rc = result;
+ }
+ }
+
+ return rc;
+}
+
/* Helper functions */
static int
diff --git a/ldap/servers/slapd/slapi-plugin.h b/ldap/servers/slapd/slapi-plugin.h
index 9fdcaccc8..a84a60c92 100644
--- a/ldap/servers/slapd/slapi-plugin.h
+++ b/ldap/servers/slapd/slapi-plugin.h
@@ -5965,6 +5965,7 @@ void slapi_add_entry_internal_set_pb(Slapi_PBlock *pb, Slapi_Entry *e, LDAPContr
int slapi_add_internal_set_pb(Slapi_PBlock *pb, const char *dn, LDAPMod **attrs, LDAPControl **controls, Slapi_ComponentId *plugin_identity, int operation_flags);
void slapi_modify_internal_set_pb(Slapi_PBlock *pb, const char *dn, LDAPMod **mods, LDAPControl **controls, const char *uniqueid, Slapi_ComponentId *plugin_identity, int operation_flags);
void slapi_modify_internal_set_pb_ext(Slapi_PBlock *pb, const Slapi_DN *sdn, LDAPMod **mods, LDAPControl **controls, const char *uniqueid, Slapi_ComponentId *plugin_identity, int operation_flags);
+int slapi_single_modify_internal_override(Slapi_PBlock *pb, const Slapi_DN *sdn, LDAPMod **mod, Slapi_ComponentId *plugin_identity, int operation_flags);
/**
* Set \c Slapi_PBlock to perform modrdn/rename internally
*
--
2.51.1

View File

@ -1,415 +0,0 @@
From ccaaaa31a86eb059315580249838d72e4a51bf8b Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Tue, 14 Jan 2025 18:12:56 +0100
Subject: [PATCH] Issue 6470 - Some replication status data are reset upon a
restart (#6471)
Bug description:
The replication agreement contains operational attributes
related to the total init: nsds5replicaLastInitStart,
nsds5replicaLastInitEnd, nsds5replicaLastInitStatus.
Those attributes are reset at restart
Fix description:
When reading the replication agreement from config
(agmt_new_from_entry) restore the attributes into
the in-memory RA.
Updates the RA config entry from the in-memory RA
during shutdown/cleanallruv/enable_ra
fixes: #6470
Reviewed by: Simon Pichugin (Thanks !!)
(cherry picked from commit 90071a334517be523e498bded5b663c50c40ee3f)
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
.../suites/replication/single_master_test.py | 128 ++++++++++++++++
ldap/servers/plugins/replication/repl5.h | 4 +
ldap/servers/plugins/replication/repl5_agmt.c | 140 +++++++++++++++++-
.../plugins/replication/repl5_agmtlist.c | 1 +
.../replication/repl5_replica_config.c | 1 +
.../plugins/replication/repl_globals.c | 3 +
6 files changed, 273 insertions(+), 4 deletions(-)
diff --git a/dirsrvtests/tests/suites/replication/single_master_test.py b/dirsrvtests/tests/suites/replication/single_master_test.py
index e927e6cfd..f448d2342 100644
--- a/dirsrvtests/tests/suites/replication/single_master_test.py
+++ b/dirsrvtests/tests/suites/replication/single_master_test.py
@@ -13,6 +13,7 @@ from lib389.utils import *
from lib389.idm.user import UserAccounts, TEST_USER_PROPERTIES
from lib389.replica import ReplicationManager, Replicas
+from lib389.agreement import Agreements
from lib389.backend import Backends
from lib389.topologies import topology_m1c1 as topo_r # Replication
@@ -154,6 +155,133 @@ def test_lastupdate_attr_before_init(topo_nr):
json_obj = json.loads(json_status)
log.debug("JSON status message: {}".format(json_obj))
+def test_total_init_operational_attr(topo_r):
+ """Check that operation attributes nsds5replicaLastInitStatus
+ nsds5replicaLastInitStart and nsds5replicaLastInitEnd
+ are preserved between restart
+
+ :id: 6ba00bb1-87c0-47dd-86e0-ccf892b3985b
+ :customerscenario: True
+ :setup: Replication setup with supplier and consumer instances,
+ test user on supplier
+ :steps:
+ 1. Check that user was replicated to consumer
+ 2. Trigger a first total init
+ 3. Check status/start/end values are set on the supplier
+ 4. Restart supplier
+ 5. Check previous status/start/end values are preserved
+ 6. Trigger a second total init
+ 7. Check status/start/end values are set on the supplier
+ 8. Restart supplier
+ 9. Check previous status/start/end values are preserved
+ 10. Check status/start/end values are different between
+ first and second total init
+ :expectedresults:
+ 1. The user should be replicated to consumer
+ 2. Total init should be successful
+ 3. It must exist a values
+ 4. Operation should be successful
+ 5. Check values are identical before/after restart
+ 6. Total init should be successful
+ 7. It must exist a values
+ 8. Operation should be successful
+ 9. Check values are identical before/after restart
+ 10. values must differ between first/second total init
+ """
+
+ supplier = topo_r.ms["supplier1"]
+ consumer = topo_r.cs["consumer1"]
+ repl = ReplicationManager(DEFAULT_SUFFIX)
+
+ # Create a test user
+ m_users = UserAccounts(topo_r.ms["supplier1"], DEFAULT_SUFFIX)
+ m_user = m_users.ensure_state(properties=TEST_USER_PROPERTIES)
+ m_user.ensure_present('mail', 'testuser@redhat.com')
+
+ # Then check it is replicated
+ log.info("Check that replication is working")
+ repl.wait_for_replication(supplier, consumer)
+ c_users = UserAccounts(topo_r.cs["consumer1"], DEFAULT_SUFFIX)
+ c_user = c_users.get('testuser')
+ assert c_user
+
+ # Retrieve the replication agreement S1->C1
+ replica_supplier = Replicas(supplier).get(DEFAULT_SUFFIX)
+ agmts_supplier = Agreements(supplier, replica_supplier.dn)
+ supplier_consumer = None
+ for agmt in agmts_supplier.list():
+ if (agmt.get_attr_val_utf8('nsDS5ReplicaPort') == str(consumer.port) and
+ agmt.get_attr_val_utf8('nsDS5ReplicaHost') == consumer.host):
+ supplier_consumer = agmt
+ break
+ assert supplier_consumer
+
+ # Trigger a first total init and check that
+ # start/end/status is updated AND preserved during a restart
+ log.info("First total init")
+ supplier_consumer.begin_reinit()
+ (done, error) = supplier_consumer.wait_reinit()
+ assert done is True
+
+ status_1 = supplier_consumer.get_attr_val_utf8("nsds5replicaLastInitStatus")
+ assert status_1
+
+ initStart_1 = supplier_consumer.get_attr_val_utf8("nsds5replicaLastInitStart")
+ assert initStart_1
+
+ initEnd_1 = supplier_consumer.get_attr_val_utf8("nsds5replicaLastInitEnd")
+ assert initEnd_1
+
+ log.info("Check values from first total init are preserved")
+ supplier.restart()
+ post_restart_status_1 = supplier_consumer.get_attr_val_utf8("nsds5replicaLastInitStatus")
+ assert post_restart_status_1
+ assert post_restart_status_1 == status_1
+
+ post_restart_initStart_1 = supplier_consumer.get_attr_val_utf8("nsds5replicaLastInitStart")
+ assert post_restart_initStart_1
+ assert post_restart_initStart_1 == initStart_1
+
+ post_restart_initEnd_1 = supplier_consumer.get_attr_val_utf8("nsds5replicaLastInitEnd")
+ assert post_restart_initEnd_1 == initEnd_1
+
+ # Trigger a second total init and check that
+ # start/end/status is updated (differ from previous values)
+ # AND new values are preserved during a restart
+ time.sleep(1)
+ log.info("Second total init")
+ supplier_consumer.begin_reinit()
+ (done, error) = supplier_consumer.wait_reinit()
+ assert done is True
+
+ status_2 = supplier_consumer.get_attr_val_utf8("nsds5replicaLastInitStatus")
+ assert status_2
+
+ initStart_2 = supplier_consumer.get_attr_val_utf8("nsds5replicaLastInitStart")
+ assert initStart_2
+
+ initEnd_2 = supplier_consumer.get_attr_val_utf8("nsds5replicaLastInitEnd")
+ assert initEnd_2
+
+ log.info("Check values from second total init are preserved")
+ supplier.restart()
+ post_restart_status_2 = supplier_consumer.get_attr_val_utf8("nsds5replicaLastInitStatus")
+ assert post_restart_status_2
+ assert post_restart_status_2 == status_2
+
+ post_restart_initStart_2 = supplier_consumer.get_attr_val_utf8("nsds5replicaLastInitStart")
+ assert post_restart_initStart_2
+ assert post_restart_initStart_2 == initStart_2
+
+ post_restart_initEnd_2 = supplier_consumer.get_attr_val_utf8("nsds5replicaLastInitEnd")
+ assert post_restart_initEnd_2 == initEnd_2
+
+ # Check that values are updated by total init
+ log.info("Check values from first/second total init are different")
+ assert status_2 == status_1
+ assert initStart_2 != initStart_1
+ assert initEnd_2 != initEnd_1
+
if __name__ == '__main__':
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/plugins/replication/repl5.h b/ldap/servers/plugins/replication/repl5.h
index c2fbff8c0..65e2059e7 100644
--- a/ldap/servers/plugins/replication/repl5.h
+++ b/ldap/servers/plugins/replication/repl5.h
@@ -165,6 +165,9 @@ extern const char *type_nsds5ReplicaBootstrapCredentials;
extern const char *type_nsds5ReplicaBootstrapBindMethod;
extern const char *type_nsds5ReplicaBootstrapTransportInfo;
extern const char *type_replicaKeepAliveUpdateInterval;
+extern const char *type_nsds5ReplicaLastInitStart;
+extern const char *type_nsds5ReplicaLastInitEnd;
+extern const char *type_nsds5ReplicaLastInitStatus;
/* Attribute names for windows replication agreements */
extern const char *type_nsds7WindowsReplicaArea;
@@ -430,6 +433,7 @@ void agmt_notify_change(Repl_Agmt *ra, Slapi_PBlock *pb);
Object *agmt_get_consumer_ruv(Repl_Agmt *ra);
ReplicaId agmt_get_consumer_rid(Repl_Agmt *ra, void *conn);
int agmt_set_consumer_ruv(Repl_Agmt *ra, RUV *ruv);
+void agmt_update_init_status(Repl_Agmt *ra);
void agmt_update_consumer_ruv(Repl_Agmt *ra);
CSN *agmt_get_consumer_schema_csn(Repl_Agmt *ra);
void agmt_set_consumer_schema_csn(Repl_Agmt *ra, CSN *csn);
diff --git a/ldap/servers/plugins/replication/repl5_agmt.c b/ldap/servers/plugins/replication/repl5_agmt.c
index a71343dec..c3b8d298c 100644
--- a/ldap/servers/plugins/replication/repl5_agmt.c
+++ b/ldap/servers/plugins/replication/repl5_agmt.c
@@ -56,6 +56,7 @@
#include "repl5_prot_private.h"
#include "cl5_api.h"
#include "slapi-plugin.h"
+#include "slap.h"
#define DEFAULT_TIMEOUT 120 /* (seconds) default outbound LDAP connection */
#define DEFAULT_FLOWCONTROL_WINDOW 1000 /* #entries sent without acknowledgment */
@@ -510,10 +511,33 @@ agmt_new_from_entry(Slapi_Entry *e)
ra->last_update_status[0] = '\0';
ra->update_in_progress = PR_FALSE;
ra->stop_in_progress = PR_FALSE;
- ra->last_init_end_time = 0UL;
- ra->last_init_start_time = 0UL;
- ra->last_init_status[0] = '\0';
- ra->changecounters = (struct changecounter **)slapi_ch_calloc(MAX_NUM_OF_MASTERS + 1,
+ val = (char *)slapi_entry_attr_get_ref(e, type_nsds5ReplicaLastInitEnd);
+ if (val) {
+ time_t init_end_time;
+
+ init_end_time = parse_genTime((char *) val);
+ if (init_end_time == NO_TIME || init_end_time == SLAPD_END_TIME) {
+ ra->last_init_end_time = 0UL;
+ } else {
+ ra->last_init_end_time = init_end_time;
+ }
+ }
+ val = (char *)slapi_entry_attr_get_ref(e, type_nsds5ReplicaLastInitStart);
+ if (val) {
+ time_t init_start_time;
+
+ init_start_time = parse_genTime((char *) val);
+ if (init_start_time == NO_TIME || init_start_time == SLAPD_END_TIME) {
+ ra->last_init_start_time = 0UL;
+ } else {
+ ra->last_init_start_time = init_start_time;
+ }
+ }
+ val = (char *)slapi_entry_attr_get_ref(e, type_nsds5ReplicaLastInitStatus);
+ if (val) {
+ strcpy(ra->last_init_status, val);
+ }
+ ra->changecounters = (struct changecounter **)slapi_ch_calloc(MAX_NUM_OF_SUPPLIERS + 1,
sizeof(struct changecounter *));
ra->num_changecounters = 0;
ra->max_changecounters = MAX_NUM_OF_MASTERS;
@@ -2504,6 +2528,113 @@ agmt_set_consumer_ruv(Repl_Agmt *ra, RUV *ruv)
return 0;
}
+void
+agmt_update_init_status(Repl_Agmt *ra)
+{
+ int rc;
+ Slapi_PBlock *pb;
+ LDAPMod **mods;
+ int nb_mods = 0;
+ int mod_idx;
+ Slapi_Mod smod_start_time = {0};
+ Slapi_Mod smod_end_time = {0};
+ Slapi_Mod smod_status = {0};
+
+ PR_ASSERT(ra);
+ PR_Lock(ra->lock);
+
+ if (ra->last_init_start_time) {
+ nb_mods++;
+ }
+ if (ra->last_init_end_time) {
+ nb_mods++;
+ }
+ if (ra->last_init_status[0] != '\0') {
+ nb_mods++;
+ }
+ if (nb_mods == 0) {
+ /* shortcut. no need to go further */
+ PR_Unlock(ra->lock);
+ return;
+ }
+ mods = (LDAPMod **) slapi_ch_malloc((nb_mods + 1) * sizeof(LDAPMod *));
+ mod_idx = 0;
+ if (ra->last_init_start_time) {
+ struct berval val;
+ char *time_tmp = NULL;
+ slapi_mod_init(&smod_start_time, 1);
+ slapi_mod_set_type(&smod_start_time, type_nsds5ReplicaLastInitStart);
+ slapi_mod_set_operation(&smod_start_time, LDAP_MOD_REPLACE | LDAP_MOD_BVALUES);
+
+ time_tmp = format_genTime(ra->last_init_start_time);
+ val.bv_val = time_tmp;
+ val.bv_len = strlen(time_tmp);
+ slapi_mod_add_value(&smod_start_time, &val);
+ slapi_ch_free((void **)&time_tmp);
+ mods[mod_idx] = (LDAPMod *)slapi_mod_get_ldapmod_byref(&smod_start_time);
+ mod_idx++;
+ }
+ if (ra->last_init_end_time) {
+ struct berval val;
+ char *time_tmp = NULL;
+ slapi_mod_init(&smod_end_time, 1);
+ slapi_mod_set_type(&smod_end_time, type_nsds5ReplicaLastInitEnd);
+ slapi_mod_set_operation(&smod_end_time, LDAP_MOD_REPLACE | LDAP_MOD_BVALUES);
+
+ time_tmp = format_genTime(ra->last_init_end_time);
+ val.bv_val = time_tmp;
+ val.bv_len = strlen(time_tmp);
+ slapi_mod_add_value(&smod_end_time, &val);
+ slapi_ch_free((void **)&time_tmp);
+ mods[mod_idx] = (LDAPMod *)slapi_mod_get_ldapmod_byref(&smod_end_time);
+ mod_idx++;
+ }
+ if (ra->last_init_status[0] != '\0') {
+ struct berval val;
+ char *init_status = NULL;
+ slapi_mod_init(&smod_status, 1);
+ slapi_mod_set_type(&smod_status, type_nsds5ReplicaLastInitStatus);
+ slapi_mod_set_operation(&smod_status, LDAP_MOD_REPLACE | LDAP_MOD_BVALUES);
+
+ init_status = slapi_ch_strdup(ra->last_init_status);
+ val.bv_val = init_status;
+ val.bv_len = strlen(init_status);
+ slapi_mod_add_value(&smod_status, &val);
+ slapi_ch_free((void **)&init_status);
+ mods[mod_idx] = (LDAPMod *)slapi_mod_get_ldapmod_byref(&smod_status);
+ mod_idx++;
+ }
+
+ if (nb_mods) {
+ /* it is ok to release the lock here because we are done with the agreement data.
+ we have to do it before issuing the modify operation because it causes
+ agmtlist_notify_all to be called which uses the same lock - hence the deadlock */
+ PR_Unlock(ra->lock);
+
+ pb = slapi_pblock_new();
+ mods[nb_mods] = NULL;
+
+ slapi_modify_internal_set_pb_ext(pb, ra->dn, mods, NULL, NULL,
+ repl_get_plugin_identity(PLUGIN_MULTISUPPLIER_REPLICATION), 0);
+ slapi_modify_internal_pb(pb);
+
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
+ if (rc != LDAP_SUCCESS && rc != LDAP_NO_SUCH_ATTRIBUTE) {
+ slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "agmt_update_consumer_ruv - "
+ "%s: agmt_update_consumer_ruv: "
+ "failed to update consumer's RUV; LDAP error - %d\n",
+ ra->long_name, rc);
+ }
+
+ slapi_pblock_destroy(pb);
+ } else {
+ PR_Unlock(ra->lock);
+ }
+ slapi_mod_done(&smod_start_time);
+ slapi_mod_done(&smod_end_time);
+ slapi_mod_done(&smod_status);
+}
+
void
agmt_update_consumer_ruv(Repl_Agmt *ra)
{
@@ -3123,6 +3254,7 @@ agmt_set_enabled_from_entry(Repl_Agmt *ra, Slapi_Entry *e, char *returntext)
PR_Unlock(ra->lock);
agmt_stop(ra);
agmt_update_consumer_ruv(ra);
+ agmt_update_init_status(ra);
agmt_set_last_update_status(ra, 0, 0, "agreement disabled");
return rc;
}
diff --git a/ldap/servers/plugins/replication/repl5_agmtlist.c b/ldap/servers/plugins/replication/repl5_agmtlist.c
index 18b641f8c..e3b1e814c 100644
--- a/ldap/servers/plugins/replication/repl5_agmtlist.c
+++ b/ldap/servers/plugins/replication/repl5_agmtlist.c
@@ -782,6 +782,7 @@ agmtlist_shutdown()
ra = (Repl_Agmt *)object_get_data(ro);
agmt_stop(ra);
agmt_update_consumer_ruv(ra);
+ agmt_update_init_status(ra);
next_ro = objset_next_obj(agmt_set, ro);
/* Object ro was released in objset_next_obj,
* but the address ro can be still used to remove ro from objset. */
diff --git a/ldap/servers/plugins/replication/repl5_replica_config.c b/ldap/servers/plugins/replication/repl5_replica_config.c
index aea2cf506..8cc7423bf 100644
--- a/ldap/servers/plugins/replication/repl5_replica_config.c
+++ b/ldap/servers/plugins/replication/repl5_replica_config.c
@@ -2006,6 +2006,7 @@ clean_agmts(cleanruv_data *data)
cleanruv_log(data->task, data->rid, CLEANALLRUV_ID, SLAPI_LOG_INFO, "Cleaning agmt...");
agmt_stop(agmt);
agmt_update_consumer_ruv(agmt);
+ agmt_update_init_status(agmt);
agmt_start(agmt);
agmt_obj = agmtlist_get_next_agreement_for_replica(data->replica, agmt_obj);
}
diff --git a/ldap/servers/plugins/replication/repl_globals.c b/ldap/servers/plugins/replication/repl_globals.c
index 797ca957f..e6b89c33b 100644
--- a/ldap/servers/plugins/replication/repl_globals.c
+++ b/ldap/servers/plugins/replication/repl_globals.c
@@ -118,6 +118,9 @@ const char *type_nsds5ReplicaBootstrapBindDN = "nsds5ReplicaBootstrapBindDN";
const char *type_nsds5ReplicaBootstrapCredentials = "nsds5ReplicaBootstrapCredentials";
const char *type_nsds5ReplicaBootstrapBindMethod = "nsds5ReplicaBootstrapBindMethod";
const char *type_nsds5ReplicaBootstrapTransportInfo = "nsds5ReplicaBootstrapTransportInfo";
+const char *type_nsds5ReplicaLastInitStart = "nsds5replicaLastInitStart";
+const char *type_nsds5ReplicaLastInitEnd = "nsds5replicaLastInitEnd";
+const char *type_nsds5ReplicaLastInitStatus = "nsds5replicaLastInitStatus";
/* windows sync specific attributes */
const char *type_nsds7WindowsReplicaArea = "nsds7WindowsReplicaSubtree";
--
2.51.1

View File

@ -1,671 +0,0 @@
From dfb7e19fdcefe4af683a235ea7113956248571e3 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Thu, 17 Nov 2022 14:21:17 +0100
Subject: [PATCH] Issue 3729 - RFE Extend log of operations statistics in
access log (#5508)
Bug description:
Create a per operation framework to collect/display
statistics about internal ressource consumption
Fix description:
The fix contains 2 parts
The framework, that registers a per operation object extension
(op_stat_init). The extension is used to store/retrieve
collected statistics.
To reduce the impact of collecting/logging it uses a toggle
with config attribute 'nsslapd-statlog-level' that is a bit mask.
So that data are collected and logged only if the appropriate
statistic level is set.
An exemple of statistic level regarding indexes fetching
during the evaluation of a search filter.
it is implemented in filterindex.c (store) and result.c (retrieve/log).
This path uses LDAP_STAT_READ_INDEX=0x1.
For LDAP_STAT_READ_INDEX, the collected data are:
- for each key (attribute, type, value) the number of
IDs
- the duration to fetch all the values
design https://www.port389.org/docs/389ds/design/log-operation-stats.html
relates: #3729
Reviewed by: Pierre Rogier, Mark Reynolds (thanks !)
(cherry picked from commit a480d2cbfa2b1325f44ab3e1c393c5ee348b388e)
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
.../tests/suites/ds_logs/ds_logs_test.py | 73 ++++++++++++++++
ldap/servers/slapd/back-ldbm/filterindex.c | 49 +++++++++++
ldap/servers/slapd/libglobs.c | 48 +++++++++++
ldap/servers/slapd/log.c | 26 ++++++
ldap/servers/slapd/log.h | 1 +
ldap/servers/slapd/main.c | 1 +
ldap/servers/slapd/operation.c | 86 +++++++++++++++++++
ldap/servers/slapd/proto-slap.h | 8 ++
ldap/servers/slapd/result.c | 64 ++++++++++++++
ldap/servers/slapd/slap.h | 4 +
ldap/servers/slapd/slapi-private.h | 27 ++++++
11 files changed, 387 insertions(+)
diff --git a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
index 84d721756..43288f67f 100644
--- a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
+++ b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
@@ -27,6 +27,7 @@ from lib389.idm.group import Groups
from lib389.idm.organizationalunit import OrganizationalUnits
from lib389._constants import DEFAULT_SUFFIX, LOG_ACCESS_LEVEL, PASSWORD
from lib389.utils import ds_is_older, ds_is_newer
+from lib389.dseldif import DSEldif
import ldap
import glob
import re
@@ -1250,6 +1251,78 @@ def test_missing_backend_suffix(topology_st, request):
request.addfinalizer(fin)
+def test_stat_index(topology_st, request):
+ """Testing nsslapd-statlog-level with indexing statistics
+
+ :id: fcabab05-f000-468c-8eb4-02ce3c39c902
+ :setup: Standalone instance
+ :steps:
+ 1. Check that nsslapd-statlog-level is 0 (default)
+ 2. Create 20 users with 'cn' starting with 'user\_'
+ 3. Check there is no statistic record in the access log with ADD
+ 4. Check there is no statistic record in the access log with SRCH
+ 5. Set nsslapd-statlog-level=LDAP_STAT_READ_INDEX (0x1) to get
+ statistics when reading indexes
+ 6. Check there is statistic records in access log with SRCH
+ :expectedresults:
+ 1. This should pass
+ 2. This should pass
+ 3. This should pass
+ 4. This should pass
+ 5. This should pass
+ 6. This should pass
+ """
+ topology_st.standalone.start()
+
+ # Step 1
+ log.info("Assert nsslapd-statlog-level is by default 0")
+ assert topology_st.standalone.config.get_attr_val_int("nsslapd-statlog-level") == 0
+
+ # Step 2
+ users = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX)
+ users_set = []
+ log.info('Adding 20 users')
+ for i in range(20):
+ name = 'user_%d' % i
+ last_user = users.create(properties={
+ 'uid': name,
+ 'sn': name,
+ 'cn': name,
+ 'uidNumber': '1000',
+ 'gidNumber': '1000',
+ 'homeDirectory': '/home/%s' % name,
+ 'mail': '%s@example.com' % name,
+ 'userpassword': 'pass%s' % name,
+ })
+ users_set.append(last_user)
+
+ # Step 3
+ assert not topology_st.standalone.ds_access_log.match('.*STAT read index.*')
+
+ # Step 4
+ entries = topology_st.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "cn=user_*")
+ assert not topology_st.standalone.ds_access_log.match('.*STAT read index.*')
+
+ # Step 5
+ log.info("Set nsslapd-statlog-level: 1 to enable indexing statistics")
+ topology_st.standalone.config.set("nsslapd-statlog-level", "1")
+
+ # Step 6
+ entries = topology_st.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "cn=user_*")
+ topology_st.standalone.stop()
+ assert topology_st.standalone.ds_access_log.match('.*STAT read index.*')
+ assert topology_st.standalone.ds_access_log.match('.*STAT read index: attribute.*')
+ assert topology_st.standalone.ds_access_log.match('.*STAT read index: duration.*')
+ topology_st.standalone.start()
+
+ def fin():
+ log.info('Deleting users')
+ for user in users_set:
+ user.delete()
+ topology_st.standalone.config.set("nsslapd-statlog-level", "0")
+
+ request.addfinalizer(fin)
+
if __name__ == '__main__':
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/back-ldbm/filterindex.c b/ldap/servers/slapd/back-ldbm/filterindex.c
index 8a79848c3..30550dde7 100644
--- a/ldap/servers/slapd/back-ldbm/filterindex.c
+++ b/ldap/servers/slapd/back-ldbm/filterindex.c
@@ -1040,13 +1040,57 @@ keys2idl(
int allidslimit)
{
IDList *idl = NULL;
+ Op_stat *op_stat;
+ PRBool collect_stat = PR_FALSE;
slapi_log_err(SLAPI_LOG_TRACE, "keys2idl", "=> type %s indextype %s\n", type, indextype);
+
+ /* Before reading the index take the start time */
+ if (LDAP_STAT_READ_INDEX & config_get_statlog_level()) {
+ op_stat = op_stat_get_operation_extension(pb);
+ if (op_stat->search_stat) {
+ collect_stat = PR_TRUE;
+ clock_gettime(CLOCK_MONOTONIC, &(op_stat->search_stat->keys_lookup_start));
+ }
+ }
+
for (uint32_t i = 0; ivals[i] != NULL; i++) {
IDList *idl2 = NULL;
+ struct component_keys_lookup *key_stat;
+ int key_len;
idl2 = index_read_ext_allids(pb, be, type, indextype, slapi_value_get_berval(ivals[i]), txn, err, unindexed, allidslimit);
+ if (collect_stat) {
+ /* gather the index lookup statistics */
+ key_stat = (struct component_keys_lookup *) slapi_ch_calloc(1, sizeof (struct component_keys_lookup));
+
+ /* indextype e.g. "eq" or "sub" (see index.c) */
+ if (indextype) {
+ key_stat->index_type = slapi_ch_strdup(indextype);
+ }
+ /* key value e.g. '^st' or 'smith'*/
+ key_len = slapi_value_get_length(ivals[i]);
+ if (key_len) {
+ key_stat->key = (char *) slapi_ch_calloc(1, key_len + 1);
+ memcpy(key_stat->key, slapi_value_get_string(ivals[i]), key_len);
+ }
+ /* attribute name e.g. 'uid' */
+ if (type) {
+ key_stat->attribute_type = slapi_ch_strdup(type);
+ }
+
+ /* Number of lookup IDs with the key */
+ key_stat->id_lookup_cnt = idl2 ? idl2->b_nids : 0;
+ if (op_stat->search_stat->keys_lookup) {
+ /* it already exist key stat. add key_stat at the head */
+ key_stat->next = op_stat->search_stat->keys_lookup;
+ } else {
+ /* this is the first key stat record */
+ key_stat->next = NULL;
+ }
+ op_stat->search_stat->keys_lookup = key_stat;
+ }
#ifdef LDAP_ERROR_LOGGING
/* XXX if ( slapd_ldap_debug & LDAP_DEBUG_TRACE ) { XXX */
{
@@ -1080,5 +1124,10 @@ keys2idl(
}
}
+ /* All the keys have been fetch, time to take the completion time */
+ if (collect_stat) {
+ clock_gettime(CLOCK_MONOTONIC, &(op_stat->search_stat->keys_lookup_end));
+ }
+
return (idl);
}
diff --git a/ldap/servers/slapd/libglobs.c b/ldap/servers/slapd/libglobs.c
index 2097ab93c..99b2c5d8e 100644
--- a/ldap/servers/slapd/libglobs.c
+++ b/ldap/servers/slapd/libglobs.c
@@ -712,6 +712,10 @@ static struct config_get_and_set
NULL, 0,
(void **)&global_slapdFrontendConfig.accessloglevel,
CONFIG_INT, NULL, SLAPD_DEFAULT_ACCESSLOG_LEVEL_STR, NULL},
+ {CONFIG_STATLOGLEVEL_ATTRIBUTE, config_set_statlog_level,
+ NULL, 0,
+ (void **)&global_slapdFrontendConfig.statloglevel,
+ CONFIG_INT, NULL, SLAPD_DEFAULT_STATLOG_LEVEL, NULL},
{CONFIG_ERRORLOG_LOGROTATIONTIMEUNIT_ATTRIBUTE, NULL,
log_set_rotationtimeunit, SLAPD_ERROR_LOG,
(void **)&global_slapdFrontendConfig.errorlog_rotationunit,
@@ -1748,6 +1752,7 @@ FrontendConfig_init(void)
cfg->accessloglevel = SLAPD_DEFAULT_ACCESSLOG_LEVEL;
init_accesslogbuffering = cfg->accesslogbuffering = LDAP_ON;
init_csnlogging = cfg->csnlogging = LDAP_ON;
+ cfg->statloglevel = SLAPD_DEFAULT_STATLOG_LEVEL;
init_errorlog_logging_enabled = cfg->errorlog_logging_enabled = LDAP_ON;
init_external_libs_debug_enabled = cfg->external_libs_debug_enabled = LDAP_OFF;
@@ -5382,6 +5387,38 @@ config_set_accesslog_level(const char *attrname, char *value, char *errorbuf, in
return retVal;
}
+int
+config_set_statlog_level(const char *attrname, char *value, char *errorbuf, int apply)
+{
+ int retVal = LDAP_SUCCESS;
+ long level = 0;
+ char *endp = NULL;
+
+ slapdFrontendConfig_t *slapdFrontendConfig = getFrontendConfig();
+
+ if (config_value_is_null(attrname, value, errorbuf, 1)) {
+ return LDAP_OPERATIONS_ERROR;
+ }
+
+ errno = 0;
+ level = strtol(value, &endp, 10);
+
+ if (*endp != '\0' || errno == ERANGE || level < 0) {
+ slapi_create_errormsg(errorbuf, SLAPI_DSE_RETURNTEXT_SIZE, "%s: stat log level \"%s\" is invalid,"
+ " access log level must range from 0 to %lld",
+ attrname, value, (long long int)LONG_MAX);
+ retVal = LDAP_OPERATIONS_ERROR;
+ return retVal;
+ }
+
+ if (apply) {
+ CFG_LOCK_WRITE(slapdFrontendConfig);
+ g_set_statlog_level(level);
+ slapdFrontendConfig->statloglevel = level;
+ CFG_UNLOCK_WRITE(slapdFrontendConfig);
+ }
+ return retVal;
+}
/* set the referral-mode url (which puts us into referral mode) */
int
config_set_referral_mode(const char *attrname __attribute__((unused)), char *url, char *errorbuf, int apply)
@@ -6612,6 +6649,17 @@ config_get_accesslog_level()
return retVal;
}
+int
+config_get_statlog_level()
+{
+ slapdFrontendConfig_t *slapdFrontendConfig = getFrontendConfig();
+ int retVal;
+
+ retVal = slapdFrontendConfig->statloglevel;
+
+ return retVal;
+}
+
/* return integer -- don't worry about locking similar to config_check_referral_mode
below */
diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c
index 8074735e2..837a9c6fd 100644
--- a/ldap/servers/slapd/log.c
+++ b/ldap/servers/slapd/log.c
@@ -233,6 +233,17 @@ g_set_accesslog_level(int val)
LOG_ACCESS_UNLOCK_WRITE();
}
+/******************************************************************************
+* Set the stat level
+******************************************************************************/
+void
+g_set_statlog_level(int val)
+{
+ LOG_ACCESS_LOCK_WRITE();
+ loginfo.log_access_stat_level = val;
+ LOG_ACCESS_UNLOCK_WRITE();
+}
+
/******************************************************************************
* Set whether the process is alive or dead
* If it is detached, then we write the error in 'stderr'
@@ -283,6 +294,7 @@ g_log_init()
if ((loginfo.log_access_buffer->lock = PR_NewLock()) == NULL) {
exit(-1);
}
+ loginfo.log_access_stat_level = cfg->statloglevel;
/* ERROR LOG */
loginfo.log_error_state = cfg->errorlog_logging_enabled;
@@ -2640,7 +2652,21 @@ vslapd_log_access(char *fmt, va_list ap)
return (rc);
}
+int
+slapi_log_stat(int loglevel, const char *fmt, ...)
+{
+ char buf[2048];
+ va_list args;
+ int rc = LDAP_SUCCESS;
+ if (loglevel & loginfo.log_access_stat_level) {
+ va_start(args, fmt);
+ PR_vsnprintf(buf, sizeof(buf), fmt, args);
+ rc = slapi_log_access(LDAP_DEBUG_STATS, "%s", buf);
+ va_end(args);
+ }
+ return rc;
+}
int
slapi_log_access(int level,
char *fmt,
diff --git a/ldap/servers/slapd/log.h b/ldap/servers/slapd/log.h
index 9fb4e7425..6ac37bd29 100644
--- a/ldap/servers/slapd/log.h
+++ b/ldap/servers/slapd/log.h
@@ -120,6 +120,7 @@ struct logging_opts
int log_access_exptime; /* time */
int log_access_exptimeunit; /* unit time */
int log_access_exptime_secs; /* time in secs */
+ int log_access_stat_level; /* statistics level in access log file */
int log_access_level; /* access log level */
char *log_access_file; /* access log file path */
diff --git a/ldap/servers/slapd/main.c b/ldap/servers/slapd/main.c
index ac45c85d1..9b5b845cb 100644
--- a/ldap/servers/slapd/main.c
+++ b/ldap/servers/slapd/main.c
@@ -1040,6 +1040,7 @@ main(int argc, char **argv)
* changes are replicated as soon as the replication plugin is started.
*/
pw_exp_init();
+ op_stat_init();
plugin_print_lists();
plugin_startall(argc, argv, NULL /* specific plugin list */);
diff --git a/ldap/servers/slapd/operation.c b/ldap/servers/slapd/operation.c
index 4dd3481c7..dacd1838f 100644
--- a/ldap/servers/slapd/operation.c
+++ b/ldap/servers/slapd/operation.c
@@ -652,6 +652,92 @@ slapi_operation_time_expiry(Slapi_Operation *o, time_t timeout, struct timespec
slapi_timespec_expire_rel(timeout, &(o->o_hr_time_rel), expiry);
}
+
+/*
+ * Operation extension for operation statistics
+ */
+static int op_stat_objtype = -1;
+static int op_stat_handle = -1;
+
+Op_stat *
+op_stat_get_operation_extension(Slapi_PBlock *pb)
+{
+ Slapi_Operation *op;
+
+ slapi_pblock_get(pb, SLAPI_OPERATION, &op);
+ return (Op_stat *)slapi_get_object_extension(op_stat_objtype,
+ op, op_stat_handle);
+}
+
+void
+op_stat_set_operation_extension(Slapi_PBlock *pb, Op_stat *op_stat)
+{
+ Slapi_Operation *op;
+
+ slapi_pblock_get(pb, SLAPI_OPERATION, &op);
+ slapi_set_object_extension(op_stat_objtype, op,
+ op_stat_handle, (void *)op_stat);
+}
+
+/*
+ * constructor for the operation object extension.
+ */
+static void *
+op_stat_constructor(void *object __attribute__((unused)), void *parent __attribute__((unused)))
+{
+ Op_stat *op_statp = NULL;
+ op_statp = (Op_stat *)slapi_ch_calloc(1, sizeof(Op_stat));
+ op_statp->search_stat = (Op_search_stat *)slapi_ch_calloc(1, sizeof(Op_search_stat));
+
+ return op_statp;
+}
+/*
+ * destructor for the operation object extension.
+ */
+static void
+op_stat_destructor(void *extension, void *object __attribute__((unused)), void *parent __attribute__((unused)))
+{
+ Op_stat *op_statp = (Op_stat *)extension;
+
+ if (NULL == op_statp) {
+ return;
+ }
+
+ if (op_statp->search_stat) {
+ struct component_keys_lookup *keys, *next;
+
+ /* free all the individual key counter */
+ keys = op_statp->search_stat->keys_lookup;
+ while (keys) {
+ next = keys->next;
+ slapi_ch_free_string(&keys->attribute_type);
+ slapi_ch_free_string(&keys->key);
+ slapi_ch_free_string(&keys->index_type);
+ slapi_ch_free((void **) &keys);
+ keys = next;
+ }
+ slapi_ch_free((void **) &op_statp->search_stat);
+ }
+ slapi_ch_free((void **) &op_statp);
+}
+
+#define SLAPI_OP_STAT_MODULE "Module to collect operation stat"
+/* Called once from main */
+void
+op_stat_init(void)
+{
+ if (slapi_register_object_extension(SLAPI_OP_STAT_MODULE,
+ SLAPI_EXT_OPERATION,
+ op_stat_constructor,
+ op_stat_destructor,
+ &op_stat_objtype,
+ &op_stat_handle) != 0) {
+ slapi_log_err(SLAPI_LOG_ERR, "op_stat_init",
+ "slapi_register_object_extension failed; "
+ "operation statistics is not enabled\n");
+ }
+}
+
/* Set the time the operation actually started */
void
slapi_operation_set_time_started(Slapi_Operation *o)
diff --git a/ldap/servers/slapd/proto-slap.h b/ldap/servers/slapd/proto-slap.h
index 410a3c5fe..3a049ee76 100644
--- a/ldap/servers/slapd/proto-slap.h
+++ b/ldap/servers/slapd/proto-slap.h
@@ -291,6 +291,7 @@ int config_set_defaultreferral(const char *attrname, struct berval **value, char
int config_set_timelimit(const char *attrname, char *value, char *errorbuf, int apply);
int config_set_errorlog_level(const char *attrname, char *value, char *errorbuf, int apply);
int config_set_accesslog_level(const char *attrname, char *value, char *errorbuf, int apply);
+int config_set_statlog_level(const char *attrname, char *value, char *errorbuf, int apply);
int config_set_auditlog(const char *attrname, char *value, char *errorbuf, int apply);
int config_set_auditfaillog(const char *attrname, char *value, char *errorbuf, int apply);
int config_set_userat(const char *attrname, char *value, char *errorbuf, int apply);
@@ -510,6 +511,7 @@ long long config_get_pw_minage(void);
long long config_get_pw_warning(void);
int config_get_errorlog_level(void);
int config_get_accesslog_level(void);
+int config_get_statlog_level();
int config_get_auditlog_logging_enabled(void);
int config_get_auditfaillog_logging_enabled(void);
char *config_get_auditlog_display_attrs(void);
@@ -815,10 +817,15 @@ int lock_fclose(FILE *fp, FILE *lfp);
#define LDAP_DEBUG_INFO 0x08000000 /* 134217728 */
#define LDAP_DEBUG_DEBUG 0x10000000 /* 268435456 */
#define LDAP_DEBUG_ALL_LEVELS 0xFFFFFF
+
+#define LDAP_STAT_READ_INDEX 0x00000001 /* 1 */
+#define LDAP_STAT_FREE_1 0x00000002 /* 2 */
+
extern int slapd_ldap_debug;
int loglevel_is_set(int level);
int slapd_log_error_proc(int sev_level, char *subsystem, char *fmt, ...);
+int slapi_log_stat(int loglevel, const char *fmt, ...);
int slapi_log_access(int level, char *fmt, ...)
#ifdef __GNUC__
@@ -874,6 +881,7 @@ int check_log_max_size(
void g_set_accesslog_level(int val);
+void g_set_statlog_level(int val);
void log__delete_rotated_logs(void);
/*
diff --git a/ldap/servers/slapd/result.c b/ldap/servers/slapd/result.c
index adcef9539..e94533d72 100644
--- a/ldap/servers/slapd/result.c
+++ b/ldap/servers/slapd/result.c
@@ -38,6 +38,7 @@ static PRLock *current_conn_count_mutex;
static int flush_ber(Slapi_PBlock *pb, Connection *conn, Operation *op, BerElement *ber, int type);
static char *notes2str(unsigned int notes, char *buf, size_t buflen);
+static void log_op_stat(Slapi_PBlock *pb);
static void log_result(Slapi_PBlock *pb, Operation *op, int err, ber_tag_t tag, int nentries);
static void log_entry(Operation *op, Slapi_Entry *e);
static void log_referral(Operation *op);
@@ -2050,6 +2051,68 @@ notes2str(unsigned int notes, char *buf, size_t buflen)
return (buf);
}
+static void
+log_op_stat(Slapi_PBlock *pb)
+{
+
+ Connection *conn = NULL;
+ Operation *op = NULL;
+ Op_stat *op_stat;
+ struct timespec duration;
+ char stat_etime[ETIME_BUFSIZ] = {0};
+
+ if (config_get_statlog_level() == 0) {
+ return;
+ }
+
+ slapi_pblock_get(pb, SLAPI_CONNECTION, &conn);
+ slapi_pblock_get(pb, SLAPI_OPERATION, &op);
+ op_stat = op_stat_get_operation_extension(pb);
+
+ if (conn == NULL || op == NULL || op_stat == NULL) {
+ return;
+ }
+ /* process the operation */
+ switch (op->o_tag) {
+ case LDAP_REQ_BIND:
+ case LDAP_REQ_UNBIND:
+ case LDAP_REQ_ADD:
+ case LDAP_REQ_DELETE:
+ case LDAP_REQ_MODRDN:
+ case LDAP_REQ_MODIFY:
+ case LDAP_REQ_COMPARE:
+ break;
+ case LDAP_REQ_SEARCH:
+ if ((LDAP_STAT_READ_INDEX & config_get_statlog_level()) &&
+ op_stat->search_stat) {
+ struct component_keys_lookup *key_info;
+ for (key_info = op_stat->search_stat->keys_lookup; key_info; key_info = key_info->next) {
+ slapi_log_stat(LDAP_STAT_READ_INDEX,
+ "conn=%" PRIu64 " op=%d STAT read index: attribute=%s key(%s)=%s --> count %d\n",
+ op->o_connid, op->o_opid,
+ key_info->attribute_type, key_info->index_type, key_info->key,
+ key_info->id_lookup_cnt);
+ }
+
+ /* total elapsed time */
+ slapi_timespec_diff(&op_stat->search_stat->keys_lookup_end, &op_stat->search_stat->keys_lookup_start, &duration);
+ snprintf(stat_etime, ETIME_BUFSIZ, "%" PRId64 ".%.09" PRId64 "", (int64_t)duration.tv_sec, (int64_t)duration.tv_nsec);
+ slapi_log_stat(LDAP_STAT_READ_INDEX,
+ "conn=%" PRIu64 " op=%d STAT read index: duration %s\n",
+ op->o_connid, op->o_opid, stat_etime);
+ }
+ break;
+ case LDAP_REQ_ABANDON_30:
+ case LDAP_REQ_ABANDON:
+ break;
+
+ default:
+ slapi_log_err(SLAPI_LOG_ERR,
+ "log_op_stat", "Ignoring unknown LDAP request (conn=%" PRIu64 ", tag=0x%lx)\n",
+ conn->c_connid, op->o_tag);
+ break;
+ }
+}
static void
log_result(Slapi_PBlock *pb, Operation *op, int err, ber_tag_t tag, int nentries)
@@ -2206,6 +2269,7 @@ log_result(Slapi_PBlock *pb, Operation *op, int err, ber_tag_t tag, int nentries
} else {
ext_str = "";
}
+ log_op_stat(pb);
slapi_log_access(LDAP_DEBUG_STATS,
"conn=%" PRIu64 " op=%d RESULT err=%d"
" tag=%" BERTAG_T " nentries=%d wtime=%s optime=%s etime=%s%s%s%s\n",
diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h
index 927576b70..82550527c 100644
--- a/ldap/servers/slapd/slap.h
+++ b/ldap/servers/slapd/slap.h
@@ -348,6 +348,8 @@ typedef void (*VFPV)(); /* takes undefined arguments */
#define SLAPD_DEFAULT_FE_ERRORLOG_LEVEL_STR "16384"
#define SLAPD_DEFAULT_ACCESSLOG_LEVEL 256
#define SLAPD_DEFAULT_ACCESSLOG_LEVEL_STR "256"
+#define SLAPD_DEFAULT_STATLOG_LEVEL 0
+#define SLAPD_DEFAULT_STATLOG_LEVEL_STR "0"
#define SLAPD_DEFAULT_DISK_THRESHOLD 2097152
#define SLAPD_DEFAULT_DISK_THRESHOLD_STR "2097152"
@@ -2082,6 +2084,7 @@ typedef struct _slapdEntryPoints
#define CONFIG_SCHEMAREPLACE_ATTRIBUTE "nsslapd-schemareplace"
#define CONFIG_LOGLEVEL_ATTRIBUTE "nsslapd-errorlog-level"
#define CONFIG_ACCESSLOGLEVEL_ATTRIBUTE "nsslapd-accesslog-level"
+#define CONFIG_STATLOGLEVEL_ATTRIBUTE "nsslapd-statlog-level"
#define CONFIG_ACCESSLOG_MODE_ATTRIBUTE "nsslapd-accesslog-mode"
#define CONFIG_ERRORLOG_MODE_ATTRIBUTE "nsslapd-errorlog-mode"
#define CONFIG_AUDITLOG_MODE_ATTRIBUTE "nsslapd-auditlog-mode"
@@ -2457,6 +2460,7 @@ typedef struct _slapdFrontendConfig
int accessloglevel;
slapi_onoff_t accesslogbuffering;
slapi_onoff_t csnlogging;
+ int statloglevel;
/* ERROR LOG */
slapi_onoff_t errorlog_logging_enabled;
diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h
index 4b6cf29eb..bd7a4b39d 100644
--- a/ldap/servers/slapd/slapi-private.h
+++ b/ldap/servers/slapd/slapi-private.h
@@ -449,6 +449,33 @@ int operation_is_flag_set(Slapi_Operation *op, int flag);
unsigned long operation_get_type(Slapi_Operation *op);
LDAPMod **copy_mods(LDAPMod **orig_mods);
+/* Structures use to collect statistics per operation */
+/* used for LDAP_STAT_READ_INDEX */
+struct component_keys_lookup
+{
+ char *index_type;
+ char *attribute_type;
+ char *key;
+ int id_lookup_cnt;
+ struct component_keys_lookup *next;
+};
+typedef struct op_search_stat
+{
+ struct component_keys_lookup *keys_lookup;
+ struct timespec keys_lookup_start;
+ struct timespec keys_lookup_end;
+} Op_search_stat;
+
+/* structure store in the operation extension */
+typedef struct op_stat
+{
+ Op_search_stat *search_stat;
+} Op_stat;
+
+void op_stat_init(void);
+Op_stat *op_stat_get_operation_extension(Slapi_PBlock *pb);
+void op_stat_set_operation_extension(Slapi_PBlock *pb, Op_stat *op_stat);
+
/*
* From ldap.h
* #define LDAP_MOD_ADD 0x00
--
2.51.1

View File

@ -1,297 +0,0 @@
From bc2629db166667cdb01fde2b9e249253d5d868b5 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Mon, 21 Nov 2022 11:41:15 +0100
Subject: [PATCH] Issue 3729 - (cont) RFE Extend log of operations statistics
in access log (#5538)
Bug description:
This is a continuation of the #3729
The previous fix did not manage internal SRCH, so
statistics of internal SRCH were not logged
Fix description:
For internal operation log_op_stat uses
connid/op_id/op_internal_id/op_nested_count that have been
computed log_result
For direct operation log_op_stat uses info from the
operation itself (o_connid and o_opid)
log_op_stat relies on operation_type rather than
o_tag that is not available for internal operation
relates: #3729
Reviewed by: Pierre Rogier
(cherry picked from commit 7915e85a55476647ac54330de4f6e89faf6f2934)
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
.../tests/suites/ds_logs/ds_logs_test.py | 90 ++++++++++++++++++-
ldap/servers/slapd/proto-slap.h | 2 +-
ldap/servers/slapd/result.c | 74 +++++++++------
3 files changed, 136 insertions(+), 30 deletions(-)
diff --git a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
index 43288f67f..fbb8d7bf1 100644
--- a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
+++ b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py
@@ -21,7 +21,7 @@ from lib389.idm.domain import Domain
from lib389.configurations.sample import create_base_domain
from lib389._mapped_object import DSLdapObject
from lib389.topologies import topology_st
-from lib389.plugins import AutoMembershipPlugin, ReferentialIntegrityPlugin, AutoMembershipDefinitions
+from lib389.plugins import AutoMembershipPlugin, ReferentialIntegrityPlugin, AutoMembershipDefinitions, MemberOfPlugin
from lib389.idm.user import UserAccounts, UserAccount
from lib389.idm.group import Groups
from lib389.idm.organizationalunit import OrganizationalUnits
@@ -1323,6 +1323,94 @@ def test_stat_index(topology_st, request):
request.addfinalizer(fin)
+def test_stat_internal_op(topology_st, request):
+ """Check that statistics can also be collected for internal operations
+
+ :id: 19f393bd-5866-425a-af7a-4dade06d5c77
+ :setup: Standalone Instance
+ :steps:
+ 1. Check that nsslapd-statlog-level is 0 (default)
+ 2. Enable memberof plugins
+ 3. Create a user
+ 4. Remove access log (to only detect new records)
+ 5. Enable statistic logging nsslapd-statlog-level=1
+ 6. Check that on direct SRCH there is no 'Internal' Stat records
+ 7. Remove access log (to only detect new records)
+ 8. Add group with the user, so memberof triggers internal search
+ and check it exists 'Internal' Stat records
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. Success
+ 6. Success
+ 7. Success
+ 8. Success
+ """
+
+ inst = topology_st.standalone
+
+ # Step 1
+ log.info("Assert nsslapd-statlog-level is by default 0")
+ assert topology_st.standalone.config.get_attr_val_int("nsslapd-statlog-level") == 0
+
+ # Step 2
+ memberof = MemberOfPlugin(inst)
+ memberof.enable()
+ inst.restart()
+
+ # Step 3 Add setup entries
+ users = UserAccounts(inst, DEFAULT_SUFFIX, rdn=None)
+ user = users.create(properties={'uid': 'test_1',
+ 'cn': 'test_1',
+ 'sn': 'test_1',
+ 'description': 'member',
+ 'uidNumber': '1000',
+ 'gidNumber': '2000',
+ 'homeDirectory': '/home/testuser'})
+ # Step 4 reset accesslog
+ topology_st.standalone.stop()
+ lpath = topology_st.standalone.ds_access_log._get_log_path()
+ os.unlink(lpath)
+ topology_st.standalone.start()
+
+ # Step 5 enable statistics
+ log.info("Set nsslapd-statlog-level: 1 to enable indexing statistics")
+ topology_st.standalone.config.set("nsslapd-statlog-level", "1")
+
+ # Step 6 for direct SRCH only non internal STAT records
+ entries = topology_st.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "uid=test_1")
+ topology_st.standalone.stop()
+ assert topology_st.standalone.ds_access_log.match('.*STAT read index.*')
+ assert topology_st.standalone.ds_access_log.match('.*STAT read index: attribute.*')
+ assert topology_st.standalone.ds_access_log.match('.*STAT read index: duration.*')
+ assert not topology_st.standalone.ds_access_log.match('.*Internal.*STAT.*')
+ topology_st.standalone.start()
+
+ # Step 7 reset accesslog
+ topology_st.standalone.stop()
+ lpath = topology_st.standalone.ds_access_log._get_log_path()
+ os.unlink(lpath)
+ topology_st.standalone.start()
+
+ # Step 8 trigger internal searches and check internal stat records
+ groups = Groups(inst, DEFAULT_SUFFIX, rdn=None)
+ group = groups.create(properties={'cn': 'mygroup',
+ 'member': 'uid=test_1,%s' % DEFAULT_SUFFIX,
+ 'description': 'group'})
+ topology_st.standalone.restart()
+ assert topology_st.standalone.ds_access_log.match('.*Internal.*STAT read index.*')
+ assert topology_st.standalone.ds_access_log.match('.*Internal.*STAT read index: attribute.*')
+ assert topology_st.standalone.ds_access_log.match('.*Internal.*STAT read index: duration.*')
+
+ def fin():
+ log.info('Deleting user/group')
+ user.delete()
+ group.delete()
+
+ request.addfinalizer(fin)
+
if __name__ == '__main__':
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/proto-slap.h b/ldap/servers/slapd/proto-slap.h
index 3a049ee76..6e473a08e 100644
--- a/ldap/servers/slapd/proto-slap.h
+++ b/ldap/servers/slapd/proto-slap.h
@@ -511,7 +511,7 @@ long long config_get_pw_minage(void);
long long config_get_pw_warning(void);
int config_get_errorlog_level(void);
int config_get_accesslog_level(void);
-int config_get_statlog_level();
+int config_get_statlog_level(void);
int config_get_auditlog_logging_enabled(void);
int config_get_auditfaillog_logging_enabled(void);
char *config_get_auditlog_display_attrs(void);
diff --git a/ldap/servers/slapd/result.c b/ldap/servers/slapd/result.c
index e94533d72..87641e92f 100644
--- a/ldap/servers/slapd/result.c
+++ b/ldap/servers/slapd/result.c
@@ -38,7 +38,7 @@ static PRLock *current_conn_count_mutex;
static int flush_ber(Slapi_PBlock *pb, Connection *conn, Operation *op, BerElement *ber, int type);
static char *notes2str(unsigned int notes, char *buf, size_t buflen);
-static void log_op_stat(Slapi_PBlock *pb);
+static void log_op_stat(Slapi_PBlock *pb, uint64_t connid, int32_t op_id, int32_t op_internal_id, int32_t op_nested_count);
static void log_result(Slapi_PBlock *pb, Operation *op, int err, ber_tag_t tag, int nentries);
static void log_entry(Operation *op, Slapi_Entry *e);
static void log_referral(Operation *op);
@@ -2051,65 +2051,82 @@ notes2str(unsigned int notes, char *buf, size_t buflen)
return (buf);
}
+#define STAT_LOG_CONN_OP_FMT_INT_INT "conn=Internal(%" PRIu64 ") op=%d(%d)(%d)"
+#define STAT_LOG_CONN_OP_FMT_EXT_INT "conn=%" PRIu64 " (Internal) op=%d(%d)(%d)"
static void
-log_op_stat(Slapi_PBlock *pb)
+log_op_stat(Slapi_PBlock *pb, uint64_t connid, int32_t op_id, int32_t op_internal_id, int32_t op_nested_count)
{
-
- Connection *conn = NULL;
Operation *op = NULL;
Op_stat *op_stat;
struct timespec duration;
char stat_etime[ETIME_BUFSIZ] = {0};
+ int internal_op;
if (config_get_statlog_level() == 0) {
return;
}
- slapi_pblock_get(pb, SLAPI_CONNECTION, &conn);
slapi_pblock_get(pb, SLAPI_OPERATION, &op);
+ internal_op = operation_is_flag_set(op, OP_FLAG_INTERNAL);
op_stat = op_stat_get_operation_extension(pb);
- if (conn == NULL || op == NULL || op_stat == NULL) {
+ if (op == NULL || op_stat == NULL) {
return;
}
/* process the operation */
- switch (op->o_tag) {
- case LDAP_REQ_BIND:
- case LDAP_REQ_UNBIND:
- case LDAP_REQ_ADD:
- case LDAP_REQ_DELETE:
- case LDAP_REQ_MODRDN:
- case LDAP_REQ_MODIFY:
- case LDAP_REQ_COMPARE:
+ switch (operation_get_type(op)) {
+ case SLAPI_OPERATION_BIND:
+ case SLAPI_OPERATION_UNBIND:
+ case SLAPI_OPERATION_ADD:
+ case SLAPI_OPERATION_DELETE:
+ case SLAPI_OPERATION_MODRDN:
+ case SLAPI_OPERATION_MODIFY:
+ case SLAPI_OPERATION_COMPARE:
+ case SLAPI_OPERATION_EXTENDED:
break;
- case LDAP_REQ_SEARCH:
+ case SLAPI_OPERATION_SEARCH:
if ((LDAP_STAT_READ_INDEX & config_get_statlog_level()) &&
op_stat->search_stat) {
struct component_keys_lookup *key_info;
for (key_info = op_stat->search_stat->keys_lookup; key_info; key_info = key_info->next) {
- slapi_log_stat(LDAP_STAT_READ_INDEX,
- "conn=%" PRIu64 " op=%d STAT read index: attribute=%s key(%s)=%s --> count %d\n",
- op->o_connid, op->o_opid,
- key_info->attribute_type, key_info->index_type, key_info->key,
- key_info->id_lookup_cnt);
+ if (internal_op) {
+ slapi_log_stat(LDAP_STAT_READ_INDEX,
+ connid == 0 ? STAT_LOG_CONN_OP_FMT_INT_INT "STAT read index: attribute=%s key(%s)=%s --> count %d\n":
+ STAT_LOG_CONN_OP_FMT_EXT_INT "STAT read index: attribute=%s key(%s)=%s --> count %d\n",
+ connid, op_id, op_internal_id, op_nested_count,
+ key_info->attribute_type, key_info->index_type, key_info->key,
+ key_info->id_lookup_cnt);
+ } else {
+ slapi_log_stat(LDAP_STAT_READ_INDEX,
+ "conn=%" PRIu64 " op=%d STAT read index: attribute=%s key(%s)=%s --> count %d\n",
+ connid, op_id,
+ key_info->attribute_type, key_info->index_type, key_info->key,
+ key_info->id_lookup_cnt);
+ }
}
/* total elapsed time */
slapi_timespec_diff(&op_stat->search_stat->keys_lookup_end, &op_stat->search_stat->keys_lookup_start, &duration);
snprintf(stat_etime, ETIME_BUFSIZ, "%" PRId64 ".%.09" PRId64 "", (int64_t)duration.tv_sec, (int64_t)duration.tv_nsec);
- slapi_log_stat(LDAP_STAT_READ_INDEX,
- "conn=%" PRIu64 " op=%d STAT read index: duration %s\n",
- op->o_connid, op->o_opid, stat_etime);
+ if (internal_op) {
+ slapi_log_stat(LDAP_STAT_READ_INDEX,
+ connid == 0 ? STAT_LOG_CONN_OP_FMT_INT_INT "STAT read index: duration %s\n":
+ STAT_LOG_CONN_OP_FMT_EXT_INT "STAT read index: duration %s\n",
+ connid, op_id, op_internal_id, op_nested_count, stat_etime);
+ } else {
+ slapi_log_stat(LDAP_STAT_READ_INDEX,
+ "conn=%" PRIu64 " op=%d STAT read index: duration %s\n",
+ op->o_connid, op->o_opid, stat_etime);
+ }
}
break;
- case LDAP_REQ_ABANDON_30:
- case LDAP_REQ_ABANDON:
+ case SLAPI_OPERATION_ABANDON:
break;
default:
slapi_log_err(SLAPI_LOG_ERR,
- "log_op_stat", "Ignoring unknown LDAP request (conn=%" PRIu64 ", tag=0x%lx)\n",
- conn->c_connid, op->o_tag);
+ "log_op_stat", "Ignoring unknown LDAP request (conn=%" PRIu64 ", op_type=0x%lx)\n",
+ connid, operation_get_type(op));
break;
}
}
@@ -2269,7 +2286,7 @@ log_result(Slapi_PBlock *pb, Operation *op, int err, ber_tag_t tag, int nentries
} else {
ext_str = "";
}
- log_op_stat(pb);
+ log_op_stat(pb, op->o_connid, op->o_opid, 0, 0);
slapi_log_access(LDAP_DEBUG_STATS,
"conn=%" PRIu64 " op=%d RESULT err=%d"
" tag=%" BERTAG_T " nentries=%d wtime=%s optime=%s etime=%s%s%s%s\n",
@@ -2284,6 +2301,7 @@ log_result(Slapi_PBlock *pb, Operation *op, int err, ber_tag_t tag, int nentries
}
} else {
int optype;
+ log_op_stat(pb, connid, op_id, op_internal_id, op_nested_count);
#define LOG_MSG_FMT " tag=%" BERTAG_T " nentries=%d wtime=%s optime=%s etime=%s%s%s\n"
slapi_log_access(LDAP_DEBUG_ARGS,
connid == 0 ? LOG_CONN_OP_FMT_INT_INT LOG_MSG_FMT :
--
2.51.1

View File

@ -1,124 +0,0 @@
From f6eca13762139538d974c1cb285ddf1354fe7837 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Tue, 28 Mar 2023 10:27:01 +0200
Subject: [PATCH] Issue 5710 - subtree search statistics for index lookup does
not report ancestorid/entryrdn lookups (#5711)
Bug description:
The RFE #3729 allows to collect index lookups per search
operation. For subtree searches the server lookup ancestorid
and those lookup are not recorded
Fix description:
if statistics are enabled, record ancestorid lookup
relates: #5710
Reviewed by: Mark Reynolds (thanks)
(cherry picked from commit fca27c3d0487c9aea9dc7da151a79e3ce0fc7d35)
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
ldap/servers/slapd/back-ldbm/ldbm_search.c | 59 ++++++++++++++++++++++
1 file changed, 59 insertions(+)
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_search.c b/ldap/servers/slapd/back-ldbm/ldbm_search.c
index 8c07d1395..5d98e288e 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_search.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_search.c
@@ -35,6 +35,7 @@ static IDList *onelevel_candidates(Slapi_PBlock *pb, backend *be, const char *ba
static back_search_result_set *new_search_result_set(IDList *idl, int vlv, int lookthroughlimit);
static void delete_search_result_set(Slapi_PBlock *pb, back_search_result_set **sr);
static int can_skip_filter_test(Slapi_PBlock *pb, struct slapi_filter *f, int scope, IDList *idl);
+static void stat_add_srch_lookup(Op_stat *op_stat, char * attribute_type, const char* index_type, char *key_value, int lookup_cnt);
/* This is for performance testing, allows us to disable ACL checking altogether */
#if defined(DISABLE_ACL_CHECK)
@@ -1167,6 +1168,45 @@ create_subtree_filter(Slapi_Filter *filter, int managedsait, Slapi_Filter **focr
return ftop;
}
+static void
+stat_add_srch_lookup(Op_stat *op_stat, char * attribute_type, const char* index_type, char *key_value, int lookup_cnt)
+{
+ struct component_keys_lookup *key_stat;
+
+ if ((op_stat == NULL) || (op_stat->search_stat == NULL)) {
+ return;
+ }
+
+ /* gather the index lookup statistics */
+ key_stat = (struct component_keys_lookup *) slapi_ch_calloc(1, sizeof (struct component_keys_lookup));
+
+ /* indextype is "eq" */
+ if (index_type) {
+ key_stat->index_type = slapi_ch_strdup(index_type);
+ }
+
+ /* key value e.g. '1234' */
+ if (key_value) {
+ key_stat->key = (char *) slapi_ch_calloc(1, strlen(key_value) + 1);
+ memcpy(key_stat->key, key_value, strlen(key_value));
+ }
+
+ /* attribute name is e.g. 'uid' */
+ if (attribute_type) {
+ key_stat->attribute_type = slapi_ch_strdup(attribute_type);
+ }
+
+ /* Number of lookup IDs with the key */
+ key_stat->id_lookup_cnt = lookup_cnt;
+ if (op_stat->search_stat->keys_lookup) {
+ /* it already exist key stat. add key_stat at the head */
+ key_stat->next = op_stat->search_stat->keys_lookup;
+ } else {
+ /* this is the first key stat record */
+ key_stat->next = NULL;
+ }
+ op_stat->search_stat->keys_lookup = key_stat;
+}
/*
* Build a candidate list for a SUBTREE scope search.
@@ -1232,6 +1272,17 @@ subtree_candidates(
if (candidates != NULL && (idl_length(candidates) > FILTER_TEST_THRESHOLD) && e) {
IDList *tmp = candidates, *descendants = NULL;
back_txn txn = {NULL};
+ Op_stat *op_stat = NULL;
+ char key_value[32] = {0};
+
+ /* statistics for index lookup is enabled */
+ if (LDAP_STAT_READ_INDEX & config_get_statlog_level()) {
+ op_stat = op_stat_get_operation_extension(pb);
+ if (op_stat) {
+ /* easier to just record the entry ID */
+ PR_snprintf(key_value, sizeof(key_value), "%lu", (u_long) e->ep_id);
+ }
+ }
slapi_pblock_get(pb, SLAPI_TXN, &txn.back_txn_txn);
if (entryrdn_get_noancestorid()) {
@@ -1239,12 +1290,20 @@ subtree_candidates(
*err = entryrdn_get_subordinates(be,
slapi_entry_get_sdn_const(e->ep_entry),
e->ep_id, &descendants, &txn, 0);
+ if (op_stat) {
+ /* record entryrdn lookups */
+ stat_add_srch_lookup(op_stat, LDBM_ENTRYRDN_STR, indextype_EQUALITY, key_value, descendants ? descendants->b_nids : 0);
+ }
idl_insert(&descendants, e->ep_id);
candidates = idl_intersection(be, candidates, descendants);
idl_free(&tmp);
idl_free(&descendants);
} else if (!has_tombstone_filter && !is_bulk_import) {
*err = ldbm_ancestorid_read_ext(be, &txn, e->ep_id, &descendants, allidslimit);
+ if (op_stat) {
+ /* records ancestorid lookups */
+ stat_add_srch_lookup(op_stat, LDBM_ANCESTORID_STR, indextype_EQUALITY, key_value, descendants ? descendants->b_nids : 0);
+ }
idl_insert(&descendants, e->ep_id);
candidates = idl_intersection(be, candidates, descendants);
idl_free(&tmp);
--
2.51.1

View File

@ -1,249 +0,0 @@
From aced6f575f3be70f16756860f8b852d3447df867 Mon Sep 17 00:00:00 2001
From: tbordaz <tbordaz@redhat.com>
Date: Tue, 6 May 2025 16:09:36 +0200
Subject: [PATCH] Issue 6764 - statistics about index lookup report a wrong
duration (#6765)
Bug description:
During a SRCH statistics about indexes lookup
(when nsslapd-statlog-level=1) reports a duration.
It is wrong because it should report a duration per filter
component.
Fix description:
Record a index lookup duration per key
using key_lookup_start/key_lookup_end
fixes: #6764
Reviewed by: Pierre Rogier (Thanks !)
(cherry picked from commit cd8069a76bcbb2d7bb4ac3bb9466019b01cc6db3)
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
ldap/servers/slapd/back-ldbm/filterindex.c | 17 +++++++-----
ldap/servers/slapd/back-ldbm/ldbm_search.c | 31 +++++++++++++++-------
ldap/servers/slapd/result.c | 12 +++++----
ldap/servers/slapd/slapi-plugin.h | 9 +++++++
ldap/servers/slapd/slapi-private.h | 2 ++
ldap/servers/slapd/time.c | 13 +++++++++
6 files changed, 62 insertions(+), 22 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/filterindex.c b/ldap/servers/slapd/back-ldbm/filterindex.c
index 30550dde7..abc502b96 100644
--- a/ldap/servers/slapd/back-ldbm/filterindex.c
+++ b/ldap/servers/slapd/back-ldbm/filterindex.c
@@ -1040,8 +1040,7 @@ keys2idl(
int allidslimit)
{
IDList *idl = NULL;
- Op_stat *op_stat;
- PRBool collect_stat = PR_FALSE;
+ Op_stat *op_stat = NULL;
slapi_log_err(SLAPI_LOG_TRACE, "keys2idl", "=> type %s indextype %s\n", type, indextype);
@@ -1049,8 +1048,9 @@ keys2idl(
if (LDAP_STAT_READ_INDEX & config_get_statlog_level()) {
op_stat = op_stat_get_operation_extension(pb);
if (op_stat->search_stat) {
- collect_stat = PR_TRUE;
clock_gettime(CLOCK_MONOTONIC, &(op_stat->search_stat->keys_lookup_start));
+ } else {
+ op_stat = NULL;
}
}
@@ -1059,11 +1059,14 @@ keys2idl(
struct component_keys_lookup *key_stat;
int key_len;
- idl2 = index_read_ext_allids(pb, be, type, indextype, slapi_value_get_berval(ivals[i]), txn, err, unindexed, allidslimit);
- if (collect_stat) {
+ if (op_stat) {
/* gather the index lookup statistics */
key_stat = (struct component_keys_lookup *) slapi_ch_calloc(1, sizeof (struct component_keys_lookup));
-
+ clock_gettime(CLOCK_MONOTONIC, &(key_stat->key_lookup_start));
+ }
+ idl2 = index_read_ext_allids(pb, be, type, indextype, slapi_value_get_berval(ivals[i]), txn, err, unindexed, allidslimit);
+ if (op_stat) {
+ clock_gettime(CLOCK_MONOTONIC, &(key_stat->key_lookup_end));
/* indextype e.g. "eq" or "sub" (see index.c) */
if (indextype) {
key_stat->index_type = slapi_ch_strdup(indextype);
@@ -1125,7 +1128,7 @@ keys2idl(
}
/* All the keys have been fetch, time to take the completion time */
- if (collect_stat) {
+ if (op_stat) {
clock_gettime(CLOCK_MONOTONIC, &(op_stat->search_stat->keys_lookup_end));
}
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_search.c b/ldap/servers/slapd/back-ldbm/ldbm_search.c
index 5d98e288e..27301f453 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_search.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_search.c
@@ -35,7 +35,7 @@ static IDList *onelevel_candidates(Slapi_PBlock *pb, backend *be, const char *ba
static back_search_result_set *new_search_result_set(IDList *idl, int vlv, int lookthroughlimit);
static void delete_search_result_set(Slapi_PBlock *pb, back_search_result_set **sr);
static int can_skip_filter_test(Slapi_PBlock *pb, struct slapi_filter *f, int scope, IDList *idl);
-static void stat_add_srch_lookup(Op_stat *op_stat, char * attribute_type, const char* index_type, char *key_value, int lookup_cnt);
+static void stat_add_srch_lookup(Op_stat *op_stat, struct component_keys_lookup *key_stat, char * attribute_type, const char* index_type, char *key_value, int lookup_cnt);
/* This is for performance testing, allows us to disable ACL checking altogether */
#if defined(DISABLE_ACL_CHECK)
@@ -1169,17 +1169,12 @@ create_subtree_filter(Slapi_Filter *filter, int managedsait, Slapi_Filter **focr
}
static void
-stat_add_srch_lookup(Op_stat *op_stat, char * attribute_type, const char* index_type, char *key_value, int lookup_cnt)
+stat_add_srch_lookup(Op_stat *op_stat, struct component_keys_lookup *key_stat, char * attribute_type, const char* index_type, char *key_value, int lookup_cnt)
{
- struct component_keys_lookup *key_stat;
-
- if ((op_stat == NULL) || (op_stat->search_stat == NULL)) {
+ if ((op_stat == NULL) || (op_stat->search_stat == NULL) || (key_stat == NULL)) {
return;
}
- /* gather the index lookup statistics */
- key_stat = (struct component_keys_lookup *) slapi_ch_calloc(1, sizeof (struct component_keys_lookup));
-
/* indextype is "eq" */
if (index_type) {
key_stat->index_type = slapi_ch_strdup(index_type);
@@ -1286,23 +1281,39 @@ subtree_candidates(
slapi_pblock_get(pb, SLAPI_TXN, &txn.back_txn_txn);
if (entryrdn_get_noancestorid()) {
+ struct component_keys_lookup *key_stat;
+
+ if (op_stat) {
+ /* gather the index lookup statistics */
+ key_stat = (struct component_keys_lookup *) slapi_ch_calloc(1, sizeof (struct component_keys_lookup));
+ clock_gettime(CLOCK_MONOTONIC, &key_stat->key_lookup_start);
+ }
/* subtree-rename: on && no ancestorid */
*err = entryrdn_get_subordinates(be,
slapi_entry_get_sdn_const(e->ep_entry),
e->ep_id, &descendants, &txn, 0);
if (op_stat) {
+ clock_gettime(CLOCK_MONOTONIC, &key_stat->key_lookup_end);
/* record entryrdn lookups */
- stat_add_srch_lookup(op_stat, LDBM_ENTRYRDN_STR, indextype_EQUALITY, key_value, descendants ? descendants->b_nids : 0);
+ stat_add_srch_lookup(op_stat, key_stat, LDBM_ENTRYRDN_STR, indextype_EQUALITY, key_value, descendants ? descendants->b_nids : 0);
}
idl_insert(&descendants, e->ep_id);
candidates = idl_intersection(be, candidates, descendants);
idl_free(&tmp);
idl_free(&descendants);
} else if (!has_tombstone_filter && !is_bulk_import) {
+ struct component_keys_lookup *key_stat;
+
+ if (op_stat) {
+ /* gather the index lookup statistics */
+ key_stat = (struct component_keys_lookup *) slapi_ch_calloc(1, sizeof (struct component_keys_lookup));
+ clock_gettime(CLOCK_MONOTONIC, &key_stat->key_lookup_start);
+ }
*err = ldbm_ancestorid_read_ext(be, &txn, e->ep_id, &descendants, allidslimit);
if (op_stat) {
+ clock_gettime(CLOCK_MONOTONIC, &key_stat->key_lookup_end);
/* records ancestorid lookups */
- stat_add_srch_lookup(op_stat, LDBM_ANCESTORID_STR, indextype_EQUALITY, key_value, descendants ? descendants->b_nids : 0);
+ stat_add_srch_lookup(op_stat, key_stat, LDBM_ANCESTORID_STR, indextype_EQUALITY, key_value, descendants ? descendants->b_nids : 0);
}
idl_insert(&descendants, e->ep_id);
candidates = idl_intersection(be, candidates, descendants);
diff --git a/ldap/servers/slapd/result.c b/ldap/servers/slapd/result.c
index 87641e92f..f40556de8 100644
--- a/ldap/servers/slapd/result.c
+++ b/ldap/servers/slapd/result.c
@@ -2089,19 +2089,21 @@ log_op_stat(Slapi_PBlock *pb, uint64_t connid, int32_t op_id, int32_t op_interna
op_stat->search_stat) {
struct component_keys_lookup *key_info;
for (key_info = op_stat->search_stat->keys_lookup; key_info; key_info = key_info->next) {
+ slapi_timespec_diff(&key_info->key_lookup_end, &key_info->key_lookup_start, &duration);
+ snprintf(stat_etime, ETIME_BUFSIZ, "%" PRId64 ".%.09" PRId64 "", (int64_t)duration.tv_sec, (int64_t)duration.tv_nsec);
if (internal_op) {
slapi_log_stat(LDAP_STAT_READ_INDEX,
- connid == 0 ? STAT_LOG_CONN_OP_FMT_INT_INT "STAT read index: attribute=%s key(%s)=%s --> count %d\n":
- STAT_LOG_CONN_OP_FMT_EXT_INT "STAT read index: attribute=%s key(%s)=%s --> count %d\n",
+ connid == 0 ? STAT_LOG_CONN_OP_FMT_INT_INT "STAT read index: attribute=%s key(%s)=%s --> count %d (duration %s)\n":
+ STAT_LOG_CONN_OP_FMT_EXT_INT "STAT read index: attribute=%s key(%s)=%s --> count %d (duration %s)\n",
connid, op_id, op_internal_id, op_nested_count,
key_info->attribute_type, key_info->index_type, key_info->key,
- key_info->id_lookup_cnt);
+ key_info->id_lookup_cnt, stat_etime);
} else {
slapi_log_stat(LDAP_STAT_READ_INDEX,
- "conn=%" PRIu64 " op=%d STAT read index: attribute=%s key(%s)=%s --> count %d\n",
+ "conn=%" PRIu64 " op=%d STAT read index: attribute=%s key(%s)=%s --> count %d (duration %s)\n",
connid, op_id,
key_info->attribute_type, key_info->index_type, key_info->key,
- key_info->id_lookup_cnt);
+ key_info->id_lookup_cnt, stat_etime);
}
}
diff --git a/ldap/servers/slapd/slapi-plugin.h b/ldap/servers/slapd/slapi-plugin.h
index a84a60c92..00e9722d2 100644
--- a/ldap/servers/slapd/slapi-plugin.h
+++ b/ldap/servers/slapd/slapi-plugin.h
@@ -8314,6 +8314,15 @@ void DS_Sleep(PRIntervalTime ticks);
* \param struct timespec c the difference.
*/
void slapi_timespec_diff(struct timespec *a, struct timespec *b, struct timespec *diff);
+
+/**
+ * add 'new' timespect into 'cumul'
+ * clock_monotonic to find time taken to perform operations.
+ *
+ * \param struct timespec cumul to compute total duration.
+ * \param struct timespec new is a additional duration
+ */
+void slapi_timespec_add(struct timespec *cumul, struct timespec *new);
/**
* Given an operation, determine the time elapsed since the op
* began.
diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h
index bd7a4b39d..dfb0e272a 100644
--- a/ldap/servers/slapd/slapi-private.h
+++ b/ldap/servers/slapd/slapi-private.h
@@ -457,6 +457,8 @@ struct component_keys_lookup
char *attribute_type;
char *key;
int id_lookup_cnt;
+ struct timespec key_lookup_start;
+ struct timespec key_lookup_end;
struct component_keys_lookup *next;
};
typedef struct op_search_stat
diff --git a/ldap/servers/slapd/time.c b/ldap/servers/slapd/time.c
index 0406c3689..0dd457fbe 100644
--- a/ldap/servers/slapd/time.c
+++ b/ldap/servers/slapd/time.c
@@ -272,6 +272,19 @@ slapi_timespec_diff(struct timespec *a, struct timespec *b, struct timespec *dif
diff->tv_nsec = nsec;
}
+void
+slapi_timespec_add(struct timespec *cumul, struct timespec *new)
+{
+ /* Now add the two */
+ time_t sec = cumul->tv_sec + new->tv_sec;
+ long nsec = cumul->tv_nsec + new->tv_nsec;
+
+ sec += nsec / 1000000000;
+ nsec = nsec % 1000000000;
+ cumul->tv_sec = sec;
+ cumul->tv_nsec = nsec;
+}
+
void
slapi_timespec_expire_at(time_t timeout, struct timespec *expire)
{
--
2.51.1

View File

@ -1,82 +0,0 @@
From c8a5594efdb2722b6dceaed16219039d8e59c888 Mon Sep 17 00:00:00 2001
From: Thierry Bordaz <tbordaz@redhat.com>
Date: Thu, 5 Jun 2025 10:33:29 +0200
Subject: [PATCH] Issue 6470 (Cont) - Some replication status data are reset
upon a restart
(cherry picked from commit a8b419dab31f4fa9fca8c33fe04a79e7a34965e5)
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
ldap/servers/plugins/replication/repl5_agmt.c | 4 ++--
ldap/servers/slapd/slapi-plugin.h | 8 --------
ldap/servers/slapd/time.c | 13 -------------
3 files changed, 2 insertions(+), 23 deletions(-)
diff --git a/ldap/servers/plugins/replication/repl5_agmt.c b/ldap/servers/plugins/replication/repl5_agmt.c
index c3b8d298c..229783763 100644
--- a/ldap/servers/plugins/replication/repl5_agmt.c
+++ b/ldap/servers/plugins/replication/repl5_agmt.c
@@ -537,7 +537,7 @@ agmt_new_from_entry(Slapi_Entry *e)
if (val) {
strcpy(ra->last_init_status, val);
}
- ra->changecounters = (struct changecounter **)slapi_ch_calloc(MAX_NUM_OF_SUPPLIERS + 1,
+ ra->changecounters = (struct changecounter **)slapi_ch_calloc(MAX_NUM_OF_MASTERS + 1,
sizeof(struct changecounter *));
ra->num_changecounters = 0;
ra->max_changecounters = MAX_NUM_OF_MASTERS;
@@ -2615,7 +2615,7 @@ agmt_update_init_status(Repl_Agmt *ra)
mods[nb_mods] = NULL;
slapi_modify_internal_set_pb_ext(pb, ra->dn, mods, NULL, NULL,
- repl_get_plugin_identity(PLUGIN_MULTISUPPLIER_REPLICATION), 0);
+ repl_get_plugin_identity(PLUGIN_MULTIMASTER_REPLICATION), 0);
slapi_modify_internal_pb(pb);
slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
diff --git a/ldap/servers/slapd/slapi-plugin.h b/ldap/servers/slapd/slapi-plugin.h
index 00e9722d2..677be1db0 100644
--- a/ldap/servers/slapd/slapi-plugin.h
+++ b/ldap/servers/slapd/slapi-plugin.h
@@ -8315,14 +8315,6 @@ void DS_Sleep(PRIntervalTime ticks);
*/
void slapi_timespec_diff(struct timespec *a, struct timespec *b, struct timespec *diff);
-/**
- * add 'new' timespect into 'cumul'
- * clock_monotonic to find time taken to perform operations.
- *
- * \param struct timespec cumul to compute total duration.
- * \param struct timespec new is a additional duration
- */
-void slapi_timespec_add(struct timespec *cumul, struct timespec *new);
/**
* Given an operation, determine the time elapsed since the op
* began.
diff --git a/ldap/servers/slapd/time.c b/ldap/servers/slapd/time.c
index 0dd457fbe..0406c3689 100644
--- a/ldap/servers/slapd/time.c
+++ b/ldap/servers/slapd/time.c
@@ -272,19 +272,6 @@ slapi_timespec_diff(struct timespec *a, struct timespec *b, struct timespec *dif
diff->tv_nsec = nsec;
}
-void
-slapi_timespec_add(struct timespec *cumul, struct timespec *new)
-{
- /* Now add the two */
- time_t sec = cumul->tv_sec + new->tv_sec;
- long nsec = cumul->tv_nsec + new->tv_nsec;
-
- sec += nsec / 1000000000;
- nsec = nsec % 1000000000;
- cumul->tv_sec = sec;
- cumul->tv_nsec = nsec;
-}
-
void
slapi_timespec_expire_at(time_t timeout, struct timespec *expire)
{
--
2.51.1

View File

@ -1,101 +0,0 @@
From a7231528b5ad7e887eeed4317de48d054cd046cd Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Wed, 23 Jul 2025 19:35:32 -0400
Subject: [PATCH] Issue 6895 - Crash if repl keep alive entry can not be
created
Description:
Heap use after free when logging that the replicaton keep-alive entry can not
be created. slapi_add_internal_pb() frees the slapi entry, then
we try and get the dn from the entry and we get a use-after-free crash.
Relates: https://github.com/389ds/389-ds-base/issues/6895
Reviewed by: spichugi(Thanks!)
(cherry picked from commit 43ab6b1d1de138d6be03b657f27cbb6ba19ddd14)
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
ldap/servers/plugins/chainingdb/cb_config.c | 3 +--
ldap/servers/plugins/posix-winsync/posix-winsync.c | 1 -
ldap/servers/plugins/replication/repl5_init.c | 3 ---
ldap/servers/plugins/replication/repl5_replica.c | 8 ++++----
4 files changed, 5 insertions(+), 10 deletions(-)
diff --git a/ldap/servers/plugins/chainingdb/cb_config.c b/ldap/servers/plugins/chainingdb/cb_config.c
index 40a7088d7..24fa1bcb3 100644
--- a/ldap/servers/plugins/chainingdb/cb_config.c
+++ b/ldap/servers/plugins/chainingdb/cb_config.c
@@ -44,8 +44,7 @@ cb_config_add_dse_entries(cb_backend *cb, char **entries, char *string1, char *s
slapi_pblock_get(util_pb, SLAPI_PLUGIN_INTOP_RESULT, &res);
if (LDAP_SUCCESS != res && LDAP_ALREADY_EXISTS != res) {
slapi_log_err(SLAPI_LOG_ERR, CB_PLUGIN_SUBSYSTEM,
- "cb_config_add_dse_entries - Unable to add config entry (%s) to the DSE: %s\n",
- slapi_entry_get_dn(e),
+ "cb_config_add_dse_entries - Unable to add config entry to the DSE: %s\n",
ldap_err2string(res));
rc = res;
slapi_pblock_destroy(util_pb);
diff --git a/ldap/servers/plugins/posix-winsync/posix-winsync.c b/ldap/servers/plugins/posix-winsync/posix-winsync.c
index 56efb2330..ab37497cd 100644
--- a/ldap/servers/plugins/posix-winsync/posix-winsync.c
+++ b/ldap/servers/plugins/posix-winsync/posix-winsync.c
@@ -1625,7 +1625,6 @@ posix_winsync_end_update_cb(void *cbdata __attribute__((unused)),
"posix_winsync_end_update_cb: "
"add task entry\n");
}
- /* slapi_entry_free(e_task); */
slapi_pblock_destroy(pb);
pb = NULL;
posix_winsync_config_reset_MOFTaskCreated();
diff --git a/ldap/servers/plugins/replication/repl5_init.c b/ldap/servers/plugins/replication/repl5_init.c
index 5a748e35a..9b6523a2e 100644
--- a/ldap/servers/plugins/replication/repl5_init.c
+++ b/ldap/servers/plugins/replication/repl5_init.c
@@ -682,7 +682,6 @@ create_repl_schema_policy(void)
repl_schema_top,
ldap_err2string(return_value));
rc = -1;
- slapi_entry_free(e); /* The entry was not consumed */
goto done;
}
slapi_pblock_destroy(pb);
@@ -703,7 +702,6 @@ create_repl_schema_policy(void)
repl_schema_supplier,
ldap_err2string(return_value));
rc = -1;
- slapi_entry_free(e); /* The entry was not consumed */
goto done;
}
slapi_pblock_destroy(pb);
@@ -724,7 +722,6 @@ create_repl_schema_policy(void)
repl_schema_consumer,
ldap_err2string(return_value));
rc = -1;
- slapi_entry_free(e); /* The entry was not consumed */
goto done;
}
slapi_pblock_destroy(pb);
diff --git a/ldap/servers/plugins/replication/repl5_replica.c b/ldap/servers/plugins/replication/repl5_replica.c
index d67f1bc71..cec140140 100644
--- a/ldap/servers/plugins/replication/repl5_replica.c
+++ b/ldap/servers/plugins/replication/repl5_replica.c
@@ -440,10 +440,10 @@ replica_subentry_create(const char *repl_root, ReplicaId rid)
if (return_value != LDAP_SUCCESS &&
return_value != LDAP_ALREADY_EXISTS &&
return_value != LDAP_REFERRAL /* CONSUMER */) {
- slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "replica_subentry_create - Unable to "
- "create replication keep alive entry %s: error %d - %s\n",
- slapi_entry_get_dn_const(e),
- return_value, ldap_err2string(return_value));
+ slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "replica_subentry_create - "
+ "Unable to create replication keep alive entry 'cn=%s %d,%s': error %d - %s\n",
+ KEEP_ALIVE_ENTRY, rid, repl_root,
+ return_value, ldap_err2string(return_value));
rc = -1;
goto done;
}
--
2.51.1

View File

@ -1,720 +0,0 @@
From 18a807e0e23b1160ea61e05e721da9fbd0c560b1 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Mon, 28 Jul 2025 15:41:29 -0700
Subject: [PATCH] Issue 6884 - Mask password hashes in audit logs (#6885)
Description: Fix the audit log functionality to mask password hash values for
userPassword, nsslapd-rootpw, nsmultiplexorcredentials, nsds5ReplicaCredentials,
and nsds5ReplicaBootstrapCredentials attributes in ADD and MODIFY operations.
Update auditlog.c to detect password attributes and replace their values with
asterisks (**********************) in both LDIF and JSON audit log formats.
Add a comprehensive test suite audit_password_masking_test.py to verify
password masking works correctly across all log formats and operation types.
Fixes: https://github.com/389ds/389-ds-base/issues/6884
Reviewed by: @mreynolds389, @vashirov (Thanks!!)
(cherry picked from commit 24f9aea1ae7e29bd885212825dc52d2a5db08a03)
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
.../logging/audit_password_masking_test.py | 457 ++++++++++++++++++
ldap/servers/slapd/auditlog.c | 144 +++++-
ldap/servers/slapd/slapi-private.h | 1 +
src/lib389/lib389/chaining.py | 3 +-
4 files changed, 586 insertions(+), 19 deletions(-)
create mode 100644 dirsrvtests/tests/suites/logging/audit_password_masking_test.py
diff --git a/dirsrvtests/tests/suites/logging/audit_password_masking_test.py b/dirsrvtests/tests/suites/logging/audit_password_masking_test.py
new file mode 100644
index 000000000..ae379cbba
--- /dev/null
+++ b/dirsrvtests/tests/suites/logging/audit_password_masking_test.py
@@ -0,0 +1,457 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2025 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 re
+import time
+import ldap
+from lib389._constants import DEFAULT_SUFFIX, DN_DM, PW_DM
+from lib389.topologies import topology_m2 as topo
+from lib389.idm.user import UserAccounts
+from lib389.plugins import ChainingBackendPlugin
+from lib389.chaining import ChainingLinks
+from lib389.agreement import Agreements
+from lib389.replica import ReplicationManager, Replicas
+from lib389.idm.directorymanager import DirectoryManager
+
+log = logging.getLogger(__name__)
+
+MASKED_PASSWORD = "**********************"
+TEST_PASSWORD = "MySecret123"
+TEST_PASSWORD_2 = "NewPassword789"
+TEST_PASSWORD_3 = "NewPassword101"
+
+
+def setup_audit_logging(inst, log_format='default', display_attrs=None):
+ """Configure audit logging settings"""
+ inst.config.replace('nsslapd-auditlog-logging-enabled', 'on')
+
+ if display_attrs is not None:
+ inst.config.replace('nsslapd-auditlog-display-attrs', display_attrs)
+
+ inst.deleteAuditLogs()
+
+
+def check_password_masked(inst, log_format, expected_password, actual_password):
+ """Helper function to check password masking in audit logs"""
+
+ inst.restart() # Flush the logs
+
+ # List of all password/credential attributes that should be masked
+ password_attributes = [
+ 'userPassword',
+ 'nsslapd-rootpw',
+ 'nsmultiplexorcredentials',
+ 'nsDS5ReplicaCredentials',
+ 'nsDS5ReplicaBootstrapCredentials'
+ ]
+
+ # Get password schemes to check for hash leakage
+ user_password_scheme = inst.config.get_attr_val_utf8('passwordStorageScheme')
+ root_password_scheme = inst.config.get_attr_val_utf8('nsslapd-rootpwstoragescheme')
+
+ # Check LDIF format logs
+ found_masked = False
+ found_actual = False
+ found_hashed = False
+
+ # Check each password attribute for masked password
+ for attr in password_attributes:
+ if inst.ds_audit_log.match(f"{attr}: {re.escape(expected_password)}"):
+ found_masked = True
+ if inst.ds_audit_log.match(f"{attr}: {actual_password}"):
+ found_actual = True
+
+ # Check for hashed passwords in LDIF format
+ if user_password_scheme:
+ if inst.ds_audit_log.match(f"userPassword: {{{user_password_scheme}}}"):
+ found_hashed = True
+ if root_password_scheme:
+ if inst.ds_audit_log.match(f"nsslapd-rootpw: {{{root_password_scheme}}}"):
+ found_hashed = True
+
+ # Delete audit logs to avoid interference with other tests
+ # We need to reset the root password to default as deleteAuditLogs()
+ # opens a new connection with the default password
+ dm = DirectoryManager(inst)
+ dm.change_password(PW_DM)
+ inst.deleteAuditLogs()
+
+ return found_masked, found_actual, found_hashed
+
+
+@pytest.mark.parametrize("log_format,display_attrs", [
+ ("default", None),
+ ("default", "*"),
+ ("default", "userPassword"),
+])
+def test_password_masking_add_operation(topo, log_format, display_attrs):
+ """Test password masking in ADD operations
+
+ :id: 4358bd75-bcc7-401c-b492-d3209b10412d
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure audit logging format
+ 2. Add user with password
+ 3. Check that password is masked in audit log
+ 4. Verify actual password does not appear in log
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Password should be masked with asterisks
+ 4. Actual password should not be found in log
+ """
+ inst = topo.ms['supplier1']
+ setup_audit_logging(inst, log_format, display_attrs)
+
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ user = None
+
+ try:
+ user = users.create(properties={
+ 'uid': 'test_add_pwd_mask',
+ 'cn': 'Test Add User',
+ 'sn': 'User',
+ 'uidNumber': '1000',
+ 'gidNumber': '1000',
+ 'homeDirectory': '/home/test_add',
+ 'userPassword': TEST_PASSWORD
+ })
+
+ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD)
+
+ assert found_masked, f"Masked password not found in {log_format} ADD operation"
+ assert not found_actual, f"Actual password found in {log_format} ADD log (should be masked)"
+ assert not found_hashed, f"Hashed password found in {log_format} ADD log (should be masked)"
+
+ finally:
+ if user is not None:
+ try:
+ user.delete()
+ except:
+ pass
+
+
+@pytest.mark.parametrize("log_format,display_attrs", [
+ ("default", None),
+ ("default", "*"),
+ ("default", "userPassword"),
+])
+def test_password_masking_modify_operation(topo, log_format, display_attrs):
+ """Test password masking in MODIFY operations
+
+ :id: e6963aa9-7609-419c-aae2-1d517aa434bd
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure audit logging format
+ 2. Add user without password
+ 3. Add password via MODIFY operation
+ 4. Check that password is masked in audit log
+ 5. Modify password to new value
+ 6. Check that new password is also masked
+ 7. Verify actual passwords do not appear in log
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Password should be masked with asterisks
+ 5. Success
+ 6. New password should be masked with asterisks
+ 7. No actual password values should be found in log
+ """
+ inst = topo.ms['supplier1']
+ setup_audit_logging(inst, log_format, display_attrs)
+
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ user = None
+
+ try:
+ user = users.create(properties={
+ 'uid': 'test_modify_pwd_mask',
+ 'cn': 'Test Modify User',
+ 'sn': 'User',
+ 'uidNumber': '2000',
+ 'gidNumber': '2000',
+ 'homeDirectory': '/home/test_modify'
+ })
+
+ user.replace('userPassword', TEST_PASSWORD)
+
+ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD)
+ assert found_masked, f"Masked password not found in {log_format} MODIFY operation (first password)"
+ assert not found_actual, f"Actual password found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed, f"Hashed password found in {log_format} MODIFY log (should be masked)"
+
+ user.replace('userPassword', TEST_PASSWORD_2)
+
+ found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2)
+ assert found_masked_2, f"Masked password not found in {log_format} MODIFY operation (second password)"
+ assert not found_actual_2, f"Second actual password found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed_2, f"Second hashed password found in {log_format} MODIFY log (should be masked)"
+
+ finally:
+ if user is not None:
+ try:
+ user.delete()
+ except:
+ pass
+
+
+@pytest.mark.parametrize("log_format,display_attrs", [
+ ("default", None),
+ ("default", "*"),
+ ("default", "nsslapd-rootpw"),
+])
+def test_password_masking_rootpw_modify_operation(topo, log_format, display_attrs):
+ """Test password masking for nsslapd-rootpw MODIFY operations
+
+ :id: ec8c9fd4-56ba-4663-ab65-58efb3b445e4
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure audit logging format
+ 2. Modify nsslapd-rootpw in configuration
+ 3. Check that root password is masked in audit log
+ 4. Modify root password to new value
+ 5. Check that new root password is also masked
+ 6. Verify actual root passwords do not appear in log
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Root password should be masked with asterisks
+ 4. Success
+ 5. New root password should be masked with asterisks
+ 6. No actual root password values should be found in log
+ """
+ inst = topo.ms['supplier1']
+ setup_audit_logging(inst, log_format, display_attrs)
+ dm = DirectoryManager(inst)
+
+ try:
+ dm.change_password(TEST_PASSWORD)
+ dm.rebind(TEST_PASSWORD)
+ dm.change_password(PW_DM)
+
+ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD)
+ assert found_masked, f"Masked root password not found in {log_format} MODIFY operation (first root password)"
+ assert not found_actual, f"Actual root password found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed, f"Hashed root password found in {log_format} MODIFY log (should be masked)"
+
+ dm.change_password(TEST_PASSWORD_2)
+ dm.rebind(TEST_PASSWORD_2)
+ dm.change_password(PW_DM)
+
+ found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2)
+ assert found_masked_2, f"Masked root password not found in {log_format} MODIFY operation (second root password)"
+ assert not found_actual_2, f"Second actual root password found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed_2, f"Second hashed root password found in {log_format} MODIFY log (should be masked)"
+
+ finally:
+ dm.change_password(PW_DM)
+ dm.rebind(PW_DM)
+
+
+@pytest.mark.parametrize("log_format,display_attrs", [
+ ("default", None),
+ ("default", "*"),
+ ("default", "nsmultiplexorcredentials"),
+])
+def test_password_masking_multiplexor_credentials(topo, log_format, display_attrs):
+ """Test password masking for nsmultiplexorcredentials in chaining/multiplexor configurations
+
+ :id: 161a9498-b248-4926-90be-a696a36ed36e
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure audit logging format
+ 2. Create a chaining backend configuration entry with nsmultiplexorcredentials
+ 3. Check that multiplexor credentials are masked in audit log
+ 4. Modify the credentials
+ 5. Check that updated credentials are also masked
+ 6. Verify actual credentials do not appear in log
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Multiplexor credentials should be masked with asterisks
+ 4. Success
+ 5. Updated credentials should be masked with asterisks
+ 6. No actual credential values should be found in log
+ """
+ inst = topo.ms['supplier1']
+ setup_audit_logging(inst, log_format, display_attrs)
+
+ # Enable chaining plugin and create chaining link
+ chain_plugin = ChainingBackendPlugin(inst)
+ chain_plugin.enable()
+
+ chains = ChainingLinks(inst)
+ chain = None
+
+ try:
+ # Create chaining link with multiplexor credentials
+ chain = chains.create(properties={
+ 'cn': 'testchain',
+ 'nsfarmserverurl': 'ldap://localhost:389/',
+ 'nsslapd-suffix': 'dc=example,dc=com',
+ 'nsmultiplexorbinddn': 'cn=manager',
+ 'nsmultiplexorcredentials': TEST_PASSWORD,
+ 'nsCheckLocalACI': 'on',
+ 'nsConnectionLife': '30',
+ })
+
+ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD)
+ assert found_masked, f"Masked multiplexor credentials not found in {log_format} ADD operation"
+ assert not found_actual, f"Actual multiplexor credentials found in {log_format} ADD log (should be masked)"
+ assert not found_hashed, f"Hashed multiplexor credentials found in {log_format} ADD log (should be masked)"
+
+ # Modify the credentials
+ chain.replace('nsmultiplexorcredentials', TEST_PASSWORD_2)
+
+ found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2)
+ assert found_masked_2, f"Masked multiplexor credentials not found in {log_format} MODIFY operation"
+ assert not found_actual_2, f"Actual multiplexor credentials found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed_2, f"Hashed multiplexor credentials found in {log_format} MODIFY log (should be masked)"
+
+ finally:
+ chain_plugin.disable()
+ if chain is not None:
+ inst.delete_branch_s(chain.dn, ldap.SCOPE_ONELEVEL)
+ chain.delete()
+
+
+@pytest.mark.parametrize("log_format,display_attrs", [
+ ("default", None),
+ ("default", "*"),
+ ("default", "nsDS5ReplicaCredentials"),
+])
+def test_password_masking_replica_credentials(topo, log_format, display_attrs):
+ """Test password masking for nsDS5ReplicaCredentials in replication agreements
+
+ :id: 7bf9e612-1b7c-49af-9fc0-de4c7df84b2a
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure audit logging format
+ 2. Create a replication agreement entry with nsDS5ReplicaCredentials
+ 3. Check that replica credentials are masked in audit log
+ 4. Modify the credentials
+ 5. Check that updated credentials are also masked
+ 6. Verify actual credentials do not appear in log
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Replica credentials should be masked with asterisks
+ 4. Success
+ 5. Updated credentials should be masked with asterisks
+ 6. No actual credential values should be found in log
+ """
+ inst = topo.ms['supplier2']
+ setup_audit_logging(inst, log_format, display_attrs)
+ agmt = None
+
+ try:
+ replicas = Replicas(inst)
+ replica = replicas.get(DEFAULT_SUFFIX)
+ agmts = replica.get_agreements()
+ agmt = agmts.create(properties={
+ 'cn': 'testagmt',
+ 'nsDS5ReplicaHost': 'localhost',
+ 'nsDS5ReplicaPort': '389',
+ 'nsDS5ReplicaBindDN': 'cn=replication manager,cn=config',
+ 'nsDS5ReplicaCredentials': TEST_PASSWORD,
+ 'nsDS5ReplicaRoot': DEFAULT_SUFFIX
+ })
+
+ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD)
+ assert found_masked, f"Masked replica credentials not found in {log_format} ADD operation"
+ assert not found_actual, f"Actual replica credentials found in {log_format} ADD log (should be masked)"
+ assert not found_hashed, f"Hashed replica credentials found in {log_format} ADD log (should be masked)"
+
+ # Modify the credentials
+ agmt.replace('nsDS5ReplicaCredentials', TEST_PASSWORD_2)
+
+ found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2)
+ assert found_masked_2, f"Masked replica credentials not found in {log_format} MODIFY operation"
+ assert not found_actual_2, f"Actual replica credentials found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed_2, f"Hashed replica credentials found in {log_format} MODIFY log (should be masked)"
+
+ finally:
+ if agmt is not None:
+ agmt.delete()
+
+
+@pytest.mark.parametrize("log_format,display_attrs", [
+ ("default", None),
+ ("default", "*"),
+ ("default", "nsDS5ReplicaBootstrapCredentials"),
+])
+def test_password_masking_bootstrap_credentials(topo, log_format, display_attrs):
+ """Test password masking for nsDS5ReplicaCredentials and nsDS5ReplicaBootstrapCredentials in replication agreements
+
+ :id: 248bd418-ffa4-4733-963d-2314c60b7c5b
+ :parametrized: yes
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure audit logging format
+ 2. Create a replication agreement entry with both nsDS5ReplicaCredentials and nsDS5ReplicaBootstrapCredentials
+ 3. Check that both credentials are masked in audit log
+ 4. Modify both credentials
+ 5. Check that both updated credentials are also masked
+ 6. Verify actual credentials do not appear in log
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Both credentials should be masked with asterisks
+ 4. Success
+ 5. Both updated credentials should be masked with asterisks
+ 6. No actual credential values should be found in log
+ """
+ inst = topo.ms['supplier2']
+ setup_audit_logging(inst, log_format, display_attrs)
+ agmt = None
+
+ try:
+ replicas = Replicas(inst)
+ replica = replicas.get(DEFAULT_SUFFIX)
+ agmts = replica.get_agreements()
+ agmt = agmts.create(properties={
+ 'cn': 'testbootstrapagmt',
+ 'nsDS5ReplicaHost': 'localhost',
+ 'nsDS5ReplicaPort': '389',
+ 'nsDS5ReplicaBindDN': 'cn=replication manager,cn=config',
+ 'nsDS5ReplicaCredentials': TEST_PASSWORD,
+ 'nsDS5replicabootstrapbinddn': 'cn=bootstrap manager,cn=config',
+ 'nsDS5ReplicaBootstrapCredentials': TEST_PASSWORD_2,
+ 'nsDS5ReplicaRoot': DEFAULT_SUFFIX
+ })
+
+ found_masked_bootstrap, found_actual_bootstrap, found_hashed_bootstrap = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2)
+ assert found_masked_bootstrap, f"Masked bootstrap credentials not found in {log_format} ADD operation"
+ assert not found_actual_bootstrap, f"Actual bootstrap credentials found in {log_format} ADD log (should be masked)"
+ assert not found_hashed_bootstrap, f"Hashed bootstrap credentials found in {log_format} ADD log (should be masked)"
+
+ agmt.replace('nsDS5ReplicaBootstrapCredentials', TEST_PASSWORD_3)
+
+ found_masked_bootstrap_2, found_actual_bootstrap_2, found_hashed_bootstrap_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_3)
+ assert found_masked_bootstrap_2, f"Masked bootstrap credentials not found in {log_format} MODIFY operation"
+ assert not found_actual_bootstrap_2, f"Actual bootstrap credentials found in {log_format} MODIFY log (should be masked)"
+ assert not found_hashed_bootstrap_2, f"Hashed bootstrap credentials found in {log_format} MODIFY log (should be masked)"
+
+ finally:
+ if agmt is not None:
+ agmt.delete()
+
+
+
+if __name__ == '__main__':
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main(["-s", CURRENT_FILE])
\ No newline at end of file
diff --git a/ldap/servers/slapd/auditlog.c b/ldap/servers/slapd/auditlog.c
index 0597ecc6f..c41415725 100644
--- a/ldap/servers/slapd/auditlog.c
+++ b/ldap/servers/slapd/auditlog.c
@@ -37,6 +37,89 @@ static void write_audit_file(Slapi_Entry *entry, int logtype, int optype, const
static const char *modrdn_changes[4];
+/* Helper function to check if an attribute is a password that needs masking */
+static int
+is_password_attribute(const char *attr_name)
+{
+ return (strcasecmp(attr_name, SLAPI_USERPWD_ATTR) == 0 ||
+ strcasecmp(attr_name, CONFIG_ROOTPW_ATTRIBUTE) == 0 ||
+ strcasecmp(attr_name, SLAPI_MB_CREDENTIALS) == 0 ||
+ strcasecmp(attr_name, SLAPI_REP_CREDENTIALS) == 0 ||
+ strcasecmp(attr_name, SLAPI_REP_BOOTSTRAP_CREDENTIALS) == 0);
+}
+
+/* Helper function to create a masked string representation of an entry */
+static char *
+create_masked_entry_string(Slapi_Entry *original_entry, int *len)
+{
+ Slapi_Attr *attr = NULL;
+ char *entry_str = NULL;
+ char *current_pos = NULL;
+ char *line_start = NULL;
+ char *next_line = NULL;
+ char *colon_pos = NULL;
+ int has_password_attrs = 0;
+
+ if (original_entry == NULL) {
+ return NULL;
+ }
+
+ /* Single pass through attributes to check for password attributes */
+ for (slapi_entry_first_attr(original_entry, &attr); attr != NULL;
+ slapi_entry_next_attr(original_entry, attr, &attr)) {
+
+ char *attr_name = NULL;
+ slapi_attr_get_type(attr, &attr_name);
+
+ if (is_password_attribute(attr_name)) {
+ has_password_attrs = 1;
+ break;
+ }
+ }
+
+ /* If no password attributes, return original string - no masking needed */
+ entry_str = slapi_entry2str(original_entry, len);
+ if (!has_password_attrs) {
+ return entry_str;
+ }
+
+ /* Process the string in-place, replacing password values */
+ current_pos = entry_str;
+ while ((line_start = current_pos) != NULL && *line_start != '\0') {
+ /* Find the end of current line */
+ next_line = strchr(line_start, '\n');
+ if (next_line != NULL) {
+ *next_line = '\0'; /* Temporarily terminate line */
+ current_pos = next_line + 1;
+ } else {
+ current_pos = NULL; /* Last line */
+ }
+
+ /* Find the colon that separates attribute name from value */
+ colon_pos = strchr(line_start, ':');
+ if (colon_pos != NULL) {
+ char saved_colon = *colon_pos;
+ *colon_pos = '\0'; /* Temporarily null-terminate attribute name */
+
+ /* Check if this is a password attribute that needs masking */
+ if (is_password_attribute(line_start)) {
+ strcpy(colon_pos + 1, " **********************");
+ }
+
+ *colon_pos = saved_colon; /* Restore colon */
+ }
+
+ /* Restore newline if it was there */
+ if (next_line != NULL) {
+ *next_line = '\n';
+ }
+ }
+
+ /* Update length since we may have shortened the string */
+ *len = strlen(entry_str);
+ return entry_str; /* Return the modified original string */
+}
+
void
write_audit_log_entry(Slapi_PBlock *pb)
{
@@ -248,7 +331,21 @@ add_entry_attrs(Slapi_Entry *entry, lenstr *l)
{
slapi_entry_attr_find(entry, req_attr, &entry_attr);
if (entry_attr) {
- log_entry_attr(entry_attr, req_attr, l);
+ if (strcmp(req_attr, PSEUDO_ATTR_UNHASHEDUSERPASSWORD) == 0) {
+ /* Do not write the unhashed clear-text password */
+ continue;
+ }
+
+ /* Check if this is a password attribute that needs masking */
+ if (is_password_attribute(req_attr)) {
+ /* userpassword/rootdn password - mask the value */
+ addlenstr(l, "#");
+ addlenstr(l, req_attr);
+ addlenstr(l, ": **********************\n");
+ } else {
+ /* Regular attribute - log normally */
+ log_entry_attr(entry_attr, req_attr, l);
+ }
}
}
} else {
@@ -262,13 +359,11 @@ add_entry_attrs(Slapi_Entry *entry, lenstr *l)
continue;
}
- if (strcasecmp(attr, SLAPI_USERPWD_ATTR) == 0 ||
- strcasecmp(attr, CONFIG_ROOTPW_ATTRIBUTE) == 0)
- {
+ if (is_password_attribute(attr)) {
/* userpassword/rootdn password - mask the value */
addlenstr(l, "#");
addlenstr(l, attr);
- addlenstr(l, ": ****************************\n");
+ addlenstr(l, ": **********************\n");
continue;
}
log_entry_attr(entry_attr, attr, l);
@@ -354,6 +449,10 @@ write_audit_file(
break;
}
}
+
+ /* Check if this is a password attribute that needs masking */
+ int is_password_attr = is_password_attribute(mods[j]->mod_type);
+
switch (operationtype) {
case LDAP_MOD_ADD:
addlenstr(l, "add: ");
@@ -378,18 +477,27 @@ write_audit_file(
break;
}
if (operationtype != LDAP_MOD_IGNORE) {
- for (i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) {
- char *buf, *bufp;
- len = strlen(mods[j]->mod_type);
- len = LDIF_SIZE_NEEDED(len, mods[j]->mod_bvalues[i]->bv_len) + 1;
- buf = slapi_ch_malloc(len);
- bufp = buf;
- slapi_ldif_put_type_and_value_with_options(&bufp, mods[j]->mod_type,
- mods[j]->mod_bvalues[i]->bv_val,
- mods[j]->mod_bvalues[i]->bv_len, 0);
- *bufp = '\0';
- addlenstr(l, buf);
- slapi_ch_free((void **)&buf);
+ if (is_password_attr) {
+ /* Add masked password */
+ for (i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) {
+ addlenstr(l, mods[j]->mod_type);
+ addlenstr(l, ": **********************\n");
+ }
+ } else {
+ /* Add actual values for non-password attributes */
+ for (i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) {
+ char *buf, *bufp;
+ len = strlen(mods[j]->mod_type);
+ len = LDIF_SIZE_NEEDED(len, mods[j]->mod_bvalues[i]->bv_len) + 1;
+ buf = slapi_ch_malloc(len);
+ bufp = buf;
+ slapi_ldif_put_type_and_value_with_options(&bufp, mods[j]->mod_type,
+ mods[j]->mod_bvalues[i]->bv_val,
+ mods[j]->mod_bvalues[i]->bv_len, 0);
+ *bufp = '\0';
+ addlenstr(l, buf);
+ slapi_ch_free((void **)&buf);
+ }
}
}
addlenstr(l, "-\n");
@@ -400,7 +508,7 @@ write_audit_file(
e = change;
addlenstr(l, attr_changetype);
addlenstr(l, ": add\n");
- tmp = slapi_entry2str(e, &len);
+ tmp = create_masked_entry_string(e, &len);
tmpsave = tmp;
while ((tmp = strchr(tmp, '\n')) != NULL) {
tmp++;
diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h
index dfb0e272a..af2180e55 100644
--- a/ldap/servers/slapd/slapi-private.h
+++ b/ldap/servers/slapd/slapi-private.h
@@ -843,6 +843,7 @@ void task_cleanup(void);
/* for reversible encyrption */
#define SLAPI_MB_CREDENTIALS "nsmultiplexorcredentials"
#define SLAPI_REP_CREDENTIALS "nsds5ReplicaCredentials"
+#define SLAPI_REP_BOOTSTRAP_CREDENTIALS "nsds5ReplicaBootstrapCredentials"
int pw_rever_encode(Slapi_Value **vals, char *attr_name);
int pw_rever_decode(char *cipher, char **plain, const char *attr_name);
diff --git a/src/lib389/lib389/chaining.py b/src/lib389/lib389/chaining.py
index 533b83ebf..33ae78c8b 100644
--- a/src/lib389/lib389/chaining.py
+++ b/src/lib389/lib389/chaining.py
@@ -134,7 +134,7 @@ class ChainingLink(DSLdapObject):
"""
# Create chaining entry
- super(ChainingLink, self).create(rdn, properties, basedn)
+ link = super(ChainingLink, self).create(rdn, properties, basedn)
# Create mapping tree entry
dn_comps = ldap.explode_dn(properties['nsslapd-suffix'][0])
@@ -149,6 +149,7 @@ class ChainingLink(DSLdapObject):
self._mts.ensure_state(properties=mt_properties)
except ldap.ALREADY_EXISTS:
pass
+ return link
class ChainingLinks(DSLdapObjects):
--
2.51.1

View File

@ -1,137 +0,0 @@
From 284da99d0cd1ad16c702f4a4f68d2a479ac41576 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Wed, 18 Jun 2025 11:12:28 +0200
Subject: [PATCH] Issue 6819 - Incorrect pwdpolicysubentry returned for an
entry with user password policy
Bug Description:
When both subtree and user password policies exist, pwdpolicysubentry
points to the subtree password policy instead of user password policy.
Fix Description:
Update the template for CoS pointer definition to use
`operational-default` modifier instead of `operational`.
Fixes: https://github.com/389ds/389-ds-base/issues/6819
Reviewed by: @droideck, @tbordaz (Thanks!)
(cherry picked from commit 622c191302879035ef7450a29aa7569ee768c3ab)
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
.../password/pwdPolicy_attribute_test.py | 73 +++++++++++++++++--
src/lib389/lib389/pwpolicy.py | 2 +-
2 files changed, 66 insertions(+), 9 deletions(-)
diff --git a/dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py b/dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py
index c2c1e47fb..0dde8d637 100644
--- a/dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py
+++ b/dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py
@@ -59,17 +59,39 @@ def test_user(topology_st, request):
return user
-@pytest.fixture(scope="module")
-def password_policy(topology_st, test_user):
+@pytest.fixture(scope="function")
+def password_policy(topology_st, request, test_user):
"""Set up password policy for subtree and user"""
pwp = PwPolicyManager(topology_st.standalone)
policy_props = {}
- log.info('Create password policy for subtree {}'.format(OU_PEOPLE))
- pwp.create_subtree_policy(OU_PEOPLE, policy_props)
+ log.info(f"Create password policy for subtree {OU_PEOPLE}")
+ try:
+ pwp.create_subtree_policy(OU_PEOPLE, policy_props)
+ except ldap.ALREADY_EXISTS:
+ log.info(f"Subtree password policy for {OU_PEOPLE} already exist, skipping")
+
+ log.info(f"Create password policy for user {TEST_USER_DN}")
+ try:
+ pwp.create_user_policy(TEST_USER_DN, policy_props)
+ except ldap.ALREADY_EXISTS:
+ log.info(f"User password policy for {TEST_USER_DN} already exist, skipping")
+
+ def fin():
+ log.info(f"Delete password policy for subtree {OU_PEOPLE}")
+ try:
+ pwp.delete_local_policy(OU_PEOPLE)
+ except ValueError:
+ log.info(f"Subtree password policy for {OU_PEOPLE} doesn't exist, skipping")
+
+ log.info(f"Delete password policy for user {TEST_USER_DN}")
+ try:
+ pwp.delete_local_policy(TEST_USER_DN)
+ except ValueError:
+ log.info(f"User password policy for {TEST_USER_DN} doesn't exist, skipping")
+
+ request.addfinalizer(fin)
- log.info('Create password policy for user {}'.format(TEST_USER_DN))
- pwp.create_user_policy(TEST_USER_DN, policy_props)
@pytest.mark.skipif(ds_is_older('1.4.3.3'), reason="Not implemented")
def test_pwd_reset(topology_st, test_user):
@@ -257,8 +279,43 @@ def test_pwd_min_age(topology_st, test_user, password_policy):
log.info('Bind as DM')
topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
user.reset_password(TEST_USER_PWD)
- pwp.delete_local_policy(TEST_USER_DN)
- pwp.delete_local_policy(OU_PEOPLE)
+
+
+def test_pwdpolicysubentry(topology_st, password_policy):
+ """Verify that 'pwdpolicysubentry' attr works as expected
+ User should have a priority over a subtree.
+
+ :id: 4ab0c62a-623b-40b4-af67-99580c77b36c
+ :setup: Standalone instance, a test user,
+ password policy entries for a user and a subtree
+ :steps:
+ 1. Create a subtree policy
+ 2. Create a user policy
+ 3. Search for 'pwdpolicysubentry' in the user entry
+ 4. Delete the user policy
+ 5. Search for 'pwdpolicysubentry' in the user entry
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Should point to the user policy entry
+ 4. Success
+ 5. Should point to the subtree policy entry
+
+ """
+
+ users = UserAccounts(topology_st.standalone, OU_PEOPLE, rdn=None)
+ user = users.get(TEST_USER_NAME)
+
+ pwp_subentry = user.get_attr_vals_utf8('pwdpolicysubentry')[0]
+ assert 'nsPwPolicyEntry_subtree' not in pwp_subentry
+ assert 'nsPwPolicyEntry_user' in pwp_subentry
+
+ pwp = PwPolicyManager(topology_st.standalone)
+ pwp.delete_local_policy(TEST_USER_DN)
+ pwp_subentry = user.get_attr_vals_utf8('pwdpolicysubentry')[0]
+ assert 'nsPwPolicyEntry_subtree' in pwp_subentry
+ assert 'nsPwPolicyEntry_user' not in pwp_subentry
+
if __name__ == '__main__':
# Run isolated
diff --git a/src/lib389/lib389/pwpolicy.py b/src/lib389/lib389/pwpolicy.py
index 7ffe449cc..6a47a44fe 100644
--- a/src/lib389/lib389/pwpolicy.py
+++ b/src/lib389/lib389/pwpolicy.py
@@ -168,7 +168,7 @@ class PwPolicyManager(object):
# The CoS specification entry at the subtree level
cos_pointer_defs = CosPointerDefinitions(self._instance, dn)
- cos_pointer_defs.create(properties={'cosAttribute': 'pwdpolicysubentry default operational',
+ cos_pointer_defs.create(properties={'cosAttribute': 'pwdpolicysubentry default operational-default',
'cosTemplateDn': cos_template.dn,
'cn': 'nsPwPolicy_CoS'})
except ldap.LDAPError as e:
--
2.51.1

View File

@ -1,572 +0,0 @@
From 23e56fd01eaa24a2fa945430f91600dd9c726d34 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Tue, 19 Aug 2025 14:30:15 -0700
Subject: [PATCH] Issue 6936 - Make user/subtree policy creation idempotent
(#6937)
Description: Correct the CLI mapping typo to use 'nsslapd-pwpolicy-local',
rework subtree policy detection to validate CoS templates and add user-policy detection.
Make user/subtree policy creation idempotent via ensure_state, and improve deletion
logic to distinguish subtree vs user policies and fail if none exist.
Add a test suite (pwp_history_local_override_test.py) exercising global-only and local-only
history enforcement, local overriding global counts, immediate effect of dsconf updates,
and fallback to global after removing a user policy, ensuring reliable behavior
and preventing regressions.
Fixes: https://github.com/389ds/389-ds-base/issues/6936
Reviewed by: @mreynolds389 (Thanks!)
(cherry picked from commit da4eea126cc9019f540b57c1db9dec7988cade10)
Signed-off-by: Masahiro Matsuya <mmatsuya@redhat.com>
---
.../pwp_history_local_override_test.py | 351 ++++++++++++++++++
src/lib389/lib389/cli_conf/pwpolicy.py | 4 +-
src/lib389/lib389/pwpolicy.py | 107 ++++--
3 files changed, 424 insertions(+), 38 deletions(-)
create mode 100644 dirsrvtests/tests/suites/password/pwp_history_local_override_test.py
diff --git a/dirsrvtests/tests/suites/password/pwp_history_local_override_test.py b/dirsrvtests/tests/suites/password/pwp_history_local_override_test.py
new file mode 100644
index 000000000..6d72725fa
--- /dev/null
+++ b/dirsrvtests/tests/suites/password/pwp_history_local_override_test.py
@@ -0,0 +1,351 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2025 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import os
+import time
+import ldap
+import pytest
+import subprocess
+import logging
+
+from lib389._constants import DEFAULT_SUFFIX, DN_DM, PASSWORD, DN_CONFIG
+from lib389.topologies import topology_st
+from lib389.idm.user import UserAccounts
+from lib389.idm.domain import Domain
+from lib389.pwpolicy import PwPolicyManager
+
+pytestmark = pytest.mark.tier1
+
+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__)
+
+OU_DN = f"ou=People,{DEFAULT_SUFFIX}"
+USER_ACI = '(targetattr="userpassword || passwordHistory")(version 3.0; acl "pwp test"; allow (all) userdn="ldap:///self";)'
+
+
+@pytest.fixture(autouse=True, scope="function")
+def restore_global_policy(topology_st, request):
+ """Snapshot and restore global password policy around each test in this file."""
+ inst = topology_st.standalone
+ inst.simple_bind_s(DN_DM, PASSWORD)
+
+ attrs = [
+ 'nsslapd-pwpolicy-local',
+ 'nsslapd-pwpolicy-inherit-global',
+ 'passwordHistory',
+ 'passwordInHistory',
+ 'passwordChange',
+ ]
+
+ entry = inst.getEntry(DN_CONFIG, ldap.SCOPE_BASE, '(objectClass=*)', attrs)
+ saved = {attr: entry.getValue(attr) for attr in attrs}
+
+ def fin():
+ inst.simple_bind_s(DN_DM, PASSWORD)
+ for attr, value in saved.items():
+ inst.config.replace(attr, value)
+
+ request.addfinalizer(fin)
+
+
+@pytest.fixture(scope="function")
+def setup_entries(topology_st, request):
+ """Create test OU and user, and install an ACI for self password changes."""
+
+ inst = topology_st.standalone
+
+ suffix = Domain(inst, DEFAULT_SUFFIX)
+ suffix.add('aci', USER_ACI)
+
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ try:
+ user = users.create_test_user(uid=1)
+ except ldap.ALREADY_EXISTS:
+ user = users.get("test_user_1")
+
+ def fin():
+ pwp = PwPolicyManager(inst)
+ try:
+ pwp.delete_local_policy(OU_DN)
+ except Exception as e:
+ if "No password policy" in str(e):
+ pass
+ else:
+ raise e
+ try:
+ pwp.delete_local_policy(user.dn)
+ except Exception as e:
+ if "No password policy" in str(e):
+ pass
+ else:
+ raise e
+ suffix.remove('aci', USER_ACI)
+ request.addfinalizer(fin)
+
+ return user
+
+
+def set_user_password(inst, user, new_password, bind_as_user_password=None, expect_violation=False):
+ if bind_as_user_password is not None:
+ user.rebind(bind_as_user_password)
+ try:
+ user.reset_password(new_password)
+ if expect_violation:
+ pytest.fail("Password change unexpectedly succeeded")
+ except ldap.CONSTRAINT_VIOLATION:
+ if not expect_violation:
+ pytest.fail("Password change unexpectedly rejected with CONSTRAINT_VIOLATION")
+ finally:
+ inst.simple_bind_s(DN_DM, PASSWORD)
+ time.sleep(1)
+
+
+def set_global_history(inst, enabled: bool, count: int, inherit_global: str = 'on'):
+ inst.simple_bind_s(DN_DM, PASSWORD)
+ inst.config.replace('nsslapd-pwpolicy-local', 'on')
+ inst.config.replace('nsslapd-pwpolicy-inherit-global', inherit_global)
+ inst.config.replace('passwordHistory', 'on' if enabled else 'off')
+ inst.config.replace('passwordInHistory', str(count))
+ inst.config.replace('passwordChange', 'on')
+ time.sleep(1)
+
+
+def ensure_local_subtree_policy(inst, count: int, track_update_time: str = 'on'):
+ pwp = PwPolicyManager(inst)
+ pwp.create_subtree_policy(OU_DN, {
+ 'passwordChange': 'on',
+ 'passwordHistory': 'on',
+ 'passwordInHistory': str(count),
+ 'passwordTrackUpdateTime': track_update_time,
+ })
+ time.sleep(1)
+
+
+def set_local_history_via_cli(inst, count: int):
+ sbin_dir = inst.get_sbin_dir()
+ inst_name = inst.serverid
+ cmd = [f"{sbin_dir}/dsconf", inst_name, "localpwp", "set", f"--pwdhistorycount={count}", OU_DN]
+ rc = subprocess.call(cmd)
+ assert rc == 0, f"dsconf command failed rc={rc}: {' '.join(cmd)}"
+ time.sleep(1)
+
+
+def test_global_history_only_enforced(topology_st, setup_entries):
+ """Global-only history enforcement with count 2
+
+ :id: 3d8cf35b-4a33-4587-9814-ebe18b7a1f92
+ :setup: Standalone instance, test OU and user, ACI for self password changes
+ :steps:
+ 1. Remove local policies
+ 2. Set global policy: passwordHistory=on, passwordInHistory=2
+ 3. Set password to Alpha1, then change to Alpha2 and Alpha3 as the user
+ 4. Attempt to change to Alpha1 and Alpha2
+ 5. Attempt to change to Alpha4
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Changes to Welcome1 and Welcome2 are rejected with CONSTRAINT_VIOLATION
+ 5. Change to Welcome4 is accepted
+ """
+ inst = topology_st.standalone
+ inst.simple_bind_s(DN_DM, PASSWORD)
+
+ set_global_history(inst, enabled=True, count=2)
+
+ user = setup_entries
+ user.reset_password('Alpha1')
+ set_user_password(inst, user, 'Alpha2', bind_as_user_password='Alpha1')
+ set_user_password(inst, user, 'Alpha3', bind_as_user_password='Alpha2')
+
+ # Within last 2
+ set_user_password(inst, user, 'Alpha2', bind_as_user_password='Alpha3', expect_violation=True)
+ set_user_password(inst, user, 'Alpha1', bind_as_user_password='Alpha3', expect_violation=True)
+
+ # New password should be allowed
+ set_user_password(inst, user, 'Alpha4', bind_as_user_password='Alpha3', expect_violation=False)
+
+
+def test_local_overrides_global_history(topology_st, setup_entries):
+ """Local subtree policy (history=3) overrides global (history=1)
+
+ :id: 97c22f56-5ea6-40c1-8d8c-1cece3bf46fd
+ :setup: Standalone instance, test OU and user
+ :steps:
+ 1. Set global policy passwordInHistory=1
+ 2. Create local subtree policy on the OU with passwordInHistory=3
+ 3. Set password to Bravo1, then change to Bravo2 and Bravo3 as the user
+ 4. Attempt to change to Bravo1
+ 5. Attempt to change to Bravo5
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Change to Welcome1 is rejected (local policy wins)
+ 5. Change to Welcome5 is accepted
+ """
+ inst = topology_st.standalone
+ inst.simple_bind_s(DN_DM, PASSWORD)
+
+ set_global_history(inst, enabled=True, count=1, inherit_global='on')
+
+ ensure_local_subtree_policy(inst, count=3)
+
+ user = setup_entries
+ user.reset_password('Bravo1')
+ set_user_password(inst, user, 'Bravo2', bind_as_user_password='Bravo1')
+ set_user_password(inst, user, 'Bravo3', bind_as_user_password='Bravo2')
+
+ # Third prior should be rejected under local policy count=3
+ set_user_password(inst, user, 'Bravo1', bind_as_user_password='Bravo3', expect_violation=True)
+
+ # New password allowed
+ set_user_password(inst, user, 'Bravo5', bind_as_user_password='Bravo3', expect_violation=False)
+
+
+def test_change_local_history_via_cli_affects_enforcement(topology_st, setup_entries):
+ """Changing local policy via CLI is enforced immediately
+
+ :id: 5a6d0d14-4009-4bad-86e1-cde5000c43dc
+ :setup: Standalone instance, test OU and user, dsconf available
+ :steps:
+ 1. Ensure local subtree policy passwordInHistory=3
+ 2. Set password to Charlie1, then change to Charlie2 and Charlie3 as the user
+ 3. Attempt to change to Charlie1 (within last 3)
+ 4. Run: dsconf <inst> localpwp set --pwdhistorycount=1 "ou=product testing,<suffix>"
+ 5. Attempt to change to Charlie1 again
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Change to Welcome1 is rejected
+ 4. CLI command succeeds
+ 5. Change to Welcome1 now succeeds (only last 1 is disallowed)
+ """
+ inst = topology_st.standalone
+ inst.simple_bind_s(DN_DM, PASSWORD)
+
+ ensure_local_subtree_policy(inst, count=3)
+
+ user = setup_entries
+ user.reset_password('Charlie1')
+ set_user_password(inst, user, 'Charlie2', bind_as_user_password='Charlie1', expect_violation=False)
+ set_user_password(inst, user, 'Charlie3', bind_as_user_password='Charlie2', expect_violation=False)
+
+ # With count=3, Welcome1 is within history
+ set_user_password(inst, user, 'Charlie1', bind_as_user_password='Charlie3', expect_violation=True)
+
+ # Reduce local count to 1 via CLI to exercise CLI mapping and updated code
+ set_local_history_via_cli(inst, count=1)
+
+ # Now Welcome1 should be allowed
+ set_user_password(inst, user, 'Charlie1', bind_as_user_password='Charlie3', expect_violation=False)
+
+
+def test_history_local_only_enforced(topology_st, setup_entries):
+ """Local-only history enforcement with count 3
+
+ :id: af6ff34d-ac94-4108-a7b6-2b589c960154
+ :setup: Standalone instance, test OU and user
+ :steps:
+ 1. Disable global password history (passwordHistory=off, passwordInHistory=0, inherit off)
+ 2. Ensure local subtree policy with passwordInHistory=3
+ 3. Set password to Delta1, then change to Delta2 and Delta3 as the user
+ 4. Attempt to change to Delta1
+ 5. Attempt to change to Delta5
+ 6. Change once more to Delta6, then change to Delta1
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Change to Welcome1 is rejected (within last 3)
+ 5. Change to Welcome5 is accepted
+ 6. Welcome1 is now older than the last 3 and is accepted
+ """
+ inst = topology_st.standalone
+ inst.simple_bind_s(DN_DM, PASSWORD)
+
+ set_global_history(inst, enabled=False, count=0, inherit_global='off')
+
+ ensure_local_subtree_policy(inst, count=3)
+
+ user = setup_entries
+ user.reset_password('Delta1')
+ set_user_password(inst, user, 'Delta2', bind_as_user_password='Delta1')
+ set_user_password(inst, user, 'Delta3', bind_as_user_password='Delta2')
+
+ # Within last 2
+ set_user_password(inst, user, 'Delta1', bind_as_user_password='Delta3', expect_violation=True)
+
+ # New password allowed
+ set_user_password(inst, user, 'Delta5', bind_as_user_password='Delta3', expect_violation=False)
+
+ # Now Welcome1 is older than last 2 after one more change
+ set_user_password(inst, user, 'Delta6', bind_as_user_password='Delta5', expect_violation=False)
+ set_user_password(inst, user, 'Delta1', bind_as_user_password='Delta6', expect_violation=False)
+
+
+def test_user_policy_detection_and_enforcement(topology_st, setup_entries):
+ """User local policy is detected and enforced; removal falls back to global policy
+
+ :id: 2213126a-1f47-468c-8337-0d2ee5d2d585
+ :setup: Standalone instance, test OU and user
+ :steps:
+ 1. Set global policy passwordInHistory=1
+ 2. Create a user local password policy on the user with passwordInHistory=3
+ 3. Verify is_user_policy(USER_DN) is True
+ 4. Set password to Echo1, then change to Echo2 and Echo3 as the user
+ 5. Attempt to change to Echo1 (within last 3)
+ 6. Delete the user local policy
+ 7. Verify is_user_policy(USER_DN) is False
+ 8. Attempt to change to Echo1 again (now only last 1 disallowed by global)
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. is_user_policy returns True
+ 4. Success
+ 5. Change to Welcome1 is rejected
+ 6. Success
+ 7. is_user_policy returns False
+ 8. Change to Welcome1 succeeds (two back is allowed by global=1)
+ """
+ inst = topology_st.standalone
+ inst.simple_bind_s(DN_DM, PASSWORD)
+
+ set_global_history(inst, enabled=True, count=1, inherit_global='on')
+
+ pwp = PwPolicyManager(inst)
+ user = setup_entries
+ pwp.create_user_policy(user.dn, {
+ 'passwordChange': 'on',
+ 'passwordHistory': 'on',
+ 'passwordInHistory': '3',
+ })
+
+ assert pwp.is_user_policy(user.dn) is True
+
+ user.reset_password('Echo1')
+ set_user_password(inst, user, 'Echo2', bind_as_user_password='Echo1', expect_violation=False)
+ set_user_password(inst, user, 'Echo3', bind_as_user_password='Echo2', expect_violation=False)
+ set_user_password(inst, user, 'Echo1', bind_as_user_password='Echo3', expect_violation=True)
+
+ pwp.delete_local_policy(user.dn)
+ assert pwp.is_user_policy(user.dn) is False
+
+ # With only global=1, Echo1 (two back) is allowed
+ set_user_password(inst, user, 'Echo1', bind_as_user_password='Echo3', expect_violation=False)
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
diff --git a/src/lib389/lib389/cli_conf/pwpolicy.py b/src/lib389/lib389/cli_conf/pwpolicy.py
index 2d4ba9b21..a3e59a90c 100644
--- a/src/lib389/lib389/cli_conf/pwpolicy.py
+++ b/src/lib389/lib389/cli_conf/pwpolicy.py
@@ -1,5 +1,5 @@
# --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2023 Red Hat, Inc.
+# Copyright (C) 2025 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
@@ -43,7 +43,7 @@ def _get_pw_policy(inst, targetdn, log, use_json=None):
targetdn = 'cn=config'
policydn = targetdn
basedn = targetdn
- attr_list.extend(['passwordisglobalpolicy', 'nsslapd-pwpolicy_local'])
+ attr_list.extend(['passwordisglobalpolicy', 'nsslapd-pwpolicy-local'])
all_attrs = inst.config.get_attrs_vals_utf8(attr_list)
attrs = {k: v for k, v in all_attrs.items() if len(v) > 0}
else:
diff --git a/src/lib389/lib389/pwpolicy.py b/src/lib389/lib389/pwpolicy.py
index 6a47a44fe..539c230a9 100644
--- a/src/lib389/lib389/pwpolicy.py
+++ b/src/lib389/lib389/pwpolicy.py
@@ -1,5 +1,5 @@
# --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2018 Red Hat, Inc.
+# Copyright (C) 2025 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
@@ -7,6 +7,7 @@
# --- END COPYRIGHT BLOCK ---
import ldap
+from ldap import filter as ldap_filter
from lib389._mapped_object import DSLdapObject, DSLdapObjects
from lib389.backend import Backends
from lib389.config import Config
@@ -74,19 +75,56 @@ class PwPolicyManager(object):
}
def is_subtree_policy(self, dn):
- """Check if the entry has a subtree password policy. If we can find a
- template entry it is subtree policy
+ """Check if a subtree password policy exists for a given entry DN.
- :param dn: Entry DN with PwPolicy set up
+ A subtree policy is indicated by the presence of any CoS template
+ (under `cn=nsPwPolicyContainer,<dn>`) that has a `pwdpolicysubentry`
+ attribute pointing to an existing entry with objectClass `passwordpolicy`.
+
+ :param dn: Entry DN to check for subtree policy
:type dn: str
- :returns: True if the entry has a subtree policy, False otherwise
+ :returns: True if a subtree policy exists, False otherwise
+ :rtype: bool
"""
- cos_templates = CosTemplates(self._instance, 'cn=nsPwPolicyContainer,{}'.format(dn))
try:
- cos_templates.get('cn=nsPwTemplateEntry,%s' % dn)
- return True
- except:
+ container_basedn = 'cn=nsPwPolicyContainer,{}'.format(dn)
+ templates = CosTemplates(self._instance, container_basedn).list()
+ for tmpl in templates:
+ pwp_dn = tmpl.get_attr_val_utf8('pwdpolicysubentry')
+ if not pwp_dn:
+ continue
+ # Validate that the referenced entry exists and is a passwordpolicy
+ pwp_entry = PwPolicyEntry(self._instance, pwp_dn)
+ if pwp_entry.exists() and pwp_entry.present('objectClass', 'passwordpolicy'):
+ return True
+ except ldap.LDAPError:
+ pass
+ return False
+
+ def is_user_policy(self, dn):
+ """Check if the entry has a user password policy.
+
+ A user policy is indicated by the target entry having a
+ `pwdpolicysubentry` attribute that points to an existing
+ entry with objectClass `passwordpolicy`.
+
+ :param dn: Entry DN to check
+ :type dn: str
+
+ :returns: True if the entry has a user policy, False otherwise
+ :rtype: bool
+ """
+ try:
+ entry = Account(self._instance, dn)
+ if not entry.exists():
+ return False
+ pwp_dn = entry.get_attr_val_utf8('pwdpolicysubentry')
+ if not pwp_dn:
+ return False
+ pwp_entry = PwPolicyEntry(self._instance, pwp_dn)
+ return pwp_entry.exists() and pwp_entry.present('objectClass', 'passwordpolicy')
+ except ldap.LDAPError:
return False
def create_user_policy(self, dn, properties):
@@ -114,10 +152,10 @@ class PwPolicyManager(object):
pwp_containers = nsContainers(self._instance, basedn=parentdn)
pwp_container = pwp_containers.ensure_state(properties={'cn': 'nsPwPolicyContainer'})
- # Create policy entry
+ # Create or update the policy entry
properties['cn'] = 'cn=nsPwPolicyEntry_user,%s' % dn
pwp_entries = PwPolicyEntries(self._instance, pwp_container.dn)
- pwp_entry = pwp_entries.create(properties=properties)
+ pwp_entry = pwp_entries.ensure_state(properties=properties)
try:
# Add policy to the entry
user_entry.replace('pwdpolicysubentry', pwp_entry.dn)
@@ -152,32 +190,27 @@ class PwPolicyManager(object):
pwp_containers = nsContainers(self._instance, basedn=dn)
pwp_container = pwp_containers.ensure_state(properties={'cn': 'nsPwPolicyContainer'})
- # Create policy entry
- pwp_entry = None
+ # Create or update the policy entry
properties['cn'] = 'cn=nsPwPolicyEntry_subtree,%s' % dn
pwp_entries = PwPolicyEntries(self._instance, pwp_container.dn)
- pwp_entry = pwp_entries.create(properties=properties)
- try:
- # The CoS template entry (nsPwTemplateEntry) that has the pwdpolicysubentry
- # value pointing to the above (nsPwPolicyEntry) entry
- cos_template = None
- cos_templates = CosTemplates(self._instance, pwp_container.dn)
- cos_template = cos_templates.create(properties={'cosPriority': '1',
- 'pwdpolicysubentry': pwp_entry.dn,
- 'cn': 'cn=nsPwTemplateEntry,%s' % dn})
-
- # The CoS specification entry at the subtree level
- cos_pointer_defs = CosPointerDefinitions(self._instance, dn)
- cos_pointer_defs.create(properties={'cosAttribute': 'pwdpolicysubentry default operational-default',
- 'cosTemplateDn': cos_template.dn,
- 'cn': 'nsPwPolicy_CoS'})
- except ldap.LDAPError as e:
- # Something went wrong, remove what we have done
- if pwp_entry is not None:
- pwp_entry.delete()
- if cos_template is not None:
- cos_template.delete()
- raise e
+ pwp_entry = pwp_entries.ensure_state(properties=properties)
+
+ # Ensure the CoS template entry (nsPwTemplateEntry) that points to the
+ # password policy entry
+ cos_templates = CosTemplates(self._instance, pwp_container.dn)
+ cos_template = cos_templates.ensure_state(properties={
+ 'cosPriority': '1',
+ 'pwdpolicysubentry': pwp_entry.dn,
+ 'cn': 'cn=nsPwTemplateEntry,%s' % dn
+ })
+
+ # Ensure the CoS specification entry at the subtree level
+ cos_pointer_defs = CosPointerDefinitions(self._instance, dn)
+ cos_pointer_defs.ensure_state(properties={
+ 'cosAttribute': 'pwdpolicysubentry default operational-default',
+ 'cosTemplateDn': cos_template.dn,
+ 'cn': 'nsPwPolicy_CoS'
+ })
# make sure that local policies are enabled
self.set_global_policy({'nsslapd-pwpolicy-local': 'on'})
@@ -244,10 +277,12 @@ class PwPolicyManager(object):
if self.is_subtree_policy(entry.dn):
parentdn = dn
subtree = True
- else:
+ elif self.is_user_policy(entry.dn):
dn_comps = ldap.dn.explode_dn(dn)
dn_comps.pop(0)
parentdn = ",".join(dn_comps)
+ else:
+ raise ValueError('The target entry dn does not have a password policy')
# Starting deleting the policy, ignore the parts that might already have been removed
pwp_container = nsContainer(self._instance, 'cn=nsPwPolicyContainer,%s' % parentdn)
--
2.51.1

Some files were not shown because too many files have changed in this diff Show More