public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Sumit Bose <sbose@redhat.com>
To: git-commits@fedoraproject.org
Subject: [rpms/realmd] rawhide: sync with latest upstream patches
Date: Thu, 25 Jun 2026 12:01:45 GMT	[thread overview]
Message-ID: <178238890518.1.17645956437046536477.rpms-realmd-0ba19c36de45@fedoraproject.org> (raw)

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
 

                 reply	other threads:[~2026-06-25 12:01 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=178238890518.1.17645956437046536477.rpms-realmd-0ba19c36de45@fedoraproject.org \
    --to=sbose@redhat.com \
    --cc=git-commits@fedoraproject.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox