diff --git a/SOURCES/0133-ipatests-remove-xfail-for-PKI-11.7.patch b/SOURCES/0133-ipatests-remove-xfail-for-PKI-11.7.patch new file mode 100644 index 0000000..b13e242 --- /dev/null +++ b/SOURCES/0133-ipatests-remove-xfail-for-PKI-11.7.patch @@ -0,0 +1,37 @@ +From b923355ff04dd88b1530d0bb2e032280afc5d315 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Tue, 26 Aug 2025 09:00:48 +0200 +Subject: [PATCH] ipatests: remove xfail for PKI 11.7 + +The test test_ca_show_error_handling is green with PKI 11.7 +because the PKI regression has been fixed. +Update the xfail condition to 11.5 <= version < 11.7. + +Fixes: https://pagure.io/freeipa/issue/9606 +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: Alexander Bokovoy +Reviewed-By: Rob Crittenden +--- + ipatests/test_integration/test_cert.py | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/ipatests/test_integration/test_cert.py b/ipatests/test_integration/test_cert.py +index 05b20b910b249af24039a497538f96dad07162aa..84adf2ceafe013e6cfc973fb2cb650c40f36971d 100644 +--- a/ipatests/test_integration/test_cert.py ++++ b/ipatests/test_integration/test_cert.py +@@ -558,8 +558,10 @@ class TestCAShowErrorHandling(IntegrationTest): + ) + error_msg = 'ipa: ERROR: The certificate for ' \ + '{} is not available on this server.'.format(lwca) +- bad_version = (tasks.get_pki_version(self.master) +- >= tasks.parse_version('11.5.0')) ++ pki_version = tasks.get_pki_version(self.master) ++ # The regression was introduced in 11.5 and fixed in 11.7 ++ bad_version = (tasks.parse_version('11.5.0') <= pki_version ++ < tasks.parse_version('11.7.0')) + with xfail_context(bad_version, + reason="https://pagure.io/freeipa/issue/9606"): + assert error_msg in result.stderr_text +-- +2.52.0 + diff --git a/SOURCES/0134-GetEntryFromLDIF-handle-DNs-case-insensitive.patch b/SOURCES/0134-GetEntryFromLDIF-handle-DNs-case-insensitive.patch new file mode 100644 index 0000000..d4ff60b --- /dev/null +++ b/SOURCES/0134-GetEntryFromLDIF-handle-DNs-case-insensitive.patch @@ -0,0 +1,53 @@ +From 0a5509665485baa5190f2e3c6fdd765c966a4405 Mon Sep 17 00:00:00 2001 +From: Alexander Bokovoy +Date: Mon, 15 Sep 2025 09:41:31 +0300 +Subject: [PATCH] GetEntryFromLDIF: handle DNs case-insensitive + +LDAP expects case-insensitive DNs, so modify LDIF parser to +compare DNs as case-insensitive strings and use case-preserving but +case-insensitive dictionary. + +Fixes: https://pagure.io/freeipa/issue/9854 + +Signed-off-by: Alexander Bokovoy +Reviewed-By: Florence Blanc-Renaud +--- + ipaserver/install/upgradeinstance.py | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/ipaserver/install/upgradeinstance.py b/ipaserver/install/upgradeinstance.py +index b84f50b059c5d9b19719495ec10f06f67e4a7c8a..ecd0a08ea1ef18bf1225ef339ed0e17a9666e4f3 100644 +--- a/ipaserver/install/upgradeinstance.py ++++ b/ipaserver/install/upgradeinstance.py +@@ -29,7 +29,7 @@ import traceback + from ipalib import api + from ipaplatform.paths import paths + from ipaplatform import services +-from ipapython import ipaldap ++from ipapython import ipaldap, ipautil + + from ipaserver.install import installutils + from ipaserver.install import schemaupdate +@@ -57,8 +57,8 @@ class GetEntryFromLDIF(ldif.LDIFParser): + returned if list is empty. + """ + ldif.LDIFParser.__init__(self, input_file) +- self.entries_dn = entries_dn +- self.results = {} ++ self.entries_dn = [e.lower() for e in entries_dn] ++ self.results = ipautil.CIDict() + + def get_results(self): + """ +@@ -67,7 +67,7 @@ class GetEntryFromLDIF(ldif.LDIFParser): + return self.results + + def handle(self, dn, entry): +- if self.entries_dn and dn not in self.entries_dn: ++ if self.entries_dn and dn.lower() not in self.entries_dn: + return + + self.results[dn] = entry +-- +2.52.0 + diff --git a/SOURCES/0135-Tests-xmlrpc-mark-xfail-tests-requesting-cert-with-s.patch b/SOURCES/0135-Tests-xmlrpc-mark-xfail-tests-requesting-cert-with-s.patch new file mode 100644 index 0000000..756d878 --- /dev/null +++ b/SOURCES/0135-Tests-xmlrpc-mark-xfail-tests-requesting-cert-with-s.patch @@ -0,0 +1,70 @@ +From a1f2fef8c9f1c61392faf3e96a9e89b124221ba9 Mon Sep 17 00:00:00 2001 +From: Florence Blanc-Renaud +Date: Tue, 30 Sep 2025 09:46:09 +0200 +Subject: [PATCH] Tests xmlrpc: mark xfail tests requesting cert with subca + +With PKI 11.7, requesting a cert with a subca fails. +Mark the tests as xfail for now. + +Related: RHEL-108293 +Signed-off-by: Florence Blanc-Renaud +Reviewed-By: David Hanina +--- + ipatests/test_xmlrpc/test_caacl_profile_enforcement.py | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/ipatests/test_xmlrpc/test_caacl_profile_enforcement.py b/ipatests/test_xmlrpc/test_caacl_profile_enforcement.py +index 98653904f91d6b2d670f04f32de9849250c1cb25..df20c55c5a62394eb085ea0bcca04a33e03a1fe9 100644 +--- a/ipatests/test_xmlrpc/test_caacl_profile_enforcement.py ++++ b/ipatests/test_xmlrpc/test_caacl_profile_enforcement.py +@@ -306,6 +306,7 @@ class TestCertSignMIMEwithSubCA(XMLRPC_test): + def test_add_group_to_acl(self, smime_group, smime_acl): + smime_acl.add_user(group=smime_group) + ++ @pytest.mark.xfail(reason='pki RHEL-108293') + def test_sign_smime_csr(self, smime_profile, smime_user, smime_signing_ca): + csr = generate_user_csr(smime_user) + with change_principal(smime_user, SMIME_USER_PW): +@@ -313,6 +314,7 @@ class TestCertSignMIMEwithSubCA(XMLRPC_test): + profile_id=smime_profile.name, + cacn=smime_signing_ca.name) + ++ @pytest.mark.xfail(reason='pki RHEL-108293') + def test_sign_smime_csr_full_principal( + self, smime_profile, smime_user, smime_signing_ca): + csr = generate_user_csr(smime_user) +@@ -322,6 +324,7 @@ class TestCertSignMIMEwithSubCA(XMLRPC_test): + profile_id=smime_profile.name, + cacn=smime_signing_ca.name) + ++ @pytest.mark.xfail(reason='pki RHEL-108293') + def test_verify_cert_issuer_dn_is_subca( + self, smime_profile, smime_user, smime_signing_ca): + csr = generate_user_csr(smime_user) +@@ -541,6 +544,7 @@ class TestPrincipalAliasForSubjectAltNameDnsName(SubjectAltNameOneServiceBase): + santest_service_host_1.name, + santest_service_host_2.name) + ++ @pytest.mark.xfail(reason='pki RHEL-108293') + def test_request_cert_with_SAN_matching_principal_alias( + self, santest_subca, santest_host_1, + santest_service_host_1, santest_csr): +@@ -604,6 +608,7 @@ class TestSignServiceCertManagedByMultipleHosts(CAACLEnforcementOnCertBase): + santest_subca_acl.add_host(santest_host_2.name) + santest_subca_acl.add_service(santest_service_host_2.name) + ++ @pytest.mark.xfail(reason='pki RHEL-108293') + def test_request_cert_with_additional_host( + self, santest_subca, santest_host_1, santest_host_2, + santest_service_host_1, santest_csr): +@@ -696,6 +701,7 @@ class TestManagedByACIOnCertRequest(CAACLEnforcementOnCertBase): + cacn=santest_subca.name + ) + ++ @pytest.mark.xfail(reason='pki RHEL-108293') + def test_issuing_service_cert_by_related_host(self, + santest_subca, + santest_host_1, +-- +2.52.0 + diff --git a/SOURCES/0136-Manual-backport-of-8002.patch b/SOURCES/0136-Manual-backport-of-8002.patch new file mode 100644 index 0000000..0d60d0f --- /dev/null +++ b/SOURCES/0136-Manual-backport-of-8002.patch @@ -0,0 +1,3762 @@ +From 39481b84c20a2e88cedc96bd2f5d8fbac692383a Mon Sep 17 00:00:00 2001 +From: PRANAV THUBE +Date: Mon, 22 Dec 2025 16:55:57 +0530 +Subject: [PATCH] Manual backport of #8002 + +Reviewed-By: Florence Blanc-Renaud +Reviewed-By: Rob Crittenden +Reviewed-By: Anuja More +--- + ipatests/pytest_ipa/integration/tasks.py | 148 + + .../test_integration/test_hbac_functional.py | 3580 +++++++++++++++++ + 2 files changed, 3728 insertions(+) + create mode 100644 ipatests/test_integration/test_hbac_functional.py + +diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py +index 5c8cc119097ec8862b0d5b0a0cf7621c853368c6..7def8fe3962487f28999cb975aff512cd36331fe 100755 +--- a/ipatests/pytest_ipa/integration/tasks.py ++++ b/ipatests/pytest_ipa/integration/tasks.py +@@ -2145,6 +2145,154 @@ def group_add_member(host, groupname, users=None, + return host.run_command(cmd, raiseonerr=raiseonerr) + + ++def hbacrule_add(host, rulename, extra_args=()): ++ cmd = [ ++ "ipa", "hbacrule-add", rulename, ++ ] ++ cmd.extend(extra_args) ++ return host.run_command(cmd) ++ ++ ++def hbacrule_add_user(host, rulename, users=None, groups=None, ++ raiseonerr=True, extra_args=()): ++ cmd = [ ++ "ipa", "hbacrule-add-user", rulename ++ ] ++ if users: ++ cmd.append(f"--users={users}") ++ if groups: ++ cmd.append(f"--groups={groups}") ++ cmd.extend(extra_args) ++ return host.run_command(cmd, raiseonerr=raiseonerr) ++ ++ ++def hbacrule_add_host(host, rulename, hosts=None, ++ raiseonerr=True, extra_args=()): ++ cmd = [ ++ "ipa", "hbacrule-add-host", rulename ++ ] ++ if hosts: ++ cmd.append("--hosts") ++ cmd.append(hosts) ++ cmd.extend(extra_args) ++ return host.run_command(cmd, raiseonerr=raiseonerr) ++ ++ ++def hbacrule_add_service(host, rulename, services=None, ++ raiseonerr=True, extra_args=()): ++ cmd = [ ++ "ipa", "hbacrule-add-service", rulename ++ ] ++ if services: ++ cmd.append("--hbacsvcs") ++ cmd.append(services) ++ cmd.extend(extra_args) ++ return host.run_command(cmd, raiseonerr=raiseonerr) ++ ++ ++def hostgroup_add(host, groupname, extra_args=()): ++ cmd = [ ++ "ipa", "hostgroup-add", groupname, ++ ] ++ cmd.extend(extra_args) ++ return host.run_command(cmd) ++ ++ ++def hostgroup_add_member(host, groupname, hosts=None, ++ raiseonerr=True, extra_args=()): ++ cmd = [ ++ "ipa", "hostgroup-add-member", groupname ++ ] ++ if hosts: ++ cmd.append("--hosts") ++ cmd.append(hosts) ++ cmd.extend(extra_args) ++ return host.run_command(cmd, raiseonerr=raiseonerr) ++ ++ ++def hbacrule_show(host, rulename, extra_args=()): ++ cmd = [ ++ "ipa", "hbacrule-show", rulename ++ ] ++ cmd.extend(extra_args) ++ return host.run_command(cmd) ++ ++ ++def hbacrule_remove_user(host, rulename, users=None, groups=None, ++ raiseonerr=True, extra_args=()): ++ cmd = [ ++ "ipa", "hbacrule-remove-user", rulename ++ ] ++ if users: ++ cmd.append(f"--users={users}") ++ if groups: ++ cmd.append(f"--groups={groups}") ++ cmd.extend(extra_args) ++ return host.run_command(cmd, raiseonerr=raiseonerr) ++ ++ ++def hbacrule_remove_service(host, rulename, services=None, ++ raiseonerr=True, extra_args=()): ++ cmd = [ ++ "ipa", "hbacrule-remove-service", rulename ++ ] ++ if services: ++ cmd.append("--hbacsvcs") ++ cmd.append(services) ++ cmd.extend(extra_args) ++ return host.run_command(cmd, raiseonerr=raiseonerr) ++ ++ ++def hbacrule_del(host, rulename, extra_args=()): ++ cmd = [ ++ "ipa", "hbacrule-del", rulename ++ ] ++ cmd.extend(extra_args) ++ return host.run_command(cmd) ++ ++ ++def hbacrule_enable(host, rulename, raiseonerr=True): ++ cmd = ["ipa", "hbacrule-enable", rulename] ++ return host.run_command(cmd, raiseonerr=raiseonerr) ++ ++ ++def hbacrule_disable(host, rulename, raiseonerr=True): ++ cmd = ["ipa", "hbacrule-disable", rulename] ++ return host.run_command(cmd, raiseonerr=raiseonerr) ++ ++ ++def hbacsvc_add(host, svcname, extra_args=(), raiseonerr=True): ++ cmd = ["ipa", "hbacsvc-add", svcname] ++ cmd.extend(extra_args) ++ return host.run_command(cmd, raiseonerr=raiseonerr) ++ ++ ++def hbacsvc_del(host, svcname, raiseonerr=True): ++ cmd = ["ipa", "hbacsvc-del", svcname] ++ return host.run_command(cmd, raiseonerr=raiseonerr) ++ ++ ++def hbacsvcgroup_add(host, groupname, extra_args=()): ++ cmd = ["ipa", "hbacsvcgroup-add", groupname] ++ cmd.extend(extra_args) ++ return host.run_command(cmd) ++ ++ ++def hbacsvcgroup_add_member(host, groupname, services=None, ++ raiseonerr=True, extra_args=()): ++ cmd = ["ipa", "hbacsvcgroup-add-member", groupname] ++ if services: ++ cmd.append("--hbacsvc") ++ cmd.append(services) ++ cmd.extend(extra_args) ++ return host.run_command(cmd, raiseonerr=raiseonerr) ++ ++ ++def hostgroup_del(host, groupname, raiseonerr=True): ++ cmd = ["ipa", "hostgroup-del", groupname] ++ return host.run_command(cmd, raiseonerr=raiseonerr) ++ ++ + def ldapmodify_dm(host, ldif_text, **kwargs): + """Run ldapmodify as Directory Manager + +diff --git a/ipatests/test_integration/test_hbac_functional.py b/ipatests/test_integration/test_hbac_functional.py +new file mode 100644 +index 0000000000000000000000000000000000000000..715a334d5a78d0a924a12b592b81a94b41d8704e +--- /dev/null ++++ b/ipatests/test_integration/test_hbac_functional.py +@@ -0,0 +1,3580 @@ ++# Copyright (C) 2025 FreeIPA Contributors see COPYING for license ++ ++""" ++HBAC functional tests ported from bash test suite ++This module tests Host-Based Access Control functionality ++""" ++ ++from __future__ import absolute_import ++import re ++from ipatests.pytest_ipa.integration import tasks ++from ipatests.test_integration.base import IntegrationTest ++ ++ ++class TestHBACFunctional(IntegrationTest): ++ """HBAC functional tests ported from bash test suite""" ++ ++ topology = 'star' ++ num_clients = 2 ++ ++ # Constants ++ USER_PASSWORD = 'Secret123' ++ USER_1 = 'user1' ++ USER_2 = 'user2' ++ USER_3 = 'user3' ++ HOSTGROUP_1 = 'hostgrp1' ++ HOSTGROUP_2 = 'hostgrp2' ++ GROUP = 'group1' ++ GROUP_2 = 'group2' ++ HBAC_RULE = 'rule1' ++ UTF8_HBAC_RULE = 'Ì' ++ HBAC_SERVICE_GROUP1 = 'sshdgrp1' ++ HBAC_SERVICE_GROUP2 = 'emptygrp2' ++ ++ @classmethod ++ def install(cls, mh): ++ """Install and initial setup""" ++ # Install FTP ++ tasks.install_packages(cls.master, ['vsftpd']) ++ tasks.install_packages(cls.clients[0], ['ftp']) ++ tasks.install_packages(cls.clients[1], ['ftp']) ++ if cls.domain_level is not None: ++ domain_level = cls.domain_level ++ else: ++ domain_level = cls.master.config.domain_level ++ tasks.install_topo(cls.topology, ++ cls.master, [], ++ cls.clients, domain_level, ++ clients_extra_args=('--mkhomedir',)) ++ tasks.kinit_admin(cls.master) ++ # Setup common test users ++ cls.setup_common_users() ++ ++ @classmethod ++ def setup_common_users(cls): ++ """Create common test users""" ++ tasks.kinit_admin(cls.master) ++ for user in [cls.USER_1, cls.USER_2, cls.USER_3]: ++ # Create the test active user's ++ tasks.create_active_user( ++ cls.master, user, password=cls.USER_PASSWORD, extra_args=[ ++ '--homedir', '/home/{}'.format(user)] ++ ) ++ # Set home directory ownership and permissions ++ home_dir = f'/home/{user}' ++ cls.master.run_command(['mkdir', '-p', home_dir]) ++ cls.master.run_command(['chown', f'{user}:{user}', home_dir]) ++ cls.master.run_command(['chmod', '755', home_dir]) ++ tasks.clear_sssd_cache(cls.master) ++ ++ def ssh_auth_success(self, user, password, host, source_host=None): ++ """ ++ Test SSH authentication success ++ Args: ++ user: Username to authenticate ++ password: Password for authentication ++ host: Host to connect to (target host) ++ source_host: Host to run command from (optional, ++ defaults to target host) ++ Returns: ++ True if authentication succeeds, False otherwise ++ """ ++ if source_host is None: ++ source_host = host ++ ++ result = source_host.run_command([ ++ 'sshpass', '-p', password, 'ssh', ++ '-o', 'StrictHostKeyChecking=no', ++ '-o', 'PasswordAuthentication=yes', ++ '-l', user, host.hostname, 'echo login successful' ++ ], raiseonerr=False) ++ ++ if result.returncode == 0 and 'login successful' in result.stdout_text: ++ return True ++ return False ++ ++ def ssh_auth_failure(self, user, password, host, source_host=None): ++ """ ++ Test SSH authentication failure ++ Args: ++ user: Username to authenticate ++ password: Password for authentication ++ host: Host to connect to (target host) ++ source_host: Host to run command from (optional, ++ defaults to target host) ++ Returns: ++ True if authentication fails as expected, False otherwise ++ """ ++ return not self.ssh_auth_success(user, password, host, source_host) ++ ++ def ftp_auth_success(self, user, password, target_host, source_host): ++ """ ++ Test FTP authentication success ++ Args: ++ user: Username to authenticate ++ password: Password for authentication ++ target_host: Host to connect to (FTP server) ++ source_host: Host to run command from ++ Returns: ++ True if authentication succeeds, False otherwise ++ """ ++ ftp_script = ( ++ f'printf "user {user} {password}\nquit\n" | ' ++ f'ftp -inv {target_host.hostname}' ++ ) ++ result = source_host.run_command( ++ ['bash', '-c', ftp_script], ++ raiseonerr=False ++ ) ++ ++ if (result.returncode == 0 ++ and 'Login successful.' in result.stdout_text): ++ return True ++ return False ++ ++ def ftp_auth_failure(self, user, password, target_host, source_host): ++ """ ++ Test FTP authentication failure ++ Args: ++ user: Username to authenticate ++ password: Password for authentication ++ target_host: Host to connect to (FTP server) ++ source_host: Host to run command from ++ Returns: ++ True if authentication fails as expected, False otherwise ++ """ ++ return not self.ftp_auth_success( ++ user, password, target_host, source_host ++ ) ++ ++ def refresh_user_cache( ++ self, host, users, kdestroy=True ++ ): ++ """ ++ Clear SSSD cache, run id and getent for users, and optionally destroy ++ kerberos tickets. ++ Args: ++ host: The host/client to run commands on ++ users: A single username (str) or a list of usernames to process ++ kdestroy: If True (default), destroy all kerberos tickets after ++ refreshing cache. Set to False to skip kdestroy. ++ kinit_admin: If True, run kinit_admin after clearing cache. ++ Defaults to False. ++ """ ++ # Normalize users to a list ++ if isinstance(users, str): ++ users = [users] ++ # Clear SSSD cache ++ tasks.clear_sssd_cache(host) ++ # Run id command for each user ++ for user in users: ++ host.run_command(["id", user], raiseonerr=False) ++ # Run getent for each user ++ for user in users: ++ host.run_command( ++ ["getent", "-s", "sss", "passwd", user] ++ ) ++ # Destroy all kerberos tickets if requested ++ if kdestroy: ++ tasks.kdestroy_all(host) ++ ++ def _build_hbactest_cmd(self, user, host, service, **kwargs): ++ """Build the hbactest command with given parameters.""" ++ cmd = [ ++ "ipa", "hbactest", ++ f"--user={user}", ++ f"--host={host}", ++ f"--service={service}" ++ ] ++ if kwargs.get('rule'): ++ cmd.append(f"--rule={kwargs['rule']}") ++ if kwargs.get('nodetail'): ++ cmd.append("--nodetail") ++ return cmd ++ ++ def _run_hbactest_with_patterns(self, user, host, service, **kwargs): ++ """Run hbactest and verify expected patterns are present.""" ++ cmd = self._build_hbactest_cmd(user, host, service, **kwargs) ++ result = self.master.run_command(cmd, raiseonerr=False) ++ for pattern in kwargs['expected_patterns']: ++ assert re.search(pattern, result.stdout_text), ( ++ f"Expected pattern '{pattern}' not found in " ++ f"output: {result.stdout_text}" ++ ) ++ ++ def _run_hbactest_with_forbidden_patterns(self, user, host, service, ++ **kwargs): ++ """Run hbactest and verify forbidden patterns are absent.""" ++ cmd = self._build_hbactest_cmd(user, host, service, **kwargs) ++ result = self.master.run_command(cmd, raiseonerr=False) ++ for pattern in kwargs['forbidden_patterns']: ++ assert not re.search(pattern, result.stdout_text), ( ++ f"Forbidden pattern '{pattern}' found in output: " ++ f"{result.stdout_text}" ++ ) ++ ++ def _run_hbactest_expect_unresolved(self, user, host, service, **kwargs): ++ """Run hbactest expecting unresolved rules.""" ++ cmd = self._build_hbactest_cmd(user, host, service, **kwargs) ++ result = self.master.run_command(cmd, raiseonerr=False) ++ rule = kwargs.get('rule') ++ assert ("Unresolved rules in --rules" in result.stdout_text ++ or (rule and f"error: {rule}" in result.stdout_text)), ( ++ f"Expected unresolved rule for {rule}, " ++ f"got: {result.stdout_text}" ++ ) ++ ++ def _run_hbactest_expect_error(self, user, host, service, **kwargs): ++ """Run hbactest expecting an error.""" ++ cmd = self._build_hbactest_cmd(user, host, service, **kwargs) ++ result = self.master.run_command(cmd, raiseonerr=False) ++ rule = kwargs.get('rule') ++ assert (f"error: {rule}" in result.stdout_text ++ or "error:" in result.stdout_text.lower()), ( ++ f"Expected error in output, got: {result.stdout_text}" ++ ) ++ ++ def _run_hbactest_basic(self, user, host, service, should_allow, **kwargs): ++ """Run basic hbactest and verify access granted/denied.""" ++ cmd = self._build_hbactest_cmd(user, host, service, **kwargs) ++ result = self.master.run_command(cmd, raiseonerr=False) ++ rule = kwargs.get('rule') ++ if should_allow: ++ assert ("Access granted: True" in result.stdout_text ++ or (rule and f"matched: {rule}" in ++ result.stdout_text)), ( ++ f"Expected access granted for {user} to " ++ f"{host}:{service}, got: {result.stdout_text}" ++ ) ++ else: ++ assert ("Access granted: False" in result.stdout_text ++ or (rule and f"notmatched: {rule}" in ++ result.stdout_text)), ( ++ f"Expected access denied for {user} to " ++ f"{host}:{service}, got: {result.stdout_text}" ++ ) ++ ++ def run_hbactest(self, user, host, service, should_allow=True, **kwargs): ++ """ ++ Run HBAC test with flexible parameters. ++ ++ Args: ++ user: Username to test ++ host: Hostname to test ++ service: Service name to test ++ should_allow: Expected result (True=access granted, ++ False=denied) ++ ++ Kwargs: ++ rule: Optional rule name to test specific rule ++ nodetail: If True, use --nodetail flag ++ expect_unresolved: If True, expect unresolved rules in output ++ expect_error: If True, expect error in output ++ expected_patterns: List of regex patterns to check in output ++ forbidden_patterns: List of regex patterns that must NOT ++ be in output ++ """ ++ if 'expected_patterns' in kwargs: ++ self._run_hbactest_with_patterns(user, host, service, **kwargs) ++ elif 'forbidden_patterns' in kwargs: ++ self._run_hbactest_with_forbidden_patterns( ++ user, host, service, **kwargs ++ ) ++ elif kwargs.get('expect_unresolved'): ++ self._run_hbactest_expect_unresolved(user, host, service, **kwargs) ++ elif kwargs.get('expect_error'): ++ self._run_hbactest_expect_error(user, host, service, **kwargs) ++ else: ++ self._run_hbactest_basic(user, host, service, should_allow, ++ **kwargs) ++ ++ def cleanup_resources(self, users=None, groups=None, hostgroups=None, ++ hbacrules=None, hbacsvcs=None, hbacsvcgroups=None, ++ netgroups=None, clear_cache=True, kinit_admin=True): ++ """ ++ Common cleanup method for HBAC test resources. ++ All parameters accept lists of resource names to delete. ++ ++ Example: ++ self.cleanup_resources( ++ hbacrules=['rule1'], ++ hostgroups=['hostgrp1'], ++ groups=['group1'] ++ ) ++ """ ++ if kinit_admin: ++ tasks.kinit_admin(self.master) ++ ++ # Define cleanup order (dependencies first - rules before groups) ++ cleanup_map = [ ++ ('hbacrule-del', hbacrules), ++ ('hbacsvcgroup-del', hbacsvcgroups), ++ ('hbacsvc-del', hbacsvcs), ++ ('hostgroup-del', hostgroups), ++ ('netgroup-del', netgroups), ++ ('group-del', groups), ++ ('user-del', users), ++ ] ++ ++ for cmd, resources in cleanup_map: ++ if resources: ++ for resource in resources: ++ self.master.run_command( ++ ["ipa", cmd, resource], raiseonerr=False ++ ) ++ if cmd == 'user-del': ++ self.master.run_command( ++ ["rm", "-rf", f"/home/{resource}"], ++ raiseonerr=False ++ ) ++ ++ if clear_cache: ++ tasks.clear_sssd_cache(self.master) ++ if hasattr(self, 'clients'): ++ for client in self.clients: ++ tasks.clear_sssd_cache(client) ++ ++ # Test 001: User access to client ++ def test_hbacsvc_master_001(self, request): ++ """hbacsvc_master_001: user access to client, on master, ++ add rule""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE, "admin_allow_all"] ++ )) ++ tasks.kinit_admin(self.master) ++ ++ # Test SSH before HBAC setup ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.master ++ ) ++ tasks.kdestroy_all(self.master) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.master ++ ) ++ assert self.ssh_auth_success( ++ self.USER_3, self.USER_PASSWORD, self.master ++ ) ++ ++ tasks.kinit_admin(self.master) ++ ++ # Setup admin rule and disable allow_all ++ tasks.hbacrule_add( ++ self.master, "admin_allow_all", ++ extra_args=["--hostcat=all", "--servicecat=all"] ++ ) ++ tasks.hbacrule_add_user( ++ self.master, "admin_allow_all", ++ groups="admins" ++ ) ++ tasks.hbacrule_disable(self.master, "allow_all") ++ ++ # Create rule1: allow user1 to access client0 with ssh ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[0].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test with hbactest ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule="rule2", ++ expected_patterns=[ ++ r"Unresolved rules in --rules", ++ r"Non-existent or invalid rules: rule2" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ # Test with --nodetail ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ # client 1 test ++ self.refresh_user_cache(self.clients[0], self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[0] ++ ) ++ # client 2 test ++ self.refresh_user_cache(self.clients[1], self.USER_1) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1] ++ ) ++ ++ # Test 002: User access to master for FTP ++ def test_hbacsvc_master_002(self): ++ """hbacsvc_master_002: user access to master for ftp, on master, add ++ rule""" ++ tasks.kinit_admin(self.master) ++ # FTP Configs. ++ self.master.run_command( ++ ["systemctl", "start", "vsftpd"], ++ raiseonerr=False ++ ) ++ self.master.run_command( ++ ["setsebool", "-P", "tftp_home_dir", "on"], ++ raiseonerr=False ++ ) ++ self.master.run_command( ++ ["firewall-cmd", "--permanent", "--add-service=ftp"], ++ raiseonerr=False ++ ) ++ self.master.run_command(["firewall-cmd", "--reload"], raiseonerr=False) ++ ++ # Create HBAC RULE: allow user1 to access master with service vsftpd ++ tasks.kinit_admin(self.master) ++ # Create rule: allow user1 to access master with service vsftpd ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.master.hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="vsftpd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ tasks.clear_sssd_cache(self.master) ++ ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.master.hostname, "vsftpd", ++ should_allow=True, rule=self.HBAC_RULE) ++ self.run_hbactest(self.USER_2, self.master.hostname, "vsftpd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "vsftpd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "vsftpd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.master.hostname, "vsftpd", ++ rule=self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ]) ++ self.run_hbactest(self.USER_1, self.master.hostname, "vsftpd", ++ rule="rule2", ++ expected_patterns=[ ++ r"Unresolved rules in --rules", ++ r"Non-existent or invalid rules: rule2" ++ ]) ++ self.run_hbactest(self.USER_2, self.master.hostname, "vsftpd", ++ rule="rule2", ++ expected_patterns=[ ++ r"Unresolved rules in --rules", ++ r"Non-existent or invalid rules: rule2" ++ ]) ++ self.run_hbactest(self.USER_1, self.master.hostname, "vsftpd", ++ should_allow=True, rule=self.HBAC_RULE, ++ nodetail=True) ++ ++ # Test both clients ++ for client in self.clients: ++ self.refresh_user_cache( ++ client, [self.USER_1, self.USER_2] ++ ) ++ # Test: Can client connect to master via FTP? ++ assert self.ftp_auth_success( ++ self.USER_1, self.USER_PASSWORD, ++ self.master, client ++ ) ++ assert self.ftp_auth_failure( ++ self.USER_2, self.USER_PASSWORD, ++ self.master, client ++ ) ++ ++ # Test 002_1: Delete service and test ++ def test_hbacsvc_master_002_1(self, request): ++ """hbacsvc_master_002_1: user access to master after ftp removed, on ++ master, remove ftp from rule""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.hbacrule_disable(self.master, "allow_all") ++ tasks.hbacsvc_del(self.master, "vsftpd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.master.hostname, "vsftpd", ++ should_allow=False, rule=self.HBAC_RULE) ++ self.run_hbactest(self.USER_2, self.master.hostname, "vsftpd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "vsftpd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "vsftpd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.master.hostname, "vsftpd", ++ rule=self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False" ++ ]) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "vsftpd", ++ rule="rule2", ++ expected_patterns=[ ++ r"Unresolved rules in --rules", ++ r"Non-existent or invalid rules: rule2" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.master.hostname, "vsftpd", ++ rule="rule2", ++ expected_patterns=[ ++ r"Unresolved rules in --rules", ++ r"Non-existent or invalid rules: rule2" ++ ] ++ ) ++ self.run_hbactest(self.USER_1, self.master.hostname, "vsftpd", ++ should_allow=False, rule=self.HBAC_RULE, ++ nodetail=True) ++ ++ # Client 1 and client 2 Test ++ for client in self.clients: ++ self.refresh_user_cache( ++ client, self.USER_1, ++ kdestroy=False ++ ) ++ assert self.ftp_auth_failure( ++ self.USER_1, self.USER_PASSWORD, ++ self.master, client ++ ) ++ assert self.ftp_auth_failure( ++ self.USER_2, self.USER_PASSWORD, ++ self.master, source_host=self.clients[1] ++ ) ++ ++ # Test 003: FTP service group ++ def test_hbacsvc_master_003(self, request): ++ """hbacsvc_master_003: user access to master for ftp service group, ++ on master, add rule""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE] ++ )) ++ tasks.kinit_admin(self.master) ++ # Create rule with service group (verifies bug 746227): ++ # allow ftp access to user1 on master through a service group ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacsvc_add(self.master, "vsftpd", raiseonerr=False) ++ tasks.hbacsvcgroup_add_member(self.master, "ftp", services="vsftpd") ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ extra_args=["--hbacsvcgroups=ftp"]) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.master.hostname) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test (verifies bug 746227) ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.master.hostname, "vsftpd", ++ should_allow=True, rule=self.HBAC_RULE) ++ self.run_hbactest(self.USER_2, self.master.hostname, "vsftpd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "vsftpd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "vsftpd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.master.hostname, "vsftpd", ++ rule=self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ]) ++ self.run_hbactest(self.USER_1, self.master.hostname, "ftp", ++ rule=self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ]) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "vsftpd", ++ rule="rule2", ++ expected_patterns=[ ++ r"Unresolved rules in --rules", ++ r"Non-existent or invalid rules: rule2" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.master.hostname, "vsftpd", ++ rule="rule2", ++ expected_patterns=[ ++ r"Unresolved rules in --rules", ++ r"Non-existent or invalid rules: rule2" ++ ] ++ ) ++ self.run_hbactest(self.USER_1, self.master.hostname, "vsftpd", ++ should_allow=True, rule=self.HBAC_RULE, ++ nodetail=True) ++ ++ # Client 1 and client 2 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ftp_auth_success( ++ self.USER_1, self.USER_PASSWORD, ++ self.master, src_client ++ ) ++ assert self.ftp_auth_failure( ++ self.USER_2, self.USER_PASSWORD, ++ self.master, source_host=self.clients[0] ++ ) ++ ++ # Test 004: Hostgroup access (verifies bug 733663) ++ def test_hbacsvc_master_004(self, request): ++ """hbacsvc_master_004: user access to hostgroup, on master, ++ add rule""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hostgroups=[self.HOSTGROUP_1] ++ )) ++ tasks.kinit_admin(self.master) ++ # Create hostgroup ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[1].hostname) ++ ++ # Create rule: allow ssh access for user1 to hosts member of ++ # the hostgroup (=client1) ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hostgroups={self.HOSTGROUP_1}"]) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test (verifies bug 733663) ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.master.hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_3, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.HOSTGROUP_1, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client Test ++ self.refresh_user_cache(self.clients[0], self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=self.clients[0] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[1], ++ source_host=self.clients[1] ++ ) ++ ++ # Test 005: Hostgroup with user removal ++ def test_hbacsvc_master_005(self): ++ """hbacsvc_master_005: user access to hostgroup, on master, ++ add rule""" ++ tasks.kinit_admin(self.master) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[1].hostname) ++ ++ # Create rule: allow ssh access for user1 to hosts member of ++ # the hostgroup (=client1) ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hostgroups={self.HOSTGROUP_1}"]) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.master.hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_3, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.HOSTGROUP_1, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client Test ++ self.refresh_user_cache(self.clients[0], self.USER_1) ++ self.refresh_user_cache(self.clients[1], self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=self.clients[0] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ ++ # Test 005_1: Remove user from rule ++ def test_hbacsvc_master_005_1(self, request): ++ """hbacsvc_master_005_1: user access after user removed, on master, ++ remove user from rule""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hostgroups=[self.HOSTGROUP_1] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.hbacrule_remove_user( ++ self.master, self.HBAC_RULE, users=self.USER_1) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE) ++ self.run_hbactest(self.USER_2, self.clients[1].hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE) ++ self.run_hbactest(self.USER_1, self.master.hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE) ++ self.run_hbactest( ++ self.USER_1, self.HOSTGROUP_1, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ ++ # Client Test ++ self.refresh_user_cache(self.clients[0], self.USER_1) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[0] ++ ) ++ ++ # Test 006: Hostgroup to hostgroup access ++ def test_hbacsvc_master_006(self, request): ++ """hbacsvc_master_006: user access to hostgroup from hostgroup2, ++ on master, add rule""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hostgroups=[self.HOSTGROUP_1, self.HOSTGROUP_2] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[0].hostname) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_2) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_2, ++ hosts=self.clients[1].hostname) ++ # Create rule: allow ssh access for user1 to hosts member of ++ # the hostgroup (=client0) ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hostgroups={self.HOSTGROUP_1}"]) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client Test ++ self.refresh_user_cache(self.clients[0], self.USER_1) ++ self.refresh_user_cache(self.clients[1], self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=self.clients[1] ++ ) ++ ++ # Test 007: Hostgroup for HBAC service group (verifies bug 830347) ++ def test_hbacsvc_master_007(self): ++ """hbacsvc_master_007: user access to hostgroup for hbac service ++ group, on master (bz830347)""" ++ tasks.kinit_admin(self.master) ++ ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[0].hostname) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_2) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_2, ++ hosts=self.clients[1].hostname) ++ # Create rule: allow ssh access for user1 to hosts member of ++ # the hostgroup (HOSTGROUP_1) (=client0) ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hostgroups={self.HOSTGROUP_1}"]) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[0] ++ ) ++ ++ # Test 007_1: Remove user from rule ++ def test_hbacsvc_master_007_1(self, request): ++ """hbacsvc_master_007_1: user access after user removed, on master, ++ remove user from rule""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hostgroups=[self.HOSTGROUP_1, self.HOSTGROUP_2] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.hbacrule_remove_user( ++ self.master, self.HBAC_RULE, users=self.USER_1) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.HOSTGROUP_1, "sshd", ++ self.HBAC_RULE, ++ forbidden_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.HOSTGROUP_1, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ ++ self.run_hbactest( ++ self.USER_1, self.HOSTGROUP_2, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.HOSTGROUP_2, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client Test ++ self.refresh_user_cache(self.clients[0], self.USER_1) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[0] ++ ) ++ ++ # Test 008: Group access to client ++ def test_hbacsvc_master_008(self): ++ """hbacsvc_master_008: group access to client2, on master, ++ add rule""" ++ tasks.kinit_admin(self.master) ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add_member(self.master, self.GROUP, users=self.USER_1) ++ # Create rule: allow ssh access for users in group (GROUP) to client1 ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user( ++ self.master, self.HBAC_RULE, ++ groups=self.GROUP) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[1].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 and 2 test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[1] ++ ) ++ ++ # Test 008_1: Remove group from rule ++ def test_hbacsvc_master_008_1(self, request): ++ """hbacsvc_master_008_1: group access after group removed, on ++ master, remove group from rule""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ groups=[self.GROUP] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.hbacrule_remove_user( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--groups={self.GROUP}"]) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_2, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ forbidden_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client Test ++ self.refresh_user_cache(self.clients[1], self.USER_1) ++ for src_client in self.clients: ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=src_client ++ ) ++ ++ # Test 009: Group access to client2 for HBAC service group ++ def test_hbacsvc_master_009(self): ++ """hbacsvc_master_009: group access to client2 for hbac service ++ group, on master, add rule""" ++ tasks.kinit_admin(self.master) ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add_member(self.master, self.GROUP, users=self.USER_1) ++ # Create rule: allow ssh access for users in group (GROUP) to client1 ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user( ++ self.master, self.HBAC_RULE, ++ groups=self.GROUP) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[1].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 and 2 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[1] ++ ) ++ ++ def test_hbacsvc_master_009_1(self, request): ++ """hbacsvc_master_009_1: verify group still in rule (srchost ++ validation deprecated)""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ groups=[self.GROUP] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 and 2 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[1] ++ ) ++ ++ # Test 010: Group access to hostgroup ++ def test_hbacsvc_master_010(self, request): ++ """hbacsvc_master_010: group access to hostgroup, on master, add ++ rule""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hostgroups=[self.HOSTGROUP_1], ++ groups=[self.GROUP] ++ )) ++ tasks.kinit_admin(self.master) ++ ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add_member(self.master, self.GROUP, users=self.USER_1) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[1].hostname) ++ # Create rule: allow ssh access for users in group (GROUP) to hosts ++ # member of the hostgroup (HOSTGROUP_1) (=client1) ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user( ++ self.master, self.HBAC_RULE, ++ groups=self.GROUP) ++ tasks.hbacrule_add_host( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hostgroups={self.HOSTGROUP_1}"]) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 and 2 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[1] ++ ) ++ ++ # Test 011: Group access to hostgroup for HBAC service group ++ def test_hbacsvc_master_011(self): ++ """hbacsvc_master_011: group access to hostgroup for hbac service ++ group, on master, add rule""" ++ tasks.kinit_admin(self.master) ++ ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add_member(self.master, self.GROUP, users=self.USER_1) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[0].hostname) ++ # Create rule: allow ssh access for users in group to client1 ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user( ++ self.master, self.HBAC_RULE, ++ groups=self.GROUP) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[1].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[1] ++ ) ++ ++ def test_hbacsvc_master_011_1(self, request): ++ """hbacsvc_master_011_1: remove hbac service group""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hostgroups=[self.HOSTGROUP_1], ++ groups=[self.GROUP] ++ )) ++ tasks.kinit_admin(self.master) ++ ++ tasks.hbacrule_remove_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ forbidden_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 and 2 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=src_client ++ ) ++ ++ # Test 012: Group access to client2 from hostgroup for HBAC service group ++ def test_hbacsvc_master_012(self, request): ++ """hbacsvc_master_012: group access to client2 from hostgroup for ++ hbac service group""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hbacsvcgroups=[self.HBAC_SERVICE_GROUP1], ++ hostgroups=[self.HOSTGROUP_1], ++ groups=[self.GROUP] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add_member(self.master, self.GROUP, users=self.USER_1) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[0].hostname) ++ # Create rule: allow ssh access for users in group to client1 through ++ # a service group ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user( ++ self.master, self.HBAC_RULE, ++ groups=self.GROUP) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[1].hostname) ++ tasks.hbacsvcgroup_add(self.master, self.HBAC_SERVICE_GROUP1) ++ tasks.hbacsvcgroup_add_member( ++ self.master, self.HBAC_SERVICE_GROUP1, ++ services="sshd") ++ tasks.hbacrule_add_service( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hbacsvcgroup={self.HBAC_SERVICE_GROUP1}"]) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 Test ++ self.refresh_user_cache(self.clients[0], self.USER_1) ++ self.refresh_user_cache(self.clients[1], self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=self.clients[0] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[0] ++ ) ++ # Client 2 Test ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ ++ # Test 013: Group access to hostgroup from hostgroup2 ++ def test_hbacsvc_master_013(self, request): ++ """hbacsvc_master_013: group access to hostgroup from hostgroup2, ++ on master, add rule""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hostgroups=[self.HOSTGROUP_1, self.HOSTGROUP_2], ++ groups=[self.GROUP])) ++ tasks.kinit_admin(self.master) ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add_member(self.master, self.GROUP, users=self.USER_1) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[0].hostname) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_2) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_2, ++ hosts=self.clients[1].hostname) ++ # Create rule: allow ssh access for users in group (GROUP) to hosts ++ # member of the hostgroup (HOSTGROUP_1) (=client0) ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user( ++ self.master, self.HBAC_RULE, ++ groups=self.GROUP) ++ tasks.hbacrule_add_host( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hostgroups={self.HOSTGROUP_1}"]) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 Test ++ self.refresh_user_cache(self.clients[0], self.USER_1) ++ ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=self.clients[0] ++ ) ++ # Client 2 Test ++ self.refresh_user_cache(self.clients[1], self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ ++ # Test 014: Similar to 013 ++ def test_hbacsvc_master_014(self, request): ++ """hbacsvc_master_014: group access to hostgroup from hostgroup2, ++ on master""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hostgroups=[self.HOSTGROUP_1, self.HOSTGROUP_2], ++ groups=[self.GROUP])) ++ tasks.kinit_admin(self.master) ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add_member(self.master, self.GROUP, users=self.USER_1) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[0].hostname) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_2) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_2, ++ hosts=self.clients[1].hostname) ++ # Create rule: allow ssh access for users in group to hosts member of ++ # the hostgroup (=client0) ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, groups=self.GROUP) ++ tasks.hbacrule_add_host( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hostgroups={self.HOSTGROUP_1}"]) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 Test ++ self.refresh_user_cache(self.clients[0], self.USER_1) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=self.clients[0] ++ ) ++ # Client 2 Test ++ self.refresh_user_cache(self.clients[1], self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ ++ # Test 015: Nested group access to client ++ def test_hbacsvc_master_015(self): ++ """hbacsvc_master_015: nested group access to client, on master, ++ add rule""" ++ tasks.kinit_admin(self.master) ++ ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add(self.master, self.GROUP_2) ++ tasks.group_add_member(self.master, self.GROUP_2, users=self.USER_1) ++ tasks.group_add_member( ++ self.master, self.GROUP, ++ extra_args=[f"--groups={self.GROUP_2}"]) ++ # Create rule: allow ssh access for users in nested group to client0 ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user( ++ self.master, self.HBAC_RULE, ++ groups=self.GROUP) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[0].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[0] ++ ) ++ ++ def test_hbacsvc_master_015_1(self, request): ++ """hbacsvc_master_015_1: nested group access after group removed""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ groups=[self.GROUP, self.GROUP_2] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.hbacrule_remove_user( ++ self.master, self.HBAC_RULE, ++ groups=self.GROUP) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ forbidden_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Test both clients ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=src_client ++ ) ++ ++ # Test 016: Nested group access for HBAC service group ++ def test_hbacsvc_master_016(self): ++ """hbacsvc_master_016: nested group access to client for hbac ++ service group""" ++ tasks.kinit_admin(self.master) ++ ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add(self.master, self.GROUP_2) ++ tasks.group_add_member(self.master, self.GROUP_2, users=self.USER_1) ++ tasks.group_add_member( ++ self.master, self.GROUP, ++ extra_args=[f"--groups={self.GROUP_2}"]) ++ # Create rule: allow ssh access for users in nested group to client0 ++ # through a service group ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user( ++ self.master, self.HBAC_RULE, ++ groups=self.GROUP) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[0].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[0] ++ ) ++ ++ def test_hbacsvc_master_016_1(self, request): ++ """hbacsvc_master_016_1: nested group access after group removed""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ groups=[self.GROUP, self.GROUP_2] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.hbacrule_remove_user( ++ self.master, self.HBAC_RULE, ++ groups=self.GROUP) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Test both clients ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=src_client ++ ) ++ ++ # Test 017: Nested group access from hostgroup ++ def test_hbacsvc_master_017(self, request): ++ """hbacsvc_master_017: nested group access to client from ++ hostgroup""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hostgroups=[self.HOSTGROUP_1], ++ groups=[self.GROUP, self.GROUP_2] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add(self.master, self.GROUP_2) ++ tasks.group_add_member(self.master, self.GROUP_2, users=self.USER_1) ++ tasks.group_add_member( ++ self.master, self.GROUP, ++ extra_args=[f"--groups={self.GROUP_2}"]) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[1].hostname) ++ # Create rule: allow ssh access for users in nested group to client0 ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user( ++ self.master, self.HBAC_RULE, ++ groups=self.GROUP) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[0].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ ++ # Test 018: Nested group access to hostgroup from hostgroup for HBAC ++ # service group ++ def test_hbacsvc_master_018(self, request): ++ """hbacsvc_master_018: nested group to hostgroup from hostgroup ++ for hbac service group""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hostgroups=[self.HOSTGROUP_1], ++ groups=[self.GROUP, self.GROUP_2] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add(self.master, self.GROUP_2) ++ tasks.group_add_member(self.master, self.GROUP_2, users=self.USER_1) ++ tasks.group_add_member( ++ self.master, self.GROUP, ++ extra_args=[f"--groups={self.GROUP_2}"]) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[1].hostname) ++ # Create rule: allow ssh access for users in nested group to client0 ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user( ++ self.master, self.HBAC_RULE, ++ groups=self.GROUP) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[0].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ ++ # Test 019: Nested group access to hostgroup from hostgroup2 ++ def test_hbacsvc_master_019(self, request): ++ """hbacsvc_master_019: nested group to hostgroup from ++ hostgroup2""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hostgroups=[self.HOSTGROUP_1, self.HOSTGROUP_2], ++ groups=[self.GROUP, self.GROUP_2] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add(self.master, self.GROUP_2) ++ tasks.group_add_member(self.master, self.GROUP_2, users=self.USER_1) ++ tasks.group_add_member( ++ self.master, self.GROUP, ++ extra_args=[f"--groups={self.GROUP_2}"]) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[0].hostname) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_2) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_2, ++ hosts=self.clients[1].hostname) ++ # Create rule: allow ssh access for users in nested group to hosts ++ # member of the hostgroup (=client0) ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ groups=self.GROUP_2) ++ tasks.hbacrule_add_host( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hostgroups={self.HOSTGROUP_1}"]) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ ++ # Test 020: Nested group for HBAC service group ++ def test_hbacsvc_master_020(self): ++ """hbacsvc_master_020: nested group to hostgroup for hbac ++ service group""" ++ tasks.kinit_admin(self.master) ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add(self.master, self.GROUP_2) ++ tasks.group_add_member(self.master, self.GROUP_2, users=self.USER_1) ++ tasks.group_add_member( ++ self.master, self.GROUP, ++ extra_args=[f"--groups={self.GROUP_2}"]) ++ ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_1, ++ hosts=self.clients[0].hostname) ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_2) ++ tasks.hostgroup_add_member(self.master, self.HOSTGROUP_2, ++ hosts=self.clients[1].hostname) ++ # Create rule: allow ssh access for users in nested group to hosts ++ # member of the hostgroup (=client0) ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ groups=self.GROUP_2) ++ tasks.hbacrule_add_host( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hostgroups={self.HOSTGROUP_1}"]) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ ++ def test_hbacsvc_master_020_1(self, request): ++ """hbacsvc_master_020_1: after rule removed""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hostgroups=[self.HOSTGROUP_1, self.HOSTGROUP_2], ++ groups=[self.GROUP, self.GROUP_2] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.hbacrule_del(self.master, self.HBAC_RULE) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.HOSTGROUP_1, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.HOSTGROUP_1, "sshd", ++ self.HBAC_RULE, ++ forbidden_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.HOSTGROUP_1, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[r"Access granted: True"] ++ ) ++ ++ # Test both clients --> CLIENT 1 ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], ++ source_host=src_client ++ ) ++ ++ # Test 021: User access client from external host ++ def test_hbacsvc_master_021(self, request): ++ """hbacsvc_master_021: user access client from external host, ++ on master, add rule and run hbactest""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE] ++ )) ++ tasks.kinit_admin(self.master) ++ # Create rule: allow user1 to access client0 with ssh ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[0].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, "externalhost.randomhost.com", "sshd", ++ should_allow=False ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, "externalhost.randomhost.com", "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Test 023: Group access to client2 from external host ++ def test_hbacsvc_master_023(self, request): ++ """hbacsvc_master_023: group access client2 from external host, ++ on master, add rule and run hbactest""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ groups=[self.GROUP] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add_member(self.master, self.GROUP, users=self.USER_1) ++ # Create rule: allow ssh access for users in group to client1 ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[1].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, "externalhost.randomhost.com", "sshd", ++ should_allow=False ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, "externalhost.randomhost.com", "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Test 025: Group access to client from external host 2 ++ def test_hbacsvc_master_025(self, request): ++ """hbacsvc_master_025: group access client from external host 2, ++ on master, add rule and run hbactest""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ groups=[self.GROUP, self.GROUP_2] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.group_add(self.master, self.GROUP) ++ tasks.group_add(self.master, self.GROUP_2) ++ tasks.group_add_member(self.master, self.GROUP, users=self.USER_1) ++ tasks.group_add_member( ++ self.master, self.GROUP_2, ++ extra_args=[f"--groups={self.GROUP}"]) ++ # Create rule: allow user1 to access client0 with ssh ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[0].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, "externalhost.randomhost.com", "sshd", ++ should_allow=False ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, "externalhost.randomhost.com", "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Test 027: Empty HBAC service group ++ def test_hbacsvc_master_027(self, request): ++ """hbacsvc_master_027: user access with empty hbac service ++ group""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hbacsvcgroups=[self.HBAC_SERVICE_GROUP2] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.hbacsvcgroup_add(self.master, self.HBAC_SERVICE_GROUP2) ++ # Create rule: allow user1 to access client0 with ssh through a ++ # service group ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[0].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_add_service( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hbacsvcgroup={self.HBAC_SERVICE_GROUP2}"]) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 and 2 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[0] ++ ) ++ ++ # Test 028: Multiple HBAC services in rule ++ def test_hbacsvc_master_028(self, request): ++ """hbacsvc_master_028: user access with multiple hbac ++ services""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE] ++ )) ++ tasks.kinit_admin(self.master) ++ ++ tasks.hbacsvc_add(self.master, "sshdtest") ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[0].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshdtest") ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 and 2 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[0] ++ ) ++ ++ # Test 029: Empty group in rule ++ def test_hbacsvc_master_029(self, request): ++ """hbacsvc_master_029: user access with empty group in rule""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hbacsvcgroups=[self.HBAC_SERVICE_GROUP2], ++ groups=[self.GROUP] ++ )) ++ tasks.kinit_admin(self.master) ++ ++ tasks.group_add(self.master, self.GROUP) ++ tasks.hbacsvcgroup_add(self.master, self.HBAC_SERVICE_GROUP2) ++ # Create rule: allow user1 and users in group to access client0 with ++ # ssh through a service group ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_user( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--groups={self.GROUP}"]) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[0].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_add_service( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hbacsvcgroup={self.HBAC_SERVICE_GROUP2}"]) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test access ++ # Test with hbactest - Basic tests ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 and 2 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[0] ++ ) ++ ++ # Test 030: Empty group, hostgroup and service group in rule ++ def test_hbacsvc_master_030(self, request): ++ """hbacsvc_master_030: user access with empty group hostgroup ++ and service group""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE], ++ hbacsvcgroups=[self.HBAC_SERVICE_GROUP2], ++ hostgroups=[self.HOSTGROUP_1], ++ groups=[self.GROUP] ++ )) ++ tasks.kinit_admin(self.master) ++ ++ tasks.hostgroup_add(self.master, self.HOSTGROUP_1) ++ tasks.group_add(self.master, self.GROUP) ++ tasks.hbacsvcgroup_add(self.master, self.HBAC_SERVICE_GROUP2) ++ # Create rule: allow user1 and users in group to access client0 and ++ # hosts in hostgroup with ssh through a service group ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_user( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--groups={self.GROUP}"]) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[0].hostname) ++ tasks.hbacrule_add_host( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hostgroups={self.HOSTGROUP_1}"]) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_add_service( ++ self.master, self.HBAC_RULE, ++ extra_args=[f"--hbacsvcgroup={self.HBAC_SERVICE_GROUP2}"]) ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test with hbactest ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ rule=self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ rule=self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Client 1 and 2 Test ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0], ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[0] ++ ) ++ ++ # Test 031: UTF-8 rule name ++ def test_hbacsvc_master_031(self, request): ++ """hbacsvc_master_031: user access with UTF-8 rule name""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.UTF8_HBAC_RULE] ++ )) ++ # Now test with UTF-8 rule name ++ tasks.kinit_admin(self.master) ++ # Create rule: allow user1 to access client0 with ssh ++ tasks.hbacrule_add(self.master, self.UTF8_HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.UTF8_HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, self.UTF8_HBAC_RULE, ++ hosts=self.clients[0].hostname) ++ tasks.hbacrule_add_service(self.master, self.UTF8_HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show( ++ self.master, self.UTF8_HBAC_RULE, extra_args=["--all"]) ++ ++ # Test with hbactest ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.UTF8_HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.UTF8_HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule="rule2", ++ expected_patterns=[ ++ r"Unresolved rules in --rules" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, self.clients[0].hostname, "sshd", ++ rule=self.UTF8_HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True, rule=self.UTF8_HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.clients[0].hostname, "sshd", ++ rule=self.UTF8_HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.UTF8_HBAC_RULE}"] ++ ) ++ ++ # Client 1 and 2 Test ++ for client in self.clients: ++ self.refresh_user_cache(client, self.USER_1) ++ ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[0] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1] ++ ) ++ ++ # Test 033: Offline client caching for enabled default HBAC rule ++ def test_hbacsvc_master_033(self): ++ """hbacsvc_master_033: offline caching with allow_all enabled""" ++ tasks.kinit_admin(self.master) ++ ++ # Enable allow_all ++ self.master.run_command( ++ ["ipa", "hbacrule-enable", "allow_all"], ++ raiseonerr=False ++ ) ++ ++ # Test with hbactest - all should be granted ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[1].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_1, self.master.hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.master.hostname, "sshd", ++ should_allow=True) ++ # -------------------------------------------------- ++ # Test SSH access from client0 to client1 and master ++ # -------------------------------------------------- ++ self.refresh_user_cache( ++ self.clients[0], [self.USER_1, self.USER_2], kdestroy=False ++ ) ++ for user in [self.USER_1, self.USER_2]: ++ assert self.ssh_auth_success( ++ user, self.USER_PASSWORD, self.clients[1], ++ source_host=self.clients[0] ++ ) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.master, ++ source_host=self.clients[0] ++ ) ++ tasks.stop_ipa_server(self.master) ++ for user in [self.USER_1, self.USER_2]: ++ assert self.ssh_auth_success( ++ user, self.USER_PASSWORD, self.clients[1], ++ source_host=self.clients[0] ++ ) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.master, ++ source_host=self.clients[0] ++ ) ++ tasks.start_ipa_server(self.master) ++ tasks.clear_sssd_cache(self.master) ++ tasks.clear_sssd_cache(self.clients[0]) ++ # -------------------------------------------------- ++ # Test SSH access from client1 to client0 and master ++ # -------------------------------------------------- ++ self.refresh_user_cache( ++ self.clients[1], [self.USER_1, self.USER_2], kdestroy=False ++ ) ++ for user in [self.USER_1, self.USER_2]: ++ assert self.ssh_auth_success( ++ user, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ assert self.ssh_auth_success( ++ user, self.USER_PASSWORD, self.master, ++ source_host=self.clients[1] ++ ) ++ tasks.stop_ipa_server(self.master) ++ for user in [self.USER_1, self.USER_2]: ++ assert self.ssh_auth_success( ++ user, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ assert self.ssh_auth_success( ++ user, self.USER_PASSWORD, self.master, ++ source_host=self.clients[1] ++ ) ++ tasks.start_ipa_server(self.master) ++ tasks.clear_sssd_cache(self.master) ++ tasks.clear_sssd_cache(self.clients[1]) ++ ++ # Test 034: Offline client caching for disabled default HBAC rule ++ def test_hbacsvc_master_034(self): ++ """hbacsvc_master_034: offline caching with allow_all disabled""" ++ tasks.kinit_admin(self.master) ++ ++ # Disable allow_all ++ tasks.hbacrule_disable(self.master, "allow_all") ++ ++ # Test with hbactest - all should be denied ++ self.run_hbactest(self.USER_1, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_2, self.clients[0].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_2, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_1, self.master.hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest(self.USER_2, self.master.hostname, "sshd", ++ should_allow=False) ++ # -------------------------------------------------- ++ # Test SSH access from client0 to client1 and master - should fail ++ # -------------------------------------------------- ++ self.refresh_user_cache( ++ self.clients[0], [self.USER_1, self.USER_2], kdestroy=False ++ ) ++ for user in [self.USER_1, self.USER_2]: ++ assert self.ssh_auth_failure( ++ user, self.USER_PASSWORD, self.clients[1], ++ source_host=self.clients[0] ++ ) ++ assert self.ssh_auth_failure( ++ user, self.USER_PASSWORD, self.master, ++ source_host=self.clients[0] ++ ) ++ tasks.stop_ipa_server(self.master) ++ for user in [self.USER_1, self.USER_2]: ++ assert self.ssh_auth_failure( ++ user, self.USER_PASSWORD, self.clients[1], ++ source_host=self.clients[0] ++ ) ++ assert self.ssh_auth_failure( ++ user, self.USER_PASSWORD, self.master, ++ source_host=self.clients[0] ++ ) ++ tasks.start_ipa_server(self.master) ++ tasks.clear_sssd_cache(self.master) ++ tasks.clear_sssd_cache(self.clients[0]) ++ # -------------------------------------------------- ++ # Test SSH access from client1 to client0 and master - should fail ++ # -------------------------------------------------- ++ self.refresh_user_cache( ++ self.clients[1], [self.USER_1, self.USER_2], kdestroy=False ++ ) ++ for user in [self.USER_1, self.USER_2]: ++ assert self.ssh_auth_failure( ++ user, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ assert self.ssh_auth_failure( ++ user, self.USER_PASSWORD, self.master, ++ source_host=self.clients[1] ++ ) ++ tasks.stop_ipa_server(self.master) ++ for user in [self.USER_1, self.USER_2]: ++ assert self.ssh_auth_failure( ++ user, self.USER_PASSWORD, self.clients[0], ++ source_host=self.clients[1] ++ ) ++ assert self.ssh_auth_failure( ++ user, self.USER_PASSWORD, self.master, ++ source_host=self.clients[1] ++ ) ++ tasks.start_ipa_server(self.master) ++ tasks.clear_sssd_cache(self.master) ++ tasks.clear_sssd_cache(self.clients[0]) ++ ++ # Test 035: Offline client caching for custom HBAC rule ++ def test_hbacsvc_master_035(self, request): ++ """hbacsvc_master_035: offline caching with custom HBAC rule""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE] ++ )) ++ tasks.kinit_admin(self.master) ++ # Disable allow_all and create custom rule ++ tasks.hbacrule_disable(self.master, "allow_all") ++ ++ # Create rule: allow user1 to access client0 with ssh ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.clients[1].hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ ++ # Test with hbactest ++ self.run_hbactest(self.USER_1, self.clients[1].hostname, "sshd", ++ should_allow=True) ++ self.run_hbactest(self.USER_2, self.clients[1].hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, self.clients[1].hostname, "vsftpd", ++ should_allow=False ++ ) ++ # -------------------------------------------------- ++ # Test SSH access from client0 to client1 and master ++ # -------------------------------------------------- ++ tasks.clear_sssd_cache(self.master) ++ self.refresh_user_cache( ++ self.clients[0], [self.USER_1, self.USER_2], kdestroy=False ++ ) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], self.clients[0] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[1], self.clients[0] ++ ) ++ assert self.ftp_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], self.clients[0] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.master, self.clients[0] ++ ) ++ tasks.stop_ipa_server(self.master) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], self.clients[0] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[1], self.clients[0] ++ ) ++ assert self.ftp_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], self.clients[1] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.master, self.clients[0] ++ ) ++ tasks.start_ipa_server(self.master) ++ tasks.clear_sssd_cache(self.master) ++ tasks.clear_sssd_cache(self.clients[0]) ++ # -------------------------------------------------- ++ # Test SSH access from client1 to client1 and master ++ # -------------------------------------------------- ++ self.refresh_user_cache( ++ self.clients[1], [self.USER_1, self.USER_2], kdestroy=False ++ ) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], self.clients[1] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[1], self.clients[1] ++ ) ++ assert self.ftp_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], self.clients[1] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.master, self.clients[1] ++ ) ++ tasks.stop_ipa_server(self.master) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], self.clients[1] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.clients[1], self.clients[1] ++ ) ++ assert self.ftp_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.clients[1], self.clients[1] ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.master, self.clients[1] ++ ) ++ tasks.start_ipa_server(self.master) ++ tasks.clear_sssd_cache(self.master) ++ tasks.clear_sssd_cache(self.clients[1]) ++ ++ # ------------------------------------------------- ++ # Bug-specific tests ++ # ------------------------------------------------- ++ def test_hbacsvc_master_bug736314(self, request): ++ """hbacsvc_master_bug736314: user access master with multiple ++ external hosts (bz736314)""" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[self.HBAC_RULE] ++ )) ++ tasks.kinit_admin(self.master) ++ tasks.hbacrule_disable(self.master, "allow_all") ++ # Create rule: allow user1 to access master with ssh ++ tasks.hbacrule_add(self.master, self.HBAC_RULE) ++ tasks.hbacrule_add_user(self.master, self.HBAC_RULE, ++ users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, self.HBAC_RULE, ++ hosts=self.master.hostname) ++ tasks.hbacrule_add_service(self.master, self.HBAC_RULE, ++ services="sshd") ++ tasks.hbacrule_show(self.master, self.HBAC_RULE, extra_args=["--all"]) ++ # Test (verifies bug 736314) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE ++ ) ++ self.run_hbactest(self.USER_2, self.master.hostname, "sshd", ++ should_allow=False) ++ self.run_hbactest( ++ self.USER_1, "externalhost2.randomhost.com", "sshd", ++ should_allow=False ++ ) ++ self.run_hbactest( ++ self.USER_1, "externalhost.randomhost.com", "sshd", ++ should_allow=False ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_2, "externalhost.randomhost.com", "sshd", ++ self.HBAC_RULE, ++ expected_patterns=[ ++ r"Access granted: False", ++ rf"Not matched rules: {self.HBAC_RULE}" ++ ] ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ should_allow=True, rule=self.HBAC_RULE, nodetail=True ++ ) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ rule=self.HBAC_RULE, nodetail=True, ++ forbidden_patterns=[rf"Matched rules: {self.HBAC_RULE}"] ++ ) ++ ++ # Test SSH access from client to master ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.master, ++ source_host=src_client ++ ) ++ assert self.ssh_auth_failure( ++ self.USER_2, self.USER_PASSWORD, self.master, self.clients[1] ++ ) ++ ++ def test_hbacsvc_master_bug782927(self): ++ """hbacsvc_master_bug782927: test sizelimit option to hbactest ++ (bz782927)""" ++ tasks.kinit_admin(self.master) ++ try: ++ # Create multiple rules to test sizelimit ++ for i in range(1000, 1011): ++ # Create rule: test rule for size limit ++ tasks.hbacrule_add(self.master, f"rule_{i}") ++ self.master.run_command( ++ ["ipa", "config-mod", "--searchrecordslimit=10"] ++ ) ++ self.master.run_command(["ipa", "config-show"]) ++ # Test without specific size limit ++ result = self.master.run_command(["ipa", "hbacrule-find"]) ++ assert "Number of entries returned" in result.stdout_text ++ assert "10" in result.stdout_text ++ # Test with specific size limit ++ result = self.master.run_command( ++ ["ipa", "hbacrule-find", "--sizelimit=7"] ++ ) ++ assert "Number of entries returned" in result.stdout_text ++ assert "7" in result.stdout_text ++ finally: ++ # Restore config ++ self.master.run_command( ++ ["ipa", "config-mod", "--searchrecordslimit=100"] ++ ) ++ self.master.run_command(["ipa", "config-show"]) ++ # Cleanup ++ for i in range(1000, 1011): ++ tasks.hbacrule_del(self.master, f"rule_{i}") ++ ++ def test_hbacsvc_master_bug772852(self): ++ """hbacsvc_master_bug772852: error message with hbacrule in rules ++ option (bz772852)""" ++ tasks.kinit_admin(self.master) ++ RULE_772852 = "bug772852" ++ try: ++ # Create multiple rules to test with size limit ++ for i in range(1000, 1011): ++ # Create rule: test rule for size limit ++ tasks.hbacrule_add(self.master, f"rule_{i}") ++ self.master.run_command( ++ ["ipa", "config-mod", "--searchrecordslimit=15"] ++ ) ++ # Create rule: test rule for bug 772852 ++ tasks.hbacrule_add(self.master, RULE_772852) ++ tasks.hbacrule_add_user( ++ self.master, RULE_772852, users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, RULE_772852, ++ hosts=self.master.hostname) ++ tasks.hbacrule_add_service(self.master, RULE_772852, ++ services="sshd") ++ ++ # Test with --rules option (verifies bug 772852 - should not ++ # show "Unresolved rules") ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ RULE_772852, ++ expected_patterns=[ ++ r"Access granted: True", ++ rf"Matched rules: {RULE_772852}" ++ ] ++ ) ++ finally: ++ # Restore config ++ self.master.run_command( ++ ["ipa", "config-mod", "--searchrecordslimit=100"] ++ ) ++ self.master.run_command(["ipa", "config-show"]) ++ # Cleanup ++ for i in range(1000, 1011): ++ tasks.hbacrule_del(self.master, f"rule_{i}") ++ tasks.hbacrule_del(self.master, RULE_772852) ++ ++ def test_hbacsvc_master_bug766876(self): ++ """hbacsvc_master_bug766876: Make HBAC srchost processing optional ++ (bz766876)""" ++ RULE_766876 = "bug766876" ++ ++ tasks.kinit_admin(self.master) ++ # Create rule: allow user1 to access master with ssh ++ tasks.hbacrule_add(self.master, RULE_766876) ++ tasks.hbacrule_add_user(self.master, RULE_766876, ++ users=self.USER_1) ++ tasks.hbacrule_add_host(self.master, RULE_766876, ++ hosts=self.master.hostname) ++ tasks.hbacrule_add_service(self.master, RULE_766876, ++ services="sshd") ++ tasks.hbacrule_show(self.master, RULE_766876, extra_args=["--all"]) ++ ++ # Test (verifies bug 766876) Client-1 ++ for src_client in self.clients: ++ self.refresh_user_cache(src_client, self.USER_1) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.master, ++ source_host=src_client ++ ) ++ ++ def test_hbacsvc_master_bug766876_2(self, request): ++ """hbacsvc_master_bug766876_2: Make HBAC srchost processing ++ optional Case2, on master, bz766876""" ++ RULE_766876 = "bug766876" ++ request.addfinalizer(lambda: self.cleanup_resources( ++ hbacrules=[RULE_766876] ++ )) ++ tasks.kinit_admin(self.master) ++ self.master.run_command( ++ ["sed", "-i", "6iipa_hbac_support_srchost = true", ++ "/etc/sssd/sssd.conf"] ++ ) ++ tasks.clear_sssd_cache(self.master) ++ # Test (verifies bug 766876) Client ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.master ++ ) ++ ++ def test_hbacsvc_master_bug801769(self): ++ """hbacsvc_master_bug801769: hbactest returns failure when ++ hostgroups are chained (bz801769)""" ++ RULE_801769 = "bug801769" ++ HOSTGROUP_801769 = "hostgroup801769" ++ HOSTGROUP_801769_2 = "hostgroup801769_2" ++ tasks.kinit_admin(self.master) ++ try: ++ # Create hostgroup ++ tasks.hostgroup_add(self.master, HOSTGROUP_801769) ++ tasks.hostgroup_add_member(self.master, HOSTGROUP_801769, ++ hosts=self.master.hostname) ++ # Create rule: allow user1 to access master with ssh through a ++ # hostgroup ++ tasks.hbacrule_add(self.master, RULE_801769) ++ tasks.hbacrule_add_user( ++ self.master, RULE_801769, users=self.USER_1) ++ tasks.hbacrule_add_host( ++ self.master, RULE_801769, ++ extra_args=[f"--hostgroups={HOSTGROUP_801769}"]) ++ tasks.hbacrule_add_service(self.master, RULE_801769, ++ services="sshd") ++ tasks.hbacrule_show(self.master, RULE_801769, extra_args=["--all"]) ++ ++ # Test (verifies bug 801769) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ should_allow=True, rule=RULE_801769 ++ ) ++ ++ # Create chained hostgroup ++ tasks.hostgroup_add(self.master, HOSTGROUP_801769_2) ++ tasks.hostgroup_add_member( ++ self.master, HOSTGROUP_801769_2, ++ extra_args=[f"--hostgroups={HOSTGROUP_801769}"]) ++ ++ # Test with chained hostgroup (verifies bug is fixed) ++ self.run_hbactest( ++ self.USER_1, self.master.hostname, "sshd", ++ should_allow=True, rule=RULE_801769 ++ ) ++ finally: ++ tasks.hbacrule_del(self.master, RULE_801769) ++ tasks.hostgroup_del( ++ self.master, HOSTGROUP_801769_2, raiseonerr=False) ++ tasks.hostgroup_del( ++ self.master, HOSTGROUP_801769, raiseonerr=False) ++ tasks.clear_sssd_cache(self.master) ++ ++ def test_hbacsvc_master_bug771706(self): ++ """hbacsvc_master_bug771706: sssd crashes with empty service group ++ or hostgroup (bz771706)""" ++ RULE_771706 = "bug771706" ++ SVCGROUP_771706 = "svcgroup771706" ++ ++ tasks.kinit_admin(self.master) ++ # Create rule with empty service group (verifies bug 771706) ++ tasks.hbacrule_add( ++ self.master, RULE_771706, extra_args=["--hostcat=all"]) ++ tasks.hbacrule_add_user(self.master, RULE_771706, ++ users=self.USER_1) ++ tasks.hbacsvcgroup_add(self.master, SVCGROUP_771706) ++ tasks.hbacrule_add_service( ++ self.master, RULE_771706, ++ extra_args=[f"--hbacsvcgroups={SVCGROUP_771706}"]) ++ ++ # Access should fail with empty service group ++ tasks.kdestroy_all(self.master) ++ assert self.ssh_auth_failure( ++ self.USER_1, self.USER_PASSWORD, self.master ++ ) ++ ++ # Delete and recreate with valid service ++ tasks.kinit_admin(self.master) ++ tasks.hbacrule_del(self.master, RULE_771706) ++ # Create rule: allow user1 to access master with ssh ++ tasks.hbacrule_add(self.master, RULE_771706) ++ tasks.hbacrule_add_user(self.master, RULE_771706, ++ users=self.USER_1) ++ tasks.hbacrule_add_service(self.master, RULE_771706, ++ services="sshd") ++ tasks.hbacrule_add_host(self.master, RULE_771706, ++ hosts=self.master.hostname) ++ ++ # Now access should succeed ++ tasks.kdestroy_all(self.master) ++ self.master.run_command( ++ ["sed", "-i", "/ipa_hbac_support_srchost/d", ++ "/etc/sssd/sssd.conf"] ++ ) ++ tasks.clear_sssd_cache(self.master) ++ assert self.ssh_auth_success( ++ self.USER_1, self.USER_PASSWORD, self.master ++ ) +-- +2.52.0 + diff --git a/SOURCES/0137-ipatests-Add-DNS-functional-integration-tests.patch b/SOURCES/0137-ipatests-Add-DNS-functional-integration-tests.patch new file mode 100644 index 0000000..39e8013 --- /dev/null +++ b/SOURCES/0137-ipatests-Add-DNS-functional-integration-tests.patch @@ -0,0 +1,1279 @@ +From b2c83c462aba3d7367f01f665f14b126b7e74b9e 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/SOURCES/0138-ipatests-add-Random-Password-based-replica-promotion.patch b/SOURCES/0138-ipatests-add-Random-Password-based-replica-promotion.patch new file mode 100644 index 0000000..0dacb1f --- /dev/null +++ b/SOURCES/0138-ipatests-add-Random-Password-based-replica-promotion.patch @@ -0,0 +1,145 @@ +From 55b01956676add56e1660b23317107f3010fdf5d 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 +--- + ipatests/pytest_ipa/integration/tasks.py | 15 ++++ + .../test_replica_promotion.py | 87 +++++++++++++++++++ + 2 files changed, 102 insertions(+) + +diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py +index 561e021..c7b4f97 100755 +--- a/ipatests/pytest_ipa/integration/tasks.py ++++ b/ipatests/pytest_ipa/integration/tasks.py +@@ -3260,3 +3260,18 @@ def check_journal_does_not_contain_secret(host, cmd): + result = host.run_command(journalctl_cmd, raiseonerr=False) + assert (host.config.admin_password not in result.stdout_text) + assert (host.config.dirman_password not in result.stdout_text) ++ ++ ++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 3c67833..7859b56 100644 +--- a/ipatests/test_integration/test_replica_promotion.py ++++ b/ipatests/test_integration/test_replica_promotion.py +@@ -1367,3 +1367,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/SOURCES/0139-ipatests-Add-integration-tests-for-ipa-join-command.patch b/SOURCES/0139-ipatests-Add-integration-tests-for-ipa-join-command.patch new file mode 100644 index 0000000..3f20147 --- /dev/null +++ b/SOURCES/0139-ipatests-Add-integration-tests-for-ipa-join-command.patch @@ -0,0 +1,696 @@ +From 05198e8fcf3410d4f8a97e8c6282cbabef193980 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/pytest_ipa/integration/tasks.py | 49 ++ + ipatests/test_integration/test_ipa_join.py | 614 +++++++++++++++++++++ + 2 files changed, 663 insertions(+) + create mode 100644 ipatests/test_integration/test_ipa_join.py + +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/SOURCES/0140-ipatests-Add-DNS-bugzilla-integration-tests.patch b/SOURCES/0140-ipatests-Add-DNS-bugzilla-integration-tests.patch new file mode 100644 index 0000000..789da2e --- /dev/null +++ b/SOURCES/0140-ipatests-Add-DNS-bugzilla-integration-tests.patch @@ -0,0 +1,1627 @@ +From 8419410a181c411e37d5ef513691b72417423285 Mon Sep 17 00:00:00 2001 +From: PRANAV THUBE +Date: Wed, 21 Jan 2026 13:48:05 +0530 +Subject: [PATCH] ipatests: Add DNS bugzilla integration tests. + +Added TestDNSBugs class with tests for: +- DNS record operations (A, AAAA, CNAME, DNAME, TXT, KX, NS, PTR) +- Zone management and validation +- SOA serial number updates +- PTR record synchronization +- Allow-query and allow-transfer settings +- Dynamic update policy handling +- LDAP reconnection scenarios + +Fixes: https://pagure.io/freeipa/issue/9911 +Reviewed-By: Rob Crittenden +Reviewed-By: David Hanina +Reviewed-By: Rafael Guterres Jeffman +--- + ipatests/pytest_ipa/integration/tasks.py | 65 +- + ipatests/test_integration/test_dns.py | 1142 +++++++++++++++++++++- + ipatests/test_xmlrpc/test_dns_plugin.py | 142 +++ + 3 files changed, 1289 insertions(+), 60 deletions(-) + +diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py +index 47330d6d93401485e4eb7b2501cf5ea37498d719..d9f142794521ced8f96c2af46b1d2fb59cdcad87 100755 +--- a/ipatests/pytest_ipa/integration/tasks.py ++++ b/ipatests/pytest_ipa/integration/tasks.py +@@ -1629,7 +1629,7 @@ def add_a_record(master, host): + + + def add_dns_record(master, zone, name, record_type, record_value, +- raiseonerr=True): ++ *extra_args, raiseonerr=True): + """Add DNS record of any type. + + :param master: The IPA master host to run command on +@@ -1639,6 +1639,7 @@ def add_dns_record(master, zone, name, record_type, record_value, + 'txt', 'srv', 'mx', 'ptr', 'naptr', 'dname', + 'cert', 'loc', 'kx', etc. + :param record_value: List of values for the record ++ :param extra_args: Additional arguments (variable positional args) + :param raiseonerr: If True, raise exception on command failure + :return: Command result object + """ +@@ -1646,6 +1647,7 @@ def add_dns_record(master, zone, name, record_type, record_value, + opt = f'--{record_type}-rec' + for val in record_value: + command.extend([opt, val]) ++ command.extend(extra_args) + return master.run_command(command, raiseonerr=raiseonerr) + + +@@ -1678,19 +1680,31 @@ def find_dns_record(master, zone, name=None, raiseonerr=True): + return master.run_command(command, raiseonerr=raiseonerr) + + +-def mod_dns_record(master, zone, name, record_type, old_value, new_value, +- raiseonerr=True): ++def show_dns_record(master, zone, name, *extra_args, raiseonerr=True): ++ """Show DNS record details. ++ ++ :param master: The IPA host to run command on ++ :param zone: DNS zone name ++ :param name: Record name ++ :param extra_args: Additional arguments (variable positional args) ++ :param raiseonerr: If True, raise exception on command failure ++ :return: Command result object ++ """ ++ command = ['ipa', 'dnsrecord-show', zone, name] ++ command.extend(extra_args) ++ return master.run_command(command, raiseonerr=raiseonerr) ++ ++ ++def mod_dns_record(master, zone, name, *extra_args, 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 ++ :param zone: DNS zone name ++ :param name: Record name ++ :param extra_args: Additional arguments for dnsrecord-mod command + """ +- return master.run_command([ +- 'ipa', 'dnsrecord-mod', zone, name, +- f'--{record_type}-rec={old_value}', +- f'--{record_type}-data={new_value}' +- ], raiseonerr=raiseonerr) ++ command = ['ipa', 'dnsrecord-mod', zone, name] ++ command.extend(extra_args) ++ return master.run_command(command, raiseonerr=raiseonerr) + + + def resolve_record(nameserver, query, rtype="SOA", retry=True, timeout=100): +@@ -2121,11 +2135,36 @@ def find_dns_zone(host, zone, all_attrs=False, raiseonerr=True): + return host.run_command(command, raiseonerr=raiseonerr) + + +-def show_dns_zone(host, zone, all_attrs=False, raiseonerr=True): +- """Show DNS zone.""" ++def show_dns_zone(host, zone, all_attrs=False, raw=False, ++ *extra_args, raiseonerr=True): ++ """Show DNS zone. ++ ++ :param host: The IPA host to run command on ++ :param zone: DNS zone name ++ :param all_attrs: If True, show all attributes ++ :param raw: If True, show raw LDAP attributes ++ :param extra_args: Additional arguments (variable positional args) ++ :param raiseonerr: If True, raise exception on command failure ++ :return: Command result object ++ """ + command = ['ipa', 'dnszone-show', zone] + if all_attrs: + command.append('--all') ++ if raw: ++ command.append('--raw') ++ command.extend(extra_args) ++ return host.run_command(command, raiseonerr=raiseonerr) ++ ++ ++def mod_dns_zone(host, zone, *extra_args, raiseonerr=True): ++ """Modify DNS zone. ++ ++ :param host: The IPA host to run command on ++ :param zone: DNS zone name ++ :param extra_args: Additional arguments (variable positional args) ++ """ ++ command = ['ipa', 'dnszone-mod', zone] ++ command.extend(extra_args) + return host.run_command(command, raiseonerr=raiseonerr) + + +diff --git a/ipatests/test_integration/test_dns.py b/ipatests/test_integration/test_dns.py +index 905227497c8879bfe4cf8b027e396689c0451208..6840a10d1a301a8f19cfdf95896e08be7cfd7ea2 100644 +--- a/ipatests/test_integration/test_dns.py ++++ b/ipatests/test_integration/test_dns.py +@@ -7,7 +7,9 @@ from __future__ import absolute_import + + import time + ++import dns.exception + import dns.resolver ++from ipapython.dn import DN + from ipapython.dnsutil import DNSResolver + from ipatests.pytest_ipa.integration import tasks + from ipatests.test_integration.base import IntegrationTest +@@ -188,8 +190,7 @@ class TestDNSAcceptance(IntegrationTest): + 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]) ++ tasks.add_dns_record(self.master, ZONE, 'allll', 'a', [A_RECORD]) + ans = self.resolver.resolve(f'allll.{ZONE}', 'A') + assert A_RECORD in [r.address for r in ans] + +@@ -204,8 +205,7 @@ class TestDNSAcceptance(IntegrationTest): + + # 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) ++ tasks.add_dns_record(self.master, ZONE, 'aa2', 'a', 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] +@@ -224,12 +224,12 @@ class TestDNSAcceptance(IntegrationTest): + # ========================================================================= + + def test_aaaa_record(self): +- """Test AAAA record add, verify, delete, and invalid values.""" ++ """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]) ++ tasks.add_dns_record(self.master, ZONE, 'aaaa', 'aaaa', [AAAA]) + ans = self.resolver.resolve(f'aaaa.{ZONE}', 'AAAA') + assert AAAA in [r.address for r in ans] + +@@ -244,9 +244,7 @@ class TestDNSAcceptance(IntegrationTest): + + # 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) ++ 'aaaa', [AAAA_BAD1], raiseonerr=False) + assert result.returncode != 0 + try: + self.resolver.resolve(f'aaaab.{ZONE}', 'AAAA') +@@ -265,8 +263,7 @@ class TestDNSAcceptance(IntegrationTest): + + # AFSDB record: add, verify, delete, verify deleted + tasks.add_dns_record(self.master, ZONE, 'afsdb', +- record_type='afsdb', +- record_value=[f'0 {AFSDB}']) ++ 'afsdb', [f'0 {AFSDB}']) + ans = self.resolver.resolve(f'afsdb.{ZONE}', 'AFSDB') + assert AFSDB in [str(r.hostname) for r in ans] + +@@ -289,15 +286,13 @@ class TestDNSAcceptance(IntegrationTest): + 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]) ++ tasks.add_dns_record(self.master, ZONE, 'cname', 'cname', [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) ++ 'cname', ['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] +@@ -320,8 +315,7 @@ class TestDNSAcceptance(IntegrationTest): + 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]) ++ tasks.add_dns_record(self.master, ZONE, 'txt', 'txt', [TXT]) + ans = self.resolver.resolve(f'txt.{ZONE}', 'TXT') + assert any(TXT in str(r) for r in ans) + +@@ -344,8 +338,7 @@ class TestDNSAcceptance(IntegrationTest): + + # SRV record: add, verify, delete, verify deleted + tasks.add_dns_record(self.master, ZONE, '_srv', +- record_type='srv', +- record_value=[f'{SRV_A} {SRV}']) ++ 'srv', [f'{SRV_A} {SRV}']) + ans = self.resolver.resolve(f'_srv.{ZONE}', 'SRV') + assert SRV in [str(r.target) for r in ans] + +@@ -367,8 +360,7 @@ class TestDNSAcceptance(IntegrationTest): + + # MX record: add, verify, delete, verify deleted + tasks.add_dns_record(self.master, ZONE, '@', +- record_type='mx', +- record_value=[f'10 {self.MX}.']) ++ 'mx', [f'10 {self.MX}.']) + ans = self.resolver.resolve(ZONE, 'MX') + assert f'{self.MX}.' in [str(r.exchange) for r in ans] + +@@ -430,8 +422,7 @@ class TestDNSAcceptance(IntegrationTest): + 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]) ++ tasks.add_dns_record(self.master, PTR_ZONE, PTR, 'ptr', [PTR_VALUE]) + ans = self.resolver.resolve(f'{PTR}.{PTR_ZONE}', 'PTR') + assert PTR_VALUE in [str(r.target) for r in ans] + +@@ -454,8 +445,7 @@ class TestDNSAcceptance(IntegrationTest): + 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]) ++ tasks.add_dns_record(self.master, ZONE, 'naptr', 'naptr', [NAPTR]) + ans = self.resolver.resolve(f'naptr.{ZONE}', 'NAPTR') + assert any(NAPTR_FIND in str(r.regexp) for r in ans) + +@@ -477,16 +467,13 @@ class TestDNSAcceptance(IntegrationTest): + 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]) ++ tasks.add_dns_record(self.master, ZONE, 'dname', 'dname', [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) ++ 'dname', [DNAME2], raiseonerr=False) + assert result.returncode != 0 + + tasks.del_dns_record(self.master, ZONE, 'dname', +@@ -499,8 +486,7 @@ class TestDNSAcceptance(IntegrationTest): + 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]) ++ tasks.add_dns_record(self.master, ZONE, 'dname', 'dname', [DNAME2]) + ans = self.resolver.resolve(f'dname.{ZONE}', 'DNAME') + assert DNAME2 in [str(r.target) for r in ans] + +@@ -523,8 +509,7 @@ class TestDNSAcceptance(IntegrationTest): + + # CERT record: add, verify, delete, verify deleted + tasks.add_dns_record(self.master, ZONE, 'cert', +- record_type='cert', +- record_value=[f'{CERT_B} {CERT}']) ++ 'cert', [f'{CERT_B} {CERT}']) + ans = self.resolver.resolve(f'cert.{ZONE}', 'CERT') + assert any(CERT in str(r) for r in ans) + +@@ -547,8 +532,7 @@ class TestDNSAcceptance(IntegrationTest): + 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]) ++ tasks.add_dns_record(self.master, ZONE, '@', 'loc', [LOC]) + ans = self.resolver.resolve(ZONE, 'LOC') + assert any(LOC in str(r) for r in ans) + +@@ -571,8 +555,7 @@ class TestDNSAcceptance(IntegrationTest): + + # 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]) ++ tasks.add_dns_record(self.master, ZONE, '@', 'kx', [kx_val]) + ans = self.resolver.resolve(ZONE, 'KX') + assert int(KX_PREF1) in [r.preference for r in ans] + +@@ -592,8 +575,8 @@ class TestDNSAcceptance(IntegrationTest): + ] + 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) ++ self.master, ZONE, '@', 'kx', ++ [f'{bad_pref} {bad_target}'], raiseonerr=False) + assert result.returncode != 0 + try: + self.resolver.resolve(ZONE, 'KX') +@@ -718,15 +701,14 @@ class TestDNSAcceptance(IntegrationTest): + 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]) ++ tasks.add_dns_record( ++ self.master, self.ZONE_PSEARCH, 'txt', 'txt', [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) ++ f'--txt-rec={TXT}', f'--txt-data={NEW_TXT}') + ans = self.resolver.resolve(f'txt.{self.ZONE_PSEARCH}', 'TXT') + assert any(NEW_TXT in str(r) for r in ans) + +@@ -736,8 +718,7 @@ class TestDNSAcceptance(IntegrationTest): + + # 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) ++ f'--txt-rec={NEW_TXT}', f'--txt-data={NEWER_TXT}') + + # Verify serial increased + ans = self.resolver.resolve(self.ZONE_PSEARCH, 'SOA') +@@ -745,3 +726,1070 @@ class TestDNSAcceptance(IntegrationTest): + assert new_serial > old_serial, ( + f"New serial ({new_serial}) should be higher " + f"than old ({old_serial})") ++ ++ ++class TestDNSMisc(IntegrationTest): ++ """Tests for DNS related bugzilla fixes. ++ ++ This test class covers various DNS bugzilla fixes ported from ++ the shell-based test suite (t.dns_bz.sh). ++ ++ Tests are ordered to match the sequence in the original bash test file. ++ """ ++ topology = 'line' ++ num_clients = 0 ++ ++ # Test zone constants ++ ZONE = "newbzzone" ++ EMAIL = "ipaqar.redhat.com" ++ ++ @classmethod ++ def install(cls, mh): ++ super(TestDNSMisc, cls).install(mh) ++ tasks.kinit_admin(cls.master) ++ # Create test zone for bug tests ++ tasks.add_dns_zone( ++ cls.master, cls.ZONE, ++ skip_overlap_check=True, ++ admin_email=cls.EMAIL, ++ raiseonerr=False ++ ) ++ # Setup DNS resolver for test queries ++ cls.resolver = DNSResolver() ++ cls.resolver.nameservers = [cls.master.ip] ++ cls.resolver.lifetime = 10 ++ ++ @classmethod ++ def uninstall(cls, mh): ++ tasks.kinit_admin(cls.master) ++ # Cleanup test zone ++ tasks.del_dns_zone(cls.master, cls.ZONE) ++ super(TestDNSMisc, cls).uninstall(mh) ++ ++ def test_dns_local_zone_query_no_ldap_error(self): ++ """Test DNS server handles local zone queries without LDAP errors. ++ ++ Verify that DNS queries to local zone with invalid characters ++ (like commas) do not cause LDAP connection loss or DN syntax errors. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=814495 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ ++ # Restart IPA and wait for services ++ self.master.run_command(['ipactl', 'restart']) ++ ++ # Query with invalid characters - should not cause LDAP errors ++ try: ++ self.resolver.resolve(f'abc,xyz.{domain}', 'A') ++ except dns.exception.DNSException: ++ pass ++ time.sleep(10) ++ ++ # Check recent logs for LDAP errors (use journalctl for compatibility) ++ result = self.master.run_command([ ++ 'journalctl', '-u', 'named', '-n', '40', '--no-pager' ++ ], raiseonerr=False) ++ log_tail = result.stdout_text ++ ldap_err1 = 'connection to the ldap server was lost' ++ ldap_err2 = 'LDAP error: Invalid DN syntax' ++ assert (ldap_err1 not in log_tail.lower() ++ and ldap_err2 not in log_tail), "LDAP error found in logs" ++ ++ def test_dns_special_chars_no_crash(self): ++ """Test DNS queries with special characters don't cause crash. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=841900 ++ CVE-2012-3429 bind dyndb ldap named DoS via special chars. ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ ++ # Query with various special characters ++ # Using self.resolver which targets self.master.ip ++ special_chars = ['$', '@', '"', '(', ')', '..', ';', '\\'] ++ for char in special_chars: ++ try: ++ self.resolver.resolve(f'{char}.{domain}', 'A') ++ except dns.exception.DNSException: ++ pass ++ ++ # Check recent logs for crashes (use journalctl for compatibility) ++ result = self.master.run_command([ ++ 'journalctl', '-u', 'named', '-n', '40', '--no-pager' ++ ], raiseonerr=False) ++ log_tail = result.stdout_text ++ has_crash = 'REQUIRE' in log_tail and 'failed, back trace' in log_tail ++ assert not has_crash and '/var/named/core' not in log_tail, \ ++ "Crash or core dump found in logs" ++ ++ def test_dynamic_update_flag_preserved(self): ++ """Test DNS zone dynamic flag is not changed unexpectedly. ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=766075 ++ """ ++ tasks.kinit_admin(self.master) ++ zone = "example766075.com" ++ ++ try: ++ # Create zone with dynamic update enabled ++ tasks.add_dns_zone( ++ self.master, zone, ++ skip_overlap_check=True, ++ dynamic_update=True, ++ admin_email='admin@example.com' ++ ) ++ ++ # Verify dynamic update is True ++ result = tasks.show_dns_zone(self.master, zone) ++ assert "Dynamic update: True" in result.stdout_text ++ ++ # Modify another attribute, dynamic update should remain True ++ tasks.mod_dns_zone(self.master, zone, '--retry=600') ++ result = tasks.show_dns_zone(self.master, zone, all_attrs=True) ++ assert "Dynamic update: True" in result.stdout_text ++ ++ # Explicitly disable dynamic update ++ tasks.mod_dns_zone( ++ self.master, zone, '--dynamic-update=false' ++ ) ++ result = tasks.show_dns_zone(self.master, zone, all_attrs=True) ++ assert "Dynamic update: False" in result.stdout_text ++ ++ finally: ++ tasks.del_dns_zone(self.master, zone) ++ ++ def test_skip_invalid_record_in_zone(self): ++ """Test invalid record is skipped instead of refusing entire zone. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=751776 ++ """ ++ tasks.kinit_admin(self.master) ++ zone = "example751776.com" ++ ++ try: ++ # Add zone and a valid A record ++ tasks.add_dns_zone( ++ self.master, zone, ++ skip_overlap_check=True, ++ admin_email=f'admin@{zone}' ++ ) ++ tasks.add_dns_record( ++ self.master, zone, 'foo', 'a', ['10.0.0.1'] ++ ) ++ ++ # Verify A record resolves ++ result = self.resolver.resolve(f'foo.{zone}', 'A') ++ assert '10.0.0.1' in [r.to_text() for r in result] ++ ++ # Add a valid KX record ++ tasks.add_dns_record( ++ self.master, zone, '@', 'kx', [f'1 foo.{zone}'] ++ ) ++ ++ # Corrupt the KX record via LDAP (invalid format, no preference) ++ ldap = self.master.ldap_connect() ++ dn = DN( ++ ('idnsname', f'{zone}.'), ++ ('cn', 'dns'), ++ self.master.domain.basedn ++ ) ++ entry = ldap.get_entry(dn) ++ entry['kXRecord'] = [f'foo.{zone}'] ++ ldap.update_entry(entry) ++ ++ time.sleep(5) ++ ++ # Verify A record still resolves despite invalid KX record ++ result = self.resolver.resolve(f'foo.{zone}', 'A') ++ assert '10.0.0.1' in [r.to_text() for r in result] ++ ++ finally: ++ tasks.del_dns_zone(self.master, zone, raiseonerr=False) ++ ++ def test_bool_attributes_encoded_properly(self): ++ """Test bool attributes are encoded properly in setattr/addattr. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=797561 ++ """ ++ tasks.kinit_admin(self.master) ++ zone = "example797561.com" ++ ++ try: ++ tasks.add_dns_zone( ++ self.master, zone, ++ skip_overlap_check=True, ++ admin_email='admin@example.com' ++ ) ++ ++ # Check initial state (raw output needed for attribute name) ++ result = tasks.show_dns_zone( ++ self.master, zone, all_attrs=True, raw=True ++ ) ++ assert "idnsallowdynupdate: FALSE" in result.stdout_text ++ ++ # setattr should not allow adding when value exists ++ result = tasks.mod_dns_zone( ++ self.master, zone, ++ '--addattr=idnsAllowDynUpdate=true', ++ raiseonerr=False ++ ) ++ assert result.returncode != 0 ++ err_msg = "idnsallowdynupdate: Only one value allowed." ++ assert err_msg in result.stderr_text ++ ++ # setattr should work ++ tasks.mod_dns_zone( ++ self.master, zone, ++ '--setattr=idnsAllowDynUpdate=true' ++ ) ++ result = tasks.show_dns_zone( ++ self.master, zone, all_attrs=True, raw=True ++ ) ++ assert "idnsallowdynupdate: TRUE" in result.stdout_text ++ ++ finally: ++ tasks.del_dns_zone(self.master, zone) ++ ++ def test_admin_email_formatting(self): ++ """Test dnszone mod formats administrator's email properly. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=750806 ++ """ ++ tasks.kinit_admin(self.master) ++ zone = "example750806.com" ++ ++ try: ++ tasks.add_dns_zone( ++ self.master, zone, ++ skip_overlap_check=True, ++ admin_email='admin@example.com' ++ ) ++ ++ # Modify admin email with dots ++ tasks.mod_dns_zone( ++ self.master, zone, ++ '--admin-email=foo.bar@example.com' ++ ) ++ ++ result = tasks.show_dns_zone(self.master, zone) ++ # The dot in foo.bar should be escaped ++ assert "foo\\.bar.example.com" in result.stdout_text ++ ++ finally: ++ tasks.del_dns_zone(self.master, zone) ++ ++ def test_dns_zone_allow_query_transfer(self): ++ """Test DNS zones load when idnsAllowQuery/idnsAllowTransfer is filled. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=733371 ++ """ ++ tasks.kinit_admin(self.master) ++ zone = "example733371.com" ++ master_ip = self.master.ip ++ ++ try: ++ # Add zone ++ tasks.add_dns_zone( ++ self.master, zone, ++ skip_overlap_check=True, ++ admin_email="admin@example.com" ++ ) ++ ++ # Add a record ++ tasks.add_dns_record( ++ self.master, zone, 'foo', 'a', ['10.0.1.1'] ++ ) ++ ++ # Set allow-query to master IP ++ tasks.mod_dns_zone( ++ self.master, zone, ++ f'--allow-query={master_ip}' ++ ) ++ tasks.restart_named(self.master) ++ ++ # Query should work from allowed IP ++ result = self.master.run_command([ ++ 'dig', '+short', '-t', 'A', f'foo.{zone}', f'@{master_ip}' ++ ]) ++ assert '10.0.1.1' in result.stdout_text ++ ++ # Set allow-query to different IP (not master) ++ tasks.mod_dns_zone( ++ self.master, zone, ++ '--allow-query=10.0.1.1' ++ ) ++ tasks.restart_named(self.master) ++ ++ # Query should fail/be refused from master IP ++ result = self.master.run_command([ ++ 'dig', '+short', '-t', 'A', f'foo.{zone}', f'@{master_ip}' ++ ], raiseonerr=False) ++ # Should not return the IP when query is not allowed ++ assert '10.0.1.1' not in result.stdout_text ++ ++ finally: ++ tasks.del_dns_zone(self.master, zone, raiseonerr=False) ++ ++ def test_zone_deleted_when_removed_from_ldap(self): ++ """Test plugin deletes zone when removed from LDAP with zonerefresh. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=767492 ++ """ ++ tasks.kinit_admin(self.master) ++ zone = "unknownexample767492.com" ++ ++ try: ++ tasks.add_dns_zone( ++ self.master, zone, ++ skip_overlap_check=True, ++ admin_email='admin@unknownexample.com' ++ ) ++ tasks.mod_dns_zone(self.master, zone, '--refresh=30') ++ tasks.add_dns_record( ++ self.master, zone, 'foo', 'a', ['10.0.2.2'] ++ ) ++ ++ time.sleep(35) ++ # Verify record is resolvable ++ ans = self.resolver.resolve(f'foo.{zone}', 'A') ++ assert '10.0.2.2' in [r.address for r in ans] ++ ++ # Delete zone ++ tasks.del_dns_zone(self.master, zone, raiseonerr=True) ++ ++ # Verify zone is gone ++ result = tasks.find_dns_zone(self.master, zone, raiseonerr=False) ++ assert result.returncode != 0 ++ ++ time.sleep(35) ++ # Record should no longer resolve after zone deletion ++ try: ++ self.resolver.resolve(f'foo.{zone}', 'A') ++ raise AssertionError( ++ f"Resolving foo.{zone} should have failed") ++ except (dns.resolver.NXDOMAIN, dns.resolver.NoNameservers, ++ dns.resolver.NoAnswer): ++ pass # Expected - zone was deleted ++ ++ finally: ++ tasks.del_dns_zone(self.master, zone) ++ ++ def test_auto_ptr_record(self): ++ """Test automatic PTR record creation for A and AAAA records. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=767494 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ ++ # IPv4 test variables ++ ipv4_rev = "1.1.10.in-addr.arpa." ++ ipv4_addr = "10.1.1.10" ++ ++ # IPv6 test variables ++ ipv6_rev = "7.4.2.2.0.0.0.0.2.5.0.0.0.2.6.2.ip6.arpa." ++ ipv6_addr = "2620:52:0:2247:221:5eff:fe86:16b4" ++ ipv6_ptr = "4.b.6.1.6.8.e.f.f.f.e.5.1.2.2.0" ++ ++ # === IPv4 Test === ++ try: ++ tasks.add_dns_zone( ++ self.master, ipv4_rev, ++ skip_overlap_check=True, admin_email=self.EMAIL ++ ) ++ tasks.add_dns_record( ++ self.master, domain, 'foo', 'a', [ipv4_addr], ++ '--a-create-reverse' ++ ) ++ result = tasks.show_dns_record(self.master, ipv4_rev, '10') ++ assert f"foo.{domain}" in result.stdout_text ++ ++ # Duplicate should fail ++ result = tasks.add_dns_record( ++ self.master, domain, 'foo', 'a', [ipv4_addr], ++ '--a-create-reverse', raiseonerr=False ++ ) ++ assert result.returncode != 0 ++ assert "already exists" in result.stderr_text ++ ++ finally: ++ for zone, rec in [(ipv4_rev, '10'), (domain, 'foo')]: ++ tasks.del_dns_record( ++ self.master, zone, rec, del_all=True, raiseonerr=False ++ ) ++ tasks.del_dns_zone(self.master, ipv4_rev, raiseonerr=False) ++ ++ # === IPv6 Test === ++ try: ++ tasks.add_dns_zone( ++ self.master, ipv6_rev, ++ skip_overlap_check=True, admin_email=self.EMAIL ++ ) ++ tasks.add_dns_record( ++ self.master, domain, 'bar', 'aaaa', [ipv6_addr], ++ '--aaaa-create-reverse' ++ ) ++ result = tasks.show_dns_record(self.master, ipv6_rev, ipv6_ptr) ++ assert f"bar.{domain}" in result.stdout_text ++ ++ # Duplicate should fail ++ result = tasks.add_dns_record( ++ self.master, domain, 'bar', 'aaaa', [ipv6_addr], ++ '--aaaa-create-reverse', raiseonerr=False ++ ) ++ assert result.returncode != 0 ++ assert "already exists" in result.stderr_text ++ ++ finally: ++ for zone, rec in [(ipv6_rev, ipv6_ptr), (domain, 'bar')]: ++ tasks.del_dns_record( ++ self.master, zone, rec, del_all=True, raiseonerr=False ++ ) ++ tasks.del_dns_zone(self.master, ipv6_rev, raiseonerr=False) ++ ++ def test_serial_number_updates(self): ++ """Test DNS zone serial number updates when record changes. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=804619 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ ++ try: ++ # Get initial serial number (raw output needed) ++ result = tasks.show_dns_zone( ++ self.master, domain, all_attrs=True, raw=True ++ ) ++ serial_line = [ ++ line for line in result.stdout_text.split('\n') ++ if 'idnssoaserial' in line.lower() ++ ][0] ++ initial_serial = int(serial_line.split(':')[1].strip()) ++ ++ # Add a record ++ tasks.add_dns_record( ++ self.master, domain, 'dns175', 'a', ['192.168.0.1'] ++ ) ++ ++ # Get new serial number ++ result = tasks.show_dns_zone( ++ self.master, domain, all_attrs=True, raw=True ++ ) ++ serial_line = [ ++ line for line in result.stdout_text.split('\n') ++ if 'idnssoaserial' in line.lower() ++ ][0] ++ new_serial = int(serial_line.split(':')[1].strip()) ++ ++ assert new_serial > initial_serial, ( ++ f"Serial should have increased: " ++ f"{initial_serial} -> {new_serial}" ++ ) ++ ++ finally: ++ tasks.del_dns_record( ++ self.master, domain, 'dns175', ++ record_type='a', record_value=['192.168.0.1'] ++ ) ++ ++ def test_ns_hostname_requires_a_aaaa(self): ++ """Test NS record hostname must have A or AAAA record. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=804562 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ result = tasks.add_dns_record( ++ self.master, domain, 'dns176', ++ 'ns', [f'ns1.shanks.{domain}.'], raiseonerr=False ++ ) ++ assert result.returncode != 0 ++ assert "does not have a corresponding A/AAAA record" in \ ++ result.stderr_text ++ ++ def test_zone_forwarder_settings(self): ++ """Test zone forwarder settings can be modified. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=795414 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ ++ try: ++ # Set forwarders ++ tasks.mod_dns_zone( ++ self.master, domain, ++ '--forwarder=10.65.202.128', ++ '--forwarder=10.65.202.129', ++ '--forward-policy=first' ++ ) ++ ++ # Remove forwarders ++ tasks.mod_dns_zone( ++ self.master, domain, ++ '--forwarder=', '--forward-policy=' ++ ) ++ ++ finally: ++ # Ensure cleanup happens ++ tasks.mod_dns_zone( ++ self.master, domain, ++ '--forwarder=', '--forward-policy=', ++ raiseonerr=False ++ ) ++ ++ def test_soa_serial_length(self): ++ """Test correct SOA serial number length during installation. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=805871 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ ++ result = tasks.show_dns_zone(self.master, domain) ++ ++ # Extract serial from output ++ for line in result.stdout_text.splitlines(): ++ if 'Serial' in line: ++ serial = line.split(':')[1].strip() ++ # Serial should be 10 digits (YYYYMMDDNN format) ++ assert len(serial) == 10, \ ++ f"Serial length should be 10, got {len(serial)}" ++ break ++ ++ def test_dnsrecord_mod_error_messages(self): ++ """Test proper error message in dnsrecord-mod operations. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=804572 ++ """ ++ tasks.kinit_admin(self.master) ++ zone = "example804572.com" ++ ++ try: ++ tasks.add_dns_zone( ++ self.master, zone, ++ skip_overlap_check=True, ++ admin_email=self.EMAIL ++ ) ++ ++ # Test error when cname-hostname and cname-rec both provided ++ result = tasks.add_dns_record( ++ self.master, zone, 'bz804572', 'cname', [''], ++ f'--cname-hostname=bz804572.{zone}', raiseonerr=False ++ ) ++ assert result.returncode != 0 ++ err_msg = ("invalid 'cname_hostname': Raw value of a DNS record " ++ 'was already set by "cname_rec" option') ++ assert err_msg in result.stderr_text ++ ++ # Test error when modifying without specifying record ++ result = tasks.mod_dns_record( ++ self.master, zone, 'testbz804572', ++ '--a-ip-address=1.2.3.4', ++ raiseonerr=False ++ ) ++ assert result.returncode != 0 ++ assert "'a_rec' is required" in result.stderr_text ++ ++ finally: ++ tasks.del_dns_zone(self.master, zone) ++ ++ def test_reverse_dns_creation_option(self): ++ """Test option for adding Reverse DNS record upon forward creation. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=772301 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ ++ # IPv4 test variables ++ ipv4_addr = "10.1.1.10" ++ ipv4_rev = "1.1.10.in-addr.arpa." ++ ++ # IPv6 test variables ++ ipv6_addr = "2620:52:0:2247:221:5eff:fe86:16b4" ++ ipv6_rev = "7.4.2.2.0.0.0.0.2.5.0.0.0.2.6.2.ip6.arpa." ++ ipv6_ptr = "4.b.6.1.6.8.e.f.f.f.e.5.1.2.2.0" ++ ++ # === IPv4 Test === ++ try: ++ tasks.add_dns_zone( ++ self.master, ipv4_rev, ++ skip_overlap_check=True, admin_email=self.EMAIL ++ ) ++ tasks.add_dns_record( ++ self.master, domain, 'myhost', 'a', [ipv4_addr], ++ '--a-create-reverse' ++ ) ++ ++ # Verify forward record ++ result = tasks.find_dns_record(self.master, domain, 'myhost') ++ assert result.returncode == 0 ++ ++ # Verify reverse record ++ result = tasks.find_dns_record(self.master, ipv4_rev, '10') ++ assert result.returncode == 0 ++ ++ finally: ++ tasks.del_dns_record( ++ self.master, ipv4_rev, '10', del_all=True, raiseonerr=False ++ ) ++ tasks.del_dns_zone(self.master, ipv4_rev, raiseonerr=False) ++ ++ # === IPv6 Test === ++ try: ++ tasks.add_dns_zone( ++ self.master, ipv6_rev, ++ skip_overlap_check=True, admin_email=self.EMAIL ++ ) ++ tasks.add_dns_record( ++ self.master, domain, 'myhost', 'aaaa', [ipv6_addr], ++ '--aaaa-create-reverse' ++ ) ++ ++ # Verify forward record (now has AAAA) ++ result = tasks.find_dns_record(self.master, domain, 'myhost') ++ assert result.returncode == 0 ++ ++ # Verify reverse record ++ result = tasks.find_dns_record(self.master, ipv6_rev, ipv6_ptr) ++ assert result.returncode == 0 ++ ++ finally: ++ for zone, rec in [(ipv6_rev, ipv6_ptr), (domain, 'myhost')]: ++ tasks.del_dns_record( ++ self.master, zone, rec, del_all=True, raiseonerr=False ++ ) ++ tasks.del_dns_zone(self.master, ipv6_rev, raiseonerr=False) ++ ++ def test_non_ascii_chars_escaping(self): ++ """Test bind dyndb ldap escapes non ASCII characters correctly. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=818933 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ ++ # Query with comma in hostname ++ try: ++ self.resolver.resolve(f'foo,bar.{domain}', 'A') ++ except dns.exception.DNSException: ++ pass ++ ++ # Check logs for handle_connection_error bug (use journalctl) ++ result = self.master.run_command([ ++ 'journalctl', '-u', 'named', '-n', '100', '--no-pager' ++ ], raiseonerr=False) ++ assert 'bug in handle_connection_error' not in result.stdout_text, \ ++ "Bug in handle_connection_error found" ++ ++ def test_forwarder_help_text(self): ++ """Test proper help page for forwarder option. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=819635 ++ """ ++ tasks.kinit_admin(self.master) ++ ++ result = self.master.run_command([ ++ 'ipa', 'dnszone-mod', '--help' ++ ]) ++ # Should mention per-zone forwarders, not global ++ assert "global forwarders" not in result.stdout_text.lower() or \ ++ "per-zone forwarders" in result.stdout_text.lower() ++ ++ def test_delete_host_updates_dns(self): ++ """Test DNS is updated when deleting host with --updatedns. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=828687 ++ """ ++ tasks.kinit_admin(self.master) ++ ip_base = self.master.ip.rsplit('.', 1)[0] ++ test_ip = f"{ip_base}.252" ++ test_zone = "llnewzone." ++ # Reverse zone: take first 3 octets, reverse, join ++ rev = '.'.join(self.master.ip.split('.')[:3][::-1]) ++ reverse_zone = f"{rev}.in-addr.arpa." ++ ++ try: ++ tasks.add_dns_zone(self.master, test_zone, skip_overlap_check=True, ++ admin_email=self.EMAIL) ++ self.master.run_command([ ++ 'ipa', 'host-add', f'--ip-address={test_ip}', f'tt.{test_zone}' ++ ]) ++ ++ # Verify PTR record was added ++ result = self.master.run_command([ ++ 'ipa', 'dnsrecord-find', reverse_zone, '252' ++ ], raiseonerr=False) ++ if result.returncode == 0: ++ assert test_zone in result.stdout_text ++ ++ # Delete host with --updatedns ++ self.master.run_command([ ++ 'ipa', 'host-del', f'tt.{test_zone}', '--updatedns' ++ ]) ++ ++ finally: ++ self.master.run_command([ ++ 'ipa', 'host-del', f'tt.{test_zone}' ++ ], raiseonerr=False) ++ tasks.del_dns_zone(self.master, test_zone, raiseonerr=False) ++ ++ def test_ns_record_nonfqdn_validation(self): ++ """Test NS record validation appends zone name to non-FQDN. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=813380 ++ """ ++ tasks.kinit_admin(self.master) ++ host_ip = self.master.ip.rsplit('.', 1)[0] + '.253' ++ zone = "llnewzone813380.com" ++ host = f'nsnew.{zone}' ++ ++ try: ++ tasks.add_dns_zone(self.master, zone, skip_overlap_check=True, ++ admin_email=self.EMAIL) ++ ++ # Create host for NS ++ self.master.run_command([ ++ 'ipa', 'host-add', host, f'--ip-address={host_ip}' ++ ]) ++ ++ # Add non-FQDN NS record (should work by appending zone) ++ tasks.add_dns_record(self.master, zone, '@', 'ns', ['nsnew']) ++ ++ # Verify NS record ++ result = tasks.show_dns_record(self.master, zone, '@') ++ assert 'nsnew' in result.stdout_text ++ ++ finally: ++ self.master.run_command([ ++ 'ipa', 'host-del', host, '--updatedns' ++ ], raiseonerr=False) ++ tasks.del_dns_zone(self.master, zone, raiseonerr=False) ++ ++ def test_reverse_zone_creation(self): ++ """Test reverse zones are created correctly from IP prefix. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=798493 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ ++ test_cases = [ ++ ('10.11.12.0/24', '12.11.10.in-addr.arpa.'), ++ ('10.11.12.0/20', '11.10.in-addr.arpa.'), ++ ('10.11.12.0/16', '11.10.in-addr.arpa.'), ++ ] ++ ++ for forward, reverse in test_cases: ++ try: ++ # Create reverse zone from IP (special --name-from-ip option) ++ self.master.run_command([ ++ 'ipa', 'dnszone-add', ++ f'--admin-email=admin@{domain}', ++ f'--name-from-ip={forward}', ++ '--skip-overlap-check' ++ ], stdin_text='\n') ++ ++ # Verify zone was created ++ result = tasks.find_dns_zone(self.master, reverse) ++ assert f"Zone name: {reverse}" in result.stdout_text ++ ++ finally: ++ tasks.del_dns_zone(self.master, reverse) ++ ++ def test_rndc_reload_no_crash(self): ++ """Test rndc reload does not cause crash with persistent search. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=829728 ++ """ ++ tasks.kinit_admin(self.master) ++ ++ # Verify named is running ++ assert tasks.host_service_active(self.master, 'named') ++ ++ # Run rndc reload ++ result = self.master.run_command(['rndc', 'reload']) ++ assert result.returncode == 0 ++ ++ # Verify named is still running ++ assert tasks.host_service_active(self.master, 'named') ++ ++ def test_zone_transfer_non_fqdn(self): ++ """Test zone transfers work for certain non-FQDNs. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=829388 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ host = "bz829388" ++ ++ try: ++ # Enable zone transfers ++ tasks.mod_dns_zone( ++ self.master, domain, ++ "--allow-transfer=any;" ++ ) ++ ++ # Add a CNAME record without FQDN ++ tasks.add_dns_record( ++ self.master, domain, host, 'cname', [host] ++ ) ++ ++ # Restart IPA ++ self.master.run_command(['ipactl', 'restart']) ++ ++ # Check zone transfer includes FQDN ++ result = self.master.run_command([ ++ 'dig', '-t', 'AXFR', f'@{self.master.hostname}', domain ++ ]) ++ assert f'{host}.{domain}.' in result.stdout_text ++ ++ finally: ++ tasks.del_dns_record( ++ self.master, domain, host, ++ record_type='cname', record_value=[host], ++ raiseonerr=False ++ ) ++ tasks.mod_dns_zone( ++ self.master, domain, ++ "--allow-transfer=none;", ++ raiseonerr=False ++ ) ++ ++ def test_dname_cname_conflict(self): ++ """Test DNAME record validation and conflict with CNAME. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=915805 ++ """ ++ tasks.kinit_admin(self.master) ++ zone = "newzonebz915805" ++ cname = "m.l.k." ++ dname = f"bar.{zone}." ++ dname2 = f"bar_underscore.{zone}." ++ ++ try: ++ # Create zone ++ tasks.add_dns_zone( ++ self.master, zone, ++ admin_email=self.EMAIL, ++ refresh=303, retry=101, ++ expire=1202, minimum=33, ttl=55 ++ ) ++ ++ # Add CNAME ++ tasks.add_dns_record( ++ self.master, zone, 'cname', 'cname', [cname] ++ ) ++ ++ # DNAME on same name should fail (conflicts with CNAME) ++ result = tasks.add_dns_record( ++ self.master, zone, 'cname', 'dname', [dname], raiseonerr=False ++ ) ++ assert result.returncode != 0 ++ ++ # DNAME on different name should succeed ++ tasks.add_dns_record( ++ self.master, zone, 'dname', 'dname', [dname] ++ ) ++ ++ # Second DNAME on same name should fail ++ result = tasks.add_dns_record( ++ self.master, zone, 'dname', 'dname', [dname2], raiseonerr=False ++ ) ++ assert result.returncode != 0 ++ ++ finally: ++ tasks.del_dns_zone(self.master, zone) ++ ++ def test_soa_serial_increments(self): ++ """Test SOA serial number increments for external changes. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=840383 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ zone = f"zone840383.{domain}" ++ txt_value = "bug test" ++ new_txt_value = "Bug Test for 840383" ++ ++ try: ++ # Add zone ++ tasks.add_dns_zone( ++ self.master, zone, ++ skip_overlap_check=True, ++ admin_email=self.EMAIL ++ ) ++ ++ # Add TXT record ++ tasks.add_dns_record( ++ self.master, zone, 'txt', 'txt', [f'"{txt_value}"'] ++ ) ++ ++ # Enable zone transfers ++ tasks.mod_dns_zone( ++ self.master, zone, ++ "--allow-transfer=any;" ++ ) ++ ++ self.master.run_command(['ipactl', 'restart']) ++ ++ # Verify TXT record is part of zone transfer ++ result = self.master.run_command([ ++ 'dig', f'@{self.master.ip}', '-t', 'AXFR', zone ++ ]) ++ assert 'TXT' in result.stdout_text ++ ++ # Get old serial ++ result = self.master.run_command([ ++ 'dig', zone, '+multiline', '-t', 'SOA' ++ ]) ++ old_serial = next( ++ int(ln.split()[0]) for ln in result.stdout_text.splitlines() ++ if 'serial' in ln.lower() ++ ) ++ ++ # Modify TXT record ++ tasks.mod_dns_record( ++ self.master, zone, 'txt', ++ f'--txt-rec="{txt_value}"', f'--txt-data="{new_txt_value}"' ++ ) ++ ++ # Get new serial - should increment after record modification ++ result = self.master.run_command([ ++ 'dig', zone, '+multiline', '-t', 'SOA' ++ ]) ++ new_serial = next( ++ int(ln.split()[0]) for ln in result.stdout_text.splitlines() ++ if 'serial' in ln.lower() ++ ) ++ assert new_serial > old_serial ++ ++ # Enable dynamic updates - serial should NOT change ++ tasks.mod_dns_zone( ++ self.master, zone, ++ "--dynamic-update=true" ++ ) ++ ++ # Get current serial - should NOT change for zone attr update ++ result = self.master.run_command([ ++ 'dig', zone, '+multiline', '-t', 'SOA' ++ ]) ++ current_serial = next( ++ int(ln.split()[0]) for ln in result.stdout_text.splitlines() ++ if 'serial' in ln.lower() ++ ) ++ assert current_serial == new_serial ++ ++ finally: ++ tasks.del_dns_zone(self.master, zone, raiseonerr=False) ++ ++ def test_allow_query_transfer_ipv6(self): ++ """Test allow-query and allow-transfer with IPv4 and IPv6. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=701677 ++ """ ++ tasks.kinit_admin(self.master) ++ zone = "example.com" ++ ipv4 = self.master.ip ++ ipv6_added = False ++ temp_ipv6 = '2001:0db8:0:f101::1/64' ++ ++ # Get default network interface ++ result = self.master.run_command([ ++ 'sh', '-c', ++ "/sbin/ip -4 route show | grep ^default | " ++ "awk '{print $5}' | head -1" ++ ]) ++ eth = result.stdout_text.strip() ++ ++ # Add temporary IPv6 if none exists ++ result = self.master.run_command( ++ ['ip', 'addr', 'show', 'scope', 'global'], raiseonerr=False ++ ) ++ if 'inet6' not in result.stdout_text: ++ self.master.run_command([ ++ '/sbin/ip', '-6', 'addr', 'add', temp_ipv6, 'dev', eth ++ ]) ++ ipv6_added = True ++ ++ # Get IPv6 address ++ result = self.master.run_command([ ++ 'sh', '-c', ++ "ip addr show scope global | " ++ "sed -e's/^.*inet6 \\([^ ]*\\)\\/.*/\\1/;t;d'" ++ ]) ++ ipv6 = result.stdout_text.strip().split('\n')[0] ++ ++ try: ++ tasks.add_dns_zone(self.master, zone, skip_overlap_check=True, ++ admin_email=self.EMAIL) ++ ++ # Test allow-query: IPv4 allowed, IPv6 denied ++ tasks.mod_dns_zone( ++ self.master, zone, ++ f"--allow-query={ipv4};!{ipv6};" ++ ) ++ result = self.master.run_command( ++ ['dig', f'@{ipv4}', '-t', 'soa', zone], raiseonerr=False ++ ) ++ assert 'ANSWER SECTION' in result.stdout_text ++ result = self.master.run_command( ++ ['dig', f'@{ipv6}', '-t', 'soa', zone], raiseonerr=False ++ ) ++ assert 'ANSWER SECTION' not in result.stdout_text ++ ++ # Test allow-query: IPv6 allowed, IPv4 denied ++ tasks.mod_dns_zone( ++ self.master, zone, ++ f"--allow-query={ipv6};!{ipv4};" ++ ) ++ result = self.master.run_command( ++ ['dig', f'@{ipv4}', '-t', 'soa', zone], raiseonerr=False ++ ) ++ assert 'ANSWER SECTION' not in result.stdout_text ++ result = self.master.run_command( ++ ['dig', f'@{ipv6}', '-t', 'soa', zone], raiseonerr=False ++ ) ++ assert 'ANSWER SECTION' in result.stdout_text ++ ++ # Reset allow-query to any ++ tasks.mod_dns_zone( ++ self.master, zone, "--allow-query=any;" ++ ) ++ ++ # Test allow-transfer: IPv4 allowed, IPv6 denied ++ tasks.mod_dns_zone( ++ self.master, zone, ++ f"--allow-transfer={ipv4};!{ipv6};" ++ ) ++ result = self.master.run_command( ++ ['dig', f'@{ipv4}', zone, 'axfr'], raiseonerr=False ++ ) ++ assert 'Transfer failed' not in result.stdout_text ++ result = self.master.run_command( ++ ['dig', f'@{ipv6}', zone, 'axfr'], raiseonerr=False ++ ) ++ assert 'Transfer failed' in result.stdout_text ++ ++ # Test allow-transfer: IPv6 allowed, IPv4 denied ++ tasks.mod_dns_zone( ++ self.master, zone, ++ f"--allow-transfer={ipv6};!{ipv4};" ++ ) ++ result = self.master.run_command( ++ ['dig', f'@{ipv4}', zone, 'axfr'], raiseonerr=False ++ ) ++ assert 'Transfer failed' in result.stdout_text ++ result = self.master.run_command( ++ ['dig', f'@{ipv6}', zone, 'axfr'], raiseonerr=False ++ ) ++ assert 'Transfer failed' not in result.stdout_text ++ ++ finally: ++ tasks.del_dns_zone(self.master, zone, raiseonerr=False) ++ if ipv6_added: ++ self.master.run_command([ ++ '/sbin/ip', '-6', 'addr', 'del', temp_ipv6, 'dev', eth ++ ], raiseonerr=False) +diff --git a/ipatests/test_xmlrpc/test_dns_plugin.py b/ipatests/test_xmlrpc/test_dns_plugin.py +index bff4b40aef6e5adec21c8929719e99669b80cdf0..3ab54674b8a0154f5a4ef36568ba53af73548f79 100644 +--- a/ipatests/test_xmlrpc/test_dns_plugin.py ++++ b/ipatests/test_xmlrpc/test_dns_plugin.py +@@ -529,6 +529,16 @@ class test_dns(Declarative): + ), + + ++ # Test for BZ 783272: proper error for record add to nonexistent zone ++ dict( ++ desc='Try to add record to non-existent zone (BZ 783272)', ++ command=('dnsrecord_add', [u'unknowndomain.test.', u'testrecord'], ++ {'locrecord': u'49 11 42.4 N 16 36 29.6 E 227.64m'}), ++ expected=errors.NotFound( ++ reason=u'unknowndomain.test.: DNS zone not found'), ++ ), ++ ++ + dict( + desc='Create zone %r' % zone1, + command=( +@@ -1267,6 +1277,18 @@ class test_dns(Declarative): + ), + + ++ # Test for BZ 789919: IP address with three octets should be rejected ++ dict( ++ desc='Try to add A record with 3-octet IP to %r in zone %r ' ++ '(BZ 789919)' % (name1, zone1), ++ command=('dnsrecord_add', [zone1, name1], ++ {'arecord': u'1.1.1'}), ++ 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), +@@ -1299,6 +1321,17 @@ class test_dns(Declarative): + }, + ), + ++ # Test for BZ 789987: error when deleting non-existent AAAA value ++ dict( ++ desc='Try to delete non-existent AAAA record value from %r ' ++ '(BZ 789987)' % name1, ++ command=('dnsrecord_del', [zone1, name1], ++ {'aaaarecord': u'2620:52:0:41c9:5054:ff:fe62:65'}), ++ expected=errors.AttrValueNotFound( ++ attr='AAAA record', ++ value=u'2620:52:0:41c9:5054:ff:fe62:65'), ++ ), ++ + dict( + desc='Try to add invalid MX record to zone %r using dnsrecord_add' % (zone1), + command=('dnsrecord_add', [zone1, u'@'], {'mxrecord': zone1_ns }), +@@ -1655,6 +1688,26 @@ class test_dns(Declarative): + u' (see RFC 2230 for details)'), + ), + ++ # Test for BZ 738788: KX record with negative preference ++ dict( ++ desc='Try to add KX record with negative preference (BZ 738788)', ++ command=('dnsrecord_add', [zone1, name1], ++ {'kxrecord': u'-1 1.2.3.4'}), ++ expected=errors.ValidationError( ++ name='preference', ++ error=u'must be at least 0'), ++ ), ++ ++ # Test for BZ 738788: KX record with preference exceeding max ++ dict( ++ desc='Try to add KX record with preference > max (BZ 738788)', ++ command=('dnsrecord_add', [zone1, name1], ++ {'kxrecord': u'333383838383 1.2.3.4'}), ++ expected=errors.ValidationError( ++ name='preference', ++ error=u'can be at most 65535'), ++ ), ++ + dict( + desc='Add KX record to %r using dnsrecord_add' % (name1), + command=('dnsrecord_add', [zone1, name1], {'kxrecord': u'1 foo-1' }), +@@ -6599,6 +6652,95 @@ class test_dns_soa(Declarative): + ), + ), + ++ # BZ 817413: Test middle label longer than 63 chars ++ dict( ++ desc='Try to add zone with middle label > 63 chars (BZ 817413)', ++ command=( ++ 'dnszone_add', ++ [u'domain.sixthreemax.' ++ u'12345678901234567890123345678901234567890' ++ u'123456789012345678901234567890.com'], ++ {} ++ ), ++ expected=errors.ConversionError( ++ name='name', ++ error=u'DNS label cannot be longer than 63 characters' ++ ), ++ ), ++ ++ # BZ 817413: Test first label longer than 63 chars ++ dict( ++ desc='Try to add zone with first label > 63 chars (BZ 817413)', ++ command=( ++ 'dnszone_add', ++ [u'firstlkjhjklasghduygasiudfygvq7i6ertf78q6t4871y8347y2r8734' ++ u'y87aylfisduhcvkljasnkljnasdljdnclakj.long.com'], ++ {} ++ ), ++ expected=errors.ConversionError( ++ name='name', ++ error=u'DNS label cannot be longer than 63 characters' ++ ), ++ ), ++ ++ # BZ 817413: Test TLD longer than 63 chars ++ dict( ++ desc='Try to add zone with TLD > 63 chars (BZ 817413)', ++ command=( ++ 'dnszone_add', ++ [u'long.tld.tldlkjhjklasghduygasiudfygvq7i6ertf78q6t4871y8347' ++ u'y2r8734y87aylfisduhcvkljasnkljnasdljdnclakj'], ++ {} ++ ), ++ expected=errors.ConversionError( ++ name='name', ++ error=u'DNS label cannot be longer than 63 characters' ++ ), ++ ), ++ ++ # BZ 817413: Test numeric TLD is allowed (success case) ++ dict( ++ desc='Add zone with numeric TLD (BZ 817413)', ++ command=('dnszone_add', [u'domain.numeric.123.'], {}), ++ expected={ ++ 'value': DNSName(u'domain.numeric.123.'), ++ 'summary': None, ++ 'result': { ++ 'dn': DN(('idnsname', 'domain.numeric.123.'), ++ api.env.container_dns, api.env.basedn), ++ 'idnsname': [DNSName(u'domain.numeric.123.')], ++ 'idnszoneactive': [True], ++ 'idnssoamname': [DNSName(api.env.host)], ++ 'nsrecord': lambda x: True, ++ 'idnssoarname': lambda x: True, ++ 'idnssoaserial': [fuzzy_digits], ++ 'idnssoarefresh': [fuzzy_digits], ++ 'idnssoaretry': [fuzzy_digits], ++ 'idnssoaexpire': [fuzzy_digits], ++ 'idnssoaminimum': [fuzzy_digits], ++ 'idnsallowdynupdate': [False], ++ 'idnsupdatepolicy': [u'grant %(realm)s krb5-self * A; ' ++ u'grant %(realm)s krb5-self * AAAA; ' ++ u'grant %(realm)s krb5-self * SSHFP;' ++ % dict(realm=api.env.realm)], ++ 'idnsallowtransfer': [u'none;'], ++ 'idnsallowquery': [u'any;'], ++ 'objectclass': objectclasses.dnszone, ++ }, ++ }, ++ ), ++ ++ # BZ 817413: Delete zone with numeric TLD (cleanup) ++ dict( ++ desc='Delete zone with numeric TLD (BZ 817413)', ++ command=('dnszone_del', [u'domain.numeric.123.'], {}), ++ expected={ ++ 'value': [DNSName(u'domain.numeric.123.')], ++ 'summary': u'Deleted DNS zone "domain.numeric.123."', ++ 'result': {'failed': []}, ++ }, ++ ), ++ + dict( + desc='Adding a zone - %r - with invalid s e-mail - %r' % + (zone6, zone6_rname_invalid_dnsname), +-- +2.52.0 + diff --git a/SOURCES/0141-ipatests-Add-DNS-integration-tests.patch b/SOURCES/0141-ipatests-Add-DNS-integration-tests.patch new file mode 100644 index 0000000..af59939 --- /dev/null +++ b/SOURCES/0141-ipatests-Add-DNS-integration-tests.patch @@ -0,0 +1,1122 @@ +From 0430d73f026309b2d2822b3110487a1a2f51bcc7 Mon Sep 17 00:00:00 2001 +From: PRANAV THUBE +Date: Thu, 12 Feb 2026 10:54:17 +0530 +Subject: [PATCH] ipatests: Add DNS integration tests. + +Tests cover LDAP connection handling, PTR synchronization, +update policies, record validation, and zone management fixes. + +Fixes: https://pagure.io/freeipa/issue/9911 +Reviewed-By: Sudhir Menon +Reviewed-By: David Hanina +--- + ipatests/test_integration/test_dns.py | 1078 ++++++++++++++++++++++++- + 1 file changed, 1076 insertions(+), 2 deletions(-) + +diff --git a/ipatests/test_integration/test_dns.py b/ipatests/test_integration/test_dns.py +index 6840a10d1a301a8f19cfdf95896e08be7cfd7ea2..4b9ab1fe8d7b8884760ed637cb2fcc5d5a060df0 100644 +--- a/ipatests/test_integration/test_dns.py ++++ b/ipatests/test_integration/test_dns.py +@@ -6,12 +6,11 @@ + from __future__ import absolute_import + + import time +- + import dns.exception + import dns.resolver + from ipapython.dn import DN + from ipapython.dnsutil import DNSResolver +-from ipatests.pytest_ipa.integration import tasks ++from ipatests.pytest_ipa.integration import tasks, skip_if_fips + from ipatests.test_integration.base import IntegrationTest + + # ============================================================================= +@@ -71,6 +70,35 @@ PTR_TTL = 59 + NEW_TXT = "newip=5.6.7.8" + NEWER_TXT = "newip=8.7.6.5" + ++# TSIG key configuration for dynamic DNS updates (hmac-md5) ++KEY_NAME = "selfupdate" ++KEY_SECRET = "05Fu1ACKv1/1Ag==" ++KEY_CONFIG = f'''key {KEY_NAME} {{ ++ algorithm hmac-md5; ++ secret "{KEY_SECRET}"; ++}}; ++''' ++ ++# nsupdate template for deleting A record ++NSUPDATE_DELETE_A_TEMPLATE = """debug ++update delete {hostname} IN A {ip} ++send ++""" ++ ++# nsupdate template for adding AAAA record ++NSUPDATE_ADD_AAAA_TEMPLATE = """debug ++update add {hostname} {ttl} IN AAAA {ip} ++send ++""" ++ ++# nsupdate template for adding A record with TSIG key ++NSUPDATE_ADD_A_WITH_KEY_TEMPLATE = """server {server} ++zone {zone} ++key {key_name} {key_secret} ++update add {hostname} {ttl} IN A {ip} ++send ++""" ++ + + class TestDNS(IntegrationTest): + """Tests for DNS feature. +@@ -1793,3 +1821,1049 @@ class TestDNSMisc(IntegrationTest): + self.master.run_command([ + '/sbin/ip', '-6', 'addr', 'del', temp_ipv6, 'dev', eth + ], raiseonerr=False) ++ ++ @skip_if_fips(reason='hmac-md5 not supported in FIPS mode') ++ def test_updatepolicy_zonesub(self): ++ """Test BIND with bind-dyndb-ldap works with zonesub updatepolicy. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=921167 ++ ++ This test verifies that dynamic DNS updates work correctly when ++ using the 'zonesub' match type in the update policy. Note that ++ this test requires hmac-md5 which is not supported in FIPS mode. ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ realm = self.master.domain.realm ++ ++ # Get IP address components for test record ++ ip_parts = self.master.ip.split('.') ++ test_ip = f'{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}.100' ++ ++ # Backup named.conf ++ self.master.run_command([ ++ 'cp', '/etc/named.conf', '/etc/named.conf.bak' ++ ]) ++ ++ try: ++ # Add key to named.conf for dynamic updates ++ self.master.run_command([ ++ 'sh', '-c', ++ f'echo \'{KEY_CONFIG}\' >> /etc/named.conf' ++ ]) ++ ++ # Update zone policy to include zonesub match type ++ update_policy = ( ++ f'grant {realm} krb5-self * A; ' ++ f'grant {realm} krb5-self * AAAA; ' ++ f'grant {realm} krb5-self * SSHFP; ' ++ 'grant selfupdate zonesub A;' ++ ) ++ tasks.mod_dns_zone( ++ self.master, domain, f'--update-policy={update_policy}' ++ ) ++ ++ # Restart named to pick up changes ++ tasks.restart_named(self.master) ++ ++ # Create nsupdate commands file ++ nsupdate_content = NSUPDATE_ADD_A_WITH_KEY_TEMPLATE.format( ++ server=self.master.hostname, ++ zone=domain, ++ key_name=KEY_NAME, ++ key_secret=KEY_SECRET, ++ hostname=f'foobz921167.{domain}.', ++ ttl=60, ++ ip=test_ip ++ ) ++ self.master.put_file_contents( ++ '/tmp/dnsupdate.txt', nsupdate_content ++ ) ++ ++ # Execute nsupdate with zonesub policy and verify NOERROR ++ result = self.master.run_command([ ++ 'nsupdate', '-v', '-D', '/tmp/dnsupdate.txt' ++ ]) ++ assert 'NOERROR' in result.stdout_text, \ ++ 'Dynamic update did not return NOERROR' ++ ++ finally: ++ # Restore original named.conf ++ self.master.run_command([ ++ 'cp', '/etc/named.conf.bak', '/etc/named.conf' ++ ], raiseonerr=False) ++ ++ # Restore original update policy ++ original_policy = ( ++ f'grant {realm} krb5-self * A; ' ++ f'grant {realm} krb5-self * AAAA; ' ++ f'grant {realm} krb5-self * SSHFP;' ++ ) ++ tasks.mod_dns_zone( ++ self.master, domain, f'--update-policy={original_policy}', ++ raiseonerr=False ++ ) ++ ++ # Cleanup test record if it was created ++ tasks.del_dns_record( ++ self.master, domain, 'foobz921167', ++ del_all=True, raiseonerr=False ++ ) ++ ++ # Restart named ++ tasks.restart_named(self.master) ++ ++ def test_bind_shutdown_ldap_failure(self): ++ """Test BIND shuts down correctly when psearch enabled and LDAP fails. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=802375 ++ ++ This test verifies that named service can be stopped cleanly ++ even when LDAP connection is configured with an invalid URI. ++ The test modifies named.conf to point to an invalid LDAP URI, ++ then verifies that 'rndc stop' works correctly. ++ """ ++ tasks.kinit_admin(self.master) ++ ++ # Get realm name with dashes instead of dots for socket path ++ realm_inst = self.master.domain.realm.replace('.', '-') ++ ++ try: ++ # Check named is running ++ assert tasks.host_service_active(self.master, 'named') ++ ++ # Backup named.conf ++ self.master.run_command(['cp', '/etc/named.conf', '/root/']) ++ ++ # Comment out the valid LDAP URI and add invalid one ++ # The valid URI looks like: ++ # uri "ldapi://%2fvar%2frun%2fslapd-REALM.socket"; ++ sed_pattern = ( ++ f's|uri "ldapi://%2fvar%2frun%2fslapd-{realm_inst}.socket";' ++ f'|#uri "ldapi://%2fvar%2frun%2fslapd-{realm_inst}.socket";|g' ++ ) ++ self.master.run_command([ ++ 'sed', '-i', sed_pattern, '/etc/named.conf' ++ ]) ++ ++ # Add invalid LDAP URI before the commented line ++ insert_pattern = ( ++ f'/ldapi:\\/\\/%2fvar%2frun%2fslapd-{realm_inst}.socket/i ' ++ 'uri "ldapi://127.0.0.1";' ++ ) ++ self.master.run_command([ ++ 'sed', '-i', insert_pattern, '/etc/named.conf' ++ ]) ++ ++ # Restart named with invalid LDAP config ++ tasks.restart_named(self.master) ++ ++ # Try to stop named with rndc - should work without hanging ++ self.master.run_command(['rndc', 'stop'], raiseonerr=False) ++ ++ # Wait a moment for service to stop ++ time.sleep(5) ++ ++ # Verify named is not running (exit code 3 = inactive) ++ result = self.master.run_command( ++ ['systemctl', 'is-active', 'named'], raiseonerr=False ++ ) ++ assert result.returncode != 0, "named should be stopped" ++ ++ finally: ++ # Restore original named.conf ++ # Remove the invalid URI line ++ self.master.run_command([ ++ 'sed', '-i', '/ldapi:\\/\\/127.0.0.1/d', '/etc/named.conf' ++ ], raiseonerr=False) ++ ++ # Uncomment the original valid URI ++ uncomment_pattern = ( ++ f's|#uri "ldapi://%2fvar%2frun%2fslapd-{realm_inst}.socket";' ++ f'|uri "ldapi://%2fvar%2frun%2fslapd-{realm_inst}.socket";|g' ++ ) ++ self.master.run_command([ ++ 'sed', '-i', uncomment_pattern, '/etc/named.conf' ++ ], raiseonerr=False) ++ ++ # Restart named to restore normal operation ++ tasks.restart_named(self.master) ++ ++ def test_bind_ldap_reconnect(self): ++ """Test reconnect to LDAP when the first connection fails. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=767489 ++ ++ This test verifies that named service can handle LDAP connection ++ failures gracefully. It modifies named.conf to use an invalid LDAP ++ URI while blocking LDAP ports with iptables, then restarts named ++ to verify it handles the reconnection properly when connectivity ++ is restored. ++ """ ++ tasks.kinit_admin(self.master) ++ ++ # Get realm name with dashes instead of dots for socket path ++ realm_inst = self.master.domain.realm.replace('.', '-') ++ ++ # Firewall rich rules to block LDAP ports ++ ldap_reject = 'rule family=ipv4 port port=389 protocol=tcp reject' ++ ldaps_reject = 'rule family=ipv4 port port=636 protocol=tcp reject' ++ ++ try: ++ # Check named is running ++ assert tasks.host_service_active(self.master, 'named') ++ ++ # Backup named.conf ++ self.master.run_command(['cp', '/etc/named.conf', '/root/']) ++ ++ # Comment out the valid LDAP URI and add invalid one ++ sed_pattern = ( ++ f's|uri "ldapi://%2fvar%2frun%2fslapd-{realm_inst}.socket";' ++ f'|#uri "ldapi://%2fvar%2frun%2fslapd-{realm_inst}.socket";|g' ++ ) ++ self.master.run_command([ ++ 'sed', '-i', sed_pattern, '/etc/named.conf' ++ ]) ++ ++ # Add invalid LDAP URI before the commented line ++ insert_pattern = ( ++ f'/ldapi:\\/\\/%2fvar%2frun%2fslapd-{realm_inst}.socket/i ' ++ 'uri "ldapi://127.0.0.1";' ++ ) ++ self.master.run_command([ ++ 'sed', '-i', insert_pattern, '/etc/named.conf' ++ ]) ++ ++ # Block LDAP ports with firewall-cmd to ensure connection fails ++ self.master.run_command([ ++ 'firewall-cmd', f'--add-rich-rule={ldap_reject}' ++ ]) ++ self.master.run_command([ ++ 'firewall-cmd', f'--add-rich-rule={ldaps_reject}' ++ ]) ++ ++ # Restart named - it should handle the LDAP connection failure ++ tasks.restart_named(self.master) ++ ++ # Verify named is still running (may be in degraded state) ++ result = self.master.run_command( ++ ['systemctl', 'is-active', 'named'], raiseonerr=False ++ ) ++ # Named may be active or activating depending on timing ++ assert result.returncode == 0 or 'activating' in result.stdout_text ++ ++ finally: ++ # Remove firewall rules to restore LDAP connectivity ++ self.master.run_command([ ++ 'firewall-cmd', f'--remove-rich-rule={ldap_reject}' ++ ], raiseonerr=False) ++ self.master.run_command([ ++ 'firewall-cmd', f'--remove-rich-rule={ldaps_reject}' ++ ], raiseonerr=False) ++ ++ # Restore original named.conf ++ # Remove the invalid URI line ++ self.master.run_command([ ++ 'sed', '-i', '/ldapi:\\/\\/127.0.0.1/d', '/etc/named.conf' ++ ], raiseonerr=False) ++ ++ # Uncomment the original valid URI ++ uncomment_pattern = ( ++ f's|#uri "ldapi://%2fvar%2frun%2fslapd-{realm_inst}.socket";' ++ f'|uri "ldapi://%2fvar%2frun%2fslapd-{realm_inst}.socket";|g' ++ ) ++ self.master.run_command([ ++ 'sed', '-i', uncomment_pattern, '/etc/named.conf' ++ ], raiseonerr=False) ++ ++ # Restart named to restore normal operation ++ tasks.restart_named(self.master) ++ ++ def test_dnsrecord_mod_nonexistent_error(self): ++ """Test correct error message when modifying nonexistent DNS record. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=856281 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ ++ # Try to modify a non-existent record ++ result = tasks.mod_dns_record( ++ self.master, domain, 'this.does.not.exist', ++ '--txt-rec=foo', raiseonerr=False ++ ) ++ ++ assert result.returncode != 0 ++ expected_msg = "this.does.not.exist: DNS resource record not found" ++ assert expected_msg in result.stderr_text ++ ++ def test_zone_without_update_policy(self): ++ """Test zone without idnsUpdatePolicy works during zone refresh. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=908780 ++ ++ As per bind-dyndb-ldap/NEWS, psearch, serial_autoincrement and ++ zone_refresh were deprecated and removed. This test verifies ++ that zones without idnsUpdatePolicy still work correctly after ++ service restart. ++ """ ++ tasks.kinit_admin(self.master) ++ zone = "bz908780zone" ++ ++ try: ++ # Add test zone ++ tasks.add_dns_zone( ++ self.master, zone, ++ admin_email=self.EMAIL ++ ) ++ ++ # Modify zone to remove idnsUpdatePolicy attribute ++ tasks.mod_dns_zone(self.master, zone, '--update-policy=') ++ ++ # Restart named service ++ tasks.restart_named(self.master) ++ ++ # Check logs for error message (use journalctl for compatibility) ++ result = self.master.run_command([ ++ 'journalctl', '-u', 'named', '-n', '100', '--no-pager' ++ ]) ++ ++ # Verify no error about zone failing to transfer ++ err_msg = "unchanged. zone may fail to transfer to slaves" ++ assert err_msg not in result.stdout_text ++ ++ finally: ++ tasks.del_dns_zone(self.master, zone, raiseonerr=False) ++ ++ def test_txt_record_with_comma(self): ++ """Test dnsrecord works if TXT record data contains comma. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=910460 ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ txt_value = ( ++ 'Holmes laughed. "It is quite a pretty little problem," ' ++ 'said he.' ++ ) ++ txt_mod_value = ( ++ 'Holmes laughed. "It is quite a pretty little problem," ' ++ 'said me.' ++ ) ++ ++ try: ++ # Add TXT record with comma - should not cause traceback ++ result = tasks.add_dns_record( ++ self.master, domain, '@', 'txt', [txt_value] ++ ) ++ assert result.returncode == 0 ++ ++ # Modify TXT record with comma - should not cause traceback ++ result = tasks.mod_dns_record( ++ self.master, domain, '@', ++ f'--txt-rec={txt_value}', f'--txt-data={txt_mod_value}' ++ ) ++ assert result.returncode == 0 ++ ++ finally: ++ # Delete TXT record ++ tasks.del_dns_record( ++ self.master, domain, '@', ++ record_type='txt', record_value=[txt_mod_value], ++ raiseonerr=False ++ ) ++ ++ def test_dname_single_value(self): ++ """Test DNAME record attribute allows single value only. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=915797 ++ ++ Verify that adding a second DNAME record to the same name fails ++ with appropriate error message. ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ dname1 = f"foo.{domain}" ++ dname2 = f"bar.{domain}" ++ ++ try: ++ # Add first DNAME record ++ tasks.add_dns_record( ++ self.master, domain, 'dnamebz915797', 'dname', [dname1] ++ ) ++ ++ # Try to add second DNAME record - should fail ++ result = tasks.add_dns_record( ++ self.master, domain, 'dnamebz915797', 'dname', [dname2], ++ raiseonerr=False ++ ) ++ assert result.returncode != 0 ++ expected_msg = "only one DNAME record is allowed per name" ++ assert expected_msg in result.stderr_text ++ ++ finally: ++ tasks.del_dns_record( ++ self.master, domain, 'dnamebz915797', ++ del_all=True, raiseonerr=False ++ ) ++ ++ def test_cname_single_value(self): ++ """Test CNAME record allows single value only. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=915807 ++ ++ Verify that adding a second CNAME record to the same name fails ++ with appropriate error message. ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ cname1 = f"foo.{domain}" ++ cname2 = f"bar.{domain}" ++ ++ try: ++ # Add first CNAME record ++ tasks.add_dns_record( ++ self.master, domain, 'cnamebz915807', 'cname', [cname1] ++ ) ++ ++ # Try to add second CNAME record - should fail ++ result = tasks.add_dns_record( ++ self.master, domain, 'cnamebz915807', 'cname', [cname2], ++ raiseonerr=False ++ ) ++ assert result.returncode != 0 ++ expected_msg = "only one CNAME record is allowed per name" ++ assert expected_msg in result.stderr_text ++ ++ finally: ++ tasks.del_dns_record( ++ self.master, domain, 'cnamebz915807', ++ del_all=True, raiseonerr=False ++ ) ++ ++ def test_idnszone_schema_no_cn(self): ++ """Test cn attribute is not present in idnsZone objectClasses. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=947911 ++ ++ Verify that 'cn' attribute is removed from idnsZone objectClasses ++ in LDAP schema. Note: 'cn' was returned back to idnsRecord to fix ++ bug 1167964, so we only check idnsZone. ++ """ ++ tasks.kinit_admin(self.master) ++ ++ # Check that cn attribute is not in idnsZone objectClass ++ # Use ldif-wrap=no to keep each objectClass on a single line ++ result = self.master.run_command([ ++ 'ldapsearch', '-x', '-D', ++ 'cn=Directory Manager', ++ '-w', self.master.config.dirman_password, ++ '-b', 'cn=schema', 'objectClasses', ++ '-o', 'ldif-wrap=no' ++ ]) ++ ++ # Verify idnsZone objectClass exists and doesn't contain cn attribute ++ lines = result.stdout_text.split('\n') ++ is_valid = any( ++ "NAME 'idnsZone'" in line and ' cn ' not in line ++ for line in lines ++ ) ++ assert is_valid, "cn attribute should not be in idnsZone objectClass" ++ ++ def test_ptr_sync_preserves_txt_record(self): ++ """Test PTR sync deletes only PTR record, preserves TXT record. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=958140 ++ ++ When PTR record synchronization is enabled and an A record is ++ deleted via dynamic update, only the corresponding PTR record ++ should be deleted. Other records (like TXT) on the same name ++ in the reverse zone should be preserved. ++ """ ++ tasks.kinit_admin(self.master) ++ test_zone = "examplebz958140.com" ++ reverse_zone = "3.2.1.in-addr.arpa." ++ realm = self.master.domain.realm ++ hostname = f"testbz958140.{test_zone}" ++ ++ try: ++ # Add test zones ++ tasks.add_dns_zone( ++ self.master, test_zone, ++ skip_overlap_check=True, ++ admin_email=f"hostmaster.{test_zone}" ++ ) ++ tasks.add_dns_zone( ++ self.master, reverse_zone, ++ skip_overlap_check=True, ++ admin_email=f"hostmaster.{reverse_zone}" ++ ) ++ ++ # Reload named so it can serve the new zones ++ self.master.run_command(['rndc', 'reload']) ++ ++ # Add A record with reverse PTR ++ tasks.add_dns_record( ++ self.master, test_zone, 'testbz958140', 'a', ['1.2.3.4'], ++ '--a-create-reverse' ++ ) ++ ++ # Add TXT record to reverse zone on same name as PTR ++ tasks.add_dns_record( ++ self.master, reverse_zone, '4', 'txt', ['text'] ++ ) ++ ++ # Enable PTR record synchronization ++ tasks.mod_dns_zone( ++ self.master, test_zone, '--dynamic-update=True' ++ ) ++ tasks.mod_dns_zone( ++ self.master, test_zone, '--allow-sync-ptr=True' ++ ) ++ tasks.mod_dns_zone( ++ self.master, reverse_zone, '--dynamic-update=True' ++ ) ++ ++ # Add host and get keytab for dynamic updates ++ self.master.run_command([ ++ 'ipa', 'host-add', hostname, '--force' ++ ]) ++ self.master.run_command([ ++ 'ipa-getkeytab', '-s', self.master.hostname, ++ '-p', f'host/{hostname}@{realm}', ++ '-k', '/tmp/bz958140.keytab' ++ ]) ++ ++ self.master.run_command(['rndc', 'reload']) ++ self.master.run_command(['ipactl', 'restart']) ++ ++ # Use nsupdate to delete A record (should trigger PTR sync) ++ nsupdate_content = NSUPDATE_DELETE_A_TEMPLATE.format( ++ hostname=hostname, ip='1.2.3.4' ++ ) ++ self.master.put_file_contents('/tmp/nsupdate958140.txt', ++ nsupdate_content) ++ self.master.run_command([ ++ 'kinit', '-k', '-t', '/tmp/bz958140.keytab', ++ f'host/{hostname}' ++ ]) ++ self.master.run_command([ ++ 'nsupdate', '-g', '-v', '/tmp/nsupdate958140.txt' ++ ], raiseonerr=False) ++ ++ tasks.kinit_admin(self.master) ++ ++ # Verify A record is deleted ++ result = tasks.find_dns_record( ++ self.master, test_zone, 'testbz958140', raiseonerr=False ++ ) ++ assert 'A record' not in result.stdout_text ++ ++ # Verify PTR record is deleted ++ result = self.master.run_command([ ++ 'ipa', 'dnsrecord-find', reverse_zone, '4' ++ ], raiseonerr=False) ++ assert 'PTR record' not in result.stdout_text ++ ++ # Verify TXT record is preserved ++ assert 'TXT record' in result.stdout_text ++ ++ finally: ++ tasks.kinit_admin(self.master) ++ self.master.run_command([ ++ 'ipa', 'host-del', hostname ++ ], raiseonerr=False) ++ tasks.del_dns_zone(self.master, test_zone, raiseonerr=False) ++ tasks.del_dns_zone(self.master, reverse_zone, raiseonerr=False) ++ self.master.run_command(['rm', '-f', '/tmp/bz958140.keytab', ++ '/tmp/nsupdate958140.txt'], ++ raiseonerr=False) ++ ++ def test_invalid_policy_disables_operations(self): ++ """Test invalid policy disables updates, transfers, and queries. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=958141 ++ ++ When zone policy (update, transfer, or query) contains invalid ++ values, the corresponding operation should be disabled/refused. ++ """ ++ tasks.kinit_admin(self.master) ++ test_zone = 'example.test' ++ realm = self.master.domain.realm ++ hostname = f'test.{test_zone}' ++ ++ try: ++ # Add test zone and get original update policy ++ tasks.add_dns_zone( ++ self.master, test_zone, ++ skip_overlap_check=True, ++ admin_email=f'hostmaster.{test_zone}' ++ ) ++ ++ # Get original update policy ++ result = self.master.run_command([ ++ 'ipa', 'dnszone-show', test_zone, '--all' ++ ]) ++ original_policy = None ++ for line in result.stdout_text.split('\n'): ++ if 'BIND update policy:' in line: ++ original_policy = line.split(':', 1)[1].strip() ++ break ++ ++ tasks.add_dns_record( ++ self.master, test_zone, 'test', 'a', ['1.2.3.4'] ++ ) ++ ++ # Test 1: Verify dynamic update works with valid policy ++ tasks.mod_dns_zone( ++ self.master, test_zone, '--dynamic-update=True' ++ ) ++ ++ self.master.run_command([ ++ 'ipa', 'host-add', hostname ++ ]) ++ self.master.run_command([ ++ 'ipa-getkeytab', '-s', self.master.hostname, ++ '-p', f'host/{hostname}@{realm}', ++ '-k', '/tmp/bz958141.keytab' ++ ]) ++ ++ self.master.run_command(['rndc', 'reload']) ++ ++ # Create nsupdate file to delete A record ++ nsupdate_content = NSUPDATE_DELETE_A_TEMPLATE.format( ++ hostname=hostname, ip='1.2.3.4' ++ ) ++ self.master.put_file_contents('/tmp/nsupdate958141.txt', ++ nsupdate_content) ++ ++ self.master.run_command([ ++ 'kinit', '-k', '-t', '/tmp/bz958141.keytab', ++ f'host/{hostname}' ++ ]) ++ ++ # Dynamic update should succeed with valid policy ++ result = self.master.run_command([ ++ 'nsupdate', '-g', '-v', '/tmp/nsupdate958141.txt' ++ ], raiseonerr=False) ++ assert result.returncode == 0, \ ++ 'Dynamic update should succeed with valid policy' ++ ++ # Set invalid update policy ++ tasks.kinit_admin(self.master) ++ tasks.mod_dns_zone( ++ self.master, test_zone, '--update-policy=invalid value' ++ ) ++ ++ # Re-add A record for next test ++ tasks.add_dns_record( ++ self.master, test_zone, 'test', 'a', ['1.2.3.4'] ++ ) ++ ++ # Dynamic update should fail with invalid policy ++ self.master.run_command([ ++ 'kinit', '-k', '-t', '/tmp/bz958141.keytab', ++ f'host/{hostname}' ++ ]) ++ result = self.master.run_command([ ++ 'nsupdate', '-g', '/tmp/nsupdate958141.txt' ++ ], raiseonerr=False) ++ assert result.returncode == 2, \ ++ 'Dynamic update should fail with invalid policy' ++ ++ # Restore original update policy ++ tasks.kinit_admin(self.master) ++ tasks.mod_dns_zone( ++ self.master, test_zone, ++ f'--update-policy={original_policy}' ++ ) ++ ++ # Test 2: Invalid transfer policy disables zone transfers ++ tasks.mod_dns_zone( ++ self.master, test_zone, '--allow-transfer=any' ++ ) ++ ++ # Verify zone transfer works with valid policy ++ result = self.master.run_command([ ++ 'dig', '@127.0.0.1', '-t', 'AXFR', test_zone ++ ], raiseonerr=False) ++ assert 'Transfer failed' not in result.stdout_text ++ ++ # Set invalid transfer policy via LDAP ++ ldap = self.master.ldap_connect() ++ zone_dn = DN( ++ ('idnsname', f'{test_zone}.'), ++ ('cn', 'dns'), ++ self.master.domain.basedn ++ ) ++ entry = ldap.get_entry(zone_dn) ++ entry['idnsAllowTransfer'] = ['192.0.2..0/24'] # Invalid ++ ldap.update_entry(entry) ++ ++ # Verify zone transfer fails with invalid policy ++ result = self.master.run_command([ ++ 'dig', '@127.0.0.1', '-t', 'AXFR', test_zone ++ ], raiseonerr=False) ++ assert 'Transfer failed' in result.stdout_text ++ ++ # Restore valid transfer policy ++ tasks.mod_dns_zone( ++ self.master, test_zone, '--allow-transfer=none' ++ ) ++ ++ # Test 3: Invalid query policy disables queries ++ # First verify query works with valid policy ++ result = self.master.run_command([ ++ 'dig', f'test.{test_zone}' ++ ], raiseonerr=False) ++ assert 'NOERROR' in result.stdout_text ++ ++ # Set invalid query policy via LDAP ++ entry = ldap.get_entry(zone_dn) ++ entry['idnsAllowQuery'] = ['192.0.2..0/24'] # Invalid ++ ldap.update_entry(entry) ++ ++ # Verify query is refused ++ result = self.master.run_command([ ++ 'dig', f'test.{test_zone}' ++ ], raiseonerr=False) ++ assert 'REFUSED' in result.stdout_text ++ ++ # Restore valid query policy ++ tasks.mod_dns_zone( ++ self.master, test_zone, '--allow-query=any' ++ ) ++ ++ finally: ++ tasks.kinit_admin(self.master) ++ self.master.run_command([ ++ 'ipa', 'host-del', hostname ++ ], raiseonerr=False) ++ tasks.del_dns_zone(self.master, test_zone, raiseonerr=False) ++ self.master.run_command([ ++ 'rm', '-f', '/tmp/bz958141.keytab', '/tmp/nsupdate958141.txt' ++ ], raiseonerr=False) ++ ++ def test_ptr_sync_ipv6(self): ++ """Test PTR record synchronization works with IPv6 addresses. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=962814 ++ ++ Verify that when AAAA record is added via dynamic update with ++ PTR sync enabled, the corresponding PTR record is created in ++ the IPv6 reverse zone. ++ """ ++ tasks.kinit_admin(self.master) ++ test_zone = "examplebz962814.com" ++ # Reverse zone for 1:2:3:4:5:6::/96 ++ reverse_zone = ( ++ "6.0.0.0.5.0.0.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.ip6.arpa." ++ ) ++ realm = self.master.domain.realm ++ hostname = f"test.{test_zone}" ++ ++ try: ++ # Add test zones ++ tasks.add_dns_zone( ++ self.master, test_zone, ++ skip_overlap_check=True, ++ admin_email=f"hostmaster.{test_zone}" ++ ) ++ tasks.add_dns_zone( ++ self.master, reverse_zone, ++ skip_overlap_check=True, ++ admin_email=f"hostmaster.{reverse_zone}" ++ ) ++ ++ # Enable PTR record synchronization ++ tasks.mod_dns_zone( ++ self.master, test_zone, '--dynamic-update=True' ++ ) ++ tasks.mod_dns_zone( ++ self.master, test_zone, '--allow-sync-ptr=True' ++ ) ++ tasks.mod_dns_zone( ++ self.master, reverse_zone, '--dynamic-update=True' ++ ) ++ ++ # Add host and get keytab ++ self.master.run_command([ ++ 'ipa', 'host-add', hostname, '--force' ++ ]) ++ self.master.run_command([ ++ 'ipa-getkeytab', '-s', self.master.hostname, ++ '-p', f'host/{hostname}@{realm}', ++ '-k', '/tmp/bz962814.keytab' ++ ]) ++ ++ self.master.run_command(['rndc', 'reload']) ++ ++ # Use nsupdate to add AAAA record ++ nsupdate_content = NSUPDATE_ADD_AAAA_TEMPLATE.format( ++ hostname=hostname, ttl=3600, ip='1:2:3:4:5:6:7:8' ++ ) ++ self.master.put_file_contents('/tmp/nsupdate962814.txt', ++ nsupdate_content) ++ self.master.run_command([ ++ 'kinit', '-k', '-t', '/tmp/bz962814.keytab', ++ f'host/{hostname}' ++ ]) ++ ++ time.sleep(60) ++ self.master.run_command([ ++ 'nsupdate', '-g', '/tmp/nsupdate962814.txt' ++ ]) ++ ++ tasks.kinit_admin(self.master) ++ ++ # Verify AAAA record was added ++ result = tasks.find_dns_record( ++ self.master, test_zone, 'test', raiseonerr=False ++ ) ++ assert 'AAAA record' in result.stdout_text ++ ++ # Verify PTR record was created in reverse zone ++ # PTR for 1:2:3:4:5:6:7:8 should be at 8.0.0.0.7.0.0.0 ++ result = self.master.run_command([ ++ 'ipa', 'dnsrecord-find', reverse_zone, '8.0.0.0.7.0.0.0' ++ ], raiseonerr=False) ++ assert 'PTR record' in result.stdout_text ++ ++ finally: ++ tasks.kinit_admin(self.master) ++ self.master.run_command([ ++ 'ipa', 'host-del', hostname ++ ], raiseonerr=False) ++ tasks.del_dns_zone(self.master, test_zone, raiseonerr=False) ++ tasks.del_dns_zone(self.master, reverse_zone, raiseonerr=False) ++ self.master.run_command(['rm', '-f', '/tmp/bz962814.keytab', ++ '/tmp/nsupdate962814.txt'], ++ raiseonerr=False) ++ ++ def test_ipv6_private_reverse_zone(self): ++ """Test serving reverse zones for IPv6 private ranges. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=962815 ++ ++ Verify that reverse zones for IPv6 private/documentation ranges ++ (like 2001:db8::/32) can be served without manual changes to ++ named.conf. These are in the automatic empty zones list. ++ """ ++ tasks.kinit_admin(self.master) ++ # 2001:db8::/32 documentation prefix reverse zone ++ reverse_zone = "8.b.d.0.1.0.0.2.ip6.arpa" ++ test_record = "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0" ++ test_ptr = "test.example.com" ++ test_ipv6 = "2001:0db8::1" ++ ++ try: ++ # Add reverse zone for IPv6 documentation prefix ++ tasks.add_dns_zone( ++ self.master, reverse_zone, ++ admin_email=f"hostmaster.{reverse_zone}" ++ ) ++ ++ # Add PTR record ++ tasks.add_dns_record( ++ self.master, f"{reverse_zone}.", test_record, ++ 'ptr', [test_ptr] ++ ) ++ ++ self.master.run_command(['ipactl', 'restart']) ++ ++ # Verify reverse lookup works ++ result = self.master.run_command([ ++ 'dig', '-x', test_ipv6 ++ ]) ++ assert test_record in result.stdout_text ++ assert test_ptr in result.stdout_text ++ ++ # Restart named and verify zone loads ++ tasks.restart_named(self.master) ++ time.sleep(5) ++ ++ # Check logs that zone was loaded without errors ++ result = self.master.run_command([ ++ 'journalctl', '-u', 'named', '-n', '100', '--no-pager' ++ ]) ++ # Verify zone is mentioned in logs (loaded) ++ assert reverse_zone in result.stdout_text, \ ++ f'Zone {reverse_zone} not found in named logs' ++ ++ finally: ++ tasks.del_dns_zone(self.master, reverse_zone, raiseonerr=False) ++ ++ def test_tld_with_numbers(self): ++ """Test DNS record allows top-level domains with numbers. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=907913 ++ ++ Verify that DNS zones and records can be created with TLDs ++ that contain numbers, and NS records can reference them. ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ test_zone = "TBADTEST0" # TLD with number ++ test_record = "TBADTEST" ++ ++ try: ++ # Add zone with numeric TLD ++ tasks.add_dns_zone( ++ self.master, test_zone, ++ admin_email=f"hostmaster.{test_zone}" ++ ) ++ ++ # Add A record in the zone ++ tasks.add_dns_record( ++ self.master, test_zone, test_record, 'a', ['1.2.3.4'] ++ ) ++ ++ self.master.run_command(['ipactl', 'restart']) ++ ++ # Verify NS hostname with numeric TLD is valid ++ result = tasks.add_dns_record( ++ self.master, domain, test_record, 'ns', ++ [f'{test_record}.{test_zone}.'] ++ ) ++ assert result.returncode == 0 ++ ++ # Cleanup NS record ++ tasks.del_dns_record( ++ self.master, domain, test_record, ++ record_type='ns', ++ record_value=[f'{test_record}.{test_zone}.'] ++ ) ++ ++ finally: ++ tasks.del_dns_zone(self.master, test_zone, raiseonerr=False) ++ ++ def test_rfc2317_classless_arpa(self): ++ """Test dnszone-add supports RFC 2317 classless in-addr.arpa. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=1058688 ++ ++ Verify that DNS zones with RFC 2317 classless reverse delegation ++ format (like 0/27.10.10.in-addr.arpa) can be created. ++ """ ++ tasks.kinit_admin(self.master) ++ # RFC 2317 classless delegation format ++ test_zone = "0/27.10.10.in-addr.arpa." ++ ++ try: ++ # Add zone with RFC 2317 format ++ tasks.add_dns_zone( ++ self.master, test_zone, ++ skip_overlap_check=True, ++ admin_email="hostmaster.0.0.10.10.in-addr.arpa." ++ ) ++ ++ # Verify zone was created ++ result = tasks.find_dns_zone(self.master, test_zone) ++ assert result.returncode == 0 ++ assert test_zone in result.stdout_text ++ ++ finally: ++ tasks.del_dns_zone(self.master, test_zone, raiseonerr=False) ++ ++ def test_dnsrecord_mod_no_warning(self): ++ """Test dnsrecord-mod doesn't display API version warning. ++ ++ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=1054869 ++ ++ Verify that modifying DNS records doesn't show warning message ++ about API version. Also tests that setting txt-rec to empty ++ deletes the record. ++ """ ++ tasks.kinit_admin(self.master) ++ domain = self.master.domain.name ++ record_name = "bz1054869" ++ ++ try: ++ # Add TXT record ++ tasks.add_dns_record( ++ self.master, domain, record_name, 'txt', ['1054869'] ++ ) ++ ++ # Verify record was added ++ result = tasks.show_dns_record( ++ self.master, domain, record_name, '--all', '--raw' ++ ) ++ assert 'txtrecord: 1054869' in result.stdout_text.lower() ++ ++ # Modify record with empty txt-rec (deletes the record) ++ result = tasks.mod_dns_record( ++ self.master, domain, record_name, ++ '--txt-rec=' ++ ) ++ # Verify no API version warning ++ assert 'WARNING: API Version' not in result.stdout_text ++ ++ # Verify record was deleted ++ result = tasks.find_dns_record( ++ self.master, domain, record_name, raiseonerr=False ++ ) ++ assert result.returncode != 0 ++ ++ finally: ++ tasks.del_dns_record( ++ self.master, domain, record_name, ++ del_all=True, raiseonerr=False ++ ) ++ ++ def test_dnszone_disable_enable_and_system_records(self): ++ """Test dnszone-disable, dnszone-enable and dns-update-system-records. ++ ++ This test verifies that: ++ 1. A DNS zone can be disabled and enabled ++ 2. dns-update-system-records works correctly (dry-run and actual) ++ """ ++ tasks.kinit_admin(self.master) ++ test_zone = 'disabletest.test' ++ ++ try: ++ # Add test zone ++ tasks.add_dns_zone( ++ self.master, test_zone, ++ admin_email=f'hostmaster.{test_zone}' ++ ) ++ ++ result = self.master.run_command([ ++ 'ipa', 'dnszone-show', test_zone ++ ]) ++ assert 'Active zone: True' in result.stdout_text ++ ++ # Disable the zone ++ result = self.master.run_command([ ++ 'ipa', 'dnszone-disable', test_zone ++ ]) ++ assert f'Disabled DNS zone "{test_zone}."' in result.stdout_text ++ ++ result = self.master.run_command([ ++ 'ipa', 'dnszone-show', test_zone ++ ]) ++ assert 'Active zone: False' in result.stdout_text ++ ++ # Enable the zone ++ result = self.master.run_command([ ++ 'ipa', 'dnszone-enable', test_zone ++ ]) ++ assert f'Enabled DNS zone "{test_zone}."' in result.stdout_text ++ ++ result = self.master.run_command([ ++ 'ipa', 'dnszone-show', test_zone ++ ]) ++ assert 'Active zone: True' in result.stdout_text ++ ++ # Test dns-update-system-records --dry-run ++ result = self.master.run_command([ ++ 'ipa', 'dns-update-system-records', '--dry-run' ++ ]) ++ assert result.returncode == 0 ++ ++ result = self.master.run_command([ ++ 'ipa', 'dns-update-system-records' ++ ]) ++ assert result.returncode == 0 ++ ++ finally: ++ tasks.del_dns_zone(self.master, test_zone, raiseonerr=False) +-- +2.52.0 + diff --git a/SOURCES/0142-Allow-32bit-gid.patch b/SOURCES/0142-Allow-32bit-gid.patch new file mode 100644 index 0000000..6d83595 --- /dev/null +++ b/SOURCES/0142-Allow-32bit-gid.patch @@ -0,0 +1,38 @@ +From a5ff0a2261e2df7b2157e6509cdbecf65033e578 Mon Sep 17 00:00:00 2001 +From: David Hanina +Date: Tue, 10 Mar 2026 10:26:33 +0100 +Subject: [PATCH] Allow 32bit gid + +We should allow 32bit groups, by setting maxvalue we allow that. + +Fixes: https://pagure.io/freeipa/issue/9953 +Signed-off-by: David Hanina +Reviewed-By: David Hanina +Reviewed-By: Sudhir Menon +--- + ipaserver/plugins/group.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/ipaserver/plugins/group.py b/ipaserver/plugins/group.py +index f05a39f69ec3eb3257909bd61b42a9a21212c14d..308e5458c1d00be34753f42675367a7307331514 100644 +--- a/ipaserver/plugins/group.py ++++ b/ipaserver/plugins/group.py +@@ -26,6 +26,7 @@ import re + from ipalib import api + from ipalib import Int, Str, Flag + from ipalib.constants import PATTERN_GROUPUSER_NAME, ERRMSG_GROUPUSER_NAME ++from ipalib.parameters import MAX_UINT32 + from ipalib.plugable import Registry + from .baseldap import ( + add_external_post_callback, +@@ -354,6 +355,7 @@ class group(LDAPObject): + label=_('GID'), + doc=_('GID (use this option to set it manually)'), + minvalue=1, ++ maxvalue=MAX_UINT32, + ), + ipaexternalmember_param, + ) +-- +2.52.0 + diff --git a/SOURCES/0143-ipatests-Fix-test_allow_query_transfer_ipv6-when-IPv.patch b/SOURCES/0143-ipatests-Fix-test_allow_query_transfer_ipv6-when-IPv.patch new file mode 100644 index 0000000..790d26d --- /dev/null +++ b/SOURCES/0143-ipatests-Fix-test_allow_query_transfer_ipv6-when-IPv.patch @@ -0,0 +1,138 @@ +From 7de7c0e2ed1afb2887bc7adeb7363109cbc5f3f9 Mon Sep 17 00:00:00 2001 +From: PRANAV THUBE +Date: Wed, 11 Mar 2026 20:13:31 +0530 +Subject: [PATCH] ipatests: Fix test_allow_query_transfer_ipv6 when IPv6 is + disabled + +The test was failing in environments where IPv6 is disabled at the +kernel level because it attempted to add a temporary IPv6 address +without first checking if IPv6 is enabled on the interface. + +This fix restructures the test to: +- Check if IPv6 is disabled via sysctl before attempting IPv6 setup +- Always run IPv4 allow-query and allow-transfer tests +- Only run IPv6-related tests when IPv6 is available + +This ensures the test passes in IPv4-only environments while still +providing full coverage when IPv6 is enabled. + +Fixes: https://pagure.io/freeipa/issue/9944 +Signed-off-by: Pranav Thube pthube@redhat.com +Reviewed-By: David Hanina +Reviewed-By: Florence Blanc-Renaud +--- + ipatests/test_integration/test_dns.py | 62 ++++++--------------------- + 1 file changed, 14 insertions(+), 48 deletions(-) + +diff --git a/ipatests/test_integration/test_dns.py b/ipatests/test_integration/test_dns.py +index 4b9ab1fe8d7b8884760ed637cb2fcc5d5a060df0..947cff5c02ea2662cc2de88860cfa294e396792d 100644 +--- a/ipatests/test_integration/test_dns.py ++++ b/ipatests/test_integration/test_dns.py +@@ -5,6 +5,7 @@ + + from __future__ import absolute_import + ++import pytest + import time + import dns.exception + import dns.resolver +@@ -1714,13 +1715,12 @@ class TestDNSMisc(IntegrationTest): + tasks.del_dns_zone(self.master, zone, raiseonerr=False) + + def test_allow_query_transfer_ipv6(self): +- """Test allow-query and allow-transfer with IPv4 and IPv6. ++ """Test allow-query and allow-transfer with IPv6. + + Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=701677 + """ + tasks.kinit_admin(self.master) +- zone = "example.com" +- ipv4 = self.master.ip ++ zone = "example6.com" + ipv6_added = False + temp_ipv6 = '2001:0db8:0:f101::1/64' + +@@ -1732,6 +1732,13 @@ class TestDNSMisc(IntegrationTest): + ]) + eth = result.stdout_text.strip() + ++ # Check if IPv6 is disabled on the interface ++ result = self.master.run_command([ ++ 'sysctl', '-n', f'net.ipv6.conf.{eth}.disable_ipv6' ++ ]) ++ if result.stdout_text.strip() == '1': ++ pytest.skip(f"IPv6 is disabled on interface {eth}") ++ + # Add temporary IPv6 if none exists + result = self.master.run_command( + ['ip', 'addr', 'show', 'scope', 'global'], raiseonerr=False +@@ -1754,62 +1761,21 @@ class TestDNSMisc(IntegrationTest): + tasks.add_dns_zone(self.master, zone, skip_overlap_check=True, + admin_email=self.EMAIL) + +- # Test allow-query: IPv4 allowed, IPv6 denied +- tasks.mod_dns_zone( +- self.master, zone, +- f"--allow-query={ipv4};!{ipv6};" +- ) +- result = self.master.run_command( +- ['dig', f'@{ipv4}', '-t', 'soa', zone], raiseonerr=False +- ) +- assert 'ANSWER SECTION' in result.stdout_text +- result = self.master.run_command( +- ['dig', f'@{ipv6}', '-t', 'soa', zone], raiseonerr=False +- ) +- assert 'ANSWER SECTION' not in result.stdout_text +- +- # Test allow-query: IPv6 allowed, IPv4 denied ++ # Test allow-query: IPv6 allowed + tasks.mod_dns_zone( + self.master, zone, +- f"--allow-query={ipv6};!{ipv4};" +- ) +- result = self.master.run_command( +- ['dig', f'@{ipv4}', '-t', 'soa', zone], raiseonerr=False ++ f"--allow-query={ipv6};" + ) +- assert 'ANSWER SECTION' not in result.stdout_text + result = self.master.run_command( + ['dig', f'@{ipv6}', '-t', 'soa', zone], raiseonerr=False + ) + assert 'ANSWER SECTION' in result.stdout_text + +- # Reset allow-query to any +- tasks.mod_dns_zone( +- self.master, zone, "--allow-query=any;" +- ) +- +- # Test allow-transfer: IPv4 allowed, IPv6 denied +- tasks.mod_dns_zone( +- self.master, zone, +- f"--allow-transfer={ipv4};!{ipv6};" +- ) +- result = self.master.run_command( +- ['dig', f'@{ipv4}', zone, 'axfr'], raiseonerr=False +- ) +- assert 'Transfer failed' not in result.stdout_text +- result = self.master.run_command( +- ['dig', f'@{ipv6}', zone, 'axfr'], raiseonerr=False +- ) +- assert 'Transfer failed' in result.stdout_text +- +- # Test allow-transfer: IPv6 allowed, IPv4 denied ++ # Test allow-transfer: IPv6 allowed + tasks.mod_dns_zone( + self.master, zone, +- f"--allow-transfer={ipv6};!{ipv4};" +- ) +- result = self.master.run_command( +- ['dig', f'@{ipv4}', zone, 'axfr'], raiseonerr=False ++ f"--allow-transfer={ipv6};" + ) +- assert 'Transfer failed' in result.stdout_text + result = self.master.run_command( + ['dig', f'@{ipv6}', zone, 'axfr'], raiseonerr=False + ) +-- +2.52.0 + diff --git a/SPECS/freeipa.spec b/SPECS/freeipa.spec index e0a3aab..1992387 100644 --- a/SPECS/freeipa.spec +++ b/SPECS/freeipa.spec @@ -232,7 +232,7 @@ Name: %{package_name} Version: %{IPA_VERSION} -Release: 22%{?rc_version:.%rc_version}%{?dist}.3 +Release: 22%{?rc_version:.%rc_version}.0.1%{?dist}.4 Summary: The Identity, Policy and Audit system License: GPL-3.0-or-later @@ -388,6 +388,17 @@ Patch0129: 0129-ipa-pwd-extop-Don-t-manipulate-the-config-if-not-ret.patch Patch0130: 0130-ipatests-fix-kdcproxy-tests-against-AD.patch Patch0131: 0131-ipatests-update-the-Let-s-Encrypt-cert-chain.patch Patch0132: 0132-ipa-join-initialize-pointer.patch +Patch0133: 0133-ipatests-remove-xfail-for-PKI-11.7.patch +Patch0134: 0134-GetEntryFromLDIF-handle-DNs-case-insensitive.patch +Patch0135: 0135-Tests-xmlrpc-mark-xfail-tests-requesting-cert-with-s.patch +Patch0136: 0136-Manual-backport-of-8002.patch +Patch0137: 0137-ipatests-Add-DNS-functional-integration-tests.patch +Patch0138: 0138-ipatests-add-Random-Password-based-replica-promotion.patch +Patch0139: 0139-ipatests-Add-integration-tests-for-ipa-join-command.patch +Patch0140: 0140-ipatests-Add-DNS-bugzilla-integration-tests.patch +Patch0141: 0141-ipatests-Add-DNS-integration-tests.patch +Patch0142: 0142-Allow-32bit-gid.patch +Patch0143: 0143-ipatests-Fix-test_allow_query_transfer_ipv6-when-IPv.patch Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch %endif %endif @@ -716,6 +727,7 @@ BuildArch: noarch Requires: %{name}-client-common = %{version}-%{release} Requires: httpd >= %{httpd_version} Requires: systemd-units >= %{systemd_version} +Requires: bind >= %{bind_version} %if 0%{?rhel} >= 8 && ! 0%{?eln} Requires: system-logos-ipa >= 80.4 %endif @@ -1191,7 +1203,8 @@ autoreconf -ivf %{enable_server_option} \ %{with_ipatests_option} \ %{with_ipa_join_xml_option} \ - %{linter_options} + %{linter_options} \ + --with-ipaplatform=rhel # run build in default dir # -Onone is workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1398405 @@ -2041,6 +2054,15 @@ fi %endif %changelog +* Wed Apr 22 2026 EL Errata - 4.12.2-22.0.1.el9_7.4 +- Set IPAPLATFORM=rhel when build on Oracle Linux [Orabug: 29516674] +- Add bind to ipa-server-common Requires [Orabug: 36518596] + +* Mon Mar 16 2026 David Hanina - 4.12.2-22.4 +- Resolves: RHEL-155038 Pagure #9953: Adding a group with 32Bit Idrange fails. +- Resolves: RHEL-153628 Include latest fixes in python3-ipatests package +- Resolves: RHEL-153621 Pagure #9854: Erroneous case-sensitivity in offline DSE lookup + * Thu Feb 5 2026 Florence Blanc-Renaud - 4.12.2-22.3 - Resolves: RHEL-141322 Memory leaks in IPA plugins