From feea051c4f90c26c3d59894ee259e2d75c5eb073 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:10:46 +0000 Subject: [PATCH] feat(status): restrict status page to admin-only and move to admin menu Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- app/views/status.py | 18 +++++- frontend/templates/base.html | 23 +++---- tests/test_views_status.py | 119 ++++++++++++++++++++++++++++++++++- 3 files changed, 141 insertions(+), 19 deletions(-) diff --git a/app/views/status.py b/app/views/status.py index 585a9df4..11fa3b0b 100644 --- a/app/views/status.py +++ b/app/views/status.py @@ -6,7 +6,8 @@ import logging import os from datetime import datetime -from fastapi import Request +from fastapi import Request, status +from fastapi.responses import RedirectResponse from app.utils.config_validator import get_provider_status from app.views.base import APIRouter, require_login, settings, templates @@ -15,12 +16,25 @@ logger = logging.getLogger(__name__) router = APIRouter() +def _require_admin(request: Request): + """Return the session user if they are an admin, else return None.""" + user = request.session.get("user") + if not user or not user.get("is_admin"): + user_email = user.get("email", "anonymous") if user else "anonymous" + logger.warning(f"Non-admin user {user_email} attempted to access /status") + return None + return user + + @router.get("/status") @require_login async def status_dashboard(request: Request): """ - Status dashboard showing all configured integration targets + Status dashboard showing all configured integration targets (admin only). """ + user = _require_admin(request) + if user is None: + return RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) # Get provider status providers = get_provider_status() diff --git a/frontend/templates/base.html b/frontend/templates/base.html index 37a7414b..7b0d9d14 100644 --- a/frontend/templates/base.html +++ b/frontend/templates/base.html @@ -168,18 +168,14 @@ Backup & Restore + + Status + - - - Status - - {% endif %}{# end multi_user_enabled / is_logged_in check #} @@ -314,16 +310,13 @@ Backup & Restore + + Status + - - - Status - - {% endif %}{# end multi_user_enabled / is_logged_in check #} diff --git a/tests/test_views_status.py b/tests/test_views_status.py index be90a83b..353af12b 100644 --- a/tests/test_views_status.py +++ b/tests/test_views_status.py @@ -1,16 +1,74 @@ """Tests for app/views/status.py module.""" -from unittest.mock import Mock, mock_open, patch +from unittest.mock import MagicMock, Mock, mock_open, patch import pytest +@pytest.mark.unit +class TestRequireAdmin: + """Unit tests for the _require_admin helper in views/status.py.""" + + def test_returns_none_when_no_user_in_session(self): + """_require_admin returns None when session has no user.""" + from app.views.status import _require_admin + + mock_request = MagicMock() + mock_request.session = {} + + result = _require_admin(mock_request) + assert result is None + + def test_returns_none_for_non_admin_user(self): + """_require_admin returns None when user is not an admin.""" + from app.views.status import _require_admin + + mock_request = MagicMock() + mock_request.session = {"user": {"email": "user@example.com", "is_admin": False}} + + result = _require_admin(mock_request) + assert result is None + + def test_logs_warning_for_non_admin(self): + """_require_admin logs a warning when a non-admin attempts access.""" + from app.views.status import _require_admin + + mock_request = MagicMock() + mock_request.session = {"user": {"email": "user@example.com", "is_admin": False}} + + with patch("app.views.status.logger") as mock_logger: + _require_admin(mock_request) + mock_logger.warning.assert_called_once() + + def test_logs_warning_when_no_user(self): + """_require_admin logs a warning when there is no user in the session.""" + from app.views.status import _require_admin + + mock_request = MagicMock() + mock_request.session = {} + + with patch("app.views.status.logger") as mock_logger: + _require_admin(mock_request) + mock_logger.warning.assert_called_once() + + def test_returns_user_for_admin(self): + """_require_admin returns the user dict when the user is an admin.""" + from app.views.status import _require_admin + + admin_user = {"email": "admin@example.com", "is_admin": True} + mock_request = MagicMock() + mock_request.session = {"user": admin_user} + + result = _require_admin(mock_request) + assert result == admin_user + + @pytest.mark.integration class TestStatusViews: """Tests for status view routes.""" def test_status_dashboard(self, client): - """Test status dashboard page.""" + """Test status dashboard page returns 200 (auth disabled in tests).""" response = client.get("/status") assert response.status_code == 200 @@ -19,6 +77,63 @@ class TestStatusViews: class TestStatusDashboard: """Tests for status_dashboard function.""" + @pytest.mark.asyncio + async def test_redirects_non_admin_to_home(self): + """status_dashboard redirects to '/' when user is not an admin.""" + from fastapi.responses import RedirectResponse + + from app.views.status import status_dashboard + + mock_request = MagicMock() + mock_request.session = {"user": {"email": "user@example.com", "is_admin": False}} + + result = await status_dashboard(mock_request) + + assert isinstance(result, RedirectResponse) + assert result.status_code == 302 + assert result.headers["location"] == "/" + + @pytest.mark.asyncio + async def test_redirects_when_no_user_in_session(self): + """status_dashboard redirects to '/' when no user is in the session.""" + from fastapi.responses import RedirectResponse + + from app.views.status import status_dashboard + + mock_request = MagicMock() + mock_request.session = {} + + result = await status_dashboard(mock_request) + + assert isinstance(result, RedirectResponse) + assert result.status_code == 302 + + @patch("app.views.status.get_provider_status") + @patch("app.views.status.templates") + @patch("app.views.status.settings") + @patch("app.views.status.os.path.exists") + @pytest.mark.asyncio + async def test_returns_template_for_admin_user(self, mock_exists, mock_settings, mock_templates, mock_providers): + """status_dashboard renders the template for an authenticated admin user.""" + from app.views.status import status_dashboard + + mock_exists.return_value = False + mock_providers.return_value = {} + mock_settings.version = "1.0.0" + mock_settings.build_date = "2024-01-01" + mock_settings.debug = False + mock_settings.git_sha = "abc123" + mock_settings.notification_urls = [] + + mock_request = MagicMock() + mock_request.session = {"user": {"email": "admin@example.com", "is_admin": True}} + + await status_dashboard(mock_request) + + mock_templates.TemplateResponse.assert_called_once() + call_args = mock_templates.TemplateResponse.call_args + assert call_args[0][0] == "status_dashboard.html" + @patch("app.views.status.get_provider_status") @patch("app.views.status.templates") @patch("app.views.status.settings")