mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-06-28 06:41:43 +00:00
feat: add support of pam authentication
Add naive implementation of user password check by calling su command. Also change some authentication method to require username to be string instead of optional string
This commit is contained in:
@ -2,6 +2,7 @@ import pytest
|
||||
|
||||
from ahriman.core.auth.mapping import Mapping
|
||||
from ahriman.core.auth.oauth import OAuth
|
||||
from ahriman.core.auth.pam import PAM
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
|
||||
@ -35,3 +36,19 @@ def oauth(configuration: Configuration, database: SQLite) -> OAuth:
|
||||
"""
|
||||
configuration.set("web", "address", "https://example.com")
|
||||
return OAuth(configuration, database)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pam(configuration: Configuration, database: SQLite) -> PAM:
|
||||
"""
|
||||
PAM provider fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
database(SQLite): database fixture
|
||||
|
||||
Returns:
|
||||
PAM: PAM service instance
|
||||
"""
|
||||
configuration.set_option("auth", "full_access_group", "wheel")
|
||||
return PAM(configuration, database)
|
||||
|
@ -1,6 +1,7 @@
|
||||
from ahriman.core.auth import Auth
|
||||
from ahriman.core.auth.mapping import Mapping
|
||||
from ahriman.core.auth.oauth import OAuth
|
||||
from ahriman.core.auth.pam import PAM
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.models.user import User
|
||||
@ -51,14 +52,22 @@ def test_load_oauth(configuration: Configuration, database: SQLite) -> None:
|
||||
assert isinstance(auth, OAuth)
|
||||
|
||||
|
||||
def test_load_pam(configuration: Configuration, database: SQLite) -> None:
|
||||
"""
|
||||
must load pam validator if option set
|
||||
"""
|
||||
configuration.set_option("auth", "target", "pam")
|
||||
configuration.set_option("auth", "full_access_group", "wheel")
|
||||
auth = Auth.load(configuration, database)
|
||||
assert isinstance(auth, PAM)
|
||||
|
||||
|
||||
async def test_check_credentials(auth: Auth, user: User) -> None:
|
||||
"""
|
||||
must pass any credentials
|
||||
"""
|
||||
assert await auth.check_credentials(user.username, user.password)
|
||||
assert await auth.check_credentials(None, "")
|
||||
assert await auth.check_credentials("", None)
|
||||
assert await auth.check_credentials(None, None)
|
||||
|
||||
|
||||
async def test_known_username(auth: Auth, user: User) -> None:
|
||||
|
@ -21,9 +21,7 @@ async def test_check_credentials_empty(mapping: Mapping) -> None:
|
||||
"""
|
||||
must reject on empty credentials
|
||||
"""
|
||||
assert not await mapping.check_credentials(None, "")
|
||||
assert not await mapping.check_credentials("", None)
|
||||
assert not await mapping.check_credentials(None, None)
|
||||
|
||||
|
||||
async def test_check_credentials_unknown(mapping: Mapping, user: User) -> None:
|
||||
@ -66,9 +64,8 @@ async def test_known_username(mapping: Mapping, user: User, mocker: MockerFixtur
|
||||
|
||||
async def test_known_username_unknown(mapping: Mapping, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must not allow only known users
|
||||
must not allow unknown users
|
||||
"""
|
||||
assert not await mapping.known_username(None)
|
||||
mocker.patch("ahriman.core.database.SQLite.user_get", return_value=None)
|
||||
assert not await mapping.known_username(user.password)
|
||||
|
||||
|
118
tests/ahriman/core/auth/test_pam.py
Normal file
118
tests/ahriman/core/auth/test_pam.py
Normal file
@ -0,0 +1,118 @@
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.auth.pam import PAM
|
||||
from ahriman.core.exceptions import CalledProcessError
|
||||
from ahriman.models.user import User
|
||||
from ahriman.models.user_access import UserAccess
|
||||
|
||||
|
||||
def test_group_members() -> None:
|
||||
"""
|
||||
must return current group members
|
||||
"""
|
||||
assert "root" in PAM.group_members("root")
|
||||
|
||||
|
||||
def test_group_members_unknown() -> None:
|
||||
"""
|
||||
must return empty list for unknown group
|
||||
"""
|
||||
assert not PAM.group_members("somerandomgroupname")
|
||||
|
||||
|
||||
async def test_check_credentials(pam: PAM, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly check user credentials via PAM
|
||||
"""
|
||||
authenticate_mock = mocker.patch("ahriman.core.auth.pam.check_output")
|
||||
mapping_mock = mocker.patch("ahriman.core.auth.mapping.Mapping.check_credentials")
|
||||
|
||||
assert await pam.check_credentials(user.username, user.password)
|
||||
authenticate_mock.assert_called_once_with("su", "--command", "true", "-", user.username,
|
||||
input_data=user.password)
|
||||
mapping_mock.assert_not_called()
|
||||
|
||||
|
||||
async def test_check_credentials_empty(pam: PAM) -> None:
|
||||
"""
|
||||
must reject on empty credentials
|
||||
"""
|
||||
assert not await pam.check_credentials("", None)
|
||||
|
||||
|
||||
async def test_check_credentials_root(pam: PAM, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must reject on root logon attempt
|
||||
"""
|
||||
mocker.patch("ahriman.core.auth.pam.check_output")
|
||||
assert not await pam.check_credentials("root", user.password)
|
||||
|
||||
pam.permit_root_login = True
|
||||
assert await pam.check_credentials("root", user.password)
|
||||
|
||||
|
||||
async def test_check_credentials_mapping(pam: PAM, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly check user credentials via database if PAM rejected
|
||||
"""
|
||||
mocker.patch("ahriman.core.auth.pam.check_output",
|
||||
side_effect=CalledProcessError(1, ["command"], "error"))
|
||||
mapping_mock = mocker.patch("ahriman.core.auth.mapping.Mapping.check_credentials")
|
||||
|
||||
await pam.check_credentials(user.username, user.password)
|
||||
mapping_mock.assert_called_once_with(pam, user.username, user.password)
|
||||
|
||||
|
||||
async def test_known_username(pam: PAM, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must check if user exists in system
|
||||
"""
|
||||
getpwnam_mock = mocker.patch("ahriman.core.auth.pam.getpwnam")
|
||||
mapping_mock = mocker.patch("ahriman.core.auth.mapping.Mapping.known_username")
|
||||
|
||||
assert await pam.known_username(user.username)
|
||||
getpwnam_mock.assert_called_once_with(user.username)
|
||||
mapping_mock.assert_not_called()
|
||||
|
||||
|
||||
async def test_known_username_mapping(pam: PAM, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must fallback to username checking to database if no user found in system
|
||||
"""
|
||||
mocker.patch("ahriman.core.auth.pam.getpwnam", side_effect=KeyError)
|
||||
mapping_mock = mocker.patch("ahriman.core.auth.mapping.Mapping.known_username")
|
||||
|
||||
await pam.known_username(user.username)
|
||||
mapping_mock.assert_called_once_with(pam, user.username)
|
||||
|
||||
|
||||
async def test_verify_access(pam: PAM, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must verify user access via PAM groups
|
||||
"""
|
||||
mocker.patch("ahriman.core.auth.pam.PAM.get_user", return_value=None)
|
||||
mocker.patch("ahriman.core.auth.pam.PAM.group_members", return_value=[user.username])
|
||||
assert await pam.verify_access(user.username, UserAccess.Full, None)
|
||||
|
||||
|
||||
async def test_verify_access_readonly(pam: PAM, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must set user access to read only if it doesn't belong to the admin group
|
||||
"""
|
||||
mocker.patch("ahriman.core.auth.pam.PAM.get_user", return_value=None)
|
||||
mocker.patch("ahriman.core.auth.pam.PAM.group_members", return_value=[])
|
||||
|
||||
assert not await pam.verify_access(user.username, UserAccess.Full, None)
|
||||
assert not await pam.verify_access(user.username, UserAccess.Reporter, None)
|
||||
assert await pam.verify_access(user.username, UserAccess.Read, None)
|
||||
|
||||
|
||||
async def test_verify_access_override(pam: PAM, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must verify user access via database if there is override
|
||||
"""
|
||||
mocker.patch("ahriman.core.auth.pam.PAM.get_user", return_value=user)
|
||||
group_mock = mocker.patch("ahriman.core.auth.pam.PAM.group_members")
|
||||
|
||||
assert await pam.verify_access(user.username, user.access, None)
|
||||
group_mock.assert_not_called()
|
@ -26,6 +26,9 @@ def test_from_option_valid() -> None:
|
||||
assert AuthSettings.from_option("mapping") == AuthSettings.Configuration
|
||||
assert AuthSettings.from_option("MAPPing") == AuthSettings.Configuration
|
||||
|
||||
assert AuthSettings.from_option("pam") == AuthSettings.PAM
|
||||
assert AuthSettings.from_option("PAM") == AuthSettings.PAM
|
||||
|
||||
|
||||
def test_is_enabled() -> None:
|
||||
"""
|
||||
|
Reference in New Issue
Block a user