Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/guides/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ How-To Guides
.. toctree::

skipping-conditional-tests
testing-adforest
testing-authentication
testing-autofs
testing-dbus
Expand Down
111 changes: 111 additions & 0 deletions docs/guides/testing-adforest.rst
Original file line number Diff line number Diff line change
@@ -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`
85 changes: 85 additions & 0 deletions sssd_test_framework/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
21 changes: 21 additions & 0 deletions sssd_test_framework/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from .config import SSSDTopologyMark
from .topology_controllers import (
ADForestTopologyController,
ADTopologyController,
ClientTopologyController,
GDMTopologyController,
Expand Down Expand Up @@ -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)),
Expand Down
25 changes: 25 additions & 0 deletions sssd_test_framework/topology_controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"LDAPKRB5TopologyController",
"IPATopologyController",
"ADTopologyController",
"ADForestTopologyController",
"SambaTopologyController",
"IPATrustADTopologyController",
"IPATrustSambaTopologyController",
Expand Down Expand Up @@ -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.
Expand Down
Loading