public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/realmd] rawhide: sync with latest upstream patches
@ 2026-06-25 12:01 Sumit Bose
0 siblings, 0 replies; only message in thread
From: Sumit Bose @ 2026-06-25 12:01 UTC (permalink / raw)
To: git-commits
A new commit has been pushed.
Repo : rpms/realmd
Branch : rawhide
Commit : 0ba19c36de451f3447d44814218975afb38acb60
Author : Sumit Bose <sbose@redhat.com>
Date : 2026-06-25T13:42:10+02:00
Stats : +3453/-1 in 21 file(s)
URL : https://src.fedoraproject.org/rpms/realmd/c/0ba19c36de451f3447d44814218975afb38acb60?branch=rawhide
Log:
sync with latest upstream patches
---
diff --git a/0001-Tests-initial-framework-and-tests-for-realm.patch b/0001-Tests-initial-framework-and-tests-for-realm.patch
new file mode 100644
index 0000000..99b5816
--- /dev/null
+++ b/0001-Tests-initial-framework-and-tests-for-realm.patch
@@ -0,0 +1,538 @@
+From 2c9f029ccc486449878742420b524d3576ceb692 Mon Sep 17 00:00:00 2001
+From: shridhargadekar <shridhar.always@gmail.com>
+Date: Mon, 2 Jun 2025 22:07:48 +0530
+Subject: [PATCH 01/20] Tests: initial framework and tests for realm
+
+Initial framework related configurations. Elementary testcases
+to test fundamental methods of realm
+
+Reviewed-by: Jakub Vavra <jvavra@redhat.com>
+Reviewed-by: Sumit Bose <sbose@redhat.com>
+---
+ tests/__init__.py | 0
+ tests/conftest.py | 20 +++++++
+ tests/mhc.yaml | 48 +++++++++++++++
+ tests/pyproject.toml | 18 ++++++
+ tests/pytest.ini | 15 +++++
+ tests/readme.pytest | 28 +++++++++
+ tests/requirements.txt | 7 +++
+ tests/setup.cfg | 12 ++++
+ tests/test_realmd.py | 70 ++++++++++++++++++++++
+ tests/topology.py | 109 +++++++++++++++++++++++++++++++++
+ tests/topology_controllers.py | 110 ++++++++++++++++++++++++++++++++++
+ 11 files changed, 437 insertions(+)
+ create mode 100644 tests/__init__.py
+ create mode 100644 tests/conftest.py
+ create mode 100644 tests/mhc.yaml
+ create mode 100644 tests/pyproject.toml
+ create mode 100644 tests/pytest.ini
+ create mode 100644 tests/readme.pytest
+ create mode 100644 tests/requirements.txt
+ create mode 100644 tests/setup.cfg
+ create mode 100644 tests/test_realmd.py
+ create mode 100644 tests/topology.py
+ create mode 100644 tests/topology_controllers.py
+
+diff --git a/tests/__init__.py b/tests/__init__.py
+new file mode 100644
+index 0000000..e69de29
+diff --git a/tests/conftest.py b/tests/conftest.py
+new file mode 100644
+index 0000000..c53ae5f
+--- /dev/null
++++ b/tests/conftest.py
+@@ -0,0 +1,20 @@
++# Configuration file for multihost tests.
++
++from __future__ import annotations
++
++from pytest_mh import MultihostPlugin
++from sssd_test_framework.config import SSSDMultihostConfig
++
++# Load additional plugins
++pytest_plugins = (
++ "pytest_importance",
++ "pytest_mh",
++ "pytest_ticket",
++ "sssd_test_framework.fixtures",
++ "sssd_test_framework.markers",
++)
++
++
++def pytest_plugin_registered(plugin) -> None:
++ if isinstance(plugin, MultihostPlugin):
++ plugin.config_class = SSSDMultihostConfig
+diff --git a/tests/mhc.yaml b/tests/mhc.yaml
+new file mode 100644
+index 0000000..176173f
+--- /dev/null
++++ b/tests/mhc.yaml
+@@ -0,0 +1,48 @@
++domains:
++- id: sssd
++ hosts:
++ - hostname: client.test
++ role: client
++ artifacts:
++ - /etc/sssd/*
++ - /var/log/sssd/*
++ - /var/lib/sss/db/*
++
++ - hostname: dc.ad.test
++ role: ad
++ os:
++ family: windows
++ conn:
++ type: ssh
++ username: Administrator@ad.test
++ password: vagrant
++ config:
++ adminpw: vagrant
++ client:
++ ad_domain: ad.test
++
++ - hostname: dc.samba.test
++ role: samba
++ config:
++ binddn: CN=Administrator,CN=Users,DC=samba,DC=test
++ bindpw: Secret123
++ client:
++ ad_domain: samba.test
++ krb5_keytab: /var/enrollment/samba.test.keytab
++ ldap_krb5_keytab: /var/enrollment/samba.test.keytab
++
++ - hostname: master.ipa.test
++ role: ipa
++ config:
++ client:
++ ipa_domain: ipa.test
++ krb5_keytab: /var/enrollment/ipa.test.keytab
++ ldap_krb5_keytab: /var/enrollment/ipa.test.keytab
++ artifacts:
++ - /etc/sssd/*
++ - /var/log/dirsrv/*
++ - /var/log/httpd/*
++ - /var/log/ipa/*
++ - /var/log/krb5kdc.log
++ - /var/log/sssd/*
++ - /var/lib/sss/db/*
+diff --git a/tests/pyproject.toml b/tests/pyproject.toml
+new file mode 100644
+index 0000000..3cfce46
+--- /dev/null
++++ b/tests/pyproject.toml
+@@ -0,0 +1,18 @@
++[tool.mypy]
++exclude = "docs"
++
++[[tool.mypy.overrides]]
++module = "jc.*"
++ignore_missing_imports = true
++
++[[tool.mypy.overrides]]
++module = "ldap.*"
++ignore_missing_imports = true
++
++[tool.isort]
++line_length = 119
++profile = "black"
++add_imports = "from __future__ import annotations"
++
++[tool.black]
++line-length = 119
+diff --git a/tests/pytest.ini b/tests/pytest.ini
+new file mode 100644
+index 0000000..397817d
+--- /dev/null
++++ b/tests/pytest.ini
+@@ -0,0 +1,15 @@
++# For marker descriptions please look at https://tests.sssd.io/en/latest/marks.html
++[pytest]
++addopts = --strict-markers
++testpaths = .
++markers =
++ discover:
++ list:
++ join:
++ leave:
++ contains_workaround_for(gh=...,bz=...):
++ permit:
++ deny:
++ config:
++ticket_tools = bz,gh,jira
++
+diff --git a/tests/readme.pytest b/tests/readme.pytest
+new file mode 100644
+index 0000000..93b1c1e
+--- /dev/null
++++ b/tests/readme.pytest
+@@ -0,0 +1,28 @@
++# realm integration tests
++
++These tests will cover test scenarios for realm perform actions on a Domain-Controller.
++
++## Set up environment and install requirements
++** Refer following to set up environment
++```
++https://tests.sssd.io/en/latest/running-tests.html
++https://github.com/SSSD/sssd-ci-containers
++```
++
++** Setup python virtual environment**
++```
++python3 -m venv .venv
++source .venv/bin/activate
++```
++
++** Setup sssd-test-framework **
++```
++pip install -r requirements.txt
++```
++
++## To run tests from inside realmd/tests
++```
++# pytest --mh-config=mhc.yaml --mh-lazy-ssh -v
++Or
++# pytest --mh-config=mhc.yaml --mh-log-path=/dev/stderr --pdb test_realmd.py
++````
+diff --git a/tests/requirements.txt b/tests/requirements.txt
+new file mode 100644
+index 0000000..d2c5ddf
+--- /dev/null
++++ b/tests/requirements.txt
+@@ -0,0 +1,7 @@
++flaky
++pytest
++git+https://github.com/next-actions/pytest-importance
++git+https://github.com/next-actions/pytest-mh
++git+https://github.com/next-actions/pytest-ticket
++git+https://github.com/next-actions/pytest-output
++git+https://github.com/SSSD/sssd-test-framework
+diff --git a/tests/setup.cfg b/tests/setup.cfg
+new file mode 100644
+index 0000000..8ef0a96
+--- /dev/null
++++ b/tests/setup.cfg
+@@ -0,0 +1,12 @@
++[metadata]
++description-file = readme.md
++
++[flake8]
++max-line-length = 119
++ignore = E203,W503
++exclude = .venv
++
++[pycodestyle]
++max-line-length = 119
++ignore = E203,W503
++exclude = .venv
+diff --git a/tests/test_realmd.py b/tests/test_realmd.py
+new file mode 100644
+index 0000000..c9b833d
+--- /dev/null
++++ b/tests/test_realmd.py
+@@ -0,0 +1,70 @@
++from __future__ import annotations
++
++import pytest
++import time
++import os
++import sys
++
++
++from .topology import KnownTopology, KnownTopologyGroup
++from sssd_test_framework.roles.ad import AD
++from sssd_test_framework.roles.client import Client
++from sssd_test_framework.roles.ipa import IPA
++from sssd_test_framework.roles.generic import GenericADProvider
++from sssd_test_framework.utils.realmd import RealmUtils
++
++
++@pytest.mark.importance("critical")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_discover(client: Client, provider: Any):
++ """
++ :title: realm discover a domain
++ :steps:
++ 1. Request information about a domain
++ :expectedresults:
++ 1. Information about a domain is retrieved
++ """
++ r = client.realm.discover(provider.host.domain, args=["--all", "--verbose"])
++ assert provider.host.domain in r.stdout, "realm failed to discover domain info"
++
++
++@pytest.mark.importance("critical")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_join(client: Client, provider: Any):
++ """
++ :title: realm join
++ :steps:
++ 1. Join a client system to the domain
++ :expectedresults:
++ 1. A client system joined to the domain successfully
++ """
++ r = client.realm.join(provider.host.domain, krb=False, user=provider.host.adminuser, password=provider.host.adminpw)
++ assert r.rc == 0, "realm join operation failed!"
++ assert "Successfully enrolled machine in realm" in r.stderr, "realm failed to join client to the domain"
++
++
++@pytest.mark.importance("critical")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_leave(client: Client, provider: Any):
++ """
++ :title: realm leave
++ :steps:
++ :expectedresults:
++ """
++ client.realm.join(provider.host.domain, krb=False, user=provider.host.adminuser, password=provider.host.adminpw)
++ r = client.realm.leave(provider.host.domain)
++ assert r.rc == 0, "realm leave operation failed!"
++ assert "Successfully unenrolled machine from realm" in r.stderr, "realm failed to leave domain!"
++
++
++@pytest.mark.importance("critical")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_list(client: Client, provider: Any):
++ """
++ :title: realm list
++ :steps:
++ :expectedresults:
++ """
++ r = client.realm.list(args=["--all"])
++ assert r.rc == 0, "realm list operation failed!"
++ assert provider.host.domain in r.stdout, "realm failed to list domain"
+diff --git a/tests/topology.py b/tests/topology.py
+new file mode 100644
+index 0000000..7d85910
+--- /dev/null
++++ b/tests/topology.py
+@@ -0,0 +1,109 @@
++"""SSSD predefined well-known topologies."""
++
++from __future__ import annotations
++
++from enum import unique
++from typing import final
++
++from pytest_mh import KnownTopologyBase, KnownTopologyGroupBase, Topology, TopologyDomain
++
++from sssd_test_framework.config import SSSDTopologyMark
++from .topology_controllers import (
++ ADTopologyController,
++ IPATopologyController,
++ SambaTopologyController,
++)
++
++__all__ = [
++ "KnownTopology",
++ "KnownTopologyGroup",
++]
++
++
++@final
++@unique
++class KnownTopology(KnownTopologyBase):
++ """
++ Well-known topologies that can be given to ``pytest.mark.topology``
++ directly. It is expected to use these values in favor of providing
++ custom marker values.
++
++ .. code-block:: python
++ :caption: Example usage
++
++ @pytest.mark.topology(KnownTopology.LDAP)
++ def test_ldap(client: Client, ldap: LDAP):
++ assert True
++ """
++
++ """
++ Client = SSSDTopologyMark(
++ name="client",
++ topology=Topology(TopologyDomain("sssd", client=1)),
++ controller=ClientTopologyController(),
++ fixtures=dict(client="sssd.client[0]"),
++ )
++ """
++ """
++ .. topology-mark:: KnownTopology.Client
++ """
++
++ AD = SSSDTopologyMark(
++ name="ad",
++ topology=Topology(TopologyDomain("sssd", client=1, ad=1)),
++ controller=ADTopologyController(),
++ domains=dict(test="sssd.ad[0]"),
++ fixtures=dict(client="sssd.client[0]", ad="sssd.ad[0]", provider="sssd.ad[0]"),
++ )
++ """
++ .. topology-mark:: KnownTopology.AD
++ """
++
++ Samba = SSSDTopologyMark(
++ name="samba",
++ topology=Topology(TopologyDomain("sssd", client=1, samba=1)),
++ controller=SambaTopologyController(),
++ domains={"test": "sssd.samba[0]"},
++ fixtures=dict(client="sssd.client[0]", samba="sssd.samba[0]", provider="sssd.samba[0]"),
++ )
++ """
++ .. topology-mark:: KnownTopology.Samba
++ """
++
++ IPA = SSSDTopologyMark(
++ name="ipa",
++ topology=Topology(TopologyDomain("sssd", client=1, ipa=1)),
++ controller=IPATopologyController(),
++ domains=dict(test="sssd.ipa[0]"),
++ fixtures=dict(client="sssd.client[0]", ipa="sssd.ipa[0]", provider="sssd.ipa[0]"),
++ )
++ """
++ .. topology-mark:: KnownTopology.IPA
++ """
++
++
++class KnownTopologyGroup(KnownTopologyGroupBase):
++ """
++ Groups of well-known topologies that can be given to ``pytest.mark.topology``
++ directly. It is expected to use these values in favor of providing
++ custom marker values.
++
++ The test is parametrized and runs multiple times, once per each topology.
++
++ .. code-block:: python
++ :caption: Example usage (runs on AD, IPA, LDAP and Samba topology)
++
++ @pytest.mark.topology(KnownTopologyGroup.AnyProvider)
++ def test_ldap(client: Client, provider: GenericProvider):
++ assert True
++ """
++
++ AnyAD = [KnownTopology.AD, KnownTopology.Samba]
++ """
++ .. topology-mark:: KnownTopologyGroup.AnyAD
++ """
++
++ AnyProvider = [KnownTopology.AD, KnownTopology.Samba]
++ """
++ .. topology-mark:: KnownTopologyGroup.AnyProvider
++ """
+diff --git a/tests/topology_controllers.py b/tests/topology_controllers.py
+new file mode 100644
+index 0000000..73a5c49
+--- /dev/null
++++ b/tests/topology_controllers.py
+@@ -0,0 +1,110 @@
++from __future__ import annotations
++
++from pytest_mh import BackupTopologyController
++from pytest_mh.conn import ProcessResult
++
++from sssd_test_framework.config import SSSDMultihostConfig
++from sssd_test_framework.hosts.ad import ADHost
++from sssd_test_framework.hosts.client import ClientHost
++from sssd_test_framework.hosts.samba import SambaHost
++from sssd_test_framework.hosts.ipa import IPAHost
++from sssd_test_framework.misc.ssh import retry_command
++
++__all__ = [
++ "ADTopologyController",
++ "SambaTopologyController",
++ "IPATopologyController",
++]
++
++
++class ProvisionedBackupTopologyController(BackupTopologyController[SSSDMultihostConfig]):
++ """
++ Provide basic restore functionality for topologies.
++ """
++
++ def __init__(self) -> None:
++ super().__init__()
++
++ self.provisioned: bool = False
++
++ def init(self, *args, **kwargs):
++ super().init(*args, **kwargs)
++ self.provisioned = self.name in self.multihost.provisioned_topologies
++
++ def topology_teardown(self) -> None:
++ if self.provisioned:
++ return
++
++ super().topology_teardown()
++
++ def teardown(self) -> None:
++ if self.provisioned:
++ self.restore_vanilla()
++ return
++
++ super().teardown()
++
++
++class ClientTopologyController(ProvisionedBackupTopologyController):
++ """
++ Client Topology Controller.
++ """
++
++ pass
++
++
++class ADTopologyController(ProvisionedBackupTopologyController):
++ """
++ AD Topology Controller.
++ """
++
++ @BackupTopologyController.restore_vanilla_on_error
++ def topology_setup(self, client: ClientHost, provider: ADHost) -> None:
++ if self.provisioned:
++ self.logger.info(f"Topology '{self.name}' is already provisioned")
++ return
++
++ # Remove any existing Kerberos configuration and keytab
++ client.fs.rm("/etc/krb5.conf")
++ client.fs.rm("/etc/krb5.keytab")
++
++ # Backup so we can restore to this state after each test
++ super().topology_setup()
++
++
++class IPATopologyController(ProvisionedBackupTopologyController):
++ """
++ IPA Topology Controller.
++ """
++
++ @BackupTopologyController.restore_vanilla_on_error
++ def topology_setup(self, client: ClientHost, ipa: IPAHost) -> None:
++ if self.provisioned:
++ self.logger.info(f"Topology '{self.name}' is already provisioned")
++ return
++
++ #self.logger.info(f"Enrolling {client.hostname} into {ipa.domain}")
++ self.logger.info(f"{client.hostname} into {ipa.domain}")
++ self.logger.info("**************************"*77)
++
++ # Remove any existing Kerberos configuration and keytab
++ client.fs.rm("/etc/krb5.conf")
++ client.fs.rm("/etc/krb5.keytab")
++
++ # Backup ipa-client-install files
++ client.fs.backup("/etc/ipa")
++ client.fs.backup("/var/lib/ipa-client")
++
++ # Join ipa domain
++ #client.conn.exec(["realm", "leave", ipa.domain], input=ipa.adminpw)
++
++ # Backup so we can restore to this state after each test
++ super().topology_setup()
++
++
++class SambaTopologyController(ADTopologyController):
++ """
++ Samba Topology Controller.
++ """
++
++ pass
+--
+2.54.0
+
diff --git a/0002-service-do-not-set-config_file_version-in-sssd.conf.patch b/0002-service-do-not-set-config_file_version-in-sssd.conf.patch
new file mode 100644
index 0000000..386e736
--- /dev/null
+++ b/0002-service-do-not-set-config_file_version-in-sssd.conf.patch
@@ -0,0 +1,61 @@
+From 8a1734b3feeb4bf90f9b60d6c5bb2eda4e29d749 Mon Sep 17 00:00:00 2001
+From: Sumit Bose <sbose@redhat.com>
+Date: Wed, 25 Jun 2025 09:55:39 +0200
+Subject: [PATCH 02/20] service: do not set config_file_version in sssd.conf
+
+The config_file_version option is not used by SSSD since quite some time
+and was removed recently.
+
+Resolves: https://issues.redhat.com/browse/RHEL-99877
+---
+ service/realm-sssd-config.c | 2 --
+ tests/test-sssd-config.c | 6 +++---
+ 2 files changed, 3 insertions(+), 5 deletions(-)
+
+diff --git a/service/realm-sssd-config.c b/service/realm-sssd-config.c
+index 140d7dc..d02b01f 100644
+--- a/service/realm-sssd-config.c
++++ b/service/realm-sssd-config.c
+@@ -156,8 +156,6 @@ realm_sssd_config_add_domain (RealmIniConfig *config,
+
+ /* Setup a default sssd section */
+ realm_ini_config_set_list_diff (config, "sssd", "services", ", ", services, NULL);
+- if (!realm_ini_config_have (config, "sssd", "config_file_version"))
+- realm_ini_config_set (config, "sssd", "config_file_version", "2", NULL);
+
+ domains[0] = domain;
+ domains[1] = NULL;
+diff --git a/tests/test-sssd-config.c b/tests/test-sssd-config.c
+index 8f3fec5..f0a2562 100644
+--- a/tests/test-sssd-config.c
++++ b/tests/test-sssd-config.c
+@@ -90,7 +90,7 @@ test_add_domain (Test *test,
+ gconstpointer unused)
+ {
+ const gchar *data = "[domain/one]\nval=1\n[sssd]\ndomains=one";
+- const gchar *check = "[domain/one]\nval=1\n[sssd]\ndomains = one, two\nconfig_file_version = 2\nservices = nss, pam\n\n[domain/two]\ndos = 2\n";
++ const gchar *check = "[domain/one]\nval=1\n[sssd]\ndomains = one, two\nservices = nss, pam\n\n[domain/two]\ndos = 2\n";
+ GError *error = NULL;
+ gchar *output;
+ gboolean ret;
+@@ -140,7 +140,7 @@ static void
+ test_add_domain_only (Test *test,
+ gconstpointer unused)
+ {
+- const gchar *check = "\n[sssd]\ndomains = two\nconfig_file_version = 2\nservices = nss, pam\n\n[domain/two]\ndos = 2\n";
++ const gchar *check = "\n[sssd]\ndomains = two\nservices = nss, pam\n\n[domain/two]\ndos = 2\n";
+ GError *error = NULL;
+ gchar *output;
+ gboolean ret;
+@@ -342,7 +342,7 @@ static void
+ test_remove_and_add_domain (Test *test,
+ gconstpointer unused)
+ {
+- const gchar *data = "[domain/one]\nval = 1\n\n[nss]\ndefault_shell = /bin/bash\n\n[sssd]\ndomains = one, two\nconfig_file_version = 2\nservices = nss, pam\n\n[domain/two]\nval = 2\n";
++ const gchar *data = "[domain/one]\nval = 1\n\n[nss]\ndefault_shell = /bin/bash\n\n[sssd]\ndomains = one, two\nservices = nss, pam\n\n[domain/two]\nval = 2\n";
+ GError *error = NULL;
+ gchar *output;
+ gboolean ret;
+--
+2.54.0
+
diff --git a/0003-tools-fix-help-message-for-realm-deny.patch b/0003-tools-fix-help-message-for-realm-deny.patch
new file mode 100644
index 0000000..c463313
--- /dev/null
+++ b/0003-tools-fix-help-message-for-realm-deny.patch
@@ -0,0 +1,43 @@
+From ea7c143276537aac7ae665970083234dffc9ce99 Mon Sep 17 00:00:00 2001
+From: Sumit Bose <sbose@redhat.com>
+Date: Mon, 30 Jun 2025 13:24:33 +0200
+Subject: [PATCH 03/20] tools: fix help message for realm deny
+
+Some option not support by 'realm deny' were listed in the help output.
+---
+ tools/realm-logins.c | 9 ++++++++-
+ 1 file changed, 8 insertions(+), 1 deletion(-)
+
+diff --git a/tools/realm-logins.c b/tools/realm-logins.c
+index 8276a57..00d39db 100644
+--- a/tools/realm-logins.c
++++ b/tools/realm-logins.c
+@@ -198,17 +198,24 @@ realm_permit_or_deny (RealmClient *client,
+ GOptionEntry option_entries[] = {
+ { "all", 'a', 0, G_OPTION_ARG_NONE, &arg_all,
+ permit ? N_("Permit any realm account login") : N_("Deny any realm account login"), NULL },
++ { "realm", 'R', 0, G_OPTION_ARG_STRING, &realm_name, N_("Realm to permit/deny logins for"), NULL },
++ { NULL, }
++ };
++
++ GOptionEntry option_entries_permit[] = {
+ { "withdraw", 'x', 0, G_OPTION_ARG_NONE, &arg_withdraw,
+ N_("Withdraw permit for a realm account to login"), NULL },
+ { "groups", 'g', 0, G_OPTION_ARG_NONE, &arg_groups,
+ N_("Treat names as groups which to permit"), NULL },
+- { "realm", 'R', 0, G_OPTION_ARG_STRING, &realm_name, N_("Realm to permit/deny logins for"), NULL },
+ { NULL, }
+ };
+
+ context = g_option_context_new ("realm");
+ g_option_context_set_translation_domain (context, GETTEXT_PACKAGE);
+ g_option_context_add_main_entries (context, option_entries, NULL);
++ if (permit) {
++ g_option_context_add_main_entries (context, option_entries_permit, NULL);
++ }
+ g_option_context_add_main_entries (context, realm_global_options, NULL);
+
+ if (!g_option_context_parse (context, &argc, &argv, &error)) {
+--
+2.54.0
+
diff --git a/0004-disco-check-IPA-specific-extension-in-rootDSE.patch b/0004-disco-check-IPA-specific-extension-in-rootDSE.patch
new file mode 100644
index 0000000..7af461a
--- /dev/null
+++ b/0004-disco-check-IPA-specific-extension-in-rootDSE.patch
@@ -0,0 +1,152 @@
+From d6031da2b493e50aacc8ad9741d33ec251483bee Mon Sep 17 00:00:00 2001
+From: Sumit Bose <sbose@redhat.com>
+Date: Wed, 14 May 2025 14:14:49 +0200
+Subject: [PATCH 04/20] disco: check IPA specific extension in rootDSE
+
+In case an IPA server has restricted anonymous access it might not be
+possible to read the based DN to find the domain name or the Kerberos
+realm. As long as the rootDSE can be read we can identify an IPA domain
+by IPA specific LDAP extensions and derived the domain name from the
+default domain suffix. With this patch this information is used as a
+fallback if the base DN cannot be read.
+
+Resolves: https://gitlab.freedesktop.org/realmd/realmd/-/issues/44
+---
+ service/realm-disco-rootdse.c | 78 ++++++++++++++++++++++++++++++-----
+ 1 file changed, 68 insertions(+), 10 deletions(-)
+
+diff --git a/service/realm-disco-rootdse.c b/service/realm-disco-rootdse.c
+index d9b44b3..20febe2 100644
+--- a/service/realm-disco-rootdse.c
++++ b/service/realm-disco-rootdse.c
+@@ -34,6 +34,7 @@ struct _Closure {
+
+ gchar *default_naming_context;
+ gint msgid;
++ gboolean has_ipa_keytab_set_oid;
+
+ gboolean (* request) (GTask *task,
+ Closure *clo,
+@@ -177,6 +178,31 @@ request_krb_realm (GTask *task,
+ LDAP_SCOPE_SUB, "(objectClass=krbRealmContainer)", attrs);
+ }
+
++static gchar * get_domain_from_dn (const gchar *dn)
++{
++ char *domain;
++ gchar *out;
++
++ int ret;
++
++ ret = ldap_dn2domain ( (const char *) dn, &domain);
++ if (ret != 0 ) {
++ g_debug ("Failed to get domain name from DN %s", dn);
++ return NULL;
++ }
++ if (!realm_options_check_domain_name (domain)) {
++ ber_memfree (domain);
++ g_message ("Invalid value in domain name %s derived from %s",
++ domain, dn);
++ return NULL;
++ }
++
++ out = g_strdup (domain);
++ ber_memfree (domain);
++
++ return out;
++}
++
+ static gboolean
+ result_domain_info (GTask *task,
+ Closure *clo,
+@@ -188,8 +214,11 @@ result_domain_info (GTask *task,
+
+ entry = ldap_first_entry (ldap, message);
+
+- /* If we can't retrieve this, then nothing more to do */
+- if (entry == NULL) {
++ /* If we can't retrieve this, then nothing more to do. If we can
++ * already safely assume that the domain is IPA because an IPA
++ * specific LDAP extension was found, we try to derive the domain name
++ * and the Kerberos realm from the default naming context. */
++ if (entry == NULL && !clo->has_ipa_keytab_set_oid) {
+ g_debug ("Couldn't read default naming context");
+ g_task_return_new_error (task, REALM_LDAP_ERROR, LDAP_NO_SUCH_OBJECT,
+ "Couldn't lookup domain name on LDAP server");
+@@ -198,21 +227,40 @@ result_domain_info (GTask *task,
+
+ /* What kind of server is it? */
+ clo->disco->server_software = NULL;
+- bvs = ldap_get_values_len (ldap, entry, "info");
+- if (bvs && bvs[0] && bvs[0]->bv_len >= 3) {
+- if (g_ascii_strncasecmp (bvs[0]->bv_val, "IPA", 3) == 0)
+- clo->disco->server_software = REALM_DBUS_IDENTIFIER_IPA;
++ if (entry == NULL) {
++ g_debug ("Couldn't read default naming context, assuming IPA");
++ clo->disco->server_software = REALM_DBUS_IDENTIFIER_IPA;
++ } else {
++ bvs = ldap_get_values_len (ldap, entry, "info");
++ if (bvs && bvs[0] && bvs[0]->bv_len >= 3) {
++ if (g_ascii_strncasecmp (bvs[0]->bv_val, "IPA", 3) == 0)
++ clo->disco->server_software = REALM_DBUS_IDENTIFIER_IPA;
++ }
++ ldap_value_free_len (bvs);
+ }
+- ldap_value_free_len (bvs);
+
+ if (clo->disco->server_software)
+ g_debug ("Got server software: %s", clo->disco->server_software);
+
+ /* What is the domain name? */
+ g_free (clo->disco->domain_name);
+- clo->disco->domain_name = entry_get_attribute (ldap, entry, "associatedDomain", TRUE);
+
+- g_debug ("Got associatedDomain: %s", clo->disco->domain_name);
++ if (entry == NULL) {
++ clo->disco->domain_name = get_domain_from_dn (clo->default_naming_context);
++ if (clo->disco->domain_name != NULL) {
++ clo->disco->kerberos_realm = g_ascii_strup (clo->disco->domain_name, -1);
++ }
++ } else {
++ clo->disco->domain_name = entry_get_attribute (ldap, entry, "associatedDomain", TRUE);
++ }
++
++ g_debug ("Got domain name: %s", clo->disco->domain_name);
++
++ if (entry == NULL) {
++ /* LDAP already failed, no need for another try */
++ g_task_return_boolean (task, TRUE);
++ return FALSE;
++ }
+
+ /* Next search for Kerberos container */
+ clo->request = request_krb_realm;
+@@ -376,6 +424,15 @@ result_root_dse (GTask *task,
+ return FALSE;
+ }
+
++ /* Check for IPA's KEYTAB_SET_OID LDAP extension. Even if it
++ * is not present we continue to check for IPA since there is
++ * currently no other server type supported. */
++ clo->has_ipa_keytab_set_oid = FALSE;
++ if (entry_has_attribute (ldap, entry, "supportedExtension",
++ "2.16.840.1.113730.3.8.10.1")) {
++ clo->has_ipa_keytab_set_oid = TRUE;
++ }
++
+ /* Next search for IPA field */
+ clo->request = request_domain_info;
+ clo->result = NULL;
+@@ -388,7 +445,8 @@ request_root_dse (GTask *task,
+ Closure *clo,
+ LDAP *ldap)
+ {
+- const char *attrs[] = { "defaultNamingContext", "supportedCapabilities", NULL };
++ const char *attrs[] = { "defaultNamingContext", "supportedCapabilities",
++ "supportedExtension", NULL };
+
+ clo->request = NULL;
+ clo->result = result_root_dse;
+--
+2.54.0
+
diff --git a/0005-tools-add-message-after-successful-join.patch b/0005-tools-add-message-after-successful-join.patch
new file mode 100644
index 0000000..87d15f1
--- /dev/null
+++ b/0005-tools-add-message-after-successful-join.patch
@@ -0,0 +1,167 @@
+From b28ff5488bb4441ae6fae6ed4dd8acdd0c736736 Mon Sep 17 00:00:00 2001
+From: Sumit Bose <sbose@redhat.com>
+Date: Fri, 19 Sep 2025 15:37:31 +0200
+Subject: [PATCH 05/20] tools: add message after successful join
+
+If the realm join command is successful it will tell it now at the end
+of the operation. Additionally it will give some information if the
+system is configured for fully-qualified names or if short names can be
+used.
+---
+ tools/realm-join.c | 126 +++++++++++++++++++++++++++++++++++++++++++++
+ 1 file changed, 126 insertions(+)
+
+diff --git a/tools/realm-join.c b/tools/realm-join.c
+index fc69678..cb290cc 100644
+--- a/tools/realm-join.c
++++ b/tools/realm-join.c
+@@ -277,6 +277,107 @@ perform_join (RealmClient *client,
+ return ret;
+ }
+
++static gchar *
++disco_realm_name (RealmClient *client, RealmJoinArgs *args)
++{
++ RealmDbusKerberosMembership *membership;
++ RealmDbusRealm *realm;
++ GError *error = NULL;
++ GList *realms = NULL;
++ gchar *name = NULL;
++
++ realms = realm_client_discover (client, "", args->use_ldaps, args->client_software,
++ args->server_software, args->membership_software,
++ REALM_DBUS_KERBEROS_MEMBERSHIP_INTERFACE,
++ NULL, &error);
++
++ if (error != NULL) {
++ if (realms != NULL) {
++ g_list_free_full (realms, g_object_unref);
++ }
++ realm_handle_error (error, _("Failed to discover realm"));
++ return NULL;
++ }
++
++ if (realms == NULL) {
++ realm_handle_error (NULL, _("No realm found"));
++ return NULL;
++ }
++
++ membership = realms->data;
++ realm = realm_client_to_realm (client, membership);
++ if (realm != NULL) {
++ name = g_strdup (realm_dbus_realm_get_name (realm));
++ g_object_unref (realm);
++ }
++ g_list_free_full (realms, g_object_unref);
++
++ return name;
++}
++
++static int
++realm_client_domain_has_fully_qualified_names (RealmClient *client,
++ const gchar *const input_name,
++ RealmJoinArgs *args,
++ gboolean *fully_qualified_names)
++{
++ RealmDbusProvider *provider;
++ RealmDbusRealm *realm = NULL;
++ const gchar *const *realms;
++ const gchar *name;
++ size_t c;
++ const gchar *const *formats;
++ const gchar *realm_name;
++ gchar *tmp_name = NULL;
++
++ if (input_name == NULL || *input_name == '\0') {
++ tmp_name = disco_realm_name (client, args);
++ if (tmp_name == NULL) {
++ return ENOENT;
++ }
++ realm_name = tmp_name;
++ } else {
++ realm_name = input_name;
++ }
++
++ provider = realm_client_get_provider (client);
++ realms = realm_dbus_provider_get_realms (provider);
++
++ *fully_qualified_names = false;
++ for (c = 0; realms && realms[c] != NULL; c++) {
++ g_clear_object (&realm);
++ realm = realm_client_get_realm (client, realms[c]);
++ if (realm == NULL || !realm_is_configured (realm)) {
++ continue;
++ }
++
++ name = realm_dbus_realm_get_name (realm);
++ if (name == NULL || strcasecmp (name, realm_name) != 0) {
++ continue;
++ }
++
++ formats = realm_dbus_realm_get_login_formats (realm);
++ /* The first entry in the array is the preferred
++ * format and "%U" is the placeholder for the short user
++ * name. */
++ if (formats != NULL && formats[0] != NULL
++ && *formats[0] != '\0'
++ && strcmp (formats[0], "%U") != 0) {
++ *fully_qualified_names = true;
++ }
++
++ break;
++ }
++
++ g_free (tmp_name);
++ g_clear_object (&realm);
++ if (realms == NULL || realms[c] == NULL) {
++ return ENOENT;
++ }
++
++ return 0;
++}
++
+ int
+ realm_join (RealmClient *client,
+ int argc,
+@@ -288,6 +389,7 @@ realm_join (RealmClient *client,
+ RealmJoinArgs args;
+ GOptionGroup *group;
+ gint ret = 0;
++ gboolean has_fqn = false;
+
+ GOptionEntry option_entries[] = {
+ { "automatic-id-mapping", 0, G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
+@@ -355,6 +457,30 @@ realm_join (RealmClient *client,
+ ret = perform_join (client, realm_name, &args);
+ }
+
++ if (ret == 0 && !realm_unattended) {
++ g_print ("Join is successful!\n");
++ if (realm_client_domain_has_fully_qualified_names (client, realm_name, &args, &has_fqn) != 0) {
++ g_printerr ("%s: %s\n", g_get_prgname (),
++ _("Cannot determine if fully-qualified name are used or not"));
++ } else {
++ if (has_fqn) {
++ g_print (
++ _("Please note that by default, you have to use fully-qualified names\n"
++ "during AD user lookup & login instead of just short names\n"
++ "e.g. 'username@domain' instead of just 'username'. Although\n"
++ "fully-qualified names are recommended, this behaviour can be changed\n"
++ "in sssd.conf file with the 'use_fully_qualified_names = False' option.\n"));
++ } else {
++ g_print (
++ _("Please note that by default, you can use short names during AD user\n"
++ "lookup & login instead of fully-qualified names\n"
++ "e.g. 'username' instead of 'username@domain'. If you prefer\n"
++ "fully-qualified names, this behaviour can be changed\n"
++ "in sssd.conf file with the 'use_fully_qualified_names = True' option.\n"));
++ }
++ }
++ }
++
+ g_option_context_free (context);
+ return ret;
+ }
+--
+2.54.0
+
diff --git a/0006-doc-add-renew-option-of-realm-man-page.patch b/0006-doc-add-renew-option-of-realm-man-page.patch
new file mode 100644
index 0000000..a778a69
--- /dev/null
+++ b/0006-doc-add-renew-option-of-realm-man-page.patch
@@ -0,0 +1,90 @@
+From 5ad0311459db3e291db88e1b9c2bcde912698cce Mon Sep 17 00:00:00 2001
+From: Sumit Bose <sbose@redhat.com>
+Date: Tue, 14 Oct 2025 10:37:01 +0200
+Subject: [PATCH 06/20] doc: add 'renew' option of realm man page
+
+---
+ doc/manual/realm.xml | 60 ++++++++++++++++++++++++++++++++++++++++++++
+ 1 file changed, 60 insertions(+)
+
+diff --git a/doc/manual/realm.xml b/doc/manual/realm.xml
+index 0693283..caa6308 100644
+--- a/doc/manual/realm.xml
++++ b/doc/manual/realm.xml
+@@ -38,6 +38,9 @@
+ <cmdsynopsis>
+ <command>realm leave</command> <arg choice="opt">-U user</arg> <arg choice="opt">realm-name</arg>
+ </cmdsynopsis>
++ <cmdsynopsis>
++ <command>realm renew</command> <arg choice="opt">realm-name</arg>
++ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>realm list</command>
+ </cmdsynopsis>
+@@ -407,6 +410,63 @@ $ realm leave domain.example.com
+
+ </refsect1>
+
++<refsect1 id="man-renew">
++ <title>Renew</title>
++
++ <para>Renew the machine account password and update the keytab.</para>
++
++ <informalexample>
++<programlisting>
++$ realm renew
++</programlisting>
++<programlisting>
++$ realm renew --computer-password-lifetime=10 domain.example.com
++</programlisting>
++ </informalexample>
++
++ <para>Renew the machine account password with the help of the existing one
++ from a keytab and store the new version in the keytab. If no domain name is
++ given it is derived from the existing configuration.</para>
++
++ <para>The following options can be used:</para>
++
++ <variablelist>
++ <varlistentry>
++ <term><option>--membership-software=xxx</option></term>
++ <listitem><para>Use specified membership software, currently
++ only <replaceable>adcli</replaceable> is supported.
++ </para></listitem>
++ </varlistentry>
++ <varlistentry>
++ <term><option>--use-ldaps</option></term>
++ <listitem><para>See option description in
++ <xref linkend="man-join"/>.</para></listitem>
++ </varlistentry>
++ <varlistentry>
++ <term><option>--host-keytab=xxx</option></term>
++ <listitem><para>Path to the keytab, if not specified the
++ default keytab file will be used.</para></listitem>
++ </varlistentry>
++ <varlistentry>
++ <term><option>--host-fqdn=xxx</option></term>
++ <listitem><para>Fully-qualified name of the host, only needed
++ if it is not determined correctly automatically.
++ </para></listitem>
++ </varlistentry>
++ <varlistentry>
++ <term><option>--computer-password-lifetime=xxx</option></term>
++ <listitem><para>Lifetime of the machine account password in days,
++ default is 30.</para></listitem>
++ </varlistentry>
++ <varlistentry>
++ <term><option>--add-samba-data</option></term>
++ <listitem><para>Try to update Samba's internal machine account
++ password as well if a membership software other than Samba is used.
++ </para></listitem>
++ </varlistentry>
++ </variablelist>
++</refsect1>
++
+ <refsect1 id="man-list">
+ <title>List</title>
+
+--
+2.54.0
+
diff --git a/0007-Testcases-Adding-multiple-testcases.patch b/0007-Testcases-Adding-multiple-testcases.patch
new file mode 100644
index 0000000..2884b82
--- /dev/null
+++ b/0007-Testcases-Adding-multiple-testcases.patch
@@ -0,0 +1,235 @@
+From 8f0514034e3e521f27bae3b9108a249ebef32cfd Mon Sep 17 00:00:00 2001
+From: shridhargadekar <shridhar.always@gmail.com>
+Date: Fri, 5 Dec 2025 14:19:13 +0530
+Subject: [PATCH 07/20] Testcases: Adding multiple testcases
+
+Added Testcases for following scenarios:
+1. realm join --do-not-touch-config
+2. Realm leave remove computer
+3. Realm renew command
+
+Rearranged the code, imports, doc-strigs.
+---
+ tests/test_realmd.py | 172 ++++++++++++++++++++++++++++++++++++++++---
+ 1 file changed, 163 insertions(+), 9 deletions(-)
+
+diff --git a/tests/test_realmd.py b/tests/test_realmd.py
+index c9b833d..a513fa7 100644
+--- a/tests/test_realmd.py
++++ b/tests/test_realmd.py
+@@ -1,18 +1,21 @@
++"""realmd test cases"""
++
+ from __future__ import annotations
+
+-import pytest
+-import time
+ import os
+ import sys
++import time
++from typing import Any
+
+-
+-from .topology import KnownTopology, KnownTopologyGroup
++import pytest
+ from sssd_test_framework.roles.ad import AD
+ from sssd_test_framework.roles.client import Client
+-from sssd_test_framework.roles.ipa import IPA
+ from sssd_test_framework.roles.generic import GenericADProvider
++from sssd_test_framework.roles.ipa import IPA
+ from sssd_test_framework.utils.realmd import RealmUtils
+
++from .topology import KnownTopology, KnownTopologyGroup
++
+
+ @pytest.mark.importance("critical")
+ @pytest.mark.topology(KnownTopologyGroup.AnyAD)
+@@ -38,7 +41,12 @@ def test_realm_join(client: Client, provider: Any):
+ :expectedresults:
+ 1. A client system joined to the domain successfully
+ """
+- r = client.realm.join(provider.host.domain, krb=False, user=provider.host.adminuser, password=provider.host.adminpw)
++ r = client.realm.join(
++ provider.host.domain,
++ krb=False,
++ user=f"{provider.host.adminuser}@{provider.host.domain.upper()}",
++ password=provider.host.adminpw,
++ )
+ assert r.rc == 0, "realm join operation failed!"
+ assert "Successfully enrolled machine in realm" in r.stderr, "realm failed to join client to the domain"
+
+@@ -48,11 +56,25 @@ def test_realm_join(client: Client, provider: Any):
+ def test_realm_leave(client: Client, provider: Any):
+ """
+ :title: realm leave
++ :setup:
++ 1. Join client to the domain
+ :steps:
++ 1. Leave realm
+ :expectedresults:
++ 1. Client system is deconfigured for realm use
+ """
+- client.realm.join(provider.host.domain, krb=False, user=provider.host.adminuser, password=provider.host.adminpw)
+- r = client.realm.leave(provider.host.domain)
++ client.realm.join(
++ provider.host.domain,
++ krb=False,
++ user=f"{provider.host.adminuser}@{provider.host.domain.upper()}",
++ password=provider.host.adminpw,
++ )
++ r = client.realm.leave(
++ provider.host.domain,
++ krb=False,
++ user=f"{provider.host.adminuser}@{provider.host.domain.upper()}",
++ password=provider.host.adminpw,
++ )
+ assert r.rc == 0, "realm leave operation failed!"
+ assert "Successfully unenrolled machine from realm" in r.stderr, "realm failed to leave domain!"
+
+@@ -61,10 +83,142 @@ def test_realm_leave(client: Client, provider: Any):
+ @pytest.mark.topology(KnownTopologyGroup.AnyAD)
+ def test_realm_list(client: Client, provider: Any):
+ """
+- :title: realm list
++ :title: realm list available domains
+ :steps:
++ 1. Run realm list --all
+ :expectedresults:
++ 1. List all configured and discovered realms
+ """
+ r = client.realm.list(args=["--all"])
+ assert r.rc == 0, "realm list operation failed!"
+ assert provider.host.domain in r.stdout, "realm failed to list domain"
++
++
++@pytest.mark.importance("critical")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_join_no_config_modification(client: Client, provider: Any):
++ """
++ :title: realm join without modifying local config
++ :steps:
++ 1. Join a client system to the domain without modifying local configuration files.
++ :expectedresults:
++ 1. A client system joined to the domain successfully, without modifying
++ local /etc/sssd/sssd.conf, /etc/krb5.conf.
++ """
++ config_path = {"sssd": "/etc/sssd/sssd.conf", "krb5": "/etc/krb5.conf"}
++ original_config = {"sssd": None, "krb5": None} # original config content set to None
++
++ # Check original config file status
++ for key in config_path:
++ if client.fs.exists(config_path[key]):
++ original_config[key] = client.fs.read(config_path[key])
++ else:
++ original_config[key] = None # config file didn't exist
++
++ r = client.realm.join(
++ provider.host.domain,
++ krb=False,
++ args=["--do-not-touch-config"],
++ user=f"{provider.host.adminuser}@{provider.host.domain.upper()}",
++ password=provider.host.adminpw,
++ )
++ assert r.rc == 0, "realm join operation failed!"
++ assert "Successfully enrolled machine in realm" in r.stderr, "realm failed to join client to the domain!"
++
++ # Using kinit -k validates that the keys in the keytab actually work against the KDC.
++ s = client.host.hostname.split('.')[0].upper()
++ p = f"{s}$@{provider.host.domain.upper()}"
++ k = client.host.conn.exec(["kinit", "-k", p])
++ assert k.rc == 0, f"kinit -k failed, keytab may be invalid or missing: {k.stderr}"
++
++ # Verify /etc/sssd/sssd.conf and /etc/krb5.conf are not modified
++ for key in original_config:
++ if original_config[key] is None:
++ assert not client.fs.exists(config_path[key]), f"{config_path[key]} was created unexpectedly!"
++ else:
++ new_conf = client.fs.read(config_path[key])
++ assert new_conf == original_config[key], f"{config_path[key]} was modified unexpectedly!"
++
++
++@pytest.mark.importance("critical")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_leave_remove_computer(client: Client, provider: Any):
++ """
++ :title: Realm leave remove computer
++ :steps:
++ 1. Leave the realm with '--remove' option
++ :expectedresults:
++ 2. Client computer account removed from Domain controller.
++ """
++ client.realm.join(
++ provider.host.domain,
++ krb=True,
++ user=f"{provider.host.adminuser}@{provider.host.domain.upper()}",
++ password=provider.host.adminpw,
++ )
++
++ r = client.realm.leave(
++ provider.host.domain,
++ krb=False,
++ args=["--remove"],
++ user=f"{provider.host.adminuser}@{provider.host.domain.upper()}",
++ password=provider.host.adminpw,
++ )
++
++ assert r.rc == 0, "realm leave operation failed!"
++ assert "Successfully unenrolled machine from realm" in r.stderr, "realm failed to leave domain!"
++
++ s = client.adcli.show_computer(
++ domain=provider.host.domain,
++ args=["--login-user", "Administrator", "--verbose"],
++ login_user="Administrator",
++ krb=False,
++ password=provider.host.adminpw,
++ )
++
++ assert s.rc != 0, "computer account exists!"
++
++
++@pytest.mark.importance("critical")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_renew(client: Client, provider: GenericADProvider):
++ """
++ :title: Realm renew command
++ :setup:
++ 1. Join client to AD
++ 2. Get the kvno number from hostkeytab
++ :steps:
++ 1.Renew host-keytab with
++ :expectedresults:
++ 1. Host-keytab is renewed
++ """
++ client.realm.join(
++ provider.host.domain,
++ krb=False,
++ user=provider.host.adminuser,
++ args=["--membership-software=adcli"],
++ password=provider.host.adminpw,
++ )
++
++ def get_kvno() -> int:
++ """Helper to get the current kvno from the keytab."""
++ klist_cmd = client.host.conn.exec(["klist", "-kt"])
++ assert klist_cmd.rc == 0, f"klist -kt failed: {klist_cmd.stderr}!"
++ # Parse klist output to find the kvno
++ kvno = []
++ for line in klist_cmd.stdout_lines:
++ line = line.strip()
++ if line.split()[0].isnumeric():
++ kvno.append(int(line.split()[0]))
++ return max(kvno)
++ raise ValueError("Could not parse kvno from klist output.")
++
++ old_kvno = get_kvno()
++
++ # Renew host-keytab
++ renew_cmd = client.realm.renew(provider.host.domain, args=["--computer-password-lifetime=0"])
++ assert renew_cmd.rc == 0, f"realm renew failed: {renew_cmd.stderr}!"
++
++ new_kvno = get_kvno()
++
++ assert new_kvno > old_kvno, "Keytab was not renewed (kvno did not increase).!"
+--
+2.54.0
+
diff --git a/0008-doc-Be-explicit-about-default-settings-in-realmd.con.patch b/0008-doc-Be-explicit-about-default-settings-in-realmd.con.patch
new file mode 100644
index 0000000..8034d31
--- /dev/null
+++ b/0008-doc-Be-explicit-about-default-settings-in-realmd.con.patch
@@ -0,0 +1,44 @@
+From efa2e284606683acee63afeee6f319e07b85bbd5 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Pavel=20Filipensk=C3=BD?= <pfilipensky@samba.org>
+Date: Thu, 29 Jan 2026 20:40:39 +0100
+Subject: [PATCH 08/20] doc: Be explicit about default settings in realmd.conf
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Signed-off-by: Pavel Filipenský <pfilipensky@samba.org>
+---
+ doc/manual/realmd.conf.xml | 6 ++++--
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+diff --git a/doc/manual/realmd.conf.xml b/doc/manual/realmd.conf.xml
+index ad17639..54072c5 100644
+--- a/doc/manual/realmd.conf.xml
++++ b/doc/manual/realmd.conf.xml
+@@ -210,12 +210,13 @@ os-version = 9.9.9.9.9
+ <listitem>
+ <para>Set this to <parameter>no</parameter> to disable automatic
+ installation of packages via package-kit.</para>
++ <para>The default setting for this is <parameter>yes</parameter>.</para>
+
+ <informalexample>
+ <programlisting language="js">
+ [service]
+-automatic-install = no
+-# automatic-install = yes
++automatic-install = yes
++# automatic-install = no
+ </programlisting>
+ </informalexample>
+ </listitem>
+@@ -227,6 +228,7 @@ automatic-install = no
+ <para>Set this to <parameter>yes</parameter> to create a Samba
+ configuration file with id-mapping options used by Samba-3.5
+ and earlier version.</para>
++ <para>The default setting for this is <parameter>no</parameter>.</para>
+
+ <informalexample>
+ <programlisting language="js">
+--
+2.54.0
+
diff --git a/0009-doc-document-debug-option-in-service-section-of-real.patch b/0009-doc-document-debug-option-in-service-section-of-real.patch
new file mode 100644
index 0000000..8d9a838
--- /dev/null
+++ b/0009-doc-document-debug-option-in-service-section-of-real.patch
@@ -0,0 +1,48 @@
+From e6c79f6248baf32a54852960d667468af639268e Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Pavel=20Filipensk=C3=BD?= <pfilipensky@samba.org>
+Date: Thu, 29 Jan 2026 20:10:05 +0100
+Subject: [PATCH 09/20] doc: document debug option in [service] section of
+ realmd.conf
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+The debug setting was already present in the defaults file
+realmd-defaults.conf, but was not documented in the manual page.
+
+Signed-off-by: Pavel Filipenský <pfilipensky@samba.org>
+---
+ doc/manual/realmd.conf.xml | 17 +++++++++++++++++
+ 1 file changed, 17 insertions(+)
+
+diff --git a/doc/manual/realmd.conf.xml b/doc/manual/realmd.conf.xml
+index 54072c5..6f2d433 100644
+--- a/doc/manual/realmd.conf.xml
++++ b/doc/manual/realmd.conf.xml
+@@ -205,6 +205,23 @@ os-version = 9.9.9.9.9
+
+ <variablelist>
+
++ <varlistentry>
++ <term><option>debug</option></term>
++ <listitem>
++ <para>Set this to <parameter>yes</parameter> to enable verbose
++ debug logging in realmd.</para>
++ <para>The default setting for this is <parameter>no</parameter>.</para>
++
++ <informalexample>
++<programlisting language="js">
++[service]
++debug = no
++# debug = yes
++</programlisting>
++ </informalexample>
++ </listitem>
++ </varlistentry>
++
+ <varlistentry>
+ <term><option>automatic-install</option></term>
+ <listitem>
+--
+2.54.0
+
diff --git a/0010-samba-add-debug-level-10-to-net-commands-when-realmd.patch b/0010-samba-add-debug-level-10-to-net-commands-when-realmd.patch
new file mode 100644
index 0000000..84a2412
--- /dev/null
+++ b/0010-samba-add-debug-level-10-to-net-commands-when-realmd.patch
@@ -0,0 +1,39 @@
+From e1eb08832fa58fcea5a3752277778d3d40c08a42 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Pavel=20Filipensk=C3=BD?= <pfilipensky@samba.org>
+Date: Thu, 29 Jan 2026 14:49:51 +0100
+Subject: [PATCH 10/20] samba: add debug level 10 to net commands when realmd
+ runs in debug mode
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Enable verbose debugging output for all net ads commands (join, leave,
+keytab) by adding the -d 10 flag when the realmd daemon is started with
+the -d flag or if 'debug' option is set to 'yes' in [service] section of
+realmd.conf
+
+Signed-off-by: Pavel Filipenský <pfilipensky@samba.org>
+---
+ service/realm-samba-enroll.c | 6 ++++++
+ 1 file changed, 6 insertions(+)
+
+diff --git a/service/realm-samba-enroll.c b/service/realm-samba-enroll.c
+index 9d776fa..ecf2792 100644
+--- a/service/realm-samba-enroll.c
++++ b/service/realm-samba-enroll.c
+@@ -277,6 +277,12 @@ begin_net_process (JoinClosure *join,
+ g_ptr_array_add (args, join->disco->explicit_server);
+ }
+
++ /* Add debug level when daemon is running in debug mode */
++ if (realm_daemon_has_debug_flag ()) {
++ g_ptr_array_add (args, "-d");
++ g_ptr_array_add (args, "10");
++ }
++
+ va_start (va, user_data);
+ do {
+ arg = va_arg (va, gchar *);
+--
+2.54.0
+
diff --git a/0011-When-running-in-debug-make-warnings-not-fatal.patch b/0011-When-running-in-debug-make-warnings-not-fatal.patch
new file mode 100644
index 0000000..968ad1a
--- /dev/null
+++ b/0011-When-running-in-debug-make-warnings-not-fatal.patch
@@ -0,0 +1,25 @@
+From 8c64f1f9c46e4be2789402d8fe0a631d8c42e21f Mon Sep 17 00:00:00 2001
+From: Sumit Bose <sbose@redhat.com>
+Date: Tue, 3 Feb 2026 15:18:33 +0100
+Subject: [PATCH 11/20] When running in --debug make warnings not fatal
+
+---
+ service/realm-daemon.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/service/realm-daemon.c b/service/realm-daemon.c
+index 85350a7..0ccb733 100644
+--- a/service/realm-daemon.c
++++ b/service/realm-daemon.c
+@@ -533,7 +533,7 @@ main (int argc,
+
+ if (service_debug) {
+ g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, on_realm_log_debug, NULL);
+- g_log_set_always_fatal (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING);
++ g_log_set_always_fatal (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL);
+ }
+
+ realm_error = realm_error_quark ();
+--
+2.54.0
+
diff --git a/0012-service-only-use-single-value-password-server-option.patch b/0012-service-only-use-single-value-password-server-option.patch
new file mode 100644
index 0000000..d254b47
--- /dev/null
+++ b/0012-service-only-use-single-value-password-server-option.patch
@@ -0,0 +1,34 @@
+From ac7d4ab38e8d7647e9efb25d8e8d821a8f87a517 Mon Sep 17 00:00:00 2001
+From: Sumit Bose <sbose@redhat.com>
+Date: Tue, 3 Feb 2026 16:27:27 +0100
+Subject: [PATCH 12/20] service: only use single value 'password server' option
+
+Samba's 'password server' option can contain multiple values and even a
+wildcard character which are not suitable as a value for the '-S' option
+of the 'net' command. The 'password server value is only used if it does
+not contain '*' or ','.
+---
+ service/realm-samba.c | 7 +++++++
+ 1 file changed, 7 insertions(+)
+
+diff --git a/service/realm-samba.c b/service/realm-samba.c
+index bc976f1..57e03f6 100644
+--- a/service/realm-samba.c
++++ b/service/realm-samba.c
+@@ -728,6 +728,13 @@ realm_samba_discover_myself (RealmKerberos *realm,
+ disco->explicit_netbios = value;
+
+ value = realm_ini_config_get (self->config, REALM_SAMBA_CONFIG_GLOBAL, "password server");
++ /* Only set explicit_server to the value of 'password server' if it
++ * neither contains the wildcard character '*' nor a list separator
++ * character used by Samba. */
++ if (value != NULL && strpbrk (value, "* \t,;") != NULL) {
++ g_free (value);
++ value = NULL;
++ }
+ g_free (disco->explicit_server);
+ disco->explicit_server = value;
+ }
+--
+2.54.0
+
diff --git a/0013-Refresh-license-text.patch b/0013-Refresh-license-text.patch
new file mode 100644
index 0000000..ca527d9
--- /dev/null
+++ b/0013-Refresh-license-text.patch
@@ -0,0 +1,51 @@
+From 0b577534b61b0bb66430e0fc4651597f33592994 Mon Sep 17 00:00:00 2001
+From: Sumit Bose <sbose@redhat.com>
+Date: Wed, 4 Feb 2026 14:56:44 +0100
+Subject: [PATCH 13/20] Refresh license text
+
+Use the latest version of the license text from
+https://ftp.gnu.org/gnu/Licenses/lgpl-2.1.txt.
+
+Please note, the license is not changed only the text is refreshed.
+---
+ COPYING | 11 +++++------
+ 1 file changed, 5 insertions(+), 6 deletions(-)
+
+diff --git a/COPYING b/COPYING
+index f166cc5..f6683e7 100644
+--- a/COPYING
++++ b/COPYING
+@@ -2,7 +2,7 @@
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
++ <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+@@ -484,8 +484,7 @@ convey the exclusion of warranty; and each file should have at least the
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+- License along with this library; if not, write to the Free Software
+- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
++ License along with this library; if not, see <https://www.gnu.org/licenses/>.
+
+ Also add information on how to contact you by electronic and paper mail.
+
+@@ -496,7 +495,7 @@ necessary. Here is a sample; alter the names:
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+- <signature of Ty Coon>, 1 April 1990
+- Ty Coon, President of Vice
++ <signature of Moe Ghoul>, 1 April 1990
++ Moe Ghoul, President of Vice
+
+-That's all there is to it!
+\ No newline at end of file
++That's all there is to it!
+--
+2.54.0
+
diff --git a/0014-Testcases-realm-join-leave-user-login.patch b/0014-Testcases-realm-join-leave-user-login.patch
new file mode 100644
index 0000000..c4f31b5
--- /dev/null
+++ b/0014-Testcases-realm-join-leave-user-login.patch
@@ -0,0 +1,280 @@
+From d249eeb87ea3f1ce1c0f140c31cc3bfde72e10ea Mon Sep 17 00:00:00 2001
+From: Shridhar Gadekar <shridhar.always@gmail.com>
+Date: Wed, 11 Feb 2026 23:31:26 +0530
+Subject: [PATCH 14/20] Testcases: realm join leave, user login
+
+Testcases for realm join and leave with
+supported combintations of --client-software
+and --member-software
+user login allow, deny
+---
+ tests/test_realmd.py | 169 +++++++++++++++++++++++++++++++++-
+ tests/topology_controllers.py | 48 ++++++++--
+ 2 files changed, 207 insertions(+), 10 deletions(-)
+
+diff --git a/tests/test_realmd.py b/tests/test_realmd.py
+index a513fa7..6e25cb9 100644
+--- a/tests/test_realmd.py
++++ b/tests/test_realmd.py
+@@ -126,7 +126,7 @@ def test_realm_join_no_config_modification(client: Client, provider: Any):
+ assert "Successfully enrolled machine in realm" in r.stderr, "realm failed to join client to the domain!"
+
+ # Using kinit -k validates that the keys in the keytab actually work against the KDC.
+- s = client.host.hostname.split('.')[0].upper()
++ s = client.host.hostname.split(".")[0].upper()
+ p = f"{s}$@{provider.host.domain.upper()}"
+ k = client.host.conn.exec(["kinit", "-k", p])
+ assert k.rc == 0, f"kinit -k failed, keytab may be invalid or missing: {k.stderr}"
+@@ -222,3 +222,170 @@ def test_realm_renew(client: Client, provider: GenericADProvider):
+ new_kvno = get_kvno()
+
+ assert new_kvno > old_kvno, "Keytab was not renewed (kvno did not increase).!"
++
++
++@pytest.mark.importance("critical")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++@pytest.mark.parametrize(
++ "client_software, membership_software",
++ [
++ ("sssd", "adcli"), # Standard modern Linux integration
++ ("sssd", "samba"), # SSSD utilizing Samba tools for join
++ ("winbind", "samba"), # Classic Samba/Winbind integration
++ ],
++)
++def test_realm_join_leave_combinations(
++ client: Client, provider: GenericADProvider, client_software: str, membership_software: str
++):
++ """
++ :title: Verify Realm Join/Leave with specific software combinations.
++ :description: Test joining and leaving the domain using supported combinations
++ of client software (SSSD/Winbind) and membership software (adcli/Samba).
++ :id: realm-join-leave-combos
++ :steps:
++ 1. Install required packages for the specific combination.
++ 2. Join the domain explicitly setting client and membership software.
++ 3. Verify the join was successful.
++ 4. Leave the domain.
++ 5. Verify the leave was successful and config cleaned up (for SSSD).
++ """
++
++ client.host.conn.exec(["dnf", "install", "-y", "samba-winbind-clients"], raise_on_error=False)
++ client.fs.touch("/etc/krb5.conf")
++
++ join_args = [f"--client-software={client_software}", f"--membership-software={membership_software}", "--verbose"]
++
++ join_cmd = client.realm.join(
++ domain=provider.host.domain,
++ krb=False,
++ user=f"{provider.host.adminuser}@{provider.host.domain.upper()}",
++ password=provider.host.adminpw,
++ args=join_args,
++ )
++
++ assert join_cmd.rc == 0, (
++ f"Realm join failed for {client_software}+{membership_software}!\n" f"Stderr: {join_cmd.stderr}"
++ )
++
++ leave_cmd = client.realm.leave(
++ domain=provider.host.domain,
++ krb=False,
++ user=f"{provider.host.adminuser}@{provider.host.domain.upper()}",
++ password=provider.host.adminpw,
++ args=["--verbose", "--remove"],
++ )
++
++ assert leave_cmd.rc == 0, (
++ f"Realm leave failed for {client_software}+{membership_software}!\n" f"Stderr: {leave_cmd.stderr}"
++ )
++
++ if client_software == "sssd":
++ sssd_conf_path = "/etc/sssd/sssd.conf"
++
++ if client.host.fs.exists(sssd_conf_path):
++ content = client.host.fs.read(sssd_conf_path)
++
++ assert provider.host.domain not in content, (
++ f"Cleanup Failed: Domain '{provider.host.domain}' configuration "
++ f"still found in {sssd_conf_path} after leave."
++ )
++
++
++@pytest.mark.importance("high")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_permit_user_access_control(client: Client, provider: GenericADProvider):
++ """
++ :title: Allow and deny a user to locally log in
++ :steps:
++ 1. Join the domain.
++ 2. Create two users.
++ :steps:
++ 1. Permit user1 to log in locally.
++ 2. sssd.conf contains 'access_provider = simple' and allowed user
++ 3. Permitted user is able to log in locally
++ 4. Non-permitted user is not able to log in locally
++ 5. Permit user2 from log in locally
++ 6. Withdraw user1 from log in
++ :expectedresults:
++ 1. Permit command succeeds.
++ 2. SSSD configuration contains "access_provider" and "allowed user"
++ 3. Permitted user is allowed to log in local system.
++ 4. Non-permitted user is denied to log in local system.
++ 5. User2 able to log in
++ 6. User1 not able to log in
++ """
++ u = "testuser"
++ u2 = "testuser2"
++ provider.user(u).add()
++ provider.user(u2).add()
++
++ client.realm.join(
++ domain=provider.host.domain, user=provider.host.adminuser, password=provider.host.adminpw, krb=False
++ )
++
++ client.sssd.domain["use_fully_qualified_names"] = "False"
++ client.realm.permit(args=[f"{u}@{provider.host.domain}", "--verbose"])
++
++ sssd_conf_path = "/etc/sssd/sssd.conf"
++ if client.host.fs.exists(sssd_conf_path):
++ conf_content = client.host.fs.read(sssd_conf_path)
++ assert "access_provider = simple" in conf_content, "Access provider not set to simple after realm deny/permit"
++ assert f"simple_allow_users = {u}" in conf_content, f"User {u} not in simple_allow_users list"
++
++ assert client.auth.ssh.password(f"{u}@{provider.host.domain}", "Secret123"), f"User {u} log in failed!"
++ assert not client.auth.ssh.password(f"{u2}@{provider.host.domain}", "Secret123"), "Failed to DENY log in!"
++
++ client.realm.permit(args=[f"{u2}@{provider.host.domain}", "--verbose"])
++ assert client.auth.ssh.password(f"{u2}@{provider.host.domain}", "Secret123"), f"User {u2} log in failed!"
++
++ client.realm.permit(args=["--withdraw", f"{u}@{provider.host.domain}", "--verbose"])
++ assert not client.auth.ssh.password(f"{u}@{provider.host.domain}", "Secret123"), "Failed to DENY log in!"
++
++
++@pytest.mark.importance("high")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_permit_group_access_control(client: Client, provider: GenericADProvider):
++ """
++ :title: Allow and deny a group based log in
++ :steps:
++ 1. Join the domain.
++ 2. Create two users and a group.
++ 3. Add one user to the group.
++ :steps:
++ 1. Permit group-members to log in locally.
++ 2. sssd.conf contains 'access_provider = simple' and allowed user
++ 3. Permitted user is able to log in locally
++ 4. Non-permitted user is not able to log in locally
++ 5. Withdraw log in for group
++ :expectedresults:
++ 1. Permit command succeeds.
++ 2. SSSD configuration contains "access_provider" and "allowed user"
++ 3. Permitted user is allowed to log in local system.
++ 4. Non-permitted user is denied to log in local system.
++ 5. Groupmember user is denied to log in local system.
++ """
++ g = "testgroup"
++ ug1 = "user1"
++ u1 = provider.user(ug1).add()
++ ug2 = "user2"
++ provider.user(ug2).add()
++ provider.group(g).add().add_members([u1])
++
++ client.realm.join(
++ domain=provider.host.domain, user=provider.host.adminuser, password=provider.host.adminpw, krb=False
++ )
++
++ client.realm.permit(args=["--groups", g, "--verbose"])
++ i = client.tools.id(f"{ug1}@{provider.host.domain}")
++ assert ug1 in i.user.name, "User resolution failed!"
++ sssd_conf_path = "/etc/sssd/sssd.conf"
++ if client.host.fs.exists(sssd_conf_path):
++ conf_content = client.host.fs.read(sssd_conf_path)
++ assert "access_provider = simple" in conf_content, "Access provider not set to simple after realm deny/permit"
++ assert f"simple_allow_groups = {g}" in conf_content, f"group {g} not in simple_allow_groups list"
++
++ assert client.auth.ssh.password(f"{ug1}@{provider.host.domain}", "Secret123"), "User log in failed!"
++ assert not client.auth.ssh.password(f"{ug2}@{provider.host.domain}", "Secret123"), "Failed to DENY log in!"
++
++ client.realm.permit(args=["--withdraw", "--groups", g, "--verbose"])
++ assert not client.auth.ssh.password(f"{ug1}@{provider.host.domain}", "Secret123"), "Failed to DENY log in!"
+diff --git a/tests/topology_controllers.py b/tests/topology_controllers.py
+index 73a5c49..ab168f2 100644
+--- a/tests/topology_controllers.py
++++ b/tests/topology_controllers.py
+@@ -59,13 +59,42 @@ class ADTopologyController(ProvisionedBackupTopologyController):
+ """
+
+ @BackupTopologyController.restore_vanilla_on_error
+- def topology_setup(self, client: ClientHost, provider: ADHost) -> None:
++ def topology_setup(self, client: ClientHost, provider: ADHost | SambaHost) -> None:
+ if self.provisioned:
+ self.logger.info(f"Topology '{self.name}' is already provisioned")
+ return
+
+- # Remove any existing Kerberos configuration and keytab
+- client.fs.rm("/etc/krb5.conf")
++ domain = provider.domain.lower()
++ realm = provider.domain.upper()
++ # Fallback to hostname if the conn object doesn't have a direct IP/host attribute exposed
++ kdc_hostname = getattr(provider.conn, "host", provider.hostname)
++
++ # 1. Force the correct FQDN on the client VM
++ # client_fqdn = f"{client.hostname.split('.')[0]}.{domain}"
++ # client.conn.exec(["hostnamectl", "set-hostname", client_fqdn])
++
++ # 2. Write the strict Kerberos configuration (replaces removing it)
++ client.fs.write(
++ "/etc/krb5.conf",
++ f"""[libdefaults]
++ default_realm = {realm}
++ dns_lookup_realm = true
++ dns_lookup_kdc = true
++ rdns = false
++
++[realms]
++ {realm} = {{
++ kdc = {kdc_hostname}
++ admin_server = {kdc_hostname}
++ }}
++
++[domain_realm]
++ .{domain} = {realm}
++ {domain} = {realm}
++""",
++ )
++
++ # 3. Remove any existing keytab
+ client.fs.rm("/etc/krb5.keytab")
+
+ # Backup so we can restore to this state after each test
+@@ -83,11 +112,15 @@ class IPATopologyController(ProvisionedBackupTopologyController):
+ self.logger.info(f"Topology '{self.name}' is already provisioned")
+ return
+
+- #self.logger.info(f"Enrolling {client.hostname} into {ipa.domain}")
+ self.logger.info(f"{client.hostname} into {ipa.domain}")
+- self.logger.info("**************************"*77)
++ self.logger.info("**************************" * 77)
++
++ # 1. Force the correct FQDN on the client VM
++ domain = ipa.domain.lower()
++ client_fqdn = f"{client.hostname.split('.')[0]}.{domain}"
++ client.conn.exec(["hostnamectl", "set-hostname", client_fqdn])
+
+- # Remove any existing Kerberos configuration and keytab
++ # 2. Remove any existing Kerberos configuration and keytab (IPA requires a clean slate)
+ client.fs.rm("/etc/krb5.conf")
+ client.fs.rm("/etc/krb5.keytab")
+
+@@ -95,9 +128,6 @@ class IPATopologyController(ProvisionedBackupTopologyController):
+ client.fs.backup("/etc/ipa")
+ client.fs.backup("/var/lib/ipa-client")
+
+- # Join ipa domain
+- #client.conn.exec(["realm", "leave", ipa.domain], input=ipa.adminpw)
+-
+ # Backup so we can restore to this state after each test
+ super().topology_setup()
+
+--
+2.54.0
+
diff --git a/0015-Testcases-realm-renew.patch b/0015-Testcases-realm-renew.patch
new file mode 100644
index 0000000..67dcf55
--- /dev/null
+++ b/0015-Testcases-realm-renew.patch
@@ -0,0 +1,231 @@
+From 06e2a4945ee25f39d1c10be4c1ee2035412d76a3 Mon Sep 17 00:00:00 2001
+From: Shridhar Gadekar <shridhar.always@gmail.com>
+Date: Mon, 13 Apr 2026 23:28:14 +0530
+Subject: [PATCH 15/20] Testcases: realm renew
+
+Testcases added:
+ 1. realm renew update keytab
+ 2. join a pre-staged computer in delegated ou
+---
+ tests/test_realmd.py | 199 +++++++++++++++++++++++++++++++++++++++++++
+ 1 file changed, 199 insertions(+)
+
+diff --git a/tests/test_realmd.py b/tests/test_realmd.py
+index 6e25cb9..072fc38 100644
+--- a/tests/test_realmd.py
++++ b/tests/test_realmd.py
+@@ -3,8 +3,10 @@
+ from __future__ import annotations
+
+ import os
++import re
+ import sys
+ import time
++import base64
+ from typing import Any
+
+ import pytest
+@@ -389,3 +391,200 @@ def test_realm_permit_group_access_control(client: Client, provider: GenericADPr
+
+ client.realm.permit(args=["--withdraw", "--groups", g, "--verbose"])
+ assert not client.auth.ssh.password(f"{ug1}@{provider.host.domain}", "Secret123"), "Failed to DENY log in!"
++
++
++@pytest.mark.importance("high")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_renew_keytab(client: Client, provider: GenericADProvider):
++ """
++ :title: realm renew updates machine account password and increments KVNO
++ :steps:
++ 1. Join the client to the AD domain using realm.
++ 2. Check the initial KVNO of the machine account in the keytab.
++ 3. Execute realm.renew() to trigger a password renewal.
++ 4. Verify the KVNO in the keytab has incremented.
++ 5. Verify authentication succeeds with the new keytab using kinit.
++ 6. Verify the AD reports the matching updated KVNO using the kvno tool.
++ :expectedresults:
++ 1. Client joins successfully.
++ 2. realm renew completes without errors.
++ 3. The keytab contains new entries with a higher KVNO.
++ 4. kinit and kvno commands succeed against the DC.
++ """
++ short_hostname = client.host.hostname.split(".")[0].upper()
++ domain = provider.host.domain
++ realm = domain.upper()
++ machine_principal = f"{short_hostname}$@{realm}"
++ host_principal = f"host/{short_hostname}@{realm}"
++
++ # Step 1: Join the domain using native wrapper
++ join_command = client.realm.join(
++ domain=domain,
++ user=provider.host.adminuser,
++ password=provider.host.adminpw,
++ args=["--membership-software=adcli", "--verbose"],
++ )
++ assert join_command.rc == 0, f"realm join failed: {join_command.stderr}"
++
++ # Step 2: Get initial KVNO from keytab
++ res_klist1 = client.host.conn.run("klist -tk")
++ assert res_klist1.rc == 0, "Failed to read keytab"
++
++ # Extract all KVNOs for the machine principal and find the max (current)
++ kvnos1 = re.findall(rf"^\s*(\d+).*?{re.escape(machine_principal)}", res_klist1.stdout, re.MULTILINE)
++ assert kvnos1, "Could not find initial KVNO in keytab"
++ initial_kvno = max(int(k) for k in kvnos1)
++
++ # Step 3: Run the new realm renew subcommand via native wrapper
++ renew_command = client.realm.renew(domain=domain, args=["--computer-password-lifetime=0"])
++ assert renew_command.rc == 0, f"realm renew failed: {renew_command.stderr}"
++
++ # Step 4: Get updated KVNO from keytab and assert it incremented
++ res_klist2 = client.host.conn.run("klist -tk")
++ kvnos2 = re.findall(rf"^\s*(\d+).*?{re.escape(machine_principal)}", res_klist2.stdout, re.MULTILINE)
++ assert kvnos2, "Could not find new KVNO in keytab"
++ new_kvno = max(int(k) for k in kvnos2)
++
++ assert new_kvno > initial_kvno, f"KVNO did not increment! Old: {initial_kvno}, New: {new_kvno}"
++
++ # Step 5: Verify authentication with kinit using the machine account
++ res_kinit = client.host.conn.run(f"kinit -k '{machine_principal}'")
++ assert res_kinit.rc == 0, f"kinit failed after renew: {res_kinit.stderr}"
++
++ # Step 6: Verify AD agrees on the KVNO
++ res_kvno = client.host.conn.run(f"kvno '{host_principal}'")
++ assert res_kvno.rc == 0, f"kvno command failed: {res_kvno.stderr}"
++
++ # Parse 'kvno = X' from the output
++ kvno_match = re.search(r"kvno = (\d+)", res_kvno.stdout)
++ assert kvno_match, "Failed to parse kvno output from AD"
++ ad_kvno = int(kvno_match.group(1))
++
++ assert ad_kvno == new_kvno, f"AD KVNO ({ad_kvno}) does not match local keytab KVNO ({new_kvno})"
++
++ # Teardown: Clean up tickets and leave the domain using the native wrapper
++ client.host.conn.run("kdestroy", raise_on_error=False)
++ client.realm.leave(
++ domain=domain, user=provider.host.adminuser, password=provider.host.adminpw, raise_on_error=False
++ )
++
++
++@pytest.mark.importance("high")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_join_prestaged_delegated_explicit_ou(client: Client, provider: GenericADProvider):
++ """
++ :title: realm join pre-staged client with delegated user specifying computer-ou
++ :setup:
++ 1. Create a delegated OU and a delegated user in AD.
++ 2. Pre-stage the computer in the delegated OU using adcli (as Domain Admin).
++ 3. Explicitly strip dNSHostName and servicePrincipalName to simulate a blank object.
++ 4. Grant the delegated user rights to modify the computer object.
++ :steps:
++ 1. Join the client using realm as the delegated user, explicitly passing --computer-ou.
++ :expectedresults:
++ 1. realm successfully finds the computer object in the specified OU.
++ 2. realm successfully joins the domain and modifies the missing attributes (dNSHostName).
++ 3. Keytab entries are populated properly.
++ """
++ short_hostname = client.host.hostname.split(".")[0].upper()
++ ou_name = "DelegatedOU"
++ ou_dn = f"OU={ou_name},{provider.host.naming_context}"
++ delegate_user = "delegate_user"
++ delegate_pass = "Secret123!Qaz"
++
++ # Helper to run simple PowerShell commands for AD management
++ def run_ps(cmd: str):
++ import base64
++
++ encoded = base64.b64encode(cmd.encode("utf-16-le")).decode("utf-8")
++ return provider.host.conn.run(f"powershell -EncodedCommand {encoded}", raise_on_error=False)
++
++ # Setup 1: Clean previous runs, create the OU, and create the delegated user
++ setup_ps = f"""
++ $ErrorActionPreference = 'SilentlyContinue'
++ Remove-ADUser -Identity '{delegate_user}' -Confirm:$false
++ Set-ADOrganizationalUnit -Identity '{ou_dn}' -ProtectedFromAccidentalDeletion $false
++ Remove-ADOrganizationalUnit -Identity '{ou_dn}' -Recursive -Confirm:$false
++
++ $ErrorActionPreference = 'Stop'
++ New-ADOrganizationalUnit -Name '{ou_name}' -Path '{provider.host.naming_context}' -ProtectedFromAccidentalDeletion $false
++ $pwd = ConvertTo-SecureString "{delegate_pass}" -AsPlainText -Force
++ New-ADUser -Name "{delegate_user}" -SamAccountName "{delegate_user}" -AccountPassword $pwd -Enabled $true
++ """
++ run_ps(setup_ps)
++
++ try:
++ # Clean state on client before starting
++ client.host.conn.run("rm -f /etc/krb5.keytab", raise_on_error=False)
++
++ # Setup 2: Pre-stage the computer using the framework's adcli wrapper (As Admin)
++ preset_command = client.adcli.preset_computer(
++ domain=provider.host.domain,
++ login_user=provider.host.adminuser,
++ password=provider.host.adminpw,
++ args=["--verbose", f"--domain-ou={ou_dn}", short_hostname],
++ krb=False,
++ )
++ assert preset_command.rc == 0, f"adcli failed to pre-stage the computer: {preset_command.stderr}"
++
++ # Setup 3 & 4: Strip attributes and delegate permissions to the user
++ delegate_ps = f"""
++ $ErrorActionPreference = 'Stop'
++ Set-ADComputer -Identity '{short_hostname}' -Clear 'servicePrincipalName', 'dNSHostName'
++
++ $Comp = Get-ADComputer -Identity '{short_hostname}'
++ $User = Get-ADUser -Identity '{delegate_user}'
++ $Acl = Get-Acl -Path "AD:\\$($Comp.DistinguishedName)"
++
++ $Rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
++ $User.SID,
++ "WriteProperty, ExtendedRight",
++ "Allow",
++ [guid]"00000000-0000-0000-0000-000000000000"
++ )
++ $Acl.AddAccessRule($Rule)
++ Set-Acl -Path "AD:\\$($Comp.DistinguishedName)" -AclObject $Acl
++ """
++ run_ps(delegate_ps)
++
++ # Step 1: Join the domain using realm WITH explicit --computer-ou as the delegated user
++ join_command = client.realm.join(
++ domain=provider.host.domain,
++ user=delegate_user,
++ password=delegate_pass,
++ args=["--membership-software=adcli", "--verbose", f"--computer-ou={ou_dn}"],
++ krb=False,
++ )
++ assert (
++ join_command.rc == 0
++ ), f"realm failed to discover and join the pre-staged computer: {join_command.stderr}"
++
++ # Assert realmd performed the dNSHostName modification seen in your logs
++ assert (
++ "Modifying computer account: dNSHostName" in join_command.stderr
++ ), "realm did not repopulate dNSHostName as expected"
++
++ # Step 2: Verify the keytab was populated correctly
++ klist_cmd = client.host.conn.run("klist -kt")
++ assert klist_cmd.rc == 0, "Failed to read /etc/krb5.keytab after join"
++ assert short_hostname in klist_cmd.stdout.upper(), "Machine account not found in keytab"
++
++ finally:
++ # Teardown: Leave AD, remove keytab, and clean up the AD objects
++ try:
++ client.realm.leave(
++ domain=provider.host.domain, user=provider.host.adminuser, password=provider.host.adminpw
++ )
++ except Exception:
++ # Catching the exception ensures failure to leave doesn't block AD object cleanup
++ pass
++
++ client.host.conn.run("rm -f /etc/krb5.keytab", raise_on_error=False)
++
++ teardown_ps = f"""
++ $ErrorActionPreference = 'SilentlyContinue'
++ Remove-ADUser -Identity '{delegate_user}' -Confirm:$false
++ Set-ADOrganizationalUnit -Identity '{ou_dn}' -ProtectedFromAccidentalDeletion $false
++ Remove-ADOrganizationalUnit -Identity '{ou_dn}' -Recursive -Confirm:$false
++ """
++ run_ps(teardown_ps)
+--
+2.54.0
+
diff --git a/0016-Testcases-realm-renew-and-prejoin-testcases.patch b/0016-Testcases-realm-renew-and-prejoin-testcases.patch
new file mode 100644
index 0000000..d53fb0e
--- /dev/null
+++ b/0016-Testcases-realm-renew-and-prejoin-testcases.patch
@@ -0,0 +1,272 @@
+From 37c4850c5cfd6cba16bd13a6b5f129c6ae1817f7 Mon Sep 17 00:00:00 2001
+From: Shridhar Gadekar <shridhar.always@gmail.com>
+Date: Sat, 18 Apr 2026 00:24:57 +0530
+Subject: [PATCH 16/20] Testcases: realm renew and prejoin testcases
+
+Testcases added:
+ 1. realm renew keytab
+ 2. realm join a preset computer
+---
+ tests/test_realmd.py | 184 +++++++++++++++++--------------------------
+ 1 file changed, 73 insertions(+), 111 deletions(-)
+
+diff --git a/tests/test_realmd.py b/tests/test_realmd.py
+index 072fc38..ac51bf9 100644
+--- a/tests/test_realmd.py
++++ b/tests/test_realmd.py
+@@ -7,6 +7,7 @@ import re
+ import sys
+ import time
+ import base64
++import uuid
+ from typing import Any
+
+ import pytest
+@@ -417,7 +418,6 @@ def test_realm_renew_keytab(client: Client, provider: GenericADProvider):
+ machine_principal = f"{short_hostname}$@{realm}"
+ host_principal = f"host/{short_hostname}@{realm}"
+
+- # Step 1: Join the domain using native wrapper
+ join_command = client.realm.join(
+ domain=domain,
+ user=provider.host.adminuser,
+@@ -426,7 +426,7 @@ def test_realm_renew_keytab(client: Client, provider: GenericADProvider):
+ )
+ assert join_command.rc == 0, f"realm join failed: {join_command.stderr}"
+
+- # Step 2: Get initial KVNO from keytab
++ # Get initial KVNO from keytab
+ res_klist1 = client.host.conn.run("klist -tk")
+ assert res_klist1.rc == 0, "Failed to read keytab"
+
+@@ -435,11 +435,9 @@ def test_realm_renew_keytab(client: Client, provider: GenericADProvider):
+ assert kvnos1, "Could not find initial KVNO in keytab"
+ initial_kvno = max(int(k) for k in kvnos1)
+
+- # Step 3: Run the new realm renew subcommand via native wrapper
+ renew_command = client.realm.renew(domain=domain, args=["--computer-password-lifetime=0"])
+ assert renew_command.rc == 0, f"realm renew failed: {renew_command.stderr}"
+
+- # Step 4: Get updated KVNO from keytab and assert it incremented
+ res_klist2 = client.host.conn.run("klist -tk")
+ kvnos2 = re.findall(rf"^\s*(\d+).*?{re.escape(machine_principal)}", res_klist2.stdout, re.MULTILINE)
+ assert kvnos2, "Could not find new KVNO in keytab"
+@@ -447,144 +445,108 @@ def test_realm_renew_keytab(client: Client, provider: GenericADProvider):
+
+ assert new_kvno > initial_kvno, f"KVNO did not increment! Old: {initial_kvno}, New: {new_kvno}"
+
+- # Step 5: Verify authentication with kinit using the machine account
+ res_kinit = client.host.conn.run(f"kinit -k '{machine_principal}'")
+ assert res_kinit.rc == 0, f"kinit failed after renew: {res_kinit.stderr}"
+
+- # Step 6: Verify AD agrees on the KVNO
+ res_kvno = client.host.conn.run(f"kvno '{host_principal}'")
+ assert res_kvno.rc == 0, f"kvno command failed: {res_kvno.stderr}"
+
+- # Parse 'kvno = X' from the output
+ kvno_match = re.search(r"kvno = (\d+)", res_kvno.stdout)
+ assert kvno_match, "Failed to parse kvno output from AD"
+ ad_kvno = int(kvno_match.group(1))
+
+ assert ad_kvno == new_kvno, f"AD KVNO ({ad_kvno}) does not match local keytab KVNO ({new_kvno})"
+
+- # Teardown: Clean up tickets and leave the domain using the native wrapper
++ # Teardown
+ client.host.conn.run("kdestroy", raise_on_error=False)
+ client.realm.leave(
+- domain=domain, user=provider.host.adminuser, password=provider.host.adminpw, raise_on_error=False
++ domain=domain, user=provider.host.adminuser, password=provider.host.adminpw,
+ )
+
+
+ @pytest.mark.importance("high")
+-@pytest.mark.topology(KnownTopologyGroup.AnyAD)
+-def test_realm_join_prestaged_delegated_explicit_ou(client: Client, provider: GenericADProvider):
++@pytest.mark.topology(KnownTopology.AD)
++def test_realm_join_prestaged_delegated_explicit_ou(client: Client, provider: AD):
+ """
+ :title: realm join pre-staged client with delegated user specifying computer-ou
+ :setup:
+- 1. Create a delegated OU and a delegated user in AD.
++ 1. Create a dedicated OU and delegated user in AD.
+ 2. Pre-stage the computer in the delegated OU using adcli (as Domain Admin).
+- 3. Explicitly strip dNSHostName and servicePrincipalName to simulate a blank object.
++ 3. Strip critical attributes (dNSHostName, servicePrincipalName) to simulate incomplete setup.
+ 4. Grant the delegated user rights to modify the computer object.
+ :steps:
+- 1. Join the client using realm as the delegated user, explicitly passing --computer-ou.
++ 1. Join the client using realm as the delegated user with explicit --computer-ou parameter.
++ 2. Verify realm repopulates the missing dNSHostName attribute.
++ 3. Verify keytab is created with correct machine account principals.
+ :expectedresults:
+- 1. realm successfully finds the computer object in the specified OU.
+- 2. realm successfully joins the domain and modifies the missing attributes (dNSHostName).
+- 3. Keytab entries are populated properly.
++ 1. realm join succeeds without admin credentials.
++ 2. dNSHostName attribute is repopulated by realm.
++ 3. Keytab contains machine account principals.
+ """
++ DELEGATE_USER = f"joinuser_{uuid.uuid4().hex[:6]}"
++ DELEGATE_PASS = "Secret123!Qaz"
++ OU_NAME = f"DelegatedOU_{uuid.uuid4().hex[:4]}"
+ short_hostname = client.host.hostname.split(".")[0].upper()
+- ou_name = "DelegatedOU"
+- ou_dn = f"OU={ou_name},{provider.host.naming_context}"
+- delegate_user = "delegate_user"
+- delegate_pass = "Secret123!Qaz"
+-
+- # Helper to run simple PowerShell commands for AD management
+- def run_ps(cmd: str):
+- import base64
+-
+- encoded = base64.b64encode(cmd.encode("utf-16-le")).decode("utf-8")
+- return provider.host.conn.run(f"powershell -EncodedCommand {encoded}", raise_on_error=False)
+-
+- # Setup 1: Clean previous runs, create the OU, and create the delegated user
+- setup_ps = f"""
+- $ErrorActionPreference = 'SilentlyContinue'
+- Remove-ADUser -Identity '{delegate_user}' -Confirm:$false
+- Set-ADOrganizationalUnit -Identity '{ou_dn}' -ProtectedFromAccidentalDeletion $false
+- Remove-ADOrganizationalUnit -Identity '{ou_dn}' -Recursive -Confirm:$false
+-
+- $ErrorActionPreference = 'Stop'
+- New-ADOrganizationalUnit -Name '{ou_name}' -Path '{provider.host.naming_context}' -ProtectedFromAccidentalDeletion $false
+- $pwd = ConvertTo-SecureString "{delegate_pass}" -AsPlainText -Force
+- New-ADUser -Name "{delegate_user}" -SamAccountName "{delegate_user}" -AccountPassword $pwd -Enabled $true
+- """
+- run_ps(setup_ps)
++ ou_dn = f"OU={OU_NAME},{provider.host.naming_context}"
++ joined_domain = False
+
+ try:
+- # Clean state on client before starting
+- client.host.conn.run("rm -f /etc/krb5.keytab", raise_on_error=False)
++ # Cleanup and create OU
++ for cmd in [f"Remove-ADUser -Identity '{DELEGATE_USER}' -Confirm:$false -ErrorAction SilentlyContinue",
++ f"Remove-ADOrganizationalUnit -Identity '{ou_dn}' -Recursive -Confirm:$false -ErrorAction SilentlyContinue"]:
++ enc = base64.b64encode(cmd.encode('utf-16-le')).decode('utf-8')
++ provider.host.conn.run(f"powershell.exe -EncodedCommand {enc}", raise_on_error=False)
+
+- # Setup 2: Pre-stage the computer using the framework's adcli wrapper (As Admin)
+- preset_command = client.adcli.preset_computer(
+- domain=provider.host.domain,
+- login_user=provider.host.adminuser,
+- password=provider.host.adminpw,
+- args=["--verbose", f"--domain-ou={ou_dn}", short_hostname],
+- krb=False,
+- )
+- assert preset_command.rc == 0, f"adcli failed to pre-stage the computer: {preset_command.stderr}"
+-
+- # Setup 3 & 4: Strip attributes and delegate permissions to the user
+- delegate_ps = f"""
+- $ErrorActionPreference = 'Stop'
+- Set-ADComputer -Identity '{short_hostname}' -Clear 'servicePrincipalName', 'dNSHostName'
+-
+- $Comp = Get-ADComputer -Identity '{short_hostname}'
+- $User = Get-ADUser -Identity '{delegate_user}'
+- $Acl = Get-Acl -Path "AD:\\$($Comp.DistinguishedName)"
+-
+- $Rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
+- $User.SID,
+- "WriteProperty, ExtendedRight",
+- "Allow",
+- [guid]"00000000-0000-0000-0000-000000000000"
+- )
+- $Acl.AddAccessRule($Rule)
+- Set-Acl -Path "AD:\\$($Comp.DistinguishedName)" -AclObject $Acl
+- """
+- run_ps(delegate_ps)
+-
+- # Step 1: Join the domain using realm WITH explicit --computer-ou as the delegated user
+- join_command = client.realm.join(
+- domain=provider.host.domain,
+- user=delegate_user,
+- password=delegate_pass,
+- args=["--membership-software=adcli", "--verbose", f"--computer-ou={ou_dn}"],
+- krb=False,
+- )
+- assert (
+- join_command.rc == 0
+- ), f"realm failed to discover and join the pre-staged computer: {join_command.stderr}"
+-
+- # Assert realmd performed the dNSHostName modification seen in your logs
+- assert (
+- "Modifying computer account: dNSHostName" in join_command.stderr
+- ), "realm did not repopulate dNSHostName as expected"
+-
+- # Step 2: Verify the keytab was populated correctly
+- klist_cmd = client.host.conn.run("klist -kt")
+- assert klist_cmd.rc == 0, "Failed to read /etc/krb5.keytab after join"
+- assert short_hostname in klist_cmd.stdout.upper(), "Machine account not found in keytab"
++ enc = base64.b64encode(f"New-ADOrganizationalUnit -Name '{OU_NAME}' -Path '{provider.host.naming_context}'".encode('utf-16-le')).decode('utf-8')
++ assert provider.host.conn.run(f"powershell.exe -EncodedCommand {enc}", raise_on_error=False).rc == 0
+
+- finally:
+- # Teardown: Leave AD, remove keytab, and clean up the AD objects
+- try:
+- client.realm.leave(
+- domain=provider.host.domain, user=provider.host.adminuser, password=provider.host.adminpw
+- )
+- except Exception:
+- # Catching the exception ensures failure to leave doesn't block AD object cleanup
+- pass
++ # Create delegated user
++ ps = f"$pwd = ConvertTo-SecureString '{DELEGATE_PASS}' -AsPlainText -Force; New-ADUser -Name '{DELEGATE_USER}' -SamAccountName '{DELEGATE_USER}' -AccountPassword $pwd -Enabled $true"
++ enc = base64.b64encode(ps.encode('utf-16-le')).decode('utf-8')
++ assert provider.host.conn.run(f"powershell.exe -EncodedCommand {enc}", raise_on_error=False).rc == 0
+
++ # Setup client
+ client.host.conn.run("rm -f /etc/krb5.keytab", raise_on_error=False)
++ client.host.conn.run(f"echo '[libdefaults]\n default_realm = {provider.host.domain.upper()}\n dns_lookup_realm = true\n dns_lookup_kdc = true\n' > /etc/krb5.conf")
++
++ # Pre-stage computer
++ result = client.adcli.preset_computer(domain=provider.host.domain, login_user=provider.host.adminuser,
++ password=provider.host.adminpw, args=["--verbose", f"--domain-ou={ou_dn}", short_hostname], krb=False)
++ assert result.rc == 0, f"Pre-stage failed: {result.stderr}"
++
++ # Strip attributes
++ ldif = f"dn: CN={short_hostname},{ou_dn}\nchangetype: modify\ndelete: dNSHostName\n-\ndelete: servicePrincipalName\n-\n"
++ client.host.conn.run(f"cat > /tmp/clear_attrs.ldif <<'EOF'\n{ldif}\nEOF")
++ result = client.host.conn.run(f"ldapmodify -x -H ldap://{provider.host.hostname} -D '{provider.host.adminuser}@{provider.host.domain}' -w '{provider.host.adminpw}' -f /tmp/clear_attrs.ldif", raise_on_error=False)
++ assert result.rc in [0, 16], f"Strip attributes failed: {result.stderr}"
++
++ # Delegate permissions
++ ps = f"$Comp = Get-ADComputer -Identity '{short_hostname}'; $User = Get-ADUser -Identity '{DELEGATE_USER}'; $Acl = Get-Acl -Path \"AD:$($Comp.DistinguishedName)\"; $Rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule($User.SID, 'WriteProperty, ExtendedRight', 'Allow', [guid]'00000000-0000-0000-0000-000000000000'); $Acl.AddAccessRule($Rule); Set-Acl -Path \"AD:$($Comp.DistinguishedName)\" -AclObject $Acl"
++ enc = base64.b64encode(ps.encode('utf-16-le')).decode('utf-8')
++ assert provider.host.conn.run(f"powershell.exe -EncodedCommand {enc}", raise_on_error=False).rc == 0
++
++ # Join domain with delegated user
++ join_result = client.realm.join(domain=provider.host.domain, user=DELEGATE_USER, password=DELEGATE_PASS,
++ args=["--membership-software=adcli", "--verbose", f"--computer-ou={ou_dn}"], krb=False)
++ joined_domain = join_result.rc == 0
++ assert join_result.rc == 0, f"Join failed: {join_result.stderr}"
++ assert "Modifying computer account: dNSHostName" in join_result.stderr
++
++ # Verify keytab
++ klist = client.host.conn.run("klist -kt", raise_on_error=False)
++ assert klist.rc == 0 and short_hostname in klist.stdout.upper()
+
+- teardown_ps = f"""
+- $ErrorActionPreference = 'SilentlyContinue'
+- Remove-ADUser -Identity '{delegate_user}' -Confirm:$false
+- Set-ADOrganizationalUnit -Identity '{ou_dn}' -ProtectedFromAccidentalDeletion $false
+- Remove-ADOrganizationalUnit -Identity '{ou_dn}' -Recursive -Confirm:$false
+- """
+- run_ps(teardown_ps)
++ finally:
++ # Cleanup
++ if joined_domain:
++ try:
++ client.realm.leave(domain=provider.host.domain, user=provider.host.adminuser, password=provider.host.adminpw, args=["--remove"])
++ except:
++ client.host.conn.run(f"realm leave {provider.host.domain}", raise_on_error=False)
++
++ client.host.conn.run("rm -f /etc/krb5.keytab /tmp/clear_attrs.ldif", raise_on_error=False)
++ for cmd in [f"Remove-ADComputer -Identity '{short_hostname}' -Confirm:$false -ErrorAction SilentlyContinue",
++ f"Remove-ADUser -Identity '{DELEGATE_USER}' -Confirm:$false -ErrorAction SilentlyContinue",
++ f"Remove-ADOrganizationalUnit -Identity '{ou_dn}' -Recursive -Confirm:$false -ErrorAction SilentlyContinue"]:
++ enc = base64.b64encode(cmd.encode('utf-16-le')).decode('utf-8')
++ provider.host.conn.run(f"powershell.exe -EncodedCommand {enc}", raise_on_error=False)
+--
+2.54.0
+
diff --git a/0017-Fix-gboolean-typo-use-FALSE-TRUE-for-false-true.patch b/0017-Fix-gboolean-typo-use-FALSE-TRUE-for-false-true.patch
new file mode 100644
index 0000000..60ef8d8
--- /dev/null
+++ b/0017-Fix-gboolean-typo-use-FALSE-TRUE-for-false-true.patch
@@ -0,0 +1,53 @@
+From 1f91a9cf3af41a2dfe98bca6abe3e190d7f3c2d5 Mon Sep 17 00:00:00 2001
+From: Shridhar Gadekar <shridhar.always@gmail.com>
+Date: Wed, 29 Apr 2026 22:39:20 +0530
+Subject: [PATCH 17/20] Fix gboolean typo: use FALSE/TRUE for false/true
+
+The variables fully_qualified_names and has_fqn are of type gboolean
+from GLib, which uses FALSE and TRUE constants. Using C99's false and
+true is incorrect for this type.
+
+Fixed:
+- Line 346: *fully_qualified_names = FALSE;
+- Line 366: *fully_qualified_names = TRUE;
+- Line 392: gboolean has_fqn = FALSE;
+
+Signed-off-by: Shridhar Gadekar <shridhar.always@gmail.com>
+---
+ tools/realm-join.c | 6 +++---
+ 1 file changed, 3 insertions(+), 3 deletions(-)
+
+diff --git a/tools/realm-join.c b/tools/realm-join.c
+index cb290cc..6f6a0b3 100644
+--- a/tools/realm-join.c
++++ b/tools/realm-join.c
+@@ -343,7 +343,7 @@ realm_client_domain_has_fully_qualified_names (RealmClient *client,
+ provider = realm_client_get_provider (client);
+ realms = realm_dbus_provider_get_realms (provider);
+
+- *fully_qualified_names = false;
++ *fully_qualified_names = FALSE;
+ for (c = 0; realms && realms[c] != NULL; c++) {
+ g_clear_object (&realm);
+ realm = realm_client_get_realm (client, realms[c]);
+@@ -363,7 +363,7 @@ realm_client_domain_has_fully_qualified_names (RealmClient *client,
+ if (formats != NULL && formats[0] != NULL
+ && *formats[0] != '\0'
+ && strcmp (formats[0], "%U") != 0) {
+- *fully_qualified_names = true;
++ *fully_qualified_names = TRUE;
+ }
+
+ break;
+@@ -389,7 +389,7 @@ realm_join (RealmClient *client,
+ RealmJoinArgs args;
+ GOptionGroup *group;
+ gint ret = 0;
+- gboolean has_fqn = false;
++ gboolean has_fqn = FALSE;
+
+ GOptionEntry option_entries[] = {
+ { "automatic-id-mapping", 0, G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK,
+--
+2.54.0
+
diff --git a/0018-tests-Add-delegated-user-join-leave-test.patch b/0018-tests-Add-delegated-user-join-leave-test.patch
new file mode 100644
index 0000000..b94b8c8
--- /dev/null
+++ b/0018-tests-Add-delegated-user-join-leave-test.patch
@@ -0,0 +1,200 @@
+From 215ca592862de3ed6e308c0bbeb7d4181e7cd9e4 Mon Sep 17 00:00:00 2001
+From: Shridhar Gadekar <shridhar.always@gmail.com>
+Date: Thu, 30 Apr 2026 13:04:00 +0530
+Subject: [PATCH 18/20] tests: Add delegated user join/leave test
+
+Add test_realm_join_leave_delegated_user to verify that:
+- A delegated user (with CreateChild + GenericAll permissions) can realm join
+- The same delegated user can realm leave (without --remove)
+- realm join without automatic UID/GID mapping
+
+Signed-off-by: Shridhar Gadekar <shridhar.always@gmail.com>
+---
+ tests/test_realmd.py | 173 ++++++++++++++++++++++++++++++++++++++++++-
+ 1 file changed, 169 insertions(+), 4 deletions(-)
+
+diff --git a/tests/test_realmd.py b/tests/test_realmd.py
+index ac51bf9..da99333 100644
+--- a/tests/test_realmd.py
++++ b/tests/test_realmd.py
+@@ -545,8 +545,173 @@ def test_realm_join_prestaged_delegated_explicit_ou(client: Client, provider: AD
+ client.host.conn.run(f"realm leave {provider.host.domain}", raise_on_error=False)
+
+ client.host.conn.run("rm -f /etc/krb5.keytab /tmp/clear_attrs.ldif", raise_on_error=False)
+- for cmd in [f"Remove-ADComputer -Identity '{short_hostname}' -Confirm:$false -ErrorAction SilentlyContinue",
+- f"Remove-ADUser -Identity '{DELEGATE_USER}' -Confirm:$false -ErrorAction SilentlyContinue",
+- f"Remove-ADOrganizationalUnit -Identity '{ou_dn}' -Recursive -Confirm:$false -ErrorAction SilentlyContinue"]:
+- enc = base64.b64encode(cmd.encode('utf-16-le')).decode('utf-8')
++ for cmd in [
++ f"Remove-ADComputer -Identity '{short_hostname}' -Confirm:$false -ErrorAction SilentlyContinue",
++ f"Remove-ADUser -Identity '{DELEGATE_USER}' -Confirm:$false -ErrorAction SilentlyContinue",
++ (
++ f"Remove-ADOrganizationalUnit -Identity '{ou_dn}' "
++ f"-Recursive -Confirm:$false -ErrorAction SilentlyContinue"
++ ),
++ ]:
++ enc = base64.b64encode(cmd.encode("utf-16-le")).decode("utf-8")
+ provider.host.conn.run(f"powershell.exe -EncodedCommand {enc}", raise_on_error=False)
++
++
++@pytest.mark.importance("high")
++@pytest.mark.topology(KnownTopology.AD)
++def test_realm_join_leave_delegated_user(client: Client, provider: AD):
++ """
++ :title: realm join and leave using delegated user account
++ :setup:
++ 1. Create a delegated user with "Add workstations to domain" permission
++ :steps:
++ 1. Join the client using realm as the delegated user (not Domain Admin)
++ 2. Verify the join succeeds
++ 3. Verify keytab is created with correct machine account principals
++ 4. Leave the domain using realm with the same delegated user (specifying user implies --remove)
++ 5. Verify the leave succeeds
++ :expectedresults:
++ 1. realm join succeeds without admin credentials
++ 2. Computer account is created in AD
++ 3. Keytab contains machine account principals
++ 4. realm leave succeeds with delegated user
++ 5. Computer object is removed from AD (specifying user implies --remove per man page)
++ :customerscenario: False
++ """
++ DELEGATE_USER = f"joinuser_{uuid.uuid4().hex[:6]}"
++ DELEGATE_PASS = "Secret123!Qaz"
++ short_hostname = client.host.hostname.split(".")[0].upper()
++
++ # Create delegated user using test framework
++ delegate_user = provider.user(DELEGATE_USER).add(password=DELEGATE_PASS)
++
++ # Grant permissions to join computers in the default Computers container
++ ps = (
++ f"$User = Get-ADUser -Identity '{DELEGATE_USER}'; "
++ f"$Domain = Get-ADDomain; "
++ f"$ComputersContainer = 'CN=Computers,' + $Domain.DistinguishedName; "
++ f'$Acl = Get-Acl -Path "AD:$ComputersContainer"; '
++ f"$CompGuid = [guid]'bf967a86-0de6-11d0-a285-00aa003049e2'; "
++ # Grant CreateChild to create computer objects
++ f"$Rule1 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule("
++ f"$User.SID, 'CreateChild', 'Allow', $CompGuid); "
++ # Grant GenericAll on descendant computer objects to set all attributes
++ f"$Rule2 = New-Object System.DirectoryServices.ActiveDirectoryAccessRule("
++ f"$User.SID, 'GenericAll', 'Allow', 'Descendents', $CompGuid); "
++ f"$Acl.AddAccessRule($Rule1); "
++ f"$Acl.AddAccessRule($Rule2); "
++ f'Set-Acl -Path "AD:$ComputersContainer" -AclObject $Acl'
++ )
++ enc = base64.b64encode(ps.encode("utf-16-le")).decode("utf-8")
++ result = provider.host.conn.run(f"powershell.exe -EncodedCommand {enc}", raise_on_error=False)
++ assert result.rc == 0, f"Failed to grant join permissions: {result.stderr}!"
++
++ # Join domain with delegated user
++ join_result = client.realm.join(
++ domain=provider.host.domain,
++ user=DELEGATE_USER,
++ password=DELEGATE_PASS,
++ args=["--membership-software=adcli", "--verbose"],
++ krb=False,
++ )
++ assert join_result.rc == 0, f"Join failed: {join_result.stderr}!"
++
++ # Verify keytab
++ klist = client.host.conn.run("klist -kt", raise_on_error=False)
++ assert klist.rc == 0, f"klist failed: {klist.stderr}!"
++ assert short_hostname in klist.stdout.upper(), f"Machine account not in keytab: {klist.stdout}!"
++
++ # Verify computer object exists in AD using adcli
++ show_result = client.adcli.show_computer(
++ domain=provider.host.domain, login_user=DELEGATE_USER, password=DELEGATE_PASS, krb=False
++ )
++ assert show_result.rc == 0, f"Computer not found in AD: {show_result.stderr}!"
++
++ # Leave domain with delegated user (without --remove)
++ leave_result = client.realm.leave(domain=provider.host.domain, user=DELEGATE_USER, password=DELEGATE_PASS)
++ assert leave_result.rc == 0, f"Leave failed: {leave_result.stderr}!"
++
++ # Verify show-computer fails after leave (client is no longer joined)
++ show_after_leave = client.adcli.show_computer(
++ domain=provider.host.domain, login_user=DELEGATE_USER, password=DELEGATE_PASS, krb=False
++ )
++ assert show_after_leave.rc != 0, f"Show computer should fail after leave: {show_after_leave.stdout}!"
++
++
++@pytest.mark.importance("high")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_join_no_automatic_id_mapping(client: Client, provider: GenericADProvider):
++ """
++ :title: realm join without automatic UID/GID mapping
++ :setup:
++ 1. Client and AD/Samba domain ready
++ 2. Create a test user with explicit uidNumber and gidNumber in AD
++ :steps:
++ 1. Join the domain with --automatic-id-mapping=no option
++ 2. Verify the join succeeds
++ 3. Verify SSSD configuration has ldap_id_mapping = False
++ 4. Lookup the test user and verify it uses explicit UID/GID from AD
++ :expectedresults:
++ 1. realm join succeeds with --automatic-id-mapping=no
++ 2. Join completes successfully
++ 3. SSSD configuration shows ldap_id_mapping = False
++ 4. User lookup returns the explicit uidNumber/gidNumber from AD
++ :customerscenario: False
++ """
++ TEST_USER = f"testuser_{uuid.uuid4().hex[:6]}"
++ EXPLICIT_UID = 10001
++ EXPLICIT_GID = 10001
++
++ # Create test user with explicit UID/GID
++ test_user = provider.user(TEST_USER).add(uid=EXPLICIT_UID, gid=EXPLICIT_GID)
++
++ # Join domain without automatic ID mapping
++ join_result = client.realm.join(
++ domain=provider.host.domain,
++ user=provider.host.adminuser,
++ password=provider.host.adminpw,
++ args=["--automatic-id-mapping=no", "--verbose"],
++ )
++ assert join_result.rc == 0, f"Join failed: {join_result.stderr}!"
++
++ # Verify SSSD configuration has ID mapping disabled
++ sssd_conf = client.host.conn.run("cat /etc/sssd/sssd.conf", raise_on_error=False)
++ assert sssd_conf.rc == 0, f"Failed to read SSSD config: {sssd_conf.stderr}!"
++ assert "ldap_id_mapping = False" in sssd_conf.stdout, f"ID mapping not disabled in SSSD config: {sssd_conf.stdout}!"
++
++ # Lookup test user and verify explicit UID/GID (realmd requires fully-qualified names)
++ result = client.tools.id(f"{TEST_USER}@{provider.host.domain}")
++ assert result is not None, f"User {TEST_USER}@{provider.host.domain} not found!"
++ assert result.user.id == EXPLICIT_UID, f"Expected UID {EXPLICIT_UID}, got {result.user.id}!"
++ assert result.group.id == EXPLICIT_GID, f"Expected GID {EXPLICIT_GID}, got {result.group.id}!"
++
++
++@pytest.mark.importance("high")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_join_without_authentication(client: Client, provider: GenericADProvider):
++ """
++ :title: realm join should fail with invalid authentication
++ :setup:
++ 1. Client and AD/Samba domain ready
++ :steps:
++ 1. Attempt to join the domain with invalid credentials
++ 2. Verify the join fails
++ :expectedresults:
++ 1. realm join command fails
++ 2. Error indicates authentication failure
++ :customerscenario: False
++ """
++ # Attempt to join with invalid credentials
++ join_result = client.host.conn.run(
++ f"realm join --verbose -U invaliduser {provider.host.domain}",
++ input="wrongpassword\n",
++ raise_on_error=False,
++ )
++ assert join_result.rc != 0, f"Join should fail with invalid credentials but succeeded: {join_result.stdout}!"
++
++ # Verify error message indicates authentication issue
++ error_output = join_result.stderr.lower()
++ assert any(
++ keyword in error_output
++ for keyword in ["password", "credential", "authentication", "failed", "permission", "invalid", "not found"]
++ ), f"Expected authentication error but got: {join_result.stderr}!"
+--
+2.54.0
+
diff --git a/0019-Add-GitLab-CI-CD-pipeline-configuration.patch b/0019-Add-GitLab-CI-CD-pipeline-configuration.patch
new file mode 100644
index 0000000..acee30c
--- /dev/null
+++ b/0019-Add-GitLab-CI-CD-pipeline-configuration.patch
@@ -0,0 +1,466 @@
+From 5b2609adafa6fcc3c7694a61dd6ad7d90f5c3b41 Mon Sep 17 00:00:00 2001
+From: Shridhar Gadekar <shridhar.always@gmail.com>
+Date: Wed, 29 Apr 2026 00:13:05 +0530
+Subject: [PATCH 19/20] Add GitLab CI/CD pipeline configuration
+
+Adds automated CI/CD pipelines for realmd on GitLab freedesktop.org.
+Pipelines run automatically on:
+- Merge request creation
+- Pushes to main/master branch
+- Tag creation
+
+Pipeline stages:
+- Build: Compile realmd on Fedora and CentOS Stream 10
+- Test: Run pytest, shellcheck, documentation builds
+- Integration: Run pytest tests
+
+The configuration uses GitLab shared runners with container-based testing.
+Full integration testing with real AD/IPA requires custom runners or
+Testing Farm integration
+
+Signed-off-by: Shridhar Gadekar <shridhar.always@gmail.com>
+---
+ .gitlab-ci.yml | 85 ++++++++++++++++++++++++
+ tests/pyproject.toml | 39 ++++++++++--
+ tests/test_realmd.py | 117 +++++++++++++++++++++++-----------
+ tests/topology.py | 2 +-
+ tests/topology_controllers.py | 5 +-
+ 5 files changed, 202 insertions(+), 46 deletions(-)
+ create mode 100644 .gitlab-ci.yml
+
+diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
+new file mode 100644
+index 0000000..f91f717
+--- /dev/null
++++ b/.gitlab-ci.yml
+@@ -0,0 +1,85 @@
++# GitLab CI configuration for realmd
++# Runs tests automatically when MR is created
++
++# Use official freedesktop.org shared runners
++# https://gitlab.freedesktop.org/freedesktop/freedesktop/-/wikis/gitlab-ci
++
++stages:
++ - build
++ - test
++
++# Default settings for all jobs
++default:
++ # Use Fedora 44
++ image: registry.fedoraproject.org/fedora:44
++
++ before_script:
++ - dnf install -y git
++
++# Template for build jobs
++.build_template:
++ stage: build
++ script:
++ # Note: If you have a local spec file, prefer: dnf builddep -y path/to/realmd.spec
++ - dnf builddep realmd -y
++ - ./autogen.sh
++ - ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var
++ - make -j$(nproc)
++ - make check
++ artifacts:
++ paths:
++ - realmd
++ - service/realmd.service
++ expire_in: 1 hour
++ reports:
++ junit: test-suite.log
++
++# Build on Fedora 44
++build:fedora:
++ extends: .build_template
++ image: registry.fedoraproject.org/fedora:44
++
++# Build on CentOS Stream 10
++build:centos10:
++ extends: .build_template
++ image: quay.io/centos/centos:stream10
++ before_script:
++ - dnf install -y 'dnf-command(config-manager)'
++ - dnf config-manager --set-enabled crb
++ - dnf install -y git
++
++# Python code quality checks (using Ruff - fast, all-in-one linter)
++lint:python:
++ stage: test
++ image: registry.fedoraproject.org/fedora:44
++ script:
++ # Install ruff natively via dnf to avoid Python PEP 668 externally-managed errors
++ - dnf install -y ruff
++ - |
++ if [ -d "tests" ]; then
++ cd tests
++ # Only lint pytest test files, not legacy Python 2 D-Bus examples
++ TEST_FILES=$(find . -name "test_*.py" -o -name "topology*.py" -o -name "conftest.py" -o -name "__init__.py" | tr '\n' ' ')
++ if [ -z "$TEST_FILES" ]; then
++ echo "No Python test files found, skipping Python linting"
++ exit 0
++ fi
++ echo "Running ruff check (replaces flake8, isort, pycodestyle)..."
++ ruff check $TEST_FILES
++ echo "Running ruff format check (replaces black)..."
++ ruff format --check $TEST_FILES
++ else
++ echo "No tests directory found, skipping Python linting"
++ fi
++ rules:
++ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
++
++# Workflow rules - when to run pipelines
++workflow:
++ rules:
++ # Run on merge requests
++ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
++ # Run on main/master branch
++ - if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master"'
++ # Run on tags
++ - if: '$CI_COMMIT_TAG'
+diff --git a/tests/pyproject.toml b/tests/pyproject.toml
+index 3cfce46..be729ba 100644
+--- a/tests/pyproject.toml
++++ b/tests/pyproject.toml
+@@ -1,5 +1,6 @@
+ [tool.mypy]
+ exclude = "docs"
++disable_error_code = ["attr-defined", "assignment", "operator", "union-attr"]
+
+ [[tool.mypy.overrides]]
+ module = "jc.*"
+@@ -9,10 +10,38 @@ ignore_missing_imports = true
+ module = "ldap.*"
+ ignore_missing_imports = true
+
+-[tool.isort]
+-line_length = 119
+-profile = "black"
+-add_imports = "from __future__ import annotations"
++[[tool.mypy.overrides]]
++module = "pytest_mh.*"
++ignore_missing_imports = true
++
++[[tool.mypy.overrides]]
++module = "sssd_test_framework.*"
++ignore_missing_imports = true
+
+-[tool.black]
++[[tool.mypy.overrides]]
++module = "pytest.*"
++ignore_missing_imports = true
++
++# Ruff configuration - replaces black, isort, flake8, pycodestyle
++[tool.ruff]
+ line-length = 119
++target-version = "py311"
++
++# Linting rules
++[tool.ruff.lint]
++select = [
++ "E", # pycodestyle errors
++ "W", # pycodestyle warnings
++ "F", # pyflakes
++ "I", # isort
++]
++ignore = []
++
++# Formatting (black-compatible)
++[tool.ruff.format]
++quote-style = "double"
++indent-style = "space"
++
++# Import sorting (isort-compatible)
++[tool.ruff.lint.isort]
++required-imports = ["from __future__ import annotations"]
+diff --git a/tests/test_realmd.py b/tests/test_realmd.py
+index da99333..f4e8f4a 100644
+--- a/tests/test_realmd.py
++++ b/tests/test_realmd.py
+@@ -2,27 +2,21 @@
+
+ from __future__ import annotations
+
+-import os
+-import re
+-import sys
+-import time
+ import base64
++import re
+ import uuid
+-from typing import Any
+
+ import pytest
+ from sssd_test_framework.roles.ad import AD
+ from sssd_test_framework.roles.client import Client
+ from sssd_test_framework.roles.generic import GenericADProvider
+-from sssd_test_framework.roles.ipa import IPA
+-from sssd_test_framework.utils.realmd import RealmUtils
+
+ from .topology import KnownTopology, KnownTopologyGroup
+
+
+ @pytest.mark.importance("critical")
+ @pytest.mark.topology(KnownTopologyGroup.AnyAD)
+-def test_realm_discover(client: Client, provider: Any):
++def test_realm_discover(client: Client, provider: GenericADProvider):
+ """
+ :title: realm discover a domain
+ :steps:
+@@ -36,7 +30,7 @@ def test_realm_discover(client: Client, provider: Any):
+
+ @pytest.mark.importance("critical")
+ @pytest.mark.topology(KnownTopologyGroup.AnyAD)
+-def test_realm_join(client: Client, provider: Any):
++def test_realm_join(client: Client, provider: GenericADProvider):
+ """
+ :title: realm join
+ :steps:
+@@ -56,7 +50,7 @@ def test_realm_join(client: Client, provider: Any):
+
+ @pytest.mark.importance("critical")
+ @pytest.mark.topology(KnownTopologyGroup.AnyAD)
+-def test_realm_leave(client: Client, provider: Any):
++def test_realm_leave(client: Client, provider: GenericADProvider):
+ """
+ :title: realm leave
+ :setup:
+@@ -84,7 +78,7 @@ def test_realm_leave(client: Client, provider: Any):
+
+ @pytest.mark.importance("critical")
+ @pytest.mark.topology(KnownTopologyGroup.AnyAD)
+-def test_realm_list(client: Client, provider: Any):
++def test_realm_list(client: Client, provider: GenericADProvider):
+ """
+ :title: realm list available domains
+ :steps:
+@@ -99,7 +93,7 @@ def test_realm_list(client: Client, provider: Any):
+
+ @pytest.mark.importance("critical")
+ @pytest.mark.topology(KnownTopologyGroup.AnyAD)
+-def test_realm_join_no_config_modification(client: Client, provider: Any):
++def test_realm_join_no_config_modification(client: Client, provider: GenericADProvider):
+ """
+ :title: realm join without modifying local config
+ :steps:
+@@ -145,7 +139,7 @@ def test_realm_join_no_config_modification(client: Client, provider: Any):
+
+ @pytest.mark.importance("critical")
+ @pytest.mark.topology(KnownTopologyGroup.AnyAD)
+-def test_realm_leave_remove_computer(client: Client, provider: Any):
++def test_realm_leave_remove_computer(client: Client, provider: GenericADProvider):
+ """
+ :title: Realm leave remove computer
+ :steps:
+@@ -267,7 +261,7 @@ def test_realm_join_leave_combinations(
+ )
+
+ assert join_cmd.rc == 0, (
+- f"Realm join failed for {client_software}+{membership_software}!\n" f"Stderr: {join_cmd.stderr}"
++ f"Realm join failed for {client_software}+{membership_software}!\nStderr: {join_cmd.stderr}"
+ )
+
+ leave_cmd = client.realm.leave(
+@@ -279,7 +273,7 @@ def test_realm_join_leave_combinations(
+ )
+
+ assert leave_cmd.rc == 0, (
+- f"Realm leave failed for {client_software}+{membership_software}!\n" f"Stderr: {leave_cmd.stderr}"
++ f"Realm leave failed for {client_software}+{membership_software}!\nStderr: {leave_cmd.stderr}"
+ )
+
+ if client_software == "sssd":
+@@ -460,7 +454,9 @@ def test_realm_renew_keytab(client: Client, provider: GenericADProvider):
+ # Teardown
+ client.host.conn.run("kdestroy", raise_on_error=False)
+ client.realm.leave(
+- domain=domain, user=provider.host.adminuser, password=provider.host.adminpw,
++ domain=domain,
++ user=provider.host.adminuser,
++ password=provider.host.adminpw,
+ )
+
+
+@@ -492,42 +488,84 @@ def test_realm_join_prestaged_delegated_explicit_ou(client: Client, provider: AD
+
+ try:
+ # Cleanup and create OU
+- for cmd in [f"Remove-ADUser -Identity '{DELEGATE_USER}' -Confirm:$false -ErrorAction SilentlyContinue",
+- f"Remove-ADOrganizationalUnit -Identity '{ou_dn}' -Recursive -Confirm:$false -ErrorAction SilentlyContinue"]:
+- enc = base64.b64encode(cmd.encode('utf-16-le')).decode('utf-8')
++ for cmd in [
++ f"Remove-ADUser -Identity '{DELEGATE_USER}' -Confirm:$false -ErrorAction SilentlyContinue",
++ (
++ f"Remove-ADOrganizationalUnit -Identity '{ou_dn}' "
++ f"-Recursive -Confirm:$false -ErrorAction SilentlyContinue"
++ ),
++ ]:
++ enc = base64.b64encode(cmd.encode("utf-16-le")).decode("utf-8")
+ provider.host.conn.run(f"powershell.exe -EncodedCommand {enc}", raise_on_error=False)
+
+- enc = base64.b64encode(f"New-ADOrganizationalUnit -Name '{OU_NAME}' -Path '{provider.host.naming_context}'".encode('utf-16-le')).decode('utf-8')
++ enc = base64.b64encode(
++ f"New-ADOrganizationalUnit -Name '{OU_NAME}' -Path '{provider.host.naming_context}'".encode("utf-16-le")
++ ).decode("utf-8")
+ assert provider.host.conn.run(f"powershell.exe -EncodedCommand {enc}", raise_on_error=False).rc == 0
+
+ # Create delegated user
+- ps = f"$pwd = ConvertTo-SecureString '{DELEGATE_PASS}' -AsPlainText -Force; New-ADUser -Name '{DELEGATE_USER}' -SamAccountName '{DELEGATE_USER}' -AccountPassword $pwd -Enabled $true"
+- enc = base64.b64encode(ps.encode('utf-16-le')).decode('utf-8')
++ ps = (
++ f"$pwd = ConvertTo-SecureString '{DELEGATE_PASS}' -AsPlainText -Force; "
++ f"New-ADUser -Name '{DELEGATE_USER}' -SamAccountName '{DELEGATE_USER}' "
++ f"-AccountPassword $pwd -Enabled $true"
++ )
++ enc = base64.b64encode(ps.encode("utf-16-le")).decode("utf-8")
+ assert provider.host.conn.run(f"powershell.exe -EncodedCommand {enc}", raise_on_error=False).rc == 0
+
+ # Setup client
+ client.host.conn.run("rm -f /etc/krb5.keytab", raise_on_error=False)
+- client.host.conn.run(f"echo '[libdefaults]\n default_realm = {provider.host.domain.upper()}\n dns_lookup_realm = true\n dns_lookup_kdc = true\n' > /etc/krb5.conf")
++ krb5_conf = (
++ f"echo '[libdefaults]\n default_realm = {provider.host.domain.upper()}\n"
++ f" dns_lookup_realm = true\n dns_lookup_kdc = true\n' > /etc/krb5.conf"
++ )
++ client.host.conn.run(krb5_conf)
+
+ # Pre-stage computer
+- result = client.adcli.preset_computer(domain=provider.host.domain, login_user=provider.host.adminuser,
+- password=provider.host.adminpw, args=["--verbose", f"--domain-ou={ou_dn}", short_hostname], krb=False)
++ result = client.adcli.preset_computer(
++ domain=provider.host.domain,
++ login_user=provider.host.adminuser,
++ password=provider.host.adminpw,
++ args=["--verbose", f"--domain-ou={ou_dn}", short_hostname],
++ krb=False,
++ )
+ assert result.rc == 0, f"Pre-stage failed: {result.stderr}"
+
+ # Strip attributes
+- ldif = f"dn: CN={short_hostname},{ou_dn}\nchangetype: modify\ndelete: dNSHostName\n-\ndelete: servicePrincipalName\n-\n"
++ ldif = (
++ f"dn: CN={short_hostname},{ou_dn}\nchangetype: modify\n"
++ f"delete: dNSHostName\n-\ndelete: servicePrincipalName\n-\n"
++ )
+ client.host.conn.run(f"cat > /tmp/clear_attrs.ldif <<'EOF'\n{ldif}\nEOF")
+- result = client.host.conn.run(f"ldapmodify -x -H ldap://{provider.host.hostname} -D '{provider.host.adminuser}@{provider.host.domain}' -w '{provider.host.adminpw}' -f /tmp/clear_attrs.ldif", raise_on_error=False)
++ ldap_cmd = (
++ f"ldapmodify -x -H ldap://{provider.host.hostname} "
++ f"-D '{provider.host.adminuser}@{provider.host.domain}' "
++ f"-w '{provider.host.adminpw}' -f /tmp/clear_attrs.ldif"
++ )
++ result = client.host.conn.run(ldap_cmd, raise_on_error=False)
+ assert result.rc in [0, 16], f"Strip attributes failed: {result.stderr}"
+
+ # Delegate permissions
+- ps = f"$Comp = Get-ADComputer -Identity '{short_hostname}'; $User = Get-ADUser -Identity '{DELEGATE_USER}'; $Acl = Get-Acl -Path \"AD:$($Comp.DistinguishedName)\"; $Rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule($User.SID, 'WriteProperty, ExtendedRight', 'Allow', [guid]'00000000-0000-0000-0000-000000000000'); $Acl.AddAccessRule($Rule); Set-Acl -Path \"AD:$($Comp.DistinguishedName)\" -AclObject $Acl"
+- enc = base64.b64encode(ps.encode('utf-16-le')).decode('utf-8')
++ ps = (
++ f"$Comp = Get-ADComputer -Identity '{short_hostname}'; "
++ f"$User = Get-ADUser -Identity '{DELEGATE_USER}'; "
++ f'$Acl = Get-Acl -Path "AD:$($Comp.DistinguishedName)"; '
++ f"$Rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule("
++ f"$User.SID, 'WriteProperty, ExtendedRight', 'Allow', "
++ f"[guid]'00000000-0000-0000-0000-000000000000'); "
++ f"$Acl.AddAccessRule($Rule); "
++ f'Set-Acl -Path "AD:$($Comp.DistinguishedName)" -AclObject $Acl'
++ )
++ enc = base64.b64encode(ps.encode("utf-16-le")).decode("utf-8")
+ assert provider.host.conn.run(f"powershell.exe -EncodedCommand {enc}", raise_on_error=False).rc == 0
+
+ # Join domain with delegated user
+- join_result = client.realm.join(domain=provider.host.domain, user=DELEGATE_USER, password=DELEGATE_PASS,
+- args=["--membership-software=adcli", "--verbose", f"--computer-ou={ou_dn}"], krb=False)
++ join_result = client.realm.join(
++ domain=provider.host.domain,
++ user=DELEGATE_USER,
++ password=DELEGATE_PASS,
++ args=["--membership-software=adcli", "--verbose", f"--computer-ou={ou_dn}"],
++ krb=False,
++ )
+ joined_domain = join_result.rc == 0
+ assert join_result.rc == 0, f"Join failed: {join_result.stderr}"
+ assert "Modifying computer account: dNSHostName" in join_result.stderr
+@@ -540,8 +578,13 @@ def test_realm_join_prestaged_delegated_explicit_ou(client: Client, provider: AD
+ # Cleanup
+ if joined_domain:
+ try:
+- client.realm.leave(domain=provider.host.domain, user=provider.host.adminuser, password=provider.host.adminpw, args=["--remove"])
+- except:
++ client.realm.leave(
++ domain=provider.host.domain,
++ user=provider.host.adminuser,
++ password=provider.host.adminpw,
++ args=["--remove"],
++ )
++ except Exception:
+ client.host.conn.run(f"realm leave {provider.host.domain}", raise_on_error=False)
+
+ client.host.conn.run("rm -f /etc/krb5.keytab /tmp/clear_attrs.ldif", raise_on_error=False)
+@@ -583,7 +626,7 @@ def test_realm_join_leave_delegated_user(client: Client, provider: AD):
+ short_hostname = client.host.hostname.split(".")[0].upper()
+
+ # Create delegated user using test framework
+- delegate_user = provider.user(DELEGATE_USER).add(password=DELEGATE_PASS)
++ provider.user(DELEGATE_USER).add(password=DELEGATE_PASS)
+
+ # Grant permissions to join computers in the default Computers container
+ ps = (
+@@ -663,7 +706,7 @@ def test_realm_join_no_automatic_id_mapping(client: Client, provider: GenericADP
+ EXPLICIT_GID = 10001
+
+ # Create test user with explicit UID/GID
+- test_user = provider.user(TEST_USER).add(uid=EXPLICIT_UID, gid=EXPLICIT_GID)
++ provider.user(TEST_USER).add(uid=EXPLICIT_UID, gid=EXPLICIT_GID)
+
+ # Join domain without automatic ID mapping
+ join_result = client.realm.join(
+@@ -677,7 +720,9 @@ def test_realm_join_no_automatic_id_mapping(client: Client, provider: GenericADP
+ # Verify SSSD configuration has ID mapping disabled
+ sssd_conf = client.host.conn.run("cat /etc/sssd/sssd.conf", raise_on_error=False)
+ assert sssd_conf.rc == 0, f"Failed to read SSSD config: {sssd_conf.stderr}!"
+- assert "ldap_id_mapping = False" in sssd_conf.stdout, f"ID mapping not disabled in SSSD config: {sssd_conf.stdout}!"
++ assert "ldap_id_mapping = False" in sssd_conf.stdout, (
++ f"ID mapping not disabled in SSSD config: {sssd_conf.stdout}!"
++ )
+
+ # Lookup test user and verify explicit UID/GID (realmd requires fully-qualified names)
+ result = client.tools.id(f"{TEST_USER}@{provider.host.domain}")
+diff --git a/tests/topology.py b/tests/topology.py
+index 7d85910..5a60f8d 100644
+--- a/tests/topology.py
++++ b/tests/topology.py
+@@ -6,8 +6,8 @@ from enum import unique
+ from typing import final
+
+ from pytest_mh import KnownTopologyBase, KnownTopologyGroupBase, Topology, TopologyDomain
+-
+ from sssd_test_framework.config import SSSDTopologyMark
++
+ from .topology_controllers import (
+ ADTopologyController,
+ IPATopologyController,
+diff --git a/tests/topology_controllers.py b/tests/topology_controllers.py
+index ab168f2..4ec95a9 100644
+--- a/tests/topology_controllers.py
++++ b/tests/topology_controllers.py
+@@ -1,14 +1,11 @@
+ from __future__ import annotations
+
+ from pytest_mh import BackupTopologyController
+-from pytest_mh.conn import ProcessResult
+-
+ from sssd_test_framework.config import SSSDMultihostConfig
+ from sssd_test_framework.hosts.ad import ADHost
+ from sssd_test_framework.hosts.client import ClientHost
+-from sssd_test_framework.hosts.samba import SambaHost
+ from sssd_test_framework.hosts.ipa import IPAHost
+-from sssd_test_framework.misc.ssh import retry_command
++from sssd_test_framework.hosts.samba import SambaHost
+
+ __all__ = [
+ "ADTopologyController",
+--
+2.54.0
+
diff --git a/0020-Tests-realmd-testcase-set-8.patch b/0020-Tests-realmd-testcase-set-8.patch
new file mode 100644
index 0000000..4bbb5a9
--- /dev/null
+++ b/0020-Tests-realmd-testcase-set-8.patch
@@ -0,0 +1,400 @@
+From c0c8f6b1e1347ee75c86ee06579ccb4b86b5e534 Mon Sep 17 00:00:00 2001
+From: Shridhar Gadekar <shridhar.always@gmail.com>
+Date: Thu, 23 Apr 2026 15:42:22 +0530
+Subject: [PATCH 20/20] Tests: realmd testcase set 8
+
+Added following scenarios:
+ 1. with `realm join` populate OS-name, os-version attributes
+ 2. realm join with qualified-names disabled using winbind
+ 3. realm join with qualified-names disabled
+ 4. realm join with modified default-home and default-shell
+
+Signed-off-by: Shridhar Gadekar <shridhar.always@gmail.com>
+---
+ tests/test_realmd.py | 319 ++++++++++++++++++++++++++++++++++
+ tests/topology_controllers.py | 21 ++-
+ 2 files changed, 338 insertions(+), 2 deletions(-)
+
+diff --git a/tests/test_realmd.py b/tests/test_realmd.py
+index f4e8f4a..8c4b475 100644
+--- a/tests/test_realmd.py
++++ b/tests/test_realmd.py
+@@ -4,6 +4,7 @@ from __future__ import annotations
+
+ import base64
+ import re
++import time
+ import uuid
+
+ import pytest
+@@ -731,6 +732,65 @@ def test_realm_join_no_automatic_id_mapping(client: Client, provider: GenericADP
+ assert result.group.id == EXPLICIT_GID, f"Expected GID {EXPLICIT_GID}, got {result.group.id}!"
+
+
++@pytest.mark.importance("high")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_join_custom_computer_and_os_attributes(client: Client, provider: GenericADProvider):
++ """
++ :title: realm join with custom computer-name, os-name, and os-version
++ :steps:
++ 1. Join domain with --computer-name, --os-name, and --os-version parameters
++ 2. Use adcli show-computer to retrieve computer object attributes from AD
++ 3. Verify computer name, operatingSystem, and operatingSystemVersion are set correctly
++ 4. Leave domain with --remove to cleanup
++ :expectedresults:
++ 1. Domain join succeeds with custom parameters
++ 2. adcli show-computer retrieves computer object attributes successfully
++ 3. All custom attributes are set correctly (computer name, operatingSystem, operatingSystemVersion)
++ 4. Domain leave succeeds and computer object is removed
++ :customerscenario: False
++ """
++ custom_computer_name = "testpc-" + str(uuid.uuid4())[:8]
++ custom_os_name = "TestOS-Linux"
++ custom_os_version = "1.2.3.4.5"
++
++ domain = provider.host.domain
++
++ # Join with all custom attributes
++ join_result = client.realm.join(
++ domain=domain,
++ user=provider.host.adminuser,
++ password=provider.host.adminpw,
++ args=[
++ f"--computer-name={custom_computer_name}",
++ f"--os-name={custom_os_name}",
++ f"--os-version={custom_os_version}",
++ "--verbose",
++ ],
++ krb=False,
++ )
++
++ assert join_result.rc == 0, f"realm join failed: {join_result.stderr}"
++ assert "Successfully enrolled machine in realm" in join_result.stderr, "realm join success message not found"
++
++ # Query computer object using adcli show-computer
++ show_result = client.adcli.show_computer(
++ domain=domain,
++ login_user=provider.host.adminuser,
++ password=provider.host.adminpw,
++ args=["--verbose", custom_computer_name],
++ krb=False,
++ )
++
++ assert show_result.rc == 0, f"adcli show-computer failed: {show_result.stderr}!"
++
++ # Verify all custom attributes in output (check both stdout and stderr)
++ output = (show_result.stdout + show_result.stderr).lower()
++
++ assert custom_computer_name.lower() in output, f"Computer name '{custom_computer_name}' not found in AD!"
++ assert custom_os_name.lower() in output, f"operatingSystem '{custom_os_name}' not found in AD!"
++ assert custom_os_version.lower() in output, f"operatingSystemVersion '{custom_os_version}' not found in AD!"
++
++
+ @pytest.mark.importance("high")
+ @pytest.mark.topology(KnownTopologyGroup.AnyAD)
+ def test_realm_join_without_authentication(client: Client, provider: GenericADProvider):
+@@ -760,3 +820,262 @@ def test_realm_join_without_authentication(client: Client, provider: GenericADPr
+ keyword in error_output
+ for keyword in ["password", "credential", "authentication", "failed", "permission", "invalid", "not found"]
+ ), f"Expected authentication error but got: {join_result.stderr}!"
++
++
++@pytest.mark.importance("high")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_join_modified_default_home_and_shell(client: Client, provider: GenericADProvider):
++ """
++ :title: realm join with modified default-home and default-shell
++ :setup:
++ 1. Client and AD domain are available
++ :steps:
++ 1. Configure /etc/realmd.conf with custom default-home and default-shell
++ 2. Restart realmd service to apply configuration
++ 3. Join domain
++ 4. Create a test user on AD
++ 5. Query user information with getent passwd
++ 6. Verify home directory matches custom path pattern
++ 7. Verify shell matches custom setting
++ 8. Verify sssd.conf contains the custom settings
++ :expectedresults:
++ 1. Configuration file created successfully
++ 2. realmd service restarts successfully
++ 3. Domain join succeeds
++ 4. Test user created successfully
++ 5. getent returns user information
++ 6. User home directory uses custom path: /home/<domain>/test/<user>
++ 7. User shell is /bin/sh
++ 8. sssd.conf contains default_shell and fallback_homedir settings
++ :customerscenario: False
++ """
++ domain = provider.host.domain
++ custom_home = "/home/%D/test/%U"
++ custom_shell = "/bin/sh"
++
++ # Configure /etc/realmd.conf with custom home and shell
++ realmd_conf = f"""[users]
++default-home = {custom_home}
++default-shell = {custom_shell}
++"""
++ client.fs.write("/etc/realmd.conf", realmd_conf)
++
++ # Restart realmd service to apply configuration
++ client.svc.restart("realmd.service")
++
++ # Join domain
++ join_result = client.realm.join(
++ domain=domain,
++ user=provider.host.adminuser,
++ password=provider.host.adminpw,
++ args=["--verbose"],
++ krb=False,
++ )
++
++ assert join_result.rc == 0, f"realm join failed: {join_result.stderr}"
++ assert "Successfully enrolled machine in realm" in join_result.stderr, "realm join success message not found"
++
++ # Create a test user on AD to verify
++ user = provider.user("testuser1").add()
++
++ # Query user information
++ user_query = f"{user.name}@{domain}"
++ getent_result = client.tools.getent.passwd(user_query)
++
++ assert getent_result is not None, f"getent passwd failed for user '{user_query}'"
++
++ # Verify home directory and shell in getent output
++ expected_home = f"/home/{domain}/test/{user.name}"
++
++ assert getent_result.home == expected_home, (
++ f"Custom home directory mismatch: expected '{expected_home}', got '{getent_result.home}'"
++ )
++ assert getent_result.shell == custom_shell, (
++ f"Custom shell mismatch: expected '{custom_shell}', got '{getent_result.shell}'"
++ )
++
++ # Verify sssd.conf contains the custom settings
++ sssd_conf = client.fs.read("/etc/sssd/sssd.conf")
++
++ assert "default_shell = /bin/sh" in sssd_conf, "default_shell not found in sssd.conf"
++ assert "fallback_homedir = /home/%d/test/%u" in sssd_conf, "fallback_homedir not found in sssd.conf"
++
++
++@pytest.mark.importance("high")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_join_qualified_names_off(client: Client, provider: GenericADProvider):
++ """
++ :title: realm join with qualified-names disabled
++ :setup:
++ 1. Client and AD domain are available
++ :steps:
++ 1. Configure /etc/realmd.conf with fully-qualified-names = no
++ 2. Restart realmd service to apply configuration
++ 3. Join domain without specifying client-software
++ 4. Verify realm list shows login-formats: %U (unqualified)
++ 5. Create test user on AD
++ 6. Query user without @domain suffix using getent
++ 7. Query group without domain qualification
++ :expectedresults:
++ 1. Configuration file created successfully
++ 2. realmd service restarts successfully
++ 3. Domain join succeeds
++ 4. realm list shows login-formats: %U (not %U@%D)
++ 5. Test user created successfully
++ 6. Users can be queried without @domain suffix
++ 7. Groups can be queried without domain qualification
++ :customerscenario: False
++ """
++ domain = provider.host.domain
++
++ # Configure /etc/realmd.conf to disable fully-qualified-names
++ realmd_conf = f"""[{domain}]
++fully-qualified-names = no
++"""
++ client.fs.write("/etc/realmd.conf", realmd_conf)
++
++ # Restart realmd service to apply configuration
++ client.svc.restart("realmd.service")
++
++ joined = False
++ try:
++ # Join domain
++ join_result = client.realm.join(
++ domain=domain,
++ user=provider.host.adminuser,
++ password=provider.host.adminpw,
++ args=["--verbose"],
++ krb=False,
++ )
++
++ assert join_result.rc == 0, f"realm join failed: {join_result.stderr}"
++ assert "Successfully enrolled machine in realm" in join_result.stderr, "realm join success message not found"
++ joined = True
++
++ # Give services time to fully start
++ time.sleep(10)
++
++ # Verify realm list shows unqualified login format
++ list_result = client.realm.list()
++ assert list_result.rc == 0, f"realm list failed: {list_result.stderr}"
++
++ # Check for bz#967011 - login-formats should be %U (not %U@%D)
++ assert re.search(r"login-formats:\s*%U\s*$", list_result.stdout, re.MULTILINE | re.IGNORECASE), (
++ "realm list should show 'login-formats: %U' (unqualified)"
++ )
++
++ # Create a test user on AD
++ user = provider.user("testuser2").add()
++
++ # Wait for user to be available
++ time.sleep(5)
++
++ # Query user WITHOUT @domain suffix
++ getent_user_result = client.tools.getent.passwd(user.name)
++ assert getent_user_result is not None, f"getent passwd with unqualified name failed for user '{user.name}'"
++ assert getent_user_result.name == user.name, (
++ f"User name mismatch: expected '{user.name}', got '{getent_user_result.name}'"
++ )
++
++ # Query group without domain qualification
++ getent_group_result = client.tools.getent.group("domain users")
++ assert getent_group_result is not None, "getent group with unqualified name failed for 'domain users'"
++
++ finally:
++ # Cleanup: Leave the domain if joined
++ if joined:
++ client.realm.leave(
++ domain=domain,
++ user=provider.host.adminuser,
++ password=provider.host.adminpw,
++ )
++
++
++@pytest.mark.importance("high")
++@pytest.mark.topology(KnownTopologyGroup.AnyAD)
++def test_realm_join_qualified_names_off_winbind(client: Client, provider: GenericADProvider):
++ """
++ :title: realm join with qualified-names disabled using winbind
++ :setup:
++ 1. Client and AD domain are available
++ :steps:
++ 1. Configure /etc/realmd.conf with fully-qualified-names = no
++ 2. Restart realmd service to apply configuration
++ 3. Join domain with --client-software=winbind
++ 4. Verify realm list shows login-formats: %U (unqualified)
++ 5. Create test user on AD
++ 6. Query user without @domain suffix using getent
++ 7. Query group without domain qualification
++ :expectedresults:
++ 1. Configuration file created successfully
++ 2. realmd service restarts successfully
++ 3. Domain join succeeds with winbind
++ 4. realm list shows login-formats: %U (not %U@%D)
++ 5. Test user created successfully
++ 6. Users can be queried without @domain suffix
++ 7. Groups can be queried without domain qualification
++ :customerscenario: False
++ """
++ domain = provider.host.domain
++
++ # Configure /etc/realmd.conf to disable fully-qualified-names
++ realmd_conf = f"""[{domain}]
++fully-qualified-names = no
++"""
++ client.fs.write("/etc/realmd.conf", realmd_conf)
++
++ # Restart realmd service to apply configuration
++ client.svc.restart("realmd.service")
++
++ joined = False
++ try:
++ # Join domain with winbind
++ join_result = client.realm.join(
++ domain=domain,
++ user=provider.host.adminuser,
++ password=provider.host.adminpw,
++ args=["--client-software=winbind", "--verbose"],
++ krb=False,
++ )
++
++ assert join_result.rc == 0, f"realm join failed: {join_result.stderr}"
++ assert "Successfully enrolled machine in realm" in join_result.stderr, "realm join success message not found"
++ joined = True
++
++ # Give winbind time to fully start (it's slow)
++ time.sleep(30)
++
++ # Verify realm list shows unqualified login format
++ list_result = client.realm.list()
++ assert list_result.rc == 0, f"realm list failed: {list_result.stderr}"
++
++ # Check for bz#967011 - login-formats should be %U (not %U@%D)
++ assert re.search(r"login-formats:\s*%U\s*$", list_result.stdout, re.MULTILINE | re.IGNORECASE), (
++ "realm list should show 'login-formats: %U' (unqualified)"
++ )
++
++ # Create a test user on AD
++ user = provider.user("testuser3").add()
++
++ # Wait for user to be available
++ time.sleep(5)
++
++ # Query user WITHOUT @domain suffix
++ getent_user_result = client.tools.getent.passwd(user.name)
++ assert getent_user_result is not None, f"getent passwd with unqualified name failed for user '{user.name}'"
++ assert getent_user_result.name == user.name, (
++ f"User name mismatch: expected '{user.name}', got '{getent_user_result.name}'"
++ )
++
++ # Query group without domain qualification
++ getent_group_result = client.tools.getent.group("domain users")
++ assert getent_group_result is not None, "getent group with unqualified name failed for 'domain users'"
++
++ finally:
++ # Cleanup: Leave the domain if joined
++ if joined:
++ client.realm.leave(
++ domain=domain,
++ user=provider.host.adminuser,
++ password=provider.host.adminpw,
++ )
+diff --git a/tests/topology_controllers.py b/tests/topology_controllers.py
+index 4ec95a9..4c32e8d 100644
+--- a/tests/topology_controllers.py
++++ b/tests/topology_controllers.py
+@@ -70,7 +70,7 @@ class ADTopologyController(ProvisionedBackupTopologyController):
+ # client_fqdn = f"{client.hostname.split('.')[0]}.{domain}"
+ # client.conn.exec(["hostnamectl", "set-hostname", client_fqdn])
+
+- # 2. Write the strict Kerberos configuration (replaces removing it)
++ # 2. Write the strict Kerberos configuration first (needed for cleanup commands)
+ client.fs.write(
+ "/etc/krb5.conf",
+ f"""[libdefaults]
+@@ -91,7 +91,24 @@ class ADTopologyController(ProvisionedBackupTopologyController):
+ """,
+ )
+
+- # 3. Remove any existing keytab
++ # 3. Clean up any existing domain memberships and computer accounts
++ # Leave realm with --remove to delete computer account from AD/Samba
++ client.conn.run(
++ f"realm leave --remove -U {provider.adminuser}@{realm} {domain}",
++ input=provider.adminpw,
++ raise_on_error=False,
++ )
++
++ # 4. Clean up any orphaned computer accounts (backup in case realm leave failed)
++ short_hostname = client.hostname.split(".")[0]
++ client.conn.run(
++ f"adcli delete-computer '{short_hostname}' --domain={domain} "
++ f"--login-user={provider.adminuser} --stdin-password",
++ input=provider.adminpw,
++ raise_on_error=False,
++ )
++
++ # 5. Remove any existing keytab
+ client.fs.rm("/etc/krb5.keytab")
+
+ # Backup so we can restore to this state after each test
+--
+2.54.0
+
diff --git a/realmd.spec b/realmd.spec
index 31297b3..8c5ba77 100644
--- a/realmd.spec
+++ b/realmd.spec
@@ -1,6 +1,6 @@
Name: realmd
Version: 0.17.1
-Release: 19%{?dist}
+Release: 20%{?dist}
Summary: Kerberos realm enrollment service
License: LGPL-2.1-or-later
URL: https://gitlab.freedesktop.org/realmd/realmd
@@ -23,6 +23,26 @@ Patch0014: 0001-Initial-implementation-of-a-renew-request.patch
Patch0015: 0002-renew-implement-support-for-adcli.patch
Patch0016: 0003-service-use-proper-macro-for-os-name-and-os-version.patch
Patch0017: 0004-renew-fix-issues-found-by-Coverity.patch
+Patch0018: 0001-Tests-initial-framework-and-tests-for-realm.patch
+Patch0019: 0002-service-do-not-set-config_file_version-in-sssd.conf.patch
+Patch0020: 0003-tools-fix-help-message-for-realm-deny.patch
+Patch0021: 0004-disco-check-IPA-specific-extension-in-rootDSE.patch
+Patch0022: 0005-tools-add-message-after-successful-join.patch
+Patch0023: 0006-doc-add-renew-option-of-realm-man-page.patch
+Patch0024: 0007-Testcases-Adding-multiple-testcases.patch
+Patch0025: 0008-doc-Be-explicit-about-default-settings-in-realmd.con.patch
+Patch0026: 0009-doc-document-debug-option-in-service-section-of-real.patch
+Patch0027: 0010-samba-add-debug-level-10-to-net-commands-when-realmd.patch
+Patch0028: 0011-When-running-in-debug-make-warnings-not-fatal.patch
+Patch0029: 0012-service-only-use-single-value-password-server-option.patch
+Patch0030: 0013-Refresh-license-text.patch
+Patch0031: 0014-Testcases-realm-join-leave-user-login.patch
+Patch0032: 0015-Testcases-realm-renew.patch
+Patch0033: 0016-Testcases-realm-renew-and-prejoin-testcases.patch
+Patch0034: 0017-Fix-gboolean-typo-use-FALSE-TRUE-for-false-true.patch
+Patch0035: 0018-tests-Add-delegated-user-join-leave-test.patch
+Patch0036: 0019-Add-GitLab-CI-CD-pipeline-configuration.patch
+Patch0037: 0020-Tests-realmd-testcase-set-8.patch
BuildRequires: make
BuildRequires: gcc
@@ -109,6 +129,9 @@ make check
%doc ChangeLog
%changelog
+* Thu Jun 25 2026 Sumit Bose <sbose@redhat.com> - 0.17.1-20
+- sync with latest upstream patches
+
* Sat Jan 17 2026 Fedora Release Engineering <releng@fedoraproject.org> - 0.17.1-19
- Rebuilt for https://fedoraproject.org/wiki/Fedora_44_Mass_Rebuild
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-06-25 12:01 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-25 12:01 [rpms/realmd] rawhide: sync with latest upstream patches Sumit Bose
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox