From 920ecff2d499b8534ec1bd6bf7b76ec27b742643 Mon Sep 17 00:00:00 2001 From: Andrew Lukoshko Date: Tue, 3 Jun 2025 08:53:59 +0000 Subject: [PATCH] import CS 389-ds-base-1.4.3.39-13.el8 --- ...agent-fails-to-start-because-of-perm.patch | 520 ++++++++++++++++++ ...DAP-version-autodetection-doesn-t-wo.patch | 60 ++ .../0036-Issue-1925-Add-a-CI-test-5936.patch | 245 +++++++++ ...arious-errors-when-using-extended-ma.patch | 75 +++ ...arious-errors-when-using-extended-ma.patch | 45 ++ ...arious-errors-when-using-extended-ma.patch | 72 +++ ...9-Configure-replication-for-multiple.patch | 357 ++++++++++++ ...eplication-release-replica-decoding-.patch | 126 +++++ .../0042-Issue-6655-fix-merge-conflict.patch | 26 + ...d-group-does-not-receive-memberOf-at.patch | 291 ++++++++++ ...ested-group-does-not-receive-memberO.patch | 272 +++++++++ ...fter-configuring-invalid-filtered-ro.patch | 192 +++++++ ...e-enabling-user-accounts-that-reache.patch | 455 +++++++++++++++ ...-to-run-replication-status-without-a.patch | 70 +++ SPECS/389-ds-base.spec | 56 +- 15 files changed, 2848 insertions(+), 14 deletions(-) create mode 100644 SOURCES/0034-Issue-6155-ldap-agent-fails-to-start-because-of-perm.patch create mode 100644 SOURCES/0035-Issue-5305-OpenLDAP-version-autodetection-doesn-t-wo.patch create mode 100644 SOURCES/0036-Issue-1925-Add-a-CI-test-5936.patch create mode 100644 SOURCES/0037-Issue-6494-2nd-Various-errors-when-using-extended-ma.patch create mode 100644 SOURCES/0038-Issue-6494-3rd-Various-errors-when-using-extended-ma.patch create mode 100644 SOURCES/0039-Issue-6494-4th-Various-errors-when-using-extended-ma.patch create mode 100644 SOURCES/0040-Issue-6497-lib389-Configure-replication-for-multiple.patch create mode 100644 SOURCES/0041-Issue-6655-fix-replication-release-replica-decoding-.patch create mode 100644 SOURCES/0042-Issue-6655-fix-merge-conflict.patch create mode 100644 SOURCES/0043-Issue-6571-Nested-group-does-not-receive-memberOf-at.patch create mode 100644 SOURCES/0044-Issue-6571-2nd-Nested-group-does-not-receive-memberO.patch create mode 100644 SOURCES/0045-Issue-6698-NPE-after-configuring-invalid-filtered-ro.patch create mode 100644 SOURCES/0046-Issue-6686-CLI-Re-enabling-user-accounts-that-reache.patch create mode 100644 SOURCES/0047-Issue-6302-Allow-to-run-replication-status-without-a.patch diff --git a/SOURCES/0034-Issue-6155-ldap-agent-fails-to-start-because-of-perm.patch b/SOURCES/0034-Issue-6155-ldap-agent-fails-to-start-because-of-perm.patch new file mode 100644 index 0000000..a9cb528 --- /dev/null +++ b/SOURCES/0034-Issue-6155-ldap-agent-fails-to-start-because-of-perm.patch @@ -0,0 +1,520 @@ +From b8c079c770d3eaa4de49e997d42e1501c28a153b Mon Sep 17 00:00:00 2001 +From: progier389 +Date: Mon, 8 Jul 2024 11:19:09 +0200 +Subject: [PATCH] Issue 6155 - ldap-agent fails to start because of permission + error (#6179) + +Issue: dirsrv-snmp service fails to starts when SELinux is enforced because of AVC preventing to open some files +One workaround is to use the dac_override capability but it is a bad practice. +Fix: Setting proper permissions: + +Running ldap-agent with uid=root and gid=dirsrv to be able to access both snmp and dirsrv resources. +Setting read permission on the group for the dse.ldif file +Setting r/w permissions on the group for the snmp semaphore and mmap file +For that one special care is needed because ns-slapd umask overrides the file creation permission +as is better to avoid changing the umask (changing umask within the code is not thread safe, +and the current 0022 umask value is correct for most of the files) so the safest way is to chmod the snmp file +if the needed permission are not set. +Issue: #6155 + +Reviewed by: @droideck , @vashirov (Thanks ! ) + +(cherry picked from commit eb7e57d77b557b63c65fdf38f9069893b021f049) +--- + .github/scripts/generate_matrix.py | 4 +- + dirsrvtests/tests/suites/snmp/snmp.py | 214 ++++++++++++++++++++++++++ + ldap/servers/slapd/agtmmap.c | 72 ++++++++- + ldap/servers/slapd/agtmmap.h | 13 ++ + ldap/servers/slapd/dse.c | 6 +- + ldap/servers/slapd/slap.h | 6 + + ldap/servers/slapd/snmp_collator.c | 4 +- + src/lib389/lib389/instance/setup.py | 5 + + wrappers/systemd-snmp.service.in | 1 + + 9 files changed, 313 insertions(+), 12 deletions(-) + create mode 100644 dirsrvtests/tests/suites/snmp/snmp.py + +diff --git a/.github/scripts/generate_matrix.py b/.github/scripts/generate_matrix.py +index 584374597..8d67a1dc7 100644 +--- a/.github/scripts/generate_matrix.py ++++ b/.github/scripts/generate_matrix.py +@@ -21,8 +21,8 @@ else: + # Use tests from the source + suites = next(os.walk('dirsrvtests/tests/suites/'))[1] + +- # Filter out snmp as it is an empty directory: +- suites.remove('snmp') ++ # Filter out webui because of broken tests ++ suites.remove('webui') + + # Run each replication test module separately to speed things up + suites.remove('replication') +diff --git a/dirsrvtests/tests/suites/snmp/snmp.py b/dirsrvtests/tests/suites/snmp/snmp.py +new file mode 100644 +index 000000000..0952deb40 +--- /dev/null ++++ b/dirsrvtests/tests/suites/snmp/snmp.py +@@ -0,0 +1,214 @@ ++# --- BEGIN COPYRIGHT BLOCK --- ++# Copyright (C) 2024 Red Hat, Inc. ++# All rights reserved. ++# ++# License: GPL (version 3 or any later version). ++# See LICENSE for details. ++# --- END COPYRIGHT BLOCK --- ++# ++import os ++import pytest ++import logging ++import subprocess ++import ldap ++from datetime import datetime ++from shutil import copyfile ++from lib389.topologies import topology_m2 as topo_m2 ++from lib389.utils import selinux_present ++ ++ ++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__) ++ ++ ++SNMP_USER = 'user_name' ++SNMP_PASSWORD = 'authentication_password' ++SNMP_PRIVATE = 'private_password' ++ ++# LDAP OID in MIB ++LDAP_OID = '.1.3.6.1.4.1.2312.6.1.1' ++LDAPCONNECTIONS_OID = f'{LDAP_OID}.21' ++ ++ ++def run_cmd(cmd, check_returncode=True): ++ """Run a command""" ++ ++ log.info(f'Run: {cmd}') ++ result = subprocess.run(cmd, capture_output=True, universal_newlines=True) ++ log.info(f'STDOUT of {cmd} is:\n{result.stdout}') ++ log.info(f'STDERR of {cmd} is:\n{result.stderr}') ++ if check_returncode: ++ result.check_returncode() ++ return result ++ ++ ++def add_lines(lines, filename): ++ """Add lines that are not already present at the end of a file""" ++ ++ log.info(f'add_lines({lines}, {filename})') ++ try: ++ with open(filename, 'r') as fd: ++ for line in fd: ++ try: ++ lines.remove(line.strip()) ++ except ValueError: ++ pass ++ except FileNotFoundError: ++ pass ++ if lines: ++ with open(filename, 'a') as fd: ++ for line in lines: ++ fd.write(f'{line}\n') ++ ++ ++def remove_lines(lines, filename): ++ """Remove lines in a file""" ++ ++ log.info(f'remove_lines({lines}, {filename})') ++ file_lines = [] ++ with open(filename, 'r') as fd: ++ for line in fd: ++ if not line.strip() in lines: ++ file_lines.append(line) ++ with open(filename, 'w') as fd: ++ for line in file_lines: ++ fd.write(line) ++ ++ ++@pytest.fixture(scope="module") ++def setup_snmp(topo_m2, request): ++ """Install snmp and configure it ++ ++ Returns the time just before dirsrv-snmp get restarted ++ """ ++ ++ inst1 = topo_m2.ms["supplier1"] ++ inst2 = topo_m2.ms["supplier2"] ++ ++ # Check for the test prerequisites ++ if os.getuid() != 0: ++ pytest.skip('This test should be run by root superuser') ++ return None ++ if not inst1.with_systemd_running(): ++ pytest.skip('This test requires systemd') ++ return None ++ required_packages = { ++ '389-ds-base-snmp': os.path.join(inst1.get_sbin_dir(), 'ldap-agent'), ++ 'net-snmp': '/etc/snmp/snmpd.conf', } ++ skip_msg = "" ++ for package,file in required_packages.items(): ++ if not os.path.exists(file): ++ skip_msg += f"Package {package} is not installed ({file} is missing).\n" ++ if skip_msg != "": ++ pytest.skip(f'This test requires the following package(s): {skip_msg}') ++ return None ++ ++ # Install snmp ++ # run_cmd(['/usr/bin/dnf', 'install', '-y', 'net-snmp', 'net-snmp-utils', '389-ds-base-snmp']) ++ ++ # Prepare the lines to add/remove in files: ++ # master agentx ++ # snmp user (user_name - authentication_password - private_password) ++ # ldap_agent ds instances ++ # ++ # Adding rwuser and createUser lines is the same as running: ++ # net-snmp-create-v3-user -A authentication_password -a SHA -X private_password -x AES user_name ++ # but has the advantage of removing the user at cleanup phase ++ # ++ agent_cfg = '/etc/dirsrv/config/ldap-agent.conf' ++ lines_dict = { '/etc/snmp/snmpd.conf' : ['master agentx', f'rwuser {SNMP_USER}'], ++ '/var/lib/net-snmp/snmpd.conf' : [ ++ f'createUser {SNMP_USER} SHA "{SNMP_PASSWORD}" AES "{SNMP_PRIVATE}"',], ++ agent_cfg : [] } ++ for inst in topo_m2: ++ lines_dict[agent_cfg].append(f'server slapd-{inst.serverid}') ++ ++ # Prepare the cleanup ++ def fin(): ++ run_cmd(['systemctl', 'stop', 'dirsrv-snmp']) ++ if not DEBUGGING: ++ run_cmd(['systemctl', 'stop', 'snmpd']) ++ try: ++ os.remove('/usr/share/snmp/mibs/redhat-directory.mib') ++ except FileNotFoundError: ++ pass ++ for filename,lines in lines_dict.items(): ++ remove_lines(lines, filename) ++ run_cmd(['systemctl', 'start', 'snmpd']) ++ ++ request.addfinalizer(fin) ++ ++ # Copy RHDS MIB in default MIB search path (Ugly because I have not found how to change the search path) ++ copyfile('/usr/share/dirsrv/mibs/redhat-directory.mib', '/usr/share/snmp/mibs/redhat-directory.mib') ++ ++ run_cmd(['systemctl', 'stop', 'snmpd']) ++ for filename,lines in lines_dict.items(): ++ add_lines(lines, filename) ++ ++ run_cmd(['systemctl', 'start', 'snmpd']) ++ ++ curtime = datetime.now().strftime('%Y-%m-%d %H:%M:%S') ++ ++ run_cmd(['systemctl', 'start', 'dirsrv-snmp']) ++ return curtime ++ ++ ++@pytest.mark.skipif(not os.path.exists('/usr/bin/snmpwalk'), reason="net-snmp-utils package is not installed") ++def test_snmpwalk(topo_m2, setup_snmp): ++ """snmp smoke tests. ++ ++ :id: e5d29998-1c21-11ef-a654-482ae39447e5 ++ :setup: Two suppliers replication setup, snmp ++ :steps: ++ 1. use snmpwalk to display LDAP statistics ++ 2. use snmpwalk to get the number of open connections ++ :expectedresults: ++ 1. Success and no messages in stderr ++ 2. The number of open connections should be positive ++ """ ++ ++ inst1 = topo_m2.ms["supplier1"] ++ inst2 = topo_m2.ms["supplier2"] ++ ++ ++ cmd = [ '/usr/bin/snmpwalk', '-v3', '-u', SNMP_USER, '-l', 'AuthPriv', ++ '-m', '+RHDS-MIB', '-A', SNMP_PASSWORD, '-a', 'SHA', ++ '-X', SNMP_PRIVATE, '-x', 'AES', 'localhost', ++ LDAP_OID ] ++ result = run_cmd(cmd) ++ assert not result.stderr ++ ++ cmd = [ '/usr/bin/snmpwalk', '-v3', '-u', SNMP_USER, '-l', 'AuthPriv', ++ '-m', '+RHDS-MIB', '-A', SNMP_PASSWORD, '-a', 'SHA', ++ '-X', SNMP_PRIVATE, '-x', 'AES', 'localhost', ++ f'{LDAPCONNECTIONS_OID}.{inst1.port}', '-Ov' ] ++ result = run_cmd(cmd) ++ nbconns = int(result.stdout.split()[1]) ++ log.info(f'There are {nbconns} open connections on {inst1.serverid}') ++ assert nbconns > 0 ++ ++ ++@pytest.mark.skipif(not selinux_present(), reason="SELinux is not enabled") ++def test_snmp_avc(topo_m2, setup_snmp): ++ """snmp smoke tests. ++ ++ :id: fb79728e-1d0d-11ef-9213-482ae39447e5 ++ :setup: Two suppliers replication setup, snmp ++ :steps: ++ 1. Get the system journal about ldap-agent ++ :expectedresults: ++ 1. No AVC should be present ++ """ ++ result = run_cmd(['journalctl', '-S', setup_snmp, '-g', 'ldap-agent']) ++ assert not 'AVC' in result.stdout ++ ++ ++if __name__ == '__main__': ++ # Run isolated ++ # -s for DEBUG mode ++ CURRENT_FILE = os.path.realpath(__file__) ++ pytest.main("-s %s" % CURRENT_FILE) +diff --git a/ldap/servers/slapd/agtmmap.c b/ldap/servers/slapd/agtmmap.c +index bc5fe1ee1..4dc67dcfb 100644 +--- a/ldap/servers/slapd/agtmmap.c ++++ b/ldap/servers/slapd/agtmmap.c +@@ -34,6 +34,70 @@ + agt_mmap_context_t mmap_tbl[2] = {{AGT_MAP_UNINIT, -1, (caddr_t)-1}, + {AGT_MAP_UNINIT, -1, (caddr_t)-1}}; + ++#define CHECK_MAP_FAILURE(addr) ((addr)==NULL || (addr) == (caddr_t) -1) ++ ++ ++/**************************************************************************** ++ * ++ * agt_set_fmode () - try to increase file mode if some flags are missing. ++ * ++ * ++ * Inputs: ++ * fd -> The file descriptor. ++ * ++ * mode -> the wanted mode ++ * ++ * Outputs: None ++ * Return Values: None ++ * ++ ****************************************************************************/ ++static void ++agt_set_fmode(int fd, mode_t mode) ++{ ++ /* ns-slapd umask is 0022 which is usually fine. ++ * but ldap-agen needs S_IWGRP permission on snmp semaphore and mmap file ++ * ( when SELinux is enforced process with uid=0 does not bypass the file permission ++ * (unless the unfamous dac_override capability is set) ++ * Changing umask could lead to race conditions so it is better to check the ++ * file permission and change them if needed and if the process own the file. ++ */ ++ struct stat fileinfo = {0}; ++ if (fstat(fd, &fileinfo) == 0 && fileinfo.st_uid == getuid() && ++ (fileinfo.st_mode & mode) != mode) { ++ (void) fchmod(fd, fileinfo.st_mode | mode); ++ } ++} ++ ++/**************************************************************************** ++ * ++ * agt_sem_open () - Like sem_open but ignores umask ++ * ++ * ++ * Inputs: see sem_open man page. ++ * Outputs: see sem_open man page. ++ * Return Values: see sem_open man page. ++ * ++ ****************************************************************************/ ++sem_t * ++agt_sem_open(const char *name, int oflag, mode_t mode, unsigned int value) ++{ ++ sem_t *sem = sem_open(name, oflag, mode, value); ++ char *semname = NULL; ++ ++ if (sem != NULL) { ++ if (asprintf(&semname, "/dev/shm/sem.%s", name+1) > 0) { ++ int fd = open(semname, O_RDONLY); ++ if (fd >= 0) { ++ agt_set_fmode(fd, mode); ++ (void) close(fd); ++ } ++ free(semname); ++ semname = NULL; ++ } ++ } ++ return sem; ++} ++ + /**************************************************************************** + * + * agt_mopen_stats () - open and Memory Map the stats file. agt_mclose_stats() +@@ -52,7 +116,6 @@ agt_mmap_context_t mmap_tbl[2] = {{AGT_MAP_UNINIT, -1, (caddr_t)-1}, + * as defined in , otherwise. + * + ****************************************************************************/ +- + int + agt_mopen_stats(char *statsfile, int mode, int *hdl) + { +@@ -64,6 +127,7 @@ agt_mopen_stats(char *statsfile, int mode, int *hdl) + int err; + size_t sz; + struct stat fileinfo; ++ mode_t rw_mode = S_IWUSR | S_IRUSR | S_IRGRP | S_IWGRP | S_IROTH; + + switch (mode) { + case O_RDONLY: +@@ -128,10 +192,7 @@ agt_mopen_stats(char *statsfile, int mode, int *hdl) + break; + + case O_RDWR: +- fd = open(path, +- O_RDWR | O_CREAT, +- S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH); +- ++ fd = open(path, O_RDWR | O_CREAT, rw_mode); + if (fd < 0) { + err = errno; + #if (0) +@@ -140,6 +201,7 @@ agt_mopen_stats(char *statsfile, int mode, int *hdl) + rc = err; + goto bail; + } ++ agt_set_fmode(fd, rw_mode); + + if (fstat(fd, &fileinfo) != 0) { + close(fd); +diff --git a/ldap/servers/slapd/agtmmap.h b/ldap/servers/slapd/agtmmap.h +index fb27ab2c4..99a8584a3 100644 +--- a/ldap/servers/slapd/agtmmap.h ++++ b/ldap/servers/slapd/agtmmap.h +@@ -28,6 +28,7 @@ + #include + #include + #include ++#include + #include + #include "nspr.h" + +@@ -188,6 +189,18 @@ int agt_mclose_stats(int hdl); + + int agt_mread_stats(int hdl, struct hdr_stats_t *, struct ops_stats_t *, struct entries_stats_t *); + ++/**************************************************************************** ++ * ++ * agt_sem_open () - Like sem_open but ignores umask ++ * ++ * ++ * Inputs: see sem_open man page. ++ * Outputs: see sem_open man page. ++ * Return Values: see sem_open man page. ++ * ++ ****************************************************************************/ ++sem_t *agt_sem_open(const char *name, int oflag, mode_t mode, unsigned int value); ++ + #ifdef __cplusplus + } + #endif +diff --git a/ldap/servers/slapd/dse.c b/ldap/servers/slapd/dse.c +index b04fafde6..f1e48c6b1 100644 +--- a/ldap/servers/slapd/dse.c ++++ b/ldap/servers/slapd/dse.c +@@ -683,7 +683,7 @@ dse_read_one_file(struct dse *pdse, const char *filename, Slapi_PBlock *pb, int + "The configuration file %s could not be accessed, error %d\n", + filename, rc); + rc = 0; /* Fail */ +- } else if ((prfd = PR_Open(filename, PR_RDONLY, SLAPD_DEFAULT_FILE_MODE)) == NULL) { ++ } else if ((prfd = PR_Open(filename, PR_RDONLY, SLAPD_DEFAULT_DSE_FILE_MODE)) == NULL) { + slapi_log_err(SLAPI_LOG_ERR, "dse_read_one_file", + "The configuration file %s could not be read. " SLAPI_COMPONENT_NAME_NSPR " %d (%s)\n", + filename, +@@ -871,7 +871,7 @@ dse_rw_permission_to_one_file(const char *name, int loglevel) + PRFileDesc *prfd; + + prfd = PR_Open(name, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, +- SLAPD_DEFAULT_FILE_MODE); ++ SLAPD_DEFAULT_DSE_FILE_MODE); + if (NULL == prfd) { + prerr = PR_GetError(); + accesstype = "create"; +@@ -970,7 +970,7 @@ dse_write_file_nolock(struct dse *pdse) + fpw.fpw_prfd = NULL; + + if (NULL != pdse->dse_filename) { +- if ((fpw.fpw_prfd = PR_Open(pdse->dse_tmpfile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, SLAPD_DEFAULT_FILE_MODE)) == NULL) { ++ if ((fpw.fpw_prfd = PR_Open(pdse->dse_tmpfile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, SLAPD_DEFAULT_DSE_FILE_MODE)) == NULL) { + rc = PR_GetOSError(); + slapi_log_err(SLAPI_LOG_ERR, "dse_write_file_nolock", "Cannot open " + "temporary DSE file \"%s\" for update: OS error %d (%s)\n", +diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h +index 469874fd1..927576b70 100644 +--- a/ldap/servers/slapd/slap.h ++++ b/ldap/servers/slapd/slap.h +@@ -238,6 +238,12 @@ typedef void (*VFPV)(); /* takes undefined arguments */ + */ + + #define SLAPD_DEFAULT_FILE_MODE S_IRUSR | S_IWUSR ++/* ldap_agent run as uid=root gid=dirsrv and requires S_IRGRP | S_IWGRP ++ * on semaphore and mmap file if SELinux is enforced. ++ */ ++#define SLAPD_DEFAULT_SNMP_FILE_MODE S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP ++/* ldap_agent run as uid=root gid=dirsrv and requires S_IRGRP on dse.ldif if SELinux is enforced. */ ++#define SLAPD_DEFAULT_DSE_FILE_MODE S_IRUSR | S_IWUSR | S_IRGRP + #define SLAPD_DEFAULT_DIR_MODE S_IRWXU + #define SLAPD_DEFAULT_IDLE_TIMEOUT 3600 /* seconds - 0 == never */ + #define SLAPD_DEFAULT_IDLE_TIMEOUT_STR "3600" +diff --git a/ldap/servers/slapd/snmp_collator.c b/ldap/servers/slapd/snmp_collator.c +index c998d4262..bd7020585 100644 +--- a/ldap/servers/slapd/snmp_collator.c ++++ b/ldap/servers/slapd/snmp_collator.c +@@ -474,7 +474,7 @@ static void + snmp_collator_create_semaphore(void) + { + /* First just try to create the semaphore. This should usually just work. */ +- if ((stats_sem = sem_open(stats_sem_name, O_CREAT | O_EXCL, SLAPD_DEFAULT_FILE_MODE, 1)) == SEM_FAILED) { ++ if ((stats_sem = agt_sem_open(stats_sem_name, O_CREAT | O_EXCL, SLAPD_DEFAULT_SNMP_FILE_MODE, 1)) == SEM_FAILED) { + if (errno == EEXIST) { + /* It appears that we didn't exit cleanly last time and left the semaphore + * around. Recreate it since we don't know what state it is in. */ +@@ -486,7 +486,7 @@ snmp_collator_create_semaphore(void) + exit(1); + } + +- if ((stats_sem = sem_open(stats_sem_name, O_CREAT | O_EXCL, SLAPD_DEFAULT_FILE_MODE, 1)) == SEM_FAILED) { ++ if ((stats_sem = agt_sem_open(stats_sem_name, O_CREAT | O_EXCL, SLAPD_DEFAULT_SNMP_FILE_MODE, 1)) == SEM_FAILED) { + /* No dice */ + slapi_log_err(SLAPI_LOG_EMERG, "snmp_collator_create_semaphore", + "Failed to create semaphore for stats file (/dev/shm/sem.%s). Error %d (%s).\n", +diff --git a/src/lib389/lib389/instance/setup.py b/src/lib389/lib389/instance/setup.py +index 036664447..fca03383e 100644 +--- a/src/lib389/lib389/instance/setup.py ++++ b/src/lib389/lib389/instance/setup.py +@@ -10,6 +10,7 @@ + import os + import sys + import shutil ++import stat + import pwd + import grp + import re +@@ -773,6 +774,10 @@ class SetupDs(object): + ldapi_autobind="on", + ) + file_dse.write(dse_fmt) ++ # Set minimum permission required by snmp ldap-agent ++ status = os.fstat(file_dse.fileno()) ++ os.fchmod(file_dse.fileno(), status.st_mode | stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) ++ os.chown(os.path.join(slapd['config_dir'], 'dse.ldif'), slapd['user_uid'], slapd['group_gid']) + + self.log.info("Create file system structures ...") + # Create all the needed paths +diff --git a/wrappers/systemd-snmp.service.in b/wrappers/systemd-snmp.service.in +index f18766cb4..d344367a0 100644 +--- a/wrappers/systemd-snmp.service.in ++++ b/wrappers/systemd-snmp.service.in +@@ -9,6 +9,7 @@ After=network.target + + [Service] + Type=forking ++Group=@defaultgroup@ + PIDFile=/run/dirsrv/ldap-agent.pid + ExecStart=@sbindir@/ldap-agent @configdir@/ldap-agent.conf + +-- +2.49.0 + diff --git a/SOURCES/0035-Issue-5305-OpenLDAP-version-autodetection-doesn-t-wo.patch b/SOURCES/0035-Issue-5305-OpenLDAP-version-autodetection-doesn-t-wo.patch new file mode 100644 index 0000000..0b0420b --- /dev/null +++ b/SOURCES/0035-Issue-5305-OpenLDAP-version-autodetection-doesn-t-wo.patch @@ -0,0 +1,60 @@ +From 12870f410545fb055f664b588df2a2b7ab1c228e Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Mon, 4 Mar 2024 07:22:00 +0100 +Subject: [PATCH] Issue 5305 - OpenLDAP version autodetection doesn't work + +Bug Description: +An error is logged during a build in `mock` with Bash 4.4: + +``` +checking for --with-libldap-r... ./configure: command substitution: line 22848: syntax error near unexpected token `>' +./configure: command substitution: line 22848: `ldapsearch -VV 2> >(sed -n '/ldapsearch/ s/.*ldapsearch \([0-9]\+\.[0-9]\+\.[0-9]\+\) .*/\1/p')' +no +``` + +`mock` runs Bash as `sh` (POSIX mode). Support for process substitution +in POSIX mode was added in version 5.1: +https://lists.gnu.org/archive/html/bug-bash/2020-12/msg00002.html + +> Process substitution is now available in posix mode. + +Fix Description: +* Add missing `BuildRequires` for openldap-clients +* Replace process substitution with a pipe + +Fixes: https://github.com/389ds/389-ds-base/issues/5305 + +Reviewed by: @progier389, @tbordaz (Thanks!) +--- + configure.ac | 2 +- + rpm/389-ds-base.spec.in | 1 + + 2 files changed, 2 insertions(+), 1 deletion(-) + +diff --git a/configure.ac b/configure.ac +index ffc2aac14..a690765a3 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -912,7 +912,7 @@ AC_ARG_WITH(libldap-r, AS_HELP_STRING([--with-libldap-r],[Use lldap_r shared lib + AC_SUBST(with_libldap_r) + fi + ], +-OPENLDAP_VERSION=`ldapsearch -VV 2> >(sed -n '/ldapsearch/ s/.*ldapsearch \([[[0-9]]]\+\.[[[0-9]]]\+\.[[[0-9]]]\+\) .*/\1/p')` ++OPENLDAP_VERSION=`ldapsearch -VV 2>&1 | sed -n '/ldapsearch/ s/.*ldapsearch \([[[0-9]]]\+\.[[[0-9]]]\+\.[[[0-9]]]\+\) .*/\1/p'` + AX_COMPARE_VERSION([$OPENLDAP_VERSION], [lt], [2.5], [ with_libldap_r=yes ], [ with_libldap_r=no ]) + AC_MSG_RESULT($with_libldap_r)) + +diff --git a/rpm/389-ds-base.spec.in b/rpm/389-ds-base.spec.in +index cd86138ea..b8c14cd14 100644 +--- a/rpm/389-ds-base.spec.in ++++ b/rpm/389-ds-base.spec.in +@@ -65,6 +65,7 @@ Provides: ldif2ldbm + # Attach the buildrequires to the top level package: + BuildRequires: nspr-devel + BuildRequires: nss-devel >= 3.34 ++BuildRequires: openldap-clients + BuildRequires: openldap-devel + BuildRequires: libdb-devel + BuildRequires: cyrus-sasl-devel +-- +2.49.0 + diff --git a/SOURCES/0036-Issue-1925-Add-a-CI-test-5936.patch b/SOURCES/0036-Issue-1925-Add-a-CI-test-5936.patch new file mode 100644 index 0000000..a19a90f --- /dev/null +++ b/SOURCES/0036-Issue-1925-Add-a-CI-test-5936.patch @@ -0,0 +1,245 @@ +From eca6f5fe18f768fd407d38c85624a5212bcf16ab Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Wed, 27 Sep 2023 15:40:33 -0700 +Subject: [PATCH] Issue 1925 - Add a CI test (#5936) + +Description: Verify that the issue is not present. Cover the scenario when +we remove existing VLVs, create new VLVs (with the same name) and then +we do online re-indexing. + +Related: https://github.com/389ds/389-ds-base/issues/1925 + +Reviewed by: @progier389 (Thanks!) + +(cherry picked from the 9633e8d32d28345409680f8e462fb4a53d3b4f83) +--- + .../tests/suites/vlv/regression_test.py | 175 +++++++++++++++--- + 1 file changed, 145 insertions(+), 30 deletions(-) + +diff --git a/dirsrvtests/tests/suites/vlv/regression_test.py b/dirsrvtests/tests/suites/vlv/regression_test.py +index 6ab709bd3..536fe950f 100644 +--- a/dirsrvtests/tests/suites/vlv/regression_test.py ++++ b/dirsrvtests/tests/suites/vlv/regression_test.py +@@ -1,5 +1,5 @@ + # --- BEGIN COPYRIGHT BLOCK --- +-# Copyright (C) 2018 Red Hat, Inc. ++# Copyright (C) 2023 Red Hat, Inc. + # All rights reserved. + # + # License: GPL (version 3 or any later version). +@@ -9,12 +9,16 @@ + import pytest, time + from lib389.tasks import * + from lib389.utils import * +-from lib389.topologies import topology_m2 ++from lib389.topologies import topology_m2, topology_st + from lib389.replica import * + from lib389._constants import * ++from lib389.properties import TASK_WAIT + from lib389.index import * + from lib389.mappingTree import * + from lib389.backend import * ++from lib389.idm.user import UserAccounts ++from ldap.controls.vlv import VLVRequestControl ++from ldap.controls.sss import SSSRequestControl + + pytestmark = pytest.mark.tier1 + +@@ -22,6 +26,88 @@ logging.getLogger(__name__).setLevel(logging.DEBUG) + log = logging.getLogger(__name__) + + ++def open_new_ldapi_conn(dsinstance): ++ ldapurl, certdir = get_ldapurl_from_serverid(dsinstance) ++ assert 'ldapi://' in ldapurl ++ conn = ldap.initialize(ldapurl) ++ conn.sasl_interactive_bind_s("", ldap.sasl.external()) ++ return conn ++ ++ ++def check_vlv_search(conn): ++ before_count=1 ++ after_count=3 ++ offset=3501 ++ ++ vlv_control = VLVRequestControl(criticality=True, ++ before_count=before_count, ++ after_count=after_count, ++ offset=offset, ++ content_count=0, ++ greater_than_or_equal=None, ++ context_id=None) ++ ++ sss_control = SSSRequestControl(criticality=True, ordering_rules=['cn']) ++ result = conn.search_ext_s( ++ base='dc=example,dc=com', ++ scope=ldap.SCOPE_SUBTREE, ++ filterstr='(uid=*)', ++ serverctrls=[vlv_control, sss_control] ++ ) ++ imin = offset + 998 - before_count ++ imax = offset + 998 + after_count ++ ++ for i, (dn, entry) in enumerate(result, start=imin): ++ assert i <= imax ++ expected_dn = f'uid=testuser{i},ou=People,dc=example,dc=com' ++ log.debug(f'found {dn} expected {expected_dn}') ++ assert dn.lower() == expected_dn.lower() ++ ++ ++def add_users(inst, users_num): ++ users = UserAccounts(inst, DEFAULT_SUFFIX) ++ log.info(f'Adding {users_num} users') ++ for i in range(0, users_num): ++ uid = 1000 + i ++ user_properties = { ++ 'uid': f'testuser{uid}', ++ 'cn': f'testuser{uid}', ++ 'sn': 'user', ++ 'uidNumber': str(uid), ++ 'gidNumber': str(uid), ++ 'homeDirectory': f'/home/testuser{uid}' ++ } ++ users.create(properties=user_properties) ++ ++ ++ ++def create_vlv_search_and_index(inst, basedn=DEFAULT_SUFFIX, bename='userRoot', ++ scope=ldap.SCOPE_SUBTREE, prefix="vlv", vlvsort="cn"): ++ vlv_searches = VLVSearch(inst) ++ vlv_search_properties = { ++ "objectclass": ["top", "vlvSearch"], ++ "cn": f"{prefix}Srch", ++ "vlvbase": basedn, ++ "vlvfilter": "(uid=*)", ++ "vlvscope": str(scope), ++ } ++ vlv_searches.create( ++ basedn=f"cn={bename},cn=ldbm database,cn=plugins,cn=config", ++ properties=vlv_search_properties ++ ) ++ ++ vlv_index = VLVIndex(inst) ++ vlv_index_properties = { ++ "objectclass": ["top", "vlvIndex"], ++ "cn": f"{prefix}Idx", ++ "vlvsort": vlvsort, ++ } ++ vlv_index.create( ++ basedn=f"cn={prefix}Srch,cn={bename},cn=ldbm database,cn=plugins,cn=config", ++ properties=vlv_index_properties ++ ) ++ return vlv_searches, vlv_index ++ + class BackendHandler: + def __init__(self, inst, bedict, scope=ldap.SCOPE_ONELEVEL): + self.inst = inst +@@ -101,34 +187,6 @@ class BackendHandler: + 'dn' : dn} + + +-def create_vlv_search_and_index(inst, basedn=DEFAULT_SUFFIX, bename='userRoot', +- scope=ldap.SCOPE_SUBTREE, prefix="vlv", vlvsort="cn"): +- vlv_searches = VLVSearch(inst) +- vlv_search_properties = { +- "objectclass": ["top", "vlvSearch"], +- "cn": f"{prefix}Srch", +- "vlvbase": basedn, +- "vlvfilter": "(uid=*)", +- "vlvscope": str(scope), +- } +- vlv_searches.create( +- basedn=f"cn={bename},cn=ldbm database,cn=plugins,cn=config", +- properties=vlv_search_properties +- ) +- +- vlv_index = VLVIndex(inst) +- vlv_index_properties = { +- "objectclass": ["top", "vlvIndex"], +- "cn": f"{prefix}Idx", +- "vlvsort": vlvsort, +- } +- vlv_index.create( +- basedn=f"cn={prefix}Srch,cn={bename},cn=ldbm database,cn=plugins,cn=config", +- properties=vlv_index_properties +- ) +- return vlv_searches, vlv_index +- +- + @pytest.fixture + def vlv_setup_with_uid_mr(topology_st, request): + inst = topology_st.standalone +@@ -245,6 +303,62 @@ def test_bulk_import_when_the_backend_with_vlv_was_recreated(topology_m2): + entries = M2.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(objectclass=*)") + + ++def test_vlv_recreation_reindex(topology_st): ++ """Test VLV recreation and reindexing. ++ ++ :id: 29f4567f-4ac0-410f-bc99-a32e217a939f ++ :setup: Standalone instance. ++ :steps: ++ 1. Create new VLVs and do the reindex. ++ 2. Test the new VLVs. ++ 3. Remove the existing VLVs. ++ 4. Create new VLVs (with the same name). ++ 5. Perform online re-indexing of the new VLVs. ++ 6. Test the new VLVs. ++ :expectedresults: ++ 1. Should Success. ++ 2. Should Success. ++ 3. Should Success. ++ 4. Should Success. ++ 5. Should Success. ++ 6. Should Success. ++ """ ++ ++ inst = topology_st.standalone ++ reindex_task = Tasks(inst) ++ ++ # Create and test VLVs ++ 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_users(inst, 5000) ++ ++ conn = open_new_ldapi_conn(inst.serverid) ++ assert len(conn.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(cn=*)")) > 0 ++ check_vlv_search(conn) ++ ++ # Remove and recreate VLVs ++ vlv_index.delete() ++ vlv_search.delete() ++ ++ 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 ++ ++ conn = open_new_ldapi_conn(inst.serverid) ++ assert len(conn.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(cn=*)")) > 0 ++ check_vlv_search(conn) ++ ++ + def test_vlv_with_mr(vlv_setup_with_uid_mr): + """ + Testing vlv having specific matching rule +@@ -288,6 +402,7 @@ def test_vlv_with_mr(vlv_setup_with_uid_mr): + assert inst.status() + + ++ + if __name__ == "__main__": + # Run isolated + # -s for DEBUG mode +-- +2.49.0 + diff --git a/SOURCES/0037-Issue-6494-2nd-Various-errors-when-using-extended-ma.patch b/SOURCES/0037-Issue-6494-2nd-Various-errors-when-using-extended-ma.patch new file mode 100644 index 0000000..9806b54 --- /dev/null +++ b/SOURCES/0037-Issue-6494-2nd-Various-errors-when-using-extended-ma.patch @@ -0,0 +1,75 @@ +From af3fa90f91efda86f4337e8823bca6581ab61792 Mon Sep 17 00:00:00 2001 +From: Thierry Bordaz +Date: Fri, 7 Feb 2025 09:43:08 +0100 +Subject: [PATCH] Issue 6494 - (2nd) Various errors when using extended + matching rule on vlv sort filter + +--- + .../tests/suites/indexes/regression_test.py | 40 +++++++++++++++++++ + 1 file changed, 40 insertions(+) + +diff --git a/dirsrvtests/tests/suites/indexes/regression_test.py b/dirsrvtests/tests/suites/indexes/regression_test.py +index 2196fb2ed..b5bcccc8f 100644 +--- a/dirsrvtests/tests/suites/indexes/regression_test.py ++++ b/dirsrvtests/tests/suites/indexes/regression_test.py +@@ -11,17 +11,57 @@ import os + import pytest + import ldap + from lib389._constants import DEFAULT_BENAME, DEFAULT_SUFFIX ++from lib389.backend import Backend, Backends, DatabaseConfig + from lib389.cos import CosClassicDefinition, CosClassicDefinitions, CosTemplate ++from lib389.dbgen import dbgen_users + from lib389.index import Indexes + from lib389.backend import Backends + from lib389.idm.user import UserAccounts + from lib389.topologies import topology_st as topo + from lib389.utils import ds_is_older + from lib389.idm.nscontainer import nsContainer ++from lib389.properties import TASK_WAIT ++from lib389.tasks import Tasks, Task + + pytestmark = pytest.mark.tier1 + + ++SUFFIX2 = 'dc=example2,dc=com' ++BENAME2 = 'be2' ++ ++DEBUGGING = os.getenv("DEBUGGING", default=False) ++ ++@pytest.fixture(scope="function") ++def add_backend_and_ldif_50K_users(request, topo): ++ """ ++ Add an empty backend and associated 50K users ldif file ++ """ ++ ++ tasks = Tasks(topo.standalone) ++ import_ldif = f'{topo.standalone.ldifdir}/be2_50K_users.ldif' ++ be2 = Backend(topo.standalone) ++ be2.create(properties={ ++ 'cn': BENAME2, ++ 'nsslapd-suffix': SUFFIX2, ++ }, ++ ) ++ ++ def fin(): ++ nonlocal be2 ++ if not DEBUGGING: ++ be2.delete() ++ ++ request.addfinalizer(fin) ++ parent = f'ou=people,{SUFFIX2}' ++ dbgen_users(topo.standalone, 50000, import_ldif, SUFFIX2, generic=True, parent=parent) ++ assert tasks.importLDIF( ++ suffix=SUFFIX2, ++ input_file=import_ldif, ++ args={TASK_WAIT: True} ++ ) == 0 ++ ++ return import_ldif ++ + @pytest.fixture(scope="function") + def add_a_group_with_users(request, topo): + """ +-- +2.49.0 + diff --git a/SOURCES/0038-Issue-6494-3rd-Various-errors-when-using-extended-ma.patch b/SOURCES/0038-Issue-6494-3rd-Various-errors-when-using-extended-ma.patch new file mode 100644 index 0000000..b2a6fa0 --- /dev/null +++ b/SOURCES/0038-Issue-6494-3rd-Various-errors-when-using-extended-ma.patch @@ -0,0 +1,45 @@ +From 0ad0eb34972c99f30334d7d420f3056e0e794d74 Mon Sep 17 00:00:00 2001 +From: Thierry Bordaz +Date: Fri, 7 Feb 2025 14:33:46 +0100 +Subject: [PATCH] Issue 6494 - (3rd) Various errors when using extended + matching rule on vlv sort filter + +(cherry picked from the commit f2f917ca55c34c81b578bce1dd5275abff6abb72) +--- + dirsrvtests/tests/suites/vlv/regression_test.py | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +diff --git a/dirsrvtests/tests/suites/vlv/regression_test.py b/dirsrvtests/tests/suites/vlv/regression_test.py +index 536fe950f..d069fdbaf 100644 +--- a/dirsrvtests/tests/suites/vlv/regression_test.py ++++ b/dirsrvtests/tests/suites/vlv/regression_test.py +@@ -16,12 +16,16 @@ from lib389.properties import TASK_WAIT + from lib389.index import * + from lib389.mappingTree import * + from lib389.backend import * +-from lib389.idm.user import UserAccounts ++from lib389.idm.user import UserAccounts, UserAccount ++from lib389.idm.organization import Organization ++from lib389.idm.organizationalunit import OrganizationalUnits + from ldap.controls.vlv import VLVRequestControl + from ldap.controls.sss import SSSRequestControl + + pytestmark = pytest.mark.tier1 + ++DEMO_PW = 'secret12' ++ + logging.getLogger(__name__).setLevel(logging.DEBUG) + log = logging.getLogger(__name__) + +@@ -169,7 +173,7 @@ class BackendHandler: + 'loginShell': '/bin/false', + 'userpassword': DEMO_PW }) + # Add regular user +- add_users(self.inst, 10, suffix=suffix) ++ add_users(self.inst, 10) + # Removing ou2 + ou2.delete() + # And export +-- +2.49.0 + diff --git a/SOURCES/0039-Issue-6494-4th-Various-errors-when-using-extended-ma.patch b/SOURCES/0039-Issue-6494-4th-Various-errors-when-using-extended-ma.patch new file mode 100644 index 0000000..c05a4bd --- /dev/null +++ b/SOURCES/0039-Issue-6494-4th-Various-errors-when-using-extended-ma.patch @@ -0,0 +1,72 @@ +From 52041811b200292af6670490c9ebc1f599439a22 Mon Sep 17 00:00:00 2001 +From: Masahiro Matsuya +Date: Sat, 22 Mar 2025 01:25:25 +0900 +Subject: [PATCH] Issue 6494 - (4th) Various errors when using extended + matching rule on vlv sort filter + +test_vlv_with_mr uses vlv_setup_with_uid_mr fixture to setup backend +and testusers. add_users function is called in beh.setup without any +suffix for the created backend. As a result, testusers always are +created in the DEFAULT_SUFFIX only by add_users function. Another test +like test_vlv_recreation_reindex can create the same test user in +DEFAULT_SUFFIX, and it caused the ALREADY_EXISTS failure in +test_vlv_with_mr test. + +In main branch, add_users have suffix argument. Test users are created +on the specific suffix, and the backend is cleaned up after the test. +This PR is to follow the same implementation. + +Also, suppressing ldap.ALREADY_EXISTS makes the add_users func to be +used easily. + +Related: https://github.com/389ds/389-ds-base/issues/6494 +--- + dirsrvtests/tests/suites/vlv/regression_test.py | 11 ++++++----- + 1 file changed, 6 insertions(+), 5 deletions(-) + +diff --git a/dirsrvtests/tests/suites/vlv/regression_test.py b/dirsrvtests/tests/suites/vlv/regression_test.py +index d069fdbaf..e9408117b 100644 +--- a/dirsrvtests/tests/suites/vlv/regression_test.py ++++ b/dirsrvtests/tests/suites/vlv/regression_test.py +@@ -21,6 +21,7 @@ from lib389.idm.organization import Organization + from lib389.idm.organizationalunit import OrganizationalUnits + from ldap.controls.vlv import VLVRequestControl + from ldap.controls.sss import SSSRequestControl ++from contextlib import suppress + + pytestmark = pytest.mark.tier1 + +@@ -68,8 +69,8 @@ def check_vlv_search(conn): + assert dn.lower() == expected_dn.lower() + + +-def add_users(inst, users_num): +- users = UserAccounts(inst, DEFAULT_SUFFIX) ++def add_users(inst, users_num, suffix=DEFAULT_SUFFIX): ++ users = UserAccounts(inst, suffix) + log.info(f'Adding {users_num} users') + for i in range(0, users_num): + uid = 1000 + i +@@ -81,8 +82,8 @@ def add_users(inst, users_num): + 'gidNumber': str(uid), + 'homeDirectory': f'/home/testuser{uid}' + } +- users.create(properties=user_properties) +- ++ with suppress(ldap.ALREADY_EXISTS): ++ users.create(properties=user_properties) + + + def create_vlv_search_and_index(inst, basedn=DEFAULT_SUFFIX, bename='userRoot', +@@ -173,7 +174,7 @@ class BackendHandler: + 'loginShell': '/bin/false', + 'userpassword': DEMO_PW }) + # Add regular user +- add_users(self.inst, 10) ++ add_users(self.inst, 10, suffix=suffix) + # Removing ou2 + ou2.delete() + # And export +-- +2.49.0 + diff --git a/SOURCES/0040-Issue-6497-lib389-Configure-replication-for-multiple.patch b/SOURCES/0040-Issue-6497-lib389-Configure-replication-for-multiple.patch new file mode 100644 index 0000000..a59ff9d --- /dev/null +++ b/SOURCES/0040-Issue-6497-lib389-Configure-replication-for-multiple.patch @@ -0,0 +1,357 @@ +From b812afe4da6db134c1221eb48a6155480e4c2cb3 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Tue, 14 Jan 2025 13:55:03 -0500 +Subject: [PATCH] Issue 6497 - lib389 - Configure replication for multiple + suffixes (#6498) + +Bug Description: When trying to set up replication across multiple suffixes - +particularly if one of those suffixes is a subsuffix - lib389 fails to properly +configure the replication agreements, service accounts, and required groups. +The references to the replication_managers group and service account +naming do not correctly account for non-default additional suffixes. + +Fix Description: Ensure replication DNs and credentials are correctly tied to each suffix. +Enable DSLdapObject.present method to compare values as +a normalized DNs if they are DNs. +Add a test (test_multi_subsuffix_replication) to verify multi-suffix +replication across four suppliers. +Fix tests that are related to repl service accounts. + +Fixes: https://github.com/389ds/389-ds-base/issues/6497 + +Reviewed: @progier389 (Thanks!) +--- + .../tests/suites/ds_tools/replcheck_test.py | 4 +- + .../suites/replication/acceptance_test.py | 153 ++++++++++++++++++ + .../cleanallruv_shutdown_crash_test.py | 4 +- + .../suites/replication/regression_m2_test.py | 2 +- + .../replication/tls_client_auth_repl_test.py | 4 +- + src/lib389/lib389/_mapped_object.py | 21 ++- + src/lib389/lib389/replica.py | 10 +- + 7 files changed, 182 insertions(+), 16 deletions(-) + +diff --git a/dirsrvtests/tests/suites/ds_tools/replcheck_test.py b/dirsrvtests/tests/suites/ds_tools/replcheck_test.py +index f61fc432d..dfa1d9423 100644 +--- a/dirsrvtests/tests/suites/ds_tools/replcheck_test.py ++++ b/dirsrvtests/tests/suites/ds_tools/replcheck_test.py +@@ -67,10 +67,10 @@ def topo_tls_ldapi(topo): + + # Create the replication dns + services = ServiceAccounts(m1, DEFAULT_SUFFIX) +- repl_m1 = services.get('%s:%s' % (m1.host, m1.sslport)) ++ repl_m1 = services.get(f'{DEFAULT_SUFFIX}:{m1.host}:{m1.sslport}') + repl_m1.set('nsCertSubjectDN', m1.get_server_tls_subject()) + +- repl_m2 = services.get('%s:%s' % (m2.host, m2.sslport)) ++ repl_m2 = services.get(f'{DEFAULT_SUFFIX}:{m2.host}:{m2.sslport}') + repl_m2.set('nsCertSubjectDN', m2.get_server_tls_subject()) + + # Check the replication is "done". +diff --git a/dirsrvtests/tests/suites/replication/acceptance_test.py b/dirsrvtests/tests/suites/replication/acceptance_test.py +index d1cfa8bdb..fc8622051 100644 +--- a/dirsrvtests/tests/suites/replication/acceptance_test.py ++++ b/dirsrvtests/tests/suites/replication/acceptance_test.py +@@ -9,6 +9,7 @@ + import pytest + import logging + import time ++from lib389.backend import Backend + from lib389.replica import Replicas + from lib389.tasks import * + from lib389.utils import * +@@ -325,6 +326,158 @@ def test_modify_stripattrs(topo_m4): + assert attr_value in entries[0].data['nsds5replicastripattrs'] + + ++def test_multi_subsuffix_replication(topo_m4): ++ """Check that replication works with multiple subsuffixes ++ ++ :id: ac1aaeae-173e-48e7-847f-03b9867443c4 ++ :setup: Four suppliers replication setup ++ :steps: ++ 1. Create additional suffixes ++ 2. Setup replication for all suppliers ++ 3. Generate test data for each suffix (add, modify, remove) ++ 4. Wait for replication to complete across all suppliers for each suffix ++ 5. Check that all expected data is present on all suppliers ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Success ++ 5. Success (the data is replicated everywhere) ++ """ ++ ++ SUFFIX_2 = "dc=test2" ++ SUFFIX_3 = f"dc=test3,{DEFAULT_SUFFIX}" ++ all_suffixes = [DEFAULT_SUFFIX, SUFFIX_2, SUFFIX_3] ++ ++ test_users_by_suffix = {suffix: [] for suffix in all_suffixes} ++ created_backends = [] ++ ++ suppliers = [ ++ topo_m4.ms["supplier1"], ++ topo_m4.ms["supplier2"], ++ topo_m4.ms["supplier3"], ++ topo_m4.ms["supplier4"] ++ ] ++ ++ try: ++ # Setup additional backends and replication for the new suffixes ++ for suffix in [SUFFIX_2, SUFFIX_3]: ++ repl = ReplicationManager(suffix) ++ for supplier in suppliers: ++ # Create a new backend for this suffix ++ props = { ++ 'cn': f'userRoot_{suffix.split(",")[0][3:]}', ++ 'nsslapd-suffix': suffix ++ } ++ be = Backend(supplier) ++ be.create(properties=props) ++ be.create_sample_entries('001004002') ++ ++ # Track the backend so we can remove it later ++ created_backends.append((supplier, props['cn'])) ++ ++ # Enable replication ++ if supplier == suppliers[0]: ++ repl.create_first_supplier(supplier) ++ else: ++ repl.join_supplier(suppliers[0], supplier) ++ ++ # Create a full mesh topology for this suffix ++ for i, supplier_i in enumerate(suppliers): ++ for j, supplier_j in enumerate(suppliers): ++ if i != j: ++ repl.ensure_agreement(supplier_i, supplier_j) ++ ++ # Generate test data for each suffix (add, modify, remove) ++ for suffix in all_suffixes: ++ # Create some user entries in supplier1 ++ for i in range(20): ++ user_dn = f'uid=test_user_{i},{suffix}' ++ test_user = UserAccount(suppliers[0], user_dn) ++ test_user.create(properties={ ++ 'uid': f'test_user_{i}', ++ 'cn': f'Test User {i}', ++ 'sn': f'User{i}', ++ 'userPassword': 'password', ++ 'uidNumber': str(1000 + i), ++ 'gidNumber': '2000', ++ 'homeDirectory': f'/home/test_user_{i}' ++ }) ++ test_users_by_suffix[suffix].append(test_user) ++ ++ # Perform modifications on these entries ++ for user in test_users_by_suffix[suffix]: ++ # Add some attributes ++ for j in range(3): ++ user.add('description', f'Description {j}') ++ # Replace an attribute ++ user.replace('cn', f'Modified User {user.get_attr_val_utf8("uid")}') ++ # Delete the attributes we added ++ for j in range(3): ++ try: ++ user.remove('description', f'Description {j}') ++ except Exception: ++ pass ++ ++ # Wait for replication to complete across all suppliers, for each suffix ++ for suffix in all_suffixes: ++ repl = ReplicationManager(suffix) ++ for i, supplier_i in enumerate(suppliers): ++ for j, supplier_j in enumerate(suppliers): ++ if i != j: ++ repl.wait_for_replication(supplier_i, supplier_j) ++ ++ # Verify that each user and modification replicated to all suppliers ++ for suffix in all_suffixes: ++ for i in range(20): ++ user_dn = f'uid=test_user_{i},{suffix}' ++ # Retrieve this user from all suppliers ++ all_user_objs = topo_m4.all_get_dsldapobject(user_dn, UserAccount) ++ # Ensure it exists in all 4 suppliers ++ assert len(all_user_objs) == 4, ( ++ f"User {user_dn} not found on all suppliers. " ++ f"Found only on {len(all_user_objs)} suppliers." ++ ) ++ # Check modifications: 'cn' should now be 'Modified User test_user_{i}' ++ for user_obj in all_user_objs: ++ expected_cn = f"Modified User test_user_{i}" ++ actual_cn = user_obj.get_attr_val_utf8("cn") ++ assert actual_cn == expected_cn, ( ++ f"User {user_dn} has unexpected 'cn': {actual_cn} " ++ f"(expected '{expected_cn}') on supplier {user_obj._instance.serverid}" ++ ) ++ # And check that 'description' attributes were removed ++ desc_vals = user_obj.get_attr_vals_utf8('description') ++ for j in range(3): ++ assert f"Description {j}" not in desc_vals, ( ++ f"User {user_dn} on supplier {user_obj._instance.serverid} " ++ f"still has 'Description {j}'" ++ ) ++ finally: ++ for suffix, test_users in test_users_by_suffix.items(): ++ for user in test_users: ++ try: ++ if user.exists(): ++ user.delete() ++ except Exception: ++ pass ++ ++ for suffix in [SUFFIX_2, SUFFIX_3]: ++ repl = ReplicationManager(suffix) ++ for supplier in suppliers: ++ try: ++ repl.remove_supplier(supplier) ++ except Exception: ++ pass ++ ++ for (supplier, backend_name) in created_backends: ++ be = Backend(supplier, backend_name) ++ try: ++ be.delete() ++ except Exception: ++ pass ++ ++ + def test_new_suffix(topo_m4, new_suffix): + """Check that we can enable replication on a new suffix + +diff --git a/dirsrvtests/tests/suites/replication/cleanallruv_shutdown_crash_test.py b/dirsrvtests/tests/suites/replication/cleanallruv_shutdown_crash_test.py +index b4b74e339..fe9955e7e 100644 +--- a/dirsrvtests/tests/suites/replication/cleanallruv_shutdown_crash_test.py ++++ b/dirsrvtests/tests/suites/replication/cleanallruv_shutdown_crash_test.py +@@ -66,10 +66,10 @@ def test_clean_shutdown_crash(topology_m2): + + log.info('Creating replication dns') + services = ServiceAccounts(m1, DEFAULT_SUFFIX) +- repl_m1 = services.get('%s:%s' % (m1.host, m1.sslport)) ++ repl_m1 = services.get(f'{DEFAULT_SUFFIX}:{m1.host}:{m1.sslport}') + repl_m1.set('nsCertSubjectDN', m1.get_server_tls_subject()) + +- repl_m2 = services.get('%s:%s' % (m2.host, m2.sslport)) ++ repl_m2 = services.get(f'{DEFAULT_SUFFIX}:{m2.host}:{m2.sslport}') + repl_m2.set('nsCertSubjectDN', m2.get_server_tls_subject()) + + log.info('Changing auth type') +diff --git a/dirsrvtests/tests/suites/replication/regression_m2_test.py b/dirsrvtests/tests/suites/replication/regression_m2_test.py +index 72d4b9f89..9c707615f 100644 +--- a/dirsrvtests/tests/suites/replication/regression_m2_test.py ++++ b/dirsrvtests/tests/suites/replication/regression_m2_test.py +@@ -64,7 +64,7 @@ class _AgmtHelper: + self.binddn = f'cn={cn},cn=config' + else: + self.usedn = False +- self.cn = f'{self.from_inst.host}:{self.from_inst.sslport}' ++ self.cn = ldap.dn.escape_dn_chars(f'{DEFAULT_SUFFIX}:{self.from_inst.host}:{self.from_inst.sslport}') + self.binddn = f'cn={self.cn}, ou=Services, {DEFAULT_SUFFIX}' + self.original_state = [] + self._pass = False +diff --git a/dirsrvtests/tests/suites/replication/tls_client_auth_repl_test.py b/dirsrvtests/tests/suites/replication/tls_client_auth_repl_test.py +index a00dc5b78..ca17554c7 100644 +--- a/dirsrvtests/tests/suites/replication/tls_client_auth_repl_test.py ++++ b/dirsrvtests/tests/suites/replication/tls_client_auth_repl_test.py +@@ -56,10 +56,10 @@ def tls_client_auth(topo_m2): + + # Create the replication dns + services = ServiceAccounts(m1, DEFAULT_SUFFIX) +- repl_m1 = services.get('%s:%s' % (m1.host, m1.sslport)) ++ repl_m1 = services.get(f'{DEFAULT_SUFFIX}:{m1.host}:{m1.sslport}') + repl_m1.set('nsCertSubjectDN', m1.get_server_tls_subject()) + +- repl_m2 = services.get('%s:%s' % (m2.host, m2.sslport)) ++ repl_m2 = services.get(f'{DEFAULT_SUFFIX}:{m2.host}:{m2.sslport}') + repl_m2.set('nsCertSubjectDN', m2.get_server_tls_subject()) + + # Check the replication is "done". +diff --git a/src/lib389/lib389/_mapped_object.py b/src/lib389/lib389/_mapped_object.py +index b7391d8cc..ae00c95d0 100644 +--- a/src/lib389/lib389/_mapped_object.py ++++ b/src/lib389/lib389/_mapped_object.py +@@ -19,7 +19,7 @@ from lib389._constants import DIRSRV_STATE_ONLINE + from lib389._mapped_object_lint import DSLint, DSLints + from lib389.utils import ( + ensure_bytes, ensure_str, ensure_int, ensure_list_bytes, ensure_list_str, +- ensure_list_int, display_log_value, display_log_data ++ ensure_list_int, display_log_value, display_log_data, is_a_dn, normalizeDN + ) + + # This function filter and term generation provided thanks to +@@ -292,15 +292,28 @@ class DSLdapObject(DSLogging, DSLint): + _search_ext_s(self._instance,self._dn, ldap.SCOPE_BASE, self._object_filter, attrlist=[attr, ], + serverctrls=self._server_controls, clientctrls=self._client_controls, + escapehatch='i am sure')[0] +- values = self.get_attr_vals_bytes(attr) ++ values = self.get_attr_vals_utf8(attr) + self._log.debug("%s contains %s" % (self._dn, values)) + + if value is None: + # We are just checking if SOMETHING is present .... + return len(values) > 0 ++ ++ # Otherwise, we are checking a specific value ++ if is_a_dn(value): ++ normalized_value = normalizeDN(value) + else: +- # Check if a value really does exist. +- return ensure_bytes(value).lower() in [x.lower() for x in values] ++ normalized_value = ensure_bytes(value).lower() ++ ++ # Normalize each returned value depending on whether it is a DN ++ normalized_values = [] ++ for v in values: ++ if is_a_dn(v): ++ normalized_values.append(normalizeDN(v)) ++ else: ++ normalized_values.append(ensure_bytes(v.lower())) ++ ++ return normalized_value in normalized_values + + def add(self, key, value): + """Add an attribute with a value +diff --git a/src/lib389/lib389/replica.py b/src/lib389/lib389/replica.py +index 1f321972d..cd46e86d5 100644 +--- a/src/lib389/lib389/replica.py ++++ b/src/lib389/lib389/replica.py +@@ -2011,7 +2011,7 @@ class ReplicationManager(object): + return repl_group + else: + try: +- repl_group = groups.get('replication_managers') ++ repl_group = groups.get(dn=f'cn=replication_managers,{self._suffix}') + return repl_group + except ldap.NO_SUCH_OBJECT: + self._log.warning("{} doesn't have cn=replication_managers,{} entry \ +@@ -2035,7 +2035,7 @@ class ReplicationManager(object): + services = ServiceAccounts(from_instance, self._suffix) + # Generate the password and save the credentials + # for putting them into agreements in the future +- service_name = '{}:{}'.format(to_instance.host, port) ++ service_name = f'{self._suffix}:{to_instance.host}:{port}' + creds = password_generate() + repl_service = services.ensure_state(properties={ + 'cn': service_name, +@@ -2299,7 +2299,7 @@ class ReplicationManager(object): + Internal Only. + """ + +- rdn = '{}:{}'.format(from_instance.host, from_instance.sslport) ++ rdn = f'{self._suffix}:{from_instance.host}:{from_instance.sslport}' + try: + creds = self._repl_creds[rdn] + except KeyError: +@@ -2499,8 +2499,8 @@ class ReplicationManager(object): + # Touch something then wait_for_replication. + from_groups = Groups(from_instance, basedn=self._suffix, rdn=None) + to_groups = Groups(to_instance, basedn=self._suffix, rdn=None) +- from_group = from_groups.get('replication_managers') +- to_group = to_groups.get('replication_managers') ++ from_group = from_groups.get(dn=f'cn=replication_managers,{self._suffix}') ++ to_group = to_groups.get(dn=f'cn=replication_managers,{self._suffix}') + + change = str(uuid.uuid4()) + +-- +2.49.0 + diff --git a/SOURCES/0041-Issue-6655-fix-replication-release-replica-decoding-.patch b/SOURCES/0041-Issue-6655-fix-replication-release-replica-decoding-.patch new file mode 100644 index 0000000..3f11d8a --- /dev/null +++ b/SOURCES/0041-Issue-6655-fix-replication-release-replica-decoding-.patch @@ -0,0 +1,126 @@ +From ebe986c78c6cd4e1f10172d8a8a11faf814fbc22 Mon Sep 17 00:00:00 2001 +From: Mark Reynolds +Date: Thu, 6 Mar 2025 16:49:53 -0500 +Subject: [PATCH] Issue 6655 - fix replication release replica decoding error + +Description: + +When a start replication session extended op is received acquire and +release exclusive access before returning the result to the client. +Otherwise there is a race condition where a "end" replication extended +op can arrive before the replica is released and that leads to a +decoding error on the other replica. + +Relates: https://github.com/389ds/389-ds-base/issues/6655 + +Reviewed by: spichugi, tbordaz, and vashirov(Thanks!!!) +--- + .../suites/replication/acceptance_test.py | 12 ++++++++++ + ldap/servers/plugins/replication/repl_extop.c | 24 ++++++++++++------- + 2 files changed, 27 insertions(+), 9 deletions(-) + +diff --git a/dirsrvtests/tests/suites/replication/acceptance_test.py b/dirsrvtests/tests/suites/replication/acceptance_test.py +index fc8622051..0f18edb44 100644 +--- a/dirsrvtests/tests/suites/replication/acceptance_test.py ++++ b/dirsrvtests/tests/suites/replication/acceptance_test.py +@@ -1,5 +1,9 @@ + # --- BEGIN COPYRIGHT BLOCK --- ++<<<<<<< HEAD + # Copyright (C) 2021 Red Hat, Inc. ++======= ++# Copyright (C) 2025 Red Hat, Inc. ++>>>>>>> a623c3f90 (Issue 6655 - fix replication release replica decoding error) + # All rights reserved. + # + # License: GPL (version 3 or any later version). +@@ -453,6 +457,13 @@ def test_multi_subsuffix_replication(topo_m4): + f"User {user_dn} on supplier {user_obj._instance.serverid} " + f"still has 'Description {j}'" + ) ++ ++ # Check there are no decoding errors ++ assert not topo_m4.ms["supplier1"].ds_error_log.match('.*decoding failed.*') ++ assert not topo_m4.ms["supplier2"].ds_error_log.match('.*decoding failed.*') ++ assert not topo_m4.ms["supplier3"].ds_error_log.match('.*decoding failed.*') ++ assert not topo_m4.ms["supplier4"].ds_error_log.match('.*decoding failed.*') ++ + finally: + for suffix, test_users in test_users_by_suffix.items(): + for user in test_users: +@@ -507,6 +518,7 @@ def test_new_suffix(topo_m4, new_suffix): + repl.remove_supplier(m1) + repl.remove_supplier(m2) + ++ + def test_many_attrs(topo_m4, create_entry): + """Check a replication with many attributes (add and delete) + +diff --git a/ldap/servers/plugins/replication/repl_extop.c b/ldap/servers/plugins/replication/repl_extop.c +index 14b756df1..dacc611c0 100644 +--- a/ldap/servers/plugins/replication/repl_extop.c ++++ b/ldap/servers/plugins/replication/repl_extop.c +@@ -1134,6 +1134,12 @@ send_response: + slapi_pblock_set(pb, SLAPI_EXT_OP_RET_OID, REPL_NSDS50_REPLICATION_RESPONSE_OID); + } + ++ /* connext (release our hold on it at least) */ ++ if (NULL != connext) { ++ /* don't free it, just let go of it */ ++ consumer_connection_extension_relinquish_exclusive_access(conn, connid, opid, PR_FALSE); ++ } ++ + slapi_pblock_set(pb, SLAPI_EXT_OP_RET_VALUE, resp_bval); + slapi_log_err(SLAPI_LOG_REPL, repl_plugin_name, + "multimaster_extop_StartNSDS50ReplicationRequest - " +@@ -1251,12 +1257,6 @@ send_response: + if (NULL != ruv_bervals) { + ber_bvecfree(ruv_bervals); + } +- /* connext (our hold on it at least) */ +- if (NULL != connext) { +- /* don't free it, just let go of it */ +- consumer_connection_extension_relinquish_exclusive_access(conn, connid, opid, PR_FALSE); +- connext = NULL; +- } + + return return_value; + } +@@ -1389,6 +1389,13 @@ multimaster_extop_EndNSDS50ReplicationRequest(Slapi_PBlock *pb) + } + } + send_response: ++ /* connext (release our hold on it at least) */ ++ if (NULL != connext) { ++ /* don't free it, just let go of it */ ++ consumer_connection_extension_relinquish_exclusive_access(conn, connid, opid, PR_FALSE); ++ connext = NULL; ++ } ++ + /* Send the response code */ + if ((resp_bere = der_alloc()) == NULL) { + goto free_and_return; +@@ -1419,11 +1426,10 @@ free_and_return: + if (NULL != resp_bval) { + ber_bvfree(resp_bval); + } +- /* connext (our hold on it at least) */ ++ /* connext (release our hold on it if not already released) */ + if (NULL != connext) { + /* don't free it, just let go of it */ + consumer_connection_extension_relinquish_exclusive_access(conn, connid, opid, PR_FALSE); +- connext = NULL; + } + + return return_value; +@@ -1516,7 +1522,7 @@ multimaster_extop_abort_cleanruv(Slapi_PBlock *pb) + rid); + } + /* +- * Get the replica ++ * Get the replica + */ + if ((r = replica_get_replica_from_root(repl_root)) == NULL) { + slapi_log_err(SLAPI_LOG_ERR, repl_plugin_name, "multimaster_extop_abort_cleanruv - " +-- +2.49.0 + diff --git a/SOURCES/0042-Issue-6655-fix-merge-conflict.patch b/SOURCES/0042-Issue-6655-fix-merge-conflict.patch new file mode 100644 index 0000000..7a4184e --- /dev/null +++ b/SOURCES/0042-Issue-6655-fix-merge-conflict.patch @@ -0,0 +1,26 @@ +From 5b12463bfeb518f016acb14bc118b5f8ad3eef5e Mon Sep 17 00:00:00 2001 +From: Viktor Ashirov +Date: Thu, 15 May 2025 09:22:22 +0200 +Subject: [PATCH] Issue 6655 - fix merge conflict + +--- + dirsrvtests/tests/suites/replication/acceptance_test.py | 4 ---- + 1 file changed, 4 deletions(-) + +diff --git a/dirsrvtests/tests/suites/replication/acceptance_test.py b/dirsrvtests/tests/suites/replication/acceptance_test.py +index 0f18edb44..6b5186127 100644 +--- a/dirsrvtests/tests/suites/replication/acceptance_test.py ++++ b/dirsrvtests/tests/suites/replication/acceptance_test.py +@@ -1,9 +1,5 @@ + # --- BEGIN COPYRIGHT BLOCK --- +-<<<<<<< HEAD +-# Copyright (C) 2021 Red Hat, Inc. +-======= + # Copyright (C) 2025 Red Hat, Inc. +->>>>>>> a623c3f90 (Issue 6655 - fix replication release replica decoding error) + # All rights reserved. + # + # License: GPL (version 3 or any later version). +-- +2.49.0 + diff --git a/SOURCES/0043-Issue-6571-Nested-group-does-not-receive-memberOf-at.patch b/SOURCES/0043-Issue-6571-Nested-group-does-not-receive-memberOf-at.patch new file mode 100644 index 0000000..7ee7cc5 --- /dev/null +++ b/SOURCES/0043-Issue-6571-Nested-group-does-not-receive-memberOf-at.patch @@ -0,0 +1,291 @@ +From 8d62124fb4d0700378b6f0669cc9d47338a8151c Mon Sep 17 00:00:00 2001 +From: tbordaz +Date: Tue, 25 Mar 2025 09:20:50 +0100 +Subject: [PATCH] Issue 6571 - Nested group does not receive memberOf attribute + (#6679) + +Bug description: + There is a risk to create a loop in group membership. + For example G2 is member of G1 and G1 is member of G2. + Memberof plugins iterates from a node to its ancestors + to update the 'memberof' values of the node. + The plugin uses a valueset ('already_seen_ndn_vals') + to keep the track of the nodes it already visited. + It uses this valueset to detect a possible loop and + in that case it does not add the ancestor as the + memberof value of the node. + This is an error in case there are multiples paths + up to an ancestor. + +Fix description: + The ancestor should be added to the node systematically, + just in case the ancestor is in 'already_seen_ndn_vals' + it skips the final recursion + +fixes: #6571 + +Reviewed by: Pierre Rogier, Mark Reynolds (Thanks !!!) +--- + .../suites/memberof_plugin/regression_test.py | 109 ++++++++++++++++++ + .../tests/suites/plugins/memberof_test.py | 5 + + ldap/servers/plugins/memberof/memberof.c | 52 ++++----- + 3 files changed, 137 insertions(+), 29 deletions(-) + +diff --git a/dirsrvtests/tests/suites/memberof_plugin/regression_test.py b/dirsrvtests/tests/suites/memberof_plugin/regression_test.py +index 4c681a909..dba908975 100644 +--- a/dirsrvtests/tests/suites/memberof_plugin/regression_test.py ++++ b/dirsrvtests/tests/suites/memberof_plugin/regression_test.py +@@ -467,6 +467,21 @@ def _find_memberof_ext(server, user_dn=None, group_dn=None, find_result=True): + else: + assert (not found) + ++def _check_membership(server, entry, expected_members, expected_memberof): ++ assert server ++ assert entry ++ ++ memberof = entry.get_attr_vals('memberof') ++ member = entry.get_attr_vals('member') ++ assert len(member) == len(expected_members) ++ assert len(memberof) == len(expected_memberof) ++ for e in expected_members: ++ server.log.info("Checking %s has member %s" % (entry.dn, e.dn)) ++ assert e.dn.encode() in member ++ for e in expected_memberof: ++ server.log.info("Checking %s is member of %s" % (entry.dn, e.dn)) ++ assert e.dn.encode() in memberof ++ + + @pytest.mark.ds49161 + def test_memberof_group(topology_st): +@@ -535,6 +550,100 @@ def test_memberof_group(topology_st): + _find_memberof_ext(inst, dn1, g2n, True) + _find_memberof_ext(inst, dn2, g2n, True) + ++def test_multipaths(topology_st, request): ++ """Test memberof succeeds to update memberof when ++ there are multiple paths from a leaf to an intermediate node ++ ++ :id: 35aa704a-b895-4153-9dcb-1e8a13612ebf ++ ++ :setup: Single instance ++ ++ :steps: ++ 1. Create a graph G1->U1, G2->G21->U1 ++ 2. Add G2 as member of G1: G1->U1, G1->G2->G21->U1 ++ 3. Check members and memberof in entries G1,G2,G21,User1 ++ ++ :expectedresults: ++ 1. Graph should be created ++ 2. succeed ++ 3. Membership is okay ++ """ ++ ++ inst = topology_st.standalone ++ memberof = MemberOfPlugin(inst) ++ memberof.enable() ++ memberof.replace('memberOfEntryScope', SUFFIX) ++ if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() == "on"): ++ delay = 3 ++ else: ++ delay = 0 ++ inst.restart() ++ ++ # ++ # Create the hierarchy ++ # ++ # ++ # Grp1 ---------------> User1 ++ # ^ ++ # / ++ # Grp2 ----> Grp21 ------/ ++ # ++ users = UserAccounts(inst, SUFFIX, rdn=None) ++ user1 = users.create(properties={'uid': "user1", ++ 'cn': "user1", ++ 'sn': 'SN', ++ 'description': 'leaf', ++ 'uidNumber': '1000', ++ 'gidNumber': '2000', ++ 'homeDirectory': '/home/user1' ++ }) ++ group = Groups(inst, SUFFIX, rdn=None) ++ g1 = group.create(properties={'cn': 'group1', ++ 'member': user1.dn, ++ 'description': 'group1'}) ++ g21 = group.create(properties={'cn': 'group21', ++ 'member': user1.dn, ++ 'description': 'group21'}) ++ g2 = group.create(properties={'cn': 'group2', ++ 'member': [g21.dn], ++ 'description': 'group2'}) ++ ++ # Enable debug logs if necessary ++ #inst.config.replace('nsslapd-errorlog-level', '65536') ++ #inst.config.set('nsslapd-accesslog-level','260') ++ #inst.config.set('nsslapd-plugin-logging', 'on') ++ #inst.config.set('nsslapd-auditlog-logging-enabled','on') ++ #inst.config.set('nsslapd-auditfaillog-logging-enabled','on') ++ ++ # ++ # Update the hierarchy ++ # ++ # ++ # Grp1 ----------------> User1 ++ # \ ^ ++ # \ / ++ # --> Grp2 --> Grp21 -- ++ # ++ g1.add_member(g2.dn) ++ time.sleep(delay) ++ ++ # ++ # Check G1, G2, G21 and User1 members and memberof ++ # ++ _check_membership(inst, g1, expected_members=[g2, user1], expected_memberof=[]) ++ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[g1]) ++ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2, g1]) ++ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1]) ++ ++ def fin(): ++ try: ++ user1.delete() ++ g1.delete() ++ g2.delete() ++ g21.delete() ++ except: ++ pass ++ request.addfinalizer(fin) + + def _config_memberof_entrycache_on_modrdn_failure(server): + +diff --git a/dirsrvtests/tests/suites/plugins/memberof_test.py b/dirsrvtests/tests/suites/plugins/memberof_test.py +index 2de1389fd..621c45daf 100644 +--- a/dirsrvtests/tests/suites/plugins/memberof_test.py ++++ b/dirsrvtests/tests/suites/plugins/memberof_test.py +@@ -2168,9 +2168,14 @@ def test_complex_group_scenario_6(topology_st): + + # add Grp[1-4] (uniqueMember) to grp5 + # it creates a membership loop !!! ++ topology_st.standalone.config.replace('nsslapd-errorlog-level', '65536') + mods = [(ldap.MOD_ADD, 'uniqueMember', memofegrp020_5)] + for grp in [memofegrp020_1, memofegrp020_2, memofegrp020_3, memofegrp020_4]: + topology_st.standalone.modify_s(ensure_str(grp), mods) ++ topology_st.standalone.config.replace('nsslapd-errorlog-level', '0') ++ ++ results = topology_st.standalone.ds_error_log.match('.*detecting a loop in group.*') ++ assert results + + time.sleep(5) + # assert user[1-4] are member of grp20_[1-4] +diff --git a/ldap/servers/plugins/memberof/memberof.c b/ldap/servers/plugins/memberof/memberof.c +index e75b99b14..32bdcf3f1 100644 +--- a/ldap/servers/plugins/memberof/memberof.c ++++ b/ldap/servers/plugins/memberof/memberof.c +@@ -1592,7 +1592,7 @@ memberof_call_foreach_dn(Slapi_PBlock *pb __attribute__((unused)), Slapi_DN *sdn + ht_grp = ancestors_cache_lookup(config, (const void *)ndn); + if (ht_grp) { + #if MEMBEROF_CACHE_DEBUG +- slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_call_foreach_dn: Ancestors of %s already cached (%x)\n", ndn, ht_grp); ++ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_call_foreach_dn: Ancestors of %s already cached (%lx)\n", ndn, (ulong) ht_grp); + #endif + add_ancestors_cbdata(ht_grp, callback_data); + *cached = 1; +@@ -1600,7 +1600,7 @@ memberof_call_foreach_dn(Slapi_PBlock *pb __attribute__((unused)), Slapi_DN *sdn + } + } + #if MEMBEROF_CACHE_DEBUG +- slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_call_foreach_dn: Ancestors of %s not cached\n", ndn); ++ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, "memberof_call_foreach_dn: Ancestors of %s not cached\n", slapi_sdn_get_ndn(sdn)); + #endif + + /* Escape the dn, and build the search filter. */ +@@ -3233,7 +3233,8 @@ cache_ancestors(MemberOfConfig *config, Slapi_Value **member_ndn_val, memberof_g + return; + } + #if MEMBEROF_CACHE_DEBUG +- if (double_check = ancestors_cache_lookup(config, (const void*) key)) { ++ double_check = ancestors_cache_lookup(config, (const void*) key); ++ if (double_check) { + dump_cache_entry(double_check, "read back"); + } + #endif +@@ -3263,13 +3264,13 @@ merge_ancestors(Slapi_Value **member_ndn_val, memberof_get_groups_data *v1, memb + sval_dn = slapi_value_new_string(slapi_value_get_string(sval)); + if (sval_dn) { + /* Use the normalized dn from v1 to search it +- * in v2 +- */ ++ * in v2 ++ */ + val_sdn = slapi_sdn_new_dn_byval(slapi_value_get_string(sval_dn)); + sval_ndn = slapi_value_new_string(slapi_sdn_get_ndn(val_sdn)); + if (!slapi_valueset_find( + ((memberof_get_groups_data *)v2)->config->group_slapiattrs[0], v2_group_norm_vals, sval_ndn)) { +-/* This ancestor was not already present in v2 => Add it ++ /* This ancestor was not already present in v2 => Add it + * Using slapi_valueset_add_value it consumes val + * so do not free sval + */ +@@ -3318,7 +3319,7 @@ memberof_get_groups_r(MemberOfConfig *config, Slapi_DN *member_sdn, memberof_get + + merge_ancestors(&member_ndn_val, &member_data, data); + if (!cached && member_data.use_cache) +- cache_ancestors(config, &member_ndn_val, &member_data); ++ cache_ancestors(config, &member_ndn_val, data); + + slapi_value_free(&member_ndn_val); + slapi_valueset_free(groupvals); +@@ -3379,25 +3380,6 @@ memberof_get_groups_callback(Slapi_Entry *e, void *callback_data) + goto bail; + } + +- /* Have we been here before? Note that we don't loop through all of the group_slapiattrs +- * in config. We only need this attribute for it's syntax so the comparison can be +- * performed. Since all of the grouping attributes are validated to use the Dinstinguished +- * Name syntax, we can safely just use the first group_slapiattr. */ +- if (slapi_valueset_find( +- ((memberof_get_groups_data *)callback_data)->config->group_slapiattrs[0], already_seen_ndn_vals, group_ndn_val)) { +- /* we either hit a recursive grouping, or an entry is +- * a member of a group through multiple paths. Either +- * way, we can just skip processing this entry since we've +- * already gone through this part of the grouping hierarchy. */ +- slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, +- "memberof_get_groups_callback - Possible group recursion" +- " detected in %s\n", +- group_ndn); +- slapi_value_free(&group_ndn_val); +- ((memberof_get_groups_data *)callback_data)->use_cache = PR_FALSE; +- goto bail; +- } +- + /* if the group does not belong to an excluded subtree, adds it to the valueset */ + if (memberof_entry_in_scope(config, group_sdn)) { + /* Push group_dn_val into the valueset. This memory is now owned +@@ -3407,9 +3389,21 @@ memberof_get_groups_callback(Slapi_Entry *e, void *callback_data) + group_dn_val = slapi_value_new_string(group_dn); + slapi_valueset_add_value_ext(groupvals, group_dn_val, SLAPI_VALUE_FLAG_PASSIN); + +- /* push this ndn to detect group recursion */ +- already_seen_ndn_val = slapi_value_new_string(group_ndn); +- slapi_valueset_add_value_ext(already_seen_ndn_vals, already_seen_ndn_val, SLAPI_VALUE_FLAG_PASSIN); ++ if (slapi_valueset_find( ++ ((memberof_get_groups_data *)callback_data)->config->group_slapiattrs[0], already_seen_ndn_vals, group_ndn_val)) { ++ /* The group group_ndn_val has already been processed ++ * skip the final recursion to prevent infinite loop ++ */ ++ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, ++ "memberof_get_groups_callback - detecting a loop in group %s (stop building memberof)\n", ++ group_ndn); ++ ((memberof_get_groups_data *)callback_data)->use_cache = PR_FALSE; ++ goto bail; ++ } else { ++ /* keep this ndn to detect a possible group recursion */ ++ already_seen_ndn_val = slapi_value_new_string(group_ndn); ++ slapi_valueset_add_value_ext(already_seen_ndn_vals, already_seen_ndn_val, SLAPI_VALUE_FLAG_PASSIN); ++ } + } + if (!config->skip_nested || config->fixup_task) { + /* now recurse to find ancestors groups of e */ +-- +2.49.0 + diff --git a/SOURCES/0044-Issue-6571-2nd-Nested-group-does-not-receive-memberO.patch b/SOURCES/0044-Issue-6571-2nd-Nested-group-does-not-receive-memberO.patch new file mode 100644 index 0000000..1999b23 --- /dev/null +++ b/SOURCES/0044-Issue-6571-2nd-Nested-group-does-not-receive-memberO.patch @@ -0,0 +1,272 @@ +From 17da0257b24749765777a4e64c3626cb39cca639 Mon Sep 17 00:00:00 2001 +From: tbordaz +Date: Mon, 31 Mar 2025 11:05:01 +0200 +Subject: [PATCH] Issue 6571 - (2nd) Nested group does not receive memberOf + attribute (#6697) + +Bug description: + erroneous debug change made in previous fix + where cache_ancestors is called with the wrong parameter + +Fix description: + Restore the orginal param 'member_data' + Increase the set of tests around multipaths + +fixes: #6571 + +review by: Simon Pichugin (Thanks !!) +--- + .../suites/memberof_plugin/regression_test.py | 154 ++++++++++++++++++ + ldap/servers/plugins/memberof/memberof.c | 50 +++++- + 2 files changed, 203 insertions(+), 1 deletion(-) + +diff --git a/dirsrvtests/tests/suites/memberof_plugin/regression_test.py b/dirsrvtests/tests/suites/memberof_plugin/regression_test.py +index dba908975..9ba40a0c3 100644 +--- a/dirsrvtests/tests/suites/memberof_plugin/regression_test.py ++++ b/dirsrvtests/tests/suites/memberof_plugin/regression_test.py +@@ -598,6 +598,8 @@ def test_multipaths(topology_st, request): + 'homeDirectory': '/home/user1' + }) + group = Groups(inst, SUFFIX, rdn=None) ++ g0 = group.create(properties={'cn': 'group0', ++ 'description': 'group0'}) + g1 = group.create(properties={'cn': 'group1', + 'member': user1.dn, + 'description': 'group1'}) +@@ -635,6 +637,158 @@ def test_multipaths(topology_st, request): + _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2, g1]) + _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1]) + ++ #inst.config.replace('nsslapd-errorlog-level', '65536') ++ #inst.config.set('nsslapd-accesslog-level','260') ++ #inst.config.set('nsslapd-plugin-logging', 'on') ++ #inst.config.set('nsslapd-auditlog-logging-enabled','on') ++ #inst.config.set('nsslapd-auditfaillog-logging-enabled','on') ++ # ++ # Update the hierarchy ++ # ++ # ++ # Grp1 ----------------> User1 ++ # ^ ++ # / ++ # Grp2 --> Grp21 -- ++ # ++ g1.remove_member(g2.dn) ++ time.sleep(delay) ++ ++ # ++ # Check G1, G2, G21 and User1 members and memberof ++ # ++ _check_membership(inst, g1, expected_members=[user1], expected_memberof=[]) ++ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[]) ++ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2]) ++ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1]) ++ ++ # ++ # Update the hierarchy ++ # ++ # ++ # Grp1 ----------------> User1 ++ # \__________ ^ ++ # | / ++ # v / ++ # Grp2 --> Grp21 ---- ++ # ++ g1.add_member(g21.dn) ++ time.sleep(delay) ++ ++ # ++ # Check G1, G2, G21 and User1 members and memberof ++ # ++ _check_membership(inst, g1, expected_members=[user1, g21], expected_memberof=[]) ++ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[]) ++ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2, g1]) ++ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1]) ++ ++ # ++ # Update the hierarchy ++ # ++ # ++ # Grp1 ----------------> User1 ++ # ^ ++ # / ++ # Grp2 --> Grp21 -- ++ # ++ g1.remove_member(g21.dn) ++ time.sleep(delay) ++ ++ # ++ # Check G1, G2, G21 and User1 members and memberof ++ # ++ _check_membership(inst, g1, expected_members=[user1], expected_memberof=[]) ++ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[]) ++ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2]) ++ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1]) ++ ++ # ++ # Update the hierarchy ++ # ++ # ++ # Grp1 ----------------> User1 ++ # ^ ++ # / ++ # Grp0 ---> Grp2 ---> Grp21 --- ++ # ++ g0.add_member(g2.dn) ++ time.sleep(delay) ++ ++ # ++ # Check G0,G1, G2, G21 and User1 members and memberof ++ # ++ _check_membership(inst, g0, expected_members=[g2], expected_memberof=[]) ++ _check_membership(inst, g1, expected_members=[user1], expected_memberof=[]) ++ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[g0]) ++ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g0, g2]) ++ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1, g0]) ++ ++ # ++ # Update the hierarchy ++ # ++ # ++ # Grp1 ----------------> User1 ++ # ^ ^ ++ # / / ++ # Grp0 ---> Grp2 ---> Grp21 --- ++ # ++ g0.add_member(g1.dn) ++ time.sleep(delay) ++ ++ # ++ # Check G0,G1, G2, G21 and User1 members and memberof ++ # ++ _check_membership(inst, g0, expected_members=[g1,g2], expected_memberof=[]) ++ _check_membership(inst, g1, expected_members=[user1], expected_memberof=[g0]) ++ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[g0]) ++ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g0, g2]) ++ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1, g0]) ++ ++ # ++ # Update the hierarchy ++ # ++ # ++ # Grp1 ----------------> User1 ++ # ^ \_____________ ^ ++ # / | / ++ # / V / ++ # Grp0 ---> Grp2 ---> Grp21 --- ++ # ++ g1.add_member(g21.dn) ++ time.sleep(delay) ++ ++ # ++ # Check G0,G1, G2, G21 and User1 members and memberof ++ # ++ _check_membership(inst, g0, expected_members=[g1, g2], expected_memberof=[]) ++ _check_membership(inst, g1, expected_members=[user1, g21], expected_memberof=[g0]) ++ _check_membership(inst, g2, expected_members=[g21], expected_memberof=[g0]) ++ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g0, g1, g2]) ++ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1, g0]) ++ ++ # ++ # Update the hierarchy ++ # ++ # ++ # Grp1 ----------------> User1 ++ # ^ \_____________ ^ ++ # / | / ++ # / V / ++ # Grp0 ---> Grp2 Grp21 --- ++ # ++ g2.remove_member(g21.dn) ++ time.sleep(delay) ++ ++ # ++ # Check G0,G1, G2, G21 and User1 members and memberof ++ # ++ _check_membership(inst, g0, expected_members=[g1, g2], expected_memberof=[]) ++ _check_membership(inst, g1, expected_members=[user1, g21], expected_memberof=[g0]) ++ _check_membership(inst, g2, expected_members=[], expected_memberof=[g0]) ++ _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g0, g1]) ++ _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g1, g0]) ++ + def fin(): + try: + user1.delete() +diff --git a/ldap/servers/plugins/memberof/memberof.c b/ldap/servers/plugins/memberof/memberof.c +index 32bdcf3f1..f79b083a9 100644 +--- a/ldap/servers/plugins/memberof/memberof.c ++++ b/ldap/servers/plugins/memberof/memberof.c +@@ -3258,6 +3258,35 @@ merge_ancestors(Slapi_Value **member_ndn_val, memberof_get_groups_data *v1, memb + Slapi_ValueSet *v2_group_norm_vals = *((memberof_get_groups_data *)v2)->group_norm_vals; + int merged_cnt = 0; + ++#if MEMBEROF_CACHE_DEBUG ++ { ++ Slapi_Value *val = 0; ++ int hint = 0; ++ struct berval *bv; ++ hint = slapi_valueset_first_value(v2_groupvals, &val); ++ while (val) { ++ /* this makes a copy of the berval */ ++ bv = slapi_value_get_berval(val); ++ if (bv && bv->bv_len) { ++ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, ++ "merge_ancestors: V2 contains %s\n", ++ bv->bv_val); ++ } ++ hint = slapi_valueset_next_value(v2_groupvals, hint, &val); ++ } ++ hint = slapi_valueset_first_value(v1_groupvals, &val); ++ while (val) { ++ /* this makes a copy of the berval */ ++ bv = slapi_value_get_berval(val); ++ if (bv && bv->bv_len) { ++ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, ++ "merge_ancestors: add %s (from V1)\n", ++ bv->bv_val); ++ } ++ hint = slapi_valueset_next_value(v1_groupvals, hint, &val); ++ } ++ } ++#endif + hint = slapi_valueset_first_value(v1_groupvals, &sval); + while (sval) { + if (memberof_compare(config, member_ndn_val, &sval)) { +@@ -3319,7 +3348,7 @@ memberof_get_groups_r(MemberOfConfig *config, Slapi_DN *member_sdn, memberof_get + + merge_ancestors(&member_ndn_val, &member_data, data); + if (!cached && member_data.use_cache) +- cache_ancestors(config, &member_ndn_val, data); ++ cache_ancestors(config, &member_ndn_val, &member_data); + + slapi_value_free(&member_ndn_val); + slapi_valueset_free(groupvals); +@@ -4285,6 +4314,25 @@ memberof_fix_memberof_callback(Slapi_Entry *e, void *callback_data) + + /* get a list of all of the groups this user belongs to */ + groups = memberof_get_groups(config, sdn); ++#if MEMBEROF_CACHE_DEBUG ++ { ++ Slapi_Value *val = 0; ++ int hint = 0; ++ struct berval *bv; ++ hint = slapi_valueset_first_value(groups, &val); ++ while (val) { ++ /* this makes a copy of the berval */ ++ bv = slapi_value_get_berval(val); ++ if (bv && bv->bv_len) { ++ slapi_log_err(SLAPI_LOG_PLUGIN, MEMBEROF_PLUGIN_SUBSYSTEM, ++ "memberof_fix_memberof_callback: %s belongs to %s\n", ++ ndn, ++ bv->bv_val); ++ } ++ hint = slapi_valueset_next_value(groups, hint, &val); ++ } ++ } ++#endif + + if (config->group_filter) { + if (slapi_filter_test_simple(e, config->group_filter)) { +-- +2.49.0 + diff --git a/SOURCES/0045-Issue-6698-NPE-after-configuring-invalid-filtered-ro.patch b/SOURCES/0045-Issue-6698-NPE-after-configuring-invalid-filtered-ro.patch new file mode 100644 index 0000000..b7d8652 --- /dev/null +++ b/SOURCES/0045-Issue-6698-NPE-after-configuring-invalid-filtered-ro.patch @@ -0,0 +1,192 @@ +From ff364a4b1c88e1a8f678e056af88cce50cd8717c Mon Sep 17 00:00:00 2001 +From: progier389 +Date: Fri, 28 Mar 2025 17:32:14 +0100 +Subject: [PATCH] Issue 6698 - NPE after configuring invalid filtered role + (#6699) + +Server crash when doing search after configuring filtered role with invalid filter. +Reason: The part of the filter that should be overwritten are freed before knowing that the filter is invalid. +Solution: Check first that the filter is valid before freeing the filtere bits + +Issue: #6698 + +Reviewed by: @tbordaz , @mreynolds389 (Thanks!) + +(cherry picked from commit 31e120d2349eda7a41380cf78fc04cf41e394359) +--- + dirsrvtests/tests/suites/roles/basic_test.py | 80 ++++++++++++++++++-- + ldap/servers/slapd/filter.c | 17 ++++- + 2 files changed, 88 insertions(+), 9 deletions(-) + +diff --git a/dirsrvtests/tests/suites/roles/basic_test.py b/dirsrvtests/tests/suites/roles/basic_test.py +index 875ac47c1..b79816c58 100644 +--- a/dirsrvtests/tests/suites/roles/basic_test.py ++++ b/dirsrvtests/tests/suites/roles/basic_test.py +@@ -28,6 +28,7 @@ from lib389.dbgen import dbgen_users + from lib389.tasks import ImportTask + from lib389.utils import get_default_db_lib + from lib389.rewriters import * ++from lib389._mapped_object import DSLdapObject + from lib389.backend import Backends + + logging.getLogger(__name__).setLevel(logging.INFO) +@@ -427,7 +428,6 @@ def test_vattr_on_filtered_role_restart(topo, request): + log.info("Check the default value of attribute nsslapd-ignore-virtual-attrs should be OFF") + assert topo.standalone.config.present('nsslapd-ignore-virtual-attrs', 'off') + +- + log.info("Check the virtual attribute definition is found (after a required delay)") + topo.standalone.restart() + time.sleep(5) +@@ -541,7 +541,7 @@ def test_managed_and_filtered_role_rewrite(topo, request): + indexes = backend.get_indexes() + try: + index = indexes.create(properties={ +- 'cn': attrname, ++ 'cn': attrname, + 'nsSystemIndex': 'false', + 'nsIndexType': ['eq', 'pres'] + }) +@@ -593,7 +593,6 @@ def test_managed_and_filtered_role_rewrite(topo, request): + dn = "uid=%s0000%d,%s" % (RDN, i, PARENT) + topo.standalone.modify_s(dn, [(ldap.MOD_REPLACE, 'nsRoleDN', [role.dn.encode()])]) + +- + # Now check that search is fast, evaluating only 4 entries + search_start = time.time() + entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn) +@@ -676,7 +675,7 @@ def test_not_such_entry_role_rewrite(topo, request): + indexes = backend.get_indexes() + try: + index = indexes.create(properties={ +- 'cn': attrname, ++ 'cn': attrname, + 'nsSystemIndex': 'false', + 'nsIndexType': ['eq', 'pres'] + }) +@@ -730,7 +729,7 @@ def test_not_such_entry_role_rewrite(topo, request): + + # Enable plugin level to check message + topo.standalone.config.loglevel(vals=(ErrorLog.DEFAULT,ErrorLog.PLUGIN)) +- ++ + # Now check that search is fast, evaluating only 4 entries + search_start = time.time() + entries = topo.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(|(nsrole=%s)(nsrole=cn=not_such_entry_role,%s))" % (role.dn, DEFAULT_SUFFIX)) +@@ -758,6 +757,77 @@ def test_not_such_entry_role_rewrite(topo, request): + + request.addfinalizer(fin) + ++ ++def test_rewriter_with_invalid_filter(topo, request): ++ """Test that server does not crash when having ++ invalid filter in filtered role ++ ++ :id: 5013b0b2-0af6-11f0-8684-482ae39447e5 ++ :setup: standalone server ++ :steps: ++ 1. Setup filtered role with good filter ++ 2. Setup nsrole rewriter ++ 3. Restart the server ++ 4. Search for entries ++ 5. Setup filtered role with bad filter ++ 6. Search for entries ++ :expectedresults: ++ 1. Operation should succeed ++ 2. Operation should succeed ++ 3. Operation should succeed ++ 4. Operation should succeed ++ 5. Operation should succeed ++ 6. Operation should succeed ++ """ ++ inst = topo.standalone ++ entries = [] ++ ++ def fin(): ++ inst.start() ++ for entry in entries: ++ entry.delete() ++ request.addfinalizer(fin) ++ ++ # Setup filtered role ++ roles = FilteredRoles(inst, f'ou=people,{DEFAULT_SUFFIX}') ++ filter_ko = '(&((objectClass=top)(objectClass=nsPerson))' ++ filter_ok = '(&(objectClass=top)(objectClass=nsPerson))' ++ role_properties = { ++ 'cn': 'TestFilteredRole', ++ 'nsRoleFilter': filter_ok, ++ 'description': 'Test good filter', ++ } ++ role = roles.create(properties=role_properties) ++ entries.append(role) ++ ++ # Setup nsrole rewriter ++ rewriters = Rewriters(inst) ++ rewriter_properties = { ++ "cn": "nsrole", ++ "nsslapd-libpath": 'libroles-plugin', ++ "nsslapd-filterrewriter": 'role_nsRole_filter_rewriter', ++ } ++ rewriter = rewriters.ensure_state(properties=rewriter_properties) ++ entries.append(rewriter) ++ ++ # Restart thge instance ++ inst.restart() ++ ++ # Search for entries ++ entries = inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn) ++ ++ # Set bad filter ++ role_properties = { ++ 'cn': 'TestFilteredRole', ++ 'nsRoleFilter': filter_ko, ++ 'description': 'Test bad filter', ++ } ++ role.ensure_state(properties=role_properties) ++ ++ # Search for entries ++ entries = inst.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, "(nsrole=%s)" % role.dn) ++ ++ + if __name__ == "__main__": + CURRENT_FILE = os.path.realpath(__file__) + pytest.main("-s -v %s" % CURRENT_FILE) +diff --git a/ldap/servers/slapd/filter.c b/ldap/servers/slapd/filter.c +index ce09891b8..f541b8fc1 100644 +--- a/ldap/servers/slapd/filter.c ++++ b/ldap/servers/slapd/filter.c +@@ -1038,9 +1038,11 @@ slapi_filter_get_subfilt( + } + + /* +- * Before calling this function, you must free all the parts ++ * The function does not know how to free all the parts + * which will be overwritten (i.e. slapi_free_the_filter_bits), +- * this function dosn't know how to do that ++ * so the caller must take care of that. ++ * But it must do so AFTER calling slapi_filter_replace_ex to ++ * avoid getting invalid filter if slapi_filter_replace_ex fails. + */ + int + slapi_filter_replace_ex(Slapi_Filter *f, char *s) +@@ -1099,8 +1101,15 @@ slapi_filter_free_bits(Slapi_Filter *f) + int + slapi_filter_replace_strfilter(Slapi_Filter *f, char *strfilter) + { +- slapi_filter_free_bits(f); +- return (slapi_filter_replace_ex(f, strfilter)); ++ /* slapi_filter_replace_ex may fail and we cannot ++ * free filter bits before calling it. ++ */ ++ Slapi_Filter save_f = *f; ++ int ret = slapi_filter_replace_ex(f, strfilter); ++ if (ret == 0) { ++ slapi_filter_free_bits(&save_f); ++ } ++ return ret; + } + + static void +-- +2.49.0 + diff --git a/SOURCES/0046-Issue-6686-CLI-Re-enabling-user-accounts-that-reache.patch b/SOURCES/0046-Issue-6686-CLI-Re-enabling-user-accounts-that-reache.patch new file mode 100644 index 0000000..cab0c6c --- /dev/null +++ b/SOURCES/0046-Issue-6686-CLI-Re-enabling-user-accounts-that-reache.patch @@ -0,0 +1,455 @@ +From 446a23d0ed2d3ffa76c5fb5e9576d6876bdbf04f Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Fri, 28 Mar 2025 11:28:54 -0700 +Subject: [PATCH] Issue 6686 - CLI - Re-enabling user accounts that reached + inactivity limit fails with error (#6687) + +Description: When attempting to unlock a user account that has been locked due +to exceeding the Account Policy Plugin's inactivity limit, the dsidm account +unlock command fails with a Python type error: "float() argument must be a +string or a number, not 'NoneType'". + +Enhance the unlock method to properly handle different account locking states, +including inactivity limit exceeded states. +Add test cases to verify account inactivity locking/unlocking functionality +with CoS and role-based indirect locking. + +Fix CoS template class to include the required 'ldapsubentry' objectClass. +Improv error messages to provide better guidance on unlocking indirectly +locked accounts. + +Fixes: https://github.com/389ds/389-ds-base/issues/6686 + +Reviewed by: @mreynolds389 (Thanks!) +--- + .../clu/dsidm_account_inactivity_test.py | 329 ++++++++++++++++++ + src/lib389/lib389/cli_idm/account.py | 25 +- + src/lib389/lib389/idm/account.py | 28 +- + 3 files changed, 377 insertions(+), 5 deletions(-) + create mode 100644 dirsrvtests/tests/suites/clu/dsidm_account_inactivity_test.py + +diff --git a/dirsrvtests/tests/suites/clu/dsidm_account_inactivity_test.py b/dirsrvtests/tests/suites/clu/dsidm_account_inactivity_test.py +new file mode 100644 +index 000000000..88a34abf6 +--- /dev/null ++++ b/dirsrvtests/tests/suites/clu/dsidm_account_inactivity_test.py +@@ -0,0 +1,329 @@ ++# --- BEGIN COPYRIGHT BLOCK --- ++# Copyright (C) 2025 Red Hat, Inc. ++# All rights reserved. ++# ++# License: GPL (version 3 or any later version). ++# See LICENSE for details. ++# --- END COPYRIGHT BLOCK --- ++# ++import ldap ++import time ++import pytest ++import logging ++import os ++from datetime import datetime, timedelta ++ ++from lib389 import DEFAULT_SUFFIX, DN_PLUGIN, DN_CONFIG ++from lib389.cli_idm.account import entry_status, unlock ++from lib389.topologies import topology_st ++from lib389.cli_base import FakeArgs ++from lib389.utils import ds_is_older ++from lib389.plugins import AccountPolicyPlugin, AccountPolicyConfigs ++from lib389.idm.role import FilteredRoles ++from lib389.idm.user import UserAccounts ++from lib389.cos import CosTemplate, CosPointerDefinition ++from lib389.idm.domain import Domain ++from . import check_value_in_log_and_reset ++ ++pytestmark = pytest.mark.tier0 ++ ++logging.getLogger(__name__).setLevel(logging.DEBUG) ++log = logging.getLogger(__name__) ++ ++# Constants ++PLUGIN_ACCT_POLICY = "Account Policy Plugin" ++ACCP_DN = f"cn={PLUGIN_ACCT_POLICY},{DN_PLUGIN}" ++ACCP_CONF = f"{DN_CONFIG},{ACCP_DN}" ++POLICY_NAME = "Account Inactivity Policy" ++POLICY_DN = f"cn={POLICY_NAME},{DEFAULT_SUFFIX}" ++COS_TEMPLATE_NAME = "TemplateCoS" ++COS_TEMPLATE_DN = f"cn={COS_TEMPLATE_NAME},{DEFAULT_SUFFIX}" ++COS_DEFINITION_NAME = "DefinitionCoS" ++COS_DEFINITION_DN = f"cn={COS_DEFINITION_NAME},{DEFAULT_SUFFIX}" ++TEST_USER_NAME = "test_inactive_user" ++TEST_USER_DN = f"uid={TEST_USER_NAME},{DEFAULT_SUFFIX}" ++TEST_USER_PW = "password" ++INACTIVITY_LIMIT = 30 ++ ++ ++@pytest.fixture(scope="function") ++def account_policy_setup(topology_st, request): ++ """Set up account policy plugin, configuration, and CoS objects""" ++ log.info("Setting up Account Policy Plugin and CoS") ++ ++ # Enable Account Policy Plugin ++ plugin = AccountPolicyPlugin(topology_st.standalone) ++ if not plugin.status(): ++ plugin.enable() ++ plugin.set('nsslapd-pluginarg0', ACCP_CONF) ++ ++ # Configure Account Policy ++ accp_configs = AccountPolicyConfigs(topology_st.standalone) ++ accp_config = accp_configs.ensure_state( ++ properties={ ++ 'cn': 'config', ++ 'alwaysrecordlogin': 'yes', ++ 'stateattrname': 'lastLoginTime', ++ 'altstateattrname': '1.1', ++ 'specattrname': 'acctPolicySubentry', ++ 'limitattrname': 'accountInactivityLimit' ++ } ++ ) ++ ++ # Add ACI for anonymous access if it doesn't exist ++ domain = Domain(topology_st.standalone, DEFAULT_SUFFIX) ++ anon_aci = '(targetattr="*")(version 3.0; acl "Anonymous read access"; allow (read,search,compare) userdn="ldap:///anyone";)' ++ domain.ensure_present('aci', anon_aci) ++ ++ # Restart the server to apply plugin configuration ++ topology_st.standalone.restart() ++ ++ # Create or update account policy entry ++ accp_configs = AccountPolicyConfigs(topology_st.standalone, basedn=DEFAULT_SUFFIX) ++ policy = accp_configs.ensure_state( ++ properties={ ++ 'cn': POLICY_NAME, ++ 'objectClass': ['top', 'ldapsubentry', 'extensibleObject', 'accountpolicy'], ++ 'accountInactivityLimit': str(INACTIVITY_LIMIT) ++ } ++ ) ++ ++ # Create or update CoS template entry ++ cos_template = CosTemplate(topology_st.standalone, dn=COS_TEMPLATE_DN) ++ cos_template.ensure_state( ++ properties={ ++ 'cn': COS_TEMPLATE_NAME, ++ 'objectClass': ['top', 'cosTemplate', 'extensibleObject'], ++ 'acctPolicySubentry': policy.dn ++ } ++ ) ++ ++ # Create or update CoS definition entry ++ cos_def = CosPointerDefinition(topology_st.standalone, dn=COS_DEFINITION_DN) ++ cos_def.ensure_state( ++ properties={ ++ 'cn': COS_DEFINITION_NAME, ++ 'objectClass': ['top', 'ldapsubentry', 'cosSuperDefinition', 'cosPointerDefinition'], ++ 'cosTemplateDn': COS_TEMPLATE_DN, ++ 'cosAttribute': 'acctPolicySubentry default operational-default' ++ } ++ ) ++ ++ # Restart server to ensure CoS is applied ++ topology_st.standalone.restart() ++ ++ def fin(): ++ log.info('Cleaning up Account Policy settings') ++ try: ++ # Delete CoS and policy entries ++ if cos_def.exists(): ++ cos_def.delete() ++ if cos_template.exists(): ++ cos_template.delete() ++ if policy.exists(): ++ policy.delete() ++ ++ # Disable the plugin ++ if plugin.status(): ++ plugin.disable() ++ topology_st.standalone.restart() ++ except Exception as e: ++ log.error(f'Failed to clean up: {e}') ++ ++ request.addfinalizer(fin) ++ ++ return topology_st.standalone ++ ++ ++@pytest.fixture(scope="function") ++def create_test_user(topology_st, account_policy_setup, request): ++ """Create a test user for the inactivity test""" ++ log.info('Creating test user') ++ ++ users = UserAccounts(topology_st.standalone, DEFAULT_SUFFIX) ++ user = users.ensure_state( ++ properties={ ++ 'uid': TEST_USER_NAME, ++ 'cn': TEST_USER_NAME, ++ 'sn': TEST_USER_NAME, ++ 'userPassword': TEST_USER_PW, ++ 'uidNumber': '1000', ++ 'gidNumber': '2000', ++ 'homeDirectory': f'/home/{TEST_USER_NAME}' ++ } ++ ) ++ ++ def fin(): ++ log.info('Deleting test user') ++ if user.exists(): ++ user.delete() ++ ++ request.addfinalizer(fin) ++ return user ++ ++ ++@pytest.mark.skipif(ds_is_older("1.4.2"), reason="Indirect account locking not implemented") ++def test_dsidm_account_inactivity_lock_unlock(topology_st, create_test_user): ++ """Test dsidm account unlock functionality with indirectly locked accounts ++ ++ :id: d7b57083-6111-4dbf-af84-6fca7fc7fb31 ++ :setup: Standalone instance with Account Policy Plugin and CoS configured ++ :steps: ++ 1. Create a test user ++ 2. Bind as the test user to set lastLoginTime ++ 3. Check account status - should be active ++ 4. Set user's lastLoginTime to a time in the past that exceeds inactivity limit ++ 5. Check account status - should be locked due to inactivity ++ 6. Attempt to bind as the user - should fail with constraint violation ++ 7. Unlock the account using dsidm account unlock ++ 8. Verify account status is active again ++ 9. Verify the user can bind again ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Account status shows as activated ++ 4. Success ++ 5. Account status shows as inactivity limit exceeded ++ 6. Bind attempt fails with constraint violation ++ 7. Account unlocked successfully ++ 8. Account status shows as activated ++ 9. User can bind successfully ++ """ ++ standalone = topology_st.standalone ++ user = create_test_user ++ ++ # Set up FakeArgs for dsidm commands ++ args = FakeArgs() ++ args.dn = user.dn ++ args.json = False ++ args.details = False ++ ++ # 1. Check initial account status - should be active ++ log.info('Step 1: Checking initial account status') ++ entry_status(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, check_value='Entry State: activated') ++ ++ # 2. Bind as test user to set initial lastLoginTime ++ log.info('Step 2: Binding as test user to set lastLoginTime') ++ try: ++ conn = user.bind(TEST_USER_PW) ++ conn.unbind() ++ log.info("Successfully bound as test user") ++ except ldap.LDAPError as e: ++ pytest.fail(f"Failed to bind as test user: {e}") ++ ++ # 3. Set lastLoginTime to a time in the past that exceeds inactivity limit ++ log.info('Step 3: Setting lastLoginTime to the past') ++ past_time = datetime.utcnow() - timedelta(seconds=INACTIVITY_LIMIT * 2) ++ past_time_str = past_time.strftime('%Y%m%d%H%M%SZ') ++ user.replace('lastLoginTime', past_time_str) ++ ++ # 4. Check account status - should now be locked due to inactivity ++ log.info('Step 4: Checking account status after setting old lastLoginTime') ++ entry_status(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, check_value='Entry State: inactivity limit exceeded') ++ ++ # 5. Attempt to bind as the user - should fail ++ log.info('Step 5: Attempting to bind as user (should fail)') ++ with pytest.raises(ldap.CONSTRAINT_VIOLATION) as excinfo: ++ conn = user.bind(TEST_USER_PW) ++ assert "Account inactivity limit exceeded" in str(excinfo.value) ++ ++ # 6. Unlock the account using dsidm account unlock ++ log.info('Step 6: Unlocking the account with dsidm') ++ unlock(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, ++ check_value='now unlocked by resetting lastLoginTime') ++ ++ # 7. Verify account status is active again ++ log.info('Step 7: Checking account status after unlock') ++ entry_status(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, check_value='Entry State: activated') ++ ++ # 8. Verify the user can bind again ++ log.info('Step 8: Verifying user can bind again') ++ try: ++ conn = user.bind(TEST_USER_PW) ++ conn.unbind() ++ log.info("Successfully bound as test user after unlock") ++ except ldap.LDAPError as e: ++ pytest.fail(f"Failed to bind as test user after unlock: {e}") ++ ++ ++@pytest.mark.skipif(ds_is_older("1.4.2"), reason="Indirect account locking not implemented") ++def test_dsidm_indirectly_locked_via_role(topology_st, create_test_user): ++ """Test dsidm account unlock functionality with accounts indirectly locked via role ++ ++ :id: 7bfe69bb-cf99-4214-a763-051ab2b9cf89 ++ :setup: Standalone instance with Role and user configured ++ :steps: ++ 1. Create a test user ++ 2. Create a Filtered Role that includes the test user ++ 3. Lock the role ++ 4. Check account status - should be indirectly locked through the role ++ 5. Attempt to unlock the account - should fail with appropriate message ++ 6. Unlock the role ++ 7. Verify account status is active again ++ :expectedresults: ++ 1. Success ++ 2. Success ++ 3. Success ++ 4. Account status shows as indirectly locked ++ 5. Unlock attempt fails with appropriate error message ++ 6. Success ++ 7. Account status shows as activated ++ """ ++ standalone = topology_st.standalone ++ user = create_test_user ++ ++ # Use FilteredRoles and ensure_state for role creation ++ log.info('Step 1: Creating Filtered Role') ++ roles = FilteredRoles(standalone, DEFAULT_SUFFIX) ++ role = roles.ensure_state( ++ properties={ ++ 'cn': 'TestFilterRole', ++ 'nsRoleFilter': f'(uid={TEST_USER_NAME})' ++ } ++ ) ++ ++ # Set up FakeArgs for dsidm commands ++ args = FakeArgs() ++ args.dn = user.dn ++ args.json = False ++ args.details = False ++ ++ # 2. Check account status before locking role ++ log.info('Step 2: Checking account status before locking role') ++ entry_status(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, check_value='Entry State: activated') ++ ++ # 3. Lock the role ++ log.info('Step 3: Locking the role') ++ role.lock() ++ ++ # 4. Check account status - should be indirectly locked ++ log.info('Step 4: Checking account status after locking role') ++ entry_status(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, check_value='Entry State: indirectly locked through a Role') ++ ++ # 5. Attempt to unlock the account - should fail ++ log.info('Step 5: Attempting to unlock indirectly locked account') ++ unlock(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, ++ check_value='Account is locked through role') ++ ++ # 6. Unlock the role ++ log.info('Step 6: Unlocking the role') ++ role.unlock() ++ ++ # 7. Verify account status is active again ++ log.info('Step 7: Checking account status after unlocking role') ++ entry_status(standalone, DEFAULT_SUFFIX, topology_st.logcap.log, args) ++ check_value_in_log_and_reset(topology_st, check_value='Entry State: activated') ++ ++ ++if __name__ == '__main__': ++ # Run isolated ++ # -s for DEBUG mode ++ CURRENT_FILE = os.path.realpath(__file__) ++ pytest.main(["-s", CURRENT_FILE]) +\ No newline at end of file +diff --git a/src/lib389/lib389/cli_idm/account.py b/src/lib389/lib389/cli_idm/account.py +index 15f766588..a0dfd8f65 100644 +--- a/src/lib389/lib389/cli_idm/account.py ++++ b/src/lib389/lib389/cli_idm/account.py +@@ -176,8 +176,29 @@ def unlock(inst, basedn, log, args): + dn = _get_dn_arg(args.dn, msg="Enter dn to unlock") + accounts = Accounts(inst, basedn) + acct = accounts.get(dn=dn) +- acct.unlock() +- log.info(f'Entry {dn} is unlocked') ++ ++ try: ++ # Get the account status before attempting to unlock ++ status = acct.status() ++ state = status["state"] ++ ++ # Attempt to unlock the account ++ acct.unlock() ++ ++ # Success message ++ log.info(f'Entry {dn} is unlocked') ++ if state == AccountState.DIRECTLY_LOCKED: ++ log.info(f'The entry was directly locked') ++ elif state == AccountState.INACTIVITY_LIMIT_EXCEEDED: ++ log.info(f'The entry was locked due to inactivity and is now unlocked by resetting lastLoginTime') ++ ++ except ValueError as e: ++ # Provide a more detailed error message based on failure reason ++ if "through role" in str(e): ++ log.error(f"Cannot unlock {dn}: {str(e)}") ++ log.info("To unlock this account, you must modify the role that's locking it.") ++ else: ++ log.error(f"Failed to unlock {dn}: {str(e)}") + + + def reset_password(inst, basedn, log, args): +diff --git a/src/lib389/lib389/idm/account.py b/src/lib389/lib389/idm/account.py +index 4b823b662..faf6f6f16 100644 +--- a/src/lib389/lib389/idm/account.py ++++ b/src/lib389/lib389/idm/account.py +@@ -140,7 +140,8 @@ class Account(DSLdapObject): + "nsAccountLock", state_attr]) + + last_login_time = self._dict_get_with_ignore_indexerror(account_data, state_attr) +- if not last_login_time: ++ # if last_login_time not exist then check alt_state_attr only if its not disabled and exist ++ if not last_login_time and alt_state_attr in account_data: + last_login_time = self._dict_get_with_ignore_indexerror(account_data, alt_state_attr) + + create_time = self._dict_get_with_ignore_indexerror(account_data, "createTimestamp") +@@ -203,12 +204,33 @@ class Account(DSLdapObject): + self.replace('nsAccountLock', 'true') + + def unlock(self): +- """Unset nsAccountLock""" ++ """Unset nsAccountLock if it's set and reset lastLoginTime if account is locked due to inactivity""" + + current_status = self.status() ++ + if current_status["state"] == AccountState.ACTIVATED: + raise ValueError("Account is already active") +- self.remove('nsAccountLock', None) ++ ++ if current_status["state"] == AccountState.DIRECTLY_LOCKED: ++ # Account is directly locked with nsAccountLock attribute ++ self.remove('nsAccountLock', None) ++ elif current_status["state"] == AccountState.INACTIVITY_LIMIT_EXCEEDED: ++ # Account is locked due to inactivity - reset lastLoginTime to current time ++ # The lastLoginTime attribute stores its value in GMT/UTC time (Zulu time zone) ++ current_time = time.strftime('%Y%m%d%H%M%SZ', time.gmtime()) ++ self.replace('lastLoginTime', current_time) ++ elif current_status["state"] == AccountState.INDIRECTLY_LOCKED: ++ # Account is locked through a role ++ role_dn = current_status.get("role_dn") ++ if role_dn: ++ raise ValueError(f"Account is locked through role {role_dn}. " ++ f"Please modify the role to unlock this account.") ++ else: ++ raise ValueError("Account is locked through an unknown role. " ++ "Please check the roles configuration to unlock this account.") ++ else: ++ # Should not happen, but just in case ++ raise ValueError(f"Unknown lock state: {current_status['state'].value}") + + # If the account can be bound to, this will attempt to do so. We don't check + # for exceptions, just pass them back! +-- +2.49.0 + diff --git a/SOURCES/0047-Issue-6302-Allow-to-run-replication-status-without-a.patch b/SOURCES/0047-Issue-6302-Allow-to-run-replication-status-without-a.patch new file mode 100644 index 0000000..37085d0 --- /dev/null +++ b/SOURCES/0047-Issue-6302-Allow-to-run-replication-status-without-a.patch @@ -0,0 +1,70 @@ +From 09a284ee43c2b4346da892f8756f97accd15ca68 Mon Sep 17 00:00:00 2001 +From: Simon Pichugin +Date: Wed, 4 Dec 2024 21:59:40 -0500 +Subject: [PATCH] Issue 6302 - Allow to run replication status without a prompt + (#6410) + +Description: We should allow running replication status and +other similar commands without requesting a password and bind DN. + +This way, the current instance's root DN and root PW will be used on other +instances when requesting CSN info. If they are incorrect, +then the info won't be printed, but otherwise, the agreement status +will be displayed correctly. + +Fixes: https://github.com/389ds/389-ds-base/issues/6302 + +Reviewed by: @progier389 (Thanks!) +--- + src/lib389/lib389/cli_conf/replication.py | 15 +++------------ + 1 file changed, 3 insertions(+), 12 deletions(-) + +diff --git a/src/lib389/lib389/cli_conf/replication.py b/src/lib389/lib389/cli_conf/replication.py +index 399d0d2f8..cd4a331a8 100644 +--- a/src/lib389/lib389/cli_conf/replication.py ++++ b/src/lib389/lib389/cli_conf/replication.py +@@ -319,12 +319,9 @@ def list_suffixes(inst, basedn, log, args): + def get_repl_status(inst, basedn, log, args): + replicas = Replicas(inst) + replica = replicas.get(args.suffix) +- pw_and_dn_prompt = False + if args.bind_passwd_file is not None: + args.bind_passwd = get_passwd_from_file(args.bind_passwd_file) +- if args.bind_passwd_prompt or args.bind_dn is None or args.bind_passwd is None: +- pw_and_dn_prompt = True +- status = replica.status(binddn=args.bind_dn, bindpw=args.bind_passwd, pwprompt=pw_and_dn_prompt) ++ status = replica.status(binddn=args.bind_dn, bindpw=args.bind_passwd, pwprompt=args.bind_passwd_prompt) + if args.json: + log.info(json.dumps({"type": "list", "items": status}, indent=4)) + else: +@@ -335,12 +332,9 @@ def get_repl_status(inst, basedn, log, args): + def get_repl_winsync_status(inst, basedn, log, args): + replicas = Replicas(inst) + replica = replicas.get(args.suffix) +- pw_and_dn_prompt = False + if args.bind_passwd_file is not None: + args.bind_passwd = get_passwd_from_file(args.bind_passwd_file) +- if args.bind_passwd_prompt or args.bind_dn is None or args.bind_passwd is None: +- pw_and_dn_prompt = True +- status = replica.status(binddn=args.bind_dn, bindpw=args.bind_passwd, winsync=True, pwprompt=pw_and_dn_prompt) ++ status = replica.status(binddn=args.bind_dn, bindpw=args.bind_passwd, winsync=True, pwprompt=args.bind_passwd_prompt) + if args.json: + log.info(json.dumps({"type": "list", "items": status}, indent=4)) + else: +@@ -874,12 +868,9 @@ def poke_agmt(inst, basedn, log, args): + + def get_agmt_status(inst, basedn, log, args): + agmt = get_agmt(inst, args) +- pw_and_dn_prompt = False + if args.bind_passwd_file is not None: + args.bind_passwd = get_passwd_from_file(args.bind_passwd_file) +- if args.bind_passwd_prompt or args.bind_dn is None or args.bind_passwd is None: +- pw_and_dn_prompt = True +- status = agmt.status(use_json=args.json, binddn=args.bind_dn, bindpw=args.bind_passwd, pwprompt=pw_and_dn_prompt) ++ status = agmt.status(use_json=args.json, binddn=args.bind_dn, bindpw=args.bind_passwd, pwprompt=args.bind_passwd_prompt) + log.info(status) + + +-- +2.49.0 + diff --git a/SPECS/389-ds-base.spec b/SPECS/389-ds-base.spec index 45da920..648a660 100644 --- a/SPECS/389-ds-base.spec +++ b/SPECS/389-ds-base.spec @@ -45,10 +45,14 @@ ExcludeArch: i686 # Filter argparse-manpage from autogenerated package Requires %global __requires_exclude ^python.*argparse-manpage +# Force to require nss version greater or equal as the version available at the build time +# See bz1986327 +%define dirsrv_requires_ge() %(LC_ALL="C" echo '%*' | xargs -r rpm -q --qf 'Requires: %%{name} >= %%{epoch}:%%{version}\\n' | sed -e 's/ (none):/ /' -e 's/ 0:/ /' | grep -v "is not") + Summary: 389 Directory Server (base) Name: 389-ds-base Version: 1.4.3.39 -Release: %{?relprefix}12%{?prerel}%{?dist} +Release: %{?relprefix}13%{?prerel}%{?dist} License: GPL-3.0-or-later WITH GPL-3.0-389-ds-base-exception AND (0BSD OR Apache-2.0 OR MIT) AND (Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT) AND (Apache-2.0 OR BSD-2-Clause OR MIT) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR LGPL-2.1-or-later OR MIT) AND (Apache-2.0 OR MIT OR Zlib) AND (Apache-2.0 OR MIT) AND (MIT OR Apache-2.0) AND Unicode-3.0 AND (MIT OR Unlicense) AND Apache-2.0 AND MIT AND MPL-2.0 AND Zlib URL: https://www.port389.org Group: System Environment/Daemons @@ -163,9 +167,10 @@ Provides: bundled(crate(zeroize_derive)) = 1.4.2 ##### Bundled cargo crates list - END ##### -BuildRequires: nspr-devel >= 4.32 -BuildRequires: nss-devel >= 3.67.0-7 +BuildRequires: nspr-devel +BuildRequires: nss-devel BuildRequires: perl-generators +BuildRequires: openldap-clients BuildRequires: openldap-devel BuildRequires: libdb-devel BuildRequires: cyrus-sasl-devel @@ -247,8 +252,9 @@ Requires: python%{python3_pkgversion}-ldap # this is needed to setup SSL if you are not using the # administration server package Requires: nss-tools -Requires: nspr >= 4.32 +%dirsrv_requires_ge nss Requires: nss >= 3.67.0-7 +Requires: nspr >= 4.32 # these are not found by the auto-dependency method # they are required to support the mandatory LDAP SASL mechs @@ -265,6 +271,7 @@ Requires: cracklib-dicts # 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: perl-Errno >= 1.23-360 +Requires: acl # Needed by logconv.pl Requires: perl-DB_File @@ -323,6 +330,21 @@ Patch30: 0030-Issue-5841-dsconf-incorrectly-setting-up-Pass-Throug.patc Patch31: 0031-Issue-6067-Add-hidden-v-and-j-options-to-each-CLI-su.patch Patch32: 0032-Issue-6067-Improve-dsidm-CLI-No-Such-Entry-handling-.patch Patch33: 0033-Issue-6067-Update-dsidm-to-prioritize-basedn-from-.d.patch +Patch34: 0034-Issue-6155-ldap-agent-fails-to-start-because-of-perm.patch +Patch35: 0035-Issue-5305-OpenLDAP-version-autodetection-doesn-t-wo.patch +Patch36: 0036-Issue-1925-Add-a-CI-test-5936.patch +Patch37: 0037-Issue-6494-2nd-Various-errors-when-using-extended-ma.patch +Patch38: 0038-Issue-6494-3rd-Various-errors-when-using-extended-ma.patch +Patch39: 0039-Issue-6494-4th-Various-errors-when-using-extended-ma.patch +Patch40: 0040-Issue-6497-lib389-Configure-replication-for-multiple.patch +Patch41: 0041-Issue-6655-fix-replication-release-replica-decoding-.patch +Patch42: 0042-Issue-6655-fix-merge-conflict.patch +Patch43: 0043-Issue-6571-Nested-group-does-not-receive-memberOf-at.patch +Patch44: 0044-Issue-6571-2nd-Nested-group-does-not-receive-memberO.patch +Patch45: 0045-Issue-6698-NPE-after-configuring-invalid-filtered-ro.patch +Patch46: 0046-Issue-6686-CLI-Re-enabling-user-accounts-that-reache.patch +Patch47: 0047-Issue-6302-Allow-to-run-replication-status-without-a.patch + Patch100: cargo.patch @@ -338,8 +360,8 @@ Please see http://seclists.org/oss-sec/2016/q1/363 for more information. %package libs Summary: Core libraries for 389 Directory Server Group: System Environment/Daemons -BuildRequires: nspr-devel >= 4.32 -BuildRequires: nss-devel >= 3.67.0-7 +BuildRequires: nspr-devel +BuildRequires: nss-devel BuildRequires: openldap-devel BuildRequires: libdb-devel BuildRequires: cyrus-sasl-devel @@ -392,8 +414,8 @@ Summary: Development libraries for 389 Directory Server Group: Development/Libraries Requires: %{name}-libs = %{version}-%{release} Requires: pkgconfig -Requires: nspr-devel >= 4.32 -Requires: nss-devel >= 3.67.0-7 +Requires: nspr-devel +Requires: nss-devel Requires: openldap-devel Requires: libtalloc Requires: libevent @@ -420,7 +442,7 @@ SNMP Agent for the 389 Directory Server base package. Summary: A library for accessing, testing, and configuring the 389 Directory Server BuildArch: noarch Group: Development/Libraries -Requires: 389-ds-base +Requires: %{name} = %{version}-%{release} Requires: openssl Requires: iproute Requires: platform-python @@ -446,7 +468,8 @@ Summary: Cockpit UI Plugin for configuring and administering the 389 Di BuildArch: noarch Requires: cockpit Requires: platform-python -Requires: python%{python3_pkgversion}-lib389 +Requires: %{name} = %{version}-%{release} +Requires: python%{python3_pkgversion}-lib389 = %{version}-%{release} %description -n cockpit-389-ds A cockpit UI Plugin for configuring and administering the 389 Directory Server @@ -514,7 +537,7 @@ pushd ../%{jemalloc_name}-%{jemalloc_ver} --libdir=%{_libdir}/%{pkgname}/lib \ --bindir=%{_libdir}/%{pkgname}/bin \ --enable-prof -make %{?_smp_mflags} +%make_build popd %endif @@ -548,8 +571,7 @@ sed -i "1s/\"1\"/\"8\"/" %{_builddir}/%{name}-%{version}%{?prerel}/src/lib389/m # Generate symbolic info for debuggers export XCFLAGS=$RPM_OPT_FLAGS -#make %{?_smp_mflags} -make +%make_build %install @@ -571,7 +593,7 @@ popd mkdir -p $RPM_BUILD_ROOT/var/log/%{pkgname} mkdir -p $RPM_BUILD_ROOT/var/lib/%{pkgname} -mkdir -p $RPM_BUILD_ROOT/var/3lock/%{pkgname} +mkdir -p $RPM_BUILD_ROOT/var/lock/%{pkgname} # for systemd mkdir -p $RPM_BUILD_ROOT%{_sysconfdir}/systemd/system/%{groupname}.wants @@ -947,6 +969,12 @@ exit 0 %doc README.md %changelog +* Thu May 15 2025 Viktor Ashirov - 1.4.3.39-13 +- Resolves: RHEL-89749 - Nested group does not receive memberOf attribute [rhel-8.10.z] +- Resolves: RHEL-89758 - dsidm Error: float() argument must be a string or a number, not 'NoneType' [rhel-8.10.z] +- Resolves: RHEL-89765 - Crash in __strlen_sse2 when using the nsRole filter rewriter. [rhel-8.10.z] +- Resolves: RHEL-89778 - RHDS12.2 NSMMReplicationPlugin - release_replica Unable to parse the response [rhel-8.10.z] + * Thu Apr 03 2025 Viktor Ashirov - 1.4.3.39-12 - Resolves: RHEL-85499 - [RFE] defer memberof nested updates [rhel-8.10.z] - Resolves: RHEL-65663 - dsconf incorrectly setting up Pass-Through Authentication