Skip to content

Commit 90d3ca3

Browse files
committed
fix(ldap-login): create custom serializer to fix login field
1 parent 5e02eb4 commit 90d3ca3

File tree

3 files changed

+51
-2
lines changed

3 files changed

+51
-2
lines changed

api/app/settings/common.py

+3
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,7 @@
808808
"SEND_CONFIRMATION_EMAIL": False,
809809
"SERIALIZERS": {
810810
"token": "custom_auth.serializers.CustomTokenSerializer",
811+
"token_create": "custom_auth.serializers.CustomTokenCreateSerializer",
811812
"user_create": "custom_auth.serializers.CustomUserCreateSerializer",
812813
"user_delete": "custom_auth.serializers.CustomUserDelete",
813814
"current_user": "users.serializers.CustomCurrentUserSerializer",
@@ -1125,6 +1126,7 @@
11251126
"GITHUB_APP_URL",
11261127
default=None,
11271128
)
1129+
LOGIN_FIELD = "email"
11281130

11291131
# LDAP setting
11301132
LDAP_INSTALLED = importlib.util.find_spec("flagsmith_ldap")
@@ -1204,6 +1206,7 @@
12041206
# The LDAP user username and password used by `sync_ldap_users_and_groups` command
12051207
LDAP_SYNC_USER_USERNAME = env.str("LDAP_SYNC_USER_USERNAME", None)
12061208
LDAP_SYNC_USER_PASSWORD = env.str("LDAP_SYNC_USER_PASSWORD", None)
1209+
LOGIN_FIELD = "username"
12071210

12081211
SEGMENT_CONDITION_VALUE_LIMIT = env.int("SEGMENT_CONDITION_VALUE_LIMIT", default=1000)
12091212
if not 0 <= SEGMENT_CONDITION_VALUE_LIMIT < 2000000:

api/custom_auth/serializers.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.conf import settings
2-
from djoser.serializers import UserCreateSerializer
2+
from django.contrib.auth import authenticate
3+
from djoser.serializers import TokenCreateSerializer, UserCreateSerializer
34
from rest_framework import serializers
45
from rest_framework.authtoken.models import Token
56
from rest_framework.exceptions import PermissionDenied
@@ -17,6 +18,27 @@
1718
)
1819

1920

21+
class CustomTokenCreateSerializer(TokenCreateSerializer):
22+
def validate(self, attrs):
23+
password = attrs.get("password")
24+
# NOTE: Some authentication backends (e.g., LDAP) support only
25+
# username and password authentication. However, the front-end
26+
# currently sends the email as the login key. To accommodate
27+
# this, we map the provided email to the corresponding login
28+
# field (which is configurable in settings).
29+
params = {settings.LOGIN_FIELD: attrs.get(FFAdminUser.USERNAME_FIELD)}
30+
self.user = authenticate(
31+
request=self.context.get("request"), **params, password=password
32+
)
33+
if not self.user:
34+
self.user = FFAdminUser.objects.filter(**params).first()
35+
if self.user and not self.user.check_password(password):
36+
self.fail("invalid_credentials")
37+
if self.user and self.user.is_active:
38+
return attrs
39+
self.fail("invalid_credentials")
40+
41+
2042
class CustomTokenSerializer(serializers.ModelSerializer):
2143
class Meta:
2244
model = Token

api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import pytest
22
from django.test import RequestFactory
33
from pytest_django.fixtures import SettingsWrapper
4+
from pytest_mock import MockerFixture
45
from rest_framework.exceptions import PermissionDenied
56

67
from custom_auth.constants import (
78
USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE,
89
)
9-
from custom_auth.serializers import CustomUserCreateSerializer
10+
from custom_auth.serializers import (
11+
CustomTokenCreateSerializer,
12+
CustomUserCreateSerializer,
13+
)
1014
from organisations.invites.models import InviteLink
1115
from users.models import FFAdminUser, SignUpType
1216

@@ -145,3 +149,23 @@ def test_invite_link_validation_mixin_validate_fails_if_invite_link_hash_not_val
145149

146150
# Then
147151
assert exc_info.value.detail == USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE
152+
153+
154+
def test_CustomTokenCreateSerializer_validate_uses_login_field_to_authenticate(
155+
settings: SettingsWrapper, mocker: MockerFixture
156+
) -> None:
157+
# Given
158+
settings.LOGIN_FIELD = "username"
159+
160+
mocked_authenticate = mocker.patch("custom_auth.serializers.authenticate")
161+
serializer = CustomTokenCreateSerializer(
162+
data={"email": "some_username", "password": "some_password"}
163+
)
164+
165+
# When
166+
serializer.is_valid(raise_exception=True)
167+
168+
# Then
169+
mocked_authenticate.assert_called_with(
170+
request=None, username="some_username", password="some_password"
171+
)

0 commit comments

Comments
 (0)