From 700cfc754ee3191615f87ce04e0e65bdeece6411 Mon Sep 17 00:00:00 2001 From: Lukas Juhrich Date: Wed, 4 Sep 2024 13:48:24 +0200 Subject: [PATCH] Add function checking for login availability --- pycroft/lib/user.py | 29 +++++++++++++++++++---- tests/lib/user/test_login_availability.py | 25 +++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 tests/lib/user/test_login_availability.py diff --git a/pycroft/lib/user.py b/pycroft/lib/user.py index cf4ecd27f..702aa9fce 100644 --- a/pycroft/lib/user.py +++ b/pycroft/lib/user.py @@ -26,7 +26,7 @@ from pycroft.helpers.i18n import deferred_gettext from pycroft.helpers.interval import closed, Interval, starting_from from pycroft.helpers.printing import generate_user_sheet as generate_pdf -from pycroft.helpers.user import generate_random_str +from pycroft.helpers.user import generate_random_str, login_hash from pycroft.helpers.utc import DateTimeTz from pycroft.lib.address import get_or_create_address from pycroft.lib.exc import PycroftLibException @@ -62,7 +62,7 @@ PropertyGroup, Membership, ) -from pycroft.model.unix_account import UnixAccount +from pycroft.model.unix_account import UnixAccount, UnixTombstone from pycroft.model.webstorage import WebStorage from pycroft.task import send_mails_async @@ -274,9 +274,15 @@ def create_user( :param passwd_hash: Use password hash instead of generating a new password :param send_confirm_mail: If a confirmation mail should be send to the user :return: + + :raises LoginTakenException: if the login is used or has been used in the past """ now = session.utcnow() + + if not login_available(login, session.session): + raise LoginTakenException(login) + plain_password: str | None = user_helper.generate_password(12) # create a new user new_user = User( @@ -321,6 +327,20 @@ def create_user( return new_user, plain_password +def login_available(login: str, session: Session) -> bool: + """Check whether there is a tombstone with the hash of the given login""" + hash = login_hash(login) + stmt = select( + ~func.exists( + select() + .select_from(UnixTombstone) + .filter(UnixTombstone.login_hash == hash) + .add_columns(1) + ) + ) + return session.scalar(stmt) + + @with_transaction def move_in( user: User, @@ -1139,8 +1159,9 @@ def send_confirmation_email(user: BaseUser) -> None: class LoginTakenException(PycroftLibException): - def __init__(self) -> None: - super().__init__("Login already taken") + def __init__(self, login: str | None = None) -> None: + msg = "Login already taken" if not login else f"Login {login!r} already taken" + super().__init__(msg) class EmailTakenException(PycroftLibException): diff --git a/tests/lib/user/test_login_availability.py b/tests/lib/user/test_login_availability.py new file mode 100644 index 000000000..5c1944122 --- /dev/null +++ b/tests/lib/user/test_login_availability.py @@ -0,0 +1,25 @@ +import pytest + +from pycroft.helpers.user import login_hash +from pycroft.lib.user import login_available +from pycroft.model.unix_account import UnixTombstone + + +def test_login_reuse_present(session, processor): + assert not login_available(processor.login, session) + + +LOGIN = "taken" + + +def test_login_reuse_past(session, tombstone): + assert not login_available(LOGIN, session) + assert login_available(f"not-{LOGIN}", session) + + +@pytest.fixture(scope="module") +def tombstone(module_session) -> UnixTombstone: + ts = UnixTombstone(login_hash=login_hash(LOGIN)) + with module_session.begin_nested(): + module_session.add(ts) + return ts