ipa-4.13.1-3.1

- Resolves: RHEL-166865 Include latest fixes in python3-ipatests package [rhel-9.8.z]
- Resolves: RHEL-155037 Pagure #9953: Adding a group with 32Bit Idrange fails. [rhel-9.8.z]
- Resolves: RHEL-153146 IdM password policy Min lifetime is not enforced when high minlife is set [rhel-9.8.z]
- Resolves: RHEL-168047 ipa ca-show ipa --all failing to list RSN version

Signed-off-by: David Hanina <dhanina@redhat.com>
This commit is contained in:
David Hanina 2026-04-14 14:53:38 +02:00
parent 33a8859bee
commit 287858bc9e
24 changed files with 9745 additions and 1 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,154 @@
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

@ -0,0 +1,42 @@
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

@ -0,0 +1,36 @@
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

@ -0,0 +1,42 @@
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

@ -0,0 +1,154 @@
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

@ -0,0 +1,37 @@
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

@ -0,0 +1,400 @@
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

@ -0,0 +1,138 @@
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

@ -0,0 +1,217 @@
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

@ -0,0 +1,229 @@
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

@ -0,0 +1,421 @@
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

@ -0,0 +1,260 @@
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

@ -0,0 +1,543 @@
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

@ -0,0 +1,285 @@
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

@ -0,0 +1,288 @@
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

@ -0,0 +1,506 @@
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

@ -0,0 +1,506 @@
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

@ -0,0 +1,36 @@
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

@ -0,0 +1,48 @@
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

@ -0,0 +1,28 @@
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

@ -229,7 +229,7 @@
Name: %{package_name}
Version: %{IPA_VERSION}
Release: 3%{?rc_version:.%rc_version}%{?dist}
Release: 3%{?rc_version:.%rc_version}%{?dist}.1
Summary: The Identity, Policy and Audit system
License: GPL-3.0-or-later
@ -267,6 +267,29 @@ 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
Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch
%endif
%endif
@ -1979,6 +2002,12 @@ fi
%endif
%changelog
* Mon Apr 13 2026 David Hanina <dhanina@redhat.com> - 4.13.1-3.1
- Resolves: RHEL-166865 Include latest fixes in python3-ipatests package [rhel-9.8.z]
- Resolves: RHEL-155037 Pagure #9953: Adding a group with 32Bit Idrange fails. [rhel-9.8.z]
- Resolves: RHEL-153146 IdM password policy Min lifetime is not enforced when high minlife is set [rhel-9.8.z]
- Resolves: RHEL-168047 ipa ca-show ipa --all failing to list RSN version
* Tue Feb 10 2026 Florence Blanc-Renaud <flo@redhat.com> - 4.13.1-3
- RHEL-148282 ipa-replica-conncheck fails with "an internal error has occured"
- RHEL-148481 Pruning is enabled by default with RSN on RHEL 9.8