diff --git a/dirsrvtests/tests/suites/ds_tools/replcheck_test.py b/dirsrvtests/tests/suites/ds_tools/replcheck_test.py index a9212bc95c..deecdfc92e 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 f287751e10..a1a6a6f720 100644 --- a/dirsrvtests/tests/suites/replication/acceptance_test.py +++ b/dirsrvtests/tests/suites/replication/acceptance_test.py @@ -324,6 +324,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 b4b74e3396..fe9955e7e9 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 dbace33127..ed138013d1 100644 --- a/dirsrvtests/tests/suites/replication/regression_m2_test.py +++ b/dirsrvtests/tests/suites/replication/regression_m2_test.py @@ -68,7 +68,7 @@ def __init__(self, from_inst, to_inst, cn = None): 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 274de94ac0..846e046453 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/dirsrvtests/tests/suites/tls/tls_repl_clientauth_test.py b/dirsrvtests/tests/suites/tls/tls_repl_clientauth_test.py index 011927f767..9fe7bb8461 100644 --- a/dirsrvtests/tests/suites/tls/tls_repl_clientauth_test.py +++ b/dirsrvtests/tests/suites/tls/tls_repl_clientauth_test.py @@ -47,7 +47,7 @@ def topo_tls_ldapi(topo): # Create replication DNs services = ServiceAccounts(m1, DEFAULT_SUFFIX) for instance in (m1, m2): - repl = services.get(f'{instance.host}:{instance.sslport}') + repl = services.get(f'{DEFAULT_SUFFIX}:{instance.host}:{instance.sslport}') repl.set('nsCertSubjectDN', instance.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 62bd1c1b5d..d378d53aa5 100644 --- a/src/lib389/lib389/_mapped_object.py +++ b/src/lib389/lib389/_mapped_object.py @@ -19,7 +19,7 @@ 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 @@ -295,15 +295,28 @@ def present(self, attr, value=None): _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