From 943a17d82c81b8ef92ff2ae186b0d26c354c83b6 Mon Sep 17 00:00:00 2001 From: David Hanina Date: Tue, 21 Jul 2026 13:27:46 +0200 Subject: [PATCH] ipa-4.13.2-1 - Resolves: RHEL-166742 [Cursor Automated] Include latest fixes in python3-ipatests package [RHEL9.9] - Resolves: RHEL-151581 Pagure #9948: Extend SELinux policy to allow use of SSSD authentication helpers on non-enrolled hosts [rhel-9] - Resolves: RHEL-151560 ipa-migrate doesn't process users' sshpublickeys [rhel-9] - Resolves: RHEL-179216 ipa-migrate tool is renaming host records & host info in automount information [RHEL 9.9] - Resolves: RHEL-212442 freeipa-client %triggerin and %post can fail (rpm scriptlets should never fail, by policy) [rhel-9] - Resolves: RHEL-212443 ipa-migrate doesn't properly handle replication conflicts [rhel-9] - Resolves: RHEL-212444 ipa-migrate doesn't migrate IPA locations [rhel-9] - Resolves: RHEL-212445 Pagure #9948: Extend SELinux policy to allow use of SSSD authentication helpers on non-enrolled hosts [rhel-9] - Resolves: RHEL-212446 [RFE] X-Content-Type-Options header in IdM WebUI [rhel-9] - Resolves: RHEL-212447 [RFE] Implement Referrer-Policy on IdM WebUI [rhel-9] - Resolves: RHEL-212448 [RFE] CSP not implemented in IdM WebUI [rhel-9] - Resolves: RHEL-212449 IPA compatibility with tomcat 10 [rhel-9] - Resolves: RHEL-212450 Webui 3007 Requirement error "userCertificate" is required (not allowing certificate management for idview) [rhel-9] - Resolves: RHEL-212451 To add "SameSite" flag to ipa_session cookies [rhel-9] - Resolves: RHEL-212452 FreeIPA JSON-RPC CSRF Referer Validation Uses Naive Prefix Match - [rhel-9] - Resolves: RHEL-212471 Pagure #9820: ipalockout_postop is incorrectly warning about locked user [rhel-9] - Resolves: RHEL-178920 Rebase ipa to latest version for RHEL 9.9 Signed-off-by: David Hanina --- .gitignore | 2 + 0003-Revert-Remove-NIS-server-support.patch | 20 +- 0004-Change-u-to-u-for-sysusersd.patch | 31 + ...-Stop-using-deprecated-pkg_resources.patch | 21 +- ...pire_password-fixture-into-TestIPACo.patch | 120 - ...-Fix-xfail-assertion-for-sssd-2.12.0.patch | 48 - ...Add-DNS-functional-integration-tests.patch | 1279 -------- ...-card-client-script-does-not-need-kr.patch | 99 - ...olver-state-tracking-in-enforced-DNS.patch | 177 -- ...Use-systemd-sysusers-to-setup-users-.patch | 224 -- ...dom-Password-based-replica-promotion.patch | 196 -- ...tegration-tests-for-ipa-join-command.patch | 760 ----- ...e-case-insensitive-comparison-for-do.patch | 48 - ...andle-IPACertificate-types-in-xmlrpc.patch | 43 - ...place-None-with-when-uninstalling-CA.patch | 159 - ...-xmlrpc-tests-for-ipa-delegation-cli.patch | 416 --- 0019-ipa-join-initialize-pointer.patch | 49 - ...uning-is-enabled-when-RSN-is-enabled.patch | 39 - ...s-Add-DNS-bugzilla-integration-tests.patch | 1627 ----------- ...d-int-overflow-with-pwpolicy-minlife.patch | 154 - ...-fix-install-method-for-BasePWpolicy.patch | 42 - ...te-expected-max-value-for-krbminpwdl.patch | 36 - 0025-ipatests-Add-DNS-integration-tests.patch | 1123 ------- ...-make-tests-compatible-with-Pytest-9.patch | 42 - ...d-ipa-selfservice-BZ-tests-to-xmlrpc.patch | 154 - 0028-Allow-32bit-gid.patch | 37 - ...pa-selfservice-users-tests-to-xmlrpc.patch | 400 --- ...t_allow_query_transfer_ipv6-when-IPv.patch | 138 - ...L-RPC-tests-for-i18n-user-attributes.patch | 217 -- ...fservice-add-and-selfservice-del-cli.patch | 229 -- ...s-Additional-tests-for-32BitIdranges.patch | 421 --- ...P-GSSAPI-Kerberos-authentication-tes.patch | 260 -- ...atests-Extend-netgroup-test-coverage.patch | 2595 ----------------- ...r-principal-ipa-getkeytab-and-ipa-rm.patch | 543 ---- ...nal-tests-for-ipa-ipa-migration-test.patch | 285 -- ...atests-ipa-migrate-ds-test-scenarios.patch | 288 -- ...-selfservice-show-and-selfservice-mo.patch | 506 ---- ...fservice-find-cli-tests-to-xmlrpc-Ad.patch | 506 ---- ...ipatests-fix-the-method-add_a_record.patch | 36 - ...ts-Remove-xfail-for-sssd-issues-7169.patch | 48 - ...show-ipa-all-not-listing-RSN-version.patch | 28 - 0044-ipatests-fix-migration-test.patch | 49 - freeipa.spec | 94 +- sources | 4 +- 44 files changed, 94 insertions(+), 13499 deletions(-) create mode 100644 0004-Change-u-to-u-for-sysusersd.patch delete mode 100644 0007-ipatests-Move-expire_password-fixture-into-TestIPACo.patch delete mode 100644 0008-ipatests-Fix-xfail-assertion-for-sssd-2.12.0.patch delete mode 100644 0009-ipatests-Add-DNS-functional-integration-tests.patch delete mode 100644 0010-ipa-advise-smart-card-client-script-does-not-need-kr.patch delete mode 100644 0011-ipatests-Fix-resolver-state-tracking-in-enforced-DNS.patch delete mode 100644 0012-freeipa.spec.in-Use-systemd-sysusers-to-setup-users-.patch delete mode 100644 0013-ipatests-add-Random-Password-based-replica-promotion.patch delete mode 100644 0014-ipatests-Add-integration-tests-for-ipa-join-command.patch delete mode 100644 0015-fetch_domains-Use-case-insensitive-comparison-for-do.patch delete mode 100644 0016-Handle-IPACertificate-types-in-xmlrpc.patch delete mode 100644 0017-Replace-None-with-when-uninstalling-CA.patch delete mode 100644 0018-ipatests-Add-xmlrpc-tests-for-ipa-delegation-cli.patch delete mode 100644 0019-ipa-join-initialize-pointer.patch delete mode 100644 0020-ipatests-pruning-is-enabled-when-RSN-is-enabled.patch delete mode 100644 0021-ipatests-Add-DNS-bugzilla-integration-tests.patch delete mode 100644 0022-Avoid-int-overflow-with-pwpolicy-minlife.patch delete mode 100644 0023-ipatests-fix-install-method-for-BasePWpolicy.patch delete mode 100644 0024-webui-tests-update-expected-max-value-for-krbminpwdl.patch delete mode 100644 0025-ipatests-Add-DNS-integration-tests.patch delete mode 100644 0026-ipatest-make-tests-compatible-with-Pytest-9.patch delete mode 100644 0027-ipatests-Add-ipa-selfservice-BZ-tests-to-xmlrpc.patch delete mode 100644 0028-Allow-32bit-gid.patch delete mode 100644 0029-ipatests-Add-ipa-selfservice-users-tests-to-xmlrpc.patch delete mode 100644 0030-ipatests-Fix-test_allow_query_transfer_ipv6-when-IPv.patch delete mode 100644 0031-ipatests-Add-XML-RPC-tests-for-i18n-user-attributes.patch delete mode 100644 0032-ipatests-Add-selfservice-add-and-selfservice-del-cli.patch delete mode 100644 0033-ipatests-Additional-tests-for-32BitIdranges.patch delete mode 100644 0034-ipatests-add-HTTP-GSSAPI-Kerberos-authentication-tes.patch delete mode 100644 0035-ipatests-Extend-netgroup-test-coverage.patch delete mode 100644 0036-ipatests-Add-user-principal-ipa-getkeytab-and-ipa-rm.patch delete mode 100644 0037-ipatests-Additional-tests-for-ipa-ipa-migration-test.patch delete mode 100644 0038-ipatests-ipa-migrate-ds-test-scenarios.patch delete mode 100644 0039-ipatests-Add-ipa-selfservice-show-and-selfservice-mo.patch delete mode 100644 0040-ipatests-Add-selfservice-find-cli-tests-to-xmlrpc-Ad.patch delete mode 100644 0041-ipatests-fix-the-method-add_a_record.patch delete mode 100644 0042-ipatests-Remove-xfail-for-sssd-issues-7169.patch delete mode 100644 0043-Fix-ipa-ca-show-ipa-all-not-listing-RSN-version.patch delete mode 100644 0044-ipatests-fix-migration-test.patch diff --git a/.gitignore b/.gitignore index 7024cf3..d7226bf 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,5 @@ /freeipa-4.13.0.tar.gz.asc /freeipa-4.13.1.tar.gz /freeipa-4.13.1.tar.gz.asc +/freeipa-4.13.2.tar.gz +/freeipa-4.13.2.tar.gz.asc \ No newline at end of file diff --git a/0003-Revert-Remove-NIS-server-support.patch b/0003-Revert-Remove-NIS-server-support.patch index fb9b690..63976ba 100644 --- a/0003-Revert-Remove-NIS-server-support.patch +++ b/0003-Revert-Remove-NIS-server-support.patch @@ -32,10 +32,10 @@ This reverts commit e98a80827bcc42dc16b516355077fed844220107. create mode 100644 ipaserver/install/plugins/update_nis.py diff --git a/freeipa.spec.in b/freeipa.spec.in -index b93199a5783a89e0ff58affd3d416556f72dd00c..6ee0f43f059dad4bba2288b1b0d3a63437371861 100644 +index 36cf4f185c5d269b485e08a57641c33986876daa..42bcd57e64b6d8de3ac457e9d2faa4925d29250c 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in -@@ -1619,6 +1619,7 @@ fi +@@ -1607,6 +1607,7 @@ fi %{_sbindir}/ipa-ldap-updater %{_sbindir}/ipa-otptoken-import %{_sbindir}/ipa-compat-manage @@ -43,7 +43,7 @@ index b93199a5783a89e0ff58affd3d416556f72dd00c..6ee0f43f059dad4bba2288b1b0d3a634 %{_sbindir}/ipa-managed-entries %{_sbindir}/ipactl %{_sbindir}/ipa-advise -@@ -1694,6 +1695,7 @@ fi +@@ -1682,6 +1683,7 @@ fi %{_mandir}/man1/ipa-ca-install.1* %{_mandir}/man1/ipa-kra-install.1* %{_mandir}/man1/ipa-compat-manage.1* @@ -591,7 +591,7 @@ index 5bc0e3ac3eed257fd7fa6a0830e3e2dbeb3d01d2..b84925bfc7d9af8584d9d4b185aa04b6 SCHEMA_COMPAT_POST_ULDIF = "/usr/share/ipa/schema_compat_post.uldif" SUBID_GENERATORS_ULDIF = "/usr/share/ipa/subid-generators.uldif" diff --git a/ipaserver/install/ipa_migrate.py b/ipaserver/install/ipa_migrate.py -index f07a878e739002346df894f93745ea60345e0557..b0a27de5dd40aa661271faa3a8a0855226b5892f 100644 +index 9f909a2e70ed7f7b7bf52001950c6a504f78f3cd..126a3c7618c4dd2a5a0b7ee6056a1d31c3a2c3da 100644 --- a/ipaserver/install/ipa_migrate.py +++ b/ipaserver/install/ipa_migrate.py @@ -32,7 +32,7 @@ from ipapython.admintool import admin_cleanup_global_argv @@ -600,7 +600,7 @@ index f07a878e739002346df894f93745ea60345e0557..b0a27de5dd40aa661271faa3a8a08552 STRIP_OP_ATTRS, STRIP_ATTRS, STRIP_OC, PROD_ATTRS, - DNA_REGEN_VAL, DNA_REGEN_ATTRS, IGNORE_ATTRS, + DNA_REGEN_VAL, DNA_REGEN_ATTRS, NIS_PLUGIN, IGNORE_ATTRS, - DB_EXCLUDE_TREES, POLICY_OP_ATTRS, STATE_OPTIONS + DB_EXCLUDE_TREES, POLICY_OP_ATTRS, STATE_OPTIONS, REDACTED_ATTRS, ) @@ -749,7 +749,8 @@ class IPAMigrate(): @@ -613,7 +613,7 @@ index f07a878e739002346df894f93745ea60345e0557..b0a27de5dd40aa661271faa3a8a08552 logged_something = True if not self.log_stats(DS_INDEXES) and not logged_something: self.log_info(" - No updates") -@@ -1983,6 +1984,28 @@ class IPAMigrate(): +@@ -2063,6 +2064,28 @@ class IPAMigrate(): add_missing=True) stats['config_processed'] += 1 @@ -643,10 +643,10 @@ index f07a878e739002346df894f93745ea60345e0557..b0a27de5dd40aa661271faa3a8a08552 # Migration # diff --git a/ipaserver/install/ipa_migrate_constants.py b/ipaserver/install/ipa_migrate_constants.py -index 65e920d4bdc7511adb60bbdf87db88beb2630645..4fe8b467978936e4f8d621b7297ff577127c7298 100644 +index 64dd11cd1244ee30a2eceea3ef823e9092035738..e2f97e6d38c4d47553b854e29b8e0e8903477639 100644 --- a/ipaserver/install/ipa_migrate_constants.py +++ b/ipaserver/install/ipa_migrate_constants.py -@@ -527,6 +527,30 @@ DS_CONFIG = { +@@ -542,6 +542,30 @@ DS_CONFIG = { }, } @@ -788,10 +788,10 @@ index 6c86bbb657a0d9a7b74ef34ad20a796a10073315..4acf8dd5290c687723a9c43a21e194bf (['ipa-replica-manage', 'list'], 1, IPA_NOT_CONFIGURED, None), (['ipa-server-certinstall'], 2, None, IPA_NOT_CONFIGURED), diff --git a/ipatests/test_integration/test_commands.py b/ipatests/test_integration/test_commands.py -index 5bc871ab71bc05ec1c26df8352e996a9e627b466..8f8548494eaec3afd00961c096772b9d0b1ab3a4 100644 +index 5021747b67e0b97d322b13261f7e90e688f50cfe..bcd4e2aeb30f45b2a5f2713d213f077b571b6191 100644 --- a/ipatests/test_integration/test_commands.py +++ b/ipatests/test_integration/test_commands.py -@@ -1382,6 +1382,93 @@ class TestIPACommand(IntegrationTest): +@@ -1480,6 +1480,93 @@ class TestIPACommand(IntegrationTest): serverid = realm_to_serverid(self.master.domain.realm) return ("dirsrv@%s.service" % serverid) diff --git a/0004-Change-u-to-u-for-sysusersd.patch b/0004-Change-u-to-u-for-sysusersd.patch new file mode 100644 index 0000000..5d8d7e5 --- /dev/null +++ b/0004-Change-u-to-u-for-sysusersd.patch @@ -0,0 +1,31 @@ +From c1f9626a4b2b42cade05f83bd5699418e60767a4 Mon Sep 17 00:00:00 2001 +From: David Hanina +Date: Tue, 21 Jul 2026 15:10:49 +0200 +Subject: [PATCH] Change u! to u for sysusersd + +Adapts the sysusersd settings for RHEL9 to only use u, instead of u!, +which errors out. + +Signed-off-by: David Hanina +--- + init/sysusersd/ipa.conf.in | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/init/sysusersd/ipa.conf.in b/init/sysusersd/ipa.conf.in +index dcddfc2fc..0955fd341 100644 +--- a/init/sysusersd/ipa.conf.in ++++ b/init/sysusersd/ipa.conf.in +@@ -1,8 +1,8 @@ + # IPA KDC Proxy user and group +-u! kdcproxy - "IPA KDC Proxy User" ++u kdcproxy - "IPA KDC Proxy User" + + # IPA API user and group +-u! ipaapi - "IPA API User" ++u ipaapi - "IPA API User" + + # - add Apache system account to ipaapi group (platform-specific) + m @HTTPD_USER@ ipaapi +-- +2.54.0 + diff --git a/0006-Revert-Stop-using-deprecated-pkg_resources.patch b/0006-Revert-Stop-using-deprecated-pkg_resources.patch index a21b23c..2b16861 100644 --- a/0006-Revert-Stop-using-deprecated-pkg_resources.patch +++ b/0006-Revert-Stop-using-deprecated-pkg_resources.patch @@ -29,7 +29,7 @@ diff --git a/freeipa.spec.in b/freeipa.spec.in index b93199a5783a89e0ff58affd3d416556f72dd00c..bf6fa216dcc4cfbac903aad43c6219f1e57db4dd 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in -@@ -983,7 +983,6 @@ Requires: python3-jwcrypto >= 0.4.2 +@@ -977,7 +977,6 @@ Requires: python3-jwcrypto >= 0.4.2 Requires: python3-libipa_hbac Requires: python3-netaddr >= %{python_netaddr_version} Requires: python3-netifaces >= 0.10.4 @@ -37,7 +37,7 @@ index b93199a5783a89e0ff58affd3d416556f72dd00c..bf6fa216dcc4cfbac903aad43c6219f1 Requires: python3-pyasn1 >= 0.3.2-2 Requires: python3-pyasn1-modules >= 0.3.2-2 Requires: python3-pyusb -@@ -992,6 +991,11 @@ Requires: python3-requests +@@ -986,6 +985,11 @@ Requires: python3-requests Requires: python3-six Requires: python3-sss-murmur Requires: python3-yubico >= 1.3.2-7 @@ -269,15 +269,14 @@ diff --git a/ipatests/test_integration/test_idp.py b/ipatests/test_integration/t index 9983b3c9d62826afa395c2739b4fb9945afed80d..a29333ef232b0ae8229484e404866d8bb720554a 100644 --- a/ipatests/test_integration/test_idp.py +++ b/ipatests/test_integration/test_idp.py -@@ -12,7 +12,7 @@ from ipatests.pytest_ipa.integration import tasks, create_keycloak - user_code_script = textwrap.dedent(""" - from selenium import webdriver - from datetime import datetime --from packaging.version import parse as parse_version -+from pkg_resources import parse_version - from selenium.webdriver.firefox.options import Options - from selenium.webdriver.common.by import By - from selenium.webdriver.support.ui import WebDriverWait +@@ -36,6 +36,6 @@ SELENIUM_REMOTE_HEAD = textwrap.dedent( + from selenium import webdriver + from datetime import datetime +- from packaging.version import parse as parse_version ++ from pkg_resources import parse_version + from selenium.webdriver.firefox.options import Options + from selenium.webdriver.common.by import By + from selenium.webdriver.support.ui import WebDriverWait diff --git a/ipatests/test_integration/test_ipahealthcheck.py b/ipatests/test_integration/test_ipahealthcheck.py index 47392cec9345b6d2447b728fc21050edabe8f0cd..15fc38a6e155b343259ee61c6c7c5c561c7f6410 100644 --- a/ipatests/test_integration/test_ipahealthcheck.py diff --git a/0007-ipatests-Move-expire_password-fixture-into-TestIPACo.patch b/0007-ipatests-Move-expire_password-fixture-into-TestIPACo.patch deleted file mode 100644 index 53a9baa..0000000 --- a/0007-ipatests-Move-expire_password-fixture-into-TestIPACo.patch +++ /dev/null @@ -1,120 +0,0 @@ -From 1c86c973c8dc778a71c8e2abf54ff37ececdd696 Mon Sep 17 00:00:00 2001 -From: PRANAV THUBE -Date: Tue, 13 Jan 2026 19:30:09 +0530 -Subject: [PATCH] ipatests: Move expire_password fixture into TestIPACommand - class - -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: Rob Crittenden ---- - .../nightly_ipa-4-13_latest.yaml | 2 +- - .../nightly_ipa-4-13_latest_selinux.yaml | 2 +- - ipatests/test_integration/test_commands.py | 59 +++++++++---------- - 3 files changed, 31 insertions(+), 32 deletions(-) - -diff --git a/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml b/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml -index 210739d4b576882ee43e06d85bce819ff30d2357..aff55727e463207fb235ff340989491e62162149 100644 ---- a/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml -+++ b/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml -@@ -1914,7 +1914,7 @@ jobs: - class: RunPytest - args: - build_url: '{fedora-latest-ipa-4-13/build_url}' -- test_suite: test_integration/test_random_serial_numbers.py::TestIPACommand_RSN::test_certificate_out_write_to_file -+ test_suite: test_integration/test_random_serial_numbers.py::TestIPACommand_RSN - template: *ci-ipa-4-13-latest - timeout: 5400 - topology: *master_1repl_1client -diff --git a/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml b/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml -index 0fb7c050b97bf66645599fbff46b53c048211f96..e6c57ea060b3bb8bfdf8b6f981f8fd28e4a7d320 100644 ---- a/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml -+++ b/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml -@@ -2066,7 +2066,7 @@ jobs: - args: - build_url: '{fedora-latest-ipa-4-13/build_url}' - selinux_enforcing: True -- test_suite: test_integration/test_random_serial_numbers.py::TestIPACommand_RSN::test_certificate_out_write_to_file -+ test_suite: test_integration/test_random_serial_numbers.py::TestIPACommand_RSN - template: *ci-ipa-4-13-latest - timeout: 5400 - topology: *master_1repl_1client -diff --git a/ipatests/test_integration/test_commands.py b/ipatests/test_integration/test_commands.py -index 01001e4c92037599b075e9bca3ddda7d0c8e8ffa..eda98e92b6b494752d74c6381bdcb6d5c47a26d1 100644 ---- a/ipatests/test_integration/test_commands.py -+++ b/ipatests/test_integration/test_commands.py -@@ -199,36 +199,6 @@ duplicatesubject = ( - duplicate_serial = "4097" - - --@pytest.fixture() --def expire_password(): -- """ -- Fixture to expire a user's password far into the future past -- 2038, then revert time back. -- """ -- hosts = dict() -- -- def _expire_password(host): -- hosts['host'] = host -- tasks.move_date(host, 'stop', '+20Years') -- host.run_command( -- ['ipactl', 'restart', '--ignore-service-failures'] -- ) -- -- yield _expire_password -- -- host = hosts.pop('host') -- # Prior to uninstall remove all the cert tracking to prevent -- # errors from certmonger trying to check the status of certs -- # that don't matter because we are uninstalling. -- host.run_command(['systemctl', 'stop', 'certmonger']) -- # Important: run_command with a str argument is able to -- # perform shell expansion but run_command with a list of -- # arguments is not -- host.run_command('rm -fv ' + paths.CERTMONGER_REQUESTS_DIR + '*') -- tasks.uninstall_master(host) -- tasks.move_date(host, 'start', '-20Years') -- -- - class TestIPACommand(IntegrationTest): - """ - A lot of commands can be executed against a single IPA installation -@@ -239,6 +209,35 @@ class TestIPACommand(IntegrationTest): - num_replicas = 1 - num_clients = 1 - -+ @pytest.fixture -+ def expire_password(self): -+ """ -+ Fixture to expire a user's password far into the future past -+ 2038, then revert time back. -+ """ -+ hosts = dict() -+ -+ def _expire_password(host): -+ hosts['host'] = host -+ tasks.move_date(host, 'stop', '+20Years') -+ host.run_command( -+ ['ipactl', 'restart', '--ignore-service-failures'] -+ ) -+ -+ yield _expire_password -+ -+ host = hosts.pop('host') -+ # Prior to uninstall remove all the cert tracking to prevent -+ # errors from certmonger trying to check the status of certs -+ # that don't matter because we are uninstalling. -+ host.run_command(['systemctl', 'stop', 'certmonger']) -+ # Important: run_command with a str argument is able to -+ # perform shell expansion but run_command with a list of -+ # arguments is not -+ host.run_command('rm -fv ' + paths.CERTMONGER_REQUESTS_DIR + '*') -+ tasks.uninstall_master(host) -+ tasks.move_date(host, 'start', '-20Years') -+ - @pytest.fixture - def pwpolicy_global(self): - """Fixture to change global password history policy and reset it""" --- -2.52.0 - diff --git a/0008-ipatests-Fix-xfail-assertion-for-sssd-2.12.0.patch b/0008-ipatests-Fix-xfail-assertion-for-sssd-2.12.0.patch deleted file mode 100644 index b564201..0000000 --- a/0008-ipatests-Fix-xfail-assertion-for-sssd-2.12.0.patch +++ /dev/null @@ -1,48 +0,0 @@ -From ecc0efa96e3c446586425d470657de7f2d5376bf Mon Sep 17 00:00:00 2001 -From: Florence Blanc-Renaud -Date: Mon, 19 Jan 2026 17:00:41 +0100 -Subject: [PATCH] ipatests: Fix xfail assertion for sssd 2.12.0 - -SSSD 2.12.0 provides fixes for -https://github.com/SSSD/sssd/issues/5989 -https://github.com/SSSD/sssd/issues/7169 - -Update the condition expecting test failures in test_trust.py - -Signed-off-by: Florence Blanc-Renaud -Reviewed-By: David Hanina ---- - ipatests/test_integration/test_trust.py | 10 ++++++---- - 1 file changed, 6 insertions(+), 4 deletions(-) - -diff --git a/ipatests/test_integration/test_trust.py b/ipatests/test_integration/test_trust.py -index 13ad0afa4c1fb032d50f40cf7cb9b79283203225..0cab277c910a6d35f35b57e3068ee6f38706af59 100644 ---- a/ipatests/test_integration/test_trust.py -+++ b/ipatests/test_integration/test_trust.py -@@ -1219,9 +1219,10 @@ class TestNonPosixAutoPrivateGroup(BaseTestTrust): - assert (uid == self.uid_override and gid == self.gid_override) - test_group = self.clients[0].run_command( - ["id", nonposixuser]).stdout_text -- cond2 = ((type == 'false' -- and sssd_version >= tasks.parse_version("2.9.4")) -- or type == 'hybrid') -+ cond2 = (((type == 'false' -+ and sssd_version >= tasks.parse_version("2.9.4")) -+ or type == 'hybrid') -+ and sssd_version < tasks.parse_version("2.12.0")) - with xfail_context(cond2, - 'https://github.com/SSSD/sssd/issues/5989 ' - 'and 7169'): -@@ -1347,7 +1348,8 @@ class TestPosixAutoPrivateGroup(BaseTestTrust): - and gid == self.gid_override) - result = self.clients[0].run_command(['id', posixuser]) - sssd_version = tasks.get_sssd_version(self.clients[0]) -- bad_version = sssd_version >= tasks.parse_version("2.9.4") -+ bad_version = (tasks.parse_version("2.9.4") <= sssd_version -+ < tasks.parse_version("2.12.0")) - with xfail_context(bad_version and type in ('false', 'hybrid'), - "https://github.com/SSSD/sssd/issues/7169"): - assert "10047(testgroup@{0})".format( --- -2.52.0 - diff --git a/0009-ipatests-Add-DNS-functional-integration-tests.patch b/0009-ipatests-Add-DNS-functional-integration-tests.patch deleted file mode 100644 index 10085a7..0000000 --- a/0009-ipatests-Add-DNS-functional-integration-tests.patch +++ /dev/null @@ -1,1279 +0,0 @@ -From aa90ce09c3f6d496ee2cf63123c619f1008140b4 Mon Sep 17 00:00:00 2001 -From: PRANAV THUBE -Date: Wed, 31 Dec 2025 12:15:51 +0530 -Subject: [PATCH] ipatests: Add DNS functional integration tests. - -Add tests covering DNS zone management and all record types: -A, AAAA, AFSDB, CNAME, TXT, SRV, MX, PTR, NAPTR, DNAME, CERT, LOC, KX -Including zone permissions and negative test cases. - -Related: https://pagure.io/freeipa/issue/9911 -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: Rafael Guterres Jeffman -Reviewed-By: David Hanina ---- - ipatests/pytest_ipa/integration/tasks.py | 126 +++- - ipatests/test_integration/test_dns.py | 707 +++++++++++++++++++++++ - ipatests/test_xmlrpc/test_dns_plugin.py | 288 ++++++++- - 3 files changed, 1113 insertions(+), 8 deletions(-) - -diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py -index 7def8fe3962487f28999cb975aff512cd36331fe..32ac5cbc2c6fe87850dfb15c1d5beae6fa648dfb 100755 ---- a/ipatests/pytest_ipa/integration/tasks.py -+++ b/ipatests/pytest_ipa/integration/tasks.py -@@ -1628,6 +1628,71 @@ def add_a_record(master, host): - '--a-rec', host.ip]) - - -+def add_dns_record(master, zone, name, record_type, record_value, -+ raiseonerr=True): -+ """Add DNS record of any type. -+ -+ :param master: The IPA master host to run command on -+ :param zone: DNS zone name (e.g., 'example.com.') -+ :param name: Record name (e.g., 'www' or '@' for zone apex) -+ :param record_type: Record type like 'a', 'aaaa', 'afsdb', 'cname', -+ 'txt', 'srv', 'mx', 'ptr', 'naptr', 'dname', -+ 'cert', 'loc', 'kx', etc. -+ :param record_value: List of values for the record -+ :param raiseonerr: If True, raise exception on command failure -+ :return: Command result object -+ """ -+ command = ['ipa', 'dnsrecord-add', zone, name] -+ opt = f'--{record_type}-rec' -+ for val in record_value: -+ command.extend([opt, val]) -+ return master.run_command(command, raiseonerr=raiseonerr) -+ -+ -+def del_dns_record(master, zone, name, record_type=None, record_value=None, -+ del_all=False, raiseonerr=True): -+ """Delete DNS record of any type. -+ -+ :param record_type: Record type like 'a', 'aaaa', 'afsdb', 'cname', etc. -+ :param record_value: List of values (optional) -+ :param del_all: If True, delete all records for this name -+ """ -+ command = ['ipa', 'dnsrecord-del', zone, name] -+ if del_all: -+ command.append('--del-all') -+ elif record_type and record_value: -+ opt = f'--{record_type}-rec' -+ for val in record_value: -+ command.extend([opt, val]) -+ return master.run_command(command, raiseonerr=raiseonerr) -+ -+ -+def find_dns_record(master, zone, name=None, raiseonerr=True): -+ """Find DNS record. -+ -+ :param name: Record name, if not provided searches all records in zone -+ """ -+ command = ['ipa', 'dnsrecord-find', zone] -+ if name is not None: -+ command.append(name) -+ return master.run_command(command, raiseonerr=raiseonerr) -+ -+ -+def mod_dns_record(master, zone, name, record_type, old_value, new_value, -+ raiseonerr=True): -+ """Modify DNS record value. -+ -+ :param record_type: Record type like 'a', 'aaaa', 'txt', etc. -+ :param old_value: Current record value -+ :param new_value: New record value -+ """ -+ return master.run_command([ -+ 'ipa', 'dnsrecord-mod', zone, name, -+ f'--{record_type}-rec={old_value}', -+ f'--{record_type}-data={new_value}' -+ ], raiseonerr=raiseonerr) -+ -+ - def resolve_record(nameserver, query, rtype="SOA", retry=True, timeout=100): - """Resolve DNS record - :retry: if resolution failed try again until timeout is reached -@@ -2002,7 +2067,9 @@ def ldappasswd_sysaccount_change(user, oldpw, newpw, master, use_dirman=False): - - - def add_dns_zone(master, zone, skip_overlap_check=False, -- dynamic_update=False, add_a_record_hosts=None): -+ dynamic_update=False, add_a_record_hosts=None, -+ admin_email=None, refresh=None, retry=None, -+ expire=None, minimum=None, ttl=None, raiseonerr=True): - """ - Add DNS zone if it is not already added. - """ -@@ -2010,14 +2077,27 @@ def add_dns_zone(master, zone, skip_overlap_check=False, - result = master.run_command( - ['ipa', 'dnszone-show', zone], raiseonerr=False) - -+ # Verify both return code and zone name before adding - if result.returncode != 0: - command = ['ipa', 'dnszone-add', zone] - if skip_overlap_check: - command.append('--skip-overlap-check') - if dynamic_update: - command.append('--dynamic-update=True') -- -- master.run_command(command) -+ if admin_email: -+ command.append('--admin-email=' + admin_email) -+ if refresh: -+ command.append('--refresh=' + str(refresh)) -+ if retry: -+ command.append('--retry=' + str(retry)) -+ if expire: -+ command.append('--expire=' + str(expire)) -+ if minimum: -+ command.append('--minimum=' + str(minimum)) -+ if ttl: -+ command.append('--ttl=' + str(ttl)) -+ -+ master.run_command(command, raiseonerr=raiseonerr) - - if add_a_record_hosts: - for host in add_a_record_hosts: -@@ -2027,6 +2107,46 @@ def add_dns_zone(master, zone, skip_overlap_check=False, - logger.debug('Zone %s already added.', zone) - - -+def del_dns_zone(host, zone, raiseonerr=False): -+ """Delete DNS zone.""" -+ return host.run_command( -+ ['ipa', 'dnszone-del', zone], raiseonerr=raiseonerr) -+ -+ -+def find_dns_zone(host, zone, all_attrs=False, raiseonerr=True): -+ """Find DNS zone.""" -+ command = ['ipa', 'dnszone-find', zone] -+ if all_attrs: -+ command.append('--all') -+ return host.run_command(command, raiseonerr=raiseonerr) -+ -+ -+def show_dns_zone(host, zone, all_attrs=False, raiseonerr=True): -+ """Show DNS zone.""" -+ command = ['ipa', 'dnszone-show', zone] -+ if all_attrs: -+ command.append('--all') -+ return host.run_command(command, raiseonerr=raiseonerr) -+ -+ -+def add_dns_zone_permission(host, zone, raiseonerr=True): -+ """Add permission to manage DNS zone.""" -+ return host.run_command(['ipa', 'dnszone-add-permission', zone], -+ raiseonerr=raiseonerr) -+ -+ -+def remove_dns_zone_permission(host, zone, raiseonerr=True): -+ """Remove permission to manage DNS zone.""" -+ return host.run_command(['ipa', 'dnszone-remove-permission', zone], -+ raiseonerr=raiseonerr) -+ -+ -+def find_permission(host, permission, raiseonerr=True): -+ """Find permission.""" -+ return host.run_command(['ipa', 'permission-find', permission], -+ raiseonerr=raiseonerr) -+ -+ - def sign_ca_and_transport(host, csr_name, root_ca_name, ipa_ca_name, - root_ca_path_length=None, ipa_ca_path_length=1, - key_size=None, root_ca_extensions=()): -diff --git a/ipatests/test_integration/test_dns.py b/ipatests/test_integration/test_dns.py -index 1cef22525b690050aa1230733f115cd7f4099c53..905227497c8879bfe4cf8b027e396689c0451208 100644 ---- a/ipatests/test_integration/test_dns.py -+++ b/ipatests/test_integration/test_dns.py -@@ -5,9 +5,70 @@ - - from __future__ import absolute_import - -+import time -+ -+import dns.resolver -+from ipapython.dnsutil import DNSResolver - from ipatests.pytest_ipa.integration import tasks - from ipatests.test_integration.base import IntegrationTest - -+# ============================================================================= -+# DNS Test Constants -+# ============================================================================= -+# Test zone configuration -+ZONE = "testzone" -+EMAIL = "ipaqar.redhat.com" -+REFRESH = 303 -+RETRY = 101 -+EXPIRE = 1202 -+MINIMUM = 33 -+TTL = 55 -+ -+# A record values -+A_RECORD = "1.2.3.4" -+MULTI_A_RECORD1 = "1.2.3.4" -+MULTI_A_RECORD2 = "2.3.4.5" -+ -+# AAAA record values -+AAAA = "fec0:0:a10:6000:10:16ff:fe98:193" -+AAAA_BAD1 = "bada:aaaa:real:ly:bad:dude:extr:a" -+AAAA_BAD2 = "aaaa:bbbb:cccc:dddd:eeee:fffff" -+ -+# Other record types -+AFSDB = "green.femto.edu." -+CNAME = "m.l.k." -+TXT = "none=1.2.3.4" -+SRV_A = "0 100 389" -+SRV = "why.go.here.com." -+NAPTR = '100 10 U E2U+msg !^.*$!mailto:info@example.com! .' -+NAPTR_FIND = "info@example.com" -+DNAME = f"bar.{ZONE}." -+DNAME2 = f"bar_underscore.{ZONE}." -+CERT_B = "1 1 1" -+CERT = "F835EDA21E94B565716F" -+LOC = "37 23 30.900 N 121 59 19.000 W 7.00m 100.00m 100.00m 2.00m" -+ -+# KX records -+KX_PREF1 = "1234" -+KX_BAD_PREF1 = "-1" -+KX_BAD_PREF2 = "123345678" -+ -+# PTR zone configuration -+PTR_OCTET = "4.4.4" -+PTR_ZONE = f"{PTR_OCTET}.in-addr.arpa." -+PTR = "8" -+PTR_VALUE = "in.awesome.domain." -+PTR_EMAIL = "ipaqar.redhat.com" -+PTR_REFRESH = 393 -+PTR_RETRY = 191 -+PTR_EXPIRE = 1292 -+PTR_MINIMUM = 39 -+PTR_TTL = 59 -+ -+# Persistent search test values -+NEW_TXT = "newip=5.6.7.8" -+NEWER_TXT = "newip=8.7.6.5" -+ - - class TestDNS(IntegrationTest): - """Tests for DNS feature. -@@ -38,3 +99,649 @@ class TestDNS(IntegrationTest): - cmd = self.master.run_command(['dig', '+short', '-t', 'SOA', - self.master.domain.name]) - assert 'fake' not in cmd.stdout_text -+ -+ -+class TestDNSAcceptance(IntegrationTest): -+ """DNS Acceptance tests. -+ -+ This test class covers all the DNS acceptance tests including -+ zone management, record types (A, AAAA, AFSDB, CNAME, TXT, SRV, MX, -+ PTR, NAPTR, DNAME, CERT, LOC, KX), zone permissions, and persistent -+ search functionality. -+ -+ Converted from bash test script t.dns.sh -+ """ -+ topology = 'line' -+ num_replicas = 0 -+ -+ @classmethod -+ def install(cls, mh): -+ super(TestDNSAcceptance, cls).install(mh) -+ # Set domain-dependent values -+ cls.MANAGED_ZONE = f"qa.{cls.master.domain.name}" -+ cls.MANAGED_ZONE1 = f"dev.{cls.master.domain.name}" -+ cls.NONEXISTENT_ZONE = f"nonexistent.{cls.master.domain.name}" -+ cls.ZONE_PSEARCH = f"westford.{cls.master.domain.name}" -+ cls.MX = f"mail.{cls.master.domain.name}" -+ cls.A_HOST = f"1.{cls.master.domain.name}" -+ # Setup DNS resolver for test queries -+ cls.resolver = DNSResolver() -+ cls.resolver.nameservers = [cls.master.ip] -+ cls.resolver.lifetime = 10 -+ -+ @classmethod -+ def uninstall(cls, mh): -+ # Cleanup zones if they exist -+ tasks.kinit_admin(cls.master) -+ for zone in [ZONE, PTR_ZONE, cls.MANAGED_ZONE, -+ cls.MANAGED_ZONE1, cls.ZONE_PSEARCH]: -+ if zone: -+ cls.master.run_command( -+ ['ipa', 'dnszone-del', zone], raiseonerr=False -+ ) -+ super(TestDNSAcceptance, cls).uninstall(mh) -+ -+ # ========================================================================= -+ # DNS Zone Tests -+ # ========================================================================= -+ -+ def test_dns_zone(self): -+ """Test DNS zone creation, verification, and dig queries. -+ -+ This test covers zone management operations including creating zones -+ with valid and invalid parameters, and verifying zone attributes -+ via IPA and dig. -+ """ -+ tasks.kinit_admin(self.master) -+ -+ # Create a new DNS zone with all SOA parameters -+ tasks.add_dns_zone( -+ self.master, ZONE, -+ admin_email=EMAIL, -+ refresh=REFRESH, -+ retry=RETRY, -+ expire=EXPIRE, -+ minimum=MINIMUM, -+ ttl=TTL -+ ) -+ # Verify the new zone was created and is findable -+ result = tasks.find_dns_zone(self.master, ZONE, all_attrs=True) -+ assert ZONE in result.stdout_text -+ -+ # Verify DNS server returns correct SOA attributes using DNS API -+ self.master.run_command(['ipactl', 'restart']) -+ time.sleep(5) -+ soa = self.resolver.resolve(ZONE, 'SOA')[0] -+ assert self.master.hostname in str(soa.mname) -+ assert EMAIL.replace('@', '.') in str(soa.rname) -+ assert soa.refresh == REFRESH -+ assert soa.retry == RETRY -+ assert soa.expire == EXPIRE -+ assert soa.minimum == MINIMUM -+ -+ # ========================================================================= -+ # A Record Tests -+ # ========================================================================= -+ -+ def test_a_record(self): -+ """Test A record add, verify, and delete operations.""" -+ tasks.kinit_admin(self.master) -+ -+ # Single A record: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, ZONE, 'allll', -+ record_type='a', record_value=[A_RECORD]) -+ ans = self.resolver.resolve(f'allll.{ZONE}', 'A') -+ assert A_RECORD in [r.address for r in ans] -+ -+ tasks.del_dns_record(self.master, ZONE, 'allll', -+ record_type='a', record_value=[A_RECORD]) -+ try: -+ self.resolver.resolve(f'allll.{ZONE}', 'A') -+ raise AssertionError( -+ f"Resolving allll.{ZONE} should have raised NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Record deleted, name no longer exists - expected -+ -+ # Multiple A records: add, verify, delete, verify deleted -+ multi_recs = [MULTI_A_RECORD1, MULTI_A_RECORD2] -+ tasks.add_dns_record(self.master, ZONE, 'aa2', -+ record_type='a', record_value=multi_recs) -+ ans = self.resolver.resolve(f'aa2.{ZONE}', 'A') -+ assert MULTI_A_RECORD1 in [r.address for r in ans] -+ assert MULTI_A_RECORD2 in [r.address for r in ans] -+ -+ tasks.del_dns_record(self.master, ZONE, 'aa2', -+ record_type='a', record_value=multi_recs) -+ try: -+ self.resolver.resolve(f'aa2.{ZONE}', 'A') -+ raise AssertionError( -+ f"Resolving aa2.{ZONE} should have raised NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Records deleted, name no longer exists - expected -+ -+ # ========================================================================= -+ # AAAA Record Tests -+ # ========================================================================= -+ -+ def test_aaaa_record(self): -+ """Test AAAA record add, verify, delete, and invalid values.""" -+ tasks.kinit_admin(self.master) -+ -+ # AAAA record: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, ZONE, 'aaaa', -+ record_type='aaaa', record_value=[AAAA]) -+ ans = self.resolver.resolve(f'aaaa.{ZONE}', 'AAAA') -+ assert AAAA in [r.address for r in ans] -+ -+ tasks.del_dns_record(self.master, ZONE, 'aaaa', -+ record_type='aaaa', record_value=[AAAA]) -+ try: -+ self.resolver.resolve(f'aaaa.{ZONE}', 'AAAA') -+ raise AssertionError( -+ f"Resolving aaaa.{ZONE} should have raised NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Record deleted, name no longer exists - expected -+ -+ # Invalid AAAA record should fail and not be created -+ result = tasks.add_dns_record(self.master, ZONE, 'aaaab', -+ record_type='aaaa', -+ record_value=[AAAA_BAD1], -+ raiseonerr=False) -+ assert result.returncode != 0 -+ try: -+ self.resolver.resolve(f'aaaab.{ZONE}', 'AAAA') -+ raise AssertionError( -+ f"Resolving aaaab.{ZONE} should have raised NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Record was never created - expected -+ -+ # ========================================================================= -+ # AFSDB Record Tests -+ # ========================================================================= -+ -+ def test_afsdb_record(self): -+ """Test AFSDB record add, verify, and delete operations.""" -+ tasks.kinit_admin(self.master) -+ -+ # AFSDB record: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, ZONE, 'afsdb', -+ record_type='afsdb', -+ record_value=[f'0 {AFSDB}']) -+ ans = self.resolver.resolve(f'afsdb.{ZONE}', 'AFSDB') -+ assert AFSDB in [str(r.hostname) for r in ans] -+ -+ tasks.del_dns_record(self.master, ZONE, 'afsdb', -+ record_type='afsdb', -+ record_value=[f'0 {AFSDB}']) -+ try: -+ self.resolver.resolve(f'afsdb.{ZONE}', 'AFSDB') -+ raise AssertionError( -+ f"Resolving afsdb.{ZONE} should have raised NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Record deleted, name no longer exists - expected -+ -+ # ========================================================================= -+ # CNAME Record Tests -+ # ========================================================================= -+ -+ def test_cname_record(self): -+ """Test CNAME record add, verify, delete, and duplicate (bz915807).""" -+ tasks.kinit_admin(self.master) -+ -+ # CNAME record: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, ZONE, 'cname', -+ record_type='cname', record_value=[CNAME]) -+ ans = self.resolver.resolve(f'cname.{ZONE}', 'CNAME') -+ assert CNAME in [str(r.target) for r in ans] -+ -+ # Duplicate CNAME should fail and not be created (bz915807) -+ result = tasks.add_dns_record(self.master, ZONE, 'cname', -+ record_type='cname', -+ record_value=['a.b.c'], raiseonerr=False) -+ assert result.returncode != 0 -+ ans = self.resolver.resolve(f'cname.{ZONE}', 'CNAME') -+ assert 'a.b.c' not in [str(r.target) for r in ans] -+ -+ tasks.del_dns_record(self.master, ZONE, 'cname', -+ record_type='cname', record_value=[CNAME]) -+ try: -+ self.resolver.resolve(f'cname.{ZONE}', 'CNAME') -+ raise AssertionError( -+ f"Resolving cname.{ZONE} should have raised NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Record deleted, name no longer exists - expected -+ -+ # ========================================================================= -+ # TXT Record Tests -+ # ========================================================================= -+ -+ def test_txt_record(self): -+ """Test TXT record add, verify, and delete operations.""" -+ tasks.kinit_admin(self.master) -+ -+ # TXT record: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, ZONE, 'txt', -+ record_type='txt', record_value=[TXT]) -+ ans = self.resolver.resolve(f'txt.{ZONE}', 'TXT') -+ assert any(TXT in str(r) for r in ans) -+ -+ tasks.del_dns_record(self.master, ZONE, 'txt', -+ record_type='txt', record_value=[TXT]) -+ try: -+ self.resolver.resolve(f'txt.{ZONE}', 'TXT') -+ raise AssertionError( -+ f"Resolving txt.{ZONE} should have raised NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Record deleted, name no longer exists - expected -+ -+ # ========================================================================= -+ # SRV Record Tests -+ # ========================================================================= -+ -+ def test_srv_record(self): -+ """Test SRV record add, verify, and delete operations.""" -+ tasks.kinit_admin(self.master) -+ -+ # SRV record: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, ZONE, '_srv', -+ record_type='srv', -+ record_value=[f'{SRV_A} {SRV}']) -+ ans = self.resolver.resolve(f'_srv.{ZONE}', 'SRV') -+ assert SRV in [str(r.target) for r in ans] -+ -+ tasks.del_dns_record(self.master, ZONE, '_srv', del_all=True) -+ try: -+ self.resolver.resolve(f'_srv.{ZONE}', 'SRV') -+ raise AssertionError( -+ f"Resolving _srv.{ZONE} should have raised NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Record deleted, name no longer exists - expected -+ -+ # ========================================================================= -+ # MX Record Tests -+ # ========================================================================= -+ -+ def test_mx_record(self): -+ """Test MX record add, verify, and delete operations.""" -+ tasks.kinit_admin(self.master) -+ -+ # MX record: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, ZONE, '@', -+ record_type='mx', -+ record_value=[f'10 {self.MX}.']) -+ ans = self.resolver.resolve(ZONE, 'MX') -+ assert f'{self.MX}.' in [str(r.exchange) for r in ans] -+ -+ tasks.del_dns_record(self.master, ZONE, '@', -+ record_type='mx', -+ record_value=[f'10 {self.MX}.']) -+ try: -+ self.resolver.resolve(ZONE, 'MX') -+ raise AssertionError( -+ f"Resolving MX for {ZONE} should have raised NoAnswer") -+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): -+ pass # Record deleted or no MX records left - expected -+ -+ # ========================================================================= -+ # PTR Zone Tests -+ # ========================================================================= -+ def test_ptr_zone(self): -+ """Test PTR zone creation and verification. -+ -+ This test covers PTR zone management including creating zone -+ with all SOA parameters, and verifying attributes via IPA and dig. -+ """ -+ tasks.kinit_admin(self.master) -+ # Clean up if zone exists -+ tasks.del_dns_zone(self.master, PTR_ZONE) -+ -+ # Create PTR zone with all SOA parameters -+ tasks.add_dns_zone( -+ self.master, PTR_ZONE, -+ skip_overlap_check=True, -+ admin_email=PTR_EMAIL, -+ refresh=PTR_REFRESH, -+ retry=PTR_RETRY, -+ expire=PTR_EXPIRE, -+ minimum=PTR_MINIMUM, -+ ttl=PTR_TTL -+ ) -+ -+ # Verify PTR zone gets created with the correct attributes -+ result = tasks.find_dns_zone( -+ self.master, PTR_ZONE, all_attrs=True) -+ assert PTR_ZONE in result.stdout_text -+ -+ # Verify PTR zone SOA attributes using DNS resolver -+ soa = self.resolver.resolve(PTR_ZONE, 'SOA')[0] -+ assert self.master.hostname in str(soa.mname) -+ assert PTR_EMAIL.replace('@', '.') in str(soa.rname) -+ assert soa.refresh == PTR_REFRESH -+ assert soa.retry == PTR_RETRY -+ assert soa.expire == PTR_EXPIRE -+ assert soa.minimum == PTR_MINIMUM -+ -+ # ========================================================================= -+ # PTR Record Tests -+ # ========================================================================= -+ -+ def test_ptr_record(self): -+ """Test PTR record add, verify, and delete operations.""" -+ tasks.kinit_admin(self.master) -+ -+ # PTR record: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, PTR_ZONE, PTR, -+ record_type='ptr', record_value=[PTR_VALUE]) -+ ans = self.resolver.resolve(f'{PTR}.{PTR_ZONE}', 'PTR') -+ assert PTR_VALUE in [str(r.target) for r in ans] -+ -+ tasks.del_dns_record(self.master, PTR_ZONE, PTR, -+ record_type='ptr', record_value=[PTR_VALUE]) -+ try: -+ self.resolver.resolve(f'{PTR}.{PTR_ZONE}', 'PTR') -+ raise AssertionError( -+ f"Resolving {PTR}.{PTR_ZONE} should have raised " -+ "NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Record deleted, name no longer exists - expected -+ -+ # ========================================================================= -+ # NAPTR Record Tests -+ # ========================================================================= -+ -+ def test_naptr_record(self): -+ """Test NAPTR record add, verify, and delete operations.""" -+ tasks.kinit_admin(self.master) -+ -+ # NAPTR record: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, ZONE, 'naptr', -+ record_type='naptr', record_value=[NAPTR]) -+ ans = self.resolver.resolve(f'naptr.{ZONE}', 'NAPTR') -+ assert any(NAPTR_FIND in str(r.regexp) for r in ans) -+ -+ tasks.del_dns_record(self.master, ZONE, 'naptr', -+ record_type='naptr', record_value=[NAPTR]) -+ try: -+ self.resolver.resolve(f'naptr.{ZONE}', 'NAPTR') -+ raise AssertionError( -+ f"Resolving naptr.{ZONE} should have raised NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Record deleted, name no longer exists - expected -+ -+ # ========================================================================= -+ # DNAME Record Tests -+ # ========================================================================= -+ -+ def test_dname_record(self): -+ """Test DNAME record add, verify, delete, and underscore (bz915797).""" -+ tasks.kinit_admin(self.master) -+ -+ # DNAME record: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, ZONE, 'dname', -+ record_type='dname', record_value=[DNAME]) -+ ans = self.resolver.resolve(f'dname.{ZONE}', 'DNAME') -+ assert DNAME in [str(r.target) for r in ans] -+ -+ # Duplicate DNAME should fail (bz915797) -+ result = tasks.add_dns_record(self.master, ZONE, 'dname', -+ record_type='dname', -+ record_value=[DNAME2], -+ raiseonerr=False) -+ assert result.returncode != 0 -+ -+ tasks.del_dns_record(self.master, ZONE, 'dname', -+ record_type='dname', record_value=[DNAME]) -+ try: -+ self.resolver.resolve(f'dname.{ZONE}', 'DNAME') -+ raise AssertionError( -+ f"Resolving dname.{ZONE} should have raised NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Record deleted - expected -+ -+ # DNAME with underscore: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, ZONE, 'dname', -+ record_type='dname', record_value=[DNAME2]) -+ ans = self.resolver.resolve(f'dname.{ZONE}', 'DNAME') -+ assert DNAME2 in [str(r.target) for r in ans] -+ -+ tasks.del_dns_record(self.master, ZONE, 'dname', -+ record_type='dname', record_value=[DNAME2]) -+ try: -+ self.resolver.resolve(f'dname.{ZONE}', 'DNAME') -+ raise AssertionError( -+ f"Resolving dname.{ZONE} should have raised NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Record deleted - expected -+ -+ # ========================================================================= -+ # CERT Record Tests -+ # ========================================================================= -+ -+ def test_cert_record(self): -+ """Test CERT record add, verify, and delete operations.""" -+ tasks.kinit_admin(self.master) -+ -+ # CERT record: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, ZONE, 'cert', -+ record_type='cert', -+ record_value=[f'{CERT_B} {CERT}']) -+ ans = self.resolver.resolve(f'cert.{ZONE}', 'CERT') -+ assert any(CERT in str(r) for r in ans) -+ -+ tasks.del_dns_record(self.master, ZONE, 'cert', -+ record_type='cert', -+ record_value=[f'{CERT_B} {CERT}']) -+ try: -+ self.resolver.resolve(f'cert.{ZONE}', 'CERT') -+ raise AssertionError( -+ f"Resolving cert.{ZONE} should have raised NXDOMAIN") -+ except dns.resolver.NXDOMAIN: -+ pass # Record deleted - expected -+ -+ # ========================================================================= -+ # LOC Record Tests -+ # ========================================================================= -+ -+ def test_loc_record(self): -+ """Test LOC record add, verify, and delete operations.""" -+ tasks.kinit_admin(self.master) -+ -+ # LOC record: add, verify, delete, verify deleted -+ tasks.add_dns_record(self.master, ZONE, '@', -+ record_type='loc', record_value=[LOC]) -+ ans = self.resolver.resolve(ZONE, 'LOC') -+ assert any(LOC in str(r) for r in ans) -+ -+ tasks.del_dns_record(self.master, ZONE, '@', -+ record_type='loc', record_value=[LOC]) -+ try: -+ self.resolver.resolve(ZONE, 'LOC') -+ raise AssertionError( -+ f"Resolving LOC for {ZONE} should have raised NoAnswer") -+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): -+ pass # Record deleted - expected -+ -+ # ========================================================================= -+ # KX Record Tests -+ # ========================================================================= -+ -+ def test_kx_record(self): -+ """Test KX record add, verify, and delete operations.""" -+ tasks.kinit_admin(self.master) -+ -+ # KX record: add, verify, delete, verify deleted -+ kx_val = f'{KX_PREF1} {self.A_HOST}' -+ tasks.add_dns_record(self.master, ZONE, '@', -+ record_type='kx', record_value=[kx_val]) -+ ans = self.resolver.resolve(ZONE, 'KX') -+ assert int(KX_PREF1) in [r.preference for r in ans] -+ -+ tasks.del_dns_record(self.master, ZONE, '@', -+ record_type='kx', record_value=[kx_val]) -+ try: -+ self.resolver.resolve(ZONE, 'KX') -+ raise AssertionError( -+ f"Resolving KX for {ZONE} should have raised NoAnswer") -+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): -+ pass # Record deleted or no KX records left - expected -+ -+ # Invalid KX records should fail and not be created -+ bad_vals = [ -+ (KX_BAD_PREF1, A_RECORD), -+ (KX_BAD_PREF2, ZONE) -+ ] -+ for bad_pref, bad_target in bad_vals: -+ result = tasks.add_dns_record( -+ self.master, ZONE, '@', record_type='kx', -+ record_value=[f'{bad_pref} {bad_target}'], raiseonerr=False) -+ assert result.returncode != 0 -+ try: -+ self.resolver.resolve(ZONE, 'KX') -+ raise AssertionError( -+ f"Resolving KX for {ZONE} should have raised " -+ "NoAnswer") -+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): -+ pass # No KX records - expected -+ -+ # ========================================================================= -+ # Zone Permission Tests -+ # ========================================================================= -+ -+ def test_zone_permission(self): -+ """Test DNS zone permission add, verify, and remove operations. -+ -+ This test covers zone permission management including adding -+ permission, verifying managedby attribute, removing permission, -+ testing duplicate add, testing non-existent zone, and verifying -+ permission cleanup on zone deletion. -+ """ -+ tasks.kinit_admin(self.master) -+ # Clean up if zone exists -+ tasks.del_dns_zone(self.master, self.MANAGED_ZONE) -+ -+ # Add zone, then permission to manage it -+ tasks.add_dns_zone( -+ self.master, self.MANAGED_ZONE, admin_email=EMAIL) -+ result = tasks.add_dns_zone_permission(self.master, self.MANAGED_ZONE) -+ assert result.returncode == 0 -+ -+ # Verify managedby attribute is set -+ result = tasks.show_dns_zone( -+ self.master, self.MANAGED_ZONE, all_attrs=True) -+ assert 'managedby' in result.stdout_text.lower() -+ -+ # Verify permission is added -+ perm_name = f'Manage DNS zone {self.MANAGED_ZONE}' -+ result = tasks.find_permission(self.master, permission=perm_name) -+ assert result.returncode == 0 -+ -+ # Remove permission to manage zone -+ result = tasks.remove_dns_zone_permission( -+ self.master, self.MANAGED_ZONE) -+ assert result.returncode == 0 -+ -+ # Verify managedby attribute is not available -+ result = tasks.show_dns_zone( -+ self.master, self.MANAGED_ZONE, all_attrs=True) -+ assert 'managedby' not in result.stdout_text.lower() -+ -+ # Verify permission is removed -+ result = tasks.find_permission( -+ self.master, permission=perm_name, raiseonerr=False) -+ assert result.returncode != 0 -+ -+ # Add zone with permission, delete zone, verify permission deleted -+ tasks.del_dns_zone(self.master, self.MANAGED_ZONE1) -+ tasks.add_dns_zone( -+ self.master, self.MANAGED_ZONE1, admin_email=EMAIL) -+ tasks.add_dns_zone_permission(self.master, self.MANAGED_ZONE1) -+ tasks.del_dns_zone(self.master, self.MANAGED_ZONE1) -+ perm_name1 = f'Manage DNS zone {self.MANAGED_ZONE1}' -+ result = tasks.find_permission( -+ self.master, permission=perm_name1, raiseonerr=False) -+ assert result.returncode != 0 -+ -+ # Add duplicate permission to manage zone -+ tasks.add_dns_zone_permission( -+ self.master, self.MANAGED_ZONE, raiseonerr=False) -+ result = tasks.add_dns_zone_permission( -+ self.master, self.MANAGED_ZONE, raiseonerr=False) -+ assert result.returncode != 0 -+ assert 'already exists' in result.stderr_text -+ -+ # Add permission to manage non-existent zone -+ result = tasks.add_dns_zone_permission( -+ self.master, self.NONEXISTENT_ZONE, raiseonerr=False) -+ assert result.returncode != 0 -+ assert 'DNS zone not found' in result.stderr_text -+ -+ # Remove permission to manage zone again (should fail) -+ tasks.remove_dns_zone_permission( -+ self.master, self.MANAGED_ZONE, raiseonerr=False) -+ result = tasks.remove_dns_zone_permission( -+ self.master, self.MANAGED_ZONE, raiseonerr=False) -+ assert result.returncode != 0 -+ assert 'permission not found' in result.stderr_text -+ -+ # Remove permission for non-existent zone -+ result = tasks.remove_dns_zone_permission(self.master, -+ self.NONEXISTENT_ZONE, -+ raiseonerr=False) -+ assert result.returncode != 0 -+ assert 'DNS zone not found' in result.stderr_text -+ -+ # Cleanup zones from zone permission tests -+ tasks.del_dns_zone(self.master, self.MANAGED_ZONE) -+ -+ # ========================================================================= -+ # Persistent Search Tests -+ # ========================================================================= -+ -+ def test_psearch(self): -+ """Test persistent search and zone serial updates.""" -+ tasks.kinit_admin(self.master) -+ -+ # Verify psearch is not used when IPA server is installed -+ result = self.master.run_command([ -+ 'grep', 'psearch yes', '/etc/named.conf' -+ ], raiseonerr=False) -+ assert result.returncode != 0 -+ -+ # Create zone with SOA parameters -+ tasks.add_dns_zone( -+ self.master, self.ZONE_PSEARCH, admin_email=EMAIL, -+ refresh=REFRESH, retry=RETRY, expire=EXPIRE, -+ minimum=MINIMUM, ttl=TTL) -+ -+ # Verify zone SOA exists -+ ans = self.resolver.resolve(self.ZONE_PSEARCH, 'SOA') -+ assert len(ans) > 0 -+ -+ # Add TXT record and verify -+ tasks.add_dns_record(self.master, self.ZONE_PSEARCH, 'txt', -+ record_type='txt', record_value=[TXT]) -+ ans = self.resolver.resolve(f'txt.{self.ZONE_PSEARCH}', 'TXT') -+ assert any(TXT in str(r) for r in ans) -+ -+ # Update TXT record and verify -+ tasks.mod_dns_record(self.master, self.ZONE_PSEARCH, 'txt', -+ record_type='txt', old_value=TXT, -+ new_value=NEW_TXT) -+ ans = self.resolver.resolve(f'txt.{self.ZONE_PSEARCH}', 'TXT') -+ assert any(NEW_TXT in str(r) for r in ans) -+ -+ # Get old serial -+ ans = self.resolver.resolve(self.ZONE_PSEARCH, 'SOA') -+ old_serial = ans[0].serial -+ -+ # Update TXT record again -+ tasks.mod_dns_record(self.master, self.ZONE_PSEARCH, 'txt', -+ record_type='txt', old_value=NEW_TXT, -+ new_value=NEWER_TXT) -+ -+ # Verify serial increased -+ ans = self.resolver.resolve(self.ZONE_PSEARCH, 'SOA') -+ new_serial = ans[0].serial -+ assert new_serial > old_serial, ( -+ f"New serial ({new_serial}) should be higher " -+ f"than old ({old_serial})") -diff --git a/ipatests/test_xmlrpc/test_dns_plugin.py b/ipatests/test_xmlrpc/test_dns_plugin.py -index 864d5287f8317a5154aec4c792f56deab7ff0120..bff4b40aef6e5adec21c8929719e99669b80cdf0 100644 ---- a/ipatests/test_xmlrpc/test_dns_plugin.py -+++ b/ipatests/test_xmlrpc/test_dns_plugin.py -@@ -1088,7 +1088,7 @@ class test_dns(Declarative): - - - dict( -- desc='Create record %r in zone %r' % (zone1, name1), -+ desc='Create single A record %r in zone %r' % (name1, zone1), - command=('dnsrecord_add', [zone1, name1], {'arecord': arec2}), - expected={ - 'value': name1_dnsname, -@@ -1132,8 +1132,20 @@ class test_dns(Declarative): - - - dict( -- desc='Add A record to %r in zone %r' % (name1, zone1), -- command=('dnsrecord_add', [zone1, name1], {'arecord': arec3}), -+ desc='Delete single A record from %r in zone %r' % (name1, zone1), -+ command=('dnsrecord_del', [zone1, name1], {'arecord': arec2}), -+ expected={ -+ 'value': [name1_dnsname], -+ 'summary': u'Deleted record "%s"' % name1, -+ 'result': {'failed': []}, -+ }, -+ ), -+ -+ -+ dict( -+ desc='Add multiple A records to %r in zone %r' % (name1, zone1), -+ command=('dnsrecord_add', [zone1, name1], -+ {'arecord': [arec2, arec3]}), - expected={ - 'value': name1_dnsname, - 'summary': None, -@@ -1148,14 +1160,29 @@ class test_dns(Declarative): - - - dict( -- desc='Remove A record from %r in zone %r' % (name1, zone1), -- command=('dnsrecord_del', [zone1, name1], {'arecord': arec2}), -+ desc='Delete multiple A records from %r in zone %r' % ( -+ name1, zone1), -+ command=('dnsrecord_del', [zone1, name1], -+ {'arecord': [arec2, arec3]}), - expected={ - 'value': [name1_dnsname], -+ 'summary': u'Deleted record "%s"' % name1, -+ 'result': {'failed': []}, -+ }, -+ ), -+ -+ -+ dict( -+ desc='Re-add A record %r for subsequent tests' % arec3, -+ command=('dnsrecord_add', [zone1, name1], {'arecord': arec3}), -+ expected={ -+ 'value': name1_dnsname, - 'summary': None, - 'result': { -+ 'dn': name1_dn, - 'idnsname': [name1_dnsname], - 'arecord': [arec3], -+ 'objectclass': objectclasses.dnsrecord, - }, - }, - ), -@@ -1228,6 +1255,50 @@ class test_dns(Declarative): - }, - ), - -+ -+ dict( -+ desc='Try to add invalid AAAA record to %r in zone %r' % ( -+ name1, zone1), -+ command=('dnsrecord_add', [zone1, name1], -+ {'aaaarecord': u'invalid:ipv6:addr'}), -+ expected=errors.ValidationError( -+ name='ip_address', -+ error=u'invalid IP address format'), -+ ), -+ -+ -+ dict( -+ desc='Add AAAA record to %r in zone %r using dnsrecord_add' % ( -+ name1, zone1), -+ command=('dnsrecord_add', [zone1, name1], {'aaaarecord': aaaarec1}), -+ expected={ -+ 'value': name1_dnsname, -+ 'summary': None, -+ 'result': { -+ 'dn': name1_dn, -+ 'idnsname': [name1_dnsname], -+ 'arecord': [arec3], -+ 'aaaarecord': [aaaarec1], -+ 'objectclass': objectclasses.dnsrecord, -+ }, -+ }, -+ ), -+ -+ -+ dict( -+ desc='Delete AAAA record from %r in zone %r using dnsrecord_del' % ( -+ name1, zone1), -+ command=('dnsrecord_del', [zone1, name1], {'aaaarecord': aaaarec1}), -+ expected={ -+ 'value': [name1_dnsname], -+ 'summary': None, -+ 'result': { -+ 'idnsname': [name1_dnsname], -+ 'arecord': [arec3], -+ }, -+ }, -+ ), -+ - dict( - desc='Try to add invalid MX record to zone %r using dnsrecord_add' % (zone1), - command=('dnsrecord_add', [zone1, u'@'], {'mxrecord': zone1_ns }), -@@ -1350,6 +1421,37 @@ class test_dns(Declarative): - }, - ), - -+ -+ dict( -+ desc='Add NAPTR record to zone %r using dnsrecord_add' % (zone1), -+ command=('dnsrecord_add', [zone1, u'_naptr'], -+ {'naptrrecord': u'100 10 "U" "E2U+sip" "" _sip._udp'}), -+ expected={ -+ 'value': DNSName(u'_naptr'), -+ 'summary': None, -+ 'result': { -+ 'objectclass': objectclasses.dnsrecord, -+ 'dn': DN(('idnsname', '_naptr'), zone1_dn), -+ 'idnsname': [DNSName(u'_naptr')], -+ 'naptrrecord': [u'100 10 "U" "E2U+sip" "" _sip._udp'], -+ }, -+ }, -+ ), -+ -+ -+ dict( -+ desc='Delete NAPTR record from zone %r using dnsrecord_del' % ( -+ zone1), -+ command=('dnsrecord_del', [zone1, u'_naptr'], -+ {'naptrrecord': u'100 10 "U" "E2U+sip" "" _sip._udp'}), -+ expected={ -+ 'value': [DNSName(u'_naptr')], -+ 'summary': u'Deleted record "%s"' % u'_naptr', -+ 'result': {'failed': []}, -+ }, -+ ), -+ -+ - dict( - desc='Try to add CNAME record to %r using dnsrecord_add' % (name1), - command=('dnsrecord_add', [zone1, name1], {'cnamerecord': absnxname}), -@@ -1456,6 +1558,66 @@ class test_dns(Declarative): - '(RFC 2181, section 6.1)'), - ), - -+ -+ dict( -+ desc='Add DNAME record with underscore to zone %r' % (zone1), -+ command=('dnsrecord_add', [zone1, u'bar_underscore'], -+ {'dnamerecord': absnxname}), -+ expected={ -+ 'value': DNSName(u'bar_underscore'), -+ 'summary': None, -+ 'result': { -+ 'objectclass': objectclasses.dnsrecord, -+ 'dn': DN(('idnsname', 'bar_underscore'), zone1_dn), -+ 'idnsname': [DNSName(u'bar_underscore')], -+ 'dnamerecord': [absnxname], -+ }, -+ }, -+ ), -+ -+ -+ dict( -+ desc='Delete DNAME record with underscore from zone %r' % (zone1), -+ command=('dnsrecord_del', [zone1, u'bar_underscore'], -+ {'dnamerecord': absnxname}), -+ expected={ -+ 'value': [DNSName(u'bar_underscore')], -+ 'summary': u'Deleted record "%s"' % u'bar_underscore', -+ 'result': {'failed': []}, -+ }, -+ ), -+ -+ -+ dict( -+ desc='Add CERT record to zone %r using dnsrecord_add' % (zone1), -+ command=('dnsrecord_add', [zone1, u'_cert'], -+ {'certrecord': u'1 1 1 F835EDA21E94B565716F'}), -+ expected={ -+ 'value': DNSName(u'_cert'), -+ 'summary': None, -+ 'result': { -+ 'objectclass': objectclasses.dnsrecord, -+ 'dn': DN(('idnsname', '_cert'), zone1_dn), -+ 'idnsname': [DNSName(u'_cert')], -+ 'certrecord': [u'1 1 1 F835EDA21E94B565716F'], -+ }, -+ }, -+ ), -+ -+ -+ dict( -+ desc='Delete CERT record from zone %r using dnsrecord_del' % ( -+ zone1), -+ command=('dnsrecord_del', [zone1, u'_cert'], -+ {'certrecord': u'1 1 1 F835EDA21E94B565716F'}), -+ expected={ -+ 'value': [DNSName(u'_cert')], -+ 'summary': u'Deleted record "%s"' % u'_cert', -+ 'result': {'failed': []}, -+ }, -+ ), -+ -+ - dict( - desc='Add NS+DNAME record to %r zone record using dnsrecord_add' % (zone2), - command=('dnsrecord_add', [zone2, u'@'], -@@ -1527,6 +1689,22 @@ class test_dns(Declarative): - ), - - -+ dict( -+ desc='Delete TXT record from %r using dnsrecord_del' % (name1), -+ command=('dnsrecord_del', [zone1, name1], -+ {'txtrecord': u'foo bar'}), -+ expected={ -+ 'value': [name1_dnsname], -+ 'summary': None, -+ 'result': { -+ 'idnsname': [name1_dnsname], -+ 'arecord': [arec3], -+ 'kxrecord': [u'1 foo-1'], -+ }, -+ }, -+ ), -+ -+ - dict( - desc='Try to add unresolvable absolute NS record to %r using dnsrecord_add' % (name_ns), - command=( -@@ -1879,6 +2057,38 @@ class test_dns(Declarative): - }, - ), - -+ -+ dict( -+ desc='Delete PTR record %r from %r using dnsrecord_del' % ( -+ revname1, revzone1), -+ command=('dnsrecord_del', [revzone1, revname1], -+ {'ptrrecord': absnxname}), -+ expected={ -+ 'value': [revname1_dnsname], -+ 'summary': u'Deleted record "%s"' % revname1, -+ 'result': {'failed': []}, -+ }, -+ ), -+ -+ -+ dict( -+ desc='Re-add PTR record %r to %r for subsequent tests' % ( -+ revname1, revzone1), -+ command=('dnsrecord_add', [revzone1, revname1], -+ {'ptrrecord': absnxname}), -+ expected={ -+ 'value': revname1_dnsname, -+ 'summary': None, -+ 'result': { -+ 'objectclass': objectclasses.dnsrecord, -+ 'dn': revname1_dn, -+ 'idnsname': [revname1_dnsname], -+ 'ptrrecord': [absnxname], -+ }, -+ }, -+ ), -+ -+ - dict( - desc='Update global DNS settings', - command=('dnsconfig_mod', [], {'idnsforwarders' : [fwd_ip],}), -@@ -3114,6 +3324,19 @@ class test_dns(Declarative): - ), - - -+ dict( -+ desc='Delete AFSDB record from %r in zone %r' % ( -+ dnsafsdbres1, idnzone1), -+ command=('dnsrecord_del', [idnzone1, dnsafsdbres1], -+ {'afsdbrecord': u'0 ' + idnzone1_mname}), -+ expected={ -+ 'value': [dnsafsdbres1_dnsname], -+ 'summary': u'Deleted record "%s"' % dnsafsdbres1, -+ 'result': {'failed': []}, -+ }, -+ ), -+ -+ - dict( - desc='Add A denormalized record in zone %r' % (idnzone1), - command=('dnsrecord_add', [idnzone1, u'gro\xdf'], {'arecord': u'172.16.0.1'}), -@@ -6427,6 +6650,61 @@ class test_dns_soa(Declarative): - u"A/AAAA record" % - zone6_unresolvable_ns_dnsname,), - ), -+ -+ dict( -+ desc='Adding a zone - %r - with invalid SOA refresh value' % zone6, -+ command=( -+ 'dnszone_add', [zone6], { -+ 'idnssoarefresh': 12345678901234, -+ }), -+ expected=errors.ValidationError( -+ name='refresh', -+ error=u'can be at most 2147483647'), -+ ), -+ -+ dict( -+ desc='Adding a zone - %r - with invalid SOA retry value' % zone6, -+ command=( -+ 'dnszone_add', [zone6], { -+ 'idnssoaretry': 12345678901234, -+ }), -+ expected=errors.ValidationError( -+ name='retry', -+ error=u'can be at most 2147483647'), -+ ), -+ -+ dict( -+ desc='Adding a zone - %r - with invalid SOA expire value' % zone6, -+ command=( -+ 'dnszone_add', [zone6], { -+ 'idnssoaexpire': 12345678901234, -+ }), -+ expected=errors.ValidationError( -+ name='expire', -+ error=u'can be at most 2147483647'), -+ ), -+ -+ dict( -+ desc='Adding a zone - %r - with invalid SOA minimum value' % zone6, -+ command=( -+ 'dnszone_add', [zone6], { -+ 'idnssoaminimum': 12345678901234, -+ }), -+ expected=errors.ValidationError( -+ name='minimum', -+ error=u'can be at most 2147483647'), -+ ), -+ -+ dict( -+ desc='Adding a zone - %r - with invalid TTL value' % zone6, -+ command=( -+ 'dnszone_add', [zone6], { -+ 'dnsttl': 12345678901234, -+ }), -+ expected=errors.ValidationError( -+ name='ttl', -+ error=u'can be at most 2147483647'), -+ ), - ] - - --- -2.52.0 - diff --git a/0010-ipa-advise-smart-card-client-script-does-not-need-kr.patch b/0010-ipa-advise-smart-card-client-script-does-not-need-kr.patch deleted file mode 100644 index 06dff82..0000000 --- a/0010-ipa-advise-smart-card-client-script-does-not-need-kr.patch +++ /dev/null @@ -1,99 +0,0 @@ -From 5756ed2af940378c16d9d52e083b8c4005d41a13 Mon Sep 17 00:00:00 2001 -From: Florence Blanc-Renaud -Date: Wed, 21 Jan 2026 17:19:18 +0100 -Subject: [PATCH] ipa-advise: smart card client script does not need krb ticket - -The script generated by ipa-advise config-client-for-smart-card-auth -currently requires a kerberos ticket because it calls ipa-certupdate. - -Since IPA 4.9.0 and commit 1a09ce9, ipa-certupdate can be called -without a ticket. Update the script so that it detects if it gets -executed on a client recent enough to skip that requirement. - -Update the test for config-client-for-smart-card-auth, do not -call kinit admin on the client. - -Fixes: https://pagure.io/freeipa/issue/9923 -Signed-off-by: Florence Blanc-Renaud -Reviewed-By: Rob Crittenden ---- - ipaserver/advise/plugins/smart_card_auth.py | 22 ++++++++++++++++++++- - ipatests/test_integration/test_advise.py | 10 +++++++--- - 2 files changed, 28 insertions(+), 4 deletions(-) - -diff --git a/ipaserver/advise/plugins/smart_card_auth.py b/ipaserver/advise/plugins/smart_card_auth.py -index b79797dcaee0c881d3ef752a268ed520d96b433b..a0e50e9806f7843d2981141d8941d5e37f53c0cd 100644 ---- a/ipaserver/advise/plugins/smart_card_auth.py -+++ b/ipaserver/advise/plugins/smart_card_auth.py -@@ -34,6 +34,26 @@ class common_smart_card_auth_config(Advice): - 'Use kinit as privileged user to obtain Kerberos credentials' - ]) - -+ def check_ccache_not_empty_if_old_version(self): -+ self.log.comment("On version before IPA 4.9, " -+ "check that the credential cache is not empty") -+ self.log.command( -+ "python3 -c \"from ipapython.version import VERSION;" -+ "from ipaplatform.tasks import tasks;" -+ "exit(tasks.parse_ipa_version(VERSION) >= " -+ "tasks.parse_ipa_version('4.9.0'))\"") -+ with self.log.if_branch('[ "$?" -eq "0" ]'): -+ self.log.exit_on_failed_command( -+ 'klist', -+ [ -+ "Credential cache is empty", -+ 'Use kinit as privileged user to obtain Kerberos ' -+ 'credentials' -+ ]) -+ with self.log.else_branch(): -+ self.log.command( -+ "echo 'Version 4.9.0+ does not require Kerberos credentials'") -+ - def check_and_set_ca_cert_paths(self): - ca_paths_variable = self.smart_card_ca_certs_variable_name - single_ca_path_variable = self.single_ca_cert_variable_name -@@ -260,7 +280,7 @@ class config_client_for_smart_card_auth(common_smart_card_auth_config): - def get_info(self): - self.log.exit_on_nonroot_euid() - self.check_and_set_ca_cert_paths() -- self.check_ccache_not_empty() -+ self.check_ccache_not_empty_if_old_version() - self.check_and_remove_pam_pkcs11() - self.install_opensc_and_dconf_packages() - self.install_krb5_client_dependencies() -diff --git a/ipatests/test_integration/test_advise.py b/ipatests/test_integration/test_advise.py -index 3d5cadee319ebba14ebc43ebb1dc90a502e5d3b8..a336634ae9627133c5ad4dea4b1c43ffd726df10 100644 ---- a/ipatests/test_integration/test_advise.py -+++ b/ipatests/test_integration/test_advise.py -@@ -60,13 +60,17 @@ class TestAdvice(IntegrationTest): - ) - tasks.install_client(cls.master, cls.clients[0]) - -- def execute_advise(self, host, advice_id, *args): -+ def execute_advise(self, host, advice_id, *args, kinit=True): - # ipa-advise script is only available on a server - tasks.kinit_admin(self.master) - advice = self.master.run_command(['ipa-advise', advice_id]) - # execute script on host (client or master) - if host is not self.master: -- tasks.kinit_admin(host) -+ if kinit: -+ tasks.kinit_admin(host) -+ else: -+ # Make sure we don't have any ticket -+ tasks.kdestroy_all(host) - filename = tasks.upload_temp_contents(host, advice.stdout_text) - cmd = ['sh', filename] - cmd.extend(args) -@@ -181,7 +185,7 @@ class TestAdvice(IntegrationTest): - ca_pem = ExternalCA().create_ca() - ca_file = tasks.upload_temp_contents(client, ca_pem) - try: -- self.execute_advise(client, advice_id, ca_file) -+ self.execute_advise(client, advice_id, ca_file, kinit=False) - finally: - client.run_command(['rm', '-f', ca_file]) - --- -2.52.0 - diff --git a/0011-ipatests-Fix-resolver-state-tracking-in-enforced-DNS.patch b/0011-ipatests-Fix-resolver-state-tracking-in-enforced-DNS.patch deleted file mode 100644 index bc1e9a4..0000000 --- a/0011-ipatests-Fix-resolver-state-tracking-in-enforced-DNS.patch +++ /dev/null @@ -1,177 +0,0 @@ -From fd84cec77d18e0e608285ab712f8211b0b49a7fe Mon Sep 17 00:00:00 2001 -From: PRANAV THUBE -Date: Thu, 18 Dec 2025 17:13:29 +0530 -Subject: [PATCH] ipatests: Fix resolver state tracking in enforced DNS policy - tests. - -Use resolver.setup_resolver() instead of direct file manipulation to -prevent state tracking failures in IDM-CI. Remove redundant manual -resolv.conf changes and fix operation order. - -Related: https://pagure.io/freeipa/issue/9904 -Reviewed-By: Antonio Torres ---- - ipatests/test_integration/test_edns.py | 70 ++++++++++++++++++-------- - 1 file changed, 50 insertions(+), 20 deletions(-) - -diff --git a/ipatests/test_integration/test_edns.py b/ipatests/test_integration/test_edns.py -index d02ce45a1f9c0b04332e4ff4a055ab4c61b35eb4..54b7859da1b9fa320c07bdf29f79d2fdb292c337 100644 ---- a/ipatests/test_integration/test_edns.py -+++ b/ipatests/test_integration/test_edns.py -@@ -15,6 +15,9 @@ from ipatests.test_integration.test_dns import TestDNS - from ipatests.pytest_ipa.integration.firewall import Firewall - from ipaplatform.osinfo import osinfo - from ipaplatform.paths import paths -+from ipatests.pytest_ipa.integration.resolver import ( -+ resolver as detect_resolver -+) - - - def apply_enforced_dns_preconfig(host, master_ip, master_host, -@@ -30,6 +33,9 @@ def apply_enforced_dns_preconfig(host, master_ip, master_host, - ca_cert_path: Path to the CA certificate on the master - dest_cert_name: Destination certificate filename in trust anchors - """ -+ # Backup resolver before making any changes (with original resolver type) -+ host.resolver.backup() -+ - # Get the network interface for routing - iface_cmd = host.run_command([ - "bash", "-c", -@@ -43,7 +49,8 @@ def apply_enforced_dns_preconfig(host, master_ip, master_host, - host.put_file_contents(dest_ca_path, ca_cert_data) - host.run_command(["update-ca-trust", "extract"]) - -- if osinfo.id in ['rhel', 'centos']: -+ platform = tasks.get_platform(host) -+ if platform in ['rhel', 'centos']: - # RHEL/CentOS configuration - # Install and configure dnsconfd - tasks.install_packages(host, ['dnsconfd']) -@@ -51,31 +58,60 @@ def apply_enforced_dns_preconfig(host, master_ip, master_host, - host.run_command(["systemctl", "enable", "--now", "dnsconfd"]) - host.run_command(["nmcli", "g", "reload"]) - -- # Configure DNS over TLS via NetworkManager -+ # Configure DNS over TLS via NetworkManager device-specific settings. -+ # Device-specific DNS settings work alongside global resolver config. - host.run_command([ - "nmcli", "device", "modify", iface, - "ipv4.dns", f"dns+tls://{master_ip}" - ]) - -- elif osinfo.id == 'fedora': -- # Fedora configuration -+ elif platform == 'fedora': - # Configure systemd-resolved for DNS over TLS - host.run_command([ -- "ln", "-sf", "../run/systemd/resolve/stub-resolv.conf", -+ "ln", "-sf", "/run/systemd/resolve/stub-resolv.conf", - "/etc/resolv.conf" - ]) -+ # Restart systemd-resolved to ensure DoT settings are fully applied -+ # and resolv.conf is updated - host.run_command(["systemctl", "restart", "systemd-resolved"]) -- -- # Configure DNS over TLS via systemd-resolve -+ # Configure DNS over TLS via systemd-resolve per-interface settings -+ # Per-interface DNS settings work alongside the global resolver config - host.run_command([ - "systemd-resolve", "--set-dns", master_ip, - "--set-dnsovertls=yes", f"--interface={iface}" - ]) - else: - raise ValueError( -- f"Unsupported OS for enforced DNS policy: {osinfo.id}" -+ f"Unsupported OS for enforced DNS policy: {platform}" - ) - -+ # Re-detect resolver since we may have changed the resolver type -+ # (e.g., from PlainFileResolver to ResolvedResolver on Fedora) -+ # This ensures state tracking works correctly with the new resolver type -+ # Store the original resolver so we can restore from it during uninstall -+ host._enforced_dns_original_resolver = host.resolver -+ host.resolver = detect_resolver(host) -+ host.resolver.current_state = host.resolver._get_state() -+ -+ -+def restore_enforced_dns_resolver(host): -+ """ -+ Restore resolver state after enforced DNS preconfig. -+ -+ This restores the original resolver backup created by -+ apply_enforced_dns_preconfig() and syncs host.resolver state. -+ """ -+ if not hasattr(host, '_enforced_dns_original_resolver'): -+ return -+ original_resolver = host._enforced_dns_original_resolver -+ if original_resolver.has_backups(): -+ original_resolver.current_state = original_resolver._get_state() -+ original_resolver.restore() -+ # Sync host.resolver state since original_resolver changed the config -+ host.resolver.backups.clear() -+ host.resolver.current_state = host.resolver._get_state() -+ delattr(host, '_enforced_dns_original_resolver') -+ - - def setup_dns_over_tls_environment(cls): - """ -@@ -451,17 +487,12 @@ class TestDNSOverTLS_EnforcedPolicy_IPA_CA(IntegrationTest): - tasks.install_master(self.master, extra_args=args) - - # Apply pre-configuration on client before installation -+ # This configures the resolver with master IP and domain - apply_enforced_dns_preconfig( - self.clients[0], self.master.ip, self.master, - paths.IPA_CA_CRT, "ca.crt" - ) - -- # Configure client nameserver -- self.clients[0].put_file_contents( -- paths.RESOLV_CONF, -- "nameserver %s" % self.master.ip -- ) -- - # Install client with enforced policy - args = [ - "--dns-over-tls", -@@ -506,6 +537,8 @@ class TestDNSOverTLS_EnforcedPolicy_IPA_CA(IntegrationTest): - """ - This test ensures that all hosts can be uninstalled correctly. - """ -+ for host in [self.clients[0], self.replicas[0]]: -+ restore_enforced_dns_resolver(host) - tasks.uninstall_client(self.clients[0]) - tasks.uninstall_replica(self.master, self.replicas[0]) - tasks.uninstall_master(self.master) -@@ -559,17 +592,12 @@ class TestDNSOverTLS_EnforcedPolicy_External_CA(IntegrationTest): - - # Apply pre-configuration on client before installation - # (includes CA cert setup) -+ # This configures the resolver with master IP and domain - apply_enforced_dns_preconfig( - self.clients[0], self.master.ip, self.master, - cert_dest, "certificate.pem" - ) - -- # Configure client nameserver -- self.clients[0].put_file_contents( -- paths.RESOLV_CONF, -- "nameserver %s" % self.master.ip -- ) -- - # Install client with enforced policy - args = [ - "--dns-over-tls", -@@ -615,6 +643,8 @@ class TestDNSOverTLS_EnforcedPolicy_External_CA(IntegrationTest): - """ - This test ensures that all hosts can be uninstalled correctly. - """ -+ for host in [self.clients[0], self.replicas[0]]: -+ restore_enforced_dns_resolver(host) - tasks.uninstall_client(self.clients[0]) - tasks.uninstall_replica(self.master, self.replicas[0]) - tasks.uninstall_master(self.master) --- -2.52.0 - diff --git a/0012-freeipa.spec.in-Use-systemd-sysusers-to-setup-users-.patch b/0012-freeipa.spec.in-Use-systemd-sysusers-to-setup-users-.patch deleted file mode 100644 index 23b13cf..0000000 --- a/0012-freeipa.spec.in-Use-systemd-sysusers-to-setup-users-.patch +++ /dev/null @@ -1,224 +0,0 @@ -From 0800065ac5555dba102f05c947ca47b5dc9a81af Mon Sep 17 00:00:00 2001 -From: Rafael Guterres Jeffman -Date: Fri, 23 Jan 2026 16:49:31 -0300 -Subject: [PATCH] freeipa.spec.in: Use systemd-sysusers to setup users and - groups - -System accounts for `kdcproxy` and `ipaapi` are now created with -sysusers configuration and macros. User `apache` is updated, by -adding it to group `ipaapi` using sysusers configuration. - -Fixes: https://pagure.io/freeipa/issue/9572 - -AI agent usage info: - -The initial changes were created by Claude by providing the following -context: - ->> Add support for creating users through systemd-sysusers by creating ->> a folder init/sysusersd, similar to init/tmpfilesd, changing install ->> paths in init/sysusersd/Makefile.am, adding configure option ->> --with-systemdsysusersdir similar to --with-systemdtmpfilesdir, and ->> adding a new file init/sysusersd/freeeipo.sysusers.in with the ->> contents: ->> ``` ->> # system accounts for IPA ->> u! kdcproxy - "IPA KDC Proxy Uer" ->> u! ipaapi - "IPA Framework User" ->> # - add Apache HTTPd user to ipaapi group ->> m apache ipaapi ->> ``` ->> and updating de spec file freeipa.spec.in - -LLM model used was Claude Sonnet 4.5, and a CLAUDE.md file was -automatically created by claude based on the freeipa repository. -No custom context was available for the agent. - -Assisted-by: Claude -Signed-off-by: Rafael Guterres Jeffman -Reviewed-By: David Hanina -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: Alexander Bokovoy ---- - configure.ac | 42 ++++++++++++++++++++++++++------------ - freeipa.spec.in | 16 +++------------ - init/Makefile.am | 2 +- - init/sysusersd/Makefile.am | 12 +++++++++++ - init/sysusersd/ipa.conf.in | 8 ++++++++ - 5 files changed, 53 insertions(+), 27 deletions(-) - create mode 100644 init/sysusersd/Makefile.am - create mode 100644 init/sysusersd/ipa.conf.in - -diff --git a/configure.ac b/configure.ac -index 8b9adec1559c8831ef39c27860c1d31496ec5474..b0462bf779dedb7c2fe59494d4eb64a6dd121b1a 100644 ---- a/configure.ac -+++ b/configure.ac -@@ -267,6 +267,13 @@ AC_ARG_WITH([systemdtmpfilesdir], - [systemdtmpfilesdir=$($PKG_CONFIG --define-variable=prefix='${prefix}' --variable=tmpfilesdir systemd)]) - AC_SUBST([systemdtmpfilesdir]) - -+AC_ARG_WITH([systemdsysusersdir], -+ AS_HELP_STRING([--with-systemdsysusersdir=DIR], -+ [Directory for systemd-sysusers configuration files]), -+ [systemdsysusersdir=$with_systemdsysusersdir], -+ [systemdsysusersdir=$($PKG_CONFIG --define-variable=prefix='${prefix}' --variable=sysusersdir systemd)]) -+AC_SUBST([systemdsysusersdir]) -+ - AC_ARG_WITH([systemdcatalogdir], - AS_HELP_STRING([--with-systemdcatalogdir=DIR], - [Directory for systemd journal catalog files]), -@@ -398,22 +405,29 @@ AC_SUBST([IPAPLATFORM]) - AC_MSG_RESULT([${IPAPLATFORM}]) - - if test "x${IPAPLATFORM}" == "xdebian"; then -- HTTPD_GROUP="www-data" -- KRB5KDC_SERVICE="krb5-kdc.service" -- NAMED_GROUP="bind" -- ODS_USER="opendnssec" -- ODS_GROUP="opendnssec" -- # see https://www.debian.org/doc/packaging-manuals/python-policy/ap-packaging_tools.html -- PYTHON_INSTALL_EXTRA_OPTIONS="--install-layout=deb" -+ dnl Ubuntu http user is www-data -+ HTTPD_USER="www-data" -+ HTTPD_GROUP="www-data" -+ KRB5KDC_SERVICE="krb5-kdc.service" -+ NAMED_GROUP="bind" -+ ODS_USER="opendnssec" -+ ODS_GROUP="opendnssec" -+ # see https://www.debian.org/doc/packaging-manuals/python-policy/ap-packaging_tools.html -+ PYTHON_INSTALL_EXTRA_OPTIONS="--install-layout=deb" - else -- HTTPD_GROUP="apache" -- KRB5KDC_SERVICE="krb5kdc.service" -- NAMED_GROUP="named" -- ODS_USER="ods" -- ODS_GROUP="ods" -- PYTHON_INSTALL_EXTRA_OPTIONS="" -+ HTTPD_USER="apache" -+ HTTPD_GROUP="apache" -+ KRB5KDC_SERVICE="krb5kdc.service" -+ NAMED_GROUP="named" -+ ODS_USER="ods" -+ ODS_GROUP="ods" -+ PYTHON_INSTALL_EXTRA_OPTIONS="" - fi - -+AC_MSG_CHECKING([HTTPD_USER]) -+AC_SUBST([HTTPD_USER]) -+AC_MSG_RESULT([${HTTPD_USER}]) -+ - AC_MSG_CHECKING([HTTPD_GROUP]) - AC_SUBST([HTTPD_GROUP]) - AC_MSG_RESULT([${HTTPD_GROUP}]) -@@ -654,6 +668,7 @@ AC_CONFIG_FILES([ - daemons/ipa-slapi-plugins/topology/Makefile - init/systemd/Makefile - init/tmpfilesd/Makefile -+ init/sysusersd/Makefile - init/Makefile - install/Makefile - install/certmonger/Makefile -@@ -736,6 +751,7 @@ AM_COND_IF([ENABLE_SERVER], [ - KRAD libs: ${KRAD_LIBS} - krb5rundir: ${krb5rundir} - systemdtmpfilesdir: ${systemdtmpfilesdir} -+ systemdsysusersdir: ${systemdsysusersdir} - build mode: server & client" - ], [ - echo "\ -diff --git a/freeipa.spec.in b/freeipa.spec.in -index f3b45a5308f93928a4d4bb4cbb2ae96c487cf88a..48912185073472c11f08d000dacf3a0b7f2ec668 100644 ---- a/freeipa.spec.in -+++ b/freeipa.spec.in -@@ -620,7 +620,7 @@ Requires: systemd-units >= %{systemd_version} - Requires: system-logos-ipa >= 80.4 - %endif - --# The list below is automatically generated by `fix-spec.sh -i` -+# The list below is automatically generated by `fix-spec.sh -i` - # from the install/freeipa-webui - Provides: bundled(npm(attr-accept)) = 2.2.5 - Provides: bundled(npm(cookie)) = 1.0.2 -@@ -1274,6 +1274,7 @@ fi - /bin/systemctl reload-or-try-restart dbus - /bin/systemctl reload-or-try-restart oddjobd - -+%sysusers_create %{_sysusersdir}/ipa.conf - %tmpfiles_create ipa.conf - %journal_catalog_update - -@@ -1331,18 +1332,6 @@ if [ -e /usr/sbin/ipa_kpasswd ]; then - fi - - --%pre server-common --# create users and groups --# create kdcproxy group and user --getent group kdcproxy >/dev/null || groupadd -f -r kdcproxy --getent passwd kdcproxy >/dev/null || useradd -r -g kdcproxy -s /sbin/nologin -d / -c "IPA KDC Proxy User" kdcproxy --# create ipaapi group and user --getent group ipaapi >/dev/null || groupadd -f -r ipaapi --getent passwd ipaapi >/dev/null || useradd -r -g ipaapi -s /sbin/nologin -d / -c "IPA Framework User" ipaapi --# add apache to ipaaapi group --id -Gn apache | grep '\bipaapi\b' >/dev/null || usermod apache -a -G ipaapi -- -- - %post server-dns - %systemd_post ipa-dnskeysyncd.service ipa-ods-exporter.socket ipa-ods-exporter.service - -@@ -1729,6 +1718,7 @@ fi - %dir %attr(0755,root,root) %{_sysconfdir}/ipa/kdcproxy - %config(noreplace) %{_sysconfdir}/ipa/kdcproxy/kdcproxy.conf - # NOTE: systemd specific section -+%{_sysusersdir}/ipa.conf - %{_tmpfilesdir}/ipa.conf - %attr(644,root,root) %{_unitdir}/ipa-custodia.service - %ghost %attr(644,root,root) %{etc_systemd_dir}/httpd.d/ipa.conf -diff --git a/init/Makefile.am b/init/Makefile.am -index 8f4d1d0a8f7e9739cf7587de6e000dd027a85146..1d4a85ab20e892c8a7c428b84a6393d29e9616e5 100644 ---- a/init/Makefile.am -+++ b/init/Makefile.am -@@ -2,7 +2,7 @@ - # - AUTOMAKE_OPTIONS = 1.7 - --SUBDIRS = systemd tmpfilesd -+SUBDIRS = systemd tmpfilesd sysusersd - - dist_sysconfenv_DATA = \ - ipa-dnskeysyncd \ -diff --git a/init/sysusersd/Makefile.am b/init/sysusersd/Makefile.am -new file mode 100644 -index 0000000000000000000000000000000000000000..8577255a61ac796353995d3d1f99de195f9bd7c0 ---- /dev/null -+++ b/init/sysusersd/Makefile.am -@@ -0,0 +1,12 @@ -+dist_noinst_DATA = \ -+ ipa.conf.in -+ -+systemdsysusers_DATA = \ -+ ipa.conf -+ -+CLEANFILES = $(systemdsysusers_DATA) -+ -+%: %.in Makefile -+ sed \ -+ -e 's|@HTTPD_USER[@]|$(HTTPD_USER)|g' \ -+ '$(srcdir)/$@.in' >$@ -diff --git a/init/sysusersd/ipa.conf.in b/init/sysusersd/ipa.conf.in -new file mode 100644 -index 0000000000000000000000000000000000000000..dcddfc2fc7969b86913ffcd8c397152e4f800fda ---- /dev/null -+++ b/init/sysusersd/ipa.conf.in -@@ -0,0 +1,8 @@ -+# IPA KDC Proxy user and group -+u kdcproxy - "IPA KDC Proxy User" -+ -+# IPA API user and group -+u ipaapi - "IPA API User" -+ -+# - add Apache system account to ipaapi group (platform-specific) -+m @HTTPD_USER@ ipaapi --- -2.52.0 - diff --git a/0013-ipatests-add-Random-Password-based-replica-promotion.patch b/0013-ipatests-add-Random-Password-based-replica-promotion.patch deleted file mode 100644 index e32bda0..0000000 --- a/0013-ipatests-add-Random-Password-based-replica-promotion.patch +++ /dev/null @@ -1,196 +0,0 @@ -From a55f9185c96457bdffe9099ddde39ec696f1f998 Mon Sep 17 00:00:00 2001 -From: Anuja More -Date: Tue, 6 Jan 2026 18:30:06 +0530 -Subject: [PATCH] ipatests: add Random Password based replica promotion - coverage - -Added missing test coverage for : -- Installing IPA replica server using random password. -- Installing IPA replica server using random password installed client - -- Automated with Cursor+Claude - -Fixes: https://pagure.io/freeipa/issue/9922 - -Signed-off-by: Anuja More -Reviewed-By: David Hanina -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: Rob Crittenden -Reviewed-By: Rob Crittenden -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: David Hanina ---- - .../nightly_ipa-4-13_latest.yaml | 12 +++ - .../nightly_ipa-4-13_latest_selinux.yaml | 13 +++ - ipatests/pytest_ipa/integration/tasks.py | 15 ++++ - .../test_replica_promotion.py | 87 +++++++++++++++++++ - 4 files changed, 127 insertions(+) - -diff --git a/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml b/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml -index aff55727e463207fb235ff340989491e62162149..c61701ef5f88760f1d6fc36d4acce453a22b6f8f 100644 ---- a/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml -+++ b/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml -@@ -1000,6 +1000,18 @@ jobs: - timeout: 7200 - topology: *ad_master_1repl_1client - -+ fedora-latest-ipa-4-13/test_replica_promotion_TestReplicaPromotionRandomPassword: -+ requires: [fedora-latest-ipa-4-13/build] -+ priority: 50 -+ job: -+ class: RunPytest -+ args: -+ build_url: '{fedora-latest-ipa-4-13/build_url}' -+ test_suite: test_integration/test_replica_promotion.py::TestReplicaPromotionRandomPassword -+ template: *ci-ipa-4-13-latest -+ timeout: 7200 -+ topology: *master_1repl -+ - fedora-latest-ipa-4-13/test_upgrade: - requires: [fedora-latest-ipa-4-13/build] - priority: 50 -diff --git a/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml b/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml -index e6c57ea060b3bb8bfdf8b6f981f8fd28e4a7d320..9b96f3e857e2125478b45632d8d58e42b6e92668 100644 ---- a/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml -+++ b/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml -@@ -1078,6 +1078,19 @@ jobs: - timeout: 7200 - topology: *ad_master_1repl_1client - -+ fedora-latest-ipa-4-13/test_replica_promotion_TestReplicaPromotionRandomPassword: -+ requires: [fedora-latest-ipa-4-13/build] -+ priority: 50 -+ job: -+ class: RunPytest -+ args: -+ build_url: '{fedora-latest-ipa-4-13/build_url}' -+ selinux_enforcing: True -+ test_suite: test_integration/test_replica_promotion.py::TestReplicaPromotionRandomPassword -+ template: *ci-ipa-4-13-latest -+ timeout: 7200 -+ topology: *master_1repl -+ - fedora-latest-ipa-4-13/test_upgrade: - requires: [fedora-latest-ipa-4-13/build] - priority: 50 -diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py -index 32ac5cbc2c6fe87850dfb15c1d5beae6fa648dfb..ff2ea9792d04ebd2e6bd7bb3b51d97f35cb3fbfb 100755 ---- a/ipatests/pytest_ipa/integration/tasks.py -+++ b/ipatests/pytest_ipa/integration/tasks.py -@@ -3340,3 +3340,18 @@ def service_control_dirsrv(host, function='restart'): - instance = realm_to_serverid(host.domain.realm) - cmd = host.run_command(['systemctl', function, f"dirsrv@{instance}"]) - assert cmd.returncode == 0 -+ -+ -+def host_add_with_random_password(host, new_host): -+ """ -+ Add a new host with a random password and return the generated password. -+ """ -+ kinit_admin(host) -+ cmd = host.run_command( -+ ['ipa', 'host-add', new_host.hostname, '--random'] -+ ) -+ result = re.search("Random password: (?P.*$)", -+ cmd.stdout_text, -+ re.MULTILINE) -+ randpasswd1 = result.group('password') -+ return randpasswd1 -diff --git a/ipatests/test_integration/test_replica_promotion.py b/ipatests/test_integration/test_replica_promotion.py -index 76d6aa24e2ab3d88b7013e0d107d0e27ae7f3426..f8c8414eefbc015cfc0947de575ea349a65a5e73 100644 ---- a/ipatests/test_integration/test_replica_promotion.py -+++ b/ipatests/test_integration/test_replica_promotion.py -@@ -1368,3 +1368,90 @@ class TestReplicaConn(IntegrationTest): - logs = self.replica.get_file_contents(paths.IPAREPLICA_CONNCHECK_LOG) - error = "not allowed to perform server connection check" - assert error.encode() not in logs -+ -+ -+class TestReplicaPromotionRandomPassword(IntegrationTest): -+ """ -+ Test installation of a replica using Random Password -+ (one step install and two-steps installation -+ with client and promotion). -+ """ -+ num_replicas = 1 -+ -+ @classmethod -+ def install(cls, mh): -+ tasks.install_master(cls.master, setup_dns=True) -+ cls.replicas[0].resolver.backup() -+ nameservers = cls.master.ip -+ cls.replicas[0].resolver.setup_resolver( -+ nameservers, cls.master.domain.name -+ ) -+ -+ @replicas_cleanup -+ def test_replica_random_password_install(self): -+ """ -+ Installing IPA replica server using Random Password. -+ -+ Steps: -+ 1. Ensure replica host/server entries are clean and add DNS A record. -+ 2. Add the replica host with a random password and add it to -+ the ipaservers hostgroup. -+ 3. Install the replica using random password. -+ """ -+ replica = self.replicas[0] -+ tasks.kinit_admin(self.master) -+ tasks.add_a_record(self.master, replica) -+ randpasswd = tasks.host_add_with_random_password(self.master, -+ replica) -+ self.master.run_command([ -+ 'ipa', 'hostgroup-add-member', '--hosts', -+ replica.hostname, 'ipaservers' -+ ]) -+ replica.run_command( -+ ['ipa-replica-install', '-p', randpasswd, '-U'] -+ ) -+ -+ @replicas_cleanup -+ def test_replica_two_step_install(self): -+ """ -+ Installing IPA replica server using Random Password installed client -+ -+ Steps: -+ 1. Ensure replica host/server entries are clean and add DNS A record. -+ 2. Add the replica host with a random password and add it to -+ the ipaservers hostgroup. -+ 3. Install the IPA client using the Random Password. -+ 4. Promote the client to a replica. -+ 5. Install CA on the replica and verify the server role. -+ """ -+ replica = self.replicas[0] -+ replica.resolver.backup() -+ tasks.kinit_admin(self.master) -+ tasks.add_a_record(self.master, replica) -+ randpasswd = tasks.host_add_with_random_password(self.master, -+ replica) -+ self.master.run_command([ -+ 'ipa', 'hostgroup-add-member', '--hosts', -+ replica.hostname, 'ipaservers' -+ ]) -+ replica.resolver.setup_resolver( -+ self.master.ip, self.master.domain.name -+ ) -+ replica.run_command( -+ ['ipa-client-install', '-w', randpasswd, '-U'] -+ ) -+ Firewall(replica).enable_services(["freeipa-ldap", -+ "freeipa-ldaps"]) -+ replica.run_command(['ipa-replica-install', '-U']) -+ tasks.kinit_admin(replica) -+ replica.run_command([ -+ 'ipa-ca-install', '-p', -+ self.master.config.admin_password, -+ '-w', self.master.config.admin_password -+ ]) -+ result = self.replicas[0].run_command([ -+ 'ipa', 'server-role-find', -+ '--server', self.replicas[0].hostname, -+ '--role', 'CA server' -+ ]) -+ assert 'Role status: enabled' in result.stdout_text --- -2.52.0 - diff --git a/0014-ipatests-Add-integration-tests-for-ipa-join-command.patch b/0014-ipatests-Add-integration-tests-for-ipa-join-command.patch deleted file mode 100644 index c94343b..0000000 --- a/0014-ipatests-Add-integration-tests-for-ipa-join-command.patch +++ /dev/null @@ -1,760 +0,0 @@ -From 7cb2c5d38a84eb31aa9ddd20cf9dc0b6d90fa242 Mon Sep 17 00:00:00 2001 -From: PRANAV THUBE -Date: Tue, 27 Jan 2026 18:45:53 +0530 -Subject: [PATCH] ipatests: Add integration tests for ipa-join command - -Add tests for ipa-join command covering hostname, server, keytab, -and bindpw options with positive and negative scenarios. - -Related: https://pagure.io/freeipa/issue/9930 -Reviewed-By: Rob Crittenden ---- - ipatests/prci_definitions/gating.yaml | 12 + - .../nightly_ipa-4-13_latest.yaml | 12 + - .../nightly_ipa-4-13_latest_selinux.yaml | 13 + - ipatests/pytest_ipa/integration/tasks.py | 49 ++ - ipatests/test_integration/test_ipa_join.py | 614 ++++++++++++++++++ - 5 files changed, 700 insertions(+) - create mode 100644 ipatests/test_integration/test_ipa_join.py - -diff --git a/ipatests/prci_definitions/gating.yaml b/ipatests/prci_definitions/gating.yaml -index 0c2a0dafa9de12add8959d9974f080ba8bec0706..182f2c5a097856d22336fa66a0876bdfcf3f3f8d 100644 ---- a/ipatests/prci_definitions/gating.yaml -+++ b/ipatests/prci_definitions/gating.yaml -@@ -394,3 +394,15 @@ jobs: - template: *ci-ipa-4-13-latest - timeout: 7200 - topology: *master_3client -+ -+ fedora-latest-ipa-4-13/test_ipa_join: -+ requires: [fedora-latest-ipa-4-13/build] -+ priority: 100 -+ job: -+ class: RunPytest -+ args: -+ build_url: '{fedora-latest-ipa-4-13/build_url}' -+ test_suite: test_integration/test_ipa_join.py -+ template: *ci-ipa-4-13-latest -+ timeout: 3600 -+ topology: *master_1repl_1client -diff --git a/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml b/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml -index c61701ef5f88760f1d6fc36d4acce453a22b6f8f..aba33a5a05185460305c7516c93ae25c60f9dda7 100644 ---- a/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml -+++ b/ipatests/prci_definitions/nightly_ipa-4-13_latest.yaml -@@ -2292,3 +2292,15 @@ jobs: - template: *ci-ipa-4-13-latest - timeout: 2400 - topology: *ipaserver -+ -+ fedora-latest-ipa-4-13/test_ipa_join: -+ requires: [fedora-latest-ipa-4-13/build] -+ priority: 50 -+ job: -+ class: RunPytest -+ args: -+ build_url: '{fedora-latest-ipa-4-13/build_url}' -+ test_suite: test_integration/test_ipa_join.py -+ template: *ci-ipa-4-13-latest -+ timeout: 3600 -+ topology: *master_1repl_1client -diff --git a/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml b/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml -index 9b96f3e857e2125478b45632d8d58e42b6e92668..7184b722076ba2cab7d782d990a9bc218158a09f 100644 ---- a/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml -+++ b/ipatests/prci_definitions/nightly_ipa-4-13_latest_selinux.yaml -@@ -2476,3 +2476,16 @@ jobs: - template: *ci-ipa-4-13-latest - timeout: 2400 - topology: *ipaserver -+ -+ fedora-latest-ipa-4-13/test_ipa_join: -+ requires: [fedora-latest-ipa-4-13/build] -+ priority: 50 -+ job: -+ class: RunPytest -+ args: -+ build_url: '{fedora-latest-ipa-4-13/build_url}' -+ selinux_enforcing: True -+ test_suite: test_integration/test_ipa_join.py -+ template: *ci-ipa-4-13-latest -+ timeout: 3600 -+ topology: *master_1repl_1client -diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py -index ff2ea9792d04ebd2e6bd7bb3b51d97f35cb3fbfb..47330d6d93401485e4eb7b2501cf5ea37498d719 100755 ---- a/ipatests/pytest_ipa/integration/tasks.py -+++ b/ipatests/pytest_ipa/integration/tasks.py -@@ -3355,3 +3355,52 @@ def host_add_with_random_password(host, new_host): - re.MULTILINE) - randpasswd1 = result.group('password') - return randpasswd1 -+ -+ -+def ipa_join(host, *extra_args, raiseonerr=True): -+ """Run ipa-join command. -+ -+ :param host: The host to run command on -+ :param extra_args: Additional arguments (variable positional args) -+ e.g., '--hostname=client.example.com', -+ '--server=master.example.com', -+ '--keytab=/tmp/test.keytab', -+ '--bindpw=password', -+ '-u' (for unenroll) -+ :param raiseonerr: If True, raise exception on command failure -+ :return: Command result object -+ """ -+ command = ['ipa-join'] -+ command.extend(extra_args) -+ return host.run_command(command, raiseonerr=raiseonerr) -+ -+ -+def host_del(host, hostname, *extra_args, raiseonerr=True): -+ """Delete a host from IPA. -+ -+ :param host: The IPA host to run command on -+ :param hostname: Hostname to delete -+ :param extra_args: Additional arguments (variable positional args) -+ :param raiseonerr: If True, raise exception on command failure -+ :return: Command result object -+ """ -+ command = ['ipa', 'host-del', hostname] -+ command.extend(extra_args) -+ return host.run_command(command, raiseonerr=raiseonerr) -+ -+ -+def host_add(host, hostname, *extra_args, password=None, raiseonerr=True): -+ """Add a host to IPA. -+ -+ :param host: The IPA host to run command on -+ :param hostname: Hostname to add -+ :param extra_args: Additional arguments (variable positional args) -+ :param password: OTP/enrollment password for the host (optional) -+ :param raiseonerr: If True, raise exception on command failure -+ :return: Command result object -+ """ -+ command = ['ipa', 'host-add', hostname] -+ if password: -+ command.append(f'--password={password}') -+ command.extend(extra_args) -+ return host.run_command(command, raiseonerr=raiseonerr) -diff --git a/ipatests/test_integration/test_ipa_join.py b/ipatests/test_integration/test_ipa_join.py -new file mode 100644 -index 0000000000000000000000000000000000000000..1f7592aec8db1bfd048ec574d06d25bc24373499 ---- /dev/null -+++ b/ipatests/test_integration/test_ipa_join.py -@@ -0,0 +1,614 @@ -+# -+# Copyright (C) 2026 FreeIPA Contributors see COPYING for license -+# -+ -+""" -+Tests for ipa-join command functionality. -+ -+This module tests various combinations of ipa-join options including: -+- hostname -+- server -+- keytab -+- bindpw (OTP/enrollment password) -+- unenroll -+ -+Ported from the shell-based test suite (t.ipajoin.sh and t.ipaotp.sh). -+""" -+ -+from __future__ import absolute_import -+ -+from ipapython.ipautil import ipa_generate_password -+from ipatests.pytest_ipa.integration import tasks -+from ipatests.test_integration.base import IntegrationTest -+ -+ -+# Constants -+OTP = ipa_generate_password(special=None) -+INVALID_PASSWORD = "WrongPassword" -+INVALID_SERVER = "No.Such.IPA.Server.Domain.com" -+TEST_KEYTAB = "/tmp/ipajoin.test.keytab" -+ -+# Error messages -+ERR_SASL_BIND_FAILED = "SASL Bind failed" -+ERR_UNAUTHENTICATED_BIND = "Unauthenticated binds are not allowed" -+ERR_COULD_NOT_RESOLVE = "JSON-RPC call failed: Could not resolve hostname" -+ERR_UNABLE_ROOT_DN = "Unable to determine root DN" -+ERR_NO_CONFIG = "Unable to determine IPA server from /etc/ipa/default.conf" -+ERR_PREAUTH_FAILED = "Generic preauthentication failure" -+ -+# Exit codes -+EXIT_SUCCESS = 0 -+EXIT_GENERAL_ERROR = 1 -+EXIT_PREAUTH_ERROR = 19 -+EXIT_ROOT_DN_ERROR = 14 -+EXIT_SASL_BIND_FAILED = 15 -+EXIT_RESOLVE_ERROR = 17 -+ -+ -+class TestIPAJoin(IntegrationTest): -+ """Tests for ipa-join command functionality. -+ -+ This test class covers various ipa-join scenarios including: -+ - Basic enrollment and unenrollment -+ - Using hostname, server, keytab, and bindpw options -+ - Positive and negative test cases -+ - OTP (one-time password) enrollment tests -+ -+ Tests require one master and one client. -+ """ -+ -+ topology = 'line' -+ num_clients = 1 -+ -+ @classmethod -+ def install(cls, mh): -+ tasks.install_master(cls.master, setup_dns=True) -+ tasks.install_client(cls.master, cls.clients[0]) -+ -+ @classmethod -+ def uninstall(cls, mh): -+ # Cleanup test keytab if exists -+ cls.clients[0].run_command( -+ ['rm', '-f', TEST_KEYTAB], -+ raiseonerr=False -+ ) -+ tasks.uninstall_client(cls.clients[0]) -+ tasks.uninstall_master(cls.master) -+ -+ # ========================================================================= -+ # ipa-join basic tests -+ # ========================================================================= -+ -+ def test_unenroll(self): -+ """Test ipa-join --unenroll option.""" -+ result = tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ assert result.returncode == EXIT_SUCCESS -+ -+ def test_unenroll_already_unenrolled(self): -+ """Test ipa-join -u on an already unenrolled client. -+ -+ When trying to unenroll a client that is not enrolled, -+ ipa-join should fail with a preauthentication error. -+ """ -+ # Client is already unenrolled from previous test -+ result = tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ assert result.returncode == EXIT_PREAUTH_ERROR -+ assert ERR_PREAUTH_FAILED in result.stderr_text -+ -+ def test_hostname_with_kerberos(self): -+ """Test ipa-join with --hostname using Kerberos auth.""" -+ tasks.kinit_admin(self.clients[0]) -+ try: -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_hostname_bindpw_invalid(self): -+ """Test ipa-join with hostname and invalid bindpw.""" -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--bindpw={INVALID_PASSWORD}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_SASL_BIND_FAILED -+ assert ERR_SASL_BIND_FAILED in result.stderr_text -+ -+ def test_hostname_bindpw_valid(self): -+ """Test ipa-join with hostname and valid OTP.""" -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--bindpw={OTP}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_hostname_keytab_with_kerberos(self): -+ """Test ipa-join with hostname and keytab using Kerberos.""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ -+ try: -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--keytab={TEST_KEYTAB}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_hostname_keytab_bindpw_invalid(self): -+ """Test ipa-join with hostname, keytab, and invalid bindpw.""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--keytab={TEST_KEYTAB}', -+ f'--bindpw={INVALID_PASSWORD}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_SASL_BIND_FAILED -+ assert ERR_SASL_BIND_FAILED in result.stderr_text -+ -+ def test_hostname_keytab_bindpw_valid(self): -+ """Test ipa-join with hostname, keytab, and valid OTP.""" -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--keytab={TEST_KEYTAB}', -+ f'--bindpw={OTP}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_hostname_server_invalid_with_kerberos(self): -+ """Test ipa-join with hostname and invalid server.""" -+ tasks.kinit_admin(self.clients[0]) -+ try: -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--server={INVALID_SERVER}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_RESOLVE_ERROR -+ assert ERR_COULD_NOT_RESOLVE in result.stderr_text -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ -+ def test_hostname_server_invalid_bindpw_valid(self): -+ """Test ipa-join with hostname, invalid server, and valid OTP.""" -+ tasks.kinit_admin(self.clients[0]) -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--server={INVALID_SERVER}', -+ f'--bindpw={OTP}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_ROOT_DN_ERROR -+ assert ERR_UNABLE_ROOT_DN in result.stderr_text -+ -+ def test_hostname_server_invalid_keytab_with_kerberos(self): -+ """Test ipa-join with hostname, invalid server, keytab.""" -+ tasks.kinit_admin(self.clients[0]) -+ try: -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--server={INVALID_SERVER}', -+ f'--keytab={TEST_KEYTAB}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_RESOLVE_ERROR -+ assert ERR_COULD_NOT_RESOLVE in result.stderr_text -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ -+ def test_hostname_server_invalid_keytab_bindpw_valid(self): -+ """Test ipa-join with hostname, invalid server, keytab, valid OTP.""" -+ tasks.kinit_admin(self.clients[0]) -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--server={INVALID_SERVER}', -+ f'--keytab={TEST_KEYTAB}', -+ f'--bindpw={OTP}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_ROOT_DN_ERROR -+ assert ERR_UNABLE_ROOT_DN in result.stderr_text -+ -+ def test_hostname_server_valid_with_kerberos(self): -+ """Test ipa-join with hostname and valid server.""" -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ -+ try: -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--server={self.master.hostname}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_hostname_server_valid_bindpw_invalid(self): -+ """Test ipa-join with hostname, valid server, invalid bindpw.""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--server={self.master.hostname}', -+ f'--bindpw={INVALID_PASSWORD}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_SASL_BIND_FAILED -+ assert ERR_SASL_BIND_FAILED in result.stderr_text -+ -+ def test_hostname_server_valid_bindpw_valid(self): -+ """Test ipa-join with hostname, valid server, valid OTP.""" -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--server={self.master.hostname}', -+ f'--bindpw={OTP}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_hostname_server_valid_keytab_with_kerberos(self): -+ """Test ipa-join with hostname, valid server, keytab.""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname) -+ -+ try: -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--server={self.master.hostname}', -+ f'--keytab={TEST_KEYTAB}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_hostname_server_valid_keytab_bindpw_invalid(self): -+ """Test ipa-join with hostname, valid server, keytab, bad bindpw.""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--server={self.master.hostname}', -+ f'--keytab={TEST_KEYTAB}', -+ f'--bindpw={INVALID_PASSWORD}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_SASL_BIND_FAILED -+ # Note: Original test had "SASL Bind Failed" (capital F), checking both -+ assert "SASL Bind" in result.stderr_text -+ assert "ailed" in result.stderr_text -+ -+ def test_hostname_server_valid_keytab_bindpw_valid(self): -+ """Test ipa-join with hostname, valid server, keytab, valid OTP.""" -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--server={self.master.hostname}', -+ f'--keytab={TEST_KEYTAB}', -+ f'--bindpw={OTP}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_keytab_only_with_kerberos(self): -+ """Test ipa-join with keytab only using Kerberos.""" -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname) -+ -+ try: -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--keytab={TEST_KEYTAB}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_keytab_bindpw_invalid(self): -+ """Test ipa-join with keytab and invalid bindpw.""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--keytab={TEST_KEYTAB}', -+ f'--bindpw={INVALID_PASSWORD}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_SASL_BIND_FAILED -+ assert ERR_SASL_BIND_FAILED in result.stderr_text -+ -+ def test_keytab_bindpw_valid(self): -+ """Test ipa-join with keytab and valid OTP.""" -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--keytab={TEST_KEYTAB}', -+ f'--bindpw={OTP}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_server_invalid_only_with_kerberos(self): -+ """Test ipa-join with invalid server only.""" -+ tasks.kinit_admin(self.clients[0]) -+ try: -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--server={INVALID_SERVER}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_RESOLVE_ERROR -+ assert ERR_COULD_NOT_RESOLVE in result.stderr_text -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ -+ def test_server_invalid_bindpw_valid(self): -+ """Test ipa-join with invalid server and valid OTP.""" -+ tasks.kinit_admin(self.clients[0]) -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--server={INVALID_SERVER}', -+ f'--bindpw={OTP}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_ROOT_DN_ERROR -+ assert ERR_UNABLE_ROOT_DN in result.stderr_text -+ -+ def test_server_invalid_keytab_with_kerberos(self): -+ """Test ipa-join with invalid server and keytab.""" -+ tasks.kinit_admin(self.clients[0]) -+ try: -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--server={INVALID_SERVER}', -+ f'--keytab={TEST_KEYTAB}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_RESOLVE_ERROR -+ assert ERR_COULD_NOT_RESOLVE in result.stderr_text -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ -+ def test_server_valid_only_with_kerberos(self): -+ """Test ipa-join with valid server only.""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname) -+ -+ try: -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--server={self.master.hostname}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_server_valid_bindpw_invalid(self): -+ """Test ipa-join with valid server and invalid bindpw.""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--server={self.master.hostname}', -+ f'--bindpw={INVALID_PASSWORD}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_SASL_BIND_FAILED -+ assert ERR_SASL_BIND_FAILED in result.stderr_text -+ -+ def test_server_valid_bindpw_valid(self): -+ """Test ipa-join with valid server and valid OTP.""" -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--server={self.master.hostname}', -+ f'--bindpw={OTP}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_server_valid_keytab_with_kerberos(self): -+ """Test ipa-join with valid server and keytab.""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname) -+ -+ try: -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--server={self.master.hostname}', -+ f'--keytab={TEST_KEYTAB}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_bindpw_invalid_only(self): -+ """Test ipa-join with invalid bindpw only.""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--bindpw={INVALID_PASSWORD}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_SASL_BIND_FAILED -+ assert ERR_SASL_BIND_FAILED in result.stderr_text -+ -+ # ========================================================================= -+ # OTP (One-Time Password) tests -+ # ========================================================================= -+ -+ def test_otp_empty_password(self): -+ """Test ipa-join with empty OTP password (ipa_otp_1001).""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ '--bindpw=', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_ROOT_DN_ERROR -+ assert ERR_UNAUTHENTICATED_BIND in result.stderr_text -+ -+ def test_otp_wrong_password(self): -+ """Test ipa-join with wrong OTP password (ipa_otp_1002).""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--bindpw={INVALID_PASSWORD}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_SASL_BIND_FAILED -+ assert ERR_SASL_BIND_FAILED in result.stderr_text -+ -+ def test_otp_valid_password(self): -+ """Test ipa-join with valid OTP password (ipa_otp_1003).""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ try: -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--bindpw={OTP}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ def test_otp_reuse_fails(self): -+ """Test that reusing the same OTP fails (ipa_otp_1004).""" -+ tasks.kinit_admin(self.clients[0]) -+ tasks.kinit_admin(self.master) -+ tasks.host_del(self.master, self.clients[0].hostname, raiseonerr=False) -+ tasks.host_add(self.master, self.clients[0].hostname, password=OTP) -+ try: -+ # First use should succeed -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--bindpw={OTP}' -+ ) -+ assert result.returncode == EXIT_SUCCESS -+ finally: -+ self.clients[0].run_command(['kdestroy', '-A'], raiseonerr=False) -+ tasks.ipa_join(self.clients[0], '-u', raiseonerr=False) -+ -+ # Second use of same OTP should fail -+ result = tasks.ipa_join( -+ self.clients[0], -+ f'--hostname={self.clients[0].hostname}', -+ f'--bindpw={OTP}', -+ raiseonerr=False -+ ) -+ -+ assert result.returncode == EXIT_SASL_BIND_FAILED -+ assert ERR_SASL_BIND_FAILED in result.stderr_text --- -2.52.0 - diff --git a/0015-fetch_domains-Use-case-insensitive-comparison-for-do.patch b/0015-fetch_domains-Use-case-insensitive-comparison-for-do.patch deleted file mode 100644 index 3fdbc7e..0000000 --- a/0015-fetch_domains-Use-case-insensitive-comparison-for-do.patch +++ /dev/null @@ -1,48 +0,0 @@ -From 7f6a2835f0972af5e94b58daf47fa60bfade4279 Mon Sep 17 00:00:00 2001 -From: Florence Blanc-Renaud -Date: Thu, 22 Jan 2026 10:02:11 +0100 -Subject: [PATCH] fetch_domains: Use case-insensitive comparison for domains - names - -The fetch_domains method is using netr_DsRGetForestTrustInformation -to retrieve the forest trust information. The returned data contains -domain entries, with a DNS domain name that can contain mixed case -(for instance adDomain.Test). -The method compares the domain name with the provided parameter in a -case sensitive comparison, while it should use a case-insensitive -method (DNS names are case-insensitive). - -Fix the method and compare the lowercase value instead. - -Fixes: https://pagure.io/freeipa/issue/9924 -Signed-off-by: Florence Blanc-Renaud -Reviewed-By: Alexander Bokovoy ---- - ipaserver/dcerpc.py | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/ipaserver/dcerpc.py b/ipaserver/dcerpc.py -index 1182f128b4988bc699fe7a40d4834f1bead82cf5..5c05ffedb889e774e342cb6cb85ff954d06ac5e9 100644 ---- a/ipaserver/dcerpc.py -+++ b/ipaserver/dcerpc.py -@@ -1635,7 +1635,7 @@ def fetch_domains(api, mydomain, trustdomain, creds=None, server=None): - t.forest_trust_data.netbios_domain_name.string - - tname = unicode(t.forest_trust_data.dns_domain_name.string) -- if tname != trustdomain: -+ if tname.lower() != trustdomain.lower(): - result['domains'][tname] = { - 'cn': tname, - 'ipantflatname': unicode( -@@ -1647,7 +1647,7 @@ def fetch_domains(api, mydomain, trustdomain, creds=None, server=None): - record.data.string = t.forest_trust_data.string - - tname = unicode(t.forest_trust_data.string) -- if tname == trustdomain: -+ if tname.lower() == trustdomain.lower(): - continue - - result['suffixes'][tname] = {'cn': tname} --- -2.52.0 - diff --git a/0016-Handle-IPACertificate-types-in-xmlrpc.patch b/0016-Handle-IPACertificate-types-in-xmlrpc.patch deleted file mode 100644 index 1ac2456..0000000 --- a/0016-Handle-IPACertificate-types-in-xmlrpc.patch +++ /dev/null @@ -1,43 +0,0 @@ -From 8deb4be0962b25dfd43e1245307a8bb9d58cfc48 Mon Sep 17 00:00:00 2001 -From: Rob Crittenden -Date: Tue, 3 Feb 2026 09:46:25 -0500 -Subject: [PATCH] Handle IPACertificate types in xmlrpc - -The wrapping code didn't understand the IPACertificate class -so retrieving any entry that contained one would fail. - -Treat it the same was as its parent class cryptography.Certificate. - -Fixes: https://pagure.io/freeipa/issue/9935 - -Signed-off-by: Rob Crittenden -Reviewed-By: David Hanina ---- - ipalib/rpc.py | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/ipalib/rpc.py b/ipalib/rpc.py -index ed35afc965308e03269f05e01400660b207b548d..9773626eb054dd404256267c5fffbba1aa0579dd 100644 ---- a/ipalib/rpc.py -+++ b/ipalib/rpc.py -@@ -56,7 +56,7 @@ from ipalib.errors import (errors_by_code, UnknownError, NetworkError, - XMLRPCMarshallError, JSONError) - from ipalib import errors, capabilities - from ipalib.request import context, Connection --from ipalib.x509 import Encoding as x509_Encoding -+from ipalib.x509 import Encoding as x509_Encoding, IPACertificate - from ipapython import ipautil - from ipapython import session_storage - from ipapython.cookie import Cookie -@@ -220,7 +220,7 @@ def xml_wrap(value, version): - if isinstance(value, Principal): - return unicode(value) - -- if isinstance(value, crypto_x509.Certificate): -+ if isinstance(value, (crypto_x509.Certificate, IPACertificate)): - return base64.b64encode( - value.public_bytes(x509_Encoding.DER)).decode('ascii') - --- -2.52.0 - diff --git a/0017-Replace-None-with-when-uninstalling-CA.patch b/0017-Replace-None-with-when-uninstalling-CA.patch deleted file mode 100644 index f3f750d..0000000 --- a/0017-Replace-None-with-when-uninstalling-CA.patch +++ /dev/null @@ -1,159 +0,0 @@ -From a583b0dc08536a50e10b76e27861864b61906355 Mon Sep 17 00:00:00 2001 -From: David Hanina -Date: Mon, 2 Feb 2026 11:14:48 +0100 -Subject: [PATCH] Replace None with '' when uninstalling CA - -At many places we're obtaining records from a config file and expect the -config to have those keys, but the user may delete the line instead of -setting the value to false, this then leads to failed uninstall. This -patch replaces None with '' at some places that do not check for None. - -Fixes: https://pagure.io/freeipa/issue/9921 -Signed-off-by: David Hanina -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: Rob Crittenden ---- - ipaserver/install/ca.py | 12 ++++-- - ipaserver/install/cainstance.py | 14 ++++++- - ipaserver/install/server/upgrade.py | 3 +- - .../test_integration/test_crlgen_manage.py | 3 +- - .../test_integration/test_uninstallation.py | 41 +++++++++++++++++++ - 5 files changed, 66 insertions(+), 7 deletions(-) - -diff --git a/ipaserver/install/ca.py b/ipaserver/install/ca.py -index 5a026aa4c556f7012552052cd08223746f3c39ae..2e953a567a3a230cb2a5e35192af76c61f8c1047 100644 ---- a/ipaserver/install/ca.py -+++ b/ipaserver/install/ca.py -@@ -340,14 +340,20 @@ def uninstall_crl_check(options): - - try: - crlgen_enabled = ca.is_crlgen_enabled() -- except cainstance.InconsistentCRLGenConfigException: -+ except cainstance.InconsistentCRLGenConfigException as e: - # If config is inconsistent, let's be safe and act as if - # crl gen was enabled -+ print(e) - crlgen_enabled = True - - if crlgen_enabled: -- print("Deleting this server will leave your installation " -- "without a CRL generation master.") -+ if not options.ignore_last_of_role: -+ print("Deleting this server will leave your installation " -+ "without a CRL generation master. Use --ignore-last-of-role " -+ "to bypass this check.") -+ else: -+ print("Deleting this server will leave your installation " -+ "without a CRL generation master.") - if (options.unattended and not options.ignore_last_of_role) or \ - not (options.unattended or ipautil.user_input( - "Are you sure you want to continue with the uninstall " -diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py -index b8267a625554f9375d27160f39b67ee2e64a2dbb..4933ad23d7323859af92bd02f6ae156803e29997 100644 ---- a/ipaserver/install/cainstance.py -+++ b/ipaserver/install/cainstance.py -@@ -1421,12 +1421,22 @@ class CAInstance(DogtagInstance): - try: - cache = directivesetter.get_directive( - self.config, 'ca.crl.MasterCRL.enableCRLCache', '=') -+ -+ if cache is None: -+ raise InconsistentCRLGenConfigException( -+ "Configuration is inconsistent, please check " -+ "ca.crl.MasterCRL.enableCRLCache, " -+ "ca.crl.MasterCRL.enableCRLUpdates and " -+ "ca.listenToCloneModifications in {} and " -+ "run ipa-crlgen-manage [enable|disable] to repair".format( -+ self.config)) -+ - enableCRLCache = cache.lower() == 'true' - updates = directivesetter.get_directive( -- self.config, 'ca.crl.MasterCRL.enableCRLUpdates', '=') -+ self.config, 'ca.crl.MasterCRL.enableCRLUpdates', '=') or '' - enableCRLUpdates = updates.lower() == 'true' - listen = directivesetter.get_directive( -- self.config, 'ca.listenToCloneModifications', '=') -+ self.config, 'ca.listenToCloneModifications', '=') or '' - enableToClone = listen.lower() == 'true' - updateinterval = directivesetter.get_directive( - self.config, 'ca.certStatusUpdateInterval', '=') -diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py -index 548ee02e1e8524ce0002dca1764d48728eb0509a..8692c983409426193e1746f07fa1a0514621cb4a 100644 ---- a/ipaserver/install/server/upgrade.py -+++ b/ipaserver/install/server/upgrade.py -@@ -1712,7 +1712,8 @@ def upgrade_configuration(): - if ca.is_configured(): - crl = directivesetter.get_directive( - paths.CA_CS_CFG_PATH, 'ca.crl.MasterCRL.enableCRLUpdates', '=') -- sub_dict['CLONE']='#' if crl.lower() == 'true' else '' -+ sub_dict['CLONE'] = '#' if crl is not None and \ -+ crl.lower() == 'true' else '' - - ds_dirname = dsinstance.config_dirname(ds.serverid) - -diff --git a/ipatests/test_integration/test_crlgen_manage.py b/ipatests/test_integration/test_crlgen_manage.py -index c6f41ebf8939bad8006b1e5eaf37bad30dbfd9d8..8a2a28a75b76158fcc61a8e7612f81343336b64f 100644 ---- a/ipatests/test_integration/test_crlgen_manage.py -+++ b/ipatests/test_integration/test_crlgen_manage.py -@@ -302,7 +302,8 @@ class TestCRLGenManage(IntegrationTest): - ['ipa-server-install', '--uninstall', '-U'], raiseonerr=False) - assert result.returncode == 1 - expected_msg = "Deleting this server will leave your installation " \ -- "without a CRL generation master" -+ "without a CRL generation master. Use " \ -+ "--ignore-last-of-role to bypass this check." - assert expected_msg in result.stdout_text - - def test_uninstall_with_ignore_last_of_role(self): -diff --git a/ipatests/test_integration/test_uninstallation.py b/ipatests/test_integration/test_uninstallation.py -index 8d83f72868f5c103b0c31d2aa96630c00b2dfbd8..12b10caa60745dcbc2d811bff65d57fb5d865f09 100644 ---- a/ipatests/test_integration/test_uninstallation.py -+++ b/ipatests/test_integration/test_uninstallation.py -@@ -237,3 +237,44 @@ class TestUninstallReinstall(IntegrationTest): - - def test_reinstall_server(self): - tasks.install_master(self.master, setup_dns=False) -+ -+ -+class TestUninstallCRLGen(IntegrationTest): -+ """Test uninstallation of a replica with broken CRL configuration. -+ -+ Removing ca.crl.MasterCRL.enableCRLCache from CS.cfg crashed. -+ https://pagure.io/freeipa/issue/9921 -+ """ -+ -+ num_replicas = 1 -+ topology = 'line' -+ -+ @classmethod -+ def install(cls, mh): -+ tasks.install_master(cls.master, setup_dns=False) -+ tasks.install_replica(cls.master, cls.replicas[0]) -+ -+ def test_uninstall_replica_with_broken_crlgen(self): -+ self.replicas[0].run_command(['ipa-crlgen-manage', 'enable']) -+ -+ self.replicas[0].run_command([ -+ '/bin/sed', -+ '-i', -+ '/ca.crl.MasterCRL.enableCRLCache=true/d', -+ paths.CA_CS_CFG_PATH, -+ ]) -+ -+ result = self.replicas[0].run_command([ -+ 'ipa-server-install', -+ '--ignore-last-of-role', -+ '--uninstall', '-U', -+ ]) -+ -+ expected_msg = "Configuration is inconsistent, please check " \ -+ "ca.crl.MasterCRL.enableCRLCache, " \ -+ "ca.crl.MasterCRL.enableCRLUpdates and " \ -+ "ca.listenToCloneModifications in {} and run " \ -+ "ipa-crlgen-manage [enable|disable] to repair".format( -+ paths.CA_CS_CFG_PATH) -+ -+ assert expected_msg in result.stdout_text --- -2.52.0 - diff --git a/0018-ipatests-Add-xmlrpc-tests-for-ipa-delegation-cli.patch b/0018-ipatests-Add-xmlrpc-tests-for-ipa-delegation-cli.patch deleted file mode 100644 index c80a432..0000000 --- a/0018-ipatests-Add-xmlrpc-tests-for-ipa-delegation-cli.patch +++ /dev/null @@ -1,416 +0,0 @@ -From 91a6618e51b0e767c5cc5e4b1719531dbbd7268d Mon Sep 17 00:00:00 2001 -From: Sudhir Menon -Date: Thu, 22 Jan 2026 12:43:56 +0530 -Subject: [PATCH] ipatests: Add xmlrpc tests for ipa-delegation-cli - -This patch adds below test cases to the the XML-RPC delegation plugin test suite -coverage of delegation operations and important bug regressions. - -Test cases added: - - Test basic delegation creation with write permission - Test delegation creation with --all flag - Test delegation creation with --raw flag (ACI format) - Test deletion of delegation with ipausers group - Test finding delegation by name criteria - Test finding delegation by membergroup filter - Test showing delegation by name - Test modifying delegation attrs - Test modifying delegation permissions - BZ 783548: Verify mod fails when membergroup doesn't exist - BZ 783554: Verify mod with empty attrs fails properly - BZ 888524: Verify find --group option works correctly - -Fixes: https://pagure.io/freeipa/issue/9931 -Assisted-by: Claude -Signed-off-by: Sudhir Menon -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: Rob Crittenden -Reviewed-By: Anuja More ---- - .../test_xmlrpc/test_delegation_plugin.py | 371 ++++++++++++++++++ - 1 file changed, 371 insertions(+) - -diff --git a/ipatests/test_xmlrpc/test_delegation_plugin.py b/ipatests/test_xmlrpc/test_delegation_plugin.py -index b3d2aadbddbaaff6f40e1046e4df32bcc9ee7e2d..9245f259e21cad166c3c5b0565da3bb56a341e6b 100644 ---- a/ipatests/test_xmlrpc/test_delegation_plugin.py -+++ b/ipatests/test_xmlrpc/test_delegation_plugin.py -@@ -333,4 +333,375 @@ class test_delegation(Declarative): - summary=u'Deleted delegation "%s"' % delegation1, - ) - ), -+ -+ -+ dict( -+ desc='Create delegation with mobile attr and write permission', -+ command=( -+ 'delegation_add', [u'test_mobile_delegation'], dict( -+ attrs=[u'mobile'], -+ permissions=u'write', -+ group=u'editors', -+ memberof=u'admins', -+ ) -+ ), -+ expected=dict( -+ value=u'test_mobile_delegation', -+ summary=u'Added delegation "test_mobile_delegation"', -+ result=dict( -+ attrs=[u'mobile'], -+ permissions=[u'write'], -+ aciname=u'test_mobile_delegation', -+ group=u'editors', -+ memberof=member1, -+ ), -+ ), -+ ), -+ -+ -+ dict( -+ desc='Create delegation with --all flag', -+ command=( -+ 'delegation_add', [u'test_all_flag'], dict( -+ attrs=[u'mobile'], -+ permissions=u'write', -+ group=u'editors', -+ memberof=u'admins', -+ all=True, -+ ) -+ ), -+ expected=dict( -+ value=u'test_all_flag', -+ summary=u'Added delegation "test_all_flag"', -+ result=dict( -+ attrs=[u'mobile'], -+ permissions=[u'write'], -+ aciname=u'test_all_flag', -+ group=u'editors', -+ memberof=member1, -+ ), -+ ), -+ ), -+ -+ -+ dict( -+ desc='Create delegation with --raw flag', -+ command=( -+ 'delegation_add', [u'test_raw_flag'], dict( -+ attrs=[u'mobile'], -+ permissions=u'write', -+ group=u'editors', -+ memberof=u'admins', -+ raw=True, -+ ) -+ ), -+ expected=dict( -+ value=u'test_raw_flag', -+ summary=u'Added delegation "test_raw_flag"', -+ result={ -+ 'aci': u'(targetattr = "mobile")(targetfilter = ' -+ u'"(memberOf=%s)")(version 3.0;acl ' -+ u'"delegation:test_raw_flag";allow (write) ' -+ u'groupdn = "ldap:///%s";)' % ( -+ DN(('cn', 'admins'), ('cn', 'groups'), -+ ('cn', 'accounts'), api.env.basedn), -+ DN(('cn', 'editors'), ('cn', 'groups'), -+ ('cn', 'accounts'), api.env.basedn)) -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Delete test_mobile_delegation', -+ command=('delegation_del', [u'test_mobile_delegation'], {}), -+ expected=dict( -+ result=True, -+ value=u'test_mobile_delegation', -+ summary=u'Deleted delegation "test_mobile_delegation"', -+ ) -+ ), -+ -+ -+ dict( -+ desc='Delete test_all_flag', -+ command=('delegation_del', [u'test_all_flag'], {}), -+ expected=dict( -+ result=True, -+ value=u'test_all_flag', -+ summary=u'Deleted delegation "test_all_flag"', -+ ) -+ ), -+ -+ -+ dict( -+ desc='Delete test_raw_flag', -+ command=('delegation_del', [u'test_raw_flag'], {}), -+ expected=dict( -+ result=True, -+ value=u'test_raw_flag', -+ summary=u'Deleted delegation "test_raw_flag"', -+ ) -+ ), -+ -+ -+ dict( -+ desc='Create delegation for ipausers group', -+ command=( -+ 'delegation_add', [u'delegation_del_positive_1001'], dict( -+ attrs=[u'mobile'], -+ group=u'ipausers', -+ memberof=u'admins', -+ ) -+ ), -+ expected=dict( -+ value=u'delegation_del_positive_1001', -+ summary=u'Added delegation "delegation_del_positive_1001"', -+ result=dict( -+ attrs=[u'mobile'], -+ permissions=[u'write'], -+ aciname=u'delegation_del_positive_1001', -+ group=u'ipausers', -+ memberof=member1, -+ ), -+ ), -+ ), -+ -+ -+ dict( -+ desc='Delete delegation_del_positive_1001', -+ command=('delegation_del', [u'delegation_del_positive_1001'], {}), -+ expected=dict( -+ result=True, -+ value=u'delegation_del_positive_1001', -+ summary=u'Deleted delegation "delegation_del_positive_1001"', -+ ) -+ ), -+ -+ -+ dict( -+ desc='Create delegation for find, show, and mod tests', -+ command=( -+ 'delegation_add', [u'delegation_find_show_mod_test'], dict( -+ attrs=[u'mobile'], -+ permissions=u'write', -+ group=u'editors', -+ memberof=u'admins', -+ ) -+ ), -+ expected=dict( -+ value=u'delegation_find_show_mod_test', -+ summary=u'Added delegation "delegation_find_show_mod_test"', -+ result=dict( -+ attrs=[u'mobile'], -+ permissions=[u'write'], -+ aciname=u'delegation_find_show_mod_test', -+ group=u'editors', -+ memberof=member1, -+ ), -+ ), -+ ), -+ -+ -+ dict( -+ desc='Find delegation by name', -+ command=('delegation_find', [u'delegation_find_show_mod_test'], {}), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary=u'1 delegation matched', -+ result=[ -+ { -+ 'attrs': [u'mobile'], -+ 'permissions': [u'write'], -+ 'aciname': u'delegation_find_show_mod_test', -+ 'group': u'editors', -+ 'memberof': member1, -+ }, -+ ], -+ ), -+ ), -+ -+ -+ dict( -+ desc='Find delegation by membergroup', -+ command=('delegation_find', [], {'memberof': member1}), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary=u'1 delegation matched', -+ result=[ -+ { -+ 'attrs': [u'mobile'], -+ 'permissions': [u'write'], -+ 'aciname': u'delegation_find_show_mod_test', -+ 'group': u'editors', -+ 'memberof': member1, -+ }, -+ ], -+ ), -+ ), -+ -+ -+ dict( -+ desc='Show delegation by name', -+ command=('delegation_show', [u'delegation_find_show_mod_test'], {}), -+ expected=dict( -+ value=u'delegation_find_show_mod_test', -+ summary=None, -+ result={ -+ 'attrs': [u'mobile'], -+ 'permissions': [u'write'], -+ 'aciname': u'delegation_find_show_mod_test', -+ 'group': u'editors', -+ 'memberof': member1, -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Modify delegation attrs', -+ command=( -+ 'delegation_mod', [u'delegation_find_show_mod_test'], -+ dict(attrs=[u'l']) -+ ), -+ expected=dict( -+ value=u'delegation_find_show_mod_test', -+ summary=u'Modified delegation "delegation_find_show_mod_test"', -+ result=dict( -+ attrs=[u'l'], -+ permissions=[u'write'], -+ aciname=u'delegation_find_show_mod_test', -+ group=u'editors', -+ memberof=member1, -+ ), -+ ), -+ ), -+ -+ -+ dict( -+ desc='Modify delegation permissions', -+ command=( -+ 'delegation_mod', [u'delegation_find_show_mod_test'], -+ dict(permissions=u'read') -+ ), -+ expected=dict( -+ value=u'delegation_find_show_mod_test', -+ summary=u'Modified delegation "delegation_find_show_mod_test"', -+ result=dict( -+ attrs=[u'l'], -+ permissions=[u'read'], -+ aciname=u'delegation_find_show_mod_test', -+ group=u'editors', -+ memberof=member1, -+ ), -+ ), -+ ), -+ -+ -+ dict( -+ desc='Delete delegation_find_show_mod_test', -+ command=('delegation_del', [u'delegation_find_show_mod_test'], {}), -+ expected=dict( -+ result=True, -+ value=u'delegation_find_show_mod_test', -+ summary=u'Deleted delegation "delegation_find_show_mod_test"', -+ ) -+ ), -+ -+ -+ dict( -+ desc='Create delegation for BZ tests', -+ command=( -+ 'delegation_add', [u'delegation_bz_test'], dict( -+ attrs=[u'mobile'], -+ permissions=u'write', -+ group=u'ipausers', -+ memberof=u'admins', -+ ) -+ ), -+ expected=dict( -+ value=u'delegation_bz_test', -+ summary=u'Added delegation "delegation_bz_test"', -+ result=dict( -+ attrs=[u'mobile'], -+ permissions=[u'write'], -+ aciname=u'delegation_bz_test', -+ group=u'ipausers', -+ memberof=member1, -+ ), -+ ), -+ ), -+ -+ -+ dict( -+ desc='Try to modify with non-existent membergroup (BZ 783548)', -+ command=( -+ 'delegation_mod', [u'delegation_bz_test'], -+ dict(memberof=u'badmembergroup') -+ ), -+ expected=errors.NotFound( -+ reason=u'badmembergroup: group not found'), -+ ), -+ -+ -+ dict( -+ desc='Try to modify attrs with empty value (BZ 783554)', -+ command=( -+ 'delegation_mod', [u'delegation_bz_test'], dict(attrs=u'') -+ ), -+ expected=errors.RequirementError(name='attrs'), -+ ), -+ -+ -+ dict( -+ desc='Modify attrs to prepare for next BZ test', -+ command=( -+ 'delegation_mod', [u'delegation_bz_test'], dict(attrs=[u'l']) -+ ), -+ expected=dict( -+ value=u'delegation_bz_test', -+ summary=u'Modified delegation "delegation_bz_test"', -+ result=dict( -+ attrs=[u'l'], -+ permissions=[u'write'], -+ aciname=u'delegation_bz_test', -+ group=u'ipausers', -+ memberof=member1, -+ ), -+ ), -+ ), -+ -+ -+ dict( -+ desc='Find delegation by group filter (BZ 888524)', -+ command=('delegation_find', [], {'group': u'ipausers'}), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary=u'1 delegation matched', -+ result=[ -+ { -+ 'attrs': [u'l'], -+ 'permissions': [u'write'], -+ 'aciname': u'delegation_bz_test', -+ 'group': u'ipausers', -+ 'memberof': member1, -+ }, -+ ], -+ ), -+ ), -+ -+ -+ dict( -+ desc='Delete delegation_bz_test', -+ command=('delegation_del', [u'delegation_bz_test'], {}), -+ expected=dict( -+ result=True, -+ value=u'delegation_bz_test', -+ summary=u'Deleted delegation "delegation_bz_test"', -+ ) -+ ), -+ - ] --- -2.52.0 - diff --git a/0019-ipa-join-initialize-pointer.patch b/0019-ipa-join-initialize-pointer.patch deleted file mode 100644 index edf13e6..0000000 --- a/0019-ipa-join-initialize-pointer.patch +++ /dev/null @@ -1,49 +0,0 @@ -From 7cc96e42683a6d3ec9f2dc2a19e99330b6f3ce58 Mon Sep 17 00:00:00 2001 -From: Florence Blanc-Renaud -Date: Wed, 4 Feb 2026 09:21:14 +0100 -Subject: [PATCH] ipa-join: initialize pointer - -OpenScanHub detected an uninitialized pointer in ipa_join: -Slapi_DN *sdn; -... -if (sdn) slapi_sdn_free(&sdn); - -Initialize to NULL -Also initialize Slapi_Backend *be=NULL and char * filter=NULL -to avoid potential issues. - -Fixes: https://pagure.io/freeipa/issue/9936 -Signed-off-by: Florence Blanc-Renaud -Reviewed-By: Rob Crittenden -Reviewed-By: Rafael Guterres Jeffman ---- - daemons/ipa-slapi-plugins/ipa-enrollment/ipa_enrollment.c | 6 +++--- - 1 file changed, 3 insertions(+), 3 deletions(-) - -diff --git a/daemons/ipa-slapi-plugins/ipa-enrollment/ipa_enrollment.c b/daemons/ipa-slapi-plugins/ipa-enrollment/ipa_enrollment.c -index 3a70dd0a5594fc623e7e808ab8a734349a748a49..2f8923e10310a8a6e19ac701070d6451915c3be3 100644 ---- a/daemons/ipa-slapi-plugins/ipa-enrollment/ipa_enrollment.c -+++ b/daemons/ipa-slapi-plugins/ipa-enrollment/ipa_enrollment.c -@@ -129,8 +129,8 @@ ipa_join(Slapi_PBlock *pb) - Slapi_PBlock *pbte = NULL; - Slapi_PBlock *pbtm = NULL; - Slapi_Entry *targetEntry=NULL; -- Slapi_DN *sdn; -- Slapi_Backend *be; -+ Slapi_DN *sdn=NULL; -+ Slapi_Backend *be=NULL; - Slapi_Entry **es = NULL; - int rc=0, ret=0, res; - size_t i; -@@ -139,7 +139,7 @@ ipa_join(Slapi_PBlock *pb) - char *fqdn = NULL; - Slapi_Mods *smods = NULL; - char *attrlist[] = {"fqdn", "krbPrincipalKey", "krbLastPwdChange", "krbPrincipalName", NULL }; -- char * filter; -+ char * filter=NULL; - - int scope = LDAP_SCOPE_SUBTREE; - char *principal = NULL; --- -2.52.0 - diff --git a/0020-ipatests-pruning-is-enabled-when-RSN-is-enabled.patch b/0020-ipatests-pruning-is-enabled-when-RSN-is-enabled.patch deleted file mode 100644 index 88a5d7d..0000000 --- a/0020-ipatests-pruning-is-enabled-when-RSN-is-enabled.patch +++ /dev/null @@ -1,39 +0,0 @@ -From d5efb4decb74b50c05b1d252add1c075e660d154 Mon Sep 17 00:00:00 2001 -From: Florence Blanc-Renaud -Date: Wed, 11 Feb 2026 10:23:36 +0100 -Subject: [PATCH] ipatests: pruning is enabled when RSN is enabled - -The test TestACMEPrune installs the server with --random-serial-numbers -but expects pruning to be disabled if the 389ds backend is BDB. - -This is a wrong expectation as pruning is enabled as soon as RSN -are enabled (since commit 3777d2b). -Fix the test expectation. - -Signed-off-by: Florence Blanc-Renaud ---- - ipatests/test_integration/test_acme.py | 8 ++------ - 1 file changed, 2 insertions(+), 6 deletions(-) - -diff --git a/ipatests/test_integration/test_acme.py b/ipatests/test_integration/test_acme.py -index 4c66e4348beeaca95577a786a46e53fdc1532ef7..bedec9d1f9f0c168c11aceb155978b6a0dae8dd7 100644 ---- a/ipatests/test_integration/test_acme.py -+++ b/ipatests/test_integration/test_acme.py -@@ -718,12 +718,8 @@ class TestACMEPrune(IntegrationTest): - < tasks.parse_version('11.3.0')): - raise pytest.skip("Certificate pruning is not available") - -- # Pruning is enabled by default when the host supports lmdb -- if get_389ds_backend(self.master) == 'bdb': -- cs_cfg = self.master.get_file_contents(paths.CA_CS_CFG_PATH) -- assert "jobsScheduler.job.pruning.enabled=false".encode() in cs_cfg -- self.master.run_command(['ipa-acme-manage', 'pruning', '--enable']) -- -+ # Pruning is enabled by default when server is installed -+ # with --random-serial-numbers - cs_cfg = self.master.get_file_contents(paths.CA_CS_CFG_PATH) - assert "jobsScheduler.enabled=true".encode() in cs_cfg - assert "jobsScheduler.job.pruning.enabled=true".encode() in cs_cfg --- -2.52.0 - diff --git a/0021-ipatests-Add-DNS-bugzilla-integration-tests.patch b/0021-ipatests-Add-DNS-bugzilla-integration-tests.patch deleted file mode 100644 index b2c5e90..0000000 --- a/0021-ipatests-Add-DNS-bugzilla-integration-tests.patch +++ /dev/null @@ -1,1627 +0,0 @@ -From ee33a646ac9ad4c2d028e7633c2091239a3c0d6b 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: Rafael Guterres Jeffman -Reviewed-By: David Hanina ---- - 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 47330d6d9..d9f142794 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 905227497..6840a10d1 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 bff4b40ae..3ab54674b 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/0022-Avoid-int-overflow-with-pwpolicy-minlife.patch b/0022-Avoid-int-overflow-with-pwpolicy-minlife.patch deleted file mode 100644 index 45bee9e..0000000 --- a/0022-Avoid-int-overflow-with-pwpolicy-minlife.patch +++ /dev/null @@ -1,154 +0,0 @@ -From 7b0ac4f3dd8febf0c22a11bf2db9b780a242e302 Mon Sep 17 00:00:00 2001 -From: Rob Crittenden -Date: Mon, 26 Jan 2026 19:25:17 -0500 -Subject: [PATCH] Avoid int overflow with pwpolicy minlife - -This converts the value to an unsigned integer instead and -sets the max value to match the maxlife (~54 years). - -A syslog log message was added to explain why the "too young" -message is returned - -Fixes: https://pagure.io/freeipa/issue/9929 - -Signed-off-by: Rob Crittenden -Reviewed-By: David Hanina -Reviewed-By: Rafael Guterres Jeffman ---- - .../ipa-slapi-plugins/ipa-pwd-extop/common.c | 2 +- - ipaserver/plugins/pwpolicy.py | 1 + - ipatests/test_integration/test_pwpolicy.py | 50 ++++++++++++++++--- - util/ipa_pwd.c | 1 + - util/ipa_pwd.h | 2 +- - 5 files changed, 48 insertions(+), 8 deletions(-) - -diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c -index 0e69f3410..71642540a 100644 ---- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c -+++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c -@@ -407,7 +407,7 @@ int ipapwd_getPolicy(const char *dn, - } - - /* read data out of policy object */ -- policy->min_pwd_life = slapi_entry_attr_get_int(pe, "krbMinPwdLife"); -+ policy->min_pwd_life = slapi_entry_attr_get_uint(pe, "krbMinPwdLife"); - - policy->max_pwd_life = slapi_entry_attr_get_int(pe, "krbMaxPwdLife"); - -diff --git a/ipaserver/plugins/pwpolicy.py b/ipaserver/plugins/pwpolicy.py -index e49a2e1cd..2d9907d9d 100644 ---- a/ipaserver/plugins/pwpolicy.py -+++ b/ipaserver/plugins/pwpolicy.py -@@ -338,6 +338,7 @@ class pwpolicy(LDAPObject): - label=_('Min lifetime (hours)'), - doc=_('Minimum password lifetime (in hours)'), - minvalue=0, -+ maxvalue=480000, # about 54 years, same as maxlife - ), - Int('krbpwdhistorylength?', - cli_name='history', -diff --git a/ipatests/test_integration/test_pwpolicy.py b/ipatests/test_integration/test_pwpolicy.py -index a627b66ce..f7fd40ada 100644 ---- a/ipatests/test_integration/test_pwpolicy.py -+++ b/ipatests/test_integration/test_pwpolicy.py -@@ -16,17 +16,14 @@ PASSWORD = 'Secret123' - POLICY = 'test' - - --class TestPWPolicy(IntegrationTest): -+class BasePWpolicy(IntegrationTest): - """ -- Test password policy in action. -+ Base class for testing password policies including libpwquality - """ -- num_replicas = 1 -- -- topology = 'line' - - @classmethod - def install(cls, mh): -- super(TestPWPolicy, cls).install(mh) -+ tasks.install_master(cls.master, setup_dns=True) - - tasks.kinit_admin(cls.master) - cls.master.run_command(['ipa', 'user-add', USER, -@@ -66,6 +63,15 @@ class TestPWPolicy(IntegrationTest): - host.run_command(['ipa', 'user-unlock', user]) - tasks.kdestroy_all(host) - -+ -+class TestPWquality(BasePWpolicy): -+ """ -+ libpwquality tests -+ """ -+ num_replicas = 1 -+ -+ topology = 'line' -+ - def set_pwpolicy(self, minlength=None, maxrepeat=None, maxsequence=None, - dictcheck=None, usercheck=None, minclasses=None, - dcredit=None, ucredit=None, lcredit=None, ocredit=None): -@@ -534,3 +540,35 @@ class TestPWPolicy(IntegrationTest): - self.master, dn, ['passwordgraceusertime',], - ) - assert 'passwordgraceusertime: 0' in result.stdout_text.lower() -+ -+ -+class TestPWpolicy(BasePWpolicy): -+ """ -+ Tests for original/Kerberos password policies. Excludes libpwquality -+ """ -+ -+ # NOTE: set/reset/clear methods to be added later once there is more -+ # than a single test. -+ -+ def test_minlife_overflow(self): -+ """Test that a large minlife doesn't overflow an unsigned int.""" -+ newpassword = "Secret.1234" -+ -+ tasks.kinit_admin(self.master) -+ -+ self.master.run_command( -+ ["ipa", "pwpolicy-mod", POLICY, "--minlife", "480000", -+ "--maxlife", "20000",] -+ ) -+ -+ self.kinit_as_user(self.master, PASSWORD, PASSWORD) -+ -+ result = self.master.run_command( -+ ["ipa", "passwd", USER], -+ raiseonerr=False, -+ stdin_text='{password}\n{password}\n{newpassword}'.format( -+ password=PASSWORD, newpassword=newpassword -+ )) -+ -+ assert result.returncode == 1 -+ assert "Too soon to change password" in result.stderr_text -diff --git a/util/ipa_pwd.c b/util/ipa_pwd.c -index ba6860106..56135aaff 100644 ---- a/util/ipa_pwd.c -+++ b/util/ipa_pwd.c -@@ -561,6 +561,7 @@ int ipapwd_check_policy(struct ipapwd_policy *policy, - * policy is set */ - - if (cur_time < last_pwd_change + policy->min_pwd_life) { -+ syslog(LOG_ERR, "Password too young. %d seconds since last change, policy requires %u seconds.\n", (cur_time - last_pwd_change), policy->min_pwd_life); - return IPAPWD_POLICY_PWD_TOO_YOUNG; - } - } -diff --git a/util/ipa_pwd.h b/util/ipa_pwd.h -index aa2c6e978..d3c5f8be7 100644 ---- a/util/ipa_pwd.h -+++ b/util/ipa_pwd.h -@@ -54,7 +54,7 @@ enum ipapwd_error { - }; - - struct ipapwd_policy { -- int min_pwd_life; -+ unsigned int min_pwd_life; - int max_pwd_life; - int min_pwd_length; - int history_length; --- -2.52.0 - diff --git a/0023-ipatests-fix-install-method-for-BasePWpolicy.patch b/0023-ipatests-fix-install-method-for-BasePWpolicy.patch deleted file mode 100644 index e4a0aaa..0000000 --- a/0023-ipatests-fix-install-method-for-BasePWpolicy.patch +++ /dev/null @@ -1,42 +0,0 @@ -From 5cd2639f539ce220c291b00afafa72fd35e1d07e Mon Sep 17 00:00:00 2001 -From: Florence Blanc-Renaud -Date: Mon, 16 Feb 2026 15:47:32 +0100 -Subject: [PATCH] ipatests: fix install method for BasePWpolicy - -The test was broken by the previous commit, which creates a -new test class TestPWquality that inherits from BasePWpolicy. - -As BasePWpolicy overrides the install method with -task.install_master instead of super(TestPWPolicy, cls).install(mh), -only the master gets installed. - -Fix the BasePWpolicy class. - -Fixes: https://pagure.io/freeipa/issue/9946 -Signed-off-by: Florence Blanc-Renaud -Reviewed-By: David Hanina ---- - ipatests/test_integration/test_pwpolicy.py | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) - -diff --git a/ipatests/test_integration/test_pwpolicy.py b/ipatests/test_integration/test_pwpolicy.py -index f7fd40ada..a68252bf9 100644 ---- a/ipatests/test_integration/test_pwpolicy.py -+++ b/ipatests/test_integration/test_pwpolicy.py -@@ -21,9 +21,12 @@ class BasePWpolicy(IntegrationTest): - Base class for testing password policies including libpwquality - """ - -+ num_replicas = 0 -+ topology = 'line' -+ - @classmethod - def install(cls, mh): -- tasks.install_master(cls.master, setup_dns=True) -+ super(BasePWpolicy, cls).install(mh) - - tasks.kinit_admin(cls.master) - cls.master.run_command(['ipa', 'user-add', USER, --- -2.52.0 - diff --git a/0024-webui-tests-update-expected-max-value-for-krbminpwdl.patch b/0024-webui-tests-update-expected-max-value-for-krbminpwdl.patch deleted file mode 100644 index 6c724e7..0000000 --- a/0024-webui-tests-update-expected-max-value-for-krbminpwdl.patch +++ /dev/null @@ -1,36 +0,0 @@ -From 8a7486bc980b7f9b19e6b15dee6642679198c40f Mon Sep 17 00:00:00 2001 -From: Florence Blanc-Renaud -Date: Mon, 16 Feb 2026 16:18:06 +0100 -Subject: [PATCH] webui tests: update expected max value for - krbminpwdlife - -The Maximum allowed value for krbminpwdlife has been changed -by a recent commit to 480000. -Update the test to be consistent. - -Fixes: https://pagure.io/freeipa/issue/9947 -Signed-off-by: Florence Blanc-Renaud -Reviewed-By: Carla Martinez -Reviewed-By: David Hanina ---- - ipatests/test_webui/test_pwpolicy.py | 4 ++++ - 1 file changed, 4 insertions(+) - -diff --git a/ipatests/test_webui/test_pwpolicy.py b/ipatests/test_webui/test_pwpolicy.py -index 74484ab1b..d84bcea41 100644 ---- a/ipatests/test_webui/test_pwpolicy.py -+++ b/ipatests/test_webui/test_pwpolicy.py -@@ -151,6 +151,10 @@ class test_pwpolicy(UI_driver): - elif field == 'krbpwdmindiffchars': - self.check_expected_error(field, 'Maximum value is 5', - maximum_value) -+ # verifying if field value is more than 480000 -+ elif field == 'krbminpwdlife': -+ self.check_expected_error(field, 'Maximum value is 480000', -+ maximum_value) - # verifying if field value is more than 2147483647 - else: - self.check_expected_error(field, 'Maximum value is 2147483647', --- -2.52.0 - diff --git a/0025-ipatests-Add-DNS-integration-tests.patch b/0025-ipatests-Add-DNS-integration-tests.patch deleted file mode 100644 index 25f3729..0000000 --- a/0025-ipatests-Add-DNS-integration-tests.patch +++ /dev/null @@ -1,1123 +0,0 @@ -From 69f9978dc8bfe33b0f02cfb62a7d9d6a6051657c 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: Florence Blanc-Renaud -Reviewed-By: David Hanina -Reviewed-By: Sudhir Menon ---- - 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 6840a10d1..4b9ab1fe8 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/0026-ipatest-make-tests-compatible-with-Pytest-9.patch b/0026-ipatest-make-tests-compatible-with-Pytest-9.patch deleted file mode 100644 index 1dae595..0000000 --- a/0026-ipatest-make-tests-compatible-with-Pytest-9.patch +++ /dev/null @@ -1,42 +0,0 @@ -From 0bf2a549a8a858c393ef59487bc1d395e5535c07 Mon Sep 17 00:00:00 2001 -From: Stanislav Levin -Date: Tue, 24 Feb 2026 19:44:51 +0300 -Subject: [PATCH] ipatest: make tests compatible with Pytest 9 - -https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function - -> Applying a mark to a fixture function never had any effect, but it is a common user error. - -Move marks from the fixture to test class. - -Fixes: https://pagure.io/freeipa/issue/9950 -Signed-off-by: Stanislav Levin -Reviewed-By: Florence Blanc-Renaud ---- - ipatests/test_ipapython/test_ldap_cache.py | 3 +-- - 1 file changed, 1 insertion(+), 2 deletions(-) - -diff --git a/ipatests/test_ipapython/test_ldap_cache.py b/ipatests/test_ipapython/test_ldap_cache.py -index c960db027..49724ad2f 100644 ---- a/ipatests/test_ipapython/test_ldap_cache.py -+++ b/ipatests/test_ipapython/test_ldap_cache.py -@@ -20,8 +20,6 @@ def hits_and_misses(cache, hits, misses): - - - @pytest.fixture(scope='class') --@pytest.mark.tier1 --@pytest.mark.needs_ipaapi - def class_cache(request): - cache = ipaldap.LDAPCache(api.env.ldap_uri) - hits_and_misses(cache, 0, 0) -@@ -56,6 +54,7 @@ def class_cache(request): - @pytest.mark.usefixtures('class_cache') - @pytest.mark.skip_ipaclient_unittest - @pytest.mark.needs_ipaapi -+@pytest.mark.tier1 - class TestLDAPCache: - - def test_one(self): --- -2.52.0 - diff --git a/0027-ipatests-Add-ipa-selfservice-BZ-tests-to-xmlrpc.patch b/0027-ipatests-Add-ipa-selfservice-BZ-tests-to-xmlrpc.patch deleted file mode 100644 index 7b18408..0000000 --- a/0027-ipatests-Add-ipa-selfservice-BZ-tests-to-xmlrpc.patch +++ /dev/null @@ -1,154 +0,0 @@ -From f60df430602db1e3949fa67f3744d40bd9b1d971 Mon Sep 17 00:00:00 2001 -From: Jay Gondaliya -Date: Fri, 27 Feb 2026 15:38:54 +0530 -Subject: [PATCH] ipatests: Add ipa-selfservice BZ tests to xmlrpc -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - --Convert Bugzilla regression tests (BZ 772106, 772675, 747730, 747741, 747720, 747722) from bash to Python and add them to the existing selfservice test file as the TestSelfserviceMisc Declarative class. --Tests verify that --raw output, empty permissions/attrs, and invalid attrs do not cause internal errors or accidental ACI deletion. --Use a single selfservice rule (selfservice1) across all BZ tests instead of creating and deleting a separate rule per test case, reducing churn and keeping the tests fast. --Drop BZ 747693 (selfservice-find --raw) as it is already covered by the existing "Search for 'testself' with --raw" test in the main test_selfservice CRUD class (test 0011). - -Signed-off-by: Jay Gondaliya jgondali@redhat.com -Fixes: https://pagure.io/freeipa/issue/9945 -Assisted-by: Claude noreply@anthropic.com - -Continuation of PR #8190 - -Fixes made: --Fixed lambda expected checkers — replaced defensive .get("result", {}) chains with direct output["result"] key access. --Removed redundant delete test case — dropped explicit selfservice_del test, relying solely on cleanup_commands. --Renamed class TestSelfserviceMisc → test_selfservice_misc. - -Reviewed-By: Rob Crittenden -Reviewed-By: David Hanina ---- - .../test_xmlrpc/test_selfservice_plugin.py | 106 +++++++++++++++++- - 1 file changed, 105 insertions(+), 1 deletion(-) - -diff --git a/ipatests/test_xmlrpc/test_selfservice_plugin.py b/ipatests/test_xmlrpc/test_selfservice_plugin.py -index 02aea35e1..4c5059519 100644 ---- a/ipatests/test_xmlrpc/test_selfservice_plugin.py -+++ b/ipatests/test_xmlrpc/test_selfservice_plugin.py -@@ -28,7 +28,6 @@ import pytest - selfservice1 = u'testself' - invalid_selfservice1 = u'bad+name' - -- - @pytest.mark.tier1 - class test_selfservice(Declarative): - -@@ -290,3 +289,108 @@ class test_selfservice(Declarative): - ), - - ] -+ -+ -+@pytest.mark.tier1 -+class test_selfservice_misc(Declarative): -+ """Bugzilla regression tests for selfservice plugin.""" -+ -+ cleanup_commands = [ -+ ("selfservice_del", [selfservice1], {}), -+ ] -+ -+ tests = [ -+ # BZ 772106: selfservice-add with --raw must not return internal error -+ dict( -+ desc="Create %r with --raw for BZ 772106" % selfservice1, -+ command=( -+ "selfservice_add", -+ [selfservice1], -+ dict(attrs=["l"], raw=True), -+ ), -+ expected=dict( -+ value=selfservice1, -+ summary='Added selfservice "%s"' % selfservice1, -+ result={ -+ "aci": '(targetattr = "l")(version 3.0;acl ' -+ '"selfservice:%s";allow (write) ' -+ 'userdn = "ldap:///self";)' % selfservice1, -+ }, -+ ), -+ ), -+ # BZ 772675: selfservice-mod with --raw must not return internal error -+ dict( -+ desc="Modify %r with --raw for BZ 772675" % selfservice1, -+ command=( -+ "selfservice_mod", -+ [selfservice1], -+ dict(attrs=["mobile"], raw=True), -+ ), -+ expected=dict( -+ value=selfservice1, -+ summary='Modified selfservice "%s"' % selfservice1, -+ result={ -+ "aci": '(targetattr = "mobile")(version 3.0;acl ' -+ '"selfservice:%s";allow (write) ' -+ 'userdn = "ldap:///self";)' % selfservice1, -+ }, -+ ), -+ ), -+ # BZ 747730: selfservice-mod --permissions="" must not delete the entry -+ dict( -+ desc=( -+ "Modify %r with empty permissions for BZ 747730" -+ % selfservice1 -+ ), -+ command=( -+ "selfservice_mod", -+ [selfservice1], -+ dict(permissions=""), -+ ), -+ expected=lambda got, output: True, -+ ), -+ dict( -+ desc="Verify %r still exists after BZ 747730" % selfservice1, -+ command=("selfservice_show", [selfservice1], {}), -+ expected=lambda got, output: ( -+ got is None -+ and output["result"]["aciname"] == selfservice1 -+ ), -+ ), -+ # BZ 747741: selfservice-mod --attrs=badattrs must not delete the entry -+ dict( -+ desc="Modify %r with bad attrs for BZ 747741" % selfservice1, -+ command=( -+ "selfservice_mod", -+ [selfservice1], -+ dict(attrs=["badattrs"]), -+ ), -+ expected=lambda got, output: True, -+ ), -+ dict( -+ desc="Verify %r still exists after BZ 747741" % selfservice1, -+ command=("selfservice_show", [selfservice1], {}), -+ expected=lambda got, output: ( -+ got is None -+ and output["result"]["aciname"] == selfservice1 -+ ), -+ ), -+ # BZ 747720: selfservice-find --permissions="" must not return -+ # internal error -+ dict( -+ desc="BZ 747720: selfservice-find with empty permissions", -+ command=("selfservice_find", [], dict(permissions="")), -+ expected=lambda got, output: ( -+ got is None and isinstance(output["result"], (list, tuple)) -+ ), -+ ), -+ # BZ 747722: selfservice-find --attrs="" must not return -+ # internal error -+ dict( -+ desc="BZ 747722: selfservice-find with empty attrs", -+ command=("selfservice_find", [], dict(attrs="")), -+ expected=lambda got, output: ( -+ got is None and isinstance(output["result"], (list, tuple)) -+ ), -+ ), -+ ] --- -2.52.0 - diff --git a/0028-Allow-32bit-gid.patch b/0028-Allow-32bit-gid.patch deleted file mode 100644 index 67495f4..0000000 --- a/0028-Allow-32bit-gid.patch +++ /dev/null @@ -1,37 +0,0 @@ -From 58239f9fbe33408b5cb5c52ba5132f6deb6b8f40 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: Florence Blanc-Renaud ---- - ipaserver/plugins/group.py | 2 ++ - 1 file changed, 2 insertions(+) - -diff --git a/ipaserver/plugins/group.py b/ipaserver/plugins/group.py -index f05a39f69..308e5458c 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/0029-ipatests-Add-ipa-selfservice-users-tests-to-xmlrpc.patch b/0029-ipatests-Add-ipa-selfservice-users-tests-to-xmlrpc.patch deleted file mode 100644 index aa25ae3..0000000 --- a/0029-ipatests-Add-ipa-selfservice-users-tests-to-xmlrpc.patch +++ /dev/null @@ -1,400 +0,0 @@ -From ffae7d109b9cc968bb9942da78ea4238eee5497b Mon Sep 17 00:00:00 2001 -From: Jay Gondaliya -Date: Thu, 5 Mar 2026 19:07:06 +0530 -Subject: [PATCH] ipatests: Add ipa-selfservice users tests to xmlrpc - -Convert self-service users tests from bash to Python and add them to the existing selfservice test file. - -Tests verify that users can modify their own allowed attributes under default and custom selfservice rules, -that disallowed attributes are rejected with ACIError, and that cross-user modification is blocked. -Also covers atomic failure on mixed permissions, self-password-change, and user-find by phone, fax, and manager -(BZ 1188195, 781208, 985016, 967509, 985013). - -Signed-off-by: Jay Gondaliya jgondali@redhat.com -Fixes: https://pagure.io/freeipa/issue/9945 -Assisted-by: Claude noreply@anthropic.com -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: David Hanina -Reviewed-By: PRANAV THUBE ---- - .../test_xmlrpc/test_selfservice_plugin.py | 360 +++++++++++++++++- - 1 file changed, 358 insertions(+), 2 deletions(-) - -diff --git a/ipatests/test_xmlrpc/test_selfservice_plugin.py b/ipatests/test_xmlrpc/test_selfservice_plugin.py -index 4c5059519..e55502a2d 100644 ---- a/ipatests/test_xmlrpc/test_selfservice_plugin.py -+++ b/ipatests/test_xmlrpc/test_selfservice_plugin.py -@@ -21,8 +21,12 @@ - Test the `ipaserver/plugins/selfservice.py` module. - """ - --from ipalib import errors --from ipatests.test_xmlrpc.xmlrpc_test import Declarative -+from ipalib import api, errors -+from ipatests.test_xmlrpc.xmlrpc_test import ( -+ Declarative, XMLRPC_test, assert_attr_equal, -+) -+from ipatests.test_xmlrpc.tracker.user_plugin import UserTracker -+from ipatests.util import change_principal, unlock_principal_password - import pytest - - selfservice1 = u'testself' -@@ -394,3 +398,355 @@ class test_selfservice_misc(Declarative): - ), - ), - ] -+ -+ -+SS_USER1 = 'ssuser0001' -+SS_USER1_PASSWORD = 'Passw0rd1' -+SS_USER2 = 'ssuser0002' -+SS_USER2_PASSWORD = 'Passw0rd2' -+SS_GOOD_MANAGER = 'ss_good_manager' -+SS_GOOD_MANAGER_PASSWORD = 'Passw0rd3' -+ -+SS_DEFAULT_SELFSERVICE = 'User Self service' -+SS_CUSTOM_RULE = 'ss_test_rule0001' -+ -+SS_DEFAULT_SELFSERVICE_ATTRS = [ -+ 'givenname', 'sn', 'cn', 'displayname', 'title', 'initials', -+ 'loginshell', 'gecos', 'homephone', 'mobile', 'pager', -+ 'facsimiletelephonenumber', 'telephonenumber', 'street', -+ 'roomnumber', 'l', 'st', 'postalcode', 'manager', 'secretary', -+ 'description', 'carlicense', 'labeleduri', 'inetuserhttpurl', -+ 'seealso', 'employeetype', 'businesscategory', 'ou', -+] -+ -+SS_CUSTOM_RULE_ATTRS = [ -+ 'mobile', 'pager', -+ 'facsimiletelephonenumber', 'telephonenumber', -+] -+ -+ -+def _safe_del_selfservice(name): -+ """Delete a selfservice rule, ignoring NotFound.""" -+ try: -+ api.Command['selfservice_del'](name) -+ except errors.NotFound: -+ pass -+ -+ -+@pytest.fixture -+def custom_selfservice_rule(xmlrpc_setup): -+ """Replace the default selfservice rule with the narrow custom rule.""" -+ api.Command['selfservice_del'](SS_DEFAULT_SELFSERVICE) -+ api.Command['selfservice_add']( -+ SS_CUSTOM_RULE, attrs=SS_CUSTOM_RULE_ATTRS, -+ ) -+ yield -+ _safe_del_selfservice(SS_CUSTOM_RULE) -+ api.Command['selfservice_add']( -+ SS_DEFAULT_SELFSERVICE, attrs=SS_DEFAULT_SELFSERVICE_ATTRS, -+ ) -+ -+ -+@pytest.fixture(scope='class') -+def ss_user1(request, xmlrpc_setup): -+ tracker = UserTracker( -+ name=SS_USER1, givenname='Test', sn='User0001', -+ userpassword=SS_USER1_PASSWORD, -+ ) -+ tracker.make_fixture(request) -+ tracker.make_create_command()() -+ tracker.exists = True -+ unlock_principal_password( -+ SS_USER1, SS_USER1_PASSWORD, SS_USER1_PASSWORD, -+ ) -+ return tracker -+ -+ -+@pytest.fixture(scope='class') -+def ss_user2(request, xmlrpc_setup): -+ tracker = UserTracker( -+ name=SS_USER2, givenname='Test', sn='User0002', -+ userpassword=SS_USER2_PASSWORD, -+ ) -+ tracker.make_fixture(request) -+ tracker.make_create_command()() -+ tracker.exists = True -+ unlock_principal_password( -+ SS_USER2, SS_USER2_PASSWORD, SS_USER2_PASSWORD, -+ ) -+ return tracker -+ -+ -+@pytest.fixture(scope='class') -+def ss_good_manager(request, xmlrpc_setup): -+ tracker = UserTracker( -+ name=SS_GOOD_MANAGER, givenname='Good', sn='Manager', -+ userpassword=SS_GOOD_MANAGER_PASSWORD, -+ ) -+ tracker.make_fixture(request) -+ tracker.make_create_command()() -+ tracker.exists = True -+ unlock_principal_password( -+ SS_GOOD_MANAGER, SS_GOOD_MANAGER_PASSWORD, SS_GOOD_MANAGER_PASSWORD, -+ ) -+ return tracker -+ -+ -+@pytest.mark.tier1 -+@pytest.mark.usefixtures('ss_user1', 'ss_user2', 'ss_good_manager') -+class test_selfservice_users(XMLRPC_test): -+ """Test self-service user attribute modification permissions.""" -+ -+ # usertest_1001: Set all attrs allowed by default self-service rule. -+ def test_set_all_default_selfservice_attrs(self): -+ """Set all attrs allowed by the default self-service rule.""" -+ attrs = { -+ 'givenname': 'Good', -+ 'sn': 'User', -+ 'cn': 'gooduser', -+ 'displayname': 'gooduser', -+ 'initials': 'GU', -+ 'gecos': 'gooduser@good.example.com', -+ 'loginshell': '/bin/bash', -+ 'street': 'Good_Street_Rd', -+ 'l': 'Good_City', -+ 'st': 'Goodstate', -+ 'postalcode': '33333', -+ 'telephonenumber': '333-333-3333', -+ 'mobile': '333-333-3333', -+ 'pager': '333-333-3333', -+ 'facsimiletelephonenumber': '333-333-3333', -+ 'ou': 'good-org', -+ 'title': 'good_admin', -+ 'manager': SS_GOOD_MANAGER, -+ 'carlicense': 'good-3333', -+ } -+ -+ with change_principal(SS_USER1, SS_USER1_PASSWORD): -+ for attr, value in attrs.items(): -+ api.Command['user_mod'](SS_USER1, **{attr: value}) -+ -+ entry = api.Command['user_show'](SS_USER1, all=True)['result'] -+ for attr, value in attrs.items(): -+ assert_attr_equal(entry, attr, value) -+ -+ # usertest_1002: Test that default disallowed attributes are rejected. -+ def test_reject_uidnumber_by_default(self): -+ """uidnumber change is rejected by default.""" -+ with change_principal(SS_USER1, SS_USER1_PASSWORD): -+ with pytest.raises(errors.ACIError): -+ api.Command['user_mod'](SS_USER1, uidnumber=9999) -+ -+ def test_reject_gidnumber_by_default(self): -+ """gidnumber change is rejected by default.""" -+ with change_principal(SS_USER1, SS_USER1_PASSWORD): -+ with pytest.raises(errors.ACIError): -+ api.Command['user_mod'](SS_USER1, gidnumber=9999) -+ -+ def test_reject_homedirectory_by_default(self): -+ """homedirectory change is rejected by default.""" -+ with change_principal(SS_USER1, SS_USER1_PASSWORD): -+ with pytest.raises(errors.ACIError): -+ api.Command['user_mod']( -+ SS_USER1, homedirectory='/home/gooduser') -+ -+ def test_reject_email_by_default(self): -+ """email change is rejected by default.""" -+ with change_principal(SS_USER1, SS_USER1_PASSWORD): -+ with pytest.raises(errors.ACIError): -+ api.Command['user_mod']( -+ SS_USER1, mail='gooduser@good.example.com') -+ -+ # usertest_1003: All attrs rejected when the default rule is deleted. -+ def test_all_attrs_rejected_without_default_rule(self): -+ """All attrs are rejected when the default rule is deleted.""" -+ attrs = { -+ 'givenname': 'Bad', -+ 'sn': 'LUser', -+ 'cn': 'badluser', -+ 'displayname': 'badluser', -+ 'initials': 'BL', -+ 'gecos': 'badluser@bad.example.com', -+ 'loginshell': '/bin/tcsh', -+ 'street': 'Bad_Street_Av', -+ 'l': 'Bad_City', -+ 'st': 'Badstate', -+ 'postalcode': '99999', -+ 'telephonenumber': '999-999-9999', -+ 'mobile': '999-999-9999', -+ 'pager': '999-999-9999', -+ 'facsimiletelephonenumber': '999-999-9999', -+ 'ou': 'bad-org', -+ 'title': 'bad_admin', -+ 'manager': 'admin', -+ 'carlicense': 'bad-9999', -+ } -+ -+ api.Command['selfservice_del'](SS_DEFAULT_SELFSERVICE) -+ try: -+ with change_principal(SS_USER1, SS_USER1_PASSWORD): -+ for attr, value in attrs.items(): -+ with pytest.raises(errors.ACIError): -+ api.Command['user_mod'](SS_USER1, **{attr: value}) -+ finally: -+ api.Command['selfservice_add']( -+ SS_DEFAULT_SELFSERVICE, -+ attrs=SS_DEFAULT_SELFSERVICE_ATTRS, -+ ) -+ -+ # usertest_1004: Custom rule grants write access to its specified attrs. -+ def test_custom_rule_grants_write_access( -+ self, custom_selfservice_rule): -+ """Custom rule grants write access to its specified attrs.""" -+ with change_principal(SS_USER1, SS_USER1_PASSWORD): -+ api.Command['user_mod']( -+ SS_USER1, telephonenumber='777-777-7777') -+ api.Command['user_mod'](SS_USER1, mobile='777-777-7777') -+ api.Command['user_mod'](SS_USER1, pager='777-777-7777') -+ api.Command['user_mod']( -+ SS_USER1, -+ facsimiletelephonenumber='777-777-7777') -+ -+ # usertest_1005: Persisted attrs and user-find by phone, fax, manager. -+ def test_verify_persisted_attrs(self): -+ """Verify attrs set by previous tests are persisted.""" -+ expected = { -+ 'givenname': 'Good', -+ 'sn': 'User', -+ 'cn': 'gooduser', -+ 'displayname': 'gooduser', -+ 'initials': 'GU', -+ 'gecos': 'gooduser@good.example.com', -+ 'loginshell': '/bin/bash', -+ 'street': 'Good_Street_Rd', -+ 'l': 'Good_City', -+ 'st': 'Goodstate', -+ 'postalcode': '33333', -+ 'telephonenumber': '777-777-7777', -+ 'mobile': '777-777-7777', -+ 'pager': '777-777-7777', -+ 'facsimiletelephonenumber': '777-777-7777', -+ 'ou': 'good-org', -+ 'title': 'good_admin', -+ 'carlicense': 'good-3333', -+ } -+ -+ entry = api.Command['user_show'](SS_USER1, all=True)['result'] -+ for attr, value in expected.items(): -+ assert_attr_equal(entry, attr, value) -+ assert_attr_equal(entry, 'manager', SS_GOOD_MANAGER) -+ -+ def test_user_find_by_phone(self): -+ """BZ 1188195: user-find by phone number returns results.""" -+ result = api.Command['user_find']( -+ telephonenumber='777-777-7777') -+ assert result['count'] >= 1 -+ uids = [e['uid'][0] for e in result['result']] -+ assert SS_USER1 in uids -+ -+ def test_user_find_by_fax(self): -+ """BZ 1188195: user-find by fax number returns results.""" -+ result = api.Command['user_find']( -+ facsimiletelephonenumber='777-777-7777') -+ assert result['count'] >= 1 -+ uids = [e['uid'][0] for e in result['result']] -+ assert SS_USER1 in uids -+ -+ def test_user_find_by_manager(self): -+ """BZ 781208: user-find by manager returns matches.""" -+ result = api.Command['user_find']( -+ SS_USER1, manager=SS_GOOD_MANAGER) -+ assert result['count'] >= 1, ( -+ 'BZ 781208: user-find --manager did not find matches' -+ ) -+ uids = [e['uid'][0] for e in result['result']] -+ assert SS_USER1 in uids -+ -+ # usertest_1006: BZ 985016, 967509: user can modify an allowed attr. -+ def test_user_can_modify_allowed_attr(self): -+ """BZ 985016, 967509: user can modify an allowed attr.""" -+ with change_principal(SS_USER1, SS_USER1_PASSWORD): -+ api.Command['user_mod'](SS_USER1, mobile='888-888-8888') -+ entry = api.Command['user_show'](SS_USER1, all=True)['result'] -+ assert_attr_equal(entry, 'mobile', '888-888-8888') -+ -+ # usertest_1007: BZ 985016, 967509: disallowed attribute is rejected. -+ def test_disallowed_attr_rejected_with_custom_rule( -+ self, custom_selfservice_rule): -+ """BZ 985016, 967509: disallowed attribute is rejected.""" -+ with change_principal(SS_USER1, SS_USER1_PASSWORD): -+ with pytest.raises(errors.ACIError): -+ api.Command['user_mod'](SS_USER1, title='Dr') -+ -+ # usertest_1008: user-mod fails atomically on mixed attr permissions. -+ def test_user_mod_atomic_failure_mixed_perms( -+ self, custom_selfservice_rule): -+ """user-mod fails atomically when one attr is disallowed.""" -+ original_title = api.Command['user_show']( -+ SS_USER1)['result'].get('title') -+ with change_principal(SS_USER1, SS_USER1_PASSWORD): -+ with pytest.raises(errors.ACIError): -+ api.Command['user_mod']( -+ SS_USER1, -+ title='notgonnawork', -+ telephonenumber='999-999-9990', -+ ) -+ result = api.Command['user_find']( -+ SS_USER1, telephonenumber='999-999-9990') -+ assert result['count'] == 0, ( -+ 'Phone was changed despite disallowed title in same call' -+ ) -+ after = api.Command['user_show'](SS_USER1)['result'] -+ assert after.get('title') == original_title, ( -+ 'Title was modified despite being disallowed' -+ ) -+ -+ # usertest_1009: BZ 985013: user can change their own password. -+ def test_self_password_change_via_passwd(self): -+ """BZ 985013: user can change their own password via passwd.""" -+ policy = api.Command['pwpolicy_show']()['result'] -+ orig_minlife = policy.get('krbminpwdlife', ('1',))[0] -+ -+ api.Command['pwpolicy_mod'](krbminpwdlife=0) -+ try: -+ with change_principal(SS_USER1, SS_USER1_PASSWORD): -+ api.Command['passwd']( -+ SS_USER1, -+ password='MyN3wP@55', -+ current_password=SS_USER1_PASSWORD, -+ ) -+ # Reset password so the next test can authenticate -+ unlock_principal_password( -+ SS_USER1, 'MyN3wP@55', SS_USER1_PASSWORD, -+ ) -+ finally: -+ api.Command['pwpolicy_mod'](krbminpwdlife=int(orig_minlife)) -+ -+ def test_self_password_change_via_user_mod(self): -+ """BZ 985013: user can change their own password via user_mod.""" -+ policy = api.Command['pwpolicy_show']()['result'] -+ orig_minlife = policy.get('krbminpwdlife', ('1',))[0] -+ -+ api.Command['pwpolicy_mod'](krbminpwdlife=0) -+ try: -+ with change_principal(SS_USER1, SS_USER1_PASSWORD): -+ api.Command['user_mod']( -+ SS_USER1, -+ userpassword='MyN3wP@55', -+ ) -+ finally: -+ api.Command['pwpolicy_mod'](krbminpwdlife=int(orig_minlife)) -+ -+ # usertest_1010: User cannot modify another user's attributes. -+ def test_cross_user_modification_rejected(self): -+ """User cannot modify another user's attributes.""" -+ with change_principal(SS_USER2, SS_USER2_PASSWORD): -+ with pytest.raises(errors.ACIError): -+ api.Command['user_mod'](SS_USER1, mobile='867-5309') -+ -+ def test_verify_cross_user_modification_rejected(self): -+ """Verify attrs did not change after cross-user modification.""" -+ result = api.Command['user_find'](SS_USER1, mobile='867-5309') -+ assert result['count'] == 0, ( -+ 'Mobile was changed by a different user' -+ ) --- -2.52.0 - diff --git a/0030-ipatests-Fix-test_allow_query_transfer_ipv6-when-IPv.patch b/0030-ipatests-Fix-test_allow_query_transfer_ipv6-when-IPv.patch deleted file mode 100644 index 5370afd..0000000 --- a/0030-ipatests-Fix-test_allow_query_transfer_ipv6-when-IPv.patch +++ /dev/null @@ -1,138 +0,0 @@ -From 313bd8ff118a79dca5aad0b19ec8f69519258f89 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 4b9ab1fe8..947cff5c0 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/0031-ipatests-Add-XML-RPC-tests-for-i18n-user-attributes.patch b/0031-ipatests-Add-XML-RPC-tests-for-i18n-user-attributes.patch deleted file mode 100644 index 3035409..0000000 --- a/0031-ipatests-Add-XML-RPC-tests-for-i18n-user-attributes.patch +++ /dev/null @@ -1,217 +0,0 @@ -From c8a832e19699a7bb6ff486055015f033a3137e5f Mon Sep 17 00:00:00 2001 -From: PRANAV THUBE -Date: Mon, 16 Mar 2026 14:29:12 +0530 -Subject: [PATCH] ipatests: Add XML-RPC tests for i18n user attributes -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Add tests for internationalization support in user plugin: -- User creation/deletion with i18n givenname and sn -- Lastname modification with Swedish/European names (13 values) -- Firstname modification with European accented names (4 values) -- Firstname modification with single i18n characters (67 values) - -Test data includes characters like Çándide, Örjan, Éric, ß, ü, etc. - -Related: https://pagure.io/freeipa/issue/9959 -Signed-off-by: Pranav Thube pthube@redhat.com -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: David Hanina -Reviewed-By: Carla Martinez ---- - ipatests/test_xmlrpc/test_i18n_user_plugin.py | 182 ++++++++++++++++++ - 1 file changed, 182 insertions(+) - create mode 100644 ipatests/test_xmlrpc/test_i18n_user_plugin.py - -diff --git a/ipatests/test_xmlrpc/test_i18n_user_plugin.py b/ipatests/test_xmlrpc/test_i18n_user_plugin.py -new file mode 100644 -index 000000000..146ffdc51 ---- /dev/null -+++ b/ipatests/test_xmlrpc/test_i18n_user_plugin.py -@@ -0,0 +1,182 @@ -+# Authors: -+# Pranav Thube -+# -+# Copyright (C) 2026 Red Hat -+# see file 'COPYING' for use and warranty information -+# -+# This program is free software; you can redistribute it and/or modify -+# it under the terms of the GNU General Public License as published by -+# the Free Software Foundation, either version 3 of the License, or -+# (at your option) any later version. -+# -+# This program is distributed in the hope that it will be useful, -+# but WITHOUT ANY WARRANTY; without even the implied warranty of -+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+# GNU General Public License for more details. -+# -+# You should have received a copy of the GNU General Public License -+# along with this program. If not, see . -+ -+""" -+Test the i18n (internationalization) support for user plugin. -+ -+This module tests that IPA correctly handles international characters -+in user attributes such as first name (givenname) and last name (sn). -+""" -+ -+import pytest -+ -+from ipatests.test_xmlrpc.xmlrpc_test import XMLRPC_test -+from ipatests.test_xmlrpc.tracker.user_plugin import UserTracker -+ -+ -+# Test data Users with i18n names -+I18N_USERS = { -+ 'user1': { -+ 'name': 'i18nuser1', -+ 'givenname': 'Çándide', -+ 'sn': 'Rùiz', -+ }, -+ 'user2': { -+ 'name': 'i18nuser2', -+ 'givenname': 'Rôséñe', -+ 'sn': 'zackr', -+ }, -+ 'user3': { -+ 'name': 'i18nuser3', -+ 'givenname': 'Älka', -+ 'sn': 'Màrzella', -+ }, -+ 'user4': { -+ 'name': 'i18nuser4', -+ 'givenname': 'Feâtlëss', -+ 'sn': 'Watérmân', -+ }, -+} -+ -+# CNS test data - Swedish/European last names -+CNS_LASTNAMES = [ -+ 'Oskar', -+ 'Anders', -+ 'Örjan', -+ 'Jonas', -+ 'Ulf', -+ 'Äke', -+ 'Bertold', -+ 'Bruno', -+ 'Didier', -+ 'Éric', -+ 'Jean-Luc', -+ 'Laurent', -+ 'Têko', -+] -+ -+# European names with mixed accents for firstname tests -+EUROPEAN_FIRSTNAMES = [ -+ 'Rôséñel', -+ 'Tàrqùinio', -+ 'PASSWÖRD', -+ 'Nomeuropéen', -+ # Names with special characters (apostrophe, space) -+ "O'Brian", -+ 'Maria José', -+] -+ -+# Firstname test data - Single characters including accented -+# 73 characters total: 26 ASCII A-Z + 47 accented/special characters -+FIRSTNAME_SINGLE_CHARS = [ -+ # ASCII uppercase letters A-Z (26 characters) -+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', -+ 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', -+ 'U', 'V', 'W', 'X', 'Y', 'Z', -+ # Extended Latin uppercase characters (20 characters) -+ 'À', 'Á', 'Â', 'Ä', 'Ç', 'È', 'É', 'Ê', 'Ë', -+ 'Í', 'Î', 'Ï', 'Ñ', 'Ó', 'Ô', 'Ö', 'Ù', 'Ú', 'Û', 'Ü', -+ # German eszett (1 character) -+ 'ß', -+ # Extended Latin lowercase characters (20 characters) -+ 'à', 'á', 'â', 'ä', 'ç', 'è', 'é', 'ê', 'ë', -+ 'í', 'î', 'ï', 'ñ', 'ó', 'ô', 'ö', 'ù', 'ú', 'û', 'ü', -+ # Nordic characters (4 characters) -+ 'Ø', 'ø', 'Å', 'å', -+ # Polish character (2 characters) -+ 'Ł', 'ł', -+] -+ -+ -+@pytest.fixture(scope='class') -+def i18n_users(request, xmlrpc_setup): -+ """Single fixture providing all i18n test users as a dictionary""" -+ users = {} -+ for user_key, user_data in I18N_USERS.items(): -+ tracker = UserTracker( -+ name=user_data['name'], -+ givenname=user_data['givenname'], -+ sn=user_data['sn'] -+ ) -+ users[user_key] = tracker.make_fixture(request) -+ return users -+ -+ -+@pytest.mark.tier1 -+class TestI18nUser(XMLRPC_test): -+ """ -+ Test i18n (internationalization) support for user plugin. -+ -+ Tests that IPA correctly handles international characters in user -+ attributes such as first name (givenname) and last name (sn). -+ """ -+ -+ ########################################################################## -+ # User Creation Tests -+ ########################################################################## -+ -+ @pytest.mark.parametrize('user_key', I18N_USERS.keys()) -+ def test_add_i18n_user(self, i18n_users, user_key): -+ """Adding i18n user""" -+ i18n_users[user_key].create() -+ -+ @pytest.mark.parametrize('user_key', I18N_USERS.keys()) -+ def test_verify_i18n_user(self, i18n_users, user_key): -+ """Verify i18n user has correct full name""" -+ user = i18n_users[user_key] -+ user.ensure_exists() -+ command = user.make_find_command(uid=user.uid, all=True) -+ result = command() -+ assert result['count'] == 1 -+ entry = result['result'][0] -+ assert I18N_USERS[user_key]['givenname'] in entry['givenname'] -+ assert I18N_USERS[user_key]['sn'] in entry['sn'] -+ -+ ########################################################################## -+ # CNS Tests - Lastname modification with Swedish/European names -+ ########################################################################## -+ -+ @pytest.mark.parametrize('lastname', CNS_LASTNAMES) -+ def test_cns_modify_lastname(self, i18n_users, lastname): -+ """Modify lastname to Swedish/European name""" -+ user = i18n_users['user1'] -+ user.ensure_exists() -+ user.update(dict(sn=lastname)) -+ -+ ########################################################################## -+ # European accented firstname tests -+ ########################################################################## -+ -+ @pytest.mark.parametrize('firstname', EUROPEAN_FIRSTNAMES) -+ def test_european_modify_firstname(self, i18n_users, firstname): -+ """Modify firstname to European accented name""" -+ user = i18n_users['user2'] -+ user.ensure_exists() -+ user.update(dict(givenname=firstname)) -+ -+ ########################################################################## -+ # Firstname Tests - Single character modification -+ ########################################################################## -+ -+ @pytest.mark.parametrize('char', FIRSTNAME_SINGLE_CHARS) -+ def test_firstname_modify_single_char(self, i18n_users, char): -+ """Modify firstname to single character""" -+ user = i18n_users['user3'] -+ user.ensure_exists() -+ user.update(dict(givenname=char)) --- -2.52.0 - diff --git a/0032-ipatests-Add-selfservice-add-and-selfservice-del-cli.patch b/0032-ipatests-Add-selfservice-add-and-selfservice-del-cli.patch deleted file mode 100644 index 638bf40..0000000 --- a/0032-ipatests-Add-selfservice-add-and-selfservice-del-cli.patch +++ /dev/null @@ -1,229 +0,0 @@ -From 3056bf8fb27732213591f4c86044ce6980054ec9 Mon Sep 17 00:00:00 2001 -From: Jay Gondaliya -Date: Tue, 10 Mar 2026 19:14:48 +0530 -Subject: [PATCH] ipatests: Add selfservice-add and selfservice-del cli - tests - -Add a new Declarative test class `test_selfservice_cli_add_del` covering CLI-level behaviour of the selfservice-add and selfservice-del commands: - -- add_1002: bad attrs with valid permissions rejects with InvalidSyntax -- add_1003: valid attrs with invalid permissions rejects with ValidationError -- add_1004: valid attrs and permissions with --all --raw succeeds and - returns the raw ACI string (BZ 772106) -- add_1005: bad attrs only rejects with InvalidSyntax -- add_1006: valid attrs only succeeds with default write permission -- del_1001: deleting an existing selfservice rule succeeds -- del_1002: deleting a non-existent rule raises NotFound - -Signed-off-by: Jay Gondaliya -Fixes: https://pagure.io/freeipa/issue/9945 -Assisted-by: Claude noreply@anthropic.com -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: David Hanina ---- - .../test_xmlrpc/test_selfservice_plugin.py | 192 ++++++++++++++++++ - 1 file changed, 192 insertions(+) - -diff --git a/ipatests/test_xmlrpc/test_selfservice_plugin.py b/ipatests/test_xmlrpc/test_selfservice_plugin.py -index e55502a2d..8f2307a20 100644 ---- a/ipatests/test_xmlrpc/test_selfservice_plugin.py -+++ b/ipatests/test_xmlrpc/test_selfservice_plugin.py -@@ -750,3 +750,195 @@ class test_selfservice_users(XMLRPC_test): - assert result['count'] == 0, ( - 'Mobile was changed by a different user' - ) -+ -+ -+# Module-level constants for CLI test classes -+# selfservice-add / selfservice-del CLI tests -+SS_CLI_ADD_1004 = 'selfservice_add_1004' -+SS_CLI_ADD_1006 = 'selfservice_add_1006' -+SS_CLI_DEL_1001 = 'selfservice_del_1001' -+ -+ -+@pytest.mark.tier1 -+class test_selfservice_cli_add_del(Declarative): -+ """CLI tests for selfservice-add and selfservice-del commands.""" -+ -+ cleanup_commands = [ -+ ('selfservice_del', [SS_CLI_ADD_1004], {}), -+ ('selfservice_del', [SS_CLI_ADD_1006], {}), -+ ] -+ -+ tests = [ -+ -+ # add_1002: bad attrs + valid permissions + --all --raw -+ dict( -+ desc='add_1002: selfservice-add with bad attrs, valid permissions,' -+ ' --all --raw', -+ command=( -+ 'selfservice_add', -+ ['selfservice_add_1002'], -+ dict( -+ attrs=['badattr'], -+ permissions='write', -+ all=True, -+ raw=True, -+ ), -+ ), -+ expected=errors.InvalidSyntax( -+ attr=r'targetattr "badattr" does not exist in schema. ' -+ r'Please add attributeTypes "badattr" to ' -+ r'schema if necessary. ' -+ r'ACL Syntax Error(-5):' -+ r'(targetattr = \22badattr\22)' -+ r'(version 3.0;acl ' -+ r'\22selfservice:selfservice_add_1002\22;' -+ r'allow (write) userdn = \22ldap:///self\22;)', -+ ), -+ ), -+ -+ # add_1003: valid attrs + bad permissions + --all --raw -+ dict( -+ desc='add_1003: selfservice-add with valid attrs, bad permissions,' -+ ' --all --raw', -+ command=( -+ 'selfservice_add', -+ ['selfservice_add_1003'], -+ dict( -+ attrs=[ -+ 'telephonenumber', 'mobile', -+ 'pager', 'facsimiletelephonenumber', -+ ], -+ permissions='badperm', -+ all=True, -+ raw=True, -+ ), -+ ), -+ expected=errors.ValidationError( -+ name='permissions', -+ error='"badperm" is not a valid permission', -+ ), -+ ), -+ -+ # add_1004: valid attrs + valid permissions + --all --raw (BZ 772106) -+ # selfservice-add with --raw must not return "internal error" message. -+ dict( -+ desc='add_1004: selfservice-add with valid attrs and permissions,' -+ ' --all --raw (BZ 772106)', -+ command=( -+ 'selfservice_add', -+ [SS_CLI_ADD_1004], -+ dict( -+ attrs=[ -+ 'telephonenumber', 'mobile', -+ 'pager', 'facsimiletelephonenumber', -+ ], -+ permissions='write', -+ all=True, -+ raw=True, -+ ), -+ ), -+ expected=dict( -+ value=SS_CLI_ADD_1004, -+ summary='Added selfservice "%s"' % SS_CLI_ADD_1004, -+ result={ -+ 'aci': ( -+ '(targetattr = "telephonenumber || mobile || pager' -+ ' || facsimiletelephonenumber")' -+ '(version 3.0;acl "selfservice:%s";' -+ 'allow (write) userdn = "ldap:///self";)' -+ % SS_CLI_ADD_1004 -+ ), -+ }, -+ ), -+ ), -+ -+ # add_1005: bad attrs only -+ dict( -+ desc='add_1005: selfservice-add with bad attrs only', -+ command=( -+ 'selfservice_add', -+ ['selfservice_add_1005'], -+ dict(attrs=['badattrs']), -+ ), -+ expected=errors.InvalidSyntax( -+ attr=r'targetattr "badattrs" does not exist in schema. ' -+ r'Please add attributeTypes "badattrs" to ' -+ r'schema if necessary. ' -+ r'ACL Syntax Error(-5):' -+ r'(targetattr = \22badattrs\22)' -+ r'(version 3.0;acl ' -+ r'\22selfservice:selfservice_add_1005\22;' -+ r'allow (write) userdn = \22ldap:///self\22;)', -+ ), -+ ), -+ -+ # add_1006: valid attrs only -+ dict( -+ desc='add_1006: selfservice-add with valid attrs only', -+ command=( -+ 'selfservice_add', -+ [SS_CLI_ADD_1006], -+ dict(attrs=[ -+ 'telephonenumber', 'mobile', -+ 'pager', 'facsimiletelephonenumber', -+ ]), -+ ), -+ expected=dict( -+ value=SS_CLI_ADD_1006, -+ summary='Added selfservice "%s"' % SS_CLI_ADD_1006, -+ result=dict( -+ attrs=[ -+ 'telephonenumber', 'mobile', -+ 'pager', 'facsimiletelephonenumber', -+ ], -+ permissions=['write'], -+ selfaci=True, -+ aciname=SS_CLI_ADD_1006, -+ ), -+ ), -+ ), -+ -+ # Setup for del tests: create the rule that del_1001 will delete. -+ dict( -+ desc=( -+ 'Setup: create %r for selfservice-del tests' -+ % SS_CLI_DEL_1001 -+ ), -+ command=( -+ 'selfservice_add', -+ [SS_CLI_DEL_1001], -+ dict(attrs=['l'], permissions='write'), -+ ), -+ expected=dict( -+ value=SS_CLI_DEL_1001, -+ summary='Added selfservice "%s"' % SS_CLI_DEL_1001, -+ result=dict( -+ attrs=['l'], -+ permissions=['write'], -+ selfaci=True, -+ aciname=SS_CLI_DEL_1001, -+ ), -+ ), -+ ), -+ -+ # del_1001: delete an existing rule -+ dict( -+ desc='del_1001: selfservice-del of an existing rule', -+ command=('selfservice_del', [SS_CLI_DEL_1001], {}), -+ expected=dict( -+ result=True, -+ value=SS_CLI_DEL_1001, -+ summary='Deleted selfservice "%s"' % SS_CLI_DEL_1001, -+ ), -+ ), -+ -+ # del_1002: delete a non-existent rule -+ dict( -+ desc='del_1002: selfservice-del of a non-existent rule', -+ command=('selfservice_del', ['badname'], {}), -+ expected=errors.NotFound( -+ reason='ACI with name "badname" not found', -+ ), -+ ), -+ -+ ] --- -2.52.0 - diff --git a/0033-ipatests-Additional-tests-for-32BitIdranges.patch b/0033-ipatests-Additional-tests-for-32BitIdranges.patch deleted file mode 100644 index 43bdb97..0000000 --- a/0033-ipatests-Additional-tests-for-32BitIdranges.patch +++ /dev/null @@ -1,421 +0,0 @@ -From b05586c2a6a81c7121dd40f8d627cd8a2c5908d8 Mon Sep 17 00:00:00 2001 -From: Sudhir Menon -Date: Wed, 18 Mar 2026 16:36:06 +0530 -Subject: [PATCH] ipatests: Additional tests for 32BitIdranges - -Below tests are added - -1. Create ipauser with 32bit id. -2. Create ipagroup with 32Bit id. -3. Create ipauser with 32Bit groupid range. -4. Test ssh login with 32Bit id user. -5. Test that ipauser with 32Bit is replicated. -6. Test that 32Bit idrange is created in IPA-AD trust enviornment. - -Signed-off-by: Sudhir Menon -Reviewed-By: Rob Crittenden -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: David Hanina ---- - .../test_integration/test_32bit_idranges.py | 333 +++++++++++++++--- - 1 file changed, 284 insertions(+), 49 deletions(-) - -diff --git a/ipatests/test_integration/test_32bit_idranges.py b/ipatests/test_integration/test_32bit_idranges.py -index a928628d3..9b91fc618 100644 ---- a/ipatests/test_integration/test_32bit_idranges.py -+++ b/ipatests/test_integration/test_32bit_idranges.py -@@ -4,17 +4,85 @@ - - from __future__ import absolute_import - -+import re -+ - from ipatests.pytest_ipa.integration import tasks - from ipatests.test_integration.base import IntegrationTest - from ipatests.test_integration.test_trust import BaseTestTrust - -+# The tests focus on 32-bit UID/GID creation and replication, -+# SID behavior is not covered in the tests. -+ -+# Range with First Posix ID >= 2^31 is considered a 32-bit range. -+IDRANGE_32BIT_NAME = "{realm}_upper_32bit_range" -+IDRANGE_32BIT_BASE_ID = 1 << 31 # 2147483648 -+ -+ -+def _32bit_idrange_exists(master): -+ """ -+ Return True if an ipa-local range with base ID >= 2^31 already exists. -+ """ -+ result = master.run_command( -+ ["ipa", "idrange-find", "--type", "ipa-local"] -+ ) -+ # Parse all "First Posix ID of the range: in the output' -+ for match in re.finditer( -+ r"First Posix ID of the range:\s*(\d+)", -+ result.stdout_text -+ ): -+ if int(match.group(1)) >= IDRANGE_32BIT_BASE_ID: -+ return True -+ return False -+ -+ -+def _add_32bit_idrange_if_missing(master): -+ """ -+ Create the 32-bit ID range only if it does not already exist. -+ Returns True if the range was added, False if it already existed. -+ """ -+ if _32bit_idrange_exists(master): -+ return False -+ idrange = IDRANGE_32BIT_NAME.format(realm=master.domain.realm) -+ id_length = 10000 -+ rid_base = 300_000_000 -+ secondary_rid_base = 500_000_000 -+ master.run_command( -+ [ -+ "ipa", -+ "idrange-add", -+ idrange, -+ "--base-id", str(IDRANGE_32BIT_BASE_ID), -+ "--range-size", str(id_length), -+ "--rid-base", str(rid_base), -+ "--secondary-rid-base", str(secondary_rid_base), -+ "--type=ipa-local" -+ ] -+ ) -+ # Restart dirsrv instance after the new idrange is added. -+ tasks.restart_ipa_server(master) -+ # Clear SSSD cache -+ tasks.clear_sssd_cache(master) -+ return True -+ - - class Test32BitIdRanges(IntegrationTest): - topology = "line" -+ num_replicas = 1 -+ num_clients = 1 -+ # Counter for 32-bit UID/GID allocation; reset in install() so each -+ # test run starts from 0 (install/uninstall gives a fresh environment). -+ id_counter = 0 -+ -+ def get_next_32bit_id(self): -+ """ -+ Generate unique 32-bit IDs for testing -+ """ -+ self.id_counter += 1 -+ return IDRANGE_32BIT_BASE_ID + self.__class__.id_counter - - def test_remove_subid_range(self): - """ -- Test that allocating subid will fail after disabling global option -+ Test that allocating subids will fail after disabling the attribute - """ - master = self.master - tasks.kinit_admin(master) -@@ -23,19 +91,28 @@ class Test32BitIdRanges(IntegrationTest): - master.run_command( - ["ipa", "config-mod", "--addattr", "ipaconfigstring=SubID:Disable"] - ) -- master.run_command(["ipa", "idrange-del", idrange]) -+ master.run_command( -+ ["ipa", "idrange-del", idrange] -+ ) -+ master.run_command(["systemctl", "restart", "sssd"]) - - tasks.user_add(master, 'subiduser') -- result = master.run_command( -- ["ipa", "subid-generate", "--owner", "subiduser"], raiseonerr=False -- ) -- assert result.returncode > 0 -- assert "Support for subordinate IDs is disabled" in result.stderr_text -- tasks.user_del(master, 'subiduser') -+ try: -+ result = master.run_command( -+ ["ipa", "subid-generate", "--owner", "subiduser"], -+ raiseonerr=False -+ ) -+ assert result.returncode > 0 -+ assert "Support for subordinate IDs is disabled" in \ -+ result.stderr_text -+ finally: -+ # Cleanup: Remove test user -+ tasks.user_del(master, 'subiduser') - - def test_invoke_upgrader(self): -- """Test that ipa-server-upgrade does not add subid ranges back""" -- -+ """ -+ Test that ipa-server-upgrade does not add subid ranges back. -+ """ - master = self.master - master.run_command(['ipa-server-upgrade'], raiseonerr=True) - idrange = f"{master.domain.realm}_subid_range" -@@ -58,69 +135,227 @@ class Test32BitIdRanges(IntegrationTest): - assert "dnatype: " not in output - - def test_create_user_with_32bit_id(self): -- """Test that ID range above 2^31 can be used to assign IDs -- to users and groups. Also check that SIDs generated properly. - """ -+ Test checks that 32Bit idrange is assigned to the user -+ and getent passwd returns the output. -+ """ -+ master = self.master -+ _add_32bit_idrange_if_missing(master) -+ -+ uid = self.get_next_32bit_id() -+ gid = self.get_next_32bit_id() -+ -+ tasks.clear_sssd_cache(master) -+ username = "user" -+ tasks.create_active_user( -+ master, username, "Secret123", -+ extra_args=["--uid", str(uid), "--gid", str(gid)] -+ ) -+ tasks.kinit_admin(master) -+ try: -+ result = master.run_command( -+ ["ipa", "user-show", username, "--all", "--raw"] -+ ) -+ assert result.returncode == 0, ( -+ f"User not found: {result.stderr_text}" -+ ) -+ assert "ipantsecurityidentifier" in \ -+ result.stdout_text.lower(), ( -+ "SID not found in user entry" -+ ) -+ if hasattr(self, 'clients') and self.clients: -+ client = self.clients[0] -+ tasks.clear_sssd_cache(client) -+ result = client.run_command( -+ ["getent", "passwd", username], raiseonerr=False -+ ) -+ assert result.returncode == 0, ( -+ f"getent passwd failed: {result.stderr_text}" -+ ) -+ assert str(uid) in result.stdout_text -+ assert str(gid) in result.stdout_text -+ finally: -+ tasks.user_del(master, username) -+ -+ def test_create_group_with_32bit_gid(self): -+ """ -+ Test that a group can be created with a GID from the 32-bit range. -+ """ -+ master = self.master -+ groupname = 'grp32bit' -+ gid = self.get_next_32bit_id() -+ tasks.group_add(master, groupname, extra_args=["--gid", str(gid)]) -+ try: -+ result = master.run_command( -+ ["ipa", "group-show", groupname, "--all", "--raw"] -+ ) -+ assert result.returncode == 0 -+ assert str(gid) in result.stdout_text, ( -+ f"GID {gid} not in group entry" -+ ) -+ finally: -+ tasks.group_del(master, groupname) - -+ def test_user_in_group_with_32bit_ids(self): -+ """ -+ Test user with 32-bit UID in a group with 32-bit GID. -+ """ - master = self.master -- idrange = f"{master.domain.realm}_upper_32bit_range" -- id_base = 1 << 31 -- id_length = (1 << 31) - 2 -- uid = id_base + 1 -- gid = id_base + 1 -- master.run_command( -- [ -- "ipa", -- "idrange-add", -- idrange, -- "--base-id", str(id_base), -- "--range-size", str(id_length), -- "--rid-base", str(int(id_base >> 3)), -- "--secondary-rid-base", str(int(id_base >> 3) + id_length), -- "--type=ipa-local" -- ] -+ groupname = 'grp32bit2' -+ username = 'user32bit' -+ uid = self.get_next_32bit_id() -+ gid = self.get_next_32bit_id() -+ tasks.group_add(master, groupname, extra_args=["--gid", str(gid)]) -+ tasks.create_active_user( -+ master, username, "Secret123", -+ extra_args=["--uid", str(uid), "--gid", str(gid)] - ) -+ tasks.kinit_admin(master) -+ try: -+ tasks.group_add_member(master, groupname, users=username) -+ result = master.run_command( -+ ["ipa", "group-show", groupname, "--all", "--raw"] -+ ) -+ assert result.returncode == 0 -+ assert username in result.stdout_text -+ assert f"gidnumber: {gid}" in result.stdout_text, ( -+ f"GID {gid} not found in group entry" -+ ) -+ assert "ipaNTSecurityIdentifier:" in result.stdout_text, ( -+ "Group does not contain a SID" -+ ) -+ result = master.run_command( -+ ["ipa", "user-show", username, "--all", "--raw"] -+ ) -+ assert result.returncode == 0 -+ finally: -+ master.run_command( -+ ["ipa", "group-remove-member", groupname, -+ "--users", username], -+ raiseonerr=False -+ ) -+ tasks.user_del(master, username) -+ tasks.group_del(master, groupname) - -- # We added new ID range, SIDGEN will only take it after -- # restarting a directory server instance. -- tasks.restart_ipa_server(master) -+ def test_ssh_login_with_32bit_id(self): -+ """ -+ Test that a user with 32-bit UID/GID can kinit and log in via SSH -+ from the client to the master using GSSAPI (Kerberos). -+ """ -+ client = self.clients[0] -+ master = self.master -+ testuser = 'sshuser32bit' -+ password = 'Secret123' -+ uid = self.get_next_32bit_id() -+ gid = self.get_next_32bit_id() - -- # Clear SSSD cache to pick up new ID range - tasks.clear_sssd_cache(master) -+ tasks.create_active_user( -+ master, testuser, password, -+ extra_args=["--uid", str(uid), "--gid", str(gid)] -+ ) -+ tasks.kinit_admin(master) -+ hbac_rule = "allow_ssh_32bit_test" -+ tasks.hbacrule_add(master, hbac_rule, extra_args=["--hostcat=all"]) -+ tasks.hbacrule_add_user(master, hbac_rule, users=testuser) -+ tasks.hbacrule_add_service(master, hbac_rule, services="sshd") -+ try: -+ result = master.run_command( -+ ["ipa", "user-show", testuser, "--all", "--raw"] -+ ) -+ assert result.returncode == 0, ( -+ f"User {testuser} not found: {result.stderr_text}" -+ ) - -- tasks.user_add(master, "user", extra_args=[ -- "--uid", str(uid), "--gid", str(gid) -- ]) -+ tasks.clear_sssd_cache(client) -+ tasks.clear_sssd_cache(master) -+ tasks.kdestroy_all(client) -+ tasks.kinit_as_user(client, testuser, password) -+ result = client.run_command([ -+ 'ssh', '-o', 'StrictHostKeyChecking=no', '-K', -+ '-l', testuser, master.hostname, 'echo login successful' -+ ], raiseonerr=False) -+ assert result.returncode == 0, ( -+ "SSH (GSSAPI) from client to master failed: " -+ f"{result.stderr_text}" -+ ) -+ assert 'login successful' in result.stdout_text, ( -+ "SSH succeeded but expected output missing: " -+ f"{result.stdout_text}" -+ ) -+ finally: -+ tasks.kdestroy_all(client) -+ master.run_command( -+ ["ipa", "hbacrule-del", hbac_rule], raiseonerr=False -+ ) -+ tasks.kinit_admin(master) -+ tasks.user_del(master, testuser) - -- result = master.run_command( -- ["ipa", "user-show", "user", "--all", "--raw"], raiseonerr=False -- ) -- assert result.returncode == 0 -- assert "ipaNTSecurityIdentifier:" in result.stdout_text -+ def test_32bit_id_replication(self): -+ """ -+ Test that users with 32-bit IDs replicate correctly -+ """ -+ master = self.master -+ replica = self.replicas[0] -+ tasks.kinit_admin(master) -+ testuser = 'repluser32bit' -+ uid = self.get_next_32bit_id() -+ gid = self.get_next_32bit_id() - -- result = master.run_command( -- ["id", "user"], raiseonerr=False -+ tasks.clear_sssd_cache(master) -+ -+ # Create user on master -+ tasks.create_active_user( -+ master, testuser, "Secret123", -+ extra_args=["--uid", str(uid), "--gid", str(gid)] - ) -- assert result.returncode == 0 -- assert str(uid) in result.stdout_text -+ tasks.kinit_admin(master) -+ try: -+ tasks.wait_for_replication(master.ldap_connect()) -+ -+ result = master.run_command( -+ ["ipa", "user-show", testuser, "--all", "--raw"], -+ raiseonerr=False -+ ) -+ assert result.returncode == 0, ( -+ f"User {testuser} not found on master" -+ ) -+ assert str(uid) in result.stdout_text, ( -+ f"UID {uid} not on master" -+ ) -+ -+ tasks.kinit_admin(replica) -+ result = replica.run_command( -+ ["ipa", "user-show", testuser, "--all", "--raw"], -+ raiseonerr=False -+ ) -+ assert result.returncode == 0, ( -+ f"User {testuser} not replicated to replica" -+ ) -+ assert str(uid) in result.stdout_text, ( -+ f"UID {uid} not on replica" -+ ) -+ finally: -+ # Cleanup: Remove test user from master -+ tasks.kinit_admin(master) -+ tasks.user_del(master, testuser) - - - class Test32BitIdrangeInTrustEnv(Test32BitIdRanges, BaseTestTrust): - """ - Tests to check 32BitIdrange functionality -- in IPA-AD trust enviornment -+ in IPA-AD trust environment - """ - topology = 'line' -+ num_replicas = 1 - num_ad_domains = 1 - num_ad_subdomains = 0 - num_ad_treedomains = 0 -- num_clients = 0 -+ num_clients = 1 - - @classmethod - def install(cls, mh): -- super(BaseTestTrust, cls).install(mh) -+ super(Test32BitIdrangeInTrustEnv, cls).install(mh) - cls.ad = cls.ads[0] -- cls.ad_domain = cls.ad.domain.name - tasks.configure_dns_for_trust(cls.master, cls.ad) -- tasks.install_adtrust(cls.master) - tasks.establish_trust_with_ad(cls.master, cls.ad.domain.name) --- -2.52.0 - diff --git a/0034-ipatests-add-HTTP-GSSAPI-Kerberos-authentication-tes.patch b/0034-ipatests-add-HTTP-GSSAPI-Kerberos-authentication-tes.patch deleted file mode 100644 index a97efd1..0000000 --- a/0034-ipatests-add-HTTP-GSSAPI-Kerberos-authentication-tes.patch +++ /dev/null @@ -1,260 +0,0 @@ -From 3acf55ed7fcf9a3b38deb0efb98d222d57bafbbc Mon Sep 17 00:00:00 2001 -From: Anuja More -Date: Thu, 26 Feb 2026 12:04:50 +0530 -Subject: [PATCH] ipatests: add HTTP GSSAPI Kerberos authentication - tests with AD trust - -Add TestTrustFunctionalHttp integration test class covering GSSAPI-protected -HTTP access in an AD trust environment: - -- test_ipa_trust_func_http_krb_ipauser: IPA user with a valid Kerberos ticket - can access the GSSAPI-protected webapp; AD root and subdomain users are - denied when only the IPA user is in the Allow list -- test_ipa_trust_func_http_krb_aduser: AD root domain and subdomain users with - valid Kerberos tickets can access the webapp; IPA users are denied when the - Allow list is configured for an AD user -- test_ipa_trust_func_http_krb_nouser: a user without a Kerberos ticket - receives a 401 Unauthorized response - -The class sets up an Apache httpd instance with mod_auth_gssapi on the IPA -client, obtains a service keytab via ipa-getkeytab, and uses curl with -GSSAPI negotiate to drive each scenario. - -Related: https://pagure.io/freeipa/issue/9845 -Assisted-by: Claude noreply@anthropic.com -Signed-off-by: Anuja More -Reviewed-By: Rafael Guterres Jeffman -Reviewed-By: David Hanina -Reviewed-By: Florence Blanc-Renaud ---- - .../test_integration/test_trust_functional.py | 209 +++++++++++++++++- - 1 file changed, 208 insertions(+), 1 deletion(-) - -diff --git a/ipatests/test_integration/test_trust_functional.py b/ipatests/test_integration/test_trust_functional.py -index a85f21e96463757b9a446df666d5361e65ba686c..5a9eae8cebfdae23bf37d3298e455bab8e3304ef 100644 ---- a/ipatests/test_integration/test_trust_functional.py -+++ b/ipatests/test_integration/test_trust_functional.py -@@ -2,8 +2,9 @@ - - from __future__ import absolute_import - -+import re - import time -- -+import textwrap - from ipaplatform.paths import paths - from ipatests.pytest_ipa.integration import tasks - from ipatests.test_integration.test_trust import BaseTestTrust -@@ -658,3 +659,209 @@ class TestTrustFunctionalSudo(BaseTestTrust): - raiseonerr=False) - finally: - self._cleanup_srule(srule) -+ -+ -+class TestTrustFunctionalHttp(BaseTestTrust): -+ topology = 'line' -+ num_ad_treedomains = 0 -+ -+ ad_user_password = 'Secret123' -+ -+ # Apache configuration for GSSAPI-protected webapp. The /mywebapp -+ # location requires Kerberos authentication and restricts access by -+ # domain: IPA users (@IPA_REALM) or AD users (@AD_DOMAIN). -+ apache_conf = textwrap.dedent(''' -+ Alias /mywebapp "/var/www/html/mywebapp" -+ -+ Allow from all -+ -+ -+ LogLevel debug -+ AuthType GSSAPI -+ AuthName "IPA Kerberos authentication" -+ GssapiNegotiateOnce on -+ GssapiBasicAuthMech krb5 -+ GssapiCredStore keytab:{keytab_path} -+ -+ Require valid-user -+ # Require expr: restrict access by domain. REMOTE_USER is set by -+ # mod_auth_gssapi after GSSAPI authentication. Allow users whose -+ # principal ends with the domain (IPA realm or AD domain). -+ Require expr %{{REMOTE_USER}} =~ /{allowed_domain_regex}$/ -+ -+ -+ ''') -+ -+ def _configure_webapp(self, allowed_domain): -+ """Write the GSSAPI vhost config and restart httpd on the client. -+ -+ allowed_domain: realm/domain for access control (e.g. IPA.TEST for -+ IPA users, AD.DOMAIN for AD users). Users whose principal ends with -+ @allowed_domain are granted access. -+ """ -+ # Escape dots for regex (e.g. IPA.TEST -> IPA\\.TEST) -+ escaped = re.escape(allowed_domain) -+ allowed_domain_regex = '.*@' + escaped -+ keytab_path = f"/etc/httpd/conf/{self.clients[0].hostname}.keytab" -+ self.clients[0].put_file_contents( -+ '/etc/httpd/conf.d/mywebapp.conf', -+ self.apache_conf.format( -+ keytab_path=keytab_path, -+ allowed_domain_regex=allowed_domain_regex, -+ ) -+ ) -+ self.clients[0].run_command(['systemctl', 'restart', 'httpd']) -+ -+ def _assert_curl_ok(self, msg=None): -+ """Run curl with GSSAPI negotiate and assert the webapp responds.""" -+ url = f"http://{self.clients[0].hostname}/mywebapp/index.html" -+ result = self.clients[0].run_command([ -+ paths.BIN_CURL, '-v', '--negotiate', '-u:', url -+ ]) -+ assert "TEST_MY_WEB_APP" in result.stdout_text, ( -+ msg or f"Expected webapp content at {url}" -+ ) -+ -+ def _assert_curl_GSSAPI_access_denied(self, msg=None): -+ """Run curl with GSSAPI negotiate and assert a 401 is returned.""" -+ url = f"http://{self.clients[0].hostname}/mywebapp/index.html" -+ result = self.clients[0].run_command([ -+ paths.BIN_CURL, '-v', '--negotiate', '-u:', url -+ ], raiseonerr=False) -+ output = f"{result.stdout_text}{result.stderr_text}" -+ assert ("401" in output -+ or "Unauthorized" in output -+ or "Authorization Required" in output), ( -+ msg or f"Expected 401/Unauthorized at {url}, got: {output[:200]}" -+ ) -+ -+ @classmethod -+ def install(cls, mh): -+ """Extend base install to configure Apache/GSSAPI for HTTP tests. -+ -+ Runs once before any test in this class. Sets up the AD trust, -+ creates the HTTP service principal and IPA test user, installs -+ mod_auth_gssapi, retrieves the service keytab, and provisions the -+ static webapp content used by all HTTP tests. -+ """ -+ super().install(mh) -+ tasks.configure_dns_for_trust(cls.master, cls.ad) -+ tasks.establish_trust_with_ad( -+ cls.master, cls.ad_domain, -+ extra_args=['--range-type', 'ipa-ad-trust']) -+ -+ # Create HTTP service principal on master -+ service_principal = f"HTTP/{cls.clients[0].hostname}" -+ cls.master.run_command( -+ ["ipa", "service-add", service_principal] -+ ) -+ -+ # Create IPA user for HTTP tests -+ tasks.create_active_user( -+ cls.master, "ipahttpuser1", password="Passw0rd1", -+ first="f", last="l" -+ ) -+ -+ # Clear SSSD cache on master -+ tasks.clear_sssd_cache(cls.master) -+ tasks.wait_for_sssd_domain_status_online(cls.master) -+ -+ # Install Apache and the GSSAPI module on the IPA client -+ tasks.install_packages( -+ cls.clients[0], ['mod_auth_gssapi', 'httpd'] -+ ) -+ -+ # Retrieve and protect the HTTP service keytab -+ keytab_path = f"/etc/httpd/conf/{cls.clients[0].hostname}.keytab" -+ cls.clients[0].run_command([ -+ 'ipa-getkeytab', '-s', cls.master.hostname, -+ '-k', keytab_path, -+ '-p', service_principal -+ ]) -+ cls.clients[0].run_command( -+ ['chown', 'apache:apache', keytab_path] -+ ) -+ -+ # Create webapp directory and static content -+ cls.clients[0].run_command( -+ ['mkdir', '-p', '/var/www/html/mywebapp'] -+ ) -+ cls.clients[0].put_file_contents( -+ '/var/www/html/mywebapp/index.html', -+ 'TEST_MY_WEB_APP\n' -+ ) -+ -+ def test_ipa_trust_func_http_krb_ipauser(self): -+ """ -+ Test IPA User access http with kerberos ticket via valid user. -+ -+ This test verifies that an IPA user with a valid Kerberos ticket -+ can successfully access an HTTP resource protected by GSSAPI -+ authentication and restricted to IPA users. -+ """ -+ ipa_realm = self.clients[0].domain.realm -+ self._configure_webapp(ipa_realm) -+ -+ tasks.kdestroy_all(self.clients[0]) -+ tasks.kinit_as_user( -+ self.clients[0], f'ipahttpuser1@{ipa_realm}', "Passw0rd1" -+ ) -+ -+ self._assert_curl_ok() -+ -+ users = [ -+ (self.aduser, self.ad_domain), -+ (self.subaduser, self.ad_subdomain), -+ ] -+ for aduser, domain in users: -+ tasks.kdestroy_all(self.clients[0]) -+ # pylint: disable=use-maxsplit-arg -+ principal = f"{aduser.split('@')[0]}@{domain.upper()}" -+ tasks.kinit_as_user( -+ self.clients[0], principal, self.ad_user_password -+ ) -+ self._assert_curl_GSSAPI_access_denied( -+ msg=f"Expected 401 for AD user {aduser}" -+ ) -+ -+ def test_ipa_trust_func_http_krb_aduser(self): -+ """ -+ Test AD root and subdomain users access http with kerberos ticket. -+ -+ This test verifies that both a root AD domain user and a child -+ subdomain user with valid Kerberos tickets can successfully access -+ an HTTP resource protected by GSSAPI authentication and restricted -+ to AD domain / AD subdomain users. -+ """ -+ users = [ -+ (self.aduser, self.ad_domain), -+ (self.subaduser, self.ad_subdomain), -+ ] -+ for aduser, domain in users: -+ tasks.kdestroy_all(self.clients[0]) -+ # pylint: disable=use-maxsplit-arg -+ principal = f"{aduser.split('@')[0]}@{domain.upper()}" -+ self._configure_webapp(domain.upper()) -+ tasks.kinit_as_user( -+ self.clients[0], principal, self.ad_user_password -+ ) -+ self._assert_curl_ok( -+ msg=f"Expected webapp content for AD user {aduser}" -+ ) -+ tasks.kdestroy_all(self.clients[0]) -+ tasks.kinit_as_user(self.clients[0], "ipahttpuser1", "Passw0rd1") -+ self._assert_curl_GSSAPI_access_denied( -+ msg=f"Expected 401 for IPA user after AD user {aduser}" -+ ) -+ -+ def test_ipa_trust_func_http_krb_nouser(self): -+ """ -+ Test User cannot access http without kerberos ticket via valid user. -+ -+ This test verifies that an user without a valid Kerberos ticket -+ is denied access to an HTTP resource protected by GSSAPI -+ authentication, receiving a 401 Unauthorized error. -+ """ -+ tasks.kdestroy_all(self.clients[0]) -+ -+ self._assert_curl_GSSAPI_access_denied() --- -2.52.0 - diff --git a/0035-ipatests-Extend-netgroup-test-coverage.patch b/0035-ipatests-Extend-netgroup-test-coverage.patch deleted file mode 100644 index caf5e5e..0000000 --- a/0035-ipatests-Extend-netgroup-test-coverage.patch +++ /dev/null @@ -1,2595 +0,0 @@ -From 0390076bfdeb122b33437a55a08ade9880127deb Mon Sep 17 00:00:00 2001 -From: PRANAV THUBE -Date: Wed, 4 Mar 2026 20:06:07 +0530 -Subject: [PATCH] ipatests: Extend netgroup test coverage. - -Add new test cases for netgroup plugin: - -netgroup_add: -- Negative tests for space in nisdomain/description -- Invalid usercategory/hostcategory values -- Invalid addattr/setattr attributes -- Positive tests for custom nisdomain, usercat=all, hostcat=all, - combined usercat+hostcat, and externalHost via addattr - -netgroup_mod: -- Modify nisdomain, usercategory, hostcategory, description -- setattr/addattr/delattr for description and externalHost -- Negative tests for invalid values and disallowed attributes - -netgroup_add_member: -- Negative tests for non-existent user, group, hostgroup members -- Add member to non-existent netgroup - -netgroup_remove_member: -- Remove member from non-existent netgroup - -netgroup_find: -- Search by description and nisdomain -- Negative searches returning zero results - -Integration tests: -- Managed netgroups plugin (ipa-managed-entries) -- Find with --in-netgroups/--not-in-netgroups across entities -- CLI-specific options (--pkey-only, --sizelimit, --timelimit) -- Complex multi-host/DNS setup scenarios - -Related: https://pagure.io/freeipa/issue/9952 -Signed-off-by: Pranav Thube pthube@redhat.com -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: David Hanina -Reviewed-By: Carla Martinez ---- - ipatests/pytest_ipa/integration/tasks.py | 8 +- - ipatests/test_integration/test_netgroup.py | 211 +- - ipatests/test_xmlrpc/test_netgroup_plugin.py | 2091 ++++++++++++++++-- - 3 files changed, 2059 insertions(+), 251 deletions(-) - -diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py -index d9f142794..ee2befd69 100755 ---- a/ipatests/pytest_ipa/integration/tasks.py -+++ b/ipatests/pytest_ipa/integration/tasks.py -@@ -2272,9 +2272,9 @@ def user_add(host, login, first='test', last='user', extra_args=(), - return host.run_command(cmd, stdin_text=stdin_text) - - --def user_del(host, login): -+def user_del(host, login, raiseonerr=True): - cmd = ["ipa", "user-del", login] -- return host.run_command(cmd) -+ return host.run_command(cmd, raiseonerr=raiseonerr) - - - def group_add(host, groupname, extra_args=()): -@@ -2285,11 +2285,11 @@ def group_add(host, groupname, extra_args=()): - return host.run_command(cmd) - - --def group_del(host, groupname): -+def group_del(host, groupname, raiseonerr=True): - cmd = [ - "ipa", "group-del", groupname, - ] -- return host.run_command(cmd) -+ return host.run_command(cmd, raiseonerr=raiseonerr) - - - def group_add_member(host, groupname, users=None, -diff --git a/ipatests/test_integration/test_netgroup.py b/ipatests/test_integration/test_netgroup.py -index 5fae9e03f..86b156a83 100644 ---- a/ipatests/test_integration/test_netgroup.py -+++ b/ipatests/test_integration/test_netgroup.py -@@ -5,7 +5,20 @@ - import pytest - - from ipatests.test_integration.base import IntegrationTest --from ipatests.pytest_ipa.integration.tasks import clear_sssd_cache -+from ipatests.pytest_ipa.integration import tasks -+ -+# In-netgroups test entities -+# *_MEMBER entities are added to INNG_NG_PARENT netgroup -+# *_NONMEMBER entities are NOT added to any netgroup -+INNG_USER_MEMBER = 'inng_user_member' -+INNG_USER_NONMEMBER = 'inng_user_nonmember' -+INNG_GRP_MEMBER = 'inng_grp_member' -+INNG_GRP_NONMEMBER = 'inng_grp_nonmember' -+INNG_HG_MEMBER = 'inng_hg_member' -+INNG_HG_NONMEMBER = 'inng_hg_nonmember' -+INNG_NG_PARENT = 'inng_ng_parent' -+INNG_NG_MEMBER = 'inng_ng_member' -+INNG_NG_NONMEMBER = 'inng_ng_nonmember' - - - test_data = [] -@@ -56,16 +69,14 @@ def three_netgroups(request): - - - class TestNetgroups(IntegrationTest): -- """ -- Test Netgroups -- """ -+ """Test Netgroups - nested, managed, find, show, delete operations.""" - - topology = 'line' - - def check_users_in_netgroups(self): - """Check if users are in groups, no nested things""" - master = self.master -- clear_sssd_cache(master) -+ tasks.clear_sssd_cache(master) - - for d in test_data: - result = master.run_command(['getent', 'passwd', -@@ -86,7 +97,7 @@ class TestNetgroups(IntegrationTest): - def check_nested_netgroup_hierarchy(self): - """Check if nested netgroups hierarchy is complete""" - master = self.master -- clear_sssd_cache(master) -+ tasks.clear_sssd_cache(master) - - for d in test_data: - result = master.run_command(['getent', 'netgroup', d['netgroup']], -@@ -135,7 +146,7 @@ class TestNetgroups(IntegrationTest): - test_data[1]['netgroup']], - raiseonerr=False) - assert result.returncode == 0 -- clear_sssd_cache(master) -+ tasks.clear_sssd_cache(master) - - result = master.run_command(['getent', 'netgroup', - test_data[1]['netgroup']], -@@ -158,7 +169,7 @@ class TestNetgroups(IntegrationTest): - test_data[2]['netgroup']], - raiseonerr=False) - assert result.returncode == 0 -- clear_sssd_cache(master) -+ tasks.clear_sssd_cache(master) - - result = master.run_command(['getent', 'netgroup', - test_data[2]['netgroup']], -@@ -167,3 +178,187 @@ class TestNetgroups(IntegrationTest): - assert trinity[0] not in result.stdout_text - assert trinity[1] not in result.stdout_text - assert trinity[2] in result.stdout_text -+ -+ # --- Managed Netgroups Tests --- -+ -+ def test_managed_netgroup_lifecycle(self): -+ """Test managed netgroup created/deleted with hostgroup.""" -+ master = self.master -+ # Enable NGP plugin -+ master.run_command( -+ ['ipa-managed-entries', '--entry=NGP Definition', 'enable'], -+ raiseonerr=False -+ ) -+ # Add hostgroup - managed netgroup should be created -+ tasks.hostgroup_add(master, 'mng_testgrp', ('--desc=test',)) -+ master.run_command( -+ ['ipa', 'netgroup-find', '--managed', 'mng_testgrp'] -+ ) -+ -+ # Delete hostgroup - managed netgroup should be deleted -+ tasks.hostgroup_del(master, 'mng_testgrp') -+ # ipa *-find returns exit code 1 when 0 results found -+ result = master.run_command( -+ ['ipa', 'netgroup-find', '--managed', 'mng_testgrp'], -+ raiseonerr=False -+ ) -+ assert '0 netgroups matched' in result.stdout_text -+ -+ def test_cannot_delete_managed_netgroup(self): -+ """Deleting managed netgroup directly should fail.""" -+ master = self.master -+ master.run_command( -+ ['ipa-managed-entries', '--entry=NGP Definition', 'enable'], -+ raiseonerr=False -+ ) -+ tasks.hostgroup_add(master, 'mng_nodelete', ('--desc=test',)) -+ try: -+ result = master.run_command( -+ ['ipa', 'netgroup-del', 'mng_nodelete'], raiseonerr=False -+ ) -+ assert result.returncode != 0 -+ assert 'Deleting a managed entry is not allowed' in \ -+ result.stderr_text -+ finally: -+ tasks.hostgroup_del(master, 'mng_nodelete', raiseonerr=False) -+ -+ # --- In-Netgroups Tests for Various Entity Types --- -+ -+ @pytest.fixture(scope='class') -+ def in_netgroups_setup(self, request): -+ """Setup for --in-netgroups tests across entity types.""" -+ master = self.master -+ domain = master.domain.name -+ innghost1 = f'innghost1.{domain}' -+ innghost2 = f'innghost2.{domain}' -+ -+ # Register cleanup FIRST so it runs even if setup fails -+ def cleanup(): -+ for ng in [INNG_NG_PARENT, INNG_NG_MEMBER, INNG_NG_NONMEMBER]: -+ master.run_command( -+ ['ipa', 'netgroup-del', ng], raiseonerr=False -+ ) -+ tasks.hostgroup_del(master, INNG_HG_MEMBER, raiseonerr=False) -+ tasks.hostgroup_del(master, INNG_HG_NONMEMBER, raiseonerr=False) -+ tasks.host_del(master, innghost1, raiseonerr=False) -+ tasks.host_del(master, innghost2, raiseonerr=False) -+ tasks.group_del(master, INNG_GRP_MEMBER, raiseonerr=False) -+ tasks.group_del(master, INNG_GRP_NONMEMBER, raiseonerr=False) -+ tasks.user_del(master, INNG_USER_MEMBER, raiseonerr=False) -+ tasks.user_del(master, INNG_USER_NONMEMBER, raiseonerr=False) -+ request.addfinalizer(cleanup) -+ -+ # Create entities -+ tasks.user_add(master, INNG_USER_MEMBER) -+ tasks.user_add(master, INNG_USER_NONMEMBER) -+ tasks.group_add(master, INNG_GRP_MEMBER, ('--desc=test',)) -+ tasks.group_add(master, INNG_GRP_NONMEMBER, ('--desc=test',)) -+ tasks.host_add(master, innghost1, '--force') -+ tasks.host_add(master, innghost2, '--force') -+ tasks.hostgroup_add(master, INNG_HG_MEMBER, ('--desc=test',)) -+ tasks.hostgroup_add(master, INNG_HG_NONMEMBER, ('--desc=test',)) -+ -+ for ng in [INNG_NG_PARENT, INNG_NG_MEMBER, INNG_NG_NONMEMBER]: -+ master.run_command(['ipa', 'netgroup-add', ng, '--desc=test']) -+ -+ # Add members to parent netgroup only -+ master.run_command(['ipa', 'netgroup-add-member', INNG_NG_PARENT, -+ f'--users={INNG_USER_MEMBER}', -+ f'--groups={INNG_GRP_MEMBER}', -+ f'--hosts={innghost1}', -+ f'--hostgroups={INNG_HG_MEMBER}', -+ f'--netgroups={INNG_NG_MEMBER}']) -+ -+ def test_in_netgroups_across_entities(self, in_netgroups_setup): -+ """Test --in-netgroups and --not-in-netgroups for all entity types.""" -+ master = self.master -+ domain = master.domain.name -+ -+ entity_tests = [ -+ ('user-find', INNG_USER_MEMBER, INNG_USER_NONMEMBER), -+ ('group-find', INNG_GRP_MEMBER, INNG_GRP_NONMEMBER), -+ ('host-find', f'innghost1.{domain}', f'innghost2.{domain}'), -+ ('hostgroup-find', INNG_HG_MEMBER, INNG_HG_NONMEMBER), -+ ('netgroup-find', INNG_NG_MEMBER, INNG_NG_NONMEMBER), -+ ] -+ -+ for cmd, member, non_member in entity_tests: -+ # Test --in-netgroups -+ result = master.run_command( -+ ['ipa', cmd, f'--in-netgroups={INNG_NG_PARENT}'] -+ ) -+ assert member in result.stdout_text, \ -+ f"--in-netgroups failed for {cmd}" -+ assert non_member not in result.stdout_text -+ -+ # Test --not-in-netgroups -+ result = master.run_command( -+ ['ipa', cmd, f'--not-in-netgroups={INNG_NG_PARENT}'] -+ ) -+ assert non_member in result.stdout_text, \ -+ f"--not-in-netgroups failed for {cmd}" -+ -+ def test_removed_member_not_in_netgroups(self): -+ """Verify removed member no longer appears in --in-netgroups.""" -+ master = self.master -+ user = 'removal_test_user' -+ netgroup = 'removal_test_ng' -+ -+ try: -+ tasks.user_add(master, user) -+ master.run_command(['ipa', 'netgroup-add', netgroup]) -+ master.run_command([ -+ 'ipa', 'netgroup-add-member', netgroup, f'--users={user}' -+ ]) -+ -+ # Verify user is in netgroup -+ result = master.run_command( -+ ['ipa', 'user-find', f'--in-netgroups={netgroup}'] -+ ) -+ assert user in result.stdout_text -+ -+ # Remove user and verify not in netgroup -+ master.run_command([ -+ 'ipa', 'netgroup-remove-member', netgroup, f'--users={user}' -+ ]) -+ result = master.run_command( -+ ['ipa', 'user-find', f'--in-netgroups={netgroup}'], -+ raiseonerr=False -+ ) -+ assert user not in result.stdout_text -+ -+ finally: -+ master.run_command( -+ ['ipa', 'netgroup-del', netgroup], raiseonerr=False) -+ tasks.user_del(master, user, raiseonerr=False) -+ -+ def test_delete_multiple_netgroups(self): -+ """Delete multiple netgroups and verify they're deleted.""" -+ master = self.master -+ -+ try: -+ # Create multiple netgroups -+ for i in range(1, 4): -+ master.run_command( -+ ['ipa', 'netgroup-add', f'del_ng{i}', '--desc=test'] -+ ) -+ -+ # Delete all at once -+ master.run_command( -+ ['ipa', 'netgroup-del', 'del_ng1', 'del_ng2', 'del_ng3'] -+ ) -+ -+ # Verify all deleted -+ for i in range(1, 4): -+ result = master.run_command( -+ ['ipa', 'netgroup-show', f'del_ng{i}'], raiseonerr=False -+ ) -+ assert result.returncode != 0 -+ assert 'netgroup not found' in result.stderr_text -+ -+ finally: -+ # Cleanup: ensure netgroups are deleted even if test fails -+ for i in range(1, 4): -+ master.run_command( -+ ['ipa', 'netgroup-del', f'del_ng{i}'], raiseonerr=False -+ ) -diff --git a/ipatests/test_xmlrpc/test_netgroup_plugin.py b/ipatests/test_xmlrpc/test_netgroup_plugin.py -index 3f1fc0bc1..bee3e3f8e 100644 ---- a/ipatests/test_xmlrpc/test_netgroup_plugin.py -+++ b/ipatests/test_xmlrpc/test_netgroup_plugin.py -@@ -62,6 +62,27 @@ invalidnisdomain1=u'domain1,domain2' - invalidnisdomain2=u'+invalidnisdomain' - invalidhost=u'+invalid&host' - -+netgroup_nisdomain = 'netgroup_nisdomain' -+netgroup_usercat = 'netgroup_usercat' -+netgroup_hostcat = 'netgroup_hostcat' -+netgroup_usercat_hostcat = 'netgroup_usercat_hostcat' -+netgroup_exthost = 'netgroup_exthost' -+netgroup_usercat_mod = 'netgroup_usercat_mod' -+netgroup_hostcat_mod = 'netgroup_hostcat_mod' -+custom_nisdomain = 'testnis.dom' -+external_host = 'ipaqatesthost' -+ -+# Entities for netgroup-find tests -+fnd_ng1 = 'fndng1' -+fnd_ng2 = 'fndng2' -+fnd_ng3 = 'fndng3' -+fnd_user1 = 'fnduser1' -+fnd_user2 = 'fnduser2' -+fnd_group = 'fndgroup' -+fnd_host = 'fndhost1.%s' % api.env.domain -+fnd_hostgroup = 'fndhostgroup' -+fnd_nisdomain = 'testdomain' -+ - - @pytest.mark.tier1 - class test_netgroup(Declarative): -@@ -72,11 +93,28 @@ class test_netgroup(Declarative): - cleanup_commands = [ - ('netgroup_del', [netgroup1], {}), - ('netgroup_del', [netgroup2], {}), -+ ('netgroup_del', [netgroup_single], {}), -+ ('netgroup_del', [netgroup_nisdomain], {}), -+ ('netgroup_del', [netgroup_usercat], {}), -+ ('netgroup_del', [netgroup_hostcat], {}), -+ ('netgroup_del', [netgroup_usercat_hostcat], {}), -+ ('netgroup_del', [netgroup_exthost], {}), -+ ('netgroup_del', [netgroup_usercat_mod], {}), -+ ('netgroup_del', [netgroup_hostcat_mod], {}), - ('host_del', [host1], {}), - ('hostgroup_del', [hostgroup1], {}), - ('user_del', [user1], {}), - ('user_del', [user2], {}), - ('group_del', [group1], {}), -+ # Cleanup for netgroup-find tests -+ ('netgroup_del', [fnd_ng1], {}), -+ ('netgroup_del', [fnd_ng2], {}), -+ ('netgroup_del', [fnd_ng3], {}), -+ ('hostgroup_del', [fnd_hostgroup], {}), -+ ('host_del', [fnd_host], {}), -+ ('group_del', [fnd_group], {}), -+ ('user_del', [fnd_user1], {}), -+ ('user_del', [fnd_user2], {}), - ] - - tests=[ -@@ -133,6 +171,106 @@ class test_netgroup(Declarative): - ), - - -+ dict( -+ desc='Test netgroup add with space in nisdomain', -+ command=( -+ 'netgroup_add', [netgroup1], -+ dict(description='Test', nisdomainname=' ') -+ ), -+ expected=errors.ValidationError( -+ name='nisdomain', -+ error='may only include letters, numbers, _, -, and .'), -+ ), -+ -+ -+ dict( -+ desc='Test netgroup add with space in description', -+ command=( -+ 'netgroup_add', [netgroup1], -+ dict(description=' ') -+ ), -+ expected=errors.ValidationError( -+ name='desc', -+ error='Leading and trailing spaces are not allowed'), -+ ), -+ -+ -+ dict( -+ desc='Test netgroup add with invalid usercat', -+ command=( -+ 'netgroup_add', [netgroup1], -+ dict(description='Test', usercategory='badcat') -+ ), -+ expected=errors.ValidationError( -+ name='usercat', error="must be 'all'"), -+ ), -+ -+ -+ dict( -+ desc='Test netgroup add with space for usercat', -+ command=( -+ 'netgroup_add', [netgroup1], -+ dict(description='Test', usercategory=' ') -+ ), -+ expected=errors.ValidationError( -+ name='usercat', error="must be 'all'"), -+ ), -+ -+ -+ dict( -+ desc='Test netgroup add with invalid hostcat', -+ command=( -+ 'netgroup_add', [netgroup1], -+ dict(description='Test', hostcategory='badcat') -+ ), -+ expected=errors.ValidationError( -+ name='hostcat', error="must be 'all'"), -+ ), -+ -+ -+ dict( -+ desc='Test netgroup add with space for hostcat', -+ command=( -+ 'netgroup_add', [netgroup1], -+ dict(description='Test', hostcategory=' ') -+ ), -+ expected=errors.ValidationError( -+ name='hostcat', error="must be 'all'"), -+ ), -+ -+ -+ dict( -+ desc='Test netgroup add with both desc and addattr description', -+ command=( -+ 'netgroup_add', [netgroup1], -+ dict(description='Test', addattr='description=duplicate') -+ ), -+ expected=errors.OnlyOneValueAllowed(attr='description'), -+ ), -+ -+ -+ dict( -+ desc='Test netgroup add with invalid setattr attribute', -+ command=( -+ 'netgroup_add', [netgroup1], -+ dict(description='Test', setattr='badattr=somevalue') -+ ), -+ expected=errors.ObjectclassViolation( -+ info='attribute "badattr" not allowed'), -+ ), -+ -+ -+ dict( -+ desc='Test netgroup add with invalid addattr attribute', -+ command=( -+ 'netgroup_add', [netgroup1], -+ dict(description='Test', addattr='badattr=somevalue') -+ ), -+ expected=errors.ObjectclassViolation( -+ info='attribute "badattr" not allowed'), -+ ), -+ -+ - dict( - desc='Create %r' % netgroup1, - command=('netgroup_add', [netgroup1], -@@ -204,6 +342,178 @@ class test_netgroup(Declarative): - ), - - -+ dict( -+ desc='Create netgroup %r with custom nisdomain' % ( -+ netgroup_nisdomain), -+ command=( -+ 'netgroup_add', [netgroup_nisdomain], -+ dict(description='Test with custom nisdomain', -+ nisdomainname=custom_nisdomain) -+ ), -+ expected=dict( -+ value=netgroup_nisdomain, -+ summary='Added netgroup "%s"' % netgroup_nisdomain, -+ result=dict( -+ dn=fuzzy_netgroupdn, -+ cn=[netgroup_nisdomain], -+ objectclass=objectclasses.netgroup, -+ description=['Test with custom nisdomain'], -+ nisdomainname=[custom_nisdomain], -+ ipauniqueid=[fuzzy_uuid], -+ ), -+ ), -+ ), -+ -+ dict( -+ desc='Delete %r' % netgroup_nisdomain, -+ command=('netgroup_del', [netgroup_nisdomain], {}), -+ expected=dict( -+ value=[netgroup_nisdomain], -+ summary='Deleted netgroup "%s"' % netgroup_nisdomain, -+ result=dict(failed=[]), -+ ), -+ ), -+ -+ -+ dict( -+ desc='Create netgroup %r with usercat=all' % netgroup_usercat, -+ command=( -+ 'netgroup_add', [netgroup_usercat], -+ dict(description='Test with usercat all', -+ nisdomainname=custom_nisdomain, usercategory='all') -+ ), -+ expected=dict( -+ value=netgroup_usercat, -+ summary='Added netgroup "%s"' % netgroup_usercat, -+ result=dict( -+ dn=fuzzy_netgroupdn, -+ cn=[netgroup_usercat], -+ objectclass=objectclasses.netgroup, -+ description=['Test with usercat all'], -+ nisdomainname=[custom_nisdomain], -+ usercategory=['all'], -+ ipauniqueid=[fuzzy_uuid], -+ ), -+ ), -+ ), -+ -+ dict( -+ desc='Delete %r' % netgroup_usercat, -+ command=('netgroup_del', [netgroup_usercat], {}), -+ expected=dict( -+ value=[netgroup_usercat], -+ summary='Deleted netgroup "%s"' % netgroup_usercat, -+ result=dict(failed=[]), -+ ), -+ ), -+ -+ -+ dict( -+ desc='Create netgroup %r with hostcat=all' % netgroup_hostcat, -+ command=( -+ 'netgroup_add', [netgroup_hostcat], -+ dict(description='Test with hostcat all', -+ nisdomainname=custom_nisdomain, hostcategory='all') -+ ), -+ expected=dict( -+ value=netgroup_hostcat, -+ summary='Added netgroup "%s"' % netgroup_hostcat, -+ result=dict( -+ dn=fuzzy_netgroupdn, -+ cn=[netgroup_hostcat], -+ objectclass=objectclasses.netgroup, -+ description=['Test with hostcat all'], -+ nisdomainname=[custom_nisdomain], -+ hostcategory=['all'], -+ ipauniqueid=[fuzzy_uuid], -+ ), -+ ), -+ ), -+ -+ dict( -+ desc='Delete %r' % netgroup_hostcat, -+ command=('netgroup_del', [netgroup_hostcat], {}), -+ expected=dict( -+ value=[netgroup_hostcat], -+ summary='Deleted netgroup "%s"' % netgroup_hostcat, -+ result=dict(failed=[]), -+ ), -+ ), -+ -+ -+ dict( -+ desc='Create netgroup %r with usercat/hostcat=all' % ( -+ netgroup_usercat_hostcat), -+ command=( -+ 'netgroup_add', [netgroup_usercat_hostcat], -+ dict(description='Test with usercat and hostcat all', -+ nisdomainname=custom_nisdomain, -+ usercategory='all', hostcategory='all') -+ ), -+ expected=dict( -+ value=netgroup_usercat_hostcat, -+ summary='Added netgroup "%s"' % netgroup_usercat_hostcat, -+ result=dict( -+ dn=fuzzy_netgroupdn, -+ cn=[netgroup_usercat_hostcat], -+ objectclass=objectclasses.netgroup, -+ description=['Test with usercat and hostcat all'], -+ nisdomainname=[custom_nisdomain], -+ usercategory=['all'], -+ hostcategory=['all'], -+ ipauniqueid=[fuzzy_uuid], -+ ), -+ ), -+ ), -+ -+ dict( -+ desc='Delete %r' % netgroup_usercat_hostcat, -+ command=('netgroup_del', [netgroup_usercat_hostcat], {}), -+ expected=dict( -+ value=[netgroup_usercat_hostcat], -+ summary='Deleted netgroup "%s"' % netgroup_usercat_hostcat, -+ result=dict(failed=[]), -+ ), -+ ), -+ -+ -+ dict( -+ desc='Create netgroup %r with externalHost' % netgroup_exthost, -+ command=( -+ 'netgroup_add', [netgroup_exthost], -+ dict(description='Test with externalHost', -+ nisdomainname=custom_nisdomain, -+ usercategory='all', hostcategory='all', -+ addattr='externalHost=%s' % external_host) -+ ), -+ expected=dict( -+ value=netgroup_exthost, -+ summary='Added netgroup "%s"' % netgroup_exthost, -+ result=dict( -+ dn=fuzzy_netgroupdn, -+ cn=[netgroup_exthost], -+ objectclass=objectclasses.netgroup, -+ description=['Test with externalHost'], -+ nisdomainname=[custom_nisdomain], -+ usercategory=['all'], -+ hostcategory=['all'], -+ externalhost=[external_host], -+ ipauniqueid=[fuzzy_uuid], -+ ), -+ ), -+ ), -+ -+ dict( -+ desc='Delete %r' % netgroup_exthost, -+ command=('netgroup_del', [netgroup_exthost], {}), -+ expected=dict( -+ value=[netgroup_exthost], -+ summary='Deleted netgroup "%s"' % netgroup_exthost, -+ result=dict(failed=[]), -+ ), -+ ), -+ -+ - dict( - desc='Try to create duplicate %r' % netgroup1, - command=('netgroup_add', [netgroup1], -@@ -628,6 +938,124 @@ class test_netgroup(Declarative): - ), - - -+ dict( -+ desc='Add non-existent user to netgroup %r' % netgroup1, -+ command=( -+ 'netgroup_add_member', [netgroup1], -+ dict(user='notfounduser') -+ ), -+ expected=dict( -+ completed=0, -+ failed=dict( -+ member=dict( -+ netgroup=tuple(), -+ ), -+ memberuser=dict( -+ group=tuple(), -+ user=[('notfounduser', 'no such entry')], -+ ), -+ memberhost=dict( -+ hostgroup=tuple(), -+ host=tuple(), -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Test netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Add non-existent group to netgroup %r' % netgroup1, -+ command=( -+ 'netgroup_add_member', [netgroup1], -+ dict(group='notfoundgroup') -+ ), -+ expected=dict( -+ completed=0, -+ failed=dict( -+ member=dict( -+ netgroup=tuple(), -+ ), -+ memberuser=dict( -+ group=[('notfoundgroup', 'no such entry')], -+ user=tuple(), -+ ), -+ memberhost=dict( -+ hostgroup=tuple(), -+ host=tuple(), -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Test netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Add non-existent hostgroup to netgroup %r' % netgroup1, -+ command=( -+ 'netgroup_add_member', [netgroup1], -+ dict(hostgroup='notfoundhg') -+ ), -+ expected=dict( -+ completed=0, -+ failed=dict( -+ member=dict( -+ netgroup=tuple(), -+ ), -+ memberuser=dict( -+ group=tuple(), -+ user=tuple(), -+ ), -+ memberhost=dict( -+ hostgroup=[('notfoundhg', 'no such entry')], -+ host=tuple(), -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Test netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Add member to non-existent netgroup', -+ command=( -+ 'netgroup_add_member', ['notfoundnetgroup'], dict(user=user1) -+ ), -+ expected=errors.NotFound( -+ reason='notfoundnetgroup: netgroup not found'), -+ ), -+ -+ - dict( - desc='Add duplicate user %r to netgroup %r' % (user1, netgroup1), - command=( -@@ -1035,358 +1463,1543 @@ class test_netgroup(Declarative): - - - dict( -- desc='Update %r' % netgroup1, -- command=('netgroup_mod', [netgroup1], -- dict(description=u'Updated netgroup 1') -+ desc='Search for %r by description' % netgroup1, -+ command=( -+ 'netgroup_find', [], -+ dict(description='Test netgroup 1') - ), - expected=dict( -- value=netgroup1, -- summary=u'Modified netgroup "%s"' % netgroup1, -- result={ -- 'memberhost_host': (host1,), -- 'memberhost_hostgroup': (hostgroup1,), -- 'memberuser_user': (user1,), -- 'memberuser_group': (group1,), -- 'member_netgroup': (netgroup2,), -+ count=1, -+ truncated=False, -+ summary=u'1 netgroup matched', -+ result=[ -+ { -+ 'dn': fuzzy_netgroupdn, - 'cn': [netgroup1], -- 'description': [u'Updated netgroup 1'], -- 'nisdomainname': [u'%s' % api.env.domain], -+ 'description': ['Test netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], - 'externalhost': [unknown_host], -- }, -+ }, -+ ], - ), - ), - - - dict( -- desc='Remove host %r from netgroup %r' % (host1, netgroup1), -+ desc='Search for netgroups by nisdomain', - command=( -- 'netgroup_remove_member', [netgroup1], dict(host=host1) -+ 'netgroup_find', [], -+ dict(nisdomainname='%s' % api.env.domain) - ), - expected=dict( -- completed=1, -- failed=dict( -- member=dict( -- netgroup=tuple(), -- ), -- memberuser=dict( -- group=tuple(), -- user=tuple(), -- ), -- memberhost=dict( -- hostgroup=tuple(), -- host=tuple(), -- ), -- ), -- result={ -+ count=2, -+ truncated=False, -+ summary='2 netgroups matched', -+ result=[ -+ { - 'dn': fuzzy_netgroupdn, -- 'memberhost_hostgroup': (hostgroup1,), -- 'memberuser_user': (user1,), -- 'memberuser_group': (group1,), -- 'member_netgroup': (netgroup2,), - 'cn': [netgroup1], -- 'description': [u'Updated netgroup 1'], -- 'nisdomainname': [u'%s' % api.env.domain], -+ 'description': ['Test netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], - 'externalhost': [unknown_host], -- }, -+ }, -+ { -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [netgroup2], -+ 'description': ['Test netgroup 2'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ }, -+ ], - ), - ), - - - dict( -- desc='Remove hostgroup %r from netgroup %r' % (hostgroup1, netgroup1), -- command=( -- 'netgroup_remove_member', [netgroup1], dict(hostgroup=hostgroup1) -+ desc='Search with non-existent criteria returns zero', -+ command=('netgroup_find', ['doesnotexist'], {}), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ -+ dict( -+ desc='Search with non-existent description returns zero', -+ command=('netgroup_find', [], dict(description='baddesc')), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], - ), -+ ), -+ -+ -+ dict( -+ desc='Search with non-existent nisdomain returns zero', -+ command=('netgroup_find', [], dict(nisdomainname='baddomain')), - expected=dict( -- completed=1, -- failed=dict( -- member=dict( -- netgroup=tuple(), -- ), -- memberuser=dict( -- group=tuple(), -- user=tuple(), -- ), -- memberhost=dict( -- hostgroup=tuple(), -- host=tuple(), -- ), -- ), -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ -+ dict( -+ desc='Update %r' % netgroup1, -+ command=('netgroup_mod', [netgroup1], -+ dict(description='Updated netgroup 1')), -+ expected=dict( -+ value=netgroup1, -+ summary='Modified netgroup "%s"' % netgroup1, - result={ -- 'dn': fuzzy_netgroupdn, -- 'memberuser_user': (user1,), -- 'memberuser_group': (group1,), -- 'member_netgroup': (netgroup2,), -- 'cn': [netgroup1], -- 'description': [u'Updated netgroup 1'], -- 'nisdomainname': [u'%s' % api.env.domain], -- 'externalhost': [unknown_host], -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], - }, - ), - ), - - - dict( -- desc='Remove user %r from netgroup %r' % (user1, netgroup1), -+ desc='Modify nisdomain of %r' % netgroup1, - command=( -- 'netgroup_remove_member', [netgroup1], dict(user=user1) -+ 'netgroup_mod', [netgroup1], -+ dict(nisdomainname='newnisdom1') - ), - expected=dict( -- completed=1, -- failed=dict( -- member=dict( -- netgroup=tuple(), -- ), -- memberuser=dict( -- group=tuple(), -- user=tuple(), -- ), -- memberhost=dict( -- hostgroup=tuple(), -- host=tuple(), -- ), -- ), -+ value=netgroup1, -+ summary='Modified netgroup "%s"' % netgroup1, - result={ -- 'dn': fuzzy_netgroupdn, -- 'memberuser_group': (group1,), -- 'member_netgroup': (netgroup2,), -- 'cn': [netgroup1], -- 'description': [u'Updated netgroup 1'], -- 'nisdomainname': [u'%s' % api.env.domain], -- 'externalhost': [unknown_host], -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['newnisdom1'], -+ 'externalhost': [unknown_host], - }, - ), - ), - - - dict( -- desc='Remove group %r from netgroup %r' % (group1, netgroup1), -+ desc='Restore nisdomain of %r' % netgroup1, - command=( -- 'netgroup_remove_member', [netgroup1], dict(group=group1) -+ 'netgroup_mod', [netgroup1], -+ dict(nisdomainname='%s' % api.env.domain) - ), - expected=dict( -- completed=1, -- failed=dict( -- member=dict( -- netgroup=tuple(), -- ), -- memberuser=dict( -- group=tuple(), -- user=tuple(), -- ), -- memberhost=dict( -- hostgroup=tuple(), -- host=tuple(), -- ), -- ), -+ value=netgroup1, -+ summary='Modified netgroup "%s"' % netgroup1, - result={ -- 'dn': fuzzy_netgroupdn, -- 'member_netgroup': (netgroup2,), -- 'cn': [netgroup1], -- 'description': [u'Updated netgroup 1'], -- 'nisdomainname': [u'%s' % api.env.domain], -- 'externalhost': [unknown_host], -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], - }, - ), - ), - - - dict( -- desc='Remove netgroup %r from netgroup %r' % (netgroup2, netgroup1), -+ desc='Modify description using setattr for %r' % netgroup1, - command=( -- 'netgroup_remove_member', [netgroup1], dict(netgroup=netgroup2) -+ 'netgroup_mod', [netgroup1], -+ dict(setattr='description=setattr description') - ), - expected=dict( -- completed=1, -- failed=dict( -- member=dict( -- netgroup=tuple(), -- ), -- memberuser=dict( -- group=tuple(), -- user=tuple(), -- ), -- memberhost=dict( -- hostgroup=tuple(), -- host=tuple(), -- ), -- ), -+ value=netgroup1, -+ summary='Modified netgroup "%s"' % netgroup1, - result={ -- 'dn': fuzzy_netgroupdn, -- 'cn': [netgroup1], -- 'description': [u'Updated netgroup 1'], -- 'nisdomainname': [u'%s' % api.env.domain], -- 'externalhost': [unknown_host], -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['setattr description'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], - }, - ), - ), - - - dict( -- desc='Remove host %r from netgroup %r again' % (host1, netgroup1), -+ desc='Modify nisdomain using setattr for %r' % netgroup1, - command=( -- 'netgroup_remove_member', [netgroup1], dict(host=host1) -+ 'netgroup_mod', [netgroup1], -+ dict(setattr='nisdomainname=setattrnisdom') - ), - expected=dict( -- completed=0, -- failed=dict( -- member=dict( -- netgroup=tuple(), -- ), -- memberuser=dict( -- group=tuple(), -- user=tuple(), -- ), -- memberhost=dict( -- hostgroup=tuple(), -- host=[('%s' % host1, u'This entry is not a member')] -- ), -- ), -+ value=netgroup1, -+ summary='Modified netgroup "%s"' % netgroup1, - result={ -- 'dn': fuzzy_netgroupdn, -- 'cn': [netgroup1], -- 'description': [u'Updated netgroup 1'], -- 'nisdomainname': [u'%s' % api.env.domain], -- 'externalhost': [unknown_host], -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['setattr description'], -+ 'nisdomainname': ['setattrnisdom'], -+ 'externalhost': [unknown_host], - }, - ), - ), - - - dict( -- desc='Remove hostgroup %r from netgroup %r again' % (hostgroup1, netgroup1), -+ desc='Restore description and nisdomain for %r' % netgroup1, - command=( -- 'netgroup_remove_member', [netgroup1], dict(hostgroup=hostgroup1) -+ 'netgroup_mod', [netgroup1], -+ dict(description='Updated netgroup 1', -+ nisdomainname='%s' % api.env.domain) - ), - expected=dict( -- completed=0, -- failed=dict( -- member=dict( -- netgroup=tuple(), -- ), -- memberuser=dict( -- group=tuple(), -- user=tuple(), -- ), -- memberhost=dict( -- hostgroup=[('%s' % hostgroup1, u'This entry is not a member')], -- host=tuple(), -- ), -- ), -+ value=netgroup1, -+ summary='Modified netgroup "%s"' % netgroup1, - result={ -- 'dn': fuzzy_netgroupdn, -- 'cn': [netgroup1], -- 'description': [u'Updated netgroup 1'], -- 'nisdomainname': [u'%s' % api.env.domain], -- 'externalhost': [unknown_host], -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], - }, - ), - ), - - - dict( -- desc='Remove user %r from netgroup %r again' % (user1, netgroup1), -+ desc='Set externalhost using setattr for %r' % netgroup1, - command=( -- 'netgroup_remove_member', [netgroup1], dict(user=user1) -+ 'netgroup_mod', [netgroup1], -+ dict(setattr='externalhost=setattr_exthost') - ), - expected=dict( -- completed=0, -- failed=dict( -- member=dict( -- netgroup=tuple(), -- ), -- memberuser=dict( -- group=tuple(), -- user=[('%s' % user1, u'This entry is not a member')], -- ), -- memberhost=dict( -- hostgroup=tuple(), -- host=tuple(), -- ), -- ), -+ value=netgroup1, -+ summary='Modified netgroup "%s"' % netgroup1, - result={ -- 'dn': fuzzy_netgroupdn, -- 'cn': [netgroup1], -- 'description': [u'Updated netgroup 1'], -- 'nisdomainname': [u'%s' % api.env.domain], -- 'externalhost': [unknown_host], -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': ['setattr_exthost'], - }, - ), - ), - - - dict( -- desc='Remove group %r from netgroup %r again' % (group1, netgroup1), -+ desc='Add externalhost using addattr for %r' % netgroup1, - command=( -- 'netgroup_remove_member', [netgroup1], dict(group=group1) -+ 'netgroup_mod', [netgroup1], -+ dict(addattr='externalhost=addattr_exthost') - ), - expected=dict( -- completed=0, -- failed=dict( -- member=dict( -- netgroup=tuple(), -- ), -- memberuser=dict( -- group= [('%s' % group1, u'This entry is not a member')], -- user=tuple(), -- ), -- memberhost=dict( -- hostgroup=tuple(), -- host=tuple(), -- ), -- ), -+ value=netgroup1, -+ summary='Modified netgroup "%s"' % netgroup1, - result={ -- 'dn': fuzzy_netgroupdn, -- 'cn': [netgroup1], -- 'description': [u'Updated netgroup 1'], -- 'nisdomainname': [u'%s' % api.env.domain], -- 'externalhost': [unknown_host], -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': ['addattr_exthost', 'setattr_exthost'], - }, - ), - ), - - - dict( -- desc='Remove netgroup %r from netgroup %r again' % (netgroup2, netgroup1), -+ desc='Delete one externalhost using delattr for %r' % netgroup1, - command=( -- 'netgroup_remove_member', [netgroup1], dict(netgroup=netgroup2) -+ 'netgroup_mod', [netgroup1], -+ dict(delattr='externalhost=setattr_exthost') - ), - expected=dict( -- completed=0, -- failed=dict( -- member=dict( -- netgroup=[('%s' % netgroup2, u'This entry is not a member')], -- ), -- memberuser=dict( -- group=tuple(), -- user=tuple(), -- ), -- memberhost=dict( -- hostgroup=tuple(), -- host=tuple(), -- ), -- ), -+ value=netgroup1, -+ summary='Modified netgroup "%s"' % netgroup1, - result={ -- 'dn': fuzzy_netgroupdn, -- 'cn': [netgroup1], -- 'description': [u'Updated netgroup 1'], -- 'nisdomainname': [u'%s' % api.env.domain], -- 'externalhost': [unknown_host], -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': ['addattr_exthost'], - }, - ), - ), - - - dict( -- desc='Delete %r' % netgroup1, -- command=('netgroup_del', [netgroup1], {}), -+ desc='Clear all externalhosts using setattr for %r' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(setattr='externalhost=') -+ ), - expected=dict( -- value=[netgroup1], -- summary=u'Deleted netgroup "%s"' % netgroup1, -- result=dict(failed=[]), -+ value=netgroup1, -+ summary='Modified netgroup "%s"' % netgroup1, -+ result={ -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ }, - ), - ), - -+ -+ dict( -+ desc='Restore externalhost for %r' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(addattr='externalhost=%s' % unknown_host) -+ ), -+ expected=dict( -+ value=netgroup1, -+ summary='Modified netgroup "%s"' % netgroup1, -+ result={ -+ 'memberhost_host': (host1,), -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Modify %r invalid usercat' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(usercategory='badcat') -+ ), -+ expected=errors.ValidationError( -+ name='usercat', error="must be 'all'"), -+ ), -+ -+ -+ dict( -+ desc='Modify %r invalid hostcat' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(hostcategory='badcat') -+ ), -+ expected=errors.ValidationError( -+ name='hostcat', error="must be 'all'"), -+ ), -+ -+ -+ dict( -+ desc='Modify %r with invalid addattr on nisDomainName' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(addattr='nisdomainname=seconddomain') -+ ), -+ expected=errors.OnlyOneValueAllowed(attr='nisdomainname'), -+ ), -+ -+ -+ dict( -+ desc='Modify %r with invalid setattr on ipauniqueid' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(setattr='ipauniqueid=mynew-unique-id') -+ ), -+ expected=errors.ValidationError( -+ name='ipauniqueid', error='attribute is not configurable'), -+ ), -+ -+ -+ dict( -+ desc='Modify %r with invalid setattr on dn' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(setattr='dn=cn=newdn') -+ ), -+ expected=errors.ObjectclassViolation( -+ info='attribute "distinguishedName" not allowed'), -+ ), -+ -+ -+ dict( -+ desc='Modify %r with invalid membergroup attribute' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(setattr='membergroup=%s' % group1) -+ ), -+ expected=errors.ObjectclassViolation( -+ info='attribute "membergroup" not allowed'), -+ ), -+ -+ -+ dict( -+ desc='Modify %r with invalid memberhostgroup attr' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(setattr='memberhostgroup=%s' % hostgroup1) -+ ), -+ expected=errors.ObjectclassViolation( -+ info='attribute "memberhostgroup" not allowed'), -+ ), -+ -+ -+ dict( -+ desc='Modify %r with invalid membernetgroup attribute' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(setattr='membernetgroup=%s' % netgroup2) -+ ), -+ expected=errors.ObjectclassViolation( -+ info='attribute "membernetgroup" not allowed'), -+ ), -+ -+ -+ dict( -+ desc='Modify %r with invalid addattr on description' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(addattr='description=newdesc') -+ ), -+ expected=errors.OnlyOneValueAllowed(attr='description'), -+ ), -+ -+ -+ dict( -+ desc='Modify %r with invalid nisdomain commas' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(nisdomainname='test1,test2') -+ ), -+ expected=errors.ValidationError( -+ name='nisdomain', -+ error='may only include letters, numbers, _, -, and .'), -+ ), -+ -+ -+ dict( -+ desc='Modify %r with invalid setattr nisdomain commas' % netgroup1, -+ command=( -+ 'netgroup_mod', [netgroup1], -+ dict(setattr='nisdomainname=test1,test2') -+ ), -+ expected=errors.ValidationError( -+ name='nisdomainname', -+ error='may only include letters, numbers, _, -, and .'), -+ ), -+ -+ -+ dict( -+ desc='Remove host %r from netgroup %r' % (host1, netgroup1), -+ command=( -+ 'netgroup_remove_member', [netgroup1], dict(host=host1) -+ ), -+ expected=dict( -+ completed=1, -+ failed=dict( -+ member=dict( -+ netgroup=tuple(), -+ ), -+ memberuser=dict( -+ group=tuple(), -+ user=tuple(), -+ ), -+ memberhost=dict( -+ hostgroup=tuple(), -+ host=tuple(), -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'memberhost_hostgroup': (hostgroup1,), -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Remove hostgroup %r from netgroup %r' % ( -+ hostgroup1, netgroup1), -+ command=( -+ 'netgroup_remove_member', [netgroup1], -+ dict(hostgroup=hostgroup1) -+ ), -+ expected=dict( -+ completed=1, -+ failed=dict( -+ member=dict( -+ netgroup=tuple(), -+ ), -+ memberuser=dict( -+ group=tuple(), -+ user=tuple(), -+ ), -+ memberhost=dict( -+ hostgroup=tuple(), -+ host=tuple(), -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'memberuser_user': (user1,), -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Remove user %r from netgroup %r' % (user1, netgroup1), -+ command=( -+ 'netgroup_remove_member', [netgroup1], dict(user=user1) -+ ), -+ expected=dict( -+ completed=1, -+ failed=dict( -+ member=dict( -+ netgroup=tuple(), -+ ), -+ memberuser=dict( -+ group=tuple(), -+ user=tuple(), -+ ), -+ memberhost=dict( -+ hostgroup=tuple(), -+ host=tuple(), -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'memberuser_group': (group1,), -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Remove group %r from netgroup %r' % (group1, netgroup1), -+ command=( -+ 'netgroup_remove_member', [netgroup1], dict(group=group1) -+ ), -+ expected=dict( -+ completed=1, -+ failed=dict( -+ member=dict( -+ netgroup=tuple(), -+ ), -+ memberuser=dict( -+ group=tuple(), -+ user=tuple(), -+ ), -+ memberhost=dict( -+ hostgroup=tuple(), -+ host=tuple(), -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'member_netgroup': (netgroup2,), -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Remove netgroup %r from netgroup %r' % (netgroup2, netgroup1), -+ command=( -+ 'netgroup_remove_member', [netgroup1], dict(netgroup=netgroup2) -+ ), -+ expected=dict( -+ completed=1, -+ failed=dict( -+ member=dict( -+ netgroup=tuple(), -+ ), -+ memberuser=dict( -+ group=tuple(), -+ user=tuple(), -+ ), -+ memberhost=dict( -+ hostgroup=tuple(), -+ host=tuple(), -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Remove host %r from netgroup %r again' % (host1, netgroup1), -+ command=( -+ 'netgroup_remove_member', [netgroup1], dict(host=host1) -+ ), -+ expected=dict( -+ completed=0, -+ failed=dict( -+ member=dict( -+ netgroup=tuple(), -+ ), -+ memberuser=dict( -+ group=tuple(), -+ user=tuple(), -+ ), -+ memberhost=dict( -+ hostgroup=tuple(), -+ host=[('%s' % host1, 'This entry is not a member')] -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Remove hostgroup %r from netgroup %r again' % ( -+ hostgroup1, netgroup1), -+ command=( -+ 'netgroup_remove_member', [netgroup1], -+ dict(hostgroup=hostgroup1) -+ ), -+ expected=dict( -+ completed=0, -+ failed=dict( -+ member=dict( -+ netgroup=tuple(), -+ ), -+ memberuser=dict( -+ group=tuple(), -+ user=tuple(), -+ ), -+ memberhost=dict( -+ hostgroup=[( -+ '%s' % hostgroup1, -+ 'This entry is not a member')], -+ host=tuple(), -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Remove user %r from netgroup %r again' % (user1, netgroup1), -+ command=( -+ 'netgroup_remove_member', [netgroup1], dict(user=user1) -+ ), -+ expected=dict( -+ completed=0, -+ failed=dict( -+ member=dict( -+ netgroup=tuple(), -+ ), -+ memberuser=dict( -+ group=tuple(), -+ user=[('%s' % user1, 'This entry is not a member')], -+ ), -+ memberhost=dict( -+ hostgroup=tuple(), -+ host=tuple(), -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Remove group %r from netgroup %r again' % (group1, netgroup1), -+ command=( -+ 'netgroup_remove_member', [netgroup1], dict(group=group1) -+ ), -+ expected=dict( -+ completed=0, -+ failed=dict( -+ member=dict( -+ netgroup=tuple(), -+ ), -+ memberuser=dict( -+ group=[('%s' % group1, 'This entry is not a member')], -+ user=tuple(), -+ ), -+ memberhost=dict( -+ hostgroup=tuple(), -+ host=tuple(), -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Remove netgroup %r from netgroup %r again' % ( -+ netgroup2, netgroup1), -+ command=( -+ 'netgroup_remove_member', [netgroup1], dict(netgroup=netgroup2) -+ ), -+ expected=dict( -+ completed=0, -+ failed=dict( -+ member=dict( -+ netgroup=[( -+ '%s' % netgroup2, -+ 'This entry is not a member')], -+ ), -+ memberuser=dict( -+ group=tuple(), -+ user=tuple(), -+ ), -+ memberhost=dict( -+ hostgroup=tuple(), -+ host=tuple(), -+ ), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [netgroup1], -+ 'description': ['Updated netgroup 1'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'externalhost': [unknown_host], -+ }, -+ ), -+ ), -+ -+ -+ dict( -+ desc='Remove member from non-existent netgroup', -+ command=( -+ 'netgroup_remove_member', ['notfoundnetgroup'], -+ dict(user=user1) -+ ), -+ expected=errors.NotFound( -+ reason='notfoundnetgroup: netgroup not found'), -+ ), -+ -+ -+ dict( -+ desc='Create %r with usercat=all' % netgroup_usercat_mod, -+ command=( -+ 'netgroup_add', [netgroup_usercat_mod], -+ dict(description='Test usercat mod', -+ nisdomainname=custom_nisdomain, usercategory='all') -+ ), -+ expected=dict( -+ value=netgroup_usercat_mod, -+ summary='Added netgroup "%s"' % netgroup_usercat_mod, -+ result=dict( -+ dn=fuzzy_netgroupdn, -+ cn=[netgroup_usercat_mod], -+ objectclass=objectclasses.netgroup, -+ description=['Test usercat mod'], -+ nisdomainname=[custom_nisdomain], -+ usercategory=['all'], -+ ipauniqueid=[fuzzy_uuid], -+ ), -+ ), -+ ), -+ -+ dict( -+ desc='Clear usercat via setattr for %r' % netgroup_usercat_mod, -+ command=( -+ 'netgroup_mod', [netgroup_usercat_mod], -+ dict(setattr='usercategory=') -+ ), -+ expected=dict( -+ value=netgroup_usercat_mod, -+ summary='Modified netgroup "%s"' % netgroup_usercat_mod, -+ result={ -+ 'cn': [netgroup_usercat_mod], -+ 'description': ['Test usercat mod'], -+ 'nisdomainname': [custom_nisdomain], -+ }, -+ ), -+ ), -+ -+ dict( -+ desc='Create %r with hostcat=all' % netgroup_hostcat_mod, -+ command=( -+ 'netgroup_add', [netgroup_hostcat_mod], -+ dict(description='Test hostcat mod', -+ nisdomainname=custom_nisdomain, hostcategory='all') -+ ), -+ expected=dict( -+ value=netgroup_hostcat_mod, -+ summary='Added netgroup "%s"' % netgroup_hostcat_mod, -+ result=dict( -+ dn=fuzzy_netgroupdn, -+ cn=[netgroup_hostcat_mod], -+ objectclass=objectclasses.netgroup, -+ description=['Test hostcat mod'], -+ nisdomainname=[custom_nisdomain], -+ hostcategory=['all'], -+ ipauniqueid=[fuzzy_uuid], -+ ), -+ ), -+ ), -+ -+ dict( -+ desc='Clear hostcat via setattr for %r' % netgroup_hostcat_mod, -+ command=( -+ 'netgroup_mod', [netgroup_hostcat_mod], -+ dict(setattr='hostcategory=') -+ ), -+ expected=dict( -+ value=netgroup_hostcat_mod, -+ summary='Modified netgroup "%s"' % netgroup_hostcat_mod, -+ result={ -+ 'cn': [netgroup_hostcat_mod], -+ 'description': ['Test hostcat mod'], -+ 'nisdomainname': [custom_nisdomain], -+ }, -+ ), -+ ), -+ -+ -+ # ===== netgroup-find tests ===== -+ # Setup: Create test entities for find tests -+ dict( -+ desc='Create user %r for find tests' % fnd_user1, -+ command=( -+ 'user_add', [fnd_user1], -+ dict(givenname='Find', sn='User1') -+ ), -+ expected=dict( -+ value=fnd_user1, -+ summary='Added user "%s"' % fnd_user1, -+ result=get_user_result(fnd_user1, 'Find', 'User1', 'add'), -+ ), -+ ), -+ -+ dict( -+ desc='Create user %r for find tests' % fnd_user2, -+ command=( -+ 'user_add', [fnd_user2], -+ dict(givenname='Find', sn='User2') -+ ), -+ expected=dict( -+ value=fnd_user2, -+ summary='Added user "%s"' % fnd_user2, -+ result=get_user_result(fnd_user2, 'Find', 'User2', 'add'), -+ ), -+ ), -+ -+ dict( -+ desc='Create group %r for find tests' % fnd_group, -+ command=( -+ 'group_add', [fnd_group], -+ dict(description='test') -+ ), -+ expected=dict( -+ value=fnd_group, -+ summary='Added group "%s"' % fnd_group, -+ result=dict( -+ cn=[fnd_group], -+ description=['test'], -+ gidnumber=[fuzzy_digits], -+ objectclass=fuzzy_set_optional_oc( -+ objectclasses.posixgroup, 'ipantgroupattrs'), -+ ipauniqueid=[fuzzy_uuid], -+ dn=DN( -+ ('cn', fnd_group), ('cn', 'groups'), -+ ('cn', 'accounts'), api.env.basedn), -+ ), -+ ), -+ ), -+ -+ dict( -+ desc='Create host %r for find tests' % fnd_host, -+ command=( -+ 'host_add', [fnd_host], -+ dict( -+ description='Test host for find', -+ l='Undisclosed location', -+ force=True, -+ ) -+ ), -+ expected=dict( -+ value=fnd_host, -+ summary='Added host "%s"' % fnd_host, -+ result=dict( -+ dn=DN( -+ ('fqdn', fnd_host), ('cn', 'computers'), -+ ('cn', 'accounts'), api.env.basedn), -+ fqdn=[fnd_host], -+ description=['Test host for find'], -+ l=['Undisclosed location'], -+ krbprincipalname=[ -+ 'host/%s@%s' % (fnd_host, api.env.realm)], -+ krbcanonicalname=[ -+ 'host/%s@%s' % (fnd_host, api.env.realm)], -+ objectclass=objectclasses.host, -+ ipauniqueid=[fuzzy_uuid], -+ managedby_host=[fnd_host], -+ has_keytab=False, -+ has_password=False, -+ ), -+ ), -+ ), -+ -+ dict( -+ desc='Create hostgroup %r for find tests' % fnd_hostgroup, -+ command=( -+ 'hostgroup_add', [fnd_hostgroup], -+ dict(description='test') -+ ), -+ expected=dict( -+ value=fnd_hostgroup, -+ summary='Added hostgroup "%s"' % fnd_hostgroup, -+ result=dict( -+ dn=DN( -+ ('cn', fnd_hostgroup), ('cn', 'hostgroups'), -+ ('cn', 'accounts'), api.env.basedn), -+ cn=[fnd_hostgroup], -+ objectclass=objectclasses.hostgroup, -+ description=['test'], -+ mepmanagedentry=[DN( -+ ('cn', fnd_hostgroup), ('cn', 'ng'), ('cn', 'alt'), -+ api.env.basedn)], -+ ipauniqueid=[fuzzy_uuid], -+ ), -+ ), -+ ), -+ -+ # Create netgroups with various attributes for find tests -+ dict( -+ desc='Create netgroup %r with nisdomain for find tests' % fnd_ng1, -+ command=( -+ 'netgroup_add', [fnd_ng1], -+ dict(description='findtest', nisdomainname=fnd_nisdomain) -+ ), -+ expected=dict( -+ value=fnd_ng1, -+ summary='Added netgroup "%s"' % fnd_ng1, -+ result=dict( -+ dn=fuzzy_netgroupdn, -+ cn=[fnd_ng1], -+ objectclass=objectclasses.netgroup, -+ description=['findtest'], -+ nisdomainname=[fnd_nisdomain], -+ ipauniqueid=[fuzzy_uuid], -+ ), -+ ), -+ ), -+ -+ dict( -+ desc='Create netgroup %r with usercat/hostcat=all' % fnd_ng2, -+ command=( -+ 'netgroup_add', [fnd_ng2], -+ dict(description='findtest2', -+ usercategory='all', hostcategory='all') -+ ), -+ expected=dict( -+ value=fnd_ng2, -+ summary='Added netgroup "%s"' % fnd_ng2, -+ result=dict( -+ dn=fuzzy_netgroupdn, -+ cn=[fnd_ng2], -+ objectclass=objectclasses.netgroup, -+ description=['findtest2'], -+ nisdomainname=['%s' % api.env.domain], -+ usercategory=['all'], -+ hostcategory=['all'], -+ ipauniqueid=[fuzzy_uuid], -+ ), -+ ), -+ ), -+ -+ dict( -+ desc='Create netgroup %r for find tests' % fnd_ng3, -+ command=( -+ 'netgroup_add', [fnd_ng3], -+ dict(description='findtest3') -+ ), -+ expected=dict( -+ value=fnd_ng3, -+ summary='Added netgroup "%s"' % fnd_ng3, -+ result=dict( -+ dn=fuzzy_netgroupdn, -+ cn=[fnd_ng3], -+ objectclass=objectclasses.netgroup, -+ description=['findtest3'], -+ nisdomainname=['%s' % api.env.domain], -+ ipauniqueid=[fuzzy_uuid], -+ ), -+ ), -+ ), -+ -+ # Add members to fnd_ng1 -+ dict( -+ desc='Add user %r to netgroup %r' % (fnd_user1, fnd_ng1), -+ command=( -+ 'netgroup_add_member', [fnd_ng1], -+ dict(user=fnd_user1) -+ ), -+ expected=dict( -+ completed=1, -+ failed=dict( -+ member=dict(netgroup=tuple()), -+ memberuser=dict(group=tuple(), user=tuple()), -+ memberhost=dict(hostgroup=tuple(), host=tuple()), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'memberuser_user': (fnd_user1,), -+ 'cn': [fnd_ng1], -+ 'description': ['findtest'], -+ 'nisdomainname': [fnd_nisdomain], -+ }, -+ ), -+ ), -+ -+ dict( -+ desc='Add group %r to netgroup %r' % (fnd_group, fnd_ng1), -+ command=( -+ 'netgroup_add_member', [fnd_ng1], -+ dict(group=fnd_group) -+ ), -+ expected=dict( -+ completed=1, -+ failed=dict( -+ member=dict(netgroup=tuple()), -+ memberuser=dict(group=tuple(), user=tuple()), -+ memberhost=dict(hostgroup=tuple(), host=tuple()), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'memberuser_user': (fnd_user1,), -+ 'memberuser_group': (fnd_group,), -+ 'cn': [fnd_ng1], -+ 'description': ['findtest'], -+ 'nisdomainname': [fnd_nisdomain], -+ }, -+ ), -+ ), -+ -+ dict( -+ desc='Add host %r to netgroup %r' % (fnd_host, fnd_ng1), -+ command=( -+ 'netgroup_add_member', [fnd_ng1], -+ dict(host=fnd_host) -+ ), -+ expected=dict( -+ completed=1, -+ failed=dict( -+ member=dict(netgroup=tuple()), -+ memberuser=dict(group=tuple(), user=tuple()), -+ memberhost=dict(hostgroup=tuple(), host=tuple()), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'memberuser_user': (fnd_user1,), -+ 'memberuser_group': (fnd_group,), -+ 'memberhost_host': (fnd_host,), -+ 'cn': [fnd_ng1], -+ 'description': ['findtest'], -+ 'nisdomainname': [fnd_nisdomain], -+ }, -+ ), -+ ), -+ -+ dict( -+ desc='Add hostgroup %r to netgroup %r' % (fnd_hostgroup, fnd_ng1), -+ command=( -+ 'netgroup_add_member', [fnd_ng1], -+ dict(hostgroup=fnd_hostgroup) -+ ), -+ expected=dict( -+ completed=1, -+ failed=dict( -+ member=dict(netgroup=tuple()), -+ memberuser=dict(group=tuple(), user=tuple()), -+ memberhost=dict(hostgroup=tuple(), host=tuple()), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'memberuser_user': (fnd_user1,), -+ 'memberuser_group': (fnd_group,), -+ 'memberhost_host': (fnd_host,), -+ 'memberhost_hostgroup': (fnd_hostgroup,), -+ 'cn': [fnd_ng1], -+ 'description': ['findtest'], -+ 'nisdomainname': [fnd_nisdomain], -+ }, -+ ), -+ ), -+ -+ # Add fnd_ng1 as member of fnd_ng3 (nested netgroup) -+ dict( -+ desc='Add netgroup %r to netgroup %r' % (fnd_ng1, fnd_ng3), -+ command=( -+ 'netgroup_add_member', [fnd_ng3], -+ dict(netgroup=fnd_ng1) -+ ), -+ expected=dict( -+ completed=1, -+ failed=dict( -+ member=dict(netgroup=tuple()), -+ memberuser=dict(group=tuple(), user=tuple()), -+ memberhost=dict(hostgroup=tuple(), host=tuple()), -+ ), -+ result={ -+ 'dn': fuzzy_netgroupdn, -+ 'member_netgroup': (fnd_ng1,), -+ 'cn': [fnd_ng3], -+ 'description': ['findtest3'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ }, -+ ), -+ ), -+ -+ # Find tests - positive scenarios -+ dict( -+ desc='Find netgroup by exact name %r' % fnd_ng1, -+ command=('netgroup_find', [], dict(cn=fnd_ng1)), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 netgroup matched', -+ result=[ -+ { -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [fnd_ng1], -+ 'description': ['findtest'], -+ 'nisdomainname': [fnd_nisdomain], -+ }, -+ ], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup by description findtest', -+ command=('netgroup_find', [], dict(description='findtest')), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 netgroup matched', -+ result=[ -+ { -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [fnd_ng1], -+ 'description': ['findtest'], -+ 'nisdomainname': [fnd_nisdomain], -+ }, -+ ], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup by nisdomain %r' % fnd_nisdomain, -+ command=('netgroup_find', [], dict(nisdomainname=fnd_nisdomain)), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 netgroup matched', -+ result=[ -+ { -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [fnd_ng1], -+ 'description': ['findtest'], -+ 'nisdomainname': [fnd_nisdomain], -+ }, -+ ], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup by usercat=all', -+ command=('netgroup_find', [], dict(usercategory='all')), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 netgroup matched', -+ result=[ -+ { -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [fnd_ng2], -+ 'description': ['findtest2'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'usercategory': ['all'], -+ 'hostcategory': ['all'], -+ }, -+ ], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup by hostcat=all', -+ command=('netgroup_find', [], dict(hostcategory='all')), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 netgroup matched', -+ result=[ -+ { -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [fnd_ng2], -+ 'description': ['findtest2'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ 'usercategory': ['all'], -+ 'hostcategory': ['all'], -+ }, -+ ], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup by user %r' % fnd_user1, -+ command=('netgroup_find', [], dict(user=fnd_user1)), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 netgroup matched', -+ result=[ -+ { -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [fnd_ng1], -+ 'description': ['findtest'], -+ 'nisdomainname': [fnd_nisdomain], -+ }, -+ ], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup by group %r' % fnd_group, -+ command=('netgroup_find', [], dict(group=fnd_group)), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 netgroup matched', -+ result=[ -+ { -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [fnd_ng1], -+ 'description': ['findtest'], -+ 'nisdomainname': [fnd_nisdomain], -+ }, -+ ], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup by host %r' % fnd_host, -+ command=('netgroup_find', [], dict(host=fnd_host)), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 netgroup matched', -+ result=[ -+ { -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [fnd_ng1], -+ 'description': ['findtest'], -+ 'nisdomainname': [fnd_nisdomain], -+ }, -+ ], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup by hostgroup %r' % fnd_hostgroup, -+ command=('netgroup_find', [], dict(hostgroup=fnd_hostgroup)), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 netgroup matched', -+ result=[ -+ { -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [fnd_ng1], -+ 'description': ['findtest'], -+ 'nisdomainname': [fnd_nisdomain], -+ }, -+ ], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup by member netgroup %r' % fnd_ng1, -+ command=('netgroup_find', [], dict(netgroup=fnd_ng1)), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 netgroup matched', -+ result=[ -+ { -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [fnd_ng3], -+ 'description': ['findtest3'], -+ 'nisdomainname': ['%s' % api.env.domain], -+ }, -+ ], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup by in_netgroup %r' % fnd_ng3, -+ command=('netgroup_find', [], dict(in_netgroup=fnd_ng3)), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 netgroup matched', -+ result=[ -+ { -+ 'dn': fuzzy_netgroupdn, -+ 'cn': [fnd_ng1], -+ 'description': ['findtest'], -+ 'nisdomainname': [fnd_nisdomain], -+ }, -+ ], -+ ), -+ ), -+ -+ # Find tests - negative scenarios (0 matches) -+ dict( -+ desc='Find netgroup with nonexistent name returns 0', -+ command=('netgroup_find', ['nonexistent_fnd'], {}), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup with bad user returns 0', -+ command=('netgroup_find', [], dict(user='baduser_fnd')), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup with bad group returns 0', -+ command=('netgroup_find', [], dict(group='badgroup_fnd')), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup with bad host returns 0', -+ command=('netgroup_find', [], dict(host='badhost_fnd')), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup with bad hostgroup returns 0', -+ command=('netgroup_find', [], dict(hostgroup='badhg_fnd')), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup with bad netgroup returns 0', -+ command=('netgroup_find', [], dict(netgroup='badng_fnd')), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ # Invalid parameter type tests -+ dict( -+ desc='Find netgroup with invalid timelimit type', -+ command=('netgroup_find', [], dict(timelimit='bad')), -+ expected=errors.ConversionError( -+ name='timelimit', -+ error='must be an integer'), -+ ), -+ -+ dict( -+ desc='Find netgroup with invalid sizelimit type', -+ command=('netgroup_find', [], dict(sizelimit='bad')), -+ expected=errors.ConversionError( -+ name='sizelimit', -+ error='must be an integer'), -+ ), -+ -+ # Space input tests (bz798792) -+ dict( -+ desc='Find netgroup with space in netgroup param (bz798792)', -+ command=('netgroup_find', [], dict(netgroup=' ')), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup with space in user param (bz798792)', -+ command=('netgroup_find', [], dict(user=' ')), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup with space in group param (bz798792)', -+ command=('netgroup_find', [], dict(group=' ')), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup with space in host param (bz798792)', -+ command=('netgroup_find', [], dict(host=' ')), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup with space in hostgroup param (bz798792)', -+ command=('netgroup_find', [], dict(hostgroup=' ')), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), -+ -+ dict( -+ desc='Find netgroup with space in in_netgroup param (bz798792)', -+ command=('netgroup_find', [], dict(in_netgroup=' ')), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 netgroups matched', -+ result=[], -+ ), -+ ), - ] - - # No way to convert this test just yet. --- -2.52.0 - diff --git a/0036-ipatests-Add-user-principal-ipa-getkeytab-and-ipa-rm.patch b/0036-ipatests-Add-user-principal-ipa-getkeytab-and-ipa-rm.patch deleted file mode 100644 index afe6dec..0000000 --- a/0036-ipatests-Add-user-principal-ipa-getkeytab-and-ipa-rm.patch +++ /dev/null @@ -1,543 +0,0 @@ -From 9a8553fc297987b006b28ade2098d613f9b5360f Mon Sep 17 00:00:00 2001 -From: Jay Gondaliya -Date: Thu, 12 Mar 2026 19:03:24 +0530 -Subject: [PATCH] ipatests: Add user-principal ipa-getkeytab and - ipa-rmkeytab cmdline tests -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Port bash acceptance tests (getkeytab_001–getkeytab_007, rmkeytab_001-rmkeytab_003) from downstream to cmdline tests in test_ipagetkeytab.py: - - test_getkeytab_users: - getkeytab_001: quiet mode, insufficient access, empty ccache - getkeytab_002: --server / -s with valid and invalid hostnames, DNS-based server discovery when -s is omitted - getkeytab_003: --principal / -p with unknown, invalid-realm, - bare, and realm-qualified principals - getkeytab_004: --keytab / -k creation, enctype content, invalid path - getkeytab_005: -e single enctype filtering and kinit validation - getkeytab_006: --password / -P key rotation - getkeytab_007: -D / -w bind DN error cases (empty DN, wrong - password, missing password) - - test_rmkeytab_cmd: - rmkeytab_001: removal by principal (valid and invalid) - rmkeytab_002: removal by realm - rmkeytab_003: invalid keytab path - -Tests that duplicate existing coverage are skipped with comments referencing the original tests. Parametrized variants are used where flags accept both short and long forms. - -Signed-off-by: Jay Gondaliya -Fixes: https://pagure.io/freeipa/issue/9957 -Assisted-by: Claude -Reviewed-By: Rob Crittenden -Reviewed-By: Florence Blanc-Renaud ---- - ipatests/test_cmdline/test_ipagetkeytab.py | 459 +++++++++++++++++++++ - 1 file changed, 459 insertions(+) - -diff --git a/ipatests/test_cmdline/test_ipagetkeytab.py b/ipatests/test_cmdline/test_ipagetkeytab.py -index c4678a76d..9ec9efae6 100755 ---- a/ipatests/test_cmdline/test_ipagetkeytab.py -+++ b/ipatests/test_cmdline/test_ipagetkeytab.py -@@ -22,6 +22,7 @@ Test `ipa-getkeytab` - - from __future__ import absolute_import - -+import configparser - import os - import shutil - import tempfile -@@ -40,6 +41,28 @@ from ipatests.test_xmlrpc.tracker import host_plugin, service_plugin - from ipatests.test_xmlrpc.xmlrpc_test import fuzzy_digits, add_oc - from contextlib import contextmanager - -+from ipatests.test_xmlrpc.tracker.user_plugin import UserTracker -+from ipatests.util import unlock_principal_password -+ -+GK_USER1 = 'gkuser1' -+GK_USER2 = 'gkuser2' -+GK_INIT_PW = 'TempSecret123!' -+GK_USER_PW = 'Secret123' -+ -+CRYPTO_POLICIES_KRB5 = '/etc/krb5.conf.d/crypto-policies' -+ -+ -+def get_permitted_enctypes(): -+ """Read permitted encryption types from the system crypto-policies. -+ -+ Returns a list of enctype name strings, e.g. -+ ['aes256-cts-hmac-sha1-96', 'aes128-cts-hmac-sha1-96', ...]. -+ """ -+ cfg = configparser.ConfigParser() -+ cfg.read(CRYPTO_POLICIES_KRB5) -+ raw = cfg.get('libdefaults', 'permitted_enctypes', fallback='') -+ return [e for e in raw.split() if e] -+ - - @contextmanager - def use_keytab(principal, keytab): -@@ -63,6 +86,57 @@ def use_keytab(principal, keytab): - setattr(context, 'principal', old_principal) - - -+@contextmanager -+def kinit_as_user(principal, password): -+ """Kinit as *principal* into a private ccache; yield its path. -+ -+ private_ccache() already sets KRB5CCNAME in os.environ, so callers -+ (and ipautil.run) inherit it automatically. -+ """ -+ with private_ccache() as ccache_path: -+ ipautil.run( -+ ['kinit', principal], -+ stdin=password + '\n', -+ raiseonerr=True, -+ capture_output=True, -+ capture_error=True, -+ ) -+ yield ccache_path -+ -+ -+def run_getkeytab(args, env=None, stdin=None): -+ """Run ipa-getkeytab with arbitrary arguments.""" -+ return ipautil.run( -+ [paths.IPA_GETKEYTAB] + list(args), -+ raiseonerr=False, -+ capture_output=True, -+ capture_error=True, -+ stdin=stdin, -+ env=env, -+ ) -+ -+ -+def run_rmkeytab(args, env=None): -+ """Run ipa-rmkeytab with arbitrary arguments.""" -+ return ipautil.run( -+ [paths.IPA_RMKEYTAB] + list(args), -+ raiseonerr=False, -+ capture_output=True, -+ capture_error=True, -+ env=env, -+ ) -+ -+ -+def klist_keytab(keytab_path): -+ """Run ``klist -ekt`` and return the result object.""" -+ return ipautil.run( -+ ['klist', '-ekt', keytab_path], -+ raiseonerr=False, -+ capture_output=True, -+ capture_error=True, -+ ) -+ -+ - @pytest.fixture(scope='class') - def test_host(request): - host_tracker = host_plugin.HostTracker(u'test-host') -@@ -76,6 +150,20 @@ def test_service(request, test_host, keytab_retrieval_setup): - return service_tracker.make_fixture(request) - - -+@pytest.fixture(scope='class') -+def gk_users(request, keytab_retrieval_setup): -+ """Create GK_USER1 and GK_USER2; delete them after the class.""" -+ for uid in (GK_USER1, GK_USER2): -+ tracker = UserTracker( -+ name=uid, givenname='Test', sn='GKUser', -+ userpassword=GK_INIT_PW, -+ ) -+ tracker.make_fixture(request) -+ tracker.make_create_command()() -+ tracker.exists = True -+ unlock_principal_password(uid, GK_INIT_PW, GK_USER_PW) -+ -+ - @pytest.mark.needs_ipaapi - class KeytabRetrievalTest(cmdline_test): - """ -@@ -134,6 +222,22 @@ class KeytabRetrievalTest(cmdline_test): - rc = result.returncode - assert rc == retcode - -+ def _get_user1_keytab(self, keytab_path=None): -+ """Populate *keytab_path* (default self.keytabname) with GK_USER1's -+ keytab.""" -+ keytab = keytab_path or self.keytabname -+ run_getkeytab( -+ ['-s', api.env.host, '-p', GK_USER1, '-k', keytab] -+ ) -+ -+ def _assert_getkeytab_succeeds(self, result): -+ """Assert ipa-getkeytab returned success with the expected message.""" -+ assert result.returncode == 0 -+ assert ( -+ 'Keytab successfully retrieved and stored in:' -+ in result.error_output -+ ) -+ - - @pytest.mark.tier0 - class test_ipagetkeytab(KeytabRetrievalTest): -@@ -495,3 +599,358 @@ class test_smb_service(KeytabRetrievalTest): - ipanthash = entry.single_value.get('ipanthash') - conn.disconnect() - assert ipanthash != b'MagicRegen', 'LDBM backend entry corruption' -+ -+ -+# ----------------------------------------------------------------------- -+# User-principal tests ported from bash acceptance tests -+# (t.ipa-get-rm-keytabs.sh: getkeytab_001 – getkeytab_006) -+# ----------------------------------------------------------------------- -+ -+ -+@pytest.mark.tier1 -+@pytest.mark.usefixtures('gk_users') -+class test_getkeytab_users(KeytabRetrievalTest): -+ """ -+ User-principal ipa-getkeytab tests (bash getkeytab_001 – getkeytab_006). -+ """ -+ command = paths.IPA_GETKEYTAB -+ -+ def _ensure_keytab_absent(self, keytab_path=None): -+ try: -+ os.unlink(keytab_path or self.keytabname) -+ except OSError: -+ pass -+ -+ # --- getkeytab_001: quiet mode, access rights, missing ccache --- -+ -+ def test_insufficient_access_as_other_user(self): -+ """Retrieving another user's keytab without admin rights fails.""" -+ principal1 = '{}@{}'.format(GK_USER1, api.env.realm) -+ principal2 = '{}@{}'.format(GK_USER2, api.env.realm) -+ with kinit_as_user(principal1, GK_USER_PW): -+ result = run_getkeytab( -+ ['-s', api.env.host, -+ '-p', principal2, -+ '-k', self.keytabname], -+ ) -+ assert result.returncode == 9 -+ assert ( -+ 'Failed to parse result: Insufficient access rights' -+ in result.error_output -+ ) -+ -+ def test_empty_ccache_returns_exit6(self): -+ """An empty ccache (simulating kdestroy) returns exit 6.""" -+ principal1 = '{}@{}'.format(GK_USER1, api.env.realm) -+ with private_ccache(): -+ result = run_getkeytab( -+ ['-s', api.env.host, '-p', principal1, -+ '-k', self.keytabname], -+ ) -+ assert result.returncode == 6 -+ assert ( -+ 'Kerberos User Principal not found. ' -+ 'Do you have a valid Credential Cache?' -+ in result.error_output -+ ) -+ -+ def test_quiet_suppresses_success_message(self): -+ """-q must suppress the keytab-stored success message. -+ -+ Note: test_6_quiet_mode checks -q returncode with a service -+ principal but does not verify the message is absent. -+ """ -+ result = run_getkeytab( -+ ['-q', '-s', api.env.host, '-p', GK_USER1, -+ '-k', self.keytabname], -+ ) -+ assert result.returncode == 0 -+ assert result.error_output == '', ( -+ f"Expected no output with -q, got: {result.error_output!r}" -+ ) -+ -+ # Skipped: test_normal_mode_shows_success_message -+ # — covered by test_6_quiet_mode which verifies the success message -+ # appears without -q. -+ -+ # --- getkeytab_002: --server / -s --- -+ -+ @pytest.mark.parametrize('flag', ['--server', '-s']) -+ def test_invalid_server_fails(self, flag): -+ """An invalid server hostname returns exit 9 with bind error.""" -+ result = run_getkeytab( -+ [flag, 'invalid.ipaserver.com', -+ '-p', GK_USER1, -+ '-k', self.keytabname], -+ ) -+ assert result.returncode == 9 -+ assert 'Failed to bind to server' in result.error_output -+ -+ @pytest.mark.parametrize('flag', ['--server', '-s']) -+ def test_valid_server_succeeds(self, flag): -+ """A valid server hostname retrieves the keytab successfully. -+ -+ Note: -s case is also covered by test_7_server_name_check -+ with a service principal. -+ """ -+ result = run_getkeytab( -+ [flag, api.env.host, '-p', GK_USER1, -+ '-k', self.keytabname], -+ ) -+ self._assert_getkeytab_succeeds(result) -+ -+ def test_dns_discovery_succeeds(self): -+ """ipa-getkeytab discovers the server via DNS when -s is omitted.""" -+ result = run_getkeytab( -+ ['-p', GK_USER1, '-k', self.keytabname], -+ ) -+ self._assert_getkeytab_succeeds(result) -+ -+ # --- getkeytab_003: --principal / -p --- -+ -+ @pytest.mark.parametrize('flag', ['--principal', '-p']) -+ @pytest.mark.parametrize('principal', [ -+ pytest.param('unknownuser', id='unknown'), -+ pytest.param( -+ '{}@INVALID.IPASERVER.REALM.COM'.format(GK_USER1), -+ id='invalid-realm', -+ ), -+ ]) -+ def test_principal_not_found(self, flag, principal): -+ """Unresolvable principals return exit 9 with not-found error. -+ -+ Note: test_1_run tests a similar scenario with a non-existent -+ service principal. -+ """ -+ result = run_getkeytab( -+ ['-s', api.env.host, flag, principal, -+ '-k', self.keytabname], -+ ) -+ assert result.returncode == 9 -+ assert ( -+ 'Failed to parse result: PrincipalName not found.' -+ in result.error_output -+ ) -+ -+ @pytest.mark.parametrize('flag', ['--principal', '-p']) -+ @pytest.mark.parametrize('with_realm', [False, True], -+ ids=['bare', 'with-realm']) -+ def test_valid_principal_succeeds(self, flag, with_realm): -+ """Both bare and realm-qualified principals retrieve the keytab.""" -+ principal = ('{}@{}'.format(GK_USER1, api.env.realm) -+ if with_realm else GK_USER1) -+ result = run_getkeytab( -+ ['-s', api.env.host, flag, principal, -+ '-k', self.keytabname], -+ ) -+ self._assert_getkeytab_succeeds(result) -+ -+ # --- getkeytab_004: --keytab / -k --- -+ -+ @pytest.mark.parametrize('flag', ['--keytab', '-k']) -+ def test_creates_keytab_when_absent(self, flag): -+ """The keytab file is created when it does not previously exist.""" -+ self._ensure_keytab_absent() -+ result = run_getkeytab( -+ ['-s', api.env.host, '-p', GK_USER1, flag, self.keytabname], -+ ) -+ self._assert_getkeytab_succeeds(result) -+ assert os.path.isfile(self.keytabname) -+ -+ @pytest.mark.parametrize('flag', ['--keytab', '-k']) -+ def test_keytab_contains_aes_enctypes(self, flag): -+ """The created keytab contains both aes256 and aes128 entries.""" -+ run_getkeytab( -+ ['-s', api.env.host, '-p', GK_USER1, flag, self.keytabname] -+ ) -+ klist_result = klist_keytab(self.keytabname) -+ assert api.env.realm in klist_result.output -+ assert 'aes128' in klist_result.output -+ assert 'aes256' in klist_result.output -+ -+ @pytest.mark.parametrize('flag', ['--keytab', '-k']) -+ def test_text_file_path_returns_exit11(self, flag): -+ """Writing to a pre-existing plain-text file returns exit 11.""" -+ txtfd, txt_path = tempfile.mkstemp(suffix='.txt') -+ os.close(txtfd) -+ try: -+ result = run_getkeytab( -+ ['-s', api.env.host, '-p', GK_USER1, flag, txt_path], -+ ) -+ assert result.returncode == 11 -+ assert 'Failed to add key to the keytab' in result.error_output -+ finally: -+ try: -+ os.unlink(txt_path) -+ except OSError: -+ pass -+ -+ # --- getkeytab_005: -e encryption types --- -+ -+ def test_system_keytab_has_default_enctypes(self): -+ """The system keytab must contain both aes256 and aes128.""" -+ klist_result = klist_keytab('/etc/krb5.keytab') -+ assert '(aes256-cts-hmac-sha1-96)' in klist_result.output -+ assert '(aes128-cts-hmac-sha1-96)' in klist_result.output -+ -+ @pytest.mark.parametrize('enctype', get_permitted_enctypes()) -+ def test_single_enctype(self, enctype): -+ """With -e only that enctype appears and kinit works. -+ -+ Note: test_8_keytab_encryption_check tests -e with multiple -+ enctypes but only asserts success, not klist content. -+ """ -+ expected = f"({enctype})" -+ self._ensure_keytab_absent() -+ run_getkeytab( -+ ['-s', api.env.host, '-p', GK_USER1, -+ '-k', self.keytabname, '-e', enctype], -+ ) -+ klist_result = klist_keytab(self.keytabname) -+ assert expected in klist_result.output -+ for enc in get_permitted_enctypes(): -+ if enc != enctype: -+ assert f"({enc})" not in klist_result.output -+ assert '(des3-cbc-sha1)' not in klist_result.output -+ assert '(arcfour-hmac)' not in klist_result.output -+ with private_ccache(): -+ ipautil.run( -+ ['kinit', '-k', '-t', self.keytabname, -+ '{}@{}'.format(GK_USER1, api.env.realm)], -+ raiseonerr=True, -+ capture_output=True, -+ capture_error=True, -+ ) -+ -+ def test_invalid_enctype_returns_exit8(self): -+ """An invalid -e value returns exit 8 and creates no keytab.""" -+ self._ensure_keytab_absent() -+ result = run_getkeytab( -+ ['-s', api.env.host, '-p', GK_USER1, -+ '-k', self.keytabname, '-e', 'invalid'], -+ ) -+ assert result.returncode == 8 -+ assert 'Warning unrecognized encryption type' in result.error_output -+ assert not os.path.isfile(self.keytabname) -+ -+ # --- getkeytab_006: --password / -P --- -+ -+ @pytest.mark.parametrize('flag', ['--password', '-P']) -+ def test_password_flag_rotates_keys(self, flag): -+ """After --password and a random-key reset, the original password -+ will no longer authenticate. -+ -+ Sequence: admin sets password-derived keys -> user kinits -> user -+ regenerates random keys -> kinit with the original password must fail. -+ """ -+ self._ensure_keytab_absent() -+ principal1 = '{}@{}'.format(GK_USER1, api.env.realm) -+ stdin = '{pw}\n{pw}\n'.format(pw=GK_USER_PW) -+ result = run_getkeytab( -+ ['-s', api.env.host, '-p', GK_USER1, -+ '-k', self.keytabname, flag], -+ stdin=stdin, -+ ) -+ assert result.returncode == 0 -+ with kinit_as_user(principal1, GK_USER_PW): -+ regen = run_getkeytab( -+ ['-s', api.env.host, '-p', GK_USER1, -+ '-k', self.keytabname], -+ ) -+ assert regen.returncode == 0 -+ with private_ccache(): -+ kinit_result = ipautil.run( -+ ['kinit', principal1], -+ stdin=GK_USER_PW + '\n', -+ raiseonerr=False, -+ capture_output=True, -+ capture_error=True, -+ ) -+ assert kinit_result.returncode != 0 -+ assert ( -+ 'Password incorrect' in kinit_result.error_output -+ or 'Preauthentication failed' in kinit_result.error_output -+ ) -+ -+ # --- getkeytab_007: -D / -w bind DN error cases --- -+ -+ # Skipped: test_no_ccache_returns_exit6 -+ # — identical to test_empty_ccache_returns_exit6 above. -+ # Skipped: test_valid_dm_credentials_succeed -+ # — covered by TestBindMethods.test_retrieval_with_dm_creds. -+ -+ @pytest.mark.parametrize('extra_args,exitcode,message', [ -+ (['-D', ' ', '-w', GK_USER_PW], -+ 9, 'Anonymous Binds are not allowed'), -+ (['-D', 'cn=Directory Manager', '-w', ' '], -+ 9, 'Simple bind failed'), -+ (['-D', 'cn=Directory Manager'], -+ 10, 'Bind password required when using a bind DN'), -+ ], ids=['empty-dn', 'wrong-password', 'missing-password']) -+ def test_binddn_error_cases(self, extra_args, exitcode, message): -+ """Bind DN error cases return appropriate exit codes.""" -+ result = run_getkeytab( -+ ['--server', 'localhost', -+ '-p', GK_USER1, -+ '-k', self.keytabname] + extra_args, -+ ) -+ assert result.returncode == exitcode -+ assert message in result.error_output -+ -+ -+# ----------------------------------------------------------------------- -+# ipa-rmkeytab tests (bash rmkeytab_001 – rmkeytab_003) -+# ----------------------------------------------------------------------- -+ -+ -+@pytest.mark.tier1 -+@pytest.mark.usefixtures('gk_users') -+class test_rmkeytab_cmd(KeytabRetrievalTest): -+ """ -+ ipa-rmkeytab tests (bash rmkeytab_001 – rmkeytab_003). -+ """ -+ command = paths.IPA_RMKEYTAB -+ -+ # --- rmkeytab_001: -p removes a named principal --- -+ -+ def test_invalid_principal_returns_exit5(self): -+ """A non-existent principal name returns exit 5.""" -+ self._get_user1_keytab() -+ result = run_rmkeytab(['-p', 'invalidprinc', '-k', self.keytabname]) -+ assert result.returncode == 5 -+ assert 'principal not found' in result.error_output -+ -+ def test_valid_principal_removed(self): -+ """A present principal is removed and absent from klist.""" -+ self._get_user1_keytab() -+ result = run_rmkeytab(['-p', GK_USER1, '-k', self.keytabname]) -+ assert result.returncode == 0 -+ assert ( -+ 'Removing principal {}'.format(GK_USER1) -+ in result.error_output -+ ) -+ assert GK_USER1 not in klist_keytab(self.keytabname).output -+ -+ # --- rmkeytab_002: -r removes all principals of the given realm --- -+ -+ def test_realm_removes_all_principals(self): -+ """All principals of the realm are removed and absent from klist.""" -+ self._get_user1_keytab() -+ result = run_rmkeytab(['-r', api.env.realm, '-k', self.keytabname]) -+ assert result.returncode == 0 -+ assert ( -+ 'Removing principal {}@{}'.format(GK_USER1, api.env.realm) -+ in result.error_output -+ ) -+ assert api.env.realm not in klist_keytab(self.keytabname).output -+ -+ # --- rmkeytab_003: -k with a non-existent path --- -+ -+ def test_invalid_keytab_path_returns_exit7(self): -+ """A non-existent keytab path returns exit 7.""" -+ self._get_user1_keytab() -+ result = run_rmkeytab( -+ ['-p', GK_USER1, '-k', '/opt/invalid.keytab'] -+ ) -+ assert result.returncode == 7 -+ assert 'Failed to set cursor' in result.error_output --- -2.52.0 - diff --git a/0037-ipatests-Additional-tests-for-ipa-ipa-migration-test.patch b/0037-ipatests-Additional-tests-for-ipa-ipa-migration-test.patch deleted file mode 100644 index 01ae7f3..0000000 --- a/0037-ipatests-Additional-tests-for-ipa-ipa-migration-test.patch +++ /dev/null @@ -1,285 +0,0 @@ -From b6d50197882dd62b5416948cb232c77c0349c0d3 Mon Sep 17 00:00:00 2001 -From: Sudhir Menon -Date: Thu, 5 Mar 2026 19:29:26 +0530 -Subject: [PATCH] ipatests: Additional tests for ipa-ipa migration - testsuite - -Covers tests scenarios for sshpubkey. - -1. Test to check sshpubkey is migrated for regular ipa user -2. Test to check sshpubkey is migrated for staged user -3. Test to check sshpubkey is migrated for preserved user -4. Test to check idoverides is migrated. - -Covers test scenarios for TestIPAMigrationDNSRecords - -1. Test to check dns zone is migrated -2. Test to check dynamic update value is preserved when migrated -3. Test to check dnsforwardzone is migrated -4. Test to check A record is migrated - -Covers test scenarios for IPA Privieleges and Permissions - -1. Test to check that IPA role and its privilege are migrated in Prod mode -2. Test to check that PBAC Permissions are migrated in Prod mode -3. Test to check that sysaccounts are migrated. -4. Test to check selinuxusermap is migrated - -Assisted-by: Claude -Signed-off-by: Sudhir Menon -Fixes: https://pagure.io/freeipa/issue/9964 -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: David Hanina -Reviewed-By: Sudhir Menon ---- - .../test_ipa_ipa_migration.py | 164 +++++++++++++++++- - 1 file changed, 155 insertions(+), 9 deletions(-) - -diff --git a/ipatests/test_integration/test_ipa_ipa_migration.py b/ipatests/test_integration/test_ipa_ipa_migration.py -index 18f335de196a9d6c7d30e4adf35385122376b7d9..105c79b610804b68522e568b4028be5a2dc72714 100644 ---- a/ipatests/test_integration/test_ipa_ipa_migration.py -+++ b/ipatests/test_integration/test_ipa_ipa_migration.py -@@ -201,6 +201,12 @@ def prepare_ipa_server(master): - master.run_command( - ["ipa", "dnszone-mod", "example.test", "--dynamic-update=TRUE"] - ) -+ master.run_command( -+ [ -+ "ipa", "dnsrecord-add", "example.test", "migratetest", -+ "--a-rec", "192.0.2.100", -+ ] -+ ) - - # Add hbac rule - master.run_command(["ipa", "hbacrule-add", "--usercat=all", "test1"]) -@@ -221,7 +227,7 @@ def prepare_ipa_server(master): - "dnsforwardzone-add", - "forwardzone.test", - "--forwarder", -- "10.11.12.13", -+ "192.168.124.10", - ] - ) - -@@ -293,6 +299,11 @@ def prepare_ipa_server(master): - ] - ) - -+ # Add sysaccount -+ master.run_command( -+ ["ipa", "sysaccount-add", "migrate-test-sysaccount", "--random"] -+ ) -+ - - def run_migrate( - host, mode, remote_host, bind_dn=None, bind_pwd=None, extra_args=None -@@ -664,15 +675,15 @@ class TestIPAMigrateCLIOptions(MigrationTest): - realm_name = self.master.domain.realm - base_dn = str(self.master.domain.basedn) - dse_ldif = textwrap.dedent( -- f""" -+ """ - dn: cn={realm_name},cn=kerberos,{base_dn} - cn: {realm_name} - objectClass: top - objectClass: krbrealmcontainer - """ - ).format( -- realm_name=self.master.domain.realm, -- base_dn=str(self.master.domain.basedn), -+ realm_name=realm_name, -+ base_dn=base_dn, - ) - self.replicas[0].put_file_contents(ldif_file_path, dse_ldif) - result = run_migrate( -@@ -731,7 +742,6 @@ class TestIPAMigrateCLIOptions(MigrationTest): - base_dn = str(self.master.domain.basedn) - subtree = 'cn=security,{}'.format(base_dn) - params = ['-s', subtree, '-n', '-x'] -- base_dn = str(self.master.domain.basedn) - CUSTOM_SUBTREE_LOG = ( - "Add db entry 'cn=security,{} - custom'" - ).format(base_dn) -@@ -853,7 +863,7 @@ class TestIPAMigrateCLIOptions(MigrationTest): - def test_ipa_migrate_stage_mode_with_cert(self): - """ - This testcase checks that ipa-migrate command -- works without the 'ValuerError' -+ works without the 'ValueError' - when -Z option is used with valid cert - """ - cert_file = '/tmp/ipa.crt' -@@ -878,7 +888,7 @@ class TestIPAMigrateCLIOptions(MigrationTest): - error when invalid cert is specified with - -Z option - """ -- cert_file = '/tmp/invaid_cert.crt' -+ cert_file = '/tmp/invalid_cert.crt' - invalid_cert = ( - b'-----BEGIN CERTIFICATE-----\n' - b'MIIFazCCDQYJKoZIhvcNAQELBQAw\n' -@@ -1026,6 +1036,59 @@ class TestIPAMigrationProdMode(MigrationTest): - assert 'Rule name: readfiles\n' in cmd1.stdout_text - assert 'Sudo Command: /usr/bin/less\n' in cmd2.stdout_text - -+ def test_ipa_migrate_prod_mode_roles_privileges(self): -+ """ -+ Test that IPA roles are migrated from remote to local server -+ """ -+ role_name = "junioradmin" -+ privilege_name = "User Administrators" -+ tasks.kinit_admin(self.replicas[0]) -+ result = self.replicas[0].run_command( -+ ["ipa", "role-show", role_name] -+ ) -+ assert "Role name: {}".format(role_name) in result.stdout_text -+ assert "Privileges: {}".format(privilege_name) in result.stdout_text -+ -+ def test_ipa_migrate_prod_mode_permission(self): -+ """ -+ Test that PBAC permission is migrated -+ """ -+ permission_name = "Add Users" -+ tasks.kinit_admin(self.replicas[0]) -+ result = self.replicas[0].run_command( -+ ["ipa", "permission-show", permission_name] -+ ) -+ assert (f"Permission name: {permission_name}" in -+ result.stdout_text) -+ -+ def test_ipa_migrate_prod_mode_sysaccounts(self): -+ """ -+ Test that system accounts (sysaccounts) are migrated -+ from remote server to local server in prod mode. -+ """ -+ sysaccount_name = "migrate-test-sysaccount" -+ tasks.kinit_admin(self.replicas[0]) -+ result = self.replicas[0].run_command( -+ ["ipa", "sysaccount-show", sysaccount_name] -+ ) -+ assert sysaccount_name in result.stdout_text -+ assert (f"System account ID: {sysaccount_name}" in -+ result.stdout_text) -+ -+ def test_ipa_migrate_prod_mode_selinuxusermap(self): -+ """ -+ Test that SELinux usermap is migrated from remote -+ to local server. -+ """ -+ usermap_name = "test1" -+ tasks.kinit_admin(self.replicas[0]) -+ result = self.replicas[0].run_command( -+ ["ipa", "selinuxusermap-show", usermap_name] -+ ) -+ assert result.returncode == 0 -+ assert f"Rule name: {usermap_name}" in result.stdout_text -+ assert "xguest_u:s0" in result.stdout_text -+ - def test_ipa_migrate_prod_mode_new_user_sid(self): - """ - This testcase checks that in prod-mode uid/gid of the -@@ -1188,6 +1251,89 @@ class TestIPAMigrationProdMode(MigrationTest): - assert DEBUG_LOG in install_msg - - -+class TestIPAMigrationDNSRecords(MigrationTest): -+ """ -+ Tests to verify all DNS zones, forward zones, and DNS records -+ are migrated when ipa-migrate is run with --migrate-dns (-B). -+ "By default all DNS entries are migrated" / -B to migrate DNS. -+ """ -+ num_replicas = 1 -+ num_clients = 1 -+ topology = "line" -+ -+ @pytest.fixture(autouse=True) -+ def run_migration_with_dns(self): -+ """ -+ Run full prod-mode migration with -B so that DNS is migrated -+ to the local server. All tests in this class assume DNS has -+ been migrated. -+ """ -+ tasks.kinit_admin(self.master) -+ tasks.kinit_admin(self.replicas[0]) -+ run_migrate( -+ self.replicas[0], -+ "prod-mode", -+ self.master.hostname, -+ "cn=Directory Manager", -+ self.master.config.admin_password, -+ extra_args=["-B", "-n"], -+ ) -+ -+ def test_dns_zone_example_test_migrated(self): -+ """ -+ Check that DNS zone example.test (from prepare_ipa_server) -+ is migrated to the local server. -+ """ -+ zone_name = "example.test" -+ result = self.replicas[0].run_command( -+ ["ipa", "dnszone-show", zone_name] -+ ) -+ assert result.returncode == 0 -+ assert "Zone name: {}".format(zone_name) in result.stdout_text -+ -+ def test_dns_zone_dynamic_update_preserved(self): -+ """ -+ Check that zone attribute dynamic update is preserved -+ (prepare_ipa_server sets dynamic-update=TRUE for example.test). -+ """ -+ zone_name = "example.test" -+ result = self.replicas[0].run_command( -+ ["ipa", "dnszone-show", zone_name] -+ ) -+ assert result.returncode == 0 -+ assert "Dynamic update: True" in result.stdout_text -+ -+ def test_dns_zone_has_system_records(self): -+ """ -+ Check that migrated zone has system records (NS/SOA). -+ """ -+ zone_name = "example.test" -+ result = self.replicas[0].run_command( -+ ["ipa", "dnsrecord-find", zone_name] -+ ) -+ assert result.returncode == 0 -+ # Zone should have records (e.g. NS, SOA, or record list) -+ assert ( -+ "NS record" in result.stdout_text -+ or "SOA record" in result.stdout_text -+ or "Record name" in result.stdout_text -+ ) -+ -+ def test_dns_record_a_migrated(self): -+ """ -+ Verify that the A record added in prepare_ipa_server is -+ migrated to the local server. -+ """ -+ zone_name = "example.test" -+ record_name = "migratetest" -+ record_value = "192.0.2.100" -+ result = self.replicas[0].run_command( -+ ["ipa", "dnsrecord-show", zone_name, record_name] -+ ) -+ assert record_name in result.stdout_text -+ assert record_value in result.stdout_text -+ -+ - class TestIPAMigrationWithADtrust(IntegrationTest): - """ - Test for ipa-migrate tool with IPA Master having trust setup -@@ -1319,9 +1465,9 @@ class TestIPAMigratewithBackupRestore(IntegrationTest): - DB_LDIF_FILE = '{}-userRoot.ldif'.format( - dashed_domain_name - ) -- SCHEMA_LDIF_FILE = '{}''/config_files/schema/99user.ldif'.format( -+ SCHEMA_LDIF_FILE = "{}/config_files/schema/99user.ldif".format( - dashed_domain_name) -- CONFIG_LDIF_FILE = '{}''/config_files/dse.ldif'.format( -+ CONFIG_LDIF_FILE = "{}/config_files/dse.ldif".format( - dashed_domain_name) - param = [ - '-n', '-g', CONFIG_LDIF_FILE, '-m', SCHEMA_LDIF_FILE, --- -2.52.0 - diff --git a/0038-ipatests-ipa-migrate-ds-test-scenarios.patch b/0038-ipatests-ipa-migrate-ds-test-scenarios.patch deleted file mode 100644 index 8e5ed13..0000000 --- a/0038-ipatests-ipa-migrate-ds-test-scenarios.patch +++ /dev/null @@ -1,288 +0,0 @@ -From 8f2bbb02ae64f322fb34f073d7a7cdb00a69aea0 Mon Sep 17 00:00:00 2001 -From: Sudhir Menon -Date: Thu, 19 Mar 2026 17:12:12 +0530 -Subject: [PATCH] ipatests: ipa-migrate-ds test scenarios - -This patch tests ipa-migrated-ds related sceanrios - -1. 389-ds instance is setup on client system. -2. Attached sample instance1.ldif file in data/ds_migration folder. -3. Attempt ipa-migrate-ds with configuration disabled. -3. Attempt ipa-migrate-ds over ldaps. - -Assisted by: claude-4.6-opus-high -Signed-off-by: Sudhir Menon -Reviewed-By: Florence Blanc-Renaud ---- - ipatests/setup.py | 2 +- - .../data/ds_migration/instance1.ldif | 48 +++++ - .../test_integration/test_ds_migration.py | 189 ++++++++++++++++++ - 3 files changed, 238 insertions(+), 1 deletion(-) - create mode 100644 ipatests/test_integration/data/ds_migration/instance1.ldif - create mode 100644 ipatests/test_integration/test_ds_migration.py - -diff --git a/ipatests/setup.py b/ipatests/setup.py -index 0aec4a70d..07595a501 100644 ---- a/ipatests/setup.py -+++ b/ipatests/setup.py -@@ -56,7 +56,7 @@ if __name__ == '__main__': - 'ipatests': ['prci_definitions/*'], - 'ipatests.test_custodia': ['*.conf', 'empty.conf.d/*.conf'], - 'ipatests.test_install': ['*.update'], -- 'ipatests.test_integration': ['scripts/*'], -+ 'ipatests.test_integration': ['data/*/*.ldif'], - 'ipatests.test_ipaclient': ['data/*/*/*'], - 'ipatests.test_ipalib': ['data/*'], - 'ipatests.test_ipaplatform': ['data/*'], -diff --git a/ipatests/test_integration/data/ds_migration/instance1.ldif b/ipatests/test_integration/data/ds_migration/instance1.ldif -new file mode 100644 -index 000000000..a148b52c8 ---- /dev/null -+++ b/ipatests/test_integration/data/ds_migration/instance1.ldif -@@ -0,0 +1,48 @@ -+# Minimal DS migration test data: base OUs and sample user/group -+# Used when setting up 389-ds on the client for migrate-ds tests. -+# Base suffix dc=testrealm,dc=test is created by dscreate. -+ -+dn: dc=testrealm,dc=test -+objectClass: top -+objectClass: domain -+objectClass: dcObject -+dc: testrealm -+ -+dn: ou=People,dc=testrealm,dc=test -+objectClass: top -+objectClass: organizationalUnit -+ou: People -+ -+dn: ou=Groups,dc=testrealm,dc=test -+objectClass: top -+objectClass: organizationalUnit -+ou: Groups -+ -+dn: cn=Directory Administrators, ou=Groups, dc=testrealm,dc=test -+cn: Directory Administrators -+objectclass: top -+objectclass: groupofuniquenames -+ou: Groups -+uniquemember: uid=ldapuser_0001, ou=People, dc=testrealm,dc=test -+ -+dn: uid=ldapuser_0001,ou=People,dc=testrealm,dc=test -+objectClass: top -+objectClass: person -+objectClass: posixAccount -+uid: ldapuser_0001 -+cn: LDAP User 1 -+sn: User 1 -+uidNumber: 1001 -+gidNumber: 1001 -+homeDirectory: /home/ldapuser_0001 -+userPassword: fo0m4nchU -+telephonenumber: +1 123 444 555 -+ -+dn: cn=ldapgroup_0001,ou=Groups,dc=testrealm,dc=test -+objectClass: top -+objectClass: groupOfNames -+objectClass: posixGroup -+cn: ldapgroup_0001 -+gidNumber: 1001 -+member: uid=ldapuser_0001,ou=People,dc=testrealm,dc=test -+ -diff --git a/ipatests/test_integration/test_ds_migration.py b/ipatests/test_integration/test_ds_migration.py -new file mode 100644 -index 000000000..f16759b37 ---- /dev/null -+++ b/ipatests/test_integration/test_ds_migration.py -@@ -0,0 +1,189 @@ -+# -+# Copyright (C) 2026 FreeIPA Contributors see COPYING for license -+# -+ -+""" -+ipa-migrate-ds migration acceptance tests. -+""" -+ -+from __future__ import absolute_import -+ -+import os -+import textwrap -+ -+from ipatests.test_integration.base import IntegrationTest -+from ipatests.pytest_ipa.integration import tasks -+ -+# 389-ds instance name and base DN on the client -+DS_INSTANCE_NAME = "dsinstance_01" -+DS_BASEDN = "dc=testrealm,dc=test" -+DS_PORT = 389 -+DS_SECURE_PORT = 636 -+ -+ -+def _setup_389ds_on_client(client, admin_password): -+ """ -+ Install 389 Directory Server on the client and load migration -+ test data from instance1.ldif i.e (ou=People, ou=groups, -+ sample user/group). -+ """ -+ tasks.install_packages(client, ["389-ds-base"]) -+ -+ # Create instance via dscreate -+ inf_content = textwrap.dedent("""\ -+ [general] -+ full_machine_name = {hostname} -+ [slapd] -+ instance_name = {instance} -+ port = {port} -+ secure_port = {secure_port} -+ root_dn = cn=Directory Manager -+ root_password = {password} -+ [backend-userroot] -+ sample_entries = no -+ suffix = {basedn} -+ """).format( -+ hostname=client.hostname, -+ instance=DS_INSTANCE_NAME, -+ port=DS_PORT, -+ secure_port=DS_SECURE_PORT, -+ password=admin_password, -+ basedn=DS_BASEDN, -+ ) -+ client.put_file_contents("/tmp/ds-instance.inf", inf_content) -+ client.run_command( -+ ["dscreate", "from-file", "/tmp/ds-instance.inf"] -+ ) -+ -+ # Load migration test data from instance1.ldif -+ test_dir = os.path.dirname(os.path.abspath(__file__)) -+ ldif_path = os.path.join( -+ test_dir, "data", "ds_migration", "instance1.ldif" -+ ) -+ with open(ldif_path) as f: -+ ldif_content = f.read() -+ client.put_file_contents("/tmp/instance1.ldif", ldif_content) -+ client.run_command( -+ [ -+ "/usr/bin/ldapmodify", -+ "-a", "-x", "-H", "ldap://localhost:{}".format(DS_PORT), -+ "-D", "cn=Directory Manager", "-w", admin_password, -+ "-f", "/tmp/instance1.ldif", -+ ] -+ ) -+ -+ -+class TestDSMigrationConfig(IntegrationTest): -+ """ -+ Test ipa migrate-ds related scenarios. -+ -+ Uses a client host with 389-ds populated from instance1.ldif -+ (ou=People, ou=groups, dc=testrealm,dc=test) for migration tests. -+ """ -+ -+ topology = "line" -+ num_replicas = 0 -+ num_clients = 1 -+ -+ @classmethod -+ def install(cls, mh): -+ # Install master and IPA client (full topology) -+ super(TestDSMigrationConfig, cls).install(mh) -+ # On the client host, set up 389-ds with migration test data -+ _setup_389ds_on_client( -+ cls.clients[0], -+ cls.master.config.admin_password, -+ ) -+ -+ def test_attempt_migration_with_configuration_false(self): -+ """ -+ Test attempts ipa-migrate-ds with migration disabled. -+ """ -+ tasks.kinit_admin(self.master) -+ # Ensure migration is disabled -+ cmd = ["ipa", "config-mod", "--enable-migration", "FALSE"] -+ error_msg = "ipa: ERROR: no modifications to be performed" -+ result = self.master.run_command(cmd, raiseonerr=False) -+ assert result.returncode != 0 -+ tasks.assert_error(result, error_msg) -+ client_host = self.clients[0].hostname -+ ldap_uri = "ldap://{}:{}".format(client_host, DS_PORT) -+ result = self.master.run_command( -+ [ -+ "ipa", -+ "migrate-ds", -+ "--user-container=ou=People", -+ "--group-container=ou=groups", -+ ldap_uri, -+ ], -+ stdin_text=self.master.config.admin_password, -+ raiseonerr=False, -+ ) -+ assert ( -+ result.returncode != 0 -+ ), "migrate-ds should fail when migration is disabled" -+ assert "migration mode is disabled" in ( -+ result.stdout_text + result.stderr_text -+ ).lower() -+ -+ def test_migration_over_ldaps(self): -+ """ -+ Migrate from the client's 389-ds over LDAPS (port 636). -+ """ -+ ca_cert_file = "/etc/ipa/remoteds.crt" -+ tasks.kinit_admin(self.master) -+ self.master.run_command( -+ ["ipa", "config-mod", "--enable-migration", "TRUE"], -+ ) -+ -+ # Copy 389-ds CA cert from client to master for LDAPS verification -+ client = self.clients[0] -+ ds_cert_dir = "/etc/dirsrv/slapd-{}".format(DS_INSTANCE_NAME) -+ cert_result = client.run_command( -+ [ -+ "certutil", "-d", ds_cert_dir, "-L", "-n", -+ "Self-Signed-CA", "-a", -+ ], -+ ) -+ self.master.put_file_contents( -+ ca_cert_file, cert_result.stdout_text -+ ) -+ self.master.run_command( -+ ["restorecon", ca_cert_file], raiseonerr=False -+ ) -+ -+ client_host = client.hostname -+ ldaps_uri = "ldaps://{}:{}".format(client_host, DS_SECURE_PORT) -+ user_container = "ou=People,{}".format(DS_BASEDN) -+ group_container = "ou=groups,{}".format(DS_BASEDN) -+ -+ self.master.run_command( -+ [ -+ "ipa", -+ "migrate-ds", -+ "--with-compat", -+ "--user-container", -+ user_container, -+ "--group-container", -+ group_container, -+ ldaps_uri, -+ "--ca-cert-file", -+ ca_cert_file, -+ ], -+ stdin_text=self.master.config.admin_password, -+ ) -+ # Verify migrated user and group from instance1.ldif -+ self.master.run_command( -+ ["ipa", "user-show", "ldapuser_0001"] -+ ) -+ self.master.run_command( -+ ["ipa", "group-show", "ldapgroup_0001"] -+ ) -+ -+ # Clean up migrated user and group -+ self.master.run_command( -+ ["ipa", "user-del", "ldapuser_0001"], raiseonerr=False -+ ) -+ self.master.run_command( -+ ["ipa", "group-del", "ldapgroup_0001"], raiseonerr=False -+ ) --- -2.52.0 - diff --git a/0039-ipatests-Add-ipa-selfservice-show-and-selfservice-mo.patch b/0039-ipatests-Add-ipa-selfservice-show-and-selfservice-mo.patch deleted file mode 100644 index 506a8e2..0000000 --- a/0039-ipatests-Add-ipa-selfservice-show-and-selfservice-mo.patch +++ /dev/null @@ -1,506 +0,0 @@ -From 51e669d46f912d39841bc40560682f9f414f68fe Mon Sep 17 00:00:00 2001 -From: Jay Gondaliya -Date: Mon, 16 Mar 2026 15:03:17 +0530 -Subject: [PATCH] ipatests: Add ipa selfservice-show and - selfservice-mod CLI tests to xmlrpc - -Add two new Declarative test classes to test_selfservice_plugin.py covering the selfservice-show and selfservice-mod CLI command options, converted from the legacy bash test suite (selfservice_show_1001-1003 and selfservice_mod_1002-1008). - -test_selfservice_show_cli covers: -- selfservice-show --all -- selfservice-show --all --raw -- selfservice-show --raw - -test_selfservice_mod_cli covers: -- selfservice-mod with invalid attrs (negative) -- selfservice-mod with invalid permissions (negative) -- selfservice-mod with no changes / EmptyModlist (negative) -- selfservice-mod with bad attrs must not delete the entry (BZ 747741) -- selfservice-mod changing and expanding attrs (positive) -- selfservice-mod with invalid/same permissions (negative) -- selfservice-mod changing and expanding permissions (positive) - -Tests for selfservice_mod_1001 and selfservice_mod_1009, which require interactive input and are not valid in a non-interactive context, are covered as integration tests in test_commands.py by test_selfservice_mod_no_attrs_or_permissions_all and test_selfservice_mod_no_attrs_or_permissions_raw. Tests for BZ 772675 (mod --raw) and BZ 747741 (bad attrs) cross-reference existing coverage in test_selfservice_misc. - -Fixes: https://pagure.io/freeipa/issue/9945 -Assisted-by: Claude -Signed-off-by: Jay Gondaliya -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: David Hanina ---- - ipatests/test_integration/test_commands.py | 54 +++ - .../test_xmlrpc/test_selfservice_plugin.py | 396 ++++++++++++++++++ - 2 files changed, 450 insertions(+) - -diff --git a/ipatests/test_integration/test_commands.py b/ipatests/test_integration/test_commands.py -index 6d7ee8f2d..5021747b6 100644 ---- a/ipatests/test_integration/test_commands.py -+++ b/ipatests/test_integration/test_commands.py -@@ -391,6 +391,60 @@ class TestIPACommand(IntegrationTest): - assert result.returncode == 1 - assert "Number of permissions added 0" in result.stdout_text - -+ def test_selfservice_mod_no_attrs_or_permissions_all(self): -+ """selfservice-mod --all with no --attrs or --permissions fails. -+ -+ When selfservice-mod is invoked with only --all and neither --attrs -+ nor --permissions is supplied, the CLI has no modifications to apply -+ and must return a non-zero exit code with an appropriate error message. -+ """ -+ entry = 'test_selfservice_mod_all' -+ tasks.kinit_admin(self.master) -+ self.master.run_command( -+ ['ipa', 'selfservice-add', entry, -+ '--attrs=l', '--permissions=write'] -+ ) -+ try: -+ result = self.master.run_command( -+ ['ipa', 'selfservice-mod', entry, '--all'], -+ stdin_text='\n', -+ raiseonerr=False, -+ ) -+ assert result.returncode != 0 -+ assert 'no modifications to be performed' in result.stderr_text -+ finally: -+ self.master.run_command( -+ ['ipa', 'selfservice-del', entry], -+ raiseonerr=False, -+ ) -+ -+ def test_selfservice_mod_no_attrs_or_permissions_raw(self): -+ """selfservice-mod --raw with no --attrs or --permissions fails. -+ -+ When selfservice-mod is invoked with only --raw and neither --attrs -+ nor --permissions is supplied, the CLI has no modifications to apply -+ and must return a non-zero exit code with an appropriate error message. -+ """ -+ entry = 'test_selfservice_mod_raw' -+ tasks.kinit_admin(self.master) -+ self.master.run_command( -+ ['ipa', 'selfservice-add', entry, -+ '--attrs=l', '--permissions=write'] -+ ) -+ try: -+ result = self.master.run_command( -+ ['ipa', 'selfservice-mod', entry, '--raw'], -+ stdin_text='\n', -+ raiseonerr=False, -+ ) -+ assert result.returncode != 0 -+ assert 'no modifications to be performed' in result.stderr_text -+ finally: -+ self.master.run_command( -+ ['ipa', 'selfservice-del', entry], -+ raiseonerr=False, -+ ) -+ - def test_change_sysaccount_password_issue7561(self): - sysuser = 'system' - original_passwd = 'Secret123' -diff --git a/ipatests/test_xmlrpc/test_selfservice_plugin.py b/ipatests/test_xmlrpc/test_selfservice_plugin.py -index 8f2307a20..48dfd7cc3 100644 ---- a/ipatests/test_xmlrpc/test_selfservice_plugin.py -+++ b/ipatests/test_xmlrpc/test_selfservice_plugin.py -@@ -942,3 +942,399 @@ class test_selfservice_cli_add_del(Declarative): - ), - - ] -+ -+ -+# selfservice-show & selfservice-mod CLI test rule names -+ -+SS_CLI_SHOW = 'SELFSERVICE_SHOW_TEST' -+ -+ -+@pytest.mark.tier1 -+class test_selfservice_show_cli(Declarative): -+ """Test selfservice-show CLI options.""" -+ -+ cleanup_commands = [ -+ ('selfservice_del', [SS_CLI_SHOW], {}), -+ ] -+ -+ tests = [ -+ dict( -+ desc='Create %r for show tests' % SS_CLI_SHOW, -+ command=( -+ 'selfservice_add', -+ [SS_CLI_SHOW], -+ dict( -+ attrs=['l'], -+ permissions='write', -+ ), -+ ), -+ expected=dict( -+ value=SS_CLI_SHOW, -+ summary='Added selfservice "%s"' % SS_CLI_SHOW, -+ result=dict( -+ attrs=['l'], -+ permissions=['write'], -+ selfaci=True, -+ aciname=SS_CLI_SHOW, -+ ), -+ ), -+ ), -+ -+ # Show with --all (positive test) -+ dict( -+ desc='Show %r with --all' % SS_CLI_SHOW, -+ command=( -+ 'selfservice_show', -+ [SS_CLI_SHOW], -+ {'all': True}, -+ ), -+ expected=dict( -+ value=SS_CLI_SHOW, -+ summary=None, -+ result=dict( -+ attrs=['l'], -+ permissions=['write'], -+ selfaci=True, -+ aciname=SS_CLI_SHOW, -+ ), -+ ), -+ ), -+ -+ # Show with --all and --raw (positive test) -+ dict( -+ desc='Show %r with --all and --raw' % SS_CLI_SHOW, -+ command=( -+ 'selfservice_show', -+ [SS_CLI_SHOW], -+ {'all': True, 'raw': True}, -+ ), -+ expected=dict( -+ value=SS_CLI_SHOW, -+ summary=None, -+ result={ -+ 'aci': '(targetattr = "l")' -+ '(version 3.0;acl ' -+ '"selfservice:%s";' -+ 'allow (write) ' -+ 'userdn = "ldap:///self";)' -+ % SS_CLI_SHOW, -+ }, -+ ), -+ ), -+ -+ # Show with --raw (positive test) -+ dict( -+ desc='Show %r with --raw' % SS_CLI_SHOW, -+ command=( -+ 'selfservice_show', -+ [SS_CLI_SHOW], -+ {'raw': True}, -+ ), -+ expected=dict( -+ value=SS_CLI_SHOW, -+ summary=None, -+ result={ -+ 'aci': '(targetattr = "l")' -+ '(version 3.0;acl ' -+ '"selfservice:%s";' -+ 'allow (write) ' -+ 'userdn = "ldap:///self";)' -+ % SS_CLI_SHOW, -+ }, -+ ), -+ ), -+ -+ dict( -+ desc='Delete %r' % SS_CLI_SHOW, -+ command=('selfservice_del', [SS_CLI_SHOW], {}), -+ expected=dict( -+ result=True, -+ value=SS_CLI_SHOW, -+ summary='Deleted selfservice "%s"' % SS_CLI_SHOW, -+ ), -+ ), -+ -+ ] -+ -+ -+SS_CLI_MOD = 'SELFSERVICE_MOD_TEST' -+ -+ -+@pytest.mark.tier1 -+class test_selfservice_mod_cli(Declarative): -+ """Test selfservice-mod CLI options.""" -+ -+ cleanup_commands = [ -+ ('selfservice_del', [SS_CLI_MOD], {}), -+ ] -+ -+ tests = [ -+ dict( -+ desc='Create %r for mod tests' % SS_CLI_MOD, -+ command=( -+ 'selfservice_add', -+ [SS_CLI_MOD], -+ dict( -+ attrs=['l'], -+ permissions='write', -+ ), -+ ), -+ expected=dict( -+ value=SS_CLI_MOD, -+ summary='Added selfservice "%s"' % SS_CLI_MOD, -+ result=dict( -+ attrs=['l'], -+ permissions=['write'], -+ selfaci=True, -+ aciname=SS_CLI_MOD, -+ ), -+ ), -+ ), -+ -+ # test_selfservice_mod_no_attrs_or_permissions_all and -+ # test_selfservice_mod_no_attrs_or_permissions_raw are covered in -+ # integration tests in test_commands.py since they are -+ # interactive tests. -+ -+ # Modify with --all --attrs=badattr --permissions=write --raw -+ # (negative test - invalid attr value) -+ dict( -+ desc='Try to modify %r with invalid attrs' % SS_CLI_MOD, -+ command=( -+ 'selfservice_mod', -+ [SS_CLI_MOD], -+ dict( -+ attrs=['badattr'], -+ permissions='write', -+ all=True, -+ raw=True, -+ ), -+ ), -+ expected=errors.InvalidSyntax( -+ attr=( -+ r'targetattr "badattr" does not exist in schema. ' -+ r'Please add attributeTypes "badattr" to ' -+ r'schema if necessary. ' -+ r'ACL Syntax Error(-5):' -+ r'(targetattr = \22badattr\22)' -+ r'(version 3.0;acl ' -+ r'\22selfservice:%s\22;' -+ r'allow (write) userdn = \22ldap:///self\22;)' -+ ) % SS_CLI_MOD, -+ ), -+ ), -+ -+ # Modify with --all --attrs=l --permissions=badperm --raw -+ # (negative test - invalid permission value) -+ dict( -+ desc=( -+ 'Try to modify %r with invalid permissions' -+ % SS_CLI_MOD -+ ), -+ command=( -+ 'selfservice_mod', -+ [SS_CLI_MOD], -+ dict( -+ attrs=['l'], -+ permissions='badperm', -+ all=True, -+ raw=True, -+ ), -+ ), -+ expected=errors.ValidationError( -+ name='permissions', -+ error='"badperm" is not a valid permission', -+ ), -+ ), -+ -+ # Modify with same attrs and perms already set -+ # (negative test - no modifications error) -+ dict( -+ desc='Try to modify %r with no changes' % SS_CLI_MOD, -+ command=( -+ 'selfservice_mod', -+ [SS_CLI_MOD], -+ dict( -+ attrs=['l'], -+ permissions='write', -+ all=True, -+ raw=True, -+ ), -+ ), -+ expected=errors.EmptyModlist(), -+ ), -+ -+ # Modify with --attrs=badattrs (negative test, -+ # BZ 747741 - a bad attrs mod must not delete the entry). -+ dict( -+ desc=( -+ 'Try to modify %r with bad attrs (BZ 747741)' -+ % SS_CLI_MOD -+ ), -+ command=( -+ 'selfservice_mod', -+ [SS_CLI_MOD], -+ dict(attrs=['badattrs']), -+ ), -+ expected=errors.InvalidSyntax( -+ attr=( -+ r'targetattr "badattrs" does not exist in schema. ' -+ r'Please add attributeTypes "badattrs" to ' -+ r'schema if necessary. ' -+ r'ACL Syntax Error(-5):' -+ r'(targetattr = \22badattrs\22)' -+ r'(version 3.0;acl ' -+ r'\22selfservice:%s\22;' -+ r'allow (write) userdn = \22ldap:///self\22;)' -+ ) % SS_CLI_MOD, -+ ), -+ ), -+ -+ # Verify entry still exists after failed mod (BZ 747741) -+ dict( -+ desc=( -+ 'Verify %r still exists after failed mod' -+ % SS_CLI_MOD -+ ), -+ command=('selfservice_show', [SS_CLI_MOD], {}), -+ expected=dict( -+ value=SS_CLI_MOD, -+ summary=None, -+ result=dict( -+ attrs=['l'], -+ permissions=['write'], -+ selfaci=True, -+ aciname=SS_CLI_MOD, -+ ), -+ ), -+ ), -+ -+ # Modify with --attrs=mobile (positive test - change attr) -+ dict( -+ desc='Modify %r attrs to mobile' % SS_CLI_MOD, -+ command=( -+ 'selfservice_mod', -+ [SS_CLI_MOD], -+ dict(attrs=['mobile']), -+ ), -+ expected=dict( -+ value=SS_CLI_MOD, -+ summary='Modified selfservice "%s"' % SS_CLI_MOD, -+ result=dict( -+ attrs=['mobile'], -+ permissions=['write'], -+ selfaci=True, -+ aciname=SS_CLI_MOD, -+ ), -+ ), -+ ), -+ -+ # Modify with --attrs={mobile,l} (positive test - add attr) -+ dict( -+ desc='Modify %r attrs to mobile and l' % SS_CLI_MOD, -+ command=( -+ 'selfservice_mod', -+ [SS_CLI_MOD], -+ dict(attrs=['mobile', 'l']), -+ ), -+ expected=dict( -+ value=SS_CLI_MOD, -+ summary='Modified selfservice "%s"' % SS_CLI_MOD, -+ result=dict( -+ attrs=['mobile', 'l'], -+ permissions=['write'], -+ selfaci=True, -+ aciname=SS_CLI_MOD, -+ ), -+ ), -+ ), -+ -+ # Modify with --permissions=badperm -+ # (negative test - invalid permission string) -+ dict( -+ desc=( -+ 'Try to modify %r with invalid permissions' -+ % SS_CLI_MOD -+ ), -+ command=( -+ 'selfservice_mod', -+ [SS_CLI_MOD], -+ dict(permissions='badperm'), -+ ), -+ expected=errors.ValidationError( -+ name='permissions', -+ error='"badperm" is not a valid permission', -+ ), -+ ), -+ -+ # Modify with --permissions=write -+ # (negative test - same perm already set, no modification) -+ dict( -+ desc='Try to modify %r with same permissions' % SS_CLI_MOD, -+ command=( -+ 'selfservice_mod', -+ [SS_CLI_MOD], -+ dict(permissions='write'), -+ ), -+ expected=errors.EmptyModlist(), -+ ), -+ -+ # Modify with --permissions=read (positive test - change perm) -+ dict( -+ desc='Modify %r permissions to read' % SS_CLI_MOD, -+ command=( -+ 'selfservice_mod', -+ [SS_CLI_MOD], -+ dict(permissions='read'), -+ ), -+ expected=dict( -+ value=SS_CLI_MOD, -+ summary='Modified selfservice "%s"' % SS_CLI_MOD, -+ result=dict( -+ attrs=['mobile', 'l'], -+ permissions=['read'], -+ selfaci=True, -+ aciname=SS_CLI_MOD, -+ ), -+ ), -+ ), -+ -+ # Modify with --permissions={read,write} (positive test - add perm) -+ dict( -+ desc=( -+ 'Modify %r permissions to read and write' -+ % SS_CLI_MOD -+ ), -+ command=( -+ 'selfservice_mod', -+ [SS_CLI_MOD], -+ dict(permissions=['read', 'write']), -+ ), -+ expected=dict( -+ value=SS_CLI_MOD, -+ summary='Modified selfservice "%s"' % SS_CLI_MOD, -+ result=dict( -+ attrs=['mobile', 'l'], -+ permissions=['read', 'write'], -+ selfaci=True, -+ aciname=SS_CLI_MOD, -+ ), -+ ), -+ ), -+ -+ # test_selfservice_mod_no_attrs_or_permissions_all and -+ # test_selfservice_mod_no_attrs_or_permissions_raw are covered in -+ # integration tests in test_commands.py since they are -+ # interactive tests. -+ -+ dict( -+ desc='Delete %r' % SS_CLI_MOD, -+ command=('selfservice_del', [SS_CLI_MOD], {}), -+ expected=dict( -+ result=True, -+ value=SS_CLI_MOD, -+ summary='Deleted selfservice "%s"' % SS_CLI_MOD, -+ ), -+ ), -+ -+ ] --- -2.52.0 - diff --git a/0040-ipatests-Add-selfservice-find-cli-tests-to-xmlrpc-Ad.patch b/0040-ipatests-Add-selfservice-find-cli-tests-to-xmlrpc-Ad.patch deleted file mode 100644 index f333392..0000000 --- a/0040-ipatests-Add-selfservice-find-cli-tests-to-xmlrpc-Ad.patch +++ /dev/null @@ -1,506 +0,0 @@ -From c294159878de52fa5762025ee4be893f280c5320 Mon Sep 17 00:00:00 2001 -From: Florence Blanc-Renaud -Date: Tue, 31 Mar 2026 15:38:36 +0200 -Subject: [PATCH] ipatests: Add selfservice-find cli tests to xmlrpc - Add a new Declarative test class test_selfservice_cli_find covering CLI-level - behaviour of the selfservice-find command: - -- find with --all succeeds and returns the matching rule -- non-existent or wrong attrs filter returns zero results -- bad --name filter returns zero results with valid and invalid - positional arg -- bad permissions filter with --all --raw returns zero results, - no internal error (BZ 747693) -- all valid params with --all --raw succeeds and returns the raw - ACI string (BZ 747693) -- wrong or non-existent attrs filter returns zero results with and - without name arg -- valid --attrs filter succeeds with positional name arg and with - --name option -- valid --name filter succeeds and returns the matching rule -- bad permissions filter returns zero results -- valid --permissions filter succeeds with positional name arg and - with --name option -- --raw only succeeds and returns the raw ACI string, no internal - error (BZ 747693) - -Signed-off-by: Jay Gondaliya -Fixes: https://pagure.io/freeipa/issue/9945 -Assisted-by: Claude -Reviewed-By: Florence Blanc-Renaud -Reviewed-By: Rafael Guterres Jeffman -Reviewed-By: David Hanina ---- - .../test_xmlrpc/test_selfservice_plugin.py | 456 ++++++++++++++++++ - 1 file changed, 456 insertions(+) - -diff --git a/ipatests/test_xmlrpc/test_selfservice_plugin.py b/ipatests/test_xmlrpc/test_selfservice_plugin.py -index 48dfd7cc3..9e921156a 100644 ---- a/ipatests/test_xmlrpc/test_selfservice_plugin.py -+++ b/ipatests/test_xmlrpc/test_selfservice_plugin.py -@@ -944,6 +944,462 @@ class test_selfservice_cli_add_del(Declarative): - ] - - -+# selfservice-find CLI test rule name -+SS_CLI_FIND = 'SELFSERVICE_FIND_TEST' -+ -+ -+@pytest.mark.tier1 -+class test_selfservice_cli_find(Declarative): -+ """Tests for the selfservice-find CLI command.""" -+ -+ cleanup_commands = [ -+ ('selfservice_del', [SS_CLI_FIND], {}), -+ ] -+ -+ tests = [ -+ -+ # Setup: create the rule used by all find tests -+ dict( -+ desc='Setup: create %r' % SS_CLI_FIND, -+ command=( -+ 'selfservice_add', -+ [SS_CLI_FIND], -+ dict(attrs=['l'], permissions='write'), -+ ), -+ expected=dict( -+ value=SS_CLI_FIND, -+ summary='Added selfservice "%s"' % SS_CLI_FIND, -+ result=dict( -+ attrs=['l'], -+ permissions=['write'], -+ selfaci=True, -+ aciname=SS_CLI_FIND, -+ ), -+ ), -+ ), -+ -+ # Find with --all returns the parsed result -+ dict( -+ desc='Search for %r with --all' % SS_CLI_FIND, -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict(all=True), -+ ), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 selfservice matched', -+ result=[{ -+ 'attrs': ['l'], -+ 'permissions': ['write'], -+ 'selfaci': True, -+ 'aciname': SS_CLI_FIND, -+ }], -+ ), -+ ), -+ -+ # Bad attrs filter -- aci_find does pure string -+ # comparison; no schema validation in find. -+ dict( -+ desc=( -+ 'Non-existent attr with all filters' -+ ' returns no match (--all --raw)' -+ ), -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict( -+ all=True, -+ attrs=['badattrs'], -+ aciname=SS_CLI_FIND, -+ permissions='write', -+ raw=True, -+ ), -+ ), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 selfservices matched', -+ result=[], -+ ), -+ ), -+ -+ # Wrong attr for this rule (has 'l', not 'mobile') -+ dict( -+ desc=( -+ 'Wrong attr for rule with all filters' -+ ' returns no match (--all --raw)' -+ ), -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict( -+ all=True, -+ attrs=['mobile'], -+ aciname=SS_CLI_FIND, -+ permissions='write', -+ raw=True, -+ ), -+ ), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 selfservices matched', -+ result=[], -+ ), -+ ), -+ -+ # Bad --name filter with --all --raw -+ dict( -+ desc=( -+ 'Valid name arg with bad --name filter' -+ ' returns no match (--all --raw)' -+ ), -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict( -+ all=True, -+ attrs=['l'], -+ aciname='badname', -+ permissions='write', -+ raw=True, -+ ), -+ ), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 selfservices matched', -+ result=[], -+ ), -+ ), -+ -+ # Bad name arg also set to 'badname' -+ dict( -+ desc=( -+ 'Bad name arg with bad --name filter' -+ ' returns no match (--all --raw)' -+ ), -+ command=( -+ 'selfservice_find', -+ ['badname'], -+ dict( -+ all=True, -+ attrs=['l'], -+ aciname='badname', -+ permissions='write', -+ raw=True, -+ ), -+ ), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 selfservices matched', -+ result=[], -+ ), -+ ), -+ -+ # Bad permissions with --all --raw (BZ 747693) -+ # selfservice-find --raw must not return "internal error". -+ # aci_find treats permissions as a plain string filter (no -+ # validation), so 'badperm' simply matches nothing. -+ dict( -+ desc=( -+ 'Bad permissions with --all --raw' -+ ' returns no match (BZ 747693)' -+ ), -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict( -+ all=True, -+ attrs=['l'], -+ aciname=SS_CLI_FIND, -+ permissions='badperm', -+ raw=True, -+ ), -+ ), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 selfservices matched', -+ result=[], -+ ), -+ ), -+ -+ # All valid params with --all --raw (BZ 747693) -+ # selfservice-find --raw must not return "internal error". -+ dict( -+ desc=( -+ 'All valid params with --all --raw' -+ ' returns raw ACI (BZ 747693)' -+ ), -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict( -+ all=True, -+ attrs=['l'], -+ aciname=SS_CLI_FIND, -+ permissions='write', -+ raw=True, -+ ), -+ ), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 selfservice matched', -+ result=[{ -+ 'aci': ( -+ '(targetattr = "l")' -+ '(version 3.0;acl "selfservice:%s";' -+ 'allow (write) ' -+ 'userdn = "ldap:///self";)' -+ % SS_CLI_FIND -+ ), -+ }], -+ ), -+ ), -+ -+ # Bad attrs filter without --all --raw -+ dict( -+ desc='Wrong attr filter returns no match', -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict(attrs=['mobile']), -+ ), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 selfservices matched', -+ result=[], -+ ), -+ ), -+ -+ # Non-existent attr in filter -+ dict( -+ desc='Non-existent attr filter returns no match', -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict(attrs=['badattrs']), -+ ), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 selfservices matched', -+ result=[], -+ ), -+ ), -+ -+ # Non-existent attr without name arg -+ dict( -+ desc='Non-existent attr without name arg returns no match', -+ command=( -+ 'selfservice_find', -+ [], -+ dict(attrs=['badattrs']), -+ ), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 selfservices matched', -+ result=[], -+ ), -+ ), -+ -+ # Valid attrs filter -+ dict( -+ desc='Valid attrs filter with name arg returns match', -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict(attrs=['l']), -+ ), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 selfservice matched', -+ result=[{ -+ 'attrs': ['l'], -+ 'permissions': ['write'], -+ 'selfaci': True, -+ 'aciname': SS_CLI_FIND, -+ }], -+ ), -+ ), -+ -+ # Without positional name arg but with --name -+ # filter to get a deterministic result. -+ dict( -+ desc='Valid attrs filter with --name option returns match', -+ command=( -+ 'selfservice_find', -+ [], -+ dict(attrs=['l'], aciname=SS_CLI_FIND), -+ ), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 selfservice matched', -+ result=[{ -+ 'attrs': ['l'], -+ 'permissions': ['write'], -+ 'selfaci': True, -+ 'aciname': SS_CLI_FIND, -+ }], -+ ), -+ ), -+ -+ # Bad --name filter -+ dict( -+ desc='Valid name arg with bad --name filter returns no match', -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict(aciname='badname'), -+ ), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 selfservices matched', -+ result=[], -+ ), -+ ), -+ -+ # Bad name arg also set to 'badname' -+ dict( -+ desc='Bad name arg with bad --name filter returns no match', -+ command=( -+ 'selfservice_find', -+ ['badname'], -+ dict(aciname='badname'), -+ ), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 selfservices matched', -+ result=[], -+ ), -+ ), -+ -+ # Valid --name filter -+ dict( -+ desc='Valid --name filter returns match', -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict(aciname=SS_CLI_FIND), -+ ), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 selfservice matched', -+ result=[{ -+ 'attrs': ['l'], -+ 'permissions': ['write'], -+ 'selfaci': True, -+ 'aciname': SS_CLI_FIND, -+ }], -+ ), -+ ), -+ -+ # Bad permissions filter -- aci_find treats permissions -+ # as a plain string filter; 'badperm' matches nothing. -+ dict( -+ desc='Bad permissions filter returns no match', -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict(permissions='badperm'), -+ ), -+ expected=dict( -+ count=0, -+ truncated=False, -+ summary='0 selfservices matched', -+ result=[], -+ ), -+ ), -+ -+ # Valid permissions filter -+ dict( -+ desc='Valid permissions filter with name arg returns match', -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict(permissions='write'), -+ ), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 selfservice matched', -+ result=[{ -+ 'attrs': ['l'], -+ 'permissions': ['write'], -+ 'selfaci': True, -+ 'aciname': SS_CLI_FIND, -+ }], -+ ), -+ ), -+ -+ # Without positional name arg but with --name -+ # filter to get a deterministic result. -+ dict( -+ desc=( -+ 'Valid permissions filter with --name' -+ ' option returns match' -+ ), -+ command=( -+ 'selfservice_find', -+ [], -+ dict( -+ permissions='write', -+ aciname=SS_CLI_FIND, -+ ), -+ ), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 selfservice matched', -+ result=[{ -+ 'attrs': ['l'], -+ 'permissions': ['write'], -+ 'selfaci': True, -+ 'aciname': SS_CLI_FIND, -+ }], -+ ), -+ ), -+ -+ # Raw output only (BZ 747693) -+ # selfservice-find --raw must not return "internal error". -+ dict( -+ desc='Raw output returns ACI string without error (BZ 747693)', -+ command=( -+ 'selfservice_find', -+ [SS_CLI_FIND], -+ dict(raw=True), -+ ), -+ expected=dict( -+ count=1, -+ truncated=False, -+ summary='1 selfservice matched', -+ result=[{ -+ 'aci': ( -+ '(targetattr = "l")' -+ '(version 3.0;acl "selfservice:%s";' -+ 'allow (write) ' -+ 'userdn = "ldap:///self";)' -+ % SS_CLI_FIND -+ ), -+ }], -+ ), -+ ), -+ -+ ] -+ -+ - # selfservice-show & selfservice-mod CLI test rule names - - SS_CLI_SHOW = 'SELFSERVICE_SHOW_TEST' --- -2.52.0 - diff --git a/0041-ipatests-fix-the-method-add_a_record.patch b/0041-ipatests-fix-the-method-add_a_record.patch deleted file mode 100644 index 275d4d6..0000000 --- a/0041-ipatests-fix-the-method-add_a_record.patch +++ /dev/null @@ -1,36 +0,0 @@ -From 0306d2c5ebb1732fb33f3da4199f29ec887e1db8 Mon Sep 17 00:00:00 2001 -From: Florence Blanc-Renaud -Date: Thu, 9 Apr 2026 12:49:18 +0200 -Subject: [PATCH] ipatests: fix the method add_a_record - -The method add_a_record first checks if a DNS record exists. -If it finds one, it assumes there is already a DNS A record for -the host but this assumption is wrong. The dnsrecord-show command -may return another type of DNS record (for instance an SSHFP record). - -Create the A record if dnsrecord-show fails or if it doesn't return -any A record. - -Fixes: https://pagure.io/freeipa/issue/9972 -Signed-off-by: Florence Blanc-Renaud -Reviewed-By: Anuja More ---- - ipatests/pytest_ipa/integration/tasks.py | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py -index ee2befd6998da3b8035e4e015d7528b5a9676a7b..5ce18483d6c1049104afc1cef6524a7a19fc9047 100755 ---- a/ipatests/pytest_ipa/integration/tasks.py -+++ b/ipatests/pytest_ipa/integration/tasks.py -@@ -1620,7 +1620,7 @@ def add_a_record(master, host): - raiseonerr=False) - - # If not, add it -- if cmd.returncode != 0: -+ if cmd.returncode != 0 or 'A record' not in cmd.stdout_text: - master.run_command(['ipa', - 'dnsrecord-add', - master.domain.name, --- -2.52.0 - diff --git a/0042-ipatests-Remove-xfail-for-sssd-issues-7169.patch b/0042-ipatests-Remove-xfail-for-sssd-issues-7169.patch deleted file mode 100644 index 646a3e1..0000000 --- a/0042-ipatests-Remove-xfail-for-sssd-issues-7169.patch +++ /dev/null @@ -1,48 +0,0 @@ -From a1b1a45c45f521659ee25709141b6470599030d0 Mon Sep 17 00:00:00 2001 -From: David Hanina -Date: Mon, 13 Apr 2026 16:23:24 +0200 -Subject: [PATCH] ipatests-Remove xfail for sssd/issues/7169 - -4 of the failing tests are now succeeding, removing the xfail. - -Signed-off-by: David Hanina ---- - ipatests/test_integration/test_trust.py | 18 +++--------------- - 1 file changed, 3 insertions(+), 15 deletions(-) - -diff --git a/ipatests/test_integration/test_trust.py b/ipatests/test_integration/test_trust.py -index 0cab277c910a6d35f35b57e3068ee6f38706af59..d7f25d658b24f3b261260e735d3679e81e35ed15 100644 ---- a/ipatests/test_integration/test_trust.py -+++ b/ipatests/test_integration/test_trust.py -@@ -1219,14 +1219,7 @@ class TestNonPosixAutoPrivateGroup(BaseTestTrust): - assert (uid == self.uid_override and gid == self.gid_override) - test_group = self.clients[0].run_command( - ["id", nonposixuser]).stdout_text -- cond2 = (((type == 'false' -- and sssd_version >= tasks.parse_version("2.9.4")) -- or type == 'hybrid') -- and sssd_version < tasks.parse_version("2.12.0")) -- with xfail_context(cond2, -- 'https://github.com/SSSD/sssd/issues/5989 ' -- 'and 7169'): -- assert "domain users@{0}".format(self.ad_domain) in test_group -+ assert "domain users@{0}".format(self.ad_domain) in test_group - - @pytest.mark.parametrize('type', ['hybrid', 'true', "false"]) - def test_nonposixuser_nondefault_primary_group(self, type): -@@ -1347,10 +1340,5 @@ class TestPosixAutoPrivateGroup(BaseTestTrust): - assert (uid == self.uid_override - and gid == self.gid_override) - result = self.clients[0].run_command(['id', posixuser]) -- sssd_version = tasks.get_sssd_version(self.clients[0]) -- bad_version = (tasks.parse_version("2.9.4") <= sssd_version -- < tasks.parse_version("2.12.0")) -- with xfail_context(bad_version and type in ('false', 'hybrid'), -- "https://github.com/SSSD/sssd/issues/7169"): -- assert "10047(testgroup@{0})".format( -- self.ad_domain) in result.stdout_text -+ assert "10047(testgroup@{0})".format( -+ self.ad_domain) in result.stdout_text --- -2.52.0 - diff --git a/0043-Fix-ipa-ca-show-ipa-all-not-listing-RSN-version.patch b/0043-Fix-ipa-ca-show-ipa-all-not-listing-RSN-version.patch deleted file mode 100644 index d473134..0000000 --- a/0043-Fix-ipa-ca-show-ipa-all-not-listing-RSN-version.patch +++ /dev/null @@ -1,28 +0,0 @@ -From 86a6e9cef5a3fdea3cb84c39575c90481bfe1623 Mon Sep 17 00:00:00 2001 -From: David Hanina -Date: Tue, 14 Apr 2026 14:24:13 +0200 -Subject: [PATCH] Fix ipa ca-show ipa --all not listing RSN version - -Resolves: RHEL-168047 - -Signed-off-by: David Hanina ---- - ipaserver/install/cainstance.py | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py -index 4738517..8e7512f 100644 ---- a/ipaserver/install/cainstance.py -+++ b/ipaserver/install/cainstance.py -@@ -1681,7 +1681,7 @@ class CAInstance(DogtagInstance): - api.env.basedn) - entry_attrs = api.Backend.ldap2.get_entry(dn) - version = entry_attrs.single_value.get( -- "ipaCaRandomSerialNumberVersion", "0" -+ "ipaCaRandomSerialNumberVersion", "-" - ) - if str(version) == str(value): - return --- -2.52.0 - diff --git a/0044-ipatests-fix-migration-test.patch b/0044-ipatests-fix-migration-test.patch deleted file mode 100644 index 22f6c0b..0000000 --- a/0044-ipatests-fix-migration-test.patch +++ /dev/null @@ -1,49 +0,0 @@ -From 3d5fe37d41332cdca486c9ca42089934e5ed7f89 Mon Sep 17 00:00:00 2001 -From: Florence Blanc-Renaud -Date: Mon, 30 Mar 2026 10:39:51 +0200 -Subject: [PATCH] ipatests: fix migration test - -A previous commit modified the DNS zone forwarder's IP address -but forgot to change the expected value in the test. -Use a constant in the test setup and in the test expectation. - -Related: https://pagure.io/freeipa/issue/9964 -Signed-off-by: Florence Blanc-Renaud -Reviewed-By: Sudhir Menon ---- - ipatests/test_integration/test_ipa_ipa_migration.py | 5 +++-- - 1 file changed, 3 insertions(+), 2 deletions(-) - -diff --git a/ipatests/test_integration/test_ipa_ipa_migration.py b/ipatests/test_integration/test_ipa_ipa_migration.py -index 105c79b..436e222 100644 ---- a/ipatests/test_integration/test_ipa_ipa_migration.py -+++ b/ipatests/test_integration/test_ipa_ipa_migration.py -@@ -15,6 +15,7 @@ import pytest - import re - import textwrap - -+TEST_ZONE_FORWARDER = "10.11.12.13" - - def prepare_ipa_server(master): - """ -@@ -227,7 +228,7 @@ def prepare_ipa_server(master): - "dnsforwardzone-add", - "forwardzone.test", - "--forwarder", -- "192.168.124.10", -+ TEST_ZONE_FORWARDER, - ] - ) - -@@ -827,7 +828,7 @@ class TestIPAMigrateCLIOptions(MigrationTest): - ) - assert 'Zone name: {}'.format(zone_name) in result.stdout_text - assert 'Active zone: True' in result.stdout_text -- assert 'Zone forwarders: 10.11.12.13' in result.stdout_text -+ assert f'Zone forwarders: {TEST_ZONE_FORWARDER}' in result.stdout_text - assert 'Forward policy: first' in result.stdout_text - - def test_ipa_migrate_version_option(self): --- -2.52.0 - diff --git a/freeipa.spec b/freeipa.spec index bb6089d..51b8d9c 100644 --- a/freeipa.spec +++ b/freeipa.spec @@ -69,7 +69,7 @@ %global krb5_kdb_version 9.0 # 0.7.16: https://github.com/drkjam/netaddr/issues/71 %global python_netaddr_version 0.7.19 -%global samba_version 4.24.3 +%global samba_version 4.24.3-1 %global slapi_nis_version 0.60.0 %global python_ldap_version 3.1.0-1 # version supporting Allow Uniqueness plugin to search uniqueness attributes @@ -79,12 +79,12 @@ %else %global ds_version 3.1.3-5 %endif -%global selinux_policy_version 38.1.1-1 +%global selinux_policy_version 38.1.83-1 %if 0%{?rhel} >= 10 %global krb5_version 1.21.3-6 %elif 0%{?rhel} == 9 -%global krb5_version 1.21.1-5 +%global krb5_version 1.21.1-10 %else %global krb5_version 1.18.2-31 %endif @@ -93,7 +93,7 @@ %global httpd_version 2.4.37-21 # support for passkey -%global sssd_version 2.9.7 +%global sssd_version 2.9.9-3 %else # Fedora @@ -164,7 +164,7 @@ # BIND employs 'pkcs11' OpenSSL engine instead of native PKCS11 # Fedora 31+ uses OpenSSL engine, as well as Fedora ELN (RHEL9) %if 0%{?fedora} || 0%{?rhel} >= 9 - %global openssl_pkcs11_version 0.4.10-6 + %global openssl_pkcs11_version 0.4.11-9 %global softhsm_version 2.5.0-4 %else %global with_bind_pkcs11 1 @@ -216,7 +216,7 @@ # Work-around fact that RPM SPEC parser does not accept # "Version: @VERSION@" in freeipa.spec.in used for Autoconf string replacement -%define IPA_VERSION 4.13.1 +%define IPA_VERSION 4.13.2 # Release candidate version -- uncomment with one percent for RC versions #%%global rc_version %define AT_SIGN @ @@ -229,7 +229,7 @@ Name: %{package_name} Version: %{IPA_VERSION} -Release: 4%{?rc_version:.%rc_version}%{?dist} +Release: 1%{?rc_version:.%rc_version}%{?dist} Summary: The Identity, Policy and Audit system License: GPL-3.0-or-later @@ -252,45 +252,8 @@ Source1: https://releases.pagure.org/freeipa/freeipa-%{version}%{?rc_vers Patch0001: 0001-Revert-Replace-netifaces-with-ifaddr.patch Patch0002: 0002-Revert-custodia-do-not-use-deprecated-jwcrypto-wrapp.patch Patch0003: 0003-Revert-Remove-NIS-server-support.patch +Patch0004: 0004-Change-u-to-u-for-sysusersd.patch Patch0006: 0006-Revert-Stop-using-deprecated-pkg_resources.patch -Patch0007: 0007-ipatests-Move-expire_password-fixture-into-TestIPACo.patch -Patch0008: 0008-ipatests-Fix-xfail-assertion-for-sssd-2.12.0.patch -Patch0009: 0009-ipatests-Add-DNS-functional-integration-tests.patch -Patch0010: 0010-ipa-advise-smart-card-client-script-does-not-need-kr.patch -Patch0011: 0011-ipatests-Fix-resolver-state-tracking-in-enforced-DNS.patch -Patch0012: 0012-freeipa.spec.in-Use-systemd-sysusers-to-setup-users-.patch -Patch0013: 0013-ipatests-add-Random-Password-based-replica-promotion.patch -Patch0014: 0014-ipatests-Add-integration-tests-for-ipa-join-command.patch -Patch0015: 0015-fetch_domains-Use-case-insensitive-comparison-for-do.patch -Patch0016: 0016-Handle-IPACertificate-types-in-xmlrpc.patch -Patch0017: 0017-Replace-None-with-when-uninstalling-CA.patch -Patch0018: 0018-ipatests-Add-xmlrpc-tests-for-ipa-delegation-cli.patch -Patch0019: 0019-ipa-join-initialize-pointer.patch -Patch0020: 0020-ipatests-pruning-is-enabled-when-RSN-is-enabled.patch -Patch0021: 0021-ipatests-Add-DNS-bugzilla-integration-tests.patch -Patch0022: 0022-Avoid-int-overflow-with-pwpolicy-minlife.patch -Patch0023: 0023-ipatests-fix-install-method-for-BasePWpolicy.patch -Patch0024: 0024-webui-tests-update-expected-max-value-for-krbminpwdl.patch -Patch0025: 0025-ipatests-Add-DNS-integration-tests.patch -Patch0026: 0026-ipatest-make-tests-compatible-with-Pytest-9.patch -Patch0027: 0027-ipatests-Add-ipa-selfservice-BZ-tests-to-xmlrpc.patch -Patch0028: 0028-Allow-32bit-gid.patch -Patch0029: 0029-ipatests-Add-ipa-selfservice-users-tests-to-xmlrpc.patch -Patch0030: 0030-ipatests-Fix-test_allow_query_transfer_ipv6-when-IPv.patch -Patch0031: 0031-ipatests-Add-XML-RPC-tests-for-i18n-user-attributes.patch -Patch0032: 0032-ipatests-Add-selfservice-add-and-selfservice-del-cli.patch -Patch0033: 0033-ipatests-Additional-tests-for-32BitIdranges.patch -Patch0034: 0034-ipatests-add-HTTP-GSSAPI-Kerberos-authentication-tes.patch -Patch0035: 0035-ipatests-Extend-netgroup-test-coverage.patch -Patch0036: 0036-ipatests-Add-user-principal-ipa-getkeytab-and-ipa-rm.patch -Patch0037: 0037-ipatests-Additional-tests-for-ipa-ipa-migration-test.patch -Patch0038: 0038-ipatests-ipa-migrate-ds-test-scenarios.patch -Patch0039: 0039-ipatests-Add-ipa-selfservice-show-and-selfservice-mo.patch -Patch0040: 0040-ipatests-Add-selfservice-find-cli-tests-to-xmlrpc-Ad.patch -Patch0041: 0041-ipatests-fix-the-method-add_a_record.patch -Patch0042: 0042-ipatests-Remove-xfail-for-sssd-issues-7169.patch -Patch0043: 0043-Fix-ipa-ca-show-ipa-all-not-listing-RSN-version.patch -Patch0044: 0044-ipatests-fix-migration-test.patch Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch %endif %endif @@ -348,7 +311,7 @@ BuildRequires: libsss_nss_idmap-devel >= %{sssd_version} BuildRequires: nodejs(abi) == 127 %elif 0%{?fedora} >= 39 # Do not use nodejs20 on fedora < 39, https://pagure.io/freeipa/issue/9374 -BuildRequires: nodejs(abi) < 127 +BuildRequires: nodejs(abi) < 127, /usr/bin/node, /usr/bin/npm %else BuildRequires: nodejs(abi) < 111 %endif @@ -1429,8 +1392,8 @@ if [ $1 -gt 1 ] ; then if grep -E -q 'Include' $SSH_CLIENT_SYSTEM_CONF 2>/dev/null ; then SSH_CLIENT_SYSTEM_CONF="/etc/ssh/ssh_config.d/04-ipa.conf" fi - sed -E --in-place=.orig 's/^(GlobalKnownHostsFile \/var\/lib\/sss\/pubconf\/known_hosts)$/# disabled by ipa-client update\n# \1/' $SSH_CLIENT_SYSTEM_CONF - sed -E --in-place=.orig 's/(ProxyCommand \/usr\/bin\/sss_ssh_knownhostsproxy -p \%p \%h)/# replaced by ipa-client update\n KnownHostsCommand \/usr\/bin\/sss_ssh_knownhosts \%H/' $SSH_CLIENT_SYSTEM_CONF + sed -E --in-place=.orig 's/^(GlobalKnownHostsFile \/var\/lib\/sss\/pubconf\/known_hosts)$/# disabled by ipa-client update\n# \1/' $SSH_CLIENT_SYSTEM_CONF || : + sed -E --in-place=.orig 's/(ProxyCommand \/usr\/bin\/sss_ssh_knownhostsproxy -p \%p \%h)/# replaced by ipa-client update\n KnownHostsCommand \/usr\/bin\/sss_ssh_knownhosts \%H/' $SSH_CLIENT_SYSTEM_CONF || : fi fi fi @@ -1502,9 +1465,9 @@ if [ -f '/etc/ssh/sshd_config' -a $restore -ge 2 ]; then GLOBALKNOWNHOSTFILE="GlobalKnownHostsFile /var/lib/sss/pubconf/known_hosts/" grep -qF '$GLOBALKNOWNHOSTFILE' $SSH_CLIENT_SYSTEM_CONF if [ $? -ne 0 ]; then - sed -E --in-place=.orig '/(# IPA-related configuration changes to ssh_config)/a # added by ipa-client update\n'"$GLOBALKNOWNHOSTFILE"'' $SSH_CLIENT_SYSTEM_CONF + sed -E --in-place=.orig '/(# IPA-related configuration changes to ssh_config)/a # added by ipa-client update\n'"$GLOBALKNOWNHOSTFILE"'' $SSH_CLIENT_SYSTEM_CONF || : fi - sed -E --in-place=.orig 's/(KnownHostsCommand \/usr\/bin\/sss_ssh_knownhosts \%H)/ProxyCommand \/usr\/bin\/sss_ssh_knownhostsproxy -p \%p \%h/' $SSH_CLIENT_SYSTEM_CONF + sed -E --in-place=.orig 's/(KnownHostsCommand \/usr\/bin\/sss_ssh_knownhosts \%H)/ProxyCommand \/usr\/bin\/sss_ssh_knownhostsproxy -p \%p \%h/' $SSH_CLIENT_SYSTEM_CONF || : fi fi fi @@ -1523,8 +1486,8 @@ if [ -f '/etc/ssh/sshd_config' -a $restore -ge 2 ]; then if grep -E -q 'Include' $SSH_CLIENT_SYSTEM_CONF 2>/dev/null ; then SSH_CLIENT_SYSTEM_CONF="/etc/ssh/ssh_config.d/04-ipa.conf" fi - sed -E --in-place=.orig 's/^(GlobalKnownHostsFile \/var\/lib\/sss\/pubconf\/known_hosts)$/# disabled by ipa-client update\n# \1/' $SSH_CLIENT_SYSTEM_CONF - sed -E --in-place=.orig 's/(ProxyCommand \/usr\/bin\/sss_ssh_knownhostsproxy -p \%p \%h)/# replaced by ipa-client update\n KnownHostsCommand \/usr\/bin\/sss_ssh_knownhosts \%H/' $SSH_CLIENT_SYSTEM_CONF + sed -E --in-place=.orig 's/^(GlobalKnownHostsFile \/var\/lib\/sss\/pubconf\/known_hosts)$/# disabled by ipa-client update\n# \1/' $SSH_CLIENT_SYSTEM_CONF || : + sed -E --in-place=.orig 's/(ProxyCommand \/usr\/bin\/sss_ssh_knownhostsproxy -p \%p \%h)/# replaced by ipa-client update\n KnownHostsCommand \/usr\/bin\/sss_ssh_knownhosts \%H/' $SSH_CLIENT_SYSTEM_CONF || : fi fi fi @@ -1759,6 +1722,8 @@ fi %dir %{_usr}/share/ipa/ui/css %{_usr}/share/ipa/ui/css/*.css %dir %{_usr}/share/ipa/ui/js +%dir %{_usr}/share/ipa/ui/js/inline +%{_usr}/share/ipa/ui/js/inline/*.js %dir %{_usr}/share/ipa/ui/js/dojo %{_usr}/share/ipa/ui/js/dojo/dojo.js %dir %{_usr}/share/ipa/ui/js/libs @@ -1789,8 +1754,6 @@ fi %ghost %attr(0640,root,root) %config(noreplace) %{_sysconfdir}/httpd/conf.d/ipa-pki-proxy.conf %ghost %attr(0644,root,root) %config(noreplace) %{_sysconfdir}/ipa/kdcproxy/ipa-kdc-proxy.conf %ghost %attr(0644,root,root) %config(noreplace) %{_usr}/share/ipa/html/ca.crt -%ghost %attr(0640,root,named) %config(noreplace) %{_sysconfdir}/named/ipa-ext.conf -%ghost %attr(0640,root,named) %config(noreplace) %{_sysconfdir}/named/ipa-options-ext.conf %dir %{_usr}/share/ipa/updates/ %{_usr}/share/ipa/updates/* %dir %{_localstatedir}/lib/ipa @@ -1803,7 +1766,6 @@ fi %attr(700,root,root) %dir %{_localstatedir}/lib/ipa/private %attr(700,root,root) %dir %{_localstatedir}/lib/ipa/passwds %ghost %attr(775,root,pkiuser) %{_localstatedir}/lib/ipa/pki-ca/publish -%ghost %attr(770,named,named) %{_localstatedir}/named/dyndb-ldap/ipa %dir %attr(0700,root,root) %{_sysconfdir}/ipa/custodia %dir %{_usr}/share/ipa/schema.d %attr(0644,root,root) %{_usr}/share/ipa/schema.d/README @@ -1825,6 +1787,9 @@ fi %attr(644,root,root) %{_unitdir}/ipa-dnskeysyncd.service %attr(644,root,root) %{_unitdir}/ipa-ods-exporter.socket %attr(644,root,root) %{_unitdir}/ipa-ods-exporter.service +%ghost %attr(0640,root,named) %config(noreplace) %{_sysconfdir}/named/ipa-ext.conf +%ghost %attr(0640,root,named) %config(noreplace) %{_sysconfdir}/named/ipa-options-ext.conf +%ghost %attr(770,named,named) %{_localstatedir}/named/dyndb-ldap/ipa %files server-encrypted-dns %doc README.md Contributors.txt @@ -2003,6 +1968,25 @@ fi %endif %changelog +* Mon Jul 20 2026 David Hanina - 4.13.2-1 +- Resolves: RHEL-166742 [Cursor Automated] Include latest fixes in python3-ipatests package [RHEL9.9] +- Resolves: RHEL-151581 Pagure #9948: Extend SELinux policy to allow use of SSSD authentication helpers on non-enrolled hosts [rhel-9] +- Resolves: RHEL-151560 ipa-migrate doesn't process users' sshpublickeys [rhel-9] +- Resolves: RHEL-179216 ipa-migrate tool is renaming host records & host info in automount information [RHEL 9.9] +- Resolves: RHEL-212442 freeipa-client %triggerin and %post can fail (rpm scriptlets should never fail, by policy) [rhel-9] +- Resolves: RHEL-212443 ipa-migrate doesn't properly handle replication conflicts [rhel-9] +- Resolves: RHEL-212444 ipa-migrate doesn't migrate IPA locations [rhel-9] +- Resolves: RHEL-212445 Pagure #9948: Extend SELinux policy to allow use of SSSD authentication helpers on non-enrolled hosts [rhel-9] +- Resolves: RHEL-212446 [RFE] X-Content-Type-Options header in IdM WebUI [rhel-9] +- Resolves: RHEL-212447 [RFE] Implement Referrer-Policy on IdM WebUI [rhel-9] +- Resolves: RHEL-212448 [RFE] CSP not implemented in IdM WebUI [rhel-9] +- Resolves: RHEL-212449 IPA compatibility with tomcat 10 [rhel-9] +- Resolves: RHEL-212450 Webui 3007 Requirement error "userCertificate" is required (not allowing certificate management for idview) [rhel-9] +- Resolves: RHEL-212451 To add "SameSite" flag to ipa_session cookies [rhel-9] +- Resolves: RHEL-212452 FreeIPA JSON-RPC CSRF Referer Validation Uses Naive Prefix Match - [rhel-9] +- Resolves: RHEL-212471 Pagure #9820: ipalockout_postop is incorrectly warning about locked user [rhel-9] +- Resolves: RHEL-178920 Rebase ipa to latest version for RHEL 9.9 + * Tue Jun 02 2026 Florence Blanc-Renaud - 4.13.1-4 - Related: RHEL-175124 Rebuilt for samba 4.24.3 diff --git a/sources b/sources index a619003..029125a 100644 --- a/sources +++ b/sources @@ -1,2 +1,2 @@ -SHA512 (freeipa-4.13.1.tar.gz) = 78d7675aa49e9a3323b36fd225e0f2aebfb0df58bff4f232cce96933dbe250a90bf062d64d1838b2ab84cb0764537646a7441c7c643672e2757a5501871d6311 -SHA512 (freeipa-4.13.1.tar.gz.asc) = ddde77e1bd04440f3aeb0fb1f28b017e8374f586c9287b6805fa482161e44bed7fc502a7dbb38bbf6dc7ac5013c353c950ba27148ffe915b88a7a746fb4e4f95 +SHA512 (freeipa-4.13.2.tar.gz) = 11fa7de4facbfe0b0fb278114d92a9d7b7750754d9194942e8967ecaf1fff3d5a61d5f409e4a50fbc3f65a14addb8eb865a16e7361632fcaf314464c5320ba73 +SHA512 (freeipa-4.13.2.tar.gz.asc) = 5ed637e30198ab527ff74f1f5940c5409bd6395a8b91668ed01221f49e61010c709206d74863dce40b60022bdb77bd608140d6cfda7ff5eee0662fca1a604964