Compare commits

...

3 Commits

Author SHA1 Message Date
AlmaLinux RelEng Bot
1b2fb8c72e import CS 389-ds-base-3.2.0-6.el10 2026-04-07 06:30:11 -04:00
2760ce3ca2 import CS 389-ds-base-3.1.3-5.el10 2025-10-27 08:58:41 +00:00
89b7694df5 import RHEL 10 Beta 389-ds-base-3.0.4-3.el10 2024-11-25 09:05:59 +00:00
88 changed files with 24640 additions and 3564 deletions

View File

@ -1,3 +0,0 @@
bd9aab32d9cbf9231058d585479813f3420dc872 SOURCES/389-ds-base-1.4.3.39.tar.bz2
1c8f2d0dfbf39fa8cd86363bf3314351ab21f8d4 SOURCES/jemalloc-5.3.0.tar.bz2
978b7c5e4a9e5784fddb23ba1abe4dc5a071589f SOURCES/vendor-1.4.3.39-1.tar.gz

7
.gitignore vendored
View File

@ -1,3 +1,4 @@
SOURCES/389-ds-base-1.4.3.39.tar.bz2
SOURCES/jemalloc-5.3.0.tar.bz2
SOURCES/vendor-1.4.3.39-1.tar.gz
389-ds-base-3.2.0.tar.bz2
jemalloc-5.3.0.tar.bz2
libdb-5.3.28-59.tar.bz2
vendor-3.2.0-4.tar.gz

View File

@ -0,0 +1,318 @@
From 1c9c535888b9a850095794787d67900b04924a76 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
function idl_id_is_in_idlist is not scaling with large database (#7145)
Bug description:
During a online total initialization, the supplier sorts
the candidate list of entries so that the parents are sent before
children entries.
With large DB the ID array used for the sorting is not
scaling. It takes so long to build the candidate list that
the connection gets closed
Fix description:
Instead of using an ID array, uses a list of ID ranges
fixes: #7096
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 | 30 ++--
.../servers/slapd/back-ldbm/proto-back-ldbm.h | 3 +
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 1bc36720d..b187c26bc 100644
--- a/ldap/servers/slapd/back-ldbm/back-ldbm.h
+++ b/ldap/servers/slapd/back-ldbm/back-ldbm.h
@@ -282,6 +282,18 @@ typedef struct _idlist_set
#define INDIRECT_BLOCK(idl) ((idl)->b_nids == INDBLOCK)
#define IDL_NIDS(idl) (idl ? (idl)->b_nids : (NIDS)0)
+/*
+ * used by the supplier during online total init
+ * it stores the ranges of ID that are already present
+ * in the candidate list ('parentid>=1')
+ */
+typedef struct IdRange {
+ ID first;
+ ID last;
+ struct IdRange *next;
+} IdRange_t;
+
+
typedef size_t idl_iterator;
/* 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 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)
return (a->b_nids > b->b_nids ? b : a);
}
+/*
+ * This is a faster version of idl_id_is_in_idlist.
+ * idl_id_is_in_idlist uses an array of ID so lookup is expensive
+ * idl_id_is_in_idlist_ranges uses a list of ranges of ID lookup is faster
+ * returns
+ * 1: 'id' is present in idrange_list
+ * 0: 'id' is not present in idrange_list
+ */
+int
+idl_id_is_in_idlist_ranges(IDList *idl, IdRange_t *idrange_list, ID id)
+{
+ IdRange_t *range = idrange_list;
+ int found = 0;
+
+ if (NULL == idl || NOID == id) {
+ return 0; /* not in the list */
+ }
+ if (ALLIDS(idl)) {
+ return 1; /* in the list */
+ }
+
+ for(;range; range = range->next) {
+ if (id > range->last) {
+ /* check if it belongs to the next range */
+ continue;
+ }
+ if (id >= range->first) {
+ /* It belongs to that range [first..last ] */
+ found = 1;
+ break;
+ } else {
+ /* this range is after id */
+ break;
+ }
+ }
+ return found;
+}
+
+/* This function is used during the online total initialisation
+ * (see next function)
+ * It frees all ranges of ID in the list
+ */
+void idrange_free(IdRange_t **head)
+{
+ IdRange_t *curr, *sav;
+
+ if ((head == NULL) || (*head == NULL)) {
+ return;
+ }
+ curr = *head;
+ sav = NULL;
+ for (; curr;) {
+ sav = curr;
+ curr = curr->next;
+ slapi_ch_free((void *) &sav);
+ }
+ if (sav) {
+ slapi_ch_free((void *) &sav);
+ }
+ *head = NULL;
+}
+
+/* This function is used during the online total initialisation
+ * Because a MODRDN can move entries under a parent that
+ * has a higher ID we need to sort the IDList so that parents
+ * are sent, to the consumer, before the children are sent.
+ * The sorting with a simple IDlist does not scale instead
+ * a list of IDs ranges is much faster.
+ * In that list we only ADD/lookup ID.
+ */
+IdRange_t *idrange_add_id(IdRange_t **head, ID id)
+{
+ if (head == NULL) {
+ slapi_log_err(SLAPI_LOG_ERR, "idrange_add_id",
+ "Can not add ID %d in non defined list\n", id);
+ return NULL;
+ }
+
+ if (*head == NULL) {
+ /* This is the first range */
+ IdRange_t *new_range = (IdRange_t *)slapi_ch_malloc(sizeof(IdRange_t));
+ new_range->first = id;
+ new_range->last = id;
+ new_range->next = NULL;
+ *head = new_range;
+ return *head;
+ }
+
+ IdRange_t *curr = *head, *prev = NULL;
+
+ /* First, find if id already falls within any existing range, or it is adjacent to any */
+ while (curr) {
+ if (id >= curr->first && id <= curr->last) {
+ /* inside a range, nothing to do */
+ return curr;
+ }
+
+ if (id == curr->last + 1) {
+ /* Extend this range upwards */
+ curr->last = id;
+
+ /* Check for possible merge with next range */
+ IdRange_t *next = curr->next;
+ if (next && curr->last + 1 >= next->first) {
+ slapi_log_err(SLAPI_LOG_REPL, "idrange_add_id",
+ "(id=%d) merge current with next range [%d..%d]\n", id, curr->first, curr->last);
+ curr->last = (next->last > curr->last) ? next->last : curr->last;
+ curr->next = next->next;
+ slapi_ch_free((void*) &next);
+ } else {
+ slapi_log_err(SLAPI_LOG_REPL, "idrange_add_id",
+ "(id=%d) extend forward current range [%d..%d]\n", id, curr->first, curr->last);
+ }
+ return curr;
+ }
+
+ if (id + 1 == curr->first) {
+ /* Extend this range downwards */
+ curr->first = id;
+
+ /* Check for possible merge with previous range */
+ if (prev && prev->last + 1 >= curr->first) {
+ prev->last = curr->last;
+ prev->next = curr->next;
+ slapi_ch_free((void *) &curr);
+ slapi_log_err(SLAPI_LOG_REPL, "idrange_add_id",
+ "(id=%d) merge current with previous range [%d..%d]\n", id, prev->first, prev->last);
+ return prev;
+ } else {
+ slapi_log_err(SLAPI_LOG_REPL, "idrange_add_id",
+ "(id=%d) extend backward current range [%d..%d]\n", id, curr->first, curr->last);
+ return curr;
+ }
+ }
+
+ /* If id is before the current range, break so we can insert before */
+ if (id < curr->first) {
+ break;
+ }
+
+ prev = curr;
+ curr = curr->next;
+ }
+ /* Need to insert a new standalone IdRange */
+ IdRange_t *new_range = (IdRange_t *)slapi_ch_malloc(sizeof(IdRange_t));
+ new_range->first = id;
+ new_range->last = id;
+ new_range->next = curr;
+
+ if (prev) {
+ slapi_log_err(SLAPI_LOG_REPL, "idrange_add_id",
+ "(id=%d) add new range [%d..%d]\n", id, new_range->first, new_range->last);
+ prev->next = new_range;
+ } else {
+ /* Insert at head */
+ slapi_log_err(SLAPI_LOG_REPL, "idrange_add_id",
+ "(id=%d) head range [%d..%d]\n", id, new_range->first, new_range->last);
+ *head = new_range;
+ }
+ return *head;
+}
+
+
int
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 5fbcaff2e..2d978353f 100644
--- a/ldap/servers/slapd/back-ldbm/idl_new.c
+++ b/ldap/servers/slapd/back-ldbm/idl_new.c
@@ -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) {
@@ -578,10 +578,12 @@ idl_new_range_fetch(
* found entry is the one from the suffix
*/
suffix = key;
- idl_rc = idl_append_extend(&idl, id);
- } else if ((key == suffix) || idl_id_is_in_idlist(idl, key)) {
+ idl_append_extend(&idl, id);
+ idrange_add_id(&idrange_list, id);
+ } else if ((key == suffix) || idl_id_is_in_idlist_ranges(idl, idrange_list, key)) {
/* the parent is the suffix or already in idl. */
- idl_rc = idl_append_extend(&idl, id);
+ idl_append_extend(&idl, id);
+ idrange_add_id(&idrange_list, id);
} else {
/* Otherwise, keep the {key,id} in leftover array */
if (!leftover) {
@@ -596,13 +598,7 @@ idl_new_range_fetch(
leftovercnt++;
}
} else {
- idl_rc = idl_append_extend(&idl, id);
- }
- if (idl_rc) {
- slapi_log_err(SLAPI_LOG_ERR, "idl_new_range_fetch",
- "Unable to extend id list (err=%d)\n", idl_rc);
- idl_free(&idl);
- goto error;
+ idl_append_extend(&idl, id);
}
count++;
@@ -695,21 +691,17 @@ error:
while(remaining > 0) {
for (size_t i = 0; i < leftovercnt; i++) {
- if (leftover[i].key > 0 && idl_id_is_in_idlist(idl, leftover[i].key) != 0) {
+ if (leftover[i].key > 0 && idl_id_is_in_idlist_ranges(idl, idrange_list, leftover[i].key) != 0) {
/* if the leftover key has its parent in the idl */
- idl_rc = idl_append_extend(&idl, leftover[i].id);
- if (idl_rc) {
- slapi_log_err(SLAPI_LOG_ERR, "idl_new_range_fetch",
- "Unable to extend id list (err=%d)\n", idl_rc);
- idl_free(&idl);
- return NULL;
- }
+ idl_append_extend(&idl, leftover[i].id);
+ idrange_add_id(&idrange_list, leftover[i].id);
leftover[i].key = 0;
remaining--;
}
}
}
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",
diff --git a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
index 91d61098a..30a7aa11f 100644
--- a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
+++ b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
@@ -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);
+IdRange_t *idrange_add_id(IdRange_t **head, ID id);
+void idrange_free(IdRange_t **head);
+int idl_id_is_in_idlist_ranges(IDList *idl, IdRange_t *idrange_list, ID id);
int idl_id_is_in_idlist(IDList *idl, ID id);
idl_iterator idl_iterator_init(const IDList *idl);
--
2.52.0

View File

@ -0,0 +1,765 @@
From 446bc42e7b64a8496c2c3fe486f86bba318bed5e 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 6024fcd31..1f47c531c 100644
--- a/ldap/servers/slapd/abandon.c
+++ b/ldap/servers/slapd/abandon.c
@@ -179,7 +179,7 @@ do_abandon(Slapi_PBlock *pb)
logpb.tv_sec = -1;
logpb.tv_nsec = -1;
- 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)) {
if (log_format != LOG_FORMAT_DEFAULT) {
/* JSON logging */
logpb.target_op = "Simple Paged Results";
diff --git a/ldap/servers/slapd/opshared.c b/ldap/servers/slapd/opshared.c
index a5cddfd23..bf800f7dc 100644
--- a/ldap/servers/slapd/opshared.c
+++ b/ldap/servers/slapd/opshared.c
@@ -572,8 +572,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);
@@ -619,14 +619,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);
}
@@ -743,17 +736,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;
@@ -764,14 +755,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 */
@@ -789,22 +779,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;
}
}
@@ -915,7 +905,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);
#pragma GCC diagnostic pop
}
@@ -954,7 +944,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) {
@@ -967,23 +957,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);
@@ -993,7 +983,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 765c12bf5..455d6d718 100644
--- a/ldap/servers/slapd/proto-slap.h
+++ b/ldap/servers/slapd/proto-slap.h
@@ -1614,20 +1614,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);
@@ -1639,15 +1641,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 11c5602e3..d494931c2 100644
--- a/ldap/servers/slapd/slap.h
+++ b/ldap/servers/slapd/slap.h
@@ -89,6 +89,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>
@@ -1669,7 +1673,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

@ -0,0 +1,183 @@
From 4936f953fa3b0726c2b178f135cd78dcac7463ba Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Thu, 8 Jan 2026 10:02:39 -0800
Subject: [PATCH] Issue 7108 - Fix shutdown crash in entry cache destruction
(#7163)
Description: The entry cache could experience LRU list corruption when
using pinned entries, leading to crashes during cache flush operations.
In entrycache_add_int(), when returning an existing cached entry, the
code checked the wrong entry's state before calling lru_delete(). It
checked the new entry 'e' but operated on the existing entry 'my_alt',
causing lru_delete() to be called on entries not in the LRU list. This
is fixed by checking my_alt's refcnt and pinned state instead.
In flush_hash(), pinned_remove() and lru_delete() were both called on
pinned entries. Since pinned entries are in the pinned list, calling
lru_delete() afterwards corrupted the list. This is fixed by calling
either pinned_remove() or lru_delete() based on the entry's state.
A NULL check is added in entrycache_flush() and dncache_flush() to
gracefully handle corrupted LRU lists and prevent crashes when
traversing backwards through the list encounters an unexpected NULL.
Entry pointers are now always cleared after lru_delete() removal to
prevent stale pointer issues in non-debug builds.
Fixes: https://github.com/389ds/389-ds-base/issues/7108
Reviewed by: @progier389, @vashirov (Thanks!!)
---
ldap/servers/slapd/back-ldbm/cache.c | 48 +++++++++++++++++++++++++---
1 file changed, 43 insertions(+), 5 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/cache.c b/ldap/servers/slapd/back-ldbm/cache.c
index 2e4126134..a87f30687 100644
--- a/ldap/servers/slapd/back-ldbm/cache.c
+++ b/ldap/servers/slapd/back-ldbm/cache.c
@@ -458,11 +458,13 @@ static void
lru_delete(struct cache *cache, void *ptr)
{
struct backcommon *e;
+
if (NULL == ptr) {
LOG("=> lru_delete\n<= lru_delete (null entry)\n");
return;
}
e = (struct backcommon *)ptr;
+
#ifdef LDAP_CACHE_DEBUG_LRU
pinned_verify(cache, __LINE__);
lru_verify(cache, e, 1);
@@ -475,8 +477,9 @@ lru_delete(struct cache *cache, void *ptr)
e->ep_lrunext->ep_lruprev = e->ep_lruprev;
else
cache->c_lrutail = e->ep_lruprev;
-#ifdef LDAP_CACHE_DEBUG_LRU
+ /* Always clear pointers after removal to prevent stale pointer issues */
e->ep_lrunext = e->ep_lruprev = NULL;
+#ifdef LDAP_CACHE_DEBUG_LRU
lru_verify(cache, e, 0);
#endif
}
@@ -633,9 +636,14 @@ flush_hash(struct cache *cache, struct timespec *start_time, int32_t type)
if (entry->ep_refcnt == 0) {
entry->ep_refcnt++;
if (entry->ep_state & ENTRY_STATE_PINNED) {
+ /* Entry is in pinned list, not LRU - remove from pinned only.
+ * pinned_remove clears lru pointers and won't add to LRU since refcnt > 0.
+ */
pinned_remove(cache, laste);
+ } else {
+ /* Entry is in LRU list - remove from LRU */
+ lru_delete(cache, laste);
}
- lru_delete(cache, laste);
if (type == ENTRY_CACHE) {
entrycache_remove_int(cache, laste);
entrycache_return(cache, (struct backentry **)&laste, PR_TRUE);
@@ -679,9 +687,14 @@ flush_hash(struct cache *cache, struct timespec *start_time, int32_t type)
if (entry->ep_refcnt == 0) {
entry->ep_refcnt++;
if (entry->ep_state & ENTRY_STATE_PINNED) {
+ /* Entry is in pinned list, not LRU - remove from pinned only.
+ * pinned_remove clears lru pointers and won't add to LRU since refcnt > 0.
+ */
pinned_remove(cache, laste);
+ } else {
+ /* Entry is in LRU list - remove from LRU */
+ lru_delete(cache, laste);
}
- lru_delete(cache, laste);
entrycache_remove_int(cache, laste);
entrycache_return(cache, (struct backentry **)&laste, PR_TRUE);
} else {
@@ -772,6 +785,11 @@ entrycache_flush(struct cache *cache)
} else {
e = BACK_LRU_PREV(e, struct backentry *);
}
+ if (e == NULL) {
+ slapi_log_err(SLAPI_LOG_WARNING, "entrycache_flush",
+ "Unexpected NULL entry while flushing cache - LRU list may be corrupted\n");
+ break;
+ }
ASSERT(e->ep_refcnt == 0);
e->ep_refcnt++;
if (entrycache_remove_int(cache, e) < 0) {
@@ -1160,6 +1178,7 @@ pinned_remove(struct cache *cache, void *ptr)
{
struct backentry *e = (struct backentry *)ptr;
ASSERT(e->ep_state & ENTRY_STATE_PINNED);
+
cache->c_pinned_ctx->npinned--;
cache->c_pinned_ctx->size -= e->ep_size;
e->ep_state &= ~ENTRY_STATE_PINNED;
@@ -1172,13 +1191,23 @@ pinned_remove(struct cache *cache, void *ptr)
cache->c_pinned_ctx->head = cache->c_pinned_ctx->tail = NULL;
} else {
cache->c_pinned_ctx->head = BACK_LRU_NEXT(e, struct backentry *);
+ /* Update new head's prev pointer to NULL */
+ if (cache->c_pinned_ctx->head) {
+ cache->c_pinned_ctx->head->ep_lruprev = NULL;
+ }
}
} else if (cache->c_pinned_ctx->tail == e) {
cache->c_pinned_ctx->tail = BACK_LRU_PREV(e, struct backentry *);
+ /* Update new tail's next pointer to NULL */
+ if (cache->c_pinned_ctx->tail) {
+ cache->c_pinned_ctx->tail->ep_lrunext = NULL;
+ }
} else {
+ /* Middle of list: update both neighbors to point to each other */
BACK_LRU_PREV(e, struct backentry *)->ep_lrunext = BACK_LRU_NEXT(e, struct backcommon *);
BACK_LRU_NEXT(e, struct backentry *)->ep_lruprev = BACK_LRU_PREV(e, struct backcommon *);
}
+ /* Clear the removed entry's pointers */
e->ep_lrunext = e->ep_lruprev = NULL;
if (e->ep_refcnt == 0) {
lru_add(cache, ptr);
@@ -1245,6 +1274,7 @@ pinned_add(struct cache *cache, void *ptr)
return false;
}
/* Now it is time to insert the entry in the pinned list */
+
cache->c_pinned_ctx->npinned++;
cache->c_pinned_ctx->size += e->ep_size;
e->ep_state |= ENTRY_STATE_PINNED;
@@ -1754,7 +1784,7 @@ entrycache_add_int(struct cache *cache, struct backentry *e, int state, struct b
* 3) ep_state: 0 && state: 0
* ==> increase the refcnt
*/
- if (e->ep_refcnt == 0)
+ if (e->ep_refcnt == 0 && (e->ep_state & ENTRY_STATE_PINNED) == 0)
lru_delete(cache, (void *)e);
e->ep_refcnt++;
e->ep_state &= ~ENTRY_STATE_UNAVAILABLE;
@@ -1781,7 +1811,7 @@ entrycache_add_int(struct cache *cache, struct backentry *e, int state, struct b
} else {
if (alt) {
*alt = my_alt;
- if (e->ep_refcnt == 0 && (e->ep_state & ENTRY_STATE_PINNED) == 0)
+ if (my_alt->ep_refcnt == 0 && (my_alt->ep_state & ENTRY_STATE_PINNED) == 0)
lru_delete(cache, (void *)*alt);
(*alt)->ep_refcnt++;
LOG("the entry %s already exists. returning existing entry %s (state: 0x%x)\n",
@@ -2379,6 +2409,14 @@ dncache_flush(struct cache *cache)
} else {
dn = BACK_LRU_PREV(dn, struct backdn *);
}
+ if (dn == NULL) {
+ /* Safety check: we should normally exit via the CACHE_LRU_HEAD check.
+ * If we get here, c_lruhead may be NULL or the LRU list is corrupted.
+ */
+ slapi_log_err(SLAPI_LOG_WARNING, "dncache_flush",
+ "Unexpected NULL entry while flushing cache - LRU list may be corrupted\n");
+ break;
+ }
ASSERT(dn->ep_refcnt == 0);
dn->ep_refcnt++;
if (dncache_remove_int(cache, dn) < 0) {
--
2.52.0

View File

@ -0,0 +1,215 @@
From 742c12e0247ab64e87da000a4de2f3e5c99044ab 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 | 107 +++++++++++++++++++++++-
2 files changed, 123 insertions(+), 9 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/instance.c b/ldap/servers/slapd/back-ldbm/instance.c
index cb002c379..71bf0f6fa 100644
--- a/ldap/servers/slapd/back-ldbm/instance.c
+++ b/ldap/servers/slapd/back-ldbm/instance.c
@@ -190,6 +190,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;
@@ -248,10 +249,14 @@ ldbm_instance_create_default_indexes(backend *be)
ldbm_instance_config_add_index_entry(inst, e, flags);
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);
@@ -288,10 +293,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);
slapi_ch_free_string(&parentid_indexes_limit);
diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
index 858392564..b02e37ed6 100644
--- a/ldap/servers/slapd/upgrade.c
+++ b/ldap/servers/slapd/upgrade.c
@@ -330,6 +330,107 @@ upgrade_remove_subtree_rename(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 the base config of the PAM PTA plugin.
*
@@ -547,7 +648,11 @@ upgrade_server(void)
if (upgrade_pam_pta_default_config() != UPGRADE_SUCCESS) {
return UPGRADE_FAILURE;
}
-
+
+ if (upgrade_check_id_index_matching_rule() != UPGRADE_SUCCESS) {
+ return UPGRADE_FAILURE;
+ }
+
return UPGRADE_SUCCESS;
}
--
2.52.0

View File

@ -0,0 +1,67 @@
From f5de84e309d5a4435198c9cc9b31b5722979f1ff Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Mon, 12 Jan 2026 10:58:02 +0100
Subject: [PATCH] Issue 7172 - (2nd) Index ordering mismatch after upgrade
(#7180)
Commit 742c12e0247ab64e87da000a4de2f3e5c99044ab introduced a regression
where the check to skip creating parentid/ancestorid indexes if they
already exist was incorrect.
The `ainfo_get()` function falls back to returning
LDBM_PSEUDO_ATTR_DEFAULT attrinfo when the requested attribute is not
found.
Since LDBM_PSEUDO_ATTR_DEFAULT is created before the ancestorid check,
`ainfo_get()` returns LDBM_PSEUDO_ATTR_DEFAULT instead of NULL, causing
the ancestorid index creation to be skipped entirely.
When operations later try to use the ancestorid index, they fall back to
LDBM_PSEUDO_ATTR_DEFAULT, and attempting to open the .default dbi
mid-transaction fails with MDB_NOTFOUND (-30798).
Fix Description:
Instead of just checking if `ainfo_get()` returns non-NULL, verify that
the returned attrinfo is actually for the requested attribute.
Fixes: https://github.com/389ds/389-ds-base/issues/7172
Reviewed by: @tbordaz (Thanks!)
---
ldap/servers/slapd/back-ldbm/instance.c | 8 +++++---
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 71bf0f6fa..2a6e8cbb8 100644
--- a/ldap/servers/slapd/back-ldbm/instance.c
+++ b/ldap/servers/slapd/back-ldbm/instance.c
@@ -190,7 +190,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;
+ int index_already_configured = 0;
struct index_idlistsizeinfo *iter;
int cookie;
int limit;
@@ -250,7 +250,8 @@ ldbm_instance_create_default_indexes(backend *be)
slapi_entry_free(e);
ainfo_get(be, (char *)LDBM_PARENTID_STR, &ai);
- index_already_configured = ai;
+ /* Check if the attrinfo is actually for parentid, not a fallback to .default */
+ index_already_configured = (ai != NULL && strcmp(ai->ai_type, LDBM_PARENTID_STR) == 0);
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);
@@ -294,7 +295,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);
- index_already_configured = ai;
+ /* Check if the attrinfo is actually for ancestorid, not a fallback to .default */
+ index_already_configured = (ai != NULL && strcmp(ai->ai_type, LDBM_ANCESTORID_STR) == 0);
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);
--
2.52.0

View File

@ -0,0 +1,924 @@
From fb23c9e366f5eafa6bdbb8cd71afd78e3edefde2 Mon Sep 17 00:00:00 2001
From: Lenka Doudova <mirielka@users.noreply.github.com>
Date: Tue, 13 Jan 2026 10:44:15 +0100
Subject: [PATCH] Issue 6753 - Port ticket 548 test (#7101)
Description:
Port ticket 548 test into
dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py
Relates: #6753
Author: Lenka Doudova, aadhikar
Assisted by: Cursor
Reviewer: @droideck(Thanks!)
---
.../password/pwdPolicy_attribute_test.py | 464 +++++++++++++++++-
dirsrvtests/tests/tickets/ticket548_test.py | 408 ---------------
2 files changed, 463 insertions(+), 409 deletions(-)
delete mode 100644 dirsrvtests/tests/tickets/ticket548_test.py
diff --git a/dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py b/dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py
index d0c172f94..d021f4720 100644
--- a/dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py
+++ b/dirsrvtests/tests/suites/password/pwdPolicy_attribute_test.py
@@ -9,12 +9,12 @@
import pytest
from lib389.tasks import *
from lib389.utils import *
-import pdb
from lib389.topologies import topology_st
from lib389.pwpolicy import PwPolicyManager
from lib389.idm.user import UserAccount, UserAccounts, TEST_USER_PROPERTIES
from lib389.idm.organizationalunit import OrganizationalUnits
from lib389._constants import (DEFAULT_SUFFIX, DN_DM, PASSWORD)
+from lib389.idm.directorymanager import DirectoryManager
pytestmark = pytest.mark.tier1
@@ -361,6 +361,468 @@ def test_pwdpolicysubentry(topology_st, password_policy):
assert 'nsPwPolicyEntry_user' not in pwp_subentry
+@pytest.fixture(scope="function")
+def shadowUser(request, topology_st):
+ """ Create a user with shadowAccount objectclass """
+ users = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX)
+ shadowUser = users.create(properties={
+ 'objectclass': ['top', 'person', 'organizationalPerson',
+ 'inetOrgPerson', 'extensibleObject', 'shadowAccount'],
+ 'sn': '1',
+ 'cn': 'shadowUser',
+ 'uid': 'shadowUser',
+ 'uidNumber': '1',
+ 'gidNumber': '11',
+ 'homeDirectory': '/home/shadowUser',
+ 'displayName': 'Shadow User',
+ 'givenname': 'Shadow',
+ 'mail':f'shadowuser@{DEFAULT_SUFFIX}',
+ 'userpassword': 'password'
+ })
+
+ def fin():
+ if shadowUser.exists():
+ shadowUser.delete()
+
+ request.addfinalizer(fin)
+
+ return shadowUser
+
+
+def days_to_secs(days):
+ """ Convert days to seconds """
+ return days * 86400
+
+
+def check_shadow_attr_value(inst, user_dn, attr_type, expected):
+ """ Check that shadowAccount attribute has expected value """
+ dm = DirectoryManager(inst)
+ dm.rebind()
+ user = UserAccount(inst, user_dn)
+ assert user.present(attr_type), f'Entry {user_dn} does not have {attr_type} attribute'
+ actual = int(user.get_attr_val_utf8(attr_type))
+ assert actual == expected, f'{attr_type} of entry {user_dn} is {actual}, expected {expected}'
+ log.info(f'{attr_type} of entry {user_dn} has expected value {actual}')
+
+
+def setup_pwp(inst, pwp_mgr, policy, dn=None, policy_props=None):
+ """ Setup password policy """
+
+ log.info(f'Setting up {policy} password policy for {dn}')
+ dm = DirectoryManager(inst)
+ dm.rebind()
+
+ log.info(f'Configuring {policy} password policy')
+
+ assert policy == 'global' or dn, 'dn is required for non-global policy'
+
+ if not policy_props:
+ policy_props = {
+ 'passwordMinAge': str(days_to_secs(1)),
+ 'passwordExp': 'on',
+ 'passwordMaxAge': str(days_to_secs(10)),
+ 'passwordWarning': str(days_to_secs(3))
+ }
+
+ if policy == 'global':
+ pwp_mgr.set_global_policy(policy_props)
+ elif policy == 'subtree':
+ pwp_mgr.create_subtree_policy(dn, policy_props)
+ elif policy == 'user':
+ pwp_mgr.create_user_policy(dn, policy_props)
+ else:
+ raise ValueError(f'Invalid type of password policy: {policy}')
+
+
+def modify_pwp(inst, pwp_mgr, policy, dn=None, policy_props=None):
+ """ Modify password policy """
+ dm = DirectoryManager(inst)
+ dm.rebind()
+
+ assert policy == 'global' or dn, 'dn is required for non-global policy'
+
+ if not policy_props:
+ policy_props = {
+ 'passwordMinAge': str(days_to_secs(3)),
+ 'passwordMaxAge': str(days_to_secs(30)),
+ 'passwordWarning': str(days_to_secs(9))
+ }
+
+ if policy == 'global':
+ pwp_mgr.set_global_policy(properties=policy_props)
+ elif policy in ['subtree', 'user']:
+ policy_entry = pwp_mgr.get_pwpolicy_entry(dn)
+ policy_entry.replace_many(*policy_props.items())
+ else:
+ raise ValueError(f'Invalid type of password policy: {policy}')
+ log.info(f'Modified {policy} policy with {policy_props}.')
+
+@pytest.mark.skipif(ds_is_older('1.3.6'), reason="Not implemented")
+def test_shadowaccount_no_policy(topology_st, shadowUser):
+ """Check shadowAccount under no password policy
+
+ :id: a1b2c3d4-5e6f-7890-abcd-ef1234567890
+ :setup: Standalone instance
+ :steps:
+ 1. Add a user with shadowAccount objectclass
+ 2. Bind as the user
+ 3. Check shadowLastChange attribute is set correctly
+ :expectedresults:
+ 1. User is added successfully
+ 2. Bind is successful
+ 3. shadowLastChange is set correctly (days since epoch)
+ """
+
+ edate = int(time.time() / (60 * 60 * 24))
+
+ log.info(f"Bind as {shadowUser.dn}")
+ shadowUser.rebind('password')
+ check_shadow_attr_value(topology_st.standalone, shadowUser.dn,
+ 'shadowLastChange', edate)
+
+
+@pytest.mark.skipif(ds_is_older('1.3.6'), reason="Not implemented")
+def test_shadowaccount_global_policy(topology_st, shadowUser, request):
+ """Check shadowAccount with global password policy
+
+ :id: b2c3d4e5-6f7a-8901-bcde-f23456789012
+ :setup: Standalone instance
+ :steps:
+ 1. Set global password policy
+ 2. Add a second shadowAccount user
+ 3. Bind as each user and check shadowAccount attributes
+ 4. Modify global password policy
+ 5. Change user password (as the user, not DM)
+ 6. Re-bind with new password
+ 7. Check shadowAccount attributes are updated
+ 8. Clean up - delete second user and reset policy
+ :expectedresults:
+ 1. Global password policy is set successfully
+ 2. Second user is added
+ 3. shadowAccount attributes match policy values for both users
+ 4. Password policy is modified successfully
+ 5. Password is changed successfully
+ 6. Re-bind with new password is successful
+ 7. shadowAccount attributes are updated to match new policy values
+ 8. Cleanup is successful
+ """
+ inst = topology_st.standalone
+
+ log.info('Create second shadowAccount user')
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ shadowUser2 = users.create(properties={
+ 'objectclass': ['top', 'person', 'organizationalPerson',
+ 'inetOrgPerson', 'extensibleObject', 'shadowAccount'],
+ 'sn': '2',
+ 'cn': 'shadowUser2',
+ 'uid': 'shadowUser2',
+ 'uidNumber': '2',
+ 'gidNumber': '22',
+ 'homeDirectory': '/home/shadowUser2',
+ 'displayName': 'Shadow User 2',
+ 'givenname': 'Shadow2',
+ 'mail': f'shadowuser2@{DEFAULT_SUFFIX}',
+ 'userpassword': 'password'
+ })
+
+ def fin():
+ log.info('Clean up - delete second user and reset global policy')
+ dm = DirectoryManager(inst)
+ dm.rebind()
+ try:
+ shadowUser2.delete()
+ except Exception:
+ pass
+ inst.config.replace('passwordMinAge', '0')
+ inst.config.replace('passwordMaxAge', '8640000')
+ inst.config.replace('passwordWarning', '86400')
+ inst.config.replace('passwordExp', 'off')
+ request.addfinalizer(fin)
+
+ log.info('Configure global password policy')
+ pwp_mgr = PwPolicyManager(inst)
+ setup_pwp(inst, pwp_mgr, 'global')
+
+ edate = int(time.time() / (60 * 60 * 24))
+
+ log.info('Verify attributes of shadowUser (user 1)')
+ shadowUser.rebind('password')
+ check_shadow_attr_value(inst, shadowUser.dn,
+ 'shadowLastChange', edate)
+ check_shadow_attr_value(inst, shadowUser.dn,
+ 'shadowMin', 1)
+ check_shadow_attr_value(inst, shadowUser.dn,
+ 'shadowMax', 10)
+ check_shadow_attr_value(inst, shadowUser.dn,
+ 'shadowWarning', 3)
+
+ log.info('Verify attributes of shadowUser2 (user 2)')
+ shadowUser2.rebind('password')
+ check_shadow_attr_value(inst, shadowUser2.dn,
+ 'shadowLastChange', edate)
+ check_shadow_attr_value(inst, shadowUser2.dn,
+ 'shadowMin', 1)
+ check_shadow_attr_value(inst, shadowUser2.dn,
+ 'shadowMax', 10)
+ check_shadow_attr_value(inst, shadowUser2.dn,
+ 'shadowWarning', 3)
+
+ log.info('Modify global password policy')
+ modify_pwp(inst, pwp_mgr, 'global')
+
+ log.info('Change shadowUser2 password as the user')
+ shadowUser2.rebind('password')
+ shadowUser2.replace('userpassword', 'password2')
+ time.sleep(1)
+
+ log.info('Re-bind as shadowUser2 with new password')
+ shadowUser2.rebind('password2')
+
+ log.info('Verify modified shadowUser2 attributes')
+ check_shadow_attr_value(inst, shadowUser2.dn,
+ 'shadowMin', 3)
+ check_shadow_attr_value(inst, shadowUser2.dn,
+ 'shadowMax', 30)
+ check_shadow_attr_value(inst, shadowUser2.dn,
+ 'shadowWarning', 9)
+
+
+@pytest.mark.skipif(ds_is_older('1.3.6'), reason="Not implemented")
+def test_shadowaccount_subtree_policy(topology_st, request):
+ """Check shadowAccount with subtree level password policy
+
+ :id: c3d4e5f6-7a8b-9012-cdef-345678901234
+ :setup: Standalone instance
+ :steps:
+ 1. Create subtree password policy for DEFAULT_SUFFIX with passwordMustChange on
+ 2. Add a new shadowAccount user under the subtree
+ 3. Check shadowLastChange is 0 (since passwordMustChange is on)
+ 4. Verify search as user fails with UNWILLING_TO_PERFORM
+ 5. Change user password (as user)
+ 6. Re-bind with new password
+ 7. Check shadowAccount attributes are updated with correct values
+ 8. Clean up subtree password policy
+ :expectedresults:
+ 1. Subtree password policy is created successfully
+ 2. User is added successfully
+ 3. shadowLastChange is 0 until password is changed
+ 4. Search fails with UNWILLING_TO_PERFORM as expected
+ 5. Password is changed successfully
+ 6. Re-bind with new password is successful
+ 7. shadowAccount attributes are updated to match policy values
+ 8. Subtree password policy is deleted successfully
+ """
+ inst = topology_st.standalone
+ subtree_dn = DEFAULT_SUFFIX
+
+ log.info('Configure subtree password policy with passwordMustChange on')
+ properties = {
+ 'passwordMustChange': 'on',
+ 'passwordExp': 'on',
+ 'passwordMinAge': str(days_to_secs(2)),
+ 'passwordMaxAge': str(days_to_secs(20)),
+ 'passwordWarning': str(days_to_secs(6)),
+ 'passwordChange': 'on',
+ 'passwordStorageScheme': 'clear'
+ }
+
+ pwp_mgr = PwPolicyManager(inst)
+ setup_pwp(inst, pwp_mgr, 'subtree', dn=subtree_dn, policy_props=properties)
+
+ def fin():
+ log.info('Clean up: delete subtree password policy')
+ dm = DirectoryManager(inst)
+ dm.rebind()
+ try:
+ pwp_mgr.delete_local_policy(subtree_dn)
+ except Exception:
+ pass
+ try:
+ subtree_user.delete()
+ except Exception:
+ pass
+ request.addfinalizer(fin)
+
+ log.info('Add a new shadowAccount user under the subtree')
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ subtree_user = users.create(properties={
+ 'objectclass': ['top', 'person', 'organizationalPerson',
+ 'inetOrgPerson', 'extensibleObject', 'shadowAccount'],
+ 'sn': '3',
+ 'cn': 'subtreeUser',
+ 'uid': 'subtreeUser',
+ 'uidNumber': '3',
+ 'gidNumber': '33',
+ 'homeDirectory': '/home/subtreeUser',
+ 'displayName': 'Subtree User',
+ 'givenname': 'Subtree',
+ 'mail': f'subtreeuser@{DEFAULT_SUFFIX}',
+ 'userpassword': 'password'
+ })
+
+ dm = DirectoryManager(inst)
+ dm.rebind()
+
+ log.info('Verify shadowLastChange is 0 since passwordMustChange is on')
+ check_shadow_attr_value(inst, subtree_user.dn,
+ 'shadowLastChange', 0)
+ check_shadow_attr_value(inst, subtree_user.dn,
+ 'shadowMin', 2)
+ check_shadow_attr_value(inst, subtree_user.dn,
+ 'shadowMax', 20)
+ check_shadow_attr_value(inst, subtree_user.dn,
+ 'shadowWarning', 6)
+
+ log.info(f'Bind as {subtree_user.dn} and verify search fails with UNWILLING_TO_PERFORM')
+ subtree_user.rebind('password')
+ with pytest.raises(ldap.UNWILLING_TO_PERFORM):
+ subtree_user.exists()
+
+ log.info('Modify subtree password policy')
+ dm.rebind()
+ modify_properties = {
+ 'passwordMinAge': str(days_to_secs(4)),
+ 'passwordMaxAge': str(days_to_secs(40)),
+ 'passwordWarning': str(days_to_secs(12))
+ }
+ modify_pwp(inst, pwp_mgr, 'subtree', dn=subtree_dn, policy_props=modify_properties)
+
+ log.info(f'Change {subtree_user.dn} password as the user')
+ subtree_user.rebind('password')
+ subtree_user.replace('userpassword', 'password0')
+ time.sleep(1)
+
+ log.info(f'Re-bind as {subtree_user.dn} with new password')
+ subtree_user.rebind('password0')
+
+ edate = int(time.time() / (60 * 60 * 24))
+
+ log.info('Verify shadowLastChange is now set to today after password change')
+ check_shadow_attr_value(inst, subtree_user.dn,
+ 'shadowLastChange', edate)
+ check_shadow_attr_value(inst, subtree_user.dn,
+ 'shadowMin', 4)
+ check_shadow_attr_value(inst, subtree_user.dn,
+ 'shadowMax', 40)
+ check_shadow_attr_value(inst, subtree_user.dn,
+ 'shadowWarning', 12)
+
+
+@pytest.mark.skipif(ds_is_older('1.3.6'), reason="Not implemented")
+def test_shadowaccount_user_policy(topology_st, request):
+ """Check shadowAccount with user level password policy
+
+ :id: d4e5f6a7-8b9c-0123-def0-456789012345
+ :setup: Standalone instance
+ :steps:
+ 1. Create a new shadowAccount user
+ 2. Create user password policy
+ 3. Verify shadowAccount attributes match policy values
+ 4. Modify user password policy
+ 5. Change user password
+ 6. Re-bind with new password
+ 7. Check shadowAccount attributes are updated
+ 8. Clean up user password policy
+ :expectedresults:
+ 1. User is created successfully
+ 2. User password policy is created successfully
+ 3. shadowAccount attributes match policy values
+ 4. Password policy is modified successfully
+ 5. Password is changed successfully
+ 6. Re-bind with new password is successful
+ 7. shadowAccount attributes are updated to match new policy values
+ 8. User password policy is deleted successfully
+ """
+ inst = topology_st.standalone
+
+ log.info('Create a new shadowAccount user for user policy test')
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+ user_policy_user = users.create(properties={
+ 'objectclass': ['top', 'person', 'organizationalPerson',
+ 'inetOrgPerson', 'extensibleObject', 'shadowAccount'],
+ 'sn': '4',
+ 'cn': 'userPolicyUser',
+ 'uid': 'userPolicyUser',
+ 'uidNumber': '4',
+ 'gidNumber': '44',
+ 'homeDirectory': '/home/userPolicyUser',
+ 'displayName': 'User Policy User',
+ 'givenname': 'UserPolicy',
+ 'mail': f'userpolicyuser@{DEFAULT_SUFFIX}',
+ 'userpassword': 'password'
+ })
+
+ pwp_mgr = PwPolicyManager(inst)
+
+ def fin():
+ log.info('Clean up: delete user password policy and user')
+ dm = DirectoryManager(inst)
+ dm.rebind()
+ try:
+ pwp_mgr.delete_local_policy(user_policy_user.dn)
+ except Exception:
+ pass
+ try:
+ user_policy_user.delete()
+ except Exception:
+ pass
+ request.addfinalizer(fin)
+
+ log.info('Configure user password policy')
+ properties = {
+ 'passwordExp': 'on',
+ 'passwordMinAge': str(days_to_secs(2)),
+ 'passwordMaxAge': str(days_to_secs(20)),
+ 'passwordWarning': str(days_to_secs(6)),
+ 'passwordChange': 'on',
+ 'passwordStorageScheme': 'clear'
+ }
+ setup_pwp(inst, pwp_mgr, 'user', dn=user_policy_user.dn, policy_props=properties)
+
+ edate = int(time.time() / (60 * 60 * 24))
+
+ dm = DirectoryManager(inst)
+ dm.rebind()
+
+ log.info('Verify shadowAccount attributes match user policy')
+ check_shadow_attr_value(inst, user_policy_user.dn,
+ 'shadowLastChange', edate)
+ check_shadow_attr_value(inst, user_policy_user.dn,
+ 'shadowMin', 2)
+ check_shadow_attr_value(inst, user_policy_user.dn,
+ 'shadowMax', 20)
+ check_shadow_attr_value(inst, user_policy_user.dn,
+ 'shadowWarning', 6)
+
+ log.info('Modify user password policy')
+ modify_properties = {
+ 'passwordMinAge': str(days_to_secs(4)),
+ 'passwordMaxAge': str(days_to_secs(40)),
+ 'passwordWarning': str(days_to_secs(12))
+ }
+ modify_pwp(inst, pwp_mgr, 'user', dn=user_policy_user.dn, policy_props=modify_properties)
+
+ log.info(f'Change {user_policy_user.dn} password as the user')
+ user_policy_user.rebind('password')
+ user_policy_user.replace('userpassword', 'password0')
+ time.sleep(1)
+
+ log.info(f'Re-bind as {user_policy_user.dn} with new password')
+ user_policy_user.rebind('password0')
+
+ edate = int(time.time() / (60 * 60 * 24))
+
+ log.info('Verify shadowAccount attributes are updated after password change')
+ check_shadow_attr_value(inst, user_policy_user.dn,
+ 'shadowLastChange', edate)
+ check_shadow_attr_value(inst, user_policy_user.dn,
+ 'shadowMin', 4)
+ check_shadow_attr_value(inst, user_policy_user.dn,
+ 'shadowMax', 40)
+ check_shadow_attr_value(inst, user_policy_user.dn,
+ 'shadowWarning', 12)
+
+
if __name__ == '__main__':
# Run isolated
# -s for DEBUG mode
diff --git a/dirsrvtests/tests/tickets/ticket548_test.py b/dirsrvtests/tests/tickets/ticket548_test.py
deleted file mode 100644
index cac3cc5f8..000000000
--- a/dirsrvtests/tests/tickets/ticket548_test.py
+++ /dev/null
@@ -1,408 +0,0 @@
-# --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2016 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.tasks import *
-from lib389.utils import *
-from lib389.topologies import topology_st
-
-from lib389._constants import DEFAULT_SUFFIX, DN_CONFIG, DN_DM, PASSWORD, DEFAULT_SUFFIX_ESCAPED
-
-# Skip on older versions
-pytestmark = [pytest.mark.tier2,
- pytest.mark.skipif(ds_is_older('1.3.6'), reason="Not implemented")]
-
-log = logging.getLogger(__name__)
-
-# Assuming DEFAULT_SUFFIX is "dc=example,dc=com", otherwise it does not work... :(
-SUBTREE_CONTAINER = 'cn=nsPwPolicyContainer,' + DEFAULT_SUFFIX
-SUBTREE_PWPDN = 'cn=nsPwPolicyEntry,' + DEFAULT_SUFFIX
-SUBTREE_PWP = 'cn=cn\3DnsPwPolicyEntry\2C' + DEFAULT_SUFFIX_ESCAPED + ',' + SUBTREE_CONTAINER
-SUBTREE_COS_TMPLDN = 'cn=nsPwTemplateEntry,' + DEFAULT_SUFFIX
-SUBTREE_COS_TMPL = 'cn=cn\3DnsPwTemplateEntry\2C' + DEFAULT_SUFFIX_ESCAPED + ',' + SUBTREE_CONTAINER
-SUBTREE_COS_DEF = 'cn=nsPwPolicy_CoS,' + DEFAULT_SUFFIX
-
-USER1_DN = 'uid=user1,' + DEFAULT_SUFFIX
-USER2_DN = 'uid=user2,' + DEFAULT_SUFFIX
-USER3_DN = 'uid=user3,' + DEFAULT_SUFFIX
-USER_PW = 'password'
-
-
-def days_to_secs(days):
- # Value of 60 * 60 * 24
- return days * 86400
-
-
-# Values are in days
-def set_global_pwpolicy(topology_st, min_=1, max_=10, warn=3):
- log.info(" +++++ Enable global password policy +++++\n")
- # Enable password policy
- try:
- topology_st.standalone.modify_s(DN_CONFIG, [(ldap.MOD_REPLACE, 'nsslapd-pwpolicy-local', b'on')])
- except ldap.LDAPError as e:
- log.error('Failed to set pwpolicy-local: error ' + e.message['desc'])
- assert False
-
- # Convert our values to seconds
- min_secs = days_to_secs(min_)
- max_secs = days_to_secs(max_)
- warn_secs = days_to_secs(warn)
-
- log.info(" Set global password Min Age -- %s day\n" % min_)
- try:
- topology_st.standalone.modify_s(DN_CONFIG, [(ldap.MOD_REPLACE, 'passwordMinAge', ('%s' % min_secs).encode())])
- except ldap.LDAPError as e:
- log.error('Failed to set passwordMinAge: error ' + e.message['desc'])
- assert False
-
- log.info(" Set global password Expiration -- on\n")
- try:
- topology_st.standalone.modify_s(DN_CONFIG, [(ldap.MOD_REPLACE, 'passwordExp', b'on')])
- except ldap.LDAPError as e:
- log.error('Failed to set passwordExp: error ' + e.message['desc'])
- assert False
-
- log.info(" Set global password Max Age -- %s days\n" % max_)
- try:
- topology_st.standalone.modify_s(DN_CONFIG, [(ldap.MOD_REPLACE, 'passwordMaxAge', ('%s' % max_secs).encode())])
- except ldap.LDAPError as e:
- log.error('Failed to set passwordMaxAge: error ' + e.message['desc'])
- assert False
-
- log.info(" Set global password Warning -- %s days\n" % warn)
- try:
- topology_st.standalone.modify_s(DN_CONFIG, [(ldap.MOD_REPLACE, 'passwordWarning', ('%s' % warn_secs).encode())])
- except ldap.LDAPError as e:
- log.error('Failed to set passwordWarning: error ' + e.message['desc'])
- assert False
-
-
-def set_subtree_pwpolicy(topology_st, min_=2, max_=20, warn=6):
- log.info(" +++++ Enable subtree level password policy +++++\n")
-
- # Convert our values to seconds
- min_secs = days_to_secs(min_)
- max_secs = days_to_secs(max_)
- warn_secs = days_to_secs(warn)
-
- log.info(" Add the container")
- try:
- topology_st.standalone.add_s(Entry((SUBTREE_CONTAINER, {'objectclass': 'top nsContainer'.split(),
- 'cn': 'nsPwPolicyContainer'})))
- except ldap.ALREADY_EXISTS:
- pass
- except ldap.LDAPError as e:
- log.error('Failed to add subtree container: error ' + e.message['desc'])
- # assert False
-
- try:
- # Purge the old policy
- topology_st.standalone.delete_s(SUBTREE_PWP)
- except:
- pass
-
- log.info(
- " Add the password policy subentry {passwordMustChange: on, passwordMinAge: %s, passwordMaxAge: %s, passwordWarning: %s}" % (
- min_, max_, warn))
- try:
- topology_st.standalone.add_s(Entry((SUBTREE_PWP, {'objectclass': 'top ldapsubentry passwordpolicy'.split(),
- 'cn': SUBTREE_PWPDN,
- 'passwordMustChange': 'on',
- 'passwordExp': 'on',
- 'passwordMinAge': '%s' % min_secs,
- 'passwordMaxAge': '%s' % max_secs,
- 'passwordWarning': '%s' % warn_secs,
- 'passwordChange': 'on',
- 'passwordStorageScheme': 'clear'})))
- except ldap.LDAPError as e:
- log.error('Failed to add passwordpolicy: error ' + e.message['desc'])
- assert False
-
- log.info(" Add the COS template")
- try:
- topology_st.standalone.add_s(
- Entry((SUBTREE_COS_TMPL, {'objectclass': 'top ldapsubentry costemplate extensibleObject'.split(),
- 'cn': SUBTREE_PWPDN,
- 'cosPriority': '1',
- 'cn': SUBTREE_COS_TMPLDN,
- 'pwdpolicysubentry': SUBTREE_PWP})))
- except ldap.ALREADY_EXISTS:
- pass
- except ldap.LDAPError as e:
- log.error('Failed to add COS template: error ' + e.message['desc'])
- # assert False
-
- log.info(" Add the COS definition")
- try:
- topology_st.standalone.add_s(
- Entry((SUBTREE_COS_DEF, {'objectclass': 'top ldapsubentry cosSuperDefinition cosPointerDefinition'.split(),
- 'cn': SUBTREE_PWPDN,
- 'costemplatedn': SUBTREE_COS_TMPL,
- 'cosAttribute': 'pwdpolicysubentry default operational-default'})))
- except ldap.ALREADY_EXISTS:
- pass
- except ldap.LDAPError as e:
- log.error('Failed to add COS def: error ' + e.message['desc'])
- # assert False
-
- time.sleep(1)
-
-
-def update_passwd(topology_st, user, passwd, newpasswd):
- log.info(" Bind as {%s,%s}" % (user, passwd))
- topology_st.standalone.simple_bind_s(user, passwd)
- try:
- topology_st.standalone.modify_s(user, [(ldap.MOD_REPLACE, 'userpassword', newpasswd.encode())])
- except ldap.LDAPError as e:
- log.fatal('test_ticket548: Failed to update the password ' + cpw + ' of user ' + user + ': error ' + e.message[
- 'desc'])
- assert False
-
- time.sleep(1)
-
-
-def check_shadow_attr_value(entry, attr_type, expected, dn):
- if entry.hasAttr(attr_type):
- actual = entry.getValue(attr_type)
- if int(actual) == expected:
- log.info('%s of entry %s has expected value %s' % (attr_type, dn, actual))
- assert True
- else:
- log.fatal('%s %s of entry %s does not have expected value %s' % (attr_type, actual, dn, expected))
- assert False
- else:
- log.fatal('entry %s does not have %s attr' % (dn, attr_type))
- assert False
-
-
-def test_ticket548_test_with_no_policy(topology_st):
- """
- Check shadowAccount under no password policy
- """
- log.info("Case 1. No password policy")
-
- log.info("Bind as %s" % DN_DM)
- topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
-
- log.info('Add an entry' + USER1_DN)
- try:
- topology_st.standalone.add_s(
- Entry((USER1_DN, {'objectclass': "top person organizationalPerson inetOrgPerson shadowAccount".split(),
- 'sn': '1',
- 'cn': 'user 1',
- 'uid': 'user1',
- 'givenname': 'user',
- 'mail': 'user1@' + DEFAULT_SUFFIX,
- 'userpassword': USER_PW})))
- except ldap.LDAPError as e:
- log.fatal('test_ticket548: Failed to add user' + USER1_DN + ': error ' + e.message['desc'])
- assert False
-
- edate = int(time.time() / (60 * 60 * 24))
- log.info('Search entry %s' % USER1_DN)
-
- log.info("Bind as %s" % USER1_DN)
- topology_st.standalone.simple_bind_s(USER1_DN, USER_PW)
- entry = topology_st.standalone.getEntry(USER1_DN, ldap.SCOPE_BASE, "(objectclass=*)", ['shadowLastChange'])
- check_shadow_attr_value(entry, 'shadowLastChange', edate, USER1_DN)
-
- log.info("Check shadowAccount with no policy was successfully verified.")
-
-
-def test_ticket548_test_global_policy(topology_st):
- """
- Check shadowAccount with global password policy
- """
-
- log.info("Case 2. Check shadowAccount with global password policy")
-
- log.info("Bind as %s" % DN_DM)
- topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
-
- set_global_pwpolicy(topology_st)
-
- log.info('Add an entry' + USER2_DN)
- try:
- topology_st.standalone.add_s(
- Entry((USER2_DN, {'objectclass': "top person organizationalPerson inetOrgPerson shadowAccount".split(),
- 'sn': '2',
- 'cn': 'user 2',
- 'uid': 'user2',
- 'givenname': 'user',
- 'mail': 'user2@' + DEFAULT_SUFFIX,
- 'userpassword': USER_PW})))
- except ldap.LDAPError as e:
- log.fatal('test_ticket548: Failed to add user' + USER2_DN + ': error ' + e.message['desc'])
- assert False
-
- edate = int(time.time() / (60 * 60 * 24))
-
- log.info("Bind as %s" % USER1_DN)
- topology_st.standalone.simple_bind_s(USER1_DN, USER_PW)
-
- log.info('Search entry %s' % USER1_DN)
- entry = topology_st.standalone.getEntry(USER1_DN, ldap.SCOPE_BASE, "(objectclass=*)")
- check_shadow_attr_value(entry, 'shadowLastChange', edate, USER1_DN)
-
- # passwordMinAge -- 1 day
- check_shadow_attr_value(entry, 'shadowMin', 1, USER1_DN)
-
- # passwordMaxAge -- 10 days
- check_shadow_attr_value(entry, 'shadowMax', 10, USER1_DN)
-
- # passwordWarning -- 3 days
- check_shadow_attr_value(entry, 'shadowWarning', 3, USER1_DN)
-
- log.info("Bind as %s" % USER2_DN)
- topology_st.standalone.simple_bind_s(USER2_DN, USER_PW)
-
- log.info('Search entry %s' % USER2_DN)
- entry = topology_st.standalone.getEntry(USER2_DN, ldap.SCOPE_BASE, "(objectclass=*)")
- check_shadow_attr_value(entry, 'shadowLastChange', edate, USER2_DN)
-
- # passwordMinAge -- 1 day
- check_shadow_attr_value(entry, 'shadowMin', 1, USER2_DN)
-
- # passwordMaxAge -- 10 days
- check_shadow_attr_value(entry, 'shadowMax', 10, USER2_DN)
-
- # passwordWarning -- 3 days
- check_shadow_attr_value(entry, 'shadowWarning', 3, USER2_DN)
-
- # Bind as DM again, change policy
- log.info("Bind as %s" % DN_DM)
- topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
- set_global_pwpolicy(topology_st, 3, 30, 9)
-
- # change the user password, then check again.
- log.info("Bind as %s" % USER2_DN)
- topology_st.standalone.simple_bind_s(USER2_DN, USER_PW)
-
- newpasswd = USER_PW + '2'
- update_passwd(topology_st, USER2_DN, USER_PW, newpasswd)
-
- log.info("Re-bind as %s with new password" % USER2_DN)
- topology_st.standalone.simple_bind_s(USER2_DN, newpasswd)
-
- ## This tests if we update the shadow values on password change.
- log.info('Search entry %s' % USER2_DN)
- entry = topology_st.standalone.getEntry(USER2_DN, ldap.SCOPE_BASE, "(objectclass=*)")
-
- # passwordMinAge -- 1 day
- check_shadow_attr_value(entry, 'shadowMin', 3, USER2_DN)
-
- # passwordMaxAge -- 10 days
- check_shadow_attr_value(entry, 'shadowMax', 30, USER2_DN)
-
- # passwordWarning -- 3 days
- check_shadow_attr_value(entry, 'shadowWarning', 9, USER2_DN)
-
- log.info("Check shadowAccount with global policy was successfully verified.")
-
-
-def test_ticket548_test_subtree_policy(topology_st):
- """
- Check shadowAccount with subtree level password policy
- """
-
- log.info("Case 3. Check shadowAccount with subtree level password policy")
-
- log.info("Bind as %s" % DN_DM)
- topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
- # Check the global policy values
-
- set_subtree_pwpolicy(topology_st, 2, 20, 6)
-
- log.info('Add an entry' + USER3_DN)
- try:
- topology_st.standalone.add_s(
- Entry((USER3_DN, {'objectclass': "top person organizationalPerson inetOrgPerson shadowAccount".split(),
- 'sn': '3',
- 'cn': 'user 3',
- 'uid': 'user3',
- 'givenname': 'user',
- 'mail': 'user3@' + DEFAULT_SUFFIX,
- 'userpassword': USER_PW})))
- except ldap.LDAPError as e:
- log.fatal('test_ticket548: Failed to add user' + USER3_DN + ': error ' + e.message['desc'])
- assert False
-
- log.info('Search entry %s' % USER3_DN)
- entry0 = topology_st.standalone.getEntry(USER3_DN, ldap.SCOPE_BASE, "(objectclass=*)")
-
- log.info('Expecting shadowLastChange 0 since passwordMustChange is on')
- check_shadow_attr_value(entry0, 'shadowLastChange', 0, USER3_DN)
-
- # passwordMinAge -- 2 day
- check_shadow_attr_value(entry0, 'shadowMin', 2, USER3_DN)
-
- # passwordMaxAge -- 20 days
- check_shadow_attr_value(entry0, 'shadowMax', 20, USER3_DN)
-
- # passwordWarning -- 6 days
- check_shadow_attr_value(entry0, 'shadowWarning', 6, USER3_DN)
-
- log.info("Bind as %s" % USER3_DN)
- topology_st.standalone.simple_bind_s(USER3_DN, USER_PW)
-
- log.info('Search entry %s' % USER3_DN)
- try:
- entry1 = topology_st.standalone.getEntry(USER3_DN, ldap.SCOPE_BASE, "(objectclass=*)")
- except ldap.UNWILLING_TO_PERFORM:
- log.info('test_ticket548: Search by' + USER3_DN + ' failed by UNWILLING_TO_PERFORM as expected')
- except ldap.LDAPError as e:
- log.fatal('test_ticket548: Failed to serch user' + USER3_DN + ' by self: error ' + e.message['desc'])
- assert False
-
- log.info("Bind as %s and updating the password with a new one" % USER3_DN)
- topology_st.standalone.simple_bind_s(USER3_DN, USER_PW)
-
- # Bind as DM again, change policy
- log.info("Bind as %s" % DN_DM)
- topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
-
- set_subtree_pwpolicy(topology_st, 4, 40, 12)
-
- newpasswd = USER_PW + '0'
- update_passwd(topology_st, USER3_DN, USER_PW, newpasswd)
-
- log.info("Re-bind as %s with new password" % USER3_DN)
- topology_st.standalone.simple_bind_s(USER3_DN, newpasswd)
-
- try:
- entry2 = topology_st.standalone.getEntry(USER3_DN, ldap.SCOPE_BASE, "(objectclass=*)")
- except ldap.LDAPError as e:
- log.fatal('test_ticket548: Failed to serch user' + USER3_DN + ' by self: error ' + e.message['desc'])
- assert False
-
- edate = int(time.time() / (60 * 60 * 24))
-
- log.info('Expecting shadowLastChange %d once userPassword is updated', edate)
- check_shadow_attr_value(entry2, 'shadowLastChange', edate, USER3_DN)
-
- log.info('Search entry %s' % USER3_DN)
- entry = topology_st.standalone.getEntry(USER3_DN, ldap.SCOPE_BASE, "(objectclass=*)")
- check_shadow_attr_value(entry, 'shadowLastChange', edate, USER3_DN)
-
- # passwordMinAge -- 1 day
- check_shadow_attr_value(entry, 'shadowMin', 4, USER3_DN)
-
- # passwordMaxAge -- 10 days
- check_shadow_attr_value(entry, 'shadowMax', 40, USER3_DN)
-
- # passwordWarning -- 3 days
- check_shadow_attr_value(entry, 'shadowWarning', 12, USER3_DN)
-
- log.info("Check shadowAccount with subtree level policy was successfully verified.")
-
-
-if __name__ == '__main__':
- # Run isolated
- # -s for DEBUG mode
- CURRENT_FILE = os.path.realpath(__file__)
- pytest.main("-s %s" % CURRENT_FILE)
--
2.52.0

View File

@ -0,0 +1,82 @@
From 7c8a16c6bed524fb54d18a5b7e93d4bd5bb19d49 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Wed, 14 Jan 2026 17:55:29 +0100
Subject: [PATCH] Issue 7152 - ns-slapd fails to shutdown when deferred
memberof update is in progress (#7187)
Bug Description:
When a deferred memberof update is in progress during shutdown, the
backend operations (add, modify, delete, modrdn) wait in a polling loop
for the deferred task to complete. However, if the deferred thread exits
before clearing the SLAPI_DEFERRED_MEMBEROF flag, the loop becomes
infinite, causing the server to hang during shutdown.
Fix Description:
Add additional check to the polling loops so they exit immediately when
the server is shutting down.
Fixes: https://github.com/389ds/389-ds-base/issues/7152
Reviewed by: @tbordaz (Thanks!)
---
ldap/servers/slapd/back-ldbm/ldbm_add.c | 2 +-
ldap/servers/slapd/back-ldbm/ldbm_delete.c | 2 +-
ldap/servers/slapd/back-ldbm/ldbm_modify.c | 2 +-
ldap/servers/slapd/back-ldbm/ldbm_modrdn.c | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_add.c b/ldap/servers/slapd/back-ldbm/ldbm_add.c
index db6024636..90d5abc3d 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_add.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_add.c
@@ -1452,7 +1452,7 @@ common_return:
slapi_pblock_get(pb, SLAPI_DEFERRED_MEMBEROF, &deferred);
if (deferred) {
PRIntervalTime delay = PR_MillisecondsToInterval(100);
- while (deferred) {
+ while (deferred && !g_get_shutdown()) {
DS_Sleep(delay);
slapi_pblock_get(pb, SLAPI_DEFERRED_MEMBEROF, &deferred);
}
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_delete.c b/ldap/servers/slapd/back-ldbm/ldbm_delete.c
index 498342f2d..cbfb5bca9 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_delete.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_delete.c
@@ -1536,7 +1536,7 @@ diskfull_return:
slapi_pblock_get(pb, SLAPI_DEFERRED_MEMBEROF, &deferred);
if (deferred) {
PRIntervalTime delay = PR_MillisecondsToInterval(100);
- while (deferred) {
+ while (deferred && !g_get_shutdown()) {
DS_Sleep(delay);
slapi_pblock_get(pb, SLAPI_DEFERRED_MEMBEROF, &deferred);
}
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_modify.c b/ldap/servers/slapd/back-ldbm/ldbm_modify.c
index ea49a4c56..c57ba43ae 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_modify.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_modify.c
@@ -1179,7 +1179,7 @@ common_return:
slapi_pblock_get(pb, SLAPI_DEFERRED_MEMBEROF, &deferred);
if (deferred) {
PRIntervalTime delay = PR_MillisecondsToInterval(100);
- while (deferred) {
+ while (deferred && !g_get_shutdown()) {
DS_Sleep(delay);
slapi_pblock_get(pb, SLAPI_DEFERRED_MEMBEROF, &deferred);
}
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c b/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c
index 018ad9e49..759edb80d 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c
@@ -1469,7 +1469,7 @@ common_return:
slapi_pblock_get(pb, SLAPI_DEFERRED_MEMBEROF, &deferred);
if (deferred) {
PRIntervalTime delay = PR_MillisecondsToInterval(100);
- while (deferred) {
+ while (deferred && !g_get_shutdown()) {
DS_Sleep(delay);
slapi_pblock_get(pb, SLAPI_DEFERRED_MEMBEROF, &deferred);
}
--
2.52.0

View File

@ -0,0 +1,61 @@
From a84a6a7d8323316bfa4055729a3604a0fcfebaf9 Mon Sep 17 00:00:00 2001
From: Akshay Adhikari <aadhikar@redhat.com>
Date: Fri, 16 Jan 2026 19:35:42 +0530
Subject: [PATCH] Issue 7169 - Fix automember_plugin CI test failures (#7181)
Description: Issue 7053 removed member cleanup from MemberOf plugin,
transferring it to Referential Integrity plugin. Enable this plugin
in automember tests and clean up groups before rebuild task tests.
Fixes: #7169
Reviewed by: @progier389
---
.../tests/suites/automember_plugin/basic_test.py | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/dirsrvtests/tests/suites/automember_plugin/basic_test.py b/dirsrvtests/tests/suites/automember_plugin/basic_test.py
index 6f2cf3326..f3629c811 100644
--- a/dirsrvtests/tests/suites/automember_plugin/basic_test.py
+++ b/dirsrvtests/tests/suites/automember_plugin/basic_test.py
@@ -19,7 +19,8 @@ from lib389.idm.organizationalunit import OrganizationalUnits
from lib389.idm.domain import Domain
from lib389.idm.posixgroup import PosixGroups
from lib389.plugins import AutoMembershipPlugin, AutoMembershipDefinitions, \
- MemberOfPlugin, AutoMembershipRegexRules, AutoMembershipDefinition, RetroChangelogPlugin
+ MemberOfPlugin, AutoMembershipRegexRules, AutoMembershipDefinition, RetroChangelogPlugin, \
+ ReferentialIntegrityPlugin
from lib389.backend import Backends
from lib389.config import Config
from lib389._constants import DEFAULT_SUFFIX
@@ -196,6 +197,7 @@ def _create_all_entries(topo):
auto = AutoMembershipPlugin(topo.ms["supplier1"])
auto.add("nsslapd-pluginConfigArea", "cn=autoMembersPlugin,{}".format(BASE_REPL))
MemberOfPlugin(topo.ms["supplier1"]).enable()
+ ReferentialIntegrityPlugin(topo.ms["supplier1"]).enable()
automembers_definitions = AutoMembershipDefinitions(topo.ms["supplier1"])
automembers_definitions.create(properties={
'cn': 'userGroups',
@@ -950,8 +952,18 @@ def _startuptask(topo):
@pytest.fixture(scope="function")
def _fixture_for_build_task(request, topo):
+ supplier = topo.ms['supplier1']
+ managers_grp = "cn=Managers,ou=userGroups,{}".format(BASE_SUFF)
+ contract_grp = "cn=Contractors,ou=userGroups,{}".format(BASE_SUFF)
+
+ for grp in (managers_grp, contract_grp):
+ group = Group(supplier, grp)
+ try:
+ group.remove_all('member')
+ except ldap.NO_SUCH_ATTRIBUTE:
+ pass
+
def finof():
- supplier = topo.ms['supplier1']
auto_mem_scope = "ou=TaskEmployees,{}".format(BASE_SUFF)
for user in nsAdminGroups(supplier, auto_mem_scope, rdn=None).list():
user.delete()
--
2.52.0

View File

@ -0,0 +1,115 @@
From 5b32479abcd68f8b37d2fb207c502113a5b4b16c Mon Sep 17 00:00:00 2001
From: Akshay Adhikari <aadhikar@redhat.com>
Date: Mon, 19 Jan 2026 19:45:29 +0530
Subject: [PATCH] Issue 6758 - Use OUIA selectors for WebUI plugin tests
(#7182)
Description:
Add ouiaId to plugin NavItems and update tests to use OUIA selectors
instead of text matching.
Relates: #6758
Reviewed by: @droideck
---
.../suites/webui/plugins/plugins_test.py | 28 +++++++++----------
src/cockpit/389-console/src/plugins.jsx | 2 +-
2 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/dirsrvtests/tests/suites/webui/plugins/plugins_test.py b/dirsrvtests/tests/suites/webui/plugins/plugins_test.py
index e4bd7f039..a849bfb91 100644
--- a/dirsrvtests/tests/suites/webui/plugins/plugins_test.py
+++ b/dirsrvtests/tests/suites/webui/plugins/plugins_test.py
@@ -168,8 +168,8 @@ def test_linked_attributes_plugin_visibility(topology_st, page, browser_name):
log.info('Click on Plugins tab, click on Linked Attributes plugin and check if element is loaded.')
frame.get_by_role('tab', name='Plugins', exact=True).click()
- frame.get_by_text('Linked Attributes').wait_for()
- frame.get_by_text('Linked Attributes').click()
+ frame.locator('[data-ouia-component-id="linkedAttributes"]').wait_for()
+ frame.locator('[data-ouia-component-id="linkedAttributes"]').click()
frame.get_by_role('button', name='Add Config').wait_for()
assert frame.get_by_role('button', name='Add Config').is_visible()
@@ -254,8 +254,8 @@ def test_ldap_pass_through_auth_plugin_visibility(topology_st, page, browser_nam
log.info('Click on Plugins tab, click on LDAP Pass Through Auth plugin and check if element is loaded.')
frame.get_by_role('tab', name='Plugins', exact=True).click()
- frame.get_by_text('LDAP Pass Through Auth').wait_for()
- frame.get_by_text('LDAP Pass Through Auth').click()
+ frame.locator('[data-ouia-component-id="passthroughAuthentication"]').wait_for()
+ frame.locator('[data-ouia-component-id="passthroughAuthentication"]').click()
frame.get_by_role('button', name='Add URL').wait_for()
assert frame.get_by_role('button', name='Add URL').is_visible()
@@ -280,8 +280,8 @@ def test_pam_pass_through_auth_plugin_visibility(topology_st, page, browser_name
log.info('Click on Plugins tab, click on PAM Pass Through Auth plugin and check if element is loaded.')
frame.get_by_role('tab', name='Plugins', exact=True).click()
- frame.get_by_text('PAM Pass Through Auth').wait_for()
- frame.get_by_text('PAM Pass Through Auth').click()
+ frame.locator('[data-ouia-component-id="pamPassthroughAuthentication"]').wait_for()
+ frame.locator('[data-ouia-component-id="pamPassthroughAuthentication"]').click()
frame.get_by_role('button', name='Add Config').wait_for()
assert frame.get_by_role('button', name='Add Config').is_visible()
@@ -306,8 +306,8 @@ def test_posix_winsync_plugin_visibility(topology_st, page, browser_name):
log.info('Click on Plugins tab, click on Posix Winsync plugin and check if element is loaded.')
frame.get_by_role('tab', name='Plugins', exact=True).click()
- frame.get_by_text('Posix Winsync').wait_for()
- frame.get_by_text('Posix Winsync').click()
+ frame.locator('[data-ouia-component-id="winsync"]').wait_for()
+ frame.locator('[data-ouia-component-id="winsync"]').click()
frame.locator('#posixWinsyncCreateMemberOfTask').wait_for()
assert frame.locator('#posixWinsyncCreateMemberOfTask').is_visible()
@@ -332,8 +332,8 @@ def test_referential_integrity_plugin_visibility(topology_st, page, browser_name
log.info('Click on Plugins tab, click on Referential Integrity plugin and check if element is loaded.')
frame.get_by_role('tab', name='Plugins', exact=True).click()
- frame.get_by_text('Referential Integrity').wait_for()
- frame.get_by_text('Referential Integrity').click()
+ frame.locator('[data-ouia-component-id="referentialIntegrity"]').wait_for()
+ frame.locator('[data-ouia-component-id="referentialIntegrity"]').click()
frame.locator('#entryScope').wait_for()
assert frame.locator('#entryScope').is_visible()
@@ -358,8 +358,8 @@ def test_retro_changelog_plugin_visibility(topology_st, page, browser_name):
log.info('Click on Plugins tab, click on Retro Changelog plugin and check if element is loaded.')
frame.get_by_role('tab', name='Plugins', exact=True).click()
- frame.get_by_text('Retro Changelog').wait_for()
- frame.get_by_text('Retro Changelog').click()
+ frame.locator('[data-ouia-component-id="retroChangelog"]').wait_for()
+ frame.locator('[data-ouia-component-id="retroChangelog"]').click()
frame.locator('#isReplicated').wait_for()
assert frame.locator('#isReplicated').is_visible()
@@ -384,8 +384,8 @@ def test_rootdn_access_control_plugin_visibility(topology_st, page, browser_name
log.info('Click on Plugins tab, click on RootDN Access Control plugin and check if element is loaded.')
frame.get_by_role('tab', name='Plugins', exact=True).click()
- frame.get_by_text('RootDN Access Control').wait_for()
- frame.get_by_text('RootDN Access Control').click()
+ frame.locator('[data-ouia-component-id="rootDnaAccessControl"]').wait_for()
+ frame.locator('[data-ouia-component-id="rootDnaAccessControl"]').click()
frame.locator('#allowMon').wait_for()
assert frame.locator('#allowMon').is_visible()
diff --git a/src/cockpit/389-console/src/plugins.jsx b/src/cockpit/389-console/src/plugins.jsx
index 4124a77ef..e24255c76 100644
--- a/src/cockpit/389-console/src/plugins.jsx
+++ b/src/cockpit/389-console/src/plugins.jsx
@@ -680,7 +680,7 @@ export class Plugins extends React.Component {
<Nav key={this.state.pluginTableKey} theme="light" onSelect={(event, item) => this.handleSelect(event, item)}>
<NavList>
{Object.entries(selectPlugins).map(([id, item]) => (
- <NavItem key={item.name} itemId={item.name} isActive={this.state.activePlugin === item.name}>
+ <NavItem key={item.name} itemId={item.name} ouiaId={id} isActive={this.state.activePlugin === item.name}>
{item.icon}
</NavItem>
))}
--
2.52.0

View File

@ -0,0 +1,32 @@
From 9bd93dc618c261d52222e713c56500abbce4113e Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Mon, 19 Jan 2026 17:24:40 +0100
Subject: [PATCH] Issue 7196 - DynamicCertificates returns empty DER (#7197)
Fixing a mistake done while fixing memory leaks.
Value was freed before being added in the entry rather than after ...
Issue: #7196
Reviewed by: @jchapma (thanks!)
---
ldap/servers/slapd/dyncerts.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ldap/servers/slapd/dyncerts.c b/ldap/servers/slapd/dyncerts.c
index 50b92aa5f..efeaa6eb6 100644
--- a/ldap/servers/slapd/dyncerts.c
+++ b/ldap/servers/slapd/dyncerts.c
@@ -786,8 +786,8 @@ dyncerts_cert2entry(CERTCertificate *cert)
COND_STR(e, DYCATTR_TYPE, "OBJECT SIGNING CA", cert->nsCertType & NS_CERT_TYPE_OBJECT_SIGNING_CA);
slapi_entry_add_string(e, DYCATTR_TOKEN, PK11_GetTokenName(cert->slot));
secitemv(&cert->derCert, &tmpv);
- value_done(&tmpv);
slapi_entry_add_value(e, DYCATTR_CERTDER, &tmpv);
+ value_done(&tmpv);
tmpstr = secitem2hex(&cert->serialNumber);
slapi_entry_add_string(e, DYCATTR_SN, tmpstr);
slapi_ch_free_string(&tmpstr);
--
2.52.0

View File

@ -0,0 +1,235 @@
From c6f458b421598b18a545582441472b910b5ba56e Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Tue, 20 Jan 2026 09:52:47 +0100
Subject: [PATCH] Issue 7189 - DSBLE0007 generates incorrect remediation
commands for scan limits
Bug Description:
The generated dsconf commands for fixing missing system indexes had two issues:
1. The --add-scanlimit value was not quoted, causing the shell to interpret
"limit=5000 type=eq flags=AND" as multiple arguments instead of a single
value, resulting in "unrecognized arguments: type=eq flags=AND" error.
2. When both matching rule and scanlimit were missing, two separate commands
were generated where the second would fail because the matching rule was
already added by the first command.
Fix Description:
1. Quote the scanlimit value in all remediation commands
2. Combine matching rule and scanlimit fixes into a single command when
both are missing for the same index instead of expected_scanlimit)
Fixes: https://github.com/389ds/389-ds-base/issues/7189
Reviewed by: @progier389, @droideck (Thanks!)
---
.../healthcheck/health_system_indexes_test.py | 126 ++++++++++++++++++
src/lib389/lib389/backend.py | 39 +++---
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 a977b71d1..486fad44b 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -408,6 +408,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)
+def test_missing_scanlimit(topology_st, log_buffering_enabled):
+ """Check if healthcheck returns DSBLE0007 code when parentId index is missing scanlimit
+
+ :id: 40e1bf6a-2397-459b-bdf3-f787ca118b86
+ :setup: Standalone instance
+ :steps:
+ 1. Create DS instance
+ 2. Remove nsIndexIDListScanLimit from parentId index
+ 3. Use healthcheck without --json option
+ 4. Use healthcheck with --json option
+ 5. Verify the remediation command has properly quoted scanlimit
+ 6. Re-add the scanlimit
+ 7. Use healthcheck without --json option
+ 8. Use healthcheck with --json option
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. healthcheck reports DSBLE0007 code and related details
+ 4. healthcheck reports DSBLE0007 code and related details
+ 5. The scanlimit value is quoted in the remediation command
+ 6. Success
+ 7. healthcheck reports no issues found
+ 8. healthcheck reports no issues found
+ """
+
+ RET_CODE = "DSBLE0007"
+ PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
+ SCANLIMIT_VALUE = "limit=5000 type=eq flags=AND"
+
+ standalone = topology_st.standalone
+
+ log.info("Remove nsIndexIDListScanLimit from parentId index")
+ parentid_index = Index(standalone, PARENTID_DN)
+ parentid_index.remove("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
+
+ run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=RET_CODE)
+
+ # Verify the remediation command has properly quoted scanlimit
+ args = FakeArgs()
+ args.instance = standalone.serverid
+ args.verbose = standalone.verbose
+ args.list_errors = False
+ args.list_checks = False
+ args.exclude_check = []
+ args.check = ["backends"]
+ args.dry_run = False
+ args.json = False
+ health_check_run(standalone, topology_st.logcap.log, args)
+ # Check that the scanlimit is quoted in the output
+ assert topology_st.logcap.contains('--add-scanlimit "limit=5000 type=eq flags=AND"')
+ log.info("Verified scanlimit is properly quoted in remediation command")
+ topology_st.logcap.flush()
+
+ run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=RET_CODE)
+
+ log.info("Re-add the nsIndexIDListScanLimit")
+ parentid_index = Index(standalone, PARENTID_DN)
+ parentid_index.add("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
+
+ 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)
+
+
+def test_missing_matching_rule_and_scanlimit(topology_st, log_buffering_enabled):
+ """Check if healthcheck generates a single combined command when both matching rule and scanlimit are missing
+
+ :id: af8214ad-5e4c-422a-8f74-3e99227551df
+ :setup: Standalone instance
+ :steps:
+ 1. Create DS instance
+ 2. Remove both integerOrderingMatch and nsIndexIDListScanLimit from parentId index
+ 3. Use healthcheck and verify a single combined command is generated
+ 4. Re-add the matching rule and scanlimit
+ 5. Use healthcheck without --json option
+ 6. Use healthcheck with --json option
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. healthcheck reports DSBLE0007 and generates a single command with both --add-mr and --add-scanlimit
+ 4. Success
+ 5. healthcheck reports no issues found
+ 6. healthcheck reports no issues found
+ """
+
+ RET_CODE = "DSBLE0007"
+ PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
+ SCANLIMIT_VALUE = "limit=5000 type=eq flags=AND"
+
+ standalone = topology_st.standalone
+
+ log.info("Remove both integerOrderingMatch and nsIndexIDListScanLimit from parentId index")
+ parentid_index = Index(standalone, PARENTID_DN)
+ parentid_index.remove("nsMatchingRule", "integerOrderingMatch")
+ parentid_index.remove("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
+
+ # Run healthcheck and verify combined command
+ args = FakeArgs()
+ args.instance = standalone.serverid
+ args.verbose = standalone.verbose
+ args.list_errors = False
+ args.list_checks = False
+ args.exclude_check = []
+ args.check = ["backends"]
+ args.dry_run = False
+ args.json = False
+ health_check_run(standalone, topology_st.logcap.log, args)
+
+ # Verify DSBLE0007 is reported
+ assert topology_st.logcap.contains(RET_CODE)
+ log.info("healthcheck returned code: %s" % RET_CODE)
+
+ # Verify a single combined command is generated with both --add-mr and --add-scanlimit
+ assert topology_st.logcap.contains('--add-mr integerOrderingMatch --add-scanlimit "limit=5000 type=eq flags=AND"')
+ log.info("Verified combined command with both --add-mr and --add-scanlimit")
+
+ topology_st.logcap.flush()
+
+ log.info("Re-add the integerOrderingMatch matching rule and scanlimit")
+ parentid_index = Index(standalone, PARENTID_DN)
+ parentid_index.add("nsMatchingRule", "integerOrderingMatch")
+ parentid_index.add("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
+
+ 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)
+
+
def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
"""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 fba95987b..db464b43a 100644
--- a/src/lib389/lib389/backend.py
+++ b/src/lib389/lib389/backend.py
@@ -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'):
- cmd += f" --add-scanlimit {expected_config['scanlimit']}"
+ cmd += f" --add-scanlimit \"{expected_config['scanlimit']}\""
remediation_commands.append(cmd)
reindex_attrs.add(attr_name) # New index needs reindexing
else:
@@ -700,28 +700,31 @@ class Backend(DSLdapObject):
remediation_commands.append(cmd)
reindex_attrs.add(attr_name)
- # Check matching rules
+ # Check matching rules and scanlimit together to generate a single combined command
expected_mr = expected_config.get('matching_rule')
+ expected_scanlimit = expected_config.get('scanlimit')
+
+ missing_mr = False
if expected_mr:
actual_mrs_lower = [mr.lower() for mr in actual_mrs]
if expected_mr.lower() not in actual_mrs_lower:
discrepancies.append(f"Index {attr_name} missing matching rule: {expected_mr}")
- # Add the missing matching rule
- cmd = f"dsconf YOUR_INSTANCE backend index set {bename} --attr {attr_name} --add-mr {expected_mr}"
- remediation_commands.append(cmd)
- reindex_attrs.add(attr_name)
-
- # Check fine grain definitions for parentid ONLY
- expected_scanlimit = expected_config.get('scanlimit')
- if (attr_name.lower() == "parentid") and expected_scanlimit and (len(actual_scanlimit) == 0):
- discrepancies.append(f"Index {attr_name} missing fine grain definition of IDs limit: {expected_mr}")
- # Add the missing scanlimit
- if expected_mr:
- cmd = f"dsconf YOUR_INSTANCE backend index set {bename} --attr {attr_name} --add-mr {expected_mr} --add-scanlimit {expected_scanlimit}"
- else:
- cmd = f"dsconf YOUR_INSTANCE backend index set {bename} --attr {attr_name} --add-scanlimit {expected_scanlimit}"
- remediation_commands.append(cmd)
- reindex_attrs.add(attr_name)
+ missing_mr = True
+
+ missing_scanlimit = False
+ if expected_scanlimit and (len(actual_scanlimit) == 0):
+ discrepancies.append(f"Index {attr_name} missing fine grain definition of IDs limit: {expected_scanlimit}")
+ missing_scanlimit = True
+
+ # Generate a single combined command for all missing items
+ if missing_mr or missing_scanlimit:
+ cmd = f"dsconf YOUR_INSTANCE backend index set {bename} --attr {attr_name}"
+ if missing_mr:
+ cmd += f" --add-mr {expected_mr}"
+ if missing_scanlimit:
+ cmd += f" --add-scanlimit \"{expected_scanlimit}\""
+ remediation_commands.append(cmd)
+ reindex_attrs.add(attr_name)
except Exception as e:
self._log.debug(f"_lint_system_indexes - Error checking index {attr_name}: {e}")
--
2.52.0

View File

@ -0,0 +1,504 @@
From 6ce33b1bedd2cd13d7e6544692354715f6e613b8 Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Tue, 20 Jan 2026 19:41:05 +0100
Subject: [PATCH] Issue 7170 - Support of PQC keys (#7188)
Support of Post Quantum Cryptography Keys in certificates:
Added support of a new key type: ML_DSA
Enable the following policies (that are not enabled by defaut): ML-DSA-44, ML-DSA-65- ML-DSA-87
Replaced deprecated function SSL_ConfigSecureServer by SSL_ConfigServerCert
Added test case for ML-DSA certificate. That test case rely of openssl command because python cryptography module does not yet support ML-DSA keys
Issue: #7170
Reviewed by: @tbordaz, @droideck and @vashirov (Thanks!)
---
dirsrvtests/tests/suites/tls/mldsa_test.py | 322 +++++++++++++++++++++
ldap/servers/slapd/ssl.c | 95 +++++-
2 files changed, 407 insertions(+), 10 deletions(-)
create mode 100644 dirsrvtests/tests/suites/tls/mldsa_test.py
diff --git a/dirsrvtests/tests/suites/tls/mldsa_test.py b/dirsrvtests/tests/suites/tls/mldsa_test.py
new file mode 100644
index 000000000..2c815088b
--- /dev/null
+++ b/dirsrvtests/tests/suites/tls/mldsa_test.py
@@ -0,0 +1,322 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2026 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 sys
+import itertools
+import rpm
+import socket
+import subprocess
+from lib389.utils import ds_is_older
+from lib389._constants import DN_DM, PW_DM, DEFAULT_SUFFIX
+from lib389.config import Encryption, CertmapLegacy
+from lib389.idm.user import UserAccount
+from lib389.topologies import topology_st as topo
+from tempfile import TemporaryDirectory
+
+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__)
+
+
+def rpm_is_older(pkg, version):
+ ts = rpm.TransactionSet()
+ mi = ts.dbMatch('name', pkg)
+ for h in mi:
+ print(f"{pkg} {h['version']} {version}")
+ for n1,n2 in itertools.zip_longest(h['version'].split('.'), version.split('.'), fillvalue=""):
+ try:
+ if int(n1) < int(n2):
+ return True
+ except ValueError:
+ if n1 < n2:
+ return True
+ return False
+
+
+script_content="""
+#!/bin/bash
+set -e # Exit if a command fails
+set -x # Log the commands
+
+cd {dir}
+inst={instname}
+url={url}
+rootdn="{rootdn}"
+rootpw="{rootpw}"
+
+################################
+###### GENERATE CA CERT ########
+################################
+
+echo "
+[ req ]
+distinguished_name = req_distinguished_name
+policy = policy_match
+x509_extensions = v3_ca
+
+# For the CA policy
+[ policy_match ]
+countryName = optional
+stateOrProvinceName = optional
+organizationName = optional
+organizationalUnitName = optional
+commonName = supplied
+emailAddress = optional
+
+[ req_distinguished_name ]
+countryName = Country Name (2 letter code)
+countryName_default = FR
+countryName_min = 2
+countryName_max = 2
+
+stateOrProvinceName = State or Province Name (full name)
+stateOrProvinceName_default = test
+
+localityName = Locality Name (eg, city)
+
+0.organizationName = Organization Name (eg, company)
+0.organizationName_default = test-ML-DSA-CA
+
+organizationalUnitName = Organizational Unit Name (eg, section)
+#organizationalUnitName_default =
+
+commonName = Common Name (e.g. server FQDN or YOUR name)
+commonName_max = 64
+
+
+[ v3_ca ]
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+basicConstraints = critical,CA:true
+#nsComment = "OpenSSL Generated Certificate"
+keyUsage=critical, keyCertSign
+" >ca.conf
+
+
+openssl genpkey -algorithm ML-DSA-87 -out ca.key
+openssl req -x509 -new -sha256 -key ca.key -nodes -days 3650 -config ca.conf -subj "/CN=`hostname`/O=test-ML-DSA-CA/C=FR" -out ca.pem -keyout ca.key
+openssl x509 -outform der -in ca.pem -out ca.crt
+
+openssl x509 -text -in ca.pem
+
+####################################
+###### GENERATE SERVER CERT ########
+####################################
+
+echo "
+[ req ]
+distinguished_name = req_distinguished_name
+policy = policy_match
+x509_extensions = v3_cert
+
+# For the cert policy
+[ policy_match ]
+countryName = optional
+stateOrProvinceName = optional
+organizationName = optional
+organizationalUnitName = optional
+commonName = supplied
+emailAddress = optional
+
+[ req_distinguished_name ]
+countryName = Country Name (2 letter code)
+countryName_default = FR
+countryName_min = 2
+countryName_max = 2
+
+stateOrProvinceName = State or Province Name (full name)
+
+localityName = Locality Name (eg, city)
+
+0.organizationName = Organization Name (eg, company)
+0.organizationName_default = test-ML-DSA
+
+organizationalUnitName = Organizational Unit Name (eg, section)
+#organizationalUnitName_default =
+
+commonName = Common Name (e.g. server FQDN or YOUR name)
+commonName_max = 64
+
+
+[ v3_cert ]
+basicConstraints = critical,CA:false
+subjectAltName=DNS:`hostname`
+keyUsage=digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
+#nsComment = "OpenSSL Generated Certificate"
+extendedKeyUsage=clientAuth, serverAuth
+nsCertType=client, server
+" >cert.conf
+
+openssl genpkey -algorithm ML-DSA-65 -out cert.key
+openssl req -new -sha256 -key cert.key -nodes -config cert.conf -subj "/CN=`hostname`/O=test-ML-DSA/C=FR" -out cert.csr
+openssl x509 -req -sha256 -days 3650 -extensions v3_cert -extfile cert.conf -in cert.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out cert.pem
+openssl pkcs12 -export -inkey cert.key -in cert.pem -name mldsacert -out cert.p12 -passout pass:secret12
+
+openssl x509 -text -in cert.pem
+
+
+
+####################################
+###### GENERATE CLIENT CERT ########
+####################################
+
+echo "
+[ req ]
+distinguished_name = req_distinguished_name
+policy = policy_match
+x509_extensions = v3_cert
+
+# For the cert policy
+[ policy_match ]
+countryName = optional
+stateOrProvinceName = optional
+organizationName = optional
+organizationalUnitName = optional
+commonName = supplied
+emailAddress = optional
+
+[ req_distinguished_name ]
+countryName = Country Name (2 letter code)
+countryName_default = FR
+countryName_min = 2
+countryName_max = 2
+
+stateOrProvinceName = State or Province Name (full name)
+
+localityName = Locality Name (eg, city)
+
+0.organizationName = Organization Name (eg, company)
+0.organizationName_default = test-ML-DSA
+
+organizationalUnitName = Organizational Unit Name (eg, section)
+#organizationalUnitName_default =
+
+commonName = Common Name (e.g. server FQDN or YOUR name)
+commonName_max = 64
+
+
+[ v3_cert ]
+basicConstraints = critical,CA:false
+subjectAltName=DNS:`hostname`
+keyUsage=digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
+#nsComment = "OpenSSL Generated Certificate"
+extendedKeyUsage=clientAuth
+nsCertType=client, server
+" >client.conf
+
+openssl genpkey -algorithm ML-DSA-65 -out client.key
+openssl req -new -sha256 -key client.key -nodes -config client.conf -subj "/CN=`hostname`/O=client-test-ML-DSA/C=FR" -out client.csr
+openssl x509 -req -sha256 -days 3650 -extensions v3_cert -extfile client.conf -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client.pem
+openssl pkcs12 -export -inkey client.key -in client.pem -name mldsacert2 -out client.p12 -passout pass:secret12
+
+openssl x509 -text -in client.pem
+
+
+#############################
+###### INSTALL CERTS ########
+#############################
+
+certdbdir=$PREFIX/etc/dirsrv/slapd-$inst
+rm -f $certdbdir/cert9.db $certdbdir/key4.db
+certutil -N -d $certdbdir -f $certdbdir/pwdfile.txt
+
+certutil -A -n Self-Signed-CA -t CT,, -f $certdbdir/pwdfile.txt -d $certdbdir -a -i ca.pem
+certutil -A -n Client-Cert -t u,, -f $certdbdir/pwdfile.txt -d $certdbdir -a -i client.pem
+
+dsctl $inst tls import-server-key-cert cert.pem cert.key
+
+dsctl $inst restart
+
+
+#########################
+###### TEST CERT ########
+#########################
+export LDAPTLS_CACERT=$PWD/ca.pem
+export LDAPTLS_CERT=$PWD/client.pem
+export LDAPTLS_KEY=$PWD/client.key
+
+ldapsearch -x -H $url -D "$rootdn" -w "$rootpw" -b "" -s base
+ldapsearch -Y external -H $url -b "" -s base
+"""
+
+@pytest.mark.skipif(rpm_is_older("openssl", "3.5"), reason="OpenSSL too old to support PQC")
+@pytest.mark.skipif(rpm_is_older("nss", "3.119.1"), reason="NSS too old to support PQC")
+def test_mldsa(topo):
+ """Test using ML-DSA Certificate - (PQC)
+
+ :id: 87fb19ef-672d-4fa7-934d-dfb4397f2312
+ :setup: Standalone Instance
+ :steps:
+ 1. Configure the certmap
+ 2. Create user mapped with theclient certificate
+ 3. Generate the test script
+ 4. Run the test script
+ 5. Check that ldapsearch returned the namingcontext
+ :expectedresults:
+ 1. No error
+ 2. No error
+ 3. No error
+ 4. No error and exit code should be 0
+ 5. namingcontext should be in the script output
+ """
+
+ inst = topo.standalone
+ inst.enable_tls()
+
+ cm = CertmapLegacy(inst)
+ certmaps = cm.list()
+ certmaps['default'].update({'DNComps': None, 'CmapLdapAttr': 'description'})
+ cm.set(certmaps)
+
+ cert_dn = f'C=FR,O=client-test-ML-DSA,CN={socket.gethostname()}'
+ dn = f'uid=test_user,ou=people,{DEFAULT_SUFFIX}'
+ UserAccount(inst, dn=dn).create( properties= {
+ 'uid': 'test_user',
+ 'cn': 'Test user',
+ 'sn': 'Test user',
+ 'uidNumber': '99998',
+ 'gidNumber': '99998',
+ 'homeDirectory': '/var/empty',
+ 'loginShell': '/bin/false',
+ 'description': cert_dn })
+
+ tmpdir_kwargs = {}
+ if sys.version_info >= (3, 12):
+ tmpdir_kwargs['delete'] = not DEBUGGING
+ with TemporaryDirectory(**tmpdir_kwargs) as dir:
+ scriptname = f"{dir}/doit"
+ d = {
+ 'dir': dir,
+ 'instname': inst.serverid,
+ 'url': f"ldaps://localhost:{inst.sslport}",
+ 'rootdn': DN_DM,
+ 'rootpw': PW_DM,
+ }
+ with open(scriptname, 'w') as f:
+ f.write(script_content.format(**d))
+ res = subprocess.run(('/bin/bash', scriptname), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8')
+ assert res
+ log.info(res.stdout)
+ res.check_returncode()
+ # If ldapsearch is successful then defaultnamingcontext should be in res.stdout
+ assert "defaultnamingcontext" in res.stdout
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main(["-s", CURRENT_FILE])
diff --git a/ldap/servers/slapd/ssl.c b/ldap/servers/slapd/ssl.c
index a9c17ef42..053db5424 100644
--- a/ldap/servers/slapd/ssl.c
+++ b/ldap/servers/slapd/ssl.c
@@ -169,6 +169,20 @@ PRBool enableTLS1 = PR_TRUE;
/* CA cert pem file */
static char *CACertPemFile = NULL;
+static const struct {
+ KeyType kt;
+ const char *shortname;
+ const char *fullname;
+} supported_key_types[] = {
+ { rsaKey, "RSA", "RivestShamirAdleman" },
+ { ecKey, "EC", "Elliptic Curve" },
+#ifdef MAX_ML_DSA_PRIVATE_KEY_LEN
+ { mldsaKey, "ML-DSA", "Module-Lattice-Based Digital Signature Algorithm (post-quantum)" },
+#endif
+ { 0 }
+};
+
+
/* helper functions for openldap update. */
static int slapd_extract_cert(Slapi_Entry *entry, int isCA);
static int slapd_extract_key(Slapi_Entry *entry, char *token, PK11SlotInfo *slot);
@@ -718,9 +732,31 @@ SSLPLCY_Install(void)
{
SECStatus s = 0;
+#ifdef MAX_ML_DSA_PRIVATE_KEY_LEN
+ int flags = NSS_USE_ALG_IN_SIGNATURE | NSS_USE_ALG_IN_SSL;
+ static const SECOidTag oids[] = {
+ SEC_OID_ML_DSA_44,
+ SEC_OID_ML_DSA_65,
+ SEC_OID_ML_DSA_87,
+ };
+#endif
s = NSS_SetDomesticPolicy();
+#ifdef MAX_ML_DSA_PRIVATE_KEY_LEN
+ /* Should rely on the crypto module policy in FIPS mode */
+ if (!slapd_pk11_isFIPS()) {
+ /* Set explicitly PQC algorithm policy if it is not set by default */
+ for (size_t i=0; s == SECSuccess && i < PR_ARRAY_SIZE(oids); i++) {
+ int oflags = 0;
+ (void) NSS_GetAlgorithmPolicy(oids[i], &oflags);
+ if ((oflags & flags) != flags) {
+ s = NSS_SetAlgorithmPolicy(oids[i], flags, 0);
+ }
+ }
+ }
+#endif
+
return s ? PR_FAILURE : PR_SUCCESS;
}
@@ -1640,7 +1676,7 @@ slapd_ssl_init2(PRFileDesc **fd, int startTLS)
/*
* Now, get the complete list of cipher families. Each family
* has a token name and personality name which we'll use to find
- * appropriate keys and certs, and call SSL_ConfigSecureServer
+ * appropriate keys and certs, and call SSL_ConfigServerCert
* with.
*/
@@ -1759,8 +1795,6 @@ slapd_ssl_init2(PRFileDesc **fd, int startTLS)
}
if (SECSuccess == rv) {
- SSLKEAType certKEA;
-
/* If we want weak dh params, flag it on the socket now! */
rv = SSL_OptionSet(*fd, SSL_ENABLE_SERVER_DHE, PR_TRUE);
if (rv != SECSuccess) {
@@ -1774,11 +1808,10 @@ slapd_ssl_init2(PRFileDesc **fd, int startTLS)
}
}
- certKEA = NSS_FindCertKEAType(cert);
- rv = SSL_ConfigSecureServer(*fd, cert, key, certKEA);
+ rv = SSL_ConfigServerCert(*fd, cert, key, NULL, 0);
if (SECSuccess != rv) {
errorCode = PR_GetError();
- slapd_SSL_warn("ConfigSecureServer: "
+ slapd_SSL_warn("SSL_ConfigServerCert: "
"Server key/certificate is "
"bad for cert %s of family %s (" SLAPI_COMPONENT_NAME_NSPR " error %d - %s)",
cert_name, *family, errorCode,
@@ -2663,6 +2696,38 @@ bail:
return rv;
}
+/* Helper for get_supported_key_type */
+static char *
+buf_add_str(char *buf, char *bufend, const char *str)
+{
+ /* bufend is sizeof(buf)-4 (to avoid overflow with ...) */
+ char *ret = buf+strlen(str);
+ if (ret > bufend) {
+ ret = bufend;
+ strcpy(buf, "...");
+ } else {
+ strcpy(buf, str);
+ }
+ return ret;
+}
+
+static void
+get_supported_key_type_names(char *buf, size_t bufsize)
+{
+ char *bufend = buf + bufsize - 4;
+ for (size_t i=0; supported_key_types[i].kt; i++) {
+ if (i>0) {
+ if (supported_key_types[i+1].kt == 0) {
+ /* Last supported key type */
+ buf = buf_add_str(buf, bufend, " or ");
+ } else {
+ buf = buf_add_str(buf, bufend, ", ");
+ }
+ }
+ buf = buf_add_str(buf, bufend, supported_key_types[i].shortname);
+ }
+}
+
/*
* Borrowed from keyutil.c (crypto-util)
*
@@ -2723,10 +2788,20 @@ extractKeysAndSubject(
}
keytype = (*privkey)->keyType;
- if (keytype != rsaKey && keytype != ecKey) {
- slapi_log_err(SLAPI_LOG_ERR, "extractKeysAndSubject",
- "Unexpected key algorythm in certificate: %s. Only rsa and ec keys are supported.\n", nickname);
- goto bail;
+ for (size_t i=0; ;i++) {
+ KeyType kt = supported_key_types[i].kt;
+ if (kt == keytype && keytype != 0) {
+ /* Stop looping if the key type is supported */
+ break;
+ }
+ if (kt == 0) {
+ /* No supported key type have been found. */
+ char sktnames[100] = "";
+ get_supported_key_type_names(sktnames, sizeof sktnames);
+ slapi_log_err(SLAPI_LOG_ERR, "extractKeysAndSubject",
+ "Unexpected key algorithm in certificate: %s. Only %s are supported.\n", nickname, sktnames);
+ goto bail;
+ }
}
*subject = CERT_AsciiToName(cert->subjectName);
--
2.52.0

View File

@ -0,0 +1,56 @@
From 4068f68bea77f466f9b3d87c766ea14d2f175b17 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 21 Jan 2026 19:58:46 -0800
Subject: [PATCH] Bump lodash from 4.17.21 to 4.17.23 in
/src/cockpit/389-console (#7203)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)
---
updated-dependencies:
- dependency-name: lodash
dependency-version: 4.17.23
dependency-type: indirect
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
src/cockpit/389-console/package-lock.json | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/cockpit/389-console/package-lock.json b/src/cockpit/389-console/package-lock.json
index 0aa5bbbb9..23faef62f 100644
--- a/src/cockpit/389-console/package-lock.json
+++ b/src/cockpit/389-console/package-lock.json
@@ -4833,9 +4833,9 @@
}
},
"node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
@@ -11087,9 +11087,9 @@
}
},
"lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="
},
"lodash.merge": {
"version": "4.6.2",
--
2.52.0

View File

@ -0,0 +1,517 @@
From 3ff253af76df07fe0519481795b7ed155fefaa9e Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Fri, 23 Jan 2026 17:35:45 -0800
Subject: [PATCH] Issue 7198 - Web console doesn't show sub-suffix when
parent-suffix points to an entry (#7202)
Description: The web console doesn't show sub-suffixes when the
nsslapd-parent-suffix attribute points to an entry rather than a backend
suffix.
For example, creating a sub-suffix ou=foo,ou=people,dc=example,dc=com
with parent-suffix ou=people,dc=example,dc=com (where ou=people is just an
entry, not a suffix) would not appear in the web console tree.
Fix: In backend_build_tree() and get_sub_suffixes(), the code only matched
when nsslapd-parent-suffix exactly equaled an existing backend suffix.
Now it also checks if the parent-suffix is an entry under the current
suffix (ends with ,suffix) and is not itself a backend suffix. This
correctly attaches sub-suffixes to their containing suffix when the
parent-suffix points to an intermediate entry.
Fixes: https://github.com/389ds/389-ds-base/issues/7198
Reviewed by: @progier389 (Thanks!)
---
.../suites/lib389/subsuffix_tree_test.py | 313 ++++++++++++++++++
src/lib389/lib389/backend.py | 47 ++-
src/lib389/lib389/cli_conf/backend.py | 34 +-
3 files changed, 370 insertions(+), 24 deletions(-)
create mode 100644 dirsrvtests/tests/suites/lib389/subsuffix_tree_test.py
diff --git a/dirsrvtests/tests/suites/lib389/subsuffix_tree_test.py b/dirsrvtests/tests/suites/lib389/subsuffix_tree_test.py
new file mode 100644
index 000000000..fa10ba530
--- /dev/null
+++ b/dirsrvtests/tests/suites/lib389/subsuffix_tree_test.py
@@ -0,0 +1,313 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2026 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+import logging
+import os
+import pytest
+from lib389.topologies import topology_st as topo
+from lib389.backend import Backends
+from lib389.idm.organizationalunit import OrganizationalUnits
+from lib389._constants import DEFAULT_SUFFIX
+
+pytestmark = pytest.mark.tier1
+
+logging.getLogger(__name__).setLevel(logging.INFO)
+log = logging.getLogger(__name__)
+
+
+@pytest.fixture(scope="function")
+def setup_subsuffix_with_entry_parent(topo, request):
+ """Setup a sub-suffix whose parent-suffix points to an entry, not a suffix."""
+ inst = topo.standalone
+
+ # Create ou=people entry under the root suffix
+ log.info("Creating ou=people,dc=example,dc=com entry")
+ ous = OrganizationalUnits(inst, DEFAULT_SUFFIX)
+ ou_people = ous.get('people')
+
+ # Create sub-suffix with parent-suffix pointing to the entry
+ log.info("Creating sub-suffix ou=foo,ou=people,dc=example,dc=com")
+ backends = Backends(inst)
+ subsuffix_dn = 'ou=foo,ou=people,dc=example,dc=com'
+ parent_suffix_dn = 'ou=people,dc=example,dc=com'
+
+ foo_backend = backends.create(properties={
+ 'cn': 'foo',
+ 'nsslapd-suffix': subsuffix_dn,
+ 'parent': parent_suffix_dn,
+ })
+
+ # Create the suffix entry
+ foo_ous = OrganizationalUnits(inst, parent_suffix_dn)
+ foo_ou = foo_ous.create(properties={'ou': 'foo'})
+
+ def cleanup():
+ log.info("Cleaning up test backends and entries")
+ try:
+ foo_ou.delete()
+ except Exception as e:
+ log.warning(f"Failed to delete foo_ou: {e}")
+ try:
+ foo_backend.delete()
+ except Exception as e:
+ log.warning(f"Failed to delete foo_backend: {e}")
+
+ request.addfinalizer(cleanup)
+
+ return {
+ 'instance': inst,
+ 'backends': backends,
+ 'foo_backend': foo_backend,
+ 'ou_people': ou_people,
+ 'subsuffix_dn': subsuffix_dn,
+ 'parent_suffix_dn': parent_suffix_dn,
+ }
+
+
+def test_subsuffix_with_entry_parent_in_tree(topo, setup_subsuffix_with_entry_parent):
+ """Test that a sub-suffix with parent pointing to an entry is visible in the tree.
+
+ :id: 256f36f5-76ad-4043-ad8d-1f9e2afc4e1d
+ :setup: Standalone instance with sub-suffix whose parent is an entry
+ :steps:
+ 1. Verify the sub-suffix backend exists
+ 2. Get sub-suffixes of the root backend
+ 3. Verify the sub-suffix appears in the list
+ :expectedresults:
+ 1. Backend should exist
+ 2. Sub-suffixes should be retrievable
+ 3. Sub-suffix should be visible (this is where the bug manifested)
+ """
+ backends = setup_subsuffix_with_entry_parent['backends']
+ foo_backend = setup_subsuffix_with_entry_parent['foo_backend']
+ subsuffix_dn = setup_subsuffix_with_entry_parent['subsuffix_dn']
+
+ # Step 1: Verify the sub-suffix backend exists
+ assert foo_backend.exists(), "The foo backend should exist"
+
+ # Step 2: Get sub-suffixes of the root backend
+ root_backend = backends.get(DEFAULT_SUFFIX)
+ sub_suffixes = root_backend.get_sub_suffixes()
+ log.info(f"Sub-suffixes found: {[s.get_attr_val_utf8('nsslapd-suffix') for s in sub_suffixes]}")
+
+ # Step 3: Verify sub-suffix is in the list
+ sub_suffix_found = any(
+ s.get_attr_val_utf8_l('nsslapd-suffix') == subsuffix_dn.lower()
+ for s in sub_suffixes
+ )
+
+ assert sub_suffix_found, (
+ f"Sub-suffix {subsuffix_dn} should be visible in get_sub_suffixes(). "
+ "The parent-suffix points to an entry, not a backend suffix."
+ )
+
+
+def test_subsuffix_in_backend_list(topo, setup_subsuffix_with_entry_parent):
+ """Test that the sub-suffix appears in the backend list.
+
+ :id: 0ccc49af-91bb-4e8f-b0e1-1bd0b75c041b
+ :setup: Standalone instance with sub-suffix configuration
+ :steps:
+ 1. Get all backends
+ 2. Verify both root suffix and sub-suffix are present
+ :expectedresults:
+ 1. Should retrieve all backends
+ 2. Both suffixes should be listed
+ """
+ backends = setup_subsuffix_with_entry_parent['backends']
+ subsuffix_dn = setup_subsuffix_with_entry_parent['subsuffix_dn']
+
+ be_list = backends.list()
+ suffixes = [be.get_attr_val_utf8_l('nsslapd-suffix') for be in be_list]
+
+ assert DEFAULT_SUFFIX.lower() in suffixes, \
+ f"Root suffix {DEFAULT_SUFFIX} should be in the list"
+ assert subsuffix_dn.lower() in suffixes, \
+ f"Sub-suffix {subsuffix_dn} should be in the list"
+
+
+def test_subsuffix_dn_boundary_matching():
+ """Test that suffix matching respects DN component boundaries.
+
+ :id: 0b856e26-c394-4c36-b9ba-d7894aa2ed11
+ :setup: None (unit test)
+ :steps:
+ 1. Test exact suffix match
+ 2. Test proper DN ancestor match (ends with ,suffix)
+ 3. Test that partial string matches are rejected
+ :expectedresults:
+ 1. Exact match should return True
+ 2. Proper ancestor should return True
+ 3. Partial string match should return False
+ """
+ from lib389.backend import is_subsuffix_of
+
+ all_suffixes = {'dc=com', 'dc=example,dc=com', 'ou=dept,dc=example,dc=com'}
+
+ # Test 1: Exact match
+ assert is_subsuffix_of('dc=example,dc=com', 'dc=example,dc=com', all_suffixes), \
+ "Exact match should return True"
+
+ # Test 2: Parent is an entry under the suffix (not itself a suffix)
+ assert is_subsuffix_of('ou=people,dc=example,dc=com', 'dc=example,dc=com', all_suffixes), \
+ "Parent entry under suffix should return True"
+
+ # Test 3: Parent IS a suffix - should return False (handled separately)
+ assert not is_subsuffix_of('ou=dept,dc=example,dc=com', 'dc=example,dc=com', all_suffixes), \
+ "Parent that is itself a suffix should return False"
+
+ # Test 4: Edge case - wrong DN boundary (string ends with suffix but wrong boundary)
+ edge_suffixes = {'dc=com', 'st,dc=com'}
+ assert is_subsuffix_of('dc=test,dc=com', 'dc=com', edge_suffixes), \
+ "dc=test,dc=com should match dc=com"
+ assert not is_subsuffix_of('dc=test,dc=com', 'st,dc=com', edge_suffixes), \
+ "dc=test,dc=com should NOT match st,dc=com (wrong DN boundary)"
+
+ # Test 5: None input
+ assert not is_subsuffix_of(None, 'dc=com', all_suffixes), \
+ "None parent should return False"
+
+ # Test 6: Closest ancestor - should only match the nearest suffix
+ # Hierarchy: dc=com -> dc=example,dc=com -> ou=branch,dc=example,dc=com (suffix)
+ # -> ou=dept,ou=branch,dc=example,dc=com (entry) -> subsuffix
+ # The subsuffix should only appear under ou=branch, not under dc=example,dc=com
+ nested_suffixes = {'dc=com', 'dc=example,dc=com', 'ou=branch,dc=example,dc=com'}
+ entry_parent = 'ou=dept,ou=branch,dc=example,dc=com'
+ # Should match ou=branch (closest)
+ assert is_subsuffix_of(entry_parent, 'ou=branch,dc=example,dc=com', nested_suffixes), \
+ "Should match closest ancestor suffix (ou=branch)"
+ # Should NOT match dc=example,dc=com (not closest)
+ assert not is_subsuffix_of(entry_parent, 'dc=example,dc=com', nested_suffixes), \
+ "Should NOT match distant ancestor (dc=example) - ou=branch is closer"
+ # Should NOT match dc=com (not closest)
+ assert not is_subsuffix_of(entry_parent, 'dc=com', nested_suffixes), \
+ "Should NOT match distant ancestor (dc=com) - ou=branch is closer"
+
+ log.info("All DN boundary edge cases passed")
+
+
+def test_deep_suffix_hierarchy(topo, request):
+ """Test complex hierarchy: suffix -> suffix -> entry -> suffix -> suffix.
+
+ :id: fd06491a-defa-4780-8472-78c077febdfb
+ :setup: Standalone instance
+ :steps:
+ 1. Create sub-suffix ou=branch (parent=dc=example,dc=com - a suffix)
+ 2. Create entry ou=dept,ou=branch (not a suffix)
+ 3. Create sub-suffix ou=team,ou=dept,ou=branch (parent=ou=dept - an entry)
+ 4. Create sub-suffix ou=sub,ou=team,ou=dept,ou=branch (parent=ou=team - a suffix)
+ 5. Verify all sub-suffixes are correctly placed in the tree
+ :expectedresults:
+ 1. Sub-suffix created successfully
+ 2. Entry created successfully
+ 3. Sub-suffix with entry parent created successfully
+ 4. Sub-suffix with suffix parent created successfully
+ 5. Tree hierarchy is correct
+ """
+ inst = topo.standalone
+ backends = Backends(inst)
+
+ # Define the hierarchy
+ branch_suffix = f'ou=branch,{DEFAULT_SUFFIX}'
+ dept_entry = f'ou=dept,{branch_suffix}' # This is an ENTRY, not a suffix
+ team_suffix = f'ou=team,{dept_entry}'
+ sub_suffix = f'ou=sub,{team_suffix}'
+
+ created_backends = []
+ created_entries = []
+
+ def cleanup():
+ log.info("Cleaning up deep hierarchy test")
+ for entry in reversed(created_entries):
+ try:
+ entry.delete()
+ except Exception as e:
+ log.warning(f"Failed to delete entry: {e}")
+ for be in reversed(created_backends):
+ try:
+ be.delete()
+ except Exception as e:
+ log.warning(f"Failed to delete backend: {e}")
+
+ request.addfinalizer(cleanup)
+
+ # Step 1: Create ou=branch sub-suffix (parent is root suffix)
+ log.info(f"Creating sub-suffix {branch_suffix}")
+ branch_be = backends.create(properties={
+ 'cn': 'branch',
+ 'nsslapd-suffix': branch_suffix,
+ 'parent': DEFAULT_SUFFIX,
+ })
+ created_backends.append(branch_be)
+ branch_ous = OrganizationalUnits(inst, DEFAULT_SUFFIX)
+ branch_ou = branch_ous.create(properties={'ou': 'branch'})
+ created_entries.append(branch_ou)
+
+ # Step 2: Create ou=dept entry under branch (NOT a suffix)
+ log.info(f"Creating entry {dept_entry}")
+ dept_ous = OrganizationalUnits(inst, branch_suffix)
+ dept_ou = dept_ous.create(properties={'ou': 'dept'})
+ created_entries.append(dept_ou)
+
+ # Step 3: Create ou=team sub-suffix (parent is dept ENTRY, not a suffix)
+ log.info(f"Creating sub-suffix {team_suffix} with entry parent {dept_entry}")
+ team_be = backends.create(properties={
+ 'cn': 'team',
+ 'nsslapd-suffix': team_suffix,
+ 'parent': dept_entry, # Parent is an ENTRY!
+ })
+ created_backends.append(team_be)
+ team_ous = OrganizationalUnits(inst, dept_entry)
+ team_ou = team_ous.create(properties={'ou': 'team'})
+ created_entries.append(team_ou)
+
+ # Step 4: Create ou=sub sub-suffix (parent is team suffix)
+ log.info(f"Creating sub-suffix {sub_suffix} with suffix parent {team_suffix}")
+ sub_be = backends.create(properties={
+ 'cn': 'sub',
+ 'nsslapd-suffix': sub_suffix,
+ 'parent': team_suffix, # Parent is a SUFFIX
+ })
+ created_backends.append(sub_be)
+ sub_ous = OrganizationalUnits(inst, team_suffix)
+ sub_ou = sub_ous.create(properties={'ou': 'sub'})
+ created_entries.append(sub_ou)
+
+ # Step 5: Verify the tree hierarchy
+ log.info("Verifying tree hierarchy...")
+
+ # Root should have branch as sub-suffix
+ root_be = backends.get(DEFAULT_SUFFIX)
+ root_subs = root_be.get_sub_suffixes()
+ root_sub_suffixes = [s.get_attr_val_utf8_l('nsslapd-suffix') for s in root_subs]
+ log.info(f"Root sub-suffixes: {root_sub_suffixes}")
+ assert branch_suffix.lower() in root_sub_suffixes, \
+ f"branch should be under root suffix"
+
+ # Branch should have team as sub-suffix (even though team's parent is an entry)
+ branch_be_obj = backends.get(branch_suffix)
+ branch_subs = branch_be_obj.get_sub_suffixes()
+ branch_sub_suffixes = [s.get_attr_val_utf8_l('nsslapd-suffix') for s in branch_subs]
+ log.info(f"Branch sub-suffixes: {branch_sub_suffixes}")
+ assert team_suffix.lower() in branch_sub_suffixes, \
+ f"team should be under branch suffix (parent is entry under branch)"
+
+ # Team should have sub as sub-suffix
+ team_be_obj = backends.get(team_suffix)
+ team_subs = team_be_obj.get_sub_suffixes()
+ team_sub_suffixes = [s.get_attr_val_utf8_l('nsslapd-suffix') for s in team_subs]
+ log.info(f"Team sub-suffixes: {team_sub_suffixes}")
+ assert sub_suffix.lower() in team_sub_suffixes, \
+ f"sub should be under team suffix"
+
+ log.info("Deep hierarchy test passed!")
+
+
+if __name__ == '__main__':
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main(["-s", CURRENT_FILE])
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
index db464b43a..274d45abe 100644
--- a/src/lib389/lib389/backend.py
+++ b/src/lib389/lib389/backend.py
@@ -38,6 +38,36 @@ from lib389.lint import DSBLE0001, DSBLE0002, DSBLE0003, DSBLE0004, DSBLE0005, D
from lib389.plugins import USNPlugin
+def is_subsuffix_of(sub_parent, be_suffix, all_suffixes):
+ """Check if sub_parent indicates this is a sub-suffix of be_suffix.
+
+ Returns True only if be_suffix is the CLOSEST ancestor suffix of sub_parent.
+ This prevents a sub-suffix from appearing under multiple ancestors.
+
+ :param sub_parent: The nsslapd-parent-suffix value (lowercase)
+ :param be_suffix: The suffix to check against (lowercase)
+ :param all_suffixes: Set of all backend suffixes (lowercase)
+ :returns: True if be_suffix is the closest ancestor suffix
+ """
+ if not sub_parent:
+ return False
+ if sub_parent == be_suffix:
+ return True
+ if sub_parent in all_suffixes:
+ # sub_parent is itself a suffix, will be handled separately
+ return False
+ if not sub_parent.endswith(',' + be_suffix):
+ return False
+ # Find the closest (longest) matching suffix for this parent
+ best_match = None
+ for sfx in all_suffixes:
+ if sub_parent == sfx or sub_parent.endswith(',' + sfx):
+ if best_match is None or len(sfx) > len(best_match):
+ best_match = sfx
+ # Only return True if be_suffix is the closest match
+ return best_match == be_suffix
+
+
class BackendLegacy(object):
proxied_methods = 'search_s getEntry'.split()
@@ -1104,22 +1134,27 @@ class Backend(DSLdapObject):
vlv.create(rdn="cn=" + vlvname, properties=props, basedn=basedn)
def get_sub_suffixes(self):
- """Return a list of Backend's
- returns: a List of subsuffix entries
+ """Return a list of Backend's that are sub-suffixes of this backend.
+ :returns: A list of Backend instances that are sub-suffixes
"""
subsuffixes = []
top_be_suffix = self.get_attr_val_utf8_l('nsslapd-suffix')
+ if not top_be_suffix:
+ return subsuffixes
+
mts = self._mts.list()
+ be_insts = Backends(self._instance).list()
+ all_suffixes = {be.get_attr_val_utf8_l('nsslapd-suffix') for be in be_insts}
+
for mt in mts:
parent_suffix = mt.get_attr_val_utf8_l('nsslapd-parent-suffix')
if parent_suffix is None:
continue
- if parent_suffix == top_be_suffix:
+
+ if is_subsuffix_of(parent_suffix, top_be_suffix, all_suffixes):
child_suffix = mt.get_attr_val_utf8_l('cn')
- be_insts = Backends(self._instance).list()
for be in be_insts:
- be_suffix = be.get_attr_val_utf8_l('nsslapd-suffix')
- if child_suffix == be_suffix:
+ if child_suffix == be.get_attr_val_utf8_l('nsslapd-suffix'):
subsuffixes.append(be)
break
return subsuffixes
diff --git a/src/lib389/lib389/cli_conf/backend.py b/src/lib389/lib389/cli_conf/backend.py
index d0ec4bd9e..9772e39d4 100644
--- a/src/lib389/lib389/cli_conf/backend.py
+++ b/src/lib389/lib389/cli_conf/backend.py
@@ -7,7 +7,7 @@
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---
-from lib389.backend import Backend, Backends, DatabaseConfig, BackendSuffixView
+from lib389.backend import Backend, Backends, DatabaseConfig, BackendSuffixView, is_subsuffix_of
from lib389.configurations.sample import (
create_base_domain,
create_base_org,
@@ -338,6 +338,7 @@ def is_db_replicated(inst, suffix):
def backend_get_subsuffixes(inst, basedn, log, args):
subsuffixes = []
be_insts = MANY(inst).list()
+ all_suffixes = {be.get_attr_val_utf8_l('nsslapd-suffix') for be in be_insts}
for be in be_insts:
be_suffix = be.get_attr_val_utf8_l('nsslapd-suffix')
if be_suffix == args.be_name.lower():
@@ -347,7 +348,7 @@ def backend_get_subsuffixes(inst, basedn, log, args):
db_type = "suffix"
sub = mt.get_attr_val_utf8_l('nsslapd-parent-suffix')
sub_be = mt.get_attr_val_utf8_l('nsslapd-backend')
- if sub == be_suffix:
+ if is_subsuffix_of(sub, be_suffix, all_suffixes):
# We have a subsuffix (maybe a db link?)
if is_db_link(inst, sub_be):
db_type = "link"
@@ -399,38 +400,34 @@ def build_node(suffix, be_name, subsuf=False, link=False, replicated=False):
}
-def backend_build_tree(inst, be_insts, nodes):
- """Recursively build the tree
- """
- if len(nodes) == 0:
- # Done
+def backend_build_tree(inst, be_insts, nodes, all_suffixes):
+ """Recursively build the tree."""
+ if not nodes:
return
for node in nodes:
- node_suffix = node['id']
+ node_suffix = node['id'].lower()
# Get sub suffixes and chaining of node
for be in be_insts:
be_suffix = be.get_attr_val_utf8_l('nsslapd-suffix')
- if be_suffix == node_suffix.lower():
+ if be_suffix == node_suffix:
# We have our parent, now find the children
mts = be._mts.list()
-
for mt in mts:
sub_parent = mt.get_attr_val_utf8_l('nsslapd-parent-suffix')
sub_be = mt.get_attr_val_utf8_l('nsslapd-backend')
sub_suffix = mt.get_attr_val_utf8_l('cn')
- if sub_parent == be_suffix:
+ if is_subsuffix_of(sub_parent, be_suffix, all_suffixes):
# We have a subsuffix (maybe a db link?)
link = is_db_link(inst, sub_be)
replicated = is_db_replicated(inst, sub_suffix)
node['children'].append(build_node(sub_suffix,
- sub_be,
- subsuf=True,
- link=link,
- replicated=replicated))
-
+ sub_be,
+ subsuf=True,
+ link=link,
+ replicated=replicated))
# Recurse over the new subsuffixes
- backend_build_tree(inst, be_insts, node['children'])
+ backend_build_tree(inst, be_insts, node['children'], all_suffixes)
break
@@ -471,7 +468,8 @@ def backend_get_tree(inst, basedn, log, args):
else:
# Build the tree
be_insts = Backends(inst).list()
- backend_build_tree(inst, be_insts, nodes)
+ all_suffixes = {be.get_attr_val_utf8_l('nsslapd-suffix') for be in be_insts}
+ backend_build_tree(inst, be_insts, nodes, all_suffixes)
# Done
if args.json:
--
2.52.0

View File

@ -0,0 +1,38 @@
From aa24c00d9ed1ea9730c0e8c5dccc1bbb5b61e312 Mon Sep 17 00:00:00 2001
From: Lenka Doudova <lryznaro@redhat.com>
Date: Mon, 26 Jan 2026 16:21:23 +0100
Subject: [PATCH] Issue 7014 - memberOf - ignored deferred updates with LMDB
Description:
Fix typo in pytest marker reason.
Relates: #7014
Author: Lenka Doudova
Reviewed by: ???
---
.../suites/memberof_plugin/memberof_deferred_lmdb_test.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/dirsrvtests/tests/suites/memberof_plugin/memberof_deferred_lmdb_test.py b/dirsrvtests/tests/suites/memberof_plugin/memberof_deferred_lmdb_test.py
index 0d9f793c1..12fcfa3ec 100644
--- a/dirsrvtests/tests/suites/memberof_plugin/memberof_deferred_lmdb_test.py
+++ b/dirsrvtests/tests/suites/memberof_plugin/memberof_deferred_lmdb_test.py
@@ -1,4 +1,5 @@
# --- BEGIN COPYRIGHT BLOCK ---
+
# Copyright (C) 2025 Red Hat, Inc.
# All rights reserved.
#
@@ -28,7 +29,7 @@ else:
logging.getLogger(__name__).setLevel(logging.INFO)
-@pytest.mark.skipif(get_default_db_lib() != "mdb", reason="Not supported over mdb")
+@pytest.mark.skipif(get_default_db_lib() != "mdb", reason="Not supported over bdb")
def test_memberof_deferred_update_lmdb_rejection(topo):
"""Test that memberOf plugin rejects deferred update configuration with LMDB backend
--
2.52.0

View File

@ -0,0 +1,48 @@
From 4a73a31e6c91e507e5aa2cba1e5bd55d1d07894d 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

@ -0,0 +1,174 @@
From bc28406778534a77064a26b4f0467dffecde33ea Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Tue, 27 Jan 2026 09:40:12 +0100
Subject: [PATCH] Issue 6947 - Revise time skew check in healthcheck tool - add
tests (#7208)
Description:
Add tests for DSSKEWLE0003 and DSSKEWLE0004 checks.
Relates: https://github.com/389ds/389-ds-base/issues/6947
Reviewed by: @droideck (Thanks!)
---
.../suites/healthcheck/health_skew_test.py | 148 ++++++++++++++++++
1 file changed, 148 insertions(+)
create mode 100644 dirsrvtests/tests/suites/healthcheck/health_skew_test.py
diff --git a/dirsrvtests/tests/suites/healthcheck/health_skew_test.py b/dirsrvtests/tests/suites/healthcheck/health_skew_test.py
new file mode 100644
index 000000000..9ee070364
--- /dev/null
+++ b/dirsrvtests/tests/suites/healthcheck/health_skew_test.py
@@ -0,0 +1,148 @@
+# --- BEGIN COPYRIGHT BLOCK ---
+# Copyright (C) 2026 Red Hat, Inc.
+# All rights reserved.
+#
+# License: GPL (version 3 or any later version).
+# See LICENSE for details.
+# --- END COPYRIGHT BLOCK ---
+#
+
+import logging
+import os
+import pytest
+import subprocess
+from lib389._constants import DEFAULT_SUFFIX
+from lib389.dseldif import DSEldif
+from lib389.replica import ReplicationManager
+from lib389.topologies import topology_m2
+
+pytestmark = pytest.mark.tier1
+
+CMD_OUTPUT = 'No issues found.'
+JSON_OUTPUT = '[]'
+
+log = logging.getLogger(__name__)
+
+
+def run_healthcheck_and_check_result(instance, searched_code, json=False, isnot=False):
+ """Run healthcheck and verify the expected code is in the output"""
+ cmd = ['dsctl']
+ if json:
+ cmd.append('--json')
+ if searched_code == CMD_OUTPUT:
+ searched_code = JSON_OUTPUT
+ cmd.append(instance.serverid)
+ cmd.extend(['healthcheck', '--check', 'dseldif:nsstate'])
+
+ result = subprocess.run(cmd, capture_output=True, universal_newlines=True)
+ log.info(f'Running: {cmd}')
+ log.info(f'Stdout: {result.stdout}')
+ log.info(f'Stderr: {result.stderr}')
+ log.info(f'Return code: {result.returncode}')
+ stdout = result.stdout
+
+ # stdout should not be empty
+ assert stdout is not None
+ assert len(stdout) > 0
+
+ if isnot:
+ assert searched_code not in stdout, \
+ f'{searched_code} should NOT be in healthcheck output but was found'
+ log.info(f'Verified {searched_code} is NOT in healthcheck output')
+ else:
+ assert searched_code in stdout, \
+ f'{searched_code} should be in healthcheck output but was not found'
+ log.info(f'Verified {searched_code} is in healthcheck output')
+
+
+def test_healthcheck_time_skew_extensive(topology_m2):
+ """Check if HealthCheck returns DSSKEWLE0003 and DSSKEWLE0004 codes for extensive time skew
+
+ :id: 7591cd2b-8d66-4d33-9f8d-babff0571086
+ :setup: Two suppliers replication topology
+ :steps:
+ 1. Create a replicated topology
+ 2. Stop supplier1
+ 3. Increase time skew on supplier1 to over 24 hours
+ 4. Start supplier1
+ 5. Use HealthCheck and verify DSSKEWLE0003 is reported
+ 6. Set nsslapd-ignore-time-skew to on
+ 7. Use HealthCheck and verify DSSKEWLE0003 is NOT reported
+ 8. Stop supplier1
+ 9. Increase time skew on supplier1 to over 365 days
+ 10. Start supplier1
+ 11. Use HealthCheck and verify only DSSKEWLE0004 is reported
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. Healthcheck reports DSSKEWLE0003 code
+ 6. Success
+ 7. Healthcheck does not report DSSKEWLE0003 code
+ 8. Success
+ 9. Success
+ 10. Success
+ 11. Healthcheck reports DSSKEWLE0004 code and not DSSKEWLE0003
+ """
+
+ M1 = topology_m2.ms['supplier1']
+ M2 = topology_m2.ms['supplier2']
+
+ # Ensure replication is working first
+ repl = ReplicationManager(DEFAULT_SUFFIX)
+ repl.wait_for_replication(M1, M2)
+
+ # Step 2-4: Stop supplier1, increase time skew to over 24 hours, start supplier1
+ log.info('Stop supplier1 to modify dse.ldif')
+ M1.stop()
+
+ # Set time skew to over 24 hours (86400 seconds)
+ # Add a margin to be safely over the threshold
+ time_skew_24h = 86400 + 3600 # 25 hours
+ log.info(f'Increase time skew on supplier1 by {time_skew_24h} seconds')
+ DSEldif(M1)._increaseTimeSkew(DEFAULT_SUFFIX, time_skew_24h)
+
+ log.info('Start supplier1')
+ M1.start()
+
+ # Step 5: Verify DSSKEWLE0003 is reported
+ log.info('Run healthcheck and verify DSSKEWLE0003 is reported')
+ run_healthcheck_and_check_result(M1, 'DSSKEWLE0003', json=False)
+ run_healthcheck_and_check_result(M1, 'DSSKEWLE0003', json=True)
+
+ # Step 6: Set nsslapd-ignore-time-skew to on
+ log.info('Set nsslapd-ignore-time-skew to on')
+ M1.config.set('nsslapd-ignore-time-skew', 'on')
+
+ # Step 7: Verify DSSKEWLE0003 is NOT reported when ignoring time skew
+ log.info('Run healthcheck and verify DSSKEWLE0003 is NOT reported')
+ run_healthcheck_and_check_result(M1, 'DSSKEWLE0003', json=False, isnot=True)
+ run_healthcheck_and_check_result(M1, 'DSSKEWLE0003', json=True, isnot=True)
+
+ # Step 8-10: Stop supplier1, increase time skew to over 365 days, start supplier1
+ log.info('Stop supplier1 to modify dse.ldif')
+ M1.stop()
+
+ # Increase time skew to over 365 days (31536000 seconds)
+ # We need to add enough to go from current ~25 hours to over 365 days
+ time_skew_year = (86400 * 365) + 86400 # 366 days total additional
+ log.info(f'Increase time skew on supplier1 by {time_skew_year} seconds')
+ DSEldif(M1)._increaseTimeSkew(DEFAULT_SUFFIX, time_skew_year)
+
+ log.info('Start supplier1')
+ M1.start()
+
+ # Step 11: Verify only DSSKEWLE0004 is reported (not DSSKEWLE0003)
+ log.info('Run healthcheck and verify only DSSKEWLE0004 is reported')
+ run_healthcheck_and_check_result(M1, 'DSSKEWLE0004', json=False)
+ run_healthcheck_and_check_result(M1, 'DSSKEWLE0004', json=True)
+ run_healthcheck_and_check_result(M1, 'DSSKEWLE0003', json=False, isnot=True)
+ run_healthcheck_and_check_result(M1, 'DSSKEWLE0003', json=True, isnot=True)
+
+
+if __name__ == '__main__':
+ # Run isolated
+ # -s for DEBUG mode
+ CURRENT_FILE = os.path.realpath(__file__)
+ pytest.main("-s %s" % CURRENT_FILE)
--
2.52.0

View File

@ -0,0 +1,215 @@
From a53c0e4dea5c35fef196500b2a36c92bc8f07a51 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Tue, 27 Jan 2026 09:49:16 +0100
Subject: [PATCH] Issue 7201 - Syscall overhead in LMDB import writer thread
(#7204)
Bug Description:
ldif2db import is slower with LMDB than with BDB (~3500 vs ~4500
entries/s) due to 2 issues:
1. The MDB_STAT_STEP macro calls `clock_gettime()` to collect
performance statistics. This was called on every single operation inside
the writer loop, resulting in ~3 syscalls per write or ~6000 syscalls
per transaction (with 2000 as the default batch size).
2. In `dbmdb_import_workerq_push()`, after copying work to a worker
slot, the condition variable was never signaled. This caused workers to
spend up to 100ms in `safe_cond_wait()` before checking for new work,
severely limiting import throughput.
Fix Description:
1. Add a new config parameter `nsslapd-mdb-import-stats` (default: off)
to control whether performance statistics collection is enabled.
2. Add `pthread_cond_broadcast()` immediately after copying data to wake
the workers.
After applying these fixes ldif2db import rate is about ~10000 entries/s.
Fixes: https://github.com/389ds/389-ds-base/issues/7201
Reviewed by: @progier389 (Thanks!)
---
.../slapd/back-ldbm/db-mdb/mdb_config.c | 23 +++++++++++
.../back-ldbm/db-mdb/mdb_import_threads.c | 40 ++++++++++---------
.../slapd/back-ldbm/db-mdb/mdb_layer.h | 2 +
3 files changed, 46 insertions(+), 19 deletions(-)
diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_config.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_config.c
index 96295ed5b..ade42b1d7 100644
--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_config.c
+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_config.c
@@ -477,6 +477,28 @@ dbmdb_ctx_t_db_durable_transactions_set(void *arg, void *value, char *errorbuf _
return retval;
}
+static void *
+dbmdb_ctx_t_db_import_stats_get(void *arg)
+{
+ struct ldbminfo *li = (struct ldbminfo *)arg;
+
+ return (void *)((uintptr_t)(MDB_CONFIG(li)->dsecfg.import_stats));
+}
+
+static int
+dbmdb_ctx_t_db_import_stats_set(void *arg, void *value, char *errorbuf __attribute__((unused)), int phase __attribute__((unused)), int apply)
+{
+ struct ldbminfo *li = (struct ldbminfo *)arg;
+ int retval = LDAP_SUCCESS;
+ int val = (int)((uintptr_t)value);
+
+ if (apply) {
+ MDB_CONFIG(li)->dsecfg.import_stats = val;
+ }
+
+ return retval;
+}
+
static int
dbmdb_ctx_t_set_bypass_filter_test(void *arg,
void *value,
@@ -589,6 +611,7 @@ static config_info dbmdb_ctx_t_param[] = {
{CONFIG_MDB_MAX_DBS, CONFIG_TYPE_INT, "512", &dbmdb_ctx_t_db_max_dbs_get, &dbmdb_ctx_t_db_max_dbs_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
{CONFIG_MAXPASSBEFOREMERGE, CONFIG_TYPE_INT, "100", &dbmdb_ctx_t_maxpassbeforemerge_get, &dbmdb_ctx_t_maxpassbeforemerge_set, 0},
{CONFIG_DB_DURABLE_TRANSACTIONS, CONFIG_TYPE_ONOFF, "on", &dbmdb_ctx_t_db_durable_transactions_get, &dbmdb_ctx_t_db_durable_transactions_set, CONFIG_FLAG_ALWAYS_SHOW},
+ {CONFIG_MDB_IMPORT_STATS, CONFIG_TYPE_ONOFF, "off", &dbmdb_ctx_t_db_import_stats_get, &dbmdb_ctx_t_db_import_stats_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
{CONFIG_BYPASS_FILTER_TEST, CONFIG_TYPE_STRING, "on", &dbmdb_ctx_t_get_bypass_filter_test, &dbmdb_ctx_t_set_bypass_filter_test, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
{CONFIG_SERIAL_LOCK, CONFIG_TYPE_ONOFF, "on", &dbmdb_ctx_t_serial_lock_get, &dbmdb_ctx_t_serial_lock_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
{CONFIG_CACHE_AUTOSIZE, CONFIG_TYPE_INT, "25", &mdb_config_cache_autosize_get, &mdb_config_cache_autosize_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c
index 2270abd69..65b29343e 100644
--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c
+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c
@@ -59,9 +59,9 @@
/* import thread usage statistics */
-#define MDB_STAT_INIT(stats) { mdb_stat_collect(&stats, MDB_STAT_RUN, 1); }
-#define MDB_STAT_END(stats) { mdb_stat_collect(&stats, MDB_STAT_RUN, 0); }
-#define MDB_STAT_STEP(stats, step) { mdb_stat_collect(&stats, (step), 0); }
+#define MDB_STAT_INIT(stats, enabled) { if (enabled) mdb_stat_collect(&stats, MDB_STAT_RUN, 1); }
+#define MDB_STAT_END(stats, enabled) { if (enabled) mdb_stat_collect(&stats, MDB_STAT_RUN, 0); }
+#define MDB_STAT_STEP(stats, step, enabled) { if (enabled) mdb_stat_collect(&stats, (step), 0); }
typedef enum {
MDB_STAT_RUN,
@@ -424,6 +424,7 @@ dbmdb_import_workerq_push(ImportQueue_t *q, WorkerQueueData_t *data)
return -1;
}
dbmdb_dup_worker_slot(q, data, slot);
+ pthread_cond_broadcast(&q->cv);
pthread_mutex_unlock(&q->mutex);
return 0;
}
@@ -3914,14 +3915,15 @@ dbmdb_import_writer(void*param)
int count = 0;
int rc = 0;
mdb_stat_info_t stats = {0};
+ int stats_enabled = ctx->ctx->dsecfg.import_stats;
- MDB_STAT_INIT(stats);
+ MDB_STAT_INIT(stats, stats_enabled);
while (!rc && !info_is_finished(info)) {
- MDB_STAT_STEP(stats, MDB_STAT_PAUSE);
+ MDB_STAT_STEP(stats, MDB_STAT_PAUSE, stats_enabled);
wait_for_starting(info);
- MDB_STAT_STEP(stats, MDB_STAT_READ);
+ MDB_STAT_STEP(stats, MDB_STAT_READ, stats_enabled);
slot = dbmdb_import_q_getall(&ctx->writerq);
- MDB_STAT_STEP(stats, MDB_STAT_RUN);
+ MDB_STAT_STEP(stats, MDB_STAT_RUN, stats_enabled);
if (info_is_finished(info)) {
dbmdb_import_q_flush(&ctx->writerq);
break;
@@ -3932,14 +3934,14 @@ dbmdb_import_writer(void*param)
for (; slot; slot = nextslot) {
if (!txn) {
- MDB_STAT_STEP(stats, MDB_STAT_TXNSTART);
+ MDB_STAT_STEP(stats, MDB_STAT_TXNSTART, stats_enabled);
rc = TXN_BEGIN(ctx->ctx->env, NULL, 0, &txn);
}
if (!rc) {
- MDB_STAT_STEP(stats, MDB_STAT_WRITE);
+ MDB_STAT_STEP(stats, MDB_STAT_WRITE, stats_enabled);
rc = MDB_PUT(txn, slot->dbi->dbi, &slot->key, &slot->data, 0);
}
- MDB_STAT_STEP(stats, MDB_STAT_RUN);
+ MDB_STAT_STEP(stats, MDB_STAT_RUN, stats_enabled);
nextslot = slot->next;
slapi_ch_free((void**)&slot);
}
@@ -3947,9 +3949,9 @@ dbmdb_import_writer(void*param)
break;
}
if (count++ >= WRITER_MAX_OPS_IN_TXN) {
- MDB_STAT_STEP(stats, MDB_STAT_TXNSTOP);
+ MDB_STAT_STEP(stats, MDB_STAT_TXNSTOP, stats_enabled);
rc = TXN_COMMIT(txn);
- MDB_STAT_STEP(stats, MDB_STAT_RUN);
+ MDB_STAT_STEP(stats, MDB_STAT_RUN, stats_enabled);
if (rc) {
break;
}
@@ -3958,32 +3960,32 @@ dbmdb_import_writer(void*param)
}
}
if (txn && !rc) {
- MDB_STAT_STEP(stats, MDB_STAT_TXNSTOP);
+ MDB_STAT_STEP(stats, MDB_STAT_TXNSTOP, stats_enabled);
rc = TXN_COMMIT(txn);
- MDB_STAT_STEP(stats, MDB_STAT_RUN);
+ MDB_STAT_STEP(stats, MDB_STAT_RUN, stats_enabled);
if (!rc) {
txn = NULL;
}
}
if (txn) {
- MDB_STAT_STEP(stats, MDB_STAT_TXNSTOP);
+ MDB_STAT_STEP(stats, MDB_STAT_TXNSTOP, stats_enabled);
TXN_ABORT(txn);
- MDB_STAT_STEP(stats, MDB_STAT_RUN);
+ MDB_STAT_STEP(stats, MDB_STAT_RUN, stats_enabled);
txn = NULL;
}
- MDB_STAT_STEP(stats, MDB_STAT_WRITE);
+ MDB_STAT_STEP(stats, MDB_STAT_WRITE, stats_enabled);
if (!rc) {
/* Ensure that all data are written on disk */
rc = mdb_env_sync(ctx->ctx->env, 1);
}
- MDB_STAT_END(stats);
+ MDB_STAT_END(stats, stats_enabled);
if (rc) {
slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
"Failed to write in the database. Error is 0x%x: %s.\n",
rc, mdb_strerror(rc));
thread_abort(info);
- } else {
+ } else if (stats_enabled) {
char buf[200];
char *summary = mdb_stat_summarize(&stats, buf, sizeof buf);
if (summary) {
diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.h b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.h
index 647d00db9..cf6d00dd4 100644
--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.h
+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.h
@@ -41,6 +41,7 @@
#define CONFIG_MDB_MAX_SIZE "nsslapd-mdb-max-size"
#define CONFIG_MDB_MAX_READERS "nsslapd-mdb-max-readers"
#define CONFIG_MDB_MAX_DBS "nsslapd-mdb-max-dbs"
+#define CONFIG_MDB_IMPORT_STATS "nsslapd-mdb-import-stats"
#define DBMDB_DB_MINSIZE ( 4LL * MEGABYTE )
#define DBMDB_DISK_RESERVE(disksize) ((disksize)*2ULL/1000ULL)
@@ -76,6 +77,7 @@ typedef struct
int max_readers;
int max_dbs;
uint64_t max_size;
+ int import_stats;
} dbmdb_cfg_t;
/* config parameters limits */
--
2.52.0

View File

@ -0,0 +1,135 @@
From 40484cb0b5034bc3c1e23b2ae1f2d39eedce07e9 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

@ -0,0 +1,149 @@
From 8c6d1cfca22f87b20b79a419ceabdb182846ff4a Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Tue, 27 Jan 2026 15:39:39 +0100
Subject: [PATCH] Issue 7206 - Should log whether TLS key is PQC or not (#7207)
Append [PQC] to the cipher name when logging SSL/TLS if the Key Exchange is one of the KEM group
If connection debug level is enabled, logs in error log the keaType and keaGroup (as integer)
Issue: #7206
Reviewed by: @tbordaz, @droideck (Thanks!)
* Issue 7206 - Should log whether TLS key is PQC or not
* Fix Sourcery A/I comments
* Check PQC in test case
* Update ldap/servers/slapd/auth.c
Co-authored-by: Simon Pichugin <spichugi@redhat.com>
---------
Co-authored-by: Simon Pichugin <spichugi@redhat.com>
---
dirsrvtests/tests/suites/tls/mldsa_test.py | 7 ++++
ldap/servers/slapd/auth.c | 45 +++++++++++++++++++++-
ldap/servers/slapd/ssl.c | 2 +-
3 files changed, 52 insertions(+), 2 deletions(-)
diff --git a/dirsrvtests/tests/suites/tls/mldsa_test.py b/dirsrvtests/tests/suites/tls/mldsa_test.py
index 2c815088b..d6cbaf662 100644
--- a/dirsrvtests/tests/suites/tls/mldsa_test.py
+++ b/dirsrvtests/tests/suites/tls/mldsa_test.py
@@ -274,6 +274,7 @@ def test_mldsa(topo):
"""
inst = topo.standalone
+ inst.config.set('nsslapd-errorlog-level', str(16384 + 8))
inst.enable_tls()
cm = CertmapLegacy(inst)
@@ -293,6 +294,9 @@ def test_mldsa(topo):
'loginShell': '/bin/false',
'description': cert_dn })
+ inst.config.set("nsslapd-accesslog-logbuffering", "off")
+ inst.config.set("nsslapd-errorlog-logbuffering", "off")
+
tmpdir_kwargs = {}
if sys.version_info >= (3, 12):
tmpdir_kwargs['delete'] = not DEBUGGING
@@ -313,6 +317,9 @@ def test_mldsa(topo):
res.check_returncode()
# If ldapsearch is successful then defaultnamingcontext should be in res.stdout
assert "defaultnamingcontext" in res.stdout
+ assert inst.ds_access_log.match('.*RESULT.*dn="uid=test_user,ou=people,dc=example,dc=com".*')
+ assert inst.ds_access_log.match('.*TLS.*[PQC].*')
+ assert inst.ds_error_log.match('.*check_pqc.*')
if __name__ == '__main__':
diff --git a/ldap/servers/slapd/auth.c b/ldap/servers/slapd/auth.c
index 48e4b7129..9e83408d4 100644
--- a/ldap/servers/slapd/auth.c
+++ b/ldap/servers/slapd/auth.c
@@ -395,6 +395,43 @@ handle_bad_certificate(void *clientData, PRFileDesc *prfd)
}
+/*
+ * Determine if the connection key exchange is Post Quantum Cryptography aware.
+ * This function may need to evolve with NSS as more PQC methods get supported.
+ */
+static bool
+check_pqc(uint64_t connid, SSLChannelInfo *sci)
+{
+ /*
+ * FYI: To interpret the values:
+ * KeaType and KeaGroup values are defined in
+ * https://github.com/nss-dev/nss/blob/master/lib/ssl/sslt.h
+ * Respectively in SSLKEAType and SSLNamedGroup enums
+ */
+ slapi_log_err(SLAPI_LOG_CONNS, "check_pqc", "conn=%" PRIu64 " TLS keaType=%d keaGroup=%d\n",
+ connid, sci->keaType, sci->keaGroup);
+#ifdef MAX_ML_DSA_PRIVATE_KEY_LEN
+ /* NSS supports PQC (because of the ifdef). Now lets check if the connection uses it */
+ /* Check that PQC KeaType is hybrid */
+ switch (sci->keaType) {
+ case ssl_kea_ecdh_hybrid:
+ case ssl_kea_ecdh_hybrid_psk:
+ break;
+ default:
+ return false;
+ }
+ /* Check that PQC keaGroup is KEM */
+ switch (sci->keaGroup) {
+ case ssl_grp_kem_secp256r1mlkem768:
+ case ssl_grp_kem_secp384r1mlkem1024:
+ case ssl_grp_kem_mlkem768x25519:
+ case ssl_grp_kem_xyber768d00:
+ return true;
+ }
+#endif
+ return false;
+}
+
/*
* Get an identity from the client's certificate (if any was sent).
*
@@ -417,6 +454,7 @@ handle_handshake_done(PRFileDesc *prfd, void *clientData)
SSLCipherSuiteInfo cipherInfo;
char *subject = NULL;
char sslversion[64];
+ bool pqc = false;
int err = 0;
if ((slapd_ssl_getChannelInfo(prfd, &channelInfo, sizeof(channelInfo))) != SECSuccess) {
@@ -454,7 +492,12 @@ handle_handshake_done(PRFileDesc *prfd, void *clientData)
}
keySize = cipherInfo.effectiveKeyBits;
- cipher = slapi_ch_strdup(cipherInfo.symCipherName);
+ pqc = check_pqc(conn->c_connid, &channelInfo);
+ if (pqc) {
+ cipher = slapi_ch_smprintf("%s[PQC]", cipherInfo.symCipherName);
+ } else {
+ cipher = slapi_ch_strdup(cipherInfo.symCipherName);
+ }
/* If inside an Start TLS operation, perform the privacy level discovery
* and if the security degree achieved after the handshake is not reckoned
diff --git a/ldap/servers/slapd/ssl.c b/ldap/servers/slapd/ssl.c
index 053db5424..7d5db2cdd 100644
--- a/ldap/servers/slapd/ssl.c
+++ b/ldap/servers/slapd/ssl.c
@@ -748,7 +748,7 @@ SSLPLCY_Install(void)
if (!slapd_pk11_isFIPS()) {
/* Set explicitly PQC algorithm policy if it is not set by default */
for (size_t i=0; s == SECSuccess && i < PR_ARRAY_SIZE(oids); i++) {
- int oflags = 0;
+ PRUint32 oflags = 0;
(void) NSS_GetAlgorithmPolicy(oids[i], &oflags);
if ((oflags & flags) != flags) {
s = NSS_SetAlgorithmPolicy(oids[i], flags, 0);
--
2.52.0

View File

@ -0,0 +1,53 @@
From 27d3ea211847fa7ae674c5e4dcf485706e4ac591 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 ea744ac1e..80c07382a 100644
--- a/ldap/servers/slapd/log.c
+++ b/ldap/servers/slapd/log.c
@@ -206,8 +206,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;
}
@@ -217,17 +217,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

@ -0,0 +1,195 @@
From 5ebce22d4214bec5ed94ad84c4448164be99389a 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
---
.../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 f7847ac74..7cdf16a84 100644
--- a/dirsrvtests/tests/suites/vlv/regression_test.py
+++ b/dirsrvtests/tests/suites/vlv/regression_test.py
@@ -1175,6 +1175,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 d320ecbeb..cd797621d 100644
--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c
+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c
@@ -2134,10 +2134,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 8f9263f25..f2f882b5a 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

@ -0,0 +1,285 @@
From 56f1881b5fb0198af4810eeca9e98736c297a2a5 Mon Sep 17 00:00:00 2001
From: Lenka Doudova <mirielka@users.noreply.github.com>
Date: Tue, 3 Feb 2026 10:28:02 +0100
Subject: [PATCH] Issue 6753 - Port ticket 47781 test (#7210)
Description:
Port ticket 47781 test into dirsrvtests/tests/suites/replication/replication_deadlock_test.py
Relates: #6753
Author: Lenka Doudova
Assisted by: Cursor
Reviewer: Mark Reynolds
---
.../replication/replication_deadlock_test.py | 129 +++++++++++++++++-
dirsrvtests/tests/tickets/ticket47781_test.py | 104 --------------
2 files changed, 128 insertions(+), 105 deletions(-)
delete mode 100644 dirsrvtests/tests/tickets/ticket47781_test.py
diff --git a/dirsrvtests/tests/suites/replication/replication_deadlock_test.py b/dirsrvtests/tests/suites/replication/replication_deadlock_test.py
index 8ddbb43bd..9c122add3 100644
--- a/dirsrvtests/tests/suites/replication/replication_deadlock_test.py
+++ b/dirsrvtests/tests/suites/replication/replication_deadlock_test.py
@@ -15,14 +15,20 @@ with replication data when replication agreements are present.
import logging
import os
import pytest
+import ldap
+from lib389._mapped_object import DSLdapObjects
from lib389.topologies import topology_m2 as topo
+from lib389.topologies import topology_st
from lib389.replica import Replicas, ReplicationManager
from lib389.agreement import Agreements
from lib389.tasks import ImportTask, ExportTask
from lib389.idm.user import UserAccounts
from lib389.tombstone import Tombstones
-from lib389._constants import DEFAULT_SUFFIX
+from lib389.backend import Backends
+from lib389._constants import (DEFAULT_SUFFIX, DEFAULT_BENAME, defaultProperties,
+ REPLICATION_BIND_DN, REPLICATION_BIND_PW,
+ REPLICATION_BIND_METHOD, REPLICATION_TRANSPORT)
pytestmark = pytest.mark.tier2
@@ -200,6 +206,127 @@ def test_replication_deadlock_tombstone_search(topo):
os.remove(export_ldif)
+def test_replication_deadlock_tombstone_invalid_agreement(topology_st):
+ """Test deadlock scenario after importing LDIF with replication data and invalid agreement
+
+ This test verifies that a deadlock does not occur while searching for tombstones after
+ setting up an invalid replication agreement (pointing to a non-existent server)
+
+ :id: a1b2c3d4-e5f6-4781-9abc-def012345678
+ :setup: Standalone instance with replication enabled
+ :steps:
+ 1. Create a supplier with an invalid replication agreement (port 5555, non-existent server)
+ 2. Add two test user entries
+ 3. Export LDIF with replication data
+ 4. Restart the server
+ 5. Import the LDIF back
+ 6. Search for tombstone entries with timeout (should not hang/deadlock)
+ 7. Cleanup: restore original timeout settings, delete invalid agreement, test entries, and export file
+ :expectedresults:
+ 1. Invalid replication agreement is created successfully
+ 2. Test entries are created successfully
+ 3. LDIF export completes successfully
+ 4. Server restarts successfully
+ 5. LDIF import completes successfully
+ 6. Tombstone search completes without deadlock (returns at least one entry; no hang/timeout)
+ 7. Cleanup completes successfully
+ """
+
+ standalone = topology_st.standalone
+ export_ldif = os.path.join(standalone.get_ldif_dir(), 'export.ldif')
+
+ # Step 1: Create supplier with invalid replication agreement
+ log.info('Creating supplier with invalid replication agreement (port 5555 - non-existent server)')
+ repl = ReplicationManager(DEFAULT_SUFFIX)
+ repl.create_first_supplier(standalone)
+
+ replicas = Replicas(standalone)
+ replica = replicas.get(DEFAULT_SUFFIX)
+ agreements = Agreements(standalone, basedn=replica.dn)
+
+ # Create agreement with invalid port (non-existent server)
+ invalid_agreement = agreements.create(properties={
+ 'cn': r'meTo_$host:$port',
+ 'nsDS5ReplicaRoot': DEFAULT_SUFFIX,
+ 'nsDS5ReplicaHost': standalone.host,
+ 'nsDS5ReplicaPort': '5555', # Invalid port - server does not exist
+ 'nsDS5ReplicaBindDN': defaultProperties[REPLICATION_BIND_DN],
+ 'nsDS5ReplicaBindMethod': defaultProperties[REPLICATION_BIND_METHOD],
+ 'nsDS5ReplicaTransportInfo': defaultProperties[REPLICATION_TRANSPORT],
+ 'nsDS5ReplicaCredentials': defaultProperties[REPLICATION_BIND_PW]
+ })
+
+ # Step 2: Add two test entries
+ log.info('Adding two test entries')
+ users = UserAccounts(standalone, DEFAULT_SUFFIX)
+
+ entry1 = users.create(properties={
+ 'uid': 'entry1',
+ 'cn': 'entry1',
+ 'sn': 'user',
+ 'userpassword': 'password',
+ 'uidNumber': '1001',
+ 'gidNumber': '2001',
+ 'homeDirectory': '/home/entry1'
+ })
+
+ entry2 = users.create(properties={
+ 'uid': 'entry2',
+ 'cn': 'entry2',
+ 'sn': 'user',
+ 'userpassword': 'password',
+ 'uidNumber': '1002',
+ 'gidNumber': '2002',
+ 'homeDirectory': '/home/entry2'
+ })
+
+ # Step 3: Export LDIF with replication data
+ log.info('Exporting LDIF with replication data')
+ backends = Backends(standalone)
+ export_task = backends.export_ldif(be_names=DEFAULT_BENAME, ldif=export_ldif, replication=True)
+ export_task.wait()
+
+ # Step 4: Restart the server
+ log.info('Restarting server')
+ standalone.restart()
+
+ # Step 5: Import the LDIF
+ log.info('Importing replication LDIF file')
+ import_task = ImportTask(standalone)
+ import_task.import_suffix_from_ldif(ldiffile=export_ldif, suffix=DEFAULT_SUFFIX)
+ import_task.wait()
+
+ # Step 6: Search for tombstones with timeout - should not hang/deadlock
+ log.info('Searching for tombstone entries (should find entries and not hang)')
+ # Save original timeout settings so we can restore them in cleanup
+ orig_network_timeout = standalone.get_option(ldap.OPT_NETWORK_TIMEOUT)
+ orig_timeout = standalone.get_option(ldap.OPT_TIMEOUT)
+ # Set explicit timeouts to detect deadlocks
+ standalone.set_option(ldap.OPT_NETWORK_TIMEOUT, 5)
+ standalone.set_option(ldap.OPT_TIMEOUT, 5)
+
+ # Verify at least one tombstone exists and the search does not hang/timeout
+ try:
+ results = DSLdapObjects(standalone, DEFAULT_SUFFIX).filter('(objectclass=nsTombstone)')
+ log.info(f'Found {len(results)} tombstone entries')
+ assert len(results) > 0, "Tombstone search should return at least one entry"
+ except Exception as e:
+ log.error(f"Tombstone search failed with error: {e}")
+ pytest.fail(f"Tombstone search failed: {e}")
+ finally:
+ # Restore original timeout settings
+ standalone.set_option(ldap.OPT_NETWORK_TIMEOUT, orig_network_timeout)
+ standalone.set_option(ldap.OPT_TIMEOUT, orig_timeout)
+ # Cleanup test entries and export file
+ log.info('Cleaning up test entries')
+ invalid_agreement.delete()
+ entry1.delete()
+ entry2.delete()
+ if os.path.exists(export_ldif):
+ os.remove(export_ldif)
+
+ log.info('Test completed successfully - no deadlock detected')
+
if __name__ == '__main__':
CURRENT_FILE = os.path.realpath(__file__)
pytest.main(["-s", CURRENT_FILE])
diff --git a/dirsrvtests/tests/tickets/ticket47781_test.py b/dirsrvtests/tests/tickets/ticket47781_test.py
deleted file mode 100644
index ffb9a5e1a..000000000
--- a/dirsrvtests/tests/tickets/ticket47781_test.py
+++ /dev/null
@@ -1,104 +0,0 @@
-# --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2016 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
-from lib389.tasks import *
-from lib389.topologies import topology_st
-from lib389.replica import ReplicationManager
-
-from lib389._constants import (defaultProperties, DEFAULT_SUFFIX, ReplicaRole,
- REPLICAID_SUPPLIER_1, REPLICATION_BIND_DN, REPLICATION_BIND_PW,
- REPLICATION_BIND_METHOD, REPLICATION_TRANSPORT, RA_NAME,
- RA_BINDDN, RA_BINDPW, RA_METHOD, RA_TRANSPORT_PROT)
-
-pytestmark = pytest.mark.tier2
-
-log = logging.getLogger(__name__)
-
-
-def test_ticket47781(topology_st):
- """
- Testing for a deadlock after doing an online import of an LDIF with
- replication data. The replication agreement should be invalid.
- """
-
- log.info('Testing Ticket 47781 - Testing for deadlock after importing LDIF with replication data')
-
- supplier = topology_st.standalone
- repl = ReplicationManager(DEFAULT_SUFFIX)
- repl.create_first_supplier(supplier)
-
- properties = {RA_NAME: r'meTo_$host:$port',
- RA_BINDDN: defaultProperties[REPLICATION_BIND_DN],
- RA_BINDPW: defaultProperties[REPLICATION_BIND_PW],
- RA_METHOD: defaultProperties[REPLICATION_BIND_METHOD],
- RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
- # The agreement should point to a server that does NOT exist (invalid port)
- repl_agreement = supplier.agreement.create(suffix=DEFAULT_SUFFIX,
- host=supplier.host,
- port=5555,
- properties=properties)
-
- #
- # add two entries
- #
- log.info('Adding two entries...')
-
- supplier.add_s(Entry(('cn=entry1,dc=example,dc=com', {
- 'objectclass': 'top person'.split(),
- 'sn': 'user',
- 'cn': 'entry1'})))
-
- supplier.add_s(Entry(('cn=entry2,dc=example,dc=com', {
- 'objectclass': 'top person'.split(),
- 'sn': 'user',
- 'cn': 'entry2'})))
-
- #
- # export the replication ldif
- #
- log.info('Exporting replication ldif...')
- args = {EXPORT_REPL_INFO: True}
- exportTask = Tasks(supplier)
- exportTask.exportLDIF(DEFAULT_SUFFIX, None, "/tmp/export.ldif", args)
-
- #
- # Restart the server
- #
- log.info('Restarting server...')
- supplier.stop()
- supplier.start()
-
- #
- # Import the ldif
- #
- log.info('Import replication LDIF file...')
- importTask = Tasks(supplier)
- args = {TASK_WAIT: True}
- importTask.importLDIF(DEFAULT_SUFFIX, None, "/tmp/export.ldif", args)
- os.remove("/tmp/export.ldif")
-
- #
- # Search for tombstones - we should not hang/timeout
- #
- log.info('Search for tombstone entries(should find one and not hang)...')
- supplier.set_option(ldap.OPT_NETWORK_TIMEOUT, 5)
- supplier.set_option(ldap.OPT_TIMEOUT, 5)
- entries = supplier.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, 'objectclass=nsTombstone')
- if not entries:
- log.fatal('Search failed to find any entries.')
- assert PR_False
-
-
-if __name__ == '__main__':
- # Run isolated
- # -s for DEBUG mode
- CURRENT_FILE = os.path.realpath(__file__)
- pytest.main("-s %s" % CURRENT_FILE)
--
2.52.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,265 @@
From 603f4deb1e87c819c1830f58c7be4281d981fbbd Mon Sep 17 00:00:00 2001
From: Lenka Doudova <lryznaro@redhat.com>
Date: Mon, 2 Feb 2026 16:46:52 +0100
Subject: [PATCH] Issue 6753 - Port ticket 48896 test
Description:
Port ticket 48896 test into dirsrvtests/tests/suites/password/pwdPolicy_token_test.py
Relates: #6753
Author: Lenka Doudova
Assisted by: Cursor
Reviewer: Mark Reynolds
---
.../suites/password/pwdPolicy_token_test.py | 68 ++++++---
dirsrvtests/tests/tickets/ticket48896_test.py | 139 ------------------
2 files changed, 44 insertions(+), 163 deletions(-)
delete mode 100644 dirsrvtests/tests/tickets/ticket48896_test.py
diff --git a/dirsrvtests/tests/suites/password/pwdPolicy_token_test.py b/dirsrvtests/tests/suites/password/pwdPolicy_token_test.py
index ae4eb300f..a3caaa230 100644
--- a/dirsrvtests/tests/suites/password/pwdPolicy_token_test.py
+++ b/dirsrvtests/tests/suites/password/pwdPolicy_token_test.py
@@ -15,6 +15,7 @@ from lib389._constants import *
from lib389.idm.user import UserAccounts
from lib389.idm.organizationalunit import OrganizationalUnits
from lib389.topologies import topology_st as topo
+from lib389.idm.directorymanager import DirectoryManager
pytestmark = pytest.mark.tier1
@@ -25,14 +26,13 @@ else:
logging.getLogger(__name__).setLevel(logging.INFO)
log = logging.getLogger(__name__)
-USER_DN = 'uid=Test_user1,ou=People,dc=example,dc=com'
USER_ACI = '(targetattr="userpassword")(version 3.0; acl "pwp test"; allow (all) userdn="ldap:///self";)'
-TOKEN = 'test_user1'
+TOKEN = 'test_user123'
user_properties = {
- 'uid': 'Test_user1',
- 'cn': 'test_user1',
- 'sn': 'test_user1',
+ 'uid': 'Test_user123',
+ 'cn': 'test_user123',
+ 'sn': 'test_user123',
'uidNumber': '1001',
'gidNumber': '2001',
'userpassword': PASSWORD,
@@ -59,28 +59,48 @@ def test_token_lengths(topo):
:id: dae9d916-2a03-4707-b454-9e901d295b13
:setup: Standalone instance
:steps:
- 1. Test token length rejects password of the same length as rdn value
+ 1. Create user, setup global password policy
+ 2. Bind as user, change password to 'Abcd012+'
+ 3. Bind as user with 'Abcd012+', attempt changes to 'user', 'us123', 'Tuse!1234', 'Tuse!0987', 'Tabc!1234'
+ 4. For each passwordMinTokenLength 4, 6, 10: change settings, rebind as user, attempt password with token of that length from TOKEN
+ 5. Cleanup - delete user
:expectedresults:
- 1. Passwords are rejected
+ 1. User created, password policy enabled and set
+ 2. Success
+ 3. All attempts fail with CONSTRAINT_VIOLATION
+ 4. All attempts fail with CONSTRAINT_VIOLATION
+ 5. User successfully deleted
"""
user = pwd_setup(topo)
- for length in ['4', '6', '10']:
- topo.standalone.simple_bind_s(DN_DM, PASSWORD)
- topo.standalone.config.set('passwordMinTokenLength', length)
- topo.standalone.simple_bind_s(USER_DN, PASSWORD)
- time.sleep(1)
-
- try:
- passwd = TOKEN[:int(length)]
- log.info("Testing password len {} token ({})".format(length, passwd))
- user.replace('userpassword', passwd)
- log.fatal('Password incorrectly allowed!')
- assert False
- except ldap.CONSTRAINT_VIOLATION as e:
- log.info('Password correctly rejected: ' + str(e))
- except ldap.LDAPError as e:
- log.fatal('Unexpected failure ' + str(e))
- assert False
+ dm = DirectoryManager(topo.standalone)
+
+ try:
+ # Verify that the user can change their password
+ user.rebind(PASSWORD)
+ user.replace('userpassword', 'Abcd012+')
+
+ # Verify that the default password policy is enforced
+ user.rebind('Abcd012+')
+ for new_password in ['user', 'us123', 'Tuse!1234', 'Tuse!0987', 'Tabc!1234']:
+ log.info(f"Testing password {new_password}")
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ user.replace('userpassword', new_password)
+
+ # Verify that the password policy is enforced for different token lengths
+ for length in ['4', '6', '10']:
+ dm.rebind(PASSWORD)
+ topo.standalone.config.set('passwordMinTokenLength', length)
+ user.rebind('Abcd012+')
+ time.sleep(1)
+
+ with pytest.raises(ldap.CONSTRAINT_VIOLATION):
+ passwd = TOKEN[:int(length)]
+ log.info("Testing password len {} token ({})".format(length, passwd))
+ user.replace('userpassword', passwd)
+
+ finally:
+ # Cleanup
+ user.delete()
if __name__ == '__main__':
diff --git a/dirsrvtests/tests/tickets/ticket48896_test.py b/dirsrvtests/tests/tickets/ticket48896_test.py
deleted file mode 100644
index a1897589e..000000000
--- a/dirsrvtests/tests/tickets/ticket48896_test.py
+++ /dev/null
@@ -1,139 +0,0 @@
-# --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2016 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.tasks import *
-from lib389.utils import *
-from lib389.topologies import topology_st
-
-from lib389._constants import DEFAULT_SUFFIX, DN_DM, PASSWORD
-
-# Skip on older versions
-pytestmark = [pytest.mark.tier2,
- pytest.mark.skipif(ds_is_older('1.3.6'), reason="Not implemented")]
-
-logging.getLogger(__name__).setLevel(logging.DEBUG)
-log = logging.getLogger(__name__)
-
-CONFIG_DN = 'cn=config'
-UID = 'buser123'
-TESTDN = 'uid=%s,' % UID + DEFAULT_SUFFIX
-
-
-def check_attr_val(topology_st, dn, attr, expected):
- try:
- centry = topology_st.standalone.search_s(dn, ldap.SCOPE_BASE, 'cn=*')
- if centry:
- val = centry[0].getValue(attr)
- if val == expected:
- log.info('Default value of %s is %s' % (attr, expected))
- else:
- log.info('Default value of %s is not %s, but %s' % (attr, expected, val))
- assert False
- else:
- log.fatal('Failed to get %s' % dn)
- assert False
- except ldap.LDAPError as e:
- log.fatal('Failed to search ' + dn + ': ' + e.message['desc'])
- assert False
-
-
-def replace_pw(server, curpw, newpw, expstr, rc):
- log.info('Binding as {%s, %s}' % (TESTDN, curpw))
- server.simple_bind_s(TESTDN, curpw)
-
- hit = 0
- log.info('Replacing password: %s -> %s, which should %s' % (curpw, newpw, expstr))
- try:
- server.modify_s(TESTDN, [(ldap.MOD_REPLACE, 'userPassword', ensure_bytes(newpw))])
- except Exception as e:
- log.info("Exception (expected): %s" % type(e).__name__)
- hit = 1
- assert isinstance(e, rc)
-
- if (0 != rc) and (0 == hit):
- log.info('Expected to fail with %s, but passed' % rc.__name__)
- assert False
-
- log.info('PASSED')
-
-
-def test_ticket48896(topology_st):
- """
- """
- log.info('Testing Ticket 48896 - Default Setting for passwordMinTokenLength does not work')
-
- log.info("Setting global password policy with password syntax.")
- topology_st.standalone.simple_bind_s(DN_DM, PASSWORD)
- topology_st.standalone.modify_s(CONFIG_DN, [(ldap.MOD_REPLACE, 'passwordCheckSyntax', b'on'),
- (ldap.MOD_REPLACE, 'nsslapd-pwpolicy-local', b'on')])
-
- config = topology_st.standalone.search_s(CONFIG_DN, ldap.SCOPE_BASE, 'cn=*')
- mintokenlen = config[0].getValue('passwordMinTokenLength')
- history = config[0].getValue('passwordInHistory')
-
- log.info('Default passwordMinTokenLength == %s' % mintokenlen)
- log.info('Default passwordInHistory == %s' % history)
-
- log.info('Adding a user.')
- curpw = 'password'
- topology_st.standalone.add_s(Entry((TESTDN,
- {'objectclass': "top person organizationalPerson inetOrgPerson".split(),
- 'cn': 'test user',
- 'sn': 'user',
- 'userPassword': curpw})))
-
- newpw = 'Abcd012+'
- exp = 'be ok'
- rc = 0
- replace_pw(topology_st.standalone, curpw, newpw, exp, rc)
-
- curpw = 'Abcd012+'
- newpw = 'user'
- exp = 'fail'
- rc = ldap.CONSTRAINT_VIOLATION
- replace_pw(topology_st.standalone, curpw, newpw, exp, rc)
-
- curpw = 'Abcd012+'
- newpw = UID
- exp = 'fail'
- rc = ldap.CONSTRAINT_VIOLATION
- replace_pw(topology_st.standalone, curpw, newpw, exp, rc)
-
- curpw = 'Abcd012+'
- newpw = 'Tuse!1234'
- exp = 'fail'
- rc = ldap.CONSTRAINT_VIOLATION
- replace_pw(topology_st.standalone, curpw, newpw, exp, rc)
-
- curpw = 'Abcd012+'
- newpw = 'Tuse!0987'
- exp = 'fail'
- rc = ldap.CONSTRAINT_VIOLATION
- replace_pw(topology_st.standalone, curpw, newpw, exp, rc)
-
- curpw = 'Abcd012+'
- newpw = 'Tabc!1234'
- exp = 'fail'
- rc = ldap.CONSTRAINT_VIOLATION
- replace_pw(topology_st.standalone, curpw, newpw, exp, rc)
-
- curpw = 'Abcd012+'
- newpw = 'Direc+ory389'
- exp = 'be ok'
- rc = 0
- replace_pw(topology_st.standalone, curpw, newpw, exp, rc)
-
- log.info('SUCCESS')
-
-
-if __name__ == '__main__':
- # Run isolated
- # -s for DEBUG mode
- CURRENT_FILE = os.path.realpath(__file__)
- pytest.main("-s %s" % CURRENT_FILE)
--
2.52.0

View File

@ -0,0 +1,30 @@
From ff44acffcc67c985148c4df280685a674fec010a Mon Sep 17 00:00:00 2001
From: Akshay Adhikari <aadhikar@redhat.com>
Date: Thu, 5 Feb 2026 15:19:58 +0530
Subject: [PATCH] Issue 6810 - Fix PAM PTA test (#7219)
Description: Fix the PAM PTA test by add missing yield in fixture.
Relates: #6810
Reviewed by: @jchapma
---
dirsrvtests/tests/suites/plugins/pam_pta_test.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/dirsrvtests/tests/suites/plugins/pam_pta_test.py b/dirsrvtests/tests/suites/plugins/pam_pta_test.py
index 484c1bc80..e55a7f7ee 100644
--- a/dirsrvtests/tests/suites/plugins/pam_pta_test.py
+++ b/dirsrvtests/tests/suites/plugins/pam_pta_test.py
@@ -104,6 +104,8 @@ def pam_service_ldapserver(migrated_child_config):
f.write(line + "\n")
os.chmod(pam_file, 0o644)
+ yield
+
except Exception as e:
if os.path.exists(backup_file):
# Restore backup on error
--
2.52.0

View File

@ -0,0 +1,57 @@
From 2a0ed9c267fc56a14e84ad53ffaeb0b822594367 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 759edb80d..e859789b3 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_modrdn.c
@@ -102,6 +102,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;
@@ -1181,6 +1182,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) {
@@ -1363,12 +1366,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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,93 @@
From d5a83e8f2ccd0c9d11792f026947c4785996b4a6 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 c843a4b24..a49e378c3 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 = 25 if is_fips() else 20 # Reserve descriptor constant (higher in FIPS mode)
- 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

@ -0,0 +1,94 @@
From 9bfbec8c4aad1c698fd80b3086e11f06fd9df26d Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Mon, 9 Feb 2026 13:15:44 +0100
Subject: [PATCH] Issue 7178 - Bundled jemalloc fails to build with GCC 15
(#7216)
Description:
Update spec file to fix build failures on Fedora Rawhide
Fixes: https://github.com/389ds/389-ds-base/issues/7178
Reviewed by: @progier389, @droideck (Thanks!)
---
rpm.mk | 1 +
rpm/389-ds-base.spec.in | 10 +++++-----
rpm/jemalloc-5.3.0_throw_bad_alloc.patch | 12 ++++++++++++
3 files changed, 18 insertions(+), 5 deletions(-)
create mode 100644 rpm/jemalloc-5.3.0_throw_bad_alloc.patch
diff --git a/rpm.mk b/rpm.mk
index f91011814..372cee5a6 100644
--- a/rpm.mk
+++ b/rpm.mk
@@ -143,6 +143,7 @@ srpmdistdir:
rpmbuildprep:
cp dist/sources/$(TARBALL) $(RPMBUILD)/SOURCES/
cp rpm/$(PACKAGE)-* $(RPMBUILD)/SOURCES/
+ cp rpm/jemalloc-5.3.0_throw_bad_alloc.patch $(RPMBUILD)/SOURCES/
if [ $(BUNDLE_JEMALLOC) -eq 1 ]; then \
cp dist/sources/$(JEMALLOC_TARBALL) $(RPMBUILD)/SOURCES/ ; \
fi
diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in
index 51bfd7e77..dc8c75dac 100644
--- a/rpm/389-ds-base.spec.in
+++ b/rpm/389-ds-base.spec.in
@@ -152,8 +152,8 @@ BuildRequires: python%{python3_pkgversion}-devel
# For cockpit
%if %{with cockpit}
BuildRequires: rsync
-BuildRequires: npm
-BuildRequires: nodejs
+BuildRequires: /usr/bin/npm
+BuildRequires: /usr/bin/node
%endif
# END BUILD REQUIRES
@@ -188,9 +188,7 @@ Requires: %{name}-robdb-libs = %{version}-%{release}
Requires: libdb
%endif
%endif
-Requires: lmdb
-# This picks up libperl.so as a Requires, so we add this versioned one
-Requires: perl(:MODULE_COMPAT_%(eval "`%{__perl} -V:version`"; echo $version))
+Requires: lmdb-libs
# Needed by logconv.pl
%if %{without libbdb_ro}
%if %{without bundle_libdb}
@@ -217,6 +215,7 @@ Source0: %{name}-%{version}.tar.bz2
Source2: %{name}-devel.README
%if %{with bundle_jemalloc}
Source3: https://github.com/jemalloc/%{jemalloc_name}/releases/download/%{jemalloc_ver}/%{jemalloc_name}-%{jemalloc_ver}.tar.bz2
+Source5: jemalloc-5.3.0_throw_bad_alloc.patch
%endif
%if %{with bundle_libdb}
Source4: https://fedorapeople.org/groups/389ds/libdb-5.3.28-59.tar.bz2
@@ -434,6 +433,7 @@ COCKPIT_FLAGS="--disable-cockpit"
# Build jemalloc
pushd ../%{jemalloc_name}-%{jemalloc_ver}
+patch -p1 -F3 < %{SOURCE5}
%configure \
--libdir=%{_libdir}/%{pkgname}/lib \
--bindir=%{_libdir}/%{pkgname}/bin \
diff --git a/rpm/jemalloc-5.3.0_throw_bad_alloc.patch b/rpm/jemalloc-5.3.0_throw_bad_alloc.patch
new file mode 100644
index 000000000..685e6968c
--- /dev/null
+++ b/rpm/jemalloc-5.3.0_throw_bad_alloc.patch
@@ -0,0 +1,12 @@
+diff --git a/src/jemalloc_cpp.cpp b/src/jemalloc_cpp.cpp
+index fffd6aee..5a682991 100644
+--- a/src/jemalloc_cpp.cpp
++++ b/src/jemalloc_cpp.cpp
+@@ -93,7 +93,7 @@ handleOOM(std::size_t size, bool nothrow) {
+ }
+
+ if (ptr == nullptr && !nothrow)
+- std::__throw_bad_alloc();
++ throw std::bad_alloc();
+ return ptr;
+ }
--
2.52.0

View File

@ -0,0 +1,71 @@
From fb4254a97fe0da25064d2f6296705c9e1810ffda 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

@ -0,0 +1,778 @@
From 3938942a5418add83616b1413f3070d394c30a7f 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
This reverts changes introduced by the following commits:
c6f458b42 Issue 7189 - DSBLE0007 generates incorrect remediation commands for scan limits
8b6b3a9f9 Issue 6966 - On large DB, unlimited IDL scan limit reduce the SRCH performance
Relates: https://github.com/389ds/389-ds-base/issues/7223
Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
---
.../tests/suites/config/config_test.py | 27 +---
.../healthcheck/health_system_indexes_test.py | 136 +-----------------
.../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 | 104 +++-----------
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, 40 insertions(+), 364 deletions(-)
diff --git a/dirsrvtests/tests/suites/config/config_test.py b/dirsrvtests/tests/suites/config/config_test.py
index cbb8875fa..2c7d949d0 100644
--- a/dirsrvtests/tests/suites/config/config_test.py
+++ b/dirsrvtests/tests/suites/config/config_test.py
@@ -706,19 +706,17 @@ def test_ndn_cache_size_enforcement(topo, request):
request.addfinalizer(fin)
-def test_require_index(topo, request):
+def test_require_index(topo):
"""Validate that unindexed searches are rejected
:id: fb6e31f2-acc2-4e75-a195-5c356faeb803
:setup: Standalone instance
:steps:
1. Set "nsslapd-require-index" to "on"
- 2. ancestorid/idlscanlimit to 100
- 3. Test an unindexed search is rejected
+ 2. Test an unindexed search is rejected
:expectedresults:
1. Success
2. Success
- 3. Success
"""
# Set the config
@@ -729,10 +727,6 @@ def test_require_index(topo, request):
db_cfg = DatabaseConfig(topo.standalone)
db_cfg.set([('nsslapd-idlistscanlimit', '100')])
- backend = Backends(topo.standalone).get_backend(DEFAULT_SUFFIX)
- ancestorid_index = backend.get_index('ancestorid')
- ancestorid_index.replace("nsIndexIDListScanLimit", ensure_bytes("limit=100 type=eq flags=AND"))
- topo.standalone.restart()
users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
for i in range(101):
@@ -743,15 +737,10 @@ def test_require_index(topo, request):
with pytest.raises(ldap.UNWILLING_TO_PERFORM):
raw_objects.filter("(description=test*)")
- def fin():
- ancestorid_index.replace("nsIndexIDListScanLimit", ensure_bytes("limit=5000 type=eq flags=AND"))
-
- request.addfinalizer(fin)
-
@pytest.mark.skipif(ds_is_older('1.4.2'), reason="The config setting only exists in 1.4.2 and higher")
-def test_require_internal_index(topo, request):
+def test_require_internal_index(topo):
"""Ensure internal operations require indexed attributes
:id: 22b94f30-59e3-4f27-89a1-c4f4be036f7f
@@ -782,10 +771,6 @@ def test_require_internal_index(topo, request):
# Create a bunch of users
db_cfg = DatabaseConfig(topo.standalone)
db_cfg.set([('nsslapd-idlistscanlimit', '100')])
- backend = Backends(topo.standalone).get_backend(DEFAULT_SUFFIX)
- ancestorid_index = backend.get_index('ancestorid')
- ancestorid_index.replace("nsIndexIDListScanLimit", ensure_bytes("limit=100 type=eq flags=AND"))
- topo.standalone.restart()
users = UserAccounts(topo.standalone, DEFAULT_SUFFIX)
for i in range(102, 202):
users.create_test_user(uid=i)
@@ -810,12 +795,6 @@ def test_require_internal_index(topo, request):
with pytest.raises(ldap.UNWILLING_TO_PERFORM):
user.delete()
- def fin():
- ancestorid_index.replace("nsIndexIDListScanLimit", ensure_bytes("limit=5000 type=eq flags=AND"))
-
- request.addfinalizer(fin)
-
-
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 486fad44b..140845a33 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -172,9 +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"],
- idlistscanlimit=['limit=5000 type=eq flags=AND'])
- standalone.restart()
+ backend.add_index("parentid", ["eq"], matching_rules=["integerOrderingMatch"])
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)
@@ -263,8 +261,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")
- backend.add_index("entryusn", ["eq"], matching_rules=["integerOrderingMatch"],
- idlistscanlimit=['limit=5000 type=eq flags=AND'])
+ backend.add_index("entryusn", ["eq"], matching_rules=["integerOrderingMatch"])
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)
@@ -408,132 +405,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)
-def test_missing_scanlimit(topology_st, log_buffering_enabled):
- """Check if healthcheck returns DSBLE0007 code when parentId index is missing scanlimit
-
- :id: 40e1bf6a-2397-459b-bdf3-f787ca118b86
- :setup: Standalone instance
- :steps:
- 1. Create DS instance
- 2. Remove nsIndexIDListScanLimit from parentId index
- 3. Use healthcheck without --json option
- 4. Use healthcheck with --json option
- 5. Verify the remediation command has properly quoted scanlimit
- 6. Re-add the scanlimit
- 7. Use healthcheck without --json option
- 8. Use healthcheck with --json option
- :expectedresults:
- 1. Success
- 2. Success
- 3. healthcheck reports DSBLE0007 code and related details
- 4. healthcheck reports DSBLE0007 code and related details
- 5. The scanlimit value is quoted in the remediation command
- 6. Success
- 7. healthcheck reports no issues found
- 8. healthcheck reports no issues found
- """
-
- RET_CODE = "DSBLE0007"
- PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
- SCANLIMIT_VALUE = "limit=5000 type=eq flags=AND"
-
- standalone = topology_st.standalone
-
- log.info("Remove nsIndexIDListScanLimit from parentId index")
- parentid_index = Index(standalone, PARENTID_DN)
- parentid_index.remove("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
-
- run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=RET_CODE)
-
- # Verify the remediation command has properly quoted scanlimit
- args = FakeArgs()
- args.instance = standalone.serverid
- args.verbose = standalone.verbose
- args.list_errors = False
- args.list_checks = False
- args.exclude_check = []
- args.check = ["backends"]
- args.dry_run = False
- args.json = False
- health_check_run(standalone, topology_st.logcap.log, args)
- # Check that the scanlimit is quoted in the output
- assert topology_st.logcap.contains('--add-scanlimit "limit=5000 type=eq flags=AND"')
- log.info("Verified scanlimit is properly quoted in remediation command")
- topology_st.logcap.flush()
-
- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=RET_CODE)
-
- log.info("Re-add the nsIndexIDListScanLimit")
- parentid_index = Index(standalone, PARENTID_DN)
- parentid_index.add("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
-
- 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)
-
-
-def test_missing_matching_rule_and_scanlimit(topology_st, log_buffering_enabled):
- """Check if healthcheck generates a single combined command when both matching rule and scanlimit are missing
-
- :id: af8214ad-5e4c-422a-8f74-3e99227551df
- :setup: Standalone instance
- :steps:
- 1. Create DS instance
- 2. Remove both integerOrderingMatch and nsIndexIDListScanLimit from parentId index
- 3. Use healthcheck and verify a single combined command is generated
- 4. Re-add the matching rule and scanlimit
- 5. Use healthcheck without --json option
- 6. Use healthcheck with --json option
- :expectedresults:
- 1. Success
- 2. Success
- 3. healthcheck reports DSBLE0007 and generates a single command with both --add-mr and --add-scanlimit
- 4. Success
- 5. healthcheck reports no issues found
- 6. healthcheck reports no issues found
- """
-
- RET_CODE = "DSBLE0007"
- PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
- SCANLIMIT_VALUE = "limit=5000 type=eq flags=AND"
-
- standalone = topology_st.standalone
-
- log.info("Remove both integerOrderingMatch and nsIndexIDListScanLimit from parentId index")
- parentid_index = Index(standalone, PARENTID_DN)
- parentid_index.remove("nsMatchingRule", "integerOrderingMatch")
- parentid_index.remove("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
-
- # Run healthcheck and verify combined command
- args = FakeArgs()
- args.instance = standalone.serverid
- args.verbose = standalone.verbose
- args.list_errors = False
- args.list_checks = False
- args.exclude_check = []
- args.check = ["backends"]
- args.dry_run = False
- args.json = False
- health_check_run(standalone, topology_st.logcap.log, args)
-
- # Verify DSBLE0007 is reported
- assert topology_st.logcap.contains(RET_CODE)
- log.info("healthcheck returned code: %s" % RET_CODE)
-
- # Verify a single combined command is generated with both --add-mr and --add-scanlimit
- assert topology_st.logcap.contains('--add-mr integerOrderingMatch --add-scanlimit "limit=5000 type=eq flags=AND"')
- log.info("Verified combined command with both --add-mr and --add-scanlimit")
-
- topology_st.logcap.flush()
-
- log.info("Re-add the integerOrderingMatch matching rule and scanlimit")
- parentid_index = Index(standalone, PARENTID_DN)
- parentid_index.add("nsMatchingRule", "integerOrderingMatch")
- parentid_index.add("nsIndexIDListScanLimit", SCANLIMIT_VALUE)
-
- 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)
-
-
def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
"""Check if healthcheck returns DSBLE0007 code when multiple system indexes are missing
@@ -574,8 +445,7 @@ def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
log.info("Re-add the missing system indexes")
backend = Backends(standalone).get("userRoot")
- backend.add_index("parentid", ["eq"], matching_rules=["integerOrderingMatch"],
- idlistscanlimit=['limit=5000 type=eq flags=AND'])
+ backend.add_index("parentid", ["eq"], matching_rules=["integerOrderingMatch"])
backend.add_index("nsuniqueid", ["eq"])
standalone.restart()
diff --git a/dirsrvtests/tests/suites/paged_results/paged_results_test.py b/dirsrvtests/tests/suites/paged_results/paged_results_test.py
index 61d6702da..1bb94b53a 100644
--- a/dirsrvtests/tests/suites/paged_results/paged_results_test.py
+++ b/dirsrvtests/tests/suites/paged_results/paged_results_test.py
@@ -306,19 +306,19 @@ def test_search_success(topology_st, create_user, page_size, users_num):
del_users(users_list)
-@pytest.mark.parametrize("page_size,users_num,suffix,attr_name,attr_value,expected_err, restart", [
+@pytest.mark.parametrize("page_size,users_num,suffix,attr_name,attr_value,expected_err", [
(50, 200, 'cn=config,%s' % DN_LDBM, 'nsslapd-idlistscanlimit', '100',
- ldap.UNWILLING_TO_PERFORM, True),
+ ldap.UNWILLING_TO_PERFORM),
(5, 15, DN_CONFIG, 'nsslapd-timelimit', '20',
- ldap.UNAVAILABLE_CRITICAL_EXTENSION, False),
+ ldap.UNAVAILABLE_CRITICAL_EXTENSION),
(21, 50, DN_CONFIG, 'nsslapd-sizelimit', '20',
- ldap.SIZELIMIT_EXCEEDED, False),
+ ldap.SIZELIMIT_EXCEEDED),
(21, 50, DN_CONFIG, 'nsslapd-pagedsizelimit', '5',
- ldap.SIZELIMIT_EXCEEDED, False),
+ ldap.SIZELIMIT_EXCEEDED),
(5, 50, 'cn=config,%s' % DN_LDBM, 'nsslapd-lookthroughlimit', '20',
- ldap.ADMINLIMIT_EXCEEDED, False)])
+ ldap.ADMINLIMIT_EXCEEDED)])
def test_search_limits_fail(topology_st, create_user, page_size, users_num,
- suffix, attr_name, attr_value, expected_err, restart):
+ suffix, attr_name, attr_value, expected_err):
"""Verify that search with a simple paged results control
throws expected exceptoins when corresponding limits are
exceeded.
@@ -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)
- ancestorid_index = None
- if attr_name == 'nsslapd-idlistscanlimit':
- backend = Backends(topology_st.standalone).get_backend(DEFAULT_SUFFIX)
- ancestorid_index = backend.get_index('ancestorid')
- ancestorid_index.replace("nsIndexIDListScanLimit", ensure_bytes("limit=100 type=eq flags=AND"))
-
- if (restart):
- log.info('Instance restarted')
- topology_st.standalone.restart()
conf_param_dict = {attr_name: attr_value}
search_flt = r'(uid=test*)'
searchreq_attrlist = ['dn', 'sn']
@@ -402,8 +393,6 @@ def test_search_limits_fail(topology_st, create_user, page_size, users_num,
else:
break
finally:
- if ancestorid_index:
- ancestorid_index.replace("nsIndexIDListScanLimit", ensure_bytes("limit=5000 type=eq flags=AND"))
del_users(users_list)
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 b187c26bc..e23e7ff43 100644
--- a/ldap/servers/slapd/back-ldbm/back-ldbm.h
+++ b/ldap/servers/slapd/back-ldbm/back-ldbm.h
@@ -583,7 +583,6 @@ struct ldbminfo
int li_mode;
int li_lookthroughlimit;
int li_allidsthreshold;
- int li_system_allidsthreshold;
char *li_directory;
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 0ab82948c..a5004be19 100644
--- a/ldap/servers/slapd/back-ldbm/index.c
+++ b/ldap/servers/slapd/back-ldbm/index.c
@@ -997,8 +997,6 @@ index_read_ext_allids(
}
if (pb) {
slapi_pblock_get(pb, SLAPI_SEARCH_IS_AND, &is_and);
- } else if (strcasecmp(type, LDBM_ANCESTORID_STR) == 0) {
- is_and = 1;
}
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 2a6e8cbb8..2b71cd4f7 100644
--- a/ldap/servers/slapd/back-ldbm/instance.c
+++ b/ldap/servers/slapd/back-ldbm/instance.c
@@ -16,7 +16,7 @@
/* Forward declarations */
static void ldbm_instance_destructor(void **arg);
-Slapi_Entry *ldbm_instance_init_config_entry(char *cn_val, char *v1, char *v2, char *v3, char *v4, char *mr, char *scanlimit);
+Slapi_Entry *ldbm_instance_init_config_entry(char *cn_val, char *v1, char *v2, char *v3, char *v4, char *mr);
/* Creates and initializes a new ldbm_instance structure.
@@ -126,7 +126,7 @@ done:
* Take a bunch of strings, and create a index config entry
*/
Slapi_Entry *
-ldbm_instance_init_config_entry(char *cn_val, char *val1, char *val2, char *val3, char *val4, char *mr, char *scanlimit)
+ldbm_instance_init_config_entry(char *cn_val, char *val1, char *val2, char *val3, char *val4, char *mr)
{
Slapi_Entry *e = slapi_entry_alloc();
struct berval *vals[2];
@@ -167,11 +167,6 @@ ldbm_instance_init_config_entry(char *cn_val, char *val1, char *val2, char *val3
slapi_entry_add_values(e, "nsMatchingRule", vals);
}
- if (scanlimit) {
- val.bv_val = scanlimit;
- val.bv_len = strlen(scanlimit);
- slapi_entry_add_values(e, "nsIndexIDListScanLimit", vals);
- }
return e;
}
@@ -184,60 +179,8 @@ ldbm_instance_create_default_indexes(backend *be)
{
Slapi_Entry *e;
ldbm_instance *inst = (ldbm_instance *)be->be_instance_info;
- struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private;
/* write the dse file only on the final index */
int flags = LDBM_INSTANCE_CONFIG_DONT_WRITE;
- char *ancestorid_indexes_limit = NULL;
- char *parentid_indexes_limit = NULL;
- struct attrinfo *ai = NULL;
- int index_already_configured = 0;
- struct index_idlistsizeinfo *iter;
- int cookie;
- int limit;
-
- ainfo_get(be, (char *)LDBM_ANCESTORID_STR, &ai);
- if (ai && ai->ai_idlistinfo) {
- iter = (struct index_idlistsizeinfo *)dl_get_first(ai->ai_idlistinfo, &cookie);
- if (iter) {
- limit = iter->ai_idlistsizelimit;
- slapi_log_err(SLAPI_LOG_BACKLDBM, "ldbm_instance_create_default_indexes",
- "set ancestorid limit to %d from attribute index\n",
- limit);
- } else {
- limit = li->li_system_allidsthreshold;
- slapi_log_err(SLAPI_LOG_BACKLDBM, "ldbm_instance_create_default_indexes",
- "set ancestorid limit to %d from default (fail to read limit)\n",
- limit);
- }
- ancestorid_indexes_limit = slapi_ch_smprintf("limit=%d type=eq flags=AND", limit);
- } else {
- ancestorid_indexes_limit = slapi_ch_smprintf("limit=%d type=eq flags=AND", li->li_system_allidsthreshold);
- slapi_log_err(SLAPI_LOG_BACKLDBM, "ldbm_instance_create_default_indexes",
- "set ancestorid limit to %d from default (no attribute or limit)\n",
- li->li_system_allidsthreshold);
- }
-
- ainfo_get(be, (char *)LDBM_PARENTID_STR, &ai);
- if (ai && ai->ai_idlistinfo) {
- iter = (struct index_idlistsizeinfo *)dl_get_first(ai->ai_idlistinfo, &cookie);
- if (iter) {
- limit = iter->ai_idlistsizelimit;
- slapi_log_err(SLAPI_LOG_BACKLDBM, "ldbm_instance_create_default_indexes",
- "set parentid limit to %d from attribute index\n",
- limit);
- } else {
- limit = li->li_system_allidsthreshold;
- slapi_log_err(SLAPI_LOG_BACKLDBM, "ldbm_instance_create_default_indexes",
- "set parentid limit to %d from default (fail to read limit)\n",
- limit);
- }
- parentid_indexes_limit = slapi_ch_smprintf("limit=%d type=eq flags=AND", limit);
- } else {
- parentid_indexes_limit = slapi_ch_smprintf("limit=%d type=eq flags=AND", li->li_system_allidsthreshold);
- slapi_log_err(SLAPI_LOG_BACKLDBM, "ldbm_instance_create_default_indexes",
- "set parentid limit to %d from default (no attribute or limit)\n",
- li->li_system_allidsthreshold);
- }
/*
* Always index (entrydn or entryrdn), parentid, objectclass,
@@ -245,48 +188,42 @@ ldbm_instance_create_default_indexes(backend *be)
* since they are used by some searches, replication and the
* ACL routines.
*/
- e = ldbm_instance_init_config_entry(LDBM_ENTRYRDN_STR, "subtree", 0, 0, 0, 0, 0);
+ e = ldbm_instance_init_config_entry(LDBM_ENTRYRDN_STR, "subtree", 0, 0, 0, 0);
ldbm_instance_config_add_index_entry(inst, e, flags);
slapi_entry_free(e);
- ainfo_get(be, (char *)LDBM_PARENTID_STR, &ai);
- /* Check if the attrinfo is actually for parentid, not a fallback to .default */
- index_already_configured = (ai != NULL && strcmp(ai->ai_type, LDBM_PARENTID_STR) == 0);
- 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(LDBM_PARENTID_STR, "eq", 0, 0, 0, "integerOrderingMatch");
+ ldbm_instance_config_add_index_entry(inst, e, flags);
+ slapi_entry_free(e);
- 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("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);
- 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);
- 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);
ldbm_instance_config_add_index_entry(inst, e, flags);
slapi_entry_free(e);
/* For MMR, we need this attribute (to replace use of dncomp in delete). */
- e = ldbm_instance_init_config_entry(ATTR_NSDS5_REPLCONFLICT, "eq", "pres", 0, 0, 0, 0);
+ e = ldbm_instance_init_config_entry(ATTR_NSDS5_REPLCONFLICT, "eq", "pres", 0, 0, 0);
ldbm_instance_config_add_index_entry(inst, e, flags);
slapi_entry_free(e);
/* write the dse file only on the final index */
- e = ldbm_instance_init_config_entry(SLAPI_ATTR_NSCP_ENTRYDN, "eq", 0, 0, 0, 0, 0);
+ e = ldbm_instance_init_config_entry(SLAPI_ATTR_NSCP_ENTRYDN, "eq", 0, 0, 0, 0);
ldbm_instance_config_add_index_entry(inst, e, flags);
slapi_entry_free(e);
/* ldbm_instance_config_add_index_entry(inst, 2, argv); */
- e = ldbm_instance_init_config_entry(LDBM_PSEUDO_ATTR_DEFAULT, "none", 0, 0, 0, 0, 0);
+ e = ldbm_instance_init_config_entry(LDBM_PSEUDO_ATTR_DEFAULT, "none", 0, 0, 0, 0);
attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
slapi_entry_free(e);
@@ -294,18 +231,9 @@ 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.
*/
- ainfo_get(be, (char *)LDBM_ANCESTORID_STR, &ai);
- /* Check if the attrinfo is actually for ancestorid, not a fallback to .default */
- index_already_configured = (ai != NULL && strcmp(ai->ai_type, LDBM_ANCESTORID_STR) == 0);
- 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);
- slapi_ch_free_string(&parentid_indexes_limit);
+ e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, "integerOrderingMatch");
+ attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
+ slapi_entry_free(e);
return 0;
}
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_config.c b/ldap/servers/slapd/back-ldbm/ldbm_config.c
index c24e3d766..6a2ce4c27 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_config.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_config.c
@@ -385,35 +385,6 @@ ldbm_config_allidsthreshold_set(void *arg, void *value, char *errorbuf __attribu
return retval;
}
-static void *
-ldbm_config_system_allidsthreshold_get(void *arg)
-{
- struct ldbminfo *li = (struct ldbminfo *)arg;
-
- return (void *)((uintptr_t)(li->li_system_allidsthreshold));
-}
-
-static int
-ldbm_config_system_allidsthreshold_set(void *arg, void *value, char *errorbuf __attribute__((unused)), int phase __attribute__((unused)), int apply)
-{
- struct ldbminfo *li = (struct ldbminfo *)arg;
- int retval = LDAP_SUCCESS;
- int val = (int)((uintptr_t)value);
-
- /* Do whatever we can to make sure the data is ok. */
-
- /* Catch attempts to configure a stupidly low ancestorid allidsthreshold */
- if ((val > -1) && (val < 5000)) {
- val = 5000;
- }
-
- if (apply) {
- li->li_system_allidsthreshold = val;
- }
-
- return retval;
-}
-
static void *
ldbm_config_pagedallidsthreshold_get(void *arg)
{
@@ -1094,7 +1065,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},
- {CONFIG_SYSTEMIDLISTSCANLIMIT, CONFIG_TYPE_INT, "5000", &ldbm_config_system_allidsthreshold_get, &ldbm_config_system_allidsthreshold_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE},
{CONFIG_DIRECTORY, CONFIG_TYPE_STRING, "", &ldbm_config_directory_get, &ldbm_config_directory_set, CONFIG_FLAG_ALWAYS_SHOW | CONFIG_FLAG_ALLOW_RUNNING_CHANGE | CONFIG_FLAG_SKIP_DEFAULT_SETTING},
{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 29a3426ab..e69bfeedf 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
#define CONFIG_RANGELOOKTHROUGHLIMIT "nsslapd-rangelookthroughlimit"
#define CONFIG_PAGEDLOOKTHROUGHLIMIT "nsslapd-pagedlookthroughlimit"
#define CONFIG_IDLISTSCANLIMIT "nsslapd-idlistscanlimit"
-#define CONFIG_SYSTEMIDLISTSCANLIMIT "nsslapd-systemidlistscanlimit"
#define CONFIG_PAGEDIDLISTSCANLIMIT "nsslapd-pagedidlistscanlimit"
#define CONFIG_DIRECTORY "nsslapd-directory"
#define CONFIG_MODE "nsslapd-mode"
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_index_config.c b/ldap/servers/slapd/back-ldbm/ldbm_index_config.c
index bae2a64b9..38e7368e1 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_index_config.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_index_config.c
@@ -384,14 +384,6 @@ ldbm_instance_config_add_index_entry(
}
}
- /* get nsIndexIDListScanLimit and its values, and add them */
- if (0 == slapi_entry_attr_find(e, "nsIndexIDListScanLimit", &attr)) {
- for (j = slapi_attr_first_value(attr, &sval); j != -1; j = slapi_attr_next_value(attr, j, &sval)) {
- attrValue = slapi_value_get_berval(sval);
- eBuf = PR_sprintf_append(eBuf, "nsIndexIDListScanLimit: %s\n", attrValue->bv_val);
- }
- }
-
ldbm_config_add_dse_entry(li, eBuf, flags);
if (eBuf) {
PR_smprintf_free(eBuf);
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
index 274d45abe..f3dbe7c92 100644
--- a/src/lib389/lib389/backend.py
+++ b/src/lib389/lib389/backend.py
@@ -645,10 +645,11 @@ class Backend(DSLdapObject):
indexes = self.get_indexes()
# 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.
+ # Only parentid has a DSE config entry (for the integerOrderingMatch rule).
expected_system_indexes = {
- 'entryrdn': {'types': ['subtree'], 'matching_rule': None},
- 'parentid': {'types': ['eq'], 'matching_rule': 'integerOrderingMatch', 'scanlimit': 'limit=5000 type=eq flags=AND'},
- 'ancestorid': {'types': ['eq'], 'matching_rule': 'integerOrderingMatch', 'scanlimit': 'limit=5000 type=eq flags=AND'},
+ 'parentid': {'types': ['eq'], 'matching_rule': 'integerOrderingMatch'},
'objectClass': {'types': ['eq'], 'matching_rule': None},
'aci': {'types': ['pres'], 'matching_rule': None},
'nscpEntryDN': {'types': ['eq'], 'matching_rule': None},
@@ -705,17 +706,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}"
- if expected_config.get('matching_rule'):
+ if expected_config['matching_rule']:
cmd += f" --matching-rule {expected_config['matching_rule']}"
- if expected_config.get('scanlimit'):
- cmd += f" --add-scanlimit \"{expected_config['scanlimit']}\""
remediation_commands.append(cmd)
reindex_attrs.add(attr_name) # New index needs reindexing
else:
# Index exists, check configuration
actual_types = index.get_attr_vals_utf8('nsIndexType') or []
actual_mrs = index.get_attr_vals_utf8('nsMatchingRule') or []
- actual_scanlimit = index.get_attr_vals_utf8('nsIndexIDListScanLimit') or []
# Normalize to lowercase for comparison
actual_types = [t.lower() for t in actual_types]
@@ -730,31 +728,16 @@ class Backend(DSLdapObject):
remediation_commands.append(cmd)
reindex_attrs.add(attr_name)
- # Check matching rules and scanlimit together to generate a single combined command
+ # Check matching rules
expected_mr = expected_config.get('matching_rule')
- expected_scanlimit = expected_config.get('scanlimit')
-
- missing_mr = False
if expected_mr:
actual_mrs_lower = [mr.lower() for mr in actual_mrs]
if expected_mr.lower() not in actual_mrs_lower:
discrepancies.append(f"Index {attr_name} missing matching rule: {expected_mr}")
- missing_mr = True
-
- missing_scanlimit = False
- if expected_scanlimit and (len(actual_scanlimit) == 0):
- discrepancies.append(f"Index {attr_name} missing fine grain definition of IDs limit: {expected_scanlimit}")
- missing_scanlimit = True
-
- # Generate a single combined command for all missing items
- if missing_mr or missing_scanlimit:
- cmd = f"dsconf YOUR_INSTANCE backend index set {bename} --attr {attr_name}"
- if missing_mr:
- cmd += f" --add-mr {expected_mr}"
- if missing_scanlimit:
- cmd += f" --add-scanlimit \"{expected_scanlimit}\""
- remediation_commands.append(cmd)
- reindex_attrs.add(attr_name)
+ # Add the missing matching rule
+ cmd = f"dsconf YOUR_INSTANCE backend index set {bename} --attr {attr_name} --add-mr {expected_mr}"
+ remediation_commands.append(cmd)
+ reindex_attrs.add(attr_name)
except Exception as e:
self._log.debug(f"_lint_system_indexes - Error checking index {attr_name}: {e}")
@@ -993,13 +976,12 @@ class Backend(DSLdapObject):
return
raise ValueError("Can not delete index because it does not exist")
- def add_index(self, attr_name, types, matching_rules=None, idlistscanlimit=None, reindex=False):
+ def add_index(self, attr_name, types, matching_rules=None, reindex=False):
""" Add an index.
:param attr_name - name of the attribute to index
:param types - a List of index types(eq, pres, sub, approx)
:param matching_rules - a List of matching rules for the index
- :param idlistscanlimit - a List of fine grain definitions for scanning limit
:param reindex - If set to True then index the attribute after creating it.
"""
@@ -1029,15 +1011,6 @@ class Backend(DSLdapObject):
# Only add if there are actually rules present in the list.
if len(mrs) > 0:
props['nsMatchingRule'] = mrs
-
- if idlistscanlimit is not None:
- scanlimits = []
- for scanlimit in idlistscanlimit:
- scanlimits.append(scanlimit)
- # Only add if there are actually limits in the list.
- if len(scanlimits) > 0:
- props['nsIndexIDListScanLimit'] = scanlimits
-
new_index.create(properties=props, basedn="cn=index," + self._dn)
if reindex:
@@ -1349,7 +1322,6 @@ class DatabaseConfig(DSLdapObject):
'nsslapd-lookthroughlimit',
'nsslapd-mode',
'nsslapd-idlistscanlimit',
- 'nsslapd-systemidlistscanlimit',
'nsslapd-directory',
'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 9772e39d4..68efa795c 100644
--- a/src/lib389/lib389/cli_conf/backend.py
+++ b/src/lib389/lib389/cli_conf/backend.py
@@ -39,7 +39,6 @@ arg_to_attr = {
'mode': 'nsslapd-mode',
'state': 'nsslapd-state',
'idlistscanlimit': 'nsslapd-idlistscanlimit',
- 'systemidlistscanlimit': 'nsslapd-systemidlistscanlimit',
'directory': 'nsslapd-directory',
'dbcachesize': 'nsslapd-dbcachesize',
'logdirectory': 'nsslapd-db-logdirectory',
@@ -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')
- if args.replace_scanlimit is not None:
- for replace_scanlimit in args.replace_scanlimit:
- index.replace('nsIndexIDListScanLimit', replace_scanlimit)
-
- if args.add_scanlimit is not None:
- for add_scanlimit in args.add_scanlimit:
- index.add('nsIndexIDListScanLimit', add_scanlimit)
-
- if args.del_scanlimit is not None:
- for del_scanlimit in args.del_scanlimit:
- try:
- index.remove('nsIndexIDListScanLimit', del_scanlimit)
- except ldap.NO_SUCH_ATTRIBUTE:
- raise ValueError('Can not delete a fine grain limit definition because it does not exist')
-
if args.reindex:
be.reindex(attrs=[args.attr])
log.info("Index successfully updated")
@@ -963,9 +947,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')
- edit_index_parser.add_argument('--add-scanlimit', action='append', help='Adds a fine grain limit definiton to the index')
- edit_index_parser.add_argument('--replace-scanlimit', action='append', help='Replaces a fine grain limit definiton to the index')
- edit_index_parser.add_argument('--del-scanlimit', action='append', help='Removes a fine grain limit definiton to the index')
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')
@@ -1092,7 +1073,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')
- set_db_config_parser.add_argument('--systemidlistscanlimit', help='Specifies the number of entry IDs that are fetch from ancestorid/parentid indexes')
set_db_config_parser.add_argument('--directory', help='Specifies absolute path to database instance')
set_db_config_parser.add_argument('--dbcachesize', help='Specifies the database index cache size in bytes')
set_db_config_parser.add_argument('--logdirectory', help='Specifies the path to the directory that contains the database transaction logs')
--
2.52.0

View File

@ -0,0 +1,212 @@
From 4c44e4c522afc0f5401754f29a47645889e21aca 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
nsIndexIDListScanLimit from parentid
Description:
Add `upgrade_remove_index_scanlimit()` function that removes the
nsIndexIDListScanLimit attribute from parentid index configuration
if present.
This attribute was incorrectly added by a previous version and can
cause issues with index configuration. The upgrade function runs
automatically on server startup and removes the attribute if found.
Relates: https://github.com/389ds/389-ds-base/issues/7223
Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
---
.../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 140845a33..aea88e0e2 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -453,6 +453,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)
+def test_upgrade_removes_parentid_scanlimit(topology_st):
+ """Check if upgrade function removes nsIndexIDListScanLimit from parentid index
+
+ :id: 2808886e-c1c1-441d-b3a3-299c4ef1ab4a
+ :setup: Standalone instance
+ :steps:
+ 1. Create DS instance
+ 2. Stop the server
+ 3. Use DSEldif to add nsIndexIDListScanLimit to parentid index
+ 4. Start the server (triggers upgrade)
+ 5. Verify nsIndexIDListScanLimit is removed from parentid index
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. nsIndexIDListScanLimit is no longer present
+ """
+ from lib389.dseldif import DSEldif
+
+ standalone = topology_st.standalone
+ PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
+ SCANLIMIT_VALUE = "limit=5000 type=eq flags=AND"
+
+ log.info("Stop the server")
+ standalone.stop()
+
+ log.info("Add nsIndexIDListScanLimit to parentid index using DSEldif")
+ dse_ldif = DSEldif(standalone)
+ dse_ldif.add(PARENTID_DN, "nsIndexIDListScanLimit", SCANLIMIT_VALUE)
+
+ # Verify it was added
+ scanlimit = dse_ldif.get(PARENTID_DN, "nsIndexIDListScanLimit")
+ assert scanlimit is not None, "Failed to add nsIndexIDListScanLimit"
+ log.info(f"Added nsIndexIDListScanLimit: {scanlimit}")
+
+ log.info("Start the server (triggers upgrade)")
+ standalone.start()
+
+ log.info("Verify nsIndexIDListScanLimit was removed by upgrade")
+ # Check via LDAP - the upgrade should have removed it
+ parentid_index = Index(standalone, PARENTID_DN)
+ scanlimit_after = parentid_index.get_attr_vals_utf8("nsIndexIDListScanLimit")
+ log.info(f"nsIndexIDListScanLimit after upgrade: {scanlimit_after}")
+
+ # The upgrade function should have removed nsIndexIDListScanLimit
+ assert not scanlimit_after, \
+ f"nsIndexIDListScanLimit should have been removed but found: {scanlimit_after}"
+
+ log.info("Upgrade successfully removed nsIndexIDListScanLimit from parentid index")
+
+
if __name__ == "__main__":
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
index b02e37ed6..dcd16940b 100644
--- a/ldap/servers/slapd/upgrade.c
+++ b/ldap/servers/slapd/upgrade.c
@@ -330,6 +330,107 @@ upgrade_remove_subtree_rename(void)
return UPGRADE_SUCCESS;
}
+/*
+ * Remove nsIndexIDListScanLimit from parentid index configuration.
+ *
+ * This attribute was incorrectly added by a previous version and can
+ * cause issues with index configuration. Remove it if present.
+ */
+static upgrade_status
+upgrade_remove_index_scanlimit(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", 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_dn = slapi_entry_get_dn_const(backends[be_idx]);
+ const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
+ if (!be_dn || !be_name) {
+ continue;
+ }
+
+ 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,%s",
+ attr_name, be_dn);
+ char *idx_filter = "(objectclass=nsIndex)";
+
+ 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]) {
+ /* Check if nsIndexIDListScanLimit is present */
+ if (slapi_entry_attr_get_ref(idx_entries[0], "nsIndexIDListScanLimit") != NULL) {
+ /* Remove nsIndexIDListScanLimit */
+ Slapi_PBlock *mod_pb = slapi_pblock_new();
+ Slapi_Mods smods;
+ int rc;
+
+ slapi_mods_init(&smods, 1);
+ slapi_mods_add(&smods, LDAP_MOD_DELETE, "nsIndexIDListScanLimit", 0, NULL);
+
+ slapi_modify_internal_set_pb(
+ mod_pb, idx_dn,
+ slapi_mods_get_ldapmods_byref(&smods),
+ NULL, NULL,
+ plugin_get_default_component_id(), 0);
+ slapi_modify_internal_pb(mod_pb);
+ slapi_pblock_get(mod_pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
+
+ if (rc == LDAP_SUCCESS) {
+ slapi_log_err(SLAPI_LOG_NOTICE, "upgrade_remove_index_scanlimit",
+ "Removed 'nsIndexIDListScanLimit' from index '%s' in backend '%s'\n",
+ attr_name, be_name);
+ } else if (rc != LDAP_NO_SUCH_ATTRIBUTE) {
+ slapi_log_err(SLAPI_LOG_ERR, "upgrade_remove_index_scanlimit",
+ "Failed to remove 'nsIndexIDListScanLimit' from index '%s' in backend '%s': error %d\n",
+ attr_name, be_name, rc);
+ }
+
+ slapi_mods_done(&smods);
+ slapi_pblock_destroy(mod_pb);
+ }
+ }
+
+ 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;
+}
+
/*
* Check if parentid/ancestorid indexes are missing the integerOrderingMatch
* matching rule.
@@ -649,6 +750,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;
}
--
2.52.0

View File

@ -0,0 +1,313 @@
From 41670301ccad5558296a3380a4974f7c0d4baede 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 ancestorid index
config entry
Description:
Add `upgrade_remove_ancestorid_index_config()` function that removes:
* ancestorid from `cn=default indexes`
* ancestorid index config entries from each backend's `cn=index`
Also remove ancestorid index configuration from template-dse.ldif.
Relates: https://github.com/389ds/389-ds-base/issues/7223
Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
---
.../healthcheck/health_system_indexes_test.py | 85 +++++++++++
ldap/ldif/template-dse.ldif.in | 8 --
ldap/servers/slapd/upgrade.c | 133 +++++++++++++++++-
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 aea88e0e2..eb727b902 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -504,6 +504,91 @@ def test_upgrade_removes_parentid_scanlimit(topology_st):
log.info("Upgrade successfully removed nsIndexIDListScanLimit from parentid index")
+ # Verify idempotency - restart again and ensure no errors
+ log.info("Restart server again to verify idempotency (no errors on second run)")
+ standalone.restart()
+ # Verify the attribute is still absent
+ scanlimit_after_second = parentid_index.get_attr_vals_utf8("nsIndexIDListScanLimit")
+ assert not scanlimit_after_second, \
+ f"nsIndexIDListScanLimit should still be absent after second restart but found: {scanlimit_after_second}"
+ log.info("Idempotency verified - no issues on second restart")
+
+
+def test_upgrade_removes_ancestorid_index_config(topology_st):
+ """Check if upgrade function removes ancestorid index config entry
+
+ :id: 3f3d6e9b-75ac-4f0d-b2ce-7204e6eacd0a
+ :setup: Standalone instance
+ :steps:
+ 1. Create DS instance
+ 2. Stop the server
+ 3. Use DSEldif to add an ancestorid index config entry
+ 4. Start the server (triggers upgrade)
+ 5. Verify ancestorid index config entry is removed
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. ancestorid index config entry is no longer present
+ """
+ from lib389.dseldif import DSEldif
+
+ standalone = topology_st.standalone
+ ANCESTORID_DN = "cn=ancestorid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
+
+ log.info("Stop the server")
+ standalone.stop()
+
+ log.info("Add ancestorid index config entry using DSEldif")
+ dse_ldif = DSEldif(standalone)
+
+ # Create a fake ancestorid index entry
+ ancestorid_entry = [
+ "dn: {}\n".format(ANCESTORID_DN),
+ "objectClass: top\n",
+ "objectClass: nsIndex\n",
+ "cn: ancestorid\n",
+ "nsSystemIndex: true\n",
+ "nsIndexType: eq\n",
+ "nsMatchingRule: integerOrderingMatch\n",
+ "\n"
+ ]
+ dse_ldif.add_entry(ancestorid_entry)
+
+ # Verify it was added by re-reading dse.ldif
+ dse_ldif2 = DSEldif(standalone)
+ cn_value = dse_ldif2.get(ANCESTORID_DN, "cn")
+ assert cn_value is not None, "Failed to add ancestorid index config entry"
+ log.info(f"Added ancestorid index entry with cn: {cn_value}")
+
+ log.info("Start the server (triggers upgrade)")
+ standalone.start()
+
+ log.info("Verify ancestorid index config entry was removed by upgrade")
+ # Check via LDAP - the upgrade should have removed the entry
+ try:
+ ancestorid_index = Index(standalone, ANCESTORID_DN)
+ # If we can get the entry, it wasn't removed - this is a failure
+ cn_after = ancestorid_index.get_attr_vals_utf8("cn")
+ assert False, f"ancestorid index config entry should have been removed but still exists: {cn_after}"
+ except Exception as e:
+ # Entry should not exist - this is expected
+ log.info(f"ancestorid index config entry correctly removed (got exception: {e})")
+
+ log.info("Upgrade successfully removed ancestorid index config entry")
+
+ # Verify idempotency - restart again and ensure no errors
+ log.info("Restart server again to verify idempotency (no errors on second run)")
+ standalone.restart()
+ # Verify the entry is still absent
+ try:
+ ancestorid_index = Index(standalone, ANCESTORID_DN)
+ cn_after_second = ancestorid_index.get_attr_vals_utf8("cn")
+ assert False, f"ancestorid index config entry should still be absent after second restart but found: {cn_after_second}"
+ except Exception as e:
+ log.info(f"Idempotency verified - ancestorid still absent after second restart (got exception: {e})")
+
if __name__ == "__main__":
# Run isolated
diff --git a/ldap/ldif/template-dse.ldif.in b/ldap/ldif/template-dse.ldif.in
index bb8c71cd9..b6ab6f6c6 100644
--- a/ldap/ldif/template-dse.ldif.in
+++ b/ldap/ldif/template-dse.ldif.in
@@ -998,14 +998,6 @@ cn: aci
nssystemindex: true
nsindextype: pres
-dn: cn=ancestorid,cn=default indexes, cn=config,cn=ldbm database,cn=plugins,cn=config
-objectclass: top
-objectclass: nsIndex
-cn: ancestorid
-nssystemindex: true
-nsindextype: eq
-nsmatchingrule: integerOrderingMatch
-
dn: cn=cn,cn=default indexes, cn=config,cn=ldbm database,cn=plugins,cn=config
objectclass: top
objectclass: nsIndex
diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
index dcd16940b..6b1b012da 100644
--- a/ldap/servers/slapd/upgrade.c
+++ b/ldap/servers/slapd/upgrade.c
@@ -431,6 +431,126 @@ upgrade_remove_index_scanlimit(void)
return uresult;
}
+/*
+ * Remove ancestorid index configuration entry if present.
+ *
+ * The ancestorid index is special - it has no corresponding attribute type
+ * and should not have a DSE config entry. If an entry exists, remove it.
+ *
+ * This function removes:
+ * 1. The ancestorid entry from cn=default indexes (to prevent re-creation on startup)
+ * 2. The ancestorid entry from each backend's cn=index (if it exists)
+ */
+static upgrade_status
+upgrade_remove_ancestorid_index_config(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)";
+ upgrade_status uresult = UPGRADE_SUCCESS;
+ int rc;
+
+ /*
+ * First, remove ancestorid from cn=default indexes to prevent
+ * ldbm_instance_create_default_user_indexes() from re-creating it.
+ */
+ {
+ Slapi_PBlock *def_pb = slapi_pblock_new();
+ char *def_idx_dn = slapi_create_dn_string(
+ "cn=ancestorid,cn=default indexes,cn=config,%s", be_base_dn);
+
+ if (def_idx_dn) {
+ slapi_delete_internal_set_pb(
+ def_pb, def_idx_dn, NULL, NULL,
+ plugin_get_default_component_id(), 0);
+ slapi_delete_internal_pb(def_pb);
+ slapi_pblock_get(def_pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
+
+ if (rc == LDAP_SUCCESS) {
+ slapi_log_err(SLAPI_LOG_NOTICE, "upgrade_remove_ancestorid_index_config",
+ "Removed 'ancestorid' from default indexes.\n");
+ } else if (rc != LDAP_NO_SUCH_OBJECT) {
+ slapi_log_err(SLAPI_LOG_ERR, "upgrade_remove_ancestorid_index_config",
+ "Failed to remove 'ancestorid' from default indexes: error %d\n", rc);
+ }
+
+ slapi_ch_free_string(&def_idx_dn);
+ }
+ slapi_pblock_destroy(def_pb);
+ }
+
+ /* 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_dn = slapi_entry_get_dn_const(backends[be_idx]);
+ const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
+ if (!be_dn || !be_name) {
+ continue;
+ }
+
+ struct slapi_pblock *idx_pb = slapi_pblock_new();
+ Slapi_Entry **idx_entries = NULL;
+ char *idx_dn = slapi_create_dn_string("cn=ancestorid,cn=index,%s",
+ be_dn);
+ char *idx_filter = "(objectclass=nsIndex)";
+
+ 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]) {
+ /* ancestorid index entry exists - delete it */
+ Slapi_PBlock *del_pb = slapi_pblock_new();
+
+ slapi_delete_internal_set_pb(
+ del_pb, idx_dn, NULL, NULL,
+ plugin_get_default_component_id(), 0);
+ slapi_delete_internal_pb(del_pb);
+ slapi_pblock_get(del_pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
+
+ if (rc == LDAP_SUCCESS) {
+ slapi_log_err(SLAPI_LOG_NOTICE, "upgrade_remove_ancestorid_index_config",
+ "Removed 'ancestorid' index config entry in backend '%s'.\n",
+ be_name);
+ } else if (rc != LDAP_NO_SUCH_OBJECT) {
+ slapi_log_err(SLAPI_LOG_ERR, "upgrade_remove_ancestorid_index_config",
+ "Failed to remove 'ancestorid' index config entry in backend '%s': error %d\n",
+ be_name, rc);
+ }
+
+ slapi_pblock_destroy(del_pb);
+ }
+
+ 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;
+}
+
/*
* Check if parentid/ancestorid indexes are missing the integerOrderingMatch
* matching rule.
@@ -445,7 +565,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)";
- const char *attrs_to_check[] = {"parentid", "ancestorid", NULL};
+ const char *attrs_to_check[] = {"parentid", NULL};
upgrade_status uresult = UPGRADE_SUCCESS;
/* Search for all backend instances */
@@ -459,8 +579,9 @@ upgrade_check_id_index_matching_rule(void)
if (backends) {
for (size_t be_idx = 0; backends[be_idx] != NULL; be_idx++) {
+ const char *be_dn = slapi_entry_get_dn_const(backends[be_idx]);
const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
- if (!be_name) {
+ if (!be_dn || !be_name) {
continue;
}
@@ -469,8 +590,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;
- char *idx_dn = slapi_create_dn_string("cn=%s,cn=index,cn=%s,%s",
- attr_name, be_name, be_base_dn);
+ char *idx_dn = slapi_create_dn_string("cn=%s,cn=index,%s",
+ attr_name, be_dn);
char *idx_filter = "(objectclass=nsIndex)";
PRBool has_matching_rule = PR_FALSE;
@@ -754,6 +875,10 @@ upgrade_server(void)
return UPGRADE_FAILURE;
}
+ if (upgrade_remove_ancestorid_index_config() != UPGRADE_SUCCESS) {
+ return UPGRADE_FAILURE;
+ }
+
if (upgrade_check_id_index_matching_rule() != UPGRADE_SUCCESS) {
return UPGRADE_FAILURE;
}
--
2.52.0

View File

@ -0,0 +1,300 @@
From a260b50aa0c8e6c5b8b3fd0b164e9bbc4a15983f 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
backend startup
Description:
Add `ldbm_instance_check_index_config()` function that checks on-disk
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, @droideck (Thanks!)
---
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 2b71cd4f7..17bfc09a0 100644
--- a/ldap/servers/slapd/back-ldbm/instance.c
+++ b/ldap/servers/slapd/back-ldbm/instance.c
@@ -239,6 +239,266 @@ ldbm_instance_create_default_indexes(backend *be)
}
+/*
+ * Check if an index has integerOrderingMatch configured in DSE.
+ *
+ * This function performs an internal LDAP search to check if the index
+ * configuration entry has nsMatchingRule: integerOrderingMatch.
+ *
+ * Parameters:
+ * inst_name - backend instance name (e.g., "userRoot")
+ * index_name - name of the index to check (e.g., "parentid", "ancestorid")
+ *
+ * Returns:
+ * PR_TRUE if integerOrderingMatch is configured
+ * PR_FALSE if not configured or index entry doesn't exist
+ */
+static PRBool
+ldbm_instance_index_has_int_order_in_dse(const char *inst_name, const char *index_name)
+{
+ Slapi_PBlock *pb = NULL;
+ Slapi_Entry **entries = NULL;
+ char *idx_dn = NULL;
+ PRBool has_int_order = PR_FALSE;
+
+ idx_dn = slapi_create_dn_string("cn=%s,cn=index,cn=%s,cn=ldbm database,cn=plugins,cn=config",
+ index_name, inst_name);
+ if (idx_dn == NULL) {
+ return PR_FALSE;
+ }
+
+ pb = slapi_pblock_new();
+ slapi_search_internal_set_pb(pb, idx_dn, LDAP_SCOPE_BASE,
+ "(objectclass=nsIndex)", NULL, 0, NULL, NULL,
+ plugin_get_default_component_id(), 0);
+ slapi_search_internal_pb(pb);
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &entries);
+
+ if (entries && entries[0]) {
+ Slapi_Attr *mr_attr = NULL;
+ if (slapi_entry_attr_find(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_int_order = PR_TRUE;
+ break;
+ }
+ }
+ }
+ }
+
+ slapi_ch_free_string(&idx_dn);
+ slapi_free_search_results_internal(pb);
+ slapi_pblock_destroy(pb);
+
+ return has_int_order;
+}
+
+/*
+ * Check a system index for ordering mismatch between config and on-disk data.
+ *
+ * This function compares what's configured in DSE (nsMatchingRule) with
+ * what's actually on disk. A mismatch can occur in two scenarios:
+ * 1. Ordering rule is configured but disk has lexicographic order
+ * (rule was added after index was created)
+ * 2. No ordering rule configured but disk has integer order
+ * (rule was removed after index was created with it)
+ *
+ * This function reads the first keys from the specified index and checks
+ * if they are stored in lexicographic order (string: "1" < "10" < "2") or
+ * integer order (numeric: "1" < "2" < "10").
+ *
+ * Parameters:
+ * be - backend
+ * index_name - name of the index to check (e.g., "parentid", "ancestorid")
+ *
+ */
+static void
+ldbm_instance_check_index_config(backend *be, const char *index_name)
+{
+ ldbm_instance *inst = (ldbm_instance *)be->be_instance_info;
+ struct attrinfo *ai = NULL;
+ 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 */
+ ID prev_id = 0;
+ int key_count = 0;
+ PRBool first_key = PR_TRUE;
+ PRBool found_ordering_evidence = PR_FALSE;
+
+ slapi_log_err(SLAPI_LOG_DEBUG, "ldbm_instance_check_index_config",
+ "Backend '%s': checking %s index ordering...\n",
+ inst->inst_name, index_name);
+
+ /* Check if integerOrderingMatch is configured in DSE */
+ config_has_int_order = ldbm_instance_index_has_int_order_in_dse(inst->inst_name, index_name);
+
+ /* Get attrinfo for the index */
+ ainfo_get(be, (char *)index_name, &ai);
+ if (ai == NULL || strcmp(ai->ai_type, index_name) != 0) {
+ /* No index config found */
+ slapi_log_err(SLAPI_LOG_DEBUG, "ldbm_instance_check_index_config",
+ "Backend '%s': no %s attrinfo found, skipping check\n",
+ inst->inst_name, index_name);
+ return;
+ }
+
+ /* Open the index file */
+ ret = dblayer_get_index_file(be, ai, &db, 0);
+ if (ret != 0 || db == NULL) {
+ /* Index file doesn't exist or can't be opened - this is fine for new instances */
+ slapi_log_err(SLAPI_LOG_DEBUG, "ldbm_instance_check_index_config",
+ "Backend '%s': could not open %s index file (ret=%d), skipping order check\n",
+ inst->inst_name, index_name, ret);
+ return;
+ }
+
+ /* Create a cursor to read keys */
+ 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",
+ inst->inst_name, index_name, ret);
+ dblayer_release_index_file(be, ai, db);
+ return;
+ }
+
+ dblayer_value_init(be, &key);
+ dblayer_value_init(be, &data);
+
+ /*
+ * Read up to 100 unique keys and check their ordering.
+ * With lexicographic ordering: "1" < "10" < "100" < "2" < "20" < "3"
+ * With integer ordering: "1" < "2" < "3" < "10" < "20" < "100"
+ *
+ * If we find a case where prev_id > current_id (numerically), but the
+ * keys are still in order (lexicographically), then the index uses
+ * lexicographic ordering.
+ */
+ while (key_count < 100) {
+ ID current_id;
+
+ 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 */
+ }
+
+ /* Skip non-equality keys */
+ if (key.size < 2 || *(char *)key.data != EQ_PREFIX) {
+ continue;
+ }
+
+ /* Parse the ID from the key (format: "=<id>") */
+ current_id = (ID)strtoul((char *)key.data + 1, NULL, 10);
+ if (current_id == 0) {
+ continue; /* Invalid ID, skip */
+ }
+
+ key_count++;
+
+ if (prev_id != 0) {
+ /*
+ * Check ordering: if prev_id > current_id numerically,
+ * but we got this key after prev in DB order, then
+ * the index is using lexicographic ordering.
+ *
+ * Example: if we see "10" followed by "2", that's lexicographic
+ * because "10" < "2" as strings, but 10 > 2 as integers.
+ */
+ if (prev_id > current_id) {
+ /* Found evidence of lexicographic ordering */
+ disk_has_int_order = PR_FALSE;
+ found_ordering_evidence = PR_TRUE;
+ break;
+ } else if (prev_id < current_id) {
+ /*
+ * This is consistent with integer ordering, but we need
+ * to find a case that proves lexicographic ordering.
+ * For example, seeing "1" followed by "2" is ambiguous,
+ * but seeing "1" followed by "10" (not "2") proves lexicographic.
+ *
+ * A definitive test: if we see an ID followed by a smaller
+ * ID, that's lexicographic. If all IDs are strictly increasing,
+ * it could be either (or the index only has sequential IDs).
+ */
+ found_ordering_evidence = PR_TRUE;
+ }
+ }
+ prev_id = current_id;
+ }
+
+ /* Close the cursor and free values */
+ 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);
+
+ /*
+ * Report findings and check for config/disk mismatch.
+ * Log an error if there's a discrepancy between what's configured
+ * in DSE and what's actually on disk.
+ */
+ if (!found_ordering_evidence) {
+ slapi_log_err(SLAPI_LOG_DEBUG, "ldbm_instance_check_index_config",
+ "Backend '%s': %s index ordering check - "
+ "could not determine on-disk ordering (index may be empty or have sequential IDs only). "
+ "Config has integerOrderingMatch: %s\n",
+ inst->inst_name, index_name, config_has_int_order ? "yes" : "no");
+ } else if (config_has_int_order && !disk_has_int_order) {
+ /* Config expects integer ordering, but disk has lexicographic - MISMATCH */
+ slapi_log_err(SLAPI_LOG_ERR, "ldbm_instance_check_index_config",
+ "Backend '%s': MISMATCH - %s index has integerOrderingMatch configured, "
+ "but on-disk data uses lexicographic ordering. "
+ "This will cause searches to return incorrect or incomplete results. "
+ "Please reindex the %s attribute: "
+ "dsconf <instance> backend index reindex --attr %s %s\n",
+ inst->inst_name, index_name, index_name, index_name, inst->inst_name);
+ } else if (!config_has_int_order && disk_has_int_order) {
+ /* Config expects lexicographic ordering, but disk has integer - MISMATCH */
+ slapi_log_err(SLAPI_LOG_ERR, "ldbm_instance_check_index_config",
+ "Backend '%s': MISMATCH - %s index does not have integerOrderingMatch configured, "
+ "but on-disk data uses integer ordering. "
+ "This will cause searches to return incorrect or incomplete results. "
+ "Please reindex the %s attribute: "
+ "dsconf <instance> backend index reindex --attr %s %s\n",
+ inst->inst_name, index_name, index_name, index_name, inst->inst_name);
+ } else {
+ /* Config and disk ordering match - no action needed */
+ slapi_log_err(SLAPI_LOG_DEBUG, "ldbm_instance_check_index_config",
+ "Backend '%s': %s index ordering check passed - "
+ "config has integerOrderingMatch: %s, on-disk data matches.\n",
+ inst->inst_name, index_name, config_has_int_order ? "yes" : "no");
+ }
+}
+
+/*
+ * Check system indexes for ordering mismatches.
+ * If a mismatch is detected, log an error advising the administrator
+ * to reindex the affected attribute.
+ *
+ * Note: We only check parentid here. The ancestorid index is a special
+ * system index that has no DSE config entry - its ordering is hardcoded
+ * in ldbm_instance_init_config_entry() and cannot be changed by users.
+ */
+static void
+ldbm_instance_check_indexes(backend *be)
+{
+ /* Check parentid index */
+ ldbm_instance_check_index_config(be, LDBM_PARENTID_STR);
+}
+
/* Starts a backend instance */
int
ldbm_instance_start(backend *be)
@@ -308,6 +568,8 @@ ldbm_instance_startall(struct ldbminfo *li)
ldbm_instance_register_modify_callback(inst);
vlv_init(inst);
slapi_mtn_be_started(inst->inst_be);
+ /* Check index configuration for potential issues */
+ ldbm_instance_check_indexes(inst->inst_be);
}
if (slapi_exist_referral(inst->inst_be)) {
slapi_be_set_flag(inst->inst_be, SLAPI_BE_FLAG_CONTAINS_REFERRAL);
--
2.52.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
From 3ebb30a65e2b40610301e5feedda0408ac9f3631 Mon Sep 17 00:00:00 2001
From: James Chapman <jachapma@redhat.com>
Date: Tue, 10 Feb 2026 10:35:48 +0000
Subject: [PATCH] Issue 7230 - Regression in healtcheck NssCheck (#7235)
Description:
Dynamic Certificate lib389 updadates modified get_cert_details() to
return a dict instead of tuple format. _lint_certificate_expiration() and
tls.list_cas() still assumes tuple style access.
Fix:
Update method to use dict key.
Fixes: https://github.com/389ds/389-ds-base/issues/7230
Co-authored-by: @flo-renaud
Reviewed by: @droideck (Thank you)
---
src/lib389/lib389/cli_ctl/tls.py | 4 ++--
src/lib389/lib389/nss_ssl.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/lib389/lib389/cli_ctl/tls.py b/src/lib389/lib389/cli_ctl/tls.py
index 8834f5758..91f1f3a59 100644
--- a/src/lib389/lib389/cli_ctl/tls.py
+++ b/src/lib389/lib389/cli_ctl/tls.py
@@ -25,9 +25,9 @@ def list_client_cas(inst, log, args):
def list_cas(inst, log, args):
tls = NssSsl(dirsrv=inst)
- # This turns an array of [('CA', 'C,,')]
+ # Returns a list of cert dicts, eg {'cn': 'nickname', etc}
for c in tls.list_ca_certs():
- log.info(c[0])
+ log.info(c['cn'])
def show_cert(inst, log, args):
diff --git a/src/lib389/lib389/nss_ssl.py b/src/lib389/lib389/nss_ssl.py
index 764434166..fae65d19c 100644
--- a/src/lib389/lib389/nss_ssl.py
+++ b/src/lib389/lib389/nss_ssl.py
@@ -91,13 +91,13 @@ class NssSsl(DSLint):
if diff_date < timedelta(days=0):
# Expired
report = copy.deepcopy(DSCERTLE0002)
- report['detail'] = report['detail'].replace('CERT', cert[0])
+ report['detail'] = report['detail'].replace('CERT', cert['cn'])
report['check'] = f'tls:certificate_expiration'
yield report
elif diff_date < timedelta(days=30):
# Expiring within 30 days
report = copy.deepcopy(DSCERTLE0001)
- report['detail'] = report['detail'].replace('CERT', cert[0])
+ report['detail'] = report['detail'].replace('CERT', cert['cn'])
report['check'] = f'tls:certificate_expiration'
yield report
--
2.52.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,97 @@
From ae4a39474df08d56064453f0fb6c2272e6c3dc8b Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Fri, 6 Feb 2026 15:13:30 -0500
Subject: [PATCH] Issue 7221 - CI tests - fix some flaky tests
Description:
Try to harden some of the flaky tests with sleeps and more relaxed contraints
Relates: https://github.com/389ds/389-ds-base/issues/7221
Reviewed by: spichugi(Thanks!)
---
dirsrvtests/tests/suites/acl/acivattr_test.py | 7 +++++--
dirsrvtests/tests/suites/import/regression_test.py | 2 +-
.../suites/replication/wait_for_async_feature_test.py | 2 +-
dirsrvtests/tests/suites/retrocl/basic_test.py | 2 +-
4 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/dirsrvtests/tests/suites/acl/acivattr_test.py b/dirsrvtests/tests/suites/acl/acivattr_test.py
index d55eea023..efe2d5ef1 100644
--- a/dirsrvtests/tests/suites/acl/acivattr_test.py
+++ b/dirsrvtests/tests/suites/acl/acivattr_test.py
@@ -1,12 +1,12 @@
# --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2019 Red Hat, Inc.
+# Copyright (C) 2026 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---
-import pytest, os, ldap
+import pytest, os, ldap, time
from lib389._constants import DEFAULT_SUFFIX, PW_DM
from lib389.idm.user import UserAccount
from lib389.idm.organization import Organization
@@ -190,6 +190,8 @@ def test_positive(topo, _add_user, aci_of_user, user, entry, aci):
"""
# set aci
Domain(topo.standalone, DNBASE).set("aci", aci)
+ time.sleep(.5)
+
# create connection
conn = UserAccount(topo.standalone, user).bind(PW_DM)
# according to the aci , user will be able to change description
@@ -242,6 +244,7 @@ def test_negative(topo, _add_user, aci_of_user, user, entry, aci):
"""
# set aci
Domain(topo.standalone, DNBASE).set("aci", aci)
+ time.sleep(.5)
# create connection
conn = UserAccount(topo.standalone, user).bind(PW_DM)
# according to the aci , user will not be able to change description
diff --git a/dirsrvtests/tests/suites/import/regression_test.py b/dirsrvtests/tests/suites/import/regression_test.py
index 0e3ba1930..25a7f359d 100644
--- a/dirsrvtests/tests/suites/import/regression_test.py
+++ b/dirsrvtests/tests/suites/import/regression_test.py
@@ -687,7 +687,7 @@ def test_ldif2db_after_backend_create(topo, verify):
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) < 15
+ assert abs(import_time_1 - import_time_2) < 20
def test_ldif_missing_suffix_entry(topo, request, verify):
diff --git a/dirsrvtests/tests/suites/replication/wait_for_async_feature_test.py b/dirsrvtests/tests/suites/replication/wait_for_async_feature_test.py
index c5ab585e4..84ce1ca2b 100644
--- a/dirsrvtests/tests/suites/replication/wait_for_async_feature_test.py
+++ b/dirsrvtests/tests/suites/replication/wait_for_async_feature_test.py
@@ -26,7 +26,7 @@ log = logging.getLogger(__name__)
installation1_prefix = None
# Expected minimum and maximum number of async result in usual cases
-USUAL_MIN_AP = 3
+USUAL_MIN_AP = 2
USUAL_MAX_AP = 11
@pytest.fixture(params=[(None, (USUAL_MIN_AP, USUAL_MAX_AP)),
diff --git a/dirsrvtests/tests/suites/retrocl/basic_test.py b/dirsrvtests/tests/suites/retrocl/basic_test.py
index b53a60851..2fce72049 100644
--- a/dirsrvtests/tests/suites/retrocl/basic_test.py
+++ b/dirsrvtests/tests/suites/retrocl/basic_test.py
@@ -492,7 +492,7 @@ def test_retrocl_trimming_entries(topology_st):
if inst.searchErrorsLog("trim_changelog: removed "):
log.info(f'Trimming detected after {attempt * 6} seconds')
break
-
+
log.info('Verify trimming occurred by checking error log')
assert inst.searchErrorsLog("trim_changelog: removed ")
--
2.52.0

View File

@ -0,0 +1,56 @@
From 58f5d129496cc8b4271daf5d0cd3ab31e8b926a8 Mon Sep 17 00:00:00 2001
From: Akshay Adhikari <aadhikar@redhat.com>
Date: Thu, 12 Feb 2026 12:47:23 +0530
Subject: [PATCH] Issue 7233 - test_produce_division_by_zero fails with
IsADirectoryError in conftest.py (#7234)
Description: glob('/*/*') matches directories causing open() to fail.
Fixes: #7233
Reviewed by: @droideck (Thanks!)
---
dirsrvtests/conftest.py | 2 ++
.../suites/disk_monitoring/disk_monitoring_divide_test.py | 7 ++++---
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/dirsrvtests/conftest.py b/dirsrvtests/conftest.py
index 0db6045f4..19b34c4a4 100644
--- a/dirsrvtests/conftest.py
+++ b/dirsrvtests/conftest.py
@@ -126,6 +126,8 @@ def pytest_runtest_makereport(item, call):
text = asan_report.read()
extra.append(pytest_html.extras.text(text, name=os.path.basename(f)))
for f in glob.glob(f'{p.log_dir.split("/slapd",1)[0]}/*/*'):
+ if not os.path.isfile(f):
+ continue
if f.endswith('gz'):
with gzip.open(f, 'rb') as dirsrv_log:
text = dirsrv_log.read()
diff --git a/dirsrvtests/tests/suites/disk_monitoring/disk_monitoring_divide_test.py b/dirsrvtests/tests/suites/disk_monitoring/disk_monitoring_divide_test.py
index 9d952f93a..3e52e7c6b 100644
--- a/dirsrvtests/tests/suites/disk_monitoring/disk_monitoring_divide_test.py
+++ b/dirsrvtests/tests/suites/disk_monitoring/disk_monitoring_divide_test.py
@@ -31,15 +31,16 @@ def create_dummy_mount(topology_st, request):
log.info('Create dummy mount')
for cmd in cmds:
log.info('Command used : %s' % cmd)
- subprocess.Popen(cmd, shell=True)
+ subprocess.run(cmd, shell=True)
def fin():
cmds = ['umount /var/log/dirsrv/slapd-{}/tmp'.format(topology_st.standalone.serverid),
+ 'rmdir /var/log/dirsrv/slapd-{}/tmp'.format(topology_st.standalone.serverid),
'setenforce 1']
for cmd in cmds:
- log.info('Command used : %s' % cmds)
- subprocess.Popen(cmd, shell=True)
+ log.info('Command used : %s' % cmd)
+ subprocess.run(cmd, shell=True)
request.addfinalizer(fin)
--
2.52.0

View File

@ -0,0 +1,226 @@
From bbda49b86f3841ac5100894da426edc541b6226c Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Thu, 12 Feb 2026 09:15:18 +0100
Subject: [PATCH] Issue 7241 - Drop dateutil (#7242)
Bug Description:
python-dateutil is unmaintained upstream and is marked for deprecation.
Fix Description:
* Replace `dateutil.tz.tzoffset` with `datetime.timezone(datetime.timedelta())`.
* Replace `dateutil.parser.parse` with standard `datetime` calls.
* Import `datetime` as `dt` to avoid confusion between module and class.
* Fix month lookup bug ('Oct': 9 / 'Sep': 10).
Fixes: https://github.com/389ds/389-ds-base/issues/7241
Reviewed by: jchapma, droideck (Thanks!)
---
.../suites/password/pwdPolicy_warning_test.py | 5 +-
src/lib389/lib389/dirsrv_log.py | 48 ++++++++++++-------
src/lib389/lib389/tests/dirsrv_log_test.py | 13 +++--
src/lib389/pyproject.toml | 2 -
src/lib389/requirements.txt | 1 -
5 files changed, 38 insertions(+), 31 deletions(-)
diff --git a/dirsrvtests/tests/suites/password/pwdPolicy_warning_test.py b/dirsrvtests/tests/suites/password/pwdPolicy_warning_test.py
index 154ec01f1..2341da6eb 100644
--- a/dirsrvtests/tests/suites/password/pwdPolicy_warning_test.py
+++ b/dirsrvtests/tests/suites/password/pwdPolicy_warning_test.py
@@ -15,9 +15,8 @@ from lib389.topologies import topology_st
from lib389.idm.user import UserAccounts
from lib389.idm.organizationalunit import OrganizationalUnits
from lib389._constants import (DEFAULT_SUFFIX, DN_CONFIG, PASSWORD, DN_DM)
-from dateutil.parser import parse as dt_parse
from lib389.config import Config
-import datetime
+import datetime as dt
pytestmark = pytest.mark.tier1
@@ -351,7 +350,7 @@ def test_with_different_password_states(topology_st, global_policy, add_user):
old_ts = user.get_attr_val_utf8('passwordExpirationTime')
log.info("Old passwordExpirationTime: {}".format(old_ts))
- new_ts = (dt_parse(old_ts) - datetime.timedelta(31)).strftime('%Y%m%d%H%M%SZ')
+ new_ts = (dt.datetime.strptime(old_ts, '%Y%m%d%H%M%SZ') - dt.timedelta(31)).strftime('%Y%m%d%H%M%SZ')
log.info("New passwordExpirationTime: {}".format(new_ts))
user.replace('passwordExpirationTime', new_ts)
diff --git a/src/lib389/lib389/dirsrv_log.py b/src/lib389/lib389/dirsrv_log.py
index e40105ad3..3d923a8bf 100644
--- a/src/lib389/lib389/dirsrv_log.py
+++ b/src/lib389/lib389/dirsrv_log.py
@@ -10,11 +10,11 @@
"""
import copy
+import datetime as dt
import json
import glob
import re
import gzip
-from dateutil.parser import parse as dt_parse
from lib389.utils import ensure_bytes
from lib389._mapped_object_lint import DSLint
from lib389.lint import (
@@ -34,8 +34,8 @@ MONTH_LOOKUP = {
'Jun': 6,
'Jul': 7,
'Aug': 8,
- 'Oct': 9,
- 'Sep': 10,
+ 'Sep': 9,
+ 'Oct': 10,
'Nov': 11,
'Dec': 12,
}
@@ -50,9 +50,9 @@ class DirsrvLog(DSLint):
"""
self.dirsrv = dirsrv
self.log = self.dirsrv.log
- self.prog_timestamp = re.compile(r'\[(?P<day>\d*)\/(?P<month>\w*)\/(?P<year>\d*):(?P<hour>\d*):(?P<minute>\d*):(?P<second>\d*)(.(?P<nanosecond>\d*))+\s(?P<tz>[\+\-]\d*)') # noqa
+ self.prog_timestamp = re.compile(r'\[(?P<day>\d*)\/(?P<month>\w*)\/(?P<year>\d*):(?P<hour>\d*):(?P<minute>\d*):(?P<second>\d*)(.(?P<nanosecond>\d*))+\s(?P<tz>[\+\-]\d{4})') # noqa
# JSON timestamp uses strftime %FT%T --> 2025-02-12T17:00:47.663123181 -0500
- self.prog_json_timestamp = re.compile(r'(?P<year>\d*)-(?P<month>\w*)-(?P<day>\d*)T(?P<hour>\d*):(?P<minute>\d*):(?P<second>\d*)(.(?P<nanosecond>\d*))+\s(?P<tz>[\+\-]\d*)') # noqa
+ self.prog_json_timestamp = re.compile(r'(?P<year>\d*)-(?P<month>\w*)-(?P<day>\d*)T(?P<hour>\d*):(?P<minute>\d*):(?P<second>\d*)(.(?P<nanosecond>\d*))+\s(?P<tz>[\+\-]\d{4})') # noqa
self.prog_datetime = re.compile(r'^(?P<timestamp>\[.*\])')
self.jsonFormat = False
@@ -157,20 +157,32 @@ class DirsrvLog(DSLint):
else:
timedata = self.prog_timestamp.match(ts).groupdict()
- # Now, have to convert month to an int.
- dt_str = '{YEAR}-{MONTH}-{DAY} {HOUR}-{MINUTE}-{SECOND} {TZ}'.format(
- YEAR=timedata['year'],
- MONTH=timedata['month'],
- DAY=timedata['day'],
- HOUR=timedata['hour'],
- MINUTE=timedata['minute'],
- SECOND=timedata['second'],
- TZ=timedata['tz'],
- )
- dt = dt_parse(dt_str)
+ # Convert month to an int.
+ month = timedata['month']
+ if not month.isdigit():
+ month = MONTH_LOOKUP[month]
+ else:
+ month = int(month)
+
+ # Parse timezone offset string (e.g. "+1000" or "-0500") into a timezone
+ tz_str = timedata['tz']
+ tz_sign = 1 if tz_str[0] == '+' else -1
+ tz_hours = int(tz_str[1:3])
+ tz_minutes = int(tz_str[3:5])
+ tz = dt.timezone(dt.timedelta(hours=tz_sign * tz_hours, minutes=tz_sign * tz_minutes))
+
+ parsed_dt = dt.datetime(
+ int(timedata['year']),
+ month,
+ int(timedata['day']),
+ int(timedata['hour']),
+ int(timedata['minute']),
+ int(timedata['second']),
+ tzinfo=tz
+ )
if timedata['nanosecond']:
- dt = dt.replace(microsecond=int(int(timedata['nanosecond']) / 1000))
- return dt
+ parsed_dt = parsed_dt.replace(microsecond=int(timedata['nanosecond']) // 1000)
+ return parsed_dt
def get_time_in_secs(self, log_line):
"""Take the timestamp (not the date) from a DS access log and convert
diff --git a/src/lib389/lib389/tests/dirsrv_log_test.py b/src/lib389/lib389/tests/dirsrv_log_test.py
index 920e67a01..d0259ced9 100644
--- a/src/lib389/lib389/tests/dirsrv_log_test.py
+++ b/src/lib389/lib389/tests/dirsrv_log_test.py
@@ -12,8 +12,7 @@ from lib389 import DirSrv, Entry
import pytest
import time
import shutil
-import datetime
-from dateutil.tz import tzoffset
+import datetime as dt
INSTANCE_PORT = 54321
INSTANCE_SERVERID = 'standalone'
@@ -74,7 +73,7 @@ def test_access_log(topology):
topology.standalone.ds_access_log.parse_line('[27/Apr/2016:12:49:49.726093186 +1000] conn=1 fd=64 slot=64 connection from ::1 to ::1') ==
{
'slot': '64', 'remote': '::1', 'action': 'CONNECT', 'timestamp': '[27/Apr/2016:12:49:49.726093186 +1000]', 'fd': '64', 'conn': '1', 'local': '::1',
- 'datetime': datetime.datetime(2016, 4, 27, 12, 0, 0, 726093, tzinfo=tzoffset(None, 36000))
+ 'datetime': dt.datetime(2016, 4, 27, 12, 49, 49, 726093, tzinfo=dt.timezone(dt.timedelta(seconds=36000)))
}
)
assert(
@@ -82,21 +81,21 @@ def test_access_log(topology):
{
'rem': 'base="cn=config" scope=0 filter="(objectClass=*)" attrs="nsslapd-instancedir nsslapd-errorlog nsslapd-accesslog nsslapd-auditlog nsslapd-certdir nsslapd-schemadir nsslapd-bakdir nsslapd-ldifdir"', # noqa
'action': 'SRCH', 'timestamp': '[27/Apr/2016:12:49:49.727235997 +1000]', 'conn': '1', 'op': '2',
- 'datetime': datetime.datetime(2016, 4, 27, 12, 0, 0, 727235, tzinfo=tzoffset(None, 36000))
+ 'datetime': dt.datetime(2016, 4, 27, 12, 49, 49, 727235, tzinfo=dt.timezone(dt.timedelta(seconds=36000)))
}
)
assert(
topology.standalone.ds_access_log.parse_line('[27/Apr/2016:12:49:49.736297002 +1000] conn=1 op=4 fd=64 closed - U1') ==
{
'status': 'U1', 'fd': '64', 'action': 'DISCONNECT', 'timestamp': '[27/Apr/2016:12:49:49.736297002 +1000]', 'conn': '1', 'op': '4',
- 'datetime': datetime.datetime(2016, 4, 27, 12, 0, 0, 736297, tzinfo=tzoffset(None, 36000))
+ 'datetime': dt.datetime(2016, 4, 27, 12, 49, 49, 736297, tzinfo=dt.timezone(dt.timedelta(seconds=36000)))
}
)
assert(
topology.standalone.ds_access_log.parse_line('[27/Apr/2016:12:49:49.736297002 -1000] conn=1 op=4 fd=64 closed - U1') ==
{
'status': 'U1', 'fd': '64', 'action': 'DISCONNECT', 'timestamp': '[27/Apr/2016:12:49:49.736297002 -1000]', 'conn': '1', 'op': '4',
- 'datetime': datetime.datetime(2016, 4, 27, 12, 0, 0, 736297, tzinfo=tzoffset(None, -36000))
+ 'datetime': dt.datetime(2016, 4, 27, 12, 49, 49, 736297, tzinfo=dt.timezone(dt.timedelta(seconds=-36000)))
}
)
@@ -113,7 +112,7 @@ def test_error_log(topology):
topology.standalone.ds_error_log.parse_line('[27/Apr/2016:13:46:35.775670167 +1000] slapd started. Listening on All Interfaces port 54321 for LDAP requests') == # noqa
{
'timestamp': '[27/Apr/2016:13:46:35.775670167 +1000]', 'message': 'slapd started. Listening on All Interfaces port 54321 for LDAP requests',
- 'datetime': datetime.datetime(2016, 4, 27, 13, 0, 0, 775670, tzinfo=tzoffset(None, 36000))
+ 'datetime': dt.datetime(2016, 4, 27, 13, 46, 35, 775670, tzinfo=dt.timezone(dt.timedelta(seconds=36000)))
}
)
diff --git a/src/lib389/pyproject.toml b/src/lib389/pyproject.toml
index 63c7c9710..e067d1590 100644
--- a/src/lib389/pyproject.toml
+++ b/src/lib389/pyproject.toml
@@ -5,7 +5,6 @@ requires = [
"argparse-manpage[setuptools]",
"pyasn1",
"pyasn1-modules",
- "python-dateutil",
"argcomplete",
"python-ldap",
"distro",
@@ -43,7 +42,6 @@ classifiers = [
dependencies = [
"pyasn1",
"pyasn1-modules",
- "python-dateutil",
"argcomplete",
"python-ldap",
"distro",
diff --git a/src/lib389/requirements.txt b/src/lib389/requirements.txt
index 5e1b3dad7..94b10e3c2 100644
--- a/src/lib389/requirements.txt
+++ b/src/lib389/requirements.txt
@@ -1,6 +1,5 @@
pyasn1
pyasn1-modules
-python-dateutil
argcomplete
argparse-manpage
python-ldap
--
2.52.0

View File

@ -0,0 +1,94 @@
From d19c50372d5c5d901f05ce6e7dd03313f41fc197 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 | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py b/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py
index 85b4ac078..d0e7e8a32 100644
--- a/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py
+++ b/dirsrvtests/tests/suites/syncrepl_plugin/basic_test.py
@@ -21,7 +21,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,10 +214,13 @@ class Sync_persist(threading.Thread, ReconnectLDAPObject, SyncreplConsumer):
def run(self):
"""Start a sync repl client"""
- ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, os.path.join(self.inst.get_config_dir(), "ca.crt"))
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",
@@ -257,6 +260,7 @@ def test_sync_repl_mep(topology, request):
5. Success
"""
inst = topology[0]
+ inst.enable_tls()
# Enable/configure retroCL
plugin = RetroChangelogPlugin(inst)
@@ -342,6 +346,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)
@@ -408,6 +413,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()
@@ -551,6 +558,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()
@@ -609,7 +617,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

@ -0,0 +1,33 @@
From 1df3852cf0e073cfe006d661aecdd909862fc79a 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

@ -0,0 +1,201 @@
From 63bf648f699b8fcd8f319254b0348d969ceea7a0 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 c7275e4cb..ad6d05a1e 100644
--- a/dirsrvtests/tests/suites/import/import_test.py
+++ b/dirsrvtests/tests/suites/import/import_test.py
@@ -514,7 +514,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 d57a91929..01b3ea23c 100644
--- a/src/lib389/lib389/__init__.py
+++ b/src/lib389/lib389/__init__.py
@@ -2953,7 +2953,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
@@ -2966,34 +2966,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

@ -0,0 +1,74 @@
From d52901f69e9b7952b33b219ec197308a1a20bda9 Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Fri, 13 Feb 2026 15:13:05 +0100
Subject: [PATCH] Issue 7252 - PQC - Need to iterate on SECOidTag instead of
using OID (#7254)
* Issue 7252 - PQC - Need to iterate on SECOidTag instead of using OID
Need to dynamically iterate on SECOidTag instead of using SEC_OID_ML_DSA_* OIDs to avoid issue with upcoming nss versions and fix a RHEL build break with nss 3.112
Issue: #7252
Reviewed by: @mreynolds389, @droideck, @vashirov
* Update ldap/servers/slapd/ssl.c
Co-authored-by: Simon Pichugin <spichugi@redhat.com>
---------
Co-authored-by: Simon Pichugin <spichugi@redhat.com>
---
ldap/servers/slapd/ssl.c | 24 +++++++++---------------
1 file changed, 9 insertions(+), 15 deletions(-)
diff --git a/ldap/servers/slapd/ssl.c b/ldap/servers/slapd/ssl.c
index 7d5db2cdd..d05c64fb1 100644
--- a/ldap/servers/slapd/ssl.c
+++ b/ldap/servers/slapd/ssl.c
@@ -732,31 +732,25 @@ SSLPLCY_Install(void)
{
SECStatus s = 0;
-#ifdef MAX_ML_DSA_PRIVATE_KEY_LEN
int flags = NSS_USE_ALG_IN_SIGNATURE | NSS_USE_ALG_IN_SSL;
- static const SECOidTag oids[] = {
- SEC_OID_ML_DSA_44,
- SEC_OID_ML_DSA_65,
- SEC_OID_ML_DSA_87,
- };
-#endif
+ SECOidData *oid = NULL;
s = NSS_SetDomesticPolicy();
-#ifdef MAX_ML_DSA_PRIVATE_KEY_LEN
/* Should rely on the crypto module policy in FIPS mode */
if (!slapd_pk11_isFIPS()) {
/* Set explicitly PQC algorithm policy if it is not set by default */
- for (size_t i=0; s == SECSuccess && i < PR_ARRAY_SIZE(oids); i++) {
- PRUint32 oflags = 0;
- (void) NSS_GetAlgorithmPolicy(oids[i], &oflags);
- if ((oflags & flags) != flags) {
- s = NSS_SetAlgorithmPolicy(oids[i], flags, 0);
+ for (SECOidTag tag = 1; s == SECSuccess && (oid = SECOID_FindOIDByTag(tag)) != NULL; tag++) {
+ if (oid->mechanism != CKM_INVALID_MECHANISM &&
+ PL_strncasecmp(oid->desc, "ML-DSA-", 7) == 0) {
+ PRUint32 oflags = 0;
+ (void) NSS_GetAlgorithmPolicy(tag, &oflags);
+ if ((oflags & flags) != flags) {
+ s = NSS_SetAlgorithmPolicy(tag, flags, 0);
+ }
}
}
}
-#endif
-
return s ? PR_FAILURE : PR_SUCCESS;
}
--
2.52.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
From 48ad61231203d9ccb96d0fe542aae93dbb74a9bf 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

@ -0,0 +1,32 @@
From c7ef5b3073bbd94a5d2b544556368c830c165e0d 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 7cdf16a84..89a747199 100644
--- a/dirsrvtests/tests/suites/vlv/regression_test.py
+++ b/dirsrvtests/tests/suites/vlv/regression_test.py
@@ -1175,6 +1175,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

@ -0,0 +1,34 @@
From 7e575cc8cc6f1bf558f50ca0fc55145e469d60d2 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
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
integerOrderingMatch in the in-memory attrinfo, but ancestorid on disk
might be using lexicographic ordering (data before the upgrade or after
ldif2db import).
Relates: https://github.com/389ds/389-ds-base/issues/7223
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 17bfc09a0..1569eb7ff 100644
--- a/ldap/servers/slapd/back-ldbm/instance.c
+++ b/ldap/servers/slapd/back-ldbm/instance.c
@@ -231,7 +231,7 @@ 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");
+ e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, 0);
attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
slapi_entry_free(e);
--
2.52.0

View File

@ -0,0 +1,38 @@
From 245bc3b53f385e12e4dc9d2cb765a55e10e0fdc5 Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Fri, 13 Feb 2026 17:51:12 +0100
Subject: [PATCH] Issue 3134 - Fix build break (#7260)
Fix build break of PR #7238 related to import rpm
Issue: #3134
Reviewed by: @vashirov (Thanks!)
---
src/lib389/lib389/utils.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py
index 07cb34d93..4df36cf1f 100644
--- a/src/lib389/lib389/utils.py
+++ b/src/lib389/lib389/utils.py
@@ -28,7 +28,6 @@ from datetime import (datetime, timedelta)
import sys
import filecmp
import pwd
-import rpm
import shlex
import operator
import subprocess
@@ -2135,6 +2134,8 @@ def get_timeout_scale():
def rpm_is_older(pkg, version):
"""Check if an RPM package version is older than specified version"""
+ # rpm module is not installed in build environment so let import it only when used.
+ import rpm
ts = rpm.TransactionSet()
mi = ts.dbMatch('name', pkg)
for h in mi:
--
2.52.0

View File

@ -0,0 +1,87 @@
From ac3d9253e0a7a4b5f0108506bcf25255b302fd16 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 | 7 ++++---
ldap/servers/slapd/modify.c | 2 +-
ldap/servers/slapd/pw.c | 13 ++++++++++++-
3 files changed, 17 insertions(+), 5 deletions(-)
diff --git a/dirsrvtests/tests/suites/password/pwp_history_test.py b/dirsrvtests/tests/suites/password/pwp_history_test.py
index cf68d743c..78b448a87 100644
--- a/dirsrvtests/tests/suites/password/pwp_history_test.py
+++ b/dirsrvtests/tests/suites/password/pwp_history_test.py
@@ -189,9 +189,9 @@ def test_history_is_not_overwritten(topology_st, user):
@pytest.mark.parametrize('policy',
- [(pytest.param('global', marks=pytest.mark.xfail(reason="DS7052"))),
- (pytest.param('subtree', marks=pytest.mark.xfail(reason="DS7066, DS7052"))),
- (pytest.param('user', marks=pytest.mark.xfail(reason="DS7066, DS7052")))])
+ [(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
@@ -282,6 +282,7 @@ def test_basic(topology_st, user, policy):
# Password history [password3, password4], current password is "password1"
# Reset password by Directory Manager(admin reset)
+ dm = DirectoryManager(topology_st.standalone)
dm.rebind()
time.sleep(.5)
change_password(user, 'password-reset', success=True)
diff --git a/ldap/servers/slapd/modify.c b/ldap/servers/slapd/modify.c
index 9e5bce80b..0ecce9bc8 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 055ec0d74..c53ecf23d 100644
--- a/ldap/servers/slapd/pw.c
+++ b/ldap/servers/slapd/pw.c
@@ -1535,7 +1535,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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
From b216b86c5607dc0421eb609c46f3004844fb37c0 Mon Sep 17 00:00:00 2001
From: Akshay Adhikari <aadhikar@redhat.com>
Date: Tue, 17 Feb 2026 17:40:44 +0530
Subject: [PATCH] Issue 6758 - Fix Enable Replication dropdown not opening
(#7262)
Description: Removed hardcoded isOpen={false} and empty onToggle handler that
prevented dropdown from opening. Let component manage its own state.
Relates: #6758
Reviewed by: @vashirov
---
src/cockpit/389-console/src/lib/replication/replModals.jsx | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/cockpit/389-console/src/lib/replication/replModals.jsx b/src/cockpit/389-console/src/lib/replication/replModals.jsx
index ba4859617..83a8b75a5 100644
--- a/src/cockpit/389-console/src/lib/replication/replModals.jsx
+++ b/src/cockpit/389-console/src/lib/replication/replModals.jsx
@@ -1757,8 +1757,6 @@ export class EnableReplModal extends React.Component {
handleChange(syntheticEvent);
}}
options={[_("Supplier"), _("Hub"), _("Consumer")]}
- isOpen={false}
- onToggle={() => {}}
placeholder={_("Select role...")}
ariaLabel="Replication role selection"
isMulti={false}
--
2.52.0

View File

@ -0,0 +1,538 @@
From 6ce19a9a3e36213a5604144aa5eb3cba666e5ed4 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
parentid (#7264)
Description:
integerOrderingMatch was introduced as a requirement for parentid and
ancestorid indexes for performance reasons. But after #7096 the order
for parentid doesn't make a lot of difference.
Fix Description:
* Remove integerOrderingMatch requirement for parentid.
* Read only first 100 keys from dbscan in index ordering check
* Do not run dsctl index-check during RPM upgrade
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 | 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(+), 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 dd42cd197..8dc82c779 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):
def test_missing_matching_rule(topology_st, log_buffering_enabled):
- """Check if healthcheck returns DSBLE0007 code when parentId index is missing integerOrderingMatch
+ """Check that healthcheck does NOT report DSBLE0007 when parentId index is missing integerOrderingMatch.
+ Both lexicographic and integer orderings are valid for parentid.
:id: 7ffa71db-8995-430a-bed8-59bce944221c
:setup: Standalone instance
@@ -189,19 +190,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
- 6. Use healthcheck without --json option
- 7. Use healthcheck with --json option
:expectedresults:
1. Success
2. Success
- 3. healthcheck reports DSBLE0007 code and related details
- 4. healthcheck reports DSBLE0007 code and related details
+ 3. healthcheck reports no issues found
+ 4. healthcheck reports no issues found
5. Success
- 6. healthcheck reports no issues found
- 7. healthcheck reports no issues found
"""
- RET_CODE = "DSBLE0007"
PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
standalone = topology_st.standalone
@@ -210,17 +206,14 @@ def test_missing_matching_rule(topology_st, log_buffering_enabled):
parentid_index = Index(standalone, PARENTID_DN)
parentid_index.remove("nsMatchingRule", "integerOrderingMatch")
- 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)
+ 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("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)
-
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
@@ -910,7 +903,9 @@ def test_index_check_fixes_ancestorid_config(topology_st):
def test_index_check_fixes_missing_matching_rule(topology_st):
- """Check if dsctl index-check --fix adds missing integerOrderingMatch
+ """Check that removing integerOrderingMatch from parentid config is not
+ flagged as an issue when disk ordering cannot be determined.
+ Both lexicographic and integer orderings are valid for parentid.
:id: 6c1d4e9f-0a3b-4d5c-1e7f-8a9b0c2d3e4f
:setup: Standalone instance
@@ -918,18 +913,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
- 4. Run dsctl index-check (should detect issue)
- 5. Run dsctl index-check --fix
- 6. Verify integerOrderingMatch was added back
- 7. Start the server
+ 4. Run dsctl index-check (should NOT detect issue since disk ordering is unknown)
+ 5. Start the server
:expectedresults:
1. Success
2. Success
3. Success
- 4. index-check returns False and detects missing matching rule
- 5. index-check returns True after fix
- 6. integerOrderingMatch is present
- 7. Success
+ 4. index-check returns True (no issues, disk ordering unknown)
+ 5. Success
"""
from lib389.cli_ctl.dbtasks import dbtasks_index_check
from lib389.dseldif import DSEldif
@@ -963,34 +954,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")
- log.info("Run index-check without --fix (should detect issue)")
+ log.info("Run index-check (should NOT detect issue - disk ordering unknown)")
args = FakeArgs()
args.backend = "userRoot"
args.fix = False
result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
- assert result is False, "index-check should detect missing matching rule"
- assert topology_st.logcap.contains("missing integerOrderingMatch")
+ assert result is True, \
+ "index-check should not flag missing integerOrderingMatch when disk ordering is unknown"
+ assert topology_st.logcap.contains("could not determine disk ordering")
topology_st.logcap.flush()
- log.info("Run index-check with --fix")
- args.fix = True
- result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
- assert result is True, "index-check --fix should succeed"
- assert topology_st.logcap.contains("integerOrderingMatch")
- topology_st.logcap.flush()
-
- log.info("Verify integerOrderingMatch was added back")
- dse_ldif = DSEldif(standalone) # Reload to get fresh data
- matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
- assert matching_rules is not None, "nsMatchingRule should be present"
- found_int_order = False
- for mr in matching_rules:
- if "integerorderingmatch" in mr.lower():
- found_int_order = True
- break
- assert found_int_order, f"integerOrderingMatch should be present, got: {matching_rules}"
- log.info("integerOrderingMatch successfully added back")
+ log.info("Restore integerOrderingMatch and start the server")
+ dse_ldif = DSEldif(standalone)
+ dse_ldif.add(parentid_dn, "nsMatchingRule", "integerOrderingMatch")
log.info("Start the server")
standalone.start()
@@ -1080,7 +1057,7 @@ def test_index_check_fixes_multiple_issues(topology_st):
:steps:
1. Create DS instance
2. Stop the server
- 3. Add multiple issues: scanlimit, ancestorid config, missing matching rule
+ 3. Add multiple issues: scanlimit and ancestorid config
4. Run dsctl index-check (should detect all issues)
5. Run dsctl index-check --fix
6. Verify all issues were fixed
@@ -1122,14 +1099,6 @@ def test_index_check_fixes_multiple_issues(topology_st):
]
dse_ldif.add_entry(ancestorid_entry)
- log.info("Add issue 3: Remove integerOrderingMatch from parentid")
- dse_ldif = DSEldif(standalone) # Reload
- matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
- if matching_rules:
- for mr in matching_rules:
- if "integerorderingmatch" in mr.lower():
- dse_ldif.delete(parentid_dn, "nsMatchingRule", mr)
-
log.info("Run index-check without --fix (should detect all issues)")
args = FakeArgs()
args.backend = "userRoot"
@@ -1160,16 +1129,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}"
- # Check matching rule added back
- matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
- found_int_order = False
- if matching_rules:
- for mr in matching_rules:
- if "integerorderingmatch" in mr.lower():
- found_int_order = True
- break
- assert found_int_order, f"integerOrderingMatch should be present, got: {matching_rules}"
-
log.info("All issues verified as fixed")
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 6b1b012da..9557e9066 100644
--- a/ldap/servers/slapd/upgrade.c
+++ b/ldap/servers/slapd/upgrade.c
@@ -551,107 +551,6 @@ upgrade_remove_ancestorid_index_config(void)
return uresult;
}
-/*
- * 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", 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_dn = slapi_entry_get_dn_const(backends[be_idx]);
- const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
- if (!be_dn || !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,%s",
- attr_name, be_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 the base config of the PAM PTA plugin.
@@ -879,10 +778,6 @@ upgrade_server(void)
return UPGRADE_FAILURE;
}
- if (upgrade_check_id_index_matching_rule() != UPGRADE_SUCCESS) {
- return UPGRADE_FAILURE;
- }
-
return UPGRADE_SUCCESS;
}
diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in
index 0e0e28285..370e3abd4 100644
--- a/rpm/389-ds-base.spec.in
+++ b/rpm/389-ds-base.spec.in
@@ -650,9 +650,6 @@ for dir in "$instbase"/slapd-* ; do
else
echo "instance $inst is not running" >> "$output" 2>&1 || :
fi
- # 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
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
index f3dbe7c92..6c8cbc018 100644
--- a/src/lib389/lib389/backend.py
+++ b/src/lib389/lib389/backend.py
@@ -647,9 +647,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.
- # Only parentid has a DSE config entry (for the integerOrderingMatch rule).
+ # parentid works correctly with both lexicographic and integer ordering,
+ # so integerOrderingMatch is not required.
expected_system_indexes = {
- 'parentid': {'types': ['eq'], 'matching_rule': 'integerOrderingMatch'},
+ 'parentid': {'types': ['eq'], 'matching_rule': None},
'objectClass': {'types': ['eq'], 'matching_rule': None},
'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 cd96cdaf7..b02de203f 100644
--- a/src/lib389/lib389/cli_ctl/dbtasks.py
+++ b/src/lib389/lib389/cli_ctl/dbtasks.py
@@ -10,6 +10,7 @@
import glob
import os
import re
+import signal
import subprocess
from enum import Enum
from lib389._constants import TaskWarning
@@ -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
+ # Only read the first 100 lines from dbscan to avoid scanning the
+ # entire index (which can take hours on large databases).
try:
- result = subprocess.run(
+ proc = subprocess.Popen(
[dbscan_path, "-f", index_file],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
- timeout=60,
)
- if result.returncode != 0:
- log.warning(" dbscan returned non-zero exit code for %s", index_file)
- return IndexOrdering.UNKNOWN
-
- # Parse keys from dbscan output
keys = []
- for line in result.stdout.split("\n"):
+ line_count = 0
+ assert proc.stdout is not None
+ for line in proc.stdout:
+ line_count += 1
+ if line_count > 100:
+ break
line = line.strip()
if line.startswith("="):
match = re.match(r"^=(\d+)", line)
if match:
keys.append(int(match.group(1)))
+ proc.terminate()
+ try:
+ proc.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ proc.wait()
+
+ if proc.returncode not in (0, -signal.SIGTERM):
+ log.warning(" dbscan returned non-zero exit code for %s", index_file)
+ return IndexOrdering.UNKNOWN
+
if len(keys) < 2:
return IndexOrdering.UNKNOWN
# Check if keys are in integer order by looking for decreasing numeric values
# (which would indicate lexicographic ordering, e.g., "3" < "30" < "4")
prev_id = keys[0]
- for i in range(1, min(len(keys), 100)):
- current_id = keys[i]
+ for current_id in keys[1:]:
if prev_id > current_id:
return IndexOrdering.LEXICOGRAPHIC
prev_id = current_id
return IndexOrdering.INTEGER
- except subprocess.TimeoutExpired:
- log.warning(" dbscan timed out for %s", index_file)
- return IndexOrdering.UNKNOWN
except OSError as e:
log.warning(" Error running dbscan: %s", e)
return IndexOrdering.UNKNOWN
@@ -375,8 +384,7 @@ def dbtasks_index_check(inst, log, args):
# Track all issues found
all_ok = True
- mismatches = [] # (backend, index_name) tuples needing reindex
- missing_matching_rules = [] # (backend, index_name) tuples missing integerOrderingMatch
+ config_fixes = [] # (backend, index_name, action) tuples: action is "add_mr" or "remove_mr"
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
@@ -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)
- # For parentid, still check if matching rule is missing
- if index_name == "parentid":
- config_has_int_order = _has_integer_ordering_match(dse_ldif, backend, index_name)
- if not config_has_int_order:
- log.warning(" %s - missing integerOrderingMatch in config", index_name)
- missing_matching_rules.append((backend, index_name))
- all_ok = False
continue
config_has_int_order = _has_integer_ordering_match(dse_ldif, backend, index_name)
@@ -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)
- # For parentid, the desired state is always integer ordering
+ # Both orderings are valid for parentid, but config must match disk.
if index_name == "parentid":
- if not config_has_int_order:
- log.warning(" %s - missing integerOrderingMatch in config", index_name)
- if (backend, index_name) not in missing_matching_rules:
- missing_matching_rules.append((backend, index_name))
+ if config_has_int_order and disk_ordering == IndexOrdering.LEXICOGRAPHIC:
+ log.warning(" %s - MISMATCH: config has integerOrderingMatch but disk is lexicographic", index_name)
+ config_fixes.append((backend, index_name, "remove_mr"))
all_ok = False
-
- if disk_ordering == IndexOrdering.LEXICOGRAPHIC:
- log.warning(" %s - disk ordering is lexicographic, needs reindex", index_name)
- if (backend, index_name) not in mismatches:
- mismatches.append((backend, index_name))
+ elif not config_has_int_order and disk_ordering == IndexOrdering.INTEGER:
+ log.warning(" %s - MISMATCH: config is lexicographic but disk has integer ordering", index_name)
+ config_fixes.append((backend, index_name, "add_mr"))
all_ok = False
# Handle issues
@@ -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
- # Add missing matching rules to dse.ldif
- for backend, index_name in missing_matching_rules:
+ # Fix config-vs-disk ordering mismatches by adjusting config to match disk
+ for backend, index_name, action in config_fixes:
index_dn = "cn={},cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(
index_name, backend
)
- log.info(" Adding integerOrderingMatch to %s in backend %s...", index_name, backend)
- try:
- dse_ldif.add(index_dn, "nsMatchingRule", "integerOrderingMatch")
- log.info(" Updated dse.ldif with integerOrderingMatch for %s", index_name)
- except Exception as e:
- log.error(" Failed to update dse.ldif for %s: %s", index_name, e)
- return False
-
- # Reindex indexes with disk ordering issues
- for backend, index_name in mismatches:
- log.info(" Reindexing %s in backend %s...", index_name, backend)
- if not inst.db2index(bename=backend, attrs=[index_name]):
- log.error(" Failed to reindex %s", index_name)
- return False
- log.info(" Reindex of %s completed successfully", index_name)
+ if action == "add_mr":
+ log.info(" Adding integerOrderingMatch to %s in backend %s...", index_name, backend)
+ try:
+ dse_ldif.add(index_dn, "nsMatchingRule", "integerOrderingMatch")
+ log.info(" Updated dse.ldif with integerOrderingMatch for %s", index_name)
+ except Exception as e:
+ log.error(" Failed to update dse.ldif for %s: %s", index_name, e)
+ return False
+ elif action == "remove_mr":
+ log.info(" Removing integerOrderingMatch from %s in backend %s...", index_name, backend)
+ try:
+ dse_ldif.delete(index_dn, "nsMatchingRule", "integerOrderingMatch")
+ log.info(" Removed integerOrderingMatch from %s", index_name)
+ except Exception as e:
+ log.error(" Failed to remove integerOrderingMatch from %s: %s", index_name, e)
+ return False
log.info("All issues fixed")
return True
@@ -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,
- help="Fix mismatches by reindexing affected indexes")
+ help="Fix mismatches by adjusting config to match on-disk data")
index_check_parser.set_defaults(func=dbtasks_index_check)
--
2.52.0

View File

@ -0,0 +1,227 @@
From 26feecae026581e39a43f001faff59e81a92c03d Mon Sep 17 00:00:00 2001
From: Lenka Doudova <mirielka@users.noreply.github.com>
Date: Wed, 18 Feb 2026 14:33:49 +0100
Subject: [PATCH] Issue 7236 - Fix GSSAPI tests (#7237)
* Issue 7236 - Fix GSSAPI tests
Description:
Fix for failing GSSAPI tests
Add GSSAPI_ACK variable to pytest workflow for proper execution in
Github CI
Relates: #7236
Author: Lenka Doudova
Reviewer: Barbora Simonova, Viktor Ashirov
---
.github/workflows/lmdbpytest.yml | 2 +-
.github/workflows/pytest.yml | 2 +-
.../tests/suites/gssapi/simple_gssapi_test.py | 2 +
.../suites/gssapi_repl/gssapi_repl_test.py | 43 +++++---------
src/lib389/lib389/topologies.py | 57 +++++++++++++++++++
5 files changed, 76 insertions(+), 30 deletions(-)
diff --git a/.github/workflows/lmdbpytest.yml b/.github/workflows/lmdbpytest.yml
index 2d0a122bf..376090bf6 100644
--- a/.github/workflows/lmdbpytest.yml
+++ b/.github/workflows/lmdbpytest.yml
@@ -120,7 +120,7 @@ jobs:
sudo docker exec $CID sh -c "systemctl enable --now cockpit.socket"
sudo docker exec $CID sh -c "mkdir -p /workspace/assets/cores && chmod 777 /workspace{,/assets{,/cores}}"
sudo docker exec $CID sh -c "echo '/workspace/assets/cores/core.%e.%P' > /proc/sys/kernel/core_pattern"
- sudo docker exec -e WEBUI=1 -e NSSLAPD_DB_LIB=mdb -e DEBUG=pw:api -e PASSWD="${PASSWD}" $CID py.test --suppress-no-test-exit-code -m "not flaky" --junit-xml=pytest.xml --html=pytest.html --browser=firefox --browser=chromium -v dirsrvtests/tests/suites/${{ matrix.suite }}
+ sudo docker exec -e WEBUI=1 -e NSSLAPD_DB_LIB=mdb -e DEBUG=pw:api -e PASSWD="${PASSWD}" -e GSSAPI_ACK=1 $CID py.test --suppress-no-test-exit-code -m "not flaky" --junit-xml=pytest.xml --html=pytest.html --browser=firefox --browser=chromium -v dirsrvtests/tests/suites/${{ matrix.suite }}
- name: Make the results file readable by all
if: always()
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 8a543be85..a51553656 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -125,7 +125,7 @@ jobs:
echo "Tests skipped because read-only Berkeley Database is installed." > pytest.html
echo "<?xml version="1.0" encoding="utf-8"?>'Tests skipped because read-only Berkeley Database is installed.'" > pytest.xml
else
- sudo docker exec -e WEBUI=1 -e NSSLAPD_DB_LIB=bdb -e DEBUG=pw:api -e PASSWD="${PASSWD}" $CID py.test --suppress-no-test-exit-code -m "not flaky" --junit-xml=pytest.xml --html=pytest.html --browser=firefox --browser=chromium -v dirsrvtests/tests/suites/${{ matrix.suite }}
+ sudo docker exec -e WEBUI=1 -e NSSLAPD_DB_LIB=bdb -e DEBUG=pw:api -e PASSWD="${PASSWD}" -e GSSAPI_ACK=1 $CID py.test --suppress-no-test-exit-code -m "not flaky" --junit-xml=pytest.xml --html=pytest.html --browser=firefox --browser=chromium -v dirsrvtests/tests/suites/${{ matrix.suite }}
fi
- name: Make the results file readable by all
diff --git a/dirsrvtests/tests/suites/gssapi/simple_gssapi_test.py b/dirsrvtests/tests/suites/gssapi/simple_gssapi_test.py
index be6f68a9a..e48de3491 100644
--- a/dirsrvtests/tests/suites/gssapi/simple_gssapi_test.py
+++ b/dirsrvtests/tests/suites/gssapi/simple_gssapi_test.py
@@ -34,6 +34,8 @@ def testuser(topology_st_gssapi):
})
# Give them a krb princ
user.create_keytab()
+ # Make krb5 config readable by everyone for the tests to work
+ os.chmod(user._instance.realm.krb5confrealm, 0o644)
return user
@gssapi_ack
diff --git a/dirsrvtests/tests/suites/gssapi_repl/gssapi_repl_test.py b/dirsrvtests/tests/suites/gssapi_repl/gssapi_repl_test.py
index 402684aab..fa7fc9c24 100644
--- a/dirsrvtests/tests/suites/gssapi_repl/gssapi_repl_test.py
+++ b/dirsrvtests/tests/suites/gssapi_repl/gssapi_repl_test.py
@@ -10,7 +10,7 @@ import pytest
from lib389.tasks import *
from lib389.utils import *
from lib389.agreement import *
-from lib389.topologies import topology_m2
+from lib389.topologies import topology_m2_gssapi, gssapi_ack
pytestmark = pytest.mark.tier2
@@ -69,25 +69,8 @@ def _allow_machine_account(inst, name):
(ldap.MOD_REPLACE, 'nsDS5ReplicaBindDN', f"uid={name},ou=Machines,{DEFAULT_SUFFIX}".encode('utf-8'))
])
-def _verify_etc_hosts():
- #Check if /etc/hosts is compatible with the test
- NEEDED_HOSTS = ( ('ldapkdc.example.com', '127.0.0.1'),
- ('ldapkdc1.example.com', '127.0.1.1'),
- ('ldapkdc2.example.com', '127.0.2.1'))
- found_hosts = {}
- with open('/etc/hosts','r') as f:
- for l in f:
- s = l.split()
- if len(s) < 2:
- continue
- for nh in NEEDED_HOSTS:
- if (s[0] == nh[1] and s[1] == nh[0]):
- found_hosts[s[1]] = True
- return len(found_hosts) == len(NEEDED_HOSTS)
-
-@pytest.mark.skipif(not _verify_etc_hosts(), reason="/etc/hosts does not contains the needed hosts.")
-@pytest.mark.skipif(True, reason="Test disabled because it requires specific kerberos requirement (server principal, keytab, etc ...")
-def test_gssapi_repl(topology_m2):
+@gssapi_ack
+def test_gssapi_repl(topology_m2_gssapi):
"""Test gssapi authenticated replication agreement of two suppliers using KDC
:id: 552850aa-afc3-473e-9c39-aae802b46f11
@@ -112,8 +95,8 @@ def test_gssapi_repl(topology_m2):
6. Test User should be created on M1 and M2 both
7. Test User should be created on M1 and M2 both
"""
- supplier1 = topology_m2.ms["supplier1"]
- supplier2 = topology_m2.ms["supplier2"]
+ supplier1 = topology_m2_gssapi.ms["supplier1"]
+ supplier2 = topology_m2_gssapi.ms["supplier2"]
# Create the locations on each supplier for the other to bind to.
_create_machine_ou(supplier1)
@@ -134,10 +117,9 @@ def test_gssapi_repl(topology_m2):
# Creating agreement from supplier 1 to supplier 2
# Set the replica bind method to sasl gssapi
- properties = {RA_NAME: r'meTo_$host:$port',
+ properties = {RA_NAME: 'meTo_' + supplier2.host + ':' + str(supplier2.port),
RA_METHOD: 'SASL/GSSAPI',
RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
- supplier1.agreement.delete(suffix=SUFFIX, consumer_host=supplier2.host, consumer_port=supplier2.port)
m1_m2_agmt = supplier1.agreement.create(suffix=SUFFIX, host=supplier2.host, port=supplier2.port, properties=properties)
if not m1_m2_agmt:
log.fatal("Fail to create a supplier -> supplier replica agreement")
@@ -147,10 +129,9 @@ def test_gssapi_repl(topology_m2):
# Creating agreement from supplier 2 to supplier 1
# Set the replica bind method to sasl gssapi
- properties = {RA_NAME: r'meTo_$host:$port',
+ properties = {RA_NAME: 'meTo_' + supplier1.host + ':' + str(supplier1.port),
RA_METHOD: 'SASL/GSSAPI',
RA_TRANSPORT_PROT: defaultProperties[REPLICATION_TRANSPORT]}
- supplier2.agreement.delete(suffix=SUFFIX, consumer_host=supplier1.host, consumer_port=supplier1.port)
m2_m1_agmt = supplier2.agreement.create(suffix=SUFFIX, host=supplier1.host, port=supplier1.port, properties=properties)
if not m2_m1_agmt:
log.fatal("Fail to create a supplier -> supplier replica agreement")
@@ -169,9 +150,15 @@ def test_gssapi_repl(topology_m2):
# Check replication is working...
if supplier1.testReplication(DEFAULT_SUFFIX, supplier2):
- log.info('Replication is working.')
+ log.info('Replication is working: supplier1 -> supplier2')
else:
- log.fatal('Replication is not working.')
+ log.fatal('Replication is not working: supplier1 -> supplier2')
+ assert False
+
+ if supplier2.testReplication(DEFAULT_SUFFIX, supplier1):
+ log.info('Replication is working: supplier2 -> supplier1')
+ else:
+ log.fatal('Replication is not working: supplier2 -> supplier1')
assert False
# Add a user to supplier 1
diff --git a/src/lib389/lib389/topologies.py b/src/lib389/lib389/topologies.py
index 33341f669..84e620cb3 100644
--- a/src/lib389/lib389/topologies.py
+++ b/src/lib389/lib389/topologies.py
@@ -499,6 +499,63 @@ def topology_m2(request):
topology.logcap = LogCapture()
return topology
+@pytest.fixture(scope="module")
+def topology_m2_gssapi(request):
+ """Create Replication Deployment with two suppliers with GSSAPI enabled.
+
+ Similar to topology_st_gssapi but for two suppliers. Configures Kerberos
+ realm, principals and keytabs for ldap/ldapkdc1.<domain> and ldap/ldapkdc2.<domain>,
+ SASL mappings, and disables SSL port on both instances so GSSAPI can be used.
+ """
+ hostname = socket.gethostname().split('.', 1)
+ assert len(hostname) == 2
+ domain = hostname[1]
+ REALM = domain.upper()
+ host_supplier_1 = 'ldapkdc1.' + domain
+ host_supplier_2 = 'ldapkdc2.' + domain
+
+ topology = create_topology({ReplicaRole.SUPPLIER: 2}, request=request,
+ cleanup_cb=lambda x: krb.destroy_realm())
+
+ supplier1 = topology.ms["supplier1"]
+ supplier2 = topology.ms["supplier2"]
+ supplier1.host = host_supplier_1
+ supplier2.host = host_supplier_2
+
+ krb = MitKrb5(realm=REALM, debug=DEBUGGING)
+ if krb.check_realm():
+ krb.destroy_realm()
+ krb.create_realm()
+
+ krb.create_principal(principal=f'ldap/{host_supplier_1}')
+ krb.create_principal(principal=f'ldap/{host_supplier_2}')
+ krb.create_keytab(principal=f'ldap/{host_supplier_1}', keytab='/etc/krb5.keytab')
+ krb.create_keytab(principal=f'ldap/{host_supplier_2}', keytab='/etc/krb5.keytab')
+
+ os.chown('/etc/krb5.keytab', supplier1.get_user_uid(), supplier1.get_group_gid())
+
+ for inst, host in [(supplier1, host_supplier_1), (supplier2, host_supplier_2)]:
+ saslmappings = SaslMappings(inst)
+ for m in saslmappings.list():
+ m.delete()
+ saslmappings.create(properties={
+ 'cn': 'suffix map',
+ 'nsSaslMapRegexString': '\\(.*\\)',
+ 'nsSaslMapBaseDNTemplate': inst.creation_suffix,
+ 'nsSaslMapFilterTemplate': '(uid=\\1)'
+ })
+ inst.realm = krb
+ inst.config.set('nsslapd-localhost', host)
+ inst.sslport = None
+
+ supplier1.restart()
+ supplier2.restart()
+ supplier1.clearTmpDir(__file__)
+ supplier2.clearTmpDir(__file__)
+
+ topology.logcap = LogCapture()
+ return topology
+
@pytest.fixture(scope="module")
def topology_m3(request):
--
2.52.0

View File

@ -0,0 +1,260 @@
From a4ae29afc6547e8231b933cfa1b95d7f7b37a25c Mon Sep 17 00:00:00 2001
From: Lenka Doudova <lryznaro@redhat.com>
Date: Tue, 10 Feb 2026 05:45:32 +0100
Subject: [PATCH] Issue 6753 - Port ticket 49039 test
Description:
Port ticket 49039 test into
dirsrvtests/tests/suites/password/pwp_test.py
Relates: #6753
Author: Lenka Doudova
Assisted by: Cursor
Reviewer: Barbora Simonova, Viktor Ashirov
---
dirsrvtests/tests/suites/password/pwp_test.py | 83 +++++++++++-
dirsrvtests/tests/tickets/ticket49039_test.py | 127 ------------------
2 files changed, 82 insertions(+), 128 deletions(-)
delete mode 100644 dirsrvtests/tests/tickets/ticket49039_test.py
diff --git a/dirsrvtests/tests/suites/password/pwp_test.py b/dirsrvtests/tests/suites/password/pwp_test.py
index 663d9bea9..6dae08cb2 100644
--- a/dirsrvtests/tests/suites/password/pwp_test.py
+++ b/dirsrvtests/tests/suites/password/pwp_test.py
@@ -9,11 +9,14 @@
"""
import os
+import subprocess
import pytest
from lib389.topologies import topology_st as topo
from lib389.idm.user import UserAccounts, UserAccount
-from lib389._constants import DEFAULT_SUFFIX
+from lib389.idm.directorymanager import DirectoryManager
+from lib389._constants import DEFAULT_SUFFIX, PASSWORD
from lib389.config import Config
+from lib389.pwpolicy import PwPolicyManager
from lib389.idm.group import Group
from lib389.utils import ds_is_older
import ldap
@@ -512,6 +515,84 @@ def test_passwordlockout(topo, _fix_password):
_change_password_with_own(topo, user.dn, 'dby3rs2', 'secreter')
+def test_password_must_change_ignores_min_age(topo):
+ """Test that passwordMinAge does not block password update when the password was reset.
+
+ :id: a1b2c3d4-e5f6-4903-9abc-def012345678
+ :setup: Standalone instance
+ :steps:
+ 1. Enable TLS (for ldappasswd StartTLS)
+ 2. Set global policy via PwPolicyManager: passwordMustChange, passwordExp,
+ passwordMaxAge, passwordMinAge (high), passwordChange
+ 3. Bind as Directory Manager
+ 4. Create user
+ 5. Reset user password as Directory Manager
+ 6. User binds and changes own password (must succeed; min age must not block)
+ 7. Rebind as Directory Manager, reset user password again
+ 8. Run ldappasswd as user (StartTLS) to change password to password2
+ 9. Bind as user with password2 to verify
+ 10. Cleanup: delete user
+ :expectedresults:
+ 1. TLS enabled
+ 2. Policy set successfully
+ 3. Bind succeeds
+ 4. User created
+ 5. Reset succeeds
+ 6. User password change succeeds (min age does not block after reset)
+ 7. Reset succeeds
+ 8. ldappasswd succeeds
+ 9. Bind succeeds
+ 10. User deleted
+ """
+
+ topo.standalone.enable_tls()
+
+ policy = PwPolicyManager(topo.standalone)
+ policy.set_global_policy(properties={'nsslapd-pwpolicy-local': 'on',
+ 'passwordMustChange': 'on',
+ 'passwordExp': 'on',
+ 'passwordMaxAge': '86400000',
+ 'passwordMinAge': '8640000',
+ 'passwordChange': 'on'})
+ dm = DirectoryManager(topo.standalone)
+ dm.bind()
+
+ user = _create_user(topo, 'user', 'Test User', '1002', PASSWORD)
+ try:
+ # Reset password as Directory Manager
+ user.replace('userpassword', PASSWORD)
+ time.sleep(1)
+
+ # Reset password as user (must succeed; min age must not block after reset)
+ user.rebind(PASSWORD)
+ user.replace('userpassword', PASSWORD)
+ time.sleep(1)
+
+ # Reset again as Directory Manager
+ dm.rebind(PASSWORD)
+ user.replace('userpassword', PASSWORD)
+ time.sleep(1)
+
+ # Change password through ldappasswd as user to ensure functionality
+ env = os.environ.copy()
+ env['LDAPTLS_CACERTDIR'] = topo.standalone.get_cert_dir()
+ cmd = [
+ 'ldappasswd',
+ '-ZZ','-H', f"ldap://{topo.standalone.host}:{topo.standalone.port}",
+ '-D', user.dn, '-w', PASSWORD,
+ '-a', PASSWORD, '-s', 'password2',
+ user.dn,
+ ]
+ result = subprocess.run(cmd, env=env, capture_output=True, text=True)
+ assert result.returncode == 0, f'ldappasswd failed: {result.stderr}'
+
+ # Bind as user with new password
+ user.bind('password2')
+ finally:
+ dm.rebind(PASSWORD)
+ user.delete()
+
+
if __name__ == "__main__":
CURRENT_FILE = os.path.realpath(__file__)
pytest.main("-s -v %s" % CURRENT_FILE)
diff --git a/dirsrvtests/tests/tickets/ticket49039_test.py b/dirsrvtests/tests/tickets/ticket49039_test.py
deleted file mode 100644
index 0313f69a3..000000000
--- a/dirsrvtests/tests/tickets/ticket49039_test.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# --- BEGIN COPYRIGHT BLOCK ---
-# Copyright (C) 2022 Red Hat, Inc.
-# All rights reserved.
-#
-# License: GPL (version 3 or any later version).
-# See LICENSE for details.
-# --- END COPYRIGHT BLOCK ---
-#
-import time
-import ldap
-import logging
-import pytest
-import os
-from lib389 import Entry
-from lib389._constants import *
-from lib389.properties import *
-from lib389.tasks import *
-from lib389.utils import *
-from lib389.topologies import topology_st as topo
-from lib389.pwpolicy import PwPolicyManager
-
-
-pytestmark = pytest.mark.tier2
-
-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__)
-
-USER_DN = 'uid=user,dc=example,dc=com'
-
-
-def test_ticket49039(topo):
- """Test "password must change" verses "password min age". Min age should not
- block password update if the password was reset.
- """
-
- # Setup SSL (for ldappasswd test)
- topo.standalone.enable_tls()
-
- # Configure password policy
- try:
- policy = PwPolicyManager(topo.standalone)
- policy.set_global_policy(properties={'nsslapd-pwpolicy-local': 'on',
- 'passwordMustChange': 'on',
- 'passwordExp': 'on',
- 'passwordMaxAge': '86400000',
- 'passwordMinAge': '8640000',
- 'passwordChange': 'on'})
- except ldap.LDAPError as e:
- log.fatal('Failed to set password policy: ' + str(e))
-
- # Add user, bind, and set password
- try:
- topo.standalone.add_s(Entry((USER_DN, {
- 'objectclass': 'top extensibleObject'.split(),
- 'uid': 'user1',
- 'userpassword': PASSWORD
- })))
- except ldap.LDAPError as e:
- log.fatal('Failed to add user: error ' + e.args[0]['desc'])
- assert False
-
- # Reset password as RootDN
- try:
- topo.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE, 'userpassword', ensure_bytes(PASSWORD))])
- except ldap.LDAPError as e:
- log.fatal('Failed to bind: error ' + e.args[0]['desc'])
- assert False
-
- time.sleep(1)
-
- # Reset password as user
- try:
- topo.standalone.simple_bind_s(USER_DN, PASSWORD)
- except ldap.LDAPError as e:
- log.fatal('Failed to bind: error ' + e.args[0]['desc'])
- assert False
-
- try:
- topo.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE, 'userpassword', ensure_bytes(PASSWORD))])
- except ldap.LDAPError as e:
- log.fatal('Failed to change password: error ' + e.args[0]['desc'])
- assert False
-
- ###################################
- # Make sure ldappasswd also works
- ###################################
-
- # Reset password as RootDN
- try:
- topo.standalone.simple_bind_s(DN_DM, PASSWORD)
- except ldap.LDAPError as e:
- log.fatal('Failed to bind as rootdn: error ' + e.args[0]['desc'])
- assert False
-
- try:
- topo.standalone.modify_s(USER_DN, [(ldap.MOD_REPLACE, 'userpassword', ensure_bytes(PASSWORD))])
- except ldap.LDAPError as e:
- log.fatal('Failed to bind: error ' + e.args[0]['desc'])
- assert False
-
- time.sleep(1)
-
- # Run ldappasswd as the User.
- os.environ["LDAPTLS_CACERTDIR"] = topo.standalone.get_cert_dir()
- cmd = ('ldappasswd' + ' -h ' + topo.standalone.host + ' -Z -p 38901 -D ' + USER_DN +
- ' -w password -a password -s password2 ' + USER_DN)
- os.system(cmd)
- time.sleep(1)
-
- try:
- topo.standalone.simple_bind_s(USER_DN, "password2")
- except ldap.LDAPError as e:
- log.fatal('Failed to bind: error ' + e.args[0]['desc'])
- assert False
-
- log.info('Test Passed')
-
-
-if __name__ == '__main__':
- # Run isolated
- # -s for DEBUG mode
- CURRENT_FILE = os.path.realpath(__file__)
- pytest.main("-s %s" % CURRENT_FILE)
--
2.52.0

View File

@ -0,0 +1,952 @@
From 0e2d9c4288873446dcb3d8bff61c558ff2b6681a 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

@ -0,0 +1,86 @@
From 8b4dbf35ace326e5b836982d428e7029313a2247 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

@ -0,0 +1,40 @@
From ffe1909e69ab2aecef396f31cf95cdcecd992782 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Mon, 23 Feb 2026 12:10:32 -0500
Subject: [PATCH] Issue 7273 - In a chaining environment binding as remote user
causes an invalid error in the logs
Description:
In a database link/chaining environment you can bind as a remote user, and
this triggers an error when trying to "upgrade_on_bind" as the user does not
locally have a userpassword since it's remote. There is no strong case to
log an error in this situation.
relates: http://github.com/389ds/389-ds-base/issues/7273
Reviewed by: vashirov(Thanks!)
---
ldap/servers/slapd/pw.c | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/ldap/servers/slapd/pw.c b/ldap/servers/slapd/pw.c
index c53ecf23d..88e32537d 100644
--- a/ldap/servers/slapd/pw.c
+++ b/ldap/servers/slapd/pw.c
@@ -3564,10 +3564,8 @@ int32_t update_pw_encoding(Slapi_PBlock *orig_pb, Slapi_Entry *e, Slapi_DN *sdn,
* Does the entry have a pw?
*/
if (e == NULL || slapi_entry_attr_find(e, SLAPI_USERPWD_ATTR, &pw) != 0 || pw == NULL) {
- slapi_log_err(SLAPI_LOG_WARNING,
- "update_pw_encoding", "Could not read password attribute on '%s'\n",
- dn);
- res = -1;
+ /* The entry does not have a userpassword attribute so there is nothing to do.
+ * This typically happens when chaining is involved. */
goto free_and_return;
}
--
2.53.0

View File

@ -0,0 +1,46 @@
From 3a233116a564fc339aee1021913c995504951d86 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <spichugi@redhat.com>
Date: Tue, 24 Feb 2026 09:28:24 -0800
Subject: [PATCH] Issue 7279 - UI - Fix typo in export certificate dialog
(#7280)
Description: Fix typo "cetificate" -> "certificate" in the
export certificate dialog message.
Fixes: https://github.com/389ds/389-ds-base/issues/7279
Reviewed by: @vashirov (Thanks!)
---
src/cockpit/389-console/po/ja.po | 2 +-
src/cockpit/389-console/src/lib/security/securityModals.jsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/cockpit/389-console/po/ja.po b/src/cockpit/389-console/po/ja.po
index 27d21bf7a..12c10ded4 100644
--- a/src/cockpit/389-console/po/ja.po
+++ b/src/cockpit/389-console/po/ja.po
@@ -11560,7 +11560,7 @@ msgstr "証明書のエクスポート:"
#: src/lib/security/securityModals.jsx:58
msgid ""
"Enter the full path and file name, if the path portion is omitted the "
-"cetificate is written to the server's certificate directory "
+"certificate is written to the server's certificate directory "
msgstr ""
"ファイル名を含むフルパスを入力してください。パス部分を省略した場合、証明書は"
"サーバの証明書ディレクトリに書き込まれます。"
diff --git a/src/cockpit/389-console/src/lib/security/securityModals.jsx b/src/cockpit/389-console/src/lib/security/securityModals.jsx
index 4d6631fd2..f32d47596 100644
--- a/src/cockpit/389-console/src/lib/security/securityModals.jsx
+++ b/src/cockpit/389-console/src/lib/security/securityModals.jsx
@@ -55,7 +55,7 @@ export class ExportCertModal extends React.Component {
}
const title = <>{_("Export Certificate:")} &nbsp;&nbsp;<i>{nickName}</i></>;
- const desc = <>{_("Enter the full path and file name, if the path portion is omitted the cetificate is written to the server's certificate directory ")}<i>{certDir}</i></>;
+ const desc = <>{_("Enter the full path and file name, if the path portion is omitted the certificate is written to the server's certificate directory ")}<i>{certDir}</i></>;
return (
<Modal
--
2.53.0

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
From f4d8df66187c0d1db6c9f5c9aa55946a8564de4b Mon Sep 17 00:00:00 2001
From: Sam Morris <sam@robots.org.uk>
Date: Wed, 25 Feb 2026 12:30:01 +0000
Subject: [PATCH] Issue 7246 - correct formatting of 'Gen as CSN' in dsctl
get-nsstate output (#7247)
Description: CSNs are formatted as hexadecimal, but the replica id and
sequence number are displayed in decomal.
Fix: use correct format specifiers for hexadecimal output.
Fixes: https://github.com/389ds/389-ds-base/issues/7246
Signed-off-by: Sam Morris <sam@robots.org.uk>
---
src/lib389/lib389/dseldif.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/lib389/lib389/dseldif.py b/src/lib389/lib389/dseldif.py
index 7834d9468..eac17aa4c 100644
--- a/src/lib389/lib389/dseldif.py
+++ b/src/lib389/lib389/dseldif.py
@@ -407,7 +407,7 @@ class DSEldif(DSLint):
'endian': endian,
'rid': str(rid),
'gen_time': str(sampled_time),
- 'gencsn': "%08x%04d%04d0000" % (sampled_time, seq_num, rid),
+ 'gencsn': "%08x%04x%04x0000" % (sampled_time, seq_num, rid),
'gen_time_str': time.ctime(sampled_time),
'local_offset': str(local_offset),
'local_offset_str': print_nice_time(local_offset),
--
2.53.0

View File

@ -0,0 +1,93 @@
From 2e424110def2e3998f6045e136fb0d43f47b7f5a 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
Description:
A vulnerability was found in the 389 Directory Server.
The 389 Directory Server present a risk of heap buffer overflow that
can be exploited to excute a Denial of Service and potential Remote
Code Execution
References:
- https://access.redhat.com/security/cve/CVE-2025-14905
- https://bugzilla.redhat.com/show_bug.cgi?id=2423624
---
ldap/servers/slapd/schema.c | 47 ++++++++++++++++++++++++++++++-------
1 file changed, 38 insertions(+), 9 deletions(-)
diff --git a/ldap/servers/slapd/schema.c b/ldap/servers/slapd/schema.c
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)
const char *attr_desc, *syntaxoid;
char *outp, syntaxlengthbuf[128];
int i;
+ int nb_aliases = 0;
vals[0] = &val;
@@ -1435,6 +1436,7 @@ schema_attr_enum_callback(struct asyntaxinfo *asip, void *arg)
if (asip->asi_aliases != NULL) {
for (i = 0; asip->asi_aliases[i] != NULL; ++i) {
aliaslen += strlen(asip->asi_aliases[i]);
+ nb_aliases++;
}
}
@@ -1452,15 +1454,42 @@ schema_attr_enum_callback(struct asyntaxinfo *asip, void *arg)
* XXX: 256 is a magic number... it must be big enough to account for
* all of the fixed sized items we output.
*/
- sizedbuffer_allocate(aew->psbAttrTypes, 256 + strlen(asip->asi_oid) +
- strlen(asip->asi_name) +
- aliaslen + strlen_null_ok(attr_desc) +
- strlen(syntaxoid) +
- strlen_null_ok(asip->asi_superior) +
- strlen_null_ok(asip->asi_mr_equality) +
- strlen_null_ok(asip->asi_mr_ordering) +
- strlen_null_ok(asip->asi_mr_substring) +
- strcat_extensions(NULL, asip->asi_extensions));
+ {
+ int asi_oid_strlen = strlen(asip->asi_oid) + 8; /* "( %s NAME " */
+ int asi_name_strlen = strlen(asip->asi_name) + 6; /* "( '%s' ...)" */
+ int asi_aliases_strlen = aliaslen + nb_aliases * 3; /* "'%s' " */
+ int asi_desc_strlen = strlen_null_ok(attr_desc) + 7; /* "DESC '%s'" */
+ int asi_syntaxoid_strlen = strlen("SYNTAX ") + strlen(syntaxoid) + strlen(syntaxlengthbuf);
+ int asi_superior_strlen = strlen("SUP ") + strlen_null_ok(asip->asi_superior);
+ int asi_mr_equality_strlen = strlen("EQUALITY ") + strlen_null_ok(asip->asi_mr_equality);
+ int asi_mr_ordering_strlen = strlen("ORDERING ") + strlen_null_ok(asip->asi_mr_ordering);
+ int asi_mr_substring_strlen = strlen("SUBSTR ") + strlen_null_ok(asip->asi_mr_substring);
+ int asi_flags_strlen = strlen("SINGLE-VALUE ") +
+ strlen(schema_obsolete_with_spaces) +
+ strlen(schema_collective_with_spaces) +
+ strlen(schema_nousermod_with_spaces) +
+ strlen("USAGE distributedOperation ") +
+ strlen("USAGE dSAOperation ") +
+ strlen("USAGE directoryOperation ");
+ int asi_extension_strlen = strcat_extensions(NULL, asip->asi_extensions);
+
+ if (aew->enquote_sup_oc) {
+ /* it enquote the syntax oid */
+ asi_syntaxoid_strlen += 2;
+ }
+
+ sizedbuffer_allocate(aew->psbAttrTypes, 256 + asi_oid_strlen +
+ asi_name_strlen +
+ asi_aliases_strlen +
+ asi_desc_strlen +
+ asi_syntaxoid_strlen +
+ asi_superior_strlen +
+ asi_mr_equality_strlen +
+ asi_mr_ordering_strlen +
+ asi_mr_substring_strlen +
+ asi_extension_strlen +
+ asi_flags_strlen);
+ }
/*
* Overall strategy is to maintain a pointer to the next location in
--
2.53.0

View File

@ -0,0 +1,654 @@
From c7e1eb08eb36fd9a16c745935f56fa4a4b2a99df Mon Sep 17 00:00:00 2001
From: progier389 <progier@redhat.com>
Date: Wed, 25 Feb 2026 18:00:24 +0100
Subject: [PATCH] Issue 7267 - MDB_BAD_VALSIZE error when updating index
(#7268)
* Issue 7267 - MDB_BAD_VALSIZE error when updating index
* Improve import log when writer fails
* Fix Sourcery AI comments
* Fix INDEX_KEY_LENGTH typo
Problem with the key prefix handling when key is too long and must be hashed.
The issue is that the # that is prepended is not reset when iterating over the valueset values (Ending up with very long prefix)
Also refactored the code to avoid duplicate the code that prepare the key from the attribute value (used when updating the index or retrieving a value from an index)
Issue: #7267
Reviewed by: @tbordaz , @vashirov (Thanks!)
Co-authored-by: Viktor Ashirov <vashirov@redhat.com>
---------
Co-authored-by: Viktor Ashirov <vashirov@redhat.com>
---
.../tests/suites/indexes/regression_test.py | 58 +++++++
ldap/servers/slapd/back-ldbm/attrcrypt.h | 2 +-
ldap/servers/slapd/back-ldbm/back-ldbm.h | 2 +
.../slapd/back-ldbm/db-bdb/bdb_import.c | 39 +----
.../back-ldbm/db-mdb/mdb_import_threads.c | 46 ++++-
ldap/servers/slapd/back-ldbm/index.c | 161 +++++++-----------
ldap/servers/slapd/back-ldbm/ldbm_attrcrypt.c | 13 +-
.../servers/slapd/back-ldbm/proto-back-ldbm.h | 2 +-
ldap/servers/slapd/log.c | 43 +++++
ldap/servers/slapd/slapi-private.h | 2 +
10 files changed, 224 insertions(+), 144 deletions(-)
diff --git a/dirsrvtests/tests/suites/indexes/regression_test.py b/dirsrvtests/tests/suites/indexes/regression_test.py
index 8176d6db0..a4218a2b5 100644
--- a/dirsrvtests/tests/suites/indexes/regression_test.py
+++ b/dirsrvtests/tests/suites/indexes/regression_test.py
@@ -1022,6 +1022,64 @@ def test_idl_range_limit(topo, add_some_entries):
assert len(entries) == 3
+def test_large_multivalued_sn_attribute(topo):
+ """Test adding a user entry with 512 values for sn attribute, each 512 bytes
+
+ :id: 8f2a9b3c-e8d7-11ef-9a5f-482ae39447e5
+ :setup: Standalone Instance
+ :steps:
+ 1. Create a user with 512 sn values, each 512 bytes long
+ 2. Verify the user was created successfully
+ 3. Search for the user and verify all sn values are present
+ 4. Clean up the user entry
+ :expectedresults:
+ 1. User is created successfully
+ 2. User entry exists
+ 3. All 512 sn values are present and have correct length
+ 4. User is deleted successfully
+ """
+
+ inst = topo.standalone
+ users = UserAccounts(inst, DEFAULT_SUFFIX)
+
+ log.info("Creating user with 512 sn values, each 512 bytes")
+
+ # Generate 512 unique sn values, each 512 bytes long
+ # Use a pattern that makes each value unique but predictable
+ sn_values = []
+ for i in range(512):
+ # Create a 512-byte value with unique identifier at the start
+ value = f'sn_value_{i:04d}_' + 'x' * (512 - len(f'sn_value_{i:04d}_'))
+ sn_values.append(value)
+
+ # Create the user with first sn value
+ user_name = 'test_user_large_sn'
+ user = users.create(properties={
+ 'uid': user_name,
+ 'cn': user_name,
+ 'sn': sn_values,
+ 'uidNumber': '99999',
+ 'gidNumber': '99999',
+ 'homeDirectory': f'/home/{user_name}'
+ })
+
+ # Verify the entry was created and has all sn values
+ log.info("Verifying all sn values are present")
+ sn_attr_values = user.get_attr_vals_utf8('sn')
+
+ assert len(sn_attr_values) == 512, f"Expected 512 sn values, got {len(sn_attr_values)}"
+
+ # Verify each value has the correct length
+ for idx, value in enumerate(sn_attr_values):
+ assert len(value) == 512, f"sn value {idx} has length {len(value)}, expected 512"
+
+ log.info("Successfully created and verified user with 512 sn values of 512 bytes each")
+
+ # Clean up
+ user.delete()
+ log.info("User entry deleted successfully")
+
+
if __name__ == "__main__":
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/back-ldbm/attrcrypt.h b/ldap/servers/slapd/back-ldbm/attrcrypt.h
index d653ba951..dcbea80fe 100644
--- a/ldap/servers/slapd/back-ldbm/attrcrypt.h
+++ b/ldap/servers/slapd/back-ldbm/attrcrypt.h
@@ -10,7 +10,7 @@
#include <config.h>
#endif
-/* Private tructures and #defines used in the attribute encryption code. */
+/* Private structures and #defines used in the attribute encryption code. */
#ifndef _ATTRCRYPT_H_
#define _ATTRCRYPT_H_
diff --git a/ldap/servers/slapd/back-ldbm/back-ldbm.h b/ldap/servers/slapd/back-ldbm/back-ldbm.h
index e23e7ff43..92aa1ddbb 100644
--- a/ldap/servers/slapd/back-ldbm/back-ldbm.h
+++ b/ldap/servers/slapd/back-ldbm/back-ldbm.h
@@ -104,6 +104,8 @@ typedef unsigned short u_int16_t;
*/
#define BE_CHANGELOG_FILE "replication_changelog"
+#define INDEX_KEY_LENGTH(lenval,lenprefix) (lenval+lenprefix+2)
+
#define BDB_IMPL "bdb"
#define BDB_BACKEND "libback-ldbm" /* This backend plugin */
#define BDB_NEWIDL "newidl" /* new idl format */
diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c
index a6cb10aec..489433801 100644
--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c
+++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_import.c
@@ -75,9 +75,9 @@ static IDList *bdb_idl_union_allids(backend *be, struct attrinfo *ai, IDList *a,
#define DEBUG_SUBCOUNT_MSG(msg, ...) { debug_subcount(__FUNCTION__, __LINE__, (msg), __VA_ARGS__); }
#define DUMP_SUBCOUNT_KEY(msg, key, ret) { debug_subcount(__FUNCTION__, __LINE__, "ret=%d size=%u ulen=%u doff=%u dlen=%u", \
ret, (key).size, (key).ulen, (key).doff, (key).dlen); \
- if (ret == 0) hexadump(msg, (key).data, 0, (key).size); \
+ if (ret == 0) slapi_log_hexadump(SLAPI_LOG_INFO, msg, (key).data, (key).size); \
else if (ret == DB_BUFFER_SMALL) \
- hexadump(msg, (key).data, 0, (key).ulen); }
+ slapi_log_hexadump(SLAPI_LOG_INFO, msg, (key).data, (key).ulen); }
static void
debug_subcount(const char *funcname, int line, char *msg, ...)
@@ -90,41 +90,6 @@ debug_subcount(const char *funcname, int line, char *msg, ...)
slapi_log_err(SLAPI_LOG_INFO, (char*)funcname, "DEBUG SUBCOUNT [%d] %s\n", line, buff);
}
-/*
- * Dump a memory buffer in hexa and ascii in error log
- *
- * addr - The memory buffer address.
- * len - The memory buffer lenght.
- */
-static void
-hexadump(char *msg, const void *addr, size_t offset, size_t len)
-{
-#define HEXADUMP_TAB 4
-/* 4 characters per bytes: 2 hexa digits, 1 space and the ascii */
-#define HEXADUMP_BUF_SIZE (4*16+HEXADUMP_TAB)
- char hexdigit[] = "0123456789ABCDEF";
-
- const unsigned char *pt = addr;
- char buff[HEXADUMP_BUF_SIZE+1];
- memset (buff, ' ', HEXADUMP_BUF_SIZE);
- buff[HEXADUMP_BUF_SIZE] = '\0';
- while (len > 0) {
- int dpl;
- for (dpl = 0; dpl < 16 && len>0; dpl++, len--) {
- buff[3*dpl] = hexdigit[((*pt) >> 4) & 0xf];
- buff[3*dpl+1] = hexdigit[(*pt) & 0xf];
- buff[3*16+HEXADUMP_TAB+dpl] = (*pt>=0x20 && *pt<0x7f) ? *pt : '.';
- pt++;
- }
- for (;dpl < 16; dpl++) {
- buff[3*dpl] = ' ';
- buff[3*dpl+1] = ' ';
- buff[3*16+HEXADUMP_TAB+dpl] = ' ';
- }
- slapi_log_err(SLAPI_LOG_INFO, msg, "[0x%08lx] %s\n", offset, buff);
- offset += 16;
- }
-}
#else
#define DEBUG_SUBCOUNT_MSG(msg, ...)
#define DUMP_SUBCOUNT_KEY(msg, key, ret)
diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c
index 65b29343e..b969790da 100644
--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c
+++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_import_threads.c
@@ -1123,6 +1123,21 @@ dbmdb_import_entry_info_by_backentry(mdb_privdb_t *db, BulkQueueData_t *bqdata,
return dnrc;
}
+/* Log wqelmt details */
+void
+log_wqelmt(int loglvl, char *fname, WorkerQueueData_t *wqelmt)
+{
+ if (wqelmt->dn) {
+ slapi_log_err(loglvl, fname, "log_wqelmt: dn=%s\n", wqelmt->dn);
+ }
+ if (wqelmt->filename && wqelmt->lineno) {
+ slapi_log_err(loglvl, fname, "log_wqelmt: ldif=%s[%d]\n", wqelmt->filename, wqelmt->lineno);
+ }
+ if (wqelmt->data) {
+ size_t len = wqelmt->datalen ? wqelmt->datalen : strlen(wqelmt->data);
+ slapi_log_hexadump(loglvl, "log_wqelmt:data", wqelmt->data, len);
+ }
+}
/* producer thread for ldif import case:
* read through the given file list, parsing entries (str2entry), assigning
@@ -1255,6 +1270,7 @@ dbmdb_import_producer(void *param)
import_log_notice(job, SLAPI_LOG_ERR, "dbmdb_import_producer",
"ns_slapd software error: unexpected dbmdb_import_entry_info return code: %d.",
wqelmt.dnrc);
+ log_wqelmt(SLAPI_LOG_ERR, "dbmdb_import_producer", &wqelmt);
abort();
case DNRC_OK:
case DNRC_SUFFIX:
@@ -1758,6 +1774,7 @@ dbmdb_index_producer(void *param)
import_log_notice(job, SLAPI_LOG_ERR, "dbmdb_index_producer",
"ns_slapd software error: unexpected dbmdb_import_entry_info return code: %d.",
tmpslot.dnrc);
+ log_wqelmt(SLAPI_LOG_ERR, "dbmdb_index_producer", &tmpslot);
abort();
case DNRC_OK:
case DNRC_SUFFIX:
@@ -3936,10 +3953,24 @@ dbmdb_import_writer(void*param)
if (!txn) {
MDB_STAT_STEP(stats, MDB_STAT_TXNSTART, stats_enabled);
rc = TXN_BEGIN(ctx->ctx->env, NULL, 0, &txn);
+ if (rc) {
+ slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
+ "Failed to begin a txn. Error is 0x%x: %s.\n",
+ rc, mdb_strerror(rc));
+ }
}
if (!rc) {
MDB_STAT_STEP(stats, MDB_STAT_WRITE, stats_enabled);
rc = MDB_PUT(txn, slot->dbi->dbi, &slot->key, &slot->data, 0);
+ if (rc) {
+ slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
+ "Failed to write record in dbi %s. Error is 0x%x: %s.\n",
+ slot->dbi->dbname, rc, mdb_strerror(rc));
+ slapi_log_hexadump(SLAPI_LOG_ERR, "dbmdb_import_writer:key",
+ slot->key.mv_data, slot->key.mv_size);
+ slapi_log_hexadump(SLAPI_LOG_ERR, "dbmdb_import_writer:data",
+ slot->data.mv_data, slot->data.mv_size);
+ }
}
MDB_STAT_STEP(stats, MDB_STAT_RUN, stats_enabled);
nextslot = slot->next;
@@ -3953,6 +3984,9 @@ dbmdb_import_writer(void*param)
rc = TXN_COMMIT(txn);
MDB_STAT_STEP(stats, MDB_STAT_RUN, stats_enabled);
if (rc) {
+ slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
+ "Failed to commit the txn. Error is 0x%x: %s.\n",
+ rc, mdb_strerror(rc));
break;
}
count = 0;
@@ -3965,6 +3999,10 @@ dbmdb_import_writer(void*param)
MDB_STAT_STEP(stats, MDB_STAT_RUN, stats_enabled);
if (!rc) {
txn = NULL;
+ } else {
+ slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
+ "Failed to commit the txn. Error is 0x%x: %s.\n",
+ rc, mdb_strerror(rc));
}
}
if (txn) {
@@ -3977,13 +4015,17 @@ dbmdb_import_writer(void*param)
if (!rc) {
/* Ensure that all data are written on disk */
rc = mdb_env_sync(ctx->ctx->env, 1);
+ if (rc) {
+ slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
+ "mdb_env_sync failed. Error is 0x%x: %s.\n",
+ rc, mdb_strerror(rc));
+ }
}
MDB_STAT_END(stats, stats_enabled);
if (rc) {
slapi_log_err(SLAPI_LOG_ERR, "dbmdb_import_writer",
- "Failed to write in the database. Error is 0x%x: %s.\n",
- rc, mdb_strerror(rc));
+ "Aborting import after failure.\n");
thread_abort(info);
} else if (stats_enabled) {
char buf[200];
diff --git a/ldap/servers/slapd/back-ldbm/index.c b/ldap/servers/slapd/back-ldbm/index.c
index a5004be19..c108bce3c 100644
--- a/ldap/servers/slapd/back-ldbm/index.c
+++ b/ldap/servers/slapd/back-ldbm/index.c
@@ -881,6 +881,67 @@ index_read(
return index_read_ext(be, (char *)type, indextype, val, txn, err, NULL);
}
+/* Prepare an index key (hashed if too long, encrypted if needed from attribute value */
+int
+prepare_key(backend *be, struct attrinfo *a, char **buf, size_t *buflen,
+ int flags, const char *prefix, const struct berval *bvp, dbi_val_t *key)
+{
+ /* Key format is [Hash?] [prefix] [val] [\0] */
+ struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private;
+ size_t plen = strlen(prefix);
+ struct berval *hashed_bvp = NULL;
+ struct berval *encrypted_bvp = NULL;
+ int rc = 0;
+
+ /* Hash large index key if necessary */
+ if (INDEX_KEY_LENGTH(bvp->bv_len,plen) >= li->li_max_key_len) {
+ rc = attrcrypt_hash_large_index_key(be, prefix, a, bvp, &hashed_bvp);
+ if (rc) {
+ slapi_log_err(SLAPI_LOG_ERR, "index_read_ext_allids",
+ "Failed to hash large index key for %s\n", a->ai_type);
+ return rc;
+ } else {
+ bvp = hashed_bvp;
+ }
+ }
+
+ /* Encrypt the index key if necessary */
+ if (rc == 0 && a->ai_attrcrypt && (0 == (flags & BE_INDEX_DONT_ENCRYPT))) {
+ rc = attrcrypt_encrypt_index_key(be, a, bvp, &encrypted_bvp);
+ if (rc) {
+ slapi_log_err(SLAPI_LOG_ERR, "addordel_values_sv",
+ "Failed to encrypt index key for %s\n", a->ai_type);
+ } else {
+ bvp = encrypted_bvp;
+ }
+ }
+ if (hashed_bvp) {
+ prefix = slapi_ch_smprintf("%c%s",HASH_PREFIX, prefix);
+ plen++;
+ }
+ if (buf && buflen) {
+ if (plen+bvp->bv_len+1 > *buflen) {
+ *buflen = plen+bvp->bv_len+1;
+ *buf = slapi_ch_realloc(*buf, *buflen);
+ }
+ dblayer_value_concat(be, key, *buf, *buflen, prefix, plen, bvp->bv_val, bvp->bv_len, "", 1);
+ } else {
+ dblayer_value_concat(be, key, NULL, 0, prefix, plen, bvp->bv_val, bvp->bv_len, "", 1);
+ }
+
+ if (hashed_bvp) {
+ ber_bvfree(hashed_bvp);
+ hashed_bvp = NULL;
+ slapi_ch_free_string((char**)&prefix);
+ }
+ if (encrypted_bvp) {
+ ber_bvfree(encrypted_bvp);
+ encrypted_bvp = NULL;
+ }
+ return rc;
+}
+
+
/*
* Extended version of index_read.
* The unindexed flag can be used to distinguish between a
@@ -917,7 +978,6 @@ index_read_ext_allids(
struct berval *hashed_val = NULL;
int is_and = 0;
unsigned int ai_flags = 0;
- struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private;
*err = 0;
@@ -1028,36 +1088,7 @@ index_read_ext_allids(
}
if (val != NULL) {
- size_t vlen;
- int ret = 0;
-
- /* If necessary, hash this index key */
- if (val->bv_len >= li->li_max_key_len) {
- ret = attrcrypt_hash_large_index_key(be, &prefix, ai, val, &hashed_val);
- if (ret) {
- slapi_log_err(SLAPI_LOG_ERR, "index_read_ext_allids",
- "Failed to hash large index key for %s\n", basetype);
- *err = DBI_RC_OTHER;
- index_free_prefix(prefix);
- slapi_ch_free_string(&basetmp);
- return (NULL);
- }
- if (hashed_val) {
- val = hashed_val;
- }
- }
- /* If necessary, encrypt this index key */
- ret = attrcrypt_encrypt_index_key(be, ai, val, &encrypted_val);
- if (ret) {
- slapi_log_err(SLAPI_LOG_ERR, "index_read_ext_allids",
- "Failed to encrypt index key for %s\n", basetype);
- }
- if (encrypted_val) {
- val = encrypted_val;
- }
- vlen = val->bv_len;
- dblayer_value_concat(be, &key, buf, sizeof(buf),
- prefix, strlen(prefix), val->bv_val, vlen, "", 1);
+ (void) prepare_key(be, ai, NULL, 0, 0, prefix, val, &key);
} else {
dblayer_value_concat(be, &key, buf, sizeof(buf), prefix, strlen(prefix),
"", 1, NULL, 0);
@@ -1824,6 +1855,7 @@ index_range_read(
return index_range_read_ext(pb, be, type, indextype, operator, val, nextval, range, txn, err, 0);
}
+
static int
addordel_values_sv(
backend *be,
@@ -1842,15 +1874,10 @@ addordel_values_sv(
int i = 0;
dbi_val_t key = {0};
dbi_txn_t *db_txn = NULL;
- size_t plen, vlen, len;
char *tmpbuf = NULL;
size_t tmpbuflen = 0;
- char *realbuf;
char *prefix = NULL;
const struct berval *bvp;
- struct berval *hashed_bvp = NULL;
- struct berval *encrypted_bvp = NULL;
- struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private;
char *index_id = get_index_name(be, db, a);
slapi_log_err(SLAPI_LOG_TRACE, "addordel_values_sv", "%s_values\n",
@@ -1889,66 +1916,14 @@ addordel_values_sv(
return (rc);
}
- plen = strlen(prefix);
for (i = 0; vals[i] != NULL; i++) {
bvp = slapi_value_get_berval(vals[i]);
- /* Hash large index key if necessary */
- if (bvp->bv_len >= li->li_max_key_len) {
- rc = attrcrypt_hash_large_index_key(be, &prefix, a, bvp, &hashed_bvp);
- if (rc) {
- slapi_log_err(SLAPI_LOG_ERR, "index_read_ext_allids",
- "Failed to hash large index key for %s\n", a->ai_type);
- break;
- } else {
- bvp = hashed_bvp;
- plen = strlen(prefix);
- }
- }
- /* Encrypt the index key if necessary */
- {
- if (a->ai_attrcrypt && (0 == (flags & BE_INDEX_DONT_ENCRYPT))) {
- rc = attrcrypt_encrypt_index_key(be, a, bvp, &encrypted_bvp);
- if (rc) {
- slapi_log_err(SLAPI_LOG_ERR, "addordel_values_sv",
- "Failed to encrypt index key for %s\n", a->ai_type);
- } else {
- bvp = encrypted_bvp;
- }
- }
+ rc = prepare_key(be, a, &tmpbuf, &tmpbuflen, flags, prefix, bvp, &key);
+ if (rc) {
+ break;
}
- vlen = bvp->bv_len;
- len = plen + vlen;
-
- if (len < tmpbuflen) {
- realbuf = tmpbuf;
- } else {
- tmpbuf = slapi_ch_realloc(tmpbuf, len + 1);
- tmpbuflen = len + 1;
- realbuf = tmpbuf;
- }
-
- assert(realbuf); /* For coverity */
- memcpy(realbuf, prefix, plen);
- memcpy(realbuf + plen, bvp->bv_val, vlen);
- realbuf[len] = '\0';
- /* Free the encrypted berval if necessary */
- if (hashed_bvp) {
- ber_bvfree(hashed_bvp);
- hashed_bvp = NULL;
- }
- if (encrypted_bvp) {
- ber_bvfree(encrypted_bvp);
- encrypted_bvp = NULL;
- }
- /* should be okay to use USERMEM here because we know what
- * the key is and it should never return a different value
- * than the one we pass in.
- */
- dblayer_value_set_buffer(be, &key, realbuf, plen + vlen + 1);
- key.ulen = tmpbuflen;
-
if (slapi_is_loglevel_set(LDAP_DEBUG_TRACE)) {
char encbuf[BUFSIZ];
@@ -1981,10 +1956,6 @@ addordel_values_sv(
ldbm_nasty(NASTY_MSG("addordel_values_sv"), index_id, 1130, rc);
break;
}
- if (NULL != key.dptr && realbuf != key.dptr) { /* realloc'ed */
- tmpbuf = key.dptr;
- tmpbuflen = key.size;
- }
}
index_free_prefix(prefix);
if (tmpbuf != NULL) {
diff --git a/ldap/servers/slapd/back-ldbm/ldbm_attrcrypt.c b/ldap/servers/slapd/back-ldbm/ldbm_attrcrypt.c
index 124810426..6655633fc 100644
--- a/ldap/servers/slapd/back-ldbm/ldbm_attrcrypt.c
+++ b/ldap/servers/slapd/back-ldbm/ldbm_attrcrypt.c
@@ -1079,15 +1079,15 @@ attrcrypt_decrypt_index_key(backend *be,
* : NULL - no hash or failure
*/
int
-attrcrypt_hash_large_index_key(backend *be, char **prefix, struct attrinfo *ai, const struct berval *in, struct berval **out)
+attrcrypt_hash_large_index_key(backend *be, const char *prefix, struct attrinfo *ai, const struct berval *in, struct berval **out)
{
int ret = 0;
struct berval *out_berval = NULL;
struct ldbminfo *li = (struct ldbminfo *)be->be_database->plg_private;
- char *new_prefix;
+ size_t final_key_len = INDEX_KEY_LENGTH(in->bv_len, strlen(prefix));
/* If the index key is too long (i.e mdb case) we must hash it */
- if (in->bv_len >= li->li_max_key_len) {
+ if (final_key_len >= li->li_max_key_len) {
PK11Context *c = PK11_CreateDigestContext(SEC_OID_MD5);
if (c != NULL) {
unsigned char hash[32];
@@ -1101,16 +1101,13 @@ attrcrypt_hash_large_index_key(backend *be, char **prefix, struct attrinfo *ai,
return ENOMEM;
}
slapi_log_err(SLAPI_LOG_TRACE, "attrcrypt_hash_large_index_key",
- "Key lenght (%lu) >= max key lenght (%lu) so key must be hashed\n", in->bv_len, li->li_max_key_len);
+ "Key lenght (%lu) >= max key lenght (%lu) so key must be hashed\n", final_key_len, li->li_max_key_len);
slapi_be_set_flag(be, SLAPI_BE_FLAG_DONT_BYPASS_FILTERTEST);
PK11_DigestBegin(c);
/* Compute hash for the key without the prefix */
PK11_DigestOp(c, (unsigned char *)in->bv_val, in->bv_len);
PK11_DigestFinal(c, hash, &hashLen, sizeof hash);
- /* Add HASH_PREFIX before the prefix */
- new_prefix = slapi_ch_smprintf("%c%s", HASH_PREFIX, *prefix);
- index_free_prefix(*prefix);
- *prefix = new_prefix;
+
/* Build the key: hash value in hexa */
hkey = slapi_ch_malloc(1+2*sizeof hash);
out_berval->bv_val = hkey;
diff --git a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
index 30a7aa11f..c882dac7b 100644
--- a/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
+++ b/ldap/servers/slapd/back-ldbm/proto-back-ldbm.h
@@ -622,7 +622,7 @@ int attrcrypt_encrypt_entry_inplace(backend *be, const struct backentry *inout);
int attrcrypt_encrypt_entry(backend *be, const struct backentry *in, struct backentry **out);
int attrcrypt_encrypt_index_key(backend *be, struct attrinfo *ai, const struct berval *in, struct berval **out);
int attrcrypt_decrypt_index_key(backend *be, struct attrinfo *ai, const struct berval *in, struct berval **out);
-int attrcrypt_hash_large_index_key(backend *be, char **prefix, struct attrinfo *ai, const struct berval *in, struct berval **out);
+int attrcrypt_hash_large_index_key(backend *be, const char *prefix, struct attrinfo *ai, const struct berval *in, struct berval **out);
int attrcrypt_init(ldbm_instance *li);
int attrcrypt_cleanup_private(ldbm_instance *li);
diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c
index 80c07382a..93101494b 100644
--- a/ldap/servers/slapd/log.c
+++ b/ldap/servers/slapd/log.c
@@ -93,6 +93,10 @@ static int slapi_log_map[] = {
#define FLUSH PR_TRUE
#define NO_FLUSH PR_FALSE
+#define HEXADUMP_TAB 4
+/* 4 characters per bytes: 2 hexa digits, 1 space and the ascii */
+#define HEXADUMP_BUF_SIZE (4*16+HEXADUMP_TAB)
+
/**************************************************************************
* PROTOTYPES
*************************************************************************/
@@ -3133,6 +3137,45 @@ slapi_log_backtrace(int loglevel)
}
}
+/*
+ * Dump a memory buffer in hexa and ascii in error log
+ *
+ * addr - The memory buffer address.
+ * len - The memory buffer lenght.
+ */
+void
+slapi_log_hexadump(int loglevel, char *fname, const void *addr, size_t len)
+{
+ char hexdigit[] = "0123456789ABCDEF";
+ const unsigned char *pt = addr;
+ char buff[HEXADUMP_BUF_SIZE+1];
+ size_t offset = 0;
+
+ if (!slapi_is_loglevel_set(loglevel)) {
+ return;
+ }
+ memset (buff, ' ', HEXADUMP_BUF_SIZE);
+ buff[HEXADUMP_BUF_SIZE] = '\0';
+ while (len > 0) {
+ int dpl;
+ for (dpl = 0; dpl < 16 && len>0; dpl++, len--) {
+ buff[3*dpl] = hexdigit[((*pt) >> 4) & 0xf];
+ buff[3*dpl+1] = hexdigit[(*pt) & 0xf];
+ buff[3*16+HEXADUMP_TAB+dpl] = (*pt>=0x20 && *pt<0x7f) ? *pt : '.';
+ pt++;
+ }
+ for (;dpl < 16; dpl++) {
+ buff[3*dpl] = ' ';
+ buff[3*dpl+1] = ' ';
+ buff[3*16+HEXADUMP_TAB+dpl] = ' ';
+ }
+ slapi_log_err(loglevel, fname, "[0x%08lx] %s\n", offset, buff);
+ offset += 16;
+ }
+}
+
+
+
/******************************************************************************
* write in the access log
******************************************************************************/
diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h
index 72f4cd6f0..2da37ff6e 100644
--- a/ldap/servers/slapd/slapi-private.h
+++ b/ldap/servers/slapd/slapi-private.h
@@ -1527,6 +1527,8 @@ void slapi_pblock_set_task_warning(Slapi_PBlock *pb, task_warning warning);
int slapi_exists_or_add_internal(Slapi_DN *dn, const char *filter, const char *entry, const char *modifier_name);
void slapi_log_backtrace(int loglevel);
+void slapi_log_hexadump(int loglevel, char *fname, const void *addr, size_t len);
+
/*
* accesslog.c
--
2.53.0

View File

@ -0,0 +1,291 @@
From 6fc10eae5b44989c703b67bdca157202d35d8a92 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 | 8 +++--
ldap/servers/slapd/pblock.c | 36 +++++++++++++------
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, 74 insertions(+), 22 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 19c6d7e48..7a2ca2ae2 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).
@@ -1416,6 +1416,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",
@@ -3178,7 +3182,7 @@ void
wait4certs_refresh(daemon_ports_t *ports)
{
/*
- * Block listening and accept threads until
+ * Block listening and accept threads until
* certificates refresh is complete
* Note:
* Listening threads have a NULL ports
diff --git a/ldap/servers/slapd/pblock.c b/ldap/servers/slapd/pblock.c
index 24cb9b15e..3405e6da6 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>
@@ -1008,6 +1008,13 @@ slapi_pblock_get_plugin_close_fn(Slapi_PBlock *pblock, void *value)
return 0;
}
+static int32_t
+slapi_pblock_get_plugin_pre_close_fn(Slapi_PBlock *pblock, void *value)
+{
+ (*(IFP *)value) = pblock->pb_plugin->plg_pre_close;
+ return 0;
+}
+
static int32_t
slapi_pblock_get_plugin_cleanup_fn(Slapi_PBlock *pblock, void *value)
{
@@ -4100,6 +4107,13 @@ slapi_pblock_set_plugin_close_fn(Slapi_PBlock *pblock, void *value)
return 0;
}
+static int32_t
+slapi_pblock_set_plugin_pre_close_fn(Slapi_PBlock *pblock, void *value)
+{
+ pblock->pb_plugin->plg_pre_close = (IFP)value;
+ return 0;
+}
+
static int32_t
slapi_pblock_set_plugin_cleanup_fn(Slapi_PBlock *pblock, void *value)
{
@@ -6948,7 +6962,7 @@ static int32_t (*get_cbtable[])(Slapi_PBlock *, void *) = {
slapi_pblock_get_plugin_db_abandon_fn,
slapi_pblock_get_plugin_db_config_fn,
slapi_pblock_get_plugin_close_fn,
- NULL, /* slot 211 available */
+ slapi_pblock_get_plugin_pre_close_fn,
slapi_pblock_get_plugin_start_fn,
slapi_pblock_get_plugin_db_seq_fn,
slapi_pblock_get_plugin_db_entry_fn,
@@ -8923,7 +8937,7 @@ static int32_t (*set_cbtable[])(Slapi_PBlock *, void *) = {
slapi_pblock_set_plugin_db_abandon_fn,
slapi_pblock_set_plugin_db_config_fn,
slapi_pblock_set_plugin_close_fn,
- NULL, /* slot 211 available */
+ slapi_pblock_set_plugin_pre_close_fn,
slapi_pblock_set_plugin_start_fn,
slapi_pblock_set_plugin_db_seq_fn,
slapi_pblock_set_plugin_db_entry_fn,
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 455d6d718..ce7e4f047 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).
@@ -997,6 +997,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 d494931c2..3e50f77b2 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:
@@ -1046,6 +1046,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 85bbf8fa1..70677123d 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.
*
@@ -7085,6 +7085,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

@ -0,0 +1,34 @@
From 5cc3edb9732784e8a3edb033962b6312f2bb7b55 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

4
389-ds-base-devel.README Normal file
View File

@ -0,0 +1,4 @@
For detailed information on developing plugins for 389 Directory Server visit
https://www.port389.org/docs/389ds/design/plugins.html
https://github.com/389ds/389-ds-base/blob/main/src/slapi_r_plugin/README.md

1233
389-ds-base.spec Normal file

File diff suppressed because it is too large Load Diff

3
389-ds-base.sysusers Normal file
View File

@ -0,0 +1,3 @@
#Type Name ID GECOS Home directory Shell
g dirsrv 389
u dirsrv 389:389 "user for 389-ds-base" /usr/share/dirsrv/ /sbin/nologin

File diff suppressed because it is too large Load Diff

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,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

@ -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

@ -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,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,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

@ -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

@ -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

@ -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

@ -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

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,4 +0,0 @@
For detailed information on developing plugins for
389 Directory Server visit.
http://port389/wiki/Plugins

View File

@ -1,16 +0,0 @@
#!/bin/bash
DATE=`date +%Y%m%d`
# use a real tag name here
VERSION=1.3.5.14
PKGNAME=389-ds-base
TAG=${TAG:-$PKGNAME-$VERSION}
URL="https://git.fedorahosted.org/git/?p=389/ds.git;a=snapshot;h=$TAG;sf=tgz"
SRCNAME=$PKGNAME-$VERSION
wget -O $SRCNAME.tar.gz "$URL"
echo convert tgz format to tar.bz2 format
gunzip $PKGNAME-$VERSION.tar.gz
bzip2 $PKGNAME-$VERSION.tar

File diff suppressed because it is too large Load Diff

4
sources Normal file
View File

@ -0,0 +1,4 @@
SHA512 (389-ds-base-3.2.0.tar.bz2) = 9ff6aa56b30863c619f4f324344dca72cc883236bfe8d94520e8469d9e306f54b373ee2504eda18dcb0ecda33f915a3e64a6f3cdaa93a69b74d901caa48545e1
SHA512 (jemalloc-5.3.0.tar.bz2) = 22907bb052096e2caffb6e4e23548aecc5cc9283dce476896a2b1127eee64170e3562fa2e7db9571298814a7a2c7df6e8d1fbe152bd3f3b0c1abec22a2de34b1
SHA512 (libdb-5.3.28-59.tar.bz2) = 731a434fa2e6487ebb05c458b0437456eb9f7991284beb08cb3e21931e23bdeddddbc95bfabe3a2f9f029fe69cd33a2d4f0f5ce6a9811e9c3b940cb6fde4bf79
SHA512 (vendor-3.2.0-4.tar.gz) = b7daee9351fd007ef54a51a3c41f15fa5538b04341a85df2e889728569139ec4b94fcd7f167a2886bc29b40a01866774f6e6f78fbd80021e3fcfe393a95d7efb