test_endpoint_auth.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. """Integration tests for API endpoint authentication.
  2. Tests that verify endpoints properly enforce authentication when auth is enabled,
  3. and allow access when auth is disabled (CVE-2026-25505 fix verification).
  4. """
  5. from unittest.mock import AsyncMock, patch
  6. import pytest
  7. from httpx import AsyncClient
  8. class TestEndpointAuthenticationEnforcement:
  9. """Tests that endpoints enforce authentication when auth is enabled."""
  10. @pytest.fixture
  11. async def user_factory(self, db_session):
  12. """Factory to create test users."""
  13. async def _create_user(**kwargs):
  14. from passlib.hash import bcrypt
  15. from backend.app.models.user import User
  16. defaults = {
  17. "username": "testuser",
  18. "password_hash": bcrypt.hash("testpass123"),
  19. "is_admin": False,
  20. }
  21. defaults.update(kwargs)
  22. user = User(**defaults)
  23. db_session.add(user)
  24. await db_session.commit()
  25. await db_session.refresh(user)
  26. return user
  27. return _create_user
  28. @pytest.fixture
  29. async def admin_user(self, user_factory, db_session):
  30. """Create an admin user for testing."""
  31. from sqlalchemy import select
  32. from backend.app.models.group import Group
  33. # Get or create admin group
  34. result = await db_session.execute(select(Group).where(Group.name == "Administrators"))
  35. admin_group = result.scalar_one_or_none()
  36. user = await user_factory(username="admin", is_admin=True)
  37. if admin_group:
  38. user.groups.append(admin_group)
  39. await db_session.commit()
  40. return user
  41. @pytest.fixture
  42. async def auth_token(self, admin_user, async_client: AsyncClient):
  43. """Get a valid auth token for the admin user."""
  44. response = await async_client.post(
  45. "/api/v1/auth/login",
  46. json={"username": "admin", "password": "testpass123"},
  47. )
  48. if response.status_code == 200:
  49. return response.json().get("access_token")
  50. return None
  51. @pytest.mark.asyncio
  52. @pytest.mark.integration
  53. async def test_filaments_list_accessible_without_auth_when_disabled(self, async_client: AsyncClient):
  54. """Verify filaments list is accessible when auth is disabled."""
  55. with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
  56. response = await async_client.get("/api/v1/filaments/")
  57. assert response.status_code == 200
  58. @pytest.mark.asyncio
  59. @pytest.mark.integration
  60. async def test_external_links_list_accessible_without_auth_when_disabled(self, async_client: AsyncClient):
  61. """Verify external links list is accessible when auth is disabled."""
  62. with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
  63. response = await async_client.get("/api/v1/external-links/")
  64. assert response.status_code == 200
  65. @pytest.mark.asyncio
  66. @pytest.mark.integration
  67. async def test_notifications_list_accessible_without_auth_when_disabled(self, async_client: AsyncClient):
  68. """Verify notifications list is accessible when auth is disabled."""
  69. with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
  70. response = await async_client.get("/api/v1/notifications/")
  71. assert response.status_code == 200
  72. @pytest.mark.asyncio
  73. @pytest.mark.integration
  74. async def test_maintenance_types_accessible_without_auth_when_disabled(self, async_client: AsyncClient):
  75. """Verify maintenance types is accessible when auth is disabled."""
  76. with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
  77. response = await async_client.get("/api/v1/maintenance/types")
  78. assert response.status_code == 200
  79. @pytest.mark.asyncio
  80. @pytest.mark.integration
  81. async def test_system_info_accessible_without_auth_when_disabled(self, async_client: AsyncClient):
  82. """Verify system info is accessible when auth is disabled."""
  83. with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
  84. response = await async_client.get("/api/v1/system/info")
  85. assert response.status_code == 200
  86. class TestImageEndpointsPublicAccess:
  87. """Tests that image endpoints remain accessible without auth.
  88. These endpoints serve images via <img> tags which cannot send Authorization headers.
  89. """
  90. @pytest.fixture
  91. async def link_with_icon(self, db_session):
  92. """Create an external link with a custom icon for testing."""
  93. from backend.app.models.external_link import ExternalLink
  94. link = ExternalLink(
  95. name="Test Link",
  96. url="https://example.com",
  97. icon="Link",
  98. sort_order=0,
  99. custom_icon=None, # No custom icon set
  100. )
  101. db_session.add(link)
  102. await db_session.commit()
  103. await db_session.refresh(link)
  104. return link
  105. @pytest.mark.asyncio
  106. @pytest.mark.integration
  107. async def test_external_link_icon_returns_404_when_no_icon(self, async_client: AsyncClient, link_with_icon):
  108. """Verify icon endpoint returns 404 (not 401) when no icon is set.
  109. This confirms the endpoint doesn't require auth - a 401 would indicate
  110. auth is being enforced, but 404 means the endpoint is accessible but
  111. no icon exists.
  112. """
  113. response = await async_client.get(f"/api/v1/external-links/{link_with_icon.id}/icon")
  114. # Should be 404 (no icon set), not 401 (unauthorized)
  115. assert response.status_code == 404
  116. assert "No custom icon set" in response.json().get("detail", "")
  117. class TestAuthenticationPatterns:
  118. """Tests for authentication helper functions and patterns."""
  119. @pytest.mark.asyncio
  120. @pytest.mark.integration
  121. async def test_require_permission_if_auth_enabled_allows_access_when_disabled(self, async_client: AsyncClient):
  122. """Verify require_permission_if_auth_enabled allows access when auth disabled."""
  123. with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
  124. # Test a protected endpoint
  125. response = await async_client.get("/api/v1/filaments/")
  126. assert response.status_code == 200
  127. @pytest.mark.asyncio
  128. @pytest.mark.integration
  129. async def test_multiple_endpoints_accessible_when_auth_disabled(self, async_client: AsyncClient):
  130. """Verify multiple protected endpoints are accessible when auth is disabled."""
  131. with patch("backend.app.core.auth.is_auth_enabled", return_value=False):
  132. endpoints = [
  133. "/api/v1/filaments/",
  134. "/api/v1/external-links/",
  135. "/api/v1/notifications/",
  136. "/api/v1/maintenance/types",
  137. ]
  138. for endpoint in endpoints:
  139. response = await async_client.get(endpoint)
  140. assert response.status_code == 200, f"Endpoint {endpoint} should be accessible"