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 <dhanina@redhat.com>
This commit is contained in:
David Hanina 2026-07-21 13:27:46 +02:00
parent 1876a10ce0
commit 943a17d82c
44 changed files with 94 additions and 13499 deletions

2
.gitignore vendored
View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,31 @@
From c1f9626a4b2b42cade05f83bd5699418e60767a4 Mon Sep 17 00:00:00 2001
From: David Hanina <dhanina@redhat.com>
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 <dhanina@redhat.com>
---
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

View File

@ -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

View File

@ -1,120 +0,0 @@
From 1c86c973c8dc778a71c8e2abf54ff37ececdd696 Mon Sep 17 00:00:00 2001
From: PRANAV THUBE <pthube@redhat.com>
Date: Tue, 13 Jan 2026 19:30:09 +0530
Subject: [PATCH] ipatests: Move expire_password fixture into TestIPACommand
class
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
.../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

View File

@ -1,48 +0,0 @@
From ecc0efa96e3c446586425d470657de7f2d5376bf Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
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 <flo@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
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

File diff suppressed because it is too large Load Diff

View File

@ -1,99 +0,0 @@
From 5756ed2af940378c16d9d52e083b8c4005d41a13 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
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 <flo@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
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

View File

@ -1,177 +0,0 @@
From fd84cec77d18e0e608285ab712f8211b0b49a7fe Mon Sep 17 00:00:00 2001
From: PRANAV THUBE <pthube@redhat.com>
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 <antorres@redhat.com>
---
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

View File

@ -1,224 +0,0 @@
From 0800065ac5555dba102f05c947ca47b5dc9a81af Mon Sep 17 00:00:00 2001
From: Rafael Guterres Jeffman <rjeffman@redhat.com>
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 <noreply@anthropic.com>
Signed-off-by: Rafael Guterres Jeffman <rjeffman@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
---
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

View File

@ -1,196 +0,0 @@
From a55f9185c96457bdffe9099ddde39ec696f1f998 Mon Sep 17 00:00:00 2001
From: Anuja More <amore@redhat.com>
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 <amore@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
.../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<password>.*$)",
+ 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

View File

@ -1,760 +0,0 @@
From 7cb2c5d38a84eb31aa9ddd20cf9dc0b6d90fa242 Mon Sep 17 00:00:00 2001
From: PRANAV THUBE <pthube@redhat.com>
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 <rcritten@redhat.com>
---
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

View File

@ -1,48 +0,0 @@
From 7f6a2835f0972af5e94b58daf47fa60bfade4279 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
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 <flo@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
---
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

View File

@ -1,43 +0,0 @@
From 8deb4be0962b25dfd43e1245307a8bb9d58cfc48 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
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 <rcritten@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
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

View File

@ -1,159 +0,0 @@
From a583b0dc08536a50e10b76e27861864b61906355 Mon Sep 17 00:00:00 2001
From: David Hanina <dhanina@redhat.com>
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 <dhanina@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
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

View File

@ -1,416 +0,0 @@
From 91a6618e51b0e767c5cc5e4b1719531dbbd7268d Mon Sep 17 00:00:00 2001
From: Sudhir Menon <sumenon@redhat.com>
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 <noreply@anthropic.com>
Signed-off-by: Sudhir Menon <sumenon@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Anuja More <amore@redhat.com>
---
.../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

View File

@ -1,49 +0,0 @@
From 7cc96e42683a6d3ec9f2dc2a19e99330b6f3ce58 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
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 <flo@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Rafael Guterres Jeffman <rjeffman@redhat.com>
---
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

View File

@ -1,39 +0,0 @@
From d5efb4decb74b50c05b1d252add1c075e660d154 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
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 <flo@redhat.com>
---
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

File diff suppressed because it is too large Load Diff

View File

@ -1,154 +0,0 @@
From 7b0ac4f3dd8febf0c22a11bf2db9b780a242e302 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
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 <rcritten@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
Reviewed-By: Rafael Guterres Jeffman <rjeffman@redhat.com>
---
.../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

View File

@ -1,42 +0,0 @@
From 5cd2639f539ce220c291b00afafa72fd35e1d07e Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
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 <flo@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
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

View File

@ -1,36 +0,0 @@
From 8a7486bc980b7f9b19e6b15dee6642679198c40f Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
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 <flo@redhat.com>
Reviewed-By: Carla Martinez <carlmart@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
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

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +0,0 @@
From 0bf2a549a8a858c393ef59487bc1d395e5535c07 Mon Sep 17 00:00:00 2001
From: Stanislav Levin <slev@altlinux.org>
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 <slev@altlinux.org>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
---
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

View File

@ -1,154 +0,0 @@
From f60df430602db1e3949fa67f3744d40bd9b1d971 Mon Sep 17 00:00:00 2001
From: Jay Gondaliya <jgondali@redhat.com>
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 <rcritten@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
.../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

View File

@ -1,37 +0,0 @@
From 58239f9fbe33408b5cb5c52ba5132f6deb6b8f40 Mon Sep 17 00:00:00 2001
From: David Hanina <dhanina@redhat.com>
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 <dhanina@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
---
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

View File

@ -1,400 +0,0 @@
From ffae7d109b9cc968bb9942da78ea4238eee5497b Mon Sep 17 00:00:00 2001
From: Jay Gondaliya <jgondali@redhat.com>
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 <frenaud@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
Reviewed-By: PRANAV THUBE <pthube@redhat.com>
---
.../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

View File

@ -1,138 +0,0 @@
From 313bd8ff118a79dca5aad0b19ec8f69519258f89 Mon Sep 17 00:00:00 2001
From: PRANAV THUBE <pthube@redhat.com>
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 <dhanina@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
---
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

View File

@ -1,217 +0,0 @@
From c8a832e19699a7bb6ff486055015f033a3137e5f Mon Sep 17 00:00:00 2001
From: PRANAV THUBE <pthube@redhat.com>
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 <flo@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
Reviewed-By: Carla Martinez <carlmart@redhat.com>
---
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 <pthube@redhat.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+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

View File

@ -1,229 +0,0 @@
From 3056bf8fb27732213591f4c86044ce6980054ec9 Mon Sep 17 00:00:00 2001
From: Jay Gondaliya <jgondali@redhat.com>
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 <jgondali@redhat.com>
Fixes: https://pagure.io/freeipa/issue/9945
Assisted-by: Claude noreply@anthropic.com
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
.../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

View File

@ -1,421 +0,0 @@
From b05586c2a6a81c7121dd40f8d627cd8a2c5908d8 Mon Sep 17 00:00:00 2001
From: Sudhir Menon <sumenon@redhat.com>
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 <sumenon@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
.../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 <username> 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

View File

@ -1,260 +0,0 @@
From 3acf55ed7fcf9a3b38deb0efb98d222d57bafbbc Mon Sep 17 00:00:00 2001
From: Anuja More <amore@redhat.com>
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 <amore@redhat.com>
Reviewed-By: Rafael Guterres Jeffman <rjeffman@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
---
.../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"
+ <Directory "/var/www/html/mywebapp">
+ Allow from all
+ </Directory>
+ <Location "/mywebapp">
+ LogLevel debug
+ AuthType GSSAPI
+ AuthName "IPA Kerberos authentication"
+ GssapiNegotiateOnce on
+ GssapiBasicAuthMech krb5
+ GssapiCredStore keytab:{keytab_path}
+ <RequireAll>
+ 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}$/
+ </RequireAll>
+ </Location>
+ ''')
+
+ 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

File diff suppressed because it is too large Load Diff

View File

@ -1,543 +0,0 @@
From 9a8553fc297987b006b28ade2098d613f9b5360f Mon Sep 17 00:00:00 2001
From: Jay Gondaliya <jgondali@redhat.com>
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_001getkeytab_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 <jgondali@redhat.com>
Fixes: https://pagure.io/freeipa/issue/9957
Assisted-by: Claude <noreply@anthropic.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
---
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 <enctype> 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

View File

@ -1,285 +0,0 @@
From b6d50197882dd62b5416948cb232c77c0349c0d3 Mon Sep 17 00:00:00 2001
From: Sudhir Menon <sumenon@redhat.com>
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 <noreply@anthropic.com>
Signed-off-by: Sudhir Menon <sumenon@redhat.com>
Fixes: https://pagure.io/freeipa/issue/9964
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
Reviewed-By: Sudhir Menon <sumenon@redhat.com>
---
.../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 <cert> 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

View File

@ -1,288 +0,0 @@
From 8f2bbb02ae64f322fb34f073d7a7cdb00a69aea0 Mon Sep 17 00:00:00 2001
From: Sudhir Menon <sumenon@redhat.com>
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 <noreply@anthropic.com>
Signed-off-by: Sudhir Menon <sumenon@redhat.com>
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
---
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

View File

@ -1,506 +0,0 @@
From 51e669d46f912d39841bc40560682f9f414f68fe Mon Sep 17 00:00:00 2001
From: Jay Gondaliya <jgondali@redhat.com>
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 <noreply@anthropic.com>
Signed-off-by: Jay Gondaliya <jgondali@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
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

View File

@ -1,506 +0,0 @@
From c294159878de52fa5762025ee4be893f280c5320 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
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 <jgondali@redhat.com>
Fixes: https://pagure.io/freeipa/issue/9945
Assisted-by: Claude <noreply@anthropic.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Reviewed-By: Rafael Guterres Jeffman <rjeffman@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
.../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

View File

@ -1,36 +0,0 @@
From 0306d2c5ebb1732fb33f3da4199f29ec887e1db8 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
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 <flo@redhat.com>
Reviewed-By: Anuja More <amore@redhat.com>
---
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

View File

@ -1,48 +0,0 @@
From a1b1a45c45f521659ee25709141b6470599030d0 Mon Sep 17 00:00:00 2001
From: David Hanina <dhanina@redhat.com>
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 <dhanina@redhat.com>
---
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

View File

@ -1,28 +0,0 @@
From 86a6e9cef5a3fdea3cb84c39575c90481bfe1623 Mon Sep 17 00:00:00 2001
From: David Hanina <dhanina@redhat.com>
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 <dhanina@redhat.com>
---
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

View File

@ -1,49 +0,0 @@
From 3d5fe37d41332cdca486c9ca42089934e5ed7f89 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
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 <flo@redhat.com>
Reviewed-By: Sudhir Menon <sumenon@redhat.com>
---
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

View File

@ -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 <dhanina@redhat.com> - 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 <flo@redhat.com> - 4.13.1-4
- Related: RHEL-175124 Rebuilt for samba 4.24.3

View File

@ -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