|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | 5 | import base64 |
| 6 | +import tempfile |
| 7 | +import time |
6 | 8 | from datetime import datetime |
7 | 9 | from enum import Enum |
8 | | -from typing import Any, Generic, TypeVar |
| 10 | +from typing import TYPE_CHECKING, Any, Generic, TypeVar |
9 | 11 |
|
10 | 12 | import ldap |
11 | 13 | import ldap.ldapobject |
|
17 | 19 | from .generic import GenericNetgroupMember, GenericPasswordPolicy, ProtocolName |
18 | 20 | from .nfs import NFSExport |
19 | 21 |
|
| 22 | +if TYPE_CHECKING: |
| 23 | + from .kdc import KDC |
| 24 | + |
20 | 25 | __all__ = [ |
21 | 26 | "LDAPRoleType", |
22 | 27 | "LDAPPasswordPolicy", |
@@ -239,6 +244,141 @@ def setup(self) -> None: |
239 | 244 | except ldap.TYPE_OR_VALUE_EXISTS: |
240 | 245 | pass |
241 | 246 |
|
| 247 | + def enable_gssapi(self, kdc: KDC) -> None: |
| 248 | + """ |
| 249 | + Configure Directory Server for GSSAPI/SASL authentication. |
| 250 | +
|
| 251 | + This method sets up the LDAP server to accept GSSAPI (Kerberos) authentication |
| 252 | + by creating a service principal, exporting the keytab, and configuring Directory Server. |
| 253 | +
|
| 254 | + .. code-block:: python |
| 255 | + :caption: Example usage |
| 256 | +
|
| 257 | + @pytest.mark.topology(KnownTopology.LDAP_KRB5) |
| 258 | + def test_ldap_gssapi(client: Client, ldap: LDAP, kdc: KDC): |
| 259 | + # Enable GSSAPI on LDAP server |
| 260 | + ldap.enable_gssapi(kdc) |
| 261 | +
|
| 262 | + ldap.user('testuser').add() |
| 263 | + kdc.principal('testuser').add() |
| 264 | +
|
| 265 | + # Configure SSSD to use GSSAPI |
| 266 | + client.sssd.domain["ldap_sasl_mech"] = "GSSAPI" |
| 267 | + client.sssd.start() |
| 268 | +
|
| 269 | + result = client.tools.id('testuser') |
| 270 | + assert result is not None |
| 271 | +
|
| 272 | + :param kdc: KDC role object to create service principal |
| 273 | + :type kdc: KDC |
| 274 | + """ |
| 275 | + |
| 276 | + # 1. Install required packages |
| 277 | + self.host.conn.run( |
| 278 | + "dnf install -y cyrus-sasl-gssapi krb5-workstation || " |
| 279 | + "yum install -y cyrus-sasl-gssapi krb5-workstation", |
| 280 | + ) |
| 281 | + self.host.conn.run("rpm -q cyrus-sasl-gssapi") |
| 282 | + |
| 283 | + # 2. Create LDAP service principal |
| 284 | + ldap_principal = f"ldap/{self.host.hostname}" |
| 285 | + kdc.principal(ldap_principal).add(password=None) |
| 286 | + |
| 287 | + # 3. Export keytab to LDAP server (same transfer pattern as LDAPKRB5TopologyController) |
| 288 | + keytab_path = "/etc/dirsrv/ds.keytab" |
| 289 | + keytab_staging = "/tmp/sssd-test-framework-ds.keytab" |
| 290 | + qualified_principal = kdc.qualify(ldap_principal) |
| 291 | + kdc.host.conn.run(f"rm -f {keytab_staging}", raise_on_error=False) |
| 292 | + kdc.host.conn.run( |
| 293 | + f"kadmin.local -q 'ktadd -k {keytab_staging} -norandkey \"{qualified_principal}\"'" |
| 294 | + ) |
| 295 | + with tempfile.NamedTemporaryFile() as tmp: |
| 296 | + kdc.host.fs.download(keytab_staging, tmp.name) |
| 297 | + self.host.fs.upload(tmp.name, keytab_path) |
| 298 | + kdc.host.conn.run(f"rm -f {keytab_staging}", raise_on_error=False) |
| 299 | + self.host.conn.run(f"chown dirsrv:dirsrv {keytab_path}") |
| 300 | + self.host.conn.run(f"chmod 600 {keytab_path}") |
| 301 | + |
| 302 | + # 4. Copy krb5.conf from KDC to LDAP server |
| 303 | + krb5_conf = kdc.config() |
| 304 | + self.host.conn.run(f"cat > /etc/krb5.conf << 'EOFKRB5'\n{krb5_conf}\nEOFKRB5") |
| 305 | + |
| 306 | + # Add default_keytab_name to krb5.conf as fallback |
| 307 | + self.host.conn.run( |
| 308 | + f"sed -i '/\\[libdefaults\\]/a\\ default_keytab_name = {keytab_path}' /etc/krb5.conf" |
| 309 | + ) |
| 310 | + |
| 311 | + # 5. Configure Cyrus SASL to use the keytab |
| 312 | + # This is critical - without this, the SASL GSSAPI plugin won't know where to find the keytab |
| 313 | + self.host.conn.run( |
| 314 | + f"mkdir -p /etc/sasl2 && " |
| 315 | + f"cat > /etc/sasl2/slapd.conf << 'EOFSASL'\n" |
| 316 | + f"mech_list: GSSAPI EXTERNAL PLAIN LOGIN\n" |
| 317 | + f"keytab: {keytab_path}\n" |
| 318 | + f"EOFSASL" |
| 319 | + ) |
| 320 | + |
| 321 | + # Also create for other possible SASL application names |
| 322 | + self.host.conn.run("cp /etc/sasl2/slapd.conf /etc/sasl2/ns-slapd.conf") |
| 323 | + self.host.conn.run("cp /etc/sasl2/slapd.conf /etc/sasl2/ldap.conf") |
| 324 | + |
| 325 | + # 6. Set KRB5_KTNAME environment variable for Directory Server via sysconfig |
| 326 | + # Note: systemd Environment= directives don't work reliably in containers |
| 327 | + # Use EnvironmentFile instead |
| 328 | + self.host.conn.run( |
| 329 | + f"echo 'KRB5_KTNAME={keytab_path}' > /etc/sysconfig/dirsrv-localhost" |
| 330 | + ) |
| 331 | + |
| 332 | + # 7. Configure SASL identity mapping in Directory Server |
| 333 | + # Align default Kerberos maps with the data suffix (sssd-qe krb_credential_cache) and |
| 334 | + # add a high-priority map for host/ldap service principals used by SSSD GSSAPI binds. |
| 335 | + realm = kdc.realm |
| 336 | + base_dn = self.naming_context |
| 337 | + binddn = self.host.binddn |
| 338 | + bindpw = self.host.bindpw |
| 339 | + |
| 340 | + sasl_ldif = "" |
| 341 | + for cn in ( |
| 342 | + "Kerberos uid mapping", |
| 343 | + "rfc 2829 dn syntax", |
| 344 | + "rfc 2829 u syntax", |
| 345 | + "uid mapping", |
| 346 | + ): |
| 347 | + sasl_ldif += ( |
| 348 | + f"dn: cn={cn},cn=mapping,cn=sasl,cn=config\n" |
| 349 | + "changetype: modify\n" |
| 350 | + "replace: nsSaslMapBaseDNTemplate\n" |
| 351 | + f"nsSaslMapBaseDNTemplate: {base_dn}\n" |
| 352 | + "\n" |
| 353 | + ) |
| 354 | + self.host.conn.run( |
| 355 | + f"ldapmodify -x -D '{binddn}' -w '{bindpw}' -H ldap://localhost", |
| 356 | + input=sasl_ldif, |
| 357 | + ) |
| 358 | + service_map_ldif = ( |
| 359 | + "dn: cn=SSSD service principals,cn=mapping,cn=sasl,cn=config\n" |
| 360 | + "changetype: add\n" |
| 361 | + "objectClass: top\n" |
| 362 | + "objectClass: nsSaslMapping\n" |
| 363 | + "cn: SSSD service principals\n" |
| 364 | + f"nsSaslMapRegexString: ^(host|ldap)/.*@{realm}$\n" |
| 365 | + "nsSaslMapBaseDNTemplate: cn=Directory Manager\n" |
| 366 | + "nsSaslMapFilterTemplate: (objectclass=*)\n" |
| 367 | + "nsSaslMapPriority: 10\n" |
| 368 | + ) |
| 369 | + self.host.conn.run( |
| 370 | + f"ldapmodify -x -D '{binddn}' -w '{bindpw}' -H ldap://localhost", |
| 371 | + input=service_map_ldif, |
| 372 | + raise_on_error=False, |
| 373 | + ) |
| 374 | + |
| 375 | + # 8. Reload systemd and restart Directory Server |
| 376 | + self.host.conn.run("systemctl daemon-reload") |
| 377 | + self.host.conn.run("systemctl restart dirsrv@localhost") |
| 378 | + |
| 379 | + # Wait for Directory Server to fully start with GSSAPI support |
| 380 | + time.sleep(3) |
| 381 | + |
242 | 382 | def fqn(self, name: str) -> str: |
243 | 383 | """ |
244 | 384 | Return fully qualified name in form name@domain. |
|
0 commit comments