From ceeccf7e2e6d2a2037c219a83515070ba5f3d16a Mon Sep 17 00:00:00 2001 From: Viktor Ashirov Date: Tue, 5 Aug 2025 19:38:33 +0200 Subject: [PATCH] Bump version to 2.7.0-5 - Resolves: RHEL-89762 - dsidm Error: float() argument must be a string or a number, not 'NoneType' [rhel-9] - Resolves: RHEL-92041 - Memory leak in roles_cache_create_object_from_entry - Resolves: RHEL-95444 - ns-slapd[xxxx]: segfault at 10d7d0d0 ip 00007ff734050cdb sp 00007ff6de9f1430 error 6 in libslapd.so.0.1.0[7ff733ec0000+1b3000] [rhel-9] - Resolves: RHEL-104821 - ipa-restore fails to restore SELinux contexts, causes ns-slapd AVC denials on /dev/shm after restore. - Resolves: RHEL-107005 - Failure to get Server monitoring data when NDN cache is disabled. [rhel-9] - Resolves: RHEL-107581 - segfault - error 4 in libpthread-2.28.so [rhel-9] - Resolves: RHEL-107585 - ns-slapd crashed when we add nsslapd-referral [rhel-9] - Resolves: RHEL-107586 - CWE-284 dirsrv log rotation creates files with world readable permission [rhel-9] - Resolves: RHEL-107587 - CWE-532 Created user password hash available to see in audit log [rhel-9] - Resolves: RHEL-107588 - CWE-778 Log doesn't show what user gets password changed by administrator [rhel-9] --- .gitignore | 2 + ...stance-read-only-mode-is-broken-6681.patch | 6 +- ...N-Access-Control-Plugin-with-wildcar.patch | 125 ++ ...ronise-accept_thread-with-slapd_daem.patch | 50 + ...ue-6782-Improve-paged-result-locking.patch | 127 ++ ...nd-creation-cleanup-and-Database-UI-.patch | 488 +++++ ...iq-allow-specifying-match-rules-in-t.patch | 45 + ...I-Properly-handle-disabled-NDN-cache.patch | 1201 ++++++++++++ ...ilter-is-not-fully-applying-matching.patch | 399 ++++ ...essed-log-rotation-creates-files-wit.patch | 163 ++ ...nt-repeated-disconnect-logs-during-s.patch | 116 ++ ...f-Replicas-with-the-consumer-role-al.patch | 67 + ...ser-that-is-updated-during-password-.patch | 143 ++ ...-if-repl-keep-alive-entry-can-not-be.patch | 98 + ...est-for-entryUSN-overflow-on-failed-.patch | 352 ++++ ...est-for-numSubordinates-replication-.patch | 172 ++ ...k-password-hashes-in-audit-logs-6885.patch | 814 ++++++++ ...isk-monitoring-test-failures-and-imp.patch | 1721 +++++++++++++++++ ...ss-Coverity-scan-issues-in-memberof-.patch | 63 + ...6468-CLI-Fix-default-error-log-level.patch | 31 + ...13-6886-6250-Adjust-xfail-marks-6914.patch | 222 +++ ...llow-system-to-manage-uid-gid-at-sta.patch | 32 + ...y-leak-in-roles_cache_create_object_.patch | 92 + ...y-leak-in-roles_cache_create_object_.patch | 262 +++ ...essSanitizer-memory-leak-in-mdb_init.patch | 65 + ...8-AddressSanitizer-leak-in-do_search.patch | 58 + ...ssSanitizer-leak-in-agmt_update_init.patch | 58 + ...apd-crashes-when-a-referral-is-added.patch | 97 + 389-ds-base.spec | 87 +- sources | 2 + 30 files changed, 7136 insertions(+), 22 deletions(-) create mode 100644 0004-Issue-6825-RootDN-Access-Control-Plugin-with-wildcar.patch create mode 100644 0005-Issue-6119-Synchronise-accept_thread-with-slapd_daem.patch create mode 100644 0006-Issue-6782-Improve-paged-result-locking.patch create mode 100644 0007-Issue-6822-Backend-creation-cleanup-and-Database-UI-.patch create mode 100644 0008-Issue-6857-uiduniq-allow-specifying-match-rules-in-t.patch create mode 100644 0009-Issue-6756-CLI-UI-Properly-handle-disabled-NDN-cache.patch create mode 100644 0010-Issue-6859-str2filter-is-not-fully-applying-matching.patch create mode 100644 0011-Issue-6872-compressed-log-rotation-creates-files-wit.patch create mode 100644 0012-Issue-6878-Prevent-repeated-disconnect-logs-during-s.patch create mode 100644 0013-Issue-6772-dsconf-Replicas-with-the-consumer-role-al.patch create mode 100644 0014-Issue-6893-Log-user-that-is-updated-during-password-.patch create mode 100644 0015-Issue-6895-Crash-if-repl-keep-alive-entry-can-not-be.patch create mode 100644 0016-Issue-6250-Add-test-for-entryUSN-overflow-on-failed-.patch create mode 100644 0017-Issue-6594-Add-test-for-numSubordinates-replication-.patch create mode 100644 0018-Issue-6884-Mask-password-hashes-in-audit-logs-6885.patch create mode 100644 0019-Issue-6897-Fix-disk-monitoring-test-failures-and-imp.patch create mode 100644 0020-Issue-6339-Address-Coverity-scan-issues-in-memberof-.patch create mode 100644 0021-Issue-6468-CLI-Fix-default-error-log-level.patch create mode 100644 0022-Issues-6913-6886-6250-Adjust-xfail-marks-6914.patch create mode 100644 0023-Issue-6181-RFE-Allow-system-to-manage-uid-gid-at-sta.patch create mode 100644 0024-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch create mode 100644 0025-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch create mode 100644 0026-Issue-6850-AddressSanitizer-memory-leak-in-mdb_init.patch create mode 100644 0027-Issue-6848-AddressSanitizer-leak-in-do_search.patch create mode 100644 0028-Issue-6865-AddressSanitizer-leak-in-agmt_update_init.patch create mode 100644 0029-Issue-6768-ns-slapd-crashes-when-a-referral-is-added.patch diff --git a/.gitignore b/.gitignore index 18ea466..71ff1b8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /389-ds-base-*.tar.bz2 /jemalloc-*.tar.bz2 /libdb-5.3.28-59.tar.bz2 +/Cargo-*.lock +/vendor-*.tar.gz diff --git a/0003-Issue-6680-instance-read-only-mode-is-broken-6681.patch b/0003-Issue-6680-instance-read-only-mode-is-broken-6681.patch index 338b090..ac0af7b 100644 --- a/0003-Issue-6680-instance-read-only-mode-is-broken-6681.patch +++ b/0003-Issue-6680-instance-read-only-mode-is-broken-6681.patch @@ -16,7 +16,7 @@ Reviewed by: @droideck, @tbordaz (thanks!) 3 files changed, 247 insertions(+), 13 deletions(-) diff --git a/dirsrvtests/tests/suites/config/regression_test.py b/dirsrvtests/tests/suites/config/regression_test.py -index 8dbba8cd2f..6e313ac8ab 100644 +index 8dbba8cd2..6e313ac8a 100644 --- a/dirsrvtests/tests/suites/config/regression_test.py +++ b/dirsrvtests/tests/suites/config/regression_test.py @@ -28,6 +28,8 @@ CUSTOM_MEM = '9100100100' @@ -91,7 +91,7 @@ index 8dbba8cd2f..6e313ac8ab 100644 + assert inst.config.get_attr_val_utf8(RO_ATTR) == "off" + inst.config.replace(attr, val) diff --git a/ldap/servers/slapd/dse.c b/ldap/servers/slapd/dse.c -index e3157c1ce5..0f266f0d70 100644 +index e3157c1ce..0f266f0d7 100644 --- a/ldap/servers/slapd/dse.c +++ b/ldap/servers/slapd/dse.c @@ -1031,6 +1031,114 @@ dse_check_for_readonly_error(Slapi_PBlock *pb, struct dse *pdse) @@ -219,7 +219,7 @@ index e3157c1ce5..0f266f0d70 100644 } diff --git a/ldap/servers/slapd/mapping_tree.c b/ldap/servers/slapd/mapping_tree.c -index dd7b1af37c..e51b3b9484 100644 +index dd7b1af37..e51b3b948 100644 --- a/ldap/servers/slapd/mapping_tree.c +++ b/ldap/servers/slapd/mapping_tree.c @@ -2058,6 +2058,82 @@ slapi_dn_write_needs_referral(Slapi_DN *target_sdn, Slapi_Entry **referral) diff --git a/0004-Issue-6825-RootDN-Access-Control-Plugin-with-wildcar.patch b/0004-Issue-6825-RootDN-Access-Control-Plugin-with-wildcar.patch new file mode 100644 index 0000000..67ee402 --- /dev/null +++ b/0004-Issue-6825-RootDN-Access-Control-Plugin-with-wildcar.patch @@ -0,0 +1,125 @@ +From 5613937623f0037a54490b22c60f7eb1aa52cf4e Mon Sep 17 00:00:00 2001 +From: James Chapman +Date: Wed, 25 Jun 2025 14:11:05 +0000 +Subject: [PATCH] =?UTF-8?q?Issue=206825=20-=20RootDN=20Access=20Control=20?= + =?UTF-8?q?Plugin=20with=20wildcards=20for=20IP=20addre=E2=80=A6=20(#6826)?= +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Bug description: +RootDN Access Control Plugin with wildcards for IP addresses fails withi +an error "Invalid IP address" + +socket.inet_aton() validates IPv4 IP addresses and does not support wildcards. + +Fix description: +Add a regex pattern to match wildcard IP addresses, check each octet is +between 0-255 + +Fixes: https://github.com/389ds/389-ds-base/issues/6825 + +Reviewed by: @droideck (Thank you) +--- + .../lib389/cli_conf/plugins/rootdn_ac.py | 16 +++----- + src/lib389/lib389/utils.py | 40 +++++++++++++++++++ + 2 files changed, 45 insertions(+), 11 deletions(-) + +diff --git a/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py b/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py +index 65486fff8..1456f5ebe 100644 +--- a/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py ++++ b/src/lib389/lib389/cli_conf/plugins/rootdn_ac.py +@@ -8,7 +8,7 @@ + + import socket + from lib389.plugins import RootDNAccessControlPlugin +-from lib389.utils import is_valid_hostname ++from lib389.utils import is_valid_hostname, is_valid_ip + from lib389.cli_conf import add_generic_plugin_parsers, generic_object_edit + from lib389.cli_base import CustomHelpFormatter + +@@ -62,19 +62,13 @@ def validate_args(args): + + if args.allow_ip is not None: + for ip in args.allow_ip: +- if ip != "delete": +- try: +- socket.inet_aton(ip) +- except socket.error: +- raise ValueError(f"Invalid IP address ({ip}) for '--allow-ip'") ++ if ip != "delete" and not is_valid_ip(ip): ++ raise ValueError(f"Invalid IP address ({ip}) for '--allow-ip'") + + if args.deny_ip is not None and args.deny_ip != "delete": + for ip in args.deny_ip: +- if ip != "delete": +- try: +- socket.inet_aton(ip) +- except socket.error: +- raise ValueError(f"Invalid IP address ({ip}) for '--deny-ip'") ++ if ip != "delete" and not is_valid_ip(ip): ++ raise ValueError(f"Invalid IP address ({ip}) for '--deny-ip'") + + if args.allow_host is not None: + for hostname in args.allow_host: +diff --git a/src/lib389/lib389/utils.py b/src/lib389/lib389/utils.py +index afc282e94..3937fc1a8 100644 +--- a/src/lib389/lib389/utils.py ++++ b/src/lib389/lib389/utils.py +@@ -31,6 +31,7 @@ import logging + import shutil + import ldap + import socket ++import ipaddress + import time + import stat + from datetime import (datetime, timedelta) +@@ -1707,6 +1708,45 @@ def is_valid_hostname(hostname): + allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(? +Date: Fri, 8 Mar 2024 16:15:52 +0000 +Subject: [PATCH] Issue 6119 - Synchronise accept_thread with slapd_daemon + (#6120) + +Bug Description: A corner cases exists, where the slapd_daemon has +begun its shutdown process but the accept_thread is still running +and capable of handling new connections. When this scenario occurs, +the connection subsystem has been partially deallocated and is in +an unstable state. A segfault is generated when attempting to get a +new connection from the connection table. + +Fix Description: The connection table is only deallocated when the +number of active threads is 0. Modify the accept_thread to adjust the +the active thread count during creation/destruction, meaning the connection +table can only be freed when the accept_thread has completed + +Relates: https://github.com/389ds/389-ds-base/issues/6119 + +Reviewed by: @tbordaz, @Firstyear , @mreynolds389 (Thank you) +--- + ldap/servers/slapd/daemon.c | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/ldap/servers/slapd/daemon.c b/ldap/servers/slapd/daemon.c +index 5d01a2526..a43fc9285 100644 +--- a/ldap/servers/slapd/daemon.c ++++ b/ldap/servers/slapd/daemon.c +@@ -868,6 +868,8 @@ accept_thread(void *vports) + slapi_ch_free((void **)&listener_idxs); + slapd_sockets_ports_free(ports); + slapi_ch_free((void **)&fds); ++ g_decr_active_threadcnt(); ++ slapi_log_err(SLAPI_LOG_INFO, "slapd_daemon", "slapd shutting down - accept_thread\n"); + } + + void +@@ -1158,6 +1160,8 @@ slapd_daemon(daemon_ports_t *ports) + slapi_log_err(SLAPI_LOG_EMERG, "slapd_daemon", "Unable to fd accept thread - Shutting Down (" SLAPI_COMPONENT_NAME_NSPR " error %d - %s)\n", + errorCode, slapd_pr_strerror(errorCode)); + g_set_shutdown(SLAPI_SHUTDOWN_EXIT); ++ } else{ ++ g_incr_active_threadcnt(); + } + + #ifdef WITH_SYSTEMD +-- +2.49.0 + diff --git a/0006-Issue-6782-Improve-paged-result-locking.patch b/0006-Issue-6782-Improve-paged-result-locking.patch new file mode 100644 index 0000000..5c9558b --- /dev/null +++ b/0006-Issue-6782-Improve-paged-result-locking.patch @@ -0,0 +1,127 @@ +From 7943443bb92fca6676922349fb12503a527cb6b1 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Thu, 15 May 2025 10:35:27 -0400 +Subject: [PATCH] Issue 6782 - Improve paged result locking + +Description: + +When cleaning a slot, instead of mem setting everything to Zero and restoring +the mutex, manually reset all the values leaving the mutex pointer +intact. + +There is also a deadlock possibility when checking for abandoned PR search +in opshared.c, and we were checking a flag value outside of the per_conn +lock. + +Relates: https://github.com/389ds/389-ds-base/issues/6782 + +Reviewed by: progier & spichugi(Thanks!!) +--- + ldap/servers/slapd/opshared.c | 10 +++++++++- + ldap/servers/slapd/pagedresults.c | 27 +++++++++++++++++---------- + 2 files changed, 26 insertions(+), 11 deletions(-) + +diff --git a/ldap/servers/slapd/opshared.c b/ldap/servers/slapd/opshared.c +index 7dc2d5983..14a7dcdfb 100644 +--- a/ldap/servers/slapd/opshared.c ++++ b/ldap/servers/slapd/opshared.c +@@ -592,6 +592,14 @@ 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() ++ */ + pagedresults_mutex = pageresult_lock_get_addr(pb_conn); + } + +@@ -717,11 +725,11 @@ op_shared_search(Slapi_PBlock *pb, int send_result) + pr_search_result = pagedresults_get_search_result(pb_conn, operation, 1 /*locked*/, pr_idx); + if (pr_search_result) { + if (pagedresults_is_abandoned_or_notavailable(pb_conn, 1 /*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; +- pthread_mutex_unlock(pagedresults_mutex); + goto free_and_return; + } else { + slapi_pblock_set(pb, SLAPI_SEARCH_RESULT_SET, pr_search_result); +diff --git a/ldap/servers/slapd/pagedresults.c b/ldap/servers/slapd/pagedresults.c +index 642aefb3d..c3f3aae01 100644 +--- a/ldap/servers/slapd/pagedresults.c ++++ b/ldap/servers/slapd/pagedresults.c +@@ -48,7 +48,6 @@ pageresult_lock_get_addr(Connection *conn) + static void + _pr_cleanup_one_slot(PagedResults *prp) + { +- PRLock *prmutex = NULL; + if (!prp) { + return; + } +@@ -56,13 +55,17 @@ _pr_cleanup_one_slot(PagedResults *prp) + /* sr is left; release it. */ + prp->pr_current_be->be_search_results_release(&(prp->pr_search_result_set)); + } +- /* clean up the slot */ +- if (prp->pr_mutex) { +- /* pr_mutex is reused; back it up and reset it. */ +- prmutex = prp->pr_mutex; +- } +- memset(prp, '\0', sizeof(PagedResults)); +- prp->pr_mutex = prmutex; ++ ++ /* clean up the slot except the mutex */ ++ prp->pr_current_be = NULL; ++ prp->pr_search_result_set = NULL; ++ prp->pr_search_result_count = 0; ++ prp->pr_search_result_set_size_estimate = 0; ++ prp->pr_sort_result_code = 0; ++ prp->pr_timelimit_hr.tv_sec = 0; ++ prp->pr_timelimit_hr.tv_nsec = 0; ++ prp->pr_flags = 0; ++ prp->pr_msgid = 0; + } + + /* +@@ -1007,7 +1010,8 @@ op_set_pagedresults(Operation *op) + + /* + * pagedresults_lock/unlock -- introduced to protect search results for the +- * asynchronous searches. ++ * 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) +@@ -1045,6 +1049,8 @@ int + pagedresults_is_abandoned_or_notavailable(Connection *conn, int locked, int index) + { + PagedResults *prp; ++ int32_t result; ++ + if (!conn || (index < 0) || (index >= conn->c_pagedresults.prl_maxlen)) { + return 1; /* not abandoned, but do not want to proceed paged results op. */ + } +@@ -1052,10 +1058,11 @@ pagedresults_is_abandoned_or_notavailable(Connection *conn, int locked, int inde + pthread_mutex_lock(pageresult_lock_get_addr(conn)); + } + prp = conn->c_pagedresults.prl_list + index; ++ result = prp->pr_flags & CONN_FLAG_PAGEDRESULTS_ABANDONED; + if (!locked) { + pthread_mutex_unlock(pageresult_lock_get_addr(conn)); + } +- return prp->pr_flags & CONN_FLAG_PAGEDRESULTS_ABANDONED; ++ return result; + } + + int +-- +2.49.0 + diff --git a/0007-Issue-6822-Backend-creation-cleanup-and-Database-UI-.patch b/0007-Issue-6822-Backend-creation-cleanup-and-Database-UI-.patch new file mode 100644 index 0000000..cd0029d --- /dev/null +++ b/0007-Issue-6822-Backend-creation-cleanup-and-Database-UI-.patch @@ -0,0 +1,488 @@ +From b6729a99f3a3d4c6ebe82d4bb60ea2a6f8727782 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Fri, 27 Jun 2025 18:43:39 -0700 +Subject: [PATCH] Issue 6822 - Backend creation cleanup and Database UI tab + error handling (#6823) + +Description: Add rollback functionality when mapping tree creation fails +during backend creation to prevent orphaned backends. +Improve error handling in Database, Replication and Monitoring UI tabs +to gracefully handle backend get-tree command failures. + +Fixes: https://github.com/389ds/389-ds-base/issues/6822 + +Reviewed by: @mreynolds389 (Thanks!) +--- + src/cockpit/389-console/src/database.jsx | 119 ++++++++------ + src/cockpit/389-console/src/monitor.jsx | 172 +++++++++++--------- + src/cockpit/389-console/src/replication.jsx | 55 ++++--- + src/lib389/lib389/backend.py | 18 +- + 4 files changed, 210 insertions(+), 154 deletions(-) + +diff --git a/src/cockpit/389-console/src/database.jsx b/src/cockpit/389-console/src/database.jsx +index c0c4be414..276125dfc 100644 +--- a/src/cockpit/389-console/src/database.jsx ++++ b/src/cockpit/389-console/src/database.jsx +@@ -478,6 +478,59 @@ export class Database extends React.Component { + } + + loadSuffixTree(fullReset) { ++ const treeData = [ ++ { ++ name: _("Global Database Configuration"), ++ icon: , ++ id: "dbconfig", ++ }, ++ { ++ name: _("Chaining Configuration"), ++ icon: , ++ id: "chaining-config", ++ }, ++ { ++ name: _("Backups & LDIFs"), ++ icon: , ++ id: "backups", ++ }, ++ { ++ name: _("Password Policies"), ++ id: "pwp", ++ icon: , ++ children: [ ++ { ++ name: _("Global Policy"), ++ icon: , ++ id: "pwpolicy", ++ }, ++ { ++ name: _("Local Policies"), ++ icon: , ++ id: "localpwpolicy", ++ }, ++ ], ++ defaultExpanded: true ++ }, ++ { ++ name: _("Suffixes"), ++ icon: , ++ id: "suffixes-tree", ++ children: [], ++ defaultExpanded: true, ++ action: ( ++ ++ ), ++ } ++ ]; ++ + const cmd = [ + "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "backend", "get-tree", +@@ -491,58 +544,20 @@ export class Database extends React.Component { + suffixData = JSON.parse(content); + this.processTree(suffixData); + } +- const treeData = [ +- { +- name: _("Global Database Configuration"), +- icon: , +- id: "dbconfig", +- }, +- { +- name: _("Chaining Configuration"), +- icon: , +- id: "chaining-config", +- }, +- { +- name: _("Backups & LDIFs"), +- icon: , +- id: "backups", +- }, +- { +- name: _("Password Policies"), +- id: "pwp", +- icon: , +- children: [ +- { +- name: _("Global Policy"), +- icon: , +- id: "pwpolicy", +- }, +- { +- name: _("Local Policies"), +- icon: , +- id: "localpwpolicy", +- }, +- ], +- defaultExpanded: true +- }, +- { +- name: _("Suffixes"), +- icon: , +- id: "suffixes-tree", +- children: suffixData, +- defaultExpanded: true, +- action: ( +- +- ), +- } +- ]; ++ ++ let current_node = this.state.node_name; ++ if (fullReset) { ++ current_node = DB_CONFIG; ++ } ++ ++ treeData[4].children = suffixData; // suffixes node ++ this.setState(() => ({ ++ nodes: treeData, ++ node_name: current_node, ++ }), this.loadAttrs); ++ }) ++ .fail(err => { ++ // Handle backend get-tree failure gracefully + let current_node = this.state.node_name; + if (fullReset) { + current_node = DB_CONFIG; +diff --git a/src/cockpit/389-console/src/monitor.jsx b/src/cockpit/389-console/src/monitor.jsx +index ad48d1f87..91a8e3e37 100644 +--- a/src/cockpit/389-console/src/monitor.jsx ++++ b/src/cockpit/389-console/src/monitor.jsx +@@ -200,6 +200,84 @@ export class Monitor extends React.Component { + } + + loadSuffixTree(fullReset) { ++ const basicData = [ ++ { ++ name: _("Server Statistics"), ++ icon: , ++ id: "server-monitor", ++ type: "server", ++ }, ++ { ++ name: _("Replication"), ++ icon: , ++ id: "replication-monitor", ++ type: "replication", ++ defaultExpanded: true, ++ children: [ ++ { ++ name: _("Synchronization Report"), ++ icon: , ++ id: "sync-report", ++ item: "sync-report", ++ type: "repl-mon", ++ }, ++ { ++ name: _("Log Analysis"), ++ icon: , ++ id: "log-analysis", ++ item: "log-analysis", ++ type: "repl-mon", ++ } ++ ], ++ }, ++ { ++ name: _("Database"), ++ icon: , ++ id: "database-monitor", ++ type: "database", ++ children: [], // Will be populated with treeData on success ++ defaultExpanded: true, ++ }, ++ { ++ name: _("Logging"), ++ icon: , ++ id: "log-monitor", ++ defaultExpanded: true, ++ children: [ ++ { ++ name: _("Access Log"), ++ icon: , ++ id: "access-log-monitor", ++ type: "log", ++ }, ++ { ++ name: _("Audit Log"), ++ icon: , ++ id: "audit-log-monitor", ++ type: "log", ++ }, ++ { ++ name: _("Audit Failure Log"), ++ icon: , ++ id: "auditfail-log-monitor", ++ type: "log", ++ }, ++ { ++ name: _("Errors Log"), ++ icon: , ++ id: "error-log-monitor", ++ type: "log", ++ }, ++ { ++ name: _("Security Log"), ++ icon: , ++ id: "security-log-monitor", ++ type: "log", ++ }, ++ ] ++ }, ++ ]; ++ + const cmd = [ + "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "backend", "get-tree", +@@ -210,83 +288,7 @@ export class Monitor extends React.Component { + .done(content => { + const treeData = JSON.parse(content); + this.processTree(treeData); +- const basicData = [ +- { +- name: _("Server Statistics"), +- icon: , +- id: "server-monitor", +- type: "server", +- }, +- { +- name: _("Replication"), +- icon: , +- id: "replication-monitor", +- type: "replication", +- defaultExpanded: true, +- children: [ +- { +- name: _("Synchronization Report"), +- icon: , +- id: "sync-report", +- item: "sync-report", +- type: "repl-mon", +- }, +- { +- name: _("Log Analysis"), +- icon: , +- id: "log-analysis", +- item: "log-analysis", +- type: "repl-mon", +- } +- ], +- }, +- { +- name: _("Database"), +- icon: , +- id: "database-monitor", +- type: "database", +- children: [], +- defaultExpanded: true, +- }, +- { +- name: _("Logging"), +- icon: , +- id: "log-monitor", +- defaultExpanded: true, +- children: [ +- { +- name: _("Access Log"), +- icon: , +- id: "access-log-monitor", +- type: "log", +- }, +- { +- name: _("Audit Log"), +- icon: , +- id: "audit-log-monitor", +- type: "log", +- }, +- { +- name: _("Audit Failure Log"), +- icon: , +- id: "auditfail-log-monitor", +- type: "log", +- }, +- { +- name: _("Errors Log"), +- icon: , +- id: "error-log-monitor", +- type: "log", +- }, +- { +- name: _("Security Log"), +- icon: , +- id: "security-log-monitor", +- type: "log", +- }, +- ] +- }, +- ]; ++ + let current_node = this.state.node_name; + let type = this.state.node_type; + if (fullReset) { +@@ -296,6 +298,22 @@ export class Monitor extends React.Component { + basicData[2].children = treeData; // database node + this.processReplSuffixes(basicData[1].children); + ++ this.setState(() => ({ ++ nodes: basicData, ++ node_name: current_node, ++ node_type: type, ++ }), this.update_tree_nodes); ++ }) ++ .fail(err => { ++ // Handle backend get-tree failure gracefully ++ let current_node = this.state.node_name; ++ let type = this.state.node_type; ++ if (fullReset) { ++ current_node = "server-monitor"; ++ type = "server"; ++ } ++ this.processReplSuffixes(basicData[1].children); ++ + this.setState(() => ({ + nodes: basicData, + node_name: current_node, +diff --git a/src/cockpit/389-console/src/replication.jsx b/src/cockpit/389-console/src/replication.jsx +index fa492fd2a..aa535bfc7 100644 +--- a/src/cockpit/389-console/src/replication.jsx ++++ b/src/cockpit/389-console/src/replication.jsx +@@ -177,6 +177,16 @@ export class Replication extends React.Component { + loaded: false + }); + ++ const basicData = [ ++ { ++ name: _("Suffixes"), ++ icon: , ++ id: "repl-suffixes", ++ children: [], ++ defaultExpanded: true ++ } ++ ]; ++ + const cmd = [ + "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", + "backend", "get-tree", +@@ -199,15 +209,7 @@ export class Replication extends React.Component { + } + } + } +- const basicData = [ +- { +- name: _("Suffixes"), +- icon: , +- id: "repl-suffixes", +- children: [], +- defaultExpanded: true +- } +- ]; ++ + let current_node = this.state.node_name; + let current_type = this.state.node_type; + let replicated = this.state.node_replicated; +@@ -258,6 +260,19 @@ export class Replication extends React.Component { + } + + basicData[0].children = treeData; ++ this.setState({ ++ nodes: basicData, ++ node_name: current_node, ++ node_type: current_type, ++ node_replicated: replicated, ++ }, () => { this.update_tree_nodes() }); ++ }) ++ .fail(err => { ++ // Handle backend get-tree failure gracefully ++ let current_node = this.state.node_name; ++ let current_type = this.state.node_type; ++ let replicated = this.state.node_replicated; ++ + this.setState({ + nodes: basicData, + node_name: current_node, +@@ -905,18 +920,18 @@ export class Replication extends React.Component { + disableTree: false + }); + }); +- }) +- .fail(err => { +- const errMsg = JSON.parse(err); +- this.props.addNotification( +- "error", +- cockpit.format(_("Error loading replication agreements configuration - $0"), errMsg.desc) +- ); +- this.setState({ +- suffixLoading: false, +- disableTree: false ++ }) ++ .fail(err => { ++ const errMsg = JSON.parse(err); ++ this.props.addNotification( ++ "error", ++ cockpit.format(_("Error loading replication agreements configuration - $0"), errMsg.desc) ++ ); ++ this.setState({ ++ suffixLoading: false, ++ disableTree: false ++ }); + }); +- }); + }) + .fail(err => { + // changelog failure +diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py +index 1319fa0cd..5bff61c58 100644 +--- a/src/lib389/lib389/backend.py ++++ b/src/lib389/lib389/backend.py +@@ -694,24 +694,32 @@ class Backend(DSLdapObject): + parent_suffix = properties.pop('parent', False) + + # Okay, now try to make the backend. +- super(Backend, self).create(dn, properties, basedn) ++ backend_obj = super(Backend, self).create(dn, properties, basedn) + + # We check if the mapping tree exists in create, so do this *after* + if create_mapping_tree is True: +- properties = { ++ mapping_tree_properties = { + 'cn': self._nprops_stash['nsslapd-suffix'], + 'nsslapd-state': 'backend', + 'nsslapd-backend': self._nprops_stash['cn'], + } + if parent_suffix: + # This is a subsuffix, set the parent suffix +- properties['nsslapd-parent-suffix'] = parent_suffix +- self._mts.create(properties=properties) ++ mapping_tree_properties['nsslapd-parent-suffix'] = parent_suffix ++ ++ try: ++ self._mts.create(properties=mapping_tree_properties) ++ except Exception as e: ++ try: ++ backend_obj.delete() ++ except Exception as cleanup_error: ++ self._instance.log.error(f"Failed to cleanup backend after mapping tree creation failure: {cleanup_error}") ++ raise e + + # We can't create the sample entries unless a mapping tree was installed. + if sample_entries is not False and create_mapping_tree is True: + self.create_sample_entries(sample_entries) +- return self ++ return backend_obj + + def delete(self): + """Deletes the backend, it's mapping tree and all related indices. +-- +2.49.0 + diff --git a/0008-Issue-6857-uiduniq-allow-specifying-match-rules-in-t.patch b/0008-Issue-6857-uiduniq-allow-specifying-match-rules-in-t.patch new file mode 100644 index 0000000..5d62b75 --- /dev/null +++ b/0008-Issue-6857-uiduniq-allow-specifying-match-rules-in-t.patch @@ -0,0 +1,45 @@ +From 0a7fe7c6e18759459499f468443ded4313ebdeab Mon Sep 17 00:00:00 2001 +From: Alexander Bokovoy +Date: Wed, 9 Jul 2025 12:08:09 +0300 +Subject: [PATCH] Issue 6857 - uiduniq: allow specifying match rules in the + filter + +Allow uniqueness plugin to work with attributes where uniqueness should +be enforced using different matching rule than the one defined for the +attribute itself. + +Since uniqueness plugin configuration can contain multiple attributes, +add matching rule right to the attribute as it is used in the LDAP rule +(e.g. 'attribute:caseIgnoreMatch:' to force 'attribute' to be searched +with case-insensitive matching rule instead of the original matching +rule. + +Fixes: https://github.com/389ds/389-ds-base/issues/6857 + +Signed-off-by: Alexander Bokovoy +--- + ldap/servers/plugins/uiduniq/uid.c | 7 +++++++ + 1 file changed, 7 insertions(+) + +diff --git a/ldap/servers/plugins/uiduniq/uid.c b/ldap/servers/plugins/uiduniq/uid.c +index 053af4f9d..887e79d78 100644 +--- a/ldap/servers/plugins/uiduniq/uid.c ++++ b/ldap/servers/plugins/uiduniq/uid.c +@@ -1030,7 +1030,14 @@ preop_add(Slapi_PBlock *pb) + } + + for (i = 0; attrNames && attrNames[i]; i++) { ++ char *attr_match = strchr(attrNames[i], ':'); ++ if (attr_match != NULL) { ++ attr_match[0] = '\0'; ++ } + err = slapi_entry_attr_find(e, attrNames[i], &attr); ++ if (attr_match != NULL) { ++ attr_match[0] = ':'; ++ } + if (!err) { + /* + * Passed all the requirements - this is an operation we +-- +2.49.0 + diff --git a/0009-Issue-6756-CLI-UI-Properly-handle-disabled-NDN-cache.patch b/0009-Issue-6756-CLI-UI-Properly-handle-disabled-NDN-cache.patch new file mode 100644 index 0000000..f519007 --- /dev/null +++ b/0009-Issue-6756-CLI-UI-Properly-handle-disabled-NDN-cache.patch @@ -0,0 +1,1201 @@ +From b28b00ee5169cfb00414bc9bcca67f88432ad567 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Thu, 10 Jul 2025 11:53:12 -0700 +Subject: [PATCH] Issue 6756 - CLI, UI - Properly handle disabled NDN cache + (#6757) + +Description: Fix the db_monitor function in monitor.py to check if +nsslapd-ndn-cache-enabled is off and conditionally include NDN cache +statistics only when enabled. + +Update dbMonitor.jsx components to detect when NDN cache is disabled and +conditionally render NDN cache tabs, charts, and related content with proper +fallback display when disabled. + +Add test_ndn_cache_disabled to verify both JSON and non-JSON output formats +correctly handle when NDN cache is turned off and on. + +Fixes: https://github.com/389ds/389-ds-base/issues/6756 + +Reviewed by: @mreynolds389 (Thanks!) +--- + dirsrvtests/tests/suites/clu/dbmon_test.py | 90 +++ + src/cockpit/389-console/src/database.jsx | 4 +- + .../src/lib/database/databaseConfig.jsx | 48 +- + .../389-console/src/lib/monitor/dbMonitor.jsx | 735 ++++++++++-------- + src/lib389/lib389/cli_conf/monitor.py | 77 +- + 5 files changed, 580 insertions(+), 374 deletions(-) + +diff --git a/dirsrvtests/tests/suites/clu/dbmon_test.py b/dirsrvtests/tests/suites/clu/dbmon_test.py +index 4a82eb0ef..b04ee67c9 100644 +--- a/dirsrvtests/tests/suites/clu/dbmon_test.py ++++ b/dirsrvtests/tests/suites/clu/dbmon_test.py +@@ -11,6 +11,7 @@ import subprocess + import pytest + import json + import glob ++import re + + from lib389.tasks import * + from lib389.utils import * +@@ -274,6 +275,95 @@ def test_dbmon_mp_pagesize(topology_st): + assert real_free_percentage == dbmon_free_percentage + + ++def test_ndn_cache_disabled(topology_st): ++ """Test dbmon output when ndn-cache-enabled is turned off ++ ++ :id: 760e217c-70e8-4767-b504-dda7ba2e1f64 ++ :setup: Standalone instance ++ :steps: ++ 1. Run dbmon with nsslapd-ndn-cache-enabled=on (default) ++ 2. Verify NDN cache stats are present in the output ++ 3. Set nsslapd-ndn-cache-enabled=off and restart ++ 4. Run dbmon again and verify NDN cache stats are not present ++ 5. Set nsslapd-ndn-cache-enabled=on and restart ++ 6. Run dbmon again and verify NDN cache stats are back ++ :expectedresults: ++ 1. Success ++ 2. Should display NDN cache data ++ 3. Success ++ 4. Should not display NDN cache data ++ 5. Success ++ 6. Should display NDN cache data ++ """ ++ inst = topology_st.standalone ++ args = FakeArgs() ++ args.backends = None ++ args.indexes = False ++ args.json = True ++ lc = LogCapture() ++ ++ log.info("Testing with NDN cache enabled (default)") ++ db_monitor(inst, DEFAULT_SUFFIX, lc.log, args) ++ db_mon_as_str = "".join((str(rec) for rec in lc.outputs)) ++ db_mon_as_str = re.sub("^[^{]*{", "{", db_mon_as_str)[:-2] ++ db_mon = json.loads(db_mon_as_str) ++ ++ assert 'ndncache' in db_mon ++ assert 'hit_ratio' in db_mon['ndncache'] ++ lc.flush() ++ ++ log.info("Setting nsslapd-ndn-cache-enabled to OFF") ++ inst.config.set('nsslapd-ndn-cache-enabled', 'off') ++ inst.restart() ++ ++ log.info("Testing with NDN cache disabled") ++ db_monitor(inst, DEFAULT_SUFFIX, lc.log, args) ++ db_mon_as_str = "".join((str(rec) for rec in lc.outputs)) ++ db_mon_as_str = re.sub("^[^{]*{", "{", db_mon_as_str)[:-2] ++ db_mon = json.loads(db_mon_as_str) ++ ++ assert 'ndncache' not in db_mon ++ lc.flush() ++ ++ log.info("Setting nsslapd-ndn-cache-enabled to ON") ++ inst.config.set('nsslapd-ndn-cache-enabled', 'on') ++ inst.restart() ++ ++ log.info("Testing with NDN cache re-enabled") ++ db_monitor(inst, DEFAULT_SUFFIX, lc.log, args) ++ db_mon_as_str = "".join((str(rec) for rec in lc.outputs)) ++ db_mon_as_str = re.sub("^[^{]*{", "{", db_mon_as_str)[:-2] ++ db_mon = json.loads(db_mon_as_str) ++ ++ assert 'ndncache' in db_mon ++ assert 'hit_ratio' in db_mon['ndncache'] ++ lc.flush() ++ ++ args.json = False ++ ++ log.info("Testing with NDN cache enabled - non-JSON output") ++ db_monitor(inst, DEFAULT_SUFFIX, lc.log, args) ++ output = "".join((str(rec) for rec in lc.outputs)) ++ ++ assert "Normalized DN Cache:" in output ++ assert "Cache Hit Ratio:" in output ++ lc.flush() ++ ++ log.info("Setting nsslapd-ndn-cache-enabled to OFF") ++ inst.config.set('nsslapd-ndn-cache-enabled', 'off') ++ inst.restart() ++ ++ log.info("Testing with NDN cache disabled - non-JSON output") ++ db_monitor(inst, DEFAULT_SUFFIX, lc.log, args) ++ output = "".join((str(rec) for rec in lc.outputs)) ++ ++ assert "Normalized DN Cache:" not in output ++ lc.flush() ++ ++ inst.config.set('nsslapd-ndn-cache-enabled', 'on') ++ inst.restart() ++ ++ + if __name__ == '__main__': + # Run isolated + # -s for DEBUG mode +diff --git a/src/cockpit/389-console/src/database.jsx b/src/cockpit/389-console/src/database.jsx +index 276125dfc..86b642b92 100644 +--- a/src/cockpit/389-console/src/database.jsx ++++ b/src/cockpit/389-console/src/database.jsx +@@ -198,7 +198,7 @@ export class Database extends React.Component { + }); + const cmd = [ + "dsconf", "-j", "ldapi://%2fvar%2frun%2fslapd-" + this.props.serverId + ".socket", +- "config", "get", "nsslapd-ndn-cache-max-size" ++ "config", "get", "nsslapd-ndn-cache-max-size", "nsslapd-ndn-cache-enabled" + ]; + log_cmd("loadNDN", "Load NDN cache size", cmd); + cockpit +@@ -206,10 +206,12 @@ export class Database extends React.Component { + .done(content => { + const config = JSON.parse(content); + const attrs = config.attrs; ++ const ndn_cache_enabled = attrs['nsslapd-ndn-cache-enabled'][0] === "on"; + this.setState(prevState => ({ + globalDBConfig: { + ...prevState.globalDBConfig, + ndncachemaxsize: attrs['nsslapd-ndn-cache-max-size'][0], ++ ndn_cache_enabled: ndn_cache_enabled, + }, + configUpdated: 0, + loaded: true, +diff --git a/src/cockpit/389-console/src/lib/database/databaseConfig.jsx b/src/cockpit/389-console/src/lib/database/databaseConfig.jsx +index 4c7fce706..adb8227d7 100644 +--- a/src/cockpit/389-console/src/lib/database/databaseConfig.jsx ++++ b/src/cockpit/389-console/src/lib/database/databaseConfig.jsx +@@ -2,12 +2,16 @@ import cockpit from "cockpit"; + import React from "react"; + import { log_cmd } from "../tools.jsx"; + import { ++ Alert, + Button, + Checkbox, ++ Form, + Grid, + GridItem, ++ Hr, + NumberInput, + Spinner, ++ Switch, + Tab, + Tabs, + TabTitleText, +@@ -852,12 +856,29 @@ export class GlobalDatabaseConfig extends React.Component { + + {_("NDN Cache")}}> +
++ ++ {this.props.data.ndn_cache_enabled === false && ( ++ ++ ++ {_("The Normalized DN Cache is currently disabled. To enable it, go to Server Settings → Tuning & Limits and enable 'Normalized DN Cache', then restart the server for the changes to take effect.")} ++ ++ ++ )} ++ + + +- {_("Normalized DN Cache Max Size")} ++ {_("Normalized DN Cache Max Size") } + + + + + +@@ -1470,7 +1491,7 @@ export class GlobalDatabaseConfigMDB extends React.Component { + {_("Database Size")}}> +
+ + +@@ -1641,6 +1662,23 @@ export class GlobalDatabaseConfigMDB extends React.Component { + + {_("NDN Cache")}}> +
++ ++ {this.props.data.ndn_cache_enabled === false && ( ++ ++ ++ {_("The Normalized DN Cache is currently disabled. To enable it, go to Server Settings → Tuning & Limits and enable 'Normalized DN Cache', then restart the server for the changes to take effect.")} ++ ++ ++ )} ++ + 0; ++ let ndn_chart_data = this.state.ndnCacheList; ++ let ndn_util_chart_data = this.state.ndnCacheUtilList; ++ ++ // Only build NDN cache chart data if NDN cache is enabled ++ if (ndn_cache_enabled) { ++ const ndnratio = config.attrs.normalizeddncachehitratio[0]; ++ ndn_chart_data = this.state.ndnCacheList; ++ ndn_chart_data.shift(); ++ ndn_chart_data.push({ name: _("Cache Hit Ratio"), x: count.toString(), y: parseInt(ndnratio) }); ++ ++ // Build up the NDN Cache Util chart data ++ ndn_util_chart_data = this.state.ndnCacheUtilList; ++ const currNDNSize = parseInt(config.attrs.currentnormalizeddncachesize[0]); ++ const maxNDNSize = parseInt(config.attrs.maxnormalizeddncachesize[0]); ++ const ndn_utilization = (currNDNSize / maxNDNSize) * 100; ++ ndn_util_chart_data.shift(); ++ ndn_util_chart_data.push({ name: _("Cache Utilization"), x: ndnCount.toString(), y: parseInt(ndn_utilization) }); ++ } + + this.setState({ + data: config.attrs, +@@ -157,7 +167,8 @@ export class DatabaseMonitor extends React.Component { + ndnCacheList: ndn_chart_data, + ndnCacheUtilList: ndn_util_chart_data, + count, +- ndnCount ++ ndnCount, ++ ndn_cache_enabled + }); + }) + .fail(() => { +@@ -197,13 +208,20 @@ export class DatabaseMonitor extends React.Component { + + if (!this.state.loading) { + dbcachehit = parseInt(this.state.data.dbcachehitratio[0]); +- ndncachehit = parseInt(this.state.data.normalizeddncachehitratio[0]); +- ndncachemax = parseInt(this.state.data.maxnormalizeddncachesize[0]); +- ndncachecurr = parseInt(this.state.data.currentnormalizeddncachesize[0]); +- utilratio = Math.round((ndncachecurr / ndncachemax) * 100); +- if (utilratio === 0) { +- // Just round up to 1 +- utilratio = 1; ++ ++ // Check if NDN cache is enabled ++ const ndn_cache_enabled = this.state.data.normalizeddncachehitratio && ++ this.state.data.normalizeddncachehitratio.length > 0; ++ ++ if (ndn_cache_enabled) { ++ ndncachehit = parseInt(this.state.data.normalizeddncachehitratio[0]); ++ ndncachemax = parseInt(this.state.data.maxnormalizeddncachesize[0]); ++ ndncachecurr = parseInt(this.state.data.currentnormalizeddncachesize[0]); ++ utilratio = Math.round((ndncachecurr / ndncachemax) * 100); ++ if (utilratio === 0) { ++ // Just round up to 1 ++ utilratio = 1; ++ } + } + + // Database cache +@@ -214,119 +232,131 @@ export class DatabaseMonitor extends React.Component { + } else { + chartColor = ChartThemeColor.purple; + } +- // NDN cache ratio +- if (ndncachehit > 89) { +- ndnChartColor = ChartThemeColor.green; +- } else if (ndncachehit > 74) { +- ndnChartColor = ChartThemeColor.orange; +- } else { +- ndnChartColor = ChartThemeColor.purple; +- } +- // NDN cache utilization +- if (utilratio > 95) { +- ndnUtilColor = ChartThemeColor.purple; +- } else if (utilratio > 90) { +- ndnUtilColor = ChartThemeColor.orange; +- } else { +- ndnUtilColor = ChartThemeColor.green; ++ ++ // NDN cache colors only if enabled ++ if (ndn_cache_enabled) { ++ // NDN cache ratio ++ if (ndncachehit > 89) { ++ ndnChartColor = ChartThemeColor.green; ++ } else if (ndncachehit > 74) { ++ ndnChartColor = ChartThemeColor.orange; ++ } else { ++ ndnChartColor = ChartThemeColor.purple; ++ } ++ // NDN cache utilization ++ if (utilratio > 95) { ++ ndnUtilColor = ChartThemeColor.purple; ++ } else if (utilratio > 90) { ++ ndnUtilColor = ChartThemeColor.orange; ++ } else { ++ ndnUtilColor = ChartThemeColor.green; ++ } + } + +- content = ( +- +- {_("Database Cache")}}> +-
+- +- +-
+-
+- +- +- {_("Cache Hit Ratio")} +- +- +- +- +- {dbcachehit}% +- +- +-
+-
+- `${datum.name}: ${datum.y}`} constrainToVisibleArea />} +- height={200} +- maxDomain={{ y: 100 }} +- minDomain={{ y: 0 }} +- padding={{ +- bottom: 30, +- left: 40, +- top: 10, +- right: 10, +- }} +- width={500} +- themeColor={chartColor} +- > +- +- +- +- +- +- +-
++ // Create tabs based on what caches are available ++ const tabs = []; ++ ++ // Database Cache tab is always available ++ tabs.push( ++ {_("Database Cache")}}> ++
++ ++ ++
++
++ ++ ++ {_("Cache Hit Ratio")} ++ ++ ++ ++ ++ {dbcachehit}% ++ ++ +
+- +- +-
++
++ `${datum.name}: ${datum.y}`} constrainToVisibleArea />} ++ height={200} ++ maxDomain={{ y: 100 }} ++ minDomain={{ y: 0 }} ++ padding={{ ++ bottom: 30, ++ left: 40, ++ top: 10, ++ right: 10, ++ }} ++ width={500} ++ themeColor={chartColor} ++ > ++ ++ ++ ++ ++ ++ ++
++
++ ++ ++
++ ++ ++ ++ {_("Database Cache Hit Ratio:")} ++ ++ ++ {this.state.data.dbcachehitratio}% ++ ++ ++ {_("Database Cache Tries:")} ++ ++ ++ {numToCommas(this.state.data.dbcachetries)} ++ ++ ++ {_("Database Cache Hits:")} ++ ++ ++ {numToCommas(this.state.data.dbcachehits)} ++ ++ ++ {_("Cache Pages Read:")} ++ ++ ++ {numToCommas(this.state.data.dbcachepagein)} ++ ++ ++ {_("Cache Pages Written:")} ++ ++ ++ {numToCommas(this.state.data.dbcachepageout)} ++ ++ ++ {_("Read-Only Page Evictions:")} ++ ++ ++ {numToCommas(this.state.data.dbcacheroevict)} ++ ++ ++ {_("Read-Write Page Evictions:")} ++ ++ ++ {numToCommas(this.state.data.dbcacherwevict)} ++ ++ ++ ++ ); + +- +- +- {_("Database Cache Hit Ratio:")} +- +- +- {this.state.data.dbcachehitratio}% +- +- +- {_("Database Cache Tries:")} +- +- +- {numToCommas(this.state.data.dbcachetries)} +- +- +- {_("Database Cache Hits:")} +- +- +- {numToCommas(this.state.data.dbcachehits)} +- +- +- {_("Cache Pages Read:")} +- +- +- {numToCommas(this.state.data.dbcachepagein)} +- +- +- {_("Cache Pages Written:")} +- +- +- {numToCommas(this.state.data.dbcachepageout)} +- +- +- {_("Read-Only Page Evictions:")} +- +- +- {numToCommas(this.state.data.dbcacheroevict)} +- +- +- {_("Read-Write Page Evictions:")} +- +- +- {numToCommas(this.state.data.dbcacherwevict)} +- +- +- +- {_("Normalized DN Cache")}}> ++ // Only add NDN Cache tab if NDN cache is enabled ++ if (ndn_cache_enabled) { ++ tabs.push( ++ {_("Normalized DN Cache")}}> +
+ + +@@ -487,6 +517,12 @@ export class DatabaseMonitor extends React.Component { + +
+
++ ); ++ } ++ ++ content = ( ++ ++ {tabs} + + ); + } +@@ -533,7 +569,8 @@ export class DatabaseMonitorMDB extends React.Component { + ndnCount: 5, + dbCacheList: [], + ndnCacheList: [], +- ndnCacheUtilList: [] ++ ndnCacheUtilList: [], ++ ndn_cache_enabled: false + }; + + // Toggle currently active tab +@@ -585,6 +622,7 @@ export class DatabaseMonitorMDB extends React.Component { + { name: "", x: "4", y: 0 }, + { name: "", x: "5", y: 0 }, + ], ++ ndn_cache_enabled: false + }); + } + +@@ -605,19 +643,28 @@ export class DatabaseMonitorMDB extends React.Component { + count = 1; + } + +- // Build up the NDN Cache chart data +- const ndnratio = config.attrs.normalizeddncachehitratio[0]; +- const ndn_chart_data = this.state.ndnCacheList; +- ndn_chart_data.shift(); +- ndn_chart_data.push({ name: _("Cache Hit Ratio"), x: count.toString(), y: parseInt(ndnratio) }); +- +- // Build up the DB Cache Util chart data +- const ndn_util_chart_data = this.state.ndnCacheUtilList; +- const currNDNSize = parseInt(config.attrs.currentnormalizeddncachesize[0]); +- const maxNDNSize = parseInt(config.attrs.maxnormalizeddncachesize[0]); +- const ndn_utilization = (currNDNSize / maxNDNSize) * 100; +- ndn_util_chart_data.shift(); +- ndn_util_chart_data.push({ name: _("Cache Utilization"), x: ndnCount.toString(), y: parseInt(ndn_utilization) }); ++ // Check if NDN cache is enabled ++ const ndn_cache_enabled = config.attrs.normalizeddncachehitratio && ++ config.attrs.normalizeddncachehitratio.length > 0; ++ let ndn_chart_data = this.state.ndnCacheList; ++ let ndn_util_chart_data = this.state.ndnCacheUtilList; ++ ++ // Only build NDN cache chart data if NDN cache is enabled ++ if (ndn_cache_enabled) { ++ // Build up the NDN Cache chart data ++ const ndnratio = config.attrs.normalizeddncachehitratio[0]; ++ ndn_chart_data = this.state.ndnCacheList; ++ ndn_chart_data.shift(); ++ ndn_chart_data.push({ name: _("Cache Hit Ratio"), x: count.toString(), y: parseInt(ndnratio) }); ++ ++ // Build up the DB Cache Util chart data ++ ndn_util_chart_data = this.state.ndnCacheUtilList; ++ const currNDNSize = parseInt(config.attrs.currentnormalizeddncachesize[0]); ++ const maxNDNSize = parseInt(config.attrs.maxnormalizeddncachesize[0]); ++ const ndn_utilization = (currNDNSize / maxNDNSize) * 100; ++ ndn_util_chart_data.shift(); ++ ndn_util_chart_data.push({ name: _("Cache Utilization"), x: ndnCount.toString(), y: parseInt(ndn_utilization) }); ++ } + + this.setState({ + data: config.attrs, +@@ -625,7 +672,8 @@ export class DatabaseMonitorMDB extends React.Component { + ndnCacheList: ndn_chart_data, + ndnCacheUtilList: ndn_util_chart_data, + count, +- ndnCount ++ ndnCount, ++ ndn_cache_enabled + }); + }) + .fail(() => { +@@ -662,197 +710,214 @@ export class DatabaseMonitorMDB extends React.Component { + ); + + if (!this.state.loading) { +- ndncachehit = parseInt(this.state.data.normalizeddncachehitratio[0]); +- ndncachemax = parseInt(this.state.data.maxnormalizeddncachesize[0]); +- ndncachecurr = parseInt(this.state.data.currentnormalizeddncachesize[0]); +- utilratio = Math.round((ndncachecurr / ndncachemax) * 100); +- if (utilratio === 0) { +- // Just round up to 1 +- utilratio = 1; +- } +- +- // NDN cache ratio +- if (ndncachehit > 89) { +- ndnChartColor = ChartThemeColor.green; +- } else if (ndncachehit > 74) { +- ndnChartColor = ChartThemeColor.orange; +- } else { +- ndnChartColor = ChartThemeColor.purple; +- } +- // NDN cache utilization +- if (utilratio > 95) { +- ndnUtilColor = ChartThemeColor.purple; +- } else if (utilratio > 90) { +- ndnUtilColor = ChartThemeColor.orange; +- } else { +- ndnUtilColor = ChartThemeColor.green; +- } +- +- content = ( +- +- {_("Normalized DN Cache")}}> +-
+- +- +- +- +-
+-
+- +- +- {_("Cache Hit Ratio")} +- +- +- +- +- {ndncachehit}% +- +- +-
+-
+- `${datum.name}: ${datum.y}`} constrainToVisibleArea />} +- height={200} +- maxDomain={{ y: 100 }} +- minDomain={{ y: 0 }} +- padding={{ +- bottom: 40, +- left: 60, +- top: 10, +- right: 15, +- }} +- width={350} +- themeColor={ndnChartColor} +- > +- +- +- +- +- +- +-
+-
+-
+-
+-
+- +- +- +-
+-
+- +- +- {_("Cache Utilization")} +- +- +- +- +- {utilratio}% +- +- +- +- +- {_("Cached DN's")} +- +- +- {numToCommas(this.state.data.currentnormalizeddncachecount[0])} ++ // Check if NDN cache is enabled ++ const ndn_cache_enabled = this.state.data.normalizeddncachehitratio && ++ this.state.data.normalizeddncachehitratio.length > 0; ++ ++ if (ndn_cache_enabled) { ++ ndncachehit = parseInt(this.state.data.normalizeddncachehitratio[0]); ++ ndncachemax = parseInt(this.state.data.maxnormalizeddncachesize[0]); ++ ndncachecurr = parseInt(this.state.data.currentnormalizeddncachesize[0]); ++ utilratio = Math.round((ndncachecurr / ndncachemax) * 100); ++ if (utilratio === 0) { ++ // Just round up to 1 ++ utilratio = 1; ++ } ++ ++ // NDN cache ratio ++ if (ndncachehit > 89) { ++ ndnChartColor = ChartThemeColor.green; ++ } else if (ndncachehit > 74) { ++ ndnChartColor = ChartThemeColor.orange; ++ } else { ++ ndnChartColor = ChartThemeColor.purple; ++ } ++ // NDN cache utilization ++ if (utilratio > 95) { ++ ndnUtilColor = ChartThemeColor.purple; ++ } else if (utilratio > 90) { ++ ndnUtilColor = ChartThemeColor.orange; ++ } else { ++ ndnUtilColor = ChartThemeColor.green; ++ } ++ ++ content = ( ++ ++ {_("Normalized DN Cache")}}> ++
++ ++ ++ ++ ++
++
++ ++ ++ {_("Cache Hit Ratio")} ++ ++ ++ ++ ++ {ndncachehit}% ++ ++ ++
++
++ `${datum.name}: ${datum.y}`} constrainToVisibleArea />} ++ height={200} ++ maxDomain={{ y: 100 }} ++ minDomain={{ y: 0 }} ++ padding={{ ++ bottom: 40, ++ left: 60, ++ top: 10, ++ right: 15, ++ }} ++ width={350} ++ themeColor={ndnChartColor} ++ > ++ ++ ++ ++ ++ ++ ++
+
+-
+- `${datum.name}: ${datum.y}`} constrainToVisibleArea />} +- height={200} +- maxDomain={{ y: 100 }} +- minDomain={{ y: 0 }} +- padding={{ +- bottom: 40, +- left: 60, +- top: 10, +- right: 15, +- }} +- width={350} +- themeColor={ndnUtilColor} +- > +- +- +- +- +- +- ++ ++ ++ ++ ++ ++ ++
++
++ ++ ++ {_("Cache Utilization")} ++ ++ ++ ++ ++ {utilratio}% ++ ++ ++ ++ ++ {_("Cached DN's")} ++ ++ ++ {numToCommas(this.state.data.currentnormalizeddncachecount[0])} ++
++
++ `${datum.name}: ${datum.y}`} constrainToVisibleArea />} ++ height={200} ++ maxDomain={{ y: 100 }} ++ minDomain={{ y: 0 }} ++ padding={{ ++ bottom: 40, ++ left: 60, ++ top: 10, ++ right: 15, ++ }} ++ width={350} ++ themeColor={ndnUtilColor} ++ > ++ ++ ++ ++ ++ ++ ++
+
+-
+-
+-
+-
+-
+- +- +- +- {_("NDN Cache Hit Ratio:")} +- +- +- {this.state.data.normalizeddncachehitratio}% +- +- +- {_("NDN Cache Max Size:")} +- +- +- {displayBytes(this.state.data.maxnormalizeddncachesize)} +- +- +- {_("NDN Cache Tries:")} +- +- +- {numToCommas(this.state.data.normalizeddncachetries)} +- +- +- {_("NDN Current Cache Size:")} +- +- +- {displayBytes(this.state.data.currentnormalizeddncachesize)} +- +- +- {_("NDN Cache Hits:")} +- +- +- {numToCommas(this.state.data.normalizeddncachehits)} +- +- +- {_("NDN Cache DN Count:")} +- +- +- {numToCommas(this.state.data.currentnormalizeddncachecount)} +- +- +- {_("NDN Cache Evictions:")} +- +- +- {numToCommas(this.state.data.normalizeddncacheevictions)} +- +- +- {_("NDN Cache Thread Size:")} +- +- +- {numToCommas(this.state.data.normalizeddncachethreadsize)} +- +- +- {_("NDN Cache Thread Slots:")} +- +- +- {numToCommas(this.state.data.normalizeddncachethreadslots)} +- +- +-
+-
+-
+- ); ++ ++ ++ ++ ++ ++ ++ ++ {_("NDN Cache Hit Ratio:")} ++ ++ ++ {this.state.data.normalizeddncachehitratio}% ++ ++ ++ {_("NDN Cache Max Size:")} ++ ++ ++ {displayBytes(this.state.data.maxnormalizeddncachesize)} ++ ++ ++ {_("NDN Cache Tries:")} ++ ++ ++ {numToCommas(this.state.data.normalizeddncachetries)} ++ ++ ++ {_("NDN Current Cache Size:")} ++ ++ ++ {displayBytes(this.state.data.currentnormalizeddncachesize)} ++ ++ ++ {_("NDN Cache Hits:")} ++ ++ ++ {numToCommas(this.state.data.normalizeddncachehits)} ++ ++ ++ {_("NDN Cache DN Count:")} ++ ++ ++ {numToCommas(this.state.data.currentnormalizeddncachecount)} ++ ++ ++ {_("NDN Cache Evictions:")} ++ ++ ++ {numToCommas(this.state.data.normalizeddncacheevictions)} ++ ++ ++ {_("NDN Cache Thread Size:")} ++ ++ ++ {numToCommas(this.state.data.normalizeddncachethreadsize)} ++ ++ ++ {_("NDN Cache Thread Slots:")} ++ ++ ++ {numToCommas(this.state.data.normalizeddncachethreadslots)} ++ ++ ++
++ ++ ++ ); ++ } else { ++ // No NDN cache available ++ content = ( ++
++ ++ ++ {_("Normalized DN Cache is disabled")} ++ ++ ++
++ ); ++ } + } + + return ( +diff --git a/src/lib389/lib389/cli_conf/monitor.py b/src/lib389/lib389/cli_conf/monitor.py +index b01796549..c7f9322d1 100644 +--- a/src/lib389/lib389/cli_conf/monitor.py ++++ b/src/lib389/lib389/cli_conf/monitor.py +@@ -129,6 +129,14 @@ def db_monitor(inst, basedn, log, args): + # Gather the global DB stats + report_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ldbm_mon = ldbm_monitor.get_status() ++ ndn_cache_enabled = inst.config.get_attr_val_utf8('nsslapd-ndn-cache-enabled') == 'on' ++ ++ # Build global cache stats ++ result = { ++ 'date': report_time, ++ 'backends': {}, ++ } ++ + if ldbm_monitor.inst_db_impl == DB_IMPL_BDB: + dbcachesize = int(ldbm_mon['nsslapd-db-cache-size-bytes'][0]) + # Warning: there are two different page sizes associated with bdb: +@@ -153,32 +161,6 @@ def db_monitor(inst, basedn, log, args): + dbcachefree = max(int(dbcachesize - (pagesize * dbpages)), 0) + dbcachefreeratio = dbcachefree/dbcachesize + +- ndnratio = ldbm_mon['normalizeddncachehitratio'][0] +- ndncursize = int(ldbm_mon['currentnormalizeddncachesize'][0]) +- ndnmaxsize = int(ldbm_mon['maxnormalizeddncachesize'][0]) +- ndncount = ldbm_mon['currentnormalizeddncachecount'][0] +- ndnevictions = ldbm_mon['normalizeddncacheevictions'][0] +- if ndncursize > ndnmaxsize: +- ndnfree = 0 +- ndnfreeratio = 0 +- else: +- ndnfree = ndnmaxsize - ndncursize +- ndnfreeratio = "{:.1f}".format(ndnfree / ndnmaxsize * 100) +- +- # Build global cache stats +- result = { +- 'date': report_time, +- 'ndncache': { +- 'hit_ratio': ndnratio, +- 'free': convert_bytes(str(ndnfree)), +- 'free_percentage': ndnfreeratio, +- 'count': ndncount, +- 'evictions': ndnevictions +- }, +- 'backends': {}, +- } +- +- if ldbm_monitor.inst_db_impl == DB_IMPL_BDB: + result['dbcache'] = { + 'hit_ratio': dbhitratio, + 'free': convert_bytes(str(dbcachefree)), +@@ -188,6 +170,32 @@ def db_monitor(inst, basedn, log, args): + 'pageout': dbcachepageout + } + ++ # Add NDN cache stats only if enabled ++ if ndn_cache_enabled: ++ try: ++ ndnratio = ldbm_mon['normalizeddncachehitratio'][0] ++ ndncursize = int(ldbm_mon['currentnormalizeddncachesize'][0]) ++ ndnmaxsize = int(ldbm_mon['maxnormalizeddncachesize'][0]) ++ ndncount = ldbm_mon['currentnormalizeddncachecount'][0] ++ ndnevictions = ldbm_mon['normalizeddncacheevictions'][0] ++ if ndncursize > ndnmaxsize: ++ ndnfree = 0 ++ ndnfreeratio = 0 ++ else: ++ ndnfree = ndnmaxsize - ndncursize ++ ndnfreeratio = "{:.1f}".format(ndnfree / ndnmaxsize * 100) ++ ++ result['ndncache'] = { ++ 'hit_ratio': ndnratio, ++ 'free': convert_bytes(str(ndnfree)), ++ 'free_percentage': ndnfreeratio, ++ 'count': ndncount, ++ 'evictions': ndnevictions ++ } ++ # In case, the user enabled NDN cache but still have not restarted the instance ++ except IndexError: ++ ndn_cache_enabled = False ++ + # Build the backend results + for be in backend_objs: + be_name = be.rdn +@@ -277,13 +285,16 @@ def db_monitor(inst, basedn, log, args): + log.info(" - Pages In: {}".format(result['dbcache']['pagein'])) + log.info(" - Pages Out: {}".format(result['dbcache']['pageout'])) + log.info("") +- log.info("Normalized DN Cache:") +- log.info(" - Cache Hit Ratio: {}%".format(result['ndncache']['hit_ratio'])) +- log.info(" - Free Space: {}".format(result['ndncache']['free'])) +- log.info(" - Free Percentage: {}%".format(result['ndncache']['free_percentage'])) +- log.info(" - DN Count: {}".format(result['ndncache']['count'])) +- log.info(" - Evictions: {}".format(result['ndncache']['evictions'])) +- log.info("") ++ ++ if ndn_cache_enabled: ++ log.info("Normalized DN Cache:") ++ log.info(" - Cache Hit Ratio: {}%".format(result['ndncache']['hit_ratio'])) ++ log.info(" - Free Space: {}".format(result['ndncache']['free'])) ++ log.info(" - Free Percentage: {}%".format(result['ndncache']['free_percentage'])) ++ log.info(" - DN Count: {}".format(result['ndncache']['count'])) ++ log.info(" - Evictions: {}".format(result['ndncache']['evictions'])) ++ log.info("") ++ + log.info("Backends:") + for be_name, attr_dict in result['backends'].items(): + log.info(f" - {attr_dict['suffix']} ({be_name}):") +-- +2.49.0 + diff --git a/0010-Issue-6859-str2filter-is-not-fully-applying-matching.patch b/0010-Issue-6859-str2filter-is-not-fully-applying-matching.patch new file mode 100644 index 0000000..09cc802 --- /dev/null +++ b/0010-Issue-6859-str2filter-is-not-fully-applying-matching.patch @@ -0,0 +1,399 @@ +From 5198da59d622dbc39afe2ece9c6f40f4fb249d52 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Wed, 9 Jul 2025 14:18:50 -0400 +Subject: [PATCH] Issue 6859 - str2filter is not fully applying matching rules + +Description: + +When we have an extended filter, one with a MR applied, it is ignored during +internal searches: + + "(cn:CaseExactMatch:=Value)" + +For internal searches we use str2filter() and it doesn't fully apply extended +search filter matching rules + +Also needed to update attr uniqueness plugin to apply this change for mod +operations (previously only Adds were correctly handling these attribute +filters) + +Relates: https://github.com/389ds/389-ds-base/issues/6857 +Relates: https://github.com/389ds/389-ds-base/issues/6859 + +Reviewed by: spichugi & tbordaz(Thanks!!) +--- + .../tests/suites/plugins/attruniq_test.py | 295 +++++++++++++++++- + ldap/servers/plugins/uiduniq/uid.c | 7 + + ldap/servers/slapd/plugin_mr.c | 2 +- + ldap/servers/slapd/str2filter.c | 8 + + 4 files changed, 309 insertions(+), 3 deletions(-) + +diff --git a/dirsrvtests/tests/suites/plugins/attruniq_test.py b/dirsrvtests/tests/suites/plugins/attruniq_test.py +index b190e0ec1..b338f405f 100644 +--- a/dirsrvtests/tests/suites/plugins/attruniq_test.py ++++ b/dirsrvtests/tests/suites/plugins/attruniq_test.py +@@ -1,5 +1,5 @@ + # --- BEGIN COPYRIGHT BLOCK --- +-# Copyright (C) 2021 Red Hat, Inc. ++# Copyright (C) 2025 Red Hat, Inc. + # All rights reserved. + # + # License: GPL (version 3 or any later version). +@@ -80,4 +80,295 @@ def test_modrdn_attr_uniqueness(topology_st): + log.debug(excinfo.value) + + log.debug('Move user2 to group1') +- user2.rename(f'uid={user2.rdn}', group1.dn) +\ No newline at end of file ++ ++ user2.rename(f'uid={user2.rdn}', group1.dn) ++ ++ # Cleanup for next test ++ user1.delete() ++ user2.delete() ++ attruniq.disable() ++ attruniq.delete() ++ ++ ++def test_multiple_attr_uniqueness(topology_st): ++ """ Test that attribute uniqueness works properly with multiple attributes ++ ++ :id: c49aa5c1-7e65-45fd-b064-55e0b815e9bc ++ :setup: Standalone instance ++ :steps: ++ 1. Setup attribute uniqueness plugin to ensure uniqueness of attributes 'mail' and 'mailAlternateAddress' ++ 2. Add user with unique 'mail=non-uniq@value.net' and 'mailAlternateAddress=alt-mail@value.net' ++ 3. Try adding another user with 'mail=non-uniq@value.net' ++ 4. Try adding another user with 'mailAlternateAddress=alt-mail@value.net' ++ 5. Try adding another user with 'mail=alt-mail@value.net' ++ 6. Try adding another user with 'mailAlternateAddress=non-uniq@value.net' ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Should raise CONSTRAINT_VIOLATION ++ 4. Should raise CONSTRAINT_VIOLATION ++ 5. Should raise CONSTRAINT_VIOLATION ++ 6. Should raise CONSTRAINT_VIOLATION ++ """ ++ attruniq = AttributeUniquenessPlugin(topology_st.standalone, dn="cn=attruniq,cn=plugins,cn=config") ++ ++ try: ++ log.debug(f'Setup PLUGIN_ATTR_UNIQUENESS plugin for {MAIL_ATTR_VALUE} attribute for the group2') ++ attruniq.create(properties={'cn': 'attruniq'}) ++ attruniq.add_unique_attribute('mail') ++ attruniq.add_unique_attribute('mailAlternateAddress') ++ attruniq.add_unique_subtree(DEFAULT_SUFFIX) ++ attruniq.enable_all_subtrees() ++ log.debug(f'Enable PLUGIN_ATTR_UNIQUENESS plugin as "ON"') ++ attruniq.enable() ++ except ldap.LDAPError as e: ++ log.fatal('test_multiple_attribute_uniqueness: Failed to configure plugin for "mail": error {}'.format(e.args[0]['desc'])) ++ assert False ++ ++ topology_st.standalone.restart() ++ ++ users = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX) ++ ++ testuser1 = users.create_test_user(100,100) ++ testuser1.add('objectclass', 'extensibleObject') ++ testuser1.add('mail', MAIL_ATTR_VALUE) ++ testuser1.add('mailAlternateAddress', MAIL_ATTR_VALUE_ALT) ++ ++ testuser2 = users.create_test_user(200, 200) ++ testuser2.add('objectclass', 'extensibleObject') ++ ++ with pytest.raises(ldap.CONSTRAINT_VIOLATION): ++ testuser2.add('mail', MAIL_ATTR_VALUE) ++ ++ with pytest.raises(ldap.CONSTRAINT_VIOLATION): ++ testuser2.add('mailAlternateAddress', MAIL_ATTR_VALUE_ALT) ++ ++ with pytest.raises(ldap.CONSTRAINT_VIOLATION): ++ testuser2.add('mail', MAIL_ATTR_VALUE_ALT) ++ ++ with pytest.raises(ldap.CONSTRAINT_VIOLATION): ++ testuser2.add('mailAlternateAddress', MAIL_ATTR_VALUE) ++ ++ # Cleanup ++ testuser1.delete() ++ testuser2.delete() ++ attruniq.disable() ++ attruniq.delete() ++ ++ ++def test_exclude_subtrees(topology_st): ++ """ Test attribute uniqueness with exclude scope ++ ++ :id: 43d29a60-40e1-4ebd-b897-6ef9f20e9f27 ++ :setup: Standalone instance ++ :steps: ++ 1. Setup and enable attribute uniqueness plugin for telephonenumber unique attribute ++ 2. Create subtrees and test users ++ 3. Add a unique attribute to a user within uniqueness scope ++ 4. Add exclude subtree ++ 5. Try to add existing value attribute to an entry within uniqueness scope ++ 6. Try to add existing value attribute to an entry within exclude scope ++ 7. Remove the attribute from affected entries ++ 8. Add a unique attribute to a user within exclude scope ++ 9. Try to add existing value attribute to an entry within uniqueness scope ++ 10. Try to add existing value attribute to another entry within uniqueness scope ++ 11. Remove the attribute from affected entries ++ 12. Add another exclude subtree ++ 13. Add a unique attribute to a user within uniqueness scope ++ 14. Try to add existing value attribute to an entry within uniqueness scope ++ 15. Try to add existing value attribute to an entry within exclude scope ++ 16. Try to add existing value attribute to an entry within another exclude scope ++ 17. Clean up entries ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success ++ 5. Should raise CONSTRAINT_VIOLATION ++ 6. Success ++ 7. Success ++ 8. Success ++ 9. Success ++ 10. Should raise CONSTRAINT_VIOLATION ++ 11. Success ++ 12. Success ++ 13. Success ++ 14. Should raise CONSTRAINT_VIOLATION ++ 15. Success ++ 16. Success ++ 17. Success ++ """ ++ log.info('Setup attribute uniqueness plugin') ++ attruniq = AttributeUniquenessPlugin(topology_st.standalone, dn="cn=attruniq,cn=plugins,cn=config") ++ attruniq.create(properties={'cn': 'attruniq'}) ++ attruniq.add_unique_attribute('telephonenumber') ++ attruniq.add_unique_subtree(DEFAULT_SUFFIX) ++ attruniq.enable_all_subtrees() ++ attruniq.enable() ++ topology_st.standalone.restart() ++ ++ log.info('Create subtrees container') ++ containers = nsContainers(topology_st.standalone, DEFAULT_SUFFIX) ++ cont1 = containers.create(properties={'cn': EXCLUDED_CONTAINER_CN}) ++ cont2 = containers.create(properties={'cn': EXCLUDED_BIS_CONTAINER_CN}) ++ cont3 = containers.create(properties={'cn': ENFORCED_CONTAINER_CN}) ++ ++ log.info('Create test users') ++ users = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX, ++ rdn='cn={}'.format(ENFORCED_CONTAINER_CN)) ++ users_excluded = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX, ++ rdn='cn={}'.format(EXCLUDED_CONTAINER_CN)) ++ users_excluded2 = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX, ++ rdn='cn={}'.format(EXCLUDED_BIS_CONTAINER_CN)) ++ ++ user1 = users.create(properties={'cn': USER_1_CN, ++ 'uid': USER_1_CN, ++ 'sn': USER_1_CN, ++ 'uidNumber': '1', ++ 'gidNumber': '11', ++ 'homeDirectory': '/home/{}'.format(USER_1_CN)}) ++ user2 = users.create(properties={'cn': USER_2_CN, ++ 'uid': USER_2_CN, ++ 'sn': USER_2_CN, ++ 'uidNumber': '2', ++ 'gidNumber': '22', ++ 'homeDirectory': '/home/{}'.format(USER_2_CN)}) ++ user3 = users_excluded.create(properties={'cn': USER_3_CN, ++ 'uid': USER_3_CN, ++ 'sn': USER_3_CN, ++ 'uidNumber': '3', ++ 'gidNumber': '33', ++ 'homeDirectory': '/home/{}'.format(USER_3_CN)}) ++ user4 = users_excluded2.create(properties={'cn': USER_4_CN, ++ 'uid': USER_4_CN, ++ 'sn': USER_4_CN, ++ 'uidNumber': '4', ++ 'gidNumber': '44', ++ 'homeDirectory': '/home/{}'.format(USER_4_CN)}) ++ ++ UNIQUE_VALUE = '1234' ++ ++ try: ++ log.info('Create user with unique attribute') ++ user1.add('telephonenumber', UNIQUE_VALUE) ++ assert user1.present('telephonenumber', UNIQUE_VALUE) ++ ++ log.info('Add exclude subtree') ++ attruniq.add_exclude_subtree(EXCLUDED_CONTAINER_DN) ++ topology_st.standalone.restart() ++ ++ log.info('Verify an already used attribute value cannot be added within the same subtree') ++ with pytest.raises(ldap.CONSTRAINT_VIOLATION): ++ user2.add('telephonenumber', UNIQUE_VALUE) ++ ++ log.info('Verify an entry with same attribute value can be added within exclude subtree') ++ user3.add('telephonenumber', UNIQUE_VALUE) ++ assert user3.present('telephonenumber', UNIQUE_VALUE) ++ ++ log.info('Cleanup unique attribute values') ++ user1.remove_all('telephonenumber') ++ user3.remove_all('telephonenumber') ++ ++ log.info('Add a unique value to an entry in excluded scope') ++ user3.add('telephonenumber', UNIQUE_VALUE) ++ assert user3.present('telephonenumber', UNIQUE_VALUE) ++ ++ log.info('Verify the same value can be added to an entry within uniqueness scope') ++ user1.add('telephonenumber', UNIQUE_VALUE) ++ assert user1.present('telephonenumber', UNIQUE_VALUE) ++ ++ log.info('Verify that yet another same value cannot be added to another entry within uniqueness scope') ++ with pytest.raises(ldap.CONSTRAINT_VIOLATION): ++ user2.add('telephonenumber', UNIQUE_VALUE) ++ ++ log.info('Cleanup unique attribute values') ++ user1.remove_all('telephonenumber') ++ user3.remove_all('telephonenumber') ++ ++ log.info('Add another exclude subtree') ++ attruniq.add_exclude_subtree(EXCLUDED_BIS_CONTAINER_DN) ++ topology_st.standalone.restart() ++ ++ user1.add('telephonenumber', UNIQUE_VALUE) ++ log.info('Verify an already used attribute value cannot be added within the same subtree') ++ with pytest.raises(ldap.CONSTRAINT_VIOLATION): ++ user2.add('telephonenumber', UNIQUE_VALUE) ++ ++ log.info('Verify an already used attribute can be added to an entry in exclude scope') ++ user3.add('telephonenumber', UNIQUE_VALUE) ++ assert user3.present('telephonenumber', UNIQUE_VALUE) ++ user4.add('telephonenumber', UNIQUE_VALUE) ++ assert user4.present('telephonenumber', UNIQUE_VALUE) ++ ++ finally: ++ log.info('Clean up users, containers and attribute uniqueness plugin') ++ user1.delete() ++ user2.delete() ++ user3.delete() ++ user4.delete() ++ cont1.delete() ++ cont2.delete() ++ cont3.delete() ++ attruniq.disable() ++ attruniq.delete() ++ ++ ++def test_matchingrule_attr(topology_st): ++ """ Test list extension MR attribute. Check for "cn" using CES (versus it ++ being defined as CIS) ++ ++ :id: 5cde4342-6fa3-4225-b23d-0af918981075 ++ :setup: Standalone instance ++ :steps: ++ 1. Setup and enable attribute uniqueness plugin to use CN attribute ++ with a matching rule of CaseExactMatch. ++ 2. Add user with CN value is lowercase ++ 3. Add second user with same lowercase CN which should be rejected ++ 4. Add second user with same CN value but with mixed case ++ 5. Modify second user replacing CN value to lc which should be rejected ++ ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success ++ 5. Success ++ """ ++ ++ inst = topology_st.standalone ++ ++ attruniq = AttributeUniquenessPlugin(inst, ++ dn="cn=attribute uniqueness,cn=plugins,cn=config") ++ attruniq.add_unique_attribute('cn:CaseExactMatch:') ++ attruniq.enable_all_subtrees() ++ attruniq.enable() ++ inst.restart() ++ ++ users = UserAccounts(inst, DEFAULT_SUFFIX) ++ users.create(properties={'cn': "common_name", ++ 'uid': "uid_name", ++ 'sn': "uid_name", ++ 'uidNumber': '1', ++ 'gidNumber': '11', ++ 'homeDirectory': '/home/uid_name'}) ++ ++ log.info('Add entry with the exact CN value which should be rejected') ++ with pytest.raises(ldap.CONSTRAINT_VIOLATION): ++ users.create(properties={'cn': "common_name", ++ 'uid': "uid_name2", ++ 'sn': "uid_name2", ++ 'uidNumber': '11', ++ 'gidNumber': '111', ++ 'homeDirectory': '/home/uid_name2'}) ++ ++ log.info('Add entry with the mixed case CN value which should be allowed') ++ user = users.create(properties={'cn': "Common_Name", ++ 'uid': "uid_name2", ++ 'sn': "uid_name2", ++ 'uidNumber': '11', ++ 'gidNumber': '111', ++ 'homeDirectory': '/home/uid_name2'}) ++ ++ log.info('Mod entry with exact case CN value which should be rejected') ++ with pytest.raises(ldap.CONSTRAINT_VIOLATION): ++ user.replace('cn', 'common_name') +diff --git a/ldap/servers/plugins/uiduniq/uid.c b/ldap/servers/plugins/uiduniq/uid.c +index 887e79d78..fdb1404a0 100644 +--- a/ldap/servers/plugins/uiduniq/uid.c ++++ b/ldap/servers/plugins/uiduniq/uid.c +@@ -1178,6 +1178,10 @@ preop_modify(Slapi_PBlock *pb) + for (; mods && *mods; mods++) { + mod = *mods; + for (i = 0; attrNames && attrNames[i]; i++) { ++ char *attr_match = strchr(attrNames[i], ':'); ++ if (attr_match != NULL) { ++ attr_match[0] = '\0'; ++ } + if ((slapi_attr_type_cmp(mod->mod_type, attrNames[i], 1) == 0) && /* mod contains target attr */ + (mod->mod_op & LDAP_MOD_BVALUES) && /* mod is bval encoded (not string val) */ + (mod->mod_bvalues && mod->mod_bvalues[0]) && /* mod actually contains some values */ +@@ -1186,6 +1190,9 @@ preop_modify(Slapi_PBlock *pb) + { + addMod(&checkmods, &checkmodsCapacity, &modcount, mod); + } ++ if (attr_match != NULL) { ++ attr_match[0] = ':'; ++ } + } + } + if (modcount == 0) { +diff --git a/ldap/servers/slapd/plugin_mr.c b/ldap/servers/slapd/plugin_mr.c +index b262820c5..67051a5ff 100644 +--- a/ldap/servers/slapd/plugin_mr.c ++++ b/ldap/servers/slapd/plugin_mr.c +@@ -626,7 +626,7 @@ attempt_mr_filter_create(mr_filter_t *f, struct slapdplugin *mrp, Slapi_PBlock * + int rc; + IFP mrf_create = NULL; + f->mrf_match = NULL; +- pblock_init(pb); ++ slapi_pblock_init(pb); + if (!(rc = slapi_pblock_set(pb, SLAPI_PLUGIN, mrp)) && + !(rc = slapi_pblock_get(pb, SLAPI_PLUGIN_MR_FILTER_CREATE_FN, &mrf_create)) && + mrf_create != NULL && +diff --git a/ldap/servers/slapd/str2filter.c b/ldap/servers/slapd/str2filter.c +index 9fdc500f7..5620b7439 100644 +--- a/ldap/servers/slapd/str2filter.c ++++ b/ldap/servers/slapd/str2filter.c +@@ -344,6 +344,14 @@ str2simple(char *str, int unescape_filter) + return NULL; /* error */ + } else { + f->f_choice = LDAP_FILTER_EXTENDED; ++ if (f->f_mr_oid) { ++ /* apply the MR indexers */ ++ rc = plugin_mr_filter_create(&f->f_mr); ++ if (rc) { ++ slapi_filter_free(f, 1); ++ return NULL; /* error */ ++ } ++ } + } + } else if (str_find_star(value) == NULL) { + f->f_choice = LDAP_FILTER_EQUALITY; +-- +2.49.0 + diff --git a/0011-Issue-6872-compressed-log-rotation-creates-files-wit.patch b/0011-Issue-6872-compressed-log-rotation-creates-files-wit.patch new file mode 100644 index 0000000..cc717a3 --- /dev/null +++ b/0011-Issue-6872-compressed-log-rotation-creates-files-wit.patch @@ -0,0 +1,163 @@ +From 406563c136d78235751e34a3c7e22ccaf114f754 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Tue, 15 Jul 2025 17:56:18 -0400 +Subject: [PATCH] Issue 6872 - compressed log rotation creates files with world + readable permission + +Description: + +When compressing a log file, first create the empty file using open() +so we can set the correct permissions right from the start. gzopen() +always uses permission 644 and that is not safe. So after creating it +with open(), with the correct permissions, then pass the FD to gzdopen() +and write the compressed content. + +relates: https://github.com/389ds/389-ds-base/issues/6872 + +Reviewed by: progier(Thanks!) +--- + .../logging/logging_compression_test.py | 15 ++++++++-- + ldap/servers/slapd/log.c | 28 +++++++++++++------ + ldap/servers/slapd/schema.c | 2 +- + 3 files changed, 33 insertions(+), 12 deletions(-) + +diff --git a/dirsrvtests/tests/suites/logging/logging_compression_test.py b/dirsrvtests/tests/suites/logging/logging_compression_test.py +index e30874cc0..3a987d62c 100644 +--- a/dirsrvtests/tests/suites/logging/logging_compression_test.py ++++ b/dirsrvtests/tests/suites/logging/logging_compression_test.py +@@ -1,5 +1,5 @@ + # --- BEGIN COPYRIGHT BLOCK --- +-# Copyright (C) 2022 Red Hat, Inc. ++# Copyright (C) 2025 Red Hat, Inc. + # All rights reserved. + # + # License: GPL (version 3 or any later version). +@@ -22,12 +22,21 @@ log = logging.getLogger(__name__) + + pytestmark = pytest.mark.tier1 + ++ + def log_rotated_count(log_type, log_dir, check_compressed=False): +- # Check if the log was rotated ++ """ ++ Check if the log was rotated and has the correct permissions ++ """ + log_file = f'{log_dir}/{log_type}.2*' + if check_compressed: + log_file += ".gz" +- return len(glob.glob(log_file)) ++ log_files = glob.glob(log_file) ++ for logf in log_files: ++ # Check permissions ++ st = os.stat(logf) ++ assert oct(st.st_mode) == '0o100600' # 0600 ++ ++ return len(log_files) + + + def update_and_sleep(inst, suffix, sleep=True): +diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c +index a018ca2d5..178d29b89 100644 +--- a/ldap/servers/slapd/log.c ++++ b/ldap/servers/slapd/log.c +@@ -172,17 +172,28 @@ get_syslog_loglevel(int loglevel) + } + + static int +-compress_log_file(char *log_name) ++compress_log_file(char *log_name, int32_t mode) + { + char gzip_log[BUFSIZ] = {0}; + char buf[LOG_CHUNK] = {0}; + size_t bytes_read = 0; + gzFile outfile = NULL; + FILE *source = NULL; ++ int fd = 0; + + PR_snprintf(gzip_log, sizeof(gzip_log), "%s.gz", log_name); +- if ((outfile = gzopen(gzip_log,"wb")) == NULL) { +- /* Failed to open new gzip file */ ++ ++ /* ++ * Try to open the file as we may have an incorrect path. We also need to ++ * set the permissions using open() as gzopen() creates the file with ++ * 644 permissions (world readable - bad). So we create an empty file with ++ * the correct permissions, then we pass the FD to gzdopen() to write the ++ * compressed content. ++ */ ++ if ((fd = open(gzip_log, O_WRONLY|O_CREAT|O_TRUNC, mode)) >= 0) { ++ /* FIle successfully created, now pass the FD to gzdopen() */ ++ outfile = gzdopen(fd, "ab"); ++ } else { + return -1; + } + +@@ -191,6 +202,7 @@ compress_log_file(char *log_name) + gzclose(outfile); + return -1; + } ++ + bytes_read = fread(buf, 1, LOG_CHUNK, source); + while (bytes_read > 0) { + int bytes_written = gzwrite(outfile, buf, bytes_read); +@@ -3291,7 +3303,7 @@ log__open_accesslogfile(int logfile_state, int locked) + return LOG_UNABLE_TO_OPENFILE; + } + } else if (loginfo.log_access_compress) { +- if (compress_log_file(newfile) != 0) { ++ if (compress_log_file(newfile, loginfo.log_access_mode) != 0) { + slapi_log_err(SLAPI_LOG_ERR, "log__open_auditfaillogfile", + "failed to compress rotated access log (%s)\n", + newfile); +@@ -3455,7 +3467,7 @@ log__open_securitylogfile(int logfile_state, int locked) + return LOG_UNABLE_TO_OPENFILE; + } + } else if (loginfo.log_security_compress) { +- if (compress_log_file(newfile) != 0) { ++ if (compress_log_file(newfile, loginfo.log_security_mode) != 0) { + slapi_log_err(SLAPI_LOG_ERR, "log__open_securitylogfile", + "failed to compress rotated security audit log (%s)\n", + newfile); +@@ -6172,7 +6184,7 @@ log__open_errorlogfile(int logfile_state, int locked) + return LOG_UNABLE_TO_OPENFILE; + } + } else if (loginfo.log_error_compress) { +- if (compress_log_file(newfile) != 0) { ++ if (compress_log_file(newfile, loginfo.log_error_mode) != 0) { + PR_snprintf(buffer, sizeof(buffer), "Failed to compress errors log file (%s)\n", newfile); + log__error_emergency(buffer, 1, 1); + } else { +@@ -6355,7 +6367,7 @@ log__open_auditlogfile(int logfile_state, int locked) + return LOG_UNABLE_TO_OPENFILE; + } + } else if (loginfo.log_audit_compress) { +- if (compress_log_file(newfile) != 0) { ++ if (compress_log_file(newfile, loginfo.log_audit_mode) != 0) { + slapi_log_err(SLAPI_LOG_ERR, "log__open_auditfaillogfile", + "failed to compress rotated audit log (%s)\n", + newfile); +@@ -6514,7 +6526,7 @@ log__open_auditfaillogfile(int logfile_state, int locked) + return LOG_UNABLE_TO_OPENFILE; + } + } else if (loginfo.log_auditfail_compress) { +- if (compress_log_file(newfile) != 0) { ++ if (compress_log_file(newfile, loginfo.log_auditfail_mode) != 0) { + slapi_log_err(SLAPI_LOG_ERR, "log__open_auditfaillogfile", + "failed to compress rotated auditfail log (%s)\n", + newfile); +diff --git a/ldap/servers/slapd/schema.c b/ldap/servers/slapd/schema.c +index a8e6b1210..9ef4ee4bf 100644 +--- a/ldap/servers/slapd/schema.c ++++ b/ldap/servers/slapd/schema.c +@@ -903,7 +903,7 @@ oc_check_allowed_sv(Slapi_PBlock *pb, Slapi_Entry *e, const char *type, struct o + + if (pb) { + PR_snprintf(errtext, sizeof(errtext), +- "attribute \"%s\" not allowed\n", ++ "attribute \"%s\" not allowed", + escape_string(type, ebuf)); + slapi_pblock_set(pb, SLAPI_PB_RESULT_TEXT, errtext); + } +-- +2.49.0 + diff --git a/0012-Issue-6878-Prevent-repeated-disconnect-logs-during-s.patch b/0012-Issue-6878-Prevent-repeated-disconnect-logs-during-s.patch new file mode 100644 index 0000000..8dbfc90 --- /dev/null +++ b/0012-Issue-6878-Prevent-repeated-disconnect-logs-during-s.patch @@ -0,0 +1,116 @@ +From 9b8b23f6d46f16fbc1784b26cfc04dd6b4fa94e1 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Fri, 18 Jul 2025 18:50:33 -0700 +Subject: [PATCH] Issue 6878 - Prevent repeated disconnect logs during shutdown + (#6879) + +Description: Avoid logging non-active initialized connections via CONN in disconnect_server_nomutex_ext by adding a check to skip invalid conn=0 with invalid sockets, preventing excessive repeated messages. + +Update ds_logs_test.py by adding test_no_repeated_disconnect_messages to verify the fix. + +Fixes: https://github.com/389ds/389-ds-base/issues/6878 + +Reviewed by: @mreynolds389 (Thanks!) +--- + .../tests/suites/ds_logs/ds_logs_test.py | 51 ++++++++++++++++++- + ldap/servers/slapd/connection.c | 15 +++--- + 2 files changed, 59 insertions(+), 7 deletions(-) + +diff --git a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py +index 2c22347bb..b86c72687 100644 +--- a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py ++++ b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py +@@ -24,7 +24,7 @@ from lib389.plugins import AutoMembershipPlugin, ReferentialIntegrityPlugin, Aut + from lib389.idm.user import UserAccounts, UserAccount + from lib389.idm.group import Groups + from lib389.idm.organizationalunit import OrganizationalUnits +-from lib389._constants import DEFAULT_SUFFIX, LOG_ACCESS_LEVEL, PASSWORD ++from lib389._constants import DEFAULT_SUFFIX, LOG_ACCESS_LEVEL, PASSWORD, ErrorLog + from lib389.utils import ds_is_older, ds_is_newer + from lib389.config import RSA + from lib389.dseldif import DSEldif +@@ -1435,6 +1435,55 @@ def test_errorlog_buffering(topology_st, request): + assert inst.ds_error_log.match(".*slapd_daemon - slapd started.*") + + ++def test_no_repeated_disconnect_messages(topology_st): ++ """Test that there are no repeated "Not setting conn 0 to be disconnected: socket is invalid" messages on restart ++ ++ :id: 72b5e1ce-2db8-458f-b2cd-0a0b6525f51f ++ :setup: Standalone Instance ++ :steps: ++ 1. Set error log level to CONNECTION ++ 2. Clear existing error logs ++ 3. Restart the server with 30 second timeout ++ 4. Check error log for repeated disconnect messages ++ 5. Verify there are no more than 10 occurrences of the disconnect message ++ :expectedresults: ++ 1. Error log level should be set successfully ++ 2. Error logs should be cleared ++ 3. Server should restart successfully within 30 seconds ++ 4. Error log should be accessible ++ 5. There should be no more than 10 repeated disconnect messages ++ """ ++ ++ inst = topology_st.standalone ++ ++ log.info('Set error log level to CONNECTION') ++ inst.config.loglevel([ErrorLog.CONNECT]) ++ current_level = inst.config.get_attr_val_int('nsslapd-errorlog-level') ++ log.info(f'Error log level set to: {current_level}') ++ ++ log.info('Clear existing error logs') ++ inst.deleteErrorLogs() ++ ++ log.info('Restart the server with 30 second timeout') ++ inst.restart(timeout=30) ++ ++ log.info('Check error log for repeated disconnect messages') ++ disconnect_message = "Not setting conn 0 to be disconnected: socket is invalid" ++ ++ # Count occurrences of the disconnect message ++ error_log_lines = inst.ds_error_log.readlines() ++ disconnect_count = 0 ++ ++ for line in error_log_lines: ++ if disconnect_message in line: ++ disconnect_count += 1 ++ ++ log.info(f'Found {disconnect_count} occurrences of disconnect message') ++ ++ log.info('Verify there are no more than 10 occurrences') ++ assert disconnect_count <= 10, f"Found {disconnect_count} repeated disconnect messages, expected <= 10" ++ ++ + if __name__ == '__main__': + # Run isolated + # -s for DEBUG mode +diff --git a/ldap/servers/slapd/connection.c b/ldap/servers/slapd/connection.c +index bb4fcd77f..2967de15b 100644 +--- a/ldap/servers/slapd/connection.c ++++ b/ldap/servers/slapd/connection.c +@@ -2465,12 +2465,15 @@ disconnect_server_nomutex_ext(Connection *conn, PRUint64 opconnid, int opid, PRE + } + + } else { +- slapi_log_err(SLAPI_LOG_CONNS, "disconnect_server_nomutex_ext", +- "Not setting conn %d to be disconnected: %s\n", +- conn->c_sd, +- (conn->c_sd == SLAPD_INVALID_SOCKET) ? "socket is invalid" : +- ((conn->c_connid != opconnid) ? "conn id does not match op conn id" : +- ((conn->c_flags & CONN_FLAG_CLOSING) ? "conn is closing" : "unknown"))); ++ /* We avoid logging an invalid conn=0 connection as it is not a real connection. */ ++ if (!(conn->c_sd == SLAPD_INVALID_SOCKET && conn->c_connid == 0)) { ++ slapi_log_err(SLAPI_LOG_CONNS, "disconnect_server_nomutex_ext", ++ "Not setting conn %d to be disconnected: %s\n", ++ conn->c_sd, ++ (conn->c_sd == SLAPD_INVALID_SOCKET) ? "socket is invalid" : ++ ((conn->c_connid != opconnid) ? "conn id does not match op conn id" : ++ ((conn->c_flags & CONN_FLAG_CLOSING) ? "conn is closing" : "unknown"))); ++ } + } + } + +-- +2.49.0 + diff --git a/0013-Issue-6772-dsconf-Replicas-with-the-consumer-role-al.patch b/0013-Issue-6772-dsconf-Replicas-with-the-consumer-role-al.patch new file mode 100644 index 0000000..64f23ea --- /dev/null +++ b/0013-Issue-6772-dsconf-Replicas-with-the-consumer-role-al.patch @@ -0,0 +1,67 @@ +From fef4875a9c3d67ef424a1fb1698ae011152735b1 Mon Sep 17 00:00:00 2001 +From: Anuar Beisembayev <111912342+abeisemb@users.noreply.github.com> +Date: Wed, 23 Jul 2025 23:48:11 -0400 +Subject: [PATCH] Issue 6772 - dsconf - Replicas with the "consumer" role allow + for viewing and modification of their changelog. (#6773) + +dsconf currently allows users to set and retrieve changelogs in consumer replicas, which do not have officially supported changelogs. This can lead to undefined behavior and confusion. +This commit prints a warning message if the user tries to interact with a changelog on a consumer replica. + +Resolves: https://github.com/389ds/389-ds-base/issues/6772 + +Reviewed by: @droideck +--- + src/lib389/lib389/cli_conf/replication.py | 23 +++++++++++++++++++++++ + 1 file changed, 23 insertions(+) + +diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py +index 6f77f34ca..a18bf83ca 100644 +--- a/src/lib389/lib389/cli_conf/replication.py ++++ b/src/lib389/lib389/cli_conf/replication.py +@@ -686,6 +686,9 @@ def set_per_backend_cl(inst, basedn, log, args): + replace_list = [] + did_something = False + ++ if (is_replica_role_consumer(inst, suffix)): ++ log.info("Warning: Changelogs are not supported for consumer replicas. You may run into undefined behavior.") ++ + if args.encrypt: + cl.replace('nsslapd-encryptionalgorithm', 'AES') + del args.encrypt +@@ -715,6 +718,10 @@ def set_per_backend_cl(inst, basedn, log, args): + # that means there is a changelog config entry per backend (aka suffix) + def get_per_backend_cl(inst, basedn, log, args): + suffix = args.suffix ++ ++ if (is_replica_role_consumer(inst, suffix)): ++ log.info("Warning: Changelogs are not supported for consumer replicas. You may run into undefined behavior.") ++ + cl = Changelog(inst, suffix) + if args and args.json: + log.info(cl.get_all_attrs_json()) +@@ -822,6 +829,22 @@ def del_repl_manager(inst, basedn, log, args): + + log.info("Successfully deleted replication manager: " + manager_dn) + ++def is_replica_role_consumer(inst, suffix): ++ """Helper function for get_per_backend_cl and set_per_backend_cl. ++ Makes sure the instance in question is not a consumer, which is a role that ++ does not support changelogs. ++ """ ++ replicas = Replicas(inst) ++ try: ++ replica = replicas.get(suffix) ++ role = replica.get_role() ++ except ldap.NO_SUCH_OBJECT: ++ raise ValueError(f"Backend \"{suffix}\" is not enabled for replication") ++ ++ if role == ReplicaRole.CONSUMER: ++ return True ++ else: ++ return False + + # + # Agreements +-- +2.49.0 + diff --git a/0014-Issue-6893-Log-user-that-is-updated-during-password-.patch b/0014-Issue-6893-Log-user-that-is-updated-during-password-.patch new file mode 100644 index 0000000..d75bc8d --- /dev/null +++ b/0014-Issue-6893-Log-user-that-is-updated-during-password-.patch @@ -0,0 +1,143 @@ +From 4cb50f83397e6a5e14a9b75ed15f24189ee2792b Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Mon, 21 Jul 2025 18:07:21 -0400 +Subject: [PATCH] Issue 6893 - Log user that is updated during password modify + extended operation + +Description: + +When a user's password is updated via an extended operation (password modify +plugin) we only log the bind DN and not what user was updated. While "internal +operation" logging will display the the user it should be logged by the default +logging level. + +Add access logging using "EXT_INFO" where we display the bind dn, target +dn, and message. + +Relates: https://github.com/389ds/389-ds-base/issues/6893 + +Reviewed by: spichugi & tbordaz(Thanks!!) +--- + ldap/servers/slapd/passwd_extop.c | 56 +++++++++++++++---------------- + 1 file changed, 28 insertions(+), 28 deletions(-) + +diff --git a/ldap/servers/slapd/passwd_extop.c b/ldap/servers/slapd/passwd_extop.c +index 4bb60afd6..0296d64fb 100644 +--- a/ldap/servers/slapd/passwd_extop.c ++++ b/ldap/servers/slapd/passwd_extop.c +@@ -465,12 +465,13 @@ passwd_modify_extop(Slapi_PBlock *pb) + BerElement *response_ber = NULL; + Slapi_Entry *targetEntry = NULL; + Connection *conn = NULL; ++ Operation *pb_op = NULL; + LDAPControl **req_controls = NULL; + LDAPControl **resp_controls = NULL; + passwdPolicy *pwpolicy = NULL; + Slapi_DN *target_sdn = NULL; + Slapi_Entry *referrals = NULL; +- /* Slapi_DN sdn; */ ++ Slapi_Backend *be = NULL; + + slapi_log_err(SLAPI_LOG_TRACE, "passwd_modify_extop", "=>\n"); + +@@ -647,7 +648,7 @@ parse_req_done: + } + dn = slapi_sdn_get_ndn(target_sdn); + if (dn == NULL || *dn == '\0') { +- /* Refuse the operation because they're bound anonymously */ ++ /* Invalid DN - refuse the operation */ + errMesg = "Invalid dn."; + rc = LDAP_INVALID_DN_SYNTAX; + goto free_and_return; +@@ -724,14 +725,19 @@ parse_req_done: + ber_free(response_ber, 1); + } + +- slapi_pblock_set(pb, SLAPI_ORIGINAL_TARGET, (void *)dn); ++ slapi_pblock_get(pb, SLAPI_OPERATION, &pb_op); ++ if (pb_op == NULL) { ++ slapi_log_err(SLAPI_LOG_ERR, "passwd_modify_extop", "pb_op is NULL\n"); ++ goto free_and_return; ++ } + ++ slapi_pblock_set(pb, SLAPI_ORIGINAL_TARGET, (void *)dn); + /* Now we have the DN, look for the entry */ + ret = passwd_modify_getEntry(dn, &targetEntry); + /* If we can't find the entry, then that's an error */ + if (ret) { + /* Couldn't find the entry, fail */ +- errMesg = "No such Entry exists."; ++ errMesg = "No such entry exists."; + rc = LDAP_NO_SUCH_OBJECT; + goto free_and_return; + } +@@ -742,30 +748,18 @@ parse_req_done: + leak any useful information to the client such as current password + wrong, etc. + */ +- Operation *pb_op = NULL; +- slapi_pblock_get(pb, SLAPI_OPERATION, &pb_op); +- if (pb_op == NULL) { +- slapi_log_err(SLAPI_LOG_ERR, "passwd_modify_extop", "pb_op is NULL\n"); +- goto free_and_return; +- } +- + operation_set_target_spec(pb_op, slapi_entry_get_sdn(targetEntry)); + slapi_pblock_set(pb, SLAPI_REQUESTOR_ISROOT, &pb_op->o_isroot); + +- /* In order to perform the access control check , we need to select a backend (even though +- * we don't actually need it otherwise). +- */ +- { +- Slapi_Backend *be = NULL; +- +- be = slapi_mapping_tree_find_backend_for_sdn(slapi_entry_get_sdn(targetEntry)); +- if (NULL == be) { +- errMesg = "Failed to find backend for target entry"; +- rc = LDAP_OPERATIONS_ERROR; +- goto free_and_return; +- } +- slapi_pblock_set(pb, SLAPI_BACKEND, be); ++ /* In order to perform the access control check, we need to select a backend (even though ++ * we don't actually need it otherwise). */ ++ be = slapi_mapping_tree_find_backend_for_sdn(slapi_entry_get_sdn(targetEntry)); ++ if (NULL == be) { ++ errMesg = "Failed to find backend for target entry"; ++ rc = LDAP_NO_SUCH_OBJECT; ++ goto free_and_return; + } ++ slapi_pblock_set(pb, SLAPI_BACKEND, be); + + /* Check if the pwpolicy control is present */ + slapi_pblock_get(pb, SLAPI_PWPOLICY, &need_pwpolicy_ctrl); +@@ -797,10 +791,7 @@ parse_req_done: + /* Check if password policy allows users to change their passwords. We need to do + * this here since the normal modify code doesn't perform this check for + * internal operations. */ +- +- Connection *pb_conn; +- slapi_pblock_get(pb, SLAPI_CONNECTION, &pb_conn); +- if (!pb_op->o_isroot && !pb_conn->c_needpw && !pwpolicy->pw_change) { ++ if (!pb_op->o_isroot && !conn->c_needpw && !pwpolicy->pw_change) { + if (NULL == bindSDN) { + bindSDN = slapi_sdn_new_normdn_byref(bindDN); + } +@@ -848,6 +839,15 @@ free_and_return: + slapi_log_err(SLAPI_LOG_PLUGIN, "passwd_modify_extop", + "%s\n", errMesg ? errMesg : "success"); + ++ if (dn) { ++ /* Log the target ndn (if we have a target ndn) */ ++ slapi_log_access(LDAP_DEBUG_STATS, ++ "conn=%" PRIu64 " op=%d EXT_INFO name=\"passwd_modify_plugin\" bind_dn=\"%s\" target_dn=\"%s\" msg=\"%s\" rc=%d\n", ++ conn ? conn->c_connid : -1, pb_op ? pb_op->o_opid : -1, ++ bindDN ? bindDN : "", dn, ++ errMesg ? errMesg : "success", rc); ++ } ++ + if ((rc == LDAP_REFERRAL) && (referrals)) { + send_referrals_from_entry(pb, referrals); + } else { +-- +2.49.0 + diff --git a/0015-Issue-6895-Crash-if-repl-keep-alive-entry-can-not-be.patch b/0015-Issue-6895-Crash-if-repl-keep-alive-entry-can-not-be.patch new file mode 100644 index 0000000..914d27b --- /dev/null +++ b/0015-Issue-6895-Crash-if-repl-keep-alive-entry-can-not-be.patch @@ -0,0 +1,98 @@ +From ffc3a81ed5852b7f1fbaed79b9b776af23d65b7c Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Wed, 23 Jul 2025 19:35:32 -0400 +Subject: [PATCH] Issue 6895 - Crash if repl keep alive entry can not be + created + +Description: + +Heap use after free when logging that the replicaton keep-alive entry can not +be created. slapi_add_internal_pb() frees the slapi entry, then +we try and get the dn from the entry and we get a use-after-free crash. + +Relates: https://github.com/389ds/389-ds-base/issues/6895 + +Reviewed by: spichugi(Thanks!) +--- + ldap/servers/plugins/chainingdb/cb_config.c | 3 +-- + ldap/servers/plugins/posix-winsync/posix-winsync.c | 1 - + ldap/servers/plugins/replication/repl5_init.c | 3 --- + ldap/servers/plugins/replication/repl5_replica.c | 8 ++++---- + 4 files changed, 5 insertions(+), 10 deletions(-) + +diff --git a/ldap/servers/plugins/chainingdb/cb_config.c b/ldap/servers/plugins/chainingdb/cb_config.c +index 40a7088d7..24fa1bcb3 100644 +--- a/ldap/servers/plugins/chainingdb/cb_config.c ++++ b/ldap/servers/plugins/chainingdb/cb_config.c +@@ -44,8 +44,7 @@ cb_config_add_dse_entries(cb_backend *cb, char **entries, char *string1, char *s + slapi_pblock_get(util_pb, SLAPI_PLUGIN_INTOP_RESULT, &res); + if (LDAP_SUCCESS != res && LDAP_ALREADY_EXISTS != res) { + slapi_log_err(SLAPI_LOG_ERR, CB_PLUGIN_SUBSYSTEM, +- "cb_config_add_dse_entries - Unable to add config entry (%s) to the DSE: %s\n", +- slapi_entry_get_dn(e), ++ "cb_config_add_dse_entries - Unable to add config entry to the DSE: %s\n", + ldap_err2string(res)); + rc = res; + slapi_pblock_destroy(util_pb); +diff --git a/ldap/servers/plugins/posix-winsync/posix-winsync.c b/ldap/servers/plugins/posix-winsync/posix-winsync.c +index 51a55b643..3a002bb70 100644 +--- a/ldap/servers/plugins/posix-winsync/posix-winsync.c ++++ b/ldap/servers/plugins/posix-winsync/posix-winsync.c +@@ -1626,7 +1626,6 @@ posix_winsync_end_update_cb(void *cbdata __attribute__((unused)), + "posix_winsync_end_update_cb: " + "add task entry\n"); + } +- /* slapi_entry_free(e_task); */ + slapi_pblock_destroy(pb); + pb = NULL; + posix_winsync_config_reset_MOFTaskCreated(); +diff --git a/ldap/servers/plugins/replication/repl5_init.c b/ldap/servers/plugins/replication/repl5_init.c +index 8bc0b5372..5047fb8dc 100644 +--- a/ldap/servers/plugins/replication/repl5_init.c ++++ b/ldap/servers/plugins/replication/repl5_init.c +@@ -682,7 +682,6 @@ create_repl_schema_policy(void) + repl_schema_top, + ldap_err2string(return_value)); + rc = -1; +- slapi_entry_free(e); /* The entry was not consumed */ + goto done; + } + slapi_pblock_destroy(pb); +@@ -703,7 +702,6 @@ create_repl_schema_policy(void) + repl_schema_supplier, + ldap_err2string(return_value)); + rc = -1; +- slapi_entry_free(e); /* The entry was not consumed */ + goto done; + } + slapi_pblock_destroy(pb); +@@ -724,7 +722,6 @@ create_repl_schema_policy(void) + repl_schema_consumer, + ldap_err2string(return_value)); + rc = -1; +- slapi_entry_free(e); /* The entry was not consumed */ + goto done; + } + slapi_pblock_destroy(pb); +diff --git a/ldap/servers/plugins/replication/repl5_replica.c b/ldap/servers/plugins/replication/repl5_replica.c +index 59062b46b..a97c807e9 100644 +--- a/ldap/servers/plugins/replication/repl5_replica.c ++++ b/ldap/servers/plugins/replication/repl5_replica.c +@@ -465,10 +465,10 @@ replica_subentry_create(const char *repl_root, ReplicaId rid) + if (return_value != LDAP_SUCCESS && + return_value != LDAP_ALREADY_EXISTS && + return_value != LDAP_REFERRAL /* CONSUMER */) { +- slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "replica_subentry_create - Unable to " +- "create replication keep alive entry %s: error %d - %s\n", +- slapi_entry_get_dn_const(e), +- return_value, ldap_err2string(return_value)); ++ slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "replica_subentry_create - " ++ "Unable to create replication keep alive entry 'cn=%s %d,%s': error %d - %s\n", ++ KEEP_ALIVE_ENTRY, rid, repl_root, ++ return_value, ldap_err2string(return_value)); + rc = -1; + goto done; + } +-- +2.49.0 + diff --git a/0016-Issue-6250-Add-test-for-entryUSN-overflow-on-failed-.patch b/0016-Issue-6250-Add-test-for-entryUSN-overflow-on-failed-.patch new file mode 100644 index 0000000..0e3949e --- /dev/null +++ b/0016-Issue-6250-Add-test-for-entryUSN-overflow-on-failed-.patch @@ -0,0 +1,352 @@ +From 191634746fdcb7e26a154cd00a22324e02a10110 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Mon, 28 Jul 2025 10:50:26 -0700 +Subject: [PATCH] Issue 6250 - Add test for entryUSN overflow on failed add + operations (#6821) + +Description: Add comprehensive test to reproduce the entryUSN +overflow issue where failed attempts to add existing entries followed by +modify operations cause entryUSN values to underflow/overflow instead of +incrementing properly. + +Related: https://github.com/389ds/389-ds-base/issues/6250 + +Reviewed by: @tbordaz (Thanks!) +--- + .../suites/plugins/entryusn_overflow_test.py | 323 ++++++++++++++++++ + 1 file changed, 323 insertions(+) + create mode 100644 dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py + +diff --git a/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py b/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py +new file mode 100644 +index 000000000..a23d734ca +--- /dev/null ++++ b/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py +@@ -0,0 +1,323 @@ ++# --- BEGIN COPYRIGHT BLOCK --- ++# Copyright (C) 2025 Red Hat, Inc. ++# All rights reserved. ++# ++# License: GPL (version 3 or any later version). ++# See LICENSE for details. ++# --- END COPYRIGHT BLOCK --- ++# ++import os ++import ldap ++import logging ++import pytest ++import time ++import random ++from lib389._constants import DEFAULT_SUFFIX ++from lib389.config import Config ++from lib389.plugins import USNPlugin ++from lib389.idm.user import UserAccounts ++from lib389.topologies import topology_st ++from lib389.rootdse import RootDSE ++ ++pytestmark = pytest.mark.tier2 ++ ++log = logging.getLogger(__name__) ++ ++# Test constants ++DEMO_USER_BASE_DN = "uid=demo_user,ou=people," + DEFAULT_SUFFIX ++TEST_USER_PREFIX = "Demo User" ++MAX_USN_64BIT = 18446744073709551615 # 2^64 - 1 ++ITERATIONS = 10 ++ADD_EXISTING_ENTRY_MAX_ATTEMPTS = 5 ++ ++ ++@pytest.fixture(scope="module") ++def setup_usn_test(topology_st, request): ++ """Setup USN plugin and test data for entryUSN overflow testing""" ++ ++ inst = topology_st.standalone ++ ++ log.info("Enable the USN plugin...") ++ plugin = USNPlugin(inst) ++ plugin.enable() ++ plugin.enable_global_mode() ++ ++ inst.restart() ++ ++ # Create initial test users ++ users = UserAccounts(inst, DEFAULT_SUFFIX) ++ created_users = [] ++ ++ log.info("Creating initial test users...") ++ for i in range(3): ++ user_props = { ++ 'uid': f'{TEST_USER_PREFIX}-{i}', ++ 'cn': f'{TEST_USER_PREFIX}-{i}', ++ 'sn': f'User{i}', ++ 'uidNumber': str(1000 + i), ++ 'gidNumber': str(1000 + i), ++ 'homeDirectory': f'/home/{TEST_USER_PREFIX}-{i}', ++ 'userPassword': 'password123' ++ } ++ try: ++ user = users.create(properties=user_props) ++ created_users.append(user) ++ log.info(f"Created user: {user.dn}") ++ except ldap.ALREADY_EXISTS: ++ log.info(f"User {user_props['uid']} already exists, skipping creation") ++ user = users.get(user_props['uid']) ++ created_users.append(user) ++ ++ def fin(): ++ log.info("Cleaning up test users...") ++ for user in created_users: ++ try: ++ user.delete() ++ except ldap.NO_SUCH_OBJECT: ++ pass ++ ++ request.addfinalizer(fin) ++ ++ return created_users ++ ++ ++def test_entryusn_overflow_on_add_existing_entries(topology_st, setup_usn_test): ++ """Test that reproduces entryUSN overflow when adding existing entries ++ ++ :id: a5a8c33d-82f3-4113-be2b-027de51791c8 ++ :setup: Standalone instance with USN plugin enabled and test users ++ :steps: ++ 1. Record initial entryUSN values for existing users ++ 2. Attempt to add existing entries multiple times (should fail) ++ 3. Perform modify operations on the entries ++ 4. Check that entryUSN values increment correctly without overflow ++ 5. Verify lastusn values are consistent ++ :expectedresults: ++ 1. Initial entryUSN values are recorded successfully ++ 2. Add operations fail with ALREADY_EXISTS error ++ 3. Modify operations succeed ++ 4. EntryUSN values increment properly without underflow/overflow ++ 5. LastUSN values are consistent and increasing ++ """ ++ ++ inst = topology_st.standalone ++ users = setup_usn_test ++ ++ # Enable detailed logging for debugging ++ config = Config(inst) ++ config.replace('nsslapd-accesslog-level', '260') # Internal op logging ++ config.replace('nsslapd-errorlog-level', '65536') ++ config.replace('nsslapd-plugin-logging', 'on') ++ ++ root_dse = RootDSE(inst) ++ ++ log.info("Starting entryUSN overflow reproduction test") ++ ++ # Record initial state ++ initial_usn_values = {} ++ for user in users: ++ initial_usn = user.get_attr_val_int('entryusn') ++ initial_usn_values[user.dn] = initial_usn ++ log.info(f"Initial entryUSN for {user.get_attr_val_utf8('cn')}: {initial_usn}") ++ ++ initial_lastusn = root_dse.get_attr_val_int("lastusn") ++ log.info(f"Initial lastUSN: {initial_lastusn}") ++ ++ # Perform test iterations ++ for iteration in range(1, ITERATIONS + 1): ++ log.info(f"\n--- Iteration {iteration} ---") ++ ++ # Step 1: Try to add existing entries multiple times ++ selected_user = random.choice(users) ++ cn_value = selected_user.get_attr_val_utf8('cn') ++ attempts = random.randint(1, ADD_EXISTING_ENTRY_MAX_ATTEMPTS) ++ ++ log.info(f"Attempting to add existing entry '{cn_value}' {attempts} times") ++ ++ # Get user attributes for recreation attempt ++ user_attrs = { ++ 'uid': selected_user.get_attr_val_utf8('uid'), ++ 'cn': selected_user.get_attr_val_utf8('cn'), ++ 'sn': selected_user.get_attr_val_utf8('sn'), ++ 'uidNumber': selected_user.get_attr_val_utf8('uidNumber'), ++ 'gidNumber': selected_user.get_attr_val_utf8('gidNumber'), ++ 'homeDirectory': selected_user.get_attr_val_utf8('homeDirectory'), ++ 'userPassword': 'password123' ++ } ++ ++ users_collection = UserAccounts(inst, DEFAULT_SUFFIX) ++ ++ # Try to add the existing user multiple times ++ for attempt in range(attempts): ++ try: ++ users_collection.create(properties=user_attrs) ++ log.error(f"ERROR: Add operation should have failed but succeeded on attempt {attempt + 1}") ++ assert False, "Add operation should have failed with ALREADY_EXISTS" ++ except ldap.ALREADY_EXISTS: ++ log.info(f"Attempt {attempt + 1}: Got expected ALREADY_EXISTS error") ++ except Exception as e: ++ log.error(f"Unexpected error on attempt {attempt + 1}: {e}") ++ raise ++ ++ # Step 2: Perform modify operation ++ target_user = random.choice(users) ++ cn_value = target_user.get_attr_val_utf8('cn') ++ old_usn = target_user.get_attr_val_int('entryusn') ++ ++ # Modify the user entry ++ new_description = f"Modified in iteration {iteration} - {time.time()}" ++ target_user.replace('description', new_description) ++ ++ # Get new USN value ++ new_usn = target_user.get_attr_val_int('entryusn') ++ ++ log.info(f"Modified entry '{cn_value}': old USN = {old_usn}, new USN = {new_usn}") ++ ++ # Step 3: Validate USN values ++ # Check for overflow/underflow conditions ++ assert new_usn > 0, f"EntryUSN should be positive, got {new_usn}" ++ assert new_usn < MAX_USN_64BIT, f"EntryUSN overflow detected: {new_usn} >= {MAX_USN_64BIT}" ++ ++ # Check that USN didn't wrap around (underflow detection) ++ usn_diff = new_usn - old_usn ++ assert usn_diff < 1000, f"USN increment too large, possible overflow: {usn_diff}" ++ ++ # Verify lastUSN is also reasonable ++ current_lastusn = root_dse.get_attr_val_int("lastusn") ++ assert current_lastusn >= new_usn, f"LastUSN ({current_lastusn}) should be >= entryUSN ({new_usn})" ++ assert current_lastusn < MAX_USN_64BIT, f"LastUSN overflow detected: {current_lastusn}" ++ ++ log.info(f"USN validation passed for iteration {iteration}") ++ ++ # Add a new entry occasionally to increase USN diversity ++ if iteration % 3 == 0: ++ new_user_props = { ++ 'uid': f'{TEST_USER_PREFIX}-new-{iteration}', ++ 'cn': f'{TEST_USER_PREFIX}-new-{iteration}', ++ 'sn': f'NewUser{iteration}', ++ 'uidNumber': str(2000 + iteration), ++ 'gidNumber': str(2000 + iteration), ++ 'homeDirectory': f'/home/{TEST_USER_PREFIX}-new-{iteration}', ++ 'userPassword': 'newpassword123' ++ } ++ try: ++ new_user = users_collection.create(properties=new_user_props) ++ new_user_usn = new_user.get_attr_val_int('entryusn') ++ log.info(f"Created new entry '{new_user.get_attr_val_utf8('cn')}' with USN: {new_user_usn}") ++ users.append(new_user) # Add to cleanup list ++ except Exception as e: ++ log.warning(f"Failed to create new user in iteration {iteration}: {e}") ++ ++ # Final validation: Check all USN values are reasonable ++ log.info("\nFinal USN validation") ++ final_lastusn = root_dse.get_attr_val_int("lastusn") ++ ++ for user in users: ++ try: ++ final_usn = user.get_attr_val_int('entryusn') ++ cn_value = user.get_attr_val_utf8('cn') ++ log.info(f"Final entryUSN for '{cn_value}': {final_usn}") ++ ++ # Ensure no overflow occurred ++ assert final_usn > 0, f"Final entryUSN should be positive for {cn_value}: {final_usn}" ++ assert final_usn < MAX_USN_64BIT, f"EntryUSN overflow for {cn_value}: {final_usn}" ++ ++ except ldap.NO_SUCH_OBJECT: ++ log.info(f"User {user.dn} was deleted during test") ++ ++ log.info(f"Final lastUSN: {final_lastusn}") ++ assert final_lastusn > initial_lastusn, "LastUSN should have increased during test" ++ assert final_lastusn < MAX_USN_64BIT, f"LastUSN overflow detected: {final_lastusn}" ++ ++ log.info("EntryUSN overflow test completed successfully") ++ ++ ++def test_entryusn_consistency_after_failed_adds(topology_st, setup_usn_test): ++ """Test that entryUSN remains consistent after failed add operations ++ ++ :id: e380ccad-527b-427e-a331-df5c41badbed ++ :setup: Standalone instance with USN plugin enabled and test users ++ :steps: ++ 1. Record entryUSN values before failed add attempts ++ 2. Attempt to add existing entries (should fail) ++ 3. Verify entryUSN values haven't changed due to failed operations ++ 4. Perform successful modify operations ++ 5. Verify entryUSN increments correctly ++ :expectedresults: ++ 1. Initial entryUSN values recorded ++ 2. Add operations fail as expected ++ 3. EntryUSN values unchanged after failed adds ++ 4. Modify operations succeed ++ 5. EntryUSN values increment correctly without overflow ++ """ ++ ++ inst = topology_st.standalone ++ users = setup_usn_test ++ ++ log.info("Testing entryUSN consistency after failed adds") ++ ++ # Record USN values before any operations ++ pre_operation_usns = {} ++ for user in users: ++ usn = user.get_attr_val_int('entryusn') ++ pre_operation_usns[user.dn] = usn ++ log.info(f"Pre-operation entryUSN for {user.get_attr_val_utf8('cn')}: {usn}") ++ ++ # Attempt to add existing entries - these should fail ++ users_collection = UserAccounts(inst, DEFAULT_SUFFIX) ++ ++ for user in users: ++ cn_value = user.get_attr_val_utf8('cn') ++ log.info(f"Attempting to add existing user: {cn_value}") ++ ++ user_attrs = { ++ 'uid': user.get_attr_val_utf8('uid'), ++ 'cn': cn_value, ++ 'sn': user.get_attr_val_utf8('sn'), ++ 'uidNumber': user.get_attr_val_utf8('uidNumber'), ++ 'gidNumber': user.get_attr_val_utf8('gidNumber'), ++ 'homeDirectory': user.get_attr_val_utf8('homeDirectory'), ++ 'userPassword': 'password123' ++ } ++ ++ try: ++ users_collection.create(properties=user_attrs) ++ assert False, f"Add operation should have failed for existing user {cn_value}" ++ except ldap.ALREADY_EXISTS: ++ log.info(f"Got expected ALREADY_EXISTS for {cn_value}") ++ ++ # Verify USN values haven't changed after failed adds ++ log.info("Verifying entryUSN values after failed add operations...") ++ for user in users: ++ current_usn = user.get_attr_val_int('entryusn') ++ expected_usn = pre_operation_usns[user.dn] ++ cn_value = user.get_attr_val_utf8('cn') ++ ++ assert current_usn == expected_usn, \ ++ f"EntryUSN changed after failed add for {cn_value}: was {expected_usn}, now {current_usn}" ++ log.info(f"EntryUSN unchanged for {cn_value}: {current_usn}") ++ ++ # Now perform successful modify operations ++ log.info("Performing successful modify operations...") ++ for i, user in enumerate(users): ++ cn_value = user.get_attr_val_utf8('cn') ++ old_usn = user.get_attr_val_int('entryusn') ++ ++ # Modify the user ++ user.replace('description', f'Consistency test modification {i + 1}') ++ ++ new_usn = user.get_attr_val_int('entryusn') ++ log.info(f"Modified {cn_value}: USN {old_usn} -> {new_usn}") ++ ++ # Verify proper increment ++ assert (new_usn - old_usn) == 1, f"EntryUSN should increment by 1 for {cn_value}: {old_usn} -> {new_usn}" ++ assert new_usn < MAX_USN_64BIT, f"EntryUSN overflow for {cn_value}: {new_usn}" ++ ++ log.info("EntryUSN consistency test completed successfully") ++ ++ ++if __name__ == '__main__': ++ # Run isolated ++ # -s for DEBUG mode ++ CURRENT_FILE = os.path.realpath(__file__) ++ pytest.main("-s %s" % CURRENT_FILE) +\ No newline at end of file +-- +2.49.0 + diff --git a/0017-Issue-6594-Add-test-for-numSubordinates-replication-.patch b/0017-Issue-6594-Add-test-for-numSubordinates-replication-.patch new file mode 100644 index 0000000..9680aa3 --- /dev/null +++ b/0017-Issue-6594-Add-test-for-numSubordinates-replication-.patch @@ -0,0 +1,172 @@ +From 37a56f75afac2805e1ba958eebd496e77b7079e7 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Mon, 28 Jul 2025 15:35:50 -0700 +Subject: [PATCH] Issue 6594 - Add test for numSubordinates replication + consistency with tombstones (#6862) + +Description: Add a comprehensive test to verify that numSubordinates and +tombstoneNumSubordinates attributes are correctly replicated between +instances when tombstone entries are present. + +Fixes: https://github.com/389ds/389-ds-base/issues/6594 + +Reviewed by: @progier389 (Thanks!) +--- + .../numsubordinates_replication_test.py | 144 ++++++++++++++++++ + 1 file changed, 144 insertions(+) + create mode 100644 dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py + +diff --git a/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py b/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py +new file mode 100644 +index 000000000..9ba10657d +--- /dev/null ++++ b/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py +@@ -0,0 +1,144 @@ ++# --- BEGIN COPYRIGHT BLOCK --- ++# Copyright (C) 2025 Red Hat, Inc. ++# All rights reserved. ++# ++# License: GPL (version 3 or any later version). ++# See LICENSE for details. ++# --- END COPYRIGHT BLOCK --- ++ ++import os ++import logging ++import pytest ++from lib389._constants import DEFAULT_SUFFIX ++from lib389.replica import ReplicationManager ++from lib389.idm.organizationalunit import OrganizationalUnits ++from lib389.idm.user import UserAccounts ++from lib389.topologies import topology_i2 as topo_i2 ++ ++ ++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 test_numsubordinates_tombstone_replication_mismatch(topo_i2): ++ """Test that numSubordinates values match between replicas after tombstone creation ++ ++ :id: c43ecc7a-d706-42e8-9179-1ff7d0e7163a ++ :setup: Two standalone instances ++ :steps: ++ 1. Create a container (organizational unit) on the first instance ++ 2. Create a user object in that container ++ 3. Delete the user object (this creates a tombstone) ++ 4. Set up replication between the two instances ++ 5. Wait for replication to complete ++ 6. Check numSubordinates on both instances ++ 7. Check tombstoneNumSubordinates on both instances ++ 8. Verify that numSubordinates values match on both instances ++ :expectedresults: ++ 1. Container should be created successfully ++ 2. User object should be created successfully ++ 3. User object should be deleted successfully ++ 4. Replication should be set up successfully ++ 5. Replication should complete successfully ++ 6. numSubordinates should be accessible on both instances ++ 7. tombstoneNumSubordinates should be accessible on both instances ++ 8. numSubordinates values should match on both instances ++ """ ++ ++ instance1 = topo_i2.ins["standalone1"] ++ instance2 = topo_i2.ins["standalone2"] ++ ++ log.info("Create a container (organizational unit) on the first instance") ++ ous1 = OrganizationalUnits(instance1, DEFAULT_SUFFIX) ++ container = ous1.create(properties={ ++ 'ou': 'test_container', ++ 'description': 'Test container for numSubordinates replication test' ++ }) ++ container_rdn = container.rdn ++ log.info(f"Created container: {container_rdn}") ++ ++ log.info("Create a user object in that container") ++ users1 = UserAccounts(instance1, DEFAULT_SUFFIX, rdn=f"ou={container_rdn}") ++ test_user = users1.create_test_user(uid=1001) ++ log.info(f"Created user: {test_user.dn}") ++ ++ log.info("Checking initial numSubordinates on container") ++ container_obj1 = OrganizationalUnits(instance1, DEFAULT_SUFFIX).get(container_rdn) ++ initial_numsubordinates = container_obj1.get_attr_val_int('numSubordinates') ++ log.info(f"Initial numSubordinates: {initial_numsubordinates}") ++ assert initial_numsubordinates == 1 ++ ++ log.info("Delete the user object (this creates a tombstone)") ++ test_user.delete() ++ ++ log.info("Checking numSubordinates after deletion") ++ after_delete_numsubordinates = container_obj1.get_attr_val_int('numSubordinates') ++ log.info(f"numSubordinates after deletion: {after_delete_numsubordinates}") ++ ++ log.info("Checking tombstoneNumSubordinates after deletion") ++ try: ++ tombstone_numsubordinates = container_obj1.get_attr_val_int('tombstoneNumSubordinates') ++ log.info(f"tombstoneNumSubordinates: {tombstone_numsubordinates}") ++ except Exception as e: ++ log.info(f"tombstoneNumSubordinates not found or error: {e}") ++ tombstone_numsubordinates = 0 ++ ++ log.info("Set up replication between the two instances") ++ repl = ReplicationManager(DEFAULT_SUFFIX) ++ repl.create_first_supplier(instance1) ++ repl.join_supplier(instance1, instance2) ++ ++ log.info("Wait for replication to complete") ++ repl.wait_for_replication(instance1, instance2) ++ ++ log.info("Check numSubordinates on both instances") ++ container_obj1 = OrganizationalUnits(instance1, DEFAULT_SUFFIX).get(container_rdn) ++ numsubordinates_instance1 = container_obj1.get_attr_val_int('numSubordinates') ++ log.info(f"numSubordinates on instance1: {numsubordinates_instance1}") ++ ++ container_obj2 = OrganizationalUnits(instance2, DEFAULT_SUFFIX).get(container_rdn) ++ numsubordinates_instance2 = container_obj2.get_attr_val_int('numSubordinates') ++ log.info(f"numSubordinates on instance2: {numsubordinates_instance2}") ++ ++ log.info("Check tombstoneNumSubordinates on both instances") ++ try: ++ tombstone_numsubordinates_instance1 = container_obj1.get_attr_val_int('tombstoneNumSubordinates') ++ log.info(f"tombstoneNumSubordinates on instance1: {tombstone_numsubordinates_instance1}") ++ except Exception as e: ++ log.info(f"tombstoneNumSubordinates not found on instance1: {e}") ++ tombstone_numsubordinates_instance1 = 0 ++ ++ try: ++ tombstone_numsubordinates_instance2 = container_obj2.get_attr_val_int('tombstoneNumSubordinates') ++ log.info(f"tombstoneNumSubordinates on instance2: {tombstone_numsubordinates_instance2}") ++ except Exception as e: ++ log.info(f"tombstoneNumSubordinates not found on instance2: {e}") ++ tombstone_numsubordinates_instance2 = 0 ++ ++ log.info("Verify that numSubordinates values match on both instances") ++ log.info(f"Comparison: instance1 numSubordinates={numsubordinates_instance1}, " ++ f"instance2 numSubordinates={numsubordinates_instance2}") ++ log.info(f"Comparison: instance1 tombstoneNumSubordinates={tombstone_numsubordinates_instance1}, " ++ f"instance2 tombstoneNumSubordinates={tombstone_numsubordinates_instance2}") ++ ++ assert numsubordinates_instance1 == numsubordinates_instance2, ( ++ f"numSubordinates mismatch: instance1 has {numsubordinates_instance1}, " ++ f"instance2 has {numsubordinates_instance2}. " ++ ) ++ assert tombstone_numsubordinates_instance1 == tombstone_numsubordinates_instance2, ( ++ f"tombstoneNumSubordinates mismatch: instance1 has {tombstone_numsubordinates_instance1}, " ++ f"instance2 has {tombstone_numsubordinates_instance2}. " ++ ) ++ ++ ++if __name__ == '__main__': ++ # Run isolated ++ # -s for DEBUG mode ++ CURRENT_FILE = os.path.realpath(__file__) ++ pytest.main("-s %s" % CURRENT_FILE) +\ No newline at end of file +-- +2.49.0 + diff --git a/0018-Issue-6884-Mask-password-hashes-in-audit-logs-6885.patch b/0018-Issue-6884-Mask-password-hashes-in-audit-logs-6885.patch new file mode 100644 index 0000000..f7f9a2b --- /dev/null +++ b/0018-Issue-6884-Mask-password-hashes-in-audit-logs-6885.patch @@ -0,0 +1,814 @@ +From e05653cbff500c47b89e43e4a1c85b7cb30321ff Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Mon, 28 Jul 2025 15:41:29 -0700 +Subject: [PATCH] Issue 6884 - Mask password hashes in audit logs (#6885) + +Description: Fix the audit log functionality to mask password hash values for +userPassword, nsslapd-rootpw, nsmultiplexorcredentials, nsds5ReplicaCredentials, +and nsds5ReplicaBootstrapCredentials attributes in ADD and MODIFY operations. +Update auditlog.c to detect password attributes and replace their values with +asterisks (**********************) in both LDIF and JSON audit log formats. +Add a comprehensive test suite audit_password_masking_test.py to verify +password masking works correctly across all log formats and operation types. + +Fixes: https://github.com/389ds/389-ds-base/issues/6884 + +Reviewed by: @mreynolds389, @vashirov (Thanks!!) +--- + .../logging/audit_password_masking_test.py | 501 ++++++++++++++++++ + ldap/servers/slapd/auditlog.c | 170 +++++- + ldap/servers/slapd/slapi-private.h | 1 + + src/lib389/lib389/chaining.py | 3 +- + 4 files changed, 652 insertions(+), 23 deletions(-) + create mode 100644 dirsrvtests/tests/suites/logging/audit_password_masking_test.py + +diff --git a/dirsrvtests/tests/suites/logging/audit_password_masking_test.py b/dirsrvtests/tests/suites/logging/audit_password_masking_test.py +new file mode 100644 +index 000000000..3b6a54849 +--- /dev/null ++++ b/dirsrvtests/tests/suites/logging/audit_password_masking_test.py +@@ -0,0 +1,501 @@ ++# --- BEGIN COPYRIGHT BLOCK --- ++# Copyright (C) 2025 Red Hat, Inc. ++# All rights reserved. ++# ++# License: GPL (version 3 or any later version). ++# See LICENSE for details. ++# --- END COPYRIGHT BLOCK --- ++# ++import logging ++import pytest ++import os ++import re ++import time ++import ldap ++from lib389._constants import DEFAULT_SUFFIX, DN_DM, PW_DM ++from lib389.topologies import topology_m2 as topo ++from lib389.idm.user import UserAccounts ++from lib389.dirsrv_log import DirsrvAuditJSONLog ++from lib389.plugins import ChainingBackendPlugin ++from lib389.chaining import ChainingLinks ++from lib389.agreement import Agreements ++from lib389.replica import ReplicationManager, Replicas ++from lib389.idm.directorymanager import DirectoryManager ++ ++log = logging.getLogger(__name__) ++ ++MASKED_PASSWORD = "**********************" ++TEST_PASSWORD = "MySecret123" ++TEST_PASSWORD_2 = "NewPassword789" ++TEST_PASSWORD_3 = "NewPassword101" ++ ++ ++def setup_audit_logging(inst, log_format='default', display_attrs=None): ++ """Configure audit logging settings""" ++ inst.config.replace('nsslapd-auditlog-logbuffering', 'off') ++ inst.config.replace('nsslapd-auditlog-logging-enabled', 'on') ++ inst.config.replace('nsslapd-auditlog-log-format', log_format) ++ ++ if display_attrs is not None: ++ inst.config.replace('nsslapd-auditlog-display-attrs', display_attrs) ++ ++ inst.deleteAuditLogs() ++ ++ ++def check_password_masked(inst, log_format, expected_password, actual_password): ++ """Helper function to check password masking in audit logs""" ++ ++ time.sleep(1) # Allow log to flush ++ ++ # List of all password/credential attributes that should be masked ++ password_attributes = [ ++ 'userPassword', ++ 'nsslapd-rootpw', ++ 'nsmultiplexorcredentials', ++ 'nsDS5ReplicaCredentials', ++ 'nsDS5ReplicaBootstrapCredentials' ++ ] ++ ++ # Get password schemes to check for hash leakage ++ user_password_scheme = inst.config.get_attr_val_utf8('passwordStorageScheme') ++ root_password_scheme = inst.config.get_attr_val_utf8('nsslapd-rootpwstoragescheme') ++ ++ if log_format == 'json': ++ # Check JSON format logs ++ audit_log = DirsrvAuditJSONLog(inst) ++ log_lines = audit_log.readlines() ++ ++ found_masked = False ++ found_actual = False ++ found_hashed = False ++ ++ for line in log_lines: ++ # Check if any password attribute is present in the line ++ for attr in password_attributes: ++ if attr in line: ++ if expected_password in line: ++ found_masked = True ++ if actual_password in line: ++ found_actual = True ++ # Check for password scheme indicators (hashed passwords) ++ if user_password_scheme and f'{{{user_password_scheme}}}' in line: ++ found_hashed = True ++ if root_password_scheme and f'{{{root_password_scheme}}}' in line: ++ found_hashed = True ++ break # Found a password attribute, no need to check others for this line ++ ++ else: ++ # Check LDIF format logs ++ found_masked = False ++ found_actual = False ++ found_hashed = False ++ ++ # Check each password attribute for masked password ++ for attr in password_attributes: ++ if inst.ds_audit_log.match(f"{attr}: {re.escape(expected_password)}"): ++ found_masked = True ++ if inst.ds_audit_log.match(f"{attr}: {actual_password}"): ++ found_actual = True ++ ++ # Check for hashed passwords in LDIF format ++ if user_password_scheme: ++ if inst.ds_audit_log.match(f"userPassword: {{{user_password_scheme}}}"): ++ found_hashed = True ++ if root_password_scheme: ++ if inst.ds_audit_log.match(f"nsslapd-rootpw: {{{root_password_scheme}}}"): ++ found_hashed = True ++ ++ # Delete audit logs to avoid interference with other tests ++ # We need to reset the root password to default as deleteAuditLogs() ++ # opens a new connection with the default password ++ dm = DirectoryManager(inst) ++ dm.change_password(PW_DM) ++ inst.deleteAuditLogs() ++ ++ return found_masked, found_actual, found_hashed ++ ++ ++@pytest.mark.parametrize("log_format,display_attrs", [ ++ ("default", None), ++ ("default", "*"), ++ ("default", "userPassword"), ++ ("json", None), ++ ("json", "*"), ++ ("json", "userPassword") ++]) ++def test_password_masking_add_operation(topo, log_format, display_attrs): ++ """Test password masking in ADD operations ++ ++ :id: 4358bd75-bcc7-401c-b492-d3209b10412d ++ :parametrized: yes ++ :setup: Standalone Instance ++ :steps: ++ 1. Configure audit logging format ++ 2. Add user with password ++ 3. Check that password is masked in audit log ++ 4. Verify actual password does not appear in log ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Password should be masked with asterisks ++ 4. Actual password should not be found in log ++ """ ++ inst = topo.ms['supplier1'] ++ setup_audit_logging(inst, log_format, display_attrs) ++ ++ users = UserAccounts(inst, DEFAULT_SUFFIX) ++ user = None ++ ++ try: ++ user = users.create(properties={ ++ 'uid': 'test_add_pwd_mask', ++ 'cn': 'Test Add User', ++ 'sn': 'User', ++ 'uidNumber': '1000', ++ 'gidNumber': '1000', ++ 'homeDirectory': '/home/test_add', ++ 'userPassword': TEST_PASSWORD ++ }) ++ ++ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD) ++ ++ assert found_masked, f"Masked password not found in {log_format} ADD operation" ++ assert not found_actual, f"Actual password found in {log_format} ADD log (should be masked)" ++ assert not found_hashed, f"Hashed password found in {log_format} ADD log (should be masked)" ++ ++ finally: ++ if user is not None: ++ try: ++ user.delete() ++ except: ++ pass ++ ++ ++@pytest.mark.parametrize("log_format,display_attrs", [ ++ ("default", None), ++ ("default", "*"), ++ ("default", "userPassword"), ++ ("json", None), ++ ("json", "*"), ++ ("json", "userPassword") ++]) ++def test_password_masking_modify_operation(topo, log_format, display_attrs): ++ """Test password masking in MODIFY operations ++ ++ :id: e6963aa9-7609-419c-aae2-1d517aa434bd ++ :parametrized: yes ++ :setup: Standalone Instance ++ :steps: ++ 1. Configure audit logging format ++ 2. Add user without password ++ 3. Add password via MODIFY operation ++ 4. Check that password is masked in audit log ++ 5. Modify password to new value ++ 6. Check that new password is also masked ++ 7. Verify actual passwords do not appear in log ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Password should be masked with asterisks ++ 5. Success ++ 6. New password should be masked with asterisks ++ 7. No actual password values should be found in log ++ """ ++ inst = topo.ms['supplier1'] ++ setup_audit_logging(inst, log_format, display_attrs) ++ ++ users = UserAccounts(inst, DEFAULT_SUFFIX) ++ user = None ++ ++ try: ++ user = users.create(properties={ ++ 'uid': 'test_modify_pwd_mask', ++ 'cn': 'Test Modify User', ++ 'sn': 'User', ++ 'uidNumber': '2000', ++ 'gidNumber': '2000', ++ 'homeDirectory': '/home/test_modify' ++ }) ++ ++ user.replace('userPassword', TEST_PASSWORD) ++ ++ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD) ++ assert found_masked, f"Masked password not found in {log_format} MODIFY operation (first password)" ++ assert not found_actual, f"Actual password found in {log_format} MODIFY log (should be masked)" ++ assert not found_hashed, f"Hashed password found in {log_format} MODIFY log (should be masked)" ++ ++ user.replace('userPassword', TEST_PASSWORD_2) ++ ++ found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2) ++ assert found_masked_2, f"Masked password not found in {log_format} MODIFY operation (second password)" ++ assert not found_actual_2, f"Second actual password found in {log_format} MODIFY log (should be masked)" ++ assert not found_hashed_2, f"Second hashed password found in {log_format} MODIFY log (should be masked)" ++ ++ finally: ++ if user is not None: ++ try: ++ user.delete() ++ except: ++ pass ++ ++ ++@pytest.mark.parametrize("log_format,display_attrs", [ ++ ("default", None), ++ ("default", "*"), ++ ("default", "nsslapd-rootpw"), ++ ("json", None), ++ ("json", "*"), ++ ("json", "nsslapd-rootpw") ++]) ++def test_password_masking_rootpw_modify_operation(topo, log_format, display_attrs): ++ """Test password masking for nsslapd-rootpw MODIFY operations ++ ++ :id: ec8c9fd4-56ba-4663-ab65-58efb3b445e4 ++ :parametrized: yes ++ :setup: Standalone Instance ++ :steps: ++ 1. Configure audit logging format ++ 2. Modify nsslapd-rootpw in configuration ++ 3. Check that root password is masked in audit log ++ 4. Modify root password to new value ++ 5. Check that new root password is also masked ++ 6. Verify actual root passwords do not appear in log ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Root password should be masked with asterisks ++ 4. Success ++ 5. New root password should be masked with asterisks ++ 6. No actual root password values should be found in log ++ """ ++ inst = topo.ms['supplier1'] ++ setup_audit_logging(inst, log_format, display_attrs) ++ dm = DirectoryManager(inst) ++ ++ try: ++ dm.change_password(TEST_PASSWORD) ++ dm.rebind(TEST_PASSWORD) ++ ++ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD) ++ assert found_masked, f"Masked root password not found in {log_format} MODIFY operation (first root password)" ++ assert not found_actual, f"Actual root password found in {log_format} MODIFY log (should be masked)" ++ assert not found_hashed, f"Hashed root password found in {log_format} MODIFY log (should be masked)" ++ ++ dm.change_password(TEST_PASSWORD_2) ++ dm.rebind(TEST_PASSWORD_2) ++ ++ found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2) ++ assert found_masked_2, f"Masked root password not found in {log_format} MODIFY operation (second root password)" ++ assert not found_actual_2, f"Second actual root password found in {log_format} MODIFY log (should be masked)" ++ assert not found_hashed_2, f"Second hashed root password found in {log_format} MODIFY log (should be masked)" ++ ++ finally: ++ dm.change_password(PW_DM) ++ dm.rebind(PW_DM) ++ ++ ++@pytest.mark.parametrize("log_format,display_attrs", [ ++ ("default", None), ++ ("default", "*"), ++ ("default", "nsmultiplexorcredentials"), ++ ("json", None), ++ ("json", "*"), ++ ("json", "nsmultiplexorcredentials") ++]) ++def test_password_masking_multiplexor_credentials(topo, log_format, display_attrs): ++ """Test password masking for nsmultiplexorcredentials in chaining/multiplexor configurations ++ ++ :id: 161a9498-b248-4926-90be-a696a36ed36e ++ :parametrized: yes ++ :setup: Standalone Instance ++ :steps: ++ 1. Configure audit logging format ++ 2. Create a chaining backend configuration entry with nsmultiplexorcredentials ++ 3. Check that multiplexor credentials are masked in audit log ++ 4. Modify the credentials ++ 5. Check that updated credentials are also masked ++ 6. Verify actual credentials do not appear in log ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Multiplexor credentials should be masked with asterisks ++ 4. Success ++ 5. Updated credentials should be masked with asterisks ++ 6. No actual credential values should be found in log ++ """ ++ inst = topo.ms['supplier1'] ++ setup_audit_logging(inst, log_format, display_attrs) ++ ++ # Enable chaining plugin and create chaining link ++ chain_plugin = ChainingBackendPlugin(inst) ++ chain_plugin.enable() ++ ++ chains = ChainingLinks(inst) ++ chain = None ++ ++ try: ++ # Create chaining link with multiplexor credentials ++ chain = chains.create(properties={ ++ 'cn': 'testchain', ++ 'nsfarmserverurl': 'ldap://localhost:389/', ++ 'nsslapd-suffix': 'dc=example,dc=com', ++ 'nsmultiplexorbinddn': 'cn=manager', ++ 'nsmultiplexorcredentials': TEST_PASSWORD, ++ 'nsCheckLocalACI': 'on', ++ 'nsConnectionLife': '30', ++ }) ++ ++ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD) ++ assert found_masked, f"Masked multiplexor credentials not found in {log_format} ADD operation" ++ assert not found_actual, f"Actual multiplexor credentials found in {log_format} ADD log (should be masked)" ++ assert not found_hashed, f"Hashed multiplexor credentials found in {log_format} ADD log (should be masked)" ++ ++ # Modify the credentials ++ chain.replace('nsmultiplexorcredentials', TEST_PASSWORD_2) ++ ++ found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2) ++ assert found_masked_2, f"Masked multiplexor credentials not found in {log_format} MODIFY operation" ++ assert not found_actual_2, f"Actual multiplexor credentials found in {log_format} MODIFY log (should be masked)" ++ assert not found_hashed_2, f"Hashed multiplexor credentials found in {log_format} MODIFY log (should be masked)" ++ ++ finally: ++ chain_plugin.disable() ++ if chain is not None: ++ inst.delete_branch_s(chain.dn, ldap.SCOPE_ONELEVEL) ++ chain.delete() ++ ++ ++@pytest.mark.parametrize("log_format,display_attrs", [ ++ ("default", None), ++ ("default", "*"), ++ ("default", "nsDS5ReplicaCredentials"), ++ ("json", None), ++ ("json", "*"), ++ ("json", "nsDS5ReplicaCredentials") ++]) ++def test_password_masking_replica_credentials(topo, log_format, display_attrs): ++ """Test password masking for nsDS5ReplicaCredentials in replication agreements ++ ++ :id: 7bf9e612-1b7c-49af-9fc0-de4c7df84b2a ++ :parametrized: yes ++ :setup: Standalone Instance ++ :steps: ++ 1. Configure audit logging format ++ 2. Create a replication agreement entry with nsDS5ReplicaCredentials ++ 3. Check that replica credentials are masked in audit log ++ 4. Modify the credentials ++ 5. Check that updated credentials are also masked ++ 6. Verify actual credentials do not appear in log ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Replica credentials should be masked with asterisks ++ 4. Success ++ 5. Updated credentials should be masked with asterisks ++ 6. No actual credential values should be found in log ++ """ ++ inst = topo.ms['supplier2'] ++ setup_audit_logging(inst, log_format, display_attrs) ++ agmt = None ++ ++ try: ++ replicas = Replicas(inst) ++ replica = replicas.get(DEFAULT_SUFFIX) ++ agmts = replica.get_agreements() ++ agmt = agmts.create(properties={ ++ 'cn': 'testagmt', ++ 'nsDS5ReplicaHost': 'localhost', ++ 'nsDS5ReplicaPort': '389', ++ 'nsDS5ReplicaBindDN': 'cn=replication manager,cn=config', ++ 'nsDS5ReplicaCredentials': TEST_PASSWORD, ++ 'nsDS5ReplicaRoot': DEFAULT_SUFFIX ++ }) ++ ++ found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD) ++ assert found_masked, f"Masked replica credentials not found in {log_format} ADD operation" ++ assert not found_actual, f"Actual replica credentials found in {log_format} ADD log (should be masked)" ++ assert not found_hashed, f"Hashed replica credentials found in {log_format} ADD log (should be masked)" ++ ++ # Modify the credentials ++ agmt.replace('nsDS5ReplicaCredentials', TEST_PASSWORD_2) ++ ++ found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2) ++ assert found_masked_2, f"Masked replica credentials not found in {log_format} MODIFY operation" ++ assert not found_actual_2, f"Actual replica credentials found in {log_format} MODIFY log (should be masked)" ++ assert not found_hashed_2, f"Hashed replica credentials found in {log_format} MODIFY log (should be masked)" ++ ++ finally: ++ if agmt is not None: ++ agmt.delete() ++ ++ ++@pytest.mark.parametrize("log_format,display_attrs", [ ++ ("default", None), ++ ("default", "*"), ++ ("default", "nsDS5ReplicaBootstrapCredentials"), ++ ("json", None), ++ ("json", "*"), ++ ("json", "nsDS5ReplicaBootstrapCredentials") ++]) ++def test_password_masking_bootstrap_credentials(topo, log_format, display_attrs): ++ """Test password masking for nsDS5ReplicaCredentials and nsDS5ReplicaBootstrapCredentials in replication agreements ++ ++ :id: 248bd418-ffa4-4733-963d-2314c60b7c5b ++ :parametrized: yes ++ :setup: Standalone Instance ++ :steps: ++ 1. Configure audit logging format ++ 2. Create a replication agreement entry with both nsDS5ReplicaCredentials and nsDS5ReplicaBootstrapCredentials ++ 3. Check that both credentials are masked in audit log ++ 4. Modify both credentials ++ 5. Check that both updated credentials are also masked ++ 6. Verify actual credentials do not appear in log ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Both credentials should be masked with asterisks ++ 4. Success ++ 5. Both updated credentials should be masked with asterisks ++ 6. No actual credential values should be found in log ++ """ ++ inst = topo.ms['supplier2'] ++ setup_audit_logging(inst, log_format, display_attrs) ++ agmt = None ++ ++ try: ++ replicas = Replicas(inst) ++ replica = replicas.get(DEFAULT_SUFFIX) ++ agmts = replica.get_agreements() ++ agmt = agmts.create(properties={ ++ 'cn': 'testbootstrapagmt', ++ 'nsDS5ReplicaHost': 'localhost', ++ 'nsDS5ReplicaPort': '389', ++ 'nsDS5ReplicaBindDN': 'cn=replication manager,cn=config', ++ 'nsDS5ReplicaCredentials': TEST_PASSWORD, ++ 'nsDS5replicabootstrapbinddn': 'cn=bootstrap manager,cn=config', ++ 'nsDS5ReplicaBootstrapCredentials': TEST_PASSWORD_2, ++ 'nsDS5ReplicaRoot': DEFAULT_SUFFIX ++ }) ++ ++ found_masked_bootstrap, found_actual_bootstrap, found_hashed_bootstrap = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2) ++ assert found_masked_bootstrap, f"Masked bootstrap credentials not found in {log_format} ADD operation" ++ assert not found_actual_bootstrap, f"Actual bootstrap credentials found in {log_format} ADD log (should be masked)" ++ assert not found_hashed_bootstrap, f"Hashed bootstrap credentials found in {log_format} ADD log (should be masked)" ++ ++ agmt.replace('nsDS5ReplicaBootstrapCredentials', TEST_PASSWORD_3) ++ ++ found_masked_bootstrap_2, found_actual_bootstrap_2, found_hashed_bootstrap_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_3) ++ assert found_masked_bootstrap_2, f"Masked bootstrap credentials not found in {log_format} MODIFY operation" ++ assert not found_actual_bootstrap_2, f"Actual bootstrap credentials found in {log_format} MODIFY log (should be masked)" ++ assert not found_hashed_bootstrap_2, f"Hashed bootstrap credentials found in {log_format} MODIFY log (should be masked)" ++ ++ finally: ++ if agmt is not None: ++ agmt.delete() ++ ++ ++ ++if __name__ == '__main__': ++ CURRENT_FILE = os.path.realpath(__file__) ++ pytest.main(["-s", CURRENT_FILE]) +\ No newline at end of file +diff --git a/ldap/servers/slapd/auditlog.c b/ldap/servers/slapd/auditlog.c +index 3945b0533..3a34959f6 100644 +--- a/ldap/servers/slapd/auditlog.c ++++ b/ldap/servers/slapd/auditlog.c +@@ -39,6 +39,89 @@ static void write_audit_file(Slapi_PBlock *pb, Slapi_Entry *entry, int logtype, + + static const char *modrdn_changes[4]; + ++/* Helper function to check if an attribute is a password that needs masking */ ++static int ++is_password_attribute(const char *attr_name) ++{ ++ return (strcasecmp(attr_name, SLAPI_USERPWD_ATTR) == 0 || ++ strcasecmp(attr_name, CONFIG_ROOTPW_ATTRIBUTE) == 0 || ++ strcasecmp(attr_name, SLAPI_MB_CREDENTIALS) == 0 || ++ strcasecmp(attr_name, SLAPI_REP_CREDENTIALS) == 0 || ++ strcasecmp(attr_name, SLAPI_REP_BOOTSTRAP_CREDENTIALS) == 0); ++} ++ ++/* Helper function to create a masked string representation of an entry */ ++static char * ++create_masked_entry_string(Slapi_Entry *original_entry, int *len) ++{ ++ Slapi_Attr *attr = NULL; ++ char *entry_str = NULL; ++ char *current_pos = NULL; ++ char *line_start = NULL; ++ char *next_line = NULL; ++ char *colon_pos = NULL; ++ int has_password_attrs = 0; ++ ++ if (original_entry == NULL) { ++ return NULL; ++ } ++ ++ /* Single pass through attributes to check for password attributes */ ++ for (slapi_entry_first_attr(original_entry, &attr); attr != NULL; ++ slapi_entry_next_attr(original_entry, attr, &attr)) { ++ ++ char *attr_name = NULL; ++ slapi_attr_get_type(attr, &attr_name); ++ ++ if (is_password_attribute(attr_name)) { ++ has_password_attrs = 1; ++ break; ++ } ++ } ++ ++ /* If no password attributes, return original string - no masking needed */ ++ entry_str = slapi_entry2str(original_entry, len); ++ if (!has_password_attrs) { ++ return entry_str; ++ } ++ ++ /* Process the string in-place, replacing password values */ ++ current_pos = entry_str; ++ while ((line_start = current_pos) != NULL && *line_start != '\0') { ++ /* Find the end of current line */ ++ next_line = strchr(line_start, '\n'); ++ if (next_line != NULL) { ++ *next_line = '\0'; /* Temporarily terminate line */ ++ current_pos = next_line + 1; ++ } else { ++ current_pos = NULL; /* Last line */ ++ } ++ ++ /* Find the colon that separates attribute name from value */ ++ colon_pos = strchr(line_start, ':'); ++ if (colon_pos != NULL) { ++ char saved_colon = *colon_pos; ++ *colon_pos = '\0'; /* Temporarily null-terminate attribute name */ ++ ++ /* Check if this is a password attribute that needs masking */ ++ if (is_password_attribute(line_start)) { ++ strcpy(colon_pos + 1, " **********************"); ++ } ++ ++ *colon_pos = saved_colon; /* Restore colon */ ++ } ++ ++ /* Restore newline if it was there */ ++ if (next_line != NULL) { ++ *next_line = '\n'; ++ } ++ } ++ ++ /* Update length since we may have shortened the string */ ++ *len = strlen(entry_str); ++ return entry_str; /* Return the modified original string */ ++} ++ + void + write_audit_log_entry(Slapi_PBlock *pb) + { +@@ -279,10 +362,31 @@ add_entry_attrs_ext(Slapi_Entry *entry, lenstr *l, PRBool use_json, json_object + { + slapi_entry_attr_find(entry, req_attr, &entry_attr); + if (entry_attr) { +- if (use_json) { +- log_entry_attr_json(entry_attr, req_attr, id_list); ++ if (strcmp(req_attr, PSEUDO_ATTR_UNHASHEDUSERPASSWORD) == 0) { ++ /* Do not write the unhashed clear-text password */ ++ continue; ++ } ++ ++ /* Check if this is a password attribute that needs masking */ ++ if (is_password_attribute(req_attr)) { ++ /* userpassword/rootdn password - mask the value */ ++ if (use_json) { ++ json_object *secret_obj = json_object_new_object(); ++ json_object_object_add(secret_obj, req_attr, ++ json_object_new_string("**********************")); ++ json_object_array_add(id_list, secret_obj); ++ } else { ++ addlenstr(l, "#"); ++ addlenstr(l, req_attr); ++ addlenstr(l, ": **********************\n"); ++ } + } else { +- log_entry_attr(entry_attr, req_attr, l); ++ /* Regular attribute - log normally */ ++ if (use_json) { ++ log_entry_attr_json(entry_attr, req_attr, id_list); ++ } else { ++ log_entry_attr(entry_attr, req_attr, l); ++ } + } + } + } +@@ -297,9 +401,7 @@ add_entry_attrs_ext(Slapi_Entry *entry, lenstr *l, PRBool use_json, json_object + continue; + } + +- if (strcasecmp(attr, SLAPI_USERPWD_ATTR) == 0 || +- strcasecmp(attr, CONFIG_ROOTPW_ATTRIBUTE) == 0) +- { ++ if (is_password_attribute(attr)) { + /* userpassword/rootdn password - mask the value */ + if (use_json) { + json_object *secret_obj = json_object_new_object(); +@@ -309,7 +411,7 @@ add_entry_attrs_ext(Slapi_Entry *entry, lenstr *l, PRBool use_json, json_object + } else { + addlenstr(l, "#"); + addlenstr(l, attr); +- addlenstr(l, ": ****************************\n"); ++ addlenstr(l, ": **********************\n"); + } + continue; + } +@@ -478,6 +580,9 @@ write_audit_file_json(Slapi_PBlock *pb, Slapi_Entry *entry, int logtype, + } + } + ++ /* Check if this is a password attribute that needs masking */ ++ int is_password_attr = is_password_attribute(mods[j]->mod_type); ++ + mod = json_object_new_object(); + switch (operationtype) { + case LDAP_MOD_ADD: +@@ -502,7 +607,12 @@ write_audit_file_json(Slapi_PBlock *pb, Slapi_Entry *entry, int logtype, + json_object *val_list = NULL; + val_list = json_object_new_array(); + for (size_t i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) { +- json_object_array_add(val_list, json_object_new_string(mods[j]->mod_bvalues[i]->bv_val)); ++ if (is_password_attr) { ++ /* Mask password values */ ++ json_object_array_add(val_list, json_object_new_string("**********************")); ++ } else { ++ json_object_array_add(val_list, json_object_new_string(mods[j]->mod_bvalues[i]->bv_val)); ++ } + } + json_object_object_add(mod, "values", val_list); + } +@@ -514,8 +624,11 @@ write_audit_file_json(Slapi_PBlock *pb, Slapi_Entry *entry, int logtype, + + case SLAPI_OPERATION_ADD: + int len; ++ + e = change; +- tmp = slapi_entry2str(e, &len); ++ ++ /* Create a masked string representation for password attributes */ ++ tmp = create_masked_entry_string(e, &len); + tmpsave = tmp; + while ((tmp = strchr(tmp, '\n')) != NULL) { + tmp++; +@@ -662,6 +775,10 @@ write_audit_file( + break; + } + } ++ ++ /* Check if this is a password attribute that needs masking */ ++ int is_password_attr = is_password_attribute(mods[j]->mod_type); ++ + switch (operationtype) { + case LDAP_MOD_ADD: + addlenstr(l, "add: "); +@@ -686,18 +803,27 @@ write_audit_file( + break; + } + if (operationtype != LDAP_MOD_IGNORE) { +- for (i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) { +- char *buf, *bufp; +- len = strlen(mods[j]->mod_type); +- len = LDIF_SIZE_NEEDED(len, mods[j]->mod_bvalues[i]->bv_len) + 1; +- buf = slapi_ch_malloc(len); +- bufp = buf; +- slapi_ldif_put_type_and_value_with_options(&bufp, mods[j]->mod_type, +- mods[j]->mod_bvalues[i]->bv_val, +- mods[j]->mod_bvalues[i]->bv_len, 0); +- *bufp = '\0'; +- addlenstr(l, buf); +- slapi_ch_free((void **)&buf); ++ if (is_password_attr) { ++ /* Add masked password */ ++ for (i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) { ++ addlenstr(l, mods[j]->mod_type); ++ addlenstr(l, ": **********************\n"); ++ } ++ } else { ++ /* Add actual values for non-password attributes */ ++ for (i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) { ++ char *buf, *bufp; ++ len = strlen(mods[j]->mod_type); ++ len = LDIF_SIZE_NEEDED(len, mods[j]->mod_bvalues[i]->bv_len) + 1; ++ buf = slapi_ch_malloc(len); ++ bufp = buf; ++ slapi_ldif_put_type_and_value_with_options(&bufp, mods[j]->mod_type, ++ mods[j]->mod_bvalues[i]->bv_val, ++ mods[j]->mod_bvalues[i]->bv_len, 0); ++ *bufp = '\0'; ++ addlenstr(l, buf); ++ slapi_ch_free((void **)&buf); ++ } + } + } + addlenstr(l, "-\n"); +@@ -708,7 +834,7 @@ write_audit_file( + e = change; + addlenstr(l, attr_changetype); + addlenstr(l, ": add\n"); +- tmp = slapi_entry2str(e, &len); ++ tmp = create_masked_entry_string(e, &len); + tmpsave = tmp; + while ((tmp = strchr(tmp, '\n')) != NULL) { + tmp++; +diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h +index 7a3eb3fdf..fb88488b1 100644 +--- a/ldap/servers/slapd/slapi-private.h ++++ b/ldap/servers/slapd/slapi-private.h +@@ -848,6 +848,7 @@ void task_cleanup(void); + /* for reversible encyrption */ + #define SLAPI_MB_CREDENTIALS "nsmultiplexorcredentials" + #define SLAPI_REP_CREDENTIALS "nsds5ReplicaCredentials" ++#define SLAPI_REP_BOOTSTRAP_CREDENTIALS "nsds5ReplicaBootstrapCredentials" + int pw_rever_encode(Slapi_Value **vals, char *attr_name); + int pw_rever_decode(char *cipher, char **plain, const char *attr_name); + +diff --git a/src/lib389/lib389/chaining.py b/src/lib389/lib389/chaining.py +index 533b83ebf..33ae78c8b 100644 +--- a/src/lib389/lib389/chaining.py ++++ b/src/lib389/lib389/chaining.py +@@ -134,7 +134,7 @@ class ChainingLink(DSLdapObject): + """ + + # Create chaining entry +- super(ChainingLink, self).create(rdn, properties, basedn) ++ link = super(ChainingLink, self).create(rdn, properties, basedn) + + # Create mapping tree entry + dn_comps = ldap.explode_dn(properties['nsslapd-suffix'][0]) +@@ -149,6 +149,7 @@ class ChainingLink(DSLdapObject): + self._mts.ensure_state(properties=mt_properties) + except ldap.ALREADY_EXISTS: + pass ++ return link + + + class ChainingLinks(DSLdapObjects): +-- +2.49.0 + diff --git a/0019-Issue-6897-Fix-disk-monitoring-test-failures-and-imp.patch b/0019-Issue-6897-Fix-disk-monitoring-test-failures-and-imp.patch new file mode 100644 index 0000000..e9a2e57 --- /dev/null +++ b/0019-Issue-6897-Fix-disk-monitoring-test-failures-and-imp.patch @@ -0,0 +1,1721 @@ +From 590c11b8fb24dde31910614eff1810e2eb0377a9 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Mon, 28 Jul 2025 20:02:09 -0700 +Subject: [PATCH] Issue 6897 - Fix disk monitoring test failures and improve + test maintainability (#6898) + +Description: Refactor disk_monitoring_test.py to address failures and +improve maintainability. Replace manual sleep loops with proper wait +conditions using wait_for_condition() and wait_for_log_entry() helpers. +Add comprehensive logging throughout all tests for better debugging. +Implement configuration capture/restore to prevent test pollution +between runs. +Change fixture scope from module to function level for better test +isolation and ensure proper cleanup in all test cases. + +Fixes: https://github.com/389ds/389-ds-base/issues/6897 + +Reviewed by: @mreynolds389 (Thanks!) +--- + .../disk_monitoring/disk_monitoring_test.py | 1429 +++++++++++------ + 1 file changed, 940 insertions(+), 489 deletions(-) + +diff --git a/dirsrvtests/tests/suites/disk_monitoring/disk_monitoring_test.py b/dirsrvtests/tests/suites/disk_monitoring/disk_monitoring_test.py +index 78d7dd794..a4c445748 100644 +--- a/dirsrvtests/tests/suites/disk_monitoring/disk_monitoring_test.py ++++ b/dirsrvtests/tests/suites/disk_monitoring/disk_monitoring_test.py +@@ -1,17 +1,18 @@ + # --- BEGIN COPYRIGHT BLOCK --- +-# Copyright (C) 2018 Red Hat, Inc. ++# Copyright (C) 2025 Red Hat, Inc. + # All rights reserved. + # + # License: GPL (version 3 or any later version). + # See LICENSE for details. + # --- END COPYRIGHT BLOCK --- + +- + import os + import subprocess + import re + import time ++import ldap + import pytest ++import logging + from lib389.tasks import * + from lib389._constants import * + from lib389.utils import ensure_bytes +@@ -20,95 +21,221 @@ from lib389.topologies import topology_st as topo + from lib389.paths import * + from lib389.idm.user import UserAccounts + ++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__) ++ + pytestmark = pytest.mark.tier2 + disk_monitoring_ack = pytest.mark.skipif(not os.environ.get('DISK_MONITORING_ACK', False), reason="Disk monitoring tests may damage system configuration.") + +-THRESHOLD = '30' +-THRESHOLD_BYTES = '30000000' ++THRESHOLD_BYTES = 30000000 + + +-def _withouterrorlog(topo, condition, maxtimesleep): +- timecount = 0 +- while eval(condition): +- time.sleep(1) +- timecount += 1 +- if timecount >= maxtimesleep: break +- assert not eval(condition) ++def presetup(inst): ++ """Presetup function to mount a tmpfs for log directory to simulate disk space limits.""" + ++ log.info("Setting up tmpfs for disk monitoring tests") ++ inst.stop() ++ log_dir = inst.ds_paths.log_dir + +-def _witherrorlog(topo, condition, maxtimesleep): +- timecount = 0 +- with open(topo.standalone.errlog, 'r') as study: study = study.read() +- while condition not in study: +- time.sleep(1) +- timecount += 1 +- with open(topo.standalone.errlog, 'r') as study: study = study.read() +- if timecount >= maxtimesleep: break +- assert condition in study ++ if os.path.exists(log_dir): ++ log.debug(f"Mounting tmpfs on existing directory: {log_dir}") ++ subprocess.call(['mount', '-t', 'tmpfs', '-o', 'size=35M', 'tmpfs', log_dir]) ++ else: ++ log.debug(f"Creating and mounting tmpfs on new directory: {log_dir}") ++ os.mkdir(log_dir) ++ subprocess.call(['mount', '-t', 'tmpfs', '-o', 'size=35M', 'tmpfs', log_dir]) ++ ++ subprocess.call(f'chown {DEFAULT_USER}: -R {log_dir}', shell=True) ++ subprocess.call(f'chown {DEFAULT_USER}: -R {log_dir}/*', shell=True) ++ subprocess.call(f'restorecon -FvvR {log_dir}', shell=True) ++ inst.start() ++ log.info("tmpfs setup completed") ++ ++ ++def setupthesystem(inst): ++ """Setup system configuration for disk monitoring tests.""" ++ ++ log.info("Configuring system for disk monitoring tests") ++ inst.start() ++ inst.config.set('nsslapd-disk-monitoring-grace-period', '1') ++ inst.config.set('nsslapd-accesslog-logbuffering', 'off') ++ inst.config.set('nsslapd-disk-monitoring-threshold', ensure_bytes(str(THRESHOLD_BYTES))) ++ inst.restart() ++ log.info("System configuration completed") ++ ++ ++def capture_config(inst): ++ """Capture current configuration values for later restoration.""" ++ ++ log.info("Capturing current configuration values") ++ ++ config_attrs = [ ++ 'nsslapd-disk-monitoring', ++ 'nsslapd-disk-monitoring-threshold', ++ 'nsslapd-disk-monitoring-grace-period', ++ 'nsslapd-disk-monitoring-logging-critical', ++ 'nsslapd-disk-monitoring-readonly-on-threshold', ++ 'nsslapd-accesslog-logbuffering', ++ 'nsslapd-errorlog-level', ++ 'nsslapd-accesslog-logging-enabled', ++ 'nsslapd-accesslog-maxlogsize', ++ 'nsslapd-accesslog-logrotationtimeunit', ++ 'nsslapd-accesslog-level', ++ 'nsslapd-external-libs-debug-enabled', ++ 'nsslapd-errorlog-logging-enabled' ++ ] ++ ++ captured_config = {} ++ for config_attr in config_attrs: ++ try: ++ current_value = inst.config.get_attr_val_utf8(config_attr) ++ captured_config[config_attr] = current_value ++ log.debug(f"Captured {config_attr}: {current_value}") ++ except Exception as e: ++ log.debug(f"Could not capture {config_attr}: {e}") ++ captured_config[config_attr] = None + ++ log.info("Configuration capture completed") ++ return captured_config + +-def presetup(topo): +- """ +- This is function is part of fixture function setup , will setup the environment for this test. +- """ +- topo.standalone.stop() +- if os.path.exists(topo.standalone.ds_paths.log_dir): +- subprocess.call(['mount', '-t', 'tmpfs', '-o', 'size=35M', 'tmpfs', topo.standalone.ds_paths.log_dir]) +- else: +- os.mkdir(topo.standalone.ds_paths.log_dir) +- subprocess.call(['mount', '-t', 'tmpfs', '-o', 'size=35M', 'tmpfs', topo.standalone.ds_paths.log_dir]) +- subprocess.call('chown {}: -R {}'.format(DEFAULT_USER, topo.standalone.ds_paths.log_dir), shell=True) +- subprocess.call('chown {}: -R {}/*'.format(DEFAULT_USER, topo.standalone.ds_paths.log_dir), shell=True) +- subprocess.call('restorecon -FvvR {}'.format(topo.standalone.ds_paths.log_dir), shell=True) +- topo.standalone.start() + ++def restore_config(inst, captured_config): ++ """Restore configuration values to previously captured state.""" + +-def setupthesystem(topo): +- """ +- This function is part of fixture function setup , will setup the environment for this test. +- """ +- global TOTAL_SIZE, USED_SIZE, AVAIL_SIZE, HALF_THR_FILL_SIZE, FULL_THR_FILL_SIZE +- topo.standalone.start() +- topo.standalone.config.set('nsslapd-disk-monitoring-grace-period', '1') +- topo.standalone.config.set('nsslapd-accesslog-logbuffering', 'off') +- topo.standalone.config.set('nsslapd-disk-monitoring-threshold', ensure_bytes(THRESHOLD_BYTES)) +- TOTAL_SIZE = int(re.findall(r'\d+', str(os.statvfs(topo.standalone.ds_paths.log_dir)))[2])*4096/1024/1024 +- AVAIL_SIZE = round(int(re.findall(r'\d+', str(os.statvfs(topo.standalone.ds_paths.log_dir)))[3]) * 4096 / 1024 / 1024) +- USED_SIZE = TOTAL_SIZE - AVAIL_SIZE +- HALF_THR_FILL_SIZE = TOTAL_SIZE - float(THRESHOLD) + 5 - USED_SIZE +- FULL_THR_FILL_SIZE = TOTAL_SIZE - 0.5 * float(THRESHOLD) + 5 - USED_SIZE +- HALF_THR_FILL_SIZE = round(HALF_THR_FILL_SIZE) +- FULL_THR_FILL_SIZE = round(FULL_THR_FILL_SIZE) +- topo.standalone.restart() +- +- +-@pytest.fixture(scope="module") ++ log.info("Restoring configuration to captured values") ++ ++ for config_attr, original_value in captured_config.items(): ++ if original_value is not None: ++ try: ++ current_value = inst.config.get_attr_val_utf8(config_attr) ++ if current_value != original_value: ++ log.debug(f"Restoring {config_attr} from '{current_value}' to '{original_value}'") ++ inst.config.set(config_attr, ensure_bytes(original_value)) ++ except Exception as e: ++ log.debug(f"Could not restore {config_attr}: {e}") ++ ++ log.info("Configuration restoration completed") ++ ++ ++@pytest.fixture(scope="function") + def setup(request, topo): +- """ +- This is the fixture function , will run before running every test case. +- """ +- presetup(topo) +- setupthesystem(topo) ++ """Module-level fixture to setup the test environment.""" ++ ++ log.info("Starting module setup for disk monitoring tests") ++ inst = topo.standalone ++ ++ # Capture current configuration before making any changes ++ original_config = capture_config(inst) ++ ++ presetup(inst) ++ setupthesystem(inst) + + def fin(): +- topo.standalone.stop() +- subprocess.call(['umount', '-fl', topo.standalone.ds_paths.log_dir]) +- topo.standalone.start() ++ log.info("Running module cleanup for disk monitoring tests") ++ inst.stop() ++ subprocess.call(['umount', '-fl', inst.ds_paths.log_dir]) ++ # Restore configuration to original values ++ inst.start() ++ restore_config(inst, original_config) ++ log.info("Module cleanup completed") + + request.addfinalizer(fin) + + ++def wait_for_condition(inst, condition_str, timeout=30): ++ """Wait until the given condition evaluates to False.""" ++ ++ log.debug(f"Waiting for condition to be False: {condition_str} (timeout: {timeout}s)") ++ start_time = time.time() ++ while time.time() - start_time < timeout: ++ if not eval(condition_str): ++ log.debug(f"Condition satisfied after {time.time() - start_time:.2f}s") ++ return ++ time.sleep(1) ++ raise AssertionError(f"Condition '{condition_str}' still True after {timeout} seconds") ++ ++ ++def wait_for_log_entry(inst, message, timeout=30): ++ """Wait for a specific message to appear in the error log.""" ++ ++ log.debug(f"Waiting for log entry: '{message}' (timeout: {timeout}s)") ++ start_time = time.time() ++ while time.time() - start_time < timeout: ++ with open(inst.errlog, 'r') as log_file: ++ if message in log_file.read(): ++ log.debug(f"Found log entry after {time.time() - start_time:.2f}s") ++ return ++ time.sleep(1) ++ raise AssertionError(f"Message '{message}' not found in error log after {timeout} seconds") ++ ++ ++def get_avail_bytes(path): ++ """Get available bytes on the filesystem at the given path.""" ++ ++ stat = os.statvfs(path) ++ return stat.f_bavail * stat.f_bsize ++ ++ ++def fill_to_target_avail(path, target_avail_bytes): ++ """Fill the disk to reach the target available bytes by creating a large file.""" ++ ++ avail = get_avail_bytes(path) ++ fill_bytes = avail - target_avail_bytes ++ log.debug(f"Current available: {avail}, target: {target_avail_bytes}, will create {fill_bytes} byte file") ++ if fill_bytes <= 0: ++ raise ValueError("Already below target avail") ++ ++ fill_file = os.path.join(path, 'fill.dd') ++ bs = 4096 ++ count = (fill_bytes + bs - 1) // bs # ceil division to ensure enough ++ log.info(f"Creating fill file {fill_file} with {count} blocks of {bs} bytes") ++ subprocess.check_call(['dd', 'if=/dev/zero', f'of={fill_file}', f'bs={bs}', f'count={count}']) ++ return fill_file ++ ++ + @pytest.fixture(scope="function") + def reset_logs(topo): +- """ +- Reset the errors log file before the test +- """ +- open('{}/errors'.format(topo.standalone.ds_paths.log_dir), 'w').close() ++ """Function-level fixture to reset the error log before each test.""" ++ ++ log.debug("Resetting error logs before test") ++ topo.standalone.deleteErrorLogs() ++ ++ ++def generate_access_log_activity(inst, num_users=10, num_binds=100): ++ """Generate access log activity by creating users and performing binds.""" ++ ++ log.info(f"Generating access log activity with {num_users} users and {num_binds} binds each") ++ users = UserAccounts(inst, DEFAULT_SUFFIX) ++ ++ # Create test users ++ for i in range(num_users): ++ user_properties = { ++ 'uid': f'cn=user{i}', ++ 'cn': f'cn=user{i}', ++ 'sn': f'cn=user{i}', ++ 'userPassword': "Itsme123", ++ 'uidNumber': f'1{i}', ++ 'gidNumber': f'2{i}', ++ 'homeDirectory': f'/home/{i}' ++ } ++ users.create(properties=user_properties) ++ ++ # Perform bind operations ++ for j in range(num_binds): ++ for user in users.list(): ++ user.bind('Itsme123') ++ ++ log.info("Access log activity generation completed") ++ return users + + + @disk_monitoring_ack + def test_verify_operation_when_disk_monitoring_is_off(topo, setup, reset_logs): +- """Verify operation when Disk monitoring is off ++ """Verify operation when Disk monitoring is off. + + :id: 73a97536-fe9e-11e8-ba9f-8c16451d917b + :setup: Standalone +@@ -117,94 +244,127 @@ def test_verify_operation_when_disk_monitoring_is_off(topo, setup, reset_logs): + 2. Go below the threshold + 3. Check DS is up and not entering shutdown mode + :expectedresults: +- 1. Should Success +- 2. Should Success +- 3. Should Success ++ 1. Success ++ 2. Success ++ 3. Success + """ ++ log.info("Starting test_verify_operation_when_disk_monitoring_is_off") ++ inst = topo.standalone ++ fill_file = None ++ + try: +- # Turn off disk monitoring +- topo.standalone.config.set('nsslapd-disk-monitoring', 'off') +- topo.standalone.restart() +- # go below the threshold +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(FULL_THR_FILL_SIZE)]) +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo1'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(FULL_THR_FILL_SIZE)]) +- # Wait for disk monitoring plugin thread to wake up +- _withouterrorlog(topo, 'topo.standalone.status() != True', 10) ++ log.info("Disabling disk monitoring") ++ inst.config.set('nsslapd-disk-monitoring', 'off') ++ inst.restart() ++ ++ log.info(f"Filling disk to go below threshold ({THRESHOLD_BYTES} bytes)") ++ fill_file = fill_to_target_avail(inst.ds_paths.log_dir, THRESHOLD_BYTES - 1) ++ ++ log.info("Verifying server stays up despite being below threshold") ++ wait_for_condition(inst, 'inst.status() != True', 11) ++ + # Check DS is up and not entering shutdown mode +- assert topo.standalone.status() == True ++ assert inst.status() == True ++ log.info("Verified: server remains operational when disk monitoring is disabled") ++ + finally: +- os.remove('{}/foo'.format(topo.standalone.ds_paths.log_dir)) +- os.remove('{}/foo1'.format(topo.standalone.ds_paths.log_dir)) ++ if fill_file and os.path.exists(fill_file): ++ log.debug(f"Cleaning up fill file: {fill_file}") ++ os.remove(fill_file) ++ ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_enable_external_libs_debug_log(topo, setup, reset_logs): +- """Check that OpenLDAP logs are successfully enabled and disabled when +- disk threshold is reached ++ """Check that OpenLDAP logs are successfully enabled and disabled when disk threshold is reached. + + :id: 121b2b24-ecba-48e2-9ee2-312d929dc8c6 + :setup: Standalone instance +- :steps: 1. Set nsslapd-external-libs-debug-enabled to "on" +- 2. Go straight below 1/2 of the threshold +- 3. Verify that the external libs debug setting is disabled +- 4. Go back above 1/2 of the threshold +- 5. Verify that the external libs debug setting is enabled back +- :expectedresults: 1. Success +- 2. Success +- 3. Success +- 4. Success +- 5. Success ++ :steps: ++ 1. Set nsslapd-external-libs-debug-enabled to "on" ++ 2. Go straight below 1/2 of the threshold ++ 3. Verify that the external libs debug setting is disabled ++ 4. Go back above 1/2 of the threshold ++ 5. Verify that the external libs debug setting is enabled back ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success ++ 5. Success + """ ++ log.info("Starting test_enable_external_libs_debug_log") ++ inst = topo.standalone ++ fill_file = None ++ + try: +- # Verify that verbose logging was set to default level +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-logging-critical', 'off') +- assert topo.standalone.config.set('nsslapd-external-libs-debug-enabled', 'on') +- assert topo.standalone.config.set('nsslapd-errorlog-level', '8') +- topo.standalone.restart() +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(HALF_THR_FILL_SIZE)]) +- # Verify that logging is disabled +- _withouterrorlog(topo, "topo.standalone.config.get_attr_val_utf8('nsslapd-external-libs-debug-enabled') != 'off'", 31) ++ log.info("Configuring disk monitoring and external libs debug") ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.config.set('nsslapd-disk-monitoring-logging-critical', 'off') ++ inst.config.set('nsslapd-external-libs-debug-enabled', 'on') ++ inst.config.set('nsslapd-errorlog-level', '8') ++ inst.restart() ++ ++ log.info(f"Filling disk to go below half threshold ({THRESHOLD_BYTES // 2} bytes)") ++ fill_file = fill_to_target_avail(inst.ds_paths.log_dir, THRESHOLD_BYTES // 2 - 1) ++ ++ log.info("Verifying external libs debug is automatically disabled") ++ wait_for_condition(inst, "inst.config.get_attr_val_utf8('nsslapd-external-libs-debug-enabled') != 'off'", 31) ++ + finally: +- os.remove('{}/foo'.format(topo.standalone.ds_paths.log_dir)) +- _withouterrorlog(topo, "topo.standalone.config.get_attr_val_utf8('nsslapd-external-libs-debug-enabled') != 'on'", 31) +- assert topo.standalone.config.set('nsslapd-external-libs-debug-enabled', 'off') ++ if fill_file and os.path.exists(fill_file): ++ log.debug(f"Cleaning up fill file: {fill_file}") ++ os.remove(fill_file) ++ ++ log.info("Verifying external libs debug is re-enabled after freeing space") ++ wait_for_condition(inst, "inst.config.get_attr_val_utf8('nsslapd-external-libs-debug-enabled') != 'on'", 31) ++ inst.config.set('nsslapd-external-libs-debug-enabled', 'off') ++ ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_free_up_the_disk_space_and_change_ds_config(topo, setup, reset_logs): +- """Free up the disk space and change DS config ++ """Free up the disk space and change DS config. + + :id: 7be4d560-fe9e-11e8-a307-8c16451d917b + :setup: Standalone + :steps: +- 1. Enabling Disk Monitoring plugin and setting disk monitoring logging to critical ++ 1. Enable Disk Monitoring plugin and set disk monitoring logging to critical + 2. Verify no message about loglevel is present in the error log + 3. Verify no message about disabling logging is present in the error log + 4. Verify no message about removing rotated logs is present in the error log + :expectedresults: +- 1. Should Success +- 2. Should Success +- 3. Should Success +- 4. Should Success ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success + """ +- # Enabling Disk Monitoring plugin and setting disk monitoring logging to critical +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-logging-critical', 'on') +- assert topo.standalone.config.set('nsslapd-errorlog-level', '8') +- topo.standalone.restart() +- # Verify no message about loglevel is present in the error log +- # Verify no message about disabling logging is present in the error log +- # Verify no message about removing rotated logs is present in the error log +- with open(topo.standalone.errlog, 'r') as study: study = study.read() +- assert 'temporarily setting error loglevel to zero' not in study +- assert 'disabling access and audit logging' not in study +- assert 'deleting rotated logs' not in study ++ log.info("Starting test_free_up_the_disk_space_and_change_ds_config") ++ inst = topo.standalone ++ ++ log.info("Enabling disk monitoring with critical logging") ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.config.set('nsslapd-disk-monitoring-logging-critical', 'on') ++ inst.config.set('nsslapd-errorlog-level', '8') ++ inst.restart() ++ ++ log.info("Verifying no premature disk monitoring messages in error log") ++ with open(inst.errlog, 'r') as err_log: ++ content = err_log.read() ++ ++ assert 'temporarily setting error loglevel to zero' not in content ++ assert 'disabling access and audit logging' not in content ++ assert 'deleting rotated logs' not in content ++ ++ log.info("Verified: no unexpected disk monitoring messages found") ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_verify_operation_with_nsslapd_disk_monitoring_logging_critical_off(topo, setup, reset_logs): +- """Verify operation with "nsslapd-disk-monitoring-logging-critical: off ++ """Verify operation with "nsslapd-disk-monitoring-logging-critical: off". + + :id: 82363bca-fe9e-11e8-9ae7-8c16451d917b + :setup: Standalone +@@ -213,39 +373,59 @@ def test_verify_operation_with_nsslapd_disk_monitoring_logging_critical_off(topo + 2. Verify that logging is disabled + 3. Verify that rotated logs were not removed + :expectedresults: +- 1. Should Success +- 2. Should Success +- 3. Should Success ++ 1. Success ++ 2. Success ++ 3. Success + """ ++ log.info("Starting test_verify_operation_with_nsslapd_disk_monitoring_logging_critical_off") ++ inst = topo.standalone ++ fill_file = None ++ + try: +- # Verify that verbose logging was set to default level +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-logging-critical', 'off') +- assert topo.standalone.config.set('nsslapd-errorlog-level', '8') +- topo.standalone.restart() +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(HALF_THR_FILL_SIZE)]) +- _witherrorlog(topo, 'temporarily setting error loglevel to the default level', 11) +- assert LOG_DEFAULT == int(re.findall(r'nsslapd-errorlog-level: \d+', str( +- topo.standalone.search_s('cn=config', ldap.SCOPE_SUBTREE, '(objectclass=*)', ['nsslapd-errorlog-level'])))[ +- 0].split(' ')[1]) +- # Verify that logging is disabled +- _withouterrorlog(topo, "topo.standalone.config.get_attr_val_utf8('nsslapd-accesslog-logging-enabled') != 'off'", 10) +- assert topo.standalone.config.get_attr_val_utf8('nsslapd-accesslog-logging-enabled') == 'off' +- # Verify that rotated logs were not removed +- with open(topo.standalone.errlog, 'r') as study: study = study.read() +- assert 'disabling access and audit logging' in study +- _witherrorlog(topo, 'deleting rotated logs', 11) +- study = open(topo.standalone.errlog).read() +- assert "Unable to remove file: {}".format(topo.standalone.ds_paths.log_dir) not in study +- assert 'is too far below the threshold' not in study ++ log.info("Configuring disk monitoring with critical logging disabled") ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.config.set('nsslapd-disk-monitoring-logging-critical', 'off') ++ inst.config.set('nsslapd-errorlog-level', '8') ++ inst.restart() ++ ++ log.info(f"Filling disk to go below threshold ({THRESHOLD_BYTES} bytes)") ++ fill_file = fill_to_target_avail(inst.ds_paths.log_dir, THRESHOLD_BYTES - 1) ++ ++ log.info("Waiting for loglevel to be set to default") ++ wait_for_log_entry(inst, 'temporarily setting error loglevel to the default level', 11) ++ ++ log.info("Verifying error log level was set to default") ++ config_entry = inst.search_s('cn=config', ldap.SCOPE_SUBTREE, '(objectclass=*)', ['nsslapd-errorlog-level']) ++ current_level = int(re.findall(r'nsslapd-errorlog-level: \d+', str(config_entry))[0].split(' ')[1]) ++ assert LOG_DEFAULT == current_level ++ ++ log.info("Verifying access logging is disabled") ++ wait_for_condition(inst, "inst.config.get_attr_val_utf8('nsslapd-accesslog-logging-enabled') != 'off'", 11) ++ assert inst.config.get_attr_val_utf8('nsslapd-accesslog-logging-enabled') == 'off' ++ ++ log.info("Verifying expected disk monitoring messages") ++ with open(inst.errlog, 'r') as err_log: ++ content = err_log.read() ++ ++ assert 'disabling access and audit logging' in content ++ wait_for_log_entry(inst, 'deleting rotated logs', 11) ++ assert f"Unable to remove file: {inst.ds_paths.log_dir}" not in content ++ assert 'is too far below the threshold' not in content ++ ++ log.info("All verifications passed") ++ + finally: +- os.remove('{}/foo'.format(topo.standalone.ds_paths.log_dir)) ++ if fill_file and os.path.exists(fill_file): ++ log.debug(f"Cleaning up fill file: {fill_file}") ++ os.remove(fill_file) ++ ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_operation_with_nsslapd_disk_monitoring_logging_critical_on_below_half_of_the_threshold(topo, setup, reset_logs): +- """Verify operation with \"nsslapd-disk-monitoring-logging-critical: on\" below 1/2 of the threshold +- Verify recovery ++ """Verify operation with "nsslapd-disk-monitoring-logging-critical: on" below 1/2 of the threshold. ++ Verify recovery. + + :id: 8940c502-fe9e-11e8-bcc0-8c16451d917b + :setup: Standalone +@@ -253,190 +433,277 @@ def test_operation_with_nsslapd_disk_monitoring_logging_critical_on_below_half_o + 1. Verify that DS goes into shutdown mode + 2. Verify that DS exited shutdown mode + :expectedresults: +- 1. Should Success +- 2. Should Success ++ 1. Success ++ 2. Success + """ +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-logging-critical', 'on') +- topo.standalone.restart() +- # Verify that DS goes into shutdown mode +- if float(THRESHOLD) > FULL_THR_FILL_SIZE: +- FULL_THR_FILL_SIZE_new = FULL_THR_FILL_SIZE + round(float(THRESHOLD) - FULL_THR_FILL_SIZE) + 1 +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(FULL_THR_FILL_SIZE_new)]) +- else: +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(FULL_THR_FILL_SIZE)]) +- _witherrorlog(topo, 'is too far below the threshold', 20) +- os.remove('{}/foo'.format(topo.standalone.ds_paths.log_dir)) +- # Verify that DS exited shutdown mode +- _witherrorlog(topo, 'Available disk space is now acceptable', 25) ++ log.info("Starting test_operation_with_nsslapd_disk_monitoring_logging_critical_on_below_half_of_the_threshold") ++ inst = topo.standalone ++ fill_file = None ++ ++ try: ++ log.info("Configuring disk monitoring with critical logging enabled") ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.config.set('nsslapd-disk-monitoring-logging-critical', 'on') ++ inst.restart() ++ ++ log.info(f"Filling disk to go below half threshold ({THRESHOLD_BYTES // 2} bytes)") ++ fill_file = fill_to_target_avail(inst.ds_paths.log_dir, THRESHOLD_BYTES // 2 - 1) ++ ++ log.info("Waiting for shutdown mode message") ++ wait_for_log_entry(inst, 'is too far below the threshold', 100) ++ ++ log.info("Freeing up disk space") ++ os.remove(fill_file) ++ fill_file = None ++ ++ log.info("Waiting for recovery message") ++ wait_for_log_entry(inst, 'Available disk space is now acceptable', 25) ++ ++ log.info("Verified: server entered and exited shutdown mode correctly") ++ ++ finally: ++ if fill_file and os.path.exists(fill_file): ++ log.debug(f"Cleaning up fill file: {fill_file}") ++ os.remove(fill_file) ++ ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_setting_nsslapd_disk_monitoring_logging_critical_to_off(topo, setup, reset_logs): +- """Setting nsslapd-disk-monitoring-logging-critical to "off" ++ """Setting nsslapd-disk-monitoring-logging-critical to "off". + + :id: 93265ec4-fe9e-11e8-af93-8c16451d917b + :setup: Standalone + :steps: +- 1. Setting nsslapd-disk-monitoring-logging-critical to "off" ++ 1. Set nsslapd-disk-monitoring-logging-critical to "off" + :expectedresults: +- 1. Should Success ++ 1. Success + """ +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-logging-critical', 'off') +- assert topo.standalone.config.set('nsslapd-errorlog-level', '8') +- topo.standalone.restart() +- assert topo.standalone.status() == True ++ log.info("Starting test_setting_nsslapd_disk_monitoring_logging_critical_to_off") ++ inst = topo.standalone ++ ++ log.info("Setting disk monitoring configuration") ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.config.set('nsslapd-disk-monitoring-logging-critical', 'off') ++ inst.config.set('nsslapd-errorlog-level', '8') ++ inst.restart() ++ ++ log.info("Verifying server is running normally") ++ assert inst.status() == True ++ ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_operation_with_nsslapd_disk_monitoring_logging_critical_off(topo, setup, reset_logs): +- """Verify operation with nsslapd-disk-monitoring-logging-critical: off ++ """Verify operation with nsslapd-disk-monitoring-logging-critical: off. + + :id: 97985a52-fe9e-11e8-9914-8c16451d917b + :setup: Standalone + :steps: +- 1. Verify that logging is disabled +- 2. Verify that rotated logs were removed ++ 1. Generate access log activity to create rotated logs ++ 2. Go below threshold to trigger disk monitoring + 3. Verify that verbose logging was set to default level + 4. Verify that logging is disabled + 5. Verify that rotated logs were removed + :expectedresults: +- 1. Should Success +- 2. Should Success +- 3. Should Success +- 4. Should Success +- 5. Should Success ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success ++ 5. Success + """ +- # Verify that logging is disabled ++ log.info("Starting test_operation_with_nsslapd_disk_monitoring_logging_critical_off") ++ inst = topo.standalone ++ fill_file = None ++ users = None ++ + try: +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-logging-critical', 'off') +- assert topo.standalone.config.set('nsslapd-errorlog-level', '8') +- assert topo.standalone.config.set('nsslapd-accesslog-maxlogsize', '1') +- assert topo.standalone.config.set('nsslapd-accesslog-logrotationtimeunit', 'minute') +- assert topo.standalone.config.set('nsslapd-accesslog-level', '772') +- topo.standalone.restart() +- # Verify that rotated logs were removed +- users = UserAccounts(topo.standalone, DEFAULT_SUFFIX) +- for i in range(10): +- user_properties = { +- 'uid': 'cn=anuj{}'.format(i), +- 'cn': 'cn=anuj{}'.format(i), +- 'sn': 'cn=anuj{}'.format(i), +- 'userPassword': "Itsme123", +- 'uidNumber': '1{}'.format(i), +- 'gidNumber': '2{}'.format(i), +- 'homeDirectory': '/home/{}'.format(i) +- } +- users.create(properties=user_properties) +- for j in range(100): +- for i in [i for i in users.list()]: i.bind('Itsme123') +- assert re.findall(r'access.\d+-\d+',str(os.listdir(topo.standalone.ds_paths.log_dir))) +- topo.standalone.bind_s(DN_DM, PW_DM) +- assert topo.standalone.config.set('nsslapd-accesslog-maxlogsize', '100') +- assert topo.standalone.config.set('nsslapd-accesslog-logrotationtimeunit', 'day') +- assert topo.standalone.config.set('nsslapd-accesslog-level', '256') +- topo.standalone.restart() +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo2'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(HALF_THR_FILL_SIZE)]) +- # Verify that verbose logging was set to default level +- _witherrorlog(topo, 'temporarily setting error loglevel to the default level', 10) +- assert LOG_DEFAULT == int(re.findall(r'nsslapd-errorlog-level: \d+', str( +- topo.standalone.search_s('cn=config', ldap.SCOPE_SUBTREE, '(objectclass=*)', ['nsslapd-errorlog-level'])))[0].split(' ')[1]) +- # Verify that logging is disabled +- _withouterrorlog(topo, "topo.standalone.config.get_attr_val_utf8('nsslapd-accesslog-logging-enabled') != 'off'", 20) +- with open(topo.standalone.errlog, 'r') as study: study = study.read() +- assert 'disabling access and audit logging' in study +- # Verify that rotated logs were removed +- _witherrorlog(topo, 'deleting rotated logs', 10) +- with open(topo.standalone.errlog, 'r') as study:study = study.read() +- assert 'Unable to remove file:' not in study +- assert 'is too far below the threshold' not in study +- for i in [i for i in users.list()]: i.delete() ++ log.info("Configuring disk monitoring and access log settings") ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.config.set('nsslapd-disk-monitoring-logging-critical', 'off') ++ inst.config.set('nsslapd-errorlog-level', '8') ++ inst.config.set('nsslapd-accesslog-maxlogsize', '1') ++ inst.config.set('nsslapd-accesslog-logrotationtimeunit', 'minute') ++ inst.config.set('nsslapd-accesslog-level', '772') ++ inst.restart() ++ ++ log.info("Generating access log activity to create rotated logs") ++ users = generate_access_log_activity(inst, num_users=10, num_binds=100) ++ ++ inst.bind_s(DN_DM, PW_DM) ++ ++ log.info("Resetting access log settings") ++ inst.config.set('nsslapd-accesslog-maxlogsize', '100') ++ inst.config.set('nsslapd-accesslog-logrotationtimeunit', 'day') ++ inst.config.set('nsslapd-accesslog-level', '256') ++ inst.restart() ++ ++ log.info(f"Filling disk to go below threshold ({THRESHOLD_BYTES} bytes)") ++ fill_file = fill_to_target_avail(inst.ds_paths.log_dir, THRESHOLD_BYTES - 1) ++ ++ log.info("Waiting for loglevel to be set to default") ++ wait_for_log_entry(inst, 'temporarily setting error loglevel to the default level', 11) ++ ++ log.info("Verifying error log level was set to default") ++ config_level = None ++ for _ in range(10): ++ time.sleep(1) ++ config_entry = inst.search_s('cn=config', ldap.SCOPE_SUBTREE, '(objectclass=*)', ['nsslapd-errorlog-level']) ++ config_level = int(re.findall(r'nsslapd-errorlog-level: \d+', str(config_entry))[0].split(' ')[1]) ++ if LOG_DEFAULT == config_level: ++ break ++ assert LOG_DEFAULT == config_level ++ ++ log.info("Verifying access logging is disabled") ++ wait_for_condition(inst, "inst.config.get_attr_val_utf8('nsslapd-accesslog-logging-enabled') == 'off'", 20) ++ ++ with open(inst.errlog, 'r') as err_log: ++ content = err_log.read() ++ assert 'disabling access and audit logging' in content ++ ++ log.info("Verifying rotated logs are removed") ++ wait_for_log_entry(inst, 'deleting rotated logs', 20) ++ ++ rotated_logs = re.findall(r'access.\d+-\d+', str(os.listdir(inst.ds_paths.log_dir))) ++ assert not rotated_logs, f"Found unexpected rotated logs: {rotated_logs}" ++ ++ with open(inst.errlog, 'r') as err_log: ++ content = err_log.read() ++ assert 'Unable to remove file:' not in content ++ assert 'is too far below the threshold' not in content ++ ++ log.info("All verifications passed") ++ + finally: +- os.remove('{}/foo2'.format(topo.standalone.ds_paths.log_dir)) ++ # Clean up users ++ if users: ++ log.debug("Cleaning up test users") ++ for user in users.list(): ++ try: ++ user.delete() ++ except ldap.ALREADY_EXISTS: ++ pass ++ ++ if fill_file and os.path.exists(fill_file): ++ log.debug(f"Cleaning up fill file: {fill_file}") ++ os.remove(fill_file) ++ ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_operation_with_nsslapd_disk_monitoring_logging_critical_off_below_half_of_the_threshold(topo, setup, reset_logs): +- """Verify operation with nsslapd-disk-monitoring-logging-critical: off below 1/2 of the threshold +- Verify shutdown +- Recovery and setup ++ """Verify operation with nsslapd-disk-monitoring-logging-critical: off below 1/2 of the threshold. ++ Verify shutdown and recovery. + + :id: 9d4c7d48-fe9e-11e8-b5d6-8c16451d917b + :setup: Standalone + :steps: +- 1. Verify that DS goes into shutdown mode +- 2. Verifying that DS has been shut down after the grace period +- 3. Verify logging enabled +- 4. Create rotated logfile +- 5. Enable verbose logging ++ 1. Go below half threshold to trigger shutdown ++ 2. Verify DS shutdown after grace period ++ 3. Free space and restart ++ 4. Verify logging is re-enabled ++ 5. Create rotated logs and enable verbose logging + :expectedresults: +- 1. Should Success +- 2. Should Success +- 3. Should Success +- 4. Should Success +- 5. Should Success ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success ++ 5. Success + """ +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-logging-critical', 'off') +- topo.standalone.restart() +- # Verify that DS goes into shutdown mode +- if float(THRESHOLD) > FULL_THR_FILL_SIZE: +- FULL_THR_FILL_SIZE_new = FULL_THR_FILL_SIZE + round(float(THRESHOLD) - FULL_THR_FILL_SIZE) +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(FULL_THR_FILL_SIZE_new)]) +- else: +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(FULL_THR_FILL_SIZE)]) +- # Increased sleep to avoid failure +- _witherrorlog(topo, 'is too far below the threshold', 100) +- _witherrorlog(topo, 'Signaling slapd for shutdown', 90) +- # Verifying that DS has been shut down after the grace period +- time.sleep(2) +- assert topo.standalone.status() == False +- # free_space +- os.remove('{}/foo'.format(topo.standalone.ds_paths.log_dir)) +- open('{}/errors'.format(topo.standalone.ds_paths.log_dir), 'w').close() +- # StartSlapd +- topo.standalone.start() +- # verify logging enabled +- assert topo.standalone.config.get_attr_val_utf8('nsslapd-accesslog-logging-enabled') == 'on' +- assert topo.standalone.config.get_attr_val_utf8('nsslapd-errorlog-logging-enabled') == 'on' +- with open(topo.standalone.errlog, 'r') as study: study = study.read() +- assert 'disabling access and audit logging' not in study +- assert topo.standalone.config.set('nsslapd-accesslog-maxlogsize', '1') +- assert topo.standalone.config.set('nsslapd-accesslog-logrotationtimeunit', 'minute') +- assert topo.standalone.config.set('nsslapd-accesslog-level', '772') +- topo.standalone.restart() +- # create rotated logfile +- users = UserAccounts(topo.standalone, DEFAULT_SUFFIX) +- for i in range(10): +- user_properties = { +- 'uid': 'cn=anuj{}'.format(i), +- 'cn': 'cn=anuj{}'.format(i), +- 'sn': 'cn=anuj{}'.format(i), +- 'userPassword': "Itsme123", +- 'uidNumber': '1{}'.format(i), +- 'gidNumber': '2{}'.format(i), +- 'homeDirectory': '/home/{}'.format(i) +- } +- users.create(properties=user_properties) +- for j in range(100): +- for i in [i for i in users.list()]: i.bind('Itsme123') +- assert re.findall(r'access.\d+-\d+',str(os.listdir(topo.standalone.ds_paths.log_dir))) +- topo.standalone.bind_s(DN_DM, PW_DM) +- # enable verbose logging +- assert topo.standalone.config.set('nsslapd-accesslog-maxlogsize', '100') +- assert topo.standalone.config.set('nsslapd-accesslog-logrotationtimeunit', 'day') +- assert topo.standalone.config.set('nsslapd-accesslog-level', '256') +- assert topo.standalone.config.set('nsslapd-errorlog-level', '8') +- topo.standalone.restart() +- for i in [i for i in users.list()]: i.delete() ++ log.info("Starting test_operation_with_nsslapd_disk_monitoring_logging_critical_off_below_half_of_the_threshold") ++ inst = topo.standalone ++ fill_file = None ++ users = None ++ ++ try: ++ log.info("Configuring disk monitoring with critical logging disabled") ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.config.set('nsslapd-disk-monitoring-logging-critical', 'off') ++ inst.restart() ++ ++ log.info(f"Filling disk to go below half threshold ({THRESHOLD_BYTES // 2} bytes)") ++ fill_file = fill_to_target_avail(inst.ds_paths.log_dir, THRESHOLD_BYTES // 2 - 1) ++ ++ log.info("Waiting for shutdown messages") ++ wait_for_log_entry(inst, 'is too far below the threshold', 100) ++ wait_for_log_entry(inst, 'Signaling slapd for shutdown', 90) ++ ++ log.info("Verifying server shutdown within grace period") ++ for i in range(60): ++ time.sleep(1) ++ if not inst.status(): ++ log.info(f"Server shut down after {i+1} seconds") ++ break ++ assert inst.status() == False ++ ++ log.info("Freeing disk space and cleaning logs") ++ os.remove(fill_file) ++ fill_file = None ++ open(f'{inst.ds_paths.log_dir}/errors', 'w').close() ++ ++ log.info("Starting server after freeing space") ++ inst.start() ++ ++ log.info("Verifying logging is re-enabled") ++ assert inst.config.get_attr_val_utf8('nsslapd-accesslog-logging-enabled') == 'on' ++ assert inst.config.get_attr_val_utf8('nsslapd-errorlog-logging-enabled') == 'on' ++ ++ with open(inst.errlog, 'r') as err_log: ++ content = err_log.read() ++ assert 'disabling access and audit logging' not in content ++ ++ log.info("Setting up access log rotation for testing") ++ inst.config.set('nsslapd-accesslog-maxlogsize', '1') ++ inst.config.set('nsslapd-accesslog-logrotationtimeunit', 'minute') ++ inst.config.set('nsslapd-accesslog-level', '772') ++ inst.restart() ++ ++ log.info("Creating rotated log files through user activity") ++ users = generate_access_log_activity(inst, num_users=10, num_binds=100) ++ ++ log.info("Waiting for log rotation to occur") ++ for i in range(61): ++ time.sleep(1) ++ rotated_logs = re.findall(r'access.\d+-\d+', str(os.listdir(inst.ds_paths.log_dir))) ++ if rotated_logs: ++ log.info(f"Log rotation detected after {i+1} seconds") ++ break ++ assert rotated_logs, "No rotated logs found after waiting" ++ ++ inst.bind_s(DN_DM, PW_DM) ++ ++ log.info("Enabling verbose logging") ++ inst.config.set('nsslapd-accesslog-maxlogsize', '100') ++ inst.config.set('nsslapd-accesslog-logrotationtimeunit', 'day') ++ inst.config.set('nsslapd-accesslog-level', '256') ++ inst.config.set('nsslapd-errorlog-level', '8') ++ inst.restart() ++ ++ log.info("Recovery and setup verification completed") ++ ++ finally: ++ # Clean up users ++ if users: ++ log.debug("Cleaning up test users") ++ for user in users.list(): ++ try: ++ user.delete() ++ except ldap.ALREADY_EXISTS: ++ pass ++ ++ if fill_file and os.path.exists(fill_file): ++ log.debug(f"Cleaning up fill file: {fill_file}") ++ os.remove(fill_file) ++ ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_go_straight_below_half_of_the_threshold(topo, setup, reset_logs): +- """Go straight below 1/2 of the threshold +- Recovery and setup ++ """Go straight below 1/2 of the threshold and verify recovery. + + :id: a2a0664c-fe9e-11e8-b220-8c16451d917b + :setup: Standalone +@@ -447,252 +714,417 @@ def test_go_straight_below_half_of_the_threshold(topo, setup, reset_logs): + 4. Verify DS is in shutdown mode + 5. Verify DS has recovered from shutdown + :expectedresults: +- 1. Should Success +- 2. Should Success +- 3. Should Success +- 4. Should Success +- 5. Should Success ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success ++ 5. Success + """ +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-logging-critical', 'off') +- assert topo.standalone.config.set('nsslapd-errorlog-level', '8') +- topo.standalone.restart() +- if float(THRESHOLD) > FULL_THR_FILL_SIZE: +- FULL_THR_FILL_SIZE_new = FULL_THR_FILL_SIZE + round(float(THRESHOLD) - FULL_THR_FILL_SIZE) + 1 +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(FULL_THR_FILL_SIZE_new)]) +- else: +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(FULL_THR_FILL_SIZE)]) +- _witherrorlog(topo, 'temporarily setting error loglevel to the default level', 11) +- # Verify that verbose logging was set to default level +- assert LOG_DEFAULT == int(re.findall(r'nsslapd-errorlog-level: \d+', +- str(topo.standalone.search_s('cn=config', ldap.SCOPE_SUBTREE, +- '(objectclass=*)', +- ['nsslapd-errorlog-level'])) +- )[0].split(' ')[1]) +- # Verify that logging is disabled +- _withouterrorlog(topo, "topo.standalone.config.get_attr_val_utf8('nsslapd-accesslog-logging-enabled') != 'off'", 11) +- # Verify that rotated logs were removed +- _witherrorlog(topo, 'disabling access and audit logging', 2) +- _witherrorlog(topo, 'deleting rotated logs', 11) +- with open(topo.standalone.errlog, 'r') as study:study = study.read() +- assert 'Unable to remove file:' not in study +- # Verify DS is in shutdown mode +- _withouterrorlog(topo, 'topo.standalone.status() != False', 90) +- _witherrorlog(topo, 'is too far below the threshold', 2) +- # Verify DS has recovered from shutdown +- os.remove('{}/foo'.format(topo.standalone.ds_paths.log_dir)) +- open('{}/errors'.format(topo.standalone.ds_paths.log_dir), 'w').close() +- topo.standalone.start() +- _withouterrorlog(topo, "topo.standalone.config.get_attr_val_utf8('nsslapd-accesslog-logging-enabled') != 'on'", 20) +- with open(topo.standalone.errlog, 'r') as study: study = study.read() +- assert 'disabling access and audit logging' not in study ++ log.info("Starting test_go_straight_below_half_of_the_threshold") ++ inst = topo.standalone ++ fill_file = None ++ ++ try: ++ log.info("Configuring disk monitoring with critical logging disabled") ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.config.set('nsslapd-disk-monitoring-logging-critical', 'off') ++ inst.config.set('nsslapd-errorlog-level', '8') ++ inst.restart() ++ ++ # Go straight below half threshold ++ log.info(f"Filling disk to go below half threshold ({THRESHOLD_BYTES // 2} bytes)") ++ fill_file = fill_to_target_avail(inst.ds_paths.log_dir, THRESHOLD_BYTES // 2 - 1) ++ ++ # Verify that verbose logging was set to default level ++ log.info("Waiting for loglevel to be set to default") ++ wait_for_log_entry(inst, 'temporarily setting error loglevel to the default level', 11) ++ ++ log.info("Verifying error log level was set to default") ++ config_entry = inst.search_s('cn=config', ldap.SCOPE_SUBTREE, '(objectclass=*)', ['nsslapd-errorlog-level']) ++ current_level = int(re.findall(r'nsslapd-errorlog-level: \d+', str(config_entry))[0].split(' ')[1]) ++ assert LOG_DEFAULT == current_level ++ ++ log.info("Verifying access logging is disabled") ++ wait_for_condition(inst, "inst.config.get_attr_val_utf8('nsslapd-accesslog-logging-enabled') != 'off'", 11) ++ ++ log.info("Verifying expected disk monitoring messages") ++ wait_for_log_entry(inst, 'disabling access and audit logging', 2) ++ wait_for_log_entry(inst, 'deleting rotated logs', 11) ++ ++ with open(inst.errlog, 'r') as err_log: ++ content = err_log.read() ++ assert 'Unable to remove file:' not in content ++ ++ log.info("Verifying server enters shutdown mode") ++ wait_for_condition(inst, 'inst.status() != False', 90) ++ wait_for_log_entry(inst, 'is too far below the threshold', 2) ++ ++ log.info("Freeing disk space and restarting server") ++ os.remove(fill_file) ++ fill_file = None ++ open(f'{inst.ds_paths.log_dir}/errors', 'w').close() ++ inst.start() ++ ++ log.info("Verifying server recovery") ++ wait_for_condition(inst, "inst.config.get_attr_val_utf8('nsslapd-accesslog-logging-enabled') != 'on'", 20) ++ ++ with open(inst.errlog, 'r') as err_log: ++ content = err_log.read() ++ assert 'disabling access and audit logging' not in content ++ ++ log.info("Recovery verification completed") ++ ++ finally: ++ if fill_file and os.path.exists(fill_file): ++ log.debug(f"Cleaning up fill file: {fill_file}") ++ os.remove(fill_file) ++ ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_readonly_on_threshold(topo, setup, reset_logs): +- """Verify that nsslapd-disk-monitoring-readonly-on-threshold switches the server to read-only mode ++ """Verify that nsslapd-disk-monitoring-readonly-on-threshold switches the server to read-only mode. + + :id: 06814c19-ef3c-4800-93c9-c7c6e76fcbb9 + :customerscenario: True + :setup: Standalone + :steps: +- 1. Verify that the backend is in read-only mode +- 2. Go back above the threshold +- 3. Verify that the backend is in read-write mode ++ 1. Configure readonly on threshold ++ 2. Go below threshold and verify backend is read-only ++ 3. Go back above threshold and verify backend is read-write + :expectedresults: +- 1. Should Success +- 2. Should Success +- 3. Should Success ++ 1. Success ++ 2. Success ++ 3. Success + """ +- file_path = '{}/foo'.format(topo.standalone.ds_paths.log_dir) +- backends = Backends(topo.standalone) +- backend_name = backends.list()[0].rdn +- # Verify that verbose logging was set to default level +- topo.standalone.deleteErrorLogs() +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-readonly-on-threshold', 'on') +- topo.standalone.restart() ++ log.info("Starting test_readonly_on_threshold") ++ inst = topo.standalone ++ fill_file = None ++ test_user = None ++ + try: +- subprocess.call(['dd', 'if=/dev/zero', f'of={file_path}', 'bs=1M', f'count={HALF_THR_FILL_SIZE}']) +- _witherrorlog(topo, f"Putting the backend '{backend_name}' to read-only mode", 11) ++ backends = Backends(inst) ++ backend_name = backends.list()[0].rdn ++ log.info(f"Testing with backend: {backend_name}") ++ ++ log.info("Configuring disk monitoring with readonly on threshold") ++ inst.deleteErrorLogs() ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.config.set('nsslapd-disk-monitoring-readonly-on-threshold', 'on') ++ inst.restart() ++ ++ log.info(f"Filling disk to go below threshold ({THRESHOLD_BYTES} bytes)") ++ fill_file = fill_to_target_avail(inst.ds_paths.log_dir, THRESHOLD_BYTES - 1) ++ ++ log.info("Waiting for backend to enter read-only mode") ++ wait_for_log_entry(inst, f"Putting the backend '{backend_name}' to read-only mode", 11) ++ ++ log.info("Verifying backend is in read-only mode") + users = UserAccounts(topo.standalone, DEFAULT_SUFFIX) + try: +- user = users.create_test_user() +- user.delete() ++ test_user = users.create_test_user() ++ test_user.delete() ++ assert False, "Expected UNWILLING_TO_PERFORM error for read-only mode" + except ldap.UNWILLING_TO_PERFORM as e: + if 'database is read-only' not in str(e): + raise +- os.remove(file_path) +- _witherrorlog(topo, f"Putting the backend '{backend_name}' back to read-write mode", 11) +- user = users.create_test_user() +- assert user.exists() +- user.delete() ++ log.info("Confirmed: backend correctly rejects writes in read-only mode") ++ ++ log.info("Freeing disk space") ++ os.remove(fill_file) ++ fill_file = None ++ ++ log.info("Waiting for backend to return to read-write mode") ++ wait_for_log_entry(inst, f"Putting the backend '{backend_name}' back to read-write mode", 11) ++ ++ log.info("Verifying backend is in read-write mode") ++ test_user = users.create_test_user() ++ assert test_user.exists() ++ test_user.delete() ++ test_user = None ++ ++ log.info("Confirmed: backend correctly accepts writes in read-write mode") ++ + finally: +- if os.path.exists(file_path): +- os.remove(file_path) ++ if test_user: ++ try: ++ test_user.delete() ++ except: ++ pass ++ ++ if fill_file and os.path.exists(fill_file): ++ log.debug(f"Cleaning up fill file: {fill_file}") ++ os.remove(fill_file) ++ ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_readonly_on_threshold_below_half_of_the_threshold(topo, setup, reset_logs): +- """Go below 1/2 of the threshold when readonly on threshold is enabled ++ """Go below 1/2 of the threshold when readonly on threshold is enabled. + + :id: 10262663-b41f-420e-a2d0-9532dd54fa7c + :customerscenario: True + :setup: Standalone + :steps: +- 1. Go straight below 1/2 of the threshold +- 2. Verify that the backend is in read-only mode +- 3. Go back above the threshold +- 4. Verify that the backend is in read-write mode ++ 1. Configure readonly on threshold ++ 2. Go below half threshold ++ 3. Verify backend is read-only and shutdown messages appear ++ 4. Free space and verify backend returns to read-write + :expectedresults: +- 1. Should Success +- 2. Should Success +- 3. Should Success +- 4. Should Success ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success + """ +- file_path = '{}/foo'.format(topo.standalone.ds_paths.log_dir) +- backends = Backends(topo.standalone) +- backend_name = backends.list()[0].rdn +- topo.standalone.deleteErrorLogs() +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-readonly-on-threshold', 'on') +- topo.standalone.restart() ++ log.info("Starting test_readonly_on_threshold_below_half_of_the_threshold") ++ inst = topo.standalone ++ fill_file = None ++ test_user = None ++ + try: +- if float(THRESHOLD) > FULL_THR_FILL_SIZE: +- FULL_THR_FILL_SIZE_new = FULL_THR_FILL_SIZE + round(float(THRESHOLD) - FULL_THR_FILL_SIZE) + 1 +- subprocess.call(['dd', 'if=/dev/zero', f'of={file_path}', 'bs=1M', f'count={FULL_THR_FILL_SIZE_new}']) +- else: +- subprocess.call(['dd', 'if=/dev/zero', f'of={file_path}', 'bs=1M', f'count={FULL_THR_FILL_SIZE}']) +- _witherrorlog(topo, f"Putting the backend '{backend_name}' to read-only mode", 11) +- users = UserAccounts(topo.standalone, DEFAULT_SUFFIX) ++ backends = Backends(inst) ++ backend_name = backends.list()[0].rdn ++ log.info(f"Testing with backend: {backend_name}") ++ ++ log.info("Configuring disk monitoring with readonly on threshold") ++ inst.deleteErrorLogs() ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.config.set('nsslapd-disk-monitoring-readonly-on-threshold', 'on') ++ inst.restart() ++ ++ log.info(f"Filling disk to go below half threshold ({THRESHOLD_BYTES // 2} bytes)") ++ fill_file = fill_to_target_avail(inst.ds_paths.log_dir, THRESHOLD_BYTES // 2 - 1) ++ ++ log.info("Waiting for backend to enter read-only mode") ++ wait_for_log_entry(inst, f"Putting the backend '{backend_name}' to read-only mode", 11) ++ ++ log.info("Verifying backend is in read-only mode") ++ users = UserAccounts(inst, DEFAULT_SUFFIX) + try: +- user = users.create_test_user() +- user.delete() ++ test_user = users.create_test_user() ++ test_user.delete() ++ assert False, "Expected UNWILLING_TO_PERFORM error for read-only mode" + except ldap.UNWILLING_TO_PERFORM as e: + if 'database is read-only' not in str(e): + raise +- _witherrorlog(topo, 'is too far below the threshold', 51) +- # Verify DS has recovered from shutdown +- os.remove(file_path) +- _witherrorlog(topo, f"Putting the backend '{backend_name}' back to read-write mode", 51) +- user = users.create_test_user() +- assert user.exists() +- user.delete() ++ log.info("Confirmed: backend correctly rejects writes in read-only mode") ++ ++ log.info("Waiting for shutdown threshold message") ++ wait_for_log_entry(inst, 'is too far below the threshold', 51) ++ ++ log.info("Freeing disk space") ++ os.remove(fill_file) ++ fill_file = None ++ ++ log.info("Waiting for backend to return to read-write mode") ++ wait_for_log_entry(inst, f"Putting the backend '{backend_name}' back to read-write mode", 51) ++ ++ log.info("Verifying backend is in read-write mode") ++ test_user = users.create_test_user() ++ assert test_user.exists() ++ test_user.delete() ++ test_user = None ++ ++ log.info("Confirmed: backend correctly accepts writes in read-write mode") ++ + finally: +- if os.path.exists(file_path): +- os.remove(file_path) ++ if test_user: ++ try: ++ test_user.delete() ++ except: ++ pass ++ ++ if fill_file and os.path.exists(fill_file): ++ log.debug(f"Cleaning up fill file: {fill_file}") ++ os.remove(fill_file) ++ ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_below_half_of_the_threshold_not_starting_after_shutdown(topo, setup, reset_logs): +- """Test that the instance won't start if we are below 1/2 of the threshold ++ """Test that the instance won't start if we are below 1/2 of the threshold. + + :id: cceeaefd-9fa4-45c5-9ac6-9887a0671ef8 + :customerscenario: True + :setup: Standalone + :steps: +- 1. Go straight below 1/2 of the threshold +- 2. Try to start the instance +- 3. Go back above the threshold +- 4. Try to start the instance ++ 1. Go below half threshold and wait for shutdown ++ 2. Try to start the instance and verify it fails ++ 3. Free space and verify instance starts successfully + :expectedresults: +- 1. Should Success +- 2. Should Fail +- 3. Should Success +- 4. Should Success ++ 1. Success ++ 2. Startup fails as expected ++ 3. Success + """ +- file_path = '{}/foo'.format(topo.standalone.ds_paths.log_dir) +- topo.standalone.deleteErrorLogs() +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- topo.standalone.restart() ++ log.info("Starting test_below_half_of_the_threshold_not_starting_after_shutdown") ++ inst = topo.standalone ++ fill_file = None ++ + try: +- if float(THRESHOLD) > FULL_THR_FILL_SIZE: +- FULL_THR_FILL_SIZE_new = FULL_THR_FILL_SIZE + round(float(THRESHOLD) - FULL_THR_FILL_SIZE) + 1 +- subprocess.call(['dd', 'if=/dev/zero', f'of={file_path}', 'bs=1M', f'count={FULL_THR_FILL_SIZE_new}']) +- else: +- subprocess.call(['dd', 'if=/dev/zero', f'of={file_path}', 'bs=1M', f'count={FULL_THR_FILL_SIZE}']) +- _withouterrorlog(topo, 'topo.standalone.status() == True', 120) ++ log.info("Configuring disk monitoring") ++ inst.deleteErrorLogs() ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.restart() ++ ++ log.info(f"Filling disk to go below half threshold ({THRESHOLD_BYTES // 2} bytes)") ++ fill_file = fill_to_target_avail(inst.ds_paths.log_dir, THRESHOLD_BYTES // 2 - 1) ++ ++ log.info("Waiting for server to shut down due to disk space") ++ wait_for_condition(inst, 'inst.status() == True', 120) ++ ++ log.info("Attempting to start instance (should fail)") + try: +- topo.standalone.start() ++ inst.start() ++ assert False, "Instance startup should have failed due to low disk space" + except (ValueError, subprocess.CalledProcessError): +- topo.standalone.log.info("Instance start up has failed as expected") +- _witherrorlog(topo, f'is too far below the threshold({THRESHOLD_BYTES} bytes). Exiting now', 2) +- # Verify DS has recovered from shutdown +- os.remove(file_path) +- topo.standalone.start() ++ log.info("Instance startup failed as expected due to low disk space") ++ ++ wait_for_log_entry(inst, f'is too far below the threshold({THRESHOLD_BYTES} bytes). Exiting now', 2) ++ ++ log.info("Freeing disk space") ++ os.remove(fill_file) ++ fill_file = None ++ ++ log.info("Starting instance after freeing space") ++ inst.start() ++ assert inst.status() == True ++ log.info("Instance started successfully after freeing space") ++ + finally: +- if os.path.exists(file_path): +- os.remove(file_path) ++ if fill_file and os.path.exists(fill_file): ++ log.debug(f"Cleaning up fill file: {fill_file}") ++ os.remove(fill_file) ++ ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_go_straight_below_4kb(topo, setup, reset_logs): +- """Go straight below 4KB ++ """Go straight below 4KB and verify behavior. + + :id: a855115a-fe9e-11e8-8e91-8c16451d917b + :setup: Standalone + :steps: + 1. Go straight below 4KB +- 2. Clean space ++ 2. Verify server behavior ++ 3. Clean space and restart + :expectedresults: +- 1. Should Success +- 2. Should Success ++ 1. Success ++ 2. Success ++ 3. Success + """ +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- topo.standalone.restart() +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(FULL_THR_FILL_SIZE)]) +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo1'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(FULL_THR_FILL_SIZE)]) +- _withouterrorlog(topo, 'topo.standalone.status() != False', 11) +- os.remove('{}/foo'.format(topo.standalone.ds_paths.log_dir)) +- os.remove('{}/foo1'.format(topo.standalone.ds_paths.log_dir)) +- topo.standalone.start() +- assert topo.standalone.status() == True ++ log.info("Starting test_go_straight_below_4kb") ++ inst = topo.standalone ++ fill_file = None ++ ++ try: ++ log.info("Configuring disk monitoring") ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.restart() ++ ++ log.info("Filling disk to go below 4KB") ++ fill_file = fill_to_target_avail(inst.ds_paths.log_dir, 4000) ++ ++ log.info("Waiting for server shutdown due to extreme low disk space") ++ wait_for_condition(inst, 'inst.status() != False', 11) ++ ++ log.info("Freeing disk space and restarting") ++ os.remove(fill_file) ++ fill_file = None ++ inst.start() ++ ++ assert inst.status() == True ++ log.info("Server restarted successfully after freeing space") ++ ++ finally: ++ if fill_file and os.path.exists(fill_file): ++ log.debug(f"Cleaning up fill file: {fill_file}") ++ os.remove(fill_file) ++ ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + @pytest.mark.bz982325 + def test_threshold_to_overflow_value(topo, setup, reset_logs): +- """Overflow in nsslapd-disk-monitoring-threshold ++ """Test overflow in nsslapd-disk-monitoring-threshold. + + :id: ad60ab3c-fe9e-11e8-88dc-8c16451d917b + :setup: Standalone + :steps: +- 1. Setting nsslapd-disk-monitoring-threshold to overflow_value ++ 1. Set nsslapd-disk-monitoring-threshold to overflow value ++ 2. Verify the value is set correctly + :expectedresults: +- 1. Should Success ++ 1. Success ++ 2. Success + """ ++ log.info("Starting test_threshold_to_overflow_value") ++ inst = topo.standalone ++ + overflow_value = '3000000000' +- # Setting nsslapd-disk-monitoring-threshold to overflow_value +- assert topo.standalone.config.set('nsslapd-disk-monitoring-threshold', ensure_bytes(overflow_value)) +- assert overflow_value == re.findall(r'nsslapd-disk-monitoring-threshold: \d+', str( +- topo.standalone.search_s('cn=config', ldap.SCOPE_SUBTREE, '(objectclass=*)', +- ['nsslapd-disk-monitoring-threshold'])))[0].split(' ')[1] ++ log.info(f"Setting threshold to overflow value: {overflow_value}") ++ ++ inst.config.set('nsslapd-disk-monitoring-threshold', ensure_bytes(overflow_value)) ++ ++ config_entry = inst.search_s('cn=config', ldap.SCOPE_SUBTREE, '(objectclass=*)', ['nsslapd-disk-monitoring-threshold']) ++ current_value = re.findall(r'nsslapd-disk-monitoring-threshold: \d+', str(config_entry))[0].split(' ')[1] ++ assert overflow_value == current_value ++ ++ log.info(f"Verified: threshold value set to {current_value}") ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + @pytest.mark.bz970995 + def test_threshold_is_reached_to_half(topo, setup, reset_logs): +- """RHDS not shutting down when disk monitoring threshold is reached to half. ++ """Verify RHDS not shutting down when disk monitoring threshold is reached to half. + + :id: b2d3665e-fe9e-11e8-b9c0-8c16451d917b + :setup: Standalone +- :steps: Standalone +- 1. Verify that there is not endless loop of error messages ++ :steps: ++ 1. Configure disk monitoring with critical logging ++ 2. Go below threshold ++ 3. Verify there is no endless loop of error messages + :expectedresults: +- 1. Should Success ++ 1. Success ++ 2. Success ++ 3. Success + """ ++ log.info("Starting test_threshold_is_reached_to_half") ++ inst = topo.standalone ++ fill_file = None ++ ++ try: ++ log.info("Configuring disk monitoring with critical logging enabled") ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.config.set('nsslapd-disk-monitoring-logging-critical', 'on') ++ inst.config.set('nsslapd-errorlog-level', '8') ++ inst.config.set('nsslapd-disk-monitoring-threshold', ensure_bytes(str(THRESHOLD_BYTES))) ++ inst.restart() ++ ++ log.info(f"Filling disk to go below threshold ({THRESHOLD_BYTES} bytes)") ++ fill_file = fill_to_target_avail(inst.ds_paths.log_dir, THRESHOLD_BYTES // 2 - 1) ++ ++ log.info("Waiting for loglevel message and verifying it's not repeated") ++ wait_for_log_entry(inst, "temporarily setting error loglevel to the default level", 11) ++ ++ with open(inst.errlog, 'r') as err_log: ++ content = err_log.read() ++ ++ message_count = len(re.findall("temporarily setting error loglevel to the default level", content)) ++ assert message_count == 1, f"Expected 1 occurrence of message, found {message_count}" ++ ++ log.info("Verified: no endless loop of error messages") ++ ++ finally: ++ if fill_file and os.path.exists(fill_file): ++ log.debug(f"Cleaning up fill file: {fill_file}") ++ os.remove(fill_file) + +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-logging-critical', 'on') +- assert topo.standalone.config.set('nsslapd-errorlog-level', '8') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-threshold', ensure_bytes(THRESHOLD_BYTES)) +- topo.standalone.restart() +- subprocess.call(['dd', 'if=/dev/zero', 'of={}/foo'.format(topo.standalone.ds_paths.log_dir), 'bs=1M', 'count={}'.format(HALF_THR_FILL_SIZE)]) +- # Verify that there is not endless loop of error messages +- _witherrorlog(topo, "temporarily setting error loglevel to the default level", 10) +- with open(topo.standalone.errlog, 'r') as study:study = study.read() +- assert len(re.findall("temporarily setting error loglevel to the default level", study)) == 1 +- os.remove('{}/foo'.format(topo.standalone.ds_paths.log_dir)) ++ log.info("Test completed successfully") + + + @disk_monitoring_ack +@@ -713,58 +1145,77 @@ def test_threshold_is_reached_to_half(topo, setup, reset_logs): + ("nsslapd-disk-monitoring-grace-period", '0'), + ]) + def test_negagtive_parameterize(topo, setup, reset_logs, test_input, expected): +- """Verify that invalid operations are not permitted ++ """Verify that invalid operations are not permitted. + + :id: b88efbf8-fe9e-11e8-8499-8c16451d917b + :parametrized: yes + :setup: Standalone + :steps: +- 1. Verify that invalid operations are not permitted. ++ 1. Try to set invalid configuration values + :expectedresults: +- 1. Should not success. ++ 1. Configuration change should fail + """ ++ log.info(f"Starting test_negagtive_parameterize for {test_input}={expected}") ++ inst = topo.standalone ++ ++ log.info(f"Attempting to set invalid value: {test_input}={expected}") + with pytest.raises(Exception): +- topo.standalone.config.set(test_input, ensure_bytes(expected)) ++ inst.config.set(test_input, ensure_bytes(expected)) ++ ++ log.info("Verified: invalid configuration value was rejected") ++ log.info("Test completed successfully") + + + @disk_monitoring_ack + def test_valid_operations_are_permitted(topo, setup, reset_logs): +- """Verify that valid operations are permitted ++ """Verify that valid operations are permitted. + + :id: bd4f83f6-fe9e-11e8-88f4-8c16451d917b + :setup: Standalone + :steps: +- 1. Verify that valid operations are permitted ++ 1. Perform various valid configuration operations + :expectedresults: +- 1. Should Success. ++ 1. All operations should succeed + """ +- assert topo.standalone.config.set('nsslapd-disk-monitoring', 'on') +- assert topo.standalone.config.set('nsslapd-disk-monitoring-logging-critical', 'on') +- assert topo.standalone.config.set('nsslapd-errorlog-level', '8') +- topo.standalone.restart() +- # Trying to delete nsslapd-disk-monitoring-threshold +- assert topo.standalone.modify_s('cn=config', [(ldap.MOD_DELETE, 'nsslapd-disk-monitoring-threshold', '')]) +- # Trying to add another value to nsslapd-disk-monitoring-threshold (check that it is not multivalued) +- topo.standalone.config.add('nsslapd-disk-monitoring-threshold', '2000001') +- # Trying to delete nsslapd-disk-monitoring +- assert topo.standalone.modify_s('cn=config', [(ldap.MOD_DELETE, 'nsslapd-disk-monitoring', ensure_bytes(str( +- topo.standalone.search_s('cn=config', ldap.SCOPE_SUBTREE, '(objectclass=*)', ['nsslapd-disk-monitoring'])[ +- 0]).split(' ')[2].split('\n\n')[0]))]) +- # Trying to add another value to nsslapd-disk-monitoring +- topo.standalone.config.add('nsslapd-disk-monitoring', 'off') +- # Trying to delete nsslapd-disk-monitoring-grace-period +- assert topo.standalone.modify_s('cn=config', [(ldap.MOD_DELETE, 'nsslapd-disk-monitoring-grace-period', '')]) +- # Trying to add another value to nsslapd-disk-monitoring-grace-period +- topo.standalone.config.add('nsslapd-disk-monitoring-grace-period', '61') +- # Trying to delete nsslapd-disk-monitoring-logging-critical +- assert topo.standalone.modify_s('cn=config', [(ldap.MOD_DELETE, 'nsslapd-disk-monitoring-logging-critical', +- ensure_bytes(str( +- topo.standalone.search_s('cn=config', ldap.SCOPE_SUBTREE, +- '(objectclass=*)', [ +- 'nsslapd-disk-monitoring-logging-critical'])[ +- 0]).split(' ')[2].split('\n\n')[0]))]) +- # Trying to add another value to nsslapd-disk-monitoring-logging-critical +- assert topo.standalone.config.set('nsslapd-disk-monitoring-logging-critical', 'on') ++ log.info("Starting test_valid_operations_are_permitted") ++ inst = topo.standalone ++ ++ log.info("Setting initial disk monitoring configuration") ++ inst.config.set('nsslapd-disk-monitoring', 'on') ++ inst.config.set('nsslapd-disk-monitoring-logging-critical', 'on') ++ inst.config.set('nsslapd-errorlog-level', '8') ++ inst.restart() ++ ++ log.info("Testing deletion of nsslapd-disk-monitoring-threshold") ++ inst.modify_s('cn=config', [(ldap.MOD_DELETE, 'nsslapd-disk-monitoring-threshold', '')]) ++ ++ log.info("Testing addition of nsslapd-disk-monitoring-threshold value") ++ inst.config.add('nsslapd-disk-monitoring-threshold', '2000001') ++ ++ log.info("Testing deletion of nsslapd-disk-monitoring") ++ config_entry = inst.search_s('cn=config', ldap.SCOPE_SUBTREE, '(objectclass=*)', ['nsslapd-disk-monitoring']) ++ current_value = str(config_entry[0]).split(' ')[2].split('\n\n')[0] ++ inst.modify_s('cn=config', [(ldap.MOD_DELETE, 'nsslapd-disk-monitoring', ensure_bytes(current_value))]) ++ ++ log.info("Testing addition of nsslapd-disk-monitoring value") ++ inst.config.add('nsslapd-disk-monitoring', 'off') ++ ++ log.info("Testing deletion of nsslapd-disk-monitoring-grace-period") ++ inst.modify_s('cn=config', [(ldap.MOD_DELETE, 'nsslapd-disk-monitoring-grace-period', '')]) ++ ++ log.info("Testing addition of nsslapd-disk-monitoring-grace-period value") ++ inst.config.add('nsslapd-disk-monitoring-grace-period', '61') ++ ++ log.info("Testing deletion of nsslapd-disk-monitoring-logging-critical") ++ config_entry = inst.search_s('cn=config', ldap.SCOPE_SUBTREE, '(objectclass=*)', ['nsslapd-disk-monitoring-logging-critical']) ++ current_value = str(config_entry[0]).split(' ')[2].split('\n\n')[0] ++ inst.modify_s('cn=config', [(ldap.MOD_DELETE, 'nsslapd-disk-monitoring-logging-critical', ensure_bytes(current_value))]) ++ ++ log.info("Testing addition of nsslapd-disk-monitoring-logging-critical value") ++ inst.config.set('nsslapd-disk-monitoring-logging-critical', 'on') ++ ++ log.info("All valid operations completed successfully") ++ log.info("Test completed successfully") + + + if __name__ == '__main__': +-- +2.49.0 + diff --git a/0020-Issue-6339-Address-Coverity-scan-issues-in-memberof-.patch b/0020-Issue-6339-Address-Coverity-scan-issues-in-memberof-.patch new file mode 100644 index 0000000..fd7bf88 --- /dev/null +++ b/0020-Issue-6339-Address-Coverity-scan-issues-in-memberof-.patch @@ -0,0 +1,63 @@ +From 574a5295e13cf01c34226d676104057468198616 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Fri, 4 Oct 2024 08:55:11 -0700 +Subject: [PATCH] Issue 6339 - Address Coverity scan issues in memberof and + bdb_layer (#6353) + +Description: Add null check for memberof attribute in memberof.c +Fix memory leak by freeing 'cookie' in memberof.c +Add null check for database environment in bdb_layer.c +Fix race condition by adding mutex lock/unlock in bdb_layer.c + +Fixes: https://github.com/389ds/389-ds-base/issues/6339 + +Reviewed by: @progier389, @tbordaz (Thanks!) +--- + ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c | 17 ++++++++++++++--- + 1 file changed, 14 insertions(+), 3 deletions(-) + +diff --git a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c +index b04cd68e2..4f069197e 100644 +--- a/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c ++++ b/ldap/servers/slapd/back-ldbm/db-bdb/bdb_layer.c +@@ -6987,6 +6987,7 @@ bdb_public_private_open(backend *be, const char *db_filename, int rw, dbi_env_t + bdb_config *conf = (bdb_config *)li->li_dblayer_config; + bdb_db_env **ppEnv = (bdb_db_env**)&priv->dblayer_env; + char dbhome[MAXPATHLEN]; ++ bdb_db_env *pEnv = NULL; + DB_ENV *bdb_env = NULL; + DB *bdb_db = NULL; + struct stat st = {0}; +@@ -7036,7 +7037,13 @@ bdb_public_private_open(backend *be, const char *db_filename, int rw, dbi_env_t + conf->bdb_tx_max = 50; + rc = bdb_start(li, DBLAYER_NORMAL_MODE); + if (rc == 0) { +- bdb_env = ((struct bdb_db_env*)(priv->dblayer_env))->bdb_DB_ENV; ++ pEnv = (bdb_db_env *)priv->dblayer_env; ++ if (pEnv == NULL) { ++ fprintf(stderr, "bdb_public_private_open: dbenv is not available (0x%p) for database %s\n", ++ (void *)pEnv, db_filename ? db_filename : "unknown"); ++ return EINVAL; ++ } ++ bdb_env = pEnv->bdb_DB_ENV; + } + } else { + /* Setup minimal environment */ +@@ -7080,8 +7087,12 @@ bdb_public_private_close(struct ldbminfo *li, dbi_env_t **env, dbi_db_t **db) + if (priv) { + /* Detect if db is fully set up in read write mode */ + bdb_db_env *pEnv = (bdb_db_env *)priv->dblayer_env; +- if (pEnv && pEnv->bdb_thread_count>0) { +- rw = 1; ++ if (pEnv) { ++ pthread_mutex_lock(&pEnv->bdb_thread_count_lock); ++ if (pEnv->bdb_thread_count > 0) { ++ rw = 1; ++ } ++ pthread_mutex_unlock(&pEnv->bdb_thread_count_lock); + } + } + if (rw == 0) { +-- +2.49.0 + diff --git a/0021-Issue-6468-CLI-Fix-default-error-log-level.patch b/0021-Issue-6468-CLI-Fix-default-error-log-level.patch new file mode 100644 index 0000000..884e524 --- /dev/null +++ b/0021-Issue-6468-CLI-Fix-default-error-log-level.patch @@ -0,0 +1,31 @@ +From 972ddeed2029975d5d89e165db1db554f2e8bc28 Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Tue, 29 Jul 2025 08:00:00 +0200 +Subject: [PATCH] Issue 6468 - CLI - Fix default error log level + +Description: +Default error log level is 16384 + +Relates: https://github.com/389ds/389-ds-base/issues/6468 + +Reviewed by: @droideck (Thanks!) +--- + src/lib389/lib389/cli_conf/logging.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/lib389/lib389/cli_conf/logging.py b/src/lib389/lib389/cli_conf/logging.py +index d1e32822c..c48c75faa 100644 +--- a/src/lib389/lib389/cli_conf/logging.py ++++ b/src/lib389/lib389/cli_conf/logging.py +@@ -44,7 +44,7 @@ ERROR_LEVELS = { + + "methods used for a SASL bind" + }, + "default": { +- "level": 6384, ++ "level": 16384, + "desc": "Default logging level" + }, + "filter": { +-- +2.49.0 + diff --git a/0022-Issues-6913-6886-6250-Adjust-xfail-marks-6914.patch b/0022-Issues-6913-6886-6250-Adjust-xfail-marks-6914.patch new file mode 100644 index 0000000..2f5675e --- /dev/null +++ b/0022-Issues-6913-6886-6250-Adjust-xfail-marks-6914.patch @@ -0,0 +1,222 @@ +From f28deac93c552a9c4dc9dd9c18f449fcd5cc7731 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Fri, 1 Aug 2025 09:28:39 -0700 +Subject: [PATCH] Issues 6913, 6886, 6250 - Adjust xfail marks (#6914) + +Description: Some of the ACI invalid syntax issues were fixed, +so we need to remove xfail marks. +Disk space issue should have a 'skipif' mark. +Display all attrs (nsslapd-auditlog-display-attrs: *) fails because of a bug. +EntryUSN inconsistency and overflow bugs were exposed with the tests. + +Related: https://github.com/389ds/389-ds-base/issues/6913 +Related: https://github.com/389ds/389-ds-base/issues/6886 +Related: https://github.com/389ds/389-ds-base/issues/6250 + +Reviewed by: @vashirov (Thanks!) +--- + dirsrvtests/tests/suites/acl/syntax_test.py | 13 ++++++++-- + .../tests/suites/import/regression_test.py | 18 +++++++------- + .../logging/audit_password_masking_test.py | 24 +++++++++---------- + .../suites/plugins/entryusn_overflow_test.py | 2 ++ + 4 files changed, 34 insertions(+), 23 deletions(-) + +diff --git a/dirsrvtests/tests/suites/acl/syntax_test.py b/dirsrvtests/tests/suites/acl/syntax_test.py +index 4edc7fa4b..ed9919ba3 100644 +--- a/dirsrvtests/tests/suites/acl/syntax_test.py ++++ b/dirsrvtests/tests/suites/acl/syntax_test.py +@@ -190,10 +190,9 @@ FAILED = [('test_targattrfilters_18', + f'(all)userdn="ldap:///anyone";)'), ] + + +-@pytest.mark.xfail(reason='https://bugzilla.redhat.com/show_bug.cgi?id=1691473') + @pytest.mark.parametrize("real_value", [a[1] for a in FAILED], + ids=[a[0] for a in FAILED]) +-def test_aci_invalid_syntax_fail(topo, real_value): ++def test_aci_invalid_syntax_fail(topo, real_value, request): + """Try to set wrong ACI syntax. + + :id: 83c40784-fff5-49c8-9535-7064c9c19e7e +@@ -206,6 +205,16 @@ def test_aci_invalid_syntax_fail(topo, real_value): + 1. It should pass + 2. It should not pass + """ ++ # Mark specific test cases as xfail ++ xfail_cases = [ ++ 'test_targattrfilters_18', ++ 'test_targattrfilters_20', ++ 'test_bind_rule_set_with_more_than_three' ++ ] ++ ++ if request.node.callspec.id in xfail_cases: ++ pytest.xfail("DS6913 - This test case is expected to fail") ++ + domain = Domain(topo.standalone, DEFAULT_SUFFIX) + with pytest.raises(ldap.INVALID_SYNTAX): + domain.add("aci", real_value) +diff --git a/dirsrvtests/tests/suites/import/regression_test.py b/dirsrvtests/tests/suites/import/regression_test.py +index 2f850a19a..18611de35 100644 +--- a/dirsrvtests/tests/suites/import/regression_test.py ++++ b/dirsrvtests/tests/suites/import/regression_test.py +@@ -323,7 +323,7 @@ ou: myDups00001 + + @pytest.mark.bz1749595 + @pytest.mark.tier2 +-@pytest.mark.xfail(not _check_disk_space(), reason="not enough disk space for lmdb map") ++@pytest.mark.skipif(not _check_disk_space(), reason="not enough disk space for lmdb map") + @pytest.mark.xfail(ds_is_older("1.3.10.1"), reason="bz1749595 not fixed on versions older than 1.3.10.1") + def test_large_ldif2db_ancestorid_index_creation(topo, _set_mdb_map_size): + """Import with ldif2db a large file - check that the ancestorid index creation phase has a correct performance +@@ -399,39 +399,39 @@ def test_large_ldif2db_ancestorid_index_creation(topo, _set_mdb_map_size): + log.info('Starting the server') + topo.standalone.start() + +- # With lmdb there is no more any special phase for ancestorid ++ # With lmdb there is no more any special phase for ancestorid + # because ancestorsid get updated on the fly while processing the + # entryrdn (by up the parents chain to compute the parentid +- # ++ # + # But there is still a numSubordinates generation phase + if get_default_db_lib() == "mdb": + log.info('parse the errors logs to check lines with "Generating numSubordinates complete." are present') + end_numsubordinates = str(topo.standalone.ds_error_log.match(r'.*Generating numSubordinates complete.*'))[1:-1] + assert len(end_numsubordinates) > 0 +- ++ + else: + log.info('parse the errors logs to check lines with "Starting sort of ancestorid" are present') + start_sort_str = str(topo.standalone.ds_error_log.match(r'.*Starting sort of ancestorid non-leaf IDs*'))[1:-1] + assert len(start_sort_str) > 0 +- ++ + log.info('parse the errors logs to check lines with "Finished sort of ancestorid" are present') + end_sort_str = str(topo.standalone.ds_error_log.match(r'.*Finished sort of ancestorid non-leaf IDs*'))[1:-1] + assert len(end_sort_str) > 0 +- ++ + log.info('parse the error logs for the line with "Gathering ancestorid non-leaf IDs"') + start_ancestorid_indexing_op_str = str(topo.standalone.ds_error_log.match(r'.*Gathering ancestorid non-leaf IDs*'))[1:-1] + assert len(start_ancestorid_indexing_op_str) > 0 +- ++ + log.info('parse the error logs for the line with "Created ancestorid index"') + end_ancestorid_indexing_op_str = str(topo.standalone.ds_error_log.match(r'.*Created ancestorid index*'))[1:-1] + assert len(end_ancestorid_indexing_op_str) > 0 +- ++ + log.info('get the ancestorid non-leaf IDs indexing start and end time from the collected strings') + # Collected lines look like : '[15/May/2020:05:30:27.245967313 -0400] - INFO - bdb_get_nonleaf_ids - import userRoot: Gathering ancestorid non-leaf IDs...' + # We are getting the sec.nanosec part of the date, '27.245967313' in the above example + start_time = (start_ancestorid_indexing_op_str.split()[0]).split(':')[3] + end_time = (end_ancestorid_indexing_op_str.split()[0]).split(':')[3] +- ++ + log.info('Calculate the elapsed time for the ancestorid non-leaf IDs index creation') + etime = (Decimal(end_time) - Decimal(start_time)) + # The time for the ancestorid index creation should be less than 10s for an offline import of an ldif file with 100000 entries / 5 entries per node +diff --git a/dirsrvtests/tests/suites/logging/audit_password_masking_test.py b/dirsrvtests/tests/suites/logging/audit_password_masking_test.py +index 3b6a54849..69a36cb5d 100644 +--- a/dirsrvtests/tests/suites/logging/audit_password_masking_test.py ++++ b/dirsrvtests/tests/suites/logging/audit_password_masking_test.py +@@ -117,10 +117,10 @@ def check_password_masked(inst, log_format, expected_password, actual_password): + + @pytest.mark.parametrize("log_format,display_attrs", [ + ("default", None), +- ("default", "*"), ++ pytest.param("default", "*", marks=pytest.mark.xfail(reason="DS6886")), + ("default", "userPassword"), + ("json", None), +- ("json", "*"), ++ pytest.param("json", "*", marks=pytest.mark.xfail(reason="DS6886")), + ("json", "userPassword") + ]) + def test_password_masking_add_operation(topo, log_format, display_attrs): +@@ -173,10 +173,10 @@ def test_password_masking_add_operation(topo, log_format, display_attrs): + + @pytest.mark.parametrize("log_format,display_attrs", [ + ("default", None), +- ("default", "*"), ++ pytest.param("default", "*", marks=pytest.mark.xfail(reason="DS6886")), + ("default", "userPassword"), + ("json", None), +- ("json", "*"), ++ pytest.param("json", "*", marks=pytest.mark.xfail(reason="DS6886")), + ("json", "userPassword") + ]) + def test_password_masking_modify_operation(topo, log_format, display_attrs): +@@ -242,10 +242,10 @@ def test_password_masking_modify_operation(topo, log_format, display_attrs): + + @pytest.mark.parametrize("log_format,display_attrs", [ + ("default", None), +- ("default", "*"), ++ pytest.param("default", "*", marks=pytest.mark.xfail(reason="DS6886")), + ("default", "nsslapd-rootpw"), + ("json", None), +- ("json", "*"), ++ pytest.param("json", "*", marks=pytest.mark.xfail(reason="DS6886")), + ("json", "nsslapd-rootpw") + ]) + def test_password_masking_rootpw_modify_operation(topo, log_format, display_attrs): +@@ -297,10 +297,10 @@ def test_password_masking_rootpw_modify_operation(topo, log_format, display_attr + + @pytest.mark.parametrize("log_format,display_attrs", [ + ("default", None), +- ("default", "*"), ++ pytest.param("default", "*", marks=pytest.mark.xfail(reason="DS6886")), + ("default", "nsmultiplexorcredentials"), + ("json", None), +- ("json", "*"), ++ pytest.param("json", "*", marks=pytest.mark.xfail(reason="DS6886")), + ("json", "nsmultiplexorcredentials") + ]) + def test_password_masking_multiplexor_credentials(topo, log_format, display_attrs): +@@ -368,10 +368,10 @@ def test_password_masking_multiplexor_credentials(topo, log_format, display_attr + + @pytest.mark.parametrize("log_format,display_attrs", [ + ("default", None), +- ("default", "*"), ++ pytest.param("default", "*", marks=pytest.mark.xfail(reason="DS6886")), + ("default", "nsDS5ReplicaCredentials"), + ("json", None), +- ("json", "*"), ++ pytest.param("json", "*", marks=pytest.mark.xfail(reason="DS6886")), + ("json", "nsDS5ReplicaCredentials") + ]) + def test_password_masking_replica_credentials(topo, log_format, display_attrs): +@@ -432,10 +432,10 @@ def test_password_masking_replica_credentials(topo, log_format, display_attrs): + + @pytest.mark.parametrize("log_format,display_attrs", [ + ("default", None), +- ("default", "*"), ++ pytest.param("default", "*", marks=pytest.mark.xfail(reason="DS6886")), + ("default", "nsDS5ReplicaBootstrapCredentials"), + ("json", None), +- ("json", "*"), ++ pytest.param("json", "*", marks=pytest.mark.xfail(reason="DS6886")), + ("json", "nsDS5ReplicaBootstrapCredentials") + ]) + def test_password_masking_bootstrap_credentials(topo, log_format, display_attrs): +diff --git a/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py b/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py +index a23d734ca..8c3a537ab 100644 +--- a/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py ++++ b/dirsrvtests/tests/suites/plugins/entryusn_overflow_test.py +@@ -81,6 +81,7 @@ def setup_usn_test(topology_st, request): + return created_users + + ++@pytest.mark.xfail(reason="DS6250") + def test_entryusn_overflow_on_add_existing_entries(topology_st, setup_usn_test): + """Test that reproduces entryUSN overflow when adding existing entries + +@@ -232,6 +233,7 @@ def test_entryusn_overflow_on_add_existing_entries(topology_st, setup_usn_test): + log.info("EntryUSN overflow test completed successfully") + + ++@pytest.mark.xfail(reason="DS6250") + def test_entryusn_consistency_after_failed_adds(topology_st, setup_usn_test): + """Test that entryUSN remains consistent after failed add operations + +-- +2.49.0 + diff --git a/0023-Issue-6181-RFE-Allow-system-to-manage-uid-gid-at-sta.patch b/0023-Issue-6181-RFE-Allow-system-to-manage-uid-gid-at-sta.patch new file mode 100644 index 0000000..d208d39 --- /dev/null +++ b/0023-Issue-6181-RFE-Allow-system-to-manage-uid-gid-at-sta.patch @@ -0,0 +1,32 @@ +From 58a9e1083865e75bba3cf9867a3df109031d7810 Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Mon, 28 Jul 2025 13:18:26 +0200 +Subject: [PATCH] Issue 6181 - RFE - Allow system to manage uid/gid at startup + +Description: +Expand CapabilityBoundingSet to include CAP_FOWNER + +Relates: https://github.com/389ds/389-ds-base/issues/6181 +Relates: https://github.com/389ds/389-ds-base/issues/6906 + +Reviewed by: @progier389 (Thanks!) +--- + wrappers/systemd.template.service.in | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/wrappers/systemd.template.service.in b/wrappers/systemd.template.service.in +index fa05c9f60..6db1f6f8f 100644 +--- a/wrappers/systemd.template.service.in ++++ b/wrappers/systemd.template.service.in +@@ -25,7 +25,7 @@ MemoryAccounting=yes + + # Allow non-root instances to bind to low ports. + AmbientCapabilities=CAP_NET_BIND_SERVICE +-CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID CAP_DAC_OVERRIDE CAP_CHOWN ++CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID CAP_DAC_OVERRIDE CAP_CHOWN CAP_FOWNER + + PrivateTmp=on + # https://en.opensuse.org/openSUSE:Security_Features#Systemd_hardening_effort +-- +2.49.0 + diff --git a/0024-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch b/0024-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch new file mode 100644 index 0000000..d3ec59d --- /dev/null +++ b/0024-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch @@ -0,0 +1,92 @@ +From e03af0aa7e041fc2ca20caf3bcb5810e968043dc Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Tue, 13 May 2025 13:53:05 +0200 +Subject: [PATCH] Issue 6778 - Memory leak in + roles_cache_create_object_from_entry + +Bug Description: +`this_role` has internal allocations (`dn`, `rolescopedn`, etc.) +that are not freed. + +Fix Description: +Use `roles_cache_role_object_free` to free `this_role` and all its +internal structures. + +Fixes: https://github.com/389ds/389-ds-base/issues/6778 + +Reviewed by: @mreynolds389 (Thanks!) +--- + ldap/servers/plugins/roles/roles_cache.c | 15 ++++++++------- + 1 file changed, 8 insertions(+), 7 deletions(-) + +diff --git a/ldap/servers/plugins/roles/roles_cache.c b/ldap/servers/plugins/roles/roles_cache.c +index bbed11802..60d7182e2 100644 +--- a/ldap/servers/plugins/roles/roles_cache.c ++++ b/ldap/servers/plugins/roles/roles_cache.c +@@ -1098,7 +1098,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu + /* We determine the role type by reading the objectclass */ + if (roles_cache_is_role_entry(role_entry) == 0) { + /* Bad type */ +- slapi_ch_free((void **)&this_role); ++ roles_cache_role_object_free((caddr_t)this_role); + return SLAPI_ROLE_DEFINITION_ERROR; + } + +@@ -1108,7 +1108,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu + this_role->type = type; + } else { + /* Bad type */ +- slapi_ch_free((void **)&this_role); ++ roles_cache_role_object_free((caddr_t)this_role); + return SLAPI_ROLE_DEFINITION_ERROR; + } + +@@ -1166,7 +1166,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu + filter_attr_value = (char *)slapi_entry_attr_get_charptr(role_entry, ROLE_FILTER_ATTR_NAME); + if (filter_attr_value == NULL) { + /* Means probably no attribute or no value there */ +- slapi_ch_free((void **)&this_role); ++ roles_cache_role_object_free((caddr_t)this_role); + return SLAPI_ROLE_ERROR_NO_FILTER_SPECIFIED; + } + +@@ -1205,7 +1205,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu + (char *)slapi_sdn_get_ndn(this_role->dn), + ROLE_FILTER_ATTR_NAME, filter_attr_value, + ROLE_FILTER_ATTR_NAME); +- slapi_ch_free((void **)&this_role); ++ roles_cache_role_object_free((caddr_t)this_role); + slapi_ch_free_string(&filter_attr_value); + return SLAPI_ROLE_ERROR_FILTER_BAD; + } +@@ -1217,7 +1217,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu + filter = slapi_str2filter(filter_attr_value); + if (filter == NULL) { + /* An error has occured */ +- slapi_ch_free((void **)&this_role); ++ roles_cache_role_object_free((caddr_t)this_role); + slapi_ch_free_string(&filter_attr_value); + return SLAPI_ROLE_ERROR_FILTER_BAD; + } +@@ -1228,7 +1228,8 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu + (char *)slapi_sdn_get_ndn(this_role->dn), + filter_attr_value, + ROLE_FILTER_ATTR_NAME); +- slapi_ch_free((void **)&this_role); ++ roles_cache_role_object_free((caddr_t)this_role); ++ slapi_filter_free(filter, 1); + slapi_ch_free_string(&filter_attr_value); + return SLAPI_ROLE_ERROR_FILTER_BAD; + } +@@ -1285,7 +1286,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu + if (rc == 0) { + *result = this_role; + } else { +- slapi_ch_free((void **)&this_role); ++ roles_cache_role_object_free((caddr_t)this_role); + } + + slapi_log_err(SLAPI_LOG_PLUGIN, ROLES_PLUGIN_SUBSYSTEM, +-- +2.49.0 + diff --git a/0025-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch b/0025-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch new file mode 100644 index 0000000..286ed06 --- /dev/null +++ b/0025-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch @@ -0,0 +1,262 @@ +From c8c9d8814bd328d9772b6a248aa142b72430cba1 Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Wed, 16 Jul 2025 11:22:30 +0200 +Subject: [PATCH] Issue 6778 - Memory leak in + roles_cache_create_object_from_entry part 2 + +Bug Description: +Everytime a role with scope DN is processed, we leak rolescopeDN. + +Fix Description: +* Initialize all pointer variables to NULL +* Add additional NULL checks +* Free rolescopeDN +* Move test_rewriter_with_invalid_filter before the DB contains 90k entries +* Use task.wait() for import task completion instead of parsing logs, +increase the timeout + +Fixes: https://github.com/389ds/389-ds-base/issues/6778 + +Reviewed by: @progier389 (Thanks!) +--- + dirsrvtests/tests/suites/roles/basic_test.py | 164 +++++++++---------- + ldap/servers/plugins/roles/roles_cache.c | 10 +- + 2 files changed, 82 insertions(+), 92 deletions(-) + +diff --git a/dirsrvtests/tests/suites/roles/basic_test.py b/dirsrvtests/tests/suites/roles/basic_test.py +index d92d6f0c3..ec208bae9 100644 +--- a/dirsrvtests/tests/suites/roles/basic_test.py ++++ b/dirsrvtests/tests/suites/roles/basic_test.py +@@ -510,6 +510,76 @@ def test_vattr_on_managed_role(topo, request): + + request.addfinalizer(fin) + ++def test_rewriter_with_invalid_filter(topo, request): ++ """Test that server does not crash when having ++ invalid filter in filtered role ++ ++ :id: 5013b0b2-0af6-11f0-8684-482ae39447e5 ++ :setup: standalone server ++ :steps: ++ 1. Setup filtered role with good filter ++ 2. Setup nsrole rewriter ++ 3. Restart the server ++ 4. Search for entries ++ 5. Setup filtered role with bad filter ++ 6. Search for entries ++ :expectedresults: ++ 1. Operation should succeed ++ 2. Operation should succeed ++ 3. Operation should succeed ++ 4. Operation should succeed ++ 5. Operation should succeed ++ 6. Operation should succeed ++ """ ++ inst = topo.standalone ++ entries = [] ++ ++ def fin(): ++ inst.start() ++ for entry in entries: ++ entry.delete() ++ request.addfinalizer(fin) ++ ++ # Setup filtered role ++ roles = FilteredRoles(inst, f'ou=people,{DEFAULT_SUFFIX}') ++ filter_ko = '(&((objectClass=top)(objectClass=nsPerson))' ++ filter_ok = '(&(objectClass=top)(objectClass=nsPerson))' ++ role_properties = { ++ 'cn': 'TestFilteredRole', ++ 'nsRoleFilter': filter_ok, ++ 'description': 'Test good filter', ++ } ++ role = roles.create(properties=role_properties) ++ entries.append(role) ++ ++ # Setup nsrole rewriter ++ rewriters = Rewriters(inst) ++ rewriter_properties = { ++ "cn": "nsrole", ++ "nsslapd-libpath": 'libroles-plugin', ++ "nsslapd-filterrewriter": 'role_nsRole_filter_rewriter', ++ } ++ rewriter = rewriters.ensure_state(properties=rewriter_properties) ++ entries.append(rewriter) ++ ++ # Restart thge instance ++ inst.restart() ++ ++ # Search for entries ++ entries = inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn) ++ ++ # Set bad filter ++ role_properties = { ++ 'cn': 'TestFilteredRole', ++ 'nsRoleFilter': filter_ko, ++ 'description': 'Test bad filter', ++ } ++ role.ensure_state(properties=role_properties) ++ ++ # Search for entries ++ entries = inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn) ++ ++ + def test_managed_and_filtered_role_rewrite(topo, request): + """Test that filter components containing 'nsrole=xxx' + are reworked if xxx is either a filtered role or a managed +@@ -581,17 +651,11 @@ def test_managed_and_filtered_role_rewrite(topo, request): + PARENT="ou=people,%s" % DEFAULT_SUFFIX + dbgen_users(topo.standalone, 90000, import_ldif, DEFAULT_SUFFIX, entry_name=RDN, generic=True, parent=PARENT) + +- # online import ++ # Online import + import_task = ImportTask(topo.standalone) + import_task.import_suffix_from_ldif(ldiffile=import_ldif, suffix=DEFAULT_SUFFIX) +- # Check for up to 200sec that the completion +- for i in range(1, 20): +- if len(topo.standalone.ds_error_log.match('.*import userRoot: Import complete. Processed 9000.*')) > 0: +- break +- time.sleep(10) +- import_complete = topo.standalone.ds_error_log.match('.*import userRoot: Import complete. Processed 9000.*') +- assert (len(import_complete) == 1) +- ++ import_task.wait(timeout=400) ++ assert import_task.get_exit_code() == 0 + # Restart server + topo.standalone.restart() + +@@ -715,17 +779,11 @@ def test_not_such_entry_role_rewrite(topo, request): + PARENT="ou=people,%s" % DEFAULT_SUFFIX + dbgen_users(topo.standalone, 91000, import_ldif, DEFAULT_SUFFIX, entry_name=RDN, generic=True, parent=PARENT) + +- # online import ++ # Online import + import_task = ImportTask(topo.standalone) + import_task.import_suffix_from_ldif(ldiffile=import_ldif, suffix=DEFAULT_SUFFIX) +- # Check for up to 200sec that the completion +- for i in range(1, 20): +- if len(topo.standalone.ds_error_log.match('.*import userRoot: Import complete. Processed 9100.*')) > 0: +- break +- time.sleep(10) +- import_complete = topo.standalone.ds_error_log.match('.*import userRoot: Import complete. Processed 9100.*') +- assert (len(import_complete) == 1) +- ++ import_task.wait(timeout=400) ++ assert import_task.get_exit_code() == 0 + # Restart server + topo.standalone.restart() + +@@ -769,76 +827,6 @@ def test_not_such_entry_role_rewrite(topo, request): + request.addfinalizer(fin) + + +-def test_rewriter_with_invalid_filter(topo, request): +- """Test that server does not crash when having +- invalid filter in filtered role +- +- :id: 5013b0b2-0af6-11f0-8684-482ae39447e5 +- :setup: standalone server +- :steps: +- 1. Setup filtered role with good filter +- 2. Setup nsrole rewriter +- 3. Restart the server +- 4. Search for entries +- 5. Setup filtered role with bad filter +- 6. Search for entries +- :expectedresults: +- 1. Operation should succeed +- 2. Operation should succeed +- 3. Operation should succeed +- 4. Operation should succeed +- 5. Operation should succeed +- 6. Operation should succeed +- """ +- inst = topo.standalone +- entries = [] +- +- def fin(): +- inst.start() +- for entry in entries: +- entry.delete() +- request.addfinalizer(fin) +- +- # Setup filtered role +- roles = FilteredRoles(inst, f'ou=people,{DEFAULT_SUFFIX}') +- filter_ko = '(&((objectClass=top)(objectClass=nsPerson))' +- filter_ok = '(&(objectClass=top)(objectClass=nsPerson))' +- role_properties = { +- 'cn': 'TestFilteredRole', +- 'nsRoleFilter': filter_ok, +- 'description': 'Test good filter', +- } +- role = roles.create(properties=role_properties) +- entries.append(role) +- +- # Setup nsrole rewriter +- rewriters = Rewriters(inst) +- rewriter_properties = { +- "cn": "nsrole", +- "nsslapd-libpath": 'libroles-plugin', +- "nsslapd-filterrewriter": 'role_nsRole_filter_rewriter', +- } +- rewriter = rewriters.ensure_state(properties=rewriter_properties) +- entries.append(rewriter) +- +- # Restart thge instance +- inst.restart() +- +- # Search for entries +- entries = inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn) +- +- # Set bad filter +- role_properties = { +- 'cn': 'TestFilteredRole', +- 'nsRoleFilter': filter_ko, +- 'description': 'Test bad filter', +- } +- role.ensure_state(properties=role_properties) +- +- # Search for entries +- entries = inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn) +- +- + if __name__ == "__main__": + CURRENT_FILE = os.path.realpath(__file__) + pytest.main("-s -v %s" % CURRENT_FILE) +diff --git a/ldap/servers/plugins/roles/roles_cache.c b/ldap/servers/plugins/roles/roles_cache.c +index 60d7182e2..60f5a919a 100644 +--- a/ldap/servers/plugins/roles/roles_cache.c ++++ b/ldap/servers/plugins/roles/roles_cache.c +@@ -1117,16 +1117,17 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu + + rolescopeDN = slapi_entry_attr_get_charptr(role_entry, ROLE_SCOPE_DN); + if (rolescopeDN) { +- Slapi_DN *rolescopeSDN; +- Slapi_DN *top_rolescopeSDN, *top_this_roleSDN; ++ Slapi_DN *rolescopeSDN = NULL; ++ Slapi_DN *top_rolescopeSDN = NULL; ++ Slapi_DN *top_this_roleSDN = NULL; + + /* Before accepting to use this scope, first check if it belongs to the same suffix */ + rolescopeSDN = slapi_sdn_new_dn_byref(rolescopeDN); +- if ((strlen((char *)slapi_sdn_get_ndn(rolescopeSDN)) > 0) && ++ if (rolescopeSDN && (strlen((char *)slapi_sdn_get_ndn(rolescopeSDN)) > 0) && + (slapi_dn_syntax_check(NULL, (char *)slapi_sdn_get_ndn(rolescopeSDN), 1) == 0)) { + top_rolescopeSDN = roles_cache_get_top_suffix(rolescopeSDN); + top_this_roleSDN = roles_cache_get_top_suffix(this_role->dn); +- if (slapi_sdn_compare(top_rolescopeSDN, top_this_roleSDN) == 0) { ++ if (top_rolescopeSDN && top_this_roleSDN && slapi_sdn_compare(top_rolescopeSDN, top_this_roleSDN) == 0) { + /* rolescopeDN belongs to the same suffix as the role, we can use this scope */ + this_role->rolescopedn = rolescopeSDN; + } else { +@@ -1148,6 +1149,7 @@ roles_cache_create_object_from_entry(Slapi_Entry *role_entry, role_object **resu + rolescopeDN); + slapi_sdn_free(&rolescopeSDN); + } ++ slapi_ch_free_string(&rolescopeDN); + } + + /* Depending upon role type, pull out the remaining information we need */ +-- +2.49.0 + diff --git a/0026-Issue-6850-AddressSanitizer-memory-leak-in-mdb_init.patch b/0026-Issue-6850-AddressSanitizer-memory-leak-in-mdb_init.patch new file mode 100644 index 0000000..b99f974 --- /dev/null +++ b/0026-Issue-6850-AddressSanitizer-memory-leak-in-mdb_init.patch @@ -0,0 +1,65 @@ +From f83a1996e3438e471cec086d53fb94be0c8666aa Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Mon, 7 Jul 2025 23:11:17 +0200 +Subject: [PATCH] Issue 6850 - AddressSanitizer: memory leak in mdb_init + +Bug Description: +`dbmdb_componentid` can be allocated multiple times. To avoid a memory +leak, allocate it only once, and free at the cleanup. + +Fixes: https://github.com/389ds/389-ds-base/issues/6850 + +Reviewed by: @mreynolds389, @tbordaz (Tnanks!) +--- + ldap/servers/slapd/back-ldbm/db-mdb/mdb_config.c | 4 +++- + ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c | 2 +- + ldap/servers/slapd/back-ldbm/db-mdb/mdb_misc.c | 5 +++++ + 3 files changed, 9 insertions(+), 2 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 1f7b71442..bebc83b76 100644 +--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_config.c ++++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_config.c +@@ -146,7 +146,9 @@ dbmdb_compute_limits(struct ldbminfo *li) + int mdb_init(struct ldbminfo *li, config_info *config_array) + { + dbmdb_ctx_t *conf = (dbmdb_ctx_t *)slapi_ch_calloc(1, sizeof(dbmdb_ctx_t)); +- dbmdb_componentid = generate_componentid(NULL, "db-mdb"); ++ if (dbmdb_componentid == NULL) { ++ dbmdb_componentid = generate_componentid(NULL, "db-mdb"); ++ } + + li->li_dblayer_config = conf; + strncpy(conf->home, li->li_directory, MAXPATHLEN-1); +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 3ecc47170..c6e9f8b01 100644 +--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c ++++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_layer.c +@@ -19,7 +19,7 @@ + #include + #include + +-Slapi_ComponentId *dbmdb_componentid; ++Slapi_ComponentId *dbmdb_componentid = NULL; + + #define BULKOP_MAX_RECORDS 100 /* Max records handled by a single bulk operations */ + +diff --git a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_misc.c b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_misc.c +index 2d07db9b5..ae10ac7cf 100644 +--- a/ldap/servers/slapd/back-ldbm/db-mdb/mdb_misc.c ++++ b/ldap/servers/slapd/back-ldbm/db-mdb/mdb_misc.c +@@ -49,6 +49,11 @@ dbmdb_cleanup(struct ldbminfo *li) + } + slapi_ch_free((void **)&(li->li_dblayer_config)); + ++ if (dbmdb_componentid != NULL) { ++ release_componentid(dbmdb_componentid); ++ dbmdb_componentid = NULL; ++ } ++ + return 0; + } + +-- +2.49.0 + diff --git a/0027-Issue-6848-AddressSanitizer-leak-in-do_search.patch b/0027-Issue-6848-AddressSanitizer-leak-in-do_search.patch new file mode 100644 index 0000000..6908ba1 --- /dev/null +++ b/0027-Issue-6848-AddressSanitizer-leak-in-do_search.patch @@ -0,0 +1,58 @@ +From e98acc1bfe2194fcdd0e420777eb65a20d55a64b Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Mon, 7 Jul 2025 22:01:09 +0200 +Subject: [PATCH] Issue 6848 - AddressSanitizer: leak in do_search + +Bug Description: +When there's a BER decoding error and the function goes to +`free_and_return`, the `attrs` variable is not being freed because it's +only freed if `!psearch || rc != 0 || err != 0`, but `err` is still 0 at +that point. + +If we reach `free_and_return` from the `ber_scanf` error path, `attrs` +was never set in the pblock with `slapi_pblock_set()`, so the +`slapi_pblock_get()` call will not retrieve the potentially partially +allocated `attrs` from the BER decoding. + +Fixes: https://github.com/389ds/389-ds-base/issues/6848 + +Reviewed by: @tbordaz, @droideck (Thanks!) +--- + ldap/servers/slapd/search.c | 14 ++++++++++++-- + 1 file changed, 12 insertions(+), 2 deletions(-) + +diff --git a/ldap/servers/slapd/search.c b/ldap/servers/slapd/search.c +index e9b2c3670..f9d03c090 100644 +--- a/ldap/servers/slapd/search.c ++++ b/ldap/servers/slapd/search.c +@@ -235,6 +235,7 @@ do_search(Slapi_PBlock *pb) + log_search_access(pb, base, scope, fstr, "decoding error"); + send_ldap_result(pb, LDAP_PROTOCOL_ERROR, NULL, NULL, 0, + NULL); ++ err = 1; /* Make sure we free everything */ + goto free_and_return; + } + +@@ -420,8 +421,17 @@ free_and_return: + if (!psearch || rc != 0 || err != 0) { + slapi_ch_free_string(&fstr); + slapi_filter_free(filter, 1); +- slapi_pblock_get(pb, SLAPI_SEARCH_ATTRS, &attrs); +- charray_free(attrs); /* passing NULL is fine */ ++ ++ /* Get attrs from pblock if it was set there, otherwise use local attrs */ ++ char **pblock_attrs = NULL; ++ slapi_pblock_get(pb, SLAPI_SEARCH_ATTRS, &pblock_attrs); ++ if (pblock_attrs != NULL) { ++ charray_free(pblock_attrs); /* Free attrs from pblock */ ++ slapi_pblock_set(pb, SLAPI_SEARCH_ATTRS, NULL); ++ } else if (attrs != NULL) { ++ /* Free attrs that were allocated but never put in pblock */ ++ charray_free(attrs); ++ } + charray_free(gerattrs); /* passing NULL is fine */ + /* + * Fix for defect 526719 / 553356 : Persistent search op failed. +-- +2.49.0 + diff --git a/0028-Issue-6865-AddressSanitizer-leak-in-agmt_update_init.patch b/0028-Issue-6865-AddressSanitizer-leak-in-agmt_update_init.patch new file mode 100644 index 0000000..99b5e6f --- /dev/null +++ b/0028-Issue-6865-AddressSanitizer-leak-in-agmt_update_init.patch @@ -0,0 +1,58 @@ +From 120bc2666b682a27ffd6ace5cc238b33fab32c21 Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Fri, 11 Jul 2025 12:32:38 +0200 +Subject: [PATCH] Issue 6865 - AddressSanitizer: leak in + agmt_update_init_status + +Bug Description: +We allocate an array of `LDAPMod *` pointers, but never free it: + +``` +================================================================= +==2748356==ERROR: LeakSanitizer: detected memory leaks + +Direct leak of 24 byte(s) in 1 object(s) allocated from: + #0 0x7f05e8cb4a07 in __interceptor_malloc (/lib64/libasan.so.6+0xb4a07) + #1 0x7f05e85c0138 in slapi_ch_malloc (/usr/lib64/dirsrv/libslapd.so.0+0x1c0138) + #2 0x7f05e109e481 in agmt_update_init_status ldap/servers/plugins/replication/repl5_agmt.c:2583 + #3 0x7f05e10a0aa5 in agmtlist_shutdown ldap/servers/plugins/replication/repl5_agmtlist.c:789 + #4 0x7f05e10ab6bc in multisupplier_stop ldap/servers/plugins/replication/repl5_init.c:844 + #5 0x7f05e10ab6bc in multisupplier_stop ldap/servers/plugins/replication/repl5_init.c:837 + #6 0x7f05e862507d in plugin_call_func ldap/servers/slapd/plugin.c:2001 + #7 0x7f05e8625be1 in plugin_call_one ldap/servers/slapd/plugin.c:1950 + #8 0x7f05e8625be1 in plugin_dependency_closeall ldap/servers/slapd/plugin.c:1844 + #9 0x55e1a7ff9815 in slapd_daemon ldap/servers/slapd/daemon.c:1275 + #10 0x55e1a7fd36ef in main (/usr/sbin/ns-slapd+0x3e6ef) + #11 0x7f05e80295cf in __libc_start_call_main (/lib64/libc.so.6+0x295cf) + #12 0x7f05e802967f in __libc_start_main_alias_2 (/lib64/libc.so.6+0x2967f) + #13 0x55e1a7fd74a4 in _start (/usr/sbin/ns-slapd+0x424a4) + +SUMMARY: AddressSanitizer: 24 byte(s) leaked in 1 allocation(s). +``` + +Fix Description: +Ensure `mods` is freed in the cleanup code. + +Fixes: https://github.com/389ds/389-ds-base/issues/6865 +Relates: https://github.com/389ds/389-ds-base/issues/6470 + +Reviewed by: @mreynolds389 (Thanks!) +--- + ldap/servers/plugins/replication/repl5_agmt.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/ldap/servers/plugins/replication/repl5_agmt.c b/ldap/servers/plugins/replication/repl5_agmt.c +index 6ffb074d4..c6cfcda07 100644 +--- a/ldap/servers/plugins/replication/repl5_agmt.c ++++ b/ldap/servers/plugins/replication/repl5_agmt.c +@@ -2653,6 +2653,7 @@ agmt_update_init_status(Repl_Agmt *ra) + } else { + PR_Unlock(ra->lock); + } ++ slapi_ch_free((void **)&mods); + slapi_mod_done(&smod_start_time); + slapi_mod_done(&smod_end_time); + slapi_mod_done(&smod_status); +-- +2.49.0 + diff --git a/0029-Issue-6768-ns-slapd-crashes-when-a-referral-is-added.patch b/0029-Issue-6768-ns-slapd-crashes-when-a-referral-is-added.patch new file mode 100644 index 0000000..e82a4f9 --- /dev/null +++ b/0029-Issue-6768-ns-slapd-crashes-when-a-referral-is-added.patch @@ -0,0 +1,97 @@ +From 5cc13c70dfe22d95686bec9214c53f1b4114cd90 Mon Sep 17 00:00:00 2001 +From: James Chapman +Date: Fri, 1 Aug 2025 13:27:02 +0100 +Subject: [PATCH] Issue 6768 - ns-slapd crashes when a referral is added + (#6780) + +Bug description: When a paged result search is successfully run on a referred +suffix, we retrieve the search result set from the pblock and try to release +it. In this case the search result set is NULL, which triggers a SEGV during +the release. + +Fix description: If the search result code is LDAP_REFERRAL, skip deletion of +the search result set. Added test case. + +Fixes: https://github.com/389ds/389-ds-base/issues/6768 + +Reviewed by: @tbordaz, @progier389 (Thank you) +--- + .../paged_results/paged_results_test.py | 46 +++++++++++++++++++ + ldap/servers/slapd/opshared.c | 4 +- + 2 files changed, 49 insertions(+), 1 deletion(-) + +diff --git a/dirsrvtests/tests/suites/paged_results/paged_results_test.py b/dirsrvtests/tests/suites/paged_results/paged_results_test.py +index fca48db0f..1bb94b53a 100644 +--- a/dirsrvtests/tests/suites/paged_results/paged_results_test.py ++++ b/dirsrvtests/tests/suites/paged_results/paged_results_test.py +@@ -1271,6 +1271,52 @@ def test_search_stress_abandon(create_40k_users, create_user): + paged_search(conn, create_40k_users.suffix, [req_ctrl], search_flt, searchreq_attrlist, abandon_rate=abandon_rate) + + ++def test_search_referral(topology_st): ++ """Test a paged search on a referred suffix doesnt crash the server. ++ ++ :id: c788bdbf-965b-4f12-ac24-d4d695e2cce2 ++ ++ :setup: Standalone instance ++ ++ :steps: ++ 1. Configure a default referral. ++ 2. Create a paged result search control. ++ 3. Paged result search on referral suffix (doesnt exist on the instance, triggering a referral). ++ 4. Check the server is still running. ++ 5. Remove referral. ++ ++ :expectedresults: ++ 1. Referral sucessfully set. ++ 2. Control created. ++ 3. Search returns ldap.REFERRAL (10). ++ 4. Server still running. ++ 5. Referral removed. ++ """ ++ ++ page_size = 5 ++ SEARCH_SUFFIX = "dc=referme,dc=com" ++ REFERRAL = "ldap://localhost.localdomain:389/o%3dnetscaperoot" ++ ++ log.info('Configuring referral') ++ topology_st.standalone.config.set('nsslapd-referral', REFERRAL) ++ referral = topology_st.standalone.config.get_attr_val_utf8('nsslapd-referral') ++ assert (referral == REFERRAL) ++ ++ log.info('Create paged result search control') ++ req_ctrl = SimplePagedResultsControl(True, size=page_size, cookie='') ++ ++ log.info('Perform a paged result search on referred suffix, no chase') ++ with pytest.raises(ldap.REFERRAL): ++ topology_st.standalone.search_ext_s(SEARCH_SUFFIX, ldap.SCOPE_SUBTREE, serverctrls=[req_ctrl]) ++ ++ log.info('Confirm instance is still running') ++ assert (topology_st.standalone.status()) ++ ++ log.info('Remove referral') ++ topology_st.standalone.config.remove_all('nsslapd-referral') ++ referral = topology_st.standalone.config.get_attr_val_utf8('nsslapd-referral') ++ assert (referral == None) ++ + if __name__ == '__main__': + # Run isolated + # -s for DEBUG mode +diff --git a/ldap/servers/slapd/opshared.c b/ldap/servers/slapd/opshared.c +index 14a7dcdfb..03ed60981 100644 +--- a/ldap/servers/slapd/opshared.c ++++ b/ldap/servers/slapd/opshared.c +@@ -879,7 +879,9 @@ op_shared_search(Slapi_PBlock *pb, int send_result) + /* Free the results if not "no_such_object" */ + void *sr = NULL; + slapi_pblock_get(pb, SLAPI_SEARCH_RESULT_SET, &sr); +- be->be_search_results_release(&sr); ++ if (be->be_search_results_release != NULL) { ++ be->be_search_results_release(&sr); ++ } + } + pagedresults_set_search_result(pb_conn, operation, NULL, 1, pr_idx); + rc = pagedresults_set_current_be(pb_conn, NULL, pr_idx, 1); +-- +2.49.0 + diff --git a/389-ds-base.spec b/389-ds-base.spec index 8286119..617c251 100644 --- a/389-ds-base.spec +++ b/389-ds-base.spec @@ -47,7 +47,7 @@ ExcludeArch: i686 Summary: 389 Directory Server (base) Name: 389-ds-base Version: 2.7.0 -Release: 4%{?dist} +Release: 5%{?dist} License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (0BSD OR Apache-2.0 OR MIT) AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT OR Zlib) AND (Apache-2.0 OR MIT) AND (MIT OR Apache-2.0) AND Unicode-3.0 AND (MIT OR Unlicense) AND Apache-2.0 AND MIT AND MPL-2.0 AND Zlib URL: https://www.port389.org Conflicts: selinux-policy-base < 3.9.8 @@ -68,16 +68,16 @@ Provides: bundled(crate(base64)) = 0.13.1 Provides: bundled(crate(bitflags)) = 2.9.1 Provides: bundled(crate(byteorder)) = 1.5.0 Provides: bundled(crate(cbindgen)) = 0.26.0 -Provides: bundled(crate(cc)) = 1.2.27 +Provides: bundled(crate(cc)) = 1.2.31 Provides: bundled(crate(cfg-if)) = 1.0.1 Provides: bundled(crate(clap)) = 3.2.25 Provides: bundled(crate(clap_lex)) = 0.2.4 -Provides: bundled(crate(concread)) = 0.5.6 +Provides: bundled(crate(concread)) = 0.5.7 Provides: bundled(crate(crossbeam-epoch)) = 0.9.18 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.12 +Provides: bundled(crate(errno)) = 0.3.13 Provides: bundled(crate(fastrand)) = 2.3.0 Provides: bundled(crate(fernet)) = 0.1.4 Provides: bundled(crate(foldhash)) = 0.1.5 @@ -89,6 +89,7 @@ Provides: bundled(crate(hashbrown)) = 0.15.4 Provides: bundled(crate(heck)) = 0.4.1 Provides: bundled(crate(hermit-abi)) = 0.1.19 Provides: bundled(crate(indexmap)) = 1.9.3 +Provides: bundled(crate(io-uring)) = 0.7.9 Provides: bundled(crate(itoa)) = 1.0.15 Provides: bundled(crate(jobserver)) = 0.1.33 Provides: bundled(crate(libc)) = 0.2.174 @@ -97,6 +98,7 @@ Provides: bundled(crate(log)) = 0.4.27 Provides: bundled(crate(lru)) = 0.13.0 Provides: bundled(crate(memchr)) = 2.7.5 Provides: bundled(crate(miniz_oxide)) = 0.8.9 +Provides: bundled(crate(mio)) = 1.0.4 Provides: bundled(crate(object)) = 0.36.7 Provides: bundled(crate(once_cell)) = 1.21.3 Provides: bundled(crate(openssl)) = 0.10.73 @@ -111,21 +113,22 @@ Provides: bundled(crate(proc-macro-hack)) = 0.5.20+deprecated Provides: bundled(crate(proc-macro2)) = 1.0.95 Provides: bundled(crate(quote)) = 1.0.40 Provides: bundled(crate(r-efi)) = 5.3.0 -Provides: bundled(crate(rustc-demangle)) = 0.1.25 -Provides: bundled(crate(rustix)) = 1.0.7 +Provides: bundled(crate(rustc-demangle)) = 0.1.26 +Provides: bundled(crate(rustix)) = 1.0.8 Provides: bundled(crate(ryu)) = 1.0.20 Provides: bundled(crate(serde)) = 1.0.219 Provides: bundled(crate(serde_derive)) = 1.0.219 -Provides: bundled(crate(serde_json)) = 1.0.140 +Provides: bundled(crate(serde_json)) = 1.0.142 Provides: bundled(crate(shlex)) = 1.3.0 +Provides: bundled(crate(slab)) = 0.4.10 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.103 +Provides: bundled(crate(syn)) = 2.0.104 Provides: bundled(crate(tempfile)) = 3.20.0 Provides: bundled(crate(termcolor)) = 1.4.1 Provides: bundled(crate(textwrap)) = 0.16.2 -Provides: bundled(crate(tokio)) = 1.45.1 +Provides: bundled(crate(tokio)) = 1.47.1 Provides: bundled(crate(toml)) = 0.5.11 Provides: bundled(crate(tracing)) = 0.1.41 Provides: bundled(crate(tracing-attributes)) = 0.1.30 @@ -138,16 +141,17 @@ Provides: bundled(crate(winapi)) = 0.3.9 Provides: bundled(crate(winapi-i686-pc-windows-gnu)) = 0.4.0 Provides: bundled(crate(winapi-util)) = 0.1.9 Provides: bundled(crate(winapi-x86_64-pc-windows-gnu)) = 0.4.0 -Provides: bundled(crate(windows-sys)) = 0.59.0 -Provides: bundled(crate(windows-targets)) = 0.52.6 -Provides: bundled(crate(windows_aarch64_gnullvm)) = 0.52.6 -Provides: bundled(crate(windows_aarch64_msvc)) = 0.52.6 -Provides: bundled(crate(windows_i686_gnu)) = 0.52.6 -Provides: bundled(crate(windows_i686_gnullvm)) = 0.52.6 -Provides: bundled(crate(windows_i686_msvc)) = 0.52.6 -Provides: bundled(crate(windows_x86_64_gnu)) = 0.52.6 -Provides: bundled(crate(windows_x86_64_gnullvm)) = 0.52.6 -Provides: bundled(crate(windows_x86_64_msvc)) = 0.52.6 +Provides: bundled(crate(windows-link)) = 0.1.3 +Provides: bundled(crate(windows-sys)) = 0.60.2 +Provides: bundled(crate(windows-targets)) = 0.53.3 +Provides: bundled(crate(windows_aarch64_gnullvm)) = 0.53.0 +Provides: bundled(crate(windows_aarch64_msvc)) = 0.53.0 +Provides: bundled(crate(windows_i686_gnu)) = 0.53.0 +Provides: bundled(crate(windows_i686_gnullvm)) = 0.53.0 +Provides: bundled(crate(windows_i686_msvc)) = 0.53.0 +Provides: bundled(crate(windows_x86_64_gnu)) = 0.53.0 +Provides: bundled(crate(windows_x86_64_gnullvm)) = 0.53.0 +Provides: bundled(crate(windows_x86_64_msvc)) = 0.53.0 Provides: bundled(crate(wit-bindgen-rt)) = 0.39.0 Provides: bundled(crate(zeroize)) = 1.8.1 Provides: bundled(crate(zeroize_derive)) = 1.4.2 @@ -282,9 +286,38 @@ Source3: https://github.com/jemalloc/%{jemalloc_name}/releases/download %endif Source4: 389-ds-base.sysusers +Source5: vendor-%{version}-1.tar.gz +Source6: Cargo-%{version}-1.lock + Patch: 0001-Issue-6377-syntax-error-in-setup.py-6378.patch Patch: 0002-Issue-6838-lib389-replica.py-is-using-nonexistent-da.patch Patch: 0003-Issue-6680-instance-read-only-mode-is-broken-6681.patch +Patch: 0004-Issue-6825-RootDN-Access-Control-Plugin-with-wildcar.patch +Patch: 0005-Issue-6119-Synchronise-accept_thread-with-slapd_daem.patch +Patch: 0006-Issue-6782-Improve-paged-result-locking.patch +Patch: 0007-Issue-6822-Backend-creation-cleanup-and-Database-UI-.patch +Patch: 0008-Issue-6857-uiduniq-allow-specifying-match-rules-in-t.patch +Patch: 0009-Issue-6756-CLI-UI-Properly-handle-disabled-NDN-cache.patch +Patch: 0010-Issue-6859-str2filter-is-not-fully-applying-matching.patch +Patch: 0011-Issue-6872-compressed-log-rotation-creates-files-wit.patch +Patch: 0012-Issue-6878-Prevent-repeated-disconnect-logs-during-s.patch +Patch: 0013-Issue-6772-dsconf-Replicas-with-the-consumer-role-al.patch +Patch: 0014-Issue-6893-Log-user-that-is-updated-during-password-.patch +Patch: 0015-Issue-6895-Crash-if-repl-keep-alive-entry-can-not-be.patch +Patch: 0016-Issue-6250-Add-test-for-entryUSN-overflow-on-failed-.patch +Patch: 0017-Issue-6594-Add-test-for-numSubordinates-replication-.patch +Patch: 0018-Issue-6884-Mask-password-hashes-in-audit-logs-6885.patch +Patch: 0019-Issue-6897-Fix-disk-monitoring-test-failures-and-imp.patch +Patch: 0020-Issue-6339-Address-Coverity-scan-issues-in-memberof-.patch +Patch: 0021-Issue-6468-CLI-Fix-default-error-log-level.patch +Patch: 0022-Issues-6913-6886-6250-Adjust-xfail-marks-6914.patch +Patch: 0023-Issue-6181-RFE-Allow-system-to-manage-uid-gid-at-sta.patch +Patch: 0024-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch +Patch: 0025-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch +Patch: 0026-Issue-6850-AddressSanitizer-memory-leak-in-mdb_init.patch +Patch: 0027-Issue-6848-AddressSanitizer-leak-in-do_search.patch +Patch: 0028-Issue-6865-AddressSanitizer-leak-in-agmt_update_init.patch +Patch: 0029-Issue-6768-ns-slapd-crashes-when-a-referral-is-added.patch %description 389 Directory Server is an LDAPv3 compliant server. The base package includes @@ -388,6 +421,10 @@ A cockpit UI Plugin for configuring and administering the 389 Directory Server %prep %autosetup -p1 -n %{name}-%{version} +rm -rf vendor +tar xzf %{SOURCE5} +cp %{SOURCE6} src/Cargo.lock + %if %{bundle_jemalloc} %setup -q -n %{name}-%{version} -T -D -b 3 %endif @@ -727,6 +764,18 @@ exit 0 %endif %changelog +* Tue Aug 05 2025 Viktor Ashirov - 2.7.0-5 +- Resolves: RHEL-89762 - dsidm Error: float() argument must be a string or a number, not 'NoneType' [rhel-9] +- Resolves: RHEL-92041 - Memory leak in roles_cache_create_object_from_entry +- Resolves: RHEL-95444 - ns-slapd[xxxx]: segfault at 10d7d0d0 ip 00007ff734050cdb sp 00007ff6de9f1430 error 6 in libslapd.so.0.1.0[7ff733ec0000+1b3000] [rhel-9] +- Resolves: RHEL-104821 - ipa-restore fails to restore SELinux contexts, causes ns-slapd AVC denials on /dev/shm after restore. +- Resolves: RHEL-107005 - Failure to get Server monitoring data when NDN cache is disabled. [rhel-9] +- Resolves: RHEL-107581 - segfault - error 4 in libpthread-2.28.so [rhel-9] +- Resolves: RHEL-107585 - ns-slapd crashed when we add nsslapd-referral [rhel-9] +- Resolves: RHEL-107586 - CWE-284 dirsrv log rotation creates files with world readable permission [rhel-9] +- Resolves: RHEL-107587 - CWE-532 Created user password hash available to see in audit log [rhel-9] +- Resolves: RHEL-107588 - CWE-778 Log doesn't show what user gets password changed by administrator [rhel-9] + * Mon Jul 21 2025 Viktor Ashirov - 2.7.0-4 - Resolves: RHEL-61347 - Directory Server is unavailable after a restart with nsslapd-readonly=on and consumes 100% CPU diff --git a/sources b/sources index 43715c6..90475a6 100644 --- a/sources +++ b/sources @@ -1,2 +1,4 @@ SHA512 (jemalloc-5.3.0.tar.bz2) = 22907bb052096e2caffb6e4e23548aecc5cc9283dce476896a2b1127eee64170e3562fa2e7db9571298814a7a2c7df6e8d1fbe152bd3f3b0c1abec22a2de34b1 SHA512 (389-ds-base-2.7.0.tar.bz2) = 3aa6ea8b2c59c6188ede25a0c027fc32153d5276857699bc43368db1cb747fea09edeff326186d30fda30fe0a98574625ae0907d99065d1868b169256de1b035 +SHA512 (Cargo-2.7.0-1.lock) = ea6db252e49de8aa2fe165f5cc773dc2eb227100d56953a36ca062680a3fc54870a961b05aaac1f7a761c69f3685cc8a7be474ac92377a1219c293fd1117f491 +SHA512 (vendor-2.7.0-1.tar.gz) = dda0afe82289812440c3e89ddd305ba21bf89104aa7debfabd398d9719c8e0b48173f52b487f270b9cc40c0b010dc1e749eb7f3a709074a0d650bea811e0e87a