Skip to content

Commit

Permalink
fix(ldap-login): create custom serializer to fix login field
Browse files Browse the repository at this point in the history
  • Loading branch information
gagantrivedi committed Aug 23, 2024
1 parent 5e02eb4 commit 0e279de
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 3 deletions.
5 changes: 4 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 @@ -1125,13 +1126,14 @@
"GITHUB_APP_URL",
default=None,
)
LOGIN_FIELD = "email"

# LDAP setting
LDAP_INSTALLED = importlib.util.find_spec("flagsmith_ldap")
# 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 +1206,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)
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
20 changes: 19 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 django.contrib.auth import authenticate
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,23 @@
)


class CustomTokenCreateSerializer(TokenCreateSerializer):
def validate(self, attrs):
password = attrs.get("password")
# 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 map the provided email to the corresponding login
# field (which is configurable in settings).
params = {settings.LOGIN_FIELD: attrs.get(FFAdminUser.USERNAME_FIELD)}
self.user = authenticate(
request=self.context.get("request"), **params, password=password
)
if self.user and self.user.is_active:
return attrs
self.fail("invalid_credentials")


class CustomTokenSerializer(serializers.ModelSerializer):
class Meta:
model = Token
Expand Down
26 changes: 25 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,16 @@
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 +149,23 @@ 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
settings.LOGIN_FIELD = "username"

mocked_authenticate = mocker.patch("custom_auth.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 0e279de

Please sign in to comment.