From a1d96b930b4eb8b7cca06f2d062fa9ccab89db0b Mon Sep 17 00:00:00 2001 From: Dan Lavu Date: Fri, 24 Jul 2026 12:31:26 -0400 Subject: [PATCH 1/4] test rewrite: legacy intg test_pam_responder.py - first batch Port sssd/src/tests/intg/test_pam_responder.py to test_smartcard.py, test_authentication.py - test_smartcard__login_fails_when_wrong_pin_is_entered - test_smartcard__login_fails_when_card_is_not_mapped - test_smartcard__cert_auth_limited_to_allowed_pam_services - test_smartcard__login_succeeds_when_cert_auth_required - test_smartcard__login_fails_when_cert_auth_required_without_card - test_authentication__custom_password_prompt_is_shown_at_login Peeling out the reviewed test cases into it's own PR, from https://github.com/SSSD/sssd/pull/8873 Co-authored-by: Cursor Model used: Claude Sonnet 4.6 --- src/tests/system/tests/test_authentication.py | 34 +++++ src/tests/system/tests/test_smartcard.py | 133 ++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/src/tests/system/tests/test_authentication.py b/src/tests/system/tests/test_authentication.py index 277f1454184..0e132876921 100644 --- a/src/tests/system/tests/test_authentication.py +++ b/src/tests/system/tests/test_authentication.py @@ -375,3 +375,37 @@ def test_ensure_localauth_plugin_is_not_configured(client: Client, provider: Gen with pytest.raises(Exception): client.fs.read("/var/lib/sss/pubconf/krb5.include.d/localauth_plugin") + + +@pytest.mark.importance("medium") +@pytest.mark.topology(KnownTopologyGroup.AnyProvider) +@pytest.mark.parametrize( + "prompting_section", + ["prompting/password", "prompting/password/su-l"], + ids=["global_prompt", "service_prompt"], +) +def test_authentication__custom_password_prompt_is_shown_at_login( + client: Client, provider: GenericProvider, prompting_section: str +): + """ + :title: Custom password prompt text is shown at login + :description: + 'su -' uses the 'su-l' PAM service, so the per-service case targets + '[prompting/password/su-l]', not '[prompting/password/su]'. + :setup: + 1. Create user + 2. Set a custom 'password_prompt', either globally or for the 'su -' PAM service ('su-l') + 3. Start SSSD + :steps: + 1. Authenticate as the user via 'su -' + :expectedresults: + 1. The custom prompt text is shown and authentication succeeds + :customerscenario: True + """ + provider.user("user1").add(password="Secret123") + client.sssd.section(prompting_section)["password_prompt"] = "My custom prompt" + client.sssd.start() + + result = client.host.conn.run("su - user1 -c 'su - user1 -c whoami'", input="Secret123") + assert "My custom prompt" in result.stderr, "Custom password prompt was not shown!" + assert "user1" in result.stdout, "'user1' failed to log in!" diff --git a/src/tests/system/tests/test_smartcard.py b/src/tests/system/tests/test_smartcard.py index e455371aa47..6645cff8c88 100644 --- a/src/tests/system/tests/test_smartcard.py +++ b/src/tests/system/tests/test_smartcard.py @@ -309,3 +309,136 @@ def test_smartcard__without_soft_ocsp_with_unreachable_responder(client: Client, assert ( "PIN" not in result.stderr or result.rc != 0 ), f"Expected authentication to fail without soft_ocsp when OCSP is unreachable! rc={result.rc}" + + +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__login_fails_when_wrong_pin_is_entered(client: Client): + """ + :title: Smartcard login fails when the wrong pin is entered. + :setup: + 1. Create a local user and initialize a smart card mapped to the user + :steps: + 1. Authenticate as the user via 'su' with an incorrect PIN + :expectedresults: + 1. Authentication fails + :customerscenario: True + """ + client.local.user("user1").add() + client.smartcard.setup_local_card(client, "user1") + + assert not client.auth.su.smartcard("user1", "000000"), "Authentication should have failed with a wrong PIN!" + + +@pytest.mark.importance("medium") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__login_fails_when_card_is_not_mapped(client: Client): + """ + :title: Smartcard authentication fails when card is not mapped to the user + :setup: + 1. Create two local users and initialize a smart card mapped to only the first user + :steps: + 1. Authenticate as the first user via 'su' with the smart card PIN + 2. Attempt to authenticate as the second user via 'su' with the same smart card PIN + :expectedresults: + 1. Authentication succeeds using the certificate + 2. Authentication fails because the certificate does not map to the second user + :customerscenario: True + """ + client.local.user("user1").add() + client.local.user("user2").add() + client.smartcard.setup_local_card(client, "user1") + + assert client.auth.su.smartcard("user1", TOKEN_PIN), "Smart card authentication failed for the mapped user!" + assert not client.auth.su.smartcard( + "user2", TOKEN_PIN + ), "Authentication should fail for a user the certificate does not map to!" + + +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.parametrize( + "pam_p11_allowed_services, expect_cert_auth", + [(None, True), ("-su-l", False)], + ids=["su_l_allowed_by_default", "su_l_removed_from_allowed_services"], +) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__certificate_authentication_is_limited_to_allowed_pam_services( + client: Client, pam_p11_allowed_services: str | None, expect_cert_auth: bool +): + """ + :title: Smartcard authentication is only used for PAM services allowed by pam_p11_allowed_services + :setup: + 1. Optionally remove the 'su-l' service (used by ``su -``) from 'pam_p11_allowed_services' + 2. Create a local user and initialize a smart card mapped to the user + :steps: + 1. Authenticate as the user via 'su -' presenting the smart card PIN + :expectedresults: + 1. Authentication uses the certificate when 'su-l' is an allowed service; when it is not, + 'su -' does not prompt for a PIN and the PIN is rejected as a regular password + :customerscenario: True + """ + client.local.user("user1").add() + if pam_p11_allowed_services is not None: + client.sssd.pam["pam_p11_allowed_services"] = pam_p11_allowed_services + client.smartcard.setup_local_card(client, "user1") + + result = client.auth.su.smartcard_with_output("user1", TOKEN_PIN) + if expect_cert_auth: + assert result.rc == 0, "Smart card authentication should have succeeded!" + assert "PIN" in result.stderr, "'su -' should have prompted for a PIN!" + else: + assert "PIN" not in result.stderr, "'su -' should not prompt for a PIN when it is not an allowed service!" + assert result.rc != 0, f"'{TOKEN_PIN}' should not be accepted as user1's login password!" + + +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__login_succeeds_when_cert_auth_required(client: Client): + """ + :title: Smartcard login succeeds when certificate authentication is required + :setup: + 1. Create a local user and initialize a smart card mapped to the user + 2. Require certificate-based authentication (authselect 'with-smartcard-required') + :steps: + 1. Authenticate as the user via 'su' with the smart card PIN + :expectedresults: + 1. Authentication succeeds + :customerscenario: True + """ + client.local.user("user1").add() + client.smartcard.setup_local_card(client, "user1") + client.authselect.select("sssd", ["with-smartcard-required"]) + + assert client.auth.su.smartcard("user1", TOKEN_PIN), "Smart card authentication failed!" + + +@pytest.mark.importance("medium") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__login_fails_when_cert_auth_required_without_card(client: Client): + """ + :title: Smartcard login fails when certificate authentication is required and no card is present + :setup: + 1. Create a local user + 2. Reduce the smart card wait timeouts + 3. Initialize a smart card mapped to the user and require certificate-based + authentication (authselect 'with-smartcard-required') + 4. Remove the smart card + :steps: + 1. Attempt to authenticate as the user via 'su' + :expectedresults: + 1. Authentication fails because no smart card was inserted before the timeout + :customerscenario: True + """ + client.local.user("user1").add() + client.sssd.pam["p11_child_timeout"] = "1" + client.sssd.pam["p11_wait_for_card_timeout"] = "1" + client.smartcard.setup_local_card(client, "user1") + client.authselect.select("sssd", ["with-smartcard-required"]) + client.smartcard.remove_card() + + assert not client.auth.su.smartcard("user1", TOKEN_PIN), "Authentication should have failed without a card!" From 491fcf45de236365aa97c22140e071622bc406aa Mon Sep 17 00:00:00 2001 From: Dan Lavu Date: Tue, 28 Jul 2026 15:17:19 -0400 Subject: [PATCH 2/4] test rewrite: remove legacy intg test_pam_responder.py cases now covered Covered by the newly ported system tests: - test_sc_auth_wrong_pin -> test_smartcard__login_fails_when_wrong_pin_is_entered - test_try_sc_auth, test_try_sc_auth_no_map -> test_smartcard__login_fails_when_card_is_not_mapped - test_sc_proxy_password_fallback, test_sc_proxy_no_password_fallback -> test_smartcard__certificate_authentication_is_limited_to_allowed_pam_services - test_require_sc_auth -> test_smartcard__login_succeeds_when_cert_auth_required - test_require_sc_auth_no_cert -> test_smartcard__login_fails_when_cert_auth_required_without_card - test_password_prompting_config_global, test_password_prompting_config_srv -> test_authentication__custom_password_prompt_is_shown_at_login Already covered - test_preauth_indicator - implementation-detail file check, no functional proof; smart-card availability is already proven functionally by test_smartcard.py - test_sc_auth - duplicate of test_smartcard__su_as_local_user - test_sc_auth_two - duplicate; multi-cert selection already exercised by test_smartcard__two_tokens_match_on_both - test_krb5_auth - basic Kerberos login success/negative-password coverage already exists via client.sssd.common.krb5_auth() in test_authentication.py / test_ldap_krb5.py Remaining cases - test_try_sc_auth_root - test_sc_auth_missing_name - test_sc_auth_name_format = test_krb5_auth_domains Co-authored-by: Cursor Model used: Claude Sonnet 4.6 --- src/tests/intg/test_pam_responder.py | 484 --------------------------- 1 file changed, 484 deletions(-) diff --git a/src/tests/intg/test_pam_responder.py b/src/tests/intg/test_pam_responder.py index cb864523958..9231e34c0c3 100644 --- a/src/tests/intg/test_pam_responder.py +++ b/src/tests/intg/test_pam_responder.py @@ -29,15 +29,12 @@ import time import config -import intg.ds_openldap import kdc import pytest from intg.util import unindent -LDAP_BASE_DN = "dc=example,dc=com" - def provider_list(): # The comma is required to indicate a list with the string 'proxy' as # only item, without it the string 'proxy' will be interpreted as list @@ -57,71 +54,6 @@ def __init__(self, p): self.p = none -@pytest.fixture(scope="module") -def ad_inst(request): - """Fake AD server instance fixture""" - instance = intg.ds_openldap.FakeAD( - config.PREFIX, 10389, LDAP_BASE_DN, - "cn=admin", "Secret123" - ) - - try: - instance.setup() - except Exception: - instance.teardown() - raise - request.addfinalizer(instance.teardown) - return instance - - -@pytest.fixture(scope="module") -def ldap_conn(request, ad_inst): - """LDAP server connection fixture""" - ldap_conn = ad_inst.bind() - ldap_conn.ad_inst = ad_inst - request.addfinalizer(ldap_conn.unbind_s) - return ldap_conn - - -def format_basic_conf(ldap_conn): - """Format a basic SSSD configuration""" - return unindent("""\ - [sssd] - domains = FakeAD - services = pam, nss - - [nss] - - [pam] - debug_level = 10 - - [domain/FakeAD] - debug_level = 10 - ldap_search_base = {ldap_conn.ad_inst.base_dn} - ldap_referrals = false - - id_provider = ldap - auth_provider = ldap - chpass_provider = ldap - access_provider = ldap - - ldap_uri = {ldap_conn.ad_inst.ldap_url} - ldap_default_bind_dn = {ldap_conn.ad_inst.admin_dn} - ldap_default_authtok_type = password - ldap_default_authtok = {ldap_conn.ad_inst.admin_pw} - - ldap_schema = ad - ldap_id_mapping = true - ldap_idmap_default_domain_sid = S-1-5-21-1305200397-2901131868-73388776 - case_sensitive = False - - [prompting/password] - password_prompt = My global prompt - - [prompting/password/pam_sss_alt_service] - password_prompt = My alt service prompt - """).format(**locals()) - USER1 = dict(name='user1', passwd='x', uid=10001, gid=20001, gecos='User for tests', dir='/home/user1', @@ -198,31 +130,6 @@ def format_pam_cert_auth_conf_name_format(config, provider): """).format(**locals()) -def format_pam_krb5_auth(config, kdc_instance): - """Format SSSD configuration for krb5 authentication""" - return unindent("""\ - [sssd] - debug_level = 10 - domains = krb5_auth - services = pam, nss - - [nss] - debug_level = 10 - - [pam] - debug_level = 10 - - [domain/krb5_auth] - debug_level = 10 - id_provider = proxy - proxy_lib_name = call - auth_provider = krb5 - - krb5_realm = PAMKRB5TEST - krb5_server = localhost:{kdc_instance.kdc_port} - """).format(**locals()) - - def format_pam_krb5_auth_domains(config, kdc_instance): """Format SSSD configuration for krb5 authentication""" return unindent("""\ @@ -356,26 +263,6 @@ def simple_pam_cert_auth(request, passwd_ops_setup): return None -@pytest.fixture -def simple_pam_cert_auth_no_cert(request, passwd_ops_setup): - """Setup SSSD with pam_cert_auth=True""" - config.PAM_CERT_DB_PATH = os.environ['PAM_CERT_DB_PATH'] - - old_softhsm2_conf = os.environ['SOFTHSM2_CONF'] - del os.environ['SOFTHSM2_CONF'] - - conf = format_pam_cert_auth_conf(config, provider_switch(request.param)) - create_conf_fixture(request, conf) - create_sssd_fixture(request) - - os.environ['SOFTHSM2_CONF'] = old_softhsm2_conf - - passwd_ops_setup.useradd(**USER1) - passwd_ops_setup.useradd(**USER2) - - return None - - @pytest.fixture def simple_pam_cert_auth_two_certs(request, passwd_ops_setup): """Setup SSSD with pam_cert_auth=True""" @@ -409,73 +296,6 @@ def simple_pam_cert_auth_name_format(request, passwd_ops_setup): return None -@pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) -def test_preauth_indicator(simple_pam_cert_auth): - """Check if preauth indicator file is created""" - statinfo = os.stat(config.PUBCONF_PATH + "/pam_preauth_available") - assert stat.S_ISREG(statinfo.st_mode) - - -@pytest.fixture -def pam_prompting_config(request, ldap_conn): - """Setup SSSD with PAM prompting config""" - conf = format_basic_conf(ldap_conn) - create_conf_fixture(request, conf) - create_sssd_fixture(request) - return None - - -def test_password_prompting_config_global(ldap_conn, pam_prompting_config, - env_for_sssctl): - """Check global change of the password prompt""" - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1_dom1-19661", - "--action=auth", "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="111") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("My global prompt") != -1 - - -def test_password_prompting_config_srv(ldap_conn, pam_prompting_config, - env_for_sssctl): - """Check change of the password prompt for dedicated service""" - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1_dom1-19661", - "--action=auth", - "--service=pam_sss_alt_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="111") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("My alt service prompt") != -1 - - @pytest.fixture def env_for_sssctl(request): pwrap_runtimedir = os.getenv("PAM_WRAPPER_SERVICE_DIR") @@ -491,79 +311,6 @@ def env_for_sssctl(request): return env_for_sssctl -@pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) -def test_sc_auth_wrong_pin(simple_pam_cert_auth, env_for_sssctl): - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="111") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user1]: " - "Authentication failure") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) -def test_sc_auth(simple_pam_cert_auth, env_for_sssctl): - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user1]: Success") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth_two_certs', provider_list(), indirect=True) -def test_sc_auth_two(simple_pam_cert_auth_two_certs, env_for_sssctl): - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="2\n123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user1]: Success") != -1 - - @pytest.mark.parametrize('simple_pam_cert_auth_two_certs', provider_list(), indirect=True) def test_sc_auth_two_missing_name(simple_pam_cert_auth_two_certs, env_for_sssctl): @@ -588,170 +335,6 @@ def test_sc_auth_two_missing_name(simple_pam_cert_auth_two_certs, env_for_sssctl assert err.find("pam_authenticate for user [user1]: Success") != -1 -@pytest.mark.parametrize('simple_pam_cert_auth', ['proxy_password'], indirect=True) -def test_sc_proxy_password_fallback(simple_pam_cert_auth, env_for_sssctl): - """ - Check that there will be a password prompt if another proxy auth module is - configured and Smartcard authentication is not allowed but a Smartcard is - present. - """ - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - assert err.find("Password:") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth', ['proxy_password_with_sc'], - indirect=True) -def test_sc_proxy_no_password_fallback(simple_pam_cert_auth, env_for_sssctl): - """ - Use the same environ as for test_sc_proxy_password_fallback but now allow - local Smartcard authentication. Here we expect that there will be a prompt - for the Smartcard PIN and that Smartcard authentication is successful. - """ - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user1]: Success") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) -def test_require_sc_auth(simple_pam_cert_auth, env_for_sssctl): - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", - "--service=pam_sss_sc_required"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user1]: Success") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth_no_cert', provider_list(), indirect=True) -def test_require_sc_auth_no_cert(simple_pam_cert_auth_no_cert, env_for_sssctl): - - # We have to wait about 20s before the command returns because there will - # be 2 run since retry=1 in the PAM configuration and both - # p11_child_timeout and p11_wait_for_card_timeout are 5s in sssd.conf, - # so 2*(5+5)=20. */ - start_time = time.time() - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", - "--service=pam_sss_sc_required"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - end_time = time.time() - assert end_time > start_time and \ - (end_time - start_time) >= 20 and \ - (end_time - start_time) < 40 - assert out.find("Please insert smart card\nPlease insert smart card") != -1 - assert err.find("pam_authenticate for user [user1]: Authentication " - "service cannot retrieve authentication info") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) -def test_try_sc_auth_no_map(simple_pam_cert_auth, env_for_sssctl): - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user2", - "--action=auth", - "--service=pam_sss_try_sc"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user2]: Authentication " - "service cannot retrieve authentication info") != -1 - - -@pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) -def test_try_sc_auth(simple_pam_cert_auth, env_for_sssctl): - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", - "--service=pam_sss_try_sc"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="123456") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find("pam_authenticate for user [user1]: Success") != -1 - - @pytest.mark.parametrize('simple_pam_cert_auth', provider_list(), indirect=True) def test_try_sc_auth_root(simple_pam_cert_auth, env_for_sssctl): """ @@ -881,73 +464,6 @@ def kdc_instance(request): return kdc_instance -@pytest.fixture -def setup_krb5(request, kdc_instance, passwd_ops_setup): - """ - Setup SSSD for Kerberos authentication with 2 users with different - passwords - """ - conf = format_pam_krb5_auth(config, kdc_instance) - create_conf_fixture(request, conf) - create_sssd_fixture(request, kdc_instance.krb5_conf_path) - - passwd_ops_setup.useradd(**USER1) - passwd_ops_setup.useradd(**USER2) - kdc_instance.add_principal("user1", "Secret123User1") - kdc_instance.add_principal("user2", "Secret123User2") - time.sleep(2) # Give KDC time to initialize - return None - - -def test_krb5_auth(setup_krb5, env_for_sssctl): - """ - Test basic Kerberos authentication, check for authentication failure when - a wrong password is used - """ - sssctl = subprocess.Popen(["sssctl", "user-checks", "user1", - "--action=auth", - "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="Secret123User1") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find(r"pam_authenticate for user [user1]: Success") != -1 - - sssctl = subprocess.Popen(["sssctl", "user-checks", "user2", - "--action=auth", - "--service=pam_sss_service"], - universal_newlines=True, - env=env_for_sssctl, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - try: - out, err = sssctl.communicate(input="Secret123User1") - except Exception: - sssctl.kill() - out, err = sssctl.communicate() - - sssctl.stdin.close() - sssctl.stdout.close() - - if sssctl.wait() != 0: - raise Exception("sssctl failed") - - assert err.find(r"pam_authenticate for user [user2]: " - "Authentication failure") != -1 - - @pytest.fixture def setup_krb5_domains(request, kdc_instance, passwd_ops_setup): """ From 3c8b2e3ff582918ac891184e92d367c6655f0050 Mon Sep 17 00:00:00 2001 From: Dan Lavu Date: Thu, 30 Jul 2026 16:52:31 -0400 Subject: [PATCH 3/4] test rewrite: legacy intg test_pam_responder.py - remaining batch Rewrite the remaining sssd/src/tests/intg/test_pam_responder.py cases to test_smartcard.py, test_authentication.py - test_try_sc_auth_root -> test_smartcard__try_cert_auth_never_used_for_root - test_sc_auth_missing_name, test_sc_auth_missing_name_whitespace -> test_smartcard__certificate_owner_resolved_when_username_is_missing - test_sc_auth_name_format -> test_smartcard__certificate_owner_resolved_with_full_name_format - test_sc_auth_two_missing_name -> test_smartcard__certificate_owner_resolved_with_two_tokens_and_missing_name - test_krb5_auth_domains -> test_authentication__pam_sss_domains_skips_non_matching_krb5_domains Peeling out the reviewed test cases into it's own PR, from https://github.com/SSSD/sssd/pull/8873 Co-authored-by: Cursor Model used: Claude Sonnet 5 --- src/tests/system/tests/test_authentication.py | 109 +++++++++++++ src/tests/system/tests/test_smartcard.py | 152 ++++++++++++++++++ 2 files changed, 261 insertions(+) diff --git a/src/tests/system/tests/test_authentication.py b/src/tests/system/tests/test_authentication.py index 0e132876921..19c7bcc7301 100644 --- a/src/tests/system/tests/test_authentication.py +++ b/src/tests/system/tests/test_authentication.py @@ -7,11 +7,15 @@ from __future__ import annotations import re +import textwrap +from inspect import cleandoc import pytest from sssd_test_framework.roles.client import Client from sssd_test_framework.roles.generic import GenericProvider +from sssd_test_framework.roles.ipa import IPA from sssd_test_framework.roles.kdc import KDC +from sssd_test_framework.roles.samba import Samba from sssd_test_framework.topology import KnownTopology, KnownTopologyGroup @@ -409,3 +413,108 @@ def test_authentication__custom_password_prompt_is_shown_at_login( result = client.host.conn.run("su - user1 -c 'su - user1 -c whoami'", input="Secret123") assert "My custom prompt" in result.stderr, "Custom password prompt was not shown!" assert "user1" in result.stdout, "'user1' failed to log in!" + + +@pytest.mark.importance("medium") +@pytest.mark.authentication +@pytest.mark.topology(KnownTopology.ALLREALMS) +def test_authentication__pam_sss_domains_skips_non_matching_krb5_domains( + client: Client, samba: Samba, ipa: IPA, kdc: KDC +): + """ + :title: pam_sss.so 'domains' authenticates only against the listed Kerberos realm domain + :description: + Local users may authenticate via Kerberos against one of several configured realms + (Samba, IPA, or a standalone KDC). The same username exists in every realm with a + different password; each PAM 'domains=' line ignores the other realms and only tries + its listed domain. + :setup: + 1. Add a local user and create 'user1' in the Samba, IPA, and KDC realms with + different passwords + 2. Configure three SSSD domains (samba, ipa, krb5) with id_provider=proxy/files and + auth_provider=krb5 using each provider's realm + 3. Replace 'su-l' with three 'sufficient' pam_sss.so lines, each limited by 'domains=' + :steps: + 1. Authenticate as the local user via 'su -' using the KDC password + 2. Change the IPA principal's password to match the KDC password and authenticate again + :expectedresults: + 1. Authentication succeeds via 'domains=krb5'; Samba and IPA users exist but are + ignored by that PAM line + 2. Authentication succeeds via 'domains=ipa', since that line is tried first and now + matches too + :customerscenario: True + """ + client.local.user("user1").add(password="LocalSecret123") + samba.user("user1").add(password="SambaSecret123") + ipa.user("user1").add(password="IPASecret123") + kdc.principal("user1").add(password="KDCSecret123") + + client.sssd.fs.write( + "/etc/krb5.conf", + textwrap.dedent(f""" + [libdefaults] + default_realm = {kdc.realm} + dns_lookup_realm = false + dns_lookup_kdc = false + ticket_lifetime = 24h + renew_lifetime = 7d + forwardable = yes + + [realms] + {samba.realm} = {{ + kdc = {samba.host.hostname} + }} + {ipa.realm} = {{ + kdc = {ipa.host.hostname} + }} + {kdc.realm} = {{ + kdc = {kdc.host.hostname}:88 + admin_server = {kdc.host.hostname}:749 + }} + """).lstrip(), + user="root", + group="root", + mode="0644", + ) + + for name, role in (("samba", samba), ("ipa", ipa), ("krb5", kdc)): + client.sssd.dom(name).update( + enabled="true", + id_provider="proxy", + proxy_lib_name="files", + auth_provider="krb5", + krb5_realm=role.realm, + krb5_server=role.host.hostname, + ) + client.sssd.sssd["domains"] = "samba, ipa, krb5" + client.sssd.default_domain = "krb5" + client.sssd.start() + + client.fs.backup("/etc/pam.d/su-l") + client.fs.write( + "/etc/pam.d/su-l", + cleandoc(""" + auth required pam_env.so + auth sufficient pam_sss.so forward_pass domains=samba + auth sufficient pam_sss.so forward_pass domains=ipa + auth sufficient pam_sss.so forward_pass domains=krb5 + auth required pam_deny.so + account required pam_sss.so + password required pam_sss.so + session required pam_sss.so + """), + ) + + assert client.auth.su.password( + "user1", "KDCSecret123" + ), "Authentication should succeed via the matching 'domains=krb5' line!" + + # IPA always forces an immediate password-expiration on an administrative password reset, + # even if 'password-expiration' is passed in the same call, so it must be pushed back out + # in a separate modification. + ipa.user("user1").modify(password="KDCSecret123") + ipa.user("user1").modify(password_expiration="20380101120000Z") + + assert client.auth.su.password( + "user1", "KDCSecret123" + ), "Authentication should also succeed via the earlier 'domains=ipa' line once its password matches!" diff --git a/src/tests/system/tests/test_smartcard.py b/src/tests/system/tests/test_smartcard.py index 6645cff8c88..83aa22a3441 100644 --- a/src/tests/system/tests/test_smartcard.py +++ b/src/tests/system/tests/test_smartcard.py @@ -442,3 +442,155 @@ def test_smartcard__login_fails_when_cert_auth_required_without_card(client: Cli client.smartcard.remove_card() assert not client.auth.su.smartcard("user1", TOKEN_PIN), "Authentication should have failed without a card!" + + +@pytest.mark.importance("critical") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__try_cert_auth_never_used_for_root(client: Client): + """ + :title: try_cert_auth never routes root's own login through certificate authentication + :description: + pam_sss.so unconditionally refuses to handle the 'root' identity. When 'try_cert_auth' + is set, that refusal must surface as PAM_AUTHINFO_UNAVAIL (so the PAM stack falls back + to another module), not as a successful or user-unknown result. This is verified via + 'sssctl user-checks' against a minimal 'auth required pam_sss.so try_cert_auth' service, + since there is no way to originate a fresh authentication attempt for the 'root' identity + itself via 'su'/'ssh' (root already owns the control connection). + :setup: + 1. Create a local user and initialize a smart card mapped to the user + 2. Install a minimal PAM service with 'pam_sss.so try_cert_auth' + :steps: + 1. Run 'sssctl user-checks root' against that service + :expectedresults: + 1. Authentication is reported unavailable, never routed through certificate auth + :customerscenario: True + """ + client.local.user("user1").add() + client.smartcard.setup_local_card(client, "user1") + client.fs.write( + "/etc/pam.d/pam_sss_try_sc", + """ + auth required pam_sss.so try_cert_auth + account required pam_sss.so + password required pam_sss.so + session required pam_sss.so + """, + ) + + result = client.sssctl.user_checks("root", action="auth", service="pam_sss_try_sc", auth_input=TOKEN_PIN) + assert ( + "pam_authenticate for user [root]: Authentication service cannot retrieve authentication info" in result.stderr + ), f"root should never be routed through certificate authentication! stderr={result.stderr}" + + +@pytest.mark.importance("high") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.parametrize("username_input", ["", " "], ids=["empty_name", "whitespace_only_name"]) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__certificate_owner_resolved_when_username_is_missing(client: Client, username_input: str): + """ + :title: allow_missing_name resolves the certificate owner when no username is given + :setup: + 1. Create a local user and initialize a smart card mapped to the user + :steps: + 1. Authenticate against the 'smartcard-auth' service with an empty or + whitespace-only username and the smart card PIN + :expectedresults: + 1. Authentication succeeds and is resolved to the certificate's mapped user + :customerscenario: True + """ + client.local.user("user1").add() + client.smartcard.setup_local_card(client, "user1") + client.authselect.select("sssd", ["with-smartcard-required"]) + client.sssd.pam["pam_p11_allowed_services"] = "+smartcard-auth" + client.sssd.restart() + + result = client.sssctl.user_checks(username_input, action="auth", service="smartcard-auth", auth_input=TOKEN_PIN) + assert ( + "pam_authenticate for user [user1]: Success" in result.stderr + ), f"Certificate owner was not resolved! stderr={result.stderr}" + + +@pytest.mark.importance("medium") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.builtwith(client="virtualsmartcard") +def test_smartcard__certificate_owner_resolved_with_full_name_format(client: Client): + """ + :title: allow_missing_name respects full_name_format when resolving the certificate owner + :setup: + 1. Create a local user and initialize a smart card mapped to the user + 2. Enable fully-qualified names with a custom 'full_name_format' + :steps: + 1. Authenticate against the 'smartcard-auth' service with no username and the smart card PIN + :expectedresults: + 1. Authentication succeeds and the resolved user name matches 'full_name_format' + :customerscenario: True + """ + client.local.user("user1").add() + client.smartcard.setup_local_card(client, "user1") + client.authselect.select("sssd", ["with-smartcard-required"]) + client.sssd.pam["pam_p11_allowed_services"] = "+smartcard-auth" + client.sssd.domain["use_fully_qualified_names"] = "True" + client.sssd.domain["full_name_format"] = "%2$s\\%1$s" + client.sssd.restart(clean=True) + + result = client.sssctl.user_checks("", action="auth", service="smartcard-auth", auth_input=TOKEN_PIN) + assert ( + "pam_authenticate for user [local\\user1]: Success" in result.stderr + ), f"Certificate owner was not resolved with full_name_format applied! stderr={result.stderr}" + + +@pytest.mark.importance("medium") +@pytest.mark.topology(KnownTopology.Client) +@pytest.mark.parametrize("cert_selection", [1, 2]) +def test_smartcard__certificate_owner_resolved_with_two_tokens_and_missing_name(client: Client, cert_selection: int): + """ + :title: allow_missing_name resolves the certificate owner when two tokens are present + :setup: + 1. Create a local user + 2. Reset the certificate CA trust store to a clean state + 3. Initialize two SoftHSM tokens, each holding a certificate mapped to the user, + and trust both certificates in the CA trust store + 4. Configure SSSD for smart card authentication and start services + :steps: + 1. Authenticate against the 'smartcard-auth' service with no username, selecting + each certificate in turn + :expectedresults: + 1. Authentication succeeds and is resolved to the certificate's mapped user for + either certificate selection + :customerscenario: True + """ + username = "user1" + client.local.user(username).add() + client.host.fs.rm("/etc/sssd/pki/sssd_auth_ca_db.pem") + + key1, cert1 = client.smartcard.generate_cert(key_path="/tmp/sc_token1.key", cert_path="/tmp/sc_token1.crt") + client.smartcard.initialize_card(label=TOKEN1_LABEL, user_pin=TOKEN_PIN, reset=True) + client.smartcard.add_key(key1, token_label=TOKEN1_LABEL, label=username) + client.smartcard.add_cert(cert1, token_label=TOKEN1_LABEL, label=username) + key2, cert2 = client.smartcard.generate_cert(key_path="/tmp/sc_token2.key", cert_path="/tmp/sc_token2.crt") + client.smartcard.initialize_card(label=TOKEN2_LABEL, user_pin=TOKEN_PIN, reset=False) + client.smartcard.add_key(key2, token_label=TOKEN2_LABEL, label=username) + client.smartcard.add_cert(cert2, token_label=TOKEN2_LABEL, label=username) + + client.sssd.common.local() + client.sssd.section(f"certmap/local/{username}")["matchrule"] = ".*CN=Test Cert.*" + client.sssd.pam["pam_cert_auth"] = "True" + for cert in (cert1, cert2): + # dedent=False is required here: fs.append() strips trailing whitespace, + # by default which will corrupt the CA bundle. + client.host.fs.append( + "/etc/sssd/pki/sssd_auth_ca_db.pem", client.host.fs.read(cert).strip() + "\n", dedent=False + ) + client.sssd.common.smartcard_with_softhsm(client.smartcard) + client.authselect.select("sssd", ["with-smartcard-required", "with-mkhomedir"]) + client.sssd.pam["pam_p11_allowed_services"] = "+smartcard-auth" + client.sssd.restart() + + result = client.sssctl.user_checks( + "", action="auth", service="smartcard-auth", auth_input=f"{cert_selection}\n{TOKEN_PIN}" + ) + assert ( + f"pam_authenticate for user [{username}]: Success" in result.stderr + ), f"Certificate owner was not resolved! stderr={result.stderr}" From f6daee98573d1fa6436543105a246dd5f874cbba Mon Sep 17 00:00:00 2001 From: Dan Lavu Date: Fri, 31 Jul 2026 02:00:01 -0400 Subject: [PATCH 4/4] DO MERGE WITH THIS COMMIT --- src/tests/system/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/system/requirements.txt b/src/tests/system/requirements.txt index 788c9285d55..7f9fb9e1240 100644 --- a/src/tests/system/requirements.txt +++ b/src/tests/system/requirements.txt @@ -5,4 +5,4 @@ git+https://github.com/next-actions/pytest-mh git+https://github.com/next-actions/pytest-ticket git+https://github.com/next-actions/pytest-tier git+https://github.com/next-actions/pytest-output -git+https://github.com/SSSD/sssd-test-framework +git+https://github.com/danlavu/sssd-test-framework@topology-all-dc