From 6554dc625d015400d9d1a7b17aaa075b7531650b Mon Sep 17 00:00:00 2001 From: nevaforget Date: Thu, 26 Mar 2026 11:48:23 +0100 Subject: [PATCH] feat: Faillock-Warnung bei wiederholten Fehlversuchen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zeigt nach dem 2. fehlgeschlagenen Login-Versuch einen Hinweis an, dass das Konto nach dem nächsten Fehlversuch gesperrt werden kann (faillock default: 3 Versuche). --- src/moongreet/greeter.py | 17 +++++++++++++++++ tests/test_integration.py | 26 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/moongreet/greeter.py b/src/moongreet/greeter.py index 595e1ec..e976198 100644 --- a/src/moongreet/greeter.py +++ b/src/moongreet/greeter.py @@ -21,11 +21,22 @@ from moongreet.sessions import Session, get_sessions from moongreet.power import reboot, shutdown LAST_USER_PATH = Path("/var/cache/moongreet/last-user") +FAILLOCK_MAX_ATTEMPTS = 3 PACKAGE_DATA = files("moongreet") / "data" DEFAULT_AVATAR_PATH = PACKAGE_DATA / "default-avatar.svg" AVATAR_SIZE = 128 +def faillock_warning(attempt_count: int) -> str | None: + """Return a warning if the user is approaching or has reached the faillock limit.""" + remaining = FAILLOCK_MAX_ATTEMPTS - attempt_count + if remaining <= 0: + return "Konto ist möglicherweise gesperrt" + if remaining == 1: + return f"Noch {remaining} Versuch vor Kontosperrung!" + return None + + class GreeterWindow(Gtk.ApplicationWindow): """The main greeter window with login UI.""" @@ -41,6 +52,7 @@ class GreeterWindow(Gtk.ApplicationWindow): self._greetd_sock: socket.socket | None = None self._default_avatar_pixbuf: GdkPixbuf.Pixbuf | None = None self._avatar_cache: dict[str, GdkPixbuf.Pixbuf] = {} + self._failed_attempts: dict[str, int] = {} self._build_ui() self._select_initial_user() @@ -388,7 +400,12 @@ class GreeterWindow(Gtk.ApplicationWindow): response = post_auth_response(sock, password) if response.get("type") == "error": + self._failed_attempts[user.username] = self._failed_attempts.get(user.username, 0) + 1 self._show_greetd_error(response, "Falsches Passwort") + warning = faillock_warning(self._failed_attempts[user.username]) + if warning: + current = self._error_label.get_text() + self._error_label.set_text(f"{current}\n{warning}") self._close_greetd_sock() return diff --git a/tests/test_integration.py b/tests/test_integration.py index d988d6a..7ebcf6f 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -10,6 +10,7 @@ from pathlib import Path import pytest +from moongreet.greeter import faillock_warning, FAILLOCK_MAX_ATTEMPTS from moongreet.ipc import create_session, post_auth_response, start_session, cancel_session @@ -177,6 +178,31 @@ class TestLoginFlow: assert mock.received[1] == {"type": "cancel_session"} +class TestFaillockWarning: + """Tests for the faillock warning message logic.""" + + def test_no_warning_on_first_attempt(self) -> None: + assert faillock_warning(1) is None + + def test_warning_on_second_attempt(self) -> None: + warning = faillock_warning(2) + assert warning is not None + assert "1" in warning # 1 Versuch übrig + + def test_warning_on_third_attempt(self) -> None: + warning = faillock_warning(3) + assert warning is not None + assert "gesperrt" in warning.lower() + + def test_warning_beyond_max_attempts(self) -> None: + warning = faillock_warning(4) + assert warning is not None + assert "gesperrt" in warning.lower() + + def test_max_attempts_constant_is_three(self) -> None: + assert FAILLOCK_MAX_ATTEMPTS == 3 + + class TestLastUser: """Tests for saving and loading the last logged-in user."""