diff --git a/0001-ipatests-Move-expire_password-fixture-into-TestIPACo.patch b/0001-ipatests-Move-expire_password-fixture-into-TestIPACo.patch new file mode 100644 index 0000000..53a9baa --- /dev/null +++ b/0001-ipatests-Move-expire_password-fixture-into-TestIPACo.patch @@ -0,0 +1,120 @@ +From 1c86c973c8dc778a71c8e2abf54ff37ececdd696 Mon Sep 17 00:00:00 2001 +From: PRANAV THUBE +Date: Tue, 13 Jan 2026 19:30:09 +0530 +Subject: [PATCH] ipatests: Move expire_password fixture into TestIPACommand + class + +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +--- + .../nightly_ipa-4-13_latest.yaml | 2 +- + .../nightly_ipa-4-13_latest_selinux.yaml | 2 +- + ipatests/test_integration/test_commands.py | 59 +++++++++---------- + 3 files changed, 31 insertions(+), 32 deletions(-) + +diff --git a/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml b/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml +index 210739d4b576882ee43e06d85bce819ff30d2357..aff55727e463207fb235ff340989491e62162149 100644 +--- a/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml ++++ b/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml +@@ -1914,7 +1914,7 @@ jobs: + class: RunPytest + args: + build_url: '{fedora-latest-ipa-4-13/build_url}' +- test_suite: test_integration/test_random_serial_numbers.py::TestIPACommand_RSN::test_certificate_out_write_to_file ++ test_suite: test_integration/test_random_serial_numbers.py::TestIPACommand_RSN + template: *ci-ipa-4-13-latest + timeout: 5400 + topology: *master_1repl_1client +diff --git a/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml b/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml +index 0fb7c050b97bf66645599fbff46b53c048211f96..e6c57ea060b3bb8bfdf8b6f981f8fd28e4a7d320 100644 +--- a/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml ++++ b/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml +@@ -2066,7 +2066,7 @@ jobs: + args: + build_url: '{fedora-latest-ipa-4-13/build_url}' + selinux_enforcing: True +- test_suite: test_integration/test_random_serial_numbers.py::TestIPACommand_RSN::test_certificate_out_write_to_file ++ test_suite: test_integration/test_random_serial_numbers.py::TestIPACommand_RSN + template: *ci-ipa-4-13-latest + timeout: 5400 + topology: *master_1repl_1client +diff --git a/ipatests/test_integration/test_commands.py b/ipatests/test_integration/test_commands.py +index 01001e4c92037599b075e9bca3ddda7d0c8e8ffa..eda98e92b6b494752d74c6381bdcb6d5c47a26d1 100644 +--- a/ipatests/test_integration/test_commands.py ++++ b/ipatests/test_integration/test_commands.py +@@ -199,36 +199,6 @@ duplicatesubject = ( + duplicate_serial = "4097" + + +-@pytest.fixture() +-def expire_password(): +- """ +- Fixture to expire a user's password far into the future past +- 2038, then revert time back. +- """ +- hosts = dict() +- +- def _expire_password(host): +- hosts['host'] = host +- tasks.move_date(host, 'stop', '+20Years') +- host.run_command( +- ['ipactl', 'restart', '--ignore-service-failures'] +- ) +- +- yield _expire_password +- +- host = hosts.pop('host') +- # Prior to uninstall remove all the cert tracking to prevent +- # errors from certmonger trying to check the status of certs +- # that don't matter because we are uninstalling. +- host.run_command(['systemctl', 'stop', 'certmonger']) +- # Important: run_command with a str argument is able to +- # perform shell expansion but run_command with a list of +- # arguments is not +- host.run_command('rm -fv ' + paths.CERTMONGER_REQUESTS_DIR + '*') +- tasks.uninstall_master(host) +- tasks.move_date(host, 'start', '-20Years') +- +- + class TestIPACommand(IntegrationTest): + """ + A lot of commands can be executed against a single IPA installation +@@ -239,6 +209,35 @@ class TestIPACommand(IntegrationTest): + num_replicas = 1 + num_clients = 1 + ++ @pytest.fixture ++ def expire_password(self): ++ """ ++ Fixture to expire a user's password far into the future past ++ 2038, then revert time back. ++ """ ++ hosts = dict() ++ ++ def _expire_password(host): ++ hosts['host'] = host ++ tasks.move_date(host, 'stop', '+20Years') ++ host.run_command( ++ ['ipactl', 'restart', '--ignore-service-failures'] ++ ) ++ ++ yield _expire_password ++ ++ host = hosts.pop('host') ++ # Prior to uninstall remove all the cert tracking to prevent ++ # errors from certmonger trying to check the status of certs ++ # that don't matter because we are uninstalling. ++ host.run_command(['systemctl', 'stop', 'certmonger']) ++ # Important: run_command with a str argument is able to ++ # perform shell expansion but run_command with a list of ++ # arguments is not ++ host.run_command('rm -fv ' + paths.CERTMONGER_REQUESTS_DIR + '*') ++ tasks.uninstall_master(host) ++ tasks.move_date(host, 'start', '-20Years') ++ + @pytest.fixture + def pwpolicy_global(self): + """Fixture to change global password history policy and reset it""" +-- +2.52.0 + diff --git a/0002-ipatests-Fix-xfail-assertion-for-sssd-2.12.0.patch b/0002-ipatests-Fix-xfail-assertion-for-sssd-2.12.0.patch new file mode 100644 index 0000000..b564201 --- /dev/null +++ b/0002-ipatests-Fix-xfail-assertion-for-sssd-2.12.0.patch @@ -0,0 +1,48 @@ +From ecc0efa96e3c446586425d470657de7f2d5376bf Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Mon, 19 Jan 2026 17:00:41 +0100 +Subject: [PATCH] ipatests: Fix xfail assertion for sssd 2.12.0 + +SSSD 2.12.0 provides fixes for +https://github.com/SSSD/sssd/issues/5989 +https://github.com/SSSD/sssd/issues/7169 + +Update the condition expecting test failures in test_trust.py + +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: David Hanina +--- + ipatests/test_integration/test_trust.py | 10 ++++++---- + 1 file changed, 6 insertions(+), 4 deletions(-) + +diff --git a/ipatests/test_integration/test_trust.py b/ipatests/test_integration/test_trust.py +index 13ad0afa4c1fb032d50f40cf7cb9b79283203225..0cab277c910a6d35f35b57e3068ee6f38706af59 100644 +--- a/ipatests/test_integration/test_trust.py ++++ b/ipatests/test_integration/test_trust.py +@@ -1219,9 +1219,10 @@ class TestNonPosixAutoPrivateGroup(BaseTestTrust): + assert (uid == self.uid_override and gid == self.gid_override) + test_group = self.clients[0].run_command( + ["id", nonposixuser]).stdout_text +- cond2 = ((type == 'false' +- and sssd_version >= tasks.parse_version("2.9.4")) +- or type == 'hybrid') ++ cond2 = (((type == 'false' ++ and sssd_version >= tasks.parse_version("2.9.4")) ++ or type == 'hybrid') ++ and sssd_version < tasks.parse_version("2.12.0")) + with xfail_context(cond2, + 'https://github.com/SSSD/sssd/issues/5989 ' + 'and 7169'): +@@ -1347,7 +1348,8 @@ class TestPosixAutoPrivateGroup(BaseTestTrust): + and gid == self.gid_override) + result = self.clients[0].run_command(['id', posixuser]) + sssd_version = tasks.get_sssd_version(self.clients[0]) +- bad_version = sssd_version >= tasks.parse_version("2.9.4") ++ bad_version = (tasks.parse_version("2.9.4") <= sssd_version ++ < tasks.parse_version("2.12.0")) + with xfail_context(bad_version and type in ('false', 'hybrid'), + "https://github.com/SSSD/sssd/issues/7169"): + assert "10047(testgroup@{0})".format( +-- +2.52.0 + diff --git a/0003-ipatests-Add-DNS-functional-integration-tests.patch b/0003-ipatests-Add-DNS-functional-integration-tests.patch new file mode 100644 index 0000000..10085a7 --- /dev/null +++ b/0003-ipatests-Add-DNS-functional-integration-tests.patch @@ -0,0 +1,1279 @@ +From aa90ce09c3f6d496ee2cf63123c619f1008140b4 Mon Sep 17 00:00:00 2001 +From: PRANAV THUBE +Date: Wed, 31 Dec 2025 12:15:51 +0530 +Subject: [PATCH] ipatests: Add DNS functional integration tests. + +Add tests covering DNS zone management and all record types: +A, AAAA, AFSDB, CNAME, TXT, SRV, MX, PTR, NAPTR, DNAME, CERT, LOC, KX +Including zone permissions and negative test cases. + +Related: https://pagure.io/freeipa/issue/9911 +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Rafael Guterres Jeffman +Reviewed-By: David Hanina +--- + ipatests/pytest_ipa/integration/tasks.py | 126 +++- + ipatests/test_integration/test_dns.py | 707 +++++++++++++++++++++++ + ipatests/test_xmlrpc/test_dns_plugin.py | 288 ++++++++- + 3 files changed, 1113 insertions(+), 8 deletions(-) + +diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py +index 7def8fe3962487f28999cb975aff512cd36331fe..32ac5cbc2c6fe87850dfb15c1d5beae6fa648dfb 100755 +--- a/ipatests/pytest_ipa/integration/tasks.py ++++ b/ipatests/pytest_ipa/integration/tasks.py +@@ -1628,6 +1628,71 @@ def add_a_record(master, host): + '--a-rec', host.ip]) + + ++def add_dns_record(master, zone, name, record_type, record_value, ++ raiseonerr=True): ++ """Add DNS record of any type. ++ ++ :param master: The IPA master host to run command on ++ :param zone: DNS zone name (e.g., 'example.com.') ++ :param name: Record name (e.g., 'www' or '@' for zone apex) ++ :param record_type: Record type like 'a', 'aaaa', 'afsdb', 'cname', ++ 'txt', 'srv', 'mx', 'ptr', 'naptr', 'dname', ++ 'cert', 'loc', 'kx', etc. ++ :param record_value: List of values for the record ++ :param raiseonerr: If True, raise exception on command failure ++ :return: Command result object ++ """ ++ command = ['ipa', 'dnsrecord-add', zone, name] ++ opt = f'--{record_type}-rec' ++ for val in record_value: ++ command.extend([opt, val]) ++ return master.run_command(command, raiseonerr=raiseonerr) ++ ++ ++def del_dns_record(master, zone, name, record_type=None, record_value=None, ++ del_all=False, raiseonerr=True): ++ """Delete DNS record of any type. ++ ++ :param record_type: Record type like 'a', 'aaaa', 'afsdb', 'cname', etc. ++ :param record_value: List of values (optional) ++ :param del_all: If True, delete all records for this name ++ """ ++ command = ['ipa', 'dnsrecord-del', zone, name] ++ if del_all: ++ command.append('--del-all') ++ elif record_type and record_value: ++ opt = f'--{record_type}-rec' ++ for val in record_value: ++ command.extend([opt, val]) ++ return master.run_command(command, raiseonerr=raiseonerr) ++ ++ ++def find_dns_record(master, zone, name=None, raiseonerr=True): ++ """Find DNS record. ++ ++ :param name: Record name, if not provided searches all records in zone ++ """ ++ command = ['ipa', 'dnsrecord-find', zone] ++ if name is not None: ++ command.append(name) ++ return master.run_command(command, raiseonerr=raiseonerr) ++ ++ ++def mod_dns_record(master, zone, name, record_type, old_value, new_value, ++ raiseonerr=True): ++ """Modify DNS record value. ++ ++ :param record_type: Record type like 'a', 'aaaa', 'txt', etc. ++ :param old_value: Current record value ++ :param new_value: New record value ++ """ ++ return master.run_command([ ++ 'ipa', 'dnsrecord-mod', zone, name, ++ f'--{record_type}-rec={old_value}', ++ f'--{record_type}-data={new_value}' ++ ], raiseonerr=raiseonerr) ++ ++ + def resolve_record(nameserver, query, rtype="SOA", retry=True, timeout=100): + """Resolve DNS record + :retry: if resolution failed try again until timeout is reached +@@ -2002,7 +2067,9 @@ def ldappasswd_sysaccount_change(user, oldpw, newpw, master, use_dirman=False): + + + def add_dns_zone(master, zone, skip_overlap_check=False, +- dynamic_update=False, add_a_record_hosts=None): ++ dynamic_update=False, add_a_record_hosts=None, ++ admin_email=None, refresh=None, retry=None, ++ expire=None, minimum=None, ttl=None, raiseonerr=True): + """ + Add DNS zone if it is not already added. + """ +@@ -2010,14 +2077,27 @@ def add_dns_zone(master, zone, skip_overlap_check=False, + result = master.run_command( + ['ipa', 'dnszone-show', zone], raiseonerr=False) + ++ # Verify both return code and zone name before adding + if result.returncode != 0: + command = ['ipa', 'dnszone-add', zone] + if skip_overlap_check: + command.append('--skip-overlap-check') + if dynamic_update: + command.append('--dynamic-update=True') +- +- master.run_command(command) ++ if admin_email: ++ command.append('--admin-email=' + admin_email) ++ if refresh: ++ command.append('--refresh=' + str(refresh)) ++ if retry: ++ command.append('--retry=' + str(retry)) ++ if expire: ++ command.append('--expire=' + str(expire)) ++ if minimum: ++ command.append('--minimum=' + str(minimum)) ++ if ttl: ++ command.append('--ttl=' + str(ttl)) ++ ++ master.run_command(command, raiseonerr=raiseonerr) + + if add_a_record_hosts: + for host in add_a_record_hosts: +@@ -2027,6 +2107,46 @@ def add_dns_zone(master, zone, skip_overlap_check=False, + logger.debug('Zone %s already added.', zone) + + ++def del_dns_zone(host, zone, raiseonerr=False): ++ """Delete DNS zone.""" ++ return host.run_command( ++ ['ipa', 'dnszone-del', zone], raiseonerr=raiseonerr) ++ ++ ++def find_dns_zone(host, zone, all_attrs=False, raiseonerr=True): ++ """Find DNS zone.""" ++ command = ['ipa', 'dnszone-find', zone] ++ if all_attrs: ++ command.append('--all') ++ return host.run_command(command, raiseonerr=raiseonerr) ++ ++ ++def show_dns_zone(host, zone, all_attrs=False, raiseonerr=True): ++ """Show DNS zone.""" ++ command = ['ipa', 'dnszone-show', zone] ++ if all_attrs: ++ command.append('--all') ++ return host.run_command(command, raiseonerr=raiseonerr) ++ ++ ++def add_dns_zone_permission(host, zone, raiseonerr=True): ++ """Add permission to manage DNS zone.""" ++ return host.run_command(['ipa', 'dnszone-add-permission', zone], ++ raiseonerr=raiseonerr) ++ ++ ++def remove_dns_zone_permission(host, zone, raiseonerr=True): ++ """Remove permission to manage DNS zone.""" ++ return host.run_command(['ipa', 'dnszone-remove-permission', zone], ++ raiseonerr=raiseonerr) ++ ++ ++def find_permission(host, permission, raiseonerr=True): ++ """Find permission.""" ++ return host.run_command(['ipa', 'permission-find', permission], ++ raiseonerr=raiseonerr) ++ ++ + def sign_ca_and_transport(host, csr_name, root_ca_name, ipa_ca_name, + root_ca_path_length=None, ipa_ca_path_length=1, + key_size=None, root_ca_extensions=()): +diff --git a/ipatests/test_integration/test_dns.py b/ipatests/test_integration/test_dns.py +index 1cef22525b690050aa1230733f115cd7f4099c53..905227497c8879bfe4cf8b027e396689c0451208 100644 +--- a/ipatests/test_integration/test_dns.py ++++ b/ipatests/test_integration/test_dns.py +@@ -5,9 +5,70 @@ + + from __future__ import absolute_import + ++import time ++ ++import dns.resolver ++from ipapython.dnsutil import DNSResolver + from ipatests.pytest_ipa.integration import tasks + from ipatests.test_integration.base import IntegrationTest + ++# ============================================================================= ++# DNS Test Constants ++# ============================================================================= ++# Test zone configuration ++ZONE = "testzone" ++EMAIL = "ipaqar.redhat.com" ++REFRESH = 303 ++RETRY = 101 ++EXPIRE = 1202 ++MINIMUM = 33 ++TTL = 55 ++ ++# A record values ++A_RECORD = "1.2.3.4" ++MULTI_A_RECORD1 = "1.2.3.4" ++MULTI_A_RECORD2 = "2.3.4.5" ++ ++# AAAA record values ++AAAA = "fec0:0:a10:6000:10:16ff:fe98:193" ++AAAA_BAD1 = "bada:aaaa:real:ly:bad:dude:extr:a" ++AAAA_BAD2 = "aaaa:bbbb:cccc:dddd:eeee:fffff" ++ ++# Other record types ++AFSDB = "green.femto.edu." ++CNAME = "m.l.k." ++TXT = "none=1.2.3.4" ++SRV_A = "0 100 389" ++SRV = "why.go.here.com." ++NAPTR = '100 10 U E2U+msg !^.*$!mailto:info@example.com! .' ++NAPTR_FIND = "info@example.com" ++DNAME = f"bar.{ZONE}." ++DNAME2 = f"bar_underscore.{ZONE}." ++CERT_B = "1 1 1" ++CERT = "F835EDA21E94B565716F" ++LOC = "37 23 30.900 N 121 59 19.000 W 7.00m 100.00m 100.00m 2.00m" ++ ++# KX records ++KX_PREF1 = "1234" ++KX_BAD_PREF1 = "-1" ++KX_BAD_PREF2 = "123345678" ++ ++# PTR zone configuration ++PTR_OCTET = "4.4.4" ++PTR_ZONE = f"{PTR_OCTET}.in-addr.arpa." ++PTR = "8" ++PTR_VALUE = "in.awesome.domain." ++PTR_EMAIL = "ipaqar.redhat.com" ++PTR_REFRESH = 393 ++PTR_RETRY = 191 ++PTR_EXPIRE = 1292 ++PTR_MINIMUM = 39 ++PTR_TTL = 59 ++ ++# Persistent search test values ++NEW_TXT = "newip=5.6.7.8" ++NEWER_TXT = "newip=8.7.6.5" ++ + + class TestDNS(IntegrationTest): + """Tests for DNS feature. +@@ -38,3 +99,649 @@ class TestDNS(IntegrationTest): + cmd = self.master.run_command(['dig', '+short', '-t', 'SOA', + self.master.domain.name]) + assert 'fake' not in cmd.stdout_text ++ ++ ++class TestDNSAcceptance(IntegrationTest): ++ """DNS Acceptance tests. ++ ++ This test class covers all the DNS acceptance tests including ++ zone management, record types (A, AAAA, AFSDB, CNAME, TXT, SRV, MX, ++ PTR, NAPTR, DNAME, CERT, LOC, KX), zone permissions, and persistent ++ search functionality. ++ ++ Converted from bash test script t.dns.sh ++ """ ++ topology = 'line' ++ num_replicas = 0 ++ ++ @classmethod ++ def install(cls, mh): ++ super(TestDNSAcceptance, cls).install(mh) ++ # Set domain-dependent values ++ cls.MANAGED_ZONE = f"qa.{cls.master.domain.name}" ++ cls.MANAGED_ZONE1 = f"dev.{cls.master.domain.name}" ++ cls.NONEXISTENT_ZONE = f"nonexistent.{cls.master.domain.name}" ++ cls.ZONE_PSEARCH = f"westford.{cls.master.domain.name}" ++ cls.MX = f"mail.{cls.master.domain.name}" ++ cls.A_HOST = f"1.{cls.master.domain.name}" ++ # Setup DNS resolver for test queries ++ cls.resolver = DNSResolver() ++ cls.resolver.nameservers = [cls.master.ip] ++ cls.resolver.lifetime = 10 ++ ++ @classmethod ++ def uninstall(cls, mh): ++ # Cleanup zones if they exist ++ tasks.kinit_admin(cls.master) ++ for zone in [ZONE, PTR_ZONE, cls.MANAGED_ZONE, ++ cls.MANAGED_ZONE1, cls.ZONE_PSEARCH]: ++ if zone: ++ cls.master.run_command( ++ ['ipa', 'dnszone-del', zone], raiseonerr=False ++ ) ++ super(TestDNSAcceptance, cls).uninstall(mh) ++ ++ # ========================================================================= ++ # DNS Zone Tests ++ # ========================================================================= ++ ++ def test_dns_zone(self): ++ """Test DNS zone creation, verification, and dig queries. ++ ++ This test covers zone management operations including creating zones ++ with valid and invalid parameters, and verifying zone attributes ++ via IPA and dig. ++ """ ++ tasks.kinit_admin(self.master) ++ ++ # Create a new DNS zone with all SOA parameters ++ tasks.add_dns_zone( ++ self.master, ZONE, ++ admin_email=EMAIL, ++ refresh=REFRESH, ++ retry=RETRY, ++ expire=EXPIRE, ++ minimum=MINIMUM, ++ ttl=TTL ++ ) ++ # Verify the new zone was created and is findable ++ result = tasks.find_dns_zone(self.master, ZONE, all_attrs=True) ++ assert ZONE in result.stdout_text ++ ++ # Verify DNS server returns correct SOA attributes using DNS API ++ self.master.run_command(['ipactl', 'restart']) ++ time.sleep(5) ++ soa = self.resolver.resolve(ZONE, 'SOA')[0] ++ assert self.master.hostname in str(soa.mname) ++ assert EMAIL.replace('@', '.') in str(soa.rname) ++ assert soa.refresh == REFRESH ++ assert soa.retry == RETRY ++ assert soa.expire == EXPIRE ++ assert soa.minimum == MINIMUM ++ ++ # ========================================================================= ++ # A Record Tests ++ # ========================================================================= ++ ++ def test_a_record(self): ++ """Test A record add, verify, and delete operations.""" ++ tasks.kinit_admin(self.master) ++ ++ # Single A record: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, ZONE, 'allll', ++ record_type='a', record_value=[A_RECORD]) ++ ans = self.resolver.resolve(f'allll.{ZONE}', 'A') ++ assert A_RECORD in [r.address for r in ans] ++ ++ tasks.del_dns_record(self.master, ZONE, 'allll', ++ record_type='a', record_value=[A_RECORD]) ++ try: ++ self.resolver.resolve(f'allll.{ZONE}', 'A') ++ raise AssertionError( ++ f"Resolving allll.{ZONE} should have raised NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Record deleted, name no longer exists - expected ++ ++ # Multiple A records: add, verify, delete, verify deleted ++ multi_recs = [MULTI_A_RECORD1, MULTI_A_RECORD2] ++ tasks.add_dns_record(self.master, ZONE, 'aa2', ++ record_type='a', record_value=multi_recs) ++ ans = self.resolver.resolve(f'aa2.{ZONE}', 'A') ++ assert MULTI_A_RECORD1 in [r.address for r in ans] ++ assert MULTI_A_RECORD2 in [r.address for r in ans] ++ ++ tasks.del_dns_record(self.master, ZONE, 'aa2', ++ record_type='a', record_value=multi_recs) ++ try: ++ self.resolver.resolve(f'aa2.{ZONE}', 'A') ++ raise AssertionError( ++ f"Resolving aa2.{ZONE} should have raised NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Records deleted, name no longer exists - expected ++ ++ # ========================================================================= ++ # AAAA Record Tests ++ # ========================================================================= ++ ++ def test_aaaa_record(self): ++ """Test AAAA record add, verify, delete, and invalid values.""" ++ tasks.kinit_admin(self.master) ++ ++ # AAAA record: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, ZONE, 'aaaa', ++ record_type='aaaa', record_value=[AAAA]) ++ ans = self.resolver.resolve(f'aaaa.{ZONE}', 'AAAA') ++ assert AAAA in [r.address for r in ans] ++ ++ tasks.del_dns_record(self.master, ZONE, 'aaaa', ++ record_type='aaaa', record_value=[AAAA]) ++ try: ++ self.resolver.resolve(f'aaaa.{ZONE}', 'AAAA') ++ raise AssertionError( ++ f"Resolving aaaa.{ZONE} should have raised NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Record deleted, name no longer exists - expected ++ ++ # Invalid AAAA record should fail and not be created ++ result = tasks.add_dns_record(self.master, ZONE, 'aaaab', ++ record_type='aaaa', ++ record_value=[AAAA_BAD1], ++ raiseonerr=False) ++ assert result.returncode != 0 ++ try: ++ self.resolver.resolve(f'aaaab.{ZONE}', 'AAAA') ++ raise AssertionError( ++ f"Resolving aaaab.{ZONE} should have raised NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Record was never created - expected ++ ++ # ========================================================================= ++ # AFSDB Record Tests ++ # ========================================================================= ++ ++ def test_afsdb_record(self): ++ """Test AFSDB record add, verify, and delete operations.""" ++ tasks.kinit_admin(self.master) ++ ++ # AFSDB record: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, ZONE, 'afsdb', ++ record_type='afsdb', ++ record_value=[f'0 {AFSDB}']) ++ ans = self.resolver.resolve(f'afsdb.{ZONE}', 'AFSDB') ++ assert AFSDB in [str(r.hostname) for r in ans] ++ ++ tasks.del_dns_record(self.master, ZONE, 'afsdb', ++ record_type='afsdb', ++ record_value=[f'0 {AFSDB}']) ++ try: ++ self.resolver.resolve(f'afsdb.{ZONE}', 'AFSDB') ++ raise AssertionError( ++ f"Resolving afsdb.{ZONE} should have raised NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Record deleted, name no longer exists - expected ++ ++ # ========================================================================= ++ # CNAME Record Tests ++ # ========================================================================= ++ ++ def test_cname_record(self): ++ """Test CNAME record add, verify, delete, and duplicate (bz915807).""" ++ tasks.kinit_admin(self.master) ++ ++ # CNAME record: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, ZONE, 'cname', ++ record_type='cname', record_value=[CNAME]) ++ ans = self.resolver.resolve(f'cname.{ZONE}', 'CNAME') ++ assert CNAME in [str(r.target) for r in ans] ++ ++ # Duplicate CNAME should fail and not be created (bz915807) ++ result = tasks.add_dns_record(self.master, ZONE, 'cname', ++ record_type='cname', ++ record_value=['a.b.c'], raiseonerr=False) ++ assert result.returncode != 0 ++ ans = self.resolver.resolve(f'cname.{ZONE}', 'CNAME') ++ assert 'a.b.c' not in [str(r.target) for r in ans] ++ ++ tasks.del_dns_record(self.master, ZONE, 'cname', ++ record_type='cname', record_value=[CNAME]) ++ try: ++ self.resolver.resolve(f'cname.{ZONE}', 'CNAME') ++ raise AssertionError( ++ f"Resolving cname.{ZONE} should have raised NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Record deleted, name no longer exists - expected ++ ++ # ========================================================================= ++ # TXT Record Tests ++ # ========================================================================= ++ ++ def test_txt_record(self): ++ """Test TXT record add, verify, and delete operations.""" ++ tasks.kinit_admin(self.master) ++ ++ # TXT record: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, ZONE, 'txt', ++ record_type='txt', record_value=[TXT]) ++ ans = self.resolver.resolve(f'txt.{ZONE}', 'TXT') ++ assert any(TXT in str(r) for r in ans) ++ ++ tasks.del_dns_record(self.master, ZONE, 'txt', ++ record_type='txt', record_value=[TXT]) ++ try: ++ self.resolver.resolve(f'txt.{ZONE}', 'TXT') ++ raise AssertionError( ++ f"Resolving txt.{ZONE} should have raised NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Record deleted, name no longer exists - expected ++ ++ # ========================================================================= ++ # SRV Record Tests ++ # ========================================================================= ++ ++ def test_srv_record(self): ++ """Test SRV record add, verify, and delete operations.""" ++ tasks.kinit_admin(self.master) ++ ++ # SRV record: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, ZONE, '_srv', ++ record_type='srv', ++ record_value=[f'{SRV_A} {SRV}']) ++ ans = self.resolver.resolve(f'_srv.{ZONE}', 'SRV') ++ assert SRV in [str(r.target) for r in ans] ++ ++ tasks.del_dns_record(self.master, ZONE, '_srv', del_all=True) ++ try: ++ self.resolver.resolve(f'_srv.{ZONE}', 'SRV') ++ raise AssertionError( ++ f"Resolving _srv.{ZONE} should have raised NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Record deleted, name no longer exists - expected ++ ++ # ========================================================================= ++ # MX Record Tests ++ # ========================================================================= ++ ++ def test_mx_record(self): ++ """Test MX record add, verify, and delete operations.""" ++ tasks.kinit_admin(self.master) ++ ++ # MX record: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, ZONE, '@', ++ record_type='mx', ++ record_value=[f'10 {self.MX}.']) ++ ans = self.resolver.resolve(ZONE, 'MX') ++ assert f'{self.MX}.' in [str(r.exchange) for r in ans] ++ ++ tasks.del_dns_record(self.master, ZONE, '@', ++ record_type='mx', ++ record_value=[f'10 {self.MX}.']) ++ try: ++ self.resolver.resolve(ZONE, 'MX') ++ raise AssertionError( ++ f"Resolving MX for {ZONE} should have raised NoAnswer") ++ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): ++ pass # Record deleted or no MX records left - expected ++ ++ # ========================================================================= ++ # PTR Zone Tests ++ # ========================================================================= ++ def test_ptr_zone(self): ++ """Test PTR zone creation and verification. ++ ++ This test covers PTR zone management including creating zone ++ with all SOA parameters, and verifying attributes via IPA and dig. ++ """ ++ tasks.kinit_admin(self.master) ++ # Clean up if zone exists ++ tasks.del_dns_zone(self.master, PTR_ZONE) ++ ++ # Create PTR zone with all SOA parameters ++ tasks.add_dns_zone( ++ self.master, PTR_ZONE, ++ skip_overlap_check=True, ++ admin_email=PTR_EMAIL, ++ refresh=PTR_REFRESH, ++ retry=PTR_RETRY, ++ expire=PTR_EXPIRE, ++ minimum=PTR_MINIMUM, ++ ttl=PTR_TTL ++ ) ++ ++ # Verify PTR zone gets created with the correct attributes ++ result = tasks.find_dns_zone( ++ self.master, PTR_ZONE, all_attrs=True) ++ assert PTR_ZONE in result.stdout_text ++ ++ # Verify PTR zone SOA attributes using DNS resolver ++ soa = self.resolver.resolve(PTR_ZONE, 'SOA')[0] ++ assert self.master.hostname in str(soa.mname) ++ assert PTR_EMAIL.replace('@', '.') in str(soa.rname) ++ assert soa.refresh == PTR_REFRESH ++ assert soa.retry == PTR_RETRY ++ assert soa.expire == PTR_EXPIRE ++ assert soa.minimum == PTR_MINIMUM ++ ++ # ========================================================================= ++ # PTR Record Tests ++ # ========================================================================= ++ ++ def test_ptr_record(self): ++ """Test PTR record add, verify, and delete operations.""" ++ tasks.kinit_admin(self.master) ++ ++ # PTR record: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, PTR_ZONE, PTR, ++ record_type='ptr', record_value=[PTR_VALUE]) ++ ans = self.resolver.resolve(f'{PTR}.{PTR_ZONE}', 'PTR') ++ assert PTR_VALUE in [str(r.target) for r in ans] ++ ++ tasks.del_dns_record(self.master, PTR_ZONE, PTR, ++ record_type='ptr', record_value=[PTR_VALUE]) ++ try: ++ self.resolver.resolve(f'{PTR}.{PTR_ZONE}', 'PTR') ++ raise AssertionError( ++ f"Resolving {PTR}.{PTR_ZONE} should have raised " ++ "NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Record deleted, name no longer exists - expected ++ ++ # ========================================================================= ++ # NAPTR Record Tests ++ # ========================================================================= ++ ++ def test_naptr_record(self): ++ """Test NAPTR record add, verify, and delete operations.""" ++ tasks.kinit_admin(self.master) ++ ++ # NAPTR record: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, ZONE, 'naptr', ++ record_type='naptr', record_value=[NAPTR]) ++ ans = self.resolver.resolve(f'naptr.{ZONE}', 'NAPTR') ++ assert any(NAPTR_FIND in str(r.regexp) for r in ans) ++ ++ tasks.del_dns_record(self.master, ZONE, 'naptr', ++ record_type='naptr', record_value=[NAPTR]) ++ try: ++ self.resolver.resolve(f'naptr.{ZONE}', 'NAPTR') ++ raise AssertionError( ++ f"Resolving naptr.{ZONE} should have raised NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Record deleted, name no longer exists - expected ++ ++ # ========================================================================= ++ # DNAME Record Tests ++ # ========================================================================= ++ ++ def test_dname_record(self): ++ """Test DNAME record add, verify, delete, and underscore (bz915797).""" ++ tasks.kinit_admin(self.master) ++ ++ # DNAME record: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, ZONE, 'dname', ++ record_type='dname', record_value=[DNAME]) ++ ans = self.resolver.resolve(f'dname.{ZONE}', 'DNAME') ++ assert DNAME in [str(r.target) for r in ans] ++ ++ # Duplicate DNAME should fail (bz915797) ++ result = tasks.add_dns_record(self.master, ZONE, 'dname', ++ record_type='dname', ++ record_value=[DNAME2], ++ raiseonerr=False) ++ assert result.returncode != 0 ++ ++ tasks.del_dns_record(self.master, ZONE, 'dname', ++ record_type='dname', record_value=[DNAME]) ++ try: ++ self.resolver.resolve(f'dname.{ZONE}', 'DNAME') ++ raise AssertionError( ++ f"Resolving dname.{ZONE} should have raised NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Record deleted - expected ++ ++ # DNAME with underscore: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, ZONE, 'dname', ++ record_type='dname', record_value=[DNAME2]) ++ ans = self.resolver.resolve(f'dname.{ZONE}', 'DNAME') ++ assert DNAME2 in [str(r.target) for r in ans] ++ ++ tasks.del_dns_record(self.master, ZONE, 'dname', ++ record_type='dname', record_value=[DNAME2]) ++ try: ++ self.resolver.resolve(f'dname.{ZONE}', 'DNAME') ++ raise AssertionError( ++ f"Resolving dname.{ZONE} should have raised NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Record deleted - expected ++ ++ # ========================================================================= ++ # CERT Record Tests ++ # ========================================================================= ++ ++ def test_cert_record(self): ++ """Test CERT record add, verify, and delete operations.""" ++ tasks.kinit_admin(self.master) ++ ++ # CERT record: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, ZONE, 'cert', ++ record_type='cert', ++ record_value=[f'{CERT_B} {CERT}']) ++ ans = self.resolver.resolve(f'cert.{ZONE}', 'CERT') ++ assert any(CERT in str(r) for r in ans) ++ ++ tasks.del_dns_record(self.master, ZONE, 'cert', ++ record_type='cert', ++ record_value=[f'{CERT_B} {CERT}']) ++ try: ++ self.resolver.resolve(f'cert.{ZONE}', 'CERT') ++ raise AssertionError( ++ f"Resolving cert.{ZONE} should have raised NXDOMAIN") ++ except dns.resolver.NXDOMAIN: ++ pass # Record deleted - expected ++ ++ # ========================================================================= ++ # LOC Record Tests ++ # ========================================================================= ++ ++ def test_loc_record(self): ++ """Test LOC record add, verify, and delete operations.""" ++ tasks.kinit_admin(self.master) ++ ++ # LOC record: add, verify, delete, verify deleted ++ tasks.add_dns_record(self.master, ZONE, '@', ++ record_type='loc', record_value=[LOC]) ++ ans = self.resolver.resolve(ZONE, 'LOC') ++ assert any(LOC in str(r) for r in ans) ++ ++ tasks.del_dns_record(self.master, ZONE, '@', ++ record_type='loc', record_value=[LOC]) ++ try: ++ self.resolver.resolve(ZONE, 'LOC') ++ raise AssertionError( ++ f"Resolving LOC for {ZONE} should have raised NoAnswer") ++ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): ++ pass # Record deleted - expected ++ ++ # ========================================================================= ++ # KX Record Tests ++ # ========================================================================= ++ ++ def test_kx_record(self): ++ """Test KX record add, verify, and delete operations.""" ++ tasks.kinit_admin(self.master) ++ ++ # KX record: add, verify, delete, verify deleted ++ kx_val = f'{KX_PREF1} {self.A_HOST}' ++ tasks.add_dns_record(self.master, ZONE, '@', ++ record_type='kx', record_value=[kx_val]) ++ ans = self.resolver.resolve(ZONE, 'KX') ++ assert int(KX_PREF1) in [r.preference for r in ans] ++ ++ tasks.del_dns_record(self.master, ZONE, '@', ++ record_type='kx', record_value=[kx_val]) ++ try: ++ self.resolver.resolve(ZONE, 'KX') ++ raise AssertionError( ++ f"Resolving KX for {ZONE} should have raised NoAnswer") ++ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): ++ pass # Record deleted or no KX records left - expected ++ ++ # Invalid KX records should fail and not be created ++ bad_vals = [ ++ (KX_BAD_PREF1, A_RECORD), ++ (KX_BAD_PREF2, ZONE) ++ ] ++ for bad_pref, bad_target in bad_vals: ++ result = tasks.add_dns_record( ++ self.master, ZONE, '@', record_type='kx', ++ record_value=[f'{bad_pref} {bad_target}'], raiseonerr=False) ++ assert result.returncode != 0 ++ try: ++ self.resolver.resolve(ZONE, 'KX') ++ raise AssertionError( ++ f"Resolving KX for {ZONE} should have raised " ++ "NoAnswer") ++ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): ++ pass # No KX records - expected ++ ++ # ========================================================================= ++ # Zone Permission Tests ++ # ========================================================================= ++ ++ def test_zone_permission(self): ++ """Test DNS zone permission add, verify, and remove operations. ++ ++ This test covers zone permission management including adding ++ permission, verifying managedby attribute, removing permission, ++ testing duplicate add, testing non-existent zone, and verifying ++ permission cleanup on zone deletion. ++ """ ++ tasks.kinit_admin(self.master) ++ # Clean up if zone exists ++ tasks.del_dns_zone(self.master, self.MANAGED_ZONE) ++ ++ # Add zone, then permission to manage it ++ tasks.add_dns_zone( ++ self.master, self.MANAGED_ZONE, admin_email=EMAIL) ++ result = tasks.add_dns_zone_permission(self.master, self.MANAGED_ZONE) ++ assert result.returncode == 0 ++ ++ # Verify managedby attribute is set ++ result = tasks.show_dns_zone( ++ self.master, self.MANAGED_ZONE, all_attrs=True) ++ assert 'managedby' in result.stdout_text.lower() ++ ++ # Verify permission is added ++ perm_name = f'Manage DNS zone {self.MANAGED_ZONE}' ++ result = tasks.find_permission(self.master, permission=perm_name) ++ assert result.returncode == 0 ++ ++ # Remove permission to manage zone ++ result = tasks.remove_dns_zone_permission( ++ self.master, self.MANAGED_ZONE) ++ assert result.returncode == 0 ++ ++ # Verify managedby attribute is not available ++ result = tasks.show_dns_zone( ++ self.master, self.MANAGED_ZONE, all_attrs=True) ++ assert 'managedby' not in result.stdout_text.lower() ++ ++ # Verify permission is removed ++ result = tasks.find_permission( ++ self.master, permission=perm_name, raiseonerr=False) ++ assert result.returncode != 0 ++ ++ # Add zone with permission, delete zone, verify permission deleted ++ tasks.del_dns_zone(self.master, self.MANAGED_ZONE1) ++ tasks.add_dns_zone( ++ self.master, self.MANAGED_ZONE1, admin_email=EMAIL) ++ tasks.add_dns_zone_permission(self.master, self.MANAGED_ZONE1) ++ tasks.del_dns_zone(self.master, self.MANAGED_ZONE1) ++ perm_name1 = f'Manage DNS zone {self.MANAGED_ZONE1}' ++ result = tasks.find_permission( ++ self.master, permission=perm_name1, raiseonerr=False) ++ assert result.returncode != 0 ++ ++ # Add duplicate permission to manage zone ++ tasks.add_dns_zone_permission( ++ self.master, self.MANAGED_ZONE, raiseonerr=False) ++ result = tasks.add_dns_zone_permission( ++ self.master, self.MANAGED_ZONE, raiseonerr=False) ++ assert result.returncode != 0 ++ assert 'already exists' in result.stderr_text ++ ++ # Add permission to manage non-existent zone ++ result = tasks.add_dns_zone_permission( ++ self.master, self.NONEXISTENT_ZONE, raiseonerr=False) ++ assert result.returncode != 0 ++ assert 'DNS zone not found' in result.stderr_text ++ ++ # Remove permission to manage zone again (should fail) ++ tasks.remove_dns_zone_permission( ++ self.master, self.MANAGED_ZONE, raiseonerr=False) ++ result = tasks.remove_dns_zone_permission( ++ self.master, self.MANAGED_ZONE, raiseonerr=False) ++ assert result.returncode != 0 ++ assert 'permission not found' in result.stderr_text ++ ++ # Remove permission for non-existent zone ++ result = tasks.remove_dns_zone_permission(self.master, ++ self.NONEXISTENT_ZONE, ++ raiseonerr=False) ++ assert result.returncode != 0 ++ assert 'DNS zone not found' in result.stderr_text ++ ++ # Cleanup zones from zone permission tests ++ tasks.del_dns_zone(self.master, self.MANAGED_ZONE) ++ ++ # ========================================================================= ++ # Persistent Search Tests ++ # ========================================================================= ++ ++ def test_psearch(self): ++ """Test persistent search and zone serial updates.""" ++ tasks.kinit_admin(self.master) ++ ++ # Verify psearch is not used when IPA server is installed ++ result = self.master.run_command([ ++ 'grep', 'psearch yes', '/etc/named.conf' ++ ], raiseonerr=False) ++ assert result.returncode != 0 ++ ++ # Create zone with SOA parameters ++ tasks.add_dns_zone( ++ self.master, self.ZONE_PSEARCH, admin_email=EMAIL, ++ refresh=REFRESH, retry=RETRY, expire=EXPIRE, ++ minimum=MINIMUM, ttl=TTL) ++ ++ # Verify zone SOA exists ++ ans = self.resolver.resolve(self.ZONE_PSEARCH, 'SOA') ++ assert len(ans) > 0 ++ ++ # Add TXT record and verify ++ tasks.add_dns_record(self.master, self.ZONE_PSEARCH, 'txt', ++ record_type='txt', record_value=[TXT]) ++ ans = self.resolver.resolve(f'txt.{self.ZONE_PSEARCH}', 'TXT') ++ assert any(TXT in str(r) for r in ans) ++ ++ # Update TXT record and verify ++ tasks.mod_dns_record(self.master, self.ZONE_PSEARCH, 'txt', ++ record_type='txt', old_value=TXT, ++ new_value=NEW_TXT) ++ ans = self.resolver.resolve(f'txt.{self.ZONE_PSEARCH}', 'TXT') ++ assert any(NEW_TXT in str(r) for r in ans) ++ ++ # Get old serial ++ ans = self.resolver.resolve(self.ZONE_PSEARCH, 'SOA') ++ old_serial = ans[0].serial ++ ++ # Update TXT record again ++ tasks.mod_dns_record(self.master, self.ZONE_PSEARCH, 'txt', ++ record_type='txt', old_value=NEW_TXT, ++ new_value=NEWER_TXT) ++ ++ # Verify serial increased ++ ans = self.resolver.resolve(self.ZONE_PSEARCH, 'SOA') ++ new_serial = ans[0].serial ++ assert new_serial > old_serial, ( ++ f"New serial ({new_serial}) should be higher " ++ f"than old ({old_serial})") +diff --git a/ipatests/test_xmlrpc/test_dns_plugin.py b/ipatests/test_xmlrpc/test_dns_plugin.py +index 864d5287f8317a5154aec4c792f56deab7ff0120..bff4b40aef6e5adec21c8929719e99669b80cdf0 100644 +--- a/ipatests/test_xmlrpc/test_dns_plugin.py ++++ b/ipatests/test_xmlrpc/test_dns_plugin.py +@@ -1088,7 +1088,7 @@ class test_dns(Declarative): + + + dict( +- desc='Create record %r in zone %r' % (zone1, name1), ++ desc='Create single A record %r in zone %r' % (name1, zone1), + command=('dnsrecord_add', [zone1, name1], {'arecord': arec2}), + expected={ + 'value': name1_dnsname, +@@ -1132,8 +1132,20 @@ class test_dns(Declarative): + + + dict( +- desc='Add A record to %r in zone %r' % (name1, zone1), +- command=('dnsrecord_add', [zone1, name1], {'arecord': arec3}), ++ desc='Delete single A record from %r in zone %r' % (name1, zone1), ++ command=('dnsrecord_del', [zone1, name1], {'arecord': arec2}), ++ expected={ ++ 'value': [name1_dnsname], ++ 'summary': u'Deleted record "%s"' % name1, ++ 'result': {'failed': []}, ++ }, ++ ), ++ ++ ++ dict( ++ desc='Add multiple A records to %r in zone %r' % (name1, zone1), ++ command=('dnsrecord_add', [zone1, name1], ++ {'arecord': [arec2, arec3]}), + expected={ + 'value': name1_dnsname, + 'summary': None, +@@ -1148,14 +1160,29 @@ class test_dns(Declarative): + + + dict( +- desc='Remove A record from %r in zone %r' % (name1, zone1), +- command=('dnsrecord_del', [zone1, name1], {'arecord': arec2}), ++ desc='Delete multiple A records from %r in zone %r' % ( ++ name1, zone1), ++ command=('dnsrecord_del', [zone1, name1], ++ {'arecord': [arec2, arec3]}), + expected={ + 'value': [name1_dnsname], ++ 'summary': u'Deleted record "%s"' % name1, ++ 'result': {'failed': []}, ++ }, ++ ), ++ ++ ++ dict( ++ desc='Re-add A record %r for subsequent tests' % arec3, ++ command=('dnsrecord_add', [zone1, name1], {'arecord': arec3}), ++ expected={ ++ 'value': name1_dnsname, + 'summary': None, + 'result': { ++ 'dn': name1_dn, + 'idnsname': [name1_dnsname], + 'arecord': [arec3], ++ 'objectclass': objectclasses.dnsrecord, + }, + }, + ), +@@ -1228,6 +1255,50 @@ class test_dns(Declarative): + }, + ), + ++ ++ dict( ++ desc='Try to add invalid AAAA record to %r in zone %r' % ( ++ name1, zone1), ++ command=('dnsrecord_add', [zone1, name1], ++ {'aaaarecord': u'invalid:ipv6:addr'}), ++ expected=errors.ValidationError( ++ name='ip_address', ++ error=u'invalid IP address format'), ++ ), ++ ++ ++ dict( ++ desc='Add AAAA record to %r in zone %r using dnsrecord_add' % ( ++ name1, zone1), ++ command=('dnsrecord_add', [zone1, name1], {'aaaarecord': aaaarec1}), ++ expected={ ++ 'value': name1_dnsname, ++ 'summary': None, ++ 'result': { ++ 'dn': name1_dn, ++ 'idnsname': [name1_dnsname], ++ 'arecord': [arec3], ++ 'aaaarecord': [aaaarec1], ++ 'objectclass': objectclasses.dnsrecord, ++ }, ++ }, ++ ), ++ ++ ++ dict( ++ desc='Delete AAAA record from %r in zone %r using dnsrecord_del' % ( ++ name1, zone1), ++ command=('dnsrecord_del', [zone1, name1], {'aaaarecord': aaaarec1}), ++ expected={ ++ 'value': [name1_dnsname], ++ 'summary': None, ++ 'result': { ++ 'idnsname': [name1_dnsname], ++ 'arecord': [arec3], ++ }, ++ }, ++ ), ++ + dict( + desc='Try to add invalid MX record to zone %r using dnsrecord_add' % (zone1), + command=('dnsrecord_add', [zone1, u'@'], {'mxrecord': zone1_ns }), +@@ -1350,6 +1421,37 @@ class test_dns(Declarative): + }, + ), + ++ ++ dict( ++ desc='Add NAPTR record to zone %r using dnsrecord_add' % (zone1), ++ command=('dnsrecord_add', [zone1, u'_naptr'], ++ {'naptrrecord': u'100 10 "U" "E2U+sip" "" _sip._udp'}), ++ expected={ ++ 'value': DNSName(u'_naptr'), ++ 'summary': None, ++ 'result': { ++ 'objectclass': objectclasses.dnsrecord, ++ 'dn': DN(('idnsname', '_naptr'), zone1_dn), ++ 'idnsname': [DNSName(u'_naptr')], ++ 'naptrrecord': [u'100 10 "U" "E2U+sip" "" _sip._udp'], ++ }, ++ }, ++ ), ++ ++ ++ dict( ++ desc='Delete NAPTR record from zone %r using dnsrecord_del' % ( ++ zone1), ++ command=('dnsrecord_del', [zone1, u'_naptr'], ++ {'naptrrecord': u'100 10 "U" "E2U+sip" "" _sip._udp'}), ++ expected={ ++ 'value': [DNSName(u'_naptr')], ++ 'summary': u'Deleted record "%s"' % u'_naptr', ++ 'result': {'failed': []}, ++ }, ++ ), ++ ++ + dict( + desc='Try to add CNAME record to %r using dnsrecord_add' % (name1), + command=('dnsrecord_add', [zone1, name1], {'cnamerecord': absnxname}), +@@ -1456,6 +1558,66 @@ class test_dns(Declarative): + '(RFC 2181, section 6.1)'), + ), + ++ ++ dict( ++ desc='Add DNAME record with underscore to zone %r' % (zone1), ++ command=('dnsrecord_add', [zone1, u'bar_underscore'], ++ {'dnamerecord': absnxname}), ++ expected={ ++ 'value': DNSName(u'bar_underscore'), ++ 'summary': None, ++ 'result': { ++ 'objectclass': objectclasses.dnsrecord, ++ 'dn': DN(('idnsname', 'bar_underscore'), zone1_dn), ++ 'idnsname': [DNSName(u'bar_underscore')], ++ 'dnamerecord': [absnxname], ++ }, ++ }, ++ ), ++ ++ ++ dict( ++ desc='Delete DNAME record with underscore from zone %r' % (zone1), ++ command=('dnsrecord_del', [zone1, u'bar_underscore'], ++ {'dnamerecord': absnxname}), ++ expected={ ++ 'value': [DNSName(u'bar_underscore')], ++ 'summary': u'Deleted record "%s"' % u'bar_underscore', ++ 'result': {'failed': []}, ++ }, ++ ), ++ ++ ++ dict( ++ desc='Add CERT record to zone %r using dnsrecord_add' % (zone1), ++ command=('dnsrecord_add', [zone1, u'_cert'], ++ {'certrecord': u'1 1 1 F835EDA21E94B565716F'}), ++ expected={ ++ 'value': DNSName(u'_cert'), ++ 'summary': None, ++ 'result': { ++ 'objectclass': objectclasses.dnsrecord, ++ 'dn': DN(('idnsname', '_cert'), zone1_dn), ++ 'idnsname': [DNSName(u'_cert')], ++ 'certrecord': [u'1 1 1 F835EDA21E94B565716F'], ++ }, ++ }, ++ ), ++ ++ ++ dict( ++ desc='Delete CERT record from zone %r using dnsrecord_del' % ( ++ zone1), ++ command=('dnsrecord_del', [zone1, u'_cert'], ++ {'certrecord': u'1 1 1 F835EDA21E94B565716F'}), ++ expected={ ++ 'value': [DNSName(u'_cert')], ++ 'summary': u'Deleted record "%s"' % u'_cert', ++ 'result': {'failed': []}, ++ }, ++ ), ++ ++ + dict( + desc='Add NS+DNAME record to %r zone record using dnsrecord_add' % (zone2), + command=('dnsrecord_add', [zone2, u'@'], +@@ -1527,6 +1689,22 @@ class test_dns(Declarative): + ), + + ++ dict( ++ desc='Delete TXT record from %r using dnsrecord_del' % (name1), ++ command=('dnsrecord_del', [zone1, name1], ++ {'txtrecord': u'foo bar'}), ++ expected={ ++ 'value': [name1_dnsname], ++ 'summary': None, ++ 'result': { ++ 'idnsname': [name1_dnsname], ++ 'arecord': [arec3], ++ 'kxrecord': [u'1 foo-1'], ++ }, ++ }, ++ ), ++ ++ + dict( + desc='Try to add unresolvable absolute NS record to %r using dnsrecord_add' % (name_ns), + command=( +@@ -1879,6 +2057,38 @@ class test_dns(Declarative): + }, + ), + ++ ++ dict( ++ desc='Delete PTR record %r from %r using dnsrecord_del' % ( ++ revname1, revzone1), ++ command=('dnsrecord_del', [revzone1, revname1], ++ {'ptrrecord': absnxname}), ++ expected={ ++ 'value': [revname1_dnsname], ++ 'summary': u'Deleted record "%s"' % revname1, ++ 'result': {'failed': []}, ++ }, ++ ), ++ ++ ++ dict( ++ desc='Re-add PTR record %r to %r for subsequent tests' % ( ++ revname1, revzone1), ++ command=('dnsrecord_add', [revzone1, revname1], ++ {'ptrrecord': absnxname}), ++ expected={ ++ 'value': revname1_dnsname, ++ 'summary': None, ++ 'result': { ++ 'objectclass': objectclasses.dnsrecord, ++ 'dn': revname1_dn, ++ 'idnsname': [revname1_dnsname], ++ 'ptrrecord': [absnxname], ++ }, ++ }, ++ ), ++ ++ + dict( + desc='Update global DNS settings', + command=('dnsconfig_mod', [], {'idnsforwarders' : [fwd_ip],}), +@@ -3114,6 +3324,19 @@ class test_dns(Declarative): + ), + + ++ dict( ++ desc='Delete AFSDB record from %r in zone %r' % ( ++ dnsafsdbres1, idnzone1), ++ command=('dnsrecord_del', [idnzone1, dnsafsdbres1], ++ {'afsdbrecord': u'0 ' + idnzone1_mname}), ++ expected={ ++ 'value': [dnsafsdbres1_dnsname], ++ 'summary': u'Deleted record "%s"' % dnsafsdbres1, ++ 'result': {'failed': []}, ++ }, ++ ), ++ ++ + dict( + desc='Add A denormalized record in zone %r' % (idnzone1), + command=('dnsrecord_add', [idnzone1, u'gro\xdf'], {'arecord': u'172.16.0.1'}), +@@ -6427,6 +6650,61 @@ class test_dns_soa(Declarative): + u"A/AAAA record" % + zone6_unresolvable_ns_dnsname,), + ), ++ ++ dict( ++ desc='Adding a zone - %r - with invalid SOA refresh value' % zone6, ++ command=( ++ 'dnszone_add', [zone6], { ++ 'idnssoarefresh': 12345678901234, ++ }), ++ expected=errors.ValidationError( ++ name='refresh', ++ error=u'can be at most 2147483647'), ++ ), ++ ++ dict( ++ desc='Adding a zone - %r - with invalid SOA retry value' % zone6, ++ command=( ++ 'dnszone_add', [zone6], { ++ 'idnssoaretry': 12345678901234, ++ }), ++ expected=errors.ValidationError( ++ name='retry', ++ error=u'can be at most 2147483647'), ++ ), ++ ++ dict( ++ desc='Adding a zone - %r - with invalid SOA expire value' % zone6, ++ command=( ++ 'dnszone_add', [zone6], { ++ 'idnssoaexpire': 12345678901234, ++ }), ++ expected=errors.ValidationError( ++ name='expire', ++ error=u'can be at most 2147483647'), ++ ), ++ ++ dict( ++ desc='Adding a zone - %r - with invalid SOA minimum value' % zone6, ++ command=( ++ 'dnszone_add', [zone6], { ++ 'idnssoaminimum': 12345678901234, ++ }), ++ expected=errors.ValidationError( ++ name='minimum', ++ error=u'can be at most 2147483647'), ++ ), ++ ++ dict( ++ desc='Adding a zone - %r - with invalid TTL value' % zone6, ++ command=( ++ 'dnszone_add', [zone6], { ++ 'dnsttl': 12345678901234, ++ }), ++ expected=errors.ValidationError( ++ name='ttl', ++ error=u'can be at most 2147483647'), ++ ), + ] + + +-- +2.52.0 + diff --git a/0004-ipa-advise-smart-card-client-script-does-not-need-kr.patch b/0004-ipa-advise-smart-card-client-script-does-not-need-kr.patch new file mode 100644 index 0000000..06dff82 --- /dev/null +++ b/0004-ipa-advise-smart-card-client-script-does-not-need-kr.patch @@ -0,0 +1,99 @@ +From 5756ed2af940378c16d9d52e083b8c4005d41a13 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Wed, 21 Jan 2026 17:19:18 +0100 +Subject: [PATCH] ipa-advise: smart card client script does not need krb ticket + +The script generated by ipa-advise config-client-for-smart-card-auth +currently requires a kerberos ticket because it calls ipa-certupdate. + +Since IPA 4.9.0 and commit 1a09ce9, ipa-certupdate can be called +without a ticket. Update the script so that it detects if it gets +executed on a client recent enough to skip that requirement. + +Update the test for config-client-for-smart-card-auth, do not +call kinit admin on the client. + +Fixes: https://pagure.io/freeipa/issue/9923 +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +--- + ipaserver/advise/plugins/smart_card_auth.py | 22 ++++++++++++++++++++- + ipatests/test_integration/test_advise.py | 10 +++++++--- + 2 files changed, 28 insertions(+), 4 deletions(-) + +diff --git a/ipaserver/advise/plugins/smart_card_auth.py b/ipaserver/advise/plugins/smart_card_auth.py +index b79797dcaee0c881d3ef752a268ed520d96b433b..a0e50e9806f7843d2981141d8941d5e37f53c0cd 100644 +--- a/ipaserver/advise/plugins/smart_card_auth.py ++++ b/ipaserver/advise/plugins/smart_card_auth.py +@@ -34,6 +34,26 @@ class common_smart_card_auth_config(Advice): + 'Use kinit as privileged user to obtain Kerberos credentials' + ]) + ++ def check_ccache_not_empty_if_old_version(self): ++ self.log.comment("On version before IPA 4.9, " ++ "check that the credential cache is not empty") ++ self.log.command( ++ "python3 -c \"from ipapython.version import VERSION;" ++ "from ipaplatform.tasks import tasks;" ++ "exit(tasks.parse_ipa_version(VERSION) >= " ++ "tasks.parse_ipa_version('4.9.0'))\"") ++ with self.log.if_branch('[ "$?" -eq "0" ]'): ++ self.log.exit_on_failed_command( ++ 'klist', ++ [ ++ "Credential cache is empty", ++ 'Use kinit as privileged user to obtain Kerberos ' ++ 'credentials' ++ ]) ++ with self.log.else_branch(): ++ self.log.command( ++ "echo 'Version 4.9.0+ does not require Kerberos credentials'") ++ + def check_and_set_ca_cert_paths(self): + ca_paths_variable = self.smart_card_ca_certs_variable_name + single_ca_path_variable = self.single_ca_cert_variable_name +@@ -260,7 +280,7 @@ class config_client_for_smart_card_auth(common_smart_card_auth_config): + def get_info(self): + self.log.exit_on_nonroot_euid() + self.check_and_set_ca_cert_paths() +- self.check_ccache_not_empty() ++ self.check_ccache_not_empty_if_old_version() + self.check_and_remove_pam_pkcs11() + self.install_opensc_and_dconf_packages() + self.install_krb5_client_dependencies() +diff --git a/ipatests/test_integration/test_advise.py b/ipatests/test_integration/test_advise.py +index 3d5cadee319ebba14ebc43ebb1dc90a502e5d3b8..a336634ae9627133c5ad4dea4b1c43ffd726df10 100644 +--- a/ipatests/test_integration/test_advise.py ++++ b/ipatests/test_integration/test_advise.py +@@ -60,13 +60,17 @@ class TestAdvice(IntegrationTest): + ) + tasks.install_client(cls.master, cls.clients[0]) + +- def execute_advise(self, host, advice_id, *args): ++ def execute_advise(self, host, advice_id, *args, kinit=True): + # ipa-advise script is only available on a server + tasks.kinit_admin(self.master) + advice = self.master.run_command(['ipa-advise', advice_id]) + # execute script on host (client or master) + if host is not self.master: +- tasks.kinit_admin(host) ++ if kinit: ++ tasks.kinit_admin(host) ++ else: ++ # Make sure we don't have any ticket ++ tasks.kdestroy_all(host) + filename = tasks.upload_temp_contents(host, advice.stdout_text) + cmd = ['sh', filename] + cmd.extend(args) +@@ -181,7 +185,7 @@ class TestAdvice(IntegrationTest): + ca_pem = ExternalCA().create_ca() + ca_file = tasks.upload_temp_contents(client, ca_pem) + try: +- self.execute_advise(client, advice_id, ca_file) ++ self.execute_advise(client, advice_id, ca_file, kinit=False) + finally: + client.run_command(['rm', '-f', ca_file]) + +-- +2.52.0 + diff --git a/0005-ipatests-Fix-resolver-state-tracking-in-enforced-DNS.patch b/0005-ipatests-Fix-resolver-state-tracking-in-enforced-DNS.patch new file mode 100644 index 0000000..bc1e9a4 --- /dev/null +++ b/0005-ipatests-Fix-resolver-state-tracking-in-enforced-DNS.patch @@ -0,0 +1,177 @@ +From fd84cec77d18e0e608285ab712f8211b0b49a7fe Mon Sep 17 00:00:00 2001 +From: PRANAV THUBE +Date: Thu, 18 Dec 2025 17:13:29 +0530 +Subject: [PATCH] ipatests: Fix resolver state tracking in enforced DNS policy + tests. + +Use resolver.setup_resolver() instead of direct file manipulation to +prevent state tracking failures in IDM-CI. Remove redundant manual +resolv.conf changes and fix operation order. + +Related: https://pagure.io/freeipa/issue/9904 +Reviewed-By: Antonio Torres +--- + ipatests/test_integration/test_edns.py | 70 ++++++++++++++++++-------- + 1 file changed, 50 insertions(+), 20 deletions(-) + +diff --git a/ipatests/test_integration/test_edns.py b/ipatests/test_integration/test_edns.py +index d02ce45a1f9c0b04332e4ff4a055ab4c61b35eb4..54b7859da1b9fa320c07bdf29f79d2fdb292c337 100644 +--- a/ipatests/test_integration/test_edns.py ++++ b/ipatests/test_integration/test_edns.py +@@ -15,6 +15,9 @@ from ipatests.test_integration.test_dns import TestDNS + from ipatests.pytest_ipa.integration.firewall import Firewall + from ipaplatform.osinfo import osinfo + from ipaplatform.paths import paths ++from ipatests.pytest_ipa.integration.resolver import ( ++ resolver as detect_resolver ++) + + + def apply_enforced_dns_preconfig(host, master_ip, master_host, +@@ -30,6 +33,9 @@ def apply_enforced_dns_preconfig(host, master_ip, master_host, + ca_cert_path: Path to the CA certificate on the master + dest_cert_name: Destination certificate filename in trust anchors + """ ++ # Backup resolver before making any changes (with original resolver type) ++ host.resolver.backup() ++ + # Get the network interface for routing + iface_cmd = host.run_command([ + "bash", "-c", +@@ -43,7 +49,8 @@ def apply_enforced_dns_preconfig(host, master_ip, master_host, + host.put_file_contents(dest_ca_path, ca_cert_data) + host.run_command(["update-ca-trust", "extract"]) + +- if osinfo.id in ['rhel', 'centos']: ++ platform = tasks.get_platform(host) ++ if platform in ['rhel', 'centos']: + # RHEL/CentOS configuration + # Install and configure dnsconfd + tasks.install_packages(host, ['dnsconfd']) +@@ -51,31 +58,60 @@ def apply_enforced_dns_preconfig(host, master_ip, master_host, + host.run_command(["systemctl", "enable", "--now", "dnsconfd"]) + host.run_command(["nmcli", "g", "reload"]) + +- # Configure DNS over TLS via NetworkManager ++ # Configure DNS over TLS via NetworkManager device-specific settings. ++ # Device-specific DNS settings work alongside global resolver config. + host.run_command([ + "nmcli", "device", "modify", iface, + "ipv4.dns", f"dns+tls://{master_ip}" + ]) + +- elif osinfo.id == 'fedora': +- # Fedora configuration ++ elif platform == 'fedora': + # Configure systemd-resolved for DNS over TLS + host.run_command([ +- "ln", "-sf", "../run/systemd/resolve/stub-resolv.conf", ++ "ln", "-sf", "/run/systemd/resolve/stub-resolv.conf", + "/etc/resolv.conf" + ]) ++ # Restart systemd-resolved to ensure DoT settings are fully applied ++ # and resolv.conf is updated + host.run_command(["systemctl", "restart", "systemd-resolved"]) +- +- # Configure DNS over TLS via systemd-resolve ++ # Configure DNS over TLS via systemd-resolve per-interface settings ++ # Per-interface DNS settings work alongside the global resolver config + host.run_command([ + "systemd-resolve", "--set-dns", master_ip, + "--set-dnsovertls=yes", f"--interface={iface}" + ]) + else: + raise ValueError( +- f"Unsupported OS for enforced DNS policy: {osinfo.id}" ++ f"Unsupported OS for enforced DNS policy: {platform}" + ) + ++ # Re-detect resolver since we may have changed the resolver type ++ # (e.g., from PlainFileResolver to ResolvedResolver on Fedora) ++ # This ensures state tracking works correctly with the new resolver type ++ # Store the original resolver so we can restore from it during uninstall ++ host._enforced_dns_original_resolver = host.resolver ++ host.resolver = detect_resolver(host) ++ host.resolver.current_state = host.resolver._get_state() ++ ++ ++def restore_enforced_dns_resolver(host): ++ """ ++ Restore resolver state after enforced DNS preconfig. ++ ++ This restores the original resolver backup created by ++ apply_enforced_dns_preconfig() and syncs host.resolver state. ++ """ ++ if not hasattr(host, '_enforced_dns_original_resolver'): ++ return ++ original_resolver = host._enforced_dns_original_resolver ++ if original_resolver.has_backups(): ++ original_resolver.current_state = original_resolver._get_state() ++ original_resolver.restore() ++ # Sync host.resolver state since original_resolver changed the config ++ host.resolver.backups.clear() ++ host.resolver.current_state = host.resolver._get_state() ++ delattr(host, '_enforced_dns_original_resolver') ++ + + def setup_dns_over_tls_environment(cls): + """ +@@ -451,17 +487,12 @@ class TestDNSOverTLS_EnforcedPolicy_IPA_CA(IntegrationTest): + tasks.install_master(self.master, extra_args=args) + + # Apply pre-configuration on client before installation ++ # This configures the resolver with master IP and domain + apply_enforced_dns_preconfig( + self.clients[0], self.master.ip, self.master, + paths.IPA_CA_CRT, "ca.crt" + ) + +- # Configure client nameserver +- self.clients[0].put_file_contents( +- paths.RESOLV_CONF, +- "nameserver %s" % self.master.ip +- ) +- + # Install client with enforced policy + args = [ + "--dns-over-tls", +@@ -506,6 +537,8 @@ class TestDNSOverTLS_EnforcedPolicy_IPA_CA(IntegrationTest): + """ + This test ensures that all hosts can be uninstalled correctly. + """ ++ for host in [self.clients[0], self.replicas[0]]: ++ restore_enforced_dns_resolver(host) + tasks.uninstall_client(self.clients[0]) + tasks.uninstall_replica(self.master, self.replicas[0]) + tasks.uninstall_master(self.master) +@@ -559,17 +592,12 @@ class TestDNSOverTLS_EnforcedPolicy_External_CA(IntegrationTest): + + # Apply pre-configuration on client before installation + # (includes CA cert setup) ++ # This configures the resolver with master IP and domain + apply_enforced_dns_preconfig( + self.clients[0], self.master.ip, self.master, + cert_dest, "certificate.pem" + ) + +- # Configure client nameserver +- self.clients[0].put_file_contents( +- paths.RESOLV_CONF, +- "nameserver %s" % self.master.ip +- ) +- + # Install client with enforced policy + args = [ + "--dns-over-tls", +@@ -615,6 +643,8 @@ class TestDNSOverTLS_EnforcedPolicy_External_CA(IntegrationTest): + """ + This test ensures that all hosts can be uninstalled correctly. + """ ++ for host in [self.clients[0], self.replicas[0]]: ++ restore_enforced_dns_resolver(host) + tasks.uninstall_client(self.clients[0]) + tasks.uninstall_replica(self.master, self.replicas[0]) + tasks.uninstall_master(self.master) +-- +2.52.0 + diff --git a/0006-freeipa.spec.in-Use-systemd-sysusers-to-setup-users-.patch b/0006-freeipa.spec.in-Use-systemd-sysusers-to-setup-users-.patch new file mode 100644 index 0000000..8c99a51 --- /dev/null +++ b/0006-freeipa.spec.in-Use-systemd-sysusers-to-setup-users-.patch @@ -0,0 +1,224 @@ +From 0800065ac5555dba102f05c947ca47b5dc9a81af Mon Sep 17 00:00:00 2001 +From: Rafael Guterres Jeffman +Date: Fri, 23 Jan 2026 16:49:31 -0300 +Subject: [PATCH] freeipa.spec.in: Use systemd-sysusers to setup users and + groups + +System accounts for `kdcproxy` and `ipaapi` are now created with +sysusers configuration and macros. User `apache` is updated, by +adding it to group `ipaapi` using sysusers configuration. + +Fixes: https://pagure.io/freeipa/issue/9572 + +AI agent usage info: + +The initial changes were created by Claude by providing the following +context: + +>> Add support for creating users through systemd-sysusers by creating +>> a folder init/sysusersd, similar to init/tmpfilesd, changing install +>> paths in init/sysusersd/Makefile.am, adding configure option +>> --with-systemdsysusersdir similar to --with-systemdtmpfilesdir, and +>> adding a new file init/sysusersd/freeeipo.sysusers.in with the +>> contents: +>> ``` +>> # system accounts for IPA +>> u! kdcproxy - "IPA KDC Proxy Uer" +>> u! ipaapi - "IPA Framework User" +>> # - add Apache HTTPd user to ipaapi group +>> m apache ipaapi +>> ``` +>> and updating de spec file freeipa.spec.in + +LLM model used was Claude Sonnet 4.5, and a CLAUDE.md file was +automatically created by claude based on the freeipa repository. +No custom context was available for the agent. + +Assisted-by: Claude +Signed-off-by: Rafael Guterres Jeffman +Reviewed-By: David Hanina +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Alexander Bokovoy +--- + configure.ac | 42 ++++++++++++++++++++++++++------------ + freeipa.spec.in | 16 +++------------ + init/Makefile.am | 2 +- + init/sysusersd/Makefile.am | 12 +++++++++++ + init/sysusersd/ipa.conf.in | 8 ++++++++ + 5 files changed, 53 insertions(+), 27 deletions(-) + create mode 100644 init/sysusersd/Makefile.am + create mode 100644 init/sysusersd/ipa.conf.in + +diff --git a/configure.ac b/configure.ac +index 8b9adec1559c8831ef39c27860c1d31496ec5474..b0462bf779dedb7c2fe59494d4eb64a6dd121b1a 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -267,6 +267,13 @@ AC_ARG_WITH([systemdtmpfilesdir], + [systemdtmpfilesdir=$($PKG_CONFIG --define-variable=prefix='${prefix}' --variable=tmpfilesdir systemd)]) + AC_SUBST([systemdtmpfilesdir]) + ++AC_ARG_WITH([systemdsysusersdir], ++ AS_HELP_STRING([--with-systemdsysusersdir=DIR], ++ [Directory for systemd-sysusers configuration files]), ++ [systemdsysusersdir=$with_systemdsysusersdir], ++ [systemdsysusersdir=$($PKG_CONFIG --define-variable=prefix='${prefix}' --variable=sysusersdir systemd)]) ++AC_SUBST([systemdsysusersdir]) ++ + AC_ARG_WITH([systemdcatalogdir], + AS_HELP_STRING([--with-systemdcatalogdir=DIR], + [Directory for systemd journal catalog files]), +@@ -398,22 +405,29 @@ AC_SUBST([IPAPLATFORM]) + AC_MSG_RESULT([${IPAPLATFORM}]) + + if test "x${IPAPLATFORM}" == "xdebian"; then +- HTTPD_GROUP="www-data" +- KRB5KDC_SERVICE="krb5-kdc.service" +- NAMED_GROUP="bind" +- ODS_USER="opendnssec" +- ODS_GROUP="opendnssec" +- # see https://www.debian.org/doc/packaging-manuals/python-policy/ap-packaging_tools.html +- PYTHON_INSTALL_EXTRA_OPTIONS="--install-layout=deb" ++ dnl Ubuntu http user is www-data ++ HTTPD_USER="www-data" ++ HTTPD_GROUP="www-data" ++ KRB5KDC_SERVICE="krb5-kdc.service" ++ NAMED_GROUP="bind" ++ ODS_USER="opendnssec" ++ ODS_GROUP="opendnssec" ++ # see https://www.debian.org/doc/packaging-manuals/python-policy/ap-packaging_tools.html ++ PYTHON_INSTALL_EXTRA_OPTIONS="--install-layout=deb" + else +- HTTPD_GROUP="apache" +- KRB5KDC_SERVICE="krb5kdc.service" +- NAMED_GROUP="named" +- ODS_USER="ods" +- ODS_GROUP="ods" +- PYTHON_INSTALL_EXTRA_OPTIONS="" ++ HTTPD_USER="apache" ++ HTTPD_GROUP="apache" ++ KRB5KDC_SERVICE="krb5kdc.service" ++ NAMED_GROUP="named" ++ ODS_USER="ods" ++ ODS_GROUP="ods" ++ PYTHON_INSTALL_EXTRA_OPTIONS="" + fi + ++AC_MSG_CHECKING([HTTPD_USER]) ++AC_SUBST([HTTPD_USER]) ++AC_MSG_RESULT([${HTTPD_USER}]) ++ + AC_MSG_CHECKING([HTTPD_GROUP]) + AC_SUBST([HTTPD_GROUP]) + AC_MSG_RESULT([${HTTPD_GROUP}]) +@@ -654,6 +668,7 @@ AC_CONFIG_FILES([ + daemons/ipa-slapi-plugins/topology/Makefile + init/systemd/Makefile + init/tmpfilesd/Makefile ++ init/sysusersd/Makefile + init/Makefile + install/Makefile + install/certmonger/Makefile +@@ -736,6 +751,7 @@ AM_COND_IF([ENABLE_SERVER], [ + KRAD libs: ${KRAD_LIBS} + krb5rundir: ${krb5rundir} + systemdtmpfilesdir: ${systemdtmpfilesdir} ++ systemdsysusersdir: ${systemdsysusersdir} + build mode: server & client" + ], [ + echo "\ +diff --git a/freeipa.spec.in b/freeipa.spec.in +index f3b45a5308f93928a4d4bb4cbb2ae96c487cf88a..48912185073472c11f08d000dacf3a0b7f2ec668 100644 +--- a/freeipa.spec.in ++++ b/freeipa.spec.in +@@ -620,7 +620,7 @@ Requires: systemd-units >= %{systemd_version} + Requires: system-logos-ipa >= 80.4 + %endif + +-# The list below is automatically generated by `fix-spec.sh -i` ++# The list below is automatically generated by `fix-spec.sh -i` + # from the install/freeipa-webui + Provides: bundled(npm(attr-accept)) = 2.2.5 + Provides: bundled(npm(cookie)) = 1.0.2 +@@ -1274,6 +1274,7 @@ fi + /bin/systemctl reload-or-try-restart dbus + /bin/systemctl reload-or-try-restart oddjobd + ++%sysusers_create %{_sysusersdir}/ipa.conf + %tmpfiles_create ipa.conf + %journal_catalog_update + +@@ -1331,18 +1332,6 @@ if [ -e /usr/sbin/ipa_kpasswd ]; then + fi + + +-%pre server-common +-# create users and groups +-# create kdcproxy group and user +-getent group kdcproxy >/dev/null || groupadd -f -r kdcproxy +-getent passwd kdcproxy >/dev/null || useradd -r -g kdcproxy -s /sbin/nologin -d / -c "IPA KDC Proxy User" kdcproxy +-# create ipaapi group and user +-getent group ipaapi >/dev/null || groupadd -f -r ipaapi +-getent passwd ipaapi >/dev/null || useradd -r -g ipaapi -s /sbin/nologin -d / -c "IPA Framework User" ipaapi +-# add apache to ipaaapi group +-id -Gn apache | grep '\bipaapi\b' >/dev/null || usermod apache -a -G ipaapi +- +- + %post server-dns + %systemd_post ipa-dnskeysyncd.service ipa-ods-exporter.socket ipa-ods-exporter.service + +@@ -1729,6 +1718,7 @@ fi + %dir %attr(0755,root,root) %{_sysconfdir}/ipa/kdcproxy + %config(noreplace) %{_sysconfdir}/ipa/kdcproxy/kdcproxy.conf + # NOTE: systemd specific section ++%{_sysusersdir}/ipa.conf + %{_tmpfilesdir}/ipa.conf + %attr(644,root,root) %{_unitdir}/ipa-custodia.service + %ghost %attr(644,root,root) %{etc_systemd_dir}/httpd.d/ipa.conf +diff --git a/init/Makefile.am b/init/Makefile.am +index 8f4d1d0a8f7e9739cf7587de6e000dd027a85146..1d4a85ab20e892c8a7c428b84a6393d29e9616e5 100644 +--- a/init/Makefile.am ++++ b/init/Makefile.am +@@ -2,7 +2,7 @@ + # + AUTOMAKE_OPTIONS = 1.7 + +-SUBDIRS = systemd tmpfilesd ++SUBDIRS = systemd tmpfilesd sysusersd + + dist_sysconfenv_DATA = \ + ipa-dnskeysyncd \ +diff --git a/init/sysusersd/Makefile.am b/init/sysusersd/Makefile.am +new file mode 100644 +index 0000000000000000000000000000000000000000..8577255a61ac796353995d3d1f99de195f9bd7c0 +--- /dev/null ++++ b/init/sysusersd/Makefile.am +@@ -0,0 +1,12 @@ ++dist_noinst_DATA = \ ++ ipa.conf.in ++ ++systemdsysusers_DATA = \ ++ ipa.conf ++ ++CLEANFILES = $(systemdsysusers_DATA) ++ ++%: %.in Makefile ++ sed \ ++ -e 's|@HTTPD_USER[@]|$(HTTPD_USER)|g' \ ++ '$(srcdir)/$@.in' >$@ +diff --git a/init/sysusersd/ipa.conf.in b/init/sysusersd/ipa.conf.in +new file mode 100644 +index 0000000000000000000000000000000000000000..dcddfc2fc7969b86913ffcd8c397152e4f800fda +--- /dev/null ++++ b/init/sysusersd/ipa.conf.in +@@ -0,0 +1,8 @@ ++# IPA KDC Proxy user and group ++u! kdcproxy - "IPA KDC Proxy User" ++ ++# IPA API user and group ++u! ipaapi - "IPA API User" ++ ++# - add Apache system account to ipaapi group (platform-specific) ++m @HTTPD_USER@ ipaapi +-- +2.52.0 + diff --git a/0007-ipatests-add-Random-Password-based-replica-promotion.patch b/0007-ipatests-add-Random-Password-based-replica-promotion.patch new file mode 100644 index 0000000..e32bda0 --- /dev/null +++ b/0007-ipatests-add-Random-Password-based-replica-promotion.patch @@ -0,0 +1,196 @@ +From a55f9185c96457bdffe9099ddde39ec696f1f998 Mon Sep 17 00:00:00 2001 +From: Anuja More +Date: Tue, 6 Jan 2026 18:30:06 +0530 +Subject: [PATCH] ipatests: add Random Password based replica promotion + coverage + +Added missing test coverage for : +- Installing IPA replica server using random password. +- Installing IPA replica server using random password installed client + +- Automated with Cursor+Claude + +Fixes: https://pagure.io/freeipa/issue/9922 + +Signed-off-by: Anuja More +Reviewed-By: David Hanina +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +Reviewed-By: Rob Crittenden +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: David Hanina +--- + .../nightly_ipa-4-13_latest.yaml | 12 +++ + .../nightly_ipa-4-13_latest_selinux.yaml | 13 +++ + ipatests/pytest_ipa/integration/tasks.py | 15 ++++ + .../test_replica_promotion.py | 87 +++++++++++++++++++ + 4 files changed, 127 insertions(+) + +diff --git a/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml b/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml +index aff55727e463207fb235ff340989491e62162149..c61701ef5f88760f1d6fc36d4acce453a22b6f8f 100644 +--- a/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml ++++ b/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml +@@ -1000,6 +1000,18 @@ jobs: + timeout: 7200 + topology: *ad_master_1repl_1client + ++ fedora-latest-ipa-4-13/test_replica_promotion_TestReplicaPromotionRandomPassword: ++ requires: [fedora-latest-ipa-4-13/build] ++ priority: 50 ++ job: ++ class: RunPytest ++ args: ++ build_url: '{fedora-latest-ipa-4-13/build_url}' ++ test_suite: test_integration/test_replica_promotion.py::TestReplicaPromotionRandomPassword ++ template: *ci-ipa-4-13-latest ++ timeout: 7200 ++ topology: *master_1repl ++ + fedora-latest-ipa-4-13/test_upgrade: + requires: [fedora-latest-ipa-4-13/build] + priority: 50 +diff --git a/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml b/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml +index e6c57ea060b3bb8bfdf8b6f981f8fd28e4a7d320..9b96f3e857e2125478b45632d8d58e42b6e92668 100644 +--- a/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml ++++ b/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml +@@ -1078,6 +1078,19 @@ jobs: + timeout: 7200 + topology: *ad_master_1repl_1client + ++ fedora-latest-ipa-4-13/test_replica_promotion_TestReplicaPromotionRandomPassword: ++ requires: [fedora-latest-ipa-4-13/build] ++ priority: 50 ++ job: ++ class: RunPytest ++ args: ++ build_url: '{fedora-latest-ipa-4-13/build_url}' ++ selinux_enforcing: True ++ test_suite: test_integration/test_replica_promotion.py::TestReplicaPromotionRandomPassword ++ template: *ci-ipa-4-13-latest ++ timeout: 7200 ++ topology: *master_1repl ++ + fedora-latest-ipa-4-13/test_upgrade: + requires: [fedora-latest-ipa-4-13/build] + priority: 50 +diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py +index 32ac5cbc2c6fe87850dfb15c1d5beae6fa648dfb..ff2ea9792d04ebd2e6bd7bb3b51d97f35cb3fbfb 100755 +--- a/ipatests/pytest_ipa/integration/tasks.py ++++ b/ipatests/pytest_ipa/integration/tasks.py +@@ -3340,3 +3340,18 @@ def service_control_dirsrv(host, function='restart'): + instance = realm_to_serverid(host.domain.realm) + cmd = host.run_command(['systemctl', function, f"dirsrv@{instance}"]) + assert cmd.returncode == 0 ++ ++ ++def host_add_with_random_password(host, new_host): ++ """ ++ Add a new host with a random password and return the generated password. ++ """ ++ kinit_admin(host) ++ cmd = host.run_command( ++ ['ipa', 'host-add', new_host.hostname, '--random'] ++ ) ++ result = re.search("Random password: (?P.*$)", ++ cmd.stdout_text, ++ re.MULTILINE) ++ randpasswd1 = result.group('password') ++ return randpasswd1 +diff --git a/ipatests/test_integration/test_replica_promotion.py b/ipatests/test_integration/test_replica_promotion.py +index 76d6aa24e2ab3d88b7013e0d107d0e27ae7f3426..f8c8414eefbc015cfc0947de575ea349a65a5e73 100644 +--- a/ipatests/test_integration/test_replica_promotion.py ++++ b/ipatests/test_integration/test_replica_promotion.py +@@ -1368,3 +1368,90 @@ class TestReplicaConn(IntegrationTest): + logs = self.replica.get_file_contents(paths.IPAREPLICA_CONNCHECK_LOG) + error = "not allowed to perform server connection check" + assert error.encode() not in logs ++ ++ ++class TestReplicaPromotionRandomPassword(IntegrationTest): ++ """ ++ Test installation of a replica using Random Password ++ (one step install and two-steps installation ++ with client and promotion). ++ """ ++ num_replicas = 1 ++ ++ @classmethod ++ def install(cls, mh): ++ tasks.install_master(cls.master, setup_dns=True) ++ cls.replicas[0].resolver.backup() ++ nameservers = cls.master.ip ++ cls.replicas[0].resolver.setup_resolver( ++ nameservers, cls.master.domain.name ++ ) ++ ++ @replicas_cleanup ++ def test_replica_random_password_install(self): ++ """ ++ Installing IPA replica server using Random Password. ++ ++ Steps: ++ 1. Ensure replica host/server entries are clean and add DNS A record. ++ 2. Add the replica host with a random password and add it to ++ the ipaservers hostgroup. ++ 3. Install the replica using random password. ++ """ ++ replica = self.replicas[0] ++ tasks.kinit_admin(self.master) ++ tasks.add_a_record(self.master, replica) ++ randpasswd = tasks.host_add_with_random_password(self.master, ++ replica) ++ self.master.run_command([ ++ 'ipa', 'hostgroup-add-member', '--hosts', ++ replica.hostname, 'ipaservers' ++ ]) ++ replica.run_command( ++ ['ipa-replica-install', '-p', randpasswd, '-U'] ++ ) ++ ++ @replicas_cleanup ++ def test_replica_two_step_install(self): ++ """ ++ Installing IPA replica server using Random Password installed client ++ ++ Steps: ++ 1. Ensure replica host/server entries are clean and add DNS A record. ++ 2. Add the replica host with a random password and add it to ++ the ipaservers hostgroup. ++ 3. Install the IPA client using the Random Password. ++ 4. Promote the client to a replica. ++ 5. Install CA on the replica and verify the server role. ++ """ ++ replica = self.replicas[0] ++ replica.resolver.backup() ++ tasks.kinit_admin(self.master) ++ tasks.add_a_record(self.master, replica) ++ randpasswd = tasks.host_add_with_random_password(self.master, ++ replica) ++ self.master.run_command([ ++ 'ipa', 'hostgroup-add-member', '--hosts', ++ replica.hostname, 'ipaservers' ++ ]) ++ replica.resolver.setup_resolver( ++ self.master.ip, self.master.domain.name ++ ) ++ replica.run_command( ++ ['ipa-client-install', '-w', randpasswd, '-U'] ++ ) ++ Firewall(replica).enable_services(["freeipa-ldap", ++ "freeipa-ldaps"]) ++ replica.run_command(['ipa-replica-install', '-U']) ++ tasks.kinit_admin(replica) ++ replica.run_command([ ++ 'ipa-ca-install', '-p', ++ self.master.config.admin_password, ++ '-w', self.master.config.admin_password ++ ]) ++ result = self.replicas[0].run_command([ ++ 'ipa', 'server-role-find', ++ '--server', self.replicas[0].hostname, ++ '--role', 'CA server' ++ ]) ++ assert 'Role status: enabled' in result.stdout_text +-- +2.52.0 + diff --git a/0008-ipatests-Add-integration-tests-for-ipa-join-command.patch b/0008-ipatests-Add-integration-tests-for-ipa-join-command.patch new file mode 100644 index 0000000..c94343b --- /dev/null +++ b/0008-ipatests-Add-integration-tests-for-ipa-join-command.patch @@ -0,0 +1,760 @@ +From 7cb2c5d38a84eb31aa9ddd20cf9dc0b6d90fa242 Mon Sep 17 00:00:00 2001 +From: PRANAV THUBE +Date: Tue, 27 Jan 2026 18:45:53 +0530 +Subject: [PATCH] ipatests: Add integration tests for ipa-join command + +Add tests for ipa-join command covering hostname, server, keytab, +and bindpw options with positive and negative scenarios. + +Related: https://pagure.io/freeipa/issue/9930 +Reviewed-By: Rob Crittenden +--- + ipatests/prci_definitions/gating.yaml | 12 + + .../nightly_ipa-4-13_latest.yaml | 12 + + .../nightly_ipa-4-13_latest_selinux.yaml | 13 + + ipatests/pytest_ipa/integration/tasks.py | 49 ++ + ipatests/test_integration/test_ipa_join.py | 614 ++++++++++++++++++ + 5 files changed, 700 insertions(+) + create mode 100644 ipatests/test_integration/test_ipa_join.py + +diff --git a/ipatests/prci_definitions/gating.yaml b/ipatests/prci_definitions/gating.yaml +index 0c2a0dafa9de12add8959d9974f080ba8bec0706..182f2c5a097856d22336fa66a0876bdfcf3f3f8d 100644 +--- a/ipatests/prci_definitions/gating.yaml ++++ b/ipatests/prci_definitions/gating.yaml +@@ -394,3 +394,15 @@ jobs: + template: *ci-ipa-4-13-latest + timeout: 7200 + topology: *master_3client ++ ++ fedora-latest-ipa-4-13/test_ipa_join: ++ requires: [fedora-latest-ipa-4-13/build] ++ priority: 100 ++ job: ++ class: RunPytest ++ args: ++ build_url: '{fedora-latest-ipa-4-13/build_url}' ++ test_suite: test_integration/test_ipa_join.py ++ template: *ci-ipa-4-13-latest ++ timeout: 3600 ++ topology: *master_1repl_1client +diff --git a/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml b/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml +index c61701ef5f88760f1d6fc36d4acce453a22b6f8f..aba33a5a05185460305c7516c93ae25c60f9dda7 100644 +--- a/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml ++++ b/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml +@@ -2292,3 +2292,15 @@ jobs: + template: *ci-ipa-4-13-latest + timeout: 2400 + topology: *ipaserver ++ ++ fedora-latest-ipa-4-13/test_ipa_join: ++ requires: [fedora-latest-ipa-4-13/build] ++ priority: 50 ++ job: ++ class: RunPytest ++ args: ++ build_url: '{fedora-latest-ipa-4-13/build_url}' ++ test_suite: test_integration/test_ipa_join.py ++ template: *ci-ipa-4-13-latest ++ timeout: 3600 ++ topology: *master_1repl_1client +diff --git a/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml b/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml +index 9b96f3e857e2125478b45632d8d58e42b6e92668..7184b722076ba2cab7d782d990a9bc218158a09f 100644 +--- a/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml ++++ b/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml +@@ -2476,3 +2476,16 @@ jobs: + template: *ci-ipa-4-13-latest + timeout: 2400 + topology: *ipaserver ++ ++ fedora-latest-ipa-4-13/test_ipa_join: ++ requires: [fedora-latest-ipa-4-13/build] ++ priority: 50 ++ job: ++ class: RunPytest ++ args: ++ build_url: '{fedora-latest-ipa-4-13/build_url}' ++ selinux_enforcing: True ++ test_suite: test_integration/test_ipa_join.py ++ template: *ci-ipa-4-13-latest ++ timeout: 3600 ++ topology: *master_1repl_1client +diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py +index ff2ea9792d04ebd2e6bd7bb3b51d97f35cb3fbfb..47330d6d93401485e4eb7b2501cf5ea37498d719 100755 +--- a/ipatests/pytest_ipa/integration/tasks.py ++++ b/ipatests/pytest_ipa/integration/tasks.py +@@ -3355,3 +3355,52 @@ def host_add_with_random_password(host, new_host): + re.MULTILINE) + randpasswd1 = result.group('password') + return randpasswd1 ++ ++ ++def ipa_join(host, *extra_args, raiseonerr=True): ++ """Run ipa-join command. ++ ++ :param host: The host to run command on ++ :param extra_args: Additional arguments (variable positional args) ++ e.g., '--hostname=client.example.com', ++ '--server=master.example.com', ++ '--keytab=/tmp/test.keytab', ++ '--bindpw=password', ++ '-u' (for unenroll) ++ :param raiseonerr: If True, raise exception on command failure ++ :return: Command result object ++ """ ++ command = ['ipa-join'] ++ command.extend(extra_args) ++ return host.run_command(command, raiseonerr=raiseonerr) ++ ++ ++def host_del(host, hostname, *extra_args, raiseonerr=True): ++ """Delete a host from IPA. ++ ++ :param host: The IPA host to run command on ++ :param hostname: Hostname to delete ++ :param extra_args: Additional arguments (variable positional args) ++ :param raiseonerr: If True, raise exception on command failure ++ :return: Command result object ++ """ ++ command = ['ipa', 'host-del', hostname] ++ command.extend(extra_args) ++ return host.run_command(command, raiseonerr=raiseonerr) ++ ++ ++def host_add(host, hostname, *extra_args, password=None, raiseonerr=True): ++ """Add a host to IPA. ++ ++ :param host: The IPA host to run command on ++ :param hostname: Hostname to add ++ :param extra_args: Additional arguments (variable positional args) ++ :param password: OTP/enrollment password for the host (optional) ++ :param raiseonerr: If True, raise exception on command failure ++ :return: Command result object ++ """ ++ command = ['ipa', 'host-add', hostname] ++ if password: ++ command.append(f'--password={password}') ++ command.extend(extra_args) ++ return host.run_command(command, raiseonerr=raiseonerr) +diff --git a/ipatests/test_integration/test_ipa_join.py b/ipatests/test_integration/test_ipa_join.py +new file mode 100644 +index 0000000000000000000000000000000000000000..1f7592aec8db1bfd048ec574d06d25bc24373499 +--- /dev/null ++++ b/ipatests/test_integration/test_ipa_join.py +@@ -0,0 +1,614 @@ ++# ++# Copyright (C) 2026 FreeIPA Contributors see COPYING for license ++# ++ ++""" ++Tests for ipa-join command functionality. ++ ++This module tests various combinations of ipa-join options including: ++- hostname ++- server ++- keytab ++- bindpw (OTP/enrollment password) ++- unenroll ++ ++Ported from the shell-based test suite (t.ipajoin.sh and t.ipaotp.sh). ++""" ++ ++from __future__ import absolute_import ++ ++from ipapython.ipautil import ipa_generate_password ++from ipatests.pytest_ipa.integration import tasks ++from ipatests.test_integration.base import IntegrationTest ++ ++ ++# Constants ++OTP = ipa_generate_password(special=None) ++INVALID_PASSWORD = "WrongPassword" ++INVALID_SERVER = "No.Such.IPA.Server.Domain.com" ++TEST_KEYTAB = "/tmp/ipajoin.test.keytab" ++ ++# Error messages ++ERR_SASL_BIND_FAILED = "SASL Bind failed" ++ERR_UNAUTHENTICATED_BIND = "Unauthenticated binds are not allowed" ++ERR_COULD_NOT_RESOLVE = "JSON-RPC call failed: Could not resolve hostname" ++ERR_UNABLE_ROOT_DN = "Unable to determine root DN" ++ERR_NO_CONFIG = "Unable to determine IPA server from /etc/ipa/default.conf" ++ERR_PREAUTH_FAILED = "Generic preauthentication failure" ++ ++# Exit codes ++EXIT_SUCCESS = 0 ++EXIT_GENERAL_ERROR = 1 ++EXIT_PREAUTH_ERROR = 19 ++EXIT_ROOT_DN_ERROR = 14 ++EXIT_SASL_BIND_FAILED = 15 ++EXIT_RESOLVE_ERROR = 17 ++ ++ ++class TestIPAJoin(IntegrationTest): ++ """Tests for ipa-join command functionality. ++ ++ This test class covers various ipa-join scenarios including: ++ - Basic enrollment and unenrollment ++ - Using hostname, server, keytab, and bindpw options ++ - Positive and negative test cases ++ - OTP (one-time password) enrollment tests ++ ++ Tests require one master and one client. ++ """ ++ ++ topology = 'line' ++ num_clients = 1 ++ ++ @classmethod ++ def install(cls, mh): ++ tasks.install_master(cls.master, setup_dns=True) ++ tasks.install_client(cls.master, cls.clients[0]) ++ ++ @classmethod ++ def uninstall(cls, mh): ++ # Cleanup test keytab if exists ++ cls.clients[0].run_command( ++ ['rm', '-f', TEST_KEYTAB], ++ raiseonerr=False ++ ) ++ tasks.uninstall_client(cls.clients[0]) ++ tasks.uninstall_master(cls.master) ++ ++ # ========================================================================= ++ # ipa-join basic tests ++ # ========================================================================= ++ ++ def test_unenroll(self): ++ """Test ipa-join --unenroll option.""" ++ result = tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ assert result.returncode == EXIT_SUCCESS ++ ++ def test_unenroll_already_unenrolled(self): ++ """Test ipa-join -u on an already unenrolled client. ++ ++ When trying to unenroll a client that is not enrolled, ++ ipa-join should fail with a preauthentication error. ++ """ ++ # Client is already unenrolled from previous test ++ result = tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ assert result.returncode == EXIT_PREAUTH_ERROR ++ assert ERR_PREAUTH_FAILED in result.stderr_text ++ ++ def test_hostname_with_kerberos(self): ++ """Test ipa-join with --hostname using Kerberos auth.""" ++ tasks.kinit_admin(self.clients[0]) ++ try: ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_hostname_bindpw_invalid(self): ++ """Test ipa-join with hostname and invalid bindpw.""" ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--bindpw={INVALID_PASSWORD}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_SASL_BIND_FAILED ++ assert ERR_SASL_BIND_FAILED in result.stderr_text ++ ++ def test_hostname_bindpw_valid(self): ++ """Test ipa-join with hostname and valid OTP.""" ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--bindpw={OTP}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_hostname_keytab_with_kerberos(self): ++ """Test ipa-join with hostname and keytab using Kerberos.""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ ++ try: ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--keytab={TEST_KEYTAB}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_hostname_keytab_bindpw_invalid(self): ++ """Test ipa-join with hostname, keytab, and invalid bindpw.""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--keytab={TEST_KEYTAB}', ++ f'--bindpw={INVALID_PASSWORD}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_SASL_BIND_FAILED ++ assert ERR_SASL_BIND_FAILED in result.stderr_text ++ ++ def test_hostname_keytab_bindpw_valid(self): ++ """Test ipa-join with hostname, keytab, and valid OTP.""" ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--keytab={TEST_KEYTAB}', ++ f'--bindpw={OTP}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_hostname_server_invalid_with_kerberos(self): ++ """Test ipa-join with hostname and invalid server.""" ++ tasks.kinit_admin(self.clients[0]) ++ try: ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--server={INVALID_SERVER}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_RESOLVE_ERROR ++ assert ERR_COULD_NOT_RESOLVE in result.stderr_text ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ ++ def test_hostname_server_invalid_bindpw_valid(self): ++ """Test ipa-join with hostname, invalid server, and valid OTP.""" ++ tasks.kinit_admin(self.clients[0]) ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--server={INVALID_SERVER}', ++ f'--bindpw={OTP}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_ROOT_DN_ERROR ++ assert ERR_UNABLE_ROOT_DN in result.stderr_text ++ ++ def test_hostname_server_invalid_keytab_with_kerberos(self): ++ """Test ipa-join with hostname, invalid server, keytab.""" ++ tasks.kinit_admin(self.clients[0]) ++ try: ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--server={INVALID_SERVER}', ++ f'--keytab={TEST_KEYTAB}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_RESOLVE_ERROR ++ assert ERR_COULD_NOT_RESOLVE in result.stderr_text ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ ++ def test_hostname_server_invalid_keytab_bindpw_valid(self): ++ """Test ipa-join with hostname, invalid server, keytab, valid OTP.""" ++ tasks.kinit_admin(self.clients[0]) ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--server={INVALID_SERVER}', ++ f'--keytab={TEST_KEYTAB}', ++ f'--bindpw={OTP}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_ROOT_DN_ERROR ++ assert ERR_UNABLE_ROOT_DN in result.stderr_text ++ ++ def test_hostname_server_valid_with_kerberos(self): ++ """Test ipa-join with hostname and valid server.""" ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ ++ try: ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--server={self.master.hostname}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_hostname_server_valid_bindpw_invalid(self): ++ """Test ipa-join with hostname, valid server, invalid bindpw.""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--server={self.master.hostname}', ++ f'--bindpw={INVALID_PASSWORD}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_SASL_BIND_FAILED ++ assert ERR_SASL_BIND_FAILED in result.stderr_text ++ ++ def test_hostname_server_valid_bindpw_valid(self): ++ """Test ipa-join with hostname, valid server, valid OTP.""" ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--server={self.master.hostname}', ++ f'--bindpw={OTP}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_hostname_server_valid_keytab_with_kerberos(self): ++ """Test ipa-join with hostname, valid server, keytab.""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname) ++ ++ try: ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--server={self.master.hostname}', ++ f'--keytab={TEST_KEYTAB}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_hostname_server_valid_keytab_bindpw_invalid(self): ++ """Test ipa-join with hostname, valid server, keytab, bad bindpw.""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--server={self.master.hostname}', ++ f'--keytab={TEST_KEYTAB}', ++ f'--bindpw={INVALID_PASSWORD}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_SASL_BIND_FAILED ++ # Note: Original test had "SASL Bind Failed" (capital F), checking both ++ assert "SASL Bind" in result.stderr_text ++ assert "ailed" in result.stderr_text ++ ++ def test_hostname_server_valid_keytab_bindpw_valid(self): ++ """Test ipa-join with hostname, valid server, keytab, valid OTP.""" ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--server={self.master.hostname}', ++ f'--keytab={TEST_KEYTAB}', ++ f'--bindpw={OTP}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_keytab_only_with_kerberos(self): ++ """Test ipa-join with keytab only using Kerberos.""" ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname) ++ ++ try: ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--keytab={TEST_KEYTAB}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_keytab_bindpw_invalid(self): ++ """Test ipa-join with keytab and invalid bindpw.""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--keytab={TEST_KEYTAB}', ++ f'--bindpw={INVALID_PASSWORD}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_SASL_BIND_FAILED ++ assert ERR_SASL_BIND_FAILED in result.stderr_text ++ ++ def test_keytab_bindpw_valid(self): ++ """Test ipa-join with keytab and valid OTP.""" ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--keytab={TEST_KEYTAB}', ++ f'--bindpw={OTP}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_server_invalid_only_with_kerberos(self): ++ """Test ipa-join with invalid server only.""" ++ tasks.kinit_admin(self.clients[0]) ++ try: ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--server={INVALID_SERVER}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_RESOLVE_ERROR ++ assert ERR_COULD_NOT_RESOLVE in result.stderr_text ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ ++ def test_server_invalid_bindpw_valid(self): ++ """Test ipa-join with invalid server and valid OTP.""" ++ tasks.kinit_admin(self.clients[0]) ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--server={INVALID_SERVER}', ++ f'--bindpw={OTP}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_ROOT_DN_ERROR ++ assert ERR_UNABLE_ROOT_DN in result.stderr_text ++ ++ def test_server_invalid_keytab_with_kerberos(self): ++ """Test ipa-join with invalid server and keytab.""" ++ tasks.kinit_admin(self.clients[0]) ++ try: ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--server={INVALID_SERVER}', ++ f'--keytab={TEST_KEYTAB}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_RESOLVE_ERROR ++ assert ERR_COULD_NOT_RESOLVE in result.stderr_text ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ ++ def test_server_valid_only_with_kerberos(self): ++ """Test ipa-join with valid server only.""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname) ++ ++ try: ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--server={self.master.hostname}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_server_valid_bindpw_invalid(self): ++ """Test ipa-join with valid server and invalid bindpw.""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--server={self.master.hostname}', ++ f'--bindpw={INVALID_PASSWORD}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_SASL_BIND_FAILED ++ assert ERR_SASL_BIND_FAILED in result.stderr_text ++ ++ def test_server_valid_bindpw_valid(self): ++ """Test ipa-join with valid server and valid OTP.""" ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--server={self.master.hostname}', ++ f'--bindpw={OTP}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_server_valid_keytab_with_kerberos(self): ++ """Test ipa-join with valid server and keytab.""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname) ++ ++ try: ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--server={self.master.hostname}', ++ f'--keytab={TEST_KEYTAB}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_bindpw_invalid_only(self): ++ """Test ipa-join with invalid bindpw only.""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--bindpw={INVALID_PASSWORD}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_SASL_BIND_FAILED ++ assert ERR_SASL_BIND_FAILED in result.stderr_text ++ ++ # ========================================================================= ++ # OTP (One-Time Password) tests ++ # ========================================================================= ++ ++ def test_otp_empty_password(self): ++ """Test ipa-join with empty OTP password (ipa_otp_1001).""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ '--bindpw=', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_ROOT_DN_ERROR ++ assert ERR_UNAUTHENTICATED_BIND in result.stderr_text ++ ++ def test_otp_wrong_password(self): ++ """Test ipa-join with wrong OTP password (ipa_otp_1002).""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--bindpw={INVALID_PASSWORD}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_SASL_BIND_FAILED ++ assert ERR_SASL_BIND_FAILED in result.stderr_text ++ ++ def test_otp_valid_password(self): ++ """Test ipa-join with valid OTP password (ipa_otp_1003).""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ try: ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--bindpw={OTP}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ def test_otp_reuse_fails(self): ++ """Test that reusing the same OTP fails (ipa_otp_1004).""" ++ tasks.kinit_admin(self.clients[0]) ++ tasks.kinit_admin(self.master) ++ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) ++ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) ++ try: ++ # First use should succeed ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--bindpw={OTP}' ++ ) ++ assert result.returncode == EXIT_SUCCESS ++ finally: ++ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) ++ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) ++ ++ # Second use of same OTP should fail ++ result = tasks.ipa_join( ++ self.clients[0], ++ f'--hostname={self.clients[0].hostname}', ++ f'--bindpw={OTP}', ++ raiseonerr=False ++ ) ++ ++ assert result.returncode == EXIT_SASL_BIND_FAILED ++ assert ERR_SASL_BIND_FAILED in result.stderr_text +-- +2.52.0 + diff --git a/0009-fetch_domains-Use-case-insensitive-comparison-for-do.patch b/0009-fetch_domains-Use-case-insensitive-comparison-for-do.patch new file mode 100644 index 0000000..3fdbc7e --- /dev/null +++ b/0009-fetch_domains-Use-case-insensitive-comparison-for-do.patch @@ -0,0 +1,48 @@ +From 7f6a2835f0972af5e94b58daf47fa60bfade4279 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Thu, 22 Jan 2026 10:02:11 +0100 +Subject: [PATCH] fetch_domains: Use case-insensitive comparison for domains + names + +The fetch_domains method is using netr_DsRGetForestTrustInformation +to retrieve the forest trust information. The returned data contains +domain entries, with a DNS domain name that can contain mixed case +(for instance adDomain.Test). +The method compares the domain name with the provided parameter in a +case sensitive comparison, while it should use a case-insensitive +method (DNS names are case-insensitive). + +Fix the method and compare the lowercase value instead. + +Fixes: https://pagure.io/freeipa/issue/9924 +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Alexander Bokovoy +--- + ipaserver/dcerpc.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py +index 1182f128b4988bc699fe7a40d4834f1bead82cf5..5c05ffedb889e774e342cb6cb85ff954d06ac5e9 100644 +--- a/ipaserver/dcerpc.py ++++ b/ipaserver/dcerpc.py +@@ -1635,7 +1635,7 @@ def fetch_domains(api, mydomain, trustdomain, creds=None, server=None): + t.forest_trust_data.netbios_domain_name.string + + tname = unicode(t.forest_trust_data.dns_domain_name.string) +- if tname != trustdomain: ++ if tname.lower() != trustdomain.lower(): + result['domains'][tname] = { + 'cn': tname, + 'ipantflatname': unicode( +@@ -1647,7 +1647,7 @@ def fetch_domains(api, mydomain, trustdomain, creds=None, server=None): + record.data.string = t.forest_trust_data.string + + tname = unicode(t.forest_trust_data.string) +- if tname == trustdomain: ++ if tname.lower() == trustdomain.lower(): + continue + + result['suffixes'][tname] = {'cn': tname} +-- +2.52.0 + diff --git a/0010-Handle-IPACertificate-types-in-xmlrpc.patch b/0010-Handle-IPACertificate-types-in-xmlrpc.patch new file mode 100644 index 0000000..1ac2456 --- /dev/null +++ b/0010-Handle-IPACertificate-types-in-xmlrpc.patch @@ -0,0 +1,43 @@ +From 8deb4be0962b25dfd43e1245307a8bb9d58cfc48 Mon Sep 17 00:00:00 2001 +From: Rob Crittenden +Date: Tue, 3 Feb 2026 09:46:25 -0500 +Subject: [PATCH] Handle IPACertificate types in xmlrpc + +The wrapping code didn't understand the IPACertificate class +so retrieving any entry that contained one would fail. + +Treat it the same was as its parent class cryptography.Certificate. + +Fixes: https://pagure.io/freeipa/issue/9935 + +Signed-off-by: Rob Crittenden +Reviewed-By: David Hanina +--- + ipalib/rpc.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/ipalib/rpc.py b/ipalib/rpc.py +index ed35afc965308e03269f05e01400660b207b548d..9773626eb054dd404256267c5fffbba1aa0579dd 100644 +--- a/ipalib/rpc.py ++++ b/ipalib/rpc.py +@@ -56,7 +56,7 @@ from ipalib.errors import (errors_by_code, UnknownError, NetworkError, + XMLRPCMarshallError, JSONError) + from ipalib import errors, capabilities + from ipalib.request import context, Connection +-from ipalib.x509 import Encoding as x509_Encoding ++from ipalib.x509 import Encoding as x509_Encoding, IPACertificate + from ipapython import ipautil + from ipapython import session_storage + from ipapython.cookie import Cookie +@@ -220,7 +220,7 @@ def xml_wrap(value, version): + if isinstance(value, Principal): + return unicode(value) + +- if isinstance(value, crypto_x509.Certificate): ++ if isinstance(value, (crypto_x509.Certificate, IPACertificate)): + return base64.b64encode( + value.public_bytes(x509_Encoding.DER)).decode('ascii') + +-- +2.52.0 + diff --git a/0011-Replace-None-with-when-uninstalling-CA.patch b/0011-Replace-None-with-when-uninstalling-CA.patch new file mode 100644 index 0000000..f3f750d --- /dev/null +++ b/0011-Replace-None-with-when-uninstalling-CA.patch @@ -0,0 +1,159 @@ +From a583b0dc08536a50e10b76e27861864b61906355 Mon Sep 17 00:00:00 2001 +From: David Hanina +Date: Mon, 2 Feb 2026 11:14:48 +0100 +Subject: [PATCH] Replace None with '' when uninstalling CA + +At many places we're obtaining records from a config file and expect the +config to have those keys, but the user may delete the line instead of +setting the value to false, this then leads to failed uninstall. This +patch replaces None with '' at some places that do not check for None. + +Fixes: https://pagure.io/freeipa/issue/9921 +Signed-off-by: David Hanina +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +--- + ipaserver/install/ca.py | 12 ++++-- + ipaserver/install/cainstance.py | 14 ++++++- + ipaserver/install/server/upgrade.py | 3 +- + .../test_integration/test_crlgen_manage.py | 3 +- + .../test_integration/test_uninstallation.py | 41 +++++++++++++++++++ + 5 files changed, 66 insertions(+), 7 deletions(-) + +diff --git a/ipaserver/install/ca.py b/ipaserver/install/ca.py +index 5a026aa4c556f7012552052cd08223746f3c39ae..2e953a567a3a230cb2a5e35192af76c61f8c1047 100644 +--- a/ipaserver/install/ca.py ++++ b/ipaserver/install/ca.py +@@ -340,14 +340,20 @@ def uninstall_crl_check(options): + + try: + crlgen_enabled = ca.is_crlgen_enabled() +- except cainstance.InconsistentCRLGenConfigException: ++ except cainstance.InconsistentCRLGenConfigException as e: + # If config is inconsistent, let's be safe and act as if + # crl gen was enabled ++ print(e) + crlgen_enabled = True + + if crlgen_enabled: +- print("Deleting this server will leave your installation " +- "without a CRL generation master.") ++ if not options.ignore_last_of_role: ++ print("Deleting this server will leave your installation " ++ "without a CRL generation master. Use --ignore-last-of-role " ++ "to bypass this check.") ++ else: ++ print("Deleting this server will leave your installation " ++ "without a CRL generation master.") + if (options.unattended and not options.ignore_last_of_role) or \ + not (options.unattended or ipautil.user_input( + "Are you sure you want to continue with the uninstall " +diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py +index b8267a625554f9375d27160f39b67ee2e64a2dbb..4933ad23d7323859af92bd02f6ae156803e29997 100644 +--- a/ipaserver/install/cainstance.py ++++ b/ipaserver/install/cainstance.py +@@ -1421,12 +1421,22 @@ class CAInstance(DogtagInstance): + try: + cache = directivesetter.get_directive( + self.config, 'ca.crl.MasterCRL.enableCRLCache', '=') ++ ++ if cache is None: ++ raise InconsistentCRLGenConfigException( ++ "Configuration is inconsistent, please check " ++ "ca.crl.MasterCRL.enableCRLCache, " ++ "ca.crl.MasterCRL.enableCRLUpdates and " ++ "ca.listenToCloneModifications in {} and " ++ "run ipa-crlgen-manage [enable|disable] to repair".format( ++ self.config)) ++ + enableCRLCache = cache.lower() == 'true' + updates = directivesetter.get_directive( +- self.config, 'ca.crl.MasterCRL.enableCRLUpdates', '=') ++ self.config, 'ca.crl.MasterCRL.enableCRLUpdates', '=') or '' + enableCRLUpdates = updates.lower() == 'true' + listen = directivesetter.get_directive( +- self.config, 'ca.listenToCloneModifications', '=') ++ self.config, 'ca.listenToCloneModifications', '=') or '' + enableToClone = listen.lower() == 'true' + updateinterval = directivesetter.get_directive( + self.config, 'ca.certStatusUpdateInterval', '=') +diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py +index 548ee02e1e8524ce0002dca1764d48728eb0509a..8692c983409426193e1746f07fa1a0514621cb4a 100644 +--- a/ipaserver/install/server/upgrade.py ++++ b/ipaserver/install/server/upgrade.py +@@ -1712,7 +1712,8 @@ def upgrade_configuration(): + if ca.is_configured(): + crl = directivesetter.get_directive( + paths.CA_CS_CFG_PATH, 'ca.crl.MasterCRL.enableCRLUpdates', '=') +- sub_dict['CLONE']='#' if crl.lower() == 'true' else '' ++ sub_dict['CLONE'] = '#' if crl is not None and \ ++ crl.lower() == 'true' else '' + + ds_dirname = dsinstance.config_dirname(ds.serverid) + +diff --git a/ipatests/test_integration/test_crlgen_manage.py b/ipatests/test_integration/test_crlgen_manage.py +index c6f41ebf8939bad8006b1e5eaf37bad30dbfd9d8..8a2a28a75b76158fcc61a8e7612f81343336b64f 100644 +--- a/ipatests/test_integration/test_crlgen_manage.py ++++ b/ipatests/test_integration/test_crlgen_manage.py +@@ -302,7 +302,8 @@ class TestCRLGenManage(IntegrationTest): + ['ipa-server-install', '--uninstall', '-U'], raiseonerr=False) + assert result.returncode == 1 + expected_msg = "Deleting this server will leave your installation " \ +- "without a CRL generation master" ++ "without a CRL generation master. Use " \ ++ "--ignore-last-of-role to bypass this check." + assert expected_msg in result.stdout_text + + def test_uninstall_with_ignore_last_of_role(self): +diff --git a/ipatests/test_integration/test_uninstallation.py b/ipatests/test_integration/test_uninstallation.py +index 8d83f72868f5c103b0c31d2aa96630c00b2dfbd8..12b10caa60745dcbc2d811bff65d57fb5d865f09 100644 +--- a/ipatests/test_integration/test_uninstallation.py ++++ b/ipatests/test_integration/test_uninstallation.py +@@ -237,3 +237,44 @@ class TestUninstallReinstall(IntegrationTest): + + def test_reinstall_server(self): + tasks.install_master(self.master, setup_dns=False) ++ ++ ++class TestUninstallCRLGen(IntegrationTest): ++ """Test uninstallation of a replica with broken CRL configuration. ++ ++ Removing ca.crl.MasterCRL.enableCRLCache from CS.cfg crashed. ++ https://pagure.io/freeipa/issue/9921 ++ """ ++ ++ num_replicas = 1 ++ topology = 'line' ++ ++ @classmethod ++ def install(cls, mh): ++ tasks.install_master(cls.master, setup_dns=False) ++ tasks.install_replica(cls.master, cls.replicas[0]) ++ ++ def test_uninstall_replica_with_broken_crlgen(self): ++ self.replicas[0].run_command(['ipa-crlgen-manage', 'enable']) ++ ++ self.replicas[0].run_command([ ++ '/bin/sed', ++ '-i', ++ '/ca.crl.MasterCRL.enableCRLCache=true/d', ++ paths.CA_CS_CFG_PATH, ++ ]) ++ ++ result = self.replicas[0].run_command([ ++ 'ipa-server-install', ++ '--ignore-last-of-role', ++ '--uninstall', '-U', ++ ]) ++ ++ expected_msg = "Configuration is inconsistent, please check " \ ++ "ca.crl.MasterCRL.enableCRLCache, " \ ++ "ca.crl.MasterCRL.enableCRLUpdates and " \ ++ "ca.listenToCloneModifications in {} and run " \ ++ "ipa-crlgen-manage [enable|disable] to repair".format( ++ paths.CA_CS_CFG_PATH) ++ ++ assert expected_msg in result.stdout_text +-- +2.52.0 + diff --git a/0012-ipatests-Add-xmlrpc-tests-for-ipa-delegation-cli.patch b/0012-ipatests-Add-xmlrpc-tests-for-ipa-delegation-cli.patch new file mode 100644 index 0000000..c80a432 --- /dev/null +++ b/0012-ipatests-Add-xmlrpc-tests-for-ipa-delegation-cli.patch @@ -0,0 +1,416 @@ +From 91a6618e51b0e767c5cc5e4b1719531dbbd7268d Mon Sep 17 00:00:00 2001 +From: Sudhir Menon +Date: Thu, 22 Jan 2026 12:43:56 +0530 +Subject: [PATCH] ipatests: Add xmlrpc tests for ipa-delegation-cli + +This patch adds below test cases to the the XML-RPC delegation plugin test suite +coverage of delegation operations and important bug regressions. + +Test cases added: + + Test basic delegation creation with write permission + Test delegation creation with --all flag + Test delegation creation with --raw flag (ACI format) + Test deletion of delegation with ipausers group + Test finding delegation by name criteria + Test finding delegation by membergroup filter + Test showing delegation by name + Test modifying delegation attrs + Test modifying delegation permissions + BZ 783548: Verify mod fails when membergroup doesn't exist + BZ 783554: Verify mod with empty attrs fails properly + BZ 888524: Verify find --group option works correctly + +Fixes: https://pagure.io/freeipa/issue/9931 +Assisted-by: Claude +Signed-off-by: Sudhir Menon +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +Reviewed-By: Anuja More +--- + .../test_xmlrpc/test_delegation_plugin.py | 371 ++++++++++++++++++ + 1 file changed, 371 insertions(+) + +diff --git a/ipatests/test_xmlrpc/test_delegation_plugin.py b/ipatests/test_xmlrpc/test_delegation_plugin.py +index b3d2aadbddbaaff6f40e1046e4df32bcc9ee7e2d..9245f259e21cad166c3c5b0565da3bb56a341e6b 100644 +--- a/ipatests/test_xmlrpc/test_delegation_plugin.py ++++ b/ipatests/test_xmlrpc/test_delegation_plugin.py +@@ -333,4 +333,375 @@ class test_delegation(Declarative): + summary=u'Deleted delegation "%s"' % delegation1, + ) + ), ++ ++ ++ dict( ++ desc='Create delegation with mobile attr and write permission', ++ command=( ++ 'delegation_add', [u'test_mobile_delegation'], dict( ++ attrs=[u'mobile'], ++ permissions=u'write', ++ group=u'editors', ++ memberof=u'admins', ++ ) ++ ), ++ expected=dict( ++ value=u'test_mobile_delegation', ++ summary=u'Added delegation "test_mobile_delegation"', ++ result=dict( ++ attrs=[u'mobile'], ++ permissions=[u'write'], ++ aciname=u'test_mobile_delegation', ++ group=u'editors', ++ memberof=member1, ++ ), ++ ), ++ ), ++ ++ ++ dict( ++ desc='Create delegation with --all flag', ++ command=( ++ 'delegation_add', [u'test_all_flag'], dict( ++ attrs=[u'mobile'], ++ permissions=u'write', ++ group=u'editors', ++ memberof=u'admins', ++ all=True, ++ ) ++ ), ++ expected=dict( ++ value=u'test_all_flag', ++ summary=u'Added delegation "test_all_flag"', ++ result=dict( ++ attrs=[u'mobile'], ++ permissions=[u'write'], ++ aciname=u'test_all_flag', ++ group=u'editors', ++ memberof=member1, ++ ), ++ ), ++ ), ++ ++ ++ dict( ++ desc='Create delegation with --raw flag', ++ command=( ++ 'delegation_add', [u'test_raw_flag'], dict( ++ attrs=[u'mobile'], ++ permissions=u'write', ++ group=u'editors', ++ memberof=u'admins', ++ raw=True, ++ ) ++ ), ++ expected=dict( ++ value=u'test_raw_flag', ++ summary=u'Added delegation "test_raw_flag"', ++ result={ ++ 'aci': u'(targetattr = "mobile")(targetfilter = ' ++ u'"(memberOf=%s)")(version 3.0;acl ' ++ u'"delegation:test_raw_flag";allow (write) ' ++ u'groupdn = "ldap:///%s";)' % ( ++ DN(('cn', 'admins'), ('cn', 'groups'), ++ ('cn', 'accounts'), api.env.basedn), ++ DN(('cn', 'editors'), ('cn', 'groups'), ++ ('cn', 'accounts'), api.env.basedn)) ++ }, ++ ), ++ ), ++ ++ ++ dict( ++ desc='Delete test_mobile_delegation', ++ command=('delegation_del', [u'test_mobile_delegation'], {}), ++ expected=dict( ++ result=True, ++ value=u'test_mobile_delegation', ++ summary=u'Deleted delegation "test_mobile_delegation"', ++ ) ++ ), ++ ++ ++ dict( ++ desc='Delete test_all_flag', ++ command=('delegation_del', [u'test_all_flag'], {}), ++ expected=dict( ++ result=True, ++ value=u'test_all_flag', ++ summary=u'Deleted delegation "test_all_flag"', ++ ) ++ ), ++ ++ ++ dict( ++ desc='Delete test_raw_flag', ++ command=('delegation_del', [u'test_raw_flag'], {}), ++ expected=dict( ++ result=True, ++ value=u'test_raw_flag', ++ summary=u'Deleted delegation "test_raw_flag"', ++ ) ++ ), ++ ++ ++ dict( ++ desc='Create delegation for ipausers group', ++ command=( ++ 'delegation_add', [u'delegation_del_positive_1001'], dict( ++ attrs=[u'mobile'], ++ group=u'ipausers', ++ memberof=u'admins', ++ ) ++ ), ++ expected=dict( ++ value=u'delegation_del_positive_1001', ++ summary=u'Added delegation "delegation_del_positive_1001"', ++ result=dict( ++ attrs=[u'mobile'], ++ permissions=[u'write'], ++ aciname=u'delegation_del_positive_1001', ++ group=u'ipausers', ++ memberof=member1, ++ ), ++ ), ++ ), ++ ++ ++ dict( ++ desc='Delete delegation_del_positive_1001', ++ command=('delegation_del', [u'delegation_del_positive_1001'], {}), ++ expected=dict( ++ result=True, ++ value=u'delegation_del_positive_1001', ++ summary=u'Deleted delegation "delegation_del_positive_1001"', ++ ) ++ ), ++ ++ ++ dict( ++ desc='Create delegation for find, show, and mod tests', ++ command=( ++ 'delegation_add', [u'delegation_find_show_mod_test'], dict( ++ attrs=[u'mobile'], ++ permissions=u'write', ++ group=u'editors', ++ memberof=u'admins', ++ ) ++ ), ++ expected=dict( ++ value=u'delegation_find_show_mod_test', ++ summary=u'Added delegation "delegation_find_show_mod_test"', ++ result=dict( ++ attrs=[u'mobile'], ++ permissions=[u'write'], ++ aciname=u'delegation_find_show_mod_test', ++ group=u'editors', ++ memberof=member1, ++ ), ++ ), ++ ), ++ ++ ++ dict( ++ desc='Find delegation by name', ++ command=('delegation_find', [u'delegation_find_show_mod_test'], {}), ++ expected=dict( ++ count=1, ++ truncated=False, ++ summary=u'1 delegation matched', ++ result=[ ++ { ++ 'attrs': [u'mobile'], ++ 'permissions': [u'write'], ++ 'aciname': u'delegation_find_show_mod_test', ++ 'group': u'editors', ++ 'memberof': member1, ++ }, ++ ], ++ ), ++ ), ++ ++ ++ dict( ++ desc='Find delegation by membergroup', ++ command=('delegation_find', [], {'memberof': member1}), ++ expected=dict( ++ count=1, ++ truncated=False, ++ summary=u'1 delegation matched', ++ result=[ ++ { ++ 'attrs': [u'mobile'], ++ 'permissions': [u'write'], ++ 'aciname': u'delegation_find_show_mod_test', ++ 'group': u'editors', ++ 'memberof': member1, ++ }, ++ ], ++ ), ++ ), ++ ++ ++ dict( ++ desc='Show delegation by name', ++ command=('delegation_show', [u'delegation_find_show_mod_test'], {}), ++ expected=dict( ++ value=u'delegation_find_show_mod_test', ++ summary=None, ++ result={ ++ 'attrs': [u'mobile'], ++ 'permissions': [u'write'], ++ 'aciname': u'delegation_find_show_mod_test', ++ 'group': u'editors', ++ 'memberof': member1, ++ }, ++ ), ++ ), ++ ++ ++ dict( ++ desc='Modify delegation attrs', ++ command=( ++ 'delegation_mod', [u'delegation_find_show_mod_test'], ++ dict(attrs=[u'l']) ++ ), ++ expected=dict( ++ value=u'delegation_find_show_mod_test', ++ summary=u'Modified delegation "delegation_find_show_mod_test"', ++ result=dict( ++ attrs=[u'l'], ++ permissions=[u'write'], ++ aciname=u'delegation_find_show_mod_test', ++ group=u'editors', ++ memberof=member1, ++ ), ++ ), ++ ), ++ ++ ++ dict( ++ desc='Modify delegation permissions', ++ command=( ++ 'delegation_mod', [u'delegation_find_show_mod_test'], ++ dict(permissions=u'read') ++ ), ++ expected=dict( ++ value=u'delegation_find_show_mod_test', ++ summary=u'Modified delegation "delegation_find_show_mod_test"', ++ result=dict( ++ attrs=[u'l'], ++ permissions=[u'read'], ++ aciname=u'delegation_find_show_mod_test', ++ group=u'editors', ++ memberof=member1, ++ ), ++ ), ++ ), ++ ++ ++ dict( ++ desc='Delete delegation_find_show_mod_test', ++ command=('delegation_del', [u'delegation_find_show_mod_test'], {}), ++ expected=dict( ++ result=True, ++ value=u'delegation_find_show_mod_test', ++ summary=u'Deleted delegation "delegation_find_show_mod_test"', ++ ) ++ ), ++ ++ ++ dict( ++ desc='Create delegation for BZ tests', ++ command=( ++ 'delegation_add', [u'delegation_bz_test'], dict( ++ attrs=[u'mobile'], ++ permissions=u'write', ++ group=u'ipausers', ++ memberof=u'admins', ++ ) ++ ), ++ expected=dict( ++ value=u'delegation_bz_test', ++ summary=u'Added delegation "delegation_bz_test"', ++ result=dict( ++ attrs=[u'mobile'], ++ permissions=[u'write'], ++ aciname=u'delegation_bz_test', ++ group=u'ipausers', ++ memberof=member1, ++ ), ++ ), ++ ), ++ ++ ++ dict( ++ desc='Try to modify with non-existent membergroup (BZ 783548)', ++ command=( ++ 'delegation_mod', [u'delegation_bz_test'], ++ dict(memberof=u'badmembergroup') ++ ), ++ expected=errors.NotFound( ++ reason=u'badmembergroup: group not found'), ++ ), ++ ++ ++ dict( ++ desc='Try to modify attrs with empty value (BZ 783554)', ++ command=( ++ 'delegation_mod', [u'delegation_bz_test'], dict(attrs=u'') ++ ), ++ expected=errors.RequirementError(name='attrs'), ++ ), ++ ++ ++ dict( ++ desc='Modify attrs to prepare for next BZ test', ++ command=( ++ 'delegation_mod', [u'delegation_bz_test'], dict(attrs=[u'l']) ++ ), ++ expected=dict( ++ value=u'delegation_bz_test', ++ summary=u'Modified delegation "delegation_bz_test"', ++ result=dict( ++ attrs=[u'l'], ++ permissions=[u'write'], ++ aciname=u'delegation_bz_test', ++ group=u'ipausers', ++ memberof=member1, ++ ), ++ ), ++ ), ++ ++ ++ dict( ++ desc='Find delegation by group filter (BZ 888524)', ++ command=('delegation_find', [], {'group': u'ipausers'}), ++ expected=dict( ++ count=1, ++ truncated=False, ++ summary=u'1 delegation matched', ++ result=[ ++ { ++ 'attrs': [u'l'], ++ 'permissions': [u'write'], ++ 'aciname': u'delegation_bz_test', ++ 'group': u'ipausers', ++ 'memberof': member1, ++ }, ++ ], ++ ), ++ ), ++ ++ ++ dict( ++ desc='Delete delegation_bz_test', ++ command=('delegation_del', [u'delegation_bz_test'], {}), ++ expected=dict( ++ result=True, ++ value=u'delegation_bz_test', ++ summary=u'Deleted delegation "delegation_bz_test"', ++ ) ++ ), ++ + ] +-- +2.52.0 + diff --git a/0013-ipa-join-initialize-pointer.patch b/0013-ipa-join-initialize-pointer.patch new file mode 100644 index 0000000..edf13e6 --- /dev/null +++ b/0013-ipa-join-initialize-pointer.patch @@ -0,0 +1,49 @@ +From 7cc96e42683a6d3ec9f2dc2a19e99330b6f3ce58 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Wed, 4 Feb 2026 09:21:14 +0100 +Subject: [PATCH] ipa-join: initialize pointer + +OpenScanHub detected an uninitialized pointer in ipa_join: +Slapi_DN *sdn; +... +if (sdn) slapi_sdn_free(&sdn); + +Initialize to NULL +Also initialize Slapi_Backend *be=NULL and char * filter=NULL +to avoid potential issues. + +Fixes: https://pagure.io/freeipa/issue/9936 +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +Reviewed-By: Rafael Guterres Jeffman +--- + daemons/ipa-slapi-plugins/ipa-enrollment/ipa_enrollment.c | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/daemons/ipa-slapi-plugins/ipa-enrollment/ipa_enrollment.c b/daemons/ipa-slapi-plugins/ipa-enrollment/ipa_enrollment.c +index 3a70dd0a5594fc623e7e808ab8a734349a748a49..2f8923e10310a8a6e19ac701070d6451915c3be3 100644 +--- a/daemons/ipa-slapi-plugins/ipa-enrollment/ipa_enrollment.c ++++ b/daemons/ipa-slapi-plugins/ipa-enrollment/ipa_enrollment.c +@@ -129,8 +129,8 @@ ipa_join(Slapi_PBlock *pb) + Slapi_PBlock *pbte = NULL; + Slapi_PBlock *pbtm = NULL; + Slapi_Entry *targetEntry=NULL; +- Slapi_DN *sdn; +- Slapi_Backend *be; ++ Slapi_DN *sdn=NULL; ++ Slapi_Backend *be=NULL; + Slapi_Entry **es = NULL; + int rc=0, ret=0, res; + size_t i; +@@ -139,7 +139,7 @@ ipa_join(Slapi_PBlock *pb) + char *fqdn = NULL; + Slapi_Mods *smods = NULL; + char *attrlist[] = {"fqdn", "krbPrincipalKey", "krbLastPwdChange", "krbPrincipalName", NULL }; +- char * filter; ++ char * filter=NULL; + + int scope = LDAP_SCOPE_SUBTREE; + char *principal = NULL; +-- +2.52.0 + diff --git a/freeipa.spec b/freeipa.spec index 1d710e6..7a25e4e 100644 --- a/freeipa.spec +++ b/freeipa.spec @@ -234,7 +234,7 @@ Name: %{package_name} Version: %{IPA_VERSION} -Release: 1%{?rc_version:.%rc_version}%{?dist} +Release: 2%{?rc_version:.%rc_version}%{?dist} Summary: The Identity, Policy and Audit system License: GPL-3.0-or-later @@ -262,6 +262,19 @@ Source2: gpgkey-0E63D716D76AC080A4A33513F40800B6298EB963.asc # RHEL spec file only: START %if %{NON_DEVELOPER_BUILD} %if 0%{?rhel} >= 9 +Patch0001: 0001-ipatests-Move-expire_password-fixture-into-TestIPACo.patch +Patch0002: 0002-ipatests-Fix-xfail-assertion-for-sssd-2.12.0.patch +Patch0003: 0003-ipatests-Add-DNS-functional-integration-tests.patch +Patch0004: 0004-ipa-advise-smart-card-client-script-does-not-need-kr.patch +Patch0005: 0005-ipatests-Fix-resolver-state-tracking-in-enforced-DNS.patch +Patch0006: 0006-freeipa.spec.in-Use-systemd-sysusers-to-setup-users-.patch +Patch0007: 0007-ipatests-add-Random-Password-based-replica-promotion.patch +Patch0008: 0008-ipatests-Add-integration-tests-for-ipa-join-command.patch +Patch0009: 0009-fetch_domains-Use-case-insensitive-comparison-for-do.patch +Patch0010: 0010-Handle-IPACertificate-types-in-xmlrpc.patch +Patch0011: 0011-Replace-None-with-when-uninstalling-CA.patch +Patch0012: 0012-ipatests-Add-xmlrpc-tests-for-ipa-delegation-cli.patch +Patch0013: 0013-ipa-join-initialize-pointer.patch Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch %endif %endif @@ -601,7 +614,7 @@ Requires: systemd-units >= %{systemd_version} Requires: system-logos-ipa >= 80.4 %endif -# The list below is automatically generated by `fix-spec.sh -i` +# The list below is automatically generated by `fix-spec.sh -i` # from the install/freeipa-webui Provides: bundled(npm(attr-accept)) = 2.2.5 Provides: bundled(npm(cookie)) = 1.0.2 @@ -1247,6 +1260,7 @@ fi /bin/systemctl reload-or-try-restart dbus /bin/systemctl reload-or-try-restart oddjobd +%sysusers_create %{_sysusersdir}/ipa.conf %tmpfiles_create ipa.conf %journal_catalog_update @@ -1304,18 +1318,6 @@ if [ -e /usr/sbin/ipa_kpasswd ]; then fi -%pre server-common -# create users and groups -# create kdcproxy group and user -getent group kdcproxy >/dev/null || groupadd -f -r kdcproxy -getent passwd kdcproxy >/dev/null || useradd -r -g kdcproxy -s /sbin/nologin -d / -c "IPA KDC Proxy User" kdcproxy -# create ipaapi group and user -getent group ipaapi >/dev/null || groupadd -f -r ipaapi -getent passwd ipaapi >/dev/null || useradd -r -g ipaapi -s /sbin/nologin -d / -c "IPA Framework User" ipaapi -# add apache to ipaaapi group -id -Gn apache | grep '\bipaapi\b' >/dev/null || usermod apache -a -G ipaapi - - %post server-dns %systemd_post ipa-dnskeysyncd.service ipa-ods-exporter.socket ipa-ods-exporter.service @@ -1702,6 +1704,7 @@ fi %dir %attr(0755,root,root) %{_sysconfdir}/ipa/kdcproxy %config(noreplace) %{_sysconfdir}/ipa/kdcproxy/kdcproxy.conf # NOTE: systemd specific section +%{_sysusersdir}/ipa.conf %{_tmpfilesdir}/ipa.conf %attr(644,root,root) %{_unitdir}/ipa-custodia.service %ghost %attr(644,root,root) %{etc_systemd_dir}/httpd.d/ipa.conf @@ -1979,6 +1982,14 @@ fi %endif %changelog +* Fri Feb 06 2026 Florence Blanc-Renaud - 4.13.1-2 +- Resolves: RHEL-146023 When using xmlrpc, ipa server failed with assert type(value) in (unicode, float, int, bool, type(None)) +-Resolves: RHEL-145855 Include latest fixes in python3-ipatests package +-Resolves: RHEL-88855 ipa uninstallation is failing with message "'NoneType' object has no attribute 'lower'" +-Resolves: RHEL-43143 ipa-advise client script requires keytab (should just require root access on client system) +-Resolves: RHEL-4895 ipa use systemd-sysusers +-Resolves: RHEL-4823 Names of domains from a trusted forest should be compared case-insentive + * Fri Jan 16 2026 Florence Blanc-Renaud - 4.13.1-1 - Resolves: RHEL-140587 Support replaceable WebUI artwork for RHEL and CentOS - Resolves: RHEL-113778 Command that retrieve and install new CA certificates