feat(auth): PAM authentication with running-user check
This commit is contained in:
@@ -95,3 +95,31 @@ def verify_session_cookie(secret: str, cookie: str, ttl_seconds: int) -> bool:
|
|||||||
return True
|
return True
|
||||||
except (BadSignature, SignatureExpired):
|
except (BadSignature, SignatureExpired):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PAM authentication
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def pam_available() -> bool:
|
||||||
|
"""Check whether the python-pam module is importable."""
|
||||||
|
try:
|
||||||
|
import pam # noqa: F401
|
||||||
|
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate_pam(username: str, password: str) -> bool:
|
||||||
|
"""Authenticate via PAM. Username must match the running process owner."""
|
||||||
|
import os as _os
|
||||||
|
import pwd
|
||||||
|
|
||||||
|
import pam
|
||||||
|
|
||||||
|
running_user = pwd.getpwuid(_os.getuid()).pw_name
|
||||||
|
if username != running_user:
|
||||||
|
return False
|
||||||
|
return pam.authenticate(username, password, service="login")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Tests for muxplex/auth.py — authentication module."""
|
"""Tests for muxplex/auth.py — authentication module."""
|
||||||
|
|
||||||
|
import os
|
||||||
import stat
|
import stat
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -170,3 +171,64 @@ def test_verify_session_cookie_expired():
|
|||||||
# age = 2 > max_age = 1, ensuring reliable expiry detection
|
# age = 2 > max_age = 1, ensuring reliable expiry detection
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
assert verify_session_cookie("test-secret", cookie, ttl_seconds=1) is False
|
assert verify_session_cookie("test-secret", cookie, ttl_seconds=1) is False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PAM authentication
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_pam_available_returns_true_when_pam_importable():
|
||||||
|
"""pam_available() returns True when python-pam is installed."""
|
||||||
|
from muxplex.auth import pam_available
|
||||||
|
|
||||||
|
# python-pam is in our deps, so it should be importable
|
||||||
|
assert pam_available() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_pam_available_returns_false_on_import_error(monkeypatch):
|
||||||
|
"""pam_available() returns False when pam cannot be imported."""
|
||||||
|
import builtins
|
||||||
|
|
||||||
|
real_import = builtins.__import__
|
||||||
|
|
||||||
|
def mock_import(name, *args, **kwargs):
|
||||||
|
if name == "pam":
|
||||||
|
raise ImportError("mock: no pam")
|
||||||
|
return real_import(name, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(builtins, "__import__", mock_import)
|
||||||
|
from muxplex.auth import pam_available
|
||||||
|
|
||||||
|
assert pam_available() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_authenticate_pam_success(monkeypatch):
|
||||||
|
"""authenticate_pam() returns True when PAM succeeds for the running user."""
|
||||||
|
import pwd
|
||||||
|
|
||||||
|
from muxplex.auth import authenticate_pam
|
||||||
|
|
||||||
|
running_user = pwd.getpwuid(os.getuid()).pw_name
|
||||||
|
monkeypatch.setattr("pam.authenticate", lambda u, p, service="login": True)
|
||||||
|
assert authenticate_pam(running_user, "correct-password") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_authenticate_pam_wrong_password(monkeypatch):
|
||||||
|
"""authenticate_pam() returns False when PAM rejects credentials."""
|
||||||
|
import pwd
|
||||||
|
|
||||||
|
from muxplex.auth import authenticate_pam
|
||||||
|
|
||||||
|
running_user = pwd.getpwuid(os.getuid()).pw_name
|
||||||
|
monkeypatch.setattr("pam.authenticate", lambda u, p, service="login": False)
|
||||||
|
assert authenticate_pam(running_user, "wrong-password") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_authenticate_pam_wrong_user_rejected(monkeypatch):
|
||||||
|
"""authenticate_pam() rejects a different username even if PAM would accept it."""
|
||||||
|
from muxplex.auth import authenticate_pam
|
||||||
|
|
||||||
|
# Mock PAM to always return True — but wrong username should still fail
|
||||||
|
monkeypatch.setattr("pam.authenticate", lambda u, p, service="login": True)
|
||||||
|
assert authenticate_pam("root", "any-password") is False
|
||||||
|
|||||||
Reference in New Issue
Block a user