Bump version to 3.2.0-3

- Resolves: RHEL-76835 - Web console doesn't show the sub suffix of ou=foo,ou=people,dc=example,dc=com.
- Resolves: RHEL-86320 - [RFE] Support updating/renewing TLS certificate without restarting slapd
- Resolves: RHEL-110192 - ns-slapd doesn't support PQC keys
- Resolves: RHEL-111220 - RHDS-11.9 dsctl db2index --attr recreates all indexes instead of selected ones
- Resolves: RHEL-111931 - Improve error messages for dsconf localpwp list
- Resolves: RHEL-121981 - Setting password history count to 0 does not flush history
- Resolves: RHEL-122625 - ipa-healthcheck is complaining about missing or incorrectly configured system indexes.
- Resolves: RHEL-128906 - Scalability issue of replication online initialization with large database
- Resolves: RHEL-137786 - Upgrading IDM to latest version: 389-ds-base and ipa-server breaks replication
- Resolves: RHEL-146769 - Remove memberof_del_dn_from_groups from MemberOf plugin
This commit is contained in:
Viktor Ashirov 2026-02-20 14:34:11 +01:00
parent e32f517791
commit b70343dc0c
52 changed files with 18219 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,212 @@
From 4c44e4c522afc0f5401754f29a47645889e21aca Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Thu, 5 Feb 2026 12:17:06 +0100
Subject: [PATCH] Issue 7223 - Add upgrade function to remove
nsIndexIDListScanLimit from parentid
Description:
Add `upgrade_remove_index_scanlimit()` function that removes the
nsIndexIDListScanLimit attribute from parentid index configuration
if present.
This attribute was incorrectly added by a previous version and can
cause issues with index configuration. The upgrade function runs
automatically on server startup and removes the attribute if found.
Relates: https://github.com/389ds/389-ds-base/issues/7223
Reviewed by: @progier389, @tbordaz, @droideck (Thanks!)
---
.../healthcheck/health_system_indexes_test.py | 52 +++++++++
ldap/servers/slapd/upgrade.c | 105 ++++++++++++++++++
2 files changed, 157 insertions(+)
diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
index 140845a33..aea88e0e2 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -453,6 +453,58 @@ def test_multiple_missing_indexes(topology_st, log_buffering_enabled):
run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
+def test_upgrade_removes_parentid_scanlimit(topology_st):
+ """Check if upgrade function removes nsIndexIDListScanLimit from parentid index
+
+ :id: 2808886e-c1c1-441d-b3a3-299c4ef1ab4a
+ :setup: Standalone instance
+ :steps:
+ 1. Create DS instance
+ 2. Stop the server
+ 3. Use DSEldif to add nsIndexIDListScanLimit to parentid index
+ 4. Start the server (triggers upgrade)
+ 5. Verify nsIndexIDListScanLimit is removed from parentid index
+ :expectedresults:
+ 1. Success
+ 2. Success
+ 3. Success
+ 4. Success
+ 5. nsIndexIDListScanLimit is no longer present
+ """
+ from lib389.dseldif import DSEldif
+
+ standalone = topology_st.standalone
+ PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
+ SCANLIMIT_VALUE = "limit=5000 type=eq flags=AND"
+
+ log.info("Stop the server")
+ standalone.stop()
+
+ log.info("Add nsIndexIDListScanLimit to parentid index using DSEldif")
+ dse_ldif = DSEldif(standalone)
+ dse_ldif.add(PARENTID_DN, "nsIndexIDListScanLimit", SCANLIMIT_VALUE)
+
+ # Verify it was added
+ scanlimit = dse_ldif.get(PARENTID_DN, "nsIndexIDListScanLimit")
+ assert scanlimit is not None, "Failed to add nsIndexIDListScanLimit"
+ log.info(f"Added nsIndexIDListScanLimit: {scanlimit}")
+
+ log.info("Start the server (triggers upgrade)")
+ standalone.start()
+
+ log.info("Verify nsIndexIDListScanLimit was removed by upgrade")
+ # Check via LDAP - the upgrade should have removed it
+ parentid_index = Index(standalone, PARENTID_DN)
+ scanlimit_after = parentid_index.get_attr_vals_utf8("nsIndexIDListScanLimit")
+ log.info(f"nsIndexIDListScanLimit after upgrade: {scanlimit_after}")
+
+ # The upgrade function should have removed nsIndexIDListScanLimit
+ assert not scanlimit_after, \
+ f"nsIndexIDListScanLimit should have been removed but found: {scanlimit_after}"
+
+ log.info("Upgrade successfully removed nsIndexIDListScanLimit from parentid index")
+
+
if __name__ == "__main__":
# Run isolated
# -s for DEBUG mode
diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
index b02e37ed6..dcd16940b 100644
--- a/ldap/servers/slapd/upgrade.c
+++ b/ldap/servers/slapd/upgrade.c
@@ -330,6 +330,107 @@ upgrade_remove_subtree_rename(void)
return UPGRADE_SUCCESS;
}
+/*
+ * Remove nsIndexIDListScanLimit from parentid index configuration.
+ *
+ * This attribute was incorrectly added by a previous version and can
+ * cause issues with index configuration. Remove it if present.
+ */
+static upgrade_status
+upgrade_remove_index_scanlimit(void)
+{
+ struct slapi_pblock *pb = slapi_pblock_new();
+ Slapi_Entry **backends = NULL;
+ const char *be_base_dn = "cn=ldbm database,cn=plugins,cn=config";
+ const char *be_filter = "(objectclass=nsBackendInstance)";
+ const char *attrs_to_check[] = {"parentid", NULL};
+ upgrade_status uresult = UPGRADE_SUCCESS;
+
+ /* Search for all backend instances */
+ slapi_search_internal_set_pb(
+ pb, be_base_dn,
+ LDAP_SCOPE_ONELEVEL,
+ be_filter, NULL, 0, NULL, NULL,
+ plugin_get_default_component_id(), 0);
+ slapi_search_internal_pb(pb);
+ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &backends);
+
+ if (backends) {
+ for (size_t be_idx = 0; backends[be_idx] != NULL; be_idx++) {
+ const char *be_dn = slapi_entry_get_dn_const(backends[be_idx]);
+ const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
+ if (!be_dn || !be_name) {
+ continue;
+ }
+
+ for (size_t attr_idx = 0; attrs_to_check[attr_idx] != NULL; attr_idx++) {
+ const char *attr_name = attrs_to_check[attr_idx];
+ struct slapi_pblock *idx_pb = slapi_pblock_new();
+ Slapi_Entry **idx_entries = NULL;
+ char *idx_dn = slapi_create_dn_string("cn=%s,cn=index,%s",
+ attr_name, be_dn);
+ char *idx_filter = "(objectclass=nsIndex)";
+
+ if (!idx_dn) {
+ slapi_pblock_destroy(idx_pb);
+ continue;
+ }
+
+ slapi_search_internal_set_pb(
+ idx_pb, idx_dn,
+ LDAP_SCOPE_BASE,
+ idx_filter, NULL, 0, NULL, NULL,
+ plugin_get_default_component_id(), 0);
+ slapi_search_internal_pb(idx_pb);
+ slapi_pblock_get(idx_pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &idx_entries);
+
+ if (idx_entries && idx_entries[0]) {
+ /* Check if nsIndexIDListScanLimit is present */
+ if (slapi_entry_attr_get_ref(idx_entries[0], "nsIndexIDListScanLimit") != NULL) {
+ /* Remove nsIndexIDListScanLimit */
+ Slapi_PBlock *mod_pb = slapi_pblock_new();
+ Slapi_Mods smods;
+ int rc;
+
+ slapi_mods_init(&smods, 1);
+ slapi_mods_add(&smods, LDAP_MOD_DELETE, "nsIndexIDListScanLimit", 0, NULL);
+
+ slapi_modify_internal_set_pb(
+ mod_pb, idx_dn,
+ slapi_mods_get_ldapmods_byref(&smods),
+ NULL, NULL,
+ plugin_get_default_component_id(), 0);
+ slapi_modify_internal_pb(mod_pb);
+ slapi_pblock_get(mod_pb, SLAPI_PLUGIN_INTOP_RESULT, &rc);
+
+ if (rc == LDAP_SUCCESS) {
+ slapi_log_err(SLAPI_LOG_NOTICE, "upgrade_remove_index_scanlimit",
+ "Removed 'nsIndexIDListScanLimit' from index '%s' in backend '%s'\n",
+ attr_name, be_name);
+ } else if (rc != LDAP_NO_SUCH_ATTRIBUTE) {
+ slapi_log_err(SLAPI_LOG_ERR, "upgrade_remove_index_scanlimit",
+ "Failed to remove 'nsIndexIDListScanLimit' from index '%s' in backend '%s': error %d\n",
+ attr_name, be_name, rc);
+ }
+
+ slapi_mods_done(&smods);
+ slapi_pblock_destroy(mod_pb);
+ }
+ }
+
+ slapi_ch_free_string(&idx_dn);
+ slapi_free_search_results_internal(idx_pb);
+ slapi_pblock_destroy(idx_pb);
+ }
+ }
+ }
+
+ slapi_free_search_results_internal(pb);
+ slapi_pblock_destroy(pb);
+
+ return uresult;
+}
+
/*
* Check if parentid/ancestorid indexes are missing the integerOrderingMatch
* matching rule.
@@ -649,6 +750,10 @@ upgrade_server(void)
return UPGRADE_FAILURE;
}
+ if (upgrade_remove_index_scanlimit() != UPGRADE_SUCCESS) {
+ return UPGRADE_FAILURE;
+ }
+
if (upgrade_check_id_index_matching_rule() != UPGRADE_SUCCESS) {
return UPGRADE_FAILURE;
}
--
2.52.0

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -0,0 +1,34 @@
From 7e575cc8cc6f1bf558f50ca0fc55145e469d60d2 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Fri, 13 Feb 2026 16:58:24 +0100
Subject: [PATCH] Issue 7223 - Use lexicographical order for ancestorid (#7256)
Description:
`ldbm_instance_create_default_indexes()` configured ancestorid with
integerOrderingMatch in the in-memory attrinfo, but ancestorid on disk
might be using lexicographic ordering (data before the upgrade or after
ldif2db import).
Relates: https://github.com/389ds/389-ds-base/issues/7223
Reviewed by: @tbordaz (Thanks!)
---
ldap/servers/slapd/back-ldbm/instance.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ldap/servers/slapd/back-ldbm/instance.c b/ldap/servers/slapd/back-ldbm/instance.c
index 17bfc09a0..1569eb7ff 100644
--- a/ldap/servers/slapd/back-ldbm/instance.c
+++ b/ldap/servers/slapd/back-ldbm/instance.c
@@ -231,7 +231,7 @@ ldbm_instance_create_default_indexes(backend *be)
* ancestorid is special, there is actually no such attr type
* but we still want to use the attr index file APIs.
*/
- e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, "integerOrderingMatch");
+ e = ldbm_instance_init_config_entry(LDBM_ANCESTORID_STR, "eq", 0, 0, 0, 0);
attr_index_config(be, "ldbm index init", 0, e, 1, 0, NULL);
slapi_entry_free(e);
--
2.52.0

View File

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

View File

@ -0,0 +1,87 @@
From ac3d9253e0a7a4b5f0108506bcf25255b302fd16 Mon Sep 17 00:00:00 2001
From: Mark Reynolds <mreynolds@redhat.com>
Date: Wed, 11 Feb 2026 15:51:47 -0500
Subject: [PATCH] Issue 7066/7052 - allow password history to be set to zero
and remove history
Description:
For local password policies the server was incorrectly rejecting updates that
set the value to zero. When password history is set to zero the old passwords
in the entry history are not cleaned as expected.
relates: https://github.com/389ds/389-ds-base/issues/7052
relates: https://github.com/389ds/389-ds-base/issues/7066
Reviewed by: progier(Thanks!)
---
.../tests/suites/password/pwp_history_test.py | 7 ++++---
ldap/servers/slapd/modify.c | 2 +-
ldap/servers/slapd/pw.c | 13 ++++++++++++-
3 files changed, 17 insertions(+), 5 deletions(-)
diff --git a/dirsrvtests/tests/suites/password/pwp_history_test.py b/dirsrvtests/tests/suites/password/pwp_history_test.py
index cf68d743c..78b448a87 100644
--- a/dirsrvtests/tests/suites/password/pwp_history_test.py
+++ b/dirsrvtests/tests/suites/password/pwp_history_test.py
@@ -189,9 +189,9 @@ def test_history_is_not_overwritten(topology_st, user):
@pytest.mark.parametrize('policy',
- [(pytest.param('global', marks=pytest.mark.xfail(reason="DS7052"))),
- (pytest.param('subtree', marks=pytest.mark.xfail(reason="DS7066, DS7052"))),
- (pytest.param('user', marks=pytest.mark.xfail(reason="DS7066, DS7052")))])
+ [(pytest.param('global')),
+ (pytest.param('subtree')),
+ (pytest.param('user'))])
def test_basic(topology_st, user, policy):
"""Test basic password policy history feature functionality with dynamic count reduction
@@ -282,6 +282,7 @@ def test_basic(topology_st, user, policy):
# Password history [password3, password4], current password is "password1"
# Reset password by Directory Manager(admin reset)
+ dm = DirectoryManager(topology_st.standalone)
dm.rebind()
time.sleep(.5)
change_password(user, 'password-reset', success=True)
diff --git a/ldap/servers/slapd/modify.c b/ldap/servers/slapd/modify.c
index 9e5bce80b..0ecce9bc8 100644
--- a/ldap/servers/slapd/modify.c
+++ b/ldap/servers/slapd/modify.c
@@ -87,7 +87,7 @@ static struct attr_value_check
{CONFIG_PW_WARNING_ATTRIBUTE, check_pw_duration_value, 0, -1},
{CONFIG_PW_MINLENGTH_ATTRIBUTE, attr_check_minmax, 2, 512},
{CONFIG_PW_MAXFAILURE_ATTRIBUTE, attr_check_minmax, 1, 32767},
- {CONFIG_PW_INHISTORY_ATTRIBUTE, attr_check_minmax, 1, 24},
+ {CONFIG_PW_INHISTORY_ATTRIBUTE, attr_check_minmax, 0, 24},
{CONFIG_PW_LOCKDURATION_ATTRIBUTE, check_pw_duration_value, -1, -1},
{CONFIG_PW_RESETFAILURECOUNT_ATTRIBUTE, check_pw_resetfailurecount_value, -1, -1},
{CONFIG_PW_GRACELIMIT_ATTRIBUTE, attr_check_minmax, 0, -1},
diff --git a/ldap/servers/slapd/pw.c b/ldap/servers/slapd/pw.c
index 055ec0d74..c53ecf23d 100644
--- a/ldap/servers/slapd/pw.c
+++ b/ldap/servers/slapd/pw.c
@@ -1535,7 +1535,18 @@ update_pw_history(Slapi_PBlock *pb, const Slapi_DN *sdn, char *old_pw)
pwpolicy = new_passwdPolicy(pb, dn);
if (pwpolicy->pw_inhistory == 0){
- /* We are only enforcing the current password, just return */
+ /* We are only enforcing the current password, just return but first
+ * cleanup any old passwords in the history */
+ attribute.mod_type = "passwordHistory";
+ attribute.mod_op = LDAP_MOD_REPLACE;
+ attribute.mod_values = NULL;
+ list_of_mods[0] = &attribute;
+ list_of_mods[1] = NULL;
+ mod_pb = slapi_pblock_new();
+ slapi_modify_internal_set_pb_ext(mod_pb, sdn, list_of_mods, NULL, NULL, pw_get_componentID(), 0);
+ slapi_modify_internal_pb(mod_pb);
+ slapi_pblock_destroy(mod_pb);
+
return res;
}
--
2.52.0

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,538 @@
From 6ce19a9a3e36213a5604144aa5eb3cba666e5ed4 Mon Sep 17 00:00:00 2001
From: Viktor Ashirov <vashirov@redhat.com>
Date: Wed, 18 Feb 2026 09:26:57 +0100
Subject: [PATCH] Issue 7223 - Remove integerOrderingMatch requirement for
parentid (#7264)
Description:
integerOrderingMatch was introduced as a requirement for parentid and
ancestorid indexes for performance reasons. But after #7096 the order
for parentid doesn't make a lot of difference.
Fix Description:
* Remove integerOrderingMatch requirement for parentid.
* Read only first 100 keys from dbscan in index ordering check
* Do not run dsctl index-check during RPM upgrade
Relates: https://github.com/389ds/389-ds-base/pull/7223
Reviewed by: @progier389, @tbordaz (Thanks!)
---
.../healthcheck/health_system_indexes_test.py | 83 ++++----------
ldap/servers/slapd/upgrade.c | 105 ------------------
rpm/389-ds-base.spec.in | 3 -
src/lib389/lib389/backend.py | 5 +-
src/lib389/lib389/cli_ctl/dbtasks.py | 99 ++++++++---------
5 files changed, 73 insertions(+), 222 deletions(-)
diff --git a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
index dd42cd197..8dc82c779 100644
--- a/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
+++ b/dirsrvtests/tests/suites/healthcheck/health_system_indexes_test.py
@@ -179,7 +179,8 @@ def test_missing_parentid(topology_st, log_buffering_enabled):
def test_missing_matching_rule(topology_st, log_buffering_enabled):
- """Check if healthcheck returns DSBLE0007 code when parentId index is missing integerOrderingMatch
+ """Check that healthcheck does NOT report DSBLE0007 when parentId index is missing integerOrderingMatch.
+ Both lexicographic and integer orderings are valid for parentid.
:id: 7ffa71db-8995-430a-bed8-59bce944221c
:setup: Standalone instance
@@ -189,19 +190,14 @@ def test_missing_matching_rule(topology_st, log_buffering_enabled):
3. Use healthcheck without --json option
4. Use healthcheck with --json option
5. Re-add the matching rule
- 6. Use healthcheck without --json option
- 7. Use healthcheck with --json option
:expectedresults:
1. Success
2. Success
- 3. healthcheck reports DSBLE0007 code and related details
- 4. healthcheck reports DSBLE0007 code and related details
+ 3. healthcheck reports no issues found
+ 4. healthcheck reports no issues found
5. Success
- 6. healthcheck reports no issues found
- 7. healthcheck reports no issues found
"""
- RET_CODE = "DSBLE0007"
PARENTID_DN = "cn=parentid,cn=index,cn=userroot,cn=ldbm database,cn=plugins,cn=config"
standalone = topology_st.standalone
@@ -210,17 +206,14 @@ def test_missing_matching_rule(topology_st, log_buffering_enabled):
parentid_index = Index(standalone, PARENTID_DN)
parentid_index.remove("nsMatchingRule", "integerOrderingMatch")
- run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=RET_CODE)
- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=RET_CODE)
+ run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
+ run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
log.info("Re-add the integerOrderingMatch matching rule")
parentid_index = Index(standalone, PARENTID_DN)
parentid_index.add("nsMatchingRule", "integerOrderingMatch")
standalone.restart()
- run_healthcheck_and_flush_log(topology_st, standalone, json=False, searched_code=CMD_OUTPUT)
- run_healthcheck_and_flush_log(topology_st, standalone, json=True, searched_code=JSON_OUTPUT)
-
def test_usn_plugin_missing_entryusn(topology_st, usn_plugin_enabled, log_buffering_enabled):
"""Check if healthcheck returns DSBLE0007 code when USN plugin is enabled but entryusn index is missing
@@ -910,7 +903,9 @@ def test_index_check_fixes_ancestorid_config(topology_st):
def test_index_check_fixes_missing_matching_rule(topology_st):
- """Check if dsctl index-check --fix adds missing integerOrderingMatch
+ """Check that removing integerOrderingMatch from parentid config is not
+ flagged as an issue when disk ordering cannot be determined.
+ Both lexicographic and integer orderings are valid for parentid.
:id: 6c1d4e9f-0a3b-4d5c-1e7f-8a9b0c2d3e4f
:setup: Standalone instance
@@ -918,18 +913,14 @@ def test_index_check_fixes_missing_matching_rule(topology_st):
1. Create DS instance
2. Stop the server
3. Remove integerOrderingMatch from parentid index using DSEldif
- 4. Run dsctl index-check (should detect issue)
- 5. Run dsctl index-check --fix
- 6. Verify integerOrderingMatch was added back
- 7. Start the server
+ 4. Run dsctl index-check (should NOT detect issue since disk ordering is unknown)
+ 5. Start the server
:expectedresults:
1. Success
2. Success
3. Success
- 4. index-check returns False and detects missing matching rule
- 5. index-check returns True after fix
- 6. integerOrderingMatch is present
- 7. Success
+ 4. index-check returns True (no issues, disk ordering unknown)
+ 5. Success
"""
from lib389.cli_ctl.dbtasks import dbtasks_index_check
from lib389.dseldif import DSEldif
@@ -963,34 +954,20 @@ def test_index_check_fixes_missing_matching_rule(topology_st):
f"integerOrderingMatch should be removed, but found: {mr}"
log.info("integerOrderingMatch removed from parentid index")
- log.info("Run index-check without --fix (should detect issue)")
+ log.info("Run index-check (should NOT detect issue - disk ordering unknown)")
args = FakeArgs()
args.backend = "userRoot"
args.fix = False
result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
- assert result is False, "index-check should detect missing matching rule"
- assert topology_st.logcap.contains("missing integerOrderingMatch")
+ assert result is True, \
+ "index-check should not flag missing integerOrderingMatch when disk ordering is unknown"
+ assert topology_st.logcap.contains("could not determine disk ordering")
topology_st.logcap.flush()
- log.info("Run index-check with --fix")
- args.fix = True
- result = dbtasks_index_check(standalone, topology_st.logcap.log, args)
- assert result is True, "index-check --fix should succeed"
- assert topology_st.logcap.contains("integerOrderingMatch")
- topology_st.logcap.flush()
-
- log.info("Verify integerOrderingMatch was added back")
- dse_ldif = DSEldif(standalone) # Reload to get fresh data
- matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
- assert matching_rules is not None, "nsMatchingRule should be present"
- found_int_order = False
- for mr in matching_rules:
- if "integerorderingmatch" in mr.lower():
- found_int_order = True
- break
- assert found_int_order, f"integerOrderingMatch should be present, got: {matching_rules}"
- log.info("integerOrderingMatch successfully added back")
+ log.info("Restore integerOrderingMatch and start the server")
+ dse_ldif = DSEldif(standalone)
+ dse_ldif.add(parentid_dn, "nsMatchingRule", "integerOrderingMatch")
log.info("Start the server")
standalone.start()
@@ -1080,7 +1057,7 @@ def test_index_check_fixes_multiple_issues(topology_st):
:steps:
1. Create DS instance
2. Stop the server
- 3. Add multiple issues: scanlimit, ancestorid config, missing matching rule
+ 3. Add multiple issues: scanlimit and ancestorid config
4. Run dsctl index-check (should detect all issues)
5. Run dsctl index-check --fix
6. Verify all issues were fixed
@@ -1122,14 +1099,6 @@ def test_index_check_fixes_multiple_issues(topology_st):
]
dse_ldif.add_entry(ancestorid_entry)
- log.info("Add issue 3: Remove integerOrderingMatch from parentid")
- dse_ldif = DSEldif(standalone) # Reload
- matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
- if matching_rules:
- for mr in matching_rules:
- if "integerorderingmatch" in mr.lower():
- dse_ldif.delete(parentid_dn, "nsMatchingRule", mr)
-
log.info("Run index-check without --fix (should detect all issues)")
args = FakeArgs()
args.backend = "userRoot"
@@ -1160,16 +1129,6 @@ def test_index_check_fixes_multiple_issues(topology_st):
cn_value = dse_ldif.get(ancestorid_dn, "cn", single=True)
assert cn_value is None, f"ancestorid config should be removed, got: {cn_value}"
- # Check matching rule added back
- matching_rules = dse_ldif.get(parentid_dn, "nsMatchingRule")
- found_int_order = False
- if matching_rules:
- for mr in matching_rules:
- if "integerorderingmatch" in mr.lower():
- found_int_order = True
- break
- assert found_int_order, f"integerOrderingMatch should be present, got: {matching_rules}"
-
log.info("All issues verified as fixed")
log.info("Run index-check again to confirm all clear")
diff --git a/ldap/servers/slapd/upgrade.c b/ldap/servers/slapd/upgrade.c
index 6b1b012da..9557e9066 100644
--- a/ldap/servers/slapd/upgrade.c
+++ b/ldap/servers/slapd/upgrade.c
@@ -551,107 +551,6 @@ upgrade_remove_ancestorid_index_config(void)
return uresult;
}
-/*
- * Check if parentid/ancestorid indexes are missing the integerOrderingMatch
- * matching rule.
- *
- * This function logs a warning if we detect this condition, advising
- * the administrator to reindex the affected attributes.
- */
-static upgrade_status
-upgrade_check_id_index_matching_rule(void)
-{
- struct slapi_pblock *pb = slapi_pblock_new();
- Slapi_Entry **backends = NULL;
- const char *be_base_dn = "cn=ldbm database,cn=plugins,cn=config";
- const char *be_filter = "(objectclass=nsBackendInstance)";
- const char *attrs_to_check[] = {"parentid", NULL};
- upgrade_status uresult = UPGRADE_SUCCESS;
-
- /* Search for all backend instances */
- slapi_search_internal_set_pb(
- pb, be_base_dn,
- LDAP_SCOPE_ONELEVEL,
- be_filter, NULL, 0, NULL, NULL,
- plugin_get_default_component_id(), 0);
- slapi_search_internal_pb(pb);
- slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &backends);
-
- if (backends) {
- for (size_t be_idx = 0; backends[be_idx] != NULL; be_idx++) {
- const char *be_dn = slapi_entry_get_dn_const(backends[be_idx]);
- const char *be_name = slapi_entry_attr_get_ref(backends[be_idx], "cn");
- if (!be_dn || !be_name) {
- continue;
- }
-
- /* Check each attribute that should have integerOrderingMatch */
- for (size_t attr_idx = 0; attrs_to_check[attr_idx] != NULL; attr_idx++) {
- const char *attr_name = attrs_to_check[attr_idx];
- struct slapi_pblock *idx_pb = slapi_pblock_new();
- Slapi_Entry **idx_entries = NULL;
- char *idx_dn = slapi_create_dn_string("cn=%s,cn=index,%s",
- attr_name, be_dn);
- char *idx_filter = "(objectclass=nsIndex)";
- PRBool has_matching_rule = PR_FALSE;
-
- if (!idx_dn) {
- slapi_pblock_destroy(idx_pb);
- continue;
- }
-
- slapi_search_internal_set_pb(
- idx_pb, idx_dn,
- LDAP_SCOPE_BASE,
- idx_filter, NULL, 0, NULL, NULL,
- plugin_get_default_component_id(), 0);
- slapi_search_internal_pb(idx_pb);
- slapi_pblock_get(idx_pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &idx_entries);
-
- if (idx_entries && idx_entries[0]) {
- /* Index exists, check if it has integerOrderingMatch */
- Slapi_Attr *mr_attr = NULL;
- if (slapi_entry_attr_find(idx_entries[0], "nsMatchingRule", &mr_attr) == 0) {
- Slapi_Value *sval = NULL;
- int idx;
- for (idx = slapi_attr_first_value(mr_attr, &sval);
- idx != -1;
- idx = slapi_attr_next_value(mr_attr, idx, &sval)) {
- const struct berval *bval = slapi_value_get_berval(sval);
- if (bval && bval->bv_val &&
- strcasecmp(bval->bv_val, "integerOrderingMatch") == 0) {
- has_matching_rule = PR_TRUE;
- break;
- }
- }
- }
-
- if (!has_matching_rule) {
- /* Index exists but doesn't have integerOrderingMatch, log a warning */
- slapi_log_err(SLAPI_LOG_ERR, "upgrade_check_id_index_matching_rule",
- "Index '%s' in backend '%s' is missing 'nsMatchingRule: integerOrderingMatch'. "
- "Incorrectly configured system indexes can lead to poor search performance, replication issues, and other operational problems. "
- "To fix this, add the matching rule and reindex: "
- "dsconf <instance> backend index set --add-mr integerOrderingMatch --attr %s %s && "
- "dsconf <instance> backend index reindex --attr %s %s. "
- "WARNING: Reindexing can be resource-intensive and may impact server performance on a live system. "
- "Consider scheduling reindexing during maintenance windows or periods of low activity.\n",
- attr_name, be_name, attr_name, be_name, attr_name, be_name);
- }
- }
-
- slapi_ch_free_string(&idx_dn);
- slapi_free_search_results_internal(idx_pb);
- slapi_pblock_destroy(idx_pb);
- }
- }
- }
-
- slapi_free_search_results_internal(pb);
- slapi_pblock_destroy(pb);
-
- return uresult;
-}
/*
* Upgrade the base config of the PAM PTA plugin.
@@ -879,10 +778,6 @@ upgrade_server(void)
return UPGRADE_FAILURE;
}
- if (upgrade_check_id_index_matching_rule() != UPGRADE_SUCCESS) {
- return UPGRADE_FAILURE;
- }
-
return UPGRADE_SUCCESS;
}
diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in
index 0e0e28285..370e3abd4 100644
--- a/rpm/389-ds-base.spec.in
+++ b/rpm/389-ds-base.spec.in
@@ -650,9 +650,6 @@ for dir in "$instbase"/slapd-* ; do
else
echo "instance $inst is not running" >> "$output" 2>&1 || :
fi
- # Run index-check on all instances (running or not)
- # This fixes index ordering mismatches from older versions
- dsctl "$inst_name" index-check --fix >> "$output2" 2>&1 || :
ninst=$((ninst + 1))
done
diff --git a/src/lib389/lib389/backend.py b/src/lib389/lib389/backend.py
index f3dbe7c92..6c8cbc018 100644
--- a/src/lib389/lib389/backend.py
+++ b/src/lib389/lib389/backend.py
@@ -647,9 +647,10 @@ class Backend(DSLdapObject):
# Default system indexes taken from ldap/servers/slapd/back-ldbm/instance.c
# Note: entryrdn and ancestorid are internal system indexes that are not
# exposed in cn=config - they are managed internally by the server.
- # Only parentid has a DSE config entry (for the integerOrderingMatch rule).
+ # parentid works correctly with both lexicographic and integer ordering,
+ # so integerOrderingMatch is not required.
expected_system_indexes = {
- 'parentid': {'types': ['eq'], 'matching_rule': 'integerOrderingMatch'},
+ 'parentid': {'types': ['eq'], 'matching_rule': None},
'objectClass': {'types': ['eq'], 'matching_rule': None},
'aci': {'types': ['pres'], 'matching_rule': None},
'nscpEntryDN': {'types': ['eq'], 'matching_rule': None},
diff --git a/src/lib389/lib389/cli_ctl/dbtasks.py b/src/lib389/lib389/cli_ctl/dbtasks.py
index cd96cdaf7..b02de203f 100644
--- a/src/lib389/lib389/cli_ctl/dbtasks.py
+++ b/src/lib389/lib389/cli_ctl/dbtasks.py
@@ -10,6 +10,7 @@
import glob
import os
import re
+import signal
import subprocess
from enum import Enum
from lib389._constants import TaskWarning
@@ -263,45 +264,53 @@ def _check_disk_ordering(db_dir, backend, index_name, dbscan_path, is_mdb, log):
if not index_file:
return IndexOrdering.UNKNOWN
+ # Only read the first 100 lines from dbscan to avoid scanning the
+ # entire index (which can take hours on large databases).
try:
- result = subprocess.run(
+ proc = subprocess.Popen(
[dbscan_path, "-f", index_file],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
- timeout=60,
)
- if result.returncode != 0:
- log.warning(" dbscan returned non-zero exit code for %s", index_file)
- return IndexOrdering.UNKNOWN
-
- # Parse keys from dbscan output
keys = []
- for line in result.stdout.split("\n"):
+ line_count = 0
+ assert proc.stdout is not None
+ for line in proc.stdout:
+ line_count += 1
+ if line_count > 100:
+ break
line = line.strip()
if line.startswith("="):
match = re.match(r"^=(\d+)", line)
if match:
keys.append(int(match.group(1)))
+ proc.terminate()
+ try:
+ proc.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ proc.wait()
+
+ if proc.returncode not in (0, -signal.SIGTERM):
+ log.warning(" dbscan returned non-zero exit code for %s", index_file)
+ return IndexOrdering.UNKNOWN
+
if len(keys) < 2:
return IndexOrdering.UNKNOWN
# Check if keys are in integer order by looking for decreasing numeric values
# (which would indicate lexicographic ordering, e.g., "3" < "30" < "4")
prev_id = keys[0]
- for i in range(1, min(len(keys), 100)):
- current_id = keys[i]
+ for current_id in keys[1:]:
if prev_id > current_id:
return IndexOrdering.LEXICOGRAPHIC
prev_id = current_id
return IndexOrdering.INTEGER
- except subprocess.TimeoutExpired:
- log.warning(" dbscan timed out for %s", index_file)
- return IndexOrdering.UNKNOWN
except OSError as e:
log.warning(" Error running dbscan: %s", e)
return IndexOrdering.UNKNOWN
@@ -375,8 +384,7 @@ def dbtasks_index_check(inst, log, args):
# Track all issues found
all_ok = True
- mismatches = [] # (backend, index_name) tuples needing reindex
- missing_matching_rules = [] # (backend, index_name) tuples missing integerOrderingMatch
+ config_fixes = [] # (backend, index_name, action) tuples: action is "add_mr" or "remove_mr"
scan_limits_to_remove = [] # (backend, index_name) tuples with nsIndexIDListScanLimit
ancestorid_configs_to_remove = [] # backend names with ancestorid config entries
remove_ancestorid_from_defaults = False # Flag to remove from cn=default indexes
@@ -409,13 +417,6 @@ def dbtasks_index_check(inst, log, args):
if disk_ordering == IndexOrdering.UNKNOWN:
log.info(" %s - could not determine disk ordering, skipping", index_name)
- # For parentid, still check if matching rule is missing
- if index_name == "parentid":
- config_has_int_order = _has_integer_ordering_match(dse_ldif, backend, index_name)
- if not config_has_int_order:
- log.warning(" %s - missing integerOrderingMatch in config", index_name)
- missing_matching_rules.append((backend, index_name))
- all_ok = False
continue
config_has_int_order = _has_integer_ordering_match(dse_ldif, backend, index_name)
@@ -423,18 +424,15 @@ def dbtasks_index_check(inst, log, args):
log.info(" %s - config: %s, disk: %s",
index_name, config_desc, disk_ordering.value)
- # For parentid, the desired state is always integer ordering
+ # Both orderings are valid for parentid, but config must match disk.
if index_name == "parentid":
- if not config_has_int_order:
- log.warning(" %s - missing integerOrderingMatch in config", index_name)
- if (backend, index_name) not in missing_matching_rules:
- missing_matching_rules.append((backend, index_name))
+ if config_has_int_order and disk_ordering == IndexOrdering.LEXICOGRAPHIC:
+ log.warning(" %s - MISMATCH: config has integerOrderingMatch but disk is lexicographic", index_name)
+ config_fixes.append((backend, index_name, "remove_mr"))
all_ok = False
-
- if disk_ordering == IndexOrdering.LEXICOGRAPHIC:
- log.warning(" %s - disk ordering is lexicographic, needs reindex", index_name)
- if (backend, index_name) not in mismatches:
- mismatches.append((backend, index_name))
+ elif not config_has_int_order and disk_ordering == IndexOrdering.INTEGER:
+ log.warning(" %s - MISMATCH: config is lexicographic but disk has integer ordering", index_name)
+ config_fixes.append((backend, index_name, "add_mr"))
all_ok = False
# Handle issues
@@ -480,26 +478,27 @@ def dbtasks_index_check(inst, log, args):
log.error(" Failed to remove ancestorid config from backend %s: %s", backend, e)
return False
- # Add missing matching rules to dse.ldif
- for backend, index_name in missing_matching_rules:
+ # Fix config-vs-disk ordering mismatches by adjusting config to match disk
+ for backend, index_name, action in config_fixes:
index_dn = "cn={},cn=index,cn={},cn=ldbm database,cn=plugins,cn=config".format(
index_name, backend
)
- log.info(" Adding integerOrderingMatch to %s in backend %s...", index_name, backend)
- try:
- dse_ldif.add(index_dn, "nsMatchingRule", "integerOrderingMatch")
- log.info(" Updated dse.ldif with integerOrderingMatch for %s", index_name)
- except Exception as e:
- log.error(" Failed to update dse.ldif for %s: %s", index_name, e)
- return False
-
- # Reindex indexes with disk ordering issues
- for backend, index_name in mismatches:
- log.info(" Reindexing %s in backend %s...", index_name, backend)
- if not inst.db2index(bename=backend, attrs=[index_name]):
- log.error(" Failed to reindex %s", index_name)
- return False
- log.info(" Reindex of %s completed successfully", index_name)
+ if action == "add_mr":
+ log.info(" Adding integerOrderingMatch to %s in backend %s...", index_name, backend)
+ try:
+ dse_ldif.add(index_dn, "nsMatchingRule", "integerOrderingMatch")
+ log.info(" Updated dse.ldif with integerOrderingMatch for %s", index_name)
+ except Exception as e:
+ log.error(" Failed to update dse.ldif for %s: %s", index_name, e)
+ return False
+ elif action == "remove_mr":
+ log.info(" Removing integerOrderingMatch from %s in backend %s...", index_name, backend)
+ try:
+ dse_ldif.delete(index_dn, "nsMatchingRule", "integerOrderingMatch")
+ log.info(" Removed integerOrderingMatch from %s", index_name)
+ except Exception as e:
+ log.error(" Failed to remove integerOrderingMatch from %s: %s", index_name, e)
+ return False
log.info("All issues fixed")
return True
@@ -563,5 +562,5 @@ def create_parser(subcommands):
index_check_parser.add_argument('backend', nargs='?', default=None,
help="Backend to check. If not specified, all backends are checked.")
index_check_parser.add_argument('--fix', action='store_true', default=False,
- help="Fix mismatches by reindexing affected indexes")
+ help="Fix mismatches by adjusting config to match on-disk data")
index_check_parser.set_defaults(func=dbtasks_index_check)
--
2.52.0

View File

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

View File

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

View File

@ -286,6 +286,57 @@ Patch: 0002-Issue-Revise-paged-result-search-locking.patch
Patch: 0003-Issue-7108-Fix-shutdown-crash-in-entry-cache-destruc.patch
Patch: 0004-Issue-7172-Index-ordering-mismatch-after-upgrade-717.patch
Patch: 0005-Issue-7172-2nd-Index-ordering-mismatch-after-upgrade.patch
Patch: 0006-Issue-6753-Port-ticket-548-test-7101.patch
Patch: 0007-Issue-7152-ns-slapd-fails-to-shutdown-when-deferred-.patch
Patch: 0008-Issue-7169-Fix-automember_plugin-CI-test-failures-71.patch
Patch: 0009-Issue-6758-Use-OUIA-selectors-for-WebUI-plugin-tests.patch
Patch: 0010-Issue-7196-DynamicCertificates-returns-empty-DER-719.patch
Patch: 0011-Issue-7189-DSBLE0007-generates-incorrect-remediation.patch
Patch: 0012-Issue-7170-Support-of-PQC-keys-7188.patch
Patch: 0013-Bump-lodash-from-4.17.21-to-4.17.23-in-src-cockpit-3.patch
Patch: 0014-Issue-7198-Web-console-doesn-t-show-sub-suffix-when-.patch
Patch: 0015-Issue-7014-memberOf-ignored-deferred-updates-with-LM.patch
Patch: 0016-Issue-7184-argparse.HelpFormatter-_format_actions_us.patch
Patch: 0017-Issue-6947-Revise-time-skew-check-in-healthcheck-too.patch
Patch: 0018-Issue-7201-Syscall-overhead-in-LMDB-import-writer-th.patch
Patch: 0019-Issue-7096-2nd-During-replication-online-total-init-.patch
Patch: 0020-Issue-7206-Should-log-whether-TLS-key-is-PQC-or-not-.patch
Patch: 0021-Issue-7027-2nd-389-ds-base-OpenScanHub-Leaks-Detecte.patch
Patch: 0022-Issue-7213-MDB_BAD_VALSIZE-error-while-handling-VLV-.patch
Patch: 0023-Issue-6753-Port-ticket-47781-test-7210.patch
Patch: 0024-Issue-7194-Repl-Log-Analysis-Add-CSN-propagation-det.patch
Patch: 0025-Issue-6753-Port-ticket-48896-test.patch
Patch: 0026-Issue-6810-Fix-PAM-PTA-test-7219.patch
Patch: 0027-Issue-7076-Fix-revert_cache-never-called-in-modrdn-7.patch
Patch: 0028-Issue-6951-Dynamic-Certificate-refresh-phase-4-Updat.patch
Patch: 0029-Issue-7224-CI-Test-Simplify-test_reserve_descriptor_.patch
Patch: 0030-Issue-7178-Bundled-jemalloc-fails-to-build-with-GCC-.patch
Patch: 0031-Issue-7121-2nd-LeakSanitizer-various-leaks-during-re.patch
Patch: 0032-Issue-7223-Revert-index-scan-limits-for-system-index.patch
Patch: 0033-Issue-7223-Add-upgrade-function-to-remove-nsIndexIDL.patch
Patch: 0034-Issue-7223-Add-upgrade-function-to-remove-ancestorid.patch
Patch: 0035-Issue-7223-Detect-and-log-index-ordering-mismatch-du.patch
Patch: 0036-Issue-7223-Add-dsctl-index-check-command-for-offline.patch
Patch: 0037-Issue-7230-Regression-in-healtcheck-NssCheck-7235.patch
Patch: 0038-Issue-3555-UI-Fix-audit-issue-with-npm-isaacs-brace-.patch
Patch: 0039-Issue-7221-CI-tests-fix-some-flaky-tests.patch
Patch: 0040-Issue-7233-test_produce_division_by_zero-fails-with-.patch
Patch: 0041-Issue-7241-Drop-dateutil-7242.patch
Patch: 0042-Issue-7231-Sync-repl-tests-fail-in-FIPS-mode-due-to-.patch
Patch: 0043-Issue-7248-CLI-attribute-uniqueness-fix-usage-for-ex.patch
Patch: 0044-Issue-CLI-dsctl-db2index-needs-some-hardening-with-M.patch
Patch: 0045-Issue-7252-PQC-Need-to-iterate-on-SECOidTag-instead-.patch
Patch: 0046-Issue-6951-Dynamic-Certificas-Refresh-CI-tests-7238.patch
Patch: 0047-Issue-7184-2nd-argparse.HelpFormatter-_format_action.patch
Patch: 0048-Issue-7213-2nd-MDB_BAD_VALSIZE-error-while-handling-.patch
Patch: 0049-Issue-7223-Use-lexicographical-order-for-ancestorid-.patch
Patch: 0050-Issue-3134-Fix-build-break-7260.patch
Patch: 0051-Issue-7066-7052-allow-password-history-to-be-set-to-.patch
Patch: 0052-Issue-7243-UI-add-support-for-hot-certificates.patch
Patch: 0053-Issue-6758-Fix-Enable-Replication-dropdown-not-openi.patch
Patch: 0054-Issue-7223-Remove-integerOrderingMatch-requirement-f.patch
Patch: 0055-Issue-7236-Fix-GSSAPI-tests-7237.patch
Patch: 0056-Issue-6753-Port-ticket-49039-test.patch
%description
389 Directory Server is an LDAPv3 compliant server. The base package includes