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
143 changes: 143 additions & 0 deletions src/tests/system/tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -375,3 +379,142 @@ 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(
Comment thread
danlavu marked this conversation as resolved.
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!"


@pytest.mark.importance("medium")
@pytest.mark.authentication
@pytest.mark.topology(KnownTopology.ALLDC)
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!"
133 changes: 133 additions & 0 deletions src/tests/system/tests/test_smartcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
danlavu marked this conversation as resolved.
@pytest.mark.builtwith(client="virtualsmartcard")
def test_smartcard__login_fails_when_wrong_pin_is_entered(client: Client):
Comment thread
danlavu marked this conversation as resolved.
"""
: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):
Comment thread
danlavu marked this conversation as resolved.
"""
: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(
Comment thread
danlavu marked this conversation as resolved.
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):
Comment thread
danlavu marked this conversation as resolved.
"""
: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!"
Loading