From dd406d1dfca3b8dbd560b4ea722d474447bd10b6 Mon Sep 17 00:00:00 2001 From: tkeriven Date: Tue, 15 Sep 2026 17:22:17 +0200 Subject: [PATCH] OP#2378 : add error if already logged --- app/__init__.py | 2 +- app/api/v1/auth/AuthUserApi.py | 2 +- app/interface/auth/InterfaceAuthUser.py | 5 +- app/utils/errors.py | 1 + .../test_auth/test_InterfaceAuthUser.py | 98 ++++++++++++++++--- 5 files changed, 93 insertions(+), 15 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index f6bd227d..b686ca14 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -254,7 +254,7 @@ def get_config_and_user() -> ResponseReturnValue | None: g.user_domain_settings = default_domain_settings else: g.user_domain_settings = init_get_user_domain_settings(user) - inter = InterfaceAuthUser(process_config, system_settings, g.user_domain_settings) + inter = InterfaceAuthUser(process_config, system_settings, g.user_domain_settings, user=user) creds_ok, new_user = inter.check_user_and_fill_info(user) if not creds_ok: return create_api_base_response(error=err.ERROR_USER_CREDS_NOT_VALID) diff --git a/app/api/v1/auth/AuthUserApi.py b/app/api/v1/auth/AuthUserApi.py index 90c69b8a..b42857b8 100644 --- a/app/api/v1/auth/AuthUserApi.py +++ b/app/api/v1/auth/AuthUserApi.py @@ -28,7 +28,7 @@ def init_admin_config() -> None: process: ProcessSetting = g.process_settings system_settings: dict = g.system_settings default_domain: dict = g.default_domain_settings - interface_api = InterfaceAuthUser(process, system_settings, default_domain) + interface_api = InterfaceAuthUser(process, system_settings, default_domain, user=g.user) g.inter = interface_api diff --git a/app/interface/auth/InterfaceAuthUser.py b/app/interface/auth/InterfaceAuthUser.py index d32744a8..9e4da377 100644 --- a/app/interface/auth/InterfaceAuthUser.py +++ b/app/interface/auth/InterfaceAuthUser.py @@ -28,7 +28,7 @@ class InterfaceAuthUser: Interface for user authentication """ - def __init__(self, process: ProcessSetting, system: dict, default_domain: dict): + def __init__(self, process: ProcessSetting, system: dict, default_domain: dict, user: User): system_settings = SystemSettingsObj(system[SystemSettings.subparent]) default_auth = AuthSettingsObj(default_domain[AuthSettings.subparent]) @@ -39,6 +39,7 @@ def __init__(self, process: ProcessSetting, system: dict, default_domain: dict): self.process = process self.default_domain = default_domain + self.user = user self.module_auth = ModuleAuth(process, system_settings, default_auth, default_us_source) self.module_user_profile = ModuleUserProfile(process, default_domain) self._module_calendar: ModuleCalendar = ModuleCalendar(process) @@ -91,6 +92,8 @@ def plain_login(self, data:dict) -> tuple[dict, int]: """ uid = data["username"] password = data["password"] + if self.user.authenticated and self.user.uid == data.get("username"): + return create_api_base_response(None, err.ERROR_ALREADY_LOGGED_IN) success, user, module_us = self._check_login(uid, password) diff --git a/app/utils/errors.py b/app/utils/errors.py index 5ccdaabf..193bd5af 100644 --- a/app/utils/errors.py +++ b/app/utils/errors.py @@ -71,6 +71,7 @@ def __init__(self, c:str, m:str, h:int = HTTPStatus.INTERNAL_SERVER_ERROR): ERROR_VALIDITY_TIME_BELOW_0 = E("S000206", "Voucher Validity Time Below 0", HTTPStatus.INTERNAL_SERVER_ERROR) ERROR_USER_CREDS_NOT_VALID = E("S000207", "User credentials stores in session are not valid anymore", HTTPStatus.UNAUTHORIZED) ERROR_LOGIN_FAILED = E("S000208", "Login Failed: Invalid Credentials", HTTPStatus.UNAUTHORIZED) +ERROR_ALREADY_LOGGED_IN = E("S000209", "User Is Already Logged In", HTTPStatus.CONFLICT) #API diff --git a/tests/test_interface/test_auth/test_InterfaceAuthUser.py b/tests/test_interface/test_auth/test_InterfaceAuthUser.py index 5e9f7d4f..62062bc9 100644 --- a/tests/test_interface/test_auth/test_InterfaceAuthUser.py +++ b/tests/test_interface/test_auth/test_InterfaceAuthUser.py @@ -145,11 +145,15 @@ def create_personal_addressbook(self, user_uid, name="Personal contacts"): class FakeUser: """Fake User object for testing.""" - def __init__(self, uid, password): + def __init__(self, uid, password, authenticated=False): self.uid = uid self.password = password self.mail = uid self.source_id = "source1" + self.authenticated = authenticated + + +ANONYMOUS_USER = FakeUser("anonymous", "anonymous", authenticated=False) class FakeModuleMail: @@ -205,7 +209,8 @@ def test_get_login_mech_success(monkeypatch): interface = InterfaceAuthUser( process={"test": "config"}, system={"SYSTEM_SETTINGS": {"test": "value"}}, - default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}} + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}}, + user=ANONYMOUS_USER ) result, status_code = interface.get_login_mech(user_uid="testuser@example.com", redirect="/dashboard") @@ -228,7 +233,8 @@ def test_get_login_mech_request_exception(monkeypatch): interface = InterfaceAuthUser( process={"test": "config"}, system={"SYSTEM_SETTINGS": {"test": "value"}}, - default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}} + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}}, + user=ANONYMOUS_USER ) result, status_code = interface.get_login_mech(user_uid="unknown@example.com", redirect="/dashboard") @@ -257,7 +263,8 @@ def fake_us_class(sources): interface = InterfaceAuthUser( process={"test": "config"}, system={"SYSTEM_SETTINGS": {"test": "value"}}, - default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}} + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}}, + user=ANONYMOUS_USER ) data = {"username": "testuser@example.com", "password": "secret123"} @@ -287,7 +294,8 @@ def fake_us_class(sources): interface = InterfaceAuthUser( process={"test": "config"}, system={"SYSTEM_SETTINGS": {"test": "value"}}, - default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}} + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}}, + user=ANONYMOUS_USER ) data = {"username": "testuser@example.com", "password": "wrong"} @@ -296,6 +304,65 @@ def fake_us_class(sources): assert status_code == 401 +def test_plain_login_already_logged_in_same_user(monkeypatch): + """Test that a login request from an already authenticated user for the same uid is short-circuited.""" + fake_auth = FakeModuleAuth(None, None, None, None) + fake_profile = FakeModuleUserProfile(None, None) + + fake_us_instance = FakeModuleUserSource({}) + def fake_us_class(sources): + return fake_us_instance + + patch_modules_on_interface(monkeypatch, fake_auth, fake_profile, fake_us_class) + + logged_user = FakeUser("testuser@example.com", "secret123", authenticated=True) + interface = InterfaceAuthUser( + process={"test": "config"}, + system={"SYSTEM_SETTINGS": {"test": "value"}}, + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}}, + user=logged_user + ) + + data = {"username": "testuser@example.com", "password": "secret123"} + result, status_code = interface.plain_login(data) + + assert status_code == err.ERROR_ALREADY_LOGGED_IN.h + assert result["error_code"] == err.ERROR_ALREADY_LOGGED_IN.c + # No new session should have been created + assert fake_auth.get_user_and_domain_user_sources_args is None + + +def test_plain_login_already_logged_in_different_user_proceeds(monkeypatch): + """Test that an authenticated user trying to login as someone else goes through the normal flow.""" + fake_auth = FakeModuleAuth(None, None, None, None) + fake_user = FakeUser("otheruser@example.com", "secret123") + fake_auth.get_user_and_domain_user_sources_result = (fake_user, {"source1": {}}) + + fake_profile = FakeModuleUserProfile(None, None) + fake_profile.is_user_profile_present_result = True # User already exists + + fake_us_instance = FakeModuleUserSource({}) + def fake_us_class(sources): + return fake_us_instance + + patch_modules_on_interface(monkeypatch, fake_auth, fake_profile, fake_us_class) + + logged_user = FakeUser("testuser@example.com", "secret123", authenticated=True) + interface = InterfaceAuthUser( + process={"test": "config"}, + system={"SYSTEM_SETTINGS": {"test": "value"}}, + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}}, + user=logged_user + ) + + data = {"username": "otheruser@example.com", "password": "secret123"} + result, status_code = interface.plain_login(data) + + assert status_code == 200 + assert result["data"]["voucher"] == "test-voucher-123" + assert fake_auth.get_user_and_domain_user_sources_args == ("otheruser@example.com", "secret123") + + def test_plain_login_create_user_profile(monkeypatch): """Test plain login creates user profile and personal calendar on first login.""" fake_auth = FakeModuleAuth(None, None, None, None) @@ -314,7 +381,8 @@ def fake_us_class(sources): interface = InterfaceAuthUser( process={"test": "config"}, system={"SYSTEM_SETTINGS": {"test": "value"}}, - default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}, "MAIL_SETTINGS": {"test": "value"}} + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}, "MAIL_SETTINGS": {"test": "value"}}, + user=ANONYMOUS_USER ) data = {"username": "newuser@example.com", "password": "secret123"} @@ -351,7 +419,8 @@ def fake_us_class(sources): interface = InterfaceAuthUser( process={"test": "config"}, system={"SYSTEM_SETTINGS": {"test": "value"}}, - default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}, "MAIL_SETTINGS": {"test": "value"}} + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}, "MAIL_SETTINGS": {"test": "value"}}, + user=ANONYMOUS_USER ) data = {"username": "newuser@example.com", "password": "secret123"} @@ -383,7 +452,8 @@ def fake_us_class(sources): interface = InterfaceAuthUser( process={"test": "config"}, system={"SYSTEM_SETTINGS": {"test": "value"}}, - default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}, "MAIL_SETTINGS": {"test": "value"}} + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}, "MAIL_SETTINGS": {"test": "value"}}, + user=ANONYMOUS_USER ) data = {"username": "newuser@example.com", "password": "secret123"} @@ -407,7 +477,8 @@ def test_logout_success(monkeypatch): interface = InterfaceAuthUser( process={"test": "config"}, system={"SYSTEM_SETTINGS": {"test": "value"}}, - default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}} + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}}, + user=ANONYMOUS_USER ) result, status_code = interface.logout("fake-jwt-token") @@ -427,7 +498,8 @@ def test_logout_empty_token(monkeypatch): interface = InterfaceAuthUser( process={"test": "config"}, system={"SYSTEM_SETTINGS": {"test": "value"}}, - default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}} + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}}, + user=ANONYMOUS_USER ) result, status_code = interface.logout("") @@ -449,7 +521,8 @@ def test_logout_request_exception_returns_error_response(monkeypatch): interface = InterfaceAuthUser( process={"test": "config"}, system={"SYSTEM_SETTINGS": {"test": "value"}}, - default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}} + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}}, + user=ANONYMOUS_USER ) result, status_code = interface.logout("expired-jwt-token") @@ -471,7 +544,8 @@ def test_logout_invalid_voucher_type_returns_error_response(monkeypatch): interface = InterfaceAuthUser( process={"test": "config"}, system={"SYSTEM_SETTINGS": {"test": "value"}}, - default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}} + default_domain={"AUTH_SETTINGS": {"test": "value"}, "USER_SOURCE": {}}, + user=ANONYMOUS_USER ) result, status_code = interface.logout(12345) # wrong type