diff --git a/api/app/settings/common.py b/api/app/settings/common.py index a69aa22b1049..5bc918b87a34 100644 --- a/api/app/settings/common.py +++ b/api/app/settings/common.py @@ -805,6 +805,7 @@ # FE uri to redirect user to from activation email "ACTIVATION_URL": "activate/{uid}/{token}", # register or activation endpoint will send confirmation email to user + "LOGIN_FIELD": "email", "SEND_CONFIRMATION_EMAIL": False, "SERIALIZERS": { "token": "custom_auth.serializers.CustomTokenSerializer", @@ -1153,8 +1154,9 @@ LDAP_INSTALLED = importlib.util.find_spec("flagsmith_ldap") # The URL of the LDAP server. LDAP_AUTH_URL = env.str("LDAP_AUTH_URL", None) +LDAP_ENABLED = LDAP_INSTALLED and LDAP_AUTH_URL -if LDAP_INSTALLED and LDAP_AUTH_URL: # pragma: no cover +if LDAP_ENABLED: # pragma: no cover AUTHENTICATION_BACKENDS.insert(0, "django_python3_ldap.auth.LDAPBackend") INSTALLED_APPS.append("flagsmith_ldap") @@ -1227,7 +1229,6 @@ # The LDAP user username and password used by `sync_ldap_users_and_groups` command LDAP_SYNC_USER_USERNAME = env.str("LDAP_SYNC_USER_USERNAME", None) LDAP_SYNC_USER_PASSWORD = env.str("LDAP_SYNC_USER_PASSWORD", None) - DJOSER["LOGIN_FIELD"] = "username" SEGMENT_CONDITION_VALUE_LIMIT = env.int("SEGMENT_CONDITION_VALUE_LIMIT", default=1000) if not 0 <= SEGMENT_CONDITION_VALUE_LIMIT < 2000000: diff --git a/api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py b/api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py index 010a861f30ab..a61eca619fbc 100644 --- a/api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py +++ b/api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py @@ -1,5 +1,9 @@ +import typing + import pytest +from django.contrib.auth.models import AbstractUser from django.test import RequestFactory +from djoser.serializers import TokenCreateSerializer from pytest_django.fixtures import SettingsWrapper from rest_framework.exceptions import PermissionDenied @@ -145,3 +149,46 @@ def test_invite_link_validation_mixin_validate_fails_if_invite_link_hash_not_val # Then assert exc_info.value.detail == USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE + + +@pytest.fixture +def django_ldap_username_field( + django_user_model: type[AbstractUser], +) -> typing.Generator[str, None, None]: + ldap_username_field = "username" + username_field = django_user_model.USERNAME_FIELD + django_user_model.USERNAME_FIELD = ldap_username_field + yield ldap_username_field + django_user_model.USERNAME_FIELD = username_field + + +# Previously, Djoser's default `TokenCreateSerializer` only respected the +# Djoser `LOGIN_FIELD` setting so it was impossible to grab the email from +# serializer and send it as a username to the login backend — which is required for LDAP to work. +# After Djoser 2.3.0, it's possible to alter `TokenCreateSerializer` behaviour +# to support this by setting the Django user model's `USERNAME_FIELD` constant +# to `"username"`, and Djoser's `LOGIN_FIELD` to `"email"`. +# This test is here to make sure Djoser behaves as expected. +def test_djoser_token_create_serializer__user_model_username_field__call_expected( + mocker: MockerFixture, + django_ldap_username_field: str, +) -> None: + # Given + expected_username = "some_username" + expected_password = "some_password" + + mocked_authenticate = mocker.patch("djoser.serializers.authenticate") + serializer = TokenCreateSerializer( + data={"email": expected_username, "password": expected_password} + ) + expected_authenticate_kwargs = { + "request": None, + django_ldap_username_field: expected_username, + "password": expected_password, + } + + # When + serializer.is_valid(raise_exception=True) + + # Then + mocked_authenticate.assert_called_with(**expected_authenticate_kwargs) diff --git a/api/users/models.py b/api/users/models.py index 2030ad05c3f7..2f0487a4592d 100644 --- a/api/users/models.py +++ b/api/users/models.py @@ -113,7 +113,7 @@ class FFAdminUser(LifecycleModel, AbstractUser): uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) - USERNAME_FIELD = "email" + USERNAME_FIELD = "username" if settings.LDAP_ENABLED else "email" REQUIRED_FIELDS = ["first_name", "last_name", "sign_up_type"] class Meta: