| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- import tests # Initialize temporary logging before importing the application.
- import unittest
- from html.parser import HTMLParser
- from unittest.mock import patch
- from fastapi.testclient import TestClient
- from app.config import Settings, settings
- from app.main import app
- from app.oauth_token import InvalidSessionToken, create_access_token, decode_access_token
- from app.permissions import Entitlement
- class LoginFormParser(HTMLParser):
- def handle_starttag(self, tag, attrs):
- attributes = dict(attrs)
- if tag == "input" and attributes.get("name") == "_csrf":
- self.csrf = attributes["value"]
- def form_csrf(response):
- parser = LoginFormParser()
- parser.feed(response.text)
- return parser.csrf
- class DebugLoginTests(unittest.TestCase):
- def setUp(self):
- self.config = patch.multiple(settings, ENVIRONMENT="development", DEBUG_AUTH=True, COOKIE_SECURE=False)
- self.config.start()
- self.addCleanup(self.config.stop)
- self.ldap = patch("app.routes.ldap_authenticate")
- self.ldap_mock = self.ldap.start()
- self.addCleanup(self.ldap.stop)
- self.client = TestClient(app, base_url="http://testserver")
- self.addCleanup(self.client.close)
- self.entitlements = (
- Entitlement("1", "10", None, "Buchhaltung", 400),
- Entitlement("1", "20", "Service", "Mitarbeiter", 100),
- )
- def login(self, username="winter", password="winter", csrf=True):
- self.client.get("/login")
- return self.client.post(
- "/login",
- data={
- "username": username,
- "password": password,
- "_csrf": self.client.cookies[settings.LOGIN_CSRF_COOKIE_NAME] if csrf else "incorrect",
- },
- follow_redirects=False,
- )
- @patch("app.routes.load_entitlements")
- @patch("app.auth.load_entitlements")
- def test_existing_user_gets_jwt_and_can_access_protected_page(self, request_rights, login_rights):
- login_rights.return_value = request_rights.return_value = self.entitlements
- response = self.login("Winter", "Winter")
- self.assertEqual(response.status_code, 303)
- payload = decode_access_token(response.cookies[settings.SESSION_COOKIE_NAME])
- self.assertEqual(payload["sub"], "winter")
- self.assertEqual(payload["role"], "Buchhaltung")
- self.assertEqual(payload["auth_source"], "debug")
- login_rights.assert_called_once()
- self.assertEqual(login_rights.call_args.args[1], "winter")
- self.assertEqual(self.client.get("/select").status_code, 200)
- request_rights.assert_called_once()
- self.ldap_mock.assert_not_called()
- @patch("app.routes.load_entitlements")
- def test_different_password_is_rejected_without_ldap_or_rights_lookup(self, login_rights):
- response = self.login("winter", "Winter")
- self.assertEqual(response.status_code, 401)
- self.assertNotIn(settings.SESSION_COOKIE_NAME, response.cookies)
- login_rights.assert_not_called()
- self.ldap_mock.assert_not_called()
- @patch("app.routes.load_entitlements", return_value=())
- def test_user_without_rights_is_rejected(self, login_rights):
- response = self.login("unknown", "unknown")
- self.assertEqual(response.status_code, 403)
- self.assertNotIn(settings.SESSION_COOKIE_NAME, response.cookies)
- self.ldap_mock.assert_not_called()
- def test_csrf_remains_required(self):
- self.assertEqual(self.login(csrf=False).status_code, 403)
- @patch("app.routes.load_entitlements")
- def test_opening_another_login_page_keeps_first_form_valid(self, login_rights):
- login_rights.return_value = self.entitlements
- first_page = self.client.get("/login")
- second_page = self.client.get("/login?next=/select")
- self.assertEqual(form_csrf(first_page), form_csrf(second_page))
- self.assertEqual(second_page.headers["cache-control"], "no-store")
- response = self.client.post(
- "/login",
- data={"username": "winter", "password": "winter", "_csrf": form_csrf(first_page)},
- follow_redirects=False,
- )
- self.assertEqual(response.status_code, 303)
- @patch("app.routes.load_entitlements")
- def test_missing_cookie_returns_fresh_form_and_allows_retry(self, login_rights):
- login_rights.return_value = self.entitlements
- page = self.client.get("/login")
- self.client.cookies.clear()
- rejected = self.client.post(
- "/login",
- data={"username": "winter", "password": "winter", "_csrf": form_csrf(page), "next": "/select"},
- )
- self.assertEqual(rejected.status_code, 403)
- self.assertIn("text/html", rejected.headers["content-type"])
- self.assertEqual(rejected.headers["cache-control"], "no-store")
- self.assertNotEqual(form_csrf(rejected), form_csrf(page))
- login_rights.assert_not_called()
- self.ldap_mock.assert_not_called()
- accepted = self.client.post(
- "/login",
- data={"username": "winter", "password": "winter", "_csrf": form_csrf(rejected), "next": "/select"},
- follow_redirects=False,
- )
- self.assertEqual(accepted.status_code, 303)
- self.assertEqual(accepted.headers["location"], "/select")
- @patch("app.routes.load_entitlements")
- def test_missing_form_token_is_rejected_before_authentication(self, login_rights):
- self.client.get("/login")
- response = self.client.post("/login", data={"username": "winter", "password": "winter"})
- self.assertEqual(response.status_code, 403)
- self.assertEqual(form_csrf(response), self.client.cookies[settings.LOGIN_CSRF_COOKIE_NAME])
- login_rights.assert_not_called()
- self.ldap_mock.assert_not_called()
- @patch("app.auth.lookup_directory_user")
- def test_proxy_header_does_not_bypass_debug_form(self, lookup):
- with patch.object(settings, "TRUSTED_PROXY_IPS", ["testclient"]):
- response = self.client.get("/login", headers={"X-Remote-User": r"DOMAIN\winter"})
- self.assertEqual(response.status_code, 200)
- lookup.assert_not_called()
- def test_debug_token_is_rejected_after_disabling_debug_mode(self):
- token = create_access_token(
- username="winter", name="winter", email="", role="Mitarbeiter", auth_source="debug"
- )
- with patch.object(settings, "DEBUG_AUTH", False):
- with self.assertRaises(InvalidSessionToken):
- decode_access_token(token)
- def test_production_configuration_rejects_debug_auth(self):
- with self.assertRaises(ValueError):
- Settings(_env_file=None, ENVIRONMENT="production", DEBUG_AUTH=True)
- def test_debug_auth_is_disabled_by_default(self):
- self.assertFalse(Settings(_env_file=None, DEBUG_AUTH=False).debug_auth_enabled)
- if __name__ == "__main__":
- unittest.main()
|