public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Viktor Ashirov <vashirov@redhat.com>
To: git-commits@fedoraproject.org
Subject: [rpms/389-ds-base] rawhide: Bump version to 3.3.0
Date: Thu, 04 Jun 2026 07:05:11 GMT [thread overview]
Message-ID: <178055671123.1.4554221671351449944.rpms-389-ds-base-721c66d081e7@fedoraproject.org> (raw)
A new commit has been pushed.
Repo : rpms/389-ds-base
Branch : rawhide
Commit : 721c66d081e7690cb2200bd830b2172d3b7952ff
Author : Viktor Ashirov <vashirov@redhat.com>
Date : 2026-06-04T09:04:51+02:00
Stats : +64/-22995 in 68 file(s)
URL : https://src.fedoraproject.org/rpms/389-ds-base/c/721c66d081e7690cb2200bd830b2172d3b7952ff?branch=rawhide
Log:
Bump version to 3.3.0
---
diff --git a/0001-Issue-7096-During-replication-online-total-init-the-.patch b/0001-Issue-7096-During-replication-online-total-init-the-.patch
deleted file mode 100644
index a5792b6..0000000
--- a/0001-Issue-7096-During-replication-online-total-init-the-.patch
+++ /dev/null
@@ -1,318 +0,0 @@
-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
-
diff --git a/0002-Issue-Revise-paged-result-search-locking.patch b/0002-Issue-Revise-paged-result-search-locking.patch
deleted file mode 100644
index e27ced3..0000000
--- a/0002-Issue-Revise-paged-result-search-locking.patch
+++ /dev/null
@@ -1,765 +0,0 @@
-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
-
diff --git a/0003-Issue-7108-Fix-shutdown-crash-in-entry-cache-destruc.patch b/0003-Issue-7108-Fix-shutdown-crash-in-entry-cache-destruc.patch
deleted file mode 100644
index bb2127c..0000000
--- a/0003-Issue-7108-Fix-shutdown-crash-in-entry-cache-destruc.patch
+++ /dev/null
@@ -1,183 +0,0 @@
-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
-
diff --git a/0004-Issue-7172-Index-ordering-mismatch-after-upgrade-717.patch b/0004-Issue-7172-Index-ordering-mismatch-after-upgrade-717.patch
deleted file mode 100644
index 2ea800b..0000000
--- a/0004-Issue-7172-Index-ordering-mismatch-after-upgrade-717.patch
+++ /dev/null
@@ -1,215 +0,0 @@
-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
-
diff --git a/0005-Issue-7172-2nd-Index-ordering-mismatch-after-upgrade.patch b/0005-Issue-7172-2nd-Index-ordering-mismatch-after-upgrade.patch
deleted file mode 100644
index 6a80a5b..0000000
--- a/0005-Issue-7172-2nd-Index-ordering-mismatch-after-upgrade.patch
+++ /dev/null
@@ -1,67 +0,0 @@
-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
-
diff --git a/0006-Issue-6753-Port-ticket-548-test-7101.patch b/0006-Issue-6753-Port-ticket-548-test-7101.patch
deleted file mode 100644
index e7a6a6c..0000000
--- a/0006-Issue-6753-Port-ticket-548-test-7101.patch
+++ /dev/null
@@ -1,924 +0,0 @@
-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
-
diff --git a/0007-Issue-7152-ns-slapd-fails-to-shutdown-when-deferred-.patch b/0007-Issue-7152-ns-slapd-fails-to-shutdown-when-deferred-.patch
deleted file mode 100644
index 0baedef..0000000
--- a/0007-Issue-7152-ns-slapd-fails-to-shutdown-when-deferred-.patch
+++ /dev/null
@@ -1,82 +0,0 @@
-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
-
diff --git a/0008-Issue-7169-Fix-automember_plugin-CI-test-failures-71.patch b/0008-Issue-7169-Fix-automember_plugin-CI-test-failures-71.patch
deleted file mode 100644
index e5c6916..0000000
--- a/0008-Issue-7169-Fix-automember_plugin-CI-test-failures-71.patch
+++ /dev/null
@@ -1,61 +0,0 @@
-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
-
diff --git a/0009-Issue-6758-Use-OUIA-selectors-for-WebUI-plugin-tests.patch b/0009-Issue-6758-Use-OUIA-selectors-for-WebUI-plugin-tests.patch
deleted file mode 100644
index 9fd2e71..0000000
--- a/0009-Issue-6758-Use-OUIA-selectors-for-WebUI-plugin-tests.patch
+++ /dev/null
@@ -1,115 +0,0 @@
-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
-
diff --git a/0010-Issue-7196-DynamicCertificates-returns-empty-DER-719.patch b/0010-Issue-7196-DynamicCertificates-returns-empty-DER-719.patch
deleted file mode 100644
index 5d8907c..0000000
--- a/0010-Issue-7196-DynamicCertificates-returns-empty-DER-719.patch
+++ /dev/null
@@ -1,32 +0,0 @@
-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
-
diff --git a/0011-Issue-7189-DSBLE0007-generates-incorrect-remediation.patch b/0011-Issue-7189-DSBLE0007-generates-incorrect-remediation.patch
deleted file mode 100644
index a37a939..0000000
--- a/0011-Issue-7189-DSBLE0007-generates-incorrect-remediation.patch
+++ /dev/null
@@ -1,235 +0,0 @@
-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
-
diff --git a/0012-Issue-7170-Support-of-PQC-keys-7188.patch b/0012-Issue-7170-Support-of-PQC-keys-7188.patch
deleted file mode 100644
index 3a9ce29..0000000
--- a/0012-Issue-7170-Support-of-PQC-keys-7188.patch
+++ /dev/null
@@ -1,504 +0,0 @@
-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", "Rivest–Shamir–Adleman" },
-+ { 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
-
diff --git a/0013-Bump-lodash-from-4.17.21-to-4.17.23-in-src-cockpit-3.patch b/0013-Bump-lodash-from-4.17.21-to-4.17.23-in-src-cockpit-3.patch
deleted file mode 100644
index f7d9c90..0000000
--- a/0013-Bump-lodash-from-4.17.21-to-4.17.23-in-src-cockpit-3.patch
+++ /dev/null
@@ -1,56 +0,0 @@
-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
-
diff --git a/0014-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch b/0014-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch
deleted file mode 100644
index bd2b63e..0000000
--- a/0014-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch
+++ /dev/null
@@ -1,517 +0,0 @@
-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
-
diff --git a/0015-Issue-7014-memberOf-ignored-deferred-updates-with-LM.patch b/0015-Issue-7014-memberOf-ignored-deferred-updates-with-LM.patch
deleted file mode 100644
index 2e70a3c..0000000
--- a/0015-Issue-7014-memberOf-ignored-deferred-updates-with-LM.patch
+++ /dev/null
@@ -1,38 +0,0 @@
-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
-
diff --git a/0016-Issue-7184-argparse.HelpFormatter-_format_actions_us.patch b/0016-Issue-7184-argparse.HelpFormatter-_format_actions_us.patch
deleted file mode 100644
index 6feaeab..0000000
--- a/0016-Issue-7184-argparse.HelpFormatter-_format_actions_us.patch
+++ /dev/null
@@ -1,48 +0,0 @@
-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
-
diff --git a/0017-Issue-6947-Revise-time-skew-check-in-healthcheck-too.patch b/0017-Issue-6947-Revise-time-skew-check-in-healthcheck-too.patch
deleted file mode 100644
index abe0cdc..0000000
--- a/0017-Issue-6947-Revise-time-skew-check-in-healthcheck-too.patch
+++ /dev/null
@@ -1,174 +0,0 @@
-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
-
diff --git a/0018-Issue-7201-Syscall-overhead-in-LMDB-import-writer-th.patch b/0018-Issue-7201-Syscall-overhead-in-LMDB-import-writer-th.patch
deleted file mode 100644
index 4934ba7..0000000
--- a/0018-Issue-7201-Syscall-overhead-in-LMDB-import-writer-th.patch
+++ /dev/null
@@ -1,215 +0,0 @@
-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
-
diff --git a/0019-Issue-7096-2nd-During-replication-online-total-init-.patch b/0019-Issue-7096-2nd-During-replication-online-total-init-.patch
deleted file mode 100644
index ccf1d37..0000000
--- a/0019-Issue-7096-2nd-During-replication-online-total-init-.patch
+++ /dev/null
@@ -1,135 +0,0 @@
-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
-
diff --git a/0020-Issue-7206-Should-log-whether-TLS-key-is-PQC-or-not-.patch b/0020-Issue-7206-Should-log-whether-TLS-key-is-PQC-or-not-.patch
deleted file mode 100644
index 000415e..0000000
--- a/0020-Issue-7206-Should-log-whether-TLS-key-is-PQC-or-not-.patch
+++ /dev/null
@@ -1,149 +0,0 @@
-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
-
diff --git a/0021-Issue-7027-2nd-389-ds-base-OpenScanHub-Leaks-Detecte.patch b/0021-Issue-7027-2nd-389-ds-base-OpenScanHub-Leaks-Detecte.patch
deleted file mode 100644
index 08277fb..0000000
--- a/0021-Issue-7027-2nd-389-ds-base-OpenScanHub-Leaks-Detecte.patch
+++ /dev/null
@@ -1,53 +0,0 @@
-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
-
diff --git a/0022-Issue-7213-MDB_BAD_VALSIZE-error-while-handling-VLV-.patch b/0022-Issue-7213-MDB_BAD_VALSIZE-error-while-handling-VLV-.patch
deleted file mode 100644
index 4e069b3..0000000
--- a/0022-Issue-7213-MDB_BAD_VALSIZE-error-while-handling-VLV-.patch
+++ /dev/null
@@ -1,195 +0,0 @@
-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
-
diff --git a/0023-Issue-6753-Port-ticket-47781-test-7210.patch b/0023-Issue-6753-Port-ticket-47781-test-7210.patch
deleted file mode 100644
index 6602444..0000000
--- a/0023-Issue-6753-Port-ticket-47781-test-7210.patch
+++ /dev/null
@@ -1,285 +0,0 @@
-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
-
diff --git a/0024-Issue-7194-Repl-Log-Analysis-Add-CSN-propagation-det.patch b/0024-Issue-7194-Repl-Log-Analysis-Add-CSN-propagation-det.patch
deleted file mode 100644
index e3c9347..0000000
--- a/0024-Issue-7194-Repl-Log-Analysis-Add-CSN-propagation-det.patch
+++ /dev/null
@@ -1,1645 +0,0 @@
-From c2921e87bf8e8fee566edd09cb528b1c3ac3e41d Mon Sep 17 00:00:00 2001
-From: Simon Pichugin <spichugi@redhat.com>
-Date: Tue, 3 Feb 2026 17:17:02 -0800
-Subject: [PATCH] Issue 7194 - Repl Log Analysis - Add CSN propagation details
- (#7195)
-MIME-Version: 1.0
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 8bit
-
-Description: The replication log analyzer now shows per‑CSN propagation
-details and the console UI can drill into them from chart points. This
-adds CSN IDs to chart datapoints, builds detailed arrivals/hops data, and
-links replica IDs to origin servers for more accurate origin detection.
-
-The report JSON now includes csnDetails and sampling metadata; when
-sampling is active, CSN details are limited to sampled IDs to control
-memory use. A new originIncludedInArrivals flag is exposed and the UI
-shows an explicit note when origin records are outside the time range.
-The cockpit report modal gains an interactive CSN detail view and
-clickable chart points.
-
-Tests were expanded to cover CSN details, origin out‑of‑scope behavior,
-and partial replication, and include helper functions to reduce duplication.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7194
-
-Reviewed by: @progier389, @mreynolds389 (Thanks!!)
----
- .../replication/repl_log_monitoring_test.py | 481 +++++++++++++++--
- .../src/lib/monitor/monitorModals.jsx | 484 +++++++++++++++++-
- src/lib389/lib389/repltools.py | 270 +++++++++-
- 3 files changed, 1166 insertions(+), 69 deletions(-)
-
-diff --git a/dirsrvtests/tests/suites/replication/repl_log_monitoring_test.py b/dirsrvtests/tests/suites/replication/repl_log_monitoring_test.py
-index 665fcb96f..855005ff9 100644
---- a/dirsrvtests/tests/suites/replication/repl_log_monitoring_test.py
-+++ b/dirsrvtests/tests/suites/replication/repl_log_monitoring_test.py
-@@ -21,7 +21,7 @@ from lib389.backend import Backends
- from lib389.topologies import topology_m4 as topo_m4
- from lib389.idm.user import UserAccount
- from lib389.replica import ReplicationManager
--from lib389.repltools import ReplicationLogAnalyzer
-+from lib389.repltools import ReplicationLogAnalyzer, DSLogParser
- from lib389._constants import *
-
- pytestmark = pytest.mark.tier0
-@@ -105,6 +105,101 @@ def _cleanup_multi_suffix_test(test_users_by_suffix, tmp_dir, suppliers, extra_s
- log.error(f"Error cleaning up temporary directory: {e}")
-
-
-+def _clear_access_logs(suppliers):
-+ """Clear access logs for all suppliers and restart."""
-+ for supplier in suppliers:
-+ supplier.deleteAccessLogs(restart=True)
-+
-+
-+def _restart_suppliers(suppliers):
-+ """Restart all suppliers."""
-+ for supplier in suppliers:
-+ supplier.restart()
-+
-+
-+def _get_log_dirs(suppliers):
-+ """Return log directories for all suppliers."""
-+ return [s.ds_paths.log_dir for s in suppliers]
-+
-+
-+def _load_json(path):
-+ """Load and return JSON from file."""
-+ with open(path, 'r') as f:
-+ return json.load(f)
-+
-+
-+def _pause_agreements(supplier, suffix):
-+ """Pause outbound agreements and return list of paused tuples."""
-+ paused = []
-+ for agmt in supplier.agreement.list(suffix=suffix):
-+ supplier.agreement.pause(agmt.dn)
-+ paused.append((supplier, agmt.dn))
-+ return paused
-+
-+
-+def _resume_agreements(paused_agreements):
-+ """Resume paused replication agreements."""
-+ for supplier_obj, dn in paused_agreements:
-+ try:
-+ supplier_obj.agreement.resume(dn)
-+ except Exception as e:
-+ log.warning(f"Failed to resume agreement {dn}: {e}")
-+
-+
-+def _assert_csn_details_schema(json_data):
-+ """Validate csnDetails presence and basic structure."""
-+ assert 'csnDetails' in json_data, "Expected csnDetails in JSON output for drill-down"
-+ csn_details = json_data['csnDetails']
-+ if csn_details:
-+ # Check structure of at least one CSN detail entry
-+ first_csn = next(iter(csn_details.values()))
-+ assert 'csn' in first_csn, "CSN detail should contain 'csn' field"
-+ assert 'targetDn' in first_csn, "CSN detail should contain 'targetDn' field"
-+ assert 'suffix' in first_csn, "CSN detail should contain 'suffix' field"
-+ assert 'globalLag' in first_csn, "CSN detail should contain 'globalLag' field"
-+ assert 'originServer' in first_csn, "CSN detail should contain 'originServer' field"
-+ assert 'arrivals' in first_csn, "CSN detail should contain 'arrivals' list"
-+ assert 'hops' in first_csn, "CSN detail should contain 'hops' list"
-+ assert isinstance(first_csn['arrivals'], list), "arrivals should be a list"
-+
-+ # Verify arrivals structure
-+ if first_csn['arrivals']:
-+ first_arrival = first_csn['arrivals'][0]
-+ assert 'server' in first_arrival, "Arrival should contain 'server' field"
-+ assert 'timestamp' in first_arrival, "Arrival should contain 'timestamp' field"
-+ assert 'relativeDelay' in first_arrival, "Arrival should contain 'relativeDelay' field"
-+
-+ # Verify csnId is included in datapoints for cross-reference
-+ if 'replicationLags' in json_data and json_data['replicationLags'].get('series'):
-+ for series in json_data['replicationLags']['series']:
-+ for datapoint in series['datapoints']:
-+ assert 'csnId' in datapoint, "Datapoint should contain 'csnId' for drill-down"
-+
-+ return csn_details
-+
-+
-+def _find_latest_logtime_for_prefix(log_dir, suffix, start_time, end_time, user_prefix):
-+ latest = None
-+ for fname in os.listdir(log_dir):
-+ if not fname.startswith('access'):
-+ continue
-+ full_path = os.path.join(log_dir, fname)
-+ parser = DSLogParser(
-+ logname=full_path,
-+ suffixes=[suffix],
-+ tz=timezone.utc,
-+ start_time=start_time,
-+ end_time=end_time
-+ )
-+ for record in parser.parse_file():
-+ target_dn = record.get('target_dn') or ''
-+ if user_prefix in target_dn:
-+ ts = record.get('timestamp')
-+ if ts and (latest is None or ts > latest):
-+ latest = ts
-+ return latest
-+
-+
- def test_replication_log_monitoring_basic(topo_m4):
- """Test basic replication log monitoring functionality
-
-@@ -128,8 +223,7 @@ def test_replication_log_monitoring_basic(topo_m4):
-
- try:
- # Clear logs and restart servers
-- for supplier in suppliers:
-- supplier.deleteAccessLogs(restart=True)
-+ _clear_access_logs(suppliers)
-
- # Generate test data with known patterns
- log.info('Creating test data...')
-@@ -140,11 +234,10 @@ def test_replication_log_monitoring_basic(topo_m4):
- repl.test_replication_topology(topo_m4)
-
- # Restart to flush logs
-- for supplier in suppliers:
-- supplier.restart()
-+ _restart_suppliers(suppliers)
-
- # Configure monitoring
-- log_dirs = [s.ds_paths.log_dir for s in suppliers]
-+ log_dirs = _get_log_dirs(suppliers)
- repl_monitor = ReplicationLogAnalyzer(
- log_dirs=log_dirs,
- suffixes=[DEFAULT_SUFFIX],
-@@ -177,23 +270,23 @@ def test_replication_log_monitoring_basic(topo_m4):
- assert DEFAULT_SUFFIX in csv_content
-
- # Verify PatternFly JSON content
-- with open(generated_files['json'], 'r') as f:
-- json_data = json.load(f)
-- assert 'replicationLags' in json_data
-- assert json_data['replicationLags']['series'], "Expected replication lag series in JSON output"
-+ json_data = _load_json(generated_files['json'])
-+ assert 'replicationLags' in json_data
-+ assert json_data['replicationLags']['series'], "Expected replication lag series in JSON output"
-+
-+ _assert_csn_details_schema(json_data)
-
- # Verify JSON summary
-- with open(generated_files['summary'], 'r') as f:
-- summary = json.load(f)
-- assert 'analysis_summary' in summary
-- stats = summary['analysis_summary']
-+ summary = _load_json(generated_files['summary'])
-+ assert 'analysis_summary' in summary
-+ stats = summary['analysis_summary']
-
-- # Verify basic stats
-- assert stats['total_servers'] == len(suppliers)
-- assert stats['total_updates'] > 0
-- assert stats['updates_by_suffix'][DEFAULT_SUFFIX] > 0
-- assert 'average_lag' in stats
-- assert 'maximum_lag' in stats
-+ # Verify basic stats
-+ assert stats['total_servers'] == len(suppliers)
-+ assert stats['total_updates'] > 0
-+ assert stats['updates_by_suffix'][DEFAULT_SUFFIX] > 0
-+ assert 'average_lag' in stats
-+ assert 'maximum_lag' in stats
-
- finally:
- _cleanup_test_data(test_users, tmp_dir)
-@@ -221,8 +314,7 @@ def test_replication_log_monitoring_advanced(topo_m4):
-
- try:
- # Clear logs and restart servers
-- for supplier in suppliers:
-- supplier.deleteAccessLogs(restart=True)
-+ _clear_access_logs(suppliers)
-
- # Generate test data
- start_time = datetime.now(timezone.utc)
-@@ -240,10 +332,9 @@ def test_replication_log_monitoring_advanced(topo_m4):
- end_time = datetime.now(timezone.utc)
-
- # Restart to flush logs
-- for supplier in suppliers:
-- supplier.restart()
-+ _restart_suppliers(suppliers)
-
-- log_dirs = [s.ds_paths.log_dir for s in suppliers]
-+ log_dirs = _get_log_dirs(suppliers)
-
- # Test 1: Lag time filtering
- repl_monitor = ReplicationLogAnalyzer(
-@@ -374,8 +465,7 @@ def test_replication_log_monitoring_multi_suffix(topo_m4):
- repl = ReplicationManager(suffix)
- repl.test_replication_topology(topo_m4)
-
-- for supplier in suppliers:
-- supplier.deleteAccessLogs(restart=True)
-+ _clear_access_logs(suppliers)
-
- start_time = datetime.now(timezone.utc)
-
-@@ -400,11 +490,10 @@ def test_replication_log_monitoring_multi_suffix(topo_m4):
- end_time = datetime.now(timezone.utc)
-
- # Restart to flush logs
-- for supplier in suppliers:
-- supplier.restart()
-+ _restart_suppliers(suppliers)
-
- # Monitor all suffixes
-- log_dirs = [s.ds_paths.log_dir for s in suppliers]
-+ log_dirs = _get_log_dirs(suppliers)
- repl_monitor = ReplicationLogAnalyzer(
- log_dirs=log_dirs,
- suffixes=all_suffixes,
-@@ -466,8 +555,7 @@ def test_replication_log_monitoring_filter_combinations(topo_m4):
-
- try:
- # Clear logs and restart servers
-- for supplier in suppliers:
-- supplier.deleteAccessLogs(restart=True)
-+ _clear_access_logs(suppliers)
-
- # Generate varied test data
- start_time = datetime.now(timezone.utc)
-@@ -475,9 +563,7 @@ def test_replication_log_monitoring_filter_combinations(topo_m4):
-
- # Create different lag patterns
- # Pause outbound agreements from supplier1 to build a replication backlog
-- for agmt in suppliers[0].agreement.list(suffix=DEFAULT_SUFFIX):
-- suppliers[0].agreement.pause(agmt.dn)
-- paused_agreements.append((suppliers[0], agmt.dn))
-+ paused_agreements = _pause_agreements(suppliers[0], DEFAULT_SUFFIX)
-
- for i, user in enumerate(test_users):
- if i % 3 == 0:
-@@ -502,10 +588,9 @@ def test_replication_log_monitoring_filter_combinations(topo_m4):
- end_time = datetime.now(timezone.utc)
-
- # Restart to flush logs
-- for supplier in suppliers:
-- supplier.restart()
-+ _restart_suppliers(suppliers)
-
-- log_dirs = [s.ds_paths.log_dir for s in suppliers]
-+ log_dirs = _get_log_dirs(suppliers)
-
- # Test combined filters
- lag_threshold = 0.5
-@@ -543,11 +628,321 @@ def test_replication_log_monitoring_filter_combinations(topo_m4):
- dt = datetime.fromtimestamp(t, timezone.utc)
- assert start_time <= dt <= end_time, "Time range filter violated"
- finally:
-- for supplier_obj, dn in paused_agreements:
-- try:
-- supplier_obj.agreement.resume(dn)
-- except Exception as e:
-- log.warning(f"Failed to resume agreement {dn}: {e}")
-+ _resume_agreements(paused_agreements)
-+ _cleanup_test_data(test_users, tmp_dir)
-+
-+
-+def test_replication_log_monitoring_csn_details_edge_cases(topo_m4):
-+ """Test CSN details edge cases and structure validation
-+
-+ :id: f43dc473-4428-4971-be4c-169c4a78726e
-+ :setup: Four suppliers replication setup
-+ :steps:
-+ 1. Test CSN details structure with various replication patterns
-+ 2. Verify arrivals ordering and hop lag calculations
-+ 3. Test partial replication scenarios
-+ 4. Verify origin server detection
-+ :expectedresults:
-+ 1. CSN details should have correct structure
-+ 2. Arrivals should be ordered by timestamp
-+ 3. Partial replication should be detected
-+ 4. Origin server should be correctly identified
-+ """
-+ tmp_dir = tempfile.mkdtemp(prefix='repl_csn_edge_')
-+ test_users = []
-+ suppliers = [topo_m4.ms[f"supplier{i}"] for i in range(1, 5)]
-+
-+ try:
-+ _clear_access_logs(suppliers)
-+
-+ log.info('Creating test data for CSN details edge case testing...')
-+ test_users = _generate_test_data(suppliers[0], DEFAULT_SUFFIX, 5)
-+
-+ repl = ReplicationManager(DEFAULT_SUFFIX)
-+ repl.test_replication_topology(topo_m4)
-+
-+ _restart_suppliers(suppliers)
-+
-+ log_dirs = _get_log_dirs(suppliers)
-+ repl_monitor = ReplicationLogAnalyzer(
-+ log_dirs=log_dirs,
-+ suffixes=[DEFAULT_SUFFIX],
-+ anonymous=False,
-+ only_fully_replicated=True
-+ )
-+
-+ repl_monitor.parse_logs()
-+ generated_files = repl_monitor.generate_report(
-+ output_dir=tmp_dir,
-+ formats=['json'],
-+ report_name='csn_edge_test'
-+ )
-+
-+ assert os.path.exists(generated_files['json'])
-+
-+ json_data = _load_json(generated_files['json'])
-+
-+ assert 'csnDetails' in json_data, "csnDetails should be present"
-+ csn_details = json_data['csnDetails']
-+
-+ if csn_details:
-+ for csn, details in csn_details.items():
-+ arrivals = details.get('arrivals', [])
-+ if len(arrivals) > 1:
-+ timestamps = [a['timestamp'] for a in arrivals]
-+ assert timestamps == sorted(timestamps), \
-+ f"Arrivals for CSN {csn} should be ordered by timestamp"
-+
-+ if arrivals:
-+ origin_arrival = next((a for a in arrivals if a.get('isOrigin')), None)
-+ assert origin_arrival is not None, "Expected an arrival marked as origin"
-+ assert origin_arrival.get('server') == details.get('originServer'), \
-+ "Origin arrival server should match originServer field"
-+
-+ for i, arrival in enumerate(arrivals[1:], start=1):
-+ assert 'hopLag' in arrival, \
-+ f"Arrival {i} should have hopLag"
-+ assert arrival['hopLag'] >= 0, \
-+ "hopLag should be non-negative"
-+
-+ if len(arrivals) > 1:
-+ global_lag = details.get('globalLag', 0)
-+ assert global_lag >= 0, "globalLag should be non-negative"
-+
-+ last_delay = arrivals[-1].get('relativeDelay', 0)
-+ assert abs(last_delay - global_lag) < 0.001, \
-+ "Last arrival's relativeDelay should match globalLag"
-+
-+ server_count = details.get('serverCount', 0)
-+ assert server_count == len(arrivals), \
-+ "serverCount should match number of arrivals"
-+
-+ total_hops = details.get('totalHops', 0)
-+ expected_hops = max(0, len(arrivals) - 1)
-+ assert total_hops == expected_hops, \
-+ f"totalHops should be {expected_hops}, got {total_hops}"
-+
-+ hops = details.get('hops', [])
-+ assert len(hops) == total_hops, \
-+ "hops list length should match totalHops"
-+
-+ if 'replicationLags' in json_data and json_data['replicationLags'].get('series'):
-+ for series in json_data['replicationLags']['series']:
-+ for datapoint in series.get('datapoints', []):
-+ csn_id = datapoint.get('csnId')
-+ if csn_id:
-+ assert csn_id in csn_details, \
-+ f"csnId {csn_id} in datapoint should exist in csnDetails"
-+
-+ finally:
-+ _cleanup_test_data(test_users, tmp_dir)
-+
-+
-+def test_replication_log_monitoring_origin_out_of_scope(topo_m4):
-+ """Test origin detection when origin server record is outside time range
-+
-+ :id: d73fd9c5-f930-47d6-ade4-ada1cf0d2c21
-+ :setup: Four suppliers replication setup
-+ :steps:
-+ 1. Pause outbound agreements from the origin supplier
-+ 2. Generate changes, then resume agreements to delay consumer arrivals
-+ 3. Use time range that excludes the origin log but includes consumer logs
-+ :expectedresults:
-+ 1. Origin server should be identified from replica ID mapping
-+ 2. At least one CSN should have origin outside the selected time range
-+ 3. JSON report should include csnDetails entries for validation
-+ """
-+ tmp_dir = tempfile.mkdtemp(prefix='repl_origin_scope_')
-+ test_users = []
-+ suppliers = [topo_m4.ms[f"supplier{i}"] for i in range(1, 5)]
-+ paused_agreements = []
-+
-+ try:
-+ # Reset access logs to make time-range cuts easier to reason about
-+ _clear_access_logs(suppliers)
-+
-+ # Pause outbound agreements so consumer logs won't see the pre-resume CSNs
-+ paused_agreements = _pause_agreements(suppliers[0], DEFAULT_SUFFIX)
-+
-+ log.info('Creating pre-resume changes for origin out-of-scope test...')
-+ # Create CSNs that originate on supplier1 but won't reach consumers yet
-+ pre_start = datetime.now(timezone.utc)
-+ test_users = _generate_test_data(
-+ suppliers[0], DEFAULT_SUFFIX, 2, user_prefix="origin_scope_pre"
-+ )
-+ pre_end = datetime.now(timezone.utc)
-+ # Locate the latest origin log time for these CSNs to set a deterministic cutoff
-+ origin_log_time = _find_latest_logtime_for_prefix(
-+ suppliers[0].ds_paths.log_dir,
-+ DEFAULT_SUFFIX,
-+ pre_start,
-+ pre_end,
-+ "origin_scope_pre_"
-+ )
-+ assert origin_log_time is not None, "Expected origin server log entries for pre-resume data"
-+ # Cut the analysis window just after origin logging, excluding supplier1 entries
-+ start_time = origin_log_time + timedelta(seconds=1)
-+ time.sleep(1)
-+
-+ # Resume agreements so the pre-resume CSNs replicate to consumers after start_time
-+ _resume_agreements(paused_agreements)
-+ paused_agreements.clear()
-+
-+ log.info('Creating post-resume changes for origin mapping...')
-+ # Additional CSNs after resume ensure normal replication continues
-+ test_users += _generate_test_data(
-+ suppliers[0], DEFAULT_SUFFIX, 2, user_prefix="origin_scope_post"
-+ )
-+
-+ # Wait for replication to finish and capture the upper bound of the time window
-+ repl = ReplicationManager(DEFAULT_SUFFIX)
-+ repl.test_replication_topology(topo_m4)
-+ end_time = datetime.now(timezone.utc)
-+
-+ # Restart to flush logs before analysis
-+ _restart_suppliers(suppliers)
-+
-+ log_dirs = _get_log_dirs(suppliers)
-+ repl_monitor = ReplicationLogAnalyzer(
-+ log_dirs=log_dirs,
-+ suffixes=[DEFAULT_SUFFIX],
-+ time_range={'start': start_time, 'end': end_time}
-+ )
-+
-+ # Parse logs within the time window and produce JSON details
-+ repl_monitor.parse_logs()
-+ generated_files = repl_monitor.generate_report(
-+ output_dir=tmp_dir,
-+ formats=['json'],
-+ report_name='origin_scope_test'
-+ )
-+
-+ json_data = _load_json(generated_files['json'])
-+
-+ csn_details = json_data.get('csnDetails', {})
-+ origin_server = suppliers[0].serverid
-+ found = False
-+ if csn_details:
-+ origin_counts = {}
-+ for details in csn_details.values():
-+ origin = details.get('originServer', 'unknown')
-+ origin_counts[origin] = origin_counts.get(origin, 0) + 1
-+ if origin_server not in origin_counts and f"slapd-{origin_server}" in origin_counts:
-+ origin_server = f"slapd-{origin_server}"
-+
-+ # Focus on the pre-resume CSNs; these should have origin out of scope
-+ pre_details = [
-+ details for details in csn_details.values()
-+ if "origin_scope_pre_" in (details.get('targetDn') or '')
-+ ]
-+ assert pre_details, "Expected pre-resume CSNs in csnDetails"
-+ # Confirm at least one CSN shows origin server missing from arrivals
-+ for details in pre_details:
-+ if details.get('originServer') != origin_server:
-+ continue
-+ arrivals = details.get('arrivals', [])
-+ arrival_servers = {a.get('server') for a in arrivals}
-+ if arrival_servers and origin_server not in arrival_servers:
-+ found = True
-+ break
-+ log.info(
-+ "Origin out-of-scope candidate: csn=%s arrivals=%s",
-+ details.get('csn'),
-+ sorted(arrival_servers)
-+ )
-+
-+ assert found, (
-+ "Expected at least one CSN where the origin server is outside the time range "
-+ "but still identified via replica ID mapping"
-+ )
-+
-+ finally:
-+ _resume_agreements(paused_agreements)
-+ _cleanup_test_data(test_users, tmp_dir)
-+
-+
-+def test_replication_log_monitoring_partial_replication(topo_m4):
-+ """Test CSN details with partial replication (not all servers reached)
-+
-+ :id: d4026fd0-d83b-400e-8c2e-44fcf676368f
-+ :setup: Four suppliers replication setup
-+ :steps:
-+ 1. Pause replication agreements to create partial replication
-+ 2. Generate changes and verify partial replication detection
-+ 3. Verify replicatedToAll flag is correct
-+ :expectedresults:
-+ 1. Partial replication should be detected
-+ 2. replicatedToAll should be False for partially replicated CSNs
-+ 3. serverCount should reflect actual servers reached
-+ """
-+ tmp_dir = tempfile.mkdtemp(prefix='repl_partial_')
-+ test_users = []
-+ suppliers = [topo_m4.ms[f"supplier{i}"] for i in range(1, 5)]
-+ paused_agreements = []
-+
-+ try:
-+ _clear_access_logs(suppliers)
-+
-+ log.info('Creating fully replicated test data...')
-+ test_users = _generate_test_data(suppliers[0], DEFAULT_SUFFIX, 3)
-+
-+ repl = ReplicationManager(DEFAULT_SUFFIX)
-+ repl.test_replication_topology(topo_m4)
-+
-+ _restart_suppliers(suppliers)
-+
-+ # Pause outbound agreements from supplier1 to create partial replication
-+ paused_agreements = _pause_agreements(suppliers[0], DEFAULT_SUFFIX)
-+
-+ log.info('Creating partially replicated test data...')
-+ test_users += _generate_test_data(suppliers[0], DEFAULT_SUFFIX, 3, user_prefix="partial_user")
-+
-+ # Allow some time for local logging; do not wait for full replication
-+ time.sleep(2)
-+
-+ log_dirs = _get_log_dirs(suppliers)
-+ repl_monitor = ReplicationLogAnalyzer(
-+ log_dirs=log_dirs,
-+ suffixes=[DEFAULT_SUFFIX],
-+ anonymous=False,
-+ only_fully_replicated=False,
-+ only_not_replicated=False
-+ )
-+
-+ repl_monitor.parse_logs()
-+ generated_files = repl_monitor.generate_report(
-+ output_dir=tmp_dir,
-+ formats=['json'],
-+ report_name='partial_repl_test'
-+ )
-+
-+ json_data = _load_json(generated_files['json'])
-+
-+ assert 'csnDetails' in json_data
-+ csn_details = json_data['csnDetails']
-+
-+ if csn_details:
-+ fully_replicated_count = sum(
-+ 1 for details in csn_details.values()
-+ if details.get('replicatedToAll', False)
-+ )
-+
-+ assert fully_replicated_count > 0, \
-+ "Should have some fully replicated CSNs"
-+
-+ total_servers = len(suppliers)
-+ for csn, details in csn_details.items():
-+ server_count = details.get('serverCount', 0)
-+ assert server_count <= total_servers, \
-+ f"serverCount ({server_count}) should not exceed total servers ({total_servers})"
-+
-+ replicated_to_all = details.get('replicatedToAll', False)
-+ if replicated_to_all:
-+ assert server_count == total_servers, \
-+ "replicatedToAll=True requires serverCount == total_servers"
-+
-+ finally:
-+ _resume_agreements(paused_agreements)
- _cleanup_test_data(test_users, tmp_dir)
-
-
-diff --git a/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx b/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
-index facbd9f5f..3c5a46a5b 100644
---- a/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
-+++ b/src/cockpit/389-console/src/lib/monitor/monitorModals.jsx
-@@ -3,17 +3,22 @@ import React from "react";
- import {
- Button,
- Checkbox,
-+ ClipboardCopy,
-+ ClipboardCopyVariant,
- EmptyState,
- EmptyStateIcon,
- EmptyStateBody,
- Grid,
- GridItem,
- Form,
-+ Label,
- Modal,
- ModalVariant,
- NumberInput,
- Radio,
- Spinner,
-+ Split,
-+ SplitItem,
- Tab,
- Tabs,
- TabTitleText,
-@@ -36,9 +41,12 @@ import {
- ListItem
- } from "@patternfly/react-core";
- import {
-+ ArrowRightIcon,
-+ CheckCircleIcon,
- CopyIcon,
- OutlinedQuestionCircleIcon,
- DownloadIcon,
-+ ServerIcon
- } from '@patternfly/react-icons';
- import PropTypes from "prop-types";
- import { get_date_string } from "../tools.jsx";
-@@ -71,6 +79,19 @@ const MAX_REPORT_JSON_SIZE = 64 * 1024 * 1024; // 64 MiB
- const MAX_BINARY_READ_SIZE = 64 * 1024 * 1024; // 64 MiB
- const CSV_PREVIEW_LINES = 20;
-
-+const formatLagSeconds = (seconds, precision = 3) => {
-+ if (seconds === undefined || seconds === null) {
-+ return null;
-+ }
-+ if (seconds >= 3600) {
-+ return `${(seconds / 3600).toFixed(precision)}h`;
-+ }
-+ if (seconds >= 60) {
-+ return `${(seconds / 60).toFixed(precision)}m`;
-+ }
-+ return `${seconds.toFixed(precision)}s`;
-+};
-+
- class TaskLogModal extends React.Component {
- render() {
- const {
-@@ -1168,6 +1189,13 @@ class ScatterLineChart extends React.PureComponent {
- }, 250);
- };
- this.toggleLegendItem = this.toggleLegendItem.bind(this);
-+ this.handlePointClick = this.handlePointClick.bind(this);
-+ }
-+
-+ handlePointClick(datum, seriesIndex) {
-+ if (this.props.onPointClick && datum.csnId) {
-+ this.props.onPointClick(datum);
-+ }
- }
-
- componentDidMount() {
-@@ -1267,15 +1295,7 @@ class ScatterLineChart extends React.PureComponent {
- const { series, yDomain } = this._getSeriesSnapshot();
-
- // Helper function to format time values
-- const formatTimeValue = (seconds) => {
-- if (seconds >= 3600) {
-- return `${(seconds / 3600).toFixed(3)}h`;
-- } else if (seconds >= 60) {
-- return `${(seconds / 60).toFixed(3)}m`;
-- } else {
-- return `${seconds.toFixed(3)}s`;
-- }
-- };
-+ const formatTimeValue = (seconds) => formatLagSeconds(seconds, 3);
-
- // Process tooltip HTML tags
- const formatTooltip = (datum) => {
-@@ -1308,13 +1328,35 @@ class ScatterLineChart extends React.PureComponent {
- labels={({ datum }) => formatTooltip(datum)}
- constrainToVisibleArea
- labelComponent={
-- <ChartTooltip
-- style={{
-- fontSize: "12px",
-- padding: 10,
-- whiteSpace: "pre-line" // Important for newlines
-- }}
-- />
-+ <ChartTooltip
-+ orientation={({ datum }) => {
-+ // Position tooltip below for high points, above for low points
-+ // This prevents the tooltip from blocking clicks on points near the top
-+ const yMax = yDomain.max;
-+ const yMin = yDomain.min;
-+ const yRange = yMax - yMin;
-+ const threshold = yMin + (yRange * 0.6);
-+ return datum.y > threshold ? "bottom" : "top";
-+ }}
-+ style={{
-+ fontSize: "12px",
-+ padding: 10,
-+ whiteSpace: "pre-line", // Important for newlines
-+ pointerEvents: "none"
-+ }}
-+ flyoutStyle={{
-+ pointerEvents: "none"
-+ }}
-+ dx={0}
-+ dy={({ datum }) => {
-+ // Add extra offset to keep tooltip away from the point
-+ const yMax = yDomain.max;
-+ const yMin = yDomain.min;
-+ const yRange = yMax - yMin;
-+ const threshold = yMin + (yRange * 0.6);
-+ return datum.y > threshold ? 10 : -10;
-+ }}
-+ />
- }
- />
- }
-@@ -1410,6 +1452,7 @@ class ScatterLineChart extends React.PureComponent {
- if (this.state.hiddenSeries[idx]) {
- return null;
- }
-+ const hasClickHandler = !!this.props.onPointClick;
- return (
- <ChartScatter
- key={`scatter-${idx}`}
-@@ -1417,9 +1460,39 @@ class ScatterLineChart extends React.PureComponent {
- data={s.datapoints}
- style={{
- data: {
-- fill: s.color
-+ fill: s.color,
-+ cursor: hasClickHandler ? 'pointer' : 'default'
- }
- }}
-+ events={hasClickHandler ? [{
-+ target: "data",
-+ eventHandlers: {
-+ onClick: () => [{
-+ target: "data",
-+ mutation: (props) => {
-+ this.handlePointClick(props.datum, idx);
-+ return null;
-+ }
-+ }],
-+ onMouseOver: () => [{
-+ target: "data",
-+ mutation: (props) => ({
-+ style: {
-+ ...props.style,
-+ fill: s.color,
-+ cursor: 'pointer',
-+ strokeWidth: 2,
-+ stroke: 'var(--pf-v5-global--active-color--100, #0066cc)',
-+ r: 6
-+ }
-+ })
-+ }],
-+ onMouseOut: () => [{
-+ target: "data",
-+ mutation: () => null
-+ }]
-+ }
-+ }] : undefined}
- />
- );
- })}
-@@ -1501,17 +1574,320 @@ class ScatterLineChart extends React.PureComponent {
- }
- }
-
-+/**
-+ * CSNDetailModal - Displays detailed CSN propagation path information
-+ * Shows the hop-by-hop timing of how a change propagated through the replication topology
-+ */
-+class CSNDetailModal extends React.Component {
-+ constructor(props) {
-+ super(props);
-+ this.formatTimestamp = this.formatTimestamp.bind(this);
-+ this.formatLag = this.formatLag.bind(this);
-+ }
-+
-+ formatTimestamp(isoString) {
-+ if (!isoString) return _("Unknown");
-+ try {
-+ const date = new Date(isoString);
-+ if (isNaN(date.getTime())) {
-+ console.warn("Invalid timestamp format:", isoString);
-+ return cockpit.format(_("Invalid: $0"), isoString);
-+ }
-+ return date.toLocaleString(undefined, {
-+ year: 'numeric',
-+ month: '2-digit',
-+ day: '2-digit',
-+ hour: '2-digit',
-+ minute: '2-digit',
-+ second: '2-digit',
-+ fractionalSecondDigits: 3,
-+ hour12: false
-+ });
-+ } catch (e) {
-+ console.warn("Error formatting timestamp:", isoString, e);
-+ return cockpit.format(_("Invalid: $0"), isoString);
-+ }
-+ }
-+
-+ formatLag(seconds) {
-+ return formatLagSeconds(seconds, 3) || _("N/A");
-+ }
-+
-+ render() {
-+ const { csnData, onClose } = this.props;
-+
-+ if (!csnData) {
-+ return null;
-+ }
-+
-+ const arrivals = csnData.arrivals || [];
-+ const pathJson = JSON.stringify(csnData, null, 2);
-+
-+ return (
-+ <Modal
-+ variant={ModalVariant.large}
-+ title={_("CSN Propagation Details")}
-+ isOpen={!!csnData}
-+ onClose={onClose}
-+ aria-label={_("CSN propagation details")}
-+ actions={[
-+ <Button key="close" variant="primary" onClick={onClose}>
-+ {_("Close")}
-+ </Button>
-+ ]}
-+ >
-+ {/* CSN Summary Information */}
-+ <Card isFlat className="ds-margin-bottom-md">
-+ <CardBody>
-+ <Grid hasGutter>
-+ <GridItem span={6}>
-+ <DescriptionList isHorizontal isCompact>
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("CSN")}</DescriptionListTerm>
-+ <DescriptionListDescription>
-+ <ClipboardCopy
-+ variant={ClipboardCopyVariant.inline}
-+ >
-+ {csnData.csn}
-+ </ClipboardCopy>
-+ </DescriptionListDescription>
-+ </DescriptionListGroup>
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("Entry DN")}</DescriptionListTerm>
-+ <DescriptionListDescription>
-+ <Tooltip content={csnData.targetDn}>
-+ <span className="pf-v5-u-text-truncate" style={{ maxWidth: '300px', display: 'inline-block' }}>
-+ {csnData.targetDn}
-+ </span>
-+ </Tooltip>
-+ </DescriptionListDescription>
-+ </DescriptionListGroup>
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("Suffix")}</DescriptionListTerm>
-+ <DescriptionListDescription>{csnData.suffix}</DescriptionListDescription>
-+ </DescriptionListGroup>
-+ </DescriptionList>
-+ </GridItem>
-+ <GridItem span={6}>
-+ <DescriptionList isHorizontal isCompact>
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("Origin Server")}</DescriptionListTerm>
-+ <DescriptionListDescription>
-+ <Label color="blue" icon={<ServerIcon />}>
-+ {csnData.originServer}
-+ </Label>
-+ </DescriptionListDescription>
-+ </DescriptionListGroup>
-+ {csnData.originIncludedInArrivals === false && (
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("Origin Note")}</DescriptionListTerm>
-+ <DescriptionListDescription>
-+ <Text component={TextVariants.small} style={{ color: 'var(--pf-v5-global--Color--200)' }}>
-+ {_("Origin server record is outside the selected time range; entry details reflect the earliest arrival.")}
-+ </Text>
-+ </DescriptionListDescription>
-+ </DescriptionListGroup>
-+ )}
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("Total Lag")}</DescriptionListTerm>
-+ <DescriptionListDescription>
-+ <strong>{this.formatLag(csnData.globalLag)}</strong>
-+ </DescriptionListDescription>
-+ </DescriptionListGroup>
-+ <DescriptionListGroup>
-+ <DescriptionListTerm>{_("Servers Reached")}</DescriptionListTerm>
-+ <DescriptionListDescription>
-+ {csnData.serverCount}
-+ {csnData.replicatedToAll && (
-+ <Label color="green" icon={<CheckCircleIcon />} className="ds-left-margin">
-+ {_("All")}
-+ </Label>
-+ )}
-+ </DescriptionListDescription>
-+ </DescriptionListGroup>
-+ </DescriptionList>
-+ </GridItem>
-+ </Grid>
-+ </CardBody>
-+ </Card>
-+
-+ {/* Arrival Timeline Visualization */}
-+ <Card isFlat className="ds-margin-bottom-md">
-+ <CardTitle>{_("Arrival Timeline")}</CardTitle>
-+ <CardBody>
-+ <Text
-+ component={TextVariants.small}
-+ className="ds-margin-bottom"
-+ style={{ color: 'var(--pf-v5-global--Color--200)', fontStyle: 'italic' }}
-+ >
-+ {_("Note: Shows arrival order by time. Actual replication topology may differ in fan-out configurations.")}
-+ </Text>
-+ <div
-+ role="list"
-+ aria-label={_("CSN propagation timeline")}
-+ style={{
-+ display: 'flex',
-+ flexWrap: 'wrap',
-+ alignItems: 'center',
-+ gap: 'var(--pf-v5-global--spacer--sm)',
-+ padding: 'var(--pf-v5-global--spacer--sm)'
-+ }}
-+ >
-+ {arrivals.map((arrival, idx) => (
-+ <React.Fragment key={idx}>
-+ {/* Server Node */}
-+ <div
-+ role="listitem"
-+ aria-label={cockpit.format(
-+ arrival.isOrigin
-+ ? _("Origin server: $0")
-+ : _("Server $0, delay: $1"),
-+ arrival.server,
-+ arrival.isOrigin ? "" : this.formatLag(arrival.relativeDelay)
-+ )}
-+ style={{
-+ display: 'flex',
-+ flexDirection: 'column',
-+ alignItems: 'center',
-+ padding: 'var(--pf-v5-global--spacer--sm)',
-+ backgroundColor: arrival.isOrigin
-+ ? 'var(--pf-v5-global--palette--blue-50, #e7f1fa)'
-+ : 'var(--pf-v5-global--BackgroundColor--200, #f0f0f0)',
-+ borderRadius: 'var(--pf-v5-global--BorderRadius--sm)',
-+ border: arrival.isOrigin
-+ ? '2px solid var(--pf-v5-global--primary-color--100, #0066cc)'
-+ : '1px solid var(--pf-v5-global--BorderColor--100, #d2d2d2)',
-+ minWidth: '120px'
-+ }}>
-+ <Text component={TextVariants.small} style={{ fontWeight: 'bold' }}>
-+ {arrival.server}
-+ </Text>
-+ <Text component={TextVariants.small} style={{ fontSize: '0.75rem', color: 'var(--pf-v5-global--Color--200)' }}>
-+ {this.formatTimestamp(arrival.timestamp)}
-+ </Text>
-+ {arrival.isOrigin && (
-+ <Label color="blue" isCompact style={{ marginTop: '4px' }}>
-+ {_("Origin")}
-+ </Label>
-+ )}
-+ {!arrival.isOrigin && (
-+ <Text component={TextVariants.small} style={{ marginTop: '4px', color: 'var(--pf-v5-global--success-color--100)' }}>
-+ +{this.formatLag(arrival.relativeDelay)}
-+ </Text>
-+ )}
-+ </div>
-+
-+ {/* Arrow between nodes */}
-+ {idx < arrivals.length - 1 && (
-+ <div
-+ role="presentation"
-+ aria-hidden="true"
-+ style={{
-+ display: 'flex',
-+ flexDirection: 'column',
-+ alignItems: 'center',
-+ padding: '0 var(--pf-v5-global--spacer--xs)'
-+ }}
-+ >
-+ <ArrowRightIcon style={{ color: 'var(--pf-v5-global--Color--200)' }} />
-+ <Text component={TextVariants.small} style={{
-+ fontSize: '0.7rem',
-+ color: 'var(--pf-v5-global--Color--200)',
-+ whiteSpace: 'nowrap'
-+ }}>
-+ {arrivals[idx + 1].hopLag !== undefined
-+ ? this.formatLag(arrivals[idx + 1].hopLag)
-+ : ''}
-+ </Text>
-+ </div>
-+ )}
-+ </React.Fragment>
-+ ))}
-+ </div>
-+ </CardBody>
-+ </Card>
-+
-+ {/* Detailed Arrivals Table */}
-+ <Card isFlat className="ds-margin-bottom-md">
-+ <CardTitle>{_("Arrival Details")}</CardTitle>
-+ <CardBody>
-+ <table className="pf-v5-c-table pf-m-compact" role="grid">
-+ <thead>
-+ <tr>
-+ <th>{_("Server")}</th>
-+ <th>{_("Arrival Time")}</th>
-+ <th>{_("Hop Lag")}</th>
-+ <th>{_("Cumulative Delay")}</th>
-+ <th>{_("Duration")}</th>
-+ </tr>
-+ </thead>
-+ <tbody>
-+ {arrivals.map((arrival, idx) => (
-+ <tr key={idx}>
-+ <td>
-+ {arrival.server}
-+ {arrival.isOrigin && (
-+ <span style={{ color: 'var(--pf-v5-global--primary-color--100)', marginLeft: 'var(--pf-v5-global--spacer--xs)' }}>
-+ ({_("Origin")})
-+ </span>
-+ )}
-+ </td>
-+ <td>{this.formatTimestamp(arrival.timestamp)}</td>
-+ <td>
-+ {arrival.isOrigin
-+ ? <em>{_("Origin")}</em>
-+ : this.formatLag(arrival.hopLag)}
-+ </td>
-+ <td>{this.formatLag(arrival.relativeDelay)}</td>
-+ <td>{arrival.duration ? this.formatLag(arrival.duration) : _("N/A")}</td>
-+ </tr>
-+ ))}
-+ </tbody>
-+ </table>
-+ </CardBody>
-+ </Card>
-+
-+ {/* Copy Actions */}
-+ <Split hasGutter>
-+ <SplitItem>
-+ <ClipboardCopy
-+ variant={ClipboardCopyVariant.expansion}
-+ isExpanded={false}
-+ isCode
-+ isReadOnly
-+ hoverTip={_("Copy full path JSON")}
-+ clickTip={_("Copied!")}
-+ >
-+ {pathJson}
-+ </ClipboardCopy>
-+ </SplitItem>
-+ </Split>
-+ </Modal>
-+ );
-+ }
-+}
-+
-+CSNDetailModal.propTypes = {
-+ csnData: PropTypes.object,
-+ onClose: PropTypes.func.isRequired
-+};
-+
-+CSNDetailModal.defaultProps = {
-+ csnData: null
-+};
-+
- class LagReportModal extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
-- activeTabKey: 0, // 0 = Summary, 1 = Charts, 2 = PNG Report, 3 = CSV Report, 4 = Report Files
-+ activeTabKey: 0,
- ...this._freshReportState(),
- loadingSummary: false,
- loadingJson: false,
- loadingCsv: false,
-- loadingPng: false
-+ loadingPng: false,
-+ selectedCsnId: null
- };
-
- this.handleTabClick = this.handleTabClick.bind(this);
-@@ -1524,6 +1900,8 @@ class LagReportModal extends React.Component {
- this.renderPngTab = this.renderPngTab.bind(this);
- this.renderCsvTab = this.renderCsvTab.bind(this);
- this.renderReportFilesTab = this.renderReportFilesTab.bind(this);
-+ this.handleCsnPointClick = this.handleCsnPointClick.bind(this);
-+ this.handleCloseCsnDetails = this.handleCloseCsnDetails.bind(this);
-
- this._activeLoadToken = 0;
- this._isMounted = false;
-@@ -1543,10 +1921,26 @@ class LagReportModal extends React.Component {
- summary: null,
- suffixStats: {},
- clientSamplingNotice: null,
-+ selectedCsnId: null,
- ...overrides
- };
- }
-
-+ handleCsnPointClick(datum) {
-+ if (datum && datum.csnId) {
-+ const { jsonData } = this.state;
-+ if (jsonData && jsonData.csnDetails && jsonData.csnDetails[datum.csnId]) {
-+ this.setState({ selectedCsnId: datum.csnId });
-+ } else {
-+ console.warn("CSN details not available for:", datum.csnId);
-+ }
-+ }
-+ }
-+
-+ handleCloseCsnDetails() {
-+ this.setState({ selectedCsnId: null });
-+ }
-+
- componentDidMount() {
- this._isMounted = true;
- this.loadData();
-@@ -2117,7 +2511,7 @@ class LagReportModal extends React.Component {
-
- renderChartsTab() {
- const { reportUrls } = this.props;
-- const { loadingJson, jsonData, error, clientSamplingNotice } = this.state;
-+ const { loadingJson, jsonData, error, clientSamplingNotice, selectedCsnId } = this.state;
-
- if (loadingJson) {
- return (
-@@ -2170,6 +2564,13 @@ class LagReportModal extends React.Component {
- jsonData.hopLags.series &&
- jsonData.hopLags.series.length > 0;
-
-+ const hasCsnDetails = jsonData && jsonData.csnDetails &&
-+ Object.keys(jsonData.csnDetails).length > 0;
-+
-+ const selectedCsnData = selectedCsnId && hasCsnDetails
-+ ? jsonData.csnDetails[selectedCsnId]
-+ : null;
-+
- if (!jsonData || (!hasReplicationLags && !hasHopLags)) {
- return (
- <EmptyState>
-@@ -2201,6 +2602,15 @@ class LagReportModal extends React.Component {
- {clientSamplingNotice}
- </Alert>
- )}
-+ {hasCsnDetails && (
-+ <Text
-+ component={TextVariants.small}
-+ className="ds-margin-bottom"
-+ style={{ color: 'var(--pf-v5-global--Color--200)', fontStyle: 'italic' }}
-+ >
-+ {_("Tip: Click on any chart point to view detailed CSN propagation path.")}
-+ </Text>
-+ )}
- {hasReplicationLags && (
- <div className="ds-margin-bottom">
- <Title headingLevel="h3">
-@@ -2231,6 +2641,7 @@ class LagReportModal extends React.Component {
- xAxisLabel={(jsonData.replicationLags.xAxisLabel || "").replace(/\s*Time\s*/g, "")}
- yAxisLabel={jsonData.replicationLags.yAxisLabel || _("Lag Time (seconds)")}
- defaultShowLegend={true}
-+ onPointClick={hasCsnDetails ? this.handleCsnPointClick : undefined}
- />
- </div>
- )}
-@@ -2250,9 +2661,17 @@ class LagReportModal extends React.Component {
- xAxisLabel={(jsonData.hopLags.xAxisLabel || "").replace(/\s*Time\s*/g, "")}
- yAxisLabel={jsonData.hopLags.yAxisLabel || _("Hop Lag Time (seconds)")}
- defaultShowLegend={false}
-+ onPointClick={hasCsnDetails ? this.handleCsnPointClick : undefined}
- />
- </div>
- )}
-+
-+ {/* CSN Detail Panel - shown when a point is clicked */}
-+ <CSNDetailModal
-+ csnData={selectedCsnData}
-+ onClose={this.handleCloseCsnDetails}
-+ />
-+
- <div className="ds-margin-top">
- <Button
- variant="secondary"
-@@ -2876,6 +3295,28 @@ class ChooseLagReportModal extends React.Component {
- }
-
- // Prototypes and defaultProps
-+ScatterLineChart.propTypes = {
-+ chartData: PropTypes.object,
-+ title: PropTypes.string,
-+ yAxisLabel: PropTypes.string,
-+ xAxisLabel: PropTypes.string,
-+ minY: PropTypes.number,
-+ maxY: PropTypes.number,
-+ defaultShowLegend: PropTypes.bool,
-+ onPointClick: PropTypes.func
-+};
-+
-+ScatterLineChart.defaultProps = {
-+ chartData: null,
-+ title: "",
-+ yAxisLabel: "Value",
-+ xAxisLabel: "",
-+ minY: null,
-+ maxY: null,
-+ defaultShowLegend: true,
-+ onPointClick: null
-+};
-+
- AgmtDetailsModal.propTypes = {
- showModal: PropTypes.bool,
- closeHandler: PropTypes.func,
-@@ -3044,6 +3485,7 @@ export {
- ReportLoginModal,
- FullReportContent,
- LagReportModal,
-- ChooseLagReportModal
-+ ChooseLagReportModal,
-+ CSNDetailModal
- };
-
-diff --git a/src/lib389/lib389/repltools.py b/src/lib389/lib389/repltools.py
-index 9d1aa4058..5ab8b3187 100644
---- a/src/lib389/lib389/repltools.py
-+++ b/src/lib389/lib389/repltools.py
-@@ -683,6 +683,7 @@ class ChartData(NamedTuple):
- lags: List[float]
- durations: List[float]
- hover: List[str]
-+ csn_ids: List[str]
-
- class VisualizationHelper:
- """Helper class for visualization-related functionality."""
-@@ -730,7 +731,7 @@ class VisualizationHelper:
- tz: tzinfo = timezone.utc) -> Dict[Tuple[str, str], ChartData]:
- """Prepare data for visualization with timezone-aware timestamps."""
- chart_data = defaultdict(lambda: {
-- 'times': [], 'lags': [], 'durations': [], 'hover': []
-+ 'times': [], 'lags': [], 'durations': [], 'hover': [], 'csn_ids': []
- })
-
- for csn, server_map in csns.items():
-@@ -766,6 +767,7 @@ class VisualizationHelper:
- data_slot['times'].append(ts_dt)
- data_slot['lags'].append(lag_val) # The same global-lag for all servers
- data_slot['durations'].append(duration_val)
-+ data_slot['csn_ids'].append(csn)
- # Format timestamp for hover display in the specified timezone
- timestamp_str = ts_dt.strftime('%Y-%m-%d %H:%M:%S')
- data_slot['hover'].append(
-@@ -784,7 +786,8 @@ class VisualizationHelper:
- times=value['times'],
- lags=value['lags'],
- durations=value['durations'],
-- hover=value['hover']
-+ hover=value['hover'],
-+ csn_ids=value['csn_ids']
- )
- for key, value in chart_data.items()
- }
-@@ -803,6 +806,14 @@ class ReplicationLogAnalyzer:
- AUTO_SAMPLING_THRESHOLD = 4000 # Trigger auto sampling above this many CSN points
- HOP_SERIES_BUDGET_RATIO = 0.25 # Allocate max 25% of chart points to hop lag series
- MIN_POINTS_PER_SERIES = 2 # Minimum points to preserve series shape after sampling
-+ MAX_CSN_DETAILS = 10000 # Maximum CSN details to include (prevents memory issues)
-+
-+ # CSN format: TTTTTTTTSSSSRRRRNNNN (20 hex chars)
-+ # T=timestamp(8), S=sequence(4), R=replicaID(4), N=subseq(4)
-+ CSN_TIMESTAMP_START = 0
-+ CSN_TIMESTAMP_END = 8
-+ CSN_REPLICA_ID_START = 12
-+ CSN_REPLICA_ID_END = 16
-
- # Precision preset configurations
- PRECISION_PRESETS = {
-@@ -867,6 +878,54 @@ class ReplicationLogAnalyzer:
- # Threshold to trigger auto sampling if lots of CSNs
- self._auto_sampling_csn_threshold = self.AUTO_SAMPLING_THRESHOLD
-
-+ # Mapping of (replica ID, suffix) to server name
-+ # Built during log parsing to correctly identify origin servers
-+ # Keyed by (replica_id, suffix) because replica IDs are per-suffix in multi-suffix deployments
-+ self._replica_id_to_server: Dict[Tuple[str, str], str] = {}
-+
-+ @staticmethod
-+ def _extract_replica_id_from_csn(csn: str) -> Optional[str]:
-+ """Extract the replica ID from a CSN string.
-+
-+ CSN format: TTTTTTTTSSSSRRRRNNNN (20 hex characters)
-+ - T = timestamp (8 chars)
-+ - S = sequence (4 chars)
-+ - R = replica ID (4 chars)
-+ - N = subseq (4 chars)
-+
-+ :param csn: The CSN string
-+ :returns: The replica ID as a string (4 hex chars), or None if invalid
-+ """
-+ if not csn or not isinstance(csn, str) or len(csn) < 16:
-+ return None
-+ try:
-+ # Extract and validate replica ID is valid hex
-+ replica_id = csn[ReplicationLogAnalyzer.CSN_REPLICA_ID_START:
-+ ReplicationLogAnalyzer.CSN_REPLICA_ID_END]
-+ int(replica_id, 16) # Validate it's valid hex
-+ return replica_id
-+ except (ValueError, IndexError):
-+ return None
-+
-+ @staticmethod
-+ def _extract_timestamp_from_csn(csn: str) -> Optional[float]:
-+ """Extract the timestamp (epoch seconds) from a CSN string.
-+
-+ CSN format: TTTTTTTTSSSSRRRRNNNN (20 hex characters)
-+ - T = timestamp (8 chars, hex, seconds since epoch)
-+
-+ :param csn: The CSN string
-+ :returns: Timestamp as float (epoch seconds), or None if invalid
-+ """
-+ if not csn or not isinstance(csn, str) or len(csn) < 8:
-+ return None
-+ try:
-+ ts_hex = csn[ReplicationLogAnalyzer.CSN_TIMESTAMP_START:
-+ ReplicationLogAnalyzer.CSN_TIMESTAMP_END]
-+ return float(int(ts_hex, 16))
-+ except (ValueError, IndexError):
-+ return None
-+
- def _should_include_record(self, csn: str, server_map: Dict[Union[int, str], Dict[str, Any]]) -> bool:
- """Determine if a record should be included based on filtering criteria."""
- total_servers = self._active_server_count or len(self.log_dirs)
-@@ -990,6 +1049,148 @@ class ReplicationLogAnalyzer:
-
- return hops
-
-+ def _build_csn_details(self, csn_whitelist: Optional[set] = None) -> Dict[str, Dict[str, Any]]:
-+ """Build detailed CSN propagation information for drill-down functionality.
-+
-+ Returns a dictionary keyed by CSN containing:
-+ - csn: The CSN string
-+ - targetDn: The target entry DN
-+ - suffix: The replication suffix
-+ - globalLag: Total propagation time (earliest to latest arrival)
-+ - originServer: The server where the change originated (determined by CSN replica ID)
-+ - originTime: ISO timestamp of origin
-+ - arrivals: Ordered list of server arrivals with timing details
-+ - hops: List of server-to-server hops with lag times
-+ - totalHops: Number of hops in the propagation path
-+ - replicatedToAll: Whether the change reached all servers
-+
-+ Note: Origin server is determined by the replica ID embedded in the CSN,
-+ not by earliest log timestamp (which can be incorrect under clock skew).
-+
-+ :param csn_whitelist: Optional set of CSN IDs to include. If provided,
-+ only these CSNs will have details generated.
-+ This prevents memory bloat when chart data is sampled.
-+ """
-+ csn_details = {}
-+ total_servers = self._active_server_count or len(self.log_dirs)
-+
-+ if csn_whitelist is not None:
-+ csn_items = [(csn, sm) for csn, sm in self.csns.items() if csn in csn_whitelist]
-+ else:
-+ csn_items = list(self.csns.items())
-+
-+ if len(csn_items) > self.MAX_CSN_DETAILS:
-+ self._logger.info(
-+ f"CSN details limited to {self.MAX_CSN_DETAILS} entries "
-+ f"(dataset has {len(csn_items)} CSNs). "
-+ "Selecting CSNs with highest global lag for drill-down."
-+ )
-+ csn_lags = []
-+ for csn, server_map in csn_items:
-+ t_list = [
-+ rec.get('logtime', 0)
-+ for key, rec in server_map.items()
-+ if isinstance(rec, dict) and key != '__hop_lags__' and 'logtime' in rec
-+ ]
-+ if t_list:
-+ lag = max(t_list) - min(t_list)
-+ csn_lags.append((csn, server_map, lag))
-+ csn_lags.sort(key=lambda x: x[2], reverse=True)
-+ csn_items = [(csn, sm) for csn, sm, _ in csn_lags[:self.MAX_CSN_DETAILS]]
-+
-+ for csn, server_map in csn_items:
-+ valid_records = []
-+ for key, data in server_map.items():
-+ if isinstance(data, dict) and key != '__hop_lags__' and 'logtime' in data:
-+ valid_records.append(data)
-+
-+ if not valid_records:
-+ continue
-+
-+ valid_records.sort(key=lambda x: x['logtime'])
-+
-+ suffix = None
-+ for rec in valid_records:
-+ if rec.get('suffix'):
-+ suffix = rec['suffix']
-+ break
-+
-+ replica_id = self._extract_replica_id_from_csn(csn)
-+ origin_server_name = None
-+ origin_record = None
-+
-+ origin_in_arrivals = False
-+ if replica_id and suffix:
-+ map_key = (replica_id, suffix)
-+ if map_key in self._replica_id_to_server:
-+ origin_server_name = self._replica_id_to_server[map_key]
-+ for rec in valid_records:
-+ if rec.get('server_name') == origin_server_name:
-+ origin_record = rec
-+ origin_in_arrivals = True
-+ break
-+
-+ if origin_record is None:
-+ origin_record = valid_records[0]
-+ if not origin_server_name:
-+ origin_server_name = origin_record.get('server_name', 'unknown')
-+
-+ csn_ts = self._extract_timestamp_from_csn(csn)
-+ origin_time = csn_ts if csn_ts is not None else origin_record['logtime']
-+ earliest_time = valid_records[0]['logtime']
-+ latest_time = valid_records[-1]['logtime']
-+ global_lag = latest_time - earliest_time
-+
-+ arrivals = []
-+ for idx, rec in enumerate(valid_records):
-+ server_name = rec.get('server_name', 'unknown')
-+ is_origin = (server_name == origin_server_name)
-+
-+ arrival_entry = {
-+ 'server': server_name,
-+ 'timestamp': datetime.fromtimestamp(rec['logtime'], tz=self.tz).isoformat(),
-+ 'relativeDelay': rec['logtime'] - earliest_time,
-+ 'duration': float(rec.get('duration', 0.0)),
-+ 'etime': rec.get('etime')
-+ }
-+
-+ if is_origin:
-+ arrival_entry['isOrigin'] = True
-+
-+ if idx > 0:
-+ prev_rec = valid_records[idx - 1]
-+ arrival_entry['hopFrom'] = prev_rec.get('server_name', 'unknown')
-+ arrival_entry['hopLag'] = rec['logtime'] - prev_rec['logtime']
-+
-+ arrivals.append(arrival_entry)
-+
-+ hops = []
-+ for i in range(1, len(valid_records)):
-+ prev_rec = valid_records[i - 1]
-+ curr_rec = valid_records[i]
-+ hops.append({
-+ 'from': prev_rec.get('server_name', 'unknown'),
-+ 'to': curr_rec.get('server_name', 'unknown'),
-+ 'lag': curr_rec['logtime'] - prev_rec['logtime']
-+ })
-+
-+ csn_details[csn] = {
-+ 'csn': csn,
-+ 'targetDn': origin_record.get('target_dn', 'unknown') or 'unknown',
-+ 'suffix': origin_record.get('suffix', 'unknown') or 'unknown',
-+ 'globalLag': global_lag,
-+ 'originServer': origin_server_name,
-+ 'originIncludedInArrivals': origin_in_arrivals,
-+ 'originTime': datetime.fromtimestamp(origin_time, tz=self.tz).isoformat(),
-+ 'arrivals': arrivals,
-+ 'hops': hops,
-+ 'totalHops': len(hops),
-+ 'serverCount': len(valid_records),
-+ 'replicatedToAll': len(valid_records) == total_servers
-+ }
-+
-+ return csn_details
-+
- def parse_logs(self) -> None:
- """Parse logs from all directories. Each directory is treated as one server
- unless anonymized, in which case we use 'server_{index}'.
-@@ -1044,6 +1245,41 @@ class ReplicationLogAnalyzer:
- 'duration': record.get('duration', 0.0),
- }
-
-+ # Build (replica ID, suffix) to server mapping based on closest CSN timestamp
-+ # Keyed by (replica_id, suffix) because replica IDs are per-suffix in multi-suffix deployments
-+ # For each (replica ID, suffix) pair, the server whose logtime is closest to the CSN
-+ # timestamp is the best origin candidate under clock skew.
-+ replica_id_best: Dict[Tuple[str, str], Tuple[bool, float, float, str]] = {}
-+ for csn, server_map in self.csns.items():
-+ replica_id = self._extract_replica_id_from_csn(csn)
-+ if not replica_id:
-+ continue
-+ csn_ts = self._extract_timestamp_from_csn(csn)
-+ for key, record in server_map.items():
-+ if not isinstance(record, dict) or key == '__hop_lags__':
-+ continue
-+ logtime = record.get('logtime')
-+ server_name = record.get('server_name')
-+ suffix = record.get('suffix')
-+ if logtime is None or not server_name or not suffix:
-+ continue
-+ # Prefer candidates where we can compare against CSN timestamp
-+ has_csn_ts = csn_ts is not None
-+ score = abs(logtime - csn_ts) if has_csn_ts else logtime
-+ map_key = (replica_id, suffix)
-+ if map_key not in replica_id_best:
-+ replica_id_best[map_key] = (has_csn_ts, score, logtime, server_name)
-+ continue
-+ prev_has_ts, prev_score, prev_logtime, _ = replica_id_best[map_key]
-+ if has_csn_ts and not prev_has_ts:
-+ replica_id_best[map_key] = (has_csn_ts, score, logtime, server_name)
-+ elif has_csn_ts == prev_has_ts:
-+ if score < prev_score or (score == prev_score and logtime < prev_logtime):
-+ replica_id_best[map_key] = (has_csn_ts, score, logtime, server_name)
-+
-+ # Store the mapping ((replica ID, suffix) -> server name)
-+ self._replica_id_to_server = {k: srv for k, (_, _, _, srv) in replica_id_best.items()}
-+
- # Apply filters after collecting all data
- filtered_csns = {}
- earliest_udt: Optional[float] = None
-@@ -1577,6 +1813,17 @@ class ReplicationLogAnalyzer:
- except Exception as e:
- raise IOError(f"Failed to write JSON summary to {outfile}: {e}")
-
-+ @staticmethod
-+ def _collect_csn_ids(series_list: List[Dict[str, Any]]) -> set:
-+ """Collect CSN IDs from chart series datapoints."""
-+ csn_ids = set()
-+ for series in series_list:
-+ for dp in series.get("datapoints", []):
-+ csn_id = dp.get("csnId")
-+ if csn_id:
-+ csn_ids.add(csn_id)
-+ return csn_ids
-+
- def _generate_patternfly_json(self, results: Dict[str, Any], outfile: str) -> None:
- """Generate JSON specifically formatted for PatternFly 5 charts."""
- chart_data = VisualizationHelper.prepare_chart_data(self.csns, self.tz)
-@@ -1637,7 +1884,8 @@ class ReplicationLogAnalyzer:
- "x": data.times[i].isoformat(),
- "y": data.lags[i],
- "duration": data.durations[i],
-- "hoverInfo": data.hover[i]
-+ "hoverInfo": data.hover[i],
-+ "csnId": data.csn_ids[i]
- } for i in indices]
- series_data.append({
- "datapoints": datapoints,
-@@ -1645,7 +1893,7 @@ class ReplicationLogAnalyzer:
- "color": color_palette[idx % len(color_palette)]
- })
-
-- hop_data: Dict[str, Dict[str, List[Any]]] = defaultdict(lambda: {"times": [], "lags": [], "hover": []})
-+ hop_data: Dict[str, Dict[str, List[Any]]] = defaultdict(lambda: {"times": [], "lags": [], "hover": [], "csn_ids": []})
- for csn, server_map in self.csns.items():
- for hop in server_map.get('__hop_lags__', []):
- source = hop.get('supplier', 'unknown')
-@@ -1655,6 +1903,7 @@ class ReplicationLogAnalyzer:
- ts = datetime.fromtimestamp(hop.get('arrival_consumer', 0.0), tz=self.tz)
- entry["times"].append(ts)
- entry["lags"].append(hop.get('hop_lag', 0.0))
-+ entry["csn_ids"].append(csn)
- ts_str = ts.strftime('%Y-%m-%d %H:%M:%S')
- entry["hover"].append(
- f"Timestamp: {ts_str}<br>"
-@@ -1689,7 +1938,10 @@ class ReplicationLogAnalyzer:
- "name": key,
- "x": entry["times"][i].isoformat(),
- "y": entry["lags"][i],
-- "hoverInfo": entry["hover"][i].replace("Suffix: None", "Suffix: unknown").replace("Entry: None", "Entry: unknown")
-+ "hoverInfo": (entry["hover"][i]
-+ .replace("Suffix: None", "Suffix: unknown")
-+ .replace("Entry: None", "Entry: unknown")),
-+ "csnId": entry["csn_ids"][i]
- } for i in indices]
- hop_series.append({
- "datapoints": datapoints,
-@@ -1702,6 +1954,13 @@ class ReplicationLogAnalyzer:
- reduced += sum(len(item["datapoints"]) for item in hop_series)
- sampling_meta["reducedTotalPoints"] = reduced
-
-+ sampled_csn_ids = None
-+ if sampling_meta["applied"]:
-+ sampled_csn_ids = self._collect_csn_ids(series_data)
-+ sampled_csn_ids.update(self._collect_csn_ids(hop_series))
-+
-+ csn_details = self._build_csn_details(csn_whitelist=sampled_csn_ids)
-+
- pf_data = {
- "replicationLags": {
- "title": "Global Replication Lag Over Time",
-@@ -1715,6 +1974,7 @@ class ReplicationLogAnalyzer:
- "xAxisLabel": "Time",
- "series": hop_series
- },
-+ "csnDetails": csn_details,
- "metadata": {
- "totalServers": self._active_server_count or len(self.log_dirs),
- "configuredLogDirs": self.log_dirs,
---
-2.52.0
-
diff --git a/0025-Issue-6753-Port-ticket-48896-test.patch b/0025-Issue-6753-Port-ticket-48896-test.patch
deleted file mode 100644
index 727049d..0000000
--- a/0025-Issue-6753-Port-ticket-48896-test.patch
+++ /dev/null
@@ -1,265 +0,0 @@
-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
-
diff --git a/0026-Issue-6810-Fix-PAM-PTA-test-7219.patch b/0026-Issue-6810-Fix-PAM-PTA-test-7219.patch
deleted file mode 100644
index b6cd689..0000000
--- a/0026-Issue-6810-Fix-PAM-PTA-test-7219.patch
+++ /dev/null
@@ -1,30 +0,0 @@
-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
-
diff --git a/0027-Issue-7076-Fix-revert_cache-never-called-in-modrdn-7.patch b/0027-Issue-7076-Fix-revert_cache-never-called-in-modrdn-7.patch
deleted file mode 100644
index 6ae4089..0000000
--- a/0027-Issue-7076-Fix-revert_cache-never-called-in-modrdn-7.patch
+++ /dev/null
@@ -1,57 +0,0 @@
-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
-
diff --git a/0028-Issue-6951-Dynamic-Certificate-refresh-phase-4-Updat.patch b/0028-Issue-6951-Dynamic-Certificate-refresh-phase-4-Updat.patch
deleted file mode 100644
index fb37375..0000000
--- a/0028-Issue-6951-Dynamic-Certificate-refresh-phase-4-Updat.patch
+++ /dev/null
@@ -1,1492 +0,0 @@
-From 3d63c2bc1ec7e89fe2ce6360ddd72748a6d1e1c5 Mon Sep 17 00:00:00 2001
-From: James Chapman <jachapma@redhat.com>
-Date: Thu, 5 Feb 2026 15:19:07 +0000
-Subject: [PATCH] Issue 6951 - Dynamic Certificate refresh phase 4 - Update
- lib389 and dsconf (#7171)
-
-Desciption:
-Add the CertManager abstraction layer and DynamicCerts backend module. Enhance
-the NssSsl backend to support importing PKCS#12 containers, containing cert and
-private key. Update dsconf to support new PKCS#12-related args, allowing users
-to supply passwords via text, stdin, or file.
-
-Fix:
-- Introduce CertManager abstraction layer for uniform cert management.
-- Implement DynamicCerts backend with add/list/delete operations.
-- Extend NssSsl.add_cert to handle PKCS#12 files with passwords via text, stdin, or file.
-- Update dsconf CLI to accept PKCS#12 arguments (--pkcs12-pin-text, --pkcs12-pin-stdin, --pkcs12-pin-path).
-
-Relates: https://github.com/389ds/389-ds-base/issues/6951
-
-Reviewed by: @progier389, @mreynolds389, @droideck (Thank you)
----
- .../tests/suites/clu/ca_cert_bundle_test.py | 5 +-
- .../tests/suites/clu/dsctl_tls_test.py | 4 +-
- .../suites/tls/tls_import_ca_chain_test.py | 2 +-
- src/lib389/lib389/cert_manager.py | 145 ++++++
- src/lib389/lib389/cli_conf/security.py | 252 ++++++----
- src/lib389/lib389/dyncerts.py | 458 ++++++++++++++++++
- src/lib389/lib389/nss_ssl.py | 196 ++++++--
- src/lib389/lib389/utils.py | 79 ++-
- 8 files changed, 987 insertions(+), 154 deletions(-)
- create mode 100644 src/lib389/lib389/cert_manager.py
- create mode 100644 src/lib389/lib389/dyncerts.py
-
-diff --git a/dirsrvtests/tests/suites/clu/ca_cert_bundle_test.py b/dirsrvtests/tests/suites/clu/ca_cert_bundle_test.py
-index 1d2ccc5a6..40ab0e093 100644
---- a/dirsrvtests/tests/suites/clu/ca_cert_bundle_test.py
-+++ b/dirsrvtests/tests/suites/clu/ca_cert_bundle_test.py
-@@ -83,7 +83,6 @@ jBqLRMRQN4FvzuCZiMl/DwJv4yhAZ8hylYjRjqjY/fEPvhvJRncPVy8z
- -----END CERTIFICATE-----
- """
-
--
- def test_ca_cert_bundle(topo):
- """Test we can add a CAS certificate bundle
-
-@@ -106,6 +105,8 @@ def test_ca_cert_bundle(topo):
-
- """
- inst = topo.standalone
-+ # Init NSS DB for dyncerts
-+ inst.enable_tls()
- lc = LogCapture()
-
- # Create PEM file with 2 CA certs
-@@ -119,6 +120,8 @@ def test_ca_cert_bundle(topo):
- args = FakeArgs()
- args.name = ['CA_CERT_1', 'CA_CERT_2']
- args.file = pem_file
-+ # Allow CA certs that fail verification (self-signed)
-+ args.force = True
- cacert_add(inst, DEFAULT_SUFFIX, log, args)
-
- # List CA certs
-diff --git a/dirsrvtests/tests/suites/clu/dsctl_tls_test.py b/dirsrvtests/tests/suites/clu/dsctl_tls_test.py
-index 9ce5b4e2a..df55b0f83 100644
---- a/dirsrvtests/tests/suites/clu/dsctl_tls_test.py
-+++ b/dirsrvtests/tests/suites/clu/dsctl_tls_test.py
-@@ -57,11 +57,11 @@ def test_tls_command_returns_error_text(topo):
- # dsctl localhost tls import-ca
- try:
- invalid_file = topo.standalone.confdir + '/dse.ldif'
-- tls.add_cert(nickname="bad", input_file=invalid_file)
-+ tls.add_cert(nickname="bad", cert_file=invalid_file)
- assert False
- except ValueError as e:
- assert '255' not in str(e)
-- assert 'Unable to load PEM file' in str(e)
-+ assert 'Unable to load certificate' in str(e)
-
- # dsctl localhost tls import-server-cert
- try:
-diff --git a/dirsrvtests/tests/suites/tls/tls_import_ca_chain_test.py b/dirsrvtests/tests/suites/tls/tls_import_ca_chain_test.py
-index 0be3a9aaa..4714703f0 100644
---- a/dirsrvtests/tests/suites/tls/tls_import_ca_chain_test.py
-+++ b/dirsrvtests/tests/suites/tls/tls_import_ca_chain_test.py
-@@ -38,7 +38,7 @@ def test_tls_import_chain(topology_st):
- tls.reinit()
-
- with pytest.raises(ValueError):
-- tls.add_cert(nickname='CA_CHAIN_1', input_file=CA_CHAIN_FILE)
-+ tls.add_cert(nickname='CA_CHAIN_1', cert_file=CA_CHAIN_FILE)
-
- with pytest.raises(ValueError):
- tls.import_rsa_crt(crt=CRT_CHAIN_FILE)
-diff --git a/src/lib389/lib389/cert_manager.py b/src/lib389/lib389/cert_manager.py
-new file mode 100644
-index 000000000..91c9c952f
---- /dev/null
-+++ b/src/lib389/lib389/cert_manager.py
-@@ -0,0 +1,145 @@
-+# --- 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 os
-+import logging
-+from typing import Optional
-+from lib389 import DirSrv
-+from lib389.nss_ssl import NssSsl
-+from lib389.dyncerts import DynamicCerts
-+from lib389.config import RSA
-+
-+log = logging.getLogger(__name__)
-+
-+class CertManager:
-+ """
-+ Certificate manager for 389 Directory Server.
-+
-+ Automatically selects DynamicCerts backend if available via LDAPI,
-+ otherwise falls back to NSS DB.
-+ """
-+
-+ def __init__(self, instance):
-+ """
-+ Initialise a CertManager object, which selects the appropriate
-+ certificate handler (DynamicCert or NSS).
-+
-+ :param instance: DirSrv instance
-+ :raises ValueError: If instance is None
-+ """
-+ if not isinstance(instance, DirSrv):
-+ raise ValueError("A DirSrv instance is required")
-+
-+ self.dirsrv = instance
-+
-+ if self.dirsrv.status():
-+ self.cert_handler = DynamicCerts(instance=self.dirsrv)
-+ else:
-+ self.cert_handler = NssSsl(dirsrv=self.dirsrv)
-+
-+ self.cert_handler_name = type(self.cert_handler).__name__
-+
-+ def list_certs(self):
-+ """
-+ Return a list of all certificates exposed by the backend.
-+
-+ :return: list of certificate dictionaries or tuples
-+ """
-+ return self.cert_handler.list_certs()
-+
-+ def list_ca_certs(self):
-+ """
-+ Return a list of all CA certificates exposed by the backend.
-+
-+ :return: list of CA certificate dictionaries or tuples
-+ """
-+ return self.cert_handler.list_ca_certs()
-+
-+ def get_cert(self, nickname: str):
-+ """
-+ Get certificate details by nickname/CN.
-+
-+ :param nickname: Certificate nickname
-+ :return: backend certificate object or None
-+ """
-+ return self.cert_handler.get_cert_details(nickname=nickname)
-+
-+ def del_cert(self, nickname: str):
-+ """
-+ Delete a certificate by nickname.
-+
-+ :param nickname: Certificate nickname
-+ :raises ValueError: If the nickname is empty.
-+ """
-+ if not nickname:
-+ raise ValueError("Certificate nickname cannot be empty")
-+
-+ self.cert_handler.del_cert(nickname)
-+
-+ def add_cert(
-+ self,
-+ cert_file: str,
-+ nickname: str,
-+ pkcs12_password: Optional[str] = None,
-+ primary: bool = False,
-+ ca: bool = False,
-+ force: bool = False
-+ ):
-+ """
-+ Add or replace a certificate.
-+
-+ :param cert_file: Path to certificate file (PEM, DER, or PKCS#12)
-+ :param nickname: Certificate nickname/CN
-+ :param pkcs12_password: Password for PKCS#12, if any
-+ :param primary: Set as the server's primary SSL certificate
-+ :param ca: Whether this certificate is a CA certificate
-+ :param force: Force the addition of a certificate that cannot be verified
-+ :raises ValueError: If cert_file or nickname is invalid
-+ """
-+ if not os.path.isfile(cert_file):
-+ raise ValueError(f"Certificate file not found: {cert_file}")
-+ if not nickname:
-+ raise ValueError("Certificate nickname must not be empty")
-+
-+ self.cert_handler.add_cert(
-+ nickname=nickname,
-+ cert_file=cert_file,
-+ pkcs12_password=pkcs12_password,
-+ ca=ca,
-+ force=force
-+ )
-+
-+ if primary:
-+ try:
-+ RSA(self.dirsrv).set("nsSSLPersonalitySSL", nickname)
-+ log.info(f"Set certificate '{nickname}' as primary SSL certificate.")
-+ except Exception as e:
-+ log.error(f"Failed to set primary SSL cert '{nickname}': {e}")
-+ raise
-+
-+ def add_ca_cert(self, cert_file: str, nickname: str, force: bool = False):
-+ """
-+ Add one or more CA certificates from a PEM bundle or single DER.
-+
-+ :param cert_file: Path to certificate file (PEM, DER)
-+ :param nickname: Certificate nickname
-+ :raises ValueError: If file is missing
-+ """
-+ if not os.path.exists(cert_file):
-+ raise ValueError(f"Certificate file does not exist: {cert_file}")
-+
-+ self.cert_handler.add_ca_cert(cert_file, nickname, force=force)
-+
-+ def edit_cert_trust(self, nickname: str, trust_flags: str):
-+ """
-+ Edit trust flags on an existing certificate.
-+
-+ :param nickname: Certificate nickname
-+ :param trust_flags: NSS style trust flag triplet, e.g. 'CT,,'
-+ """
-+ return self.cert_handler.edit_cert_trust(nickname=nickname, trust_flags=trust_flags)
-diff --git a/src/lib389/lib389/cli_conf/security.py b/src/lib389/lib389/cli_conf/security.py
-index eba138feb..18e444c4c 100644
---- a/src/lib389/lib389/cli_conf/security.py
-+++ b/src/lib389/lib389/cli_conf/security.py
-@@ -9,8 +9,10 @@
- from collections import OrderedDict, namedtuple
- import json
- import os
-+import sys
- from lib389.config import Config, Encryption, RSA
--from lib389.nss_ssl import NssSsl, CERT_NAME, CA_NAME
-+from lib389.nss_ssl import NssSsl
-+from lib389.cert_manager import CertManager
- from lib389.cli_base import _warn, CustomHelpFormatter
-
-
-@@ -140,6 +142,52 @@ def _security_generic_toggle_parsers(parent, cls, attr, help_pattern):
- return list(map(add_parser, ('Enable', 'Disable'), ('on', 'off')))
-
-
-+def _resolve_pkcs12_password(args):
-+ if args.pkcs12_pin_text:
-+ return args.pkcs12_pin_text
-+
-+ if args.pkcs12_pin_stdin:
-+ return sys.stdin.readline().rstrip("\n")
-+
-+ if args.pkcs12_pin_path:
-+ with open(args.pkcs12_pin_path) as f:
-+ return f.read().rstrip("\n")
-+
-+ return None
-+
-+def _dump_cert(cert, json_output: bool = False, log = None):
-+ """
-+ Print or return a certificate's details in text or JSON format.
-+
-+ :param cert: dict describing a certificate
-+ :param json_output: If True print JSON else print text
-+ :param log: Optional logger to output text
-+ """
-+ if not isinstance(cert, dict):
-+ raise TypeError(f"Expected dict, got {type(cert)}")
-+
-+ if json_output:
-+ return {
-+ "type": "certificate",
-+ "attrs": {
-+ "nickname": cert["cn"],
-+ "subject": cert["subject"],
-+ "issuer": cert["issuer"],
-+ "expires": cert["expires"],
-+ "flags": cert["trust_flags"],
-+ }
-+ }
-+ else:
-+ msg = (
-+ f"Certificate Name: {cert['cn']}\n"
-+ f"Subject DN: {cert['subject']}\n"
-+ f"Issuer DN: {cert['issuer']}\n"
-+ f"Expires: {cert['expires']}\n"
-+ f"Trust Flags: {cert['trust_flags']}\n"
-+ )
-+ if log:
-+ log.info(msg)
-+
- def security_enable(inst, basedn, log, args):
- dbpath = inst.get_cert_dir()
- tlsdb = NssSsl(dbpath=dbpath)
-@@ -150,7 +198,7 @@ def security_enable(inst, basedn, log, args):
-
- if len(certs) == 1:
- # If there is only cert make sure it is set as the server certificate
-- RSA(inst).set('nsSSLPersonalitySSL', certs[0][0])
-+ RSA(inst).set('nsSSLPersonalitySSL', certs[0]['cn'])
- elif args.cert_name is not None:
- # A certificate nickname was provided, set it as the server certificate
- RSA(inst).set('nsSSLPersonalitySSL', args.cert_name)
-@@ -222,26 +270,27 @@ def cert_add(inst, basedn, log, args):
- if not os.path.isfile(args.file):
- raise ValueError(f'Certificate file "{args.file}" does not exist')
-
-- tlsdb = NssSsl(dirsrv=inst)
-- if not tlsdb._db_exists(even_partial=True): # we want to be very careful
-- log.info('Security database does not exist. Creating a new one in {}.'.format(inst.get_cert_dir()))
-- tlsdb.reinit()
-- try:
-- tlsdb.get_cert_details(args.name)
-- raise ValueError("Certificate already exists with the same name")
-- except ValueError:
-- pass
--
-- if args.primary_cert:
-- # This is the server's primary certificate, update RSA entry
-- RSA(inst).set('nsSSLPersonalitySSL', args.name)
-+ pkcs12_password = None
-+ pkcs12_file = args.file.lower().endswith((".p12", ".pfx"))
-+ if pkcs12_file:
-+ pkcs12_password = _resolve_pkcs12_password(args)
-
-- # Add the cert
-- tlsdb.add_cert(args.name, args.file)
-+ certmgr = CertManager(instance=inst)
-+ cert = certmgr.get_cert(args.name)
-+ if cert:
-+ log.info(f"Certificate '{args.name}' already exists, skipping")
-+ return
-
-+ certmgr.add_cert(
-+ args.file,
-+ args.name,
-+ pkcs12_password=pkcs12_password,
-+ primary=args.primary_cert,
-+ ca=False,
-+ force=args.force
-+ )
- log.info("Successfully added certificate")
-
--
- def cacert_add(inst, basedn, log, args):
- """Add CA certificate, or CA certificate bundle
- """
-@@ -249,100 +298,81 @@ def cacert_add(inst, basedn, log, args):
- if not os.path.isfile(args.file):
- raise ValueError(f'Certificate file "{args.file}" does not exist')
-
-- tls = NssSsl(dirsrv=inst)
-- if not tls._db_exists(even_partial=True): # we want to be very careful
-- log.info('Security database does not exist. Creating a new one in {}.'.format(inst.get_cert_dir()))
-- tls.reinit()
--
-- tls.add_ca_cert_bundle(args.file, args.name)
-+ # Does it make sense to add a CA cert from p12 container ?
-+ if args.file.lower().endswith((".p12", ".pfx")):
-+ raise ValueError("PKCS#12 CA certificates not supported. Use PEM or DER file")
-
-+ certmgr = CertManager(instance=inst)
-+ certmgr.add_ca_cert(args.file, args.name, force=args.force)
-+ log.info("Successfully added CA certificate")
-
- def cert_list(inst, basedn, log, args):
- """List all the server certificates
- """
-- cert_list = []
-- tlsdb = NssSsl(dirsrv=inst)
-- certs = tlsdb.list_certs()
-- for cert in certs:
-- if args.json:
-- cert_list.append(
-- {
-- "type": "certificate",
-- "attrs": {
-- 'nickname': cert[0],
-- 'subject': cert[1],
-- 'issuer': cert[2],
-- 'expires': cert[3],
-- 'flags': cert[4],
-- }
-- }
-- )
-- else:
-- log.info('Certificate Name: {}'.format(cert[0]))
-- log.info('Subject DN: {}'.format(cert[1]))
-- log.info('Issuer DN: {}'.format(cert[2]))
-- log.info('Expires: {}'.format(cert[3]))
-- log.info('Trust Flags: {}\n'.format(cert[4]))
-- if args.json:
-- log.info(json.dumps(cert_list, indent=4))
-+ certmgr = CertManager(instance=inst)
-+ certs = certmgr.list_certs()
-+ if not certs:
-+ log.info("No certificates found.")
-+ return
-
-+ if args.json:
-+ output = [_dump_cert(cert, json_output=True) for cert in certs]
-+ log.info(json.dumps(output, indent=4))
-+ else:
-+ for cert in certs:
-+ _dump_cert(cert, json_output=False, log=log)
-
- def cacert_list(inst, basedn, log, args):
- """List all CA certs
- """
-- cert_list = []
-- tlsdb = NssSsl(dirsrv=inst)
-- certs = tlsdb.list_certs(ca=True)
-- for cert in certs:
-- if args.json:
-- cert_list.append(
-- {
-- "type": "certificate",
-- "attrs": {
-- 'nickname': cert[0],
-- 'subject': cert[1],
-- 'issuer': cert[2],
-- 'expires': cert[3],
-- 'flags': cert[4],
-- }
-- }
-- )
-- else:
-- log.info('Certificate Name: {}'.format(cert[0]))
-- log.info('Subject DN: {}'.format(cert[1]))
-- log.info('Issuer DN: {}'.format(cert[2]))
-- log.info('Expires: {}'.format(cert[3]))
-- log.info('Trust Flags: {}\n'.format(cert[4]))
-- if args.json:
-- log.info(json.dumps(cert_list, indent=4))
-+ certmgr = CertManager(instance=inst)
-+ ca_certs = certmgr.list_ca_certs()
-+ if not ca_certs:
-+ log.info("No CA certificates found.")
-+ return
-
-+ if args.json:
-+ output = [_dump_cert(cert, json_output=True) for cert in ca_certs]
-+ log.info(json.dumps(output, indent=4))
-+ else:
-+ for cert in ca_certs:
-+ _dump_cert(cert, json_output=False, log=log)
-
- def cert_get(inst, basedn, log, args):
- """Get the details about a server certificate
- """
-- tlsdb = NssSsl(dirsrv=inst)
-- details = tlsdb.get_cert_details(args.name)
-+ certmgr = CertManager(instance=inst)
-+ cert = certmgr.get_cert(args.name)
-+ if not cert:
-+ log.error(f"Certificate '{args.name}' not found.")
-+ return
-+
-+ if "C" in cert.get("trust_flags", ""):
-+ return
-+
- if args.json:
-- log.info(json.dumps(
-- {
-- "type": "certificate",
-- "attrs": {
-- 'nickname': details[0],
-- 'subject': details[1],
-- 'issuer': details[2],
-- 'expires': details[3],
-- 'flags': details[4],
-- }
-- }, indent=4
-- )
-- )
-+ output = _dump_cert(cert, json_output=args.json)
-+ log.info(json.dumps(output, indent=4))
- else:
-- log.info('Certificate Name: {}'.format(details[0]))
-- log.info('Subject DN: {}'.format(details[1]))
-- log.info('Issuer DN: {}'.format(details[2]))
-- log.info('Expires: {}'.format(details[3]))
-- log.info('Trust Flags: {}'.format(details[4]))
-+ _dump_cert(cert, json_output=args.json, log=log)
-+
-+def cacert_get(inst, basedn, log, args):
-+ """Get the details about a CA certificate
-+ """
-+ certmgr = CertManager(instance=inst)
-+ cert = certmgr.get_cert(args.name)
-+ if not cert:
-+ log.error(f"Certificate '{args.name}' not found.")
-+ return
-+
-+ if "C" not in cert.get("trust_flags", ""):
-+ return
-
-+ if args.json:
-+ output = _dump_cert(cert, json_output=args.json)
-+ log.info(json.dumps(output, indent=4))
-+ else:
-+ _dump_cert(cert, json_output=args.json, log=log)
-
- def csr_list(inst, basedn, log, args):
- """
-@@ -412,18 +442,20 @@ def csr_del(inst, basedn, log, args):
- def cert_edit(inst, basedn, log, args):
- """Edit cert
- """
-- tlsdb = NssSsl(dirsrv=inst)
-- tlsdb.edit_cert_trust(args.name, args.flags)
-+ certmgr = CertManager(instance=inst)
-+ certmgr.edit_cert_trust(args.name, args.flags)
- log.info("Successfully edited certificate trust flags")
-
-
- def cert_del(inst, basedn, log, args):
- """Delete cert
- """
-- tlsdb = NssSsl(dirsrv=inst)
-- tlsdb.del_cert(args.name)
-- log.info(f"Successfully deleted certificate")
--
-+ certmgr = CertManager(instance=inst)
-+ try:
-+ certmgr.del_cert(args.name)
-+ log.info(f"Successfully deleted certificate")
-+ except ValueError as e:
-+ log.error(f"Failed to delete certificate '{args.name}': {e}")
-
- def key_list(inst, basedn, log, args):
- """
-@@ -509,6 +541,11 @@ def create_parser(subparsers):
- help='Sets the name/nickname of the certificate')
- cert_add_parser.add_argument('--primary-cert', action='store_true',
- help="Sets this certificate as the server's certificate")
-+ cert_add_parser.add_argument('--pkcs12-pin-text', help='The PKCS#12 password as plain text. WARNING: Password may appear' \
-+ ' in process list or shell history. Use --pkcs12-pin-stdin or --pkcs12-pin-path to prevent password exposure.')
-+ cert_add_parser.add_argument('--pkcs12-pin-stdin', help='Read the PKCS#12 password from stdin', action='store_true')
-+ cert_add_parser.add_argument('--pkcs12-pin-path', help='Path to a file containing the PKCS#12 password')
-+ cert_add_parser.add_argument('--do-it', dest="force", help="Force the addition of a certificate that cannot be verified",action='store_true', default=False)
- cert_add_parser.set_defaults(func=cert_add)
-
- cert_edit_parser = certs_sub.add_parser('set-trust-flags', help='Set the Trust flags',
-@@ -519,7 +556,7 @@ def create_parser(subparsers):
- cert_edit_parser.set_defaults(func=cert_edit)
-
- cert_del_parser = certs_sub.add_parser('del', help='Delete a certificate',
-- description=('Delete a certificate from the NSS database'))
-+ description=('Delete a server certificate from the NSS database or DynamicCerts backend.'))
- cert_del_parser.add_argument('name', help='The name/nickname of the certificate')
- cert_del_parser.set_defaults(func=cert_del)
-
-@@ -529,19 +566,20 @@ def create_parser(subparsers):
- cert_get_parser.set_defaults(func=cert_get)
-
- cert_list_parser = certs_sub.add_parser('list', help='List the server certificates',
-- description=('Lists the server certificates in the NSS database'))
-+ description=('List all server certificates in the NSS database or DynamicCerts backend.'))
- cert_list_parser.set_defaults(func=cert_list)
-
- # CA certificate management
- cacerts = security_sub.add_parser('ca-certificate', help='Manage TLS certificate authorities', formatter_class=CustomHelpFormatter)
- cacerts_sub = cacerts.add_subparsers(help='ca-certificate')
- cacert_add_parser = cacerts_sub.add_parser('add', help='Add a Certificate Authority', description=(
-- 'Add a Certificate Authority to the NSS database'))
-+ 'Add a CA certificate (PEM or DER only) to the NSS database or DynamicCerts backend.'))
- cacert_add_parser.add_argument('--file', required=True,
-- help='Sets the file name of the CA certificate')
-- cacert_add_parser.add_argument('--name', nargs='+', required=True,
-- help='Sets the name/nickname of the CA certificate, if adding a PEM bundle then specify multiple names one for '
-+ help='Path to the CA certificate file (PEM or DER). If adding a PEM bundle then specify multiple names one for '
- 'each certificate, otherwise a number increment will be added to the previous name.')
-+ cacert_add_parser.add_argument('--name', nargs='+', required=True,
-+ help='Sets the name/nickname of the CA certificate')
-+ cacert_add_parser.add_argument('--do-it', dest="force", help="Force the addition of a certificate that cannot be verified",action='store_true', default=False)
- cacert_add_parser.set_defaults(func=cacert_add)
-
- cacert_edit_parser = cacerts_sub.add_parser('set-trust-flags', help='Set the Trust flags',
-@@ -559,7 +597,7 @@ def create_parser(subparsers):
- cacert_get_parser = cacerts_sub.add_parser('get', help="Displays a Certificate Authority's information",
- description=('Get detailed information about a CA certificate, like trust attributes, expiration dates, Subject and Issuer DN'))
- cacert_get_parser.add_argument('name', help='The name/nickname of the CA certificate')
-- cacert_get_parser.set_defaults(func=cert_get)
-+ cacert_get_parser.set_defaults(func=cacert_get)
-
- cacert_list_parser = cacerts_sub.add_parser('list', help='List the Certificate Authorities',
- description=('List the CA certificates in the NSS database'))
-diff --git a/src/lib389/lib389/dyncerts.py b/src/lib389/lib389/dyncerts.py
-new file mode 100644
-index 000000000..22e1a9a36
---- /dev/null
-+++ b/src/lib389/lib389/dyncerts.py
-@@ -0,0 +1,458 @@
-+# --- 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 datetime
-+import os
-+import ldap
-+import logging
-+import re
-+import tempfile
-+from typing import Optional
-+from lib389._mapped_object import DSLdapObjects, DSLdapObject
-+from lib389.utils import cert_is_ca, pem_to_der, is_pem_cert, ensure_str
-+from cryptography import x509
-+from cryptography.hazmat.backends import default_backend
-+from cryptography.hazmat.primitives import serialization
-+from cryptography.hazmat.primitives.serialization import pkcs12
-+
-+log = logging.getLogger(__name__)
-+
-+DYCATTR_CN = "cn"
-+DYNCERT_SUFFIX = "cn=dynamiccertificates"
-+
-+DYCATTR_PREFIX = "dsdynamiccertificate"
-+DYCATTR_CERTDER = DYCATTR_PREFIX + "der"
-+DYCATTR_PKEYDER = DYCATTR_PREFIX + "privatekeyder"
-+DYCATTR_SUBJECT = DYCATTR_PREFIX + "subject"
-+DYCATTR_ISSUER = DYCATTR_PREFIX + "issuer"
-+DYCATTR_TRUST = DYCATTR_PREFIX + "trustflags"
-+DYCATTR_NOTAFTER = DYCATTR_PREFIX + "notafter"
-+DYCATTR_FORCE = DYCATTR_PREFIX + "Force"
-+DYCATTR_ISCA = DYCATTR_PREFIX + "IsCA"
-+
-+CA_NAME = 'Self-Signed-CA'
-+CERT_NAME = 'Server-Cert'
-+
-+class DynamicCert(DSLdapObject):
-+ """
-+ Represents a single DynamicCert LDAP entry.
-+ """
-+
-+ _must_attributes = [DYCATTR_CN]
-+
-+ def __init__(self, instance, dn: Optional[str] = None):
-+ """
-+ Initialise a DynamicCert object.
-+
-+ :param instance: DirSrv instance
-+ :param dn: Entry distinguished name (DN)
-+ """
-+ super(DynamicCert, self).__init__(instance, dn)
-+ self._rdn_attribute = DYCATTR_CN
-+ self._create_objectclasses = ["top", "extensibleObject"]
-+ self._protected = False
-+ self._basedn = DYNCERT_SUFFIX
-+
-+ def _normalise_timestamp(self, raw: str):
-+ """
-+ Convert DynamicCert timestamp to NSS like format.
-+
-+ :param raw: Raw DynamicCert timestamp (e.g. 20260109181934Z)
-+ :return: Formatted timestamp as "YYYY-MM-DD HH:MM:SS", or original string
-+ """
-+ try:
-+ year = int(raw[0:4])
-+ month = int(raw[4:6]) + 1 # PRExplodedTime.tm_month is 0–11
-+ day = int(raw[6:8])
-+ hour = int(raw[8:10])
-+ minute = int(raw[10:12])
-+ second = int(raw[12:14])
-+ return datetime.datetime(year, month, day, hour, minute, second).strftime("%Y-%m-%d %H:%M:%S")
-+ except Exception:
-+ return raw
-+
-+ def del_cert(self):
-+ """
-+ Delete this DynamicCert entry from LDAP.
-+
-+ :raises ValueError: If the DynamicCert object does not have a DN
-+ :raises ldap.LDAPError: If an LDAP operation fails (other than NO_SUCH_OBJECT)
-+ """
-+ if not self._dn:
-+ raise ValueError("Cannot delete DynamicCert without a DN")
-+
-+ try:
-+ self.delete()
-+ except ldap.NO_SUCH_OBJECT:
-+ log.warning(f"DynamicCert already deleted: {self._dn}")
-+ except ldap.LDAPError as e:
-+ log.error(f"Failed to delete DynamicCert: {self._dn}: {e}")
-+ raise
-+
-+ def edit_trust(self, trust_flags: str):
-+ """
-+ Edit certificate trust flags.
-+
-+ :param trust_flags: Comma separated trust flags string (SSL,Email,ObjectSigning)
-+ :raises ValueError: If trust flags are invalid or empty
-+ """
-+ if not trust_flags:
-+ raise ValueError("Trust flags cannot be empty")
-+
-+ trust_fields = trust_flags.strip().split(",")
-+
-+ if len(trust_fields) != 3:
-+ raise ValueError("Trust flags must have 3 comma separated fields")
-+
-+ # Allowed field values (NSS)
-+ valid_flags = set("pPcCTu")
-+ for field in trust_fields:
-+ if field and any(flag not in valid_flags for flag in field):
-+ raise ValueError(f"Invalid characters in trust flags: '{trust_flags}'")
-+
-+ try:
-+ self.replace(DYCATTR_TRUST, trust_flags)
-+ except ldap.LDAPError as e:
-+ log.error(f"Failed to update trust flags for {self._dn}: {e}")
-+ raise
-+
-+class DynamicCerts(DSLdapObjects):
-+ """
-+ Collection of DynamicCert entries under cn=dynamiccertificates.
-+ """
-+
-+ def __init__(self, instance):
-+ """
-+ Initialise the DynamicCerts collection.
-+
-+ :param instance: DirSrv instance
-+ """
-+ super(DynamicCerts, self).__init__(instance=instance)
-+ self._objectclasses = ["extensibleObject"]
-+ self._filterattrs = [DYCATTR_CN, DYCATTR_SUBJECT, DYCATTR_ISSUER]
-+ self._childobject = DynamicCert
-+ self._basedn = DYNCERT_SUFFIX
-+
-+ def add_cert(self,
-+ cert_file: str,
-+ nickname: str,
-+ pkcs12_password: Optional[str] = None,
-+ ca: bool = False,
-+ force: bool = False,
-+ ):
-+ """
-+ Add or update a certificate (PEM, DER, or PKCS#12).
-+
-+ :param cert_file: Path to certificate file (PEM, DER, or PKCS#12)
-+ :param nickname: Certificate nickname
-+ :param pkcs12_password: Password for PKCS#12, if any
-+ :param ca: Whether this certificate is a CA certificate
-+ :param force: Force the addition of a certificate that cannot be verified
-+ """
-+ if not nickname:
-+ raise ValueError("Certificate CN cannot be empty")
-+
-+ if not os.path.isfile(cert_file):
-+ raise ValueError(f"Certificate file does not exist: {cert_file}")
-+
-+ if pkcs12_password and not isinstance(pkcs12_password, str):
-+ raise TypeError("PKCS#12 password must be a string")
-+
-+ with open(cert_file, "rb") as f:
-+ cert_bytes = f.read()
-+
-+ der_cert = None
-+ der_privkey = None
-+
-+ if cert_file.lower().endswith((".p12", ".pfx")):
-+ try:
-+ privkey, cert, _ = pkcs12.load_key_and_certificates(
-+ cert_bytes, pkcs12_password.encode() if pkcs12_password else None,
-+ backend=default_backend()
-+ )
-+ except Exception as e:
-+ raise ValueError(f"Failed to load PKCS#12 file: {cert_file}: {e}")
-+
-+ if cert is None:
-+ raise ValueError("PKCS#12 file contains no certificate")
-+
-+ der_cert = cert.public_bytes(serialization.Encoding.DER)
-+ if privkey is not None:
-+ der_privkey = privkey.private_bytes(
-+ encoding=serialization.Encoding.DER,
-+ format=serialization.PrivateFormat.PKCS8,
-+ encryption_algorithm=serialization.NoEncryption()
-+ )
-+ else:
-+ try:
-+ der_cert = pem_to_der(cert_bytes) if is_pem_cert(cert_bytes) else cert_bytes
-+ except Exception as e:
-+ raise ValueError(f"Failed to parse certificate '{cert_file}': {e}")
-+
-+ if ca and not cert_is_ca(cert_file):
-+ raise ValueError(f"Certificate ({nickname}) is not a CA certificate")
-+
-+ attrs = {
-+ "cn": [nickname.encode()],
-+ "objectClass": [b"top", b"extensibleObject"],
-+ DYCATTR_CERTDER: [ der_cert, ]
-+ }
-+ if der_privkey:
-+ attrs[DYCATTR_PKEYDER] = [der_privkey]
-+
-+ if ca:
-+ attrs[DYCATTR_TRUST] = [b"CT,,"]
-+
-+ if force:
-+ attrs[DYCATTR_FORCE] = [b"TRUE"]
-+
-+ if not der_cert:
-+ raise ValueError(f"Failed to extract DER bytes from {cert_file}")
-+
-+ # Escape the CN to handle special chars in the nickname
-+ escaped_cn = ldap.dn.escape_dn_chars(nickname)
-+ dn = ensure_str(f"cn={escaped_cn},{self._basedn}")
-+ # Raw CN used for lookup
-+ cert_obj = self.get_cert_obj(nickname)
-+ if not cert_obj:
-+ cert_obj = DynamicCert(self._instance, dn)
-+ cert_obj.create(properties=attrs)
-+ else:
-+ log.info(f"Updating existing certificate; {nickname}")
-+ attrs_list = [(attr, vals) for attr, vals in attrs.items()]
-+ cert_obj.replace_many(*attrs_list)
-+
-+ def add_ca_cert(self,
-+ cert_file: str,
-+ nickname: str,
-+ pkcs12_password: Optional[str] = None,
-+ force: bool = False
-+ ):
-+ """
-+ Add a CA certificate from a PEM bundle or single PEM/DER file.
-+
-+ :param cert_file: Path to the certificate file (PEM or DER)
-+ :param nickname: Certificate nickname
-+ :param pkcs12_password: Password for PKCS#12, if any
-+ :param force: Force the addition of a certificate that cannot be verified
-+ :raises ValueError: If file is invalid or file does not exist
-+ """
-+ if not os.path.exists(cert_file):
-+ raise ValueError(f"Certificate file does not exist: {cert_file}")
-+
-+ # Normalise nickname(s)
-+ if isinstance(nickname, str):
-+ nicknames = [nickname]
-+ elif isinstance(nickname, list):
-+ nicknames = nickname
-+ else:
-+ raise TypeError(f"nickname must be str or list[str], got {type(nickname)}")
-+
-+ # PEM (may be bundle)
-+ if cert_file.lower().endswith(".pem"):
-+ with open(cert_file, "r") as f:
-+ pem_data = f.read()
-+
-+ pem_certs = re.findall(
-+ r"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----",
-+ pem_data,
-+ re.DOTALL
-+ )
-+
-+ if not pem_certs:
-+ raise ValueError("No certificates found in PEM file")
-+
-+ temp_files = []
-+ try:
-+ for idx, cert in enumerate(pem_certs):
-+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pem", mode="w") as tmp:
-+ tmp.write(cert.strip() + "\n")
-+ tmp_cert_path = tmp.name
-+ temp_files.append(tmp_cert_path)
-+
-+ # Determine nickname, could be a list, or not enough
-+ if idx < len(nicknames):
-+ ca_nick = nicknames[idx]
-+ else:
-+ ca_nick = f"{nicknames[-1]}{idx}"
-+
-+ # Check we dont trample over installer certs
-+ if ca_nick.lower() in (CERT_NAME.lower(), CA_NAME.lower()):
-+ raise ValueError(f"You may not import a CA with the nickname {CERT_NAME} or {CA_NAME}")
-+
-+ if not cert_is_ca(tmp_cert_path):
-+ raise ValueError(f"Certificate ({ca_nick}) is not a CA certificate")
-+
-+ try:
-+ # Handle existing LDAP object
-+ if self.get_cert_details(ca_nick):
-+ if not force:
-+ raise ValueError(
-+ f"Certificate already exists with the same name ({ca_nick})"
-+ )
-+ else:
-+ log.info(f"Overwriting existing CA cert {ca_nick}")
-+ self.del_cert(ca_nick)
-+ except ValueError:
-+ pass
-+
-+ self.add_cert(
-+ tmp_cert_path,
-+ ca_nick,
-+ pkcs12_password=pkcs12_password,
-+ ca=True,
-+ force=force
-+ )
-+
-+ finally:
-+ for tmp_file in temp_files:
-+ try:
-+ os.remove(tmp_file)
-+ except OSError as e:
-+ log.debug(f"Failed to remove tmp cert file: {tmp_file}: {e}")
-+ else:
-+ # Single binary cert
-+ if len(nicknames) != 1:
-+ raise ValueError("Single cert requires exactly one nickname")
-+
-+ ca_nick = nicknames[0]
-+ try:
-+ if self.get_cert_details(ca_nick):
-+ if force:
-+ self.del_cert(ca_nick)
-+ else:
-+ raise ValueError(f"Certificate already exists: {ca_nick}")
-+ except ValueError:
-+ pass
-+
-+ self.add_cert(cert_file, ca_nick, pkcs12_password=pkcs12_password, ca=True, force=force)
-+
-+ def del_cert(self, nickname: str):
-+ """
-+ Delete a certificate.
-+
-+ :param nickname: The certificate nickname to delete.
-+ :raises ValueError: If the nickname is empty or the entry cannot be found.
-+ """
-+ if not nickname:
-+ raise ValueError("Certificate nickname cannot be empty")
-+
-+ cert_obj = self.get_cert_obj(nickname)
-+ if not cert_obj:
-+ raise ValueError(f"Certificate entry not found: {nickname}")
-+
-+ cert_obj.del_cert()
-+
-+ def list_certs(self):
-+ """
-+ List all server certificates.
-+
-+ :return: A list of certificate dictionaries for each certificate
-+ """
-+ cert_objects = self.list()
-+ certs = []
-+ for cert in cert_objects:
-+ if cert._dn == self._basedn:
-+ continue
-+ der_cert = cert.get_attr_vals_bytes(DYCATTR_CERTDER)[0]
-+ if der_cert:
-+ if not cert_is_ca(der_cert):
-+ certs.append({
-+ "cn": cert.get_attr_vals_utf8(DYCATTR_CN)[0],
-+ "subject": cert.get_attr_vals_utf8(DYCATTR_SUBJECT)[0],
-+ "issuer": cert.get_attr_vals_utf8(DYCATTR_ISSUER)[0],
-+ "expires": cert._normalise_timestamp(cert.get_attr_vals_utf8(DYCATTR_NOTAFTER)[0]),
-+ "trust_flags": cert.get_attr_vals_utf8(DYCATTR_TRUST)[0],
-+ })
-+ return certs
-+
-+ def list_ca_certs(self):
-+ """
-+ List all ca certificates.
-+
-+ :return: A list of certificate dictionaries for each certificate
-+ """
-+ cert_objects = self.list()
-+ certs = []
-+ for cert in cert_objects:
-+ if cert._dn == self._basedn:
-+ continue
-+ der_cert = cert.get_attr_vals_bytes(DYCATTR_CERTDER)[0]
-+ if der_cert:
-+ if cert_is_ca(der_cert):
-+ certs.append({
-+ "cn": cert.get_attr_vals_utf8(DYCATTR_CN)[0],
-+ "subject": cert.get_attr_vals_utf8(DYCATTR_SUBJECT)[0],
-+ "issuer": cert.get_attr_vals_utf8(DYCATTR_ISSUER)[0],
-+ "expires": cert._normalise_timestamp(cert.get_attr_vals_utf8(DYCATTR_NOTAFTER)[0]),
-+ "trust_flags": cert.get_attr_vals_utf8(DYCATTR_TRUST)[0],
-+ })
-+ return certs
-+
-+ def get_cert_obj(self, nickname: str):
-+ """
-+ Retrieve a certificate object.
-+
-+ :param cn: Certificate nickname
-+ :raises ValueError: If the cn is empty
-+ :return: DynamicCert object if found, else None
-+ """
-+ if not nickname:
-+ raise ValueError("Certificate CN cannot be empty")
-+ try:
-+ cert = self.get(nickname)
-+ except ldap.NO_SUCH_OBJECT:
-+ return None
-+
-+ return cert
-+
-+ def get_cert_details(self, nickname: str):
-+ """
-+ Get a certificates details.
-+
-+ :param nickname: Certificate nickname
-+ :raises ValueError: If the nickname is empty
-+ :return: DynamicCert object if found, else None
-+ """
-+ if not nickname:
-+ raise ValueError("Certificate CN cannot be empty")
-+ try:
-+ cert = self.get(nickname)
-+ except ldap.NO_SUCH_OBJECT:
-+ return None
-+
-+ return {
-+ "cn": cert.get_attr_vals_utf8(DYCATTR_CN)[0],
-+ "subject": cert.get_attr_vals_utf8(DYCATTR_SUBJECT)[0],
-+ "issuer": cert.get_attr_vals_utf8(DYCATTR_ISSUER)[0],
-+ "expires": cert._normalise_timestamp(cert.get_attr_vals_utf8(DYCATTR_NOTAFTER)[0]),
-+ "trust_flags": cert.get_attr_vals_utf8(DYCATTR_TRUST)[0],
-+ }
-+
-+ def edit_cert_trust(self, nickname: str, trust_flags: str):
-+ """
-+ Edit trust flags on an existing certificate.
-+
-+ :param nickname: Certificate nickname
-+ :param trust_flags: 3 field NSS trust string
-+ """
-+ cert_obj = self.get_cert_obj(nickname)
-+ if not cert_obj:
-+ raise ValueError(f"Certificate {nickname} does not exist")
-+
-+ try:
-+ cert_obj.edit_trust(trust_flags=trust_flags)
-+ except ValueError as ve:
-+ log.error(f"Invalid input for certificate '{nickname}': {ve}")
-+ raise
-+ except ldap.LDAPError as le:
-+ log.error(f"LDAP error while updating certificate '{nickname}': {le}")
-+ raise
-+ except Exception as e:
-+ log.error(f"Unexpected error while updating certificate '{nickname}': {e}")
-+ raise
-diff --git a/src/lib389/lib389/nss_ssl.py b/src/lib389/lib389/nss_ssl.py
-index 2fd2b9b89..764434166 100644
---- a/src/lib389/lib389/nss_ssl.py
-+++ b/src/lib389/lib389/nss_ssl.py
-@@ -1,5 +1,5 @@
- # --- BEGIN COPYRIGHT BLOCK ---
--# Copyright (C) 2023 Red Hat, Inc.
-+# Copyright (C) 2026 Red Hat, Inc.
- # All rights reserved.
- #
- # License: GPL (version 3 or any later version).
-@@ -18,6 +18,7 @@ import shutil
- import logging
- import subprocess
- import uuid
-+from typing import Optional
- from datetime import datetime, timedelta
- from subprocess import check_output, run, PIPE
- from lib389.passwd import password_generate
-@@ -25,7 +26,6 @@ from lib389._mapped_object_lint import DSLint
- from lib389.lint import DSCERTLE0001, DSCERTLE0002
- from lib389.utils import ensure_str, format_cmd_list, DSVersion, cert_is_ca
-
--
- KEYBITS = 4096
- CA_NAME = 'Self-Signed-CA'
- CERT_NAME = 'Server-Cert'
-@@ -85,9 +85,8 @@ class NssSsl(DSLint):
- all_certs = self._rsa_cert_list()
- for cert in all_certs:
- cert_list.append(self.get_cert_details(cert[0]))
--
- for cert in cert_list:
-- cert_date = cert[3].split()[0]
-+ cert_date = cert['expires'].split()[0]
- diff_date = datetime.strptime(cert_date, '%Y-%m-%d').date() - datetime.today().date()
- if diff_date < timedelta(days=0):
- # Expired
-@@ -1099,12 +1098,12 @@ only.
- def get_cert_details(self, nickname):
- """Get the trust flags, subject DN, issuer, and expiration date
-
-- return a list:
-- 0 - nickname
-- 1 - subject
-- 2 - issuer
-- 3 - expire date
-- 4 - trust_flags
-+ :return: dict containing certificate details with keys:
-+ - "cn" : Certificate nickname (str)
-+ - "subject" : Subject DN (str)
-+ - "issuer" : Issuer DN (str)
-+ - "expires" : Expiration date/time as a string (YYYY-MM-DD HH:MM:SS)
-+ - "trust_flags" : Trust flags from NSS (str)
- """
- all_certs = self._rsa_cert_list()
- for cert in all_certs:
-@@ -1142,26 +1141,33 @@ only.
- issuer = issuer[1:-1]
- break
-
-- return ([nickname, subject, issuer, str(end_date), trust_flags])
-+ return {
-+ "cn": nickname,
-+ "subject": subject,
-+ "issuer": issuer,
-+ "expires": str(end_date),
-+ "trust_flags": trust_flags,
-+ }
-
-- # Did not find cert with that name
-- raise ValueError("Certificate '{}' not found in NSS database".format(nickname))
-+ return None
-
- def list_certs(self, ca=False):
- all_certs = self._rsa_cert_list()
- certs = []
-- for cert in all_certs:
-- trust_flags = cert[1]
-+ for nickname, trust_flags in all_certs:
- if (ca and "CT" in trust_flags) or (not ca and "CT" not in trust_flags):
-- certs.append(self.get_cert_details(cert[0]))
-+ cert_details = self.get_cert_details(nickname)
-+ certs.append(cert_details)
- return certs
-
- def list_ca_certs(self):
-- return [
-- cert
-- for cert in self._rsa_cert_list()
-- if self._rsa_cert_is_catrust(cert)
-- ]
-+ ca_certs = []
-+ for cert in self._rsa_cert_list():
-+ if self._rsa_cert_is_catrust(cert):
-+ # cert[0] is the nickname
-+ cert_details = self.get_cert_details(cert[0])
-+ ca_certs.append(cert_details)
-+ return ca_certs
-
- def list_client_ca_certs(self):
- return [
-@@ -1170,41 +1176,107 @@ only.
- if self._rsa_cert_is_caclienttrust(cert)
- ]
-
-- def add_cert(self, nickname, input_file, ca=False):
-+ def add_cert(self,
-+ nickname: str,
-+ cert_file: str,
-+ pkcs12_password: Optional[str] = None,
-+ ca: bool = False,
-+ force: bool = False):
- """Add server or CA cert
-+
-+ :param nickname: Certificate nickname
-+ :param cert_file: Path to certificate file (PEM, DER, or PKCS#12)
-+ :param ca: Whether this is a CA certificate
-+ :param pkcs12_password: Password for PKCS#12, if any
-+ :param force: Force the addition of a certificate that cannot be verified
- """
-+ if not nickname:
-+ raise ValueError("Certificate nickname must not be empty")
-
- # Verify input_file exists
-- if not os.path.exists(input_file):
-- raise ValueError("The certificate file ({}) does not exist".format(input_file))
-+ if not os.path.isfile(cert_file):
-+ raise ValueError("The certificate file ({}) does not exist".format(cert_file))
-
- pem_file = True
-- if not input_file.lower().endswith(".pem"):
-+ if not cert_file.lower().endswith(".pem"):
- pem_file = False
- else:
-- self._assert_not_chain(input_file)
-+ self._assert_not_chain(cert_file)
-
- if ca:
- # Verify this is a CA cert
-- if not cert_is_ca(input_file):
-+ if not cert_is_ca(cert_file):
- raise ValueError(f"Certificate ({nickname}) is not a CA certificate")
- trust_flags = "CT,,"
- else:
- # Verify this is a server cert
-- if cert_is_ca(input_file):
-+ if cert_is_ca(cert_file, pkcs12_password=pkcs12_password):
- raise ValueError(f"Certificate ({nickname}) is not a server certificate")
- trust_flags = ",,"
-
-+ pkcs12_file = None
-+ if cert_file.lower().endswith((".p12", ".pfx")):
-+ pkcs12_file = True
-+
-+ if pkcs12_file:
-+ self.log.info("Importing PKCS#12 into NSS: %s", cert_file)
-+
-+ if pkcs12_password is None:
-+ pkcs12_password = ""
-+
-+ cmd = [
-+ "pk12util",
-+ "-v",
-+ "-n", nickname,
-+ "-i", cert_file,
-+ "-d", self._certdb,
-+ "-k", f"{self._certdb}/{PWD_TXT}",
-+ "-W", pkcs12_password,
-+ ]
-+
-+ # Mask the password in logs
-+ masked_cmd = [arg if arg != pkcs12_password else "****" for arg in cmd]
-+ self.log.info(f"nss import p12 cmd: {format_cmd_list(masked_cmd)}", )
-+ try:
-+ check_output(cmd, stderr=subprocess.STDOUT)
-+ except subprocess.CalledProcessError as e:
-+ raise ValueError(f"Failed to import PKCS#12 {cert_file} {e}")
-+
-+ if ca:
-+ cmd = [
-+ "certutil",
-+ "-M",
-+ "-n", nickname,
-+ "-t", "CT,,",
-+ "-d", self._certdb,
-+ "-f", f"{self._certdb}/{PWD_TXT}",
-+ ]
-+ self.log.debug(f"set CA trust cmd: {format_cmd_list(cmd)}")
-+ try:
-+ check_output(cmd, stderr=subprocess.STDOUT)
-+ except subprocess.CalledProcessError as e:
-+ raise ValueError(f"Failed to set CA trust for {nickname} {e}")
-+
-+ return
-+
-+ pem_file = cert_file.lower().endswith(".pem")
-+ if pem_file:
-+ self._assert_not_chain(cert_file)
-+
-+ trust_flags = "CT,," if ca else ",,"
-+
- cmd = [
- '/usr/bin/certutil',
- '-A',
- '-d', self._certdb,
- '-n', nickname,
- '-t', trust_flags,
-- '-i', input_file,
-+ '-i', cert_file,
- '-f',
- '%s/%s' % (self._certdb, PWD_TXT),
-+
- ]
-+
- if pem_file:
- cmd.append('-a')
-
-@@ -1270,6 +1342,62 @@ only.
- if os.path.exists(p12_bundle):
- os.remove(p12_bundle)
-
-+ def add_ca_cert(self, cert_file: str, nickname: str, force: bool = False):
-+ """
-+ Add a CA certificate from a PEM bundle or single PEM/DER file.
-+
-+ Adapter function to match abstraction layer interface.
-+
-+ :param cert_file: path to the certificate file
-+ :param nickname: nickname to assign
-+ :param force: Force the addition of a certificate that cannot be verified
-+ """
-+ # Verify input_file exists
-+ if not os.path.exists(cert_file):
-+ raise ValueError(f"The certificate file ({cert_file}) does not exist")
-+
-+ # Normalise nickname(s)
-+ if isinstance(nickname, list):
-+ nicknames = nickname
-+ elif isinstance(nickname, str):
-+ nicknames = [nickname]
-+ else:
-+ raise TypeError(f"nickname must be str or list[str], got: {type(nickname)}")
-+
-+ # Allow overwrite only for single cert
-+ if len(nicknames) == 1:
-+ single_nick = nicknames[0]
-+ try:
-+ if self.get_cert_details(single_nick):
-+ if not force:
-+ raise ValueError(
-+ f"Certificate already exists with the same name ({single_nick})"
-+ )
-+ else:
-+ log.info(f"Overwriting existing certificate ({single_nick})")
-+ self.del_cert(single_nick)
-+ except ValueError:
-+ pass
-+
-+ # Offload to PEM bundle handler
-+ if cert_file.lower().endswith(".pem"):
-+ return self.add_ca_cert_bundle(
-+ cert_file=cert_file,
-+ nicknames=nicknames
-+ )
-+
-+ if len(nicknames) != 1:
-+ raise ValueError(
-+ "Binary CA cert requires exactly one nickname"
-+ )
-+
-+ if not cert_is_ca(cert_file):
-+ raise ValueError(f"Certificate ({nickname}) is not a CA certificate")
-+
-+ self.add_cert(nicknames[0], cert_file, ca=True)
-+
-+ log.info(f"Successfully added CA certificate ({nickname})")
-+
- def add_ca_cert_bundle(self, cert_file, nicknames):
- """
- Add a PEM file that could be a bundle of CA certs
-@@ -1327,14 +1455,8 @@ only.
- ca_cert_name = nicknames[names_len - 1] + str(ca_count)
-
- # Check if certificate nickname exists
-- name_exists = False
-- try:
-- self.get_cert_details(ca_cert_name)
-- name_exists = True
-- except ValueError:
-- pass
--
-- if name_exists:
-+ cert = self.get_cert_details(ca_cert_name)
-+ if cert:
- # Not good, cleanup and raise error
- try:
- for tmp_file in ca_files_to_cleanup:
-diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py
-index f8fba816b..042def090 100644
---- a/src/lib389/lib389/utils.py
-+++ b/src/lib389/lib389/utils.py
-@@ -32,11 +32,14 @@ import operator
- import subprocess
- import math
- import errno
-+from typing import Optional, Union
- from socket import getfqdn
- from ldapurl import LDAPUrl
- from contextlib import closing
- from cryptography import x509
- from cryptography.hazmat.backends import default_backend
-+from cryptography.hazmat.primitives import serialization
-+from cryptography.hazmat.primitives.serialization import pkcs12
- import lib389
- from pathlib import Path
- from subprocess import check_output
-@@ -1993,17 +1996,62 @@ def check_cert_info(cert_file_name, search_text):
- return search_text.lower() in cert_text.lower()
-
-
--def cert_is_ca(cert_file_name):
-- with open(cert_file_name, "rb") as f:
-- if is_cert_der(cert_file_name):
-- cert = x509.load_der_x509_certificate(f.read(), default_backend())
-+def cert_is_ca(cert_data, pkcs12_password: Optional[Union[str, bytes]] = None):
-+ """
-+ Determine if a certificate is a CA.
-+
-+ Supports PEM, DER, and PKCS#12 certificates, from bytes or file paths.
-+ """
-+ # If passed bytes directly (DER,PEM)
-+ cert_file = None
-+ if isinstance(cert_data, (bytes, bytearray)):
-+ data = bytes(cert_data)
-+ else:
-+ cert_file = cert_data
-+ try:
-+ with open(cert_file, "rb") as f:
-+ data = f.read()
-+ except OSError as e:
-+ raise ValueError(f"Unable to load certificate '{cert_data}': {e}")
-+
-+ try:
-+ # p12
-+ if cert_file and cert_file.lower().endswith((".p12", ".pfx")):
-+ try:
-+ privkey, cert, _ = pkcs12.load_key_and_certificates(
-+ data, ensure_bytes(pkcs12_password),
-+ backend=default_backend()
-+ )
-+ except Exception as e:
-+ raise ValueError(f"Failed to load PKCS#12 file: {cert_file}: {e}")
-+
-+ if cert is None:
-+ raise ValueError("No certificate found in PKCS#12 container")
-+
-+ # Bytes
-+ elif cert_file is None:
-+ try:
-+ cert = x509.load_der_x509_certificate(data, default_backend())
-+ except Exception:
-+ cert = x509.load_pem_x509_certificate(data, default_backend())
-+
-+ # File
- else:
-- cert = x509.load_pem_x509_certificate(f.read(), default_backend())
-+ try:
-+ cert = x509.load_pem_x509_certificate(data, default_backend())
-+ except Exception:
-+ cert = x509.load_der_x509_certificate(data, default_backend())
-+
-+ except ValueError as ve:
-+ raise ValueError(f"Unable to load certificate '{cert_file}': {ve}")
-
- try:
-+ # Check key usage
- key_usage = cert.extensions.get_extension_for_oid(x509.oid.ExtensionOID.KEY_USAGE)
- if not key_usage.value.key_cert_sign:
- return False
-+
-+ # Check constraints
- basic_constraints = cert.extensions.get_extension_for_oid(
- x509.oid.ExtensionOID.BASIC_CONSTRAINTS
- )
-@@ -2011,10 +2059,29 @@ def cert_is_ca(cert_file_name):
- return False
- else:
- return True
-+
- except x509.ExtensionNotFound:
- # No extensions, check the cert info directly
-- return check_cert_info(cert_file_name, "CA:TRUE")
-+ return check_cert_info(cert_file, "CA:TRUE")
-+
-+def pem_to_der(blob: bytes):
-+ """
-+ Convert PEM certificate bytes to DER format.
-
-+ :param blob: PEM encoded certificate bytes
-+ :return: DER encoded certificate bytes
-+ """
-+ cert = x509.load_pem_x509_certificate(blob)
-+ return cert.public_bytes(serialization.Encoding.DER)
-+
-+def is_pem_cert(blob: bytes):
-+ """
-+ Check if the given blob is a PEM certificate.
-+
-+ :param blob: Certificate data bytes
-+ :return: True if PEM format, else False
-+ """
-+ return b"-----BEGIN CERTIFICATE-----" in blob
-
- def get_passwd_from_file(passwd_file):
- if os.path.exists(passwd_file):
---
-2.52.0
-
diff --git a/0029-Issue-7224-CI-Test-Simplify-test_reserve_descriptor_.patch b/0029-Issue-7224-CI-Test-Simplify-test_reserve_descriptor_.patch
deleted file mode 100644
index 2b612f4..0000000
--- a/0029-Issue-7224-CI-Test-Simplify-test_reserve_descriptor_.patch
+++ /dev/null
@@ -1,93 +0,0 @@
-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
-
diff --git a/0030-Issue-7178-Bundled-jemalloc-fails-to-build-with-GCC-.patch b/0030-Issue-7178-Bundled-jemalloc-fails-to-build-with-GCC-.patch
deleted file mode 100644
index cc09478..0000000
--- a/0030-Issue-7178-Bundled-jemalloc-fails-to-build-with-GCC-.patch
+++ /dev/null
@@ -1,94 +0,0 @@
-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
-
diff --git a/0031-Issue-7121-2nd-LeakSanitizer-various-leaks-during-re.patch b/0031-Issue-7121-2nd-LeakSanitizer-various-leaks-during-re.patch
deleted file mode 100644
index c060983..0000000
--- a/0031-Issue-7121-2nd-LeakSanitizer-various-leaks-during-re.patch
+++ /dev/null
@@ -1,71 +0,0 @@
-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
-
diff --git a/0032-Issue-7223-Revert-index-scan-limits-for-system-index.patch b/0032-Issue-7223-Revert-index-scan-limits-for-system-index.patch
deleted file mode 100644
index c6322fc..0000000
--- a/0032-Issue-7223-Revert-index-scan-limits-for-system-index.patch
+++ /dev/null
@@ -1,778 +0,0 @@
-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
-
diff --git a/0033-Issue-7223-Add-upgrade-function-to-remove-nsIndexIDL.patch b/0033-Issue-7223-Add-upgrade-function-to-remove-nsIndexIDL.patch
deleted file mode 100644
index 1ec56dc..0000000
--- a/0033-Issue-7223-Add-upgrade-function-to-remove-nsIndexIDL.patch
+++ /dev/null
@@ -1,212 +0,0 @@
-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
-
diff --git a/0034-Issue-7223-Add-upgrade-function-to-remove-ancestorid.patch b/0034-Issue-7223-Add-upgrade-function-to-remove-ancestorid.patch
deleted file mode 100644
index f3e2064..0000000
--- a/0034-Issue-7223-Add-upgrade-function-to-remove-ancestorid.patch
+++ /dev/null
@@ -1,313 +0,0 @@
-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
-
diff --git a/0035-Issue-7223-Detect-and-log-index-ordering-mismatch-du.patch b/0035-Issue-7223-Detect-and-log-index-ordering-mismatch-du.patch
deleted file mode 100644
index 662d9e8..0000000
--- a/0035-Issue-7223-Detect-and-log-index-ordering-mismatch-du.patch
+++ /dev/null
@@ -1,300 +0,0 @@
-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
-
diff --git a/0036-Issue-7223-Add-dsctl-index-check-command-for-offline.patch b/0036-Issue-7223-Add-dsctl-index-check-command-for-offline.patch
deleted file mode 100644
index 534c631..0000000
--- a/0036-Issue-7223-Add-dsctl-index-check-command-for-offline.patch
+++ /dev/null
@@ -1,1233 +0,0 @@
-From 9e5f22c94b822fcd3decb8f98ce2eb383cc16a7c Mon Sep 17 00:00:00 2001
-From: Viktor Ashirov <vashirov@redhat.com>
-Date: Thu, 5 Feb 2026 12:17:06 +0100
-Subject: [PATCH] Issue 7223 - Add dsctl index-check command for offline index
- repair
-
-Description:
-Add `dsctl <instance> index-check [backend] [--fix]` command for offline
-detection and repair of index ordering mismatches. This is needed after
-upgrade from versions that didn't use integerOrderingMatch for
-parentid/ancestorid system indexes.
-
-It's automatically executed as part of RPM %post scriptlet during
-upgrade.
-
-Relates: https://github.com/389ds/389-ds-base/issues/7223
-
-Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
----
- .../healthcheck/health_system_indexes_test.py | 593 ++++++++++++++++++
- rpm/389-ds-base.spec.in | 51 +-
- src/lib389/lib389/cli_ctl/dbtasks.py | 402 ++++++++++++
- src/lib389/lib389/dseldif.py | 51 +-
- 4 files changed, 1068 insertions(+), 29 deletions(-)
-
-diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-index eb727b902..dd42cd197 100644
---- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
-@@ -590,6 +590,599 @@ def test_upgrade_removes_ancestorid_index_config(topology_st):
- log.info(f"Idempotency verified - ancestorid still absent after second restart (got exception: {e})")
-
-
-+def test_index_check_basic(topology_st):
-+ """Check if dsctl index-check works correctly
-+
-+ :id: 8a4e5c2d-1f3b-4a7c-9e8d-2b6f0c4a5d3e
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Run dsctl index-check while server is running (should fail)
-+ 3. Stop the server
-+ 4. Run dsctl index-check (should pass)
-+ 5. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. index-check returns False and logs error
-+ 3. Success
-+ 4. index-check returns True (no mismatches)
-+ 5. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Run index-check while server is running")
-+ args = FakeArgs()
-+ args.backend = None
-+ args.fix = False
-+
-+ # Server should be running, index-check should fail
-+ assert standalone.status()
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is False
-+ assert topology_st.logcap.contains("index-check requires the instance to be stopped")
-+ topology_st.logcap.flush()
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Run index-check with server stopped")
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is True
-+ assert topology_st.logcap.contains("All checks passed")
-+ topology_st.logcap.flush()
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_specific_backend(topology_st):
-+ """Check if dsctl index-check works with a specific backend
-+
-+ :id: 407d8fcc-62e0-43dd-90fa-70e7090a5cfd
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Run dsctl index-check with specific backend (userRoot)
-+ 4. Run dsctl index-check with non-existent backend
-+ 5. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. index-check returns True for userRoot
-+ 4. index-check returns False for non-existent backend
-+ 5. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Run index-check for userRoot backend")
-+ args = FakeArgs()
-+ args.backend = "userRoot"
-+ args.fix = False
-+
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is True
-+ # Check for backend name in any case
-+ assert topology_st.logcap.contains("Checking backend:")
-+ topology_st.logcap.flush()
-+
-+ log.info("Run index-check for non-existent backend")
-+ args.backend = "nonExistentBackend"
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is False
-+ assert topology_st.logcap.contains("not found")
-+ topology_st.logcap.flush()
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_mismatch_detection(topology_st):
-+ """Check if dsctl index-check detects ordering mismatch
-+
-+ :id: 50d14520-b0bf-4243-9fe6-b097928d4351
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Run dsctl index-check (without --fix)
-+ 4. Verify output format
-+ 5. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. index-check returns True (no mismatch on fresh instance)
-+ 4. Log contains expected format
-+ 5. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Run index-check to verify detection logic")
-+ args = FakeArgs()
-+ args.backend = "userRoot"
-+ args.fix = False
-+
-+ # On a fresh instance, there should be no mismatch
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ # Fresh instance should have matching config and disk ordering
-+ assert result is True
-+ # Check that the backend was checked (may skip indexes if ordering can't be determined)
-+ assert topology_st.logcap.contains("Checking backend:")
-+ topology_st.logcap.flush()
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_with_fix(topology_st):
-+ """Check if dsctl index-check --fix triggers reindexing
-+
-+ :id: 38ae36e4-c861-4771-ae7d-354370376a2f
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Run dsctl index-check --fix (should pass since no mismatch)
-+ 4. Verify output indicates check passed
-+ 5. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. index-check returns True
-+ 4. Log contains "All checks passed"
-+ 5. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Run index-check with --fix option")
-+ args = FakeArgs()
-+ args.backend = None
-+ args.fix = True
-+
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ # On a fresh instance, there should be no mismatch, so no reindexing needed
-+ assert result is True
-+ assert topology_st.logcap.contains("All checks passed")
-+ topology_st.logcap.flush()
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_fixes_scanlimit(topology_st):
-+ """Check if dsctl index-check --fix removes nsIndexIDListScanLimit
-+
-+ :id: 4a9b2c7d-8e1f-4b3a-9c5d-6e7f8a0b1c2d
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Add nsIndexIDListScanLimit to parentid index using DSEldif
-+ 4. Run dsctl index-check (should detect issue)
-+ 5. Run dsctl index-check --fix
-+ 6. Verify nsIndexIDListScanLimit was removed
-+ 7. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. index-check returns False and detects scanlimit
-+ 5. index-check returns True after fix
-+ 6. nsIndexIDListScanLimit no longer present
-+ 7. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+ from lib389.dseldif import DSEldif
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Add nsIndexIDListScanLimit to parentid index using DSEldif")
-+ dse_ldif = DSEldif(standalone)
-+ parentid_dn = "cn=parentid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config"
-+ dse_ldif.add(parentid_dn, "nsIndexIDListScanLimit", "4000")
-+
-+ # Verify it was added
-+ scanlimit = dse_ldif.get(parentid_dn, "nsIndexIDListScanLimit", single=True)
-+ assert scanlimit == "4000", f"Failed to add nsIndexIDListScanLimit, got: {scanlimit}"
-+ log.info("Added nsIndexIDListScanLimit to parentid index")
-+
-+ log.info("Run index-check without --fix (should detect issue)")
-+ 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 scanlimit issue"
-+ assert topology_st.logcap.contains("nsIndexIDListScanLimit")
-+ 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("Removed nsIndexIDListScanLimit")
-+ topology_st.logcap.flush()
-+
-+ log.info("Verify nsIndexIDListScanLimit was removed")
-+ dse_ldif = DSEldif(standalone) # Reload to get fresh data
-+ scanlimit = dse_ldif.get(parentid_dn, "nsIndexIDListScanLimit", single=True)
-+ assert scanlimit is None, f"nsIndexIDListScanLimit should be removed, but got: {scanlimit}"
-+ log.info("nsIndexIDListScanLimit successfully removed")
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_fixes_ancestorid_config(topology_st):
-+ """Check if dsctl index-check --fix removes ancestorid config entries
-+
-+ :id: 5b0c3d8e-9f2a-4c4b-0d6e-7f8a9b1c2d3e
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Add ancestorid index config entry using DSEldif
-+ 4. Run dsctl index-check (should detect issue)
-+ 5. Run dsctl index-check --fix
-+ 6. Verify ancestorid config entry was removed
-+ 7. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. index-check returns False and detects ancestorid config
-+ 5. index-check returns True after fix
-+ 6. ancestorid config entry no longer present
-+ 7. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+ from lib389.dseldif import DSEldif
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Add ancestorid index config entry using DSEldif")
-+ dse_ldif = DSEldif(standalone)
-+ ancestorid_entry = [
-+ "dn: cn=ancestorid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config\n",
-+ "objectClass: top\n",
-+ "objectClass: nsIndex\n",
-+ "cn: ancestorid\n",
-+ "nsSystemIndex: true\n",
-+ "nsIndexType: eq\n",
-+ ]
-+ dse_ldif.add_entry(ancestorid_entry)
-+
-+ # Verify it was added
-+ ancestorid_dn = "cn=ancestorid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config"
-+ dse_ldif = DSEldif(standalone) # Reload
-+ cn_value = dse_ldif.get(ancestorid_dn, "cn", single=True)
-+ 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("Run index-check without --fix (should detect issue)")
-+ 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 ancestorid config issue"
-+ assert topology_st.logcap.contains("ancestorid") and topology_st.logcap.contains("config entry exists")
-+ 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("Removed ancestorid config entry")
-+ topology_st.logcap.flush()
-+
-+ log.info("Verify ancestorid config entry was removed")
-+ dse_ldif = DSEldif(standalone) # Reload to get fresh data
-+ cn_value = dse_ldif.get(ancestorid_dn, "cn", single=True)
-+ assert cn_value is None, f"ancestorid config entry should be removed, but got: {cn_value}"
-+ log.info("ancestorid config entry successfully removed")
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_fixes_missing_matching_rule(topology_st):
-+ """Check if dsctl index-check --fix adds missing integerOrderingMatch
-+
-+ :id: 6c1d4e9f-0a3b-4d5c-1e7f-8a9b0c2d3e4f
-+ :setup: Standalone instance
-+ :steps:
-+ 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
-+ :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
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+ from lib389.dseldif import DSEldif
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Remove integerOrderingMatch from parentid index using DSEldif")
-+ dse_ldif = DSEldif(standalone)
-+ parentid_dn = "cn=parentid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config"
-+
-+ # Check current matching rules
-+ matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
-+ log.info(f"Current matching rules: {matching_rules}")
-+
-+ # Remove integerOrderingMatch if present
-+ if matching_rules:
-+ for mr in matching_rules:
-+ if "integerorderingmatch" in mr.lower():
-+ dse_ldif.delete(parentid_dn, "nsMatchingRule", mr)
-+ log.info(f"Removed matching rule: {mr}")
-+
-+ # Verify it was removed
-+ dse_ldif = DSEldif(standalone) # Reload
-+ matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
-+ if matching_rules:
-+ for mr in matching_rules:
-+ assert "integerorderingmatch" not in mr.lower(), \
-+ 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)")
-+ 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")
-+ 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("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_fixes_default_ancestorid(topology_st):
-+ """Check if dsctl index-check --fix removes ancestorid from default indexes
-+
-+ :id: 7d2e5f0a-1b4c-4e6d-2f8a-9b0c1d3e4f5a
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Add ancestorid to cn=default indexes using DSEldif
-+ 4. Run dsctl index-check (should detect issue)
-+ 5. Run dsctl index-check --fix
-+ 6. Verify ancestorid was removed from default indexes
-+ 7. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. index-check returns False and detects ancestorid in default indexes
-+ 5. index-check returns True after fix
-+ 6. ancestorid no longer in default indexes
-+ 7. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+ from lib389.dseldif import DSEldif
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ log.info("Add ancestorid to cn=default indexes using DSEldif")
-+ dse_ldif = DSEldif(standalone)
-+ ancestorid_default_entry = [
-+ "dn: cn=ancestorid,cn=default indexes,cn=config,cn=ldbm database,cn=plugins,cn=config\n",
-+ "objectClass: top\n",
-+ "objectClass: nsIndex\n",
-+ "cn: ancestorid\n",
-+ "nsSystemIndex: true\n",
-+ "nsIndexType: eq\n",
-+ ]
-+ dse_ldif.add_entry(ancestorid_default_entry)
-+
-+ # Verify it was added
-+ ancestorid_default_dn = "cn=ancestorid,cn=default indexes,cn=config,cn=ldbm database,cn=plugins,cn=config"
-+ dse_ldif = DSEldif(standalone) # Reload
-+ cn_value = dse_ldif.get(ancestorid_default_dn, "cn", single=True)
-+ assert cn_value is not None, "Failed to add ancestorid to default indexes"
-+ log.info(f"Added ancestorid to default indexes with cn: {cn_value}")
-+
-+ log.info("Run index-check without --fix (should detect issue)")
-+ args = FakeArgs()
-+ args.backend = None # Check all backends including default indexes
-+ args.fix = False
-+
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is False, "index-check should detect ancestorid in default indexes"
-+ assert topology_st.logcap.contains("ancestorid found in cn=default indexes")
-+ 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("Removed ancestorid from default indexes")
-+ topology_st.logcap.flush()
-+
-+ log.info("Verify ancestorid was removed from default indexes")
-+ dse_ldif = DSEldif(standalone) # Reload to get fresh data
-+ cn_value = dse_ldif.get(ancestorid_default_dn, "cn", single=True)
-+ assert cn_value is None, f"ancestorid should be removed from default indexes, but got: {cn_value}"
-+ log.info("ancestorid successfully removed from default indexes")
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
-+def test_index_check_fixes_multiple_issues(topology_st):
-+ """Check if dsctl index-check --fix handles multiple issues at once
-+
-+ :id: 8e3f6a1b-2c5d-4f7e-3a9b-0c1d2e4f5a6b
-+ :setup: Standalone instance
-+ :steps:
-+ 1. Create DS instance
-+ 2. Stop the server
-+ 3. Add multiple issues: scanlimit, ancestorid config, missing matching rule
-+ 4. Run dsctl index-check (should detect all issues)
-+ 5. Run dsctl index-check --fix
-+ 6. Verify all issues were fixed
-+ 7. Run dsctl index-check again (should pass)
-+ 8. Start the server
-+ :expectedresults:
-+ 1. Success
-+ 2. Success
-+ 3. Success
-+ 4. index-check returns False and detects all issues
-+ 5. index-check returns True after fix
-+ 6. All issues resolved
-+ 7. index-check returns True (no issues)
-+ 8. Success
-+ """
-+ from lib389.cli_ctl.dbtasks import dbtasks_index_check
-+ from lib389.dseldif import DSEldif
-+
-+ standalone = topology_st.standalone
-+
-+ log.info("Stop the server")
-+ standalone.stop()
-+
-+ dse_ldif = DSEldif(standalone)
-+ parentid_dn = "cn=parentid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config"
-+ ancestorid_dn = "cn=ancestorid,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config"
-+
-+ log.info("Add issue 1: nsIndexIDListScanLimit to parentid")
-+ dse_ldif.add(parentid_dn, "nsIndexIDListScanLimit", "4000")
-+
-+ log.info("Add issue 2: ancestorid index config entry")
-+ ancestorid_entry = [
-+ f"dn: {ancestorid_dn}\n",
-+ "objectClass: top\n",
-+ "objectClass: nsIndex\n",
-+ "cn: ancestorid\n",
-+ "nsSystemIndex: true\n",
-+ "nsIndexType: eq\n",
-+ ]
-+ 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"
-+ args.fix = False
-+
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is False, "index-check should detect multiple issues"
-+ # Check that multiple issues were detected
-+ assert topology_st.logcap.contains("nsIndexIDListScanLimit")
-+ assert topology_st.logcap.contains("ancestorid")
-+ 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("All issues fixed")
-+ topology_st.logcap.flush()
-+
-+ log.info("Verify all issues were fixed")
-+ dse_ldif = DSEldif(standalone) # Reload
-+
-+ # Check scanlimit removed
-+ scanlimit = dse_ldif.get(parentid_dn, "nsIndexIDListScanLimit", single=True)
-+ assert scanlimit is None, f"nsIndexIDListScanLimit should be removed, got: {scanlimit}"
-+
-+ # Check ancestorid config removed
-+ 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")
-+ args.fix = False
-+ result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
-+ assert result is True, "index-check should pass after fix"
-+ assert topology_st.logcap.contains("All checks passed")
-+ topology_st.logcap.flush()
-+
-+ log.info("Start the server")
-+ standalone.start()
-+
-+
- if __name__ == "__main__":
- # Run isolated
- # -s for DEBUG mode
-diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in
-index dc8c75dac..0e0e28285 100644
---- a/rpm/389-ds-base.spec.in
-+++ b/rpm/389-ds-base.spec.in
-@@ -628,42 +628,45 @@ if ! getent passwd $USERNAME >/dev/null ; then
- fi
-
- # Reload our sysctl before we restart (if we can)
--sysctl --system &> $output; true
-+sysctl --system &> "$output"; true
-
--# Gather the running instances so we can restart them
-+# Gather running instances, stop them, run index-check, then restart
- instbase="%{_sysconfdir}/%{pkgname}"
-+instances=""
- ninst=0
--for dir in $instbase/slapd-* ; do
-- echo dir = $dir >> $output 2>&1 || :
-+
-+for dir in "$instbase"/slapd-* ; do
-+ echo "dir = $dir" >> "$output" 2>&1 || :
- if [ ! -d "$dir" ] ; then continue ; fi
- case "$dir" in *.removed) continue ;; esac
-- basename=`basename $dir`
-- inst="%{pkgname}@`echo $basename | sed -e 's/slapd-//g'`"
-- echo found instance $inst - getting status >> $output 2>&1 || :
-- if /bin/systemctl -q is-active $inst ; then
-- echo instance $inst is running >> $output 2>&1 || :
-+ basename=$(basename "$dir")
-+ inst="%{pkgname}@${basename#slapd-}"
-+ inst_name="${basename#slapd-}"
-+ echo "found instance $inst - getting status" >> "$output" 2>&1 || :
-+ if /bin/systemctl -q is-active "$inst" ; then
-+ echo "instance $inst is running - stopping for upgrade" >> "$output" 2>&1 || :
- instances="$instances $inst"
-+ /bin/systemctl stop "$inst" >> "$output" 2>&1 || :
- else
-- echo instance $inst is not running >> $output 2>&1 || :
-+ echo "instance $inst is not running" >> "$output" 2>&1 || :
- fi
-- ninst=`expr $ninst + 1`
-+ # Run index-check on all instances (running or not)
-+ # This fixes index ordering mismatches from older versions
-+ dsctl "$inst_name" index-check --fix >> "$output2" 2>&1 || :
-+ ninst=$((ninst + 1))
- done
-+
- if [ $ninst -eq 0 ] ; then
-- echo no instances to upgrade >> $output 2>&1 || :
-- exit 0 # have no instances to upgrade - just skip the rest
--else
-- # restart running instances
-- echo shutting down all instances . . . >> $output 2>&1 || :
-- for inst in $instances ; do
-- echo stopping instance $inst >> $output 2>&1 || :
-- /bin/systemctl stop $inst >> $output 2>&1 || :
-- done
-- for inst in $instances ; do
-- echo starting instance $inst >> $output 2>&1 || :
-- /bin/systemctl start $inst >> $output 2>&1 || :
-- done
-+ echo "no instances to upgrade" >> "$output" 2>&1 || :
-+ exit 0
- fi
-
-+# Restart previously running instances
-+for inst in $instances ; do
-+ echo "starting instance $inst" >> "$output" 2>&1 || :
-+ /bin/systemctl start "$inst" >> "$output" 2>&1 || :
-+done
-+
-
- %preun
- if [ $1 -eq 0 ]; then # Final removal
-diff --git a/src/lib389/lib389/cli_ctl/dbtasks.py b/src/lib389/lib389/cli_ctl/dbtasks.py
-index 856639672..16da966d1 100644
---- a/src/lib389/lib389/cli_ctl/dbtasks.py
-+++ b/src/lib389/lib389/cli_ctl/dbtasks.py
-@@ -7,12 +7,24 @@
- # See LICENSE for details.
- # --- END COPYRIGHT BLOCK ---
-
-+import glob
- import os
-+import re
-+import subprocess
-+from enum import Enum
- from lib389._constants import TaskWarning
- from lib389.cli_base import CustomHelpFormatter
-+from lib389.dseldif import DSEldif
- from pathlib import Path
-
-
-+class IndexOrdering(Enum):
-+ """Represents the ordering type of an index."""
-+ INTEGER = "integer"
-+ LEXICOGRAPHIC = "lexicographic"
-+ UNKNOWN = "unknown"
-+
-+
- def dbtasks_db2index(inst, log, args):
- rtn = False
- if not args.backend:
-@@ -126,6 +138,387 @@ def dbtasks_verify(inst, log, args):
- log.info("dbverify successful")
-
-
-+def _get_db_dir(dse_ldif):
-+ """Get the database directory.
-+
-+ Args:
-+ dse_ldif: DSEldif instance.
-+
-+ Returns:
-+ Path to the database directory, or None if not found.
-+ """
-+ try:
-+ db_dir = dse_ldif.get(
-+ "cn=config,cn=ldbm database,cn=plugins,cn=config",
-+ "nsslapd-directory",
-+ single=True,
-+ )
-+ return db_dir
-+ except (ValueError, TypeError):
-+ pass
-+ return None
-+
-+
-+
-+def _has_integer_ordering_match(dse_ldif, backend, index_name):
-+ """Check if an index has integerOrderingMatch configured in DSE.
-+
-+ Args:
-+ dse_ldif: DSEldif instance.
-+ backend: Backend name.
-+ index_name: Name of the index to check.
-+
-+ Returns:
-+ True if integerOrderingMatch is configured, False otherwise.
-+ """
-+ index_dn = "cn={},cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(
-+ index_name, backend
-+ )
-+ matching_rules = dse_ldif.get(index_dn, "nsMatchingRule", lower=True)
-+ if matching_rules:
-+ return any(mr.lower() == "integerorderingmatch" for mr in matching_rules)
-+ return False
-+
-+
-+def _has_index_scan_limit(dse_ldif, backend, index_name):
-+ """Check if an index has nsIndexIDListScanLimit configured.
-+
-+ Args:
-+ dse_ldif: DSEldif instance.
-+ backend: Backend name.
-+ index_name: Name of the index to check.
-+
-+ Returns:
-+ True if nsIndexIDListScanLimit is configured, False otherwise.
-+ """
-+ index_dn = "cn={},cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(
-+ index_name, backend
-+ )
-+ scan_limit = dse_ldif.get(index_dn, "nsIndexIDListScanLimit")
-+ return scan_limit is not None
-+
-+
-+def _index_config_exists(dse_ldif, backend, index_name):
-+ """Check if an index configuration entry exists in DSE.
-+
-+ Args:
-+ dse_ldif: DSEldif instance.
-+ backend: Backend name.
-+ index_name: Name of the index to check.
-+
-+ Returns:
-+ True if the index config entry exists, False otherwise.
-+ """
-+ index_dn = "cn={},cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(
-+ index_name, backend
-+ )
-+ try:
-+ cn = dse_ldif.get(index_dn, "cn")
-+ return cn is not None
-+ except (ValueError, KeyError):
-+ return False
-+
-+
-+def _default_index_exists(dse_ldif, index_name):
-+ """Check if an index exists in cn=default indexes.
-+
-+ Args:
-+ dse_ldif: DSEldif instance.
-+ index_name: Name of the index to check.
-+
-+ Returns:
-+ True if the index exists in default indexes, False otherwise.
-+ """
-+ index_dn = "cn={},cn=default indexes,cn=config,cn=ldbm database,cn=plugins,cn=config".format(
-+ index_name
-+ )
-+ try:
-+ cn = dse_ldif.get(index_dn, "cn")
-+ return cn is not None
-+ except (ValueError, KeyError):
-+ return False
-+
-+
-+def _check_disk_ordering(db_dir, backend, index_name, dbscan_path, is_mdb, log):
-+ """Check if index on disk uses lexicographic or integer ordering.
-+
-+ Args:
-+ db_dir: Path to the database directory.
-+ backend: Backend name.
-+ index_name: Name of the index to check.
-+ dbscan_path: Path to the dbscan binary.
-+ is_mdb: True if using MDB backend.
-+ log: Logger instance.
-+
-+ Returns:
-+ IndexOrdering: The detected ordering type.
-+ """
-+ if is_mdb:
-+ # MDB uses pseudo-paths: db_dir/backend/index.db
-+ # dbscan accesses indexes via paths like: /var/lib/dirsrv/slapd-xxx/db/userroot/parentid.db
-+ index_file = os.path.join(db_dir, backend, "{}.db".format(index_name))
-+ else:
-+ # BDB has separate directories per backend with actual index files
-+ backend_dir = os.path.join(db_dir, backend)
-+ if not os.path.exists(backend_dir):
-+ return IndexOrdering.UNKNOWN
-+ index_file = None
-+ pattern = os.path.join(backend_dir, "{}.db*".format(index_name))
-+ for f in glob.glob(pattern):
-+ if os.path.isfile(f):
-+ index_file = f
-+ break
-+ if not index_file:
-+ return IndexOrdering.UNKNOWN
-+
-+ try:
-+ result = subprocess.run(
-+ [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 = line.strip()
-+ if line.startswith("="):
-+ match = re.match(r"^=(\d+)", line)
-+ if match:
-+ keys.append(int(match.group(1)))
-+
-+ 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]
-+ 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
-+
-+
-+def dbtasks_index_check(inst, log, args):
-+ """Check and optionally fix index ordering mismatches.
-+
-+ This function detects mismatches between the configured ordering
-+ (integerOrderingMatch in DSE) and the actual on-disk ordering of
-+ parentid and ancestorid indexes.
-+
-+ Args:
-+ inst: DirSrv instance.
-+ log: Logger instance.
-+ args: Parsed command line arguments.
-+
-+ Returns:
-+ True if all checks passed, False if mismatches were detected.
-+ """
-+ # Server must be stopped
-+ if inst.status():
-+ log.error("index-check requires the instance to be stopped")
-+ return False
-+
-+ # Check for dbscan binary
-+ dbscan_path = os.path.join(inst.ds_paths.bin_dir, "dbscan")
-+ if not os.path.exists(dbscan_path):
-+ log.error("dbscan utility not found at %s", dbscan_path)
-+ return False
-+
-+ # Load DSE
-+ try:
-+ dse_ldif = DSEldif(inst)
-+ except Exception as e:
-+ log.error("Failed to read dse.ldif: %s", e)
-+ return False
-+
-+ # Get backends to check
-+ all_backends = dse_ldif.get_backends()
-+ if not all_backends:
-+ log.info("No backends found")
-+ return True
-+
-+ # Filter to specific backend if requested
-+ if args.backend:
-+ # Case-insensitive backend lookup
-+ backend_lower = args.backend.lower()
-+ matching_backend = None
-+ for be in all_backends:
-+ if be.lower() == backend_lower:
-+ matching_backend = be
-+ break
-+ if matching_backend is None:
-+ log.error("Backend '%s' not found. Available backends: %s",
-+ args.backend, ", ".join(all_backends))
-+ return False
-+ backends_to_check = [matching_backend]
-+ else:
-+ backends_to_check = all_backends
-+
-+ # Get database directory and check database type
-+ db_dir = _get_db_dir(dse_ldif)
-+ if not db_dir or not os.path.exists(db_dir):
-+ log.error("Database directory not found")
-+ return False
-+
-+ db_lib = inst.get_db_lib()
-+ is_mdb = (db_lib == "mdb")
-+ log.info("Database type: %s", db_lib.upper())
-+
-+ # Track all issues found
-+ all_ok = True
-+ mismatches = [] # (backend, index_name) tuples needing reindex
-+ missing_matching_rules = [] # (backend, index_name) tuples missing integerOrderingMatch
-+ 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
-+
-+ # Check if ancestorid exists in cn=default indexes (should be removed)
-+ if _default_index_exists(dse_ldif, "ancestorid"):
-+ log.warning("ancestorid found in cn=default indexes - should be removed")
-+ remove_ancestorid_from_defaults = True
-+ all_ok = False
-+
-+ for backend in backends_to_check:
-+ log.info("Checking backend: %s", backend)
-+
-+ # Check for ancestorid config entry (should not exist)
-+ if _index_config_exists(dse_ldif, backend, "ancestorid"):
-+ log.warning(" ancestorid - config entry exists (should be removed)")
-+ ancestorid_configs_to_remove.append(backend)
-+ all_ok = False
-+
-+ # Check parentid and ancestorid indexes
-+ for index_name in ["parentid", "ancestorid"]:
-+ # Check for scan limits (should be removed)
-+ if _has_index_scan_limit(dse_ldif, backend, index_name):
-+ log.warning(" %s - has nsIndexIDListScanLimit (should be removed)", index_name)
-+ scan_limits_to_remove.append((backend, index_name))
-+ all_ok = False
-+
-+ # Check disk ordering
-+ disk_ordering = _check_disk_ordering(db_dir, backend, index_name, dbscan_path, is_mdb, log)
-+
-+ 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)
-+ config_desc = "integer" if config_has_int_order else "lexicographic"
-+ log.info(" %s - config: %s, disk: %s",
-+ index_name, config_desc, disk_ordering.value)
-+
-+ # For parentid, the desired state is always integer ordering
-+ 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))
-+ 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))
-+ all_ok = False
-+
-+ # Handle issues
-+ if not all_ok:
-+ if args.fix:
-+ log.info("Fixing issues...")
-+
-+ # Remove ancestorid from cn=default indexes
-+ if remove_ancestorid_from_defaults:
-+ default_idx_dn = "cn=ancestorid,cn=default indexes,cn=config,cn=ldbm database,cn=plugins,cn=config"
-+ log.info(" Removing ancestorid from default indexes...")
-+ try:
-+ dse_ldif.delete_dn(default_idx_dn)
-+ log.info(" Removed ancestorid from default indexes")
-+ except Exception as e:
-+ log.error(" Failed to remove ancestorid from default indexes: %s", e)
-+ return False
-+
-+ # Remove scan limits (only for indexes that won't be deleted)
-+ for backend, index_name in scan_limits_to_remove:
-+ # Skip ancestorid if we're going to delete the whole entry anyway
-+ if index_name == "ancestorid" and backend in ancestorid_configs_to_remove:
-+ continue
-+ index_dn = "cn={},cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(
-+ index_name, backend
-+ )
-+ log.info(" Removing nsIndexIDListScanLimit from %s in backend %s...", index_name, backend)
-+ try:
-+ dse_ldif.delete(index_dn, "nsIndexIDListScanLimit")
-+ log.info(" Removed nsIndexIDListScanLimit from %s", index_name)
-+ except Exception as e:
-+ log.error(" Failed to remove nsIndexIDListScanLimit from %s: %s", index_name, e)
-+ return False
-+
-+ # Remove ancestorid config entries from backends
-+ for backend in ancestorid_configs_to_remove:
-+ index_dn = "cn=ancestorid,cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(backend)
-+ log.info(" Removing ancestorid config entry from backend %s...", backend)
-+ try:
-+ dse_ldif.delete_dn(index_dn)
-+ log.info(" Removed ancestorid config entry from backend %s", backend)
-+ except Exception as e:
-+ 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:
-+ 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)
-+
-+ log.info("All issues fixed")
-+ return True
-+ else:
-+ log.info("Issues detected. Run with --fix to repair.")
-+ return False
-+ else:
-+ log.info("All checks passed - no issues found")
-+ return True
-+
-+
- 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.")
-@@ -172,3 +565,12 @@ def create_parser(subcommands):
- ldifs_parser = subcommands.add_parser('ldifs', help="List all the LDIF files located in the server's LDIF directory", formatter_class=CustomHelpFormatter)
- ldifs_parser.add_argument('--delete', nargs=1, help="Delete LDIF file")
- ldifs_parser.set_defaults(func=dbtasks_ldifs)
-+
-+ index_check_parser = subcommands.add_parser('index-check',
-+ help="Check for index ordering mismatches (parentid/ancestorid). The server must be stopped.",
-+ formatter_class=CustomHelpFormatter)
-+ 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")
-+ index_check_parser.set_defaults(func=dbtasks_index_check)
-diff --git a/src/lib389/lib389/dseldif.py b/src/lib389/lib389/dseldif.py
-index d12c6424c..7834d9468 100644
---- a/src/lib389/lib389/dseldif.py
-+++ b/src/lib389/lib389/dseldif.py
-@@ -125,11 +125,14 @@ class DSEldif(DSLint):
- self._contents[i] = self._contents[i].replace(strfrom, strto)
- self._update()
-
-- def _find_attr(self, entry_dn, attr):
-+ def _find_attr(self, entry_dn, attr, lower=False):
- """Find all attribute values and indexes under a given entry
-
- Returns entry dn index and attribute data dict:
- relative attribute indexes and the attribute value
-+
-+ :param lower: Use case-insensitive matching for attribute name
-+ :type lower: boolean
- """
-
- entry_dn_i = self._contents.index("dn: {}\n".format(entry_dn.lower()))
-@@ -146,7 +149,11 @@ class DSEldif(DSLint):
-
- # Find the attribute
- for line in entry_slice:
-- if line.startswith("{}:".format(attr)):
-+ if lower:
-+ match = line.lower().startswith("{}:".format(attr.lower()))
-+ else:
-+ match = line.startswith("{}:".format(attr))
-+ if match:
- attr_value = line.split(" ", 1)[1][:-1]
- attr_data.update({entry_slice.index(line): attr_value})
-
-@@ -155,7 +162,7 @@ class DSEldif(DSLint):
-
- return entry_dn_i, attr_data
-
-- def get(self, entry_dn, attr, single=False):
-+ def get(self, entry_dn, attr, single=False, lower=False):
- """Return attribute values under a given entry
-
- :param entry_dn: a DN of entry we want to get attribute from
-@@ -163,11 +170,13 @@ class DSEldif(DSLint):
- :param attr: an attribute name
- :type attr: str
- :param single: Return a single value instead of a list
-- :type sigle: boolean
-+ :type single: boolean
-+ :param lower: Use case-insensitive matching for attribute name
-+ :type lower: boolean
- """
-
- try:
-- _, attr_data = self._find_attr(entry_dn, attr)
-+ _, attr_data = self._find_attr(entry_dn, attr, lower=lower)
- except ValueError:
- return None
-
-@@ -190,6 +199,38 @@ class DSEldif(DSLint):
-
- return indexes
-
-+ def get_backends(self):
-+ """Return a list of backend names from DSE.
-+
-+ Returns backend names preserving their original case, as the
-+ database directory names on disk use the original case.
-+
-+ Note: DSEldif lowercases DN lines, so we read the 'cn' attribute
-+ from each entry to get the original case.
-+
-+ :returns: List of backend names
-+ """
-+ backends = []
-+ excluded = ("config", "monitor", "index", "encrypted attributes")
-+
-+ for entry in self._contents:
-+ if (entry.startswith("dn: cn=") and
-+ ",cn=ldbm database,cn=plugins,cn=config" in entry):
-+ parts = entry.split(",")
-+ if len(parts) > 1:
-+ cn_lower = parts[0].replace("dn: cn=", "")
-+ if cn_lower not in excluded:
-+ dn = entry.strip()[4:].strip()
-+ try:
-+ suffix = self.get(dn, "nsslapd-suffix")
-+ if suffix:
-+ cn_values = self.get(dn, "cn")
-+ if cn_values:
-+ backends.append(cn_values[0])
-+ except (ValueError, IndexError):
-+ pass
-+
-+ return list(set(backends))
-
- def add_entry(self, entry):
- """Add a new entry
---
-2.52.0
-
diff --git a/0037-Issue-7230-Regression-in-healtcheck-NssCheck-7235.patch b/0037-Issue-7230-Regression-in-healtcheck-NssCheck-7235.patch
deleted file mode 100644
index 319e556..0000000
--- a/0037-Issue-7230-Regression-in-healtcheck-NssCheck-7235.patch
+++ /dev/null
@@ -1,62 +0,0 @@
-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
-
diff --git a/0038-Issue-3555-UI-Fix-audit-issue-with-npm-isaacs-brace-.patch b/0038-Issue-3555-UI-Fix-audit-issue-with-npm-isaacs-brace-.patch
deleted file mode 100644
index b0487f7..0000000
--- a/0038-Issue-3555-UI-Fix-audit-issue-with-npm-isaacs-brace-.patch
+++ /dev/null
@@ -1,1225 +0,0 @@
-From b24ae4a7710c66b8c224ebd498dc82463e46f45b Mon Sep 17 00:00:00 2001
-From: Simon Pichugin <spichugi@redhat.com>
-Date: Tue, 10 Feb 2026 13:09:22 -0800
-Subject: [PATCH] Issue 3555 - UI - Fix audit issue with npm -
- @isaacs/brace-expansion (#7228)
-
-Description: Run npm audit fix to address the vulnerability
-in @isaacs/brace-expansion.
-
-Relates: https://github.com/389ds/389-ds-base/issues/3555
-
-Reviewed by: @mreynolds389 (Thanks!)
----
- src/cockpit/389-console/package-lock.json | 586 +++++++++++++++++++---
- 1 file changed, 512 insertions(+), 74 deletions(-)
-
-diff --git a/src/cockpit/389-console/package-lock.json b/src/cockpit/389-console/package-lock.json
-index 23faef62f..1cbd940e4 100644
---- a/src/cockpit/389-console/package-lock.json
-+++ b/src/cockpit/389-console/package-lock.json
-@@ -102,8 +102,7 @@
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.2.tgz",
- "integrity": "sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==",
-- "dev": true,
-- "peer": true
-+ "dev": true
- },
- "node_modules/@csstools/css-parser-algorithms": {
- "version": "3.0.4",
-@@ -120,6 +119,7 @@
- "url": "https://opencollective.com/csstools"
- }
- ],
-+ "peer": true,
- "engines": {
- "node": ">=18"
- },
-@@ -142,6 +142,7 @@
- "url": "https://opencollective.com/csstools"
- }
- ],
-+ "peer": true,
- "engines": {
- "node": ">=18"
- }
-@@ -722,6 +723,7 @@
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
- "integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
- "hasInstallScript": true,
-+ "peer": true,
- "dependencies": {
- "@fortawesome/fontawesome-common-types": "^0.2.36"
- },
-@@ -822,9 +824,9 @@
- }
- },
- "node_modules/@isaacs/brace-expansion": {
-- "version": "5.0.0",
-- "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
-- "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
-+ "version": "5.0.1",
-+ "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
-+ "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
-@@ -1157,7 +1159,6 @@
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.13.0.tgz",
- "integrity": "sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "@typescript-eslint/scope-manager": "8.13.0",
- "@typescript-eslint/types": "8.13.0",
-@@ -1186,7 +1187,6 @@
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.13.0.tgz",
- "integrity": "sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "@typescript-eslint/types": "8.13.0",
- "@typescript-eslint/visitor-keys": "8.13.0"
-@@ -1204,7 +1204,6 @@
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.13.0.tgz",
- "integrity": "sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "@typescript-eslint/types": "8.13.0",
- "eslint-visitor-keys": "^3.4.3"
-@@ -1317,7 +1316,6 @@
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.13.0.tgz",
- "integrity": "sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==",
- "dev": true,
-- "peer": true,
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
-@@ -1331,7 +1329,6 @@
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.13.0.tgz",
- "integrity": "sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "@typescript-eslint/types": "8.13.0",
- "@typescript-eslint/visitor-keys": "8.13.0",
-@@ -1360,7 +1357,6 @@
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.13.0.tgz",
- "integrity": "sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "@typescript-eslint/types": "8.13.0",
- "eslint-visitor-keys": "^3.4.3"
-@@ -1483,13 +1479,15 @@
- "node_modules/@xterm/xterm": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
-- "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
-+ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
-+ "peer": true
- },
- "node_modules/acorn": {
- "version": "8.14.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
- "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
- "dev": true,
-+ "peer": true,
- "bin": {
- "acorn": "bin/acorn"
- },
-@@ -1856,15 +1854,13 @@
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
- "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
-- "dev": true,
-- "peer": true
-+ "dev": true
- },
- "node_modules/builtin-modules": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
- "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
- "dev": true,
-- "peer": true,
- "engines": {
- "node": ">=6"
- },
-@@ -1877,7 +1873,6 @@
- "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz",
- "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "semver": "^7.0.0"
- }
-@@ -1991,8 +1986,7 @@
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
- "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
-- "dev": true,
-- "peer": true
-+ "dev": true
- },
- "node_modules/commander": {
- "version": "2.11.0",
-@@ -2629,6 +2623,7 @@
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
-+ "peer": true,
- "bin": {
- "esbuild": "bin/esbuild"
- },
-@@ -2732,6 +2727,7 @@
- "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
- "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
- "dev": true,
-+ "peer": true,
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.6.1",
-@@ -2787,7 +2783,6 @@
- "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz",
- "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "semver": "^7.5.4"
- },
-@@ -2950,7 +2945,6 @@
- "https://github.com/sponsors/ota-meshi",
- "https://opencollective.com/eslint"
- ],
-- "peer": true,
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.1.2",
- "@eslint-community/regexpp": "^4.11.0",
-@@ -2968,6 +2962,7 @@
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
- "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
- "dev": true,
-+ "peer": true,
- "dependencies": {
- "@rtsao/scc": "^1.1.0",
- "array-includes": "^3.1.8",
-@@ -3107,7 +3102,6 @@
- "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz",
- "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "builtins": "^5.0.1",
-@@ -3137,7 +3131,6 @@
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
- "dev": true,
- "license": "MIT",
-- "peer": true,
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
-@@ -3148,7 +3141,6 @@
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
-@@ -3213,6 +3205,7 @@
- "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz",
- "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==",
- "dev": true,
-+ "peer": true,
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
-@@ -3228,6 +3221,7 @@
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz",
- "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==",
- "dev": true,
-+ "peer": true,
- "dependencies": {
- "array-includes": "^3.1.8",
- "array.prototype.findlast": "^1.2.5",
-@@ -3260,6 +3254,7 @@
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
- "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
- "dev": true,
-+ "peer": true,
- "engines": {
- "node": ">=10"
- },
-@@ -3657,6 +3652,21 @@
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true
- },
-+ "node_modules/fsevents": {
-+ "version": "2.3.3",
-+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
-+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-+ "dev": true,
-+ "hasInstallScript": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "darwin"
-+ ],
-+ "engines": {
-+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-+ }
-+ },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
-@@ -3746,7 +3756,6 @@
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz",
- "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "resolve-pkg-maps": "^1.0.0"
- },
-@@ -4281,7 +4290,6 @@
- "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
- "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "builtin-modules": "^3.3.0"
- },
-@@ -5409,6 +5417,7 @@
- "url": "https://github.com/sponsors/ai"
- }
- ],
-+ "peer": true,
- "dependencies": {
- "nanoid": "^3.3.7",
- "picocolors": "^1.1.0",
-@@ -5487,6 +5496,7 @@
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
- "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
- "dev": true,
-+ "peer": true,
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
-@@ -5610,6 +5620,7 @@
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
-+ "peer": true,
- "dependencies": {
- "loose-envify": "^1.1.0"
- },
-@@ -5621,6 +5632,7 @@
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
-+ "peer": true,
- "dependencies": {
- "loose-envify": "^1.1.0",
- "scheduler": "^0.23.2"
-@@ -5805,7 +5817,6 @@
- "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
- "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
- "dev": true,
-- "peer": true,
- "funding": {
- "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
- }
-@@ -5908,7 +5919,6 @@
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
- "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "tslib": "^2.1.0"
- }
-@@ -6000,7 +6010,6 @@
- "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.80.6.tgz",
- "integrity": "sha512-Og4aqBnaA3oJfIpHaLuNATAqzBRgUJDYJy2X15V59cot2wYOtiT/ciPnyuq1o7vpDEeOkHhEd+mSviSlXoETug==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "@bufbuild/protobuf": "^2.0.0",
- "buffer-builder": "^0.2.0",
-@@ -6039,6 +6048,244 @@
- "sass-embedded-win32-x64": "1.80.6"
- }
- },
-+ "node_modules/sass-embedded-android-arm": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.80.6.tgz",
-+ "integrity": "sha512-UeUKMTRsnz4/dh7IzvhjONxa4/jmVp539CHDd8VZOsqg9M3HcNJNIkUzQWbuwZ+nSlWrTuo7Tvn3XlypopCBzw==",
-+ "cpu": [
-+ "arm"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "android"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-android-arm64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.80.6.tgz",
-+ "integrity": "sha512-4rC4ZGM/k4ENVjLXnK3JTst8e8FI9MHSol2Fl7dCdYyJ3KLnlt4qL4AEYfU8zq1tcBb7CBOSZVR+CzCKubnXdg==",
-+ "cpu": [
-+ "arm64"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "android"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-android-ia32": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.80.6.tgz",
-+ "integrity": "sha512-Lxz2SXE2KdHnynuHF+D6flDvrd55/zaEAWUeka9MxEr6FmR66d8UBOIy5ETwCSUd//S/SE5Jl6oTnHppgD1zNA==",
-+ "cpu": [
-+ "ia32"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "android"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-android-riscv64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.80.6.tgz",
-+ "integrity": "sha512-hKdxY/oOqB+JJhSoBTDM5DJO1j/xtxQgayh2cLCCUx37IQQe3SEdc3V2JFf/4mIo5peaS4cjqwwSATF+l2zaXg==",
-+ "cpu": [
-+ "riscv64"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "android"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-android-x64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.80.6.tgz",
-+ "integrity": "sha512-Eap2Fi3kTx/rVLBsOnOp5RYPr5+lFjTZ652zR24dmYFe9/sDgasakJIOPjOvD2bRuL9z0uWEY1AXVeeOPeZKrg==",
-+ "cpu": [
-+ "x64"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "android"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-darwin-arm64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.80.6.tgz",
-+ "integrity": "sha512-0mnAx8Vq6Gxj3PQt3imgITfK33hhqrSKpyHSuab71gZZni5opsdtoggq2JawW+1taRFTEZwbZJLKZ0MBDbwCCA==",
-+ "cpu": [
-+ "arm64"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "darwin"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-darwin-x64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.80.6.tgz",
-+ "integrity": "sha512-Ib20yNZFOrJ7YVT+ltoe+JQNKPcRclM3iLAK69XZZYcSeFM/72SCoQBAaVGIpT23dxDp7FXiE4lO602c3xTRwQ==",
-+ "cpu": [
-+ "x64"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "darwin"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-linux-arm": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.80.6.tgz",
-+ "integrity": "sha512-QR0Q6TZox/ThuU2r9c0s3fKCgU2rXAEocpitdgxFp6tta+GsQlMFV3oON2unAa8Bwnuxkmf0YOaK0Oy/TwzkXw==",
-+ "cpu": [
-+ "arm"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "linux"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-linux-arm64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.80.6.tgz",
-+ "integrity": "sha512-n5r98pBXawrQQKaxIYCMM1zDpnngsqxTkOrmvsYLFiAMCSbR0lWf/7sBB33k/Pm0D6dsbp3jpHilCoQNKI3jIw==",
-+ "cpu": [
-+ "arm64"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "linux"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-linux-ia32": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.80.6.tgz",
-+ "integrity": "sha512-O6dWZdcOkryRdDCxVMGOeVowgblpDgVcAuRtZ1F1X7XfbpDriTQm64D+9vVZIrywYSPoJfQMJJ662cr0wUs9IQ==",
-+ "cpu": [
-+ "ia32"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "linux"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-linux-musl-arm": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.80.6.tgz",
-+ "integrity": "sha512-X9FC8s8fvQGRiXc+eATlZ57N44Iq3nNa0M0ugi3ysdJwkaNYvOeS4QzBHKQAaw3QiTqdxTnLUHHVBkyzdCi9pw==",
-+ "cpu": [
-+ "arm"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "linux"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-linux-musl-arm64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.80.6.tgz",
-+ "integrity": "sha512-VeUSHUi3MAsvOlg9QI4X/2j04h1659aE+7qKP/282CYBTrGkjFGSXZhIki9WKWDgIpDiSInRYXfQQRWhPhjCDg==",
-+ "cpu": [
-+ "arm64"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "linux"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-linux-musl-ia32": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.80.6.tgz",
-+ "integrity": "sha512-GqitS2Nab8ah0+wfCqaxW1hnI1piC08FimL6+lM9YWK5DbCOOF82IapbvJOy0feUmd/wNnHmyNTgE9h0zVMFdQ==",
-+ "cpu": [
-+ "ia32"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "linux"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-linux-musl-riscv64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.80.6.tgz",
-+ "integrity": "sha512-ySs15z7QSRRQK/aByEEqaJLYW/sTpfynefNPZCtsVNVEzNRwy+DRpxNChtxo+QjKq97ocXETbdG5KLik7QOTJg==",
-+ "cpu": [
-+ "riscv64"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "linux"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
- "node_modules/sass-embedded-linux-musl-x64": {
- "version": "1.80.6",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.80.6.tgz",
-@@ -6051,7 +6298,23 @@
- "os": [
- "linux"
- ],
-- "peer": true,
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-linux-riscv64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.80.6.tgz",
-+ "integrity": "sha512-AyoHJ3icV9xuJjq1YzJqpEj2XfiC/KBkVYTUrCELKiXP0DN1gi/BpUwZNCAgCM3CyEdMef4LQM/ztCYJxYzdyg==",
-+ "cpu": [
-+ "riscv64"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "linux"
-+ ],
- "engines": {
- "node": ">=14.0.0"
- }
-@@ -6068,7 +6331,57 @@
- "os": [
- "linux"
- ],
-- "peer": true,
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-win32-arm64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.80.6.tgz",
-+ "integrity": "sha512-29wETQi1ykeVvpd4zMVokpQKFSOZskGJzZawuuNCdo7BHjHKIRDsqbz8YT1CewHPBshI0hfD21fenmjxYjGXPQ==",
-+ "cpu": [
-+ "arm64"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "win32"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-win32-ia32": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.80.6.tgz",
-+ "integrity": "sha512-1s3OpK2iTIfIL/a91QhAQnffsbuWfnsM8Lx4Fxt0f7ErnxjCV6q8MUFTV/UhcLtLyTFnPCA62DLjp2KGCjMI9A==",
-+ "cpu": [
-+ "ia32"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "win32"
-+ ],
-+ "engines": {
-+ "node": ">=14.0.0"
-+ }
-+ },
-+ "node_modules/sass-embedded-win32-x64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.80.6.tgz",
-+ "integrity": "sha512-0pH4Zr9silHkcmLPC0ghnD3DI0vMsjA7dKvGR32/RbbjOSvHV5cDQRLiuVJAPp34dfMA7kJd1ysSchRdH0igAQ==",
-+ "cpu": [
-+ "x64"
-+ ],
-+ "dev": true,
-+ "license": "MIT",
-+ "optional": true,
-+ "os": [
-+ "win32"
-+ ],
- "engines": {
- "node": ">=14.0.0"
- }
-@@ -6078,7 +6391,6 @@
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "dev": true,
-- "peer": true,
- "dependencies": {
- "has-flag": "^4.0.0"
- },
-@@ -6529,6 +6841,7 @@
- "url": "https://github.com/sponsors/stylelint"
- }
- ],
-+ "peer": true,
- "dependencies": {
- "@csstools/css-parser-algorithms": "^3.0.1",
- "@csstools/css-tokenizer": "^3.0.1",
-@@ -7183,6 +7496,7 @@
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
- "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
- "dev": true,
-+ "peer": true,
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
-@@ -7245,8 +7559,7 @@
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
- "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
-- "dev": true,
-- "peer": true
-+ "dev": true
- },
- "node_modules/victory-area": {
- "version": "37.3.1",
-@@ -7837,21 +8150,22 @@
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.2.tgz",
- "integrity": "sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==",
-- "dev": true,
-- "peer": true
-+ "dev": true
- },
- "@csstools/css-parser-algorithms": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
- "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==",
- "dev": true,
-+ "peer": true,
- "requires": {}
- },
- "@csstools/css-tokenizer": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz",
- "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==",
-- "dev": true
-+ "dev": true,
-+ "peer": true
- },
- "@csstools/media-query-list-parser": {
- "version": "3.0.1",
-@@ -8116,6 +8430,7 @@
- "version": "1.2.36",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz",
- "integrity": "sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA==",
-+ "peer": true,
- "requires": {
- "@fortawesome/fontawesome-common-types": "^0.2.36"
- }
-@@ -8187,9 +8502,9 @@
- "dev": true
- },
- "@isaacs/brace-expansion": {
-- "version": "5.0.0",
-- "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
-- "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
-+ "version": "5.0.1",
-+ "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
-+ "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
- "dev": true,
- "requires": {
- "@isaacs/balanced-match": "^4.0.1"
-@@ -8458,7 +8773,6 @@
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.13.0.tgz",
- "integrity": "sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==",
- "dev": true,
-- "peer": true,
- "requires": {
- "@typescript-eslint/scope-manager": "8.13.0",
- "@typescript-eslint/types": "8.13.0",
-@@ -8472,7 +8786,6 @@
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.13.0.tgz",
- "integrity": "sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==",
- "dev": true,
-- "peer": true,
- "requires": {
- "@typescript-eslint/types": "8.13.0",
- "@typescript-eslint/visitor-keys": "8.13.0"
-@@ -8483,7 +8796,6 @@
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.13.0.tgz",
- "integrity": "sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==",
- "dev": true,
-- "peer": true,
- "requires": {
- "@typescript-eslint/types": "8.13.0",
- "eslint-visitor-keys": "^3.4.3"
-@@ -8549,15 +8861,13 @@
- "version": "8.13.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.13.0.tgz",
- "integrity": "sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==",
-- "dev": true,
-- "peer": true
-+ "dev": true
- },
- "@typescript-eslint/typescript-estree": {
- "version": "8.13.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.13.0.tgz",
- "integrity": "sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==",
- "dev": true,
-- "peer": true,
- "requires": {
- "@typescript-eslint/types": "8.13.0",
- "@typescript-eslint/visitor-keys": "8.13.0",
-@@ -8574,7 +8884,6 @@
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.13.0.tgz",
- "integrity": "sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==",
- "dev": true,
-- "peer": true,
- "requires": {
- "@typescript-eslint/types": "8.13.0",
- "eslint-visitor-keys": "^3.4.3"
-@@ -8651,13 +8960,15 @@
- "@xterm/xterm": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
-- "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
-+ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
-+ "peer": true
- },
- "acorn": {
- "version": "8.14.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
- "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
-- "dev": true
-+ "dev": true,
-+ "peer": true
- },
- "acorn-jsx": {
- "version": "5.3.2",
-@@ -8918,22 +9229,19 @@
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
- "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
-- "dev": true,
-- "peer": true
-+ "dev": true
- },
- "builtin-modules": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
- "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
-- "dev": true,
-- "peer": true
-+ "dev": true
- },
- "builtins": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz",
- "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==",
- "dev": true,
-- "peer": true,
- "requires": {
- "semver": "^7.0.0"
- }
-@@ -9018,8 +9326,7 @@
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz",
- "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==",
-- "dev": true,
-- "peer": true
-+ "dev": true
- },
- "commander": {
- "version": "2.11.0",
-@@ -9499,6 +9806,7 @@
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
- "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
- "dev": true,
-+ "peer": true,
- "requires": {
- "@esbuild/aix-ppc64": "0.25.0",
- "@esbuild/android-arm": "0.25.0",
-@@ -9576,6 +9884,7 @@
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
- "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
- "dev": true,
-+ "peer": true,
- "requires": {
- "@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.6.1",
-@@ -9652,7 +9961,6 @@
- "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz",
- "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==",
- "dev": true,
-- "peer": true,
- "requires": {
- "semver": "^7.5.4"
- }
-@@ -9735,7 +10043,6 @@
- "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz",
- "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==",
- "dev": true,
-- "peer": true,
- "requires": {
- "@eslint-community/eslint-utils": "^4.1.2",
- "@eslint-community/regexpp": "^4.11.0",
-@@ -9747,6 +10054,7 @@
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
- "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
- "dev": true,
-+ "peer": true,
- "requires": {
- "@rtsao/scc": "^1.1.0",
- "array-includes": "^3.1.8",
-@@ -9864,7 +10172,6 @@
- "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz",
- "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==",
- "dev": true,
-- "peer": true,
- "requires": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "builtins": "^5.0.1",
-@@ -9884,7 +10191,6 @@
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
- "dev": true,
-- "peer": true,
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
-@@ -9895,7 +10201,6 @@
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
-- "peer": true,
- "requires": {
- "brace-expansion": "^1.1.7"
- }
-@@ -9948,6 +10253,7 @@
- "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz",
- "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==",
- "dev": true,
-+ "peer": true,
- "requires": {}
- },
- "eslint-plugin-react": {
-@@ -9955,6 +10261,7 @@
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz",
- "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==",
- "dev": true,
-+ "peer": true,
- "requires": {
- "array-includes": "^3.1.8",
- "array.prototype.findlast": "^1.2.5",
-@@ -10028,6 +10335,7 @@
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
- "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
- "dev": true,
-+ "peer": true,
- "requires": {}
- },
- "eslint-scope": {
-@@ -10253,6 +10561,13 @@
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true
- },
-+ "fsevents": {
-+ "version": "2.3.3",
-+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
-+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-+ "dev": true,
-+ "optional": true
-+ },
- "function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
-@@ -10312,7 +10627,6 @@
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz",
- "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==",
- "dev": true,
-- "peer": true,
- "requires": {
- "resolve-pkg-maps": "^1.0.0"
- }
-@@ -10703,7 +11017,6 @@
- "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
- "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
- "dev": true,
-- "peer": true,
- "requires": {
- "builtin-modules": "^3.3.0"
- }
-@@ -11479,6 +11792,7 @@
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
- "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
- "dev": true,
-+ "peer": true,
- "requires": {
- "nanoid": "^3.3.7",
- "picocolors": "^1.1.0",
-@@ -11516,6 +11830,7 @@
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
- "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
- "dev": true,
-+ "peer": true,
- "requires": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
-@@ -11594,6 +11909,7 @@
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
-+ "peer": true,
- "requires": {
- "loose-envify": "^1.1.0"
- }
-@@ -11602,6 +11918,7 @@
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
-+ "peer": true,
- "requires": {
- "loose-envify": "^1.1.0",
- "scheduler": "^0.23.2"
-@@ -11741,8 +12058,7 @@
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
- "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
-- "dev": true,
-- "peer": true
-+ "dev": true
- },
- "reusify": {
- "version": "1.0.4",
-@@ -11808,7 +12124,6 @@
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
- "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
- "dev": true,
-- "peer": true,
- "requires": {
- "tslib": "^2.1.0"
- }
-@@ -11885,7 +12200,6 @@
- "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.80.6.tgz",
- "integrity": "sha512-Og4aqBnaA3oJfIpHaLuNATAqzBRgUJDYJy2X15V59cot2wYOtiT/ciPnyuq1o7vpDEeOkHhEd+mSviSlXoETug==",
- "dev": true,
-- "peer": true,
- "requires": {
- "@bufbuild/protobuf": "^2.0.0",
- "buffer-builder": "^0.2.0",
-@@ -11921,28 +12235,151 @@
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "dev": true,
-- "peer": true,
- "requires": {
- "has-flag": "^4.0.0"
- }
- }
- }
- },
-+ "sass-embedded-android-arm": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.80.6.tgz",
-+ "integrity": "sha512-UeUKMTRsnz4/dh7IzvhjONxa4/jmVp539CHDd8VZOsqg9M3HcNJNIkUzQWbuwZ+nSlWrTuo7Tvn3XlypopCBzw==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-android-arm64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.80.6.tgz",
-+ "integrity": "sha512-4rC4ZGM/k4ENVjLXnK3JTst8e8FI9MHSol2Fl7dCdYyJ3KLnlt4qL4AEYfU8zq1tcBb7CBOSZVR+CzCKubnXdg==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-android-ia32": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.80.6.tgz",
-+ "integrity": "sha512-Lxz2SXE2KdHnynuHF+D6flDvrd55/zaEAWUeka9MxEr6FmR66d8UBOIy5ETwCSUd//S/SE5Jl6oTnHppgD1zNA==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-android-riscv64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.80.6.tgz",
-+ "integrity": "sha512-hKdxY/oOqB+JJhSoBTDM5DJO1j/xtxQgayh2cLCCUx37IQQe3SEdc3V2JFf/4mIo5peaS4cjqwwSATF+l2zaXg==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-android-x64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.80.6.tgz",
-+ "integrity": "sha512-Eap2Fi3kTx/rVLBsOnOp5RYPr5+lFjTZ652zR24dmYFe9/sDgasakJIOPjOvD2bRuL9z0uWEY1AXVeeOPeZKrg==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-darwin-arm64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.80.6.tgz",
-+ "integrity": "sha512-0mnAx8Vq6Gxj3PQt3imgITfK33hhqrSKpyHSuab71gZZni5opsdtoggq2JawW+1taRFTEZwbZJLKZ0MBDbwCCA==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-darwin-x64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.80.6.tgz",
-+ "integrity": "sha512-Ib20yNZFOrJ7YVT+ltoe+JQNKPcRclM3iLAK69XZZYcSeFM/72SCoQBAaVGIpT23dxDp7FXiE4lO602c3xTRwQ==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-linux-arm": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.80.6.tgz",
-+ "integrity": "sha512-QR0Q6TZox/ThuU2r9c0s3fKCgU2rXAEocpitdgxFp6tta+GsQlMFV3oON2unAa8Bwnuxkmf0YOaK0Oy/TwzkXw==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-linux-arm64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.80.6.tgz",
-+ "integrity": "sha512-n5r98pBXawrQQKaxIYCMM1zDpnngsqxTkOrmvsYLFiAMCSbR0lWf/7sBB33k/Pm0D6dsbp3jpHilCoQNKI3jIw==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-linux-ia32": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.80.6.tgz",
-+ "integrity": "sha512-O6dWZdcOkryRdDCxVMGOeVowgblpDgVcAuRtZ1F1X7XfbpDriTQm64D+9vVZIrywYSPoJfQMJJ662cr0wUs9IQ==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-linux-musl-arm": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.80.6.tgz",
-+ "integrity": "sha512-X9FC8s8fvQGRiXc+eATlZ57N44Iq3nNa0M0ugi3ysdJwkaNYvOeS4QzBHKQAaw3QiTqdxTnLUHHVBkyzdCi9pw==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-linux-musl-arm64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.80.6.tgz",
-+ "integrity": "sha512-VeUSHUi3MAsvOlg9QI4X/2j04h1659aE+7qKP/282CYBTrGkjFGSXZhIki9WKWDgIpDiSInRYXfQQRWhPhjCDg==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-linux-musl-ia32": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.80.6.tgz",
-+ "integrity": "sha512-GqitS2Nab8ah0+wfCqaxW1hnI1piC08FimL6+lM9YWK5DbCOOF82IapbvJOy0feUmd/wNnHmyNTgE9h0zVMFdQ==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-linux-musl-riscv64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.80.6.tgz",
-+ "integrity": "sha512-ySs15z7QSRRQK/aByEEqaJLYW/sTpfynefNPZCtsVNVEzNRwy+DRpxNChtxo+QjKq97ocXETbdG5KLik7QOTJg==",
-+ "dev": true,
-+ "optional": true
-+ },
- "sass-embedded-linux-musl-x64": {
- "version": "1.80.6",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.80.6.tgz",
- "integrity": "sha512-DzeNqU/SN0mWFznoOH4RtVGcrg3Eoa41pUQhKMtrhNbCmIE1zNDunUiAEVTNpdHJF4nxf7ELUPXWmStM31CbUQ==",
- "dev": true,
-- "optional": true,
-- "peer": true
-+ "optional": true
-+ },
-+ "sass-embedded-linux-riscv64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.80.6.tgz",
-+ "integrity": "sha512-AyoHJ3icV9xuJjq1YzJqpEj2XfiC/KBkVYTUrCELKiXP0DN1gi/BpUwZNCAgCM3CyEdMef4LQM/ztCYJxYzdyg==",
-+ "dev": true,
-+ "optional": true
- },
- "sass-embedded-linux-x64": {
- "version": "1.80.6",
- "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.80.6.tgz",
- "integrity": "sha512-EohsE9CEqx0ycylnsEj/0DNPG99Tb0qAVZspiAs5xHFCJjXOFfp3cRQu0BRf+lZ1b72IhPFXymzVtojvzUHb7g==",
- "dev": true,
-- "optional": true,
-- "peer": true
-+ "optional": true
-+ },
-+ "sass-embedded-win32-arm64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.80.6.tgz",
-+ "integrity": "sha512-29wETQi1ykeVvpd4zMVokpQKFSOZskGJzZawuuNCdo7BHjHKIRDsqbz8YT1CewHPBshI0hfD21fenmjxYjGXPQ==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-win32-ia32": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.80.6.tgz",
-+ "integrity": "sha512-1s3OpK2iTIfIL/a91QhAQnffsbuWfnsM8Lx4Fxt0f7ErnxjCV6q8MUFTV/UhcLtLyTFnPCA62DLjp2KGCjMI9A==",
-+ "dev": true,
-+ "optional": true
-+ },
-+ "sass-embedded-win32-x64": {
-+ "version": "1.80.6",
-+ "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.80.6.tgz",
-+ "integrity": "sha512-0pH4Zr9silHkcmLPC0ghnD3DI0vMsjA7dKvGR32/RbbjOSvHV5cDQRLiuVJAPp34dfMA7kJd1ysSchRdH0igAQ==",
-+ "dev": true,
-+ "optional": true
- },
- "scheduler": {
- "version": "0.23.2",
-@@ -12238,6 +12675,7 @@
- "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.9.0.tgz",
- "integrity": "sha512-31Nm3WjxGOBGpQqF43o3wO9L5AC36TPIe6030Lnm13H3vDMTcS21DrLh69bMX+DBilKqMMVLian4iG6ybBoNRQ==",
- "dev": true,
-+ "peer": true,
- "requires": {
- "@csstools/css-parser-algorithms": "^3.0.1",
- "@csstools/css-tokenizer": "^3.0.1",
-@@ -12703,7 +13141,8 @@
- "version": "5.6.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
- "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
-- "dev": true
-+ "dev": true,
-+ "peer": true
- },
- "unbox-primitive": {
- "version": "1.0.2",
-@@ -12746,8 +13185,7 @@
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
- "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
-- "dev": true,
-- "peer": true
-+ "dev": true
- },
- "victory-area": {
- "version": "37.3.1",
---
-2.52.0
-
diff --git a/0039-Issue-7221-CI-tests-fix-some-flaky-tests.patch b/0039-Issue-7221-CI-tests-fix-some-flaky-tests.patch
deleted file mode 100644
index 1c259d9..0000000
--- a/0039-Issue-7221-CI-tests-fix-some-flaky-tests.patch
+++ /dev/null
@@ -1,97 +0,0 @@
-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
-
diff --git a/0040-Issue-7233-test_produce_division_by_zero-fails-with-.patch b/0040-Issue-7233-test_produce_division_by_zero-fails-with-.patch
deleted file mode 100644
index a4ee284..0000000
--- a/0040-Issue-7233-test_produce_division_by_zero-fails-with-.patch
+++ /dev/null
@@ -1,56 +0,0 @@
-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
-
diff --git a/0041-Issue-7241-Drop-dateutil-7242.patch b/0041-Issue-7241-Drop-dateutil-7242.patch
deleted file mode 100644
index 6e2ee65..0000000
--- a/0041-Issue-7241-Drop-dateutil-7242.patch
+++ /dev/null
@@ -1,226 +0,0 @@
-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
-
diff --git a/0042-Issue-7231-Sync-repl-tests-fail-in-FIPS-mode-due-to-.patch b/0042-Issue-7231-Sync-repl-tests-fail-in-FIPS-mode-due-to-.patch
deleted file mode 100644
index 80795d5..0000000
--- a/0042-Issue-7231-Sync-repl-tests-fail-in-FIPS-mode-due-to-.patch
+++ /dev/null
@@ -1,94 +0,0 @@
-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
-
diff --git a/0043-Issue-7248-CLI-attribute-uniqueness-fix-usage-for-ex.patch b/0043-Issue-7248-CLI-attribute-uniqueness-fix-usage-for-ex.patch
deleted file mode 100644
index 413859f..0000000
--- a/0043-Issue-7248-CLI-attribute-uniqueness-fix-usage-for-ex.patch
+++ /dev/null
@@ -1,33 +0,0 @@
-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
-
diff --git a/0044-Issue-CLI-dsctl-db2index-needs-some-hardening-with-M.patch b/0044-Issue-CLI-dsctl-db2index-needs-some-hardening-with-M.patch
deleted file mode 100644
index cda7bbf..0000000
--- a/0044-Issue-CLI-dsctl-db2index-needs-some-hardening-with-M.patch
+++ /dev/null
@@ -1,201 +0,0 @@
-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
-
diff --git a/0045-Issue-7252-PQC-Need-to-iterate-on-SECOidTag-instead-.patch b/0045-Issue-7252-PQC-Need-to-iterate-on-SECOidTag-instead-.patch
deleted file mode 100644
index 241f6b4..0000000
--- a/0045-Issue-7252-PQC-Need-to-iterate-on-SECOidTag-instead-.patch
+++ /dev/null
@@ -1,74 +0,0 @@
-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
-
diff --git a/0046-Issue-6951-Dynamic-Certificas-Refresh-CI-tests-7238.patch b/0046-Issue-6951-Dynamic-Certificas-Refresh-CI-tests-7238.patch
deleted file mode 100644
index e9f72e8..0000000
--- a/0046-Issue-6951-Dynamic-Certificas-Refresh-CI-tests-7238.patch
+++ /dev/null
@@ -1,3031 +0,0 @@
-From 2005e2670c474212a2daa7b3947b41c8db18c9c9 Mon Sep 17 00:00:00 2001
-From: progier389 <progier@redhat.com>
-Date: Fri, 13 Feb 2026 15:34:13 +0100
-Subject: [PATCH] Issue 6951 - Dynamic Certificas Refresh - CI tests (#7238)
-
-* Issue 6951 - Dynamic Certificas Refresh - CI tests
-
-Add CI test for Dynamic Certificas Refresh:
-
-clu/dsconf_dsctl_security_cli_test.py test dsconf/dsctl instance security ... interface
-tls/dynamic_certificates_test.py test the LDAP API
-Fix some issues found while running these tests:
-Fix rpm_is_older function in mldsa_test.py
-Add missing code to handle IP Address in alternate subject name
-FIx test failure related to rehash warning
-
-Issue: #6951
-
-Reviewed by: @jchapma (Thanks!)
-
-Assisted by: Claude AI
----
- .../clu/dsconf_dsctl_security_cli_test.py | 1340 +++++++++++++++
- .../suites/tls/dynamic_certificates_test.py | 1436 +++++++++++++++++
- dirsrvtests/tests/suites/tls/mldsa_test.py | 19 +-
- ldap/servers/slapd/dyncerts.c | 43 +-
- src/lib389/lib389/nss_ssl.py | 15 +-
- src/lib389/lib389/utils.py | 20 +
- 6 files changed, 2848 insertions(+), 25 deletions(-)
- create mode 100644 dirsrvtests/tests/suites/clu/dsconf_dsctl_security_cli_test.py
- create mode 100644 dirsrvtests/tests/suites/tls/dynamic_certificates_test.py
-
-diff --git a/dirsrvtests/tests/suites/clu/dsconf_dsctl_security_cli_test.py b/dirsrvtests/tests/suites/clu/dsconf_dsctl_security_cli_test.py
-new file mode 100644
-index 000000000..318a3cb33
---- /dev/null
-+++ b/dirsrvtests/tests/suites/clu/dsconf_dsctl_security_cli_test.py
-@@ -0,0 +1,1340 @@
-+# --- 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 ---
-+#
-+"""Test dsconf and dsctl security CLI subcommands using subprocess"""
-+
-+import json
-+import logging
-+import os
-+import pytest
-+import subprocess
-+import tempfile
-+from lib389.topologies import topology_st as topo
-+
-+pytestmark = pytest.mark.tier1
-+
-+log = logging.getLogger(__name__)
-+
-+
-+@pytest.fixture(scope="module")
-+def setup_tls(topo):
-+ """Enable TLS on the instance"""
-+ topo.standalone.enable_tls()
-+ yield topo
-+ # Cleanup is handled by the topology fixture
-+
-+
-+def run_cmd(cmd, env=None, check=True, stdin_input=None):
-+ """Helper function to run a command and return output
-+
-+ :param cmd: Command list to run
-+ :param env: Environment variables
-+ :param check: Whether to check return code
-+ :param stdin_input: String to send to stdin
-+ :return: tuple of (returncode, stdout, stderr)
-+ """
-+ log.info(f'Running command: {" ".join(cmd)}')
-+ proc = subprocess.Popen(
-+ cmd,
-+ stdout=subprocess.PIPE,
-+ stderr=subprocess.PIPE,
-+ stdin=subprocess.PIPE if stdin_input else None,
-+ env=env
-+ )
-+ stdin_bytes = stdin_input.encode('utf-8') if stdin_input else None
-+ stdout, stderr = proc.communicate(input=stdin_bytes)
-+ stdout_str = stdout.decode('utf-8') if stdout else ''
-+ stderr_str = stderr.decode('utf-8') if stderr else ''
-+
-+ log.info(f'Return code: {proc.returncode}')
-+ if stdout_str:
-+ log.info(f'STDOUT: {stdout_str}')
-+ if stderr_str:
-+ log.info(f'STDERR: {stderr_str}')
-+
-+ if check and proc.returncode != 0:
-+ raise subprocess.CalledProcessError(
-+ proc.returncode, cmd, stdout, stderr
-+ )
-+
-+ return proc.returncode, stdout_str, stderr_str
-+
-+
-+def test_dsconf_security_get(topo):
-+ """Test dsconf security get command via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000001
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Run dsconf security get command
-+ 2. Verify output contains security attributes
-+ 3. Run with --json flag
-+ 4. Verify JSON output is valid
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. Output contains expected attributes
-+ 3. JSON command succeeds
-+ 4. JSON is valid and parseable
-+ """
-+ inst = topo.standalone
-+ dsconf_cmd = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'get'
-+ ]
-+
-+ # Test plain output
-+ returncode, stdout, stderr = run_cmd(dsconf_cmd)
-+ assert returncode == 0
-+ assert 'nsslapd-security:' in stdout
-+ assert 'nsslapd-secureport:' in stdout or 'nsslapd-securePort:' in stdout.lower()
-+
-+ # Test JSON output
-+ dsconf_cmd_json = ['dsconf', inst.serverid, '--json', 'security', 'get']
-+ returncode, stdout, stderr = run_cmd(dsconf_cmd_json)
-+ assert returncode == 0
-+
-+ # Parse and validate JSON
-+ result = json.loads(stdout)
-+ assert 'type' in result
-+ assert 'items' in result
-+
-+
-+def test_dsconf_security_set(topo):
-+ """Test dsconf security set command via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000002
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Set session timeout using dsconf
-+ 2. Verify setting with get command
-+ 3. Reset to original value
-+ :expectedresults:
-+ 1. Set command succeeds
-+ 2. Value is correctly set
-+ 3. Reset succeeds
-+ """
-+ inst = topo.standalone
-+ dsconf_base = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security'
-+ ]
-+
-+ # Get original value
-+ cmd = ['dsconf', inst.serverid, '--json', 'security', 'get']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ original_data = json.loads(stdout)
-+ original_timeout = original_data.get('items', {}).get('nsslapd-ssl-session-timeout', ['600'])[0]
-+
-+ try:
-+ # Set new value
-+ cmd = dsconf_base + ['set', '--session-timeout', '900']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Verify new value
-+ cmd = dsconf_base + ['get']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert '900' in stdout
-+
-+ finally:
-+ # Reset to original
-+ cmd = dsconf_base + ['set', '--session-timeout', original_timeout]
-+ run_cmd(cmd, check=False)
-+
-+
-+def test_dsconf_security_enable_disable(setup_tls):
-+ """Test dsconf security enable/disable commands via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000003
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Enable security
-+ 2. Verify security is enabled
-+ 3. Disable security
-+ 4. Verify security is disabled
-+ :expectedresults:
-+ 1. Enable command succeeds
-+ 2. Security is on
-+ 3. Disable command succeeds
-+ 4. Security is off
-+ """
-+ inst = setup_tls.standalone
-+ dsconf_base = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security'
-+ ]
-+
-+ # Enable security
-+ cmd = dsconf_base + ['enable']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Verify enabled
-+ cmd = dsconf_base + ['get']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert 'nsslapd-security: on' in stdout
-+
-+ # Disable security
-+ cmd = dsconf_base + ['disable']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Verify disabled
-+ cmd = dsconf_base + ['get']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert 'nsslapd-security: off' in stdout
-+
-+
-+def test_dsconf_security_rsa_get(topo):
-+ """Test dsconf security rsa get command via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000004
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Run dsconf security rsa get command
-+ 2. Verify output contains RSA attributes
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. Output contains RSA activation attribute
-+ """
-+ inst = topo.standalone
-+ cmd = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'rsa',
-+ 'get'
-+ ]
-+
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+ assert 'nssslactivation:' in stdout.lower() or 'nsSSLActivation:' in stdout
-+
-+
-+def test_dsconf_security_rsa_enable_disable(setup_tls):
-+ """Test dsconf security rsa enable/disable via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000005
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Enable RSA
-+ 2. Verify RSA is enabled
-+ 3. Disable RSA
-+ 4. Verify RSA is disabled
-+ :expectedresults:
-+ 1. Enable succeeds
-+ 2. RSA is on
-+ 3. Disable succeeds
-+ 4. RSA is off
-+ """
-+ inst = setup_tls.standalone
-+ dsconf_base = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'rsa'
-+ ]
-+
-+ # Enable RSA
-+ cmd = dsconf_base + ['enable']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Verify enabled
-+ cmd = dsconf_base + ['get']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert 'on' in stdout.lower()
-+
-+ # Disable RSA
-+ cmd = dsconf_base + ['disable']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Verify disabled
-+ cmd = dsconf_base + ['get']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert 'off' in stdout.lower()
-+
-+
-+def test_dsconf_security_ciphers_list(setup_tls):
-+ """Test dsconf security ciphers list command via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000006
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. List all ciphers
-+ 2. List enabled ciphers
-+ 3. List supported ciphers
-+ 4. List disabled ciphers
-+ 5. Test with --json flag
-+ :expectedresults:
-+ 1. Command succeeds and shows ciphers
-+ 2. Command succeeds
-+ 3. Command succeeds
-+ 4. Command succeeds
-+ 5. JSON output is valid
-+ """
-+ inst = setup_tls.standalone
-+ dsconf_base = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'ciphers',
-+ 'list'
-+ ]
-+
-+ # List all ciphers
-+ cmd = dsconf_base.copy()
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # List enabled ciphers
-+ cmd = dsconf_base + ['--enabled']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # List supported ciphers
-+ cmd = dsconf_base + ['--supported']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # List disabled ciphers
-+ cmd = dsconf_base + ['--disabled']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Test JSON output
-+ cmd = ['dsconf', inst.serverid, '--json', 'security', 'ciphers', 'list']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+ # Validate JSON
-+ result = json.loads(stdout)
-+ assert isinstance(result, (dict, list))
-+
-+
-+def test_dsconf_security_ciphers_get(topo):
-+ """Test dsconf security ciphers get command via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000007
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Run dsconf security ciphers get command
-+ 2. Verify output is returned
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. Output contains cipher information
-+ """
-+ inst = topo.standalone
-+ cmd = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'ciphers',
-+ 'get'
-+ ]
-+
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+ # Output should contain something (cipher list or <undefined>)
-+ assert len(stdout) > 0
-+
-+
-+def test_dsconf_security_ciphers_enable_disable(setup_tls):
-+ """Test dsconf security ciphers enable/disable via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000008
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Enable a specific cipher
-+ 2. Disable a specific cipher
-+ :expectedresults:
-+ 1. Enable command succeeds
-+ 2. Disable command succeeds
-+ """
-+ inst = setup_tls.standalone
-+ dsconf_base = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'ciphers'
-+ ]
-+
-+ # Enable a cipher
-+ cmd = dsconf_base + ['enable', 'TLS_AES_128_GCM_SHA256']
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ # May fail if cipher is already enabled or not available, that's ok
-+
-+ # Disable a cipher
-+ cmd = dsconf_base + ['disable', 'TLS_AES_128_GCM_SHA256']
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ # May fail if cipher is already disabled, that's ok
-+
-+
-+def test_dsconf_security_certificate_list(setup_tls):
-+ """Test dsconf security certificate list command via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000009
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. List certificates
-+ 2. Verify Server-Cert is in list
-+ 3. Test with --json flag
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. Server-Cert is present
-+ 3. JSON output is valid
-+ """
-+ inst = setup_tls.standalone
-+ cmd = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'certificate',
-+ 'list'
-+ ]
-+
-+ # Test plain output
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+ assert 'Server-Cert' in stdout
-+
-+ # Test JSON output
-+ cmd_json = ['dsconf', inst.serverid, '--json', 'security', 'certificate', 'list']
-+ returncode, stdout, stderr = run_cmd(cmd_json)
-+ assert returncode == 0
-+ result = json.loads(stdout)
-+ assert isinstance(result, (dict, list))
-+
-+
-+def test_dsconf_security_certificate_get(setup_tls):
-+ """Test dsconf security certificate get command via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000010
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Get Server-Cert details
-+ 2. Verify output contains certificate information
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. Output shows certificate details
-+ """
-+ inst = setup_tls.standalone
-+ cmd = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'certificate',
-+ 'get',
-+ 'Server-Cert'
-+ ]
-+
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+ assert 'Server-Cert' in stdout or 'Certificate Name:' in stdout
-+
-+
-+def test_dsconf_security_certificate_add_delete(setup_tls):
-+ """Test dsconf security certificate add and delete via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000011
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Create a test certificate PEM file
-+ 2. Add certificate via dsconf
-+ 3. Verify certificate was added
-+ 4. Delete certificate
-+ 5. Verify certificate was deleted
-+ :expectedresults:
-+ 1. PEM file created
-+ 2. Add command succeeds
-+ 3. Certificate is in list
-+ 4. Delete command succeeds
-+ 5. Certificate is not in list
-+ """
-+ inst = setup_tls.standalone
-+ dsconf_base = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'certificate'
-+ ]
-+
-+ # Create test certificate
-+ pem_content = """-----BEGIN CERTIFICATE-----
-+MIIFZjCCA06gAwIBAgIFAL18JOowDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMC
-+QVUxEzARBgNVBAgTClF1ZWVuc2xhbmQxDjAMBgNVBAcTBTM4OWRzMRAwDgYDVQQK
-+Ewd0ZXN0aW5nMR8wHQYDVQQDExZzc2NhLjM4OWRzLmV4YW1wbGUuY29tMB4XDTIy
-+MTAyNTE5NDU0M1oXDTI0MTAyNTE5NDU0M1owZTELMAkGA1UEBhMCQVUxEzARBgNV
-+BAgTClF1ZWVuc2xhbmQxDjAMBgNVBAcTBTM4OWRzMRAwDgYDVQQKEwd0ZXN0aW5n
-+MR8wHQYDVQQDExZzc2NhLjM4OWRzLmV4YW1wbGUuY29tMIICIjANBgkqhkiG9w0B
-+AQEFAAOCAg8AMIICCgKCAgEA5E+pd7+8lBsbTKdjHgkSLi2Z5T5G9T+3wziDHhsz
-+F0nG+IOu5yYVkoj/bMxR3sNNlbDLk5ATyNAfytW3cAUZ3NLqm6bmEZdUjD6YycVk
-+AvrfY3zVVE9Debfw6JI3ml8JlC3t8dqn2KT7dmSjvr9zPS95HU+RepjzAqJAKY3B
-+27v0cMetUnxG4pqc7zqnSZJXVP/OXMKSNpujHnK8HyjT8tUJIYQ0YvU2JPJpz3fC
-+BJrmzgO2xYLgLPu6abhP6PQ6uUU+d4j36lG4J/4OiMY0Lr+mnaBAaD3ULPtN5eZh
-+fjQ9d+Sh89xHz92icWhkn8c7IHNEZNtMHNTNJiNbWKuU9HpBWNjWHJoxSxXn4Emr
-+DSfG+lq2UU2m9m+XrDK/7t0W/zC3S+zwcyqM8SJAiZnGEi85058wB0BB1HnnAfFX
-+gel3uZFhnR4d86O/vO5VUqg5Ko795DPzPa3SU4rR36U3nUF7g5WhEAmYNCj683D3
-+DJDPJeCZmis7xtYB5K6Wu6SnFDxBEfhcWSsamWM286KntOiUtqQEzDy4OpZEUsgq
-+s7uqQSl/dfGdY9hCpXMYhlvMfVv3aIoM5zPuXN2cE1QkTnE1pyo8gZqnPLFZnwc9
-+FT+Wjpy0EmsAM/5AIed5h+JgJ304P+wkyjf7APUZyUwf4UJN6aro6N8W23F7dAu5
-+uJ0CAwEAAaMdMBswDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAgQwDQYJKoZIhvcN
-+AQELBQADggIBADFlVdDp1gTF/gl5ysZoo4/4BBSpLx5JDeyPE6BityZ/lwHvBUq3
-+VzmIsU6kxAJfk0p9S7G4LgHIC/DVsLTIE5do6tUdyrawvcaanbYn9ScNoFVQ0GDS
-+C6Ir8PftEvc6bpI4hjkv4goK9fTq5Jtv4LSuRfxFEwoLO+WN8a65IFWlaHJl/Erp
-+9fzP+JKDo8zeh4qnMkaTSCBmLWa8kErrV462RU+qZktf/V4/gWg6k5Vp+82xNk7f
-+9/Mrg9KshNux7A4YCd8LgLEeCgsigi4N6zcfjQB0Rh5u9kXu/hzOjh379ki/vqju
-+i+MTVH97LMB47uR1LEl0VvhWSjID0ePUtbPHCJwOsxWyxBCJY6V7A9nj52uXMGuX
-+xghssZTFvRK6Bb1OiPNYRGqmuymm8rcSFdsY5yemkxJ6kfn40JIRCmVFwqaqu7MC
-+nxyaWAKpRHKM5IyeVZHkFzL9jR/2tVBbjfCAl6YSwM759VcOsw2SGMQKpGIPEBTa
-+1NBdlG45aWJBx5jBdVfOskLjxmBjosByJJHRLtrUBvg66ZBsx1k0c9XjsKmC59JP
-+AzI8zYp/TY/6T5igxM+CSx98DsJFccPBZFFJX+YYRL7DFN38Yb7jMgIUXYHS28Gc
-+1c8kz7ylcQB8lKgCgpcBCH5ZSnLVAnH3uqCygxSTgTo+jgJklKc0xFuR
-+-----END CERTIFICATE-----"""
-+
-+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem') as pem_file:
-+ pem_file.write(pem_content)
-+ pem_path = pem_file.name
-+
-+ try:
-+ # Add certificate
-+ cmd = dsconf_base + ['add', '--file', pem_path, '--name', 'TEST_CLI_CERT']
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ # May fail if cert already exists, continue anyway
-+
-+ # Verify it was added
-+ cmd = dsconf_base + ['list']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ # Certificate should be in list (or may have failed to add)
-+
-+ # Delete certificate
-+ cmd = dsconf_base + ['del', 'TEST_CLI_CERT']
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ # May fail if cert doesn't exist, that's ok
-+
-+ finally:
-+ if os.path.exists(pem_path):
-+ os.unlink(pem_path)
-+
-+
-+def test_dsconf_security_ca_certificate_list(setup_tls):
-+ """Test dsconf security ca-certificate list via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000012
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. List CA certificates
-+ 2. Verify command succeeds
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. Output is returned
-+ """
-+ inst = setup_tls.standalone
-+ cmd = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'ca-certificate',
-+ 'list'
-+ ]
-+
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+
-+def test_dsctl_tls_show_cert(setup_tls):
-+ """Test dsctl tls show-certs command via subprocess
-+
-+ :id: b1b2b3b4-0002-0002-0002-000000000001
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Run dsctl tls show-certs
-+ 2. Verify output shows certificates
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. Output contains certificate information
-+ """
-+ inst = setup_tls.standalone
-+ cmd = [
-+ 'dsctl',
-+ inst.serverid,
-+ 'tls',
-+ 'show-cert',
-+ 'Server-Cert'
-+ ]
-+
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+
-+def test_dsctl_tls_list_client_ca(setup_tls):
-+ """Test dsctl tls list-client-cas command via subprocess
-+
-+ :id: b1b2b3b4-0002-0002-0002-000000000003
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Run dsctl tls list-client-cas
-+ 2. Verify command executes
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. Output is returned
-+ """
-+ inst = setup_tls.standalone
-+ cmd = [
-+ 'dsctl',
-+ inst.serverid,
-+ 'tls',
-+ 'list-client-ca'
-+ ]
-+
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+ assert 'Self-Signed-CA' in stdout
-+
-+
-+def test_dsctl_tls_export_cert(setup_tls):
-+ """Test dsctl tls export-cert command via subprocess
-+
-+ :id: b1b2b3b4-0002-0002-0002-000000000004
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Export Server-Cert to PEM file
-+ 2. Verify file is created
-+ 3. Verify file contains certificate
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. File exists
-+ 3. File contains PEM certificate
-+ """
-+ inst = setup_tls.standalone
-+
-+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem') as export_file:
-+ export_path = export_file.name
-+
-+ try:
-+ cmd = [
-+ 'dsctl',
-+ inst.serverid,
-+ 'tls',
-+ 'export-cert',
-+ '--output-file',
-+ export_path,
-+ 'Server-Cert',
-+ ]
-+
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Verify file exists and has content
-+ assert os.path.exists(export_path)
-+ assert os.path.getsize(export_path) > 0
-+
-+ # Verify it's a PEM certificate
-+ with open(export_path, 'r') as f:
-+ content = f.read()
-+ assert '-----BEGIN CERTIFICATE-----' in content
-+
-+ finally:
-+ if os.path.exists(export_path):
-+ os.unlink(export_path)
-+
-+
-+def test_dsctl_tls_generate_server_cert_csr(topo):
-+ """Test dsctl tls generate-server-cert-csr via subprocess
-+
-+ :id: b1b2b3b4-0002-0002-0002-000000000005
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Generate a CSR with subject
-+ 2. Verify dsctl does not crash
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. Command return code is 0 or 1
-+ """
-+ inst = topo.standalone
-+ cmd = [
-+ 'dsctl',
-+ inst.serverid,
-+ 'tls',
-+ 'generate-server-cert-csr',
-+ '-s', 'CN=test.example.com,O=Example,C=US'
-+ ]
-+
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ # Command may succeed or fail depending on system configuration
-+ # Just verify it doesn't crash
-+ assert returncode in [0, 1] # 0 = success, 1 = expected error
-+
-+
-+def test_dsctl_security_options(topo):
-+ """Test dsctl with various security-related options via subprocess
-+
-+ :id: b1b2b3b4-0002-0002-0002-000000000006
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Run dsctl status command
-+ 2. Verify command works
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. Output shows instance status
-+ """
-+ inst = topo.standalone
-+ cmd = [
-+ 'dsctl',
-+ inst.serverid,
-+ 'status'
-+ ]
-+
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+ assert 'Instance' in stdout or inst.serverid in stdout
-+
-+
-+def test_dsconf_security_disable_plaintext_port(topo):
-+ """Test dsconf security disable_plain_port via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000013
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Disable plaintext port
-+ 2. Verify port is set to 0
-+ 3. Restore original port
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. Port is disabled
-+ 3. Restore succeeds
-+ """
-+ inst = topo.standalone
-+ dsconf_base = [
-+ 'dsconf',
-+ inst.serverid
-+ ]
-+
-+ # Get original port
-+ original_port = inst.port
-+
-+ try:
-+ # Disable plaintext port
-+ cmd = dsconf_base + ['security', 'disable_plain_port']
-+ returncode, stdout, stderr = run_cmd(cmd, stdin_input='Yes I am sure\n')
-+ assert returncode == 0
-+
-+ # Verify port is 0
-+ assert inst.config.get_attr_val_utf8('nsslapd-port') == "0"
-+
-+ finally:
-+ # Restore original port
-+ inst.config.set('nsslapd-port', str(original_port))
-+
-+
-+def test_dsconf_security_certificate_set_trust_flags(setup_tls):
-+ """Test dsconf security certificate set-trust-flags via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000014
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Set trust flags on Self-Signed-CA
-+ 2. Verify command succeeds
-+ :expectedresults:
-+ 1. Command succeeds
-+ 2. No errors returned
-+ """
-+ inst = setup_tls.standalone
-+ cmd = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'certificate',
-+ 'set-trust-flags',
-+ 'Self-Signed-CA',
-+ '--flags',
-+ 'CT,,'
-+ ]
-+
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+
-+def test_dsconf_help_security(topo):
-+ """Test dsconf security help commands via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000015
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Run dsconf security --help
-+ 2. Run dsconf security rsa --help
-+ 3. Run dsconf security ciphers --help
-+ 4. Run dsconf security certificate --help
-+ :expectedresults:
-+ 1. Help text is displayed
-+ 2. RSA help is displayed
-+ 3. Ciphers help is displayed
-+ 4. Certificate help is displayed
-+ """
-+ inst = topo.standalone
-+ dsconf_base = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security'
-+ ]
-+
-+ # Security help
-+ cmd = dsconf_base + ['--help']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+ assert 'usage:' in stdout.lower() or 'security' in stdout.lower()
-+
-+ # RSA help
-+ cmd = dsconf_base + ['rsa', '--help']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Ciphers help
-+ cmd = dsconf_base + ['ciphers', '--help']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Certificate help
-+ cmd = dsconf_base + ['certificate', '--help']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+
-+def test_dsctl_help_tls(topo):
-+ """Test dsctl tls help command via subprocess
-+
-+ :id: b1b2b3b4-0002-0002-0002-000000000007
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Run dsctl tls --help
-+ :expectedresults:
-+ 1. Help text is displayed
-+ """
-+ inst = topo.standalone
-+ cmd = [
-+ 'dsctl',
-+ inst.serverid,
-+ 'tls',
-+ '--help'
-+ ]
-+
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+ assert 'usage:' in stdout.lower() or 'tls' in stdout.lower()
-+
-+
-+def test_dsconf_certificate_invalid_files(setup_tls):
-+ """Test dsconf with invalid/malformed certificate files via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000016
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Try to import corrupted PEM certificate
-+ 2. Try to import truncated certificate
-+ 3. Try to import invalid PKCS#12 file
-+ 4. Verify all commands fail with appropriate errors
-+ :expectedresults:
-+ 1. Corrupted PEM import fails
-+ 2. Truncated cert import fails
-+ 3. Invalid PKCS#12 import fails
-+ 4. Error messages are meaningful (not just error codes)
-+ """
-+ inst = setup_tls.standalone
-+ dsconf_base = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'certificate'
-+ ]
-+
-+ # Test 1: Corrupted PEM certificate
-+ corrupted_pem = """-----BEGIN CERTIFICATE-----
-+CORRUPTED DATA HERE NOT VALID BASE64!@#$%^&*
-+THIS IS NOT A REAL CERTIFICATE
-+-----END CERTIFICATE-----"""
-+
-+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem') as f:
-+ f.write(corrupted_pem)
-+ corrupted_pem_path = f.name
-+
-+ try:
-+ cmd = dsconf_base + ['add', '--file', corrupted_pem_path, '--name', 'CORRUPTED_CERT']
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ assert returncode != 0
-+ # Should have error text, not just error code
-+ assert '255' not in stderr or 'base64' in stderr.lower() or 'invalid' in stderr.lower()
-+ finally:
-+ if os.path.exists(corrupted_pem_path):
-+ os.unlink(corrupted_pem_path)
-+
-+ # Test 2: Truncated certificate (incomplete PEM)
-+ truncated_pem = """-----BEGIN CERTIFICATE-----
-+MIIFZjCCA06gAwIBAgIFAL18JOowDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMC
-+QVUxEzARBgNVBAgTClF1ZWVuc2xhbmQxDjAMBgNVBAcTBTM4OWRzMRAwDgYDVQQK
-+""" # Intentionally truncated, no END CERTIFICATE
-+
-+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem') as f:
-+ f.write(truncated_pem)
-+ truncated_pem_path = f.name
-+
-+ try:
-+ cmd = dsconf_base + ['add', '--file', truncated_pem_path, '--name', 'TRUNCATED_CERT']
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ assert returncode != 0
-+ finally:
-+ if os.path.exists(truncated_pem_path):
-+ os.unlink(truncated_pem_path)
-+
-+ # Test 3: Invalid PKCS#12 file (just random data)
-+ with tempfile.NamedTemporaryFile(mode='wb', delete=False, suffix='.p12') as f:
-+ f.write(b'NOT A VALID PKCS12 FILE')
-+ invalid_p12_path = f.name
-+
-+ try:
-+ cmd = dsconf_base + ['add', '--file', invalid_p12_path, '--name', 'INVALID_P12']
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ assert returncode != 0
-+ finally:
-+ if os.path.exists(invalid_p12_path):
-+ os.unlink(invalid_p12_path)
-+
-+
-+def test_dsconf_certificate_overwrite_scenarios(setup_tls):
-+ """Test certificate overwrite with and without --force flag via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000017
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Add a test certificate
-+ 2. Try to add same certificate without --force (should fail)
-+ 3. Add same certificate with --force (should succeed)
-+ 4. Clean up
-+ :expectedresults:
-+ 1. Initial add succeeds
-+ 2. Duplicate add without --force fails
-+ 3. Duplicate add with --force succeeds
-+ 4. Cleanup succeeds
-+ """
-+ inst = setup_tls.standalone
-+ dsconf_base = [
-+ 'dsconf',
-+ inst.serverid,
-+ 'security',
-+ 'certificate'
-+ ]
-+
-+ # Valid test certificate
-+ pem_content = """-----BEGIN CERTIFICATE-----
-+MIIFZjCCA06gAwIBAgIFAL18JOowDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMC
-+QVUxEzARBgNVBAgTClF1ZWVuc2xhbmQxDjAMBgNVBAcTBTM4OWRzMRAwDgYDVQQK
-+Ewd0ZXN0aW5nMR8wHQYDVQQDExZzc2NhLjM4OWRzLmV4YW1wbGUuY29tMB4XDTIy
-+MTAyNTE5NDU0M1oXDTI0MTAyNTE5NDU0M1owZTELMAkGA1UEBhMCQVUxEzARBgNV
-+BAgTClF1ZWVuc2xhbmQxDjAMBgNVBAcTBTM4OWRzMRAwDgYDVQQKEwd0ZXN0aW5n
-+MR8wHQYDVQQDExZzc2NhLjM4OWRzLmV4YW1wbGUuY29tMIICIjANBgkqhkiG9w0B
-+AQEFAAOCAg8AMIICCgKCAgEA5E+pd7+8lBsbTKdjHgkSLi2Z5T5G9T+3wziDHhsz
-+F0nG+IOu5yYVkoj/bMxR3sNNlbDLk5ATyNAfytW3cAUZ3NLqm6bmEZdUjD6YycVk
-+AvrfY3zVVE9Debfw6JI3ml8JlC3t8dqn2KT7dmSjvr9zPS95HU+RepjzAqJAKY3B
-+27v0cMetUnxG4pqc7zqnSZJXVP/OXMKSNpujHnK8HyjT8tUJIYQ0YvU2JPJpz3fC
-+BJrmzgO2xYLgLPu6abhP6PQ6uUU+d4j36lG4J/4OiMY0Lr+mnaBAaD3ULPtN5eZh
-+fjQ9d+Sh89xHz92icWhkn8c7IHNEZNtMHNTNJiNbWKuU9HpBWNjWHJoxSxXn4Emr
-+DSfG+lq2UU2m9m+XrDK/7t0W/zC3S+zwcyqM8SJAiZnGEi85058wB0BB1HnnAfFX
-+gel3uZFhnR4d86O/vO5VUqg5Ko795DPzPa3SU4rR36U3nUF7g5WhEAmYNCj683D3
-+DJDPJeCZmis7xtYB5K6Wu6SnFDxBEfhcWSsamWM286KntOiUtqQEzDy4OpZEUsgq
-+s7uqQSl/dfGdY9hCpXMYhlvMfVv3aIoM5zPuXN2cE1QkTnE1pyo8gZqnPLFZnwc9
-+FT+Wjpy0EmsAM/5AIed5h+JgJ304P+wkyjf7APUZyUwf4UJN6aro6N8W23F7dAu5
-+uJ0CAwEAAaMdMBswDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAgQwDQYJKoZIhvcN
-+AQELBQADggIBADFlVdDp1gTF/gl5ysZoo4/4BBSpLx5JDeyPE6BityZ/lwHvBUq3
-+VzmIsU6kxAJfk0p9S7G4LgHIC/DVsLTIE5do6tUdyrawvcaanbYn9ScNoFVQ0GDS
-+C6Ir8PftEvc6bpI4hjkv4goK9fTq5Jtv4LSuRfxFEwoLO+WN8a65IFWlaHJl/Erp
-+9fzP+JKDo8zeh4qnMkaTSCBmLWa8kErrV462RU+qZktf/V4/gWg6k5Vp+82xNk7f
-+9/Mrg9KshNux7A4YCd8LgLEeCgsigi4N6zcfjQB0Rh5u9kXu/hzOjh379ki/vqju
-+i+MTVH97LMB47uR1LEl0VvhWSjID0ePUtbPHCJwOsxWyxBCJY6V7A9nj52uXMGuX
-+xghssZTFvRK6Bb1OiPNYRGqmuymm8rcSFdsY5yemkxJ6kfn40JIRCmVFwqaqu7MC
-+nxyaWAKpRHKM5IyeVZHkFzL9jR/2tVBbjfCAl6YSwM759VcOsw2SGMQKpGIPEBTa
-+1NBdlG45aWJBx5jBdVfOskLjxmBjosByJJHRLtrUBvg66ZBsx1k0c9XjsKmC59JP
-+AzI8zYp/TY/6T5igxM+CSx98DsJFccPBZFFJX+YYRL7DFN38Yb7jMgIUXYHS28Gc
-+1c8kz7ylcQB8lKgCgpcBCH5ZSnLVAnH3uqCygxSTgTo+jgJklKc0xFuR
-+-----END CERTIFICATE-----"""
-+
-+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem') as f:
-+ f.write(pem_content)
-+ pem_path = f.name
-+
-+ cert_name = 'TEST_OVERWRITE_CERT'
-+
-+ try:
-+ # Initial add
-+ cmd = dsconf_base + ['add', '--file', pem_path, '--name', cert_name]
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+
-+ if returncode == 0:
-+ # Try to add again without --force (should fail)
-+ cmd = dsconf_base + ['add', '--file', pem_path, '--name', cert_name]
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ # Should fail because cert already exists
-+ assert returncode != 0 or 'already exists' in stderr.lower()
-+
-+ # Try to add with --force flag (if supported)
-+ cmd = dsconf_base + ['add', '--file', pem_path, '--name', cert_name, '--force']
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ # Command may succeed with force or may not support --force flag
-+
-+ finally:
-+ # Cleanup
-+ cmd = dsconf_base + ['del', cert_name]
-+ run_cmd(cmd, check=False)
-+ if os.path.exists(pem_path):
-+ os.unlink(pem_path)
-+
-+
-+def test_dsconf_json_text_output_consistency(setup_tls):
-+ """Test JSON vs text output consistency for list/get commands via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000018
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Get security config in text and JSON format
-+ 2. Compare that essential data is in both
-+ 3. List certificates in text and JSON format
-+ 4. Compare that certificates appear in both
-+ 5. Get certificate details in text and JSON format
-+ 6. Verify consistency
-+ :expectedresults:
-+ 1. Both formats return data
-+ 2. Essential security attributes appear in both
-+ 3. Both formats return certificates
-+ 4. Same certificates appear in both
-+ 5. Both formats return details
-+ 6. Data is consistent between formats
-+ """
-+ inst = setup_tls.standalone
-+
-+ # Test 1: Security get - text vs JSON
-+ cmd_text = ['dsconf', inst.serverid, 'security', 'get']
-+ returncode, stdout_text, stderr = run_cmd(cmd_text)
-+ assert returncode == 0
-+
-+ cmd_json = ['dsconf', inst.serverid, '--json', 'security', 'get']
-+ returncode, stdout_json, stderr = run_cmd(cmd_json)
-+ assert returncode == 0
-+ json_data = json.loads(stdout_json)
-+ assert 'type' in json_data or 'items' in json_data or 'attrs' in json_data
-+
-+ # Test 2: Certificate list - text vs JSON
-+ cmd_text = ['dsconf', inst.serverid, 'security', 'certificate', 'list']
-+ returncode, stdout_text, stderr = run_cmd(cmd_text)
-+ assert returncode == 0
-+
-+ cmd_json = ['dsconf', inst.serverid, '--json', 'security', 'certificate', 'list']
-+ returncode, stdout_json, stderr = run_cmd(cmd_json)
-+ assert returncode == 0
-+ json_data = json.loads(stdout_json)
-+ assert isinstance(json_data, (dict, list))
-+
-+ # Verify Server-Cert appears in both
-+ assert 'Server-Cert' in stdout_text
-+ json_str = json.dumps(json_data)
-+ assert 'Server-Cert' in json_str
-+
-+ # Test 3: Certificate get - text vs JSON
-+ cmd_text = ['dsconf', inst.serverid, 'security', 'certificate', 'get', 'Server-Cert']
-+ returncode, stdout_text, stderr = run_cmd(cmd_text)
-+ assert returncode == 0
-+
-+ cmd_json = ['dsconf', inst.serverid, '--json', 'security', 'certificate', 'get', 'Server-Cert']
-+ returncode, stdout_json, stderr = run_cmd(cmd_json, check=False)
-+ # JSON format may not be supported for this command, check if it succeeds
-+ if returncode == 0:
-+ json_data = json.loads(stdout_json)
-+ assert isinstance(json_data, dict)
-+
-+
-+def test_dsctl_tls_error_messages(topo):
-+ """Test that dsctl tls commands return meaningful error messages via subprocess
-+
-+ :id: b1b2b3b4-0002-0002-0002-000000000008
-+ :setup: Standalone Instance
-+ :steps:
-+ 1. Try to generate CSR with malformed subject
-+ 2. Try to remove non-existent certificate
-+ 3. Try to export non-existent certificate
-+ 4. Verify error messages are meaningful
-+ :expectedresults:
-+ 1. CSR generation fails with meaningful error
-+ 2. Remove fails with meaningful error
-+ 3. Export fails with meaningful error
-+ 4. No bare error codes like '255' in output
-+ """
-+ inst = topo.standalone
-+
-+ # Test 1: Malformed subject in CSR generation
-+ cmd = [
-+ 'dsctl',
-+ inst.serverid,
-+ 'tls',
-+ 'generate-server-cert-csr',
-+ '-s', 'INVALID_SUBJECT'
-+ ]
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ assert returncode != 0
-+ # Should have error text, not just '255'
-+ combined_output = stdout + stderr
-+ if '255' in combined_output:
-+ assert 'improperly formatted' in combined_output.lower() or 'invalid' in combined_output.lower()
-+
-+ # Test 2: Remove non-existent certificate
-+ cmd = [
-+ 'dsctl',
-+ inst.serverid,
-+ 'tls',
-+ 'remove-cert',
-+ 'NONEXISTENT_CERT_12345'
-+ ]
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ assert returncode != 0
-+ combined_output = stdout + stderr
-+ if '255' in combined_output:
-+ assert 'not found' in combined_output.lower() or 'could not find' in combined_output.lower()
-+
-+ # Test 3: Export non-existent certificate
-+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.pem') as f:
-+ export_path = f.name
-+
-+ try:
-+ cmd = [
-+ 'dsctl',
-+ inst.serverid,
-+ 'tls',
-+ 'export-cert',
-+ '--output-file',
-+ export_path,
-+ 'NONEXISTENT_CERT_12345'
-+ ]
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ assert returncode != 0
-+ combined_output = stdout + stderr
-+ if '255' in combined_output:
-+ assert 'not found' in combined_output.lower() or 'could not find' in combined_output.lower()
-+ finally:
-+ if os.path.exists(export_path):
-+ os.unlink(export_path)
-+
-+
-+def test_dsconf_certificate_backend_consistency(setup_tls):
-+ """Test backend consistency between DynamicCerts and NssSsl via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000019
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. List certificates using dsconf
-+ 2. Verify Server-Cert appears (from NssSsl backend)
-+ 3. Check if dynamic certificates backend is accessible
-+ 4. Verify consistency of certificate data
-+ :expectedresults:
-+ 1. Certificate list succeeds
-+ 2. Server-Cert is present
-+ 3. Backend is accessible
-+ 4. Data is consistent
-+ """
-+ inst = setup_tls.standalone
-+
-+ # List certificates
-+ cmd = ['dsconf', inst.serverid, 'security', 'certificate', 'list']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+ assert 'Server-Cert' in stdout
-+
-+ # Get certificate details
-+ cmd = ['dsconf', inst.serverid, 'security', 'certificate', 'get', 'Server-Cert']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Verify we get meaningful certificate information
-+ assert 'Server-Cert' in stdout or 'Certificate' in stdout
-+
-+ # List CA certificates
-+ cmd = ['dsconf', inst.serverid, 'security', 'ca-certificate', 'list']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # The output should be consistent whether accessed via NssSsl or DynamicCerts backend
-+
-+
-+def test_dsctl_tls_pkcs12_password_scenarios(setup_tls):
-+ """Test PKCS#12 import with various password scenarios via subprocess
-+
-+ :id: b1b2b3b4-0002-0002-0002-000000000009
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Try to import PKCS#12 with wrong password
-+ 2. Try to import PKCS#12 with missing password
-+ 3. Try to import PKCS#12 with empty password when one is required
-+ 4. Verify appropriate error messages
-+ :expectedresults:
-+ 1. Wrong password fails with meaningful error
-+ 2. Missing password fails with meaningful error
-+ 3. Empty password fails with meaningful error
-+ 4. Error messages indicate password issues, not bare error codes
-+ """
-+ inst = setup_tls.standalone
-+
-+ # Create a dummy PKCS#12-like file (not a real one, just for testing error handling)
-+ with tempfile.NamedTemporaryFile(mode='wb', delete=False, suffix='.p12') as f:
-+ # Write some bytes that look like PKCS#12 header but aren't valid
-+ f.write(b'\x30\x82') # ASN.1 SEQUENCE tag
-+ f.write(b'NOT_A_VALID_PKCS12_FILE')
-+ p12_path = f.name
-+
-+ try:
-+ # Test 1: Try import with wrong password
-+ cmd = [
-+ 'dsctl',
-+ inst.serverid,
-+ 'tls',
-+ 'import-server-key-cert',
-+ p12_path,
-+ p12_path # Using same file as both key and cert for testing
-+ ]
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+ assert returncode != 0
-+ combined_output = stdout + stderr
-+ # Should get error about private key or file format, not bare '255'
-+ if '255' in combined_output:
-+ assert ('private key' in combined_output.lower() or
-+ 'password' in combined_output.lower() or
-+ 'pkcs' in combined_output.lower() or
-+ 'invalid' in combined_output.lower())
-+
-+ finally:
-+ if os.path.exists(p12_path):
-+ os.unlink(p12_path)
-+
-+
-+def test_dsconf_dynamic_certificates_attributes(setup_tls):
-+ """Test dynamic certificates attributes and timestamp handling via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000020
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. List certificates in JSON format
-+ 2. Check for dynamic certificate attributes
-+ 3. Verify timestamp format if present
-+ 4. Check certificate metadata (subject, issuer, etc.)
-+ :expectedresults:
-+ 1. Certificate list returns valid JSON
-+ 2. Expected attributes are present
-+ 3. Timestamps are properly formatted
-+ 4. Metadata is accessible and valid
-+ """
-+ inst = setup_tls.standalone
-+
-+ # Get certificate list in JSON format
-+ cmd = ['dsconf', inst.serverid, '--json', 'security', 'certificate', 'list']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ json_data = json.loads(stdout)
-+ assert isinstance(json_data, (dict, list))
-+
-+ # Get details of a specific certificate in JSON if supported
-+ cmd = ['dsconf', inst.serverid, '--json', 'security', 'certificate', 'get', 'Server-Cert']
-+ returncode, stdout, stderr = run_cmd(cmd, check=False)
-+
-+ if returncode == 0:
-+ json_data = json.loads(stdout)
-+
-+ # Check for common certificate attributes
-+ # The exact structure depends on whether DynamicCerts or NssSsl backend is used
-+ json_str = json.dumps(json_data).lower()
-+
-+ # Look for certificate-related attributes (case-insensitive)
-+ # These might be in different formats depending on the backend
-+ assert ('subject' in json_str or
-+ 'issuer' in json_str or
-+ 'cert' in json_str or
-+ 'nickname' in json_str)
-+
-+
-+def test_dsconf_security_multiple_operations(setup_tls):
-+ """Test multiple security operations in sequence via subprocess
-+
-+ :id: a1a2a3a4-0001-0001-0001-000000000021
-+ :setup: Standalone Instance with TLS
-+ :steps:
-+ 1. Disable security
-+ 2. Enable security
-+ 3. Disable RSA
-+ 4. Enable RSA
-+ 5. Get security config after each change
-+ 6. Verify state changes are reflected
-+ :expectedresults:
-+ 1. Disable succeeds
-+ 2. Enable succeeds
-+ 3. RSA disable succeeds
-+ 4. RSA enable succeeds
-+ 5. Each get returns current state
-+ 6. Changes are properly reflected
-+ """
-+ inst = setup_tls.standalone
-+ dsconf_base = ['dsconf', inst.serverid, 'security']
-+
-+ # Enable security
-+ cmd = dsconf_base + ['enable']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Verify enabled
-+ cmd = dsconf_base + ['get']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert 'nsslapd-security: on' in stdout
-+
-+ # Disable security
-+ cmd = dsconf_base + ['disable']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Verify disabled
-+ cmd = dsconf_base + ['get']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert 'nsslapd-security: off' in stdout
-+
-+ # Re-enable for RSA tests
-+ cmd = dsconf_base + ['enable']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Enable RSA
-+ cmd = dsconf_base + ['rsa', 'enable']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Verify RSA is on
-+ cmd = dsconf_base + ['rsa', 'get']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert 'on' in stdout.lower()
-+
-+ # Disable RSA
-+ cmd = dsconf_base + ['rsa', 'disable']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert returncode == 0
-+
-+ # Verify RSA is off
-+ cmd = dsconf_base + ['rsa', 'get']
-+ returncode, stdout, stderr = run_cmd(cmd)
-+ assert 'off' in stdout.lower()
-+
-+
-+if __name__ == '__main__':
-+ # Run isolated
-+ # -s for DEBUG mode
-+ CURRENT_FILE = os.path.realpath(__file__)
-+ pytest.main(["-s", "-v", CURRENT_FILE])
-diff --git a/dirsrvtests/tests/suites/tls/dynamic_certificates_test.py b/dirsrvtests/tests/suites/tls/dynamic_certificates_test.py
-new file mode 100644
-index 000000000..9cfacefc2
---- /dev/null
-+++ b/dirsrvtests/tests/suites/tls/dynamic_certificates_test.py
-@@ -0,0 +1,1436 @@
-+# --- 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 ---
-+#
-+"""
-+Comprehensive test suite for Dynamic Certificates feature
-+Tests certificate metadata extraction, trust flags, multi-token support,
-+verification, and error handling using lib389 DynamicCerts classes.
-+"""
-+
-+import os
-+import sys
-+import base64
-+import copy
-+import datetime
-+import ipaddress
-+import ldap
-+import ldapurl
-+import logging
-+import pytest
-+import secrets
-+import shutil
-+import socket
-+import subprocess
-+import textwrap
-+import time
-+from contextlib import contextmanager, suppress
-+from cryptography.hazmat.backends import default_backend
-+from cryptography.hazmat.primitives.asymmetric import ec, rsa
-+from cryptography.hazmat.primitives import hashes, serialization
-+from cryptography.hazmat.primitives.serialization import pkcs12
-+from cryptography import x509
-+from cryptography.x509.oid import NameOID, ExtensionOID
-+from lib389.cli_base import FakeArgs
-+from lib389._constants import DN_DM, PW_DM
-+from lib389.dseldif import DSEldif
-+from lib389.topologies import topology_st as topo
-+from lib389.utils import ds_is_older, ensure_str, pem_to_der, rpm_is_older
-+from lib389.dyncerts import (
-+ DynamicCerts, DynamicCert, DYNCERT_SUFFIX,
-+ DYCATTR_CN, DYCATTR_CERTDER, DYCATTR_PKEYDER )
-+from tempfile import NamedTemporaryFile
-+
-+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__)
-+
-+tls_enabled = False
-+
-+
-+def utcdate():
-+ return datetime.datetime.utcnow()
-+
-+def fix_crash_issue_7227(inst):
-+ """Work around to avoid ns-slapd crash in CERT_VerifyCertificateNow()"""
-+ inst.restart()
-+
-+
-+class RSA_Certificate:
-+ """RSA Certificate Generator for testing RSA key support"""
-+
-+ PKCS12_PASSWORD = "rsa+password"
-+
-+ def __init__(self):
-+ self.nickname = None
-+ self.trust = None
-+ self.namingAttrs = {}
-+ self.subject = None
-+ self.validity_days = 365
-+ self.isCA = False
-+ self.isRoot = False
-+
-+ @staticmethod
-+ def generateRootCA(nickname, namingAttributes={}, validity_days=3650):
-+ """Generate a self-signed RSA Root CA certificate"""
-+ ca = RSA_Certificate()
-+ ca.namingAttrs = namingAttributes
-+ ca.validity_days = validity_days
-+ ca.isCA = True
-+ ca.isRoot = True
-+ ca.nickname = nickname
-+ ca.fixNamingAttributes()
-+ ca.subject = x509.Name([x509.NameAttribute(k, v) for k, v in ca.namingAttrs.items()])
-+ ca.issuer = ca.subject
-+ ca.trust = 'CT,,'
-+
-+ # Generate RSA 2048-bit private key
-+ ca.pkey = rsa.generate_private_key(
-+ public_exponent=65537,
-+ key_size=2048,
-+ backend=default_backend()
-+ )
-+ ca.cert = (
-+ x509.CertificateBuilder()
-+ .subject_name(ca.subject)
-+ .issuer_name(ca.issuer)
-+ .public_key(ca.pkey.public_key())
-+ .serial_number(x509.random_serial_number())
-+ .not_valid_before(utcdate())
-+ .not_valid_after(utcdate() + datetime.timedelta(days=validity_days))
-+ .add_extension(
-+ x509.BasicConstraints(ca=True, path_length=0),
-+ critical=True,
-+ )
-+ .add_extension(
-+ x509.KeyUsage(
-+ digital_signature=True,
-+ key_cert_sign=True,
-+ crl_sign=True,
-+ key_encipherment=False,
-+ content_commitment=False,
-+ data_encipherment=False,
-+ key_agreement=False,
-+ encipher_only=False,
-+ decipher_only=False,
-+ ),
-+ critical=True,
-+ )
-+ .add_extension(
-+ x509.SubjectKeyIdentifier.from_public_key(ca.pkey.public_key()),
-+ critical=False,
-+ )
-+ .sign(ca.pkey, hashes.SHA256(), default_backend())
-+ )
-+ return ca
-+
-+ def fixNamingAttribute(self, name, vdef):
-+ if name not in self.namingAttrs:
-+ self.namingAttrs[name] = vdef
-+
-+ def fixNamingAttributes(self):
-+ self.fixNamingAttribute(NameOID.COMMON_NAME, self.nickname)
-+ self.fixNamingAttribute(NameOID.COUNTRY_NAME, 'US')
-+ self.fixNamingAttribute(NameOID.ORGANIZATION_NAME, 'Test Organization')
-+
-+ def generateCertificate(self, nickname, namingAttributes={}, hostname=None, validity_days=365):
-+ """Generate an RSA server certificate"""
-+ cert = RSA_Certificate()
-+ cert.namingAttrs = namingAttributes
-+ cert.validity_days = validity_days
-+ cert.isCA = False
-+ cert.isRoot = False
-+ cert.nickname = nickname
-+ cert.fixNamingAttributes()
-+ cert.subject = x509.Name([x509.NameAttribute(k, v) for k, v in cert.namingAttrs.items()])
-+ cert.issuer = self.subject
-+ cert.trust = 'u,u,u'
-+
-+ if hostname is None:
-+ hostname = socket.gethostname()
-+
-+ san_list = [
-+ x509.DNSName(hostname),
-+ x509.DNSName('localhost'),
-+ x509.IPAddress(ipaddress.ip_address("127.0.0.1")),
-+ x509.IPAddress(ipaddress.ip_address("::1"))
-+ ]
-+
-+ cert.pkey = rsa.generate_private_key(
-+ public_exponent=65537,
-+ key_size=2048,
-+ backend=default_backend()
-+ )
-+ cert.cert = (
-+ x509.CertificateBuilder()
-+ .subject_name(cert.subject)
-+ .issuer_name(cert.issuer)
-+ .public_key(cert.pkey.public_key())
-+ .serial_number(x509.random_serial_number())
-+ .not_valid_before(utcdate())
-+ .not_valid_after(utcdate() + datetime.timedelta(days=validity_days))
-+ .add_extension(
-+ x509.BasicConstraints(ca=False, path_length=None),
-+ critical=True,
-+ )
-+ .add_extension(
-+ x509.KeyUsage(
-+ digital_signature=True,
-+ key_encipherment=True,
-+ key_cert_sign=False,
-+ crl_sign=False,
-+ content_commitment=False,
-+ data_encipherment=False,
-+ key_agreement=False,
-+ encipher_only=False,
-+ decipher_only=False,
-+ ),
-+ critical=True,
-+ )
-+ .add_extension(
-+ x509.ExtendedKeyUsage([
-+ x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
-+ x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH,
-+ ]),
-+ critical=True,
-+ )
-+ .add_extension(
-+ x509.SubjectAlternativeName(san_list),
-+ critical=False,
-+ )
-+ .add_extension(
-+ x509.SubjectKeyIdentifier.from_public_key(cert.pkey.public_key()),
-+ critical=False,
-+ )
-+ .add_extension(
-+ x509.AuthorityKeyIdentifier.from_issuer_public_key(self.pkey.public_key()),
-+ critical=False,
-+ )
-+ .sign(self.pkey, hashes.SHA256(), default_backend())
-+ )
-+ return cert
-+
-+ def save_pem_file(self, filename):
-+ """Save certificate in PEM format"""
-+ with open(filename, "wb") as f:
-+ f.write(self.cert.public_bytes(serialization.Encoding.PEM))
-+
-+ def save_der_file(self, filename):
-+ """Save certificate in DER format"""
-+ with open(filename, "wb") as f:
-+ f.write(self.cert.public_bytes(serialization.Encoding.DER))
-+
-+ def save_key_der(self, filename):
-+ """Save private key in PKCS8 DER format"""
-+ with open(filename, "wb") as f:
-+ f.write(self.pkey.private_bytes(
-+ encoding=serialization.Encoding.DER,
-+ format=serialization.PrivateFormat.PKCS8,
-+ encryption_algorithm=serialization.NoEncryption()
-+ ))
-+
-+ def write_pkcs12_file(self, filename, pw=PKCS12_PASSWORD):
-+ """Save PKCS12 formatted certificate and private key"""
-+ if isinstance(pw, str):
-+ pw = pw.encode()
-+ if pw is None or pw == b"":
-+ enc = serialization.NoEncryption()
-+ else:
-+ enc = serialization.BestAvailableEncryption(pw)
-+ with open(filename, "wb") as f:
-+ f.write(pkcs12.serialize_key_and_certificates(
-+ self.nickname.encode(),
-+ self.pkey,
-+ self.cert,
-+ [],
-+ enc
-+ ))
-+
-+ def save(self, dirname):
-+ """Save all certificate formats"""
-+ name = f'{dirname}/{self.nickname}'
-+ self.pem = f"{name}.pem"
-+ self.der = f"{name}.der"
-+ self.kder = f"{name}-key.der"
-+ self.p12 = f"{name}.p12"
-+ self.save_pem_file(self.pem)
-+ self.save_der_file(self.der)
-+ self.save_key_der(self.kder)
-+ self.write_pkcs12_file(self.p12)
-+
-+
-+@pytest.fixture(scope="module")
-+def setup_tls(topo):
-+ """Enable TLS on the instance"""
-+ global tls_enabled
-+ inst = topo.standalone
-+ if not tls_enabled:
-+ inst.enable_tls()
-+ tls_enabled = True
-+ return inst
-+
-+
-+def test_certificate_metadata_extraction(topo, setup_tls):
-+ """Test that certificate metadata is correctly extracted and exposed
-+
-+ :id: a1b2c3d4-1111-2222-3333-444444444444
-+ :setup: Standalone Instance with TLS enabled
-+ :steps:
-+ 1. Generate RSA Root CA certificate
-+ 2. Add certificate dynamically using DynamicCerts
-+ 3. Retrieve certificate object and verify all metadata attributes
-+ 4. Verify subject DN is correct
-+ 5. Verify issuer DN is correct
-+ 6. Verify isCA flag is TRUE
-+ 7. Verify isRootCA flag is TRUE
-+ 8. Verify key algorithm is RSA
-+ 9. Verify trust flags are present
-+ 10. Verify validity dates (notBefore, notAfter)
-+ 11. Verify serial number is present
-+ :expectedresults:
-+ 1. Certificate generated successfully
-+ 2. Certificate added without error
-+ 3. Certificate object retrieved
-+ 4. Subject matches expected DN
-+ 5. Issuer matches subject (self-signed)
-+ 6. isCA is TRUE
-+ 7. isRootCA is TRUE
-+ 8. Key algorithm is RSA
-+ 9. Trust flags are present
-+ 10. Validity dates are within expected range
-+ 11. Serial number is present and non-empty
-+ """
-+ inst = setup_tls
-+ dir = '/tmp/dyncert_metadata_test'
-+ os.makedirs(dir, 0o700, exist_ok=True)
-+
-+ try:
-+ # Generate RSA Root CA
-+ ca = RSA_Certificate.generateRootCA(
-+ "TestMetadataCA",
-+ namingAttributes={
-+ NameOID.COMMON_NAME: "Test Metadata CA",
-+ NameOID.COUNTRY_NAME: "US",
-+ NameOID.ORGANIZATION_NAME: "Test Org",
-+ NameOID.ORGANIZATIONAL_UNIT_NAME: "Testing Unit"
-+ }
-+ )
-+ ca.save(dir)
-+
-+ # Add certificate using DynamicCerts
-+ dyncerts = DynamicCerts(inst)
-+ dyncerts.add_cert(ca.pem, ca.nickname, ca=True)
-+
-+ # Get certificate object
-+ cert_obj = dyncerts.get_cert_obj(ca.nickname)
-+ assert cert_obj is not None
-+
-+ # Verify subject and issuer
-+ subject = cert_obj.get_attr_val_utf8_l('dsDynamicCertificateSubject')
-+ issuer = cert_obj.get_attr_val_utf8_l('dsDynamicCertificateIssuer')
-+ log.info(f"Subject: {subject}")
-+ log.info(f"Issuer: {issuer}")
-+ assert "Test Metadata CA".lower() in subject
-+ assert subject == issuer # Self-signed
-+
-+ # Verify CA flags
-+ is_ca = cert_obj.get_attr_val_utf8_l('dsDynamicCertificateIsCA')
-+ is_root_ca = cert_obj.get_attr_val_utf8_l('dsDynamicCertificateIsRootCA')
-+ log.info(f"isCA: {is_ca}")
-+ log.info(f"isRootCA: {is_root_ca}")
-+ assert is_ca == 'true'
-+ assert is_root_ca == 'true'
-+
-+ # Verify key algorithm
-+ key_algo = cert_obj.get_attr_val_utf8('dsDynamicCertificateKeyAlgorithm')
-+ log.info(f"Key Algorithm: {key_algo}")
-+ assert 'RSA' in key_algo.upper()
-+
-+ # Verify trust flags
-+ trust_flags = cert_obj.get_attr_val_utf8('dsDynamicCertificateTrustFlags')
-+ log.info(f"Trust Flags: {trust_flags}")
-+ assert trust_flags is not None
-+
-+ # Verify validity dates
-+ not_before = cert_obj.get_attr_val_utf8('dsDynamicCertificateNotBefore')
-+ not_after = cert_obj.get_attr_val_utf8('dsDynamicCertificateNotAfter')
-+ log.info(f"Not Before: {not_before}")
-+ log.info(f"Not After: {not_after}")
-+ assert not_before is not None
-+ assert not_after is not None
-+
-+ # Verify serial number
-+ serial = cert_obj.get_attr_val_utf8('dsDynamicCertificateSerialNumber')
-+ log.info(f"Serial Number: {serial}")
-+ assert serial is not None
-+ assert len(serial) > 0
-+
-+ # Cleanup
-+ dyncerts.del_cert(ca.nickname)
-+
-+ finally:
-+ if not DEBUGGING:
-+ shutil.rmtree(dir, ignore_errors=True)
-+
-+
-+def test_subject_alternative_names(topo, setup_tls):
-+ """Test that Subject Alternative Names are correctly parsed and displayed
-+
-+ :id: b2c3d4e5-2222-3333-4444-555555555555
-+ :setup: Standalone Instance with TLS enabled
-+ :steps:
-+ 1. Generate server certificate with multiple SANs (DNS and IP)
-+ 2. Add certificate dynamically using DynamicCerts
-+ 3. Retrieve certificate object
-+ 4. Verify dsDynamicCertificateSubjectAltName attribute exists
-+ 5. Verify all DNS names are present
-+ 6. Verify all IP addresses are present
-+ :expectedresults:
-+ 1. Certificate generated with SANs
-+ 2. Certificate added successfully
-+ 3. Certificate object retrieved
-+ 4. SubjectAltName attribute exists
-+ 5. DNS names are correctly parsed
-+ 6. IP addresses are correctly parsed
-+ """
-+ inst = setup_tls
-+ dir = '/tmp/dyncert_san_test'
-+ os.makedirs(dir, 0o700, exist_ok=True)
-+
-+ try:
-+ ca = RSA_Certificate.generateRootCA("TestSAN_CA")
-+ ca.save(dir)
-+
-+ # Add CA first
-+ dyncerts = DynamicCerts(inst)
-+ dyncerts.add_cert(ca.pem, ca.nickname, ca=True)
-+
-+ # Generate certificate with specific hostname
-+ cert = ca.generateCertificate("TestSAN_Cert", hostname="test.example.com")
-+ cert.save(dir)
-+
-+ # Add server certificate with private key
-+ dyncerts.add_cert(cert.p12, cert.nickname, pkcs12_password=RSA_Certificate.PKCS12_PASSWORD)
-+
-+ # Get certificate object
-+ cert_obj = dyncerts.get_cert_obj(cert.nickname)
-+ assert cert_obj is not None
-+
-+ # Verify SANs
-+ san_values = cert_obj.get_attr_vals_utf8('dsDynamicCertificateSubjectAltName')
-+ log.info(f"Subject Alternative Names: {san_values}")
-+ assert san_values is not None
-+
-+ # Verify expected SANs are present
-+ san_str = ' '.join(san_values)
-+ assert 'test.example.com' in san_str or 'localhost' in san_str
-+ assert '127.0.0.1' in san_str or '::1' in san_str
-+
-+ # Cleanup
-+ dyncerts.del_cert(ca.nickname)
-+ dyncerts.del_cert(cert.nickname)
-+
-+ finally:
-+ if not DEBUGGING:
-+ shutil.rmtree(dir, ignore_errors=True)
-+
-+
-+def test_private_key_detection(topo, setup_tls):
-+ """Test detection of certificates with and without private keys
-+
-+ :id: c3d4e5f6-3333-4444-5555-666666666666
-+ :setup: Standalone Instance with TLS enabled
-+ :steps:
-+ 1. Add CA certificate without private key
-+ 2. Get certificate object and verify hasPrivateKey is FALSE
-+ 3. Add server certificate with private key
-+ 4. Get certificate object and verify hasPrivateKey is TRUE
-+ 5. Verify isServerCert is TRUE for server cert
-+ 6. Verify isServerCert is FALSE for CA cert
-+ :expectedresults:
-+ 1. CA added successfully
-+ 2. hasPrivateKey is FALSE
-+ 3. Server cert added successfully
-+ 4. hasPrivateKey is TRUE
-+ 5. Server cert properly identified
-+ 6. CA cert not identified as server cert
-+ """
-+ inst = setup_tls
-+ dir = '/tmp/dyncert_privkey_test'
-+ os.makedirs(dir, 0o700, exist_ok=True)
-+
-+ try:
-+ ca = RSA_Certificate.generateRootCA("TestPrivKey_CA")
-+ ca.save(dir)
-+ cert = ca.generateCertificate("TestPrivKey_Cert")
-+ cert.save(dir)
-+
-+ dyncerts = DynamicCerts(inst)
-+
-+ # Add CA without private key (PEM has no key)
-+ dyncerts.add_cert(ca.pem, ca.nickname, ca=True)
-+
-+ # Verify CA has no private key
-+ ca_obj = dyncerts.get_cert_obj(ca.nickname)
-+ has_key = ca_obj.get_attr_val_utf8_l('dsDynamicCertificateHasPrivateKey')
-+ log.info(f"CA hasPrivateKey: {has_key}")
-+ assert has_key == 'false'
-+
-+ # Verify it's identified as CA
-+ is_ca = ca_obj.get_attr_val_utf8_l('dsDynamicCertificateIsCA')
-+ assert is_ca == 'true'
-+
-+ # Add server cert with private key (PKCS12 has key)
-+ dyncerts.add_cert(cert.p12, cert.nickname, pkcs12_password=RSA_Certificate.PKCS12_PASSWORD)
-+
-+ # Verify server cert has private key
-+ cert_obj = dyncerts.get_cert_obj(cert.nickname)
-+ has_key = cert_obj.get_attr_val_utf8_l('dsDynamicCertificateHasPrivateKey')
-+ log.info(f"Server cert hasPrivateKey: {has_key}")
-+ assert has_key == 'true'
-+
-+ # Check if isServerCert attribute exists
-+ is_server = cert_obj.get_attr_val_utf8_l('dsDynamicCertificateIsServerCert')
-+ if is_server:
-+ log.info(f"isServerCert: {is_server}")
-+
-+ # Cleanup
-+ dyncerts.del_cert(ca.nickname)
-+ dyncerts.del_cert(cert.nickname)
-+
-+ finally:
-+ if not DEBUGGING:
-+ shutil.rmtree(dir, ignore_errors=True)
-+
-+
-+def test_trust_flags_modification(topo, setup_tls):
-+ """Test modification of certificate trust flags
-+
-+ :id: d4e5f6a7-4444-5555-6666-777777777777
-+ :setup: Standalone Instance with TLS enabled
-+ :steps:
-+ 1. Add CA certificate with default trust flags
-+ 2. Read initial trust flags
-+ 3. Modify trust flags to different values using edit_cert_trust
-+ 4. Retrieve certificate and verify trust flags changed
-+ 5. Modify trust flags back
-+ 6. Verify trust flags changed again
-+ :expectedresults:
-+ 1. Certificate added successfully
-+ 2. Initial trust flags present
-+ 3. Modification succeeds
-+ 4. Trust flags match new value
-+ 5. Second modification succeeds
-+ 6. Trust flags match updated value
-+ """
-+ inst = setup_tls
-+ dir = '/tmp/dyncert_trust_test'
-+ os.makedirs(dir, 0o700, exist_ok=True)
-+
-+ try:
-+ ca = RSA_Certificate.generateRootCA("TestTrust_CA")
-+ ca.save(dir)
-+
-+ dyncerts = DynamicCerts(inst)
-+ dyncerts.add_cert(ca.pem, ca.nickname, ca=True)
-+
-+ # Read initial trust flags
-+ ca_obj = dyncerts.get_cert_obj(ca.nickname)
-+ initial_trust = ca_obj.get_attr_val_utf8('dsDynamicCertificateTrustFlags')
-+ log.info(f"Initial trust flags: {initial_trust}")
-+
-+ # Modify trust flags
-+ new_trust = "CT,C,C"
-+ dyncerts.edit_cert_trust(ca.nickname, new_trust)
-+
-+ # Verify modification
-+ ca_obj = dyncerts.get_cert_obj(ca.nickname)
-+ updated_trust = ca_obj.get_attr_val_utf8('dsDynamicCertificateTrustFlags')
-+ log.info(f"Updated trust flags: {updated_trust}")
-+ assert new_trust in updated_trust or updated_trust in new_trust
-+
-+ # Cleanup
-+ dyncerts.del_cert(ca.nickname)
-+
-+ finally:
-+ if not DEBUGGING:
-+ shutil.rmtree(dir, ignore_errors=True)
-+
-+
-+def test_certificate_replace_operation(topo, setup_tls):
-+ """Test replacing existing certificate using add_cert operation
-+
-+ :id: e5f6a7b8-5555-6666-7777-888888888888
-+ :setup: Standalone Instance with TLS enabled
-+ :steps:
-+ 1. Add initial certificate
-+ 2. Verify certificate is present
-+ 3. Generate new certificate with same nickname
-+ 4. Replace certificate using add_cert (it detects existing and updates)
-+ 5. Retrieve certificate and verify it was replaced
-+ 6. Verify serial number changed
-+ :expectedresults:
-+ 1. Initial certificate added
-+ 2. Certificate found
-+ 3. New certificate generated
-+ 4. Replace operation succeeds
-+ 5. Certificate is different
-+ 6. Serial number is different
-+ """
-+ inst = setup_tls
-+ dir = '/tmp/dyncert_replace_test'
-+ os.makedirs(dir, 0o700, exist_ok=True)
-+
-+ try:
-+ ca = RSA_Certificate.generateRootCA("TestReplace_CA")
-+ ca.save(dir)
-+
-+ dyncerts = DynamicCerts(inst)
-+
-+ # Add initial certificate
-+ dyncerts.add_cert(ca.pem, ca.nickname, ca=True)
-+
-+ # Get initial serial number
-+ ca_obj = dyncerts.get_cert_obj(ca.nickname)
-+ serial1 = ca_obj.get_attr_val_utf8('dsDynamicCertificateSerialNumber')
-+ log.info(f"Initial serial: {serial1}")
-+
-+ # Generate new certificate with same nickname
-+ ca2 = RSA_Certificate.generateRootCA("TestReplace_CA") # Same nickname, different cert
-+ os.makedirs(dir + '/replacement', 0o700, exist_ok=True)
-+ ca2.save(dir + '/replacement')
-+
-+ # Replace certificate (add_cert detects existing and updates)
-+ dyncerts.add_cert(ca2.pem, ca2.nickname, ca=True)
-+
-+ # Verify replacement
-+ ca_obj = dyncerts.get_cert_obj(ca.nickname)
-+ serial2 = ca_obj.get_attr_val_utf8('dsDynamicCertificateSerialNumber')
-+ log.info(f"New serial: {serial2}")
-+ assert serial1 != serial2
-+
-+ # Cleanup
-+ dyncerts.del_cert(ca.nickname)
-+
-+ finally:
-+ if not DEBUGGING:
-+ shutil.rmtree(dir, ignore_errors=True)
-+
-+
-+def test_search_with_filters(topo, setup_tls):
-+ """Test searching certificates with LDAP filters using DynamicCerts.list()
-+
-+ :id: f6a7b8c9-6666-7777-8888-999999999999
-+ :setup: Standalone Instance with TLS enabled
-+ :steps:
-+ 1. Add multiple certificates (CA and server certs)
-+ 2. List all certificates using list()
-+ 3. List CA certificates using list_ca_certs()
-+ 4. List server certificates using list_certs()
-+ 5. Use get_cert_details to get specific certificate info
-+ :expectedresults:
-+ 1. All certificates added successfully
-+ 2. list() returns all certificate objects
-+ 3. list_ca_certs() returns only CA certificates
-+ 4. list_certs() returns only server certificates
-+ 5. get_cert_details returns detailed info
-+ """
-+ inst = setup_tls
-+ fix_crash_issue_7227(inst)
-+ dir = '/tmp/dyncert_filter_test'
-+ os.makedirs(dir, 0o700, exist_ok=True)
-+
-+ try:
-+ dyncerts = DynamicCerts(inst)
-+
-+ # Add CA certificate
-+ ca = RSA_Certificate.generateRootCA("TestFilter_CA")
-+ ca.save(dir)
-+ dyncerts.add_cert(ca.pem, ca.nickname, ca=True)
-+
-+ # Add server certificate with private key
-+ cert = ca.generateCertificate("TestFilter_Cert")
-+ cert.save(dir)
-+ dyncerts.add_cert(cert.p12, cert.nickname, pkcs12_password=RSA_Certificate.PKCS12_PASSWORD)
-+
-+ # List all certificate objects
-+ all_certs = dyncerts.list()
-+ log.info(f"Total certificate objects found: {len(all_certs)}")
-+ assert len(all_certs) >= 2
-+
-+ # List CA certificates only
-+ ca_certs = dyncerts.list_ca_certs()
-+ log.info(f"CA certificates found: {len(ca_certs)}")
-+ assert len(ca_certs) >= 1
-+ assert any(c['cn'] == ca.nickname for c in ca_certs)
-+
-+ # List server certificates only
-+ server_certs = dyncerts.list_certs()
-+ log.info(f"Server certificates found: {len(server_certs)}")
-+ assert len(server_certs) >= 1
-+ assert any(c['cn'] == cert.nickname for c in server_certs)
-+
-+ # Get specific certificate details
-+ ca_details = dyncerts.get_cert_details(ca.nickname)
-+ assert ca_details is not None
-+ assert ca_details['cn'] == ca.nickname
-+ log.info(f"CA details: {ca_details}")
-+
-+ # Cleanup
-+ dyncerts.del_cert(ca.nickname)
-+ dyncerts.del_cert(cert.nickname)
-+
-+ finally:
-+ if not DEBUGGING:
-+ shutil.rmtree(dir, ignore_errors=True)
-+
-+
-+def test_batch_certificate_operations(topo, setup_tls):
-+ """Test adding and managing multiple certificates
-+
-+ :id: a7b8c9d0-7777-8888-9999-000000000000
-+ :setup: Standalone Instance with TLS enabled
-+ :steps:
-+ 1. Generate 5 different certificates
-+ 2. Add all certificates using DynamicCerts
-+ 3. List all certificates
-+ 4. Verify all certificates are present
-+ 5. Delete all certificates
-+ 6. Verify all deletions succeeded
-+ :expectedresults:
-+ 1. All certificates generated
-+ 2. All additions succeed
-+ 3. List returns all certificates
-+ 4. All certificates have correct metadata
-+ 5. All deletions succeed
-+ 6. No certificates remain
-+ """
-+ inst = setup_tls
-+ dir = '/tmp/dyncert_batch_test'
-+ os.makedirs(dir, 0o700, exist_ok=True)
-+
-+ cert_list = []
-+ try:
-+ dyncerts = DynamicCerts(inst)
-+
-+ # Generate and add multiple certificates
-+ for i in range(5):
-+ ca = RSA_Certificate.generateRootCA(f"TestBatch_CA_{i}")
-+ ca.save(dir)
-+ cert_list.append(ca)
-+
-+ dyncerts.add_cert(ca.pem, ca.nickname, ca=True)
-+ log.info(f"Added certificate {i+1}/5: {ca.nickname}")
-+
-+ # List all CA certificates
-+ ca_certs = dyncerts.list_ca_certs()
-+ batch_certs = [c for c in ca_certs if c['cn'].startswith('TestBatch_CA_')]
-+ log.info(f"Found {len(batch_certs)} batch certificates")
-+ assert len(batch_certs) == 5
-+
-+ # Verify each certificate
-+ for ca in cert_list:
-+ cert_obj = dyncerts.get_cert_obj(ca.nickname)
-+ assert cert_obj is not None
-+ subject = cert_obj.get_attr_val_utf8('dsDynamicCertificateSubject')
-+ assert subject is not None
-+ log.info(f"Verified certificate: {ca.nickname}")
-+
-+ # Delete all certificates
-+ for ca in cert_list:
-+ dyncerts.del_cert(ca.nickname)
-+ log.info(f"Deleted certificate: {ca.nickname}")
-+
-+ # Verify deletions
-+ ca_certs_after = dyncerts.list_ca_certs()
-+ batch_certs_after = [c for c in ca_certs_after if c['cn'].startswith('TestBatch_CA_')]
-+ assert len(batch_certs_after) == 0
-+
-+ finally:
-+ if not DEBUGGING:
-+ shutil.rmtree(dir, ignore_errors=True)
-+
-+
-+def test_invalid_certificate_handling(topo, setup_tls):
-+ """Test error handling for invalid certificate data
-+
-+ :id: b8c9d0e1-8888-9999-0000-111111111111
-+ :setup: Standalone Instance with TLS enabled
-+ :steps:
-+ 1. Attempt to add certificate from non-existent file
-+ 2. Attempt to add certificate with empty nickname
-+ 3. Attempt to add non-CA cert as CA
-+ 4. Verify all operations fail with appropriate errors
-+ :expectedresults:
-+ 1. Non-existent file rejected with ValueError
-+ 2. Empty nickname rejected with ValueError
-+ 3. Non-CA cert rejected as CA with ValueError
-+ 4. All errors are appropriate exceptions
-+ """
-+ inst = setup_tls
-+ dir = '/tmp/dyncert_invalid_test'
-+ os.makedirs(dir, 0o700, exist_ok=True)
-+
-+ try:
-+ dyncerts = DynamicCerts(inst)
-+
-+ # Test 1: Non-existent file
-+ try:
-+ dyncerts.add_cert('/nonexistent/file.pem', 'TestInvalid')
-+ assert False, "Should have failed with non-existent file"
-+ except ValueError as e:
-+ log.info(f"Expected error for non-existent file: {e}")
-+
-+ # Test 2: Empty nickname
-+ ca = RSA_Certificate.generateRootCA("TestInvalid_CA")
-+ ca.save(dir)
-+
-+ try:
-+ dyncerts.add_cert(ca.pem, '', ca=True)
-+ assert False, "Should have failed with empty nickname"
-+ except ValueError as e:
-+ log.info(f"Expected error for empty nickname: {e}")
-+
-+ # Test 3: Non-CA cert marked as CA
-+ cert = ca.generateCertificate("TestInvalid_Cert")
-+ cert.save(dir)
-+
-+ try:
-+ dyncerts.add_cert(cert.pem, cert.nickname, ca=True)
-+ assert False, "Should have failed with non-CA cert"
-+ except ValueError as e:
-+ log.info(f"Expected error for non-CA cert: {e}")
-+
-+ finally:
-+ if not DEBUGGING:
-+ shutil.rmtree(dir, ignore_errors=True)
-+
-+
-+def test_certificate_verification_status(topo, setup_tls):
-+ """Test certificate verification status attribute
-+
-+ :id: c9d0e1f2-9999-0000-1111-222222222222
-+ :setup: Standalone Instance with TLS enabled
-+ :steps:
-+ 1. Add valid CA certificate
-+ 2. Add server certificate signed by CA
-+ 3. Retrieve certificates and check verification status
-+ 4. Verify self-signed CA has appropriate status
-+ 5. Verify server cert has verification status
-+ :expectedresults:
-+ 1. CA added successfully
-+ 2. Server cert added successfully
-+ 3. Verification status attribute accessible
-+ 4. CA status is appropriate for self-signed
-+ 5. Server cert status reflects chain validation
-+ """
-+ inst = setup_tls
-+ dir = '/tmp/dyncert_verify_test'
-+ os.makedirs(dir, 0o700, exist_ok=True)
-+
-+ try:
-+ dyncerts = DynamicCerts(inst)
-+
-+ ca = RSA_Certificate.generateRootCA("TestVerify_CA")
-+ ca.save(dir)
-+ dyncerts.add_cert(ca.pem, ca.nickname, ca=True)
-+
-+ # Check CA verification status
-+ ca_obj = dyncerts.get_cert_obj(ca.nickname)
-+ verify_status = ca_obj.get_attr_val_utf8('dsDynamicCertificateVerificationStatus')
-+ if verify_status:
-+ log.info(f"CA verification status: {verify_status}")
-+ else:
-+ log.info("Verification status attribute not present for CA")
-+
-+ # Add server certificate
-+ cert = ca.generateCertificate("TestVerify_Cert")
-+ cert.save(dir)
-+ dyncerts.add_cert(cert.p12, cert.nickname, pkcs12_password=RSA_Certificate.PKCS12_PASSWORD)
-+
-+ # Check server cert verification status
-+ cert_obj = dyncerts.get_cert_obj(cert.nickname)
-+ verify_status = cert_obj.get_attr_val_utf8('dsDynamicCertificateVerificationStatus')
-+ if verify_status:
-+ log.info(f"Server cert verification status: {verify_status}")
-+ else:
-+ log.info("Verification status attribute not present for server cert")
-+
-+ # Cleanup
-+ dyncerts.del_cert(ca.nickname)
-+ dyncerts.del_cert(cert.nickname)
-+
-+ finally:
-+ if not DEBUGGING:
-+ shutil.rmtree(dir, ignore_errors=True)
-+
-+
-+def test_expired_certificate_handling(topo, setup_tls):
-+ """Test handling of expired certificates
-+
-+ :id: d0e1f2a3-0000-1111-2222-333333333333
-+ :setup: Standalone Instance with TLS enabled
-+ :steps:
-+ 1. Generate certificate with validity starting in the past and already expired
-+ 2. Add expired certificate
-+ 3. Retrieve certificate object
-+ 4. Verify notBefore date is in the past
-+ 5. Verify notAfter date is also in the past
-+ 6. Check if verification status indicates expiration
-+ :expectedresults:
-+ 1. Expired certificate generated
-+ 2. Certificate can be added (stored)
-+ 3. Certificate object retrieved
-+ 4. notBefore shows past date
-+ 5. notAfter shows past date (expired)
-+ 6. Verification status may indicate invalid/expired
-+ """
-+ inst = setup_tls
-+ dir = '/tmp/dyncert_expired_test'
-+ os.makedirs(dir, 0o700, exist_ok=True)
-+
-+ try:
-+ # Create a certificate that's already expired
-+ ca = RSA_Certificate()
-+ ca.nickname = "TestExpired_CA"
-+ ca.isCA = True
-+ ca.isRoot = True
-+ ca.fixNamingAttributes()
-+ ca.subject = x509.Name([x509.NameAttribute(k, v) for k, v in ca.namingAttrs.items()])
-+ ca.issuer = ca.subject
-+ ca.trust = 'CT,,'
-+
-+ # Generate with dates in the past
-+ past_date = utcdate() - datetime.timedelta(days=365)
-+ expired_date = utcdate() - datetime.timedelta(days=1)
-+
-+ ca.pkey = rsa.generate_private_key(
-+ public_exponent=65537,
-+ key_size=2048,
-+ backend=default_backend()
-+ )
-+ ca.cert = (
-+ x509.CertificateBuilder()
-+ .subject_name(ca.subject)
-+ .issuer_name(ca.issuer)
-+ .public_key(ca.pkey.public_key())
-+ .serial_number(x509.random_serial_number())
-+ .not_valid_before(past_date)
-+ .not_valid_after(expired_date)
-+ .add_extension(
-+ x509.BasicConstraints(ca=True, path_length=0),
-+ critical=True,
-+ )
-+ .sign(ca.pkey, hashes.SHA256(), default_backend())
-+ )
-+ ca.save(dir)
-+
-+ # Add expired certificate (may need force flag)
-+ dyncerts = DynamicCerts(inst)
-+ try:
-+ dyncerts.add_cert(ca.pem, ca.nickname, ca=True, force=True)
-+ except Exception as e:
-+ log.info(f"Adding expired cert raised: {e}")
-+ return # Expected - server may reject expired certs
-+
-+ # Verify certificate metadata shows expiration
-+ ca_obj = dyncerts.get_cert_obj(ca.nickname)
-+ if ca_obj:
-+ not_before = ca_obj.get_attr_val_utf8('dsDynamicCertificateNotBefore')
-+ not_after = ca_obj.get_attr_val_utf8('dsDynamicCertificateNotAfter')
-+ log.info(f"Expired cert - Not Before: {not_before}")
-+ log.info(f"Expired cert - Not After: {not_after}")
-+
-+ verify_status = ca_obj.get_attr_val_utf8('dsDynamicCertificateVerificationStatus')
-+ if verify_status:
-+ log.info(f"Verification status (should indicate expired): {verify_status}")
-+
-+ # Cleanup
-+ dyncerts.del_cert(ca.nickname)
-+
-+ finally:
-+ if not DEBUGGING:
-+ shutil.rmtree(dir, ignore_errors=True)
-+
-+
-+def test_list_and_get_operations(topo, setup_tls):
-+ """Test listing and retrieval operations
-+
-+ :id: e1f2a3b4-1111-2222-3333-444444444444
-+ :setup: Standalone Instance with TLS enabled
-+ :steps:
-+ 1. Add CA and server certificates
-+ 2. Use list() to get all certificate objects
-+ 3. Use list_ca_certs() to get CA cert details
-+ 4. Use list_certs() to get server cert details
-+ 5. Use get_cert_obj() to retrieve specific certificate
-+ 6. Use get_cert_details() to get formatted details
-+ :expectedresults:
-+ 1. Certificates added successfully
-+ 2. list() returns DSLdapObject instances
-+ 3. list_ca_certs() returns dict with CA info
-+ 4. list_certs() returns dict with server cert info
-+ 5. get_cert_obj() returns DynamicCert object
-+ 6. get_cert_details() returns formatted dictionary
-+ """
-+ inst = setup_tls
-+ dir = '/tmp/dyncert_list_test'
-+ os.makedirs(dir, 0o700, exist_ok=True)
-+
-+ try:
-+ dyncerts = DynamicCerts(inst)
-+
-+ # Add certificates
-+ ca = RSA_Certificate.generateRootCA("TestList_CA")
-+ ca.save(dir)
-+ dyncerts.add_cert(ca.pem, ca.nickname, ca=True)
-+
-+ cert = ca.generateCertificate("TestList_Cert")
-+ cert.save(dir)
-+ dyncerts.add_cert(cert.p12, cert.nickname, pkcs12_password=RSA_Certificate.PKCS12_PASSWORD)
-+
-+ # Test list() - returns DynamicCert objects
-+ all_objs = dyncerts.list()
-+ log.info(f"list() returned {len(all_objs)} objects")
-+ assert any(isinstance(obj, DynamicCert) and obj.get_attr_val_utf8('cn') == ca.nickname
-+ for obj in all_objs if obj._dn != DYNCERT_SUFFIX)
-+
-+ # Test list_ca_certs() - returns list of dicts
-+ ca_list = dyncerts.list_ca_certs()
-+ log.info(f"list_ca_certs() returned {len(ca_list)} CAs")
-+ assert any(c['cn'] == ca.nickname for c in ca_list)
-+ ca_entry = next(c for c in ca_list if c['cn'] == ca.nickname)
-+ assert 'subject' in ca_entry
-+ assert 'issuer' in ca_entry
-+ assert 'expires' in ca_entry
-+ assert 'trust_flags' in ca_entry
-+
-+ # Test list_certs() - returns list of dicts
-+ cert_list = dyncerts.list_certs()
-+ log.info(f"list_certs() returned {len(cert_list)} server certs")
-+ assert any(c['cn'] == cert.nickname for c in cert_list)
-+
-+ # Test get_cert_obj() - returns DynamicCert object
-+ ca_obj = dyncerts.get_cert_obj(ca.nickname)
-+ assert isinstance(ca_obj, DynamicCert)
-+ assert ca_obj.get_attr_val_utf8('cn') == ca.nickname
-+
-+ # Test get_cert_details() - returns formatted dict
-+ ca_details = dyncerts.get_cert_details(ca.nickname)
-+ assert ca_details['cn'] == ca.nickname
-+ assert 'subject' in ca_details
-+ assert 'issuer' in ca_details
-+ assert 'expires' in ca_details
-+ assert 'trust_flags' in ca_details
-+ log.info(f"get_cert_details() returned: {ca_details}")
-+
-+ # Cleanup
-+ dyncerts.del_cert(ca.nickname)
-+ dyncerts.del_cert(cert.nickname)
-+
-+ finally:
-+ if not DEBUGGING:
-+ shutil.rmtree(dir, ignore_errors=True)
-+
-+
-+def pem_file_to_der(pem_file_path):
-+ log.debug(f"Decoding cert pem: {pem_file_path}")
-+ with open(pem_file_path, "rb") as f:
-+ pem__bytes = f.read()
-+ try:
-+ return pem_to_der(pem__bytes)
-+ except Exception as e:
-+ raise ValueError(f"Failed to parse certificate '{pem_file_path}': {e}")
-+
-+
-+@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")
-+@pytest.mark.parametrize("with_private_key", [False, pytest.param(True, marks=pytest.mark.xfail(reason='cryptography module does not support ML-DSA keys')),])
-+def test_mldsa_dynamic_certificates(topo, setup_tls, with_private_key):
-+ """Test ML-DSA (post-quantum) certificates with Dynamic Certificates feature
-+
-+ :id: f2a3b4c5-2222-3333-4444-555555555555
-+ :setup: Standalone Instance with TLS enabled
-+ :steps:
-+ 1. Generate ML-DSA-87 Root CA certificate using OpenSSL
-+ 2. Generate ML-DSA-65 server certificate signed by CA
-+ 3. Add CA certificate dynamically using DynamicCerts
-+ 4. Add server certificate dynamically using DynamicCerts (with or without private key)
-+ 5. Verify CA key algorithm shows ML-DSA
-+ 6. Verify server cert key algorithm shows ML-DSA
-+ 7. Verify all certificate metadata is correctly extracted
-+ 8. Verify CA is identified correctly
-+ 9. Verify server cert has private key based on parameter
-+ 10. Test certificate listing operations
-+ :expectedresults:
-+ 1. ML-DSA CA generated successfully
-+ 2. ML-DSA server cert generated successfully
-+ 3. CA added without error
-+ 4. Server cert added without error (with or without key)
-+ 5. CA key algorithm contains ML-DSA
-+ 6. Server cert key algorithm contains ML-DSA
-+ 7. All metadata attributes present and correct
-+ 8. isCA is TRUE for CA
-+ 9. hasPrivateKey matches with_private_key parameter
-+ 10. Certificates appear in list operations
-+ """
-+ inst = setup_tls
-+ fix_crash_issue_7227(inst)
-+ key_suffix = 'with_key' if with_private_key else 'without_key'
-+ dir = f'/tmp/dyncert_mldsa_test_{key_suffix}'
-+ os.makedirs(dir, 0o700, exist_ok=True)
-+
-+ ca_config = """
-+[ req ]
-+distinguished_name = req_distinguished_name
-+policy = policy_match
-+x509_extensions = v3_ca
-+
-+[ 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 = US
-+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 = TestMLDSA-CA-Org
-+
-+organizationalUnitName = Organizational Unit Name (eg, section)
-+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
-+keyUsage = critical, keyCertSign
-+"""
-+
-+ cert_config = """
-+[ req ]
-+distinguished_name = req_distinguished_name
-+policy = policy_match
-+x509_extensions = v3_cert
-+
-+[ 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 = US
-+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 = TestMLDSA-Org
-+
-+organizationalUnitName = Organizational Unit Name (eg, section)
-+commonName = Common Name (e.g. server FQDN or YOUR name)
-+commonName_max = 64
-+
-+[ v3_cert ]
-+basicConstraints = critical,CA:false
-+subjectAltName = DNS:localhost,IP:127.0.0.1
-+keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
-+extendedKeyUsage = clientAuth, serverAuth
-+"""
-+
-+ try:
-+ hostname = socket.gethostname()
-+
-+ # Write config files
-+ ca_conf_file = f"{dir}/ca.conf"
-+ cert_conf_file = f"{dir}/cert.conf"
-+
-+ with open(ca_conf_file, 'w') as f:
-+ f.write(ca_config)
-+
-+ with open(cert_conf_file, 'w') as f:
-+ f.write(cert_config)
-+
-+ # Generate ML-DSA-87 CA certificate
-+ log.info("Generating ML-DSA-87 CA certificate...")
-+
-+ # Generate CA private key
-+ result = subprocess.run(
-+ ['openssl', 'genpkey', '-algorithm', 'ML-DSA-87', '-out', f'{dir}/ca.key'],
-+ capture_output=True, text=True, check=True
-+ )
-+ log.debug(f"CA key generation output: {result.stderr}")
-+
-+ # Generate self-signed CA certificate
-+ result = subprocess.run(
-+ ['openssl', 'req', '-x509', '-new', '-sha256', '-key', f'{dir}/ca.key',
-+ '-nodes', '-days', '3650', '-config', ca_conf_file,
-+ '-subj', f'/CN=TestMLDSA-CA/O=TestMLDSA-CA-Org/C=US',
-+ '-out', f'{dir}/ca.pem'],
-+ capture_output=True, text=True, check=True
-+ )
-+ log.debug(f"CA cert generation output: {result.stderr}")
-+
-+ # Generate ML-DSA-65 server certificate
-+ log.info("Generating ML-DSA-65 server certificate...")
-+
-+ # Generate server private key
-+ result = subprocess.run(
-+ ['openssl', 'genpkey', '-algorithm', 'ML-DSA-65', '-out', f'{dir}/cert.key'],
-+ capture_output=True, text=True, check=True
-+ )
-+ log.debug(f"Server key generation output: {result.stderr}")
-+
-+ # Generate certificate signing request
-+ result = subprocess.run(
-+ ['openssl', 'req', '-new', '-sha256', '-key', f'{dir}/cert.key',
-+ '-nodes', '-config', cert_conf_file,
-+ '-subj', f'/CN=TestMLDSA-Cert/O=TestMLDSA-Org/C=US',
-+ '-out', f'{dir}/cert.csr'],
-+ capture_output=True, text=True, check=True
-+ )
-+ log.debug(f"CSR generation output: {result.stderr}")
-+
-+ # Sign server certificate with CA
-+ result = subprocess.run(
-+ ['openssl', 'x509', '-req', '-sha256', '-days', '3650',
-+ '-extensions', 'v3_cert', '-extfile', cert_conf_file,
-+ '-in', f'{dir}/cert.csr', '-CA', f'{dir}/ca.pem',
-+ '-CAkey', f'{dir}/ca.key', '-CAcreateserial',
-+ '-out', f'{dir}/cert.pem'],
-+ capture_output=True, text=True, check=True
-+ )
-+ log.debug(f"Cert signing output: {result.stderr}")
-+
-+ # Create PKCS12 bundle for server certificate
-+ result = subprocess.run(
-+ ['openssl', 'pkcs12', '-export', '-inkey', f'{dir}/cert.key',
-+ '-in', f'{dir}/cert.pem', '-name', 'mldsa-server-cert',
-+ '-out', f'{dir}/cert.p12', '-passout', 'pass:mldsa123'],
-+ capture_output=True, text=True, check=True
-+ )
-+ log.debug(f"PKCS12 creation output: {result.stderr}")
-+
-+ # Display certificate details for debugging
-+ result = subprocess.run(
-+ ['openssl', 'x509', '-text', '-noout', '-in', f'{dir}/ca.pem'],
-+ capture_output=True, text=True, check=True
-+ )
-+ log.info(f"CA Certificate details:\n{result.stdout}")
-+
-+ result = subprocess.run(
-+ ['openssl', 'x509', '-text', '-noout', '-in', f'{dir}/cert.pem'],
-+ capture_output=True, text=True, check=True
-+ )
-+ log.info(f"Server Certificate details:\n{result.stdout}")
-+
-+ # Add certificates using DynamicCerts
-+ dyncerts = DynamicCerts(inst)
-+
-+ # Add CA certificate
-+ log.info("Adding ML-DSA CA certificate to Dynamic Certificates...")
-+ dyncerts.add_cert(f'{dir}/ca.pem', 'TestMLDSA_CA', ca=True)
-+
-+ # Add server certificate (with or without private key based on parameter)
-+ if with_private_key:
-+ log.info("Adding ML-DSA server certificate WITH private key to Dynamic Certificates...")
-+ # Note: cannot use dyncerts.add_cert with PKCS12 because python3 cryptography
-+ # module does not yet support ML-DSA. Instead, use OpenSSL to convert to DER
-+ # and use DynamicCerts.create() directly
-+ # dyncerts.add_cert(f'{dir}/cert.p12', 'TestMLDSA_Cert', pkcs12_password='mldsa123')
-+
-+ # Convert certificate to DER using OpenSSL
-+ result = subprocess.run(
-+ ['openssl', 'x509', '-in', f'{dir}/cert.pem', '-outform', 'DER', '-out', f'{dir}/cert.der'],
-+ capture_output=True, text=True, check=True
-+ )
-+
-+ # Convert private key to DER PrivateKeyInfo (PKCS#8) using OpenSSL
-+ result = subprocess.run(
-+ ['openssl', 'pkcs8', '-topk8', '-nocrypt', '-in', f'{dir}/cert.key',
-+ '-outform', 'DER', '-out', f'{dir}/cert_key.der'],
-+ capture_output=True, text=True, check=True
-+ )
-+
-+ # Read DER data
-+ with open(f'{dir}/cert.der', 'rb') as f:
-+ cert_der = f.read()
-+ with open(f'{dir}/cert_key.der', 'rb') as f:
-+ key_der = f.read()
-+
-+ # Create entry directly with DER data
-+ properties = {
-+ DYCATTR_CN: 'TestMLDSA_Cert',
-+ DYCATTR_CERTDER: cert_der,
-+ DYCATTR_PKEYDER: key_der,
-+ }
-+ rdn = f"{DYCATTR_CN}=TestMLDSA_Cert"
-+ dyncerts.create(rdn=rdn, properties=properties)
-+ else:
-+ log.info("Adding ML-DSA server certificate WITHOUT private key to Dynamic Certificates...")
-+ dyncerts.add_cert(f'{dir}/cert.pem', 'TestMLDSA_Cert')
-+
-+ # Verify CA certificate metadata
-+ log.info("Verifying CA certificate metadata...")
-+ ca_obj = dyncerts.get_cert_obj('TestMLDSA_CA')
-+ assert ca_obj is not None
-+
-+ # Verify subject and issuer
-+ ca_subject = ca_obj.get_attr_val_utf8('dsDynamicCertificateSubject')
-+ ca_issuer = ca_obj.get_attr_val_utf8('dsDynamicCertificateIssuer')
-+ log.info(f"CA Subject: {ca_subject}")
-+ log.info(f"CA Issuer: {ca_issuer}")
-+ assert 'TestMLDSA-CA' in ca_subject
-+ assert ca_subject == ca_issuer # Self-signed
-+
-+ # Verify CA flags
-+ is_ca = ca_obj.get_attr_val_utf8_l('dsDynamicCertificateIsCA')
-+ is_root_ca = ca_obj.get_attr_val_utf8_l('dsDynamicCertificateIsRootCA')
-+ log.info(f"CA isCA: {is_ca}")
-+ log.info(f"CA isRootCA: {is_root_ca}")
-+ assert is_ca == 'true'
-+ assert is_root_ca == 'true'
-+
-+ # Verify CA key algorithm contains ML-DSA
-+ ca_key_algo = ca_obj.get_attr_val_utf8('dsDynamicCertificateKeyAlgorithm')
-+ log.info(f"CA Key Algorithm: {ca_key_algo}")
-+ assert ca_key_algo is not None
-+ assert 'ML-DSA' in ca_key_algo.upper() or 'DILITHIUM' in ca_key_algo.upper() or '2.16.840.1.101.3.4.3' in ca_key_algo
-+
-+ # Verify CA has no private key (PEM file only contains cert)
-+ ca_has_key = ca_obj.get_attr_val_utf8_l('dsDynamicCertificateHasPrivateKey')
-+ log.info(f"CA hasPrivateKey: {ca_has_key}")
-+ assert ca_has_key == 'false'
-+
-+ # Verify server certificate metadata
-+ log.info("Verifying server certificate metadata...")
-+ cert_obj = dyncerts.get_cert_obj('TestMLDSA_Cert')
-+ assert cert_obj is not None
-+
-+ # Verify subject
-+ cert_subject = cert_obj.get_attr_val_utf8('dsDynamicCertificateSubject')
-+ cert_issuer = cert_obj.get_attr_val_utf8('dsDynamicCertificateIssuer')
-+ log.info(f"Server Cert Subject: {cert_subject}")
-+ log.info(f"Server Cert Issuer: {cert_issuer}")
-+ assert 'TestMLDSA-Cert' in cert_subject
-+
-+ # Verify server cert is not a CA
-+ cert_is_ca = cert_obj.get_attr_val_utf8_l('dsDynamicCertificateIsCA')
-+ log.info(f"Server Cert isCA: {cert_is_ca}")
-+ assert cert_is_ca == 'false'
-+
-+ # Verify server cert key algorithm contains ML-DSA
-+ cert_key_algo = cert_obj.get_attr_val_utf8('dsDynamicCertificateKeyAlgorithm')
-+ log.info(f"Server Cert Key Algorithm: {cert_key_algo}")
-+ assert cert_key_algo is not None
-+ assert 'ML-DSA' in cert_key_algo.upper()
-+
-+ # Verify server cert has private key based on parameter
-+ cert_has_key = cert_obj.get_attr_val_utf8_l('dsDynamicCertificateHasPrivateKey')
-+ log.info(f"Server Cert hasPrivateKey: {cert_has_key}")
-+ expected_has_key = 'true' if with_private_key else 'false'
-+ assert cert_has_key == expected_has_key, \
-+ f"Expected hasPrivateKey={expected_has_key}, got {cert_has_key}"
-+
-+ # Verify serial numbers are present
-+ ca_serial = ca_obj.get_attr_val_utf8('dsDynamicCertificateSerialNumber')
-+ cert_serial = cert_obj.get_attr_val_utf8('dsDynamicCertificateSerialNumber')
-+ log.info(f"CA Serial: {ca_serial}")
-+ log.info(f"Server Cert Serial: {cert_serial}")
-+ assert ca_serial is not None and len(ca_serial) > 0
-+ assert cert_serial is not None and len(cert_serial) > 0
-+
-+ # Verify validity dates
-+ ca_not_before = ca_obj.get_attr_val_utf8('dsDynamicCertificateNotBefore')
-+ ca_not_after = ca_obj.get_attr_val_utf8('dsDynamicCertificateNotAfter')
-+ cert_not_before = cert_obj.get_attr_val_utf8('dsDynamicCertificateNotBefore')
-+ cert_not_after = cert_obj.get_attr_val_utf8('dsDynamicCertificateNotAfter')
-+ log.info(f"CA Valid: {ca_not_before} to {ca_not_after}")
-+ log.info(f"Server Cert Valid: {cert_not_before} to {cert_not_after}")
-+ assert ca_not_before is not None
-+ assert ca_not_after is not None
-+ assert cert_not_before is not None
-+ assert cert_not_after is not None
-+
-+ # Verify Subject Alternative Names on server cert
-+ cert_san = cert_obj.get_attr_vals_utf8('dsDynamicCertificateSubjectAltName')
-+ if cert_san:
-+ log.info(f"Server Cert SANs: {cert_san}")
-+ san_str = ' '.join(cert_san)
-+ assert 'localhost' in san_str or '127.0.0.1' in san_str
-+
-+ # Test listing operations
-+ log.info("Testing certificate listing operations...")
-+
-+ ca_list = dyncerts.list_ca_certs()
-+ mldsa_cas = [c for c in ca_list if c['cn'] == 'TestMLDSA_CA']
-+ assert len(mldsa_cas) == 1
-+ log.info(f"Found ML-DSA CA in list: {mldsa_cas[0]}")
-+
-+ cert_list = dyncerts.list_certs()
-+ mldsa_certs = [c for c in cert_list if c['cn'] == 'TestMLDSA_Cert']
-+ assert len(mldsa_certs) == 1
-+ log.info(f"Found ML-DSA server cert in list: {mldsa_certs[0]}")
-+
-+ # Test get_cert_details
-+ ca_details = dyncerts.get_cert_details('TestMLDSA_CA')
-+ assert ca_details is not None
-+ assert ca_details['cn'] == 'TestMLDSA_CA'
-+ assert 'subject' in ca_details
-+ assert 'issuer' in ca_details
-+ log.info(f"CA details: {ca_details}")
-+
-+ cert_details = dyncerts.get_cert_details('TestMLDSA_Cert')
-+ assert cert_details is not None
-+ assert cert_details['cn'] == 'TestMLDSA_Cert'
-+ log.info(f"Server cert details: {cert_details}")
-+
-+ # Cleanup
-+ log.info("Cleaning up ML-DSA certificates...")
-+ dyncerts.del_cert('TestMLDSA_CA')
-+ dyncerts.del_cert('TestMLDSA_Cert')
-+
-+ key_status = "with private key" if with_private_key else "without private key"
-+ log.info(f"ML-DSA Dynamic Certificates test ({key_status}) completed successfully!")
-+
-+ except subprocess.CalledProcessError as e:
-+ log.error(f"OpenSSL command failed: {e}")
-+ log.error(f"stdout: {e.stdout}")
-+ log.error(f"stderr: {e.stderr}")
-+ raise
-+ except Exception as e:
-+ log.error(f"Test failed with exception: {e}")
-+ raise
-+ finally:
-+ if not DEBUGGING:
-+ shutil.rmtree(dir, ignore_errors=True)
-+
-+
-+if __name__ == '__main__':
-+ CURRENT_FILE = os.path.realpath(__file__)
-+ pytest.main(["-s", "-v", CURRENT_FILE])
-diff --git a/dirsrvtests/tests/suites/tls/mldsa_test.py b/dirsrvtests/tests/suites/tls/mldsa_test.py
-index d6cbaf662..245069b86 100644
---- a/dirsrvtests/tests/suites/tls/mldsa_test.py
-+++ b/dirsrvtests/tests/suites/tls/mldsa_test.py
-@@ -10,11 +10,9 @@ 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.utils import ds_is_older, rpm_is_older
- from lib389._constants import DN_DM, PW_DM, DEFAULT_SUFFIX
- from lib389.config import Encryption, CertmapLegacy
- from lib389.idm.user import UserAccount
-@@ -31,21 +29,6 @@ else:
- 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
-diff --git a/ldap/servers/slapd/dyncerts.c b/ldap/servers/slapd/dyncerts.c
-index efeaa6eb6..6d797a127 100644
---- a/ldap/servers/slapd/dyncerts.c
-+++ b/ldap/servers/slapd/dyncerts.c
-@@ -444,6 +444,9 @@ key_algo(CERTCertificate *cert)
- switch (keyType) {
- case rsaKey: return "RSA";
- case ecKey: return "EC";
-+#ifdef MAX_ML_DSA_PRIVATE_KEY_LEN
-+ case mldsaKey: return "ML-DSA";
-+#endif
- default: return "UNKNOWN";
- }
- }
-@@ -577,8 +580,13 @@ verify_cert(Slapi_Entry *e, CERTCertificate *cert, const char *attrname)
- static ber_tag_t
- sanse_bv2 (BerElement *ber, general_name_value_t *val)
- {
-+ ber_tag_t rc = 0;
- val->nbvals = 2;
-- return ber_scanf(ber, "{oo}", &val->vals[0], &val->vals[1]);
-+ rc = ber_scanf(ber, "{oo}", &val->vals[0], &val->vals[1]);
-+ if (val->vals[0].bv_len == 0) {
-+ return LBER_ERROR;
-+ }
-+ return rc;
- }
-
- /*
-@@ -589,8 +597,13 @@ sanse_bv2 (BerElement *ber, general_name_value_t *val)
- static ber_tag_t
- sanse_edi (BerElement *ber, general_name_value_t *val)
- {
-+ ber_tag_t rc = 0;
- val->nbvals = 1;
-- return ber_scanf(ber, "{o}", &val->vals[0]);
-+ rc = ber_scanf(ber, "{o}", &val->vals[0]);
-+ if (val->vals[0].bv_len == 0) {
-+ return LBER_ERROR;
-+ }
-+ return rc;
- }
-
- /*
-@@ -600,8 +613,13 @@ sanse_edi (BerElement *ber, general_name_value_t *val)
- static ber_tag_t
- sanse_bv1 (BerElement *ber, general_name_value_t *val)
- {
-+ ber_tag_t rc = 0;
- val->nbvals = 1;
-- return ber_scanf(ber, "o", &val->vals[0]);
-+ rc = ber_scanf(ber, "o", &val->vals[0]);
-+ if (val->vals[0].bv_len == 0) {
-+ return LBER_ERROR;
-+ }
-+ return rc;
- }
-
- /*
-@@ -611,9 +629,11 @@ sanse_bv1 (BerElement *ber, general_name_value_t *val)
- static ber_tag_t
- sanse_bv0 (BerElement *ber, general_name_value_t *val)
- {
-+ ber_tag_t rc = 0;
- ber_tag_t tag = 0;
- val->nbvals = 0;
-- return ber_scanf(ber, "T", &tag);
-+ rc = ber_scanf(ber, "T", &tag);
-+ return rc;
- }
-
- /* Free data within general_name_value_t */
-@@ -693,7 +713,20 @@ gnw_cb(general_name_type_t gnt, const general_name_value_t *val, void *arg)
- switch (val->nbvals) {
- case 1:
- if (gnt == gnt_ipaddress) {
-- /* Should convert the address to string */
-+ /* Lets convert the address to string */
-+ if (val->vals[0].bv_len == 4) {
-+ /* IPv4 address */
-+ str = slapi_ch_malloc(INET_ADDRSTRLEN);
-+ if (inet_ntop(AF_INET, val->vals[0].bv_val, str, INET_ADDRSTRLEN) == NULL) {
-+ slapi_ch_free_string(&str);
-+ }
-+ } else if (val->vals[0].bv_len == 16) {
-+ /* IPv6 address */
-+ str = slapi_ch_malloc(INET6_ADDRSTRLEN);
-+ if (inet_ntop(AF_INET6, val->vals[0].bv_val, str, INET6_ADDRSTRLEN) == NULL) {
-+ slapi_ch_free_string(&str);
-+ }
-+ }
- } else {
- size_t len = val->vals[0].bv_len;
- str = slapi_ch_malloc(len+1);
-diff --git a/src/lib389/lib389/nss_ssl.py b/src/lib389/lib389/nss_ssl.py
-index fae65d19c..b0d2e115b 100644
---- a/src/lib389/lib389/nss_ssl.py
-+++ b/src/lib389/lib389/nss_ssl.py
-@@ -247,6 +247,13 @@ only.
- code. Instead, we parse the output of `openssl version` and try to
- figure out if we have a new enough version to unconditionally run rehash.
- """
-+
-+ def only_warning(text):
-+ for line in text.split('\n'):
-+ if line and not 'warning' in line.lower():
-+ return False
-+ return True
-+
- try:
- openssl_version = check_output(['/usr/bin/openssl', 'version']).decode('utf-8').strip()
- except subprocess.CalledProcessError as e:
-@@ -259,9 +266,13 @@ only.
- cmd = ['/usr/bin/c_rehash', certdir]
- self.log.debug("nss cmd: %s", format_cmd_list(cmd))
- try:
-- check_output(cmd, stderr=subprocess.STDOUT)
-+ res = run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-+ self.log.debug("nss cmd: %s returned %d STDOUT=%s",
-+ format_cmd_list(cmd), res.returncode, res.stdout)
-+ if res.returncode != 1 or not only_warning(res.stdout):
-+ res.check_returncode()
- except subprocess.CalledProcessError as e:
-- raise ValueError(e.output.decode('utf-8').rstrip())
-+ raise ValueError(e.output.rstrip())
-
- def create_rsa_ca(self, months=VALID):
- """
-diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py
-index 042def090..07cb34d93 100644
---- a/src/lib389/lib389/utils.py
-+++ b/src/lib389/lib389/utils.py
-@@ -21,12 +21,14 @@ import ldap
- import mmap
- import socket
- import ipaddress
-+import itertools
- import time
- import stat
- from datetime import (datetime, timedelta)
- import sys
- import filecmp
- import pwd
-+import rpm
- import shlex
- import operator
- import subprocess
-@@ -2130,3 +2132,21 @@ def get_timeout_scale():
- log.error(f"DS_TIMEOUT_SCALE should be a valid float. Using default value: {scale_factor}")
- return scale_factor
-
-+
-+def rpm_is_older(pkg, version):
-+ """Check if an RPM package version is older than specified version"""
-+ ts = rpm.TransactionSet()
-+ mi = ts.dbMatch('name', pkg)
-+ for h in mi:
-+ log.debug(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
-+ if n1 > n2:
-+ return False
-+ return False
-+
---
-2.52.0
-
diff --git a/0047-Issue-7184-2nd-argparse.HelpFormatter-_format_action.patch b/0047-Issue-7184-2nd-argparse.HelpFormatter-_format_action.patch
deleted file mode 100644
index 1838659..0000000
--- a/0047-Issue-7184-2nd-argparse.HelpFormatter-_format_action.patch
+++ /dev/null
@@ -1,41 +0,0 @@
-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
-
diff --git a/0048-Issue-7213-2nd-MDB_BAD_VALSIZE-error-while-handling-.patch b/0048-Issue-7213-2nd-MDB_BAD_VALSIZE-error-while-handling-.patch
deleted file mode 100644
index 8d7f5c0..0000000
--- a/0048-Issue-7213-2nd-MDB_BAD_VALSIZE-error-while-handling-.patch
+++ /dev/null
@@ -1,32 +0,0 @@
-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
-
diff --git a/0049-Issue-7223-Use-lexicographical-order-for-ancestorid-.patch b/0049-Issue-7223-Use-lexicographical-order-for-ancestorid-.patch
deleted file mode 100644
index 1291b2e..0000000
--- a/0049-Issue-7223-Use-lexicographical-order-for-ancestorid-.patch
+++ /dev/null
@@ -1,34 +0,0 @@
-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
-
diff --git a/0050-Issue-3134-Fix-build-break-7260.patch b/0050-Issue-3134-Fix-build-break-7260.patch
deleted file mode 100644
index 66d8bed..0000000
--- a/0050-Issue-3134-Fix-build-break-7260.patch
+++ /dev/null
@@ -1,38 +0,0 @@
-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
-
diff --git a/0051-Issue-7066-7052-allow-password-history-to-be-set-to-.patch b/0051-Issue-7066-7052-allow-password-history-to-be-set-to-.patch
deleted file mode 100644
index ca1daab..0000000
--- a/0051-Issue-7066-7052-allow-password-history-to-be-set-to-.patch
+++ /dev/null
@@ -1,87 +0,0 @@
-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.53.0
-
diff --git a/0052-Issue-7243-UI-add-support-for-hot-certificates.patch b/0052-Issue-7243-UI-add-support-for-hot-certificates.patch
deleted file mode 100644
index 0255888..0000000
--- a/0052-Issue-7243-UI-add-support-for-hot-certificates.patch
+++ /dev/null
@@ -1,1379 +0,0 @@
-From 56563c9083a01fc26d853edf4538ce70900d259c Mon Sep 17 00:00:00 2001
-From: Mark Reynolds <mreynolds@redhat.com>
-Date: Wed, 28 Jan 2026 12:02:54 -0500
-Subject: [PATCH] Issue 7243 - UI - add support for hot certificates
-
-Description:
-
-In the "Add Server Certificate" modal add password options for pkcs#12
-certificates. Also improved validation for certificate names
-
-relates: https://github.com/389ds/389-ds-base/issues/7243
-
-Reviewed by: jchapman & spichugi (Thanks!!)
----
- .../lib/security/certificateManagement.jsx | 366 ++++++++++++---
- .../src/lib/security/securityModals.jsx | 425 +++++++++++++++++-
- .../src/lib/security/securityTables.jsx | 85 ++--
- src/cockpit/389-console/src/security.jsx | 10 +
- src/lib389/lib389/cli_conf/security.py | 23 +-
- 5 files changed, 758 insertions(+), 151 deletions(-)
-
-diff --git a/src/cockpit/389-console/src/lib/security/certificateManagement.jsx b/src/cockpit/389-console/src/lib/security/certificateManagement.jsx
-index 475fcba27..18f98599b 100644
---- a/src/cockpit/389-console/src/lib/security/certificateManagement.jsx
-+++ b/src/cockpit/389-console/src/lib/security/certificateManagement.jsx
-@@ -19,6 +19,7 @@ import {
- import {
- EditCertModal,
- SecurityAddCertModal,
-+ SecurityAddCACertModal,
- SecurityAddCSRModal,
- SecurityViewCSRModal,
- ExportCertModal,
-@@ -37,6 +38,8 @@ export class CertificateManagement extends React.Component {
- CACerts: this.props.CACerts,
- ServerCSRs: this.props.ServerCSRs,
- ServerKeys: this.props.ServerKeys,
-+ certNicknames: this.props.certNicknames,
-+ CACertNicknames: this.props.CACertNicknames,
- tableKey: 0,
- showEditModal: false,
- showAddModal: false,
-@@ -85,6 +88,11 @@ export class CertificateManagement extends React.Component {
- uploadFileName: "",
- uploadIsLoading: false,
- uploadIsRejected: false,
-+ // PKCS#12 Password options
-+ pkcs12PinMethod: "noPassword",
-+ pkcs12PinFile: "",
-+ pkcs12PinText: "",
-+ forceCertAdd: false,
- };
-
- // File Upload functions
-@@ -191,6 +199,11 @@ export class CertificateManagement extends React.Component {
- this.onRadioChange = this.onRadioChange.bind(this);
- this.validateCertText = this.validateCertText.bind(this);
- this.getCertFiles = this.getCertFiles.bind(this);
-+ // PKCS#12 Password handlers
-+ this.onPkcs12PinMethodChange = this.onPkcs12PinMethodChange.bind(this);
-+ this.onPkcs12PinFileChange = this.onPkcs12PinFileChange.bind(this);
-+ this.onPkcs12PinTextChange = this.onPkcs12PinTextChange.bind(this);
-+ this.onForceCertAddChange = this.onForceCertAddChange.bind(this);
- }
-
- componentDidMount () {
-@@ -211,12 +224,20 @@ export class CertificateManagement extends React.Component {
- certRadioUpload: true,
- isSelectCertOpen: false,
- modalSpinning: false,
-+ pkcs12PinMethod: "noPassword",
-+ pkcs12PinFile: "",
-+ pkcs12PinText: "",
-+ forceCertAdd: false,
- });
- }
-
- closeAddModal () {
- this.setState({
- showAddModal: false,
-+ pkcs12PinMethod: "noPassword",
-+ pkcs12PinFile: "",
-+ pkcs12PinText: "",
-+ forceCertAdd: false,
- });
- }
-
-@@ -234,12 +255,20 @@ export class CertificateManagement extends React.Component {
- certRadioUpload: true,
- isSelectCertOpen: false,
- modalSpinning: false,
-+ pkcs12PinMethod: "noPassword",
-+ pkcs12PinFile: "",
-+ pkcs12PinText: "",
-+ forceCertAdd: false,
- });
- }
-
- closeAddCAModal () {
- this.setState({
- showAddCAModal: false,
-+ pkcs12PinMethod: "noPassword",
-+ pkcs12PinFile: "",
-+ pkcs12PinText: "",
-+ forceCertAdd: false,
- });
- }
-
-@@ -263,6 +292,42 @@ export class CertificateManagement extends React.Component {
- });
- }
-
-+ onPkcs12PinMethodChange(e, _) {
-+ // Handle PKCS#12 password method selection
-+ let pkcs12PinMethod = "";
-+ if (e.target.id === "pkcs12PinRadioStdin") {
-+ pkcs12PinMethod = "stdin";
-+ } else if (e.target.id === "pkcs12PinRadioFile") {
-+ pkcs12PinMethod = "file";
-+ } else if (e.target.id === "noPasswordRadio") {
-+ pkcs12PinMethod = "noPassword";
-+ }
-+ // Clear other password fields when method changes
-+ this.setState({
-+ pkcs12PinMethod,
-+ pkcs12PinFile: "",
-+ pkcs12PinText: "",
-+ });
-+ }
-+
-+ onPkcs12PinFileChange(value) {
-+ this.setState({
-+ pkcs12PinFile: value
-+ });
-+ }
-+
-+ onPkcs12PinTextChange(value) {
-+ this.setState({
-+ pkcs12PinText: value
-+ });
-+ }
-+
-+ onForceCertAddChange(checked) {
-+ this.setState({
-+ forceCertAdd: checked
-+ });
-+ }
-+
- showAddCSRModal () {
- this.setState({
- showAddCSRModal: true,
-@@ -468,43 +533,112 @@ export class CertificateManagement extends React.Component {
- "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket",
- "security", certType, "add", "--name=" + this.state.certName, "--file=" + certFile
- ];
-+
-+ // Add PKCS#12 password options
-+ if (this.state.pkcs12PinMethod === "file" && this.state.pkcs12PinFile !== "") {
-+ cmd.push("--pkcs12-pin-path=" + this.state.pkcs12PinFile);
-+ } else if (this.state.pkcs12PinMethod === "stdin") {
-+ cmd.push("--pkcs12-pin-stdin");
-+ }
-+
-+ // Add force flag
-+ if (this.state.forceCertAdd) {
-+ cmd.push("--do-it");
-+ }
-+
- log_cmd("addCert", "Adding cert (tmp): ", cmd);
-- cockpit
-- .spawn(cmd, { superuser: true, err: "message" })
-- .done(() => {
-- this.deleteTmpCert(certFile);
-- this.reloadCACerts();
-- this.setState({
-- showAddModal: false,
-- modalSpinning: false,
-- loading: false,
-+
-+ // Handle stdin method with PTY spawn
-+ if (this.state.pkcs12PinMethod === "stdin") {
-+ let buffer = "";
-+ const proc = cockpit.spawn(cmd, { pty: true, environ: ["LC_ALL=C"], superuser: true, err: "message" });
-+ proc
-+ .done(() => {
-+ this.deleteTmpCert(certFile);
-+ this.reloadCACerts();
-+ this.setState({
-+ showAddModal: false,
-+ modalSpinning: false,
-+ loading: false,
-+ });
-+ this.reloadOrphanKeys();
-+ this.props.addNotification(
-+ "success",
-+ _("Successfully added certificate")
-+ );
-+ this.closeAddCAModal();
-+ this.closeAddModal();
-+ })
-+ .fail(err => {
-+ let msg = _("Unknown error");
-+ try {
-+ const errMsg = buffer ? JSON.parse(buffer) : JSON.parse(err);
-+ msg = errMsg.desc || errMsg.message || buffer || err;
-+ if ('info' in errMsg) {
-+ msg = errMsg.desc + " - " + errMsg.info;
-+ }
-+ } catch (e) {
-+ // If parsing fails, use buffer or error directly
-+ msg = buffer || err || _("Unknown error");
-+ }
-+ this.deleteTmpCert(certFile);
-+ this.closeAddCAModal();
-+ this.closeAddModal();
-+ this.setState({
-+ modalSpinning: false,
-+ loading: false,
-+ });
-+ this.props.addNotification(
-+ "error",
-+ cockpit.format(_("Error adding certificate - $0"), msg)
-+ );
-+ })
-+ .stream(data => {
-+ buffer += data;
-+ const lines = buffer.split("\n");
-+ const last_line = lines[lines.length - 1].toLowerCase();
-+ if (last_line.includes("password") || last_line.includes("pin")) {
-+ proc.input(this.state.pkcs12PinText + "\n", true);
-+ }
- });
-- this.reloadOrphanKeys();
-- this.props.addNotification(
-- "success",
-- _("Successfully added certificate")
-- );
-- this.closeAddCAModal();
-- this.closeAddModal();
-- })
-- .fail(err => {
-- const errMsg = JSON.parse(err);
-- let msg = errMsg.desc;
-- if ('info' in errMsg) {
-- msg = errMsg.desc + " - " + errMsg.info;
-- }
-- this.deleteTmpCert(certFile);
-- this.closeAddCAModal();
-- this.closeAddModal();
-- this.setState({
-- modalSpinning: false,
-- loading: false,
-+ } else {
-+ cockpit
-+ .spawn(cmd, { superuser: true, err: "message" })
-+ .done(() => {
-+ this.deleteTmpCert(certFile);
-+ this.reloadCACerts();
-+ this.setState({
-+ showAddModal: false,
-+ modalSpinning: false,
-+ loading: false,
-+ });
-+ this.reloadOrphanKeys();
-+ this.props.addNotification(
-+ "success",
-+ _("Successfully added certificate")
-+ );
-+ this.closeAddCAModal();
-+ this.closeAddModal();
-+ })
-+ .fail(err => {
-+ const errMsg = JSON.parse(err);
-+ let msg = errMsg.desc;
-+ if ('info' in errMsg) {
-+ msg = errMsg.desc + " - " + errMsg.info;
-+ }
-+ this.deleteTmpCert(certFile);
-+ this.closeAddCAModal();
-+ this.closeAddModal();
-+ this.setState({
-+ modalSpinning: false,
-+ loading: false,
-+ });
-+ this.props.addNotification(
-+ "error",
-+ cockpit.format(_("Error adding certificate - $0"), msg)
-+ );
- });
-- this.props.addNotification(
-- "error",
-- cockpit.format(_("Error adding certificate - $0"), msg)
-- );
-- });
-+ }
- })
- .fail(err => {
- this.setState({
-@@ -529,42 +663,110 @@ export class CertificateManagement extends React.Component {
- // certRadioSelect
- cmd.push("--file=" + this.props.certDir + "/" + this.state.selectCertName);
- }
-+
-+ // Add PKCS#12 password options
-+ if (this.state.pkcs12PinMethod === "file" && this.state.pkcs12PinFile !== "") {
-+ cmd.push("--pkcs12-pin-path=" + this.state.pkcs12PinFile);
-+ } else if (this.state.pkcs12PinMethod === "stdin") {
-+ cmd.push("--pkcs12-pin-stdin");
-+ }
-+
-+ // Add force flag
-+ if (this.state.forceCertAdd) {
-+ cmd.push("--do-it");
-+ }
-+
- log_cmd("addCert", "Adding cert: ", cmd);
-- cockpit
-- .spawn(cmd, { superuser: true, err: "message" })
-- .done(() => {
-- this.reloadCACerts();
-- this.closeAddCAModal();
-- this.closeAddModal();
-- this.setState({
-- showAddModal: false,
-- certFile: '',
-- certName: '',
-- modalSpinning: false
-+
-+ // Handle stdin method with PTY spawn
-+ if (this.state.pkcs12PinMethod === "stdin") {
-+ let buffer = "";
-+ const proc = cockpit.spawn(cmd, { pty: true, environ: ["LC_ALL=C"], superuser: true, err: "message" });
-+ proc
-+ .done(() => {
-+ this.reloadCACerts();
-+ this.closeAddCAModal();
-+ this.closeAddModal();
-+ this.setState({
-+ showAddModal: false,
-+ certFile: '',
-+ certName: '',
-+ modalSpinning: false
-+ });
-+ this.reloadOrphanKeys();
-+ this.props.addNotification(
-+ "success",
-+ _("Successfully added certificate")
-+ );
-+ })
-+ .fail(err => {
-+ let msg = _("Unknown error");
-+ try {
-+ const errMsg = buffer ? JSON.parse(buffer) : JSON.parse(err);
-+ msg = errMsg.desc || errMsg.message || buffer || err;
-+ if ('info' in errMsg) {
-+ msg = errMsg.desc + " - " + errMsg.info;
-+ }
-+ } catch (e) {
-+ // If parsing fails, use buffer or error directly
-+ msg = buffer || err || _("Unknown error");
-+ }
-+ this.closeAddCAModal();
-+ this.closeAddModal();
-+ this.setState({
-+ modalSpinning: false,
-+ loading: false,
-+ });
-+ this.props.addNotification(
-+ "error",
-+ cockpit.format(_("Error adding certificate - $0"), msg)
-+ );
-+ })
-+ .stream(data => {
-+ buffer += data;
-+ const lines = buffer.split("\n");
-+ const last_line = lines[lines.length - 1].toLowerCase();
-+ if (last_line.includes("password") || last_line.includes("pin")) {
-+ proc.input(this.state.pkcs12PinText + "\n", true);
-+ }
- });
-- this.reloadOrphanKeys();
-- this.props.addNotification(
-- "success",
-- _("Successfully added certificate")
-- );
-- })
-- .fail(err => {
-- const errMsg = JSON.parse(err);
-- let msg = errMsg.desc;
-- if ('info' in errMsg) {
-- msg = errMsg.desc + " - " + errMsg.info;
-- }
-- this.closeAddCAModal();
-- this.closeAddModal();
-- this.setState({
-- modalSpinning: false,
-- loading: false,
-+ } else {
-+ cockpit
-+ .spawn(cmd, { superuser: true, err: "message" })
-+ .done(() => {
-+ this.reloadCACerts();
-+ this.closeAddCAModal();
-+ this.closeAddModal();
-+ this.setState({
-+ showAddModal: false,
-+ certFile: '',
-+ certName: '',
-+ modalSpinning: false
-+ });
-+ this.reloadOrphanKeys();
-+ this.props.addNotification(
-+ "success",
-+ _("Successfully added certificate")
-+ );
-+ })
-+ .fail(err => {
-+ const errMsg = JSON.parse(err);
-+ let msg = errMsg.desc;
-+ if ('info' in errMsg) {
-+ msg = errMsg.desc + " - " + errMsg.info;
-+ }
-+ this.closeAddCAModal();
-+ this.closeAddModal();
-+ this.setState({
-+ modalSpinning: false,
-+ loading: false,
-+ });
-+ this.props.addNotification(
-+ "error",
-+ cockpit.format(_("Error adding certificate - $0"), msg)
-+ );
- });
-- this.props.addNotification(
-- "error",
-- cockpit.format(_("Error adding certificate - $0"), msg)
-- );
-- });
-+ }
- }
- }
-
-@@ -1088,7 +1290,8 @@ export class CertificateManagement extends React.Component {
- ServerCerts: certs,
- loading: false,
- tableKey: key,
-- showConfirmCAChange: false
-+ showConfirmCAChange: false,
-+ certNicknames: certNames,
- }, this.getCertFiles);
- })
- .fail(err => {
-@@ -1180,9 +1383,14 @@ export class CertificateManagement extends React.Component {
- .spawn(cmd, { superuser: true, err: "message" })
- .done(content => {
- const certs = JSON.parse(content);
-+ const certNames = [];
-+ for (const cert of certs) {
-+ certNames.push(cert.attrs.nickname);
-+ }
- this.setState({
- CACerts: certs,
-- loading: false
-+ loading: false,
-+ CACertNicknames: certNames,
- }, this.reloadCerts);
- })
- .fail(err => {
-@@ -1311,6 +1519,7 @@ export class CertificateManagement extends React.Component {
- spinning={this.state.modalSpinning}
- />
- <SecurityAddCertModal
-+ key={"addCert-" + this.state.showAddModal}
- showModal={this.state.showAddModal}
- closeHandler={this.closeAddModal}
- handleChange={this.onChange}
-@@ -1319,6 +1528,8 @@ export class CertificateManagement extends React.Component {
- certFile={this.state.certFile}
- certName={this.state.certName}
- certNames={this.state.availCertNames}
-+ certNicknames={this.state.certNicknames}
-+ CACertNicknames={this.state.CACertNicknames}
- selectCertName={this.state.selectCertName}
- isSelectCertOpen={this.state.isSelectCertOpen}
- handleCertSelect={this.onCertSelect}
-@@ -1336,11 +1547,18 @@ export class CertificateManagement extends React.Component {
- handleFileReadStarted={this.onFileReadStarted}
- handleFileReadFinished={this.onFileReadFinished}
- handleClear={this.onClear}
-- handleFileRejected={this.state.uploadIsRejected}
-+ handleFileRejected={this.handleFileRejected}
-+ pkcs12PinMethod={this.state.pkcs12PinMethod}
-+ pkcs12PinFile={this.state.pkcs12PinFile}
-+ pkcs12PinText={this.state.pkcs12PinText}
-+ forceCertAdd={this.state.forceCertAdd}
-+ handlePkcs12PinMethodChange={this.onPkcs12PinMethodChange}
-+ handlePkcs12PinFileChange={this.onPkcs12PinFileChange}
-+ handlePkcs12PinTextChange={this.onPkcs12PinTextChange}
-+ handleForceCertAddChange={this.onForceCertAddChange}
- />
-- <SecurityAddCertModal
-+ <SecurityAddCACertModal
- showModal={this.state.showAddCAModal}
-- isCACert
- closeHandler={this.closeAddCAModal}
- handleChange={this.onChange}
- saveHandler={this.addCert}
-@@ -1348,6 +1566,8 @@ export class CertificateManagement extends React.Component {
- certFile={this.state.certFile}
- certName={this.state.certName}
- certNames={this.state.availCertNames}
-+ certNicknames={this.state.certNicknames}
-+ CACertNicknames={this.state.CACertNicknames}
- selectCertName={this.state.selectCertName}
- isSelectCertOpen={this.state.isSelectCertOpen}
- handleCertSelect={this.onCertSelect}
-@@ -1365,7 +1585,7 @@ export class CertificateManagement extends React.Component {
- handleFileReadStarted={this.onFileReadStarted}
- handleFileReadFinished={this.onFileReadFinished}
- handleClear={this.onClear}
-- handleFileRejected={this.state.uploadIsRejected}
-+ handleFileRejected={this.handleFileRejected}
- />
- <SecurityAddCSRModal
- showModal={this.state.showAddCSRModal}
-diff --git a/src/cockpit/389-console/src/lib/security/securityModals.jsx b/src/cockpit/389-console/src/lib/security/securityModals.jsx
-index a42aa50d6..4d6631fd2 100644
---- a/src/cockpit/389-console/src/lib/security/securityModals.jsx
-+++ b/src/cockpit/389-console/src/lib/security/securityModals.jsx
-@@ -6,6 +6,7 @@ import {
- ClipboardCopy,
- ClipboardCopyVariant,
- Divider,
-+ ExpandableSection,
- FileUpload,
- Form,
- FormSelect,
-@@ -22,12 +23,13 @@ import {
- TextVariants,
- TextInput,
- Tooltip,
-+ TooltipPosition,
- ValidatedOptions
- } from '@patternfly/react-core';
- import TypeaheadSelect from "../../dsBasicComponents.jsx";
- import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons/dist/js/icons/outlined-question-circle-icon';
- import PropTypes from "prop-types";
--import { bad_file_name, validHostname } from "../tools.jsx";
-+import { bad_file_name, validHostname, file_is_path } from "../tools.jsx";
-
- const _ = cockpit.gettext;
-
-@@ -124,6 +126,20 @@ export class ExportCertModal extends React.Component {
- }
-
- export class SecurityAddCertModal extends React.Component {
-+ constructor(props) {
-+ super(props);
-+ this.state = {
-+ isPasswordSectionExpanded: false
-+ };
-+ this.handlePasswordSectionToggle = this.handlePasswordSectionToggle.bind(this);
-+ }
-+
-+ handlePasswordSectionToggle(event, isExpanded) {
-+ this.setState({
-+ isPasswordSectionExpanded: isExpanded
-+ });
-+ }
-+
- render() {
- const {
- showModal,
-@@ -139,6 +155,8 @@ export class SecurityAddCertModal extends React.Component {
- handleRadioChange,
- badCertText,
- certNames,
-+ certNicknames,
-+ CACertNicknames,
- // Select server cert
- handleCertSelect,
- selectCertName,
-@@ -153,7 +171,15 @@ export class SecurityAddCertModal extends React.Component {
- handleFileReadFinished,
- handleClear,
- handleFileRejected,
-- isCACert,
-+ // PKCS#12 Password options
-+ pkcs12PinMethod,
-+ pkcs12PinFile,
-+ pkcs12PinText,
-+ forceCertAdd,
-+ handlePkcs12PinMethodChange,
-+ handlePkcs12PinFileChange,
-+ handlePkcs12PinTextChange,
-+ handleForceCertAddChange,
- } = this.props;
-
- let saveBtnName = _("Add Certificate");
-@@ -179,11 +205,358 @@ export class SecurityAddCertModal extends React.Component {
-
- let title = _("Add Server Certificate");
- let desc = _("Add a Server Certificate to the security database.");
-- if (isCACert) {
-- title = _("Add Certificate Authority");
-- desc = _("Add a CA Certificate to the security database.");
-+
-+ let selectValidated = ValidatedOptions.default;
-+ if (certRadioSelect && certNames.length === 0) {
-+ selectValidated = ValidatedOptions.error;
-+ }
-+
-+ return (
-+ <Modal
-+ variant={ModalVariant.medium}
-+ title={title}
-+ aria-labelledby="ds-modal"
-+ isOpen={showModal}
-+ onClose={closeHandler}
-+ actions={[
-+ <Button
-+ key="confirm"
-+ variant="primary"
-+ onClick={() => {
-+ saveHandler(false);
-+ }}
-+ isLoading={spinning}
-+ spinnerAriaValueText={spinning ? _("Saving") : undefined}
-+ {...extraPrimaryProps}
-+ isDisabled={
-+ certName === "" || (certRadioFile && certFile === "") ||
-+ (certRadioUpload && (uploadValue === "" || badCertText)) ||
-+ (certRadioSelect && certNames.length === 0) ||
-+ (pkcs12PinMethod === "file" && (pkcs12PinFile === "" || !file_is_path(pkcs12PinFile))) ||
-+ (pkcs12PinMethod === "stdin" && pkcs12PinText === "") ||
-+ certNicknames.includes(certName) ||
-+ CACertNicknames.includes(certName)
-+ }
-+ >
-+ {saveBtnName}
-+ </Button>,
-+ <Button key="cancel" variant="link" onClick={closeHandler}>
-+ {_("Cancel")}
-+ </Button>
-+ ]}
-+ >
-+ <Form isHorizontal autoComplete="off">
-+ <TextContent>
-+ <Text component={TextVariants.h4}>
-+ {desc}
-+ </Text>
-+ </TextContent>
-+ <Grid
-+ className="ds-margin-top-sm"
-+ title={_("Enter name/nickname of the certificate")}
-+ >
-+ <GridItem className="ds-label" span={3}>
-+ {_("Certificate Nickname")}
-+ </GridItem>
-+ <GridItem span={9}>
-+ <TextInput
-+ type="text"
-+ id="certName"
-+ aria-describedby="horizontal-form-name-helper"
-+ name="certName"
-+ onChange={(e, str) => {
-+ handleChange(e);
-+ }}
-+ validated={
-+ certName === "" ||
-+ certNicknames.includes(certName) ||
-+ CACertNicknames.includes(certName) ? ValidatedOptions.error : ValidatedOptions.default}
-+ />
-+ {(certNicknames.includes(certName) || CACertNicknames.includes(certName)) && (
-+ <HelperText>
-+ <HelperTextItem variant="error">
-+ {_("Please use a unique certificate nickname")}
-+ </HelperTextItem>
-+ </HelperText>
-+ )}
-+ </GridItem>
-+ </Grid>
-+ <ExpandableSection
-+ toggleText={this.state.isPasswordSectionExpanded ? _("Hide PKCS#12 Password Options") : _("Show PKCS#12 Password Options")}
-+ onToggle={this.handlePasswordSectionToggle}
-+ isExpanded={this.state.isPasswordSectionExpanded}
-+ >
-+ <div className="ds-indent-lg">
-+ <TextContent className="ds-margin-top">
-+ <Text component={TextVariants.p}>
-+ {_("These password settings are only used for PKCS#12 password protected certificates. These settings are ignored for other certificate types.")}
-+ </Text>
-+ </TextContent>
-+ <Grid className="ds-margin-top-lg">
-+ <GridItem span={12}>
-+ <Radio
-+ id="noPasswordRadio"
-+ label={_("Non-PKCS#12 Certificate")}
-+ name="pkcs12PinMethod"
-+ isChecked={pkcs12PinMethod === "noPassword"}
-+ onChange={handlePkcs12PinMethodChange}
-+ />
-+ <div
-+ className="ds-margin-top"
-+ title={_("Password will be read from stdin when prompted by the command")}
-+ >
-+ <Radio
-+ id="pkcs12PinRadioStdin"
-+ label={_("Password")}
-+ name="pkcs12PinMethod"
-+ isChecked={pkcs12PinMethod === "stdin"}
-+ onChange={handlePkcs12PinMethodChange}
-+ />
-+ </div>
-+ <div className="ds-margin-top ds-radio-indent">
-+ <TextInput
-+ type="password"
-+ id="pkcs12PinTextStdin"
-+ aria-describedby="pkcs12-pin-stdin-helper"
-+ name="pkcs12PinText"
-+ value={pkcs12PinText}
-+ onChange={(e, value) => {
-+ handlePkcs12PinTextChange(value);
-+ }}
-+ placeholder={_("Enter password to send via stdin")}
-+ validated={pkcs12PinMethod === "stdin" && pkcs12PinText === "" ? ValidatedOptions.error : ValidatedOptions.default}
-+ isDisabled={pkcs12PinMethod !== "stdin"}
-+ />
-+ </div>
-+ <div
-+ className="ds-margin-top"
-+ title={_("Read password from a file on the server")}
-+ >
-+ <Radio
-+ id="pkcs12PinRadioFile"
-+ className="ds-margin-top-lg"
-+ label={_("Read password from file")}
-+ name="pkcs12PinMethod"
-+ isChecked={pkcs12PinMethod === "file"}
-+ onChange={handlePkcs12PinMethodChange}
-+ />
-+ </div>
-+ <div className="ds-margin-top ds-radio-indent">
-+ <TextInput
-+ type="text"
-+ id="pkcs12PinFile"
-+ aria-describedby="pkcs12-pin-file-helper"
-+ name="pkcs12PinFile"
-+ value={pkcs12PinFile}
-+ onChange={(e, value) => {
-+ handlePkcs12PinFileChange(value);
-+ }}
-+ placeholder={_("Enter full path to password file")}
-+ isDisabled={pkcs12PinMethod !== "file"}
-+ validated={
-+ pkcs12PinMethod === "file" &&
-+ (pkcs12PinFile === "" || !file_is_path(pkcs12PinFile))
-+ ? ValidatedOptions.error
-+ : ValidatedOptions.default
-+ }
-+ />
-+ {pkcs12PinMethod === "file" && pkcs12PinFile !== "" && !file_is_path(pkcs12PinFile) && (
-+ <HelperText>
-+ <HelperTextItem variant="error">
-+ {_("Please enter a valid file path (must start with '/' and not end with '/')")}
-+ </HelperTextItem>
-+ </HelperText>
-+ )}
-+ </div>
-+ <div className="ds-margin-top-xxlg">
-+ <Tooltip
-+ position={TooltipPosition.bottomStart}
-+ content={
-+ <div>
-+ {_("Force certificate addition without validation. This bypasses certificate chain validation checks.")}
-+ </div>
-+ }
-+ >
-+ <Checkbox
-+ id="forceCertAdd"
-+ className="ds-margin-top-lg"
-+ label={_("Skip certificate verification")}
-+ isChecked={forceCertAdd}
-+ onChange={(e, checked) => {
-+ handleForceCertAddChange(checked);
-+ }}
-+ />
-+ </Tooltip>
-+ </div>
-+ </GridItem>
-+ </Grid>
-+ </div>
-+ </ExpandableSection>
-+ <Grid>
-+ <GridItem span={12}>
-+ <div title={_("Upload the contents of a PEM file from the client's system.")}>
-+ <Radio
-+ id="certRadioUpload"
-+ label={certTextLabel}
-+ name="certChoice"
-+ onChange={handleRadioChange}
-+ isChecked={certRadioUpload}
-+ />
-+ </div>
-+ <div className={certRadioUpload ? "ds-margin-top ds-radio-indent" : "ds-margin-top ds-radio-indent ds-disabled"}>
-+ <FileUpload
-+ id="uploadPEMFile"
-+ type="text"
-+ value={uploadValue}
-+ filename={uploadFileName}
-+ filenamePlaceholder={_("Drag and drop a file, or upload one")}
-+ onFileInputChange={handleFileInputChange}
-+ onDataChange={handleTextOrDataChange}
-+ onTextChange={handleTextOrDataChange}
-+ onReadStarted={handleFileReadStarted}
-+ onReadFinished={handleFileReadFinished}
-+ onClearClick={handleClear}
-+ isLoading={uploadIsLoading}
-+ dropzoneProps={{
-+ accept: '.pem',
-+ onDropRejected: handleFileRejected
-+ }}
-+ validated={
-+ uploadIsRejected ||
-+ (certRadioUpload && uploadValue === "") ||
-+ (certRadioUpload && badCertText)
-+ ? 'error'
-+ : 'default'
-+ }
-+ browseButtonText={_("Upload PEM File")}
-+ />
-+ </div>
-+ <div title={_("Choose a certificate from the server's certificate directory")}>
-+ <Radio
-+ id="certRadioSelect"
-+ className="ds-margin-top-lg"
-+ label={_("Choose Certificate From Server")}
-+ name="certChoice"
-+ isChecked={certRadioSelect}
-+ onChange={handleRadioChange}
-+ />
-+ </div>
-+ <div className={certRadioSelect ? "ds-margin-top ds-radio-indent" : "ds-margin-top ds-radio-indent ds-disabled"}>
-+ <FormSelect
-+ value={selectCertName}
-+ id="selectCertName"
-+ onChange={(e, str) => {
-+ handleCertSelect(str);
-+ }}
-+ aria-label="FormSelect Input"
-+ className="ds-cert-select"
-+ validated={selectValidated}
-+ >
-+ {certNames.length === 0 &&
-+ <FormSelectOption
-+ key="none"
-+ value=""
-+ label={_("No certificates present")}
-+ isDisabled
-+ isPlaceholder
-+ />}
-+ {certNames.length > 0 && certNames.map((option, index) => (
-+ <FormSelectOption
-+ key={index}
-+ value={option}
-+ label={option}
-+ />
-+ ))}
-+ </FormSelect>
-+ </div>
-+ <div title={_("Enter the full path on the server to and including the certificate file name")}>
-+ <Radio
-+ id="certRadioFile"
-+ className="ds-margin-top-lg"
-+ label={_("Certificate File Location")}
-+ name="certChoice"
-+ isChecked={certRadioFile}
-+ onChange={handleRadioChange}
-+ />
-+ </div>
-+ <div className={certRadioFile ? "ds-margin-top ds-radio-indent" : "ds-margin-top ds-radio-indent ds-disabled"}>
-+ <TextInput
-+ type="text"
-+ id="certFile"
-+ aria-describedby="horizontal-form-name-helper"
-+ name="certFile"
-+ onChange={(e, value) => {
-+ handleChange(e);
-+ }}
-+ validated={certRadioFile && certFile === "" ? ValidatedOptions.error : ValidatedOptions.default}
-+ />
-+ </div>
-+ </GridItem>
-+ </Grid>
-+ </Form>
-+ </Modal>
-+ );
-+ }
-+}
-+
-+export class SecurityAddCACertModal extends React.Component {
-+ render() {
-+ const {
-+ showModal,
-+ closeHandler,
-+ handleChange,
-+ saveHandler,
-+ spinning,
-+ certName,
-+ certFile,
-+ certRadioFile,
-+ certRadioSelect,
-+ certRadioUpload,
-+ handleRadioChange,
-+ badCertText,
-+ certNames,
-+ certNicknames,
-+ CACertNicknames,
-+ // Select server cert
-+ handleCertSelect,
-+ selectCertName,
-+ // File Upload
-+ uploadValue,
-+ uploadFileName,
-+ uploadIsLoading,
-+ uploadIsRejected,
-+ handleFileInputChange,
-+ handleTextOrDataChange,
-+ handleFileReadStarted,
-+ handleFileReadFinished,
-+ handleClear,
-+ handleFileRejected,
-+ } = this.props;
-+
-+ let saveBtnName = _("Add Certificate");
-+ const extraPrimaryProps = {};
-+ if (spinning) {
-+ saveBtnName = _("Adding Certificate ...");
-+ extraPrimaryProps.spinnerAriaValueText = _("Saving");
- }
-
-+ const certTextLabel = (
-+ <div>
-+ <Tooltip
-+ content={
-+ <div>
-+ {_("Paste the base64 encoded certificate that starts with \"-----BEGIN CERTIFICATE-----\" and ends with \"-----END CERTIFICATE-----\". Make sure there are no special carriage return characters after each line.")}
-+ </div>
-+ }
-+ >
-+ <div>{_("Upload PEM File, or Certificate Text")} <OutlinedQuestionCircleIcon /></div>
-+ </Tooltip>
-+ </div>
-+ );
-+
-+
-+ let title = _("Add Certificate Authority");
-+ let desc = _("Add a CA Certificate to the security database.");
- let selectValidated = ValidatedOptions.default;
- if (certRadioSelect && certNames.length === 0) {
- selectValidated = ValidatedOptions.error;
-@@ -201,7 +574,7 @@ export class SecurityAddCertModal extends React.Component {
- key="confirm"
- variant="primary"
- onClick={() => {
-- saveHandler(isCACert);
-+ saveHandler(true);
- }}
- isLoading={spinning}
- spinnerAriaValueText={spinning ? _("Saving") : undefined}
-@@ -209,7 +582,9 @@ export class SecurityAddCertModal extends React.Component {
- isDisabled={
- certName === "" || (certRadioFile && certFile === "") ||
- (certRadioUpload && (uploadValue === "" || badCertText)) ||
-- (certRadioSelect && certNames.length === 0)
-+ (certRadioSelect && certNames.length === 0) ||
-+ certNicknames.includes(certName) ||
-+ CACertNicknames.includes(certName)
- }
- >
- {saveBtnName}
-@@ -226,7 +601,7 @@ export class SecurityAddCertModal extends React.Component {
- </Text>
- </TextContent>
- <Grid
-- className="ds-margin-top-lg"
-+ className="ds-margin-top-sm"
- title={_("Enter name/nickname of the certificate")}
- >
- <GridItem className="ds-label" span={3}>
-@@ -241,11 +616,21 @@ export class SecurityAddCertModal extends React.Component {
- onChange={(e, str) => {
- handleChange(e);
- }}
-- validated={certName === "" ? ValidatedOptions.error : ValidatedOptions.default}
-+ validated={
-+ certName === "" ||
-+ certNicknames.includes(certName) ||
-+ CACertNicknames.includes(certName) ? ValidatedOptions.error : ValidatedOptions.default}
- />
-+ {(certNicknames.includes(certName) || CACertNicknames.includes(certName)) && (
-+ <HelperText>
-+ <HelperTextItem variant="error">
-+ {_("Please use a unique certificate nickname")}
-+ </HelperTextItem>
-+ </HelperText>
-+ )}
- </GridItem>
- </Grid>
-- <Grid className="ds-margin-top">
-+ <Grid className="ds-margin-top-lg">
- <GridItem span={12}>
- <div title={_("Upload the contents of a PEM file from the client's system.")}>
- <Radio
-@@ -298,7 +683,9 @@ export class SecurityAddCertModal extends React.Component {
- <FormSelect
- value={selectCertName}
- id="selectCertName"
-- onChange={handleCertSelect}
-+ onChange={(e, str) => {
-+ handleCertSelect(str);
-+ }}
- aria-label="FormSelect Input"
- className="ds-cert-select"
- validated={selectValidated}
-@@ -1103,6 +1490,22 @@ SecurityAddCertModal.defaultProps = {
- error: {},
- };
-
-+SecurityAddCACertModal.propTypes = {
-+ showModal: PropTypes.bool,
-+ closeHandler: PropTypes.func,
-+ handleChange: PropTypes.func,
-+ saveHandler: PropTypes.func,
-+ spinning: PropTypes.bool,
-+ error: PropTypes.object,
-+};
-+
-+SecurityAddCACertModal.defaultProps = {
-+ showModal: false,
-+ spinning: false,
-+ saveHandler: () => {},
-+ error: {},
-+};
-+
- SecurityAddCSRModal.propTypes = {
- showModal: PropTypes.bool,
- closeHandler: PropTypes.func,
-diff --git a/src/cockpit/389-console/src/lib/security/securityTables.jsx b/src/cockpit/389-console/src/lib/security/securityTables.jsx
-index fce4cb04e..a6c64e6c1 100644
---- a/src/cockpit/389-console/src/lib/security/securityTables.jsx
-+++ b/src/cockpit/389-console/src/lib/security/securityTables.jsx
-@@ -4,7 +4,6 @@ import {
- Grid,
- GridItem,
- Pagination,
-- PaginationVariant,
- SearchInput,
- Tooltip,
- } from '@patternfly/react-core';
-@@ -94,10 +93,10 @@ class KeyTable extends React.Component {
- ];
-
- handleSort(_event, index, direction) {
-- const sortedRows = [...this.state.rows].sort((a, b) =>
-+ const sortedRows = [...this.state.rows].sort((a, b) =>
- (a[index] < b[index] ? -1 : a[index] > b[index] ? 1 : 0)
- );
--
-+
- this.setState({
- sortBy: {
- index,
-@@ -127,7 +126,7 @@ class KeyTable extends React.Component {
- >
- <a className="ds-font-size-sm">{_("What is an orphan key?")}</a>
- </Tooltip>
-- <Table
-+ <Table
- className="ds-margin-top"
- aria-label="orph key table"
- variant="compact"
-@@ -135,7 +134,7 @@ class KeyTable extends React.Component {
- <Thead>
- <Tr>
- {columns.map((column, idx) => (
-- <Th
-+ <Th
- key={idx}
- sort={column.sortable ? {
- sortBy,
-@@ -163,7 +162,7 @@ class KeyTable extends React.Component {
- )}
- {hasRows && (
- <Td isActionCell>
-- <ActionsColumn
-+ <ActionsColumn
- items={this.getActionsForRow(row)}
- />
- </Td>
-@@ -224,7 +223,7 @@ class CSRTable extends React.Component {
- }
-
- handleSort(_event, index, direction) {
-- const sortedRows = [...this.state.rows].sort((a, b) =>
-+ const sortedRows = [...this.state.rows].sort((a, b) =>
- (a[index] < b[index] ? -1 : a[index] > b[index] ? 1 : 0)
- );
- this.setState({
-@@ -316,7 +315,7 @@ class CSRTable extends React.Component {
- onClear={(evt) => this.handleSearchChange(evt, '')}
- />
- }
-- <Table
-+ <Table
- className="ds-margin-top"
- aria-label="csr table"
- variant="compact"
-@@ -324,7 +323,7 @@ class CSRTable extends React.Component {
- <Thead>
- <Tr>
- {columns.map((column, idx) => (
-- <Th
-+ <Th
- key={idx}
- sort={column.sortable ? {
- sortBy,
-@@ -352,7 +351,7 @@ class CSRTable extends React.Component {
- )}
- {hasRows && (
- <Td isActionCell>
-- <ActionsColumn
-+ <ActionsColumn
- items={this.getActionsForRow(row)}
- />
- </Td>
-@@ -418,41 +417,11 @@ class CertTable extends React.Component {
- }
-
- handleSort(_event, columnIndex, direction) {
-- const sorted_rows = [];
-- const rows = [];
-- let count = 0;
--
-- // Convert the rows pairings into a sortable array
-- for (let idx = 0; idx < this.state.rows.length; idx += 2) {
-- sorted_rows.push({
-- expandedRow: this.state.rows[idx + 1],
-- 1: this.state.rows[idx].cells[0].content,
-- 2: this.state.rows[idx].cells[1].content,
-- 3: this.state.rows[idx].cells[2].content,
-- issuer: this.state.rows[idx].issuer,
-- flags: this.state.rows[idx].flags
-- });
-- }
-+ const rows = [...this.state.rows];
-
-- sorted_rows.sort((a, b) => (a[columnIndex + 1] > b[columnIndex + 1]) ? 1 : -1);
-+ rows.sort((a, b) => (a.cells[columnIndex].content > b.cells[columnIndex].content) ? 1 : -1);
- if (direction !== SortByDirection.asc) {
-- sorted_rows.reverse();
-- }
--
-- for (const srow of sorted_rows) {
-- rows.push({
-- isOpen: false,
-- cells: [
-- { content: srow[1] },
-- { content: srow[2] },
-- { content: srow[3] }
-- ],
-- issuer: srow.issuer,
-- flags: srow.flags,
-- });
-- srow.expandedRow.parent = count;
-- rows.push(srow.expandedRow);
-- count += 2;
-+ rows.reverse();
- }
-
- this.setState({
-@@ -534,18 +503,16 @@ class CertTable extends React.Component {
- rows.push(
- {
- isOpen: false,
-- cells: [cert.attrs.nickname, cert.attrs.subject, cert.attrs.expires],
-+ cells: [
-+ { content: cert.attrs.nickname },
-+ { content: cert.attrs.subject },
-+ { content: cert.attrs.expires }
-+ ],
- issuer: cert.attrs.issuer,
- flags: cert.attrs.flags,
--
-- },
-- {
-- parent: count,
-- fullWidth: true,
-- cells: [{ title: this.getExpandedRow(cert.attrs.issuer, cert.attrs.flags) }]
-- },
-+ }
- );
-- count += 2;
-+ count += 1;
- }
-
- this.setState({
-@@ -587,7 +554,7 @@ class CertTable extends React.Component {
- onChange={this.handleSearchChange}
- onClear={(evt) => this.handleSearchChange(evt, '')}
- />}
-- <Table
-+ <Table
- aria-label="cert table"
- variant='compact'
- >
-@@ -626,7 +593,7 @@ class CertTable extends React.Component {
- </Td>
- ))}
- <Td isActionCell>
-- <ActionsColumn
-+ <ActionsColumn
- items={this.getActionsForRow(row)}
- />
- </Td>
-@@ -646,7 +613,7 @@ class CertTable extends React.Component {
- </Table>
- {hasRows &&
- <Pagination
-- itemCount={this.state.rows.length / 2}
-+ itemCount={this.state.rows.length}
- widgetId="pagination-options-menu-bottom"
- perPage={perPage}
- page={page}
-@@ -728,7 +695,7 @@ class CRLTable extends React.Component {
- if (direction !== SortByDirection.asc) {
- sorted_rows.reverse();
- }
--
-+
- for (const srow of sorted_rows) {
- rows.push({
- isOpen: false,
-@@ -806,14 +773,14 @@ class CRLTable extends React.Component {
- onChange={this.handleSearchChange}
- onClear={(evt) => this.handleSearchChange(evt, '')}
- />
-- <Table
-+ <Table
- aria-label="CRL Table"
- variant="compact"
- >
- <Thead>
- <Tr>
- {this.state.columns.map((column, idx) => (
-- <Th
-+ <Th
- key={idx}
- sort={column.sortable ? {
- sortBy: this.state.sortBy,
-@@ -836,7 +803,7 @@ class CRLTable extends React.Component {
- ))}
- {this.state.hasRows && (
- <Td isActionCell>
-- <ActionsColumn
-+ <ActionsColumn
- items={this.getActionsForRow(row)}
- />
- </Td>
-diff --git a/src/cockpit/389-console/src/security.jsx b/src/cockpit/389-console/src/security.jsx
-index c4504066b..3aead0449 100644
---- a/src/cockpit/389-console/src/security.jsx
-+++ b/src/cockpit/389-console/src/security.jsx
-@@ -60,6 +60,8 @@ export class Security extends React.Component {
- primaryCertName: '',
- serverCertNames: [],
- serverCerts: [],
-+ CACerts: [],
-+ CACertNames: [],
- isMinSSLOpen: false,
- isMaxSSLOpen: false,
- isClientAuthOpen: false,
-@@ -330,9 +332,14 @@ export class Security extends React.Component {
- .spawn(cmd, { superuser: true, err: "message" })
- .done(content => {
- const certs = JSON.parse(content);
-+ const certNames = [];
-+ for (const cert of certs) {
-+ certNames.push(cert.attrs.nickname);
-+ }
- this.setState(() => (
- {
- CACerts: certs,
-+ CACertNames: certNames,
- }), this.loadCSRs
- );
- })
-@@ -1226,6 +1233,9 @@ export class Security extends React.Component {
- ServerKeys={this.state.serverOrphanKeys}
- addNotification={this.props.addNotification}
- certDir={this.props.certDir}
-+ certNicknames={this.state.serverCertNames}
-+ CACertNicknames={this.state.CACertNames}
-+ reloadCerts={this.loadCerts}
- />
- </Tab>
- <Tab eventKey={2} title={<TabTitleText>{_("Cipher Preferences")}</TabTitleText>}>
-diff --git a/src/lib389/lib389/cli_conf/security.py b/src/lib389/lib389/cli_conf/security.py
-index 18e444c4c..bc3a6ddaf 100644
---- a/src/lib389/lib389/cli_conf/security.py
-+++ b/src/lib389/lib389/cli_conf/security.py
-@@ -86,9 +86,9 @@ def _security_generic_get(inst, basedn, log, args, attrs_map):
- val = ""
- result[props.attr.lower()] = val
- if args.json:
-- print(json.dumps({'type': 'list', 'items': result}, indent=4))
-+ log.info(json.dumps({'type': 'list', 'items': result}, indent=4))
- else:
-- print('\n'.join([f'{attr}: {value or ""}' for attr, value in result.items()]))
-+ log.info('\n'.join([f'{attr}: {value or ""}' for attr, value in result.items()]))
-
-
- def _security_generic_set(inst, basedn, log, args, attrs_map):
-@@ -228,10 +228,10 @@ def security_ciphers_set(inst, basedn, log, args):
- def security_ciphers_get(inst, basedn, log, args):
- enc = Encryption(inst)
- if args.json:
-- print({'type': 'list', 'items': enc.ciphers})
-+ log.info({'type': 'list', 'items': enc.ciphers})
- else:
- val = ','.join(enc.ciphers)
-- print(val if val != '' else '<undefined>')
-+ log.info(val if val != '' else '<undefined>')
-
-
- def security_ciphers_list(inst, basedn, log, args):
-@@ -247,12 +247,13 @@ def security_ciphers_list(inst, basedn, log, args):
- lst = enc.ciphers
-
- if args.json:
-- print(json.dumps({'type': 'list', 'items': lst}, indent=4))
-+ log.info(json.dumps({'type': 'list', 'items': lst}, indent=4))
- else:
- if lst == []:
- log.getChild('security').warn('List of ciphers is empty')
- else:
-- print(*lst, sep='\n')
-+ for item in lst:
-+ log.info(item)
-
-
- def security_disable_plaintext_port(inst, basedn, log, args, warn=True):
-@@ -312,7 +313,10 @@ def cert_list(inst, basedn, log, args):
- certmgr = CertManager(instance=inst)
- certs = certmgr.list_certs()
- if not certs:
-- log.info("No certificates found.")
-+ if args.json:
-+ log.info(json.dumps([], indent=4))
-+ else:
-+ log.info("No certificates found.")
- return
-
- if args.json:
-@@ -328,7 +332,10 @@ def cacert_list(inst, basedn, log, args):
- certmgr = CertManager(instance=inst)
- ca_certs = certmgr.list_ca_certs()
- if not ca_certs:
-- log.info("No CA certificates found.")
-+ if args.json:
-+ log.info(json.dumps([], indent=4))
-+ else:
-+ log.info("No CA certificates found.")
- return
-
- if args.json:
---
-2.53.0
-
diff --git a/0053-Issue-6758-Fix-Enable-Replication-dropdown-not-openi.patch b/0053-Issue-6758-Fix-Enable-Replication-dropdown-not-openi.patch
deleted file mode 100644
index 12727eb..0000000
--- a/0053-Issue-6758-Fix-Enable-Replication-dropdown-not-openi.patch
+++ /dev/null
@@ -1,32 +0,0 @@
-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.53.0
-
diff --git a/0054-Issue-7223-Remove-integerOrderingMatch-requirement-f.patch b/0054-Issue-7223-Remove-integerOrderingMatch-requirement-f.patch
deleted file mode 100644
index 54b4db2..0000000
--- a/0054-Issue-7223-Remove-integerOrderingMatch-requirement-f.patch
+++ /dev/null
@@ -1,538 +0,0 @@
-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.53.0
-
diff --git a/0055-Issue-7236-Fix-GSSAPI-tests-7237.patch b/0055-Issue-7236-Fix-GSSAPI-tests-7237.patch
deleted file mode 100644
index 4139dc2..0000000
--- a/0055-Issue-7236-Fix-GSSAPI-tests-7237.patch
+++ /dev/null
@@ -1,227 +0,0 @@
-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.53.0
-
diff --git a/0056-Issue-6753-Port-ticket-49039-test.patch b/0056-Issue-6753-Port-ticket-49039-test.patch
deleted file mode 100644
index 7dd550e..0000000
--- a/0056-Issue-6753-Port-ticket-49039-test.patch
+++ /dev/null
@@ -1,260 +0,0 @@
-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.53.0
-
diff --git a/0057-Issue-5853-Update-concread-to-0.5.10.patch b/0057-Issue-5853-Update-concread-to-0.5.10.patch
deleted file mode 100644
index c22eb4e..0000000
--- a/0057-Issue-5853-Update-concread-to-0.5.10.patch
+++ /dev/null
@@ -1,952 +0,0 @@
-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
-
diff --git a/0058-Issue-7271-plugins-that-create-threads-need-to-updat.patch b/0058-Issue-7271-plugins-that-create-threads-need-to-updat.patch
deleted file mode 100644
index 71a254d..0000000
--- a/0058-Issue-7271-plugins-that-create-threads-need-to-updat.patch
+++ /dev/null
@@ -1,86 +0,0 @@
-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
-
diff --git a/0059-Issue-7273-In-a-chaining-environment-binding-as-remo.patch b/0059-Issue-7273-In-a-chaining-environment-binding-as-remo.patch
deleted file mode 100644
index 89e9966..0000000
--- a/0059-Issue-7273-In-a-chaining-environment-binding-as-remo.patch
+++ /dev/null
@@ -1,40 +0,0 @@
-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
-
diff --git a/0060-Issue-7279-UI-Fix-typo-in-export-certificate-dialog-.patch b/0060-Issue-7279-UI-Fix-typo-in-export-certificate-dialog-.patch
deleted file mode 100644
index fa4e410..0000000
--- a/0060-Issue-7279-UI-Fix-typo-in-export-certificate-dialog-.patch
+++ /dev/null
@@ -1,46 +0,0 @@
-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:")} <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
-
diff --git a/0061-Issue-7275-UI-Improve-password-policy-field-validati.patch b/0061-Issue-7275-UI-Improve-password-policy-field-validati.patch
deleted file mode 100644
index 49fee6b..0000000
--- a/0061-Issue-7275-UI-Improve-password-policy-field-validati.patch
+++ /dev/null
@@ -1,1178 +0,0 @@
-From fff8e54cb265ac6e68b0069679bd4e7685647dcb Mon Sep 17 00:00:00 2001
-From: Simon Pichugin <spichugi@redhat.com>
-Date: Tue, 24 Feb 2026 09:32:41 -0800
-Subject: [PATCH] Issue 7275 - UI - Improve password policy field validation in
- Cockpit UI (#7276)
-
-Description: Password policy fields in the Cockpit UI lack client-side validation.
-Invalid values only produce generic server-side errors after clicking Save.
-Add a shared pwpValidation module and inline validation to all numeric
-password policy fields in globalPwp.jsx and localPwp.jsx.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7275
-
-Reviewed by: @mreynolds389 (Thanks!)
----
- .../src/lib/database/globalPwp.jsx | 61 ++++++++-
- .../389-console/src/lib/database/localPwp.jsx | 117 +++++++++++++++++-
- .../src/lib/database/pwpValidation.jsx | 105 ++++++++++++++++
- 3 files changed, 272 insertions(+), 11 deletions(-)
- create mode 100644 src/cockpit/389-console/src/lib/database/pwpValidation.jsx
-
-diff --git a/src/cockpit/389-console/src/lib/database/globalPwp.jsx b/src/cockpit/389-console/src/lib/database/globalPwp.jsx
-index d479c32e7..205e4735d 100644
---- a/src/cockpit/389-console/src/lib/database/globalPwp.jsx
-+++ b/src/cockpit/389-console/src/lib/database/globalPwp.jsx
-@@ -23,6 +23,12 @@ import {
- import TypeaheadSelect from "../../dsBasicComponents.jsx";
- import PropTypes from "prop-types";
- import { SyncAltIcon } from '@patternfly/react-icons';
-+import {
-+ getValidationProps,
-+ hasInvalidField,
-+ renderValidationError,
-+ updateFieldValidation,
-+} from "./pwpValidation.jsx";
-
- const _ = cockpit.gettext;
-
-@@ -114,6 +120,7 @@ export class GlobalPwPolicy extends React.Component {
- saveSyntaxDisabled: true,
- saveTPRDisabled: true,
- isSelectOpen: false,
-+ invalidFields: {},
- };
-
- // Toggle currently active tab
-@@ -242,6 +249,7 @@ export class GlobalPwPolicy extends React.Component {
- const stateUpdate = {
- [attr]: value,
- saveGeneralDisabled: disableSaveBtn,
-+ invalidFields: updateFieldValidation(this.state.invalidFields, attr, value),
- };
-
- this.setState(stateUpdate, () => {
-@@ -363,6 +371,7 @@ export class GlobalPwPolicy extends React.Component {
- this.setState({
- [attr]: value,
- saveExpDisabled: disableSaveBtn,
-+ invalidFields: updateFieldValidation(this.state.invalidFields, attr, value),
- });
- }
-
-@@ -440,6 +449,7 @@ export class GlobalPwPolicy extends React.Component {
- this.setState({
- [attr]: value,
- saveLockoutDisabled: disableSaveBtn,
-+ invalidFields: updateFieldValidation(this.state.invalidFields, attr, value),
- });
- }
-
-@@ -541,12 +551,14 @@ export class GlobalPwPolicy extends React.Component {
- this.setState({
- [attr]: Array.isArray(selection) ? selection : [],
- saveSyntaxDisabled: disableSaveBtn,
-+ invalidFields: updateFieldValidation(this.state.invalidFields, attr, value),
- isSelectOpen: false
- });
- } else {
- this.setState({
- [attr]: value,
- saveSyntaxDisabled: disableSaveBtn,
-+ invalidFields: updateFieldValidation(this.state.invalidFields, attr, value),
- isSelectOpen: false
- });
- }
-@@ -841,6 +853,7 @@ export class GlobalPwPolicy extends React.Component {
- {
- loaded: true,
- loading: false,
-+ invalidFields: {},
- saveGeneralDisabled: true,
- savePasswordStorageDisabled: true,
- saveUserDisabled: true,
-@@ -996,10 +1009,12 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordminlength"
- aria-describedby="horizontal-form-name-helper"
- name="passwordminlength"
-+ {...getValidationProps("passwordminlength", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordminlength", this.state.invalidFields)}
- </GridItem>
- <GridItem className="ds-label" offset={6} span={3}>
- {_("Minimum Alpha's")}
-@@ -1012,10 +1027,12 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordminalphas"
- aria-describedby="horizontal-form-name-helper"
- name="passwordminalphas"
-+ {...getValidationProps("passwordminalphas", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordminalphas", this.state.invalidFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -1030,10 +1047,12 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordmindigits"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmindigits"
-+ {...getValidationProps("passwordmindigits", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordmindigits", this.state.invalidFields)}
- </GridItem>
- <GridItem className="ds-label" offset={6} span={3}>
- {_("Minimum Special")}
-@@ -1046,10 +1065,12 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordminspecials"
- aria-describedby="horizontal-form-name-helper"
- name="passwordminspecials"
-+ {...getValidationProps("passwordminspecials", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordminspecials", this.state.invalidFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -1064,10 +1085,12 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordminuppers"
- aria-describedby="horizontal-form-name-helper"
- name="passwordminuppers"
-+ {...getValidationProps("passwordminuppers", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordminuppers", this.state.invalidFields)}
- </GridItem>
- <GridItem className="ds-label" offset={6} span={3}>
- {_("Minimum Lowercase")}
-@@ -1080,10 +1103,12 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordminlowers"
- aria-describedby="horizontal-form-name-helper"
- name="passwordminlowers"
-+ {...getValidationProps("passwordminlowers", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordminlowers", this.state.invalidFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -1098,10 +1123,12 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordmin8bit"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmin8bit"
-+ {...getValidationProps("passwordmin8bit", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordmin8bit", this.state.invalidFields)}
- </GridItem>
- <GridItem className="ds-label" offset={6} span={3}>
- {_("Minimum Categories")}
-@@ -1114,10 +1141,12 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordmincategories"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmincategories"
-+ {...getValidationProps("passwordmincategories", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordmincategories", this.state.invalidFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -1132,11 +1161,13 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordmaxsequence"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmaxsequence"
-+ {...getValidationProps("passwordmaxsequence", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordmaxsequence", this.state.invalidFields)}
- <GridItem className="ds-label" offset={6} span={3}>
- {_("Max Sequence Sets")}
- </GridItem>
-@@ -1148,11 +1179,13 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordmaxseqsets"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmaxseqsets"
-+ {...getValidationProps("passwordmaxseqsets", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordmaxseqsets", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top">
- <GridItem className="ds-label" span={3}>
-@@ -1166,11 +1199,13 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordmaxclasschars"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmaxclasschars"
-+ {...getValidationProps("passwordmaxclasschars", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordmaxclasschars", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top">
- <GridItem className="ds-label" span={3}>
-@@ -1251,12 +1286,14 @@ export class GlobalPwPolicy extends React.Component {
- type="number"
- id="passwordmaxfailure"
- aria-describedby="horizontal-form-name-helper"
-- name="passwordmaxpasswordmaxfailureclasschars"
-+ name="passwordmaxfailure"
-+ {...getValidationProps("passwordmaxfailure", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleLockoutChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordmaxfailure", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("The number of seconds until an accounts failure count is reset (passwordResetFailureCount).")}>
- <GridItem className="ds-label" span={5}>
-@@ -1269,11 +1306,13 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordresetfailurecount"
- aria-describedby="horizontal-form-name-helper"
- name="passwordresetfailurecount"
-+ {...getValidationProps("passwordresetfailurecount", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleLockoutChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordresetfailurecount", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("The number of seconds, duration, before the account gets unlocked (passwordLockoutDuration).")}>
- <GridItem className="ds-label" span={5}>
-@@ -1286,11 +1325,13 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordlockoutduration"
- aria-describedby="horizontal-form-name-helper"
- name="passwordlockoutduration"
-+ {...getValidationProps("passwordlockoutduration", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleLockoutChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordlockoutduration", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("Do not lockout the user account forever, instead the account will unlock based on the lockout duration (passwordUnlock).")}>
- <GridItem className="ds-label" span={5}>
-@@ -1322,11 +1363,13 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordmaxage"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmaxage"
-+ {...getValidationProps("passwordmaxage", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleExpChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordmaxage", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("The number of logins that are allowed after the password has expired (passwordGraceLimit).")}>
- <GridItem className="ds-label" span={5}>
-@@ -1339,11 +1382,13 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordgracelimit"
- aria-describedby="horizontal-form-name-helper"
- name="passwordgracelimit"
-+ {...getValidationProps("passwordgracelimit", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleExpChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordgracelimit", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("Set the time (in seconds), before a password is about to expire, to send a warning. (passwordWarning).")}>
- <GridItem className="ds-label" span={5}>
-@@ -1356,11 +1401,13 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordwarning"
- aria-describedby="horizontal-form-name-helper"
- name="passwordwarning"
-+ {...getValidationProps("passwordwarning", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleExpChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordwarning", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("Always return a password expiring control when requested (passwordSendExpiringTime).")}>
- <GridItem className="ds-label" span={5}>
-@@ -1493,10 +1540,12 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordinhistory"
- aria-describedby="horizontal-form-name-helper"
- name="passwordinhistory"
-+ {...getValidationProps("passwordinhistory", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleGeneralChange(e);
- }}
- />
-+ {renderValidationError("passwordinhistory", this.state.invalidFields)}
- </div>
- </GridItem>
- </Grid>
-@@ -1555,11 +1604,13 @@ export class GlobalPwPolicy extends React.Component {
- id="passwordminage"
- aria-describedby="horizontal-form-name-helper"
- name="passwordminage"
-+ {...getValidationProps("passwordminage", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleGeneralChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordminage", this.state.invalidFields)}
- </Grid>
- <Grid
- title={_("The DN for a password administrator or administrator group (passwordAdminDN).")}
-@@ -1596,7 +1647,7 @@ export class GlobalPwPolicy extends React.Component {
- </Grid>
- </Form>
- <Button
-- isDisabled={this.state.saveGeneralDisabled && this.state.savePasswordStorageDisabled || this.state.saving}
-+ isDisabled={this.state.saveGeneralDisabled && this.state.savePasswordStorageDisabled || this.state.saving || hasInvalidField(general_attrs, this.state.invalidFields)}
- variant="primary"
- className="ds-margin-top-xlg ds-margin-left-sm"
- onClick={this.handleSaveGeneral}
-@@ -1624,7 +1675,7 @@ export class GlobalPwPolicy extends React.Component {
- {pwExpirationRows}
- </Form>
- <Button
-- isDisabled={this.state.saveExpDisabled || this.state.saving}
-+ isDisabled={this.state.saveExpDisabled || this.state.saving || hasInvalidField(exp_attrs, this.state.invalidFields)}
- variant="primary"
- className="ds-margin-top-xlg ds-margin-left"
- onClick={this.handleSaveExp}
-@@ -1652,7 +1703,7 @@ export class GlobalPwPolicy extends React.Component {
- {pwLockoutRows}
- </Form>
- <Button
-- isDisabled={this.state.saveLockoutDisabled || this.state.saving}
-+ isDisabled={this.state.saveLockoutDisabled || this.state.saving || hasInvalidField(lockout_attrs, this.state.invalidFields)}
- variant="primary"
- className="ds-margin-top-xlg ds-margin-left"
- onClick={this.handleSaveLockout}
-@@ -1680,7 +1731,7 @@ export class GlobalPwPolicy extends React.Component {
- {pwSyntaxRows}
- </Form>
- <Button
-- isDisabled={this.state.saveSyntaxDisabled || this.state.saving}
-+ isDisabled={this.state.saveSyntaxDisabled || this.state.saving || hasInvalidField(syntax_attrs, this.state.invalidFields)}
- variant="primary"
- className="ds-margin-top-xlg ds-margin-left"
- onClick={this.handleSaveSyntax}
-diff --git a/src/cockpit/389-console/src/lib/database/localPwp.jsx b/src/cockpit/389-console/src/lib/database/localPwp.jsx
-index 8dce89fb8..f9f9dba33 100644
---- a/src/cockpit/389-console/src/lib/database/localPwp.jsx
-+++ b/src/cockpit/389-console/src/lib/database/localPwp.jsx
-@@ -25,6 +25,12 @@ import {
- TextVariants,
- ValidatedOptions
- } from '@patternfly/react-core';
-+import {
-+ getValidationProps,
-+ hasInvalidField,
-+ renderValidationError,
-+ updateFieldValidation,
-+} from "./pwpValidation.jsx";
- import TypeaheadSelect from "../../dsBasicComponents.jsx";
- import { SyncAltIcon } from "@patternfly/react-icons";
- import PropTypes from "prop-types";
-@@ -213,11 +219,13 @@ class CreatePolicy extends React.Component {
- id="create_passwordminage"
- aria-describedby="horizontal-form-name-helper"
- name="create_passwordminage"
-+ {...getValidationProps("create_passwordminage", this.props.invalidCreateFields)}
- onChange={(e, checked) => {
- this.props.handleChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("create_passwordminage", this.props.invalidCreateFields)}
- </Grid>
- <Grid
- className="ds-margin-top"
-@@ -309,10 +317,12 @@ class CreatePolicy extends React.Component {
- id="create_passwordinhistory"
- aria-describedby="horizontal-form-name-helper"
- name="create_passwordinhistory"
-+ {...getValidationProps("create_passwordinhistory", this.props.invalidCreateFields)}
- onChange={(e, checked) => {
- this.props.handleChange(e);
- }}
- />
-+ {renderValidationError("create_passwordinhistory", this.props.invalidCreateFields)}
- </div>
- </GridItem>
- </Grid>
-@@ -352,12 +362,14 @@ class CreatePolicy extends React.Component {
- id="create_passwordmaxage"
- aria-describedby="create_passwordmaxage"
- name="create_passwordmaxage"
-+ {...getValidationProps("create_passwordmaxage", this.props.invalidCreateFields)}
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
- isDisabled={!this.props.passwordexp}
- />
- </GridItem>
-+ {renderValidationError("create_passwordmaxage", this.props.invalidCreateFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("The number of logins that are allowed after the password has expired (passwordGraceLimit).")}>
- <GridItem className="ds-label" span={4}>
-@@ -369,12 +381,14 @@ class CreatePolicy extends React.Component {
- id="create_passwordgracelimit"
- aria-describedby="create_passwordgracelimit"
- name="create_passwordgracelimit"
-+ {...getValidationProps("create_passwordgracelimit", this.props.invalidCreateFields)}
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
- isDisabled={!this.props.passwordexp}
- />
- </GridItem>
-+ {renderValidationError("create_passwordgracelimit", this.props.invalidCreateFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("Set the time (in seconds), before a password is about to expire, to send a warning. (passwordWarning).")}>
- <GridItem className="ds-label" span={4}>
-@@ -386,12 +400,14 @@ class CreatePolicy extends React.Component {
- id="create_passwordwarning"
- aria-describedby="create_passwordwarning"
- name="create_passwordwarning"
-+ {...getValidationProps("create_passwordwarning", this.props.invalidCreateFields)}
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
- isDisabled={!this.props.passwordexp}
- />
- </GridItem>
-+ {renderValidationError("create_passwordwarning", this.props.invalidCreateFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("Always return a password expiring control when requested (passwordSendExpiringTime).")}>
- <GridItem span={4}>
-@@ -441,12 +457,14 @@ class CreatePolicy extends React.Component {
- id="create_passwordmaxfailure"
- aria-describedby="create_passwordmaxfailure"
- name="create_passwordmaxfailure"
-+ {...getValidationProps("create_passwordmaxfailure", this.props.invalidCreateFields)}
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
- isDisabled={!this.props.passwordlockout}
- />
- </GridItem>
-+ {renderValidationError("create_passwordmaxfailure", this.props.invalidCreateFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("The number of seconds until an accounts failure count is reset (passwordResetFailureCount).")}>
- <GridItem className="ds-label" span={4}>
-@@ -458,12 +476,14 @@ class CreatePolicy extends React.Component {
- id="create_passwordresetfailurecount"
- aria-describedby="create_passwordresetfailurecount"
- name="create_passwordresetfailurecount"
-+ {...getValidationProps("create_passwordresetfailurecount", this.props.invalidCreateFields)}
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
- isDisabled={!this.props.passwordlockout}
- />
- </GridItem>
-+ {renderValidationError("create_passwordresetfailurecount", this.props.invalidCreateFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("The number of seconds, duration, before the account gets unlocked (passwordLockoutDuration).")}>
- <GridItem className="ds-label" span={4}>
-@@ -475,12 +495,14 @@ class CreatePolicy extends React.Component {
- id="create_passwordlockoutduration"
- aria-describedby="create_passwordlockoutduration"
- name="create_passwordlockoutduration"
-+ {...getValidationProps("create_passwordlockoutduration", this.props.invalidCreateFields)}
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
- isDisabled={!this.props.passwordlockout}
- />
- </GridItem>
-+ {renderValidationError("create_passwordlockoutduration", this.props.invalidCreateFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("Do not lockout the user account forever, instead the account will unlock based on the lockout duration (passwordUnlock).")}>
- <GridItem span={6}>
-@@ -532,8 +554,10 @@ class CreatePolicy extends React.Component {
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
-+ {...getValidationProps("create_passwordminlength", this.props.invalidCreateFields)}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordminlength", this.props.invalidCreateFields)}
- </GridItem>
- <GridItem className="ds-label" offset={5} span={3}>
- {_("Minimum Alpha's")}
-@@ -547,8 +571,10 @@ class CreatePolicy extends React.Component {
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
-+ {...getValidationProps("create_passwordminalphas", this.props.invalidCreateFields)}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordminalphas", this.props.invalidCreateFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -564,8 +590,10 @@ class CreatePolicy extends React.Component {
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
-+ {...getValidationProps("create_passwordmindigits", this.props.invalidCreateFields)}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordmindigits", this.props.invalidCreateFields)}
- </GridItem>
- <GridItem className="ds-label" offset={5} span={3}>
- {_("Minimum Special")}
-@@ -579,8 +607,10 @@ class CreatePolicy extends React.Component {
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
-+ {...getValidationProps("create_passwordminspecials", this.props.invalidCreateFields)}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordminspecials", this.props.invalidCreateFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -596,8 +626,10 @@ class CreatePolicy extends React.Component {
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
-+ {...getValidationProps("create_passwordminuppers", this.props.invalidCreateFields)}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordminuppers", this.props.invalidCreateFields)}
- </GridItem>
- <GridItem className="ds-label" offset={5} span={3}>
- {_("Minimum Lowercase")}
-@@ -611,8 +643,10 @@ class CreatePolicy extends React.Component {
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
-+ {...getValidationProps("create_passwordminlowers", this.props.invalidCreateFields)}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordminlowers", this.props.invalidCreateFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -628,8 +662,10 @@ class CreatePolicy extends React.Component {
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
-+ {...getValidationProps("create_passwordmin8bit", this.props.invalidCreateFields)}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordmin8bit", this.props.invalidCreateFields)}
- </GridItem>
- <GridItem className="ds-label" offset={5} span={3}>
- {_("Minimum Categories")}
-@@ -643,8 +679,10 @@ class CreatePolicy extends React.Component {
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
-+ {...getValidationProps("create_passwordmincategories", this.props.invalidCreateFields)}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordmincategories", this.props.invalidCreateFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -660,8 +698,10 @@ class CreatePolicy extends React.Component {
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
-+ {...getValidationProps("create_passwordmintokenlength", this.props.invalidCreateFields)}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordmintokenlength", this.props.invalidCreateFields)}
- </GridItem>
- <GridItem className="ds-label" offset={5} span={3}>
- {_("Max Repeated Chars")}
-@@ -675,8 +715,10 @@ class CreatePolicy extends React.Component {
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
-+ {...getValidationProps("create_passwordmaxrepeats", this.props.invalidCreateFields)}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordmaxrepeats", this.props.invalidCreateFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -689,11 +731,13 @@ class CreatePolicy extends React.Component {
- id="create_passwordmaxsequence"
- aria-describedby="create_passwordmaxsequence"
- name="create_passwordmaxsequence"
-+ {...getValidationProps("create_passwordmaxsequence", this.props.invalidCreateFields)}
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordmaxsequence", this.props.invalidCreateFields)}
- </GridItem>
- <GridItem className="ds-label" offset={5} span={3}>
- {_("Max Sequence Sets")}
-@@ -704,11 +748,13 @@ class CreatePolicy extends React.Component {
- id="create_passwordmaxseqsets"
- aria-describedby="create_passwordmaxseqsets"
- name="create_passwordmaxseqsets"
-+ {...getValidationProps("create_passwordmaxseqsets", this.props.invalidCreateFields)}
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordmaxseqsets", this.props.invalidCreateFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -721,11 +767,13 @@ class CreatePolicy extends React.Component {
- id="create_passwordmaxclasschars"
- aria-describedby="create_passwordmaxclasschars"
- name="create_passwordmaxclasschars"
-+ {...getValidationProps("create_passwordmaxclasschars", this.props.invalidCreateFields)}
- onChange={(e, str) => {
- this.props.handleChange(e);
- }}
- isDisabled={!this.props.passwordchecksyntax}
- />
-+ {renderValidationError("create_passwordmaxclasschars", this.props.invalidCreateFields)}
- </GridItem>
- </Grid>
- <Grid
-@@ -1082,6 +1130,9 @@ export class LocalPwPolicy extends React.Component {
- _create_passwordtprdelayvalidfrom: "-1",
- _create_passwordadmindn: "",
- _create_passwordadminskipinfoupdate: false,
-+ // Validation
-+ invalidFields: {},
-+ invalidCreateFields: {},
- // Select typeahead
- isUserAttrsCreateOpen: false,
- isUserAttrsEditOpen: false,
-@@ -1314,19 +1365,27 @@ export class LocalPwPolicy extends React.Component {
- }
- }
-
-+ // Validate constrained fields for create
-+ const newInvalidCreateFields = updateFieldValidation(this.state.invalidCreateFields, attr, value);
-+ if (newInvalidCreateFields[attr]) {
-+ disableSaveBtn = true;
-+ }
-+
- // Select Typeahead
- if (selection) {
- this.setState({
- [attr]: Array.isArray(selection) ? selection : [],
- createDisabled: disableSaveBtn,
- invalid_dn,
-+ invalidCreateFields: newInvalidCreateFields,
- isUserAttrsCreateOpen: false
- });
- } else { // Checkbox
- this.setState({
- [attr]: value,
- createDisabled: disableSaveBtn,
-- invalid_dn
-+ invalid_dn,
-+ invalidCreateFields: newInvalidCreateFields,
- });
- }
- }
-@@ -1418,6 +1477,7 @@ export class LocalPwPolicy extends React.Component {
- this.setState({
- [attr]: value,
- saveGeneralDisabled: disableSaveBtn,
-+ invalidFields: updateFieldValidation(this.state.invalidFields, attr, value),
- });
- }
-
-@@ -1495,6 +1555,7 @@ export class LocalPwPolicy extends React.Component {
- this.setState({
- [attr]: value,
- saveExpDisabled: disableSaveBtn,
-+ invalidFields: updateFieldValidation(this.state.invalidFields, attr, value),
- });
- }
-
-@@ -1572,6 +1633,7 @@ export class LocalPwPolicy extends React.Component {
- this.setState({
- [attr]: value,
- saveLockoutDisabled: disableSaveBtn,
-+ invalidFields: updateFieldValidation(this.state.invalidFields, attr, value),
- });
- }
-
-@@ -1672,12 +1734,14 @@ export class LocalPwPolicy extends React.Component {
- this.setState({
- [attr]: Array.isArray(selection) ? selection : [],
- saveSyntaxDisabled: disableSaveBtn,
-+ invalidFields: updateFieldValidation(this.state.invalidFields, attr, value),
- isUserAttrsEditOpen: false
- });
- } else {
- this.setState({
- [attr]: value,
- saveSyntaxDisabled: disableSaveBtn,
-+ invalidFields: updateFieldValidation(this.state.invalidFields, attr, value),
- isUserAttrsEditOpen: false
- });
- }
-@@ -1867,6 +1931,8 @@ export class LocalPwPolicy extends React.Component {
- policyName: "",
- deleteName: "",
- showDeletePolicy: false,
-+ invalidFields: {},
-+ invalidCreateFields: {},
- // Reset edit and create tab
- saveGeneralDisabled: true,
- saveUserDisabled: true,
-@@ -2391,10 +2457,12 @@ export class LocalPwPolicy extends React.Component {
- id="passwordminlength"
- aria-describedby="horizontal-form-name-helper"
- name="passwordminlength"
-+ {...getValidationProps("passwordminlength", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordminlength", this.state.invalidFields)}
- </GridItem>
- <GridItem className="ds-label" offset={6} span={3}>
- {_("Minimum Alpha's")}
-@@ -2407,10 +2475,12 @@ export class LocalPwPolicy extends React.Component {
- id="passwordminalphas"
- aria-describedby="horizontal-form-name-helper"
- name="passwordminalphas"
-+ {...getValidationProps("passwordminalphas", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordminalphas", this.state.invalidFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -2425,10 +2495,12 @@ export class LocalPwPolicy extends React.Component {
- id="passwordmindigits"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmindigits"
-+ {...getValidationProps("passwordmindigits", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordmindigits", this.state.invalidFields)}
- </GridItem>
- <GridItem className="ds-label" offset={6} span={3}>
- {_("Minimum Special")}
-@@ -2441,10 +2513,12 @@ export class LocalPwPolicy extends React.Component {
- id="passwordminspecials"
- aria-describedby="horizontal-form-name-helper"
- name="passwordminspecials"
-+ {...getValidationProps("passwordminspecials", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordminspecials", this.state.invalidFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -2459,10 +2533,12 @@ export class LocalPwPolicy extends React.Component {
- id="passwordminuppers"
- aria-describedby="horizontal-form-name-helper"
- name="passwordminuppers"
-+ {...getValidationProps("passwordminuppers", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordminuppers", this.state.invalidFields)}
- </GridItem>
- <GridItem className="ds-label" offset={6} span={3}>
- {_("Minimum Lowercase")}
-@@ -2475,10 +2551,12 @@ export class LocalPwPolicy extends React.Component {
- id="passwordminlowers"
- aria-describedby="horizontal-form-name-helper"
- name="passwordminlowers"
-+ {...getValidationProps("passwordminlowers", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordminlowers", this.state.invalidFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -2493,10 +2571,12 @@ export class LocalPwPolicy extends React.Component {
- id="passwordmin8bit"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmin8bit"
-+ {...getValidationProps("passwordmin8bit", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordmin8bit", this.state.invalidFields)}
- </GridItem>
- <GridItem className="ds-label" offset={6} span={3}>
- {_("Minimum Categories")}
-@@ -2509,10 +2589,12 @@ export class LocalPwPolicy extends React.Component {
- id="passwordmincategories"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmincategories"
-+ {...getValidationProps("passwordmincategories", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
-+ {renderValidationError("passwordmincategories", this.state.invalidFields)}
- </GridItem>
- </Grid>
- <Grid className="ds-margin-top">
-@@ -2527,11 +2609,13 @@ export class LocalPwPolicy extends React.Component {
- id="passwordmaxsequence"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmaxsequence"
-+ {...getValidationProps("passwordmaxsequence", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordmaxsequence", this.state.invalidFields)}
- <GridItem className="ds-label" offset={6} span={3}>
- {_("Max Sequence Sets")}
- </GridItem>
-@@ -2543,11 +2627,13 @@ export class LocalPwPolicy extends React.Component {
- id="passwordmaxseqsets"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmaxseqsets"
-+ {...getValidationProps("passwordmaxseqsets", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordmaxseqsets", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top">
- <GridItem className="ds-label" span={3}>
-@@ -2561,11 +2647,13 @@ export class LocalPwPolicy extends React.Component {
- id="passwordmaxclasschars"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmaxclasschars"
-+ {...getValidationProps("passwordmaxclasschars", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleSyntaxChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordmaxclasschars", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top">
- <GridItem className="ds-label" span={3}>
-@@ -2646,12 +2734,14 @@ export class LocalPwPolicy extends React.Component {
- type="number"
- id="passwordmaxfailure"
- aria-describedby="horizontal-form-name-helper"
-- name="passwordmaxpasswordmaxfailureclasschars"
-+ name="passwordmaxfailure"
-+ {...getValidationProps("passwordmaxfailure", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleLockoutChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordmaxfailure", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("The number of seconds until an accounts failure count is reset (passwordResetFailureCount).")}>
- <GridItem className="ds-label" span={5}>
-@@ -2664,11 +2754,13 @@ export class LocalPwPolicy extends React.Component {
- id="passwordresetfailurecount"
- aria-describedby="horizontal-form-name-helper"
- name="passwordresetfailurecount"
-+ {...getValidationProps("passwordresetfailurecount", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleLockoutChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordresetfailurecount", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("The number of seconds, duration, before the account gets unlocked (passwordLockoutDuration).")}>
- <GridItem className="ds-label" span={5}>
-@@ -2681,11 +2773,13 @@ export class LocalPwPolicy extends React.Component {
- id="passwordlockoutduration"
- aria-describedby="horizontal-form-name-helper"
- name="passwordlockoutduration"
-+ {...getValidationProps("passwordlockoutduration", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleLockoutChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordlockoutduration", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("Do not lockout the user account forever, instead the account will unlock based on the lockout duration (passwordUnlock).")}>
- <GridItem className="ds-label" span={5}>
-@@ -2717,11 +2811,13 @@ export class LocalPwPolicy extends React.Component {
- id="passwordmaxage"
- aria-describedby="horizontal-form-name-helper"
- name="passwordmaxage"
-+ {...getValidationProps("passwordmaxage", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleExpChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordmaxage", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("The number of logins that are allowed after the password has expired (passwordGraceLimit).")}>
- <GridItem className="ds-label" span={5}>
-@@ -2734,11 +2830,13 @@ export class LocalPwPolicy extends React.Component {
- id="passwordgracelimit"
- aria-describedby="horizontal-form-name-helper"
- name="passwordgracelimit"
-+ {...getValidationProps("passwordgracelimit", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleExpChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordgracelimit", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("Set the time (in seconds), before a password is about to expire, to send a warning. (passwordWarning).")}>
- <GridItem className="ds-label" span={5}>
-@@ -2751,11 +2849,13 @@ export class LocalPwPolicy extends React.Component {
- id="passwordwarning"
- aria-describedby="horizontal-form-name-helper"
- name="passwordwarning"
-+ {...getValidationProps("passwordwarning", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleExpChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordwarning", this.state.invalidFields)}
- </Grid>
- <Grid className="ds-margin-top" title={_("Always return a password expiring control when requested (passwordSendExpiringTime).")}>
- <GridItem className="ds-label" span={5}>
-@@ -2849,10 +2949,12 @@ export class LocalPwPolicy extends React.Component {
- id="passwordinhistory"
- aria-describedby="horizontal-form-name-helper"
- name="passwordinhistory"
-+ {...getValidationProps("passwordinhistory", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleGeneralChange(e);
- }}
- />
-+ {renderValidationError("passwordinhistory", this.state.invalidFields)}
- </div>
- </GridItem>
- </Grid>
-@@ -2892,11 +2994,13 @@ export class LocalPwPolicy extends React.Component {
- id="passwordminage"
- aria-describedby="horizontal-form-name-helper"
- name="passwordminage"
-+ {...getValidationProps("passwordminage", this.state.invalidFields)}
- onChange={(e, checked) => {
- this.handleGeneralChange(e);
- }}
- />
- </GridItem>
-+ {renderValidationError("passwordminage", this.state.invalidFields)}
- </Grid>
- <Grid
- title={_("The DN for a password administrator or administrator group (passwordAdminDN).")}
-@@ -2933,7 +3037,7 @@ export class LocalPwPolicy extends React.Component {
- </Grid>
- </Form>
- <Button
-- isDisabled={this.state.saveGeneralDisabled || this.state.saving}
-+ isDisabled={this.state.saveGeneralDisabled || this.state.saving || hasInvalidField(general_attrs, this.state.invalidFields)}
- variant="primary"
- className="ds-margin-top-xlg ds-margin-left-sm ds-margin-bottom-md"
- onClick={this.handleSaveGeneral}
-@@ -2961,7 +3065,7 @@ export class LocalPwPolicy extends React.Component {
- {pwExpirationRows}
- </Form>
- <Button
-- isDisabled={this.state.saveExpDisabled || this.state.saving}
-+ isDisabled={this.state.saveExpDisabled || this.state.saving || hasInvalidField(exp_attrs, this.state.invalidFields)}
- variant="primary"
- className="ds-margin-top-lg ds-margin-left"
- onClick={this.handleSaveExp}
-@@ -2989,7 +3093,7 @@ export class LocalPwPolicy extends React.Component {
- {pwLockoutRows}
- </Form>
- <Button
-- isDisabled={this.state.saveLockoutDisabled || this.state.saving}
-+ isDisabled={this.state.saveLockoutDisabled || this.state.saving || hasInvalidField(lockout_attrs, this.state.invalidFields)}
- variant="primary"
- className="ds-margin-top-lg ds-margin-left"
- onClick={this.handleSaveLockout}
-@@ -3017,7 +3121,7 @@ export class LocalPwPolicy extends React.Component {
- {pwSyntaxRows}
- </Form>
- <Button
-- isDisabled={this.state.saveSyntaxDisabled || this.state.saving}
-+ isDisabled={this.state.saveSyntaxDisabled || this.state.saving || hasInvalidField(syntax_attrs, this.state.invalidFields)}
- variant="primary"
- className="ds-margin-top-xlg ds-margin-left ds-margin-bottom-md"
- onClick={this.handleSaveSyntax}
-@@ -3167,6 +3271,7 @@ export class LocalPwPolicy extends React.Component {
- create_passwordstoragescheme={this.state.create_passwordstoragescheme}
- create_passwordadmindn={this.state.create_passwordadmindn}
- create_passwordadminskipinfoupdate={this.state.create_passwordadminskipinfoupdate}
-+ invalidCreateFields={this.state.invalidCreateFields}
- onUserAttrsCreateToggle={this.handleUserAttrsCreateToggle}
- onUserAttrsCreateClear={this.handleUserAttrsCreateClear}
- isUserAttrsCreateOpen={this.state.isUserAttrsCreateOpen}
-diff --git a/src/cockpit/389-console/src/lib/database/pwpValidation.jsx b/src/cockpit/389-console/src/lib/database/pwpValidation.jsx
-new file mode 100644
-index 000000000..667f5c6f8
---- /dev/null
-+++ b/src/cockpit/389-console/src/lib/database/pwpValidation.jsx
-@@ -0,0 +1,105 @@
-+import cockpit from "cockpit";
-+import React from "react";
-+import {
-+ FormHelperText,
-+ GridItem,
-+ ValidatedOptions,
-+} from "@patternfly/react-core";
-+
-+const _ = cockpit.gettext;
-+
-+export const FIELD_RANGES = {
-+ passwordinhistory: { min: 0, max: 24 },
-+ passwordminlength: { min: 2, max: 512 },
-+ passwordmindigits: { min: 0, max: 64 },
-+ passwordminalphas: { min: 0, max: 64 },
-+ passwordminuppers: { min: 0, max: 64 },
-+ passwordminlowers: { min: 0, max: 64 },
-+ passwordminspecials: { min: 0, max: 64 },
-+ passwordmin8bit: { min: 0, max: 64 },
-+ passwordmaxrepeats: { min: 0, max: 64 },
-+ passwordmincategories: { min: 0, max: 5 },
-+ passwordmintokenlength: { min: 1, max: 64 },
-+ passwordmaxfailure: { min: 1, max: 32767 },
-+ passwordmaxsequence: { min: 0 },
-+ passwordmaxseqsets: { min: 0 },
-+ passwordmaxclasschars: { min: 0 },
-+ passwordresetfailurecount: { min: 0 },
-+ passwordlockoutduration: { min: 0 },
-+ passwordminage: { min: 0 },
-+ passwordmaxage: { min: 0 },
-+ passwordgracelimit: { min: 0 },
-+ passwordwarning: { min: 0 },
-+};
-+
-+// Strips create_ prefix to find the base field name in FIELD_RANGES
-+const baseFieldName = (attr) => {
-+ return attr.startsWith("create_") ? attr.substring(7) : attr;
-+};
-+
-+// Returns true if the value is invalid for the given field
-+export const validateField = (attr, value) => {
-+ const range = FIELD_RANGES[baseFieldName(attr)];
-+ if (!range) return false;
-+ const num = Number(value);
-+ if (value === "" || isNaN(num) || !Number.isInteger(num) || num < range.min) {
-+ return true;
-+ }
-+ if (range.max !== undefined && num > range.max) {
-+ return true;
-+ }
-+ return false;
-+};
-+
-+// Returns a new invalidFields map after validating a field change
-+export const updateFieldValidation = (invalidFields, attr, value) => {
-+ const newInvalidFields = { ...invalidFields };
-+ if (FIELD_RANGES[baseFieldName(attr)]) {
-+ if (validateField(attr, value)) {
-+ newInvalidFields[attr] = true;
-+ } else {
-+ delete newInvalidFields[attr];
-+ }
-+ }
-+ return newInvalidFields;
-+};
-+
-+// Returns { min, max?, validated } props to spread on a TextInput
-+export const getValidationProps = (fieldName, invalidFields) => {
-+ const range = FIELD_RANGES[baseFieldName(fieldName)];
-+ if (!range) return {};
-+ const props = { min: range.min };
-+ if (range.max !== undefined) {
-+ props.max = range.max;
-+ }
-+ props.validated = invalidFields[fieldName]
-+ ? ValidatedOptions.error
-+ : ValidatedOptions.default;
-+ return props;
-+};
-+
-+// Returns FormHelperText JSX when the field is invalid, or null
-+export const renderValidationError = (fieldName, invalidFields) => {
-+ const range = FIELD_RANGES[baseFieldName(fieldName)];
-+ if (!range || !invalidFields[fieldName]) return null;
-+ const msg =
-+ range.max !== undefined
-+ ? cockpit.format(
-+ _("Value must be a number from $0 to $1"),
-+ range.min,
-+ range.max
-+ )
-+ : _("Value must be a non-negative integer");
-+ return (
-+ <GridItem span={5}>
-+ <FormHelperText className="ds-left-margin" isError isHidden={false}>
-+ {msg}
-+ </FormHelperText>
-+ </GridItem>
-+ );
-+};
-+
-+// Returns true if any attr in the group is invalid
-+export const hasInvalidField = (attrs, invalidFields) => {
-+ return attrs.some((a) => invalidFields[a]);
-+};
---
-2.53.0
-
diff --git a/0062-Issue-7246-correct-formatting-of-Gen-as-CSN-in-dsctl.patch b/0062-Issue-7246-correct-formatting-of-Gen-as-CSN-in-dsctl.patch
deleted file mode 100644
index 25564f4..0000000
--- a/0062-Issue-7246-correct-formatting-of-Gen-as-CSN-in-dsctl.patch
+++ /dev/null
@@ -1,34 +0,0 @@
-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
-
diff --git a/0063-Security-fix-for-CVE-2025-14905.patch b/0063-Security-fix-for-CVE-2025-14905.patch
deleted file mode 100644
index 1e86814..0000000
--- a/0063-Security-fix-for-CVE-2025-14905.patch
+++ /dev/null
@@ -1,93 +0,0 @@
-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
-
diff --git a/0064-Issue-7267-MDB_BAD_VALSIZE-error-when-updating-index.patch b/0064-Issue-7267-MDB_BAD_VALSIZE-error-when-updating-index.patch
deleted file mode 100644
index b48c504..0000000
--- a/0064-Issue-7267-MDB_BAD_VALSIZE-error-when-updating-index.patch
+++ /dev/null
@@ -1,654 +0,0 @@
-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
-
diff --git a/0065-Issue-7277-UI-Fix-Japanese-translation-for-Successfu.patch b/0065-Issue-7277-UI-Fix-Japanese-translation-for-Successfu.patch
deleted file mode 100644
index c940375..0000000
--- a/0065-Issue-7277-UI-Fix-Japanese-translation-for-Successfu.patch
+++ /dev/null
@@ -1,90 +0,0 @@
-From 336f1ba7dcc895929ac2fb2d6cd630ddb645e94c Mon Sep 17 00:00:00 2001
-From: Simon Pichugin <spichugi@redhat.com>
-Date: Wed, 25 Feb 2026 09:05:40 -0800
-Subject: [PATCH] Issue 7277 - UI - Fix Japanese translation for "Successfully
- updated group" in Cockpit UI (#7278)
-
-Description: The Japanese translation for "Successfully updated group"
-incorrectly displays a "failed" message instead of a "succeeded" message.
-This is a copy-paste error from the adjacent failure message translation.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7277
-
-Reviewed by: @vashirov (Thanks!)
----
- src/cockpit/389-console/po/ja.po | 18 +++++++++---------
- 1 file changed, 9 insertions(+), 9 deletions(-)
-
-diff --git a/src/cockpit/389-console/po/ja.po b/src/cockpit/389-console/po/ja.po
-index 12c10ded4..163203940 100644
---- a/src/cockpit/389-console/po/ja.po
-+++ b/src/cockpit/389-console/po/ja.po
-@@ -3247,7 +3247,7 @@ msgid ""
- msgstr ""
- "ルートユーザがディレクトリサーバへのアクセスに使用できないIPアドレス(IPv4 ま"
- "たは IPv6)を設定します。ワイルドカードが使用できます。設定されていないIPアド"
--"レスは暗黙的に拒否されます。(rootdn-allow-ip)IPアドレスが、rootdn-allow-ip属"
-+"レスは暗黙的に許可されます。(rootdn-deny-ip)IPアドレスが、rootdn-allow-ip属"
- "性とrootdn-deny-ip属性の両方に設定されている場合、アクセスは拒否されます。"
-
- #: src/lib/plugins/rootDNAccessControl.jsx:601
-@@ -3670,8 +3670,8 @@ msgid ""
- "Specifies backends or multiple-nested suffixes for the MemberOf plug-in to "
- "work on (memberOfEntryScope)"
- msgstr ""
--"グループメンバーのDNを識別するために使用するグループエントリの属性を指定しま"
--"す。(memberOfGroupAttr)"
-+"MemberOfプラグインが動作するバックエンドまたはネストされたサフィックスを指定"
-+"します。(memberOfEntryScope)"
-
- #: src/lib/plugins/memberOf.jsx:1353 src/lib/plugins/memberOf.jsx:1394
- #: src/lib/plugins/memberOf.jsx:1523 src/lib/plugins/memberOf.jsx:1564
-@@ -5872,7 +5872,7 @@ msgstr ""
- #: src/lib/database/localPwp.jsx:878 src/lib/database/localPwp.jsx:3112
- #: src/lib/database/globalPwp.jsx:1595
- msgid "Password Valid From"
--msgstr "パスワードの有効期限"
-+msgstr "パスワードの有効開始日"
-
- #: src/lib/database/localPwp.jsx:902
- msgid "Create New Policy"
-@@ -10500,7 +10500,7 @@ msgstr "エントリ名の変更に成功しました!"
-
- #: src/lib/ldap_editor/wizards/operations/renameEntry.jsx:82
- msgid "Failed to rename entry, error: "
--msgstr "エントリの削除に失敗しました、エラー: "
-+msgstr "エントリ名の変更に失敗しました、エラー: "
-
- #: src/lib/ldap_editor/wizards/operations/renameEntry.jsx:93
- msgid ""
-@@ -14302,7 +14302,7 @@ msgstr "設定アップファイル $0 の権限設定に失敗しました: $1"
-
- #: src/dsModals.jsx:299
- msgid "Failed to populate installation file! $0"
--msgstr "インストールファイルの作成に失敗しました! $0"
-+msgstr "インストールファイルへのデータ入力に失敗しました! $0"
-
- #: src/dsModals.jsx:336
- msgid "Successfully created instance: slapd-$0"
-@@ -14868,15 +14868,15 @@ msgstr ""
-
- #: src/schema.jsx:1550
- msgid "Delete An Objectclass"
--msgstr "属性を削除"
-+msgstr "オブジェクトクラスを削除"
-
- #: src/schema.jsx:1551
- msgid "Are you sure you want to delete this Objectclass?"
--msgstr "このオブジェクトを削除しますか?"
-+msgstr "このオブジェクトクラスを削除しますか?"
-
- #: src/schema.jsx:1552
- msgid "Deleting objectclass ..."
--msgstr "オブジェクトを削除しています..."
-+msgstr "オブジェクトクラスを削除しています..."
-
- #: src/schema.jsx:1563
- msgid "Delete An Attribute"
---
-2.53.0
-
diff --git a/0066-Issue-7284-Creating-local-password-policy-succeeds-w.patch b/0066-Issue-7284-Creating-local-password-policy-succeeds-w.patch
deleted file mode 100644
index c1928a1..0000000
--- a/0066-Issue-7284-Creating-local-password-policy-succeeds-w.patch
+++ /dev/null
@@ -1,50 +0,0 @@
-From 185178ef52d6f665a53c69f794daf8c2ec15b455 Mon Sep 17 00:00:00 2001
-From: James Chapman <jachapma@redhat.com>
-Date: Thu, 26 Feb 2026 12:55:47 +0000
-Subject: [PATCH] Issue 7284 - Creating local password policy succeeds with
- incorrect passwordInHistory value (#7285)
-
-Description:
-attr_check_minmax used strtol(value, NULL, 0), which silently converted
-invalid strings to 0, passing subsequent range checks.
-
-Fix:
-Add checks for NULL or empty values and uses strtol with endptr to
-validate int input before range checks.
-
-Fixes: https://github.com/389ds/389-ds-base/issues/7284
-
-Reviewed by: @vashirov, @tbordaz (Thank you)
----
- ldap/servers/slapd/attr.c | 14 +++++++++++++-
- 1 file changed, 13 insertions(+), 1 deletion(-)
-
-diff --git a/ldap/servers/slapd/attr.c b/ldap/servers/slapd/attr.c
-index f5ea22eb5..ca2eb62c3 100644
---- a/ldap/servers/slapd/attr.c
-+++ b/ldap/servers/slapd/attr.c
-@@ -932,8 +932,20 @@ attr_check_minmax(const char *attr_name, char *value, long minval, long maxval,
- {
- int retVal = LDAP_SUCCESS;
- long val;
-+ char *endptr = NULL;
-+
-+ if (!value || *value == '\0') {
-+ slapi_create_errormsg(errorbuf, ebuflen, "%s: attr value is NULL.", attr_name);
-+ return LDAP_CONSTRAINT_VIOLATION;
-+ }
-+
-+ errno = 0;
-+ val = strtol(value, &endptr, 0);
-+ if (endptr == value || *endptr != '\0' || errno != 0) {
-+ slapi_create_errormsg(errorbuf, ebuflen, "%s: invalid value \"%s\".", attr_name, value);
-+ return LDAP_CONSTRAINT_VIOLATION;
-+ }
-
-- val = strtol(value, NULL, 0);
- if ((minval != -1 ? (val < minval ? 1 : 0) : 0) ||
- (maxval != -1 ? (val > maxval ? 1 : 0) : 0)) {
- slapi_create_errormsg(errorbuf, ebuflen, "%s: invalid value \"%s\".", attr_name, value);
---
-2.53.0
-
diff --git a/389-ds-base.spec b/389-ds-base.spec
index e7302a0..92f15fe 100644
--- a/389-ds-base.spec
+++ b/389-ds-base.spec
@@ -53,12 +53,15 @@ ExcludeArch: i686
%if %{with clang}
%global toolchain clang
-%global _missing_build_ids_terminate_build 0
+%global _lto_cflags %nil
%endif
# Build cockpit plugin
%bcond cockpit 1
+# Build HIBP password breach checking (requires libcurl)
+%bcond hibp 1
+
# fedora 15 and later uses tmpfiles.d
# otherwise, comment this out
%{!?with_tmpfiles_d: %global with_tmpfiles_d %{_sysconfdir}/tmpfiles.d}
@@ -75,9 +78,9 @@ ExcludeArch: i686
Summary: 389 Directory Server (%{variant})
Name: 389-ds-base
-Version: 3.2.1
+Version: 3.3.0
Release: %{autorelease -n %{?with_asan:-e asan}}%{?dist}
-License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT) AND (CC-BY-4.0 AND MIT) AND (MIT OR Apache-2.0) AND Unicode-3.0 AND (MIT OR CC0-1.0) AND (MIT OR Unlicense) AND 0BSD AND Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND ISC AND MIT AND MIT AND ISC AND MPL-2.0 AND PSF-2.0 AND Zlib
+License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT) AND (CC-BY-4.0 AND MIT) AND (MIT OR Apache-2.0) AND Unicode-3.0 AND (MIT OR Unlicense) AND 0BSD AND Apache-2.0 AND BSD-3-Clause AND ISC AND MIT AND MIT AND ISC AND MPL-2.0 AND Zlib
URL: https://www.port389.org
Obsoletes: %{name}-legacy-tools < 1.4.4.6
Obsoletes: %{name}-legacy-tools-debuginfo < 1.4.4.6
@@ -87,13 +90,13 @@ Provides: ldif2ldbm >= 0
Provides: bundled(crate(allocator-api2)) = 0.2.21
Provides: bundled(crate(anyhow)) = 1.0.102
Provides: bundled(crate(atty)) = 0.2.14
-Provides: bundled(crate(autocfg)) = 1.5.0
+Provides: bundled(crate(autocfg)) = 1.5.1
Provides: bundled(crate(base64)) = 0.13.1
-Provides: bundled(crate(bitflags)) = 2.11.0
-Provides: bundled(crate(bumpalo)) = 3.20.2
+Provides: bundled(crate(bitflags)) = 2.12.1
+Provides: bundled(crate(bumpalo)) = 3.20.3
Provides: bundled(crate(byteorder)) = 1.5.0
Provides: bundled(crate(cbindgen)) = 0.26.0
-Provides: bundled(crate(cc)) = 1.2.56
+Provides: bundled(crate(cc)) = 1.2.63
Provides: bundled(crate(cfg-if)) = 1.0.4
Provides: bundled(crate(clap)) = 3.2.25
Provides: bundled(crate(clap_lex)) = 0.2.4
@@ -103,70 +106,74 @@ Provides: bundled(crate(crossbeam-queue)) = 0.3.12
Provides: bundled(crate(crossbeam-utils)) = 0.8.21
Provides: bundled(crate(equivalent)) = 1.0.2
Provides: bundled(crate(errno)) = 0.3.14
-Provides: bundled(crate(fastrand)) = 2.3.0
+Provides: bundled(crate(fastrand)) = 2.4.1
Provides: bundled(crate(fernet)) = 0.1.4
Provides: bundled(crate(find-msvc-tools)) = 0.1.9
Provides: bundled(crate(foldhash)) = 0.2.0
Provides: bundled(crate(foreign-types)) = 0.3.2
Provides: bundled(crate(foreign-types-shared)) = 0.1.1
-Provides: bundled(crate(getrandom)) = 0.4.1
-Provides: bundled(crate(hashbrown)) = 0.16.1
+Provides: bundled(crate(futures-core)) = 0.3.32
+Provides: bundled(crate(futures-task)) = 0.3.32
+Provides: bundled(crate(futures-util)) = 0.3.32
+Provides: bundled(crate(getrandom)) = 0.4.2
+Provides: bundled(crate(hashbrown)) = 0.17.1
Provides: bundled(crate(heck)) = 0.5.0
Provides: bundled(crate(hermit-abi)) = 0.1.19
Provides: bundled(crate(id-arena)) = 2.3.0
-Provides: bundled(crate(indexmap)) = 2.13.0
-Provides: bundled(crate(itoa)) = 1.0.17
+Provides: bundled(crate(indexmap)) = 2.14.0
+Provides: bundled(crate(itoa)) = 1.0.18
Provides: bundled(crate(jobserver)) = 0.1.34
-Provides: bundled(crate(js-sys)) = 0.3.95
+Provides: bundled(crate(js-sys)) = 0.3.99
Provides: bundled(crate(leb128fmt)) = 0.1.0
-Provides: bundled(crate(libc)) = 0.2.182
-Provides: bundled(crate(linux-raw-sys)) = 0.11.0
-Provides: bundled(crate(log)) = 0.4.29
-Provides: bundled(crate(lru)) = 0.16.3
-Provides: bundled(crate(memchr)) = 2.8.0
-Provides: bundled(crate(once_cell)) = 1.21.3
-Provides: bundled(crate(openssl)) = 0.10.78
+Provides: bundled(crate(libc)) = 0.2.186
+Provides: bundled(crate(linux-raw-sys)) = 0.12.1
+Provides: bundled(crate(log)) = 0.4.31
+Provides: bundled(crate(lru)) = 0.16.4
+Provides: bundled(crate(memchr)) = 2.8.1
+Provides: bundled(crate(once_cell)) = 1.21.4
+Provides: bundled(crate(openssl)) = 0.10.80
Provides: bundled(crate(openssl-macros)) = 0.1.1
-Provides: bundled(crate(openssl-sys)) = 0.9.114
+Provides: bundled(crate(openssl-sys)) = 0.9.116
Provides: bundled(crate(os_str_bytes)) = 6.6.1
Provides: bundled(crate(paste)) = 1.0.15
-Provides: bundled(crate(pin-project-lite)) = 0.2.16
-Provides: bundled(crate(pkg-config)) = 0.3.32
+Provides: bundled(crate(pin-project-lite)) = 0.2.17
+Provides: bundled(crate(pkg-config)) = 0.3.33
Provides: bundled(crate(prettyplease)) = 0.2.37
Provides: bundled(crate(proc-macro2)) = 1.0.106
-Provides: bundled(crate(quote)) = 1.0.44
-Provides: bundled(crate(r-efi)) = 5.3.0
-Provides: bundled(crate(rustix)) = 1.1.3
+Provides: bundled(crate(quote)) = 1.0.45
+Provides: bundled(crate(r-efi)) = 6.0.0
+Provides: bundled(crate(rustix)) = 1.1.4
Provides: bundled(crate(rustversion)) = 1.0.22
-Provides: bundled(crate(semver)) = 1.0.27
+Provides: bundled(crate(semver)) = 1.0.28
Provides: bundled(crate(serde)) = 1.0.228
Provides: bundled(crate(serde_core)) = 1.0.228
Provides: bundled(crate(serde_derive)) = 1.0.228
-Provides: bundled(crate(serde_json)) = 1.0.149
-Provides: bundled(crate(shlex)) = 1.3.0
+Provides: bundled(crate(serde_json)) = 1.0.150
+Provides: bundled(crate(shlex)) = 2.0.1
+Provides: bundled(crate(slab)) = 0.4.12
Provides: bundled(crate(smallvec)) = 1.15.1
Provides: bundled(crate(sptr)) = 0.3.2
Provides: bundled(crate(strsim)) = 0.10.0
Provides: bundled(crate(syn)) = 2.0.117
-Provides: bundled(crate(tempfile)) = 3.25.0
+Provides: bundled(crate(tempfile)) = 3.27.0
Provides: bundled(crate(termcolor)) = 1.4.1
Provides: bundled(crate(textwrap)) = 0.16.2
-Provides: bundled(crate(tokio)) = 1.49.0
+Provides: bundled(crate(tokio)) = 1.52.3
Provides: bundled(crate(toml)) = 0.5.11
Provides: bundled(crate(tracing)) = 0.1.44
Provides: bundled(crate(tracing-attributes)) = 0.1.31
Provides: bundled(crate(tracing-core)) = 0.1.36
Provides: bundled(crate(unicode-ident)) = 1.0.24
Provides: bundled(crate(unicode-xid)) = 0.2.6
-Provides: bundled(crate(uuid)) = 1.23.1
+Provides: bundled(crate(uuid)) = 1.23.2
Provides: bundled(crate(vcpkg)) = 0.2.15
Provides: bundled(crate(wasi)) = 0.11.1+wasi_snapshot_preview1
-Provides: bundled(crate(wasip2)) = 1.0.2+wasi_0.2.9
+Provides: bundled(crate(wasip2)) = 1.0.3+wasi_0.2.9
Provides: bundled(crate(wasip3)) = 0.4.0+wasi_0.3.0_rc_2026_01_06
-Provides: bundled(crate(wasm-bindgen)) = 0.2.118
-Provides: bundled(crate(wasm-bindgen-macro)) = 0.2.118
-Provides: bundled(crate(wasm-bindgen-macro-support)) = 0.2.118
-Provides: bundled(crate(wasm-bindgen-shared)) = 0.2.118
+Provides: bundled(crate(wasm-bindgen)) = 0.2.122
+Provides: bundled(crate(wasm-bindgen-macro)) = 0.2.122
+Provides: bundled(crate(wasm-bindgen-macro-support)) = 0.2.122
+Provides: bundled(crate(wasm-bindgen-shared)) = 0.2.122
Provides: bundled(crate(wasm-encoder)) = 0.244.0
Provides: bundled(crate(wasm-metadata)) = 0.244.0
Provides: bundled(crate(wasmparser)) = 0.244.0
@@ -176,7 +183,7 @@ Provides: bundled(crate(winapi-util)) = 0.1.11
Provides: bundled(crate(winapi-x86_64-pc-windows-gnu)) = 0.4.0
Provides: bundled(crate(windows-link)) = 0.2.1
Provides: bundled(crate(windows-sys)) = 0.61.2
-Provides: bundled(crate(wit-bindgen)) = 0.51.0
+Provides: bundled(crate(wit-bindgen)) = 0.57.1
Provides: bundled(crate(wit-bindgen-core)) = 0.51.0
Provides: bundled(crate(wit-bindgen-rust)) = 0.51.0
Provides: bundled(crate(wit-bindgen-rust-macro)) = 0.51.0
@@ -197,13 +204,13 @@ Provides: bundled(npm(@patternfly/react-log-viewer)) = 5.3.0
Provides: bundled(npm(@patternfly/react-styles)) = 5.4.0
Provides: bundled(npm(@patternfly/react-table)) = 5.4.1
Provides: bundled(npm(@patternfly/react-tokens)) = 5.4.0
-Provides: bundled(npm(@types/d3-array)) = 3.2.1
+Provides: bundled(npm(@types/d3-array)) = 3.2.2
Provides: bundled(npm(@types/d3-color)) = 3.1.3
Provides: bundled(npm(@types/d3-ease)) = 3.0.2
Provides: bundled(npm(@types/d3-interpolate)) = 3.0.4
Provides: bundled(npm(@types/d3-path)) = 3.1.1
Provides: bundled(npm(@types/d3-scale)) = 4.0.9
-Provides: bundled(npm(@types/d3-shape)) = 3.1.7
+Provides: bundled(npm(@types/d3-shape)) = 3.1.8
Provides: bundled(npm(@types/d3-time)) = 3.0.4
Provides: bundled(npm(@types/d3-timer)) = 3.0.2
Provides: bundled(npm(@xterm/addon-canvas)) = 0.7.0
@@ -215,7 +222,7 @@ Provides: bundled(npm(core-util-is)) = 1.0.3
Provides: bundled(npm(d3-array)) = 3.2.4
Provides: bundled(npm(d3-color)) = 3.1.0
Provides: bundled(npm(d3-ease)) = 3.0.1
-Provides: bundled(npm(d3-format)) = 3.1.0
+Provides: bundled(npm(d3-format)) = 3.1.2
Provides: bundled(npm(d3-interpolate)) = 3.0.1
Provides: bundled(npm(d3-path)) = 3.1.0
Provides: bundled(npm(d3-scale)) = 4.0.2
@@ -240,15 +247,16 @@ Provides: bundled(npm(js-sha256)) = 0.11.0
Provides: bundled(npm(js-tokens)) = 4.0.0
Provides: bundled(npm(json-stable-stringify-without-jsonify)) = 1.0.1
Provides: bundled(npm(json-stringify-safe)) = 5.0.1
-Provides: bundled(npm(lodash)) = 4.17.23
+Provides: bundled(npm(lodash)) = 4.18.1
Provides: bundled(npm(loose-envify)) = 1.4.0
Provides: bundled(npm(memoize-one)) = 5.2.1
Provides: bundled(npm(object-assign)) = 4.1.1
+Provides: bundled(npm(prettier)) = 3.8.3
Provides: bundled(npm(process-nextick-args)) = 2.0.1
Provides: bundled(npm(prop-types)) = 15.8.1
Provides: bundled(npm(react)) = 18.3.1
Provides: bundled(npm(react-dom)) = 18.3.1
-Provides: bundled(npm(react-dropzone)) = 14.3.8
+Provides: bundled(npm(react-dropzone)) = 14.4.1
Provides: bundled(npm(react-fast-compare)) = 3.2.2
Provides: bundled(npm(react-is)) = 16.13.1
Provides: bundled(npm(readable-stream)) = 2.3.8
@@ -258,11 +266,11 @@ Provides: bundled(npm(safer-buffer)) = 2.1.2
Provides: bundled(npm(scheduler)) = 0.23.2
Provides: bundled(npm(sprintf-js)) = 1.0.3
Provides: bundled(npm(string_decoder)) = 1.1.1
-Provides: bundled(npm(tabbable)) = 6.2.0
+Provides: bundled(npm(tabbable)) = 6.4.0
Provides: bundled(npm(throttle-debounce)) = 5.0.2
Provides: bundled(npm(tslib)) = 2.8.1
Provides: bundled(npm(util-deprecate)) = 1.0.2
-Provides: bundled(npm(uuid)) = 10.0.0
+Provides: bundled(npm(uuid)) = 14.0.0
Provides: bundled(npm(victory-area)) = 37.3.6
Provides: bundled(npm(victory-axis)) = 37.3.6
Provides: bundled(npm(victory-bar)) = 37.3.6
@@ -328,6 +336,9 @@ BuildRequires: libdb-devel
BuildRequires: net-snmp-devel
BuildRequires: bzip2-devel
BuildRequires: openssl-devel
+%if %{with hibp}
+BuildRequires: libcurl-devel
+%endif
# the following is for the pam passthru auth plug-in
BuildRequires: pam-devel
BuildRequires: systemd-units
@@ -438,8 +449,7 @@ Please see http://seclists.org/oss-sec/2016/q1/363 for more information.
%if %{with libbdb_ro}
%package robdb-libs
Summary: Read-only Berkeley Database Library
-# IMPORTANT - Check if it looks right. Additionally, compare with the original line. Then, remove this comment and # FIXME - part.
-# FIXME - License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR MIT) AND Unicode-3.0 AND (CC-BY-4.0 AND MIT) AND (MIT OR Unlicense) AND 0BSD AND Apache-2.0 AND BSD-3-Clause AND ISC AND MIT AND MIT AND ISC AND MPL-2.0 AND Zlib
+License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT) AND (CC-BY-4.0 AND MIT) AND (MIT OR Apache-2.0) AND Unicode-3.0 AND (MIT OR CC0-1.0) AND (MIT OR Unlicense) AND 0BSD AND Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND ISC AND MIT AND MIT AND ISC AND MPL-2.0 AND PSF-2.0 AND Zlib
%description robdb-libs
The %{name}-robdb-lib package contains a library derived from rpm
@@ -676,6 +686,9 @@ autoreconf -fiv
%if 0%{?fedora} >= 34 || 0%{?rhel} >= 9
--with-libldap-r=no \
%endif
+%if %{with hibp}
+ --enable-hibp \
+%endif
--enable-cmocka
# Avoid "Unknown key name 'XXX' in section 'Service', ignoring." warnings from systemd on older releases
@@ -827,9 +840,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
@@ -864,8 +874,6 @@ fi
%postun snmp
%systemd_postun_with_restart %{pkgname}-snmp.service
-exit 0
-
%files -f plugins.list
%if %{with bundle_jemalloc}
%doc LICENSE LICENSE.GPLv3+ LICENSE.openssl README.jemalloc
diff --git a/sources b/sources
index c86ccaf..583ac6c 100644
--- a/sources
+++ b/sources
@@ -1,6 +1,6 @@
SHA512 (jemalloc-5.3.0.tar.bz2) = 22907bb052096e2caffb6e4e23548aecc5cc9283dce476896a2b1127eee64170e3562fa2e7db9571298814a7a2c7df6e8d1fbe152bd3f3b0c1abec22a2de34b1
SHA512 (libdb-5.3.28-59.tar.bz2) = 731a434fa2e6487ebb05c458b0437456eb9f7991284beb08cb3e21931e23bdeddddbc95bfabe3a2f9f029fe69cd33a2d4f0f5ce6a9811e9c3b940cb6fde4bf79
-SHA512 (389-ds-base-3.2.1.tar.bz2) = 4ba256baae48e327829c8254393310f8c551e13d4e891076fa19beefa93819ad6058a1303504eebf48a09cbfceef14422083c44e2b1380535ea9090a4e20481e
-SHA512 (Cargo-3.2.1-1.lock) = 77ade2dead86b601ded41f072a307ebc0b7664a838ff42b229c6c64466e35219e6df8a3722c5df3bbda9710ff85862b44766c9fceee8976e6f26f94ba8e32ab4
-SHA512 (vendor-3.2.1-1.tar.gz) = e694c96cd541db5d05c89dc9a0124193b2d668503930f1553ca23201876a84918fa8bad4512e42e0953b9145e237bc9451d55629fc7ab79f4e212b904c3a0104
-SHA512 (cockpit_dist-3.2.1-1.tar.bz2) = 91153b77e0fba8030beaf7483bf799709541f11d915c68ddd851b9284169b41490a3730bb8a49e02c36f480ac1a2dc46a7dfe8125baf54a5e0524b23d048de54
+SHA512 (389-ds-base-3.3.0.tar.bz2) = e543952bf9471d08d6aa1ca08ebbb13421084ae552de10fba592e11d2980f5ef44058de985c56012e16ab8cee02841c6982047af65abba34fecea3b0a999a3d9
+SHA512 (vendor-3.3.0-1.tar.gz) = 49862f0746caf42f00cd18acca12157312a5ab99b537b23837335d442dff11a084ac7f7399a0f947fc0fb1f3a14aa2becbefdaf60ce68c5de1ce7505516d19ba
+SHA512 (Cargo-3.3.0-1.lock) = c46ef0d6d4fca82ff9e2a6f11e5c12a3808c84986ed68c131bad9c7a3a7e8441e88a7b60e2bf2bbd495f512295520b9fd75de623b0ce33e57b39ed026cd796ec
+SHA512 (cockpit_dist-3.3.0-1.tar.bz2) = 9bfafd24b407bdfc57f1bcf4ab5e2881a07d0cec1ffc596ccc84f03f51d9475d72ffdaa380ce117e83e42ae5a0851706cf43583867cea0cacd431d390a8341fa
reply other threads:[~2026-06-04 7:05 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=178055671123.1.4554221671351449944.rpms-389-ds-base-721c66d081e7@fedoraproject.org \
--to=vashirov@redhat.com \
--cc=git-commits@fedoraproject.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox