mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-11-10 12:38:32 +00:00
GET /api/debug returns a bundle with application logs, process listings and environment names, and GET /api/restart exits the process. Both are registered only when DEBUG is on, but any logged-in user could call them. Add an admin_required decorator built on login_required and use it for both routes. Non-admin sessions get 403 "Admin access required"; authentication mode "none" keeps working as before.
124 lines
4.2 KiB
Python
124 lines
4.2 KiB
Python
"""Access control tests for the DEBUG-only /api/debug and /api/restart routes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
from collections.abc import Iterator
|
|
from types import ModuleType
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from shelfmark.core.config import config as app_config
|
|
|
|
_DEBUG_OVERRIDES: dict[str, object] = {"DEBUG": True, "USING_EXTERNAL_BYPASSER": True}
|
|
_ORIGINAL_CONFIG_GET = app_config.get
|
|
|
|
|
|
def _config_get_with_debug(key: str, default: object = None, user_id: int | None = None) -> object:
|
|
if key in _DEBUG_OVERRIDES:
|
|
return _DEBUG_OVERRIDES[key]
|
|
return _ORIGINAL_CONFIG_GET(key, default, user_id=user_id)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def main_module() -> Iterator[ModuleType]:
|
|
"""Import `shelfmark.main` with DEBUG on so the debug routes get registered."""
|
|
with (
|
|
patch("shelfmark.download.orchestrator.start"),
|
|
patch.object(app_config, "get", side_effect=_config_get_with_debug),
|
|
):
|
|
import shelfmark.main as main
|
|
|
|
importlib.reload(main)
|
|
yield main
|
|
# Put the module back the way other test modules expect it (no debug routes).
|
|
with patch("shelfmark.download.orchestrator.start"):
|
|
importlib.reload(main)
|
|
|
|
|
|
@pytest.fixture
|
|
def client(main_module):
|
|
return main_module.app.test_client()
|
|
|
|
|
|
def _set_session(client, *, user_id: str, is_admin: bool) -> None:
|
|
with client.session_transaction() as sess:
|
|
sess["user_id"] = user_id
|
|
sess["is_admin"] = is_admin
|
|
|
|
|
|
@pytest.fixture
|
|
def no_side_effects(main_module):
|
|
"""Never run the real debug script or exit the process from a test."""
|
|
with (
|
|
patch.object(main_module.subprocess, "run") as run_mock,
|
|
patch.object(main_module.os, "_exit") as exit_mock,
|
|
):
|
|
yield run_mock, exit_mock
|
|
|
|
|
|
def test_debug_routes_are_registered_when_debug_is_on(main_module):
|
|
rules = {rule.rule for rule in main_module.app.url_map.iter_rules()}
|
|
assert "/api/debug" in rules
|
|
assert "/api/restart" in rules
|
|
|
|
|
|
@pytest.mark.parametrize("path", ["/api/debug", "/api/restart"])
|
|
def test_debug_routes_reject_anonymous_users(client, main_module, no_side_effects, path):
|
|
run_mock, exit_mock = no_side_effects
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
response = client.get(path)
|
|
|
|
assert response.status_code == 401
|
|
run_mock.assert_not_called()
|
|
exit_mock.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize("path", ["/api/debug", "/api/restart"])
|
|
def test_debug_routes_reject_non_admin_users(client, main_module, no_side_effects, path):
|
|
run_mock, exit_mock = no_side_effects
|
|
_set_session(client, user_id="regular-user", is_admin=False)
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
response = client.get(path)
|
|
|
|
assert response.status_code == 403
|
|
assert response.get_json() == {"error": "Admin access required"}
|
|
run_mock.assert_not_called()
|
|
exit_mock.assert_not_called()
|
|
|
|
|
|
def test_restart_allows_admin_users(client, main_module, no_side_effects):
|
|
_, exit_mock = no_side_effects
|
|
_set_session(client, user_id="admin-user", is_admin=True)
|
|
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
|
|
client.get("/api/restart")
|
|
|
|
exit_mock.assert_called_once_with(0)
|
|
|
|
|
|
def test_restart_allows_access_when_auth_is_disabled(client, main_module, no_side_effects):
|
|
_, exit_mock = no_side_effects
|
|
with patch.object(main_module, "get_auth_mode", return_value="none"):
|
|
client.get("/api/restart")
|
|
|
|
exit_mock.assert_called_once_with(0)
|
|
|
|
|
|
def test_debug_allows_admin_users(client, main_module, no_side_effects, tmp_path):
|
|
run_mock, _ = no_side_effects
|
|
bundle = tmp_path / "shelfmark-debug.zip"
|
|
bundle.write_bytes(b"PK\x05\x06" + b"\x00" * 18)
|
|
run_mock.return_value.returncode = 0
|
|
run_mock.return_value.stdout = f"{bundle}\n"
|
|
_set_session(client, user_id="admin-user", is_admin=True)
|
|
with (
|
|
patch.object(main_module, "get_auth_mode", return_value="builtin"),
|
|
patch.object(main_module.time, "sleep"),
|
|
):
|
|
response = client.get("/api/debug")
|
|
|
|
assert response.status_code == 200
|
|
assert response.mimetype == "application/zip"
|
|
run_mock.assert_called_once()
|