Skip to content

Commit

Permalink
pam: adding support to manage pam_access and pam_faillock
Browse files Browse the repository at this point in the history
  • Loading branch information
Dan Lavu committed Aug 20, 2023
1 parent b5570e6 commit 507059d
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/guides/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ How to guides
testing-offline
testing-passkey
local-users
pam
54 changes: 54 additions & 0 deletions docs/guides/pam.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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("user-1").add()
provider.user("user-1").add()
# Add rule to permit "user-1" and deny "user-2"
client.pam.access.add(["+:user-1:ALL","-:user-2:NONE"])
client.sssd.common.pam(["with-pamaccess"])
client.sssd.start()
# Check the results
assert client.auth.ssh.password("user-1", "Secret123")
assert not client.auth.ssh.password("user-2", "Secret123")
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("user-1").add()
# Setup faillock
client.pam.faillock.config()
client.sssd.common.pam(["with-faillock"])
# Start SSSD
client.sssd.start()
# Check the results
assert client.auth.ssh.password("user-1", "Secret123")
# Three failed login attempts
for i in range(3):
assert not client.auth.ssh.password("user-1", "bad_password")
assert not client.auth.ssh.password("user-1", "Secret123")
# Reset user lockout
client.pam.faillock("user-1").reset
assert client.auth.ssh.password("user-1", "Secret123")
6 changes: 6 additions & 0 deletions sssd_test_framework/roles/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
235 changes: 235 additions & 0 deletions sssd_test_framework/utils/pam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
""""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.file = None
self.fs: LinuxFileSystem = fs

def access(self, file: str | None = None) -> PAMAccess:
"""
:param file: PAM Access file name.
:type file: str, optional
:return: PAM Access object
:rtype: PAMAccess
"""
if file is None:
return PAMAccess(self)

return PAMAccess(self, file)

def faillock(self, user: str | None = None, file: str | None = None) -> PAMFaillock:
"""
:param user: Username.
:type user: str, optional
:param file: PAM Faillock file name.
:type file: str, optional
:return: PAM Faillock object
:rtype: PAMFaillock
"""
if file is None:
return PAMFaillock(self, user)
if user is None:
return PAMFaillock(self, file)
if user and file is None:
return PAMFaillock(self)

return PAMFaillock(self, user, file)


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("user-1").add()
provider.user("user-1").add()
# Add rule to permit "user-1" and deny "user-2"
client.pam.access.add(["+:user-1:ALL","-:user-2:NONE"])
client.sssd.common.pam(["with-pamaccess"])
client.sssd.start()
# Check the results
assert client.auth.ssh.password("user-1", "Secret123")
assert not client.auth.ssh.password("user-2", "Secret123")
"""

def __init__(self, util: PAMUtils, file: str = "/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("user-1").add()
# Setup faillock
client.pam.faillock.config()
client.sssd.common.pam(["with-faillock"])
# Start SSSD
client.sssd.start()
# Check the results
assert client.auth.ssh.password("user-1", "Secret123")
# Three failed login attempts
for i in range(3):
assert not client.auth.ssh.password("user-1", "bad_password")
assert not client.auth.ssh.password("user-1", "Secret123")
# Reset user lockout
client.pam.faillock("user-1").reset
assert client.auth.ssh.password("user-1", "Secret123")
"""

def __init__(self, util: PAMUtils, user: str | None = None, file: str = "/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 | None = 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", "--user", 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", "--user", self.user, "--reset"])

return self
13 changes: 13 additions & 0 deletions sssd_test_framework/utils/sssd.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,3 +783,16 @@ def autofs(self) -> None:
"""
self.sssd.authselect.select("sssd")
self.sssd.enable_responder("autofs")

def pam(self, features: [str] = []) -> None:
"""
Configure SSSD with pam.
#. Select authselect sssd profile
#. Enable pam responder
:param features: list of authselect features
:type features: list[str], optional
"""
self.sssd.authselect.select("sssd", features)
self.sssd.enable_responder("pam")

0 comments on commit 507059d

Please sign in to comment.