Skip to content

Commit

Permalink
fix(ldap-login): create custom serializer to fix login field (#4535)
Browse files Browse the repository at this point in the history
Co-authored-by: Matthew Elwell <[email protected]>
  • Loading branch information
gagantrivedi and matthewelwell authored Aug 27, 2024
1 parent 25f0ca2 commit a704c7c
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 3 deletions.
4 changes: 3 additions & 1 deletion api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@
"SEND_CONFIRMATION_EMAIL": False,
"SERIALIZERS": {
"token": "custom_auth.serializers.CustomTokenSerializer",
"token_create": "custom_auth.serializers.CustomTokenCreateSerializer",
"user_create": "custom_auth.serializers.CustomUserCreateSerializer",
"user_delete": "custom_auth.serializers.CustomUserDelete",
"current_user": "users.serializers.CustomCurrentUserSerializer",
Expand Down Expand Up @@ -1131,7 +1132,7 @@
# The URL of the LDAP server.
LDAP_AUTH_URL = env.str("LDAP_AUTH_URL", None)

if LDAP_INSTALLED and LDAP_AUTH_URL:
if LDAP_INSTALLED and LDAP_AUTH_URL: # pragma: no cover
AUTHENTICATION_BACKENDS.insert(0, "django_python3_ldap.auth.LDAPBackend")
INSTALLED_APPS.append("flagsmith_ldap")

Expand Down Expand Up @@ -1204,6 +1205,7 @@
# 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:
Expand Down
30 changes: 29 additions & 1 deletion api/custom_auth/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.conf import settings
from djoser.serializers import UserCreateSerializer
from djoser.conf import settings as djoser_settings
from djoser.serializers import TokenCreateSerializer, UserCreateSerializer
from rest_framework import serializers
from rest_framework.authtoken.models import Token
from rest_framework.exceptions import PermissionDenied
Expand All @@ -17,6 +18,33 @@
)


class CustomTokenCreateSerializer(TokenCreateSerializer):
"""
NOTE: Some authentication backends (e.g., LDAP) support only
username and password authentication. However, the front-end
currently sends the email as the login key. To accommodate
this, we override the serializer to rename the username field
to the email (or any other field configurable using djoser settings) field.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if djoser_settings.LOGIN_FIELD != FFAdminUser.USERNAME_FIELD:
# Because djoser have created a field named username(djoser_settings.LOGIN_FIELD) in the serializer
# We have to remove this and add the email(FFAdminUser.USERNAME_FIELD) field back
self.fields.pop(djoser_settings.LOGIN_FIELD)
self.fields[FFAdminUser.USERNAME_FIELD] = serializers.CharField(
required=False
)

def validate(self, attrs):
if djoser_settings.LOGIN_FIELD != FFAdminUser.USERNAME_FIELD:
attrs[djoser_settings.LOGIN_FIELD] = attrs.pop(FFAdminUser.USERNAME_FIELD)

return super().validate(attrs)


class CustomTokenSerializer(serializers.ModelSerializer):
class Meta:
model = Token
Expand Down
30 changes: 29 additions & 1 deletion api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from copy import deepcopy

import pytest
from django.test import RequestFactory
from pytest_django.fixtures import SettingsWrapper
from pytest_mock import MockerFixture
from rest_framework.exceptions import PermissionDenied

from custom_auth.constants import (
USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE,
)
from custom_auth.serializers import CustomUserCreateSerializer
from custom_auth.serializers import (
CustomTokenCreateSerializer,
CustomUserCreateSerializer,
)
from organisations.invites.models import InviteLink
from users.models import FFAdminUser, SignUpType

Expand Down Expand Up @@ -145,3 +151,25 @@ 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


def test_CustomTokenCreateSerializer_validate_uses_login_field_to_authenticate(
settings: SettingsWrapper, mocker: MockerFixture
) -> None:
# Given
djoser_settings = deepcopy(settings.DJOSER)
djoser_settings["LOGIN_FIELD"] = "username"
settings.DJOSER = djoser_settings

mocked_authenticate = mocker.patch("djoser.serializers.authenticate")
serializer = CustomTokenCreateSerializer(
data={"email": "some_username", "password": "some_password"}
)

# When
serializer.is_valid(raise_exception=True)

# Then
mocked_authenticate.assert_called_with(
request=None, username="some_username", password="some_password"
)

0 comments on commit a704c7c

Please sign in to comment.