diff --git a/docs/guides/pam.rst b/docs/guides/pam.rst new file mode 100644 index 00000000..d57c8c4a --- /dev/null +++ b/docs/guides/pam.rst @@ -0,0 +1,52 @@ +Pluggable Authentication Module (PAM) +##################################### + +Class :class:`sssd_test_framework.utils.pam.PAMUtils` provides +an API to manage PAM modules; pam_access and pam_faillock. + +pam_access: A module that performs host based access control on a system. + +.. code-block:: python + :caption: Example PAM Access usage + @pytest.mark.topology(KnownTopologyGroup.AnyProvider) + def test_example(client: Client, provider: GenericProvider): + # Add users + provider.user("user1").add(password="Secret123") + provider.user("user1").add(password="Secret123") + + # Add rule to permit "user1" and deny "user2" + client.pam.access.add(["+:user1:ALL","-:user2:NONE] + client.authselect.select("sssd", "[with-pamaccess]") + + # Start SSSD + client.sssd.start() + + # Check the results + assert client.auth.ssh.password("user1", "Secret123") + assert client.auth.ssh.password("user2", "Secret123") is False +pam_faillock: A module that sets login attempts and lock out time. + +.. code-block:: python + :caption: Example PAM Faillock usage + + @pytest.mark.topology(KnownTopologyGroup.AnyProvider) + def test_example(client: Client, provider: GenericProvider): + # Add user + provider.user("user1").add(password="Secret123") + + # Setup faillock + client.pam.faillock.config() + client.authselect.select("sssd", "with-faillock") + + # Start SSSD + client.sssd.start() + + # Check the results + assert client.auth.ssh.password("user1", "Secret123") + assert client.auth.ssh.password("user1", "bad_password") is False + assert client.auth.ssh.password("user1", "bad_password") is False + assert client.auth.ssh.password("user1", "bad_password") is False + assert client.auth.ssh.password("user1", "Secret123") is False + + client.pam.faillock("user1").reset + assert client.auth.ssh.password("user1", "Secret123") diff --git a/sssd_test_framework/roles/client.py b/sssd_test_framework/roles/client.py index c3df1c74..8351b097 100644 --- a/sssd_test_framework/roles/client.py +++ b/sssd_test_framework/roles/client.py @@ -7,6 +7,7 @@ from ..utils.automount import AutomountUtils from ..utils.ldb import LDBUtils from ..utils.local_users import LocalUsersUtils +from ..utils.pam import PAMUtils from ..utils.sss_override import SSSOverrideUtils from ..utils.sssctl import SSSCTLUtils from ..utils.sssd import SSSDUtils @@ -70,6 +71,11 @@ def __init__(self, *args, **kwargs) -> None: Managing local overrides users and groups. """ + self.pam: PAMUtils = PAMUtils(self.host, self.fs) + """ + Managing PAM modules; pam_access and pam_faillock. + """ + def setup(self) -> None: """ Called before execution of each test. diff --git a/sssd_test_framework/utils/pam.py b/sssd_test_framework/utils/pam.py new file mode 100644 index 00000000..f96153d1 --- /dev/null +++ b/sssd_test_framework/utils/pam.py @@ -0,0 +1,220 @@ +""""PAM Tools.""" + +from __future__ import annotations + +from pytest_mh import MultihostHost, MultihostUtility +from pytest_mh.utils.fs import LinuxFileSystem + +__all__ = [ + "PAMUtils", + "PAMAccess", + "PAMFaillock", +] + + +class PAMUtils(MultihostUtility[MultihostHost]): + """ + Management of PAM modules + """ + + def __init__(self, host: MultihostHost, fs: LinuxFileSystem) -> None: + """ + :param host: Remote host instance + :type host: MultihostHost + """ + super().__init__(host) + + self.fs: LinuxFileSystem = fs + + def access(self) -> PAMAccess: + """ + :return: PAM Access object + :rtype: PAMAccess + """ + return PAMAccess(self) + + def faillock(self) -> PAMFaillock: + """ + :return: PAM Faillock object + :rtype: PAMFaillock + """ + return PAMFaillock(self) + + +class PAMAccess: + """ + Management of PAM Access on the client host. + + .. code-block:: python + :caption: Example usage + + @pytest.mark.topology(KnownTopologyGroup.AnyProvider) + def test_example(client: Client, provider: GenericProvider): + # Add users + provider.user("user1").add(password="Secret123") + provider.user("user1").add(password="Secret123") + + # Add rule to permit "user1" and deny "user2" + client.pam.access.add(["+:user1:ALL","-:user2:NONE] + client.authselect.select("sssd", "[with-pamaccess]") + + # Start SSSD + client.sssd.start() + + # Check the results + assert client.auth.ssh.password("user1", "Secret123") + assert client.auth.ssh.password("user2", "Secret123") is False + """ + + def __init__(self, util: PAMUtils, file: str | None = "/etc/security/access.conf") -> None: + """ + :param util: PAMUtils utility object + :type util: PAMUtils + :param file: File name of access file + :type file: str, optional + """ + self.util: PAMUtils = util + self.file: str = file + + def get(self) -> list[str]: + """ + Get PAM rules from access file. + :return: List of PAM access rules + :rtype: list + """ + result = self.util.fs.read(self.file).split("\n") + self.util.logger.info(f"{result} are in {self.file} on {self.util.host.hostname}") + + return result + + def add(self, rules: list[str]) -> PAMAccess: + """ + Add one or more rules to access file. + :param rules: Access rule + :type rules: list[str], required + :return: self + :rtype: PAMAccess + """ + content = "" + for i in rules: + content += f"{i}\n" + + self.util.logger.info(f"{content} written to {self.file} on {self.util.host.hostname}") + self.util.fs.write(self.file, content) + + return self + + def delete(self, rules: list[str]) -> PAMAccess: + """ + Delete one or more rules from access file. + :param rules: PAM Access rule + :type rules: list[str], required + """ + result = self.get() + for i in result: + if i in rules: + result.pop() + + content = "" + for i in result: + content += f"{i}\n" + + self.util.logger.info(f"{content} written to {self.file} on {self.util.host.hostname}") + self.util.fs.write(self.file, content) + + return self + + +class PAMFaillock: + """ + Management of PAM Faillock on the client host. + + .. code-block:: python + :caption: Example usage + + @pytest.mark.topology(KnownTopologyGroup.AnyProvider) + def test_example(client: Client, provider: GenericProvider): + + # Add user + provider.user("user1").add(password="Secret123") + + # Setup faillock + client.pam.faillock.config() + client.authselect.select("sssd", "with-faillock") + + # Start SSSD + client.sssd.start() + + # Check the results + assert client.auth.ssh.password("user1", "Secret123") + assert client.auth.ssh.password("user1", "bad_password") is False + assert client.auth.ssh.password("user1", "bad_password") is False + assert client.auth.ssh.password("user1", "bad_password") is False + assert client.auth.ssh.password("user1", "Secret123") is False + + client.pam.faillock("user1").reset + assert client.auth.ssh.password("user1", "Secret123") + + """ + + def __init__( + self, util: PAMUtils, user: str | None = None, file: str | None = "/etc/security/faillock.conf" + ) -> None: + """ + :param util: PAMUtils object + :type util: PAMUtils + :param user: User + :type user: str, optional + :param file: Faillock configuration file + :type file: str, optional + """ + self.util: PAMUtils = util + self.user: str = user + self.file: str = file + + def config(self, deny: int | None = 3, unlock_time: int | None = 300) -> None: + """ + Configure the settings for PAM faillock. + :param deny: Deny attempts + :type deny: int, defaults to 3 + :param unlock_time: Unlock timeout in seconds + :type unlock_time: int, defaults to 300 + :return: Self + :rtype: PAMFaillock + """ + content = f"deny={deny}\nunlock_time={unlock_time}\nsilent" + + self.util.logger.info(f"{content} written to {self.file} on {self.util.host.hostname}") + self.util.fs.write(self.file, content) + + def get_config(self) -> str: + """ + Get the configuration for PAM Faillock. + :return: Contents of faillock.conf + :rtype: str + """ + result = self.util.fs.read(self.file) + + return result + + def info(self) -> str: + """ + Get user faillock information. + :return: Output from faillock + :rtype: str + """ + self.util.logger.info(f"Getting faillock information for {self.user} on {self.util.host.hostname}") + result = self.util.host.ssh.exec(["faillock", "--username", self.user]) + + return result.stdout + + def reset(self) -> PAMFaillock: + """ + Reset user tally information. + :return: Self + :rtype: PAMFaillock + """ + self.util.logger.info(f"Resetting faillock tally for {self.user} on {self.util.host.hostname}") + self.util.host.ssh.exec(["faillock", "--username", self.user, "--reset"]) + + return self