diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 4d546caf..446ab072 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -4,6 +4,7 @@ How-To Guides .. toctree:: skipping-conditional-tests + testing-adforest testing-authentication testing-autofs testing-dbus diff --git a/docs/guides/testing-adforest.rst b/docs/guides/testing-adforest.rst new file mode 100644 index 00000000..34b485a1 --- /dev/null +++ b/docs/guides/testing-adforest.rst @@ -0,0 +1,111 @@ +Testing AD Forest +################# + +Use :attr:`~sssd_test_framework.topology.KnownTopology.ADForest` for a single +Active Directory forest with a root domain, a child domain, and a tree domain. + +Topology +======== + +The multihost configuration must provide one client and three ``ad`` hosts, +**in this order**: forest root, child domain, tree domain. + +.. code-block:: yaml + :caption: Example ``mhc.yaml`` hosts (role ``ad`` order matters) + + - hostname: client.test + role: client + + - hostname: dc.ad.test + role: ad + config: + client: + ad_domain: ad.test + + - hostname: dc.child.ad.test + role: ad + config: + client: + ad_domain: child.ad.test + + - hostname: dc.tree.test + role: ad + config: + client: + ad_domain: tree.test + +Role fixtures from the topology mark: + +* ``client`` — SSSD client +* ``ad`` — forest root (``sssd.ad[0]``) +* ``ad_child`` — child domain (``sssd.ad[1]``) +* ``ad_tree`` — tree domain (``sssd.ad[2]``) + +Forest trusts and domain relationships must already exist (lab / IdM-CI +provisioning). The topology controller does **not** enroll the client and does +**not** set a ``provider`` fixture or auto-import an SSSD domain. + +Joining a domain +================ + +Request one of the join fixtures from +:mod:`sssd_test_framework.fixtures`. Each fixture sets the client hostname, +leaves any forest domain, joins the target with ``realm``, and leaves again on +teardown. The fixture **yields the** :class:`~sssd_test_framework.roles.ad.AD` +**role for that domain** — use that object for users, groups, GPOs, and +``client.sssd.import_domain``. + +.. list-table:: + :header-rows: 1 + :widths: 25 30 45 + + * - Fixture + - Joins + - Yields + * - ``join_ad_root`` + - forest root + - ``ad`` + * - ``join_ad_child`` + - child domain + - ``ad_child`` + * - ``join_ad_tree`` + - tree domain + - ``ad_tree`` + +.. warning:: + + Do not invent a ``provider`` alias that always points at the root. Creating + users or groups on ``ad`` while the client is joined to the child or tree + puts objects in the wrong domain. Prefer the yielded join fixture, or the + matching role (``ad`` / ``ad_child`` / ``ad_tree``). + +Example +======= + +.. code-block:: python + :caption: Join child domain, create a user there, import SSSD domain + + @pytest.mark.topology(KnownTopology.ADForest) + def test_forest__child_lookup(client: Client, join_ad_child: AD, ad: AD): + user = join_ad_child.user("child-user").add() + + client.sssd.import_domain("test", join_ad_child) + client.sssd.start() + + assert client.tools.id(user.name) is not None + # Root DC is still available when the test needs cross-domain data + assert ad.domain != join_ad_child.domain + +When joined to the forest root, SSSD can discover child and tree domains for +lookup and authentication of users in those domains. When joined to the child +or tree, configure and import the SSSD domain from the same role you joined. + +.. seealso:: + + * :attr:`sssd_test_framework.topology.KnownTopology.ADForest` + * :class:`sssd_test_framework.topology_controllers.ADForestTopologyController` + * :func:`sssd_test_framework.fixtures.join_ad_root` + * :func:`sssd_test_framework.fixtures.join_ad_child` + * :func:`sssd_test_framework.fixtures.join_ad_tree` + * :ref:`importing-domain` + * :doc:`testing-gpo` diff --git a/sssd_test_framework/fixtures.py b/sssd_test_framework/fixtures.py index d9775db0..486108e5 100644 --- a/sssd_test_framework/fixtures.py +++ b/sssd_test_framework/fixtures.py @@ -3,9 +3,13 @@ from __future__ import annotations import os +from collections.abc import Generator import pytest +from .roles.ad import AD +from .roles.client import Client + @pytest.fixture(scope="session") def datadir(request: pytest.FixtureRequest) -> str: @@ -43,3 +47,84 @@ def testdatadir(moduledatadir: str, request: pytest.FixtureRequest) -> str: name = request.node.originalname return os.path.join(moduledatadir, name) + + +def _ad_forest_leave(client: Client, domain: AD) -> None: + client.host.conn.exec( + ["realm", "leave", "--unattended", domain.domain], + input=domain.host.adminpw, + raise_on_error=False, + ) + + +def _ad_forest_join(client: Client, target: AD, forest: tuple[AD, ...]) -> str: + """Leave any forest domain, set hostname, join ``target``. Return prior hostname.""" + old_hostname = client.host.conn.run("hostname").stdout.strip() + short_hostname = old_hostname.split(".")[0].strip() + hostname = f"{short_hostname}.{target.domain}" + + client.fs.write("/etc/hostname", f"{hostname}\n") + client.host.conn.run(f"hostname {hostname}") + + for domain in forest: + _ad_forest_leave(client, domain) + + client.fs.rm("/etc/krb5.conf") + client.fs.rm("/etc/krb5.keytab") + + result = client.host.conn.exec(["realm", "join", target.domain], input=target.host.adminpw, raise_on_error=False) + if result.rc != 0: + _ad_forest_leave(client, target) + client.host.conn.exec(["realm", "join", target.domain], input=target.host.adminpw) + + return old_hostname + + +def _ad_forest_restore_hostname(client: Client, hostname: str) -> None: + client.fs.write("/etc/hostname", f"{hostname}\n") + client.host.conn.run(f"hostname {hostname}", raise_on_error=False) + + +@pytest.fixture +def join_ad_root(client: Client, ad: AD, ad_child: AD, ad_tree: AD) -> Generator[AD, None, None]: + """ + Join the client to the AD forest root. + + Yields ``ad`` — use it for users/groups and ``client.sssd.import_domain``. + For :attr:`~sssd_test_framework.topology.KnownTopology.ADForest`. Leaves on teardown. + """ + forest = (ad, ad_child, ad_tree) + old_hostname = _ad_forest_join(client, ad, forest) + yield ad + _ad_forest_leave(client, ad) + _ad_forest_restore_hostname(client, old_hostname) + + +@pytest.fixture +def join_ad_child(client: Client, ad: AD, ad_child: AD, ad_tree: AD) -> Generator[AD, None, None]: + """ + Join the client to the AD child domain. + + Yields ``ad_child`` — use it for users/groups and ``client.sssd.import_domain``. + For :attr:`~sssd_test_framework.topology.KnownTopology.ADForest`. Leaves on teardown. + """ + forest = (ad, ad_child, ad_tree) + old_hostname = _ad_forest_join(client, ad_child, forest) + yield ad_child + _ad_forest_leave(client, ad_child) + _ad_forest_restore_hostname(client, old_hostname) + + +@pytest.fixture +def join_ad_tree(client: Client, ad: AD, ad_child: AD, ad_tree: AD) -> Generator[AD, None, None]: + """ + Join the client to the AD tree domain. + + Yields ``ad_tree`` — use it for users/groups and ``client.sssd.import_domain``. + For :attr:`~sssd_test_framework.topology.KnownTopology.ADForest`. Leaves on teardown. + """ + forest = (ad, ad_child, ad_tree) + old_hostname = _ad_forest_join(client, ad_tree, forest) + yield ad_tree + _ad_forest_leave(client, ad_tree) + _ad_forest_restore_hostname(client, old_hostname) diff --git a/sssd_test_framework/topology.py b/sssd_test_framework/topology.py index 7d6815ed..31e63277 100644 --- a/sssd_test_framework/topology.py +++ b/sssd_test_framework/topology.py @@ -9,6 +9,7 @@ from .config import SSSDTopologyMark from .topology_controllers import ( + ADForestTopologyController, ADTopologyController, ClientTopologyController, GDMTopologyController, @@ -168,6 +169,26 @@ def test_ldap(client: Client, ldap: LDAP): .. topology-mark:: KnownTopology.AD """ + ADForest = SSSDTopologyMark( + name="ad-forest", + topology=Topology(TopologyDomain("sssd", client=1, ad=3)), + controller=ADForestTopologyController(), + fixtures=dict( + client="sssd.client[0]", + ad="sssd.ad[0]", + ad_child="sssd.ad[1]", + ad_tree="sssd.ad[2]", + ), + ) + """ + AD forest with root, child, and tree domains (three ``ad`` hosts, ordered + root → child → tree). Does not enroll the client or set ``provider`` / + auto-import an SSSD domain — use ``join_ad_root``, ``join_ad_child``, or + ``join_ad_tree`` and create objects / ``import_domain`` on that role. + + .. topology-mark:: KnownTopology.ADForest + """ + Samba = SSSDTopologyMark( name="samba", topology=Topology(TopologyDomain("sssd", client=1, samba=1, nfs=1)), diff --git a/sssd_test_framework/topology_controllers.py b/sssd_test_framework/topology_controllers.py index b02fdb45..454b01fa 100644 --- a/sssd_test_framework/topology_controllers.py +++ b/sssd_test_framework/topology_controllers.py @@ -21,6 +21,7 @@ "LDAPKRB5TopologyController", "IPATopologyController", "ADTopologyController", + "ADForestTopologyController", "SambaTopologyController", "IPATrustADTopologyController", "IPATrustSambaTopologyController", @@ -257,6 +258,30 @@ def topology_setup(self, client: ClientHost, provider: ADHost | SambaHost) -> No super().topology_setup() +class ADForestTopologyController(ProvisionedBackupTopologyController): + """ + AD forest topology: root, child, and tree domain controllers. + + Multihost hosts with role ``ad`` must be ordered root, child, tree. + Does not enroll the client; use ``join_ad_root``, ``join_ad_child``, or + ``join_ad_tree`` fixtures. Forest trusts must already exist (lab / IdM-CI). + """ + + @BackupTopologyController.restore_vanilla_on_error + def topology_setup( + self, + client: ClientHost, + provider: ADHost, + ad_child: ADHost, + ad_tree: ADHost, + ) -> None: + self.logger.info(f"AD forest: root={provider.domain}, child={ad_child.domain}, tree={ad_tree.domain}") + if self.provisioned: + self.logger.info(f"Topology '{self.name}' is already provisioned") + return + super().topology_setup() + + class SambaTopologyController(ADTopologyController): """ Samba Topology Controller.