ipa-4.12.2-25

- Resolves: RHEL-128238
[RFE] Support storing LWCA private keys on an HSM [rhel-9]
- Resolves: RHEL-126515
RFE: Enable external password reset agents to use ipa_pwd_extop in RHEL IdM [rhel-9]
- Resolves: RHEL-73399
RFE: Update IdM password policy configurations to meet M-22-09 by restricting spaces and require number character class
- Resolves: RHEL-128241
ATTR_NAME_BY_OID is missing OID 2.5.4.97, organizationIdentifier [rhel-9]
- Resolves: RHEL-126514
[RFE] ipa-client-automount should have an option to include domain of the machine. [rhel-9]
- Resolves: RHEL-124171
Include latest fixes in python3-ipatests package
- Resolves: RHEL-120514
Include fixes in python3-ipatests [rhel-9.8]
- Resolves: RHEL-118609
test_cacert_manage fails due to expired Let's Encrypt R3 certificate
This commit is contained in:
Florence Blanc-Renaud 2025-11-19 17:18:01 +01:00
parent 6d0d7136f5
commit 445b79154e
26 changed files with 8544 additions and 1 deletions

View File

@ -0,0 +1,97 @@
From b394ac6f7ad62d021412d34364a22ea0dc5a6362 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Tue, 6 May 2025 09:52:35 -0400
Subject: [PATCH] Add --domain option to ipa-client-automount for DNS discovery
If the client machine is not in the IPA DNS domain then discovery
will not find a server. Add a --domain option so that the set of
servers can be discovered.
Note that --domain is initialized to "" rather than None to match
the behavior in ipa-client-install.
Fixes: https://pagure.io/freeipa/issue/9780
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
client/man/ipa-client-automount.1 | 3 +++
ipaclient/install/ipa_client_automount.py | 8 +++++++-
ipatests/test_integration/test_nfs.py | 24 +++++++++++++++++++++++
3 files changed, 34 insertions(+), 1 deletion(-)
diff --git a/client/man/ipa-client-automount.1 b/client/man/ipa-client-automount.1
index d55ca994d1a8c413134a3031918bc88ce431adb1..1a061e84b7e499c573d2efc9377a5edf0b7b5f0e 100644
--- a/client/man/ipa-client-automount.1
+++ b/client/man/ipa-client-automount.1
@@ -49,6 +49,9 @@ NFSv4 is also configured. The rpc.gssd and rpc.idmapd are started on clients to
\fB\-\-server\fR=\fISERVER\fR
Set the FQDN of the IPA server to connect to.
.TP
+\fB\-\-domain\fR=\fIDOMAIN\fR
+Primary DNS domain of the IPA deployment to be used for server discovery.
+.TP
\fB\-\-location\fR=\fILOCATION\fR
Automount location.
.TP
diff --git a/ipaclient/install/ipa_client_automount.py b/ipaclient/install/ipa_client_automount.py
index 9f49ff9edeee2648d2be1dea6b09806ba0b5e041..035eefb1c85a33534fdd2640c8f329aa6fb212bf 100644
--- a/ipaclient/install/ipa_client_automount.py
+++ b/ipaclient/install/ipa_client_automount.py
@@ -61,6 +61,12 @@ def parse_options():
usage = "%prog [options]\n"
parser = IPAOptionParser(usage=usage)
parser.add_option("--server", dest="server", help="FQDN of IPA server")
+ parser.add_option(
+ "--domain",
+ dest="domain",
+ default="",
+ help="Primary DNS domain of the IPA deployment"
+ )
parser.add_option(
"--location",
dest="location",
@@ -387,7 +393,7 @@ def configure_automount():
ds = discovery.IPADiscovery()
if not options.server:
print("Searching for IPA server...")
- ret = ds.search(ca_cert_path=ca_cert_path)
+ ret = ds.search(domain=options.domain, ca_cert_path=ca_cert_path)
logger.debug('Executing DNS discovery')
if ret == discovery.NO_LDAP_SERVER:
logger.debug('Autodiscovery did not find LDAP server')
diff --git a/ipatests/test_integration/test_nfs.py b/ipatests/test_integration/test_nfs.py
index 68cba2988d2c2bd95a60846b0ae999b76fc4c289..32d107b718aecd0944eaeb790af201c17b0ab56a 100644
--- a/ipatests/test_integration/test_nfs.py
+++ b/ipatests/test_integration/test_nfs.py
@@ -355,3 +355,27 @@ class TestIpaClientAutomountFileRestore(IntegrationTest):
def test_nsswitch_backup_restore_sssd(self):
self.nsswitch_backup_restore()
+
+
+class TestIpaClientAutomountDiscovery(IntegrationTest):
+
+ num_clients = 1
+ topology = 'line'
+
+ def test_automount_invalid_domain(self):
+ """Validate that the --domain option is passed into
+ Discovery. This is expected to fail discovery.
+ """
+ testdomain = "client.test"
+ msg1 = f"Search for LDAP SRV record in {testdomain}"
+ msg2 = f"Search DNS for SRV record of _ldap._tcp.{testdomain}"
+ msg3 = "Autodiscovery did not find LDAP server"
+
+ client = self.clients[0]
+ result = client.run_command([
+ 'ipa-client-automount', '--domain', 'client.test',
+ '--debug'
+ ], stdin_text="n", raiseonerr=False)
+ assert msg1 in result.stderr_text
+ assert msg2 in result.stderr_text
+ assert msg3 in result.stderr_text
--
2.51.1

View File

@ -0,0 +1,378 @@
From 1f915d4b2a1793a0a3e536604643f80aa76b6b0c Mon Sep 17 00:00:00 2001
From: Anuja More <amore@redhat.com>
Date: Fri, 23 Aug 2024 15:08:57 +0530
Subject: [PATCH] ipatests: Update ipatests to test topology with multiple
domain.
Added changes in ipatests so that ipa server-replica-client
can be installed with two domain - ipa.test and trustedipa.test
Related: https://pagure.io/freeipa/issue/9657
Signed-off-by: Anuja More <amore@redhat.com>
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
ipatests/pytest_ipa/integration/__init__.py | 34 ++++++++-
ipatests/pytest_ipa/integration/config.py | 30 +++++++-
ipatests/test_integration/base.py | 71 +++++++++++++++++++
.../test_integration/test_multidomain_ipa.py | 54 ++++++++++++++
pylint_plugins.py | 12 ++++
5 files changed, 198 insertions(+), 3 deletions(-)
create mode 100644 ipatests/test_integration/test_multidomain_ipa.py
diff --git a/ipatests/pytest_ipa/integration/__init__.py b/ipatests/pytest_ipa/integration/__init__.py
index eb032cd72d2aa2a5ed4c476e3cb04dc77f607eaa..31e909160a0b34b5bb2dd418a20677e4883d318d 100644
--- a/ipatests/pytest_ipa/integration/__init__.py
+++ b/ipatests/pytest_ipa/integration/__init__.py
@@ -415,6 +415,16 @@ def mh(request, class_integration_logs):
'type': 'AD_TREEDOMAIN',
'hosts': {'ad_treedomain': 1}
})
+ for _i in range(cls.num_trusted_domains):
+ domain_descriptions.append({
+ 'type': 'TRUSTED_IPA',
+ 'hosts':
+ {
+ 'master': 1,
+ 'replica': cls.num_trusted_replicas,
+ 'client': cls.num_trusted_clients,
+ }
+ })
mh = make_multihost_fixture(
request,
@@ -423,10 +433,20 @@ def mh(request, class_integration_logs):
_config=get_global_config(),
)
- mh.domain = mh.config.domains[0]
+ for domain in mh.config.domains:
+ if domain.type == 'IPA':
+ mh.domain = domain
+ elif domain.type == 'TRUSTED_IPA':
+ mh.trusted_domain = domain
+
[mh.master] = mh.domain.hosts_by_role('master')
mh.replicas = mh.domain.hosts_by_role('replica')
mh.clients = mh.domain.hosts_by_role('client')
+ if mh.config.trusted_domains:
+ [mh.trusted_master] = mh.trusted_domain.hosts_by_role('master')
+ mh.trusted_replicas = mh.trusted_domain.hosts_by_role('replica')
+ mh.trusted_clients = mh.trusted_domain.hosts_by_role('client')
+
ad_domains = mh.config.ad_domains
if ad_domains:
mh.ads = []
@@ -489,6 +509,12 @@ def add_compat_attrs(cls, mh):
cls.ad_subdomains = mh.ad_subdomains
cls.ad_treedomains = mh.ad_treedomains
+ cls.trusted_domains = mh.config.trusted_domains
+ if cls.trusted_domains:
+ cls.trusted_master = mh.trusted_master
+ cls.trusted_replicas = mh.trusted_replicas
+ cls.trusted_clients = mh.trusted_clients
+
def del_compat_attrs(cls):
"""Remove convenience attributes from the test class
@@ -506,6 +532,12 @@ def del_compat_attrs(cls):
del cls.ad_treedomains
del cls.ad_domains
+ if cls.trusted_domains:
+ del cls.trusted_master
+ del cls.trusted_replicas
+ del cls.trusted_clients
+ del cls.trusted_domains
+
def skip_if_fips(reason='Not supported in FIPS mode', host='master'):
if callable(reason):
diff --git a/ipatests/pytest_ipa/integration/config.py b/ipatests/pytest_ipa/integration/config.py
index 1f4dff7f6526953ea8a98604aabd90d80a3c3403..0fb2313db0aa739a1a57f00bd6a0c6675f1616a6 100644
--- a/ipatests/pytest_ipa/integration/config.py
+++ b/ipatests/pytest_ipa/integration/config.py
@@ -88,13 +88,19 @@ class Config(pytest_multihost.config.Config):
def ad_domains(self):
return [d for d in self.domains if d.is_ad_type]
+ @property
+ def trusted_domains(self):
+ return [d for d in self.domains if d.is_trusted_ipa_type]
+
def get_all_hosts(self):
for domain in self.domains:
for host in domain.hosts:
yield host
def get_all_ipa_hosts(self):
- for ipa_domain in (d for d in self.domains if d.is_ipa_type):
+ for ipa_domain in (d for d in self.domains
+ if d.is_ipa_type or d.is_trusted_ipa_type
+ ):
for ipa_host in ipa_domain.hosts:
yield ipa_host
@@ -135,7 +141,7 @@ class Domain(pytest_multihost.config.Domain):
self.name = str(name)
self.hosts = []
- assert self.is_ipa_type or self.is_ad_type
+ assert self.is_ipa_type or self.is_ad_type or self.is_trusted_ipa_type
self.realm = self.name.upper()
self.basedn = DN(*(('dc', p) for p in name.split('.')))
@@ -143,6 +149,10 @@ class Domain(pytest_multihost.config.Domain):
def is_ipa_type(self):
return self.type == 'IPA'
+ @property
+ def is_trusted_ipa_type(self):
+ return self.type == 'TRUSTED_IPA'
+
@property
def is_ad_type(self):
return self.type == 'AD' or self.type.startswith('AD_')
@@ -158,6 +168,8 @@ class Domain(pytest_multihost.config.Domain):
return ('ad_subdomain',)
elif self.type == 'AD_TREEDOMAIN':
return ('ad_treedomain',)
+ elif self.type == 'TRUSTED_IPA':
+ return ('trusted_master', 'trusted_replica', 'trusted_client')
else:
raise LookupError(self.type)
@@ -168,6 +180,8 @@ class Domain(pytest_multihost.config.Domain):
return Host
elif self.is_ad_type:
return WinHost
+ elif self.is_trusted_ipa_type:
+ return Host
else:
raise LookupError(self.type)
@@ -175,6 +189,10 @@ class Domain(pytest_multihost.config.Domain):
def master(self):
return self.host_by_role('master')
+ @property
+ def trusted_master(self):
+ return self.host_by_role('trusted_master')
+
@property
def masters(self):
return self.hosts_by_role('master')
@@ -183,10 +201,18 @@ class Domain(pytest_multihost.config.Domain):
def replicas(self):
return self.hosts_by_role('replica')
+ @property
+ def trusted_replicas(self):
+ return self.hosts_by_role('replica')
+
@property
def clients(self):
return self.hosts_by_role('client')
+ @property
+ def trusted_clients(self):
+ return self.hosts_by_role('client')
+
@property
def ads(self):
return self.hosts_by_role('ad')
diff --git a/ipatests/test_integration/base.py b/ipatests/test_integration/base.py
index 4717667cb0c48790f77064e6dc300e673d2bb58b..cb34067d4d35cd22a83b34aa265d2df7fb47d371 100644
--- a/ipatests/test_integration/base.py
+++ b/ipatests/test_integration/base.py
@@ -33,6 +33,7 @@ class IntegrationTest:
num_replicas = 0
num_clients = 0
num_ad_domains = 0
+ num_trusted_domains = 0
num_ad_subdomains = 0
num_ad_treedomains = 0
required_extra_roles = []
@@ -95,6 +96,7 @@ class IntegrationTest:
cls.clients, domain_level,
random_serial=cls.random_serial,
extra_args=extra_args,)
+
@classmethod
def uninstall(cls, mh):
for replica in cls.replicas:
@@ -112,3 +114,72 @@ class IntegrationTest:
tasks.uninstall_client(client)
if cls.fips_mode:
cls.disable_fips_mode()
+
+
+@ordered
+@pytest.mark.usefixtures('mh')
+@pytest.mark.usefixtures('integration_logs')
+class MultiDomainIntegrationTest(IntegrationTest):
+ num_trusted_domains = 1
+ num_trusted_replicas = 0
+ num_trusted_clients = 0
+
+ @classmethod
+ def get_domains(cls):
+ return super(MultiDomainIntegrationTest, cls
+ ).get_domains() + cls.trusted_domains
+
+ @classmethod
+ def install(cls, mh):
+ super(MultiDomainIntegrationTest, cls).install(mh)
+ extra_args = []
+ if cls.topology is None:
+ return
+ else:
+ if cls.token_password:
+ extra_args.extend(('--token-password', cls.token_password,))
+ tasks.install_topo(cls.topology,
+ cls.trusted_master, cls.trusted_replicas,
+ cls.trusted_clients, 1,
+ random_serial=cls.random_serial,
+ extra_args=extra_args,)
+ tasks.kinit_admin(cls.master)
+ tasks.kinit_admin(cls.trusted_master)
+ # Now enable dnssec on the zones
+ cls.master.run_command([
+ "ipa-dns-install",
+ "--dnssec-master",
+ "--forwarder", cls.master.config.dns_forwarder,
+ "-U",
+ ])
+ cls.master.run_command([
+ "ipa", "dnszone-mod", cls.master.domain.name,
+ "--dnssec=True"
+ ])
+ cls.trusted_master.run_command([
+ "ipa-dns-install",
+ "--dnssec-master",
+ "--forwarder", cls.trusted_master.config.dns_forwarder,
+ "-U",
+ ])
+ cls.trusted_master.run_command([
+ "ipa", "dnszone-mod", cls.trusted_master.domain.name,
+ "--dnssec=True"
+ ])
+
+ @classmethod
+ def uninstall(cls, mh):
+ super(MultiDomainIntegrationTest, cls).uninstall(mh)
+ for trustedreplica in cls.trusted_replicas:
+ try:
+ tasks.run_server_del(
+ cls.trusted_master, trustedreplica.hostname, force=True,
+ ignore_topology_disconnect=True, ignore_last_of_role=True)
+ except subprocess.CalledProcessError:
+ # If the master has already been uninstalled,
+ # this call may fail
+ pass
+ tasks.uninstall_master(trustedreplica)
+ tasks.uninstall_master(cls.trusted_master)
+ for client in cls.trusted_clients:
+ tasks.uninstall_client(client)
diff --git a/ipatests/test_integration/test_multidomain_ipa.py b/ipatests/test_integration/test_multidomain_ipa.py
new file mode 100644
index 0000000000000000000000000000000000000000..b1a39a072f0e936d0a7f0f100f9f8ebba729d57c
--- /dev/null
+++ b/ipatests/test_integration/test_multidomain_ipa.py
@@ -0,0 +1,54 @@
+from ipatests.pytest_ipa.integration import tasks
+from ipatests.test_integration.base import MultiDomainIntegrationTest
+
+
+class TestMultidomain(MultiDomainIntegrationTest):
+ num_clients = 1
+ num_replicas = 1
+ num_trusted_clients = 1
+ num_trusted_replicas = 1
+ topology = 'line'
+
+ def test_multidomain_trust(self):
+ """
+ Test services on multidomain topology.
+ """
+
+ for host in (self.master, self.replicas[0],
+ self.trusted_master, self.trusted_replicas[0]
+ ):
+ tasks.start_ipa_server(host)
+
+ for host in (self.master, self.trusted_master):
+ tasks.disable_dnssec_validation(host)
+ tasks.restart_named(host)
+
+ for host in (self.master, self.replicas[0],
+ self.trusted_master, self.trusted_replicas[0],
+ self.clients[0], self.trusted_clients[0]
+ ):
+ tasks.kinit_admin(host)
+
+ # Add DNS forwarder to trusted domain on ipa domain
+ self.master.run_command([
+ "ipa", "dnsforwardzone-add", self.trusted_master.domain.name,
+ "--forwarder", self.trusted_master.ip,
+ "--forward-policy=only"
+ ])
+ self.trusted_master.run_command([
+ "ipa", "dnsforwardzone-add", self.master.domain.name,
+ "--forwarder", self.master.ip,
+ "--forward-policy=only"
+ ])
+
+ tasks.install_adtrust(self.master)
+ tasks.install_adtrust(self.trusted_master)
+
+ # Establish trust
+ # self.master.run_command([
+ # "ipa", "trust-add", "--type=ipa",
+ # "--admin", "admin@{}".format(self.trusted_master.domain.realm),
+ # "--range-type=ipa-ad-trust-posix",
+ # "--password", "--two-way=true",
+ # self.trusted_master.domain.name
+ # ], stdin_text=self.trusted_master.config.admin_password)
diff --git a/pylint_plugins.py b/pylint_plugins.py
index d75da3f4aae9e7bb5083a0618c8d242ce8117f64..75d65f016fde70a783b24689c9bb2c88a7e193de 100644
--- a/pylint_plugins.py
+++ b/pylint_plugins.py
@@ -566,6 +566,7 @@ AstroidBuilder(MANAGER).string_build(
textwrap.dedent(
"""\
from ipatests.test_integration.base import IntegrationTest
+ from ipatests.test_integration.base import MultiDomainIntegrationTest
from ipatests.pytest_ipa.integration.host import Host, WinHost
from ipatests.pytest_ipa.integration.config import Config, Domain
@@ -584,6 +585,9 @@ AstroidBuilder(MANAGER).string_build(
def __getitem__(self, key):
return Domain()
+ class PylintTrustedDomains:
+ def __getitem__(self, key):
+ return Domain()
Host.config = Config()
Host.domain = Domain()
@@ -596,6 +600,14 @@ AstroidBuilder(MANAGER).string_build(
IntegrationTest.ad_treedomains = PylintWinHosts()
IntegrationTest.ad_subdomains = PylintWinHosts()
IntegrationTest.ad_domains = PylintADDomains()
+ MultiDomainIntegrationTest.domain = Domain()
+ MultiDomainIntegrationTest.master = Host()
+ MultiDomainIntegrationTest.replicas = PylintIPAHosts()
+ MultiDomainIntegrationTest.clients = PylintIPAHosts()
+ MultiDomainIntegrationTest.trusted_master = Host()
+ MultiDomainIntegrationTest.trusted_replicas = PylintIPAHosts()
+ MultiDomainIntegrationTest.trusted_clients = PylintIPAHosts()
+ MultiDomainIntegrationTest.trusted_domains = PylintTrustedDomains()
"""
)
)
--
2.51.1

View File

@ -0,0 +1,404 @@
From 902dbeb67e0574dca4c761d058b43af3ac2cef6a Mon Sep 17 00:00:00 2001
From: Anuja More <amore@redhat.com>
Date: Mon, 26 May 2025 20:27:02 +0530
Subject: [PATCH] ipatests: Add comprehensive tests for ipa-client-automount
--domain option
- Add parametrized test for domain validation covering valid/invalid formats
- Add cross-domain discovery test showing --domain enables discovery when
client is in different domain than IPA domain
- Validate configuration in sssd.conf after successful automount setup
The new tests ensure --domain option works correctly and provides proper
hints for DNS discovery in cross-domain scenarios, reducing user friction
compared to requiring --server specification.
Related: https://pagure.io/freeipa/issue/9780
Signed-off-by: Anuja More <amore@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
.../nightly_ipa-4-12_latest.yaml | 17 ++
.../nightly_ipa-4-12_latest_selinux.yaml | 18 ++
ipatests/prci_definitions/temp_commit.yaml | 4 +
ipatests/test_integration/test_nfs.py | 215 ++++++++++++++++--
ipatests/test_ipalib/test_util.py | 21 +-
5 files changed, 255 insertions(+), 20 deletions(-)
diff --git a/ipatests/prci_definitions/nightly_ipa-4-12_latest.yaml b/ipatests/prci_definitions/nightly_ipa-4-12_latest.yaml
index bc2a10de47bb136e13bf99869fc4f41101e863cb..198c6acec368f2dc11197b55066a042473b27201 100644
--- a/ipatests/prci_definitions/nightly_ipa-4-12_latest.yaml
+++ b/ipatests/prci_definitions/nightly_ipa-4-12_latest.yaml
@@ -43,6 +43,10 @@ topologies:
name: ad_master_1repl_1client
cpu: 6
memory: 12096
+ ipa_ipa_trust: &ipa_ipa_trust
+ name: ipa_ipa_trust
+ cpu: 7
+ memory: 14750
jobs:
fedora-latest-ipa-4-12/build:
@@ -59,6 +63,19 @@ jobs:
timeout: 1800
topology: *build
+ fedora-latest-ipa-4-12/nfs_automountdiscovery:
+ requires: [fedora-latest-ipa-4-12/build]
+ priority: 50
+ job:
+ class: RunPytest
+ args:
+ build_url: '{fedora-latest-ipa-4-12/build_url}'
+ test_suite: test_integration/test_nfs.py::TestIpaClientAutomountDiscovery
+ trusted_domain: True
+ template: *ci-ipa-4-12-latest
+ timeout: 3600
+ topology: *ipa_ipa_trust
+
fedora-latest-ipa-4-12/simple_replication:
requires: [fedora-latest-ipa-4-12/build]
priority: 50
diff --git a/ipatests/prci_definitions/nightly_ipa-4-12_latest_selinux.yaml b/ipatests/prci_definitions/nightly_ipa-4-12_latest_selinux.yaml
index fc31186dfa4dcf863220044a2a5881304b39e76d..8f01bb84126e7f6b2b8e79e8e45475f85a0d8469 100644
--- a/ipatests/prci_definitions/nightly_ipa-4-12_latest_selinux.yaml
+++ b/ipatests/prci_definitions/nightly_ipa-4-12_latest_selinux.yaml
@@ -43,6 +43,10 @@ topologies:
name: ad_master_1repl_1client
cpu: 6
memory: 12096
+ ipa_ipa_trust: &ipa_ipa_trust
+ name: ipa_ipa_trust
+ cpu: 7
+ memory: 14750
jobs:
fedora-latest-ipa-4-12/build:
@@ -59,6 +63,20 @@ jobs:
timeout: 1800
topology: *build
+ fedora-latest-ipa-4-12/nfs_automountdiscovery:
+ requires: [fedora-latest-ipa-4-12/build]
+ priority: 50
+ job:
+ class: RunPytest
+ args:
+ build_url: '{fedora-latest-ipa-4-12/build_url}'
+ selinux_enforcing: True
+ test_suite: test_integration/test_nfs.py::TestIpaClientAutomountDiscovery
+ trusted_domain: True
+ template: *ci-ipa-4-12-latest
+ timeout: 3600
+ topology: *ipa_ipa_trust
+
fedora-latest-ipa-4-12/simple_replication:
requires: [fedora-latest-ipa-4-12/build]
priority: 50
diff --git a/ipatests/prci_definitions/temp_commit.yaml b/ipatests/prci_definitions/temp_commit.yaml
index 24b7b4c48f31522421a7d7a099702a7e92cfbd27..036a1f495f85d6dd9a414bd7ea94446f780711e7 100644
--- a/ipatests/prci_definitions/temp_commit.yaml
+++ b/ipatests/prci_definitions/temp_commit.yaml
@@ -49,6 +49,10 @@ topologies:
name: ad_master_1repl_1client
cpu: 6
memory: 12096
+ ipa_ipa_trust: &ipa_ipa_trust
+ name: ipa_ipa_trust
+ cpu: 7
+ memory: 14750
jobs:
fedora-latest-ipa-4-12/build:
diff --git a/ipatests/test_integration/test_nfs.py b/ipatests/test_integration/test_nfs.py
index 32d107b718aecd0944eaeb790af201c17b0ab56a..49a86fc8ef7441d26a1307e52cac0f6aa8962fcf 100644
--- a/ipatests/test_integration/test_nfs.py
+++ b/ipatests/test_integration/test_nfs.py
@@ -21,15 +21,35 @@ import time
import pytest
-from ipatests.test_integration.base import IntegrationTest
+from ipaplatform.paths import paths
+from ipatests.test_integration.base import (
+ IntegrationTest, MultiDomainIntegrationTest)
from ipatests.pytest_ipa.integration import tasks
-
# give some time for units to stabilize
# otherwise we get transient errors
WAIT_AFTER_INSTALL = 5
WAIT_AFTER_UNINSTALL = WAIT_AFTER_INSTALL
+def remove_automount(host):
+ time.sleep(WAIT_AFTER_INSTALL)
+ host.run_command([
+ 'ipa-client-automount', '--uninstall', '-U'
+ ], raiseonerr=False)
+ time.sleep(WAIT_AFTER_UNINSTALL)
+
+
+def add_automount(host, extra_args, raiseonerr=False):
+ time.sleep(WAIT_AFTER_UNINSTALL)
+ args = [
+ "ipa-client-automount",
+ ]
+ args.extend(extra_args)
+ ret = host.run_command(args, raiseonerr=raiseonerr)
+ time.sleep(WAIT_AFTER_INSTALL)
+ return ret
+
+
class TestNFS(IntegrationTest):
num_clients = 3
@@ -357,25 +377,184 @@ class TestIpaClientAutomountFileRestore(IntegrationTest):
self.nsswitch_backup_restore()
-class TestIpaClientAutomountDiscovery(IntegrationTest):
+class TestIpaClientAutomountDiscovery(MultiDomainIntegrationTest):
+ num_replicas = 0
+ num_trusted_replicas = 0
num_clients = 1
- topology = 'line'
+ num_trusted_clients = 1
+ topology = "line"
+
+ def test_automount_valid_domain(self):
+ """Test that --domain option controls which domain is used
+ for DNS SRV record lookup.
- def test_automount_invalid_domain(self):
- """Validate that the --domain option is passed into
- Discovery. This is expected to fail discovery.
+ Without --domain: client searches in its local domain (fails if no
+ IPA records) With --domain: client searches in the specified
+ domain (succeeds for IPA domain)
"""
- testdomain = "client.test"
- msg1 = f"Search for LDAP SRV record in {testdomain}"
- msg2 = f"Search DNS for SRV record of _ldap._tcp.{testdomain}"
- msg3 = "Autodiscovery did not find LDAP server"
+ testdomain1 = self.master.domain.name
+ client2 = self.trusted_clients[0]
+ tasks.uninstall_client(client2)
+ client2.run_command(["ipa-client-install", "--domain", testdomain1,
+ "--realm", self.master.domain.realm,
+ "--server", self.master.hostname,
+ "-p", client2.config.admin_name, "-w",
+ client2.config.admin_password, "-U"]
+ )
+ result = add_automount(
+ client2, extra_args=['--debug', '-U']
+ )
+ msg = "Search DNS for SRV record of _ldap._tcp.{0}"
+ assert msg.format(client2.domain.name) in result.stderr_text
+ remove_automount(client2)
+ result2 = add_automount(
+ client2, extra_args=['--debug', '--domain', testdomain1, '-U']
+ )
+ assert msg.format(testdomain1) in result2.stderr_text
+ @pytest.mark.parametrize(
+ "domain_input,expected_success,test_description", [
+ ("{ipa_domain}", True, "valid IPA domain should succeed"),
+ ("client.test", False, "non-IPA domain should fail discovery"),
+ (" example.com ", True, "whitespace should be trimmed"),
+ ("EXAMPLE.COM", True, "uppercase should work"),
+
+ ])
+ def test_automount_domain_option_integration(
+ self, domain_input, expected_success, test_description):
+ """Test for --domain affects DNS discovery and system integration.
+
+ This test verifies that the --domain option actually changes the DNS
+ discovery behavior and system configuration, not just input validation.
+
+ Test cases:
+ - Valid IPA domain: Should successfully discover IPA services and
+ configure automount correctly
+ - Non-IPA domain: Should fail discovery since no IPA DNS records
+ exist in that domain
+ """
+ client = self.clients[0]
+ ipa_domain = self.master.domain.name
+
+ # Replace placeholders in domain_input
+ domain_to_test = domain_input.format(ipa_domain=ipa_domain)
+
+ if expected_success:
+ # Should succeed
+ add_automount(
+ client, extra_args=['--domain', domain_to_test,
+ '--debug', '-U']
+ )
+ # Verify configuration if successful
+ sssd_conf = client.get_file_contents(
+ paths.SSSD_CONF).decode()
+ assert "autofs_provider = ipa" in sssd_conf, \
+ "Autofs provider should be set to ipa"
+ # Clean up
+ remove_automount(client)
+ else:
+ # Should fail
+ result = client.run_command([
+ 'ipa-client-automount', '--domain', domain_to_test,
+ '--debug'
+ ], stdin_text="n", raiseonerr=False)
+ assert (
+ result.returncode != 0
+ or "Autodiscovery did not find LDAP server" in
+ result.stderr_text
+ or "Invalid domain" in result.stderr_text
+ ), f"Should have failed: {test_description}"
+
+ def test_automount_domain_option_overrides_discovery(self):
+ """Test that explicit --domain option overrides automatic discovery."""
client = self.clients[0]
- result = client.run_command([
- 'ipa-client-automount', '--domain', 'client.test',
- '--debug'
- ], stdin_text="n", raiseonerr=False)
- assert msg1 in result.stderr_text
- assert msg2 in result.stderr_text
- assert msg3 in result.stderr_text
+ ipa_domain = self.master.domain.name
+
+ # First install without domain to establish baseline
+ result_auto = add_automount(client, extra_args=['--debug', '-U'])
+ assert "Search DNS for SRV record" in result_auto.stderr_text
+ remove_automount(client)
+
+ # Now with explicit domain
+ result_explicit = add_automount(
+ client, extra_args=['--domain', ipa_domain, '--debug', '-U']
+ )
+ explicit_domain_msg = (
+ f"Using domain '{ipa_domain}'" in result_explicit.stderr_text
+ or f"Search DNS for SRV record of _ldap._tcp.{ipa_domain}"
+ in result_explicit.stderr_text
+ )
+
+ assert explicit_domain_msg, \
+ "Explicit domain should override automatic discovery"
+
+ # Final cleanup
+ remove_automount(client)
+
+ def test_automount_domain_hint_for_cross_domain_discovery(self):
+ """Test that --domain option enables discovery when client is in
+ a different domain than the IPA domain.
+ """
+ client = self.clients[0]
+ other_domain = self.trusted_master.domain.name
+ tasks.uninstall_client(client)
+ # Add DNS forwarder
+ self.master.run_command([
+ "ipa", "dnsforwardzone-add", self.trusted_master.domain.name,
+ "--forwarder", self.trusted_master.ip,
+ "--forward-policy=only"
+ ])
+ self.trusted_master.run_command([
+ "ipa", "dnsforwardzone-add", self.master.domain.name,
+ "--forwarder", self.master.ip,
+ "--forward-policy=only"
+ ])
+ # Backup original resolv.conf
+ tasks.backup_file(client, paths.RESOLV_CONF)
+
+ try:
+ # Install client in a domain other than the IPA domain
+ non_ipa_resolv_conf = f"""search {other_domain}
+nameserver {self.trusted_master.ip}
+"""
+ client.put_file_contents(paths.RESOLV_CONF, non_ipa_resolv_conf)
+
+ # Ensure client is installed for the test
+ tasks.uninstall_client(client)
+ client.run_command(["ipa-client-install", "--domain", other_domain,
+ "--realm", self.trusted_master.domain.realm,
+ "--server", self.trusted_master.hostname,
+ "-p", client.config.admin_name, "-w",
+ client.config.admin_password, "-U"]
+ )
+ # Verify DNS discovery will fail when client is in Non-IPA domain
+ # Attempt automount with --domain hint (should succeed)
+ nodomain = add_automount(
+ client, extra_args=['--debug', '-U']
+ )
+ # Verify discovery fails.
+ assert "DNS record not found" in nodomain.stderr_text
+ remove_automount(client)
+
+ # Attempt automount with --domain hint (should succeed)
+ withdomain = add_automount(
+ client, extra_args=['--domain', other_domain,
+ '--debug', '-U']
+ )
+ # Verify discovery finds a IPA server.
+ assert "DNS record found" in withdomain.stderr_text
+ ipa_discovery = (
+ f"Validated servers: {self.trusted_master.hostname}"
+ in withdomain.stderr_text
+ )
+ assert ipa_discovery, \
+ "Autodiscovery success"
+ # Verify configuration was applied correctly
+ sssd_conf = client.get_file_contents(paths.SSSD_CONF).decode()
+ assert "autofs_provider = ipa" in sssd_conf, \
+ "Autofs provider should be configured"
+ finally:
+ # Cleanup: restore original resolv.conf and uninstall
+ tasks.restore_files(client)
+ remove_automount(client)
diff --git a/ipatests/test_ipalib/test_util.py b/ipatests/test_ipalib/test_util.py
index 74a32b72c08aef6e01396b26efe6d71f570754cc..ba1b9f120d96b998c6effd55f12c7c9ba80a2565 100644
--- a/ipatests/test_ipalib/test_util.py
+++ b/ipatests/test_ipalib/test_util.py
@@ -11,8 +11,10 @@ from unittest import mock
import pytest
from ipalib.util import (
- get_pager, create_https_connection, get_proper_tls_version_span
+ get_pager, create_https_connection, get_proper_tls_version_span,
+ validate_domain_name
)
+
from ipaplatform.constants import constants
@@ -27,7 +29,7 @@ from ipaplatform.constants import constants
def test_get_pager(pager, expected_result):
with mock.patch.dict(os.environ, {'PAGER': pager}):
pager = get_pager()
- assert(pager == expected_result or pager.endswith(expected_result))
+ assert (pager == expected_result or pager.endswith(expected_result))
BASE_CTX = ssl.SSLContext(ssl.PROTOCOL_TLS)
@@ -75,3 +77,18 @@ def test_tls_version_span(minver, maxver, opt, expected):
ctx = getattr(conn, "_context")
assert ctx.options == BASE_OPT | opt
assert ctx.get_ciphers() == BASE_CTX.get_ciphers()
+
+
+@pytest.mark.parametrize("domain_input,expected_valid,description", [
+ ("invalid..domain", False, "double dots should be rejected"),
+ (".invalid.domain", False, "leading dot should be rejected"),
+ ("invalid domain with spaces", False, "spaces should be rejected"),
+ ("toolong" + "x" * 250 + ".domain", False, "overly long domain rejected"),
+ ("", False, "empty string should be rejected"),
+ ("single", False, "single label should be rejected"),
+])
+def test_validate_domain_name(domain_input, expected_valid, description):
+ """Test domain name validation logic in ipalib.util.validate_domain_name"""
+
+ with pytest.raises((ValueError, TypeError)):
+ validate_domain_name(domain_input)
--
2.51.1

View File

@ -0,0 +1,37 @@
From b923355ff04dd88b1530d0bb2e032280afc5d315 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
Date: Tue, 26 Aug 2025 09:00:48 +0200
Subject: [PATCH] ipatests: remove xfail for PKI 11.7
The test test_ca_show_error_handling is green with PKI 11.7
because the PKI regression has been fixed.
Update the xfail condition to 11.5 <= version < 11.7.
Fixes: https://pagure.io/freeipa/issue/9606
Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Alexander Bokovoy <abbra@users.noreply.github.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
ipatests/test_integration/test_cert.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/ipatests/test_integration/test_cert.py b/ipatests/test_integration/test_cert.py
index 05b20b910b249af24039a497538f96dad07162aa..84adf2ceafe013e6cfc973fb2cb650c40f36971d 100644
--- a/ipatests/test_integration/test_cert.py
+++ b/ipatests/test_integration/test_cert.py
@@ -558,8 +558,10 @@ class TestCAShowErrorHandling(IntegrationTest):
)
error_msg = 'ipa: ERROR: The certificate for ' \
'{} is not available on this server.'.format(lwca)
- bad_version = (tasks.get_pki_version(self.master)
- >= tasks.parse_version('11.5.0'))
+ pki_version = tasks.get_pki_version(self.master)
+ # The regression was introduced in 11.5 and fixed in 11.7
+ bad_version = (tasks.parse_version('11.5.0') <= pki_version
+ < tasks.parse_version('11.7.0'))
with xfail_context(bad_version,
reason="https://pagure.io/freeipa/issue/9606"):
assert error_msg in result.stderr_text
--
2.51.1

View File

@ -0,0 +1,42 @@
From 9b631f80720fe1f2492d1a30bb1c2410af5eb587 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
Date: Wed, 3 Sep 2025 14:57:52 +0200
Subject: [PATCH] ipatests: fix test_otp
The test is performing ssh from the runner to the master
but is using the external_hostname and randomly fails.
Make sure to use the configured hostname instead.
Fixes: https://pagure.io/freeipa/issue/9849
Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
ipatests/test_integration/test_otp.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ipatests/test_integration/test_otp.py b/ipatests/test_integration/test_otp.py
index 0babb45897c6107bf354477dbb0d3a805a3116f5..a4adeee69b068b0f889165882ab4ab6c5e2a97f8 100644
--- a/ipatests/test_integration/test_otp.py
+++ b/ipatests/test_integration/test_otp.py
@@ -504,7 +504,7 @@ class TestOTPToken(IntegrationTest):
)
with xfail_context(rhel_fail or fedora_fail, reason=github_ticket):
result = ssh_2fa_with_cmd(master,
- self.master.external_hostname,
+ self.master.hostname,
USER3, PASSWORD, otpvalue=otpvalue,
command="klist")
print(result.stdout_text)
@@ -552,7 +552,7 @@ class TestOTPToken(IntegrationTest):
otpvalue = totp.generate(int(time.time())).decode('ascii')
tasks.clear_sssd_cache(self.master)
result = ssh_2fa_with_cmd(master,
- self.master.external_hostname,
+ self.master.hostname,
USER4, PASSWORD, otpvalue=otpvalue,
command="klist")
print(result.stdout_text)
--
2.51.1

View File

@ -0,0 +1,115 @@
From 94493640e10547cd4aff82b017391916149822e5 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
Date: Wed, 17 Sep 2025 10:13:44 +0200
Subject: [PATCH] ipatests: update the Let's Encrypt cert chain
The test TestIPACommand::test_cacert_manage is using
Let's Encrypt chain to check the ipa-cacert-manage install
command.
The chain isrgrootx1 > r3 must be replaced with
isrgrootx1 > r12 because r3 expired Sep 15.
Fixes: https://pagure.io/freeipa/issue/9857
Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
ipatests/test_integration/test_commands.py | 63 +++++++++++-----------
1 file changed, 31 insertions(+), 32 deletions(-)
diff --git a/ipatests/test_integration/test_commands.py b/ipatests/test_integration/test_commands.py
index ad97affe62e15c68442239d669032f0c84e7f5c9..fcf347ee068729d1b28d215b242569f02e9a549c 100644
--- a/ipatests/test_integration/test_commands.py
+++ b/ipatests/test_integration/test_commands.py
@@ -88,41 +88,40 @@ isrgrootx1 = (
)
isrgrootx1_nick = 'CN=ISRG Root X1,O=Internet Security Research Group,C=US'
-# This sub-CA expires on Sep 15, 2025 and will need to be replaced
+# This sub-CA expires on March 12, 2027 and will need to be replaced
# after this date. Otherwise TestIPACommand::test_cacert_manage fails.
-letsencryptauthorityr3 = (
+letsencryptauthorityr12 = (
b'-----BEGIN CERTIFICATE-----\n'
- b'MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw\n'
+ b'MIIFBjCCAu6gAwIBAgIRAMISMktwqbSRcdxA9+KFJjwwDQYJKoZIhvcNAQELBQAw\n'
b'TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n'
- b'cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw\n'
- b'WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg\n'
- b'RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\n'
- b'AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP\n'
- b'R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx\n'
- b'sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm\n'
- b'NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg\n'
- b'Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG\n'
- b'/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC\n'
- b'AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB\n'
- b'Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA\n'
- b'FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw\n'
- b'AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw\n'
- b'Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB\n'
- b'gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W\n'
- b'PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl\n'
- b'ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz\n'
- b'CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm\n'
- b'lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4\n'
- b'avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2\n'
- b'yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O\n'
- b'yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids\n'
- b'hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+\n'
- b'HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv\n'
- b'MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX\n'
- b'nLRbwHOoq7hHwg==\n'
+ b'cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw\n'
+ b'WhcNMjcwMzEyMjM1OTU5WjAzMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg\n'
+ b'RW5jcnlwdDEMMAoGA1UEAxMDUjEyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n'
+ b'CgKCAQEA2pgodK2+lP474B7i5Ut1qywSf+2nAzJ+Npfs6DGPpRONC5kuHs0BUT1M\n'
+ b'5ShuCVUxqqUiXXL0LQfCTUA83wEjuXg39RplMjTmhnGdBO+ECFu9AhqZ66YBAJpz\n'
+ b'kG2Pogeg0JfT2kVhgTU9FPnEwF9q3AuWGrCf4yrqvSrWmMebcas7dA8827JgvlpL\n'
+ b'Thjp2ypzXIlhZZ7+7Tymy05v5J75AEaz/xlNKmOzjmbGGIVwx1Blbzt05UiDDwhY\n'
+ b'XS0jnV6j/ujbAKHS9OMZTfLuevYnnuXNnC2i8n+cF63vEzc50bTILEHWhsDp7CH4\n'
+ b'WRt/uTp8n1wBnWIEwii9Cq08yhDsGwIDAQABo4H4MIH1MA4GA1UdDwEB/wQEAwIB\n'
+ b'hjAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwEgYDVR0TAQH/BAgwBgEB\n'
+ b'/wIBADAdBgNVHQ4EFgQUALUp8i2ObzHom0yteD763OkM0dIwHwYDVR0jBBgwFoAU\n'
+ b'ebRZ5nu25eQBc4AIiMgaWPbpm24wMgYIKwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAC\n'
+ b'hhZodHRwOi8veDEuaS5sZW5jci5vcmcvMBMGA1UdIAQMMAowCAYGZ4EMAQIBMCcG\n'
+ b'A1UdHwQgMB4wHKAaoBiGFmh0dHA6Ly94MS5jLmxlbmNyLm9yZy8wDQYJKoZIhvcN\n'
+ b'AQELBQADggIBAI910AnPanZIZTKS3rVEyIV29BWEjAK/duuz8eL5boSoVpHhkkv3\n'
+ b'4eoAeEiPdZLj5EZ7G2ArIK+gzhTlRQ1q4FKGpPPaFBSpqV/xbUb5UlAXQOnkHn3m\n'
+ b'FVj+qYv87/WeY+Bm4sN3Ox8BhyaU7UAQ3LeZ7N1X01xxQe4wIAAE3JVLUCiHmZL+\n'
+ b'qoCUtgYIFPgcg350QMUIWgxPXNGEncT921ne7nluI02V8pLUmClqXOsCwULw+PVO\n'
+ b'ZCB7qOMxxMBoCUeL2Ll4oMpOSr5pJCpLN3tRA2s6P1KLs9TSrVhOk+7LX28NMUlI\n'
+ b'usQ/nxLJID0RhAeFtPjyOCOscQBA53+NRjSCak7P4A5jX7ppmkcJECL+S0i3kXVU\n'
+ b'y5Me5BbrU8973jZNv/ax6+ZK6TM8jWmimL6of6OrX7ZU6E2WqazzsFrLG3o2kySb\n'
+ b'zlhSgJ81Cl4tv3SbYiYXnJExKQvzf83DYotox3f0fwv7xln1A2ZLplCb0O+l/AK0\n'
+ b'YE0DS2FPxSAHi0iwMfW2nNHJrXcY3LLHD77gRgje4Eveubi2xxa+Nmk/hmhLdIET\n'
+ b'iVDFanoCrMVIpQ59XWHkzdFmoHXHBV7oibVjGSO7ULSQ7MJ1Nz51phuDJSgAIU7A\n'
+ b'0zrLnOrAj/dfrlEWRhCvAgbuwLZX1A2sjNjXoPOHbsPiy+lO1KF8/XY7\n'
b'-----END CERTIFICATE-----\n'
)
-le_r3_nick = "CN=R3,O=Let's Encrypt,C=US"
+le_r12_nick = "CN=R12,O=Let's Encrypt,C=US"
# Certificates for reproducing duplicate ipaCertSubject values.
# The trick to creating the second intermediate is for the validity
@@ -1230,7 +1229,7 @@ class TestIPACommand(IntegrationTest):
result.stderr_text
# Install 3rd party CA's, Let's Encrypt in this case
- for cert in (isrgrootx1, letsencryptauthorityr3):
+ for cert in (isrgrootx1, letsencryptauthorityr12):
certfile = os.path.join(self.master.config.test_dir, 'cert.pem')
self.master.put_file_contents(certfile, cert)
result = self.master.run_command(
@@ -1257,7 +1256,7 @@ class TestIPACommand(IntegrationTest):
# deletion of a subca
result = self.master.run_command(
- ['ipa-cacert-manage', 'delete', le_r3_nick],
+ ['ipa-cacert-manage', 'delete', le_r12_nick],
raiseonerr=False
)
assert result.returncode == 0
--
2.51.1

View File

@ -0,0 +1,35 @@
From 325108c7134db2a4ea631ee4b31fc2e1b70580ff Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
Date: Fri, 19 Sep 2025 16:31:36 +0200
Subject: [PATCH] ipatests: fix TestIPAMigratewithBackupRestore setup
The test is installing a first master with DNS enabled, then a
second master (same domain name, with DNS enabled) in order to
perform migration.
Add --allow-zone-overlap to the 2nd master installation.
Fixes: https://pagure.io/freeipa/issue/9858
Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Sudhir Menon <sumenon@redhat.com>
---
ipatests/test_integration/test_ipa_ipa_migration.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/ipatests/test_integration/test_ipa_ipa_migration.py b/ipatests/test_integration/test_ipa_ipa_migration.py
index c6247e772b257748aa0c0f58bd04b53d3756125c..d076510f90aa65d37cfe72b52e915504155aa2e4 100644
--- a/ipatests/test_integration/test_ipa_ipa_migration.py
+++ b/ipatests/test_integration/test_ipa_ipa_migration.py
@@ -1283,7 +1283,8 @@ class TestIPAMigratewithBackupRestore(IntegrationTest):
def install(cls, mh):
tasks.install_master(cls.master, setup_dns=True, setup_kra=True)
prepare_ipa_server(cls.master)
- tasks.install_master(cls.replicas[0], setup_dns=True, setup_kra=True)
+ tasks.install_master(cls.replicas[0], setup_dns=True, setup_kra=True,
+ extra_args=['--allow-zone-overlap'])
tasks.install_replica(cls.master, cls.replicas[1],
setup_dns=True, setup_kra=True)
--
2.51.1

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,376 @@
From 13ab328e519ba3e8e22bcade7680bc060a11d4a1 Mon Sep 17 00:00:00 2001
From: Anuja More <amore@redhat.com>
Date: Tue, 29 Jul 2025 20:46:37 +0530
Subject: [PATCH] ipatests: Refactor and port trust functional HBAC tests.
- Tests to cover both root domain and subdomain users:
- Test that adding AD users/groups without the external group to
HBAC rules fails.
- Test HBAC rule denies SSH access for AD users.
- Test HBAC rule allows SSH access for AD users in external group.
- Test HBAC rule denies sudo access for AD users when rule doesn't
include them.
- Test HBAC rule allows sudo access for AD users in external group.
Related : https://pagure.io/freeipa/issue/9845
Signed-off-by: Anuja More <amore@redhat.com>
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
---
ipatests/test_integration/test_trust.py | 11 +-
.../test_integration/test_trust_functional.py | 306 ++++++++++++++++++
2 files changed, 315 insertions(+), 2 deletions(-)
create mode 100644 ipatests/test_integration/test_trust_functional.py
diff --git a/ipatests/test_integration/test_trust.py b/ipatests/test_integration/test_trust.py
index 4086cb30ac5d52ee595c1ecdbe86a8d511cbb704..7bb74e2f5821719ffe2ceaf2bdcd8e7d46a6cd1f 100644
--- a/ipatests/test_integration/test_trust.py
+++ b/ipatests/test_integration/test_trust.py
@@ -66,6 +66,10 @@ class BaseTestTrust(IntegrationTest):
if cls.num_ad_subdomains > 0:
cls.child_ad = cls.ad_subdomains[0]
cls.ad_subdomain = cls.child_ad.domain.name
+ cls.subaduser = f"subdomaintestuser@{cls.ad_subdomain}"
+ cls.subaduser2 = f"subdomaindisabledadu@{cls.ad_subdomain}"
+ cls.ad_sub_group = f"subdomaintestgroup@{cls.ad_subdomain}"
+
if cls.num_ad_treedomains > 0:
cls.tree_ad = cls.ad_treedomains[0]
cls.ad_treedomain = cls.tree_ad.domain.name
@@ -74,6 +78,9 @@ class BaseTestTrust(IntegrationTest):
cls.srv_gc_record_name = \
'_ldap._tcp.Default-First-Site-Name._sites.gc._msdcs'
cls.srv_gc_record_value = '0 100 389 {}.'.format(cls.master.hostname)
+ cls.aduser = f"nonposixuser@{cls.ad_domain}"
+ cls.aduser2 = f"nonposixuser1@{cls.ad_domain}"
+ cls.ad_group = f"testgroup@{cls.ad_domain}"
@classmethod
def check_sid_generation(cls):
@@ -1303,8 +1310,8 @@ class TestPosixAutoPrivateGroup(BaseTestTrust):
self.gid_override):
self.mod_idrange_auto_private_group(type)
(uid, gid) = self.get_user_id(self.clients[0], posixuser)
- assert(uid == self.uid_override
- and gid == self.gid_override)
+ 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 = sssd_version >= tasks.parse_version("2.9.4")
diff --git a/ipatests/test_integration/test_trust_functional.py b/ipatests/test_integration/test_trust_functional.py
new file mode 100644
index 0000000000000000000000000000000000000000..b40bf9675ed5cddaf51624417356ddab28e870a5
--- /dev/null
+++ b/ipatests/test_integration/test_trust_functional.py
@@ -0,0 +1,306 @@
+# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
+
+from __future__ import absolute_import
+
+from ipaplatform.paths import paths
+from ipatests.pytest_ipa.integration import tasks
+from ipatests.test_integration.test_trust import BaseTestTrust
+
+
+class TestTrustFunctionalHbac(BaseTestTrust):
+ topology = 'line'
+ num_ad_treedomains = 0
+
+ def _add_hbacrule_with_service(self, rule_name, service_name):
+ self.master.run_command(
+ ["ipa", "hbacrule-add", rule_name, "--hostcat=all"]
+ )
+ self.master.run_command(
+ [
+ "ipa",
+ "hbacrule-add-service",
+ rule_name,
+ f"--hbacsvcs={service_name}",
+ ]
+ )
+
+ def _disable_allow_all_and_wait(self):
+ tasks.kinit_admin(self.master)
+ self.master.run_command(["ipa", "hbacrule-disable", "allow_all"])
+ tasks.wait_for_sssd_domain_status_online(self.master)
+ tasks.wait_for_sssd_domain_status_online(self.clients[0])
+
+ def _cleanup_hrule_allow_all_and_wait(self, hrule):
+ tasks.kinit_admin(self.master)
+ self.master.run_command(["ipa", "hbacrule-del", hrule])
+ self.master.run_command(["ipa", "hbacrule-enable", "allow_all"])
+ tasks.wait_for_sssd_domain_status_online(self.master)
+ tasks.wait_for_sssd_domain_status_online(self.clients[0])
+
+ def _ssh_with_password(
+ self,
+ login,
+ host,
+ password,
+ success_expected=False
+ ):
+ result = self.clients[0].run_command(
+ ['sshpass', '-p', password,
+ 'ssh', '-v', '-o', 'StrictHostKeyChecking=no',
+ '-l', login, host, "id"],
+ raiseonerr=success_expected
+ )
+ output = f"{result.stdout_text}{result.stderr_text}"
+ return output
+
+ def _get_log_tail(self, host, log_path, start_offset):
+ return host.get_file_contents(log_path)[start_offset:]
+
+ def test_setup(self):
+ tasks.configure_dns_for_trust(self.master, self.ad)
+ tasks.establish_trust_with_ad(
+ self.master, self.ad_domain,
+ extra_args=['--range-type', 'ipa-ad-trust'])
+ tasks.kdestroy_all(self.master)
+ tasks.kinit_admin(self.master)
+ tasks.group_add(
+ self.master,
+ groupname="hbacgroup_external",
+ extra_args=["--external"],
+ )
+ tasks.group_add(self.master, groupname="hbacgroup")
+ tasks.group_add_member(
+ self.master,
+ groupname="hbacgroup",
+ extra_args=['--groups=hbacgroup_external'],
+ )
+ self.master.run_command([
+ 'ipa', '-n', 'group-add-member', '--external',
+ self.aduser, 'hbacgroup_external',
+ ])
+ self.master.run_command([
+ 'ipa', '-n', 'group-add-member', '--external',
+ self.subaduser, 'hbacgroup_external',
+ ])
+
+ def test_ipa_trust_func_hbac_0001(self):
+ """
+ Test that adding AD users/groups without the external group to
+ HBAC rules fails.
+
+ This test verifies that when attempting to add AD users or
+ groups directly to HABC rules, the operation fails with
+ a "no such entry" error.
+ """
+ hrule = "hbacrule_hbac_0001"
+ tasks.kinit_admin(self.master)
+ try:
+ self._add_hbacrule_with_service(hrule, 'sudo')
+ for arg in [
+ f"--users={self.aduser}", f"--users={self.subaduser}",
+ f"--groups={self.ad_group}", f"--groups={self.ad_sub_group}"
+ ]:
+ result = self.master.run_command(
+ ["ipa", "hbacrule-add-user", hrule, arg],
+ raiseonerr=False
+ )
+ output = f"{result.stdout_text}{result.stderr_text}"
+ assert result.returncode != 0
+ assert "no such entry" in output
+ finally:
+ self._cleanup_hrule_allow_all_and_wait(hrule)
+
+ def test_ipa_trust_func_hbac_0002(self):
+ """
+ Test HBAC rule denies SSH access for AD users.
+
+ This test creates an HBAC rule that allows SSH access only for admin
+ users/groups, then verifies that AD users from the trusted domain are
+ denied access. The test confirms that the denial is logged with
+ "Access denied by HBAC rules" message.
+ """
+ hrule = "hbacrule_hbac_0002"
+ tasks.kinit_admin(self.master)
+ log_file = '{0}/sssd_{1}.log'.format(
+ paths.VAR_LOG_SSSD_DIR, self.master.domain.name)
+ try:
+ self._add_hbacrule_with_service(hrule, 'sshd')
+ self.master.run_command(
+ ['ipa', 'hbacrule-add-user', hrule,
+ '--users=admin', '--groups=admins'
+ ]
+ )
+ self._disable_allow_all_and_wait()
+ for user in [self.aduser, self.subaduser]:
+ logsize = tasks.get_logsize(
+ self.clients[0], log_file
+ )
+ self._ssh_with_password(
+ user,
+ self.clients[0].hostname,
+ 'Secret123',
+ success_expected=False,
+ )
+ sssd_logs = self._get_log_tail(
+ self.clients[0], log_file, logsize
+ )
+ assert b"Access denied by HBAC rules" in sssd_logs
+ finally:
+ self._cleanup_hrule_allow_all_and_wait(hrule)
+
+ def test_ipa_trust_func_hbac_0005(self):
+ """
+ Test HBAC rule allows SSH access for AD users in external group.
+
+ This test creates an HBAC rule that allows SSH access for members of
+ the hbacgroup (which includes AD users via external group membership).
+ It verifies that AD users who are members can successfully SSH, while
+ AD users who are not members are denied access.
+ """
+ hrule = "hbacrule_hbac_0005"
+ tasks.kinit_admin(self.master)
+ try:
+ self._add_hbacrule_with_service(hrule, 'sshd')
+ self.master.run_command(
+ [
+ "ipa",
+ "hbacrule-add-user",
+ hrule,
+ "--groups=hbacgroup",
+ ]
+ )
+ self._disable_allow_all_and_wait()
+ tasks.kinit_admin(self.clients[0])
+ for user in [self.aduser, self.subaduser]:
+ tasks.kinit_admin(self.clients[0])
+ self.clients[0].run_command(
+ ["ipa", "hbactest", f"--user={user}", "--service=sshd",
+ f"--host={self.clients[0].hostname}",
+ ]
+ )
+ tasks.kdestroy_all(self.clients[0])
+ output = self._ssh_with_password(
+ user,
+ self.clients[0].hostname,
+ 'Secret123',
+ success_expected=True,
+ )
+ assert "domain users" in output
+
+ for user2 in [self.aduser2, self.subaduser2]:
+ self._ssh_with_password(
+ user2,
+ self.clients[0].hostname,
+ 'Secret123',
+ success_expected=False,
+ )
+ finally:
+ self._cleanup_hrule_allow_all_and_wait(hrule)
+
+ def test_ipa_trust_func_hbac_0008(self):
+ """
+ Test HBAC rule denies sudo access for AD users when rule doesn't
+ include them.
+
+ This test creates an HBAC rule for sudo service that only allows
+ admin users, and a sudo rule that allows admin users to run all
+ commands. It then verifies that AD users are denied sudo access
+ due to HBAC restrictions, with the denial being logged as
+ "user NOT authorized on host".
+ """
+ hrule = "hbacrule_hbac_0008"
+ srule = "sudorule_hbac_0008"
+ tasks.kinit_admin(self.master)
+ try:
+ self._add_hbacrule_with_service(hrule, 'sudo')
+ self.master.run_command(
+ ["ipa", "hbacrule-add-user", hrule, "--users=admin",
+ "--groups=admins"]
+ )
+ self.master.run_command(
+ [
+ "ipa",
+ "sudorule-add",
+ srule,
+ "--hostcat=all",
+ "--cmdcat=all",
+ ]
+ )
+ self.master.run_command(
+ [
+ "ipa",
+ "sudorule-add-user",
+ srule,
+ "--users=admin",
+ "--groups=admins"
+ ]
+ )
+ tasks.clear_sssd_cache(self.clients[0])
+ self._disable_allow_all_and_wait()
+ tasks.kdestroy_all(self.clients[0])
+
+ for user in [self.aduser, self.subaduser]:
+ test_sudo = "su {0} -c 'sudo -S id'".format(user)
+ result = self.clients[0].run_command(
+ test_sudo,
+ stdin_text='Secret123',
+ raiseonerr=False
+ )
+ output = f"{result.stdout_text}{result.stderr_text}"
+ assert (
+ "sudo: PAM account management error: Permission denied"
+ in output
+ )
+ finally:
+ self._cleanup_hrule_allow_all_and_wait(hrule)
+ self.master.run_command(["ipa", "sudorule-del", srule])
+
+ def test_ipa_trust_func_hbac_0011(self):
+ """
+ Test HBAC rule allows sudo access for AD users in external group.
+
+ This test creates an HBAC rule for sudo service that allows members of
+ the hbacgroup (which includes AD users via external group membership),
+ and a sudo rule that allows hbacgroup members to run all commands.
+ It verifies that AD users who are members of the external group can
+ successfully use sudo and gain root privileges.
+ """
+ hrule = "ipa_trust_func_hbac_0011"
+ srule = "ipa_trust_func_hbac_0011"
+ tasks.clear_sssd_cache(self.master)
+ tasks.kinit_admin(self.master)
+ try:
+ self._add_hbacrule_with_service(hrule, 'sudo')
+
+ self.master.run_command(
+ ["ipa", "hbacrule-add-user", hrule, "--groups=hbacgroup"]
+ )
+ self.master.run_command(["ipa", "hbacrule-disable", "allow_all"])
+ self.master.run_command(
+ ["ipa", "sudorule-add", srule, "--hostcat=all", "--cmdcat=all"]
+ )
+ self.master.run_command(
+ ["ipa", "sudorule-add-user", srule, "--groups=hbacgroup"]
+ )
+ tasks.clear_sssd_cache(self.master)
+ tasks.clear_sssd_cache(self.clients[0])
+ tasks.wait_for_sssd_domain_status_online(self.master)
+ test_sudo = "su {user} -c 'sudo -S id'"
+ for user in [self.aduser, self.subaduser]:
+ with self.clients[0].spawn_expect(
+ test_sudo.format(user=user)) as e:
+ e.sendline('Secret123')
+ e.expect_exit(ignore_remaining_output=True, timeout=60)
+ output = e.get_last_output()
+ assert 'uid=0(root)' in output
+ for user in [self.aduser2, self.subaduser2]:
+ test_sudo = "su {0} -c 'sudo -S id'".format(user)
+ result = self.clients[0].run_command(
+ test_sudo,
+ stdin_text='Secret123',
+ raiseonerr=False
+ )
+ assert result.returncode != 0
+ finally:
+ self._cleanup_hrule_allow_all_and_wait(hrule)
+ self.master.run_command(["ipa", "sudorule-del", srule])
--
2.51.1

View File

@ -0,0 +1,55 @@
From 5b10d0eebffe0aaec7e7cb7974b8299905d289e9 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
Date: Mon, 2 Jun 2025 15:03:40 +0200
Subject: [PATCH] ipatests: skip encrypted dns tests on fedora 41
The package ipa-server-encrypted-dns is not available on fedora 41
as it requires a more recent bind version.
Skip the tests that require this package in f41.
Fixes: https://pagure.io/freeipa/issue/9799
Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
ipatests/test_integration/test_edns.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/ipatests/test_integration/test_edns.py b/ipatests/test_integration/test_edns.py
index dd046f226926d09074d8d6ce536999c5d452fcc4..1f843c7bcc5f8420740175ca03bcdc1ddf59ce09 100644
--- a/ipatests/test_integration/test_edns.py
+++ b/ipatests/test_integration/test_edns.py
@@ -4,15 +4,20 @@
"""This covers tests for DNS over TLS related feature"""
from __future__ import absolute_import
+import pytest
import textwrap
from ipatests.pytest_ipa.integration import tasks
from ipatests.test_integration.base import IntegrationTest
from ipatests.test_integration.test_dns import TestDNS
from ipatests.pytest_ipa.integration.firewall import Firewall
+from ipaplatform.osinfo import osinfo
from ipaplatform.paths import paths
+@pytest.mark.skipif(
+ osinfo.id == 'fedora' and osinfo.version_number == (41,),
+ reason='Encrypted DNS not supported in fedora 41')
class TestDNSOverTLS(IntegrationTest):
"""Tests for DNS over TLS feature."""
@@ -246,6 +251,9 @@ class TestDNSOverTLS(IntegrationTest):
assert '''--dns-over-tls Configure DNS over TLS''' in cmdout.stdout_text # noqa: E501
+@pytest.mark.skipif(
+ osinfo.id == 'fedora' and osinfo.version_number == (41,),
+ reason='Encrypted DNS not supported in fedora 41')
class TestDNS_DoT(TestDNS):
@classmethod
--
2.51.1

View File

@ -0,0 +1,367 @@
From 79eaa672eeed6f6eb2f92ec97150fb154e963eb4 Mon Sep 17 00:00:00 2001
From: PRANAV THUBE <pthube@redhat.com>
Date: Wed, 20 Aug 2025 15:55:55 +0530
Subject: [PATCH] Extended eDNS testsuite with Relaxed policy testcases. 1.
Relaxed policy without certs and including --no-dnssec-validation 2. Relaxed
policy with external CA and including --no-dnssec-validation
Automated with Cursor+Claude
Related: https://issues.redhat.com/browse/IDM-2894
Signed-off-by: PRANAV THUBE <pthube@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Antonio Torres <antorres@redhat.com>
---
ipatests/test_integration/test_edns.py | 315 ++++++++++++++++---------
1 file changed, 202 insertions(+), 113 deletions(-)
diff --git a/ipatests/test_integration/test_edns.py b/ipatests/test_integration/test_edns.py
index 1f843c7bcc5f8420740175ca03bcdc1ddf59ce09..6556c46ce2040ea5ce94bebe69cff06e727af64a 100644
--- a/ipatests/test_integration/test_edns.py
+++ b/ipatests/test_integration/test_edns.py
@@ -6,7 +6,9 @@
from __future__ import absolute_import
import pytest
import textwrap
-
+import os
+from ipatests.test_integration.test_caless import ExternalCA
+from cryptography.hazmat.primitives import serialization
from ipatests.pytest_ipa.integration import tasks
from ipatests.test_integration.base import IntegrationTest
from ipatests.test_integration.test_dns import TestDNS
@@ -15,6 +17,54 @@ from ipaplatform.osinfo import osinfo
from ipaplatform.paths import paths
+def verify_queries_encrypted(master, replicas, clients,
+ forwarder="1.1.1.1#853",
+ dns_hostname="freeipa.org"):
+ """
+ Helper function to verify that queries are encrypted and
+ routed to the specified forwarder.
+ """
+ unbound_log_cfg = textwrap.dedent("""
+ server:
+ verbosity: 3
+ log-queries: yes
+ cache-min-ttl: 0
+ cache-max-ttl: 0
+ """)
+
+ for server in [master] + replicas:
+ server.put_file_contents(
+ os.path.join(paths.UNBOUND_CONFIG_DIR, "log.conf"),
+ unbound_log_cfg,
+ )
+ server.run_command(["systemctl", "restart", "unbound"])
+ server.run_command(
+ ["journalctl", "--flush", "--rotate", "--vacuum-time=1s"]
+ )
+ server.run_command(["dig", dns_hostname])
+ log_output = server.run_command(
+ ["journalctl", "-u", "unbound", "--grep", forwarder]
+ )
+ assert forwarder in log_output.stdout_text, (
+ f"Forwarder {forwarder} not found in logs on "
+ f"{server.hostname}"
+ )
+ server.run_command(
+ ["journalctl", "--flush", "--rotate", "--vacuum-time=1s"]
+ )
+
+ for client in clients:
+ client.run_command(["dig", dns_hostname])
+ log_output = master.run_command(
+ ["journalctl", "-u", "unbound", "--grep", forwarder]
+ )
+
+ assert forwarder in log_output.stdout_text, (
+ f"Forwarder {forwarder} not found in logs on master for "
+ f"client {client.hostname}"
+ )
+
+
@pytest.mark.skipif(
osinfo.id == 'fedora' and osinfo.version_number == (41,),
reason='Encrypted DNS not supported in fedora 41')
@@ -83,118 +133,6 @@ class TestDNSOverTLS(IntegrationTest):
"--setup-dns, ignoring") in res.stdout_text
tasks.uninstall_master(self.master)
- def test_install_dnsovertls_master(self):
- """
- This tests installs IPA server with --dns-over-tls option.
- """
- args = [
- "--dns-over-tls",
- "--dot-forwarder", "1.1.1.1#cloudflare-dns.com",
- ]
- return tasks.install_master(self.master, extra_args=args)
-
- def test_install_dnsovertls_client(self):
- """
- This tests installs IPA client with --dns-over-tls option.
- """
- self.clients[0].put_file_contents(
- paths.RESOLV_CONF,
- "nameserver %s" % self.master.ip
- )
- args = [
- "--dns-over-tls"
- ]
- return tasks.install_client(self.master,
- self.clients[0],
- nameservers=None,
- extra_args=args)
-
- def test_install_dnsovertls_replica(self):
- """
- This tests installs IPA replica with --dns-over-tls option.
- """
- args = [
- "--dns-over-tls",
- "--dot-forwarder", "1.1.1.1#cloudflare-dns.com",
- ]
- return tasks.install_replica(self.master, self.replicas[0],
- setup_dns=True, extra_args=args)
-
- def test_queries_encrypted(self):
- """
- This test performs queries from each of the hosts
- and ensures they were routed to 1.1.1.1#853 (eDNS).
- """
- unbound_log_cfg = textwrap.dedent("""
- server:
- verbosity: 3
- log-queries: yes
- """)
- # Test servers first (querying to local Unbound)
- for server in [self.master, self.replicas[0]]:
- server.put_file_contents("/etc/unbound/conf.d/log.conf",
- unbound_log_cfg)
- server.run_command(["systemctl", "restart", "unbound"])
- server.run_command(["journalctl", "--flush", "--rotate",
- "--vacuum-time=1s"])
- server.run_command(["dig", "freeipa.org"])
- server.run_command(["journalctl", "-u", "unbound",
- "--grep=1.1.1.1#853"])
- server.run_command(["journalctl", "--flush", "--rotate",
- "--vacuum-time=1s"])
- # Now, test the client (redirects query to master)
- self.clients[0].run_command(["dig", "redhat.com"])
- self.master.run_command(["journalctl", "-u", "unbound",
- "--grep=1.1.1.1#853"])
-
- def test_uninstall_all(self):
- """
- This test ensures that all hosts can be uninstalled correctly.
- """
- tasks.uninstall_client(self.clients[0])
- tasks.uninstall_replica(self.master, self.replicas[0])
- tasks.uninstall_master(self.master)
-
- def test_install_dnsovertls_master_external_ca(self):
- """
- This test ensures that IPA server can be installed
- with DoT using an external CA.
- """
- self.master.run_command(["openssl", "req", "-newkey", "rsa:2048",
- "-nodes", "-keyout",
- "/etc/pki/tls/certs/privkey.pem", "-x509",
- "-days", "36500", "-out",
- "/etc/pki/tls/certs/certificate.pem", "-subj",
- ("/C=ES/ST=Andalucia/L=Sevilla/O=CompanyName/"
- "OU=IT/CN={}/"
- "emailAddress=email@example.com")
- .format(self.master.hostname)])
- self.master.run_command(["chown", "named:named",
- "/etc/pki/tls/certs/privkey.pem",
- "/etc/pki/tls/certs/certificate.pem"])
- args = [
- "--dns-over-tls",
- "--dot-forwarder", "1.1.1.1#cloudflare-dns.com",
- "--dns-over-tls-cert", "/etc/pki/tls/certs/certificate.pem",
- "--dns-over-tls-key", "/etc/pki/tls/certs/privkey.pem"
- ]
- return tasks.install_master(self.master, extra_args=args)
-
- def test_enrollments_external_ca(self):
- """
- Test that replicas and clients can be deployed when the master
- uses an external CA.
- """
- tasks.copy_files(self.master, self.clients[0],
- ["/etc/pki/tls/certs/certificate.pem"])
- self.clients[0].run_command(["mv",
- "/etc/pki/tls/certs/certificate.pem",
- "/etc/pki/ca-trust/source/anchors/"])
- self.clients[0].run_command(["update-ca-trust", "extract"])
- self.test_install_dnsovertls_client()
- self.test_install_dnsovertls_replica()
- self.test_queries_encrypted()
-
def test_install_dnsovertls_with_invalid_ipaddress_master(self):
"""
This test installs an IPA server using the --dns-over-tls
@@ -251,6 +189,157 @@ class TestDNSOverTLS(IntegrationTest):
assert '''--dns-over-tls Configure DNS over TLS''' in cmdout.stdout_text # noqa: E501
+@pytest.mark.skipif(
+ osinfo.id == 'fedora' and osinfo.version_number < (42,),
+ reason='Encrypted DNS not supported in Fedora < 42')
+class TestDNSOverTLS_RelaxedPolicy(IntegrationTest):
+ """Tests for DNS over TLS feature."""
+
+ topology = 'line'
+ num_replicas = 1
+ num_clients = 1
+
+ @classmethod
+ def install(cls, mh):
+ Firewall(cls.master).enable_service("dns-over-tls")
+ Firewall(cls.replicas[0]).enable_service("dns-over-tls")
+ tasks.install_packages(cls.master, ['*ipa-server-encrypted-dns'])
+ tasks.install_packages(cls.replicas[0], ['*ipa-server-encrypted-dns'])
+ tasks.install_packages(cls.clients[0], ['*ipa-client-encrypted-dns'])
+
+ def test_dot_relaxed_dns_policy_with_IPA_CA(self):
+ """
+ This test installs IPA server, replica, and client with
+ --no-dnssec-validation option, relaxed DNS policy, and
+ with IPA CA, ensuring all queries are encrypted.
+ """
+ args = [
+ "--dns-over-tls",
+ "--dot-forwarder", "1.1.1.1#cloudflare-dns.com",
+ "--no-dnssec-validation",
+ "--dns-policy", "relaxed"
+ ]
+ tasks.install_master(self.master, extra_args=args)
+
+ self.clients[0].put_file_contents(
+ paths.RESOLV_CONF,
+ "nameserver %s" % self.master.ip
+ )
+ args = [
+ "--dns-over-tls",
+ "--no-dnssec-validation"
+ ]
+ tasks.install_client(
+ self.master,
+ self.clients[0],
+ nameservers=None,
+ extra_args=args
+ )
+
+ args = [
+ "--dns-over-tls",
+ "--dot-forwarder", "1.1.1.1#cloudflare-dns.com",
+ "--no-dnssec-validation",
+ "--dns-policy", "relaxed"
+ ]
+ tasks.install_replica(
+ self.master,
+ self.replicas[0],
+ setup_dns=True,
+ extra_args=args
+ )
+ verify_queries_encrypted(
+ self.master,
+ [self.replicas[0]],
+ [self.clients[0]]
+ )
+
+ def test_uninstall_all(self):
+ """
+ This test ensures that all hosts can be uninstalled correctly.
+ """
+ tasks.uninstall_client(self.clients[0])
+ tasks.uninstall_replica(self.master, self.replicas[0])
+ tasks.uninstall_master(self.master)
+
+ def test_dot_relaxed_dns_policy_with_external_ca(self):
+ """
+ This test installs IPA server, replica, and client with
+ --no-dnssec-validation option, relaxed DNS policy, and
+ with external CA, ensuring all queries are encrypted.
+ """
+ # Install Master with external CA
+ # Create external CA cert + key.
+ external_ca = ExternalCA(days=36500)
+ cert_pem = external_ca.create_ca()
+ key_pem = external_ca.ca_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption(),
+ )
+ cert_dest = "/etc/pki/tls/certs/certificate.pem"
+ key_dest = "/etc/pki/tls/certs/privkey.pem"
+
+ self.master.put_file_contents(cert_dest, cert_pem)
+ self.master.put_file_contents(key_dest, key_pem)
+
+ args = [
+ "--dns-over-tls",
+ "--dot-forwarder", "1.1.1.1#cloudflare-dns.com",
+ "--dns-over-tls-cert", cert_dest,
+ "--dns-over-tls-key", key_dest,
+ "--no-dnssec-validation",
+ "--dns-policy", "relaxed"
+ ]
+ tasks.install_master(self.master, extra_args=args)
+
+ # Install Client with external CA
+ self.clients[0].put_file_contents(
+ paths.RESOLV_CONF,
+ "nameserver %s" % self.master.ip
+ )
+ dest_file = "/etc/pki/ca-trust/source/anchors/certificate.pem"
+ data = self.master.get_file_contents(cert_dest)
+ self.clients[0].transport.put_file_contents(dest_file, data)
+ self.clients[0].run_command(["update-ca-trust", "extract"])
+
+ args = [
+ "--dns-over-tls",
+ "--no-dnssec-validation"
+ ]
+ tasks.install_client(
+ self.master,
+ self.clients[0],
+ nameservers=None,
+ extra_args=args
+ )
+
+ # Install Replica with external CA
+ dest_file = "/etc/pki/ca-trust/source/anchors/certificate.pem"
+ data = self.master.get_file_contents(cert_dest)
+ self.clients[0].transport.put_file_contents(dest_file, data)
+
+ self.replicas[0].run_command(["update-ca-trust", "extract"])
+ args = [
+ "--dns-over-tls",
+ "--dot-forwarder", "1.1.1.1#cloudflare-dns.com",
+ "--no-dnssec-validation",
+ "--dns-policy", "relaxed"
+ ]
+ tasks.install_replica(
+ self.master,
+ self.replicas[0],
+ setup_dns=True,
+ extra_args=args
+ )
+
+ verify_queries_encrypted(
+ self.master,
+ [self.replicas[0]],
+ [self.clients[0]]
+ )
+
+
@pytest.mark.skipif(
osinfo.id == 'fedora' and osinfo.version_number == (41,),
reason='Encrypted DNS not supported in fedora 41')
--
2.51.1

View File

@ -0,0 +1,36 @@
From 883f69db280071cf8003eff977f6f061651c7a7d Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
Date: Tue, 11 Mar 2025 18:32:43 +0100
Subject: [PATCH] ipatest: make test_cert more robust to replication delays
The test TestCAShowErrorHandling::test_ca_show_error_handling is
adding a subca on the replica, then checks the entry is present on the
master.
If the replication is a bit slow, the call on the master may fail to
return the newly created subca.
The test should wait for replication to complete before calling
ipa ca-find.
Fixes: https://pagure.io/freeipa/issue/9762
Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
ipatests/test_integration/test_cert.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/ipatests/test_integration/test_cert.py b/ipatests/test_integration/test_cert.py
index 91598b655a8cd6ff92c1a0cf2166c6548a7af758..c642caaf03dfb980e956cf8105911440a5ec8539 100644
--- a/ipatests/test_integration/test_cert.py
+++ b/ipatests/test_integration/test_cert.py
@@ -548,6 +548,8 @@ class TestCAShowErrorHandling(IntegrationTest):
'ipa', 'ca-add', lwca, '--subject', 'CN=LWCA 1'
])
assert 'Created CA "{}"'.format(lwca) in result.stdout_text
+ # wait for replication to propagate the change
+ tasks.wait_for_replication(self.replicas[0].ldap_connect())
result = self.master.run_command(['ipa', 'ca-find'])
assert 'Name: {}'.format(lwca) in result.stdout_text
result = self.master.run_command(
--
2.51.1

View File

@ -0,0 +1,49 @@
From 9c416a61b72b288212e03724cff9bd169390cbfe Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
Date: Wed, 8 Oct 2025 14:26:12 +0200
Subject: [PATCH] test_cert: adapt the expect error message to PKI 11.7.0-5
The error message returned by ipa ca-show has changed with PKI 11.7.
Adapt the test to succeed with old and new versions.
Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
ipatests/test_integration/test_cert.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/ipatests/test_integration/test_cert.py b/ipatests/test_integration/test_cert.py
index 3e1e8fd1fa14278f3868e6e206a979e11b70a848..21568c2421c21855df06bcf5fbb4d52b3651a523 100644
--- a/ipatests/test_integration/test_cert.py
+++ b/ipatests/test_integration/test_cert.py
@@ -575,6 +575,9 @@ class TestCAShowErrorHandling(IntegrationTest):
'ipa', 'ca-add', lwca, '--subject', 'CN=LWCA 1'
])
assert 'Created CA "{}"'.format(lwca) in result.stdout_text
+ match = re.search(r'Authority ID: (?P<id>.*)', result.stdout_text)
+ id = match.group('id')
+
# wait for replication to propagate the change
tasks.wait_for_replication(self.replicas[0].ldap_connect())
result = self.master.run_command(['ipa', 'ca-find'])
@@ -585,13 +588,16 @@ class TestCAShowErrorHandling(IntegrationTest):
)
error_msg = 'ipa: ERROR: The certificate for ' \
'{} is not available on this server.'.format(lwca)
+ new_error_msg = 'ipa: ERROR: Certificate for CA ' \
+ '"{}" not available'.format(id)
pki_version = tasks.get_pki_version(self.master)
# The regression was introduced in 11.5 and fixed in 11.7
bad_version = (tasks.parse_version('11.5.0') <= pki_version
< tasks.parse_version('11.7.0'))
with xfail_context(bad_version,
reason="https://pagure.io/freeipa/issue/9606"):
- assert error_msg in result.stderr_text
+ assert (error_msg in result.stderr_text
+ or new_error_msg in result.stderr_text)
def test_certmonger_empty_cert_not_segfault(self):
"""Test empty cert request doesn't force certmonger to segfault
--
2.51.1

View File

@ -0,0 +1,51 @@
From 4a2e912d2386dfeb9765e32dd244b32b03cbf9a5 Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
Date: Tue, 2 Sep 2025 10:05:31 +0200
Subject: [PATCH] ipatests: fix test_certmonger_ipa_responder_jsonrpc
Test scenario:
- install IPA server and client
- store the start date
- request a certificate on the client using ipa-getcert
- check in the journal after start date that the request was done using the
https://.../ipa/json URI
The test obtains the start date on the runner. As a consequence, if the runner
is late compared to the client, it may miss the message in the journal.
The date should rather be obtained on the client.
Fixes: https://pagure.io/freeipa/issue/9848
Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
ipatests/test_integration/test_cert.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/ipatests/test_integration/test_cert.py b/ipatests/test_integration/test_cert.py
index 84adf2ceafe013e6cfc973fb2cb650c40f36971d..ddc4e089a365b89a3dd26881845228ca60558bf7 100644
--- a/ipatests/test_integration/test_cert.py
+++ b/ipatests/test_integration/test_cert.py
@@ -13,7 +13,6 @@ import pytest
import random
import re
import string
-import time
import textwrap
from ipaplatform.paths import paths
@@ -70,9 +69,10 @@ class TestInstallMasterClient(IntegrationTest):
def install(cls, mh):
super().install(mh)
- # time to look into journal logs in
+ # store the start time to look into journal logs in
# test_certmonger_ipa_responder_jsonrpc
- cls.since = time.strftime('%Y-%m-%d %H:%M:%S')
+ result = cls.clients[0].run_command(['date', '+%Y-%m-%d %H:%M:%S'])
+ cls.since = result.stdout_text.strip()
def test_cacert_file_appear_with_option_F(self):
"""Test if getcert creates cacert file with -F option
--
2.51.1

View File

@ -0,0 +1,49 @@
From 9c416a61b72b288212e03724cff9bd169390cbfe Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
Date: Wed, 8 Oct 2025 14:26:12 +0200
Subject: [PATCH] test_cert: adapt the expect error message to PKI 11.7.0-5
The error message returned by ipa ca-show has changed with PKI 11.7.
Adapt the test to succeed with old and new versions.
Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
---
ipatests/test_integration/test_cert.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/ipatests/test_integration/test_cert.py b/ipatests/test_integration/test_cert.py
index 3e1e8fd1fa14278f3868e6e206a979e11b70a848..21568c2421c21855df06bcf5fbb4d52b3651a523 100644
--- a/ipatests/test_integration/test_cert.py
+++ b/ipatests/test_integration/test_cert.py
@@ -575,6 +575,9 @@ class TestCAShowErrorHandling(IntegrationTest):
'ipa', 'ca-add', lwca, '--subject', 'CN=LWCA 1'
])
assert 'Created CA "{}"'.format(lwca) in result.stdout_text
+ match = re.search(r'Authority ID: (?P<id>.*)', result.stdout_text)
+ id = match.group('id')
+
# wait for replication to propagate the change
tasks.wait_for_replication(self.replicas[0].ldap_connect())
result = self.master.run_command(['ipa', 'ca-find'])
@@ -585,13 +588,16 @@ class TestCAShowErrorHandling(IntegrationTest):
)
error_msg = 'ipa: ERROR: The certificate for ' \
'{} is not available on this server.'.format(lwca)
+ new_error_msg = 'ipa: ERROR: Certificate for CA ' \
+ '"{}" not available'.format(id)
pki_version = tasks.get_pki_version(self.master)
# The regression was introduced in 11.5 and fixed in 11.7
bad_version = (tasks.parse_version('11.5.0') <= pki_version
< tasks.parse_version('11.7.0'))
with xfail_context(bad_version,
reason="https://pagure.io/freeipa/issue/9606"):
- assert error_msg in result.stderr_text
+ assert (error_msg in result.stderr_text
+ or new_error_msg in result.stderr_text)
def test_certmonger_empty_cert_not_segfault(self):
"""Test empty cert request doesn't force certmonger to segfault
--
2.51.1

View File

@ -0,0 +1,286 @@
From 12dd94e61a245ac8645789423aa9fc47b3cc14d0 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Fri, 10 Oct 2025 19:41:15 +0000
Subject: [PATCH] Include the HSM token name when creating LWCAs
In order to generate the private key for a a LWCA (subca)
on an HSM the name of the subca needs to be the HSM token
name : name_of_subca, e.g. ipa_token:test.
This works fine now without any code changes but it requires
that admins always remember to include the prefix which is
unlikely (everyone makes mistakes). So do it for them if
an HSM is present and a token is not provided. We only support
one token at a time in IPA so for now this is sufficient.
One can also mix-and-match including the token and not.
For example you can run:
$ ipa ca-add test --subject cn=test
$ ipa ca-show ipa_token:test
It shouldn't be an issue if a lwca name contains a colon.
Fixes: https://pagure.io/freeipa/issue/9865
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Rafael Guterres Jeffman <rjeffman@redhat.com>
Reviewed-By: Rafael Guterres Jeffman <rjeffman@redhat.com>
---
ipaserver/plugins/ca.py | 42 +++++++-
ipatests/test_integration/test_hsm.py | 140 ++++++++++++++++++++++++++
2 files changed, 181 insertions(+), 1 deletion(-)
diff --git a/ipaserver/plugins/ca.py b/ipaserver/plugins/ca.py
index d35275cc6f625c4834f43a537b29da32a909326b..1eeab4048091bd990c4b27e0f7976940da734acc 100644
--- a/ipaserver/plugins/ca.py
+++ b/ipaserver/plugins/ca.py
@@ -10,7 +10,7 @@ from ipalib import api, errors, messages, output
from ipalib import Bytes, DNParam, Flag, Str, Int
from ipalib.constants import IPA_CA_CN
from ipalib.plugable import Registry
-from ipapython.dn import ATTR_NAME_BY_OID
+from ipapython.dn import ATTR_NAME_BY_OID, DN
from ipaserver.plugins.baseldap import (
LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete,
LDAPUpdate, LDAPRetrieve, LDAPQuery, pkey_to_value)
@@ -174,6 +174,26 @@ class ca(LDAPObject):
},
}
+ # LWCA are supported on HSMs but the key will only be generated
+ # there if the LWCA name is prefixed by the token name. So do
+ # that automatically for users to hide that complexity.
+
+ def add_token_key(self, *keys):
+ if len(keys) == 0 or keys[0] == IPA_CA_CN:
+ return keys
+ config = api.Command['config_show']()['result']
+ if 'hsm_token_name' in config and not keys[-1].startswith(
+ config['hsm_token_name']
+ ):
+ keys = (f"{config['hsm_token_name']}:{keys[-1]}",)
+ return keys
+
+ def remove_token_key(self, *keys):
+ config = api.Command['config_show']()['result']
+ if 'hsm_token_name' in config and ':' in keys[-1]:
+ keys = (keys[-1].split(':', 1)[1],)
+ return keys
+
def set_certificate_attrs(entry, options, want_cert=True):
"""
@@ -233,6 +253,10 @@ class ca_find(LDAPSearch):
def execute(self, *keys, **options):
ca_enabled_check(self.api)
+ keys = self.obj.add_token_key(*keys)
+ if 'cn' in options:
+ new = self.obj.add_token_key(options['cn'])
+ options['cn'] = new[0]
result = super(ca_find, self).execute(*keys, **options)
if not options.get('pkey_only', False):
for entry in result['result']:
@@ -259,6 +283,7 @@ class ca_show(LDAPRetrieve):
def execute(self, *keys, **options):
ca_enabled_check(self.api)
+ keys = self.obj.add_token_key(*keys)
result = super(ca_show, self).execute(*keys, **options)
msg = set_certificate_attrs(result['result'], options)
if msg:
@@ -301,6 +326,7 @@ class ca_add(LDAPCreate):
# check for name collision before creating CA in Dogtag
try:
+ keys = self.obj.remove_token_key(*keys)
api.Object.ca.get_dn_if_exists(keys[-1])
self.obj.handle_duplicate_entry(*keys)
except errors.NotFound:
@@ -325,6 +351,10 @@ class ca_add(LDAPCreate):
entry['ipacasubjectdn'] = [resp['dn']]
return dn
+ def execute(self, *keys, **options):
+ keys = self.obj.add_token_key(*keys)
+ return super(ca_add, self).execute(*keys, **options)
+
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
msg = set_certificate_attrs(entry_attrs, options)
if msg:
@@ -341,6 +371,12 @@ class ca_del(LDAPDelete):
def pre_callback(self, ldap, dn, *keys, **options):
ca_enabled_check(self.api)
+ # Handle an HSM-stored LWCA that is referenced without the
+ # token name. On a non-HSM install real_* will be unchanged
+ # from keys and dn.
+ real_keys = self.obj.add_token_key(*keys)
+ dn = DN(('cn', real_keys[0]), self.obj.container_dn,
+ api.env.basedn)
# ensure operator has permission to delete CA
# before contacting Dogtag
if not ldap.can_delete(dn):
@@ -386,6 +422,10 @@ class ca_mod(LDAPUpdate):
return dn
+ def execute(self, *keys, **options):
+ keys = self.obj.add_token_key(*keys)
+ return super(ca_mod, self).execute(*keys, **options)
+
class CAQuery(LDAPQuery):
has_output = output.standard_value
diff --git a/ipatests/test_integration/test_hsm.py b/ipatests/test_integration/test_hsm.py
index 42895fcd60a7c02d3b6103c2f6751a367da30b2f..159ead2fcc79982c7289f316c57aaeb2b812004e 100644
--- a/ipatests/test_integration/test_hsm.py
+++ b/ipatests/test_integration/test_hsm.py
@@ -1316,3 +1316,143 @@ class TestHSMVault(BaseHSMTest):
vault_name,
"--password", vault_password,
])
+
+
+class TestHSMLWCA(BaseHSMTest):
+ """Test that managing a LWCA on an HSM-installed system installs
+ the keys onto the HSM and not the local NSS database.
+
+ Also verify that the ca-* operations can handle hiding the
+ complexity of the token name prefix without specifying it
+ (but also allowing it).
+ """
+
+ num_replicas = 0
+
+ def remove_lwca(self, lwca, othername=None):
+ """Save a lot of duplicate code disabling and removing a lwca"""
+ self.master.run_command([
+ 'ipa', 'ca-disable', lwca,
+ ])
+
+ name = othername or lwca
+ self.master.run_command([
+ 'ipa', 'ca-del', name,
+ ])
+
+ def test_hsm_add_lwca(self):
+ lwca = "lwca"
+ lwca_fullname = "{}:{}".format(self.token_name, lwca)
+
+ check_version(self.master)
+
+ """First test add/show/disable/delete without specifying
+ the token. So hiding the complexity of including the
+ token name except the stored Name (cn) value.
+ """
+ result = self.master.run_command([
+ 'ipa', 'ca-add', lwca, '--subject', 'CN=LWCA',
+ ])
+ assert f'Name: {lwca_fullname}' in result.stdout_text
+
+ self.remove_lwca(lwca)
+
+ def test_hsm_token_add_lwca(self):
+ """Now test add/show/disable/delete with specifying
+ the token with the name. So not hiding the complexity of
+ including the token name.
+ """
+ lwca = "lwca"
+ lwca_fullname = "{}:{}".format(self.token_name, lwca)
+
+ check_version(self.master)
+
+ result = self.master.run_command([
+ 'ipa', 'ca-add', lwca_fullname, '--subject', 'CN=LWCA',
+ ])
+ assert f'Name: {lwca_fullname}' in result.stdout_text
+
+ self.remove_lwca(lwca_fullname)
+
+ def test_duplicate_token_add_lwca(self):
+ """Test that adding a duplicate name by adding with and
+ without the token included"""
+ lwca = "lwca"
+ lwca_fullname = "{}:{}".format(self.token_name, lwca)
+
+ check_version(self.master)
+
+ self.master.run_command([
+ 'ipa', 'ca-add', lwca_fullname, '--subject', 'CN=LWCA',
+ ])
+ result = self.master.run_command([
+ 'ipa', 'ca-add', lwca, '--subject', 'CN=LWCA',
+ ], raiseonerr=False)
+ assert 'Subject DN is already used' in result.stderr_text
+
+ # we can also mix and match the name in the cleanup
+ self.remove_lwca(lwca, lwca_fullname)
+
+ def test_colon_in_lwca(self):
+ """
+ Trying to add a CA using an unknown or mis-typed token
+ name will result in the real token + whatever the name.
+ """
+ lwca = "lwca"
+ colonname = "undefined:{}".format(lwca)
+ colonfullname = "{}:undefined:{}".format(self.token_name, lwca)
+
+ check_version(self.master)
+
+ result = self.master.run_command([
+ 'ipa', 'ca-add', colonname, '--subject', 'CN=LWCA',
+ ])
+ assert 'Name: {}'.format(colonfullname) in result.stdout_text
+
+ self.remove_lwca(colonfullname, colonname)
+
+ def test_show_lwca(self):
+ """Show a lwca using with and without the token name"""
+ lwca = "lwca"
+ lwca_fullname = "{}:{}".format(self.token_name, lwca)
+
+ check_version(self.master)
+
+ for name in (lwca, lwca_fullname,):
+ result = self.master.run_command([
+ 'ipa', 'ca-add', name, '--subject', f'CN={name}',
+ ])
+ assert f'Name: {lwca_fullname}' in result.stdout_text
+ self.master.run_command(['ipa', 'ca-show', name])
+ self.remove_lwca(name)
+
+ def test_find_lwca(self):
+ """Find a lwca using with and without the token name"""
+ lwca = "lwca"
+ lwca_fullname = "{}:{}".format(self.token_name, lwca)
+
+ check_version(self.master)
+
+ for name in (lwca, lwca_fullname,):
+ result = self.master.run_command([
+ 'ipa', 'ca-add', name, '--subject', f'CN={name}',
+ ])
+ assert f'Name: {lwca_fullname}' in result.stdout_text
+ self.master.run_command(['ipa', 'ca-find', '--name', name])
+ self.remove_lwca(name)
+
+ def test_mod_lwca(self):
+ """Modify a lwca using with and without the token name"""
+ lwca = "lwca"
+ lwca_fullname = "{}:{}".format(self.token_name, lwca)
+
+ check_version(self.master)
+
+ for name in (lwca, lwca_fullname,):
+ result = self.master.run_command([
+ 'ipa', 'ca-add', name, '--subject', f'CN={name}',
+ ])
+ assert f'Name: {lwca_fullname}' in result.stdout_text
+ self.master.run_command(
+ ['ipa', 'ca-mod', name, '--desc', name])
+ self.remove_lwca(name)
--
2.51.1

View File

@ -0,0 +1,403 @@
From 9d144b89ce743805e6e2d19791436ca5ecee172f Mon Sep 17 00:00:00 2001
From: Anuja More <amore@redhat.com>
Date: Wed, 8 Oct 2025 13:18:56 +0530
Subject: [PATCH] ipatests: Refactor and port trust functional SUDO tests.
- Test scenarios :
- AD users running commands as root via external groups
- AD users switching to other AD user accounts
- IPA users running commands as AD users
- Sudo rule disable/enable functionality
- Command allow/deny restrictions
- Access denial for users not in sudo rules
- Automated with Cursor+Claude
Related : https://pagure.io/freeipa/issue/9845
Signed-off-by: Anuja More <amore@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
.../test_integration/test_trust_functional.py | 356 +++++++++++++++++-
1 file changed, 355 insertions(+), 1 deletion(-)
diff --git a/ipatests/test_integration/test_trust_functional.py b/ipatests/test_integration/test_trust_functional.py
index b40bf9675ed5cddaf51624417356ddab28e870a5..a85f21e96463757b9a446df666d5361e65ba686c 100644
--- a/ipatests/test_integration/test_trust_functional.py
+++ b/ipatests/test_integration/test_trust_functional.py
@@ -2,6 +2,8 @@
from __future__ import absolute_import
+import time
+
from ipaplatform.paths import paths
from ipatests.pytest_ipa.integration import tasks
from ipatests.test_integration.test_trust import BaseTestTrust
@@ -290,7 +292,9 @@ class TestTrustFunctionalHbac(BaseTestTrust):
with self.clients[0].spawn_expect(
test_sudo.format(user=user)) as e:
e.sendline('Secret123')
- e.expect_exit(ignore_remaining_output=True, timeout=60)
+ e.sendline('exit')
+ e.expect_exit(
+ ignore_remaining_output=True, raiseonerr=False)
output = e.get_last_output()
assert 'uid=0(root)' in output
for user in [self.aduser2, self.subaduser2]:
@@ -304,3 +308,353 @@ class TestTrustFunctionalHbac(BaseTestTrust):
finally:
self._cleanup_hrule_allow_all_and_wait(hrule)
self.master.run_command(["ipa", "sudorule-del", srule])
+
+
+class TestTrustFunctionalSudo(BaseTestTrust):
+ topology = 'line'
+ num_ad_treedomains = 0
+
+ def cache_reset(self):
+ tasks.clear_sssd_cache(self.master)
+ tasks.clear_sssd_cache(self.clients[0])
+ tasks.wait_for_sssd_domain_status_online(self.master)
+ tasks.wait_for_sssd_domain_status_online(self.clients[0])
+ # give time to SSSD to retrieve new records
+ time.sleep(30)
+
+ def _cleanup_srule(self, srule):
+ tasks.kinit_admin(self.master)
+ self.master.run_command(["ipa", "sudorule-del", srule])
+ self.cache_reset()
+
+ def _run_sudo_command(self, host, command, username, password='Secret123',
+ expected_output=None, raiseonerr=True, timeout=60):
+ """
+ Run a sudo command using spawn_expect with proper error handling.
+
+ Args:
+ host: Host to run the command on
+ command: Command to execute
+ username: Username for password prompt
+ password: Password to send
+ expected_output: Expected string in output (for assertion)
+ raiseonerr: Whether to raise on error
+ timeout: Timeout for expect_exit
+
+ Returns:
+ Output from the command
+ """
+ with host.spawn_expect(command) as e:
+ e.expect(r'(?i).*Password for {}.*:'.format(username))
+ e.sendline(password)
+ e.expect_exit(ignore_remaining_output=True,
+ raiseonerr=raiseonerr, timeout=timeout)
+ output = e.get_last_output()
+
+ if expected_output:
+ assert expected_output in output, (
+ f"Expected '{expected_output}' in output, got: {output}"
+ )
+
+ return output
+
+ def test_ipa_trust_func_sudo_setup(self):
+ tasks.configure_dns_for_trust(self.master, self.ad)
+ tasks.establish_trust_with_ad(
+ self.master, self.ad_domain,
+ extra_args=['--range-type', 'ipa-ad-trust'])
+ tasks.kinit_admin(self.master)
+
+ for i in range(1, 3):
+ external_group = f"sudogroup_external{i}"
+ internal_group = f"sudogroup{i}"
+ tasks.group_add(self.master,
+ groupname=external_group,
+ extra_args=["--external"]
+ )
+ tasks.group_add(self.master,
+ groupname=internal_group
+ )
+ tasks.group_add_member(self.master,
+ groupname=internal_group,
+ extra_args=[f"--groups={external_group}"]
+ )
+
+ group_members = {
+ "sudogroup_external1": [self.aduser, self.subaduser],
+ "sudogroup_external2": [self.aduser2, self.subaduser2],
+ }
+
+ for group, members in group_members.items():
+ for member in members:
+ self.master.run_command([
+ 'ipa', '-n', 'group-add-member', '--external',
+ member, group,
+ ])
+ for user in ['sudouser1', 'sudouser2', 'ipauser1']:
+ tasks.create_active_user(
+ self.master, user, password='Secret123'
+ )
+
+ def test_ipa_trust_func_sudo_0001(self):
+ """
+ Test sudo rule allow AD user in external group to run commands as root.
+
+ This test creates a sudo rule that allows members of sudogroup1 (which
+ includes AD users via external group membership) to run all commands as
+ root. It verifies that AD users who are members of the external group
+ can successfully use sudo to gain root privileges.
+ """
+ srule = "sudorule_01"
+ cmd = ["ipa", "sudorule-add", srule, "--hostcat=all", "--cmdcat=all"]
+ try:
+ tasks.kinit_admin(self.master)
+ self.master.run_command(cmd)
+ self.master.run_command(
+ ["ipa", "sudorule-add-user", srule, "--groups=sudogroup1"]
+ )
+ self.cache_reset()
+ for user in [self.aduser, self.subaduser]:
+ test_sudo = f"su {user} -c 'sudo -S id'"
+ self._run_sudo_command(
+ self.clients[0], test_sudo, user,
+ expected_output='uid=0(root)'
+ )
+ finally:
+ self._cleanup_srule(srule)
+
+ def test_ipa_trust_func_sudo_0002(self):
+ """
+ Test sudo rule allows AD users to run commands as other AD users.
+
+ This test creates a sudo rule that allows members of sudogroup1 to run
+ commands as members of sudogroup2. It verifies that AD users can
+ successfully use sudo to switch to other AD user accounts when they
+ have the appropriate sudo permissions.
+ """
+ srule = "sudorule_02"
+ cmd = ["ipa", "sudorule-add", srule, "--hostcat=all", "--cmdcat=all"]
+ try:
+ tasks.kinit_admin(self.master)
+ self.master.run_command(cmd)
+ self.master.run_command(
+ ["ipa", "sudorule-add-user", srule, "--groups=sudogroup1"]
+ )
+ self.master.run_command(
+ ["ipa", "sudorule-add-runasuser", srule, "--groups=sudogroup2"]
+ )
+ self.cache_reset()
+ test_sudo = "su {0} -c 'sudo -S -u {1} id'".format(
+ self.aduser, self.aduser2
+ )
+ self._run_sudo_command(self.clients[0], test_sudo, self.aduser,
+ expected_output=self.aduser2
+ )
+
+ test_sudo = "su {0} -c 'sudo -S -u {1} id'".format(
+ self.subaduser, self.subaduser2
+ )
+ self._run_sudo_command(self.clients[0], test_sudo, self.subaduser,
+ expected_output=self.subaduser2
+ )
+ finally:
+ self._cleanup_srule(srule)
+
+ def test_ipa_trust_func_sudo_0004(self):
+ """
+ Test sudo rule allows IPA users to run commands as AD users.
+
+ This test creates a sudo rule that allows IPA users to run commands
+ as AD users who are members of external groups. It verifies that
+ IPA users can successfully use sudo to switch to AD user accounts
+ when they have the appropriate sudo permissions.
+ """
+ srule = "sudorule_04"
+ cmd = ["ipa", "sudorule-add", srule, "--hostcat=all", "--cmdcat=all"]
+ try:
+ tasks.kinit_admin(self.master)
+ self.master.run_command(cmd)
+ self.master.run_command(
+ ["ipa", "sudorule-add-user", srule, "--users=ipauser1"]
+ )
+ self.master.run_command(
+ ["ipa", "sudorule-add-runasuser", srule, "--groups=sudogroup1"]
+ )
+ self.cache_reset()
+ self.master.run_command(
+ ["ipa", "sudorule-show", srule, "--all"]
+ )
+ self.master.run_command(['ipa', 'group-show', 'sudogroup1'])
+ self.master.run_command(
+ ['ipa', 'group-show', 'sudogroup_external1']
+ )
+ for user in [self.aduser, self.subaduser]:
+ tasks.clear_sssd_cache(self.master)
+ test_sudo = f"su ipauser1 -c 'sudo -S -u {user} id'"
+ self._run_sudo_command(self.master, test_sudo, 'ipauser1',
+ expected_output=user)
+
+ for user in [self.aduser2, self.subaduser2]:
+ tasks.clear_sssd_cache(self.master)
+ test_sudo = f"su ipauser1 -c 'sudo -S -u {user} id'"
+ self._run_sudo_command(self.master, test_sudo, 'ipauser1',
+ expected_output="not allowed to",
+ raiseonerr=False)
+ finally:
+ self._cleanup_srule(srule)
+
+ def test_ipa_trust_func_sudo_0005(self):
+ """
+ Test sudo rule disable/enable functionality for AD users.
+
+ Test creates a sudo rule that allows AD users to run commands as root,
+ then tests the disable/enable functionality. It verifies that:
+ 1. AD users can sudo as root when the rule is enabled
+ 2. AD users are denied sudo access when the rule is disabled
+ 3. AD users can sudo as root again when the rule is re-enabled
+ """
+ srule = "sudorule_05"
+ cmd = ["ipa", "sudorule-add", srule, "--hostcat=all", "--cmdcat=all"]
+ try:
+ tasks.kinit_admin(self.master)
+ self.master.run_command(cmd)
+ self.master.run_command(
+ ["ipa", "sudorule-add-user", srule, "--groups=sudogroup1"]
+ )
+ self.cache_reset()
+ for aduser in [self.aduser, self.subaduser]:
+ # First check that user can sudo as root
+ sudo_cmd = f"su - {aduser} -c 'sudo -S id'"
+ self._run_sudo_command(self.clients[0], sudo_cmd, aduser,
+ expected_output='uid=0(root)')
+
+ # disable sudorule
+ self.master.run_command(["ipa", "sudorule-disable", srule])
+ self.cache_reset()
+
+ # now make sure user cannot sudo as root
+ sudo_cmd = f"su - {aduser} -c 'sudo -S id'"
+ self._run_sudo_command(self.clients[0], sudo_cmd, aduser,
+ expected_output="is not allowed to",
+ raiseonerr=False)
+
+ # now reenable rule
+ self.master.run_command(["ipa", "sudorule-enable", srule])
+ self.cache_reset()
+ sudo_cmd = f"su - {aduser} -c 'sudo -S id'"
+ self._run_sudo_command(self.clients[0], sudo_cmd, aduser,
+ expected_output='uid=0(root)')
+ finally:
+ self._cleanup_srule(srule)
+
+ def test_ipa_trust_func_sudo_0007(self):
+ """
+ Test sudo rule with allow/deny command restrictions for AD users.
+
+ Test creates a sudo rule that allows AD users to run commands as root,
+ but with specific command restrictions. It verifies that:
+ 1. AD users are denied access to commands in the deny list
+ 2. AD users are allowed access to commands in the allow list
+
+ The test uses /usr/bin/id as a denied command and /usr/bin/whoami as an
+ allowed command to demonstrate the allow/deny functionality.
+ """
+ srule = "sudorule_07"
+ try:
+ tasks.kinit_admin(self.master)
+ cmd = ["ipa", "sudorule-add", srule, "--hostcat=all"]
+ self.master.run_command(cmd)
+ self.master.run_command(
+ ["ipa", "sudorule-add-user", srule, "--groups=sudogroup1"]
+ )
+ self.master.run_command(['ipa', 'sudocmd-add', '/usr/bin/id'])
+ self.master.run_command(['ipa', 'sudocmd-add', '/usr/bin/whoami'])
+ self.master.run_command(['ipa', 'sudorule-add-deny-command', srule,
+ '--sudocmds', '/usr/bin/id']
+ )
+ self.master.run_command(
+ ['ipa', 'sudorule-add-allow-command', srule,
+ '--sudocmds', '/usr/bin/whoami']
+ )
+ self.cache_reset()
+ for aduser in [self.aduser, self.subaduser]:
+ sudo_cmd = f"su - {aduser} -c 'sudo -S id'"
+ self._run_sudo_command(self.clients[0], sudo_cmd, aduser,
+ expected_output="is not allowed to",
+ raiseonerr=False)
+ for aduser in [self.aduser, self.subaduser]:
+ sudo_cmd = f"su - {aduser} -c 'sudo -S whoami'"
+ self._run_sudo_command(self.clients[0], sudo_cmd, aduser,
+ expected_output='root')
+ finally:
+ self._cleanup_srule(srule)
+ self.master.run_command(['ipa', 'sudocmd-del', '/usr/bin/id'])
+ self.master.run_command(['ipa', 'sudocmd-del', '/usr/bin/whoami'])
+
+ def test_ipa_trust_func_sudo_0009(self):
+ """
+ Test sudo rule denies AD users access when they are not in the rule.
+
+ This test creates a sudo rule that only allows members of sudogroup2
+ to run commands as other members of sudogroup2. It verifies that
+ AD users who are not members of the allowed group are denied access
+ when attempting to use sudo to switch to other user accounts.
+ """
+ srule = "sudorule_09"
+ cmd = ["ipa", "sudorule-add", srule, "--hostcat=all", "--cmdcat=all"]
+ try:
+ tasks.kinit_admin(self.master)
+ self.master.run_command(cmd)
+ self.master.run_command(
+ ["ipa", "sudorule-add-user", srule, "--groups=sudogroup2"]
+ )
+ self.master.run_command(
+ ["ipa", "sudorule-add-runasuser", srule, "--groups=sudogroup2"]
+ )
+ self.cache_reset()
+ test_sudo = "su {0} -c 'sudo -S -u {1} id'".format(
+ self.aduser, self.aduser2
+ )
+ self._run_sudo_command(self.clients[0], test_sudo, self.aduser,
+ expected_output='not allowed to run sudo',
+ raiseonerr=False)
+
+ test_sudo = "su {0} -c 'sudo -S -u {1} id'".format(
+ self.subaduser, self.subaduser2
+ )
+ self._run_sudo_command(self.clients[0], test_sudo, self.subaduser,
+ expected_output='not allowed to run sudo',
+ raiseonerr=False)
+ finally:
+ self._cleanup_srule(srule)
+
+ def test_ipa_trust_func_sudo_0010(self):
+ """
+ Test sudo rule denies IPA users access to AD users not in the rule.
+
+ This test creates a sudo rule that allows IPA users to run commands as
+ members of sudogroup2 (which includes aduser2/subaduser2), but not as
+ members of sudogroup1 (which includes aduser1/subaduser1). It verifies
+ that IPA users are denied access when attempting to use sudo to switch
+ to AD user accounts that are not in the allowed runasuser group.
+ """
+ srule = "sudorule_10"
+ cmd = ["ipa", "sudorule-add", srule, "--hostcat=all", "--cmdcat=all"]
+ try:
+ tasks.kinit_admin(self.master)
+ self.master.run_command(cmd)
+ self.master.run_command(
+ ["ipa", "sudorule-add-user", srule, "--users=ipauser1"]
+ )
+ self.master.run_command(
+ ["ipa", "sudorule-add-runasuser", srule, "--groups=sudogroup2"]
+ )
+ self.cache_reset()
+
+ for aduser in [self.aduser, self.subaduser]:
+ sudo_cmd = f"su - {aduser} -c 'sudo -S id'"
+ self._run_sudo_command(self.clients[0], sudo_cmd, aduser,
+ expected_output="is not allowed to",
+ raiseonerr=False)
+ finally:
+ self._cleanup_srule(srule)
--
2.51.1

View File

@ -0,0 +1,158 @@
From 5550efd2c4fe9e71544747ca23a99544b0f43274 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy@redhat.com>
Date: Tue, 16 Sep 2025 14:48:59 +0300
Subject: [PATCH] ipa-pwd-extop: add SysAcctManagersDNs support
Add new attribute, SysAcctManagersDNs, to store list of DNs allowed to
reset user passwords without forcing the users to change them
afterwards.
This list will differ from the use of PassSyncManagersDNs by the fact
that password policy checks will still apply to those password changes.
Fixes: https://pagure.io/freeipa/issue/9842
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Rafael Guterres Jeffman <rjeffman@redhat.com>
Reviewed-By: Thomas Woerner <twoerner@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
---
.../ipa-slapi-plugins/ipa-pwd-extop/common.c | 15 ++++++++++----
.../ipa-pwd-extop/ipa_pwd_extop.c | 9 +++++----
.../ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h | 3 ++-
.../ipa-slapi-plugins/ipa-pwd-extop/prepost.c | 20 +++++++++++--------
4 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c
index c85795c1e1c4fa42bde80829861333168ea193e6..114d20417d053ad7e822bd474eedf794b2c316d6 100644
--- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c
+++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/common.c
@@ -225,7 +225,10 @@ static struct ipapwd_krbcfg *ipapwd_getConfig(void)
goto free_and_error;
}
config->passsync_mgrs =
- slapi_entry_attr_get_charray(config_entry, "passSyncManagersDNs");
+ slapi_entry_attr_get_charray(config_entry, "passSyncManagersDNs");
+ config->sysacct_mgrs =
+ slapi_entry_attr_get_charray(config_entry, "SysAcctManagersDNs");
+
/* now add Directory Manager, it is always added by default */
tmpstr = slapi_ch_strdup("cn=Directory Manager");
slapi_ch_array_add(&config->passsync_mgrs, tmpstr);
@@ -233,8 +236,6 @@ static struct ipapwd_krbcfg *ipapwd_getConfig(void)
LOG_OOM();
goto free_and_error;
}
- for (i = 0; config->passsync_mgrs[i]; i++) /* count */ ;
- config->num_passsync_mgrs = i;
slapi_entry_free(config_entry);
@@ -289,6 +290,7 @@ free_and_error:
free(config->pref_encsalts);
free(config->supp_encsalts);
slapi_ch_array_free(config->passsync_mgrs);
+ slapi_ch_array_free(config->sysacct_mgrs);
free(config);
}
slapi_entry_free(config_entry);
@@ -614,6 +616,12 @@ int ipapwd_CheckPolicy(struct ipapwd_data *data)
switch(data->changetype) {
case IPA_CHANGETYPE_NORMAL:
+ case IPA_CHANGETYPE_SYSACCT:
+ /*
+ * Treat a system account-initiated password change as the user's
+ * initiated one as well, to force password quality checks on them.
+ */
+
/* Find the entry with the password policy */
ret = ipapwd_getPolicy(data->dn, data->target, &pol);
if (ret) {
@@ -1143,4 +1151,3 @@ int ipapwd_check_max_pwd_len(size_t len, char **errMesg) {
}
return 0;
}
-
diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c
index 43c31becae45c1c91c7c2adf498aedbd05af9a69..ca48a12a68ffeca8dcb3f0ed46d789973aab2192 100644
--- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c
+++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c
@@ -583,10 +583,11 @@ parse_req_done:
(strcasecmp(ipa_changepw_principal_dn, bindDN) != 0)) {
pwdata.changetype = IPA_CHANGETYPE_ADMIN;
- for (size_t i = 0; i < krbcfg->num_passsync_mgrs; i++) {
- if (strcasecmp(krbcfg->passsync_mgrs[i], bindDN) == 0) {
- pwdata.changetype = IPA_CHANGETYPE_DSMGR;
- break;
+ if (slapi_ch_array_utf8_inlist(krbcfg->passsync_mgrs, bindDN) == 1) {
+ pwdata.changetype = IPA_CHANGETYPE_DSMGR;
+ } else {
+ if (slapi_ch_array_utf8_inlist(krbcfg->sysacct_mgrs, bindDN) == 1) {
+ pwdata.changetype = IPA_CHANGETYPE_SYSACCT;
}
}
}
diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h
index 97697000674d8fbbe3a924af63261482db173852..c2682a7ba06962147414636484cc425850398cc4 100644
--- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h
+++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h
@@ -75,6 +75,7 @@
#define IPA_CHANGETYPE_NORMAL 0
#define IPA_CHANGETYPE_ADMIN 1
#define IPA_CHANGETYPE_DSMGR 2
+#define IPA_CHANGETYPE_SYSACCT 3
struct ipapwd_data {
Slapi_Entry *target;
@@ -108,7 +109,7 @@ struct ipapwd_krbcfg {
int num_pref_encsalts;
krb5_key_salt_tuple *pref_encsalts;
char **passsync_mgrs;
- int num_passsync_mgrs;
+ char **sysacct_mgrs;
bool allow_nt_hash;
bool enforce_ldap_otp;
};
diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
index 42e880fd0a5c8b4708b145b340209eb218f60c4e..0fdb7840bbe3d800270f60c58c1438a2d8267ba2 100644
--- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
+++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/prepost.c
@@ -368,10 +368,12 @@ static int ipapwd_pre_add(Slapi_PBlock *pb)
slapi_pblock_get(pb, SLAPI_CONN_DN, &binddn);
/* if it is a passsync manager we also need to skip resets */
- for (size_t i = 0; i < krbcfg->num_passsync_mgrs; i++) {
- if (strcasecmp(krbcfg->passsync_mgrs[i], binddn) == 0) {
- pwdop->pwdata.changetype = IPA_CHANGETYPE_DSMGR;
- break;
+ if (slapi_ch_array_utf8_inlist(krbcfg->passsync_mgrs, binddn) == 1) {
+ pwdop->pwdata.changetype = IPA_CHANGETYPE_DSMGR;
+ } else {
+ /* if it is a system account allowed to skip resets, mark it so */
+ if (slapi_ch_array_utf8_inlist(krbcfg->sysacct_mgrs, binddn) == 1) {
+ pwdop->pwdata.changetype = IPA_CHANGETYPE_SYSACCT;
}
}
}
@@ -861,10 +863,12 @@ static int ipapwd_pre_mod(Slapi_PBlock *pb)
pwdop->pwdata.changetype = IPA_CHANGETYPE_ADMIN;
/* if it is a passsync manager we also need to skip resets */
- for (size_t i = 0; i < krbcfg->num_passsync_mgrs; i++) {
- if (strcasecmp(krbcfg->passsync_mgrs[i], binddn) == 0) {
- pwdop->pwdata.changetype = IPA_CHANGETYPE_DSMGR;
- break;
+ if (slapi_ch_array_utf8_inlist(krbcfg->passsync_mgrs, binddn) == 1) {
+ pwdop->pwdata.changetype = IPA_CHANGETYPE_DSMGR;
+ } else {
+ /* if it is a system account allowed to skip resets, mark it so */
+ if (slapi_ch_array_utf8_inlist(krbcfg->sysacct_mgrs, binddn) == 1) {
+ pwdop->pwdata.changetype = IPA_CHANGETYPE_SYSACCT;
}
}
}
--
2.51.1

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,462 @@
From ce907c2d805632e7d1aeb46363e37efd81b6ad04 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy@redhat.com>
Date: Thu, 18 Sep 2025 19:31:17 +0300
Subject: [PATCH] sysaccounts: add integration test
The integration test was created with the help of claude.ai by
providing the following prompt:
>> given the design document in doc/designs/sysaccounts.md, write an
>> integration test for sysaccounts module
Fixes: https://pagure.io/freeipa/issue/9842
Assisted-by: Claude <noreply@anthropic.com>
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: Rafael Guterres Jeffman <rjeffman@redhat.com>
Reviewed-By: Thomas Woerner <twoerner@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
---
ipatests/test_integration/test_sysaccounts.py | 429 ++++++++++++++++++
1 files changed, 429 insertions(+)
create mode 100644 ipatests/test_integration/test_sysaccounts.py
diff --git a/ipatests/test_integration/test_sysaccounts.py b/ipatests/test_integration/test_sysaccounts.py
new file mode 100644
index 0000000000000000000000000000000000000000..e19b1ae18633b95b14ccd6f94f863f4224ebde64
--- /dev/null
+++ b/ipatests/test_integration/test_sysaccounts.py
@@ -0,0 +1,429 @@
+# Copyright (C) 2025 Red Hat
+# see file 'COPYING' for use and warranty information
+"""Tests for FreeIPA system accounts functionality"""
+
+import time
+from ipatests.test_integration.base import IntegrationTest
+from ipatests.pytest_ipa.integration import tasks
+from ipapython.ipautil import ipa_generate_password
+
+
+def extract_password_from_result(result):
+ # Extract password from creation output
+ password_line = [
+ line
+ for line in result.stdout_text.split("\n")
+ if "Random password:" in line
+ ][0]
+ return password_line.split("Random password:")[1].strip()
+
+
+class TestSystemAccounts(IntegrationTest):
+ """Integration tests for system accounts functionality"""
+
+ topology = "line"
+ num_clients = 0
+ num_replicas = 0
+
+ def test_system_account_lifecycle(self):
+ """Test basic system account lifecycle operations"""
+ sysaccount_name = "test-app"
+ tasks.kinit_admin(self.master)
+
+ # Test creating a system account with random password
+ result = self.master.run_command(
+ ["ipa", "sysaccount-add", sysaccount_name, "--random"]
+ )
+ assert (
+ f'Added system account "{sysaccount_name}"' in result.stdout_text
+ )
+ assert "Random password:" in result.stdout_text
+
+ # Test finding system accounts
+ result = self.master.run_command(["ipa", "sysaccount-find"])
+ assert sysaccount_name in result.stdout_text
+
+ # Test showing system account details
+ result = self.master.run_command(
+ ["ipa", "sysaccount-show", sysaccount_name]
+ )
+ assert f"System account ID: {sysaccount_name}" in result.stdout_text
+
+ # Test disabling system account
+ result = self.master.run_command(
+ ["ipa", "sysaccount-disable", sysaccount_name]
+ )
+ assert (
+ f'Disabled system account "{sysaccount_name}"'
+ in result.stdout_text
+ )
+
+ # Test enabling system account
+ result = self.master.run_command(
+ ["ipa", "sysaccount-enable", sysaccount_name]
+ )
+ assert (
+ f'Enabled system account "{sysaccount_name}"' in result.stdout_text
+ )
+
+ # Clean up
+ self.master.run_command(["ipa", "sysaccount-del", sysaccount_name])
+
+ # Verify deletion
+ result = self.master.run_command(
+ ["ipa", "sysaccount-show", sysaccount_name], raiseonerr=False
+ )
+ assert result.returncode != 0
+
+ def test_system_account_ldap_bind(self):
+ """Test LDAP bind functionality with system accounts"""
+ sysaccount_name = "ldap-test-app"
+ basedn = str(self.master.domain.basedn)
+ tasks.kinit_admin(self.master)
+
+ # Create system account
+ result = self.master.run_command(
+ ["ipa", "sysaccount-add", sysaccount_name, "--random"]
+ )
+
+ # Extract password from creation output
+ password = extract_password_from_result(result)
+
+ bind_dn = f"uid={sysaccount_name},cn=sysaccounts,cn=etc,{basedn}"
+
+ # Test LDAP bind with the system account
+ ldap_uri = f"ldap://{self.master.hostname}"
+ result = self.master.run_command(
+ [
+ "ldapsearch",
+ "-D", bind_dn,
+ "-w", password,
+ "-H", ldap_uri,
+ "-b", basedn,
+ "-s", "base",
+ "(objectclass=*)",
+ "dn",
+ ]
+ )
+ assert result.returncode == 0
+ assert basedn in result.stdout_text
+
+ # Clean up
+ self.master.run_command(["ipa", "sysaccount-del", sysaccount_name])
+
+ def test_system_account_password_management_without_reset(self):
+ """Test system account password management without triggering reset"""
+ sysaccount_name = "password-mgmt-app"
+ test_user = "sysaccount-test-user"
+ dashed_domain = self.master.domain.realm.replace(".", "-")
+ basedn = str(self.master.domain.basedn)
+ tasks.kinit_admin(self.master)
+
+ try:
+ # Create test user
+ self.master.run_command(
+ [
+ "ipa",
+ "user-add", test_user,
+ "--first", "Test",
+ "--last", "User",
+ "--random",
+ ]
+ )
+
+ # Create system account
+ result = self.master.run_command(
+ ["ipa", "sysaccount-add", sysaccount_name, "--random"]
+ )
+
+ # Extract password from creation output
+ sysaccount_password = extract_password_from_result(result)
+
+ # Create privilege for password changes
+ privilege_name = f"{sysaccount_name}-password-privilege"
+ self.master.run_command(["ipa", "privilege-add", privilege_name])
+
+ # Add permission to privilege
+ self.master.run_command(
+ [
+ "ipa",
+ "privilege-add-permission", privilege_name,
+ "--permission", "System: Change User password",
+ ]
+ )
+
+ # Create role
+ role_name = f"{sysaccount_name}-role"
+ self.master.run_command(["ipa", "role-add", role_name])
+
+ # Add privilege to role
+ self.master.run_command(
+ [
+ "ipa",
+ "role-add-privilege", role_name,
+ "--privilege", privilege_name,
+ ]
+ )
+
+ # Add system account to role
+ self.master.run_command(
+ [
+ "ipa",
+ "role-add-member", role_name,
+ "--sysaccounts", sysaccount_name,
+ ]
+ )
+
+ # Enable privileged password changes for the system account
+ result = self.master.run_command(
+ [
+ "ipa",
+ "sysaccount-policy", sysaccount_name,
+ "--privileged=true",
+ ]
+ )
+ assert (
+ "Restart the Directory Server services" in result.stderr_text
+ )
+
+ # Restart directory server to apply changes
+ self.master.run_command(
+ ["systemctl", "restart", f"dirsrv@{dashed_domain}"]
+ )
+
+ # Wait for the service to restart
+ time.sleep(15)
+
+ # Set user password to a known value first
+ initial_password = ipa_generate_password()
+ self.master.run_command(
+ ["ipa", "user-mod", test_user, "--password"],
+ stdin_text=f"{initial_password}\n{initial_password}\n",
+ )
+
+ # Change user password using system account via ldappasswd
+ new_password = ipa_generate_password()
+ sysaccount_dn = (
+ f"uid={sysaccount_name},cn=sysaccounts,cn=etc,{basedn}"
+ )
+ user_dn = (
+ f"uid={test_user},cn=users,cn=accounts,{basedn}"
+ )
+
+ result = self.master.run_command(
+ [
+ "ldappasswd",
+ "-D", sysaccount_dn,
+ "-w", sysaccount_password,
+ "-a", initial_password,
+ "-s", new_password,
+ "-x",
+ "-ZZ",
+ "-H",
+ f"ldap://{self.master.hostname}",
+ user_dn,
+ ]
+ )
+ assert result.returncode == 0
+
+ # Verify password was changed and no reset is required
+ user_details = self.master.run_command(
+ ["ipa", "user-show", test_user, "--all", "--raw"]
+ )
+
+ # The key test:
+ # verify that krbLastPwdChange != krbPasswordExpiration
+ # If they are equal, it means password reset is required
+ krb_last_pwd_change = None
+ krb_pwd_expiration = None
+
+ for line in user_details.stdout_text.split("\n"):
+ if "krbLastPwdChange:" in line:
+ krb_last_pwd_change = line.split(":", 1)[1].strip()
+ elif "krbPasswordExpiration:" in line:
+ krb_pwd_expiration = line.split(":", 1)[1].strip()
+
+ # If system account privileged password change worked,
+ # these values should be different
+ if krb_last_pwd_change and krb_pwd_expiration:
+ assert krb_last_pwd_change != krb_pwd_expiration, (
+ "Password reset was not bypassed - "
+ "krbLastPwdChange equals krbPasswordExpiration"
+ )
+
+ # Test user can authenticate with new password
+ self.master.run_command(["kdestroy", "-A"])
+ result = self.master.run_command(
+ ["kinit", test_user], stdin_text=f"{new_password}\n"
+ )
+ assert result.returncode == 0
+
+ finally:
+ # Clean up
+ self.master.run_command(["kdestroy", "-A"])
+ tasks.kinit_admin(self.master)
+
+ # Remove system account policy
+ self.master.run_command(
+ [
+ "ipa",
+ "sysaccount-policy", sysaccount_name,
+ "--privileged=false",
+ ],
+ raiseonerr=False,
+ )
+
+ # Clean up entities in reverse order
+ self.master.run_command(
+ ["ipa", "role-del", role_name], raiseonerr=False
+ )
+ self.master.run_command(
+ ["ipa", "privilege-del", privilege_name], raiseonerr=False
+ )
+ self.master.run_command(
+ ["ipa", "sysaccount-del", sysaccount_name], raiseonerr=False
+ )
+ self.master.run_command(
+ ["ipa", "user-del", test_user], raiseonerr=False
+ )
+
+ # Restart directory server to apply changes
+ self.master.run_command(
+ ["systemctl", "restart", f"dirsrv@{dashed_domain}"]
+ )
+
+ # Wait for the service to restart
+ time.sleep(15)
+
+ def test_system_account_role_membership(self):
+ """Test system account membership in roles"""
+ sysaccount_name = "role-test-app"
+ role_name = "test-sysaccount-role"
+ tasks.kinit_admin(self.master)
+
+ try:
+ # Create system account
+ self.master.run_command(
+ ["ipa", "sysaccount-add", sysaccount_name, "--random"]
+ )
+
+ # Create a test role
+ self.master.run_command(["ipa", "role-add", role_name])
+
+ # Add system account to role
+ result = self.master.run_command(
+ [
+ "ipa",
+ "role-add-member", role_name,
+ "--sysaccounts", sysaccount_name,
+ ]
+ )
+ assert "Number of members added 1" in result.stdout_text
+
+ # Verify membership
+ result = self.master.run_command(["ipa", "role-show", role_name])
+ assert sysaccount_name in result.stdout_text
+
+ # Verify from system account perspective
+ result = self.master.run_command(
+ ["ipa", "sysaccount-show", sysaccount_name]
+ )
+ assert role_name in result.stdout_text
+
+ # Remove system account from role
+ result = self.master.run_command(
+ [
+ "ipa",
+ "role-remove-member", role_name,
+ "--sysaccounts", sysaccount_name,
+ ]
+ )
+ assert "Number of members removed 1" in result.stdout_text
+
+ finally:
+ # Clean up
+ self.master.run_command(
+ ["ipa", "role-del", role_name], raiseonerr=False
+ )
+ self.master.run_command(
+ ["ipa", "sysaccount-del", sysaccount_name], raiseonerr=False
+ )
+
+ def test_required_system_accounts_protection(self):
+ """Test that required system accounts cannot be deleted"""
+ required_accounts = ["passsync", "sudo"]
+ tasks.kinit_admin(self.master)
+
+ for account in required_accounts:
+ # Try to delete a required system account
+ result = self.master.run_command(
+ ["ipa", "sysaccount-del", account], raiseonerr=False
+ )
+ assert result.returncode != 0
+ assert "is required by the IPA master" in result.stderr_text
+
+ def test_system_account_password_validation(self):
+ """Test system account password validation"""
+ sysaccount_name = "password-validation-test"
+ tasks.kinit_admin(self.master)
+
+ # Test creation with explicit password
+ test_password = ipa_generate_password()
+ result = self.master.run_command(
+ ["ipa", "sysaccount-add", sysaccount_name, "--password"],
+ stdin_text=f"{test_password}\n{test_password}\n",
+ )
+ assert (
+ f'Added system account "{sysaccount_name}"' in result.stdout_text
+ )
+
+ # Clean up
+ self.master.run_command(["ipa", "sysaccount-del", sysaccount_name])
+
+ def test_system_account_implicit_password(self):
+ """Test system account password validation"""
+ sysaccount_name = "password-implicit-validation-test"
+ tasks.kinit_admin(self.master)
+
+ # Test creation with implicitly asked password
+ test_password = ipa_generate_password()
+ result = self.master.run_command(
+ ["ipa", "sysaccount-add", sysaccount_name],
+ stdin_text=f"{test_password}\n{test_password}\n",
+ )
+ assert (
+ f'Added system account "{sysaccount_name}"' in result.stdout_text
+ )
+
+ # Clean up
+ self.master.run_command(["ipa", "sysaccount-del", sysaccount_name])
+
+ def test_system_account_modify_password(self):
+ """Test modifying system account password"""
+ sysaccount_name = "modify-password-test"
+ tasks.kinit_admin(self.master)
+
+ try:
+ # Create system account
+ self.master.run_command(
+ ["ipa", "sysaccount-add", sysaccount_name, "--random"]
+ )
+
+ # Modify with new random password
+ result = self.master.run_command(
+ ["ipa", "sysaccount-mod", sysaccount_name, "--random"]
+ )
+ assert "Random password:" in result.stdout_text
+
+ # Modify with explicit password
+ new_password = ipa_generate_password()
+ self.master.run_command(
+ ["ipa", "sysaccount-mod", sysaccount_name, "--password"],
+ stdin_text=f"{new_password}\n{new_password}\n",
+ )
+
+ finally:
+ # Clean up
+ self.master.run_command(
+ ["ipa", "sysaccount-del", sysaccount_name], raiseonerr=False
+ )
--
2.51.1

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,101 @@
From 517fe4cfee29da7531728b0a508c6162935e5597 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy@redhat.com>
Date: Wed, 5 Nov 2025 16:16:21 +0200
Subject: [PATCH] sysaccount: make sure nsaccountlock is always present
Commands sysaccount-enable/sysaccount-disable allow to lock/unlock the
account. This is exposed via operational attribute `nsAccountLock`
which has to be explicitly requested to query the state. However, if
account wasn't explicitly disabled before, `nsAccountLock` will not be
returned by the LDAP server.
Ensure the nsaccountlock attribute is always retrieved, validated, and
normalized across sysaccount operations.
- Invoke validate_nsaccountlock in create and modify pre-callbacks to
enforce valid values
- Invoke convert_nsaccountlock in create, modify, list, reset, and
policy post-callbacks to normalize the nsaccountlock attribute and
default to False in case it is absent.
Fixes: https://pagure.io/freeipa/issue/9842
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Thomas Woerner <twoerner@redhat.com>
---
ipaserver/plugins/sysaccounts.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/ipaserver/plugins/sysaccounts.py b/ipaserver/plugins/sysaccounts.py
index e2dd820efc2e83ca79f8a2badbca2013b1d40c5d..a67d8b2fb17a9bd1590f95bf661d8f1b0453c807 100644
--- a/ipaserver/plugins/sysaccounts.py
+++ b/ipaserver/plugins/sysaccounts.py
@@ -15,6 +15,7 @@ from .baseldap import (
LDAPSearch,
LDAPRetrieve,
LDAPQuery)
+from .baseuser import validate_nsaccountlock, convert_nsaccountlock
from ipalib import _, ngettext
from ipalib import constants
from ipalib import output
@@ -285,6 +286,7 @@ class sysaccount_add(LDAPCreate):
error=_('Either --password or --random is required')
)
check_userpassword(entry_attrs, **options)
+ validate_nsaccountlock(entry_attrs)
return dn
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
@@ -296,6 +298,7 @@ class sysaccount_add(LDAPCreate):
except errors.NotGroupMember:
pass
self.add_message(SystemAccountUsage(uid=keys[0], dn=dn))
+ convert_nsaccountlock(entry_attrs)
return dn
@@ -353,12 +356,15 @@ class sysaccount_mod(LDAPUpdate):
if 'privileged' not in options:
self.allow_empty_update = False
+ validate_nsaccountlock(entry_attrs)
+
return dn
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
assert isinstance(dn, DN)
fill_randompassword(entry_attrs, **options)
entry_attrs['privileged'] = getattr(context, 'privileged')
+ convert_nsaccountlock(entry_attrs)
return dn
@@ -382,6 +388,7 @@ class sysaccount_find(LDAPSearch):
self.obj.get_password_attributes(ldap, entry_attrs.dn, entry_attrs)
self.obj.handle_reset(self, self,
ldap, entry_attrs.dn, entry_attrs, **options)
+ convert_nsaccountlock(entry_attrs)
return truncated
@@ -398,6 +405,7 @@ class sysaccount_show(LDAPRetrieve):
self.obj.get_password_attributes(ldap, dn, entry_attrs)
self.obj.handle_reset(self, self,
ldap, dn, entry_attrs, **options)
+ convert_nsaccountlock(entry_attrs)
return dn
@@ -413,6 +421,7 @@ class sysaccount_policy(LDAPRetrieve):
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
self.obj.handle_reset(self, self, ldap, dn, entry_attrs, **options)
+ convert_nsaccountlock(entry_attrs)
return dn
--
2.51.1

View File

@ -0,0 +1,146 @@
From 8e9e516299883e5f4f820c3a3c444513b896a36a Mon Sep 17 00:00:00 2001
From: Florence Blanc-Renaud <flo@redhat.com>
Date: Mon, 10 Nov 2025 17:59:22 +0100
Subject: [PATCH] test_sudo: do not clean the cache for offline cache tests
The tests for offline caching should not clear SSSD cache
before shutting down the IPA server. Currently most of them
follow the same steps:
- create the rule
- clear the cache
- call run_as_sudo_user (which clears the cache)
- call list_sudo_commands (which clears the cache)
- stop the master
- call run_as_sudo_user with skip_sssd_cache_clear
- call list_sudo_commands with skip_sssd_cache_clear
The scenario is wrong as skip_sssd_cache_clear should also be
added on the calls before the master is stopped.
Fixes: https://pagure.io/freeipa/issue/9874
Signed-off-by: Florence Blanc-Renaud <flo@redhat.com>
Reviewed-By: PRANAV THUBE <pthube@redhat.com>
---
ipatests/test_integration/test_sudo.py | 38 +++++++++++++++++---------
1 file changed, 25 insertions(+), 13 deletions(-)
diff --git a/ipatests/test_integration/test_sudo.py b/ipatests/test_integration/test_sudo.py
index ce4aafd12961c8e66373e0d6c6200799c0642302..639f3e6b6fef0842eefb9fc5c5fda445682d5baa 100644
--- a/ipatests/test_integration/test_sudo.py
+++ b/ipatests/test_integration/test_sudo.py
@@ -1343,13 +1343,15 @@ class TestSudo_Functional(IntegrationTest):
clear_sssd_cache(master)
clear_sssd_cache(self.client)
result = self.run_as_sudo_user(
- "date", sudo_user="root", su_user=self.USER_1)
+ "date", sudo_user="root", su_user=self.USER_1,
+ skip_sssd_cache_clear=True)
sys_day = self.client.run_command(
["date", "+%a"]).stdout_text.strip()
assert sys_day in result.stdout_text
- result = self.list_sudo_commands(self.USER_1)
+ result = self.list_sudo_commands(self.USER_1,
+ skip_sssd_cache_clear=True)
assert "(root) /bin/date" in result.stdout_text
stop_ipa_server(master)
@@ -1383,9 +1385,11 @@ class TestSudo_Functional(IntegrationTest):
clear_sssd_cache(self.client)
clear_sssd_cache(master)
result = self.run_as_sudo_user(
- "uname", sudo_user="root", su_user=self.USER_1)
+ "uname", sudo_user="root", su_user=self.USER_1,
+ skip_sssd_cache_clear=True)
- result = self.list_sudo_commands(self.USER_1)
+ result = self.list_sudo_commands(self.USER_1,
+ skip_sssd_cache_clear=True)
assert "(root) !/bin/uname" in result.stdout_text
stop_ipa_server(master)
@@ -1420,7 +1424,8 @@ class TestSudo_Functional(IntegrationTest):
clear_sssd_cache(master)
clear_sssd_cache(self.client)
result = self.run_as_sudo_user(
- "date", sudo_user=self.USER_2, su_user=self.USER_1)
+ "date", sudo_user=self.USER_2, su_user=self.USER_1,
+ skip_sssd_cache_clear=True)
sys_day = self.client.run_command(
["date", "+%a"]).stdout_text.strip()
@@ -1485,13 +1490,15 @@ class TestSudo_Functional(IntegrationTest):
)
clear_sssd_cache(self.client)
result = self.run_as_sudo_user(
- "date", sudo_user="root", su_user=self.USER_1)
+ "date", sudo_user="root", su_user=self.USER_1,
+ skip_sssd_cache_clear=True)
sys_day = self.client.run_command(
["date", "+%a"]).stdout_text.strip()
assert sys_day in result.stdout_text
- result = self.list_sudo_commands(self.USER_1)
+ result = self.list_sudo_commands(self.USER_1,
+ skip_sssd_cache_clear=True)
assert "(root) /bin/date" in result.stdout_text
stop_ipa_server(master)
@@ -1554,13 +1561,15 @@ class TestSudo_Functional(IntegrationTest):
clear_sssd_cache(self.client)
result = self.run_as_sudo_user(
- "date", sudo_user="root", su_user=self.USER_1)
+ "date", sudo_user="root", su_user=self.USER_1,
+ skip_sssd_cache_clear=True)
sys_day = self.client.run_command(
["date", "+%a"]).stdout_text.strip()
assert sys_day in result.stdout_text
- result = self.list_sudo_commands(self.USER_1)
+ result = self.list_sudo_commands(self.USER_1,
+ skip_sssd_cache_clear=True)
assert "(root) /bin/date" in result.stdout_text
stop_ipa_server(master)
@@ -1613,13 +1622,14 @@ class TestSudo_Functional(IntegrationTest):
clear_sssd_cache(self.client)
result = self.run_as_sudo_user(
"date", sudo_user="root", su_user=self.USER_1,
- skip_password=True)
+ skip_password=True, skip_sssd_cache_clear=True)
sys_day = self.client.run_command(
["date", "+%a"]).stdout_text.strip()
assert sys_day in result.stdout_text
- result = self.list_sudo_commands(self.USER_1)
+ result = self.list_sudo_commands(self.USER_1,
+ skip_sssd_cache_clear=True)
assert "(root) NOPASSWD: /bin/date" in result.stdout_text
stop_ipa_server(master)
@@ -1660,13 +1670,15 @@ class TestSudo_Functional(IntegrationTest):
master.run_command(["ipa", "sudorule-disable", self.SUDO_RULE])
clear_sssd_cache(self.client)
result = self.run_as_sudo_user(
- "date", sudo_user="root", su_user=self.USER_1)
+ "date", sudo_user="root", su_user=self.USER_1,
+ skip_sssd_cache_clear=True)
sys_day = self.client.run_command(
["date", "+%a"]).stdout_text.strip()
assert sys_day not in result.stdout_text
- result = self.list_sudo_commands(self.USER_1)
+ result = self.list_sudo_commands(self.USER_1,
+ skip_sssd_cache_clear=True)
assert "(root) /bin/date" not in result.stdout_text
stop_ipa_server(master)
--
2.51.1

View File

@ -0,0 +1,62 @@
From 5e87614ef408cfa89093bc2186242f1b99c0b251 Mon Sep 17 00:00:00 2001
From: Alexander Bokovoy <abokovoy@redhat.com>
Date: Wed, 12 Nov 2025 13:38:14 +0200
Subject: [PATCH] sysaccounts: extend permissions to include description and
account lock
Security Architect role was supposed to manage sysaccount objects. But
since description attribute is missing from the list of the managed
permissions, the role is unable to modify 'description' field. Same for
nsAccountLock which needs an explicit ACI.
Fixes: https://pagure.io/freeipa/issue/9875
Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com>
Reviewed-By: Florence Blanc-Renaud <flo@redhat.com>
---
ACI.txt | 4 ++--
ipaserver/plugins/sysaccounts.py | 5 +++--
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/ACI.txt b/ACI.txt
index 8db1634bc25ff616f67cc4bc7ccfa385c2d77c53..b3c87bac9fa50ae153eee8d2a0271c2585a8bf75 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -379,9 +379,9 @@ aci: (targetfilter = "(objectclass=simplesecurityobject)")(version 3.0;acl "perm
dn: cn=sysaccounts,cn=etc,dc=ipa,dc=example
aci: (targetattr = "userpassword")(targetfilter = "(objectclass=simplesecurityobject)")(version 3.0;acl "permission:System: Check System Accounts passwords";allow (search) groupdn = "ldap:///cn=System: Check System Accounts passwords,cn=permissions,cn=pbac,dc=ipa,dc=example";)
dn: cn=sysaccounts,cn=etc,dc=ipa,dc=example
-aci: (targetattr = "userpassword")(targetfilter = "(objectclass=simplesecurityobject)")(version 3.0;acl "permission:System: Modify System Accounts";allow (write) groupdn = "ldap:///cn=System: Modify System Accounts,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+aci: (targetattr = "description || nsaccountlock || userpassword")(targetfilter = "(objectclass=simplesecurityobject)")(version 3.0;acl "permission:System: Modify System Accounts";allow (write) groupdn = "ldap:///cn=System: Modify System Accounts,cn=permissions,cn=pbac,dc=ipa,dc=example";)
dn: cn=sysaccounts,cn=etc,dc=ipa,dc=example
-aci: (targetattr = "createtimestamp || entryusn || memberof || modifytimestamp || objectclass || uid")(targetfilter = "(objectclass=simplesecurityobject)")(version 3.0;acl "permission:System: Read System Accounts";allow (compare,read,search) userdn = "ldap:///all";)
+aci: (targetattr = "createtimestamp || description || entryusn || memberof || modifytimestamp || nsaccountlock || objectclass || uid")(targetfilter = "(objectclass=simplesecurityobject)")(version 3.0;acl "permission:System: Read System Accounts";allow (compare,read,search) userdn = "ldap:///all";)
dn: cn=sysaccounts,cn=etc,dc=ipa,dc=example
aci: (targetfilter = "(objectclass=simplesecurityobject)")(version 3.0;acl "permission:System: Remove System Accounts";allow (delete) groupdn = "ldap:///cn=System: Remove System Accounts,cn=permissions,cn=pbac,dc=ipa,dc=example";)
dn: cn=topology,cn=ipa,cn=etc,dc=ipa,dc=example
diff --git a/ipaserver/plugins/sysaccounts.py b/ipaserver/plugins/sysaccounts.py
index a67d8b2fb17a9bd1590f95bf661d8f1b0453c807..ad579eeed4521d75a274a07ea70dcd6bf96d589b 100644
--- a/ipaserver/plugins/sysaccounts.py
+++ b/ipaserver/plugins/sysaccounts.py
@@ -138,7 +138,7 @@ class sysaccount(LDAPObject):
'ipapermright': {'read', 'search', 'compare'},
'ipapermdefaultattr': {
'objectclass',
- 'uid', 'memberof'
+ 'uid', 'memberof', 'nsaccountlock', 'description'
},
},
'System: Check System Accounts passwords': {
@@ -152,7 +152,8 @@ class sysaccount(LDAPObject):
},
'System: Modify System Accounts': {
'ipapermright': {'write'},
- 'ipapermdefaultattr': {'userpassword'},
+ 'ipapermdefaultattr': {'userpassword', 'description',
+ 'nsaccountlock'},
'default_privileges': {'System Accounts Administrators'},
},
'System: Remove System Accounts': {
--
2.51.1

View File

@ -0,0 +1,464 @@
From 8905bbf7a9ad7aefc9ddad9ccdbfb050c8429863 Mon Sep 17 00:00:00 2001
From: Aleksandr Sharov <asharov@redhat.com>
Date: Mon, 20 Oct 2025 21:41:19 +0200
Subject: [PATCH] Correctly recognize OID 2.5.4.97, organizationIdentifier as a
subject/issuer DN of the CA certificate
OID 2.5.4.97 added to the ATTR_NAME_BY_OID list, and cainstance during
2-step installation is re-fetching CA certificate and re-parsing OIDs
based on the list. Example:
cert issuer: <Name(C=CZ,2.5.4.97=LLCCZ-123456789,O=Corp,CN=ROOT)>
DN(cert issuer): CN=ROOT,O=Corp,organizationIdentifier=LLCCZ-123456789,C=CZ
Fixes: https://pagure.io/freeipa/issue/9866
Signed-off-by: Aleksandr Sharov <asharov@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
ipapython/dn.py | 1 +
ipaserver/install/cainstance.py | 10 +-
ipatests/test_integration/test_external_ca.py | 372 ++++++++++++++++++
3 files changed, 381 insertions(+), 2 deletions(-)
diff --git a/ipapython/dn.py b/ipapython/dn.py
index 8974b420cb2c2d823f8ea45ee05cfd1f0c1a3954..12870d59143e89ecb729aa059218a40c368efd64 100644
--- a/ipapython/dn.py
+++ b/ipapython/dn.py
@@ -1462,4 +1462,5 @@ ATTR_NAME_BY_OID = {
cryptography.x509.ObjectIdentifier('2.5.4.9'): 'STREET',
cryptography.x509.ObjectIdentifier('2.5.4.17'): 'postalCode',
cryptography.x509.ObjectIdentifier('0.9.2342.19200300.100.1.1'): 'UID',
+ cryptography.x509.ObjectIdentifier('2.5.4.97'): 'organizationIdentifier',
}
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index b3cc0b262e8c7c770b521b25ffed2b1fe97b81b5..b2a0a76a402ca88d873d2e1dd423f637686b0fba 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -2352,10 +2352,16 @@ def ensure_ipa_authority_entry():
api.Backend.ra_lightweight_ca.override_port = 8443
with api.Backend.ra_lightweight_ca as lwca:
data = lwca.read_ca('host-authority')
+ # Loading certificate to properly re-parse issuer and subject DNs in
+ # case CA doesn't recognize some of the OIDs. DN class will re-access
+ # the OIDs based on ATTR_NAME_BY_OID list.
+ cert_data = lwca.read_ca_cert('host-authority')
+ cert = x509.load_der_x509_certificate(cert_data)
+
attrs = dict(
ipacaid=data['id'],
- ipacaissuerdn=data['issuerDN'],
- ipacasubjectdn=data['dn'],
+ ipacaissuerdn=DN(cert.issuer),
+ ipacasubjectdn=DN(cert.subject),
)
api.Backend.ra_lightweight_ca.override_port = None
diff --git a/ipatests/test_integration/test_external_ca.py b/ipatests/test_integration/test_external_ca.py
index aeb08aae725af080dcd7807857b94c23094c3155..a6a088537dbfbb389658f89f9b07fa5323a05ed8 100644
--- a/ipatests/test_integration/test_external_ca.py
+++ b/ipatests/test_integration/test_external_ca.py
@@ -23,6 +23,8 @@ import time
from cryptography import x509
from cryptography.hazmat.backends import default_backend
+from cryptography.x509.oid import ObjectIdentifier, NameOID
+from cryptography.hazmat.primitives import hashes, serialization
from ipatests.pytest_ipa.integration import tasks
from ipatests.test_integration.base import IntegrationTest
@@ -108,6 +110,142 @@ def check_ipaca_issuerDN(host, expected_dn):
assert "Issuer DN: {}".format(expected_dn) in result.stdout_text
+def create_external_ca_with_subject(subject_attrs):
+ """
+ Create an external CA with custom subject attributes including non-standard
+ OIDs.
+
+ :param subject_attrs: List of x509.NameAttribute objects to include in
+ subject
+ :return: Tuple of (ExternalCA object, root CA certificate as PEM bytes)
+
+ Example:
+ subj_attrs = [
+ x509.NameAttribute(NameOID.COMMON_NAME, 'My CA'),
+ x509.NameAttribute(NameOID.COUNTRY_NAME, 'US'),
+ x509.NameAttribute(ObjectIdentifier('2.5.4.97'), 'VATEU-123456789')
+ ]
+ external_ca, root_ca_pem = create_external_ca_with_subject(subj_attrs)
+ signed_cert = external_ca.sign_csr(csr_data)
+ """
+ external_ca = ExternalCA()
+ external_ca.create_ca_key()
+
+ # Create the custom subject
+ subject = x509.Name(subject_attrs)
+ external_ca.issuer = subject
+
+ # Build the root CA certificate
+ builder = x509.CertificateBuilder()
+ builder = builder.subject_name(subject)
+ builder = builder.issuer_name(subject) # self-signed
+ builder = builder.public_key(external_ca.ca_public_key)
+ builder = builder.serial_number(x509.random_serial_number())
+ builder = builder.not_valid_before(external_ca.now)
+ builder = builder.not_valid_after(external_ca.now + external_ca.delta)
+
+ # Add required extensions for a CA certificate
+ builder = builder.add_extension(
+ x509.KeyUsage(
+ digital_signature=False,
+ content_commitment=False,
+ key_encipherment=False,
+ data_encipherment=False,
+ key_agreement=False,
+ key_cert_sign=True,
+ crl_sign=True,
+ encipher_only=False,
+ decipher_only=False,
+ ),
+ critical=True,
+ )
+
+ builder = builder.add_extension(
+ x509.BasicConstraints(ca=True, path_length=None),
+ critical=True,
+ )
+
+ builder = builder.add_extension(
+ x509.SubjectKeyIdentifier.from_public_key(
+ external_ca.ca_public_key
+ ),
+ critical=False,
+ )
+
+ builder = builder.add_extension(
+ x509.AuthorityKeyIdentifier.from_issuer_public_key(
+ external_ca.ca_public_key
+ ),
+ critical=False,
+ )
+
+ # Sign the certificate
+ root_ca_cert = builder.sign(
+ external_ca.ca_key, hashes.SHA256(), default_backend()
+ )
+ root_ca_pem = root_ca_cert.public_bytes(serialization.Encoding.PEM)
+
+ return external_ca, root_ca_pem
+
+
+def find_cert_in_chain(cert_chain, subject_attrs=None, issuer_attrs=None):
+ """
+ Retrieves a certificate from a provided chain that matches specified
+ criteria. The search can be filtered using dictionaries of subject
+ attributes, issuer attributes, or a combination of both.
+
+ :param cert_chain: List of certificates to search through
+ :param subject_attrs: Dict of OID -> expected value for subject attributes
+ :param issuer_attrs: Dict of OID -> expected value for issuer attributes
+ :return: The matching certificate or None if not found
+
+ Example:
+ from cryptography.x509.oid import NameOID, ObjectIdentifier
+ org_id_oid = ObjectIdentifier("2.5.4.97")
+
+ # Find IPA CA cert with specific subject and issuer
+ cert = find_cert_in_chain(
+ ca_chain,
+ subject_attrs={
+ NameOID.COMMON_NAME: "Certificate Authority",
+ NameOID.ORGANIZATION_NAME: "EXAMPLE.TEST"
+ },
+ issuer_attrs={
+ org_id_oid: "VATEU-123456789"
+ }
+ )
+ """
+ for cert in cert_chain:
+ # Check subject attributes if provided
+ if subject_attrs:
+ subject_match = True
+ for oid, expected_value in subject_attrs.items():
+ attrs = [attr for attr in cert.subject if attr.oid == oid]
+ if not any(attr.value == expected_value for attr in attrs):
+ # This cert doesn't match, move to next cert
+ subject_match = False
+ break
+ if not subject_match:
+ continue
+
+ # Check issuer attributes if provided
+ if issuer_attrs:
+ issuer_match = True
+ for oid, expected_value in issuer_attrs.items():
+ attrs = [attr for attr in cert.issuer if attr.oid == oid]
+ if not any(attr.value == expected_value for attr in attrs):
+ # This cert doesn't match, move to next cert
+ issuer_match = False
+ break
+ if not issuer_match:
+ continue
+
+ # All specified attributes match, return this cert
+ return cert
+
+ return None
+
+
def check_mscs_extension(ipa_csr, template):
csr = x509.load_pem_x509_csr(ipa_csr, default_backend())
extensions = [
@@ -243,6 +381,133 @@ class TestExternalCAConstraints(IntegrationTest):
self.master.run_command(['ipa', 'ping'])
+class TestExternalCAInstallWithOrgId(IntegrationTest):
+ """Test 2-step installation with external CA containing
+ organizationIdentifier.
+
+ This test verifies that FreeIPA can successfully install with a 2-step
+ external CA process when the external CA certificate contains the
+ organizationIdentifier attribute (OID 2.5.4.97) in its issuer DN.
+
+ This tests the fix for DN parsing in ensure_ipa_authority_entry in
+ cainstance.py where the issuer DN must be properly parsed against
+ ATTR_NAME_BY_OID to recognize all OIDs including organizationIdentifier.
+ """
+ num_replicas = 0
+ num_clients = 0
+
+ def test_external_ca_install_with_organization_identifier(self):
+ """Test 2-step installation with organizationIdentifier (OID 2.5.4.97)
+
+ Verify that FreeIPA can successfully complete a 2-step installation
+ with an external CA that contains organizationIdentifier (OID 2.5.4.97)
+ in the issuer DN. The issuer DN should be properly parsed and stored
+ in LDAP during the ensure_ipa_authority_entry process.
+ """
+
+ # Test parameters
+ org_id_value = "VATEU-123456789"
+ org_id_oid = ObjectIdentifier("2.5.4.97") # organizationIdentifier OID
+ external_ca_cn = "External CA with OrgID"
+
+ # Step 1 of ipa-server-install
+ result = install_server_external_ca_step1(self.master)
+ assert result.returncode == 0
+
+ # Get the CSR generated by step 1
+ ipa_csr = self.master.get_file_contents(paths.ROOT_IPA_CSR)
+
+ # Create an external CA with organizationIdentifier in the subject
+ subject_attrs = [
+ x509.NameAttribute(NameOID.COMMON_NAME, external_ca_cn),
+ x509.NameAttribute(NameOID.COUNTRY_NAME, 'US'),
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'Test Organization'),
+ x509.NameAttribute(org_id_oid, org_id_value),
+ ]
+ external_ca, root_ca = create_external_ca_with_subject(subject_attrs)
+
+ # Sign the IPA CSR with the external CA that has organizationIdentifier
+ ipa_ca = external_ca.sign_csr(ipa_csr)
+
+ # Write certificates to files
+ root_ca_fname = os.path.join(
+ self.master.config.test_dir,
+ 'root_ca_with_orgid.crt'
+ )
+ ipa_ca_fname = os.path.join(
+ self.master.config.test_dir,
+ 'ipa_ca_signed_with_orgid.crt'
+ )
+
+ # Transport certificates to master
+ self.master.put_file_contents(root_ca_fname, root_ca)
+ self.master.put_file_contents(ipa_ca_fname, ipa_ca)
+
+ # Step 2 of ipa-server-install
+ # This should succeed despite organizationIdentifier in issuer DN
+ result = install_server_external_ca_step2(
+ self.master, ipa_ca_fname, root_ca_fname
+ )
+ assert result.returncode == 0
+
+ # Make sure IPA server is working properly
+ tasks.kinit_admin(self.master)
+ result = self.master.run_command(['ipa', 'user-show', 'admin'])
+ assert 'User login: admin' in result.stdout_text
+
+ # Verify IPA is functional
+ result = self.master.run_command(['ipa', 'ping'])
+ assert result.returncode == 0
+
+ # Verify the certificate chain contains the expected certificates
+ # Load all certificates from /etc/ipa/ca.crt (the CA chain)
+ ca_chain_content = self.master.get_file_contents(paths.IPA_CA_CRT)
+ ca_chain = ipa_x509.load_certificate_list(ca_chain_content)
+
+ # 1. Find and verify the IPA CA certificate
+ # It should have subject O=REALM, CN=Certificate Authority
+ # and issuer with organizationIdentifier
+ ipa_ca_cert = find_cert_in_chain(
+ ca_chain,
+ subject_attrs={
+ NameOID.COMMON_NAME: "Certificate Authority",
+ NameOID.ORGANIZATION_NAME: self.master.domain.realm
+ },
+ issuer_attrs={
+ org_id_oid: org_id_value,
+ NameOID.COMMON_NAME: external_ca_cn
+ }
+ )
+ assert ipa_ca_cert is not None, \
+ f"Did not find IPA CA certificate with subject " \
+ f"O={self.master.domain.realm}, CN=Certificate Authority " \
+ f"and issuer with organizationIdentifier={org_id_value}"
+
+ # 2. Find and verify the external root CA certificate
+ # It should be self-signed with organizationIdentifier in subject
+ external_ca_cert = find_cert_in_chain(
+ ca_chain,
+ subject_attrs={
+ NameOID.COMMON_NAME: external_ca_cn,
+ org_id_oid: org_id_value
+ },
+ issuer_attrs={
+ NameOID.COMMON_NAME: external_ca_cn,
+ org_id_oid: org_id_value
+ }
+ )
+ assert external_ca_cert is not None, \
+ f"Did not find external root CA certificate (CN={external_ca_cn})" \
+ f" with organizationIdentifier={org_id_value} in subject"
+
+ # 3. Verify the issuer DN is correctly stored in LDAP
+ # The issuer DN should contain organizationIdentifier
+ # Note: The order in the DN string representation matters
+ result = self.master.run_command(['ipa', 'ca-show', 'ipa'])
+ assert f"organizationIdentifier={org_id_value}" in result.stdout_text, \
+ "organizationIdentifier not found in IPA CA issuer DN"
+
+
def verify_caentry(host, cert):
"""
Verify the content of cn=DOMAIN IPA CA,cn=certificates,cn=ipa,cn=etc,basedn
@@ -475,6 +740,113 @@ class TestExternalCAInstall(IntegrationTest):
self.master.run_command([paths.IPA_CACERT_MANAGE, 'install',
root_ca_fname])
+ def test_renew_external_ca_with_organization_identifier(self):
+ """Test CA renewal with organizationIdentifier (OID 2.5.4.97)
+
+ Verify that FreeIPA can successfully renew CA with an external CA
+ that contains organizationIdentifier (OID 2.5.4.97) in the issuer DN.
+ The IPA CA will be signed by this external CA, and the issuer DN
+ will contain the organizationIdentifier attribute.
+ """
+
+ # Test parameters
+ org_id_value = "VATEU-123456789"
+ org_id_oid = ObjectIdentifier("2.5.4.97") # organizationIdentifier OID
+ external_ca_cn = "External CA with OrgID"
+
+ # Initiate CA renewal with external CA
+ result = self.master.run_command([paths.IPA_CACERT_MANAGE, 'renew',
+ '--external-ca'])
+ assert result.returncode == 0
+
+ # Get the CSR generated by the renewal process
+ ipa_csr = self.master.get_file_contents(paths.IPA_CA_CSR)
+
+ # Create an external CA with organizationIdentifier in the subject
+ subject_attrs = [
+ x509.NameAttribute(NameOID.COMMON_NAME, external_ca_cn),
+ x509.NameAttribute(NameOID.COUNTRY_NAME, 'US'),
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'Test Organization'),
+ x509.NameAttribute(org_id_oid, org_id_value),
+ ]
+ external_ca, root_ca = create_external_ca_with_subject(subject_attrs)
+
+ # Sign the IPA CSR with the external CA that has organizationIdentifier
+ ipa_ca = external_ca.sign_csr(ipa_csr)
+
+ # Write certificates to files
+ root_ca_fname = os.path.join(
+ self.master.config.test_dir,
+ 'root_ca_with_orgid.crt'
+ )
+ ipa_ca_fname = os.path.join(
+ self.master.config.test_dir,
+ 'ipa_ca_signed_with_orgid.crt'
+ )
+
+ # Transport certificates to master
+ self.master.put_file_contents(root_ca_fname, root_ca)
+ self.master.put_file_contents(ipa_ca_fname, ipa_ca)
+
+ # Complete the renewal with the signed certificates
+ # This should succeed despite organizationIdentifier in issuer DN
+ result = self.master.run_command([
+ paths.IPA_CACERT_MANAGE, 'renew',
+ '--external-cert-file', ipa_ca_fname,
+ '--external-cert-file', root_ca_fname
+ ])
+ assert result.returncode == 0
+
+ # Verify the CA was properly installed
+ result = self.master.run_command([paths.IPA_CERTUPDATE])
+ assert result.returncode == 0
+
+ # Verify IPA is still functional
+ tasks.kinit_admin(self.master)
+ result = self.master.run_command(['ipa', 'ping'])
+ assert result.returncode == 0
+
+ # Verify the certificate chain contains the expected certificates
+ # Load all certificates from /etc/ipa/ca.crt (the CA chain)
+ ca_chain_content = self.master.get_file_contents(paths.IPA_CA_CRT)
+ ca_chain = ipa_x509.load_certificate_list(ca_chain_content)
+
+ # 1. Find and verify the IPA CA certificate
+ # It should have subject O=REALM, CN=Certificate Authority
+ # and issuer with organizationIdentifier
+ ipa_ca_cert = find_cert_in_chain(
+ ca_chain,
+ subject_attrs={
+ NameOID.COMMON_NAME: "Certificate Authority",
+ NameOID.ORGANIZATION_NAME: self.master.domain.realm
+ },
+ issuer_attrs={
+ org_id_oid: org_id_value,
+ NameOID.COMMON_NAME: external_ca_cn
+ }
+ )
+ assert ipa_ca_cert is not None, \
+ f"Did not find IPA CA certificate with subject " \
+ f"O={self.master.domain.realm}, CN=Certificate Authority " \
+ f"and issuer with organizationIdentifier={org_id_value}"
+
+ # 2. Find and verify the external root CA certificate
+ # It should be self-signed with organizationIdentifier in subject
+ external_ca_cert = find_cert_in_chain(
+ ca_chain,
+ subject_attrs={
+ NameOID.COMMON_NAME: external_ca_cn,
+ org_id_oid: org_id_value
+ },
+ issuer_attrs={
+ NameOID.COMMON_NAME: external_ca_cn,
+ org_id_oid: org_id_value
+ }
+ )
+ assert external_ca_cert is not None, \
+ f"Did not find external root CA certificate (CN={external_ca_cn})"\
+ f" with organizationIdentifier={org_id_value} in subject"
+
class TestMultipleExternalCA(IntegrationTest):
"""Setup externally signed ca1
--
2.51.1

View File

@ -232,7 +232,7 @@
Name: %{package_name}
Version: %{IPA_VERSION}
Release: 24%{?rc_version:.%rc_version}%{?dist}
Release: 25%{?rc_version:.%rc_version}%{?dist}
Summary: The Identity, Policy and Audit system
License: GPL-3.0-or-later
@ -373,6 +373,30 @@ Patch0114: 0114-ipasam-remove-definitions-which-included-from-ndr_dr.patch
Patch0115: 0115-Enforce-uniqueness-across-krbprincipalname-and-krbca.patch
Patch0116: 0116-ipa-kdb-enforce-PAC-presence-on-TGT-for-TGS-REQ.patch
Patch0117: 0117-ipatests-extend-test-for-unique-krbcanonicalname.patch
Patch0118: 0118-Add-domain-option-to-ipa-client-automount-for-DNS-di.patch
Patch0119: 0119-ipatests-Update-ipatests-to-test-topology-with-multi.patch
Patch0120: 0120-ipatests-Add-comprehensive-tests-for-ipa-client-auto.patch
Patch0121: 0121-ipatests-remove-xfail-for-PKI-11.7.patch
Patch0122: 0122-ipatests-fix-test_otp.patch
Patch0123: 0123-ipatests-update-the-Let-s-Encrypt-cert-chain.patch
Patch0124: 0124-ipatests-fix-TestIPAMigratewithBackupRestore-setup.patch
Patch0125: 0125-Add-support-for-libpwpolicy-credit-to-password-polic.patch
Patch0126: 0126-ipatests-Refactor-and-port-trust-functional-HBAC-tes.patch
Patch0127: 0127-ipatests-skip-encrypted-dns-tests-on-fedora-41.patch
Patch0128: 0128-Extended-eDNS-testsuite-with-Relaxed-policy-testcase.patch
Patch0129: 0129-ipatest-make-test_cert-more-robust-to-replication-de.patch
Patch0130: 0130-ipatests-fix-test_certmonger_ipa_responder_jsonrpc.patch
Patch0131: 0131-test_cert-adapt-the-expect-error-message-to-PKI-11.7.patch
Patch0132: 0132-Include-the-HSM-token-name-when-creating-LWCAs.patch
Patch0133: 0133-ipatests-Refactor-and-port-trust-functional-SUDO-tes.patch
Patch0134: 0134-ipa-pwd-extop-add-SysAcctManagersDNs-support.patch
Patch0135: 0135-Add-system-accounts-sysaccounts.patch
Patch0136: 0136-sysaccounts-add-integration-test.patch
Patch0137: 0137-Port-bash-sudo-tests.patch
Patch0138: 0138-sysaccount-make-sure-nsaccountlock-is-always-present.patch
Patch0139: 0139-test_sudo-do-not-clean-the-cache-for-offline-cache-t.patch
Patch0140: 0140-sysaccounts-extend-permissions-to-include-descriptio.patch
Patch0141: 0141-Correctly-recognize-OID-2.5.4.97-organizationIdentif.patch
Patch1001: 1001-Change-branding-to-IPA-and-Identity-Management.patch
%endif
%endif
@ -2026,6 +2050,16 @@ fi
%endif
%changelog
* Wed Nov 19 2025 Florence Blanc-Renaud <flo@redhat.com> - 4.12.2-25
- Resolves: RHEL-128238 [RFE] Support storing LWCA private keys on an HSM [rhel-9]
- Resolves: RHEL-126515 RFE: Enable external password reset agents to use ipa_pwd_extop in RHEL IdM [rhel-9]
- Resolves: RHEL-73399 RFE: Update IdM password policy configurations to meet M-22-09 by restricting spaces and require number character class
- Resolves: RHEL-128241 ATTR_NAME_BY_OID is missing OID 2.5.4.97, organizationIdentifier [rhel-9]
- Resolves: RHEL-126514 [RFE] ipa-client-automount should have an option to include domain of the machine. [rhel-9]
- Resolves: RHEL-124171 Include latest fixes in python3-ipatests package
- Resolves: RHEL-120514 Include fixes in python3-ipatests [rhel-9.8]
- Resolves: RHEL-118609 test_cacert_manage fails due to expired Let's Encrypt R3 certificate
* Tue Sep 30 2025 Florence Blanc-Renaud <flo@redhat.com> - 4.12.2-24
- Resolves: RHEL-118448 CVE-2025-7493 ipa: Privilege escalation from host to domain admin in FreeIPA