Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ldap-login): create custom serializer to fix login field #4535

Merged
merged 3 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
)
Loading