From 23917c9198c8c968cf07127ed40b0baf535c8342 Mon Sep 17 00:00:00 2001 From: Viktor Ashirov Date: Tue, 5 Aug 2025 18:53:33 +0200 Subject: [PATCH] - Resolves: RHEL-73032 - segfault - error 4 in libpthread-2.28.so - Resolves: RHEL-79079 - Failure to get Server monitoring data when NDN cache is disabled. - Resolves: RHEL-87352 - ns-slapd crashed when we add nsslapd-referral - Resolves: RHEL-92054 - Memory leak in roles_cache_create_object_from_entry [rhel-10] - Resolves: RHEL-107001 - ipa-restore fails to restore SELinux contexts, causes ns-slapd AVC denials on /dev/shm after restore. [rhel-10] - Resolves: RHEL-107028 - CWE-284 dirsrv log rotation creates files with world readable permission - Resolves: RHEL-107035 - CWE-532 Created user password hash available to see in audit log - Resolves: RHEL-107037 - CWE-778 Log doesn't show what user gets password changed by administrator --- ...nd-creation-cleanup-and-Database-UI-.patch | 8 +- ...-6852-Move-ds-CLI-tools-back-to-sbin.patch | 2 +- ...ate-parametrized-docstring-for-tests.patch | 205 ++ ...ue-6782-Improve-paged-result-locking.patch | 127 + ...9-replica.py-is-using-nonexistent-da.patch | 37 + ...dd_exclude_subtree-and-remove_exclud.patch | 515 ++++ ...iq-allow-specifying-match-rules-in-t.patch | 45 + ...I-Properly-handle-disabled-NDN-cache.patch | 1201 +++++++++ ...tor-for-improved-data-management-685.patch | 2237 +++++++++++++++++ ...essSanitizer-memory-leak-in-mdb_init.patch | 65 + ...8-AddressSanitizer-leak-in-do_search.patch | 58 + ...ssSanitizer-leak-in-agmt_update_init.patch | 58 + ...ilter-is-not-fully-applying-matching.patch | 169 ++ ...essed-log-rotation-creates-files-wit.patch | 163 ++ ...nt-repeated-disconnect-logs-during-s.patch | 116 + ...ng-access-JSON-logging-for-TLS-Clien.patch | 590 +++++ ...ate-parametrized-docstring-for-tests.patch | 43 + ...f-Replicas-with-the-consumer-role-al.patch | 67 + ...ser-that-is-updated-during-password-.patch | 360 +++ 0021-Issue-6352-Fix-DeprecationWarning.patch | 37 + ...-6880-Fix-ds_logs-test-suite-failure.patch | 38 + ...01-Update-changelog-trimming-logging.patch | 53 + ...-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 | 1719 +++++++++++++ ...y-leak-in-roles_cache_create_object_.patch | 262 ++ ...e-changelog-trimming-logging-fix-tes.patch | 64 + ...llow-system-to-manage-uid-gid-at-sta.patch | 32 + ...6468-CLI-Fix-default-error-log-level.patch | 31 + ...apd-crashes-when-a-referral-is-added.patch | 97 + ...13-6886-6250-Adjust-xfail-marks-6914.patch | 222 ++ 0035-Issue-6875-Fix-dsidm-tests.patch | 378 +++ ...e-6519-Add-basic-dsidm-account-tests.patch | 503 ++++ 389-ds-base.spec | 73 +- 36 files changed, 10988 insertions(+), 23 deletions(-) create mode 100644 0004-Issue-6829-Update-parametrized-docstring-for-tests.patch create mode 100644 0005-Issue-6782-Improve-paged-result-locking.patch create mode 100644 0006-Issue-6838-lib389-replica.py-is-using-nonexistent-da.patch create mode 100644 0007-Issue-6753-Add-add_exclude_subtree-and-remove_exclud.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-6854-Refactor-for-improved-data-management-685.patch create mode 100644 0011-Issue-6850-AddressSanitizer-memory-leak-in-mdb_init.patch create mode 100644 0012-Issue-6848-AddressSanitizer-leak-in-do_search.patch create mode 100644 0013-Issue-6865-AddressSanitizer-leak-in-agmt_update_init.patch create mode 100644 0014-Issue-6859-str2filter-is-not-fully-applying-matching.patch create mode 100644 0015-Issue-6872-compressed-log-rotation-creates-files-wit.patch create mode 100644 0016-Issue-6878-Prevent-repeated-disconnect-logs-during-s.patch create mode 100644 0017-Issue-6888-Missing-access-JSON-logging-for-TLS-Clien.patch create mode 100644 0018-Issue-6829-Update-parametrized-docstring-for-tests.patch create mode 100644 0019-Issue-6772-dsconf-Replicas-with-the-consumer-role-al.patch create mode 100644 0020-Issue-6893-Log-user-that-is-updated-during-password-.patch create mode 100644 0021-Issue-6352-Fix-DeprecationWarning.patch create mode 100644 0022-Issue-6880-Fix-ds_logs-test-suite-failure.patch create mode 100644 0023-Issue-6901-Update-changelog-trimming-logging.patch create mode 100644 0024-Issue-6895-Crash-if-repl-keep-alive-entry-can-not-be.patch create mode 100644 0025-Issue-6250-Add-test-for-entryUSN-overflow-on-failed-.patch create mode 100644 0026-Issue-6594-Add-test-for-numSubordinates-replication-.patch create mode 100644 0027-Issue-6884-Mask-password-hashes-in-audit-logs-6885.patch create mode 100644 0028-Issue-6897-Fix-disk-monitoring-test-failures-and-imp.patch create mode 100644 0029-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch create mode 100644 0030-Issue-6901-Update-changelog-trimming-logging-fix-tes.patch create mode 100644 0031-Issue-6181-RFE-Allow-system-to-manage-uid-gid-at-sta.patch create mode 100644 0032-Issue-6468-CLI-Fix-default-error-log-level.patch create mode 100644 0033-Issue-6768-ns-slapd-crashes-when-a-referral-is-added.patch create mode 100644 0034-Issues-6913-6886-6250-Adjust-xfail-marks-6914.patch create mode 100644 0035-Issue-6875-Fix-dsidm-tests.patch create mode 100644 0036-Issue-6519-Add-basic-dsidm-account-tests.patch diff --git a/0001-Issue-6822-Backend-creation-cleanup-and-Database-UI-.patch b/0001-Issue-6822-Backend-creation-cleanup-and-Database-UI-.patch index 9be30bc..ce7a603 100644 --- a/0001-Issue-6822-Backend-creation-cleanup-and-Database-UI-.patch +++ b/0001-Issue-6822-Backend-creation-cleanup-and-Database-UI-.patch @@ -20,7 +20,7 @@ Reviewed by: @mreynolds389 (Thanks!) 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 c0c4be4144..276125dfc2 100644 +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 { @@ -157,7 +157,7 @@ index c0c4be4144..276125dfc2 100644 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 ad48d1f874..91a8e3e375 100644 +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 { @@ -354,7 +354,7 @@ index ad48d1f874..91a8e3e375 100644 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 fa492fd2a6..aa535bfc7f 100644 +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 { @@ -442,7 +442,7 @@ index fa492fd2a6..aa535bfc7f 100644 .fail(err => { // changelog failure diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py -index 1d000ed668..53f15b6b03 100644 +index 1d000ed66..53f15b6b0 100644 --- a/src/lib389/lib389/backend.py +++ b/src/lib389/lib389/backend.py @@ -694,24 +694,32 @@ class Backend(DSLdapObject): diff --git a/0002-Issue-6852-Move-ds-CLI-tools-back-to-sbin.patch b/0002-Issue-6852-Move-ds-CLI-tools-back-to-sbin.patch index 893096e..1ee668a 100644 --- a/0002-Issue-6852-Move-ds-CLI-tools-back-to-sbin.patch +++ b/0002-Issue-6852-Move-ds-CLI-tools-back-to-sbin.patch @@ -19,7 +19,7 @@ Reviewed by: @droideck (Thanks!) 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in -index 2f1df63c9f..101771574a 100644 +index 2f1df63c9..101771574 100644 --- a/rpm/389-ds-base.spec.in +++ b/rpm/389-ds-base.spec.in @@ -486,6 +486,11 @@ cp -r %{_builddir}/%{name}-%{version}/man/man3 $RPM_BUILD_ROOT/%{_mandir}/man3 diff --git a/0004-Issue-6829-Update-parametrized-docstring-for-tests.patch b/0004-Issue-6829-Update-parametrized-docstring-for-tests.patch new file mode 100644 index 0000000..65130e1 --- /dev/null +++ b/0004-Issue-6829-Update-parametrized-docstring-for-tests.patch @@ -0,0 +1,205 @@ +From 23e0b93c3bbe96a365357a3af11bc86172102c05 Mon Sep 17 00:00:00 2001 +From: Barbora Simonova +Date: Wed, 25 Jun 2025 10:43:37 +0200 +Subject: [PATCH] Issue 6829 - Update parametrized docstring for tests + +Description: +Update missing parametrized value in docstring for some tests + +Fixes: https://github.com/389ds/389-ds-base/issues/6829 + +Reviewed by: @vashirov (Thanks!) +--- + dirsrvtests/tests/suites/basic/basic_test.py | 2 +- + dirsrvtests/tests/suites/clu/dsconf_config_test.py | 8 ++++++++ + dirsrvtests/tests/suites/clu/schema_test.py | 5 +++++ + dirsrvtests/tests/suites/mapping_tree/regression_test.py | 1 + + dirsrvtests/tests/suites/password/password_test.py | 1 + + .../tests/suites/replication/regression_m2_test.py | 1 + + dirsrvtests/tests/suites/vlv/regression_test.py | 2 ++ + 7 files changed, 19 insertions(+), 1 deletion(-) + +diff --git a/dirsrvtests/tests/suites/basic/basic_test.py b/dirsrvtests/tests/suites/basic/basic_test.py +index 8f5de91aa..db6bfab56 100644 +--- a/dirsrvtests/tests/suites/basic/basic_test.py ++++ b/dirsrvtests/tests/suites/basic/basic_test.py +@@ -961,7 +961,7 @@ def test_basic_search_lookthroughlimit(topology_st, limit, resp, import_example_ + Tests normal search with lookthroughlimit set high and low. + + :id: b5119970-6c9f-41b7-9649-de9233226fec +- ++ :parametrized: yes + :setup: Standalone instance, add example.ldif to the database, search filter (uid=*). + + :steps: +diff --git a/dirsrvtests/tests/suites/clu/dsconf_config_test.py b/dirsrvtests/tests/suites/clu/dsconf_config_test.py +index d67679adf..232018097 100644 +--- a/dirsrvtests/tests/suites/clu/dsconf_config_test.py ++++ b/dirsrvtests/tests/suites/clu/dsconf_config_test.py +@@ -58,6 +58,7 @@ def test_single_value_add(topology_st, attr_name, values_dict): + """Test adding a single value to an attribute + + :id: ffc912a6-c188-413d-9c35-7f4b3774d946 ++ :parametrized: yes + :setup: Standalone DS instance + :steps: + 1. Add a single value to the specified attribute +@@ -89,6 +90,7 @@ def test_single_value_replace(topology_st, attr_name, values_dict): + """Test replacing a single value in configuration attributes + + :id: 112e3e5e-8db8-4974-9ea4-ed789c2d02f2 ++ :parametrized: yes + :setup: Standalone DS instance + :steps: + 1. Add initial value to the specified attribute +@@ -127,6 +129,7 @@ def test_multi_value_batch_add(topology_st, attr_name, values_dict): + """Test adding multiple values in a single batch command + + :id: 4c34c7f8-16cc-4ab6-938a-967537be5470 ++ :parametrized: yes + :setup: Standalone DS instance + :steps: + 1. Add multiple values to the attribute in a single command +@@ -157,6 +160,7 @@ def test_multi_value_batch_replace(topology_st, attr_name, values_dict): + """Test replacing with multiple values in a single batch command + + :id: 05cf28b8-000e-4856-a10b-7e1df012737d ++ :parametrized: yes + :setup: Standalone DS instance + :steps: + 1. Add initial single value +@@ -194,6 +198,7 @@ def test_multi_value_specific_delete(topology_st, attr_name, values_dict): + """Test deleting specific values from multi-valued attribute + + :id: bb325c9a-eae8-438a-b577-bd63540b91cb ++ :parametrized: yes + :setup: Standalone DS instance + :steps: + 1. Add multiple values to the attribute +@@ -232,6 +237,7 @@ def test_multi_value_batch_delete(topology_st, attr_name, values_dict): + """Test deleting multiple values in a single batch command + + :id: 4b105824-b060-4f83-97d7-001a01dba1a5 ++ :parametrized: yes + :setup: Standalone DS instance + :steps: + 1. Add multiple values to the attribute +@@ -269,6 +275,7 @@ def test_single_value_persists_after_restart(topology_st, attr_name, values_dict + """Test single value persists after server restart + + :id: be1a7e3d-a9ca-48a1-a3bc-062990d4f3e9 ++ :parametrized: yes + :setup: Standalone DS instance + :steps: + 1. Add single value to the attribute +@@ -310,6 +317,7 @@ def test_multi_value_batch_persists_after_restart(topology_st, attr_name, values + """Test multiple values added in batch persist after server restart + + :id: fd0435e2-90b1-465a-8968-d3a375c8fb22 ++ :parametrized: yes + :setup: Standalone DS instance + :steps: + 1. Add multiple values in a single batch command +diff --git a/dirsrvtests/tests/suites/clu/schema_test.py b/dirsrvtests/tests/suites/clu/schema_test.py +index 19ec032bc..5709471cf 100644 +--- a/dirsrvtests/tests/suites/clu/schema_test.py ++++ b/dirsrvtests/tests/suites/clu/schema_test.py +@@ -100,6 +100,7 @@ def test_origins(create_attribute): + """Test the various possibilities of x-origin + + :id: 3229f6f8-67c1-4558-9be5-71434283086a ++ :parametrized: yes + :setup: Standalone Instance + :steps: + 1. Add an attribute with different x-origin values/types +@@ -116,6 +117,7 @@ def test_mrs(create_attribute): + """Test an attribute can be added with a matching rule + + :id: e4eb06e0-7f80-41fe-8868-08c2bafc7590 ++ :parametrized: yes + :setup: Standalone Instance + :steps: + 1. Add an attribute with a matching rule +@@ -132,6 +134,7 @@ def test_edit_attributetype(create_attribute): + """Test editing an attribute type in the schema + + :id: 07c98f6a-89f8-44e5-9cc1-353d1f7bccf4 ++ :parametrized: yes + :setup: Standalone Instance + :steps: + 1. Add a new attribute type +@@ -209,6 +212,7 @@ def test_edit_attributetype_remove_superior(create_attribute): + """Test editing an attribute type to remove a parameter from it + + :id: bd6ae89f-9617-4620-adc2-465884ca568b ++ :parametrized: yes + :setup: Standalone Instance + :steps: + 1. Add a new attribute type with a superior +@@ -244,6 +248,7 @@ def test_edit_attribute_keep_custom_values(create_attribute): + """Test editing a custom schema attribute keeps all custom values + + :id: 5b1e2e8b-28c2-4f77-9c03-07eff20f763d ++ :parametrized: yes + :setup: Standalone Instance + :steps: + 1. Create a custom attribute +diff --git a/dirsrvtests/tests/suites/mapping_tree/regression_test.py b/dirsrvtests/tests/suites/mapping_tree/regression_test.py +index 51c687059..2c57c2973 100644 +--- a/dirsrvtests/tests/suites/mapping_tree/regression_test.py ++++ b/dirsrvtests/tests/suites/mapping_tree/regression_test.py +@@ -111,6 +111,7 @@ def test_sub_suffixes(topo, orphan_param): + """ check the entries found on suffix/sub-suffix + + :id: 5b4421c2-d851-11ec-a760-482ae39447e5 ++ :parametrized: yes + :feature: mapping-tree + :setup: Standalone instance with 3 additional backends: + dc=parent, dc=child1,dc=parent, dc=childr21,dc=parent +diff --git a/dirsrvtests/tests/suites/password/password_test.py b/dirsrvtests/tests/suites/password/password_test.py +index 2d4aef028..94a23e669 100644 +--- a/dirsrvtests/tests/suites/password/password_test.py ++++ b/dirsrvtests/tests/suites/password/password_test.py +@@ -156,6 +156,7 @@ def test_pwd_scheme_no_upgrade_on_bind(topology_st, crypt_scheme, request, no_up + the current hash is in nsslapd-scheme-list-no-upgrade-hash + + :id: b4d2c525-a239-4ca6-a168-5126da7abedd ++ :parametrized: yes + :setup: Standalone instance + :steps: + 1. Create a user with userpassword stored as CRYPT +diff --git a/dirsrvtests/tests/suites/replication/regression_m2_test.py b/dirsrvtests/tests/suites/replication/regression_m2_test.py +index 10a5fa419..68966ac49 100644 +--- a/dirsrvtests/tests/suites/replication/regression_m2_test.py ++++ b/dirsrvtests/tests/suites/replication/regression_m2_test.py +@@ -991,6 +991,7 @@ def test_change_repl_passwd(topo_m2, request, bind_cn): + Testing when agmt bind group are used. + + :id: a305913a-cc76-11ec-b324-482ae39447e5 ++ :parametrized: yes + :setup: 2 Supplier Instances + :steps: + 1. Insure agmt from supplier1 to supplier2 is properly set to use bind group +diff --git a/dirsrvtests/tests/suites/vlv/regression_test.py b/dirsrvtests/tests/suites/vlv/regression_test.py +index d9d1cb444..f7847ac74 100644 +--- a/dirsrvtests/tests/suites/vlv/regression_test.py ++++ b/dirsrvtests/tests/suites/vlv/regression_test.py +@@ -775,6 +775,7 @@ def test_vlv_reindex(topology_st, prefix, basedn): + """Test VLV reindexing. + + :id: d5dc0d8e-cbe6-11ee-95b1-482ae39447e5 ++ :parametrized: yes + :setup: Standalone instance. + :steps: + 1. Cleanup leftover from previous tests +@@ -830,6 +831,7 @@ def test_vlv_offline_import(topology_st, prefix, basedn): + """Test VLV after off line import. + + :id: 8732d7a8-e851-11ee-9d63-482ae39447e5 ++ :parametrized: yes + :setup: Standalone instance. + :steps: + 1. Cleanup leftover from previous tests +-- +2.49.0 + diff --git a/0005-Issue-6782-Improve-paged-result-locking.patch b/0005-Issue-6782-Improve-paged-result-locking.patch new file mode 100644 index 0000000..c058b68 --- /dev/null +++ b/0005-Issue-6782-Improve-paged-result-locking.patch @@ -0,0 +1,127 @@ +From 4f81f696e85dc7c50555df2d410222214c8ac883 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 5ea919e2d..545518748 100644 +--- a/ldap/servers/slapd/opshared.c ++++ b/ldap/servers/slapd/opshared.c +@@ -619,6 +619,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); + } + +@@ -744,11 +752,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/0006-Issue-6838-lib389-replica.py-is-using-nonexistent-da.patch b/0006-Issue-6838-lib389-replica.py-is-using-nonexistent-da.patch new file mode 100644 index 0000000..cb5c3ba --- /dev/null +++ b/0006-Issue-6838-lib389-replica.py-is-using-nonexistent-da.patch @@ -0,0 +1,37 @@ +From 0ab37e0848e6f1c4e46068bee46bd91c3bb3d22d Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Tue, 1 Jul 2025 12:44:04 +0200 +Subject: [PATCH] Issue 6838 - lib389/replica.py is using nonexistent + datetime.UTC in Python 3.9 + +Bug Description: +389-ds-base-2.x is supposed to be used with Python 3.9. +But lib389/replica.py is using `datetime.UTC`, which is an alias +to `datetime.timezone.utc` was added only in Python 3.11. + +Fix Description: +Use `datetime.timezone.utc` instead. + +Fixes: https://github.com/389ds/389-ds-base/issues/6838 + +Reviewed by: @mreynolds389 (Thanks!) +--- + src/lib389/lib389/replica.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py +index 18ce1b1d5..59be00a33 100644 +--- a/src/lib389/lib389/replica.py ++++ b/src/lib389/lib389/replica.py +@@ -917,7 +917,7 @@ class RUV(object): + ValueError("Wrong CSN value was supplied") + + timestamp = int(csn[:8], 16) +- time_str = datetime.datetime.fromtimestamp(timestamp, datetime.UTC).strftime('%Y-%m-%d %H:%M:%S') ++ time_str = datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S') + # We are parsing shorter CSN which contains only timestamp + if len(csn) == 8: + return time_str +-- +2.49.0 + diff --git a/0007-Issue-6753-Add-add_exclude_subtree-and-remove_exclud.patch b/0007-Issue-6753-Add-add_exclude_subtree-and-remove_exclud.patch new file mode 100644 index 0000000..bea64d1 --- /dev/null +++ b/0007-Issue-6753-Add-add_exclude_subtree-and-remove_exclud.patch @@ -0,0 +1,515 @@ +From 8984550568737142dd22020e1b9efd87cc0e42f8 Mon Sep 17 00:00:00 2001 +From: Lenka Doudova +Date: Mon, 9 Jun 2025 15:15:04 +0200 +Subject: [PATCH] Issue 6753 - Add 'add_exclude_subtree' and + 'remove_exclude_subtree' methods to Attribute uniqueness plugin + +Description: +Adding 'add_exclude_subtree' and 'remove_exclude_subtree' methods to AttributeUniquenessPlugin in +order to be able to easily add or remove an exclude subtree. +Porting ticket 47927 test to +dirsrvtests/tests/suites/plugins/attruniq_test.py + +Relates: #6753 + +Author: Lenka Doudova + +Reviewers: Simon Pichugin, Mark Reynolds +--- + .../tests/suites/plugins/attruniq_test.py | 171 +++++++++++ + dirsrvtests/tests/tickets/ticket47927_test.py | 267 ------------------ + src/lib389/lib389/plugins.py | 10 + + 3 files changed, 181 insertions(+), 267 deletions(-) + delete mode 100644 dirsrvtests/tests/tickets/ticket47927_test.py + +diff --git a/dirsrvtests/tests/suites/plugins/attruniq_test.py b/dirsrvtests/tests/suites/plugins/attruniq_test.py +index c1ccad9ae..aac659c29 100644 +--- a/dirsrvtests/tests/suites/plugins/attruniq_test.py ++++ b/dirsrvtests/tests/suites/plugins/attruniq_test.py +@@ -10,6 +10,7 @@ import pytest + import ldap + import logging + from lib389.plugins import AttributeUniquenessPlugin ++from lib389.idm.nscontainer import nsContainers + from lib389.idm.user import UserAccounts + from lib389.idm.group import Groups + from lib389._constants import DEFAULT_SUFFIX +@@ -22,6 +23,19 @@ log = logging.getLogger(__name__) + MAIL_ATTR_VALUE = 'non-uniq@value.net' + MAIL_ATTR_VALUE_ALT = 'alt-mail@value.net' + ++EXCLUDED_CONTAINER_CN = "excluded_container" ++EXCLUDED_CONTAINER_DN = "cn={},{}".format(EXCLUDED_CONTAINER_CN, DEFAULT_SUFFIX) ++ ++EXCLUDED_BIS_CONTAINER_CN = "excluded_bis_container" ++EXCLUDED_BIS_CONTAINER_DN = "cn={},{}".format(EXCLUDED_BIS_CONTAINER_CN, DEFAULT_SUFFIX) ++ ++ENFORCED_CONTAINER_CN = "enforced_container" ++ ++USER_1_CN = "test_1" ++USER_2_CN = "test_2" ++USER_3_CN = "test_3" ++USER_4_CN = "test_4" ++ + + def test_modrdn_attr_uniqueness(topology_st): + """Test that we can not add two entries that have the same attr value that is +@@ -154,3 +168,160 @@ def test_multiple_attr_uniqueness(topology_st): + 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() +\ No newline at end of file +diff --git a/dirsrvtests/tests/tickets/ticket47927_test.py b/dirsrvtests/tests/tickets/ticket47927_test.py +deleted file mode 100644 +index 887fe1af4..000000000 +--- a/dirsrvtests/tests/tickets/ticket47927_test.py ++++ /dev/null +@@ -1,267 +0,0 @@ +-# --- BEGIN COPYRIGHT BLOCK --- +-# Copyright (C) 2016 Red Hat, Inc. +-# All rights reserved. +-# +-# License: GPL (version 3 or any later version). +-# See LICENSE for details. +-# --- END COPYRIGHT BLOCK --- +-# +-import pytest +-from lib389.tasks import * +-from lib389.utils import * +-from lib389.topologies import topology_st +- +-from lib389._constants import SUFFIX, DEFAULT_SUFFIX, PLUGIN_ATTR_UNIQUENESS +- +-# Skip on older versions +-pytestmark = [pytest.mark.tier2, +- pytest.mark.skipif(ds_is_older('1.3.4'), reason="Not implemented")] +- +-logging.getLogger(__name__).setLevel(logging.DEBUG) +-log = logging.getLogger(__name__) +- +-EXCLUDED_CONTAINER_CN = "excluded_container" +-EXCLUDED_CONTAINER_DN = "cn=%s,%s" % (EXCLUDED_CONTAINER_CN, SUFFIX) +- +-EXCLUDED_BIS_CONTAINER_CN = "excluded_bis_container" +-EXCLUDED_BIS_CONTAINER_DN = "cn=%s,%s" % (EXCLUDED_BIS_CONTAINER_CN, SUFFIX) +- +-ENFORCED_CONTAINER_CN = "enforced_container" +-ENFORCED_CONTAINER_DN = "cn=%s,%s" % (ENFORCED_CONTAINER_CN, SUFFIX) +- +-USER_1_CN = "test_1" +-USER_1_DN = "cn=%s,%s" % (USER_1_CN, ENFORCED_CONTAINER_DN) +-USER_2_CN = "test_2" +-USER_2_DN = "cn=%s,%s" % (USER_2_CN, ENFORCED_CONTAINER_DN) +-USER_3_CN = "test_3" +-USER_3_DN = "cn=%s,%s" % (USER_3_CN, EXCLUDED_CONTAINER_DN) +-USER_4_CN = "test_4" +-USER_4_DN = "cn=%s,%s" % (USER_4_CN, EXCLUDED_BIS_CONTAINER_DN) +- +- +-def test_ticket47927_init(topology_st): +- topology_st.standalone.plugins.enable(name=PLUGIN_ATTR_UNIQUENESS) +- try: +- topology_st.standalone.modify_s('cn=' + PLUGIN_ATTR_UNIQUENESS + ',cn=plugins,cn=config', +- [(ldap.MOD_REPLACE, 'uniqueness-attribute-name', b'telephonenumber'), +- (ldap.MOD_REPLACE, 'uniqueness-subtrees', ensure_bytes(DEFAULT_SUFFIX)), +- ]) +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927: Failed to configure plugin for "telephonenumber": error ' + e.args[0]['desc']) +- assert False +- topology_st.standalone.restart(timeout=120) +- +- topology_st.standalone.add_s(Entry((EXCLUDED_CONTAINER_DN, {'objectclass': "top nscontainer".split(), +- 'cn': EXCLUDED_CONTAINER_CN}))) +- topology_st.standalone.add_s(Entry((EXCLUDED_BIS_CONTAINER_DN, {'objectclass': "top nscontainer".split(), +- 'cn': EXCLUDED_BIS_CONTAINER_CN}))) +- topology_st.standalone.add_s(Entry((ENFORCED_CONTAINER_DN, {'objectclass': "top nscontainer".split(), +- 'cn': ENFORCED_CONTAINER_CN}))) +- +- # adding an entry on a stage with a different 'cn' +- topology_st.standalone.add_s(Entry((USER_1_DN, { +- 'objectclass': "top person".split(), +- 'sn': USER_1_CN, +- 'cn': USER_1_CN}))) +- # adding an entry on a stage with a different 'cn' +- topology_st.standalone.add_s(Entry((USER_2_DN, { +- 'objectclass': "top person".split(), +- 'sn': USER_2_CN, +- 'cn': USER_2_CN}))) +- topology_st.standalone.add_s(Entry((USER_3_DN, { +- 'objectclass': "top person".split(), +- 'sn': USER_3_CN, +- 'cn': USER_3_CN}))) +- topology_st.standalone.add_s(Entry((USER_4_DN, { +- 'objectclass': "top person".split(), +- 'sn': USER_4_CN, +- 'cn': USER_4_CN}))) +- +- +-def test_ticket47927_one(topology_st): +- ''' +- Check that uniqueness is enforce on all SUFFIX +- ''' +- UNIQUE_VALUE = b'1234' +- try: +- topology_st.standalone.modify_s(USER_1_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_one: Failed to set the telephonenumber for %s: %s' % (USER_1_DN, e.args[0]['desc'])) +- assert False +- +- # we expect to fail because user1 is in the scope of the plugin +- try: +- topology_st.standalone.modify_s(USER_2_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- log.fatal('test_ticket47927_one: unexpected success to set the telephonenumber for %s' % (USER_2_DN)) +- assert False +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_one: Failed (expected) to set the telephonenumber for %s: %s' % ( +- USER_2_DN, e.args[0]['desc'])) +- pass +- +- # we expect to fail because user1 is in the scope of the plugin +- try: +- topology_st.standalone.modify_s(USER_3_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- log.fatal('test_ticket47927_one: unexpected success to set the telephonenumber for %s' % (USER_3_DN)) +- assert False +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_one: Failed (expected) to set the telephonenumber for %s: %s' % ( +- USER_3_DN, e.args[0]['desc'])) +- pass +- +- +-def test_ticket47927_two(topology_st): +- ''' +- Exclude the EXCLUDED_CONTAINER_DN from the uniqueness plugin +- ''' +- try: +- topology_st.standalone.modify_s('cn=' + PLUGIN_ATTR_UNIQUENESS + ',cn=plugins,cn=config', +- [(ldap.MOD_REPLACE, 'uniqueness-exclude-subtrees', ensure_bytes(EXCLUDED_CONTAINER_DN))]) +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_two: Failed to configure plugin for to exclude %s: error %s' % ( +- EXCLUDED_CONTAINER_DN, e.args[0]['desc'])) +- assert False +- topology_st.standalone.restart(timeout=120) +- +- +-def test_ticket47927_three(topology_st): +- ''' +- Check that uniqueness is enforced on full SUFFIX except EXCLUDED_CONTAINER_DN +- First case: it exists an entry (with the same attribute value) in the scope +- of the plugin and we set the value in an entry that is in an excluded scope +- ''' +- UNIQUE_VALUE = b'9876' +- try: +- topology_st.standalone.modify_s(USER_1_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_three: Failed to set the telephonenumber ' + e.args[0]['desc']) +- assert False +- +- # we should not be allowed to set this value (because user1 is in the scope) +- try: +- topology_st.standalone.modify_s(USER_2_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- log.fatal('test_ticket47927_three: unexpected success to set the telephonenumber for %s' % (USER_2_DN)) +- assert False +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_three: Failed (expected) to set the telephonenumber for %s: %s' % ( +- USER_2_DN, e.args[0]['desc'])) +- +- # USER_3_DN is in EXCLUDED_CONTAINER_DN so update should be successful +- try: +- topology_st.standalone.modify_s(USER_3_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- log.fatal('test_ticket47927_three: success to set the telephonenumber for %s' % (USER_3_DN)) +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_three: Failed (unexpected) to set the telephonenumber for %s: %s' % ( +- USER_3_DN, e.args[0]['desc'])) +- assert False +- +- +-def test_ticket47927_four(topology_st): +- ''' +- Check that uniqueness is enforced on full SUFFIX except EXCLUDED_CONTAINER_DN +- Second case: it exists an entry (with the same attribute value) in an excluded scope +- of the plugin and we set the value in an entry is in the scope +- ''' +- UNIQUE_VALUE = b'1111' +- # USER_3_DN is in EXCLUDED_CONTAINER_DN so update should be successful +- try: +- topology_st.standalone.modify_s(USER_3_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- log.fatal('test_ticket47927_four: success to set the telephonenumber for %s' % USER_3_DN) +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_four: Failed (unexpected) to set the telephonenumber for %s: %s' % ( +- USER_3_DN, e.args[0]['desc'])) +- assert False +- +- # we should be allowed to set this value (because user3 is excluded from scope) +- try: +- topology_st.standalone.modify_s(USER_1_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- except ldap.LDAPError as e: +- log.fatal( +- 'test_ticket47927_four: Failed to set the telephonenumber for %s: %s' % (USER_1_DN, e.args[0]['desc'])) +- assert False +- +- # we should not be allowed to set this value (because user1 is in the scope) +- try: +- topology_st.standalone.modify_s(USER_2_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- log.fatal('test_ticket47927_four: unexpected success to set the telephonenumber %s' % USER_2_DN) +- assert False +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_four: Failed (expected) to set the telephonenumber for %s: %s' % ( +- USER_2_DN, e.args[0]['desc'])) +- pass +- +- +-def test_ticket47927_five(topology_st): +- ''' +- Exclude the EXCLUDED_BIS_CONTAINER_DN from the uniqueness plugin +- ''' +- try: +- topology_st.standalone.modify_s('cn=' + PLUGIN_ATTR_UNIQUENESS + ',cn=plugins,cn=config', +- [(ldap.MOD_ADD, 'uniqueness-exclude-subtrees', ensure_bytes(EXCLUDED_BIS_CONTAINER_DN))]) +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_five: Failed to configure plugin for to exclude %s: error %s' % ( +- EXCLUDED_BIS_CONTAINER_DN, e.args[0]['desc'])) +- assert False +- topology_st.standalone.restart(timeout=120) +- topology_st.standalone.getEntry('cn=' + PLUGIN_ATTR_UNIQUENESS + ',cn=plugins,cn=config', ldap.SCOPE_BASE) +- +- +-def test_ticket47927_six(topology_st): +- ''' +- Check that uniqueness is enforced on full SUFFIX except EXCLUDED_CONTAINER_DN +- and EXCLUDED_BIS_CONTAINER_DN +- First case: it exists an entry (with the same attribute value) in the scope +- of the plugin and we set the value in an entry that is in an excluded scope +- ''' +- UNIQUE_VALUE = b'222' +- try: +- topology_st.standalone.modify_s(USER_1_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_six: Failed to set the telephonenumber ' + e.args[0]['desc']) +- assert False +- +- # we should not be allowed to set this value (because user1 is in the scope) +- try: +- topology_st.standalone.modify_s(USER_2_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- log.fatal('test_ticket47927_six: unexpected success to set the telephonenumber for %s' % (USER_2_DN)) +- assert False +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_six: Failed (expected) to set the telephonenumber for %s: %s' % ( +- USER_2_DN, e.args[0]['desc'])) +- +- # USER_3_DN is in EXCLUDED_CONTAINER_DN so update should be successful +- try: +- topology_st.standalone.modify_s(USER_3_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- log.fatal('test_ticket47927_six: success to set the telephonenumber for %s' % (USER_3_DN)) +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_six: Failed (unexpected) to set the telephonenumber for %s: %s' % ( +- USER_3_DN, e.args[0]['desc'])) +- assert False +- # USER_4_DN is in EXCLUDED_CONTAINER_DN so update should be successful +- try: +- topology_st.standalone.modify_s(USER_4_DN, +- [(ldap.MOD_REPLACE, 'telephonenumber', UNIQUE_VALUE)]) +- log.fatal('test_ticket47927_six: success to set the telephonenumber for %s' % (USER_4_DN)) +- except ldap.LDAPError as e: +- log.fatal('test_ticket47927_six: Failed (unexpected) to set the telephonenumber for %s: %s' % ( +- USER_4_DN, e.args[0]['desc'])) +- assert False +- +- +-if __name__ == '__main__': +- # Run isolated +- # -s for DEBUG mode +- CURRENT_FILE = os.path.realpath(__file__) +- pytest.main("-s %s" % CURRENT_FILE) +diff --git a/src/lib389/lib389/plugins.py b/src/lib389/lib389/plugins.py +index 31bbfa502..977091726 100644 +--- a/src/lib389/lib389/plugins.py ++++ b/src/lib389/lib389/plugins.py +@@ -175,6 +175,16 @@ class AttributeUniquenessPlugin(Plugin): + + self.set('uniqueness-across-all-subtrees', 'off') + ++ def add_exclude_subtree(self, basedn): ++ """Add a uniqueness-exclude-subtrees attribute""" ++ ++ self.add('uniqueness-exclude-subtrees', basedn) ++ ++ def remove_exclude_subtree(self, basedn): ++ """Remove a uniqueness-exclude-subtrees attribute""" ++ ++ self.remove('uniqueness-exclude-subtrees', basedn) ++ + + class AttributeUniquenessPlugins(DSLdapObjects): + """A DSLdapObjects entity which represents Attribute Uniqueness plugin instances +-- +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..0687001 --- /dev/null +++ b/0008-Issue-6857-uiduniq-allow-specifying-match-rules-in-t.patch @@ -0,0 +1,45 @@ +From 4be22be50dfdf0a5ddd27dc8f9d9618b941c8be8 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..f81127f --- /dev/null +++ b/0009-Issue-6756-CLI-UI-Properly-handle-disabled-NDN-cache.patch @@ -0,0 +1,1201 @@ +From efa21c3d7bdc7369cae184b9f66e667ee981a4d5 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 5eeaca162..bf57690c4 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 * +@@ -272,6 +273,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-6854-Refactor-for-improved-data-management-685.patch b/0010-Issue-6854-Refactor-for-improved-data-management-685.patch new file mode 100644 index 0000000..a6e3049 --- /dev/null +++ b/0010-Issue-6854-Refactor-for-improved-data-management-685.patch @@ -0,0 +1,2237 @@ +From c70c4ec35df7434135baa85de04356e1e15e7d92 Mon Sep 17 00:00:00 2001 +From: James Chapman +Date: Fri, 11 Jul 2025 10:16:13 +0000 +Subject: [PATCH] Issue 6854 - Refactor for improved data management (#6855) + +Description: Replaced standard dictionaries with defaultdict and +introduced @dataclass structures to streamline data handling. +Improved reliability and readability by reducing explicit key +checks and default initialisation logic. Simplified nested +structure management and ensured consistency across components +such as ResultData, BindData, and ConnectionData. + +Fixes: https://github.com/389ds/389-ds-base/issues/6854 + +Reviewed by: @mreynolds389 (Thank you) +--- + ldap/admin/src/logconv.py | 1307 ++++++++++++++++++++----------------- + 1 file changed, 698 insertions(+), 609 deletions(-) + +diff --git a/ldap/admin/src/logconv.py b/ldap/admin/src/logconv.py +index f4495ca35..162447b4d 100755 +--- a/ldap/admin/src/logconv.py ++++ b/ldap/admin/src/logconv.py +@@ -16,11 +16,11 @@ import argparse + import logging + import sys + import csv ++from collections import defaultdict, Counter + from datetime import datetime, timedelta, timezone ++from dataclasses import dataclass, field + import heapq +-from collections import Counter +-from collections import defaultdict +-from typing import Optional ++from typing import Optional, Dict, List, Set, Tuple, DefaultDict + import magic + + # Globals +@@ -159,9 +159,191 @@ SCOPE_LABEL = { + + STLS_OID = '1.3.6.1.4.1.1466.20037' + +-# Version +-logAnalyzerVersion = "8.3" ++logAnalyzerVersion = "8.4" + ++@dataclass ++class VLVData: ++ counters: Dict[str, int] = field(default_factory=lambda: defaultdict( ++ int, ++ { ++ 'vlv': 0 ++ } ++ )) ++ ++ rst_con_op_map: Dict = field(default_factory=lambda: defaultdict(dict)) ++ ++@dataclass ++class ServerData: ++ counters: Dict[str, int] = field(default_factory=lambda: defaultdict( ++ int, ++ { ++ 'restart': 0, ++ 'lines_parsed': 0 ++ } ++ )) ++ ++ first_time: Optional[str] = None ++ last_time: Optional[str] = None ++ parse_start_time: Optional[str] = None ++ parse_stop_time: Optional[str] = None ++ ++@dataclass ++class OperationData: ++ counters: DefaultDict[str, int] = field(default_factory=lambda: defaultdict( ++ int, ++ { ++ 'add': 0, ++ 'mod': 0, ++ 'del': 0, ++ 'modrdn': 0, ++ 'cmp': 0, ++ 'abandon': 0, ++ 'sort': 0, ++ 'internal': 0, ++ 'extnd': 0, ++ 'authzid': 0, ++ 'total': 0 ++ } ++ )) ++ ++ rst_con_op_map: DefaultDict[str, DefaultDict[str, int]] = field( ++ default_factory=lambda: defaultdict(lambda: defaultdict(int)) ++ ) ++ ++ extended: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ ++@dataclass ++class ConnectionData: ++ counters: DefaultDict[str, int] = field(default_factory=lambda: defaultdict( ++ int, ++ { ++ 'conn': 0, ++ 'fd_taken': 0, ++ 'fd_returned': 0, ++ 'fd_max': 0, ++ 'sim_conn': 0, ++ 'max_sim_conn': 0, ++ 'ldap': 0, ++ 'ldapi': 0, ++ 'ldaps': 0 ++ } ++ )) ++ ++ start_time: DefaultDict[str, str] = field(default_factory=lambda: defaultdict(str)) ++ open_conns: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ exclude_ip: DefaultDict[Tuple[str, str], str] = field(default_factory=lambda: defaultdict(str)) ++ ++ broken_pipe: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ resource_unavail: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ connection_reset: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ disconnect_code: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ ++ restart_conn_disconnect_map: DefaultDict[Tuple[int, str], int] = field(default_factory=lambda: defaultdict(int)) ++ restart_conn_ip_map: Dict[Tuple[int, str], str] = field(default_factory=dict) ++ ++ src_ip_map: DefaultDict[str, DefaultDict[str, object]] = field( ++ default_factory=lambda: defaultdict(lambda: defaultdict(object)) ++ ) ++ ++@dataclass ++class BindData: ++ counters: DefaultDict[str, int] = field(default_factory=lambda: defaultdict( ++ int, ++ { ++ 'bind': 0, ++ 'unbind': 0, ++ 'sasl': 0, ++ 'anon': 0, ++ 'autobind': 0, ++ 'rootdn': 0 ++ } ++ )) ++ ++ restart_conn_dn_map: Dict[Tuple[int, str], str] = field(default_factory=dict) ++ ++ version: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ dns: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ sasl_mech: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ conn_op_sasl_mech_map: DefaultDict[str, str] = field(default_factory=lambda: defaultdict(str)) ++ root_dn: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ ++ report_dn: DefaultDict[str, Dict[str, Set[str]]] = field( ++ default_factory=lambda: defaultdict( ++ lambda: { ++ 'conn': set(), ++ 'ips': set() ++ } ++ ) ++ ) ++ ++@dataclass ++class ResultData: ++ counters: DefaultDict[str, int] = field(default_factory=lambda: defaultdict( ++ int, { ++ 'result': 0, ++ 'notesA': 0, ++ 'notesF': 0, ++ 'notesM': 0, ++ 'notesP': 0, ++ 'notesU': 0, ++ 'timestamp': 0, ++ 'entry': 0, ++ 'referral': 0 ++ } ++ )) ++ ++ notes: DefaultDict[str, Dict] = field(default_factory=lambda: defaultdict(dict)) ++ ++ timestamp_ctr: int = 0 ++ entry_count: int = 0 ++ referral_count: int = 0 ++ ++ total_etime: float = 0.0 ++ total_wtime: float = 0.0 ++ total_optime: float = 0.0 ++ etime_stat: float = 0.0 ++ ++ etime_duration: List[float] = field(default_factory=list) ++ wtime_duration: List[float] = field(default_factory=list) ++ optime_duration: List[float] = field(default_factory=list) ++ ++ nentries_num: List[int] = field(default_factory=list) ++ nentries_set: Set[int] = field(default_factory=set) ++ ++ error_freq: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ bad_pwd_map: Dict[str, int] = field(default_factory=dict) ++ ++@dataclass ++class SearchData: ++ counters: Dict[str, int] = field(default_factory=lambda: defaultdict( ++ int, ++ { ++ 'search': 0, ++ 'base_search': 0, ++ 'persistent': 0 ++ } ++ )) ++ attrs: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ bases: DefaultDict[str, int] = field(default_factory=lambda: defaultdict(int)) ++ ++ base_rst_con_op_map: Dict[Tuple[int, str, str], str] = field(default_factory=dict) ++ scope_rst_con_op_map: Dict[Tuple[int, str, str], str] = field(default_factory=dict) ++ ++ filter_dict: Dict[str, int] = field(default_factory=dict) ++ filter_list: List[str] = field(default_factory=list) ++ filter_rst_con_op_map: Dict[Tuple[int, str, str], str] = field(default_factory=dict) ++ ++@dataclass ++class AuthData: ++ counters: Dict[str, int] = field(default_factory=lambda: defaultdict( ++ int, ++ { ++ 'ssl_client_bind_ctr': 0, ++ 'ssl_client_bind_failed_ctr': 0, ++ 'cipher_ctr': 0 ++ } ++ )) ++ auth_info: DefaultDict[str, str] = field(default_factory=lambda: defaultdict(str)) + + class logAnalyser: + """ +@@ -272,136 +454,14 @@ class logAnalyser: + self.notesP = {} + self.notesU = {} + +- self.vlv = { +- 'vlv_ctr': 0, +- 'vlv_map_rco': {} +- } +- +- self.server = { +- 'restart_ctr': 0, +- 'first_time': None, +- 'last_time': None, +- 'parse_start_time': None, +- 'parse_stop_time': None, +- 'lines_parsed': 0 +- } +- +- self.operation = { +- 'all_op_ctr': 0, +- 'add_op_ctr': 0, +- 'mod_op_ctr': 0, +- 'del_op_ctr': 0, +- 'modrdn_op_ctr': 0, +- 'cmp_op_ctr': 0, +- 'abandon_op_ctr': 0, +- 'sort_op_ctr': 0, +- 'extnd_op_ctr': 0, +- 'add_map_rco': {}, +- 'mod_map_rco': {}, +- 'del_map_rco': {}, +- 'cmp_map_rco': {}, +- 'modrdn_map_rco': {}, +- 'extop_dict': {}, +- 'extop_map_rco': {}, +- 'abandoned_map_rco': {} +- } +- +- self.connection = { +- 'conn_ctr': 0, +- 'fd_taken_ctr': 0, +- 'fd_returned_ctr': 0, +- 'fd_max_ctr': 0, +- 'sim_conn_ctr': 0, +- 'max_sim_conn_ctr': 0, +- 'ldap_ctr': 0, +- 'ldapi_ctr': 0, +- 'ldaps_ctr': 0, +- 'start_time': {}, +- 'open_conns': {}, +- 'exclude_ip_map': {}, +- 'broken_pipe': {}, +- 'resource_unavail': {}, +- 'connection_reset': {}, +- 'disconnect_code': {}, +- 'disconnect_code_map': {}, +- 'ip_map': {}, +- 'restart_conn_ip_map': {} +- } +- +- self.bind = { +- 'bind_ctr': 0, +- 'unbind_ctr': 0, +- 'sasl_bind_ctr': 0, +- 'anon_bind_ctr': 0, +- 'autobind_ctr': 0, +- 'rootdn_bind_ctr': 0, +- 'version': {}, +- 'dn_freq': {}, +- 'dn_map_rc': {}, +- 'sasl_mech_freq': {}, +- 'sasl_map_co': {}, +- 'root_dn': {}, +- 'report_dn': defaultdict(lambda: defaultdict(int, conn=set(), ips=set())) +- } +- +- self.result = { +- 'result_ctr': 0, +- 'notesA_ctr': 0, # dynamically referenced +- 'notesF_ctr': 0, # dynamically referenced +- 'notesM_ctr': 0, # dynamically referenced +- 'notesP_ctr': 0, # dynamically referenced +- 'notesU_ctr': 0, # dynamically referenced +- 'timestamp_ctr': 0, +- 'entry_count': 0, +- 'referral_count': 0, +- 'total_etime': 0.0, +- 'total_wtime': 0.0, +- 'total_optime': 0.0, +- 'notesA_map': {}, +- 'notesF_map': {}, +- 'notesM_map': {}, +- 'notesP_map': {}, +- 'notesU_map': {}, +- 'etime_stat': 0.0, +- 'etime_counts': defaultdict(int), +- 'etime_freq': [], +- 'etime_duration': [], +- 'wtime_counts': defaultdict(int), +- 'wtime_freq': [], +- 'wtime_duration': [], +- 'optime_counts': defaultdict(int), +- 'optime_freq': [], +- 'optime_duration': [], +- 'nentries_dict': defaultdict(int), +- 'nentries_num': [], +- 'nentries_set': set(), +- 'nentries_returned': [], +- 'error_freq': defaultdict(str), +- 'bad_pwd_map': {} +- } +- +- self.search = { +- 'search_ctr': 0, +- 'search_map_rco': {}, +- 'attr_dict': defaultdict(int), +- 'base_search_ctr': 0, +- 'base_map': {}, +- 'base_map_rco': {}, +- 'scope_map_rco': {}, +- 'filter_dict': {}, +- 'filter_list': [], +- 'filter_seen': set(), +- 'filter_counter': Counter(), +- 'filter_map_rco': {}, +- 'persistent_ctr': 0 +- } +- +- self.auth = { +- 'ssl_client_bind_ctr': 0, +- 'ssl_client_bind_failed_ctr': 0, +- 'cipher_ctr': 0, +- 'auth_info': {} +- } ++ self.vlv = VLVData() ++ self.server = ServerData() ++ self.operation = OperationData() ++ self.connection = ConnectionData() ++ self.bind = BindData() ++ self.result = ResultData() ++ self.search = SearchData() ++ self.auth = AuthData() + + def _init_regexes(self): + """ +@@ -564,22 +624,22 @@ class logAnalyser: + """ + print("\nBind Report") + print("====================================================================\n") +- for k, v in self.bind['report_dn'].items(): ++ for k, v in self.bind.report_dn.items(): + print(f"\nBind DN: {k}") + print("--------------------------------------------------------------------\n") + print(" Client Addresses:\n") +- ips = self.bind['report_dn'][k].get('ips', set()) ++ ips = self.bind.report_dn[k].get('ips', set()) + for i, ip in enumerate(ips, start=1): + print(f" {i}: {ip}") + print("\n Operations Performed:\n") +- print(f" Binds: {self.bind['report_dn'][k].get('bind', 0)}") +- print(f" Searches: {self.bind['report_dn'][k].get('srch', 0)}") +- print(f" Modifies: {self.bind['report_dn'][k].get('mod', 0)}") +- print(f" Adds: {self.bind['report_dn'][k].get('add', 0)}") +- print(f" Deletes: {self.bind['report_dn'][k].get('del', 0)}") +- print(f" Compares: {self.bind['report_dn'][k].get('cmp', 0)}") +- print(f" ModRDNs: {self.bind['report_dn'][k].get('modrdn', 0)}") +- print(f" Ext Ops: {self.bind['report_dn'][k].get('ext', 0)}") ++ print(f" Binds: {self.bind.report_dn[k].get('bind', 0)}") ++ print(f" Searches: {self.bind.report_dn[k].get('srch', 0)}") ++ print(f" Modifies: {self.bind.report_dn[k].get('mod', 0)}") ++ print(f" Adds: {self.bind.report_dn[k].get('add', 0)}") ++ print(f" Deletes: {self.bind.report_dn[k].get('del', 0)}") ++ print(f" Compares: {self.bind.report_dn[k].get('cmp', 0)}") ++ print(f" ModRDNs: {self.bind.report_dn[k].get('modrdn', 0)}") ++ print(f" Ext Ops: {self.bind.report_dn[k].get('ext', 0)}") + + print("Done.") + +@@ -618,12 +678,9 @@ class logAnalyser: + self.logger.error(f"Converting timestamp: {timestamp} to datetime failed with: {e}") + return False + +- # Add server restart count to groups for connection tracking +- groups['restart_ctr'] = self.server.get('restart_ctr', 0) +- + # Are there time range restrictions +- parse_start = self.server.get('parse_start_time', None) +- parse_stop = self.server.get('parse_stop_time', None) ++ parse_start = self.server.parse_start_time ++ parse_stop = self.server.parse_stop_time + + if parse_start and parse_stop: + if parse_start.microsecond == 0 and parse_stop.microsecond == 0: +@@ -634,12 +691,12 @@ class logAnalyser: + return False + + # Get the first and last timestamps +- if self.server.get('first_time') is None: +- self.server['first_time'] = timestamp +- self.server['last_time'] = timestamp ++ if self.server.first_time is None: ++ self.server.first_time = timestamp ++ self.server.last_time = timestamp + + # Bump lines parsed +- self.server['lines_parsed'] = self.server.get('lines_parsed', 0) + 1 ++ self.server.counters['lines_parsed'] += 1 + + # Call the associated method for this match + action(groups) +@@ -658,16 +715,11 @@ class logAnalyser: + """ + Process and update statistics based on the parsed result group. + +- Args: +- groups (dict): Parsed groups from the log line. +- +- + Args: + groups (dict): A dictionary containing operation information. Expected keys: + - 'timestamp': The timestamp of the connection event. + - 'conn_id': Connection identifier. + - 'op_id': Operation identifier. +- - 'restart_ctr': Server restart count. + - 'etime': Result elapsed time. + - 'wtime': Result wait time. + - 'optime': Result operation time. +@@ -679,182 +731,173 @@ class logAnalyser: + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ self.logger.debug(f"_process_result_stats - Start - {groups}") ++ + try: + timestamp = groups.get('timestamp') + conn_id = groups.get('conn_id') + op_id = groups.get('op_id') +- restart_ctr = groups.get('restart_ctr') + etime = float(groups.get('etime')) + wtime = float(groups.get('wtime')) + optime = float(groups.get('optime')) ++ nentries = int(groups.get('nentries')) + tag = groups.get('tag') + err = groups.get('err') +- nentries = int(groups.get('nentries')) + internal = groups.get('internal') ++ notes = groups.get('notes') + except KeyError as e: + self.logger.error(f"Missing key in groups: {e}") + return + + # Mapping keys for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_op_key = (restart_ctr, conn_id, op_id) + restart_conn_key = (restart_ctr, conn_id) + conn_op_key = (conn_id, op_id) + + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + +- # Bump global result count +- self.result['result_ctr'] = self.result.get('result_ctr', 0) + 1 +- +- # Bump global result count +- self.result['timestamp_ctr'] = self.result.get('timestamp_ctr', 0) + 1 ++ self.result.counters['result'] += 1 ++ self.result.counters['timestamp'] += 1 + + # Longest etime, push current etime onto the heap +- heapq.heappush(self.result['etime_duration'], float(etime)) ++ heapq.heappush(self.result.etime_duration, etime) + + # If the heap exceeds size_limit, pop the smallest element from root +- if len(self.result['etime_duration']) > self.size_limit: +- heapq.heappop(self.result['etime_duration']) ++ if len(self.result.etime_duration) > self.size_limit: ++ heapq.heappop(self.result.etime_duration) + + # Longest wtime, push current wtime onto the heap +- heapq.heappush(self.result['wtime_duration'], float(wtime)) ++ heapq.heappush(self.result.wtime_duration, wtime) + + # If the heap exceeds size_limit, pop the smallest element from root +- if len(self.result['wtime_duration']) > self.size_limit: +- heapq.heappop(self.result['wtime_duration']) ++ if len(self.result.wtime_duration) > self.size_limit: ++ heapq.heappop(self.result.wtime_duration) + + # Longest optime, push current optime onto the heap +- heapq.heappush(self.result['optime_duration'], float(optime)) ++ heapq.heappush(self.result.optime_duration, optime) + + # If the heap exceeds size_limit, pop the smallest element from root +- if len(self.result['optime_duration']) > self.size_limit: +- heapq.heappop(self.result['optime_duration']) ++ if len(self.result.optime_duration) > self.size_limit: ++ heapq.heappop(self.result.optime_duration) + + # Total result times +- self.result['total_etime'] = self.result.get('total_etime', 0) + float(etime) +- self.result['total_wtime'] = self.result.get('total_wtime', 0) + float(wtime) +- self.result['total_optime'] = self.result.get('total_optime', 0) + float(optime) ++ self.result.total_etime = self.result.total_etime + etime ++ self.result.total_wtime = self.result.total_wtime + wtime ++ self.result.total_optime = self.result.total_optime + optime + + # Statistic reporting +- self.result['etime_stat'] = round(self.result['etime_stat'] + float(etime), 8) ++ self.result.etime_stat = round(self.result.etime_stat + float(etime), 8) + +- if err: +- # Capture error code +- self.result['error_freq'][err] = self.result['error_freq'].get(err, 0) + 1 ++ if err is not None: ++ self.result.error_freq[err] += 1 + + # Check for internal operations based on either conn_id or internal flag + if 'Internal' in conn_id or internal: +- self.server['internal_op_ctr'] = self.server.get('internal_op_ctr', 0) + 1 ++ self.operation.counters['internal'] +=1 + + # Process result notes if present +- notes = groups['notes'] +- if notes is not None: +- # match.group('notes') can be A|U|F +- self.result[f'notes{notes}_ctr'] = self.result.get(f'notes{notes}_ctr', 0) + 1 +- # Track result times using server restart count, conn id and op_id as key +- self.result[f'notes{notes}_map'][restart_conn_op_key] = restart_conn_op_key +- +- # Construct the notes dict +- note_dict = getattr(self, f'notes{notes}') ++ NOTE_TYPES = {'A', 'U', 'F', 'M', 'P'} ++ if notes in NOTE_TYPES: ++ note_dict = self.result.notes[notes] ++ self.result.counters[f'notes{notes}'] += 1 + + # Exclude VLV +- if restart_conn_op_key not in self.vlv['vlv_map_rco']: +- if restart_conn_op_key in note_dict: +- note_dict[restart_conn_op_key]['time'] = timestamp +- else: +- # First time round +- note_dict[restart_conn_op_key] = {'time': timestamp} +- +- note_dict[restart_conn_op_key]['etime'] = etime +- note_dict[restart_conn_op_key]['nentries'] = nentries +- note_dict[restart_conn_op_key]['ip'] = ( +- self.connection['restart_conn_ip_map'].get(restart_conn_key, '') +- ) +- +- if restart_conn_op_key in self.search['base_map_rco']: +- note_dict[restart_conn_op_key]['base'] = self.search['base_map_rco'][restart_conn_op_key] +- del self.search['base_map_rco'][restart_conn_op_key] +- +- if restart_conn_op_key in self.search['scope_map_rco']: +- note_dict[restart_conn_op_key]['scope'] = self.search['scope_map_rco'][restart_conn_op_key] +- del self.search['scope_map_rco'][restart_conn_op_key] +- +- if restart_conn_op_key in self.search['filter_map_rco']: +- note_dict[restart_conn_op_key]['filter'] = self.search['filter_map_rco'][restart_conn_op_key] +- del self.search['filter_map_rco'][restart_conn_op_key] +- +- note_dict[restart_conn_op_key]['bind_dn'] = self.bind['dn_map_rc'].get(restart_conn_key, '') +- +- elif restart_conn_op_key in self.vlv['vlv_map_rco']: +- # This "note" result is VLV, assign the note type for later filtering +- self.vlv['vlv_map_rco'][restart_conn_op_key] = notes ++ if restart_conn_op_key not in self.vlv.rst_con_op_map: ++ # Construct the notes dict ++ note_dict = self.result.notes[notes] ++ note_entry = note_dict.setdefault(restart_conn_op_key, {}) ++ note_entry.update({ ++ 'time': timestamp, ++ 'etime': etime, ++ 'nentries': nentries, ++ 'ip': self.connection.restart_conn_ip_map.get(restart_conn_key, 'Unknown IP'), ++ 'bind_dn': self.bind.restart_conn_dn_map.get(restart_conn_key, 'Unknown DN') ++ }) ++ ++ if restart_conn_op_key in self.search.base_rst_con_op_map: ++ note_dict[restart_conn_op_key]['base'] = self.search.base_rst_con_op_map[restart_conn_op_key] ++ del self.search.base_rst_con_op_map[restart_conn_op_key] ++ ++ if restart_conn_op_key in self.search.scope_rst_con_op_map: ++ note_dict[restart_conn_op_key]['scope'] = self.search.scope_rst_con_op_map[restart_conn_op_key] ++ del self.search.scope_rst_con_op_map[restart_conn_op_key] ++ ++ if restart_conn_op_key in self.search.filter_rst_con_op_map: ++ note_dict[restart_conn_op_key]['filter'] = self.search.filter_rst_con_op_map[restart_conn_op_key] ++ del self.search.filter_rst_con_op_map[restart_conn_op_key] ++ else: ++ self.vlv.rst_con_op_map[restart_conn_op_key] = notes + + # Trim the search data we dont need (not associated with a notes=X) +- if restart_conn_op_key in self.search['base_map_rco']: +- del self.search['base_map_rco'][restart_conn_op_key] ++ if restart_conn_op_key in self.search.base_rst_con_op_map: ++ del self.search.base_rst_con_op_map[restart_conn_op_key] + +- if restart_conn_op_key in self.search['scope_map_rco']: +- del self.search['scope_map_rco'][restart_conn_op_key] ++ if restart_conn_op_key in self.search.scope_rst_con_op_map: ++ del self.search.scope_rst_con_op_map[restart_conn_op_key] + +- if restart_conn_op_key in self.search['filter_map_rco']: +- del self.search['filter_map_rco'][restart_conn_op_key] ++ if restart_conn_op_key in self.search.filter_rst_con_op_map: ++ del self.search.filter_rst_con_op_map[restart_conn_op_key] + + # Process bind response based on the tag and error code. + if tag == '97': + # Invalid credentials|Entry does not exist + if err == '49': + # if self.verbose: +- bad_pwd_dn = self.bind['dn_map_rc'].get(restart_conn_key, None) +- bad_pwd_ip = self.connection['restart_conn_ip_map'].get(restart_conn_key, None) +- self.result['bad_pwd_map'][(bad_pwd_dn, bad_pwd_ip)] = ( +- self.result['bad_pwd_map'].get((bad_pwd_dn, bad_pwd_ip), 0) + 1 ++ bad_pwd_dn = self.bind.restart_conn_dn_map[restart_conn_key] ++ bad_pwd_ip = self.connection.restart_conn_ip_map.get(restart_conn_key, None) ++ self.result.bad_pwd_map[(bad_pwd_dn, bad_pwd_ip)] = ( ++ self.result.bad_pwd_map.get((bad_pwd_dn, bad_pwd_ip), 0) + 1 + ) + # Trim items to size_limit +- if len(self.result['bad_pwd_map']) > self.size_limit: ++ if len(self.result.bad_pwd_map) > self.size_limit: + within_size_limit = dict( + sorted( +- self.result['bad_pwd_map'].items(), ++ self.result.bad_pwd_map.items(), + key=lambda item: item[1], + reverse=True + )[:self.size_limit]) +- self.result['bad_pwd_map'] = within_size_limit ++ self.result.bad_pwd_map = within_size_limit + + # Ths result is involved in the SASL bind process, decrement bind count, etc + elif err == '14': +- self.bind['bind_ctr'] = self.bind.get('bind_ctr', 0) - 1 +- self.operation['all_op_ctr'] = self.operation.get('all_op_ctr', 0) - 1 +- self.bind['sasl_bind_ctr'] = self.bind.get('sasl_bind_ctr', 0) - 1 +- self.bind['version']['3'] = self.bind['version'].get('3', 0) - 1 ++ self.bind.counters['bind'] -= 1 ++ self.operation.counters['total'] -= 1 ++ self.bind.counters['sasl'] -= 1 ++ self.bind.version['3'] = self.bind.version.get('3', 0) - 1 + + # Drop the sasl mech count also +- mech = self.bind['sasl_map_co'].get(conn_op_key, 0) ++ mech = self.bind.conn_op_sasl_mech_map[conn_op_key] + if mech: +- self.bind['sasl_mech_freq'][mech] = self.bind['sasl_mech_freq'].get(mech, 0) - 1 ++ self.bind.sasl_mech[mech] -= 1 + # Is this is a result to a sasl bind + else: + result_dn = groups['dn'] + if result_dn: + if result_dn != "": + # If this is a result of a sasl bind, grab the dn +- if conn_op_key in self.bind['sasl_map_co']: ++ if conn_op_key in self.bind.conn_op_sasl_mech_map: + if result_dn is not None: +- self.bind['dn_map_rc'][restart_conn_key] = result_dn.lower() +- self.bind['dn_freq'][result_dn] = ( +- self.bind['dn_freq'].get(result_dn, 0) + 1 ++ self.bind.restart_conn_dn_map[restart_conn_key] = result_dn.lower() ++ self.bind.dns[result_dn] = ( ++ self.bind.dns.get(result_dn, 0) + 1 + ) + # Handle other tag values + elif tag in ['100', '101', '111', '115']: + + # Largest nentry, push current nentry onto the heap, no duplicates +- if int(nentries) not in self.result['nentries_set']: +- heapq.heappush(self.result['nentries_num'], int(nentries)) +- self.result['nentries_set'].add(int(nentries)) ++ if int(nentries) not in self.result.nentries_set: ++ heapq.heappush(self.result.nentries_num, int(nentries)) ++ self.result.nentries_set.add(int(nentries)) + + # If the heap exceeds size_limit, pop the smallest element from root +- if len(self.result['nentries_num']) > self.size_limit: +- removed = heapq.heappop(self.result['nentries_num']) +- self.result['nentries_set'].remove(removed) ++ if len(self.result.nentries_num) > self.size_limit: ++ removed = heapq.heappop(self.result.nentries_num) ++ self.result.nentries_set.remove(removed) ++ ++ self.logger.debug(f"_process_result_stats - End") + + def _process_search_stats(self, groups: dict): + """ +@@ -873,10 +916,11 @@ class logAnalyser: + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ self.logger.debug(f"_process_search_stats - Start - {groups}") ++ + try: + conn_id = groups.get('conn_id') + op_id = groups.get('op_id') +- restart_ctr = groups.get('restart_ctr') + search_base = groups['search_base'] + search_scope = groups['search_scope'] + search_attrs = groups['search_attrs'] +@@ -886,33 +930,34 @@ class logAnalyser: + return + + # Create a tracking keys for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_op_key = (restart_ctr, conn_id, op_id) + restart_conn_key = (restart_ctr, conn_id) + + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + + # Bump search and global op count +- self.search['search_ctr'] = self.search.get('search_ctr', 0) + 1 +- self.operation['all_op_ctr'] = self.operation.get('all_op_ctr', 0) + 1 ++ self.search.counters['search'] += 1 ++ self.operation.counters['total'] += 1 + + # Search attributes + if search_attrs is not None: + if search_attrs == 'ALL': +- self.search['attr_dict']['All Attributes'] += 1 ++ self.search.attrs['All Attributes'] += 1 + else: + for attr in search_attrs.split(): + attr = attr.strip('"') +- self.search['attr_dict'][attr] += 1 ++ self.search.attrs[attr] += 1 + + # If the associated conn id for the bind DN matches update op counter +- for dn in self.bind['report_dn']: +- conns = self.bind['report_dn'][dn]['conn'] ++ for dn in self.bind.report_dn: ++ conns = self.bind.report_dn[dn]['conn'] + if conn_id in conns: + bind_dn_key = self._report_dn_key(dn, self.report_dn) + if bind_dn_key: +- self.bind['report_dn'][bind_dn_key]['srch'] = self.bind['report_dn'][bind_dn_key].get('srch', 0) + 1 ++ self.bind.report_dn[bind_dn_key]['srch'] = self.bind.report_dn[bind_dn_key].get('srch', 0) + 1 + + # Search base + if search_base is not None: +@@ -924,49 +969,51 @@ class logAnalyser: + search_base = base.lower() + if search_base: + if self.verbose: +- self.search['base_map'][search_base] = self.search['base_map'].get(search_base, 0) + 1 +- self.search['base_map_rco'][restart_conn_op_key] = search_base ++ self.search.bases[search_base] += 1#self.search.bases.get(search_base, 0) + 1 ++ self.search.base_rst_con_op_map[restart_conn_op_key] = search_base + + # Search scope + if search_scope is not None: + if self.verbose: +- self.search['scope_map_rco'][restart_conn_op_key] = SCOPE_LABEL[int(search_scope)] ++ self.search.scope_rst_con_op_map[restart_conn_op_key] = SCOPE_LABEL[int(search_scope)] + + # Search filter + if search_filter is not None: + if self.verbose: +- self.search['filter_map_rco'][restart_conn_op_key] = search_filter +- self.search['filter_dict'][search_filter] = self.search['filter_dict'].get(search_filter, 0) + 1 ++ self.search.filter_rst_con_op_map[restart_conn_op_key] = search_filter ++ self.search.filter_dict[search_filter] = self.search.filter_dict.get(search_filter, 0) + 1 + + found = False +- for idx, (count, filter) in enumerate(self.search['filter_list']): ++ for idx, (count, filter) in enumerate(self.search.filter_list): + if filter == search_filter: + found = True +- self.search['filter_list'][idx] = (self.search['filter_dict'][search_filter] + 1, search_filter) +- heapq.heapify(self.search['filter_list']) ++ self.search.filter_list[idx] = (self.search.filter_dict[search_filter] + 1, search_filter) ++ heapq.heapify(self.search.filter_list) + break + + if not found: +- if len(self.search['filter_list']) < self.size_limit: +- heapq.heappush(self.search['filter_list'], (1, search_filter)) ++ if len(self.search.filter_list) < self.size_limit: ++ heapq.heappush(self.search.filter_list, (1, search_filter)) + else: +- heapq.heappushpop(self.search['filter_list'], (self.search['filter_dict'][search_filter], search_filter)) ++ heapq.heappushpop(self.search.filter_list, (self.search.filter_dict[search_filter], search_filter)) + + # Check for an entire base search + if "objectclass=*" in search_filter.lower() or "objectclass=top" in search_filter.lower(): + if search_scope == '2': +- self.search['base_search_ctr'] = self.search.get('base_search_ctr', 0) + 1 ++ self.search.counters['base_search'] += 1 + + # Persistent search + if groups['options'] is not None: + options = groups['options'] + if options == 'persistent': +- self.search['persistent_ctr'] = self.search.get('persistent_ctr', 0) + 1 ++ self.search.counters['persistent'] += 1 + + # Authorization identity + if groups['authzid_dn'] is not None: + self.search['authzid'] = self.search.get('authzid', 0) + 1 + ++ self.logger.debug(f"_process_search_stats - End") ++ + def _process_bind_stats(self, groups: dict): + """ + Process and update statistics based on the parsed result group. +@@ -975,7 +1022,6 @@ class logAnalyser: + groups (dict): A dictionary containing operation information. Expected keys: + - 'conn_id': Connection identifier. + - 'op_id': Operation identifier. +- - 'restart_ctr': Server restart count. + - 'bind_dn': Bind DN. + - 'bind_method': Bind method (sasl, simple). + - 'bind_version': Bind version. +@@ -983,80 +1029,74 @@ class logAnalyser: + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ self.logger.debug(f"_process_bind_stats - Start - {groups}") ++ + try: + conn_id = groups.get('conn_id') + op_id = groups.get('op_id') +- restart_ctr = groups.get('restart_ctr') + bind_dn = groups.get('bind_dn') +- bind_method = groups['bind_method'] +- bind_version = groups['bind_version'] ++ bind_method = groups.get('bind_method') ++ bind_version = groups.get('bind_version') + except KeyError as e: + self.logger.error(f"Missing key in groups: {e}") + return + +- # If this is the first connection (indicating a server restart), increment restart counter +- if conn_id == '1': +- self.server['restart_ctr'] = self.server.get('restart_ctr', 0) + 1 +- + # Create a tracking keys for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_key = (restart_ctr, conn_id) + conn_op_key = (conn_id, op_id) + ++ if bind_dn.strip() == '': ++ bind_dn = 'Anonymous' ++ bind_dn_normalised = bind_dn.lower() if bind_dn != 'Anonymous' else 'anonymous' ++ + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + +- # Bump bind and global op count +- self.bind['bind_ctr'] = self.bind.get('bind_ctr', 0) + 1 +- self.operation['all_op_ctr'] = self.operation.get('all_op_ctr', 0) + 1 ++ # Update counters ++ self.bind.counters['bind'] += 1 ++ self.operation.counters['total'] = self.operation.counters['total'] + 1 ++ self.bind.version[bind_version] += 1 + +- # Update bind version count +- self.bind['version'][bind_version] = self.bind['version'].get(bind_version, 0) + 1 +- if bind_dn == "": +- bind_dn = 'Anonymous' + + # If we need to report on this DN, capture some info for tracking + bind_dn_key = self._report_dn_key(bind_dn, self.report_dn) + if bind_dn_key: +- # Update bind count +- self.bind['report_dn'][bind_dn_key]['bind'] = self.bind['report_dn'][bind_dn_key].get('bind', 0) + 1 +- # Connection ID +- self.bind['report_dn'][bind_dn_key]['conn'].add(conn_id) ++ self.bind.report_dn[bind_dn_key]['bind'] = self.bind.report_dn[bind_dn_key].get('bind', 0) + 1 ++ self.bind.report_dn[bind_dn_key]['conn'].add(conn_id) ++ + # Loop over IPs captured at connection time to find the associated IP +- for (ip, ip_info) in self.connection['ip_map'].items(): ++ for (ip, ip_info) in self.connection.src_ip_map.items(): + if restart_conn_key in ip_info['keys']: +- self.bind['report_dn'][bind_dn_key]['ips'].add(ip) ++ self.bind.report_dn[bind_dn_key]['ips'].add(ip) + +- # sasl or simple bind ++ # Handle SASL or simple bind + if bind_method == 'sasl': +- self.bind['sasl_bind_ctr'] = self.bind.get('sasl_bind_ctr', 0) + 1 ++ self.bind.counters['sasl'] += 1 + sasl_mech = groups['sasl_mech'] +- if sasl_mech is not None: +- # Bump sasl mechanism count +- self.bind['sasl_mech_freq'][sasl_mech] = self.bind['sasl_mech_freq'].get(sasl_mech, 0) + 1 ++ if sasl_mech: ++ self.bind.sasl_mech[sasl_mech] += 1 ++ self.bind.conn_op_sasl_mech_map[conn_op_key] = sasl_mech + +- # Keep track of bind key to handle sasl result later +- self.bind['sasl_map_co'][conn_op_key] = sasl_mech ++ if bind_dn_normalised == self.root_dn.casefold(): ++ self.bind.counters['rootdn'] += 1 + + if bind_dn != "Anonymous": +- if bind_dn.casefold() == self.root_dn.casefold(): +- self.bind['rootdn_bind_ctr'] = self.bind.get('rootdn_bind_ctr', 0) + 1 ++ self.bind.dns[bind_dn] += 1 + +- # if self.verbose: +- self.bind['dn_freq'][bind_dn] = self.bind['dn_freq'].get(bind_dn, 0) + 1 +- self.bind['dn_map_rc'][restart_conn_key] = bind_dn.lower() + else: + if bind_dn == "Anonymous": +- self.bind['anon_bind_ctr'] = self.bind.get('anon_bind_ctr', 0) + 1 +- self.bind['dn_freq']['Anonymous'] = self.bind['dn_freq'].get('Anonymous', 0) + 1 +- self.bind['dn_map_rc'][restart_conn_key] = "anonymous" ++ self.bind.counters['anon'] += 1 ++ self.bind.dns['Anonymous'] += 1 + else: + if bind_dn.casefold() == self.root_dn.casefold(): +- self.bind['rootdn_bind_ctr'] = self.bind.get('rootdn_bind_ctr', 0) + 1 ++ self.bind.counters['rootdn'] += 1 ++ self.bind.dns[bind_dn] += 1 + +- # if self.verbose: +- self.bind['dn_freq'][bind_dn] = self.bind['dn_freq'].get(bind_dn, 0) + 1 +- self.bind['dn_map_rc'][restart_conn_key] = bind_dn.lower() ++ self.bind.restart_conn_dn_map[restart_conn_key] = bind_dn_normalised ++ ++ self.logger.debug(f"_process_bind_stats - End") + + def _process_unbind_stats(self, groups: dict): + """ +@@ -1065,27 +1105,31 @@ class logAnalyser: + Args: + groups (dict): A dictionary containing operation information. Expected keys: + - 'conn_id': Connection identifier. +- - 'restart_ctr': Server restart count. + + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ ++ self.logger.debug(f"_process_unbind_stats - Start - {groups}") ++ + try: + conn_id = groups.get('conn_id') +- restart_ctr = groups.get('restart_ctr') + except KeyError as e: + self.logger.error(f"Missing key in groups: {e}") + return + + # Create a tracking key for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_key = (restart_ctr, conn_id) + + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + + # Bump unbind count +- self.bind['unbind_ctr'] = self.bind.get('unbind_ctr', 0) + 1 ++ self.bind.counters['unbind'] += 1 ++ ++ self.logger.debug(f"_process_unbind_stats - End") + + def _process_connect_stats(self, groups: dict): + """ +@@ -1094,15 +1138,18 @@ class logAnalyser: + Args: + groups (dict): A dictionary containing operation information. Expected keys: + - 'conn_id': Connection identifier. +- - 'restart_ctr': Server restart count. ++ - 'src_ip': Source IP address. ++ - 'fd': File descriptor. ++ - 'ssl': LDAPS. + + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ ++ self.logger.debug(f"_process_connect_stats - Start - {groups}") ++ + try: +- timestamp = groups.get('timestamp') + conn_id = groups.get('conn_id') +- restart_ctr = groups.get('restart_ctr') + src_ip = groups.get('src_ip') + fd = groups['fd'] + ssl = groups['ssl'] +@@ -1110,61 +1157,63 @@ class logAnalyser: + self.logger.error(f"Missing key in groups: {e}") + return + ++ # If conn=1, server has started a new lifecycle ++ if conn_id == '1': ++ self.server.counters['restart'] += 1 ++ + # Create a tracking key for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_key = (restart_ctr, conn_id) + + # Should we exclude this IP + if self.exclude_ip and src_ip in self.exclude_ip: +- self.connection['exclude_ip_map'][restart_conn_key] = src_ip ++ self.connection.exclude_ip[restart_conn_key] = src_ip + return None + + if self.verbose: + # Update open connection count +- self.connection['open_conns'][src_ip] = self.connection['open_conns'].get(src_ip, 0) + 1 ++ self.connection.open_conns[src_ip] += 1 + + # Track the connection start normalised datetime object for latency report +- self.connection['start_time'][conn_id] = groups.get('timestamp') ++ self.connection.start_time[conn_id] = groups.get('timestamp') + + # Update general connection counters +- for key in ['conn_ctr', 'sim_conn_ctr']: +- self.connection[key] = self.connection.get(key, 0) + 1 ++ self.connection.counters['conn'] += 1 ++ self.connection.counters['sim_conn'] += 1 + + # Update the maximum number of simultaneous connections seen +- self.connection['max_sim_conn_ctr'] = max( +- self.connection.get('max_sim_conn_ctr', 0), +- self.connection['sim_conn_ctr'] ++ self.connection.counters['max_sim_conn'] = max( ++ self.connection.counters['max_sim_conn'], ++ self.connection.counters['sim_conn'] + ) + + # Update protocol counters +- src_ip_tmp = 'local' if src_ip == 'local' else 'ldap' + if ssl: +- stat_count_key = 'ldaps_ctr' ++ self.connection.counters['ldaps'] += 1 ++ elif src_ip == 'local': ++ self.connection.counters['ldapi'] += 1 + else: +- stat_count_key = 'ldapi_ctr' if src_ip_tmp == 'local' else 'ldap_ctr' +- self.connection[stat_count_key] = self.connection.get(stat_count_key, 0) + 1 ++ self.connection.counters['ldap'] += 1 + + # Track file descriptor counters +- self.connection['fd_max_ctr'] = ( +- max(self.connection.get('fd_max_ctr', 0), int(fd)) +- ) +- self.connection['fd_taken_ctr'] = ( +- self.connection.get('fd_taken_ctr', 0) + 1 +- ) ++ self.connection.counters['fd_max'] = max(self.connection.counters['fd_taken'], int(fd)) ++ self.connection.counters['fd_taken'] += 1 + + # Track source IP +- self.connection['restart_conn_ip_map'][restart_conn_key] = src_ip ++ self.connection.restart_conn_ip_map[restart_conn_key] = src_ip + + # Update the count of connections seen from this IP +- if src_ip not in self.connection['ip_map']: +- self.connection['ip_map'][src_ip] = {} ++ if src_ip not in self.connection.src_ip_map: ++ self.connection.src_ip_map[src_ip] = {} ++ ++ self.connection.src_ip_map[src_ip]['count'] = self.connection.src_ip_map[src_ip].get('count', 0) + 1 + +- self.connection['ip_map'][src_ip]['count'] = self.connection['ip_map'][src_ip].get('count', 0) + 1 ++ if 'keys' not in self.connection.src_ip_map[src_ip]: ++ self.connection.src_ip_map[src_ip]['keys'] = set() + +- if 'keys' not in self.connection['ip_map'][src_ip]: +- self.connection['ip_map'][src_ip]['keys'] = set() ++ self.connection.src_ip_map[src_ip]['keys'].add(restart_conn_key) + +- self.connection['ip_map'][src_ip]['keys'].add(restart_conn_key) +- # self.connection['ip_map']['ip_key'] = restart_conn_key ++ self.logger.debug(f"_process_connect_stats - End") + + def _process_auth_stats(self, groups: dict): + """ +@@ -1173,7 +1222,6 @@ class logAnalyser: + Args: + groups (dict): A dictionary containing operation information. Expected keys: + - 'conn_id': Connection identifier. +- - 'restart_ctr': Server restart count. + - 'auth_protocol': Auth protocol (SSL, TLS). + - 'auth_version': Auth version. + - 'auth_message': Optional auth message. +@@ -1181,9 +1229,11 @@ class logAnalyser: + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ ++ self.logger.debug(f"_process_auth_stats - Start - {groups}") ++ + try: + conn_id = groups.get('conn_id') +- restart_ctr = groups.get('restart_ctr') + auth_protocol = groups.get('auth_protocol') + auth_version = groups.get('auth_version') + auth_message = groups.get('auth_message') +@@ -1192,15 +1242,16 @@ class logAnalyser: + return + + # Create a tracking key for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_key = (restart_ctr, conn_id) + + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + + if auth_protocol: +- if restart_conn_key not in self.auth['auth_info']: +- self.auth['auth_info'][restart_conn_key] = { ++ if restart_conn_key not in self.auth.auth_info: ++ self.auth.auth_info[restart_conn_key] = { + 'proto': auth_protocol, + 'version': auth_version, + 'count': 0, +@@ -1209,19 +1260,19 @@ class logAnalyser: + + if auth_message: + # Increment counters and add auth message +- self.auth['auth_info'][restart_conn_key]['message'].append(auth_message) ++ self.auth.auth_info[restart_conn_key]['message'].append(auth_message) + + # Bump auth related counters +- self.auth['cipher_ctr'] = self.auth.get('cipher_ctr', 0) + 1 +- self.auth['auth_info'][restart_conn_key]['count'] = ( +- self.auth['auth_info'][restart_conn_key].get('count', 0) + 1 +- ) ++ self.auth.counters['cipher_ctr'] += 1 ++ self.auth.auth_info[restart_conn_key]['count'] += 1 + + if auth_message: + if auth_message == 'client bound as': +- self.auth['ssl_client_bind_ctr'] = self.auth.get('ssl_client_bind_ctr', 0) + 1 ++ self.auth.counters['ssl_client_bind_ctr'] += 1 + elif auth_message == 'failed to map client certificate to LDAP DN': +- self.auth['ssl_client_bind_failed_ctr'] = self.auth.get('ssl_client_bind_failed_ctr', 0) + 1 ++ self.auth.counters['ssl_client_bind_failed_ctr'] += 1 ++ ++ self.logger.debug(f"_process_auth_stats - End") + + def _process_vlv_stats(self, groups: dict): + """ +@@ -1231,33 +1282,37 @@ class logAnalyser: + groups (dict): A dictionary containing operation information. Expected keys: + - 'conn_id': Connection identifier. + - 'op_id': Operation identifier. +- - 'restart_ctr': Server restart count. + + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ ++ self.logger.debug(f"_process_vlv_stats - Start - {groups}") ++ + try: + conn_id = groups.get('conn_id') + op_id = groups.get('op_id') +- restart_ctr = groups.get('restart_ctr') + except KeyError as e: + self.logger.error(f"Missing key in groups: {e}") + return + +- # Create tracking keys ++ # Create a tracking key for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_op_key = (restart_ctr, conn_id, op_id) + restart_conn_key = (restart_ctr, conn_id) + + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + + # Bump vlv and global op stats +- self.vlv['vlv_ctr'] = self.vlv.get('vlv_ctr', 0) + 1 +- self.operation['all_op_ctr'] = self.operation.get('all_op_ctr', 0) + 1 ++ self.vlv.counters['vlv'] += 1 ++ self.operation.counters['total'] = self.operation.counters['total'] + 1 + + # Key and value are the same, makes set operations easier later on +- self.vlv['vlv_map_rco'][restart_conn_op_key] = restart_conn_op_key ++ self.vlv.rst_con_op_map[restart_conn_op_key] = restart_conn_op_key ++ ++ self.logger.debug(f"_process_vlv_stats - End") + + def _process_abandon_stats(self, groups: dict): + """ +@@ -1267,38 +1322,41 @@ class logAnalyser: + groups (dict): A dictionary containing operation information. Expected keys: + - 'conn_id': Connection identifier. + - 'op_id': Operation identifier. +- - 'restart_ctr': Server restart count. + - 'targetop': The target operation. + - 'msgid': Message ID. + + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ ++ self.logger.debug(f"_process_abandon_stats - Start - {groups}") ++ + try: + conn_id = groups.get('conn_id') + op_id = groups.get('op_id') +- restart_ctr = groups.get('restart_ctr') + targetop = groups.get('targetop') + msgid = groups.get('msgid') + except KeyError as e: + self.logger.error(f"Missing key in groups: {e}") + return + +- # Create a tracking keys +- restart_conn_op_key = (restart_ctr, conn_id, op_id) ++ # Create a tracking key for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_key = (restart_ctr, conn_id) + + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + + # Bump some stats +- self.result['result_ctr'] = self.result.get('result_ctr', 0) + 1 +- self.operation['all_op_ctr'] = self.operation.get('all_op_ctr', 0) + 1 +- self.operation['abandon_op_ctr'] = self.operation.get('abandon_op_ctr', 0) + 1 ++ self.result.counters['result'] += 1 ++ self.operation.counters['total'] = self.operation.counters['total'] + 1 ++ self.operation.counters['abandon'] += 1 + + # Track abandoned operation for later processing +- self.operation['abandoned_map_rco'][restart_conn_op_key] = (conn_id, op_id, targetop, msgid) ++ self.operation.rst_con_op_map['abandon'] = (conn_id, op_id, targetop, msgid) ++ ++ self.logger.debug(f"_process_abandon_stats - End") + + def _process_sort_stats(self, groups: dict): + """ +@@ -1307,26 +1365,30 @@ class logAnalyser: + Args: + groups (dict): A dictionary containing operation information. Expected keys: + - 'conn_id': Connection identifier. +- - 'restart_ctr': Server restart count. + + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ ++ self.logger.debug(f"_process_sort_stats - Start - {groups}") ++ + try: + conn_id = groups.get('conn_id') +- restart_ctr = groups.get('restart_ctr') + except KeyError as e: + self.logger.error(f"Missing key in groups: {e}") + return + + # Create a tracking key for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_key = (restart_ctr, conn_id) + + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + +- self.operation['sort_op_ctr'] = self.operation.get('sort_op_ctr', 0) + 1 ++ self.operation.counters['sort'] += 1 ++ ++ self.logger.debug(f"_process_sort_stats - End") + + def _process_extend_op_stats(self, groups: dict): + """ +@@ -1342,6 +1404,9 @@ class logAnalyser: + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ ++ self.logger.debug(f"_process_extend_op_stats - Start - {groups}") ++ + try: + conn_id = groups.get('conn_id') + op_id = groups.get('op_id') +@@ -1352,31 +1417,34 @@ class logAnalyser: + return + + # Create a tracking key for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_op_key = (restart_ctr, conn_id, op_id) + restart_conn_key = (restart_ctr, conn_id) + + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + + # Increment global operation counters +- self.operation['all_op_ctr'] = self.operation.get('all_op_ctr', 0) + 1 +- self.operation['extnd_op_ctr'] = self.operation.get('extnd_op_ctr', 0) + 1 ++ self.operation.counters['total'] = self.operation.counters['total'] + 1 ++ self.operation.counters['extnd'] += 1 + + # Track extended operation data if an OID is present + if oid is not None: +- self.operation['extop_dict'][oid] = self.operation['extop_dict'].get(oid, 0) + 1 +- self.operation['extop_map_rco'][restart_conn_op_key] = ( +- self.operation['extop_map_rco'].get(restart_conn_op_key, 0) + 1 ++ self.operation.extended[oid] += 1 ++ self.operation.rst_con_op_map['extnd'][restart_conn_op_key] = ( ++ self.operation.rst_con_op_map['extnd'].get(restart_conn_op_key, 0) + 1 + ) + + # If the conn_id is associated with this DN, update op counter +- for dn in self.bind['report_dn']: +- conns = self.bind['report_dn'][dn]['conn'] ++ for dn in self.bind.report_dn: ++ conns = self.bind.report_dn[dn]['conn'] + if conn_id in conns: + bind_dn_key = self._report_dn_key(dn, self.report_dn) + if bind_dn_key: +- self.bind['report_dn'][bind_dn_key]['ext'] = self.bind['report_dn'][bind_dn_key].get('ext', 0) + 1 ++ self.bind.report_dn[bind_dn_key]['ext'] = self.bind.report_dn[bind_dn_key].get('ext', 0) + 1 ++ ++ self.logger.debug(f"_process_extend_op_stats - End") + + def _process_autobind_stats(self, groups: dict): + """ +@@ -1385,43 +1453,47 @@ class logAnalyser: + Args: + groups (dict): A dictionary containing operation information. Expected keys: + - 'conn_id': Connection identifier. +- - 'restart_ctr': Server restart count. + - 'bind_dn': Bind DN ("cn=Directory Manager") + + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ ++ self.logger.debug(f"_process_autobind_stats - Start - {groups}") ++ + try: + conn_id = groups.get('conn_id') +- restart_ctr = groups.get('restart_ctr') + bind_dn = groups.get('bind_dn') + except KeyError as e: + self.logger.error(f"Missing key in groups: {e}") + return + + # Create a tracking key for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_key = (restart_ctr, conn_id) + + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + + # Bump relevant counters +- self.bind['bind_ctr'] = self.bind.get('bind_ctr', 0) + 1 +- self.bind['autobind_ctr'] = self.bind.get('autobind_ctr', 0) + 1 +- self.operation['all_op_ctr'] = self.operation.get('all_op_ctr', 0) + 1 ++ self.bind.counters['bind'] += 1 ++ self.bind.counters['autobind'] += 1 ++ self.operation.counters['total'] += 1 + + # Handle an anonymous autobind (empty bind_dn) + if bind_dn == "": +- self.bind['anon_bind_ctr'] = self.bind.get('anon_bind_ctr', 0) + 1 ++ self.bind.counters['anon'] += 1 + else: +- # Process non-anonymous binds, does the bind_dn if exist in dn_map_rc +- bind_dn = self.bind['dn_map_rc'].get(restart_conn_key, bind_dn) ++ # Process non-anonymous binds, does the bind_dn if exist in restart_conn_dn_map ++ bind_dn = self.bind.restart_conn_dn_map.get(restart_conn_key, bind_dn) + if bind_dn: + if bind_dn.casefold() == self.root_dn.casefold(): +- self.bind['rootdn_bind_ctr'] = self.bind.get('rootdn_bind_ctr', 0) + 1 ++ self.bind.counters['rootdn'] += 1 + bind_dn = bind_dn.lower() +- self.bind['dn_freq'][bind_dn] = self.bind['dn_freq'].get(bind_dn, 0) + 1 ++ self.bind.dns[bind_dn] = self.bind.dns.get(bind_dn, 0) + 1 ++ ++ self.logger.debug(f"_process_autobind_stats - End") + + def _process_disconnect_stats(self, groups: dict): + """ +@@ -1430,7 +1502,6 @@ class logAnalyser: + Args: + groups (dict): A dictionary containing operation information. Expected keys: + - 'conn_id': Connection identifier. +- - 'restart_ctr': Server restart count. + - 'timestamp': The timestamp of the disconnect event. + - 'error_code': Error code associated with the disconnect, if any. + - 'disconnect_code': Disconnect code, if any. +@@ -1438,9 +1509,11 @@ class logAnalyser: + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ ++ self.logger.debug(f"_process_disconnect_stats - Start - {groups}") ++ + try: + conn_id = groups.get('conn_id') +- restart_ctr = groups.get('restart_ctr') + timestamp = groups.get('timestamp') + error_code = groups.get('error_code') + disconnect_code = groups.get('disconnect_code') +@@ -1449,17 +1522,18 @@ class logAnalyser: + return + + # Create a tracking key for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_key = (restart_ctr, conn_id) + + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + + if self.verbose: + # Handle verbose logging for open connections and IP addresses +- src_ip = self.connection['restart_conn_ip_map'].get(restart_conn_key) +- if src_ip and src_ip in self.connection.get('open_conns', {}): +- open_conns = self.connection['open_conns'] ++ src_ip = self.connection.restart_conn_ip_map.get(restart_conn_key) ++ if src_ip and src_ip in self.connection.open_conns: ++ open_conns = self.connection.open_conns + if open_conns[src_ip] > 1: + open_conns[src_ip] -= 1 + else: +@@ -1467,7 +1541,7 @@ class logAnalyser: + + # Handle latency and disconnect times + if self.verbose: +- start_time = self.connection['start_time'].get(conn_id, None) ++ start_time = self.connection.start_time[conn_id] + finish_time = groups.get('timestamp') + if start_time and timestamp: + latency = self.get_elapsed_time(start_time, finish_time, "seconds") +@@ -1475,28 +1549,29 @@ class logAnalyser: + LATENCY_GROUPS[bucket] += 1 + + # Reset start time for the connection +- self.connection['start_time'][conn_id] = None ++ self.connection.start_time[conn_id] = None + + # Update connection stats +- self.connection['sim_conn_ctr'] = self.connection.get('sim_conn_ctr', 0) - 1 +- self.connection['fd_returned_ctr'] = ( +- self.connection.get('fd_returned_ctr', 0) + 1 +- ) ++ self.connection.counters['sim_conn'] -= 1 ++ self.connection.counters['fd_returned'] += 1 + + # Track error and disconnect codes if provided + if error_code is not None: + error_type = DISCONNECT_ERRORS.get(error_code, 'unknown') + if disconnect_code is not None: + # Increment the count for the specific error and disconnect code +- error_map = self.connection.setdefault(error_type, {}) +- error_map[disconnect_code] = error_map.get(disconnect_code, 0) + 1 ++ # error_map = self.connection.setdefault(error_type, {}) ++ # error_map[disconnect_code] = error_map.get(disconnect_code, 0) + 1 ++ self.connection[error_type][disconnect_code] += 1 + + # Handle disconnect code and update stats + if disconnect_code is not None: +- self.connection['disconnect_code'][disconnect_code] = ( +- self.connection['disconnect_code'].get(disconnect_code, 0) + 1 ++ self.connection.disconnect_code[disconnect_code] = ( ++ self.connection.disconnect_code.get(disconnect_code, 0) + 1 + ) +- self.connection['disconnect_code_map'][restart_conn_key] = disconnect_code ++ self.connection.restart_conn_disconnect_map[restart_conn_key] = disconnect_code ++ ++ self.logger.debug(f"_process_disconnect_stats - End") + + def _group_latencies(self, latency_seconds: int): + """ +@@ -1530,49 +1605,57 @@ class logAnalyser: + Args: + groups (dict): A dictionary containing operation information. Expected keys: + - 'conn_id': Connection identifier. +- - 'restart_ctr': Server restart count. + - 'op_id': Operation identifier. + + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ self.logger.debug(f"_process_crud_stats - Start - {groups}") + try: + conn_id = groups.get('conn_id') +- restart_ctr = groups.get('restart_ctr') + op_type = groups.get('op_type') +- internal = groups.get('internal') + except KeyError as e: + self.logger.error(f"Missing key in groups: {e}") + return + + # Create a tracking key for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_key = (restart_ctr, conn_id) + + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + +- self.operation['all_op_ctr'] = self.operation.get('all_op_ctr', 0) + 1 ++ self.operation.counters['total'] = self.operation.counters['total'] + 1 + + # Use operation type as key for stats + if op_type is not None: + op_key = op_type.lower() +- self.operation[f"{op_key}_op_ctr"] = self.operation.get(f"{op_key}_op_ctr", 0) + 1 +- self.operation[f"{op_key}_map_rco"][restart_conn_key] = ( +- self.operation[f"{op_key}_map_rco"].get(restart_conn_key, 0) + 1 +- ) ++ if op_key not in self.operation.counters: ++ self.operation.counters[op_key] = 0 ++ if op_key not in self.operation.rst_con_op_map: ++ self.operation.rst_con_op_map[op_key] = {} ++ ++ # Increment op type counter ++ self.operation.counters[op_key] += 1 ++ ++ # Increment the op type map counter ++ current_count = self.operation.rst_con_op_map[op_key].get(restart_conn_key, 0) ++ self.operation.rst_con_op_map[op_key][restart_conn_key] = current_count + 1 + + # If the conn_id is associated with this DN, update op counter +- for dn in self.bind['report_dn']: +- conns = self.bind['report_dn'][dn]['conn'] ++ for dn in self.bind.report_dn: ++ conns = self.bind.report_dn[dn]['conn'] + if conn_id in conns: + bind_dn_key = self._report_dn_key(dn, self.report_dn) + if bind_dn_key: +- self.bind['report_dn'][bind_dn_key][op_key] = self.bind['report_dn'][bind_dn_key].get(op_key, 0) + 1 ++ self.bind.report_dn[bind_dn_key][op_key] = self.bind.report_dn[bind_dn_key].get(op_key, 0) + 1 + + # Authorization identity + if groups['authzid_dn'] is not None: +- self.operation['authzid'] = self.operation.get('authzid', 0) + 1 ++ self.operation.counters['authzid'] += 1 ++ ++ self.logger.debug(f"_process_crud_stats - End") + + def _process_entry_referral_stats(self, groups: dict): + """ +@@ -1581,33 +1664,36 @@ class logAnalyser: + Args: + groups (dict): A dictionary containing operation information. Expected keys: + - 'conn_id': Connection identifier. +- - 'restart_ctr': Server restart count. + - 'op_id': Operation identifier. + + Raises: + KeyError: If required keys are missing in the `groups` dictionary. + """ ++ self.logger.debug(f"_process_entry_referral_stats - Start - {groups}") ++ + try: + conn_id = groups.get('conn_id') +- restart_ctr = groups.get('restart_ctr') + op_type = groups.get('op_type') + except KeyError as e: + self.logger.error(f"Missing key in groups: {e}") + return + + # Create a tracking key for this entry ++ restart_ctr = self.server.counters['restart'] + restart_conn_key = (restart_ctr, conn_id) + + # Should we ignore this operation +- if restart_conn_key in self.connection['exclude_ip_map']: ++ if restart_conn_key in self.connection.exclude_ip: + return None + + # Process operation type + if op_type is not None: + if op_type == 'ENTRY': +- self.result['entry_count'] = self.result.get('entry_count', 0) + 1 ++ self.result.counters['entry'] += 1 + elif op_type == 'REFERRAL': +- self.result['referral_count'] = self.result.get('referral_count', 0) + 1 ++ self.result.counters['referral'] += 1 ++ ++ self.logger.debug(f"_process_entry_referral_stats - End") + + def _process_and_write_stats(self, norm_timestamp: str, bytes_read: int): + """ +@@ -1620,6 +1706,7 @@ class logAnalyser: + Returns: + None + """ ++ self.logger.debug(f"_process_and_write_stats - Start") + + if self.csv_writer is None: + self.logger.error("CSV writer not enabled.") +@@ -1627,23 +1714,23 @@ class logAnalyser: + + # Define the stat mapping + stats = { +- 'result_ctr': self.result, +- 'search_ctr': self.search, +- 'add_op_ctr': self.operation, +- 'mod_op_ctr': self.operation, +- 'modrdn_op_ctr': self.operation, +- 'cmp_op_ctr': self.operation, +- 'del_op_ctr': self.operation, +- 'abandon_op_ctr': self.operation, +- 'conn_ctr': self.connection, +- 'ldaps_ctr': self.connection, +- 'bind_ctr': self.bind, +- 'anon_bind_ctr': self.bind, +- 'unbind_ctr': self.bind, +- 'notesA_ctr': self.result, +- 'notesU_ctr': self.result, +- 'notesF_ctr': self.result, +- 'etime_stat': self.result ++ 'result': self.result.counters, ++ 'search': self.search.counters, ++ 'add': self.operation.counters, ++ 'mod': self.operation.counters, ++ 'modrdn': self.operation.counters, ++ 'cmp': self.operation.counters, ++ 'del': self.operation.counters, ++ 'abandon': self.operation.counters, ++ 'conn': self.connection.counters, ++ 'ldaps': self.connection.counters, ++ 'bind': self.bind.counters, ++ 'anon': self.bind.counters, ++ 'unbind': self.bind.counters, ++ 'notesA': self.result.counters, ++ 'notesU': self.result.counters, ++ 'notesF': self.result.counters, ++ 'etime_stat': self.result.counters + } + + # Build the current stat block +@@ -1675,8 +1762,7 @@ class logAnalyser: + + # out_stat_block[0] = self._convert_datetime_to_timestamp(out_stat_block[0]) + self.csv_writer.writerow(out_stat_block) +- +- self.result['etime_stat'] = 0.0 ++ self.result.etime_stat = 0.0 + + # Update previous stats for the next interval + self.prev_stats = curr_stat_block +@@ -1705,7 +1791,9 @@ class logAnalyser: + # Write the stat block to csv and reset elapsed time for the next interval + # out_stat_block[0] = self._convert_datetime_to_timestamp(out_stat_block[0]) + self.csv_writer.writerow(out_stat_block) +- self.result['etime_stat'] = 0.0 ++ self.result.etime_stat = 0.0 ++ ++ self.logger.debug(f"_process_and_write_stats - End") + + def process_file(self, log_num: str, filepath: str): + """ +@@ -2254,45 +2342,45 @@ def main(): + sys.exit(1) + + # Prep for display +- elapsed_time = db.get_elapsed_time(db.server['first_time'], db.server['last_time'], "hms") +- elapsed_secs = db.get_elapsed_time(db.server['first_time'], db.server['last_time'], "seconds") +- num_ops = db.operation.get('all_op_ctr', 0) +- num_results = db.result.get('result_ctr', 0) +- num_conns = db.connection.get('conn_ctr', 0) +- num_ldap = db.connection.get('ldap_ctr', 0) +- num_ldapi = db.connection.get('ldapi_ctr', 0) +- num_ldaps = db.connection.get('ldaps_ctr', 0) +- num_startls = db.operation['extop_dict'].get(STLS_OID, 0) +- num_search = db.search.get('search_ctr', 0) +- num_mod = db.operation.get('mod_op_ctr', 0) +- num_add = db.operation.get('add_op_ctr', 0) +- num_del = db.operation.get('del_op_ctr', 0) +- num_modrdn = db.operation.get('modrdn_op_ctr', 0) +- num_cmp = db.operation.get('cmp_op_ctr', 0) +- num_bind = db.bind.get('bind_ctr', 0) +- num_unbind = db.bind.get('unbind_ctr', 0) +- num_proxyd_auths = db.operation.get('authzid', 0) + db.search.get('authzid', 0) +- num_time_count = db.result.get('timestamp_ctr') ++ elapsed_time = db.get_elapsed_time(db.server.first_time, db.server.last_time, "hms") ++ elapsed_secs = db.get_elapsed_time(db.server.first_time, db.server.last_time, "seconds") ++ num_ops = db.operation.counters['total'] ++ num_results = db.result.counters['result'] ++ num_conns = db.connection.counters['conn'] ++ num_ldap = db.connection.counters['ldap'] ++ num_ldapi = db.connection.counters['ldapi'] ++ num_ldaps = db.connection.counters['ldaps'] ++ num_startls = db.operation.extended.get(STLS_OID, 0) ++ num_search = db.search.counters['search'] ++ num_mod = db.operation.counters['mod'] ++ num_add = db.operation.counters['add'] ++ num_del = db.operation.counters['del'] ++ num_modrdn = db.operation.counters['modrdn'] ++ num_cmp = db.operation.counters['cmp'] ++ num_bind = db.bind.counters['bind'] ++ num_unbind = db.bind.counters['unbind'] ++ num_proxyd_auths = db.operation.counters['authzid'] + db.search.counters['authzid'] ++ num_time_count = db.result.counters['timestamp'] + if num_time_count: +- avg_wtime = round(db.result.get('total_wtime', 0)/num_time_count, 9) +- avg_optime = round(db.result.get('total_optime', 0)/num_time_count, 9) +- avg_etime = round(db.result.get('total_etime', 0)/num_time_count, 9) +- num_fd_taken = db.connection.get('fd_taken_ctr', 0) +- num_fd_rtn = db.connection.get('fd_returned_ctr', 0) +- +- num_DM_binds = db.bind.get('rootdn_bind_ctr', 0) +- num_base_search = db.search.get('base_search_ctr', 0) ++ avg_wtime = round(db.result.total_wtime/num_time_count, 9) ++ avg_optime = round(db.result.total_optime/num_time_count, 9) ++ avg_etime = round(db.result.total_etime/num_time_count, 9) ++ num_fd_taken = db.connection.counters['fd_taken'] ++ num_fd_rtn = db.connection.counters['fd_returned'] ++ ++ num_DM_binds = db.bind.counters['rootdn'] ++ num_base_search = db.search.counters['base_search'] + try: +- log_start_time = db.convert_timestamp_to_string(db.server.get('first_time', "")) ++ log_start_time = db.convert_timestamp_to_string(db.server.first_time) + except ValueError: + log_start_time = "Unknown" + + try: +- log_end_time = db.convert_timestamp_to_string(db.server.get('last_time', "")) ++ log_end_time = db.convert_timestamp_to_string(db.server.last_time) + except ValueError: + log_end_time = "Unknown" + +- print(f"\n\nTotal Log Lines Analysed:{db.server['lines_parsed']}\n") ++ print(f"\n\nTotal Log Lines Analysed:{db.server.counters['lines_parsed']}\n") + print("\n----------- Access Log Output ------------\n") + print(f"Start of Logs: {log_start_time}") + print(f"End of Logs: {log_end_time}") +@@ -2302,12 +2390,12 @@ def main(): + db.display_bind_report() + sys.exit(1) + +- print(f"\nRestarts: {db.server.get('restart_ctr', 0)}") +- if db.auth.get('cipher_ctr', 0) > 0: ++ print(f"\nRestarts: {db.server.counters['restart']}") ++ if db.auth.counters['cipher_ctr'] > 0: + print(f"Secure Protocol Versions:") + # Group data by protocol + version + unique message + grouped_data = defaultdict(lambda: {'count': 0, 'messages': set()}) +- for _, details in db.auth['auth_info'].items(): ++ for _, details in db.auth.auth_info.items(): + # If there is no protocol version + if details['version']: + proto_version = f"{details['proto']}{details['version']}" +@@ -2323,7 +2411,7 @@ def main(): + for ((proto_version, message), data) in grouped_data.items(): + print(f" - {proto_version} {message} ({data['count']} connection{'s' if data['count'] > 1 else ''})") + +- print(f"Peak Concurrent connections: {db.connection.get('max_sim_conn_ctr', 0)}") ++ print(f"Peak Concurrent connections: {db.connection.counters['max_sim_conn']}") + print(f"Total Operations: {num_ops}") + print(f"Total Results: {num_results}") + print(f"Overall Performance: {db.get_overall_perf(num_results, num_ops)}%") +@@ -2344,111 +2432,112 @@ def main(): + print(f"\nAverage wtime (wait time): {avg_wtime:.9f}") + print(f"Average optime (op time): {avg_optime:.9f}") + print(f"Average etime (elapsed time): {avg_etime:.9f}") +- print(f"\nMulti-factor Authentications: {db.result.get('notesM_ctr', 0)}") ++ print(f"\nMulti-factor Authentications: {db.result.counters['notesM']}") + print(f"Proxied Auth Operations: {num_proxyd_auths}") +- print(f"Persistent Searches: {db.search.get('persistent_ctr', 0)}") +- print(f"Internal Operations: {db.server.get('internal_op_ctr', 0)}") +- print(f"Entry Operations: {db.result.get('entry_count', 0)}") +- print(f"Extended Operations: {db.operation.get('extnd_op_ctr', 0)}") +- print(f"Abandoned Requests: {db.operation.get('abandon_op_ctr', 0)}") +- print(f"Smart Referrals Received: {db.result.get('referral_count', 0)}") +- print(f"\nVLV Operations: {db.vlv.get('vlv_ctr', 0)}") +- print(f"VLV Unindexed Searches: {len([key for key, value in db.vlv['vlv_map_rco'].items() if value == 'A'])}") +- print(f"VLV Unindexed Components: {len([key for key, value in db.vlv['vlv_map_rco'].items() if value == 'U'])}") +- print(f"SORT Operations: {db.operation.get('sort_op_ctr', 0)}") +- print(f"\nEntire Search Base Queries: {db.search.get('base_search_ctr', 0)}") +- print(f"Paged Searches: {db.result.get('notesP_ctr', 0)}") +- num_unindexed_search = len(db.notesA.keys()) ++ print(f"Persistent Searches: {db.search.counters['persistent']}") ++ print(f"Internal Operations: {db.operation.counters['internal']}") ++ print(f"Entry Operations: {db.result.counters['entry']}") ++ print(f"Extended Operations: {db.operation.counters['extnd']}") ++ print(f"Abandoned Requests: {db.operation.counters['abandon']}") ++ print(f"Smart Referrals Received: {db.result.counters['referral']}") ++ print(f"\nVLV Operations: {db.vlv.counters['vlv']}") ++ print(f"VLV Unindexed Searches: {len([key for key, value in db.vlv.rst_con_op_map.items() if value == 'A'])}") ++ print(f"VLV Unindexed Components: {len([key for key, value in db.vlv.rst_con_op_map.items() if value == 'U'])}") ++ print(f"SORT Operations: {db.operation.counters['sort']}") ++ print(f"\nEntire Search Base Queries: {num_base_search}") ++ print(f"Paged Searches: {db.result.counters['notesP']}") ++ num_unindexed_search = len(db.result.notes['A']) + print(f"Unindexed Searches: {num_unindexed_search}") +- if db.verbose: +- if num_unindexed_search > 0: +- for num, key in enumerate(db.notesA, start=1): +- src, conn, op = key +- restart_conn_op_key = (src, conn, op) +- print(f"\nUnindexed Search #{num} (notes=A)") +- print(f" - Date/Time: {db.notesA[restart_conn_op_key]['time']}") +- print(f" - Connection Number: {conn}") +- print(f" - Operation Number: {op}") +- print(f" - Etime: {db.notesA[restart_conn_op_key]['etime']}") +- print(f" - Nentries: {db.notesA[restart_conn_op_key]['nentries']}") +- print(f" - IP Address: {db.notesA[restart_conn_op_key]['ip']}") +- print(f" - Search Base: {db.notesA[restart_conn_op_key]['base']}") +- print(f" - Search Scope: {db.notesA[restart_conn_op_key]['scope']}") +- print(f" - Search Filter: {db.notesA[restart_conn_op_key]['filter']}") +- print(f" - Bind DN: {db.notesA[restart_conn_op_key]['bind_dn']}\n") +- +- num_unindexed_component = len(db.notesU.keys()) ++ if db.verbose and num_unindexed_search > 0: ++ for num, key in enumerate(db.result.notes['A'], start=1): ++ src, conn, op = key ++ data = db.result.notes['A'][key] ++ ++ print(f"\n Unindexed Search #{num} (notes=A)") ++ print(f" - Date/Time: {data.get('time', '-')}") ++ print(f" - Connection Number: {conn}") ++ print(f" - Operation Number: {op}") ++ print(f" - Etime: {data.get('etime', '-')}") ++ print(f" - Nentries: {data.get('nentries', 0)}") ++ print(f" - IP Address: {data.get('ip', '-')}") ++ print(f" - Search Base: {data.get('base', '-')}") ++ print(f" - Search Scope: {data.get('scope', '-')}") ++ print(f" - Search Filter: {data.get('filter', '-')}") ++ print(f" - Bind DN: {data.get('bind_dn', '-')}\n") ++ ++ num_unindexed_component = len(db.result.notes['U']) + print(f"Unindexed Components: {num_unindexed_component}") +- if db.verbose: +- if num_unindexed_component > 0: +- for num, key in enumerate(db.notesU, start=1): +- src, conn, op = key +- restart_conn_op_key = (src, conn, op) +- print(f"\nUnindexed Component #{num} (notes=U)") +- print(f" - Date/Time: {db.notesU[restart_conn_op_key]['time']}") +- print(f" - Connection Number: {conn}") +- print(f" - Operation Number: {op}") +- print(f" - Etime: {db.notesU[restart_conn_op_key]['etime']}") +- print(f" - Nentries: {db.notesU[restart_conn_op_key]['nentries']}") +- print(f" - IP Address: {db.notesU[restart_conn_op_key]['ip']}") +- print(f" - Search Base: {db.notesU[restart_conn_op_key]['base']}") +- print(f" - Search Scope: {db.notesU[restart_conn_op_key]['scope']}") +- print(f" - Search Filter: {db.notesU[restart_conn_op_key]['filter']}") +- print(f" - Bind DN: {db.notesU[restart_conn_op_key]['bind_dn']}\n") +- +- num_invalid_filter = len(db.notesF.keys()) ++ if db.verbose and num_unindexed_component > 0: ++ for num, key in enumerate(db.result.notes['U'], start=1): ++ src, conn, op = key ++ data = db.result.notes['U'][key] ++ ++ print(f"\n Unindexed Component #{num} (notes=U)") ++ print(f" - Date/Time: {data.get('time', '-')}") ++ print(f" - Connection Number: {conn}") ++ print(f" - Operation Number: {op}") ++ print(f" - Etime: {data.get('etime', '-')}") ++ print(f" - Nentries: {data.get('nentries', 0)}") ++ print(f" - IP Address: {data.get('ip', '-')}") ++ print(f" - Search Base: {data.get('base', '-')}") ++ print(f" - Search Scope: {data.get('scope', '-')}") ++ print(f" - Search Filter: {data.get('filter', '-')}") ++ print(f" - Bind DN: {data.get('bind_dn', '-')}\n") ++ ++ num_invalid_filter = len(db.result.notes['F']) + print(f"Invalid Attribute Filters: {num_invalid_filter}") +- if db.verbose: +- if num_invalid_filter > 0: +- for num, key in enumerate(db.notesF, start=1): +- src, conn, op = key +- restart_conn_op_key = (src, conn, op) +- print(f"\nInvalid Attribute Filter #{num} (notes=F)") +- print(f" - Date/Time: {db.notesF[restart_conn_op_key]['time']}") +- print(f" - Connection Number: {conn}") +- print(f" - Operation Number: {op}") +- print(f" - Etime: {db.notesF[restart_conn_op_key]['etime']}") +- print(f" - Nentries: {db.notesF[restart_conn_op_key]['nentries']}") +- print(f" - IP Address: {db.notesF[restart_conn_op_key]['ip']}") +- print(f" - Search Filter: {db.notesF[restart_conn_op_key]['filter']}") +- print(f" - Bind DN: {db.notesF[restart_conn_op_key]['bind_dn']}\n") ++ if db.verbose and num_invalid_filter > 0: ++ for num, key in enumerate(db.result.notes['F'], start=1): ++ src, conn, op = key ++ data = db.result.notes['F'][key] ++ ++ print(f"\n Invalid Attribute Filter #{num} (notes=F)") ++ print(f" - Date/Time: {data.get('time', '-')}") ++ print(f" - Connection Number: {conn}") ++ print(f" - Operation Number: {op}") ++ print(f" - Etime: {data.get('etime', '-')}") ++ print(f" - Nentries: {data.get('nentries', 0)}") ++ print(f" - IP Address: {data.get('ip', '-')}") ++ print(f" - Search Filter: {data.get('filter', '-')}") ++ print(f" - Bind DN: {data.get('bind_dn', '-')}\n") ++ + print(f"FDs Taken: {num_fd_taken}") + print(f"FDs Returned: {num_fd_rtn}") +- print(f"Highest FD Taken: {db.connection.get('fd_max_ctr', 0)}\n") +- num_broken_pipe = len(db.connection['broken_pipe']) ++ print(f"Highest FD Taken: {db.connection.counters['fd_max']}\n") ++ num_broken_pipe = len(db.connection.broken_pipe) + print(f"Broken Pipes: {num_broken_pipe}") + if num_broken_pipe > 0: +- for code, count in db.connection['broken_pipe'].items(): ++ for code, count in db.connection.broken_pipe.items(): + print(f" - {count} ({code}) {DISCONNECT_MSG.get(code, 'unknown')}") + print() +- num_reset_peer = len(db.connection['connection_reset']) ++ num_reset_peer = len(db.connection.connection_reset) + print(f"Connection Reset By Peer: {num_reset_peer}") + if num_reset_peer > 0: +- for code, count in db.connection['connection_reset'].items(): ++ for code, count in db.connection.connection_reset.items(): + print(f" - {count} ({code}) {DISCONNECT_MSG.get(code, 'unknown')}") + print() +- num_resource_unavail = len(db.connection['resource_unavail']) ++ num_resource_unavail = len(db.connection.resource_unavail) + print(f"Resource Unavailable: {num_resource_unavail}") + if num_resource_unavail > 0: +- for code, count in db.connection['resource_unavail'].items(): ++ for code, count in db.connection.resource_unavail.items(): + print(f" - {count} ({code}) {DISCONNECT_MSG.get(code, 'unknown')}") + print() +- print(f"Max BER Size Exceeded: {db.connection['disconnect_code'].get('B2', 0)}\n") +- print(f"Binds: {db.bind.get('bind_ctr', 0)}") +- print(f"Unbinds: {db.bind.get('unbind_ctr', 0)}") ++ print(f"Max BER Size Exceeded: {db.connection.disconnect_code.get('B2', 0)}\n") ++ print(f"Binds: {db.bind.counters['bind']}") ++ print(f"Unbinds: {db.bind.counters['unbind']}") + print(f"----------------------------------") +- print(f"- LDAP v2 Binds: {db.bind.get('version', {}).get('2', 0)}") +- print(f"- LDAP v3 Binds: {db.bind.get('version', {}).get('3', 0)}") +- print(f"- AUTOBINDs(LDAPI): {db.bind.get('autobind_ctr', 0)}") +- print(f"- SSL Client Binds {db.auth.get('ssl_client_bind_ctr', 0)}") +- print(f"- Failed SSL Client Binds: {db.auth.get('ssl_client_bind_failed_ctr', 0)}") +- print(f"- SASL Binds: {db.bind.get('sasl_bind_ctr', 0)}") +- if db.bind.get('sasl_bind_ctr', 0) > 0: +- saslmech = db.bind['sasl_mech_freq'] ++ print(f"- LDAP v2 Binds: {db.bind.version.get('2', 0)}") ++ print(f"- LDAP v3 Binds: {db.bind.version.get('3', 0)}") ++ print(f"- AUTOBINDs(LDAPI): {db.bind.counters['autobind']}") ++ print(f"- SSL Client Binds {db.auth.counters['ssl_client_bind_ctr']}") ++ print(f"- Failed SSL Client Binds: {db.auth.counters['ssl_client_bind_failed_ctr']}") ++ print(f"- SASL Binds: {db.bind.counters['sasl']}") ++ if db.bind.counters['sasl'] > 0: ++ saslmech = db.bind.sasl_mech + for saslb in sorted(saslmech.keys(), key=lambda k: saslmech[k], reverse=True): + print(f" - {saslb:<4}: {saslmech[saslb]}") + print(f"- Directory Manager Binds: {num_DM_binds}") +- print(f"- Anonymous Binds: {db.bind.get('anon_bind_ctr', 0)}\n") ++ print(f"- Anonymous Binds: {db.bind.counters['anon']}\n") + if db.verbose: + # Connection Latency + print(f"\n ----- Connection Latency Details -----\n") +@@ -2465,7 +2554,7 @@ def main(): + f"{LATENCY_GROUPS['> 15']:^7}") + + # Open Connections +- open_conns = db.connection['open_conns'] ++ open_conns = db.connection.open_conns + if len(open_conns) > 0: + print(f"\n ----- Current Open Connection IDs -----\n") + for conn in sorted(open_conns.keys(), key=lambda k: open_conns[k], reverse=True): +@@ -2473,12 +2562,12 @@ def main(): + + # Error Codes + print(f"\n----- Errors -----\n") +- error_freq = db.result['error_freq'] ++ error_freq = db.result.error_freq + for err in sorted(error_freq.keys(), key=lambda k: error_freq[k], reverse=True): + print(f"err={err:<2} {error_freq[err]:>10} {LDAP_ERR_CODES[err]:<30}") + + # Failed Logins +- bad_pwd_map = db.result['bad_pwd_map'] ++ bad_pwd_map = db.result.bad_pwd_map + bad_pwd_map_len = len(bad_pwd_map) + if bad_pwd_map_len > 0: + print(f"\n----- Top {db.size_limit} Failed Logins ------\n") +@@ -2495,27 +2584,27 @@ def main(): + print(f"{count:<10} {ip}") + + # Connection Codes +- disconnect_codes = db.connection['disconnect_code'] ++ disconnect_codes = db.connection.disconnect_code + if len(disconnect_codes) > 0: + print(f"\n----- Total Connection Codes ----\n") + for code in disconnect_codes: + print(f"{code:<2} {disconnect_codes[code]:>10} {DISCONNECT_MSG.get(code, 'unknown'):<30}") + + # Unique IPs +- restart_conn_ip_map = db.connection['restart_conn_ip_map'] +- ip_map = db.connection['ip_map'] +- ips_len = len(ip_map) ++ restart_conn_ip_map = db.connection.restart_conn_ip_map ++ src_ip_map = db.connection.src_ip_map ++ ips_len = len(src_ip_map) + if ips_len > 0: + print(f"\n----- Top {db.size_limit} Clients -----\n") + print(f"Number of Clients: {ips_len}") +- for num, (outer_ip, ip_info) in enumerate(ip_map.items(), start=1): ++ for num, (outer_ip, ip_info) in enumerate(src_ip_map.items(), start=1): + temp = {} + print(f"\n[{num}] Client: {outer_ip}") + print(f" {ip_info['count']} - Connection{'s' if ip_info['count'] > 1 else ''}") + for id, inner_ip in restart_conn_ip_map.items(): + (src, conn) = id + if outer_ip == inner_ip: +- code = db.connection['disconnect_code_map'].get((src, conn), 0) ++ code = db.connection.restart_conn_disconnect_map[(src, conn)] + if code: + temp[code] = temp.get(code, 0) + 1 + for code, count in temp.items(): +@@ -2524,7 +2613,7 @@ def main(): + break + + # Unique Bind DN's +- binds = db.bind.get('dn_freq', 0) ++ binds = db.bind.dns + binds_len = len(binds) + if binds_len > 0: + print(f"\n----- Top {db.size_limit} Bind DN's ----\n") +@@ -2532,10 +2621,10 @@ def main(): + for num, bind in enumerate(sorted(binds.keys(), key=lambda k: binds[k], reverse=True)): + if num >= db.size_limit: + break +- print(f"{db.bind['dn_freq'][bind]:<10} {bind:<30}") ++ print(f"{db.bind.dns[bind]:<10} {bind:<30}") + + # Unique search bases +- bases = db.search['base_map'] ++ bases = db.search.bases + num_bases = len(bases) + if num_bases > 0: + print(f"\n----- Top {db.size_limit} Search Bases -----\n") +@@ -2543,10 +2632,10 @@ def main(): + for num, base in enumerate(sorted(bases.keys(), key=lambda k: bases[k], reverse=True)): + if num >= db.size_limit: + break +- print(f"{db.search['base_map'][base]:<10} {base}") ++ print(f"{db.search.bases[base]:<10} {base}") + + # Unique search filters +- filters = sorted(db.search['filter_list'], reverse=True) ++ filters = sorted(db.search.filter_list, reverse=True) + num_filters = len(filters) + if num_filters > 0: + print(f"\n----- Top {db.size_limit} Search Filters -----\n") +@@ -2556,7 +2645,7 @@ def main(): + print(f"{count:<10} {filter}") + + # Longest elapsed times +- etimes = sorted(db.result['etime_duration'], reverse=True) ++ etimes = sorted(db.result.etime_duration, reverse=True) + num_etimes = len(etimes) + if num_etimes > 0: + print(f"\n----- Top {db.size_limit} Longest etimes (elapsed times) -----\n") +@@ -2566,7 +2655,7 @@ def main(): + print(f"etime={etime:<12}") + + # Longest wait times +- wtimes = sorted(db.result['wtime_duration'], reverse=True) ++ wtimes = sorted(db.result.wtime_duration, reverse=True) + num_wtimes = len(wtimes) + if num_wtimes > 0: + print(f"\n----- Top {db.size_limit} Longest wtimes (wait times) -----\n") +@@ -2576,7 +2665,7 @@ def main(): + print(f"wtime={wtime:<12}") + + # Longest operation times +- optimes = sorted(db.result['optime_duration'], reverse=True) ++ optimes = sorted(db.result.optime_duration, reverse=True) + num_optimes = len(optimes) + if num_optimes > 0: + print(f"\n----- Top {db.size_limit} Longest optimes (actual operation times) -----\n") +@@ -2586,7 +2675,7 @@ def main(): + print(f"optime={optime:<12}") + + # Largest nentries returned +- nentries = sorted(db.result['nentries_num'], reverse=True) ++ nentries = sorted(db.result.nentries_num, reverse=True) + num_nentries = len(nentries) + if num_nentries > 0: + print(f"\n----- Top {db.size_limit} Largest nentries -----\n") +@@ -2597,7 +2686,7 @@ def main(): + print() + + # Extended operations +- oids = db.operation['extop_dict'] ++ oids = db.operation.extended + num_oids = len(oids) + if num_oids > 0: + print(f"\n----- Top {db.size_limit} Extended Operations -----\n") +@@ -2607,7 +2696,7 @@ def main(): + print(f"{oids[oid]:<12} {oid:<30} {OID_MSG.get(oid, 'Other'):<60}") + + # Commonly requested attributes +- attrs = db.search['attr_dict'] ++ attrs = db.search.attrs + num_nattrs = len(attrs) + if num_nattrs > 0: + print(f"\n----- Top {db.size_limit} Most Requested Attributes -----\n") +@@ -2617,14 +2706,14 @@ def main(): + print(f"{attrs[attr]:<11} {attr:<10}") + print() + +- abandoned = db.operation['abandoned_map_rco'] ++ abandoned = db.operation.rst_con_op_map['abandon'] + num_abandoned = len(abandoned) + if num_abandoned > 0: + print(f"\n----- Abandon Request Stats -----\n") + for num, abandon in enumerate(abandoned, start=1): + (restart, conn, op) = abandon +- conn, op, target_op, msgid = db.operation['abandoned_map_rco'][(restart, conn, op)] +- print(f"{num:<6} conn={conn} op={op} msgid={msgid} target_op:{target_op} client={db.connection['restart_conn_ip_map'].get((restart, conn), 'Unknown')}") ++ conn, op, target_op, msgid = db.operation.rst_con_op_map['abandoned'][(restart, conn, op)] ++ print(f"{num:<6} conn={conn} op={op} msgid={msgid} target_op:{target_op} client={db.connection.restart_conn_ip_map.get((restart, conn), 'Unknown')}") + print() + + if db.recommends or db.verbose: +@@ -2639,15 +2728,15 @@ def main(): + print(f"\n {rec_count}. You have unindexed components. This can be caused by a search on an unindexed attribute or by returned results exceeding the nsslapd-idlistscanlimit. Unindexed components are not recommended. To refuse unindexed searches, set 'nsslapd-require-index' to 'on' under your database entry (e.g. cn=UserRoot,cn=ldbm database,cn=plugins,cn=config).\n") + rec_count += 1 + +- if db.connection['disconnect_code'].get('T1', 0) > 0: ++ if db.connection.disconnect_code.get('T1', 0) > 0: + print(f"\n {rec_count}. You have some connections being closed by the idletimeout setting. You may want to increase the idletimeout if it is set low.\n") + rec_count += 1 + +- if db.connection['disconnect_code'].get('T2', 0) > 0: ++ if db.connection.disconnect_code.get('T2', 0) > 0: + print(f"\n {rec_count}. You have some connections being closed by the ioblocktimeout setting. You may want to increase the ioblocktimeout.\n") + rec_count += 1 + +- if db.connection['disconnect_code'].get('T3', 0) > 0: ++ if db.connection.disconnect_code.get('T3', 0) > 0: + print(f"\n {rec_count}. You have some connections being closed because a paged result search limit has been exceeded. You may want to increase the search time limit.\n") + rec_count += 1 + +@@ -2663,29 +2752,29 @@ def main(): + print(f"\n {rec_count}. You have a high number of Directory Manager binds. The Directory Manager account should only be used under certain circumstances. Avoid using this account for client applications.\n") + rec_count += 1 + +- num_success = db.result['error_freq'].get('0', 0) +- num_err = sum(v for k, v in db.result['error_freq'].items() if k != '0') ++ num_success = db.result.error_freq.get('0', 0) ++ num_err = sum(v for k, v in db.result.error_freq.items() if k != '0') + if num_err > num_success: + print(f"\n {rec_count}. You have more unsuccessful operations than successful operations. You should investigate this difference.\n") + rec_count += 1 + +- num_close_clean = db.connection['disconnect_code'].get('U1', 0) +- num_close_total = num_err = sum(v for k, v in db.connection['disconnect_code'].items()) ++ num_close_clean = db.connection.disconnect_code.get('U1', 0) ++ num_close_total = num_err = sum(v for k, v in db.connection.disconnect_code.items()) + if num_close_clean < (num_close_total - num_close_clean): + print(f"\n {rec_count}. You have more abnormal connection codes than cleanly closed connections. You may want to investigate this difference.\n") + rec_count += 1 + + if num_time_count: +- if round(avg_etime, 1) > 0: +- print(f"\n {rec_count}. Your average etime is {avg_etime:.1f}. You may want to investigate this performance problem.\n") ++ if round(avg_etime, 9) > 0: ++ print(f"\n {rec_count}. Your average etime is {avg_etime:.9f}. You may want to investigate this performance problem.\n") + rec_count += 1 + +- if round(avg_wtime, 1) > 0.5: +- print(f"\n {rec_count}. Your average wtime is {avg_wtime:.1f}. You may need to increase the number of worker threads (nsslapd-threadnumber).\n") ++ if round(avg_wtime, 9) > 0.5: ++ print(f"\n {rec_count}. Your average wtime is {avg_wtime:.9f}. You may need to increase the number of worker threads (nsslapd-threadnumber).\n") + rec_count += 1 + +- if round(avg_optime, 1) > 0: +- print(f"\n {rec_count}. Your average optime is {avg_optime:.1f}. You may want to investigate this performance problem.\n") ++ if round(avg_optime, 9) > 0: ++ print(f"\n {rec_count}. Your average optime is {avg_optime:.9f}. You may want to investigate this performance problem.\n") + rec_count += 1 + + if num_base_search > (num_search * 0.25): +@@ -2699,4 +2788,4 @@ def main(): + + + if __name__ == "__main__": +- main() ++ main() +\ No newline at end of file +-- +2.49.0 + diff --git a/0011-Issue-6850-AddressSanitizer-memory-leak-in-mdb_init.patch b/0011-Issue-6850-AddressSanitizer-memory-leak-in-mdb_init.patch new file mode 100644 index 0000000..71cfad2 --- /dev/null +++ b/0011-Issue-6850-AddressSanitizer-memory-leak-in-mdb_init.patch @@ -0,0 +1,65 @@ +From ad2a06cb64156c55d81b7a1647f9bec7071df9f4 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 447f3c70a..54ca03b0b 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 c4e87987f..ed17f979f 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/0012-Issue-6848-AddressSanitizer-leak-in-do_search.patch b/0012-Issue-6848-AddressSanitizer-leak-in-do_search.patch new file mode 100644 index 0000000..ab91a04 --- /dev/null +++ b/0012-Issue-6848-AddressSanitizer-leak-in-do_search.patch @@ -0,0 +1,58 @@ +From 98a83bb00255f77467a370d3347a8428b6659463 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/0013-Issue-6865-AddressSanitizer-leak-in-agmt_update_init.patch b/0013-Issue-6865-AddressSanitizer-leak-in-agmt_update_init.patch new file mode 100644 index 0000000..1911baf --- /dev/null +++ b/0013-Issue-6865-AddressSanitizer-leak-in-agmt_update_init.patch @@ -0,0 +1,58 @@ +From e89a5acbc1bcc1b460683aa498005d6f0ce7054e 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 c818c5857..0a81167b7 100644 +--- a/ldap/servers/plugins/replication/repl5_agmt.c ++++ b/ldap/servers/plugins/replication/repl5_agmt.c +@@ -2743,6 +2743,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/0014-Issue-6859-str2filter-is-not-fully-applying-matching.patch b/0014-Issue-6859-str2filter-is-not-fully-applying-matching.patch new file mode 100644 index 0000000..2c603d8 --- /dev/null +++ b/0014-Issue-6859-str2filter-is-not-fully-applying-matching.patch @@ -0,0 +1,169 @@ +From bf58cba210cd785d3abe6ffbbf174481258dcf5e 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 | 65 ++++++++++++++++++- + ldap/servers/plugins/uiduniq/uid.c | 7 ++ + ldap/servers/slapd/plugin_mr.c | 2 +- + ldap/servers/slapd/str2filter.c | 8 +++ + 4 files changed, 79 insertions(+), 3 deletions(-) + +diff --git a/dirsrvtests/tests/suites/plugins/attruniq_test.py b/dirsrvtests/tests/suites/plugins/attruniq_test.py +index aac659c29..046952df3 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). +@@ -324,4 +324,65 @@ def test_exclude_subtrees(topology_st): + cont2.delete() + cont3.delete() + attruniq.disable() +- attruniq.delete() +\ No newline at end of file ++ 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 9809a4374..757355dbc 100644 +--- a/ldap/servers/slapd/plugin_mr.c ++++ b/ldap/servers/slapd/plugin_mr.c +@@ -625,7 +625,7 @@ attempt_mr_filter_create(mr_filter_t *f, struct slapdplugin *mrp, Slapi_PBlock * + int rc; + int32_t (*mrf_create)(Slapi_PBlock *) = 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/0015-Issue-6872-compressed-log-rotation-creates-files-wit.patch b/0015-Issue-6872-compressed-log-rotation-creates-files-wit.patch new file mode 100644 index 0000000..156fe4b --- /dev/null +++ b/0015-Issue-6872-compressed-log-rotation-creates-files-wit.patch @@ -0,0 +1,163 @@ +From 4719e7df7ba0eb5e26830812ab9ead51f1e0c5f5 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 e859682fe..f535011ab 100644 +--- a/ldap/servers/slapd/log.c ++++ b/ldap/servers/slapd/log.c +@@ -174,17 +174,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; + } + +@@ -193,6 +204,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); +@@ -3402,7 +3414,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); +@@ -3570,7 +3582,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); +@@ -6288,7 +6300,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 { +@@ -6476,7 +6488,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); +@@ -6641,7 +6653,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/0016-Issue-6878-Prevent-repeated-disconnect-logs-during-s.patch b/0016-Issue-6878-Prevent-repeated-disconnect-logs-during-s.patch new file mode 100644 index 0000000..7a328a8 --- /dev/null +++ b/0016-Issue-6878-Prevent-repeated-disconnect-logs-during-s.patch @@ -0,0 +1,116 @@ +From 1cc3715130650d9e778430792c5e2c2e9690cc72 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 209d63b5d..6fd790c18 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 +@@ -1410,6 +1410,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 9f3c374cf..b3ca2e773 100644 +--- a/ldap/servers/slapd/connection.c ++++ b/ldap/servers/slapd/connection.c +@@ -2556,12 +2556,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/0017-Issue-6888-Missing-access-JSON-logging-for-TLS-Clien.patch b/0017-Issue-6888-Missing-access-JSON-logging-for-TLS-Clien.patch new file mode 100644 index 0000000..4ed2247 --- /dev/null +++ b/0017-Issue-6888-Missing-access-JSON-logging-for-TLS-Clien.patch @@ -0,0 +1,590 @@ +From 5ff051f936a5bc4f5e1edb17ee1c3149b66644a2 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Wed, 16 Jul 2025 20:54:48 -0400 +Subject: [PATCH] Issue 6888 - Missing access JSON logging for TLS/Client auth + +Description: + +TLS/Client auth logging was not converted to JSON (auth.c got missed) + +Relates: https://github.com/389ds/389-ds-base/issues/6888 + +Reviewed by: spichugi(Thanks!) +--- + .../logging/access_json_logging_test.py | 96 ++++++++- + ldap/servers/slapd/accesslog.c | 114 +++++++++++ + ldap/servers/slapd/auth.c | 182 +++++++++++++----- + ldap/servers/slapd/log.c | 2 + + ldap/servers/slapd/slapi-private.h | 10 + + 5 files changed, 353 insertions(+), 51 deletions(-) + +diff --git a/dirsrvtests/tests/suites/logging/access_json_logging_test.py b/dirsrvtests/tests/suites/logging/access_json_logging_test.py +index ae91dc487..f0dc861a7 100644 +--- a/dirsrvtests/tests/suites/logging/access_json_logging_test.py ++++ b/dirsrvtests/tests/suites/logging/access_json_logging_test.py +@@ -19,6 +19,8 @@ from lib389.idm.user import UserAccounts + from lib389.dirsrv_log import DirsrvAccessJSONLog + from lib389.index import VLVSearch, VLVIndex + from lib389.tasks import Tasks ++from lib389.config import CertmapLegacy ++from lib389.nss_ssl import NssSsl + from ldap.controls.vlv import VLVRequestControl + from ldap.controls.sss import SSSRequestControl + from ldap.controls import SimplePagedResultsControl +@@ -67,11 +69,11 @@ def get_log_event(inst, op, key=None, val=None, key2=None, val2=None): + if val == str(event[key]).lower() and \ + val2 == str(event[key2]).lower(): + return event +- +- elif key is not None and key in event: +- val = str(val).lower() +- if val == str(event[key]).lower(): +- return event ++ elif key is not None: ++ if key in event: ++ val = str(val).lower() ++ if val == str(event[key]).lower(): ++ return event + else: + return event + +@@ -163,6 +165,7 @@ def test_access_json_format(topo_m2, setup_test): + 14. Test PAGED SEARCH is logged correctly + 15. Test PERSISTENT SEARCH is logged correctly + 16. Test EXTENDED OP ++ 17. Test TLS_INFO is logged correctly + :expectedresults: + 1. Success + 2. Success +@@ -180,6 +183,7 @@ def test_access_json_format(topo_m2, setup_test): + 14. Success + 15. Success + 16. Success ++ 17. Success + """ + + inst = topo_m2.ms["supplier1"] +@@ -560,6 +564,88 @@ def test_access_json_format(topo_m2, setup_test): + assert event['oid_name'] == "REPL_END_NSDS50_REPLICATION_REQUEST_OID" + assert event['name'] == "replication-multisupplier-extop" + ++ # ++ # TLS INFO/TLS CLIENT INFO ++ # ++ RDN_TEST_USER = 'testuser' ++ RDN_TEST_USER_WRONG = 'testuser_wrong' ++ inst.enable_tls() ++ inst.restart() ++ ++ users = UserAccounts(inst, DEFAULT_SUFFIX) ++ user = users.create(properties={ ++ 'uid': RDN_TEST_USER, ++ 'cn': RDN_TEST_USER, ++ 'sn': RDN_TEST_USER, ++ 'uidNumber': '1000', ++ 'gidNumber': '2000', ++ 'homeDirectory': f'/home/{RDN_TEST_USER}' ++ }) ++ ++ ssca_dir = inst.get_ssca_dir() ++ ssca = NssSsl(dbpath=ssca_dir) ++ ssca.create_rsa_user(RDN_TEST_USER) ++ ssca.create_rsa_user(RDN_TEST_USER_WRONG) ++ ++ # Get the details of where the key and crt are. ++ tls_locs = ssca.get_rsa_user(RDN_TEST_USER) ++ tls_locs_wrong = ssca.get_rsa_user(RDN_TEST_USER_WRONG) ++ ++ user.enroll_certificate(tls_locs['crt_der_path']) ++ ++ # Turn on the certmap. ++ cm = CertmapLegacy(inst) ++ certmaps = cm.list() ++ certmaps['default']['DNComps'] = '' ++ certmaps['default']['FilterComps'] = ['cn'] ++ certmaps['default']['VerifyCert'] = 'off' ++ cm.set(certmaps) ++ ++ # Check that EXTERNAL is listed in supported mechns. ++ assert (inst.rootdse.supports_sasl_external()) ++ ++ # Restart to allow certmaps to be re-read: Note, we CAN NOT use post_open ++ # here, it breaks on auth. see lib389/__init__.py ++ inst.restart(post_open=False) ++ ++ # Attempt a bind with TLS external ++ inst.open(saslmethod='EXTERNAL', connOnly=True, certdir=ssca_dir, ++ userkey=tls_locs['key'], usercert=tls_locs['crt']) ++ inst.restart() ++ ++ event = get_log_event(inst, "TLS_INFO") ++ assert event is not None ++ assert 'tls_version' in event ++ assert 'keysize' in event ++ assert 'cipher' in event ++ ++ event = get_log_event(inst, "TLS_CLIENT_INFO", ++ "subject", ++ "CN=testuser,O=testing,L=389ds,ST=Queensland,C=AU") ++ assert event is not None ++ assert 'tls_version' in event ++ assert 'keysize' in event ++ assert 'issuer' in event ++ ++ event = get_log_event(inst, "TLS_CLIENT_INFO", ++ "client_dn", ++ "uid=testuser,ou=People,dc=example,dc=com") ++ assert event is not None ++ assert 'tls_version' in event ++ assert event['msg'] == "client bound" ++ ++ # Check for failed certmap error ++ with pytest.raises(ldap.INVALID_CREDENTIALS): ++ inst.open(saslmethod='EXTERNAL', connOnly=True, certdir=ssca_dir, ++ userkey=tls_locs_wrong['key'], ++ usercert=tls_locs_wrong['crt']) ++ ++ event = get_log_event(inst, "TLS_CLIENT_INFO", "err", -185) ++ assert event is not None ++ assert 'tls_version' in event ++ assert event['msg'] == "failed to map client certificate to LDAP DN" ++ assert event['err_msg'] == "Certificate couldn't be mapped to an ldap entry" ++ + + if __name__ == '__main__': + # Run isolated +diff --git a/ldap/servers/slapd/accesslog.c b/ldap/servers/slapd/accesslog.c +index 68022fe38..072ace203 100644 +--- a/ldap/servers/slapd/accesslog.c ++++ b/ldap/servers/slapd/accesslog.c +@@ -1147,3 +1147,117 @@ slapd_log_access_sort(slapd_log_pblock *logpb) + + return rc; + } ++ ++/* ++ * TLS connection ++ * ++ * int32_t log_format ++ * time_t conn_time ++ * uint64_t conn_id ++ * const char *msg ++ * const char *tls_version ++ * int32_t keysize ++ * const char *cipher ++ * int32_t err ++ * const char *err_str ++ */ ++int32_t ++slapd_log_access_tls(slapd_log_pblock *logpb) ++{ ++ int32_t rc = 0; ++ char *msg = NULL; ++ json_object *json_obj = NULL; ++ ++ if ((json_obj = build_base_obj(logpb, "TLS_INFO")) == NULL) { ++ return rc; ++ } ++ ++ if (logpb->msg) { ++ json_object_object_add(json_obj, "msg", json_obj_add_str(logpb->msg)); ++ } ++ if (logpb->tls_version) { ++ json_object_object_add(json_obj, "tls_version", json_obj_add_str(logpb->tls_version)); ++ } ++ if (logpb->cipher) { ++ json_object_object_add(json_obj, "cipher", json_obj_add_str(logpb->cipher)); ++ } ++ if (logpb->keysize) { ++ json_object_object_add(json_obj, "keysize", json_object_new_int(logpb->keysize)); ++ } ++ if (logpb->err_str) { ++ json_object_object_add(json_obj, "err", json_object_new_int(logpb->err)); ++ json_object_object_add(json_obj, "err_msg", json_obj_add_str(logpb->err_str)); ++ } ++ ++ /* Convert json object to string and log it */ ++ msg = (char *)json_object_to_json_string_ext(json_obj, logpb->log_format); ++ rc = slapd_log_access_json(msg); ++ ++ /* Done with JSON object - free it */ ++ json_object_put(json_obj); ++ ++ return rc; ++} ++ ++/* ++ * TLS client auth ++ * ++ * int32_t log_format ++ * time_t conn_time ++ * uint64_t conn_id ++ * const char* tls_version ++ * const char* keysize ++ * const char* cipher ++ * const char* msg ++ * const char* subject ++ * const char* issuer ++ * int32_t err ++ * const char* err_str ++ * const char *client_dn ++ */ ++int32_t ++slapd_log_access_tls_client_auth(slapd_log_pblock *logpb) ++{ ++ int32_t rc = 0; ++ char *msg = NULL; ++ json_object *json_obj = NULL; ++ ++ if ((json_obj = build_base_obj(logpb, "TLS_CLIENT_INFO")) == NULL) { ++ return rc; ++ } ++ ++ if (logpb->tls_version) { ++ json_object_object_add(json_obj, "tls_version", json_obj_add_str(logpb->tls_version)); ++ } ++ if (logpb->cipher) { ++ json_object_object_add(json_obj, "cipher", json_obj_add_str(logpb->cipher)); ++ } ++ if (logpb->keysize) { ++ json_object_object_add(json_obj, "keysize", json_object_new_int(logpb->keysize)); ++ } ++ if (logpb->subject) { ++ json_object_object_add(json_obj, "subject", json_obj_add_str(logpb->subject)); ++ } ++ if (logpb->issuer) { ++ json_object_object_add(json_obj, "issuer", json_obj_add_str(logpb->issuer)); ++ } ++ if (logpb->client_dn) { ++ json_object_object_add(json_obj, "client_dn", json_obj_add_str(logpb->client_dn)); ++ } ++ if (logpb->msg) { ++ json_object_object_add(json_obj, "msg", json_obj_add_str(logpb->msg)); ++ } ++ if (logpb->err_str) { ++ json_object_object_add(json_obj, "err", json_object_new_int(logpb->err)); ++ json_object_object_add(json_obj, "err_msg", json_obj_add_str(logpb->err_str)); ++ } ++ ++ /* Convert json object to string and log it */ ++ msg = (char *)json_object_to_json_string_ext(json_obj, logpb->log_format); ++ rc = slapd_log_access_json(msg); ++ ++ /* Done with JSON object - free it */ ++ json_object_put(json_obj); ++ ++ return rc; ++} +diff --git a/ldap/servers/slapd/auth.c b/ldap/servers/slapd/auth.c +index e4231bf45..48e4b7129 100644 +--- a/ldap/servers/slapd/auth.c ++++ b/ldap/servers/slapd/auth.c +@@ -1,6 +1,6 @@ + /** BEGIN COPYRIGHT BLOCK + * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission. +- * Copyright (C) 2005 Red Hat, Inc. ++ * Copyright (C) 2025 Red Hat, Inc. + * All rights reserved. + * + * License: GPL (version 3 or any later version). +@@ -363,19 +363,32 @@ handle_bad_certificate(void *clientData, PRFileDesc *prfd) + char sbuf[BUFSIZ], ibuf[BUFSIZ]; + Connection *conn = (Connection *)clientData; + CERTCertificate *clientCert = slapd_ssl_peerCertificate(prfd); +- + PRErrorCode errorCode = PR_GetError(); + char *subject = subject_of(clientCert); + char *issuer = issuer_of(clientCert); +- slapi_log_access(LDAP_DEBUG_STATS, +- "conn=%" PRIu64 " " SLAPI_COMPONENT_NAME_NSPR " error %i (%s); unauthenticated client %s; issuer %s\n", +- conn->c_connid, errorCode, slapd_pr_strerror(errorCode), +- subject ? escape_string(subject, sbuf) : "NULL", +- issuer ? escape_string(issuer, ibuf) : "NULL"); ++ int32_t log_format = config_get_accesslog_log_format(); ++ slapd_log_pblock logpb = {0}; ++ ++ if (log_format != LOG_FORMAT_DEFAULT) { ++ slapd_log_pblock_init(&logpb, log_format, NULL); ++ logpb.conn_id = conn->c_connid; ++ logpb.msg = "unauthenticated client"; ++ logpb.subject = subject ? escape_string(subject, sbuf) : "NULL"; ++ logpb.issuer = issuer ? escape_string(issuer, ibuf) : "NULL"; ++ logpb.err = errorCode; ++ logpb.err_str = slapd_pr_strerror(errorCode); ++ slapd_log_access_tls_client_auth(&logpb); ++ } else { ++ slapi_log_access(LDAP_DEBUG_STATS, ++ "conn=%" PRIu64 " " SLAPI_COMPONENT_NAME_NSPR " error %i (%s); unauthenticated client %s; issuer %s\n", ++ conn->c_connid, errorCode, slapd_pr_strerror(errorCode), ++ subject ? escape_string(subject, sbuf) : "NULL", ++ issuer ? escape_string(issuer, ibuf) : "NULL"); ++ } + if (issuer) +- free(issuer); ++ slapi_ch_free_string(&issuer); + if (subject) +- free(subject); ++ slapi_ch_free_string(&subject); + if (clientCert) + CERT_DestroyCertificate(clientCert); + return -1; /* non-zero means reject this certificate */ +@@ -394,7 +407,8 @@ handle_handshake_done(PRFileDesc *prfd, void *clientData) + { + Connection *conn = (Connection *)clientData; + CERTCertificate *clientCert = slapd_ssl_peerCertificate(prfd); +- ++ int32_t log_format = config_get_accesslog_log_format(); ++ slapd_log_pblock logpb = {0}; + char *clientDN = NULL; + int keySize = 0; + char *cipher = NULL; +@@ -403,19 +417,39 @@ handle_handshake_done(PRFileDesc *prfd, void *clientData) + SSLCipherSuiteInfo cipherInfo; + char *subject = NULL; + char sslversion[64]; ++ int err = 0; + + if ((slapd_ssl_getChannelInfo(prfd, &channelInfo, sizeof(channelInfo))) != SECSuccess) { + PRErrorCode errorCode = PR_GetError(); +- slapi_log_access(LDAP_DEBUG_STATS, +- "conn=%" PRIu64 " SSL failed to obtain channel info; " SLAPI_COMPONENT_NAME_NSPR " error %i (%s)\n", +- conn->c_connid, errorCode, slapd_pr_strerror(errorCode)); ++ if (log_format != LOG_FORMAT_DEFAULT) { ++ slapd_log_pblock_init(&logpb, log_format, NULL); ++ logpb.conn_id = conn->c_connid; ++ logpb.err = errorCode; ++ logpb.err_str = slapd_pr_strerror(errorCode); ++ logpb.msg = "SSL failed to obtain channel info; " SLAPI_COMPONENT_NAME_NSPR; ++ slapd_log_access_tls(&logpb); ++ } else { ++ slapi_log_access(LDAP_DEBUG_STATS, ++ "conn=%" PRIu64 " SSL failed to obtain channel info; " SLAPI_COMPONENT_NAME_NSPR " error %i (%s)\n", ++ conn->c_connid, errorCode, slapd_pr_strerror(errorCode)); ++ } + goto done; + } ++ + if ((slapd_ssl_getCipherSuiteInfo(channelInfo.cipherSuite, &cipherInfo, sizeof(cipherInfo))) != SECSuccess) { + PRErrorCode errorCode = PR_GetError(); +- slapi_log_access(LDAP_DEBUG_STATS, +- "conn=%" PRIu64 " SSL failed to obtain cipher info; " SLAPI_COMPONENT_NAME_NSPR " error %i (%s)\n", +- conn->c_connid, errorCode, slapd_pr_strerror(errorCode)); ++ if (log_format != LOG_FORMAT_DEFAULT) { ++ slapd_log_pblock_init(&logpb, log_format, NULL); ++ logpb.conn_id = conn->c_connid; ++ logpb.err = errorCode; ++ logpb.err_str = slapd_pr_strerror(errorCode); ++ logpb.msg = "SSL failed to obtain cipher info; " SLAPI_COMPONENT_NAME_NSPR; ++ slapd_log_access_tls(&logpb); ++ } else { ++ slapi_log_access(LDAP_DEBUG_STATS, ++ "conn=%" PRIu64 " SSL failed to obtain cipher info; " SLAPI_COMPONENT_NAME_NSPR " error %i (%s)\n", ++ conn->c_connid, errorCode, slapd_pr_strerror(errorCode)); ++ } + goto done; + } + +@@ -434,47 +468,84 @@ handle_handshake_done(PRFileDesc *prfd, void *clientData) + + if (config_get_SSLclientAuth() == SLAPD_SSLCLIENTAUTH_OFF) { + (void)slapi_getSSLVersion_str(channelInfo.protocolVersion, sslversion, sizeof(sslversion)); +- slapi_log_access(LDAP_DEBUG_STATS, "conn=%" PRIu64 " %s %i-bit %s\n", +- conn->c_connid, +- sslversion, keySize, cipher ? cipher : "NULL"); ++ if (log_format != LOG_FORMAT_DEFAULT) { ++ slapd_log_pblock_init(&logpb, log_format, NULL); ++ logpb.conn_id = conn->c_connid; ++ logpb.tls_version = sslversion; ++ logpb.keysize = keySize; ++ logpb.cipher = cipher ? cipher : "NULL"; ++ slapd_log_access_tls(&logpb); ++ } else { ++ slapi_log_access(LDAP_DEBUG_STATS, "conn=%" PRIu64 " %s %i-bit %s\n", ++ conn->c_connid, ++ sslversion, keySize, cipher ? cipher : "NULL"); ++ } + goto done; + } + if (clientCert == NULL) { + (void)slapi_getSSLVersion_str(channelInfo.protocolVersion, sslversion, sizeof(sslversion)); +- slapi_log_access(LDAP_DEBUG_STATS, "conn=%" PRIu64 " %s %i-bit %s\n", +- conn->c_connid, +- sslversion, keySize, cipher ? cipher : "NULL"); ++ if (log_format != LOG_FORMAT_DEFAULT) { ++ slapd_log_pblock_init(&logpb, log_format, NULL); ++ logpb.conn_id = conn->c_connid; ++ logpb.tls_version = sslversion; ++ logpb.keysize = keySize; ++ logpb.cipher = cipher ? cipher : "NULL"; ++ slapd_log_access_tls(&logpb); ++ } else { ++ slapi_log_access(LDAP_DEBUG_STATS, "conn=%" PRIu64 " %s %i-bit %s\n", ++ conn->c_connid, ++ sslversion, keySize, cipher ? cipher : "NULL"); ++ } + } else { + subject = subject_of(clientCert); + if (!subject) { + (void)slapi_getSSLVersion_str(channelInfo.protocolVersion, + sslversion, sizeof(sslversion)); +- slapi_log_access(LDAP_DEBUG_STATS, +- "conn=%" PRIu64 " %s %i-bit %s; missing subject\n", +- conn->c_connid, +- sslversion, keySize, cipher ? cipher : "NULL"); ++ if (log_format != LOG_FORMAT_DEFAULT) { ++ slapd_log_pblock_init(&logpb, log_format, NULL); ++ logpb.conn_id = conn->c_connid; ++ logpb.msg = "missing subject"; ++ logpb.tls_version = sslversion; ++ logpb.keysize = keySize; ++ logpb.cipher = cipher ? cipher : "NULL"; ++ slapd_log_access_tls_client_auth(&logpb); ++ } else { ++ slapi_log_access(LDAP_DEBUG_STATS, ++ "conn=%" PRIu64 " %s %i-bit %s; missing subject\n", ++ conn->c_connid, ++ sslversion, keySize, cipher ? cipher : "NULL"); ++ } + goto done; +- } +- { ++ } else { + char *issuer = issuer_of(clientCert); + char sbuf[BUFSIZ], ibuf[BUFSIZ]; + (void)slapi_getSSLVersion_str(channelInfo.protocolVersion, + sslversion, sizeof(sslversion)); +- slapi_log_access(LDAP_DEBUG_STATS, +- "conn=%" PRIu64 " %s %i-bit %s; client %s; issuer %s\n", +- conn->c_connid, +- sslversion, keySize, +- cipher ? cipher : "NULL", +- escape_string(subject, sbuf), +- issuer ? escape_string(issuer, ibuf) : "NULL"); ++ if (log_format != LOG_FORMAT_DEFAULT) { ++ slapd_log_pblock_init(&logpb, log_format, NULL); ++ logpb.conn_id = conn->c_connid; ++ logpb.tls_version = sslversion; ++ logpb.keysize = keySize; ++ logpb.cipher = cipher ? cipher : "NULL"; ++ logpb.subject = escape_string(subject, sbuf); ++ logpb.issuer = issuer ? escape_string(issuer, ibuf) : "NULL"; ++ slapd_log_access_tls_client_auth(&logpb); ++ } else { ++ slapi_log_access(LDAP_DEBUG_STATS, ++ "conn=%" PRIu64 " %s %i-bit %s; client %s; issuer %s\n", ++ conn->c_connid, ++ sslversion, keySize, ++ cipher ? cipher : "NULL", ++ escape_string(subject, sbuf), ++ issuer ? escape_string(issuer, ibuf) : "NULL"); ++ } + if (issuer) +- free(issuer); ++ slapi_ch_free_string(&issuer); + } + slapi_dn_normalize(subject); + { + LDAPMessage *chain = NULL; + char *basedn = config_get_basedn(); +- int err; + + err = ldapu_cert_to_ldap_entry(clientCert, internal_ld, basedn ? basedn : "" /*baseDN*/, &chain); + if (err == LDAPU_SUCCESS && chain) { +@@ -505,18 +576,37 @@ handle_handshake_done(PRFileDesc *prfd, void *clientData) + slapi_sdn_free(&sdn); + (void)slapi_getSSLVersion_str(channelInfo.protocolVersion, + sslversion, sizeof(sslversion)); +- slapi_log_access(LDAP_DEBUG_STATS, +- "conn=%" PRIu64 " %s client bound as %s\n", +- conn->c_connid, +- sslversion, clientDN); ++ if (log_format != LOG_FORMAT_DEFAULT) { ++ slapd_log_pblock_init(&logpb, log_format, NULL); ++ logpb.conn_id = conn->c_connid; ++ logpb.msg = "client bound"; ++ logpb.tls_version = sslversion; ++ logpb.client_dn = clientDN; ++ slapd_log_access_tls_client_auth(&logpb); ++ } else { ++ slapi_log_access(LDAP_DEBUG_STATS, ++ "conn=%" PRIu64 " %s client bound as %s\n", ++ conn->c_connid, ++ sslversion, clientDN); ++ } + } else if (clientCert != NULL) { + (void)slapi_getSSLVersion_str(channelInfo.protocolVersion, + sslversion, sizeof(sslversion)); +- slapi_log_access(LDAP_DEBUG_STATS, +- "conn=%" PRIu64 " %s failed to map client " +- "certificate to LDAP DN (%s)\n", +- conn->c_connid, +- sslversion, extraErrorMsg); ++ if (log_format != LOG_FORMAT_DEFAULT) { ++ slapd_log_pblock_init(&logpb, log_format, NULL); ++ logpb.conn_id = conn->c_connid; ++ logpb.msg = "failed to map client certificate to LDAP DN"; ++ logpb.tls_version = sslversion; ++ logpb.err = err; ++ logpb.err_str = extraErrorMsg; ++ slapd_log_access_tls_client_auth(&logpb); ++ } else { ++ slapi_log_access(LDAP_DEBUG_STATS, ++ "conn=%" PRIu64 " %s failed to map client " ++ "certificate to LDAP DN (%s)\n", ++ conn->c_connid, ++ sslversion, extraErrorMsg); ++ } + } + + /* +diff --git a/ldap/servers/slapd/log.c b/ldap/servers/slapd/log.c +index f535011ab..91ba23047 100644 +--- a/ldap/servers/slapd/log.c ++++ b/ldap/servers/slapd/log.c +@@ -7270,6 +7270,8 @@ slapd_log_pblock_init(slapd_log_pblock *logpb, int32_t log_format, Slapi_PBlock + slapi_pblock_get(pb, SLAPI_CONNECTION, &conn); + } + ++ memset(logpb, 0, sizeof(slapd_log_pblock)); ++ + logpb->loginfo = &loginfo; + logpb->level = 256; /* default log level */ + logpb->log_format = log_format; +diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h +index 6438a81fe..da232ae2f 100644 +--- a/ldap/servers/slapd/slapi-private.h ++++ b/ldap/servers/slapd/slapi-private.h +@@ -1549,6 +1549,13 @@ typedef struct slapd_log_pblock { + PRBool using_tls; + PRBool haproxied; + const char *bind_dn; ++ /* TLS */ ++ const char *tls_version; ++ int32_t keysize; ++ const char *cipher; ++ const char *subject; ++ const char *issuer; ++ const char *client_dn; + /* Close connection */ + const char *close_error; + const char *close_reason; +@@ -1619,6 +1626,7 @@ typedef struct slapd_log_pblock { + const char *oid; + const char *msg; + const char *name; ++ const char *err_str; + LDAPControl **request_controls; + LDAPControl **response_controls; + } slapd_log_pblock; +@@ -1645,6 +1653,8 @@ int32_t slapd_log_access_entry(slapd_log_pblock *logpb); + int32_t slapd_log_access_referral(slapd_log_pblock *logpb); + int32_t slapd_log_access_extop(slapd_log_pblock *logpb); + int32_t slapd_log_access_sort(slapd_log_pblock *logpb); ++int32_t slapd_log_access_tls(slapd_log_pblock *logpb); ++int32_t slapd_log_access_tls_client_auth(slapd_log_pblock *logpb); + + #ifdef __cplusplus + } +-- +2.49.0 + diff --git a/0018-Issue-6829-Update-parametrized-docstring-for-tests.patch b/0018-Issue-6829-Update-parametrized-docstring-for-tests.patch new file mode 100644 index 0000000..2ed2bb0 --- /dev/null +++ b/0018-Issue-6829-Update-parametrized-docstring-for-tests.patch @@ -0,0 +1,43 @@ +From 091016df4680e1f9ffc3f78292583800626153c2 Mon Sep 17 00:00:00 2001 +From: Barbora Simonova +Date: Thu, 17 Jul 2025 16:46:57 +0200 +Subject: [PATCH] Issue 6829 - Update parametrized docstring for tests + +Description: +Update the rest of missing parametrized values + +Relates: https://github.com/389ds/389-ds-base/issues/6829 + +Reviewed by: @droideck (Thanks!) +--- + dirsrvtests/tests/suites/logging/error_json_logging_test.py | 1 + + .../tests/suites/schema/schema_replication_origin_test.py | 1 + + 2 files changed, 2 insertions(+) + +diff --git a/dirsrvtests/tests/suites/logging/error_json_logging_test.py b/dirsrvtests/tests/suites/logging/error_json_logging_test.py +index 87e1840a6..e0b3d7317 100644 +--- a/dirsrvtests/tests/suites/logging/error_json_logging_test.py ++++ b/dirsrvtests/tests/suites/logging/error_json_logging_test.py +@@ -29,6 +29,7 @@ def test_error_json_format(topo, log_format): + """Test error log is in JSON + + :id: c9afb295-43de-4581-af8b-ec8f25a06d75 ++ :parametrized: yes + :setup: Standalone + :steps: + 1. Check error log has json and the expected data is present +diff --git a/dirsrvtests/tests/suites/schema/schema_replication_origin_test.py b/dirsrvtests/tests/suites/schema/schema_replication_origin_test.py +index 9e4ce498c..e93dddad0 100644 +--- a/dirsrvtests/tests/suites/schema/schema_replication_origin_test.py ++++ b/dirsrvtests/tests/suites/schema/schema_replication_origin_test.py +@@ -157,6 +157,7 @@ def test_schema_xorigin_repl(topology, schema_replication_init, xorigin): + schema is pushed and there is a message in the error log + + :id: 2b29823b-3e83-4b25-954a-8a081dbc15ee ++ :parametrized: yes + :setup: Supplier and consumer topology, with one user entry; + Supplier, hub and consumer topology, with one user entry + :steps: +-- +2.49.0 + diff --git a/0019-Issue-6772-dsconf-Replicas-with-the-consumer-role-al.patch b/0019-Issue-6772-dsconf-Replicas-with-the-consumer-role-al.patch new file mode 100644 index 0000000..8556f9a --- /dev/null +++ b/0019-Issue-6772-dsconf-Replicas-with-the-consumer-role-al.patch @@ -0,0 +1,67 @@ +From 863c244cc137376ee8da0f007fc9b9da88d8dbc0 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/0020-Issue-6893-Log-user-that-is-updated-during-password-.patch b/0020-Issue-6893-Log-user-that-is-updated-during-password-.patch new file mode 100644 index 0000000..80842be --- /dev/null +++ b/0020-Issue-6893-Log-user-that-is-updated-during-password-.patch @@ -0,0 +1,360 @@ +From 79b68019ff4b17c4b80fd2c6c725071a050559ca 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" for the old logging format, and +"EXTENDED_OP_INFO" for json logging 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!!) +--- + .../logging/access_json_logging_test.py | 98 +++++++++++++++---- + ldap/servers/slapd/accesslog.c | 47 +++++++++ + ldap/servers/slapd/passwd_extop.c | 69 +++++++------ + ldap/servers/slapd/slapi-private.h | 1 + + 4 files changed, 169 insertions(+), 46 deletions(-) + +diff --git a/dirsrvtests/tests/suites/logging/access_json_logging_test.py b/dirsrvtests/tests/suites/logging/access_json_logging_test.py +index f0dc861a7..699bd8c4d 100644 +--- a/dirsrvtests/tests/suites/logging/access_json_logging_test.py ++++ b/dirsrvtests/tests/suites/logging/access_json_logging_test.py +@@ -11,7 +11,7 @@ import os + import time + import ldap + import pytest +-from lib389._constants import DEFAULT_SUFFIX, PASSWORD, LOG_ACCESS_LEVEL ++from lib389._constants import DEFAULT_SUFFIX, PASSWORD, LOG_ACCESS_LEVEL, DN_DM + from lib389.properties import TASK_WAIT + from lib389.topologies import topology_m2 as topo_m2 + from lib389.idm.group import Groups +@@ -548,22 +548,6 @@ def test_access_json_format(topo_m2, setup_test): + "2.16.840.1.113730.3.4.3", + "LDAP_CONTROL_PERSISTENTSEARCH") + +- # +- # Extended op +- # +- log.info("Test EXTENDED_OP") +- event = get_log_event(inst, "EXTENDED_OP", "oid", +- "2.16.840.1.113730.3.5.12") +- assert event is not None +- assert event['oid_name'] == "REPL_START_NSDS90_REPLICATION_REQUEST_OID" +- assert event['name'] == "replication-multisupplier-extop" +- +- event = get_log_event(inst, "EXTENDED_OP", "oid", +- "2.16.840.1.113730.3.5.5") +- assert event is not None +- assert event['oid_name'] == "REPL_END_NSDS50_REPLICATION_REQUEST_OID" +- assert event['name'] == "replication-multisupplier-extop" +- + # + # TLS INFO/TLS CLIENT INFO + # +@@ -579,7 +563,8 @@ def test_access_json_format(topo_m2, setup_test): + 'sn': RDN_TEST_USER, + 'uidNumber': '1000', + 'gidNumber': '2000', +- 'homeDirectory': f'/home/{RDN_TEST_USER}' ++ 'homeDirectory': f'/home/{RDN_TEST_USER}', ++ 'userpassword': 'password' + }) + + ssca_dir = inst.get_ssca_dir() +@@ -646,6 +631,83 @@ def test_access_json_format(topo_m2, setup_test): + assert event['msg'] == "failed to map client certificate to LDAP DN" + assert event['err_msg'] == "Certificate couldn't be mapped to an ldap entry" + ++ # ++ # Extended op ++ # ++ log.info("Test EXTENDED_OP") ++ event = get_log_event(inst, "EXTENDED_OP", "oid", ++ "2.16.840.1.113730.3.5.12") ++ assert event is not None ++ assert event['oid_name'] == "REPL_START_NSDS90_REPLICATION_REQUEST_OID" ++ assert event['name'] == "replication-multisupplier-extop" ++ ++ event = get_log_event(inst, "EXTENDED_OP", "oid", ++ "2.16.840.1.113730.3.5.5") ++ assert event is not None ++ assert event['oid_name'] == "REPL_END_NSDS50_REPLICATION_REQUEST_OID" ++ assert event['name'] == "replication-multisupplier-extop" ++ ++ # ++ # Extended op info ++ # ++ log.info("Test EXTENDED_OP_INFO") ++ OLD_PASSWD = 'password' ++ NEW_PASSWD = 'newpassword' ++ ++ assert inst.simple_bind_s(DN_DM, PASSWORD) ++ ++ assert inst.passwd_s(user.dn, OLD_PASSWD, NEW_PASSWD) ++ event = get_log_event(inst, "EXTENDED_OP_INFO", "name", ++ "passwd_modify_plugin") ++ assert event is not None ++ assert event['bind_dn'] == "cn=directory manager" ++ assert event['target_dn'] == user.dn.lower() ++ assert event['msg'] == "success" ++ ++ # Test no such object ++ BAD_DN = user.dn + ",dc=not" ++ with pytest.raises(ldap.NO_SUCH_OBJECT): ++ inst.passwd_s(BAD_DN, OLD_PASSWD, NEW_PASSWD) ++ ++ event = get_log_event(inst, "EXTENDED_OP_INFO", "target_dn", BAD_DN) ++ assert event is not None ++ assert event['bind_dn'] == "cn=directory manager" ++ assert event['target_dn'] == BAD_DN.lower() ++ assert event['msg'] == "No such entry exists." ++ ++ # Test invalid old password ++ with pytest.raises(ldap.INVALID_CREDENTIALS): ++ inst.passwd_s(user.dn, "not_the_old_pw", NEW_PASSWD) ++ event = get_log_event(inst, "EXTENDED_OP_INFO", "err", 49) ++ assert event is not None ++ assert event['bind_dn'] == "cn=directory manager" ++ assert event['target_dn'] == user.dn.lower() ++ assert event['msg'] == "Invalid oldPasswd value." ++ ++ # Test user without permissions ++ user2 = users.create(properties={ ++ 'uid': RDN_TEST_USER + "2", ++ 'cn': RDN_TEST_USER + "2", ++ 'sn': RDN_TEST_USER + "2", ++ 'uidNumber': '1001', ++ 'gidNumber': '2001', ++ 'homeDirectory': f'/home/{RDN_TEST_USER + "2"}', ++ 'userpassword': 'password' ++ }) ++ inst.simple_bind_s(user2.dn, 'password') ++ with pytest.raises(ldap.INSUFFICIENT_ACCESS): ++ inst.passwd_s(user.dn, NEW_PASSWD, OLD_PASSWD) ++ event = get_log_event(inst, "EXTENDED_OP_INFO", "err", 50) ++ assert event is not None ++ assert event['bind_dn'] == user2.dn.lower() ++ assert event['target_dn'] == user.dn.lower() ++ assert event['msg'] == "Insufficient access rights" ++ ++ ++ # Reset bind ++ inst.simple_bind_s(DN_DM, PASSWORD) ++ ++ + + if __name__ == '__main__': + # Run isolated +diff --git a/ldap/servers/slapd/accesslog.c b/ldap/servers/slapd/accesslog.c +index 072ace203..46228d4a1 100644 +--- a/ldap/servers/slapd/accesslog.c ++++ b/ldap/servers/slapd/accesslog.c +@@ -1113,6 +1113,53 @@ slapd_log_access_extop(slapd_log_pblock *logpb) + return rc; + } + ++/* ++ * Extended operation information ++ * ++ * int32_t log_format ++ * time_t conn_time ++ * uint64_t conn_id ++ * int32_t op_id ++ * const char *name ++ * const char *bind_dn ++ * const char *tartet_dn ++ * const char *msg ++ */ ++int32_t ++slapd_log_access_extop_info(slapd_log_pblock *logpb) ++{ ++ int32_t rc = 0; ++ char *msg = NULL; ++ json_object *json_obj = NULL; ++ ++ if ((json_obj = build_base_obj(logpb, "EXTENDED_OP_INFO")) == NULL) { ++ return rc; ++ } ++ ++ if (logpb->name) { ++ json_object_object_add(json_obj, "name", json_obj_add_str(logpb->name)); ++ } ++ if (logpb->target_dn) { ++ json_object_object_add(json_obj, "target_dn", json_obj_add_str(logpb->target_dn)); ++ } ++ if (logpb->bind_dn) { ++ json_object_object_add(json_obj, "bind_dn", json_obj_add_str(logpb->bind_dn)); ++ } ++ if (logpb->msg) { ++ json_object_object_add(json_obj, "msg", json_obj_add_str(logpb->msg)); ++ } ++ json_object_object_add(json_obj, "err", json_object_new_int(logpb->err)); ++ ++ /* Convert json object to string and log it */ ++ msg = (char *)json_object_to_json_string_ext(json_obj, logpb->log_format); ++ rc = slapd_log_access_json(msg); ++ ++ /* Done with JSON object - free it */ ++ json_object_put(json_obj); ++ ++ return rc; ++} ++ + /* + * Sort + * +diff --git a/ldap/servers/slapd/passwd_extop.c b/ldap/servers/slapd/passwd_extop.c +index 4bb60afd6..69bb3494c 100644 +--- a/ldap/servers/slapd/passwd_extop.c ++++ b/ldap/servers/slapd/passwd_extop.c +@@ -465,12 +465,14 @@ 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; ++ int32_t log_format = config_get_accesslog_log_format(); + + slapi_log_err(SLAPI_LOG_TRACE, "passwd_modify_extop", "=>\n"); + +@@ -647,7 +649,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 +726,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 +749,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 +792,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 +840,27 @@ 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) */ ++ if (log_format != LOG_FORMAT_DEFAULT) { ++ /* JSON logging */ ++ slapd_log_pblock logpb = {0}; ++ slapd_log_pblock_init(&logpb, log_format, pb); ++ logpb.name = "passwd_modify_plugin"; ++ logpb.target_dn = dn; ++ logpb.bind_dn = bindDN; ++ logpb.msg = errMesg ? errMesg : "success"; ++ logpb.err = rc; ++ slapd_log_access_extop_info(&logpb); ++ } else { ++ 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 { +diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h +index da232ae2f..e9abf8b75 100644 +--- a/ldap/servers/slapd/slapi-private.h ++++ b/ldap/servers/slapd/slapi-private.h +@@ -1652,6 +1652,7 @@ int32_t slapd_log_access_vlv(slapd_log_pblock *logpb); + int32_t slapd_log_access_entry(slapd_log_pblock *logpb); + int32_t slapd_log_access_referral(slapd_log_pblock *logpb); + int32_t slapd_log_access_extop(slapd_log_pblock *logpb); ++int32_t slapd_log_access_extop_info(slapd_log_pblock *logpb); + int32_t slapd_log_access_sort(slapd_log_pblock *logpb); + int32_t slapd_log_access_tls(slapd_log_pblock *logpb); + int32_t slapd_log_access_tls_client_auth(slapd_log_pblock *logpb); +-- +2.49.0 + diff --git a/0021-Issue-6352-Fix-DeprecationWarning.patch b/0021-Issue-6352-Fix-DeprecationWarning.patch new file mode 100644 index 0000000..49a484a --- /dev/null +++ b/0021-Issue-6352-Fix-DeprecationWarning.patch @@ -0,0 +1,37 @@ +From ab188bdfed7a144734c715f57ba4772c8d453b6f Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Fri, 11 Jul 2025 13:12:44 +0200 +Subject: [PATCH] Issue 6352 - Fix DeprecationWarning + +Bug Description: +When pytest is used on ASAN build, pytest-html plugin collects `*asan*` +files, which results in the following deprecation warning: + +``` +The 'report.extra' attribute is deprecated and will be removed in a +future release, use 'report.extras' instead. +``` + +Fixes: https://github.com/389ds/389-ds-base/issues/6352 + +Reviwed by: @droideck (Thanks!) +--- + dirsrvtests/conftest.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/dirsrvtests/conftest.py b/dirsrvtests/conftest.py +index c989729c1..0db6045f4 100644 +--- a/dirsrvtests/conftest.py ++++ b/dirsrvtests/conftest.py +@@ -138,7 +138,7 @@ def pytest_runtest_makereport(item, call): + log_name = os.path.basename(f) + instance_name = os.path.basename(os.path.dirname(f)).split("slapd-",1)[1] + extra.append(pytest_html.extras.text(text, name=f"{instance_name}-{log_name}")) +- report.extra = extra ++ report.extras = extra + + # Make a screenshot if WebUI test fails + if call.when == "call" and "WEBUI" in os.environ: +-- +2.49.0 + diff --git a/0022-Issue-6880-Fix-ds_logs-test-suite-failure.patch b/0022-Issue-6880-Fix-ds_logs-test-suite-failure.patch new file mode 100644 index 0000000..948fbb4 --- /dev/null +++ b/0022-Issue-6880-Fix-ds_logs-test-suite-failure.patch @@ -0,0 +1,38 @@ +From 5b0188baf00c395dac807657736ed51968d5c3a0 Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Thu, 17 Jul 2025 13:41:04 +0200 +Subject: [PATCH] Issue 6880 - Fix ds_logs test suite failure + +Bug Description: +After 947ee67df6 ds_logs test suite started to fail in +test_internal_log_server_level_4. It slightly changed the order and +timing of log messages. + +Fix Description: +Do another MOD after restart to trigger the internal search. + +Fixes: https://github.com/389ds/389-ds-base/issues/6880 + +Reviewed by: @bsimonova, @droideck (Thanks!) +--- + dirsrvtests/tests/suites/ds_logs/ds_logs_test.py | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py +index 6fd790c18..eff6780cd 100644 +--- a/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py ++++ b/dirsrvtests/tests/suites/ds_logs/ds_logs_test.py +@@ -356,6 +356,10 @@ def test_internal_log_server_level_4(topology_st, clean_access_logs, disable_acc + log.info('Restart the server to flush the logs') + topo.restart() + ++ # After 947ee67 log dynamic has changed slightly ++ # Do another MOD to trigger the internal search ++ topo.config.set(LOG_ACCESS_LEVEL, access_log_level) ++ + try: + # These comments contain lines we are trying to find without regex (the op numbers are just examples) + log.info("Check if access log contains internal MOD operation in correct format") +-- +2.49.0 + diff --git a/0023-Issue-6901-Update-changelog-trimming-logging.patch b/0023-Issue-6901-Update-changelog-trimming-logging.patch new file mode 100644 index 0000000..662c640 --- /dev/null +++ b/0023-Issue-6901-Update-changelog-trimming-logging.patch @@ -0,0 +1,53 @@ +From 13b0a1637b2fb8eb8b6f5fa391721f61bfe41874 Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Thu, 24 Jul 2025 19:09:40 +0200 +Subject: [PATCH] Issue 6901 - Update changelog trimming logging + +Description: +* Set SLAPI_LOG_ERR for message in `_cl5DispatchTrimThread` +* Set correct function name for logs in `_cl5TrimEntry`. +* Add number of scanned entries to the log. + +Fixes: https://github.com/389ds/389-ds-base/issues/6901 + +Reviewed by: @mreynolds389, @progier389 (Thanks!) +--- + ldap/servers/plugins/replication/cl5_api.c | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/ldap/servers/plugins/replication/cl5_api.c b/ldap/servers/plugins/replication/cl5_api.c +index 3c356abc0..1d62aa020 100644 +--- a/ldap/servers/plugins/replication/cl5_api.c ++++ b/ldap/servers/plugins/replication/cl5_api.c +@@ -2007,7 +2007,7 @@ _cl5DispatchTrimThread(Replica *replica) + (void *)replica, PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, + PR_UNJOINABLE_THREAD, DEFAULT_THREAD_STACKSIZE); + if (NULL == pth) { +- slapi_log_err(SLAPI_LOG_REPL, repl_plugin_name_cl, ++ slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name_cl, + "_cl5DispatchTrimThread - Failed to create trimming thread for %s" + "; NSPR error - %d\n", replica_get_name(replica), + PR_GetError()); +@@ -2788,7 +2788,7 @@ _cl5TrimEntry(dbi_val_t *key, dbi_val_t *data, void *ctx) + return DBI_RC_NOTFOUND; + } else { + slapi_log_err(SLAPI_LOG_REPL, repl_plugin_name_cl, +- "_cl5TrimReplica - Changelog purge skipped anchor csn %s\n", ++ "_cl5TrimEntry - Changelog purge skipped anchor csn %s\n", + (char*)key->data); + return DBI_RC_SUCCESS; + } +@@ -2867,8 +2867,8 @@ _cl5TrimReplica(Replica *r) + slapi_ch_free((void**)&dblcictx.rids); + + if (dblcictx.changed.tot) { +- slapi_log_err(SLAPI_LOG_REPL, repl_plugin_name_cl, "_cl5TrimReplica - Trimmed %ld changes from the changelog\n", +- dblcictx.changed.tot); ++ slapi_log_err(SLAPI_LOG_REPL, repl_plugin_name_cl, "_cl5TrimReplica - Scanned %ld records, and trimmed %ld changes from the changelog\n", ++ dblcictx.seen.tot, dblcictx.changed.tot); + } + } + +-- +2.49.0 + diff --git a/0024-Issue-6895-Crash-if-repl-keep-alive-entry-can-not-be.patch b/0024-Issue-6895-Crash-if-repl-keep-alive-entry-can-not-be.patch new file mode 100644 index 0000000..78a41d6 --- /dev/null +++ b/0024-Issue-6895-Crash-if-repl-keep-alive-entry-can-not-be.patch @@ -0,0 +1,98 @@ +From 9cb193418d4dd07182d2e1a38a5cc5a2a41e2877 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/0025-Issue-6250-Add-test-for-entryUSN-overflow-on-failed-.patch b/0025-Issue-6250-Add-test-for-entryUSN-overflow-on-failed-.patch new file mode 100644 index 0000000..706a48c --- /dev/null +++ b/0025-Issue-6250-Add-test-for-entryUSN-overflow-on-failed-.patch @@ -0,0 +1,352 @@ +From a0cdf2970edb46acec06a5ac204ec04135806b35 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/0026-Issue-6594-Add-test-for-numSubordinates-replication-.patch b/0026-Issue-6594-Add-test-for-numSubordinates-replication-.patch new file mode 100644 index 0000000..b099406 --- /dev/null +++ b/0026-Issue-6594-Add-test-for-numSubordinates-replication-.patch @@ -0,0 +1,172 @@ +From 36ca93e8ad2915bcfcae0367051e0f606386f861 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/0027-Issue-6884-Mask-password-hashes-in-audit-logs-6885.patch b/0027-Issue-6884-Mask-password-hashes-in-audit-logs-6885.patch new file mode 100644 index 0000000..2c6d48c --- /dev/null +++ b/0027-Issue-6884-Mask-password-hashes-in-audit-logs-6885.patch @@ -0,0 +1,814 @@ +From 14b1407abc196df947fa50d48946ed072e4ea772 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 1121aef35..7b591e072 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) + { +@@ -282,10 +365,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); ++ } + } + } + } +@@ -300,9 +404,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(); +@@ -312,7 +414,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; + } +@@ -481,6 +583,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: +@@ -505,7 +610,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); + } +@@ -517,8 +627,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++; +@@ -665,6 +778,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: "); +@@ -689,18 +806,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"); +@@ -711,7 +837,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 e9abf8b75..02f22fd2d 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/0028-Issue-6897-Fix-disk-monitoring-test-failures-and-imp.patch b/0028-Issue-6897-Fix-disk-monitoring-test-failures-and-imp.patch new file mode 100644 index 0000000..4d8cf0a --- /dev/null +++ b/0028-Issue-6897-Fix-disk-monitoring-test-failures-and-imp.patch @@ -0,0 +1,1719 @@ +From 189524ddb43adb977c45f34e1982a4a62deac017 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 69d853332..fb19085d7 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,250 +714,415 @@ 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 + 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 + 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 +@@ -711,58 +1143,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/0029-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch b/0029-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch new file mode 100644 index 0000000..6798497 --- /dev/null +++ b/0029-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch @@ -0,0 +1,262 @@ +From c80554be0cea0eb5f2ab6d1e6e1fcef098304f69 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 3e1c5b429..05cabc3a3 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/0030-Issue-6901-Update-changelog-trimming-logging-fix-tes.patch b/0030-Issue-6901-Update-changelog-trimming-logging-fix-tes.patch new file mode 100644 index 0000000..ea74fde --- /dev/null +++ b/0030-Issue-6901-Update-changelog-trimming-logging-fix-tes.patch @@ -0,0 +1,64 @@ +From 2988a4ad320b7a4870cfa055bf7afd009424a15f Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Mon, 28 Jul 2025 13:16:10 +0200 +Subject: [PATCH] Issue 6901 - Update changelog trimming logging - fix tests + +Description: +Update changelog_trimming_test for the new error message. + +Fixes: https://github.com/389ds/389-ds-base/issues/6901 + +Reviewed by: @progier389, @aadhikar (Thanks!) +--- + .../suites/replication/changelog_trimming_test.py | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +diff --git a/dirsrvtests/tests/suites/replication/changelog_trimming_test.py b/dirsrvtests/tests/suites/replication/changelog_trimming_test.py +index 2d70d328e..27d19e8fd 100644 +--- a/dirsrvtests/tests/suites/replication/changelog_trimming_test.py ++++ b/dirsrvtests/tests/suites/replication/changelog_trimming_test.py +@@ -110,7 +110,7 @@ def test_max_age(topo, setup_max_age): + do_mods(supplier, 10) + + time.sleep(1) # Trimming should not have occurred +- if supplier.searchErrorsLog("Trimmed") is True: ++ if supplier.searchErrorsLog("trimmed") is True: + log.fatal('Trimming event unexpectedly occurred') + assert False + +@@ -120,12 +120,12 @@ def test_max_age(topo, setup_max_age): + cl.set_trim_interval('5') + + time.sleep(3) # Trimming should not have occurred +- if supplier.searchErrorsLog("Trimmed") is True: ++ if supplier.searchErrorsLog("trimmed") is True: + log.fatal('Trimming event unexpectedly occurred') + assert False + + time.sleep(3) # Trimming should have occurred +- if supplier.searchErrorsLog("Trimmed") is False: ++ if supplier.searchErrorsLog("trimmed") is False: + log.fatal('Trimming event did not occur') + assert False + +@@ -159,7 +159,7 @@ def test_max_entries(topo, setup_max_entries): + do_mods(supplier, 10) + + time.sleep(1) # Trimming should have occurred +- if supplier.searchErrorsLog("Trimmed") is True: ++ if supplier.searchErrorsLog("trimmed") is True: + log.fatal('Trimming event unexpectedly occurred') + assert False + +@@ -169,7 +169,7 @@ def test_max_entries(topo, setup_max_entries): + cl.set_trim_interval('5') + + time.sleep(6) # Trimming should have occurred +- if supplier.searchErrorsLog("Trimmed") is False: ++ if supplier.searchErrorsLog("trimmed") is False: + log.fatal('Trimming event did not occur') + assert False + +-- +2.49.0 + diff --git a/0031-Issue-6181-RFE-Allow-system-to-manage-uid-gid-at-sta.patch b/0031-Issue-6181-RFE-Allow-system-to-manage-uid-gid-at-sta.patch new file mode 100644 index 0000000..57ebce3 --- /dev/null +++ b/0031-Issue-6181-RFE-Allow-system-to-manage-uid-gid-at-sta.patch @@ -0,0 +1,32 @@ +From 36c97c19dadda7f09a1e2b3d838e12fbdc39af23 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 ada608c86..8d2b96c7e 100644 +--- a/wrappers/systemd.template.service.in ++++ b/wrappers/systemd.template.service.in +@@ -29,7 +29,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/0032-Issue-6468-CLI-Fix-default-error-log-level.patch b/0032-Issue-6468-CLI-Fix-default-error-log-level.patch new file mode 100644 index 0000000..8d27497 --- /dev/null +++ b/0032-Issue-6468-CLI-Fix-default-error-log-level.patch @@ -0,0 +1,31 @@ +From 70256cd0e90b91733516d4434428d04aa55b39bd 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 124556f1f..d9ae1ab16 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/0033-Issue-6768-ns-slapd-crashes-when-a-referral-is-added.patch b/0033-Issue-6768-ns-slapd-crashes-when-a-referral-is-added.patch new file mode 100644 index 0000000..49b4e29 --- /dev/null +++ b/0033-Issue-6768-ns-slapd-crashes-when-a-referral-is-added.patch @@ -0,0 +1,97 @@ +From d668c477158e962ebb6fb25ccabe6d9d09f30259 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 545518748..a5cddfd23 100644 +--- a/ldap/servers/slapd/opshared.c ++++ b/ldap/servers/slapd/opshared.c +@@ -910,7 +910,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/0034-Issues-6913-6886-6250-Adjust-xfail-marks-6914.patch b/0034-Issues-6913-6886-6250-Adjust-xfail-marks-6914.patch new file mode 100644 index 0000000..a905e8a --- /dev/null +++ b/0034-Issues-6913-6886-6250-Adjust-xfail-marks-6914.patch @@ -0,0 +1,222 @@ +From e0d9deaab662468e11b08105e1b155660076b5eb 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 e6fef89cc..61fdf8559 100644 +--- a/dirsrvtests/tests/suites/import/regression_test.py ++++ b/dirsrvtests/tests/suites/import/regression_test.py +@@ -320,7 +320,7 @@ ou: myDups00001 + assert standalone.ds_error_log.match('.*Duplicated DN detected.*') + + @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 +@@ -396,39 +396,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/0035-Issue-6875-Fix-dsidm-tests.patch b/0035-Issue-6875-Fix-dsidm-tests.patch new file mode 100644 index 0000000..791e659 --- /dev/null +++ b/0035-Issue-6875-Fix-dsidm-tests.patch @@ -0,0 +1,378 @@ +From e48f31b756509938d69a626744b8862fc26aa3ef Mon Sep 17 00:00:00 2001 +From: Lenka Doudova +Date: Tue, 15 Jul 2025 17:17:04 +0200 +Subject: [PATCH] Issue 6875 - Fix dsidm tests + +Description: +Adding testing of the "full_dn" option with 'dsidm list' command for all +relevant types of entries +Removing xfail markers in dsidm role tests since the issues were +resolved + +Relates: #6875 + +Author: Lenka Doudova + +Reviewed by: ??? +--- + .../tests/suites/clu/dsidm_group_test.py | 12 +++++++++- + .../clu/dsidm_organizational_unit_test.py | 13 ++++++++++- + .../tests/suites/clu/dsidm_posixgroup_test.py | 13 ++++++++++- + .../tests/suites/clu/dsidm_role_test.py | 23 +++++-------------- + .../tests/suites/clu/dsidm_services_test.py | 13 ++++++++++- + .../suites/clu/dsidm_uniquegroup_test.py | 12 +++++++++- + .../tests/suites/clu/dsidm_user_test.py | 22 +++++++++++++++++- + 7 files changed, 85 insertions(+), 23 deletions(-) + +diff --git a/dirsrvtests/tests/suites/clu/dsidm_group_test.py b/dirsrvtests/tests/suites/clu/dsidm_group_test.py +index 36723a2d0..eba823d2d 100644 +--- a/dirsrvtests/tests/suites/clu/dsidm_group_test.py ++++ b/dirsrvtests/tests/suites/clu/dsidm_group_test.py +@@ -17,7 +17,7 @@ from lib389.cli_idm.group import (list, get, get_dn, create, delete, modify, ren + members, add_member, remove_member) + from lib389.topologies import topology_st + from lib389.cli_base import FakeArgs +-from lib389.utils import ds_is_older, ensure_str ++from lib389.utils import ds_is_older, ensure_str, is_a_dn + from lib389.idm.group import Groups + from . import check_value_in_log_and_reset + +@@ -198,6 +198,7 @@ def test_dsidm_group_list(topology_st, create_test_group): + standalone = topology_st.standalone + args = FakeArgs() + args.json = False ++ args.full_dn = False + json_list = ['type', + 'list', + 'items'] +@@ -214,12 +215,21 @@ def test_dsidm_group_list(topology_st, create_test_group): + list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) + check_value_in_log_and_reset(topology_st, content_list=json_list, check_value=group_name) + ++ log.info('Test full_dn option with list') ++ args.full_dn = True ++ list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ result = topology_st.logcap.get_raw_outputs() ++ json_result = json.loads(result[0]) ++ assert is_a_dn(json_result['items'][0]) ++ args.full_dn = False ++ + log.info('Delete the group') + groups = Groups(standalone, DEFAULT_SUFFIX) + testgroup = groups.get(group_name) + testgroup.delete() + + log.info('Test empty dsidm group list with json') ++ topology_st.logcap.flush() + list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) + check_value_in_log_and_reset(topology_st, content_list=json_list, check_value_not=group_name) + +diff --git a/dirsrvtests/tests/suites/clu/dsidm_organizational_unit_test.py b/dirsrvtests/tests/suites/clu/dsidm_organizational_unit_test.py +index ee908fb22..06556b227 100644 +--- a/dirsrvtests/tests/suites/clu/dsidm_organizational_unit_test.py ++++ b/dirsrvtests/tests/suites/clu/dsidm_organizational_unit_test.py +@@ -11,12 +11,13 @@ import subprocess + import pytest + import logging + import os ++import json + + from lib389 import DEFAULT_SUFFIX + from lib389.cli_idm.organizationalunit import get, get_dn, create, modify, delete, list, rename + from lib389.topologies import topology_st + from lib389.cli_base import FakeArgs +-from lib389.utils import ds_is_older ++from lib389.utils import ds_is_older, is_a_dn + from lib389.idm.organizationalunit import OrganizationalUnits + from . import check_value_in_log_and_reset + +@@ -110,6 +111,7 @@ def test_dsidm_organizational_unit_list(topology_st, create_test_ou): + standalone = topology_st.standalone + args = FakeArgs() + args.json = False ++ args.full_dn = False + json_list = ['type', + 'list', + 'items'] +@@ -126,7 +128,16 @@ def test_dsidm_organizational_unit_list(topology_st, create_test_ou): + list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) + check_value_in_log_and_reset(topology_st, check_value=ou_name) + ++ log.info('Test full_dn option with list') ++ args.full_dn = True ++ list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ result = topology_st.logcap.get_raw_outputs() ++ json_result = json.loads(result[0]) ++ assert is_a_dn(json_result['items'][0]) ++ args.full_dn = False ++ + log.info('Delete the organizational unit') ++ topology_st.logcap.flush() + ous = OrganizationalUnits(standalone, DEFAULT_SUFFIX) + test_ou = ous.get(ou_name) + test_ou.delete() +diff --git a/dirsrvtests/tests/suites/clu/dsidm_posixgroup_test.py b/dirsrvtests/tests/suites/clu/dsidm_posixgroup_test.py +index ccafd3905..10799ee28 100644 +--- a/dirsrvtests/tests/suites/clu/dsidm_posixgroup_test.py ++++ b/dirsrvtests/tests/suites/clu/dsidm_posixgroup_test.py +@@ -9,12 +9,13 @@ + import pytest + import logging + import os ++import json + + from lib389 import DEFAULT_SUFFIX + from lib389.cli_idm.posixgroup import list, get, get_dn, create, delete, modify, rename + from lib389.topologies import topology_st + from lib389.cli_base import FakeArgs +-from lib389.utils import ds_is_older, ensure_str ++from lib389.utils import ds_is_older, ensure_str, is_a_dn + from lib389.idm.posixgroup import PosixGroups + from . import check_value_in_log_and_reset + +@@ -195,6 +196,7 @@ def test_dsidm_posixgroup_list(topology_st, create_test_posixgroup): + standalone = topology_st.standalone + args = FakeArgs() + args.json = False ++ args.full_dn = False + json_list = ['type', + 'list', + 'items'] +@@ -211,12 +213,21 @@ def test_dsidm_posixgroup_list(topology_st, create_test_posixgroup): + list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) + check_value_in_log_and_reset(topology_st, content_list=json_list, check_value=posixgroup_name) + ++ log.info('Test full_dn option with list') ++ args.full_dn = True ++ list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ result = topology_st.logcap.get_raw_outputs() ++ json_result = json.loads(result[0]) ++ assert is_a_dn(json_result['items'][0]) ++ args.full_dn = False ++ + log.info('Delete the posixgroup') + posixgroups = PosixGroups(standalone, DEFAULT_SUFFIX) + test_posixgroup = posixgroups.get(posixgroup_name) + test_posixgroup.delete() + + log.info('Test empty dsidm posixgroup list with json') ++ topology_st.logcap.flush() + list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) + check_value_in_log_and_reset(topology_st, content_list=json_list, check_value_not=posixgroup_name) + +diff --git a/dirsrvtests/tests/suites/clu/dsidm_role_test.py b/dirsrvtests/tests/suites/clu/dsidm_role_test.py +index eb5f692d7..094db2f78 100644 +--- a/dirsrvtests/tests/suites/clu/dsidm_role_test.py ++++ b/dirsrvtests/tests/suites/clu/dsidm_role_test.py +@@ -67,6 +67,7 @@ def create_test_filtered_role(topology_st, request): + + properties = FakeArgs() + properties.cn = filtered_role_name ++ properties.nsrolefilter = "(cn=*)" + create_filtered(topology_st.standalone, DEFAULT_SUFFIX, topology_st.logcap.log, properties) + test_filtered_role = filtered_roles.get(filtered_role_name) + +@@ -92,7 +93,7 @@ def create_test_nested_role(topology_st, create_test_managed_role, request): + + properties = FakeArgs() + properties.cn = nested_role_name +- properties.nsRoleDN = managed_role.dn ++ properties.nsroledn = managed_role.dn + create_nested(topology_st.standalone, DEFAULT_SUFFIX, topology_st.logcap.log, properties) + test_nested_role = nested_roles.get(nested_role_name) + +@@ -341,14 +342,8 @@ def test_dsidm_role_list(topology_st, create_test_managed_role): + @pytest.mark.parametrize( + "role_name, fixture, objectclasses", + [(managed_role_name, 'create_test_managed_role', ['nsSimpleRoleDefinition', 'nsManagedRoleDefinition']), +- (pytest.param(filtered_role_name, +- create_test_filtered_role, +- ['nsComplexRoleDefinition', 'nsFilteredRoleDefinition'], +- marks=pytest.mark.xfail(reason="DS6492"))), +- (pytest.param(nested_role_name, +- create_test_nested_role, +- ['nsComplexRoleDefinition', 'nsNestedRoleDefinition'], +- marks=pytest.mark.xfail(reason="DS6493")))]) ++ (filtered_role_name, 'create_test_filtered_role', ['nsComplexRoleDefinition', 'nsFilteredRoleDefinition']), ++ (nested_role_name, 'create_test_nested_role', ['nsComplexRoleDefinition', 'nsNestedRoleDefinition'])]) + def test_dsidm_role_get(topology_st, role_name, fixture, objectclasses, request): + """ Test dsidm role get option for managed, filtered and nested role + +@@ -422,14 +417,8 @@ def test_dsidm_role_get(topology_st, role_name, fixture, objectclasses, request) + @pytest.mark.parametrize( + "role_name, fixture, objectclasses", + [(managed_role_name, 'create_test_managed_role', ['nsSimpleRoleDefinition', 'nsManagedRoleDefinition']), +- (pytest.param(filtered_role_name, +- create_test_filtered_role, +- ['nsComplexRoleDefinition', 'nsFilteredRoleDefinition'], +- marks=pytest.mark.xfail(reason="DS6492"))), +- (pytest.param(nested_role_name, +- create_test_nested_role, +- ['nsComplexRoleDefinition', 'nsNestedRoleDefinition'], +- marks=pytest.mark.xfail(reason="DS6493")))]) ++ (filtered_role_name, 'create_test_filtered_role', ['nsComplexRoleDefinition', 'nsFilteredRoleDefinition']), ++ (nested_role_name, 'create_test_nested_role', ['nsComplexRoleDefinition', 'nsNestedRoleDefinition'])]) + def test_dsidm_role_get_by_dn(topology_st, role_name, fixture, objectclasses, request): + """ Test dsidm role get-by-dn option for managed, filtered and nested role + +diff --git a/dirsrvtests/tests/suites/clu/dsidm_services_test.py b/dirsrvtests/tests/suites/clu/dsidm_services_test.py +index 61dd0ac11..f167b1c6f 100644 +--- a/dirsrvtests/tests/suites/clu/dsidm_services_test.py ++++ b/dirsrvtests/tests/suites/clu/dsidm_services_test.py +@@ -11,12 +11,13 @@ import subprocess + import pytest + import logging + import os ++import json + + from lib389 import DEFAULT_SUFFIX + from lib389.cli_idm.service import list, get, get_dn, create, delete, modify, rename + from lib389.topologies import topology_st + from lib389.cli_base import FakeArgs +-from lib389.utils import ds_is_older, ensure_str ++from lib389.utils import ds_is_older, ensure_str, is_a_dn + from lib389.idm.services import ServiceAccounts + from . import check_value_in_log_and_reset + +@@ -73,6 +74,7 @@ def test_dsidm_service_list(topology_st, create_test_service): + standalone = topology_st.standalone + args = FakeArgs() + args.json = False ++ args.full_dn = False + service_value = 'test_service' + json_list = ['type', + 'list', +@@ -90,12 +92,21 @@ def test_dsidm_service_list(topology_st, create_test_service): + list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) + check_value_in_log_and_reset(topology_st, content_list=json_list, check_value=service_value) + ++ log.info('Test full_dn option with list') ++ args.full_dn = True ++ list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ result = topology_st.logcap.get_raw_outputs() ++ json_result = json.loads(result[0]) ++ assert is_a_dn(json_result['items'][0]) ++ args.full_dn = False ++ + log.info('Delete the service') + services = ServiceAccounts(topology_st.standalone, DEFAULT_SUFFIX) + testservice = services.get(service_value) + testservice.delete() + + log.info('Test empty dsidm service list with json') ++ topology_st.logcap.flush() + list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) + check_value_in_log_and_reset(topology_st, content_list=json_list, check_value_not=service_value) + +diff --git a/dirsrvtests/tests/suites/clu/dsidm_uniquegroup_test.py b/dirsrvtests/tests/suites/clu/dsidm_uniquegroup_test.py +index 0532791c1..4689ae34b 100644 +--- a/dirsrvtests/tests/suites/clu/dsidm_uniquegroup_test.py ++++ b/dirsrvtests/tests/suites/clu/dsidm_uniquegroup_test.py +@@ -17,7 +17,7 @@ from lib389.cli_idm.uniquegroup import (list, get, get_dn, create, delete, modif + members, add_member, remove_member) + from lib389.topologies import topology_st + from lib389.cli_base import FakeArgs +-from lib389.utils import ds_is_older, ensure_str ++from lib389.utils import ds_is_older, ensure_str, is_a_dn + from lib389.idm.group import UniqueGroups + from . import check_value_in_log_and_reset + +@@ -153,6 +153,7 @@ def test_dsidm_uniquegroup_list(topology_st, create_test_uniquegroup): + standalone = topology_st.standalone + args = FakeArgs() + args.json = False ++ args.full_dn = False + json_list = ['type', + 'list', + 'items'] +@@ -169,12 +170,21 @@ def test_dsidm_uniquegroup_list(topology_st, create_test_uniquegroup): + list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) + check_value_in_log_and_reset(topology_st, content_list=json_list, check_value=uniquegroup_name) + ++ log.info('Test full_dn option with list') ++ args.full_dn = True ++ list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ result = topology_st.logcap.get_raw_outputs() ++ json_result = json.loads(result[0]) ++ assert is_a_dn(json_result['items'][0]) ++ args.full_dn = False ++ + log.info('Delete the uniquegroup') + uniquegroups = UniqueGroups(standalone, DEFAULT_SUFFIX) + test_uniquegroup = uniquegroups.get(uniquegroup_name) + test_uniquegroup.delete() + + log.info('Test empty dsidm uniquegroup list with json') ++ topology_st.logcap.flush() + list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) + check_value_in_log_and_reset(topology_st, content_list=json_list, check_value_not=uniquegroup_name) + +diff --git a/dirsrvtests/tests/suites/clu/dsidm_user_test.py b/dirsrvtests/tests/suites/clu/dsidm_user_test.py +index 4b5491735..620e183ac 100644 +--- a/dirsrvtests/tests/suites/clu/dsidm_user_test.py ++++ b/dirsrvtests/tests/suites/clu/dsidm_user_test.py +@@ -12,12 +12,13 @@ import pytest + import logging + import os + import ldap ++import json + + from lib389 import DEFAULT_SUFFIX + from lib389.cli_idm.user import list, get, get_dn, create, delete, modify, rename + from lib389.topologies import topology_st + from lib389.cli_base import FakeArgs +-from lib389.utils import ds_is_older, ensure_str ++from lib389.utils import ds_is_older, ensure_str, is_a_dn + from lib389.idm.user import nsUserAccounts + from . import check_value_in_log_and_reset + +@@ -74,6 +75,7 @@ def test_dsidm_user_list(topology_st, create_test_user): + standalone = topology_st.standalone + args = FakeArgs() + args.json = False ++ args.full_dn = False + user_value = 'test_user_1000' + json_list = ['type', + 'list', +@@ -92,6 +94,15 @@ def test_dsidm_user_list(topology_st, create_test_user): + list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) + check_value_in_log_and_reset(topology_st, content_list=json_list, check_value=user_value) + ++ log.info('Test full_dn option with list') ++ args.full_dn = True ++ list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ result = topology_st.logcap.get_raw_outputs() ++ json_result = json.loads(result[0]) ++ assert is_a_dn(json_result['items'][0]) ++ args.full_dn = False ++ topology_st.logcap.flush() ++ + log.info('Delete the user') + users = nsUserAccounts(topology_st.standalone, DEFAULT_SUFFIX) + testuser = users.get(user_value) +@@ -777,6 +788,7 @@ def test_dsidm_user_list_rdn_after_rename(topology_st): + log.info('Test dsidm user list without json') + args = FakeArgs() + args.json = False ++ args.full_dn = False + list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) + # Should show the new name, not the original name + check_value_in_log_and_reset(topology_st, check_value=new_name, check_value_not=original_name) +@@ -787,6 +799,14 @@ def test_dsidm_user_list_rdn_after_rename(topology_st): + # Should show the new name in JSON output as well + check_value_in_log_and_reset(topology_st, check_value=new_name, check_value_not=original_name) + ++ log.info('Test full_dn option with list') ++ args.full_dn = True ++ list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ result = topology_st.logcap.get_raw_outputs() ++ json_result = json.loads(result[0]) ++ assert is_a_dn(json_result['items'][0]) ++ args.full_dn = False ++ + log.info('Directly verify RDN extraction works correctly') + renamed_user = users.get(new_name) + rdn_value = renamed_user.get_rdn_from_dn(renamed_user.dn) +-- +2.49.0 + diff --git a/0036-Issue-6519-Add-basic-dsidm-account-tests.patch b/0036-Issue-6519-Add-basic-dsidm-account-tests.patch new file mode 100644 index 0000000..2d7cccf --- /dev/null +++ b/0036-Issue-6519-Add-basic-dsidm-account-tests.patch @@ -0,0 +1,503 @@ +From 10937417415577569bd777aacf7941803e96da21 Mon Sep 17 00:00:00 2001 +From: Lenka Doudova +Date: Mon, 20 Jan 2025 14:19:51 +0100 +Subject: [PATCH] Issue 6519 - Add basic dsidm account tests + +Automating basic dsidm account tests + +Relates to: https://github.com/389ds/389-ds-base/issues/6519 + +Author: Lenka Doudova + +Reviewed by: Simon Pichugin +--- + .../tests/suites/clu/dsidm_account_test.py | 417 +++++++++++++++++- + src/lib389/lib389/cli_idm/account.py | 6 +- + 2 files changed, 409 insertions(+), 14 deletions(-) + +diff --git a/dirsrvtests/tests/suites/clu/dsidm_account_test.py b/dirsrvtests/tests/suites/clu/dsidm_account_test.py +index 4b48a11a5..c600e31fd 100644 +--- a/dirsrvtests/tests/suites/clu/dsidm_account_test.py ++++ b/dirsrvtests/tests/suites/clu/dsidm_account_test.py +@@ -6,22 +6,19 @@ + # See LICENSE for details. + # --- END COPYRIGHT BLOCK --- + # ++ + import logging + import os + import json + import pytest + import ldap + from lib389 import DEFAULT_SUFFIX +-from lib389.cli_idm.account import ( +- get_dn, +- lock, +- unlock, +- entry_status, +- subtree_status, +-) ++from lib389.cli_idm.account import list, get_dn, lock, unlock, delete, modify, rename, entry_status, \ ++ subtree_status, reset_password, change_password ++from lib389.cli_idm.user import create + from lib389.topologies import topology_st + from lib389.cli_base import FakeArgs +-from lib389.utils import ds_is_older ++from lib389.utils import ds_is_older, is_a_dn + from lib389.idm.user import nsUserAccounts + from . import check_value_in_log_and_reset + +@@ -30,13 +27,28 @@ pytestmark = pytest.mark.tier0 + logging.getLogger(__name__).setLevel(logging.DEBUG) + log = logging.getLogger(__name__) + ++test_user_name = 'test_user_1000' + + @pytest.fixture(scope="function") + def create_test_user(topology_st, request): + log.info('Create test user') + users = nsUserAccounts(topology_st.standalone, DEFAULT_SUFFIX) +- test_user = users.create_test_user() +- log.info('Created test user: %s', test_user.dn) ++ ++ if users.exists(test_user_name): ++ test_user = users.get(test_user_name) ++ test_user.delete() ++ ++ properties = FakeArgs() ++ properties.uid = test_user_name ++ properties.cn = test_user_name ++ properties.sn = test_user_name ++ properties.uidNumber = '1000' ++ properties.gidNumber = '2000' ++ properties.homeDirectory = '/home/test_user_1000' ++ properties.displayName = test_user_name ++ ++ create(topology_st.standalone, DEFAULT_SUFFIX, topology_st.logcap.log, properties) ++ test_user = users.get(test_user_name) + + def fin(): + log.info('Delete test user') +@@ -74,7 +86,7 @@ def test_dsidm_account_entry_status_with_lock(topology_st, create_test_user): + + standalone = topology_st.standalone + users = nsUserAccounts(standalone, DEFAULT_SUFFIX) +- test_user = users.get('test_user_1000') ++ test_user = users.get(test_user_name) + + entry_list = ['Entry DN: {}'.format(test_user.dn), + 'Entry Creation Date', +@@ -169,8 +181,389 @@ def test_dsidm_account_entry_get_by_dn(topology_st, create_test_user): + assert json_result['dn'] == user_dn + + ++@pytest.mark.skipif(ds_is_older("1.4.2"), reason="Not implemented") ++def test_dsidm_account_delete(topology_st, create_test_user): ++ """ Test dsidm account delete option ++ ++ :id: a7960bc2-0282-4a82-8dfb-3af2088ec661 ++ :setup: Standalone ++ :steps: ++ 1. Run dsidm account delete on a created account ++ 2. Check that a message is provided on deletion ++ 3. Check that the account no longer exists ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ """ ++ ++ standalone = topology_st.standalone ++ accounts = nsUserAccounts(standalone, DEFAULT_SUFFIX) ++ test_account = accounts.get(test_user_name) ++ output = 'Successfully deleted {}'.format(test_account.dn) ++ ++ args = FakeArgs() ++ args.dn = test_account.dn ++ ++ log.info('Test dsidm account delete') ++ delete(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args, warn=False) ++ check_value_in_log_and_reset(topology_st, check_value=output) ++ ++ log.info('Check that the account no longer exists') ++ assert not test_account.exists() ++ ++ ++@pytest.mark.skipif(ds_is_older("1.4.2"), reason="Not implemented") ++def test_dsidm_account_list(topology_st, create_test_user): ++ """ Test dsidm account list option ++ ++ :id: 4d173a3e-ee36-4a8b-8d0d-4955c792faca ++ :setup: Standalone instance ++ :steps: ++ 1. Run dsidm account list without json ++ 2. Check the output content is correct ++ 3. Run dsidm account list with json ++ 4. Check the output content is correct ++ 5. Test full_dn option with list ++ 6. Delete the account ++ 7. Check the account is not in the list with json ++ 8. Check the account is not in the list without json ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success ++ 5. Success ++ 6. Success ++ 7. Success ++ 8. Success ++ """ ++ ++ standalone = topology_st.standalone ++ args = FakeArgs() ++ args.json = False ++ args.full_dn = False ++ json_list = ['type', ++ 'list', ++ 'items'] ++ ++ log.info('Empty the log file to prevent false data to check about group') ++ topology_st.logcap.flush() ++ ++ log.info('Test dsidm account list without json') ++ list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, check_value=test_user_name) ++ ++ log.info('Test dsidm account list with json') ++ args.json = True ++ list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, content_list=json_list, check_value=test_user_name) ++ ++ log.info('Test full_dn option with list') ++ args.full_dn = True ++ list(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ result = topology_st.logcap.get_raw_outputs() ++ json_result = json.loads(result[0]) ++ assert is_a_dn(json_result['items'][0]) ++ args.full_dn = False ++ topology_st.logcap.flush() ++ ++ log.info('Delete the account') ++ accounts = nsUserAccounts(standalone, DEFAULT_SUFFIX) ++ test_account = accounts.get(test_user_name) ++ test_account.delete() ++ ++ log.info('Test empty dsidm account list with json') ++ list(standalone,DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, content_list=json_list, check_value_not=test_user_name) ++ ++ log.info('Test empty dsidm account list without json') ++ args.json = False ++ list(standalone,DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, check_value_not=test_user_name) ++ ++ ++@pytest.mark.xfail(reason='DS6515') ++@pytest.mark.skipif(ds_is_older("1.4.2"), reason="Not implemented") ++def test_dsidm_account_get_by_dn(topology_st, create_test_user): ++ """ Test dsidm account get-by-dn option ++ ++ :id: 07945577-2da0-4fd9-9237-43dd2823f7b8 ++ :setup: Standalone instance ++ :steps: ++ 1. Run dsidm account get-by-dn for an account without json ++ 2. Check the output content is correct ++ 3. Run dsidm account get-by-dn for an account with json ++ 4. Check the output content is correct ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success ++ """ ++ ++ standalone = topology_st.standalone ++ accounts = nsUserAccounts(standalone, DEFAULT_SUFFIX) ++ test_account = accounts.get(test_user_name) ++ ++ args = FakeArgs() ++ args.dn = test_account.dn ++ args.json = False ++ ++ account_content = ['dn: {}'.format(test_account.dn), ++ 'cn: {}'.format(test_account.rdn), ++ 'displayName: {}'.format(test_user_name), ++ 'gidNumber: 2000', ++ 'homeDirectory: /home/{}'.format(test_user_name), ++ 'objectClass: top', ++ 'objectClass: nsPerson', ++ 'objectClass: nsAccount', ++ 'objectClass: nsOrgPerson', ++ 'objectClass: posixAccount', ++ 'uid: {}'.format(test_user_name), ++ 'uidNumber: 1000'] ++ ++ json_content = ['attrs', ++ 'objectclass', ++ 'top', ++ 'nsPerson', ++ 'nsAccount', ++ 'nsOrgPerson', ++ 'posixAccount', ++ 'cn', ++ test_account.rdn, ++ 'gidnumber', ++ '2000', ++ 'homedirectory', ++ '/home/{}'.format(test_user_name), ++ 'displayname', ++ test_user_name, ++ 'uidnumber', ++ '1000', ++ 'creatorsname', ++ 'cn=directory manager', ++ 'modifiersname', ++ 'createtimestamp', ++ 'modifytimestamp', ++ 'nsuniqueid', ++ 'parentid', ++ 'entryid', ++ 'entryuuid', ++ 'dsentrydn', ++ 'entrydn', ++ test_account.dn] ++ ++ log.info('Empty the log file to prevent false data to check about the account') ++ topology_st.logcap.flush() ++ ++ log.info('Test dsidm account get-by-dn without json') ++ get_dn(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, content_list=account_content) ++ ++ log.info('Test dsidm account get-by-dn with json') ++ args.json = True ++ get_dn(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, content_list=json_content) ++ ++ ++@pytest.mark.skipif(ds_is_older("1.4.2"), reason="Not implemented") ++def test_dsidm_account_modify_by_dn(topology_st, create_test_user): ++ """ Test dsidm account modify-by-dn ++ ++ :id: e7288f8c-f0a8-4d8d-a00f-1b243eb117bc ++ :setup: Standalone instance ++ :steps: ++ 1. Run dsidm account modify-by-dn add description value ++ 2. Run dsidm account modify-by-dn replace description value ++ 3. Run dsidm account modify-by-dn delete description value ++ :expectedresults: ++ 1. A description value is added ++ 2. The original description value is replaced and the previous is not present ++ 3. The replaced description value is deleted ++ """ ++ ++ standalone = topology_st.standalone ++ accounts = nsUserAccounts(standalone, DEFAULT_SUFFIX) ++ test_account = accounts.get(test_user_name) ++ output = 'Successfully modified {}'.format(test_account.dn) ++ ++ args = FakeArgs() ++ args.dn = test_account.dn ++ args.changes = ['add:description:new_description'] ++ ++ log.info('Test dsidm account modify add') ++ modify(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args, warn=False) ++ check_value_in_log_and_reset(topology_st, check_value=output) ++ assert test_account.present('description', 'new_description') ++ ++ log.info('Test dsidm account modify replace') ++ args.changes = ['replace:description:replaced_description'] ++ modify(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args, warn=False) ++ check_value_in_log_and_reset(topology_st, check_value=output) ++ assert test_account.present('description', 'replaced_description') ++ assert not test_account.present('description', 'new_description') ++ ++ log.info('Test dsidm account modify delete') ++ args.changes = ['delete:description:replaced_description'] ++ modify(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args, warn=False) ++ check_value_in_log_and_reset(topology_st, check_value=output) ++ assert not test_account.present('description', 'replaced_description') ++ ++ ++@pytest.mark.skipif(ds_is_older("1.4.2"), reason="Not implemented") ++def test_dsidm_account_rename_by_dn(topology_st, create_test_user): ++ """ Test dsidm account rename-by-dn option ++ ++ :id: f4b8e491-35b1-4113-b9c4-e0a80f8985f3 ++ :setup: Standalone instance ++ :steps: ++ 1. Run dsidm account rename option on existing account ++ 2. Check the account does not have another uid attribute with the old rdn ++ 3. Check the old account is deleted ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ """ ++ ++ standalone = topology_st.standalone ++ accounts = nsUserAccounts(standalone, DEFAULT_SUFFIX) ++ test_account = accounts.get(test_user_name) ++ ++ args = FakeArgs() ++ args.dn = test_account.dn ++ args.new_name = 'renamed_account' ++ args.new_dn = 'uid=renamed_account,ou=people,{}'.format(DEFAULT_SUFFIX) ++ args.keep_old_rdn = False ++ ++ log.info('Test dsidm account rename-by-dn') ++ rename(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ new_account = accounts.get(args.new_name) ++ ++ try: ++ output = 'Successfully renamed to {}'.format(new_account.dn) ++ check_value_in_log_and_reset(topology_st, check_value=output) ++ ++ log.info('Verify the new account does not have a uid attribute with the old rdn') ++ assert not new_account.present('uid', test_user_name) ++ assert new_account.present('displayName', test_user_name) ++ ++ log.info('Verify the old account does not exist') ++ assert not test_account.exists() ++ finally: ++ log.info('Clean up') ++ new_account.delete() ++ ++ ++@pytest.mark.skipif(ds_is_older("1.4.2"), reason="Not implemented") ++def test_dsidm_account_rename_by_dn_keep_old_rdn(topology_st, create_test_user): ++ """ Test dsidm account rename-by-dn option with keep-old-rdn ++ ++ :id: a128bdbb-c0a4-4d9d-9a95-9be2d3780094 ++ :setup: Standalone instance ++ :steps: ++ 1. Run dsidm account rename option on existing account ++ 2. Check the account has another uid attribute with the old rdn ++ 3. Check the old account is deleted ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ """ ++ ++ standalone = topology_st.standalone ++ accounts = nsUserAccounts(standalone, DEFAULT_SUFFIX) ++ test_account = accounts.get(test_user_name) ++ ++ args = FakeArgs() ++ args.dn = test_account.dn ++ args.new_name = 'renamed_account' ++ args.new_dn = 'uid=renamed_account,ou=people,{}'.format(DEFAULT_SUFFIX) ++ args.keep_old_rdn = True ++ ++ log.info('Test dsidm account rename-by-dn') ++ rename(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ new_account = accounts.get(args.new_name) ++ ++ try: ++ output = 'Successfully renamed to {}'.format(new_account.dn) ++ check_value_in_log_and_reset(topology_st, check_value=output) ++ ++ log.info('Verify the new account does not have a uid attribute with the old rdn') ++ assert new_account.present('uid', test_user_name) ++ assert new_account.present('displayName', test_user_name) ++ ++ log.info('Verify the old account does not exist') ++ assert not test_account.exists() ++ finally: ++ log.info('Clean up') ++ new_account.delete() ++ ++ ++@pytest.mark.skipif(ds_is_older("1.4.2"), reason="Not implemented") ++def test_dsidm_account_reset_password(topology_st, create_test_user): ++ """ Test dsidm account reset_password option ++ ++ :id: 02ffa044-08ae-40c5-9108-b02d0c3b0521 ++ :setup: Standalone instance ++ :steps: ++ 1. Run dsidm account reset_password on an existing user ++ 2. Verify that the user has now userPassword attribute set ++ :expectedresults: ++ 1. Success ++ 2. Success ++ """ ++ ++ standalone = topology_st.standalone ++ accounts = nsUserAccounts(standalone, DEFAULT_SUFFIX) ++ test_account = accounts.get(test_user_name) ++ ++ args = FakeArgs() ++ args.dn = test_account.dn ++ args.new_password = 'newpasswd' ++ output = 'reset password for {}'.format(test_account.dn) ++ ++ log.info('Test dsidm account reset_password') ++ reset_password(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, check_value=output) ++ ++ log.info('Verify the userPassword attribute is set') ++ assert test_account.present('userPassword') ++ ++ ++@pytest.mark.skipif(ds_is_older("1.4.2"), reason="Not implemented") ++def test_dsidm_account_change_password(topology_st, create_test_user): ++ """ Test dsidm account change_password option ++ ++ :id: 24c25b8f-df2b-4d43-a88e-47e24bc4ff36 ++ :setup: Standalone instance ++ :steps: ++ 1. Run dsidm account change_password on an existing user ++ 2. Verify that the user has userPassword attribute set ++ :expectedresults: ++ 1. Success ++ 2. Success ++ """ ++ ++ standalone = topology_st.standalone ++ accounts = nsUserAccounts(standalone, DEFAULT_SUFFIX) ++ test_account = accounts.get(test_user_name) ++ ++ args = FakeArgs() ++ args.dn = test_account.dn ++ args.new_password = 'newpasswd' ++ output = 'changed password for {}'.format(test_account.dn) ++ ++ log.info('Test dsidm account change_password') ++ change_password(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, check_value=output) ++ ++ log.info('Verify the userPassword attribute is set') ++ assert test_account.present('userPassword') ++ ++ + if __name__ == '__main__': + # Run isolated + # -s for DEBUG mode + CURRENT_FILE = os.path.realpath(__file__) +- pytest.main("-s %s" % CURRENT_FILE) ++ pytest.main("-s {}".format(CURRENT_FILE)) +\ No newline at end of file +diff --git a/src/lib389/lib389/cli_idm/account.py b/src/lib389/lib389/cli_idm/account.py +index 8b6f99549..9877c533a 100644 +--- a/src/lib389/lib389/cli_idm/account.py ++++ b/src/lib389/lib389/cli_idm/account.py +@@ -12,10 +12,12 @@ import ldap + import math + from datetime import datetime + from lib389.idm.account import Account, Accounts, AccountState +-from lib389.cli_base import ( +- _generic_get_dn, ++from lib389.cli_idm import ( + _generic_list, + _generic_delete, ++ _generic_get_dn ++) ++from lib389.cli_base import ( + _generic_modify_dn, + _get_arg, + _get_dn_arg, +-- +2.49.0 + diff --git a/389-ds-base.spec b/389-ds-base.spec index d2f7131..36c99a2 100644 --- a/389-ds-base.spec +++ b/389-ds-base.spec @@ -82,16 +82,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 @@ -103,6 +103,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 @@ -111,6 +112,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 @@ -125,21 +127,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 @@ -152,16 +155,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 @@ -296,6 +300,39 @@ Source7: Cargo-%{version}-1.lock Patch: 0001-Issue-6822-Backend-creation-cleanup-and-Database-UI-.patch Patch: 0002-Issue-6852-Move-ds-CLI-tools-back-to-sbin.patch Patch: 0003-Issue-6663-Fix-NULL-subsystem-crash-in-JSON-error-lo.patch +Patch: 0004-Issue-6829-Update-parametrized-docstring-for-tests.patch +Patch: 0005-Issue-6782-Improve-paged-result-locking.patch +Patch: 0006-Issue-6838-lib389-replica.py-is-using-nonexistent-da.patch +Patch: 0007-Issue-6753-Add-add_exclude_subtree-and-remove_exclud.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-6854-Refactor-for-improved-data-management-685.patch +Patch: 0011-Issue-6850-AddressSanitizer-memory-leak-in-mdb_init.patch +Patch: 0012-Issue-6848-AddressSanitizer-leak-in-do_search.patch +Patch: 0013-Issue-6865-AddressSanitizer-leak-in-agmt_update_init.patch +Patch: 0014-Issue-6859-str2filter-is-not-fully-applying-matching.patch +Patch: 0015-Issue-6872-compressed-log-rotation-creates-files-wit.patch +Patch: 0016-Issue-6878-Prevent-repeated-disconnect-logs-during-s.patch +Patch: 0017-Issue-6888-Missing-access-JSON-logging-for-TLS-Clien.patch +Patch: 0018-Issue-6829-Update-parametrized-docstring-for-tests.patch +Patch: 0019-Issue-6772-dsconf-Replicas-with-the-consumer-role-al.patch +Patch: 0020-Issue-6893-Log-user-that-is-updated-during-password-.patch +Patch: 0021-Issue-6352-Fix-DeprecationWarning.patch +Patch: 0022-Issue-6880-Fix-ds_logs-test-suite-failure.patch +Patch: 0023-Issue-6901-Update-changelog-trimming-logging.patch +Patch: 0024-Issue-6895-Crash-if-repl-keep-alive-entry-can-not-be.patch +Patch: 0025-Issue-6250-Add-test-for-entryUSN-overflow-on-failed-.patch +Patch: 0026-Issue-6594-Add-test-for-numSubordinates-replication-.patch +Patch: 0027-Issue-6884-Mask-password-hashes-in-audit-logs-6885.patch +Patch: 0028-Issue-6897-Fix-disk-monitoring-test-failures-and-imp.patch +Patch: 0029-Issue-6778-Memory-leak-in-roles_cache_create_object_.patch +Patch: 0030-Issue-6901-Update-changelog-trimming-logging-fix-tes.patch +Patch: 0031-Issue-6181-RFE-Allow-system-to-manage-uid-gid-at-sta.patch +Patch: 0032-Issue-6468-CLI-Fix-default-error-log-level.patch +Patch: 0033-Issue-6768-ns-slapd-crashes-when-a-referral-is-added.patch +Patch: 0034-Issues-6913-6886-6250-Adjust-xfail-marks-6914.patch +Patch: 0035-Issue-6875-Fix-dsidm-tests.patch +Patch: 0036-Issue-6519-Add-basic-dsidm-account-tests.patch %description 389 Directory Server is an LDAPv3 compliant server. The base package includes