Delegate privileged authentication to sudo PAM

This commit is contained in:
Philip Henning 2026-09-03 18:35:20 +02:00
parent 7872991e0b
commit 026bbc4eff
8 changed files with 266 additions and 236 deletions

View file

@ -24,9 +24,8 @@ class TestPermissionManager:
assert not pm._sudo_validated
@patch("subprocess.run")
def test_request_sudo_already_available(self, mock_run):
"""Test requesting sudo when already available."""
# Mock successful sudo -n true
def test_request_sudo_uses_cached_authorization(self, mock_run):
"""Test cached sudo authorization enters without an interaction."""
mock_run.return_value = Mock(returncode=0)
pm = PermissionManager()
@ -38,56 +37,62 @@ class TestPermissionManager:
assert pm._sudo_validated
mock_run.assert_called_once_with(
["sudo", "-n", "true"], capture_output=True, text=True, timeout=5
["sudo", "-n", "-v"], capture_output=True, text=True, timeout=5
)
@patch("subprocess.run")
def test_request_sudo_prompt_success(self, mock_run):
"""Test requesting sudo with password prompt success."""
# First call (sudo -n true) fails, second call (sudo -S -v) succeeds
mock_run.side_effect = [
Mock(returncode=1), # sudo -n true fails
Mock(returncode=0), # sudo -S -v succeeds
]
def test_request_sudo_interactively_uses_foreground_pam(self, mock_run):
"""Test an uncached authorization delegates to foreground sudo."""
mock_run.return_value = Mock(returncode=0)
pm = PermissionManager()
success, message = pm.request_sudo("testpassword")
success, message = pm.request_sudo(interactive=True)
assert success
assert "access granted" in message
assert pm.has_sudo
assert pm._sudo_validated
assert mock_run.call_count == 2
mock_run.assert_called_once_with(["sudo", "-v"])
@patch("subprocess.run")
def test_request_sudo_no_password(self, mock_run):
"""Test requesting sudo when no password is provided."""
# sudo -n true fails (password needed)
def test_request_sudo_requires_interaction_when_cache_unavailable(self, mock_run):
"""Test an uncached authorization does not start PAM from the TUI."""
mock_run.return_value = Mock(returncode=1)
pm = PermissionManager()
success, message = pm.request_sudo()
assert not success
assert "Password required" in message
assert message == "Interactive authorization required"
assert not pm.has_sudo
assert not pm._sudo_validated
mock_run.assert_called_once_with(
["sudo", "-n", "-v"], capture_output=True, text=True, timeout=5
)
@patch("subprocess.run")
def test_request_sudo_interactively_denied(self, mock_run):
"""Test a rejected PAM conversation keeps sudo unavailable."""
mock_run.return_value = Mock(returncode=1)
pm = PermissionManager()
success, message = pm.request_sudo(interactive=True)
assert not success
assert message == "Authorization was not granted"
assert not pm.has_sudo
assert not pm._sudo_validated
@patch("subprocess.run")
def test_request_sudo_denied(self, mock_run):
"""Test requesting sudo when access is denied."""
# Both calls fail
mock_run.side_effect = [
Mock(returncode=1), # sudo -n true fails
Mock(returncode=1, stderr="access denied"), # sudo -S -v fails
]
def test_request_sudo_interactively_cancelled(self, mock_run):
"""Test cancellation during PAM authorization keeps sudo unavailable."""
mock_run.return_value = Mock(returncode=130)
pm = PermissionManager()
success, message = pm.request_sudo("testpassword")
success, message = pm.request_sudo(interactive=True)
assert not success
assert "denied" in message
assert message == "Authorization was not granted"
assert not pm.has_sudo
assert not pm._sudo_validated
@ -115,6 +120,18 @@ class TestPermissionManager:
assert "Test error" in message
assert not pm.has_sudo
@patch("subprocess.run")
def test_request_sudo_reports_when_sudo_is_unavailable(self, mock_run):
"""Test a missing sudo executable produces an actionable result."""
mock_run.side_effect = FileNotFoundError()
pm = PermissionManager()
success, message = pm.request_sudo()
assert not success
assert message == "sudo is unavailable; remaining in Read-only Mode"
assert not pm.has_sudo
@patch("subprocess.run")
def test_validate_permissions_success(self, mock_run):
"""Test validating permissions successfully."""