test_debug_auth.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import tests # Initialize temporary logging before importing the application.
  2. import unittest
  3. from html.parser import HTMLParser
  4. from unittest.mock import patch
  5. from fastapi.testclient import TestClient
  6. from app.config import Settings, settings
  7. from app.main import app
  8. from app.oauth_token import InvalidSessionToken, create_access_token, decode_access_token
  9. from app.permissions import Entitlement
  10. class LoginFormParser(HTMLParser):
  11. def handle_starttag(self, tag, attrs):
  12. attributes = dict(attrs)
  13. if tag == "input" and attributes.get("name") == "_csrf":
  14. self.csrf = attributes["value"]
  15. def form_csrf(response):
  16. parser = LoginFormParser()
  17. parser.feed(response.text)
  18. return parser.csrf
  19. class DebugLoginTests(unittest.TestCase):
  20. def setUp(self):
  21. self.config = patch.multiple(settings, ENVIRONMENT="development", DEBUG_AUTH=True, COOKIE_SECURE=False)
  22. self.config.start()
  23. self.addCleanup(self.config.stop)
  24. self.ldap = patch("app.routes.ldap_authenticate")
  25. self.ldap_mock = self.ldap.start()
  26. self.addCleanup(self.ldap.stop)
  27. self.client = TestClient(app, base_url="http://testserver")
  28. self.addCleanup(self.client.close)
  29. self.entitlements = (
  30. Entitlement("1", "10", None, "Buchhaltung", 400),
  31. Entitlement("1", "20", "Service", "Mitarbeiter", 100),
  32. )
  33. def login(self, username="winter", password="winter", csrf=True):
  34. self.client.get("/login")
  35. return self.client.post(
  36. "/login",
  37. data={
  38. "username": username,
  39. "password": password,
  40. "_csrf": self.client.cookies[settings.LOGIN_CSRF_COOKIE_NAME] if csrf else "incorrect",
  41. },
  42. follow_redirects=False,
  43. )
  44. @patch("app.routes.load_entitlements")
  45. @patch("app.auth.load_entitlements")
  46. def test_existing_user_gets_jwt_and_can_access_protected_page(self, request_rights, login_rights):
  47. login_rights.return_value = request_rights.return_value = self.entitlements
  48. response = self.login("Winter", "Winter")
  49. self.assertEqual(response.status_code, 303)
  50. payload = decode_access_token(response.cookies[settings.SESSION_COOKIE_NAME])
  51. self.assertEqual(payload["sub"], "winter")
  52. self.assertEqual(payload["role"], "Buchhaltung")
  53. self.assertEqual(payload["auth_source"], "debug")
  54. login_rights.assert_called_once()
  55. self.assertEqual(login_rights.call_args.args[1], "winter")
  56. self.assertEqual(self.client.get("/select").status_code, 200)
  57. request_rights.assert_called_once()
  58. self.ldap_mock.assert_not_called()
  59. @patch("app.routes.load_entitlements")
  60. def test_different_password_is_rejected_without_ldap_or_rights_lookup(self, login_rights):
  61. response = self.login("winter", "Winter")
  62. self.assertEqual(response.status_code, 401)
  63. self.assertNotIn(settings.SESSION_COOKIE_NAME, response.cookies)
  64. login_rights.assert_not_called()
  65. self.ldap_mock.assert_not_called()
  66. @patch("app.routes.load_entitlements", return_value=())
  67. def test_user_without_rights_is_rejected(self, login_rights):
  68. response = self.login("unknown", "unknown")
  69. self.assertEqual(response.status_code, 403)
  70. self.assertNotIn(settings.SESSION_COOKIE_NAME, response.cookies)
  71. self.ldap_mock.assert_not_called()
  72. def test_csrf_remains_required(self):
  73. self.assertEqual(self.login(csrf=False).status_code, 403)
  74. @patch("app.routes.load_entitlements")
  75. def test_opening_another_login_page_keeps_first_form_valid(self, login_rights):
  76. login_rights.return_value = self.entitlements
  77. first_page = self.client.get("/login")
  78. second_page = self.client.get("/login?next=/select")
  79. self.assertEqual(form_csrf(first_page), form_csrf(second_page))
  80. self.assertEqual(second_page.headers["cache-control"], "no-store")
  81. response = self.client.post(
  82. "/login",
  83. data={"username": "winter", "password": "winter", "_csrf": form_csrf(first_page)},
  84. follow_redirects=False,
  85. )
  86. self.assertEqual(response.status_code, 303)
  87. @patch("app.routes.load_entitlements")
  88. def test_missing_cookie_returns_fresh_form_and_allows_retry(self, login_rights):
  89. login_rights.return_value = self.entitlements
  90. page = self.client.get("/login")
  91. self.client.cookies.clear()
  92. rejected = self.client.post(
  93. "/login",
  94. data={"username": "winter", "password": "winter", "_csrf": form_csrf(page), "next": "/select"},
  95. )
  96. self.assertEqual(rejected.status_code, 403)
  97. self.assertIn("text/html", rejected.headers["content-type"])
  98. self.assertEqual(rejected.headers["cache-control"], "no-store")
  99. self.assertNotEqual(form_csrf(rejected), form_csrf(page))
  100. login_rights.assert_not_called()
  101. self.ldap_mock.assert_not_called()
  102. accepted = self.client.post(
  103. "/login",
  104. data={"username": "winter", "password": "winter", "_csrf": form_csrf(rejected), "next": "/select"},
  105. follow_redirects=False,
  106. )
  107. self.assertEqual(accepted.status_code, 303)
  108. self.assertEqual(accepted.headers["location"], "/select")
  109. @patch("app.routes.load_entitlements")
  110. def test_missing_form_token_is_rejected_before_authentication(self, login_rights):
  111. self.client.get("/login")
  112. response = self.client.post("/login", data={"username": "winter", "password": "winter"})
  113. self.assertEqual(response.status_code, 403)
  114. self.assertEqual(form_csrf(response), self.client.cookies[settings.LOGIN_CSRF_COOKIE_NAME])
  115. login_rights.assert_not_called()
  116. self.ldap_mock.assert_not_called()
  117. @patch("app.auth.lookup_directory_user")
  118. def test_proxy_header_does_not_bypass_debug_form(self, lookup):
  119. with patch.object(settings, "TRUSTED_PROXY_IPS", ["testclient"]):
  120. response = self.client.get("/login", headers={"X-Remote-User": r"DOMAIN\winter"})
  121. self.assertEqual(response.status_code, 200)
  122. lookup.assert_not_called()
  123. def test_debug_token_is_rejected_after_disabling_debug_mode(self):
  124. token = create_access_token(
  125. username="winter", name="winter", email="", role="Mitarbeiter", auth_source="debug"
  126. )
  127. with patch.object(settings, "DEBUG_AUTH", False):
  128. with self.assertRaises(InvalidSessionToken):
  129. decode_access_token(token)
  130. def test_production_configuration_rejects_debug_auth(self):
  131. with self.assertRaises(ValueError):
  132. Settings(_env_file=None, ENVIRONMENT="production", DEBUG_AUTH=True)
  133. def test_debug_auth_is_disabled_by_default(self):
  134. self.assertFalse(Settings(_env_file=None, DEBUG_AUTH=False).debug_auth_enabled)
  135. if __name__ == "__main__":
  136. unittest.main()